From d7932a066a43cffcd7cda23c926a1635537ab1a9 Mon Sep 17 00:00:00 2001
From: apriestman <apriestman@basistech.com>
Date: Fri, 29 Jan 2021 11:15:10 -0500
Subject: [PATCH] Add support for storing hosts in the data_source_info table.
 Added new methods to HostManager.

---
 bindings/java/jni/auto_db_java.cpp            |   8 +-
 bindings/java/jni/auto_db_java.h              |   3 +
 bindings/java/jni/dataModel_SleuthkitJNI.cpp  |  11 +-
 bindings/java/jni/dataModel_SleuthkitJNI.h    |   8 +-
 .../datamodel/CaseDatabaseFactory.java        |   2 +
 .../org/sleuthkit/datamodel/DataSource.java   |   9 ++
 .../org/sleuthkit/datamodel/HostManager.java  |  78 ++++++++++++
 .../src/org/sleuthkit/datamodel/Image.java    |  18 +++
 .../datamodel/LocalFilesDataSource.java       |  18 +++
 .../sleuthkit/datamodel/SleuthkitCase.java    | 116 +++++++++++++++---
 .../org/sleuthkit/datamodel/SleuthkitJNI.java |  89 ++++++++++++--
 .../sleuthkit/datamodel/TskCaseDbBridge.java  |  12 +-
 12 files changed, 337 insertions(+), 35 deletions(-)

diff --git a/bindings/java/jni/auto_db_java.cpp b/bindings/java/jni/auto_db_java.cpp
index 7cc3888cf..68c69a3c2 100644
--- a/bindings/java/jni/auto_db_java.cpp
+++ b/bindings/java/jni/auto_db_java.cpp
@@ -79,7 +79,7 @@ TskAutoDbJava::initializeJni(JNIEnv * jniEnv, jobject jobj) {
     }
     m_callbackClass = (jclass)m_jniEnv->NewGlobalRef(localCallbackClass);
 
-    m_addImageMethodID = m_jniEnv->GetMethodID(m_callbackClass, "addImageInfo", "(IJLjava/lang/String;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)J");
+    m_addImageMethodID = m_jniEnv->GetMethodID(m_callbackClass, "addImageInfo", "(IJLjava/lang/String;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J[Ljava/lang/String;)J");
     if (m_addImageMethodID == NULL) {
         return TSK_ERR;
     }
