Moved the business logic out of the AnnotationsContentViewer into its own class

This commit is contained in:
Kelly Kelly 2021-05-17 15:57:49 -04:00
parent de54f2a8c5
commit b34ef22874
5 changed files with 779 additions and 651 deletions

View File

@ -21,21 +21,25 @@
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Component id="jScrollPane5" alignment="0" pref="907" max="32767" attributes="0"/>
<Component id="scrollPane" alignment="0" pref="907" max="32767" attributes="0"/>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Component id="jScrollPane5" alignment="1" pref="435" max="32767" attributes="0"/>
<Component id="scrollPane" alignment="1" pref="435" max="32767" attributes="0"/>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Container class="javax.swing.JScrollPane" name="jScrollPane5">
<Container class="javax.swing.JScrollPane" name="scrollPane">
<AuxValues>
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
</AuxValues>
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
<SubComponents>
<Component class="javax.swing.JTextPane" name="jTextPane1">
<Component class="javax.swing.JTextPane" name="textPanel">
<Properties>
<Property name="editable" type="boolean" value="false"/>
<Property name="name" type="java.lang.String" value="" noResource="true"/>

View File

@ -19,43 +19,23 @@
package org.sleuthkit.autopsy.contentviewers;
import java.awt.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.swing.JLabel;
import javax.swing.SwingWorker;
import javax.swing.text.EditorKit;
import javax.swing.text.html.HTMLEditorKit;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import static org.openide.util.NbBundle.Messages;
import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.openide.nodes.Node;
import org.openide.util.lookup.ServiceProvider;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance;
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException;
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeUtil;
import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardArtifactTag;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.Tag;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.jsoup.Jsoup;
import org.sleuthkit.autopsy.contentviewers.application.Annotations;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
/**
* Annotations view of file contents.
@ -65,90 +45,10 @@ import org.jsoup.nodes.Element;
@Messages({
"AnnotationsContentViewer.title=Annotations",
"AnnotationsContentViewer.toolTip=Displays tags and comments associated with the selected content.",
"AnnotationsContentViewer.centralRepositoryEntry.title=Central Repository Comments",
"AnnotationsContentViewer.centralRepositoryEntryDataLabel.case=Case:",
"AnnotationsContentViewer.centralRepositoryEntryDataLabel.type=Type:",
"AnnotationsContentViewer.centralRepositoryEntryDataLabel.comment=Comment:",
"AnnotationsContentViewer.centralRepositoryEntryDataLabel.path=Path:",
"AnnotationsContentViewer.tagEntry.title=Tags",
"AnnotationsContentViewer.tagEntryDataLabel.tag=Tag:",
"AnnotationsContentViewer.tagEntryDataLabel.tagUser=Examiner:",
"AnnotationsContentViewer.tagEntryDataLabel.comment=Comment:",
"AnnotationsContentViewer.fileHitEntry.artifactCommentTitle=Artifact Comment",
"AnnotationsContentViewer.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments",
"AnnotationsContentViewer.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments",
"AnnotationsContentViewer.fileHitEntry.setName=Set Name:",
"AnnotationsContentViewer.fileHitEntry.comment=Comment:",
"AnnotationsContentViewer.sourceFile.title=Source File",
"AnnotationsContentViewer.onEmpty=No annotations were found for this particular item."
})
public class AnnotationsContentViewer extends javax.swing.JPanel implements DataContentViewer {
/**
* Describes a key value pair for an item of type T where the key is the
* field name to display and the value is retrieved from item of type T
* using a provided Function<T, string>.
*
* @param <T> The item type.
*/
private static class ItemEntry<T> {
private final String itemName;
private final Function<T, String> valueRetriever;
ItemEntry(String itemName, Function<T, String> valueRetriever) {
this.itemName = itemName;
this.valueRetriever = valueRetriever;
}
String getItemName() {
return itemName;
}
Function<T, String> getValueRetriever() {
return valueRetriever;
}
String retrieveValue(T object) {
return valueRetriever.apply(object);
}
}
/**
* Describes a section that will be appended to the annotations view panel.
*
* @param <T> The item type for items to display.
*/
private static class SectionConfig<T> {
private final String title;
private final List<ItemEntry<T>> attributes;
SectionConfig(String title, List<ItemEntry<T>> attributes) {
this.title = title;
this.attributes = attributes;
}
/**
* @return The title for the section.
*/
String getTitle() {
return title;
}
/**
* @return Describes key-value pairs on the object to display to the
* user.
*/
List<ItemEntry<T>> getAttributes() {
return attributes;
}
}
private static final Logger logger = Logger.getLogger(AnnotationsContentViewer.class.getName());
private static final String EMPTY_HTML = "<html><head></head><body></body></html>";
private static final int DEFAULT_FONT_SIZE = new JLabel().getFont().getSize();
// how big the subheader should be
@ -161,77 +61,34 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data
private static final int DEFAULT_SUBSECTION_LEFT_PAD = DEFAULT_FONT_SIZE;
// spacing occurring after an item
private static final int DEFAULT_TABLE_SPACING = DEFAULT_FONT_SIZE;
private static final int DEFAULT_SECTION_SPACING = DEFAULT_FONT_SIZE * 2;
private static final int DEFAULT_SUBSECTION_SPACING = DEFAULT_FONT_SIZE / 2;
private static final int CELL_SPACING = DEFAULT_FONT_SIZE / 2;
// html stylesheet classnames for components
private static final String MESSAGE_CLASSNAME = "message";
private static final String SUBSECTION_CLASSNAME = "subsection";
private static final String SUBHEADER_CLASSNAME = "subheader";
private static final String SECTION_CLASSNAME = "section";
private static final String HEADER_CLASSNAME = "header";
private static final String VERTICAL_TABLE_CLASSNAME = "vertical-table";
// additional styling for components
private static final String STYLE_SHEET_RULE
= String.format(" .%s { font-size: %dpx;font-style:italic; margin: 0px; padding: 0px; } ", MESSAGE_CLASSNAME, DEFAULT_FONT_SIZE)
= String.format(" .%s { font-size: %dpx;font-style:italic; margin: 0px; padding: 0px; } ", Annotations.MESSAGE_CLASSNAME, DEFAULT_FONT_SIZE)
+ String.format(" .%s {font-size:%dpx;font-weight:bold; margin: 0px; margin-top: %dpx; padding: 0px; } ",
SUBHEADER_CLASSNAME, SUBHEADER_FONT_SIZE, DEFAULT_SUBSECTION_SPACING)
+ String.format(" .%s { font-size:%dpx;font-weight:bold; margin: 0px; padding: 0px; } ", HEADER_CLASSNAME, HEADER_FONT_SIZE)
Annotations.SUBHEADER_CLASSNAME, SUBHEADER_FONT_SIZE, DEFAULT_SUBSECTION_SPACING)
+ String.format(" .%s { font-size:%dpx;font-weight:bold; margin: 0px; padding: 0px; } ", Annotations.HEADER_CLASSNAME, HEADER_FONT_SIZE)
+ String.format(" td { vertical-align: top; font-size:%dpx; text-align: left; margin: 0px; padding: 0px %dpx 0px 0px;} ", DEFAULT_FONT_SIZE, CELL_SPACING)
+ String.format(" th { vertical-align: top; text-align: left; margin: 0px; padding: 0px %dpx 0px 0px} ", DEFAULT_FONT_SIZE, CELL_SPACING)
+ String.format(" .%s { margin: %dpx 0px; padding-left: %dpx; } ", SUBSECTION_CLASSNAME, DEFAULT_SUBSECTION_SPACING, DEFAULT_SUBSECTION_LEFT_PAD)
+ String.format(" .%s { margin-bottom: %dpx; } ", SECTION_CLASSNAME, DEFAULT_SECTION_SPACING);
+ String.format(" .%s { margin: %dpx 0px; padding-left: %dpx; } ", Annotations.SUBSECTION_CLASSNAME, DEFAULT_SUBSECTION_SPACING, DEFAULT_SUBSECTION_LEFT_PAD)
+ String.format(" .%s { margin-bottom: %dpx; } ", Annotations.SECTION_CLASSNAME, DEFAULT_SECTION_SPACING);
// describing table values for a tag
private static final List<ItemEntry<Tag>> TAG_ENTRIES = Arrays.asList(
new ItemEntry<>(Bundle.AnnotationsContentViewer_tagEntryDataLabel_tag(),
(tag) -> (tag.getName() != null) ? tag.getName().getDisplayName() : null),
new ItemEntry<>(Bundle.AnnotationsContentViewer_tagEntryDataLabel_tagUser(), (tag) -> tag.getUserName()),
new ItemEntry<>(Bundle.AnnotationsContentViewer_tagEntryDataLabel_comment(), (tag) -> tag.getComment())
);
private static final long serialVersionUID = 1L;
private static final Logger logger = Logger.getLogger(AnnotationsContentViewer.class.getName());
private static final SectionConfig<Tag> TAG_CONFIG
= new SectionConfig<>(Bundle.AnnotationsContentViewer_tagEntry_title(), TAG_ENTRIES);
// file set attributes and table configurations
private static final List<ItemEntry<BlackboardArtifact>> FILESET_HIT_ENTRIES = Arrays.asList(
new ItemEntry<>(Bundle.AnnotationsContentViewer_fileHitEntry_setName(),
(bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)),
new ItemEntry<>(Bundle.AnnotationsContentViewer_fileHitEntry_comment(),
(bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT))
);
private static final SectionConfig<BlackboardArtifact> INTERESTING_FILE_CONFIG
= new SectionConfig<>(Bundle.AnnotationsContentViewer_fileHitEntry_interestingFileHitTitle(), FILESET_HIT_ENTRIES);
private static final SectionConfig<BlackboardArtifact> HASHSET_CONFIG
= new SectionConfig<>(Bundle.AnnotationsContentViewer_fileHitEntry_hashSetHitTitle(), FILESET_HIT_ENTRIES);
private static final SectionConfig<BlackboardArtifact> ARTIFACT_COMMENT_CONFIG
= new SectionConfig<>(Bundle.AnnotationsContentViewer_fileHitEntry_artifactCommentTitle(), FILESET_HIT_ENTRIES);
// central repository attributes and table configuration
private static final List<ItemEntry<CorrelationAttributeInstance>> CR_COMMENTS_ENTRIES = Arrays.asList(
new ItemEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_case(),
cai -> (cai.getCorrelationCase() != null) ? cai.getCorrelationCase().getDisplayName() : null),
new ItemEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_comment(), cai -> cai.getComment()),
new ItemEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_path(), cai -> cai.getFilePath())
);
private static final SectionConfig<CorrelationAttributeInstance> CR_COMMENTS_CONFIG
= new SectionConfig<>(Bundle.AnnotationsContentViewer_centralRepositoryEntry_title(), CR_COMMENTS_ENTRIES);
private AnnotationWorker worker;
/**
* Creates an instance of AnnotationsContentViewer.
*/
public AnnotationsContentViewer() {
initComponents();
Utilities.configureTextPaneAsHtml(jTextPane1);
Utilities.configureTextPaneAsHtml(textPanel);
// get html editor kit and apply additional style rules
EditorKit editorKit = jTextPane1.getEditorKit();
EditorKit editorKit = textPanel.getEditorKit();
if (editorKit instanceof HTMLEditorKit) {
HTMLEditorKit htmlKit = (HTMLEditorKit) editorKit;
htmlKit.getStyleSheet().addRule(STYLE_SHEET_RULE);
@ -240,472 +97,21 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data
@Override
public void setNode(Node node) {
if ((node == null) || (!isSupported(node))) {
resetComponent();
if(worker != null) {
worker.cancel(true);
worker = null;
}
if(node == null) {
return;
}
Document html = Jsoup.parse(EMPTY_HTML);
Element body = html.getElementsByTag("body").first();
BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class);
Content sourceFile = null;
try {
if (artifact != null) {
/*
* Get the source content based on the artifact to ensure we
* display the correct data instead of whatever was in the node.
*/
sourceFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID());
} else {
/*
* No artifact is present, so get the content based on what's
* present in the node. In this case, the selected item IS the
* source file.
*/
sourceFile = node.getLookup().lookup(AbstractFile.class);
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format(
"Exception while trying to retrieve a Content instance from the BlackboardArtifact '%s' (id=%d).",
artifact.getDisplayName(), artifact.getArtifactID()), ex);
worker = new AnnotationWorker(node);
worker.execute();
}
boolean somethingWasRendered = false;
if (artifact != null) {
somethingWasRendered = renderArtifact(body, artifact, sourceFile);
} else {
somethingWasRendered = renderContent(body, sourceFile, false);
}
if (!somethingWasRendered) {
appendMessage(body, Bundle.AnnotationsContentViewer_onEmpty());
}
jTextPane1.setText(html.html());
jTextPane1.setCaretPosition(0);
}
/**
* Renders annotations for an artifact.
*
* @param parent The html element to render content int.
* @param bba The blackboard artifact to render.
* @param sourceContent The content from which the blackboard artifact
* comes.
*
* @return If any content was actually rendered.
*/
private static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent) {
boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(bba), false);
if (CentralRepository.isEnabled()) {
List<CorrelationAttributeInstance> centralRepoComments = getCentralRepositoryData(bba);
boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, false);
contentRendered = contentRendered || crRendered;
}
// if artifact is a hashset hit or interesting file and has a non-blank comment
if ((ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() == bba.getArtifactTypeID()
|| ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() == bba.getArtifactTypeID())
&& (hasTskComment(bba))) {
boolean filesetRendered = appendEntries(parent, ARTIFACT_COMMENT_CONFIG, Arrays.asList(bba), false);
contentRendered = contentRendered || filesetRendered;
}
Element sourceFileSection = appendSection(parent, Bundle.AnnotationsContentViewer_sourceFile_title());
boolean sourceFileRendered = renderContent(sourceFileSection, sourceContent, true);
if (!sourceFileRendered) {
sourceFileSection.remove();
}
return contentRendered || sourceFileRendered;
}
/**
* Renders annotations for a content item.
*
* @param parent The parent within which to render.
* @param sourceContent The content for which annotations will be gathered.
* @param isSubheader True if this section should be rendered as a
* subheader as opposed to a top-level header.
*
* @return If any content was actually rendered.
*/
private static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader) {
boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(sourceContent), isSubheader);
if (sourceContent instanceof AbstractFile) {
AbstractFile sourceFile = (AbstractFile) sourceContent;
if (CentralRepository.isEnabled()) {
List<CorrelationAttributeInstance> centralRepoComments = getCentralRepositoryData(sourceFile);
boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, isSubheader);
contentRendered = contentRendered || crRendered;
}
boolean hashsetRendered = appendEntries(parent, HASHSET_CONFIG,
getFileSetHits(sourceFile, ARTIFACT_TYPE.TSK_HASHSET_HIT),
isSubheader);
boolean interestingFileRendered = appendEntries(parent, INTERESTING_FILE_CONFIG,
getFileSetHits(sourceFile, ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT),
isSubheader);
contentRendered = contentRendered || hashsetRendered || interestingFileRendered;
}
return contentRendered;
}
/**
* Retrieves tags associated with a content item.
*
* @param sourceContent The content for which to gather content.
*
* @return The Tags associated with this item.
*/
private static List<ContentTag> getTags(Content sourceContent) {
try {
SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
return tskCase.getContentTagsByContent(sourceContent);
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
}
return new ArrayList<>();
}
/**
* Retrieves tags for blackboard artifact tags.
*
* @param bba The blackboard artifact for which to retrieve tags.
*
* @return The found tags.
*/
private static List<BlackboardArtifactTag> getTags(BlackboardArtifact bba) {
try {
SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
return tskCase.getBlackboardArtifactTagsByArtifact(bba);
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
}
return new ArrayList<>();
}
/**
* Retrieves the blackboard artifacts for a source file matching a certain
* type that have a non-blank TSK_COMMENT.
*
* @param sourceFile The source file for which to fetch artifacts.
* @param type The type of blackboard artifact to fetch.
*
* @return The artifacts found matching this type.
*/
private static List<BlackboardArtifact> getFileSetHits(AbstractFile sourceFile, ARTIFACT_TYPE type) {
try {
SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
return tskCase.getBlackboardArtifacts(type, sourceFile.getId()).stream()
.filter((bba) -> hasTskComment(bba))
.collect(Collectors.toList());
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Exception while getting file set hits from the case database.", ex); //NON-NLS
}
return new ArrayList<>();
}
/**
* Returns true if the artifact contains a non-blank TSK_COMMENT attribute.
*
* @param artifact The artifact to check.
*
* @return True if it has a non-blank TSK_COMMENT.
*/
private static boolean hasTskComment(BlackboardArtifact artifact) {
return StringUtils.isNotBlank(tryGetAttribute(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT));
}
/**
* Attempts to retrieve the attribute of a particular type from a blackboard
* artifact.
*
* @param artifact The artifact from which to retrieve the information.
* @param attributeType The attribute type to retrieve from the artifact.
*
* @return The string value of the attribute or null if not found.
*/
private static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType) {
if (artifact == null) {
return null;
}
BlackboardAttribute attr = null;
try {
attr = artifact.getAttribute(new BlackboardAttribute.Type(attributeType));
} catch (TskCoreException ex) {
logger.log(Level.WARNING, String.format("Unable to fetch attribute of type %s for artifact %s", attributeType, artifact), ex);
}
if (attr == null) {
return null;
}
return attr.getValueString();
}
/**
* Gets the "Central Repository Comments" section with data for the
* blackboard artifact.
*
* @param artifact The selected artifact.
*
* @return The Correlation Attribute Instances associated with the artifact
* that have comments.
*/
private static List<CorrelationAttributeInstance> getCentralRepositoryData(BlackboardArtifact artifact) {
if (artifact == null) {
return new ArrayList<>();
}
List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys = CorrelationAttributeUtil.makeCorrAttrsForCorrelation(artifact)
.stream()
.map(cai -> Pair.of(cai.getCorrelationType(), cai.getCorrelationValue()))
.collect(Collectors.toList());
return getCorrelationAttributeComments(lookupKeys);
}
/**
* Gets the "Central Repository Comments" section with data.
*
* @param sourceFile A selected file, or a source file of the selected
* artifact.
*
* @return The Correlation Attribute Instances associated with the
* sourcefile that have comments.
*/
private static List<CorrelationAttributeInstance> getCentralRepositoryData(AbstractFile sourceFile) {
if (sourceFile == null || StringUtils.isEmpty(sourceFile.getMd5Hash())) {
return new ArrayList<>();
}
List<CorrelationAttributeInstance.Type> artifactTypes = null;
try {
artifactTypes = CentralRepository.getInstance().getDefinedCorrelationTypes();
} catch (CentralRepoException ex) {
logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
}
if (artifactTypes == null || artifactTypes.isEmpty()) {
return new ArrayList<>();
}
String md5 = sourceFile.getMd5Hash();
// get key lookups for a file attribute types and the md5 hash
List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys = artifactTypes.stream()
.filter((attributeType) -> attributeType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID)
.map((attributeType) -> Pair.of(attributeType, md5))
.collect(Collectors.toList());
return getCorrelationAttributeComments(lookupKeys);
}
/**
* Given a type and a value for that type, does a lookup in the Central
* Repository for matching values that have comments.
*
* @param lookupKeys The type and value to lookup.
*
* @return The found correlation attribute instances.
*/
private static List<CorrelationAttributeInstance> getCorrelationAttributeComments(List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys) {
List<CorrelationAttributeInstance> instancesToRet = new ArrayList<>();
try {
// use lookup instances to find the actual correlation attributes for the items selected
for (Pair<CorrelationAttributeInstance.Type, String> typeVal : lookupKeys) {
instancesToRet.addAll(CentralRepository.getInstance()
.getArtifactInstancesByTypeValue(typeVal.getKey(), typeVal.getValue())
.stream()
// for each one found, if it has a comment, return
.filter((cai) -> StringUtils.isNotBlank(cai.getComment()))
.collect(Collectors.toList()));
}
} catch (CentralRepoException ex) {
logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
} catch (CorrelationAttributeNormalizationException ex) {
logger.log(Level.SEVERE, "Error normalizing instance from Central Repository database.", ex); // NON-NLS
}
return instancesToRet;
}
/**
* Append entries to the parent element in the annotations viewer. Entries
* will be formatted as a table in the format specified in the
* SectionConfig.
*
* @param parent The parent element for which the entries will be
* attached.
* @param config The display configuration for this entry type (i.e.
* table type, name, if data is not present).
* @param items The items to display.
* @param isSubsection Whether or not this should be displayed as a
* subsection. If not displayed as a top-level section.
*
* @return If there was actual content rendered for this set of entries.
*/
private static <T> boolean appendEntries(Element parent, SectionConfig<T> config, List<? extends T> items,
boolean isSubsection) {
if (items == null || items.isEmpty()) {
return false;
}
Element sectionDiv = (isSubsection) ? appendSubsection(parent, config.getTitle()) : appendSection(parent, config.getTitle());
appendVerticalEntryTables(sectionDiv, items, config.getAttributes());
return true;
}
/**
* Appends a table where items are displayed in rows of key-value pairs.
*
* @param parent The parent to append the table.
* @param items The items to process into a series of tables.
* @param rowHeaders The keys and the means to process items in order to get
* key-value pairs.
*
* @return The parent element provided as parameter.
*/
private static <T> Element appendVerticalEntryTables(Element parent, List<? extends T> items, List<ItemEntry<T>> rowHeaders) {
boolean isFirst = true;
for (T item : items) {
if (item == null) {
continue;
}
List<List<String>> tableData = rowHeaders.stream()
.map(row -> Arrays.asList(row.getItemName(), row.retrieveValue(item)))
.collect(Collectors.toList());
Element childTable = appendTable(parent, 2, tableData, null);
childTable.attr("class", VERTICAL_TABLE_CLASSNAME);
if (isFirst) {
isFirst = false;
} else {
childTable.attr("style", String.format("margin-top: %dpx;", DEFAULT_TABLE_SPACING));
}
}
return parent;
}
/**
* Appends a generic table to the parent element.
*
* @param parent The parent element that will have a table appended
* to it.
* @param columnNumber The number of columns to append.
* @param content The content in content.get(row).get(column) format.
* @param columnHeaders The column headers or null if no column headers
* should be created.
*
* @return The created table.
*/
private static Element appendTable(Element parent, int columnNumber, List<List<String>> content, List<String> columnHeaders) {
Element table = parent.appendElement("table");
if (columnHeaders != null && !columnHeaders.isEmpty()) {
Element header = table.appendElement("thead");
appendRow(header, columnHeaders, columnNumber, true);
}
Element tableBody = table.appendElement("tbody");
content.forEach((rowData) -> appendRow(tableBody, rowData, columnNumber, false));
return table;
}
/**
* Appends a row to the parent element (should be thead or tbody).
*
* @param rowParent The parent table element.
* @param data The data to place in columns within the table.
* @param columnNumber The number of columns to append.
* @param isHeader Whether or not this should have header cells ('th')
* instead of regular cells ('td').
*
* @return The row created.
*/
private static Element appendRow(Element rowParent, List<String> data, int columnNumber, boolean isHeader) {
String cellType = isHeader ? "th" : "td";
Element row = rowParent.appendElement("tr");
for (int i = 0; i < columnNumber; i++) {
Element cell = row.appendElement(cellType);
if (data != null && i < data.size()) {
cell.text(StringUtils.isEmpty(data.get(i)) ? "" : data.get(i));
}
}
return row;
}
/**
* Appends a new section with a section header to the parent element.
*
* @param parent The element to append this section to.
* @param headerText The text for the section.
*
* @return The div for the new section.
*/
private static Element appendSection(Element parent, String headerText) {
Element sectionDiv = parent.appendElement("div");
sectionDiv.attr("class", SECTION_CLASSNAME);
Element header = sectionDiv.appendElement("h1");
header.text(headerText);
header.attr("class", HEADER_CLASSNAME);
return sectionDiv;
}
/**
* Appends a new subsection with a subsection header to the parent element.
*
* @param parent The element to append this subsection to.
* @param headerText The text for the subsection.
*
* @return The div for the new subsection.
*/
private static Element appendSubsection(Element parent, String headerText) {
Element subsectionDiv = parent.appendElement("div");
subsectionDiv.attr("class", SUBSECTION_CLASSNAME);
Element header = subsectionDiv.appendElement("h2");
header.text(headerText);
header.attr("class", SUBHEADER_CLASSNAME);
return subsectionDiv;
}
/**
* Appends a message to the parent element. This is typically used in the
* event that no data exists for a certain type.
*
* @param parent The parent element that will have this message appended to
* it.
* @param message The message to append.
*
* @return The paragraph element for the new message.
*/
private static Element appendMessage(Element parent, String message) {
Element messageEl = parent.appendElement("p");
messageEl.text(message);
messageEl.attr("class", MESSAGE_CLASSNAME);
return messageEl;
}
/**
* This method is called from within the constructor to initialize the form.
@ -716,31 +122,30 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
jScrollPane5 = new javax.swing.JScrollPane();
jTextPane1 = new javax.swing.JTextPane();
javax.swing.JScrollPane scrollPane = new javax.swing.JScrollPane();
textPanel = new javax.swing.JTextPane();
setPreferredSize(new java.awt.Dimension(100, 58));
jTextPane1.setEditable(false);
jTextPane1.setName(""); // NOI18N
jTextPane1.setPreferredSize(new java.awt.Dimension(600, 52));
jScrollPane5.setViewportView(jTextPane1);
textPanel.setEditable(false);
textPanel.setName(""); // NOI18N
textPanel.setPreferredSize(new java.awt.Dimension(600, 52));
scrollPane.setViewportView(textPanel);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jScrollPane5, javax.swing.GroupLayout.DEFAULT_SIZE, 907, Short.MAX_VALUE)
.addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 907, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jScrollPane5, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 435, Short.MAX_VALUE)
.addComponent(scrollPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 435, Short.MAX_VALUE)
);
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JScrollPane jScrollPane5;
private javax.swing.JTextPane jTextPane1;
private javax.swing.JTextPane textPanel;
// End of variables declaration//GEN-END:variables
@Override
@ -793,6 +198,49 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data
@Override
public void resetComponent() {
jTextPane1.setText(EMPTY_HTML);
textPanel.setText("");
}
/**
* A SwingWorker that will fetch the annotation information for the given
* node.
*/
private class AnnotationWorker extends SwingWorker<String, Void> {
private final Node node;
AnnotationWorker(Node node) {
this.node = node;
}
@Override
protected String doInBackground() throws Exception {
Document doc = Annotations.buildDocument(node);
if(isCancelled()) {
return null;
}
if(doc != null) {
return doc.html();
} else {
return Bundle.AnnotationsContentViewer_onEmpty();
}
}
@Override
public void done() {
if (isCancelled()) {
return;
}
try {
String text = get();
textPanel.setText(text);
textPanel.setCaretPosition(0);
} catch (InterruptedException | ExecutionException ex) {
logger.log(Level.SEVERE, "Failed to get annotation information for node", ex);
}
}
}
}

