diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/Bundle.properties-MERGED index 602b270a71..681c2f5660 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/Bundle.properties-MERGED @@ -81,6 +81,17 @@ FileSystemColumnUtils.volumeColumns.length=Length in Sectors FileSystemColumnUtils.volumeColumns.startingSector=Starting Sector FileTag.name.text=File Tag FileTypesByMimeType.name.text=By MIME Type +OsAccounts.name.text=OS Accounts +OsAccountsDAO.createSheet.comment.displayName=C +OsAccountsDAO.createSheet.count.displayName=O +OsAccountsDAO.createSheet.score.displayName=S +OsAccountsDAO.fileColumns.noDescription=No Description +OsAccountsDAO_accountHostNameProperty_displayName=Host +OsAccountsDAO_accountNameProperty_displayName=Name +OsAccountsDAO_accountRealmNameProperty_displayName=Realm Name +OsAccountsDAO_accountScopeNameProperty_displayName=Scope +OsAccountsDAO_createdTimeProperty_displayName=Creation Time +OsAccountsDAO_loginNameProperty_displayName=Login Name ResultTag.name.text=Result Tag TagsDAO.fileColumns.accessTimeColLbl=Accessed Time TagsDAO.fileColumns.changeTimeColLbl=Changed Time diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java index de0e4b5ddd..81a21ef749 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java @@ -39,6 +39,7 @@ 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(); public DataArtifactDAO getDataArtifactsDAO() { return dataArtifactDAO; @@ -59,4 +60,8 @@ public class MainDAO { public TagsDAO getTagsDAO() { return tagsDAO; } + + public OsAccountsDAO getOsAccountsDAO() { + return accountsDAO; + } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsDAO.java new file mode 100755 index 0000000000..a5d34088fe --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsDAO.java @@ -0,0 +1,198 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +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.coreutils.TimeZoneUtils; +import org.sleuthkit.autopsy.mainui.nodes.DAOFetcher; +import org.sleuthkit.datamodel.OsAccount; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Provides information to populate the results viewer for data in the OS + * Accounts section. + */ +@Messages({ + "OsAccountsDAO_accountNameProperty_displayName=Name", + "OsAccountsDAO_accountRealmNameProperty_displayName=Realm Name", + "OsAccountsDAO_accountHostNameProperty_displayName=Host", + "OsAccountsDAO_accountScopeNameProperty_displayName=Scope", + "OsAccountsDAO_createdTimeProperty_displayName=Creation Time", + "OsAccountsDAO_loginNameProperty_displayName=Login Name", + "OsAccountsDAO.createSheet.score.displayName=S", + "OsAccountsDAO.createSheet.comment.displayName=C", + "OsAccountsDAO.createSheet.count.displayName=O", + "OsAccountsDAO.fileColumns.noDescription=No Description",}) +public class OsAccountsDAO { + + private static final int CACHE_SIZE = 5; // rule of thumb: 5 entries times 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 final String OS_ACCOUNTS_TYPE_ID = "OS_ACCOUNTS"; + + private static final List OS_ACCOUNTS_WITH_SCO_COLUMNS = Arrays.asList( + getFileColumnKey(Bundle.OsAccountsDAO_accountNameProperty_displayName()), + getFileColumnKey(Bundle.OsAccountsDAO_createSheet_score_displayName()), + getFileColumnKey(Bundle.OsAccountsDAO_createSheet_comment_displayName()), + getFileColumnKey(Bundle.OsAccountsDAO_createSheet_count_displayName()), + getFileColumnKey(Bundle.OsAccountsDAO_loginNameProperty_displayName()), + getFileColumnKey(Bundle.OsAccountsDAO_accountHostNameProperty_displayName()), + getFileColumnKey(Bundle.OsAccountsDAO_accountScopeNameProperty_displayName()), + getFileColumnKey(Bundle.OsAccountsDAO_accountRealmNameProperty_displayName()), + getFileColumnKey(Bundle.OsAccountsDAO_createdTimeProperty_displayName())); + + private static OsAccountsDAO instance = null; + + synchronized static OsAccountsDAO getInstance() { + if (instance == null) { + instance = new OsAccountsDAO(); + } + + return instance; + } + + private static ColumnKey getFileColumnKey(String name) { + return new ColumnKey(name, name, Bundle.OsAccountsDAO_fileColumns_noDescription()); + } + + public SearchResultsDTO getAccounts(OsAccountsSearchParams key, long startItem, Long maxCount, boolean hardRefresh) throws ExecutionException, IllegalArgumentException { + if (key == null) { + throw new IllegalArgumentException("Search parameters are null"); + } 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, () -> fetchAccountsDTOs(searchParams)); + } + + /** + * Returns a list of paged OS Accounts results. + * + * @param accounts The OS Accounts results. + * @param searchParams The search parameters including the paging. + * + * @return The list of paged OS Accounts results. + */ + List getPaged(List accounts, SearchParams searchParams) { + Stream pagedAccountsStream = accounts.stream() + .sorted(Comparator.comparing((acct) -> acct.getId())) + .skip(searchParams.getStartItem()); + + if (searchParams.getMaxResultsCount() != null) { + pagedAccountsStream = pagedAccountsStream.limit(searchParams.getMaxResultsCount()); + } + + return pagedAccountsStream.collect(Collectors.toList()); + } + + @NbBundle.Messages({"OsAccounts.name.text=OS Accounts"}) + private SearchResultsDTO fetchAccountsDTOs(SearchParams cacheKey) throws NoCurrentCaseException, TskCoreException { + + Long dataSourceId = cacheKey.getParamData().getDataSourceId(); + + // get all accounts + List allAccounts = (dataSourceId != null && dataSourceId > 0) + ? Case.getCurrentCaseThrows().getSleuthkitCase().getOsAccountManager().getOsAccountsByDataSourceObjId(dataSourceId) + : Case.getCurrentCaseThrows().getSleuthkitCase().getOsAccountManager().getOsAccounts(); + + // get current page of accounts results + List pagedAccounts = getPaged(allAccounts, cacheKey); + + List fileRows = new ArrayList<>(); + for (OsAccount account : pagedAccounts) { + + Optional optional = account.getLoginName(); + Optional creationTimeValue = account.getCreationTime(); + String timeDisplayStr + = creationTimeValue.isPresent() ? TimeZoneUtils.getFormattedTime(creationTimeValue.get()) : ""; + List cellValues = Arrays.asList( + account.getName() != null ? account.getName() : "", + // GVDTODO handle SCO + // GVDTODO only show if (!UserPreferences.getHideSCOColumns()) + null, + null, + // GVDTODO only show if central repository enabled + null, + optional.isPresent() ? optional.get() : "", + "", + "", + "", // GVDTODO this is filled by a background GetOsAccountRealmTask task + timeDisplayStr); + + fileRows.add(new BaseRowDTO( + cellValues, + OS_ACCOUNTS_TYPE_ID, + account.getId())); + }; + + return new BaseSearchResultsDTO(OS_ACCOUNTS_TYPE_ID, Bundle.OsAccounts_name_text(), OS_ACCOUNTS_WITH_SCO_COLUMNS, fileRows, 0, allAccounts.size()); + } + + /** + * Handles fetching and paging of data for accounts. + */ + public static class AccountFetcher extends DAOFetcher { + + /** + * Main constructor. + * + * @param params Parameters to handle fetching of data. + */ + public AccountFetcher(OsAccountsSearchParams params) { + super(params); + } + + @Override + public SearchResultsDTO getSearchResults(int pageSize, int pageIdx, boolean hardRefresh) throws ExecutionException { + return MainDAO.getInstance().getOsAccountsDAO().getAccounts(this.getParameters(), pageIdx * pageSize, (long) pageSize, hardRefresh); + } + + @Override + public boolean isRefreshRequired(PropertyChangeEvent evt) { + String eventType = evt.getPropertyName(); + if (eventType.equals(Case.Events.OS_ACCOUNTS_ADDED.toString()) + || eventType.equals(Case.Events.OS_ACCOUNTS_DELETED.toString())) { + return true; + } + return false; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsSearchParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsSearchParams.java new file mode 100755 index 0000000000..e21db61826 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsSearchParams.java @@ -0,0 +1,62 @@ +/* + * 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; + +/** + * Key for accessing data about OS Accounts from the DAO. + */ +public class OsAccountsSearchParams { + + private final Long dataSourceId; + + public OsAccountsSearchParams(Long dataSourceId) { + this.dataSourceId = dataSourceId; + } + + public Long getDataSourceId() { + return dataSourceId; + } + + @Override + public int hashCode() { + int hash = 7; + 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 OsAccountsSearchParams other = (OsAccountsSearchParams) obj; + if (!Objects.equals(this.dataSourceId, other.dataSourceId)) { + return false; + } + return true; + } +} 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 992dcb9158..a42d0433f4 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 @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.concurrent.ExecutionException; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import junit.framework.Assert; @@ -37,7 +38,6 @@ import org.sleuthkit.autopsy.testutils.CaseUtils; import org.sleuthkit.autopsy.testutils.TestUtilsException; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AnalysisResult; -import org.sleuthkit.datamodel.Attribute; import org.sleuthkit.datamodel.Blackboard; import org.sleuthkit.datamodel.Blackboard.BlackboardException; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -47,9 +47,14 @@ import org.sleuthkit.datamodel.DataArtifact; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.FileSystem; import org.sleuthkit.datamodel.Host; +import org.sleuthkit.datamodel.HostManager; import org.sleuthkit.datamodel.Person; import org.sleuthkit.datamodel.Pool; import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.OsAccount; +import org.sleuthkit.datamodel.OsAccountInstance; +import org.sleuthkit.datamodel.OsAccountManager; +import org.sleuthkit.datamodel.OsAccountRealm; import org.sleuthkit.datamodel.Score; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TagName; @@ -119,6 +124,9 @@ public class TableSearchTest extends NbTestCase { private static final String PERSON_HOST_NAME1 = "Host for Person A"; private static final String PERSON_HOST_NAME2 = "Host for Person B"; + // OS Accounts test + private static final String REALM_NAME_COLUMN = "Realm Name"; + private static final String HOST_COLUMN = "Host"; ///////////////////////////////////////////////// // Data to be used across the test methods. @@ -128,6 +136,7 @@ public class TableSearchTest extends NbTestCase { SleuthkitCase db = null; // The case database Blackboard blackboard = null; // The blackboard TagsManager tagsManager = null;// Tags manager + OsAccountManager accountMgr = null; DataSource dataSource1 = null; // A local files data source DataSource dataSource2 = null; // A local files data source @@ -172,6 +181,9 @@ public class TableSearchTest extends NbTestCase { // Tags test TagName knownTag1 = null; TagName tag2 = null; + + // OS Accounts test + OsAccount osAccount1 = null; public static Test suite() { NbModuleSuite.Configuration conf = NbModuleSuite.createConfiguration(TableSearchTest.class). @@ -199,6 +211,7 @@ public class TableSearchTest extends NbTestCase { sizeSearchTest(); fileSystemTest(); tagsTest(); + OsAccountsTest(); } /** @@ -212,6 +225,7 @@ public class TableSearchTest extends NbTestCase { db = openCase.getSleuthkitCase(); blackboard = db.getBlackboard(); tagsManager = openCase.getServices().getTagsManager(); + accountMgr = openCase.getSleuthkitCase().getOsAccountManager(); // Add two logical files data sources trans = db.beginTransaction(); @@ -467,8 +481,20 @@ public class TableSearchTest extends NbTestCase { // Tag the custom file in data source 2 openCase.getServices().getTagsManager().addContentTag(customFile, knownTag1); - - } catch (TestUtilsException | TskCoreException | BlackboardException | TagsManager.TagNameAlreadyExistsException ex) { + + // Add OS Accounts --------------------- + HostManager hostMrg = openCase.getSleuthkitCase().getHostManager(); + Host host1 = hostMrg.getHostByDataSource(dataSource1); + OsAccount osAccount2 = accountMgr.newWindowsOsAccount("S-1-5-21-647283-46237-200", null, null, host1, OsAccountRealm.RealmScope.LOCAL); + accountMgr.newOsAccountInstance(osAccount2, dataSource1, OsAccountInstance.OsAccountInstanceType.ACCESSED); + OsAccount osAccount3 = accountMgr.newWindowsOsAccount("S-1-5-21-647283-46237-300", null, null, host1, OsAccountRealm.RealmScope.UNKNOWN); + accountMgr.newOsAccountInstance(osAccount3, dataSource1, OsAccountInstance.OsAccountInstanceType.REFERENCED); + + Host host2 = hostMrg.getHostByDataSource(dataSource2); + osAccount1 = accountMgr.newWindowsOsAccount("S-1-5-21-647283-46237-100", null, null, host2, OsAccountRealm.RealmScope.DOMAIN); + accountMgr.newOsAccountInstance(osAccount1, dataSource2, OsAccountInstance.OsAccountInstanceType.LAUNCHED); + + } catch (TestUtilsException | TskCoreException | BlackboardException | TagsManager.TagNameAlreadyExistsException | OsAccountManager.NotUserSIDException ex) { if (trans != null) { try { trans.rollback(); @@ -759,7 +785,52 @@ public class TableSearchTest extends NbTestCase { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } - } + } + + public void OsAccountsTest() { + // Quick test that everything is initialized + assertTrue(db != null); + + try { + OsAccountsDAO accountsDAO = MainDAO.getInstance().getOsAccountsDAO(); + + // Get OS Accounts from data source 1 + OsAccountsSearchParams param = new OsAccountsSearchParams(dataSource1.getId()); + SearchResultsDTO results = accountsDAO.getAccounts(param, 0, null, false); + assertEquals(2, results.getTotalResultsCount()); + assertEquals(2, results.getItems().size()); + + // Get OS Accounts from all data sources + param = new OsAccountsSearchParams(null); + results = accountsDAO.getAccounts(param, 0, null, false); + assertEquals(3, results.getTotalResultsCount()); + assertEquals(3, results.getItems().size()); + + // Get OS Accounts from data source 1 + param = new OsAccountsSearchParams(dataSource2.getId()); + results = accountsDAO.getAccounts(param, 0, null, false); + assertEquals(1, results.getTotalResultsCount()); + assertEquals(1, results.getItems().size()); + + // Get the row + RowDTO rowDTO = results.getItems().get(0); + assertTrue(rowDTO instanceof BaseRowDTO); + BaseRowDTO osAccountRowDTO = (BaseRowDTO) rowDTO; + + // Check that the result is for the custom OS Account + Optional addr = osAccount1.getAddr(); + assertTrue(osAccountRowDTO.getCellValues().contains(addr.get())); + + // Check that a few of the expected OS Account column names are present + List columnDisplayNames = results.getColumns().stream().map(p -> p.getDisplayName()).collect(Collectors.toList()); + assertTrue(columnDisplayNames.contains(REALM_NAME_COLUMN)); + assertTrue(columnDisplayNames.contains(HOST_COLUMN)); + + } catch (ExecutionException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex.getMessage()); + } + } public void analysisResultSearchTest() { // Quick test that everything is initialized @@ -1131,5 +1202,6 @@ public class TableSearchTest extends NbTestCase { db = null; blackboard = null; tagsManager = null; + accountMgr = null; } }