Autopsy  4.21.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
CaseMetadata.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011-2019 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.casemodule;
20 
21 import java.io.BufferedWriter;
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStreamWriter;
26 import java.io.StringWriter;
27 import java.nio.charset.StandardCharsets;
28 import java.nio.file.Path;
29 import java.nio.file.Paths;
30 import java.text.DateFormat;
31 import java.text.SimpleDateFormat;
32 import java.util.Date;
33 import java.util.Locale;
34 import javax.xml.parsers.DocumentBuilder;
35 import javax.xml.parsers.DocumentBuilderFactory;
36 import javax.xml.parsers.ParserConfigurationException;
37 import javax.xml.transform.OutputKeys;
38 import javax.xml.transform.Result;
39 import javax.xml.transform.Source;
40 import javax.xml.transform.Transformer;
41 import javax.xml.transform.TransformerException;
42 import javax.xml.transform.TransformerFactory;
43 import javax.xml.transform.dom.DOMSource;
44 import javax.xml.transform.stream.StreamResult;
45 import org.apache.commons.lang3.StringUtils;
48 import org.w3c.dom.Document;
49 import org.w3c.dom.Element;
50 import org.w3c.dom.NodeList;
51 import org.xml.sax.SAXException;
52 
56 public final class CaseMetadata {
57 
58  private static final String FILE_EXTENSION = ".aut";
59  private static final String DATE_FORMAT_STRING = "yyyy/MM/dd HH:mm:ss (z)";
60  private static final DateFormat DATE_FORMAT = new SimpleDateFormat(DATE_FORMAT_STRING, Locale.US);
61 
62  /*
63  * Fields from schema version 1
64  */
65  private static final String SCHEMA_VERSION_ONE = "1.0";
66  private final static String ROOT_ELEMENT_NAME = "AutopsyCase"; //NON-NLS
67  private final static String SCHEMA_VERSION_ELEMENT_NAME = "SchemaVersion"; //NON-NLS
68  private final static String CREATED_DATE_ELEMENT_NAME = "CreatedDate"; //NON-NLS
69  private final static String AUTOPSY_VERSION_ELEMENT_NAME = "AutopsyCreatedVersion"; //NON-NLS
70  private final static String CASE_ELEMENT_NAME = "Case"; //NON-NLS
71  private final static String CASE_NAME_ELEMENT_NAME = "Name"; //NON-NLS
72  private final static String CASE_NUMBER_ELEMENT_NAME = "Number"; //NON-NLS
73  private final static String EXAMINER_ELEMENT_NAME = "Examiner"; //NON-NLS
74  private final static String CASE_TYPE_ELEMENT_NAME = "CaseType"; //NON-NLS
75  private final static String CASE_DATABASE_NAME_ELEMENT_NAME = "DatabaseName"; //NON-NLS
76  private final static String TEXT_INDEX_NAME_ELEMENT = "TextIndexName"; //NON-NLS
77 
78  /*
79  * Fields from schema version 2
80  */
81  private static final String SCHEMA_VERSION_TWO = "2.0";
82  private final static String AUTOPSY_CREATED_BY_ELEMENT_NAME = "CreatedByAutopsyVersion"; //NON-NLS
83  private final static String CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME = "Database"; //NON-NLS
84  private final static String TEXT_INDEX_ELEMENT = "TextIndex"; //NON-NLS
85 
86  /*
87  * Fields from schema version 3
88  */
89  private static final String SCHEMA_VERSION_THREE = "3.0";
90  private final static String CASE_DISPLAY_NAME_ELEMENT_NAME = "DisplayName"; //NON-NLS
91  private final static String CASE_DB_NAME_RELATIVE_ELEMENT_NAME = "CaseDatabase"; //NON-NLS
92 
93  /*
94  * Fields from schema version 4
95  */
96  private static final String SCHEMA_VERSION_FOUR = "4.0";
97  private final static String EXAMINER_ELEMENT_PHONE = "ExaminerPhone"; //NON-NLS
98  private final static String EXAMINER_ELEMENT_EMAIL = "ExaminerEmail"; //NON-NLS
99  private final static String CASE_ELEMENT_NOTES = "CaseNotes"; //NON-NLS
100 
101  /*
102  * Fields from schema version 5
103  */
104  private static final String SCHEMA_VERSION_FIVE = "5.0";
105  private final static String ORIGINAL_CASE_ELEMENT_NAME = "OriginalCase"; //NON-NLS
106 
107  /*
108  * Fields from schema version 6
109  */
110  private static final String SCHEMA_VERSION_SIX = "6.0";
111  private final static String CONTENT_PROVIDER_ELEMENT_NAME = "ContentProvider";
112  private final static String CONTENT_PROVIDER_NAME_ELEMENT_NAME = "Name";
113 
114  /*
115  * Unread fields, regenerated on save.
116  */
117  private final static String MODIFIED_DATE_ELEMENT_NAME = "ModifiedDate"; //NON-NLS
118  private final static String AUTOPSY_SAVED_BY_ELEMENT_NAME = "SavedByAutopsyVersion"; //NON-NLS
119 
120  private final static String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_SIX;
121 
122  private final Path metadataFilePath;
124  private String caseName;
126  private String caseDatabaseName;
127  private String caseDatabasePath;
128  private String textIndexName; // Legacy
129  private String createdDate;
130  private String createdByVersion;
131  private CaseMetadata originalMetadata = null; // For portable cases
132  private String contentProviderName;
133 
139  public static String getFileExtension() {
140  return FILE_EXTENSION;
141  }
142 
148  public static DateFormat getDateFormat() {
149  return new SimpleDateFormat(DATE_FORMAT_STRING, Locale.US);
150  }
151 
162  CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails) {
163  this(caseType, caseDirectory, caseName, caseDetails, null);
164  }
165 
177  CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails, CaseMetadata originalMetadata) {
178  metadataFilePath = Paths.get(caseDirectory, caseDetails.getCaseDisplayName() + FILE_EXTENSION);
179  this.caseType = caseType;
180  this.caseName = caseName;
181  this.caseDetails = caseDetails;
182  caseDatabaseName = "";
183  caseDatabasePath = "";
184  textIndexName = "";
185  createdByVersion = Version.getVersion();
186  createdDate = CaseMetadata.DATE_FORMAT.format(new Date());
187  this.originalMetadata = originalMetadata;
188  this.contentProviderName = originalMetadata == null ? null : originalMetadata.contentProviderName;
189  }
190 
200  public CaseMetadata(Path metadataFilePath) throws CaseMetadataException {
201  this.metadataFilePath = metadataFilePath;
202  readFromFile();
203  }
204 
213  public static Path getCaseMetadataFilePath(Path directoryPath) {
214  final File[] files = directoryPath.toFile().listFiles();
215  if (files != null) {
216  for (File file : files) {
217  final String fileName = file.getName().toLowerCase();
218  if (fileName.endsWith(CaseMetadata.getFileExtension()) && file.isFile()) {
219  return file.toPath();
220  }
221  }
222  }
223  return null;
224  }
225 
230  public String getContentProviderName() {
231  return this.contentProviderName;
232  }
233 
239  public Path getFilePath() {
240  return metadataFilePath;
241  }
242 
248  public String getCaseDirectory() {
249  return StringUtils.isBlank(this.caseDatabasePath)
250  ? metadataFilePath.getParent().toString()
251  : Paths.get(this.caseDatabasePath).getParent().toString();
252  }
253 
260  return caseType;
261  }
262 
268  public String getCaseName() {
269  return caseName;
270  }
271 
278  return caseDetails;
279  }
280 
286  public String getCaseDisplayName() {
287  return caseDetails.getCaseDisplayName();
288  }
289 
290  void setCaseDetails(CaseDetails newCaseDetails) throws CaseMetadataException {
291  CaseDetails oldCaseDetails = this.caseDetails;
292  this.caseDetails = newCaseDetails;
293  try {
294  writeToFile();
295  } catch (CaseMetadataException ex) {
296  this.caseDetails = oldCaseDetails;
297  throw ex;
298  }
299  }
300 
306  public String getCaseNumber() {
307  return caseDetails.getCaseNumber();
308  }
309 
315  public String getExaminer() {
316  return caseDetails.getExaminerName();
317  }
318 
319  public String getExaminerPhone() {
320  return caseDetails.getExaminerPhone();
321  }
322 
323  public String getExaminerEmail() {
324  return caseDetails.getExaminerEmail();
325  }
326 
327  public String getCaseNotes() {
328  return caseDetails.getCaseNotes();
329  }
330 
336  public String getCaseDatabaseName() {
337  return caseDatabaseName;
338  }
339 
347  void setCaseDatabaseName(String caseDatabaseName) throws CaseMetadataException {
348  String oldCaseDatabaseName = this.caseDatabaseName;
349  this.caseDatabaseName = caseDatabaseName;
350  try {
351  writeToFile();
352  } catch (CaseMetadataException ex) {
353  this.caseDatabaseName = oldCaseDatabaseName;
354  throw ex;
355  }
356  }
357 
364  public String getTextIndexName() {
365  return textIndexName;
366  }
367 
373  public String getCreatedDate() {
374  return createdDate;
375  }
376 
385  void setCreatedDate(String createdDate) throws CaseMetadataException {
386  String oldCreatedDate = createdDate;
387  this.createdDate = createdDate;
388  try {
389  writeToFile();
390  } catch (CaseMetadataException ex) {
391  this.createdDate = oldCreatedDate;
392  throw ex;
393  }
394  }
395 
401  String getCreatedByVersion() {
402  return createdByVersion;
403  }
404 
413  void setCreatedByVersion(String buildVersion) throws CaseMetadataException {
414  String oldCreatedByVersion = this.createdByVersion;
415  this.createdByVersion = buildVersion;
416  try {
417  writeToFile();
418  } catch (CaseMetadataException ex) {
419  this.createdByVersion = oldCreatedByVersion;
420  throw ex;
421  }
422  }
423 
430  void writeToFile() throws CaseMetadataException {
431  try {
432  /*
433  * Create the XML DOM.
434  */
435  Document doc = XMLUtil.createDocument();
436  createXMLDOM(doc);
437  doc.normalize();
438 
439  /*
440  * Prepare the DOM for pretty printing to the metadata file.
441  */
442  Source source = new DOMSource(doc);
443  StringWriter stringWriter = new StringWriter();
444  Result streamResult = new StreamResult(stringWriter);
445  Transformer transformer = TransformerFactory.newInstance().newTransformer();
446  transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //NON-NLS
447  transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); //NON-NLS
448  transformer.transform(source, streamResult);
449 
450  /*
451  * Write the DOM to the metadata file. Add UTF-8 Characterset so it writes to the file
452  * correctly for non-latin characters
453  */
454  try (BufferedWriter fileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(metadataFilePath.toFile()), StandardCharsets.UTF_8))) {
455  fileWriter.write(stringWriter.toString());
456  fileWriter.flush();
457  }
458 
459  } catch (ParserConfigurationException | TransformerException | IOException ex) {
460  throw new CaseMetadataException(String.format("Error writing to case metadata file %s", metadataFilePath), ex);
461  }
462  }
463 
464  /*
465  * Creates an XML DOM from the case metadata.
466  */
467  private void createXMLDOM(Document doc) {
468  /*
469  * Create the root element and its children.
470  */
471  Element rootElement = doc.createElement(ROOT_ELEMENT_NAME);
472  doc.appendChild(rootElement);
473  createChildElement(doc, rootElement, SCHEMA_VERSION_ELEMENT_NAME, CURRENT_SCHEMA_VERSION);
474  createChildElement(doc, rootElement, CREATED_DATE_ELEMENT_NAME, createdDate);
475  createChildElement(doc, rootElement, MODIFIED_DATE_ELEMENT_NAME, DATE_FORMAT.format(new Date()));
476  createChildElement(doc, rootElement, AUTOPSY_CREATED_BY_ELEMENT_NAME, createdByVersion);
477  createChildElement(doc, rootElement, AUTOPSY_SAVED_BY_ELEMENT_NAME, Version.getVersion());
478  Element caseElement = doc.createElement(CASE_ELEMENT_NAME);
479  rootElement.appendChild(caseElement);
480 
481  Element contentProviderEl = doc.createElement(CONTENT_PROVIDER_ELEMENT_NAME);
482  rootElement.appendChild(contentProviderEl);
483 
484  Element contentProviderNameEl = doc.createElement(CONTENT_PROVIDER_NAME_ELEMENT_NAME);
485  if (this.contentProviderName != null) {
486  contentProviderNameEl.setTextContent(this.contentProviderName);
487  }
488  contentProviderEl.appendChild(contentProviderNameEl);
489 
490  /*
491  * Create the children of the case element.
492  */
493  createCaseElements(doc, caseElement, this);
494 
495  /*
496  * Add original case element
497  */
498  Element originalCaseElement = doc.createElement(ORIGINAL_CASE_ELEMENT_NAME);
499  rootElement.appendChild(originalCaseElement);
500  if (originalMetadata != null) {
501  createChildElement(doc, originalCaseElement, CREATED_DATE_ELEMENT_NAME, originalMetadata.createdDate);
502  Element originalCaseDetailsElement = doc.createElement(CASE_ELEMENT_NAME);
503  originalCaseElement.appendChild(originalCaseDetailsElement);
504  createCaseElements(doc, originalCaseDetailsElement, originalMetadata);
505  }
506 
507  }
508 
516  private void createCaseElements(Document doc, Element caseElement, CaseMetadata metadataToWrite) {
517  CaseDetails caseDetailsToWrite = metadataToWrite.caseDetails;
518  createChildElement(doc, caseElement, CASE_NAME_ELEMENT_NAME, metadataToWrite.caseName);
519  createChildElement(doc, caseElement, CASE_DISPLAY_NAME_ELEMENT_NAME, caseDetailsToWrite.getCaseDisplayName());
520  createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, caseDetailsToWrite.getCaseNumber());
521  createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, caseDetailsToWrite.getExaminerName());
522  createChildElement(doc, caseElement, EXAMINER_ELEMENT_PHONE, caseDetailsToWrite.getExaminerPhone());
523  createChildElement(doc, caseElement, EXAMINER_ELEMENT_EMAIL, caseDetailsToWrite.getExaminerEmail());
524  createChildElement(doc, caseElement, CASE_ELEMENT_NOTES, caseDetailsToWrite.getCaseNotes());
525  createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, metadataToWrite.caseType.toString());
526  createChildElement(doc, caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, metadataToWrite.caseDatabasePath);
527  createChildElement(doc, caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, metadataToWrite.caseDatabaseName);
528  createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, metadataToWrite.textIndexName);
529  }
530 
540  private void createChildElement(Document doc, Element parentElement, String elementName, String elementContent) {
541  Element element = doc.createElement(elementName);
542  element.appendChild(doc.createTextNode(elementContent));
543  parentElement.appendChild(element);
544  }
545 
552  private void readFromFile() throws CaseMetadataException {
553  try {
554  /*
555  * Parse the file into an XML DOM and get the root element.
556  */
557  DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
558  Document doc = builder.parse(this.getFilePath().toFile());
559  doc.getDocumentElement().normalize();
560  Element rootElement = doc.getDocumentElement();
561  if (!rootElement.getNodeName().equals(ROOT_ELEMENT_NAME)) {
562  throw new CaseMetadataException("Case metadata file corrupted");
563  }
564 
565  /*
566  * Get the content of the relevant children of the root element.
567  */
568  String schemaVersion = getElementTextContent(rootElement, SCHEMA_VERSION_ELEMENT_NAME, true);
569  this.createdDate = getElementTextContent(rootElement, CREATED_DATE_ELEMENT_NAME, true);
570  if (schemaVersion.equals(SCHEMA_VERSION_ONE)) {
571  this.createdByVersion = getElementTextContent(rootElement, AUTOPSY_VERSION_ELEMENT_NAME, true);
572  } else {
573  this.createdByVersion = getElementTextContent(rootElement, AUTOPSY_CREATED_BY_ELEMENT_NAME, true);
574  }
575 
576  Element contentProviderEl = getChildElOrNull(rootElement, CONTENT_PROVIDER_ELEMENT_NAME);
577  if (contentProviderEl != null) {
578  Element contentProviderNameEl = getChildElOrNull(contentProviderEl, CONTENT_PROVIDER_NAME_ELEMENT_NAME);
579  this.contentProviderName = contentProviderNameEl != null ? contentProviderNameEl.getTextContent() : null;
580  } else {
581  this.contentProviderName = null;
582  }
583 
584  /*
585  * Get the content of the children of the case element.
586  */
587  NodeList caseElements = doc.getElementsByTagName(CASE_ELEMENT_NAME);
588  if (caseElements.getLength() == 0) {
589  throw new CaseMetadataException("Case metadata file corrupted");
590  }
591  Element caseElement = (Element) caseElements.item(0);
592  this.caseName = getElementTextContent(caseElement, CASE_NAME_ELEMENT_NAME, true);
593  String caseDisplayName;
594  String caseNumber;
595  if (schemaVersion.equals(SCHEMA_VERSION_ONE) || schemaVersion.equals(SCHEMA_VERSION_TWO)) {
596  caseDisplayName = caseName;
597  } else {
598  caseDisplayName = getElementTextContent(caseElement, CASE_DISPLAY_NAME_ELEMENT_NAME, true);
599  }
600  caseNumber = getElementTextContent(caseElement, CASE_NUMBER_ELEMENT_NAME, false);
601  String examinerName = getElementTextContent(caseElement, EXAMINER_ELEMENT_NAME, false);
602  String examinerPhone;
603  String examinerEmail;
604  String caseNotes;
605  if (schemaVersion.equals(SCHEMA_VERSION_ONE) || schemaVersion.equals(SCHEMA_VERSION_TWO) || schemaVersion.equals(SCHEMA_VERSION_THREE)) {
606  examinerPhone = ""; //case had metadata file written before additional examiner details were included
607  examinerEmail = "";
608  caseNotes = "";
609  } else {
610  examinerPhone = getElementTextContent(caseElement, EXAMINER_ELEMENT_PHONE, false);
611  examinerEmail = getElementTextContent(caseElement, EXAMINER_ELEMENT_EMAIL, false);
612  caseNotes = getElementTextContent(caseElement, CASE_ELEMENT_NOTES, false);
613  }
614 
615  this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail, caseNotes);
616  this.caseType = Case.CaseType.fromString(getElementTextContent(caseElement, CASE_TYPE_ELEMENT_NAME, true));
617  if (null == this.caseType) {
618  throw new CaseMetadataException("Case metadata file corrupted");
619  }
620  switch (schemaVersion) {
621  case SCHEMA_VERSION_ONE:
622  this.caseDatabaseName = getElementTextContent(caseElement, CASE_DATABASE_NAME_ELEMENT_NAME, true);
623  this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_NAME_ELEMENT, true);
624  break;
625  case SCHEMA_VERSION_TWO:
626  this.caseDatabaseName = getElementTextContent(caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, true);
627  this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false);
628  break;
629  default:
630  this.caseDatabasePath = getElementTextContent(caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, false);
631  this.caseDatabaseName = getElementTextContent(caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, true);
632  this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false);
633  break;
634  }
635 
636  /*
637  * Fix up the case database name due to a bug that for a time caused
638  * the absolute paths of single-user case databases to be stored.
639  * Derive the missing (absolute/relative) value from the one
640  * present.
641  */
642  Path possibleAbsoluteCaseDbPath = Paths.get(this.caseDatabaseName);
643  Path caseDirectoryPath = Paths.get(getCaseDirectory());
644  if (possibleAbsoluteCaseDbPath.toFile().isAbsolute()) {
645  this.caseDatabasePath = this.caseDatabaseName;
646  this.caseDatabaseName = caseDirectoryPath.relativize(possibleAbsoluteCaseDbPath).toString();
647  }
648 
649  } catch (ParserConfigurationException | SAXException | IOException ex) {
650  throw new CaseMetadataException(String.format("Error reading from case metadata file %s", metadataFilePath), ex);
651  }
652  }
653 
654  private Element getChildElOrNull(Element parent, String childTag) {
655  NodeList nl = parent.getElementsByTagName(childTag);
656  if (nl != null && nl.getLength() > 0 && nl.item(0) instanceof Element) {
657  return (Element) nl.item(0);
658  } else {
659  return null;
660  }
661  }
662 
675  private String getElementTextContent(Element parentElement, String elementName, boolean contentIsRequired) throws CaseMetadataException {
676  NodeList elementsList = parentElement.getElementsByTagName(elementName);
677  if (elementsList.getLength() == 0) {
678  throw new CaseMetadataException(String.format("Missing %s element from case metadata file %s", elementName, metadataFilePath));
679  }
680  String textContent = elementsList.item(0).getTextContent();
681  if (textContent.isEmpty() && contentIsRequired) {
682  throw new CaseMetadataException(String.format("Empty %s element in case metadata file %s", elementName, metadataFilePath));
683  }
684  return textContent;
685  }
686 
691  public final static class CaseMetadataException extends Exception {
692 
693  private static final long serialVersionUID = 1L;
694 
695  private CaseMetadataException(String message) {
696  super(message);
697  }
698 
699  private CaseMetadataException(String message, Throwable cause) {
700  super(message, cause);
701  }
702  }
703 
712  public String getCaseDatabasePath() throws UnsupportedOperationException {
714  return StringUtils.isBlank(this.caseDatabasePath)
715  ? this.metadataFilePath.getParent().resolve(this.caseDatabaseName).toString()
716  : this.caseDatabasePath;
717  } else {
718  throw new UnsupportedOperationException();
719  }
720  }
721 
722 }
static CaseType fromString(String typeName)
Definition: Case.java:231
static Path getCaseMetadataFilePath(Path directoryPath)
Element getChildElOrNull(Element parent, String childTag)
void createCaseElements(Document doc, Element caseElement, CaseMetadata metadataToWrite)
void createChildElement(Document doc, Element parentElement, String elementName, String elementContent)
String getElementTextContent(Element parentElement, String elementName, boolean contentIsRequired)

Copyright © 2012-2022 Basis Technology. Generated on: Tue Feb 6 2024
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.