diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED index f06ceeef19ac6743527a963603a4c5f418932bd2..9d168aa0b29391cf093bf4c4c152bf6de5e2be0a 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED +++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED @@ -394,6 +394,7 @@ OsAccountStatus.Unknown.text=Unknown OsAccountStatus.Active.text=Active OsAccountStatus.Disabled.text=Disabled OsAccountStatus.Deleted.text=Deleted +OsAccountStatus.NonExistent.text=Non Existent OsAccountType.Unknown.text=Unknown OsAccountType.Service.text=Service OsAccountType.Interactive.text=Interactive diff --git a/bindings/java/src/org/sleuthkit/datamodel/OsAccountManager.java b/bindings/java/src/org/sleuthkit/datamodel/OsAccountManager.java index 2ad57d7023bceb23177617ac178ae16e622e7547..19d10064b854566cccba009646acff3a92368ddf 100755 --- a/bindings/java/src/org/sleuthkit/datamodel/OsAccountManager.java +++ b/bindings/java/src/org/sleuthkit/datamodel/OsAccountManager.java @@ -864,37 +864,11 @@ void mergeOsAccountsForRealms(OsAccountRealm sourceRealm, OsAccountRealm destRea } // Look for matching destination account - OsAccount matchingDestAccount = null; - - // First look for matching unique id - if (sourceAccount.getAddr().isPresent()) { - List<OsAccount> matchingDestAccounts = destinationAccounts.stream() - .filter(p -> p.getAddr().equals(sourceAccount.getAddr())) - .collect(Collectors.toList()); - if (!matchingDestAccounts.isEmpty()) { - matchingDestAccount = matchingDestAccounts.get(0); - } - } - - // If a match wasn't found yet, look for a matching login name. - // We will merge only if: - // - We didn't already find a unique ID match - // - The source account has no unique ID OR the destination account has no unique ID - // - destination account has a login name and matches the source account login name - if (matchingDestAccount == null && sourceAccount.getLoginName().isPresent()) { - List<OsAccount> matchingDestAccounts = destinationAccounts.stream() - .filter(p -> p.getLoginName().isPresent()) - .filter(p -> (p.getLoginName().get().equalsIgnoreCase(sourceAccount.getLoginName().get()) - && ((!sourceAccount.getAddr().isPresent()) || (!p.getAddr().isPresent())))) - .collect(Collectors.toList()); - if (!matchingDestAccounts.isEmpty()) { - matchingDestAccount = matchingDestAccounts.get(0); - } - } + Optional<OsAccount> matchingDestAccount = getMatchingAccountForMerge(sourceAccount, destinationAccounts); // If we found a match, merge the accounts. Otherwise simply update the realm id - if (matchingDestAccount != null) { - mergeOsAccounts(sourceAccount, matchingDestAccount, trans); + if (matchingDestAccount.isPresent()) { + mergeOsAccounts(sourceAccount, matchingDestAccount.get(), trans); } else { String query = "UPDATE tsk_os_accounts SET realm_id = " + destRealm.getRealmId() + " WHERE os_account_obj_id = " + sourceAccount.getId(); try (Statement s = trans.getConnection().createStatement()) { @@ -906,6 +880,70 @@ void mergeOsAccountsForRealms(OsAccountRealm sourceRealm, OsAccountRealm destRea } } } + + /** + * Checks for matching account in a list of accounts for merging + * @param sourceAccount The account to find matches for + * @param destinationAccounts List of accounts to match against + * @return Optional with OsAccount, Optional.empty if no matching OsAccount is found. + */ + private Optional<OsAccount> getMatchingAccountForMerge(OsAccount sourceAccount, List<OsAccount> destinationAccounts) { + // Look for matching destination account + OsAccount matchingDestAccount = null; + + // First look for matching unique id + if (sourceAccount.getAddr().isPresent()) { + List<OsAccount> matchingDestAccounts = destinationAccounts.stream() + .filter(p -> p.getAddr().equals(sourceAccount.getAddr())) + .collect(Collectors.toList()); + if (!matchingDestAccounts.isEmpty()) { + matchingDestAccount = matchingDestAccounts.get(0); + } + } + + // If a match wasn't found yet, look for a matching login name. + // We will merge only if: + // - We didn't already find a unique ID match + // - The source account has no unique ID OR the destination account has no unique ID + // - destination account has a login name and matches the source account login name + if (matchingDestAccount == null && sourceAccount.getLoginName().isPresent()) { + List<OsAccount> matchingDestAccounts = destinationAccounts.stream() + .filter(p -> p.getLoginName().isPresent()) + .filter(p -> (p.getLoginName().get().equalsIgnoreCase(sourceAccount.getLoginName().get()) + && ((!sourceAccount.getAddr().isPresent()) || (!p.getAddr().isPresent())))) + .collect(Collectors.toList()); + if (!matchingDestAccounts.isEmpty()) { + matchingDestAccount = matchingDestAccounts.get(0); + } + } + + return Optional.ofNullable(matchingDestAccount); + } + + /** + * Checks for matching accounts in the same realm + * and then merges the accounts if a match is found + * @param account The account to find matches for + * @param trans The current transaction. + * @throws TskCoreException + */ + private void mergeOsAccount(OsAccount account, CaseDbTransaction trans) throws TskCoreException { + // Get the realm for the account + Long realmId = account.getRealmId(); + OsAccountRealm realm = db.getOsAccountRealmManager().getRealmByRealmId(realmId, trans.getConnection()); + + // Get all users in the realm (excluding the account) + List<OsAccount> osAccounts = getOsAccounts(realm, trans.getConnection()); + osAccounts.removeIf(acc -> Objects.equals(acc.getId(), account.getId())); + + // Look for matching account + Optional<OsAccount> matchingAccount = getMatchingAccountForMerge(account, osAccounts); + + // If we find a match, merge the accounts. + if (matchingAccount.isPresent()) { + mergeOsAccounts(matchingAccount.get(), account, trans); + } + } /** * Merges data between two accounts so that only one is active at the end @@ -958,7 +996,8 @@ private void mergeOsAccounts(OsAccount sourceAccount, OsAccount destAccount, Cas s.executeUpdate(query); - // TBD: We need to emit another event which tells CT that two accounts are being merged so it can updates other dedicated tables + // register the merged accounts with the transaction to fire off an event + trans.registerMergedOsAccount(sourceAccount.getId(), destAccount.getId()); // Update the source account. Make a dummy signature to prevent problems with the unique constraint. String mergedSignature = makeMergedOsAccountSignature(); @@ -1682,6 +1721,12 @@ private OsAccountUpdateResult updateCoreWindowsOsAccountAttributes(OsAccount osA String resolvedLoginName = WindowsAccountUtils.toWellknownEnglishLoginName(loginName); OsAccountUpdateResult updateStatus = this.updateOsAccountCore(osAccount, accountSid, resolvedLoginName, trans); + Optional<OsAccount> updatedAccount = updateStatus.getUpdatedAccount(); + if (updatedAccount.isPresent()) { + // After updating account data, check if there is matching account to merge + mergeOsAccount(updatedAccount.get(), trans); + } + return updateStatus; } diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java index 63d7a9e64063c09ec9d0b712c93bf047418499fc..1d35554c0bf6a185c3251de4cde544921a0ff8fb 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java +++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java @@ -13722,6 +13722,7 @@ public static final class CaseDbTransaction { private List<Host> hostsAdded = new ArrayList<>(); private List<OsAccount> accountsChanged = new ArrayList<>(); private List<OsAccount> accountsAdded = new ArrayList<>(); + private List<TskEvent.MergedAccountsPair> accountsMerged = new ArrayList<>(); private List<Long> deletedOsAccountObjectIds = new ArrayList<>(); private List<Long> deletedResultObjectIds = new ArrayList<>(); @@ -13809,6 +13810,16 @@ void registerAddedOsAccount(OsAccount account) { } } + /** + * Saves an account that has been merged as part of this transaction. + * + * @param sourceOsAccountObjId + * @param destinationOsAccountObjId + */ + void registerMergedOsAccount(long sourceOsAccountObjId, long destinationOsAccountObjId) { + accountsMerged.add(new TskEvent.MergedAccountsPair(sourceOsAccountObjId, destinationOsAccountObjId)); + } + /** * Saves an analysis result that has been deleted as a part of this * transaction. @@ -13885,6 +13896,9 @@ public void commit() throws TskCoreException { if (!accountsChanged.isEmpty()) { sleuthkitCase.fireTSKEvent(new TskEvent.OsAccountsUpdatedTskEvent(accountsChanged)); } + if (!accountsMerged.isEmpty()) { + sleuthkitCase.fireTSKEvent(new TskEvent.OsAccountsMergedTskEvent(accountsMerged)); + } if (!deletedOsAccountObjectIds.isEmpty()) { sleuthkitCase.fireTSKEvent(new TskEvent.OsAccountsDeletedTskEvent(deletedOsAccountObjectIds)); } diff --git a/bindings/java/src/org/sleuthkit/datamodel/TskEvent.java b/bindings/java/src/org/sleuthkit/datamodel/TskEvent.java index a6e193382fe73b6e7759760c5508496ee21ae3d9..da6842036cf6546fc219e4eea2c2a24587b9ff7c 100755 --- a/bindings/java/src/org/sleuthkit/datamodel/TskEvent.java +++ b/bindings/java/src/org/sleuthkit/datamodel/TskEvent.java @@ -307,6 +307,63 @@ public List<Long> getOsAccountObjectIds() { } + /** + * An event published when one or more OS accounts are merged. + */ + public final static class OsAccountsMergedTskEvent extends TskObjectsEvent<MergedAccountsPair> { + + /** + * Constructs an event published when one or more OS accounts are + * merged. + * + * @param mergedAccounts List of the merged OS accounts. + */ + OsAccountsMergedTskEvent(List<MergedAccountsPair> mergedAccounts) { + super(mergedAccounts); + } + + + /** + * Gets the pairs of merged accounts + * + * @return + */ + public List<MergedAccountsPair> getMergedAccountPairs() { + return getDataModelObjects(); + } + + } + + /** + * Container to encapsulate the merged account ids, contains both the source and destination account ids. + */ + public final static class MergedAccountsPair { + + private final Long sourceOsAccountId; + private final Long destinationOsAccountId; + + public MergedAccountsPair(Long sourceOsAccountId, Long destinationOsAccountId) { + this.sourceOsAccountId = sourceOsAccountId; + this.destinationOsAccountId = destinationOsAccountId; + } + + /** + * Gets the source os account id. This is the account that was marked as "MERGED" + * @return The TSK object ID of the source os account + */ + public Long getSourceOsAccountId() { + return sourceOsAccountId; + } + + /** + * Gets the destination os account id. This is the account that the source was merged into. + * @return The TSK object ID of the destination os account + */ + public Long getDestinationOsAccountId() { + return destinationOsAccountId; + } + } + /** * An event published when one or more OS account instances are added. */ diff --git a/bindings/java/test/org/sleuthkit/datamodel/OsAccountTest.java b/bindings/java/test/org/sleuthkit/datamodel/OsAccountTest.java index 8f3e373d06af979f7000d9aee5d566fe724f0cc2..b18826c9b745bdaad566dd51e4270b7be1dc0bcb 100644 --- a/bindings/java/test/org/sleuthkit/datamodel/OsAccountTest.java +++ b/bindings/java/test/org/sleuthkit/datamodel/OsAccountTest.java @@ -396,6 +396,87 @@ public void mergeRealmsTests() throws TskCoreException, OsAccountManager.NotUser } } + @Test + public void updateRealmAndMergeTests() throws TskCoreException, OsAccountManager.NotUserSIDException { + + /** + * Test the scenario where an update of an account triggers an update of + * a realm and subsequent merge of realms and accounts. + */ + + Host host = caseDB.getHostManager().newHost("updateRealmAndMergeTestHost"); + + + + // Step 1: create a local account with SID and user name + String ownerUid1 = "S-1-5-21-1182664808-117526782-2525957323-13395"; + String realmName1 = null; + String loginName1 = "sandip"; + + OsAccount osAccount1 = caseDB.getOsAccountManager().newWindowsOsAccount(ownerUid1, loginName1, realmName1, host, OsAccountRealm.RealmScope.LOCAL); + OsAccountRealm realm1 = caseDB.getOsAccountRealmManager().getRealmByRealmId(osAccount1.getRealmId()); + + assertEquals(realm1.getRealmAddr().isPresent(), true); // verify the realm has a SID + assertEquals(realm1.getRealmNames().isEmpty(), true); // verify the realm has no name + + + // Step2: create a local account with domain name and username + String ownerUid2 = null; + String realmName2 = "CORP"; + String loginName2 = "sandip"; + + Optional<OsAccount> oOsAccount2 = caseDB.getOsAccountManager().getWindowsOsAccount(ownerUid2, loginName2, realmName2, host); + + // this account should not exists + assertEquals(oOsAccount2.isPresent(), false); + + // create a new account - a new realm as there is nothing to tie it to realm1 + OsAccount osAccount2 = caseDB.getOsAccountManager().newWindowsOsAccount(ownerUid2, loginName2, realmName2, host, OsAccountRealm.RealmScope.LOCAL); + OsAccountRealm realm2 = caseDB.getOsAccountRealmManager().getRealmByRealmId(osAccount2.getRealmId()); + + assertTrue(osAccount1.getId() != osAccount2.getId()); + assertTrue(realm1.getRealmId() != realm2.getRealmId()); + + + + // Step 3: now create/update the account with sid/domain/username + // this should return the existing account1, which needs to be updated. + String ownerUid3 = "S-1-5-21-1182664808-117526782-2525957323-13395"; + String realmAddr3 = "S-1-5-21-1182664808-117526782-2525957323"; + String loginName3 = "sandip"; + String realmName3 = "CORP"; + + Optional<OsAccount> oOsAccount3 = caseDB.getOsAccountManager().getWindowsOsAccount(ownerUid3, loginName3, realmName3, host); + + assertTrue(oOsAccount3.isPresent()); + + + // update the account so that its domain gets updated. + OsAccountManager.OsAccountUpdateResult updateResult = caseDB.getOsAccountManager().updateCoreWindowsOsAccountAttributes(oOsAccount3.get(), ownerUid3, loginName3, realmName3, host); + Optional<OsAccount> updatedAccount3 = updateResult.getUpdatedAccount(); + assertTrue(updatedAccount3.isPresent()); + + // this should cause the realm1 to be updated - and then realm2 to be merged into realm1 + OsAccountRealm realm3 = caseDB.getOsAccountRealmManager().getRealmByRealmId(updatedAccount3.get().getRealmId()); + + assertTrue(realm3.getRealmId() == realm1.getRealmId()); + + assertTrue(realm3.getRealmAddr().isPresent()); // verify the realm gets an addr + assertTrue(realm3.getRealmAddr().get().equalsIgnoreCase(realmAddr3)); + + assertTrue(realm3.getRealmNames().get(0).equalsIgnoreCase(realmName3)); // verify realm name. + + + // And now verify that the realm2 has been merged into realm1. + OsAccountRealm realm22 = caseDB.getOsAccountRealmManager().getRealmByRealmId(osAccount2.getRealmId()); + assertTrue(realm22.getDbStatus() == OsAccountRealm.RealmDbStatus.MERGED); + + //and account2 has been merged into account1 + OsAccount osAccount22 = caseDB.getOsAccountManager().getOsAccountByObjectId(osAccount2.getId()); + assertTrue(osAccount22.getOsAccountDbStatus() == OsAccount.OsAccountDbStatus.MERGED); + + } + @Test public void hostAddressTests() throws TskCoreException { @@ -1193,11 +1274,43 @@ public void windowsAccountUpdateTests() throws TskCoreException, OsAccountManage assertTrue(updatedAccount2.getLoginName().orElse("").equalsIgnoreCase(loginname2)); assertTrue(updatedAccount2.getSignature().equalsIgnoreCase(ownerUid2)); // account signature should now be addr - // RAMAN TODO: CT-4284 -// OsAccountRealm realm2 = caseDB.getOsAccountRealmManager().getRealmByRealmId(updatedAccount2.getRealmId()); -// assertTrue(realm2.getRealmAddr().orElse("").equalsIgnoreCase(realmAddr1)); -// assertTrue(realm2.getSignature().equalsIgnoreCase(realmSignature1)); + OsAccountRealm realm2 = caseDB.getOsAccountRealmManager().getRealmByRealmId(updatedAccount2.getRealmId()); + assertTrue(realm2.getRealmAddr().orElse("").equalsIgnoreCase(realmAddr1)); + assertTrue(realm2.getSignature().equalsIgnoreCase(realmSignature1)); } + @Test + public void windowsAccountMergeTests() throws TskCoreException, OsAccountManager.NotUserSIDException { + + String hostname1 = "windowsAccountMergeTestHost"; + Host host1 = caseDB.getHostManager().newHost(hostname1); + // 1. Create an account with a SID alone + String sid = "S-1-5-21-111111111-222222222-666666666-0001"; + OsAccount osAccount1 = caseDB.getOsAccountManager().newWindowsOsAccount(sid, null, null, host1, OsAccountRealm.RealmScope.LOCAL); + + Long realmId = osAccount1.getRealmId(); + + // 2. Create an account with loginName and realmName + String loginName = "jdoe"; + String realmName = "testRealm"; + OsAccount osAccount2 = caseDB.getOsAccountManager().newWindowsOsAccount(null, loginName, realmName, host1, OsAccountRealm.RealmScope.LOCAL); + + // 3. Lookup account by SID, loginName, and realmName + Optional<OsAccount> oOsAccount = caseDB.getOsAccountManager().getWindowsOsAccount(sid, loginName, realmName, host1); + assertTrue(oOsAccount.isPresent()); + + // 4. Update this account with all SID, loginName, and realmName + caseDB.getOsAccountManager().updateCoreWindowsOsAccountAttributes(oOsAccount.get(), sid, loginName, realmName, host1); + + // The two accounts should be merged + + // Test that there is now only one account associated with sid1 + List<OsAccount> accounts = caseDB.getOsAccountManager().getOsAccounts().stream().filter(a -> a.getAddr().isPresent() && a.getAddr().get().equals(sid)).collect(Collectors.toList()); + assertEquals(accounts.size() == 1, true); + + // Test that there is now only one account associated with loginName + accounts = caseDB.getOsAccountManager().getOsAccounts().stream().filter(p -> p.getLoginName().isPresent() && p.getLoginName().get().equals(loginName)).collect(Collectors.toList()); + assertEquals(accounts.size() == 1, true); + } } diff --git a/tsk/fs/logical_fs.cpp b/tsk/fs/logical_fs.cpp index 68c98b1c2a22503a02fc748e0374c31171956aa1..897c465645545187f421045d5e894de477ca5c30 100644 --- a/tsk/fs/logical_fs.cpp +++ b/tsk/fs/logical_fs.cpp @@ -321,7 +321,7 @@ TSK_TCHAR * create_search_path_long_path(const TSK_TCHAR *base_path) { return searchPath; #else // Nothing to do here if it's not Windows - return null; + return NULL; #endif }