diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTApiDAO.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTApiDAO.java index 25e264e49a0e0d025753afee41ef672919b3bfdb..c358feec61a460b49c71addeeb741237198a26f6 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTApiDAO.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTApiDAO.java @@ -27,7 +27,9 @@ import com.basistech.df.cybertriage.autopsy.ctapi.json.FileReputationRequest; import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseRequest; import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseResponse; +import com.basistech.df.cybertriage.autopsy.ctapi.json.MetadataUploadRequest; import com.basistech.df.cybertriage.autopsy.ctapi.util.CTHostIDGenerationUtil; +import java.io.InputStream; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -45,6 +47,8 @@ public class CTApiDAO { private static final String LICENSE_REQUEST_PATH = "/_ah/api/license/v1/activate"; private static final String AUTH_TOKEN_REQUEST_PATH = "/_ah/api/auth/v2/generate_token"; private static final String CTCLOUD_SERVER_HASH_PATH = "/_ah/api/reputation/v1/query/file/hash/md5?query_types=CORRELATION,MALWARE"; + private static final String CTCLOUD_UPLOAD_FILE_METADATA_PATH = "/_ah/api/reputation/v1/upload/meta"; + private static final String AUTOPSY_PRODUCT = "AUTOPSY"; private static final CTApiDAO instance = new CTApiDAO(); @@ -74,15 +78,27 @@ public LicenseResponse getLicenseInfo(String licenseString) throws CTCloudExcept } public AuthTokenResponse getAuthToken(DecryptedLicenseResponse decrypted) throws CTCloudException { + return getAuthToken(decrypted, false); + } + + public AuthTokenResponse getAuthToken(DecryptedLicenseResponse decrypted, boolean fileUpload) throws CTCloudException { AuthTokenRequest authTokenRequest = new AuthTokenRequest() .setAutopsyVersion(getAppVersion()) - .setRequestFileUpload(false) + .setRequestFileUpload(fileUpload) .setBoostLicenseId(decrypted.getBoostLicenseId()) .setHostId(decrypted.getLicenseHostId()); return httpClient.doPost(AUTH_TOKEN_REQUEST_PATH, authTokenRequest, AuthTokenResponse.class); } + public void uploadFile(String url, String fileName, InputStream fileIs) throws CTCloudException { + httpClient.doFileUploadPost(url, fileName, fileIs); + } + + public void uploadMeta(AuthenticatedRequestData authenticatedRequestData, MetadataUploadRequest metaRequest) throws CTCloudException { + httpClient.doPost(CTCLOUD_UPLOAD_FILE_METADATA_PATH, getAuthParams(authenticatedRequestData), metaRequest, null); + } + private static Map<String, String> getAuthParams(AuthenticatedRequestData authenticatedRequestData) { return new HashMap<String, String>() { { diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudException.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudException.java index 52d586ee5366add0266082c26129c9ee208fce46..95605c4bcee07cd28a31c0ff5a4cce2cb84f8e7a 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudException.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudException.java @@ -76,8 +76,7 @@ public ErrorCode getErrorCode() { public String getErrorDetails() { if(getErrorCode() == CTCloudException.ErrorCode.UNKNOWN && Objects.nonNull(getCause())){ - return String.format("Malware scan error %s occurred. Please try \"Re Scan\" from the dashboard to attempt Malware scaning again. " - + "\nPlease contact Basis support at %s for help if the problem presists.", + return String.format("An API error %s occurred. Please try again, and contact Basis support at %s for help if the problem persists.", StringUtils.isNotBlank(getCause().getLocalizedMessage()) ? "("+getCause().getLocalizedMessage()+")": "(Unknown)", Constants.SUPPORT_AT_CYBERTRIAGE_DOT_COM ); }else { 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 9d4b189ee9720faa9d0f4e9a8ebbca5127111dbc..0a280c87e89bc5f11b52e9f42930620bddf33e2b 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java @@ -21,42 +21,37 @@ import com.basistech.df.cybertriage.autopsy.ctapi.util.ObjectMapperUtil; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; -import java.net.Authenticator; -import java.net.InetAddress; -import java.net.PasswordAuthentication; +import java.io.InputStream; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; -import java.net.UnknownHostException; import java.security.KeyManagementException; +import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.logging.Level; +import java.util.stream.Stream; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; import org.apache.http.HttpStatus; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.NTCredentials; -import org.apache.http.client.CredentialsProvider; -import org.apache.http.client.config.AuthSchemes; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; @@ -64,102 +59,83 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.MultipartEntityBuilder; import org.sleuthkit.autopsy.coreutils.Logger; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; -import org.apache.http.impl.client.SystemDefaultCredentialsProvider; import org.apache.http.impl.client.WinHttpClients; +import org.apache.http.impl.conn.SystemDefaultRoutePlanner; +import org.apache.http.ssl.SSLInitializationException; +import org.netbeans.core.ProxySettings; +import org.openide.util.Lookup; import org.sleuthkit.autopsy.coreutils.Version; /** * Makes the http requests to CT cloud. + * + * NOTE: regarding proxy settings, the host and port are handled by the + * NbProxySelector. Any proxy authentication is handled by NbAuthenticator which + * is installed at startup (i.e. NbAuthenticator.install). See + * GeneralOptionsModel.testHttpConnection to see how the general options panel + * tests the connection. */ -public class CTCloudHttpClient { +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( - AuthSchemes.SPNEGO, - AuthSchemes.KERBEROS, - AuthSchemes.NTLM, - AuthSchemes.CREDSSP, - AuthSchemes.DIGEST, - AuthSchemes.BASIC)); + private static final String NB_PROXY_SELECTOR_NAME = "org.netbeans.core.NbProxySelector"; private static final int CONNECTION_TIMEOUT_MS = 58 * 1000; // milli sec + private static final CTCloudHttpClient instance = new CTCloudHttpClient(); + public static CTCloudHttpClient getInstance() { return instance; } private final ObjectMapper mapper = ObjectMapperUtil.getInstance().getDefaultObjectMapper(); private final SSLContext sslContext; - private String hostName = null; + private final ProxySelector proxySelector; private CTCloudHttpClient() { // leave as null for now unless we want to customize this at a later date - this.sslContext = null; + this.sslContext = createSSLContext(); + this.proxySelector = getProxySelector(); } - private ProxySettingArgs getProxySettings() { - if (StringUtils.isBlank(hostName)) { - try { - hostName = InetAddress.getLocalHost().getCanonicalHostName(); - } catch (UnknownHostException ex) { - LOGGER.log(Level.WARNING, "An error occurred while fetching the hostname", ex); - } - } + private static URI getUri(String host, String path, Map<String, String> urlReqParams) throws URISyntaxException { + String url = host + path; + URIBuilder builder = new URIBuilder(url); - int proxyPort = 0; - if (StringUtils.isNotBlank(ProxySettings.getHttpPort())) { - try { - proxyPort = Integer.parseInt(ProxySettings.getHttpsPort()); - } catch (NumberFormatException ex) { - LOGGER.log(Level.WARNING, "Unable to convert port to integer"); + if (!MapUtils.isEmpty(urlReqParams)) { + for (Entry<String, String> e : urlReqParams.entrySet()) { + String key = e.getKey(); + String value = e.getValue(); + if (StringUtils.isNotBlank(key) || StringUtils.isNotBlank(value)) { + builder.addParameter(key, value); + } } } - return new ProxySettingArgs( - ProxySettings.getProxyType() != ProxySettings.DIRECT_CONNECTION, - hostName, - ProxySettings.getHttpsHost(), - proxyPort, - ProxySettings.getAuthenticationUsername(), - ProxySettings.getAuthenticationPassword(), - null - ); + return builder.build(); } - + public <O> O doPost(String urlPath, Object jsonBody, Class<O> classType) throws CTCloudException { return doPost(urlPath, Collections.emptyMap(), jsonBody, classType); } public <O> O doPost(String urlPath, Map<String, String> urlReqParams, Object jsonBody, Class<O> classType) throws CTCloudException { - String url = HOST_URL + urlPath; - try { + URI postURI = null; + try { + postURI = getUri(HOST_URL, urlPath, urlReqParams); LOGGER.log(Level.INFO, "initiating http connection to ctcloud server"); - try (CloseableHttpClient httpclient = createConnection(getProxySettings(), sslContext)) { - URIBuilder builder = new URIBuilder(url); - - if (!MapUtils.isEmpty(urlReqParams)) { - for (Entry<String, String> e : urlReqParams.entrySet()) { - String key = e.getKey(); - String value = e.getValue(); - if (StringUtils.isNotBlank(key) || StringUtils.isNotBlank(value)) { - builder.addParameter(key, value); - } - } - } + try (CloseableHttpClient httpclient = createConnection(proxySelector, sslContext)) { - URI postURI = builder.build(); HttpPost postRequest = new HttpPost(postURI); - configureRequestTimeout(postRequest); postRequest.setHeader("Content-type", "application/json"); @@ -177,10 +153,14 @@ public <O> O doPost(String urlPath, Map<String, String> urlReqParams, Object jso if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { LOGGER.log(Level.INFO, "Response Received. - Status OK"); // Parse Response - HttpEntity entity = response.getEntity(); - String entityStr = EntityUtils.toString(entity); - O respObj = mapper.readValue(entityStr, classType); - return respObj; + if (classType != null) { + HttpEntity entity = response.getEntity(); + String entityStr = EntityUtils.toString(entity); + O respObj = mapper.readValue(entityStr, classType); + return respObj; + } else { + return null; + } } else { LOGGER.log(Level.WARNING, "Response Received. - Status Error {}", response.getStatusLine()); handleNonOKResponse(response, ""); @@ -191,16 +171,64 @@ public <O> O doPost(String urlPath, Map<String, String> urlReqParams, Object jso } } } catch (IOException ex) { - LOGGER.log(Level.WARNING, "IO Exception raised when connecting to CT Cloud using " + url, ex); + LOGGER.log(Level.WARNING, "IO Exception raised when connecting to CT Cloud using " + postURI, ex); + throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR, ex); + } catch (SSLInitializationException ex) { + LOGGER.log(Level.WARNING, "No such algorithm exception raised when creating SSL connection for CT Cloud using " + postURI, ex); throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR, ex); } catch (URISyntaxException ex) { - LOGGER.log(Level.WARNING, "Wrong URL syntax for CT Cloud " + url, ex); + LOGGER.log(Level.WARNING, "Wrong URL syntax for CT Cloud " + postURI, ex); throw new CTCloudException(CTCloudException.ErrorCode.UNKNOWN, ex); } return null; } + public void doFileUploadPost(String fullUrlPath, String fileName, InputStream fileIs) throws CTCloudException { + URI postUri; + try { + postUri = new URI(fullUrlPath); + } catch (URISyntaxException ex) { + LOGGER.log(Level.WARNING, "Wrong URL syntax for CT Cloud " + fullUrlPath, ex); + throw new CTCloudException(CTCloudException.ErrorCode.UNKNOWN, ex); + } + + try (CloseableHttpClient httpclient = createConnection(proxySelector, sslContext)) { + LOGGER.log(Level.INFO, "initiating http post request to ctcloud server " + fullUrlPath); + HttpPost post = new HttpPost(postUri); + configureRequestTimeout(post); + + post.addHeader("Connection", "keep-alive"); + + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addBinaryBody( + "file", + fileIs, + ContentType.APPLICATION_OCTET_STREAM, + fileName + ); + + HttpEntity multipart = builder.build(); + post.setEntity(multipart); + + try (CloseableHttpResponse response = httpclient.execute(post)) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) { + LOGGER.log(Level.INFO, "Response Received. - Status OK"); + } else { + LOGGER.log(Level.WARNING, MessageFormat.format("Response Received. - Status Error {0}", response.getStatusLine())); + handleNonOKResponse(response, fileName); + } + } + } catch (SSLInitializationException ex) { + LOGGER.log(Level.WARNING, "SSL exception raised when connecting to Reversing Labs for file content upload ", ex); + throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR, ex); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "IO Exception raised when connecting to Reversing Labs for file content upload ", ex); + throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR, ex); + } + } + /** * A generic way to handle the HTTP response - when the response code is NOT * 200 OK. @@ -263,147 +291,140 @@ private void configureRequestTimeout(HttpRequestBase request) { } /** - * Creates a connection to CT Cloud with the given arguments. - * @param proxySettings The network proxy settings. - * @param sslContext The ssl context or null. - * @return The connection to CT Cloud. + * Get ProxySelector present (favoring NbProxySelector if present). + * + * @return The found ProxySelector or null. */ - private static CloseableHttpClient createConnection(ProxySettingArgs proxySettings, SSLContext sslContext) { - HttpClientBuilder builder = getHttpClientBuilder(proxySettings); - - if (sslContext != null) { - builder.setSSLContext(sslContext); - } - return builder.build(); + private static ProxySelector getProxySelector() { + Collection<? extends ProxySelector> selectors = Lookup.getDefault().lookupAll(ProxySelector.class); + return (selectors != null ? selectors.stream() : Stream.empty()) + .filter(s -> s != null) + .map(s -> (ProxySelector) s) + .sorted((a, b) -> { + String aName = a.getClass().getCanonicalName(); + String bName = b.getClass().getCanonicalName(); + boolean aIsNb = aName.equalsIgnoreCase(NB_PROXY_SELECTOR_NAME); + boolean bIsNb = bName.equalsIgnoreCase(NB_PROXY_SELECTOR_NAME); + if (aIsNb == bIsNb) { + return StringUtils.compareIgnoreCase(aName, bName); + } else { + return aIsNb ? -1 : 1; + } + }) + .findFirst() + // TODO take this out to remove proxy selector logging + .map(s -> new LoggingProxySelector(s)) + .orElse(null); } - private static HttpClientBuilder getHttpClientBuilder(ProxySettingArgs proxySettings) { - - if (proxySettings.isSystemOrManualProxy()) { - - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - LOGGER.info("Requesting Password Authentication..."); - return super.getPasswordAuthentication(); - } - }); - - HttpClientBuilder builder = null; - HttpHost proxyHost = null; - CredentialsProvider proxyCredsProvider = null; - RequestConfig config = null; + /** + * Create an SSLContext object using our in-memory keystore. + * + * @return + */ + private static SSLContext createSSLContext() { + LOGGER.log(Level.INFO, "Creating custom SSL context"); + try { - if (Objects.nonNull(proxySettings.getProxyHostname()) && proxySettings.getProxyPort() > 0) { - proxyHost = new HttpHost(proxySettings.getProxyHostname(), proxySettings.getProxyPort()); + // I'm not sure how much of this is really necessary to set up, but it works + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + KeyManager[] keyManagers = getKeyManagers(); + TrustManager[] trustManagers = getTrustManagers(); + sslContext.init(keyManagers, trustManagers, new SecureRandom()); + return sslContext; + } catch (NoSuchAlgorithmException | KeyManagementException ex) { + LOGGER.log(Level.SEVERE, "Error creating SSL context", ex); + return null; + } + } - proxyCredsProvider = getProxyCredentialsProvider(proxySettings); - if (StringUtils.isNotBlank(proxySettings.getAuthScheme())) { - if (!DEFAULT_SCHEME_PRIORITY.get(0).equalsIgnoreCase(proxySettings.getAuthScheme())) { - DEFAULT_SCHEME_PRIORITY.removeIf(s -> s.equalsIgnoreCase(proxySettings.getAuthScheme())); - DEFAULT_SCHEME_PRIORITY.add(0, proxySettings.getAuthScheme()); - } - } - config = RequestConfig.custom().setProxyPreferredAuthSchemes(DEFAULT_SCHEME_PRIORITY).build(); - } + // jvm default key manager + // based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909 + private static KeyManager[] getKeyManagers() { + LOGGER.log(Level.INFO, "Using default algorithm to create trust store: " + KeyManagerFactory.getDefaultAlgorithm()); + try { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(null, null); + return kmf.getKeyManagers(); + } catch (NoSuchAlgorithmException | KeyStoreException | UnrecoverableKeyException ex) { + LOGGER.log(Level.SEVERE, "Error getting KeyManagers", ex); + return new KeyManager[0]; + } - if (Objects.isNull(proxyCredsProvider) && WinHttpClients.isWinAuthAvailable()) { - builder = WinHttpClients.custom(); - builder.useSystemProperties(); - LOGGER.log(Level.WARNING, "Using Win HTTP Client"); - } else { - builder = HttpClients.custom(); - builder.setDefaultRequestConfig(config); - if (Objects.nonNull(proxyCredsProvider)) { // make sure non null proxycreds before setting it - builder.setDefaultCredentialsProvider(proxyCredsProvider); - } - LOGGER.log(Level.WARNING, "Using default http client"); - } - if (Objects.nonNull(proxyHost)) { - builder.setProxy(proxyHost); - LOGGER.log(Level.WARNING, MessageFormat.format("Using proxy {0}", proxyHost)); - } + } - return builder; - } else { - return HttpClients.custom(); + // jvm default trust store + // based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909 + private static TrustManager[] getTrustManagers() { + try { + LOGGER.log(Level.INFO, "Using default algorithm to create trust store: " + TrustManagerFactory.getDefaultAlgorithm()); + TrustManagerFactory tmf + = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + X509TrustManager tm = (X509TrustManager) tmf.getTrustManagers()[0]; + + return new TrustManager[]{tm}; + } catch (KeyStoreException | NoSuchAlgorithmException ex) { + LOGGER.log(Level.SEVERE, "Error getting TrustManager", ex); + return new TrustManager[0]; } } /** - * Returns a CredentialsProvider for proxy, if one is configured. + * Creates a connection to CT Cloud with the given arguments. * - * @return CredentialsProvider, if a proxy is configured with credentials, - * null otherwise + * @param proxySelector The proxy selector. + * @param sslContext The ssl context or null. + * @return The connection to CT Cloud. */ - private static CredentialsProvider getProxyCredentialsProvider(ProxySettingArgs proxySettings) { - CredentialsProvider proxyCredsProvider = null; - if (proxySettings.isSystemOrManualProxy()) { - if (StringUtils.isNotBlank(proxySettings.getProxyUserId())) { - if (null != proxySettings.getProxyPassword() && proxySettings.getProxyPassword().length > 0) { // Password will be blank for KERBEROS / NEGOTIATE schemes. - proxyCredsProvider = new SystemDefaultCredentialsProvider(); - String userId = proxySettings.getProxyUserId(); - String domain = null; - if (userId.contains("\\")) { - domain = userId.split("\\\\")[0]; - userId = userId.split("\\\\")[1]; - } - String workStation = proxySettings.getHostName(); - proxyCredsProvider.setCredentials(new AuthScope(proxySettings.getProxyHostname(), proxySettings.getProxyPort()), - new NTCredentials(userId, new String(proxySettings.getProxyPassword()), workStation, domain)); - } - } - } + private static CloseableHttpClient createConnection(ProxySelector proxySelector, SSLContext sslContext) throws SSLInitializationException { + HttpClientBuilder builder; - return proxyCredsProvider; - } + if (ProxySettings.getProxyType() != ProxySettings.DIRECT_CONNECTION + && StringUtils.isBlank(ProxySettings.getAuthenticationUsername()) + && ArrayUtils.isEmpty(ProxySettings.getAuthenticationPassword()) + && WinHttpClients.isWinAuthAvailable()) { - private static class ProxySettingArgs { - - private final boolean systemOrManualProxy; - private final String hostName; - private final String proxyHostname; - private final int proxyPort; - private final String proxyUserId; - private final char[] proxyPassword; - private final String authScheme; - - ProxySettingArgs(boolean systemOrManualProxy, String hostName, String proxyHostname, int proxyPort, String proxyUserId, char[] proxyPassword, String authScheme) { - this.systemOrManualProxy = systemOrManualProxy; - this.hostName = hostName; - this.proxyHostname = proxyHostname; - this.proxyPort = proxyPort; - this.proxyUserId = proxyUserId; - this.proxyPassword = proxyPassword; - this.authScheme = authScheme; + builder = WinHttpClients.custom(); + builder.useSystemProperties(); + LOGGER.log(Level.WARNING, "Using Win HTTP Client"); + } else { + builder = HttpClients.custom(); + // builder.setDefaultRequestConfig(config); + LOGGER.log(Level.WARNING, "Using default http client"); } - boolean isSystemOrManualProxy() { - return systemOrManualProxy; + if (sslContext != null) { + builder.setSSLContext(sslContext); } - String getHostName() { - return hostName; + if (proxySelector != null) { + builder.setRoutePlanner(new SystemDefaultRoutePlanner(proxySelector)); } - String getProxyHostname() { - return proxyHostname; - } + return builder.build(); + } - int getProxyPort() { - return proxyPort; - } + private static class LoggingProxySelector extends ProxySelector { - String getProxyUserId() { - return proxyUserId; + private final ProxySelector delegate; + + public LoggingProxySelector(ProxySelector delegate) { + this.delegate = delegate; } - char[] getProxyPassword() { - return proxyPassword; + @Override + public List<Proxy> select(URI uri) { + List<Proxy> selectedProxies = delegate.select(uri); + LOGGER.log(Level.INFO, MessageFormat.format("Proxy selected for {0} are {1}", uri, selectedProxies)); + return selectedProxies; } - public String getAuthScheme() { - return authScheme; + @Override + public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { + LOGGER.log(Level.WARNING, MessageFormat.format("Connection failed connecting to {0} socket address {1}", uri, sa), ioe); + delegate.connectFailed(uri, sa, ioe); } + } } 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 9587b3cd44dbf4984aac10a9b5639661e5f4a883..8290d6621d0e05f1e6cf12c24c5813539dc837b3 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java @@ -23,7 +23,7 @@ /** * Constants regarding connections to cyber triage cloud. */ -final public class Constants { +final class Constants { public static final String CYBER_TRIAGE = "CyberTriage"; diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/ProxySettings.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/ProxySettings.java deleted file mode 100644 index f710a6ab1bb1641d4f890a70de926e93647bc021..0000000000000000000000000000000000000000 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/ProxySettings.java +++ /dev/null @@ -1,446 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2023 Basis Technology Corp. - * Contact: carrier <at> sleuthkit <dot> org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.basistech.df.cybertriage.autopsy.ctapi; - -import java.net.*; -import java.util.*; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.prefs.PreferenceChangeListener; -import java.util.prefs.Preferences; -import org.netbeans.api.keyring.Keyring; -import org.openide.util.*; -import org.openide.util.lookup.ServiceProvider; - -/** - * Taken from https://raw.githubusercontent.com/apache/netbeans/master/platform/o.n.core/src/org/netbeans/core/ProxySettings.java - * @author Jiri Rechtacek - */ -public class ProxySettings { - - public static final String PROXY_HTTP_HOST = "proxyHttpHost"; // NOI18N - public static final String PROXY_HTTP_PORT = "proxyHttpPort"; // NOI18N - public static final String PROXY_HTTPS_HOST = "proxyHttpsHost"; // NOI18N - public static final String PROXY_HTTPS_PORT = "proxyHttpsPort"; // NOI18N - public static final String PROXY_SOCKS_HOST = "proxySocksHost"; // NOI18N - public static final String PROXY_SOCKS_PORT = "proxySocksPort"; // NOI18N - public static final String NOT_PROXY_HOSTS = "proxyNonProxyHosts"; // NOI18N - public static final String PROXY_TYPE = "proxyType"; // NOI18N - public static final String USE_PROXY_AUTHENTICATION = "useProxyAuthentication"; // NOI18N - public static final String PROXY_AUTHENTICATION_USERNAME = "proxyAuthenticationUsername"; // NOI18N - public static final String PROXY_AUTHENTICATION_PASSWORD = "proxyAuthenticationPassword"; // NOI18N - public static final String USE_PROXY_ALL_PROTOCOLS = "useProxyAllProtocols"; // NOI18N - public static final String DIRECT = "DIRECT"; // NOI18N - public static final String PAC = "PAC"; // NOI18N - - public static final String SYSTEM_PROXY_HTTP_HOST = "systemProxyHttpHost"; // NOI18N - public static final String SYSTEM_PROXY_HTTP_PORT = "systemProxyHttpPort"; // NOI18N - public static final String SYSTEM_PROXY_HTTPS_HOST = "systemProxyHttpsHost"; // NOI18N - public static final String SYSTEM_PROXY_HTTPS_PORT = "systemProxyHttpsPort"; // NOI18N - public static final String SYSTEM_PROXY_SOCKS_HOST = "systemProxySocksHost"; // NOI18N - public static final String SYSTEM_PROXY_SOCKS_PORT = "systemProxySocksPort"; // NOI18N - public static final String SYSTEM_NON_PROXY_HOSTS = "systemProxyNonProxyHosts"; // NOI18N - public static final String SYSTEM_PAC = "systemPAC"; // NOI18N - - // Only for testing purpose (Test connection in General options panel) - public static final String TEST_SYSTEM_PROXY_HTTP_HOST = "testSystemProxyHttpHost"; // NOI18N - public static final String TEST_SYSTEM_PROXY_HTTP_PORT = "testSystemProxyHttpPort"; // NOI18N - public static final String HTTP_CONNECTION_TEST_URL = "https://netbeans.apache.org";// NOI18N - - private static String presetNonProxyHosts; - - /** No proxy is used to connect. */ - public static final int DIRECT_CONNECTION = 0; - - /** Proxy setting is automatically detect in OS. */ - public static final int AUTO_DETECT_PROXY = 1; // as default - - /** Manually set proxy host and port. */ - public static final int MANUAL_SET_PROXY = 2; - - /** Proxy PAC file automatically detect in OS. */ - public static final int AUTO_DETECT_PAC = 3; - - /** Proxy PAC file manually set. */ - public static final int MANUAL_SET_PAC = 4; - - private static final Logger LOGGER = Logger.getLogger(ProxySettings.class.getName()); - - private static Preferences getPreferences() { - return NbPreferences.forModule (ProxySettings.class); - } - - - public static String getHttpHost () { - return normalizeProxyHost (getPreferences ().get (PROXY_HTTP_HOST, "")); - } - - public static String getHttpPort () { - return getPreferences ().get (PROXY_HTTP_PORT, ""); - } - - public static String getHttpsHost () { - if (useProxyAllProtocols ()) { - return getHttpHost (); - } else { - return getPreferences ().get (PROXY_HTTPS_HOST, ""); - } - } - - public static String getHttpsPort () { - if (useProxyAllProtocols ()) { - return getHttpPort (); - } else { - return getPreferences ().get (PROXY_HTTPS_PORT, ""); - } - } - - public static String getSocksHost () { - if (useProxyAllProtocols ()) { - return getHttpHost (); - } else { - return getPreferences ().get (PROXY_SOCKS_HOST, ""); - } - } - - public static String getSocksPort () { - if (useProxyAllProtocols ()) { - return getHttpPort (); - } else { - return getPreferences ().get (PROXY_SOCKS_PORT, ""); - } - } - - public static String getNonProxyHosts () { - String hosts = getPreferences ().get (NOT_PROXY_HOSTS, getDefaultUserNonProxyHosts ()); - return compactNonProxyHosts(hosts); - } - - public static int getProxyType () { - int type = getPreferences ().getInt (PROXY_TYPE, AUTO_DETECT_PROXY); - if (AUTO_DETECT_PROXY == type) { - type = ProxySettings.getSystemPac() != null ? AUTO_DETECT_PAC : AUTO_DETECT_PROXY; - } - return type; - } - - - public static String getSystemHttpHost() { - return getPreferences().get(SYSTEM_PROXY_HTTP_HOST, ""); - } - - public static String getSystemHttpPort() { - return getPreferences().get(SYSTEM_PROXY_HTTP_PORT, ""); - } - - public static String getSystemHttpsHost() { - return getPreferences().get(SYSTEM_PROXY_HTTPS_HOST, ""); - } - - public static String getSystemHttpsPort() { - return getPreferences().get(SYSTEM_PROXY_HTTPS_PORT, ""); - } - - public static String getSystemSocksHost() { - return getPreferences().get(SYSTEM_PROXY_SOCKS_HOST, ""); - } - - public static String getSystemSocksPort() { - return getPreferences().get(SYSTEM_PROXY_SOCKS_PORT, ""); - } - - public static String getSystemNonProxyHosts() { - return getPreferences().get(SYSTEM_NON_PROXY_HOSTS, getModifiedNonProxyHosts("")); - } - - public static String getSystemPac() { - return getPreferences().get(SYSTEM_PAC, null); - } - - - public static String getTestSystemHttpHost() { - return getPreferences().get(TEST_SYSTEM_PROXY_HTTP_HOST, ""); - } - - public static String getTestSystemHttpPort() { - return getPreferences().get(TEST_SYSTEM_PROXY_HTTP_PORT, ""); - } - - - public static boolean useAuthentication () { - return getPreferences ().getBoolean (USE_PROXY_AUTHENTICATION, false); - } - - public static boolean useProxyAllProtocols () { - return getPreferences ().getBoolean (USE_PROXY_ALL_PROTOCOLS, false); - } - - public static String getAuthenticationUsername () { - return getPreferences ().get (PROXY_AUTHENTICATION_USERNAME, ""); - } - - public static char[] getAuthenticationPassword () { - String old = getPreferences().get(PROXY_AUTHENTICATION_PASSWORD, null); - if (old != null) { - getPreferences().remove(PROXY_AUTHENTICATION_PASSWORD); - setAuthenticationPassword(old.toCharArray()); - } - char[] pwd = Keyring.read(PROXY_AUTHENTICATION_PASSWORD); - return pwd != null ? pwd : new char[0]; - } - - public static void setAuthenticationPassword(char[] password) { - Keyring.save(ProxySettings.PROXY_AUTHENTICATION_PASSWORD, password, - // XXX consider including getHttpHost and/or getHttpsHost - NbBundle.getMessage(ProxySettings.class, "ProxySettings.password.description")); // NOI18N - } - - public static void addPreferenceChangeListener (PreferenceChangeListener l) { - getPreferences ().addPreferenceChangeListener (l); - } - - public static void removePreferenceChangeListener (PreferenceChangeListener l) { - getPreferences ().removePreferenceChangeListener (l); - } - - private static String getPresetNonProxyHosts () { - if (presetNonProxyHosts == null) { - presetNonProxyHosts = System.getProperty ("http.nonProxyHosts", ""); // NOI18N - } - return presetNonProxyHosts; - } - - private static String getDefaultUserNonProxyHosts () { - return getModifiedNonProxyHosts (getSystemNonProxyHosts ()); - } - - - private static String concatProxies(String... proxies) { - StringBuilder sb = new StringBuilder(); - for (String n : proxies) { - if (n == null) { - continue; - } - n = n.trim(); - if (n.isEmpty()) { - continue; - } - if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '|') { // NOI18N - if (!n.startsWith("|")) { // NOI18N - sb.append('|'); // NOI18N - } - } - sb.append(n); - } - return sb.toString(); - } - - private static String getModifiedNonProxyHosts (String systemPreset) { - String fromSystem = systemPreset.replace (";", "|").replace (",", "|"); //NOI18N - String fromUser = getPresetNonProxyHosts () == null ? "" : getPresetNonProxyHosts ().replace (";", "|").replace (",", "|"); //NOI18N - if (Utilities.isWindows ()) { - fromSystem = addReguralToNonProxyHosts (fromSystem); - } - final String staticNonProxyHosts = NbBundle.getMessage(ProxySettings.class, "StaticNonProxyHosts"); // NOI18N - String nonProxy = concatProxies(fromUser, fromSystem, staticNonProxyHosts); // NOI18N - String localhost; - try { - localhost = InetAddress.getLocalHost().getHostName(); - if (!"localhost".equals(localhost)) { // NOI18N - nonProxy = nonProxy + "|" + localhost; // NOI18N - } else { - // Avoid this error when hostname == localhost: - // Error in http.nonProxyHosts system property: sun.misc.REException: localhost is a duplicate - } - } - catch (UnknownHostException e) { - // OK. Sometimes a hostname is assigned by DNS, but a computer - // is later pulled off the network. It may then produce a bogus - // name for itself which can't actually be resolved. Normally - // "localhost" is aliased to 127.0.0.1 anyway. - } - /* per Milan's agreement it's removed. See issue #89868 - try { - String localhost2 = InetAddress.getLocalHost().getCanonicalHostName(); - if (!"localhost".equals(localhost2) && !localhost2.equals(localhost)) { // NOI18N - nonProxy = nonProxy + "|" + localhost2; // NOI18N - } else { - // Avoid this error when hostname == localhost: - // Error in http.nonProxyHosts system property: sun.misc.REException: localhost is a duplicate - } - } - catch (UnknownHostException e) { - // OK. Sometimes a hostname is assigned by DNS, but a computer - // is later pulled off the network. It may then produce a bogus - // name for itself which can't actually be resolved. Normally - // "localhost" is aliased to 127.0.0.1 anyway. - } - */ - return compactNonProxyHosts (nonProxy); - } - - - // avoid duplicate hosts - private static String compactNonProxyHosts (String hosts) { - StringTokenizer st = new StringTokenizer(hosts, ","); //NOI18N - StringBuilder nonProxyHosts = new StringBuilder(); - while (st.hasMoreTokens()) { - String h = st.nextToken().trim(); - if (h.length() == 0) { - continue; - } - if (nonProxyHosts.length() > 0) { - nonProxyHosts.append("|"); // NOI18N - } - nonProxyHosts.append(h); - } - st = new StringTokenizer (nonProxyHosts.toString(), "|"); //NOI18N - Set<String> set = new HashSet<String> (); - StringBuilder compactedProxyHosts = new StringBuilder(); - while (st.hasMoreTokens ()) { - String t = st.nextToken (); - if (set.add (t.toLowerCase (Locale.US))) { - if (compactedProxyHosts.length() > 0) { - compactedProxyHosts.append('|'); // NOI18N - } - compactedProxyHosts.append(t); - } - } - return compactedProxyHosts.toString(); - } - - private static String addReguralToNonProxyHosts (String nonProxyHost) { - StringTokenizer st = new StringTokenizer (nonProxyHost, "|"); // NOI18N - StringBuilder reguralProxyHosts = new StringBuilder(); - while (st.hasMoreTokens ()) { - String t = st.nextToken (); - if (t.indexOf ('*') == -1) { //NOI18N - t = t + '*'; //NOI18N - } - if (reguralProxyHosts.length() > 0) - reguralProxyHosts.append('|'); // NOI18N - reguralProxyHosts.append(t); - } - - return reguralProxyHosts.toString(); - } - - public static String normalizeProxyHost (String proxyHost) { - if (proxyHost.toLowerCase (Locale.US).startsWith ("http://")) { // NOI18N - return proxyHost.substring (7, proxyHost.length ()); - } else { - return proxyHost; - } - } - - private static InetSocketAddress analyzeProxy(URI uri) { - Parameters.notNull("uri", uri); // NOI18N - List<Proxy> proxies = ProxySelector.getDefault().select(uri); - assert proxies != null : "ProxySelector cannot return null for " + uri; // NOI18N - assert !proxies.isEmpty() : "ProxySelector cannot return empty list for " + uri; // NOI18N - String protocol = uri.getScheme(); - Proxy p = proxies.get(0); - if (Proxy.Type.DIRECT == p.type()) { - // return null for DIRECT proxy - return null; - } - if (protocol == null - || ((protocol.startsWith("http") || protocol.equals("ftp")) && Proxy.Type.HTTP == p.type()) // NOI18N - || !(protocol.startsWith("http") || protocol.equals("ftp"))) { // NOI18N - if (p.address() instanceof InetSocketAddress) { - // check is - //assert ! ((InetSocketAddress) p.address()).isUnresolved() : p.address() + " must be resolved address."; - return (InetSocketAddress) p.address(); - } else { - LOGGER.log(Level.INFO, p.address() + " is not instanceof InetSocketAddress but " + p.address().getClass()); // NOI18N - return null; - } - } else { - return null; - } - } - - public static void reload() { - Reloader reloader = Lookup.getDefault().lookup(Reloader.class); - reloader.reload(); - } - - @ServiceProvider(service = NetworkSettings.ProxyCredentialsProvider.class, position = 1000) - public static class NbProxyCredentialsProvider extends NetworkSettings.ProxyCredentialsProvider { - - @Override - public String getProxyHost(URI u) { - if (getPreferences() == null) { - return null; - } - InetSocketAddress sa = analyzeProxy(u); - return sa == null ? null : sa.getHostName(); - } - - @Override - public String getProxyPort(URI u) { - if (getPreferences() == null) { - return null; - } - InetSocketAddress sa = analyzeProxy(u); - return sa == null ? null : Integer.toString(sa.getPort()); - } - - @Override - protected String getProxyUserName(URI u) { - if (getPreferences() == null) { - return null; - } - return ProxySettings.getAuthenticationUsername(); - } - - @Override - protected char[] getProxyPassword(URI u) { - if (getPreferences() == null) { - return null; - } - return ProxySettings.getAuthenticationPassword(); - } - - @Override - protected boolean isProxyAuthentication(URI u) { - if (getPreferences() == null) { - return false; - } - return getPreferences().getBoolean(USE_PROXY_AUTHENTICATION, false); - } - - } - - /** A bridge between <code>o.n.core</code> and <code>core.network</code>. - * An implementation of this class brings a facility to reload Network Proxy Settings - * from underlying OS. - * The module <code>core.network</code> provides a implementation which may be accessible - * via <code>Lookup.getDefault()</code>. It's not guaranteed any implementation is found on all distribution. - * - * @since 3.40 - */ - public abstract static class Reloader { - - /** Reloads Network Proxy Settings from underlying system. - * - */ - public abstract void reload(); - } -} diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/DecryptedLicenseResponse.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/DecryptedLicenseResponse.java index c6f91721ef7d05f679963727e0b787c11b2ea04a..d939d6ed753343a757a0a9c0d55cc324ade23e21 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/DecryptedLicenseResponse.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/DecryptedLicenseResponse.java @@ -38,7 +38,7 @@ public class DecryptedLicenseResponse { private final Long fileUploads; private final Instant activationTime; private final String product; - private final String limitType; + private final LicenseLimitType limitType; private final String timezone; private final String customerEmail; private final String customerName; @@ -54,7 +54,7 @@ public DecryptedLicenseResponse( @JsonDeserialize(using = InstantEpochMillisDeserializer.class) @JsonProperty("activationTime") Instant activationTime, @JsonProperty("product") String product, - @JsonProperty("limitType") String limitType, + @JsonProperty("limitType") LicenseLimitType limitType, @JsonProperty("timezone") String timezone, @JsonProperty("customerEmail") String customerEmail, @JsonProperty("customerName") String customerName @@ -96,7 +96,7 @@ public String getProduct() { return product; } - public String getLimitType() { + public LicenseLimitType getLimitType() { return limitType; } diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseLimitType.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseLimitType.java new file mode 100644 index 0000000000000000000000000000000000000000..7d185f86aef9aabdbe2e13c41e4730d01b0bcd8e --- /dev/null +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseLimitType.java @@ -0,0 +1,30 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2023 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.basistech.df.cybertriage.autopsy.ctapi.json; + +/** + * The limit type (and reset) for the license. + */ +public enum LicenseLimitType { + HOURLY, + DAILY, + WEEKLY, + MONTHLY, + NO_RESET; +} diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/MetadataUploadRequest.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/MetadataUploadRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..7d7e1e7f6473e7f405368fa1f6671d72806f71a9 --- /dev/null +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/MetadataUploadRequest.java @@ -0,0 +1,109 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2023 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.basistech.df.cybertriage.autopsy.ctapi.json; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class MetadataUploadRequest { + + @JsonProperty("file_upload_url") + private String fileUploadUrl; + + @JsonProperty("sha1") + private String sha1; + + @JsonProperty("sha256") + private String sha256; + + @JsonProperty("md5") + private String md5; + + @JsonProperty("filePath") + private String filePath; + + @JsonProperty("fileSize") + private Long fileSizeBytes; + + @JsonProperty("createdDate") + private Long createdDate; + + public String getFileUploadUrl() { + return fileUploadUrl; + } + + public MetadataUploadRequest setFileUploadUrl(String fileUploadUrl) { + this.fileUploadUrl = fileUploadUrl; + return this; + } + + public String getSha1() { + return sha1; + } + + public MetadataUploadRequest setSha1(String sha1) { + this.sha1 = sha1; + return this; + } + + public String getSha256() { + return sha256; + } + + public MetadataUploadRequest setSha256(String sha256) { + this.sha256 = sha256; + return this; + } + + public String getMd5() { + return md5; + } + + public MetadataUploadRequest setMd5(String md5) { + this.md5 = md5; + return this; + } + + public String getFilePath() { + return filePath; + } + + public MetadataUploadRequest setFilePath(String filePath) { + this.filePath = filePath; + return this; + } + + public Long getFileSizeBytes() { + return fileSizeBytes; + } + + public MetadataUploadRequest setFileSizeBytes(Long fileSizeBytes) { + this.fileSizeBytes = fileSizeBytes; + return this; + } + + public Long getCreatedDate() { + return createdDate; + } + + public MetadataUploadRequest setCreatedDate(Long createdDate) { + this.createdDate = createdDate; + return this; + } + +} 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 46f62fdf7a5185355907a8f9980343d9478c03de..840725f5d73abfaaf90261560679ed393718505c 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties @@ -3,7 +3,7 @@ # Click nbfs://nbhost/SystemFileSystem/Templates/Other/properties.properties to edit this template 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.disclaimer.text=<html>The Cyber Triage Malware Scanner module uses 40+ malware scanning engines to identify if Windows executables are malicious. It requires a paid subscription 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> LicenseDisclaimerPanel.border.title=Disclaimer +LicenseDisclaimerPanel.trialLabel.text=You can try a free 7-day trial from 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 46f62fdf7a5185355907a8f9980343d9478c03de..840725f5d73abfaaf90261560679ed393718505c 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 @@ -3,7 +3,7 @@ # Click nbfs://nbhost/SystemFileSystem/Templates/Other/properties.properties to edit this template 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.disclaimer.text=<html>The Cyber Triage Malware Scanner module uses 40+ malware scanning engines to identify if Windows executables are malicious. It requires a paid subscription 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> LicenseDisclaimerPanel.border.title=Disclaimer +LicenseDisclaimerPanel.trialLabel.text=You can try a free 7-day trial from diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.form b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.form index 83e3c8440aa0228fbbdc4166d41dc85f34df8eb0..a88404c592a200dacad0601d41bd8ceec4d8c3f1 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.form +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.form @@ -6,17 +6,18 @@ <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> <TitledBorder title="Disclaimer"> <ResourceString PropertyName="titleX" bundle="com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties" key="LicenseDisclaimerPanel.border.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + <Color PropertyName="color" blue="0" green="0" red="ff" type="rgb"/> </TitledBorder> </Border> </Property> <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> - <Dimension value="[2147483647, 90]"/> + <Dimension value="[2147483647, 108]"/> </Property> <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> - <Dimension value="[562, 90]"/> + <Dimension value="[562, 108]"/> </Property> <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> - <Dimension value="[400, 90]"/> + <Dimension value="[400, 108]"/> </Property> </Properties> <AuxValues> @@ -29,7 +30,7 @@ <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> - <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,90,0,0,2,50"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,108,0,0,2,50"/> </AuxValues> <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> @@ -47,71 +48,139 @@ </AuxValues> <Constraints> <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> - <GridBagConstraints gridX="0" gridY="0" gridWidth="2" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="5" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="5" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/> </Constraint> </Constraints> </Component> - <Component class="javax.swing.JLabel" name="purchaseFromLabel"> - <Properties> - <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties" key="LicenseDisclaimerPanel.purchaseFromLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - </Properties> + <Container class="javax.swing.JPanel" name="spacer"> <AuxValues> <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> </AuxValues> <Constraints> <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> - <GridBagConstraints gridX="-1" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="3" anchor="18" weightX="0.0" weightY="0.0"/> + <GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="1.0"/> </Constraint> </Constraints> - </Component> - <Component class="javax.swing.JLabel" name="link"> - <Properties> - <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties" key="LicenseDisclaimerPanel.link.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> - <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor"> - <Color id="Hand Cursor"/> - </Property> - </Properties> - <Events> - <EventHandler event="mouseClicked" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="linkMouseClicked"/> - </Events> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <EmptySpace min="0" pref="0" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <EmptySpace min="0" pref="0" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> + </Container> + <Container class="javax.swing.JPanel" name="trialPanel"> <AuxValues> <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> </AuxValues> <Constraints> <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> - <GridBagConstraints gridX="-1" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/> + <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="10" weightX="1.0" weightY="0.0"/> </Constraint> </Constraints> - </Component> - <Container class="javax.swing.JPanel" name="spacer"> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JLabel" name="trialLabel"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties" key="LicenseDisclaimerPanel.trialLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="trialLink"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="getHtmlLink(TRIAL_URL)" type="code"/> + </Property> + <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor"> + <Color id="Hand Cursor"/> + </Property> + </Properties> + <Events> + <EventHandler event="mouseClicked" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="trialLinkMouseClicked"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="18" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> + <Container class="javax.swing.JPanel" name="purchasePanel"> <AuxValues> <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> </AuxValues> <Constraints> <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> - <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="1.0"/> + <GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="10" weightX="1.0" weightY="0.0"/> </Constraint> </Constraints> - <Layout> - <DimensionLayout dim="0"> - <Group type="103" groupAlignment="0" attributes="0"> - <EmptySpace min="0" pref="0" max="32767" attributes="0"/> - </Group> - </DimensionLayout> - <DimensionLayout dim="1"> - <Group type="103" groupAlignment="0" attributes="0"> - <EmptySpace min="0" pref="0" max="32767" attributes="0"/> - </Group> - </DimensionLayout> - </Layout> + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JLabel" name="purchaseFromLabel"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties" key="LicenseDisclaimerPanel.purchaseFromLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + <Component class="javax.swing.JLabel" name="purchaseLink"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="getHtmlLink(PURCHASE_URL)" type="code"/> + </Property> + <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor"> + <Color id="Hand Cursor"/> + </Property> + </Properties> + <Events> + <EventHandler event="mouseClicked" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="purchaseLinkMouseClicked"/> + </Events> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="1" gridY="0" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="3" insetsBottom="0" insetsRight="0" anchor="18" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> </Container> </SubComponents> </Form> 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 4299f02d620e69d06770ed0e5b98a326c58069fa..328a252467778a4307ae83cf9e060e4b0212c3b5 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/LicenseDisclaimerPanel.java @@ -32,7 +32,12 @@ 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"; + private static final String TRIAL_URL = "https://cybertriage.com/autopsy-trial"; + private static final String PURCHASE_URL = "https://cybertriage.com/autopsy-checkout"; + + private static String getHtmlLink(String url) { + return "<html><span style=\"color: blue; text-decoration: underline\">" + url + "</span></html>"; + } /** * Creates new form LicenseDisclaimerPanel @@ -52,14 +57,18 @@ private void initComponents() { java.awt.GridBagConstraints gridBagConstraints; javax.swing.JLabel disclaimer = new javax.swing.JLabel(); - javax.swing.JLabel purchaseFromLabel = new javax.swing.JLabel(); - javax.swing.JLabel link = new javax.swing.JLabel(); javax.swing.JPanel spacer = new javax.swing.JPanel(); + javax.swing.JPanel trialPanel = new javax.swing.JPanel(); + javax.swing.JLabel trialLabel = new javax.swing.JLabel(); + javax.swing.JLabel trialLink = new javax.swing.JLabel(); + javax.swing.JPanel purchasePanel = new javax.swing.JPanel(); + javax.swing.JLabel purchaseFromLabel = new javax.swing.JLabel(); + javax.swing.JLabel purchaseLink = new javax.swing.JLabel(); - setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(LicenseDisclaimerPanel.class, "LicenseDisclaimerPanel.border.title"))); // NOI18N - setMaximumSize(new java.awt.Dimension(2147483647, 90)); - setMinimumSize(new java.awt.Dimension(562, 90)); - setPreferredSize(new java.awt.Dimension(400, 90)); + setBorder(javax.swing.BorderFactory.createTitledBorder(null, org.openide.util.NbBundle.getMessage(LicenseDisclaimerPanel.class, "LicenseDisclaimerPanel.border.title"), javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, new java.awt.Font("Segoe UI", 0, 12), new java.awt.Color(255, 0, 0))); // NOI18N + setMaximumSize(new java.awt.Dimension(2147483647, 108)); + setMinimumSize(new java.awt.Dimension(562, 108)); + setPreferredSize(new java.awt.Dimension(400, 108)); setLayout(new java.awt.GridBagLayout()); org.openide.awt.Mnemonics.setLocalizedText(disclaimer, org.openide.util.NbBundle.getMessage(LicenseDisclaimerPanel.class, "LicenseDisclaimerPanel.disclaimer.text")); // NOI18N @@ -67,34 +76,12 @@ private void initComponents() { gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 0; - gridBagConstraints.gridwidth = 2; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; gridBagConstraints.weightx = 1.0; gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); add(disclaimer, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(purchaseFromLabel, org.openide.util.NbBundle.getMessage(LicenseDisclaimerPanel.class, "LicenseDisclaimerPanel.purchaseFromLabel.text")); // NOI18N - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.gridy = 1; - gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; - gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 3); - add(purchaseFromLabel, gridBagConstraints); - - org.openide.awt.Mnemonics.setLocalizedText(link, org.openide.util.NbBundle.getMessage(LicenseDisclaimerPanel.class, "LicenseDisclaimerPanel.link.text")); // NOI18N - link.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); - link.addMouseListener(new java.awt.event.MouseAdapter() { - public void mouseClicked(java.awt.event.MouseEvent evt) { - linkMouseClicked(evt); - } - }); - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.gridy = 1; - gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; - gridBagConstraints.weightx = 1.0; - gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 5); - add(link, gridBagConstraints); - javax.swing.GroupLayout spacerLayout = new javax.swing.GroupLayout(spacer); spacer.setLayout(spacerLayout); spacerLayout.setHorizontalGroup( @@ -108,24 +95,94 @@ public void mouseClicked(java.awt.event.MouseEvent evt) { gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 2; + gridBagConstraints.gridy = 3; gridBagConstraints.weighty = 1.0; add(spacer, gridBagConstraints); + + trialPanel.setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(trialLabel, org.openide.util.NbBundle.getMessage(LicenseDisclaimerPanel.class, "LicenseDisclaimerPanel.trialLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + trialPanel.add(trialLabel, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(trialLink, getHtmlLink(TRIAL_URL)); + trialLink.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); + trialLink.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + trialLinkMouseClicked(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + trialPanel.add(trialLink, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5); + add(trialPanel, gridBagConstraints); + + purchasePanel.setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(purchaseFromLabel, org.openide.util.NbBundle.getMessage(LicenseDisclaimerPanel.class, "LicenseDisclaimerPanel.purchaseFromLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + purchasePanel.add(purchaseFromLabel, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(purchaseLink, getHtmlLink(PURCHASE_URL)); + purchaseLink.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); + purchaseLink.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + purchaseLinkMouseClicked(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 0); + purchasePanel.add(purchaseLink, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5); + add(purchasePanel, gridBagConstraints); }// </editor-fold>//GEN-END:initComponents - private void linkMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_linkMouseClicked + private void purchaseLinkMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_purchaseLinkMouseClicked + gotoLink(PURCHASE_URL); + }//GEN-LAST:event_purchaseLinkMouseClicked + + private void trialLinkMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_trialLinkMouseClicked + gotoLink(TRIAL_URL); + }//GEN-LAST:event_trialLinkMouseClicked + + private void gotoLink(String url) { if (Desktop.isDesktopSupported()) { try { - Desktop.getDesktop().browse(new URI(CHECKOUT_PAGE_URL)); + Desktop.getDesktop().browse(new URI(url)); } catch (IOException | URISyntaxException e) { - LOGGER.log(Level.SEVERE, "Error opening link to: " + CHECKOUT_PAGE_URL, e); + LOGGER.log(Level.SEVERE, "Error opening link to: " + url, e); } } else { LOGGER.log(Level.WARNING, "Desktop API is not supported. Link cannot be opened."); } - }//GEN-LAST:event_linkMouseClicked - - + } // Variables declaration - do not modify//GEN-BEGIN:variables // End of variables declaration//GEN-END:variables } diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties index bd06716288dd34cc480b2c4ae540220ef0714b54..d0395d6a1af9b3129c4a10a0feab3230af9304a1 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties @@ -24,3 +24,5 @@ CTMalwareScannerOptionsPanel.licenseInfoUserLabel.text= EULADialog.cancelButton.text=Cancel EULADialog.acceptButton.text=Accept EULADialog.title=Cyber Triage End User License Agreement +CTMalwareScannerOptionsPanel.fileUploadCheckbox.text=Upload file if hash lookup produces no results +CTMalwareScannerOptionsPanel.fileUploadPanel.border.title=File Upload diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties-MERGED b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties-MERGED index 58a1befb1fec3d8c24c56663ea033db027b5158c..18e60839f3f3f38106cff8b0507a623af5221106 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties-MERGED +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties-MERGED @@ -22,6 +22,10 @@ CTMalwareScannerOptionsPanel.licenseInfoIdLabel.text= CTMalwareScannerOptionsPanel.licenseInfoExpiresLabel.text= CTMalwareScannerOptionsPanel.fileUploadsRemainingLabel.text= CTMalwareScannerOptionsPanel.licenseInfoUserLabel.text= +CTMalwareScannerOptionsPanel_getResetSuffix_daily=/day +CTMalwareScannerOptionsPanel_getResetSuffix_hourly=/hour +CTMalwareScannerOptionsPanel_getResetSuffix_monthly=/month +CTMalwareScannerOptionsPanel_getResetSuffix_weekly=/week CTMalwareScannerOptionsPanel_licenseAddDialog_desc=License Number: CTMalwareScannerOptionsPanel_licenseAddDialog_title=Add a License... CTMalwareScannerOptionsPanel_licenseAddDialogEnteredErr_desc=The license number has already been entered @@ -45,9 +49,11 @@ CTMalwareScannerOptionsPanel_malwareScans_fileUploadsRemaining=File uploads rema # {0} - hashLookupsRemaining CTMalwareScannerOptionsPanel_malwareScans_hashLookupsRemaining=Hash lookups remaining: {0} # {0} - maxDailyFileLookups -CTMalwareScannerOptionsPanel_malwareScans_maxDailyFileLookups=Max file uploads: {0}/day +# {1} - resetSuffix +CTMalwareScannerOptionsPanel_malwareScans_maxDailyFileLookups=Max file uploads: {0}{1} # {0} - maxDailyLookups -CTMalwareScannerOptionsPanel_malwareScans_maxDailyHashLookups=Max Hash lookups: {0}/day +# {1} - resetSuffix +CTMalwareScannerOptionsPanel_malwareScans_maxDailyHashLookups=Max Hash lookups: {0}{1} CTMalwareScannerOptionsPanel_MalwareScansFetcher_apiErr_title=Server Error CTMalwareScannerOptionsPanel_MalwareScansFetcher_localErr_desc=A general error occurred while fetching malware scans information. Please try again later. CTMalwareScannerOptionsPanel_MalwareScansFetcher_localErr_title=General Error @@ -56,3 +62,5 @@ CTOPtionsPanel_loadMalwareScansInfo_loading=Loading... EULADialog.cancelButton.text=Cancel EULADialog.acceptButton.text=Accept EULADialog.title=Cyber Triage End User License Agreement +CTMalwareScannerOptionsPanel.fileUploadCheckbox.text=Upload file if hash lookup produces no results +CTMalwareScannerOptionsPanel.fileUploadPanel.border.title=File Upload 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 608ea6304006f80b9a14299975a12140e2cf7d6c..3bff998901d0fc071686150e58c3eedf4c8d57df 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 @@ -27,7 +27,7 @@ /** * License dialog */ -public class CTLicenseDialog extends javax.swing.JDialog { +class CTLicenseDialog extends javax.swing.JDialog { 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/ctoptions/ctcloud/CTLicensePersistence.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicensePersistence.java index 9f7f3b8bf6c9211f227045fbeb9c94ca09dec7db..717c4f4321348b6cb7a55c98dcb0245a7bddc95a 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicensePersistence.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicensePersistence.java @@ -26,7 +26,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Paths; import java.util.Optional; import java.util.logging.Level; @@ -40,6 +39,7 @@ public class CTLicensePersistence { private static final String CT_SETTINGS_DIR = "CyberTriage"; private static final String CT_LICENSE_FILENAME = "CyberTriageLicense.json"; + private static final String MALWARE_INGEST_SETTINGS_FILENAME = "MalwareIngestSettings.json"; private static final Logger logger = Logger.getLogger(CTLicensePersistence.class.getName()); @@ -91,7 +91,44 @@ public synchronized Optional<LicenseInfo> loadLicenseInfo() { }); } + public synchronized boolean saveMalwareSettings(MalwareIngestSettings malwareIngestSettings) { + if (malwareIngestSettings != null) { + File settingsFile = getMalwareIngestFile(); + try { + settingsFile.getParentFile().mkdirs(); + objectMapper.writeValue(settingsFile, malwareIngestSettings); + return true; + } catch (IOException ex) { + logger.log(Level.WARNING, "There was an error writing malware ingest settings to file: " + settingsFile.getAbsolutePath(), ex); + } + } + + return false; + } + + public synchronized MalwareIngestSettings loadMalwareIngestSettings() { + MalwareIngestSettings settings = null; + File settingsFile = getMalwareIngestFile(); + if (settingsFile.exists() && settingsFile.isFile()) { + try { + settings = objectMapper.readValue(settingsFile, MalwareIngestSettings.class); + } catch (IOException ex) { + logger.log(Level.WARNING, "There was an error reading malware ingest settings from file: " + settingsFile.getAbsolutePath(), ex); + } + } + + if (settings == null) { + settings = new MalwareIngestSettings(); + } + + return settings; + } + private File getCTLicenseFile() { return Paths.get(PlatformUtil.getModuleConfigDirectory(), CT_SETTINGS_DIR, CT_LICENSE_FILENAME).toFile(); } + + private File getMalwareIngestFile() { + return Paths.get(PlatformUtil.getModuleConfigDirectory(), CT_SETTINGS_DIR, MALWARE_INGEST_SETTINGS_FILENAME).toFile(); + } } diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.form b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.form index 77361419b65f0831a21bdd6019e2c7d17309c199..8557f946d95770c3e1462ce2345afdbd4d85f0d5 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.form +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.form @@ -11,11 +11,54 @@ <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> - <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,-109,0,0,1,-29"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,-69,0,0,1,-29"/> </AuxValues> <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> <SubComponents> + <Container class="javax.swing.JPanel" name="fileUploadPanel"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> + <TitledBorder title="File Upload"> + <ResourceString PropertyName="titleX" bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.fileUploadPanel.border.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </TitledBorder> + </Border> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="1.0" weightY="0.0"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/> + <SubComponents> + <Component class="javax.swing.JCheckBox" name="fileUploadCheckbox"> + <Properties> + <Property name="selected" type="boolean" value="true"/> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.fileUploadCheckbox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[32767, 20]"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="fileUploadCheckboxActionPerformed"/> + </Events> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription"> + <GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="5" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="1.0"/> + </Constraint> + </Constraints> + </Component> + </SubComponents> + </Container> <Container class="javax.swing.JPanel" name="licenseInfoPanel"> <Properties> <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.java index 564eeb7ebaa4551baa66808bbe0d6b660b2fedd7..2a09c1ed5518cf3a69d834610c371b93ce16b404 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.java @@ -24,6 +24,7 @@ import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthTokenResponse; import com.basistech.df.cybertriage.autopsy.ctapi.json.DecryptedLicenseResponse; import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseInfo; +import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseLimitType; import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseResponse; import com.basistech.df.cybertriage.autopsy.ctapi.util.LicenseDecryptorUtil; import com.basistech.df.cybertriage.autopsy.ctapi.util.LicenseDecryptorUtil.InvalidLicenseException; @@ -33,6 +34,7 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Optional; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; @@ -112,6 +114,7 @@ public void componentShown(ComponentEvent e) { @Override public synchronized void saveSettings() { ctPersistence.saveLicenseResponse(getLicenseInfo()); + ctPersistence.saveMalwareSettings(getIngestSettings()); } @Override @@ -128,12 +131,27 @@ public synchronized void loadSettings() { if (licenseInfo != null) { loadMalwareScansInfo(licenseInfo); } + + MalwareIngestSettings ingestSettings = ctPersistence.loadMalwareIngestSettings(); + setIngestSettings(ingestSettings); } private synchronized LicenseResponse getLicenseInfo() { return this.licenseInfo == null ? null : this.licenseInfo.getLicenseResponse(); } + private MalwareIngestSettings getIngestSettings() { + return new MalwareIngestSettings() + .setUploadFiles(this.fileUploadCheckbox.isSelected()); + } + + private void setIngestSettings(MalwareIngestSettings ingestSettings) { + if (ingestSettings == null) { + ingestSettings = new MalwareIngestSettings(); + } + this.fileUploadCheckbox.setSelected(ingestSettings.isUploadFiles()); + } + private synchronized void setLicenseDisplay(LicenseInfo licenseInfo, String licenseMessage) { this.licenseInfo = licenseInfo; this.licenseInfoMessage = licenseMessage; @@ -202,6 +220,8 @@ private synchronized void loadMalwareScansInfo(LicenseInfo licenseInfo) { private void initComponents() { java.awt.GridBagConstraints gridBagConstraints; + javax.swing.JPanel fileUploadPanel = new javax.swing.JPanel(); + fileUploadCheckbox = new javax.swing.JCheckBox(); javax.swing.JPanel licenseInfoPanel = new javax.swing.JPanel(); licenseInfoMessageLabel = new javax.swing.JLabel(); licenseInfoUserLabel = new javax.swing.JLabel(); @@ -218,6 +238,35 @@ private void initComponents() { setLayout(new java.awt.GridBagLayout()); + fileUploadPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.fileUploadPanel.border.title"))); // NOI18N + fileUploadPanel.setLayout(new java.awt.GridBagLayout()); + + fileUploadCheckbox.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(fileUploadCheckbox, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.fileUploadCheckbox.text")); // NOI18N + fileUploadCheckbox.setMaximumSize(new java.awt.Dimension(32767, 20)); + fileUploadCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + fileUploadCheckboxActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5); + fileUploadPanel.add(fileUploadCheckbox, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + add(fileUploadPanel, gridBagConstraints); + licenseInfoPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.licenseInfoPanel.border.title"))); // NOI18N licenseInfoPanel.setLayout(new java.awt.GridBagLayout()); @@ -378,6 +427,10 @@ private void licenseInfoAddButtonActionPerformed(java.awt.event.ActionEvent evt) } }//GEN-LAST:event_licenseInfoAddButtonActionPerformed + private void fileUploadCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fileUploadCheckboxActionPerformed + this.firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + }//GEN-LAST:event_fileUploadCheckboxActionPerformed + @NbBundle.Messages({ "# {0} - userName", "# {1} - email", @@ -387,9 +440,11 @@ private void licenseInfoAddButtonActionPerformed(java.awt.event.ActionEvent evt) "# {0} - idNumber", "CTMalwareScannerOptionsPanel_licenseInfo_id=ID: {0}", "# {0} - maxDailyLookups", - "CTMalwareScannerOptionsPanel_malwareScans_maxDailyHashLookups=Max Hash lookups: {0}/day", + "# {1} - resetSuffix", + "CTMalwareScannerOptionsPanel_malwareScans_maxDailyHashLookups=Max Hash lookups: {0}{1}", "# {0} - maxDailyFileLookups", - "CTMalwareScannerOptionsPanel_malwareScans_maxDailyFileLookups=Max file uploads: {0}/day", + "# {1} - resetSuffix", + "CTMalwareScannerOptionsPanel_malwareScans_maxDailyFileLookups=Max file uploads: {0}{1}", "# {0} - countersResetDate", "CTMalwareScannerOptionsPanel_malwareScans_countersReset=Counters reset: {0}", "# {0} - hashLookupsRemaining", @@ -425,7 +480,7 @@ private synchronized void renderLicenseState() { this.malwareScansMessageLabel.setVisible(StringUtils.isNotBlank(this.authTokenMessage)); this.malwareScansMessageLabel.setText(this.authTokenMessage); - if (authTokenResponse == null) { + if (authTokenResponse == null || this.licenseInfo == null) { this.maxHashLookupsLabel.setVisible(false); this.maxFileUploadsLabel.setVisible(false); this.countersResetLabel.setVisible(false); @@ -433,15 +488,62 @@ private synchronized void renderLicenseState() { this.fileUploadsRemainingLabel.setVisible(false); } else { this.maxHashLookupsLabel.setVisible(true); - this.maxHashLookupsLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_maxDailyHashLookups(this.authTokenResponse.getHashLookupLimit())); + this.maxHashLookupsLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_maxDailyHashLookups( + this.authTokenResponse.getHashLookupLimit(), + getResetSuffix(this.licenseInfo.getDecryptedLicense().getLimitType()))); + this.maxFileUploadsLabel.setVisible(true); - this.maxFileUploadsLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_maxDailyFileLookups(this.authTokenResponse.getFileUploadLimit())); + this.maxFileUploadsLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_maxDailyFileLookups( + this.authTokenResponse.getFileUploadLimit(), + getResetSuffix(this.licenseInfo.getDecryptedLicense().getLimitType()))); + this.countersResetLabel.setVisible(true); - this.countersResetLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_countersReset(this.authTokenResponse.getResetDate() == null ? "" : MALWARE_SCANS_RESET_FORMAT.format(this.authTokenResponse.getResetDate()))); + this.countersResetLabel.setText(getCountersResetText(this.licenseInfo.getDecryptedLicense().getLimitType(), this.authTokenResponse)); + this.hashLookupsRemainingLabel.setVisible(true); - this.hashLookupsRemainingLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_hashLookupsRemaining(remaining(this.authTokenResponse.getHashLookupLimit(), this.authTokenResponse.getHashLookupCount()))); + this.hashLookupsRemainingLabel.setText( + Bundle.CTMalwareScannerOptionsPanel_malwareScans_hashLookupsRemaining( + remaining(this.authTokenResponse.getHashLookupLimit(), this.authTokenResponse.getHashLookupCount()))); + this.fileUploadsRemainingLabel.setVisible(true); - this.fileUploadsRemainingLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_fileUploadsRemaining(remaining(this.authTokenResponse.getFileUploadLimit(), this.authTokenResponse.getFileUploadCount()))); + this.fileUploadsRemainingLabel.setText( + Bundle.CTMalwareScannerOptionsPanel_malwareScans_fileUploadsRemaining( + remaining(this.authTokenResponse.getFileUploadLimit(), this.authTokenResponse.getFileUploadCount()))); + } + } + + private static String getCountersResetText(LicenseLimitType limitType, AuthTokenResponse authTokenResponse) { + if (limitType == null || limitType == LicenseLimitType.NO_RESET) { + return ""; + } else { + return Bundle.CTMalwareScannerOptionsPanel_malwareScans_countersReset( + MALWARE_SCANS_RESET_FORMAT.format(authTokenResponse.getResetDate())); + } + } + + @Messages({ + "CTMalwareScannerOptionsPanel_getResetSuffix_hourly=/hour", + "CTMalwareScannerOptionsPanel_getResetSuffix_daily=/day", + "CTMalwareScannerOptionsPanel_getResetSuffix_weekly=/week", + "CTMalwareScannerOptionsPanel_getResetSuffix_monthly=/month" + }) + private String getResetSuffix(LicenseLimitType limitType) { + if (limitType == null) { + return ""; + } + + switch (limitType) { + case HOURLY: + return Bundle.CTMalwareScannerOptionsPanel_getResetSuffix_hourly(); + case DAILY: + return Bundle.CTMalwareScannerOptionsPanel_getResetSuffix_daily(); + case WEEKLY: + return Bundle.CTMalwareScannerOptionsPanel_getResetSuffix_weekly(); + case MONTHLY: + return Bundle.CTMalwareScannerOptionsPanel_getResetSuffix_monthly(); + case NO_RESET: + default: + return ""; } } @@ -501,7 +603,7 @@ protected void done() { try { LicenseResponse licenseResponse = get(); SwingUtilities.invokeLater(() -> acceptEula(licenseResponse)); - } catch (InterruptedException ex) { + } catch (InterruptedException | CancellationException ex) { // ignore cancellation; just load current license setLicenseDisplay(licenseInfo, null); loadMalwareScansInfo(licenseInfo); @@ -557,7 +659,7 @@ protected void done() { AuthTokenResponse authTokenResponse = null; try { authTokenResponse = get(); - } catch (InterruptedException ex) { + } catch (InterruptedException | CancellationException ex) { // ignore cancellation } catch (ExecutionException ex) { if (ex.getCause() != null && ex.getCause() instanceof CTCloudException cloudEx) { @@ -589,6 +691,7 @@ protected void done() { // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JLabel countersResetLabel; + private javax.swing.JCheckBox fileUploadCheckbox; private javax.swing.JLabel fileUploadsRemainingLabel; private javax.swing.JLabel hashLookupsRemainingLabel; private javax.swing.JButton licenseInfoAddButton; diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/EULADialog.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/EULADialog.java index 31056e1f2a982412340a782755ffe77e98c5fdce..0279cafb9b70e5a4b25c9b0df972334e239ddf93 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/EULADialog.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/EULADialog.java @@ -36,7 +36,7 @@ /** * Dialog for displaying the Cyber Triage EULA before the license is saved. */ -public class EULADialog extends javax.swing.JDialog { +class EULADialog extends javax.swing.JDialog { private static final Logger LOGGER = Logger.getLogger(EULADialog.class.getName()); private static final String EULA_RESOURCE = "EULA.htm"; diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/MalwareIngestSettings.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/MalwareIngestSettings.java new file mode 100644 index 0000000000000000000000000000000000000000..11e671d56106a92b37696b0caefc1f89770092d8 --- /dev/null +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/MalwareIngestSettings.java @@ -0,0 +1,37 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2023 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.basistech.df.cybertriage.autopsy.ctoptions.ctcloud; + +/** + * Settings for the malware ingest module. + */ +public class MalwareIngestSettings { + + private boolean uploadFiles = true; + + public boolean isUploadFiles() { + return uploadFiles; + } + + public MalwareIngestSettings setUploadFiles(boolean uploadFiles) { + this.uploadFiles = uploadFiles; + return this; + } + +} 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 eab025a6416569645480ca93437cc353a22ed467..a65d319dc219b5bc5410bde84fe96846618c3dcd 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java @@ -21,14 +21,10 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.function.Consumer; /** @@ -36,7 +32,7 @@ * blocks (and subsequently add and flush operations) until previous batch * finishes. */ -public class BatchProcessor<T> { +class BatchProcessor<T> { private ExecutorService processingExecutorService = Executors.newSingleThreadExecutor(); diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/Bundle.properties-MERGED b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/Bundle.properties-MERGED index 0c97a98c2eeb0a162daa3d5feca53a591f77df7d..4380a9607c8ce4600aabba5cd89473c2aca49e73 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/Bundle.properties-MERGED +++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/Bundle.properties-MERGED @@ -1,3 +1,7 @@ +MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_desc=Waiting for all uploaded files to complete scanning. +MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_title=Waiting for File Upload Results +MalwareScanIngestModule_longPollForNotFound_timeout_desc=There was a timeout while waiting for file uploads to be processed. Please try again later. +MalwareScanIngestModule_longPollForNotFound_timeout_title=File Upload Results Timeout MalwareScanIngestModule_malwareTypeDisplayName=Malware # {0} - errorResponse MalwareScanIngestModule_SharedProcessing_authTokenResponseError_desc=Received error: ''{0}'' when fetching the API authentication token for the license @@ -6,6 +10,8 @@ MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No=NO MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes=YES MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc=The remaining hash lookups for this license have been exhausted MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title=Hash Lookups Exhausted +MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_desc=Not all files were processed because hash lookup limits were exceeded. Please try again when your limits reset. +MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_title=Lookup Limits Exceeded MalwareScanIngestModule_SharedProcessing_flushTimeout_desc=A timeout occurred while finishing processing MalwareScanIngestModule_SharedProcessing_flushTimeout_title=Processing Timeout MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc=An error occurred while processing hash lookup results @@ -16,12 +22,22 @@ MalwareScanIngestModule_SharedProcessing_repServicenResponseError_title=Lookup A MalwareScanIngestModule_ShareProcessing_batchTimeout_desc=Batch processing timed out MalwareScanIngestModule_ShareProcessing_batchTimeout_title=Batch Processing Timeout # {0} - remainingLookups -MalwareScanIngestModule_ShareProcessing_lowLimitWarning_desc=This license only has {0} lookups remaining -MalwareScanIngestModule_ShareProcessing_lowLimitWarning_title=Hash Lookups Low +MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_desc=This license only has {0} lookups remaining. +MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_title=Hash Lookups Low +# {0} - remainingUploads +MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_desc=This license only has {0} file uploads remaining. +MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_title=File Uploads Limit Low MalwareScanIngestModule_ShareProcessing_noLicense_desc=No Cyber Triage license could be loaded. Cyber Triage processing will be disabled. MalwareScanIngestModule_ShareProcessing_noLicense_title=No Cyber Triage License -MalwareScanIngestModule_ShareProcessing_noRemaining_desc=There are no more remaining hash lookups for this license at this time. Cyber Triage processing will be disabled. -MalwareScanIngestModule_ShareProcessing_noRemaining_title=No remaining lookups +MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_desc=There are no more remaining hash lookups for this license at this time. Malware scanning will be disabled. +MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_title=No remaining lookups +MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_desc=There are no more remaining file uploads for this license at this time. File uploading will be disabled. +MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_title=No remaining file uploads +MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc=There are no more file uploads on this license at this time. File uploads will be disabled for remaining uploads. +MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title=No Remaining File Uploads +# {0} - objectId +MalwareScanIngestModule_uploadFile_notUploadable_desc=A file did not meet requirements for upload (object id: {0}). +MalwareScanIngestModule_uploadFile_notUploadable_title=Not Able to Upload MalwareScanIngestModuleFactory_description=The malware scan ingest module queries the Cyber Triage cloud API for any possible malicious executables. MalwareScanIngestModuleFactory_displayName=Cyber Triage Malware Scanner MalwareScanIngestModuleFactory_version=1.0.0 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 6d3f7d4766d9a53a02b5b8e5589639ee43ed66c3..c52e72a350db8f8c6267460ec4531e6d19082912 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java @@ -19,15 +19,21 @@ package com.basistech.df.cybertriage.autopsy.malwarescan; import com.basistech.df.cybertriage.autopsy.ctapi.CTApiDAO; +import com.basistech.df.cybertriage.autopsy.ctapi.CTCloudException; import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthTokenResponse; import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthenticatedRequestData; import com.basistech.df.cybertriage.autopsy.ctapi.json.CTCloudBean; import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseInfo; +import com.basistech.df.cybertriage.autopsy.ctapi.json.MalwareResultBean.Status; +import com.basistech.df.cybertriage.autopsy.ctapi.json.MetadataUploadRequest; import com.basistech.df.cybertriage.autopsy.ctoptions.ctcloud.CTLicensePersistence; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HexFormat; import java.util.List; import java.util.Map; import java.util.Optional; @@ -36,7 +42,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.curator.shaded.com.google.common.collect.Lists; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; @@ -49,6 +57,7 @@ import org.sleuthkit.datamodel.AnalysisResult; 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; @@ -60,7 +69,7 @@ /** * Uses CT cloud API to determine if file is malware */ -public class MalwareScanIngestModule implements FileIngestModule { +class MalwareScanIngestModule implements FileIngestModule { private static final SharedProcessing sharedProcessing = new SharedProcessing(); @@ -93,6 +102,14 @@ private static class SharedProcessing { //minimum lookups left before issuing warning private static final long LOW_LOOKUPS_REMAINING = 250; + //minimum file uploads left before issuing warning + private static final long LOW_UPLOADS_REMAINING = 25; + + private static final long MIN_UPLOAD_SIZE = 1; + private static final long MAX_UPLOAD_SIZE = 1_000_000_000; + private static final int NUM_FILE_UPLOAD_RETRIES = 7; + private static final long FILE_UPLOAD_RETRY_SLEEP_MILLIS = 60 * 1000; + private static final Set<String> EXECUTABLE_MIME_TYPES = Stream.of( "application/x-bat",//NON-NLS "application/x-dosexec",//NON-NLS @@ -111,129 +128,243 @@ 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, FLUSH_SECS_TIMEOUT, this::handleBatch); + + private final BatchProcessor<FileRecord> batchProcessor = new BatchProcessor<FileRecord>( + BATCH_SIZE, + FLUSH_SECS_TIMEOUT, + (lst) -> SharedProcessing.this.handleBatch(SharedProcessing.this.ingestJobState, lst)); private final CTLicensePersistence ctSettingsPersistence = CTLicensePersistence.getInstance(); private final CTApiDAO ctApiDAO = CTApiDAO.getInstance(); - private RunState runState = null; - - private SleuthkitCase tskCase = null; - private FileTypeDetector fileTypeDetector = null; - private LicenseInfo licenseInfo = null; - private BlackboardArtifact.Type malwareType = null; - private long dsId = 0; - private long ingestJobId = 0; + private IngestJobState ingestJobState = null; @Messages({ - "MalwareScanIngestModule_ShareProcessing_lowLimitWarning_title=Hash Lookups Low", - "# {0} - remainingLookups", - "MalwareScanIngestModule_ShareProcessing_lowLimitWarning_desc=This license only has {0} lookups remaining", "MalwareScanIngestModule_malwareTypeDisplayName=Malware", "MalwareScanIngestModule_ShareProcessing_noLicense_title=No Cyber Triage License", "MalwareScanIngestModule_ShareProcessing_noLicense_desc=No Cyber Triage license could be loaded. Cyber Triage processing will be disabled.", - "MalwareScanIngestModule_ShareProcessing_noRemaining_title=No remaining lookups", - "MalwareScanIngestModule_ShareProcessing_noRemaining_desc=There are no more remaining hash lookups for this license at this time. Cyber Triage processing will be disabled." - }) + "MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_title=No remaining lookups", + "MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_desc=There are no more remaining hash lookups for this license at this time. Malware scanning will be disabled.", + "MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_title=Hash Lookups Low", + "# {0} - remainingLookups", + "MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_desc=This license only has {0} lookups remaining.", + "MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_title=No remaining file uploads", + "MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_desc=There are no more remaining file uploads for this license at this time. File uploading will be disabled.", + "MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_title=File Uploads Limit Low", + "# {0} - remainingUploads", + "MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_desc=This license only has {0} file uploads remaining.",}) synchronized void startUp(IngestJobContext context) throws IngestModuleException { // only run this code once per startup - if (runState == RunState.STARTED_UP || runState == RunState.DISABLED) { + if (ingestJobState != null) { return; } try { - // get saved license - Optional<LicenseInfo> licenseInfoOpt = ctSettingsPersistence.loadLicenseInfo(); - if (licenseInfoOpt.isEmpty() || licenseInfoOpt.get().getDecryptedLicense() == null) { - notifyWarning( - Bundle.MalwareScanIngestModule_ShareProcessing_noLicense_title(), - Bundle.MalwareScanIngestModule_ShareProcessing_noLicense_desc(), - null); - runState = RunState.DISABLED; - return; - } + ingestJobState = getNewJobState(context); + } catch (Exception ex) { + ingestJobState = IngestJobState.DISABLED; + throw new IngestModuleException("An exception occurred on MalwareScanIngestModule startup", ex); + } + } + + /** + * Sets up the state necessary for a new ingest job. + * + * @param context The ingest job context. + * @return A pair of the runtime state (i.e. started up, disabled) and + * parameters required for the job. + * @throws Exception + */ + private IngestJobState getNewJobState(IngestJobContext context) throws Exception { + // get saved license + Optional<LicenseInfo> licenseInfoOpt = ctSettingsPersistence.loadLicenseInfo(); + if (licenseInfoOpt.isEmpty() || licenseInfoOpt.get().getDecryptedLicense() == null) { + notifyWarning( + Bundle.MalwareScanIngestModule_ShareProcessing_noLicense_title(), + Bundle.MalwareScanIngestModule_ShareProcessing_noLicense_desc(), + null); + + return IngestJobState.DISABLED; + } - AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(licenseInfoOpt.get().getDecryptedLicense()); - // syncronously fetch malware scans info + AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(licenseInfoOpt.get().getDecryptedLicense()); + // syncronously fetch malware scans info - // determine lookups remaining - long lookupsRemaining = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount()); - if (lookupsRemaining <= 0) { + // determine lookups remaining + long lookupsRemaining = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount()); + if (lookupsRemaining <= 0) { + notifyWarning( + Bundle.MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_title(), + Bundle.MalwareScanIngestModule_ShareProcessing_noLookupsRemaining_desc(), + null); + + return IngestJobState.DISABLED; + } else if (lookupsRemaining < LOW_LOOKUPS_REMAINING) { + notifyWarning( + Bundle.MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_title(), + Bundle.MalwareScanIngestModule_ShareProcessing_lowLookupsLimitWarning_desc(lookupsRemaining), + null); + } + + // determine lookups remaining + boolean uploadFiles = ctSettingsPersistence.loadMalwareIngestSettings().isUploadFiles(); + if (uploadFiles) { + long uploadsRemaining = remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount()); + if (uploadsRemaining <= 0) { notifyWarning( - Bundle.MalwareScanIngestModule_ShareProcessing_noRemaining_title(), - Bundle.MalwareScanIngestModule_ShareProcessing_noRemaining_desc(), + Bundle.MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_title(), + Bundle.MalwareScanIngestModule_ShareProcessing_noUploadsRemaining_desc(), null); - runState = RunState.DISABLED; - return; - } else if (lookupsRemaining < LOW_LOOKUPS_REMAINING) { + uploadFiles = false; + } else if (lookupsRemaining < LOW_UPLOADS_REMAINING) { notifyWarning( - Bundle.MalwareScanIngestModule_ShareProcessing_lowLimitWarning_title(), - Bundle.MalwareScanIngestModule_ShareProcessing_lowLimitWarning_desc(lookupsRemaining), + Bundle.MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_title(), + Bundle.MalwareScanIngestModule_ShareProcessing_lowUploadsLimitWarning_desc(lookupsRemaining), null); } - - // setup necessary variables for processing - tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - malwareType = tskCase.getBlackboard().getOrAddArtifactType( - MALWARE_TYPE_NAME, - Bundle.MalwareScanIngestModule_malwareTypeDisplayName(), - BlackboardArtifact.Category.ANALYSIS_RESULT); - fileTypeDetector = new FileTypeDetector(); - dsId = context.getDataSource().getId(); - ingestJobId = context.getJobId(); - licenseInfo = licenseInfoOpt.get(); - - // set run state to initialized - runState = RunState.STARTED_UP; - } catch (Exception ex) { - runState = RunState.DISABLED; - throw new IngestModuleException("An exception occurred on MalwareScanIngestModule startup", ex); } + + // setup necessary variables for processing + SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + BlackboardArtifact.Type malwareType = tskCase.getBlackboard().getOrAddArtifactType( + MALWARE_TYPE_NAME, + Bundle.MalwareScanIngestModule_malwareTypeDisplayName(), + BlackboardArtifact.Category.ANALYSIS_RESULT); + + return new IngestJobState( + context, + tskCase, + new PathNormalizer(tskCase), + new FileTypeDetector(), + licenseInfoOpt.get(), + malwareType, + uploadFiles, + true + ); } + /** + * Determines remaining given a possibly null limit and used count. + * + * @param limit The limit (can be null). + * @param used The number used (can be null). + * @return The remaining amount. + */ private static long remaining(Long limit, Long used) { limit = limit == null ? 0 : limit; used = used == null ? 0 : used; return limit - used; } - private String getOrCalcHash(AbstractFile af) { - if (StringUtils.isNotBlank(af.getMd5Hash())) { - return af.getMd5Hash(); + /** + * Gets the md5 hash from the abstract file or calculates it. + * + * @param af The abstract file. + * @return The md5 hash (or null if could not be determined). + */ + private static String getOrCalcHash(AbstractFile af, HashType hashType) { + switch (hashType) { + case MD5: + if (StringUtils.isNotBlank(af.getMd5Hash())) { + return af.getMd5Hash(); + } + break; + case SHA256: + if (StringUtils.isNotBlank(af.getSha256Hash())) { + return af.getSha256Hash(); + } } try { - List<HashResult> hashResults = HashUtility.calculateHashes(af, Collections.singletonList(HashType.MD5)); + List<HashResult> hashResults = HashUtility.calculateHashes(af, Collections.singletonList(hashType)); if (CollectionUtils.isNotEmpty(hashResults)) { for (HashResult hashResult : hashResults) { - if (hashResult.getType() == HashType.MD5) { + if (hashResult.getType() == hashType) { return hashResult.getValue(); } } } } catch (TskCoreException ex) { logger.log(Level.WARNING, - MessageFormat.format("An error occurred while processing file name: {0} and obj id: {1}.", + MessageFormat.format("An error occurred while processing hash for file name: {0} and obj id: {1} and hash type {2}.", af.getName(), - af.getId()), + af.getId(), + hashType.name()), ex); } return null; } + /** + * Gets or calculates the md5 for a file. + * + * @param af The file. + * @return The hash. + */ + private static String getOrCalcMd5(AbstractFile af) { + return getOrCalcHash(af, HashType.MD5); + } + + /** + * Gets or calculates the sha256 for a file. + * + * @param af The file. + * @return The hash. + */ + private static String getOrCalcSha256(AbstractFile af) { + return getOrCalcHash(af, HashType.SHA256); + } + + /** + * Gets or calculates the sha1 for a file. + * + * @param af The file. + * @return The hash. + */ + private static String getOrCalcSha1(AbstractFile af) throws NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException { + if (StringUtils.isNotBlank(af.getSha1Hash())) { + return af.getSha1Hash(); + } + // taken from https://stackoverflow.com/questions/6293713/java-how-to-create-sha-1-for-a-file + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + ReadContentInputStream afStream = new ReadContentInputStream(af); + int n = 0; + byte[] buffer = new byte[8192]; + while (n != -1) { + n = afStream.read(buffer); + if (n > 0) { + digest.update(buffer, 0, n); + } + } + byte[] hashBytes = digest.digest(); + String hashString = HexFormat.of().formatHex(hashBytes); + return hashString; + } + + /** + * Processes a file. The file goes through the lookup process if the + * file meets acceptable criteria: 1) not FileKnown.KNOWN 2) is + * executable 3) does not have any pre-existing TSK_MALWARE results 4) + * file lookup has not been disabled. + * + * @param af The file. + * @return OK or ERROR. + */ @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 + if (ingestJobState != null + && ingestJobState.isDoFileLookups() + && !ingestJobState.getIngestJobContext().fileIngestIsCancelled() && af.getKnown() != TskData.FileKnown.KNOWN - && EXECUTABLE_MIME_TYPES.contains(StringUtils.defaultString(fileTypeDetector.getMIMEType(af)).trim().toLowerCase()) - && CollectionUtils.isEmpty(af.getAnalysisResults(malwareType))) { + && EXECUTABLE_MIME_TYPES.contains(StringUtils.defaultString(ingestJobState.getFileTypeDetector().getMIMEType(af)).trim().toLowerCase()) + && CollectionUtils.isEmpty(af.getAnalysisResults(ingestJobState.getMalwareType()))) { - String md5 = getOrCalcHash(af); + String md5 = getOrCalcMd5(af); if (StringUtils.isNotBlank(md5)) { batchProcessor.add(new FileRecord(af.getId(), md5)); } @@ -254,6 +385,13 @@ IngestModule.ProcessResult process(AbstractFile af) { } } + /** + * Handles a batch of files to be sent to CT file lookup for results. + * + * @param ingestJobState The current state of operation for the ingest + * job. + * @param fileRecords The file records to be uploaded. + */ @Messages({ "MalwareScanIngestModule_SharedProcessing_authTokenResponseError_title=Authentication API error", "# {0} - errorResponse", @@ -265,8 +403,12 @@ IngestModule.ProcessResult process(AbstractFile af) { "MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc=The remaining hash lookups for this license have been exhausted", "MalwareScanIngestModule_SharedProcessing_generalProcessingError_title=Hash Lookup Error", "MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc=An error occurred while processing hash lookup results",}) - private void handleBatch(List<FileRecord> fileRecords) { - if (runState != RunState.STARTED_UP || fileRecords == null || fileRecords.isEmpty()) { + private void handleBatch(IngestJobState ingestJobState, List<FileRecord> fileRecords) { + if (ingestJobState == null + || !ingestJobState.isDoFileLookups() + || ingestJobState.getIngestJobContext().fileIngestIsCancelled() + || fileRecords == null + || fileRecords.isEmpty()) { return; } @@ -278,11 +420,10 @@ private void handleBatch(List<FileRecord> fileRecords) { continue; } - String sanitizedMd5 = sanitizedMd5(fr.getMd5hash()); + String sanitizedMd5 = normalizedMd5(fr.getMd5hash()); md5ToObjId .computeIfAbsent(sanitizedMd5, (k) -> new ArrayList<>()) .add(fr.getObjId()); - } List<String> md5Hashes = new ArrayList<>(md5ToObjId.keySet()); @@ -292,78 +433,398 @@ private void handleBatch(List<FileRecord> fileRecords) { } try { - // get an auth token with the license - AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(licenseInfo.getDecryptedLicense()); + List<CTCloudBean> repResult = getHashLookupResults(ingestJobState, md5Hashes); + handleLookupResults(ingestJobState, md5ToObjId, repResult); + } catch (Exception ex) { + notifyWarning( + Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(), + Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(), + ex); + } + } + + /** + * Handles results received from CT Cloud. + * + * @param ingestJobState The current state of operations of the ingest + * module. + * @param md5ToObjId The mapping of md5 to a list of object ids. + * @param repResult The ct cloud results. + * @throws org.sleuthkit.datamodel.Blackboard.BlackboardException + * @throws TskCoreException + */ + @Messages({ + "MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_title=Lookup Limits Exceeded", + "MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_desc=Not all files were processed because hash lookup limits were exceeded. Please try again when your limits reset.",}) + private void handleLookupResults(IngestJobState ingestJobState, Map<String, List<Long>> md5ToObjId, List<CTCloudBean> repResult) throws Blackboard.BlackboardException, TskCoreException, TskCoreException, CTCloudException, NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException { + if (CollectionUtils.isEmpty(repResult)) { + return; + } + + Map<Status, List<CTCloudBean>> statusGroupings = repResult.stream() + .filter(bean -> bean.getMalwareResult() != null) + .collect(Collectors.groupingBy(bean -> bean.getMalwareResult().getStatus())); + + // for all found items, create analysis results + List<CTCloudBean> found = statusGroupings.get(Status.FOUND); + createAnalysisResults(ingestJobState, found, md5ToObjId); + + // if being scanned, check list to run later + handleNonFoundResults(ingestJobState, md5ToObjId, statusGroupings.get(Status.BEING_SCANNED), false); + + // if not found, try upload + handleNonFoundResults(ingestJobState, md5ToObjId, statusGroupings.get(Status.NOT_FOUND), true); + + // indicate a general error if some result in an error + if (CollectionUtils.isNotEmpty(statusGroupings.get(Status.ERROR))) { + notifyWarning( + Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(), + Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(), + null); + } + + // indicate some results were not processed if limits exceeded in results + if (CollectionUtils.isNotEmpty(statusGroupings.get(Status.LIMITS_EXCEEDED))) { + notifyWarning( + Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_title(), + Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedResultsHashLookups_desc(), + null); + } + } + + /** + * Handles a CT cloud response objects that have a status that isn't + * FOUND but still are queryable (i.e. NOT_FOUND, BEING_SCANNED). + * + * @param ingestJobState The current state of operations of the ingest + * module. + * @param md5ToObjId The mapping of md5 to a list of object ids. + * @param results The ct cloud results. + * @param performFileUpload True if the class of results warrants file + * upload (i.e. NOT_FOUND) + */ + private void handleNonFoundResults(IngestJobState ingestJobState, Map<String, List<Long>> md5ToObjId, List<CTCloudBean> results, boolean performFileUpload) throws CTCloudException, TskCoreException, NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException { + if (CollectionUtils.isNotEmpty(results) + && ingestJobState.isDoFileLookups() + && ((performFileUpload && ingestJobState.isUploadUnknownFiles()) || (!performFileUpload && ingestJobState.isQueryForMissing()))) { + + for (CTCloudBean beingScanned : CollectionUtils.emptyIfNull(results)) { + + String sanitizedMd5 = normalizedMd5(beingScanned.getMd5HashValue()); + if (StringUtils.isBlank(sanitizedMd5)) { + continue; + } + List<Long> correspondingObjIds = md5ToObjId.get(sanitizedMd5); + if (CollectionUtils.isEmpty(correspondingObjIds)) { + continue; + } + + if (performFileUpload) { + uploadFile(ingestJobState, sanitizedMd5, correspondingObjIds.get(0)); + } - // make sure we are in bounds for the remaining scans - long remainingScans = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount()); - if (remainingScans <= 0) { - runState = RunState.DISABLED; + ingestJobState.getUnidentifiedHashes().put(sanitizedMd5, correspondingObjIds); + } + } + } + + /** + * Makes CT Cloud REST API query for results regarding the status of a + * list of md5 hashes for executables. + * + * @param ingestJobState The current state of operations of the ingest + * module. + * @param md5Hashes The md5 hashes to check. + * @return The results from CT Cloud. + * @throws CTCloudException + */ + private List<CTCloudBean> getHashLookupResults(IngestJobState ingestJobState, List<String> md5Hashes) throws CTCloudException { + if (ingestJobState.getIngestJobContext().fileIngestIsCancelled()) { + return Collections.emptyList(); + } + + // get an auth token with the license + AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(ingestJobState.getLicenseInfo().getDecryptedLicense()); + + // make sure we are in bounds for the remaining scans + long remainingScans = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount()); + if (remainingScans <= 0) { + ingestJobState.disableDoFileLookups(); + notifyWarning( + Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title(), + Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc(), + null); + return Collections.emptyList(); + } else if (ingestJobState.getIngestJobContext().fileIngestIsCancelled()) { + return Collections.emptyList(); + } + + // while we have a valid auth token, also check file uploads. + if (ingestJobState.isUploadUnknownFiles()) { + long remainingUploads = remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount()); + if (remainingUploads <= 0) { + ingestJobState.disableUploadUnknownFiles(); notifyWarning( - Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title(), - Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc(), + Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title(), + Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc(), null); - return; } + } - // using auth token, get results - List<CTCloudBean> repResult = ctApiDAO.getReputationResults( - new AuthenticatedRequestData(licenseInfo.getDecryptedLicense(), authTokenResponse), - md5Hashes - ); + // using auth token, get results + return ctApiDAO.getReputationResults( + new AuthenticatedRequestData(ingestJobState.getLicenseInfo().getDecryptedLicense(), authTokenResponse), + md5Hashes + ); + } - List<BlackboardArtifact> createdArtifacts = new ArrayList<>(); - if (!CollectionUtils.isEmpty(repResult)) { - SleuthkitCase.CaseDbTransaction trans = null; - try { - trans = tskCase.beginTransaction(); - for (CTCloudBean result : repResult) { - String sanitizedMd5 = sanitizedMd5(result.getMd5HashValue()); - List<Long> objIds = md5ToObjId.remove(sanitizedMd5); - if (objIds == null || objIds.isEmpty()) { - continue; - } - - for (Long objId : objIds) { - AnalysisResult res = createAnalysisResult(objId, result, trans); - if (res != null) { - createdArtifacts.add(res); - } - } - } + /** + * Normalizes an md5 string for the purposes of lookup in a map. + * + * @param orig The original value. + * @return The normalized value + */ + private static String normalizedMd5(String orig) { + return StringUtils.defaultString(orig).trim().toLowerCase(); + } - trans.commit(); - trans = null; - } finally { - if (trans != null) { - trans.rollback(); - createdArtifacts.clear(); - trans = null; - } + /** + * Whether or not an abstract file meets the requirements to be + * uploaded. + * + * @param af The abstract file. + * @return True if can be uploaded. + */ + private static boolean isUploadable(AbstractFile af) { + long size = af.getSize(); + return size >= MIN_UPLOAD_SIZE && size <= MAX_UPLOAD_SIZE; + } + + /** + * Uploads a file to CT Cloud if the file is valid for upload. + * + * @param ingestJobState The current state of the ingest job. + * @param objId The object id of the file to upload to CT cloud. + * @return True if successfully uploaded. + * @throws CTCloudException + * @throws TskCoreException + */ + @Messages({ + "MalwareScanIngestModule_uploadFile_notUploadable_title=Not Able to Upload", + "# {0} - objectId", + "MalwareScanIngestModule_uploadFile_notUploadable_desc=A file did not meet requirements for upload (object id: {0}).", + "MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title=No Remaining File Uploads", + "MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc=There are no more file uploads on this license at this time. File uploads will be disabled for remaining uploads.",}) + private boolean uploadFile(IngestJobState ingestJobState, String md5, long objId) throws CTCloudException, TskCoreException, NoSuchAlgorithmException, ReadContentInputStream.ReadContentInputStreamException { + if (!ingestJobState.isUploadUnknownFiles() || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) { + return false; + } + + AbstractFile af = ingestJobState.getTskCase().getAbstractFileById(objId); + if (af == null) { + return false; + } + + if (!isUploadable(af)) { + notifyWarning( + Bundle.MalwareScanIngestModule_uploadFile_notUploadable_title(), + Bundle.MalwareScanIngestModule_uploadFile_notUploadable_desc(objId), + null); + return false; + } + + // get auth token / file upload url + AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(ingestJobState.getLicenseInfo().getDecryptedLicense(), true); + if (StringUtils.isBlank(authTokenResponse.getFileUploadUrl())) { + throw new CTCloudException(CTCloudException.ErrorCode.NETWORK_ERROR); + } else if (remaining(authTokenResponse.getFileUploadLimit(), authTokenResponse.getFileUploadCount()) <= 0) { + // don't proceed with upload if reached limit + ingestJobState.disableUploadUnknownFiles(); + notifyWarning( + Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_title(), + Bundle.MalwareScanIngestModule_uploadFile_noRemainingFileUploads_desc(), + null); + + return false; + } else if (ingestJobState.getIngestJobContext().fileIngestIsCancelled()) { + return false; + } + + // upload bytes + ReadContentInputStream fileInputStream = new ReadContentInputStream(af); + ctApiDAO.uploadFile(authTokenResponse.getFileUploadUrl(), af.getName(), fileInputStream); + + // upload metadata + MetadataUploadRequest metaRequest = new MetadataUploadRequest() + .setCreatedDate(af.getCrtime() == 0 ? null : af.getCrtime()) + .setFilePath(ingestJobState.getPathNormalizer().normalizePath(af.getUniquePath())) + .setFileSizeBytes(af.getSize()) + .setFileUploadUrl(authTokenResponse.getFileUploadUrl()) + .setMd5(md5) + .setSha1(getOrCalcSha1(af)) + .setSha256(getOrCalcSha256(af)); + + ctApiDAO.uploadMeta(new AuthenticatedRequestData(ingestJobState.getLicenseInfo().getDecryptedLicense(), authTokenResponse), metaRequest); + return true; + } + + /** + * Does long polling for any pending results. + * + * @param ingestJobState The state of the ingest job. + * @throws InterruptedException + * @throws CTCloudException + * @throws org.sleuthkit.datamodel.Blackboard.BlackboardException + * @throws TskCoreException + */ + @Messages({ + "MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_title=Waiting for File Upload Results", + "MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_desc=Waiting for all uploaded files to complete scanning.", + "MalwareScanIngestModule_longPollForNotFound_timeout_title=File Upload Results Timeout", + "MalwareScanIngestModule_longPollForNotFound_timeout_desc=There was a timeout while waiting for file uploads to be processed. Please try again later.",}) + private void longPollForNotFound(IngestJobState ingestJobState) throws InterruptedException, CTCloudException, Blackboard.BlackboardException, TskCoreException { + if (!ingestJobState.isDoFileLookups() + || !ingestJobState.isQueryForMissing() + || MapUtils.isEmpty(ingestJobState.getUnidentifiedHashes()) + || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) { + return; + } + + MessageNotifyUtil.Notify.info( + Bundle.MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_title(), + Bundle.MalwareScanIngestModule_longPollForNotFound_fileLookupPolling_desc() + ); + + Map<String, List<Long>> remaining = new HashMap<>(ingestJobState.getUnidentifiedHashes()); + + for (int retry = 0; retry < NUM_FILE_UPLOAD_RETRIES; retry++) { + List<List<String>> md5Batches = Lists.partition(new ArrayList<>(remaining.keySet()), BATCH_SIZE); + for (List<String> batch : md5Batches) { + // if we have exceeded limits or cancelled, then we're done. + if (!ingestJobState.isDoFileLookups() || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) { + return; } - if (!CollectionUtils.isEmpty(createdArtifacts)) { - tskCase.getBlackboard().postArtifacts(createdArtifacts, Bundle.MalwareScanIngestModuleFactory_displayName(), ingestJobId); + List<CTCloudBean> repResult = getHashLookupResults(ingestJobState, batch); + + Map<Status, List<CTCloudBean>> statusGroupings = repResult.stream() + .filter(bean -> bean.getMalwareResult() != null) + .collect(Collectors.groupingBy(bean -> bean.getMalwareResult().getStatus())); + + // for all found items, create analysis results + List<CTCloudBean> found = statusGroupings.get(Status.FOUND); + + createAnalysisResults(ingestJobState, found, remaining); + + // remove any found items from the list of items to long poll for + for (CTCloudBean foundItem : CollectionUtils.emptyIfNull(found)) { + String normalizedMd5 = normalizedMd5(foundItem.getMd5HashValue()); + remaining.remove(normalizedMd5); } } - } catch (Exception ex) { - notifyWarning( - Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(), - Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(), - ex); + + if (remaining.isEmpty()) { + return; + } + + // exponential backoff before trying again + long waitMultiplier = ((long) Math.pow(2, retry)); + for (int i = 0; i < waitMultiplier; i++) { + if (!ingestJobState.isDoFileLookups() || ingestJobState.getIngestJobContext().fileIngestIsCancelled()) { + return; + } + + Thread.sleep(FILE_UPLOAD_RETRY_SLEEP_MILLIS); + } } + + notifyWarning( + Bundle.MalwareScanIngestModule_longPollForNotFound_timeout_title(), + Bundle.MalwareScanIngestModule_longPollForNotFound_timeout_desc(), + null + ); } - private String sanitizedMd5(String orig) { - return StringUtils.defaultString(orig).trim().toLowerCase(); + /** + * Creates TSK_MALWARE analysis results based on a list of cloud beans + * received from the CT cloud api. + * + * @param ingestJobState The ingest job state. + * @param repResult The list of cloud beans. Only cloud beans with a + * malware status + * @param md5ToObjId The mapping of md5 + * @throws org.sleuthkit.datamodel.Blackboard.BlackboardException + * @throws TskCoreException + */ + private void createAnalysisResults(IngestJobState ingestJobState, List<CTCloudBean> repResult, Map<String, List<Long>> md5ToObjId) throws Blackboard.BlackboardException, TskCoreException { + if (CollectionUtils.isEmpty(repResult)) { + return; + } + + List<BlackboardArtifact> createdArtifacts = new ArrayList<>(); + SleuthkitCase.CaseDbTransaction trans = null; + try { + trans = ingestJobState.getTskCase().beginTransaction(); + for (CTCloudBean result : repResult) { + String sanitizedMd5 = normalizedMd5(result.getMd5HashValue()); + List<Long> objIds = md5ToObjId.remove(sanitizedMd5); + if (CollectionUtils.isEmpty(objIds)) { + continue; + } + + for (Long objId : objIds) { + AnalysisResult res = createAnalysisResult(ingestJobState, trans, result, objId); + if (res != null) { + createdArtifacts.add(res); + } + } + } + + trans.commit(); + trans = null; + } finally { + if (trans != null) { + trans.rollback(); + createdArtifacts.clear(); + trans = null; + } + } + + if (!CollectionUtils.isEmpty(createdArtifacts)) { + ingestJobState.getTskCase().getBlackboard().postArtifacts( + createdArtifacts, + Bundle.MalwareScanIngestModuleFactory_displayName(), + ingestJobState.getIngestJobId() + ); + } + } + /** + * Creates an analysis result for the given information. + * + * @param ingestJobState The state of the ingest job. + * @param trans The case database transaction to use. + * @param cloudBean The bean indicating the malware result. + * @param objId The object id of the corresponding file that will + * receive the analysis result. + * @return The created analysis result or null if none created. + * @throws org.sleuthkit.datamodel.Blackboard.BlackboardException + */ @Messages({ "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes=YES", "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No=NO" }) - private AnalysisResult createAnalysisResult(Long objId, CTCloudBean cloudBean, SleuthkitCase.CaseDbTransaction trans) throws Blackboard.BlackboardException { - if (objId == null || cloudBean == null || cloudBean.getMalwareResult() == null) { + private AnalysisResult createAnalysisResult(IngestJobState ingestJobState, SleuthkitCase.CaseDbTransaction trans, CTCloudBean cloudBean, Long objId) throws Blackboard.BlackboardException { + if (objId == null || cloudBean == null || cloudBean.getMalwareResult() == null || cloudBean.getMalwareResult().getStatus() != Status.FOUND) { + logger.log(Level.WARNING, MessageFormat.format("Attempting to create analysis result with invalid parameters [objId: {0}, cloud bean status: {1}]", + objId == null + ? "<null>" + : objId, + (cloudBean == null || cloudBean.getMalwareResult() == null || cloudBean.getMalwareResult().getStatus() == null) + ? "<null>" + : cloudBean.getMalwareResult().getStatus().name() + )); return null; } @@ -377,10 +838,10 @@ private AnalysisResult createAnalysisResult(Long objId, CTCloudBean cloudBean, S String justification = cloudBean.getMalwareResult().getStatusDescription(); - return tskCase.getBlackboard().newAnalysisResult( - malwareType, + return ingestJobState.getTskCase().getBlackboard().newAnalysisResult( + ingestJobState.getMalwareType(), objId, - dsId, + ingestJobState.getDsId(), score, conclusion, MALWARE_CONFIG, @@ -389,39 +850,52 @@ private AnalysisResult createAnalysisResult(Long objId, CTCloudBean cloudBean, S trans).getAnalysisResult(); } + /** + * Called when ingest should shut down. + */ @Messages({ "MalwareScanIngestModule_SharedProcessing_flushTimeout_title=Processing Timeout", "MalwareScanIngestModule_SharedProcessing_flushTimeout_desc=A timeout occurred while finishing processing" }) synchronized void shutDown() { // if already shut down, return - if (runState == RunState.SHUT_DOWN) { + if (ingestJobState == null) { return; } // flush any remaining items try { batchProcessor.flushAndReset(); + longPollForNotFound(ingestJobState); } catch (InterruptedException ex) { notifyWarning( Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_title(), Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_desc(), ex); + } catch (Exception ex) { + notifyWarning( + Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(), + Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(), + ex); } finally { // set state to shut down and clear any remaining - runState = RunState.SHUT_DOWN; + ingestJobState = null; } } - private void notifyWarning(String title, String message, Exception ex) { + /** + * Creates a warning notification to display in the lower right corner + * and a corresponding log message. + * + * @param title The title of the warning. + * @param message The message of the warning. + * @param ex The corresponding exception (or null if none). + */ + private static void notifyWarning(String title, String message, Exception ex) { MessageNotifyUtil.Notify.warn(title, message); logger.log(Level.WARNING, message, ex); } - private enum RunState { - STARTED_UP, DISABLED, SHUT_DOWN - } - class FileRecord { private final long objId; @@ -441,5 +915,116 @@ String getMd5hash() { } } + + /** + * Represents the state of the current ingest job. + * + * NOTE: if doFileLookups is false, most variables will likely be null + * (TSK case, file type detector, etc.) and should not be used. The + * contract for this class should be that if doFileLookups is true or + * uploadUnknownFiles is true, the remaining variables should be non + * null, if doFileLookups is false and uploadUnknownFiles is false, no + * other access to this class can be made reliably. + */ + static class IngestJobState { + + static final IngestJobState DISABLED = new IngestJobState( + null, + null, + null, + null, + null, + null, + false, + false + ); + + private final SleuthkitCase tskCase; + private final FileTypeDetector fileTypeDetector; + private final LicenseInfo licenseInfo; + private final BlackboardArtifact.Type malwareType; + private final long dsId; + private final long ingestJobId; + private final boolean queryForMissing; + private final Map<String, List<Long>> unidentifiedHashes = new HashMap<>(); + + // this can change mid run + private boolean uploadUnknownFiles; + private boolean doFileLookups; + private final IngestJobContext ingestJobContext; + private final PathNormalizer pathNormalizer; + + IngestJobState(IngestJobContext ingestJobContext, SleuthkitCase tskCase, PathNormalizer pathNormalizer, FileTypeDetector fileTypeDetector, LicenseInfo licenseInfo, BlackboardArtifact.Type malwareType, boolean uploadUnknownFiles, boolean doFileLookups) { + this.tskCase = tskCase; + this.fileTypeDetector = fileTypeDetector; + this.pathNormalizer = pathNormalizer; + this.licenseInfo = licenseInfo; + this.malwareType = malwareType; + this.dsId = ingestJobContext == null ? 0L : ingestJobContext.getDataSource().getId(); + this.ingestJobId = ingestJobContext == null ? 0L : ingestJobContext.getJobId(); + this.ingestJobContext = ingestJobContext; + // for now, querying for any missing files will be tied to whether initially we should upload files and do lookups at all + this.queryForMissing = uploadUnknownFiles && doFileLookups; + this.uploadUnknownFiles = uploadUnknownFiles; + this.doFileLookups = doFileLookups; + } + + SleuthkitCase getTskCase() { + return tskCase; + } + + IngestJobContext getIngestJobContext() { + return ingestJobContext; + } + + FileTypeDetector getFileTypeDetector() { + return fileTypeDetector; + } + + LicenseInfo getLicenseInfo() { + return licenseInfo; + } + + BlackboardArtifact.Type getMalwareType() { + return malwareType; + } + + long getDsId() { + return dsId; + } + + long getIngestJobId() { + return ingestJobId; + } + + Map<String, List<Long>> getUnidentifiedHashes() { + return unidentifiedHashes; + } + + boolean isQueryForMissing() { + return queryForMissing; + } + + boolean isUploadUnknownFiles() { + return uploadUnknownFiles; + } + + void disableUploadUnknownFiles() { + this.uploadUnknownFiles = false; + } + + boolean isDoFileLookups() { + return doFileLookups; + } + + void disableDoFileLookups() { + this.doFileLookups = false; + } + + public PathNormalizer getPathNormalizer() { + return pathNormalizer; + } + + } } } diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/PathNormalizer.java b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/PathNormalizer.java new file mode 100644 index 0000000000000000000000000000000000000000..f8a0a299a5771fcb3d3c90d1bb0ce597cdd3e3b7 --- /dev/null +++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/PathNormalizer.java @@ -0,0 +1,201 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2023 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.basistech.df.cybertriage.autopsy.malwarescan; + +import com.google.common.net.InetAddresses; +import java.net.InetAddress; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Utility class to anonymize paths. + */ +class PathNormalizer { + + private static final Logger LOGGER = Logger.getLogger(PathNormalizer.class.getName()); + + private static final String ANONYMIZED_USERNAME = "<user>"; + private static final String ANONYMIZED_IP = "<private_ip>"; + private static final String ANONYMIZED_HOSTNAME = "<hostname>"; + private static final String FORWARD_SLASH = "/"; + private static final String BACK_SLASH = "\\"; + + private static final Pattern USER_PATH_FORWARD_SLASH_REGEX = Pattern.compile("(?<!all )([/]{0,1}\\Qusers\\E/)(?!(public|Default|defaultAccount|All Users))([^/]+)(/){0,1}", Pattern.CASE_INSENSITIVE); + private static final Pattern USER_PATH_BACK_SLASH_REGEX = Pattern.compile("(?<!all )([\\\\]{0,1}\\Qusers\\E\\\\)(?!(public|Default|defaultAccount|All Users))([^\\\\]+)([\\\\]){0,1}", Pattern.CASE_INSENSITIVE); + + private static final Pattern USER_PATH_FORWARD_SLASH_REGEX_XP = Pattern.compile("([/]{0,1}\\Qdocuments and settings\\E/)(?!(Default User|All Users))([^/]+)(/){0,1}", Pattern.CASE_INSENSITIVE); + private static final Pattern USER_PATH_BACK_SLASH_REGEX_XP = Pattern.compile("([\\\\]{0,1}\\Qdocuments and settings\\E\\\\)(?!(Default User|All Users))([^\\\\]+)(\\\\){0,1}", Pattern.CASE_INSENSITIVE); + + private static final Pattern UNC_PATH_FORWARD_SLASH_PATTERN = Pattern.compile("(//)([^/]+)(/){0,1}"); + private static final Pattern UNC_PATH_BACK_SLASH_PATTERN = Pattern.compile("(\\\\\\\\)([^\\\\]+)(\\\\){0,1}"); + + private static final String USERNAME_REGEX_REPLACEMENT = "$1" + ANONYMIZED_USERNAME + "$4"; + + private final SleuthkitCase skCase; + + PathNormalizer(SleuthkitCase skCase) { + this.skCase = skCase; + } + + protected List<String> getUsernames() { + try { + return this.skCase.getOsAccountManager().getOsAccounts().stream() + .filter(acct -> acct != null) + .map(acct -> acct.getLoginName().orElse(null)) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "There was an error getting current os accounts", ex); + return Collections.emptyList(); + } + } + + public String normalizePath(String inputString) { + if (StringUtils.isBlank(inputString)) { + return ""; + } + + String anonymousString = anonymizeUserFromPathsWithForwardSlashes(inputString); + anonymousString = anonymizeUserFromPathsWithBackSlashes(anonymousString); + anonymousString = anonymizeServerFromUNCPath(anonymousString); + + return anonymousString; + } + + private String anonymizeUserFromPathsWithForwardSlashes(String stringWithUsername) { + String anonymousString = stringWithUsername; + anonymousString = regexReplace(anonymousString, USER_PATH_FORWARD_SLASH_REGEX_XP, USERNAME_REGEX_REPLACEMENT); + anonymousString = regexReplace(anonymousString, USER_PATH_FORWARD_SLASH_REGEX, USERNAME_REGEX_REPLACEMENT); + anonymousString = replaceFolder(anonymousString, getUsernames(), ANONYMIZED_USERNAME, FORWARD_SLASH); + return anonymousString; + } + + // Most paths in CyberTriage are normalized with forward slashes + // but there can still be strings containing paths that are not normalized such paths contained in arguments or event log payloads + private String anonymizeUserFromPathsWithBackSlashes(String stringWithUsername) { + String anonymousString = stringWithUsername; + anonymousString = regexReplace(anonymousString, USER_PATH_BACK_SLASH_REGEX_XP, USERNAME_REGEX_REPLACEMENT); + anonymousString = regexReplace(anonymousString, USER_PATH_BACK_SLASH_REGEX, USERNAME_REGEX_REPLACEMENT); + anonymousString = replaceFolder(anonymousString, getUsernames(), ANONYMIZED_USERNAME, BACK_SLASH); + + return anonymousString; + } + + private String anonymizeServerFromUNCPath(String inputString) { + + Set<String> serverNames = new HashSet<>(); + String anonymousString = inputString.toLowerCase(Locale.ENGLISH); + + Matcher forwardSlashMatcher = UNC_PATH_FORWARD_SLASH_PATTERN.matcher(anonymousString); + while (forwardSlashMatcher.find()) { + String serverName = forwardSlashMatcher.group(2); + serverNames.add(serverName); + } + + Matcher backSlashMatcher = UNC_PATH_BACK_SLASH_PATTERN.matcher(anonymousString); + while (backSlashMatcher.find()) { + String serverName = backSlashMatcher.group(2); + serverNames.add(serverName); + } + + for (String serverName : serverNames) { + + if (StringUtils.isBlank(serverName)) { + continue; + } + + if (InetAddresses.isInetAddress(serverName) && isLocalIP(serverName)) { + anonymousString = replaceFolder(anonymousString, Collections.singletonList(serverName), ANONYMIZED_IP); + } else { + anonymousString = replaceFolder(anonymousString, Collections.singletonList(serverName), ANONYMIZED_HOSTNAME); + } + + } + + return anonymousString; + } + + private static String regexReplace(String orig, Pattern pattern, String regexReplacement) { + Matcher matcher = pattern.matcher(orig); + return matcher.replaceAll(regexReplacement); + } + + private static String replaceFolder(String orig, List<String> valuesToReplace, String replacementValue) { + String anonymized = orig; + anonymized = replaceFolder(anonymized, valuesToReplace, replacementValue, FORWARD_SLASH); + anonymized = replaceFolder(anonymized, valuesToReplace, replacementValue, BACK_SLASH); + return anonymized; + } + + private static String replaceFolder(String orig, List<String> valuesToReplace, String replacementValue, String folderDelimiter) { + if (orig == null || valuesToReplace == null) { + return orig; + } + + String anonymousString = orig; + + // ensure non-null + folderDelimiter = StringUtils.defaultString(folderDelimiter); + replacementValue = StringUtils.defaultString(replacementValue); + + // replace + for (String valueToReplace : valuesToReplace) { + if (StringUtils.isNotEmpty(valueToReplace)) { + anonymousString = StringUtils.replace(anonymousString, + folderDelimiter + valueToReplace + folderDelimiter, + folderDelimiter + replacementValue + folderDelimiter); + } + } + + return anonymousString; + } + + /** + * Returns true if IP Address is Any Local / Site Local / Link Local / Loop + * back local. Sample list "0.0.0.0", wildcard addres + * "10.1.1.1","10.10.10.10", site local address "127.0.0.0","127.2.2.2", + * loopback address "169.254.0.0","169.254.10.10", Link local address + * "172.16.0.0","172.31.245.245", site local address + * + * @param ipAddress + * @return + */ + public static boolean isLocalIP(String ipAddress) { + try { + InetAddress a = InetAddresses.forString(ipAddress); + return a.isAnyLocalAddress() || a.isSiteLocalAddress() + || a.isLoopbackAddress() || a.isLinkLocalAddress(); + } catch (IllegalArgumentException ex) { + LOGGER.log(Level.WARNING, "Invalid IP string", ex); + return false; + } + } + +} diff --git a/docs/doxygen-user/images/ct_malware_license_agreement.png b/docs/doxygen-user/images/ct_malware_license_agreement.png new file mode 100644 index 0000000000000000000000000000000000000000..030a722e469c6ba1964a40fb9b5c3efcc99d11ce Binary files /dev/null and b/docs/doxygen-user/images/ct_malware_license_agreement.png differ diff --git a/docs/doxygen-user/images/ct_malware_scanner_options_panel.png b/docs/doxygen-user/images/ct_malware_scanner_options_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..1f45693d138a12a838aba40ab18a31f65c5f6d82 Binary files /dev/null and b/docs/doxygen-user/images/ct_malware_scanner_options_panel.png differ diff --git a/docs/doxygen-user/images/ct_upload_file.png b/docs/doxygen-user/images/ct_upload_file.png new file mode 100644 index 0000000000000000000000000000000000000000..38c595330d609bcf6ade1072a90a3e63f2eedc52 Binary files /dev/null and b/docs/doxygen-user/images/ct_upload_file.png differ