Merge pull request #7419 from eugene7646/com_accounts_8122

Communications accounts DAO (8122)
This commit is contained in:
Richard Cordovano 2021-11-18 13:58:23 -05:00 committed by GitHub
commit ab3540df2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 376 additions and 4 deletions

View File

@ -0,0 +1,207 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2021 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 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<SearchParams<?>, 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<CommAccountsSearchParams> 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<BlackboardArtifact> getPaged(List<? extends BlackboardArtifact> arts, SearchParams<?> searchParams) {
Stream<? extends BlackboardArtifact> 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<CommAccountsSearchParams> 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<CommAccountsSearchParams> 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<BlackboardArtifact> 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<BlackboardArtifact> 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<CommAccountsSearchParams> {
/**
* 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;
}
}
}

View File

@ -0,0 +1,74 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2021 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 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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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();
}
/**
@ -494,6 +506,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 {
@ -608,6 +629,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<String> 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