Autopsy  4.20.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
MalwareScanIngestModule.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2023 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 com.basistech.df.cybertriage.autopsy.malwarescan;
20 
27 import java.text.MessageFormat;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Optional;
34 import java.util.Set;
35 import java.util.logging.Level;
36 import java.util.stream.Collectors;
37 import java.util.stream.Stream;
38 import org.apache.commons.collections4.CollectionUtils;
39 import org.apache.commons.lang3.StringUtils;
40 import org.openide.util.NbBundle.Messages;
48 import org.sleuthkit.datamodel.AbstractFile;
49 import org.sleuthkit.datamodel.AnalysisResult;
50 import org.sleuthkit.datamodel.Blackboard;
51 import org.sleuthkit.datamodel.BlackboardArtifact;
52 import org.sleuthkit.datamodel.HashUtility;
53 import org.sleuthkit.datamodel.HashUtility.HashResult;
54 import org.sleuthkit.datamodel.HashUtility.HashType;
55 import org.sleuthkit.datamodel.Score;
56 import org.sleuthkit.datamodel.SleuthkitCase;
57 import org.sleuthkit.datamodel.TskCoreException;
58 import org.sleuthkit.datamodel.TskData;
59 
63 public class MalwareScanIngestModule implements FileIngestModule {
64 
65  private static final SharedProcessing sharedProcessing = new SharedProcessing();
66 
67  @Override
68  public void startUp(IngestJobContext context) throws IngestModuleException {
69  sharedProcessing.startUp(context);
70  }
71 
72  @Override
73  public ProcessResult process(AbstractFile af) {
74  return sharedProcessing.process(af);
75  }
76 
77  @Override
78  public void shutDown() {
79  sharedProcessing.shutDown();
80  }
81 
86  private static class SharedProcessing {
87 
88  // batch size of 200 files max
89  private static final int BATCH_SIZE = 200;
90  // 1 day timeout for all API requests
91  private static final long FLUSH_SECS_TIMEOUT = 24 * 60 * 60;
92 
93  //minimum lookups left before issuing warning
94  private static final long LOW_LOOKUPS_REMAINING = 250;
95 
96  private static final Set<String> EXECUTABLE_MIME_TYPES = Stream.of(
97  "application/x-bat",//NON-NLS
98  "application/x-dosexec",//NON-NLS
99  "application/vnd.microsoft.portable-executable",//NON-NLS
100  "application/x-msdownload",//NON-NLS
101  "application/exe",//NON-NLS
102  "application/x-exe",//NON-NLS
103  "application/dos-exe",//NON-NLS
104  "vms/exe",//NON-NLS
105  "application/x-winexe",//NON-NLS
106  "application/msdos-windows",//NON-NLS
107  "application/x-msdos-program"//NON-NLS
108  ).collect(Collectors.toSet());
109 
110  private static final String MALWARE_TYPE_NAME = "TSK_MALWARE";
111  private static final String MALWARE_CONFIG = "Cyber Triage Cloud";
112 
113  private static final Logger logger = Logger.getLogger(MalwareScanIngestModule.class.getName());
115 
118 
119  private RunState runState = null;
120 
121  private SleuthkitCase tskCase = null;
123  private LicenseInfo licenseInfo = null;
124  private BlackboardArtifact.Type malwareType = null;
125  private long dsId = 0;
126  private long ingestJobId = 0;
127 
128  @Messages({
129  "MalwareScanIngestModule_ShareProcessing_lowLimitWarning_title=Hash Lookups Low",
130  "# {0} - remainingLookups",
131  "MalwareScanIngestModule_ShareProcessing_lowLimitWarning_desc=This license only has {0} lookups remaining",
132  "MalwareScanIngestModule_malwareTypeDisplayName=Malware",
133  "MalwareScanIngestModule_ShareProcessing_noLicense_title=No Cyber Triage License",
134  "MalwareScanIngestModule_ShareProcessing_noLicense_desc=No Cyber Triage license could be loaded. Cyber Triage processing will be disabled.",
135  "MalwareScanIngestModule_ShareProcessing_noRemaining_title=No remaining lookups",
136  "MalwareScanIngestModule_ShareProcessing_noRemaining_desc=There are no more remaining hash lookups for this license at this time. Cyber Triage processing will be disabled."
137  })
138  synchronized void startUp(IngestJobContext context) throws IngestModuleException {
139  // only run this code once per startup
140  if (runState == RunState.STARTED_UP || runState == RunState.DISABLED) {
141  return;
142  }
143 
144  try {
145  // get saved license
146  Optional<LicenseInfo> licenseInfoOpt = ctSettingsPersistence.loadLicenseInfo();
147  if (licenseInfoOpt.isEmpty() || licenseInfoOpt.get().getDecryptedLicense() == null) {
149  Bundle.MalwareScanIngestModule_ShareProcessing_noLicense_title(),
150  Bundle.MalwareScanIngestModule_ShareProcessing_noLicense_desc(),
151  null);
152  runState = RunState.DISABLED;
153  return;
154  }
155 
156  AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(licenseInfoOpt.get().getDecryptedLicense());
157  // syncronously fetch malware scans info
158 
159  // determine lookups remaining
160  long lookupsRemaining = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount());
161  if (lookupsRemaining <= 0) {
163  Bundle.MalwareScanIngestModule_ShareProcessing_noRemaining_title(),
164  Bundle.MalwareScanIngestModule_ShareProcessing_noRemaining_desc(),
165  null);
166  runState = RunState.DISABLED;
167  return;
168  } else if (lookupsRemaining < LOW_LOOKUPS_REMAINING) {
170  Bundle.MalwareScanIngestModule_ShareProcessing_lowLimitWarning_title(),
171  Bundle.MalwareScanIngestModule_ShareProcessing_lowLimitWarning_desc(lookupsRemaining),
172  null);
173  }
174 
175  // setup necessary variables for processing
176  tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
177  malwareType = tskCase.getBlackboard().getOrAddArtifactType(
178  MALWARE_TYPE_NAME,
179  Bundle.MalwareScanIngestModule_malwareTypeDisplayName(),
180  BlackboardArtifact.Category.ANALYSIS_RESULT);
181  fileTypeDetector = new FileTypeDetector();
182  dsId = context.getDataSource().getId();
183  ingestJobId = context.getJobId();
184  licenseInfo = licenseInfoOpt.get();
185 
186  // set run state to initialized
187  runState = RunState.STARTED_UP;
188  } catch (Exception ex) {
189  runState = RunState.DISABLED;
190  throw new IngestModuleException("An exception occurred on MalwareScanIngestModule startup", ex);
191  }
192  }
193 
194  private static long remaining(Long limit, Long used) {
195  limit = limit == null ? 0 : limit;
196  used = used == null ? 0 : used;
197  return limit - used;
198  }
199 
200  private String getOrCalcHash(AbstractFile af) {
201  if (StringUtils.isNotBlank(af.getMd5Hash())) {
202  return af.getMd5Hash();
203  }
204 
205  try {
206  List<HashResult> hashResults = HashUtility.calculateHashes(af, Collections.singletonList(HashType.MD5));
207  if (CollectionUtils.isNotEmpty(hashResults)) {
208  for (HashResult hashResult : hashResults) {
209  if (hashResult.getType() == HashType.MD5) {
210  return hashResult.getValue();
211  }
212  }
213  }
214  } catch (TskCoreException ex) {
215  logger.log(Level.WARNING,
216  MessageFormat.format("An error occurred while processing file name: {0} and obj id: {1}.",
217  af.getName(),
218  af.getId()),
219  ex);
220  }
221 
222  return null;
223  }
224 
225  @Messages({
226  "MalwareScanIngestModule_ShareProcessing_batchTimeout_title=Batch Processing Timeout",
227  "MalwareScanIngestModule_ShareProcessing_batchTimeout_desc=Batch processing timed out"
228  })
229  IngestModule.ProcessResult process(AbstractFile af) {
230  try {
231  if (runState == RunState.STARTED_UP
232  && af.getKnown() != TskData.FileKnown.KNOWN
233  && EXECUTABLE_MIME_TYPES.contains(StringUtils.defaultString(fileTypeDetector.getMIMEType(af)).trim().toLowerCase())
234  && CollectionUtils.isEmpty(af.getAnalysisResults(malwareType))) {
235 
236  String md5 = getOrCalcHash(af);
237  if (StringUtils.isNotBlank(md5)) {
238  batchProcessor.add(new FileRecord(af.getId(), md5));
239  }
240  }
241  return ProcessResult.OK;
242  } catch (TskCoreException ex) {
244  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
245  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
246  ex);
247  return IngestModule.ProcessResult.ERROR;
248  } catch (InterruptedException ex) {
250  Bundle.MalwareScanIngestModule_ShareProcessing_batchTimeout_title(),
251  Bundle.MalwareScanIngestModule_ShareProcessing_batchTimeout_desc(),
252  ex);
253  return IngestModule.ProcessResult.ERROR;
254  }
255  }
256 
257  @Messages({
258  "MalwareScanIngestModule_SharedProcessing_authTokenResponseError_title=Authentication API error",
259  "# {0} - errorResponse",
260  "MalwareScanIngestModule_SharedProcessing_authTokenResponseError_desc=Received error: ''{0}'' when fetching the API authentication token for the license",
261  "MalwareScanIngestModule_SharedProcessing_repServicenResponseError_title=Lookup API error",
262  "# {0} - errorResponse",
263  "MalwareScanIngestModule_SharedProcessing_repServicenResponseError_desc=Received error: ''{0}'' when fetching hash lookup results",
264  "MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title=Hash Lookups Exhausted",
265  "MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc=The remaining hash lookups for this license have been exhausted",
266  "MalwareScanIngestModule_SharedProcessing_generalProcessingError_title=Hash Lookup Error",
267  "MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc=An error occurred while processing hash lookup results",})
268  private void handleBatch(List<FileRecord> fileRecords) {
269  if (runState != RunState.STARTED_UP || fileRecords == null || fileRecords.isEmpty()) {
270  return;
271  }
272 
273  // create mapping of md5 to corresponding object ids as well as just the list of md5's
274  Map<String, List<Long>> md5ToObjId = new HashMap<>();
275 
276  for (FileRecord fr : fileRecords) {
277  if (fr == null || StringUtils.isBlank(fr.getMd5hash()) || fr.getObjId() <= 0) {
278  continue;
279  }
280 
281  String sanitizedMd5 = sanitizedMd5(fr.getMd5hash());
282  md5ToObjId
283  .computeIfAbsent(sanitizedMd5, (k) -> new ArrayList<>())
284  .add(fr.getObjId());
285 
286  }
287 
288  List<String> md5Hashes = new ArrayList<>(md5ToObjId.keySet());
289 
290  if (md5Hashes.isEmpty()) {
291  return;
292  }
293 
294  try {
295  // get an auth token with the license
296  AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(licenseInfo.getDecryptedLicense());
297 
298  // make sure we are in bounds for the remaining scans
299  long remainingScans = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount());
300  if (remainingScans <= 0) {
301  runState = RunState.DISABLED;
303  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title(),
304  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc(),
305  null);
306  return;
307  }
308 
309  // using auth token, get results
310  List<CTCloudBean> repResult = ctApiDAO.getReputationResults(
311  new AuthenticatedRequestData(licenseInfo.getDecryptedLicense(), authTokenResponse),
312  md5Hashes
313  );
314 
315  List<BlackboardArtifact> createdArtifacts = new ArrayList<>();
316  if (!CollectionUtils.isEmpty(repResult)) {
317  SleuthkitCase.CaseDbTransaction trans = null;
318  try {
319  trans = tskCase.beginTransaction();
320  for (CTCloudBean result : repResult) {
321  String sanitizedMd5 = sanitizedMd5(result.getMd5HashValue());
322  List<Long> objIds = md5ToObjId.remove(sanitizedMd5);
323  if (objIds == null || objIds.isEmpty()) {
324  continue;
325  }
326 
327  for (Long objId : objIds) {
328  AnalysisResult res = createAnalysisResult(objId, result, trans);
329  if (res != null) {
330  createdArtifacts.add(res);
331  }
332  }
333  }
334 
335  trans.commit();
336  trans = null;
337  } finally {
338  if (trans != null) {
339  trans.rollback();
340  createdArtifacts.clear();
341  trans = null;
342  }
343  }
344 
345  if (!CollectionUtils.isEmpty(createdArtifacts)) {
346  tskCase.getBlackboard().postArtifacts(createdArtifacts, Bundle.MalwareScanIngestModuleFactory_displayName(), ingestJobId);
347  }
348  }
349  } catch (Exception ex) {
351  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
352  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
353  ex);
354  }
355  }
356 
357  private String sanitizedMd5(String orig) {
358  return StringUtils.defaultString(orig).trim().toLowerCase();
359  }
360 
361  @Messages({
362  "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes=YES",
363  "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No=NO"
364  })
365  private AnalysisResult createAnalysisResult(Long objId, CTCloudBean cloudBean, SleuthkitCase.CaseDbTransaction trans) throws Blackboard.BlackboardException {
366  if (objId == null || cloudBean == null || cloudBean.getMalwareResult() == null) {
367  return null;
368  }
369 
370  Score score = cloudBean.getMalwareResult().getCTScore() == null
371  ? Score.SCORE_UNKNOWN
372  : cloudBean.getMalwareResult().getCTScore().getTskCore();
373 
374  String conclusion = score.getSignificance() == Score.Significance.NOTABLE || score.getSignificance() == Score.Significance.LIKELY_NOTABLE
375  ? Bundle.MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes()
376  : Bundle.MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No();
377 
378  String justification = cloudBean.getMalwareResult().getStatusDescription();
379 
380  return tskCase.getBlackboard().newAnalysisResult(
381  malwareType,
382  objId,
383  dsId,
384  score,
385  conclusion,
386  MALWARE_CONFIG,
387  justification,
388  Collections.emptyList(),
389  trans).getAnalysisResult();
390  }
391 
392  @Messages({
393  "MalwareScanIngestModule_SharedProcessing_flushTimeout_title=Processing Timeout",
394  "MalwareScanIngestModule_SharedProcessing_flushTimeout_desc=A timeout occurred while finishing processing"
395  })
396  synchronized void shutDown() {
397  // if already shut down, return
398  if (runState == RunState.SHUT_DOWN) {
399  return;
400  }
401 
402  // flush any remaining items
403  try {
404  batchProcessor.flushAndReset();
405  } catch (InterruptedException ex) {
407  Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_title(),
408  Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_desc(),
409  ex);
410  } finally {
411  // set state to shut down and clear any remaining
412  runState = RunState.SHUT_DOWN;
413  }
414  }
415 
416  private void notifyWarning(String title, String message, Exception ex) {
417  MessageNotifyUtil.Notify.warn(title, message);
418  logger.log(Level.WARNING, message, ex);
419  }
420 
421  private enum RunState {
422  STARTED_UP, DISABLED, SHUT_DOWN
423  }
424 
425  class FileRecord {
426 
427  private final long objId;
428  private final String md5hash;
429 
430  FileRecord(long objId, String md5hash) {
431  this.objId = objId;
432  this.md5hash = md5hash;
433  }
434 
435  long getObjId() {
436  return objId;
437  }
438 
439  String getMd5hash() {
440  return md5hash;
441  }
442 
443  }
444  }
445 }
List< CTCloudBean > getReputationResults(AuthenticatedRequestData authenticatedRequestData, List< String > md5Hashes)
Definition: CTApiDAO.java:96
AuthTokenResponse getAuthToken(DecryptedLicenseResponse decrypted)
Definition: CTApiDAO.java:76
AnalysisResult createAnalysisResult(Long objId, CTCloudBean cloudBean, SleuthkitCase.CaseDbTransaction trans)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static void warn(String title, String message)

Copyright © 2012-2022 Basis Technology. Generated on: Tue Aug 1 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.