Autopsy  4.17.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
HashDbIngestModule.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011-2018 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.modules.hashdatabase;
20 
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.concurrent.atomic.AtomicLong;
27 import java.util.function.Function;
28 import java.util.logging.Level;
29 import java.util.stream.Stream;
30 import org.openide.util.NbBundle;
31 import org.openide.util.NbBundle.Messages;
43 import org.sleuthkit.datamodel.AbstractFile;
44 import org.sleuthkit.datamodel.Blackboard;
45 import org.sleuthkit.datamodel.BlackboardArtifact;
46 import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
47 import org.sleuthkit.datamodel.BlackboardAttribute;
48 import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
49 import org.sleuthkit.datamodel.HashHitInfo;
50 import org.sleuthkit.datamodel.HashUtility;
51 import org.sleuthkit.datamodel.SleuthkitCase;
52 import org.sleuthkit.datamodel.TskCoreException;
53 import org.sleuthkit.datamodel.TskData;
54 import org.sleuthkit.datamodel.TskException;
55 
59 @Messages({
60  "HashDbIngestModule.noKnownBadHashDbSetMsg=No notable hash set.",
61  "HashDbIngestModule.knownBadFileSearchWillNotExecuteWarn=Notable file search will not be executed.",
62  "HashDbIngestModule.noKnownHashDbSetMsg=No known hash set.",
63  "HashDbIngestModule.knownFileSearchWillNotExecuteWarn=Known file search will not be executed.",
64  "# {0} - fileName", "HashDbIngestModule.lookingUpKnownBadHashValueErr=Error encountered while looking up notable hash value for {0}.",
65  "# {0} - fileName", "HashDbIngestModule.lookingUpNoChangeHashValueErr=Error encountered while looking up no change hash value for {0}.",
66  "# {0} - fileName", "HashDbIngestModule.lookingUpKnownHashValueErr=Error encountered while looking up known hash value for {0}.",})
67 public class HashDbIngestModule implements FileIngestModule {
68 
69  private static final Logger logger = Logger.getLogger(HashDbIngestModule.class.getName());
70 
71  private final Function<AbstractFile, String> knownBadLookupError
72  = (file) -> Bundle.HashDbIngestModule_lookingUpKnownBadHashValueErr(file.getName());
73 
74  private final Function<AbstractFile, String> noChangeLookupError
75  = (file) -> Bundle.HashDbIngestModule_lookingUpNoChangeHashValueErr(file.getName());
76 
77  private final Function<AbstractFile, String> knownLookupError
78  = (file) -> Bundle.HashDbIngestModule_lookingUpKnownHashValueErr(file.getName());
79 
80  private static final int MAX_COMMENT_SIZE = 500;
81  private final IngestServices services = IngestServices.getInstance();
82  private final SleuthkitCase skCase;
83  private final HashDbManager hashDbManager = HashDbManager.getInstance();
84  private final HashLookupModuleSettings settings;
85  private final List<HashDb> knownBadHashSets = new ArrayList<>();
86  private final List<HashDb> knownHashSets = new ArrayList<>();
87  private final List<HashDb> noChangeHashSets = new ArrayList<>();
88  private long jobId;
89  private static final HashMap<Long, IngestJobTotals> totalsForIngestJobs = new HashMap<>();
90  private static final IngestModuleReferenceCounter refCounter = new IngestModuleReferenceCounter();
91  private Blackboard blackboard;
92 
96  private static class IngestJobTotals {
97 
98  private final AtomicLong totalKnownBadCount = new AtomicLong(0);
99  private final AtomicLong totalNoChangeCount = new AtomicLong(0);
100  private final AtomicLong totalCalctime = new AtomicLong(0);
101  private final AtomicLong totalLookuptime = new AtomicLong(0);
102  }
103 
104  private static synchronized IngestJobTotals getTotalsForIngestJobs(long ingestJobId) {
105  IngestJobTotals totals = totalsForIngestJobs.get(ingestJobId);
106  if (totals == null) {
107  totals = new HashDbIngestModule.IngestJobTotals();
108  totalsForIngestJobs.put(ingestJobId, totals);
109  }
110  return totals;
111  }
112 
122  HashDbIngestModule(HashLookupModuleSettings settings) throws NoCurrentCaseException {
123  this.settings = settings;
125  }
126 
127  @Override
129  jobId = context.getJobId();
130  if (!hashDbManager.verifyAllDatabasesLoadedCorrectly()) {
131  throw new IngestModuleException("Could not load all hash sets");
132  }
133 
134  initializeHashsets(hashDbManager.getAllHashSets());
135 
136  if (refCounter.incrementAndGet(jobId) == 1) {
137  // initialize job totals
138  getTotalsForIngestJobs(jobId);
139 
140  // if first module for this job then post error msgs if needed
141  if (knownBadHashSets.isEmpty()) {
144  Bundle.HashDbIngestModule_noKnownBadHashDbSetMsg(),
145  Bundle.HashDbIngestModule_knownBadFileSearchWillNotExecuteWarn()));
146  }
147 
148  if (knownHashSets.isEmpty()) {
151  Bundle.HashDbIngestModule_noKnownHashDbSetMsg(),
152  Bundle.HashDbIngestModule_knownFileSearchWillNotExecuteWarn()));
153  }
154  }
155  }
156 
163  private void initializeHashsets(List<HashDb> allHashSets) {
164  for (HashDb db : allHashSets) {
165  if (settings.isHashSetEnabled(db)) {
166  try {
167  if (db.isValid()) {
168  switch (db.getKnownFilesType()) {
169  case KNOWN:
170  knownHashSets.add(db);
171  break;
172  case KNOWN_BAD:
173  knownBadHashSets.add(db);
174  break;
175  case NO_CHANGE:
176  noChangeHashSets.add(db);
177  break;
178  default:
179  throw new TskCoreException("Unknown KnownFilesType: " + db.getKnownFilesType());
180  }
181  }
182  } catch (TskCoreException ex) {
183  logger.log(Level.WARNING, "Error getting index status for " + db.getDisplayName() + " hash set", ex); //NON-NLS
184  }
185  }
186  }
187  }
188 
189  @Messages({
190  "# {0} - File name",
191  "HashDbIngestModule.dialogTitle.errorFindingArtifacts=Error Finding Artifacts: {0}",
192  "# {0} - File name",
193  "HashDbIngestModule.errorMessage.lookingForFileArtifacts=Error encountered while looking for existing artifacts for {0}."
194  })
195  @Override
196  public ProcessResult process(AbstractFile file) {
197  try {
198  blackboard = Case.getCurrentCaseThrows().getSleuthkitCase().getBlackboard();
199  } catch (NoCurrentCaseException ex) {
200  logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS
201  return ProcessResult.ERROR;
202  }
203 
204  if (shouldSkip(file)) {
205  return ProcessResult.OK;
206  }
207 
208  // Safely get a reference to the totalsForIngestJobs object
209  IngestJobTotals totals = getTotalsForIngestJobs(jobId);
210 
211  // calc hash values
212  try {
213  calculateHashes(file, totals);
214  } catch (TskCoreException ex) {
215  logger.log(Level.WARNING, String.format("Error calculating hash of file '%s' (id=%d).", file.getName(), file.getId()), ex); //NON-NLS
218  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.fileReadErrorMsg", file.getName()),
219  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.calcHashValueErr",
220  file.getParentPath() + file.getName(),
221  file.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC) ? "Allocated File" : "Deleted File")));
222  }
223 
224  // the processing result of handling this file
226 
227  // look up in notable first
228  FindInHashsetsResult knownBadResult = findInHashsets(file, totals.totalKnownBadCount,
229  totals.totalLookuptime, knownBadHashSets, TskData.FileKnown.BAD, knownBadLookupError);
230 
231  boolean foundBad = knownBadResult.isFound();
232  if (knownBadResult.isError()) {
233  ret = ProcessResult.ERROR;
234  }
235 
236  // look up no change items next
237  FindInHashsetsResult noChangeResult = findInHashsets(file, totals.totalNoChangeCount,
238  totals.totalLookuptime, noChangeHashSets, TskData.FileKnown.UNKNOWN, noChangeLookupError);
239 
240  if (noChangeResult.isError()) {
241  ret = ProcessResult.ERROR;
242  }
243 
244  // If the file is not in the notable sets, search for it in the known sets.
245  // Any hit is sufficient to classify it as known, and there is no need to create
246  // a hit artifact or send a message to the application inbox.
247  if (!foundBad) {
248  for (HashDb db : knownHashSets) {
249  try {
250  long lookupstart = System.currentTimeMillis();
251  if (db.lookupMD5Quick(file)) {
252  file.setKnown(TskData.FileKnown.KNOWN);
253  break;
254  }
255  long delta = (System.currentTimeMillis() - lookupstart);
256  totals.totalLookuptime.addAndGet(delta);
257 
258  } catch (TskException ex) {
259  reportLookupError(ex, file, knownLookupError);
260  ret = ProcessResult.ERROR;
261  }
262  }
263  }
264 
265  return ret;
266  }
267 
275  private boolean shouldSkip(AbstractFile file) {
276  // Skip unallocated space files.
277  if ((file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)
278  || file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) {
279  return true;
280  }
281 
282  /*
283  * Skip directories. One reason for this is because we won't accurately
284  * calculate hashes of NTFS directories that have content that spans the
285  * IDX_ROOT and IDX_ALLOC artifacts. So we disable that until a solution
286  * for it is developed.
287  */
288  if (file.isDir()) {
289  return true;
290  }
291 
292  // bail out if we have no hashes set
293  if ((knownHashSets.isEmpty()) && (knownBadHashSets.isEmpty()) && (!settings.shouldCalculateHashes())) {
294  return true;
295  }
296 
297  return false;
298  }
299 
309  private void reportLookupError(TskException ex, AbstractFile file, Function<AbstractFile, String> lookupErrorMessage) {
310  logger.log(Level.WARNING, String.format(
311  "Couldn't lookup notable hash for file '%s' (id=%d) - see sleuthkit log for details", file.getName(), file.getId()), ex); //NON-NLS
314  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.hashLookupErrorMsg", file.getName()),
315  lookupErrorMessage.apply(file)));
316  }
317 
321  private static class FindInHashsetsResult {
322 
323  private final boolean found;
324  private final boolean error;
325 
326  FindInHashsetsResult(boolean found, boolean error) {
327  this.found = found;
328  this.error = error;
329  }
330 
336  boolean isFound() {
337  return found;
338  }
339 
347  boolean isError() {
348  return error;
349  }
350  }
351 
370  private FindInHashsetsResult findInHashsets(AbstractFile file, AtomicLong totalCount, AtomicLong totalLookupTime,
371  List<HashDb> hashSets, TskData.FileKnown statusIfFound, Function<AbstractFile, String> lookupErrorMessage) {
372 
373  boolean found = false;
374  boolean wasError = false;
375  for (HashDb db : hashSets) {
376  try {
377  long lookupstart = System.currentTimeMillis();
378  HashHitInfo hashInfo = db.lookupMD5(file);
379  if (null != hashInfo) {
380  found = true;
381 
382  totalCount.incrementAndGet();
383  file.setKnown(statusIfFound);
384  String hashSetName = db.getDisplayName();
385  String comment = generateComment(hashInfo);
386  if (!createArtifactIfNotExists(hashSetName, file, comment, db)) {
387  wasError = true;
388  }
389  }
390  long delta = (System.currentTimeMillis() - lookupstart);
391  totalLookupTime.addAndGet(delta);
392 
393  } catch (TskException ex) {
394  reportLookupError(ex, file, lookupErrorMessage);
395  wasError = true;
396  }
397  }
398 
399  return new FindInHashsetsResult(found, wasError);
400  }
401 
409  private String generateComment(HashHitInfo hashInfo) {
410  String comment = "";
411  ArrayList<String> comments = hashInfo.getComments();
412  int i = 0;
413  for (String c : comments) {
414  if (++i > 1) {
415  comment += " ";
416  }
417  comment += c;
418  if (comment.length() > MAX_COMMENT_SIZE) {
419  comment = comment.substring(0, MAX_COMMENT_SIZE) + "...";
420  break;
421  }
422  }
423  return comment;
424  }
425 
436  private boolean createArtifactIfNotExists(String hashSetName, AbstractFile file, String comment, HashDb db) {
437  /*
438  * We have a match. Now create an artifact if it is determined that one
439  * hasn't been created yet.
440  */
441  List<BlackboardAttribute> attributesList = new ArrayList<>();
442  attributesList.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, HashLookupModuleFactory.getModuleName(), hashSetName));
443  try {
444  Blackboard tskBlackboard = skCase.getBlackboard();
445  if (tskBlackboard.artifactExists(file, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, attributesList) == false) {
446  postHashSetHitToBlackboard(file, file.getMd5Hash(), hashSetName, comment, db.getSendIngestMessages());
447  }
448  } catch (TskCoreException ex) {
449  logger.log(Level.SEVERE, String.format(
450  "A problem occurred while checking for existing artifacts for file '%s' (id=%d).", file.getName(), file.getId()), ex); //NON-NLS
453  Bundle.HashDbIngestModule_dialogTitle_errorFindingArtifacts(file.getName()),
454  Bundle.HashDbIngestModule_errorMessage_lookingForFileArtifacts(file.getName())));
455  return false;
456  }
457  return true;
458  }
459 
467  private void calculateHashes(AbstractFile file, IngestJobTotals totals) throws TskCoreException {
468 
469  // First check if we've already calculated the hashes.
470  String md5Hash = file.getMd5Hash();
471  String sha256Hash = file.getSha256Hash();
472  if ((md5Hash != null && ! md5Hash.isEmpty())
473  && (sha256Hash != null && ! sha256Hash.isEmpty())) {
474  return;
475  }
476 
477  TimingMetric metric = HealthMonitor.getTimingMetric("Disk Reads: Hash calculation");
478  long calcstart = System.currentTimeMillis();
479  List<HashUtility.HashResult> newHashResults =
480  HashUtility.calculateHashes(file, Arrays.asList(HashUtility.HashType.MD5,HashUtility.HashType.SHA256 ));
481  if (file.getSize() > 0) {
482  // Surprisingly, the hash calculation does not seem to be correlated that
483  // strongly with file size until the files get large.
484  // Only normalize if the file size is greater than ~1MB.
485  if (file.getSize() < 1000000) {
487  } else {
488  // In testing, this normalization gave reasonable resuls
489  HealthMonitor.submitNormalizedTimingMetric(metric, file.getSize() / 500000);
490  }
491  }
492  for (HashUtility.HashResult hash : newHashResults) {
493  if (hash.getType().equals(HashUtility.HashType.MD5)) {
494  file.setMd5Hash(hash.getValue());
495  } else if (hash.getType().equals(HashUtility.HashType.SHA256)) {
496  file.setSha256Hash(hash.getValue());
497  }
498  }
499  long delta = (System.currentTimeMillis() - calcstart);
500  totals.totalCalctime.addAndGet(delta);
501  }
502 
513  @Messages({
514  "HashDbIngestModule.indexError.message=Failed to index hashset hit artifact for keyword search."
515  })
516  private void postHashSetHitToBlackboard(AbstractFile abstractFile, String md5Hash, String hashSetName, String comment, boolean showInboxMessage) {
517  try {
518  String moduleName = HashLookupModuleFactory.getModuleName();
519  BlackboardArtifact badFile = abstractFile.newArtifact(ARTIFACT_TYPE.TSK_HASHSET_HIT);
520  Collection<BlackboardAttribute> attributes = new ArrayList<>();
521  //TODO Revisit usage of deprecated constructor as per TSK-583
522  //BlackboardAttribute att2 = new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID(), MODULE_NAME, "Known Bad", hashSetName);
523  attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, moduleName, hashSetName));
524  attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_HASH_MD5, moduleName, md5Hash));
525  attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COMMENT, moduleName, comment));
526 
527  badFile.addAttributes(attributes);
528 
529  try {
530  /*
531  * post the artifact which will index the artifact for keyword
532  * search, and fire an event to notify UI of this new artifact
533  */
534  blackboard.postArtifact(badFile, moduleName);
535  } catch (Blackboard.BlackboardException ex) {
536  logger.log(Level.SEVERE, "Unable to index blackboard artifact " + badFile.getArtifactID(), ex); //NON-NLS
538  Bundle.HashDbIngestModule_indexError_message(), badFile.getDisplayName());
539  }
540 
541  if (showInboxMessage) {
542  StringBuilder detailsSb = new StringBuilder();
543  //details
544  detailsSb.append("<table border='0' cellpadding='4' width='280'>"); //NON-NLS
545  //hit
546  detailsSb.append("<tr>"); //NON-NLS
547  detailsSb.append("<th>") //NON-NLS
548  .append(NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.fileName"))
549  .append("</th>"); //NON-NLS
550  detailsSb.append("<td>") //NON-NLS
551  .append(abstractFile.getName())
552  .append("</td>"); //NON-NLS
553  detailsSb.append("</tr>"); //NON-NLS
554 
555  detailsSb.append("<tr>"); //NON-NLS
556  detailsSb.append("<th>") //NON-NLS
557  .append(NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.md5Hash"))
558  .append("</th>"); //NON-NLS
559  detailsSb.append("<td>").append(md5Hash).append("</td>"); //NON-NLS
560  detailsSb.append("</tr>"); //NON-NLS
561 
562  detailsSb.append("<tr>"); //NON-NLS
563  detailsSb.append("<th>") //NON-NLS
564  .append(NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.hashsetName"))
565  .append("</th>"); //NON-NLS
566  detailsSb.append("<td>").append(hashSetName).append("</td>"); //NON-NLS
567  detailsSb.append("</tr>"); //NON-NLS
568 
569  detailsSb.append("</table>"); //NON-NLS
570 
572  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.knownBadMsg", abstractFile.getName()),
573  detailsSb.toString(),
574  abstractFile.getName() + md5Hash,
575  badFile));
576  }
577  } catch (TskException ex) {
578  logger.log(Level.WARNING, "Error creating blackboard artifact", ex); //NON-NLS
579  }
580  }
581 
590  @Messages("HashDbIngestModule.complete.noChangesFound=No Change items found:")
591  private static synchronized void postSummary(long jobId, List<HashDb> knownBadHashSets,
592  List<HashDb> noChangeHashSets, List<HashDb> knownHashSets) {
593 
594  IngestJobTotals jobTotals = getTotalsForIngestJobs(jobId);
595  totalsForIngestJobs.remove(jobId);
596 
597  if ((!knownBadHashSets.isEmpty()) || (!knownHashSets.isEmpty()) || (!noChangeHashSets.isEmpty())) {
598  StringBuilder detailsSb = new StringBuilder();
599  //details
600  detailsSb.append(
601  "<table border='0' cellpadding='4' width='280'>" +
602  "<tr><td>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.knownBadsFound") + "</td>" +
603  "<td>" + jobTotals.totalKnownBadCount.get() + "</td></tr>" +
604 
605  "<tr><td>" + Bundle.HashDbIngestModule_complete_noChangesFound() + "</td>" +
606  "<td>" + jobTotals.totalNoChangeCount.get() + "</td></tr>" +
607 
608  "<tr><td>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.totalCalcTime") +
609  "</td><td>" + jobTotals.totalCalctime.get() + "</td></tr>\n" +
610 
611  "<tr><td>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.totalLookupTime") +
612  "</td><td>" + jobTotals.totalLookuptime.get() + "</td></tr>\n</table>" +
613 
614  "<p>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.databasesUsed") + "</p>\n<ul>"); //NON-NLS
615 
616  Stream.concat(knownBadHashSets.stream(), noChangeHashSets.stream()).forEach((db) -> {
617  detailsSb.append("<li>" + db.getHashSetName() + "</li>\n"); //NON-NLS
618  });
619 
620  detailsSb.append("</ul>"); //NON-NLS
621 
625  NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.hashLookupResults"),
626  detailsSb.toString()));
627  }
628  }
629 
630  @Override
631  public void shutDown() {
632  if (refCounter.decrementAndGet(jobId) == 0) {
633  postSummary(jobId, knownBadHashSets, noChangeHashSets, knownHashSets);
634  }
635  }
636 }
static IngestMessage createDataMessage(String source, String subject, String detailsHtml, String uniqueKey, BlackboardArtifact data)
static IngestMessage createErrorMessage(String source, String subject, String detailsHtml)
void startUp(org.sleuthkit.autopsy.ingest.IngestJobContext context)
static IngestMessage createMessage(MessageType messageType, String source, String subject, String detailsHtml)
static synchronized IngestJobTotals getTotalsForIngestJobs(long ingestJobId)
void calculateHashes(AbstractFile file, IngestJobTotals totals)
static TimingMetric getTimingMetric(String name)
void postHashSetHitToBlackboard(AbstractFile abstractFile, String md5Hash, String hashSetName, String comment, boolean showInboxMessage)
FindInHashsetsResult findInHashsets(AbstractFile file, AtomicLong totalCount, AtomicLong totalLookupTime, List< HashDb > hashSets, TskData.FileKnown statusIfFound, Function< AbstractFile, String > lookupErrorMessage)
void postMessage(final IngestMessage message)
void reportLookupError(TskException ex, AbstractFile file, Function< AbstractFile, String > lookupErrorMessage)
static void submitTimingMetric(TimingMetric metric)
static void error(String title, String message)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static IngestMessage createWarningMessage(String source, String subject, String detailsHtml)
boolean createArtifactIfNotExists(String hashSetName, AbstractFile file, String comment, HashDb db)
static void submitNormalizedTimingMetric(TimingMetric metric, long normalization)
static synchronized IngestServices getInstance()

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