View File

@ -15,22 +15,7 @@
# governing permissions and limitations under the License.
#
AnnotationsContentViewer.centralRepositoryEntry.title=Central Repository Comments
AnnotationsContentViewer.centralRepositoryEntryDataLabel.case=Case:
AnnotationsContentViewer.centralRepositoryEntryDataLabel.comment=Comment:
AnnotationsContentViewer.centralRepositoryEntryDataLabel.path=Path:
AnnotationsContentViewer.centralRepositoryEntryDataLabel.type=Type:
AnnotationsContentViewer.fileHitEntry.artifactCommentTitle=Artifact Comment
AnnotationsContentViewer.fileHitEntry.comment=Comment:
AnnotationsContentViewer.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments
AnnotationsContentViewer.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments
AnnotationsContentViewer.fileHitEntry.setName=Set Name:
AnnotationsContentViewer.onEmpty=No annotations were found for this particular item.
AnnotationsContentViewer.sourceFile.title=Source File
AnnotationsContentViewer.tagEntry.title=Tags
AnnotationsContentViewer.tagEntryDataLabel.comment=Comment:
AnnotationsContentViewer.tagEntryDataLabel.tag=Tag:
AnnotationsContentViewer.tagEntryDataLabel.tagUser=Examiner:
AnnotationsContentViewer.title=Annotations
AnnotationsContentViewer.toolTip=Displays tags and comments associated with the selected content.
ApplicationContentViewer.title=Application

