Autopsy 4.22.1
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 */
19package com.basistech.df.cybertriage.autopsy.malwarescan;
20
21import com.basistech.df.cybertriage.autopsy.ctapi.CTApiDAO;
22import com.basistech.df.cybertriage.autopsy.ctapi.CTCloudException;
23import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthTokenResponse;
24import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthenticatedRequestData;
25import com.basistech.df.cybertriage.autopsy.ctapi.json.CTCloudBean;
26import com.basistech.df.cybertriage.autopsy.ctapi.json.FileUploadRequest;
27import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseInfo;
28import com.basistech.df.cybertriage.autopsy.ctapi.json.MalwareResultBean.Status;
29import com.basistech.df.cybertriage.autopsy.ctapi.json.MetadataUploadRequest;
30import com.basistech.df.cybertriage.autopsy.ctoptions.ctcloud.CTLicensePersistence;
31import java.security.MessageDigest;
32import java.security.NoSuchAlgorithmException;
33import java.text.MessageFormat;
34import java.util.ArrayList;
35import java.util.Collections;
36import java.util.HashMap;
37import java.util.HexFormat;
38import java.util.List;
39import java.util.Map;
40import java.util.Optional;
41import java.util.Set;
42import java.util.logging.Level;
43import java.util.stream.Collectors;
44import java.util.stream.Stream;
45import org.apache.commons.collections4.CollectionUtils;
46import org.apache.commons.collections4.MapUtils;
47import org.apache.commons.lang3.StringUtils;
48import org.apache.curator.shaded.com.google.common.collect.Lists;
49import org.openide.util.NbBundle.Messages;
50import org.sleuthkit.autopsy.casemodule.Case;
51import org.sleuthkit.autopsy.coreutils.Logger;
52import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
53import org.sleuthkit.autopsy.ingest.FileIngestModule;
54import org.sleuthkit.autopsy.ingest.IngestJobContext;
55import org.sleuthkit.autopsy.ingest.IngestModule;
56import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
57import org.sleuthkit.datamodel.AbstractFile;
58import org.sleuthkit.datamodel.AnalysisResult;
59import org.sleuthkit.datamodel.Blackboard;
60import org.sleuthkit.datamodel.BlackboardArtifact;
61import org.sleuthkit.datamodel.ReadContentInputStream;
62import org.sleuthkit.datamodel.HashUtility;
63import org.sleuthkit.datamodel.HashUtility.HashResult;
64import org.sleuthkit.datamodel.HashUtility.HashType;
65import org.sleuthkit.datamodel.Score;
66import org.sleuthkit.datamodel.SleuthkitCase;
67import org.sleuthkit.datamodel.TskCoreException;
68import org.sleuthkit.datamodel.TskData;
69
73class 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>(
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
243 SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
244 return new IngestJobState(
245 context,
246 tskCase,
247 new PathNormalizer(tskCase),
248 new FileTypeDetector(),
249 licenseInfoOpt.get(),
250 BlackboardArtifact.Type.TSK_MALWARE,
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");
341 ReadContentInputStream afStream = new ReadContentInputStream(af);
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 })
368 IngestModule.ProcessResult process(AbstractFile af) {
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())) {
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
675 .setCreatedDate(af.getCrtime() == 0 ? null : af.getCrtime())
676 .setFilePath(ingestJobState.getPathNormalizer().normalizePath(af.getUniquePath()))
677 .setFileSizeBytes(af.getSize())
678 .setFileUploadUrl(authTokenResponse.getFileUploadUrl())
679 .setMd5(md5)
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
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();
800 if (score.getSignificance() == Score.Significance.NOTABLE || score.getSignificance() == Score.Significance.LIKELY_NOTABLE) {
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
856 ? Score.SCORE_UNKNOWN
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();
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}
FileUploadRequest setFileInputStream(InputStream fileInputStream)
void handleNonFoundResults(IngestJobState ingestJobState, Map< String, List< Long > > md5ToObjId, List< CTCloudBean > results, boolean performFileUpload)
List< CTCloudBean > getHashLookupResults(IngestJobState ingestJobState, List< String > md5Hashes)
void handleLookupResults(IngestJobState ingestJobState, Map< String, List< Long > > md5ToObjId, List< CTCloudBean > repResult)
void createAnalysisResults(IngestJobState ingestJobState, List< CTCloudBean > repResult, Map< String, List< Long > > md5ToObjId)
AnalysisResult createAnalysisResult(IngestJobState ingestJobState, SleuthkitCase.CaseDbTransaction trans, CTCloudBean cloudBean, Long objId)
void handleBatch(IngestJobState ingestJobState, List< FileRecord > fileRecords)
synchronized static Logger getLogger(String name)
Definition Logger.java:124

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