Autopsy  4.19.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
Annotations.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2021 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.contentviewers.application;
20 
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.function.Function;
25 import java.util.logging.Level;
26 import java.util.stream.Collectors;
27 import org.apache.commons.lang.StringUtils;
28 import org.apache.commons.lang3.tuple.Pair;
29 import org.jsoup.Jsoup;
30 import org.jsoup.nodes.Document;
31 import org.jsoup.nodes.Element;
32 import org.openide.nodes.Node;
33 import org.openide.util.NbBundle;
43 import org.sleuthkit.datamodel.AbstractFile;
44 import org.sleuthkit.datamodel.BlackboardArtifact;
45 import org.sleuthkit.datamodel.BlackboardArtifactTag;
46 import org.sleuthkit.datamodel.BlackboardAttribute;
47 import org.sleuthkit.datamodel.Content;
48 import org.sleuthkit.datamodel.ContentTag;
49 import org.sleuthkit.datamodel.SleuthkitCase;
50 import org.sleuthkit.datamodel.Tag;
51 import org.sleuthkit.datamodel.TskCoreException;
52 
56 public class Annotations {
57 
58  @NbBundle.Messages({
59  "Annotations.title=Annotations",
60  "Annotations.toolTip=Displays tags and comments associated with the selected content.",
61  "Annotations.centralRepositoryEntry.title=Central Repository Comments",
62  "Annotations.centralRepositoryEntryDataLabel.case=Case:",
63  "Annotations.centralRepositoryEntryDataLabel.type=Type:",
64  "Annotations.centralRepositoryEntryDataLabel.comment=Comment:",
65  "Annotations.centralRepositoryEntryDataLabel.path=Path:",
66  "Annotations.tagEntry.title=Tags",
67  "Annotations.tagEntryDataLabel.tag=Tag:",
68  "Annotations.tagEntryDataLabel.tagUser=Examiner:",
69  "Annotations.tagEntryDataLabel.comment=Comment:",
70  "Annotations.fileHitEntry.artifactCommentTitle=Artifact Comment",
71  "Annotations.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments",
72  "Annotations.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments",
73  "Annotations.fileHitEntry.setName=Set Name:",
74  "Annotations.fileHitEntry.comment=Comment:",
75  "Annotations.sourceFile.title=Source File",
76  "Annotations.onEmpty=No annotations were found for this particular item."
77  })
78 
79  private static final Logger logger = Logger.getLogger(Annotations.class.getName());
80 
81  private static final String EMPTY_HTML = "<html><head></head><body></body></html>";
82 
83  // describing table values for a tag
84  private static final List<ItemEntry<Tag>> TAG_ENTRIES = Arrays.asList(
85  new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_tag(),
86  (tag) -> (tag.getName() != null) ? tag.getName().getDisplayName() : null),
87  new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_tagUser(), (tag) -> tag.getUserName()),
88  new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_comment(), (tag) -> tag.getComment())
89  );
90 
91  private static final SectionConfig<Tag> TAG_CONFIG
92  = new SectionConfig<>(Bundle.Annotations_tagEntry_title(), TAG_ENTRIES);
93 
94  // file set attributes and table configurations
95  private static final List<ItemEntry<BlackboardArtifact>> FILESET_HIT_ENTRIES = Arrays.asList(
96  new ItemEntry<>(Bundle.Annotations_fileHitEntry_setName(),
97  (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)),
98  new ItemEntry<>(Bundle.Annotations_fileHitEntry_comment(),
99  (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT))
100  );
101 
102  private static final SectionConfig<BlackboardArtifact> INTERESTING_FILE_CONFIG
103  = new SectionConfig<>(Bundle.Annotations_fileHitEntry_interestingFileHitTitle(), FILESET_HIT_ENTRIES);
104 
105  private static final SectionConfig<BlackboardArtifact> HASHSET_CONFIG
106  = new SectionConfig<>(Bundle.Annotations_fileHitEntry_hashSetHitTitle(), FILESET_HIT_ENTRIES);
107 
108  private static final SectionConfig<BlackboardArtifact> ARTIFACT_COMMENT_CONFIG
109  = new SectionConfig<>(Bundle.Annotations_fileHitEntry_artifactCommentTitle(), FILESET_HIT_ENTRIES);
110 
111  // central repository attributes and table configuration
112  private static final List<ItemEntry<CorrelationAttributeInstance>> CR_COMMENTS_ENTRIES = Arrays.asList(
113  new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_case(),
114  cai -> (cai.getCorrelationCase() != null) ? cai.getCorrelationCase().getDisplayName() : null),
115  new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_comment(), cai -> cai.getComment()),
116  new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_path(), cai -> cai.getFilePath())
117  );
118 
119  private static final SectionConfig<CorrelationAttributeInstance> CR_COMMENTS_CONFIG
120  = new SectionConfig<>(Bundle.Annotations_centralRepositoryEntry_title(), CR_COMMENTS_ENTRIES);
121 
122  /*
123  * Private constructor for this utility class.
124  */
125  private Annotations() {
126 
127  }
128 
138  public static Document buildDocument(Node node) {
139  Document html = Jsoup.parse(EMPTY_HTML);
140  Element body = html.getElementsByTag("body").first();
141 
142  BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class);
143  Content sourceFile = null;
144 
145  try {
146  if (artifact != null) {
147  /*
148  * Get the source content based on the artifact to ensure we
149  * display the correct data instead of whatever was in the node.
150  */
151  sourceFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID());
152  } else {
153  /*
154  * No artifact is present, so get the content based on what's
155  * present in the node. In this case, the selected item IS the
156  * source file.
157  */
158  sourceFile = node.getLookup().lookup(AbstractFile.class);
159  }
160  } catch (TskCoreException ex) {
161  logger.log(Level.SEVERE, String.format(
162  "Exception while trying to retrieve a Content instance from the BlackboardArtifact '%s' (id=%d).",
163  artifact.getDisplayName(), artifact.getArtifactID()), ex);
164  }
165 
166  boolean somethingWasRendered = false;
167  if (artifact != null) {
168  somethingWasRendered = renderArtifact(body, artifact, sourceFile);
169  } else {
170  somethingWasRendered = renderContent(body, sourceFile, false);
171  }
172 
173  if (!somethingWasRendered) {
174  return null;
175  }
176 
177  return html;
178  }
179 
190  private static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent) {
191  boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(bba), false, true);
192 
194  List<CorrelationAttributeInstance> centralRepoComments = getCentralRepositoryData(bba);
195  boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, false, !contentRendered);
196  contentRendered = contentRendered || crRendered;
197  }
198 
199  // if artifact is a hashset hit or interesting file and has a non-blank comment
200  if ((BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() == bba.getArtifactTypeID()
201  || BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() == bba.getArtifactTypeID())
202  && (hasTskComment(bba))) {
203 
204  boolean filesetRendered = appendEntries(parent, ARTIFACT_COMMENT_CONFIG, Arrays.asList(bba), false, !contentRendered);
205  contentRendered = contentRendered || filesetRendered;
206  }
207 
208  Element sourceFileSection = appendSection(parent, Bundle.Annotations_sourceFile_title());
209  sourceFileSection.attr("class", ContentViewerHtmlStyles.getSpacedSectionClassName());
210 
211  Element sourceFileContainer = sourceFileSection.appendElement("div");
212  sourceFileContainer.attr("class", ContentViewerHtmlStyles.getIndentedClassName());
213 
214  boolean sourceFileRendered = renderContent(sourceFileContainer, sourceContent, true);
215 
216  if (!sourceFileRendered) {
217  sourceFileSection.remove();
218  }
219 
220  return contentRendered || sourceFileRendered;
221  }
222 
233  private static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader) {
234  boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(sourceContent), isSubheader, true);
235 
236  if (sourceContent instanceof AbstractFile) {
237  AbstractFile sourceFile = (AbstractFile) sourceContent;
238 
240  List<CorrelationAttributeInstance> centralRepoComments = getCentralRepositoryData(sourceFile);
241  boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, isSubheader,
242  !contentRendered);
243  contentRendered = contentRendered || crRendered;
244  }
245 
246  boolean hashsetRendered = appendEntries(parent, HASHSET_CONFIG,
247  getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT),
248  isSubheader,
249  !contentRendered);
250 
251  boolean interestingFileRendered = appendEntries(parent, INTERESTING_FILE_CONFIG,
252  getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT),
253  isSubheader,
254  !contentRendered);
255 
256  contentRendered = contentRendered || hashsetRendered || interestingFileRendered;
257  }
258  return contentRendered;
259  }
260 
268  private static List<ContentTag> getTags(Content sourceContent) {
269  try {
270  SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
271  return tskCase.getContentTagsByContent(sourceContent);
272  } catch (NoCurrentCaseException ex) {
273  logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
274  } catch (TskCoreException ex) {
275  logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
276  }
277  return new ArrayList<>();
278  }
279 
287  private static List<BlackboardArtifactTag> getTags(BlackboardArtifact bba) {
288  try {
289  SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
290  return tskCase.getBlackboardArtifactTagsByArtifact(bba);
291  } catch (NoCurrentCaseException ex) {
292  logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
293  } catch (TskCoreException ex) {
294  logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
295  }
296  return new ArrayList<>();
297  }
298 
308  private static List<BlackboardArtifact> getFileSetHits(AbstractFile sourceFile, BlackboardArtifact.ARTIFACT_TYPE type) {
309  try {
310  SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
311  return tskCase.getBlackboardArtifacts(type, sourceFile.getId()).stream()
312  .filter((bba) -> hasTskComment(bba))
313  .collect(Collectors.toList());
314  } catch (NoCurrentCaseException ex) {
315  logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
316  } catch (TskCoreException ex) {
317  logger.log(Level.SEVERE, "Exception while getting file set hits from the case database.", ex); //NON-NLS
318  }
319  return new ArrayList<>();
320  }
321 
329  private static boolean hasTskComment(BlackboardArtifact artifact) {
330  return StringUtils.isNotBlank(tryGetAttribute(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT));
331  }
332 
342  private static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType) {
343  if (artifact == null) {
344  return null;
345  }
346 
347  BlackboardAttribute attr = null;
348  try {
349  attr = artifact.getAttribute(new BlackboardAttribute.Type(attributeType));
350  } catch (TskCoreException ex) {
351  logger.log(Level.WARNING, String.format("Unable to fetch attribute of type %s for artifact %s", attributeType, artifact), ex);
352  }
353 
354  if (attr == null) {
355  return null;
356  }
357 
358  return attr.getValueString();
359  }
360 
370  private static List<CorrelationAttributeInstance> getCentralRepositoryData(BlackboardArtifact artifact) {
371  if (artifact == null) {
372  return new ArrayList<>();
373  }
374 
376  .stream()
377  .map(cai -> Pair.of(cai.getCorrelationType(), cai.getCorrelationValue()))
378  .collect(Collectors.toList());
379 
380  return getCorrelationAttributeComments(lookupKeys);
381  }
382 
392  private static List<CorrelationAttributeInstance> getCentralRepositoryData(AbstractFile sourceFile) {
393  if (sourceFile == null || StringUtils.isEmpty(sourceFile.getMd5Hash())) {
394  return new ArrayList<>();
395  }
396 
397  List<CorrelationAttributeInstance.Type> artifactTypes = null;
398  try {
400  } catch (CentralRepoException ex) {
401  logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
402  }
403 
404  if (artifactTypes == null || artifactTypes.isEmpty()) {
405  return new ArrayList<>();
406  }
407 
408  String md5 = sourceFile.getMd5Hash();
409 
410  // get key lookups for a file attribute types and the md5 hash
411  List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys = artifactTypes.stream()
412  .filter((attributeType) -> attributeType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID)
413  .map((attributeType) -> Pair.of(attributeType, md5))
414  .collect(Collectors.toList());
415 
416  return getCorrelationAttributeComments(lookupKeys);
417  }
418 
427  private static List<CorrelationAttributeInstance> getCorrelationAttributeComments(List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys) {
428  List<CorrelationAttributeInstance> instancesToRet = new ArrayList<>();
429 
430  try {
431  // use lookup instances to find the actual correlation attributes for the items selected
432  for (Pair<CorrelationAttributeInstance.Type, String> typeVal : lookupKeys) {
433  instancesToRet.addAll(CentralRepository.getInstance()
434  .getArtifactInstancesByTypeValue(typeVal.getKey(), typeVal.getValue())
435  .stream()
436  // for each one found, if it has a comment, return
437  .filter((cai) -> StringUtils.isNotBlank(cai.getComment()))
438  .collect(Collectors.toList()));
439  }
440 
441  } catch (CentralRepoException ex) {
442  logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
444  logger.log(Level.SEVERE, "Error normalizing instance from Central Repository database.", ex); // NON-NLS
445  }
446 
447  return instancesToRet;
448  }
449 
467  private static <T> boolean appendEntries(Element parent, Annotations.SectionConfig<T> config, List<? extends T> items,
468  boolean isSubsection, boolean isFirstSection) {
469  if (items == null || items.isEmpty()) {
470  return false;
471  }
472 
473  Element sectionDiv = (isSubsection) ? appendSubsection(parent, config.getTitle()) : appendSection(parent, config.getTitle());
474  if (!isFirstSection) {
475  sectionDiv.attr("class", ContentViewerHtmlStyles.getSpacedSectionClassName());
476  }
477 
478  Element sectionContainer = sectionDiv.appendElement("div");
479 
480  if (!isSubsection) {
481  sectionContainer.attr("class", ContentViewerHtmlStyles.getIndentedClassName());
482  }
483 
484  appendVerticalEntryTables(sectionContainer, items, config.getAttributes());
485  return true;
486  }
487 
498  private static <T> Element appendVerticalEntryTables(Element parent, List<? extends T> items, List<ItemEntry<T>> rowHeaders) {
499  boolean isFirst = true;
500  for (T item : items) {
501  if (item == null) {
502  continue;
503  }
504 
505  List<List<String>> tableData = rowHeaders.stream()
506  .map(row -> Arrays.asList(row.getItemName(), row.retrieveValue(item)))
507  .collect(Collectors.toList());
508 
509  Element childTable = appendTable(parent, 2, tableData, null);
510 
511  if (isFirst) {
512  isFirst = false;
513  } else {
514  childTable.attr("class", ContentViewerHtmlStyles.getSpacedSectionClassName());
515  }
516  }
517 
518  return parent;
519  }
520 
533  private static Element appendTable(Element parent, int columnNumber, List<List<String>> content, List<String> columnHeaders) {
534  Element table = parent.appendElement("table")
535  .attr("valign", "top")
536  .attr("align", "left");
537 
538  if (columnHeaders != null && !columnHeaders.isEmpty()) {
539  Element header = table.appendElement("thead");
540  appendRow(header, columnHeaders, columnNumber, true);
541  }
542  Element tableBody = table.appendElement("tbody");
543 
544  content.forEach((rowData) -> appendRow(tableBody, rowData, columnNumber, false));
545  return table;
546  }
547 
559  private static Element appendRow(Element rowParent, List<String> data, int columnNumber, boolean isHeader) {
560  String cellType = isHeader ? "th" : "td";
561  Element row = rowParent.appendElement("tr");
562  for (int i = 0; i < columnNumber; i++) {
563  Element cell = row.appendElement(cellType);
564 
565  if (i == 0) {
566  cell.attr("class", ContentViewerHtmlStyles.getKeyColumnClassName());
567  }
568 
569  if (data != null && i < data.size()) {
570  cell.appendElement("span")
571  .attr("class", ContentViewerHtmlStyles.getTextClassName())
572  .text(StringUtils.isEmpty(data.get(i)) ? "" : data.get(i));
573  }
574  }
575  return row;
576  }
577 
586  private static Element appendSection(Element parent, String headerText) {
587  Element sectionDiv = parent.appendElement("div");
588  Element header = sectionDiv.appendElement("h1");
589  header.text(headerText);
590  header.attr("class", ContentViewerHtmlStyles.getHeaderClassName());
591  return sectionDiv;
592  }
593 
602  private static Element appendSubsection(Element parent, String headerText) {
603  Element subsectionDiv = parent.appendElement("div");
604  Element header = subsectionDiv.appendElement("h2");
605  header.text(headerText);
606  header.attr("class", ContentViewerHtmlStyles.getHeaderClassName());
607  return subsectionDiv;
608  }
609 
620  private static Element appendMessage(Element parent, String message) {
621  Element messageEl = parent.appendElement("p");
622  messageEl.text(message);
623  messageEl.attr("class", ContentViewerHtmlStyles.getMessageClassName());
624  return messageEl;
625  }
626 
634  static class ItemEntry<T> {
635 
636  private final String itemName;
637  private final Function<T, String> valueRetriever;
638 
639  ItemEntry(String itemName, Function<T, String> valueRetriever) {
640  this.itemName = itemName;
641  this.valueRetriever = valueRetriever;
642  }
643 
644  String getItemName() {
645  return itemName;
646  }
647 
648  Function<T, String> getValueRetriever() {
649  return valueRetriever;
650  }
651 
652  String retrieveValue(T object) {
653  return valueRetriever.apply(object);
654  }
655  }
656 
662  static class SectionConfig<T> {
663 
664  private final String title;
665  private final List<ItemEntry<T>> attributes;
666 
667  SectionConfig(String title, List<ItemEntry<T>> attributes) {
668  this.title = title;
669  this.attributes = attributes;
670  }
671 
675  String getTitle() {
676  return title;
677  }
678 
683  List<ItemEntry<T>> getAttributes() {
684  return attributes;
685  }
686  }
687 
688 }
static< T > Element appendVerticalEntryTables(Element parent, List<?extends T > items, List< ItemEntry< T >> rowHeaders)
static List< ContentTag > getTags(Content sourceContent)
static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent)
static final SectionConfig< BlackboardArtifact > INTERESTING_FILE_CONFIG
static final SectionConfig< BlackboardArtifact > ARTIFACT_COMMENT_CONFIG
static Element appendRow(Element rowParent, List< String > data, int columnNumber, boolean isHeader)
static Element appendSubsection(Element parent, String headerText)
static Element appendSection(Element parent, String headerText)
static List< BlackboardArtifact > getFileSetHits(AbstractFile sourceFile, BlackboardArtifact.ARTIFACT_TYPE type)
List< CorrelationAttributeInstance > getArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value)
static List< CorrelationAttributeInstance > getCentralRepositoryData(AbstractFile sourceFile)
static< T > boolean appendEntries(Element parent, Annotations.SectionConfig< T > config, List<?extends T > items, boolean isSubsection, boolean isFirstSection)
List< CorrelationAttributeInstance.Type > getDefinedCorrelationTypes()
static List< CorrelationAttributeInstance > makeCorrAttrsForCorrelation(BlackboardArtifact artifact)
static Element appendTable(Element parent, int columnNumber, List< List< String >> content, List< String > columnHeaders)
static List< BlackboardArtifactTag > getTags(BlackboardArtifact bba)
static List< CorrelationAttributeInstance > getCorrelationAttributeComments(List< Pair< CorrelationAttributeInstance.Type, String >> lookupKeys)
static boolean hasTskComment(BlackboardArtifact artifact)
static final SectionConfig< BlackboardArtifact > HASHSET_CONFIG
static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader)
static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType)
static final List< ItemEntry< CorrelationAttributeInstance > > CR_COMMENTS_ENTRIES
static List< CorrelationAttributeInstance > getCentralRepositoryData(BlackboardArtifact artifact)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static final List< ItemEntry< Tag > > TAG_ENTRIES
static final SectionConfig< CorrelationAttributeInstance > CR_COMMENTS_CONFIG
static final List< ItemEntry< BlackboardArtifact > > FILESET_HIT_ENTRIES
static Element appendMessage(Element parent, String message)

Copyright © 2012-2021 Basis Technology. Generated on: Fri Aug 6 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.