View File

@ -0,0 +1,673 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2021 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.contentviewers.application;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.swing.JLabel;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.openide.nodes.Node;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException;
import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository;
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance;
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException;
import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeUtil;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardArtifactTag;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.Tag;
import org.sleuthkit.datamodel.TskCoreException;
/**
* The business logic for the Annotations content panel.
*/
public class Annotations {
@NbBundle.Messages({
"Annotations.title=Annotations",
"Annotations.toolTip=Displays tags and comments associated with the selected content.",
"Annotations.centralRepositoryEntry.title=Central Repository Comments",
"Annotations.centralRepositoryEntryDataLabel.case=Case:",
"Annotations.centralRepositoryEntryDataLabel.type=Type:",
"Annotations.centralRepositoryEntryDataLabel.comment=Comment:",
"Annotations.centralRepositoryEntryDataLabel.path=Path:",
"Annotations.tagEntry.title=Tags",
"Annotations.tagEntryDataLabel.tag=Tag:",
"Annotations.tagEntryDataLabel.tagUser=Examiner:",
"Annotations.tagEntryDataLabel.comment=Comment:",
"Annotations.fileHitEntry.artifactCommentTitle=Artifact Comment",
"Annotations.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments",
"Annotations.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments",
"Annotations.fileHitEntry.setName=Set Name:",
"Annotations.fileHitEntry.comment=Comment:",
"Annotations.sourceFile.title=Source File",
"Annotations.onEmpty=No annotations were found for this particular item."
})
private static final Logger logger = Logger.getLogger(Annotations.class.getName());
private static final String EMPTY_HTML = "<html><head></head><body></body></html>";
private static final int DEFAULT_FONT_SIZE = new JLabel().getFont().getSize();
// spacing occurring after an item
private static final int DEFAULT_TABLE_SPACING = DEFAULT_FONT_SIZE;
// html stylesheet classnames for components
public static final String MESSAGE_CLASSNAME = "message";
public static final String SUBSECTION_CLASSNAME = "subsection";
public static final String SUBHEADER_CLASSNAME = "subheader";
public static final String SECTION_CLASSNAME = "section";
public static final String HEADER_CLASSNAME = "header";
public static final String VERTICAL_TABLE_CLASSNAME = "vertical-table";
// describing table values for a tag
private static final List<ItemEntry<Tag>> TAG_ENTRIES = Arrays.asList(
new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_tag(),
(tag) -> (tag.getName() != null) ? tag.getName().getDisplayName() : null),
new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_tagUser(), (tag) -> tag.getUserName()),
new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_comment(), (tag) -> tag.getComment())
);
private static final SectionConfig<Tag> TAG_CONFIG
= new SectionConfig<>(Bundle.Annotations_tagEntry_title(), TAG_ENTRIES);
// file set attributes and table configurations
private static final List<ItemEntry<BlackboardArtifact>> FILESET_HIT_ENTRIES = Arrays.asList(
new ItemEntry<>(Bundle.Annotations_fileHitEntry_setName(),
(bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)),
new ItemEntry<>(Bundle.Annotations_fileHitEntry_comment(),
(bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT))
);
private static final SectionConfig<BlackboardArtifact> INTERESTING_FILE_CONFIG
= new SectionConfig<>(Bundle.Annotations_fileHitEntry_interestingFileHitTitle(), FILESET_HIT_ENTRIES);
private static final SectionConfig<BlackboardArtifact> HASHSET_CONFIG
= new SectionConfig<>(Bundle.Annotations_fileHitEntry_hashSetHitTitle(), FILESET_HIT_ENTRIES);
private static final SectionConfig<BlackboardArtifact> ARTIFACT_COMMENT_CONFIG
= new SectionConfig<>(Bundle.Annotations_fileHitEntry_artifactCommentTitle(), FILESET_HIT_ENTRIES);
// central repository attributes and table configuration
private static final List<ItemEntry<CorrelationAttributeInstance>> CR_COMMENTS_ENTRIES = Arrays.asList(
new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_case(),
cai -> (cai.getCorrelationCase() != null) ? cai.getCorrelationCase().getDisplayName() : null),
new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_comment(), cai -> cai.getComment()),
new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_path(), cai -> cai.getFilePath())
);
private static final SectionConfig<CorrelationAttributeInstance> CR_COMMENTS_CONFIG
= new SectionConfig<>(Bundle.Annotations_centralRepositoryEntry_title(), CR_COMMENTS_ENTRIES);
/*
* Private constructor for this utility class.
*/
private Annotations() {
}
/**
* Returns the formatted Annotation information for the given node. If no
* data was found the method will return null;
*
* @param node Node to get data for.
*
* @return A formatted document of annotation information for the given node
* or null.
*/
public static Document buildDocument(Node node) {
Document html = Jsoup.parse(EMPTY_HTML);
Element body = html.getElementsByTag("body").first();
BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class);
Content sourceFile = null;
try {
if (artifact != null) {
/*
* Get the source content based on the artifact to ensure we
* display the correct data instead of whatever was in the node.
*/
sourceFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID());
} else {
/*
* No artifact is present, so get the content based on what's
* present in the node. In this case, the selected item IS the
* source file.
*/
sourceFile = node.getLookup().lookup(AbstractFile.class);
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format(
"Exception while trying to retrieve a Content instance from the BlackboardArtifact '%s' (id=%d).",
artifact.getDisplayName(), artifact.getArtifactID()), ex);
}
boolean somethingWasRendered = false;
if (artifact != null) {
somethingWasRendered = renderArtifact(body, artifact, sourceFile);
} else {
somethingWasRendered = renderContent(body, sourceFile, false);
}
if (!somethingWasRendered) {
return null;
}
return html;
}
/**
* Renders annotations for an artifact.
*
* @param parent The html element to render content int.
* @param bba The blackboard artifact to render.
* @param sourceContent The content from which the blackboard artifact
* comes.
*
* @return If any content was actually rendered.
*/
private static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent) {
boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(bba), false);
if (CentralRepository.isEnabled()) {
List<CorrelationAttributeInstance> centralRepoComments = getCentralRepositoryData(bba);
boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, false);
contentRendered = contentRendered || crRendered;
}
// if artifact is a hashset hit or interesting file and has a non-blank comment
if ((BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() == bba.getArtifactTypeID()
|| BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() == bba.getArtifactTypeID())
&& (hasTskComment(bba))) {
boolean filesetRendered = appendEntries(parent, ARTIFACT_COMMENT_CONFIG, Arrays.asList(bba), false);
contentRendered = contentRendered || filesetRendered;
}
Element sourceFileSection = appendSection(parent, Bundle.Annotations_sourceFile_title());
boolean sourceFileRendered = renderContent(sourceFileSection, sourceContent, true);
if (!sourceFileRendered) {
sourceFileSection.remove();
}
return contentRendered || sourceFileRendered;
}
/**
* Renders annotations for a content item.
*
* @param parent The parent within which to render.
* @param sourceContent The content for which annotations will be gathered.
* @param isSubheader True if this section should be rendered as a
* subheader as opposed to a top-level header.
*
* @return If any content was actually rendered.
*/
private static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader) {
boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(sourceContent), isSubheader);
if (sourceContent instanceof AbstractFile) {
AbstractFile sourceFile = (AbstractFile) sourceContent;
if (CentralRepository.isEnabled()) {
List<CorrelationAttributeInstance> centralRepoComments = getCentralRepositoryData(sourceFile);
boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, isSubheader);
contentRendered = contentRendered || crRendered;
}
boolean hashsetRendered = appendEntries(parent, HASHSET_CONFIG,
getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT),
isSubheader);
boolean interestingFileRendered = appendEntries(parent, INTERESTING_FILE_CONFIG,
getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT),
isSubheader);
contentRendered = contentRendered || hashsetRendered || interestingFileRendered;
}
return contentRendered;
}
/**
* Retrieves tags associated with a content item.
*
* @param sourceContent The content for which to gather content.
*
* @return The Tags associated with this item.
*/
private static List<ContentTag> getTags(Content sourceContent) {
try {
SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
return tskCase.getContentTagsByContent(sourceContent);
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
}
return new ArrayList<>();
}
/**
* Retrieves tags for blackboard artifact tags.
*
* @param bba The blackboard artifact for which to retrieve tags.
*
* @return The found tags.
*/
private static List<BlackboardArtifactTag> getTags(BlackboardArtifact bba) {
try {
SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
return tskCase.getBlackboardArtifactTagsByArtifact(bba);
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
}
return new ArrayList<>();
}
/**
* Retrieves the blackboard artifacts for a source file matching a certain
* type that have a non-blank TSK_COMMENT.
*
* @param sourceFile The source file for which to fetch artifacts.
* @param type The type of blackboard artifact to fetch.
*
* @return The artifacts found matching this type.
*/
private static List<BlackboardArtifact> getFileSetHits(AbstractFile sourceFile, BlackboardArtifact.ARTIFACT_TYPE type) {
try {
SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
return tskCase.getBlackboardArtifacts(type, sourceFile.getId()).stream()
.filter((bba) -> hasTskComment(bba))
.collect(Collectors.toList());
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Exception while getting file set hits from the case database.", ex); //NON-NLS
}
return new ArrayList<>();
}
/**
* Returns true if the artifact contains a non-blank TSK_COMMENT attribute.
*
* @param artifact The artifact to check.
*
* @return True if it has a non-blank TSK_COMMENT.
*/
private static boolean hasTskComment(BlackboardArtifact artifact) {
return StringUtils.isNotBlank(tryGetAttribute(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT));
}
/**
* Attempts to retrieve the attribute of a particular type from a blackboard
* artifact.
*
* @param artifact The artifact from which to retrieve the information.
* @param attributeType The attribute type to retrieve from the artifact.
*
* @return The string value of the attribute or null if not found.
*/
private static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType) {
if (artifact == null) {
return null;
}
BlackboardAttribute attr = null;
try {
attr = artifact.getAttribute(new BlackboardAttribute.Type(attributeType));
} catch (TskCoreException ex) {
logger.log(Level.WARNING, String.format("Unable to fetch attribute of type %s for artifact %s", attributeType, artifact), ex);
}
if (attr == null) {
return null;
}
return attr.getValueString();
}
/**
* Gets the "Central Repository Comments" section with data for the
* blackboard artifact.
*
* @param artifact The selected artifact.
*
* @return The Correlation Attribute Instances associated with the artifact
* that have comments.
*/
private static List<CorrelationAttributeInstance> getCentralRepositoryData(BlackboardArtifact artifact) {
if (artifact == null) {
return new ArrayList<>();
}
List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys = CorrelationAttributeUtil.makeCorrAttrsForCorrelation(artifact)
.stream()
.map(cai -> Pair.of(cai.getCorrelationType(), cai.getCorrelationValue()))
.collect(Collectors.toList());
return getCorrelationAttributeComments(lookupKeys);
}
/**
* Gets the "Central Repository Comments" section with data.
*
* @param sourceFile A selected file, or a source file of the selected
* artifact.
*
* @return The Correlation Attribute Instances associated with the
* sourcefile that have comments.
*/
private static List<CorrelationAttributeInstance> getCentralRepositoryData(AbstractFile sourceFile) {
if (sourceFile == null || StringUtils.isEmpty(sourceFile.getMd5Hash())) {
return new ArrayList<>();
}
List<CorrelationAttributeInstance.Type> artifactTypes = null;
try {
artifactTypes = CentralRepository.getInstance().getDefinedCorrelationTypes();
} catch (CentralRepoException ex) {
logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
}
if (artifactTypes == null || artifactTypes.isEmpty()) {
return new ArrayList<>();
}
String md5 = sourceFile.getMd5Hash();
// get key lookups for a file attribute types and the md5 hash
List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys = artifactTypes.stream()
.filter((attributeType) -> attributeType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID)
.map((attributeType) -> Pair.of(attributeType, md5))
.collect(Collectors.toList());
return getCorrelationAttributeComments(lookupKeys);
}
/**
* Given a type and a value for that type, does a lookup in the Central
* Repository for matching values that have comments.
*
* @param lookupKeys The type and value to lookup.
*
* @return The found correlation attribute instances.
*/
private static List<CorrelationAttributeInstance> getCorrelationAttributeComments(List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys) {
List<CorrelationAttributeInstance> instancesToRet = new ArrayList<>();
try {
// use lookup instances to find the actual correlation attributes for the items selected
for (Pair<CorrelationAttributeInstance.Type, String> typeVal : lookupKeys) {
instancesToRet.addAll(CentralRepository.getInstance()
.getArtifactInstancesByTypeValue(typeVal.getKey(), typeVal.getValue())
.stream()
// for each one found, if it has a comment, return
.filter((cai) -> StringUtils.isNotBlank(cai.getComment()))
.collect(Collectors.toList()));
}
} catch (CentralRepoException ex) {
logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
} catch (CorrelationAttributeNormalizationException ex) {
logger.log(Level.SEVERE, "Error normalizing instance from Central Repository database.", ex); // NON-NLS
}
return instancesToRet;
}
/**
* Append entries to the parent element in the annotations viewer. Entries
* will be formatted as a table in the format specified in the
* SectionConfig.
*
* @param parent The parent element for which the entries will be
* attached.
* @param config The display configuration for this entry type (i.e.
* table type, name, if data is not present).
* @param items The items to display.
* @param isSubsection Whether or not this should be displayed as a
* subsection. If not displayed as a top-level section.
*
* @return If there was actual content rendered for this set of entries.
*/
private static <T> boolean appendEntries(Element parent, Annotations.SectionConfig<T> config, List<? extends T> items,
boolean isSubsection) {
if (items == null || items.isEmpty()) {
return false;
}
Element sectionDiv = (isSubsection) ? appendSubsection(parent, config.getTitle()) : appendSection(parent, config.getTitle());
appendVerticalEntryTables(sectionDiv, items, config.getAttributes());
return true;
}
/**
* Appends a table where items are displayed in rows of key-value pairs.
*
* @param parent The parent to append the table.
* @param items The items to process into a series of tables.
* @param rowHeaders The keys and the means to process items in order to get
* key-value pairs.
*
* @return The parent element provided as parameter.
*/
private static <T> Element appendVerticalEntryTables(Element parent, List<? extends T> items, List<ItemEntry<T>> rowHeaders) {
boolean isFirst = true;
for (T item : items) {
if (item == null) {
continue;
}
List<List<String>> tableData = rowHeaders.stream()
.map(row -> Arrays.asList(row.getItemName(), row.retrieveValue(item)))
.collect(Collectors.toList());
Element childTable = appendTable(parent, 2, tableData, null);
childTable.attr("class", VERTICAL_TABLE_CLASSNAME);
if (isFirst) {
isFirst = false;
} else {
childTable.attr("style", String.format("margin-top: %dpx;", DEFAULT_TABLE_SPACING));
}
}
return parent;
}
/**
* Appends a generic table to the parent element.
*
* @param parent The parent element that will have a table appended
* to it.
* @param columnNumber The number of columns to append.
* @param content The content in content.get(row).get(column) format.
* @param columnHeaders The column headers or null if no column headers
* should be created.
*
* @return The created table.
*/
private static Element appendTable(Element parent, int columnNumber, List<List<String>> content, List<String> columnHeaders) {
Element table = parent.appendElement("table");
if (columnHeaders != null && !columnHeaders.isEmpty()) {
Element header = table.appendElement("thead");
appendRow(header, columnHeaders, columnNumber, true);
}
Element tableBody = table.appendElement("tbody");
content.forEach((rowData) -> appendRow(tableBody, rowData, columnNumber, false));
return table;
}
/**
* Appends a row to the parent element (should be thead or tbody).
*
* @param rowParent The parent table element.
* @param data The data to place in columns within the table.
* @param columnNumber The number of columns to append.
* @param isHeader Whether or not this should have header cells ('th')
* instead of regular cells ('td').
*
* @return The row created.
*/
private static Element appendRow(Element rowParent, List<String> data, int columnNumber, boolean isHeader) {
String cellType = isHeader ? "th" : "td";
Element row = rowParent.appendElement("tr");
for (int i = 0; i < columnNumber; i++) {
Element cell = row.appendElement(cellType);
if (data != null && i < data.size()) {
cell.text(StringUtils.isEmpty(data.get(i)) ? "" : data.get(i));
}
}
return row;
}
/**
* Appends a new section with a section header to the parent element.
*
* @param parent The element to append this section to.
* @param headerText The text for the section.
*
* @return The div for the new section.
*/
private static Element appendSection(Element parent, String headerText) {
Element sectionDiv = parent.appendElement("div");
sectionDiv.attr("class", SECTION_CLASSNAME);
Element header = sectionDiv.appendElement("h1");
header.text(headerText);
header.attr("class", HEADER_CLASSNAME);
return sectionDiv;
}
/**
* Appends a new subsection with a subsection header to the parent element.
*
* @param parent The element to append this subsection to.
* @param headerText The text for the subsection.
*
* @return The div for the new subsection.
*/
private static Element appendSubsection(Element parent, String headerText) {
Element subsectionDiv = parent.appendElement("div");
subsectionDiv.attr("class", SUBSECTION_CLASSNAME);
Element header = subsectionDiv.appendElement("h2");
header.text(headerText);
header.attr("class", SUBHEADER_CLASSNAME);
return subsectionDiv;
}
/**
* Appends a message to the parent element. This is typically used in the
* event that no data exists for a certain type.
*
* @param parent The parent element that will have this message appended to
* it.
* @param message The message to append.
*
* @return The paragraph element for the new message.
*/
private static Element appendMessage(Element parent, String message) {
Element messageEl = parent.appendElement("p");
messageEl.text(message);
messageEl.attr("class", MESSAGE_CLASSNAME);
return messageEl;
}
/**
* Describes a key value pair for an item of type T where the key is the
* field name to display and the value is retrieved from item of type T
* using a provided Function<T, string>.
*
* @param <T> The item type.
*/
static class ItemEntry<T> {
private final String itemName;
private final Function<T, String> valueRetriever;
ItemEntry(String itemName, Function<T, String> valueRetriever) {
this.itemName = itemName;
this.valueRetriever = valueRetriever;
}
String getItemName() {
return itemName;
}
Function<T, String> getValueRetriever() {
return valueRetriever;
}
String retrieveValue(T object) {
return valueRetriever.apply(object);
}
}
/**
* Describes a section that will be appended to the annotations view panel.
*
* @param <T> The item type for items to display.
*/
static class SectionConfig<T> {
private final String title;
private final List<ItemEntry<T>> attributes;
SectionConfig(String title, List<ItemEntry<T>> attributes) {
this.title = title;
this.attributes = attributes;
}
/**
* @return The title for the section.
*/
String getTitle() {
return title;
}
/**
* @return Describes key-value pairs on the object to display to the
* user.
*/
List<ItemEntry<T>> getAttributes() {
return attributes;
}
}
}

View File

@ -0,0 +1,18 @@
Annotations.centralRepositoryEntry.title=Central Repository Comments
Annotations.centralRepositoryEntryDataLabel.case=Case:
Annotations.centralRepositoryEntryDataLabel.comment=Comment:
Annotations.centralRepositoryEntryDataLabel.path=Path:
Annotations.centralRepositoryEntryDataLabel.type=Type:
Annotations.fileHitEntry.artifactCommentTitle=Artifact Comment
Annotations.fileHitEntry.comment=Comment:
Annotations.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments
Annotations.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments
Annotations.fileHitEntry.setName=Set Name:
Annotations.onEmpty=No annotations were found for this particular item.
Annotations.sourceFile.title=Source File
Annotations.tagEntry.title=Tags
Annotations.tagEntryDataLabel.comment=Comment:
Annotations.tagEntryDataLabel.tag=Tag:
Annotations.tagEntryDataLabel.tagUser=Examiner:
Annotations.title=Annotations
Annotations.toolTip=Displays tags and comments associated with the selected content.