diff --git a/bindings/java/src/org/sleuthkit/datamodel/OsAccountManager.java b/bindings/java/src/org/sleuthkit/datamodel/OsAccountManager.java index 19d10064b854566cccba009646acff3a92368ddf..ceee46a7218d39cecb3768191db27992e2dfa86a 100755 --- a/bindings/java/src/org/sleuthkit/datamodel/OsAccountManager.java +++ b/bindings/java/src/org/sleuthkit/datamodel/OsAccountManager.java @@ -19,6 +19,7 @@ package org.sleuthkit.datamodel; import com.google.common.base.Strings; +import com.google.common.annotations.Beta; import org.apache.commons.lang3.StringUtils; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -331,6 +332,84 @@ public OsAccount newWindowsOsAccount(String sid, String loginName, OsAccountReal } } } + + /** + * Creates a local OS account with Linux-specific data. If an account already + * exists with the given id or realm/login, then the existing OS account is + * returned. + * + * @param uid Account uid, can be null if loginName is supplied. + * @param loginName Login name, can be null if uid is supplied. + * @param referringHost The associated host. + * + * @return OsAccount. + * + * @throws TskCoreException If there is an error in + * creating the OSAccount. + * + */ + @Beta + public OsAccount newLocalLinuxOsAccount(String uid, String loginName, Host referringHost) throws TskCoreException { + + if (referringHost == null) { + throw new TskCoreException("A referring host is required to create a local OS account."); + } + + // Ensure at least one of the two is supplied - a non-null unique id or a login name + if (StringUtils.isBlank(uid) && StringUtils.isBlank(loginName)) { + throw new TskCoreException("Cannot create OS account with both uniqueId and loginName as null."); + } + + OsAccountRealm localRealm = db.getOsAccountRealmManager().newLocalLinuxRealm(referringHost); + + CaseDbTransaction trans = db.beginTransaction(); + try { + + // try to create account + try { + OsAccount account = newOsAccount(uid, loginName, localRealm, OsAccount.OsAccountStatus.UNKNOWN, trans); + trans.commit(); + trans = null; + return account; + } catch (SQLException ex) { + // Rollback the transaction before proceeding + trans.rollback(); + trans = null; + + // Create may fail if an OsAccount already exists. + Optional<OsAccount> osAccount; + + // First search for account by uniqueId + if (!Strings.isNullOrEmpty(uid)) { + osAccount = getOsAccountByAddr(uid, localRealm); + if (osAccount.isPresent()) { + return osAccount.get(); + } + } + + // search by loginName + if (!Strings.isNullOrEmpty(loginName)) { + osAccount = getOsAccountByLoginName(loginName, localRealm); + if (osAccount.isPresent()) { + return osAccount.get(); + } + } + + // create failed for some other reason, throw an exception + throw new TskCoreException(String.format("Error creating OsAccount with uid = %s, loginName = %s, realm = %s, referring host = %s", + (uid != null) ? uid : "Null", + (loginName != null) ? loginName : "Null", + (!localRealm.getRealmNames().isEmpty()) ? localRealm.getRealmNames().get(0) : "Null", + localRealm.getScopeHost().isPresent() ? localRealm.getScopeHost().get().getName() : "Null"), ex); + + } + } finally { + if (trans != null) { + trans.rollback(); + } + } + } + /** * Creates a OS account with the given uid, name, and realm. @@ -426,7 +505,7 @@ private Optional<OsAccount> getOsAccountByAddr(String uniqueId, Host host, CaseD String queryString = "SELECT accounts.os_account_obj_id as os_account_obj_id, accounts.login_name, accounts.full_name, " + " accounts.realm_id, accounts.addr, accounts.signature, " - + " accounts.type, accounts.status, accounts.admin, accounts.created_date, accounts.db_status, " + + " accounts.type, accounts.status, accounts.created_date, accounts.db_status, " + " realms.realm_name as realm_name, realms.realm_addr as realm_addr, realms.realm_signature, realms.scope_host_id, realms.scope_confidence, realms.db_status as realm_db_status " + " FROM tsk_os_accounts as accounts" + " LEFT JOIN tsk_os_account_realms as realms" @@ -1212,6 +1291,52 @@ public Optional<OsAccount> getWindowsOsAccount(String sid, String loginName, Str } } + /** + * Gets an OS account using Linux-specific data. + * + * @param uid Account UID, maybe null if loginName is supplied. + * @param loginName Login name, maybe null if sid is supplied. + * @param referringHost Host referring the account. + * + * @return Optional with OsAccount, Optional.empty if no matching OsAccount + * is found. + * + * @throws TskCoreException If there is an error getting the account. + */ + @Beta + public Optional<OsAccount> getLocalLinuxOsAccount(String uid, String loginName, Host referringHost) throws TskCoreException { + + if (referringHost == null) { + throw new TskCoreException("A referring host is required to get an account."); + } + + // ensure at least one of the two is supplied - a non-null uid or a login name + if (StringUtils.isBlank(uid) && StringUtils.isBlank(loginName)) { + throw new TskCoreException("Cannot get an OS account with both UID and loginName as null."); + } + + // First get the local realm + Optional<OsAccountRealm> realm = db.getOsAccountRealmManager().getLocalLinuxRealm(referringHost); + if (!realm.isPresent()) { + return Optional.empty(); + } + + // Search by UID + if (!Strings.isNullOrEmpty(uid)) { + Optional<OsAccount> account = this.getOsAccountByAddr(uid, realm.get()); + if (account.isPresent()) { + return account; + } + } + + // Search by login name + if (!Strings.isNullOrEmpty(loginName)) { + return this.getOsAccountByLoginName(loginName, realm.get()); + } else { + return Optional.empty(); + } + } + /** * Adds a rows to the tsk_os_account_attributes table for the given set of * attribute. @@ -1730,6 +1855,70 @@ private OsAccountUpdateResult updateCoreWindowsOsAccountAttributes(OsAccount osA return updateStatus; } + /** + * Update the address and/or login name for the specified account in the + * database. + * + * A column is updated only if its current value is null and a non-null + * value has been specified. + * + * @param osAccount OsAccount that needs to be updated in the database. + * @param uid Account ID, may be null. + * @param loginName Login name, may be null. + * + * @return OsAccountUpdateResult Account update status, and the updated + * account. + * + * @throws TskCoreException If there is a database error or if the updated + * information conflicts with an existing account. + */ + public OsAccountUpdateResult updateCoreLocalLinuxOsAccountAttributes(OsAccount osAccount, String uid, String loginName) throws TskCoreException { + CaseDbTransaction trans = db.beginTransaction(); + try { + OsAccountUpdateResult updateStatus = this.updateCoreLocalLinuxOsAccountAttributes(osAccount, uid, loginName, trans); + + trans.commit(); + trans = null; + return updateStatus; + } finally { + if (trans != null) { + trans.rollback(); + } + } + } + + /** + * Update the address and/or login name for the specified account in the + * database. + * + * A column is updated only if it's current value is null and a non-null + * value has been specified. + * + * @param osAccount OsAccount that needs to be updated in the database. + * @param uid Account ID, may be null. + * @param loginName Login name, may be null. + * + * @return OsAccountUpdateResult Account update status, and the updated + * account. + * + * @throws TskCoreException If there is a database error or if the updated + * information conflicts with an existing account. + */ + @Beta + private OsAccountUpdateResult updateCoreLocalLinuxOsAccountAttributes(OsAccount osAccount, String uid, String loginName, CaseDbTransaction trans) throws TskCoreException { + + // Update the account core data + OsAccountUpdateResult updateStatus = this.updateOsAccountCore(osAccount, uid, loginName, 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; + } + /** * Update the address and/or login name for the specified account in the * database. @@ -1786,7 +1975,32 @@ private OsAccountUpdateResult updateOsAccountCore(OsAccount osAccount, String ad String newLoginName = currAccount.getLoginName().orElse(null); String newSignature = getOsAccountSignature(newAddress, newLoginName); - updateAccountSignature(osAccount.getId(), newSignature, connection); + + try { + updateAccountSignature(osAccount.getId(), newSignature, connection); + } catch (SQLException ex) { + // There's a slight chance that we're in the case where we are trying to add an addr to an OS account + // with only a name where the addr already exists on a different OS account. This will cause a unique + // constraint failure in updateAccountSignature(). This is unlikely to happen in normal use + // since we lookup OS accounts by addr before name when we have both (i.e., it would be strange to have an + // OsAccount in hand with only the loginName set when we also know the addr). + // Correctly handling every case here is non-trivial, so for the moment only look for the specific case where + // we had an OsAccount with just an addr and and OsAccount with just a login name that we now + // want to combine. + if (osAccount.getAddr().isEmpty() && !StringUtils.isBlank(address)) { + OsAccountRealm realm = db.getOsAccountRealmManager().getRealmByRealmId(osAccount.getRealmId(), connection); + Optional<OsAccount> matchingAddrAcct = getOsAccountByAddr(address, realm.getScopeHost().get(), connection); + if (matchingAddrAcct.isEmpty() + || matchingAddrAcct.get().getId() == osAccount.getId() + || matchingAddrAcct.get().getLoginName().isPresent()) { + throw ex; // Rethrow the original error + } + + // What we should have is osAccount with just a loginName and matchingAddrAcct with + // just an address, so merge them. + mergeOsAccounts(matchingAddrAcct.get(), osAccount, trans); + } + } // get the updated account from database updatedAccount = getOsAccountByObjectId(osAccount.getId(), connection); @@ -1796,7 +2010,7 @@ private OsAccountUpdateResult updateOsAccountCore(OsAccount osAccount, String ad return new OsAccountUpdateResult(updateStatusCode, updatedAccount); - } catch (SQLException ex) { + } catch (SQLException ex) { throw new TskCoreException(String.format("Error updating account with unique id = %s, account id = %d", osAccount.getAddr().orElse("Unknown"), osAccount.getId()), ex); } } diff --git a/bindings/java/src/org/sleuthkit/datamodel/OsAccountRealmManager.java b/bindings/java/src/org/sleuthkit/datamodel/OsAccountRealmManager.java index 959eb1cc8ab130a5b314e89d694bc59f5a0341fd..2b75c410aeec7267f656f7192b18d3603a98a477 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/OsAccountRealmManager.java +++ b/bindings/java/src/org/sleuthkit/datamodel/OsAccountRealmManager.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.datamodel; +import com.google.common.annotations.Beta; import com.google.common.base.Strings; import org.apache.commons.lang3.StringUtils; import java.sql.PreparedStatement; @@ -43,6 +44,7 @@ public final class OsAccountRealmManager { private static final Logger LOGGER = Logger.getLogger(OsAccountRealmManager.class.getName()); + private static final String LOCAL_REALM_NAME = "local"; private final SleuthkitCase db; @@ -148,6 +150,52 @@ public OsAccountRealm newWindowsRealm(String accountSid, String realmName, Host return newRealm(resolvedRealmName, realmAddr, signature, scopeHost, scopeConfidence); } + /** + * Create local realm to use for Linux accounts. + * + * @param referringHost Host where realm reference is found. + * + * @return OsAccountRealm. + * + * @throws TskCoreException If there is an error + * creating the realm. + */ + @Beta + public OsAccountRealm newLocalLinuxRealm(Host referringHost) throws TskCoreException { + + if (referringHost == null) { + throw new TskCoreException("A referring host is required to create a realm."); + } + + String realmName = LOCAL_REALM_NAME; + OsAccountRealm.ScopeConfidence scopeConfidence = OsAccountRealm.ScopeConfidence.KNOWN; + String signature = makeRealmSignature("", realmName, referringHost); + + // create a realm + return newRealm(realmName, "", signature, referringHost, scopeConfidence); + } + + /** + * Get local realm to use for Linux accounts. + * + * @param referringHost Host where realm reference is found. + * + * @return OsAccountRealm. + * + * @throws TskCoreException If there is an error + * creating the realm. + */ + @Beta + public Optional<OsAccountRealm> getLocalLinuxRealm(Host referringHost) throws TskCoreException { + if (referringHost == null) { + throw new TskCoreException("A referring host is required get a realm."); + } + + try (CaseDbConnection connection = this.db.getConnection()) { + return getRealmByName(LOCAL_REALM_NAME, referringHost, connection); + } + } + /** * Get a windows realm by the account SID, or the domain name. The input SID * is an user/group account SID. The domain SID is extracted from this diff --git a/bindings/java/test/org/sleuthkit/datamodel/OsAccountTest.java b/bindings/java/test/org/sleuthkit/datamodel/OsAccountTest.java index b18826c9b745bdaad566dd51e4270b7be1dc0bcb..d086cd9feb642c8999624a156c511d976dfc36f9 100644 --- a/bindings/java/test/org/sleuthkit/datamodel/OsAccountTest.java +++ b/bindings/java/test/org/sleuthkit/datamodel/OsAccountTest.java @@ -770,6 +770,80 @@ public void basicOsAccountTests() throws TskCoreException, OsAccountManager.NotU } + @Test + public void basicLinuxOsAccountTests() throws TskCoreException { + + try { + String hostname1 = "linuxTestHost"; + Host host1 = caseDB.getHostManager().newHost(hostname1); + + // Create an account with uid and username + String uid1 = "501"; + String loginName1 = "user1"; + + OsAccount osAccount1 = caseDB.getOsAccountManager().newLocalLinuxOsAccount(uid1, loginName1, host1); + assertEquals(osAccount1.getAddr().orElse("").equalsIgnoreCase(uid1), true); + assertEquals(osAccount1.getLoginName().orElse("").equalsIgnoreCase(loginName1), true); + + Optional<OsAccount> osAccount1loadWithUID = caseDB.getOsAccountManager().getLocalLinuxOsAccount(uid1, null, host1); + assertEquals(osAccount1loadWithUID.isPresent(), true); + assertEquals(osAccount1loadWithUID.get().getAddr().orElse("").equalsIgnoreCase(uid1), true); + assertEquals(osAccount1loadWithUID.get().getLoginName().orElse("").equalsIgnoreCase(loginName1), true); + + // Create an account with only a UID then update it + String uid2 = "502"; + String loginName2 = "user2"; + + OsAccount osAccount2 = caseDB.getOsAccountManager().newLocalLinuxOsAccount(uid2, null, host1); + assertEquals(osAccount2.getAddr().orElse("").equalsIgnoreCase(uid2), true); + assertEquals(osAccount2.getLoginName().isEmpty(), true); + + OsAccountManager.OsAccountUpdateResult updateResult = caseDB.getOsAccountManager() + .updateCoreLocalLinuxOsAccountAttributes(osAccount2, uid2, loginName2); + Optional<OsAccount> oOsAccount2Updated = updateResult.getUpdatedAccount(); + + assertEquals(updateResult.getUpdateStatusCode().equals(OsAccountManager.OsAccountUpdateStatus.UPDATED), true); + assertEquals(oOsAccount2Updated.isPresent(), true); + assertEquals(oOsAccount2Updated.get().getAddr().orElse("").equalsIgnoreCase(uid2), true); + assertEquals(oOsAccount2Updated.get().getLoginName().orElse("").equalsIgnoreCase(loginName2), true); + + // Test unusual merge case (unusual because we're updating the account with the name, not the UID) + String uid3 = "503"; + String loginName3 = "user3"; + + OsAccount osAccount3uidOnly = caseDB.getOsAccountManager().newLocalLinuxOsAccount(uid3, null, host1); + OsAccount osAccount3nameOnly = caseDB.getOsAccountManager().newLocalLinuxOsAccount(null, loginName3, host1); + + updateResult = caseDB.getOsAccountManager() + .updateCoreLocalLinuxOsAccountAttributes(osAccount3nameOnly, uid3, loginName3); + Optional<OsAccount> oOsAccount3Updated = updateResult.getUpdatedAccount(); + + assertEquals(updateResult.getUpdateStatusCode().equals(OsAccountManager.OsAccountUpdateStatus.UPDATED), true); + assertEquals(oOsAccount3Updated.isPresent(), true); + assertEquals(oOsAccount3Updated.get().getAddr().orElse("").equalsIgnoreCase(uid3), true); + assertEquals(oOsAccount3Updated.get().getLoginName().orElse("").equalsIgnoreCase(loginName3), true); + + // Test normal merge case + String uid4 = "504"; + String loginName4 = "user4"; + + OsAccount osAccount4uidOnly = caseDB.getOsAccountManager().newLocalLinuxOsAccount(uid4, null, host1); + OsAccount osAccount4nameOnly = caseDB.getOsAccountManager().newLocalLinuxOsAccount(null, loginName4, host1); + + updateResult = caseDB.getOsAccountManager() + .updateCoreLocalLinuxOsAccountAttributes(osAccount4uidOnly, uid4, loginName4); + Optional<OsAccount> oOsAccount4Updated = updateResult.getUpdatedAccount(); + + assertEquals(updateResult.getUpdateStatusCode().equals(OsAccountManager.OsAccountUpdateStatus.UPDATED), true); + assertEquals(oOsAccount4Updated.isPresent(), true); + assertEquals(oOsAccount4Updated.get().getAddr().orElse("").equalsIgnoreCase(uid4), true); + assertEquals(oOsAccount4Updated.get().getLoginName().orElse("").equalsIgnoreCase(loginName4), true); + + } finally { + + } + + } @Test public void windowsSpecialAccountTests() throws TskCoreException, OsAccountManager.NotUserSIDException {