Autopsy  4.15.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.noChangeHashDbSetMsg=No 'No Change' hash set.",
63  "HashDbIngestModule.noChangeFileSearchWillNotExecuteWarn='No Change' file search will not be executed.",
64  "HashDbIngestModule.noKnownHashDbSetMsg=No known hash set.",
65  "HashDbIngestModule.knownFileSearchWillNotExecuteWarn=Known file search will not be executed.",
66  "# {0} - fileName", "HashDbIngestModule.lookingUpKnownBadHashValueErr=Error encountered while looking up notable hash value for {0}.",
67  "# {0} - fileName", "HashDbIngestModule.lookingUpNoChangeHashValueErr=Error encountered while looking up no change hash value for {0}.",
68  "# {0} - fileName", "HashDbIngestModule.lookingUpKnownHashValueErr=Error encountered while looking up known hash value for {0}.",})
69 public class HashDbIngestModule implements FileIngestModule {
70 
71  private static final Logger logger = Logger.getLogger(HashDbIngestModule.class.getName());
72 
73  private final Function<AbstractFile, String> knownBadLookupError
74  = (file) -> Bundle.HashDbIngestModule_lookingUpKnownBadHashValueErr(file.getName());
75 
76  private final Function<AbstractFile, String> noChangeLookupError
77  = (file) -> Bundle.HashDbIngestModule_lookingUpNoChangeHashValueErr(file.getName());
78 
79  private final Function<AbstractFile, String> knownLookupError
80  = (file) -> Bundle.HashDbIngestModule_lookingUpKnownHashValueErr(file.getName());
81 
82  private static final int MAX_COMMENT_SIZE = 500;
83  private final IngestServices services = IngestServices.getInstance();
84  private final SleuthkitCase skCase;
85  private final HashDbManager hashDbManager = HashDbManager.getInstance();
86  private final HashLookupModuleSettings settings;
87  private final List<HashDb> knownBadHashSets = new ArrayList<>();
88  private final List<HashDb> knownHashSets = new ArrayList<>();
89  private final List<HashDb> noChangeHashSets = new ArrayList<>();
90  private long jobId;
91  private static final HashMap<Long, IngestJobTotals> totalsForIngestJobs = new HashMap<>();
92  private static final IngestModuleReferenceCounter refCounter = new IngestModuleReferenceCounter();
93  private Blackboard blackboard;
94 
98  private static class IngestJobTotals {
99 
100  private final AtomicLong totalKnownBadCount = new AtomicLong(0);
101  private final AtomicLong totalNoChangeCount = new AtomicLong(0);
102  private final AtomicLong totalCalctime = new AtomicLong(0);
103  private final AtomicLong totalLookuptime = new AtomicLong(0);
104  }
105 
106  private static synchronized IngestJobTotals getTotalsForIngestJobs(long ingestJobId) {
107  IngestJobTotals totals = totalsForIngestJobs.get(ingestJobId);
108  if (totals == null) {
109  totals = new HashDbIngestModule.IngestJobTotals();
110  totalsForIngestJobs.put(ingestJobId, totals);
111  }
112  return totals;
113  }
114 
124  HashDbIngestModule(HashLookupModuleSettings settings) throws NoCurrentCaseException {
125  this.settings = settings;
127  }
128 
129  @Override
131  jobId = context.getJobId();
132  if (!hashDbManager.verifyAllDatabasesLoadedCorrectly()) {
133  throw new IngestModuleException("Could not load all hash sets");
134  }
135 
136  initializeHashsets(hashDbManager.getAllHashSets());
137 
138  if (refCounter.incrementAndGet(jobId) == 1) {
139  // initialize job totals
140  getTotalsForIngestJobs(jobId);
141 
142  // if first module for this job then post error msgs if needed
143  if (knownBadHashSets.isEmpty()) {
146  Bundle.HashDbIngestModule_noKnownBadHashDbSetMsg(),
147  Bundle.HashDbIngestModule_knownBadFileSearchWillNotExecuteWarn()));
148  }
149 
150  if (noChangeHashSets.isEmpty()) {
153  Bundle.HashDbIngestModule_noChangeHashDbSetMsg(),
154  Bundle.HashDbIngestModule_noChangeFileSearchWillNotExecuteWarn()));
155  }
156 
157  if (knownHashSets.isEmpty()) {
160  Bundle.HashDbIngestModule_noKnownHashDbSetMsg(),
161  Bundle.HashDbIngestModule_knownFileSearchWillNotExecuteWarn()));
162  }
163  }
164  }
165 
172  private void initializeHashsets(List<HashDb> allHashSets) {
173  for (HashDb db : allHashSets) {
174  if (settings.isHashSetEnabled(db)) {
175  try {
176  if (db.isValid()) {
177  switch (db.getKnownFilesType()) {
178  case KNOWN:
179  knownHashSets.add(db);
180  break;
181  case KNOWN_BAD:
182  knownBadHashSets.add(db);
183  break;
184  case NO_CHANGE:
185  noChangeHashSets.add(db);
186  break;
187  default:
188  throw new TskCoreException("Unknown KnownFilesType: " + db.getKnownFilesType());
189  }
190  }
191  } catch (TskCoreException ex) {
192  logger.log(Level.WARNING, "Error getting index status for " + db.getDisplayName() + " hash set", ex); //NON-NLS
193  }
194  }
195  }
196  }
197 
198  @Messages({
199  "# {0} - File name",
200  "HashDbIngestModule.dialogTitle.errorFindingArtifacts=Error Finding Artifacts: {0}",
201  "# {0} - File name",
202  "HashDbIngestModule.errorMessage.lookingForFileArtifacts=Error encountered while looking for existing artifacts for {0}."
203  })
204  @Override
205  public ProcessResult process(AbstractFile file) {
206  try {
207  blackboard = Case.getCurrentCaseThrows().getSleuthkitCase().getBlackboard();
208  } catch (NoCurrentCaseException ex) {
209  logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS
210  return ProcessResult.ERROR;
211  }
212 
213  if (shouldSkip(file)) {
214  return ProcessResult.OK;
215  }
216 
217  // Safely get a reference to the totalsForIngestJobs object
218  IngestJobTotals totals = getTotalsForIngestJobs(jobId);
219 
220  // calc hash value
221  String md5Hash = getHash(file, totals);
222  if (md5Hash == null) {
223  return ProcessResult.ERROR;
224  }
225 
226  // the processing result of handling this file
228 
229  // look up in notable first
230  FindInHashsetsResult knownBadResult = findInHashsets(file, totals.totalKnownBadCount,
231  totals.totalLookuptime, knownBadHashSets, TskData.FileKnown.BAD, knownBadLookupError);
232 
233  boolean foundBad = knownBadResult.isFound();
234  if (knownBadResult.isError()) {
235  ret = ProcessResult.ERROR;
236  }
237 
238  // look up no change items next
239  FindInHashsetsResult noChangeResult = findInHashsets(file, totals.totalNoChangeCount,
240  totals.totalLookuptime, noChangeHashSets, TskData.FileKnown.UNKNOWN, noChangeLookupError);
241 
242  if (noChangeResult.isError()) {
243  ret = ProcessResult.ERROR;
244  }
245 
246  // If the file is not in the notable sets, search for it in the known sets.
247  // Any hit is sufficient to classify it as known, and there is no need to create
248  // a hit artifact or send a message to the application inbox.
249  if (!foundBad) {
250  for (HashDb db : knownHashSets) {
251  try {
252  long lookupstart = System.currentTimeMillis();
253  if (db.lookupMD5Quick(file)) {
254  file.setKnown(TskData.FileKnown.KNOWN);
255  break;
256  }
257  long delta = (System.currentTimeMillis() - lookupstart);
258  totals.totalLookuptime.addAndGet(delta);
259 
260  } catch (TskException ex) {
261  reportLookupError(ex, file, knownLookupError);
262  ret = ProcessResult.ERROR;
263  }
264  }
265  }
266 
267  return ret;
268  }
269 
277  private boolean shouldSkip(AbstractFile file) {
278  // Skip unallocated space files.
279  if ((file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)
280  || file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) {
281  return true;
282  }
283 
284  /*
285  * Skip directories. One reason for this is because we won't accurately
286  * calculate hashes of NTFS directories that have content that spans the
287  * IDX_ROOT and IDX_ALLOC artifacts. So we disable that until a solution
288  * for it is developed.
289  */
290  if (file.isDir()) {
291  return true;
292  }
293 
294  // bail out if we have no hashes set
295  if ((knownHashSets.isEmpty()) && (knownBadHashSets.isEmpty()) && (!settings.shouldCalculateHashes())) {
296  return true;
297  }
298 
299  return false;
300  }
301 
311  private void reportLookupError(TskException ex, AbstractFile file, Function<AbstractFile, String> lookupErrorMessage) {
312  logger.log(Level.WARNING, String.format(
313  "Couldn't lookup notable hash for file '%s' (id=%d) - see sleuthkit log for details", file.getName(), file.getId()), ex); //NON-NLS
316  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.hashLookupErrorMsg", file.getName()),
317  lookupErrorMessage.apply(file)));
318  }
319 
323  private static class FindInHashsetsResult {
324 
325  private final boolean found;
326  private final boolean error;
327 
328  FindInHashsetsResult(boolean found, boolean error) {
329  this.found = found;
330  this.error = error;
331  }
332 
338  boolean isFound() {
339  return found;
340  }
341 
349  boolean isError() {
350  return error;
351  }
352  }
353 
372  private FindInHashsetsResult findInHashsets(AbstractFile file, AtomicLong totalCount, AtomicLong totalLookupTime,
373  List<HashDb> hashSets, TskData.FileKnown statusIfFound, Function<AbstractFile, String> lookupErrorMessage) {
374 
375  boolean found = false;
376  boolean wasError = false;
377  for (HashDb db : hashSets) {
378  try {
379  long lookupstart = System.currentTimeMillis();
380  HashHitInfo hashInfo = db.lookupMD5(file);
381  if (null != hashInfo) {
382  found = true;
383 
384  totalCount.incrementAndGet();
385  file.setKnown(statusIfFound);
386  String hashSetName = db.getDisplayName();
387  String comment = generateComment(hashInfo);
388  if (!createArtifactIfNotExists(hashSetName, file, comment, db)) {
389  wasError = true;
390  }
391  }
392  long delta = (System.currentTimeMillis() - lookupstart);
393  totalLookupTime.addAndGet(delta);
394 
395  } catch (TskException ex) {
396  reportLookupError(ex, file, lookupErrorMessage);
397  wasError = true;
398  }
399  }
400 
401  return new FindInHashsetsResult(found, wasError);
402  }
403 
411  private String generateComment(HashHitInfo hashInfo) {
412  String comment = "";
413  ArrayList<String> comments = hashInfo.getComments();
414  int i = 0;
415  for (String c : comments) {
416  if (++i > 1) {
417  comment += " ";
418  }
419  comment += c;
420  if (comment.length() > MAX_COMMENT_SIZE) {
421  comment = comment.substring(0, MAX_COMMENT_SIZE) + "...";
422  break;
423  }
424  }
425  return comment;
426  }
427 
438  private boolean createArtifactIfNotExists(String hashSetName, AbstractFile file, String comment, HashDb db) {
439  /*
440  * We have a match. Now create an artifact if it is determined that one
441  * hasn't been created yet.
442  */
443  List<BlackboardAttribute> attributesList = new ArrayList<>();
444  attributesList.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, HashLookupModuleFactory.getModuleName(), hashSetName));
445  try {
446  Blackboard tskBlackboard = skCase.getBlackboard();
447  if (tskBlackboard.artifactExists(file, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, attributesList) == false) {
448  postHashSetHitToBlackboard(file, file.getMd5Hash(), hashSetName, comment, db.getSendIngestMessages());
449  }
450  } catch (TskCoreException ex) {
451  logger.log(Level.SEVERE, String.format(
452  "A problem occurred while checking for existing artifacts for file '%s' (id=%d).", file.getName(), file.getId()), ex); //NON-NLS
455  Bundle.HashDbIngestModule_dialogTitle_errorFindingArtifacts(file.getName()),
456  Bundle.HashDbIngestModule_errorMessage_lookingForFileArtifacts(file.getName())));
457  return false;
458  }
459  return true;
460  }
461 
472  private String getHash(AbstractFile file, IngestJobTotals totals) {
473  String md5Hash = file.getMd5Hash();
474  if (md5Hash != null && md5Hash.isEmpty()) {
475  return md5Hash;
476  }
477 
478  try {
479  TimingMetric metric = HealthMonitor.getTimingMetric("Disk Reads: Hash calculation");
480  long calcstart = System.currentTimeMillis();
481  md5Hash = HashUtility.calculateMd5Hash(file);
482  if (file.getSize() > 0) {
483  // Surprisingly, the hash calculation does not seem to be correlated that
484  // strongly with file size until the files get large.
485  // Only normalize if the file size is greater than ~1MB.
486  if (file.getSize() < 1000000) {
488  } else {
489  // In testing, this normalization gave reasonable resuls
490  HealthMonitor.submitNormalizedTimingMetric(metric, file.getSize() / 500000);
491  }
492  }
493  file.setMd5Hash(md5Hash);
494  long delta = (System.currentTimeMillis() - calcstart);
495  totals.totalCalctime.addAndGet(delta);
496  return md5Hash;
497  } catch (IOException ex) {
498  logger.log(Level.WARNING, String.format("Error calculating hash of file '%s' (id=%d).", file.getName(), file.getId()), ex); //NON-NLS
501  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.fileReadErrorMsg", file.getName()),
502  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.calcHashValueErr",
503  file.getParentPath() + file.getName(),
504  file.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC) ? "Allocated File" : "Deleted File")));
505  return null;
506  }
507  }
508 
519  @Messages({
520  "HashDbIngestModule.indexError.message=Failed to index hashset hit artifact for keyword search."
521  })
522  private void postHashSetHitToBlackboard(AbstractFile abstractFile, String md5Hash, String hashSetName, String comment, boolean showInboxMessage) {
523  try {
524  String moduleName = HashLookupModuleFactory.getModuleName();
525  BlackboardArtifact badFile = abstractFile.newArtifact(ARTIFACT_TYPE.TSK_HASHSET_HIT);
526  Collection<BlackboardAttribute> attributes = new ArrayList<>();
527  //TODO Revisit usage of deprecated constructor as per TSK-583
528  //BlackboardAttribute att2 = new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID(), MODULE_NAME, "Known Bad", hashSetName);
529  attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, moduleName, hashSetName));
530  attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_HASH_MD5, moduleName, md5Hash));
531  attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COMMENT, moduleName, comment));
532 
533  badFile.addAttributes(attributes);
534 
535  try {
536  /*
537  * post the artifact which will index the artifact for keyword
538  * search, and fire an event to notify UI of this new artifact
539  */
540  blackboard.postArtifact(badFile, moduleName);
541  } catch (Blackboard.BlackboardException ex) {
542  logger.log(Level.SEVERE, "Unable to index blackboard artifact " + badFile.getArtifactID(), ex); //NON-NLS
544  Bundle.HashDbIngestModule_indexError_message(), badFile.getDisplayName());
545  }
546 
547  if (showInboxMessage) {
548  StringBuilder detailsSb = new StringBuilder();
549  //details
550  detailsSb.append("<table border='0' cellpadding='4' width='280'>"); //NON-NLS
551  //hit
552  detailsSb.append("<tr>"); //NON-NLS
553  detailsSb.append("<th>") //NON-NLS
554  .append(NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.fileName"))
555  .append("</th>"); //NON-NLS
556  detailsSb.append("<td>") //NON-NLS
557  .append(abstractFile.getName())
558  .append("</td>"); //NON-NLS
559  detailsSb.append("</tr>"); //NON-NLS
560 
561  detailsSb.append("<tr>"); //NON-NLS
562  detailsSb.append("<th>") //NON-NLS
563  .append(NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.md5Hash"))
564  .append("</th>"); //NON-NLS
565  detailsSb.append("<td>").append(md5Hash).append("</td>"); //NON-NLS
566  detailsSb.append("</tr>"); //NON-NLS
567 
568  detailsSb.append("<tr>"); //NON-NLS
569  detailsSb.append("<th>") //NON-NLS
570  .append(NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.hashsetName"))
571  .append("</th>"); //NON-NLS
572  detailsSb.append("<td>").append(hashSetName).append("</td>"); //NON-NLS
573  detailsSb.append("</tr>"); //NON-NLS
574 
575  detailsSb.append("</table>"); //NON-NLS
576 
578  NbBundle.getMessage(this.getClass(), "HashDbIngestModule.postToBB.knownBadMsg", abstractFile.getName()),
579  detailsSb.toString(),
580  abstractFile.getName() + md5Hash,
581  badFile));
582  }
583  } catch (TskException ex) {
584  logger.log(Level.WARNING, "Error creating blackboard artifact", ex); //NON-NLS
585  }
586  }
587 
596  @Messages("HashDbIngestModule.complete.noChangesFound=No Change items found:")
597  private static synchronized void postSummary(long jobId, List<HashDb> knownBadHashSets,
598  List<HashDb> noChangeHashSets, List<HashDb> knownHashSets) {
599 
600  IngestJobTotals jobTotals = getTotalsForIngestJobs(jobId);
601  totalsForIngestJobs.remove(jobId);
602 
603  if ((!knownBadHashSets.isEmpty()) || (!knownHashSets.isEmpty()) || (!noChangeHashSets.isEmpty())) {
604  StringBuilder detailsSb = new StringBuilder();
605  //details
606  detailsSb.append(
607  "<table border='0' cellpadding='4' width='280'>" +
608  "<tr><td>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.knownBadsFound") + "</td>" +
609  "<td>" + jobTotals.totalKnownBadCount.get() + "</td></tr>" +
610 
611  "<tr><td>" + Bundle.HashDbIngestModule_complete_noChangesFound() + "</td>" +
612  "<td>" + jobTotals.totalNoChangeCount.get() + "</td></tr>" +
613 
614  "<tr><td>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.totalCalcTime") +
615  "</td><td>" + jobTotals.totalCalctime.get() + "</td></tr>\n" +
616 
617  "<tr><td>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.totalLookupTime") +
618  "</td><td>" + jobTotals.totalLookuptime.get() + "</td></tr>\n</table>" +
619 
620  "<p>" + NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.databasesUsed") + "</p>\n<ul>"); //NON-NLS
621 
622  Stream.concat(knownBadHashSets.stream(), noChangeHashSets.stream()).forEach((db) -> {
623  detailsSb.append("<li>" + db.getHashSetName() + "</li>\n"); //NON-NLS
624  });
625 
626  detailsSb.append("</ul>"); //NON-NLS
627 
631  NbBundle.getMessage(HashDbIngestModule.class, "HashDbIngestModule.complete.hashLookupResults"),
632  detailsSb.toString()));
633  }
634  }
635 
636  @Override
637  public void shutDown() {
638  if (refCounter.decrementAndGet(jobId) == 0) {
639  postSummary(jobId, knownBadHashSets, noChangeHashSets, knownHashSets);
640  }
641  }
642 }
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: Mon Jul 6 2020
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.