Autopsy  4.21.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 
31 import java.security.MessageDigest;
32 import java.security.NoSuchAlgorithmException;
33 import java.text.MessageFormat;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.HexFormat;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Optional;
41 import java.util.Set;
42 import java.util.logging.Level;
43 import java.util.stream.Collectors;
44 import java.util.stream.Stream;
45 import org.apache.commons.collections4.CollectionUtils;
46 import org.apache.commons.collections4.MapUtils;
47 import org.apache.commons.lang3.StringUtils;
48 import org.apache.curator.shaded.com.google.common.collect.Lists;
49 import org.openide.util.NbBundle.Messages;
69 
73 class MalwareScanIngestModule implements FileIngestModule {
74 
75  private static final SharedProcessing sharedProcessing = new SharedProcessing();
76  private boolean uploadFiles;
77  private boolean queryFiles;
78 
79  MalwareScanIngestModule(MalwareScanIngestSettings settings) {
80  uploadFiles = settings.shouldUploadFiles();
81  queryFiles = settings.shouldQueryFiles();
82  }
83 
84  @Override
85  public void startUp(IngestJobContext context) throws IngestModuleException {
86  sharedProcessing.startUp(context, uploadFiles);
87  }
88 
89  @Override
90  public ProcessResult process(AbstractFile af) {
91  return sharedProcessing.process(af);
92  }
93 
94  @Override
95  public void shutDown() {
96  sharedProcessing.shutDown();
97  }
98 
103  private static class SharedProcessing {
104 
105  // batch size of 200 files max
106  private static final int BATCH_SIZE = 200;
107  // 1 day timeout for all API requests
108  private static final long FLUSH_SECS_TIMEOUT = 24 * 60 * 60;
109 
110  //minimum lookups left before issuing warning
111  private static final long LOW_LOOKUPS_REMAINING = 250;
112 
113  //minimum file uploads left before issuing warning
114  private static final long LOW_UPLOADS_REMAINING = 25;
115 
116  // min and max upload size in bytes
117  private static final long MIN_UPLOAD_SIZE = 1;
118  private static final long MAX_UPLOAD_SIZE = 100_000_000; // 100MB
119 
120  private static final int NUM_FILE_UPLOAD_RETRIES = 7;
121  private static final long FILE_UPLOAD_RETRY_SLEEP_MILLIS = 60 * 1000;
122 
123  private static final Set<String> EXECUTABLE_MIME_TYPES = Stream.of(
124  "application/x-bat",//NON-NLS
125  "application/x-dosexec",//NON-NLS
126  "application/vnd.microsoft.portable-executable",//NON-NLS
127  "application/x-msdownload",//NON-NLS
128  "application/exe",//NON-NLS
129  "application/x-exe",//NON-NLS
130  "application/dos-exe",//NON-NLS
131  "vms/exe",//NON-NLS
132  "application/x-winexe",//NON-NLS
133  "application/msdos-windows",//NON-NLS
134  "application/x-msdos-program"//NON-NLS
135  ).collect(Collectors.toSet());
136 
137  private static final String MALWARE_CONFIG = ""; // NOTE: Adding a configuration complicates NTL branch UI
138 
139  private static final Logger logger = Logger.getLogger(MalwareScanIngestModule.class.getName());
140 
141  private final BatchProcessor<FileRecord> batchProcessor = new BatchProcessor<FileRecord>(
142  BATCH_SIZE,
145 
148 
149  private IngestJobState ingestJobState = null;
150 
151  @Messages({
152  "MalwareScanIngestModule_malwareTypeDisplayName=Malware",
153  "MalwareScanIngestModule_ShareProcessing_noLicense_title=No Cyber Triage License",
154  "MalwareScanIngestModule_ShareProcessing_noLicense_desc=No Cyber Triage license could be loaded. Cyber Triage processing will be disabled.",
155  "MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_title=No remaining lookups",
156  "MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_desc=There are no more remaining hash lookups for this license at this time. Malware scanning will be disabled.",
157  "MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_title=Hash Lookups Low",
158  "# {0} - remainingLookups",
159  "MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_desc=This license only has {0} lookups remaining.",
160  "MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_title=No remaining file uploads",
161  "MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_desc=There are no more remaining file uploads for this license at this time. File uploading will be disabled.",
162  "MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_title=File Uploads Limit Low",
163  "# {0} - remainingUploads",
164  "MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_desc=This license only has {0} file uploads remaining.",
165  "MalwareScanIngestModule_ShareProcessing_startup_generalException_desc=An exception occurred on MalwareScanIngestModule startup. See the log for more information.",
166  "MalwareScanIngestModule_ShareProcessing_startup_invalidLicenseWarning_title=Invalid License",
167  "MalwareScanIngestModule_ShareProcessing_startup_invalidLicenseWarning_desc=The current Cyber Triage license is no longer valid. Please remove the license from the Cyber Triage options panel."})
168  synchronized void startUp(IngestJobContext context, boolean uploadFiles) throws IngestModuleException {
169  // only run this code once per startup
170  if (ingestJobState != null) {
171  return;
172  }
173 
174  try {
175  ingestJobState = getNewJobState(context, uploadFiles);
176  } catch (CTCloudException cloudEx) {
177  ingestJobState = IngestJobState.DISABLED;
178  logger.log(Level.WARNING, "An error occurred while starting the MalwareScanIngestModule.", cloudEx);
179  throw new IngestModuleException(cloudEx.getErrorDetails(), cloudEx);
180  } catch (IllegalStateException stateEx) {
181  ingestJobState = IngestJobState.DISABLED;
182  logger.log(Level.WARNING, "An error occurred while starting the MalwareScanIngestModule.", stateEx);
183  throw new IngestModuleException(stateEx.getMessage(), stateEx);
184  } catch (Exception ex) {
185  ingestJobState = IngestJobState.DISABLED;
186  logger.log(Level.WARNING, "An error occurred while starting the MalwareScanIngestModule.", ex);
187  throw new IngestModuleException(Bundle.MalwareScanIngestModule_ShareProcessing_startup_generalException_desc(), ex);
188  }
189  }
190 
199  private IngestJobState getNewJobState(IngestJobContext context, boolean uploadFiles) throws Exception {
200  // get saved license
201  Optional<LicenseInfo> licenseInfoOpt = ctSettingsPersistence.loadLicenseInfo();
202  if (licenseInfoOpt.isEmpty() || licenseInfoOpt.get().getDecryptedLicense() == null) {
203  throw new IllegalStateException(Bundle.MalwareScanIngestModule_ShareProcessing_noLicense_desc());
204  }
205 
206  AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(licenseInfoOpt.get().getDecryptedLicense());
207  // syncronously fetch malware scans info
208 
209  // determine lookups remaining
210  long lookupsRemaining = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount());
211  if (lookupsRemaining <= 0) {
213  Bundle.MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_title(),
214  Bundle.MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_desc(),
215  null);
216 
217  return IngestJobState.DISABLED;
218  } else if (lookupsRemaining < LOW_LOOKUPS_REMAINING) {
220  Bundle.MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_title(),
221  Bundle.MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_desc(lookupsRemaining),
222  null);
223  }
224 
225  // determine lookups remaining
226  if (uploadFiles) {
227  long uploadsRemaining = remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount());
228  if (uploadsRemaining <= 0) {
230  Bundle.MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_title(),
231  Bundle.MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_desc(),
232  null);
233  uploadFiles = false;
234  } else if (lookupsRemaining < LOW_UPLOADS_REMAINING) {
236  Bundle.MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_title(),
237  Bundle.MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_desc(lookupsRemaining),
238  null);
239  }
240  }
241 
242  // setup necessary variables for processing
244  return new IngestJobState(
245  context,
246  tskCase,
247  new PathNormalizer(tskCase),
248  new FileTypeDetector(),
249  licenseInfoOpt.get(),
251  uploadFiles,
252  true
253  );
254  }
255 
263  private static long remaining(Long limit, Long used) {
264  limit = limit == null ? 0 : limit;
265  used = used == null ? 0 : used;
266  return limit - used;
267  }
268 
275  private static String getOrCalcHash(AbstractFile af, HashType hashType) {
276  switch (hashType) {
277  case MD5:
278  if (StringUtils.isNotBlank(af.getMd5Hash())) {
279  return af.getMd5Hash();
280  }
281  break;
282  case SHA256:
283  if (StringUtils.isNotBlank(af.getSha256Hash())) {
284  return af.getSha256Hash();
285  }
286  }
287 
288  try {
289  List<HashResult> hashResults = HashUtility.calculateHashes(af, Collections.singletonList(hashType));
290  if (CollectionUtils.isNotEmpty(hashResults)) {
291  for (HashResult hashResult : hashResults) {
292  if (hashResult.getType() == hashType) {
293  return hashResult.getValue();
294  }
295  }
296  }
297  } catch (TskCoreException ex) {
298  logger.log(Level.WARNING,
299  MessageFormat.format("An error occurred while processing hash for file name: {0} and obj id: {1} and hash type {2}.",
300  af.getName(),
301  af.getId(),
302  hashType.name()),
303  ex);
304  }
305 
306  return null;
307  }
308 
315  private static String getOrCalcMd5(AbstractFile af) {
316  return getOrCalcHash(af, HashType.MD5);
317  }
318 
325  private static String getOrCalcSha256(AbstractFile af) {
326  return getOrCalcHash(af, HashType.SHA256);
327  }
328 
335  private static String getOrCalcSha1(AbstractFile af) throws NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException {
336  if (StringUtils.isNotBlank(af.getSha1Hash())) {
337  return af.getSha1Hash();
338  }
339  // taken from https://stackoverflow.com/questions/6293713/java-how-to-create-sha-1-for-a-file
340  MessageDigest digest = MessageDigest.getInstance("SHA-1");
342  int n = 0;
343  byte[] buffer = new byte[8192];
344  while (n != -1) {
345  n = afStream.read(buffer);
346  if (n > 0) {
347  digest.update(buffer, 0, n);
348  }
349  }
350  byte[] hashBytes = digest.digest();
351  String hashString = HexFormat.of().formatHex(hashBytes);
352  return hashString;
353  }
354 
364  @Messages({
365  "MalwareScanIngestModule_ShareProcessing_batchTimeout_title=Batch Processing Timeout",
366  "MalwareScanIngestModule_ShareProcessing_batchTimeout_desc=Batch processing timed out"
367  })
369  try {
370  if (ingestJobState != null
371  && ingestJobState.isDoFileLookups()
372  && !ingestJobState.getIngestJobContext().fileIngestIsCancelled()
373  && af.getKnown() != TskData.FileKnown.KNOWN
374  && EXECUTABLE_MIME_TYPES.contains(StringUtils.defaultString(ingestJobState.getFileTypeDetector().getMIMEType(af)).trim().toLowerCase())
375  && CollectionUtils.isEmpty(af.getAnalysisResults(ingestJobState.getMalwareType()))) {
376 
377  String md5 = getOrCalcMd5(af);
378  if (StringUtils.isNotBlank(md5)) {
379  batchProcessor.add(new FileRecord(af.getId(), md5));
380  }
381  }
382  return ProcessResult.OK;
383  } catch (TskCoreException ex) {
385  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
386  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
387  ex);
388  return IngestModule.ProcessResult.ERROR;
389  } catch (InterruptedException ex) {
391  Bundle.MalwareScanIngestModule_ShareProcessing_batchTimeout_title(),
392  Bundle.MalwareScanIngestModule_ShareProcessing_batchTimeout_desc(),
393  ex);
394  return IngestModule.ProcessResult.ERROR;
395  }
396  }
397 
405  @Messages({
406  "MalwareScanIngestModule_SharedProcessing_authTokenResponseError_title=Authentication API error",
407  "# {0} - errorResponse",
408  "MalwareScanIngestModule_SharedProcessing_authTokenResponseError_desc=Received error: ''{0}'' when fetching the API authentication token for the license",
409  "MalwareScanIngestModule_SharedProcessing_repServicenResponseError_title=Lookup API error",
410  "# {0} - errorResponse",
411  "MalwareScanIngestModule_SharedProcessing_repServicenResponseError_desc=Received error: ''{0}'' when fetching hash lookup results",
412  "MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title=Hash Lookups Exhausted",
413  "MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc=The remaining hash lookups for this license have been exhausted",
414  "MalwareScanIngestModule_SharedProcessing_generalProcessingError_title=Hash Lookup Error",
415  "MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc=An error occurred while processing hash lookup results",})
416  private void handleBatch(IngestJobState ingestJobState, List<FileRecord> fileRecords) {
417  if (ingestJobState == null
418  || !ingestJobState.isDoFileLookups()
419  || ingestJobState.getIngestJobContext().fileIngestIsCancelled()
420  || fileRecords == null
421  || fileRecords.isEmpty()) {
422  return;
423  }
424 
425  // create mapping of md5 to corresponding object ids as well as just the list of md5's
426  Map<String, List<Long>> md5ToObjId = new HashMap<>();
427 
428  for (FileRecord fr : fileRecords) {
429  if (fr == null || StringUtils.isBlank(fr.getMd5hash()) || fr.getObjId() <= 0) {
430  continue;
431  }
432 
433  String sanitizedMd5 = normalizedMd5(fr.getMd5hash());
434  md5ToObjId
435  .computeIfAbsent(sanitizedMd5, (k) -> new ArrayList<>())
436  .add(fr.getObjId());
437  }
438 
439  List<String> md5Hashes = new ArrayList<>(md5ToObjId.keySet());
440 
441  if (md5Hashes.isEmpty()) {
442  return;
443  }
444 
445  try {
446  List<CTCloudBean> repResult = getHashLookupResults(ingestJobState, md5Hashes);
447  handleLookupResults(ingestJobState, md5ToObjId, repResult);
448  } catch (Exception ex) {
450  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
451  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
452  ex);
453  }
454  }
455 
466  @Messages({
467  "MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_title=Lookup Limits Exceeded",
468  "MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_desc=Not all files were processed because hash lookup limits were exceeded. Please try again when your limits reset.",})
469  private void handleLookupResults(IngestJobState ingestJobState, Map<String, List<Long>> md5ToObjId, List<CTCloudBean> repResult) throws Blackboard.BlackboardException, TskCoreException, TskCoreException, CTCloudException, NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException {
470  if (CollectionUtils.isEmpty(repResult)) {
471  return;
472  }
473 
474  Map<Status, List<CTCloudBean>> statusGroupings = repResult.stream()
475  .filter(bean -> bean.getMalwareResult() != null)
476  .collect(Collectors.groupingBy(bean -> bean.getMalwareResult().getStatus()));
477 
478  // for all found items, create analysis results
479  List<CTCloudBean> found = statusGroupings.get(Status.FOUND);
480  createAnalysisResults(ingestJobState, found, md5ToObjId);
481 
482  // if being scanned, check list to run later
483  handleNonFoundResults(ingestJobState, md5ToObjId, statusGroupings.get(Status.BEING_SCANNED), false);
484 
485  // if not found, try upload
486  handleNonFoundResults(ingestJobState, md5ToObjId, statusGroupings.get(Status.NOT_FOUND), true);
487 
488  // indicate a general error if some result in an error
489  if (CollectionUtils.isNotEmpty(statusGroupings.get(Status.ERROR))) {
491  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
492  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
493  null);
494  }
495 
496  // indicate some results were not processed if limits exceeded in results
497  if (CollectionUtils.isNotEmpty(statusGroupings.get(Status.LIMITS_EXCEEDED))) {
499  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_title(),
500  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_desc(),
501  null);
502  }
503  }
504 
516  private void handleNonFoundResults(IngestJobState ingestJobState, Map<String, List<Long>> md5ToObjId, List<CTCloudBean> results, boolean performFileUpload) throws CTCloudException, TskCoreException, NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException {
517  if (CollectionUtils.isNotEmpty(results)
518  && ingestJobState.isDoFileLookups()
519  && ((performFileUpload && ingestJobState.isUploadUnknownFiles()) || (!performFileUpload && ingestJobState.isQueryForMissing()))) {
520 
521  for (CTCloudBean beingScanned : CollectionUtils.emptyIfNull(results)) {
522 
523  String sanitizedMd5 = normalizedMd5(beingScanned.getMd5HashValue());
524  if (StringUtils.isBlank(sanitizedMd5)) {
525  continue;
526  }
527  List<Long> correspondingObjIds = md5ToObjId.get(sanitizedMd5);
528  if (CollectionUtils.isEmpty(correspondingObjIds)) {
529  continue;
530  }
531 
532  if (performFileUpload) {
533  uploadFile(ingestJobState, sanitizedMd5, correspondingObjIds.get(0));
534  }
535 
536  ingestJobState.getUnidentifiedHashes().put(sanitizedMd5, correspondingObjIds);
537  }
538  }
539  }
540 
551  private List<CTCloudBean> getHashLookupResults(IngestJobState ingestJobState, List<String> md5Hashes) throws CTCloudException {
552  if (ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
553  return Collections.emptyList();
554  }
555 
556  // get an auth token with the license
557  AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(ingestJobState.getLicenseInfo().getDecryptedLicense());
558 
559  // make sure we are in bounds for the remaining scans
560  long remainingScans = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount());
561  if (remainingScans <= 0) {
562  ingestJobState.disableDoFileLookups();
564  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title(),
565  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc(),
566  null);
567  return Collections.emptyList();
568  } else if (ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
569  return Collections.emptyList();
570  }
571 
572  // while we have a valid auth token, also check file uploads.
573  if (ingestJobState.isUploadUnknownFiles()) {
574  long remainingUploads = remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount());
575  if (remainingUploads <= 0) {
576  ingestJobState.disableUploadUnknownFiles();
578  Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title(),
579  Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc(),
580  null);
581  }
582  }
583 
584  // using auth token, get results
585  return ctApiDAO.getReputationResults(
586  new AuthenticatedRequestData(ingestJobState.getLicenseInfo().getDecryptedLicense(), authTokenResponse),
587  md5Hashes
588  );
589  }
590 
597  private static String normalizedMd5(String orig) {
598  return StringUtils.defaultString(orig).trim().toLowerCase();
599  }
600 
608  private static boolean isUploadable(AbstractFile af) {
609  long size = af.getSize();
610  return size >= MIN_UPLOAD_SIZE && size <= MAX_UPLOAD_SIZE;
611  }
612 
622  @Messages({
623  "MalwareScanIngestModule_uploadFile_notUploadable_title=Not Able to Upload",
624  "# {0} - objectId",
625  "MalwareScanIngestModule_uploadFile_notUploadable_desc=A file did not meet requirements for upload (object id: {0}).",
626  "MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title=No Remaining File Uploads",
627  "MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc=There are no more file uploads on this license at this time. File uploads will be disabled for remaining uploads.",})
628  private boolean uploadFile(IngestJobState ingestJobState, String md5, long objId) throws CTCloudException, TskCoreException, NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException {
629  if (!ingestJobState.isUploadUnknownFiles() || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
630  return false;
631  }
632 
633  AbstractFile af = ingestJobState.getTskCase().getAbstractFileById(objId);
634  if (af == null) {
635  return false;
636  }
637 
638  if (!isUploadable(af)) {
640  Bundle.MalwareScanIngestModule_uploadFile_notUploadable_title(),
641  Bundle.MalwareScanIngestModule_uploadFile_notUploadable_desc(objId),
642  null);
643  return false;
644  }
645 
646  // get auth token / file upload url
647  AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(ingestJobState.getLicenseInfo().getDecryptedLicense(), af.getSize());
648  if (StringUtils.isBlank(authTokenResponse.getFileUploadUrl())) {
649  throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR);
650  } else if (remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount()) <= 0) {
651  // don't proceed with upload if reached limit
652  ingestJobState.disableUploadUnknownFiles();
654  Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title(),
655  Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc(),
656  null);
657 
658  return false;
659  } else if (ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
660  return false;
661  }
662 
663  // upload bytes
664  ReadContentInputStream fileInputStream = new ReadContentInputStream(af);
665 
666  ctApiDAO.uploadFile(new FileUploadRequest()
667  .setContentLength(af.getSize())
668  .setFileInputStream(fileInputStream)
669  .setFileName(af.getName())
670  .setFullUrlPath(authTokenResponse.getFileUploadUrl())
671  );
672 
673  // upload metadata
674  MetadataUploadRequest metaRequest = new MetadataUploadRequest()
675  .setCreatedDate(af.getCrtime() == 0 ? null : af.getCrtime())
676  .setFilePath(ingestJobState.getPathNormalizer().normalizePath(af.getUniquePath()))
678  .setFileUploadUrl(authTokenResponse.getFileUploadUrl())
679  .setMd5(md5)
680  .setSha1(getOrCalcSha1(af))
682 
683  ctApiDAO.uploadMeta(new AuthenticatedRequestData(ingestJobState.getLicenseInfo().getDecryptedLicense(), authTokenResponse), metaRequest);
684  return true;
685  }
686 
696  @Messages({
697  "MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_title=Waiting for File Upload Results",
698  "MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_desc=Waiting for all uploaded files to complete scanning.",
699  "MalwareScanIngestModule_longPollForNotFound_timeout_title=File Upload Results Timeout",
700  "MalwareScanIngestModule_longPollForNotFound_timeout_desc=There was a timeout while waiting for file uploads to be processed. Please try again later.",})
701  private void longPollForNotFound(IngestJobState ingestJobState) throws InterruptedException, CTCloudException, Blackboard.BlackboardException, TskCoreException {
702  if (!ingestJobState.isDoFileLookups()
703  || !ingestJobState.isQueryForMissing()
704  || MapUtils.isEmpty(ingestJobState.getUnidentifiedHashes())
705  || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
706  return;
707  }
708 
710  Bundle.MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_title(),
711  Bundle.MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_desc()
712  );
713  logger.log(Level.INFO, "Begin polling for malware status of file uploads.");
714 
715  Map<String, List<Long>> remaining = new HashMap<>(ingestJobState.getUnidentifiedHashes());
716 
717  for (int retry = 0; retry < NUM_FILE_UPLOAD_RETRIES; retry++) {
718  List<List<String>> md5Batches = Lists.partition(new ArrayList<>(remaining.keySet()), BATCH_SIZE);
719  for (List<String> batch : md5Batches) {
720  // if we have exceeded limits or cancelled, then we're done.
721  if (!ingestJobState.isDoFileLookups() || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
722  return;
723  }
724 
725  List<CTCloudBean> repResult = getHashLookupResults(ingestJobState, batch);
726 
727  Map<Status, List<CTCloudBean>> statusGroupings = repResult.stream()
728  .filter(bean -> bean.getMalwareResult() != null)
729  .collect(Collectors.groupingBy(bean -> bean.getMalwareResult().getStatus()));
730 
731  // for all found items, create analysis results
732  List<CTCloudBean> found = statusGroupings.get(Status.FOUND);
733 
734  createAnalysisResults(ingestJobState, found, remaining);
735 
736  // remove any found items from the list of items to long poll for
737  for (CTCloudBean foundItem : CollectionUtils.emptyIfNull(found)) {
738  String normalizedMd5 = normalizedMd5(foundItem.getMd5HashValue());
739  remaining.remove(normalizedMd5);
740  }
741  }
742 
743  if (remaining.isEmpty()) {
744  return;
745  }
746 
747  // exponential backoff before trying again
748  long waitMultiplier = ((long) Math.pow(2, retry));
749 
750  logger.log(Level.INFO, MessageFormat.format("Waiting {0} milliseconds before polling again for malware status of file uploads.", (waitMultiplier * FILE_UPLOAD_RETRY_SLEEP_MILLIS)));
751 
752  for (int i = 0; i < waitMultiplier; i++) {
753  if (!ingestJobState.isDoFileLookups() || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
754  return;
755  }
756 
757  Thread.sleep(FILE_UPLOAD_RETRY_SLEEP_MILLIS);
758  }
759  }
760 
762  Bundle.MalwareScanIngestModule_longPollForNotFound_timeout_title(),
763  Bundle.MalwareScanIngestModule_longPollForNotFound_timeout_desc(),
764  null
765  );
766  }
767 
779  private void createAnalysisResults(IngestJobState ingestJobState, List<CTCloudBean> repResult, Map<String, List<Long>> md5ToObjId) throws Blackboard.BlackboardException, TskCoreException {
780  if (CollectionUtils.isEmpty(repResult)) {
781  return;
782  }
783 
784  List<BlackboardArtifact> createdArtifacts = new ArrayList<>();
785  SleuthkitCase.CaseDbTransaction trans = null;
786  try {
787  trans = ingestJobState.getTskCase().beginTransaction();
788  for (CTCloudBean result : repResult) {
789  String sanitizedMd5 = normalizedMd5(result.getMd5HashValue());
790  List<Long> objIds = md5ToObjId.remove(sanitizedMd5);
791  if (CollectionUtils.isEmpty(objIds)) {
792  continue;
793  }
794 
795  for (Long objId : objIds) {
796  AnalysisResult res = createAnalysisResult(ingestJobState, trans, result, objId);
797  if (res != null) {
798  // only post results that have score NOTABLE or LIKELY_NOTABLE
799  Score score = res.getScore();
801  createdArtifacts.add(res);
802  }
803  }
804  }
805  }
806 
807  trans.commit();
808  trans = null;
809  } finally {
810  if (trans != null) {
811  trans.rollback();
812  createdArtifacts.clear();
813  trans = null;
814  }
815  }
816 
817  if (!CollectionUtils.isEmpty(createdArtifacts)) {
818  ingestJobState.getTskCase().getBlackboard().postArtifacts(
819  createdArtifacts,
820  Bundle.MalwareScanIngestModuleFactory_displayName(),
821  ingestJobState.getIngestJobId()
822  );
823  }
824 
825  }
826 
838  @Messages({
839  "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes=YES",
840  "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No=NO"
841  })
842  private AnalysisResult createAnalysisResult(IngestJobState ingestJobState, SleuthkitCase.CaseDbTransaction trans, CTCloudBean cloudBean, Long objId) throws Blackboard.BlackboardException {
843  if (objId == null || cloudBean == null || cloudBean.getMalwareResult() == null || cloudBean.getMalwareResult().getStatus() != Status.FOUND) {
844  logger.log(Level.WARNING, MessageFormat.format("Attempting to create analysis result with invalid parameters [objId: {0}, cloud bean status: {1}]",
845  objId == null
846  ? "<null>"
847  : objId,
848  (cloudBean == null || cloudBean.getMalwareResult() == null || cloudBean.getMalwareResult().getStatus() == null)
849  ? "<null>"
850  : cloudBean.getMalwareResult().getStatus().name()
851  ));
852  return null;
853  }
854 
855  Score score = cloudBean.getMalwareResult().getCTScore() == null
857  : cloudBean.getMalwareResult().getCTScore().getTskCore();
858 
859  String conclusion = score.getSignificance() == Score.Significance.NOTABLE || score.getSignificance() == Score.Significance.LIKELY_NOTABLE
860  ? Bundle.MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes()
861  : Bundle.MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No();
862 
863  String justification = cloudBean.getMalwareResult().getStatusDescription();
864 
865  return ingestJobState.getTskCase().getBlackboard().newAnalysisResult(
866  ingestJobState.getMalwareType(),
867  objId,
868  ingestJobState.getDsId(),
869  score,
870  conclusion,
872  justification,
873  Collections.emptyList(),
874  trans).getAnalysisResult();
875  }
876 
880  @Messages({
881  "MalwareScanIngestModule_SharedProcessing_flushTimeout_title=Processing Timeout",
882  "MalwareScanIngestModule_SharedProcessing_flushTimeout_desc=A timeout occurred while finishing processing"
883  })
884  synchronized void shutDown() {
885  // if already shut down, return
886  if (ingestJobState == null) {
887  return;
888  }
889 
890  // flush any remaining items
891  try {
892  batchProcessor.flushAndReset();
893  longPollForNotFound(ingestJobState);
894  } catch (InterruptedException ex) {
896  Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_title(),
897  Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_desc(),
898  ex);
899  } catch (Exception ex) {
901  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
902  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
903  ex);
904  } finally {
905  // set state to shut down and clear any remaining
906  ingestJobState = null;
907  }
908  }
909 
918  private static void notifyWarning(String title, String message, Exception ex) {
919  MessageNotifyUtil.Notify.warn(title, message);
920  logger.log(Level.WARNING, message, ex);
921  }
922 
923  class FileRecord {
924 
925  private final long objId;
926  private final String md5hash;
927 
928  FileRecord(long objId, String md5hash) {
929  this.objId = objId;
930  this.md5hash = md5hash;
931  }
932 
933  long getObjId() {
934  return objId;
935  }
936 
937  String getMd5hash() {
938  return md5hash;
939  }
940 
941  }
942 
953  static class IngestJobState {
954 
955  static final IngestJobState DISABLED = new IngestJobState(
956  null,
957  null,
958  null,
959  null,
960  null,
961  null,
962  false,
963  false
964  );
965 
966  private final SleuthkitCase tskCase;
967  private final FileTypeDetector fileTypeDetector;
968  private final LicenseInfo licenseInfo;
969  private final BlackboardArtifact.Type malwareType;
970  private final long dsId;
971  private final long ingestJobId;
972  private final boolean queryForMissing;
973  private final Map<String, List<Long>> unidentifiedHashes = new HashMap<>();
974 
975  // this can change mid run
976  private boolean uploadUnknownFiles;
977  private boolean doFileLookups;
978  private final IngestJobContext ingestJobContext;
979  private final PathNormalizer pathNormalizer;
980 
981  IngestJobState(IngestJobContext ingestJobContext, SleuthkitCase tskCase, PathNormalizer pathNormalizer, FileTypeDetector fileTypeDetector, LicenseInfo licenseInfo, BlackboardArtifact.Type malwareType, boolean uploadUnknownFiles, boolean doFileLookups) {
982  this.tskCase = tskCase;
983  this.fileTypeDetector = fileTypeDetector;
984  this.pathNormalizer = pathNormalizer;
985  this.licenseInfo = licenseInfo;
986  this.malwareType = malwareType;
987  this.dsId = ingestJobContext == null ? 0L : ingestJobContext.getDataSource().getId();
988  this.ingestJobId = ingestJobContext == null ? 0L : ingestJobContext.getJobId();
989  this.ingestJobContext = ingestJobContext;
990  // for now, querying for any missing files will be tied to whether initially we should upload files and do lookups at all
991  this.queryForMissing = uploadUnknownFiles && doFileLookups;
992  this.uploadUnknownFiles = uploadUnknownFiles;
993  this.doFileLookups = doFileLookups;
994  }
995 
996  SleuthkitCase getTskCase() {
997  return tskCase;
998  }
999 
1000  IngestJobContext getIngestJobContext() {
1001  return ingestJobContext;
1002  }
1003 
1004  FileTypeDetector getFileTypeDetector() {
1005  return fileTypeDetector;
1006  }
1007 
1008  LicenseInfo getLicenseInfo() {
1009  return licenseInfo;
1010  }
1011 
1012  BlackboardArtifact.Type getMalwareType() {
1013  return malwareType;
1014  }
1015 
1016  long getDsId() {
1017  return dsId;
1018  }
1019 
1020  long getIngestJobId() {
1021  return ingestJobId;
1022  }
1023 
1024  Map<String, List<Long>> getUnidentifiedHashes() {
1025  return unidentifiedHashes;
1026  }
1027 
1028  boolean isQueryForMissing() {
1029  return queryForMissing;
1030  }
1031 
1032  boolean isUploadUnknownFiles() {
1033  return uploadUnknownFiles;
1034  }
1035 
1036  void disableUploadUnknownFiles() {
1037  this.uploadUnknownFiles = false;
1038  }
1039 
1040  boolean isDoFileLookups() {
1041  return doFileLookups;
1042  }
1043 
1044  void disableDoFileLookups() {
1045  this.doFileLookups = false;
1046  }
1047 
1048  public PathNormalizer getPathNormalizer() {
1049  return pathNormalizer;
1050  }
1051 
1052  }
1053  }
1054 }
static final Score SCORE_UNKNOWN
List< AnalysisResult > getAnalysisResults(BlackboardArtifact.Type artifactType)
AnalysisResult createAnalysisResult(IngestJobState ingestJobState, SleuthkitCase.CaseDbTransaction trans, CTCloudBean cloudBean, Long objId)
void uploadMeta(AuthenticatedRequestData authenticatedRequestData, MetadataUploadRequest metaRequest)
Definition: CTApiDAO.java:100
void handleNonFoundResults(IngestJobState ingestJobState, Map< String, List< Long >> md5ToObjId, List< CTCloudBean > results, boolean performFileUpload)
static List< HashResult > calculateHashes(Content content, Collection< HashType > hashTypes)
List< CTCloudBean > getReputationResults(AuthenticatedRequestData authenticatedRequestData, List< String > md5Hashes)
Definition: CTApiDAO.java:114
void createAnalysisResults(IngestJobState ingestJobState, List< CTCloudBean > repResult, Map< String, List< Long >> md5ToObjId)
void handleBatch(IngestJobState ingestJobState, List< FileRecord > fileRecords)
void uploadFile(FileUploadRequest fileUploadRequest)
Definition: CTApiDAO.java:96
AuthTokenResponse getAuthToken(DecryptedLicenseResponse decrypted)
Definition: CTApiDAO.java:81
void handleLookupResults(IngestJobState ingestJobState, Map< String, List< Long >> md5ToObjId, List< CTCloudBean > repResult)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
List< CTCloudBean > getHashLookupResults(IngestJobState ingestJobState, List< String > md5Hashes)

Copyright © 2012-2024 Sleuth Kit Labs. Generated on: Mon Mar 17 2025
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.