diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java index ba8fa4540d..b27e20e8b7 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java @@ -57,27 +57,30 @@ final public class CorrelationAttributeNormalizer { throw new CorrelationAttributeNormalizationException("Data was null."); } + String trimmedData = data.trim(); + switch (attributeType.getId()) { case CorrelationAttributeInstance.FILES_TYPE_ID: - return normalizeMd5(data); + return normalizeMd5(trimmedData); case CorrelationAttributeInstance.DOMAIN_TYPE_ID: - return normalizeDomain(data); + return normalizeDomain(trimmedData); case CorrelationAttributeInstance.EMAIL_TYPE_ID: - return normalizeEmail(data); + return normalizeEmail(trimmedData); case CorrelationAttributeInstance.PHONE_TYPE_ID: - return normalizePhone(data); + return normalizePhone(trimmedData); case CorrelationAttributeInstance.USBID_TYPE_ID: - return normalizeUsbId(data); + return normalizeUsbId(trimmedData); case CorrelationAttributeInstance.SSID_TYPE_ID: - return verifySsid(data); + return verifySsid(trimmedData); case CorrelationAttributeInstance.MAC_TYPE_ID: - return normalizeMac(data); + return normalizeMac(trimmedData); case CorrelationAttributeInstance.IMEI_TYPE_ID: - return normalizeImei(data); + return normalizeImei(trimmedData); case CorrelationAttributeInstance.IMSI_TYPE_ID: - return normalizeImsi(data); + return normalizeImsi(trimmedData); case CorrelationAttributeInstance.ICCID_TYPE_ID: - return normalizeIccid(data); + return normalizeIccid(trimmedData); + default: final String errorMessage = String.format( "Validator function not found for attribute type: %s", diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java index 2f2a35ab2a..549695432a 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java @@ -60,6 +60,7 @@ public class IngestEventsListener { final Collection recentlyAddedCeArtifacts = new LinkedHashSet<>(); private static int correlationModuleInstanceCount; private static boolean flagNotableItems; + private static boolean flagSeenDevices; private final ExecutorService jobProcessingExecutor; private static final String INGEST_EVENT_THREAD_NAME = "Ingest-Event-Listener-%d"; private final PropertyChangeListener pcl1 = new IngestModuleEventListener(); @@ -134,6 +135,15 @@ public class IngestEventsListener { return flagNotableItems; } + /** + * Are previously seen devices being flagged? + * + * @return True if flagging seen devices; otherwise false. + */ + public synchronized static boolean isFlagSeenDevices() { + return flagSeenDevices; + } + /** * Configure the listener to flag notable items or not. * @@ -143,6 +153,15 @@ public class IngestEventsListener { flagNotableItems = value; } + /** + * Configure the listener to flag previously seen devices or not. + * + * @param value True to flag seen devices; otherwise false. + */ + public synchronized static void setFlagSeenDevices(boolean value) { + flagSeenDevices = value; + } + @NbBundle.Messages({"IngestEventsListener.prevTaggedSet.text=Previously Tagged As Notable (Central Repository)", "IngestEventsListener.prevCaseComment.text=Previous Case: ", "IngestEventsListener.ingestmodule.name=Correlation Engine"}) @@ -179,6 +198,46 @@ public class IngestEventsListener { } } + /** + * Create an Interesting Aritfact hit for a device which was previously seen + * in the central repository. + * + * @param bbArtifact the artifact to create the interesting item for + */ + @NbBundle.Messages({"IngestEventsListener.prevExists.text=Previously Seen Devices (Central Repository)", + "# {0} - typeName", + "# {1} - count", + "IngestEventsListener.prevCount.text=Number of previous {0}: {1}"}) + static private void postCorrelatedPreviousArtifactToBlackboard(BlackboardArtifact bbArtifact) { + + try { + AbstractFile af = bbArtifact.getSleuthkitCase().getAbstractFileById(bbArtifact.getObjectID()); + Collection attributes = new ArrayList<>(); + String MODULE_NAME = Bundle.IngestEventsListener_ingestmodule_name(); + BlackboardArtifact tifArtifact = af.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT); + BlackboardAttribute att = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME, MODULE_NAME, + Bundle.IngestEventsListener_prevExists_text()); + attributes.add(att); + attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT, MODULE_NAME, bbArtifact.getArtifactID())); + + tifArtifact.addAttributes(attributes); + try { + // index the artifact for keyword search + Blackboard blackboard = Case.getCurrentCaseThrows().getServices().getBlackboard(); + blackboard.indexArtifact(tifArtifact); + } catch (Blackboard.BlackboardException | NoCurrentCaseException ex) { + LOGGER.log(Level.SEVERE, "Unable to index blackboard artifact " + tifArtifact.getArtifactID(), ex); //NON-NLS + } + + // fire event to notify UI of this new artifact + IngestServices.getInstance().fireModuleDataEvent(new ModuleDataEvent(MODULE_NAME, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT)); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Failed to create BlackboardArtifact.", ex); // NON-NLS + } catch (IllegalStateException ex) { + LOGGER.log(Level.SEVERE, "Failed to create BlackboardAttribute.", ex); // NON-NLS + } + } + private class IngestModuleEventListener implements PropertyChangeListener { @Override @@ -198,7 +257,8 @@ public class IngestEventsListener { case DATA_ADDED: { //if ingest isn't running create the interesting items otherwise use the ingest module setting to determine if we create interesting items boolean flagNotable = !IngestManager.getInstance().isIngestRunning() || isFlagNotableItems(); - jobProcessingExecutor.submit(new DataAddedTask(dbManager, evt, flagNotable)); + boolean flagPrevious = !IngestManager.getInstance().isIngestRunning() || isFlagSeenDevices(); + jobProcessingExecutor.submit(new DataAddedTask(dbManager, evt, flagNotable, flagPrevious)); break; } } @@ -237,11 +297,13 @@ public class IngestEventsListener { private final EamDb dbManager; private final PropertyChangeEvent event; private final boolean flagNotableItemsEnabled; + private final boolean flagPreviousItemsEnabled; - private DataAddedTask(EamDb db, PropertyChangeEvent evt, boolean flagNotableItemsEnabled) { + private DataAddedTask(EamDb db, PropertyChangeEvent evt, boolean flagNotableItemsEnabled, boolean flagPreviousItemsEnabled) { dbManager = db; event = evt; this.flagNotableItemsEnabled = flagNotableItemsEnabled; + this.flagPreviousItemsEnabled = flagPreviousItemsEnabled; } @Override @@ -279,6 +341,21 @@ public class IngestEventsListener { LOGGER.log(Level.INFO, String.format("Unable to flag notable item: %s.", eamArtifact.toString()), ex); } } + if (flagPreviousItemsEnabled + && (eamArtifact.getCorrelationType().getId() == CorrelationAttributeInstance.USBID_TYPE_ID + || eamArtifact.getCorrelationType().getId() == CorrelationAttributeInstance.ICCID_TYPE_ID + || eamArtifact.getCorrelationType().getId() == CorrelationAttributeInstance.IMEI_TYPE_ID + || eamArtifact.getCorrelationType().getId() == CorrelationAttributeInstance.IMSI_TYPE_ID + || eamArtifact.getCorrelationType().getId() == CorrelationAttributeInstance.MAC_TYPE_ID)) { + try { + Long countPreviousOccurences = dbManager.getCountArtifactInstancesByTypeValue(eamArtifact.getCorrelationType(), eamArtifact.getCorrelationValue()); + if (countPreviousOccurences > 0) { + postCorrelatedPreviousArtifactToBlackboard(bbArtifact); + } + } catch (CorrelationAttributeNormalizationException ex) { + LOGGER.log(Level.INFO, String.format("Unable to flag notable item: %s.", eamArtifact.toString()), ex); + } + } eamArtifacts.add(eamArtifact); } } catch (EamDbException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/Bundle.properties index a525713f7c..f99db1edb6 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/Bundle.properties @@ -1,2 +1,3 @@ IngestSettingsPanel.ingestSettingsLabel.text=Ingest Settings IngestSettingsPanel.flagTaggedNotableItemsCheckbox.text=Flag items previously tagged as notable +IngestSettingsPanel.flagPreviouslySeenDevicesCheckbox.text=Flag previously seen devices diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestModule.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestModule.java index 0eecc533b0..8073f8e8d7 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestModule.java @@ -61,6 +61,7 @@ import org.sleuthkit.autopsy.healthmonitor.TimingMetric; final class IngestModule implements FileIngestModule { static final boolean DEFAULT_FLAG_TAGGED_NOTABLE_ITEMS = true; + static final boolean DEFAULT_FLAG_PREVIOUS_DEVICES = true; private final static Logger logger = Logger.getLogger(IngestModule.class.getName()); private final IngestServices services = IngestServices.getInstance(); @@ -72,6 +73,7 @@ final class IngestModule implements FileIngestModule { private Blackboard blackboard; private CorrelationAttributeInstance.Type filesType; private final boolean flagTaggedNotableItems; + private final boolean flagPreviouslySeenDevices; /** * Instantiate the Correlation Engine ingest module. @@ -80,6 +82,7 @@ final class IngestModule implements FileIngestModule { */ IngestModule(IngestSettings settings) { flagTaggedNotableItems = settings.isFlagTaggedNotableItems(); + flagPreviouslySeenDevices = settings.isFlagPreviousDevices(); } @Override @@ -228,6 +231,9 @@ final class IngestModule implements FileIngestModule { if (IngestEventsListener.getCeModuleInstanceCount() == 1 || !IngestEventsListener.isFlagNotableItems()) { IngestEventsListener.setFlagNotableItems(flagTaggedNotableItems); } + if (IngestEventsListener.getCeModuleInstanceCount() == 1 || !IngestEventsListener.isFlagSeenDevices()) { + IngestEventsListener.setFlagSeenDevices(flagPreviouslySeenDevices); + } if (EamDb.isEnabled() == false) { /* diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettings.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettings.java index 32ab9e9f2d..5a0580adf4 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettings.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettings.java @@ -28,21 +28,25 @@ final class IngestSettings implements IngestModuleIngestJobSettings { private static final long serialVersionUID = 1L; private boolean flagTaggedNotableItems; + private boolean flagPreviousDevices; /** * Instantiate the ingest job settings with default values. */ IngestSettings() { this.flagTaggedNotableItems = IngestModule.DEFAULT_FLAG_TAGGED_NOTABLE_ITEMS; + this.flagPreviousDevices = IngestModule.DEFAULT_FLAG_PREVIOUS_DEVICES; } /** * Instantiate the ingest job settings. * * @param flagTaggedNotableItems Flag previously tagged notable items. + * @param flagPreviousDevices Flag devices which exist already in the Central Repository */ - IngestSettings(boolean flagTaggedNotableItems) { + IngestSettings(boolean flagTaggedNotableItems, boolean flagPreviousDevices) { this.flagTaggedNotableItems = flagTaggedNotableItems; + this.flagPreviousDevices = flagPreviousDevices; } @Override @@ -60,12 +64,11 @@ final class IngestSettings implements IngestModuleIngestJobSettings { } /** - * Flag or ignore previously identified notable items. + * Are previously seen devices to be flagged? * - * @param ignorePreviousNotableItems Are previously tagged notable items to - * be flagged? + * @return True if flagging; otherwise false. */ - void setFlagTaggedNotableItems(boolean flagTaggedNotableItems) { - this.flagTaggedNotableItems = flagTaggedNotableItems; + boolean isFlagPreviousDevices() { + return flagPreviousDevices; } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.form index 564031cb72..3c2fddca0f 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.form @@ -19,11 +19,14 @@ - - - - + + + + + + + @@ -36,7 +39,9 @@ - + + + @@ -59,5 +64,12 @@ + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java index ed36c71287..159f925355 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java @@ -42,11 +42,12 @@ final class IngestSettingsPanel extends IngestModuleIngestJobSettingsPanel { */ private void customizeComponents(IngestSettings settings) { flagTaggedNotableItemsCheckbox.setSelected(settings.isFlagTaggedNotableItems()); + flagPreviouslySeenDevicesCheckbox.setSelected(settings.isFlagPreviousDevices()); } @Override public IngestModuleIngestJobSettings getSettings() { - return new IngestSettings(flagTaggedNotableItemsCheckbox.isSelected()); + return new IngestSettings(flagTaggedNotableItemsCheckbox.isSelected(), flagPreviouslySeenDevicesCheckbox.isSelected()); } /** @@ -60,12 +61,15 @@ final class IngestSettingsPanel extends IngestModuleIngestJobSettingsPanel { ingestSettingsLabel = new javax.swing.JLabel(); flagTaggedNotableItemsCheckbox = new javax.swing.JCheckBox(); + flagPreviouslySeenDevicesCheckbox = new javax.swing.JCheckBox(); ingestSettingsLabel.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(ingestSettingsLabel, org.openide.util.NbBundle.getMessage(IngestSettingsPanel.class, "IngestSettingsPanel.ingestSettingsLabel.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(flagTaggedNotableItemsCheckbox, org.openide.util.NbBundle.getMessage(IngestSettingsPanel.class, "IngestSettingsPanel.flagTaggedNotableItemsCheckbox.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(flagPreviouslySeenDevicesCheckbox, org.openide.util.NbBundle.getMessage(IngestSettingsPanel.class, "IngestSettingsPanel.flagPreviouslySeenDevicesCheckbox.text")); // NOI18N + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -73,10 +77,12 @@ final class IngestSettingsPanel extends IngestModuleIngestJobSettingsPanel { .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(ingestSettingsLabel) .addGroup(layout.createSequentialGroup() .addGap(10, 10, 10) - .addComponent(flagTaggedNotableItemsCheckbox)) - .addComponent(ingestSettingsLabel)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(flagPreviouslySeenDevicesCheckbox) + .addComponent(flagTaggedNotableItemsCheckbox)))) .addContainerGap(65, Short.MAX_VALUE)) ); layout.setVerticalGroup( @@ -86,11 +92,14 @@ final class IngestSettingsPanel extends IngestModuleIngestJobSettingsPanel { .addComponent(ingestSettingsLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(flagTaggedNotableItemsCheckbox) - .addContainerGap(245, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(flagPreviouslySeenDevicesCheckbox) + .addContainerGap(222, Short.MAX_VALUE)) ); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JCheckBox flagPreviouslySeenDevicesCheckbox; private javax.swing.JCheckBox flagTaggedNotableItemsCheckbox; private javax.swing.JLabel ingestSettingsLabel; // End of variables declaration//GEN-END:variables diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index 7269a09e2c..5604f328d6 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -322,11 +322,10 @@ public class BlackboardArtifactNode extends AbstractContentNode(NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactType.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactType.displayName"), NO_DESCR, - associatedArtifact.getDisplayName() + " " + NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.artifact.displayName"))); + associatedArtifact.getDisplayName())); sheetSet.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactDetails.name"), NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.artifactDetails.displayName"), NO_DESCR, diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java index 8ff6645562..3262087487 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java @@ -411,11 +411,15 @@ public class HashsetHits implements AutopsyVisitableItem { BlackboardArtifact art = skCase.getBlackboardArtifact(id); artifactHits.put(id, art); } - list.add(id); } catch (TskException ex) { logger.log(Level.SEVERE, "TSK Exception occurred", ex); //NON-NLS } }); + + // Adding all keys at once is more efficient than adding one at a + // time because Netbeans triggers internal processing each time an + // element is added to the list. + list.addAll(artifactHits.keySet()); return true; } diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSet.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSet.java index 975dac7dcd..87d25453e7 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSet.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSet.java @@ -824,6 +824,18 @@ public final class FilesSet implements Serializable { private static final long serialVersionUID = 1L; + /** + * Construct a case-insensitive file name extension condition. + * + * @param extension The file name extension to be matched. + */ + public ExtensionCondition(String extension) { + // If there is a leading ".", strip it since + // AbstractFile.getFileNameExtension() returns just the + // extension chars and not the dot. + super(extension.startsWith(".") ? extension.substring(1) : extension, false); + } + /** * Construct a case-insensitive file name extension condition. * diff --git a/Core/src/org/sleuthkit/autopsy/othercasessearch/Bundle.properties b/Core/src/org/sleuthkit/autopsy/othercasessearch/Bundle.properties index 17ea13ee0b..95951884a2 100755 --- a/Core/src/org/sleuthkit/autopsy/othercasessearch/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/othercasessearch/Bundle.properties @@ -4,7 +4,7 @@ OtherCasesSearchDialog.searchButton.AccessibleContext.accessibleName=Search OtherCasesSearchDialog.searchButton.text=Search OtherCasesSearchDialog.correlationValueTextField.text= OtherCasesSearchDialog.correlationValueLabel.text=Correlation Property Value: -OtherCasesSearchDialog.descriptionLabel.text=Search data in the Central Repository from other cases. +OtherCasesSearchDialog.descriptionLabel.text=Search the Central Repository for correlation properties with a specified value. The search is case insensitive. OtherCasesSearchDialog.errorLabel.text=\ OtherCasesSearchDialog.correlationTypeLabel.text=Correlation Property Type: OtherCasesSearchDialog.casesLabel.text=\ diff --git a/Core/src/org/sleuthkit/autopsy/othercasessearch/OtherCasesSearchDialog.form b/Core/src/org/sleuthkit/autopsy/othercasessearch/OtherCasesSearchDialog.form index 00eb862b4a..cd033f994d 100755 --- a/Core/src/org/sleuthkit/autopsy/othercasessearch/OtherCasesSearchDialog.form +++ b/Core/src/org/sleuthkit/autopsy/othercasessearch/OtherCasesSearchDialog.form @@ -24,18 +24,10 @@ - + - - - - - - - - - + @@ -43,11 +35,16 @@ - + + + + + + @@ -57,25 +54,25 @@ - + - + - + - + - + @@ -149,7 +146,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/othercasessearch/OtherCasesSearchDialog.java b/Core/src/org/sleuthkit/autopsy/othercasessearch/OtherCasesSearchDialog.java index 51e21631d2..6fe9ec8ce3 100755 --- a/Core/src/org/sleuthkit/autopsy/othercasessearch/OtherCasesSearchDialog.java +++ b/Core/src/org/sleuthkit/autopsy/othercasessearch/OtherCasesSearchDialog.java @@ -26,19 +26,17 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.logging.Level; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.swing.JFrame; import javax.swing.SwingWorker; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.openide.nodes.Node; -import org.openide.util.Exceptions; import org.openide.util.NbBundle.Messages; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizer; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; @@ -55,6 +53,10 @@ import org.sleuthkit.autopsy.datamodel.EmptyNode; "OtherCasesSearchDialog.resultsDescription.text=Other Cases Search", "OtherCasesSearchDialog.emptyNode.text=No results found.", "OtherCasesSearchDialog.validation.invalidHash=The supplied value is not a valid MD5 hash.", + "OtherCasesSearchDialog.validation.invalidEmail=The supplied value is not a valid e-mail address.", + "OtherCasesSearchDialog.validation.invalidDomain=The supplied value is not a valid domain.", + "OtherCasesSearchDialog.validation.invalidPhone=The supplied value is not a valid phone number.", + "OtherCasesSearchDialog.validation.genericMessage=The supplied value is not valid.", "# {0} - number of cases", "OtherCasesSearchDialog.caseLabel.text=The current Central Repository contains {0} case(s)." }) @@ -67,9 +69,8 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { private static final Logger logger = Logger.getLogger(OtherCasesSearchDialog.class.getName()); private static final long serialVersionUID = 1L; - private static final String FILES_CORRELATION_TYPE = "Files"; - private final List correlationTypes; + private CorrelationAttributeInstance.Type selectedCorrelationType; private TextPrompt correlationValueTextFieldPrompt; /** @@ -84,23 +85,19 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { /** * Perform the other cases search. + * + * @param type The correlation type. + * @param value The value to be matched. */ - private void search() { + private void search(CorrelationAttributeInstance.Type type, String value) { new SwingWorker, Void>() { @Override protected List doInBackground() { - List correlationTypes; List correlationInstances = new ArrayList<>(); try { - correlationTypes = EamDb.getInstance().getDefinedCorrelationTypes(); - for (CorrelationAttributeInstance.Type type : correlationTypes) { - if (type.getDisplayName().equals((String) correlationTypeComboBox.getSelectedItem())) { - correlationInstances = EamDb.getInstance().getArtifactInstancesByTypeValue(type, correlationValueTextField.getText()); - break; - } - } + correlationInstances = EamDb.getInstance().getArtifactInstancesByTypeValue(type, value); } catch (EamDbException ex) { logger.log(Level.SEVERE, "Unable to connect to the Central Repository database.", ex); } catch (CorrelationAttributeNormalizationException ex) { @@ -123,9 +120,7 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { TableFilterNode tableFilterNode = new TableFilterNode(searchNode, true, searchNode.getName()); String resultsText = String.format("%s (%s; \"%s\")", - Bundle.OtherCasesSearchDialog_resultsTitle_text(), - (String) correlationTypeComboBox.getSelectedItem(), - correlationValueTextField.getText()); + Bundle.OtherCasesSearchDialog_resultsTitle_text(), type.getDisplayName(), value); final TopComponent searchResultWin; if (correlationInstances.isEmpty()) { Node emptyNode = new TableFilterNode( @@ -183,7 +178,7 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { org.openide.awt.Mnemonics.setLocalizedText(descriptionLabel, org.openide.util.NbBundle.getMessage(OtherCasesSearchDialog.class, "OtherCasesSearchDialog.descriptionLabel.text")); // NOI18N - casesLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + casesLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); org.openide.awt.Mnemonics.setLocalizedText(casesLabel, org.openide.util.NbBundle.getMessage(OtherCasesSearchDialog.class, "OtherCasesSearchDialog.casesLabel.text")); // NOI18N javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); @@ -193,44 +188,42 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addComponent(casesLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(18, 18, 18) - .addComponent(searchButton)) - .addGroup(layout.createSequentialGroup() - .addComponent(descriptionLabel) - .addGap(0, 0, Short.MAX_VALUE)) + .addComponent(descriptionLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 430, Short.MAX_VALUE) .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(correlationValueLabel) .addComponent(correlationTypeLabel)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(correlationTypeComboBox, 0, 289, Short.MAX_VALUE) + .addComponent(correlationTypeComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(correlationValueTextField) - .addComponent(errorLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) + .addComponent(errorLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addComponent(casesLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(18, 18, 18) + .addComponent(searchButton))) .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addContainerGap() - .addComponent(descriptionLabel) + .addComponent(descriptionLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 39, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(18, 18, 18) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(correlationTypeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(correlationTypeLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGap(15, 15, 15) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(correlationValueLabel) - .addComponent(correlationValueTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(correlationValueTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(correlationValueLabel)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(errorLabel) - .addGap(11, 11, 11) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 20, Short.MAX_VALUE) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(searchButton) .addComponent(casesLabel)) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) ); searchButton.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(OtherCasesSearchDialog.class, "OtherCasesSearchDialog.searchButton.AccessibleContext.accessibleName")); // NOI18N @@ -240,23 +233,57 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { }// //GEN-END:initComponents private void searchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchButtonActionPerformed - if (validateInputs()) { - /* - * Just in case, we'll lock down the type and value components to - * avoid the possibly of a race condition. - */ - correlationTypeComboBox.setEnabled(false); - correlationValueTextField.setEnabled(false); - - search(); + CorrelationAttributeInstance.Type correlationType = selectedCorrelationType; + String correlationValue = correlationValueTextField.getText().trim(); + + if (validateInputs(correlationType, correlationValue)) { + search(correlationType, correlationValue); dispose(); } else { + String validationMessage; + switch (correlationType.getId()) { + case CorrelationAttributeInstance.FILES_TYPE_ID: + validationMessage = Bundle.OtherCasesSearchDialog_validation_invalidHash(); + break; + case CorrelationAttributeInstance.DOMAIN_TYPE_ID: + validationMessage = Bundle.OtherCasesSearchDialog_validation_invalidDomain(); + break; + case CorrelationAttributeInstance.EMAIL_TYPE_ID: + validationMessage = Bundle.OtherCasesSearchDialog_validation_invalidEmail(); + break; + case CorrelationAttributeInstance.PHONE_TYPE_ID: + validationMessage = Bundle.OtherCasesSearchDialog_validation_invalidPhone(); + break; + default: + validationMessage = Bundle.OtherCasesSearchDialog_validation_genericMessage(); + break; + + } + errorLabel.setText(validationMessage); searchButton.setEnabled(false); - errorLabel.setText(Bundle.OtherCasesSearchDialog_validation_invalidHash()); correlationValueTextField.grabFocus(); } }//GEN-LAST:event_searchButtonActionPerformed - + + /** + * Validate the supplied input. + * + * @param type The correlation type. + * @param value The value to be validated. + * + * @return True if the input is valid for the given type; otherwise false. + */ + private boolean validateInputs(CorrelationAttributeInstance.Type type, String value) { + try { + CorrelationAttributeNormalizer.normalize(type, value); + } catch (CorrelationAttributeNormalizationException ex) { + // No need to log this. + return false; + } + + return true; + } + /** * Further customize the components beyond the standard initialization. */ @@ -277,20 +304,21 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { } for (CorrelationAttributeInstance.Type type : correlationTypes) { - // We only support the "Files" type for now. - if (type.getDisplayName().equals(FILES_CORRELATION_TYPE)) { - correlationTypeComboBox.addItem(type.getDisplayName()); - } + correlationTypeComboBox.addItem(type.getDisplayName()); } correlationTypeComboBox.setSelectedIndex(0); correlationTypeComboBox.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { + updateSelectedType(); + updateCorrelationValueTextFieldPrompt(); updateSearchButton(); } }); + updateSelectedType(); + /* * Create listener for text input. */ @@ -315,7 +343,12 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { } @Messages({ - "OtherCasesSearchDialog.correlationValueTextField.filesExample=Example: \"f0e1d2c3b4a5968778695a4b3c2d1e0f\"" + "OtherCasesSearchDialog.correlationValueTextField.filesExample=Example: \"f0e1d2c3b4a5968778695a4b3c2d1e0f\"", + "OtherCasesSearchDialog.correlationValueTextField.domainExample=Example: \"domain.com\"", + "OtherCasesSearchDialog.correlationValueTextField.emailExample=Example: \"user@host.com\"", + "OtherCasesSearchDialog.correlationValueTextField.phoneExample=Example: \"(800)123-4567\"", + "OtherCasesSearchDialog.correlationValueTextField.usbExample=Example: \"4&1234567&0\"", + "OtherCasesSearchDialog.correlationValueTextField.ssidExample=Example: \"WirelessNetwork-5G\"" }) /** * Update the text prompt of the name text field based on the input type @@ -325,7 +358,30 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { /** * Add text prompt to the text field. */ - String text = Bundle.OtherCasesSearchDialog_correlationValueTextField_filesExample(); + String text; + switch(selectedCorrelationType.getId()) { + case CorrelationAttributeInstance.FILES_TYPE_ID: + text = Bundle.OtherCasesSearchDialog_correlationValueTextField_filesExample(); + break; + case CorrelationAttributeInstance.DOMAIN_TYPE_ID: + text = Bundle.OtherCasesSearchDialog_correlationValueTextField_domainExample(); + break; + case CorrelationAttributeInstance.EMAIL_TYPE_ID: + text = Bundle.OtherCasesSearchDialog_correlationValueTextField_emailExample(); + break; + case CorrelationAttributeInstance.PHONE_TYPE_ID: + text = Bundle.OtherCasesSearchDialog_correlationValueTextField_phoneExample(); + break; + case CorrelationAttributeInstance.USBID_TYPE_ID: + text = Bundle.OtherCasesSearchDialog_correlationValueTextField_usbExample(); + break; + case CorrelationAttributeInstance.SSID_TYPE_ID: + text = Bundle.OtherCasesSearchDialog_correlationValueTextField_ssidExample(); + break; + default: + text = ""; + break; + } correlationValueTextFieldPrompt = new TextPrompt(text, correlationValueTextField); /** @@ -338,6 +394,19 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { repaint(); } + /** + * Update the 'selectedCorrelationType' value to match the selected type + * from the combo-box. + */ + private void updateSelectedType() { + for (CorrelationAttributeInstance.Type type : correlationTypes) { + if (type.getDisplayName().equals((String) correlationTypeComboBox.getSelectedItem())) { + selectedCorrelationType = type; + break; + } + } + } + /** * Enable or disable the Search button depending on whether or not text has * been provided for the correlation property value. @@ -345,20 +414,6 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { private void updateSearchButton() { searchButton.setEnabled(correlationValueTextField.getText().isEmpty() == false); } - - /** - * Validate the value input. - * - * @return True if the input is valid for the selected type; otherwise false. - */ - private boolean validateInputs() { - Pattern md5Pattern = Pattern.compile("^[a-fA-F0-9]{32}$"); // NON-NLS - Matcher matcher = md5Pattern.matcher(correlationValueTextField.getText().trim()); - if (matcher.find()) { - return true; - } - return false; - } /** * Display the Search Other Cases dialog. @@ -378,4 +433,4 @@ final class OtherCasesSearchDialog extends javax.swing.JDialog { private javax.swing.JLabel errorLabel; private javax.swing.JButton searchButton; // End of variables declaration//GEN-END:variables -} +} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties index 8efbfd9178..92ac26cba2 100644 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties @@ -253,4 +253,8 @@ ReportGenerator.errList.coreExceptionWhileGenRptRow=Core exception while generat ReportKML.latLongStartPoint={0};{1};;{2} (Start)\n ReportKML.latLongEndPoint={0};{1};;{2} (End)\n ReportGenerationPanel.cancelButton.actionCommand=Cancel -ReportGenerationPanel.cancelButton.text=Cancel \ No newline at end of file +ReportGenerationPanel.cancelButton.text=Cancel +ReportHTMLConfigurationPanel.headerTextField.text= +ReportHTMLConfigurationPanel.footerTextField.text= +ReportHTMLConfigurationPanel.headerLabel.text=Header: +ReportHTMLConfigurationPanel.footerLabel.text=Footer: diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/report/Bundle_ja.properties index a877c52899..b4c2844989 100644 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle_ja.properties @@ -243,4 +243,4 @@ ReportVisualPanel1.invalidModuleWarning=\u7121\u52b9\u306a\u30ec\u30dd\u30fc\u30 ReportGenerationPanel.confDlg.cancelReport.msg=\u672c\u5f53\u306b\u30ec\u30dd\u30fc\u30c8\u3092\u30ad\u30e3\u30f3\u30bb\u30eb\u3057\u307e\u3059\u304b\uff1f ReportProgressPanel.complete.processLb2.text=\u5b8c\u4e86\u3057\u307e\u3057\u305f\u304c\u3001\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f ReportGenerationPanel.cancelButton.actionCommand=\u30ad\u30e3\u30f3\u30bb\u30eb -ReportGenerationPanel.cancelButton.text=\u30ad\u30e3\u30f3\u30bb\u30eb \ No newline at end of file +ReportGenerationPanel.cancelButton.text=\u30ad\u30e3\u30f3\u30bb\u30eb diff --git a/Core/src/org/sleuthkit/autopsy/report/GeneralReportModule.java b/Core/src/org/sleuthkit/autopsy/report/GeneralReportModule.java index 1048750b9d..da0541b48a 100644 --- a/Core/src/org/sleuthkit/autopsy/report/GeneralReportModule.java +++ b/Core/src/org/sleuthkit/autopsy/report/GeneralReportModule.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012 Basis Technology Corp. + * Copyright 2012-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,8 +18,6 @@ */ package org.sleuthkit.autopsy.report; -import javax.swing.JPanel; - public interface GeneralReportModule extends ReportModule { /** @@ -33,13 +31,4 @@ public interface GeneralReportModule extends ReportModule { */ public void generateReport(String baseReportDir, ReportProgressPanel progressPanel); - /** - * Returns the configuration panel for the report, which is displayed in the - * report configuration step of the report wizard. - * - * @return Configuration panel or null if the module does not need - * configuration. - */ - public JPanel getConfigurationPanel(); - } diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java b/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java index 13a5078658..0f4a568ba1 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java @@ -47,6 +47,8 @@ import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import javax.imageio.ImageIO; +import javax.swing.JPanel; +import org.apache.commons.lang3.StringEscapeUtils; import org.openide.filesystems.FileUtil; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; @@ -56,6 +58,7 @@ import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.EscapeUtil; import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.datamodel.ContentUtils.ExtractFscContentVisitor; import org.sleuthkit.autopsy.ingest.IngestManager; @@ -89,6 +92,8 @@ class ReportHTML implements TableReportModule { private String currentDataType; // name of current data type private Integer rowCount; // number of rows (aka artifacts or tags) for the current data type private Writer out; + + private ReportHTMLConfigurationPanel configPanel; private final ReportBranding reportBranding; @@ -104,6 +109,14 @@ class ReportHTML implements TableReportModule { private ReportHTML() { reportBranding = new ReportBranding(); } + + @Override + public JPanel getConfigurationPanel() { + if (configPanel == null) { + configPanel = new ReportHTMLConfigurationPanel(); + } + return configPanel; + } // Refesh the member variables private void refresh() throws NoCurrentCaseException { @@ -332,6 +345,10 @@ class ReportHTML implements TableReportModule { */ @Override public void startReport(String baseReportDir) { + // Save settings + ModuleSettings.setConfigSetting("HTMLReport", "header", configPanel.getHeader()); //NON-NLS + ModuleSettings.setConfigSetting("HTMLReport", "footer", configPanel.getFooter()); //NON-NLS + // Refresh the HTML report try { refresh(); @@ -392,12 +409,14 @@ class ReportHTML implements TableReportModule { try { StringBuilder page = new StringBuilder(); - page.append("\n\n\t").append(name).append("\n\t\n\n\n\n"); //NON-NLS - page.append("
").append(name).append("
\n
\n"); //NON-NLS + page.append("\n\n\t").append(name).append("\n\t\n\n\n\n") //NON-NLS + .append(writePageHeader()) + .append("
").append(name).append("
\n") + .append("
\n"); //NON-NLS if (!description.isEmpty()) { page.append("

"); //NON-NLS page.append(description); - page.append("

\n"); //NON-NLS + page.append("

\n"); //NON-NLS } out.write(page.toString()); currentDataType = name; @@ -415,7 +434,10 @@ class ReportHTML implements TableReportModule { public void endDataType() { dataTypes.put(currentDataType, rowCount); try { - out.write("
\n\n\n"); //NON-NLS + StringBuilder builder = new StringBuilder(); + builder.append(writePageFooter()); + builder.append("
\n\n\n"); //NON-NLS + out.write(builder.toString()); } catch (IOException ex) { logger.log(Level.SEVERE, "Failed to write end of HTML report.", ex); //NON-NLS } finally { @@ -430,6 +452,40 @@ class ReportHTML implements TableReportModule { } } } + + /** + * Write HTML-formatted page header text based on the text provided in the + * configuration panel. + * + * @return The HTML-formatted text. + */ + private String writePageHeader() { + StringBuilder output = new StringBuilder(); + String pageHeader = configPanel.getHeader(); + if (pageHeader.isEmpty() == false) { + output.append("
") + .append(StringEscapeUtils.escapeHtml4(pageHeader)) + .append("
\n"); //NON-NLS + } + return output.toString(); + } + + /** + * Write HTML-formatted page footer text based on the text provided in the + * configuration panel. + * + * @return The HTML-formatted text. + */ + private String writePageFooter() { + StringBuilder output = new StringBuilder(); + String pageFooter = configPanel.getFooter(); + if (pageFooter.isEmpty() == false) { + output.append("
") + .append(StringEscapeUtils.escapeHtml4(pageFooter)) + .append("
"); //NON-NLS + } + return output.toString(); + } /** * Start a new set under the current data type. @@ -860,6 +916,8 @@ class ReportHTML implements TableReportModule { + //NON-NLS "#header {width:100%; padding: 10px; line-height: 25px; background: #07A; color: #FFF; font-size: 20px;}\n" + //NON-NLS + "#pageHeaderFooter {width: 100%; padding: 10px; line-height: 25px; text-align: center; font-size: 20px;}\n" + + //NON-NLS "h1 {font-size: 20px; font-weight: normal; color: #07A; padding: 0 0 7px 0; margin-top: 25px; border-bottom: 1px solid #D6D6D6;}\n" + //NON-NLS "h2 {font-size: 20px; font-weight: bolder; color: #07A;}\n" @@ -1071,6 +1129,7 @@ class ReportHTML implements TableReportModule { NbBundle.getMessage(this.getClass(), "ReportHTML.writeSum.title")).append("\n"); //NON-NLS head.append("\n"); //NON-NLS head.append("