diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java index 0fa9feda54ca30a8c4d04fc73844a54922967c32..b5d62f91de57431371dd4e3da4037b00332d4832 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java @@ -62,6 +62,7 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.SystemDefaultCredentialsProvider; import org.apache.http.impl.client.WinHttpClients; +import org.sleuthkit.autopsy.coreutils.Version; /** * Actually makes the http requests to CT cloud. @@ -70,6 +71,7 @@ public class CTCloudHttpClient { private static final CTCloudHttpClient instance = new CTCloudHttpClient(); private static final Logger LOGGER = Logger.getLogger(CTCloudHttpClient.class.getName()); + private static final String HOST_URL = Version.getBuildType() == Version.Type.RELEASE ? Constants.CT_CLOUD_SERVER : Constants.CT_CLOUD_DEV_SERVER; private static final List<String> DEFAULT_SCHEME_PRIORITY = new ArrayList<>(Arrays.asList( @@ -129,7 +131,7 @@ public <O> O doPost(String urlPath, Object jsonBody, Class<O> classType) throws } public <O> O doPost(String urlPath, Map<String, String> urlReqParams, Object jsonBody, Class<O> classType) throws CTCloudException { - String url = Constants.CT_CLOUD_SERVER + urlPath; + String url = HOST_URL + urlPath; try { LOGGER.log(Level.INFO, "initiating http connection to ctcloud server"); diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java index fa35dd7dbb7ba70d895d3e3b1ac28ddd9bf0d64d..ddda5b45089223c343b5980f77921f5c0ec8e1e1 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java @@ -34,8 +34,7 @@ final public class Constants { public static final String CT_CLOUD_DEV_SERVER = "https://cyber-triage-dev.appspot.com"; - // TODO put back - public static final String CT_CLOUD_SERVER = CT_CLOUD_DEV_SERVER; //"https://rep1.cybertriage.com"; + public static final String CT_CLOUD_SERVER = "https://rep1.cybertriage.com"; /** * Link to watch demo video diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties index 68677cb3f7ad19dbf22a1fa4cd16abd470e36590..46f62fdf7a5185355907a8f9980343d9478c03de 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties @@ -1,8 +1,8 @@ # Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license # Click nbfs://nbhost/SystemFileSystem/Templates/Other/properties.properties to edit this template -OptionsCategory_Name_CyberTriage=CyberTriage -OptionsCategory_Keywords_CyberTriage=CyberTriage,Cyber,Triage +OptionsCategory_Name_CyberTriage=Cyber Triage +OptionsCategory_Keywords_CyberTriage=Cyber Triage,Cyber,Triage LicenseDisclaimerPanel.disclaimer.text=<html>The Cyber Triage Malware Scanner module uses 40+ malware scanning engines to identify if Windows executables are malicious. It requires a non-free license to use.</html> LicenseDisclaimerPanel.purchaseFromLabel.text=You can purchase a license from LicenseDisclaimerPanel.link.text=<html><span style="color: blue; text-decoration: underline">https://cybertriage.com/autopsy-checkout</span></html> diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties-MERGED b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties-MERGED index 68677cb3f7ad19dbf22a1fa4cd16abd470e36590..46f62fdf7a5185355907a8f9980343d9478c03de 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties-MERGED +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties-MERGED @@ -1,8 +1,8 @@ # Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license # Click nbfs://nbhost/SystemFileSystem/Templates/Other/properties.properties to edit this template -OptionsCategory_Name_CyberTriage=CyberTriage -OptionsCategory_Keywords_CyberTriage=CyberTriage,Cyber,Triage +OptionsCategory_Name_CyberTriage=Cyber Triage +OptionsCategory_Keywords_CyberTriage=Cyber Triage,Cyber,Triage LicenseDisclaimerPanel.disclaimer.text=<html>The Cyber Triage Malware Scanner module uses 40+ malware scanning engines to identify if Windows executables are malicious. It requires a non-free license to use.</html> LicenseDisclaimerPanel.purchaseFromLabel.text=You can purchase a license from LicenseDisclaimerPanel.link.text=<html><span style="color: blue; text-decoration: underline">https://cybertriage.com/autopsy-checkout</span></html> diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.java index ffd3760e6fbeba02e8aba1808be4bd87b2e1841d..4299f02d620e69d06770ed0e5b98a326c58069fa 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.java @@ -22,16 +22,18 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; /** * Disclaimer for license and place to purchase CT license. */ public class LicenseDisclaimerPanel extends javax.swing.JPanel { + private static final Logger LOGGER = Logger.getLogger(LicenseDisclaimerPanel.class.getName()); - + private static final String CHECKOUT_PAGE_URL = "https://cybertriage.com/autopsy-checkout"; - + /** * Creates new form LicenseDisclaimerPanel */ @@ -116,9 +118,10 @@ private void linkMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_ try { Desktop.getDesktop().browse(new URI(CHECKOUT_PAGE_URL)); } catch (IOException | URISyntaxException e) { - /* TODO: error handling */ } + LOGGER.log(Level.SEVERE, "Error opening link to: " + CHECKOUT_PAGE_URL, e); + } } else { - /* TODO: error handling */ + LOGGER.log(Level.WARNING, "Desktop API is not supported. Link cannot be opened."); } }//GEN-LAST:event_linkMouseClicked diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.java index 11af572890df51719cdf8d22f263ba1a5b31da46..608ea6304006f80b9a14299975a12140e2cf7d6c 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.java @@ -29,9 +29,7 @@ */ public class CTLicenseDialog extends javax.swing.JDialog { - //private static final Pattern LICENSE_PATTERN = Pattern.compile("^\\s*[0-9a-zA-Z]{8}\\-[0-9a-zA-Z]{4}\\-[0-9a-zA-Z]{4}\\-[0-9a-zA-Z]{4}\\-[0-9a-zA-Z]{12}\\s*$"); - - private static final Pattern LICENSE_PATTERN = Pattern.compile("\\s*[a-zA-Z0-9\\-]+?\\s*"); + private static final Pattern LICENSE_PATTERN = Pattern.compile("^\\s*[a-zA-Z0-9\\-]+?\\s*$"); private String licenseString = null; /** diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java index dc62f6a05efe55c65229e53a10aab877db488141..eab025a6416569645480ca93437cc353a22ed467 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java @@ -38,27 +38,18 @@ */ public class BatchProcessor<T> { - private final ExecutorService processingExecutorService = Executors.newSingleThreadExecutor(); + private ExecutorService processingExecutorService = Executors.newSingleThreadExecutor(); private final BlockingQueue<T> batchingQueue; - private final List<T> processingQueue; private final int batchSize; private final Consumer<List<T>> itemsConsumer; - private final long millisTimeout; + private final long secondsTimeout; - private Future<?> lastProcessingFuture = CompletableFuture.runAsync(() -> { - }); - - public BatchProcessor(int batchSize, long millisTimeout, Consumer<List<T>> itemsConsumer) { + public BatchProcessor(int batchSize, long secondsTimeout, Consumer<List<T>> itemsConsumer) { this.batchingQueue = new LinkedBlockingQueue<>(batchSize); - this.processingQueue = new ArrayList<>(batchSize); this.batchSize = batchSize; this.itemsConsumer = itemsConsumer; - this.millisTimeout = millisTimeout; - } - - public synchronized void clearCurrentBatch() { - batchingQueue.clear(); + this.secondsTimeout = secondsTimeout; } public synchronized void add(T item) throws InterruptedException { @@ -68,40 +59,29 @@ public synchronized void add(T item) throws InterruptedException { } } - public synchronized void flush(boolean blockUntilFinished) throws InterruptedException { + public synchronized void flushAndReset() throws InterruptedException { + // get any remaining asyncProcessBatch(); - if (blockUntilFinished) { - waitCurrentFuture(); - } - } - - private synchronized void waitCurrentFuture() throws InterruptedException { - synchronized (lastProcessingFuture) { - if (!lastProcessingFuture.isDone()) { - try { - lastProcessingFuture.get(millisTimeout, TimeUnit.MILLISECONDS); - } catch (ExecutionException | TimeoutException ex) { - // ignore timeout - } - } - } + + // don't accept any new additions + processingExecutorService.shutdown(); + + // await termination + processingExecutorService.awaitTermination(secondsTimeout, TimeUnit.SECONDS); + + // get new (not shut down executor) + processingExecutorService = Executors.newSingleThreadExecutor(); } private synchronized void asyncProcessBatch() throws InterruptedException { if (!batchingQueue.isEmpty()) { - // wait for previous processing to finish - waitCurrentFuture(); - - // if 'andThen' doesn't run, clear the processing queue - processingQueue.clear(); + final List<T> processingList = new ArrayList<>(); // transfer batching queue to processing queue - batchingQueue.drainTo(processingQueue); + batchingQueue.drainTo(processingList); - // submit to processor and then clear processing queue - lastProcessingFuture = processingExecutorService.submit( - () -> itemsConsumer.andThen(processingQueue -> processingQueue.clear()).accept(processingQueue) - ); + // submit to be processed + processingExecutorService.submit(() -> itemsConsumer.accept(processingList)); } } diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java index a509310391789c7bfd9648a2e71b848ce4150ac9..e249b487d5d5972749f4979712666315f1cf9e39 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java @@ -28,6 +28,7 @@ import com.basistech.df.cybertriage.autopsy.ctapi.json.MalwareResultBean; import com.basistech.df.cybertriage.autopsy.ctapi.json.MetadataUploadRequest; import com.basistech.df.cybertriage.autopsy.ctoptions.ctcloud.CTLicensePersistence; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -54,6 +55,9 @@ import org.sleuthkit.datamodel.Blackboard; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.ReadContentInputStream; +import org.sleuthkit.datamodel.HashUtility; +import org.sleuthkit.datamodel.HashUtility.HashResult; +import org.sleuthkit.datamodel.HashUtility.HashType; import org.sleuthkit.datamodel.Score; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; @@ -89,8 +93,8 @@ private static class SharedProcessing { // batch size of 200 files max private static final int BATCH_SIZE = 200; - // 3 minute timeout for an API request - private static final long BATCH_MILLIS_TIMEOUT = 3 * 60 * 1000; + // 1 day timeout for all API requests + private static final long FLUSH_SECS_TIMEOUT = 24 * 60 * 60; //minimum lookups left before issuing warning private static final long LOW_LOOKUPS_REMAINING = 250; @@ -119,7 +123,7 @@ private static class SharedProcessing { private static final String MALWARE_CONFIG = "Cyber Triage Cloud"; private static final Logger logger = Logger.getLogger(MalwareScanIngestModule.class.getName()); - private final BatchProcessor<FileRecord> batchProcessor = new BatchProcessor<FileRecord>(BATCH_SIZE, BATCH_MILLIS_TIMEOUT, this::handleBatch); + private final BatchProcessor<FileRecord> batchProcessor = new BatchProcessor<FileRecord>(BATCH_SIZE, FLUSH_SECS_TIMEOUT, this::handleBatch); private final CTLicensePersistence ctSettingsPersistence = CTLicensePersistence.getInstance(); private final CTApiDAO ctApiDAO = CTApiDAO.getInstance(); @@ -207,18 +211,54 @@ private static long remaining(Long limit, Long used) { return limit - used; } + private String getOrCalcHash(AbstractFile af) { + if (StringUtils.isNotBlank(af.getMd5Hash())) { + return af.getMd5Hash(); + } + + try { + List<HashResult> hashResults = HashUtility.calculateHashes(af, Collections.singletonList(HashType.MD5)); + if (CollectionUtils.isNotEmpty(hashResults)) { + for (HashResult hashResult : hashResults) { + if (hashResult.getType() == HashType.MD5) { + return hashResult.getValue(); + } + } + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, + MessageFormat.format("An error occurred while processing file name: {0} and obj id: {1}.", + af.getName(), + af.getId()), + ex); + } + + return null; + } + @Messages({ "MalwareScanIngestModule_ShareProcessing_batchTimeout_title=Batch Processing Timeout", "MalwareScanIngestModule_ShareProcessing_batchTimeout_desc=Batch processing timed out" }) IngestModule.ProcessResult process(AbstractFile af) { try { - if (runState == RunState.STARTED_UP && af.getKnown() != TskData.FileKnown.KNOWN - && EXECUTABLE_MIME_TYPES.contains(StringUtils.defaultString(fileTypeDetector.getMIMEType(af)).trim().toLowerCase())) { - batchProcessor.add(new FileRecord(af.getId(), af.getMd5Hash())); - + if (runState == RunState.STARTED_UP + && af.getKnown() != TskData.FileKnown.KNOWN + && EXECUTABLE_MIME_TYPES.contains(StringUtils.defaultString(fileTypeDetector.getMIMEType(af)).trim().toLowerCase()) + && CollectionUtils.isEmpty(af.getAnalysisResults(malwareType))) { + + String md5 = getOrCalcHash(af); + if (StringUtils.isNotBlank(md5)) { + batchProcessor.add(new FileRecord(af.getId(), md5)); + } } return ProcessResult.OK; + } catch (TskCoreException ex) { + notifyWarning( + Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(), + Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(), + ex); + return IngestModule.ProcessResult.ERROR; } catch (InterruptedException ex) { notifyWarning( Bundle.MalwareScanIngestModule_ShareProcessing_batchTimeout_title(), @@ -246,7 +286,7 @@ private void handleBatch(List<FileRecord> fileRecords) { // create mapping of md5 to corresponding object ids as well as just the list of md5's Map<String, List<Long>> md5ToObjId = new HashMap<>(); - List<String> md5Hashes = new ArrayList<>(); + for (FileRecord fr : fileRecords) { if (fr == null || StringUtils.isBlank(fr.getMd5hash()) || fr.getObjId() <= 0) { continue; @@ -257,9 +297,10 @@ private void handleBatch(List<FileRecord> fileRecords) { .computeIfAbsent(sanitizedMd5, (k) -> new ArrayList<>()) .add(fr.getObjId()); - md5Hashes.add(sanitizedMd5); } + List<String> md5Hashes = new ArrayList<>(md5ToObjId.keySet()); + if (md5Hashes.isEmpty()) { return; } @@ -279,13 +320,6 @@ private void handleBatch(List<FileRecord> fileRecords) { return; } - // if the size of this batch will exceed limit, shrink list to limit and fail after processing - boolean exceededScanLimit = false; - if (remainingScans < md5Hashes.size()) { - md5Hashes = md5Hashes.subList(0, (int) remainingScans); - exceededScanLimit = true; - } - // using auth token, get results List<CTCloudBean> repResult = ctApiDAO.getReputationResults( new AuthenticatedRequestData(licenseInfo.getDecryptedLicense(), authTokenResponse), @@ -325,16 +359,6 @@ private void handleBatch(List<FileRecord> fileRecords) { if (!CollectionUtils.isEmpty(createdArtifacts)) { tskCase.getBlackboard().postArtifacts(createdArtifacts, Bundle.MalwareScanIngestModuleFactory_displayName(), ingestJobId); } - - // if we only processed part of the batch, after processing, notify that we are out of scans. - if (exceededScanLimit) { - runState = RunState.DISABLED; - notifyWarning( - Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title(), - Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc(), - null); - return; - } } } catch (Exception ex) { notifyWarning( @@ -467,7 +491,7 @@ synchronized void shutDown() { // flush any remaining items try { - batchProcessor.flush(true); + batchProcessor.flushAndReset(); } catch (InterruptedException ex) { notifyWarning( Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_title(), @@ -476,7 +500,6 @@ synchronized void shutDown() { } finally { // set state to shut down and clear any remaining runState = RunState.SHUT_DOWN; - batchProcessor.clearCurrentBatch(); } }