Esempio n. 1
0
/**
 * Stores all information for a given case. Only a single case can currently be open at a time. Use
 * getCurrentCase() to retrieve the object for the current case.
 */
public class Case {

  private static final String autopsyVer =
      Version.getVersion(); // current version of autopsy. Change it when the version is changed
  private static final String appName = Version.getName() + " " + autopsyVer;
  /**
   * Property name that indicates the name of the current case has changed. Fired with the case is
   * renamed, and when the current case is opened/closed/changed. The value is a String: the name of
   * the case. The empty string ("") is used for no open case.
   */
  public static final String CASE_NAME = "caseName";
  /**
   * Property name that indicates the number of the current case has changed. Fired with the case
   * number is changed. The value is an int: the number of the case. -1 is used for no case number
   * set.
   */
  public static final String CASE_NUMBER = "caseNumber";
  /**
   * Property name that indicates the examiner of the current case has changed. Fired with the case
   * examiner is changed. The value is a String: the name of the examiner. The empty string ("") is
   * used for no examiner set.
   */
  public static final String CASE_EXAMINER = "caseExaminer";
  /**
   * Property name that indicates a new data source (image, disk or local file) has been added to
   * the current case. The new value is the newly-added instance of the new data source, and the old
   * value is always null.
   */
  public static final String CASE_ADD_DATA_SOURCE = "addDataSource";
  /**
   * Property name that indicates a data source has been removed from the current case. The "old
   * value" is the (int) content ID of the data source that was removed, the new value is the
   * instance of the data source.
   */
  public static final String CASE_DEL_DATA_SOURCE = "removeDataSource";
  /**
   * Property name that indicates the currently open case has changed. The new value is the instance
   * of the opened Case, or null if there is no open case. The old value is the instance of the
   * closed Case, or null if there was no open case.
   */
  public static final String CASE_CURRENT_CASE = "currentCase";
  /** Name for the property that determines whether to show the dialog at startup */
  public static final String propStartup = "LBL_StartupDialog";
  // pcs is initialized in CaseListener constructor
  private static final PropertyChangeSupport pcs = new PropertyChangeSupport(Case.class);
  private String name;
  private String number;
  private String examiner;
  private String configFilePath;
  private XMLCaseManagement xmlcm;
  private SleuthkitCase db;
  // Track the current case (only set with changeCase() method)
  private static Case currentCase = null;
  private Services services;
  private static final Logger logger = Logger.getLogger(Case.class.getName());
  static final String CASE_EXTENSION = "aut";
  static final String CASE_DOT_EXTENSION = "." + CASE_EXTENSION;

  /** Constructor for the Case class */
  private Case(
      String name,
      String number,
      String examiner,
      String configFilePath,
      XMLCaseManagement xmlcm,
      SleuthkitCase db) {
    this.name = name;
    this.number = number;
    this.examiner = examiner;
    this.configFilePath = configFilePath;
    this.xmlcm = xmlcm;
    this.db = db;
    this.services = new Services(db);
  }

  /**
   * Gets the currently opened case, if there is one.
   *
   * @return the current open case
   * @throws IllegalStateException if there is no case open.
   */
  public static Case getCurrentCase() {
    if (currentCase != null) {
      return currentCase;
    } else {
      throw new IllegalStateException("Can't get the current case; there is no case open!");
    }
  }

  /**
   * Check if case is currently open
   *
   * @return true if case is open
   */
  public static boolean isCaseOpen() {
    return currentCase != null;
  }

  /**
   * Updates the current case to the given case and fires off the appropriate property-change
   *
   * @param newCase the new current case
   */
  private static void changeCase(Case newCase) {

    Case oldCase = Case.currentCase;
    Case.currentCase = null;

    String oldCaseName = oldCase != null ? oldCase.name : "";

    doCaseChange(null); // closes windows, etc
    pcs.firePropertyChange(CASE_CURRENT_CASE, oldCase, null);

    doCaseNameChange("");
    pcs.firePropertyChange(CASE_NAME, oldCaseName, "");

    if (newCase != null) {
      currentCase = newCase;

      pcs.firePropertyChange(CASE_CURRENT_CASE, null, currentCase);
      doCaseChange(currentCase);

      pcs.firePropertyChange(CASE_NAME, "", currentCase.name);
      doCaseNameChange(currentCase.name);

      RecentCases.getInstance()
          .addRecentCase(currentCase.name, currentCase.configFilePath); // update the recent cases
    }
  }

  AddImageProcess makeAddImageProcess(
      String timezone, boolean processUnallocSpace, boolean noFatOrphans) {
    return this.db.makeAddImageProcess(timezone, processUnallocSpace, noFatOrphans);
  }

  /**
   * Creates a new case (create the XML config file and database)
   *
   * @param caseDir The directory to store case data in. Will be created if it doesn't already
   *     exist. If it exists, it should have all of the needed sub dirs that createCaseDirectory()
   *     will create.
   * @param caseName the name of case
   * @param caseNumber the case number
   * @param examiner the examiner for this case
   */
  public static void create(String caseDir, String caseName, String caseNumber, String examiner)
      throws CaseActionException {
    logger.log(
        Level.INFO,
        "Creating new case.\ncaseDir: {0}\ncaseName: {1}",
        new Object[] {caseDir, caseName});

    // create case directory if it doesn't already exist.
    if (new File(caseDir).exists() == false) {
      Case.createCaseDirectory(caseDir);
    }

    String configFilePath = caseDir + File.separator + caseName + CASE_DOT_EXTENSION;

    XMLCaseManagement xmlcm = new XMLCaseManagement();
    xmlcm.create(caseDir, caseName, examiner, caseNumber); // create a new XML config file
    xmlcm.writeFile();

    String dbPath = caseDir + File.separator + "autopsy.db";
    SleuthkitCase db = null;
    try {
      db = SleuthkitCase.newCase(dbPath);
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Error creating a case: " + caseName + " in dir " + caseDir, ex);
      throw new CaseActionException(
          "Error creating a case: " + caseName + " in dir " + caseDir, ex);
    }

    Case newCase = new Case(caseName, caseNumber, examiner, configFilePath, xmlcm, db);

    changeCase(newCase);
  }

