diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java index b0f288673b..d3a0e61ecf 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java @@ -382,6 +382,31 @@ public class CentralRepoDbManager { CentralRepoDbUtil.setUseCentralRepo(true); saveNewCentralRepo(); } + + /** + * Set up a PostgresDb using the settings for the given database choice + * enum. + * + * @param choice Type of postgres DB to set up + * @throws CentralRepoException + */ + public void setupPostgresDb(CentralRepoDbChoice choice) throws CentralRepoException { + selectedDbChoice = choice; + DatabaseTestResult curStatus = testStatus(); + if (curStatus == DatabaseTestResult.DB_DOES_NOT_EXIST) { + createDb(); + curStatus = testStatus(); + } + + // the only successful setup status is tested ok + if (curStatus != DatabaseTestResult.TESTED_OK) { + throw new CentralRepoException("Unable to successfully create postgres database. Test failed with: " + curStatus); + } + + // if successfully got here, then save the settings + CentralRepoDbUtil.setUseCentralRepo(true); + saveNewCentralRepo(); + } /** * This method returns if changes to the central repository configuration diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java index 165d4ca6e0..dfb855a0b2 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java @@ -121,10 +121,12 @@ public final class PostgresCentralRepoSettings implements CentralRepoDbConnectiv * @return */ String getConnectionURL(boolean usePostgresDb) { - StringBuilder url = new StringBuilder(); - url.append(getJDBCBaseURI()); - url.append(getHost()); - url.append("/"); // NON-NLS + StringBuilder url = new StringBuilder() + .append(getJDBCBaseURI()) + .append(getHost()) + .append(":") // NON-NLS + .append(getPort()) + .append("/"); // NON-NLS if (usePostgresDb) { url.append("postgres"); // NON-NLS } else { @@ -153,7 +155,7 @@ public final class PostgresCentralRepoSettings implements CentralRepoDbConnectiv } catch (ClassNotFoundException | SQLException ex) { // TODO: Determine why a connection failure (ConnectionException) re-throws // the SQLException and does not print this log message? - LOGGER.log(Level.SEVERE, "Failed to acquire ephemeral connection to postgresql."); // NON-NLS + LOGGER.log(Level.SEVERE, "Failed to acquire ephemeral connection to postgresql.", ex); // NON-NLS conn = null; } return conn; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED index d4cca6c407..e95a759c4f 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED @@ -1,4 +1,10 @@ caseeventlistener.evidencetag=Evidence +CentralRepositoryNotificationDialog.bulletHeader=This data is used to: +CentralRepositoryNotificationDialog.bulletOne=Ignore common items (files, domains, and accounts) +CentralRepositoryNotificationDialog.bulletThree=Create personas that group accounts +CentralRepositoryNotificationDialog.bulletTwo=Identify where an item was previously seen +CentralRepositoryNotificationDialog.finalRemarks=To limit what is stored, use the Central Repository options panel. +CentralRepositoryNotificationDialog.header=Autopsy stores data about each case in its Central Repository. IngestEventsListener.ingestmodule.name=Central Repository IngestEventsListener.prevCaseComment.text=Previous Case: # {0} - typeName @@ -7,6 +13,3 @@ IngestEventsListener.prevCount.text=Number of previous {0}: {1} IngestEventsListener.prevExists.text=Previously Seen Devices (Central Repository) IngestEventsListener.prevTaggedSet.text=Previously Tagged As Notable (Central Repository) Installer.centralRepoUpgradeFailed.title=Central repository disabled -Installer.initialCreateSqlite.messageDesc=It will store information about all hashes and identifiers that you process. You can use this to ignore previously seen files and make connections between cases. -Installer.initialCreateSqlite.messageHeader=The Central Repository is not enabled. Would you like to enable it? -Installer.initialCreateSqlite.title=Enable Central Repository? diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/CentralRepositoryNotificationDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/CentralRepositoryNotificationDialog.java new file mode 100755 index 0000000000..883d979c90 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/CentralRepositoryNotificationDialog.java @@ -0,0 +1,73 @@ +/* + * Central Repository + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.eventlisteners; + +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.core.RuntimeProperties; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.coreutils.Version; + +/** + * Notifies new installations or old upgrades that the central repository will + * be enabled by default. + */ +public class CentralRepositoryNotificationDialog { + + /** + * This dialog should display iff the mode is RELEASE and the + * application is running with a GUI. + */ + static boolean shouldDisplay() { + return Version.getBuildType() == Version.Type.RELEASE + && RuntimeProperties.runningWithGUI(); + } + + /** + * Displays an informational modal dialog to the user, which is dismissed by + * pressing 'OK'. + */ + @NbBundle.Messages({ + "CentralRepositoryNotificationDialog.header=Autopsy stores data about each case in its Central Repository.", + "CentralRepositoryNotificationDialog.bulletHeader=This data is used to:", + "CentralRepositoryNotificationDialog.bulletOne=Ignore common items (files, domains, and accounts)", + "CentralRepositoryNotificationDialog.bulletTwo=Identify where an item was previously seen", + "CentralRepositoryNotificationDialog.bulletThree=Create personas that group accounts", + "CentralRepositoryNotificationDialog.finalRemarks=To limit what is stored, use the Central Repository options panel." + }) + static void display() { + assert shouldDisplay(); + + MessageNotifyUtil.Message.info( + "" + + "" + + "
" + + "

" + Bundle.CentralRepositoryNotificationDialog_header() + "

" + + "

" + Bundle.CentralRepositoryNotificationDialog_bulletHeader() + "

" + + "
    " + + "
  • " + Bundle.CentralRepositoryNotificationDialog_bulletOne() + "
  • " + + "
  • " + Bundle.CentralRepositoryNotificationDialog_bulletTwo() + "
  • " + + "
  • " + Bundle.CentralRepositoryNotificationDialog_bulletThree() + "
  • " + + "
" + + "

" + Bundle.CentralRepositoryNotificationDialog_finalRemarks() + "

" + + "
" + + "" + + "" + ); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java index d4f0253cd2..b2ef0d437e 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java @@ -25,14 +25,13 @@ import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.openide.modules.ModuleInstall; import org.openide.util.NbBundle; -import org.openide.windows.WindowManager; -import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbChoice; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbManager; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.core.RuntimeProperties; +import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ModuleSettings; -import org.sleuthkit.autopsy.coreutils.Version; /** * Adds/removes application event listeners responsible for adding data to the @@ -81,19 +80,10 @@ public class Installer extends ModuleInstall { * the org.sleuthkit.autopsy.core package when the already installed * Autopsy-Core module is restored (during application startup). */ - @NbBundle.Messages({ - "Installer.initialCreateSqlite.title=Enable Central Repository?", - "Installer.initialCreateSqlite.messageHeader=The Central Repository is not enabled. Would you like to enable it?", - "Installer.initialCreateSqlite.messageDesc=It will store information about all hashes and identifiers that you process. " - + "You can use this to ignore previously seen files and make connections between cases." - }) @Override public void restored() { addApplicationEventListeners(); - - if (Version.getBuildType() == Version.Type.RELEASE) { - setupDefaultCentralRepository(); - } + setupDefaultCentralRepository(); } /** @@ -107,9 +97,9 @@ public class Installer extends ModuleInstall { /** * Checks if the central repository has been set up and configured. If not, - * either offers to perform set up (running with a GUI) or does the set up - * unconditionally (not running with a GUI, e.g., in an automated ingest - * node). + * does the set up unconditionally. If the application is running with a + * GUI, a notification will be displayed to the user if the mode is RELEASE + * (in other words, developers are exempt from seeing the notification). */ private void setupDefaultCentralRepository() { Map centralRepoSettings = ModuleSettings.getConfigSettings("CentralRepository"); @@ -127,62 +117,30 @@ public class Installer extends ModuleInstall { ModuleSettings.setConfigSetting("CentralRepository", "initialized", "true"); } } - - // if central repository hasn't been previously initialized, initialize it - if (!initialized) { - // if running with a GUI, prompt the user - if (RuntimeProperties.runningWithGUI()) { - try { - SwingUtilities.invokeAndWait(() -> { - try { - String dialogText - = "" - + "
" - + "

" + NbBundle.getMessage(this.getClass(), "Installer.initialCreateSqlite.messageHeader") + "

" - + "

" + NbBundle.getMessage(this.getClass(), "Installer.initialCreateSqlite.messageDesc") + "

" - + "
" - + ""; - - if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(), - dialogText, - NbBundle.getMessage(this.getClass(), "Installer.initialCreateSqlite.title"), - JOptionPane.YES_NO_OPTION)) { - - setupDefaultSqliteCentralRepo(); - } - } catch (CentralRepoException ex) { - logger.log(Level.SEVERE, "There was an error while initializing the central repository database", ex); - - doMessageBoxIfRunningInGUI(ex); - } - }); - } catch (InterruptedException | InvocationTargetException ex) { - logger.log(Level.SEVERE, "There was an error while running the swing utility invoke later while creating the central repository database", ex); - } - } // if no GUI, just initialize - else { - try { - setupDefaultSqliteCentralRepo(); - } catch (CentralRepoException ex) { - logger.log(Level.SEVERE, "There was an error while initializing the central repository database", ex); - - doMessageBoxIfRunningInGUI(ex); - } - } - - ModuleSettings.setConfigSetting("CentralRepository", "initialized", "true"); + + if(initialized) { + return; // Nothing to do } - } - /** - * Sets up a default single-user SQLite central repository. - * - * @throws CentralRepoException If there is an error setting up teh central - * repository. - */ - private void setupDefaultSqliteCentralRepo() throws CentralRepoException { - CentralRepoDbManager manager = new CentralRepoDbManager(); - manager.setupDefaultSqliteDb(); + if (CentralRepositoryNotificationDialog.shouldDisplay()) { + CentralRepositoryNotificationDialog.display(); + } + + try { + CentralRepoDbManager manager = new CentralRepoDbManager(); + if (UserPreferences.getIsMultiUserModeEnabled()) { + // Set up using existing multi-user settings. + manager.setupPostgresDb(CentralRepoDbChoice.POSTGRESQL_MULTIUSER); + } else { + manager.setupDefaultSqliteDb(); + } + } catch (CentralRepoException ex) { + logger.log(Level.SEVERE, "There was an error while initializing the central repository database", ex); + + doMessageBoxIfRunningInGUI(ex); + } + + ModuleSettings.setConfigSetting("CentralRepository", "initialized", "true"); } /** diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java index 6c4601af88..651f5fe758 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java @@ -28,10 +28,11 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; +import java.util.stream.Collectors; +import org.apache.commons.lang.StringUtils; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; @@ -93,16 +94,56 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { return ARTIFACT_UPDATE_TYPE_IDS; } + /** + * Removes fileDetails entries with redundant paths, sorts by date + * descending and limits to the limit provided. + * + * @param fileDetails The file details list. + * @param limit The maximum number of entries to return. + * @return The sorted limited list with unique paths. + */ + private List getSortedLimited(List fileDetails, int limit) { + Map fileDetailsMap = fileDetails.stream() + .filter(details -> details != null) + .collect(Collectors.toMap( + d -> d.getPath().toUpperCase(), + d -> d, + (d1, d2) -> Long.compare(d1.getDateAsLong(), d2.getDateAsLong()) > 0 ? d1 : d2)); + + return fileDetailsMap.values().stream() + .sorted((a, b) -> -Long.compare(a.getDateAsLong(), b.getDateAsLong())) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * Returns a RecentFileDetails object as derived from the recent document + * artifact or null if no appropriate object can be made. + * + * @param artifact The artifact. + * @return The derived object or null if artifact is invalid. + */ + private RecentFileDetails getRecentlyOpenedDocument(BlackboardArtifact artifact) { + String path = DataSourceInfoUtilities.getStringOrNull(artifact, PATH_ATT); + Long lastOpened = DataSourceInfoUtilities.getLongOrNull(artifact, DATETIME_ATT); + + if (StringUtils.isBlank(path) || lastOpened == null || lastOpened == 0) { + return null; + } else { + return new RecentFileDetails(path, lastOpened); + } + } + /** * Return a list of the most recently opened documents based on the * TSK_RECENT_OBJECT artifact. * * @param dataSource The data source to query. - * @param maxCount The maximum number of results to return, pass 0 to get - * a list of all results. + * @param maxCount The maximum number of results to return, pass 0 to get a + * list of all results. * * @return A list RecentFileDetails representing the most recently opened - * documents or an empty list if none were found. + * documents or an empty list if none were found. * * @throws SleuthkitCaseProviderException * @throws TskCoreException @@ -112,36 +153,45 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { return Collections.emptyList(); } - List artifactList - = DataSourceInfoUtilities.getArtifacts(provider.get(), - new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_RECENT_OBJECT), - dataSource, - DATETIME_ATT, - DataSourceInfoUtilities.SortOrder.DESCENDING, - maxCount); + throwOnNonPositiveCount(maxCount); - List fileDetails = new ArrayList<>(); - for (BlackboardArtifact artifact : artifactList) { - Long accessedTime = null; - String path = ""; + List details = provider.get().getBlackboard() + .getArtifacts(ARTIFACT_TYPE.TSK_RECENT_OBJECT.getTypeID(), dataSource.getId()).stream() + .map(art -> getRecentlyOpenedDocument(art)) + .filter(d -> d != null) + .collect(Collectors.toList()); - // Get all the attributes in one call. - List attributeList = artifact.getAttributes(); - for (BlackboardAttribute attribute : attributeList) { + return getSortedLimited(details, maxCount); + } - if (attribute.getAttributeType().equals(DATETIME_ATT)) { - accessedTime = attribute.getValueLong(); - } else if (attribute.getAttributeType().equals(PATH_ATT)) { - path = attribute.getValueString(); - } - } + /** + * Returns a RecentDownloadDetails object as derived from the recent + * download artifact or null if no appropriate object can be made. + * + * @param artifact The artifact. + * @return The derived object or null if artifact is invalid. + */ + private RecentDownloadDetails getRecentDownload(BlackboardArtifact artifact) { + Long accessedTime = DataSourceInfoUtilities.getLongOrNull(artifact, DATETIME_ACCESSED_ATT); + String domain = DataSourceInfoUtilities.getStringOrNull(artifact, DOMAIN_ATT); + String path = DataSourceInfoUtilities.getStringOrNull(artifact, PATH_ATT); - if (accessedTime != null && accessedTime != 0) { - fileDetails.add(new RecentFileDetails(path, accessedTime)); - } + if (StringUtils.isBlank(path) || accessedTime == null || accessedTime == 0) { + return null; + } else { + return new RecentDownloadDetails(path, accessedTime, domain); } + } - return fileDetails; + /** + * Throws an IllegalArgumentException if count is less than 1. + * + * @param count The count. + */ + private void throwOnNonPositiveCount(int count) { + if (count < 1) { + throw new IllegalArgumentException("Invalid count: value must be greater than 0."); + } } /** @@ -149,11 +199,11 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { * artifact TSK_DATETIME_ACCESSED attribute. * * @param dataSource Data source to query. - * @param maxCount Maximum number of results to return, passing 0 will - * return all results. + * @param maxCount Maximum number of results to return, passing 0 will + * return all results. * * @return A list of RecentFileDetails objects or empty list if none were - * found. + * found. * * @throws TskCoreException * @throws SleuthkitCaseProviderException @@ -163,46 +213,23 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { return Collections.emptyList(); } - List artifactList - = DataSourceInfoUtilities.getArtifacts(provider.get(), - new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_DOWNLOAD), - dataSource, - DATETIME_ACCESSED_ATT, - DataSourceInfoUtilities.SortOrder.DESCENDING, - maxCount); + throwOnNonPositiveCount(maxCount); - List fileDetails = new ArrayList<>(); - for (BlackboardArtifact artifact : artifactList) { - // Get all the attributes in one call. - Long accessedTime = null; - String domain = ""; - String path = ""; + List details = provider.get().getBlackboard() + .getArtifacts(ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID(), dataSource.getId()).stream() + .map(art -> getRecentDownload(art)) + .filter(d -> d != null) + .collect(Collectors.toList()); - List attributeList = artifact.getAttributes(); - for (BlackboardAttribute attribute : attributeList) { - - if (attribute.getAttributeType().equals(DATETIME_ACCESSED_ATT)) { - accessedTime = attribute.getValueLong(); - } else if (attribute.getAttributeType().equals(DOMAIN_ATT)) { - domain = attribute.getValueString(); - } else if (attribute.getAttributeType().equals(PATH_ATT)) { - path = attribute.getValueString(); - } - } - if (accessedTime != null && accessedTime != 0L) { - fileDetails.add(new RecentDownloadDetails(path, accessedTime, domain)); - } - } - - return fileDetails; + return getSortedLimited(details, maxCount); } /** * Returns a list of the most recent message attachments. * * @param dataSource Data source to query. - * @param maxCount Maximum number of results to return, passing 0 will - * return all results. + * @param maxCount Maximum number of results to return, passing 0 will + * return all results. * * @return A list of RecentFileDetails of the most recent attachments. * @@ -214,115 +241,72 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { return Collections.emptyList(); } - if (maxCount < 0) { - throw new IllegalArgumentException("Invalid maxCount passed to getRecentAttachments, value must be equal to or greater than 0"); + throwOnNonPositiveCount(maxCount); + + SleuthkitCase skCase = provider.get(); + + List associatedArtifacts = skCase.getBlackboard() + .getArtifacts(ASSOCATED_OBJ_ART.getTypeID(), dataSource.getId()); + + List details = new ArrayList<>(); + for (BlackboardArtifact artifact : associatedArtifacts) { + RecentAttachmentDetails thisDetails = getRecentAttachment(artifact, skCase); + + if (thisDetails != null) { + details.add(thisDetails); + } } - return createListFromMap(buildAttachmentMap(dataSource), maxCount); + return getSortedLimited(details, maxCount); } /** - * Build a map of all of the message attachment sorted in date order. + * Creates a RecentAttachmentDetails object from the associated object + * artifact or null if no RecentAttachmentDetails object can be derived. * - * @param dataSource Data source to query. - * - * @return Returns a SortedMap of details objects returned in descending - * order. - * - * @throws SleuthkitCaseProviderException + * @param artifact The associated object artifact. + * @param skCase The current case. + * @return The derived object or null. * @throws TskCoreException */ - private SortedMap> buildAttachmentMap(DataSource dataSource) throws SleuthkitCaseProviderException, TskCoreException { - SleuthkitCase skCase = provider.get(); - TreeMap> sortedMap = new TreeMap<>(); - - List associatedArtifacts = skCase.getBlackboard().getArtifacts(ASSOCATED_OBJ_ART.getTypeID(), dataSource.getId()); - for (BlackboardArtifact artifact : associatedArtifacts) { - BlackboardAttribute attribute = artifact.getAttribute(ASSOCATED_ATT); - if (attribute == null) { - continue; - } - - BlackboardArtifact messageArtifact = skCase.getBlackboardArtifact(attribute.getValueLong()); - if (messageArtifact != null && isMessageArtifact(messageArtifact)) { - Content content = artifact.getParent(); - if (content instanceof AbstractFile) { - String sender; - Long date = null; - String path; - - BlackboardAttribute senderAttribute = messageArtifact.getAttribute(EMAIL_FROM_ATT); - if (senderAttribute != null) { - sender = senderAttribute.getValueString(); - } else { - sender = ""; - } - senderAttribute = messageArtifact.getAttribute(MSG_DATEIME_SENT_ATT); - if (senderAttribute != null) { - date = senderAttribute.getValueLong(); - } - - AbstractFile abstractFile = (AbstractFile) content; - - path = Paths.get(abstractFile.getParentPath(), abstractFile.getName()).toString(); - - if (date != null && date != 0) { - List list = sortedMap.get(date); - if (list == null) { - list = new ArrayList<>(); - sortedMap.put(date, list); - } - RecentAttachmentDetails details = new RecentAttachmentDetails(path, date, sender); - if (!list.contains(details)) { - list.add(details); - } - } - } - } - } - return sortedMap.descendingMap(); - } - - /** - * Create a list of detail objects from the given sorted map of the max - * size. - * - * @param sortedMap A Map of attachment details sorted by date. - * @param maxCount Maximum number of values to return. - * - * @return A list of the details of the most recent attachments or empty - * list if none where found. - */ - private List createListFromMap(SortedMap> sortedMap, int maxCount) { - List fileList = new ArrayList<>(); - - for (List mapList : sortedMap.values()) { - if (maxCount == 0 || fileList.size() + mapList.size() <= maxCount) { - fileList.addAll(mapList); - continue; - } - - if (maxCount == fileList.size()) { - break; - } - - for (RecentAttachmentDetails details : mapList) { - if (fileList.size() < maxCount) { - fileList.add(details); - } else { - break; - } - } + private RecentAttachmentDetails getRecentAttachment(BlackboardArtifact artifact, SleuthkitCase skCase) throws TskCoreException { + // get associated artifact or return no result + BlackboardAttribute attribute = artifact.getAttribute(ASSOCATED_ATT); + if (attribute == null) { + return null; } - return fileList; + // get associated message artifact if exists or return no result + BlackboardArtifact messageArtifact = skCase.getBlackboardArtifact(attribute.getValueLong()); + if (messageArtifact == null || !isMessageArtifact(messageArtifact)) { + return null; + } + + // get abstract file if exists or return no result + Content content = artifact.getParent(); + if (!(content instanceof AbstractFile)) { + return null; + } + + AbstractFile abstractFile = (AbstractFile) content; + + // get the path, sender, and date + String path = Paths.get(abstractFile.getParentPath(), abstractFile.getName()).toString(); + String sender = DataSourceInfoUtilities.getStringOrNull(messageArtifact, EMAIL_FROM_ATT); + Long date = DataSourceInfoUtilities.getLongOrNull(messageArtifact, MSG_DATEIME_SENT_ATT); + + if (date == null || date == 0 || StringUtils.isBlank(path)) { + return null; + } else { + return new RecentAttachmentDetails(path, date, sender); + } } /** * Is the given artifact a message. * * @param nodeArtifact An artifact that might be a message. Must not be - * null. + * null. * * @return True if the given artifact is a message artifact */ @@ -330,6 +314,7 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { final int artifactTypeID = nodeArtifact.getArtifactTypeID(); return artifactTypeID == ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID() || artifactTypeID == ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(); + } /** @@ -391,8 +376,8 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { /** * Constructor for files with just a path and date. * - * @param path File path. - * @param date File access date\time in seconds with java epoch. + * @param path File path. + * @param date File access date\time in seconds with java epoch. * @param webDomain The webdomain from which the file was downloaded. */ RecentDownloadDetails(String path, long date, String webDomain) { @@ -404,7 +389,7 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { * Returns the web domain. * * @return The web domain or empty string if not available or - * applicable. + * applicable. */ public String getWebDomain() { return webDomain; @@ -422,10 +407,10 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { * Constructor for recent download files which have a path, date and * domain value. * - * @param path File path. - * @param date File crtime. + * @param path File path. + * @param date File crtime. * @param sender The sender of the message from which the file was - * attached. + * attached. */ RecentAttachmentDetails(String path, long date, String sender) { super(path, date); @@ -436,7 +421,7 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { * Return the sender of the attached file. * * @return The sender of the attached file or empty string if not - * available. + * available. */ public String getSender() { return sender; diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java index 16da6f5c4b..b7efda6e29 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java @@ -137,7 +137,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { return (a.getProgramName() == null ? "" : a.getProgramName()) .compareToIgnoreCase((b.getProgramName() == null ? "" : b.getProgramName())); }; - + private static final Set ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList( ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY.getTypeID(), ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(), @@ -172,9 +172,9 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * is designed with unit testing in mind since mocked dependencies can be * utilized. * - * @param provider The object providing the current SleuthkitCase. + * @param provider The object providing the current SleuthkitCase. * @param translationService The translation service. - * @param logger The logger to use. + * @param logger The logger to use. */ public UserActivitySummary( SleuthkitCaseProvider provider, @@ -206,7 +206,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * Gets a list of recent domains based on the datasource. * * @param dataSource The datasource to query for recent domains. - * @param count The max count of items to return. + * @param count The max count of items to return. * * @return The list of items retrieved from the database. * @@ -242,12 +242,12 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * Creates a TopDomainsResult from data or null if no visit date exists * within DOMAIN_WINDOW_MS of mostRecentMs. * - * @param domain The domain. - * @param visits The number of visits. + * @param domain The domain. + * @param visits The number of visits. * @param mostRecentMs The most recent visit of any domain. * * @return The TopDomainsResult or null if no visits to this domain within - * 30 days of mostRecentMs. + * 30 days of mostRecentMs. */ private TopDomainsResult getDomainsResult(String domain, List visits, long mostRecentMs) { long visitCount = 0; @@ -280,9 +280,8 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * @param dataSource The datasource. * * @return A tuple where the first value is the latest web history accessed - * date in milliseconds and the second value maps normalized - * (lowercase; trimmed) domain names to when those domains were - * visited. + * date in milliseconds and the second value maps normalized (lowercase; + * trimmed) domain names to when those domains were visited. * * @throws TskCoreException * @throws SleuthkitCaseProviderException @@ -349,7 +348,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * @param artifact The artifact. * * @return The TopWebSearchResult or null if the search string or date - * accessed cannot be determined. + * accessed cannot be determined. */ private static TopWebSearchResult getWebSearchResult(BlackboardArtifact artifact) { String searchString = DataSourceInfoUtilities.getStringOrNull(artifact, TYPE_TEXT); @@ -364,11 +363,10 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * term. * * @param dataSource The data source. - * @param count The maximum number of records to be shown (must be > - * 0). + * @param count The maximum number of records to be shown (must be > 0). * * @return The list of most recent web searches where most recent search - * appears first. + * appears first. * * @throws * org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException @@ -386,21 +384,22 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { .getArtifacts(ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY.getTypeID(), dataSource.getId()); // group by search string (case insensitive) - Collection> resultGroups = webSearchArtifacts + Collection resultGroups = webSearchArtifacts .stream() // get items where search string and date is not null .map(UserActivitySummary::getWebSearchResult) // remove null records .filter(result -> result != null) - // get these messages grouped by search to string - .collect(Collectors.groupingBy((result) -> result.getSearchString().toUpperCase())) + // get the latest message for each search string + .collect(Collectors.toMap( + (result) -> result.getSearchString().toUpperCase(), + result -> result, + (result1, result2) -> TOP_WEBSEARCH_RESULT_DATE_COMPARE.compare(result1, result2) >= 0 ? result1 : result2)) .values(); // get the most recent date for each search term List results = resultGroups .stream() - // get the most recent access per search type - .map((list) -> list.stream().max(TOP_WEBSEARCH_RESULT_DATE_COMPARE).get()) // get most recent searches first .sorted(TOP_WEBSEARCH_RESULT_DATE_COMPARE.reversed()) .limit(count) @@ -424,7 +423,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * @param original The original text. * * @return The translated text or null if no translation can be determined - * or exists. + * or exists. */ private String getTranslationOrNull(String original) { if (!translationService.hasProvider() || StringUtils.isBlank(original)) { @@ -448,15 +447,34 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { return translated; } + /** + * Gives the most recent TopDeviceAttachedResult. If one is null, the other + * is returned. + * + * @param r1 A result. + * @param r2 Another result. + * @return The most recent one with a non-null date. + */ + private TopDeviceAttachedResult getMostRecentDevice(TopDeviceAttachedResult r1, TopDeviceAttachedResult r2) { + if (r2.getDateAccessed() == null) { + return r1; + } + + if (r1.getDateAccessed() == null) { + return r2; + } + + return r1.getDateAccessed().compareTo(r2.getDateAccessed()) >= 0 ? r1 : r2; + } + /** * Retrieves most recent devices used by most recent date attached. * * @param dataSource The data source. - * @param count The maximum number of records to be shown (must be > - * 0). + * @param count The maximum number of records to be shown (must be > 0). * * @return The list of most recent devices attached where most recent device - * attached appears first. + * attached appears first. * * @throws * org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException @@ -469,7 +487,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { return Collections.emptyList(); } - return DataSourceInfoUtilities.getArtifacts(caseProvider.get(), TYPE_DEVICE_ATTACHED, + Collection results = DataSourceInfoUtilities.getArtifacts(caseProvider.get(), TYPE_DEVICE_ATTACHED, dataSource, TYPE_DATETIME, DataSourceInfoUtilities.SortOrder.DESCENDING, 0) .stream() .map(artifact -> { @@ -482,9 +500,14 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { }) // remove Root Hub identifier .filter(result -> { - return result.getDeviceModel() == null + return result.getDeviceId() == null + || result.getDeviceModel() == null || !DEVICE_EXCLUDE_LIST.contains(result.getDeviceModel().trim().toUpperCase()); }) + .collect(Collectors.toMap(result -> result.getDeviceId(), result -> result, (r1, r2) -> getMostRecentDevice(r1, r2))) + .values(); + + return results.stream() .limit(count) .collect(Collectors.toList()); } @@ -495,7 +518,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * @param artifact The artifact. * * @return The TopAccountResult or null if the account type or message date - * cannot be determined. + * cannot be determined. */ private static TopAccountResult getMessageAccountResult(BlackboardArtifact artifact) { String type = DataSourceInfoUtilities.getStringOrNull(artifact, TYPE_MESSAGE_TYPE); @@ -509,12 +532,12 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * Obtains a TopAccountResult from a blackboard artifact. The date is * maximum of any found dates for attribute types provided. * - * @param artifact The artifact. + * @param artifact The artifact. * @param messageType The type of message this is. - * @param dateAttrs The date attribute types. + * @param dateAttrs The date attribute types. * * @return The TopAccountResult or null if the account type or max date are - * not provided. + * not provided. */ private static TopAccountResult getAccountResult(BlackboardArtifact artifact, String messageType, BlackboardAttribute.Type... dateAttrs) { String type = messageType; @@ -538,11 +561,10 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * sent. * * @param dataSource The data source. - * @param count The maximum number of records to be shown (must be > - * 0). + * @param count The maximum number of records to be shown (must be > 0). * * @return The list of most recent accounts used where the most recent - * account by last message sent occurs first. + * account by last message sent occurs first. * * @throws * org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException @@ -585,18 +607,19 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { Stream allResults = Stream.concat(messageResults, Stream.concat(emailResults, calllogResults)); // get them grouped by account type - Collection> groupedResults = allResults + Collection groupedResults = allResults // remove null records .filter(result -> result != null) - // get these messages grouped by account type - .collect(Collectors.groupingBy(TopAccountResult::getAccountType)) + // get these messages grouped by account type and get the most recent of each type + .collect(Collectors.toMap( + result -> result.getAccountType(), + result -> result, + (result1, result2) -> TOP_ACCOUNT_RESULT_DATE_COMPARE.compare(result1, result2) >= 0 ? result1 : result2)) .values(); // get account type sorted by most recent date return groupedResults .stream() - // get the most recent access per account type - .map((accountGroup) -> accountGroup.stream().max(TOP_ACCOUNT_RESULT_DATE_COMPARE).get()) // get most recent accounts accessed .sorted(TOP_ACCOUNT_RESULT_DATE_COMPARE.reversed()) // limit to count @@ -608,7 +631,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { /** * Determines a short folder name if any. Otherwise, returns empty string. * - * @param strPath The string path. + * @param strPath The string path. * @param applicationName The application name. * * @return The short folder name or empty string if not found. @@ -659,7 +682,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { if (StringUtils.startsWithIgnoreCase(path, WINDOWS_PREFIX)) { return null; } - + Integer count = DataSourceInfoUtilities.getIntOrNull(artifact, TYPE_COUNT); Long longCount = (count == null) ? null : (long) count; @@ -696,7 +719,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * @param long2 Second possibly null long. * * @return Returns the compare value: 1,0,-1 favoring the higher non-null - * value. + * value. */ private static int nullableCompare(Long long1, Long long2) { if (long1 == null && long2 == null) { @@ -721,7 +744,6 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { return longNum != null && longNum > 0; } - /** * Retrieves the top programs results for the given data source limited to * the count provided as a parameter. The highest run times are at the top @@ -731,12 +753,12 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * be ignored and all items will be returned. * * @param dataSource The datasource. If the datasource is null, an empty - * list will be returned. - * @param count The number of results to return. This value must be > 0 - * or an IllegalArgumentException will be thrown. + * list will be returned. + * @param count The number of results to return. This value must be > 0 or + * an IllegalArgumentException will be thrown. * * @return The sorted list and limited to the count if last run or run count - * information is available on any item. + * information is available on any item. * * @throws SleuthkitCaseProviderException * @throws TskCoreException @@ -759,7 +781,9 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { // The value will be a TopProgramsResult with the max run times // and most recent last run date for each program name / program path pair. .collect(Collectors.toMap( - res -> Pair.of(res.getProgramName(), res.getProgramPath()), + res -> Pair.of( + res.getProgramName() == null ? null : res.getProgramName().toUpperCase(), + res.getProgramPath() == null ? null : res.getProgramPath().toUpperCase()), res -> res, (res1, res2) -> { return new TopProgramsResult( @@ -852,10 +876,10 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { /** * Main constructor. * - * @param deviceId The device id. + * @param deviceId The device id. * @param dateAccessed The date last attached. - * @param deviceMake The device make. - * @param deviceModel The device model. + * @param deviceMake The device make. + * @param deviceModel The device model. */ public TopDeviceAttachedResult(String deviceId, Date dateAccessed, String deviceMake, String deviceModel) { this.deviceId = deviceId; @@ -906,7 +930,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * Main constructor. * * @param accountType The account type. - * @param lastAccess The date the account was last accessed. + * @param lastAccess The date the account was last accessed. */ public TopAccountResult(String accountType, Date lastAccess) { this.accountType = accountType; @@ -940,9 +964,9 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { /** * Describes a top domain result. * - * @param domain The domain. + * @param domain The domain. * @param visitTimes The number of times it was visited. - * @param lastVisit The date of the last visit. + * @param lastVisit The date of the last visit. */ public TopDomainsResult(String domain, Long visitTimes, Date lastVisit) { this.domain = domain; @@ -987,7 +1011,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { * * @param programName The name of the program. * @param programPath The path of the program. - * @param runTimes The number of runs. + * @param runTimes The number of runs. */ TopProgramsResult(String programName, String programPath, Long runTimes, Date lastRun) { this.programName = programName; diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties index 0036d4dd6f..15698d736c 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties @@ -1,3 +1,4 @@ ILeappAnalyzerIngestModule.init.exception.msg=Unable to find {0}. ILeappAnalyzerIngestModule.processing.file=Processing file {0} -ILeappAnalyzerIngestModule.parsing.file=Parsing file {0} \ No newline at end of file +ILeappAnalyzerIngestModule.parsing.file=Parsing file {0} +ILeappAnalyzerIngestModule.processing.filesystem=Processing filesystem \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED index b4f350f478..b4e8226a91 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED @@ -8,6 +8,7 @@ ILeappAnalyzerIngestModule.iLeapp.cancelled=iLeapp run was canceled ILeappAnalyzerIngestModule.init.exception.msg=Unable to find {0}. ILeappAnalyzerIngestModule.processing.file=Processing file {0} ILeappAnalyzerIngestModule.parsing.file=Parsing file {0} +ILeappAnalyzerIngestModule.processing.filesystem=Processing filesystem ILeappAnalyzerIngestModule.report.name=iLeapp Html Report ILeappAnalyzerIngestModule.requires.windows=iLeapp module requires windows. ILeappAnalyzerIngestModule.running.iLeapp=Running iLeapp diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java index f859919bae..abec90e6c0 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java @@ -18,8 +18,10 @@ */ package org.sleuthkit.autopsy.modules.ileappanalyzer; +import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; +import java.io.FileReader; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -32,14 +34,17 @@ import java.util.Locale; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.io.FilenameUtils; import org.openide.modules.InstalledFileLocator; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import static org.sleuthkit.autopsy.casemodule.Case.getCurrentCase; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.services.FileManager; import org.sleuthkit.autopsy.coreutils.ExecUtil; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.autopsy.ingest.DataSourceIngestModule; import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProcessTerminator; import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress; @@ -50,6 +55,7 @@ import org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.LocalFilesDataSource; +import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.TskCoreException; /** @@ -61,7 +67,9 @@ public class ILeappAnalyzerIngestModule implements DataSourceIngestModule { private static final String MODULE_NAME = ILeappAnalyzerModuleFactory.getModuleName(); private static final String ILEAPP = "iLeapp"; //NON-NLS + private static final String ILEAPP_FS = "fs_"; //NON-NLS private static final String ILEAPP_EXECUTABLE = "ileapp.exe";//NON-NLS + private static final String ILEAPP_PATHS_FILE = "iLeapp_paths.txt"; //NON-NLS private File iLeappExecutable; @@ -87,7 +95,7 @@ public class ILeappAnalyzerIngestModule implements DataSourceIngestModule { try { iLeappFileProcessor = new ILeappFileProcessor(); - } catch (IOException | IngestModuleException ex) { + } catch (IOException | IngestModuleException | NoCurrentCaseException ex) { throw new IngestModuleException(Bundle.ILeappAnalyzerIngestModule_error_ileapp_file_processor_init(), ex); } @@ -112,58 +120,49 @@ public class ILeappAnalyzerIngestModule implements DataSourceIngestModule { @Override public ProcessResult process(Content dataSource, DataSourceIngestModuleProgress statusHelper) { - if (!(context.getDataSource() instanceof LocalFilesDataSource)) { - return ProcessResult.OK; + Case currentCase = Case.getCurrentCase(); + Path tempOutputPath = Paths.get(currentCase.getTempDirectory(), ILEAPP, ILEAPP_FS + dataSource.getId()); + try { + Files.createDirectories(tempOutputPath); + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error creating iLeapp output directory %s", tempOutputPath.toString()), ex); + return ProcessResult.ERROR; + } + + List iLeappPathsToProcess = new ArrayList<>(); + ProcessBuilder iLeappCommand = buildiLeappListCommand(tempOutputPath); + try { + int result = ExecUtil.execute(iLeappCommand, new DataSourceIngestModuleProcessTerminator(context, true)); + if (result != 0) { + logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program getting file paths to search for result is %d", result)); + return ProcessResult.ERROR; + } + iLeappPathsToProcess = loadIleappPathFile(tempOutputPath); + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program getting file paths to search"), ex); + return ProcessResult.ERROR; } statusHelper.progress(Bundle.ILeappAnalyzerIngestModule_starting_iLeapp(), 0); - List iLeappFilesToProcess = findiLeappFilesToProcess(dataSource); + List iLeappFilesToProcess = new ArrayList<>(); - statusHelper.switchToDeterminate(iLeappFilesToProcess.size()); + if (!(context.getDataSource() instanceof LocalFilesDataSource)) { + extractFilesFromImage(dataSource, iLeappPathsToProcess, tempOutputPath); + statusHelper.switchToDeterminate(iLeappFilesToProcess.size()); + processILeappFs(dataSource, currentCase, statusHelper, tempOutputPath.toString()); + } else { + iLeappFilesToProcess = findiLeappFilesToProcess(dataSource); + statusHelper.switchToDeterminate(iLeappFilesToProcess.size()); - Integer filesProcessedCount = 0; - - Case currentCase = Case.getCurrentCase(); - for (AbstractFile iLeappFile : iLeappFilesToProcess) { - - String currentTime = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss z", Locale.US).format(System.currentTimeMillis());//NON-NLS - Path moduleOutputPath = Paths.get(currentCase.getModuleDirectory(), ILEAPP, currentTime); - try { - Files.createDirectories(moduleOutputPath); - } catch (IOException ex) { - logger.log(Level.SEVERE, String.format("Error creating iLeapp output directory %s", moduleOutputPath.toString()), ex); - return ProcessResult.ERROR; + Integer filesProcessedCount = 0; + for (AbstractFile iLeappFile : iLeappFilesToProcess) { + processILeappFile(dataSource, currentCase, statusHelper, filesProcessedCount, iLeappFile); + filesProcessedCount++; } - - statusHelper.progress(NbBundle.getMessage(this.getClass(), "ILeappAnalyzerIngestModule.processing.file", iLeappFile.getName()), filesProcessedCount); - ProcessBuilder iLeappCommand = buildiLeappCommand(moduleOutputPath, iLeappFile.getLocalAbsPath(), iLeappFile.getNameExtension()); - try { - int result = ExecUtil.execute(iLeappCommand, new DataSourceIngestModuleProcessTerminator(context, true)); - if (result != 0) { - // ignore if there is an error and continue to try and process the next file if there is one - continue; - } - - addILeappReportToReports(moduleOutputPath, currentCase); - - } catch (IOException ex) { - logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program against file %s", iLeappFile.getLocalAbsPath()), ex); - return ProcessResult.ERROR; - } - - if (context.dataSourceIngestIsCancelled()) { - logger.log(Level.INFO, "ILeapp Analyser ingest module run was canceled"); //NON-NLS - return ProcessResult.OK; - } - - ProcessResult fileProcessorResult = iLeappFileProcessor.processFiles(dataSource, moduleOutputPath, iLeappFile); - - if (fileProcessorResult == ProcessResult.ERROR) { - return ProcessResult.ERROR; - } - - filesProcessedCount++; + // Process the logical image as a fs in iLeapp to make sure this is not a logical fs that was added + extractFilesFromImage(dataSource, iLeappPathsToProcess, tempOutputPath); + processILeappFs(dataSource, currentCase, statusHelper, tempOutputPath.toString()); } IngestMessage message = IngestMessage.createMessage(IngestMessage.MessageType.DATA, @@ -173,6 +172,99 @@ public class ILeappAnalyzerIngestModule implements DataSourceIngestModule { return ProcessResult.OK; } + /** + * Process each tar/zip file that is found in a logical image that contains xLeapp data + * @param dataSource Datasource where the file has been found + * @param currentCase current case + * @param statusHelper Progress bar for messages to show user + * @param filesProcessedCount count that is incremented for progress bar + * @param iLeappFile abstract file that will be processed + */ + private void processILeappFile(Content dataSource, Case currentCase, DataSourceIngestModuleProgress statusHelper, int filesProcessedCount, + AbstractFile iLeappFile) { + String currentTime = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss z", Locale.US).format(System.currentTimeMillis());//NON-NLS + Path moduleOutputPath = Paths.get(currentCase.getModuleDirectory(), ILEAPP, currentTime); + try { + Files.createDirectories(moduleOutputPath); + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error creating iLeapp output directory %s", moduleOutputPath.toString()), ex); + return; + } + + statusHelper.progress(NbBundle.getMessage(this.getClass(), "ILeappAnalyzerIngestModule.processing.file", iLeappFile.getName()), filesProcessedCount); + ProcessBuilder iLeappCommand = buildiLeappCommand(moduleOutputPath, iLeappFile.getLocalAbsPath(), iLeappFile.getNameExtension()); + try { + int result = ExecUtil.execute(iLeappCommand, new DataSourceIngestModuleProcessTerminator(context, true)); + if (result != 0) { + logger.log(Level.WARNING, String.format("Error when trying to execute iLeapp program getting file paths to search for result is %d", result)); + return; + } + + addILeappReportToReports(moduleOutputPath, currentCase); + + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program against file %s", iLeappFile.getLocalAbsPath()), ex); + return; + } + + if (context.dataSourceIngestIsCancelled()) { + logger.log(Level.INFO, "ILeapp Analyser ingest module run was canceled"); //NON-NLS + return; + } + + ProcessResult fileProcessorResult = iLeappFileProcessor.processFiles(dataSource, moduleOutputPath, iLeappFile); + + if (fileProcessorResult == ProcessResult.ERROR) { + return; + } + } + + /** + * Process extracted files from a disk image using xLeapp + * @param dataSource Datasource where the file has been found + * @param currentCase current case + * @param statusHelper Progress bar for messages to show user + * @param directoryToProcess + */ + private void processILeappFs(Content dataSource, Case currentCase, DataSourceIngestModuleProgress statusHelper, String directoryToProcess) { + String currentTime = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss z", Locale.US).format(System.currentTimeMillis());//NON-NLS + Path moduleOutputPath = Paths.get(currentCase.getModuleDirectory(), ILEAPP, currentTime); + try { + Files.createDirectories(moduleOutputPath); + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error creating iLeapp output directory %s", moduleOutputPath.toString()), ex); + return; + } + + statusHelper.progress(NbBundle.getMessage(this.getClass(), "ILeappAnalyzerIngestModule.processing.filesystem")); + ProcessBuilder iLeappCommand = buildiLeappCommand(moduleOutputPath, directoryToProcess, "fs"); + try { + int result = ExecUtil.execute(iLeappCommand, new DataSourceIngestModuleProcessTerminator(context, true)); + if (result != 0) { + logger.log(Level.WARNING, String.format("Error when trying to execute iLeapp program getting file paths to search for result is %d", result)); + return; + } + + addILeappReportToReports(moduleOutputPath, currentCase); + + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program against file system"), ex); + return; + } + + if (context.dataSourceIngestIsCancelled()) { + logger.log(Level.INFO, "ILeapp Analyser ingest module run was canceled"); //NON-NLS + return; + } + + ProcessResult fileProcessorResult = iLeappFileProcessor.processFileSystem(dataSource, moduleOutputPath); + + if (fileProcessorResult == ProcessResult.ERROR) { + return; + } + + } + /** * Find the files that will be processed by the iLeapp program * @@ -197,17 +289,24 @@ public class ILeappAnalyzerIngestModule implements DataSourceIngestModule { List iLeappFilesToProcess = new ArrayList<>(); for (AbstractFile iLeappFile : iLeappFiles) { if (((iLeappFile.getLocalAbsPath() != null) - && (!iLeappFile.getNameExtension().isEmpty() && (!iLeappFile.isVirtual()))) - && ((iLeappFile.getName().toLowerCase().contains(".zip") || (iLeappFile.getName().toLowerCase().contains(".tar"))) - || iLeappFile.getName().toLowerCase().contains(".tgz"))) { - iLeappFilesToProcess.add(iLeappFile); - + && (!iLeappFile.getNameExtension().isEmpty() && (!iLeappFile.isVirtual()))) + && ((iLeappFile.getName().toLowerCase().contains(".zip") || (iLeappFile.getName().toLowerCase().contains(".tar"))) + || iLeappFile.getName().toLowerCase().contains(".tgz"))) { + iLeappFilesToProcess.add(iLeappFile); + } } return iLeappFilesToProcess; } + /** + * Build the command to run xLeapp + * @param moduleOutputPath output path for xLeapp + * @param sourceFilePath path where the xLeapp file is + * @param iLeappFileSystem type of file to process tar/zip/fs + * @return process to run + */ private ProcessBuilder buildiLeappCommand(Path moduleOutputPath, String sourceFilePath, String iLeappFileSystemType) { ProcessBuilder processBuilder = buildProcessWithRunAsInvoker( @@ -221,10 +320,26 @@ public class ILeappAnalyzerIngestModule implements DataSourceIngestModule { return processBuilder; } + /** + * Command to run xLeapp using the path option + * @param moduleOutputPath path where the file paths output will reside + * @return process to run + */ + private ProcessBuilder buildiLeappListCommand(Path moduleOutputPath) { + + ProcessBuilder processBuilder = buildProcessWithRunAsInvoker( + "\"" + iLeappExecutable + "\"", //NON-NLS + "-p" + ); + processBuilder.redirectError(moduleOutputPath.resolve("iLeapp_paths_error.txt").toFile()); //NON-NLS + processBuilder.redirectOutput(moduleOutputPath.resolve("iLeapp_paths.txt").toFile()); //NON-NLS + return processBuilder; + } + static private ProcessBuilder buildProcessWithRunAsInvoker(String... commandLine) { ProcessBuilder processBuilder = new ProcessBuilder(commandLine); /* - * Add an environment variable to force log2timeline/psort to run with + * Add an environment variable to force iLeapp to run with * the same permissions Autopsy uses. */ processBuilder.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS @@ -248,13 +363,18 @@ public class ILeappAnalyzerIngestModule implements DataSourceIngestModule { private void addILeappReportToReports(Path iLeappOutputDir, Case currentCase) { List allIndexFiles = new ArrayList<>(); - try (Stream walk = Files.walk(iLeappOutputDir)) { + try (Stream walk = Files.walk(iLeappOutputDir)) { allIndexFiles = walk.map(x -> x.toString()) .filter(f -> f.toLowerCase().endsWith("index.html")).collect(Collectors.toList()); if (!allIndexFiles.isEmpty()) { - currentCase.addReport(allIndexFiles.get(0), MODULE_NAME, Bundle.ILeappAnalyzerIngestModule_report_name()); + // Check for existance of directory that holds report data if does not exist then report contains no data + String filePath = FilenameUtils.getFullPathNoEndSeparator(allIndexFiles.get(0)); + File dataFilesDir = new File(Paths.get(filePath, "_TSV Exports").toString()); + if (dataFilesDir.exists()) { + currentCase.addReport(allIndexFiles.get(0), MODULE_NAME, Bundle.ILeappAnalyzerIngestModule_report_name()); + } } } catch (IOException | UncheckedIOException | TskCoreException ex) { @@ -264,4 +384,129 @@ public class ILeappAnalyzerIngestModule implements DataSourceIngestModule { } + /* + * Reads the iLeapp paths file to get the paths that we want to extract + * + * @param moduleOutputPath path where the file paths output will reside + */ + private List loadIleappPathFile(Path moduleOutputPath) throws FileNotFoundException, IOException { + List iLeappPathsToProcess = new ArrayList<>(); + + Path filePath = Paths.get(moduleOutputPath.toString(), ILEAPP_PATHS_FILE); + + try (BufferedReader reader = new BufferedReader(new FileReader(filePath.toString()))) { + String line = reader.readLine(); + while (line != null) { + if (line.contains("path list generation") || line.length() < 2) { + line = reader.readLine(); + continue; + } + iLeappPathsToProcess.add(line.trim()); + line = reader.readLine(); + } + } + + return iLeappPathsToProcess; + } + + /** + * Extract files from a disk image to process with xLeapp + * @param dataSource Datasource of the image + * @param iLeappPathsToProcess List of paths to extract content from + * @param moduleOutputPath path to write content to + */ + private void extractFilesFromImage(Content dataSource, List iLeappPathsToProcess, Path moduleOutputPath) { + FileManager fileManager = getCurrentCase().getServices().getFileManager(); + + for (String fullFilePath : iLeappPathsToProcess) { + + if (context.dataSourceIngestIsCancelled()) { + logger.log(Level.INFO, "ILeapp Analyser ingest module run was canceled"); //NON-NLS + break; + } + + String ffp = fullFilePath.replaceAll("\\*", "%"); + ffp = FilenameUtils.normalize(ffp, true); + String fileName = FilenameUtils.getName(ffp); + String filePath = FilenameUtils.getPath(ffp); + + List iLeappFiles = new ArrayList<>(); + try { + if (filePath.isEmpty()) { + iLeappFiles = fileManager.findFiles(dataSource, fileName); //NON-NLS + } else { + iLeappFiles = fileManager.findFiles(dataSource, fileName, filePath); //NON-NLS + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "No files found to process"); //NON-NLS + return; + } + + for (AbstractFile iLeappFile : iLeappFiles) { + Path parentPath = Paths.get(moduleOutputPath.toString(), iLeappFile.getParentPath()); + File fileParentPath = new File(parentPath.toString()); + + extractFileToOutput(dataSource, iLeappFile, fileParentPath, parentPath); + } + } + } + + /** + * Create path and file from datasource in temp + * @param dataSource datasource of the image + * @param iLeappFile abstract file to write out + * @param fileParentPath parent file path + * @param parentPath parent file + */ + private void extractFileToOutput(Content dataSource, AbstractFile iLeappFile, File fileParentPath, Path parentPath) { + if (fileParentPath.exists()) { + if (!iLeappFile.isDir()) { + writeiLeappFile(dataSource, iLeappFile, fileParentPath.toString()); + } else { + try { + Files.createDirectories(Paths.get(parentPath.toString(), iLeappFile.getName())); + } catch (IOException ex) { + logger.log(Level.INFO, String.format("Error creating iLeapp output directory %s", parentPath.toString()), ex); + } + } + } else { + try { + Files.createDirectories(parentPath); + } catch (IOException ex) { + logger.log(Level.INFO, String.format("Error creating iLeapp output directory %s", parentPath.toString()), ex); + } + if (!iLeappFile.isDir()) { + writeiLeappFile(dataSource, iLeappFile, fileParentPath.toString()); + } else { + try { + Files.createDirectories(Paths.get(parentPath.toString(), iLeappFile.getName())); + } catch (IOException ex) { + logger.log(Level.INFO, String.format("Error creating iLeapp output directory %s", parentPath.toString()), ex); + } + } + } + } + + /** + * Write out file to output + * @param dataSource datasource of disk image + * @param iLeappFile acstract file to write out + * @param parentPath path to write file to + */ + private void writeiLeappFile(Content dataSource, AbstractFile iLeappFile, String parentPath) { + String fileName = iLeappFile.getName().replace(":", "-"); + if (!fileName.matches(".") && !fileName.matches("..") && !fileName.toLowerCase().endsWith("-slack")) { + Path filePath = Paths.get(parentPath, fileName); + File localFile = new File(filePath.toString()); + try { + ContentUtils.writeToFile(iLeappFile, localFile, context::dataSourceIngestIsCancelled); + } catch (ReadContentInputStream.ReadContentInputStreamException ex) { + logger.log(Level.WARNING, String.format("Error reading file '%s' (id=%d).", + iLeappFile.getName(), iLeappFile.getId()), ex); //NON-NLS + } catch (IOException ex) { + logger.log(Level.WARNING, String.format("Error writing file local file '%s' (id=%d).", + filePath.toString(), iLeappFile.getId()), ex); //NON-NLS + } + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java index cb5ab516a6..058b0aa28d 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java @@ -44,6 +44,7 @@ import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.io.FilenameUtils; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException; @@ -76,12 +77,16 @@ public final class ILeappFileProcessor { private final Map tsvFileArtifactComments; private final Map>> tsvFileAttributes; - public ILeappFileProcessor() throws IOException, IngestModuleException { + Blackboard blkBoard; + + public ILeappFileProcessor() throws IOException, IngestModuleException, NoCurrentCaseException { this.tsvFiles = new HashMap<>(); this.tsvFileArtifacts = new HashMap<>(); this.tsvFileArtifactComments = new HashMap<>(); this.tsvFileAttributes = new HashMap<>(); + blkBoard = Case.getCurrentCaseThrows().getSleuthkitCase().getBlackboard(); + configExtractor(); loadConfigFile(); @@ -110,6 +115,19 @@ public final class ILeappFileProcessor { return ProcessResult.OK; } + public ProcessResult processFileSystem(Content dataSource, Path moduleOutputPath) { + + try { + List iLeappTsvOutputFiles = findTsvFiles(moduleOutputPath); + processiLeappFiles(iLeappTsvOutputFiles, dataSource); + } catch (IOException | IngestModuleException ex) { + logger.log(Level.SEVERE, String.format("Error trying to process iLeapp output files in directory %s. ", moduleOutputPath.toString()), ex); //NON-NLS + return ProcessResult.ERROR; + } + + return ProcessResult.OK; + } + /** * Find the tsv files in the iLeapp output directory and match them to files * we know we want to process and return the list to process those files. @@ -124,7 +142,7 @@ public final class ILeappFileProcessor { .filter(f -> f.toLowerCase().endsWith(".tsv")).collect(Collectors.toList()); for (String tsvFile : allTsvFiles) { - if (tsvFiles.containsKey(FilenameUtils.getName(tsvFile))) { + if (tsvFiles.containsKey(FilenameUtils.getName(tsvFile.toLowerCase()))) { foundTsvFiles.add(tsvFile); } } @@ -160,7 +178,41 @@ public final class ILeappFileProcessor { processFile(iLeappFile, attrList, fileName, artifactType, bbartifacts, iLeappImageFile); } catch (TskCoreException ex) { - // check this + throw new IngestModuleException(String.format("Error getting Blackboard Artifact Type for %s", tsvFileArtifacts.get(fileName)), ex); + } + } + + } + + if (!bbartifacts.isEmpty()) { + postArtifacts(bbartifacts); + } + + } + + /** + * Process the iLeapp files that were found that match the xml mapping file + * + * @param iLeappFilesToProcess List of files to process + * @param iLeappImageFile Abstract file to create artifact for + * + * @throws FileNotFoundException + * @throws IOException + */ + private void processiLeappFiles(List iLeappFilesToProcess, Content dataSource) throws FileNotFoundException, IOException, IngestModuleException { + List bbartifacts = new ArrayList<>(); + + for (String iLeappFileName : iLeappFilesToProcess) { + String fileName = FilenameUtils.getName(iLeappFileName); + File iLeappFile = new File(iLeappFileName); + if (tsvFileAttributes.containsKey(fileName)) { + List> attrList = tsvFileAttributes.get(fileName); + try { + BlackboardArtifact.Type artifactType = Case.getCurrentCase().getSleuthkitCase().getArtifactType(tsvFileArtifacts.get(fileName)); + + processFile(iLeappFile, attrList, fileName, artifactType, bbartifacts, dataSource); + + } catch (TskCoreException ex) { throw new IngestModuleException(String.format("Error getting Blackboard Artifact Type for %s", tsvFileArtifacts.get(fileName)), ex); } } @@ -174,7 +226,8 @@ public final class ILeappFileProcessor { } private void processFile(File iLeappFile, List> attrList, String fileName, BlackboardArtifact.Type artifactType, - List bbartifacts, AbstractFile iLeappImageFile) throws FileNotFoundException, IOException, IngestModuleException { + List bbartifacts, Content dataSource) throws FileNotFoundException, IOException, IngestModuleException, + TskCoreException { try (BufferedReader reader = new BufferedReader(new FileReader(iLeappFile))) { String line = reader.readLine(); // Check first line, if it is null then no heading so nothing to match to, close and go to next file. @@ -183,8 +236,8 @@ public final class ILeappFileProcessor { line = reader.readLine(); while (line != null) { Collection bbattributes = processReadLine(line, columnNumberToProcess, fileName); - if (!bbattributes.isEmpty()) { - BlackboardArtifact bbartifact = createArtifactWithAttributes(artifactType.getTypeID(), iLeappImageFile, bbattributes); + if (!bbattributes.isEmpty() && !blkBoard.artifactExists(dataSource, BlackboardArtifact.ARTIFACT_TYPE.fromID(artifactType.getTypeID()), bbattributes)) { + BlackboardArtifact bbartifact = createArtifactWithAttributes(artifactType.getTypeID(), dataSource, bbattributes); if (bbartifact != null) { bbartifacts.add(bbartifact); } @@ -234,8 +287,8 @@ public final class ILeappFileProcessor { } - private void checkAttributeType(Collection bbattributes, String attrType, String[] columnValues, Integer columnNumber, BlackboardAttribute.Type attributeType, - String fileName) { + private void checkAttributeType(Collection bbattributes, String attrType, String[] columnValues, Integer columnNumber, BlackboardAttribute.Type attributeType, + String fileName) { if (attrType.matches("STRING")) { bbattributes.add(new BlackboardAttribute(attributeType, MODULE_NAME, columnValues[columnNumber])); } else if (attrType.matches("INTEGER")) { @@ -340,7 +393,7 @@ public final class ILeappFileProcessor { for (int i = 0; i < nlist.getLength(); i++) { NamedNodeMap nnm = nlist.item(i).getAttributes(); - tsvFiles.put(nnm.getNamedItem("filename").getNodeValue(), nnm.getNamedItem("description").getNodeValue()); + tsvFiles.put(nnm.getNamedItem("filename").getNodeValue().toLowerCase(), nnm.getNamedItem("description").getNodeValue()); } @@ -393,19 +446,20 @@ public final class ILeappFileProcessor { } } - /** - * Generic method for creating a blackboard artifact with attributes - * - * @param type is a blackboard.artifact_type enum to determine - * which type the artifact should be - * @param abstractFile is the AbstractFile object that needs to have the - * artifact added for it - * @param bbattributes is the collection of blackboard attributes that - * need to be added to the artifact after the - * artifact has been created - * - * @return The newly-created artifact, or null on error - */ + + /** + * Generic method for creating a blackboard artifact with attributes + * + * @param type is a blackboard.artifact_type enum to determine which + * type the artifact should be + * @param abstractFile is the AbstractFile object that needs to have the + * artifact added for it + * @param bbattributes is the collection of blackboard attributes that need + * to be added to the artifact after the artifact has + * been created + * + * @return The newly-created artifact, or null on error + */ private BlackboardArtifact createArtifactWithAttributes(int type, AbstractFile abstractFile, Collection bbattributes) { try { BlackboardArtifact bbart = abstractFile.newArtifact(type); @@ -417,6 +471,30 @@ public final class ILeappFileProcessor { return null; } + /** + * Generic method for creating a blackboard artifact with attributes + * + * @param type is a blackboard.artifact_type enum to determine which + * type the artifact should be + * @param datasource is the Content object that needs to have the artifact + * added for it + * @param bbattributes is the collection of blackboard attributes that need + * to be added to the artifact after the artifact has + * been created + * + * @return The newly-created artifact, or null on error + */ + private BlackboardArtifact createArtifactWithAttributes(int type, Content dataSource, Collection bbattributes) { + try { + BlackboardArtifact bbart = dataSource.newArtifact(type); + bbart.addAttributes(bbattributes); + return bbart; + } catch (TskException ex) { + logger.log(Level.WARNING, Bundle.ILeappFileProcessor_error_creating_new_artifacts(), ex); //NON-NLS + } + return null; + } + /** * Method to post a list of BlackboardArtifacts to the blackboard. * diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml index 0959cbdcc4..a4169395aa 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml @@ -47,6 +47,15 @@ + + + + + + + + + @@ -120,6 +129,13 @@ + + + + + + + @@ -189,6 +205,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -207,15 +253,61 @@ - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -248,6 +340,19 @@ + + + + + + + + + + + + + @@ -288,6 +393,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -302,6 +437,18 @@ + + + + + + + + + + + + @@ -317,6 +464,18 @@ + + + + + + + + + + + + @@ -433,6 +592,102 @@ --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java index 068f0c3904..ff73c428b9 100644 --- a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java +++ b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java @@ -71,8 +71,8 @@ public class RecentFilesSummaryTest { * Means of acquiring data from a method in RecentFilesSummary. * * @param recentFilesSummary The RecentFilesSummary object. - * @param dataSource The datasource. - * @param count The number of items to retrieve. + * @param dataSource The datasource. + * @param count The number of items to retrieve. * * @return The method's return data. * @@ -95,7 +95,7 @@ public class RecentFilesSummaryTest { /** * If -1 count passed to method, should throw IllegalArgumentException. * - * @param method The method to call. + * @param method The method to call. * @param methodName The name of the metho * * @throws TskCoreException @@ -137,7 +137,7 @@ public class RecentFilesSummaryTest { * SleuthkitCase isn't called. * * @param recentFilesMethod The method to call. - * @param methodName The name of the method + * @param methodName The name of the method * * @throws SleuthkitCaseProviderException * @throws TskCoreException @@ -175,7 +175,7 @@ public class RecentFilesSummaryTest { * If SleuthkitCase returns no results, an empty list is returned. * * @param recentFilesMethod The method to call. - * @param methodName The name of the method. + * @param methodName The name of the method. * * @throws SleuthkitCaseProviderException * @throws TskCoreException @@ -220,11 +220,11 @@ public class RecentFilesSummaryTest { /** * Gets a mock BlackboardArtifact. * - * @param ds The data source to which the artifact belongs. - * @param artifactId The artifact id. - * @param artType The artifact type. + * @param ds The data source to which the artifact belongs. + * @param artifactId The artifact id. + * @param artType The artifact type. * @param attributeArgs The mapping of attribute type to value for each - * attribute in the artifact. + * attribute in the artifact. * * @return The mock artifact. */ @@ -247,10 +247,10 @@ public class RecentFilesSummaryTest { /** * Returns a mock artifact for getRecentlyOpenedDocuments. * - * @param ds The datasource for the artifact. + * @param ds The datasource for the artifact. * @param artifactId The artifact id. - * @param dateTime The time in seconds from epoch. - * @param path The path for the document. + * @param dateTime The time in seconds from epoch. + * @param path The path for the document. * * @return The mock artifact with pertinent attributes. */ @@ -292,13 +292,33 @@ public class RecentFilesSummaryTest { } } + @Test + public void getRecentlyOpenedDocuments_uniquePaths() throws SleuthkitCaseProviderException, TskCoreException { + DataSource dataSource = TskMockUtils.getDataSource(1); + + BlackboardArtifact item1 = getRecentDocumentArtifact(dataSource, 1001, DAY_SECONDS, "/a/path"); + BlackboardArtifact item2 = getRecentDocumentArtifact(dataSource, 1002, DAY_SECONDS + 1, "/a/path"); + BlackboardArtifact item3 = getRecentDocumentArtifact(dataSource, 1003, DAY_SECONDS + 2, "/a/path"); + List artifacts = Arrays.asList(item2, item3, item1); + + Pair casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts)); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + List results = summary.getRecentlyOpenedDocuments(dataSource, 10); + + // verify results (only successItem) + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + Assert.assertEquals((Long) (DAY_SECONDS + 2), results.get(0).getDateAsLong()); + Assert.assertTrue("/a/path".equalsIgnoreCase(results.get(0).getPath())); + } + @Test public void getRecentlyOpenedDocuments_filtersMissingData() throws SleuthkitCaseProviderException, TskCoreException { DataSource dataSource = TskMockUtils.getDataSource(1); BlackboardArtifact successItem = getRecentDocumentArtifact(dataSource, 1001, DAY_SECONDS, "/a/path"); BlackboardArtifact nullTime = getRecentDocumentArtifact(dataSource, 1002, null, "/a/path2"); - BlackboardArtifact zeroTime = getRecentDocumentArtifact(dataSource, 10021, 0L, "/a/path2a"); + BlackboardArtifact zeroTime = getRecentDocumentArtifact(dataSource, 10021, 0L, "/a/path2a"); List artifacts = Arrays.asList(nullTime, zeroTime, successItem); Pair casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts)); @@ -315,11 +335,11 @@ public class RecentFilesSummaryTest { /** * Creates a mock blackboard artifact for getRecentDownloads. * - * @param ds The datasource. + * @param ds The datasource. * @param artifactId The artifact id. - * @param dateTime The time in seconds from epoch. - * @param domain The domain. - * @param path The path for the download. + * @param dateTime The time in seconds from epoch. + * @param domain The domain. + * @param path The path for the download. * * @return The mock artifact. */ @@ -368,6 +388,30 @@ public class RecentFilesSummaryTest { } } + @Test + public void getRecentDownloads_uniquePaths() throws SleuthkitCaseProviderException, TskCoreException { + DataSource dataSource = TskMockUtils.getDataSource(1); + + BlackboardArtifact item1 = getRecentDownloadArtifact(dataSource, 1001, DAY_SECONDS, "domain1.com", "/a/path1"); + BlackboardArtifact item1a = getRecentDownloadArtifact(dataSource, 10011, DAY_SECONDS + 1, "domain1.com", "/a/path1"); + BlackboardArtifact item2 = getRecentDownloadArtifact(dataSource, 1002, DAY_SECONDS + 2, "domain2.com", "/a/path1"); + BlackboardArtifact item3 = getRecentDownloadArtifact(dataSource, 1003, DAY_SECONDS + 3, "domain2a.com", "/a/path1"); + List artifacts = Arrays.asList(item2, item3, item1); + + Pair casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts)); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + + // call method + List results = summary.getRecentDownloads(dataSource, 10); + + // verify results + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + Assert.assertEquals((Long) (DAY_SECONDS + 3), results.get(0).getDateAsLong()); + Assert.assertTrue("/a/path1".equalsIgnoreCase(results.get(0).getPath())); + Assert.assertTrue("domain2a.com".equalsIgnoreCase(results.get(0).getWebDomain())); + } + @Test public void getRecentDownloads_filtersMissingData() throws SleuthkitCaseProviderException, TskCoreException { DataSource dataSource = TskMockUtils.getDataSource(1); @@ -409,19 +453,17 @@ public class RecentFilesSummaryTest { * Constructor with all parameters. * * @param messageArtifactTypeId The type id for the artifact or null if - * no message artifact to be created. - * @param emailFrom Who the message is from or null not to - * include attribute. - * @param messageTime Time in seconds from epoch or null not - * to include attribute. - * @param fileParentPath The parent AbstractFile's path value. - * @param fileName The parent AbstractFile's filename - * value. - * @param associatedAttrFormed If false, the TSK_ASSOCIATED_OBJECT - * artifact has no attribute (even though - * it is required). - * @param hasParent Whether or not the artifact has a parent - * AbstractFile. + * no message artifact to be created. + * @param emailFrom Who the message is from or null not to include + * attribute. + * @param messageTime Time in seconds from epoch or null not to include + * attribute. + * @param fileParentPath The parent AbstractFile's path value. + * @param fileName The parent AbstractFile's filename value. + * @param associatedAttrFormed If false, the TSK_ASSOCIATED_OBJECT + * artifact has no attribute (even though it is required). + * @param hasParent Whether or not the artifact has a parent + * AbstractFile. */ AttachmentArtifactItem(Integer messageArtifactTypeId, String emailFrom, Long messageTime, String fileParentPath, String fileName, @@ -441,14 +483,13 @@ public class RecentFilesSummaryTest { * SleuthkitCase assumed. * * @param messageArtifactTypeId The type id for the artifact or null if - * no message artifact to be created. - * @param emailFrom Who the message is from or null not to - * include attribute. - * @param messageTime Time in seconds from epoch or null not - * to include attribute. - * @param fileParentPath The parent AbstractFile's path value. - * @param fileName The parent AbstractFile's filename - * value. + * no message artifact to be created. + * @param emailFrom Who the message is from or null not to include + * attribute. + * @param messageTime Time in seconds from epoch or null not to include + * attribute. + * @param fileParentPath The parent AbstractFile's path value. + * @param fileName The parent AbstractFile's filename value. */ AttachmentArtifactItem(Integer messageArtifactTypeId, String emailFrom, Long messageTime, String fileParentPath, String fileName) { this(messageArtifactTypeId, emailFrom, messageTime, fileParentPath, fileName, true, true); @@ -486,11 +527,11 @@ public class RecentFilesSummaryTest { /** * Sets up the associated artifact message for the TSK_ASSOCIATED_OBJECT. * - * @param artifacts The mapping of artifact id to artifact. - * @param item The record to setup. - * @param dataSource The datasource. + * @param artifacts The mapping of artifact id to artifact. + * @param item The record to setup. + * @param dataSource The datasource. * @param associatedId The associated attribute id. - * @param artifactId The artifact id. + * @param artifactId The artifact id. * * @return The associated Artifact blackboard attribute. * @@ -504,7 +545,7 @@ public class RecentFilesSummaryTest { if (item.getMessageArtifactTypeId() == null) { return associatedAttr; } - + // find the artifact type or null if not found ARTIFACT_TYPE messageType = Stream.of(ARTIFACT_TYPE.values()) .filter((artType) -> artType.getTypeID() == item.getMessageArtifactTypeId()) @@ -534,7 +575,7 @@ public class RecentFilesSummaryTest { * to return pertinent data. * * @param items Each attachment item where each item could represent a - * return result if fully formed. + * return result if fully formed. * * @return The mock SleuthkitCase and Blackboard. */ @@ -678,4 +719,34 @@ public class RecentFilesSummaryTest { .toString().equalsIgnoreCase(successItem2Details.getPath())); Assert.assertTrue(successItem2.getEmailFrom().equalsIgnoreCase(successItem2Details.getSender())); } + + @Test + public void getRecentAttachments_uniquePath() throws SleuthkitCaseProviderException, TskCoreException { + // setup data + DataSource dataSource = TskMockUtils.getDataSource(1); + + AttachmentArtifactItem item1 = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), + "person@sleuthkit.com", DAY_SECONDS, "/parent/path", "msg.pdf"); + AttachmentArtifactItem item2 = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(), + "person_on_skype", DAY_SECONDS + 1, "/parent/path", "msg.pdf"); + AttachmentArtifactItem item3 = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), + "person2@sleuthkit.com", DAY_SECONDS + 2, "/parent/path", "msg.pdf"); + + List items = Arrays.asList(item1, item2, item3); + + Pair casePair = getRecentAttachmentArtifactCase(items); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + + // get data + List results = summary.getRecentAttachments(dataSource, 10); + + // verify results + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + + Assert.assertEquals(results.get(0).getDateAsLong(), (Long) (DAY_SECONDS + 2)); + Assert.assertTrue(Paths.get(item3.getFileParentPath(), item3.getFileName()) + .toString().equalsIgnoreCase(results.get(0).getPath())); + Assert.assertTrue(results.get(0).getSender().equalsIgnoreCase(item3.getEmailFrom())); + } } diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java index 0aff8a56b5..c7f3246a4d 100644 --- a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java +++ b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java @@ -66,18 +66,22 @@ import org.sleuthkit.datamodel.TskCoreException; * Tests for UserActivitySummary. */ public class UserActivitySummaryTest { + /** - * Function to retrieve data from UserActivitySummary with the provided arguments. + * Function to retrieve data from UserActivitySummary with the provided + * arguments. */ private interface DataFunction { + /** * A UserActivitySummary method encapsulated in a uniform manner. + * * @param userActivitySummary The UserActivitySummary class to use. * @param datasource The data source. * @param count The count. * @return The list of objects to return. * @throws SleuthkitCaseProviderException - * @throws TskCoreException + * @throws TskCoreException */ List retrieve(UserActivitySummary userActivitySummary, DataSource datasource, int count) throws SleuthkitCaseProviderException, TskCoreException; @@ -117,8 +121,8 @@ public class UserActivitySummaryTest { /** * Gets a UserActivitySummary class to test. * - * @param tskCase The SleuthkitCase. - * @param hasTranslation Whether the translation service is functional. + * @param tskCase The SleuthkitCase. + * @param hasTranslation Whether the translation service is functional. * @param translateFunction Function for translation. * * @return The UserActivitySummary class to use for testing. @@ -333,6 +337,28 @@ public class UserActivitySummaryTest { } } + @Test + public void getRecentDevices_uniqueByDeviceId() + throws TskCoreException, NoServiceProviderException, SleuthkitCaseProviderException, TskCoreException, TranslationException { + + long dataSourceId = 1L; + DataSource dataSource = TskMockUtils.getDataSource(dataSourceId); + BlackboardArtifact item1 = getRecentDeviceArtifact(1001, dataSource, "ID1", "MAKE1", "MODEL1", DAY_SECONDS); + BlackboardArtifact item2 = getRecentDeviceArtifact(1002, dataSource, "ID1", "MAKE1", "MODEL1", DAY_SECONDS + 1); + BlackboardArtifact item3 = getRecentDeviceArtifact(1003, dataSource, "ID1", "MAKE1", "MODEL1", DAY_SECONDS + 2); + + Pair tskPair = getArtifactsTSKMock(Arrays.asList(item1, item2, item3)); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List results = summary.getRecentDevices(dataSource, 10); + + Assert.assertEquals(1, results.size()); + Assert.assertEquals((long) (DAY_SECONDS + 2), results.get(0).getDateAccessed().getTime() / 1000); + Assert.assertTrue("ID1".equalsIgnoreCase(results.get(0).getDeviceId())); + Assert.assertTrue("MAKE1".equalsIgnoreCase(results.get(0).getDeviceMake())); + Assert.assertTrue("MODEL1".equalsIgnoreCase(results.get(0).getDeviceModel())); + } + private static BlackboardArtifact getWebSearchArtifact(long artifactId, DataSource dataSource, String query, Long date) { try { return TskMockUtils.getArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY), artifactId, dataSource, @@ -708,8 +734,8 @@ public class UserActivitySummaryTest { * * @param artifactId The artifact id. * @param dataSource The datasource. - * @param dateRcvd The date received in seconds or null to exclude. - * @param dateSent The date sent in seconds or null to exclude. + * @param dateRcvd The date received in seconds or null to exclude. + * @param dateSent The date sent in seconds or null to exclude. * * @return The mock artifact. */ @@ -738,8 +764,8 @@ public class UserActivitySummaryTest { * * @param artifactId The artifact id. * @param dataSource The datasource. - * @param dateStart The date start in seconds or null to exclude. - * @param dateEnd The date end in seconds or null to exclude. + * @param dateStart The date start in seconds or null to exclude. + * @param dateEnd The date end in seconds or null to exclude. * * @return The mock artifact. */ @@ -768,8 +794,8 @@ public class UserActivitySummaryTest { * * @param artifactId The artifact id. * @param dataSource The datasource. - * @param type The account type. - * @param dateSent The date of the message in seconds. + * @param type The account type. + * @param dateSent The date of the message in seconds. */ private static BlackboardArtifact getMessageArtifact(long artifactId, DataSource dataSource, String type, Long dateTime) { List attributes = new ArrayList<>(); @@ -794,11 +820,11 @@ public class UserActivitySummaryTest { /** * Performs a test on UserActivitySummary.getRecentAccounts. * - * @param dataSource The datasource to use as parameter. - * @param count The count to use as a parameter. - * @param retArtifacts The artifacts to return from - * SleuthkitCase.getArtifacts. This method filters - * based on artifact type from the call. + * @param dataSource The datasource to use as parameter. + * @param count The count to use as a parameter. + * @param retArtifacts The artifacts to return from + * SleuthkitCase.getArtifacts. This method filters based on artifact type + * from the call. * @param expectedResults The expected results. * * @throws TskCoreException diff --git a/Testing/build.xml b/Testing/build.xml index f0ba6531a4..f8eff4b821 100644 --- a/Testing/build.xml +++ b/Testing/build.xml @@ -76,6 +76,10 @@ + + + + diff --git a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java index 6f6e04d7bd..f2aa476590 100644 --- a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java +++ b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,9 @@ import junit.framework.Test; import junit.framework.TestCase; import org.netbeans.jemmy.Timeouts; import org.netbeans.junit.NbModuleSuite; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbChoice; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbManager; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; /** * This test expects the following system properties to be set: img_path: The @@ -100,6 +103,21 @@ public class RegressionTest extends TestCase { public void setUp() { logger.info("######## " + AutopsyTestCases.getEscapedPath(System.getProperty("img_path")) + " #######"); Timeouts.setDefault("ComponentOperator.WaitComponentTimeout", 1000000); + + try { + if (Boolean.parseBoolean(System.getProperty("isMultiUser"))) { + // Set up a custom postgres CR using the configuration passed + // to system properties. + CentralRepoDbManager manager = new CentralRepoDbManager(); + manager.getDbSettingsPostgres().setHost(System.getProperty("crHost")); + manager.getDbSettingsPostgres().setPort(Integer.parseInt(System.getProperty("crPort"))); + manager.getDbSettingsPostgres().setUserName(System.getProperty("crUserName")); + manager.getDbSettingsPostgres().setPassword(System.getProperty("crPassword")); + manager.setupPostgresDb(CentralRepoDbChoice.POSTGRESQL_CUSTOM); + } + } catch (CentralRepoException ex) { + throw new RuntimeException("Error setting up multi user CR", ex); + } } /** diff --git a/test/script/regression.py b/test/script/regression.py index 9999a5c563..94b50c244f 100644 --- a/test/script/regression.py +++ b/test/script/regression.py @@ -471,6 +471,10 @@ class TestRunner(object): test_data.ant.append("-DsolrPort=" + str(test_config.solrPort)) test_data.ant.append("-DmessageServiceHost=" + test_config.messageServiceHost) test_data.ant.append("-DmessageServicePort=" + str(test_config.messageServicePort)) + test_data.ant.append("-DcrHost=" + str(test_config.crHost)) + test_data.ant.append("-DcrPort=" + str(test_config.crPort)) + test_data.ant.append("-DcrUserName=" + str(test_config.crUserName)) + test_data.ant.append("-DcrPassword=" + str(test_config.crPassword)) if test_data.isMultiUser: test_data.ant.append("-DisMultiUser=true") # Note: test_data has autopys_version attribute, but we couldn't see it from here. It's set after run ingest. @@ -854,6 +858,14 @@ class TestConfiguration(object): self.messageServicePort = parsed_config.getElementsByTagName("messageServicePort")[0].getAttribute("value").encode().decode("utf_8") if parsed_config.getElementsByTagName("multiUser_outdir"): self.multiUser_outdir = parsed_config.getElementsByTagName("multiUser_outdir")[0].getAttribute("value").encode().decode("utf_8") + if parsed_config.getElementsByTagName("crHost"): + self.crHost = parsed_config.getElementsByTagName("crHost")[0].getAttribute("value").encode().decode("utf_8") + if parsed_config.getElementsByTagName("crPort"): + self.crPort = parsed_config.getElementsByTagName("crPort")[0].getAttribute("value").encode().decode("utf_8") + if parsed_config.getElementsByTagName("crUserName"): + self.crUserName = parsed_config.getElementsByTagName("crUserName")[0].getAttribute("value").encode().decode("utf_8") + if parsed_config.getElementsByTagName("crPassword"): + self.crPassword = parsed_config.getElementsByTagName("crPassword")[0].getAttribute("value").encode().decode("utf_8") self._init_imgs(parsed_config) self._init_build_info(parsed_config) diff --git a/test/script/regression_utils.py b/test/script/regression_utils.py index 51fa3eb1c4..0c0229beb2 100644 --- a/test/script/regression_utils.py +++ b/test/script/regression_utils.py @@ -27,7 +27,7 @@ def make_os_path(platform, *dirs): path += str(dir).replace('\\', '/') + '/' return path_fix(path) elif platform == "win32": - return make_path(dirs) + return make_path(*dirs) else: print("Couldn't make path, because we only support Windows and Cygwin at this time.") sys.exit(1) diff --git a/thirdparty/iLeapp/ileapp.exe b/thirdparty/iLeapp/ileapp.exe index 8176b4f679..d17ab28f4d 100644 Binary files a/thirdparty/iLeapp/ileapp.exe and b/thirdparty/iLeapp/ileapp.exe differ