Merge pull request #6079 from dannysmyda/6474-Update-Case-Report-Module

6474 update case report module
This commit is contained in:
Ann Priestman 2020-07-17 08:11:54 -04:00 committed by GitHub
commit 1cd48f4480
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 454 additions and 835 deletions

View File

@ -1,466 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2018-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.autopsy.report.modules.caseuco;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.IOException;
import java.nio.file.Path;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.Case.CaseType;
import org.sleuthkit.autopsy.datamodel.ContentUtils;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.SleuthkitCase;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Writes Autopsy DataModel objects to Case UCO format.
*
* Clients are expected to add the Case first. Then they should add each data
* source before adding any files for that data source.
*
* Here is an example, where we add everything:
*
* Path directory = Paths.get("C:", "Reports");
* CaseUcoReportGenerator caseUco = new CaseUcoReportGenerator(directory, "my-report");
*
* Case caseObj = Case.getCurrentCase();
* caseUco.addCase(caseObj);
* List<Content> dataSources = caseObj.getDataSources();
* for(Content dataSource : dataSources) {
* caseUco.addDataSource(dataSource, caseObj);
* List<AbstractFile> files = getAllFilesInDataSource(dataSource);
* for(AbstractFile file : files) {
* caseUco.addFile(file, dataSource);
* }
* }
*
* Path reportOutput = caseUco.generateReport();
* //Done. Report at - "C:\Reports\my-report.json-ld"
*
* Please note that the life cycle for this class ends with generateReport().
* The underlying file handle to 'my-report.json-ld' will be closed. Any further
* calls to addX() will result in an IOException.
*/
public final class CaseUcoReportGenerator {
private static final String EXTENSION = "json-ld";
private final TimeZone timeZone;
private final Path reportPath;
private final JsonGenerator reportGenerator;
/**
* Creates a CaseUCO Report Generator that writes a report in the specified
* directory.
*
* TimeZone is assumed to be GMT+0 for formatting file creation time,
* accessed time and modified time.
*
* @param directory Directory to write the CaseUCO report file. Assumes the
* calling thread has write access to the directory and that the directory
* exists.
* @param reportName Name of the CaseUCO report file.
* @throws IOException If an I/O error occurs
*/
public CaseUcoReportGenerator(Path directory, String reportName) throws IOException {
this.reportPath = directory.resolve(reportName + "." + EXTENSION);
JsonFactory jsonGeneratorFactory = new JsonFactory();
reportGenerator = jsonGeneratorFactory.createGenerator(reportPath.toFile(), JsonEncoding.UTF8);
// Puts a newline between each Key, Value pair for readability.
reportGenerator.setPrettyPrinter(new DefaultPrettyPrinter()
.withObjectIndenter(new DefaultIndenter(" ", "\n")));
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
reportGenerator.setCodec(mapper);
reportGenerator.writeStartObject();
reportGenerator.writeFieldName("@graph");
reportGenerator.writeStartArray();
//Assume GMT+0
this.timeZone = new SimpleTimeZone(0, "GMT");
}
/**
* Adds an AbstractFile instance to the Case UCO report.
*
* @param file AbstractFile instance to write
* @param parentDataSource The parent data source for this abstract file. It
* is assumed that this parent has been written to the report (via
* addDataSource) prior to this call. Otherwise, the report may be invalid.
* @throws IOException If an I/O error occurs.
* @throws TskCoreException
*/
public void addFile(AbstractFile file, Content parentDataSource) throws IOException, TskCoreException {
addFile(file, parentDataSource, null);
}
/**
* Adds an AbstractFile instance to the Case UCO report.
*
* @param file AbstractFile instance to write
* @param parentDataSource The parent data source for this abstract file. It
* is assumed that this parent has been written to the report (via
* addDataSource) prior to this call. Otherwise, the report may be invalid.
* @param localPath The location of the file on secondary storage, somewhere
* other than the case. Example: local disk. This value will be ignored if
* it is null.
* @throws IOException
* @throws TskCoreException
*/
public void addFile(AbstractFile file, Content parentDataSource, Path localPath) throws IOException, TskCoreException {
String fileTraceId = getFileTraceId(file);
//Create the Trace CASE node, which will contain attributes about some evidence.
//Trace is the standard term for evidence. For us, this means file system files.
CASENode fileTrace = new CASENode(fileTraceId, "Trace");
//The bits of evidence for each Trace node are contained within Property
//Bundles. There are a number of Property Bundles available in the CASE ontology.
//Build up the File Property Bundle, as the name implies - properties of
//the file itself.
CASEPropertyBundle filePropertyBundle = createFileBundle(file);
fileTrace.addBundle(filePropertyBundle);
//Build up the ContentData Property Bundle, as the name implies - properties of
//the File data itself.
CASEPropertyBundle contentDataPropertyBundle = createContentDataBundle(file);
fileTrace.addBundle(contentDataPropertyBundle);
if(localPath != null) {
String urlTraceId = getURLTraceId(file);
CASENode urlTrace = new CASENode(urlTraceId, "Trace");
CASEPropertyBundle urlPropertyBundle = new CASEPropertyBundle("URL");
urlPropertyBundle.addProperty("fullValue", localPath.toString());
urlTrace.addBundle(urlPropertyBundle);
contentDataPropertyBundle.addProperty("dataPayloadReferenceUrl", urlTraceId);
reportGenerator.writeObject(urlTrace);
}
//Create the Relationship CASE node. This defines how the Trace CASE node described above
//is related to another CASE node (in this case, the parent data source).
String relationshipID = getRelationshipId(file);
CASENode relationship = createRelationshipNode(relationshipID,
fileTraceId, getDataSourceTraceId(parentDataSource));
//Build up the PathRelation bundle for the relationship node,
//as the name implies - the Path of the Trace in the data source.
CASEPropertyBundle pathRelationPropertyBundle = new CASEPropertyBundle("PathRelation");
pathRelationPropertyBundle.addProperty("path", file.getUniquePath());
relationship.addBundle(pathRelationPropertyBundle);
//This completes the triage, write them to JSON.
reportGenerator.writeObject(fileTrace);
reportGenerator.writeObject(relationship);
}
private String getURLTraceId(Content content) {
return "url-" + content.getId();
}
/**
* All relationship nodes will be the same within our context. Namely, contained-within
* and isDirectional as true.
*/
private CASENode createRelationshipNode(String relationshipID, String sourceID, String targetID) {
CASENode relationship = new CASENode(relationshipID, "Relationship");
relationship.addProperty("source", sourceID);
relationship.addProperty("target", targetID);
relationship.addProperty("kindOfRelationship", "contained-within");
relationship.addProperty("isDirectional", true);
return relationship;
}
/**
* Creates a File Property Bundle with a selection of file attributes.
*/
private CASEPropertyBundle createFileBundle(AbstractFile file) throws TskCoreException {
CASEPropertyBundle filePropertyBundle = new CASEPropertyBundle("File");
String createdTime = ContentUtils.getStringTimeISO8601(file.getCrtime(), timeZone);
String accessedTime = ContentUtils.getStringTimeISO8601(file.getAtime(), timeZone);
String modifiedTime = ContentUtils.getStringTimeISO8601(file.getMtime(), timeZone);
filePropertyBundle.addProperty("createdTime", createdTime);
filePropertyBundle.addProperty("accessedTime", accessedTime);
filePropertyBundle.addProperty("modifiedTime", modifiedTime);
if (!Strings.isNullOrEmpty(file.getNameExtension())) {
filePropertyBundle.addProperty("extension", file.getNameExtension());
}
filePropertyBundle.addProperty("fileName", file.getName());
filePropertyBundle.addProperty("filePath", file.getUniquePath());
filePropertyBundle.addProperty("isDirectory", file.isDir());
filePropertyBundle.addProperty("sizeInBytes", Long.toString(file.getSize()));
return filePropertyBundle;
}
/**
* Creates a Content Data Property Bundle with a selection of file attributes.
*/
private CASEPropertyBundle createContentDataBundle(AbstractFile file) {
CASEPropertyBundle contentDataPropertyBundle = new CASEPropertyBundle("ContentData");
if (!Strings.isNullOrEmpty(file.getMIMEType())) {
contentDataPropertyBundle.addProperty("mimeType", file.getMIMEType());
}
if (!Strings.isNullOrEmpty(file.getMd5Hash())) {
List<CASEPropertyBundle> hashPropertyBundles = new ArrayList<>();
CASEPropertyBundle md5HashPropertyBundle = new CASEPropertyBundle("Hash");
md5HashPropertyBundle.addProperty("hashMethod", "MD5");
md5HashPropertyBundle.addProperty("hashValue", file.getMd5Hash());
hashPropertyBundles.add(md5HashPropertyBundle);
contentDataPropertyBundle.addProperty("hash", hashPropertyBundles);
}
contentDataPropertyBundle.addProperty("sizeInBytes", Long.toString(file.getSize()));
return contentDataPropertyBundle;
}
/**
* Creates a unique CASE Node file trace id.
*/
private String getFileTraceId(AbstractFile file) {
return "file-" + file.getId();
}
/**
* Creates a unique CASE Node relationship id value.
*/
private String getRelationshipId(Content content) {
return "relationship-" + content.getId();
}
/**
* Adds a Content instance (which is known to be a DataSource) to the CASE
* report. This means writing a selection of attributes to a CASE or UCO
* object.
*
* @param dataSource Datasource content to write
* @param parentCase The parent case that this data source belongs in. It is
* assumed that this parent has been written to the report (via addCase)
* prior to this call. Otherwise, the report may be invalid.
*/
public void addDataSource(Content dataSource, Case parentCase) throws IOException, TskCoreException {
String dataSourceTraceId = this.getDataSourceTraceId(dataSource);
CASENode dataSourceTrace = new CASENode(dataSourceTraceId, "Trace");
CASEPropertyBundle filePropertyBundle = new CASEPropertyBundle("File");
String dataSourcePath = getDataSourcePath(dataSource);
filePropertyBundle.addProperty("filePath", dataSourcePath);
dataSourceTrace.addBundle(filePropertyBundle);
if (dataSource.getSize() > 0) {
CASEPropertyBundle contentDataPropertyBundle = new CASEPropertyBundle("ContentData");
contentDataPropertyBundle.addProperty("sizeInBytes", Long.toString(dataSource.getSize()));
dataSourceTrace.addBundle(contentDataPropertyBundle);
}
// create a "relationship" entry between the case and the data source
String caseTraceId = getCaseTraceId(parentCase);
String relationshipTraceId = getRelationshipId(dataSource);
CASENode relationship = createRelationshipNode(relationshipTraceId,
dataSourceTraceId, caseTraceId);
CASEPropertyBundle pathRelationBundle = new CASEPropertyBundle("PathRelation");
pathRelationBundle.addProperty("path", dataSourcePath);
relationship.addBundle(pathRelationBundle);
//This completes the triage, write them to JSON.
reportGenerator.writeObject(dataSourceTrace);
reportGenerator.writeObject(relationship);
}
private String getDataSourcePath(Content dataSource) {
String dataSourcePath = "";
if (dataSource instanceof Image) {
String[] paths = ((Image) dataSource).getPaths();
if (paths.length > 0) {
//Get the first data source in the path, as this will
//be reflected in each file's uniquePath.
dataSourcePath = paths[0];
}
} else {
dataSourcePath = dataSource.getName();
}
dataSourcePath = dataSourcePath.replaceAll("\\\\", "/");
return dataSourcePath;
}
/**
* Creates a unique Case UCO trace id for a data source.
*
* @param dataSource
* @return
*/
private String getDataSourceTraceId(Content dataSource) {
return "data-source-" + dataSource.getId();
}
/**
* Adds a Case instance to the Case UCO report. This means writing a
* selection of Case attributes to a CASE/UCO object.
*
* @param caseObj Case instance to include in the report.
* @throws IOException If an I/O error is encountered.
*/
public void addCase(Case caseObj) throws IOException {
SleuthkitCase skCase = caseObj.getSleuthkitCase();
String caseDirPath = skCase.getDbDirPath();
String caseTraceId = getCaseTraceId(caseObj);
CASENode caseTrace = new CASENode(caseTraceId, "Trace");
CASEPropertyBundle filePropertyBundle = new CASEPropertyBundle("File");
// replace double slashes with single ones
caseDirPath = caseDirPath.replaceAll("\\\\", "/");
Case.CaseType caseType = caseObj.getCaseType();
if (caseType.equals(CaseType.SINGLE_USER_CASE)) {
filePropertyBundle.addProperty("filePath", caseDirPath + "/" + skCase.getDatabaseName());
filePropertyBundle.addProperty("isDirectory", false);
} else {
filePropertyBundle.addProperty("filePath", caseDirPath);
filePropertyBundle.addProperty("isDirectory", true);
}
caseTrace.addBundle(filePropertyBundle);
reportGenerator.writeObject(caseTrace);
}
/**
* Creates a unique Case UCO trace id for a Case.
*
* @param caseObj
* @return
*/
private String getCaseTraceId(Case caseObj) {
return "case-" + caseObj.getName();
}
/**
* Returns a Path to the completed Case UCO report file.
*
* This marks the end of the CaseUcoReportGenerator's life cycle. This
* function will close an underlying file handles, meaning any subsequent
* calls to addX() will result in an IOException.
*
* @return The Path to the finalized report.
* @throws IOException If an I/O error occurs.
*/
public Path generateReport() throws IOException {
//Finalize the report.
reportGenerator.writeEndArray();
reportGenerator.writeEndObject();
reportGenerator.close();
return reportPath;
}
/**
* A CASE or UCO object. CASE objects can have properties and
* property bundles.
*/
private final class CASENode {
private final String id;
private final String type;
//Dynamic properties added to this CASENode.
private final Map<String, Object> properties;
private final List<CASEPropertyBundle> propertyBundle;
public CASENode(String id, String type) {
this.id = id;
this.type = type;
properties = new LinkedHashMap<>();
propertyBundle = new ArrayList<>();
}
@JsonProperty("@id")
public String getId() {
return id;
}
@JsonProperty("@type")
public String getType() {
return type;
}
@JsonAnyGetter
public Map<String, Object> getProperties() {
return properties;
}
@JsonProperty("propertyBundle")
public List<CASEPropertyBundle> getPropertyBundle() {
return propertyBundle;
}
public void addProperty(String key, Object val) {
properties.put(key, val);
}
public void addBundle(CASEPropertyBundle bundle) {
propertyBundle.add(bundle);
}
}
/**
* Contains CASE or UCO properties.
*/
private final class CASEPropertyBundle {
private final Map<String, Object> properties;
public CASEPropertyBundle(String type) {
properties = new LinkedHashMap<>();
addProperty("@type", type);
}
@JsonAnyGetter
public Map<String, Object> getProperties() {
return properties;
}
public void addProperty(String key, Object val) {
properties.put(key, val);
}
}
}