  /**
   * Opens the existing case (open the XML config file)
   *
   * @param configFilePath the path of the configuration file that's opened
   * @throws CaseActionException
   */
  static void open(String configFilePath) throws CaseActionException {
    logger.log(Level.INFO, "Opening case.\nconfigFilePath: {0}", configFilePath);

    try {
      XMLCaseManagement xmlcm = new XMLCaseManagement();

      xmlcm.open(
          configFilePath); // open and load the config file to the document handler in the XML class
      xmlcm.writeFile(); // write any changes to the config file

      String caseName = xmlcm.getCaseName();
      String caseNumber = xmlcm.getCaseNumber();
      String examiner = xmlcm.getCaseExaminer();
      // if the caseName is "", case / config file can't be opened
      if (caseName.equals("")) {
        throw new CaseActionException("Case name is blank.");
      }

      String caseDir = xmlcm.getCaseDirectory();
      String dbPath = caseDir + File.separator + "autopsy.db";
      SleuthkitCase db = SleuthkitCase.openCase(dbPath);

      checkImagesExist(db);

      Case openedCase = new Case(caseName, caseNumber, examiner, configFilePath, xmlcm, db);

      changeCase(openedCase);

    } catch (Exception ex) {
      logger.log(Level.SEVERE, "Error opening the case: ", ex);
      // close the previous case if there's any
      CaseCloseAction closeCase = SystemAction.get(CaseCloseAction.class);
      closeCase.actionPerformed(null);
      if (!configFilePath.endsWith(CASE_DOT_EXTENSION)) {
        throw new CaseActionException(
            "Check that you selected the correct case file (usually with "
                + CASE_DOT_EXTENSION
                + " extension)",
            ex);
      } else {
        throw new CaseActionException("Error opening the case", ex);
      }
    }
  }

  static Map<Long, String> getImagePaths(SleuthkitCase db) { // TODO: clean this up
    Map<Long, String> imgPaths = new HashMap<Long, String>();
    try {
      Map<Long, List<String>> imgPathsList = db.getImagePaths();
      for (Map.Entry<Long, List<String>> entry : imgPathsList.entrySet()) {
        if (entry.getValue().size() > 0) {
          imgPaths.put(entry.getKey(), entry.getValue().get(0));
        }
      }
    } catch (TskException ex) {
      logger.log(Level.WARNING, "Error getting image paths", ex);
    }
    return imgPaths;
  }

  /** Ensure that all image paths point to valid image files */
  private static void checkImagesExist(SleuthkitCase db) {
    Map<Long, String> imgPaths = getImagePaths(db);
    for (Map.Entry<Long, String> entry : imgPaths.entrySet()) {
      long obj_id = entry.getKey();
      String path = entry.getValue();
      boolean fileExists = (pathExists(path) || driveExists(path));
      if (!fileExists) {
        int ret =
            JOptionPane.showConfirmDialog(
                null,
                appName
                    + " has detected that one of the images associated with \n"
                    + "this case are missing. Would you like to search for them now?\n"
                    + "Previously, the image was located at:\n"
                    + path
                    + "\nPlease note that you will still be able to browse directories and generate reports\n"
                    + "if you choose No, but you will not be able to view file content or run the ingest process.",
                "Missing Image",
                JOptionPane.YES_NO_OPTION);
        if (ret == JOptionPane.YES_OPTION) {
          MissingImageDialog.makeDialog(obj_id, db);
        } else {
          logger.log(Level.WARNING, "Selected image files don't match old files!");
        }
      }
    }
  }

  /**
   * Adds the image to the current case after it has been added to the DB Sends out event and
   * reopens windows if needed.
   *
   * @param imgPaths the paths of the image that being added
   * @param imgId the ID of the image that being added
   * @param timeZone the timeZone of the image where it's added
   */
  Image addImage(String imgPath, long imgId, String timeZone) throws CaseActionException {
    logger.log(
        Level.INFO,
        "Adding image to Case.  imgPath: {0}  ID: {1} TimeZone: {2}",
        new Object[] {imgPath, imgId, timeZone});

    try {
      Image newImage = db.getImageById(imgId);
      pcs.firePropertyChange(
          CASE_ADD_DATA_SOURCE, null, newImage); // the new value is the instance of the image
      CoreComponentControl.openCoreWindows();
      return newImage;
    } catch (Exception ex) {
      throw new CaseActionException("Error adding image to the case", ex);
    }
  }

  /**
   * Finishes adding new local data source to the case Sends out event and reopens windows if
   * needed.
   *
   * @param newDataSource new data source added
   */
  void addLocalDataSource(Content newDataSource) {
    pcs.firePropertyChange(CASE_ADD_DATA_SOURCE, null, newDataSource);
    CoreComponentControl.openCoreWindows();
  }

  /** @return The Services object for this case. */
  public Services getServices() {
    return services;
  }

  /**
   * Get the underlying SleuthkitCase instance from the Sleuth Kit bindings library.
   *
   * @return
   */
  public SleuthkitCase getSleuthkitCase() {
    return this.db;
  }

  /** Closes this case. This methods close the xml and clear all the fields. */
  void closeCase() throws CaseActionException {
    changeCase(null);

    try {
      services.close();
      this.xmlcm.close(); // close the xmlcm
      this.db.close();
    } catch (Exception e) {
      throw new CaseActionException("Error while trying to close the current case.", e);
    }
  }

