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;
57 import org.sleuthkit.datamodel.AbstractFile;
58 import org.sleuthkit.datamodel.AnalysisResult;
59 import org.sleuthkit.datamodel.Blackboard;
60 import org.sleuthkit.datamodel.BlackboardArtifact;
61 import org.sleuthkit.datamodel.ReadContentInputStream;
62 import org.sleuthkit.datamodel.HashUtility;
63 import org.sleuthkit.datamodel.HashUtility.HashResult;
64 import org.sleuthkit.datamodel.HashUtility.HashType;
65 import org.sleuthkit.datamodel.Score;
66 import org.sleuthkit.datamodel.SleuthkitCase;
67 import org.sleuthkit.datamodel.TskCoreException;
68 import org.sleuthkit.datamodel.TskData;
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  synchronized void startUp(IngestJobContext context, boolean uploadFiles) throws IngestModuleException {
166  // only run this code once per startup
167  if (ingestJobState != null) {
168  return;
169  }
170 
171  try {
172  ingestJobState = getNewJobState(context, uploadFiles);
173  } catch (Exception ex) {
174  ingestJobState = IngestJobState.DISABLED;
175  throw new IngestModuleException("An exception occurred on MalwareScanIngestModule startup", ex);
176  }
177  }
178 
187  private IngestJobState getNewJobState(IngestJobContext context, boolean uploadFiles) throws Exception {
188  // get saved license
189  Optional<LicenseInfo> licenseInfoOpt = ctSettingsPersistence.loadLicenseInfo();
190  if (licenseInfoOpt.isEmpty() || licenseInfoOpt.get().getDecryptedLicense() == null) {
192  Bundle.MalwareScanIngestModule_ShareProcessing_noLicense_title(),
193  Bundle.MalwareScanIngestModule_ShareProcessing_noLicense_desc(),
194  null);
195 
196  return IngestJobState.DISABLED;
197  }
198 
199  AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(licenseInfoOpt.get().getDecryptedLicense());
200  // syncronously fetch malware scans info
201 
202  // determine lookups remaining
203  long lookupsRemaining = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount());
204  if (lookupsRemaining <= 0) {
206  Bundle.MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_title(),
207  Bundle.MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_desc(),
208  null);
209 
210  return IngestJobState.DISABLED;
211  } else if (lookupsRemaining < LOW_LOOKUPS_REMAINING) {
213  Bundle.MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_title(),
214  Bundle.MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_desc(lookupsRemaining),
215  null);
216  }
217 
218  // determine lookups remaining
219  if (uploadFiles) {
220  long uploadsRemaining = remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount());
221  if (uploadsRemaining <= 0) {
223  Bundle.MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_title(),
224  Bundle.MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_desc(),
225  null);
226  uploadFiles = false;
227  } else if (lookupsRemaining < LOW_UPLOADS_REMAINING) {
229  Bundle.MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_title(),
230  Bundle.MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_desc(lookupsRemaining),
231  null);
232  }
233  }
234 
235  // setup necessary variables for processing
236  SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
237  return new IngestJobState(
238  context,
239  tskCase,
240  new PathNormalizer(tskCase),
241  new FileTypeDetector(),
242  licenseInfoOpt.get(),
243  BlackboardArtifact.Type.TSK_MALWARE,
244  uploadFiles,
245  true
246  );
247  }
248 
256  private static long remaining(Long limit, Long used) {
257  limit = limit == null ? 0 : limit;
258  used = used == null ? 0 : used;
259  return limit - used;
260  }
261 
268  private static String getOrCalcHash(AbstractFile af, HashType hashType) {
269  switch (hashType) {
270  case MD5:
271  if (StringUtils.isNotBlank(af.getMd5Hash())) {
272  return af.getMd5Hash();
273  }
274  break;
275  case SHA256:
276  if (StringUtils.isNotBlank(af.getSha256Hash())) {
277  return af.getSha256Hash();
278  }
279  }
280 
281  try {
282  List<HashResult> hashResults = HashUtility.calculateHashes(af, Collections.singletonList(hashType));
283  if (CollectionUtils.isNotEmpty(hashResults)) {
284  for (HashResult hashResult : hashResults) {
285  if (hashResult.getType() == hashType) {
286  return hashResult.getValue();
287  }
288  }
289  }
290  } catch (TskCoreException ex) {
291  logger.log(Level.WARNING,
292  MessageFormat.format("An error occurred while processing hash for file name: {0} and obj id: {1} and hash type {2}.",
293  af.getName(),
294  af.getId(),
295  hashType.name()),
296  ex);
297  }
298 
299  return null;
300  }
301 
308  private static String getOrCalcMd5(AbstractFile af) {
309  return getOrCalcHash(af, HashType.MD5);
310  }
311 
318  private static String getOrCalcSha256(AbstractFile af) {
319  return getOrCalcHash(af, HashType.SHA256);
320  }
321 
328  private static String getOrCalcSha1(AbstractFile af) throws NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException {
329  if (StringUtils.isNotBlank(af.getSha1Hash())) {
330  return af.getSha1Hash();
331  }
332  // taken from https://stackoverflow.com/questions/6293713/java-how-to-create-sha-1-for-a-file
333  MessageDigest digest = MessageDigest.getInstance("SHA-1");
334  ReadContentInputStream afStream = new ReadContentInputStream(af);
335  int n = 0;
336  byte[] buffer = new byte[8192];
337  while (n != -1) {
338  n = afStream.read(buffer);
339  if (n > 0) {
340  digest.update(buffer, 0, n);
341  }
342  }
343  byte[] hashBytes = digest.digest();
344  String hashString = HexFormat.of().formatHex(hashBytes);
345  return hashString;
346  }
347 
357  @Messages({
358  "MalwareScanIngestModule_ShareProcessing_batchTimeout_title=Batch Processing Timeout",
359  "MalwareScanIngestModule_ShareProcessing_batchTimeout_desc=Batch processing timed out"
360  })
361  IngestModule.ProcessResult process(AbstractFile af) {
362  try {
363  if (ingestJobState != null
364  && ingestJobState.isDoFileLookups()
365  && !ingestJobState.getIngestJobContext().fileIngestIsCancelled()
366  && af.getKnown() != TskData.FileKnown.KNOWN
367  && EXECUTABLE_MIME_TYPES.contains(StringUtils.defaultString(ingestJobState.getFileTypeDetector().getMIMEType(af)).trim().toLowerCase())
368  && CollectionUtils.isEmpty(af.getAnalysisResults(ingestJobState.getMalwareType()))) {
369 
370  String md5 = getOrCalcMd5(af);
371  if (StringUtils.isNotBlank(md5)) {
372  batchProcessor.add(new FileRecord(af.getId(), md5));
373  }
374  }
375  return ProcessResult.OK;
376  } catch (TskCoreException ex) {
378  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
379  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
380  ex);
381  return IngestModule.ProcessResult.ERROR;
382  } catch (InterruptedException ex) {
384  Bundle.MalwareScanIngestModule_ShareProcessing_batchTimeout_title(),
385  Bundle.MalwareScanIngestModule_ShareProcessing_batchTimeout_desc(),
386  ex);
387  return IngestModule.ProcessResult.ERROR;
388  }
389  }
390 
398  @Messages({
399  "MalwareScanIngestModule_SharedProcessing_authTokenResponseError_title=Authentication API error",
400  "# {0} - errorResponse",
401  "MalwareScanIngestModule_SharedProcessing_authTokenResponseError_desc=Received error: ''{0}'' when fetching the API authentication token for the license",
402  "MalwareScanIngestModule_SharedProcessing_repServicenResponseError_title=Lookup API error",
403  "# {0} - errorResponse",
404  "MalwareScanIngestModule_SharedProcessing_repServicenResponseError_desc=Received error: ''{0}'' when fetching hash lookup results",
405  "MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title=Hash Lookups Exhausted",
406  "MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc=The remaining hash lookups for this license have been exhausted",
407  "MalwareScanIngestModule_SharedProcessing_generalProcessingError_title=Hash Lookup Error",
408  "MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc=An error occurred while processing hash lookup results",})
409  private void handleBatch(IngestJobState ingestJobState, List<FileRecord> fileRecords) {
410  if (ingestJobState == null
411  || !ingestJobState.isDoFileLookups()
412  || ingestJobState.getIngestJobContext().fileIngestIsCancelled()
413  || fileRecords == null
414  || fileRecords.isEmpty()) {
415  return;
416  }
417 
418  // create mapping of md5 to corresponding object ids as well as just the list of md5's
419  Map<String, List<Long>> md5ToObjId = new HashMap<>();
420 
421  for (FileRecord fr : fileRecords) {
422  if (fr == null || StringUtils.isBlank(fr.getMd5hash()) || fr.getObjId() <= 0) {
423  continue;
424  }
425 
426  String sanitizedMd5 = normalizedMd5(fr.getMd5hash());
427  md5ToObjId
428  .computeIfAbsent(sanitizedMd5, (k) -> new ArrayList<>())
429  .add(fr.getObjId());
430  }
431 
432  List<String> md5Hashes = new ArrayList<>(md5ToObjId.keySet());
433 
434  if (md5Hashes.isEmpty()) {
435  return;
436  }
437 
438  try {
439  List<CTCloudBean> repResult = getHashLookupResults(ingestJobState, md5Hashes);
440  handleLookupResults(ingestJobState, md5ToObjId, repResult);
441  } catch (Exception ex) {
443  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
444  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
445  ex);
446  }
447  }
448 
459  @Messages({
460  "MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_title=Lookup Limits Exceeded",
461  "MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_desc=Not all files were processed because hash lookup limits were exceeded. Please try again when your limits reset.",})
462  private void handleLookupResults(IngestJobState ingestJobState, Map<String, List<Long>> md5ToObjId, List<CTCloudBean> repResult) throws Blackboard.BlackboardException, TskCoreException, TskCoreException, CTCloudException, NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException {
463  if (CollectionUtils.isEmpty(repResult)) {
464  return;
465  }
466 
467  Map<Status, List<CTCloudBean>> statusGroupings = repResult.stream()
468  .filter(bean -> bean.getMalwareResult() != null)
469  .collect(Collectors.groupingBy(bean -> bean.getMalwareResult().getStatus()));
470 
471  // for all found items, create analysis results
472  List<CTCloudBean> found = statusGroupings.get(Status.FOUND);
473  createAnalysisResults(ingestJobState, found, md5ToObjId);
474 
475  // if being scanned, check list to run later
476  handleNonFoundResults(ingestJobState, md5ToObjId, statusGroupings.get(Status.BEING_SCANNED), false);
477 
478  // if not found, try upload
479  handleNonFoundResults(ingestJobState, md5ToObjId, statusGroupings.get(Status.NOT_FOUND), true);
480 
481  // indicate a general error if some result in an error
482  if (CollectionUtils.isNotEmpty(statusGroupings.get(Status.ERROR))) {
484  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
485  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
486  null);
487  }
488 
489  // indicate some results were not processed if limits exceeded in results
490  if (CollectionUtils.isNotEmpty(statusGroupings.get(Status.LIMITS_EXCEEDED))) {
492  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_title(),
493  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_desc(),
494  null);
495  }
496  }
497 
509  private void handleNonFoundResults(IngestJobState ingestJobState, Map<String, List<Long>> md5ToObjId, List<CTCloudBean> results, boolean performFileUpload) throws CTCloudException, TskCoreException, NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException {
510  if (CollectionUtils.isNotEmpty(results)
511  && ingestJobState.isDoFileLookups()
512  && ((performFileUpload && ingestJobState.isUploadUnknownFiles()) || (!performFileUpload && ingestJobState.isQueryForMissing()))) {
513 
514  for (CTCloudBean beingScanned : CollectionUtils.emptyIfNull(results)) {
515 
516  String sanitizedMd5 = normalizedMd5(beingScanned.getMd5HashValue());
517  if (StringUtils.isBlank(sanitizedMd5)) {
518  continue;
519  }
520  List<Long> correspondingObjIds = md5ToObjId.get(sanitizedMd5);
521  if (CollectionUtils.isEmpty(correspondingObjIds)) {
522  continue;
523  }
524 
525  if (performFileUpload) {
526  uploadFile(ingestJobState, sanitizedMd5, correspondingObjIds.get(0));
527  }
528 
529  ingestJobState.getUnidentifiedHashes().put(sanitizedMd5, correspondingObjIds);
530  }
531  }
532  }
533 
544  private List<CTCloudBean> getHashLookupResults(IngestJobState ingestJobState, List<String> md5Hashes) throws CTCloudException {
545  if (ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
546  return Collections.emptyList();
547  }
548 
549  // get an auth token with the license
550  AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(ingestJobState.getLicenseInfo().getDecryptedLicense());
551 
552  // make sure we are in bounds for the remaining scans
553  long remainingScans = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount());
554  if (remainingScans <= 0) {
555  ingestJobState.disableDoFileLookups();
557  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title(),
558  Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc(),
559  null);
560  return Collections.emptyList();
561  } else if (ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
562  return Collections.emptyList();
563  }
564 
565  // while we have a valid auth token, also check file uploads.
566  if (ingestJobState.isUploadUnknownFiles()) {
567  long remainingUploads = remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount());
568  if (remainingUploads <= 0) {
569  ingestJobState.disableUploadUnknownFiles();
571  Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title(),
572  Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc(),
573  null);
574  }
575  }
576 
577  // using auth token, get results
578  return ctApiDAO.getReputationResults(
579  new AuthenticatedRequestData(ingestJobState.getLicenseInfo().getDecryptedLicense(), authTokenResponse),
580  md5Hashes
581  );
582  }
583 
590  private static String normalizedMd5(String orig) {
591  return StringUtils.defaultString(orig).trim().toLowerCase();
592  }
593 
601  private static boolean isUploadable(AbstractFile af) {
602  long size = af.getSize();
603  return size >= MIN_UPLOAD_SIZE && size <= MAX_UPLOAD_SIZE;
604  }
605 
615  @Messages({
616  "MalwareScanIngestModule_uploadFile_notUploadable_title=Not Able to Upload",
617  "# {0} - objectId",
618  "MalwareScanIngestModule_uploadFile_notUploadable_desc=A file did not meet requirements for upload (object id: {0}).",
619  "MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title=No Remaining File Uploads",
620  "MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc=There are no more file uploads on this license at this time. File uploads will be disabled for remaining uploads.",})
621  private boolean uploadFile(IngestJobState ingestJobState, String md5, long objId) throws CTCloudException, TskCoreException, NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException {
622  if (!ingestJobState.isUploadUnknownFiles() || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
623  return false;
624  }
625 
626  AbstractFile af = ingestJobState.getTskCase().getAbstractFileById(objId);
627  if (af == null) {
628  return false;
629  }
630 
631  if (!isUploadable(af)) {
633  Bundle.MalwareScanIngestModule_uploadFile_notUploadable_title(),
634  Bundle.MalwareScanIngestModule_uploadFile_notUploadable_desc(objId),
635  null);
636  return false;
637  }
638 
639  // get auth token / file upload url
640  AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(ingestJobState.getLicenseInfo().getDecryptedLicense(), af.getSize());
641  if (StringUtils.isBlank(authTokenResponse.getFileUploadUrl())) {
642  throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR);
643  } else if (remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount()) <= 0) {
644  // don't proceed with upload if reached limit
645  ingestJobState.disableUploadUnknownFiles();
647  Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title(),
648  Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc(),
649  null);
650 
651  return false;
652  } else if (ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
653  return false;
654  }
655 
656  // upload bytes
657  ReadContentInputStream fileInputStream = new ReadContentInputStream(af);
658 
659  ctApiDAO.uploadFile(new FileUploadRequest()
660  .setContentLength(af.getSize())
661  .setFileInputStream(fileInputStream)
662  .setFileName(af.getName())
663  .setFullUrlPath(authTokenResponse.getFileUploadUrl())
664  );
665 
666  // upload metadata
667  MetadataUploadRequest metaRequest = new MetadataUploadRequest()
668  .setCreatedDate(af.getCrtime() == 0 ? null : af.getCrtime())
669  .setFilePath(ingestJobState.getPathNormalizer().normalizePath(af.getUniquePath()))
670  .setFileSizeBytes(af.getSize())
671  .setFileUploadUrl(authTokenResponse.getFileUploadUrl())
672  .setMd5(md5)
673  .setSha1(getOrCalcSha1(af))
675 
676  ctApiDAO.uploadMeta(new AuthenticatedRequestData(ingestJobState.getLicenseInfo().getDecryptedLicense(), authTokenResponse), metaRequest);
677  return true;
678  }
679 
689  @Messages({
690  "MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_title=Waiting for File Upload Results",
691  "MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_desc=Waiting for all uploaded files to complete scanning.",
692  "MalwareScanIngestModule_longPollForNotFound_timeout_title=File Upload Results Timeout",
693  "MalwareScanIngestModule_longPollForNotFound_timeout_desc=There was a timeout while waiting for file uploads to be processed. Please try again later.",})
694  private void longPollForNotFound(IngestJobState ingestJobState) throws InterruptedException, CTCloudException, Blackboard.BlackboardException, TskCoreException {
695  if (!ingestJobState.isDoFileLookups()
696  || !ingestJobState.isQueryForMissing()
697  || MapUtils.isEmpty(ingestJobState.getUnidentifiedHashes())
698  || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
699  return;
700  }
701 
703  Bundle.MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_title(),
704  Bundle.MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_desc()
705  );
706  logger.log(Level.INFO, "Begin polling for malware status of file uploads.");
707 
708  Map<String, List<Long>> remaining = new HashMap<>(ingestJobState.getUnidentifiedHashes());
709 
710  for (int retry = 0; retry < NUM_FILE_UPLOAD_RETRIES; retry++) {
711  List<List<String>> md5Batches = Lists.partition(new ArrayList<>(remaining.keySet()), BATCH_SIZE);
712  for (List<String> batch : md5Batches) {
713  // if we have exceeded limits or cancelled, then we're done.
714  if (!ingestJobState.isDoFileLookups() || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
715  return;
716  }
717 
718  List<CTCloudBean> repResult = getHashLookupResults(ingestJobState, batch);
719 
720  Map<Status, List<CTCloudBean>> statusGroupings = repResult.stream()
721  .filter(bean -> bean.getMalwareResult() != null)
722  .collect(Collectors.groupingBy(bean -> bean.getMalwareResult().getStatus()));
723 
724  // for all found items, create analysis results
725  List<CTCloudBean> found = statusGroupings.get(Status.FOUND);
726 
727  createAnalysisResults(ingestJobState, found, remaining);
728 
729  // remove any found items from the list of items to long poll for
730  for (CTCloudBean foundItem : CollectionUtils.emptyIfNull(found)) {
731  String normalizedMd5 = normalizedMd5(foundItem.getMd5HashValue());
732  remaining.remove(normalizedMd5);
733  }
734  }
735 
736  if (remaining.isEmpty()) {
737  return;
738  }
739 
740  // exponential backoff before trying again
741  long waitMultiplier = ((long) Math.pow(2, retry));
742 
743  logger.log(Level.INFO, MessageFormat.format("Waiting {0} milliseconds before polling again for malware status of file uploads.", (waitMultiplier * FILE_UPLOAD_RETRY_SLEEP_MILLIS)));
744 
745  for (int i = 0; i < waitMultiplier; i++) {
746  if (!ingestJobState.isDoFileLookups() || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) {
747  return;
748  }
749 
750  Thread.sleep(FILE_UPLOAD_RETRY_SLEEP_MILLIS);
751  }
752  }
753 
755  Bundle.MalwareScanIngestModule_longPollForNotFound_timeout_title(),
756  Bundle.MalwareScanIngestModule_longPollForNotFound_timeout_desc(),
757  null
758  );
759  }
760 
772  private void createAnalysisResults(IngestJobState ingestJobState, List<CTCloudBean> repResult, Map<String, List<Long>> md5ToObjId) throws Blackboard.BlackboardException, TskCoreException {
773  if (CollectionUtils.isEmpty(repResult)) {
774  return;
775  }
776 
777  List<BlackboardArtifact> createdArtifacts = new ArrayList<>();
778  SleuthkitCase.CaseDbTransaction trans = null;
779  try {
780  trans = ingestJobState.getTskCase().beginTransaction();
781  for (CTCloudBean result : repResult) {
782  String sanitizedMd5 = normalizedMd5(result.getMd5HashValue());
783  List<Long> objIds = md5ToObjId.remove(sanitizedMd5);
784  if (CollectionUtils.isEmpty(objIds)) {
785  continue;
786  }
787 
788  for (Long objId : objIds) {
789  AnalysisResult res = createAnalysisResult(ingestJobState, trans, result, objId);
790  if (res != null) {
791  // only post results that have score NOTABLE or LIKELY_NOTABLE
792  Score score = res.getScore();
793  if (score.getSignificance() == Score.Significance.NOTABLE || score.getSignificance() == Score.Significance.LIKELY_NOTABLE) {
794  createdArtifacts.add(res);
795  }
796  }
797  }
798  }
799 
800  trans.commit();
801  trans = null;
802  } finally {
803  if (trans != null) {
804  trans.rollback();
805  createdArtifacts.clear();
806  trans = null;
807  }
808  }
809 
810  if (!CollectionUtils.isEmpty(createdArtifacts)) {
811  ingestJobState.getTskCase().getBlackboard().postArtifacts(
812  createdArtifacts,
813  Bundle.MalwareScanIngestModuleFactory_displayName(),
814  ingestJobState.getIngestJobId()
815  );
816  }
817 
818  }
819 
831  @Messages({
832  "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes=YES",
833  "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No=NO"
834  })
835  private AnalysisResult createAnalysisResult(IngestJobState ingestJobState, SleuthkitCase.CaseDbTransaction trans, CTCloudBean cloudBean, Long objId) throws Blackboard.BlackboardException {
836  if (objId == null || cloudBean == null || cloudBean.getMalwareResult() == null || cloudBean.getMalwareResult().getStatus() != Status.FOUND) {
837  logger.log(Level.WARNING, MessageFormat.format("Attempting to create analysis result with invalid parameters [objId: {0}, cloud bean status: {1}]",
838  objId == null
839  ? "<null>"
840  : objId,
841  (cloudBean == null || cloudBean.getMalwareResult() == null || cloudBean.getMalwareResult().getStatus() == null)
842  ? "<null>"
843  : cloudBean.getMalwareResult().getStatus().name()
844  ));
845  return null;
846  }
847 
848  Score score = cloudBean.getMalwareResult().getCTScore() == null
849  ? Score.SCORE_UNKNOWN
850  : cloudBean.getMalwareResult().getCTScore().getTskCore();
851 
852  String conclusion = score.getSignificance() == Score.Significance.NOTABLE || score.getSignificance() == Score.Significance.LIKELY_NOTABLE
853  ? Bundle.MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes()
854  : Bundle.MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No();
855 
856  String justification = cloudBean.getMalwareResult().getStatusDescription();
857 
858  return ingestJobState.getTskCase().getBlackboard().newAnalysisResult(
859  ingestJobState.getMalwareType(),
860  objId,
861  ingestJobState.getDsId(),
862  score,
863  conclusion,
865  justification,
866  Collections.emptyList(),
867  trans).getAnalysisResult();
868  }
869 
873  @Messages({
874  "MalwareScanIngestModule_SharedProcessing_flushTimeout_title=Processing Timeout",
875  "MalwareScanIngestModule_SharedProcessing_flushTimeout_desc=A timeout occurred while finishing processing"
876  })
877  synchronized void shutDown() {
878  // if already shut down, return
879  if (ingestJobState == null) {
880  return;
881  }
882 
883  // flush any remaining items
884  try {
885  batchProcessor.flushAndReset();
886  longPollForNotFound(ingestJobState);
887  } catch (InterruptedException ex) {
889  Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_title(),
890  Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_desc(),
891  ex);
892  } catch (Exception ex) {
894  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
895  Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
896  ex);
897  } finally {
898  // set state to shut down and clear any remaining
899  ingestJobState = null;
900  }
901  }
902 
911  private static void notifyWarning(String title, String message, Exception ex) {
912  MessageNotifyUtil.Notify.warn(title, message);
913  logger.log(Level.WARNING, message, ex);
914  }
915 
916  class FileRecord {
917 
918  private final long objId;
919  private final String md5hash;
920 
921  FileRecord(long objId, String md5hash) {
922  this.objId = objId;
923  this.md5hash = md5hash;
924  }
925 
926  long getObjId() {
927  return objId;
928  }
929 
930  String getMd5hash() {
931  return md5hash;
932  }
933 
934  }
935 
946  static class IngestJobState {
947 
948  static final IngestJobState DISABLED = new IngestJobState(
949  null,
950  null,
951  null,
952  null,
953  null,
954  null,
955  false,
956  false
957  );
958 
959  private final SleuthkitCase tskCase;
960  private final FileTypeDetector fileTypeDetector;
961  private final LicenseInfo licenseInfo;
962  private final BlackboardArtifact.Type malwareType;
963  private final long dsId;
964  private final long ingestJobId;
965  private final boolean queryForMissing;
966  private final Map<String, List<Long>> unidentifiedHashes = new HashMap<>();
967 
968  // this can change mid run
969  private boolean uploadUnknownFiles;
970  private boolean doFileLookups;
971  private final IngestJobContext ingestJobContext;
972  private final PathNormalizer pathNormalizer;
973 
974  IngestJobState(IngestJobContext ingestJobContext, SleuthkitCase tskCase, PathNormalizer pathNormalizer, FileTypeDetector fileTypeDetector, LicenseInfo licenseInfo, BlackboardArtifact.Type malwareType, boolean uploadUnknownFiles, boolean doFileLookups) {
975  this.tskCase = tskCase;
976  this.fileTypeDetector = fileTypeDetector;
977  this.pathNormalizer = pathNormalizer;
978  this.licenseInfo = licenseInfo;
979  this.malwareType = malwareType;
980  this.dsId = ingestJobContext == null ? 0L : ingestJobContext.getDataSource().getId();
981  this.ingestJobId = ingestJobContext == null ? 0L : ingestJobContext.getJobId();
982  this.ingestJobContext = ingestJobContext;
983  // for now, querying for any missing files will be tied to whether initially we should upload files and do lookups at all
984  this.queryForMissing = uploadUnknownFiles && doFileLookups;
985  this.uploadUnknownFiles = uploadUnknownFiles;
986  this.doFileLookups = doFileLookups;
987  }
988 
989  SleuthkitCase getTskCase() {
990  return tskCase;
991  }
992 
993  IngestJobContext getIngestJobContext() {
994  return ingestJobContext;
995  }
996 
997  FileTypeDetector getFileTypeDetector() {
998  return fileTypeDetector;
999  }
1000 
1001  LicenseInfo getLicenseInfo() {
1002  return licenseInfo;
1003  }
1004 
1005  BlackboardArtifact.Type getMalwareType() {
1006  return malwareType;
1007  }
1008 
1009  long getDsId() {
1010  return dsId;
1011  }
1012 
1013  long getIngestJobId() {
1014  return ingestJobId;
1015  }
1016 
1017  Map<String, List<Long>> getUnidentifiedHashes() {
1018  return unidentifiedHashes;
1019  }
1020 
1021  boolean isQueryForMissing() {
1022  return queryForMissing;
1023  }
1024 
1025  boolean isUploadUnknownFiles() {
1026  return uploadUnknownFiles;
1027  }
1028 
1029  void disableUploadUnknownFiles() {
1030  this.uploadUnknownFiles = false;
1031  }
1032 
1033  boolean isDoFileLookups() {
1034  return doFileLookups;
1035  }
1036 
1037  void disableDoFileLookups() {
1038  this.doFileLookups = false;
1039  }
1040 
1041  public PathNormalizer getPathNormalizer() {
1042  return pathNormalizer;
1043  }
1044 
1045  }
1046  }
1047 }
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 void info(String title, String message)
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)
static void warn(String title, String message)

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