diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java index c616ba12a8..ba05038639 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java @@ -26,6 +26,7 @@ import java.util.Map; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.SleuthkitCase; /** @@ -35,12 +36,14 @@ public class DomainSearch { private final DomainSearchCache searchCache; private final DomainSearchThumbnailCache thumbnailCache; + private final DomainSearchArtifactsCache artifactsCache; /** * Construct a new DomainSearch object. */ public DomainSearch() { - this(new DomainSearchCache(), new DomainSearchThumbnailCache()); + this(new DomainSearchCache(), new DomainSearchThumbnailCache(), + new DomainSearchArtifactsCache()); } /** @@ -51,9 +54,11 @@ public class DomainSearch { * @param thumbnailCache The DomainSearchThumnailCache to use for this * DomainSearch. */ - DomainSearch(DomainSearchCache cache, DomainSearchThumbnailCache thumbnailCache) { + DomainSearch(DomainSearchCache cache, DomainSearchThumbnailCache thumbnailCache, + DomainSearchArtifactsCache artifactsCache) { this.searchCache = cache; this.thumbnailCache = thumbnailCache; + this.artifactsCache = artifactsCache; } /** @@ -139,17 +144,40 @@ public class DomainSearch { } /** - * Get a thumbnail representation of a domain name. See - * DomainSearchThumbnailRequest for more details. + * Get a thumbnail representation of a domain name. + * + * Thumbnail candidates are JPEG files that have either TSK_WEB_DOWNLOAD or + * TSK_WEB_CACHE artifacts that match the domain name (see the DomainSearch + * getArtifacts() API). JPEG files are sorted by most recent if sourced from + * TSK_WEB_DOWNLOADs and by size if sourced from TSK_WEB_CACHE artifacts. + * The first suitable thumbnail is selected. * * @param thumbnailRequest Thumbnail request for domain. * - * @return An Image instance or null if no thumbnail is available. + * @return A thumbnail of the first matching JPEG, or a default thumbnail if + * no suitable JPEG exists. * * @throws DiscoveryException If there is an error with Discovery related - * processing. + * processing. */ public Image getThumbnail(DomainSearchThumbnailRequest thumbnailRequest) throws DiscoveryException { return thumbnailCache.get(thumbnailRequest); } + + /** + * Get all blackboard artifacts that match the requested domain name. + * + * Artifacts will be selected if the requested domain name is either an + * exact match on a TSK_DOMAIN value or a substring match on a TSK_URL + * value. String matching is case insensitive. + * + * @param artifactsRequest The request containing the case, artifact type, + * and domain name. + * @return A list of blackboard artifacts that match the request criteria. + * @throws DiscoveryException If an exception is encountered during + * processing. + */ + public List getArtifacts(DomainSearchArtifactsRequest artifactsRequest) throws DiscoveryException { + return artifactsCache.get(artifactsRequest); + } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCache.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCache.java index 41cfcc7e9b..b91e67ee1a 100755 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCache.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCache.java @@ -47,6 +47,11 @@ public class DomainSearchArtifactsCache { * process. */ public List get(DomainSearchArtifactsRequest request) throws DiscoveryException { + String typeName = request.getArtifactType().getLabel(); + if (!typeName.startsWith("TSK_WEB")) { + throw new IllegalArgumentException("Only web artifacts are valid arguments"); + } + try { return cache.get(request); } catch (ExecutionException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailLoader.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailLoader.java index 309ef798e0..4a17ad6434 100755 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailLoader.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailLoader.java @@ -39,10 +39,10 @@ import org.openide.util.ImageUtilities; /** * Loads a thumbnail for the given request. Thumbnail candidates are JPEG files - * that are either TSK_WEB_DOWNLOAD or TSK_WEB_CACHE artifacts. JPEG files are - * sorted by most recent if sourced from TSK_WEB_DOWNLOADs. JPEG files are - * sorted by size if sourced from TSK_WEB_CACHE artifacts. Artifacts are first - * loaded from the DomainSearchArtifactsCache and then further analyzed. + * that have either TSK_WEB_DOWNLOAD or TSK_WEB_CACHE artifacts that match the + * domain name (see the DomainSearch getArtifacts() API). JPEG files are sorted + * by most recent if sourced from TSK_WEB_DOWNLOADs and by size if sourced from + * TSK_WEB_CACHE artifacts. The first suitable thumbnail is selected. */ public class DomainSearchThumbnailLoader extends CacheLoader { diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCacheTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCacheTest.java new file mode 100755 index 0000000000..fd4c63b6df --- /dev/null +++ b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCacheTest.java @@ -0,0 +1,189 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit 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 org.sleuthkit.autopsy.discovery.search; + +import com.google.common.collect.Lists; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +public class DomainSearchArtifactsCacheTest { + + private static final ARTIFACT_TYPE WEB_ARTIFACT_TYPE = ARTIFACT_TYPE.TSK_WEB_BOOKMARK; + private static final BlackboardAttribute.Type TSK_DOMAIN = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DOMAIN); + private static final BlackboardAttribute.Type TSK_URL = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_URL); + + @Test(expected = IllegalArgumentException.class) + public void get_NonWebArtifactType_ShouldThrow() throws DiscoveryException { + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(null, "google.com", ARTIFACT_TYPE.TSK_CALLLOG); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + cache.get(request); + } + + /* + * This test is important for ensuring artifact loading can + * be cancelled, which is necessary for a responsive UI. + */ + @Test + public void get_ThreadInterrupted_ShouldThrow() throws TskCoreException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + BlackboardArtifact mockArtifact = mock(BlackboardArtifact.class); + when(mockCase.getBlackboardArtifacts(WEB_ARTIFACT_TYPE)).thenReturn(Lists.newArrayList(mockArtifact)); + + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(mockCase, "facebook.com", WEB_ARTIFACT_TYPE); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + Thread.currentThread().interrupt(); + try { + cache.get(request); + // Clear the interrupt flag on failure. + Thread.interrupted(); + Assert.fail("Should have thrown an exception."); + } catch (DiscoveryException ex) { + // Clear the interrupt flag on success (or failure). + Thread.interrupted(); + Assert.assertEquals(InterruptedException.class, ex.getCause().getCause().getClass()); + } + } + + @Test + public void get_MatchingDomain_ShouldHaveSizeOne() throws TskCoreException, DiscoveryException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + BlackboardArtifact mockArtifact = mock(BlackboardArtifact.class); + when(mockArtifact.getAttribute(TSK_DOMAIN)).thenReturn(mockDomainAttribute("google.com")); + when(mockCase.getBlackboardArtifacts(WEB_ARTIFACT_TYPE)).thenReturn(Lists.newArrayList(mockArtifact)); + + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(mockCase, "google.com", WEB_ARTIFACT_TYPE); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + List artifacts = cache.get(request); + Assert.assertEquals(1, artifacts.size()); + Assert.assertEquals(mockArtifact, artifacts.get(0)); + } + + @Test + public void get_MatchingUrl_ShouldHaveSizeOne() throws TskCoreException, DiscoveryException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + BlackboardArtifact mockArtifact = mock(BlackboardArtifact.class); + when(mockArtifact.getAttribute(TSK_URL)).thenReturn(mockURLAttribute("https://www.google.com/search")); + when(mockCase.getBlackboardArtifacts(WEB_ARTIFACT_TYPE)).thenReturn(Lists.newArrayList(mockArtifact)); + + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(mockCase, "google.com", WEB_ARTIFACT_TYPE); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + List artifacts = cache.get(request); + Assert.assertEquals(1, artifacts.size()); + Assert.assertEquals(mockArtifact, artifacts.get(0)); + } + + @Test + public void get_MismatchedDomainName_ShouldBeEmpty() throws TskCoreException, DiscoveryException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + BlackboardArtifact mockArtifact = mock(BlackboardArtifact.class); + when(mockArtifact.getAttribute(TSK_DOMAIN)).thenReturn(mockDomainAttribute("google.com")); + when(mockCase.getBlackboardArtifacts(WEB_ARTIFACT_TYPE)).thenReturn(Lists.newArrayList(mockArtifact)); + + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(mockCase, "facebook.com", WEB_ARTIFACT_TYPE); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + List artifacts = cache.get(request); + Assert.assertEquals(0, artifacts.size()); + } + + @Test + public void get_MismatchedUrl_ShouldBeEmpty() throws DiscoveryException, TskCoreException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + BlackboardArtifact mockArtifact = mock(BlackboardArtifact.class); + when(mockArtifact.getAttribute(TSK_URL)).thenReturn(mockURLAttribute("https://www.go1ogle.com/search")); + when(mockCase.getBlackboardArtifacts(WEB_ARTIFACT_TYPE)).thenReturn(Lists.newArrayList(mockArtifact)); + + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(mockCase, "google.com", WEB_ARTIFACT_TYPE); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + List artifacts = cache.get(request); + Assert.assertEquals(0, artifacts.size()); + } + + @Test + public void get_CaseInsensitiveDomainAttribute_ShouldHaveSizeOne() throws TskCoreException, DiscoveryException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + BlackboardArtifact mockArtifact = mock(BlackboardArtifact.class); + when(mockArtifact.getAttribute(TSK_DOMAIN)).thenReturn(mockDomainAttribute("GooGle.coM")); + when(mockCase.getBlackboardArtifacts(WEB_ARTIFACT_TYPE)).thenReturn(Lists.newArrayList(mockArtifact)); + + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(mockCase, "google.com", WEB_ARTIFACT_TYPE); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + List artifacts = cache.get(request); + Assert.assertEquals(1, artifacts.size()); + Assert.assertEquals(mockArtifact, artifacts.get(0)); + } + + @Test + public void get_CaseInsensitiveRequestDomain_ShouldHaveSizeOne() throws TskCoreException, DiscoveryException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + BlackboardArtifact mockArtifact = mock(BlackboardArtifact.class); + when(mockArtifact.getAttribute(TSK_DOMAIN)).thenReturn(mockDomainAttribute("google.com")); + when(mockCase.getBlackboardArtifacts(WEB_ARTIFACT_TYPE)).thenReturn(Lists.newArrayList(mockArtifact)); + + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(mockCase, "GooGle.coM", WEB_ARTIFACT_TYPE); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + List artifacts = cache.get(request); + Assert.assertEquals(1, artifacts.size()); + Assert.assertEquals(mockArtifact, artifacts.get(0)); + } + + @Test + public void get_CaseInsensitiveUrlAttribute_ShouldHaveSizeOne() throws TskCoreException, DiscoveryException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + BlackboardArtifact mockArtifact = mock(BlackboardArtifact.class); + when(mockArtifact.getAttribute(TSK_URL)).thenReturn(mockURLAttribute("https://www.GooGle.coM/search")); + when(mockCase.getBlackboardArtifacts(WEB_ARTIFACT_TYPE)).thenReturn(Lists.newArrayList(mockArtifact)); + + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(mockCase, "google.com", WEB_ARTIFACT_TYPE); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + List artifacts = cache.get(request); + Assert.assertEquals(1, artifacts.size()); + Assert.assertEquals(mockArtifact, artifacts.get(0)); + } + + @Test + public void get_CaseInsensitiveRequestUrl_ShouldHaveSizeOne() throws TskCoreException, DiscoveryException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + BlackboardArtifact mockArtifact = mock(BlackboardArtifact.class); + when(mockArtifact.getAttribute(TSK_URL)).thenReturn(mockURLAttribute("https://www.google.com/search")); + when(mockCase.getBlackboardArtifacts(WEB_ARTIFACT_TYPE)).thenReturn(Lists.newArrayList(mockArtifact)); + + DomainSearchArtifactsRequest request = new DomainSearchArtifactsRequest(mockCase, "GooGle.cOm", WEB_ARTIFACT_TYPE); + DomainSearchArtifactsCache cache = new DomainSearchArtifactsCache(); + List artifacts = cache.get(request); + Assert.assertEquals(1, artifacts.size()); + Assert.assertEquals(mockArtifact, artifacts.get(0)); + } + + private BlackboardAttribute mockDomainAttribute(String value) { + return new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DOMAIN, "", value); + } + + private BlackboardAttribute mockURLAttribute(String value) { + return new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL, "", value); + } +} diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTest.java index 76fa103104..7dcffed663 100755 --- a/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTest.java +++ b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTest.java @@ -48,7 +48,7 @@ public class DomainSearchTest { }; when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); Map sizes = domainSearch.getGroupSizes(null, new ArrayList<>(), null, null, null, null, null); assertEquals(4, sizes.get(groupOne).longValue()); @@ -83,7 +83,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); Map sizes = domainSearch.getGroupSizes(null, new ArrayList<>(), null, null, null, null, null); assertEquals(4, sizes.get(groupOne).longValue()); @@ -97,7 +97,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(new HashMap<>()); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); Map sizes = domainSearch.getGroupSizes(null, new ArrayList<>(), null, null, null, null, null); assertEquals(0, sizes.size()); @@ -122,7 +122,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); List firstPage = domainSearch.getDomainsInGroup(null, new ArrayList<>(), null, null, null, groupOne, 0, 3, null, null); assertEquals(3, firstPage.size()); @@ -150,7 +150,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); List firstPage = domainSearch.getDomainsInGroup(null, new ArrayList<>(), null, null, null, groupOne, 0, 100, null, null); assertEquals(4, firstPage.size()); @@ -178,7 +178,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); List firstPage = domainSearch.getDomainsInGroup(null, new ArrayList<>(), null, null, null, groupOne, 0, 2, null, null); assertEquals(2, firstPage.size()); @@ -206,7 +206,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); List firstPage = domainSearch.getDomainsInGroup(null, new ArrayList<>(), null, null, null, groupOne, 3, 1, null, null); assertEquals(1, firstPage.size()); @@ -232,7 +232,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); List firstPage = domainSearch.getDomainsInGroup(null, new ArrayList<>(), null, null, null, groupOne, 20, 5, null, null); assertEquals(0, firstPage.size()); @@ -257,7 +257,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); List firstPage = domainSearch.getDomainsInGroup(null, new ArrayList<>(), null, null, null, groupOne, 0, 0, null, null); assertEquals(0, firstPage.size()); @@ -292,7 +292,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); List firstPage = domainSearch.getDomainsInGroup(null, new ArrayList<>(), null, null, null, groupOne, 0, 3, null, null); assertEquals(3, firstPage.size()); @@ -327,7 +327,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); List firstPage = domainSearch.getDomainsInGroup(null, new ArrayList<>(), null, null, null, groupTwo, 1, 2, null, null); assertEquals(2, firstPage.size()); @@ -359,7 +359,7 @@ public class DomainSearchTest { when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); - DomainSearch domainSearch = new DomainSearch(cache, null); + DomainSearch domainSearch = new DomainSearch(cache, null, null); int start = 0; int size = 2;