  /**
   * Delete this case. This methods delete all folders and files of this case.
   *
   * @param caseDir case dir to delete
   * @throws CaseActionException exception throw if case could not be deleted
   */
  void deleteCase(File caseDir) throws CaseActionException {
    logger.log(Level.INFO, "Deleting case.\ncaseDir: {0}", caseDir);

    try {

      xmlcm.close(); // close the xmlcm
      boolean result = deleteCaseDirectory(caseDir); // delete the directory

      RecentCases.getInstance()
          .removeRecentCase(this.name, this.configFilePath); // remove it from the recent case
      Case.changeCase(null);
      if (result == false) {
        throw new CaseActionException("Error deleting the case dir: " + caseDir);
      }
    } catch (Exception ex) {
      logger.log(Level.SEVERE, "Error deleting the current case dir: " + caseDir, ex);
      throw new CaseActionException("Error deleting the case dir: " + caseDir, ex);
    }
  }

  /**
   * Updates the case name.
   *
   * @param oldCaseName the old case name that wants to be updated
   * @param oldPath the old path that wants to be updated
   * @param newCaseName the new case name
   * @param newPath the new path
   */
  void updateCaseName(String oldCaseName, String oldPath, String newCaseName, String newPath)
      throws CaseActionException {
    try {
      xmlcm.setCaseName(newCaseName); // set the case
      name = newCaseName; // change the local value
      RecentCases.getInstance()
          .updateRecentCase(oldCaseName, oldPath, newCaseName, newPath); // update the recent case

      pcs.firePropertyChange(CASE_NAME, oldCaseName, newCaseName);
      doCaseNameChange(newCaseName);

    } catch (Exception e) {
      throw new CaseActionException("Error while trying to update the case name.", e);
    }
  }

  /**
   * Updates the case examiner
   *
   * @param oldExaminer the old examiner
   * @param newExaminer the new examiner
   */
  void updateExaminer(String oldExaminer, String newExaminer) throws CaseActionException {
    try {
      xmlcm.setCaseExaminer(newExaminer); // set the examiner
      examiner = newExaminer;

      pcs.firePropertyChange(CASE_EXAMINER, oldExaminer, newExaminer);
    } catch (Exception e) {
      throw new CaseActionException("Error while trying to update the examiner.", e);
    }
  }

  /**
   * Updates the case number
   *
   * @param oldCaseNumber the old case number
   * @param newCaseNumber the new case number
   */
  void updateCaseNumber(String oldCaseNumber, String newCaseNumber) throws CaseActionException {
    try {
      xmlcm.setCaseNumber(newCaseNumber); // set the case number
      number = newCaseNumber;

      pcs.firePropertyChange(CASE_NUMBER, oldCaseNumber, newCaseNumber);
    } catch (Exception e) {
      throw new CaseActionException("Error while trying to update the case number.", e);
    }
  }

  /**
   * Checks whether there is a current case open.
   *
   * @return True if a case is open.
   */
  public static boolean existsCurrentCase() {
    return currentCase != null;
  }

  /**
   * Uses the given path to store it as the configuration file path
   *
   * @param givenPath the given config file path
   */
  private void setConfigFilePath(String givenPath) {
    configFilePath = givenPath;
  }

  /**
   * Get the config file path in the given path
   *
   * @return configFilePath the path of the configuration file
   */
  String getConfigFilePath() {
    return configFilePath;
  }

  /**
   * Returns the current version of Autopsy
   *
   * @return autopsyVer
   */
  public static String getAutopsyVersion() {
    return autopsyVer;
  }

  /**
   * Gets the application name
   *
   * @return appName
   */
  public static String getAppName() {
    return appName;
  }

  /**
   * Gets the case name
   *
   * @return name
   */
  public String getName() {
    return name;
  }

  /**
   * Gets the case number
   *
   * @return number
   */
  public String getNumber() {
    return number;
  }

  /**
   * Gets the Examiner name
   *
   * @return examiner
   */
  public String getExaminer() {
    return examiner;
  }

  /**
   * Gets the case directory path
   *
   * @return caseDirectoryPath
   */
  public String getCaseDirectory() {
    if (xmlcm == null) {
      return "";
    } else {
      return xmlcm.getCaseDirectory();
    }
  }

  /**
   * Gets the full path to the temp directory of this case
   *
   * @return tempDirectoryPath
   */
  public String getTempDirectory() {
    if (xmlcm == null) {
      return "";
    } else {
      return xmlcm.getTempDir();
    }
  }

  /**
   * Gets the full path to the cache directory of this case
   *
   * @return cacheDirectoryPath
   */
  public String getCacheDirectory() {
    if (xmlcm == null) {
      return "";
    } else {
      return xmlcm.getCacheDir();
    }
  }

  /**
   * get the created date of this case
   *
   * @return case creation date
   */
  public String getCreatedDate() {
    if (xmlcm == null) {
      return "";
    } else {
      return xmlcm.getCreatedDate();
    }
  }

  /**
   * Get absolute module output directory path where modules should save their permanent data The
   * directory is a subdirectory of this case dir.
   *
   * @return absolute path to the module output dir
   */
  public String getModulesOutputDirAbsPath() {
    return this.getCaseDirectory() + File.separator + getModulesOutputDirRelPath();
  }

  /**
   * Get relative (with respect to case dir) module output directory path where modules should save
   * their permanent data The directory is a subdirectory of this case dir.
   *
   * @return relative path to the module output dir
   */
  public static String getModulesOutputDirRelPath() {
    return "ModuleOutput";
  }

  /**
   * get the PropertyChangeSupport of this class
   *
   * @return PropertyChangeSupport
   */
  public static PropertyChangeSupport getPropertyChangeSupport() {
    return pcs;
  }