@@ -216,7 +216,7 @@ TskAutoDbJava::addImageInfo(int type, TSK_OFF_T ssize, int64_t & objId, const st
     }
 
     jlong objIdj = m_jniEnv->CallLongMethod(m_javaDbObj, m_addImageMethodID,
-        type, ssize, tzj, size, md5j, sha1j, sha256j, devIdj, collj, imgNamesj);
+        type, ssize, tzj, size, md5j, sha1j, sha256j, devIdj, collj, m_hostId, imgNamesj);
     objId = (int64_t)objIdj;
 
     if (objId < 0) {
@@ -936,6 +936,10 @@ int64_t TskAutoDbJava::getImageID() {
     return m_curImgId;
 }
 
+void TskAutoDbJava::setHostId(long hostId) {
+    m_hostId = hostId;
+}
+
 void TskAutoDbJava::closeImage() {
     TskAuto::closeImage();
 }
diff --git a/bindings/java/jni/auto_db_java.h b/bindings/java/jni/auto_db_java.h
index b324a71c4..ae11e6a58 100644
--- a/bindings/java/jni/auto_db_java.h
+++ b/bindings/java/jni/auto_db_java.h
@@ -111,6 +111,8 @@ class TskAutoDbJava :public TskAuto {
 
     int64_t getImageID();
 
+    void setHostId(long hostId);
+
     TSK_RETVAL_ENUM initializeJni(JNIEnv *, jobject);
 
   private:
@@ -137,6 +139,7 @@ class TskAutoDbJava :public TskAuto {
     int64_t m_maxChunkSize; ///< Max number of unalloc bytes to process before writing to the database, even if there is no natural break. -1 for no chunking
     bool m_foundStructure;  ///< Set to true when we find either a volume or file system
     bool m_attributeAdded; ///< Set to true when an attribute was added by processAttributes
+    int64_t m_hostId;      ///< ID of host for this image (already in database)
 
     // These are used to write unallocated blocks for pools at the end of the add image
     // process. We can't load the pool_info objects directly from the database so we will
diff --git a/bindings/java/jni/dataModel_SleuthkitJNI.cpp b/bindings/java/jni/dataModel_SleuthkitJNI.cpp
index f4da3b4b6..0688cae46 100644
--- a/bindings/java/jni/dataModel_SleuthkitJNI.cpp
+++ b/bindings/java/jni/dataModel_SleuthkitJNI.cpp
@@ -805,13 +805,14 @@ JNIEXPORT jobject JNICALL Java_org_sleuthkit_datamodel_SleuthkitJNI_hashDbLookup
  * @param timeZone The time zone for the image.
  * @param addUnallocSpace Pass true to create virtual files for unallocated space. Ignored if addFileSystems is false.
  * @param skipFatFsOrphans Pass true to skip processing of orphan files for FAT file systems. Ignored if addFileSystems is false.
+ * @param hostId Id of the host (already in the database).
  *
  * @return A pointer to the process (TskAutoDbJava object) or NULL on error.
  */
 JNIEXPORT jlong JNICALL
     Java_org_sleuthkit_datamodel_SleuthkitJNI_initAddImgNat(JNIEnv * env,
-    jclass obj, jobject callbackObj, jstring timeZone, jboolean addUnallocSpace, jboolean skipFatFsOrphans) {
-    return Java_org_sleuthkit_datamodel_SleuthkitJNI_initializeAddImgNat(env, obj, callbackObj, timeZone, true, addUnallocSpace, skipFatFsOrphans);
+    jclass obj, jobject callbackObj, jstring timeZone, jboolean addUnallocSpace, jboolean skipFatFsOrphans, jlong hostId) {
+    return Java_org_sleuthkit_datamodel_SleuthkitJNI_initializeAddImgNat(env, obj, callbackObj, timeZone, true, addUnallocSpace, skipFatFsOrphans, hostId);
 }
 
 /*
@@ -823,12 +824,13 @@ JNIEXPORT jlong JNICALL
  * @param addFileSystems Pass true to attempt to add file systems within the image to the case database.
  * @param addUnallocSpace Pass true to create virtual files for unallocated space. Ignored if addFileSystems is false.
  * @param skipFatFsOrphans Pass true to skip processing of orphan files for FAT file systems. Ignored if addFileSystems is false.
+ * @param hostId The ID of the host (already in database).
  *
  * @return A pointer to the process (TskAutoDbJava object) or NULL on error.
  */
 JNIEXPORT jlong JNICALL
 Java_org_sleuthkit_datamodel_SleuthkitJNI_initializeAddImgNat(JNIEnv * env, jclass obj,
-    jobject callbackObj, jstring timeZone, jboolean addFileSystems, jboolean addUnallocSpace, jboolean skipFatFsOrphans) {
+    jobject callbackObj, jstring timeZone, jboolean addFileSystems, jboolean addUnallocSpace, jboolean skipFatFsOrphans, jlong hostId) {
     jboolean isCopy;
 
     if (env->GetStringUTFLength(timeZone) > 0) {
@@ -864,6 +866,9 @@ Java_org_sleuthkit_datamodel_SleuthkitJNI_initializeAddImgNat(JNIEnv * env, jcla
         return 0;
     }
 
+    // Save the host ID
+    tskAutoJava->setHostId(hostId);
+
     // set the options flags
     tskAutoJava->setAddFileSystems(addFileSystems?true:false);
     if (addFileSystems) {
diff --git a/bindings/java/jni/dataModel_SleuthkitJNI.h b/bindings/java/jni/dataModel_SleuthkitJNI.h
index 418db7457..c1808c422 100644
--- a/bindings/java/jni/dataModel_SleuthkitJNI.h
+++ b/bindings/java/jni/dataModel_SleuthkitJNI.h
@@ -170,18 +170,18 @@ JNIEXPORT jobject JNICALL Java_org_sleuthkit_datamodel_SleuthkitJNI_hashDbLookup
 /*
  * Class:     org_sleuthkit_datamodel_SleuthkitJNI
  * Method:    initAddImgNat
- * Signature: (Lorg/sleuthkit/datamodel/TskCaseDbBridge;Ljava/lang/String;ZZ)J
+ * Signature: (Lorg/sleuthkit/datamodel/TskCaseDbBridge;Ljava/lang/String;ZZJ)J
  */
 JNIEXPORT jlong JNICALL Java_org_sleuthkit_datamodel_SleuthkitJNI_initAddImgNat
-  (JNIEnv *, jclass, jobject, jstring, jboolean, jboolean);
+  (JNIEnv *, jclass, jobject, jstring, jboolean, jboolean, jlong);
 
 /*
  * Class:     org_sleuthkit_datamodel_SleuthkitJNI
  * Method:    initializeAddImgNat
- * Signature: (Lorg/sleuthkit/datamodel/TskCaseDbBridge;Ljava/lang/String;ZZZ)J
+ * Signature: (Lorg/sleuthkit/datamodel/TskCaseDbBridge;Ljava/lang/String;ZZZJ)J
  */
 JNIEXPORT jlong JNICALL Java_org_sleuthkit_datamodel_SleuthkitJNI_initializeAddImgNat
-  (JNIEnv *, jclass, jobject, jstring, jboolean, jboolean, jboolean);
+  (JNIEnv *, jclass, jobject, jstring, jboolean, jboolean, jboolean, jlong);
 
 /*
  * Class:     org_sleuthkit_datamodel_SleuthkitJNI
diff --git a/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java b/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
index 8497dc775..0ab4ae702 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
@@ -187,6 +187,8 @@ private void createFileTables(Statement stmt) throws SQLException {
 		stmt.execute("CREATE TABLE data_source_info (obj_id " + dbQueryHelper.getBigIntType() + " PRIMARY KEY, device_id TEXT NOT NULL, "
 				+ "time_zone TEXT NOT NULL, acquisition_details TEXT, added_date_time "+ dbQueryHelper.getBigIntType() + ", "
 				+ "acquisition_tool_settings TEXT, acquisition_tool_name TEXT, acquisition_tool_version TEXT, "
+				+ "host_id " + dbQueryHelper.getBigIntType() + ", "
+				+ "FOREIGN KEY(host_id) REFERENCES tsk_hosts(id), "
 				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
 
 		stmt.execute("CREATE TABLE tsk_fs_info (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
diff --git a/bindings/java/src/org/sleuthkit/datamodel/DataSource.java b/bindings/java/src/org/sleuthkit/datamodel/DataSource.java
index 3136038e1..416abfa3f 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/DataSource.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/DataSource.java
@@ -131,4 +131,13 @@ public interface DataSource extends Content {
 	 * @throws TskCoreException Thrown if the data can not be read
 	 */
 	Long getDateAdded() throws TskCoreException;
+	
+	/**
+	 * Gets the host for this data source.
+	 * 
+	 * @return The host
+	 * 
+	 * @throws TskCoreException 
+	 */
+	Host getHost() throws TskCoreException;
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/HostManager.java b/bindings/java/src/org/sleuthkit/datamodel/HostManager.java
index f1fe3f49b..cad1ef8ba 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/HostManager.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/HostManager.java
@@ -51,6 +51,54 @@ public final class HostManager {
 		this.db = skCase;
 	}
 	
+	/**
+	 * APTODO : using for testing - might be able to remove if desired
+	 * Get or create host with specified name.
+	 *
+	 * @param name	       Host name.
+	 *
+	 * @return Host with the specified name.
+	 *
+	 * @throws TskCoreException
+	 */
+	public Host getOrCreateHost(String name) throws TskCoreException  {
+		CaseDbTransaction trans = db.beginTransaction();
+		try {
+			Host host = getOrCreateHost(name, trans);
+			trans.commit();
+			return host;
+		} catch (TskCoreException ex) {
+			trans.rollback();
+			throw ex;
+		}
+	}	
+	
+	/**
+	 * Get all data sources associated with a given host.
+	 * 
+	 * @param host The host.
+	 * 
+	 * @return The list of data sources corresponding to the host.
+	 * 
+	 * @throws TskCoreException 
+	 */
+	public Set<DataSource> getDataSourcesForHost(Host host) throws TskCoreException {
+		String queryString = "SELECT * FROM data_source_info WHERE host_id = " + host.getId();
+
+		Set<DataSource> dataSources = new HashSet<>();
+		try (CaseDbConnection connection = this.db.getConnection();
+				Statement s = connection.createStatement();
+				ResultSet rs = connection.executeQuery(s, queryString)) {
+
+			while (rs.next()) {
+				dataSources.add(db.getDataSource(rs.getLong("obj_id")));
+			}
+
+			return dataSources;
+		} catch (SQLException | TskDataException ex) {
+			throw new TskCoreException(String.format("Error getting data sources for host " + host.getName()), ex);
+		}
+	}
 	
 	/**
 	 * Get or create host with specified name.
@@ -168,4 +216,34 @@ Set<Host> getHosts() throws TskCoreException {
 			throw new TskCoreException(String.format("Error getting hosts"), ex);
 		}
 	}
+	
+	/**
+	 * Get host for the given data source.
+	 * 
+	 * @param dataSource The data source to look up the host for.
+	 * 
+	 * @return Optional with host.  Optional.empty if no matching host is found.
+	 * 
+	 * @throws TskCoreException 
+	 */
+	Host getHost(DataSource dataSource) throws TskCoreException {
+
+		String queryString = "SELECT tsk_hosts.id AS hostId, tsk_hosts.name AS name, tsk_hosts.status AS status FROM \n" +
+			"tsk_hosts INNER JOIN data_source_info \n" +
+			"ON tsk_hosts.id = data_source_info.host_id \n" +
+			"WHERE data_source_info.obj_id = " + dataSource.getId();
+
+		try (CaseDbConnection connection = this.db.getConnection();
+				Statement s = connection.createStatement();
+				ResultSet rs = connection.executeQuery(s, queryString)) {
+
+			if (!rs.next()) {
+				throw new TskCoreException(String.format("Host not found for data source with ID = %d", dataSource.getId()));
+			} else {
+				return new Host(rs.getLong("hostId"), rs.getString("name"), Host.HostStatus.fromID(rs.getInt("status")));
+			}
+		} catch (SQLException ex) {
+			throw new TskCoreException(String.format("Error getting host for data source with ID = %d", dataSource.getId()), ex);
+		}
+	}	
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Image.java b/bindings/java/src/org/sleuthkit/datamodel/Image.java
index 0f71f7cca..57284faf6 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Image.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Image.java
@@ -43,6 +43,7 @@ public class Image extends AbstractContent implements DataSource {
 	private long size;
 	private final String[] paths;
 	private volatile long imageHandle = 0;
+	private volatile Host host = null;
 	private final String deviceId, timezone;
 	private String md5, sha1, sha256;
 	private static ResourceBundle bundle = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle");
@@ -597,6 +598,23 @@ public Long getDateAdded() throws TskCoreException {
 	public String getAcquisitionDetails() throws TskCoreException {
 		return getSleuthkitCase().getAcquisitionDetails(this);
 	}	
+	
+	/**
+	 * Gets the host for this data source.
+	 * 
+	 * @return The host
+	 * 
+	 * @throws TskCoreException 
+	 */
+	@Override
+	public Host getHost() throws TskCoreException {
+		// This is a check-then-act race condition that may occasionally result
+		// in additional processing but is safer than using locks.
+		if (host == null) {
+			host = getSleuthkitCase().getHostManager().getHost(this);
+		}
+		return host;
+	}	
 
 	/**
 	 * Updates the image's total size and sector size.This function may be used
diff --git a/bindings/java/src/org/sleuthkit/datamodel/LocalFilesDataSource.java b/bindings/java/src/org/sleuthkit/datamodel/LocalFilesDataSource.java
index e54430c08..f85c55c4d 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/LocalFilesDataSource.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/LocalFilesDataSource.java
@@ -39,6 +39,7 @@ public class LocalFilesDataSource extends VirtualDirectory implements DataSource
 	private final long objectId;
 	private final String deviceId;
 	private final String timezone;
+	private volatile Host host;
 
 	private static final Logger LOGGER = Logger.getLogger(LocalFilesDataSource.class.getName());
 
@@ -254,6 +255,23 @@ public String getAcquisitionToolName() throws TskCoreException {
 	public String getAcquisitionToolVersion() throws TskCoreException{
 		return getSleuthkitCase().getDataSourceInfoString(this, "acquisition_tool_version");
 	}
+	
+	/**
+	 * Gets the host for this data source.
+	 * 
+	 * @return The host
+	 * 
+	 * @throws TskCoreException 
+	 */
+	@Override
+	public Host getHost() throws TskCoreException {
+		// This is a check-then-act race condition that may occasionally result
+		// in additional processing but is safer than using locks.
+		if (host == null) {
+			host = getSleuthkitCase().getHostManager().getHost(this);
+		}
+		return host;
+	}	
 
 	/**
 	 * Gets the added date field from the case database.
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
index 802f95d13..d44661eb6 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
@@ -6162,19 +6162,50 @@ public LocalDirectory addLocalDirectory(long parentId, String directoryName, Cas
 	 * @throws TskCoreException if there is an error adding the data source.
 	 */
 	public LocalFilesDataSource addLocalFilesDataSource(String deviceId, String rootDirectoryName, String timeZone, CaseDbTransaction transaction) throws TskCoreException {
+		return addLocalFilesDataSource(deviceId, rootDirectoryName, timeZone, null, transaction);
+	}
+	
+	/**
+	 * Adds a local/logical files and/or directories data source.
+	 *
+	 * @param deviceId          An ASCII-printable identifier for the device
+	 *                          associated with the data source that is intended
+	 *                          to be unique across multiple cases (e.g., a
+	 *                          UUID).
+	 * @param rootDirectoryName The name for the root virtual directory for the
+	 *                          data source.
+	 * @param timeZone          The time zone used to process the data source,
+	 *                          may be the empty string.
+	 * @param host              The host for the data source (may be null)
+	 * @param transaction       A transaction in the scope of which the
+	 *                          operation is to be performed, managed by the
+	 *                          caller.
+	 *
+	 * @return The new local files data source.
+	 *
+	 * @throws TskCoreException if there is an error adding the data source.
+	 */
+	public LocalFilesDataSource addLocalFilesDataSource(String deviceId, String rootDirectoryName, String timeZone, Host host, CaseDbTransaction transaction) throws TskCoreException {
 		acquireSingleUserCaseWriteLock();
 		Statement statement = null;
 		try {
+			CaseDbConnection connection = transaction.getConnection();
+			
 			// Insert a row for the root virtual directory of the data source
 			// into the tsk_objects table.
-			CaseDbConnection connection = transaction.getConnection();
 			long newObjId = addObject(0, TskData.ObjectType.ABSTRACTFILE.getObjectType(), connection);
 
+			// If no host was supplied, make one
+			if (host == null) {
+				// APTODO review name
+				host = getHostManager().getOrCreateHost("LogicalFileSet_" + newObjId + " Host", transaction);
+			}			
+			
 			// Insert a row for the virtual directory of the data source into
 			// the data_source_info table.
 			statement = connection.createStatement();
-			statement.executeUpdate("INSERT INTO data_source_info (obj_id, device_id, time_zone) "
-					+ "VALUES(" + newObjId + ", '" + deviceId + "', '" + timeZone + "');");
+			statement.executeUpdate("INSERT INTO data_source_info (obj_id, device_id, time_zone, host_id) "
+					+ "VALUES(" + newObjId + ", '" + deviceId + "', '" + timeZone + "', " + host.getId() + ");");
 
 			// Insert a row for the root virtual directory of the data source
 			// into the tsk_files table. Note that its data source object id is
@@ -6247,6 +6278,33 @@ public Image addImage(TskData.TSK_IMG_TYPE_ENUM type, long sectorSize, long size
 			String timezone, String md5, String sha1, String sha256,
 			String deviceId,
 			CaseDbTransaction transaction) throws TskCoreException {
+		return addImage(type, sectorSize, size, displayName, imagePaths, timezone, md5, sha1, sha256, deviceId, null, transaction);
+	}	
+	
+	/**
+	 * Add an image to the database.
+	 *
+	 * @param type        Type of image
+	 * @param sectorSize  Sector size
+	 * @param size        Image size
+	 * @param displayName Display name for the image
+	 * @param imagePaths  Image path(s)
+	 * @param timezone    Time zone
+	 * @param md5         MD5 hash
+	 * @param sha1        SHA1 hash
+	 * @param sha256      SHA256 hash
+	 * @param deviceId    Device ID
+	 * @param host        Host
+	 * @param transaction Case DB transaction
+	 *
+	 * @return the newly added Image
+	 *
+	 * @throws TskCoreException
+	 */
+	public Image addImage(TskData.TSK_IMG_TYPE_ENUM type, long sectorSize, long size, String displayName, List<String> imagePaths,
+			String timezone, String md5, String sha1, String sha256,
+			String deviceId, Host host,
+			CaseDbTransaction transaction) throws TskCoreException {
 		acquireSingleUserCaseWriteLock();
 		Statement statement = null;
 		try {
@@ -6280,6 +6338,26 @@ public Image addImage(TskData.TSK_IMG_TYPE_ENUM type, long sectorSize, long size
 				preparedStatement.setLong(3, i);
 				connection.executeUpdate(preparedStatement);
 			}
+			
+			// Create the display name
+			String name = displayName;
+			if (name == null || name.isEmpty()) {
+				if (imagePaths.size() > 0) {
+					String path = imagePaths.get(0);
+					name = (new java.io.File(path)).getName();
+				} else {
+					name = "";
+				}
+			}
+			
+			// Create a host if needed
+			if (host == null) {
+				if (name.isEmpty()) {
+					getHostManager().getOrCreateHost("Image_" + newObjId + " Host", transaction);
+				} else {
+					getHostManager().getOrCreateHost(name + " Host", transaction);
+				}
+			}
 
 			// Add a row to data_source_info
 			preparedStatement = connection.getPreparedStatement(PREPARED_STATEMENT.INSERT_DATA_SOURCE_INFO);
@@ -6288,18 +6366,10 @@ public Image addImage(TskData.TSK_IMG_TYPE_ENUM type, long sectorSize, long size
 			preparedStatement.setString(2, deviceId);
 			preparedStatement.setString(3, timezone);
 			preparedStatement.setLong(4, new Date().getTime());
+			preparedStatement.setLong(5, host.getId());
 			connection.executeUpdate(preparedStatement);
 
 			// Create the new Image object
-			String name = displayName;
-			if (name == null || name.isEmpty()) {
-				if (imagePaths.size() > 0) {
-					String path = imagePaths.get(0);
-					name = (new java.io.File(path)).getName();
-				} else {
-					name = "";
-				}
-			}			
 			return new Image(this, newObjId, type.getValue(), deviceId, sectorSize, name,
 					imagePaths.toArray(new String[imagePaths.size()]), timezone, md5, sha1, sha256, savedSize);
 		} catch (SQLException ex) {
@@ -8690,7 +8760,25 @@ List<Long> getVolumeChildrenIds(Volume vol) throws TskCoreException {
 	 *                          database.
 	 */
 	public Image addImageInfo(long deviceObjId, List<String> imageFilePaths, String timeZone) throws TskCoreException {
-		long imageId = this.caseHandle.addImageInfo(deviceObjId, imageFilePaths, timeZone, this);
+		return addImageInfo(deviceObjId, imageFilePaths, timeZone, null);
+	}	
+	
+	/**
+	 * Adds an image to the case database.
+	 *
+	 * @param deviceObjId    The object id of the device associated with the
+	 *                       image.
+	 * @param imageFilePaths The image file paths.
+	 * @param timeZone       The time zone for the image.
+	 * @param host           The host for this image.
+	 *
+	 * @return An Image object.
+	 *
+	 * @throws TskCoreException if there is an error adding the image to case
+	 *                          database.
+	 */
+	public Image addImageInfo(long deviceObjId, List<String> imageFilePaths, String timeZone, Host host) throws TskCoreException {
+		long imageId = this.caseHandle.addImageInfo(deviceObjId, imageFilePaths, timeZone, host, this);
 		return getImageById(imageId);
 	}
 
@@ -11834,7 +11922,7 @@ private enum PREPARED_STATEMENT {
 		INSERT_IMAGE_NAME("INSERT INTO tsk_image_names (obj_id, name, sequence) VALUES (?, ?, ?)"),
 		INSERT_IMAGE_INFO("INSERT INTO tsk_image_info (obj_id, type, ssize, tzone, size, md5, sha1, sha256, display_name)"
 				+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
-		INSERT_DATA_SOURCE_INFO("INSERT INTO data_source_info (obj_id, device_id, time_zone, added_date_time) VALUES (?, ?, ?, ?)"),
+		INSERT_DATA_SOURCE_INFO("INSERT INTO data_source_info (obj_id, device_id, time_zone, added_date_time, host_id) VALUES (?, ?, ?, ?, ?)"),
 		INSERT_VS_INFO("INSERT INTO tsk_vs_info (obj_id, vs_type, img_offset, block_size) VALUES (?, ?, ?, ?)"),
 		INSERT_VS_PART_SQLITE("INSERT INTO tsk_vs_parts (obj_id, addr, start, length, desc, flags) VALUES (?, ?, ?, ?, ?, ?)"),
 		INSERT_VS_PART_POSTGRESQL("INSERT INTO tsk_vs_parts (obj_id, addr, start, length, descr, flags) VALUES (?, ?, ?, ?, ?, ?)"),
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java
index fbdec24d6..908ad509c 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java
@@ -438,10 +438,21 @@ void free() throws TskCoreException {
 		 * @throws TskCoreException if there is an error adding the image to
 		 *                          case database.
 		 */
-		long addImageInfo(long deviceObjId, List<String> imageFilePaths, String timeZone, SleuthkitCase skCase) throws TskCoreException {
+		long addImageInfo(long deviceObjId, List<String> imageFilePaths, String timeZone, Host host, SleuthkitCase skCase) throws TskCoreException {
 			TskCaseDbBridge dbHelper = new TskCaseDbBridge(skCase, new DefaultAddDataSourceCallbacks());
 			try {
-				long tskAutoDbPointer = initializeAddImgNat(dbHelper, timezoneLongToShort(timeZone), false, false, false);
+				if (host == null) {
+					String hostName;
+					if (imageFilePaths.size() > 0) {
+						String path = imageFilePaths.get(0);
+						hostName = (new java.io.File(path)).getName() + " Host";
+					} else {
+						hostName = "Image_" + deviceObjId + " Host";
+					}
+					host = skCase.getHostManager().getOrCreateHost(hostName);
+				}
+				
+				long tskAutoDbPointer = initializeAddImgNat(dbHelper, timezoneLongToShort(timeZone), false, false, false, host.getId());
 				runOpenAndAddImgNat(tskAutoDbPointer, UUID.randomUUID().toString(), imageFilePaths.toArray(new String[0]), imageFilePaths.size(), timeZone);				
 				long id = finishAddImgNat(tskAutoDbPointer);
 				dbHelper.finish();
@@ -532,7 +543,7 @@ public void run(String deviceId, String[] imageFilePaths, int sectorSize) throws
 				Image img = addImageToDatabase(skCase, imageFilePaths, sectorSize, "", "", "", "", deviceId);
 				run(deviceId, img, sectorSize, new DefaultAddDataSourceCallbacks());
 			}
-
+			
 			/**
 			 * Starts the process of adding an image to the case database.
 			 *
@@ -551,7 +562,35 @@ public void run(String deviceId, String[] imageFilePaths, int sectorSize) throws
 			 *                          the process)
 			 */
 			public void run(String deviceId, Image image, int sectorSize, 
-					AddDataSourceCallbacks addDataSourceCallbacks) throws TskCoreException, TskDataException {			
+					AddDataSourceCallbacks addDataSourceCallbacks) throws TskCoreException, TskDataException {	
+				run(deviceId, image, sectorSize, null, addDataSourceCallbacks);
+			}			
+
+			/**
+			 * Starts the process of adding an image to the case database.
+			 *
+			 * @param deviceId       An ASCII-printable identifier for the
+			 *                       device associated with the image that
+			 *                       should be unique across multiple cases
+			 *                       (e.g., a UUID).
+			 * @param image          The image object (has already been added to the database)
+			 * @param sectorSize     The sector size (no longer used).
+			 * @param host           The host for this image (may be null).
+			 * @param addDataSourceCallbacks  The callbacks to use to send data to ingest (may do nothing).
+			 *
+			 * @throws TskCoreException if a critical error occurs within the
+			 *                          SleuthKit.
+			 * @throws TskDataException if a non-critical error occurs within
+			 *                          the SleuthKit (should be OK to continue
+			 *                          the process)
+			 */
+			public void run(String deviceId, Image image, int sectorSize, Host host,
+					AddDataSourceCallbacks addDataSourceCallbacks) throws TskCoreException, TskDataException {	
+				
+				if (host == null) {
+					host = skCase.getHostManager().getOrCreateHost(image.getName() + " Host");
+				}
+				
 				dbHelper = new TskCaseDbBridge(skCase, addDataSourceCallbacks);
 				getTSKReadLock();
 				try {
@@ -562,7 +601,7 @@ public void run(String deviceId, Image image, int sectorSize,
 						}
 						if (!isCanceled) { //with isCanceled being guarded by this it will have the same value everywhere in this synchronized block
 							imageHandle = image.getImageHandle();
-							tskAutoDbPointer = initAddImgNat(dbHelper, timezoneLongToShort(timeZone), addUnallocSpace, skipFatFsOrphans);
+							tskAutoDbPointer = initAddImgNat(dbHelper, timezoneLongToShort(timeZone), addUnallocSpace, skipFatFsOrphans, host.getId());
 						}
 						if (0 == tskAutoDbPointer) {
 							throw new TskCoreException("initAddImgNat returned a NULL TskAutoDb pointer");
@@ -940,6 +979,29 @@ private static void cacheImageHandle(SleuthkitCase skCase, List<String> imagePat
 	public static Image addImageToDatabase(SleuthkitCase skCase, String[] imagePaths, int sectorSize,
 		String timeZone, String md5fromSettings, String sha1fromSettings, String sha256fromSettings, String deviceId) throws TskCoreException {
 		
+		return addImageToDatabase(skCase, imagePaths, sectorSize, timeZone, md5fromSettings, sha1fromSettings, sha256fromSettings, deviceId, null);
+	}	
+	
+	/**
+	 * Add an image to the database and return the open image.
+	 * 
+	 * @param skCase     The current case.
+	 * @param imagePaths The path(s) to the image (will just be the first for .e01, .001, etc).
+	 * @param sectorSize The sector size (0 for auto-detect).
+	 * @param timeZone   The time zone.
+	 * @param md5fromSettings        MD5 hash (if known).
+	 * @param sha1fromSettings       SHA1 hash (if known).
+	 * @param sha256fromSettings     SHA256 hash (if known).
+	 * @param deviceId   Device ID.
+	 * @param host       Host.
+	 * 
+	 * @return The Image object.
+	 * 
+	 * @throws TskCoreException 
+	 */
+	public static Image addImageToDatabase(SleuthkitCase skCase, String[] imagePaths, int sectorSize,
+		String timeZone, String md5fromSettings, String sha1fromSettings, String sha256fromSettings, String deviceId, Host host) throws TskCoreException {
+		
 		// Open the image
 		long imageHandle = openImgNat(imagePaths, 1, sectorSize);
 		
@@ -967,10 +1029,21 @@ public static Image addImageToDatabase(SleuthkitCase skCase, String[] imagePaths
 		//  Now save to database
 		CaseDbTransaction transaction = skCase.beginTransaction();
 		try {
+			if (host == null) {
+				String hostName;
+				if (computedPaths.size() > 0) {
+					String path = computedPaths.get(0);
+					hostName = (new java.io.File(path)).getName() + " Host";
+				} else {
+					hostName = "Image_" + deviceId + " Host";
+				}
+				host = skCase.getHostManager().getOrCreateHost(hostName, transaction);
+			}
+			
 			Image img = skCase.addImage(TskData.TSK_IMG_TYPE_ENUM.valueOf(type), computedSectorSize, 
 				size, null, computedPaths, 
 				timeZone, md5, sha1, sha256, 
-				deviceId, transaction);
+				deviceId, host, transaction);
 			if (!StringUtils.isEmpty(collectionDetails)) {
 				skCase.setAcquisitionDetails(img, collectionDetails);
 			}
@@ -2096,9 +2169,9 @@ public static long openFile(long fsHandle, long fileId, TSK_FS_ATTR_TYPE_ENUM at
 
 	private static native HashHitInfo hashDbLookupVerbose(String hash, int dbHandle) throws TskCoreException;
 
-	private static native long initAddImgNat(TskCaseDbBridge dbHelperObj, String timezone, boolean addUnallocSpace, boolean skipFatFsOrphans) throws TskCoreException;
+	private static native long initAddImgNat(TskCaseDbBridge dbHelperObj, String timezone, boolean addUnallocSpace, boolean skipFatFsOrphans, long hostId) throws TskCoreException;
 
-	private static native long initializeAddImgNat(TskCaseDbBridge dbHelperObj, String timezone, boolean addFileSystems, boolean addUnallocSpace, boolean skipFatFsOrphans) throws TskCoreException;
+	private static native long initializeAddImgNat(TskCaseDbBridge dbHelperObj, String timezone, boolean addFileSystems, boolean addUnallocSpace, boolean skipFatFsOrphans, long hostId) throws TskCoreException;
 
 	private static native void runOpenAndAddImgNat(long process, String deviceId, String[] imgPath, int splits, String timezone) throws TskCoreException, TskDataException;
 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TskCaseDbBridge.java b/bindings/java/src/org/sleuthkit/datamodel/TskCaseDbBridge.java
index cee51945a..36cc1d257 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TskCaseDbBridge.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TskCaseDbBridge.java
@@ -123,16 +123,18 @@ void finish() {
      * @param sha256      SHA256 hash.
      * @param deviceId    Device ID.
      * @param collectionDetails  The collection details.
+     * @param hostId      Host ID.
+     * @param paths       Data source path(s)
      * 
      * @return The object ID of the new image or -1 if an error occurred
      */
     long addImageInfo(int type, long ssize, String timezone, 
             long size, String md5, String sha1, String sha256, String deviceId, 
-            String collectionDetails, String[] paths) {    
+            String collectionDetails, long hostId, String[] paths) {    
         try {
             beginTransaction();
             long objId = addImageToDb(TskData.TSK_IMG_TYPE_ENUM.valueOf(type), ssize, size,
-                    timezone, md5, sha1, sha256, deviceId, collectionDetails, trans);
+                    timezone, md5, sha1, sha256, deviceId, collectionDetails, hostId, trans);
             for (int i = 0;i < paths.length;i++) {
                 addImageNameToDb(objId, paths[i], i, trans);
             }
@@ -913,6 +915,7 @@ private long addFileToDb(long parentObjId,
 	 * @param sha256            SHA256 hash.
 	 * @param deviceId          Device ID.
 	 * @param collectionDetails Collection details.
+	 * @param hostId            The ID of a host already in the database.
 	 * @param transaction       Case DB transaction.
 	 *
 	 * @return The newly added Image object ID.
@@ -921,7 +924,7 @@ private long addFileToDb(long parentObjId,
 	 */
 	private long addImageToDb(TskData.TSK_IMG_TYPE_ENUM type, long sectorSize, long size,
 			String timezone, String md5, String sha1, String sha256,
-			String deviceId, String collectionDetails,
+			String deviceId, String collectionDetails, long hostId,
 			CaseDbTransaction transaction) throws TskCoreException {
 		try {
 			// Insert a row for the Image into the tsk_objects table.
@@ -948,13 +951,14 @@ private long addImageToDb(TskData.TSK_IMG_TYPE_ENUM type, long sectorSize, long
 			connection.executeUpdate(preparedStatement);
 
 			// Add a row to data_source_info
-			String dataSourceInfoSql = "INSERT INTO data_source_info (obj_id, device_id, time_zone, acquisition_details) VALUES (?, ?, ?, ?)"; // NON-NLS
+			String dataSourceInfoSql = "INSERT INTO data_source_info (obj_id, device_id, time_zone, acquisition_details, host_id) VALUES (?, ?, ?, ?, ?)"; // NON-NLS
 			preparedStatement = connection.getPreparedStatement(dataSourceInfoSql, Statement.NO_GENERATED_KEYS);
 			preparedStatement.clearParameters();
 			preparedStatement.setLong(1, newObjId);
 			preparedStatement.setString(2, deviceId);
 			preparedStatement.setString(3, timezone);
 			preparedStatement.setString(4, collectionDetails);
+			preparedStatement.setLong(5, hostId);
 			connection.executeUpdate(preparedStatement);
 
 			return newObjId;
-- 
GitLab