View File

@ -19,7 +19,15 @@
*/
package org.sleuthkit.autopsy.report.modules.caseuco;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.stream.JsonWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -39,14 +47,20 @@ import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.report.GeneralReportModule;
import org.sleuthkit.autopsy.report.GeneralReportSettings;
import org.sleuthkit.autopsy.report.ReportProgressPanel;
import org.sleuthkit.caseuco.CaseUcoExporter;
import org.sleuthkit.caseuco.ContentNotExportableException;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.datamodel.blackboardutils.attributes.BlackboardJsonAttrUtil;
/**
* CaseUcoReportModule generates a report in CASE-UCO format. This module will
* write all files and data sources to the report.
* Exports an Autopsy case to a CASE-UCO report file. This module will write all
* files and artifacts from the selected data sources.
*/
public final class CaseUcoReportModule implements GeneralReportModule {
@ -54,14 +68,16 @@ public final class CaseUcoReportModule implements GeneralReportModule {
private static final CaseUcoReportModule SINGLE_INSTANCE = new CaseUcoReportModule();
//Supported types of TSK_FS_FILES
private static final Set<Short> SUPPORTED_TYPES = new HashSet<Short>() {{
private static final Set<Short> SUPPORTED_TYPES = new HashSet<Short>() {
{
add(TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_UNDEF.getValue());
add(TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getValue());
add(TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_VIRT.getValue());
}};
}
};
private static final String REPORT_FILE_NAME = "CASE_UCO_output";
private static final String EXTENSION = "json-ld";
private static final String EXTENSION = "jsonld";
// Hidden constructor for the report
private CaseUcoReportModule() {
@ -140,42 +156,76 @@ public final class CaseUcoReportModule implements GeneralReportModule {
return;
}
CaseUcoReportGenerator generator =
new CaseUcoReportGenerator(reportDirectory, REPORT_FILE_NAME);
Case currentCase = Case.getCurrentCaseThrows();
//First write the Case to the report file.
Case caseObj = Case.getCurrentCaseThrows();
generator.addCase(caseObj);
Path caseJsonReportFile = reportDirectory.resolve(REPORT_FILE_NAME + "." + EXTENSION);
List<Content> dataSources = caseObj.getDataSources().stream()
.filter((dataSource) -> {
if(settings.getSelectedDataSources() == null) {
// Assume all data sources if list is null.
return true;
try (OutputStream stream = new FileOutputStream(caseJsonReportFile.toFile());
JsonWriter reportWriter = new JsonWriter(new OutputStreamWriter(stream, "UTF-8"))) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
reportWriter.setIndent(" ");
reportWriter.beginObject();
reportWriter.name("@graph");
reportWriter.beginArray();
CaseUcoExporter exporter = new CaseUcoExporter(currentCase.getSleuthkitCase());
for (JsonElement element : exporter.exportSleuthkitCase()) {
gson.toJson(element, reportWriter);
}
return settings.getSelectedDataSources().contains(dataSource.getId());
})
.collect(Collectors.toList());
// Get a list of selected data sources to process.
List<DataSource> dataSources = getSelectedDataSources(currentCase, settings);
progressPanel.setIndeterminate(false);
progressPanel.setMaximumProgress(dataSources.size());
progressPanel.start();
//Then search each data source for file content.
// First stage of reporting is for files and data sources.
// Iterate through each data source and dump all files contained
// in that data source.
for (int i = 0; i < dataSources.size(); i++) {
Content dataSource = dataSources.get(i);
DataSource dataSource = dataSources.get(i);
progressPanel.updateStatusLabel(String.format(
Bundle.CaseUcoReportModule_processingDataSource(),
dataSource.getName()));
//Add the data source and then all children.
generator.addDataSource(dataSource, caseObj);
performDepthFirstSearch(dataSource, generator);
// Add the data source export.
for (JsonElement element : exporter.exportDataSource(dataSource)) {
gson.toJson(element, reportWriter);
}
// Search all children of the data source.
performDepthFirstSearch(dataSource, gson, exporter, reportWriter);
progressPanel.setProgress(i + 1);
}
//Complete the report.
Path reportPath = generator.generateReport();
caseObj.addReport(reportPath.toString(),
// Second stage of reporting handles artifacts.
Set<Long> dataSourceIds = dataSources.stream()
.map((datasource) -> datasource.getId())
.collect(Collectors.toSet());
// Write all standard artifacts that are contained within the
// selected data sources.
for (ARTIFACT_TYPE artType : currentCase.getSleuthkitCase().getBlackboardArtifactTypesInUse()) {
for (BlackboardArtifact artifact : currentCase.getSleuthkitCase().getBlackboardArtifacts(artType)) {
if (dataSourceIds.contains(artifact.getDataSource().getId())) {
try {
for (JsonElement element : exporter.exportBlackboardArtifact(artifact)) {
gson.toJson(element, reportWriter);
}
} catch (ContentNotExportableException | BlackboardJsonAttrUtil.InvalidJsonException ex) {
logger.log(Level.WARNING, String.format("Unable to export blackboard artifact (id: %d) to CASE/UCO. "
+ "The artifact type is either not supported or the artifact instance does not have any "
+ "exportable attributes.", artifact.getId()));
}
}
}
}
reportWriter.endArray();
reportWriter.endObject();
}
currentCase.addReport(caseJsonReportFile.toString(),
Bundle.CaseUcoReportModule_srcModuleName(),
REPORT_FILE_NAME);
progressPanel.complete(ReportProgressPanel.ReportStatus.COMPLETE);
@ -196,6 +246,21 @@ public final class CaseUcoReportModule implements GeneralReportModule {
progressPanel.complete(ReportProgressPanel.ReportStatus.COMPLETE);
}
/**
* Get the selected data sources from the settings instance.
*/
private List<DataSource> getSelectedDataSources(Case currentCase, GeneralReportSettings settings) throws TskCoreException {
return currentCase.getSleuthkitCase().getDataSources().stream()
.filter((dataSource) -> {
if (settings.getSelectedDataSources() == null) {
// Assume all data sources if list is null.
return true;
}
return settings.getSelectedDataSources().contains(dataSource.getId());
})
.collect(Collectors.toList());
}
/**
* Warn the user if ingest is still ongoing.
*/
@ -207,10 +272,9 @@ public final class CaseUcoReportModule implements GeneralReportModule {
/**
* Perform DFS on the data sources tree, which will search it in entirety.
* This traversal is more memory efficient than BFS (Breadth first search).
*/
private void performDepthFirstSearch(Content dataSource,
CaseUcoReportGenerator generator) throws IOException, TskCoreException {
private void performDepthFirstSearch(DataSource dataSource,
Gson gson, CaseUcoExporter exporter, JsonWriter reportWriter) throws IOException, TskCoreException {
Deque<Content> stack = new ArrayDeque<>();
stack.addAll(dataSource.getChildren());
@ -219,9 +283,12 @@ public final class CaseUcoReportModule implements GeneralReportModule {
while (!stack.isEmpty()) {
Content current = stack.pop();
if (current instanceof AbstractFile) {
AbstractFile f = (AbstractFile) (current);
if(SUPPORTED_TYPES.contains(f.getMetaType().getValue())) {
generator.addFile(f, dataSource);
AbstractFile file = (AbstractFile) (current);
if (SUPPORTED_TYPES.contains(file.getMetaType().getValue())) {
for (JsonElement element : exporter.exportAbstractFile(file)) {
gson.toJson(element, reportWriter);
}
}
}

View File

@ -20,12 +20,19 @@ package org.sleuthkit.autopsy.report.modules.portablecase;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.stream.JsonWriter;
import org.sleuthkit.autopsy.report.ReportModule;
import java.util.logging.Level;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -49,7 +56,7 @@ import org.sleuthkit.autopsy.coreutils.PlatformUtil;
import org.sleuthkit.autopsy.datamodel.ContentUtils;
import org.sleuthkit.autopsy.coreutils.FileTypeUtils.FileTypeCategory;
import org.sleuthkit.autopsy.report.ReportProgressPanel;
import org.sleuthkit.autopsy.report.modules.caseuco.CaseUcoReportGenerator;
import org.sleuthkit.caseuco.CaseUcoExporter;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardArtifactTag;
@ -76,6 +83,7 @@ import org.sleuthkit.datamodel.VolumeSystem;
* Creates a portable case from tagged files
*/
public class PortableCaseReportModule implements ReportModule {
private static final Logger logger = Logger.getLogger(PortableCaseReportModule.class.getName());
private static final String FILE_FOLDER_NAME = "PortableCaseFiles"; // NON-NLS
private static final String UNKNOWN_FILE_TYPE_FOLDER = "Other"; // NON-NLS
@ -155,9 +163,9 @@ public class PortableCaseReportModule implements ReportModule {
}
/**
* Convenience method to avoid code duplication.
* Assumes that if an exception is supplied then the error is SEVERE. Otherwise
* it is logged as a WARNING.
* Convenience method to avoid code duplication. Assumes that if an
* exception is supplied then the error is SEVERE. Otherwise it is logged as
* a WARNING.
*
* @param logWarning Warning to write to the log
* @param dialogWarning Warning to write to a pop-up window
@ -462,11 +470,17 @@ public class PortableCaseReportModule implements ReportModule {
return;
}
try {
//Try to generate case uco output.
Path reportFile = reportsDirectory.resolve(CASE_UCO_FILE_NAME);
progressPanel.updateStatusLabel(Bundle.PortableCaseReportModule_generateCaseUcoReport_startCaseUcoReportGeneration());
CaseUcoReportGenerator reportGenerator = new CaseUcoReportGenerator(reportsDirectory, CASE_UCO_FILE_NAME);
//Acquire references for file discovery
try (OutputStream stream = new FileOutputStream(reportFile.toFile());
JsonWriter reportWriter = new JsonWriter(new OutputStreamWriter(stream, "UTF-8"))) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
reportWriter.setIndent(" ");
reportWriter.beginObject();
reportWriter.name("@graph");
reportWriter.beginArray();
String caseTempDirectory = currentCase.getTempDirectory();
SleuthkitCase skCase = currentCase.getSleuthkitCase();
TagsManager tagsManager = currentCase.getServices().getTagsManager();
@ -477,7 +491,10 @@ public class PortableCaseReportModule implements ReportModule {
FileUtils.deleteDirectory(tmpDir.toFile());
Files.createDirectory(tmpDir);
reportGenerator.addCase(currentCase);
CaseUcoExporter exporter = new CaseUcoExporter(currentCase.getSleuthkitCase());
for (JsonElement element : exporter.exportSleuthkitCase()) {
gson.toJson(element, reportWriter);
}
//Load all interesting BlackboardArtifacts that belong to the selected SET_NAMEs
//binned by data source id.
@ -485,35 +502,33 @@ public class PortableCaseReportModule implements ReportModule {
//Search each data source looking for content tags and interesting
//items that match the selected tag names and set names.
for (Content dataSource : currentCase.getDataSources()) {
/**
* It is currently believed that DataSources in a CASE-UCO report
* should precede all file entities. Therefore, before
* writing a file, add the data source if it
* has yet to be included.
*/
for (DataSource dataSource : currentCase.getSleuthkitCase().getDataSources()) {
// Helper flag to ensure each data source is only written once in
// a report.
boolean dataSourceHasBeenIncluded = false;
//Search content tags and artifact tags that match
for (TagName tagName : tagNames) {
for (ContentTag ct : tagsManager.getContentTagsByTagName(tagName, dataSource.getId())) {
dataSourceHasBeenIncluded |= addUniqueFile(ct.getContent(),
dataSource, tmpDir, reportGenerator, dataSourceHasBeenIncluded);
dataSource, tmpDir, gson, exporter, reportWriter, dataSourceHasBeenIncluded);
}
for (BlackboardArtifactTag bat : tagsManager.getBlackboardArtifactTagsByTagName(tagName, dataSource.getId())) {
dataSourceHasBeenIncluded |= addUniqueFile(bat.getContent(),
dataSource, tmpDir, reportGenerator, dataSourceHasBeenIncluded);
dataSource, tmpDir, gson, exporter, reportWriter, dataSourceHasBeenIncluded);
}
}
//Search artifacts that this data source contains
for (BlackboardArtifact bArt : artifactsWithSetName.get(dataSource.getId())) {
Content sourceContent = bArt.getParent();
dataSourceHasBeenIncluded |= addUniqueFile(sourceContent, dataSource,
tmpDir, reportGenerator, dataSourceHasBeenIncluded);
tmpDir, gson, exporter, reportWriter, dataSourceHasBeenIncluded);
}
}
//Create the report.
reportGenerator.generateReport();
// Finish the report.
reportWriter.endArray();
reportWriter.endObject();
progressPanel.updateStatusLabel(Bundle.PortableCaseReportModule_generateCaseUcoReport_successCaseUcoReportGeneration());
} catch (IOException | TskCoreException ex) {
progressPanel.updateStatusLabel(Bundle.PortableCaseReportModule_generateCaseUcoReport_errorGeneratingCaseUcoReport());
@ -555,25 +570,29 @@ public class PortableCaseReportModule implements ReportModule {
* @param tmpDir Path to the tmpDir to enforce uniqueness
* @param reportGenerator Report generator instance to add the content to
* @param dataSourceHasBeenIncluded Flag determining if the data source
* should be written before the file. False will cause the data source to be written.
* should be written to the report (false indicates that it should be written).
* @throws IOException If an I/O error occurs.
* @throws TskCoreException If an internal database error occurs.
*
* return True if the data source was written during this operation.
* return True if the file was written during this operation.
*/
private boolean addUniqueFile(Content content, Content dataSource,
Path tmpDir, CaseUcoReportGenerator reportGenerator,
private boolean addUniqueFile(Content content, DataSource dataSource,
Path tmpDir, Gson gson, CaseUcoExporter exporter, JsonWriter reportWriter,
boolean dataSourceHasBeenIncluded) throws IOException, TskCoreException {
if (content instanceof AbstractFile && !(content instanceof DataSource)) {
AbstractFile absFile = (AbstractFile) content;
Path filePath = tmpDir.resolve(Long.toString(absFile.getId()));
if (!absFile.isDir() && !Files.exists(filePath)) {
if (!dataSourceHasBeenIncluded) {
reportGenerator.addDataSource(dataSource, currentCase);
for (JsonElement element : exporter.exportDataSource(dataSource)) {
gson.toJson(element, reportWriter);
}
}
String subFolder = getExportSubfolder(absFile);
String fileName = absFile.getId() + "-" + FileUtil.escapeFileName(absFile.getName());
reportGenerator.addFile(absFile, dataSource, Paths.get(FILE_FOLDER_NAME, subFolder, fileName));
for (JsonElement element : exporter.exportAbstractFile(absFile, Paths.get(FILE_FOLDER_NAME, subFolder, fileName).toString())) {
gson.toJson(element, reportWriter);
}
Files.createFile(filePath);
return true;
}
@ -604,10 +623,9 @@ public class PortableCaseReportModule implements ReportModule {
return setNames;
}
/**
* Create the case directory and case database.
* portableSkCase will be set if this completes without error.
* Create the case directory and case database. portableSkCase will be set
* if this completes without error.
*
* @param outputDir The parent for the case folder
* @param progressPanel
@ -618,8 +636,7 @@ public class PortableCaseReportModule implements ReportModule {
"PortableCaseReportModule.createCase.errorCreatingCase=Error creating case",
"# {0} - folder",
"PortableCaseReportModule.createCase.errorCreatingFolder=Error creating folder {0}",
"PortableCaseReportModule.createCase.errorStoringMaxIds=Error storing maximum database IDs",
})
"PortableCaseReportModule.createCase.errorStoringMaxIds=Error storing maximum database IDs",})
private void createCase(File outputDir, ReportProgressPanel progressPanel) {
// Create the case folder
@ -758,7 +775,8 @@ public class PortableCaseReportModule implements ReportModule {
*
* @param tag The ContentTag in the current case
*
* @return The app_data string for this content tag or an empty string if there was none
* @return The app_data string for this content tag or an empty string if
* there was none
*
* @throws TskCoreException
*/
@ -816,7 +834,6 @@ public class PortableCaseReportModule implements ReportModule {
portableSkCase.getCaseDbAccessManager().insert(ContentViewerTagManager.TABLE_NAME, insert);
}
/**
* Add all artifacts with a given tag to the portable case.
*
@ -853,9 +870,11 @@ public class PortableCaseReportModule implements ReportModule {
}
/**
* Copy an artifact into the new case. Will also copy any associated artifacts
* Copy an artifact into the new case. Will also copy any associated
* artifacts
*
* @param newContentId The content ID (in the portable case) of the source content
* @param newContentId The content ID (in the portable case) of the source
* content
* @param artifactToCopy The artifact to copy
*
* @return The new artifact in the portable case
@ -927,8 +946,9 @@ public class PortableCaseReportModule implements ReportModule {
}
/**
* Get the artifact type ID in the portable case and create new artifact type if needed.
* For built-in artifacts this will be the same as the original.
* Get the artifact type ID in the portable case and create new artifact
* type if needed. For built-in artifacts this will be the same as the
* original.
*
* @param oldArtifact The artifact in the current case
*
@ -950,8 +970,9 @@ public class PortableCaseReportModule implements ReportModule {
}
/**
* Get the attribute type ID in the portable case and create new attribute type if needed.
* For built-in attributes this will be the same as the original.
* Get the attribute type ID in the portable case and create new attribute
* type if needed. For built-in attributes this will be the same as the
* original.
*
* @param oldAttribute The attribute in the current case
*
@ -985,8 +1006,7 @@ public class PortableCaseReportModule implements ReportModule {
*/
@NbBundle.Messages({
"# {0} - File name",
"PortableCaseReportModule.copyContentToPortableCase.copyingFile=Copying file {0}",
})
"PortableCaseReportModule.copyContentToPortableCase.copyingFile=Copying file {0}",})
private long copyContentToPortableCase(Content content, ReportProgressPanel progressPanel) throws TskCoreException {
progressPanel.updateStatusLabel(Bundle.PortableCaseReportModule_copyContentToPortableCase_copyingFile(content.getUniquePath()));
return copyContent(content);
@ -1153,7 +1173,6 @@ public class PortableCaseReportModule implements ReportModule {
configPanel = new CreatePortableCasePanel();
return configPanel;
} */
private class StoreMaxIdCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback {
private final String tableName;
@ -1190,8 +1209,7 @@ public class PortableCaseReportModule implements ReportModule {
"# {0} - Temp folder path",
"PortableCaseReportModule.compressCase.errorCreatingTempFolder=Could not create temporary folder {0}",
"PortableCaseReportModule.compressCase.errorCompressingCase=Error compressing case",
"PortableCaseReportModule.compressCase.canceled=Compression canceled by user",
})
"PortableCaseReportModule.compressCase.canceled=Compression canceled by user",})
private boolean compressCase(ReportProgressPanel progressPanel) {
// Close the portable case database (we still need some of the variables that would be cleared by cleanup())