  String getImagePaths(Long imgID) {
    return getImagePaths(db).get(imgID);
  }

  /**
   * get all the image id in this case
   *
   * @return imageIDs
   */
  public Long[] getImageIDs() {
    Set<Long> ids = getImagePaths(db).keySet();
    return ids.toArray(new Long[ids.size()]);
  }

  public List<Image> getImages() throws TskCoreException {
    return db.getImages();
  }

  /**
   * Count the root objects.
   *
   * @return The number of total root objects in this case.
   */
  public int getRootObjectsCount() {
    return getRootObjects().size();
  }

  /**
   * Get the data model Content objects in the root of this case's hierarchy.
   *
   * @return a list of the root objects
   */
  public List<Content> getRootObjects() {
    try {
      return db.getRootObjects();
    } catch (TskException ex) {
      throw new RuntimeException("Error getting root objects.", ex);
    }
  }

  /**
   * Gets the time zone(s) of the image(s) in this case.
   *
   * @return time zones the set of time zones
   */
  public Set<TimeZone> getTimeZone() {
    Set<TimeZone> timezones = new HashSet<TimeZone>();
    for (Content c : getRootObjects()) {
      try {
        final Image image = c.getImage();
        if (image != null) {
          timezones.add(TimeZone.getTimeZone(image.getTimeZone()));
        }
      } catch (TskException ex) {
        logger.log(Level.INFO, "Error getting time zones", ex);
      }
    }
    return timezones;
  }

