Autopsy  4.12.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
PlasoIngestModule.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2018-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.modules.plaso;
20 
21 import java.io.BufferedReader;
22 import java.io.BufferedWriter;
23 import java.io.File;
24 import java.io.FileNotFoundException;
25 import java.io.IOException;
26 import java.io.InputStreamReader;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.nio.file.Paths;
30 import java.sql.ResultSet;
31 import java.sql.SQLException;
32 import java.text.SimpleDateFormat;
33 import java.util.Arrays;
34 import java.util.Collection;
35 import java.util.List;
36 import java.util.Locale;
37 import static java.util.Objects.nonNull;
38 import java.util.logging.Level;
39 import java.util.stream.Collectors;
40 import org.openide.modules.InstalledFileLocator;
41 import org.openide.util.Cancellable;
42 import org.openide.util.NbBundle;
56 import org.sleuthkit.datamodel.AbstractFile;
57 import org.sleuthkit.datamodel.Blackboard;
58 import org.sleuthkit.datamodel.Blackboard.BlackboardException;
59 import org.sleuthkit.datamodel.BlackboardArtifact;
60 import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_TL_EVENT;
61 import org.sleuthkit.datamodel.BlackboardAttribute;
62 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME;
63 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DESCRIPTION;
64 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TL_EVENT_TYPE;
65 import org.sleuthkit.datamodel.Content;
66 import org.sleuthkit.datamodel.Image;
67 import org.sleuthkit.datamodel.TskCoreException;
68 import org.sleuthkit.datamodel.TimelineEventType;
69 
73 public class PlasoIngestModule implements DataSourceIngestModule {
74 
75  private static final Logger logger = Logger.getLogger(PlasoIngestModule.class.getName());
76  private static final String MODULE_NAME = PlasoModuleFactory.getModuleName();
77 
78  private static final String PLASO = "plaso"; //NON-NLS
79  private static final String PLASO64 = "plaso-20180818-amd64";//NON-NLS
80  private static final String PLASO32 = "plaso-20180818-win32";//NON-NLS
81  private static final String LOG2TIMELINE_EXECUTABLE = "Log2timeline.exe";//NON-NLS
82  private static final String PSORT_EXECUTABLE = "psort.exe";//NON-NLS
83  private static final String COOKIE = "cookie";//NON-NLS
84  private static final int LOG2TIMELINE_WORKERS = 2;
85 
86  private File log2TimeLineExecutable;
87  private File psortExecutable;
88 
91  private Case currentCase;
93 
94  private Image image;
95  private AbstractFile previousFile = null; // cache used when looking up files in Autopsy DB
96 
98  this.settings = settings;
99  }
100 
101  @NbBundle.Messages({
102  "PlasoIngestModule.executable.not.found=Plaso Executable Not Found.",
103  "PlasoIngestModule.requires.windows=Plaso module requires windows.",
104  "PlasoIngestModule.dataSource.not.an.image=Datasource is not an Image."})
105  @Override
106  public void startUp(IngestJobContext context) throws IngestModuleException {
107  this.context = context;
108 
109  if (false == PlatformUtil.isWindowsOS()) {
110  throw new IngestModuleException(Bundle.PlasoIngestModule_requires_windows());
111  }
112 
113  try {
114  log2TimeLineExecutable = locateExecutable(LOG2TIMELINE_EXECUTABLE);
115  psortExecutable = locateExecutable(PSORT_EXECUTABLE);
116  } catch (FileNotFoundException exception) {
117  logger.log(Level.WARNING, "Plaso executable not found.", exception); //NON-NLS
118  throw new IngestModuleException(Bundle.PlasoIngestModule_executable_not_found(), exception);
119  }
120 
121  Content dataSource = context.getDataSource();
122  if (!(dataSource instanceof Image)) {
123  throw new IngestModuleException(Bundle.PlasoIngestModule_dataSource_not_an_image());
124  }
125  image = (Image) dataSource;
126  }
127 
128  @NbBundle.Messages({
129  "PlasoIngestModule.error.running.log2timeline=Error running log2timeline, see log file.",
130  "PlasoIngestModule.error.running.psort=Error running Psort, see log file.",
131  "PlasoIngestModule.error.creating.output.dir=Error creating Plaso module output directory.",
132  "PlasoIngestModule.starting.log2timeline=Starting Log2timeline",
133  "PlasoIngestModule.running.psort=Running Psort",
134  "PlasoIngestModule.log2timeline.cancelled=Log2timeline run was canceled",
135  "PlasoIngestModule.psort.cancelled=psort run was canceled",
136  "PlasoIngestModule.bad.imageFile=Cannot find image file name and path",
137  "PlasoIngestModule.completed=Plaso Processing Completed",
138  "PlasoIngestModule.has.run=Plaso Plugin has been run.",
139  "PlasoIngestModule.psort.fail=Plaso returned an error when sorting events. Results are not complete."})
140  @Override
141  public ProcessResult process(Content dataSource, DataSourceIngestModuleProgress statusHelper) {
142  assert dataSource.equals(image);
143 
144  statusHelper.switchToDeterminate(100);
145  currentCase = Case.getCurrentCase();
146  fileManager = currentCase.getServices().getFileManager();
147 
148  String currentTime = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss z", Locale.US).format(System.currentTimeMillis());//NON-NLS
149  Path moduleOutputPath = Paths.get(currentCase.getModuleDirectory(), PLASO, currentTime);
150  try {
151  Files.createDirectories(moduleOutputPath);
152  } catch (IOException ex) {
153  logger.log(Level.SEVERE, "Error creating Plaso module output directory.", ex); //NON-NLS
154  return ProcessResult.ERROR;
155  }
156 
157  // Run log2timeline
158  logger.log(Level.INFO, "Starting Plaso Run.");//NON-NLS
159  statusHelper.progress(Bundle.PlasoIngestModule_starting_log2timeline(), 0);
160  ProcessBuilder log2TimeLineCommand = buildLog2TimeLineCommand(moduleOutputPath, image);
161  try {
162  Process log2TimeLineProcess = log2TimeLineCommand.start();
163  try (BufferedReader log2TimeLineOutpout = new BufferedReader(new InputStreamReader(log2TimeLineProcess.getInputStream()))) {
164  L2TStatusProcessor statusReader = new L2TStatusProcessor(log2TimeLineOutpout, statusHelper, moduleOutputPath);
165  new Thread(statusReader, "log2timeline status reader").start(); //NON-NLS
166  ExecUtil.waitForTermination(LOG2TIMELINE_EXECUTABLE, log2TimeLineProcess, new DataSourceIngestModuleProcessTerminator(context));
167  statusReader.cancel();
168  }
169 
170  if (context.dataSourceIngestIsCancelled()) {
171  logger.log(Level.INFO, "Log2timeline run was canceled"); //NON-NLS
172  return ProcessResult.OK;
173  }
174  if (Files.notExists(moduleOutputPath.resolve(PLASO))) {
175  logger.log(Level.WARNING, "Error running log2timeline: there was no storage file."); //NON-NLS
176  return ProcessResult.ERROR;
177  }
178 
179  // sort the output
180  statusHelper.progress(Bundle.PlasoIngestModule_running_psort(), 33);
181  ProcessBuilder psortCommand = buildPsortCommand(moduleOutputPath);
182  int result = ExecUtil.execute(psortCommand, new DataSourceIngestModuleProcessTerminator(context));
183  if (result != 0) {
184  logger.log(Level.SEVERE, String.format("Error running Psort, error code returned %d", result)); //NON-NLS
185  MessageNotifyUtil.Notify.error(MODULE_NAME, Bundle.PlasoIngestModule_psort_fail());
186  return ProcessResult.ERROR;
187  }
188 
189  if (context.dataSourceIngestIsCancelled()) {
190  logger.log(Level.INFO, "psort run was canceled"); //NON-NLS
191  return ProcessResult.OK;
192  }
193  Path plasoFile = moduleOutputPath.resolve("plasodb.db3"); //NON-NLS
194  if (Files.notExists(plasoFile)) {
195  logger.log(Level.SEVERE, "Error running Psort: there was no sqlite db file."); //NON-NLS
196  return ProcessResult.ERROR;
197  }
198 
199  // parse the output and make artifacts
200  createPlasoArtifacts(plasoFile.toString(), statusHelper);
201 
202  } catch (IOException ex) {
203  logger.log(Level.SEVERE, "Error running Plaso.", ex);//NON-NLS
204  return ProcessResult.ERROR;
205  }
206 
208  Bundle.PlasoIngestModule_has_run(),
209  Bundle.PlasoIngestModule_completed());
211  return ProcessResult.OK;
212  }
213 
214  private ProcessBuilder buildLog2TimeLineCommand(Path moduleOutputPath, Image image) {
215  //make a csv list of disabled parsers.
216  String parsersString = settings.getParsers().entrySet().stream()
217  .filter(entry -> entry.getValue() == false)
218  .map(entry -> "!" + entry.getKey()) // '!' prepended to parsername disables it. //NON-NLS
219  .collect(Collectors.joining(","));//NON-NLS
220 
221  ProcessBuilder processBuilder = buildProcessWithRunAsInvoker(
222  "\"" + log2TimeLineExecutable + "\"", //NON-NLS
223  "--vss-stores", "all", //NON-NLS
224  "-z", image.getTimeZone(), //NON-NLS
225  "--partitions", "all", //NON-NLS
226  "--hasher_file_size_limit", "1", //NON-NLS
227  "--hashers", "none", //NON-NLS
228  "--parsers", "\"" + parsersString + "\"",//NON-NLS
229  "--no_dependencies_check", //NON-NLS
230  "--workers", String.valueOf(LOG2TIMELINE_WORKERS),//NON-NLS
231  moduleOutputPath.resolve(PLASO).toString(),
232  image.getPaths()[0]
233  );
234  processBuilder.redirectError(moduleOutputPath.resolve("log2timeline_err.txt").toFile()); //NON-NLS
235  return processBuilder;
236  }
237 
238  static private ProcessBuilder buildProcessWithRunAsInvoker(String... commandLine) {
239  ProcessBuilder processBuilder = new ProcessBuilder(commandLine);
240  /* Add an environment variable to force log2timeline/psort to run with
241  * the same permissions Autopsy uses. */
242  processBuilder.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS
243  return processBuilder;
244  }
245 
246  private ProcessBuilder buildPsortCommand(Path moduleOutputPath) {
247  ProcessBuilder processBuilder = buildProcessWithRunAsInvoker(
248  "\"" + psortExecutable + "\"", //NON-NLS
249  "-o", "4n6time_sqlite", //NON-NLS
250  "-w", moduleOutputPath.resolve("plasodb.db3").toString(), //NON-NLS
251  moduleOutputPath.resolve(PLASO).toString()
252  );
253 
254  processBuilder.redirectOutput(moduleOutputPath.resolve("psort_output.txt").toFile()); //NON-NLS
255  processBuilder.redirectError(moduleOutputPath.resolve("psort_err.txt").toFile()); //NON-NLS
256  return processBuilder;
257  }
258 
259  private static File locateExecutable(String executableName) throws FileNotFoundException {
260  String architectureFolder = PlatformUtil.is64BitOS() ? PLASO64 : PLASO32;
261  String executableToFindName = Paths.get(PLASO, architectureFolder, executableName).toString();
262 
263  File exeFile = InstalledFileLocator.getDefault().locate(executableToFindName, PlasoIngestModule.class.getPackage().getName(), false);
264  if (null == exeFile || exeFile.canExecute() == false) {
265  throw new FileNotFoundException(executableName + " executable not found.");
266  }
267  return exeFile;
268  }
269 
270  @NbBundle.Messages({
271  "PlasoIngestModule.exception.posting.artifact=Exception Posting artifact.",
272  "PlasoIngestModule.event.datetime=Event Date Time",
273  "PlasoIngestModule.event.description=Event Description",
274  "PlasoIngestModule.create.artifacts.cancelled=Cancelled Plaso Artifact Creation ",
275  "# {0} - file that events are from",
276  "PlasoIngestModule.artifact.progress=Adding events to case: {0}",
277  "PlasoIngestModule.info.empty.database=Plaso database was empty.",
278  })
279  private void createPlasoArtifacts(String plasoDb, DataSourceIngestModuleProgress statusHelper) {
280  Blackboard blackboard = currentCase.getSleuthkitCase().getBlackboard();
281 
282  String sqlStatement = "SELECT substr(filename,1) AS filename, "
283  + " strftime('%s', datetime) AS epoch_date, "
284  + " description, "
285  + " source, "
286  + " type, "
287  + " sourcetype "
288  + " FROM log2timeline "
289  + " WHERE source NOT IN ('FILE', "
290  + " 'WEBHIST') " // bad dates and duplicates with what we have.
291  + " AND sourcetype NOT IN ('UNKNOWN', "
292  + " 'PE Import Time');"; // lots of bad dates //NON-NLS
293 
294  try (SQLiteDBConnect tempdbconnect = new SQLiteDBConnect("org.sqlite.JDBC", "jdbc:sqlite:" + plasoDb); //NON-NLS
295  ResultSet resultSet = tempdbconnect.executeQry(sqlStatement)) {
296 
297  boolean dbHasData = false;
298 
299  while (resultSet.next()) {
300  dbHasData = true;
301 
302  if (context.dataSourceIngestIsCancelled()) {
303  logger.log(Level.INFO, "Cancelled Plaso Artifact Creation."); //NON-NLS
304  return;
305  }
306 
307  String currentFileName = resultSet.getString("filename"); //NON-NLS
308  statusHelper.progress(Bundle.PlasoIngestModule_artifact_progress(currentFileName), 66);
309  Content resolvedFile = getAbstractFile(currentFileName);
310  if (resolvedFile == null) {
311  logger.log(Level.INFO, "File {0} from Plaso output not found in case. Associating it with the data source instead.", currentFileName);//NON-NLS
312  resolvedFile = image;
313  }
314 
315  String description = resultSet.getString("description");
316  TimelineEventType eventType = findEventSubtype(currentFileName, resultSet);
317 
318  // If the description is empty use the event type display name
319  // as the description.
320  if ( description == null || description.isEmpty() ) {
321  if (eventType != TimelineEventType.OTHER) {
322  description = eventType.getDisplayName();
323  } else {
324  continue;
325  }
326  }
327 
328  Collection<BlackboardAttribute> bbattributes = Arrays.asList(
329  new BlackboardAttribute(
330  TSK_DATETIME, MODULE_NAME,
331  resultSet.getLong("epoch_date")), //NON-NLS
332  new BlackboardAttribute(
333  TSK_DESCRIPTION, MODULE_NAME,
334  description),//NON-NLS
335  new BlackboardAttribute(
336  TSK_TL_EVENT_TYPE, MODULE_NAME,
337  eventType.getTypeID()));
338 
339  try {
340  BlackboardArtifact bbart = resolvedFile.newArtifact(TSK_TL_EVENT);
341  bbart.addAttributes(bbattributes);
342  try {
343  /* Post the artifact which will index the artifact for
344  * keyword search, and fire an event to notify UI of
345  * this new artifact */
346  blackboard.postArtifact(bbart, MODULE_NAME);
347  } catch (BlackboardException ex) {
348  logger.log(Level.SEVERE, "Error Posting Artifact.", ex);//NON-NLS
349  }
350  } catch (TskCoreException ex) {
351  logger.log(Level.SEVERE, "Exception Adding Artifact.", ex);//NON-NLS
352  }
353  }
354 
355  // Check if there is data the db
356  if( !dbHasData ) {
357  logger.log(Level.INFO, String.format("PlasoDB was empty: %s", plasoDb));
358  MessageNotifyUtil.Notify.info(MODULE_NAME, Bundle.PlasoIngestModule_info_empty_database());
359  }
360  } catch (SQLException ex) {
361  logger.log(Level.SEVERE, "Error while trying to read into a sqlite db.", ex);//NON-NLS
362  }
363  }
364 
365  private AbstractFile getAbstractFile(String file) {
366 
367  Path path = Paths.get(file);
368  String fileName = path.getFileName().toString();
369  String filePath = path.getParent().toString().replaceAll("\\\\", "/");//NON-NLS
370  if (filePath.endsWith("/") == false) {//NON-NLS
371  filePath += "/";//NON-NLS
372  }
373 
374  // check the cached file
375  //TODO: would we reduce 'cache misses' if we retrieved the events sorted by file? Is that overhead worth it?
376  if (previousFile != null
377  && previousFile.getName().equalsIgnoreCase(fileName)
378  && previousFile.getParentPath().equalsIgnoreCase(filePath)) {
379  return previousFile;
380 
381  }
382  try {
383  List<AbstractFile> abstractFiles = fileManager.findFiles(fileName, filePath);
384  if (abstractFiles.size() == 1) {// TODO: why do we bother with this check. also we don't cache the file...
385  return abstractFiles.get(0);
386  }
387  for (AbstractFile resolvedFile : abstractFiles) {
388  // double check its an exact match
389  if (filePath.equalsIgnoreCase(resolvedFile.getParentPath())) {
390  // cache it for next time
391  previousFile = resolvedFile;
392  return resolvedFile;
393  }
394  }
395  } catch (TskCoreException ex) {
396  logger.log(Level.SEVERE, "Exception finding file.", ex);
397  }
398  return null;
399  }
400 
412  private TimelineEventType findEventSubtype(String fileName, ResultSet row) throws SQLException {
413  switch (row.getString("source")) {
414  case "WEBHIST": //These shouldn't actually be present, but keeping the logic just in case...
415  if (fileName.toLowerCase().contains(COOKIE)
416  || row.getString("type").toLowerCase().contains(COOKIE)) {//NON-NLS
417 
418  return TimelineEventType.WEB_COOKIE;
419  } else {
420  return TimelineEventType.WEB_HISTORY;
421  }
422  case "EVT":
423  case "LOG":
424  return TimelineEventType.LOG_ENTRY;
425  case "REG":
426  switch (row.getString("sourcetype").toLowerCase()) {//NON-NLS
427  case "unknown : usb entries":
428  case "unknown : usbstor entries":
429  return TimelineEventType.DEVICES_ATTACHED;
430  default:
431  return TimelineEventType.REGISTRY;
432  }
433  default:
434  return TimelineEventType.OTHER;
435  }
436  }
437 
443  private static class L2TStatusProcessor implements Runnable, Cancellable {
444 
445  private final BufferedReader log2TimeLineOutpout;
447  volatile private boolean cancelled = false;
448  private final Path outputPath;
449 
450  private L2TStatusProcessor(BufferedReader log2TimeLineOutpout, DataSourceIngestModuleProgress statusHelper, Path outputPath) throws IOException {
451  this.log2TimeLineOutpout = log2TimeLineOutpout;
452  this.statusHelper = statusHelper;
453  this.outputPath = outputPath;
454  }
455 
456  @Override
457  public void run() {
458  try (BufferedWriter writer = Files.newBufferedWriter(outputPath.resolve("log2timeline_output.txt"));) {//NON-NLS
459  String line = log2TimeLineOutpout.readLine();
460  while (cancelled == false && nonNull(line)) {
461  statusHelper.progress(line);
462  writer.write(line);
463  writer.newLine();
464  line = log2TimeLineOutpout.readLine();
465  }
466  writer.flush();
467  } catch (IOException ex) {
468  logger.log(Level.WARNING, "Error reading log2timeline output stream.", ex);//NON-NLS
469  }
470  }
471 
472  @Override
473  public boolean cancel() {
474  cancelled = true;
475  return true;
476  }
477  }
478 }
static int execute(ProcessBuilder processBuilder)
Definition: ExecUtil.java:130
L2TStatusProcessor(BufferedReader log2TimeLineOutpout, DataSourceIngestModuleProgress statusHelper, Path outputPath)
static ProcessBuilder buildProcessWithRunAsInvoker(String...commandLine)
static IngestMessage createMessage(MessageType messageType, String source, String subject, String detailsHtml)
static int waitForTermination(String command, Process process, ProcessTerminator terminator)
Definition: ExecUtil.java:215
static void info(String title, String message)
ProcessBuilder buildPsortCommand(Path moduleOutputPath)
void postMessage(final IngestMessage message)
static void error(String title, String message)
synchronized List< AbstractFile > findFiles(String fileName)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
ProcessResult process(Content dataSource, DataSourceIngestModuleProgress statusHelper)
ProcessBuilder buildLog2TimeLineCommand(Path moduleOutputPath, Image image)
TimelineEventType findEventSubtype(String fileName, ResultSet row)
void createPlasoArtifacts(String plasoDb, DataSourceIngestModuleProgress statusHelper)
static synchronized IngestServices getInstance()

Copyright © 2012-2018 Basis Technology. Generated on: Wed Sep 18 2019
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.