/** * 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(); } } }
/** 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); } }