  public static synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
    pcs.addPropertyChangeListener(listener);
  }

  public static synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
    pcs.removePropertyChangeListener(listener);
  }

  /**
   * Check if image from the given image path exists.
   *
   * @param imgPath the image path
   * @return isExist whether the path exists
   */
  public static boolean pathExists(String imgPath) {
    return new File(imgPath).isFile();
  }
  /** Does the given string refer to a physical drive? */
  private static final String pdisk = "\\\\.\\physicaldrive";

  private static final String dev = "/dev/";

  static boolean isPhysicalDrive(String path) {
    return path.toLowerCase().startsWith(pdisk) || path.toLowerCase().startsWith(dev);
  }

  /** Does the given string refer to a local drive / partition? */
  static boolean isPartition(String path) {
    return path.toLowerCase().startsWith("\\\\.\\") && path.toLowerCase().endsWith(":");
  }

  /**
   * Does the given drive path exist?
   *
   * @param path to drive
   * @return true if the drive exists, false otherwise
   */
  static boolean driveExists(String path) {
    // Test the drive by reading the first byte and checking if it's -1
    BufferedInputStream br = null;
    try {
      File tmp = new File(path);
      br = new BufferedInputStream(new FileInputStream(tmp));
      int b = br.read();
      if (b != -1) {
        return true;
      }
      return false;
    } catch (Exception ex) {
      return false;
    } finally {
      try {
        if (br != null) {
          br.close();
        }
      } catch (IOException ex) {
      }
    }
  }

  /**
   * Convert the Java timezone ID to the "formatted" string that can be accepted by the C/C++ code.
   * Example: "America/New_York" converted to "EST5EDT", etc
   *
   * @param timezoneID
   * @return
   */
  public static String convertTimeZone(String timezoneID) {
    String result = "";

    TimeZone zone = TimeZone.getTimeZone(timezoneID);
    int offset = zone.getRawOffset() / 1000;
    int hour = offset / 3600;
    int min = (offset % 3600) / 60;

    DateFormat dfm = new SimpleDateFormat("z");
    dfm.setTimeZone(zone);
    boolean hasDaylight = zone.useDaylightTime();
    String first =
        dfm.format(new GregorianCalendar(2010, 1, 1).getTime())
            .substring(0, 3); // make it only 3 letters code
    String second =
        dfm.format(new GregorianCalendar(2011, 6, 6).getTime())
            .substring(0, 3); // make it only 3 letters code
    int mid = hour * -1;
    result = first + Integer.toString(mid);
    if (min != 0) {
      result = result + ":" + Integer.toString(min);
    }
    if (hasDaylight) {
      result = result + second;
    }

    return result;
  }

  /* The methods below are used to manage the case directories (creating, checking, deleting, etc) */
  /**
   * to create the case directory
   *
   * @param caseDir Path to the case directory (typically base + case name)
   * @param caseName the case name (used only for error messages)
   * @throws CaseActionException throw if could not create the case dir @Deprecated
   */
  static void createCaseDirectory(String caseDir, String caseName) throws CaseActionException {
    createCaseDirectory(caseDir);
  }
  /**
   * Create the case directory and its needed subfolders.
   *
   * @param caseDir Path to the case directory (typically base + case name)
   * @throws CaseActionException throw if could not create the case dir
   */
  static void createCaseDirectory(String caseDir) throws CaseActionException {

    File caseDirF = new File(caseDir);
    if (caseDirF.exists()) {
      if (caseDirF.isFile()) {
        throw new CaseActionException(
            "Cannot create case dir, already exists and is not a directory: " + caseDir);
      } else if (!caseDirF.canRead() || !caseDirF.canWrite()) {
        throw new CaseActionException(
            "Cannot create case dir, already exists and cannot read/write: " + caseDir);
      }
    }

    try {
      boolean result = (caseDirF).mkdirs(); // create root case Directory
      if (result == false) {
        throw new CaseActionException("Cannot create case dir: " + caseDir);
      }

      // create the folders inside the case directory
      result =
          result
              && (new File(caseDir + File.separator + XMLCaseManagement.EXPORT_FOLDER_RELPATH))
                  .mkdir()
              && (new File(caseDir + File.separator + XMLCaseManagement.LOG_FOLDER_RELPATH)).mkdir()
              && (new File(caseDir + File.separator + XMLCaseManagement.TEMP_FOLDER_RELPATH))
                  .mkdir()
              && (new File(caseDir + File.separator + XMLCaseManagement.CACHE_FOLDER_RELPATH))
                  .mkdir();

      if (result == false) {
        throw new CaseActionException("Could not create case directory: " + caseDir);
      }

      final String modulesOutDir = caseDir + File.separator + getModulesOutputDirRelPath();
      result = new File(modulesOutDir).mkdir();
      if (result == false) {
        throw new CaseActionException(
            "Could not create modules output directory: " + modulesOutDir);
      }

    } catch (Exception e) {
      throw new CaseActionException("Could not create case directory: " + caseDir, e);
    }
  }

  /**
   * delete the given case directory
   *
   * @param casePath the case path
   * @return boolean whether the case directory is successfully deleted or not
   */
  static boolean deleteCaseDirectory(File casePath) {
    logger.log(Level.INFO, "Deleting case directory: " + casePath.getAbsolutePath());
    return FileUtil.deleteDir(casePath);
  }

  /** Invoke the creation of startup dialog window. */
  public static void invokeStartupDialog() {
    StartupWindowProvider.getInstance().open();
  }

  /** Call if there are no images in the case. Displays a dialog offering to add one. */
  private static void runAddImageAction() {
    SwingUtilities.invokeLater(
        new Runnable() {
          @Override
          public void run() {
            final AddImageAction action = Lookup.getDefault().lookup(AddImageAction.class);
            action.actionPerformed(null);
          }
        });
  }

  /**
   * Checks if a String is a valid case name
   *
   * @param caseName the candidate String
   * @return true if the candidate String is a valid case name
   */
  public static boolean isValidName(String caseName) {
    return !(caseName.contains("\\")
        || caseName.contains("/")
        || caseName.contains(":")
        || caseName.contains("*")
        || caseName.contains("?")
        || caseName.contains("\"")
        || caseName.contains("<")
        || caseName.contains(">")
        || caseName.contains("|"));
  }

  private static void clearTempFolder() {
    File tempFolder = new File(currentCase.getTempDirectory());
    if (tempFolder.isDirectory()) {
      File[] files = tempFolder.listFiles();
      if (files.length > 0) {
        for (int i = 0; i < files.length; i++) {
          if (files[i].isDirectory()) {
            deleteCaseDirectory(files[i]);
          } else {
            files[i].delete();
          }
        }
      }
    }
  }

  /**
   * Check for existence of certain case sub dirs and create them if needed.
   *
   * @param openedCase
   */
  private static void checkSubFolders(Case openedCase) {
    String modulesOutputDir = openedCase.getModulesOutputDirAbsPath();
    File modulesOutputDirF = new File(modulesOutputDir);
    if (!modulesOutputDirF.exists()) {
      logger.log(Level.INFO, "Creating modules output dir for the case.");

      try {
        if (!modulesOutputDirF.mkdir()) {
          logger.log(
              Level.SEVERE,
              "Error creating modules output dir for the case, dir: " + modulesOutputDir);
        }
      } catch (SecurityException e) {
        logger.log(
            Level.SEVERE,
            "Error creating modules output dir for the case, dir: " + modulesOutputDir,
            e);
      }
    }
  }

  // case change helper
  private static void doCaseChange(Case toChangeTo) {
    logger.log(Level.INFO, "Changing Case to: " + toChangeTo);
    if (toChangeTo != null) { // new case is open

      // clear the temp folder when the case is created / opened
      Case.clearTempFolder();
      checkSubFolders(toChangeTo);

      // enable these menus
      CallableSystemAction.get(AddImageAction.class).setEnabled(true);
      CallableSystemAction.get(CaseCloseAction.class).setEnabled(true);
      CallableSystemAction.get(CasePropertiesAction.class).setEnabled(true);
      CallableSystemAction.get(CaseDeleteAction.class).setEnabled(true); // Delete Case menu

      if (toChangeTo.getRootObjectsCount() > 0) {
        // open all top components
        CoreComponentControl.openCoreWindows();
      } else {
        // close all top components
        CoreComponentControl.closeCoreWindows();
      }
    } else { // case is closed
      // close all top components first
      CoreComponentControl.closeCoreWindows();

      // disable these menus
      CallableSystemAction.get(AddImageAction.class).setEnabled(false); // Add Image menu
      CallableSystemAction.get(CaseCloseAction.class).setEnabled(false); // Case Close menu
      CallableSystemAction.get(CasePropertiesAction.class)
          .setEnabled(false); // Case Properties menu
      CallableSystemAction.get(CaseDeleteAction.class).setEnabled(false); // Delete Case menu

      // clear pending notifications
      MessageNotifyUtil.Notify.clear();

      Frame f = WindowManager.getDefault().getMainWindow();
      f.setTitle(Case.getAppName()); // set the window name to just application name

      // try to force gc to happen
      System.gc();
      System.gc();
    }

    // log memory usage after case changed
    logger.log(Level.INFO, PlatformUtil.getAllMemUsageInfo());
  }

  // case name change helper
  private static void doCaseNameChange(String newCaseName) {
    // update case name
    if (!newCaseName.equals("")) {
      Frame f = WindowManager.getDefault().getMainWindow();
      f.setTitle(newCaseName + " - " + Case.getAppName()); // set the window name to the new value
    }
  }

  // delete image helper
  private void doDeleteImage() {
    // no more image left in this case
    if (currentCase.getRootObjectsCount() == 0) {
      // close all top components
      CoreComponentControl.closeCoreWindows();
    }
  }
}
Esempio n. 2
0
/** Performs a regular expression query to the SOLR/Lucene instance. */
final class TermComponentQuery implements KeywordSearchQuery {

