Autopsy  4.16.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.io.IOException;
22 import java.util.ArrayList;
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 value
212  String md5Hash = getHash(file, totals);
213  if (md5Hash == null) {
214  return ProcessResult.ERROR;
215  }
216 
217  // the processing result of handling this file
219 
220  // look up in notable first
221  FindInHashsetsResult knownBadResult = findInHashsets(file, totals.totalKnownBadCount,
222  totals.totalLookuptime, knownBadHashSets, TskData.FileKnown.BAD, knownBadLookupError);
223 
224  boolean foundBad = knownBadResult.isFound();
225  if (knownBadResult.isError()) {
226  ret = ProcessResult.ERROR;
227  }
228 
229  // look up no change items next
230  FindInHashsetsResult noChangeResult = findInHashsets(file, totals.totalNoChangeCount,
231  totals.totalLookuptime, noChangeHashSets, TskData.FileKnown.UNKNOWN, noChangeLookupError);
232 
233  if (noChangeResult.isError()) {
234  ret = ProcessResult.ERROR;
235  }
236 
237  // If the file is not in the notable sets, search for it in the known sets.
238  // Any hit is sufficient to classify it as known, and there is no need to create
239  // a hit artifact or send a message to the application inbox.
240  if (!foundBad) {
241  for (HashDb db : knownHashSets) {
242  try {
243  long lookupstart = System.currentTimeMillis();
244  if (db.lookupMD5Quick(file)) {
245  file.setKnown(TskData.FileKnown.KNOWN);
246  break;
247  }
248  long delta = (System.currentTimeMillis() - lookupstart);
249  totals.totalLookuptime.addAndGet(delta);
250 
251  } catch (TskException ex) {
252  reportLookupError(ex, file, knownLookupError);
253  ret = ProcessResult.ERROR;
254  }
255  }
256  }
257 
258  return ret;
259  }
260 
268  private boolean shouldSkip(AbstractFile file) {
269  // Skip unallocated space files.
270  if ((file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)
271  || file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) {
272  return true;
273  }
274 
275  /*
276  * Skip directories. One reason for this is because we won't accurately
277  * calculate hashes of NTFS directories that have content that spans the
278  * IDX_ROOT and IDX_ALLOC artifacts. So we disable that until a solution
279  * for it is developed.
280  */
281  if (file.isDir()) {
282  return true;
283  }
284 
285  // bail out if we have no hashes set
286  if ((knownHashSets.isEmpty()) && (knownBadHashSets.isEmpty()) && (!settings.shouldCalculateHashes())) {
287  return true;
288  }
289 
290  return false;
291  }
292 
302  private void reportLookupError(TskException ex, AbstractFile file, Function<AbstractFile, String> lookupErrorMessage) {
303  logger.log(Level.WARNING, String.format(
304  "Couldn't lookup notable hash for file '%s' (id=%d) - see sleuthkit log for details", file.getName(), file.getId()), ex); //NON-NLS
307  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.hashLookupErrorMsg", file.getName()),
308  lookupErrorMessage.apply(file)));
309  }
310 
314  private static class FindInHashsetsResult {
315 
316  private final boolean found;
317  private final boolean error;
318 
319  FindInHashsetsResult(boolean found, boolean error) {
320  this.found = found;
321  this.error = error;
322  }
323 
329  boolean isFound() {
330  return found;
331  }
332 
340  boolean isError() {
341  return error;
342  }
343  }
344 
363  private FindInHashsetsResult findInHashsets(AbstractFile file, AtomicLong totalCount, AtomicLong totalLookupTime,
364  List<HashDb> hashSets, TskData.FileKnown statusIfFound, Function<AbstractFile, String> lookupErrorMessage) {
365 
366  boolean found = false;
367  boolean wasError = false;
368  for (HashDb db : hashSets) {
369  try {
370  long lookupstart = System.currentTimeMillis();
371  HashHitInfo hashInfo = db.lookupMD5(file);
372  if (null != hashInfo) {
373  found = true;
374 
375  totalCount.incrementAndGet();
376  file.setKnown(statusIfFound);
377  String hashSetName = db.getDisplayName();
378  String comment = generateComment(hashInfo);
379  if (!createArtifactIfNotExists(hashSetName, file, comment, db)) {
380  wasError = true;
381  }
382  }
383  long delta = (System.currentTimeMillis() - lookupstart);
384  totalLookupTime.addAndGet(delta);
385 
386  } catch (TskException ex) {
387  reportLookupError(ex, file, lookupErrorMessage);
388  wasError = true;
389  }
390  }
391 
392  return new FindInHashsetsResult(found, wasError);
393  }
394 
402  private String generateComment(HashHitInfo hashInfo) {
403  String comment = "";
404  ArrayList<String> comments = hashInfo.getComments();
405  int i = 0;
406  for (String c : comments) {
407  if (++i > 1) {
408  comment += " ";
409  }
410  comment += c;
411  if (comment.length() > MAX_COMMENT_SIZE) {
412  comment = comment.substring(0, MAX_COMMENT_SIZE) + "...";
413  break;
414  }
415  }
416  return comment;
417  }
418 
429  private boolean createArtifactIfNotExists(String hashSetName, AbstractFile file, String comment, HashDb db) {
430  /*
431  * We have a match. Now create an artifact if it is determined that one
432  * hasn't been created yet.
433  */
434  List<BlackboardAttribute> attributesList = new ArrayList<>();
435  attributesList.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, HashLookupModuleFactory.getModuleName(), hashSetName));
436  try {
437  Blackboard tskBlackboard = skCase.getBlackboard();
438  if (tskBlackboard.artifactExists(file, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, attributesList) == false) {
439  postHashSetHitToBlackboard(file, file.getMd5Hash(), hashSetName, comment, db.getSendIngestMessages());
440  }
441  } catch (TskCoreException ex) {
442  logger.log(Level.SEVERE, String.format(
443  "A problem occurred while checking for existing artifacts for file '%s' (id=%d).", file.getName(), file.getId()), ex); //NON-NLS
446  Bundle.HashDbIngestModule_dialogTitle_errorFindingArtifacts(file.getName()),
447  Bundle.HashDbIngestModule_errorMessage_lookingForFileArtifacts(file.getName())));
448  return false;
449  }
450  return true;
451  }
452 
463  private String getHash(AbstractFile file, IngestJobTotals totals) {
464  String md5Hash = file.getMd5Hash();
465  if (md5Hash != null && md5Hash.isEmpty()) {
466  return md5Hash;
467  }
468 
469  try {
470  TimingMetric metric = HealthMonitor.getTimingMetric("Disk Reads: Hash calculation");
471  long calcstart = System.currentTimeMillis();
472  md5Hash = HashUtility.calculateMd5Hash(file);
473  if (file.getSize() > 0) {
474  // Surprisingly, the hash calculation does not seem to be correlated that
475  // strongly with file size until the files get large.
476  // Only normalize if the file size is greater than ~1MB.
477  if (file.getSize() < 1000000) {
479  } else {
480  // In testing, this normalization gave reasonable resuls
481  HealthMonitor.submitNormalizedTimingMetric(metric, file.getSize() / 500000);
482  }
483  }
484  file.setMd5Hash(md5Hash);
485  long delta = (System.currentTimeMillis() - calcstart);
486  totals.totalCalctime.addAndGet(delta);
487  return md5Hash;
488  } catch (IOException ex) {
489  logger.log(Level.WARNING, String.format("Error calculating hash of file '%s' (id=%d).", file.getName(), file.getId()), ex); //NON-NLS
492  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.fileReadErrorMsg", file.getName()),
493  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.calcHashValueErr",
494  file.getParentPath() + file.getName(),
495  file.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC) ? "Allocated File" : "Deleted File")));
496  return null;
497  }
498  }
499 
510  @Messages({
511  "HashDbIngestModule.indexError.message=Failed to index hashset hit artifact for keyword search."
512  })
513  private void postHashSetHitToBlackboard(AbstractFile abstractFile, String md5Hash, String hashSetName, String comment, boolean showInboxMessage) {
514  try {
515  String moduleName = HashLookupModuleFactory.getModuleName();
516  BlackboardArtifact badFile = abstractFile.newArtifact(ARTIFACT_TYPE.TSK_HASHSET_HIT);
517  Collection<BlackboardAttribute> attributes = new ArrayList<>();
518  //TODO Revisit usage of deprecated constructor as per TSK-583
519  //BlackboardAttribute att2 = new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID(), MODULE_NAME, "Known Bad", hashSetName);
520  attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, moduleName, hashSetName));
521  attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_HASH_MD5, moduleName, md5Hash));
522  attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COMMENT, moduleName, comment));
523 
524  badFile.addAttributes(attributes);
525 
526  try {
527  /*
528  * post the artifact which will index the artifact for keyword
529  * search, and fire an event to notify UI of this new artifact
530  */
531  blackboard.postArtifact(badFile, moduleName);
532  } catch (Blackboard.BlackboardException ex) {
533  logger.log(Level.SEVERE, "Unable to index blackboard artifact " + badFile.getArtifactID(), ex); //NON-NLS
535  Bundle.HashDbIngestModule_indexError_message(), badFile.getDisplayName());
536  }
537 
538  if (showInboxMessage) {
539  StringBuilder detailsSb = new StringBuilder();
540  //details
541  detailsSb.append("<table border='0' cellpadding='4' width='280'>"); //NON-NLS
542  //hit
543  detailsSb.append("<tr>"); //NON-NLS
544  detailsSb.append("<th>") //NON-NLS
545  .append(NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.fileName"))
546  .append("</th>"); //NON-NLS
547  detailsSb.append("<td>") //NON-NLS
548  .append(abstractFile.getName())
549  .append("</td>"); //NON-NLS
550  detailsSb.append("</tr>"); //NON-NLS
551 
552  detailsSb.append("<tr>"); //NON-NLS
553  detailsSb.append("<th>") //NON-NLS
554  .append(NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.md5Hash"))
555  .append("</th>"); //NON-NLS
556  detailsSb.append("<td>").append(md5Hash).append("</td>"); //NON-NLS
557  detailsSb.append("</tr>"); //NON-NLS
558 
559  detailsSb.append("<tr>"); //NON-NLS
560  detailsSb.append("<th>") //NON-NLS
561  .append(NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.hashsetName"))
562  .append("</th>"); //NON-NLS
563  detailsSb.append("<td>").append(hashSetName).append("</td>"); //NON-NLS
564  detailsSb.append("</tr>"); //NON-NLS
565 
566  detailsSb.append("</table>"); //NON-NLS
567 
569  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.knownBadMsg", abstractFile.getName()),
570  detailsSb.toString(),
571  abstractFile.getName() + md5Hash,
572  badFile));
573  }
574  } catch (TskException ex) {
575  logger.log(Level.WARNING, "Error creating blackboard artifact", ex); //NON-NLS
576  }
577  }
578 
587  @Messages("HashDbIngestModule.complete.noChangesFound=No Change items found:")
588  private static synchronized void postSummary(long jobId, List<HashDb> knownBadHashSets,
589  List<HashDb> noChangeHashSets, List<HashDb> knownHashSets) {
590 
591  IngestJobTotals jobTotals = getTotalsForIngestJobs(jobId);
592  totalsForIngestJobs.remove(jobId);
593 
594  if ((!knownBadHashSets.isEmpty()) || (!knownHashSets.isEmpty()) || (!noChangeHashSets.isEmpty())) {
595  StringBuilder detailsSb = new StringBuilder();
596  //details
597  detailsSb.append(
598  "<table border='0' cellpadding='4' width='280'>" +
599  "<tr><td>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.knownBadsFound") + "</td>" +
600  "<td>" + jobTotals.totalKnownBadCount.get() + "</td></tr>" +
601 
602  "<tr><td>" + Bundle.HashDbIngestModule_complete_noChangesFound() + "</td>" +
603  "<td>" + jobTotals.totalNoChangeCount.get() + "</td></tr>" +
604 
605  "<tr><td>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.totalCalcTime") +
606  "</td><td>" + jobTotals.totalCalctime.get() + "</td></tr>\n" +
607 
608  "<tr><td>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.totalLookupTime") +
609  "</td><td>" + jobTotals.totalLookuptime.get() + "</td></tr>\n</table>" +
610 
611  "<p>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.databasesUsed") + "</p>\n<ul>"); //NON-NLS
612 
613  Stream.concat(knownBadHashSets.stream(), noChangeHashSets.stream()).forEach((db) -> {
614  detailsSb.append("<li>" + db.getHashSetName() + "</li>\n"); //NON-NLS
615  });
616 
617  detailsSb.append("</ul>"); //NON-NLS
618 
622  NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.hashLookupResults"),
623  detailsSb.toString()));
624  }
625  }
626 
627  @Override
628  public void shutDown() {
629  if (refCounter.decrementAndGet(jobId) == 0) {
630  postSummary(jobId, knownBadHashSets, noChangeHashSets, knownHashSets);
631  }
632  }
633 }
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)
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)
String getHash(AbstractFile file, IngestJobTotals totals)
static synchronized IngestServices getInstance()

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