diff --git a/bindings/java/src/org/sleuthkit/datamodel/Account.java b/bindings/java/src/org/sleuthkit/datamodel/Account.java index e759d5b1a389b13221cacf4ef85c5a90c4e571ab..a5abf5908f679e2cba209a511c71b74010d17ba2 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/Account.java +++ b/bindings/java/src/org/sleuthkit/datamodel/Account.java @@ -160,4 +160,37 @@ public Account.Type getAccountType() { public long getAccountID() { return this.account_id; } + + @Override + public int hashCode() { + int hash = 5; + hash = 43 * hash + (int) (this.account_id ^ (this.account_id >>> 32)); + hash = 43 * hash + (this.accountType != null ? this.accountType.hashCode() : 0); + hash = 43 * hash + (this.typeSpecificID != null ? this.typeSpecificID.hashCode() : 0); + 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 Account other = (Account) obj; + if (this.account_id != other.account_id) { + return false; + } + if ((this.typeSpecificID == null) ? (other.typeSpecificID != null) : !this.typeSpecificID.equals(other.typeSpecificID)) { + return false; + } + if (this.accountType != other.accountType && (this.accountType == null || !this.accountType.equals(other.accountType))) { + return false; + } + return true; + } } diff --git a/bindings/java/src/org/sleuthkit/datamodel/AccountDeviceInstance.java b/bindings/java/src/org/sleuthkit/datamodel/AccountDeviceInstance.java index 226e524a5defd1d682340718badcc8135893f09e..2e16b06484214bc79af982756b3e176e130cfafe 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/AccountDeviceInstance.java +++ b/bindings/java/src/org/sleuthkit/datamodel/AccountDeviceInstance.java @@ -50,4 +50,35 @@ public Account getAccount(){ public String getDeviceId(){ return this.deviceID; } + + @Override + public int hashCode() { + int hash = 5; + hash = 11 * hash + (this.account != null ? this.account.hashCode() : 0); + hash = 11 * hash + (this.deviceID != null ? this.deviceID.hashCode() : 0); + 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 AccountDeviceInstance other = (AccountDeviceInstance) obj; + if ((this.deviceID == null) ? (other.deviceID != null) : !this.deviceID.equals(other.deviceID)) { + return false; + } + if (this.account != other.account && (this.account == null || !this.account.equals(other.account))) { + return false; + } + return true; + } + + } diff --git a/bindings/java/src/org/sleuthkit/datamodel/AccountPair.java b/bindings/java/src/org/sleuthkit/datamodel/AccountPair.java new file mode 100644 index 0000000000000000000000000000000000000000..6bae624820b1503f0367e044c24a6718bad03fb9 --- /dev/null +++ b/bindings/java/src/org/sleuthkit/datamodel/AccountPair.java @@ -0,0 +1,61 @@ +/* + * SleuthKit Java Bindings + * + * Copyright 2018 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.datamodel; + +/** + * Class representing an unordered pair of account device instances. <a,b> is + * same as <b,a> + */ +public final class AccountPair { + + private final AccountDeviceInstance account1; + private final AccountDeviceInstance account2; + + public AccountDeviceInstance getFirst() { + return account1; + } + + public AccountDeviceInstance getSecond() { + return account2; + } + + AccountPair(AccountDeviceInstance account1, AccountDeviceInstance account2) { + this.account1 = account1; + this.account2 = account2; + } + + @Override + public int hashCode() { + return account1.hashCode() + account2.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof AccountPair)) { + return false; + } + AccountPair otherPair = (AccountPair) other; + return (account1.equals(otherPair.account1) && account2.equals(otherPair.account2)) + || (account1.equals(otherPair.account2) && account2.equals(otherPair.account1)); + } + +} diff --git a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsFilter.java b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsFilter.java index 959087ea77bd5dd9b764c81f00b299dee26e9fb3..06a46dc2ef6a11b4cd7aefc791ef0c8b8ca2dfcf 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsFilter.java +++ b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsFilter.java @@ -31,7 +31,7 @@ * Defines an aggregate of filters to apply to a CommunicationsManager query. * */ -public class CommunicationsFilter { +final public class CommunicationsFilter { private final List<SubFilter> andFilters; // RAMAN TBD: figure out OR filters, I don't think we need any @@ -72,7 +72,7 @@ public void addAndFilter(SubFilter subFilter) { /** * Unit level filter. */ - public static abstract class SubFilter { + static abstract class SubFilter { /** * Returns a string description of the filter. @@ -95,7 +95,7 @@ public static abstract class SubFilter { * Filters communications by relationship type. * */ - public static class RelationshipTypeFilter extends SubFilter { + final public static class RelationshipTypeFilter extends SubFilter { private final Set<Relationship.Type> relationshipTypes; @@ -135,7 +135,7 @@ public String getSQL(CommunicationsManager commsManager) { } } - public static class DateRangeFilter extends SubFilter { + final public static class DateRangeFilter extends SubFilter { private final long startDate; private final long endDate; @@ -192,7 +192,7 @@ public String getSQL(CommunicationsManager commsManager) { * Filter communications by account type. * */ - public static class AccountTypeFilter extends SubFilter { + final public static class AccountTypeFilter extends SubFilter { private final Set<Account.Type> accountTypes; @@ -237,7 +237,7 @@ public String getSQL(CommunicationsManager commsManager) { * Filter by device ids. * */ - public static class DeviceFilter extends SubFilter { + final public static class DeviceFilter extends SubFilter { private final Set<String> deviceIds; diff --git a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java index 6ab357a18c33c75af0a4de967239f2376c4bbb02..bafd77e5f7b0944023c1d30294b7d9bc82c65dcf 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java +++ b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java @@ -1,7 +1,7 @@ /* * Sleuth Kit Data Model * - * Copyright 2017 Basis Technology Corp. + * Copyright 2017-18 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -178,8 +177,8 @@ SleuthkitCase getSleuthkitCase() { /** * Add a custom account type that is not already defined in Account.Type. - * Will not allow duplicates and will return existing type if the name is - * already defined. + * Will not allow duplicates and will return existing type if the name is + * already defined. * * @param accountTypeName account type that must be unique * @param displayName account type display name @@ -189,7 +188,7 @@ SleuthkitCase getSleuthkitCase() { * @throws TskCoreException exception thrown if a critical error occurs * within TSK core */ - // NOTE: Full name given for Type for doxygen linking + // NOTE: Full name given for Type for doxygen linking public org.sleuthkit.datamodel.Account.Type addAccountType(String accountTypeName, String displayName) throws TskCoreException { Account.Type accountType = new Account.Type(accountTypeName, displayName); @@ -260,7 +259,7 @@ public org.sleuthkit.datamodel.Account.Type addAccountType(String accountTypeNam * @throws TskCoreException exception thrown if a critical error occurs * within TSK core */ - // NOTE: Full name given for Type for doxygen linking + // NOTE: Full name given for Type for doxygen linking public AccountFileInstance createAccountFileInstance(org.sleuthkit.datamodel.Account.Type accountType, String accountUniqueID, String moduleName, Content sourceFile) throws TskCoreException { // make or get the Account (unique at the case-level) @@ -295,7 +294,7 @@ public AccountFileInstance createAccountFileInstance(org.sleuthkit.datamodel.Acc * @throws TskCoreException exception thrown if a critical error occurs * within TSK core */ - // NOTE: Full name given for Type for doxygen linking + // NOTE: Full name given for Type for doxygen linking public Account getAccount(org.sleuthkit.datamodel.Account.Type accountType, String accountUniqueID) throws TskCoreException { Account account = null; CaseDbConnection connection = db.getConnection(); @@ -338,17 +337,16 @@ public Account getAccount(org.sleuthkit.datamodel.Account.Type accountType, Stri // // return accountInstance; // } - /** - * Add one or more relationships between the sender and recipient account instances. All - * account instances must be from the same data source. + * Add one or more relationships between the sender and recipient account + * instances. All account instances must be from the same data source. * - * @param sender sender account - * @param recipients list of recipients - * @param sourceArtifact Artifact that relationships were derived from - * @param relationshipType The type of relationships to be created - * @param dateTime Date of communications/relationship, as epoch - * seconds + * @param sender sender account + * @param recipients list of recipients + * @param sourceArtifact Artifact that relationships were derived from + * @param relationshipType The type of relationships to be created + * @param dateTime Date of communications/relationship, as epoch + * seconds * * * @throws org.sleuthkit.datamodel.TskCoreException @@ -360,7 +358,7 @@ public Account getAccount(org.sleuthkit.datamodel.Account.Type accountType, Stri * relationshipType are not * compatible. */ - // NOTE: Full name given for Type for doxygen linking + // NOTE: Full name given for Type for doxygen linking public void addRelationships(AccountFileInstance sender, List<AccountFileInstance> recipients, BlackboardArtifact sourceArtifact, org.sleuthkit.datamodel.Relationship.Type relationshipType, long dateTime) throws TskCoreException, TskDataException { @@ -393,17 +391,15 @@ public void addRelationships(AccountFileInstance sender, List<AccountFileInstanc } } - Set<UnorderedAccountPair> relationships = listToUnorderedPairs(accountIDs); - Iterator<UnorderedAccountPair> iter = relationships.iterator(); - - while (iter.hasNext()) { - try { - UnorderedAccountPair accountPair = iter.next(); - addAccountsRelationship(accountPair.getFirst(), accountPair.getSecond(), - sourceArtifact, relationshipType, dateTime); - } catch (TskCoreException ex) { - // @@@ This should probably not be caught and instead we stop adding - LOGGER.log(Level.WARNING, "Error adding relationship", ex); //NON-NLS + for (int i = 0; i < accountIDs.size(); i++) { + for (int j = i + 1; j < accountIDs.size(); j++) { + try { + addAccountsRelationship(accountIDs.get(i), accountIDs.get(j), + sourceArtifact, relationshipType, dateTime); + } catch (TskCoreException ex) { + // @@@ This should probably not be caught and instead we stop adding + LOGGER.log(Level.WARNING, "Error adding relationship", ex); //NON-NLS + } } } } @@ -469,7 +465,8 @@ private Account getOrCreateAccount(Account.Type accountType, String accountUniqu * * @param accountType account type * @param accountUniqueID Unique account ID (such as email address) - * @param moduleName module name that found this instance (for the artifact) + * @param moduleName module name that found this instance (for the + * artifact) * @param sourceFile Source file (for the artifact) * * @return blackboard artifact for the instance @@ -566,12 +563,12 @@ private BlackboardArtifact getAccountFileInstanceArtifact(Account.Type accountTy * @throws TskCoreException If an error occurs accessing the case database. * */ - // NOTE: Full name given for Type for doxygen linking + // NOTE: Full name given for Type for doxygen linking public org.sleuthkit.datamodel.Account.Type getAccountType(String accountTypeName) throws TskCoreException { if (this.typeNameToAccountTypeMap.containsKey(accountTypeName)) { return this.typeNameToAccountTypeMap.get(accountTypeName); } - + CaseDbConnection connection = db.getConnection(); db.acquireSingleUserCaseReadLock(); Statement s = null; @@ -663,7 +660,7 @@ private void addAccountsRelationship(long account1_id, long account2_id, Blackbo try { String dateTimeValStr = (dateTime > 0) ? Long.toString(dateTime) : "NULL"; - + connection.beginTransaction(); s = connection.createStatement(); String query = "INTO account_relationships (account1_id, account2_id, relationship_source_obj_id, date_time, relationship_type, data_source_obj_id ) " @@ -692,8 +689,8 @@ private void addAccountsRelationship(long account1_id, long account2_id, Blackbo } /** - * Returns a list of AccountDeviceInstances that at least one relationship that meets - * the criteria listed in the filters. + * Returns a list of AccountDeviceInstances that at least one relationship + * that meets the criteria listed in the filters. * * Applicable filters: DeviceFilter, AccountTypeFilter, DateRangeFilter, * RelationshipTypeFilter @@ -798,9 +795,184 @@ public List<AccountDeviceInstance> getAccountDeviceInstancesWithRelationships(Co } /** - * Get the number of unique relationship sources (such as EMAIL artifacts) associated with - * an account on a given device (AccountDeviceInstance) that meet the filter criteria. - * + * Get the number of relationships between all pairs of accounts in the + * given set. For each pair of accounts <a2,a1> == <a1,a2>, find the number + * of relationships that pass the given filter, between those two accounts. + * + * @param accounts The set of accounts to count the relationships (pairwise) + * between. + * @param filter The filter that relationships must pass to be included in + * the count. + * + * @return The number of relationships (that pass the filter) between each + * pair of accounts, organized in a map where the key is an + * unordered pair of account ids, and the value is the number of + * relationships. + * + * @throws TskCoreException if there is a problem querying the DB. + */ + public Map<AccountPair, Long> getRelationshipCountsPairwise(Set<AccountDeviceInstance> accounts, CommunicationsFilter filter) throws TskCoreException { + + Set<Long> accountIDs = new HashSet<Long>(); + Set<String> accountDeviceIDs = new HashSet<String>(); + for (AccountDeviceInstance adi : accounts) { + accountIDs.add(adi.getAccount().getAccountID()); + accountDeviceIDs.add("'"+adi.getDeviceId()+"'"); + } + //set up applicable filters + Set<String> applicableFilters = new HashSet<String>(Arrays.asList( + CommunicationsFilter.DateRangeFilter.class.getName(), + CommunicationsFilter.DeviceFilter.class.getName(), + CommunicationsFilter.RelationshipTypeFilter.class.getName() + )); + + String accountIDsCSL = StringUtils.buildCSVString(accountIDs); + String accountDeviceIDsCSL = StringUtils.buildCSVString(accountDeviceIDs); + String filterSQL = getCommunicationsFilterSQL(filter, applicableFilters); + + final String queryString + = " SELECT count(DISTINCT relationships.relationship_source_obj_id) AS count," //realtionship count + + " data_source_info.device_id AS device_id," + //account 1 info + + " accounts1.account_id AS account1_id," + + " accounts1.account_unique_identifier AS account1_unique_identifier," + + " account_types1.type_name AS type_name1," + + " account_types1.display_name AS display_name1," + //account 2 info + + " accounts2.account_id AS account2_id," + + " accounts2.account_unique_identifier AS account2_unique_identifier," + + " account_types2.type_name AS type_name2," + + " account_types2.display_name AS display_name2" + + " FROM account_relationships AS relationships" + + " JOIN data_source_info AS data_source_info" + + " ON relationships.data_source_obj_id = data_source_info.obj_id " + //account1 aliases + + " JOIN accounts AS accounts1 " + + " ON accounts1.account_id = relationships.account1_id" + + " JOIN account_types AS account_types1" + + " ON accounts1.account_type_id = account_types1.account_type_id" + //account2 aliases + + " JOIN accounts AS accounts2 " + + " ON accounts2.account_id = relationships.account2_id" + + " JOIN account_types AS account_types2" + + " ON accounts2.account_type_id = account_types2.account_type_id" + + " WHERE (( relationships.account1_id IN (" + accountIDsCSL + ")) " + + " AND ( relationships.account2_id IN ( " + accountIDsCSL + " ))" + + " AND ( data_source_info.device_id IN (" + accountDeviceIDsCSL + "))) " + + (filterSQL.isEmpty() ? "" : " AND " + filterSQL) + + " GROUP BY data_source_info.device_id, " + + " accounts1.account_id, " + + " account_types1.type_name, " + + " account_types1.display_name, " + + " accounts2.account_id, " + + " account_types2.type_name, " + + " account_types2.display_name"; + CaseDbConnection connection = db.getConnection(); + db.acquireSingleUserCaseReadLock(); + Statement s = null; + ResultSet rs = null; + + Map<AccountPair, Long> results = new HashMap<AccountPair, Long>(); + + try { + s = connection.createStatement(); + rs = connection.executeQuery(s, queryString); //NON-NLS + + while (rs.next()) { + //make account 1 + Account.Type type1 = new Account.Type(rs.getString("type_name1"), rs.getString("display_name1")); + AccountDeviceInstance adi1 = new AccountDeviceInstance(new Account(rs.getLong("account1_id"), type1, + rs.getString("account1_unique_identifier")), + rs.getString("device_id")); + + //make account 2 + Account.Type type2 = new Account.Type(rs.getString("type_name2"), rs.getString("display_name2")); + AccountDeviceInstance adi2 = new AccountDeviceInstance(new Account(rs.getLong("account2_id"), type2, + rs.getString("account2_unique_identifier")), + rs.getString("device_id")); + + AccountPair relationshipKey = new AccountPair(adi1, adi2); + long count = rs.getLong("count"); + + //merge counts for relationships that have the accounts flipped. + Long oldCount = results.get(relationshipKey); + if (oldCount != null) { + count += oldCount; + } + results.put(relationshipKey, count); + } + return results; + } catch (SQLException ex) { + throw new TskCoreException("Error getting relationships between accounts. " + ex.getMessage(), ex); + } finally { + closeResultSet(rs); + closeStatement(s); + connection.close(); + db.releaseSingleUserCaseReadLock(); + } + } + + /** + * Get the number of relationship sources for the relationships between + * account1 and account2, that pass the given filter. + * + * @param account1 The first account. + * @param account2 The second account. + * @param filter The filter that relationships must pass to be counted. + * + * @return The number of relationship sources for the relationships between + * account1 and account2, that pass the given filter. + * + * @throws TskCoreException + */ + public long getRelationshipSourcesCount(AccountDeviceInstance account1, AccountDeviceInstance account2, CommunicationsFilter filter) throws TskCoreException { + + //set up applicable filters + Set<String> applicableFilters = new HashSet<String>(Arrays.asList( + CommunicationsFilter.DateRangeFilter.class.getName(), + CommunicationsFilter.DeviceFilter.class.getName(), + CommunicationsFilter.RelationshipTypeFilter.class.getName() + )); + String filterSQL = getCommunicationsFilterSQL(filter, applicableFilters); + final String queryString = "SELECT count(distinct artifact_id) as count " + + " FROM blackboard_artifacts AS artifacts" + + " JOIN account_relationships AS relationships" + + " ON artifacts.artifact_obj_id = relationships.relationship_source_obj_id" + + " WHERE (( relationships.account1_id = " + account1.getAccount().getAccountID() + + " AND relationships.account2_id = " + account2.getAccount().getAccountID() + + " ) OR ( relationships.account2_id = " + account1.getAccount().getAccountID() + + " AND relationships.account1_id =" + account2.getAccount().getAccountID() + " ))" + + (filterSQL.isEmpty() ? "" : " AND " + filterSQL) + + " "; + CaseDbConnection connection = db.getConnection(); + db.acquireSingleUserCaseReadLock(); + Statement s = null; + ResultSet rs = null; + + try { + s = connection.createStatement(); + rs = connection.executeQuery(s, queryString); //NON-NLS + + while (rs.next()) { + return rs.getLong("count"); + } + + } catch (SQLException ex) { + throw new TskCoreException("Error getting relationships between accounts. " + ex.getMessage(), ex); + } finally { + closeResultSet(rs); + closeStatement(s); + connection.close(); + db.releaseSingleUserCaseReadLock(); + } + return 0; + } + + /** + * Get the number of unique relationship sources (such as EMAIL artifacts) + * associated with an account on a given device (AccountDeviceInstance) that + * meet the filter criteria. + * * Applicable filters: RelationshipTypeFilter, DateRangeFilter * * @param accountDeviceInstance Account of interest @@ -856,8 +1028,9 @@ public long getRelationshipSourcesCount(AccountDeviceInstance accountDeviceInsta } /** - * Get the unique relationship sources (such as EMAIL artifacts) associated with an - * account on a given device (AccountDeviceInstance) that meet the filter criteria. + * Get the unique relationship sources (such as EMAIL artifacts) associated + * with an account on a given device (AccountDeviceInstance) that meet the + * filter criteria. * * Applicable filters: RelationshipTypeFilter, DateRangeFilter * @@ -865,7 +1038,7 @@ public long getRelationshipSourcesCount(AccountDeviceInstance accountDeviceInsta * which to get the relationship sources. * @param filter Filters to apply. * - * @return number of relationship sources found for given account(s). + * @return relationship sources found for given account(s). * * @throws org.sleuthkit.datamodel.TskCoreException */ @@ -903,8 +1076,10 @@ public Set<Content> getRelationshipSources(Set<AccountDeviceInstance> accountDev // set up applicable filters Set<String> applicableFilters = new HashSet<String>(Arrays.asList( - CommunicationsFilter.RelationshipTypeFilter.class.getName(), - CommunicationsFilter.DateRangeFilter.class.getName() + CommunicationsFilter.RelationshipTypeFilter.class + .getName(), + CommunicationsFilter.DateRangeFilter.class + .getName() )); String filterSQL = getCommunicationsFilterSQL(filter, applicableFilters); @@ -952,6 +1127,185 @@ public Set<Content> getRelationshipSources(Set<AccountDeviceInstance> accountDev } } + /** + * Get a set of AccountDeviceInstances that have relationships with the + * given AccountDeviceInstance and meet the criteria of the given filter. + * + * @param accountDeviceInstance The account device instance. + * @param filter The filters to apply. + * + * @return A set of AccountDeviceInstances that have relationships with the + * given AccountDeviceInstance and meet the criteria of the given + * filter. + * + * @throws TskCoreException if there is a serious error executing he query. + */ + public List<AccountDeviceInstance> getRelatedAccountDeviceInstances(AccountDeviceInstance accountDeviceInstance, CommunicationsFilter filter) throws TskCoreException { + final List<Long> dataSourceObjIds + = getSleuthkitCase().getDataSourceObjIds(accountDeviceInstance.getDeviceId()); + + //set up applicable filters + Set<String> applicableInnerQueryFilters = new HashSet<String>(Arrays.asList( + CommunicationsFilter.DateRangeFilter.class + .getName(), + CommunicationsFilter.DeviceFilter.class + .getName(), + CommunicationsFilter.RelationshipTypeFilter.class + .getName() + )); + + String innerQueryfilterSQL = getCommunicationsFilterSQL(filter, applicableInnerQueryFilters); + + String innerQueryTemplate + = " SELECT %1$1s as account_id," + + " data_source_obj_id" + + " FROM account_relationships as relationships" + + " WHERE %2$1s = " + accountDeviceInstance.getAccount().getAccountID() + "" + + " AND data_source_obj_id IN (" + StringUtils.buildCSVString(dataSourceObjIds) + ")" + + (innerQueryfilterSQL.isEmpty() ? "" : " AND " + innerQueryfilterSQL); + + String innerQuery1 = String.format(innerQueryTemplate, "account1_id", "account2_id"); + String innerQuery2 = String.format(innerQueryTemplate, "account2_id", "account1_id"); + + //this query groups by account_id and data_source_obj_id across both innerQueries + String combinedInnerQuery + = "SELECT account_id, data_source_obj_id " + + " FROM ( " + innerQuery1 + " UNION " + innerQuery2 + " ) AS inner_union" + + " GROUP BY account_id, data_source_obj_id"; + + // set up applicable filters + Set<String> applicableFilters = new HashSet<String>(Arrays.asList( + CommunicationsFilter.AccountTypeFilter.class + .getName() + )); + + String filterSQL = getCommunicationsFilterSQL(filter, applicableFilters); + + String queryStr + = //account info + " accounts.account_id AS account_id," + + " accounts.account_unique_identifier AS account_unique_identifier," + //account type info + + " account_types.type_name AS type_name," + //Account device instance info + + " data_source_info.device_id AS device_id" + + " FROM ( " + combinedInnerQuery + " ) AS account_device_instances" + + " JOIN accounts AS accounts" + + " ON accounts.account_id = account_device_instances.account_id" + + " JOIN account_types AS account_types" + + " ON accounts.account_type_id = account_types.account_type_id" + + " JOIN data_source_info AS data_source_info" + + " ON account_device_instances.data_source_obj_id = data_source_info.obj_id" + + (filterSQL.isEmpty() ? "" : " WHERE " + filterSQL); + + switch (db.getDatabaseType()) { + case POSTGRESQL: + queryStr = "SELECT DISTINCT ON ( accounts.account_id, data_source_info.device_id) " + queryStr; + break; + case SQLITE: + queryStr = "SELECT " + queryStr + " GROUP BY accounts.account_id, data_source_info.device_id"; + break; + default: + throw new TskCoreException("Unknown DB Type: " + db.getDatabaseType().name()); + } + + CaseDbConnection connection = db.getConnection(); + db.acquireSingleUserCaseReadLock(); + Statement s = null; + ResultSet rs = null; + + try { + s = connection.createStatement(); + + rs = connection.executeQuery(s, queryStr); //NON-NLS + ArrayList<AccountDeviceInstance> accountDeviceInstances = new ArrayList<AccountDeviceInstance>(); + while (rs.next()) { + long account_id = rs.getLong("account_id"); + String deviceID = rs.getString("device_id"); + final String type_name = rs.getString("type_name"); + final String account_unique_identifier = rs.getString("account_unique_identifier"); + + Account.Type accountType = typeNameToAccountTypeMap.get(type_name); + Account account = new Account(account_id, accountType, account_unique_identifier); + accountDeviceInstances.add(new AccountDeviceInstance(account, deviceID)); + } + + return accountDeviceInstances; + } catch (SQLException ex) { + throw new TskCoreException("Error getting account device instances. " + ex.getMessage(), ex); + } finally { + closeResultSet(rs); + closeStatement(s); + connection.close(); + db.releaseSingleUserCaseReadLock(); + } + } + + /** + * Get the relationship sources of relationships between the given account + * device instances. + * + * Applicable filters: RelationshipTypeFilter, DateRangeFilter + * + * @param account1 First AccountDeviceInstance + * @param account2 Second AccountDeviceInstance + * @param filter Filters to apply. + * + * @return relationship sources for relationships between account1 and + * account2. + * + * @throws org.sleuthkit.datamodel.TskCoreException + */ + public List<Content> getRelationshipSources(AccountDeviceInstance account1, AccountDeviceInstance account2, CommunicationsFilter filter) throws TskCoreException { + + //set up applicable filters + Set<String> applicableFilters = new HashSet<String>(Arrays.asList( + CommunicationsFilter.DateRangeFilter.class.getName(), + CommunicationsFilter.DeviceFilter.class.getName(), + CommunicationsFilter.RelationshipTypeFilter.class.getName() + )); + String filterSQL = getCommunicationsFilterSQL(filter, applicableFilters); + final String queryString = "SELECT artifacts.artifact_id AS artifact_id," + + " artifacts.obj_id AS obj_id," + + " artifacts.artifact_obj_id AS artifact_obj_id," + + " artifacts.data_source_obj_id AS data_source_obj_id," + + " artifacts.artifact_type_id AS artifact_type_id," + + " artifacts.review_status_id AS review_status_id" + + " FROM blackboard_artifacts AS artifacts" + + " JOIN account_relationships AS relationships" + + " ON artifacts.artifact_obj_id = relationships.relationship_source_obj_id" + + " WHERE (( relationships.account1_id = " + account1.getAccount().getAccountID() + + " AND relationships.account2_id = " + account2.getAccount().getAccountID() + + " ) OR ( relationships.account2_id = " + account1.getAccount().getAccountID() + + " AND relationships.account1_id =" + account2.getAccount().getAccountID() + " ))" + + (filterSQL.isEmpty() ? "" : " AND " + filterSQL); + CaseDbConnection connection = db.getConnection(); + db.acquireSingleUserCaseReadLock(); + Statement s = null; + ResultSet rs = null; + try { + s = connection.createStatement(); + rs = connection.executeQuery(s, queryString); //NON-NLS + + ArrayList<Content> artifacts = new ArrayList<Content>(); + while (rs.next()) { + BlackboardArtifact.Type bbartType = db.getArtifactType(rs.getInt("artifact_type_id")); + artifacts.add(new BlackboardArtifact(db, rs.getLong("artifact_id"), rs.getLong("obj_id"), rs.getLong("artifact_obj_id"), rs.getLong("data_source_obj_id"), + bbartType.getTypeID(), bbartType.getTypeName(), bbartType.getDisplayName(), + BlackboardArtifact.ReviewStatus.withID(rs.getInt("review_status_id")))); + } + + return artifacts; + } catch (SQLException ex) { + throw new TskCoreException("Error getting relationships between accounts. " + ex.getMessage(), ex); + } finally { + closeResultSet(rs); + closeStatement(s); + connection.close(); + db.releaseSingleUserCaseReadLock(); + } + } + /** * Get account_type_if for the given account type. * @@ -974,18 +1328,6 @@ int getAccountTypeId(Account.Type accountType) { * * @return Set<UnorderedPair<Long>> */ - private Set<UnorderedAccountPair> listToUnorderedPairs(List<Long> account_ids) { - Set<UnorderedAccountPair> relationships = new HashSet<UnorderedAccountPair>(); - - for (int i = 0; i < account_ids.size(); i++) { - for (int j = i + 1; j < account_ids.size(); j++) { - relationships.add(new UnorderedAccountPair(account_ids.get(i), account_ids.get(j))); - } - } - - return relationships; - } - private String normalizeAccountID(Account.Type accountType, String accountUniqueID) { String normailzeAccountID = accountUniqueID; @@ -1056,49 +1398,9 @@ private String getCommunicationsFilterSQL(CommunicationsFilter commFilter, Set<S sqlStr = "( " + sqlSB.toString() + " )"; } return sqlStr; - } - - /** - * Class representing an unordered pair of account ids. <a,b> is same as - * <b,a> - */ - private final class UnorderedAccountPair { - - private final long account1_id; - private final long account2_id; - - UnorderedAccountPair(long account1_id, long account2_id) { - this.account1_id = account1_id; - this.account2_id = account2_id; - } - @Override - public int hashCode() { - return new Long(account1_id).hashCode() + new Long(account2_id).hashCode(); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - if (!(other instanceof UnorderedAccountPair)) { - return false; - } - - UnorderedAccountPair otherPair = (UnorderedAccountPair) other; - return ((account1_id == otherPair.account1_id && account2_id == otherPair.account2_id) - || (account1_id == otherPair.account2_id && account2_id == otherPair.account1_id)); - } - - public long getFirst() { - return account1_id; - } - - public long getSecond() { - return account2_id; - } } +} // /** // * Get all account instances of a given type @@ -1491,54 +1793,6 @@ public long getSecond() { // } // } // /** -// * Returns relationships between two accounts -// * -// * @param account1_id account_id for account1 -// * @param account2_id account_id for account2 -// * -// * @throws TskCoreException exception thrown if a critical error occurs -// * within TSK core -// */ -// public List<BlackboardArtifact> getRelationships(long account1_id, long account2_id) throws TskCoreException { -// CaseDbConnection connection = db.getConnection(); -// db.acquireSingleUserCaseReadLock(); -// Statement s = null; -// ResultSet rs = null; -// -// try { -// s = connection.createStatement(); -// rs = connection.executeQuery(s, "SELECT artifacts.artifact_id AS artifact_id," -// + " artifacts.obj_id AS obj_id," -// + " artifacts.artifact_obj_id AS artifact_obj_id," -// + " artifacts.data_source_obj_id AS data_source_obj_id," -// + " artifacts.artifact_type_id AS artifact_type_id," -// + " artifacts.review_status_id AS review_status_id" -// + " FROM blackboard_artifacts AS artifacts" -// + " JOIN relationships AS relationships" -// + " ON artifacts.artifact_obj_id = relationships.relationship_source_obj_id" -// + " WHERE relationships.account1_id IN ( " + account1_id + ", " + account2_id + " )" -// + " AND relationships.account2_id IN ( " + account1_id + ", " + account2_id + " )" -// ); //NON-NLS -// -// ArrayList<BlackboardArtifact> artifacts = new ArrayList<BlackboardArtifact>(); -// while (rs.next()) { -// BlackboardArtifact.Type bbartType = db.getArtifactType(rs.getInt("artifact_type_id")); -// artifacts.add(new BlackboardArtifact(db, rs.getLong("artifact_id"), rs.getLong("obj_id"), rs.getLong("artifact_obj_id"), rs.getLong("data_source_obj_id"), -// bbartType.getTypeID(), bbartType.getTypeName(), bbartType.getDisplayName(), -// BlackboardArtifact.ReviewStatus.withID(rs.getInt("review_status_id")))); -// } -// -// return artifacts; -// } catch (SQLException ex) { -// throw new TskCoreException("Error getting relationships bteween accounts. " + ex.getMessage(), ex); -// } finally { -// closeResultSet(rs); -// closeStatement(s); -// connection.close(); -// db.releaseSingleUserCaseReadLock(); -// } -// } -// /** // * Returns relationships, of given type, between two accounts // * // * @param account1_id account1 artifact ID @@ -1661,4 +1915,3 @@ public long getSecond() { // db.releaseSingleUserCaseWriteLock(); // } // } -} diff --git a/bindings/java/src/org/sleuthkit/datamodel/Relationship.java b/bindings/java/src/org/sleuthkit/datamodel/Relationship.java index 68360ffbb24c9c245d994bbb927912d2d9507418..17e9cc6a7e97994e0d069e9008f5ab7d5b0383be 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/Relationship.java +++ b/bindings/java/src/org/sleuthkit/datamodel/Relationship.java @@ -1,7 +1,7 @@ /* * SleuthKit Java Bindings * - * Copyright 2017 Basis Technology Corp. + * Copyright 2017-18 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,6 +36,7 @@ */ public class Relationship { + public static final class Type { private final String displayName; diff --git a/bindings/java/test/org/sleuthkit/datamodel/CommunicationsManagerTest.java b/bindings/java/test/org/sleuthkit/datamodel/CommunicationsManagerTest.java index 4fc41b5df43f8b479b1b92f40c9fde2c4ed8165c..d2e025b95d3fd6a6b0cc8baf5afe6cf0874934b2 100644 --- a/bindings/java/test/org/sleuthkit/datamodel/CommunicationsManagerTest.java +++ b/bindings/java/test/org/sleuthkit/datamodel/CommunicationsManagerTest.java @@ -25,6 +25,7 @@ import static java.util.Collections.singleton; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -226,6 +227,10 @@ public class CommunicationsManagerTest { private static HashSet<AccountDeviceInstance> EMAILS_ABC_DS1; private static HashSet<AccountDeviceInstance> PHONES_2345_DS1; private static HashSet<AccountDeviceInstance> PHONES_12345_DS2; + private static AccountFileInstance deviceAccount_1; + private static AccountFileInstance deviceAccount_2; + private static AccountDeviceInstance ds1DeviceAccount; + private static AccountDeviceInstance ds2DeviceAccount; public CommunicationsManagerTest() { } @@ -257,11 +262,12 @@ public static void setUpClass() { // Create some commmunication artiacts from DS1 { - VirtualDirectory rootDirectory_1 = dataSource_1.getRootDirectory(); + VirtualDirectory rootDirectory_1 = dataSource_1; AbstractFile sourceContent_1 = rootDirectory_1; // Let the root dorectory be the source for all artifacts // Create a Device account for Device1 - AccountFileInstance deviceAccount_1 = caseDB.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE, DS1_DEVICEID, MODULE_NAME, rootDirectory_1); + deviceAccount_1 = caseDB.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE, DS1_DEVICEID, MODULE_NAME, rootDirectory_1); + ds1DeviceAccount = new AccountDeviceInstance(deviceAccount_1.getAccount(), DS1_DEVICEID); // Create some email message artifacts BlackboardArtifact emailMsg = addEmailMsgArtifact(EMAIL_A, EMAIL_B, "", "", @@ -305,11 +311,12 @@ public static void setUpClass() { // Create some commmunication artiacts from DS2 { - VirtualDirectory rootDirectory_2 = dataSource_2.getRootDirectory(); + VirtualDirectory rootDirectory_2 = dataSource_2; AbstractFile sourceContent_2 = rootDirectory_2; // Let the root directory be the source for all artifacts // Create a Device accocunt for Device1 - AccountFileInstance deviceAccount_2 = caseDB.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE, DS2_DEVICEID, MODULE_NAME, sourceContent_2); + deviceAccount_2 = caseDB.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE, DS2_DEVICEID, MODULE_NAME, sourceContent_2); + ds2DeviceAccount = new AccountDeviceInstance(deviceAccount_2.getAccount(), DS2_DEVICEID); // Add some Call logs addCalllogArtifact(deviceAccount_2, NAME_1, PHONENUM_1, 1483272732, 100, "Outgoing", sourceContent_2); @@ -492,6 +499,105 @@ public void accountTypeFilterTests() throws TskCoreException { } } + @Test + public void getRelationshipsTests() throws TskCoreException { + + System.out.println("CommsMgr API - getRelationships With Filters tests"); + + // Test DS1: EMAIL A <-> EMAIL B, no filters + { + + List<Content> accountDeviceInstances + = commsMgr.getRelationshipSources(EMAIL_A_DS1, EMAIL_B_DS1, null); + assertEquals(2, accountDeviceInstances.size()); + } + + // Test DS1: EMAIL A <-> EMAIL c, no filters + { + + List<Content> accountDeviceInstances + = commsMgr.getRelationshipSources(EMAIL_A_DS1, EMAIL_C_DS1, null); + assertEquals(2, accountDeviceInstances.size()); + } + + // Test DS1: EMAIL B <-> EMAIL C, no filters + { + + List<Content> accountDeviceInstances + = commsMgr.getRelationshipSources(EMAIL_B_DS1, EMAIL_C_DS1, null); + assertEquals(1, accountDeviceInstances.size()); + } + + // Test DS1: EMAIL B <-> EMAIL C, contacts + { + CommunicationsFilter communicationsFilter = new CommunicationsFilter(Arrays.asList( + new RelationshipTypeFilter(singleton(Relationship.Type.CONTACT)))); + List<Content> accountDeviceInstances + = commsMgr.getRelationshipSources(EMAIL_B_DS1, EMAIL_C_DS1, communicationsFilter); + assertEquals(0, accountDeviceInstances.size()); + } + } + + @Test + public void getRelatedAccountDeviceInstancesWithFilterTests() throws TskCoreException { + + System.out.println("CommsMgr API - getRelatedAccountDeviceInstances With Filters tests"); + + // Test DS1: EMAIL A, no filters + { + List<AccountDeviceInstance> accountDeviceInstances + = commsMgr.getRelatedAccountDeviceInstances(EMAIL_A_DS1, null); + assertEquals(2, accountDeviceInstances.size()); + + } + + // Test DS1: EMAIL A, filter on device and email accounts + { + CommunicationsFilter commsFilter = new CommunicationsFilter(Arrays.asList( + new DeviceFilter(singleton(DS1_DEVICEID)), + new AccountTypeFilter(singleton(EMAIL)) + )); + List<AccountDeviceInstance> accountDeviceInstances + = commsMgr.getRelatedAccountDeviceInstances(EMAIL_A_DS1, commsFilter); + assertEquals(2, accountDeviceInstances.size()); + + } + + // Test DS1: EMAIL A, filter - DS2 & EMAIL + { + CommunicationsFilter commsFilter = new CommunicationsFilter(Arrays.asList( + new DeviceFilter(singleton(DS2_DEVICEID)), + new AccountTypeFilter(singleton(EMAIL)) + )); + List<AccountDeviceInstance> accountDeviceInstances + = commsMgr.getRelatedAccountDeviceInstances(EMAIL_A_DS1, commsFilter); + assertEquals(0, accountDeviceInstances.size()); + + } + + // Test DS1: Phone 2 , call logs + { + CommunicationsFilter commsFilter = new CommunicationsFilter(Arrays.asList( + new RelationshipTypeFilter(Arrays.asList(CALL_LOG)) + )); + List<AccountDeviceInstance> accountDeviceInstances + = commsMgr.getRelatedAccountDeviceInstances(PHONE_2_DS1, commsFilter); + System.out.println(accountDeviceInstances); + assertEquals(1, accountDeviceInstances.size()); + } + + // Test DS2: Phone 1 , msgs + call logs + { + CommunicationsFilter commsFilter = new CommunicationsFilter(Arrays.asList( + new RelationshipTypeFilter(Arrays.asList(CALL_LOG, MESSAGE)) + )); + List<AccountDeviceInstance> accountDeviceInstances + = commsMgr.getRelatedAccountDeviceInstances(PHONE_2_DS2, commsFilter); + System.out.println(accountDeviceInstances); + assertEquals(1, accountDeviceInstances.size()); + } + } + @Test public void getAccountDeviceInstanceWithFilterTests() throws TskCoreException { @@ -1172,6 +1278,70 @@ public void communicationsWithFilterTests() throws TskCoreException { } } + @Test + public void getRelationshipCountsPairwiseTests() throws TskCoreException { + System.out.println("CommsMgr API - Get RelationshipCountsBetween test"); + + // Counts for DS1 and PHONES 2,3,4,5, no Filter + { + final Set<AccountDeviceInstance> accounts = new HashSet<AccountDeviceInstance>(); + accounts.add(ds1DeviceAccount); + accounts.addAll(PHONES_2345_DS1); + + Map<AccountPair, Long> counts = commsMgr.getRelationshipCountsPairwise(accounts, null); + + assertEquals(3, counts.size()); + assertEquals(Long.valueOf(5), counts.get(new AccountPair(ds1DeviceAccount, PHONE_2_DS1))); + assertEquals(Long.valueOf(2), counts.get(new AccountPair(ds1DeviceAccount, PHONE_3_DS1))); + assertEquals(Long.valueOf(1), counts.get(new AccountPair(ds1DeviceAccount, PHONE_5_DS1))); + assertNull(counts.get(new AccountPair(ds1DeviceAccount, PHONE_4_DS1))); + + } + + // Counts for DS1 Email A and B and PHONES 2,3,4,5, no Filter + { + final Set<AccountDeviceInstance> accounts = new HashSet<AccountDeviceInstance>(); + accounts.add(ds1DeviceAccount); + accounts.add(EMAIL_A_DS1); + accounts.add(EMAIL_B_DS1); + accounts.addAll(PHONES_2345_DS1); + + Map<AccountPair, Long> counts + = commsMgr.getRelationshipCountsPairwise(accounts, null); + assertEquals(4, counts.size()); + assertEquals(Long.valueOf(5), counts.get(new AccountPair(ds1DeviceAccount, PHONE_2_DS1))); + assertEquals(Long.valueOf(2), counts.get(new AccountPair(ds1DeviceAccount, PHONE_3_DS1))); + assertEquals(Long.valueOf(1), counts.get(new AccountPair(ds1DeviceAccount, PHONE_5_DS1))); + assertEquals(Long.valueOf(2), counts.get(new AccountPair(EMAIL_A_DS1, EMAIL_B_DS1))); + assertNull(counts.get(new AccountPair(ds1DeviceAccount, PHONE_4_DS1))); + } + + // Counts for DS1, DS2, PHONES 1, 2,3,4,5; Filter on DS2 + { + final Set<AccountDeviceInstance> accounts = new HashSet<AccountDeviceInstance>(); + accounts.add(ds1DeviceAccount); + accounts.add(ds2DeviceAccount); + accounts.addAll(PHONES_12345_DS2); + + CommunicationsFilter commsFilter = new CommunicationsFilter(Arrays.asList( + new DeviceFilter(singleton(DS2_DEVICEID)) + )); + Map<AccountPair, Long> counts + = commsMgr.getRelationshipCountsPairwise(accounts, commsFilter); + assertEquals(4, counts.size()); + assertNull(counts.get(new AccountPair(ds1DeviceAccount, PHONE_2_DS1))); + assertNull(counts.get(new AccountPair(ds1DeviceAccount, PHONE_3_DS1))); + assertNull(counts.get(new AccountPair(ds1DeviceAccount, PHONE_5_DS1))); + assertNull(counts.get(new AccountPair(EMAIL_A_DS1, EMAIL_B_DS1))); + assertNull(counts.get(new AccountPair(ds1DeviceAccount, PHONE_4_DS1))); + + assertEquals(Long.valueOf(1), counts.get(new AccountPair(ds2DeviceAccount, PHONE_1_DS2))); + assertEquals(Long.valueOf(4), counts.get(new AccountPair(ds2DeviceAccount, PHONE_2_DS2))); + assertEquals(Long.valueOf(1), counts.get(new AccountPair(ds2DeviceAccount, PHONE_3_DS2))); + assertEquals(Long.valueOf(3), counts.get(new AccountPair(ds2DeviceAccount, PHONE_4_DS2))); + } + } + /* * Adds an Email msg artifact. Also creates Email AccountInstances, if