  private static final Logger LOGGER = Logger.getLogger(TermComponentQuery.class.getName());
  private static final boolean DEBUG = Version.Type.DEVELOPMENT.equals(Version.getBuildType());

  private static final String MODULE_NAME = KeywordSearchModuleFactory.getModuleName();
  private static final BlackboardAttribute.Type KEYWORD_SEARCH_DOCUMENT_ID =
      new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_DOCUMENT_ID);

  // TODO: move these regex and the luhn check to a new class, something like:
  // CreditCardNumberValidator
  /*
   * Track 2 is numeric plus six punctuation symbolls :;<=>?
   *
   * This regex matches 12-19 digit ccns embeded in a track 2 formated string.
   * This regex matches (and extracts groups) even if the entire track is not
   * present as long as the part that is conforms to the track format.
   *
   */
  private static final Pattern TRACK2_PATTERN =
      Pattern.compile(
          "[:;<=>?]?" // (optional)start sentinel //NON-NLS
              + "(?<accountNumber>[3456]([ -]?\\d){11,18})" // 12-19 digits, with possible single
                                                            // spaces or dashes in between. first
                                                            // digit is 3,4,5, or 6 //NON-NLS
              + "(?:[:;<=>?]" // separator //NON-NLS
              + "(?:(?<expiration>\\d{4})" // 4 digit expiration date YYMM //NON-NLS
              + "(?:(?<serviceCode>\\d{3})" // 3 digit service code //NON-NLS
              + "(?:(?<discretionary>[^:;<=>?]*)" // discretionary data, not containing punctuation
                                                  // marks //NON-NLS
              + "(?:[:;<=>?]" // end sentinel //NON-NLS
              + "(?<LRC>.)" // longitudinal redundancy check //NON-NLS
              + "?)?)?)?)?)?"); // close nested optional groups //NON-NLS

  /*
   * Track 1 is alphanumeric.
   *
   * This regex matches 12-19 digit ccns embeded in a track 1 formated string.
   * This regex matches (and extracts groups) even if the entire track is not
   * present as long as the part that is conforms to the track format.
   */
  private static final Pattern TRACK1_PATTERN =
      Pattern.compile(
          "(?:" // begin nested optinal group //NON-NLS
              + "%?" // optional start sentinal: % //NON-NLS
              + "B)?" // format code  //NON-NLS
              + "(?<accountNumber>[3456]([ -]?\\d){11,18})" // 12-19 digits, with possible single
                                                            // spaces or dashes in between. first
                                                            // digit is 3,4,5, or 6 //NON-NLS
              + "\\^" // separator //NON-NLS
              + "(?<name>[^^]{2,26})" // 2-26 charachter name, not containing ^ //NON-NLS
              + "(?:\\^" // separator //NON-NLS
              + "(?:(?:\\^|(?<expiration>\\d{4}))" // separator or 4 digit expiration YYMM //NON-NLS
              + "(?:(?:\\^|(?<serviceCode>\\d{3}))" // separator or 3 digit service code //NON-NLS
              + "(?:(?<discretionary>[^?]*)" // discretionary data not containing separator
                                             // //NON-NLS
              + "(?:\\?" // end sentinal: ? //NON-NLS
              + "(?<LRC>.)" // longitudinal redundancy check //NON-NLS
              + "?)?)?)?)?)?"); // close nested optional groups //NON-NLS
  private static final Pattern CCN_PATTERN =
      Pattern.compile(
          "(?<ccn>[3456]([ -]?\\d){11,18})"); // 12-19 digits, with possible single spaces or dashes
                                              // in between. first digit is 3,4,5, or 6 //NON-NLS
  private static final LuhnCheckDigit LUHN_CHECK = new LuhnCheckDigit();

  // corresponds to field in Solr schema, analyzed with white-space tokenizer only
  private static final String TERMS_SEARCH_FIELD = Server.Schema.CONTENT_WS.toString();
  private static final String TERMS_HANDLER = "/terms"; // NON-NLS
  private static final int TERMS_TIMEOUT = 90 * 1000; // in ms
  private static final String CASE_INSENSITIVE = "case_insensitive"; // NON-NLS
  private static final int MAX_TERMS_RESULTS = 20000;

  private String escapedQuery;
  private final KeywordList keywordList;
  private final Keyword keyword;
  private boolean isEscaped;
  private final List<KeywordQueryFilter> filters = new ArrayList<>();

  TermComponentQuery(KeywordList keywordList, Keyword keyword) {
    this.keyword = keyword;

    this.keywordList = keywordList;
    this.escapedQuery = keyword.getQuery();
  }

  @Override
  public void addFilter(KeywordQueryFilter filter) {
    this.filters.add(filter);
  }

  /**
   * @param field
   * @deprecated This method is unused and no-op
   */
  @Override
  @Deprecated
  public void setField(String field) {}

  @Override
  public void setSubstringQuery() {
    escapedQuery = ".*" + escapedQuery + ".*";
  }

  @Override
  public void escape() {
    escapedQuery = Pattern.quote(keyword.getQuery());
    isEscaped = true;
  }

  @Override
  public boolean validate() {
    if (escapedQuery.isEmpty()) {
      return false;
    }

    try {
      Pattern.compile(escapedQuery);
      return true;
    } catch (IllegalArgumentException ex) {
      return false;
    }
  }

  @Override
  public boolean isEscaped() {
    return isEscaped;
  }

  @Override
  public boolean isLiteral() {
    return false;
  }

  @Override
  public String getEscapedQueryString() {
    return this.escapedQuery;
  }

  @Override
  public String getQueryString() {
    return keyword.getQuery();
  }

  @Override
  public KeywordCachedArtifact writeSingleFileHitsToBlackBoard(
      String termHit, KeywordHit hit, String snippet, String listName) {
    BlackboardArtifact newArtifact;

    Collection<BlackboardAttribute> attributes = new ArrayList<>();
    if (keyword.getType() == ATTRIBUTE_TYPE.TSK_CARD_NUMBER) {
      attributes.add(
          new BlackboardAttribute(
              ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE, MODULE_NAME, Account.Type.CREDIT_CARD.name()));

      Map<BlackboardAttribute.Type, BlackboardAttribute> parsedTrackAttributeMap = new HashMap<>();

      // try to match it against the track 1 regex
      Matcher matcher = TRACK1_PATTERN.matcher(hit.getSnippet());
      if (matcher.find()) {
        parseTrack1Data(parsedTrackAttributeMap, matcher);
      }

      // then try to match it against the track 2 regex
      matcher = TRACK2_PATTERN.matcher(hit.getSnippet());
      if (matcher.find()) {
        parseTrack2Data(parsedTrackAttributeMap, matcher);
      }

      // if we couldn't parse the CCN abort this artifact
      final BlackboardAttribute ccnAttribute =
          parsedTrackAttributeMap.get(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_CARD_NUMBER));
      if (ccnAttribute == null || StringUtils.isBlank(ccnAttribute.getValueString())) {
        if (hit.isArtifactHit()) {
          LOGGER.log(
              Level.SEVERE,
              String.format(
                  "Failed to parse credit card account number for artifact keyword hit: term = %s, snippet = '%s', artifact id = %d",
                  termHit, hit.getSnippet(), hit.getArtifact().getArtifactID()));
        } else {
          LOGGER.log(
              Level.SEVERE,
              String.format(
                  "Failed to parse credit card account number for content keyword hit: term = %s, snippet = '%s', object id = %d",
                  termHit, hit.getSnippet(), hit.getContent().getId()));
        }
        return null;
      }

      attributes.addAll(parsedTrackAttributeMap.values());

      // look up the bank name, schem, etc from the BIN
      final int bin = Integer.parseInt(ccnAttribute.getValueString().substring(0, 8));
      CreditCards.BankIdentificationNumber binInfo = CreditCards.getBINInfo(bin);
      if (binInfo != null) {
        binInfo
            .getScheme()
            .ifPresent(
                scheme ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_CARD_SCHEME, MODULE_NAME, scheme)));
        binInfo
            .getCardType()
            .ifPresent(
                cardType ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_CARD_TYPE, MODULE_NAME, cardType)));
        binInfo
            .getBrand()
            .ifPresent(
                brand ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_BRAND_NAME, MODULE_NAME, brand)));
        binInfo
            .getBankName()
            .ifPresent(
                bankName ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_BANK_NAME, MODULE_NAME, bankName)));
        binInfo
            .getBankPhoneNumber()
            .ifPresent(
                phoneNumber ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, MODULE_NAME, phoneNumber)));
        binInfo
            .getBankURL()
            .ifPresent(
                url ->
                    attributes.add(
                        new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL, MODULE_NAME, url)));
        binInfo
            .getCountry()
            .ifPresent(
                country ->
                    attributes.add(
                        new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNTRY, MODULE_NAME, country)));
        binInfo
            .getBankCity()
            .ifPresent(
                city ->
                    attributes.add(
                        new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_CITY, MODULE_NAME, city)));
      }

      /* if the hit is from unused or unalocated blocks, record the
       * KEYWORD_SEARCH_DOCUMENT_ID, so we can show just that chunk in the
       * UI
       */
      if (hit.getContent() instanceof AbstractFile) {
        AbstractFile file = (AbstractFile) hit.getContent();
        if (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS
            || file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) {
          attributes.add(
              new BlackboardAttribute(
                  KEYWORD_SEARCH_DOCUMENT_ID, MODULE_NAME, hit.getSolrDocumentId()));
        }
      }

      // make account artifact
      try {
        newArtifact = hit.getContent().newArtifact(ARTIFACT_TYPE.TSK_ACCOUNT);
      } catch (TskCoreException tskCoreException) {
        LOGGER.log(
            Level.SEVERE, "Error adding bb artifact for account", tskCoreException); // NON-NLS
        return null;
      }
    } else {

      // regex match
      attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD, MODULE_NAME, termHit));
      // regex keyword
      attributes.add(
          new BlackboardAttribute(
              ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP, MODULE_NAME, keyword.getQuery()));

      // make keyword hit artifact
      try {
        newArtifact = hit.getContent().newArtifact(ARTIFACT_TYPE.TSK_KEYWORD_HIT);

      } catch (TskCoreException tskCoreException) {
        LOGGER.log(
            Level.SEVERE, "Error adding bb artifact for keyword hit", tskCoreException); // NON-NLS
        return null;
      }
    }
    if (StringUtils.isNotBlank(listName)) {
      attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, MODULE_NAME, listName));
    }
    // preview
    if (snippet != null) {
      attributes.add(
          new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW, MODULE_NAME, snippet));
    }

    if (hit.isArtifactHit()) {
      attributes.add(
          new BlackboardAttribute(
              ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT,
              MODULE_NAME,
              hit.getArtifact().getArtifactID()));
    }

    try {
      // TODO: do we still/really need this KeywordCachedArtifact class?
      newArtifact.addAttributes(attributes);
      KeywordCachedArtifact writeResult = new KeywordCachedArtifact(newArtifact);
      writeResult.add(attributes);
      return writeResult;
    } catch (TskCoreException e) {
      LOGGER.log(
          Level.SEVERE, "Error adding bb attributes for terms search artifact", e); // NON-NLS
      return null;
    }
  }

  @Override
  public QueryResults performQuery() throws NoOpenCoreException {
    /*
     * Execute the regex query to get a list of terms that match the regex.
     * Note that the field that is being searched is tokenized based on
     * whitespace.
     */
    // create the query
    final SolrQuery q = new SolrQuery();
    q.setRequestHandler(TERMS_HANDLER);
    q.setTerms(true);
    q.setTermsRegexFlag(CASE_INSENSITIVE);
    q.setTermsRegex(escapedQuery);
    q.addTermsField(TERMS_SEARCH_FIELD);
    q.setTimeAllowed(TERMS_TIMEOUT);
    q.setShowDebugInfo(DEBUG);
    q.setTermsLimit(MAX_TERMS_RESULTS);
    LOGGER.log(Level.INFO, "Query: {0}", q.toString()); // NON-NLS

    // execute the query
    List<Term> terms = null;
    try {
      terms = KeywordSearch.getServer().queryTerms(q).getTerms(TERMS_SEARCH_FIELD);
    } catch (KeywordSearchModuleException ex) {
      LOGGER.log(
          Level.SEVERE,
          "Error executing the regex terms query: " + keyword.getQuery(),
          ex); // NON-NLS
      // TODO: this is almost certainly wrong and guaranteed to throw a NPE at some point!!!!
    }

    /*
     * For each term that matched the regex, query for full set of document
     * hits for that term.
     */
    QueryResults results = new QueryResults(this, keywordList);
    int resultSize = 0;

    for (Term term : terms) {
      final String termStr = KeywordSearchUtil.escapeLuceneQuery(term.getTerm());

      if (keyword.getType() == ATTRIBUTE_TYPE.TSK_CARD_NUMBER) {
        // If the keyword is a credit card number, pass it through luhn validator
        Matcher matcher = CCN_PATTERN.matcher(term.getTerm());
        matcher.find();
        final String ccn = CharMatcher.anyOf(" -").removeFrom(matcher.group("ccn"));
        if (false == LUHN_CHECK.isValid(ccn)) {
          continue; // if the hit does not pass the luhn check, skip it.
        }
      }

      /*
       * Note: we can't set filter query on terms query but setting filter
       * query on fileResults query will yield the same result
       */
      LuceneQuery filesQuery = new LuceneQuery(keywordList, new Keyword(termStr, true));
      filters.forEach(filesQuery::addFilter);

      try {
        QueryResults fileQueryResults = filesQuery.performQuery();
        Set<KeywordHit> filesResults = new HashSet<>();
        for (Keyword key : fileQueryResults.getKeywords()) { // flatten results into a single list
          List<KeywordHit> keyRes = fileQueryResults.getResults(key);
          resultSize += keyRes.size();
          filesResults.addAll(keyRes);
        }
        results.addResult(new Keyword(term.getTerm(), false), new ArrayList<>(filesResults));
      } catch (NoOpenCoreException | RuntimeException e) {
        LOGGER.log(Level.WARNING, "Error executing Solr query,", e); // NON-NLS
        throw e;
      }
    }

    // TODO limit how many results we store, not to hit memory limits
    LOGGER.log(Level.INFO, "Regex # results: {0}", resultSize); // NON-NLS

    return results;
  }

  @Override
  public KeywordList getKeywordList() {
    return keywordList;
  }

  /**
   * Add an attribute of the the given type to the given artifact with the value taken from the
   * matcher. If an attribute of the given type already exists on the artifact or if the value is
   * null, no attribute is added.
   *
   * @param attributeMap
   * @param attrType
   * @param groupName
   * @param matcher *
   */
  private static void addAttributeIfNotAlreadyCaptured(
      Map<BlackboardAttribute.Type, BlackboardAttribute> attributeMap,
      ATTRIBUTE_TYPE attrType,
      String groupName,
      Matcher matcher) {
    BlackboardAttribute.Type type = new BlackboardAttribute.Type(attrType);

    attributeMap.computeIfAbsent(
        type,
        (BlackboardAttribute.Type t) -> {
          String value = matcher.group(groupName);
          if (attrType.equals(ATTRIBUTE_TYPE.TSK_CARD_NUMBER)) {
            value = CharMatcher.anyOf(" -").removeFrom(value);
          }
          if (StringUtils.isNotBlank(value)) {
            return new BlackboardAttribute(attrType, MODULE_NAME, value);
          }
          return null;
        });
  }

  /**
   * Parse the track 2 data from a KeywordHit and add it to the given artifact.
   *
   * @param attributeMAp
   * @param matcher
   */
  private static void parseTrack2Data(
      Map<BlackboardAttribute.Type, BlackboardAttribute> attributeMAp, Matcher matcher) {
    // try to add all the attrributes common to track 1 and 2
    addAttributeIfNotAlreadyCaptured(
        attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_NUMBER, "accountNumber", matcher);
    addAttributeIfNotAlreadyCaptured(
        attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_EXPIRATION, "expiration", matcher);
    addAttributeIfNotAlreadyCaptured(
        attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_SERVICE_CODE, "serviceCode", matcher);
    addAttributeIfNotAlreadyCaptured(
        attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_DISCRETIONARY, "discretionary", matcher);
    addAttributeIfNotAlreadyCaptured(attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_LRC, "LRC", matcher);
  }

  /**
   * Parse the track 1 data from a KeywordHit and add it to the given artifact.
   *
   * @param attributeMap
   * @param matcher
   */
  private static void parseTrack1Data(
      Map<BlackboardAttribute.Type, BlackboardAttribute> attributeMap, Matcher matcher) {
    // track 1 has all the fields present in track 2
    parseTrack2Data(attributeMap, matcher);
    // plus it also has the account holders name
    addAttributeIfNotAlreadyCaptured(attributeMap, ATTRIBUTE_TYPE.TSK_NAME_PERSON, "name", matcher);
  }
}