diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsDAO.java new file mode 100755 index 0000000000..720ff4ece6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsDAO.java @@ -0,0 +1,207 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.mainui.datamodel; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.beans.PropertyChangeEvent; +import java.sql.SQLException; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.autopsy.mainui.nodes.DAOFetcher; +import org.sleuthkit.datamodel.Account; +import org.sleuthkit.datamodel.Blackboard; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Provides information to populate the results viewer for data in the + * Communication Accounts section. + */ +@Messages({"CommAccountsDAO.fileColumns.noDescription=No Description"}) +public class CommAccountsDAO { + + private static final int CACHE_SIZE = Account.Type.PREDEFINED_ACCOUNT_TYPES.size(); // number of cached SearchParams sub-types + private static final long CACHE_DURATION = 2; + private static final TimeUnit CACHE_DURATION_UNITS = TimeUnit.MINUTES; + private final Cache, SearchResultsDTO> searchParamsCache = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).expireAfterAccess(CACHE_DURATION, CACHE_DURATION_UNITS).build(); + + private static CommAccountsDAO instance = null; + + synchronized static CommAccountsDAO getInstance() { + if (instance == null) { + instance = new CommAccountsDAO(); + } + + return instance; + } + + public SearchResultsDTO getCommAcounts(CommAccountsSearchParams key, long startItem, Long maxCount, boolean hardRefresh) throws ExecutionException, IllegalArgumentException { + if (key.getType() == null) { + throw new IllegalArgumentException("Must have non-null type"); + } else if (key.getDataSourceId() != null && key.getDataSourceId() <= 0) { + throw new IllegalArgumentException("Data source id must be greater than 0 or null"); + } + + SearchParams searchParams = new SearchParams<>(key, startItem, maxCount); + if (hardRefresh) { + this.searchParamsCache.invalidate(searchParams); + } + + return searchParamsCache.get(searchParams, () -> fetchCommAccountsDTOs(searchParams)); + } + + /** + * Returns a list of paged artifacts. + * + * @param arts The artifacts. + * @param searchParams The search parameters including the paging. + * + * @return The list of paged artifacts. + */ + List getPaged(List arts, SearchParams searchParams) { + Stream pagedArtsStream = arts.stream() + .sorted(Comparator.comparing((art) -> art.getId())) + .skip(searchParams.getStartItem()); + + if (searchParams.getMaxResultsCount() != null) { + pagedArtsStream = pagedArtsStream.limit(searchParams.getMaxResultsCount()); + } + + return pagedArtsStream.collect(Collectors.toList()); + } + + long getTotalResultsCount(SearchParams cacheKey, long currentPageSize) throws TskCoreException, NoCurrentCaseException { + Blackboard blackboard = Case.getCurrentCaseThrows().getSleuthkitCase().getBlackboard(); + Long dataSourceId = cacheKey.getParamData().getDataSourceId(); + BlackboardArtifact.Type artType = BlackboardArtifact.Type.TSK_ACCOUNT; + + if ( (cacheKey.getStartItem() == 0) // offset is zero AND + && ( (cacheKey.getMaxResultsCount() != null && currentPageSize < cacheKey.getMaxResultsCount()) // number of results is less than max + || (cacheKey.getMaxResultsCount() == null)) ) { // OR max number of results was not specified + return currentPageSize; + } else { + if (dataSourceId != null) { + return blackboard.getArtifactsCount(artType.getTypeID(), dataSourceId); + } else { + return blackboard.getArtifactsCount(artType.getTypeID()); + } + } + } + + @NbBundle.Messages({"CommAccounts.name.text=Communication Accounts"}) + private SearchResultsDTO fetchCommAccountsDTOs(SearchParams cacheKey) throws NoCurrentCaseException, TskCoreException, SQLException { + + // get current page of communication accounts results + SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + Blackboard blackboard = skCase.getBlackboard(); + Account.Type type = cacheKey.getParamData().getType(); + Long dataSourceId = cacheKey.getParamData().getDataSourceId(); + List allArtifacts = blackboard.getArtifacts(BlackboardArtifact.Type.TSK_ACCOUNT, + BlackboardAttribute.Type.TSK_ACCOUNT_TYPE, type.getTypeName(), dataSourceId, + false); // GVDTODO handle approved/rejected account actions + + // get current page of artifacts + List pagedArtifacts = getPaged(allArtifacts, cacheKey); + + // Populate the attributes for paged artifacts in the list. This is done using one database call as an efficient way to + // load many artifacts/attributes at once. + blackboard.loadBlackboardAttributes(pagedArtifacts); + + DataArtifactDAO dataArtDAO = MainDAO.getInstance().getDataArtifactsDAO(); + BlackboardArtifactDAO.TableData tableData = dataArtDAO.createTableData(BlackboardArtifact.Type.TSK_ACCOUNT, pagedArtifacts); + return new DataArtifactTableSearchResultsDTO(BlackboardArtifact.Type.TSK_ACCOUNT, tableData.columnKeys, tableData.rows, cacheKey.getStartItem(), allArtifacts.size()); + } + + /** + * Handles fetching and paging of data for communication accounts. + */ + public static class CommAccountFetcher extends DAOFetcher { + + /** + * Main constructor. + * + * @param params Parameters to handle fetching of data. + */ + public CommAccountFetcher(CommAccountsSearchParams params) { + super(params); + } + + @Override + public SearchResultsDTO getSearchResults(int pageSize, int pageIdx, boolean hardRefresh) throws ExecutionException { + return MainDAO.getInstance().getCommAccountsDAO().getCommAcounts(this.getParameters(), pageIdx * pageSize, (long) pageSize, hardRefresh); + } + + @Override + public boolean isRefreshRequired(PropertyChangeEvent evt) { + CommAccountsSearchParams params = this.getParameters(); + String eventType = evt.getPropertyName(); + + if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked out. + * Currently, remote events may be received for a case that is + * already closed. + */ + try { + Case.getCurrentCaseThrows(); + /** + * Even with the check above, it is still possible that the + * case will be closed in a different thread before this + * code executes. If that happens, it is possible for the + * event to have a null oldValue. + */ + ModuleDataEvent eventData = (ModuleDataEvent) evt.getOldValue(); + if (null != eventData + && eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.Type.TSK_ACCOUNT.getTypeID()) { + + // check that the update is for the same account type + for (BlackboardArtifact artifact : eventData.getArtifacts()) { + for (BlackboardAttribute atribute : artifact.getAttributes()) { + if (atribute.getAttributeType() == BlackboardAttribute.Type.TSK_ACCOUNT_TYPE) { + if (atribute.getValueString().equals(params.getType().toString())) { + return true; + } + } + } + } + } + } catch (NoCurrentCaseException notUsed) { + // Case is closed, do nothing. + } catch (TskCoreException ex) { + // There is nothing we can do with the exception. + } + } + return false; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsSearchParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsSearchParams.java new file mode 100755 index 0000000000..91c5e272ae --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsSearchParams.java @@ -0,0 +1,74 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.mainui.datamodel; + +import java.util.Objects; +import org.sleuthkit.datamodel.Account; + +/** + * Key for accessing data about communication accounts from the DAO. + */ +public class CommAccountsSearchParams { + + private final Account.Type type; + private final Long dataSourceId; + + public CommAccountsSearchParams(Account.Type type, Long dataSourceId) { + this.type = type; + this.dataSourceId = dataSourceId; + } + + public Account.Type getType() { + return type; + } + + public Long getDataSourceId() { + return dataSourceId; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + Objects.hashCode(this.type); + hash = 23 * hash + Objects.hashCode(this.dataSourceId); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final CommAccountsSearchParams other = (CommAccountsSearchParams) obj; + if (!Objects.equals(this.dataSourceId, other.dataSourceId)) { + return false; + } + if (!Objects.equals(this.type, other.type)) { + return false; + } + return true; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemDAO.java index 2d59b4c2f8..b3fa860248 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemDAO.java @@ -31,7 +31,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.autopsy.mainui.datamodel.ContentRowDTO.DirectoryRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.ContentRowDTO.ImageRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.ContentRowDTO.VolumeRowDTO; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java index 81a21ef749..56a65af4ec 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java @@ -39,7 +39,8 @@ public class MainDAO { private final ViewsDAO viewsDAO = ViewsDAO.getInstance(); private final FileSystemDAO fileSystemDAO = FileSystemDAO.getInstance(); private final TagsDAO tagsDAO = TagsDAO.getInstance(); - private final OsAccountsDAO accountsDAO = OsAccountsDAO.getInstance(); + private final OsAccountsDAO osAccountsDAO = OsAccountsDAO.getInstance(); + private final CommAccountsDAO commAccountsDAO = CommAccountsDAO.getInstance(); public DataArtifactDAO getDataArtifactsDAO() { return dataArtifactDAO; @@ -62,6 +63,10 @@ public class MainDAO { } public OsAccountsDAO getOsAccountsDAO() { - return accountsDAO; + return osAccountsDAO; + } + + public CommAccountsDAO getCommAccountsDAO() { + return commAccountsDAO; } } diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/mainui/datamodel/TableSearchTest.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/mainui/datamodel/TableSearchTest.java index b68d478729..1af3f45d14 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/mainui/datamodel/TableSearchTest.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/mainui/datamodel/TableSearchTest.java @@ -37,6 +37,8 @@ import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.testutils.CaseUtils; import org.sleuthkit.autopsy.testutils.TestUtilsException; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Account; +import org.sleuthkit.datamodel.AccountFileInstance; import org.sleuthkit.datamodel.AnalysisResult; import org.sleuthkit.datamodel.Blackboard; import org.sleuthkit.datamodel.Blackboard.BlackboardException; @@ -126,8 +128,17 @@ public class TableSearchTest extends NbTestCase { // OS Accounts test private static final String REALM_NAME_COLUMN = "Realm Name"; - private static final String HOST_COLUMN = "Host"; + private static final String HOST_COLUMN = "Host"; + // Communications accounts test + private static final String ACCOUNT_TYPE_COLUMN = "Account Type"; + private static final String ID_COLUMN = "ID"; + private static final String EMAIL_A = "aaa@yahoo.com"; + private static final String EMAIL_B = "bbb@gmail.com"; + private static final String EMAIL_C = "ccc@funmail.com"; + private static final String PHONENUM_1 = "1117771111"; + private static final String PHONENUM_2 = "2223337777"; + ///////////////////////////////////////////////// // Data to be used across the test methods. // These are initialized in setUpCaseDatabase(). @@ -212,6 +223,7 @@ public class TableSearchTest extends NbTestCase { fileSystemTest(); tagsTest(); OsAccountsTest(); + commAccountsSearchTest(); } /** @@ -496,6 +508,15 @@ public class TableSearchTest extends NbTestCase { osAccount1 = accountMgr.newWindowsOsAccount("S-1-5-21-647283-46237-100", null, null, host2, OsAccountRealm.RealmScope.DOMAIN); accountMgr.newOsAccountInstance(osAccount1, dataSource2, OsAccountInstance.OsAccountInstanceType.LAUNCHED); + // Add communication accounts + openCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, EMAIL_A, "Test Module", fileA1); + openCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, EMAIL_B, "Test Module", fileA2); + openCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE, "devId1", "Test Module", fileA2); + openCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.PHONE, PHONENUM_1, "Test Module", fileA2); + + openCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, EMAIL_C, "Test Module", customFile); + openCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.PHONE, PHONENUM_2, "Test Module", customFile); + } catch (TestUtilsException | TskCoreException | BlackboardException | TagsManager.TagNameAlreadyExistsException | OsAccountManager.NotUserSIDException ex) { if (trans != null) { try { @@ -610,6 +631,72 @@ public class TableSearchTest extends NbTestCase { Assert.fail(ex.getMessage()); } } + + public void commAccountsSearchTest() { + // Quick test that everything is initialized + assertTrue(db != null); + + try { + CommAccountsDAO commAccountsDAO = MainDAO.getInstance().getCommAccountsDAO(); + + // Get emails from all data sources + CommAccountsSearchParams param = new CommAccountsSearchParams(Account.Type.EMAIL, null); + SearchResultsDTO results = commAccountsDAO.getCommAcounts(param, 0, null, false); + assertEquals(3, results.getTotalResultsCount()); + assertEquals(3, results.getItems().size()); + + // Get device accounts from data source 1 + param = new CommAccountsSearchParams(Account.Type.DEVICE, dataSource1.getId()); + results = commAccountsDAO.getCommAcounts(param, 0, null, false); + assertEquals(1, results.getTotalResultsCount()); + assertEquals(1, results.getItems().size()); + + // Get email accounts from data source 2 + param = new CommAccountsSearchParams(Account.Type.EMAIL, dataSource2.getId()); + results = commAccountsDAO.getCommAcounts(param, 0, null, false); + assertEquals(1, results.getTotalResultsCount()); + assertEquals(1, results.getItems().size()); + + // Check that a few of the expected column names are present + List columnDisplayNames = results.getColumns().stream().map(p -> p.getDisplayName()).collect(Collectors.toList()); + assertTrue(columnDisplayNames.contains(ACCOUNT_TYPE_COLUMN)); + assertTrue(columnDisplayNames.contains(ID_COLUMN)); + + // Get the row + RowDTO rowDTO = results.getItems().get(0); + assertTrue(rowDTO instanceof DataArtifactRowDTO); + DataArtifactRowDTO accountResultRowDTO = (DataArtifactRowDTO) rowDTO; + + // Check that some of the expected result column values are present + assertTrue(accountResultRowDTO.getCellValues().contains(EMAIL_C)); + assertTrue(accountResultRowDTO.getCellValues().contains(customFile.getName())); + + // Get phone accounts from all data sources + param = new CommAccountsSearchParams(Account.Type.PHONE, null); + results = commAccountsDAO.getCommAcounts(param, 0, null, false); + assertEquals(2, results.getTotalResultsCount()); + assertEquals(2, results.getItems().size()); + + // Get phone accounts from data source 2 + param = new CommAccountsSearchParams(Account.Type.PHONE, dataSource2.getId()); + results = commAccountsDAO.getCommAcounts(param, 0, null, false); + assertEquals(1, results.getTotalResultsCount()); + assertEquals(1, results.getItems().size()); + + // Get the row + rowDTO = results.getItems().get(0); + assertTrue(rowDTO instanceof DataArtifactRowDTO); + accountResultRowDTO = (DataArtifactRowDTO) rowDTO; + + // Check that some of the expected result column values are present + assertTrue(accountResultRowDTO.getCellValues().contains(PHONENUM_2)); + assertTrue(accountResultRowDTO.getCellValues().contains(customFile.getName())); + + } catch (ExecutionException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex.getMessage()); + } + } public void mimeSearchTest() { // Quick test that everything is initialized