Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • irt/sleuthkit
1 result
Show changes
Showing
with 7519 additions and 0 deletions
/*
* Sleuth Kit Data Model
*
* Copyright 2014-2021 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;
import java.util.ResourceBundle;
/**
* Represents information about an ingest module factory, used in ingest job
* info to show which ingest modules were run.
*/
public final class IngestModuleInfo {
private static final ResourceBundle bundle = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle");
/**
* Used to keep track of the module types
*/
public static enum IngestModuleType {
/*
* IMPORTANT: DO NOT CHANGE ORDER, THE ORDINAL VALUES OF THE ENUM ARE
* STORED IN THE CASE DATABASE
*/
DATA_SOURCE_LEVEL(bundle.getString("IngestModuleInfo.IngestModuleType.DataSourceLevel.displayName")),
FILE_LEVEL(bundle.getString("IngestModuleInfo.IngestModuleType.FileLevel.displayName")),
DATA_ARTIFACT(bundle.getString("IngestModuleInfo.IngestModuleType.DataArtifact.displayName")),
MULTIPLE("IngestModuleInfo.IngestModuleType.Multiple.displayName"),
ANALYSIS_RESULT(bundle.getString("IngestModuleInfo.IngestModuleType.AnalysisResult.displayName"));
private final String displayName;
private IngestModuleType(String displayName) {
this.displayName = displayName;
}
public static IngestModuleType fromID(int typeId) {
for (IngestModuleType moduleType : IngestModuleType.values()) {
if (moduleType.ordinal() == typeId) {
return moduleType;
}
}
return null;
}
/**
* @return the displayName
*/
public String getDisplayName() {
return displayName;
}
}
private final long ingestModuleId;
private final String displayName;
private final String uniqueName;
private final IngestModuleType type;
private final String version;
/**
*
* @param ingestModuleId The id of the ingest module
* @param displayName The display name of the ingest module
* @param uniqueName The unique name of the ingest module.
* @param type The ingest module type of the module.
* @param version The version number of the module.
*/
IngestModuleInfo(long ingestModuleId, String displayName, String uniqueName, IngestModuleType type, String version) {
this.ingestModuleId = ingestModuleId;
this.displayName = displayName;
this.uniqueName = uniqueName;
this.type = type;
this.version = version;
}
/**
* @return the ingestModuleId
*/
public long getIngestModuleId() {
return ingestModuleId;
}
/**
* @return the displayName
*/
public String getDisplayName() {
return displayName;
}
/**
* @return the uniqueName
*/
public String getUniqueName() {
return uniqueName;
}
/**
* @return the typeID
*/
public IngestModuleType getType() {
return type;
}
/**
* @return the version
*/
public String getVersion() {
return version;
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2020 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;
/**
* Exception thrown when an account identifier is not valid.
*
*/
public class InvalidAccountIDException extends TskCoreException {
private static final long serialVersionUID = 1L;
/**
* Default constructor when error message is not available
*/
public InvalidAccountIDException() {
super("No error message available.");
}
/**
* Create exception containing the error message.
*
* @param msg Message.
*/
public InvalidAccountIDException(String msg) {
super(msg);
}
/**
* Create exception containing the error message and cause exception.
*
* @param msg Message.
* @param ex Underlying exception.
*/
public InvalidAccountIDException(String msg, Exception ex) {
super(msg, ex);
}
}
/*
* SleuthKit Java Bindings
*
* Copyright 2011-2022 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;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.sleuthkit.datamodel.TskData.FileKnown;
import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_ATTR_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_TYPE_ENUM;
/**
* A representation of a layout file that has been added to a case. Layout files
* are not file system files, but "virtual" files created from blocks of data
* (e.g. unallocated) that are treated as files for convenience and uniformity.
*
* Because layout files are not real file system files, they only utilize a
* subset of meta-data attributes. A layout file normally contains one or more
* entry in tsk_file_layout table that define ordered byte block ranges, with
* respect to the image.
*
* The class also supports reads of layout files, reading blocks across ranges
* in a sequence.
*/
public class LayoutFile extends AbstractFile {
private long imageHandle = -1;
/**
* Constructs a representation of a layout file that has been added to a
* case. Layout files are not file system files, but "virtual" files created
* from blocks of data (e.g. unallocated) that are treated as files for
* convenience and uniformity.
*
* @param db The case database to which the file has been
* added.
* @param objId The object id of the file in the case database.
* @param dataSourceObjectId The object id of the data source for the file.
* @param fileSystemObjectId The object id of the file system. May be null.
* @param name The name of the file.
* @param fileType The type of the file.
* @param dirType The type of the file, usually as reported in
* the name structure of the file system. May be
* set to TSK_FS_NAME_TYPE_ENUM.UNDEF.
* @param metaType The type of the file, usually as reported in
* the metadata structure of the file system. May
* be set to
* TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_UNDEF.
* @param dirFlag The allocated status of the file, usually as
* reported in the name structure of the file
* system.
* @param metaFlags The allocated status of the file, usually as
* reported in the metadata structure of the file
* system.
* @param size The size of the file.
* @param ctime The changed time of the file.
* @param crtime The creation time of the file.
* @param atime The accessed time of the file
* @param mtime The modified time of the file.
* @param md5Hash The MD5 hash of the file, null if not yet
* calculated.
* @param sha256Hash sha256 hash of the file, or null if not present
* @param sha1Hash SHA-1 hash of the file, or null if not present
* @param knownState The known state of the file from a hash
* database lookup, null if not yet looked up.
* @param parentPath The path of the parent of the file.
* @param mimeType The MIME type of the file, null if it has not
* yet been determined.
* @param ownerUid UID of the file owner as found in the file
* system, can be null.
* @param osAccountObjId Obj id of the owner OS account, may be null.
*/
LayoutFile(SleuthkitCase db,
long objId,
long dataSourceObjectId,
Long fileSystemObjectId,
String name,
TSK_DB_FILES_TYPE_ENUM fileType,
TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags,
long size,
long ctime, long crtime, long atime, long mtime,
String md5Hash, String sha256Hash, String sha1Hash,
FileKnown knownState,
String parentPath, String mimeType,
String ownerUid,
Long osAccountObjId) {
super(db, objId, dataSourceObjectId, fileSystemObjectId, TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0, name, fileType, 0L, 0, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, (short) 0, 0, 0, md5Hash, sha256Hash, sha1Hash, knownState, parentPath, mimeType, SleuthkitCase.extractExtension(name), ownerUid, osAccountObjId, TskData.CollectedStatus.UNKNOWN, Collections.emptyList());
}
/**
* Gets the number of file layout ranges associated with this layout file.
*
* @return The number of file layout ranges.
*/
public int getNumParts() {
int numParts = 0;
try {
numParts = getRanges().size();
} catch (TskCoreException ex) {
Logger.getLogger(LayoutFile.class.getName()).log(Level.SEVERE, String.format("Error getting layout ranges for layout file (objId = %d)", getId()), ex); //NON-NLS
}
return numParts;
}
/**
* Indicates whether or not this layout file is the root of a file system,
* always returns false.
*
* @return False.
*/
@Override
public boolean isRoot() {
return false;
}
/**
* Does nothing, a layout file cannot be directly opened, read, or closed.
* Use the readInt method to get layout file content.
*/
@Override
public void close() {
}
/**
* Reads bytes from the layout ranges associated with this file.
*
* @param buf Buffer to read into.
* @param offset Start position in the file.
* @param len Number of bytes to read.
*
* @return Number of bytes read.
*
* @throws TskCoreException if there is a problem reading the file.
*/
@Override
protected int readInt(byte[] buf, long offset, long len) throws TskCoreException {
long offsetInThisLayoutContent = 0; // current offset in this LayoutContent
int bytesRead = 0; // Bytes read so far
// if the caller has requested more data than we have in the file
// then make sure we don't go beyond the end of the file
long readLen = len;
if (offset + readLen > size)
readLen = size - offset;
if (imageHandle == -1) {
Content dataSource = getDataSource();
if ((dataSource != null) && (dataSource instanceof Image)) {
Image image = (Image) dataSource;
imageHandle = image.getImageHandle();
} else {
throw new TskCoreException("Data Source of LayoutFile is not Image");
}
}
for (TskFileRange range : getRanges()) {
if (bytesRead < readLen) { // we haven't read enough yet
if (offset < offsetInThisLayoutContent + range.getByteLen()) { // if we are in a range object we want to read from
long offsetInRange = 0; // how far into the current range object to start reading
if (bytesRead == 0) { // we haven't read anything yet so we want to read from the correct offset in this range object
offsetInRange = offset - offsetInThisLayoutContent; // start reading from the correct offset
}
long offsetInImage = range.getByteStart() + offsetInRange; // how far into the image to start reading
long lenToReadInRange = Math.min(range.getByteLen() - offsetInRange, readLen - bytesRead); // how much we can read this time
int lenRead = readImgToOffset(imageHandle, buf, bytesRead, offsetInImage, (int) lenToReadInRange);
bytesRead += lenRead;
if (lenToReadInRange != lenRead) { // If image read failed or was cut short
break;
}
}
offsetInThisLayoutContent += range.getByteLen();
} else { // we're done reading
break;
}
}
return bytesRead;
}
/**
* Reads bytes from an image into a buffer, starting at given position in
* buffer.
*
* @param imgHandle The image to read from.
* @param buf The array to read into.
* @param offsetInBuf Where to start in the array.
* @param offsetInImage Where to start in the image.
* @param lenToRead How far to read in the image.
*
* @return the number of characters read, or -1 if the end of the stream has
* been reached
*
* @throws TskCoreException exception thrown if critical error occurs within
* TSK
*/
private int readImgToOffset(long imgHandle, byte[] buf, int offsetInBuf, long offsetInImage, int lenToRead) throws TskCoreException {
byte[] currentBuffer = new byte[lenToRead]; // the buffer for the current range object
int lenRead = SleuthkitJNI.readImg(imgHandle, currentBuffer, offsetInImage, lenToRead);
System.arraycopy(currentBuffer, 0, buf, offsetInBuf, lenToRead); // copy what we just read into the main buffer
return lenRead;
}
/**
* Accepts a content visitor (Visitor design pattern).
*
* @param visitor A ContentVisitor supplying an algorithm to run using this
* file as input.
*
* @return The output of the algorithm.
*/
@Override
public <T> T accept(ContentVisitor<T> visitor) {
return visitor.visit(this);
}
/**
* Accepts a Sleuthkit item visitor (Visitor design pattern).
*
* @param visitor A SleuthkitItemVisitor supplying an algorithm to run using
* this file as input.
*
* @return The output of the algorithm.
*/
@Override
public <T> T accept(SleuthkitItemVisitor<T> visitor) {
return visitor.visit(this);
}
/**
* Provides a string representation of this file.
*
* @param preserveState True if state should be included in the string
* representation of this object.
*/
@Override
public String toString(boolean preserveState) {
return super.toString(preserveState) + "LayoutFile [\t" + "]\t"; //NON-NLS
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2013 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;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
/**
* Collection of methods to load libraries embedded in the TSK Datamodel Jar
* file.
*
* @author jwallace
*/
public class LibraryUtils {
public static final String[] EXTS = new String[]{".so", ".dylib", ".dll", ".jnilib"}; //NON-NLS
/**
* The libraries the TSK Datamodel needs.
*/
public enum Lib {
MSVCP("msvcp100", ""), //NON-NLS
MSVCR("msvcr100", ""), //NON-NLS
ZLIB("zlib", "z"), //NON-NLS
LIBEWF("libewf", "ewf"), //NON-NLS
LIBVMDK("libvmdk", "vmdk"), //NON-NLS
LIBVHDI("libvhdi", "vhd"), //NON-NLS
TSK_JNI("libtsk_jni", "tsk_jni"); //NON-NLS
private final String name;
private final String unixName;
Lib(String name, String unixName) {
this.name = name;
this.unixName = unixName;
}
public String getLibName() {
return this.name;
}
public String getUnixName() {
return this.unixName;
}
}
/**
* Load the Sleuthkit JNI.
*
* @return true if library was found and loaded
*/
public static boolean loadSleuthkitJNI() {
boolean loaded = LibraryUtils.loadNativeLibFromTskJar(Lib.TSK_JNI);
if (!loaded) {
System.out.println("SleuthkitJNI: failed to load " + Lib.TSK_JNI.getLibName()); //NON-NLS
} else {
// We want minimal console output for command line use case
//System.out.println("SleuthkitJNI: loaded " + Lib.TSK_JNI.getLibName()); //NON-NLS
}
return loaded;
}
/**
* Get the name of the current platform.
*
* @return a platform identifier, formatted as "OS_ARCH/OS_NAME"
*/
private static String getPlatform() {
String os = System.getProperty("os.name").toLowerCase();
if (LibraryUtils.isWindows()) {
os = "win"; //NON-NLS
} else if (LibraryUtils.isMac()) {
os = "mac"; //NON-NLS
} else if (LibraryUtils.isLinux()) {
os = "linux"; //NON-NLS
}
// os.arch represents the architecture of the JVM, not the os
String arch = System.getProperty("os.arch");
return arch.toLowerCase() + "/" + os.toLowerCase();
}
/**
* Is the platform Windows?
*
* @return
*/
private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("windows"); //NON-NLS
}
/**
* Is the platform Mac?
*
* @return
*/
private static boolean isMac() {
return System.getProperty("os.name").toLowerCase().contains("mac"); //NON-NLS
}
/**
* Is the platform Linux?
*
* @return
*/
private static boolean isLinux() {
return System.getProperty("os.name").equals("Linux"); //NON-NLS
}
/**
* Attempt to extract and load the specified native library.
*
* @param library
*
* @return
*/
private static boolean loadNativeLibFromTskJar(Lib library) {
String libName = library.getLibName();
String userName = System.getProperty("user.name");
// find the library in the jar file
StringBuilder pathInJarBase = new StringBuilder();
pathInJarBase.append("/NATIVELIBS/"); //NON-NLS
pathInJarBase.append(getPlatform());
pathInJarBase.append("/");
pathInJarBase.append(libName);
URL urlInJar = null;
String libExt = null;
for (String ext : EXTS) {
urlInJar = SleuthkitJNI.class.getResource(pathInJarBase.toString() + ext);
if (urlInJar != null) {
libExt = ext;
break;
}
}
if (urlInJar == null) {
System.out.println("Library not found in jar (" + libName + ")"); //NON-NLS
return false;
}
StringBuilder pathToTempFile = new StringBuilder();
pathToTempFile.append(System.getProperty("java.io.tmpdir"));
pathToTempFile.append(java.io.File.separator);
pathToTempFile.append(libName);
pathToTempFile.append("_");
pathToTempFile.append(userName);
pathToTempFile.append(libExt);
// copy library to temp folder and load it
try {
java.io.File tempLibFile = new java.io.File(pathToTempFile.toString()); //NON-NLS
// We want minimal console output for command line use case
//System.out.println("Temp Folder for Libraries: " + tempLibFile.getParent()); //NON-NLS
// cycle through the libraries and delete them.
// we used to copy dlls into here.
// delete any than may still exist from previous installations.
// Dec 2013
for (Lib l : Lib.values()) {
String ext = getExtByPlatform();
// try the windows version
java.io.File f = new java.io.File(l.getLibName() + ext);
//System.out.println(f.getName());
if (f.exists()) {
f.delete();
} else {
// try the unix version
java.io.File fUnix = new java.io.File(l.getUnixName() + ext);
//System.out.println(fUnix.getName());
if (fUnix.exists()) {
fUnix.delete();
}
}
}
// Delete old file
if (tempLibFile.exists()) {
if (tempLibFile.delete() == false) {
System.out.println("Error deleting old native library. Is the app already running? (" + tempLibFile.toString() + ")"); //NON-NLS
return false;
}
}
// copy it
InputStream in = urlInJar.openStream();
OutputStream out = new FileOutputStream(tempLibFile);
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
in.close();
out.close();
// load it
System.load(tempLibFile.getAbsolutePath());
} catch (IOException e) {
// Loading failed.
System.out.println("Error loading library: " + e.getMessage()); //NON-NLS
return false;
}
return true;
}
private static String getExtByPlatform() {
if (isWindows()) {
return ".dll"; //NON-NLS
} else if (isMac()) {
return ".dylib"; //NON-NLS
} else {
return ".so"; //NON-NLS
}
}
}
/*
* SleuthKit Java Bindings
*
* Copyright 2011-2022 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;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.sleuthkit.datamodel.TskData.FileKnown;
import org.sleuthkit.datamodel.TskData.TSK_FS_ATTR_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_TYPE_ENUM;
/**
* A local directory that can be used as a parent for local files.
* Not a file system
*/
public class LocalDirectory extends SpecialDirectory {
private static final Logger logger = Logger.getLogger(LocalDirectory.class.getName());
/**
* Constructs a local directory that can be used as a parent for
* local files. Not a file system directory.
*
* @param db The case database.
* @param objId The object id of the local directory.
* @param dataSourceObjectId The object id of the data source for the
* local directory
* @param name The name of the local directory.
* @param dirType The TSK_FS_NAME_TYPE_ENUM for the local
* directory.
* @param metaType The TSK_FS_META_TYPE_ENUM for the local
* directory.
* @param dirFlag The TSK_FS_META_TYPE_ENUM for the local
* directory.
* @param metaFlags The meta flags for the local directory.
* @param size The size of the local directory, should be
* zero.
* @param md5Hash The MD5 hash for the local directory.
* @param sha256Hash sha256 hash of the file, or null if not present
* @param sha1Hash SHA-1 hash of the file, or null if not present
* @param knownState The known state for the local directory
* @param parentPath The parent path for the local directory
*/
LocalDirectory(SleuthkitCase db,
long objId,
long dataSourceObjectId,
String name,
TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags,
String md5Hash, String sha256Hash, String sha1Hash,
FileKnown knownState,
String parentPath) {
super(db, objId, dataSourceObjectId, null, TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0, name,
TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL_DIR, 0L, 0, dirType, metaType, dirFlag,
metaFlags, 0L, 0L, 0L, 0L, 0L, (short) 0, 0, 0, md5Hash, sha256Hash, sha1Hash, knownState, parentPath, null);
}
/**
* Check whether this LocalDirectory is a data source.
* Will always be false.
* @return false
*/
@Override
public boolean isDataSource() {
return false;
}
/**
* Indicates whether or not this directory is the root of a file
* system. Local directories should only be the root of a file
* system in a portable case.
*
* @return true if the parent of this directory is a file system
*/
@Override
public boolean isRoot() {
try {
return (getParent() instanceof FileSystem);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error getting parent of LocalDirectory with object ID " + getId(), ex);
return false;
}
}
/**
* Accepts a content visitor (Visitor design pattern).
*
* @param visitor A ContentVisitor supplying an algorithm to run using this
* local directory as input.
*
* @return The output of the algorithm.
*/
@Override
public <T> T accept(ContentVisitor<T> visitor) {
return visitor.visit(this);
}
/**
* Accepts a Sleuthkit item visitor (Visitor design pattern).
*
* @param visitor A SleuthkitItemVisitor supplying an algorithm to run using
* this local directory as input.
*
* @return The output of the algorithm.
*/
@Override
public <T> T accept(SleuthkitItemVisitor<T> visitor) {
return visitor.visit(this);
}
/**
* Provides a string representation of this local directory.
*
* @param preserveState True if state should be included in the string
* representation of this object.
*
* @return string representation of this local directory
* @throws TskCoreException if there was an error querying the case
* database.
*/
@Override
public String toString(boolean preserveState) {
return super.toString(preserveState) + "LocalDirectory [\t" + "]\t"; //NON-NLS
}
}
/*
* SleuthKit Java Bindings
*
* Copyright 2011-2022 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;
import java.util.Collections;
import java.util.List;
import org.sleuthkit.datamodel.TskData.FileKnown;
import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_ATTR_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_TYPE_ENUM;
/**
* A representation of a local/logical file (e.g., on a user's machine) that has
* been added to a case.
*/
public class LocalFile extends AbstractFile {
/**
* Constructs a representation of a local/logical file (e.g., on a user's
* machine) that has been added to the case database.
*
* @param db The case database to which the file has been
* added.
* @param objId The object id of the file in the case database.
* @param name The name of the file.
* @param fileType The type of the file.
* @param dirType The type of the file, usually as reported in
* the name structure of the file system. May be
* set to TSK_FS_NAME_TYPE_ENUM.UNDEF.
* @param metaType The type of the file, usually as reported in
* the metadata structure of the file system. May
* be set to
* TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_UNDEF.
* @param dirFlag The allocated status of the file, usually as
* reported in the name structure of the file
* system.
* @param metaFlags The allocated status of the file, usually as
* reported in the metadata structure of the file
* system.
* @param size The size of the file.
* @param ctime The changed time of the file.
* @param crtime The created time of the file.
* @param atime The accessed time of the file.
* @param mtime The modified time of the file.
* @param mimeType The MIME type of the file, null if it has not
* yet been determined.
* @param md5Hash The MD5 hash of the file, null if not yet
* calculated.
* @param sha256Hash sha256 hash of the file, or null if not present
* @param sha1Hash SHA-1 hash of the file, or null if not present
* @param knownState The known state of the file from a hash
* database lookup, null if not yet looked up.
* @param parentId The object id of parent of the file.
* @param parentPath The path of the parent of the file.
* @param dataSourceObjectId The object id of the data source for the file.
* @param localPath The absolute path of the file in secondary
* storage.
* @param encodingType The encoding type of the file.
* @param extension The extension part of the file name (not
* including the '.'), can be null.
* @param ownerUid String UID of the user as found in in the file
* system, can be null.
* @param osAccountObjId Obj id of the owner OS account, may be null.
*/
LocalFile(SleuthkitCase db,
long objId,
String name,
TSK_DB_FILES_TYPE_ENUM fileType,
TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags,
long size,
long ctime, long crtime, long atime, long mtime,
String mimeType, String md5Hash, String sha256Hash, String sha1Hash,
FileKnown knownState,
long parentId, String parentPath,
long dataSourceObjectId,
String localPath,
TskData.EncodingType encodingType,
String extension,
String ownerUid,
Long osAccountObjId) {
super(db, objId, dataSourceObjectId, null, TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0,
name, fileType, 0L, 0, dirType, metaType, dirFlag,
metaFlags, size, ctime, crtime, atime, mtime, (short) 0, 0, 0, md5Hash, sha256Hash, sha1Hash, knownState, parentPath, mimeType, extension, ownerUid, osAccountObjId, TskData.CollectedStatus.UNKNOWN, Collections.emptyList());
// TODO (AUT-1904): The parent id should be passed to AbstractContent
// through the class hierarchy contructors, using
// AbstractContent.UNKNOWN_ID as needed.
if (parentId > 0) {
setParentId(parentId);
}
super.setLocalFilePath(localPath);
setEncodingType(encodingType);
}
/**
* Gets the extents in terms of byte addresses of this local file within its
* data source, an empty list.
*
* @return An empty list of extents (TskFileRange objects)
*
* @throws TskCoreException if there was an error querying the case
* database.
*/
@Override
public List<TskFileRange> getRanges() throws TskCoreException {
return Collections.<TskFileRange>emptyList();
}
/**
* Indicates whether or not this local file is the root of a file system,
* always returns false.
*
* @return False.
*/
@Override
public boolean isRoot() {
return false;
}
/**
* Accepts a content visitor (Visitor design pattern).
*
* @param <T> The type returned by the visitor.
* @param visitor A ContentVisitor supplying an algorithm to run using this
* local file as input.
*
* @return The output of the algorithm.
*/
@Override
public <T> T accept(ContentVisitor<T> visitor) {
return visitor.visit(this);
}
/**
* Accepts a Sleuthkit item visitor (Visitor design pattern).
*
* @param <T> The type returned by the visitor.
* @param visitor A SleuthkitItemVisitor supplying an algorithm to run using
* this local file as input.
*
* @return The output of the algorithm.
*/
@Override
public <T> T accept(SleuthkitItemVisitor<T> visitor) {
return visitor.visit(this);
}
/**
* Provides a string representation of this local file.
*
* @param preserveState True if state should be included in the string
* representation of this object.
*/
@Override
public String toString(boolean preserveState) {
return super.toString(preserveState) + "LocalFile [\t" + "]\t"; //NON-NLS
}
/**
* Constructs a representation of a local/logical file (e.g., on a user's
* machine) that has been added to the case database.
*
* @param db The case database to which the file has been added.
* @param objId The object id of the file in the case database.
* @param name The name of the file.
* @param fileType The type of the file.
* @param dirType The type of the file, usually as reported in the name
* structure of the file system. May be set to
* TSK_FS_NAME_TYPE_ENUM.UNDEF.
* @param metaType The type of the file, usually as reported in the
* metadata structure of the file system. May be set to
* TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_UNDEF.
* @param dirFlag The allocated status of the file, usually as reported
* in the name structure of the file system.
* @param metaFlags The allocated status of the file, usually as reported
* in the metadata structure of the file system.
* @param size The size of the file.
* @param ctime The changed time of the file.
* @param crtime The created time of the file.
* @param atime The accessed time of the file.
* @param mtime The modified time of the file.
* @param md5Hash The MD5 hash of the file, null if not yet calculated.
* @param knownState The known state of the file from a hash database
* lookup, null if not yet looked up.
* @param parentPath The path of the parent of the file.
* @param localPath The absolute path of the file in secondary storage.
*
* @deprecated Do not make subclasses outside of this package.
*/
@Deprecated
@SuppressWarnings("deprecation")
protected LocalFile(SleuthkitCase db,
long objId,
String name,
TSK_DB_FILES_TYPE_ENUM fileType,
TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags,
long size,
long ctime, long crtime, long atime, long mtime,
String md5Hash, FileKnown knownState,
String parentPath,
String localPath) {
this(db,
objId,
name,
fileType,
dirType, metaType,
dirFlag, metaFlags,
size,
ctime, crtime, atime, mtime,
null, md5Hash, null, null, knownState,
AbstractContent.UNKNOWN_ID, parentPath,
db.getDataSourceObjectId(objId),
localPath,
TskData.EncodingType.NONE, null, OsAccount.NO_OWNER_ID, OsAccount.NO_ACCOUNT);
}
/**
* Constructs a representation of a local/logical file (e.g., on a user's
* machine) that has been added to the case database.
*
* @param db The case database to which the file has been added.
* @param objId The object id of the file in the case database.
* @param name The name of the file.
* @param fileType The type of the file.
* @param dirType The type of the file, usually as reported in the name
* structure of the file system. May be set to
* TSK_FS_NAME_TYPE_ENUM.UNDEF.
* @param metaType The type of the file, usually as reported in the
* metadata structure of the file system. May be set to
* TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_UNDEF.
* @param dirFlag The allocated status of the file, usually as reported
* in the name structure of the file system.
* @param metaFlags The allocated status of the file, usually as reported
* in the metadata structure of the file system.
* @param size The size of the file.
* @param ctime The changed time of the file.
* @param crtime The created time of the file.
* @param atime The accessed time of the file.
* @param mtime The modified time of the file.
* @param md5Hash The MD5 hash of the file, null if not yet calculated.
* @param knownState The known state of the file from a hash database
* lookup, null if not yet looked up.
* @param parentPath The path of the parent of the file.
* @param localPath The absolute path of the file in secondary storage.
* @param parentId The object id of parent of the file.
*
* @deprecated Do not make subclasses outside of this package.
*/
@Deprecated
protected LocalFile(SleuthkitCase db,
long objId,
String name,
TSK_DB_FILES_TYPE_ENUM fileType,
TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags,
long size,
long ctime, long crtime, long atime, long mtime,
String md5Hash, FileKnown knownState,
String parentPath, String localPath, long parentId) {
this(db, objId, name, fileType, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, md5Hash, knownState, parentPath, localPath);
}
/**
* Constructs a representation of a local/logical file (e.g., on a user's
* machine) that has been added to the case.
*
* @param db The case database to which the file has been added.
* @param objId The object id of the file in the case database.
* @param name The name of the file.
* @param dirType The type of the file, usually as reported in the name
* structure of the file system. May be set to
* TSK_FS_NAME_TYPE_ENUM.UNDEF.
* @param metaType The type of the file, usually as reported in the
* metadata structure of the file system. May be set to
* TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_UNDEF.
* @param dirFlag The allocated status of the file, usually as reported
* in the name structure of the file system.
* @param metaFlags The allocated status of the file, usually as reported
* in the metadata structure of the file system.
* @param size The size of the file.
* @param ctime The changed time of the file.
* @param crtime The created time of the file.
* @param atime The accessed time of the file.
* @param mtime The modified time of the file.
* @param md5Hash The MD5 hash of the file, null if not yet calculated.
* @param knownState The known state of the file from a hash database
* lookup, null if not yet looked up.
* @param parentPath The path of the parent of the file.
* @param localPath The absolute path of the file in secondary storage.
* @param parentId The object id of parent of the file.
*
* @deprecated Do not make subclasses outside of this package.
*/
@Deprecated
protected LocalFile(SleuthkitCase db,
long objId,
String name,
TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags,
long size,
long ctime, long crtime, long atime, long mtime,
String md5Hash, FileKnown knownState,
String parentPath, String localPath, long parentId) {
this(db, objId, name, TSK_DB_FILES_TYPE_ENUM.LOCAL, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, md5Hash, knownState, parentPath, localPath);
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2011-2022 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;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A local/logical files and/or directories data source.
*
* NOTE: The DataSource interface is an emerging feature and at present is only
* useful for obtaining the object id and the device id, 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). In the future, this interface
* will extend the Content interface and the AbstractDataSource will become an
* abstract superclass.
*/
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());
/**
* Constructs a local/logical files and/or directories data source.
*
* @param db The case database.
* @param objId The object id of the virtual directory.
* @param dataSourceObjectId The object id of the data source for the
* virtual directory; same as objId if the virtual
* directory is a data source.
* @param name The name of the virtual directory.
* @param dirType The TSK_FS_NAME_TYPE_ENUM for the virtual
* directory.
* @param deviceId The device ID for the data source.
* @param metaType The TSK_FS_META_TYPE_ENUM for the virtual
* directory.
* @param dirFlag The TSK_FS_META_TYPE_ENUM for the virtual
* directory.
* @param metaFlags The meta flags for the virtual directory.
* @param timezone The timezone for the data source.
* @param md5Hash The MD5 hash for the virtual directory.
* @param sha256Hash The SHA-256 hash for the virtual directory.
* @param sha1Hash SHA-1 hash of the file, or null if not present
* @param knownState The known state for the virtual directory
* @param parentPath The parent path for the virtual directory,
* should be "/" if the virtual directory is a
* data source.
*/
public LocalFilesDataSource(SleuthkitCase db, long objId, long dataSourceObjectId, String deviceId, String name, TskData.TSK_FS_NAME_TYPE_ENUM dirType, TskData.TSK_FS_META_TYPE_ENUM metaType, TskData.TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags, String timezone, String md5Hash, String sha256Hash, String sha1Hash, TskData.FileKnown knownState, String parentPath) {
super(db, objId, dataSourceObjectId, null, name, dirType, metaType, dirFlag, metaFlags, md5Hash, sha256Hash, sha1Hash, knownState, parentPath);
this.objectId = objId;
this.deviceId = deviceId;
this.timezone = timezone;
}
/**
* Returns the VirtualDirectory instance. /deprecated LocalFilesDataSource
* is already a VirtualDirectory.
*
* @return This object.
*
* @deprecated LocalFilesDataSource is already a VirtualDirectory.
*/
@Deprecated
public VirtualDirectory getRootDirectory() {
return this;
}
/**
* Gets the ASCII-printable identifier for the device associated with the
* data source. This identifier is intended to be unique across multiple
* cases (e.g., a UUID).
*
* @return The device id.
*/
@Override
public String getDeviceId() {
return deviceId;
}
/**
* Gets the time zone that was used to process the data source.
*
* @return The time zone.
*/
@Override
public String getTimeZone() {
return timezone;
}
/**
* Set the name for this data source.
*
* @param newName The new name for the data source
*
* @throws TskCoreException Thrown if an error occurs while updating the database
*/
@Override
public void setDisplayName(String newName) throws TskCoreException {
this.getSleuthkitCase().setFileName(newName, objectId);
}
/**
* Gets the size of the contents of the data source in bytes. This size can
* change as archive files within the data source are expanded, files are
* carved, etc., and is different from the size of the data source as
* returned by Content.getSize, which is the size of the data source as a
* file.
*
* @param sleuthkitCase The sleuthkit case instance from which to make calls
* to the database.
*
* @return The size in bytes.
*
* @throws TskCoreException Thrown when there is an issue trying to retrieve
* data from the database.
*/
@Override
public long getContentSize(SleuthkitCase sleuthkitCase) throws TskCoreException {
return getContentSize(sleuthkitCase, objectId);
}
/**
* Gets the size of the contents of the data source in bytes given a data
* source object ID. This size can change as archive files within the data
* source are expanded, files are carved, etc., and is different from the
* size of the data source as returned by Content.getSize, which is the size
* of the data source as a file.
*
* @param sleuthkitCase The sleuthkit case instance from which to make calls
* to the database.
*
* @return The size in bytes.
*
* @throws TskCoreException Thrown when there is an issue trying to retrieve
* data from the database.
*/
static long getContentSize(SleuthkitCase sleuthkitCase, long dataSourceObjId) throws TskCoreException {
SleuthkitCase.CaseDbConnection connection;
Statement statement = null;
ResultSet resultSet = null;
long contentSize = 0;
connection = sleuthkitCase.getConnection();
try {
statement = connection.createStatement();
resultSet = connection.executeQuery(statement, "SELECT SUM (size) FROM tsk_files WHERE tsk_files.data_source_obj_id = " + dataSourceObjId);
if (resultSet.next()) {
contentSize = resultSet.getLong("sum");
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("There was a problem while querying the database for size data for object ID %d.", dataSourceObjId), ex);
} finally {
closeResultSet(resultSet);
closeStatement(statement);
connection.close();
}
return contentSize;
}
@Override
public String getUniquePath() throws TskCoreException {
return "/" + getName();
}
/**
* Sets the acquisition details field in the case database.
*
* @param details The acquisition details
*
* @throws TskCoreException Thrown if the data can not be written
*/
@Override
public void setAcquisitionDetails(String details) throws TskCoreException {
getSleuthkitCase().setAcquisitionDetails(this, details);
}
/**
* Sets the acquisition tool details such as its name, version number and
* any settings used during the acquisition to acquire data.
*
* @param name The name of the acquisition tool. May be NULL.
* @param version The acquisition tool version number. May be NULL.
* @param settings The settings used by the acquisition tool. May be NULL.
*
* @throws TskCoreException Thrown if the data can not be written
*/
@Override
public void setAcquisitionToolDetails(String name, String version, String settings) throws TskCoreException {
getSleuthkitCase().setAcquisitionToolDetails(this, name, version, settings);
}
/**
* Gets the acquisition details field from the case database.
*
* @return The acquisition details
*
* @throws TskCoreException Thrown if the data can not be read
*/
@Override
public String getAcquisitionDetails() throws TskCoreException {
return getSleuthkitCase().getAcquisitionDetails(this);
}
/**
* Gets the acquisition tool settings field from the case database.
*
* @return The acquisition tool settings. May be Null if not set.
*
* @throws TskCoreException Thrown if the data can not be read
*/
@Override
public String getAcquisitionToolSettings() throws TskCoreException {
return getSleuthkitCase().getDataSourceInfoString(this, "acquisition_tool_settings");
}
/**
* Gets the acquisition tool name field from the case database.
*
* @return The acquisition tool name. May be Null if not set.
*
* @throws TskCoreException Thrown if the data can not be read
*/
public String getAcquisitionToolName() throws TskCoreException {
return getSleuthkitCase().getDataSourceInfoString(this, "acquisition_tool_name");
}
/**
* Gets the acquisition tool version field from the case database.
*
* @return The acquisition tool version. May be Null if not set.
*
* @throws TskCoreException Thrown if the data can not be read
*/
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().getHostByDataSource(this);
}
return host;
}
/**
* Gets the added date field from the case database.
*
* @return The date time when the image was added in epoch seconds.
*
* @throws TskCoreException Thrown if the data can not be read
*/
public Long getDateAdded() throws TskCoreException {
return getSleuthkitCase().getDataSourceInfoLong(this, "added_date_time");
}
/**
* Close a ResultSet.
*
* @param resultSet The ResultSet to be closed.
*/
private static void closeResultSet(ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException ex) {
LOGGER.log(Level.SEVERE, "Error closing ResultSet", ex); //NON-NLS
}
}
}
/**
* Close a Statement.
*
* @param statement The Statement to be closed.
*/
private static void closeStatement(Statement statement) {
if (statement != null) {
try {
statement.close();
} catch (SQLException ex) {
LOGGER.log(Level.SEVERE, "Error closing Statement", ex); //NON-NLS
}
}
}
/**
* Accepts a content visitor (Visitor design pattern).
*
* @param <T> The type returned by the visitor.
* @param visitor A ContentVisitor supplying an algorithm to run using this
* virtual directory as input.
*
* @return The output of the algorithm.
*/
@Override
public <T> T accept(ContentVisitor<T> visitor) {
return visitor.visit(this);
}
/**
* Accepts a Sleuthkit item visitor (Visitor design pattern).
*
* @param <T> The type returned by the visitor.
* @param visitor A SleuthkitItemVisitor supplying an algorithm to run using
* this virtual directory as input.
*
* @return The output of the algorithm.
*/
@Override
public <T> T accept(SleuthkitItemVisitor<T> visitor) {
return visitor.visit(this);
}
/**
* Constructs a local/logical files and/or directories data source.
*
* @param db The case database.
* @param objId The object id of the virtual directory.
* @param dataSourceObjectId The object id of the data source for the
* virtual directory; same as objId if the virtual
* directory is a data source.
* @param name The name of the virtual directory.
* @param dirType The TSK_FS_NAME_TYPE_ENUM for the virtual
* directory.
* @param deviceId The device ID for the data source.
* @param metaType The TSK_FS_META_TYPE_ENUM for the virtual
* directory.
* @param dirFlag The TSK_FS_META_TYPE_ENUM for the virtual
* directory.
* @param metaFlags The meta flags for the virtual directory.
* @param timezone The timezone for the data source.
* @param md5Hash The MD5 hash for the virtual directory.
* @param knownState The known state for the virtual directory
* @param parentPath The parent path for the virtual directory,
* should be "/" if the virtual directory is a
* data source.
*
* @deprecated Use version with SHA-256 parameter
*/
@Deprecated
public LocalFilesDataSource(SleuthkitCase db, long objId, long dataSourceObjectId, String deviceId, String name, TskData.TSK_FS_NAME_TYPE_ENUM dirType, TskData.TSK_FS_META_TYPE_ENUM metaType, TskData.TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags, String timezone, String md5Hash, TskData.FileKnown knownState, String parentPath) {
this(db, objId, dataSourceObjectId, deviceId, name, dirType, metaType, dirFlag, metaFlags, timezone, md5Hash, null, null, knownState, parentPath);
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2011-2017 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;
/**
* This class is used to abstract a message folder in an email container,
* like an mbox or pst file
*/
class MessageFolder {
private final long srcObjID;
private final String pathName;
private boolean hasSubfolders;
public MessageFolder(String pathName, long objID) {
this(pathName, objID, false);
}
public MessageFolder(String pathName, long objID, boolean hasSubfolders) {
this.pathName = pathName;
this.srcObjID = objID;
this.hasSubfolders = hasSubfolders;
}
public String getName() {
return this.pathName;
}
public long getSrcOBjID() {
return this.srcObjID;
}
public synchronized boolean hasSubfolders() {
return this.hasSubfolders;
}
public synchronized void setHasSubfolders(boolean hasSubFolders) {
this.hasSubfolders = hasSubFolders;
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2013 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;
import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class to hold information from OS Info artifacts
*/
public class OSInfo {
private final List<BlackboardArtifact> artifacts;
private final Map<Integer, String> attributeMap;
private final boolean isBackup;
private final boolean haveFsContent;
private final long fileSystemId;
private final boolean haveParentId;
private final long parentObjId;
public OSInfo() {
artifacts = new ArrayList<BlackboardArtifact>();
attributeMap = new HashMap<Integer, String>();
isBackup = false;
fileSystemId = 0;
haveFsContent = false;
parentObjId = 0;
haveParentId = false;
}
/**
* Initialize an OSInfo object
*
* @param a_art - OSInfo artifact associated with one registry hive
* @param a_isBackup - True if the registry hive was found in a
* "RegBack" directory
* @param a_fileSystemId - File system ID for FS containing the registry
* hive
* @param a_parent - Parent directory containing the registry hive.
* Can be null
*
* @throws TskCoreException
*/
public OSInfo(BlackboardArtifact a_art, boolean a_isBackup, long a_fileSystemId, Content a_parent) throws TskCoreException {
artifacts = new ArrayList<BlackboardArtifact>();
artifacts.add(a_art);
isBackup = a_isBackup;
fileSystemId = a_fileSystemId;
haveFsContent = true;
attributeMap = new HashMap<Integer, String>();
for (BlackboardAttribute attr : a_art.getAttributes()) {
attributeMap.put(attr.getAttributeType().getTypeID(), attr.getValueString());
}
if (a_parent != null) {
parentObjId = a_parent.getId();
haveParentId = true;
} else {
parentObjId = 0;
haveParentId = false;
}
}
/**
* Initialize an OSInfo object (without file system information)
*
* @param a_art - OSInfo artifact associated with one registry hive
* @param a_isBackup - True if the registry hive was found in a "RegBack"
* directory
* @param a_parent - Parent directory containing the registry hive. Can be
* null
*
* @throws TskCoreException
*/
public OSInfo(BlackboardArtifact a_art, boolean a_isBackup, Content a_parent) throws TskCoreException {
artifacts = new ArrayList<BlackboardArtifact>();
artifacts.add(a_art);
isBackup = a_isBackup;
fileSystemId = 0;
haveFsContent = false;
if (a_parent != null) {
parentObjId = a_parent.getId();
haveParentId = true;
} else {
parentObjId = 0;
haveParentId = false;
}
attributeMap = new HashMap<Integer, String>();
for (BlackboardAttribute attr : a_art.getAttributes()) {
attributeMap.put(attr.getAttributeType().getTypeID(), attr.getValueString());
}
}
/**
* Determine whether two OSInfo objects should be combined.
*
* @param a_osInfo - the OSInfo object to compare against
*
* @return
*/
public boolean matches(OSInfo a_osInfo) {
// Check if the two are in the same directory.
// OSInfo is only dependant on SYSTEM and SOFTWARE, which should always be in the same directory
// on the file system.
if (haveParentId && a_osInfo.haveParentId) {
return (parentObjId == a_osInfo.parentObjId);
}
// If we don't have a parent directory, just see if they're on the same file system,
// and both have the same backup status.
if (haveFsContent && a_osInfo.haveFsContent) {
return ((a_osInfo.isBackup == isBackup) && (a_osInfo.fileSystemId == fileSystemId));
}
return false;
}
/**
* Combine the attribute map for two OSInfo objects.
*
* @param a_osInfo - The OSInfo object to combine with
*/
public void combine(OSInfo a_osInfo) {
artifacts.addAll(a_osInfo.artifacts);
attributeMap.putAll(a_osInfo.attributeMap);
}
public List<BlackboardArtifact> getArtifacts() {
return artifacts;
}
public boolean haveFileSystem() {
return haveFsContent;
}
public long getFileSystemId() {
return fileSystemId;
}
public boolean getIsBackup() {
return isBackup;
}
/**
* Generic method to get an OSInfo attribute value by ATTRIBUTE_TYPE.
*
* @param attrType - the attribute to get
*
* @return
*/
public String getAttributeValue(ATTRIBUTE_TYPE attrType) {
if (attributeMap.containsKey(attrType.getTypeID())) {
return attributeMap.get(attrType.getTypeID());
}
return "";
}
/*
* Dedicated getters for the most common attributes.
*/
public String getCompName() {
return getAttributeValue(ATTRIBUTE_TYPE.TSK_NAME);
}
public String getProcessorArchitecture() {
return getAttributeValue(ATTRIBUTE_TYPE.TSK_PROCESSOR_ARCHITECTURE);
}
public String getDomain() {
return getAttributeValue(ATTRIBUTE_TYPE.TSK_DOMAIN);
}
public String getOSName() {
return getAttributeValue(ATTRIBUTE_TYPE.TSK_PROG_NAME);
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2013 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;
import java.util.List;
import java.util.ArrayList;
import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
/**
* Utility class to combine information from various OS info artifacts into
* fewer objects.
*/
public class OSUtility {
private OSUtility() {
}
/**
* Get all non-backup OSInfo data
*
* @param skCase - Have to pass this in because we don't have access to the
* normal method
*
* @return List of OSInfo objects
*
* @throws TskCoreException
*/
public static List<OSInfo> getOSInfo(SleuthkitCase skCase) throws TskCoreException {
return getOSInfoInternal(skCase, false, false, 0);
}
/**
* Get OSInfo from the same file system as the given object. Will not
* include backups.
*
* @param skCase - Have to pass this in because we don't have access to the
* normal method
* @param fsc - FsContent from the same file system we want the OS
* information from
*
* @return - List of OSInfo objects
*
* @throws TskCoreException
*/
public static List<OSInfo> getOSInfo(SleuthkitCase skCase, FsContent fsc) throws TskCoreException {
return getOSInfoInternal(skCase, false, true, fsc.getFileSystemId());
}
/**
* Creates a list of all OS Info data on any file system, including the
* backups
*
* @param skCase - Have to pass this in because we don't have access to the
* normal method
*
* @return - List of OSInfo objects
*
* @throws TskCoreException
*/
public static List<OSInfo> getAllOSInfo(SleuthkitCase skCase) throws TskCoreException {
return getOSInfoInternal(skCase, true, false, 0);
}
/**
* Internal method to find and combine the requested OS Info data.
*
* @param skCase - Have to pass this in because we don't have access
* to the normal method
* @param includeBackups - true if we should include registry data found in
* "RegBack"
* @param restrictFs - true if an file system id is being provided to
* match against
* @param fsId - the file system ID that the registry hives must
* be on (if restrictFs is set)
*
* @return - List of OSInfo objects
*
* @throws TskCoreException
*/
private static List<OSInfo> getOSInfoInternal(SleuthkitCase skCase, boolean includeBackups,
boolean restrictFs, long fsId) throws TskCoreException {
List<OSInfo> infoList = new ArrayList<OSInfo>();
// Get all OS_INFO artifacts for this case
ArrayList<BlackboardArtifact> results = skCase.getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_INFO);
for (BlackboardArtifact art : results) {
AbstractFile file = skCase.getAbstractFileById(art.getObjectID());
if (file == null) {
continue;
}
// Check if we're in a backup directory. If so and we're not including backups,
// skip this artifact.
boolean isBackup = file.getParentPath().contains("RegBack");
if (isBackup && (!includeBackups)) {
continue;
}
// FsContent allows us to get the file system ID.
if (file instanceof FsContent) {
FsContent fsc = (FsContent) file;
// If we're restricting the file system, skip any that don't match
if (restrictFs && (fsId != fsc.getFileSystemId())) {
continue;
}
// Make a new OSInfo object
OSInfo newInfo = new OSInfo(art, isBackup, fsc.getFileSystemId(), file.getParent());
// Attempt to merge it with an existing object
boolean mergedInfo = false;
for (OSInfo info : infoList) {
if (info.matches(newInfo)) {
info.combine(newInfo);
mergedInfo = true;
break;
}
}
// If nothing matched, add the new object to the list
if (!mergedInfo) {
infoList.add(newInfo);
}
} else if (!restrictFs) {
// Make a new OSInfo object (no file system ID in this case)
OSInfo newInfo = new OSInfo(art, isBackup, file.getParent());
// Attempt to merge it with an existing object
boolean mergedInfo = false;
for (OSInfo info : infoList) {
if (info.matches(newInfo)) {
info.combine(newInfo);
mergedInfo = true;
break;
}
}
// If nothing matched, add the new object to the list
if (!mergedInfo) {
infoList.add(newInfo);
}
} else {
// If we're limiting the search to one FS, don't include any
// data we can't find the FS for
}
}
return infoList;
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2020-2021 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;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
/**
* Abstracts an OS user account. OS Accounts have a scope, which is defined by
* their parent OsAccountRealm.
*
* An OS user account may own files and (some) artifacts.
*
* OsAcounts can be created with minimal data and updated as more is learned.
* Caller must call update() to save any new data.
*/
public final class OsAccount extends AbstractContent {
private static final ResourceBundle bundle = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle");
final static Long NO_ACCOUNT = null;
final static String NO_OWNER_ID = null;
private final SleuthkitCase sleuthkitCase;
private final long osAccountObjId; // Object ID within the database
private final long realmId; // realm where the account exists in (could be local or domain scoped)
private final String loginName; // user login name - may be null
private final String addr; // a unique user sid/uid, may be null
private final String signature; // This exists only to prevent duplicates.
// Together realm_id & signature must be unique for each account.
// It is either addr if addr is defined,
// or the login_name if login_name is defined.
private final String fullName; // full name, may be null
private final OsAccountType osAccountType;
private final OsAccountStatus osAccountStatus;
private final OsAccountDbStatus osAccountDbStatus; // Status of row in the database
private final Long creationTime;
private List<OsAccountAttribute> osAccountAttributes = null;
/**
* Encapsulates status of an account - whether is it active or disabled or
* deleted.
*/
public enum OsAccountStatus {
UNKNOWN(0, bundle.getString("OsAccountStatus.Unknown.text")),
ACTIVE(1, bundle.getString("OsAccountStatus.Active.text")),
DISABLED(2, bundle.getString("OsAccountStatus.Disabled.text")),
@Deprecated /** Use NON_EXISTENT **/
DELETED(3, bundle.getString("OsAccountStatus.Deleted.text")),
NON_EXISTENT(4, bundle.getString("OsAccountStatus.NonExistent.text"));
private final int id;
private final String name;
OsAccountStatus(int id, String name) {
this.id = id;
this.name = name;
}
/**
* Get account status id.
*
* @return Account status id.
*/
public int getId() {
return id;
}
/**
* Get the account status enum name.
*
* @return
*/
public String getName() {
return name;
}
/**
* Gets account status enum from id.
*
* @param statusId Id to look for.
*
* @return Account status enum.
*/
public static OsAccountStatus fromID(int statusId) {
for (OsAccountStatus statusType : OsAccountStatus.values()) {
if (statusType.ordinal() == statusId) {
return statusType;
}
}
return null;
}
}
/**
* Encapsulates status of OsAccount row. OsAccounts that are not "Active"
* are generally invisible - they will not be returned by any queries on the
* string fields.
*/
enum OsAccountDbStatus {
ACTIVE(0, "Active"),
MERGED(1, "Merged"),
DELETED(2, "Deleted");
private final int id;
private final String name;
OsAccountDbStatus(int id, String name) {
this.id = id;
this.name = name;
}
int getId() {
return id;
}
String getName() {
return name;
}
static OsAccountDbStatus fromID(int typeId) {
for (OsAccountDbStatus type : OsAccountDbStatus.values()) {
if (type.ordinal() == typeId) {
return type;
}
}
return null;
}
}
/**
* Encapsulates an account type - whether it's an interactive login account
* or a service account.
*/
public enum OsAccountType {
UNKNOWN(0, bundle.getString("OsAccountType.Unknown.text")),
SERVICE(1, bundle.getString("OsAccountType.Service.text")),
INTERACTIVE(2, bundle.getString("OsAccountType.Interactive.text"));
private final int id;
private final String name;
OsAccountType(int id, String name) {
this.id = id;
this.name = name;
}
/**
* Get account type id.
*
* @return Account type id.
*/
public int getId() {
return id;
}
/**
* Get account type name.
*
* @return Account type name.
*/
public String getName() {
return name;
}
/**
* Gets account type enum from id.
*
* @param typeId Id to look for.
*
* @return Account type enum.
*/
public static OsAccountType fromID(int typeId) {
for (OsAccountType accountType : OsAccountType.values()) {
if (accountType.ordinal() == typeId) {
return accountType;
}
}
return null;
}
}
/**
* Constructs an OsAccount with a realm/username and unique id, and
* signature.
*
* @param sleuthkitCase The SleuthKit case (case database) that contains
* the artifact data.
* @param osAccountobjId Obj id of the account in tsk_objects table.
* @param realmId Realm - defines the scope of this account.
* @param loginName Login name for the account. May be null.
* @param uniqueId An id unique within the realm - a SID or uid. May
* be null, only if login name is not null.
* @param signature A unique signature constructed from realm id and
* loginName or uniqueId.
* @param fullName Full name.
* @param creationTime Account creation time.
* @param accountType Account type.
* @param accountStatus Account status.
* @param dbStatus Status of row in database.
*/
OsAccount(SleuthkitCase sleuthkitCase, long osAccountobjId, long realmId, String loginName, String uniqueId, String signature,
String fullName, Long creationTime, OsAccountType accountType, OsAccountStatus accountStatus, OsAccountDbStatus accountDbStatus) {
super(sleuthkitCase, osAccountobjId, signature);
this.sleuthkitCase = sleuthkitCase;
this.osAccountObjId = osAccountobjId;
this.realmId = realmId;
this.loginName = loginName;
this.addr = uniqueId;
this.signature = signature;
this.fullName = fullName;
this.creationTime = creationTime;
this.osAccountType = accountType;
this.osAccountStatus = accountStatus;
this.osAccountDbStatus = accountDbStatus;
}
/**
* This function is used by OsAccountManger to update the list of OsAccount
* attributes.
*
* @param osAccountAttributes The osAccount attributes that are to be added.
*/
synchronized void setAttributesInternal(List<OsAccountAttribute> osAccountAttributes) {
this.osAccountAttributes = osAccountAttributes;
}
/**
* Get the account Object Id that is unique within the scope of the case.
*
* @return Account
* id.
*/
public long getId() {
return osAccountObjId;
}
/**
* Get the unique identifier for the account, such as UID or SID. The id is
* unique within the account realm.
*
* @return Optional unique identifier.
*/
public Optional<String> getAddr() {
return Optional.ofNullable(addr);
}
/**
* Get the ID for the account realm. Get the Realm via
* OsAccountRealmManager.getRealmByRealmId() NOTE: The realm may get updated
* as more data is parsed, so listen for events to update as needed.
*
* @return
*/
public long getRealmId() {
return realmId;
}
/**
* Get account login name, such as "jdoe"
*
* @return Optional login name.
*/
public Optional<String> getLoginName() {
return Optional.ofNullable(loginName);
}
/**
* Get the account signature.
*
* @return Account signature.
*/
String getSignature() {
return signature;
}
/**
* Get account user full name, such as "John Doe"
*
* @return Optional with full name.
*/
public Optional<String> getFullName() {
return Optional.ofNullable(fullName);
}
/**
* Get account creation time.
*
* @return Optional with account creation time.
*/
public Optional<Long> getCreationTime() {
return Optional.ofNullable(creationTime);
}
/**
* Get account type.
*
* @return Optional with account type.
*/
public Optional<OsAccountType> getOsAccountType() {
return Optional.ofNullable(osAccountType);
}
/**
* Get account status.
*
* @return Optional with account status.
*/
public Optional<OsAccountStatus> getOsAccountStatus() {
return Optional.ofNullable(osAccountStatus);
}
/**
* Get account status in the database.
*
* @return Database account status.
*/
public OsAccountDbStatus getOsAccountDbStatus() {
return osAccountDbStatus;
}
/**
* Get additional account attributes.
*
* @return List of additional account attributes. May return an empty list.
*
* @throws TskCoreException
*/
public synchronized List<OsAccountAttribute> getExtendedOsAccountAttributes() throws TskCoreException {
if (osAccountAttributes == null) {
osAccountAttributes = sleuthkitCase.getOsAccountManager().getOsAccountAttributes(this);
}
return Collections.unmodifiableList(osAccountAttributes);
}
/**
* Return the os account instances.
*
* @return List of all the OsAccountInstances. May return an empty list.
*
* @throws TskCoreException
*/
public synchronized List<OsAccountInstance> getOsAccountInstances() throws TskCoreException {
return sleuthkitCase.getOsAccountManager().getOsAccountInstances(this);
}
/**
* Gets the SleuthKit case database for this account.
*
* @return The SleuthKit case object.
*/
@Override
public SleuthkitCase getSleuthkitCase() {
return sleuthkitCase;
}
@Override
public int read(byte[] buf, long offset, long len) throws TskCoreException {
// No data to read.
return 0;
}
@Override
public void close() {
// nothing to close
}
@Override
public long getSize() {
// No data.
return 0;
}
@Override
public <T> T accept(ContentVisitor<T> v) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public <T> T accept(SleuthkitItemVisitor<T> v) {
return v.visit(this);
}
/**
* Abstracts attributes of an OS account. An attribute may be specific to a
* host, or applicable across all hosts.
*
* As an example, last login time is host specific, whereas last password
* reset date is independent of a host.
*
*/
public final class OsAccountAttribute extends AbstractAttribute {
private final long osAccountObjId; // OS account to which this attribute belongs.
private final Long hostId; // Host to which this attribute applies, may be null
private final Long sourceObjId; // Object id of the source where the attribute was discovered.
/**
* Creates an os account attribute with int value.
*
* @param attributeType Attribute type.
* @param valueInt Int value.
* @param osAccount Account which the attribute pertains to.
* @param host Host on which the attribute applies to. Pass
* Null if the attribute applies to all the hosts
* in the realm.
* @param sourceObj Source where the attribute was found, may be
* null.
*/
public OsAccountAttribute(BlackboardAttribute.Type attributeType, int valueInt, OsAccount osAccount, Host host, Content sourceObj) {
super(attributeType, valueInt);
this.osAccountObjId = osAccount.getId();
this.hostId = (host != null ? host.getHostId() : null);
this.sourceObjId = (sourceObj != null ? sourceObj.getId() : null);
}
/**
* Creates an os account attribute with long value.
*
* @param attributeType Attribute type.
* @param valueLong Long value.
* @param osAccount Account which the attribute pertains to.
* @param host Host on which the attribute applies to. Pass
* Null if it applies across hosts.
* @param sourceObj Source where the attribute was found.
*/
public OsAccountAttribute(BlackboardAttribute.Type attributeType, long valueLong, OsAccount osAccount, Host host, Content sourceObj) {
super(attributeType, valueLong);
this.osAccountObjId = osAccount.getId();
this.hostId = (host != null ? host.getHostId() : null);
this.sourceObjId = (sourceObj != null ? sourceObj.getId() : null);
}
/**
* Creates an os account attribute with double value.
*
* @param attributeType Attribute type.
* @param valueDouble Double value.
* @param osAccount Account which the attribute pertains to.
* @param host Host on which the attribute applies to. Pass
* Null if it applies across hosts.
* @param sourceObj Source where the attribute was found.
*/
public OsAccountAttribute(BlackboardAttribute.Type attributeType, double valueDouble, OsAccount osAccount, Host host, Content sourceObj) {
super(attributeType, valueDouble);
this.osAccountObjId = osAccount.getId();
this.hostId = (host != null ? host.getHostId() : null);
this.sourceObjId = (sourceObj != null ? sourceObj.getId() : null);
}
/**
* Creates an os account attribute with string value.
*
* @param attributeType Attribute type.
* @param valueString String value.
* @param osAccount Account which the attribute pertains to.
* @param host Host on which the attribute applies to. Pass
* Null if applies across hosts.
* @param sourceObj Source where the attribute was found.
*/
public OsAccountAttribute(BlackboardAttribute.Type attributeType, String valueString, OsAccount osAccount, Host host, Content sourceObj) {
super(attributeType, valueString);
this.osAccountObjId = osAccount.getId();
this.hostId = (host != null ? host.getHostId() : null);
this.sourceObjId = (sourceObj != null ? sourceObj.getId() : null);
}
/**
* Creates an os account attribute with byte-array value.
*
* @param attributeType Attribute type.
* @param valueBytes Bytes value.
* @param osAccount Account which the attribute pertains to.
* @param host Host on which the attribute applies to. Pass
* Null if it applies across hosts.
* @param sourceObj Source where the attribute was found.
*/
public OsAccountAttribute(BlackboardAttribute.Type attributeType, byte[] valueBytes, OsAccount osAccount, Host host, Content sourceObj) {
super(attributeType, valueBytes);
this.osAccountObjId = osAccount.getId();
this.hostId = (host != null ? host.getHostId() : null);
this.sourceObjId = (sourceObj != null ? sourceObj.getId() : null);
}
/**
* Constructor to be used when creating an attribute after reading the
* data from the table.
*
* @param attributeType Attribute type.
* @param valueInt Int value.
* @param valueLong Long value.
* @param valueDouble Double value.
* @param valueString String value.
* @param valueBytes Bytes value.
* @param sleuthkitCase Sleuthkit case.
* @param osAccount Account which the attribute pertains to.
* @param host Host on which the attribute applies to. Pass
* Null if it applies across hosts.
* @param sourceObj Source where the attribute was found.
*/
OsAccountAttribute(BlackboardAttribute.Type attributeType, int valueInt, long valueLong, double valueDouble, String valueString, byte[] valueBytes,
SleuthkitCase sleuthkitCase, OsAccount osAccount, Host host, Content sourceObj) {
super(attributeType,
valueInt, valueLong, valueDouble, valueString, valueBytes,
sleuthkitCase);
this.osAccountObjId = osAccount.getId();
this.hostId = (host != null ? host.getHostId() : null);
this.sourceObjId = (sourceObj != null ? sourceObj.getId() : null);
}
/**
* Get the host id for the account attribute.
*
* @return Optional with Host id.
*/
public Optional<Long> getHostId() {
return Optional.ofNullable(hostId);
}
/**
* Get the object id of account to which this attribute applies.
*
* @return Account row id.
*/
public long getOsAccountObjectId() {
return osAccountObjId;
}
/**
* Get the object id of the source where the attribute was found.
*
* @return Object id of source.
*/
public Optional<Long> getSourceObjectId() {
return Optional.ofNullable(sourceObjId);
}
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2021 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;
import java.util.Arrays;
import java.util.Objects;
import java.util.ResourceBundle;
/**
* An OsAccountInstance represents the appearance of a particular OsAccount on a
* particular data source.
*/
public class OsAccountInstance implements Comparable<OsAccountInstance> {
private static final ResourceBundle bundle = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle");
private final SleuthkitCase skCase;
private final long instanceId;
private final long accountId;
private final long dataSourceId;
private final OsAccountInstanceType instanceType;
private OsAccount account;
private DataSource dataSource;
/**
* Constructs a representation of an OS account instance.
*
*
* @param skCase The case database.
* @param instanceId The instance ID.
* @param account The OS account of which this object is an
* instance.
* @param dataSourceObjId The object ID of the data source where the
* instance was found.
* @param instanceType The instance type.
*/
OsAccountInstance(SleuthkitCase skCase, long instanceId, OsAccount account, long dataSourceId, OsAccountInstanceType instanceType) {
this(skCase, instanceId, account.getId(), dataSourceId, instanceType);
this.account = account;
}
/**
* Constructs a representation of an OS account instance.
*
* @param skCase The case database.
* @param instanceId The instance ID.
* @param accountObjId The object ID of the OS account of which this
* object is an instance.
* @param dataSourceObjId The object ID of the data source where the
* instance was found.
* @param instanceType The instance type.
*/
OsAccountInstance(SleuthkitCase skCase, long instanceId, long accountObjId, long dataSourceObjId, OsAccountInstanceType instanceType) {
this.skCase = skCase;
this.instanceId = instanceId;
this.accountId = accountObjId;
this.dataSourceId = dataSourceObjId;
this.instanceType = instanceType;
}
/**
* Gets the instance ID of this OS account instance.
*
* @return The instance ID.
*/
public long getInstanceId() {
return instanceId;
}
/**
* Returns the OsAccount object for this instance.
*
* @return The OsAccount object.
*
* @throws TskCoreException Exception thrown if there is an error querying
* the case database.
*/
public OsAccount getOsAccount() throws TskCoreException {
if (account == null) {
try {
account = skCase.getOsAccountManager().getOsAccountByObjectId(accountId);
} catch (TskCoreException ex) {
throw new TskCoreException(String.format("Failed to get OsAccount for id %d", accountId), ex);
}
}
return account;
}
/**
* Returns the data source for this account instance.
*
* @return Return the data source instance.
*
* @throws TskCoreException
*/
public DataSource getDataSource() throws TskCoreException {
if (dataSource == null) {
try {
dataSource = skCase.getDataSource(dataSourceId);
} catch (TskDataException ex) {
throw new TskCoreException(String.format("Failed to get DataSource for id %d", dataSourceId), ex);
}
}
return dataSource;
}
/**
* Returns the type for this OsAccount instance.
*
* @return
*/
public OsAccountInstanceType getInstanceType() {
return instanceType;
}
/**
* Return the dataSourceId value.
*
* @return Id of the instance data source.
*/
private long getDataSourceId() {
return dataSourceId;
}
@Override
public int compareTo(OsAccountInstance other) {
if (equals(other)) {
return 0;
}
if (dataSourceId != other.getDataSourceId()) {
return Long.compare(dataSourceId, other.getDataSourceId());
}
return Long.compare(accountId, other.accountId);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final OsAccountInstance other = (OsAccountInstance) obj;
if(this.instanceId != other.instanceId) {
return false;
}
if (this.accountId != other.accountId) {
return false;
}
if(this.instanceType != other.instanceType) {
return false;
}
return this.dataSourceId == other.getDataSourceId();
}
@Override
public int hashCode() {
int hash = 7;
hash = 67 * hash + Objects.hashCode(this.instanceId);
hash = 67 * hash + Objects.hashCode(this.dataSourceId);
hash = 67 * hash + Objects.hashCode(this.accountId);
hash = 67 * hash + Objects.hashCode(this.instanceType);
return hash;
}
/**
* Describes what is known about what an OS Account did on an specific host.
*
* Note: lower ordinal value is more significant than higher ordinal value.
* Order of significance: LAUNCHED > ACCESSED > REFERENCED.
*/
public enum OsAccountInstanceType {
LAUNCHED(0, bundle.getString("OsAccountInstanceType.Launched.text"), bundle.getString("OsAccountInstanceType.Launched.descr.text")), // user had an interactive session or launched a program on the host
ACCESSED(1, bundle.getString("OsAccountInstanceType.Accessed.text"), bundle.getString("OsAccountInstanceType.Accessed.descr.text")), // user accesed a resource/file for read/write. Could have been via a service (such as a file share) or a SID on a random file from an unknown location. NOTE: Because Windows event logs do not show if an authentication was for an interactive login or accessing a service, we mark a user as ACCESSED based on authentication. They become LAUNCHED if we have proof of them starting a program or getting an interactive login.
REFERENCED(2, bundle.getString("OsAccountInstanceType.Referenced.text"), bundle.getString("OsAccountInstanceType.Referenced.descr.text")); // user was referenced in a log file (e.g. in a event log) or registry, but there was no evidence of activity or ownership on the host. Examples include an account that was never used and entries on a log server.
private final int id;
private final String name;
private final String description;
OsAccountInstanceType(int id, String name, String description) {
this.id = id;
this.name = name;
this.description = description;
}
/**
* Get account instance type id.
*
* @return Account instance type id.
*/
public int getId() {
return id;
}
/**
* Get account instance type name.
*
* @return Account instance type name.
*/
public String getName() {
return name;
}
/**
* Get account instance type description.
*
* @return Account instance type description.
*/
public String getDescription() {
return description;
}
/**
* Gets account instance type enum from id.
*
* @param typeId Id to look for.
*
* @return Account instance type enum.
*/
public static OsAccountInstanceType fromID(int typeId) {
for (OsAccountInstanceType statusType : OsAccountInstanceType.values()) {
if (statusType.ordinal() == typeId) {
return statusType;
}
}
return null;
}
/**
* Gets account instance type enum from name.
*
* @param name Name to look for.
*
* @return Account instance type enum, null if no match is found.
*/
public static OsAccountInstanceType fromString(String name) {
return Arrays.stream(values())
.filter(val -> val.getName().equals(name))
.findFirst().orElse(null);
}
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2020-2022 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;
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;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.stream.Collectors;
import org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE;
import org.sleuthkit.datamodel.OsAccount.OsAccountStatus;
import org.sleuthkit.datamodel.OsAccount.OsAccountType;
import org.sleuthkit.datamodel.OsAccount.OsAccountAttribute;
import org.sleuthkit.datamodel.OsAccountRealmManager.OsRealmUpdateResult;
import org.sleuthkit.datamodel.OsAccountRealmManager.OsRealmUpdateStatus;
import org.sleuthkit.datamodel.SleuthkitCase.CaseDbConnection;
import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
import org.sleuthkit.datamodel.TskEvent.OsAccountsUpdatedTskEvent;
import static org.sleuthkit.datamodel.WindowsAccountUtils.isWindowsWellKnownSid;
import static org.sleuthkit.datamodel.WindowsAccountUtils.getWindowsWellKnownSidFullName;
/**
* Responsible for creating/updating/retrieving the OS accounts for files and
* artifacts.
*/
public final class OsAccountManager {
private final SleuthkitCase db;
private final Object osAcctInstancesCacheLock;
private final NavigableMap<OsAccountInstanceKey, OsAccountInstance> osAccountInstanceCache;
/**
* Construct a OsUserManager for the given SleuthkitCase.
*
* @param skCase The SleuthkitCase
*
*/
OsAccountManager(SleuthkitCase skCase) {
db = skCase;
osAcctInstancesCacheLock = new Object();
osAccountInstanceCache = new ConcurrentSkipListMap<>();
}
/**
* Creates an OS account with given unique id and given realm id. If an
* account already exists with the given id, then the existing OS account is
* returned.
*
* @param uniqueAccountId Account sid/uid.
* @param realm Account realm.
*
* @return OsAccount.
*
* @throws TskCoreException If there is an error in creating the OSAccount.
*
*/
OsAccount newOsAccount(String uniqueAccountId, OsAccountRealm realm) throws TskCoreException {
// ensure unique id is provided
if (Strings.isNullOrEmpty(uniqueAccountId)) {
throw new TskCoreException("Cannot create OS account with null uniqueId.");
}
if (realm == null) {
throw new TskCoreException("Cannot create OS account without a realm.");
}
CaseDbTransaction trans = db.beginTransaction();
try {
// try to create account
try {
OsAccount account = newOsAccount(uniqueAccountId, null, realm, OsAccount.OsAccountStatus.UNKNOWN, trans);
trans.commit();
trans = null;
return account;
} catch (SQLException ex) {
// Close the transaction before moving on
trans.rollback();
trans = null;
// Create may fail if an OsAccount already exists.
Optional<OsAccount> osAccount = this.getOsAccountByAddr(uniqueAccountId, realm);
if (osAccount.isPresent()) {
return osAccount.get();
}
// create failed for some other reason, throw an exception
throw new TskCoreException(String.format("Error creating OsAccount with uniqueAccountId = %s in realm id = %d", uniqueAccountId, realm.getRealmId()), ex);
}
} finally {
if (trans != null) {
trans.rollback();
}
}
}
/**
* Creates an OS account with Windows-specific data. If an account already
* exists with the given id or realm/login, then the existing OS account is
* returned.
*
* If the account realm already exists, but is missing the address or the
* realm name, the realm is updated.
*
* @param sid Account sid/uid, can be null if loginName is
* supplied.
* @param loginName Login name, can be null if sid is supplied.
* @param realmName Realm within which the accountId or login name is
* unique. Can be null if sid is supplied.
* @param referringHost Host referring the account.
* @param realmScope Realm scope.
*
* @return OsAccount.
*
* @throws TskCoreException If there is an error in
* creating the OSAccount.
* @throws OsAccountManager.NotUserSIDException If the given SID is not a
* user SID.
*
*/
public OsAccount newWindowsOsAccount(String sid, String loginName, String realmName, Host referringHost, OsAccountRealm.RealmScope realmScope) throws TskCoreException, NotUserSIDException {
if (realmScope == null) {
throw new TskCoreException("RealmScope cannot be null. Use UNKNOWN if scope is not known.");
}
if (referringHost == null) {
throw new TskCoreException("A referring host is required to create an account.");
}
// ensure at least one of the two is supplied - a non-null unique id or a login name
if ((StringUtils.isBlank(sid) || sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID))
&& StringUtils.isBlank(loginName)) {
throw new TskCoreException("Cannot create OS account with both uniqueId and loginName as null.");
}
// Realm name is required if the sid is null.
if ((StringUtils.isBlank(sid) || sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID))
&& StringUtils.isBlank(realmName)) {
throw new TskCoreException("Realm name or SID is required to create a Windows account.");
}
if (!StringUtils.isBlank(sid) && !sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID) && !WindowsAccountUtils.isWindowsUserSid(sid)) {
throw new OsAccountManager.NotUserSIDException(String.format("SID = %s is not a user SID.", sid));
}
// If no SID is given and the given realm/login names is a well known account, get and use the well known SID
if (StringUtils.isBlank(sid)
&& !StringUtils.isBlank(loginName) && !StringUtils.isBlank(realmName)
&& WindowsAccountUtils.isWindowsWellKnownAccountName(loginName, realmName)) {
sid = WindowsAccountUtils.getWindowsWellKnownAccountSid(loginName, realmName);
}
OsRealmUpdateResult realmUpdateResult;
Optional<OsAccountRealm> anotherRealmWithSameName = Optional.empty();
Optional<OsAccountRealm> anotherRealmWithSameAddr = Optional.empty();
// get the realm for the account, and update it if it is missing addr or name.
OsAccountRealm realm = null;
try (CaseDbConnection connection = db.getConnection()) {
realmUpdateResult = db.getOsAccountRealmManager().getAndUpdateWindowsRealm(sid, realmName, referringHost, connection);
Optional<OsAccountRealm> realmOptional = realmUpdateResult.getUpdatedRealm();
if (realmOptional.isPresent()) {
realm = realmOptional.get();
if (realmUpdateResult.getUpdateStatus() == OsRealmUpdateStatus.UPDATED) {
// Check if update of the realm triggers a merge with any other realm,
// say another realm with same name but no SID, or same SID but no name
//1. Check if there is any OTHER realm with the same name, same host but no addr
anotherRealmWithSameName = db.getOsAccountRealmManager().getAnotherRealmByName(realmOptional.get(), realmName, referringHost, connection);
// 2. Check if there is any OTHER realm with same addr and host, but NO name
anotherRealmWithSameAddr = db.getOsAccountRealmManager().getAnotherRealmByAddr(realmOptional.get(), realmName, referringHost, connection);
}
}
}
if (null == realm) {
// realm was not found, create it.
realm = db.getOsAccountRealmManager().newWindowsRealm(sid, realmName, referringHost, realmScope);
} else if (realmUpdateResult.getUpdateStatus() == OsRealmUpdateStatus.UPDATED) {
// if the realm already existed and was updated, and there are other realms with same name or addr that should now be merged into the updated realm
if (anotherRealmWithSameName.isPresent() || anotherRealmWithSameAddr.isPresent()) {
CaseDbTransaction trans = this.db.beginTransaction();
try {
if (anotherRealmWithSameName.isPresent()) {
db.getOsAccountRealmManager().mergeRealms(anotherRealmWithSameName.get(), realm, trans);
}
if (anotherRealmWithSameAddr.isPresent()) {
db.getOsAccountRealmManager().mergeRealms(anotherRealmWithSameAddr.get(), realm, trans);
}
trans.commit();
} catch (TskCoreException ex) {
trans.rollback();
throw ex; // rethrow
}
}
}
return newWindowsOsAccount(sid, loginName, realm);
}
/**
* Creates an OS account with Windows-specific data. If an account already
* exists with the given id or realm/login, then the existing OS account is
* returned.
*
* @param sid Account sid/uid, can be null if loginName is supplied.
* @param loginName Login name, can be null if sid is supplied.
* @param realm The associated realm.
*
* @return OsAccount.
*
* @throws TskCoreException If there is an error in
* creating the OSAccount.
* @throws OsAccountManager.NotUserSIDException If the given SID is not a
* user SID.
*
*/
public OsAccount newWindowsOsAccount(String sid, String loginName, OsAccountRealm realm) throws TskCoreException, NotUserSIDException {
// ensure at least one of the two is supplied - a non-null unique id or a login name
if ((StringUtils.isBlank(sid) || sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID))
&& StringUtils.isBlank(loginName)) {
throw new TskCoreException("Cannot create OS account with both uniqueId and loginName as null.");
}
if (!StringUtils.isBlank(sid) && !sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID) && !WindowsAccountUtils.isWindowsUserSid(sid)) {
throw new OsAccountManager.NotUserSIDException(String.format("SID = %s is not a user SID.", sid));
}
// If the login name is well known, we use the well known english name.
String resolvedLoginName = WindowsAccountUtils.toWellknownEnglishLoginName(loginName);
CaseDbTransaction trans = db.beginTransaction();
try {
// try to create account
try {
String uniqueId = (!StringUtils.isBlank(sid) && !sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID)) ? sid : null;
if (!StringUtils.isBlank(sid) && !sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID) && isWindowsWellKnownSid(sid)) {
// if the SID is a Windows well known SID, then prefer to use the default well known login name
String wellKnownLoginName = WindowsAccountUtils.getWindowsWellKnownSidLoginName(sid);
if (!StringUtils.isEmpty(wellKnownLoginName)) {
resolvedLoginName = wellKnownLoginName;
}
}
OsAccount account = newOsAccount(uniqueId, resolvedLoginName, realm, OsAccount.OsAccountStatus.UNKNOWN, trans);
// If the SID indicates a special windows account, then set its full name.
if (!StringUtils.isBlank(sid) && !sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID) && isWindowsWellKnownSid(sid)) {
String fullName = getWindowsWellKnownSidFullName(sid);
if (StringUtils.isNotBlank(fullName)) {
OsAccountUpdateResult updateResult = updateStandardOsAccountAttributes(account, fullName, null, null, null, trans);
if (updateResult.getUpdatedAccount().isPresent()) {
account = updateResult.getUpdatedAccount().get();
}
}
}
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(sid)) {
osAccount = getOsAccountByAddr(sid, realm);
if (osAccount.isPresent()) {
return osAccount.get();
}
}
// search by loginName
if (!Strings.isNullOrEmpty(resolvedLoginName)) {
osAccount = getOsAccountByLoginName(resolvedLoginName, realm);
if (osAccount.isPresent()) {
return osAccount.get();
}
}
// create failed for some other reason, throw an exception
throw new TskCoreException(String.format("Error creating OsAccount with sid = %s, loginName = %s, realm = %s, referring host = %s",
(sid != null) ? sid : "Null",
(resolvedLoginName != null) ? resolvedLoginName : "Null",
(!realm.getRealmNames().isEmpty()) ? realm.getRealmNames().get(0) : "Null",
realm.getScopeHost().isPresent() ? realm.getScopeHost().get().getName() : "Null"), ex);
}
} finally {
if (trans != null) {
trans.rollback();
}
}
}
/**
* 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.
*
* @param uniqueId Account sid/uid. May be null.
* @param loginName Login name. May be null only if SID is not null.
* @param realm Realm.
* @param accountStatus Account status.
* @param trans Open transaction to use.
*
* @return OS account.
*
* @throws TskCoreException If there is an error creating the account.
*/
private OsAccount newOsAccount(String uniqueId, String loginName, OsAccountRealm realm, OsAccount.OsAccountStatus accountStatus, CaseDbTransaction trans) throws TskCoreException, SQLException {
if (Objects.isNull(realm)) {
throw new TskCoreException("Cannot create an OS Account, realm is NULL.");
}
String signature = getOsAccountSignature(uniqueId, loginName);
OsAccount account;
CaseDbConnection connection = trans.getConnection();
// first create a tsk_object for the OsAccount.
// RAMAN TODO: need to get the correct parent obj id.
// Create an Object Directory parent and used its id.
long parentObjId = 0;
int objTypeId = TskData.ObjectType.OS_ACCOUNT.getObjectType();
long osAccountObjId = db.addObject(parentObjId, objTypeId, connection);
String accountInsertSQL = "INSERT INTO tsk_os_accounts(os_account_obj_id, login_name, realm_id, addr, signature, status)"
+ " VALUES (?, ?, ?, ?, ?, ?)"; // NON-NLS
PreparedStatement preparedStatement = connection.getPreparedStatement(accountInsertSQL, Statement.NO_GENERATED_KEYS);
preparedStatement.clearParameters();
preparedStatement.setLong(1, osAccountObjId);
preparedStatement.setString(2, loginName);
preparedStatement.setLong(3, realm.getRealmId());
preparedStatement.setString(4, uniqueId);
preparedStatement.setString(5, signature);
preparedStatement.setInt(6, accountStatus.getId());
connection.executeUpdate(preparedStatement);
account = new OsAccount(db, osAccountObjId, realm.getRealmId(), loginName, uniqueId, signature,
null, null, null, accountStatus, OsAccount.OsAccountDbStatus.ACTIVE);
trans.registerAddedOsAccount(account);
return account;
}
/**
* Get the OS account with the given unique id.
*
* @param addr Account sid/uid.
* @param host Host for account realm, may be null.
*
* @return Optional with OsAccount, Optional.empty if no matching account is
* found.
*
* @throws TskCoreException If there is an error getting the account.
*/
private Optional<OsAccount> getOsAccountByAddr(String addr, Host host) throws TskCoreException {
try (CaseDbConnection connection = db.getConnection()) {
return getOsAccountByAddr(addr, host, connection);
}
}
/**
* Gets the OS account for the given unique id.
*
* @param uniqueId Account SID/uid.
* @param host Host to match the realm, may be null.
* @param connection Database connection to use.
*
* @return Optional with OsAccount, Optional.empty if no account with
* matching uniqueId is found.
*
* @throws TskCoreException
*/
private Optional<OsAccount> getOsAccountByAddr(String uniqueId, Host host, CaseDbConnection connection) throws TskCoreException {
String whereHostClause = (host == null)
? " 1 = 1 "
: " ( realms.scope_host_id = " + host.getHostId() + " OR realms.scope_host_id IS NULL) ";
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.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"
+ " ON accounts.realm_id = realms.id"
+ " WHERE " + whereHostClause
+ " AND accounts.db_status = " + OsAccount.OsAccountDbStatus.ACTIVE.getId()
+ " AND LOWER(accounts.addr) = LOWER('" + uniqueId + "')";
db.acquireSingleUserCaseReadLock();
try (Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
if (!rs.next()) {
return Optional.empty(); // no match found
} else {
return Optional.of(osAccountFromResultSet(rs));
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting OS account for unique id = %s and host = %s", uniqueId, (host != null ? host.getName() : "null")), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Gets an active OS Account by the realm and unique id.
*
* @param uniqueId Account unique id.
* @param realm Account realm.
*
* @return Optional with OsAccount, Optional.empty, if no user is found with
* matching realm and unique id.
*
* @throws TskCoreException
*/
Optional<OsAccount> getOsAccountByAddr(String uniqueId, OsAccountRealm realm) throws TskCoreException {
String queryString = "SELECT * FROM tsk_os_accounts"
+ " WHERE LOWER(addr) = LOWER('" + uniqueId + "')"
+ " AND db_status = " + OsAccount.OsAccountDbStatus.ACTIVE.getId()
+ " AND realm_id = " + realm.getRealmId();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
if (!rs.next()) {
return Optional.empty(); // no match found
} else {
return Optional.of(osAccountFromResultSet(rs));
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting OS account for realm = %s and uniqueId = %s.", (realm != null) ? realm.getSignature() : "NULL", uniqueId), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Gets a OS Account by the realm and login name.
*
* @param loginName Login name.
* @param realm Account realm.
*
* @return Optional with OsAccount, Optional.empty, if no user is found with
* matching realm and login name.
*
* @throws TskCoreException
*/
Optional<OsAccount> getOsAccountByLoginName(String loginName, OsAccountRealm realm) throws TskCoreException {
String queryString = "SELECT * FROM tsk_os_accounts"
+ " WHERE LOWER(login_name) = LOWER('" + loginName + "')"
+ " AND db_status = " + OsAccount.OsAccountDbStatus.ACTIVE.getId()
+ " AND realm_id = " + realm.getRealmId();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
if (!rs.next()) {
return Optional.empty(); // no match found
} else {
return Optional.of(osAccountFromResultSet(rs));
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting OS account for realm = %s and loginName = %s.", (realm != null) ? realm.getSignature() : "NULL", loginName), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Get the OS Account with the given object id.
*
* @param osAccountObjId Object id for the account.
*
* @return OsAccount.
*
* @throws TskCoreException If there is an error getting the account.
*/
public OsAccount getOsAccountByObjectId(long osAccountObjId) throws TskCoreException {
try (CaseDbConnection connection = this.db.getConnection()) {
return getOsAccountByObjectId(osAccountObjId, connection);
}
}
/**
* Get the OsAccount with the given object id.
*
* @param osAccountObjId Object id for the account.
* @param connection Database connection to use.
*
* @return OsAccount.
*
* @throws TskCoreException If there is an error getting the account.
*/
OsAccount getOsAccountByObjectId(long osAccountObjId, CaseDbConnection connection) throws TskCoreException {
String queryString = "SELECT * FROM tsk_os_accounts"
+ " WHERE os_account_obj_id = " + osAccountObjId;
db.acquireSingleUserCaseReadLock();
try (Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
if (!rs.next()) {
throw new TskCoreException(String.format("No account found with obj id = %d ", osAccountObjId));
} else {
return osAccountFromResultSet(rs);
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting account with obj id = %d ", osAccountObjId), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Records that an OsAccount was used or referenced on a given data source.
* This data is automatically recorded when a file or DataArtifact is
* created.
*
* Use this method to explicitly record the association when: - Parsing
* account information (such as in the registry) because the account may
* already exist in the database, but the account did not create any files.
* Therefore, no instance for it would be automatically created, even though
* you found data about it. - You want to associate more than one OsAccount
* with a DataArtifact. Call this for each OsAccount not specified in
* 'newDataArtifact()'.
*
* This method does nothing if the instance is already recorded.
*
* @param osAccount Account for which an instance needs to be added.
* @param dataSource Data source where the instance is found.
* @param instanceType Instance type.
*
* @return OsAccountInstance Existing or newly created account instance.
*
* @throws TskCoreException If there is an error creating the account
* instance.
*/
public OsAccountInstance newOsAccountInstance(OsAccount osAccount, DataSource dataSource, OsAccountInstance.OsAccountInstanceType instanceType) throws TskCoreException {
if (osAccount == null) {
throw new TskCoreException("Cannot create account instance with null account.");
}
if (dataSource == null) {
throw new TskCoreException("Cannot create account instance with null data source.");
}
// check the cache first
Optional<OsAccountInstance> existingInstance = cachedAccountInstance(osAccount.getId(), dataSource.getId(), instanceType);
if (existingInstance.isPresent()) {
return existingInstance.get();
}
try (CaseDbConnection connection = this.db.getConnection()) {
return newOsAccountInstance(osAccount.getId(), dataSource.getId(), instanceType, connection);
}
}
/**
* Adds a row to the tsk_os_account_instances table. Does nothing if the
* instance already exists in the table.
*
* @param osAccountId Account id for which an instance needs to be
* added.
* @param dataSourceObjId Data source id where the instance is found.
* @param instanceType Instance type.
* @param connection The current database connection.
*
* @return OsAccountInstance Existing or newly created account instance.
*
* @throws TskCoreException If there is an error creating the account
* instance.
*/
OsAccountInstance newOsAccountInstance(long osAccountId, long dataSourceObjId, OsAccountInstance.OsAccountInstanceType instanceType, CaseDbConnection connection) throws TskCoreException {
Optional<OsAccountInstance> existingInstance = cachedAccountInstance(osAccountId, dataSourceObjId, instanceType);
if (existingInstance.isPresent()) {
return existingInstance.get();
}
/*
* Create the OS account instance.
*/
db.acquireSingleUserCaseWriteLock();
try {
String accountInsertSQL = db.getInsertOrIgnoreSQL("INTO tsk_os_account_instances(os_account_obj_id, data_source_obj_id, instance_type)"
+ " VALUES (?, ?, ?)"); // NON-NLS
PreparedStatement preparedStatement = connection.getPreparedStatement(accountInsertSQL, Statement.RETURN_GENERATED_KEYS);
preparedStatement.clearParameters();
preparedStatement.setLong(1, osAccountId);
preparedStatement.setLong(2, dataSourceObjId);
preparedStatement.setInt(3, instanceType.getId());
connection.executeUpdate(preparedStatement);
try (ResultSet resultSet = preparedStatement.getGeneratedKeys();) {
if (resultSet.next()) {
OsAccountInstance accountInstance = new OsAccountInstance(db, resultSet.getLong(1), osAccountId, dataSourceObjId, instanceType);
synchronized (osAcctInstancesCacheLock) {
OsAccountInstanceKey key = new OsAccountInstanceKey(osAccountId, dataSourceObjId);
// remove from cache any instances less significant (higher ordinal) than this instance
for (OsAccountInstance.OsAccountInstanceType type : OsAccountInstance.OsAccountInstanceType.values()) {
if (accountInstance.getInstanceType().compareTo(type) < 0) {
osAccountInstanceCache.remove(key);
}
}
// add the new most significant instance to the cache
osAccountInstanceCache.put(key, accountInstance);
}
/*
* There is a potential issue here. The cache of OS account
* instances is an optimization and was not intended to be
* used as an authoritative indicator of whether or not a
* particular OS account instance was already added to the
* case. In fact, the entire cache is flushed during merge
* operations. But regardless, there is a check-then-act
* race condition for multi-user cases, with or without the
* cache. And although the case database schema and the SQL
* returned by getInsertOrIgnoreSQL() seamlessly prevents
* duplicates in the case database, a valid row ID is
* returned here even if the INSERT is not done. So the
* bottom line is that a redundant event may be published
* from time to time.
*/
db.fireTSKEvent(new TskEvent.OsAcctInstancesAddedTskEvent(Collections.singletonList(accountInstance)));
return accountInstance;
} else {
// there is the possibility that another thread may be adding the same os account instance at the same time
// the database may be updated prior to the cache being updated so this provides an extra opportunity to check
// the cache before throwing the exception
Optional<OsAccountInstance> existingInstanceRetry = cachedAccountInstance(osAccountId, dataSourceObjId, instanceType);
if (existingInstanceRetry.isPresent()) {
return existingInstanceRetry.get();
}
}
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error adding OS account instance for OS account object id = %d, data source object id = %d", osAccountId, dataSourceObjId), ex);
} finally {
db.releaseSingleUserCaseWriteLock();
}
// It's possible that we weren't able to load the account instance because it
// is already in the database but the instance cache was cleared during an account merge.
// Try loading it here and re-adding to the cache.
String whereClause = "tsk_os_account_instances.os_account_obj_id = " + osAccountId
+ "AND tsk_os_account_instances.data_source_obj_id = " + dataSourceObjId;
List<OsAccountInstance> instances = getOsAccountInstances(whereClause);
if (instances.isEmpty()) {
throw new TskCoreException(String.format("Could not get autogen key after row insert or reload instance for OS account instance. OS account object id = %d, data source object id = %d", osAccountId, dataSourceObjId));
}
OsAccountInstance accountInstance = instances.get(0);
synchronized (osAcctInstancesCacheLock) {
OsAccountInstanceKey key = new OsAccountInstanceKey(osAccountId, dataSourceObjId);
// remove from cache any instances less significant (higher ordinal) than this instance
for (OsAccountInstance.OsAccountInstanceType type : OsAccountInstance.OsAccountInstanceType.values()) {
if (accountInstance.getInstanceType().compareTo(type) < 0) {
osAccountInstanceCache.remove(key);
}
}
// add the most significant instance to the cache
osAccountInstanceCache.put(key, accountInstance);
}
return accountInstance;
}
/**
* Check if an account instance for exists in the cache for given account
* id, data source and instance type.
*
* Instance type does not need to be an exact match - an existing instance
* with an instance type more significant than the specified type is
* considered a match.
*
* @param osAccountId Account id.
* @param dataSourceObjId Data source object id.
* @param instanceType Account instance type.
*
* @return Optional with OsAccountInstance, Optional.empty if there is no
* matching instance in cache.
*
*/
private Optional<OsAccountInstance> cachedAccountInstance(long osAccountId, long dataSourceObjId, OsAccountInstance.OsAccountInstanceType instanceType) {
/*
* Check the cache of OS account instances for an existing instance for
* this OS account and data source. Note that the account instance
* created here has a bogus instance ID. This is possible since the
* instance ID is not considered in the equals() and hashCode() methods
* of this class.
*/
synchronized (osAcctInstancesCacheLock) {
OsAccountInstanceKey key = new OsAccountInstanceKey(osAccountId, dataSourceObjId);
OsAccountInstance instance = osAccountInstanceCache.get(key);
if (instance != null) {
// if the new instance type same or less significant than the existing instance (i.e. same or higher ordinal value) it's a match.
if (instanceType.compareTo(instance.getInstanceType()) >= 0) {
return Optional.of(instance);
}
}
return Optional.empty();
}
}
/**
* Get all accounts that had an instance on the specified host.
*
* @param host Host for which to look accounts for.
*
* @return Set of OsAccounts, may be empty.
*
* @throws org.sleuthkit.datamodel.TskCoreException
*/
public List<OsAccount> getOsAccounts(Host host) throws TskCoreException {
String queryString = "SELECT * FROM tsk_os_accounts accounts "
+ "WHERE accounts.os_account_obj_id IN "
+ "(SELECT instances.os_account_obj_id "
+ "FROM tsk_os_account_instances instances "
+ "INNER JOIN data_source_info datasources ON datasources.obj_id = instances.data_source_obj_id "
+ "WHERE datasources.host_id = " + host.getHostId() + ") "
+ "AND accounts.db_status = " + OsAccount.OsAccountDbStatus.ACTIVE.getId();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
List<OsAccount> accounts = new ArrayList<>();
while (rs.next()) {
accounts.add(osAccountFromResultSet(rs));
}
return accounts;
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting OS accounts for host id = %d", host.getHostId()), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Get all accounts that had an instance on the specified data source.
*
* @param dataSourceId Data source id for which to look accounts for.
*
* @return Set of OsAccounts, may be empty.
*
* @throws org.sleuthkit.datamodel.TskCoreException
*/
public List<OsAccount> getOsAccountsByDataSourceObjId(long dataSourceId) throws TskCoreException {
String queryString = "SELECT * FROM tsk_os_accounts acc "
+ "WHERE acc.os_account_obj_id IN "
+ "(SELECT instance.os_account_obj_id "
+ "FROM tsk_os_account_instances instance "
+ "WHERE instance.data_source_obj_id = " + dataSourceId + ") "
+ "AND acc.db_status = " + OsAccount.OsAccountDbStatus.ACTIVE.getId();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
List<OsAccount> accounts = new ArrayList<>();
while (rs.next()) {
accounts.add(osAccountFromResultSet(rs));
}
return accounts;
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting OS accounts for data source id = %d", dataSourceId), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Merge all OS accounts from sourceRealm into destRealm. After this call: -
* sourceRealm's accounts will have been moved or merged - References to
* sourceRealm accounts will be updated - sourceRealm will still exist, but
* will be empty
*
* @param sourceRealm The source realm.
* @param destRealm The destination realm.
* @param trans The current transaction.
*
* @throws TskCoreException
*/
void mergeOsAccountsForRealms(OsAccountRealm sourceRealm, OsAccountRealm destRealm, CaseDbTransaction trans) throws TskCoreException {
List<OsAccount> destinationAccounts = getOsAccounts(destRealm, trans.getConnection());
List<OsAccount> sourceAccounts = getOsAccounts(sourceRealm, trans.getConnection());
for (OsAccount sourceAccount : sourceAccounts) {
// First a check for the case where the source account has both the login name and unique ID set and
// we have separate matches in the destination account for both. If we find this case, we need to first merge
// the two accounts in the destination realm. This will ensure that all source accounts match at most one
// destination account.
// Note that we only merge accounts based on login name if the unique ID is empty.
if (sourceAccount.getAddr().isPresent() && sourceAccount.getLoginName().isPresent()) {
List<OsAccount> duplicateDestAccounts = destinationAccounts.stream()
.filter(p -> p.getAddr().equals(sourceAccount.getAddr())
|| (p.getLoginName().equals(sourceAccount.getLoginName()) && (!p.getAddr().isPresent())))
.collect(Collectors.toList());
if (duplicateDestAccounts.size() > 1) {
OsAccount combinedDestAccount = duplicateDestAccounts.get(0);
duplicateDestAccounts.remove(combinedDestAccount);
for (OsAccount dupeDestAccount : duplicateDestAccounts) {
mergeOsAccounts(dupeDestAccount, combinedDestAccount, trans);
}
}
}
// Look for matching destination account
Optional<OsAccount> matchingDestAccount = getMatchingAccountForMerge(sourceAccount, destinationAccounts);
// If we found a match, merge the accounts. Otherwise simply update the realm id
if (matchingDestAccount.isPresent()) {
mergeOsAccounts(sourceAccount, matchingDestAccount.get(), trans);
} else {
String query = "UPDATE tsk_os_accounts SET realm_id = " + destRealm.getRealmId() + " WHERE os_account_obj_id = " + sourceAccount.getId();
try (Statement s = trans.getConnection().createStatement()) {
s.executeUpdate(query);
} catch (SQLException ex) {
throw new TskCoreException("Error executing SQL update: " + query, ex);
}
trans.registerChangedOsAccount(sourceAccount);
}
}
}
/**
* Checks for matching account in a list of accounts for merging
* @param sourceAccount The account to find matches for
* @param destinationAccounts List of accounts to match against
* @return Optional with OsAccount, Optional.empty if no matching OsAccount is found.
*/
private Optional<OsAccount> getMatchingAccountForMerge(OsAccount sourceAccount, List<OsAccount> destinationAccounts) {
// Look for matching destination account
OsAccount matchingDestAccount = null;
// First look for matching unique id
if (sourceAccount.getAddr().isPresent()) {
List<OsAccount> matchingDestAccounts = destinationAccounts.stream()
.filter(p -> p.getAddr().equals(sourceAccount.getAddr()))
.collect(Collectors.toList());
if (!matchingDestAccounts.isEmpty()) {
matchingDestAccount = matchingDestAccounts.get(0);
}
}
// If a match wasn't found yet, look for a matching login name.
// We will merge only if:
// - We didn't already find a unique ID match
// - The source account has no unique ID OR the destination account has no unique ID
// - destination account has a login name and matches the source account login name
if (matchingDestAccount == null && sourceAccount.getLoginName().isPresent()) {
List<OsAccount> matchingDestAccounts = destinationAccounts.stream()
.filter(p -> p.getLoginName().isPresent())
.filter(p -> (p.getLoginName().get().equalsIgnoreCase(sourceAccount.getLoginName().get())
&& ((!sourceAccount.getAddr().isPresent()) || (!p.getAddr().isPresent()))))
.collect(Collectors.toList());
if (!matchingDestAccounts.isEmpty()) {
matchingDestAccount = matchingDestAccounts.get(0);
}
}
return Optional.ofNullable(matchingDestAccount);
}
/**
* Checks for matching accounts in the same realm
* and then merges the accounts if a match is found
* @param account The account to find matches for
* @param trans The current transaction.
* @throws TskCoreException
*/
private void mergeOsAccount(OsAccount account, CaseDbTransaction trans) throws TskCoreException {
// Get the realm for the account
Long realmId = account.getRealmId();
OsAccountRealm realm = db.getOsAccountRealmManager().getRealmByRealmId(realmId, trans.getConnection());
// Get all users in the realm (excluding the account)
List<OsAccount> osAccounts = getOsAccounts(realm, trans.getConnection());
osAccounts.removeIf(acc -> Objects.equals(acc.getId(), account.getId()));
// Look for matching account
Optional<OsAccount> matchingAccount = getMatchingAccountForMerge(account, osAccounts);
// If we find a match, merge the accounts.
if (matchingAccount.isPresent()) {
mergeOsAccounts(matchingAccount.get(), account, trans);
}
}
/**
* Merges data between two accounts so that only one is active at the end
* and all references are to it. Data from the destination account will take
* priority. Basic operation: - Update the destination if source has names,
* etc. not already in the destination - Update any references to the source
* (such as in tsk_files) to point to destination - Mark the source as
* "MERGED" and it will not come back in future queries.
*
* @param sourceAccount The source account.
* @param destAccount The destination account.
* @param trans The current transaction.
*
* @throws TskCoreException
*/
private void mergeOsAccounts(OsAccount sourceAccount, OsAccount destAccount, CaseDbTransaction trans) throws TskCoreException {
String query = "";
try (Statement s = trans.getConnection().createStatement()) {
// Update all references
query = makeOsAccountUpdateQuery("tsk_os_account_attributes", sourceAccount, destAccount);
s.executeUpdate(query);
// tsk_os_account_instances has a unique constraint on os_account_obj_id, data_source_obj_id, and instance_type,
// so delete any rows that would be duplicates.
query = "DELETE FROM tsk_os_account_instances "
+ "WHERE id IN ( "
+ "SELECT "
+ " sourceAccountInstance.id "
+ "FROM "
+ " tsk_os_account_instances destAccountInstance "
+ "INNER JOIN tsk_os_account_instances sourceAccountInstance ON destAccountInstance.data_source_obj_id = sourceAccountInstance.data_source_obj_id "
+ "WHERE destAccountInstance.os_account_obj_id = " + destAccount.getId()
+ " AND sourceAccountInstance.os_account_obj_id = " + sourceAccount.getId()
+ " AND sourceAccountInstance.instance_type = destAccountInstance.instance_type" + ")";
s.executeUpdate(query);
query = makeOsAccountUpdateQuery("tsk_os_account_instances", sourceAccount, destAccount);
s.executeUpdate(query);
synchronized (osAcctInstancesCacheLock) {
osAccountInstanceCache.clear();
}
query = makeOsAccountUpdateQuery("tsk_files", sourceAccount, destAccount);
s.executeUpdate(query);
query = makeOsAccountUpdateQuery("tsk_data_artifacts", sourceAccount, destAccount);
s.executeUpdate(query);
// register the merged accounts with the transaction to fire off an event
trans.registerMergedOsAccount(sourceAccount.getId(), destAccount.getId());
// Update the source account. Make a dummy signature to prevent problems with the unique constraint.
String mergedSignature = makeMergedOsAccountSignature();
query = "UPDATE tsk_os_accounts SET merged_into = " + destAccount.getId()
+ ", db_status = " + OsAccount.OsAccountDbStatus.MERGED.getId()
+ ", signature = '" + mergedSignature + "' "
+ " WHERE os_account_obj_id = " + sourceAccount.getId();
s.executeUpdate(query);
trans.registerDeletedOsAccount(sourceAccount.getId());
// Merge and update the destination account. Note that this must be done after updating
// the source account to prevent conflicts when merging two accounts in the
// same realm.
mergeOsAccountObjectsAndUpdateDestAccount(sourceAccount, destAccount, trans);
} catch (SQLException ex) {
throw new TskCoreException("Error executing SQL update: " + query, ex);
}
}
/**
* Create a random signature for accounts that have been merged.
*
* @return The random signature.
*/
private String makeMergedOsAccountSignature() {
return "MERGED " + UUID.randomUUID().toString();
}
/**
* Create the query to update the os account column to the merged account.
*
* @param tableName Name of table to update.
* @param sourceAccount The source account.
* @param destAccount The destination account.
*
* @return The query.
*/
private String makeOsAccountUpdateQuery(String tableName, OsAccount sourceAccount, OsAccount destAccount) {
return "UPDATE " + tableName + " SET os_account_obj_id = " + destAccount.getId() + " WHERE os_account_obj_id = " + sourceAccount.getId();
}
/**
* Copy all fields from sourceAccount that are not set in destAccount.
*
* Updates the dest account in the database.
*
* @param sourceAccount The source account.
* @param destAccount The destination account.
* @param trans Transaction to use for database operations.
*
* @return OsAccount Updated account.
*/
private OsAccount mergeOsAccountObjectsAndUpdateDestAccount(OsAccount sourceAccount, OsAccount destAccount, CaseDbTransaction trans) throws TskCoreException {
OsAccount mergedDestAccount = destAccount;
String destLoginName = null;
String destAddr = null;
// Copy any fields that aren't set in the destination to the value from the source account.
if (!destAccount.getLoginName().isPresent() && sourceAccount.getLoginName().isPresent()) {
destLoginName = sourceAccount.getLoginName().get();
}
if (!destAccount.getAddr().isPresent() && sourceAccount.getAddr().isPresent()) {
destAddr = sourceAccount.getAddr().get();
}
// update the dest account core
OsAccountUpdateResult updateStatus = this.updateOsAccountCore(destAccount, destAddr, destLoginName, trans);
if (updateStatus.getUpdateStatusCode() == OsAccountUpdateStatus.UPDATED && updateStatus.getUpdatedAccount().isPresent()) {
mergedDestAccount = updateStatus.getUpdatedAccount().get();
}
String destFullName = null;
Long destCreationTime = null;
if (!destAccount.getFullName().isPresent() && sourceAccount.getFullName().isPresent()) {
destFullName = sourceAccount.getFullName().get();
}
if (!destAccount.getCreationTime().isPresent() && sourceAccount.getCreationTime().isPresent()) {
destCreationTime = sourceAccount.getCreationTime().get();
}
// update the dest account properties
updateStatus = this.updateStandardOsAccountAttributes(destAccount, destFullName, null, null, destCreationTime, trans);
if (updateStatus.getUpdateStatusCode() == OsAccountUpdateStatus.UPDATED && updateStatus.getUpdatedAccount().isPresent()) {
mergedDestAccount = updateStatus.getUpdatedAccount().get();
}
return mergedDestAccount;
}
/**
* Get all active accounts associated with the given realm.
*
* @param realm Realm for which to look accounts for.
* @param connection Current database connection.
*
* @return Set of OsAccounts, may be empty.
*
* @throws org.sleuthkit.datamodel.TskCoreException
*/
private List<OsAccount> getOsAccounts(OsAccountRealm realm, CaseDbConnection connection) throws TskCoreException {
String queryString = "SELECT * FROM tsk_os_accounts"
+ " WHERE realm_id = " + realm.getRealmId()
+ " AND db_status = " + OsAccount.OsAccountDbStatus.ACTIVE.getId()
+ " ORDER BY os_account_obj_id";
try (Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
List<OsAccount> accounts = new ArrayList<>();
while (rs.next()) {
accounts.add(osAccountFromResultSet(rs));
}
return accounts;
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting OS accounts for realm id = %d", realm.getRealmId()), ex);
}
}
/**
* Get all active accounts.
*
* @return Set of OsAccounts, may be empty.
*
* @throws org.sleuthkit.datamodel.TskCoreException
*/
public List<OsAccount> getOsAccounts() throws TskCoreException {
String queryString = "SELECT * FROM tsk_os_accounts"
+ " WHERE db_status = " + OsAccount.OsAccountDbStatus.ACTIVE.getId();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
List<OsAccount> accounts = new ArrayList<>();
while (rs.next()) {
accounts.add(osAccountFromResultSet(rs));
}
return accounts;
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting OS accounts"), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Gets an OS account using Windows-specific data.
*
* @param sid Account SID, maybe null if loginName is supplied.
* @param loginName Login name, maybe null if sid is supplied.
* @param realmName Realm within which the accountId or login name is
* unique. Can be 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.
* @throws NotUserSIDException If the given SID is not a user SID.
*/
public Optional<OsAccount> getWindowsOsAccount(String sid, String loginName, String realmName, Host referringHost) throws TskCoreException, NotUserSIDException {
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 sid or a login name
if ((StringUtils.isBlank(sid) || (sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID)) ) && StringUtils.isBlank(loginName)) {
throw new TskCoreException("Cannot get an OS account with both SID and loginName as null.");
}
// If no SID is given and the given realm/login names is a well known account, get and use the well known SID
if (StringUtils.isBlank(sid)
&& !StringUtils.isBlank(loginName) && !StringUtils.isBlank(realmName)
&& WindowsAccountUtils.isWindowsWellKnownAccountName(loginName, realmName)) {
sid = WindowsAccountUtils.getWindowsWellKnownAccountSid(loginName, realmName);
}
// first get the realm for the given sid
Optional<OsAccountRealm> realm = db.getOsAccountRealmManager().getWindowsRealm(sid, realmName, referringHost);
if (!realm.isPresent()) {
return Optional.empty();
}
// search by SID
if (!Strings.isNullOrEmpty(sid) && !(sid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID))) {
if (!WindowsAccountUtils.isWindowsUserSid(sid)) {
throw new OsAccountManager.NotUserSIDException(String.format("SID = %s is not a user SID.", sid));
}
Optional<OsAccount> account = this.getOsAccountByAddr(sid, realm.get());
if (account.isPresent()) {
return account;
}
}
// search by login name
if (!Strings.isNullOrEmpty(loginName)) {
String resolvedLoginName = WindowsAccountUtils.toWellknownEnglishLoginName(loginName);
return this.getOsAccountByLoginName(resolvedLoginName, realm.get());
} else {
return Optional.empty();
}
}
/**
* 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.
*
* @param account Account for which the attributes is being added.
* @param accountAttributes List of attributes to add.
*
* @throws TskCoreException
*/
public void addExtendedOsAccountAttributes(OsAccount account, List<OsAccountAttribute> accountAttributes) throws TskCoreException {
synchronized (account) { // synchronized to prevent multiple threads trying to add osAccount attributes concurrently to the same osAccount.
db.acquireSingleUserCaseWriteLock();
try (CaseDbConnection connection = db.getConnection()) {
for (OsAccountAttribute accountAttribute : accountAttributes) {
String attributeInsertSQL = "INSERT INTO tsk_os_account_attributes(os_account_obj_id, host_id, source_obj_id, attribute_type_id, value_type, value_byte, value_text, value_int32, value_int64, value_double)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; // NON-NLS
PreparedStatement preparedStatement = connection.getPreparedStatement(attributeInsertSQL, Statement.RETURN_GENERATED_KEYS);
preparedStatement.clearParameters();
preparedStatement.setLong(1, account.getId());
if (accountAttribute.getHostId().isPresent()) {
preparedStatement.setLong(2, accountAttribute.getHostId().get());
} else {
preparedStatement.setNull(2, java.sql.Types.NULL);
}
if (accountAttribute.getSourceObjectId().isPresent()) {
preparedStatement.setLong(3, accountAttribute.getSourceObjectId().get());
} else {
preparedStatement.setNull(3, java.sql.Types.NULL);
}
preparedStatement.setLong(4, accountAttribute.getAttributeType().getTypeID());
preparedStatement.setLong(5, accountAttribute.getAttributeType().getValueType().getType());
if (accountAttribute.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE) {
preparedStatement.setBytes(6, accountAttribute.getValueBytes());
} else {
preparedStatement.setBytes(6, null);
}
if (accountAttribute.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING
|| accountAttribute.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) {
preparedStatement.setString(7, accountAttribute.getValueString());
} else {
preparedStatement.setString(7, null);
}
if (accountAttribute.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER) {
preparedStatement.setInt(8, accountAttribute.getValueInt());
} else {
preparedStatement.setNull(8, java.sql.Types.NULL);
}
if (accountAttribute.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME
|| accountAttribute.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG) {
preparedStatement.setLong(9, accountAttribute.getValueLong());
} else {
preparedStatement.setNull(9, java.sql.Types.NULL);
}
if (accountAttribute.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE) {
preparedStatement.setDouble(10, accountAttribute.getValueDouble());
} else {
preparedStatement.setNull(10, java.sql.Types.NULL);
}
connection.executeUpdate(preparedStatement);
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error adding OS Account attribute for account id = %d", account.getId()), ex);
} finally {
db.releaseSingleUserCaseWriteLock();
}
// set the atrribute list in account to the most current list from the database
List<OsAccountAttribute> currentAttribsList = getOsAccountAttributes(account);
account.setAttributesInternal(currentAttribsList);
}
fireChangeEvent(account);
}
/**
* Get the OS account attributes for the given account.
*
* @param account Account to get the attributes for.
*
* @return List of attributes, may be an empty list.
*
* @throws TskCoreException
*/
List<OsAccountAttribute> getOsAccountAttributes(OsAccount account) throws TskCoreException {
String queryString = "SELECT attributes.os_account_obj_id as os_account_obj_id, attributes.host_id as host_id, attributes.source_obj_id as source_obj_id, "
+ " attributes.attribute_type_id as attribute_type_id, attributes.value_type as value_type, attributes.value_byte as value_byte, "
+ " attributes.value_text as value_text, attributes.value_int32 as value_int32, attributes.value_int64 as value_int64, attributes.value_double as value_double, "
+ " hosts.id, hosts.name as host_name, hosts.db_status as host_status "
+ " FROM tsk_os_account_attributes as attributes"
+ " LEFT JOIN tsk_hosts as hosts "
+ " ON attributes.host_id = hosts.id "
+ " WHERE os_account_obj_id = " + account.getId();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
List<OsAccountAttribute> attributes = new ArrayList<>();
while (rs.next()) {
Host host = null;
long hostId = rs.getLong("host_id");
if (!rs.wasNull()) {
host = new Host(hostId, rs.getString("host_name"), Host.HostDbStatus.fromID(rs.getInt("host_status")));
}
Content sourceContent = null;
long sourceObjId = rs.getLong("source_obj_id");
if (!rs.wasNull()) {
sourceContent = this.db.getContentById(sourceObjId);
}
BlackboardAttribute.Type attributeType = db.getBlackboard().getAttributeType(rs.getInt("attribute_type_id"));
OsAccountAttribute attribute = account.new OsAccountAttribute(attributeType, rs.getInt("value_int32"), rs.getLong("value_int64"),
rs.getDouble("value_double"), rs.getString("value_text"), rs.getBytes("value_byte"),
db, account, host, sourceContent);
attributes.add(attribute);
}
return attributes;
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting OS account attributes for account obj id = %d", account.getId()), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Gets the OS account instances for a given OS account.
*
* @param account The OS account.
*
* @return The OS account instances, may be an empty list.
*
* @throws TskCoreException
*/
public List<OsAccountInstance> getOsAccountInstances(OsAccount account) throws TskCoreException {
String whereClause = "tsk_os_account_instances.os_account_obj_id = " + account.getId();
return getOsAccountInstances(whereClause);
}
/**
* Gets the OS account instances with the given instance IDs.
*
* @param instanceIDs The instance IDs.
*
* @return The OS account instances.
*
* @throws TskCoreException Thrown if there is an error querying the case
* database.
*/
public List<OsAccountInstance> getOsAccountInstances(List<Long> instanceIDs) throws TskCoreException {
String instanceIds = instanceIDs.stream().map(id -> id.toString()).collect(Collectors.joining(","));
List<OsAccountInstance> osAcctInstances = new ArrayList<>();
String querySQL = "SELECT * FROM tsk_os_account_instances "
+ " WHERE tsk_os_account_instances.id IN (" + instanceIds + ")";
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = db.getConnection();
PreparedStatement preparedStatement = connection.getPreparedStatement(querySQL, Statement.NO_GENERATED_KEYS);
ResultSet results = connection.executeQuery(preparedStatement)) {
osAcctInstances = getOsAccountInstancesFromResultSet(results);
} catch (SQLException ex) {
throw new TskCoreException("Failed to get OsAccountInstances (SQL = " + querySQL + ")", ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
return osAcctInstances;
}
/**
* Gets the OS account instances that satisfy the given SQL WHERE clause.
*
* Note: this query returns only the most significant instance type (least
* ordinal) for each instance, that matches the specified WHERE clause.
*
* @param whereClause The SQL WHERE clause.
*
* @return The OS account instances.
*
* @throws TskCoreException Thrown if there is an error querying the case
* database.
*/
private List<OsAccountInstance> getOsAccountInstances(String whereClause) throws TskCoreException {
List<OsAccountInstance> osAcctInstances = new ArrayList<>();
String querySQL
= "SELECT tsk_os_account_instances.* "
+ " FROM tsk_os_account_instances "
+ " INNER JOIN ( SELECT os_account_obj_id, data_source_obj_id, MIN(instance_type) AS min_instance_type "
+ " FROM tsk_os_account_instances"
+ " GROUP BY os_account_obj_id, data_source_obj_id ) grouped_instances "
+ " ON tsk_os_account_instances.os_account_obj_id = grouped_instances.os_account_obj_id "
+ " AND tsk_os_account_instances.instance_type = grouped_instances.min_instance_type "
+ " WHERE " + whereClause;
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = db.getConnection();
PreparedStatement preparedStatement = connection.getPreparedStatement(querySQL, Statement.NO_GENERATED_KEYS);
ResultSet results = connection.executeQuery(preparedStatement)) {
osAcctInstances = getOsAccountInstancesFromResultSet(results);
} catch (SQLException ex) {
throw new TskCoreException("Failed to get OsAccountInstances (SQL = " + querySQL + ")", ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
return osAcctInstances;
}
/**
* Returns list of OS account instances from the given result set.
*
* @param results Result set from a SELECT tsk_os_account_instances.* query.
*
* @return List of OS account instances.
*
* @throws SQLException
*/
private List<OsAccountInstance> getOsAccountInstancesFromResultSet(ResultSet results) throws SQLException {
List<OsAccountInstance> osAcctInstances = new ArrayList<>();
while (results.next()) {
long instanceId = results.getLong("id");
long osAccountObjID = results.getLong("os_account_obj_id");
long dataSourceObjId = results.getLong("data_source_obj_id");
int instanceType = results.getInt("instance_type");
osAcctInstances.add(new OsAccountInstance(db, instanceId, osAccountObjID, dataSourceObjId, OsAccountInstance.OsAccountInstanceType.fromID(instanceType)));
}
return osAcctInstances;
}
/**
* Updates the properties of the specified account in the database.
*
* A column is updated only if a non-null value has been specified.
*
* @param osAccount OsAccount that needs to be updated in the database.
* @param fullName Full name, may be null.
* @param accountType Account type, may be null
* @param accountStatus Account status, may be null.
* @param creationTime Creation time, may be null.
*
* @return OsAccountUpdateResult Account update status, and updated account.
*
* @throws TskCoreException If there is a database error or if the updated
* information conflicts with an existing account.
*/
public OsAccountUpdateResult updateStandardOsAccountAttributes(OsAccount osAccount, String fullName, OsAccountType accountType, OsAccountStatus accountStatus, Long creationTime) throws TskCoreException {
CaseDbTransaction trans = db.beginTransaction();
try {
OsAccountUpdateResult updateStatus = updateStandardOsAccountAttributes(osAccount, fullName, accountType, accountStatus, creationTime, trans);
trans.commit();
trans = null;
return updateStatus;
} finally {
if (trans != null) {
trans.rollback();
}
}
}
/**
* Updates the properties of the specified account in the database.
*
* A column is updated only if a non-null value has been specified.
*
* @param osAccount OsAccount that needs to be updated in the database.
* @param fullName Full name, may be null.
* @param accountType Account type, may be null
* @param accountStatus Account status, may be null.
* @param creationTime Creation time, may be null.
* @param trans Transaction to use for database operation.
*
* @return OsAccountUpdateResult Account update status, and updated account.
*
* @throws TskCoreException If there is a database error or if the updated
* information conflicts with an existing account.
*/
OsAccountUpdateResult updateStandardOsAccountAttributes(OsAccount osAccount, String fullName, OsAccountType accountType, OsAccountStatus accountStatus, Long creationTime, CaseDbTransaction trans) throws TskCoreException {
OsAccountUpdateStatus updateStatusCode = OsAccountUpdateStatus.NO_CHANGE;
try {
CaseDbConnection connection = trans.getConnection();
if (!StringUtils.isBlank(fullName)) {
updateAccountColumn(osAccount.getId(), "full_name", fullName, connection);
updateStatusCode = OsAccountUpdateStatus.UPDATED;
}
if (Objects.nonNull(accountType)) {
updateAccountColumn(osAccount.getId(), "type", accountType.getId(), connection);
updateStatusCode = OsAccountUpdateStatus.UPDATED;
}
if (Objects.nonNull(accountStatus)) {
updateAccountColumn(osAccount.getId(), "status", accountStatus.getId(), connection);
updateStatusCode = OsAccountUpdateStatus.UPDATED;
}
if (Objects.nonNull(creationTime)) {
updateAccountColumn(osAccount.getId(), "created_date", creationTime, connection);
updateStatusCode = OsAccountUpdateStatus.UPDATED;
}
// if nothing has been changed, return
if (updateStatusCode == OsAccountUpdateStatus.NO_CHANGE) {
return new OsAccountUpdateResult(updateStatusCode, null);
}
// get the updated account from database
OsAccount updatedAccount = getOsAccountByObjectId(osAccount.getId(), connection);
// register the updated account with the transaction to fire off an event
trans.registerChangedOsAccount(updatedAccount);
return new OsAccountUpdateResult(updateStatusCode, updatedAccount);
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error updating account with addr = %s, account id = %d", osAccount.getAddr().orElse("Unknown"), osAccount.getId()), ex);
}
}
/**
* Updates specified column in the tsk_os_accounts table to the specified
* value.
*
* @param <T> Type of value - must be a String, Long or an Integer.
* @param accountObjId Object id of the account to be updated.
* @param colName Name of column o be updated.
* @param colValue New column value.
* @param connection Database connection to use.
*
* @throws SQLException If there is an error updating the database.
* @throws TskCoreException If the value type is not handled.
*/
private <T> void updateAccountColumn(long accountObjId, String colName, T colValue, CaseDbConnection connection) throws SQLException, TskCoreException {
String updateSQL = "UPDATE tsk_os_accounts "
+ " SET " + colName + " = ? "
+ " WHERE os_account_obj_id = ?";
db.acquireSingleUserCaseWriteLock();
try {
PreparedStatement preparedStatement = connection.getPreparedStatement(updateSQL, Statement.NO_GENERATED_KEYS);
preparedStatement.clearParameters();
if (Objects.isNull(colValue)) {
preparedStatement.setNull(1, Types.NULL); // handle null value
} else {
if (colValue instanceof String) {
preparedStatement.setString(1, (String) colValue);
} else if (colValue instanceof Long) {
preparedStatement.setLong(1, (Long) colValue);
} else if (colValue instanceof Integer) {
preparedStatement.setInt(1, (Integer) colValue);
} else {
throw new TskCoreException(String.format("Unhandled column data type received while updating the account (%d) ", accountObjId));
}
}
preparedStatement.setLong(2, accountObjId);
connection.executeUpdate(preparedStatement);
} finally {
db.releaseSingleUserCaseWriteLock();
}
}
/**
* Updates the signature of the specified account, if the db status of the
* account is active.
*
* @param accountObjId Object id of the account to be updated.
* @param signature New signature.
* @param connection Database connection to use.
*
* @throws SQLException If there is an error updating the database.
*/
private void updateAccountSignature(long accountObjId, String signature, CaseDbConnection connection) throws SQLException {
String updateSQL = "UPDATE tsk_os_accounts SET "
+ " signature = "
+ " CASE WHEN db_status = " + OsAccount.OsAccountDbStatus.ACTIVE.getId() + " THEN ? ELSE signature END "
+ " WHERE os_account_obj_id = ?"; // 8
PreparedStatement preparedStatement = connection.getPreparedStatement(updateSQL, Statement.NO_GENERATED_KEYS);
preparedStatement.clearParameters();
preparedStatement.setString(1, signature);
preparedStatement.setLong(2, accountObjId);
connection.executeUpdate(preparedStatement);
}
/**
* Update the address and/or login name for the specified account in the
* database. Also update the realm addr/name if needed.
*
* 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 accountSid Account SID, may be null.
* @param loginName Login name, may be null.
* @param realmName Realm name for the account.
* @param referringHost Host.
*
* @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 updateCoreWindowsOsAccountAttributes(OsAccount osAccount, String accountSid, String loginName, String realmName, Host referringHost) throws TskCoreException, NotUserSIDException {
CaseDbTransaction trans = db.beginTransaction();
try {
OsAccountUpdateResult updateStatus = this.updateCoreWindowsOsAccountAttributes(osAccount, accountSid, loginName, realmName, referringHost, 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. Also update the realm addr/name if needed.
*
* 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 accountSid Account SID, may be null.
* @param loginName Login name, may be null.
* @param realmName Account realm name. May be null if accountSid is not
* 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.
*/
private OsAccountUpdateResult updateCoreWindowsOsAccountAttributes(OsAccount osAccount, String accountSid, String loginName, String realmName, Host referringHost, CaseDbTransaction trans) throws TskCoreException, NotUserSIDException {
// first get and update the realm - if we have the info to find the realm
if ((!StringUtils.isBlank(accountSid) && !accountSid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID)) || !StringUtils.isBlank(realmName)) {
// If the SID is a well known SID, ensure we use the well known english name
String resolvedRealmName = WindowsAccountUtils.toWellknownEnglishRealmName(realmName);
OsRealmUpdateResult realmUpdateResult = db.getOsAccountRealmManager().getAndUpdateWindowsRealm(accountSid, resolvedRealmName, referringHost, trans.getConnection());
Optional<OsAccountRealm> realmOptional = realmUpdateResult.getUpdatedRealm();
if (realmOptional.isPresent()) {
if (realmUpdateResult.getUpdateStatus() == OsRealmUpdateStatus.UPDATED) {
// Check if update of the realm triggers a merge with any other realm,
// say another realm with same name but no SID, or same SID but no name
//1. Check if there is any OTHER realm with the same name, same host but no addr
Optional<OsAccountRealm> anotherRealmWithSameName = db.getOsAccountRealmManager().getAnotherRealmByName(realmOptional.get(), realmName, referringHost, trans.getConnection());
// 2. Check if there is any OTHER realm with same addr and host, but NO name
Optional<OsAccountRealm> anotherRealmWithSameAddr = db.getOsAccountRealmManager().getAnotherRealmByAddr(realmOptional.get(), realmName, referringHost, trans.getConnection());
if (anotherRealmWithSameName.isPresent()) {
db.getOsAccountRealmManager().mergeRealms(anotherRealmWithSameName.get(), realmOptional.get(), trans);
}
if (anotherRealmWithSameAddr.isPresent()) {
db.getOsAccountRealmManager().mergeRealms(anotherRealmWithSameAddr.get(), realmOptional.get(), trans);
}
}
}
}
// now update the account core data
String resolvedLoginName = WindowsAccountUtils.toWellknownEnglishLoginName(loginName);
OsAccountUpdateResult updateStatus = this.updateOsAccountCore(osAccount, accountSid, resolvedLoginName, trans);
Optional<OsAccount> updatedAccount = updateStatus.getUpdatedAccount();
if (updatedAccount.isPresent()) {
// After updating account data, check if there is matching account to merge
mergeOsAccount(updatedAccount.get(), trans);
}
return updateStatus;
}
/**
* 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.
*
* A column is updated only if its current value is null and a non-null
* value has been specified.
*
*
* NOTE: Will not merge accounts if the updated information conflicts with
* an existing account (such as adding an ID to an account that has only a
* name and there already being an account with that ID).
*
* @param osAccount OsAccount that needs to be updated in the database.
* @param address Account address, 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.
*/
private OsAccountUpdateResult updateOsAccountCore(OsAccount osAccount, String address, String loginName, CaseDbTransaction trans) throws TskCoreException {
OsAccountUpdateStatus updateStatusCode = OsAccountUpdateStatus.NO_CHANGE;
OsAccount updatedAccount;
try {
CaseDbConnection connection = trans.getConnection();
// if a new non-null addr is provided and the account already has an address, and they are not the same, throw an exception
if (!StringUtils.isBlank(address) && !address.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID) && !StringUtils.isBlank(osAccount.getAddr().orElse(null)) && !address.equalsIgnoreCase(osAccount.getAddr().orElse(""))) {
throw new TskCoreException(String.format("Account (%d) already has an address (%s), address cannot be updated.", osAccount.getId(), osAccount.getAddr().orElse("NULL")));
}
if (StringUtils.isBlank(osAccount.getAddr().orElse(null)) && !StringUtils.isBlank(address) && !address.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID)) {
updateAccountColumn(osAccount.getId(), "addr", address, connection);
updateStatusCode = OsAccountUpdateStatus.UPDATED;
}
if (StringUtils.isBlank(osAccount.getLoginName().orElse(null)) && !StringUtils.isBlank(loginName)) {
updateAccountColumn(osAccount.getId(), "login_name", loginName, connection);
updateStatusCode = OsAccountUpdateStatus.UPDATED;
}
// if nothing is changed, return
if (updateStatusCode == OsAccountUpdateStatus.NO_CHANGE) {
return new OsAccountUpdateResult(updateStatusCode, osAccount);
}
// update signature if needed, based on the most current addr/loginName
OsAccount currAccount = getOsAccountByObjectId(osAccount.getId(), connection);
String newAddress = currAccount.getAddr().orElse(null);
String newLoginName = currAccount.getLoginName().orElse(null);
String newSignature = getOsAccountSignature(newAddress, newLoginName);
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);
// register the updated account with the transaction to fire off an event
trans.registerChangedOsAccount(updatedAccount);
return new OsAccountUpdateResult(updateStatusCode, updatedAccount);
} 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);
}
}
/**
* Returns a list of hosts where the OsAccount has appeared.
*
* @param account OsAccount
*
* @return List of Hosts that reference the given OsAccount.
*
* @throws TskCoreException
*/
public List<Host> getHosts(OsAccount account) throws TskCoreException {
List<Host> hostList = new ArrayList<>();
String query = "SELECT tsk_hosts.id AS hostId, name, db_status FROM tsk_hosts "
+ " JOIN data_source_info ON tsk_hosts.id = data_source_info.host_id"
+ " JOIN tsk_os_account_instances ON data_source_info.obj_id = tsk_os_account_instances.data_source_obj_id"
+ " WHERE os_account_obj_id = " + account.getId();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, query)) {
while (rs.next()) {
hostList.add(new Host(rs.getLong("hostId"), rs.getString("name"), Host.HostDbStatus.fromID(rs.getInt("db_status"))));
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Failed to get host list for os account %d", account.getId()), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
return hostList;
}
/**
* Takes in a result with a row from tsk_os_accounts table and creates an
* OsAccount.
*
* @param rs ResultSet.
* @param realmId Realm.
*
* @return OsAccount OS Account.
*
* @throws SQLException
*/
private OsAccount osAccountFromResultSet(ResultSet rs) throws SQLException {
OsAccountType accountType = null;
int typeId = rs.getInt("type");
if (!rs.wasNull()) {
accountType = OsAccount.OsAccountType.fromID(typeId);
}
Long creationTime = rs.getLong("created_date"); // getLong returns 0 if value is null
if (rs.wasNull()) {
creationTime = null;
}
return new OsAccount(db, rs.getLong("os_account_obj_id"), rs.getLong("realm_id"), rs.getString("login_name"), rs.getString("addr"),
rs.getString("signature"), rs.getString("full_name"), creationTime, accountType, OsAccount.OsAccountStatus.fromID(rs.getInt("status")),
OsAccount.OsAccountDbStatus.fromID(rs.getInt("db_status")));
}
/**
* Fires an OsAccountChangeEvent for the given OsAccount. Do not call this
* with an open transaction.
*
* @param account Updated account.
*/
private void fireChangeEvent(OsAccount account) {
db.fireTSKEvent(new OsAccountsUpdatedTskEvent(Collections.singletonList(account)));
}
/**
* Created an account signature for an OS Account. This signature is simply
* to prevent duplicate accounts from being created. Signature is set to:
* uniqueId: if the account has a uniqueId, otherwise loginName: if the
* account has a login name.
*
* @param uniqueId Unique id.
* @param loginName Login name.
*
* @return Account signature.
*
* @throws TskCoreException If there is an error creating the account
* signature.
*/
static String getOsAccountSignature(String uniqueId, String loginName) throws TskCoreException {
// Create a signature.
String signature;
if (Strings.isNullOrEmpty(uniqueId) == false) {
signature = uniqueId;
} else if (Strings.isNullOrEmpty(loginName) == false) {
signature = loginName;
} else {
throw new TskCoreException("OS Account must have either a uniqueID or a login name.");
}
return signature;
}
/**
* Exception thrown if a given SID is a valid SID but is a group SID, and
* not an individual user SID.
*/
public static class NotUserSIDException extends TskException {
private static final long serialVersionUID = 1L;
/**
* Default constructor when error message is not available
*/
public NotUserSIDException() {
super("No error message available.");
}
/**
* Create exception containing the error message
*
* @param msg the message
*/
public NotUserSIDException(String msg) {
super(msg);
}
/**
* Create exception containing the error message and cause exception
*
* @param msg the message
* @param ex cause exception
*/
public NotUserSIDException(String msg, Exception ex) {
super(msg, ex);
}
}
/**
* Status of an account update.
*/
public enum OsAccountUpdateStatus {
NO_CHANGE, /// no change was made to account.
UPDATED, /// account was updated
MERGED /// account update triggered a merge
}
/**
* Container that encapsulates the account update status and the updated
* account.
*/
public final static class OsAccountUpdateResult {
private final OsAccountUpdateStatus updateStatus;
private final OsAccount updatedAccount;
OsAccountUpdateResult(OsAccountUpdateStatus updateStatus, OsAccount updatedAccount) {
this.updateStatus = updateStatus;
this.updatedAccount = updatedAccount;
}
public OsAccountUpdateStatus getUpdateStatusCode() {
return updateStatus;
}
public Optional<OsAccount> getUpdatedAccount() {
return Optional.ofNullable(updatedAccount);
}
}
/**
* Represents the osAccountId\dataSourceId pair for use with the cache of
* OsAccountInstances.
*/
private class OsAccountInstanceKey implements Comparable<OsAccountInstanceKey>{
private final long osAccountId;
private final long dataSourceId;
OsAccountInstanceKey(long osAccountId, long dataSourceId) {
this.osAccountId = osAccountId;
this.dataSourceId = dataSourceId;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if (getClass() != other.getClass()) {
return false;
}
final OsAccountInstanceKey otherKey = (OsAccountInstanceKey) other;
if (osAccountId != otherKey.osAccountId) {
return false;
}
return dataSourceId == otherKey.dataSourceId;
}
@Override
public int hashCode() {
int hash = 5;
hash = 53 * hash + (int) (this.osAccountId ^ (this.osAccountId >>> 32));
hash = 53 * hash + (int) (this.dataSourceId ^ (this.dataSourceId >>> 32));
return hash;
}
@Override
public int compareTo(OsAccountInstanceKey other) {
if(this.equals(other)) {
return 0;
}
if (dataSourceId != other.dataSourceId) {
return Long.compare(dataSourceId, other.dataSourceId);
}
return Long.compare(osAccountId, other.osAccountId);
}
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2020 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;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ResourceBundle;
import org.apache.commons.lang3.StringUtils;
/**
* Realm encapsulates the scope of an OsAccount. An account is unique within a realm.
*
* A realm may be host scoped, say for a local standalone computer, or
* domain scoped.
*
* Many times, we may learn about the existence of a realm without fully understanding
* it. Such as when we find a Windows SID before we've parsed the registry to know if
* it is for the local computer or domain. By default, a realm is created with a
* host-level scope and a confidence of "inferred".
*/
public final class OsAccountRealm {
private static final ResourceBundle bundle = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle");
private final long id; // row id
// a realm may have multiple names - for exmple, for a user ABCCorp\\user1 or user1@ABCcorp.com - 'ABCCorp' and 'ABCcorp.com' both refer to the same realm.
// currently we only support a single name, this could be expanded in future.
private final String realmName; // realm name
private final String realmAddr; // realm address
private String signature; // either realm address or name (if address is not known), plus a scope indicator
private final Host host; // if the realm consists of a single host. Will be null if the realm is domain scoped.
private final ScopeConfidence scopeConfidence; // confidence in realm scope.
private final RealmDbStatus dbStatus; // Status of row in database.
/**
* Creates OsAccountRealm.
*
* @param id Row Id.
* @param realmName Realm name, may be null.
* @param realmAddr Unique numeric address for realm, may be null only
* if realm name is not null.
* @param signature Either the address or the name, plus a scope indicator.
* @param host Host if the realm is host scoped.
* @param scopeConfidence Scope confidence.
*/
OsAccountRealm(long id, String realmName, String realmAddr, String signature, Host host, ScopeConfidence scopeConfidence, RealmDbStatus dbStatus) {
this.id = id;
this.realmName = realmName;
this.realmAddr = realmAddr;
this.signature = signature;
this.host = host;
this.scopeConfidence = scopeConfidence;
this.dbStatus = dbStatus;
}
/**
* Get the realm row id.
*
* @return Realm id.
*/
long getRealmId() {
return id;
}
/**
* Get realm names list.
*
* Currently we only support a single name for realm, so this list may have
* at most a single name. And the list may be empty if there is no name.
*
* @return List of realm names, may be empty.
*/
public List<String> getRealmNames() {
List<String> namesList = new ArrayList<>();
if (!Objects.isNull(realmName)) {
namesList.add(realmName);
}
return namesList;
}
/**
* Get the realm address, such as part of a Windows SID.
*
* @return Optional realm unique address.
*/
public Optional<String> getRealmAddr() {
return Optional.ofNullable(realmAddr);
}
/**
* Get the realm signature.
*
* @return Realm signature.
*/
String getSignature() {
return signature;
}
/**
* Get the realm scope host, if it's a single host realm.
*
* @return Optional host. Is empty if the scope of the realm is domain-scoped.
*/
public Optional<Host> getScopeHost() {
return Optional.ofNullable(host);
}
/**
* Get realm scope confidence.
*
* @return Realm scope confidence.
*/
public ScopeConfidence getScopeConfidence() {
return scopeConfidence;
}
/**
* Get the database status of this realm.
*
* @return Realm database status.
*/
RealmDbStatus getDbStatus() {
return dbStatus;
}
/**
* Get the realm scope.
*
* @return Realm scope.
*/
public RealmScope getScope() {
return getScopeHost().isPresent() ? RealmScope.LOCAL : RealmScope.DOMAIN;
}
/**
* Enum to encapsulate a realm scope.
*
* Scope of a realm may extend to a single host (local)
* or to a domain.
*/
public enum RealmScope {
UNKNOWN(0, bundle.getString("OsAccountRealm.Unknown.text")), // realm scope is unknown.
LOCAL(1, bundle.getString("OsAccountRealm.Local.text")), // realm scope is a single host.
DOMAIN(2, bundle.getString("OsAccountRealm.Domain.text")); // realm scope is a domain.
private final int id;
private final String name;
RealmScope(int id, String name) {
this.id = id;
this.name = name;
}
/**
* Get the id of the realm scope.
*
* @return Realm scope id.
*/
public int getId() {
return id;
}
/**
* Get the realm scope name.
*
* @return Realm scope name.
*/
public String getName() {
return name;
}
/**
* Gets a realm scope confidence enum by id.
*
* @param typeId Realm scope confidence id.
*
* @return ScopeConfidence enum.
*/
public static RealmScope fromID(int typeId) {
for (RealmScope scopeType : RealmScope.values()) {
if (scopeType.ordinal() == typeId) {
return scopeType;
}
}
return null;
}
}
/**
* Enum to encapsulate scope confidence.
*
* We may know for sure that a realm is domain scope or host scope, based
* on where it is found. Occasionally, we may have to infer or assume a scope to
* initially create a realm.
*/
public enum ScopeConfidence {
KNOWN(0, bundle.getString("OsAccountRealm.Known.text")), // realm scope is known for sure.
INFERRED(1, bundle.getString("OsAccountRealm.Inferred.text")); // realm scope is inferred
private final int id;
private final String name;
ScopeConfidence(int id, String name) {
this.id = id;
this.name = name;
}
/**
* Get the id of the realm scope confidence.
*
* @return Realm scope confidence id.
*/
public int getId() {
return id;
}
/**
* Get the realm scope confidence name.
*
* @return Realm scope confidence name.
*/
public String getName() {
return name;
}
/**
* Gets a realm scope confidence enum by id.
*
* @param typeId Realm scope confidence id.
*
* @return ScopeConfidence enum.
*/
public static ScopeConfidence fromID(int typeId) {
for (ScopeConfidence statusType : ScopeConfidence.values()) {
if (statusType.ordinal() == typeId) {
return statusType;
}
}
return null;
}
}
/**
* Set the signature for the account realm.
*
* @param signature Realm signature.
*
* @return Returns true of the address is set, false if the address was not
* changed.
*/
boolean setSignature(String signature) {
if (StringUtils.isNotBlank(signature)) {
this.signature = signature;
return true;
}
return false;
}
/**
* Encapsulates status of realm row.
*/
enum RealmDbStatus {
ACTIVE(0, "Active"),
MERGED(1, "Merged"),
DELETED(2, "Deleted");
private final int id;
private final String name;
RealmDbStatus(int id, String name) {
this.id = id;
this.name = name;
}
int getId() {
return id;
}
String getName() {
return name;
}
static RealmDbStatus fromID(int typeId) {
for (RealmDbStatus type : RealmDbStatus.values()) {
if (type.ordinal() == typeId) {
return type;
}
}
return null;
}
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2020-2022 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;
import com.google.common.annotations.Beta;
import com.google.common.base.Strings;
import org.apache.commons.lang3.StringUtils;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
import org.sleuthkit.datamodel.OsAccountRealm.ScopeConfidence;
import org.sleuthkit.datamodel.SleuthkitCase.CaseDbConnection;
import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
/**
* Create/Retrieve/Update OS account realms. Realms represent either an individual
* host with local accounts or a domain.
*/
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;
/**
* Construct a OsAccountRealmManager for the given SleuthkitCase.
*
* @param skCase The SleuthkitCase
*
*/
OsAccountRealmManager(SleuthkitCase skCase) {
this.db = skCase;
}
/**
* Create realm based on Windows information. The input SID is a user/group
* SID.The domain SID is extracted from this incoming SID.
*
* @param accountSid User/group SID. May be null only if name is not
* null.
* @param realmName Realm name. May be null only if SID is not null.
* @param referringHost Host where realm reference is found.
* @param realmScope Scope of realm. Use UNKNOWN if you are not sure and
* the method will try to detect the correct scope.
*
* @return OsAccountRealm.
*
* @throws TskCoreException If there is an error
* creating the realm.
* @throws OsAccountManager.NotUserSIDException If the SID is not a user
* SID.
*/
public OsAccountRealm newWindowsRealm(String accountSid, String realmName, Host referringHost, OsAccountRealm.RealmScope realmScope) throws TskCoreException, OsAccountManager.NotUserSIDException {
if (realmScope == null) {
throw new TskCoreException("RealmScope cannot be null. Use UNKNOWN if scope is not known.");
}
if (referringHost == null) {
throw new TskCoreException("A referring host is required to create a realm.");
}
if ((StringUtils.isBlank(accountSid) || accountSid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID))
&& StringUtils.isBlank(realmName)) {
throw new TskCoreException("Either an address or a name is required to create a realm.");
}
Host scopeHost;
OsAccountRealm.ScopeConfidence scopeConfidence;
switch (realmScope) {
case DOMAIN:
scopeHost = null;
scopeConfidence = OsAccountRealm.ScopeConfidence.KNOWN;
break;
case LOCAL:
scopeHost = referringHost;
scopeConfidence = OsAccountRealm.ScopeConfidence.KNOWN;
break;
case UNKNOWN:
default:
// NOTE: if there's a well known SID, the scope will be changed to LOCAL later.
// check if the referring host already has a realm
boolean isHostRealmKnown = isHostRealmKnown(referringHost);
if (isHostRealmKnown) {
scopeHost = null; // the realm does not scope to the referring host since it already has one.
scopeConfidence = OsAccountRealm.ScopeConfidence.KNOWN;
} else {
scopeHost = referringHost;
scopeConfidence = OsAccountRealm.ScopeConfidence.INFERRED;
}
break;
}
// get windows realm address from sid
String realmAddr = null;
String resolvedRealmName = WindowsAccountUtils.toWellknownEnglishRealmName(realmName);
if (!Strings.isNullOrEmpty(accountSid) && !accountSid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID)) {
if (!WindowsAccountUtils.isWindowsUserSid(accountSid)) {
throw new OsAccountManager.NotUserSIDException(String.format("SID = %s is not a user SID.", accountSid ));
}
realmAddr = WindowsAccountUtils.getWindowsRealmAddress(accountSid);
if (WindowsAccountUtils.isWindowsWellKnownSid(accountSid)) {
// if the sid is a Windows well known SID, create a local realm for it.
scopeHost = referringHost;
scopeConfidence = OsAccountRealm.ScopeConfidence.KNOWN;
// if the SID is a Windows well known SID, then prefer to use the default well known name to create the realm
String wellKnownRealmName = WindowsAccountUtils.getWindowsWellKnownSidRealmName(accountSid);
if (!StringUtils.isEmpty(wellKnownRealmName)) {
resolvedRealmName = wellKnownRealmName;
}
}
}
String signature = makeRealmSignature(realmAddr, resolvedRealmName, scopeHost);
// create a realm
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
* incoming SID.
*
* @param accountSid Account SID, may be null.
* @param realmName Realm name, may be null only if accountSid is not
* null.
* @param referringHost Referring Host.
*
* @return Optional with OsAccountRealm, Optional.empty if no matching realm
* is found.
*
* @throws TskCoreException
* @throws OsAccountManager.NotUserSIDException If the SID is not a user
* SID.
*/
public Optional<OsAccountRealm> getWindowsRealm(String accountSid, String realmName, Host referringHost) throws TskCoreException, OsAccountManager.NotUserSIDException {
if (referringHost == null) {
throw new TskCoreException("A referring host is required get a realm.");
}
// need at least one of the two, the addr or name to look up
if ((Strings.isNullOrEmpty(accountSid) || accountSid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID) )
&& Strings.isNullOrEmpty(realmName)) {
throw new TskCoreException("Realm address or name is required get a realm.");
}
try (CaseDbConnection connection = this.db.getConnection()) {
return getWindowsRealm(accountSid, realmName, 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 incoming SID.
*
* @param accountSid Account SID, may be null.
* @param realmName Realm name, may be null only if accountSid is not
* null.
* @param referringHost Referring Host.
* @param connection Database connection to use.
*
* @return Optional with OsAccountRealm, Optional.empty if no matching realm is found.
*
* @throws TskCoreException
*/
Optional<OsAccountRealm> getWindowsRealm(String accountSid, String realmName, Host referringHost, CaseDbConnection connection) throws TskCoreException, OsAccountManager.NotUserSIDException {
if (referringHost == null) {
throw new TskCoreException("A referring host is required get a realm.");
}
// need at least one of the two, the addr or name to look up
if ((StringUtils.isBlank(accountSid) || accountSid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID))
&& StringUtils.isBlank(realmName)) {
throw new TskCoreException("Realm address or name is required get a realm.");
}
// If a non null accountSID is provided search for realm by addr.
if (!Strings.isNullOrEmpty(accountSid) && !accountSid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID)) {
if (!WindowsAccountUtils.isWindowsUserSid(accountSid)) {
throw new OsAccountManager.NotUserSIDException(String.format("SID = %s is not a user SID.", accountSid ));
}
// get realm addr from the account SID.
String realmAddr = WindowsAccountUtils.getWindowsRealmAddress(accountSid);
Optional<OsAccountRealm> realm = getRealmByAddr(realmAddr, referringHost, connection);
if (realm.isPresent()) {
return realm;
}
}
// ensure we are using English names for any well known SIDs.
String resolvedRealmName = WindowsAccountUtils.toWellknownEnglishRealmName(realmName);
// No realm addr so search by name.
Optional<OsAccountRealm> realm = getRealmByName(resolvedRealmName, referringHost, connection);
if (realm.isPresent() && !Strings.isNullOrEmpty(accountSid) && !accountSid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID)) {
// If we were given a non-null accountSID, make sure there isn't one set on the matching realm.
// We know it won't match because the previous search by SID failed.
if (realm.get().getRealmAddr().isPresent()) {
return Optional.empty();
}
}
return realm;
}
/**
* 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
* incoming SID.
*
* If a realm is found but is missing either the SID or the realmName, then
* the realm is updated.
*
* @param accountSid Account SID, may be null.
* @param realmName Realm name, may be null only if accountSid is not
* null.
* @param referringHost Referring Host.
* @param connection Database connection to use.
*
* @return OsRealmUpdateResult account update result.
*
* @throws TskCoreException
*/
OsRealmUpdateResult getAndUpdateWindowsRealm(String accountSid, String realmName, Host referringHost, CaseDbConnection connection) throws TskCoreException, OsAccountManager.NotUserSIDException {
// get realm
Optional<OsAccountRealm> realmOptional = getWindowsRealm(accountSid, realmName, referringHost, connection);
// if found, update it if needed
if (realmOptional.isPresent()) {
String realmAddr = (StringUtils.isNotBlank(accountSid) && !accountSid.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID)) ? WindowsAccountUtils.getWindowsRealmAddress(accountSid) : null;
OsRealmUpdateResult realmUpdateResult = updateRealm(realmOptional.get(), realmAddr, realmName, connection);
return realmUpdateResult;
} else {
return new OsRealmUpdateResult(OsRealmUpdateStatus.NO_CHANGE, null);
}
}
/**
* Updates the realm address and/or name, if a non blank address/name is
* specified and the current address/name is blank.
*
* NOTE: This will not merge two realms if the updated information exists
* for another realm (i.e. such as adding an address to a realm that has
* only a name and there is already a realm with that address).
*
*
* @param realm Realm to update.
* @param realmAddr Realm address, may be null if the address doesn't need
* to be updated.
* @param realmName Realm name, may be null if the name doesn't need to be
* updated.
*
* @return OsRealmUpdateResult Update status and updated realm.
*
* @throws TskCoreException If there is a database error or if a realm
* already exists with that information.
*/
public OsRealmUpdateResult updateRealm(OsAccountRealm realm, String realmAddr, String realmName) throws TskCoreException {
try (CaseDbConnection connection = db.getConnection()) {
return updateRealm(realm, realmAddr, realmName, connection);
}
}
/**
* Updates the realm address and/or name, if a non blank address/name is
* specified and the current address/name is blank.
*
* The realm name will not be updated regardless of the value in realmName
* if the passed in realm has an address equal to SPECIAL_WINDOWS_REALM_ADDR.
*
* @param realm Realm to update.
* @param realmAddr Realm address, may be null if the address doesn't need
* to be updated.
* @param realmName Realm name, may be null if the name doesn't need to be
* updated.
* @param connection Current database connection.
*
* @return OsRealmUpdateResult Update status and updated realm.
*
* @throws TskCoreException If there is a database error or if a realm
* already exists with that information.
*/
private OsRealmUpdateResult updateRealm(OsAccountRealm realm, String realmAddr, String realmName, CaseDbConnection connection) throws TskCoreException {
// need at least one of the two
if ( (StringUtils.isBlank(realmAddr) || realmAddr.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID))
&& StringUtils.isBlank(realmName)) {
throw new TskCoreException("Realm address or name is required to update realm.");
}
OsRealmUpdateStatus updateStatusCode = OsRealmUpdateStatus.NO_CHANGE;
OsAccountRealm updatedRealm = null;
db.acquireSingleUserCaseWriteLock();
try {
String currRealmAddr = realm.getRealmAddr().orElse(null);
// set name and address to new values only if the current value is blank and the new value isn't.
if ((StringUtils.isBlank(currRealmAddr) && StringUtils.isNotBlank(realmAddr) && !realmAddr.equalsIgnoreCase(WindowsAccountUtils.WINDOWS_NULL_SID))) {
updateRealmColumn(realm.getRealmId(), "realm_addr", realmAddr, connection);
currRealmAddr = realmAddr;
updateStatusCode = OsRealmUpdateStatus.UPDATED;
}
List<String> realmNames = realm.getRealmNames();
String currRealmName = realmNames.isEmpty() ? null : realmNames.get(0); // currently there is only one name.
// Update realm name if:
// Current realm name is empty
// The passed in realm name is not empty
if (StringUtils.isBlank(currRealmName) && StringUtils.isNotBlank(realmName)) {
updateRealmColumn(realm.getRealmId(), "realm_name", realmName, connection);
updateStatusCode = OsRealmUpdateStatus.UPDATED;
}
// if nothing is to be changed, return
if (updateStatusCode == OsRealmUpdateStatus.NO_CHANGE) {
return new OsRealmUpdateResult(updateStatusCode, realm);
}
// update realm signature - based on the most current address and name
OsAccountRealm currRealm = getRealmByRealmId(realm.getRealmId(), connection);
String newRealmAddr = currRealm.getRealmAddr().orElse(null);
String newRealmName = (currRealm.getRealmNames().isEmpty() == false) ? currRealm.getRealmNames().get(0) : null;
// make new signature
String newSignature = makeRealmSignature(newRealmAddr, newRealmName, realm.getScopeHost().orElse(null));
// Use a random string as the signature if the realm is not active.
String updateSQL = "UPDATE tsk_os_account_realms SET "
+ " realm_signature = "
+ " CASE WHEN db_status = " + OsAccountRealm.RealmDbStatus.ACTIVE.getId() + " THEN ? ELSE realm_signature END "
+ " WHERE id = ?";
PreparedStatement preparedStatement = connection.getPreparedStatement(updateSQL, Statement.NO_GENERATED_KEYS);
preparedStatement.clearParameters();
preparedStatement.setString(1, newSignature); // Is only set for active accounts
preparedStatement.setLong(2, realm.getRealmId());
connection.executeUpdate(preparedStatement);
// read the updated realm
updatedRealm = this.getRealmByRealmId(realm.getRealmId(), connection);
return new OsRealmUpdateResult(updateStatusCode, updatedRealm);
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error updating realm with id = %d, name = %s, addr = %s", realm.getRealmId(), realmName != null ? realmName : "Null", realm.getRealmAddr().orElse("Null")), ex);
} finally {
db.releaseSingleUserCaseWriteLock();
}
}
/**
* Updates specified column in the tsk_os_account_realms table to the specified value.
*
* @param <T> Type of value - must be a String, Long or an Integer.
* @param realmId Id of the realm to be updated.
* @param colName Name of column o be updated.
* @param colValue New column value.
* @param connection Database connection to use.
*
* @throws SQLException If there is an error updating the database.
* @throws TskCoreException If the value type is not handled.
*/
private <T> void updateRealmColumn(long realmId, String colName, T colValue, CaseDbConnection connection) throws SQLException, TskCoreException {
String updateSQL = "UPDATE tsk_os_account_realms "
+ " SET " + colName + " = ? "
+ " WHERE id = ?";
db.acquireSingleUserCaseWriteLock();
try {
PreparedStatement preparedStatement = connection.getPreparedStatement(updateSQL, Statement.NO_GENERATED_KEYS);
preparedStatement.clearParameters();
if (Objects.isNull(colValue)) {
preparedStatement.setNull(1, Types.NULL); // handle null value
} else {
if (colValue instanceof String) {
preparedStatement.setString(1, (String) colValue);
} else if (colValue instanceof Long) {
preparedStatement.setLong(1, (Long) colValue);
} else if (colValue instanceof Integer) {
preparedStatement.setInt(1, (Integer) colValue);
} else {
throw new TskCoreException(String.format("Unhandled column data type received while updating the realm (id = %d) ", realmId));
}
}
preparedStatement.setLong(2, realmId);
connection.executeUpdate(preparedStatement);
} finally {
db.releaseSingleUserCaseWriteLock();
}
}
private final static String REALM_QUERY_STRING = "SELECT realms.id as realm_id, realms.realm_name as realm_name,"
+ " realms.realm_addr as realm_addr, realms.realm_signature as realm_signature, realms.scope_host_id, realms.scope_confidence, realms.db_status,"
+ " hosts.id, hosts.name as host_name "
+ " FROM tsk_os_account_realms as realms"
+ " LEFT JOIN tsk_hosts as hosts"
+ " ON realms.scope_host_id = hosts.id";
/**
* Get the realm from the given row id.
*
* @param id Realm row id.
*
* @return Realm.
* @throws TskCoreException on error
*/
public OsAccountRealm getRealmByRealmId(long id) throws TskCoreException {
try (CaseDbConnection connection = this.db.getConnection()) {
return getRealmByRealmId(id, connection);
}
}
/**
* Get the realm from the given row id.
*
* @param id Realm row id.
* @param connection Database connection to use.
*
* @return Realm.
* @throws TskCoreException
*/
OsAccountRealm getRealmByRealmId(long id, CaseDbConnection connection) throws TskCoreException {
String queryString = REALM_QUERY_STRING
+ " WHERE realms.id = " + id;
db.acquireSingleUserCaseReadLock();
try ( Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
OsAccountRealm accountRealm = null;
if (rs.next()) {
accountRealm = resultSetToAccountRealm(rs);
} else {
throw new TskCoreException(String.format("No realm found with id = %d", id));
}
return accountRealm;
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error running the realms query = %s", queryString), ex);
}
finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Get the realm with the given realm address.
*
* @param realmAddr Realm address.
* @param host Host for realm, may be null.
* @param connection Database connection to use.
*
* @return Optional with OsAccountRealm, Optional.empty if no realm found with matching real address.
*
* @throws TskCoreException.
*/
Optional<OsAccountRealm> getRealmByAddr(String realmAddr, Host host, CaseDbConnection connection) throws TskCoreException {
// If a host is specified, we want to match the realm with matching addr and specified host, or a realm with matching addr and no host.
// If no host is specified, then we return the first realm with matching addr.
String whereHostClause = (host == null)
? " 1 = 1 "
: " ( realms.scope_host_id = " + host.getHostId() + " OR realms.scope_host_id IS NULL) ";
String queryString = REALM_QUERY_STRING
+ " WHERE LOWER(realms.realm_addr) = LOWER('"+ realmAddr + "') "
+ " AND " + whereHostClause
+ " AND realms.db_status = " + OsAccountRealm.RealmDbStatus.ACTIVE.getId()
+ " ORDER BY realms.scope_host_id IS NOT NULL, realms.scope_host_id"; // ensure that non null host_id is at the front
return getRealmUsingQuery(queryString, host, connection);
}
/**
* Get another realm with the given addr
* that's different from the specified realm.
*
* @param realm A known realm, the returned realm should be different from this.
* @param host Host for realm, may be null.
* @param connection Database connection to use.
*
* @return Optional with OsAccountRealm, Optional.empty if no realm found with matching real address.
*
* @throws TskCoreException.
*/
Optional<OsAccountRealm> getAnotherRealmByAddr(OsAccountRealm realm, String realmAddr, Host host, CaseDbConnection connection) throws TskCoreException {
// If the given realm has a host id, then the other realm should have the same host id
// If the given realm has no host id, then the other realm should have no host id
String whereHostClause = realm.getScopeHost().isPresent()
? " ( realms.scope_host_id = " + realm.getScopeHost().get().getHostId() + " ) "
: " realms.scope_host_id IS NULL ";
String queryString = REALM_QUERY_STRING
+ " WHERE LOWER(realms.realm_addr) = LOWER('"+ realmAddr + "') "
+ " AND " + whereHostClause
+ " AND realms.id <> " + realm.getRealmId()
+ " AND realms.db_status = " + OsAccountRealm.RealmDbStatus.ACTIVE.getId()
+ " ORDER BY realms.scope_host_id IS NOT NULL, realms.scope_host_id"; // ensure that non null host_id is at the front
return getRealmUsingQuery(queryString, host, connection);
}
/**
* Get the realm with the given name and specified host.
*
* @param realmName Realm name.
* @param host Host for realm, may be null.
* @param connection Database connection to use.
*
* @return Optional with OsAccountRealm, Optional.empty if no matching realm is found.
* @throws TskCoreException.
*/
Optional<OsAccountRealm> getRealmByName(String realmName, Host host, CaseDbConnection connection) throws TskCoreException {
// If a host is specified, we want to match the realm with matching name and specified host, or a realm with matching name and no host.
// If no host is specified, then we return the first realm with matching name.
String whereHostClause = (host == null)
? " 1 = 1 "
: " ( realms.scope_host_id = " + host.getHostId() + " OR realms.scope_host_id IS NULL ) ";
String queryString = REALM_QUERY_STRING
+ " WHERE LOWER(realms.realm_name) = LOWER('" + realmName + "')"
+ " AND " + whereHostClause
+ " AND realms.db_status = " + OsAccountRealm.RealmDbStatus.ACTIVE.getId()
+ " ORDER BY realms.scope_host_id IS NOT NULL, realms.scope_host_id"; // ensure that non null host_id are at the front
return getRealmUsingQuery(queryString, host, connection);
}
/**
* Get another realm with the given name
* that's different from the specified realm.
*
* @param realm A known realm, the returned realm should be different from this.
* @param realmName Realm name.
* @param host Host for realm, may be null.
* @param connection Database connection to use.
*
* @return Optional with OsAccountRealm, Optional.empty if no matching realm is found.
* @throws TskCoreException.
*/
Optional<OsAccountRealm> getAnotherRealmByName(OsAccountRealm realm, String realmName, Host host, CaseDbConnection connection) throws TskCoreException {
// If the given realm has a host id, then the other realm should have the same host id
// If the given realm has no host id, then the other realm should have no host id
String whereHostClause = realm.getScopeHost().isPresent()
? " ( realms.scope_host_id = " + realm.getScopeHost().get().getHostId() + " ) "
: " realms.scope_host_id IS NULL ";
String queryString = REALM_QUERY_STRING
+ " WHERE LOWER(realms.realm_name) = LOWER('" + realmName + "')"
+ " AND " + whereHostClause
+ " AND realms.id <> " + realm.getRealmId()
+ " AND realms.db_status = " + OsAccountRealm.RealmDbStatus.ACTIVE.getId()
+ " ORDER BY realms.scope_host_id IS NOT NULL, realms.scope_host_id"; // ensure that non null host_id are at the front
return getRealmUsingQuery(queryString, host, connection);
}
/**
* Get the realm using the given realm query.
*
* @param queryString Query string
*
* @param host Host for realm, may be null.
* @param connection Database connection to use.
*
* @return Optional with OsAccountRealm, Optional.empty if no matching realm is found.
* @throws TskCoreException
*/
private Optional<OsAccountRealm> getRealmUsingQuery(String queryString, Host host, CaseDbConnection connection) throws TskCoreException {
db.acquireSingleUserCaseReadLock();
try (Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
OsAccountRealm accountRealm = null;
if (rs.next()) {
Host realmHost = null;
long hostId = rs.getLong("scope_host_id");
if (!rs.wasNull()) {
if (host != null ) {
realmHost = host;
} else {
realmHost = new Host(hostId, rs.getString("host_name"));
}
}
accountRealm = new OsAccountRealm(rs.getLong("realm_id"), rs.getString("realm_name"),
rs.getString("realm_addr"), rs.getString("realm_signature"),
realmHost, ScopeConfidence.fromID(rs.getInt("scope_confidence")),
OsAccountRealm.RealmDbStatus.fromID(rs.getInt("db_status")));
}
return Optional.ofNullable(accountRealm);
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting realm using query = %s", queryString), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Check if there is any realm with a host-scope and KNOWN confidence for the given host.
* If we can assume that a host will have only a single host-scoped realm, then you can
* assume a new realm is domain-scoped when this method returns true. I.e. once we know
* the host-scoped realm, then everything else is domain-scoped.
*
* NOTE: a host may now have several local realms for Windows well known SIDs.
* The above assumption only holds for a non well known SID.
* Caller must take the account SID into consideration when using this method.
*
* @param host Host for which to look for a realm.
*
* @return True if there exists a a realm with the host scope matching the host. False otherwise
*/
private boolean isHostRealmKnown(Host host) throws TskCoreException {
// check if this host has a local known realm aleady, other than the special windows realm.
String queryString = REALM_QUERY_STRING
+ " WHERE realms.scope_host_id = " + host.getHostId()
+ " AND realms.scope_confidence = " + OsAccountRealm.ScopeConfidence.KNOWN.getId()
+ " AND realms.db_status = " + OsAccountRealm.RealmDbStatus.ACTIVE.getId();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
// return true if there is any match.
return rs.next();
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting account realm for with host = %s", host.getName()), ex);
}
finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Creates a OsAccountRealm from the resultset of a REALM_QUERY_STRING query.
*
* @param rs ResultSet
* @return
* @throws SQLException
*/
private OsAccountRealm resultSetToAccountRealm(ResultSet rs) throws SQLException {
long hostId = rs.getLong("scope_host_id");
Host realmHost = null;
if (!rs.wasNull()) {
realmHost = new Host(hostId, rs.getString("host_name"));
}
return new OsAccountRealm(rs.getLong("realm_id"), rs.getString("realm_name"),
rs.getString("realm_addr"), rs.getString("realm_signature"),
realmHost, ScopeConfidence.fromID(rs.getInt("scope_confidence")),
OsAccountRealm.RealmDbStatus.fromID(rs.getInt("db_status")));
}
// /**
// * Get all realms.
// *
// * @return Collection of OsAccountRealm
// */
// Collection<OsAccountRealm> getRealms() throws TskCoreException {
// String queryString = "SELECT realms.id as realm_id, realms.realm_name as realm_name, realms.realm_addr as realm_addr, realms.scope_host_id, realms.scope_confidence, "
// + " hosts.id, hosts.name as host_name "
// + " FROM tsk_os_account_realms as realms"
// + " LEFT JOIN tsk_hosts as hosts"
// + " ON realms.scope_host_id = hosts.id";
//
// db.acquireSingleUserCaseReadLock();
// try (CaseDbConnection connection = this.db.getConnection();
// Statement s = connection.createStatement();
// ResultSet rs = connection.executeQuery(s, queryString)) {
//
// ArrayList<OsAccountRealm> accountRealms = new ArrayList<>();
// while (rs.next()) {
// long hostId = rs.getLong("scope_host_id");
// Host host = null;
// if (!rs.wasNull()) {
// host = new Host(hostId, rs.getString("host_name"));
// }
//
// accountRealms.add(new OsAccountRealm(rs.getLong("realm_id"), rs.getString("realm_name"),
// ScopeConfidence.fromID(rs.getInt("scope_confidence")),
// rs.getString("realm_addr"), host));
// }
//
// return accountRealms;
// } catch (SQLException ex) {
// throw new TskCoreException(String.format("Error running the realms query = %s", queryString), ex);
// }
// finally {
// db.releaseSingleUserCaseReadLock();
// }
// }
/**
* Adds a row to the realms table.
*
* If the add fails, it tries to get the realm, in case the realm already exists.
*
* @param realmName Realm name, may be null.
* @param realmAddr SID or some other identifier. May be null if name
* is not null.
* @param signature Signature, either the address or the name.
* @param host Host, if the realm is host scoped. Can be null
* realm is domain scoped.
* @param scopeConfidence Confidence in realm scope.
*
* @return OsAccountRealm Realm just created.
*
* @throws TskCoreException If there is an internal error.
*/
private OsAccountRealm newRealm(String realmName, String realmAddr, String signature, Host host, OsAccountRealm.ScopeConfidence scopeConfidence) throws TskCoreException {
db.acquireSingleUserCaseWriteLock();
try (CaseDbConnection connection = this.db.getConnection()) {
String realmInsertSQL = "INSERT INTO tsk_os_account_realms(realm_name, realm_addr, realm_signature, scope_host_id, scope_confidence)"
+ " VALUES (?, ?, ?, ?, ?)"; // NON-NLS
PreparedStatement preparedStatement = connection.getPreparedStatement(realmInsertSQL, Statement.RETURN_GENERATED_KEYS);
preparedStatement.clearParameters();
preparedStatement.setString(1, realmName);
preparedStatement.setString(2, realmAddr);
preparedStatement.setString(3, signature);
if (host != null) {
preparedStatement.setLong(4, host.getHostId());
} else {
preparedStatement.setNull(4, java.sql.Types.BIGINT);
}
preparedStatement.setInt(5, scopeConfidence.getId());
connection.executeUpdate(preparedStatement);
// Read back the row id
try (ResultSet resultSet = preparedStatement.getGeneratedKeys();) {
long rowId = resultSet.getLong(1); // last_insert_rowid()
return new OsAccountRealm(rowId, realmName, realmAddr, signature, host, scopeConfidence, OsAccountRealm.RealmDbStatus.ACTIVE);
}
} catch (SQLException ex) {
// Create may have failed if the realm already exists. Try and get the matching realm
try (CaseDbConnection connection = this.db.getConnection()) {
if (!Strings.isNullOrEmpty(realmAddr)) {
Optional<OsAccountRealm> accountRealm = this.getRealmByAddr(realmAddr, host, connection);
if (accountRealm.isPresent()) {
return accountRealm.get();
}
} else if (!Strings.isNullOrEmpty(realmName)) {
Optional<OsAccountRealm> accountRealm = this.getRealmByName(realmName, host, connection);
if (accountRealm.isPresent()) {
return accountRealm.get();
}
}
// some other failure - throw an exception
throw new TskCoreException(String.format("Error creating realm with address = %s and name = %s, with host = %s",
realmAddr != null ? realmAddr : "", realmName != null ? realmName : "", host != null ? host.getName() : ""), ex);
}
} finally {
db.releaseSingleUserCaseWriteLock();
}
}
/**
* Makes a realm signature based on given realm address, name scope host.
*
* The signature is primarily to provide uniqueness in the database.
*
* Signature is built as:
* (addr|name)_(hostId|"DOMAIN")
*
* @param realmAddr Realm address, may be null.
* @param realmName Realm name, may be null only if address is not null.
* @param scopeHost Realm scope host. May be null.
*
* @return Realm Signature.
*
* @throws TskCoreException If there is an error making the signature.
*/
static String makeRealmSignature(String realmAddr, String realmName, Host scopeHost) throws TskCoreException {
// need at least one of the two, the addr or name to look up
if (Strings.isNullOrEmpty(realmAddr) && Strings.isNullOrEmpty(realmName)) {
throw new TskCoreException("Realm address and name can't both be null.");
}
String signature = String.format("%s_%s", !Strings.isNullOrEmpty(realmAddr) ? realmAddr : realmName,
scopeHost != null ? scopeHost.getHostId() : "DOMAIN");
return signature;
}
/**
* Create a random signature for realms that have been merged.
*
* @return The random signature.
*/
private String makeMergedRealmSignature() {
return "MERGED " + UUID.randomUUID().toString();
}
/**
* Move source realm into the destination host or merge with an existing realm.
*
* @param sourceRealm
* @param destHost
* @param trans
* @throws TskCoreException
*/
void moveOrMergeRealm(OsAccountRealm sourceRealm, Host destHost, CaseDbTransaction trans) throws TskCoreException {
// Look for a matching realm by address
Optional<OsAccountRealm> optDestRealmAddr = Optional.empty();
if (sourceRealm.getRealmAddr().isPresent()) {
optDestRealmAddr = db.getOsAccountRealmManager().getRealmByAddr(sourceRealm.getRealmAddr().get(), destHost, trans.getConnection());
}
// Look for a matching realm by name
Optional<OsAccountRealm> optDestRealmName = Optional.empty();
if (!sourceRealm.getRealmNames().isEmpty()) {
optDestRealmName = db.getOsAccountRealmManager().getRealmByName(sourceRealm.getRealmNames().get(0), destHost, trans.getConnection());
}
// Decide how to proceed:
// - If we only got one match:
// -- If the address matched, set destRealm to the matching address realm
// -- If the name matched but the original and the matching realm have different addresses, leave destRealm null (it'll be a move)
// -- If the name matched and at least one of the address fields was null, set destRealm to the matching name realm
// - If we got no matches, leave destRealm null (we'll do a move not a merge)
// - If we got two of the same matches, set destRealm to that realm
// - If we got two different matches:
// -- If the name match has no address set, merge the matching name realm into the matching address realm, then
// set destRealm to the matching address realm
// -- Otherwise we're in the case where the addresses are different. We will consider the address the
// stronger match and set destRealm to the matching address realm and leave the matching name realm as-is.
OsAccountRealm destRealm = null;
if (optDestRealmAddr.isPresent() && optDestRealmName.isPresent()) {
if (optDestRealmAddr.get().getRealmId() == optDestRealmName.get().getRealmId()) {
// The two matches are the same
destRealm = optDestRealmAddr.get();
} else {
if (optDestRealmName.get().getRealmAddr().isPresent()) {
// The addresses are different, so use the one with the matching address
destRealm = optDestRealmAddr.get();
} else {
// Merge the realm with the matching name into the realm with the matching address.
// Reload from database afterward to make sure everything is up-to-date.
mergeRealms(optDestRealmName.get(), optDestRealmAddr.get(), trans);
destRealm = getRealmByRealmId(optDestRealmAddr.get().getRealmId(), trans.getConnection());
}
}
} else if (optDestRealmAddr.isPresent()) {
// Only address matched - use it
destRealm = optDestRealmAddr.get();
} else if (optDestRealmName.isPresent()) {
// Only name matched - check whether both have addresses set.
// Due to earlier checks we know the address fields can't be the same, so
// don't do anything if both have addresses - we consider the address to be a stronger identifier than the name
if (! (optDestRealmName.get().getRealmAddr().isPresent() && sourceRealm.getRealmAddr().isPresent())) {
destRealm = optDestRealmName.get();
}
}
// Move or merge the source realm
if (destRealm == null) {
moveRealm(sourceRealm, destHost, trans);
} else {
mergeRealms(sourceRealm, destRealm, trans);
}
}
/**
* Move a realm to a different host.
* A check should be done to make sure there are no matching realms in
* the destination host before calling this method.
*
* @param sourceRealm The source realm.
* @param destHost The destination host.
* @param trans The open transaction.
*
* @throws TskCoreException
*/
private void moveRealm(OsAccountRealm sourceRealm, Host destHost, CaseDbTransaction trans) throws TskCoreException {
try(Statement s = trans.getConnection().createStatement()) {
String query = "UPDATE tsk_os_account_realms SET scope_host_id = " + destHost.getHostId() + " WHERE id = " + sourceRealm.getRealmId();
s.executeUpdate(query);
} catch (SQLException ex) {
throw new TskCoreException("Error moving realm with id: " + sourceRealm.getRealmId() + " to host with id: " + destHost.getHostId(), ex);
}
}
/**
* Merge one realm into another, moving or combining all associated OsAccounts.
*
* @param sourceRealm The sourceRealm realm.
* @param destRealm The destination realm.
* @param trans The open transaction.
*
* @throws TskCoreException
*/
void mergeRealms(OsAccountRealm sourceRealm, OsAccountRealm destRealm, CaseDbTransaction trans) throws TskCoreException {
// Update accounts
db.getOsAccountManager().mergeOsAccountsForRealms(sourceRealm, destRealm, trans);
// Update the sourceRealm realm
CaseDbConnection connection = trans.getConnection();
try (Statement statement = connection.createStatement()) {
String updateStr = "UPDATE tsk_os_account_realms SET db_status = " + OsAccountRealm.RealmDbStatus.MERGED.getId()
+ ", merged_into = " + destRealm.getRealmId()
+ ", realm_signature = '" + makeMergedRealmSignature() + "' "
+ " WHERE id = " + sourceRealm.getRealmId();
connection.executeUpdate(statement, updateStr);
} catch (SQLException ex) {
throw new TskCoreException ("Error updating status of realm with id: " + sourceRealm.getRealmId(), ex);
}
// Update the destination realm if it doesn't have the name or addr set and the source realm does
if (!destRealm.getRealmAddr().isPresent() && sourceRealm.getRealmAddr().isPresent()) {
updateRealm(destRealm, sourceRealm.getRealmAddr().get(), null, trans.getConnection());
} else if (destRealm.getRealmNames().isEmpty() && !sourceRealm.getRealmNames().isEmpty()) {
updateRealm(destRealm, null, sourceRealm.getRealmNames().get(0), trans.getConnection());
}
}
/**
* Get all realms associated with the given host.
*
* @param host The host.
* @param connection The current database connection.
*
* @return List of realms for the given host.
*
* @throws TskCoreException
*/
List<OsAccountRealm> getRealmsByHost(Host host, CaseDbConnection connection) throws TskCoreException {
List<OsAccountRealm> results = new ArrayList<>();
String queryString = REALM_QUERY_STRING
+ " WHERE realms.scope_host_id = " + host.getHostId();
db.acquireSingleUserCaseReadLock();
try ( Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
while (rs.next()) {
results.add(resultSetToAccountRealm(rs));
}
return results;
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error gettings realms for host with id = " + host.getHostId()), ex);
}
finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Status of a realm update.
*/
public enum OsRealmUpdateStatus {
NO_CHANGE, /// no change was made to account.
UPDATED, /// account was updated
MERGED /// account update triggered a merge
}
/**
* Container to encapsulate the status returned by the realm update api, and
* the updated realm.
*/
public final static class OsRealmUpdateResult {
private final OsRealmUpdateStatus updateStatus;
private final OsAccountRealm updatedRealm;
OsRealmUpdateResult(OsRealmUpdateStatus updateStatus, OsAccountRealm updatedRealm) {
this.updateStatus = updateStatus;
this.updatedRealm = updatedRealm;
}
public OsRealmUpdateStatus getUpdateStatus() {
return updateStatus;
}
public Optional<OsAccountRealm> getUpdatedRealm() {
return Optional.ofNullable(updatedRealm);
}
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2021 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;
import java.util.Objects;
/**
* Encapsulates a person.
*/
public final class Person {
private final long id;
private String name;
Person(long id, String name) {
this.id = id;
this.name = name;
}
/**
* Gets the row id for the person.
*
* @return Row id.
*/
public long getPersonId() {
return id;
}
/**
* Gets the name for the person.
*
* @return Person name.
*/
public String getName() {
return name;
}
/**
* Sets the name for the person. Does not update the database.
*
* @param newName The new name.
*/
public void setName(String newName) {
this.name = newName;
}
@Override
public int hashCode() {
int hash = 5;
hash = 67 * hash + (int) (this.id ^ (this.id >>> 32));
hash = 67 * hash + Objects.hashCode(this.name);
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 Person other = (Person) obj;
if (this.id != other.id) {
return false;
}
if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
return false;
}
return true;
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2021 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;
import com.google.common.base.Strings;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.sleuthkit.datamodel.SleuthkitCase.CaseDbConnection;
import org.sleuthkit.datamodel.TskEvent.PersonsAddedTskEvent;
/**
* Responsible for creating/updating/retrieving Persons.
*/
public final class PersonManager {
private final SleuthkitCase db;
/**
* Construct a PersonManager for the given SleuthkitCase.
*
* @param skCase The SleuthkitCase
*
*/
PersonManager(SleuthkitCase skCase) {
this.db = skCase;
}
/**
* Get all persons in the database.
*
* @return List of persons
*
* @throws TskCoreException
*/
public List<Person> getPersons() throws TskCoreException {
String queryString = "SELECT * FROM tsk_persons";
List<Person> persons = new ArrayList<>();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
while (rs.next()) {
persons.add(new Person(rs.getLong("id"), rs.getString("name")));
}
return persons;
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting persons"), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Update the database to match the given Person.
*
* @param person The person to update.
*
* @return person The person that was updated.
*
* @throws TskCoreException
*/
public Person updatePerson(Person person) throws TskCoreException {
// Must have a non-empty name
if (Strings.isNullOrEmpty(person.getName())) {
throw new TskCoreException("Illegal argument passed to updatePerson: Name field for person with ID " + person.getPersonId() + " is null/empty. Will not update database.");
}
String queryString = "UPDATE tsk_persons"
+ " SET name = ? WHERE id = " + person.getPersonId();
db.acquireSingleUserCaseWriteLock();
try (CaseDbConnection connection = db.getConnection()) {
PreparedStatement s = connection.getPreparedStatement(queryString, Statement.NO_GENERATED_KEYS);
s.clearParameters();
s.setString(1, person.getName());
s.executeUpdate();
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error updating person with id = %d", person.getPersonId()), ex);
} finally {
db.releaseSingleUserCaseWriteLock();
}
db.fireTSKEvent(new TskEvent.PersonsUpdatedTskEvent(Collections.singletonList(person)));
return person;
}
/**
* Delete a person. Name comparison is case-insensitive.
*
* @param name Name of the person to delete
*
* @throws TskCoreException
*/
public void deletePerson(String name) throws TskCoreException {
String queryString = "DELETE FROM tsk_persons"
+ " WHERE LOWER(name) = LOWER(?)";
Person deletedPerson = null;
db.acquireSingleUserCaseWriteLock();
try (CaseDbConnection connection = db.getConnection()) {
PreparedStatement s = connection.getPreparedStatement(queryString, Statement.RETURN_GENERATED_KEYS);
s.clearParameters();
s.setString(1, name);
s.executeUpdate();
try (ResultSet resultSet = s.getGeneratedKeys()) {
if (resultSet.next()) {
deletedPerson = new Person(resultSet.getLong(1), name);
}
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error deleting person with name %s", name), ex);
} finally {
db.releaseSingleUserCaseWriteLock();
}
if (deletedPerson != null) {
db.fireTSKEvent(new TskEvent.PersonsDeletedTskEvent(Collections.singletonList(deletedPerson.getPersonId())));
}
}
/**
* Get person with given name. Name comparison is case-insensitive.
*
* @param name Person name to look for.
*
* @return Optional with person. Optional.empty if no matching person is
* found.
*
* @throws TskCoreException
*/
public Optional<Person> getPerson(String name) throws TskCoreException {
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection()) {
return getPerson(name, connection);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Get person with given id.
*
* @param id Id of the person to look for.
*
* @return Optional with person. Optional.empty if no matching person is
* found.
*
* @throws TskCoreException
*/
public Optional<Person> getPerson(long id) throws TskCoreException {
String queryString = "SELECT * FROM tsk_persons WHERE id = " + id;
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
if (rs.next()) {
return Optional.of(new Person(rs.getLong("id"), rs.getString("name")));
} else {
return Optional.empty();
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting persons"), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Create a person with specified name. If a person already exists with the
* given name, it returns the existing person. Name comparison is
* case-insensitive.
*
* @param name Person name.
*
* @return Person with the specified name.
*
* @throws TskCoreException
*/
public Person newPerson(String name) throws TskCoreException {
// Must have a name
if (Strings.isNullOrEmpty(name)) {
throw new TskCoreException("Illegal argument passed to createPerson: Non-empty name is required.");
}
Person toReturn = null;
CaseDbConnection connection = null;
db.acquireSingleUserCaseWriteLock();
try {
connection = db.getConnection();
// First try to load it from the database. This is a case-insensitive look-up
// to attempt to prevent having two entries with the same lower-case name.
Optional<Person> person = getPerson(name, connection);
if (person.isPresent()) {
return person.get();
}
// Attempt to insert the new Person.
String personInsertSQL = "INSERT INTO tsk_persons(name) VALUES (?)"; // NON-NLS
PreparedStatement preparedStatement = connection.getPreparedStatement(personInsertSQL, Statement.RETURN_GENERATED_KEYS);
preparedStatement.clearParameters();
preparedStatement.setString(1, name);
connection.executeUpdate(preparedStatement);
// Read back the row id.
try (ResultSet resultSet = preparedStatement.getGeneratedKeys();) {
if (resultSet.next()) {
toReturn = new Person(resultSet.getLong(1), name); //last_insert_rowid()
} else {
throw new SQLException("Error executing SQL: " + personInsertSQL);
}
}
} catch (SQLException ex) {
if (connection != null) {
// The insert may have failed because this person was just added on another thread, so try getting the person again.
// (Note: the SingleUserCaseWriteLock is a no-op for multi-user cases so acquiring it does not prevent this situation)
Optional<Person> person = getPerson(name, connection);
if (person.isPresent()) {
return person.get();
}
}
throw new TskCoreException(String.format("Error adding person with name = %s", name), ex);
} finally {
db.releaseSingleUserCaseWriteLock();
}
if (toReturn != null) {
db.fireTSKEvent(new PersonsAddedTskEvent(Collections.singletonList(toReturn)));
}
return toReturn;
}
/**
* Get all hosts associated with the given person.
*
* @param person The person.
*
* @return The list of hosts corresponding to the person.
*
* @throws TskCoreException Thrown if there is an issue querying the case
* database.
*/
public List<Host> getHostsForPerson(Person person) throws TskCoreException {
return executeHostsQuery("SELECT * FROM tsk_hosts WHERE person_id = " + person.getPersonId());
}
/**
* Gets all hosts not associated with any person.
*
* @return The hosts.
*
* @throws TskCoreException Thrown if there is an issue querying the case
* database.
*/
public List<Host> getHostsWithoutPersons() throws TskCoreException {
return executeHostsQuery("SELECT * FROM tsk_hosts WHERE person_id IS NULL");
}
/**
* Executes a query of the tsk_hosts table in the case database.
*
* @param hostsQuery The SQL query to execute.
*
* @throws TskCoreException Thrown if there is an issue querying the case
* database.
*
* @throws TskCoreException
*/
private List<Host> executeHostsQuery(String hostsQuery) throws TskCoreException {
String sql = hostsQuery + " AND db_status = " + Host.HostDbStatus.ACTIVE.getId();
List<Host> hosts = new ArrayList<>();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, sql)) {
while (rs.next()) {
hosts.add(new Host(rs.getLong("id"), rs.getString("name"), Host.HostDbStatus.fromID(rs.getInt("db_status"))));
}
return hosts;
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error executing '" + sql + "'"), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Get person with given name. Name comparison is case-insensitive.
*
* @param name Person name to look for.
* @param connection Database connection to use.
*
* @return Optional with person. Optional.empty if no matching person is
* found.
*
* @throws TskCoreException
*/
private Optional<Person> getPerson(String name, CaseDbConnection connection) throws TskCoreException {
String queryString = "SELECT * FROM tsk_persons"
+ " WHERE LOWER(name) = LOWER(?)";
try {
PreparedStatement s = connection.getPreparedStatement(queryString, Statement.RETURN_GENERATED_KEYS);
s.clearParameters();
s.setString(1, name);
try (ResultSet rs = s.executeQuery()) {
if (!rs.next()) {
return Optional.empty(); // no match found
} else {
return Optional.of(new Person(rs.getLong("id"), rs.getString("name")));
}
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting person with name = %s", name), ex);
}
}
/**
* Get person for the given host or empty if no associated person.
*
* @param host The host.
*
* @return The parent person or empty if no parent person.
*
* @throws TskCoreException if error occurs.
*/
public Optional<Person> getPerson(Host host) throws TskCoreException {
String queryString = "SELECT p.id AS personId, p.name AS name FROM \n"
+ "tsk_persons p INNER JOIN tsk_hosts h\n"
+ "ON p.id = h.person_id \n"
+ "WHERE h.id = " + host.getHostId();
db.acquireSingleUserCaseReadLock();
try (CaseDbConnection connection = this.db.getConnection();
Statement s = connection.createStatement();
ResultSet rs = connection.executeQuery(s, queryString)) {
if (rs.next()) {
return Optional.of(new Person(rs.getLong("personId"), rs.getString("name")));
} else {
return Optional.empty();
}
} catch (SQLException ex) {
throw new TskCoreException(String.format("Error getting person for host with ID = %d", host.getHostId()), ex);
} finally {
db.releaseSingleUserCaseReadLock();
}
}
/**
* Adds one or more hosts to a person.
*
* @param person The person.
* @param hosts The hosts.
*
* @throws TskCoreException Thrown if the operation cannot be completed.
*/
public void addHostsToPerson(Person person, List<Host> hosts) throws TskCoreException {
if (person == null) {
throw new TskCoreException("Illegal argument: person must be non-null");
}
if (hosts == null || hosts.isEmpty()) {
throw new TskCoreException("Illegal argument: hosts must be non-null and non-empty");
}
executeHostsUpdate(person, getHostIds(hosts), new TskEvent.HostsAddedToPersonTskEvent(person, hosts));
}
/**
* Removes one or more hosts from a person.
*
* @param person The person.
* @param hosts The hosts.
*
* @throws TskCoreException Thrown if the operation cannot be completed.
*/
public void removeHostsFromPerson(Person person, List<Host> hosts) throws TskCoreException {
if (person == null) {
throw new TskCoreException("Illegal argument: person must be non-null");
}
if (hosts == null || hosts.isEmpty()) {
throw new TskCoreException("Illegal argument: hosts must be non-null and non-empty");
}
List<Long> hostIds = getHostIds(hosts);
executeHostsUpdate(null, hostIds, new TskEvent.HostsRemovedFromPersonTskEvent(person, hostIds));
}
/**
* Executes an update of the person_id column for one or more hosts in the
* tsk_hosts table in the case database.
*
* @param person The person to get the person ID from or null if the person
* ID of the hosts should be set to NULL.
* @param hostIds The host IDs of the hosts.
* @param event A TSK event to be published if the update succeeds.
*
* @throws TskCoreException Thrown if the update fails.
*/
private void executeHostsUpdate(Person person, List<Long> hostIds, TskEvent event) throws TskCoreException {
String updateSql = null;
db.acquireSingleUserCaseWriteLock();
try (CaseDbConnection connection = this.db.getConnection(); Statement statement = connection.createStatement()) {
updateSql = (person == null)
? String.format("UPDATE tsk_hosts SET person_id = NULL")
: String.format("UPDATE tsk_hosts SET person_id = %d", person.getPersonId());
String hostIdsCsvList = hostIds.stream()
.map(hostId -> hostId.toString())
.collect(Collectors.joining(","));
updateSql += " WHERE id IN (" + hostIdsCsvList + ")";
statement.executeUpdate(updateSql);
db.fireTSKEvent(event);
} catch (SQLException ex) {
throw new TskCoreException(String.format(updateSql == null ? "Error connecting to case database" : "Error executing '" + updateSql + "'"), ex);
} finally {
db.releaseSingleUserCaseWriteLock();
}
}
/**
* Gets a list of host IDs from a list of hosts.
*
* @param hosts The hosts.
*
* @return The host IDs.
*/
private List<Long> getHostIds(List<Host> hosts) {
List<Long> hostIds = new ArrayList<>();
hostIds.addAll(hosts.stream()
.map(host -> host.getHostId())
.collect(Collectors.toList()));
return hostIds;
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2011-2017 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;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.sleuthkit.datamodel.TskData.TSK_POOL_TYPE_ENUM;
/**
* Represents a pool. Populated based on data in database.
*/
public class Pool extends AbstractContent {
private static final Logger logger = Logger.getLogger(Pool.class.getName());
private volatile long poolHandle = 0;
private final long type;
/**
* Constructor most inputs are from the database
*
* @param db case database handle
* @param obj_id the unique content object id for the pool
* @param name name of the pool
* @param type type of the pool
*/
protected Pool(SleuthkitCase db, long obj_id, String name, long type) {
super(db, obj_id, name);
this.type = type;
}
@Override
public int read(byte[] readBuffer, long offset, long len) throws TskCoreException {
synchronized (this) {
if (poolHandle == 0) {
getPoolHandle();
}
}
return SleuthkitJNI.readPool(poolHandle, readBuffer, offset, len);
}
@Override
public long getSize() {
try {
return getParent().getSize();
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error getting parent of pool with obj ID {0}", getId());
return 0;
}
}
/**
* get the type
*
* @return type
*/
public TSK_POOL_TYPE_ENUM getType() {
return TskData.TSK_POOL_TYPE_ENUM.valueOf(type);
}
/**
* Lazily loads the internal pool structure: won't be loaded until
* this is called and maintains the handle to it to reuse it
*
* @return a pool pointer from the sleuthkit
*
* @throws TskCoreException exception throw if an internal tsk core error
* occurs
*/
long getPoolHandle() throws TskCoreException {
// Note that once poolHandle is set, it will never be changed or reset to zero
if (poolHandle == 0) {
synchronized (this) {
if (poolHandle == 0) {
Content dataSource = getDataSource();
if ((dataSource != null) && (dataSource instanceof Image)) {
Image image = (Image) dataSource;
poolHandle = SleuthkitJNI.openPool(image.getImageHandle(), getPoolOffset(image), getSleuthkitCase());
} else {
throw new TskCoreException("Data Source of pool is not an image");
}
}
}
}
return this.poolHandle;
}
/**
* Get the offset of the pool from the parent object.
* Needs to be in bytes.
*
* @return the offset to the pool
*/
private long getPoolOffset(Image image) throws TskCoreException {
if (this.getParent() instanceof Image) {
// If the parent is an image, then the pool starts at offset zero
return 0;
} else if (this.getParent() instanceof Volume) {
// If the parent is a volume, then the pool starts at the volume offset
Volume parent = (Volume)this.getParent();
if (parent.getParent() instanceof VolumeSystem) {
// uses block size from parent volume system
return parent.getStart() * ((VolumeSystem) parent.getParent()).getBlockSize(); // Offset needs to be in bytes
} else {
// uses sector size from parent image (old behavior fallback)
return parent.getStart() * image.getSsize(); // Offset needs to be in bytes
}
}
throw new TskCoreException("Pool with object ID " + this.getId() + " does not have Image or Volume parent");
}
@Override
public void close() {
// Pools will be closed during case closing by the JNI code.
}
@SuppressWarnings("deprecation")
@Override
protected void finalize() throws Throwable {
try {
close();
} finally {
super.finalize();
}
}
@Override
public <T> T accept(SleuthkitItemVisitor<T> v) {
return v.visit(this);
}
@Override
public <T> T accept(ContentVisitor<T> v) {
return v.visit(this);
}
@Override
public List<Content> getChildren() throws TskCoreException {
return getSleuthkitCase().getPoolChildren(this);
}
@Override
public List<Long> getChildrenIds() throws TskCoreException {
return getSleuthkitCase().getPoolChildrenIds(this);
}
@Override
public String toString(boolean preserveState) {
return super.toString(preserveState) + "Pool [\t" + "type " + type + "]\t"; //NON-NLS
}
}
/*
* Sleuth Kit Data Model
*
* Copyright 2011-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;
import java.io.IOException;
import java.io.InputStream;
/**
* InputStream to read bytes from a Content object's data
*/
public final class ReadContentInputStream extends InputStream {
private long currentOffset;
private final long contentSize;
private final Content content;
public ReadContentInputStream(Content content) {
this.content = content;
this.currentOffset = 0;
this.contentSize = content.getSize();
}
@Override
public int read() throws ReadContentInputStreamException {
byte[] buff = new byte[1];
return (read(buff) != -1) ? buff[0] : -1;
}
@Override
public int read(byte[] b) throws ReadContentInputStreamException {
return read(b, 0, b.length);
}
@Override
public int read(byte[] b, int off, int len) throws ReadContentInputStreamException {
final int buffLen = b.length;
//must return 0 for zero-length arrays
if (buffLen == 0 || len == 0) {
return 0;
}
//would get an error from TSK if we try to read an empty file
if (contentSize == 0) {
return -1;
}
// check off. Must be in bounds of buffer
if (off < 0 || off >= buffLen) {
return -1;
}
//eof, no data remains to be read
if (currentOffset >= contentSize) {
return -1;
}
// Is the file big enough for the full request?
int lenToRead = (int) Math.min(contentSize - currentOffset, len);
// is the buffer big enough?
lenToRead = Math.min(lenToRead, buffLen - off);
byte[] retBuf;
if (off == 0) {
//write directly to user buffer
retBuf = b;
} else {
//write to a temp buffer, then copy to user buffer
retBuf = new byte[lenToRead];
}
try {
final int lenRead = content.read(retBuf, currentOffset, lenToRead);
if (lenRead == 0 || lenRead == -1) {
//error or no more bytes to read, report EOF
return -1;
} else {
currentOffset += lenRead;
//if read into user-specified offset, copy back from temp buffer to user
if (off != 0) {
System.arraycopy(retBuf, 0, b, off, lenRead);
}
return lenRead;
}
} catch (TskCoreException ex) {
throw new ReadContentInputStreamException(String.format("Error reading file '%s' (id=%d) at offset %d.", content.getName(), content.getId(), currentOffset), ex);
}
}
@Override
public int available() throws IOException {
long len = contentSize - currentOffset;
if (len < 0) {
return 0;
}
return (int) len;
}
@Override
public long skip(long n) throws IOException {
//more efficient skip() implementation than superclass
//as it does not involve reads
long toSkip = Math.min(n, contentSize - currentOffset); //allow to skip to EOF
currentOffset += toSkip;
return toSkip;
//0 1 2 3 4 5 len: 6
}
@Override
public void close() throws IOException {
super.close();
//nothing to be done currently, file handles are closed when content is gc'ed
}
@Override
public boolean markSupported() {
return false;
}
/// additional methods to facilitate stream seeking
/**
* Get total length of the stream
*
* @return number of bytes that can be read from this stream
*/
public long getLength() {
return contentSize;
}
/**
* Get current position in the stream
*
* @return current offset in bytes
*/
public long getCurPosition() {
return currentOffset;
}
/**
* Set new current position in the stream, up to and including EOF
*
* @param newPosition new position in the stream to be set
*
* @return the actual position set, which can be less than position passed
* in if EOF has been reached
*/
public long seek(long newPosition) {
if (newPosition < 0) {
throw new IllegalArgumentException("Illegal negative new position in the stream");
}
currentOffset = Math.min(newPosition, contentSize);
return currentOffset;
}
/**
* Exception thrown when there's an error reading from the
* ReadContentInputStream.
*/
public final static class ReadContentInputStreamException extends IOException {
private static final long serialVersionUID = 1L;
public ReadContentInputStreamException(String message) {
super(message);
}
public ReadContentInputStreamException(String message, Throwable cause) {
super(message, cause);
}
}
}
/*
* SleuthKit Java Bindings
*
* Copyright 2017-18 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;
import java.util.Arrays;
import java.util.Collections;
import static java.util.Collections.singleton;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG;
import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT;
import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG;
import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE;
import static org.sleuthkit.datamodel.CollectionUtils.hashSetOf;
/**
* A relationship between Accounts, such as a communication ( email, sms, phone
* call (call log) ) or presence in a contact book.
*/
public final class Relationship {
public static final class Type {
private final String displayName;
private final String typeName;
private final int typeID;
public static final Relationship.Type MESSAGE = new Type("MESSAGE", "Message", 1);
public static final Relationship.Type CALL_LOG = new Type("CALL_LOG", "Call Log", 2);
public static final Relationship.Type CONTACT = new Type("CONTACT", "Contact", 3);
private final static HashMap<Type, Set<Integer>> typesToArtifactTypeIDs = new HashMap<Type, Set<Integer>>();
static {
typesToArtifactTypeIDs.put(MESSAGE, hashSetOf(
TSK_EMAIL_MSG.getTypeID(),
TSK_MESSAGE.getTypeID()));
typesToArtifactTypeIDs.put(CALL_LOG, singleton(
TSK_CALLLOG.getTypeID()));
typesToArtifactTypeIDs.put(CONTACT, singleton(
TSK_CONTACT.getTypeID()));
}
private static final Set<Type> PREDEFINED_COMMUNICATION_TYPES
= Collections.unmodifiableSet(new HashSet<Relationship.Type>(Arrays.asList(
MESSAGE, CALL_LOG)));
/**
* Subset of predefined types that represent communications.
*
* @return A subset of predefined types that represent communications.
*
*/
static Set<Relationship.Type> getPredefinedCommunicationTypes() {
return PREDEFINED_COMMUNICATION_TYPES;
}
private Type(String name, String displayName, int id) {
this.typeName = name;
this.displayName = displayName;
this.typeID = id;
}
/**
* Get the display name.
*
* @return The display name.
*/
public String getDisplayName() {
return displayName;
}
/**
* Get the unique type name
*
* @return The unique type name.
*/
public String getTypeName() {
return typeName;
}
/**
* Get the id of this type.
*
* @return The type ID.
*/
public int getTypeID() {
return typeID;
}
@Override
public int hashCode() {
int hash = 7;
hash = 37 * hash + (this.typeName != null ? this.typeName.hashCode() : 0);
hash = 37 * hash + this.typeID;
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 Type other = (Type) obj;
if (this.typeID != other.typeID) {
return false;
}
if ((this.typeName == null) ? (other.typeName != null) : !this.typeName.equals(other.typeName)) {
return false;
}
return true;
}
@Override
public String toString() {
return "{" + this.getClass().getName() + ": typeID=" + typeName + ", displayName=" + this.displayName + ", typeName=" + this.typeName + "}";
}
/**
* Is this type creatable from the given artifact. Specifically do they
* have compatible types.
*
* @param relationshipArtifact the relationshipArtifact to test
* creatability from
*
* @return if a relationship of this type can be created from the given
* artifact.
*/
boolean isCreatableFrom(BlackboardArtifact relationshipArtifact) {
Set<Integer> get = typesToArtifactTypeIDs.get(this);
return get != null && get.contains(relationshipArtifact.getArtifactTypeID());
}
}
}