diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileBlackboardArtifactTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileBlackboardArtifactTagAction.java index 86696b0c63..dd2108c1fc 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileBlackboardArtifactTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileBlackboardArtifactTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2017 Basis Technology Corp. + * Copyright 2017-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,7 +38,6 @@ import org.openide.util.actions.Presenter; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifactTag; import org.sleuthkit.datamodel.TagName; @@ -54,7 +53,7 @@ import org.sleuthkit.datamodel.TskData; }) public class DeleteFileBlackboardArtifactTagAction extends AbstractAction implements Presenter.Popup { - private static final Logger LOGGER = Logger.getLogger(DeleteFileBlackboardArtifactTagAction.class.getName()); + private static final Logger logger = Logger.getLogger(DeleteFileBlackboardArtifactTagAction.class.getName()); private static final long serialVersionUID = 1L; private static final String MENU_TEXT = NbBundle.getMessage(DeleteFileBlackboardArtifactTagAction.class, @@ -98,17 +97,11 @@ public class DeleteFileBlackboardArtifactTagAction extends AbstractAction implem protected Void doInBackground() throws Exception { TagsManager tagsManager = Case.getCurrentCase().getServices().getTagsManager(); - // Pull the from the global context to avoid unnecessary calls - // to the database. - final Collection selectedFilesList - = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); - AbstractFile file = selectedFilesList.iterator().next(); - try { - LOGGER.log(Level.INFO, "Removing tag {0} from {1}", new Object[]{tagName.getDisplayName(), file.getName()}); //NON-NLS + logger.log(Level.INFO, "Removing tag {0} from {1}", new Object[]{tagName.getDisplayName(), artifactTag.getContent().getName()}); //NON-NLS tagsManager.deleteBlackboardArtifactTag(artifactTag); } catch (TskCoreException tskCoreException) { - LOGGER.log(Level.SEVERE, "Error untagging artifact", tskCoreException); //NON-NLS + logger.log(Level.SEVERE, "Error untagging artifact", tskCoreException); //NON-NLS Platform.runLater(() -> new Alert(Alert.AlertType.ERROR, Bundle.DeleteFileBlackboardArtifactTagAction_deleteTag_alert(artifactId)).show() ); @@ -122,7 +115,7 @@ public class DeleteFileBlackboardArtifactTagAction extends AbstractAction implem try { get(); } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Unexpected exception while untagging artifact", ex); //NON-NLS + logger.log(Level.SEVERE, "Unexpected exception while untagging artifact", ex); //NON-NLS } } }.execute(); diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java index 5c8d4abb74..c11ffd39fa 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2017 Basis Technology Corp. + * Copyright 2017-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -52,7 +52,7 @@ import org.sleuthkit.datamodel.TskData; }) public class DeleteFileContentTagAction extends AbstractAction implements Presenter.Popup { - private static final Logger LOGGER = Logger.getLogger(DeleteFileContentTagAction.class.getName()); + private static final Logger logger = Logger.getLogger(DeleteFileContentTagAction.class.getName()); private static final long serialVersionUID = 1L; private static final String MENU_TEXT = NbBundle.getMessage(DeleteFileContentTagAction.class, @@ -97,17 +97,11 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen protected Void doInBackground() throws Exception { TagsManager tagsManager = Case.getCurrentCase().getServices().getTagsManager(); - // Pull the from the global context to avoid unnecessary calls - // to the database. - final Collection selectedFilesList = - new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); - AbstractFile file = selectedFilesList.iterator().next(); - try { - LOGGER.log(Level.INFO, "Removing tag {0} from {1}", new Object[]{tagName.getDisplayName(), file.getName()}); //NON-NLS + logger.log(Level.INFO, "Removing tag {0} from {1}", new Object[]{tagName.getDisplayName(), contentTag.getContent().getName()}); //NON-NLS tagsManager.deleteContentTag(contentTag); } catch (TskCoreException tskCoreException) { - LOGGER.log(Level.SEVERE, "Error untagging file", tskCoreException); //NON-NLS + logger.log(Level.SEVERE, "Error untagging file", tskCoreException); //NON-NLS Platform.runLater(() -> new Alert(Alert.AlertType.ERROR, Bundle.DeleteFileContentTagAction_deleteTag_alert(fileId)).show() ); @@ -121,7 +115,7 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen try { get(); } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Unexpected exception while untagging file", ex); //NON-NLS + logger.log(Level.SEVERE, "Unexpected exception while untagging file", ex); //NON-NLS } } }.execute(); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java index 24aaf70d2e..4f25030fd3 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java @@ -29,6 +29,8 @@ import org.netbeans.swing.outline.Outline; import org.openide.nodes.Node; import java.awt.EventQueue; import java.io.File; +import java.io.IOException; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -207,28 +209,34 @@ class CaseBrowser extends javax.swing.JPanel implements ExplorerManager.Provider List nodeList = CoordinationService.getInstance().getNodeList(CoordinationService.CategoryNode.CASES); for (String node : nodeList) { - Path casePath = Paths.get(node); - File caseFolder = casePath.toFile(); - if (caseFolder.exists()) { - /* - * Search for '*.aut' files. - */ - File[] fileArray = caseFolder.listFiles(); - if (fileArray == null) { - continue; - } - String autFilePath = null; - for (File file : fileArray) { - String name = file.getName().toLowerCase(); - if (autFilePath == null && name.endsWith(".aut")) { - try { - caseList.add(new CaseMetadata(Paths.get(file.getAbsolutePath()))); - } catch (CaseMetadata.CaseMetadataException ex) { - LOGGER.log(Level.SEVERE, String.format("Error reading case metadata file '%s'.", autFilePath), ex); + Path casePath; + try { + casePath = Paths.get(node).toRealPath(LinkOption.NOFOLLOW_LINKS); + + File caseFolder = casePath.toFile(); + if (caseFolder.exists()) { + /* + * Search for '*.aut' files. + */ + File[] fileArray = caseFolder.listFiles(); + if (fileArray == null) { + continue; + } + String autFilePath = null; + for (File file : fileArray) { + String name = file.getName().toLowerCase(); + if (autFilePath == null && name.endsWith(".aut")) { + try { + caseList.add(new CaseMetadata(Paths.get(file.getAbsolutePath()))); + } catch (CaseMetadata.CaseMetadataException ex) { + LOGGER.log(Level.SEVERE, String.format("Error reading case metadata file '%s'.", autFilePath), ex); + } + break; } - break; } } + } catch (IOException ignore) { + //if a path could not be resolved to a real path do add it to the caseList } } return caseList; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java index ad642a9cbc..c098112efa 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java @@ -489,12 +489,16 @@ public class DataContentViewerOtherCases extends javax.swing.JPanel implements D corAttrInstances.addAll(getCorrelatedInstances(corAttr, dataSourceName, deviceId)); corAttrInstances.forEach((corAttrInstance) -> { - CorrelationAttribute newCeArtifact = new CorrelationAttribute( - corAttr.getCorrelationType(), - corAttr.getCorrelationValue() - ); - newCeArtifact.addInstance(corAttrInstance); - tableModel.addEamArtifact(newCeArtifact); + try { + CorrelationAttribute newCeArtifact = new CorrelationAttribute( + corAttr.getCorrelationType(), + corAttr.getCorrelationValue() + ); + newCeArtifact.addInstance(corAttrInstance); + tableModel.addEamArtifact(newCeArtifact); + } catch (EamDbException ex){ + LOGGER.log(Level.SEVERE, "Error creating correlation attribute", ex); + } }); } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java index 87478529f7..18af7f6c81 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java @@ -271,6 +271,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public void updateCase(CorrelationCase eamCase) throws EamDbException { + if(eamCase == null) { + throw new EamDbException("CorrelationCase argument is null"); + } + Connection conn = connect(); PreparedStatement preparedStatement = null; @@ -444,6 +448,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public CorrelationDataSource getDataSource(CorrelationCase correlationCase, String dataSourceDeviceId) throws EamDbException { + if(correlationCase == null) { + throw new EamDbException("CorrelationCase argument is null"); + } + Connection conn = connect(); CorrelationDataSource eamDataSourceResult = null; @@ -513,6 +521,16 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public void addArtifact(CorrelationAttribute eamArtifact) throws EamDbException { + if(eamArtifact == null) { + throw new EamDbException("CorrelationAttribute is null"); + } + if(eamArtifact.getCorrelationType() == null) { + throw new EamDbException("Correlation type is null"); + } + if(eamArtifact.getCorrelationValue() == null) { + throw new EamDbException("Correlation value is null"); + } + Connection conn = connect(); List eamInstances = eamArtifact.getInstances(); @@ -526,11 +544,21 @@ public abstract class AbstractSqlEamDb implements EamDb { sql.append("(case_id, data_source_id, value, file_path, known_status, comment) "); sql.append("VALUES ((SELECT id FROM cases WHERE case_uid=? LIMIT 1), "); sql.append("(SELECT id FROM data_sources WHERE device_id=? AND case_id=? LIMIT 1), ?, ?, ?, ?)"); - + try { preparedStatement = conn.prepareStatement(sql.toString()); for (CorrelationAttributeInstance eamInstance : eamInstances) { if (!eamArtifact.getCorrelationValue().isEmpty()) { + if(eamInstance.getCorrelationCase() == null) { + throw new EamDbException("CorrelationAttributeInstance has null case"); + } + if(eamInstance.getCorrelationDataSource() == null) { + throw new EamDbException("CorrelationAttributeInstance has null data source"); + } + if(eamInstance.getKnownStatus() == null) { + throw new EamDbException("CorrelationAttributeInstance has null known status"); + } + preparedStatement.setString(1, eamInstance.getCorrelationCase().getCaseUUID()); preparedStatement.setString(2, eamInstance.getCorrelationDataSource().getDeviceID()); preparedStatement.setInt(3, eamInstance.getCorrelationDataSource().getCaseID()); @@ -567,6 +595,9 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public List getArtifactInstancesByTypeValue(CorrelationAttribute.Type aType, String value) throws EamDbException { + if(aType == null) { + throw new EamDbException("Correlation type is null"); + } Connection conn = connect(); List artifactInstances = new ArrayList<>(); @@ -619,6 +650,12 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public List getArtifactInstancesByPath(CorrelationAttribute.Type aType, String filePath) throws EamDbException { + if(aType == null) { + throw new EamDbException("Correlation type is null"); + } + if(filePath == null) { + throw new EamDbException("Correlation value is null"); + } Connection conn = connect(); List artifactInstances = new ArrayList<>(); @@ -641,7 +678,7 @@ public abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement = conn.prepareStatement(sql.toString()); - preparedStatement.setString(1, filePath); + preparedStatement.setString(1, filePath.toLowerCase()); resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { artifactInstance = getEamArtifactInstanceFromResultSet(resultSet); @@ -670,6 +707,13 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public Long getCountArtifactInstancesByTypeValue(CorrelationAttribute.Type aType, String value) throws EamDbException { + if(aType == null) { + throw new EamDbException("Correlation type is null"); + } + if(value == null) { + throw new EamDbException("Correlation value is null"); + } + Connection conn = connect(); Long instanceCount = 0L; @@ -684,7 +728,7 @@ public abstract class AbstractSqlEamDb implements EamDb { try { preparedStatement = conn.prepareStatement(sql.toString()); - preparedStatement.setString(1, value); + preparedStatement.setString(1, value.toLowerCase()); resultSet = preparedStatement.executeQuery(); resultSet.next(); instanceCount = resultSet.getLong(1); @@ -701,6 +745,9 @@ public abstract class AbstractSqlEamDb implements EamDb { @Override public int getFrequencyPercentage(CorrelationAttribute corAttr) throws EamDbException { + if (corAttr == null) { + throw new EamDbException("Correlation attribute is null"); + } Double uniqueTypeValueTuples = getCountUniqueCaseDataSourceTuplesHavingTypeValue(corAttr.getCorrelationType(), corAttr.getCorrelationValue()).doubleValue(); Double uniqueCaseDataSourceTuples = getCountUniqueDataSources().doubleValue(); Double commonalityPercentage = uniqueTypeValueTuples / uniqueCaseDataSourceTuples * 100; @@ -719,6 +766,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public Long getCountUniqueCaseDataSourceTuplesHavingTypeValue(CorrelationAttribute.Type aType, String value) throws EamDbException { + if(aType == null) { + throw new EamDbException("Correlation type is null"); + } + Connection conn = connect(); Long instanceCount = 0L; @@ -840,6 +891,10 @@ public abstract class AbstractSqlEamDb implements EamDb { @Override public void prepareBulkArtifact(CorrelationAttribute eamArtifact) throws EamDbException { + if(eamArtifact.getCorrelationType() == null) { + throw new EamDbException("Correlation type is null"); + } + synchronized (bulkArtifacts) { bulkArtifacts.get(eamArtifact.getCorrelationType().getDbTableName()).add(eamArtifact); bulkArtifactsCount++; @@ -893,6 +948,17 @@ public abstract class AbstractSqlEamDb implements EamDb { for (CorrelationAttributeInstance eamInstance : eamInstances) { if (!eamArtifact.getCorrelationValue().isEmpty()) { + + if(eamInstance.getCorrelationCase() == null) { + throw new EamDbException("Correlation attribute instance has null case"); + } + if(eamInstance.getCorrelationDataSource() == null) { + throw new EamDbException("Correlation attribute instance has null data source"); + } + if(eamInstance.getKnownStatus()== null) { + throw new EamDbException("Correlation attribute instance has null known known status"); + } + bulkPs.setString(1, eamInstance.getCorrelationCase().getCaseUUID()); bulkPs.setString(2, eamInstance.getCorrelationDataSource().getDeviceID()); bulkPs.setInt(3, eamInstance.getCorrelationDataSource().getCaseID()); @@ -929,12 +995,16 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public void bulkInsertCases(List cases) throws EamDbException { - Connection conn = connect(); - + if(cases == null) { + throw new EamDbException("cases argument is null"); + } + if (cases.isEmpty()) { return; } + Connection conn = connect(); + int counter = 0; PreparedStatement bulkPs = null; try { @@ -1012,15 +1082,28 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public void setArtifactInstanceKnownStatus(CorrelationAttribute eamArtifact, TskData.FileKnown knownStatus) throws EamDbException { - Connection conn = connect(); - + if(eamArtifact == null) { + throw new EamDbException("Correlation attribute is null"); + } + if(knownStatus == null) { + throw new EamDbException("Known status is null"); + } if (1 != eamArtifact.getInstances().size()) { throw new EamDbException("Error: Artifact must have exactly one (1) Artifact Instance to set as notable."); // NON-NLS } - + List eamInstances = eamArtifact.getInstances(); CorrelationAttributeInstance eamInstance = eamInstances.get(0); + if(eamInstance.getCorrelationCase() == null) { + throw new EamDbException("Correlation case is null"); + } + if(eamInstance.getCorrelationDataSource() == null) { + throw new EamDbException("Correlation data source is null"); + } + + Connection conn = connect(); + PreparedStatement preparedUpdate = null; PreparedStatement preparedQuery = null; ResultSet resultSet = null; @@ -1103,6 +1186,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public List getArtifactInstancesKnownBad(CorrelationAttribute.Type aType, String value) throws EamDbException { + if(aType == null) { + throw new EamDbException("Correlation type is null"); + } + Connection conn = connect(); List artifactInstances = new ArrayList<>(); @@ -1153,6 +1240,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public Long getCountArtifactInstancesKnownBad(CorrelationAttribute.Type aType, String value) throws EamDbException { + if(aType == null) { + throw new EamDbException("Correlation type is null"); + } + Connection conn = connect(); Long badInstances = 0L; @@ -1197,6 +1288,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public List getListCasesHavingArtifactInstancesKnownBad(CorrelationAttribute.Type aType, String value) throws EamDbException { + if(aType == null) { + throw new EamDbException("Correlation type is null"); + } + Connection conn = connect(); Collection caseNames = new LinkedHashSet<>(); @@ -1313,7 +1408,7 @@ public abstract class AbstractSqlEamDb implements EamDb { @Override public boolean referenceSetIsValid(int referenceSetID, String setName, String version) throws EamDbException { EamGlobalSet refSet = this.getReferenceSetByID(referenceSetID); - if (refSet == null) { + if(refSet == null) { return false; } @@ -1382,6 +1477,9 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public boolean isArtifactKnownBadByReference(CorrelationAttribute.Type aType, String value) throws EamDbException { + if(aType == null) { + throw new EamDbException("null correlation type"); + } // TEMP: Only support file correlation type if (aType.getId() != CorrelationAttribute.FILES_TYPE_ID) { @@ -1424,6 +1522,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public long newOrganization(EamOrganization eamOrg) throws EamDbException { + if(eamOrg == null) { + throw new EamDbException("EamOrganization is null"); + } + Connection conn = connect(); ResultSet generatedKeys = null; PreparedStatement preparedStatement = null; @@ -1529,6 +1631,9 @@ public abstract class AbstractSqlEamDb implements EamDb { public EamOrganization getReferenceSetOrganization(int referenceSetID) throws EamDbException { EamGlobalSet globalSet = getReferenceSetByID(referenceSetID); + if(globalSet == null) { + throw new EamDbException("Reference set with ID " + referenceSetID + " not found"); + } return (getOrganizationByID(globalSet.getOrgID())); } @@ -1542,6 +1647,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public void updateOrganization(EamOrganization updatedOrganization) throws EamDbException { + if(updatedOrganization == null) { + throw new EamDbException("null updatedOrganization"); + } + Connection conn = connect(); PreparedStatement preparedStatement = null; String sql = "UPDATE organizations SET org_name = ?, poc_name = ?, poc_email = ?, poc_phone = ? WHERE id = ?"; @@ -1566,6 +1675,10 @@ public abstract class AbstractSqlEamDb implements EamDb { "AbstractSqlEamDb.deleteOrganization.errorDeleting.message=Error executing query when attempting to delete organization by id."}) @Override public void deleteOrganization(EamOrganization organizationToDelete) throws EamDbException { + if(organizationToDelete == null) { + throw new EamDbException("Organization to delete is null"); + } + Connection conn = connect(); PreparedStatement checkIfUsedStatement = null; ResultSet resultSet = null; @@ -1605,6 +1718,18 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public int newReferenceSet(EamGlobalSet eamGlobalSet) throws EamDbException { + if(eamGlobalSet == null){ + throw new EamDbException("EamGlobalSet argument is null"); + } + + if(eamGlobalSet.getFileKnownStatus() == null){ + throw new EamDbException("File known status on the EamGlobalSet is null"); + } + + if(eamGlobalSet.getType() == null){ + throw new EamDbException("Type on the EamGlobalSet is null"); + } + Connection conn = connect(); PreparedStatement preparedStatement1 = null; @@ -1666,8 +1791,11 @@ public abstract class AbstractSqlEamDb implements EamDb { preparedStatement1 = conn.prepareStatement(sql1); preparedStatement1.setInt(1, referenceSetID); resultSet = preparedStatement1.executeQuery(); - resultSet.next(); - return getEamGlobalSetFromResultSet(resultSet); + if(resultSet.next()) { + return getEamGlobalSetFromResultSet(resultSet); + } else { + return null; + } } catch (SQLException ex) { throw new EamDbException("Error getting reference set by id.", ex); // NON-NLS @@ -1689,6 +1817,11 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public List getAllReferenceSets(CorrelationAttribute.Type correlationType) throws EamDbException { + + if(correlationType == null){ + throw new EamDbException("Correlation type is null"); + } + List results = new ArrayList<>(); Connection conn = connect(); @@ -1723,6 +1856,13 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public void addReferenceInstance(EamGlobalFileInstance eamGlobalFileInstance, CorrelationAttribute.Type correlationType) throws EamDbException { + if(eamGlobalFileInstance.getKnownStatus() == null){ + throw new EamDbException("known status of EamGlobalFileInstance is null"); + } + if(correlationType == null){ + throw new EamDbException("Correlation type is null"); + } + Connection conn = connect(); PreparedStatement preparedStatement = null; @@ -1786,6 +1926,13 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public void bulkInsertReferenceTypeEntries(Set globalInstances, CorrelationAttribute.Type contentType) throws EamDbException { + if(contentType == null) { + throw new EamDbException("Null correlation type"); + } + if(globalInstances == null) { + throw new EamDbException("Null set of EamGlobalFileInstance"); + } + Connection conn = connect(); PreparedStatement bulkPs = null; @@ -1799,6 +1946,10 @@ public abstract class AbstractSqlEamDb implements EamDb { bulkPs = conn.prepareStatement(String.format(sql, EamDbUtil.correlationTypeToReferenceTableName(contentType))); for (EamGlobalFileInstance globalInstance : globalInstances) { + if(globalInstance.getKnownStatus() == null){ + throw new EamDbException("EamGlobalFileInstance with value " + globalInstance.getMD5Hash() + " has null known status"); + } + bulkPs.setInt(1, globalInstance.getGlobalSetID()); bulkPs.setString(2, globalInstance.getMD5Hash()); bulkPs.setByte(3, globalInstance.getKnownStatus().getFileKnownValue()); @@ -1808,7 +1959,7 @@ public abstract class AbstractSqlEamDb implements EamDb { bulkPs.executeBatch(); conn.commit(); - } catch (SQLException ex) { + } catch (SQLException | EamDbException ex) { try { conn.rollback(); } catch (SQLException ex2) { @@ -1833,6 +1984,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public List getReferenceInstancesByTypeValue(CorrelationAttribute.Type aType, String aValue) throws EamDbException { + if(aType == null) { + throw new EamDbException("correlation type is null"); + } + Connection conn = connect(); List globalFileInstances = new ArrayList<>(); @@ -1869,6 +2024,10 @@ public abstract class AbstractSqlEamDb implements EamDb { */ @Override public int newCorrelationType(CorrelationAttribute.Type newType) throws EamDbException { + if (newType == null) { + throw new EamDbException("null correlation type"); + } + Connection conn = connect(); PreparedStatement preparedStatement = null; @@ -1883,7 +2042,7 @@ public abstract class AbstractSqlEamDb implements EamDb { } else { insertSql = "INSERT INTO correlation_types(id, display_name, db_table_name, supported, enabled) VALUES (?, ?, ?, ?, ?)"; } - querySql = "SELECT id FROM correlation_types WHERE display_name=? AND db_table_name=?"; + querySql = "SELECT * FROM correlation_types WHERE display_name=? AND db_table_name=?"; try { preparedStatement = conn.prepareStatement(insertSql); @@ -2073,9 +2232,12 @@ public abstract class AbstractSqlEamDb implements EamDb { preparedStatement = conn.prepareStatement(sql); preparedStatement.setInt(1, typeId); resultSet = preparedStatement.executeQuery(); - resultSet.next(); - aType = getCorrelationTypeFromResultSet(resultSet); - return aType; + if(resultSet.next()) { + aType = getCorrelationTypeFromResultSet(resultSet); + return aType; + } else { + throw new EamDbException("Failed to find entry for correlation type ID = " + typeId); + } } catch (SQLException ex) { throw new EamDbException("Error getting correlation type by id.", ex); // NON-NLS @@ -2131,8 +2293,8 @@ public abstract class AbstractSqlEamDb implements EamDb { } CorrelationDataSource eamDataSource = new CorrelationDataSource( - resultSet.getInt("id"), resultSet.getInt("case_id"), + resultSet.getInt("id"), resultSet.getString("device_id"), resultSet.getString("name") ); @@ -2166,7 +2328,7 @@ public abstract class AbstractSqlEamDb implements EamDb { * * @throws SQLException when an expected column name is not in the resultSet */ - private CorrelationAttributeInstance getEamArtifactInstanceFromResultSet(ResultSet resultSet) throws SQLException { + private CorrelationAttributeInstance getEamArtifactInstanceFromResultSet(ResultSet resultSet) throws SQLException, EamDbException { if (null == resultSet) { return null; } @@ -2216,7 +2378,7 @@ public abstract class AbstractSqlEamDb implements EamDb { return eamGlobalSet; } - private EamGlobalFileInstance getEamGlobalFileInstanceFromResultSet(ResultSet resultSet) throws SQLException { + private EamGlobalFileInstance getEamGlobalFileInstanceFromResultSet(ResultSet resultSet) throws SQLException, EamDbException { if (null == resultSet) { return null; } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttribute.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttribute.java index 39968d7922..427bbc97bb 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttribute.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttribute.java @@ -28,7 +28,7 @@ import org.openide.util.NbBundle.Messages; /** * Represents a type and value pair that can be used for correlation. * CorrelationAttributeInstances store information about the actual - * occurences of the attribute. + * occurrences of the attribute. */ public class CorrelationAttribute implements Serializable { @@ -66,7 +66,10 @@ public class CorrelationAttribute implements Serializable { return DEFAULT_CORRELATION_TYPES; } - public CorrelationAttribute(Type correlationType, String correlationValue) { + public CorrelationAttribute(Type correlationType, String correlationValue) throws EamDbException { + if(correlationValue == null) { + throw new EamDbException ("Correlation value is null"); + } this.ID = ""; this.correlationType = correlationType; // Lower-case all values to normalize and improve correlation hits, going forward make sure this makes sense for all correlation types @@ -181,9 +184,12 @@ public class CorrelationAttribute implements Serializable { * Must start with a lowercase letter and only contain * lowercase letters, numbers, and '_' characters. * @param supported Is this Type currently supported - * @param enabled Is this Type currentl enabled. + * @param enabled Is this Type currently enabled. */ public Type(int id, String displayName, String dbTableName, Boolean supported, Boolean enabled) throws EamDbException { + if(dbTableName == null) { + throw new EamDbException("dbTableName is null"); + } this.id = id; this.displayName = displayName; this.dbTableName = dbTableName; @@ -195,7 +201,7 @@ public class CorrelationAttribute implements Serializable { } /** - * Constructior for custom types where we do not know the Type ID until + * Constructor for custom types where we do not know the Type ID until * the row has been entered into the correlation_types table * in the central repository. * @@ -204,7 +210,7 @@ public class CorrelationAttribute implements Serializable { * Must start with a lowercase letter and only contain * lowercase letters, numbers, and '_' characters. * @param supported Is this Type currently supported - * @param enabled Is this Type currentl enabled. + * @param enabled Is this Type currently enabled. */ public Type(String displayName, String dbTableName, Boolean supported, Boolean enabled) throws EamDbException { this(-1, displayName, dbTableName, supported, enabled); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java index f75364c580..5ca99abc7a 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java @@ -46,7 +46,7 @@ public class CorrelationAttributeInstance implements Serializable { public CorrelationAttributeInstance( CorrelationCase eamCase, CorrelationDataSource eamDataSource - ) { + ) throws EamDbException { this(-1, eamCase, eamDataSource, "", null, TskData.FileKnown.UNKNOWN); } @@ -54,7 +54,7 @@ public class CorrelationAttributeInstance implements Serializable { CorrelationCase eamCase, CorrelationDataSource eamDataSource, String filePath - ) { + ) throws EamDbException { this(-1, eamCase, eamDataSource, filePath, null, TskData.FileKnown.UNKNOWN); } @@ -63,7 +63,7 @@ public class CorrelationAttributeInstance implements Serializable { CorrelationDataSource eamDataSource, String filePath, String comment - ) { + ) throws EamDbException { this(-1, eamCase, eamDataSource, filePath, comment, TskData.FileKnown.UNKNOWN); } @@ -73,7 +73,7 @@ public class CorrelationAttributeInstance implements Serializable { String filePath, String comment, TskData.FileKnown knownStatus - ) { + ) throws EamDbException { this(-1, eamCase, eamDataSource, filePath, comment, knownStatus); } @@ -84,7 +84,11 @@ public class CorrelationAttributeInstance implements Serializable { String filePath, String comment, TskData.FileKnown knownStatus - ) { + ) throws EamDbException { + if(filePath == null) { + throw new EamDbException("file path is null"); + } + this.ID = ID; this.correlationCase = eamCase; this.correlationDataSource = eamDataSource; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationCase.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationCase.java index 79d94837ee..2d441881de 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationCase.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationCase.java @@ -50,6 +50,10 @@ public class CorrelationCase implements Serializable { * @param caseUUID Globally unique identifier * @param displayName */ + public CorrelationCase(String caseUUID, String displayName) { + this(-1, caseUUID, displayName); + } + CorrelationCase(int ID, String caseUUID, String displayName) { this(ID, caseUUID, null, displayName, DATE_FORMAT.format(new Date()), null, null, null, null, null); } @@ -156,7 +160,7 @@ public class CorrelationCase implements Serializable { /** * @return the database ID for the case or -1 if it is unknown (or not in the DB) */ - int getID() { + public int getID() { // @@@ Should probably have some lazy logic here to lead the ID from the DB if it is -1 return databaseId; } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java index 9bad6dbac7..9aa9fada32 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java @@ -38,6 +38,16 @@ public class CorrelationDataSource implements Serializable { private final String deviceID; //< Unique to its associated case (not necessarily globally unique) private final String name; + /** + * + * @param caseId + * @param deviceId + * @param name + */ + public CorrelationDataSource(int caseId, String deviceId, String name) { + this(caseId, -1, deviceId, name); + } + CorrelationDataSource(int caseId, int dataSourceId, String deviceId, diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamArtifactUtil.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamArtifactUtil.java index 171444fa42..c0810b52d3 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamArtifactUtil.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamArtifactUtil.java @@ -135,7 +135,8 @@ public class EamArtifactUtil { * @return the new EamArtifact, or null if one was not created because * bbArtifact did not contain the needed data */ - private static CorrelationAttribute getCorrelationAttributeFromBlackboardArtifact(CorrelationAttribute.Type correlationType, BlackboardArtifact bbArtifact) { + private static CorrelationAttribute getCorrelationAttributeFromBlackboardArtifact(CorrelationAttribute.Type correlationType, + BlackboardArtifact bbArtifact) throws EamDbException { String value = null; int artifactTypeID = bbArtifact.getArtifactTypeID(); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamGlobalFileInstance.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamGlobalFileInstance.java index 87d974f353..3c538e67c8 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamGlobalFileInstance.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamGlobalFileInstance.java @@ -36,7 +36,7 @@ public class EamGlobalFileInstance { int globalSetID, String MD5Hash, TskData.FileKnown knownStatus, - String comment) { + String comment) throws EamDbException { this(-1, globalSetID, MD5Hash, knownStatus, comment); } @@ -45,7 +45,13 @@ public class EamGlobalFileInstance { int globalSetID, String MD5Hash, TskData.FileKnown knownStatus, - String comment) { + String comment) throws EamDbException { + if(MD5Hash == null){ + throw new EamDbException("null MD5 hash"); + } + if(knownStatus == null){ + throw new EamDbException("null known status"); + } this.instanceID = instanceID; this.globalSetID = globalSetID; // Normalize hashes by lower casing @@ -111,7 +117,10 @@ public class EamGlobalFileInstance { /** * @param MD5Hash the MD5Hash to set */ - public void setMD5Hash(String MD5Hash) { + public void setMD5Hash(String MD5Hash) throws EamDbException { + if(MD5Hash == null){ + throw new EamDbException("null MD5 hash"); + } // Normalize hashes by lower casing this.MD5Hash = MD5Hash.toLowerCase(); } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java index 13c4fb00a7..d9d1ea2fa3 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java @@ -22,6 +22,7 @@ import java.io.File; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; +import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -165,6 +166,7 @@ public class SqliteEamDb extends AbstractSqlEamDb { connectionPool.setMaxIdle(-1); connectionPool.setMaxWaitMillis(1000); connectionPool.setValidationQuery(dbSettings.getValidationQuery()); + connectionPool.setConnectionInitSqls(Arrays.asList("PRAGMA foreign_keys = ON")); } /** diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties index 72ecac2866..9faba7a7b3 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties @@ -17,13 +17,13 @@ EamSqliteSettingsDialog.bnDatabasePathFileOpen.text=Browse... EamSqliteSettingsDialog.tfDatabasePath.toolTipText=Filename and path to store SQLite db file EamSqliteSettingsDialog.tfDatabasePath.text= EamSqliteSettingsDialog.lbDatabasePath.text=Database Path : -ImportHashDatabaseDialog.tfDatabaseName.tooltip=Name for this database -ImportHashDatabaseDialog.tfDatabaseVersion.tooltip.text=Database Version Number -ImportHashDatabaseDialog.tfDatabaseName.tooltip=Name for this database -ImportHashDatabaseDialog.tfDatabaseVersion.tooltip.text=Database Version Number +ImportHashDatabaseDialog.tfDatabaseName.tooltip=Name for this hash set +ImportHashDatabaseDialog.tfDatabaseVersion.tooltip.text=Hash Set Version Number +ImportHashDatabaseDialog.tfDatabaseName.tooltip=Name for this hash set +ImportHashDatabaseDialog.tfDatabaseVersion.tooltip.text=Hash Set Version Number GlobalSettingsPanel.tbOops.text= GlobalSettingsPanel.lbDatabaseSettings.text=Database Settings -GlobalSettingsPanel.bnImportDatabase.label=Import Hash Database +GlobalSettingsPanel.bnImportDatabase.label=Import Hash Set AddNewOrganizationDialog.lbPocPhone.text=Phone: AddNewOrganizationDialog.lbPocEmail.text=Email: AddNewOrganizationDialog.lbPocName.text=Name: diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java index a7a97e4b75..7a4df69e9d 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java @@ -453,7 +453,7 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont return; } - Content content = (selectedNode).getLookup().lookup(Content.class); + Content content = DataContentViewerUtility.getDefaultContent(selectedNode); if (content == null) { resetComponent(); return; diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerString.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerString.java index 14cb2eb2c1..936ff3c46b 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerString.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerString.java @@ -452,8 +452,7 @@ public class DataContentViewerString extends javax.swing.JPanel implements DataC return; } - Lookup lookup = selectedNode.getLookup(); - Content content = lookup.lookup(Content.class); + Content content = DataContentViewerUtility.getDefaultContent(selectedNode); if (content != null) { this.setDataView(content, 0); return; diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java new file mode 100755 index 0000000000..53491b407e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java @@ -0,0 +1,54 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2018 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.corecomponents; + +import org.sleuthkit.datamodel.Content; +import org.openide.nodes.Node; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * Utility classes for content viewers. + * In theory, this would live in the contentviewer package, + * but the initial method was needed only be viewers in + * corecomponents and therefore can stay out of public API. + */ +class DataContentViewerUtility { + /** + * Returns the first non-Blackboard Artifact from a Node. + * Needed for (at least) Hex and Strings that want to view + * all types of content (not just AbstractFile), but don't want + * to display an artifact unless that's the only thing there. + * Scenario is hash hit or interesting item hit. + * + * @param node Node passed into content viewer + * @return highest priority content or null if there is no content + */ + static Content getDefaultContent(Node node) { + Content bbContentSeen = null; + for (Content content : (node).getLookup().lookupAll(Content.class)) { + if (content instanceof BlackboardArtifact) { + bbContentSeen = content; + } + else { + return content; + } + } + return bbContentSeen; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java index 9117b32a14..795afd0838 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java @@ -19,22 +19,16 @@ package org.sleuthkit.autopsy.modules.filetypeid; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; -import java.util.logging.Level; import java.util.stream.Collectors; import org.apache.tika.Tika; import org.apache.tika.io.TikaInputStream; import org.apache.tika.mime.MimeTypes; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.services.Blackboard; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; @@ -278,52 +272,30 @@ public class FileTypeDetector { } /** - * Determines whether or not the a file matches a user-defined custom file - * type. If the file matches and corresponds to an interesting files type - * rule, this method has the side effect of creating an interesting files - * hit artifact and indexing that artifact for keyword search. + * Determines whether or not a file matches a user-defined custom file type. * * @param file The file to test. * - * @return The file type name string or null, if no match is detected. - * - * @throws TskCoreException + * @return The MIME type as a string if a match is found; otherwise null. */ private String detectUserDefinedType(AbstractFile file) { + String retValue = null; + for (FileType fileType : userDefinedFileTypes) { if (fileType.matches(file)) { - if (fileType.createInterestingFileHit()) { - try { - BlackboardArtifact artifact; - artifact = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT); - Collection attributes = new ArrayList<>(); - BlackboardAttribute setNameAttribute = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME, FileTypeIdModuleFactory.getModuleName(), fileType.getInterestingFilesSetName()); - attributes.add(setNameAttribute); - BlackboardAttribute ruleNameAttribute = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_CATEGORY, FileTypeIdModuleFactory.getModuleName(), fileType.getMimeType()); - attributes.add(ruleNameAttribute); - artifact.addAttributes(attributes); - try { - Case.getCurrentCase().getServices().getBlackboard().indexArtifact(artifact); - } catch (Blackboard.BlackboardException ex) { - logger.log(Level.SEVERE, String.format("Unable to index TSK_INTERESTING_FILE_HIT blackboard artifact %d (file obj_id=%d)", artifact.getArtifactID(), file.getId()), ex); //NON-NLS - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Unable to create TSK_INTERESTING_FILE_HIT artifact for file (obj_id=%d)", file.getId()), ex); //NON-NLS - } - } - return fileType.getMimeType(); + retValue = fileType.getMimeType(); + break; } } - return null; + return retValue; } /** - * Determines whether or not the a file matches a custom file type defined - * by Autopsy. + * Determines whether or not a file matches a custom file type defined by Autopsy. * * @param file The file to test. * - * @return The file type name string or null, if no match is detected. + * @return The MIME type as a string if a match is found; otherwise null. */ private String detectAutopsyDefinedType(AbstractFile file) { for (FileType fileType : autopsyDefinedFileTypes) { @@ -395,7 +367,7 @@ public class FileTypeDetector { * * @throws TskCoreException if detection is required and there is a problem * writing the result to the case database. - * @deprecated Use detectMIMEType instead, and call AbstractFile.setMIMEType + * @deprecated Use getMIMEType instead, and call AbstractFile.setMIMEType * and AbstractFile.save to save the result to the file object and the * database. */ @@ -419,7 +391,7 @@ public class FileTypeDetector { * @throws TskCoreException if detection is required and there is a problem * writing the result to the case database. * - * @deprecated Use detectMIMEType instead, and call AbstractFile.setMIMEType + * @deprecated Use getMIMEType instead, and call AbstractFile.setMIMEType * and AbstractFile.save to save the result to the file object and the * database. */ @@ -441,7 +413,7 @@ public class FileTypeDetector { * were uncertain, octet-stream is returned. * * @throws TskCoreException - * @deprecated Use detectMIMEType instead. + * @deprecated Use getMIMEType instead. */ @Deprecated public String detect(AbstractFile file) throws TskCoreException { diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdIngestModule.java index 8e6b3a58b2..be0bcdfbb3 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdIngestModule.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2013-2015 Basis Technology Corp. + * + * Copyright 2013-2018 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. @@ -18,9 +18,14 @@ */ package org.sleuthkit.autopsy.modules.filetypeid; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.logging.Level; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.services.Blackboard; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.FileIngestModule; import org.sleuthkit.autopsy.ingest.IngestJobContext; @@ -29,13 +34,16 @@ import org.sleuthkit.autopsy.ingest.IngestServices; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.autopsy.ingest.IngestModule.ProcessResult; import org.sleuthkit.autopsy.ingest.IngestModuleReferenceCounter; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; /** * Detects the type of a file based on signature (magic) values. Posts results * to the blackboard. */ @NbBundle.Messages({ - "CannotRunFileTypeDetection=Unable to run file type detection." + "CannotRunFileTypeDetection=Unable to run file type detection." }) public class FileTypeIdIngestModule implements FileIngestModule { @@ -91,7 +99,12 @@ public class FileTypeIdIngestModule implements FileIngestModule { */ try { long startTime = System.currentTimeMillis(); - fileTypeDetector.getMIMEType(file); + String mimeType = fileTypeDetector.getMIMEType(file); + file.setMIMEType(mimeType); + FileType fileType = detectUserDefinedFileType(file); + if (fileType != null && fileType.createInterestingFileHit()) { + createInterestingFileHit(file, fileType); + } addToTotals(jobId, (System.currentTimeMillis() - startTime)); return ProcessResult.OK; } catch (Exception e) { @@ -100,6 +113,57 @@ public class FileTypeIdIngestModule implements FileIngestModule { } } + /** + * Determines whether or not a file matches a user-defined custom file type. + * + * @param file The file to test. + * + * @return The file type if a match is found; otherwise null. + * + * @throws CustomFileTypesException If there is an issue getting an instance + * of CustomFileTypesManager. + */ + private FileType detectUserDefinedFileType(AbstractFile file) throws CustomFileTypesManager.CustomFileTypesException { + FileType retValue = null; + + CustomFileTypesManager customFileTypesManager = CustomFileTypesManager.getInstance(); + List fileTypesList = customFileTypesManager.getUserDefinedFileTypes(); + for (FileType fileType : fileTypesList) { + if (fileType.matches(file)) { + retValue = fileType; + break; + } + } + + return retValue; + } + + /** + * Create an Interesting File hit using the specified file type rule. + * + * @param file The file from which to generate an artifact. + * @param fileType The file type rule for categorizing the hit. + */ + private void createInterestingFileHit(AbstractFile file, FileType fileType) { + try { + BlackboardArtifact artifact; + artifact = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT); + Collection attributes = new ArrayList<>(); + BlackboardAttribute setNameAttribute = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME, FileTypeIdModuleFactory.getModuleName(), fileType.getInterestingFilesSetName()); + attributes.add(setNameAttribute); + BlackboardAttribute ruleNameAttribute = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_CATEGORY, FileTypeIdModuleFactory.getModuleName(), fileType.getMimeType()); + attributes.add(ruleNameAttribute); + artifact.addAttributes(attributes); + try { + Case.getCurrentCase().getServices().getBlackboard().indexArtifact(artifact); + } catch (Blackboard.BlackboardException ex) { + logger.log(Level.SEVERE, String.format("Unable to index TSK_INTERESTING_FILE_HIT blackboard artifact %d (file obj_id=%d)", artifact.getArtifactID(), file.getId()), ex); //NON-NLS + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format("Unable to create TSK_INTERESTING_FILE_HIT artifact for file (obj_id=%d)", file.getId()), ex); //NON-NLS + } + } + @Override public void shutDown() { /** diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/AddContentToHashDbAction.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/AddContentToHashDbAction.java index 5ec70ce3b9..6488661987 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/AddContentToHashDbAction.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/AddContentToHashDbAction.java @@ -149,7 +149,7 @@ final class AddContentToHashDbAction extends AbstractAction implements Presenter if (null != md5Hash) { // don't let them add the hash for an empty file to the DB if (HashUtility.isNoDataMd5(md5Hash)) { //NON-NLS - Logger.getLogger(AddContentToHashDbAction.class.getName()).log(Level.INFO, "Not adding " + file.getName() + " to database (empty content)"); //NON-NLS + Logger.getLogger(AddContentToHashDbAction.class.getName()).log(Level.INFO, "Not adding " + file.getName() + " to hash set (empty content)"); //NON-NLS JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(), "AddContentToHashDbAction.addFilesToHashSet.unableToAddFileEmptyMsg", @@ -162,7 +162,7 @@ final class AddContentToHashDbAction extends AbstractAction implements Presenter try { hashSet.addHashes(file); } catch (TskCoreException ex) { - Logger.getLogger(AddContentToHashDbAction.class.getName()).log(Level.SEVERE, "Error adding to hash database", ex); //NON-NLS + Logger.getLogger(AddContentToHashDbAction.class.getName()).log(Level.SEVERE, "Error adding to hash set", ex); //NON-NLS JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(), "AddContentToHashDbAction.addFilesToHashSet.unableToAddFileMsg", diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties index 7e4b10fc5d..b1600cd89d 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties @@ -1,11 +1,11 @@ OpenIDE-Module-Display-Category=Ingest Module OpenIDE-Module-Long-Description=\ - Hash Database ingest module. \n\n\ - The ingest module analyzes files in the disk image and marks them as "known" (based on NSRL database lookup for "known" files) and "bad / interesting" (based on one or more databases supplied by the user).\n\n\ - The module also contains additional non-ingest tools that are integrated in the GUI, such as file lookup by hash and hash database configuration. + Hash Set ingest module. \n\n\ + The ingest module analyzes files in the disk image and marks them as "known" (based on NSRL hashset lookup for "known" files) and "bad / interesting" (based on one or more hash sets supplied by the user).\n\n\ + The module also contains additional non-ingest tools that are integrated in the GUI, such as file lookup by hash and hash set configuration. OpenIDE-Module-Name=HashDatabases -OptionsCategory_Name_HashDatabase=Hash Databases -OptionsCategory_Keywords_HashDatabase=Hash Databases +OptionsCategory_Name_HashDatabase=Hash Sets +OptionsCategory_Keywords_HashDatabase=Hash Sets HashDbSearchPanel.hashTable.columnModel.title0=MD5 Hashes HashDbSearchPanel.addButton.text=Add Hash HashDbSearchPanel.hashField.text= @@ -16,11 +16,11 @@ HashDbSearchPanel.titleLabel.text=Search for files with the following MD5 hash(e HashDbSearchPanel.errorField.text=Error: Not all files have been hashed. HashDbSearchPanel.saveBox.text=Remember Hashes HashDbSearchPanel.cancelButton.text=Cancel -OpenIDE-Module-Short-Description=Hash Database Ingest Module and hash db tools +OpenIDE-Module-Short-Description=Hash Set Ingest Module and hash set tools HashDbImportDatabaseDialog.jLabel1.text=Name: HashDbImportDatabaseDialog.databasePathTextField.text= HashDbImportDatabaseDialog.knownBadRadioButton.text=Notable -HashDbImportDatabaseDialog.jLabel2.text=Type of database\: +HashDbImportDatabaseDialog.jLabel2.text=Type of hash set\: HashDbImportDatabaseDialog.okButton.text=OK HashDbImportDatabaseDialog.cancelButton.text=Cancel HashDbCreateDatabaseDialog.jLabel2.text=Type: @@ -28,13 +28,13 @@ HashDbCreateDatabaseDialog.knownBadRadioButton.text=Notable HashDbCreateDatabaseDialog.cancelButton.text=Cancel ModalNoButtons.CURRENTDB_LABEL.text=(CurrentDb) ModalNoButtons.CURRENTLYON_LABEL.text=Currently Indexing x of y -ModalNoButtons.GO_GET_COFFEE_LABEL.text=Hash databases are currently being indexed, this may take some time. +ModalNoButtons.GO_GET_COFFEE_LABEL.text=Hash sets are currently being indexed, this may take some time. ModalNoButtons.CANCEL_BUTTON.text=Cancel HashDbImportDatabaseDialog.knownRadioButton.text=Known (NSRL or other) HashDbCreateDatabaseDialog.knownRadioButton.text=Known HashDbCreateDatabaseDialog.saveAsButton.text=Save As... HashDbCreateDatabaseDialog.hashSetNameTextField.text= -HashDbImportDatabaseDialog.jLabel3.text=Database Path: +HashDbImportDatabaseDialog.jLabel3.text=Hash Set Path: HashDbCreateDatabaseDialog.sendIngestMessagesCheckbox.text=Send ingest inbox messages for each hit HashDbImportDatabaseDialog.sendIngestMessagesCheckbox.text=Send ingest inbox message for each hit HashDbImportDatabaseDialog.hashSetNameTextField.text= @@ -42,17 +42,17 @@ HashDbImportDatabaseDialog.openButton.text=Open... HashDbCreateDatabaseDialog.jLabel3.text=Name: HashDbCreateDatabaseDialog.okButton.text=OK HashDbCreateDatabaseDialog.databasePathTextField.text= -AddContentToHashDbAction.ContentMenu.noHashDbsConfigd=No hash databases configured -AddContentToHashDbAction.ContentMenu.createDbItem=Create database... -AddContentToHashDbAction.addFilesToHashSet.addToHashDbErr1.text=Add to Hash Database Error -AddContentToHashDbAction.addFilesToHashSet.addToHashDbErr2.text=Add to Hash Database Error -AddContentToHashDbAction.addFilesToHashSet.addToHashDbErr3.text=Add to Hash Database Error -AddContentToHashDbAction.addFilesToHashSet.unableToAddFileMsg=Unable to add {0} to the hash database. -AddContentToHashDbAction.addFilesToHashSet.unableToAddFileEmptyMsg=Unable to add {0} to the hash database. File has no content. -AddContentToHashDbAction.addFilesToHashSet.unableToAddFileSzMsg=Unable to add the {0} to the hash database. Hashes have not been calculated. Please configure and run an appropriate ingest module. +AddContentToHashDbAction.ContentMenu.noHashDbsConfigd=No hash sets configured +AddContentToHashDbAction.ContentMenu.createDbItem=Create hash set... +AddContentToHashDbAction.addFilesToHashSet.addToHashDbErr1.text=Add to Hash Set Error +AddContentToHashDbAction.addFilesToHashSet.addToHashDbErr2.text=Add to Hash Set Error +AddContentToHashDbAction.addFilesToHashSet.addToHashDbErr3.text=Add to Hash Set Error +AddContentToHashDbAction.addFilesToHashSet.unableToAddFileMsg=Unable to add {0} to the hash set. +AddContentToHashDbAction.addFilesToHashSet.unableToAddFileEmptyMsg=Unable to add {0} to the hash set. File has no content. +AddContentToHashDbAction.addFilesToHashSet.unableToAddFileSzMsg=Unable to add the {0} to the hash set. Hashes have not been calculated. Please configure and run an appropriate ingest module. HashDatabaseOptionsPanelController.moduleErr=Module Error HashDatabaseOptionsPanelController.moduleErrMsg=A module caused an error listening to HashDatabaseOptionsPanelController updates. See log to determine which module. Some data could be incomplete. -HashDbConfigPanel.noSelectionText=No database selected +HashDbConfigPanel.noSelectionText= HashDbConfigPanel.errorGettingPathText=Error occurred getting path HashDbConfigPanel.errorGettingIndexStatusText=Error occurred getting status HashDbConfigPanel.indexButtonText.index=Index @@ -62,35 +62,33 @@ HashDbConfigPanel.indexStatusText.indexOnly=Index only HashDbConfigPanel.indexStatusText.indexed=Indexed HashDbConfigPanel.indexButtonText.reIndex=Re-Index HashDbConfigPanel.indexStatusText.noIndex=No index -HashDbConfigPanel.dbsNotIndexedMsg=The following databases are not indexed, would you like to index them now? \n {0} -HashDbConfigPanel.dbNotIndexedMsg=The following database is not indexed, would you like to index it now? \n{0} -HashDbConfigPanel.unindexedDbsMsg=Unindexed databases -HashDbConfigPanel.allUnindexedDbsRmFromListMsg=All unindexed databases will be removed from the list +HashDbConfigPanel.dbsNotIndexedMsg=The following hash sets are not indexed, would you like to index them now? \n {0} +HashDbConfigPanel.dbNotIndexedMsg=The following hash set is not indexed, would you like to index it now? \n{0} +HashDbConfigPanel.unindexedDbsMsg=Unindexed hash sets +HashDbConfigPanel.allUnindexedDbsRmFromListMsg=All unindexed hash sets will be removed from the list HashDbConfigPanel.nameColLbl=Name HashDbConfigPanel.editingCellsNotSupportedMsg=Editing of cells is not supported -HashDbConfigPanel.deleteDbActionConfirmMsg=This will remove the hash database for all cases. Do you want to proceed? -HashDbConfigPanel.deleteDbActionMsg=Delete Hash Database from Configuration HashDbCreateDatabaseDialog.defaultFileName=hashset -HashDbCreateDatabaseDialog.createHashDbMsg=Create Hash Database -HashDbCreateDatabaseDialog.hashDbMustHaveFileExtensionMsg=The hash database file must have a .{0} extension. +HashDbCreateDatabaseDialog.createHashDbMsg=Create Hash Set +HashDbCreateDatabaseDialog.hashDbMustHaveFileExtensionMsg=The hash set file must have a .{0} extension. HashDbCreateDatabaseDialog.fileNameErr=File Name Error HashDbCreateDatabaseDialog.fileNameAlreadyExistsMsg=A file with this name already exists. Please choose a new file name. HashDbCreateDatabaseDialog.fileExistsErr=File Already Exists Error HashDbCreateDatabaseDialog.mustEnterHashSetNameMsg=A hash set name must be entered. -HashDbCreateDatabaseDialog.createHashDbErr=Create Hash Database Error -HashDbCreateDatabaseDialog.mustEnterHashDbPathMsg=A database path must be entered. -HashDbCreateDatabaseDialog.errMsg.hashDbCreationErr=Hash database creation error -HashDbCreateDatabaseDialog.cannotCreateFileAtLocMsg=Cannot create a hash database file at the selected location. -HashDbCreateDatabaseDialog.failedToCreateHashDbMsg=Failed to create the hash database. -HashDbImportDatabaseDialog.importHashDbMsg=Import Hash Database -HashDbImportDatabaseDialog.fileNameExtFilter.text=Hash Database File -HashDbImportDatabaseDialog.failedToGetDbPathMsg=Failed to get the path of the selected database. -HashDbImportDatabaseDialog.importHashDbErr=Import Hash Database Error -HashDbImportDatabaseDialog.mustSelectHashDbFilePathMsg=A hash database file path must be selected. -HashDbImportDatabaseDialog.hashDbDoesNotExistMsg=The selected hash database does not exist. -HashDbImportDatabaseDialog.errorMessage.failedToOpenHashDbMsg=Failed to open hash database at {0}. +HashDbCreateDatabaseDialog.createHashDbErr=Create Hash Set Error +HashDbCreateDatabaseDialog.mustEnterHashDbPathMsg=A hash set path must be entered. +HashDbCreateDatabaseDialog.errMsg.hashDbCreationErr=Hash set creation error +HashDbCreateDatabaseDialog.cannotCreateFileAtLocMsg=Cannot create a hash set file at the selected location. +HashDbCreateDatabaseDialog.failedToCreateHashDbMsg=Failed to create the hash set. +HashDbImportDatabaseDialog.importHashDbMsg=Import Hash Set +HashDbImportDatabaseDialog.fileNameExtFilter.text=Hash Set File +HashDbImportDatabaseDialog.failedToGetDbPathMsg=Failed to get the path of the selected hash set. +HashDbImportDatabaseDialog.importHashDbErr=Import Hash Set Error +HashDbImportDatabaseDialog.mustSelectHashDbFilePathMsg=A hash set file path must be selected. +HashDbImportDatabaseDialog.hashDbDoesNotExistMsg=The selected hash set does not exist. +HashDbImportDatabaseDialog.errorMessage.failedToOpenHashDbMsg=Failed to open hash set at {0}. HashDbIngestModule.moduleName=Hash Lookup -HashDbIngestModule.moduleDescription=Identifies known and notable files using supplied hash databases, such as a standard NSRL database. +HashDbIngestModule.moduleDescription=Identifies known and notable files using supplied hash sets, such as a standard NSRL hash set. HashDbIngestModule.fileReadErrorMsg=Read Error\: {0} HashDbIngestModule.calcHashValueErr=Error encountered while calculating the hash value for {0}. HashDbIngestModule.hashLookupErrorMsg=Hash Lookup Error\: {0} @@ -99,25 +97,25 @@ HashDbIngestModule.lookingUpKnownBadHashValueErr=Error encountered while looking HashDbIngestModule.lookingUpKnownHashValueErr=Error encountered while looking up known hash value for {0}. HashDbIngestModule.postToBB.fileName=File Name HashDbIngestModule.postToBB.md5Hash=MD5 Hash -HashDbIngestModule.postToBB.hashsetName=Hashset Name +HashDbIngestModule.postToBB.hashsetName=Hash Set Name HashDbIngestModule.postToBB.knownBadMsg=Notable\: {0} HashDbIngestModule.complete.knownBadsFound=Notables found\: HashDbIngestModule.complete.totalCalcTime=Total Calculation Time HashDbIngestModule.complete.totalLookupTime=Total Lookup Time -HashDbIngestModule.complete.databasesUsed=Databases Used\: +HashDbIngestModule.complete.databasesUsed=Hash Sets Used\: HashDbIngestModule.complete.hashLookupResults=Hash Lookup Results HashDbManager.moduleErrorListeningToUpdatesMsg=A module caused an error listening to HashDbManager updates. See log to determine which module. Some data could be incomplete. HashDbManager.replacingDuplicateHashsetNameMsg=Duplicate hash set name {0} found.\nReplacing with {1}. -HashDbManager.openHashDbErr=Open Hash Database Error -HashDbManager.unableToOpenHashDbMsg=Unable to open {0} hash database. +HashDbManager.openHashDbErr=Open Hash Set Error +HashDbManager.unableToOpenHashDbMsg=Unable to open {0} hash set. HashDbManager.savedBackupOfOldConfigMsg={0}\nA backup copy of the old configuration has been saved as\n{1} -HashDbManager.baseMessage.updatedFormatHashDbConfig=The format of the hash database configuration file has been updated. +HashDbManager.baseMessage.updatedFormatHashDbConfig=The format of the hash set configuration file has been updated. HashDbManager.msgBoxTitle.confFileFmtChanged=Configuration File Format Changed -HashDbManager.dlgMsg.dbNotFoundAtLoc=Database {0} could not be found at location\n{1}\nWould you like to search for the file? -HashDbManager.dlgTitle.MissingDb=Missing Database +HashDbManager.dlgMsg.dbNotFoundAtLoc=Hash set {0} could not be found at location\n{1}\nWould you like to search for the file? +HashDbManager.dlgTitle.MissingDb=Missing Hash Set HashDbManager.progress.indexingHashSet=Indexing {0} -HashDbManager.dlgMsg.errorIndexingHashSet=Error indexing {0} hash database. -HashDbManager.hashDbIndexingErr=Hash Database Indexing Error +HashDbManager.dlgMsg.errorIndexingHashSet=Error indexing {0} hash set. +HashDbManager.hashDbIndexingErr=Hash Set Indexing Error HashDbPanelSearchAction.actionName=File Search by MD5 Hash HashDbSearchAction.dlgMsg.noFilesHaveMD5Calculated=No files currently have an MD5 hash calculated, run HashDB ingest first. HashDbSearchManager.MD5HashSearch=MD5 Hash Search @@ -130,56 +128,56 @@ HashDbSearchPanel.errorText.invalidMD5HashMsg=Error\: That is not a valid MD5 ha HashDbSearchThread.progress.cancellingSearch={0} (Cancelling...) HashDbSearchThread.name.searching=Searching HashDbSearchThread.noMoreFilesWithMD5Msg=No other files with the same MD5 hash were found. -ModalNoButtons.indexingDbsTitle=Indexing databases -ModalNoButtons.indexingDbTitle=Indexing database -ModalNoButtons.exitHashDbIndexingMsg=You are about to exit out of indexing your hash databases. \n\ +ModalNoButtons.indexingDbsTitle=Indexing hash sets +ModalNoButtons.indexingDbTitle=Indexing hash set +ModalNoButtons.exitHashDbIndexingMsg=You are about to exit out of indexing your hash sets. \n\ The generated index will be left unusable. If you choose to continue,\n\ please delete the corresponding -md5.idx file in the hash folder.\n\ Exit indexing? ModalNoButtons.dlgTitle.unfinishedIndexing=Unfinished Indexing -ModalNoButtons.indexThis.currentlyIndexing1Db=Currently indexing 1 database +ModalNoButtons.indexThis.currentlyIndexing1Db=Currently indexing 1 hash set ModalNoButtons.indexThese.currentlyIndexing1OfNDbs=Currently indexing 1 of {0} ModalNoButtons.propChg.currentlyIndexingXofN=Currently indexing {0} of {1} -HashDbManager.duplicateHashSetNameExceptionMsg=The hash set name ''{0}'' has already been used for another hash database. -HashDbManager.hashDbDoesNotExistExceptionMsg=No hash database found at\n{0} +HashDbManager.duplicateHashSetNameExceptionMsg=The hash set name ''{0}'' has already been used for another hash set. +HashDbManager.hashDbDoesNotExistExceptionMsg=No hash set found at\n{0} HashDbManager.hashDbFileExistsExceptionMsg=A file already exists at\n{0} -HashDbManager.hashDbAlreadyAddedExceptionMsg=The hash database at\n{0}\nhas already been created or imported. -HashDbManager.illegalHashDbFileNameExtensionMsg=The hash database file name must have a .{0} extension. +HashDbManager.hashDbAlreadyAddedExceptionMsg=The hash set at\n{0}\nhas already been created or imported. +HashDbManager.illegalHashDbFileNameExtensionMsg=The hash set file name must have a .{0} extension. HashDbManager.moduleErr=Module Error HashDbManager.knownBad.text=Notable HashDbManager.known.text=Known -HashDbManager.fileNameExtensionFilter.title=Hash Database File +HashDbManager.fileNameExtensionFilter.title=Hash Set File HashDbSearchAction.dlgMsg.title=File Search by MD5 Hash HashDbSearchAction.getName.text=Hash Search HashDbSearchPanel.dlgMsg.title=File Search by MD5 Hash -AddContentToHashDbAction.singleSelectionName=Add file to hash database -AddContentToHashDbAction.multipleSelectionName=Add files to hash database +AddContentToHashDbAction.singleSelectionName=Add file to hash set +AddContentToHashDbAction.multipleSelectionName=Add files to hash set HashDbManager.ingestRunningExceptionMsg=Ingest is ongoing; this service will be unavailable until it finishes. HashDbManager.saveErrorExceptionMsg=Error saving hash configuration -HashLookupSettingsPanel.jButton3.text=Import Database +HashLookupSettingsPanel.jButton3.text=Import Hash Set HashLookupSettingsPanel.jLabel6.text=Type: HashLookupSettingsPanel.jLabel4.text=Location: HashLookupSettingsPanel.jLabel2.text=Name: -HashLookupModuleSettingsPanel.alwaysCalcHashesCheckbox.text=Calculate MD5 even if no hash database is selected -HashLookupModuleSettingsPanel.knownHashDbsLabel.text=Select known hash databases to use: -HashLookupModuleSettingsPanel.knownBadHashDbsLabel.text=Select notable hash databases to use: +HashLookupModuleSettingsPanel.alwaysCalcHashesCheckbox.text=Calculate MD5 even if no hash set is selected +HashLookupModuleSettingsPanel.knownHashDbsLabel.text=Select known hash sets to use: +HashLookupModuleSettingsPanel.knownBadHashDbsLabel.text=Select notable hash sets to use: AddContentToHashDbAction.addFilesToHashSet.files=files AddContentToHashDbAction.addFilesToHashSet.file=file HashDbManager.errCreatingIndex.title=Error creating index HashDbManager.errCreatingIndex.msg=Error creating index\: {0} HashLookupModuleFactory.getIngestJobSettingsPanel.exception.msg=Expected settings argument to be instanceof HashLookupModuleSettings HashLookupModuleFactory.createFileIngestModule.exception.msg=Expected settings argument to be instanceof HashLookupModuleSettings -HashLookupModuleSettingsPanel.alwaysCalcHashesCheckbox.toolTipText=Calculate MD5 even if no hash database is selected +HashLookupModuleSettingsPanel.alwaysCalcHashesCheckbox.toolTipText=Calculate MD5 even if no hash set is selected HashDbSearchPanel.hashTable.defaultModel.title.text=MD5 Hashes -AddHashValuesToDatabaseDialog.JDialog.Title=Add Hashes to Database +AddHashValuesToDatabaseDialog.JDialog.Title=Add Hashes to Hash Set AddHashValuesToDatabaseDialog.instructionLabel.text_1=Paste MD5 hash values (one per line) below: AddHashValuesToDatabaseDialog.cancelButton.text_2=Cancel -AddHashValuesToDatabaseDialog.AddValuesToHashDatabaseButton.text_2=Add Hashes to Database +AddHashValuesToDatabaseDialog.AddValuesToHashDatabaseButton.text_2=Add Hashes to Hash Set AddHashValuesToDatabaseDialog.pasteFromClipboardButton.text_2=Paste From Clipboard AddHashValuesToDatabaseProgressDialog.okButton.text=OK AddHashValuesToDatabaseProgressDialog.statusLabel.text=status -AddHashValuesToDatabaseProgressDialog.title=Add Hashes to Database Progress -AddHashValuesToDatabaseDialog.title=Add Hashes to Database +AddHashValuesToDatabaseProgressDialog.title=Add Hashes to Hash Set Progress +AddHashValuesToDatabaseDialog.title=Add Hashes to Hash Set AddHashValuesToDatabaseProgressDialog.showErrorsButton.text=Show Errors AddHashValuesToDatabaseProgressDialog.addHashValuesToDatabase.parsing=Parsing text for MD5 hashes... AddHashValuesToDatabaseProgressDialog.addHashValuesToDatabase.invalidHash=The input contains invalid hash. @@ -187,47 +185,47 @@ AddHashValuesToDatabaseProgressDialog.addHashValuesToDatabase.invaliHash.msg=Inv AddHashValuesToDatabaseProgressDialog.addHashValuesToDatabase.noHashesToAdd=There are no hashes to add. AddHashValuesToDatabaseProgressDialog.addHashValuesToDatabase.success={0} Hashes added successfully. AddHashValuesToDatabaseProgressDialog.addHashValuesToDatabase.errorAddingValidHash=There is an error adding valid hashes. -AddHashValuesToDatabaseProgressDialog.addHashValuesToDatabase.errorAddingValidHash.msg=Error adding valid hashes to the database: +AddHashValuesToDatabaseProgressDialog.addHashValuesToDatabase.errorAddingValidHash.msg=Error adding valid hashes to the hash set: HashLookupSettingsPanel.ingestWarningLabel.text=Ingest is ongoing, some settings will be unavailable until it finishes. -HashLookupSettingsPanel.addHashesToDatabaseButton.text=Add Hashes to Database -HashLookupSettingsPanel.indexPathLabel.text=No database selected +HashLookupSettingsPanel.addHashesToDatabaseButton.text=Add Hashes to Hash Set +HashLookupSettingsPanel.indexPathLabel.text= HashLookupSettingsPanel.indexPathLabelLabel.text=Index Path: HashLookupSettingsPanel.createDatabaseButton.toolTipText= -HashLookupSettingsPanel.createDatabaseButton.text=New database +HashLookupSettingsPanel.createDatabaseButton.text=New Hash Set HashLookupSettingsPanel.optionsLabel.text=Options HashLookupSettingsPanel.informationLabel.text=Information HashLookupSettingsPanel.sendIngestMessagesCheckBox.text=Send ingest inbox message for each hit HashLookupSettingsPanel.indexButton.text=Index HashLookupSettingsPanel.indexLabel.text=Index Status: -HashLookupSettingsPanel.hashDbIndexStatusLabel.text=No database selected -HashLookupSettingsPanel.hashDbTypeLabel.text=No database selected +HashLookupSettingsPanel.hashDbIndexStatusLabel.text= +HashLookupSettingsPanel.hashDbTypeLabel.text= HashLookupSettingsPanel.typeLabel.text=Type: -HashLookupSettingsPanel.locationLabel.text=Database Path: -HashLookupSettingsPanel.hashDbLocationLabel.text=No database selected -HashLookupSettingsPanel.hashDbNameLabel.text=No database selected +HashLookupSettingsPanel.locationLabel.text=Hash Set Path: +HashLookupSettingsPanel.hashDbLocationLabel.text= +HashLookupSettingsPanel.hashDbNameLabel.text= HashLookupSettingsPanel.nameLabel.text=Name: -HashLookupSettingsPanel.hashDatabasesLabel.text=Hash Databases: +HashLookupSettingsPanel.hashDatabasesLabel.text=Hash Sets: HashLookupSettingsPanel.importDatabaseButton.toolTipText= -HashLookupSettingsPanel.importDatabaseButton.text=Import database -HashLookupSettingsPanel.deleteDatabaseButton.text=Delete database -ImportHashDatabaseDialog.lbFilePath.text=Database Path: -ImportHashDatabaseDialog.tfDatabaseName.tooltip=Name for this database -ImportHashDatabaseDialog.tfDatabaseVersion.tooltip.text=Database Version Number -ImportHashDatabaseDialog.tfDatabaseName.tooltip=Name for this database -ImportHashDatabaseDialog.tfDatabaseVersion.tooltip.text=Database Version Number +HashLookupSettingsPanel.importDatabaseButton.text=Import Hash Set +HashLookupSettingsPanel.deleteDatabaseButton.text=Delete Hash Set +ImportHashDatabaseDialog.lbFilePath.text=Hash Set Path: +ImportHashDatabaseDialog.tfDatabaseName.tooltip=Name for this hash set +ImportHashDatabaseDialog.tfDatabaseVersion.tooltip.text=Hash Set Version Number +ImportHashDatabaseDialog.tfDatabaseName.tooltip=Name for this hash set +ImportHashDatabaseDialog.tfDatabaseVersion.tooltip.text=Hash Set Version Number ImportCentralRepoDbProgressDialog.lbProgress.text=Starting import... ImportCentralRepoDbProgressDialog.bnOk.text=OK ImportCentralRepoDbProgressDialog.bnCancel.text=Cancel HashLookupSettingsPanel.versionLabel.text_1=Version: -HashLookupSettingsPanel.hashDbVersionLabel.text_1=No database selected +HashLookupSettingsPanel.hashDbVersionLabel.text_1= HashLookupSettingsPanel.orgLabel.text_1=Organization: -HashLookupSettingsPanel.hashDbOrgLabel.text_1=No database selected +HashLookupSettingsPanel.hashDbOrgLabel.text_1= HashLookupSettingsPanel.readOnlyLabel.text_1=Read only: -HashLookupSettingsPanel.hashDbReadOnlyLabel.text_1=No database selected +HashLookupSettingsPanel.hashDbReadOnlyLabel.text_1= ImportCentralRepoDbProgressDialog.jLabel1.text=Importing hash set into the central repository HashDbImportDatabaseDialog.lbVersion.text=Version: HashDbImportDatabaseDialog.lbOrg.text=Source Organization: -HashDbImportDatabaseDialog.readOnlyCheckbox.text=Make database read-only +HashDbImportDatabaseDialog.readOnlyCheckbox.text=Make hash set read-only HashDbImportDatabaseDialog.orgButton.text=Manage Organizations HashDbImportDatabaseDialog.versionTextField.text=1.0 HashDbImportDatabaseDialog.fileTypeRadioButton.text=Local @@ -238,4 +236,4 @@ HashDbCreateDatabaseDialog.fileTypeRadioButton.text=Local HashDbCreateDatabaseDialog.centralRepoRadioButton.text=Remote (Central Repository) HashDbCreateDatabaseDialog.lbOrg.text=Source Organization: HashDbCreateDatabaseDialog.orgButton.text=Manage Organizations -HashDbCreateDatabaseDialog.databasePathLabel.text=Database Path: +HashDbCreateDatabaseDialog.databasePathLabel.text=Hash Set Path: diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle_ja.properties index 983b48e2a7..828b027bdb 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle_ja.properties @@ -58,8 +58,6 @@ HashDbConfigPanel.unindexedDbsMsg=\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u5316\u30 HashDbConfigPanel.allUnindexedDbsRmFromListMsg=\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u5316\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306f\u30ea\u30b9\u30c8\u304b\u3089\u524a\u9664\u3055\u308c\u307e\u3059 HashDbConfigPanel.nameColLbl=\u540d\u524d HashDbConfigPanel.editingCellsNotSupportedMsg=\u30bb\u30eb\u306f\u7de8\u96c6\u4e0d\u53ef\u3067\u3059 -HashDbConfigPanel.deleteDbActionConfirmMsg=\u5168\u3066\u306e\u30b1\u30fc\u30b9\u306b\u304a\u3051\u308b\u30cf\u30c3\u30b7\u30e5\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u3092\u524a\u9664\u3057\u307e\u3059\u3002\u5b9f\u884c\u3057\u307e\u3059\u304b\uff1f -HashDbConfigPanel.deleteDbActionMsg=\u8a2d\u5b9a\u304b\u3089\u30cf\u30c3\u30b7\u30e5\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u3092\u524a\u9664 HashDbCreateDatabaseDialog.createHashDbMsg=\u30cf\u30c3\u30b7\u30e5\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u3092\u4f5c\u6210 HashDbCreateDatabaseDialog.hashDbMustHaveFileExtensionMsg=\u30cf\u30c3\u30b7\u30e5\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30d5\u30a1\u30a4\u30eb\u306f .{0} \u306e\u62e1\u5f35\u5b50\u304c\u5fc5\u8981\u3067\u3059\u3002 HashDbCreateDatabaseDialog.fileNameErr=\u30d5\u30a1\u30a4\u30eb\u540d\u30a8\u30e9\u30fc diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDatabaseOptionsPanelController.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDatabaseOptionsPanelController.java index ed77c459c5..a4d85eef9b 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDatabaseOptionsPanelController.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDatabaseOptionsPanelController.java @@ -35,7 +35,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; keywordsCategory = "HashDatabase", id = "HashDatabase") // moved messages to Bundle.properties -//@org.openide.util.NbBundle.Messages({"OptionsCategory_Name_HashDatabase=Hash Database", "OptionsCategory_Keywords_HashDatabase=Hash Database"}) +//@org.openide.util.NbBundle.Messages({"OptionsCategory_Name_HashDatabase=Hash Set", "OptionsCategory_Keywords_HashDatabase=Hash Set"}) public final class HashDatabaseOptionsPanelController extends OptionsPanelController { private HashLookupSettingsPanel panel; diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java index 0c5d94277b..aff3e7d196 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbImportDatabaseDialog.java @@ -420,7 +420,7 @@ final class HashDbImportDatabaseDialog extends javax.swing.JDialog { } ModuleSettings.setConfigSetting(ModuleSettings.MAIN_SETTINGS, LAST_FILE_PATH_KEY, databaseFile.getParent()); } catch (IOException ex) { - Logger.getLogger(HashDbImportDatabaseDialog.class.getName()).log(Level.SEVERE, "Failed to get path of selected database", ex); //NON-NLS + Logger.getLogger(HashDbImportDatabaseDialog.class.getName()).log(Level.SEVERE, "Failed to get path of selected hash set", ex); //NON-NLS JOptionPane.showMessageDialog(this, NbBundle.getMessage(this.getClass(), "HashDbImportDatabaseDialog.failedToGetDbPathMsg")); diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java index 696a105416..022cda8a9c 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java @@ -51,9 +51,9 @@ import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskException; @NbBundle.Messages({ - "HashDbIngestModule.noKnownBadHashDbSetMsg=No notable hash database set.", + "HashDbIngestModule.noKnownBadHashDbSetMsg=No notable hash set.", "HashDbIngestModule.knownBadFileSearchWillNotExecuteWarn=Notable file search will not be executed.", - "HashDbIngestModule.noKnownHashDbSetMsg=No known hash database set.", + "HashDbIngestModule.noKnownHashDbSetMsg=No known hash set.", "HashDbIngestModule.knownFileSearchWillNotExecuteWarn=Known file search will not be executed." }) public class HashDbIngestModule implements FileIngestModule { @@ -95,7 +95,7 @@ public class HashDbIngestModule implements FileIngestModule { public void startUp(org.sleuthkit.autopsy.ingest.IngestJobContext context) throws IngestModuleException { jobId = context.getJobId(); if (!hashDbManager.verifyAllDatabasesLoadedCorrectly()) { - throw new IngestModuleException("Could not load all hash databases"); + throw new IngestModuleException("Could not load all hash sets"); } updateEnabledHashSets(hashDbManager.getKnownBadFileHashSets(), knownBadHashSets); updateEnabledHashSets(hashDbManager.getKnownFileHashSets(), knownHashSets); @@ -136,7 +136,7 @@ public class HashDbIngestModule implements FileIngestModule { enabledHashSets.add(db); } } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Error getting index status for " + db.getDisplayName()+ " hash database", ex); //NON-NLS + logger.log(Level.WARNING, "Error getting index status for " + db.getDisplayName()+ " hash set", ex); //NON-NLS } } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java index c032bdc83e..78981d4787 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java @@ -276,14 +276,14 @@ public class HashDbManager implements PropertyChangeListener { boolean readOnly) throws TskCoreException{ if(! EamDb.isEnabled()){ - throw new TskCoreException("Could not load central repository database " + hashSetName + " - central repository is not enabled"); + throw new TskCoreException("Could not load central repository hash set " + hashSetName + " - central repository is not enabled"); } CentralRepoHashSet db = new CentralRepoHashSet(hashSetName, version, referenceSetID, searchDuringIngest, sendIngestMessages, knownFilesType, readOnly); if(! db.isValid()){ - throw new TskCoreException("Error finding database " + hashSetName + " in central repository"); + throw new TskCoreException("Error finding hash set " + hashSetName + " in central repository"); } // Add the hash database to the collection @@ -320,7 +320,7 @@ public class HashDbManager implements PropertyChangeListener { hashSetPaths.add(indexPath); } } catch (TskCoreException ex) { - Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error getting index path of " + hashDb.getHashSetName() + " hash database after indexing", ex); //NON-NLS + Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error getting index path of " + hashDb.getHashSetName() + " hash set after indexing", ex); //NON-NLS } } } @@ -363,7 +363,7 @@ public class HashDbManager implements PropertyChangeListener { hashSetPaths.remove(hashDatabase.getIndexPath()); } } catch (TskCoreException ex) { - Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error getting index path of " + hashDatabase.getHashSetName() + " hash database when removing the database", ex); //NON-NLS + Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error getting index path of " + hashDatabase.getHashSetName() + " hash set when removing the hash set", ex); //NON-NLS } try { @@ -371,13 +371,13 @@ public class HashDbManager implements PropertyChangeListener { hashSetPaths.remove(hashDatabase.getDatabasePath()); } } catch (TskCoreException ex) { - Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error getting database path of " + hashDatabase.getHashSetName() + " hash database when removing the database", ex); //NON-NLS + Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error getting hash set path of " + hashDatabase.getHashSetName() + " hash set when removing the hash set", ex); //NON-NLS } try { hashDatabase.close(); } catch (TskCoreException ex) { - Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error closing " + hashDb.getHashSetName() + " hash database when removing the database", ex); //NON-NLS + Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error closing " + hashDb.getHashSetName() + " hash set when removing the hash set", ex); //NON-NLS } } @@ -480,7 +480,7 @@ public class HashDbManager implements PropertyChangeListener { updateableDbs.add(db); } } catch (TskCoreException ex) { - Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error checking updateable status of " + db.getHashSetName() + " hash database", ex); //NON-NLS + Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error checking updateable status of " + db.getHashSetName() + " hash set", ex); //NON-NLS } } return updateableDbs; @@ -532,7 +532,7 @@ public class HashDbManager implements PropertyChangeListener { try { ((SleuthkitHashSet)database).close(); } catch (TskCoreException ex) { - Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error closing " + database.getHashSetName() + " hash database", ex); //NON-NLS + Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error closing " + database.getHashSetName() + " hash set", ex); //NON-NLS } } } @@ -554,7 +554,7 @@ public class HashDbManager implements PropertyChangeListener { * * @param settings The settings to configure. */ - @Messages({"# {0} - database name", "HashDbManager.noDbPath.message=Couldn't get valid database path for: {0}", + @Messages({"# {0} - hash set name", "HashDbManager.noDbPath.message=Couldn't get valid hash set path for: {0}", "HashDbManager.centralRepoLoadError.message=Error loading central repository hash sets"}) private void configureSettings(HashLookupSettings settings) { allDatabasesLoadedCorrectly = true; @@ -578,7 +578,7 @@ public class HashDbManager implements PropertyChangeListener { } } } catch (TskCoreException ex) { - Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error opening hash database", ex); //NON-NLS + Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error opening hash set", ex); //NON-NLS JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(), "HashDbManager.unableToOpenHashDbMsg", hashDbInfo.getHashSetName()), @@ -592,7 +592,7 @@ public class HashDbManager implements PropertyChangeListener { try{ updateHashSetsFromCentralRepository(); } catch (TskCoreException ex){ - Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error opening hash database", ex); //NON-NLS + Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error opening hash set", ex); //NON-NLS JOptionPane.showMessageDialog(null, Bundle.HashDbManager_centralRepoLoadError_message(), @@ -615,7 +615,7 @@ public class HashDbManager implements PropertyChangeListener { allDatabasesLoadedCorrectly = true; } catch (HashLookupSettings.HashLookupSettingsException ex) { allDatabasesLoadedCorrectly = false; - logger.log(Level.SEVERE, "Could not overwrite hash database settings.", ex); + logger.log(Level.SEVERE, "Could not overwrite hash set settings.", ex); } } } @@ -1232,9 +1232,10 @@ public class HashDbManager implements PropertyChangeListener { } else { type = TskData.FileKnown.KNOWN; } - EamGlobalFileInstance fileInstance = new EamGlobalFileInstance(referenceSetID, file.getMd5Hash(), - type, comment); + try{ + EamGlobalFileInstance fileInstance = new EamGlobalFileInstance(referenceSetID, file.getMd5Hash(), + type, comment); EamDb.getInstance().addReferenceInstance(fileInstance,EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.FILES_TYPE_ID)); } catch (EamDbException ex){ throw new TskCoreException("Error adding hashes to " + getDisplayName(), ex); @@ -1259,8 +1260,12 @@ public class HashDbManager implements PropertyChangeListener { type = TskData.FileKnown.BAD; } else { type = TskData.FileKnown.KNOWN; - } - globalFileInstances.add(new EamGlobalFileInstance(referenceSetID, hashEntry.getMd5Hash(), type, hashEntry.getComment())); + } + try { + globalFileInstances.add(new EamGlobalFileInstance(referenceSetID, hashEntry.getMd5Hash(), type, hashEntry.getComment())); + } catch (EamDbException ex){ + throw new TskCoreException("Error adding hashes to " + getDisplayName(), ex); + } } try{ @@ -1344,7 +1349,7 @@ public class HashDbManager implements PropertyChangeListener { try{ return EamDb.getInstance().referenceSetIsValid(this.referenceSetID, this.hashSetName, this.version); } catch (EamDbException ex){ - Logger.getLogger(CentralRepoHashSet.class.getName()).log(Level.SEVERE, "Error validating hash database " + hashSetName, ex); //NON-NLS + Logger.getLogger(CentralRepoHashSet.class.getName()).log(Level.SEVERE, "Error validating hash set " + hashSetName, ex); //NON-NLS return false; } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupModuleSettings.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupModuleSettings.java index 99dd50d291..99204ab1ca 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupModuleSettings.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupModuleSettings.java @@ -47,7 +47,7 @@ final class HashLookupModuleSettings implements IngestModuleIngestJobSettings { try{ databaseInfoList = HashLookupSettings.convertHashSetList(hashDbList); } catch (HashLookupSettings.HashLookupSettingsException ex){ - Logger.getLogger(HashLookupModuleSettings.class.getName()).log(Level.SEVERE, "Error creating hash database settings.", ex); //NON-NLS + Logger.getLogger(HashLookupModuleSettings.class.getName()).log(Level.SEVERE, "Error creating hash set settings.", ex); //NON-NLS databaseInfoList = new ArrayList<>(); } } @@ -87,7 +87,7 @@ final class HashLookupModuleSettings implements IngestModuleIngestJobSettings { dbInfo.setSearchDuringIngest(true); databaseInfoList.add(dbInfo); } catch (TskCoreException ex){ - Logger.getLogger(HashLookupModuleSettings.class.getName()).log(Level.SEVERE, "Error creating hash database settings for " + db.getHashSetName(), ex); //NON-NLS + Logger.getLogger(HashLookupModuleSettings.class.getName()).log(Level.SEVERE, "Error creating hash set settings for " + db.getHashSetName(), ex); //NON-NLS } } for(HashDb db:disabledHashSets){ @@ -96,7 +96,7 @@ final class HashLookupModuleSettings implements IngestModuleIngestJobSettings { dbInfo.setSearchDuringIngest(false); databaseInfoList.add(dbInfo); } catch (TskCoreException ex){ - Logger.getLogger(HashLookupModuleSettings.class.getName()).log(Level.SEVERE, "Error creating hash database settings for " + db.getHashSetName(), ex); //NON-NLS + Logger.getLogger(HashLookupModuleSettings.class.getName()).log(Level.SEVERE, "Error creating hash set settings for " + db.getHashSetName(), ex); //NON-NLS } } @@ -152,7 +152,7 @@ final class HashLookupModuleSettings implements IngestModuleIngestJobSettings { try{ databaseInfoList = HashLookupSettings.convertHashSetList(HashDbManager.getInstance().getAllHashSets()); } catch (HashLookupSettings.HashLookupSettingsException ex){ - Logger.getLogger(HashLookupModuleSettings.class.getName()).log(Level.SEVERE, "Error updating hash database settings.", ex); //NON-NLS + Logger.getLogger(HashLookupModuleSettings.class.getName()).log(Level.SEVERE, "Error updating hash set settings.", ex); //NON-NLS return; } diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettings.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettings.java index 948f18451d..68bf4a32b2 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettings.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettings.java @@ -80,7 +80,7 @@ final class HashLookupSettings implements Serializable { try{ dbInfoList.add(new HashDbInfo(db)); } catch (TskCoreException ex){ - logger.log(Level.SEVERE, "Could not load database settings for {0}", db.getHashSetName()); + logger.log(Level.SEVERE, "Could not load hash set settings for {0}", db.getHashSetName()); } } return dbInfoList; @@ -128,7 +128,7 @@ final class HashLookupSettings implements Serializable { return filesSetsSettings; } } catch (IOException | ClassNotFoundException ex) { - throw new HashLookupSettingsException("Could not read hash database settings.", ex); + throw new HashLookupSettingsException("Could not read hash set settings.", ex); } } @@ -285,7 +285,7 @@ final class HashLookupSettings implements Serializable { out.writeObject(settings); return true; } catch (Exception ex) { - logger.log(Level.SEVERE, "Could not write hash database settings."); + logger.log(Level.SEVERE, "Could not write hash set settings."); return false; } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java index 1adb75594d..6e972437f0 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 - 2015 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -175,7 +175,7 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan try { addHashesToDatabaseButton.setEnabled(!ingestIsRunning && db.isUpdateable()); } catch (TskCoreException ex) { - Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error identifying if the database is updateable.", ex); //NON-NLS + Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error identifying if the hash set is updateable.", ex); //NON-NLS addHashesToDatabaseButton.setEnabled(false); } @@ -192,14 +192,14 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan try { hashDbLocationLabel.setText(shortenPath(db.getDatabasePath())); } catch (TskCoreException ex) { - Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error getting database path of " + db.getHashSetName() + " hash database", ex); //NON-NLS + Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error getting hash set path of " + db.getHashSetName() + " hash set", ex); //NON-NLS hashDbLocationLabel.setText(ERROR_GETTING_PATH_TEXT); } try { indexPathLabel.setText(shortenPath(hashDb.getIndexPath())); } catch (TskCoreException ex) { - Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error getting index path of " + db.getHashSetName() + " hash database", ex); //NON-NLS + Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error getting index path of " + db.getHashSetName() + " hash set", ex); //NON-NLS indexPathLabel.setText(ERROR_GETTING_PATH_TEXT); } @@ -237,7 +237,7 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan indexButton.setEnabled(true); } } catch (TskCoreException ex) { - Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error getting index state of hash database", ex); //NON-NLS + Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error getting index state of hash set", ex); //NON-NLS hashDbIndexStatusLabel.setText(ERROR_GETTING_INDEX_STATUS_TEXT); hashDbIndexStatusLabel.setForeground(Color.red); indexButton.setText(NbBundle.getMessage(this.getClass(), "HashDbConfigPanel.indexButtonText.index")); @@ -299,9 +299,9 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan } @Override - @Messages({"HashLookupSettingsPanel.saveFail.message=Couldn't save hash db settings.", + @Messages({"HashLookupSettingsPanel.saveFail.message=Couldn't save hash set settings.", "HashLookupSettingsPanel.saveFail.title=Save Fail"}) - public void saveSettings() { + public void saveSettings() { // Clear out the list of new central repo hash sets. They don't need to be // indexed so will all be saved on both code paths. newReferenceSetIDs.clear(); @@ -316,7 +316,7 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan unindexed.add(hashDatabase); } } catch (TskCoreException ex) { - Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error getting index info for hash database", ex); //NON-NLS + Logger.getLogger(HashLookupSettingsPanel.class.getName()).log(Level.SEVERE, "Error getting index info for hash set", ex); //NON-NLS } } } @@ -399,7 +399,7 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan * unindexed, along with solutions. This method is related to * ModalNoButtons, to be removed at a later date. * - * @param plural Whether or not there are multiple unindexed databases + * @param plural Whether or not there are multiple unindexed databases * @param unindexed The list of unindexed databases. Can be of size 1. */ private void showInvalidIndex(boolean plural, List unindexed) { @@ -515,7 +515,7 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan try { return hashSets.get(rowIndex).isValid(); } catch (TskCoreException ex) { - Logger.getLogger(HashSetTableModel.class.getName()).log(Level.SEVERE, "Error getting index info for hash database", ex); //NON-NLS + Logger.getLogger(HashSetTableModel.class.getName()).log(Level.SEVERE, "Error getting index info for hash set", ex); //NON-NLS return false; } } @@ -998,12 +998,14 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan } }//GEN-LAST:event_importDatabaseButtonActionPerformed - @Messages({}) + @Messages({ + "HashLookupSettingsPanel.promptTitle.deleteHashDb=Delete Hash Database from Configuration", + "HashLookupSettingsPanel.promptMessage.deleteHashDb=This will make the hash database unavailable for lookup. Do you want to proceed?\n\nNote: The hash database can still be re-imported later." + }) private void deleteDatabaseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteDatabaseButtonActionPerformed if (JOptionPane.showConfirmDialog(null, - NbBundle.getMessage(this.getClass(), - "HashDbConfigPanel.deleteDbActionConfirmMsg"), - NbBundle.getMessage(this.getClass(), "HashDbConfigPanel.deleteDbActionMsg"), + Bundle.HashLookupSettingsPanel_promptMessage_deleteHashDb(), + Bundle.HashLookupSettingsPanel_promptTitle_deleteHashDb(), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION) { HashDb hashDb = ((HashSetTable) hashSetTable).getSelection(); diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/KdbHashSetParser.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/KdbHashSetParser.java index 5935784087..94d2724995 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/KdbHashSetParser.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/KdbHashSetParser.java @@ -64,7 +64,7 @@ public class KdbHashSetParser implements HashSetParser { totalHashes = resultSet.getLong("count"); } else { close(); - throw new TskCoreException("Error getting hash count from database " + filename); + throw new TskCoreException("Error getting hash count from hash set " + filename); } // Get the hashes @@ -72,7 +72,7 @@ public class KdbHashSetParser implements HashSetParser { // At this point, getNextHash can read each hash from the result set } catch (ClassNotFoundException | SQLException ex) { - throw new TskCoreException("Error opening/reading database " + filename, ex); + throw new TskCoreException("Error opening/reading hash set " + filename, ex); } } @@ -101,10 +101,10 @@ public class KdbHashSetParser implements HashSetParser { totalHashesRead++; return sb.toString(); } else { - throw new TskCoreException("Could not read expected number of hashes from database " + filename); + throw new TskCoreException("Could not read expected number of hashes from hash set " + filename); } } catch (SQLException ex) { - throw new TskCoreException("Error reading hash from result set for database " + filename, ex); + throw new TskCoreException("Error reading hash from result set for hash set " + filename, ex); } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties index f38bcbc815..830901a6d9 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties @@ -38,8 +38,6 @@ FilesSetRulePanel.interesting.jLabel5.text=Enter information about files that yo FilesSetRulePanel.ingest.jLabel5.text=Enter information about files that you want to run ingest on. FilesSetRulePanel.nameCheck.text=Name Pattern: FilesSetRulePanel.pathCheck.text=Path Pattern: -FilesSetRulePanel.mimeCheck.text=MIME Type: -FilesSetRulePanel.fileSizeCheck.text=File Size: FilesSetRulePanel.filesRadioButton.text=Files FilesSetRulePanel.dirsRadioButton.text=Directories FilesSetDefsPanel.interesting.setsListLabel.text=Rule Sets: @@ -79,3 +77,10 @@ FilesSetDefsPanel.ingoreUnallocCheckbox.text=Ignore Unallocated Space FilesSetDefsPanel.ingoreUnallocCheckbox.toolTipText=Ignores unallocated space, such as deleted files. May run faster but produce less complete results. FilesSetDefsPanel.ingestWarningLabel.text=Ingest is ongoing, some settings will be unavailable until it finishes. FilesSetDefsPanel.allRadioButton.text=All +FilesSetRulePanel.dateCheck.text=Modified Within: +FilesSetRulePanel.fileSizeCheck.text=File Size: +FilesSetRulePanel.mimeCheck.text=MIME Type: +FilesSetDefsPanel.modifiedDateLabel.text=Modified Within: +FilesSetDefsPanel.daysIncludedTextField.text= +FilesSetDefsPanel.daysIncludedLabel.text=day(s) +FilesSetRulePanel.daysIncludedLabel.text=day(s) diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSet.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSet.java index 38d7285fc1..3a3e9b2360 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSet.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSet.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; import java.util.regex.Pattern; +import org.openide.util.NbBundle; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskData; @@ -169,6 +170,7 @@ public final class FilesSet implements Serializable { private final ParentPathCondition pathCondition; private final MimeTypeCondition mimeTypeCondition; private final FileSizeCondition fileSizeCondition; + private final DateCondition dateCondition; private final List conditions = new ArrayList<>(); /** @@ -180,8 +182,10 @@ public final class FilesSet implements Serializable { * @param pathCondition A file path condition, may be null. * @param mimeTypeCondition A file mime type condition, may be null. * @param fileSizeCondition A file size condition, may be null. + * @param dateCondition A file date created or modified condition, + * may be null */ - Rule(String ruleName, FileNameCondition fileNameCondition, MetaTypeCondition metaTypeCondition, ParentPathCondition pathCondition, MimeTypeCondition mimeTypeCondition, FileSizeCondition fileSizeCondition) { + Rule(String ruleName, FileNameCondition fileNameCondition, MetaTypeCondition metaTypeCondition, ParentPathCondition pathCondition, MimeTypeCondition mimeTypeCondition, FileSizeCondition fileSizeCondition, DateCondition dateCondition) { // since ruleName is optional, ruleUUID can be used to uniquely identify a rule. this.uuid = UUID.randomUUID().toString(); if (metaTypeCondition == null) { @@ -216,6 +220,10 @@ public final class FilesSet implements Serializable { if (this.pathCondition != null) { this.conditions.add(this.pathCondition); } + this.dateCondition = dateCondition; + if (this.dateCondition != null) { + this.conditions.add(this.dateCondition); + } } /** @@ -254,6 +262,10 @@ public final class FilesSet implements Serializable { return this.pathCondition; } + DateCondition getDateCondition() { + return this.dateCondition; + } + /** * Determines whether or not a file satisfies the rule. * @@ -270,6 +282,10 @@ public final class FilesSet implements Serializable { return true; } + @NbBundle.Messages({ + "# {0} - daysIncluded", + "FilesSet.rule.dateRule.toString=(modified within {0} day(s))" + }) @Override public String toString() { // This override is designed to provide a display name for use with @@ -283,6 +299,8 @@ public final class FilesSet implements Serializable { } else if (this.fileSizeCondition != null) { return this.ruleName + " (" + fileSizeCondition.getComparator().getSymbol() + " " + fileSizeCondition.getSizeValue() + " " + fileSizeCondition.getUnit().getName() + ")"; + } else if (this.dateCondition != null) { + return this.ruleName + Bundle.FilesSet_rule_dateRule_toString(dateCondition.getDaysIncluded()); } else { return this.ruleName + " ()"; } @@ -537,7 +555,7 @@ public final class FilesSet implements Serializable { case FILES: return file.isFile(); case DIRECTORIES: - return file.getMetaType() == TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR + return file.getMetaType() == TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR || file.getMetaType() == TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_VIRT_DIR; case FILES_AND_DIRECTORIES: return file.getMetaType() == TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG @@ -737,6 +755,46 @@ public final class FilesSet implements Serializable { } + /** + * A class for checking whether a file's creation or modification + * occured in a specific range of time + */ + static final class DateCondition implements FileAttributeCondition { + + private final static long SECS_PER_DAY = 60 * 60 * 24; + + private int daysIncluded; + + /** + * Construct a new DateCondition + * + * @param days - files created or modified more recently than this + * number of days will pass + */ + DateCondition(int days) { + daysIncluded = days; + } + + /** + * Get the number of days which this condition allows to pass + * + * @return integer value of the number days which will pass + */ + int getDaysIncluded() { + return daysIncluded; + } + + @Override + public boolean passes(AbstractFile file) { + long dateThreshold = System.currentTimeMillis() / 1000 - daysIncluded * SECS_PER_DAY; + if (file.getCrtime() > dateThreshold || file.getMtime() > dateThreshold) { + return true; + } + return false; + } + + } + /** * A file name extension condition for an interesting files set * membership rule. The immutability of a file name extension condition diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form index bffb00f33b..4d8f94589f 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form @@ -90,109 +90,91 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -213,9 +195,9 @@ - - - + + + @@ -223,7 +205,7 @@ - + @@ -232,14 +214,14 @@ - + - + @@ -250,26 +232,32 @@ - + - + - + - + - + + + + + + + @@ -437,7 +425,7 @@ - + @@ -996,6 +984,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java index c67051be97..4fd933b167 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -251,6 +251,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp this.fileNameRegexCheckbox.setSelected(false); this.filesRadioButton.setSelected(true); this.rulePathConditionTextField.setText(""); + this.daysIncludedTextField.setText(""); this.rulePathConditionRegexCheckBox.setSelected(false); this.mimeTypeComboBox.setSelectedIndex(0); this.equalitySignComboBox.setSelectedIndex(2); @@ -326,7 +327,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp FilesSet.Rule.ParentPathCondition pathCondition = rule.getPathCondition(); FilesSet.Rule.MimeTypeCondition mimeTypeCondition = rule.getMimeTypeCondition(); FilesSet.Rule.FileSizeCondition fileSizeCondition = rule.getFileSizeCondition(); - + FilesSet.Rule.DateCondition dateCondition = rule.getDateCondition(); // Populate the components that display the properties of the // selected rule. if (nameCondition != null) { @@ -372,7 +373,12 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp FilesSetDefsPanel.this.equalitySignComboBox.setSelectedIndex(2); FilesSetDefsPanel.this.fileSizeSpinner.setValue(0); } - + if (dateCondition != null){ + FilesSetDefsPanel.this.daysIncludedTextField.setText(Integer.toString(dateCondition.getDaysIncluded())); + } + else { + FilesSetDefsPanel.this.daysIncludedTextField.setText(""); + } // Enable the new, edit and delete rule buttons. FilesSetDefsPanel.this.newRuleButton.setEnabled(true && canBeEnabled); FilesSetDefsPanel.this.editRuleButton.setEnabled(true && canBeEnabled); @@ -477,7 +483,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp if (selectedRule != null) { rules.remove(selectedRule.getUuid()); } - FilesSet.Rule newRule = new FilesSet.Rule(panel.getRuleName(), panel.getFileNameCondition(), panel.getMetaTypeCondition(), panel.getPathCondition(), panel.getMimeTypeCondition(), panel.getFileSizeCondition()); + FilesSet.Rule newRule = new FilesSet.Rule(panel.getRuleName(), panel.getFileNameCondition(), panel.getMetaTypeCondition(), panel.getPathCondition(), panel.getMimeTypeCondition(), panel.getFileSizeCondition(), panel.getDateCondition()); rules.put(newRule.getUuid(), newRule); // Add the new/edited files set definition, replacing any previous @@ -590,6 +596,9 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp copySetButton = new javax.swing.JButton(); importSetButton = new javax.swing.JButton(); exportSetButton = new javax.swing.JButton(); + modifiedDateLabel = new javax.swing.JLabel(); + daysIncludedTextField = new javax.swing.JTextField(); + daysIncludedLabel = new javax.swing.JLabel(); setFont(getFont().deriveFont(getFont().getStyle() & ~java.awt.Font.BOLD, 11)); @@ -643,7 +652,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp setDescriptionTextArea.setColumns(20); setDescriptionTextArea.setFont(setDescriptionTextArea.getFont().deriveFont(setDescriptionTextArea.getFont().getStyle() & ~java.awt.Font.BOLD, 13)); setDescriptionTextArea.setLineWrap(true); - setDescriptionTextArea.setRows(10); + setDescriptionTextArea.setRows(6); setDescriptionTextArea.setMinimumSize(new java.awt.Dimension(10, 22)); setDescScrollPanel.setViewportView(setDescriptionTextArea); @@ -841,6 +850,17 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp } }); + org.openide.awt.Mnemonics.setLocalizedText(modifiedDateLabel, org.openide.util.NbBundle.getMessage(FilesSetDefsPanel.class, "FilesSetDefsPanel.modifiedDateLabel.text")); // NOI18N + + daysIncludedTextField.setEditable(false); + daysIncludedTextField.setHorizontalAlignment(javax.swing.JTextField.TRAILING); + daysIncludedTextField.setText(org.openide.util.NbBundle.getMessage(FilesSetDefsPanel.class, "FilesSetDefsPanel.daysIncludedTextField.text")); // NOI18N + daysIncludedTextField.setMinimumSize(new java.awt.Dimension(60, 20)); + daysIncludedTextField.setPreferredSize(new java.awt.Dimension(60, 20)); + + org.openide.awt.Mnemonics.setLocalizedText(daysIncludedLabel, org.openide.util.NbBundle.getMessage(FilesSetDefsPanel.class, "FilesSetDefsPanel.daysIncludedLabel.text")); // NOI18N + daysIncludedLabel.setEnabled(false); + javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); jPanel1.setLayout(jPanel1Layout); jPanel1Layout.setHorizontalGroup( @@ -867,83 +887,74 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp .addComponent(setsListLabel)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(separator, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(rulesListScrollPane, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(setDescScrollPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(jPanel1Layout.createSequentialGroup() - .addGap(101, 101, 101) - .addComponent(filesRadioButton, javax.swing.GroupLayout.PREFERRED_SIZE, 47, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(16, 16, 16) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jLabel7) + .addComponent(jLabel8) + .addComponent(jLabel2) + .addComponent(jLabel4) + .addComponent(modifiedDateLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(dirsRadioButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(allRadioButton)) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGap(105, 105, 105) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(rulePathConditionTextField) + .addComponent(fileNameTextField, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(mimeTypeComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(fileNameRadioButton) - .addGap(4, 4, 4) - .addComponent(fileNameExtensionRadioButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(fileNameRegexCheckbox)) - .addComponent(rulePathConditionRegexCheckBox))) - .addGroup(jPanel1Layout.createSequentialGroup() - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(rulesListLabel) - .addComponent(jLabel1) - .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(newRuleButton) - .addGap(18, 18, 18) - .addComponent(editRuleButton) - .addGap(18, 18, 18) - .addComponent(deleteRuleButton)) - .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(ignoreKnownFilesCheckbox) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(ingoreUnallocCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 158, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jLabel5) - .addComponent(jLabel6)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(ingestWarningLabel))))) - .addGap(24, 28, Short.MAX_VALUE)) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGap(22, 22, 22) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jLabel7) - .addComponent(jLabel8)) - .addGap(18, 18, 18)) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jLabel3) - .addComponent(jLabel2)) - .addGap(6, 6, 6)) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() - .addComponent(jLabel4) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)))) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(equalitySignComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 44, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(equalitySignComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 60, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(fileSizeSpinner, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(fileSizeUnitComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(rulePathConditionTextField) - .addComponent(fileNameTextField, javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(mimeTypeComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(fileNameRadioButton) + .addComponent(rulePathConditionRegexCheckBox) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(daysIncludedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(daysIncludedLabel)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(filesRadioButton, javax.swing.GroupLayout.PREFERRED_SIZE, 47, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(dirsRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(allRadioButton))) + .addGap(0, 0, Short.MAX_VALUE))))) + .addGap(8, 8, 8)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(rulesListLabel) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(ignoreKnownFilesCheckbox) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(ingoreUnallocCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 158, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel1Layout.createSequentialGroup() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(rulesListScrollPane, javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(setDescScrollPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) - .addGap(8, 8, 8)))) + .addComponent(jLabel5) + .addComponent(jLabel6)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(ingestWarningLabel)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGap(174, 174, 174) + .addComponent(fileNameExtensionRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(fileNameRegexCheckbox)) + .addComponent(jLabel1) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(newRuleButton) + .addGap(18, 18, 18) + .addComponent(editRuleButton) + .addGap(18, 18, 18) + .addComponent(deleteRuleButton))) + .addGap(24, 28, Short.MAX_VALUE)))) ); jPanel1Layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {copySetButton, deleteSetButton, editSetButton, exportSetButton, importSetButton, newSetButton}); @@ -963,15 +974,15 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp .addGap(1, 1, 1)) .addComponent(ingestWarningLabel, javax.swing.GroupLayout.Alignment.TRAILING)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(setDescScrollPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(6, 6, 6) + .addComponent(setDescScrollPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 69, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(ignoreKnownFilesCheckbox) .addComponent(ingoreUnallocCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(rulesListLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(rulesListScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 82, Short.MAX_VALUE) + .addComponent(rulesListScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 61, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(newRuleButton) @@ -979,13 +990,13 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp .addComponent(deleteRuleButton)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jLabel1) - .addGap(8, 8, 8) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel2) .addComponent(filesRadioButton) .addComponent(dirsRadioButton) .addComponent(allRadioButton)) - .addGap(8, 8, 8) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel3) .addComponent(fileNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE)) @@ -994,23 +1005,28 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp .addComponent(fileNameRadioButton) .addComponent(fileNameExtensionRadioButton) .addComponent(fileNameRegexCheckbox)) - .addGap(8, 8, 8) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel4) .addComponent(rulePathConditionTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGap(6, 6, 6) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(rulePathConditionRegexCheckBox) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel7) .addComponent(mimeTypeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jLabel8) .addComponent(equalitySignComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(fileSizeSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(fileSizeUnitComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGap(5, 5, 5)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(modifiedDateLabel) + .addComponent(daysIncludedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(daysIncludedLabel)) + .addContainerGap()) .addGroup(jPanel1Layout.createSequentialGroup() .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) @@ -1262,6 +1278,8 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JRadioButton allRadioButton; private javax.swing.JButton copySetButton; + private javax.swing.JLabel daysIncludedLabel; + private javax.swing.JTextField daysIncludedTextField; private javax.swing.JButton deleteRuleButton; private javax.swing.JButton deleteSetButton; private javax.swing.JRadioButton dirsRadioButton; @@ -1294,6 +1312,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp private javax.swing.JScrollPane jScrollPane2; private javax.swing.JTextArea jTextArea1; private javax.swing.JComboBox mimeTypeComboBox; + private javax.swing.JLabel modifiedDateLabel; private javax.swing.JButton newRuleButton; private javax.swing.JButton newSetButton; private javax.swing.JCheckBox rulePathConditionRegexCheckBox; diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.form b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.form index 4777c108b4..b91bb34d36 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.form @@ -23,82 +23,80 @@ - - + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - + - + @@ -106,17 +104,22 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -134,7 +137,7 @@ - + @@ -146,7 +149,13 @@ - + + + + + + + @@ -170,9 +179,6 @@ - - - @@ -380,5 +386,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java index d70a5474ee..4965652c5f 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-2017 Basis Technology Corp. + * Copyright 2014-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ package org.sleuthkit.autopsy.modules.interestingitems; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.util.List; import java.util.SortedSet; import java.util.logging.Level; @@ -50,6 +49,8 @@ final class FilesSetRulePanel extends javax.swing.JPanel { "FilesSetRulePanel.NoMimeTypeError=Please select a valid MIME type.", "FilesSetRulePanel.NoNameError=Name cannot be empty", "FilesSetRulePanel.NoPathError=Path cannot be empty", + "FilesSetRulePanel.DaysIncludedEmptyError=Number of days included cannot be empty.", + "FilesSetRulePanel.DaysIncludedInvalidError=Number of days included must be a positive integer.", "FilesSetRulePanel.ZeroFileSizeError=File size condition value must not be 0 (Unless = is selected)." }) @@ -82,6 +83,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { } else { populateMimeTypesComboBox(); } + this.dateCheckActionPerformed(null); populateComponentsWithDefaultValues(); this.setButtons(okButton, cancelButton); } @@ -108,12 +110,14 @@ final class FilesSetRulePanel extends javax.swing.JPanel { populateMimeTypesComboBox(); populateMimeConditionComponents(rule); populateSizeConditionComponents(rule); + } populateMimeTypesComboBox(); populateRuleNameComponent(rule); populateTypeConditionComponents(rule); populateNameConditionComponents(rule); populatePathConditionComponents(rule); + populateDateConditionComponents(rule); this.setButtons(okButton, cancelButton); } @@ -176,7 +180,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { private void setOkButton() { if (this.okButton != null) { this.okButton.setEnabled(this.fileSizeCheck.isSelected() || this.mimeCheck.isSelected() - || this.nameCheck.isSelected() || this.pathCheck.isSelected()); + || this.nameCheck.isSelected() || this.pathCheck.isSelected() || this.dateCheck.isSelected()); } } @@ -274,6 +278,21 @@ final class FilesSetRulePanel extends javax.swing.JPanel { } } + /** + * Populates the UI components that display the optional date condition for + * a rule. + * + * @param rule The files set rule to be edited. + */ + private void populateDateConditionComponents(FilesSet.Rule rule) { + FilesSet.Rule.DateCondition dateCondition = rule.getDateCondition(); + if (dateCondition != null) { + this.dateCheck.setSelected(true); + this.dateCheckActionPerformed(null); + this.daysIncludedTextField.setText(Integer.toString(dateCondition.getDaysIncluded())); + } + } + /** * Returns whether or not the data entered in the panel constitutes a valid * files set membership rule definition, displaying a dialog explaining the @@ -283,7 +302,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { */ boolean isValidRuleDefinition() { - if (!(this.mimeCheck.isSelected() || this.fileSizeCheck.isSelected() || this.pathCheck.isSelected() || this.nameCheck.isSelected())) { + if (!(this.mimeCheck.isSelected() || this.fileSizeCheck.isSelected() || this.pathCheck.isSelected() || this.nameCheck.isSelected() || this.dateCheck.isSelected())) { NotifyDescriptor notifyDesc = new NotifyDescriptor.Message( Bundle.FilesSetRulePanel_NoConditionError(), NotifyDescriptor.WARNING_MESSAGE); @@ -367,6 +386,28 @@ final class FilesSetRulePanel extends javax.swing.JPanel { } } + if (this.dateCheck.isSelected()) { + if (this.daysIncludedTextField.getText().isEmpty()) { + NotifyDescriptor notifyDesc = new NotifyDescriptor.Message( + Bundle.FilesSetRulePanel_DaysIncludedEmptyError(), + NotifyDescriptor.WARNING_MESSAGE); + DialogDisplayer.getDefault().notify(notifyDesc); + return false; + } + try { + int value = Integer.parseInt(daysIncludedTextField.getText()); + if (value < 0) { + throw new NumberFormatException("Negative numbers are not allowed for the within N days condition"); + } + } catch (NumberFormatException e) { + //field did not contain an integer + NotifyDescriptor notifyDesc = new NotifyDescriptor.Message( + Bundle.FilesSetRulePanel_DaysIncludedInvalidError(), + NotifyDescriptor.WARNING_MESSAGE); + DialogDisplayer.getDefault().notify(notifyDesc); + return false; + } + } return true; } @@ -504,6 +545,23 @@ final class FilesSetRulePanel extends javax.swing.JPanel { return condition; } + /** + * Gets the optional date condition for the rule that was created or edited. + * Should only be called if isValidDefintion() returns true. + * + * @return A date condition or null if no date condition was specified. + * + * @throws IllegalStateException if the specified date condition is not + * valid. + */ + FilesSet.Rule.DateCondition getDateCondition() { + FilesSet.Rule.DateCondition dateCondition = null; + if (!daysIncludedTextField.getText().isEmpty()) { + dateCondition = new FilesSet.Rule.DateCondition(Integer.parseInt(daysIncludedTextField.getText())); + } + return dateCondition; + } + /** * Checks an input string for the use of illegal characters. * @@ -582,15 +640,13 @@ final class FilesSetRulePanel extends javax.swing.JPanel { filesRadioButton = new javax.swing.JRadioButton(); dirsRadioButton = new javax.swing.JRadioButton(); allRadioButton = new javax.swing.JRadioButton(); + daysIncludedTextField = new javax.swing.JTextField(); + daysIncludedLabel = new javax.swing.JLabel(); + dateCheck = new javax.swing.JCheckBox(); org.openide.awt.Mnemonics.setLocalizedText(ruleNameLabel, org.openide.util.NbBundle.getMessage(FilesSetRulePanel.class, "FilesSetRulePanel.ruleNameLabel.text")); // NOI18N ruleNameTextField.setText(org.openide.util.NbBundle.getMessage(FilesSetRulePanel.class, "FilesSetRulePanel.ruleNameTextField.text")); // NOI18N - ruleNameTextField.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - ruleNameTextFieldActionPerformed(evt); - } - }); org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(FilesSetRulePanel.class, "FilesSetRulePanel.jLabel1.text")); // NOI18N @@ -685,85 +741,99 @@ final class FilesSetRulePanel extends javax.swing.JPanel { } }); + daysIncludedTextField.setEnabled(false); + daysIncludedTextField.setMinimumSize(new java.awt.Dimension(60, 20)); + daysIncludedTextField.setPreferredSize(new java.awt.Dimension(60, 20)); + + org.openide.awt.Mnemonics.setLocalizedText(daysIncludedLabel, org.openide.util.NbBundle.getMessage(FilesSetRulePanel.class, "FilesSetRulePanel.daysIncludedLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(dateCheck, org.openide.util.NbBundle.getMessage(FilesSetRulePanel.class, "FilesSetRulePanel.dateCheck.text")); // NOI18N + dateCheck.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + dateCheckActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGap(8, 8, 8) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(ruleNameLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(ruleNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 234, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addComponent(ruleNameLabel) + .addGap(5, 5, 5) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(mimeTypeComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(pathTextField) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addComponent(equalitySymbolComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(fileSizeSpinner) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(fileSizeComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addComponent(pathRegexCheckBox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(pathSeparatorInfoLabel)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(daysIncludedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 69, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(daysIncludedLabel)) + .addComponent(ruleNameTextField, javax.swing.GroupLayout.DEFAULT_SIZE, 249, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(fullNameRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(extensionRadioButton, javax.swing.GroupLayout.PREFERRED_SIZE, 98, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(nameRegexCheckbox))) + .addGap(1, 1, 1)))) + .addComponent(jLabel5) .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jLabel5) + .addComponent(nameCheck, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, 95, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel1)) + .addGap(16, 16, 16) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addComponent(jLabel1) - .addGap(65, 65, 65) .addComponent(filesRadioButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(dirsRadioButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(allRadioButton))) - .addGap(0, 0, Short.MAX_VALUE)))) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(nameCheck) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(nameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 249, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() - .addContainerGap() - .addComponent(pathCheck) - .addGap(4, 4, 4) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(pathRegexCheckBox) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(pathSeparatorInfoLabel)) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addGap(0, 0, Short.MAX_VALUE) - .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 250, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(layout.createSequentialGroup() - .addComponent(fullNameRadioButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(extensionRadioButton, javax.swing.GroupLayout.PREFERRED_SIZE, 114, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(nameRegexCheckbox) - .addGap(0, 0, Short.MAX_VALUE)))) + .addComponent(allRadioButton)) + .addComponent(nameTextField))))) .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(pathCheck) .addComponent(mimeCheck) - .addComponent(fileSizeCheck)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addGroup(layout.createSequentialGroup() - .addComponent(equalitySymbolComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 36, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(18, 18, 18) - .addComponent(fileSizeSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, 94, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(18, 18, 18) - .addComponent(fileSizeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 82, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(mimeTypeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 250, javax.swing.GroupLayout.PREFERRED_SIZE)))) + .addComponent(fileSizeCheck) + .addComponent(dateCheck)) + .addGap(0, 0, Short.MAX_VALUE))) .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(jLabel5) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jLabel1) - .addComponent(filesRadioButton) - .addComponent(dirsRadioButton) - .addComponent(allRadioButton)) - .addGap(5, 5, 5) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(nameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(nameCheck)) + .addGap(3, 3, 3) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createSequentialGroup() + .addComponent(jLabel1) + .addGap(10, 10, 10) + .addComponent(nameCheck)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(filesRadioButton) + .addComponent(dirsRadioButton) + .addComponent(allRadioButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(nameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE))) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(fullNameRadioButton) @@ -777,7 +847,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(pathRegexCheckBox) .addComponent(pathSeparatorInfoLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 8, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(mimeTypeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(mimeCheck)) @@ -787,7 +857,12 @@ final class FilesSetRulePanel extends javax.swing.JPanel { .addComponent(fileSizeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(fileSizeSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(fileSizeCheck)) - .addGap(15, 15, 15) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(daysIncludedTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(daysIncludedLabel) + .addComponent(dateCheck)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(ruleNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(ruleNameLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) @@ -795,10 +870,6 @@ final class FilesSetRulePanel extends javax.swing.JPanel { ); }// //GEN-END:initComponents - private void ruleNameTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_ruleNameTextFieldActionPerformed - // TODO add your handling code here: - }//GEN-LAST:event_ruleNameTextFieldActionPerformed - private void nameCheckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_nameCheckActionPerformed if (!this.nameCheck.isSelected()) { this.nameTextField.setEnabled(false); @@ -831,15 +902,30 @@ final class FilesSetRulePanel extends javax.swing.JPanel { this.setOkButton(); }//GEN-LAST:event_pathCheckActionPerformed - private void mimeCheckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_mimeCheckActionPerformed - if (!this.mimeCheck.isSelected()) { - this.mimeTypeComboBox.setEnabled(false); - this.mimeTypeComboBox.setSelectedIndex(0); + private void filesRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_filesRadioButtonActionPerformed + + this.setComponentsForSearchType(); + }//GEN-LAST:event_filesRadioButtonActionPerformed + + private void dirsRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_dirsRadioButtonActionPerformed + this.setComponentsForSearchType(); + }//GEN-LAST:event_dirsRadioButtonActionPerformed + + private void allRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_allRadioButtonActionPerformed + this.setComponentsForSearchType(); + }//GEN-LAST:event_allRadioButtonActionPerformed + + private void dateCheckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_dateCheckActionPerformed + if (!this.dateCheck.isSelected()) { + this.daysIncludedTextField.setEnabled(false); + this.daysIncludedLabel.setEnabled(false); + this.daysIncludedTextField.setText(""); } else { - this.mimeTypeComboBox.setEnabled(true); + this.daysIncludedTextField.setEnabled(true); + this.daysIncludedLabel.setEnabled(true); } this.setOkButton(); - }//GEN-LAST:event_mimeCheckActionPerformed + }//GEN-LAST:event_dateCheckActionPerformed private void fileSizeCheckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fileSizeCheckActionPerformed if (!this.fileSizeCheck.isSelected()) { @@ -855,21 +941,21 @@ final class FilesSetRulePanel extends javax.swing.JPanel { this.setOkButton(); }//GEN-LAST:event_fileSizeCheckActionPerformed - private void filesRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_filesRadioButtonActionPerformed - - this.setComponentsForSearchType(); - }//GEN-LAST:event_filesRadioButtonActionPerformed - - private void dirsRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_dirsRadioButtonActionPerformed - this.setComponentsForSearchType(); - }//GEN-LAST:event_dirsRadioButtonActionPerformed - - private void allRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_allRadioButtonActionPerformed - this.setComponentsForSearchType(); - }//GEN-LAST:event_allRadioButtonActionPerformed + private void mimeCheckActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_mimeCheckActionPerformed + if (!this.mimeCheck.isSelected()) { + this.mimeTypeComboBox.setEnabled(false); + this.mimeTypeComboBox.setSelectedIndex(0); + } else { + this.mimeTypeComboBox.setEnabled(true); + } + this.setOkButton(); + }//GEN-LAST:event_mimeCheckActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JRadioButton allRadioButton; + private javax.swing.JCheckBox dateCheck; + private javax.swing.JLabel daysIncludedLabel; + private javax.swing.JTextField daysIncludedTextField; private javax.swing.JRadioButton dirsRadioButton; private javax.swing.JComboBox equalitySymbolComboBox; private javax.swing.JRadioButton extensionRadioButton; diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetsManager.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetsManager.java index 6350805ee6..8397d77c72 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetsManager.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetsManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -52,7 +52,7 @@ public final class FilesSetsManager extends Observable { { put(Bundle.FilesSetsManager_allFilesAndDirectories(), new Rule(Bundle.FilesSetsManager_allFilesAndDirectories(), null, - new MetaTypeCondition(MetaTypeCondition.Type.ALL), null, null, null)); + new MetaTypeCondition(MetaTypeCondition.Type.ALL), null, null, null, null)); } }); private static final FilesSet FILES_DIRS_UNALLOC_INGEST_FILTER = new FilesSet( @@ -61,7 +61,7 @@ public final class FilesSetsManager extends Observable { { put(Bundle.FilesSetsManager_allFilesDirectoriesAndUnallocated(), new Rule(Bundle.FilesSetsManager_allFilesDirectoriesAndUnallocated(), null, - new MetaTypeCondition(MetaTypeCondition.Type.ALL), null, null, null)); + new MetaTypeCondition(MetaTypeCondition.Type.ALL), null, null, null, null)); } }); diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemsFilesSetSettings.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemsFilesSetSettings.java index de88aee68e..3f4cca4465 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemsFilesSetSettings.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemsFilesSetSettings.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,6 +44,7 @@ import org.sleuthkit.autopsy.modules.interestingitems.FilesSet.Rule.FileSizeCond import org.sleuthkit.autopsy.modules.interestingitems.FilesSet.Rule.MetaTypeCondition; import org.sleuthkit.autopsy.modules.interestingitems.FilesSet.Rule.MimeTypeCondition; import org.sleuthkit.autopsy.modules.interestingitems.FilesSet.Rule.ParentPathCondition; +import org.sleuthkit.autopsy.modules.interestingitems.FilesSet.Rule.DateCondition; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -67,6 +68,7 @@ class InterestingItemsFilesSetSettings implements Serializable { private static final String FILE_SET_TAG = "INTERESTING_FILE_SET"; //NON-NLS private static final String NAME_RULE_TAG = "NAME"; //NON-NLS private static final String NAME_ATTR = "name"; //NON-NLS + private static final String DAYS_INCLUDED_ATTR = "daysIncluded"; private static final String MIME_ATTR = "mimeType"; private static final String FS_COMPARATOR_ATTR = "comparatorSymbol"; private static final String FS_SIZE_ATTR = "sizeValue"; @@ -166,6 +168,35 @@ class InterestingItemsFilesSetSettings implements Serializable { return pathCondition; } + /** + * Construct a date condition for a FilesSet membership rule from data in an + * XML element. + * + * @param ruleElement The XML element. + * + * @return The date condition, or null if there is an error (logged). + * + * @throws + * org.sleuthkit.autopsy.modules.interestingitems.FilesSetsManager.FilesSetsManagerException + */ + private static DateCondition readDateCondition(Element ruleElement) throws FilesSetsManager.FilesSetsManagerException { + // Read in the optional path condition. Null is o.k., but if the attribute + // is there, be sure it is not malformed. + DateCondition dateCondition = null; + if (!ruleElement.getAttribute(DAYS_INCLUDED_ATTR).isEmpty()) { + String daysIncluded = ruleElement.getAttribute(DAYS_INCLUDED_ATTR); + if (!daysIncluded.isEmpty()) { + try { + dateCondition = new DateCondition(Integer.parseInt(daysIncluded)); + } catch (NumberFormatException ex) { + logger.log(Level.SEVERE, "Error creating condition for " + daysIncluded + ", ignoring malformed date condition definition", ex); // NON-NLS + throw new FilesSetsManager.FilesSetsManagerException(String.format("error compiling %s regex", DAYS_INCLUDED_ATTR), ex); + } + } + } + return dateCondition; + } + /** * Attempts to compile a regular expression. * @@ -183,12 +214,13 @@ class InterestingItemsFilesSetSettings implements Serializable { } /** - * Construct a fileset membership rule from the data in an xml element for + * Construct a fileset membership rule from the data in an xml element for * use in a FilesSet. * * @param elem The XML element. * - * @return A file set constructed from the conditions available in the XML element + * @return A file set constructed from the conditions available in the XML + * element * * @throws * org.sleuthkit.autopsy.modules.interestingitems.FilesSetsManager.FilesSetsManagerException @@ -200,17 +232,17 @@ class InterestingItemsFilesSetSettings implements Serializable { ParentPathCondition pathCondition = readPathCondition(elem); MimeTypeCondition mimeCondition = readMimeCondition(elem); FileSizeCondition sizeCondition = readSizeCondition(elem); - //if meta type condition or all four types of conditions the user can create are all null then don't make the rule - if (metaCondition == null || (nameCondition == null && pathCondition == null && mimeCondition == null && sizeCondition == null)) { + DateCondition dateCondition = readDateCondition(elem); //if meta type condition or all four types of conditions the user can create are all null then don't make the rule + if (metaCondition == null || (nameCondition == null && pathCondition == null && mimeCondition == null && sizeCondition == null && dateCondition == null)) { logger.log(Level.WARNING, "Error Reading Rule, " + ruleName + " was either missing a meta condition or contained only a meta condition. No rule was imported."); // NON-NLS throw new FilesSetsManager.FilesSetsManagerException(String.format("Invalid Rule in FilesSet xml, missing necessary conditions for %s", ruleName)); } - return new FilesSet.Rule(ruleName, nameCondition, metaCondition, pathCondition, mimeCondition, sizeCondition); + return new FilesSet.Rule(ruleName, nameCondition, metaCondition, pathCondition, mimeCondition, sizeCondition, dateCondition); } /** - * Construct a file name condition for a FilesSet membership rule from data in an - * XML element. + * Construct a file name condition for a FilesSet membership rule from data + * in an XML element. * * @param ruleElement The XML element. * @@ -256,8 +288,8 @@ class InterestingItemsFilesSetSettings implements Serializable { } /** - * Construct a MIME type condition for a FilesSet membership rule from data in an - * XML element. + * Construct a MIME type condition for a FilesSet membership rule from data + * in an XML element. * * @param ruleElement The XML element. * @@ -275,8 +307,8 @@ class InterestingItemsFilesSetSettings implements Serializable { } /** - * Construct a file size condition for a FilesSet membership rule from data in an - * XML element. + * Construct a file size condition for a FilesSet membership rule from data + * in an XML element. * * @param ruleElement The XML element. * @@ -544,6 +576,13 @@ class InterestingItemsFilesSetSettings implements Serializable { ruleElement.setAttribute(FS_SIZE_ATTR, Integer.toString(sizeCondition.getSizeValue())); ruleElement.setAttribute(FS_UNITS_ATTR, sizeCondition.getUnit().getName()); } + + //Add the optional date condition + DateCondition dateCondition = rule.getDateCondition(); + if (dateCondition != null) { + ruleElement.setAttribute(DAYS_INCLUDED_ATTR, Integer.toString(dateCondition.getDaysIncluded())); + } + setElement.appendChild(ruleElement); } rootElement.appendChild(setElement); diff --git a/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java index 30aa69ba75..04f7f42355 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java @@ -37,6 +37,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import org.openide.modules.InstalledFileLocator; +import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.ExecUtil; @@ -78,6 +79,7 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { private static final String PHOTOREC_DIRECTORY = "photorec_exec"; //NON-NLS private static final String PHOTOREC_EXECUTABLE = "photorec_win.exe"; //NON-NLS + private static final String PHOTOREC_LINUX_EXECUTABLE = "photorec"; private static final String PHOTOREC_RESULTS_BASE = "results"; //NON-NLS private static final String PHOTOREC_RESULTS_EXTENDED = "results.1"; //NON-NLS private static final String PHOTOREC_REPORT = "report.xml"; //NON-NLS @@ -136,8 +138,8 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { this.rootOutputDirPath = createModuleOutputDirectoryForCase(); - Path execName = Paths.get(PHOTOREC_DIRECTORY, PHOTOREC_EXECUTABLE); - executableFile = locateExecutable(execName.toString()); + //Set photorec executable directory based on operating system. + executableFile = locateExecutable(); if (PhotoRecCarverFileIngestModule.refCounter.incrementAndGet(this.jobId) == 1) { try { @@ -222,13 +224,13 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { // Scan the file with Unallocated Carver. ProcessBuilder processAndSettings = new ProcessBuilder( - "\"" + executableFile + "\"", + executableFile.toString(), "/d", // NON-NLS - "\"" + outputDirPath.toAbsolutePath() + File.separator + PHOTOREC_RESULTS_BASE + "\"", + outputDirPath.toAbsolutePath().toString() + File.separator + PHOTOREC_RESULTS_BASE, "/cmd", // NON-NLS - "\"" + tempFilePath.toFile() + "\"", + tempFilePath.toFile().toString(), "search"); // NON-NLS - + // Add environment variable to force PhotoRec to run with the same permissions Autopsy uses processAndSettings.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS processAndSettings.redirectErrorStream(true); @@ -435,17 +437,32 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { * * @throws IngestModuleException */ - public static File locateExecutable(String executableToFindName) throws IngestModule.IngestModuleException { - // Must be running under a Windows operating system. - if (!PlatformUtil.isWindowsOS()) { - throw new IngestModule.IngestModuleException(Bundle.unsupportedOS_message()); + public static File locateExecutable() throws IngestModule.IngestModuleException { + File exeFile = null; + Path execName = null; + String photorec_linux_directory = "/usr/bin"; + if (PlatformUtil.isWindowsOS()) { + execName = Paths.get(PHOTOREC_DIRECTORY, PHOTOREC_EXECUTABLE); + exeFile = InstalledFileLocator.getDefault().locate(execName.toString(), PhotoRecCarverFileIngestModule.class.getPackage().getName(), false); + } else { + File usrBin = new File("/usr/bin/photorec"); + File usrLocalBin = new File("/usr/local/bin/photorec"); + if (usrBin.canExecute() && usrBin.exists() && !usrBin.isDirectory()) { + photorec_linux_directory = "/usr/bin"; + }else if(usrLocalBin.canExecute() && usrLocalBin.exists() && !usrLocalBin.isDirectory()){ + photorec_linux_directory = "/usr/local/bin"; + }else{ + throw new IngestModule.IngestModuleException("Photorec not found"); + } + execName = Paths.get(photorec_linux_directory, PHOTOREC_LINUX_EXECUTABLE); + exeFile = new File(execName.toString()); } - File exeFile = InstalledFileLocator.getDefault().locate(executableToFindName, PhotoRecCarverFileIngestModule.class.getPackage().getName(), false); if (null == exeFile) { throw new IngestModule.IngestModuleException(Bundle.missingExecutable_message()); } - + + if (!exeFile.canExecute()) { throw new IngestModule.IngestModuleException(Bundle.cannotRunExecutable_message()); } diff --git a/Core/src/org/sleuthkit/autopsy/report/taggedhashes/AddTaggedHashesToHashDb.java b/Core/src/org/sleuthkit/autopsy/report/taggedhashes/AddTaggedHashesToHashDb.java index 1de1db7eaa..d3560c514c 100644 --- a/Core/src/org/sleuthkit/autopsy/report/taggedhashes/AddTaggedHashesToHashDb.java +++ b/Core/src/org/sleuthkit/autopsy/report/taggedhashes/AddTaggedHashesToHashDb.java @@ -55,7 +55,7 @@ public class AddTaggedHashesToHashDb implements GeneralReportModule { @Override public String getDescription() { - return "Adds hashes of tagged files to a hash database."; + return "Adds hashes of tagged files to a hash set."; } @Override @@ -92,17 +92,17 @@ public class AddTaggedHashesToHashDb implements GeneralReportModule { try { hashSet.addHashes(tag.getContent(), Case.getCurrentCase().getDisplayName()); } catch (TskCoreException ex) { - Logger.getLogger(AddTaggedHashesToHashDb.class.getName()).log(Level.SEVERE, "Error adding hash for obj_id = " + tag.getContent().getId() + " to hash database " + hashSet.getHashSetName(), ex); + Logger.getLogger(AddTaggedHashesToHashDb.class.getName()).log(Level.SEVERE, "Error adding hash for obj_id = " + tag.getContent().getId() + " to hash set " + hashSet.getHashSetName(), ex); failedExports.add(tag.getContent().getName()); } } else { - JOptionPane.showMessageDialog(null, "Unable to add the " + (tags.size() > 1 ? "files" : "file") + " to the hash database. Hashes have not been calculated. Please configure and run an appropriate ingest module.", "Add to Hash Database Error", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(null, "Unable to add the " + (tags.size() > 1 ? "files" : "file") + " to the hash set. Hashes have not been calculated. Please configure and run an appropriate ingest module.", "Add to Hash Set Error", JOptionPane.ERROR_MESSAGE); break; } } } } catch (TskCoreException ex) { - Logger.getLogger(AddTaggedHashesToHashDb.class.getName()).log(Level.SEVERE, "Error adding to hash database", ex); + Logger.getLogger(AddTaggedHashesToHashDb.class.getName()).log(Level.SEVERE, "Error adding to hash set", ex); JOptionPane.showMessageDialog(null, "Error getting selected tags for case.", "Hash Export Error", JOptionPane.ERROR_MESSAGE); } } diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java new file mode 100644 index 0000000000..cce1e807e1 --- /dev/null +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java @@ -0,0 +1,2620 @@ +/* + * Central Repository + * + * Copyright 2018 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.datamodel; + +import java.io.IOException; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Set; +import java.util.HashSet; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Collectors; +import junit.framework.Test; +import junit.framework.TestCase; +import org.apache.commons.io.FileUtils; +import org.netbeans.junit.NbModuleSuite; +import org.openide.util.Exceptions; +import org.python.icu.impl.Assert; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.CaseActionException; +import org.sleuthkit.autopsy.casemodule.CaseDetails; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; +import org.sleuthkit.datamodel.TskData; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +/** + * + */ +public class CentralRepoDatamodelTest extends TestCase { + + private static final String PROPERTIES_FILE = "CentralRepository"; + private static final String CR_DB_NAME = "testcentralrepo.db"; + private static final Path testDirectory = Paths.get(System.getProperty("java.io.tmpdir"), "CentralRepoDatamodelTest"); + SqliteEamDbSettings dbSettingsSqlite; + + private CorrelationCase case1; + private CorrelationCase case2; + private CorrelationDataSource dataSource1fromCase1; + private CorrelationDataSource dataSource2fromCase1; + private CorrelationDataSource dataSource1fromCase2; + private EamOrganization org1; + private EamOrganization org2; + CorrelationAttribute.Type fileType; + + private Map propertiesMap = null; + + public static Test suite() { + NbModuleSuite.Configuration conf = NbModuleSuite.createConfiguration(CentralRepoDatamodelTest.class). + clusters(".*"). + enableModules(".*"); + return conf.suite(); + } + + @Override + public void setUp() { + dbSettingsSqlite = new SqliteEamDbSettings(); + + // Delete the test directory, if it exists + if (testDirectory.toFile().exists()) { + try { + FileUtils.deleteDirectory(testDirectory.toFile()); + } catch (IOException ex) { + Assert.fail(ex); + } + } + assertFalse("Unable to delete existing test directory", testDirectory.toFile().exists()); + + // Create the test directory + testDirectory.toFile().mkdirs(); + assertTrue("Unable to create test directory", testDirectory.toFile().exists()); + + // Save the current central repo settings + propertiesMap = ModuleSettings.getConfigSettings(PROPERTIES_FILE); + + try { + dbSettingsSqlite.setDbName(CR_DB_NAME); + dbSettingsSqlite.setDbDirectory(testDirectory.toString()); + if (!dbSettingsSqlite.dbDirectoryExists()) { + dbSettingsSqlite.createDbDirectory(); + } + + assertTrue("Failed to created central repo directory " + dbSettingsSqlite.getDbDirectory(), dbSettingsSqlite.dbDirectoryExists()); + + boolean result = dbSettingsSqlite.initializeDatabaseSchema() + && dbSettingsSqlite.insertDefaultDatabaseContent(); + + assertTrue("Failed to initialize central repo database", result); + + dbSettingsSqlite.saveSettings(); + EamDbUtil.setUseCentralRepo(true); + EamDbPlatformEnum.setSelectedPlatform(EamDbPlatformEnum.SQLITE.name()); + EamDbPlatformEnum.saveSelectedPlatform(); + } catch (Exception ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + Path crDbFilePath = Paths.get(testDirectory.toString(), CR_DB_NAME); + assertTrue("Failed to create central repo database at " + crDbFilePath, crDbFilePath.toFile().exists()); + + // Set up some default objects to be used by the tests + try { + case1 = new CorrelationCase("case1_uuid", "case1"); + case1 = EamDb.getInstance().newCase(case1); + assertTrue("Failed to create test object case1", case1 != null); + + case2 = new CorrelationCase("case2_uuid", "case2"); + case2 = EamDb.getInstance().newCase(case2); + assertTrue("Failed to create test object case2", case2 != null); + + dataSource1fromCase1 = new CorrelationDataSource(case1.getID(), "dataSource1_deviceID", "dataSource1"); + EamDb.getInstance().newDataSource(dataSource1fromCase1); + dataSource1fromCase1 = EamDb.getInstance().getDataSource(case1, dataSource1fromCase1.getDeviceID()); + assertTrue("Failed to create test object dataSource1fromCase1", dataSource1fromCase1 != null); + + dataSource2fromCase1 = new CorrelationDataSource(case1.getID(), "dataSource2_deviceID", "dataSource2"); + EamDb.getInstance().newDataSource(dataSource2fromCase1); + dataSource2fromCase1 = EamDb.getInstance().getDataSource(case1, dataSource2fromCase1.getDeviceID()); + assertTrue("Failed to create test object dataSource2fromCase1", dataSource2fromCase1 != null); + + dataSource1fromCase2 = new CorrelationDataSource(case2.getID(), "dataSource3_deviceID", "dataSource3"); + EamDb.getInstance().newDataSource(dataSource1fromCase2); + dataSource1fromCase2 = EamDb.getInstance().getDataSource(case2, dataSource1fromCase2.getDeviceID()); + assertTrue("Failed to create test object dataSource1fromCase2", dataSource1fromCase2 != null); + + org1 = new EamOrganization("org1"); + org1.setOrgID((int) EamDb.getInstance().newOrganization(org1)); + + org2 = new EamOrganization("org2"); + org2.setOrgID((int) EamDb.getInstance().newOrganization(org2)); + + // Store the file type object for later use + fileType = EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.FILES_TYPE_ID); + assertTrue("getCorrelationTypeById(FILES_TYPE_ID) returned null", fileType != null); + + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + } + + @Override + public void tearDown() { + + // Restore the original properties + ModuleSettings.setConfigSettings(PROPERTIES_FILE, propertiesMap); + + // Close and delete the test case and central repo db + try { + EamDb.getInstance().shutdownConnections(); + FileUtils.deleteDirectory(testDirectory.toFile()); + } catch (EamDbException | IOException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + assertFalse("Error deleting test directory " + testDirectory.toString(), testDirectory.toFile().exists()); + } + + /** + * Test the notable status of artifacts + * addArtifact(CorrelationAttribute eamArtifact) tests: + * - Test that two artifacts created with BAD status still have it when fetched from the database + * - Test that two artifacts created with BAD and KNOWN status still have the correct status when fetched from the database + * setArtifactInstanceKnownStatus(CorrelationAttribute eamArtifact, TskData.FileKnown knownStatus) tests: + * - Test updating status + * - Test updating artifact with two instances + * - Test updating null artifact + * - Test updating artifact with null known status + * - Test updating artifact with null case + * - Test updating artifact with null data source + * getArtifactInstancesKnownBad(CorrelationAttribute.Type aType, String value) tests: + * - Test getting two notable instances + * - Test getting notable instances where one instance is notable and the other is known + * - Test getting notable instances with null type + * - Test getting notable instances with null value + * getCountArtifactInstancesKnownBad(CorrelationAttribute.Type aType, String value) tests: + * - Test getting count of two notable instances + * - Test getting notable instance count where one instance is notable and the other is known + * - Test getting notable instance count with null type + * - Test getting notable instance count with null value + * getListCasesHavingArtifactInstancesKnownBad(CorrelationAttribute.Type aType, String value) tests: + * - Test getting cases with notable instances (all instances are notable) + * - Test getting cases with notable instances (only one instance is notable) + * - Test getting cases with null type + * - Test getting cases with null value + */ + public void testNotableArtifactStatus() { + + String notableHashInBothCases = "e34a8899ef6468b74f8a1048419ccc8b"; + String notableHashInOneCaseKnownOther = "d293f2f5cebcb427cde3bb95db5e1797"; + String hashToChangeToNotable = "23bd4ea37ec6304e75ac723527472a0f"; + + // Add two instances with notable status + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, notableHashInBothCases); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase1, "path1", + "", TskData.FileKnown.BAD)); + attr.addInstance(new CorrelationAttributeInstance(case2, dataSource1fromCase2, "path2", + "", TskData.FileKnown.BAD)); + EamDb.getInstance().addArtifact(attr); + + List attrs = EamDb.getInstance().getArtifactInstancesByTypeValue(fileType, notableHashInBothCases); + assertTrue("getArtifactInstancesByTypeValue returned " + attrs.size() + " values - expected 2", attrs.size() == 2); + for (CorrelationAttributeInstance a : attrs) { + assertTrue("Artifact did not have expected BAD status", a.getKnownStatus().equals(TskData.FileKnown.BAD)); + } + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Add two instances with one notable, one known + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, notableHashInOneCaseKnownOther); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase1, "path3", + "", TskData.FileKnown.BAD)); + attr.addInstance(new CorrelationAttributeInstance(case2, dataSource1fromCase2, "path4", + "", TskData.FileKnown.KNOWN)); + EamDb.getInstance().addArtifact(attr); + + List attrs = EamDb.getInstance().getArtifactInstancesByTypeValue(fileType, notableHashInOneCaseKnownOther); + assertTrue("getArtifactInstancesByTypeValue returned " + attrs.size() + " values - expected 2", attrs.size() == 2); + for (CorrelationAttributeInstance a : attrs) { + if (case1.getCaseUUID().equals(a.getCorrelationCase().getCaseUUID())) { + assertTrue("Artifact did not have expected BAD status", a.getKnownStatus().equals(TskData.FileKnown.BAD)); + } else if (case2.getCaseUUID().equals(a.getCorrelationCase().getCaseUUID())) { + assertTrue("Artifact did not have expected KNOWN status", a.getKnownStatus().equals(TskData.FileKnown.KNOWN)); + } else { + Assert.fail("getArtifactInstancesByTypeValue returned unexpected case"); + } + } + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Add an artifact and then update its status + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, hashToChangeToNotable); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase2, "path5", + "", TskData.FileKnown.KNOWN)); + EamDb.getInstance().addArtifact(attr); + + EamDb.getInstance().setArtifactInstanceKnownStatus(attr, TskData.FileKnown.BAD); + + List attrs = EamDb.getInstance().getArtifactInstancesByTypeValue(fileType, hashToChangeToNotable); + assertTrue("getArtifactInstancesByTypeValue returned " + attrs.size() + " values - expected 1", attrs.size() == 1); + assertTrue("Artifact status did not change to BAD", attrs.get(0).getKnownStatus().equals(TskData.FileKnown.BAD)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Try to update artifact with two CorrelationAttributeInstance instances + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, "badHash"); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase1, "badPath", + "", TskData.FileKnown.KNOWN)); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase2, "badPath", + "", TskData.FileKnown.KNOWN)); + + EamDb.getInstance().setArtifactInstanceKnownStatus(attr, TskData.FileKnown.BAD); + Assert.fail("setArtifactInstanceKnownStatus failed to throw exception for multiple Correlation Attribute Instances"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Try to update null artifact + try { + EamDb.getInstance().setArtifactInstanceKnownStatus(null, TskData.FileKnown.BAD); + Assert.fail("setArtifactInstanceKnownStatus failed to throw exception for null correlation attribute"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Try to update artifact with null known status + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, "badHash"); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase1, "badPath", + "", TskData.FileKnown.KNOWN)); + + EamDb.getInstance().setArtifactInstanceKnownStatus(attr, null); + Assert.fail("setArtifactInstanceKnownStatus failed to throw exception for null known status"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Try to update artifact with null case + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, "badHash"); + attr.addInstance(new CorrelationAttributeInstance(null, dataSource1fromCase1, "badPath", + "", TskData.FileKnown.KNOWN)); + + EamDb.getInstance().setArtifactInstanceKnownStatus(attr, TskData.FileKnown.BAD); + Assert.fail("setArtifactInstanceKnownStatus failed to throw exception for null case"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Try to update artifact with null data source + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, "badHash"); + attr.addInstance(new CorrelationAttributeInstance(case1, null, "badPath", + "", TskData.FileKnown.KNOWN)); + + EamDb.getInstance().setArtifactInstanceKnownStatus(attr, TskData.FileKnown.BAD); + Assert.fail("setArtifactInstanceKnownStatus failed to throw exception for null case"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting two notable instances + try { + List attrs = EamDb.getInstance().getArtifactInstancesKnownBad(fileType, notableHashInBothCases); + assertTrue("getArtifactInstancesKnownBad returned " + attrs.size() + " values - expected 2", attrs.size() == 2); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting notable instances where one instance is notable and the other is known + try { + List attrs = EamDb.getInstance().getArtifactInstancesKnownBad(fileType, notableHashInOneCaseKnownOther); + assertTrue("getArtifactInstancesKnownBad returned " + attrs.size() + " values - expected 1", attrs.size() == 1); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting notable instances with null type + try { + EamDb.getInstance().getArtifactInstancesKnownBad(null, notableHashInOneCaseKnownOther); + Assert.fail("getArtifactInstancesKnownBad failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting notable instances with null value (should work fine) + try { + List attrs = EamDb.getInstance().getArtifactInstancesKnownBad(fileType, null); + assertTrue("getArtifactInstancesKnownBad returned " + attrs.size() + " values - expected ", attrs.isEmpty()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting count of two notable instances + try { + long count = EamDb.getInstance().getCountArtifactInstancesKnownBad(fileType, notableHashInBothCases); + assertTrue("getCountArtifactInstancesKnownBad returned " + count + " values - expected 2", count == 2); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting notable instance count where one instance is notable and the other is known + try { + long count = EamDb.getInstance().getCountArtifactInstancesKnownBad(fileType, notableHashInOneCaseKnownOther); + assertTrue("getCountArtifactInstancesKnownBad returned " + count + " values - expected 1", count == 1); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting notable instance count with null type + try { + EamDb.getInstance().getCountArtifactInstancesKnownBad(null, notableHashInOneCaseKnownOther); + Assert.fail("getCountArtifactInstancesKnownBad failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting notable instance count with null value (should work fine) + try { + long count = EamDb.getInstance().getCountArtifactInstancesKnownBad(fileType, null); + assertTrue("getCountArtifactInstancesKnownBad returned " + count + " values - expected ", count == 0); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting cases with notable instances (all instances are notable) + try { + List cases = EamDb.getInstance().getListCasesHavingArtifactInstancesKnownBad(fileType, notableHashInBothCases); + assertTrue("getListCasesHavingArtifactInstancesKnownBad returned " + cases.size() + " values - expected 2", cases.size() == 2); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting cases with notable instances (only one instance is notable) + try { + List cases = EamDb.getInstance().getListCasesHavingArtifactInstancesKnownBad(fileType, notableHashInOneCaseKnownOther); + assertTrue("getListCasesHavingArtifactInstancesKnownBad returned " + cases.size() + " values - expected 1", cases.size() == 1); + assertTrue("getListCasesHavingArtifactInstancesKnownBad returned unexpected case " + cases.get(0), case1.getDisplayName().equals(cases.get(0))); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting cases with null type + try { + EamDb.getInstance().getListCasesHavingArtifactInstancesKnownBad(null, notableHashInOneCaseKnownOther); + Assert.fail("getListCasesHavingArtifactInstancesKnownBad failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting cases with null value (should work fine) + try { + List cases = EamDb.getInstance().getListCasesHavingArtifactInstancesKnownBad(fileType, null); + assertTrue("getListCasesHavingArtifactInstancesKnownBad returned " + cases.size() + " values - expected ", cases.isEmpty()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + } + + /** + * Test the methods associated with bulk artifacts (prepareBulkArtifact and + * bulkInsertArtifacts). + * First test the normal use case of a large number of valid artifacts getting added. + * Next test the error conditions: + * - Test preparing artifact with null type + * - Test preparing artifact with null case + * - Test preparing artifact with null data source + * - Test preparing artifact with null path + * - Test preparing artifact with null known status + */ + public void testBulkArtifacts() { + + // Test normal addition of bulk artifacts + // Steps: + // - Make a list of artifacts roughly half the threshold size + // - Call prepareBulkArtifact on all of them + // - Verify that nothing has been written to the database + // - Make a list of artifacts equal to the threshold size + // - Call prepareBulkArtifact on all of them + // - Verify that the bulk threshold number of them were written to the database + // - Call bulkInsertArtifacts to insert the remainder + // - Verify that the database now has all the artifacts + try { + // Make sure there are no artifacts in the database to start + long originalArtifactCount = EamDb.getInstance().getCountArtifactInstancesByCaseDataSource(case1.getCaseUUID(), dataSource1fromCase1.getDeviceID()); + assertTrue("getCountArtifactInstancesByCaseDataSource returned non-zero count", originalArtifactCount == 0); + + // Create the first list, which will have (bulkThreshold / 2) entries + List list1 = new ArrayList<>(); + for (int i = 0; i < dbSettingsSqlite.getBulkThreshold() / 2; i++) { + String value = "bulkInsertValue1_" + String.valueOf(i); + String path = "C:\\bulkInsertPath1\\file" + String.valueOf(i); + + CorrelationAttribute attr = new CorrelationAttribute(fileType, value); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase1, path)); + list1.add(attr); + } + + // Queue up the current list. There should not be enough to trigger the insert + for (CorrelationAttribute attr : list1) { + EamDb.getInstance().prepareBulkArtifact(attr); + } + + // Check that nothing has been written yet + assertTrue("Artifacts written to database before threshold was reached", + originalArtifactCount == EamDb.getInstance().getCountArtifactInstancesByCaseDataSource(case1.getCaseUUID(), dataSource1fromCase1.getDeviceID())); + + // Make a second list with length equal to bulkThreshold + List list2 = new ArrayList<>(); + for (int i = 0; i < dbSettingsSqlite.getBulkThreshold(); i++) { + String value = "bulkInsertValue2_" + String.valueOf(i); + String path = "C:\\bulkInsertPath2\\file" + String.valueOf(i); + + CorrelationAttribute attr = new CorrelationAttribute(fileType, value); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase1, path)); + list2.add(attr); + } + + // Queue up the current list. This will trigger an insert partway through + for (CorrelationAttribute attr : list2) { + EamDb.getInstance().prepareBulkArtifact(attr); + } + + // There should now be bulkThreshold artifacts in the database + long count = EamDb.getInstance().getCountArtifactInstancesByCaseDataSource(case1.getCaseUUID(), dataSource1fromCase1.getDeviceID()); + assertTrue("Artifact count " + count + " does not match bulkThreshold " + dbSettingsSqlite.getBulkThreshold(), count == dbSettingsSqlite.getBulkThreshold()); + + // Now call bulkInsertArtifacts() to insert the rest of queue + EamDb.getInstance().bulkInsertArtifacts(); + count = EamDb.getInstance().getCountArtifactInstancesByCaseDataSource(case1.getCaseUUID(), dataSource1fromCase1.getDeviceID()); + int expectedCount = list1.size() + list2.size(); + assertTrue("Artifact count " + count + " does not match expected count " + expectedCount, count == expectedCount); + + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test preparing artifact with null type + try { + CorrelationAttribute attr = new CorrelationAttribute(null, "value"); + EamDb.getInstance().prepareBulkArtifact(attr); + Assert.fail("prepareBulkArtifact failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test preparing artifact with null case + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, "value"); + attr.addInstance(new CorrelationAttributeInstance(null, dataSource1fromCase1, "path")); + EamDb.getInstance().prepareBulkArtifact(attr); + EamDb.getInstance().bulkInsertArtifacts(); + Assert.fail("bulkInsertArtifacts failed to throw exception for null case"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test preparing artifact with null data source + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, "value"); + attr.addInstance(new CorrelationAttributeInstance(case1, null, "path")); + EamDb.getInstance().prepareBulkArtifact(attr); + EamDb.getInstance().bulkInsertArtifacts(); + Assert.fail("prepareBulkArtifact failed to throw exception for null data source"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test preparing artifact with null path + // CorrelationAttributeInstance will throw an exception + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, "value"); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase1, null)); + Assert.fail("CorrelationAttributeInstance failed to throw exception for null path"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test preparing artifact with null known status + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, "value"); + attr.addInstance(new CorrelationAttributeInstance(case1, dataSource1fromCase1, "path", "comment", null)); + EamDb.getInstance().prepareBulkArtifact(attr); + EamDb.getInstance().bulkInsertArtifacts(); + Assert.fail("prepareBulkArtifact failed to throw exception for null known status"); + } catch (EamDbException ex) { + // This is the expected behavior + } + } + + /** + * Test most methods related to artifacts + * addArtifact(CorrelationAttribute eamArtifact) tests: + * - Test adding artifact with one instance + * - Test adding artifact with one instance in each data source + * - Test adding artifact with two instances in the same data source + * - Test adding email artifact + * - Test adding phone artifact + * - Test adding domain artifact + * - Test adding device artifact + * - Test adding artifact with null case + * - Test adding artifact with invalid case ID + * - Test adding artifact with null data source + * - Test adding artifact with invalid data source ID + * - Test adding artifact with null path + * - Test adding artifact with null known status + * - Test adding artifact with null correlation type + * - Test adding artifact with null value + * getArtifactInstancesByTypeValue(CorrelationAttribute.Type aType, String value) tests: + * - Test getting three expected instances + * - Test getting no expected instances + * - Test with null type + * - Test with null value + * getArtifactInstancesByPath(CorrelationAttribute.Type aType, String filePath) tests: + * - Test with existing path + * - Test with non-existent path + * - Test with null type + * - Test with null path + * getCountArtifactInstancesByTypeValue(CorrelationAttribute.Type aType, String value) tests: + * - Test getting three expected instances + * - Test getting no expected instances + * - Test with null type + * - Test with null value + * getFrequencyPercentage(CorrelationAttribute corAttr) tests: + * - Test value in every data source + * - Test value in one data source twice + * - Test email + * - Test value in no data sources + * - Test with null type + * - Test with null attribute + * getCountArtifactInstancesByCaseDataSource(String caseUUID, String dataSourceID) tests: + * - Test data source with seven instances + * - Test with null case UUID + * - Test with null device ID + * getCountUniqueCaseDataSourceTuplesHavingTypeValue(CorrelationAttribute.Type aType, String value) tests: + * - Test value in every data source + * - Test value in one data source twice + * - Test value in no data sources + * - Test with null type + * - Test with null value + */ + public void testArtifacts() { + + String inAllDataSourcesHash = "6cddb0e31787b79cfdcc0676b98a71ce"; + String inAllDataSourcesPath = "C:\\files\\path0.txt"; + String inDataSource1twiceHash = "b2f5ff47436671b6e533d8dc3614845d"; + String inDataSource1twicePath1 = "C:\\files\\path1.txt"; + String inDataSource1twicePath2 = "C:\\files\\path2.txt"; + String onlyInDataSource3Hash = "2af54305f183778d87de0c70c591fae4"; + String onlyInDataSource3Path = "C:\\files\\path3.txt"; + + // These will all go in dataSource1fromCase1 + String emailValue = "test@gmail.com"; + String emailPath = "C:\\files\\emailPath.txt"; + String phoneValue = "202-555-1234"; + String phonePath = "C:\\files\\phonePath.txt"; + String domainValue = "www.mozilla.com"; + String domainPath = "C:\\files\\domainPath.txt"; + String devIdValue = "94B21234"; + String devIdPath = "C:\\files\\devIdPath.txt"; + + // Store the email type + CorrelationAttribute.Type emailType; + try { + emailType = EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.EMAIL_TYPE_ID); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test adding attribute with one instance + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, onlyInDataSource3Hash); + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(case2, dataSource1fromCase2, onlyInDataSource3Path); + attr.addInstance(inst); + EamDb.getInstance().addArtifact(attr); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test adding attribute with an instance in each data source + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, inAllDataSourcesHash); + CorrelationAttributeInstance inst1 = new CorrelationAttributeInstance(case1, dataSource1fromCase1, inAllDataSourcesPath); + attr.addInstance(inst1); + CorrelationAttributeInstance inst2 = new CorrelationAttributeInstance(case1, dataSource2fromCase1, inAllDataSourcesPath); + attr.addInstance(inst2); + CorrelationAttributeInstance inst3 = new CorrelationAttributeInstance(case2, dataSource1fromCase2, inAllDataSourcesPath); + attr.addInstance(inst3); + EamDb.getInstance().addArtifact(attr); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test adding attribute with two instances in one data source + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, inDataSource1twiceHash); + CorrelationAttributeInstance inst1 = new CorrelationAttributeInstance(case1, dataSource1fromCase1, inDataSource1twicePath1); + attr.addInstance(inst1); + CorrelationAttributeInstance inst2 = new CorrelationAttributeInstance(case1, dataSource1fromCase1, inDataSource1twicePath2); + attr.addInstance(inst2); + EamDb.getInstance().addArtifact(attr); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test adding the other types + // Test adding an email artifact + try { + CorrelationAttribute attr = new CorrelationAttribute(emailType, emailValue); + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(case1, dataSource1fromCase1, emailPath); + attr.addInstance(inst); + EamDb.getInstance().addArtifact(attr); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test adding a phone artifact + try { + CorrelationAttribute attr = new CorrelationAttribute(EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.PHONE_TYPE_ID), + phoneValue); + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(case1, dataSource1fromCase1, phonePath); + attr.addInstance(inst); + EamDb.getInstance().addArtifact(attr); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test adding a domain artifact + try { + CorrelationAttribute attr = new CorrelationAttribute(EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.DOMAIN_TYPE_ID), + domainValue); + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(case1, dataSource1fromCase1, domainPath); + attr.addInstance(inst); + EamDb.getInstance().addArtifact(attr); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test adding a device ID artifact + try { + CorrelationAttribute attr = new CorrelationAttribute(EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.USBID_TYPE_ID), + devIdValue); + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(case1, dataSource1fromCase1, devIdPath); + attr.addInstance(inst); + EamDb.getInstance().addArtifact(attr); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test CorrelationAttributeInstance failure cases + // Create an attribute to use in the next few tests + CorrelationAttribute failAttr; + try { + failAttr = new CorrelationAttribute(fileType, "badInstances"); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test adding instance with null case + try { + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(null, dataSource1fromCase2, "badPath"); + failAttr.addInstance(inst); + EamDb.getInstance().addArtifact(failAttr); + Assert.fail("addArtifact failed to throw exception for null case"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test adding instance with invalid case ID + try { + CorrelationCase badCase = new CorrelationCase("badCaseUuid", "badCaseName"); + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(badCase, dataSource1fromCase2, "badPath"); + failAttr.addInstance(inst); + EamDb.getInstance().addArtifact(failAttr); + Assert.fail("addArtifact failed to throw exception for invalid case"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test adding instance with null data source + try { + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(case1, null, "badPath"); + failAttr.addInstance(inst); + EamDb.getInstance().addArtifact(failAttr); + Assert.fail("addArtifact failed to throw exception for null data source"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test adding instance with invalid data source ID + try { + CorrelationDataSource badDS = new CorrelationDataSource(case1.getID(), "badDSUuid", "badDSName"); + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(case1, badDS, "badPath"); + failAttr.addInstance(inst); + EamDb.getInstance().addArtifact(failAttr); + Assert.fail("addArtifact failed to throw exception for invalid data source"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test adding instance with null path + // This will fail in the CorrelationAttributeInstance constructor + try { + new CorrelationAttributeInstance(case1, dataSource1fromCase1, null); + Assert.fail("CorrelationAttributeInstance failed to throw exception for null path"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test adding instance with null known status + try { + CorrelationAttributeInstance inst = new CorrelationAttributeInstance(case1, dataSource1fromCase1, null, "comment", null); + failAttr.addInstance(inst); + EamDb.getInstance().addArtifact(failAttr); + Assert.fail("addArtifact failed to throw exception for null known status"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test CorrelationAttribute failure cases + // Test null type + try { + CorrelationAttribute attr = new CorrelationAttribute(null, "badInstances"); + EamDb.getInstance().addArtifact(attr); + Assert.fail("addArtifact failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test null value + // This will fail in the CorrelationAttribute constructor + try { + new CorrelationAttribute(fileType, null); + Assert.fail("addArtifact failed to throw exception for null value"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting instances with expected resuls + try { + List instances = EamDb.getInstance().getArtifactInstancesByTypeValue(fileType, inAllDataSourcesHash); + assertTrue("getArtifactInstancesByTypeValue returned " + instances.size() + " results - expected 3", instances.size() == 3); + + // This test works because all the instances of this hash were set to the same path + for (CorrelationAttributeInstance inst : instances) { + assertTrue("getArtifactInstancesByTypeValue returned instance with unexpected path " + inst.getFilePath(), + inAllDataSourcesPath.equalsIgnoreCase(inst.getFilePath())); + } + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting instances expecting no results + try { + List instances = EamDb.getInstance().getArtifactInstancesByTypeValue( + emailType, inAllDataSourcesHash); + assertTrue("getArtifactInstancesByTypeValue returned " + instances.size() + " results - expected 0", instances.isEmpty()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting instances with null type + try { + EamDb.getInstance().getArtifactInstancesByTypeValue(null, inAllDataSourcesHash); + Assert.fail("getArtifactInstancesByTypeValue failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting instances with null value + // Should just return nothing + try { + List instances = EamDb.getInstance().getArtifactInstancesByTypeValue(fileType, null); + assertTrue("getArtifactInstancesByTypeValue returned non-empty list for null value", instances.isEmpty()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting instances with path that should produce results + try { + List instances = EamDb.getInstance().getArtifactInstancesByPath(fileType, inAllDataSourcesPath); + assertTrue("getArtifactInstancesByPath returned " + instances.size() + " objects - expected 3", instances.size() == 3); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting instances with path that should not produce results + try { + List instances = EamDb.getInstance().getArtifactInstancesByPath(fileType, "xyz"); + assertTrue("getArtifactInstancesByPath returned " + instances.size() + " objects - expected 0", instances.isEmpty()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting instances with null type + try { + EamDb.getInstance().getArtifactInstancesByPath(null, inAllDataSourcesPath); + Assert.fail("getArtifactInstancesByPath failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting instances with null path + try { + EamDb.getInstance().getArtifactInstancesByPath(fileType, null); + Assert.fail("getArtifactInstancesByPath failed to throw exception for null path"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting instance count with path that should produce results + try { + long count = EamDb.getInstance().getCountArtifactInstancesByTypeValue(fileType, inAllDataSourcesHash); + assertTrue("getCountArtifactInstancesByTypeValue returned " + count + " - expected 3", count == 3); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting instance count with path that should not produce results + try { + long count = EamDb.getInstance().getCountArtifactInstancesByTypeValue(fileType, "xyz"); + assertTrue("getCountArtifactInstancesByTypeValue returned " + count + " - expected 0", count == 0); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting instance count with null type + try { + EamDb.getInstance().getCountArtifactInstancesByTypeValue(null, inAllDataSourcesHash); + Assert.fail("getCountArtifactInstancesByTypeValue failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting instance count with null value + try { + EamDb.getInstance().getCountArtifactInstancesByTypeValue(fileType, null); + Assert.fail("getCountArtifactInstancesByTypeValue failed to throw exception for null value"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting frequency of value that is in all three data sources + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, inAllDataSourcesHash); + int freq = EamDb.getInstance().getFrequencyPercentage(attr); + assertTrue("getFrequencyPercentage returned " + freq + " - expected 100", freq == 100); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting frequency of value that appears twice in a single data source + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, inDataSource1twiceHash); + int freq = EamDb.getInstance().getFrequencyPercentage(attr); + assertTrue("getFrequencyPercentage returned " + freq + " - expected 33", freq == 33); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting frequency of non-file type + try { + CorrelationAttribute attr = new CorrelationAttribute(emailType, emailValue); + int freq = EamDb.getInstance().getFrequencyPercentage(attr); + assertTrue("getFrequencyPercentage returned " + freq + " - expected 33", freq == 33); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting frequency of non-existent value + try { + CorrelationAttribute attr = new CorrelationAttribute(fileType, "randomValue"); + int freq = EamDb.getInstance().getFrequencyPercentage(attr); + assertTrue("getFrequencyPercentage returned " + freq + " - expected 0", freq == 0); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting frequency with null type + try { + CorrelationAttribute attr = new CorrelationAttribute(null, "randomValue"); + EamDb.getInstance().getFrequencyPercentage(attr); + Assert.fail("getFrequencyPercentage failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting frequency with null attribute + try { + EamDb.getInstance().getFrequencyPercentage(null); + Assert.fail("getFrequencyPercentage failed to throw exception for null attribute"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting count for dataSource1fromCase1 (includes all types) + try { + long count = EamDb.getInstance().getCountArtifactInstancesByCaseDataSource(case1.getCaseUUID(), dataSource1fromCase1.getDeviceID()); + assertTrue("getCountArtifactInstancesByCaseDataSource returned " + count + " - expected 7", count == 7); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting count with null case UUID + try { + long count = EamDb.getInstance().getCountArtifactInstancesByCaseDataSource(null, dataSource1fromCase1.getDeviceID()); + assertTrue("getCountArtifactInstancesByCaseDataSource returned " + count + " - expected 0", count == 0); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting count with null device ID + try { + long count = EamDb.getInstance().getCountArtifactInstancesByCaseDataSource(case1.getCaseUUID(), null); + assertTrue("getCountArtifactInstancesByCaseDataSource returned " + count + " - expected 0", count == 0); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting data source count for entry that is in all three + try { + long count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, inAllDataSourcesHash); + assertTrue("getCountUniqueCaseDataSourceTuplesHavingTypeValue returned " + count + " - expected 3", count == 3); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting data source count for entry that is in one data source twice + try { + long count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, inDataSource1twiceHash); + assertTrue("getCountUniqueCaseDataSourceTuplesHavingTypeValue returned " + count + " - expected 1", count == 1); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting data source count for entry that is not in any data sources + try { + long count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, "abcdef"); + assertTrue("getCountUniqueCaseDataSourceTuplesHavingTypeValue returned " + count + " - expected 0", count == 0); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting data source count for null type + try { + EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(null, "abcdef"); + Assert.fail("getCountUniqueCaseDataSourceTuplesHavingTypeValue failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting data source count for null value + try { + long count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(fileType, null); + assertTrue("getCountUniqueCaseDataSourceTuplesHavingTypeValue returned " + count + " - expected 0", count == 0); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + } + + /** + * Test methods related to correlation types + * newCorrelationType(CorrelationAttribute.Type newType) tests: + * - Test with valid data + * - Test with duplicate data + * - Test with null name + * - Test with null db name + * - Test with null type + * getDefinedCorrelationTypes() tests: + * - Test that the expected number are returned + * getEnabledCorrelationTypes() tests: + * - Test that the expected number are returned + * getSupportedCorrelationTypes() tests: + * - Test that the expected number are returned + * getCorrelationTypeById(int typeId) tests: + * - Test with valid ID + * - Test with invalid ID + * updateCorrelationType(CorrelationAttribute.Type aType) tests: + * - Test with existing type + * - Test with non-existent type + * - Test updating to null name + * - Test with null type + */ + public void testCorrelationTypes() { + + CorrelationAttribute.Type customType; + String customTypeName = "customType"; + String customTypeDb = "custom_type"; + + // Test new type with valid data + try { + customType = new CorrelationAttribute.Type(customTypeName, customTypeDb, false, false); + customType.setId(EamDb.getInstance().newCorrelationType(customType)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test new type with duplicate data + try { + CorrelationAttribute.Type temp = new CorrelationAttribute.Type(customTypeName, customTypeDb, false, false); + EamDb.getInstance().newCorrelationType(temp); + Assert.fail("newCorrelationType failed to throw exception for duplicate name/db table"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test new type with null name + try { + CorrelationAttribute.Type temp = new CorrelationAttribute.Type(null, "temp_type", false, false); + EamDb.getInstance().newCorrelationType(temp); + Assert.fail("newCorrelationType failed to throw exception for null name table"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test new type with null db name + // The constructor should fail in this case + try { + new CorrelationAttribute.Type("temp", null, false, false); + Assert.fail("CorrelationAttribute.Type failed to throw exception for null db table name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test new type with null type + try { + EamDb.getInstance().newCorrelationType(null); + Assert.fail("newCorrelationType failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting all correlation types + try { + List types = EamDb.getInstance().getDefinedCorrelationTypes(); + + // We expect 6 total - 5 default and the custom one made earlier + assertTrue("getDefinedCorrelationTypes returned " + types.size() + " entries - expected 6", types.size() == 6); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting enabled correlation types + try { + List types = EamDb.getInstance().getEnabledCorrelationTypes(); + + // We expect 5 - the custom type is disabled + assertTrue("getDefinedCorrelationTypes returned " + types.size() + " enabled entries - expected 5", types.size() == 5); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting supported correlation types + try { + List types = EamDb.getInstance().getSupportedCorrelationTypes(); + + // We expect 5 - the custom type is not supported + assertTrue("getDefinedCorrelationTypes returned " + types.size() + " supported entries - expected 5", types.size() == 5); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting the type with a valid ID + try { + CorrelationAttribute.Type temp = EamDb.getInstance().getCorrelationTypeById(customType.getId()); + assertTrue("getCorrelationTypeById returned type with unexpected name " + temp.getDisplayName(), customTypeName.equals(temp.getDisplayName())); + assertTrue("getCorrelationTypeById returned type with unexpected db table name " + temp.getDbTableName(), customTypeDb.equals(temp.getDbTableName())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting the type with a invalid ID + try { + EamDb.getInstance().getCorrelationTypeById(5555); + Assert.fail("getCorrelationTypeById failed to throw exception for invalid ID"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test updating a valid type + try { + String newName = "newName"; + String newDbTable = "new_db_table"; + customType.setDisplayName(newName); + customType.setDbTableName(newDbTable); + customType.setEnabled(true); // These were originally false + customType.setSupported(true); + + EamDb.getInstance().updateCorrelationType(customType); + + // Get a fresh copy from the database + CorrelationAttribute.Type temp = EamDb.getInstance().getCorrelationTypeById(customType.getId()); + + assertTrue("updateCorrelationType failed to update name", newName.equals(temp.getDisplayName())); + assertTrue("updateCorrelationType failed to update db table name", newDbTable.equals(temp.getDbTableName())); + assertTrue("updateCorrelationType failed to update enabled status", temp.isEnabled()); + assertTrue("updateCorrelationType failed to update supported status", temp.isSupported()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test updating a type with an invalid ID + // Nothing should happen + try { + CorrelationAttribute.Type temp = new CorrelationAttribute.Type(customTypeName, customTypeDb, false, false); + temp.setId(12345); + EamDb.getInstance().updateCorrelationType(temp); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test updating a type to a null name + try { + customType.setDisplayName(null); + EamDb.getInstance().updateCorrelationType(customType); + Assert.fail("updateCorrelationType failed to throw exception for null name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test updating a null type + try { + customType.setDisplayName(null); + EamDb.getInstance().updateCorrelationType(customType); + Assert.fail("updateCorrelationType failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + } + + /** + * Test the methods related to organizations + * newOrganization(EamOrganization eamOrg) tests: + * - Test with just org name + * - Test with org name and poc info + * - Test adding duplicate org + * - Test adding null org + * - Test adding org with null name + * getOrganizations() tests: + * - Test getting the list of orgs + * getOrganizationByID(int orgID) tests: + * - Test with valid ID + * - Test with invalid ID + * updateOrganization(EamOrganization updatedOrganization) tests: + * - Test updating valid org + * - Test updating invalid org + * - Test updating null org + * - Test updating org to null name + * deleteOrganization(EamOrganization organizationToDelete) tests: + * - Test deleting org that isn't in use + * - Test deleting org that is in use + * - Test deleting invalid org + * - Test deleting null org + */ + public void testOrganizations() { + + EamOrganization orgA; + String orgAname = "orgA"; + EamOrganization orgB; + String orgBname = "orgB"; + String orgBpocName = "pocName"; + String orgBpocEmail = "pocEmail"; + String orgBpocPhone = "pocPhone"; + + // Test adding a basic organization + try { + orgA = new EamOrganization(orgAname); + orgA.setOrgID((int) EamDb.getInstance().newOrganization(orgA)); + assertTrue("Organization ID is still -1 after adding to db", orgA.getOrgID() != -1); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test adding an organization with additional fields + try { + orgB = new EamOrganization(orgBname, orgBpocName, orgBpocEmail, orgBpocPhone); + orgB.setOrgID((int) EamDb.getInstance().newOrganization(orgB)); + assertTrue("Organization ID is still -1 after adding to db", orgB.getOrgID() != -1); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test adding a duplicate organization + try { + EamOrganization temp = new EamOrganization(orgAname); + EamDb.getInstance().newOrganization(temp); + Assert.fail("newOrganization failed to throw exception for duplicate org name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test adding null organization + try { + EamDb.getInstance().newOrganization(null); + Assert.fail("newOrganization failed to throw exception for null org"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test adding organization with null name + try { + EamOrganization temp = new EamOrganization(null); + EamDb.getInstance().newOrganization(temp); + Assert.fail("newOrganization failed to throw exception for null name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting organizations + // We expect five - the default org, two from setUp, and two from this method + try { + List orgs = EamDb.getInstance().getOrganizations(); + assertTrue("getOrganizations returned null list", orgs != null); + assertTrue("getOrganizations returned " + orgs.size() + " orgs - expected 5", orgs.size() == 5); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting org with valid ID + try { + EamOrganization temp = EamDb.getInstance().getOrganizationByID(orgB.getOrgID()); + assertTrue("getOrganizationByID returned null for valid ID", temp != null); + assertTrue("getOrganizationByID returned unexpected name for organization", orgBname.equals(temp.getName())); + assertTrue("getOrganizationByID returned unexpected poc name for organization", orgBpocName.equals(temp.getPocName())); + assertTrue("getOrganizationByID returned unexpected poc email for organization", orgBpocEmail.equals(temp.getPocEmail())); + assertTrue("getOrganizationByID returned unexpected poc phone for organization", orgBpocPhone.equals(temp.getPocPhone())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting org with invalid ID + try { + EamDb.getInstance().getOrganizationByID(12345); + Assert.fail("getOrganizationByID failed to throw exception for invalid ID"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test updating valid org + try { + String newName = "newOrgName"; + String newPocName = "newPocName"; + String newPocEmail = "newPocEmail"; + String newPocPhone = "newPocPhone"; + orgA.setName(newName); + orgA.setPocName(newPocName); + orgA.setPocEmail(newPocEmail); + orgA.setPocPhone(newPocPhone); + + EamDb.getInstance().updateOrganization(orgA); + + EamOrganization copyOfA = EamDb.getInstance().getOrganizationByID(orgA.getOrgID()); + + assertTrue("getOrganizationByID returned null for valid ID", copyOfA != null); + assertTrue("updateOrganization failed to update org name", newName.equals(copyOfA.getName())); + assertTrue("updateOrganization failed to update poc name", newPocName.equals(copyOfA.getPocName())); + assertTrue("updateOrganization failed to update poc email", newPocEmail.equals(copyOfA.getPocEmail())); + assertTrue("updateOrganization failed to update poc phone", newPocPhone.equals(copyOfA.getPocPhone())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test updating invalid org + // Shouldn't do anything + try { + EamOrganization temp = new EamOrganization("invalidOrg"); + temp.setOrgID(3434); + EamDb.getInstance().updateOrganization(temp); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test updating null org + try { + EamDb.getInstance().updateOrganization(null); + Assert.fail("updateOrganization failed to throw exception for null org"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test updating org to null name + try { + EamOrganization copyOfA = EamDb.getInstance().getOrganizationByID(orgA.getOrgID()); + copyOfA.setName(null); + EamDb.getInstance().updateOrganization(copyOfA); + Assert.fail("updateOrganization failed to throw exception for null name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test deleting existing org that isn't in use + try { + EamOrganization orgToDelete = new EamOrganization("deleteThis"); + orgToDelete.setOrgID((int) EamDb.getInstance().newOrganization(orgToDelete)); + int orgCount = EamDb.getInstance().getOrganizations().size(); + + EamDb.getInstance().deleteOrganization(orgToDelete); + assertTrue("getOrganizations returned unexpected count after deletion", orgCount - 1 == EamDb.getInstance().getOrganizations().size()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test deleting existing org that is in use + try { + // Make a new org + EamOrganization inUseOrg = new EamOrganization("inUseOrg"); + inUseOrg.setOrgID((int) EamDb.getInstance().newOrganization(inUseOrg)); + + // Make a reference set that uses it + EamGlobalSet tempSet = new EamGlobalSet(inUseOrg.getOrgID(), "inUseOrgTest", "1.0", TskData.FileKnown.BAD, false, fileType); + EamDb.getInstance().newReferenceSet(tempSet); + + // It should now throw an exception if we try to delete it + EamDb.getInstance().deleteOrganization(inUseOrg); + Assert.fail("deleteOrganization failed to throw exception for in use organization"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test deleting non-existent org + // Should do nothing + try { + EamOrganization temp = new EamOrganization("temp"); + temp.setOrgID(9876); + EamDb.getInstance().deleteOrganization(temp); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test deleting null org + try { + EamDb.getInstance().deleteOrganization(null); + Assert.fail("deleteOrganization failed to throw exception for null organization"); + } catch (EamDbException ex) { + // This is the expected behavior + } + } + + /** + * Tests for adding / retrieving reference instances + * Only the files type is currently implemented + * addReferenceInstance(EamGlobalFileInstance eamGlobalFileInstance, CorrelationAttribute.Type correlationType) tests: + * - Test adding multiple valid entries + * - Test invalid reference set ID + * - Test null hash (EamGlobalFileInstance constructor) + * - Test null known status (EamGlobalFileInstance constructor) + * - Test null correlation type + * bulkInsertReferenceTypeEntries(Set globalInstances, CorrelationAttribute.Type contentType) tests: + * - Test with large valid list + * - Test with null list + * - Test with invalid reference set ID + * - Test with null correlation type + * getReferenceInstancesByTypeValue(CorrelationAttribute.Type aType, String aValue) tests: + * - Test with valid entries + * - Test with non-existent value + * - Test with invalid type + * - Test with null type + * - Test with null value + * isFileHashInReferenceSet(String hash, int referenceSetID)tests: + * - Test existing hash/ID + * - Test non-existent (but valid) hash/ID + * - Test invalid ID + * - Test null hash + * isValueInReferenceSet(String value, int referenceSetID, int correlationTypeID) tests: + * - Test existing value/ID + * - Test non-existent (but valid) value/ID + * - Test invalid ID + * - Test null value + * - Test invalid type ID + * isArtifactKnownBadByReference(CorrelationAttribute.Type aType, String value) tests: + * - Test notable value + * - Test known value + * - Test non-existent value + * - Test null value + * - Test null type + * - Test invalid type + */ + public void testReferenceSetInstances() { + + // After the two initial testing blocks, the reference sets should contain: + // notableSet1 - notableHash1, inAllSetsHash + // notableSet2 - inAllSetsHash + // knownSet1 - knownHash1, inAllSetsHash + EamGlobalSet notableSet1; + int notableSet1id; + EamGlobalSet notableSet2; + int notableSet2id; + EamGlobalSet knownSet1; + int knownSet1id; + + String notableHash1 = "d46feecd663c41648dbf690d9343cf4b"; + String knownHash1 = "39c844daee70485143da4ff926601b5b"; + String inAllSetsHash = "6449b39bb23c42879fa0c243726e27f7"; + + CorrelationAttribute.Type emailType; + + // Store the email type object for later use + try { + emailType = EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.EMAIL_TYPE_ID); + assertTrue("getCorrelationTypeById(EMAIL_TYPE_ID) returned null", emailType != null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Set up a few reference sets + try { + notableSet1 = new EamGlobalSet(org1.getOrgID(), "notable set 1", "1.0", TskData.FileKnown.BAD, false, fileType); + notableSet1id = EamDb.getInstance().newReferenceSet(notableSet1); + notableSet2 = new EamGlobalSet(org1.getOrgID(), "notable set 2", "2.4", TskData.FileKnown.BAD, false, fileType); + notableSet2id = EamDb.getInstance().newReferenceSet(notableSet2); + knownSet1 = new EamGlobalSet(org1.getOrgID(), "known set 1", "5.5.4", TskData.FileKnown.KNOWN, false, fileType); + knownSet1id = EamDb.getInstance().newReferenceSet(knownSet1); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test adding file instances with valid data + try { + EamGlobalFileInstance temp = new EamGlobalFileInstance(notableSet1id, inAllSetsHash, TskData.FileKnown.BAD, "comment1"); + EamDb.getInstance().addReferenceInstance(temp, fileType); + + temp = new EamGlobalFileInstance(notableSet2id, inAllSetsHash, TskData.FileKnown.BAD, "comment2"); + EamDb.getInstance().addReferenceInstance(temp, fileType); + + temp = new EamGlobalFileInstance(knownSet1id, inAllSetsHash, TskData.FileKnown.KNOWN, "comment3"); + EamDb.getInstance().addReferenceInstance(temp, fileType); + + temp = new EamGlobalFileInstance(notableSet1id, notableHash1, TskData.FileKnown.BAD, "comment4"); + EamDb.getInstance().addReferenceInstance(temp, fileType); + + temp = new EamGlobalFileInstance(knownSet1id, knownHash1, TskData.FileKnown.KNOWN, "comment5"); + EamDb.getInstance().addReferenceInstance(temp, fileType); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test adding file instance with invalid reference set ID + try { + EamGlobalFileInstance temp = new EamGlobalFileInstance(2345, inAllSetsHash, TskData.FileKnown.BAD, "comment"); + EamDb.getInstance().addReferenceInstance(temp, fileType); + Assert.fail("addReferenceInstance failed to throw exception for invalid ID"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating file instance with null hash + // Since it isn't possible to get a null hash into the EamGlobalFileInstance, skip trying to + // call addReferenceInstance and just test the EamGlobalFileInstance constructor + try { + new EamGlobalFileInstance(notableSet1id, null, TskData.FileKnown.BAD, "comment"); + Assert.fail("EamGlobalFileInstance failed to throw exception for null hash"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test adding file instance with null known status + // Since it isn't possible to get a null known status into the EamGlobalFileInstance, skip trying to + // call addReferenceInstance and just test the EamGlobalFileInstance constructor + try { + new EamGlobalFileInstance(notableSet1id, inAllSetsHash, null, "comment"); + Assert.fail("EamGlobalFileInstance failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test adding file instance with null correlation type + try { + EamGlobalFileInstance temp = new EamGlobalFileInstance(notableSet1id, inAllSetsHash, TskData.FileKnown.BAD, "comment"); + EamDb.getInstance().addReferenceInstance(temp, null); + Assert.fail("addReferenceInstance failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test bulk insert with large valid set + try { + // Create a list of global file instances. Make enough that the bulk threshold should be hit once. + Set instances = new HashSet<>(); + String bulkTestHash = "bulktesthash_"; + for (int i = 0; i < dbSettingsSqlite.getBulkThreshold() * 1.5; i++) { + String hash = bulkTestHash + String.valueOf(i); + instances.add(new EamGlobalFileInstance(notableSet2id, hash, TskData.FileKnown.BAD, null)); + } + + // Insert the list + EamDb.getInstance().bulkInsertReferenceTypeEntries(instances, fileType); + + // There's no way to get a count of the number of entries in the database, so just do a spot check + if (dbSettingsSqlite.getBulkThreshold() > 10) { + String hash = bulkTestHash + "10"; + assertTrue("Sample bulk insert instance not found", EamDb.getInstance().isFileHashInReferenceSet(hash, notableSet2id)); + } + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test bulk add file instance with null list + try { + EamDb.getInstance().bulkInsertReferenceTypeEntries(null, fileType); + Assert.fail("bulkInsertReferenceTypeEntries failed to throw exception for null list"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test bulk add file instance with invalid reference set ID + try { + Set tempSet = new HashSet<>(Arrays.asList(new EamGlobalFileInstance(2345, inAllSetsHash, TskData.FileKnown.BAD, "comment"))); + EamDb.getInstance().bulkInsertReferenceTypeEntries(tempSet, fileType); + Assert.fail("bulkInsertReferenceTypeEntries failed to throw exception for invalid ID"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test bulk add file instance with null correlation type + try { + Set tempSet = new HashSet<>(Arrays.asList(new EamGlobalFileInstance(notableSet1id, inAllSetsHash, TskData.FileKnown.BAD, "comment"))); + EamDb.getInstance().bulkInsertReferenceTypeEntries(tempSet, null); + Assert.fail("bulkInsertReferenceTypeEntries failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting reference instances with valid data + try { + List temp = EamDb.getInstance().getReferenceInstancesByTypeValue(fileType, inAllSetsHash); + assertTrue("getReferenceInstancesByTypeValue returned " + temp.size() + " instances - expected 3", temp.size() == 3); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting reference instances with non-existent data + try { + List temp = EamDb.getInstance().getReferenceInstancesByTypeValue(fileType, "testHash"); + assertTrue("getReferenceInstancesByTypeValue returned " + temp.size() + " instances for non-existent value - expected 0", temp.isEmpty()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting reference instances an invalid type (the email table is not yet implemented) + try { + EamDb.getInstance().getReferenceInstancesByTypeValue(emailType, inAllSetsHash); + Assert.fail("getReferenceInstancesByTypeValue failed to throw exception for invalid table"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting reference instances with null type + try { + EamDb.getInstance().getReferenceInstancesByTypeValue(null, inAllSetsHash); + Assert.fail("getReferenceInstancesByTypeValue failed to throw exception for null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting reference instances with null value + try { + List temp = EamDb.getInstance().getReferenceInstancesByTypeValue(fileType, null); + assertTrue("getReferenceInstancesByTypeValue returned non-empty list given null value", temp.isEmpty()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test checking existing hash/ID + try { + assertTrue("isFileHashInReferenceSet returned false for valid data", EamDb.getInstance().isFileHashInReferenceSet(knownHash1, knownSet1id)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test checking non-existent (but valid) hash/ID + try { + assertFalse("isFileHashInReferenceSet returned true for non-existent data", EamDb.getInstance().isFileHashInReferenceSet(knownHash1, notableSet1id)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test checking invalid reference set ID + try { + assertFalse("isFileHashInReferenceSet returned true for invalid data", EamDb.getInstance().isFileHashInReferenceSet(knownHash1, 5678)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test checking null hash + try { + assertFalse("isFileHashInReferenceSet returned true for null hash", EamDb.getInstance().isFileHashInReferenceSet(null, knownSet1id)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test checking existing hash/ID + try { + assertTrue("isValueInReferenceSet returned false for valid data", + EamDb.getInstance().isValueInReferenceSet(knownHash1, knownSet1id, fileType.getId())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test checking non-existent (but valid) hash/ID + try { + assertFalse("isValueInReferenceSet returned true for non-existent data", + EamDb.getInstance().isValueInReferenceSet(knownHash1, notableSet1id, fileType.getId())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test checking invalid reference set ID + try { + assertFalse("isValueInReferenceSet returned true for invalid data", + EamDb.getInstance().isValueInReferenceSet(knownHash1, 5678, fileType.getId())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test checking null hash + try { + assertFalse("isValueInReferenceSet returned true for null value", + EamDb.getInstance().isValueInReferenceSet(null, knownSet1id, fileType.getId())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test checking invalid type + try { + EamDb.getInstance().isValueInReferenceSet(knownHash1, knownSet1id, emailType.getId()); + Assert.fail("isValueInReferenceSet failed to throw exception for invalid type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test known bad with notable data + try { + assertTrue("isArtifactKnownBadByReference returned false for notable value", + EamDb.getInstance().isArtifactKnownBadByReference(fileType, notableHash1)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test known bad with known data + try { + assertFalse("isArtifactKnownBadByReference returned true for known value", + EamDb.getInstance().isArtifactKnownBadByReference(fileType, knownHash1)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test known bad with non-existent data + try { + assertFalse("isArtifactKnownBadByReference returned true for non-existent value", + EamDb.getInstance().isArtifactKnownBadByReference(fileType, "abcdef")); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test known bad with null hash + try { + assertFalse("isArtifactKnownBadByReference returned true for null value", + EamDb.getInstance().isArtifactKnownBadByReference(fileType, null)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test known bad with null type + try { + EamDb.getInstance().isArtifactKnownBadByReference(null, knownHash1); + Assert.fail("isArtifactKnownBadByReference failed to throw exception from null type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test known bad with invalid type + try { + assertFalse("isArtifactKnownBadByReference returned true for invalid type", EamDb.getInstance().isArtifactKnownBadByReference(emailType, null)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + } + + /** + * Test method for the methods related to reference sets (does not include + * instance testing) Only the files type is currently implemented + * newReferenceSet(EamGlobalSet eamGlobalSet) tests: + * - Test creating notable reference set + * - Test creating known reference set + * - Test creating duplicate reference set + * - Test creating almost duplicate reference set + * - Test with invalid org ID + * - Test with null name + * - Test with null version + * - Test with null known status + * - Test with null file type + * referenceSetIsValid(int referenceSetID, String referenceSetName, String version) tests: + * - Test on existing reference set + * - Test on invalid reference set + * - Test with null name + * - Test with null version + * referenceSetExists(String referenceSetName, String version) tests: + * - Test on existing reference set + * - Test on invalid reference set + * - Test with null name + * - Test with null version + * getReferenceSetByID(int globalSetID) tests: + * - Test with valid ID + * - Test with invalid ID + * getAllReferenceSets(CorrelationAttribute.Type correlationType) tests: + * - Test getting all file sets + * - Test getting all email sets + * - Test with null type parameter + * deleteReferenceSet(int referenceSetID) tests: + * - Test on valid reference set ID + * - Test on invalid reference set ID + * getReferenceSetOrganization(int referenceSetID) tests: + * - Test on valid reference set ID + * - Test on invalid reference set ID + */ + public void testReferenceSets() { + String set1name = "referenceSet1"; + String set1version = "1.0"; + EamGlobalSet set1; + int set1id; + String set2name = "referenceSet2"; + EamGlobalSet set2; + EamGlobalSet set3; + + // Test creating a notable reference set + try { + set1 = new EamGlobalSet(org1.getOrgID(), set1name, set1version, TskData.FileKnown.BAD, false, fileType); + set1id = EamDb.getInstance().newReferenceSet(set1); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test creating a known reference set + try { + set2 = new EamGlobalSet(org2.getOrgID(), set2name, "", TskData.FileKnown.KNOWN, false, fileType); + EamDb.getInstance().newReferenceSet(set2); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test creating a reference set with the same name and version + try { + EamGlobalSet temp = new EamGlobalSet(org1.getOrgID(), set1name, "1.0", TskData.FileKnown.BAD, false, fileType); + EamDb.getInstance().newReferenceSet(temp); + Assert.fail("newReferenceSet failed to throw exception from duplicate name/version pair"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating a reference set with the same name but different version + try { + set3 = new EamGlobalSet(org1.getOrgID(), set1name, "2.0", TskData.FileKnown.BAD, false, fileType); + EamDb.getInstance().newReferenceSet(set3); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test creating a reference set with invalid org ID + try { + EamGlobalSet temp = new EamGlobalSet(5000, "tempName", "", TskData.FileKnown.BAD, false, fileType); + EamDb.getInstance().newReferenceSet(temp); + Assert.fail("newReferenceSet failed to throw exception from invalid org ID"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating a reference set with null name + try { + EamGlobalSet temp = new EamGlobalSet(org2.getOrgID(), null, "", TskData.FileKnown.BAD, false, fileType); + EamDb.getInstance().newReferenceSet(temp); + Assert.fail("newReferenceSet failed to throw exception from null name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating a reference set with null version + try { + EamGlobalSet temp = new EamGlobalSet(org2.getOrgID(), "tempName", null, TskData.FileKnown.BAD, false, fileType); + EamDb.getInstance().newReferenceSet(temp); + Assert.fail("newReferenceSet failed to throw exception from null version"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating a reference set with null file known status + try { + EamGlobalSet temp = new EamGlobalSet(org2.getOrgID(), "tempName", "", null, false, fileType); + EamDb.getInstance().newReferenceSet(temp); + Assert.fail("newReferenceSet failed to throw exception from null file known status"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating a reference set with null file type + try { + EamGlobalSet temp = new EamGlobalSet(org2.getOrgID(), "tempName", "", TskData.FileKnown.BAD, false, null); + EamDb.getInstance().newReferenceSet(temp); + Assert.fail("newReferenceSet failed to throw exception from null file type"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test validation with a valid reference set + try { + assertTrue("referenceSetIsValid returned false for valid reference set", EamDb.getInstance().referenceSetIsValid(set1id, set1name, set1version)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test validation with an invalid reference set + try { + assertFalse("referenceSetIsValid returned true for invalid reference set", EamDb.getInstance().referenceSetIsValid(5000, set1name, set1version)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test validation with a null name + try { + assertFalse("referenceSetIsValid returned true with null name", EamDb.getInstance().referenceSetIsValid(set1id, null, set1version)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test validation with a null version + try { + assertFalse("referenceSetIsValid returned true with null version", EamDb.getInstance().referenceSetIsValid(set1id, set1name, null)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test existence with a valid reference set + try { + assertTrue("referenceSetExists returned false for valid reference set", EamDb.getInstance().referenceSetExists(set1name, set1version)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test existence with an invalid reference set + try { + assertFalse("referenceSetExists returned true for invalid reference set", EamDb.getInstance().referenceSetExists(set1name, "5.5")); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test existence with null name + try { + assertFalse("referenceSetExists returned true for null name", EamDb.getInstance().referenceSetExists(null, "1.0")); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test existence with null version + try { + assertFalse("referenceSetExists returned true for null version", EamDb.getInstance().referenceSetExists(set1name, null)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting global set with valid ID + try { + EamGlobalSet temp = EamDb.getInstance().getReferenceSetByID(set1id); + assertTrue("getReferenceSetByID returned null for valid ID", temp != null); + assertTrue("getReferenceSetByID returned set with incorrect name and/or version", + set1name.equals(temp.getSetName()) && set1version.equals(temp.getVersion())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting global set with invalid ID + try { + EamGlobalSet temp = EamDb.getInstance().getReferenceSetByID(1234); + assertTrue("getReferenceSetByID returned non-null result for invalid ID", temp == null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting all file reference sets + try { + List referenceSets = EamDb.getInstance().getAllReferenceSets(fileType); + assertTrue("getAllReferenceSets(FILES) returned unexpected number", referenceSets.size() == 3); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting all email reference sets + try { + List referenceSets = EamDb.getInstance().getAllReferenceSets(EamDb.getInstance().getCorrelationTypeById(CorrelationAttribute.EMAIL_TYPE_ID)); + assertTrue("getAllReferenceSets(EMAIL) returned unexpected number", referenceSets.isEmpty()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test null argument to getAllReferenceSets + try { + EamDb.getInstance().getAllReferenceSets(null); + Assert.fail("getAllReferenceSets failed to throw exception from null type argument"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test deleting an existing reference set + // First: create a new reference set, check that it's in the database, and get the number of reference sets + // Second: delete the reference set, check that it is no longer in the database, and the total number of sets decreased by one + try { + EamGlobalSet setToDelete = new EamGlobalSet(org1.getOrgID(), "deleteThis", "deleteThisVersion", TskData.FileKnown.BAD, false, fileType); + int setToDeleteID = EamDb.getInstance().newReferenceSet(setToDelete); + assertTrue("setToDelete wasn't found in database", EamDb.getInstance().referenceSetIsValid(setToDeleteID, setToDelete.getSetName(), setToDelete.getVersion())); + int currentCount = EamDb.getInstance().getAllReferenceSets(fileType).size(); + + EamDb.getInstance().deleteReferenceSet(setToDeleteID); + assertFalse("Deleted reference set was found in database", EamDb.getInstance().referenceSetIsValid(setToDeleteID, setToDelete.getSetName(), setToDelete.getVersion())); + assertTrue("Unexpected number of reference sets in database after deletion", currentCount - 1 == EamDb.getInstance().getAllReferenceSets(fileType).size()); + + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test deleting a non-existent reference set + // The expectation is that nothing will happen + try { + int currentCount = EamDb.getInstance().getAllReferenceSets(fileType).size(); + EamDb.getInstance().deleteReferenceSet(1234); + assertTrue("Number of reference sets changed after deleting non-existent set", currentCount == EamDb.getInstance().getAllReferenceSets(fileType).size()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting reference set organization for valid ID with org set + try { + EamOrganization org = EamDb.getInstance().getReferenceSetOrganization(set1id); + assertTrue("getReferenceSetOrganization returned null for valid set", org != null); + assertTrue("getReferenceSetOrganization returned the incorrect organization", org.getOrgID() == org1.getOrgID()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting reference set organization for non-existent reference set + try { + EamDb.getInstance().getReferenceSetOrganization(4567); + Assert.fail("getReferenceSetOrganization failed to throw exception for invalid reference set ID"); + } catch (EamDbException ex) { + // This is the expected behavior + } + } + + /** + * Test method for the methods related to the data sources table + * newDataSource(CorrelationDataSource eamDataSource) tests: + * - Test with valid data + * - Test with duplicate data + * - Test with duplicate device ID and name but different case + * - Test with invalid case ID + * - Test with null device ID + * - Test with null name + * getDataSource(CorrelationCase correlationCase, String dataSourceDeviceId) tests: + * - Test with valid data + * - Test with non-existent data + * - Test with null correlationCase + * - Test with null device ID + * getDataSources()tests: + * - Test that the count and device IDs are as expected + * getCountUniqueDataSources() tests: + * - Test that the result is as expected + */ + public void testDataSources() { + final String dataSourceAname = "dataSourceA"; + final String dataSourceAid = "dataSourceA_deviceID"; + CorrelationDataSource dataSourceA; + CorrelationDataSource dataSourceB; + + // Test creating a data source with valid case, name, and ID + try { + dataSourceA = new CorrelationDataSource(case2.getID(), dataSourceAid, dataSourceAname); + EamDb.getInstance().newDataSource(dataSourceA); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test creating a data source with the same case, name, and ID + try { + CorrelationDataSource temp = new CorrelationDataSource(case2.getID(), dataSourceAid, dataSourceAname); + EamDb.getInstance().newDataSource(temp); + Assert.fail("newDataSource did not throw exception from duplicate data source"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating a data source with the same name and ID but different case + try { + dataSourceB = new CorrelationDataSource(case1.getID(), dataSourceAid, dataSourceAname); + EamDb.getInstance().newDataSource(dataSourceB); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test creating a data source with an invalid case ID + try { + CorrelationDataSource temp = new CorrelationDataSource(5000, "tempID", "tempName"); + EamDb.getInstance().newDataSource(temp); + Assert.fail("newDataSource did not throw exception from invalid case ID"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating a data source with null device ID + try { + CorrelationDataSource temp = new CorrelationDataSource(case2.getID(), null, "tempName"); + EamDb.getInstance().newDataSource(temp); + Assert.fail("newDataSource did not throw exception from null device ID"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating a data source with null name + try { + CorrelationDataSource temp = new CorrelationDataSource(case2.getID(), "tempID", null); + EamDb.getInstance().newDataSource(temp); + Assert.fail("newDataSource did not throw exception from null name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting a data source with valid case and ID + try { + CorrelationDataSource temp = EamDb.getInstance().getDataSource(case2, dataSourceAid); + assertTrue("Failed to get data source", temp != null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting a data source with non-existent ID + try { + CorrelationDataSource temp = EamDb.getInstance().getDataSource(case2, "badID"); + assertTrue("getDataSource returned non-null value for non-existent data source", temp == null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting a data source with a null case + try { + EamDb.getInstance().getDataSource(null, dataSourceAid); + Assert.fail("getDataSource did not throw exception from null case"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting a data source with null ID + try { + CorrelationDataSource temp = EamDb.getInstance().getDataSource(case2, null); + assertTrue("getDataSource returned non-null value for null data source", temp == null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting the list of data sources + // There should be five data sources, and we'll check for the expected device IDs + try { + List dataSources = EamDb.getInstance().getDataSources(); + List devIdList + = dataSources.stream().map(c -> c.getDeviceID()).collect(Collectors.toList()); + assertTrue("getDataSources returned unexpected number of data sources", dataSources.size() == 5); + assertTrue("getDataSources is missing expected data sources", + devIdList.contains(dataSourceAid) + && devIdList.contains(dataSource1fromCase1.getDeviceID()) + && devIdList.contains(dataSource2fromCase1.getDeviceID()) + && devIdList.contains(dataSource1fromCase2.getDeviceID())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test the data source count + try { + assertTrue("getCountUniqueDataSources returned unexpected number of data sources", + EamDb.getInstance().getCountUniqueDataSources() == 5); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + } + + /** + * Test method for the methods related to the cases table + * newCase(CorrelationCase eamCase) tests: + * - Test valid data + * - Test null UUID + * - Test null case name + * - Test repeated UUID + * newCase(Case autopsyCase) tests: + * - Test valid data + * - Test null autopsyCase + * updateCase(CorrelationCase eamCase) tests: + * - Test with valid data, checking all fields + * - Test null eamCase + * getCase(Case autopsyCase) tests: + * - Test with current Autopsy case + * getCaseByUUID(String caseUUID) + * - Test with UUID that is in the database + * - Test with UUID that is not in the database + * - Test with null UUID + * getCases() tests: + * - Test getting all cases, checking the count and fields + * bulkInsertCases(List cases) + * - Test on a list of cases larger than the bulk insert threshold. + * - Test on a null list + */ + public void testCases() { + final String caseAname = "caseA"; + final String caseAuuid = "caseA_uuid"; + CorrelationCase caseA; + CorrelationCase caseB; + + try { + // Set up an Autopsy case for testing + try { + Case.createAsCurrentCase(Case.CaseType.SINGLE_USER_CASE, testDirectory.toString(), new CaseDetails("CentralRepoDatamodelTestCase")); + } catch (CaseActionException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + assertTrue("Failed to create test case", testDirectory.toFile().exists()); + + // Test creating a case with valid name and uuid + try { + caseA = new CorrelationCase(caseAuuid, caseAname); + caseA = EamDb.getInstance().newCase(caseA); + assertTrue("Failed to create case", caseA != null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test null uuid + try { + CorrelationCase tempCase = new CorrelationCase(null, "nullUuidCase"); + EamDb.getInstance().newCase(tempCase); + Assert.fail("newCase did not throw expected exception from null uuid"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test null name + try { + CorrelationCase tempCase = new CorrelationCase("nullCaseUuid", null); + EamDb.getInstance().newCase(tempCase); + Assert.fail("newCase did not throw expected exception from null name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test creating a case with an already used UUID + // This should just return the existing case object. Check that the total + // number of cases does not increase. + try { + int nCases = EamDb.getInstance().getCases().size(); + CorrelationCase tempCase = new CorrelationCase(caseAuuid, "newCaseWithSameUUID"); + tempCase = EamDb.getInstance().newCase(tempCase); + assertTrue("newCase returned null for existing UUID", tempCase != null); + assertTrue("newCase created a new case for an already existing UUID", nCases == EamDb.getInstance().getCases().size()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test creating a case from an Autopsy case + // The case may already be in the database - the result is the same either way + try { + caseB = EamDb.getInstance().newCase(Case.getCurrentCase()); + assertTrue("Failed to create correlation case from Autopsy case", caseB != null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + return; + } + + // Test null Autopsy case + try { + Case nullCase = null; + EamDb.getInstance().newCase(nullCase); + Assert.fail("newCase did not throw expected exception from null case"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test update case + // Will update the fields of an existing case object, save it, and then + // pull a new copy out of the database + try { + assertTrue(caseA != null); + String caseNumber = "12-34-56"; + String creationDate = "01/12/2018"; + String displayName = "Test Case"; + String examinerEmail = "john@sample.com"; + String examinerName = "John Doe"; + String examinerPhone = "123-555-4567"; + String notes = "Notes"; + + caseA.setCaseNumber(caseNumber); + caseA.setCreationDate(creationDate); + caseA.setDisplayName(displayName); + caseA.setExaminerEmail(examinerEmail); + caseA.setExaminerName(examinerName); + caseA.setExaminerPhone(examinerPhone); + caseA.setNotes(notes); + caseA.setOrg(org1); + + EamDb.getInstance().updateCase(caseA); + + // Retrievex a new copy of the case from the database to check that the + // fields were properly updated + CorrelationCase updatedCase = EamDb.getInstance().getCaseByUUID(caseA.getCaseUUID()); + + assertTrue("updateCase failed to update case number", caseNumber.equals(updatedCase.getCaseNumber())); + assertTrue("updateCase failed to update creation date", creationDate.equals(updatedCase.getCreationDate())); + assertTrue("updateCase failed to update display name", displayName.equals(updatedCase.getDisplayName())); + assertTrue("updateCase failed to update examiner email", examinerEmail.equals(updatedCase.getExaminerEmail())); + assertTrue("updateCase failed to update examiner name", examinerName.equals(updatedCase.getExaminerName())); + assertTrue("updateCase failed to update examiner phone number", examinerPhone.equals(updatedCase.getExaminerPhone())); + assertTrue("updateCase failed to update notes", notes.equals(updatedCase.getNotes())); + assertTrue("updateCase failed to update org (org is null)", updatedCase.getOrg() != null); + assertTrue("updateCase failed to update org (org ID is wrong)", org1.getOrgID() == updatedCase.getOrg().getOrgID()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test update case with null case + try { + EamDb.getInstance().updateCase(null); + Assert.fail("updateCase did not throw expected exception from null case"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test getting a case from an Autopsy case + try { + CorrelationCase tempCase = EamDb.getInstance().getCase(Case.getCurrentCase()); + assertTrue("getCase returned null for current Autopsy case", tempCase != null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting a case by UUID + try { + CorrelationCase tempCase = EamDb.getInstance().getCaseByUUID(caseAuuid); + assertTrue("Failed to get case by UUID", tempCase != null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting a case with a non-existent UUID + try { + CorrelationCase tempCase = EamDb.getInstance().getCaseByUUID("badUUID"); + assertTrue("getCaseByUUID returned non-null case for non-existent UUID", tempCase == null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting a case with null UUID + try { + CorrelationCase tempCase = EamDb.getInstance().getCaseByUUID(null); + assertTrue("getCaseByUUID returned non-null case for null UUID", tempCase == null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test getting the list of cases + // The test is to make sure the three cases we know are in the database are in the list + try { + List caseList = EamDb.getInstance().getCases(); + List uuidList + = caseList.stream().map(c -> c.getCaseUUID()).collect(Collectors.toList()); + assertTrue("getCases is missing data for existing cases", uuidList.contains(case1.getCaseUUID()) + && uuidList.contains(case2.getCaseUUID()) && (uuidList.contains(caseA.getCaseUUID())) + && uuidList.contains(caseB.getCaseUUID())); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test bulk case insert + try { + // Create a list of correlation cases. Make enough that the bulk threshold should be hit once. + List cases = new ArrayList<>(); + String bulkTestUuid = "bulkTestUUID_"; + String bulkTestName = "bulkTestName_"; + for (int i = 0; i < dbSettingsSqlite.getBulkThreshold() * 1.5; i++) { + String name = bulkTestUuid + String.valueOf(i); + String uuid = bulkTestName + String.valueOf(i); + cases.add(new CorrelationCase(uuid, name)); + } + + // Get the current case count + int nCases = EamDb.getInstance().getCases().size(); + + // Insert the big list of cases + EamDb.getInstance().bulkInsertCases(cases); + + // Check that the case count is what is expected + assertTrue("bulkInsertCases did not insert the expected number of cases", nCases + cases.size() == EamDb.getInstance().getCases().size()); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test bulk case insert with null list + try { + EamDb.getInstance().bulkInsertCases(null); + Assert.fail("bulkInsertCases did not throw expected exception from null list"); + } catch (EamDbException ex) { + // This is the expected behavior + } + } finally { + try { + Case.closeCurrentCase(); + // This seems to help in allowing the Autopsy case to be deleted + try { + Thread.sleep(2000); + } catch (Exception ex) { + + } + } catch (CaseActionException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + } + } + + /** + * Test method for the three methods related to the db_info table + * newDbInfo(String name, String value) tests: + * - Test valid data + * - Test null name + * - Test null value + * getDbInfo(String name) + * - Test getting value for existing name + * - Test getting value for non-existing name + * - Test getting value for null name + * updateDbInfo(String name, String value) + * - Test updating existing name to valid new value + * - Test updating existing name to null value + * - Test updating null name + * - Test updating non-existing name to new value + */ + public void testDbInfo() { + final String name1 = "testName1"; + final String name2 = "testName2"; + final String name3 = "testName3"; + final String value1 = "testValue1"; + final String value2 = "testValue2"; + + // Test setting a valid value in DbInfo + try { + EamDb.getInstance().newDbInfo(name1, value1); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Test null name + try { + EamDb.getInstance().newDbInfo(null, value1); + Assert.fail("newDbInfo did not throw expected exception from null name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Test null value + try { + EamDb.getInstance().newDbInfo(name2, null); + Assert.fail("newDbInfo did not throw expected exception from null value"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Try getting the dbInfo entry that should exist + try { + String tempVal = EamDb.getInstance().getDbInfo(name1); + assertTrue("dbInfo value for name1 does not match", value1.equals(tempVal)); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Try getting the dbInfo entry that should not exist + try { + String tempVal = EamDb.getInstance().getDbInfo(name3); + assertTrue("dbInfo value is unexpectedly non-null given non-existent name", tempVal == null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Try getting dbInfo for a null value + try { + String tempVal = EamDb.getInstance().getDbInfo(null); + assertTrue("dbInfo value is unexpectedly non-null given null name", tempVal == null); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Try updating an existing value to a valid new value + try { + EamDb.getInstance().updateDbInfo(name1, value2); + assertTrue("dbInfo value failed to update to expected value", value2.equals(EamDb.getInstance().getDbInfo(name1))); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Try updating an existing value to null + try { + EamDb.getInstance().updateDbInfo(name1, null); + Assert.fail("updateDbInfo did not throw expected exception from null value"); + } catch (EamDbException ex) { + // This is the expected behavior + } + + // Try updating a null name + // This seems like SQLite would throw an exception, but it does not + try { + EamDb.getInstance().updateDbInfo(null, value1); + } catch (EamDbException ex) { + Exceptions.printStackTrace(ex); + Assert.fail(ex); + } + + // Try updating the value for a non-existant name + try { + EamDb.getInstance().updateDbInfo(name1, null); + Assert.fail("updateDbInfo did not throw expected exception from non-existent name"); + } catch (EamDbException ex) { + // This is the expected behavior + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java index 1720b4b647..860019238b 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java @@ -876,7 +876,7 @@ public class SharedConfiguration { if (!sharedDbPath.exists()) { if (!sharedDbPath.mkdirs()) { - throw new SharedConfigurationException("Error creating shared hash database directory " + sharedDbPath.getAbsolutePath()); + throw new SharedConfigurationException("Error creating shared hash set directory " + sharedDbPath.getAbsolutePath()); } } @@ -1010,7 +1010,7 @@ public class SharedConfiguration { if (!localDb.getParentFile().exists()) { if (!localDb.getParentFile().mkdirs()) { - throw new SharedConfigurationException("Error creating hash database directory " + localDb.getParentFile().getAbsolutePath()); + throw new SharedConfigurationException("Error creating hash set directory " + localDb.getParentFile().getAbsolutePath()); } } @@ -1025,7 +1025,7 @@ public class SharedConfiguration { break; } } catch (TskCoreException ex) { - throw new SharedConfigurationException(String.format("Error getting hash database path info for %s", localDb.getParentFile().getAbsolutePath()), ex); + throw new SharedConfigurationException(String.format("Error getting hash set path info for %s", localDb.getParentFile().getAbsolutePath()), ex); } } @@ -1033,7 +1033,7 @@ public class SharedConfiguration { try { HashDbManager.getInstance().removeHashDatabase(matchingDb); } catch (HashDbManager.HashDbManagerException ex) { - throw new SharedConfigurationException(String.format("Error updating hash database info for %s", localDb.getAbsolutePath()), ex); + throw new SharedConfigurationException(String.format("Error updating hash set info for %s", localDb.getAbsolutePath()), ex); } } @@ -1130,7 +1130,7 @@ public class SharedConfiguration { } } } catch (TskCoreException ex) { - throw new SharedConfigurationException("Unable to read hash databases", ex); + throw new SharedConfigurationException("Unable to read hash sets", ex); } return results; } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/DeleteTagAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/DeleteTagAction.java index a21d163fdc..40451e8443 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/DeleteTagAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/DeleteTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2017 Basis Technology Corp. + * Copyright 2017-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -51,7 +51,7 @@ import org.sleuthkit.datamodel.TskData; */ public class DeleteTagAction extends Action { - private static final Logger LOGGER = Logger.getLogger(DeleteTagAction.class.getName()); + private static final Logger logger = Logger.getLogger(DeleteTagAction.class.getName()); private final ImageGalleryController controller; private final long fileId; @@ -83,17 +83,11 @@ public class DeleteTagAction extends Action { protected Void doInBackground() throws Exception { DrawableTagsManager tagsManager = controller.getTagsManager(); - // Pull the from the global context to avoid unnecessary calls - // to the database. - final Collection selectedFilesList - = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); - AbstractFile file = selectedFilesList.iterator().next(); - try { - LOGGER.log(Level.INFO, "Removing tag {0} from {1}", new Object[]{tagName.getDisplayName(), file.getName()}); //NON-NLS + logger.log(Level.INFO, "Removing tag {0} from {1}", new Object[]{tagName.getDisplayName(), contentTag.getContent().getName()}); //NON-NLS tagsManager.deleteContentTag(contentTag); } catch (TskCoreException tskCoreException) { - LOGGER.log(Level.SEVERE, "Error untagging file", tskCoreException); //NON-NLS + logger.log(Level.SEVERE, "Error untagging file", tskCoreException); //NON-NLS Platform.runLater(() -> new Alert(Alert.AlertType.ERROR, Bundle.DeleteDrawableTagAction_deleteTag_alert(fileId)).show() ); @@ -107,7 +101,7 @@ public class DeleteTagAction extends Action { try { get(); } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Unexpected exception while untagging file", ex); //NON-NLS + logger.log(Level.SEVERE, "Unexpected exception while untagging file", ex); //NON-NLS } } }.execute(); diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AccountsText.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AccountsText.java index e5f17cef12..00d5670151 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AccountsText.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AccountsText.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -284,15 +284,6 @@ class AccountsText implements IndexedText { } @Override - @NbBundle.Messages({"AccountsText.getMarkup.noMatchMsg=" - + "
There were no keyword hits on this page. 
" - + "The keyword could have been in the file name." - + "
Advance to another page if present, or to view the original text, choose File Text" - + "
in the drop down menu to the right...
", - "AccountsText.getMarkup.queryFailedMsg=" - + "
Failed to retrieve keyword hit results."
-        + " 
Confirm that Autopsy can connect to the Solr server. " - + "
"}) public String getText() { try { loadPageInfo(); //inits once @@ -321,7 +312,7 @@ class AccountsText implements IndexedText { return "
" + highlightedText + "
"; //NON-NLS } catch (Exception ex) { logger.log(Level.SEVERE, "Error getting highlighted text for Solr doc id " + this.solrObjectId + ", chunkID " + this.currentPage, ex); //NON-NLS - return Bundle.AccountsText_getMarkup_queryFailedMsg(); + return Bundle.IndexedText_errorMessage_errorGettingText(); } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties index 8be786efda..38cad58ce5 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties @@ -54,11 +54,6 @@ AbstractKeywordSearchPerformer.search.noFilesInIdxMsg=No files are in inde AbstractKeywordSearchPerformer.search.noFilesIdxdMsg=No files were indexed.
Re-ingest the image with the Keyword Search Module enabled. ExtractedContentViewer.toolTip=Displays extracted text from files and keyword-search results. Requires Keyword Search ingest to be run on a file to activate this viewer. ExtractedContentViewer.getTitle=Indexed Text -ExtractedContentViewer.getSolrContent.knownFileMsg=

{0} is a known file (based on MD5 hash) and does not have text in the index.

-ExtractedContentViewer.getSolrContent.noTxtYetMsg=

{0} does not have text in the index.
It may have no text, not been analyzed yet, or keyword search was not enabled during ingest.

-ExtractedContentViewer.getSolrContent.txtBodyItal={0} -HighlightedMatchesSource.getMarkup.noMatchMsg=Failed to retrieve indexed text for keyword hit. Advance to another page if present, or to view the original text, choose File Text in the drop down menu to the right. Alternatively, you may choose to extract file content and search for the hit using an external application (e.g. a text editor). -HighlightedMatchesSource.getMarkup.queryFailedMsg=
Failed to retrieve keyword hit results. 
Confirm that Autopsy can connect to the Solr server.
HighlightedMatchesSource.toString=Search Results Installer.reportPortError=Indexing server port {0} is not available. Check if your security software does not block {1} and consider changing {2} in {3} property file in the application user folder. Then try rebooting your system if another process was causing the conflict. Installer.reportStopPortError=Indexing server stop port {0} is not available. Consider changing {1} in {2} property file in the application user folder. @@ -229,7 +224,7 @@ KeywordSearchGlobalSearchSettingsPanel.timeRadioButton1.text=20 minutes (slowest KeywordSearchGlobalSearchSettingsPanel.timeRadioButton2.toolTipText=10 minutes (faster overall ingest time than default) KeywordSearchGlobalSearchSettingsPanel.timeRadioButton2.text=10 minutes (slower feedback, faster ingest) KeywordSearchGlobalSearchSettingsPanel.frequencyLabel.text=Results update frequency during ingest: -KeywordSearchGlobalSearchSettingsPanel.skipNSRLCheckBox.toolTipText=Requires Hash DB service to had run previously, or be selected for next ingest. +KeywordSearchGlobalSearchSettingsPanel.skipNSRLCheckBox.toolTipText=Requires Hash Set service to had run previously, or be selected for next ingest. KeywordSearchGlobalSearchSettingsPanel.skipNSRLCheckBox.text=Do not add files in NSRL (known files) to keyword index during ingest KeywordSearchGlobalSearchSettingsPanel.informationLabel.text=Information KeywordSearchGlobalSearchSettingsPanel.settingsLabel.text=Settings @@ -272,7 +267,6 @@ KeywordSearchGlobalSearchSettingsPanel.timeRadioButton5.text=No periodic searche SolrConnectionCheck.HostnameOrPort=Invalid hostname and/or port number. SolrConnectionCheck.Hostname=Invalid hostname. SolrConnectionCheck.MissingHostname=Missing hostname. -RawText.getText.error.msg=Error getting text GlobalListsManagementPanel.newListButton.text=New List GlobalListsManagementPanel.importButton.text=Import List GlobalListsManagementPanel.keywordListsLabel.text=Keyword Lists: diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties index bdcc19f6f6..2442e5bbc4 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties @@ -46,7 +46,6 @@ AbstractKeywordSearchPerformer.search.noFilesIdxdMsg=\u30a4\u30f3\u30c7\u3 ExtractedContentPanel.setMarkup.panelTxt=\u30c6\u30ad\u30b9\u30c8\u30ed\u30fc\u30c9\u4e2d...\u3057\u3070\u3089\u304f\u304a\u5f85\u3061\u304f\u3060\u3055\u3044\u3002 ExtractedContentViewer.toolTip=\u30d5\u30a1\u30a4\u30eb\u3084\u30ad\u30fc\u30ef\u30fc\u30c9\u691c\u7d22\u7d50\u679c\u304b\u3089\u62bd\u51fa\u3055\u308c\u305f\u30c6\u30ad\u30b9\u30c8\u3092\u8868\u793a\u3002\u3053\u306e\u30d3\u30e5\u30fc\u30a2\u3092\u6709\u52b9\u5316\u3059\u308b\u306b\u306f\u3001\u30d5\u30a1\u30a4\u30eb\u306b\u5bfe\u3057\u3066\u30ad\u30fc\u30ef\u30fc\u30c9\u691c\u7d22\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u3092\u5b9f\u884c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002 ExtractedContentViewer.getTitle=\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u5316\u3055\u308c\u305f\u30c6\u30ad\u30b9\u30c8 -ExtractedContentViewer.getSolrContent.knownFileMsg=

{0}\u306f\u65e2\u77e5\u30d5\u30a1\u30a4\u30eb\u3067\u3059\uff08MDS\u30cf\u30c3\u30b7\u30e5\u306b\u57fa\u3065\u304f\u3068\uff09\u3002\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306b\u30c6\u30ad\u30b9\u30c8\u304c\u3042\u308a\u307e\u305b\u3093\u3002

ExtractedContentViewer.getSolrContent.noTxtYetMsg=

{0}\u306e\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306b\u30c6\u30ad\u30b9\u30c8\u304c\u3042\u308a\u307e\u305b\u3093\u3002
\u30c6\u30ad\u30b9\u30c8\u304c\u7121\u3044\u304b\u3001\u307e\u3060\u89e3\u6790\u3055\u308c\u3066\u3044\u306a\u3044\u304b\u3001\u30ad\u30fc\u30ef\u30fc\u30c9\u691c\u7d22\u304c\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u4e2d\u306b\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u306a\u304b\u3063\u305f\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002

HighlightedMatchesSource.toString=\u691c\u7d22\u7d50\u679c Installer.reportPortError=\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u30b5\u30fc\u30d0\u30fc\u30dd\u30fc\u30c8 {0} \u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002\u4f7f\u7528\u3057\u3066\u3044\u308b\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30bd\u30d5\u30c8\u30a6\u30a7\u30a2\u304c {1} \u3092\u30d6\u30ed\u30c3\u30af\u3057\u3066\u3044\u306a\u3044\u304b\u78ba\u8a8d\u3057\u3001\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30e6\u30fc\u30b6\u30fc\u30d5\u30a9\u30eb\u30c0\u30fc\u5185\u306e{3}\u30d7\u30ed\u30d1\u30c6\u30a3\u30d5\u30a1\u30a4\u30eb\u306e{2}\u3092\u5909\u66f4\u3059\u308b\u691c\u8a0e\u3092\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3082\u3057\u4ed6\u306e\u51e6\u7406\u304c\u554f\u984c\u306e\u539f\u56e0\u3067\u3042\u308c\u3070\u3001\u30b7\u30b9\u30c6\u30e0\u3092\u518d\u8d77\u52d5\u3057\u3066\u4e0b\u3055\u3044\u3002 @@ -200,7 +199,6 @@ KeywordSearchIngestModule.doInBackGround.finalizeMsg=- \u6700\u7d42\u51e6\u7406\ KeywordSearchIngestModule.doInBackGround.pendingMsg=\uff08\u30da\u30f3\u30c7\u30a3\u30f3\u30b0\uff09 SearchRunner.doInBackGround.cancelMsg=\uff08\u30ad\u30e3\u30f3\u30bb\u30eb\u4e2d\u2026\uff09 Server.addDoc.exception.msg2=\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30cf\u30f3\u30c9\u30e9\u30fc\u3092\u4f7f\u7528\u3057\u307e\u3057\u305f\u304c\u3001\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306b\u6b21\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\uff1a{0} -ExtractedContentViewer.getSolrContent.txtBodyItal={0} KeywordSearchJobSettingsPanel.keywordSearchEncodings.text=- KeywordSearchJobSettingsPanel.languagesValLabel.text=- KeywordSearchJobSettingsPanel.encodingsLabel.text=\u30a8\u30f3\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\uff1a @@ -261,7 +259,6 @@ KeywordSearchModuleFactory.getIngestJobSettingsPanel.exception.msg=\u8a2d\u5b9a\ SearchRunner.Searcher.done.err.msg=\u30ad\u30fc\u30ef\u30fc\u30c9\u691c\u7d22\u3092\u5b9f\u884c\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f KeywordSearchGlobalSearchSettingsPanel.timeRadioButton5.text=\u5b9a\u671f\u7684\u691c\u7d22\u7121\u3057 KeywordSearchGlobalSearchSettingsPanel.timeRadioButton5.toolTipText=\u5168\u4f53\u7684\u306b\u4e00\u756a\u901f\u3044\u3067\u3059\u304c\u3001\u51e6\u7406\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u7d50\u679c\u306f\u8868\u793a\u3055\u308c\u307e\u305b\u3093 -HighlightedMatchesSource.getMarkup.queryFailedMsg=
\u30ad\u30fc\u30ef\u30fc\u30c9\u306b\u30d2\u30c3\u30c8\u3057\u305f\u7d50\u679c\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
Autopsy\u304cSolr\u30b5\u30fc\u30d0\u30fc\u306b\u63a5\u7d9a\u3067\u304d\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
\ KeywordSearch.openCore.notification.msg=\u30ad\u30fc\u30ef\u30fc\u30c9\u691c\u7d22\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3092\u958b\u3051\u307e\u305b\u3093\u3067\u3057\u305f KeywordSearch.closeCore.notification.msg=\u30ad\u30fc\u30ef\u30fc\u30c9\u691c\u7d22\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3092\u9589\u3058\u308b\u969b\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f KeywordSearchListsManagementPanel.fileExtensionFilterLb2=\u30ad\u30fc\u30ef\u30fc\u30c9\u30ea\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u3092\u30a8\u30f3\u30b1\u30fc\u30b9\u3059\u308b(txt) @@ -272,7 +269,7 @@ SolrConnectionCheck.HostnameOrPort=hostname\u3084\u30dd\u30fc\u30c8\u756a\u53f7\ SolrConnectionCheck.Hostname=hostname\u304c\u7121\u52b9\u3067\u3059\u3002 SolrConnectionCheck.Port=\u30dd\u30fc\u30c8\u756a\u53f7\u304c\u7121\u52b9\u3067\u3059\u3002 SolrConnectionCheck.MissingHostname=hostname\u304c\u6b20\u3051\u3066\u307e\u3059\u3002 -RawText.getText.error.msg=\u30c6\u30ad\u30b9\u30c8\u3092\u53d6\u5f97\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f +ExtractedContentViewer.getText.error.msg=\u30c6\u30ad\u30b9\u30c8\u3092\u53d6\u5f97\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f GlobalListsManagementPanel.exportButton.text=\u30ea\u30b9\u30c8\u3092\u30a8\u30af\u30b9\u30dd\u30fc\u30c8 GlobalListsManagementPanel.deleteListButton.text=\u30ea\u30b9\u30c8\u3092\u524a\u9664 GlobalListsManagementPanel.copyListButton.text=\u30ea\u30b9\u30c8\u3092\u30b3\u30d4\u30fc diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java index 01647f2a46..bc68962693 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-17 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,7 @@ import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javax.swing.JTextPane; @@ -647,8 +648,6 @@ class ExtractedContentPanel extends javax.swing.JPanel { return source.getText(); } - @NbBundle.Messages({ - "ExtractedContentPanel.SetMarkup.error=There was an error getting the text for the selected source."}) @Override protected void done() { super.done(); @@ -663,11 +662,9 @@ class ExtractedContentPanel extends javax.swing.JPanel { setPanelText("", false); } - } catch (InterruptedException | ExecutionException ex) { + } catch (InterruptedException | CancellationException | ExecutionException ex) { logger.log(Level.SEVERE, "Error getting marked up text", ex); //NON-NLS - setPanelText(Bundle.ExtractedContentPanel_SetMarkup_error(), true); - } // catch and ignore if we were cancelled - catch (java.util.concurrent.CancellationException ex) { + setPanelText(Bundle.IndexedText_errorMessage_errorGettingText(), true); } updateControls(source); diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java index dbed68e660..8ce322876a 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/ExtractedContentViewer.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,6 +33,7 @@ import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.keywordsearch.KeywordSearchResultFactory.AdHocQueryResult; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -52,7 +53,6 @@ public class ExtractedContentViewer implements DataContentViewer { private static final Logger logger = Logger.getLogger(ExtractedContentViewer.class.getName()); - private static final long INVALID_DOCUMENT_ID = 0L; private static final BlackboardAttribute.Type TSK_ASSOCIATED_ARTIFACT_TYPE = new BlackboardAttribute.Type(TSK_ASSOCIATED_ARTIFACT); public static final BlackboardAttribute.Type TSK_ACCOUNT_TYPE = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE); @@ -92,54 +92,77 @@ public class ExtractedContentViewer implements DataContentViewer { currentNode = node; } - Lookup nodeLookup = node.getLookup(); - AbstractFile content = nodeLookup.lookup(AbstractFile.class); - /* * Assemble a collection of all of the indexed text "sources" for the * node. */ List sources = new ArrayList<>(); + Lookup nodeLookup = node.getLookup(); + + AdHocQueryResult adHocQueryResult = nodeLookup.lookup(AdHocQueryResult.class); + AbstractFile file = null; + BlackboardArtifact artifact; + + /* + * If we have an ad hoc query result, pull the file and artifact objects + * from that. Otherwise, pull them from the lookup. + */ + if (adHocQueryResult != null) { + artifact = adHocQueryResult.getArtifact(); + Content content = adHocQueryResult.getContent(); + if (content instanceof AbstractFile) { + file = (AbstractFile) content; + } + } else { + artifact = nodeLookup.lookup(BlackboardArtifact.class); + file = nodeLookup.lookup(AbstractFile.class); + } + + /* + * First, get text with highlighted hits if this node is for a search + * result. + */ IndexedText highlightedHitText = null; - IndexedText rawContentText = null; - - if (null != content && solrHasContent(content.getId())) { - QueryResults hits = nodeLookup.lookup(QueryResults.class); - BlackboardArtifact artifact = nodeLookup.lookup(BlackboardArtifact.class); - if (hits != null) { + if (adHocQueryResult != null) { + /* + * The node is an ad hoc search result node. + */ + highlightedHitText = new HighlightedText(adHocQueryResult.getSolrObjectId(), adHocQueryResult.getResults()); + } else if (artifact != null) { + if (artifact.getArtifactTypeID() == TSK_KEYWORD_HIT.getTypeID()) { /* - * if there is a QueryReslt object, in the lookup use that. This - * happens when a user selects a row in an ad-hoc search result + * The node is a keyword hit artifact node. */ - highlightedHitText = new HighlightedText(content.getId(), hits); - } else if (artifact != null - && artifact.getArtifactTypeID() == TSK_ACCOUNT.getTypeID()) { try { - // if the artifact is an account artifact, get an account text . - highlightedHitText = getAccountsText(content, nodeLookup); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Failed to create AccountsText for " + content, ex); //NON-NLS - - } - } else if (artifact != null - && artifact.getArtifactTypeID() == TSK_KEYWORD_HIT.getTypeID()) { - try { - //if there is kwh artifact use that to construct the HighlightedText highlightedHitText = new HighlightedText(artifact); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Failed to create HighlightedText for " + artifact, ex); //NON-NLS } + } else if (artifact.getArtifactTypeID() == TSK_ACCOUNT.getTypeID() && file != null) { + try { + BlackboardAttribute attribute = artifact.getAttribute(TSK_ACCOUNT_TYPE); + if (attribute != null && Account.Type.CREDIT_CARD.getTypeName().equals(attribute.getValueString())) { + /* + * The node is an credit card account node. + */ + highlightedHitText = getAccountsText(file, nodeLookup); + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Failed to create AccountsText for " + file, ex); //NON-NLS + } } + } + if (highlightedHitText != null) { + sources.add(highlightedHitText); + } - if (highlightedHitText != null) { - sources.add(highlightedHitText); - } - - /* - * Next, add the "raw" (not highlighted) text, if any, for any - * content associated with the node. - */ - rawContentText = new RawText(content, content.getId()); + /* + * Next, add the "raw" (not highlighted) text, if any, for any file + * associated with the node. + */ + IndexedText rawContentText = null; + if (file != null) { + rawContentText = new RawText(file, file.getId()); sources.add(rawContentText); } @@ -149,22 +172,21 @@ public class ExtractedContentViewer implements DataContentViewer { */ IndexedText rawArtifactText = null; try { - rawArtifactText = getRawArtifactText(nodeLookup); + rawArtifactText = getRawArtifactText(artifact); + if (rawArtifactText != null) { + sources.add(rawArtifactText); + } } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error creating RawText for " + content, ex); //NON-NLS - - } - if (rawArtifactText != null) { - sources.add(rawArtifactText); + logger.log(Level.SEVERE, "Error creating RawText for " + file, ex); //NON-NLS } // Now set the default source to be displayed. - if (null != highlightedHitText) { + if (highlightedHitText != null) { currentSource = highlightedHitText; - } else if (null != rawContentText) { - currentSource = rawContentText; - } else { + } else if (rawArtifactText != null) { currentSource = rawArtifactText; + } else { + currentSource = rawContentText; } // Push the text sources into the panel. @@ -177,16 +199,15 @@ public class ExtractedContentViewer implements DataContentViewer { panel.updateControls(currentSource); String contentName = ""; - if (content != null) { - contentName = content.getName(); + if (file != null) { + contentName = file.getName(); } setPanel(contentName, sources); } - static private IndexedText getRawArtifactText(Lookup nodeLookup) throws TskCoreException { + static private IndexedText getRawArtifactText(BlackboardArtifact artifact) throws TskCoreException { IndexedText rawArtifactText = null; - BlackboardArtifact artifact = nodeLookup.lookup(BlackboardArtifact.class); if (null != artifact) { /* * For keyword hit artifacts, add the text of the artifact that hit, @@ -275,48 +296,84 @@ public class ExtractedContentViewer implements DataContentViewer { } /* - * Is there a credit card or keyword hit artifact in the lookup + * If the lookup of the node contains an ad hoc search result object, + * then there must be indexed text that produced the hit. */ - Collection artifacts = node.getLookup().lookupAll(BlackboardArtifact.class); - if (artifacts != null) { - for (BlackboardArtifact art : artifacts) { - final int artifactTypeID = art.getArtifactTypeID(); - if (artifactTypeID == TSK_ACCOUNT.getTypeID()) { - try { - BlackboardAttribute attribute = art.getAttribute(TSK_ACCOUNT_TYPE); - if (attribute != null && Account.Type.CREDIT_CARD.getTypeName().equals(attribute.getValueString())) { - return true; - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting TSK_ACCOUNT_TYPE attribute from artifact " + art.getArtifactID(), ex); + AdHocQueryResult adHocQueryResult = node.getLookup().lookup(AdHocQueryResult.class); + if (adHocQueryResult != null) { + return true; + } + + /* + * If the lookup of the node contains either a keyword hit artifact or a + * credit card account artifact from a credit card account numbers + * search, then there must be indexed text that produced the hit(s). + */ + BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); + if (artifact != null) { + final int artifactTypeID = artifact.getArtifactTypeID(); + if (artifactTypeID == TSK_KEYWORD_HIT.getTypeID()) { + return true; + } else if (artifactTypeID == TSK_ACCOUNT.getTypeID()) { + try { + BlackboardAttribute attribute = artifact.getAttribute(TSK_ACCOUNT_TYPE); + if (attribute != null && Account.Type.CREDIT_CARD.getTypeName().equals(attribute.getValueString())) { + return true; } - } else if (artifactTypeID == TSK_KEYWORD_HIT.getTypeID()) { - return true; + } catch (TskCoreException ex) { + /* + * If there was an error checking the account type, fall + * back to the check below for the file associated with the + * account (if there is one). + */ + logger.log(Level.SEVERE, "Error getting TSK_ACCOUNT_TYPE attribute from artifact " + artifact.getArtifactID(), ex); } } } /* - * No highlighted text for a keyword hit, so is there any indexed text - * at all for this node? + * If the lookup of the node contains a file, check to see if there is + * indexed text for the file. Note that there should be a file in the + * lookup of all nodes except artifact nodes that are associated with a + * data source instead of a file. */ - long documentID = getDocumentId(node); - if (INVALID_DOCUMENT_ID == documentID) { - return false; + AbstractFile file = node.getLookup().lookup(AbstractFile.class); + if (file != null && solrHasContent(file.getId())) { + return true; } - return solrHasContent(documentID); + /* + * If the lookup of the node contains an artifact that is neither a + * keyword hit artifact nor a credit card account artifact, and the + * artifact is not associated with a file, check to see if there is + * indexed text for the artifact. + */ + if (artifact != null) { + return solrHasContent(artifact.getArtifactID()); + } + + return false; } @Override public int isPreferred(Node node) { - BlackboardArtifact art = node.getLookup().lookup(BlackboardArtifact.class); - - if (art == null) { + BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); + if (artifact == null) { return 4; - } else if (art.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() - || art.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT.getTypeID()) { + } else if (artifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) { return 6; + } else if (artifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT.getTypeID()) { + try { + BlackboardAttribute attribute = artifact.getAttribute(TSK_ACCOUNT_TYPE); + if (attribute != null && Account.Type.CREDIT_CARD.getTypeName().equals(attribute.getValueString())) { + return 6; + } else { + return 4; + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error getting TSK_ACCOUNT_TYPE attribute from artifact " + artifact.getArtifactID(), ex); + return 4; + } } else { return 4; } @@ -357,57 +414,6 @@ public class ExtractedContentViewer implements DataContentViewer { } } - /** - * Gets the object ID to use as the document ID for accessing any indexed - * text for the given node. - * - * @param node The node. - * - * @return The document ID or zero, which is an invalid document ID. - */ - private Long getDocumentId(Node node) { - /** - * If the node is a Blackboard artifact node for anything other than a - * keyword hit, the document ID for the text extracted from the artifact - * (the concatenation of its attributes) is the artifact ID, a large, - * negative integer. If it is a keyword hit, see if there is an - * associated artifact. If there is, get the associated artifact's ID - * and return it. - */ - BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); - if (null != artifact) { - if (artifact.getArtifactTypeID() != BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) { - return artifact.getArtifactID(); - } else { - try { - // Get the associated artifact attribute and return its value as the ID - BlackboardAttribute blackboardAttribute = artifact.getAttribute(TSK_ASSOCIATED_ARTIFACT_TYPE); - if (blackboardAttribute != null) { - return blackboardAttribute.getValueLong(); - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting associated artifact attributes", ex); //NON-NLS - } - } - } - - /* - * For keyword search hit artifact nodes and all other nodes, the - * document ID for the extracted text is the ID of the associated - * content, if any, unless there is an associated artifact, which is - * handled above. - */ - Content content = node.getLookup().lookup(Content.class); - if (content != null) { - return content.getId(); - } - - /* - * No extracted text, return an invalid docuemnt ID. - */ - return 0L; - } - private class NextFindActionListener implements ActionListener { @Override diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedText.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedText.java index 70d2e69194..322860416a 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedText.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedText.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,6 @@ package org.sleuthkit.autopsy.keywordsearch; import com.google.common.collect.Iterators; import com.google.common.collect.Range; -import com.google.common.collect.RangeSet; import com.google.common.collect.TreeRangeSet; import java.util.Arrays; import java.util.Collection; @@ -41,7 +40,6 @@ import org.apache.solr.client.solrj.SolrRequest.METHOD; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocumentList; import org.openide.util.NbBundle; -import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.keywordsearch.KeywordQueryFilter.FilterType; @@ -70,7 +68,7 @@ class HighlightedText implements IndexedText { final private Server solrServer = KeywordSearch.getServer(); - private final long objectId; + private final long solrObjectId; /* * The keywords to highlight */ @@ -104,16 +102,16 @@ class HighlightedText implements IndexedText { /** * This constructor is used when keyword hits are accessed from the ad-hoc * search results. In that case we have the entire QueryResults object and - need to arrange the paging. + * need to arrange the paging. * - * @param objectId The objectID of the content whose text will be + * @param solrObjectId The solrObjectId of the content whose text will be * highlighted. * @param QueryResults The QueryResults for the ad-hoc search from whose - results a selection was made leading to this - HighlightedText. + * results a selection was made leading to this + * HighlightedText. */ - HighlightedText(long objectId, QueryResults hits) { - this.objectId = objectId; + HighlightedText(long solrObjectId, QueryResults hits) { + this.solrObjectId = solrObjectId; this.hits = hits; } @@ -129,9 +127,9 @@ class HighlightedText implements IndexedText { this.artifact = artifact; BlackboardAttribute attribute = artifact.getAttribute(TSK_ASSOCIATED_ARTIFACT); if (attribute != null) { - this.objectId = attribute.getValueLong(); + this.solrObjectId = attribute.getValueLong(); } else { - this.objectId = artifact.getObjectID(); + this.solrObjectId = artifact.getObjectID(); } } @@ -140,13 +138,12 @@ class HighlightedText implements IndexedText { * This method figures out which pages / chunks have hits. Invoking it a * second time has no effect. */ - @Messages({"HighlightedText.query.exception.msg=Could not perform the query to get chunk info and get highlights:"}) synchronized private void loadPageInfo() throws TskCoreException, KeywordSearchModuleException, NoOpenCoreException { if (isPageInfoLoaded) { return; } - this.numberPages = solrServer.queryNumFileChunks(this.objectId); + this.numberPages = solrServer.queryNumFileChunks(this.solrObjectId); if (artifact != null) { loadPageInfoFromArtifact(); @@ -158,7 +155,6 @@ class HighlightedText implements IndexedText { this.numberPages = 1; this.currentPage = 1; numberOfHitsPerPage.put(1, 0); - pages.add(1); currentHitPerPage.put(1, 0); isPageInfoLoaded = true; } @@ -194,7 +190,7 @@ class HighlightedText implements IndexedText { // Run a query to figure out which chunks for the current object have // hits for this keyword. - chunksQuery.addFilter(new KeywordQueryFilter(FilterType.CHUNK, this.objectId)); + chunksQuery.addFilter(new KeywordQueryFilter(FilterType.CHUNK, this.solrObjectId)); hits = chunksQuery.performQuery(); loadPageInfoFromHits(); @@ -207,16 +203,16 @@ class HighlightedText implements IndexedText { isLiteral = hits.getQuery().isLiteral(); /** - * Organize the hits by page, filter as needed. - * We process *every* keyword here because in the case of a regular - * expression search there may be multiple different keyword - * hits located in different chunks for the same file/artifact. + * Organize the hits by page, filter as needed. We process *every* + * keyword here because in the case of a regular expression search there + * may be multiple different keyword hits located in different chunks + * for the same file/artifact. */ for (Keyword k : hits.getKeywords()) { for (KeywordHit hit : hits.getResults(k)) { int chunkID = hit.getChunkId(); if (artifact != null) { - if (chunkID != 0 && this.objectId == hit.getSolrObjectId()) { + if (chunkID != 0 && this.solrObjectId == hit.getSolrObjectId()) { String hit1 = hit.getHit(); if (keywords.stream().anyMatch(hit1::contains)) { numberOfHitsPerPage.put(chunkID, 0); //unknown number of matches in the page @@ -225,7 +221,7 @@ class HighlightedText implements IndexedText { } } } else { - if (chunkID != 0 && this.objectId == hit.getSolrObjectId()) { + if (chunkID != 0 && this.solrObjectId == hit.getSolrObjectId()) { numberOfHitsPerPage.put(chunkID, 0); //unknown number of matches in the page currentHitPerPage.put(chunkID, 0); //set current hit to 0th @@ -354,7 +350,7 @@ class HighlightedText implements IndexedText { SolrQuery q = new SolrQuery(); q.setShowDebugInfo(DEBUG); //debug - String contentIdStr = Long.toString(this.objectId); + String contentIdStr = Long.toString(this.solrObjectId); if (numberPages != 0) { chunkID = Integer.toString(this.currentPage); contentIdStr += "0".equals(chunkID) ? "" : "_" + chunkID; @@ -427,8 +423,8 @@ class HighlightedText implements IndexedText { return "
" + highlightedContent + "
"; //NON-NLS } catch (TskCoreException | KeywordSearchModuleException | NoOpenCoreException ex) { - logger.log(Level.SEVERE, "Error getting highlighted text for Solr doc id " + objectId + ", chunkID " + chunkID + ", highlight query: " + highlightField, ex); //NON-NLS - return NbBundle.getMessage(this.getClass(), "HighlightedMatchesSource.getMarkup.queryFailedMsg"); + logger.log(Level.SEVERE, "Error getting highlighted text for Solr doc id " + solrObjectId + ", chunkID " + chunkID + ", highlight query: " + highlightField, ex); //NON-NLS + return Bundle.IndexedText_errorMessage_errorGettingText(); } } @@ -466,12 +462,13 @@ class HighlightedText implements IndexedText { * to a Solr query. We expect there to only ever be * a single document. * - * @return Either a string with the keyword highlighted via HTML span tags or a string - * indicating that we did not find a hit in the document. + * @return Either a string with the keyword highlighted via HTML span tags + * or a string indicating that we did not find a hit in the + * document. */ static String attemptManualHighlighting(SolrDocumentList solrDocumentList, String highlightField, Collection keywords) { if (solrDocumentList.isEmpty()) { - return NbBundle.getMessage(HighlightedText.class, "HighlightedMatchesSource.getMarkup.noMatchMsg"); + return Bundle.IndexedText_errorMessage_errorGettingText(); } // It doesn't make sense for there to be more than a single document in diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexedText.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexedText.java index 6bf6a4f3ef..17366483e3 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexedText.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexedText.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,12 +18,19 @@ */ package org.sleuthkit.autopsy.keywordsearch; +import org.openide.util.NbBundle; + /** * Interface to provide HTML text to display in ExtractedContentViewer. There is * a SOLR implementation of this that interfaces with SOLR to highlight the * keyword hits and a version that does not do markup so that you can simply * view the stored text. */ +@NbBundle.Messages({ + "IndexedText.errorMessage.errorGettingText=Error retrieving indexed text.", + "IndexedText.warningMessage.knownFile=This file is a known file (based on MD5 hash) and does not have indexed text.", + "IndexedText.warningMessage.noTextAvailable=No indexed text for this file." +}) interface IndexedText { /** diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchFilterNode.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchFilterNode.java index 7674ee5e34..98c7ca0969 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchFilterNode.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchFilterNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2017 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,6 +39,7 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.directorytree.HashSearchAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; +import org.sleuthkit.autopsy.keywordsearch.KeywordSearchResultFactory.AdHocQueryResult; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentVisitor; @@ -52,12 +53,18 @@ import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.VirtualDirectory; /** - * + * FilterNode containing properties and actions for keyword search. */ class KeywordSearchFilterNode extends FilterNode { - KeywordSearchFilterNode(QueryResults highlights, Node original) { - super(original, null, new ProxyLookup(Lookups.singleton(highlights), original.getLookup())); + /** + * Instantiate a KeywordSearchFilterNode. + * + * @param adHocQueryResult The query content. + * @param original The original source node. + */ + KeywordSearchFilterNode(AdHocQueryResult adHocQueryResult, Node original) { + super(original, null, new ProxyLookup(Lookups.singleton(adHocQueryResult), original.getLookup())); } @Override diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java index a8bc995eae..fbeb4268b7 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java @@ -37,7 +37,6 @@ import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.NbBundle; -import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; @@ -49,6 +48,7 @@ import org.sleuthkit.autopsy.datamodel.KeyValue; import org.sleuthkit.autopsy.datamodel.KeyValueNode; import org.sleuthkit.autopsy.keywordsearch.KeywordSearchResultFactory.KeyValueQueryContent; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW; @@ -66,7 +66,7 @@ import org.sleuthkit.datamodel.TskCoreException; */ class KeywordSearchResultFactory extends ChildFactory { - private static final Logger LOGGER = Logger.getLogger(KeywordSearchResultFactory.class.getName()); + private static final Logger logger = Logger.getLogger(KeywordSearchResultFactory.class.getName()); //common properties (superset of all Node properties) to be displayed as columns static final List COMMON_PROPERTIES @@ -75,10 +75,10 @@ class KeywordSearchResultFactory extends ChildFactory { TSK_KEYWORD, TSK_KEYWORD_REGEXP, TSK_KEYWORD_PREVIEW) - .map(BlackboardAttribute.ATTRIBUTE_TYPE::getDisplayName), + .map(BlackboardAttribute.ATTRIBUTE_TYPE::getDisplayName), Arrays.stream(AbstractAbstractFileNode.AbstractFilePropertyType.values()) - .map(Object::toString)) - .collect(Collectors.toList()); + .map(Object::toString)) + .collect(Collectors.toList()); private final Collection queryRequests; @@ -140,7 +140,7 @@ class KeywordSearchResultFactory extends ChildFactory { try { queryResults = queryRequest.performQuery(); } catch (KeywordSearchModuleException | NoOpenCoreException ex) { - LOGGER.log(Level.SEVERE, "Could not perform the query " + queryRequest.getQueryString(), ex); //NON-NLS + logger.log(Level.SEVERE, "Could not perform the query " + queryRequest.getQueryString(), ex); //NON-NLS MessageNotifyUtil.Notify.error(Bundle.KeywordSearchResultFactory_query_exception_msg() + queryRequest.getQueryString(), ex.getCause().getMessage()); return false; } @@ -148,7 +148,7 @@ class KeywordSearchResultFactory extends ChildFactory { try { tskCase = Case.getCurrentCase().getSleuthkitCase(); } catch (IllegalStateException ex) { - LOGGER.log(Level.SEVERE, "There was no case open.", ex); //NON-NLS + logger.log(Level.SEVERE, "There was no case open.", ex); //NON-NLS return false; } @@ -165,11 +165,11 @@ class KeywordSearchResultFactory extends ChildFactory { try { content = tskCase.getContentById(hit.getContentID()); if (content == null) { - LOGGER.log(Level.SEVERE, "There was a error getting content by id."); //NON-NLS + logger.log(Level.SEVERE, "There was a error getting content by id."); //NON-NLS return false; } } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "There was a error getting content by id.", ex); //NON-NLS + logger.log(Level.SEVERE, "There was a error getting content by id.", ex); //NON-NLS return false; } @@ -188,18 +188,20 @@ class KeywordSearchResultFactory extends ChildFactory { } String hitName; + BlackboardArtifact artifact = null; if (hit.isArtifactHit()) { try { - hitName = tskCase.getBlackboardArtifact(hit.getArtifactID().get()).getDisplayName() + " Artifact"; //NON-NLS + artifact = tskCase.getBlackboardArtifact(hit.getArtifactID().get()); + hitName = artifact.getDisplayName() + " Artifact"; //NON-NLS } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Error getting blckboard artifact by id", ex); + logger.log(Level.SEVERE, "Error getting blckboard artifact by id", ex); return false; } } else { hitName = contentName; } hitNumber++; - tempList.add(new KeyValueQueryContent(hitName, properties, hitNumber, hit.getSolrObjectId(), content, queryRequest, queryResults)); + tempList.add(new KeyValueQueryContent(hitName, properties, hitNumber, hit.getSolrObjectId(), content, artifact, queryRequest, queryResults)); } @@ -250,13 +252,12 @@ class KeywordSearchResultFactory extends ChildFactory { Node resultNode; if (key instanceof KeyValueQueryContent) { - final Content content = ((KeyValueQueryContent) key).getContent(); - QueryResults hits = ((KeyValueQueryContent) key).getHits(); + AdHocQueryResult adHocQueryResult = new AdHocQueryResult((KeyValueQueryContent) key); - Node kvNode = new KeyValueNode(key, Children.LEAF, Lookups.singleton(content)); + Node kvNode = new KeyValueNode(key, Children.LEAF); //wrap in KeywordSearchFilterNode for the markup content, might need to override FilterNode for more customization - resultNode = new KeywordSearchFilterNode(hits, kvNode); + resultNode = new KeywordSearchFilterNode(adHocQueryResult, kvNode); } else { resultNode = new EmptyNode("This Node Is Empty"); resultNode.setDisplayName(NbBundle.getMessage(this.getClass(), "KeywordSearchResultFactory.createNodeForKey.noResultsFound.text")); @@ -266,6 +267,75 @@ class KeywordSearchResultFactory extends ChildFactory { } + /** + * This class encapsulates content, query results, and an associated Solr + * object ID for storing in the Lookup to be read later. + */ + final class AdHocQueryResult { + + private final long solrObjectId; + private final Content content; + private final BlackboardArtifact artifact; + private final QueryResults results; + + /** + * Instantiate a AdHocQueryResult object. + * + * @param solrObjectId The Solr object ID associated with the object in + * which the hit was found. + * @param content The content for the query result. + * @param artifact The artifact associated with the query result. + * @param results The query results. + */ + AdHocQueryResult(KeyValueQueryContent key) { + this.solrObjectId = key.getSolrObjectId(); + this.content = key.getContent(); + this.artifact = key.getArtifact(); + this.results = key.getHits(); + } + + /** + * Get the Solr object ID associated with the object in which the hit + * was found. This could be a file or an artifact. + * + * @return The Solr object ID. + */ + long getSolrObjectId() { + return solrObjectId; + } + + /** + * Get the content for the query result. This can be either a file or a + * data source, and it may or may not be the content in which the hit + * occurred. If the hit is in a file, the Content object represents that + * file. But if the hit is in an artifact, the Content object represents + * the source file or data source of the artifact. + * + * @return The content object. + */ + Content getContent() { + return content; + } + + /** + * Get the artifact for the query result. + * + * @return The artifact. + */ + BlackboardArtifact getArtifact() { + return artifact; + } + + /** + * Get the query results. + * + * @return The query results. + */ + QueryResults getResults() { + return results; + } + } + /** * Used to display keyword search results in table. Eventually turned into a * node. @@ -275,6 +345,7 @@ class KeywordSearchResultFactory extends ChildFactory { private final long solrObjectId; private final Content content; + private final BlackboardArtifact artifact; private final QueryResults hits; private final KeywordSearchQuery query; @@ -286,15 +357,17 @@ class KeywordSearchResultFactory extends ChildFactory { * @param map Contains content metadata, snippets, etc. * (property map) * @param id User incremented ID - * @param solrObjectId - * @param content File that had the hit. + * @param solrObjectId The ID of the object. + * @param content The content object. + * @param artifact The blackboard artifact. * @param query Query used in search * @param hits Full set of search results (for all files! @@@) */ - KeyValueQueryContent(String name, Map map, int id, long solrObjectId, Content content, KeywordSearchQuery query, QueryResults hits) { + KeyValueQueryContent(String name, Map map, int id, long solrObjectId, Content content, BlackboardArtifact artifact, KeywordSearchQuery query, QueryResults hits) { super(name, map, id); this.solrObjectId = solrObjectId; this.content = content; + this.artifact = artifact; this.hits = hits; this.query = query; @@ -304,6 +377,10 @@ class KeywordSearchResultFactory extends ChildFactory { return content; } + BlackboardArtifact getArtifact() { + return artifact; + } + long getSolrObjectId() { return solrObjectId; } @@ -358,9 +435,9 @@ class KeywordSearchResultFactory extends ChildFactory { try { get(); } catch (InterruptedException | CancellationException ex) { - LOGGER.log(Level.WARNING, "User cancelled writing of ad hoc search query results for '{0}' to the blackboard", query.getQueryString()); //NON-NLS + logger.log(Level.WARNING, "User cancelled writing of ad hoc search query results for '{0}' to the blackboard", query.getQueryString()); //NON-NLS } catch (ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Error writing of ad hoc search query results for " + query.getQueryString() + " to the blackboard", ex); //NON-NLS + logger.log(Level.SEVERE, "Error writing of ad hoc search query results for " + query.getQueryString() + " to the blackboard", ex); //NON-NLS } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/RawText.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/RawText.java index 043738ae30..789de3fd50 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/RawText.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/RawText.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.keywordsearch; -import java.util.LinkedHashMap; import java.util.logging.Level; import org.apache.solr.client.solrj.SolrServerException; import org.openide.util.NbBundle; @@ -153,10 +152,10 @@ class RawText implements IndexedText { } else if (this.blackboardArtifact != null) { return getArtifactText(); } - } catch (SolrServerException ex) { - logger.log(Level.SEVERE, "Couldn't get extracted content", ex); //NON-NLS + } catch (SolrServerException | NoOpenCoreException ex) { + logger.log(Level.SEVERE, "Couldn't get extracted text", ex); //NON-NLS } - return NbBundle.getMessage(this.getClass(), "RawText.getText.error.msg"); + return Bundle.IndexedText_errorMessage_errorGettingText(); } @NbBundle.Messages({ @@ -186,7 +185,6 @@ class RawText implements IndexedText { return 0; } - @Override public int getNumberPages() { return numPages; @@ -207,11 +205,8 @@ class RawText implements IndexedText { } else { hasChunks = true; } - } catch (KeywordSearchModuleException ex) { - logger.log(Level.WARNING, "Could not get number of chunks: ", ex); //NON-NLS - - } catch (NoOpenCoreException ex) { - logger.log(Level.WARNING, "Could not get number of chunks: ", ex); //NON-NLS + } catch (KeywordSearchModuleException | NoOpenCoreException ex) { + logger.log(Level.SEVERE, "Could not get number of chunks: ", ex); //NON-NLS } } @@ -225,31 +220,32 @@ class RawText implements IndexedText { * chunks. This means we need to address the content * pages specially. * - * @return the extracted content + * @return the extracted text * - * @throws SolrServerException if something goes wrong + * @throws NoOpenCoreException If no Solr core is available. + * @throws SolrServerException If there's a Solr communication or parsing + * issue. */ - private String getContentText(int currentPage, boolean hasChunks) throws SolrServerException { + private String getContentText(int currentPage, boolean hasChunks) throws NoOpenCoreException, SolrServerException { final Server solrServer = KeywordSearch.getServer(); if (hasChunks == false) { //if no chunks, it is safe to assume there is no text content //because we are storing extracted text in chunks only //and the non-chunk stores meta-data only - String name = content.getName(); String msg = null; + if (content instanceof AbstractFile) { //we know it's AbstractFile, but do quick check to make sure if we index other objects in future boolean isKnown = TskData.FileKnown.KNOWN.equals(((AbstractFile) content).getKnown()); if (isKnown && KeywordSearchSettings.getSkipKnown()) { - msg = NbBundle.getMessage(this.getClass(), "ExtractedContentViewer.getSolrContent.knownFileMsg", name); + msg = Bundle.IndexedText_warningMessage_knownFile(); } } if (msg == null) { - msg = NbBundle.getMessage(this.getClass(), "ExtractedContentViewer.getSolrContent.noTxtYetMsg", name); + msg = Bundle.IndexedText_warningMessage_noTextAvailable(); } - String htmlMsg = NbBundle.getMessage(this.getClass(), "ExtractedContentViewer.getSolrContent.txtBodyItal", msg); - return htmlMsg; + return msg; } int chunkId = currentPage; @@ -262,33 +258,46 @@ class RawText implements IndexedText { } //not cached - try { - String indexedText = solrServer.getSolrContent(this.objectId, chunkId); - if (indexedText == null) indexedText = ""; - cachedString = EscapeUtil.escapeHtml(indexedText).trim(); - StringBuilder sb = new StringBuilder(cachedString.length() + 20); - sb.append("
").append(cachedString).append("
"); //NON-NLS - cachedString = sb.toString(); - cachedChunk = chunkId; - } catch (NoOpenCoreException ex) { - logger.log(Level.SEVERE, "No open core", ex); //NON-NLS - return ""; + String indexedText = solrServer.getSolrContent(this.objectId, chunkId); + if (indexedText == null) { + if (content instanceof AbstractFile) { + return Bundle.IndexedText_errorMessage_errorGettingText(); + } else { + return Bundle.IndexedText_warningMessage_noTextAvailable(); + } + } else if (indexedText.isEmpty()) { + return Bundle.IndexedText_warningMessage_noTextAvailable(); } + + cachedString = EscapeUtil.escapeHtml(indexedText).trim(); + StringBuilder sb = new StringBuilder(cachedString.length() + 20); + sb.append("
").append(cachedString).append("
"); //NON-NLS + cachedString = sb.toString(); + cachedChunk = chunkId; + return cachedString; } - - private String getArtifactText() throws SolrServerException{ - try { - String indexedText = KeywordSearch.getServer().getSolrContent(this.objectId, 1); - if (indexedText == null) indexedText = ""; - indexedText = EscapeUtil.escapeHtml(indexedText).trim(); - StringBuilder sb = new StringBuilder(indexedText.length() + 20); - sb.append("
").append(indexedText).append("
"); //NON-NLS - return sb.toString(); - } catch (NoOpenCoreException ex) { - logger.log(Level.SEVERE, "No open core", ex); //NON-NLS - return ""; + + /** + * Get extracted artifact for a node from Solr + * + * @return the extracted text + * + * @throws NoOpenCoreException If no Solr core is available. + * @throws SolrServerException If there's a Solr communication or parsing + * issue. + */ + private String getArtifactText() throws NoOpenCoreException, SolrServerException { + String indexedText = KeywordSearch.getServer().getSolrContent(this.objectId, 1); + if (indexedText == null || indexedText.isEmpty()) { + return Bundle.IndexedText_errorMessage_errorGettingText(); } + + indexedText = EscapeUtil.escapeHtml(indexedText).trim(); + StringBuilder sb = new StringBuilder(indexedText.length() + 20); + sb.append("
").append(indexedText).append("
"); //NON-NLS + + return sb.toString(); } - + } diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java index b0af801089..860d282d93 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java @@ -76,10 +76,13 @@ class ExtractRegistry extends Extract { final private static UsbDeviceIdMapper USB_MAPPER = new UsbDeviceIdMapper(); final private static String RIP_EXE = "rip.exe"; final private static String RIP_PL = "rip.pl"; - final private static String PERL = "perl "; + private List rrCmd = new ArrayList<>(); + private List rrFullCmd= new ArrayList<>(); + ExtractRegistry() throws IngestModuleException { moduleName = NbBundle.getMessage(ExtractIE.class, "ExtractRegistry.moduleName.text"); + final File rrRoot = InstalledFileLocator.getDefault().locate("rr", ExtractRegistry.class.getPackage().getName(), false); //NON-NLS if (rrRoot == null) { throw new IngestModuleException(Bundle.RegRipperNotFound()); @@ -98,20 +101,33 @@ class ExtractRegistry extends Extract { RR_PATH = rrHome.resolve(executableToRun).toString(); rrFullHome = rrFullRoot.toPath(); RR_FULL_PATH = rrFullHome.resolve(executableToRun).toString(); - + if (!(new File(RR_PATH).exists())) { throw new IngestModuleException(Bundle.RegRipperNotFound()); } if (!(new File(RR_FULL_PATH).exists())) { throw new IngestModuleException(Bundle.RegRipperFullNotFound()); } - - if (!PlatformUtil.isWindowsOS()) { - RR_PATH = PERL + RR_PATH; - RR_FULL_PATH = PERL + RR_FULL_PATH; + if(PlatformUtil.isWindowsOS()){ + rrCmd.add(RR_PATH); + rrFullCmd.add(RR_FULL_PATH); + }else{ + String perl; + File usrBin = new File("/usr/bin/perl"); + File usrLocalBin = new File("/usr/local/bin/perl"); + if(usrBin.canExecute() && usrBin.exists() && !usrBin.isDirectory()){ + perl = "/usr/bin/perl"; + }else if(usrLocalBin.canExecute() && usrLocalBin.exists() && !usrLocalBin.isDirectory()){ + perl = "/usr/local/bin/perl"; + }else{ + throw new IngestModuleException("perl not found in your system"); + } + rrCmd.add(perl); + rrCmd.add(RR_PATH); + rrFullCmd.add(perl); + rrFullCmd.add(RR_FULL_PATH); } } - /** * Search for the registry hives on the system. */ @@ -262,7 +278,7 @@ class ExtractRegistry extends Extract { regOutputFiles.autopsyPlugins = outFilePathBase + "-autopsy.txt"; //NON-NLS String errFilePath = outFilePathBase + "-autopsy.err.txt"; //NON-NLS logger.log(Level.INFO, "Writing RegRipper results to: {0}", regOutputFiles.autopsyPlugins); //NON-NLS - executeRegRipper(RR_PATH, rrHome, regFilePath, autopsyType, regOutputFiles.autopsyPlugins, errFilePath); + executeRegRipper(rrCmd, rrHome, regFilePath, autopsyType, regOutputFiles.autopsyPlugins, errFilePath); } if (context.dataSourceIngestIsCancelled()) { return regOutputFiles; @@ -273,15 +289,17 @@ class ExtractRegistry extends Extract { regOutputFiles.fullPlugins = outFilePathBase + "-full.txt"; //NON-NLS String errFilePath = outFilePathBase + "-full.err.txt"; //NON-NLS logger.log(Level.INFO, "Writing Full RegRipper results to: {0}", regOutputFiles.fullPlugins); //NON-NLS - executeRegRipper(RR_FULL_PATH, rrFullHome, regFilePath, fullType, regOutputFiles.fullPlugins, errFilePath); + executeRegRipper(rrFullCmd, rrFullHome, regFilePath, fullType, regOutputFiles.fullPlugins, errFilePath); } return regOutputFiles; } - private void executeRegRipper(String regRipperPath, Path regRipperHomeDir, String hiveFilePath, String hiveFileType, String outputFile, String errFile) { + private void executeRegRipper(List regRipperPath, Path regRipperHomeDir, String hiveFilePath, String hiveFileType, String outputFile, String errFile) { try { List commandLine = new ArrayList<>(); - commandLine.add(regRipperPath); + for(String cmd: regRipperPath){ + commandLine.add(cmd); + } commandLine.add("-r"); //NON-NLS commandLine.add(hiveFilePath); commandLine.add("-f"); //NON-NLS diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java index db9af9087c..9fdec551b4 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java @@ -23,9 +23,11 @@ package org.sleuthkit.autopsy.recentactivity; import java.io.File; +import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; +import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; @@ -57,6 +59,7 @@ public final class RAImageIngestModule implements DataSourceIngestModule { public void startUp(IngestJobContext context) throws IngestModuleException { this.context = context; + Extract registry = new ExtractRegistry(); Extract iexplore = new ExtractIE(); Extract recentDocuments = new RecentDocumentsByLnk(); diff --git a/Testing/src/org/sleuthkit/autopsy/testing/AutopsyTestCases.java b/Testing/src/org/sleuthkit/autopsy/testing/AutopsyTestCases.java index c716081672..d1860120a6 100644 --- a/Testing/src/org/sleuthkit/autopsy/testing/AutopsyTestCases.java +++ b/Testing/src/org/sleuthkit/autopsy/testing/AutopsyTestCases.java @@ -208,7 +208,7 @@ public class AutopsyTestCases { databases.stream().map((database) -> { JButtonOperator importButtonOperator = new JButtonOperator(hashMainDialogOperator, "Import"); importButtonOperator.pushNoBlock(); - JDialog addDatabaseDialog = JDialogOperator.waitJDialog("Import Hash Database", false, false); + JDialog addDatabaseDialog = JDialogOperator.waitJDialog("Import Hash Set", false, false); JDialogOperator addDatabaseDialogOperator = new JDialogOperator(addDatabaseDialog); JButtonOperator browseButtonOperator = new JButtonOperator(addDatabaseDialogOperator, "Open...", 0); browseButtonOperator.pushNoBlock(); diff --git a/build.xml b/build.xml index d044f68f06..ef5ad87980 100644 --- a/build.xml +++ b/build.xml @@ -32,10 +32,12 @@ - + + - + + @@ -91,7 +93,7 @@ - + @@ -101,16 +103,19 @@ - - - - - - - - - - + + + + + + + + + + + + + diff --git a/docs/doxygen/modReport.dox b/docs/doxygen/modReport.dox index 3a7e425896..1e28a5a78d 100644 --- a/docs/doxygen/modReport.dox +++ b/docs/doxygen/modReport.dox @@ -73,7 +73,7 @@ Typically a general report module should interact with both the Blackboard API i \subsection report_create_module_showing Showing Results -You should call Case.addReport() with the path to your report so that it is shown in the Autopsy tree. You can specify a specific file or folder and the user can then view it later. +You should call org.sleuthkit.autopsy.casemodule.Case.addReport() with the path to your report so that it is shown in the Autopsy tree. You can specify a specific file or folder and the user can then view it later. \subsection report_create_module_layer Installing your Report Module diff --git a/ruleset.xml b/ruleset.xml index 9c8f7e34c2..558f99fc38 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -62,7 +62,8 @@ - + + + diff --git a/test/script/regression.py b/test/script/regression.py index 53f8810ff8..3e9ee2e585 100644 --- a/test/script/regression.py +++ b/test/script/regression.py @@ -1454,10 +1454,10 @@ class Logs(object): Errors.print_error("Error: Unable to open autopsy.log.0.") Errors.print_error(str(e) + "\n") logging.warning(traceback.format_exc()) - # Start date must look like: "Fri Mar 27 13:27:34 EDT 2015" + # Start date must look like: "" # End date must look like: "Mon Jul 16 13:02:42 2012" # *** If logging time format ever changes this will break *** - start = datetime.datetime.strptime(test_data.start_date, "%a %b %d %H:%M:%S %Z %Y") + start = datetime.datetime.strptime(test_data.start_date, "%Y-%m-%d %H:%M:%S.%f") end = datetime.datetime.strptime(test_data.end_date, "%a %b %d %H:%M:%S %Y") test_data.total_test_time = str(end - start) diff --git a/thirdparty/rr-full/Parse/Win32Registry.pm b/thirdparty/rr-full/Parse/Win32Registry.pm new file mode 100644 index 0000000000..1aa4556ffa --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry.pm @@ -0,0 +1,1834 @@ +package Parse::Win32Registry; + +use 5.008_001; +use strict; +use warnings; + +our $VERSION = '1.0'; + +use base qw(Exporter); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::Win95::File; +use Parse::Win32Registry::WinNT::File; + +our @EXPORT_OK = ( + # include old function names for backwards compatibility + 'convert_filetime_to_epoch_time', + 'formatted_octets', + @Parse::Win32Registry::Base::EXPORT_OK +); + +our %EXPORT_TAGS = ( + REG_ => [grep { /^REG_[A-Z_]*$/ } @EXPORT_OK], + all => [@EXPORT_OK], + functions => [grep { /^[a-z0-9_]*$/ } @EXPORT_OK], + constants => [grep { /^[A-Z_]*$/ } @EXPORT_OK], +); + +*convert_filetime_to_epoch_time = \&Parse::Win32Registry::unpack_windows_time; +*formatted_octets = \&Parse::Win32Registry::format_octets; + +sub enable_warnings { + $Parse::Win32Registry::Base::WARNINGS = 1; +} + +sub disable_warnings { + $Parse::Win32Registry::Base::WARNINGS = 0; +} + +sub set_codepage { + my $codepage = shift; + if (defined $codepage) { + $Parse::Win32Registry::Base::CODEPAGE = $codepage; + } +} + +sub get_codepage { + $Parse::Win32Registry::Base::CODEPAGE; +} + +sub new { + my $class = shift; + my $filename = shift or croak "No filename specified"; + + open my $regfile, "<", $filename or croak "Unable to open '$filename': $!"; + sysread($regfile, my $sig, 4); + if (!defined($sig) || length($sig) != 4) { + warnf("Could not read registry file header"); + return; + } + close $regfile; + + if ($sig eq "CREG") { + # attempt to parse this as a Windows 95 Registry File + return Parse::Win32Registry::Win95::File->new($filename); + } + elsif ($sig eq "regf") { + # attempt to parse this as a Windows NT Registry File + return Parse::Win32Registry::WinNT::File->new($filename); + } + else { + warnf("Invalid registry file header"); + return; + } +} + +1; + +__END__ + +=head1 NAME + +Parse::Win32Registry - Parse Windows Registry Files + +=head1 SYNOPSIS + + use strict; + use Parse::Win32Registry qw( :REG_ + unpack_windows_time + unpack_unicode_string ); + + my $filename = shift or die "Filename?"; + + my $registry = Parse::Win32Registry->new($filename) + or die "'$filename' is not a registry file\n"; + my $root_key = $registry->get_root_key + or die "Could not get root key of '$filename'\n"; + + # The following code works on USER.DAT or NTUSER.DAT files + + my $software_key = $root_key->get_subkey(".DEFAULT\\Software") + || $root_key->get_subkey("Software"); + + if (defined($software_key)) { + my @user_key_names = ( + "Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", + "Microsoft\\Windows\\CurrentVersion\\Explorer\\RunMRU", + ); + + foreach my $name (@user_key_names) { + if (my $key = $software_key->get_subkey($name)) { + print "\n", $key->as_string, "\n"; + foreach my $value ($key->get_list_of_values) { + print $value->as_string, "\n"; + } + } + } + + # This demonstrates how you can deal with a binary value + # that contains a Unicode string + foreach my $ver (qw(8.0 9.0 10.0 11.0)) { + my $key_name = "Microsoft\\Office\\$ver\\Common\\UserInfo"; + if (my $key = $software_key->get_subkey($key_name)) { + print "\n", $key->as_string, "\n"; + my @value_names = qw(UserName UserInitials Company); + foreach my $value_name (@value_names) { + if (my $value = $key->get_value($value_name)) { + print $value->as_string, "\n"; + my $data = $value->get_data; + my $string = unpack_unicode_string($data); + print "$value_name = '$string'\n"; + } + } + } + } + } + + # The following code works on SYSTEM.DAT or SOFTWARE files + + my $software_key = $root_key->get_subkey("Software") || $root_key; + + if (defined($software_key)) { + my @software_key_names = ( + "Microsoft\\Windows\\CurrentVersion", + "Microsoft\\Windows NT\\CurrentVersion", + ); + + foreach my $name (@software_key_names) { + if (my $key = $software_key->get_subkey($name)) { + print "\n", $key->as_string, "\n"; + foreach my $value ($key->get_list_of_values) { + print $value->as_string, "\n"; + } + } + } + + # This demonstrates how you can deal with a Unix date + # found in a registry value + my $key_name = "Microsoft\\Windows NT\\CurrentVersion"; + if (my $curver_key = $software_key->get_subkey($key_name)) { + print "\n", $curver_key->as_string, "\n"; + if (my $value = $curver_key->get_value("InstallDate")) { + print $value->as_string, "\n"; + my $time = $value->get_data; + print "InstallDate = ", + scalar gmtime $time, " GMT\n"; + print "InstallDate = ", + scalar localtime $time, " Local\n"; + } + } + } + + # The following code works on SYSTEM.DAT or SYSTEM files + + my $system_key = $root_key->get_subkey("System") || $root_key; + + my $ccs_name = "CurrentControlSet"; # default for Win95 + if (my $key = $system_key->get_subkey("Select")) { + my $current_value = $key->get_value("Current"); + $ccs_name = sprintf("ControlSet%03d", $current_value->get_data); + print "CurrentControlSet = $ccs_name\n"; + } + + my $ccs_key = $system_key->get_subkey($ccs_name); + + if (defined($ccs_key)) { + my @system_key_names = ( + "Control\\ComputerName\\ComputerName", + "Control\\TimeZoneInformation", + ); + + foreach my $name (@system_key_names) { + if (my $key = $ccs_key->get_subkey($name)) { + print "\n", $key->as_string, "\n"; + foreach my $value ($key->get_list_of_values) { + print $value->as_string, "\n"; + } + } + } + + # This demonstrates how you can deal with a Windows date + # found in a registry value + my $key_name = "Control\\Windows"; + if (my $windows_key = $ccs_key->get_subkey($key_name)) { + print "\n", $windows_key->as_string, "\n"; + if (my $value = $windows_key->get_value("ShutdownTime")) { + print $value->as_string, "\n"; + my $data = $value->get_data; + my $time = unpack_windows_time($data); + print "ShutdownTime = ", + scalar gmtime $time, " GMT\n"; + print "ShutdownTime = ", + scalar localtime $time, " Local\n"; + } + } + } + +=head1 DESCRIPTION + +Parse::Win32Registry is a module for parsing Windows Registry files, +allowing you to read the keys and values of a registry file +without going through the Windows API. + +It provides an object-oriented interface to the keys and values +in a registry file. Registry files are structured as trees of keys, +with each key containing further subkeys or values. + +The module is intended to be cross-platform, and run on those platforms +where Perl will run. + +It supports both +Windows NT registry files (Windows NT, 2000, XP, 2003, Vista, 7) +and Windows 95 registry files (Windows 95, 98, Millennium Edition). + +It is intended to be used to parse offline registry files. +If a registry file is currently in use, you will not be able to open it. +However, you can save part or all of a currently loaded registry file +using the Windows reg command +if you have the appropriate administrative access. + +=head1 DEPENDENCIES + +Requires Perl 5.8.1. All required modules are standard modules. + +=head1 METHODS + +Start by creating a Registry object from a valid registry file. +Use the Registry object's get_root_key method +to obtain the root key of that registry file. +This root key is your first Key object. +From this key, you can explore the Key and Value objects +that comprise the registry file using the methods described below. + +Data is read directly from a registry file when a Key or Value object +is created, and discarded when the Key or Value object is destroyed. +This avoids any delay in parsing an entire registry file to obtain a +Key or Value object as most code only looks at a subset of the keys +and values contained in a registry file. + +=head2 Registry Object Methods + +=over 4 + +=item $registry = Parse::Win32Registry->new( 'filename' ) + +Creates a new Registry object for the specified registry file. + +=item $registry->get_root_key + +Returns the root Key object of the registry file. + +The root key of a registry file is not the same as one of the virtual +roots of the registry (HKEY_LOCAL_MACHINE, HKEY_USERS, etc) that you +might be familiar with from using tools such as REGEDIT. + +The names of root keys vary by operating system and by file. +For example, the name of the root key of a Windows XP NTUSER.DAT file +is '$$$PROTO.HIV' and the name of the root key of a Windows 98 +USER.DAT file is an empty string. + +=item $registry->get_virtual_root_key + +=item $registry->get_virtual_root_key( 'virtual root key name' ) + +Returns the virtual root Key object of the registry file. + +In all respects this is exactly the same as the root Key object, +except that it pretends to be a virtual root by simply faking its name. +It guesses the virtual root key name +by looking at the filename of the registry file. +For example, if the filename contains 'SYSTEM' +the virtual root key will be named 'HKEY_LOCAL_MACHINE\\SYSTEM'. +If the guess fails (because the filename is not recognised) +the virtual root key will be named 'HKEY_UNKNOWN'. + +You can override the guess by supplying your own root key name. +You can use this to pass in your preferred root key name. +For example, you could pass the filename of the registry file in as +the virtual root key name, which would then cause the filename to +appear as part of each key's path. + +=item $registry->get_timestamp + +Returns the embedded timestamp for the registry file as a time value +(the number of seconds since your computer's epoch) +suitable for passing to gmtime or localtime. + +Only Windows NT registry files have an embedded timestamp. + +Returns nothing if the date is out of range +or if called on a Windows 95 registry file. + +=item $registry->get_timestamp_as_string + +Returns the timestamp as a ISO 8601 string, +for example, '2010-05-30T13:57:11Z'. +The Z indicates that the time is GMT ('Zero Meridian'). + +Returns the string '(undefined)' if the date is out of range +or if called on a Windows 95 registry file. + +=item $registry->get_embedded_filename + +Returns the embedded filename for the registry file. + +Only Windows NT registry files have an embedded filename. + +Returns nothing if called on a Windows 95 registry file. + +=item $registry->get_filename + +Returns the filename of the registry file. + +=item $registry->get_length + +Returns the length of the registry file. + +=back + +=head2 Key Object Methods + +=over 4 + +=item $key->get_name + +Returns the name of the key. The root key of a Windows 95 based +registry file does not have a name; this is returned as an empty +string. + +=item $key->get_path + +Returns the path to the key. This shows the all of the keys +from the root key to the current key, +joined by the path separator '\'. + +=item $key->get_subkey( 'key name' ) + +Returns a Key object for the specified subkey name. +If a key with that name does not exist, nothing will be returned. + +You can specify a path to a subkey by separating keys +using the path separator '\'. Remember +to quote any '\' characters with a preceding '\'. +For example: + + $key->get_subkey('Software\\Microsoft\\Windows') + +A path is always relative to the current key. +It should start with the name of the first subkey in the path, +not the current key. +If any key in the path does not exist, nothing will be returned. + +=item $key->get_value( 'value name' ) + +Returns a Value object for the specified value name. +If a value with that name does not exist, nothing will be returned. + +The default value (displayed as '(Default)' by REGEDIT) +does not actually have a name. It can obtained by supplying +an empty string, e.g. $key->get_value(''); + +=item $key->get_value_data( 'value name' ) + +Returns the data for the specified value name. +If either the value or the value's data does not exist, +nothing will be returned. + +This is simply a shortcut for accessing the data of a value +without creating an intermediate Value object. + +The following code: + + my $value = $key->get_value('value name'); + if (defined $value) { + my $data = $value->get_data; + if (defined $data) { + ...process data... + } + } + +can be replaced with: + + my $data = $key->get_value_data('value name'); + if (defined $data) { + ...process data... + } + +=item $key->get_list_of_subkeys + +Returns a list of Key objects representing the subkeys of the +current key. If a key has no subkeys, an empty list will be returned. + +=item $key->get_list_of_values + +Returns a list of Value objects representing the values of the +current key. If a key has no values, an empty list will be returned. + +=item $key->get_timestamp + +Returns the timestamp for the key as a time value +(the number of seconds since your computer's epoch) +suitable for passing to gmtime or localtime. + +Only Windows NT registry keys have a timestamp. + +Returns nothing if the date is out of range +or if called on a Windows 95 registry key. + +=item $key->get_timestamp_as_string + +Returns the timestamp as an ISO 8601 string, +for example, '2010-05-30T13:57:11Z'. +The Z indicates that the time is GMT ('Zero Meridian'). + +Returns the string '(undefined)' if the date is out of range +or if called on a Windows 95 registry key. + +=item $key->as_string + +Returns the path of the key as a string. +The timestamp will be appended for Windows NT registry keys. + +=item $key->as_regedit_export + +Returns the path of the key as a string +in the Windows Registry Editor Version 5.00 export format. +The string will be terminated with a newline character. + +If used in conjunction with the get_virtual_root_key method +of Registry objects this should generate key paths +interoperable with those exported by REGEDIT. + +=item $key->get_parent + +Returns a Key object for parent of the current key. +If the key does not have a valid parent key +(which will normally only occur for the root key) +nothing will be returned. + +=item $key->is_root + +Returns true if this key is the root key. + +=item $key->get_class_name + +Returns a string containing the class name associated with a key. +Only a very few Windows NT registry key have class names. + +Returns nothing if the key has no class name +or if called on a Windows 95 registry key. + +=item $key->get_security + +Returns a Security object containing the security information +for the key. Only Windows NT registry keys have security information. + +Returns nothing if called on a Windows 95 registry key. + +=item $key->get_subkey_iterator + +Returns an iterator for retrieving the subkeys of the current key. +Each time the get_next method of the iterator is used, +it will return a single Key object. +Keys will be returned one by one +until the end of the list is reached, +when nothing will be returned. + +It can be used as follows: + + my $subkey_iter = $key->get_subkey_iterator; + while (my $subkey = $subkey_iter->get_next) { + # do something with $subkey + ... + } + +Note that it is usually simpler to just use $key->get_list_of_subkeys. +An iterator might be useful when you need to +control the amount of processing you are performing, +such as programs that need to remain responsive to user actions. + +=item $key->get_value_iterator + +Returns an iterator for retrieving the values of the current key. +Each time the get_next method of the iterator is used, +it will return a single Value object. +Values will be returned one by one +until the end of the list is reached, +when nothing will be returned. + +It can be used as follows: + + my $value_iter = $key->get_value_iterator; + while (my $value = $value_iter->get_next) { + # do something with $value + ... + } + +Note that it is usually simpler to just use $key->get_list_of_values. + +=item $key->get_subtree_iterator + +Returns an iterator for retrieving the entire subtree +of keys and values beginning at the current key. +Each time the get_next method of the iterator is used, +it will return either a Key object +or a Key object and a Value object. +Each value accompanies the key that it belongs to. +Keys or Key/Value pairs will be returned one by one +until the end of the list is reached, +when nothing will be returned. + +It can be used as follows: + + my $subtree_iter = $key->get_subtree_iterator; + while (my ($key, $value) = $subtree_iter->get_next) { + if (defined $value) { + # do something with $key and $value + ... + } + else { + # do something with $key + ... + } + } + +Keys and values will be returned in the following order: + + root_key + root_key\key1 + root_key\key1, value1 + root_key\key1, value2 + root_key\key1\key2 + root_key\key1\key2, value3 + root_key\key1\key2, value4 + +If the iterator is used in a scalar context, +only Key objects will returned. + + my $subtree_iter = $key->get_subtree_iterator; + while (my $key = $subtree_iter->get_next) { + # do something with $key + ... + } + +Keys will be returned in the following order: + + root_key + root_key\key1 + root_key\key1\key2 + + +Note that it might be simpler to write a recursive function +to process the keys and values. + + sub traverse { + my $key = shift; + + # do something with $key + ... + + foreach my $value ($key->get_list_of_values) { + # do something with $value + ... + } + + foreach my $subkey ($key->get_list_of_subkeys) { + # recursively process $key + traverse($subkey); + } + } + + traverse($root_key); + +=item $key->walk( \&callback ); + +Performs a recursive descent of all the keys +in the subtree starting with the calling key, +and calls the callback function for each key reached. + +The callback function will be passed the current key. + + $key->walk( sub { + my $key = shift; + print $key->as_string, "\n"; + } ); + + $key->walk( sub { + my $key = shift; + print $key->as_regedit_export; + foreach my $value ($key->get_list_of_values) { + print $value->as_regedit_export; + } + } ); + +=back + +=head2 Value Object Methods + +=over 4 + +=item $value->get_name + +Returns the name of the value. +In both Windows NT and Windows 95 based registry files +you can get values without a name. +This is returned as an empty string. + +=item $value->get_type + +Returns the integer representing the type of the value +(where 1 is a REG_SZ, 2 is a REG_EXPAND_SZ, etc). +The constants for the value types can be imported from +the Parse::Win32Registry module with + + use Parse::Win32Registry qw( :REG_ ); + +=item $value->get_type_as_string + +Returns the type of the value as a string instead of an integer constant, +making it more suitable for printed output. + +=item $value->get_data + +Returns the data for the value. + +REG_SZ and REG_EXPAND_SZ values will be returned as strings. +String data will be converted from Unicode (UCS-2LE) for Windows +NT based registry files. +Any terminating null characters will be removed. + +REG_MULTI_SZ values will be returned as a list of strings when +called in a list context, +and as a string with each element separated by +the list separator $" when called in a scalar context. +(The list separator defaults to the space character. +See perlvar for further information.) +String data will be converted from Unicode (UCS-2LE) for Windows +NT based registry files. + + # get REG_MULTI_SZ data as a string + my $data = $multi_sz_value->get_data; + + # get REG_MULTI_SZ data as a list + my @data = $multi_sz_value->get_data; + +REG_DWORD values are unpacked and returned as unsigned integers. + +All other types are returned as packed binary strings. +To extract data from these packed binary strings, +you will need to use Perl's unpack function, +or one of the provided support functions. + +Nothing will be returned if the data is invalid. + +=item $value->get_data_as_string + +Returns the data for a value, making binary data safe for printed output. + +REG_SZ and REG_EXPAND_SZ values will be returned directly from get_data, +REG_MULTI_SZ values will have their component strings prefixed by +indices to more clearly show the number of elements, and +REG_DWORD values will be returned as a hexadecimal number followed +by its parenthesized decimal equivalent. +All other types of values will be returned as a string of hex octets. + +'(invalid data)' will be returned if the data is invalid +(i.e. when get_data returns undef). + +'(no data)' will be returned if get_data returns an empty string. + +=item $value->get_raw_data + +Returns the data for a value exactly as it was read from the registry, +without the processing normally performed by get_data. + +It is intended for those rare occasions +when you need to access binary data that has been +inappropriately stored in +a REG_SZ, REG_EXPAND_SZ, REG_MULTI_SZ, or REG_DWORD value. + +=item $value->as_string + +Returns the name, type, and data for the value as a string, +safe for printed output. + +'(Default)' will be used for the names of +those values that do not have names. + +=item $value->as_regedit_export + +Returns the name, type, and data for the value as a string, +in the Windows Registry Editor Version 5.00 export format. +The string will contain line breaks to ensure that +no line is longer than 80 characters. +Each line will be terminated with a newline character. + +'@' will be used for the names of +those values that do not have names. + +This should generate values +interoperable with those exported by REGEDIT. + +=back + +=head2 Security Object Methods + +Only Windows NT registry files contain security information +to control access to the registry keys. +This information is stored in security entries which are distributed +through the registry file separately from the keys that they apply to. +This allows the registry to share security information +amongst a large number of keys whilst unnecessary duplication. + +Security entries link to other security entries in a circular chain, +each entry linking to the one that precedes it and the one that follows it. + +=over 4 + +=item $security->get_security_descriptor + +Returns a Security Descriptor Object representing the security descriptor +contained in the security information registry entry. + +=item $security->get_next + +Returns the next security object. + +=item $security->get_previous + +Returns the previous security object. + +=item $security->get_reference_count + +Returns the reference count for the security object. + +=back + +=head2 Security Descriptor Object Methods + +A Security Descriptor object represents a security descriptor which +contains an owner SID, a primary group SID, +a System ACL, and a Discretionary ACL. + +=over 4 + +=item $security_descriptor->get_owner + +Returns a SID Object containing the Owner SID. + +=item $security_descriptor->get_group + +Returns a SID Object containing the primary group SID. + +=item $security_descriptor->get_sacl + +Returns an ACL Object containing the System ACL. +The System ACL contains those ACEs used for auditing. +Nothing will be returned if the security descriptor does not contain +a System ACL. + +=item $security_descriptor->get_dacl + +Returns an ACL Object containing the Discretionary ACL. +The Discretionary ACL contains those ACEs used for access control. +Nothing will be returned if the security descriptor does not contain +a Discretionary ACL. + +=item $security_descriptor->as_stanza + +Returns a multi-line string containing +the security descriptor formatted for presentation. +It will contain a line for the owner SID, +the group SID, +and each component ACE of the System ACL and the Discretionary ACL. +Each line will be terminated by a newline character. + +=back + +=head2 ACL Object Methods + +An ACL object represents an Access Control List, +which comprises a list of Access Control Entries. + +=over 4 + +=item $acl->get_list_of_aces + +Returns a list of ACE Objects representing the ACEs +in the order they appear in the ACL. +If the ACL contains no ACEs, nothing will be returned. + +=item $acl->as_stanza + +Returns a multi-line string containing +the ACL formatted for presentation. +It will contain a line for each component ACE of the ACL. +Each line will be terminated by a newline character. + +=back + +=head2 ACE Object Methods + +An ACE object represents an Access Control Entry. +An ACE describes the permissions assigned (the access mask) +to a Security Identifier (the trustee). + +=over 4 + +=item $ace->get_type + +Returns an integer containing the ACE type, +where 0 indicates an ACCESS_ALLOWED ACE, +1 an ACCESS_DENIED ACE, and +2 a SYSTEM_AUDIT ACE. +Typically you will encounter +ACCESS_ALLOWED and ACCESS_DENIED ACEs in Discretionary ACLs +and SYSTEM_AUDIT ACEs in System ACLs. + +=item $ace->get_type_as_string + +Returns the type as a string, rather than integer. + +=item $ace->get_flags + +Returns an integer containing the ACE flags. + +=item $ace->get_access_mask + +Returns an integer containing the ACE access mask. +The access mask controls what actions the trustee might perform with +the object the ACE applies to. + +=item $ace->get_trustee + +Returns a SID Object containing the trustee that this ACE +is associated with. + +=item $ace->as_string + +Returns a string containing +the ACE formatted for presentation. + +=back + +=head2 SID Object Methods + +A SID object represents a Security Identifier. + +=over 4 + +=item $sid->get_name + +Returns a string containing a name for the SID +(e.g. "Administrators" for S-1-5-32-544) +if it is a "well known" SID. +See Microsoft Knowledge Base Article KB243330. + +=item $sid->as_string + +Returns a string containing the SID formatted for presentation. + +=back + +=head1 EXPORTS + +=head2 Constants + +On request, Parse::Win32Registry will export the registry type constants: + + use Parse::Win32Registry qw( :REG_ ); + +The :REG_ tag exports all of the following constants: + + REG_NONE + REG_SZ + REG_EXPAND_SZ + REG_BINARY + REG_DWORD + REG_DWORD_BIG_ENDIAN + REG_LINK + REG_MULTI_SZ + REG_RESOURCE_LIST + REG_FULL_RESOURCE_DESCRIPTOR + REG_RESOURCE_REQUIREMENTS_LIST + REG_QWORD + +You can import individual types by specifying them, for example: + + use Parse::Win32Registry qw( REG_SZ REG_DWORD ); + +=head1 SUPPORT FUNCTIONS + +Parse::Win32Registry provides a number of support functions, +which are exported on request. All of the support functions can +be imported with: + + use Parse::Win32Registry qw( :functions ); + +=head2 Unpacking Binary Data + +There are a number of functions for assisting in unpacking binary data +found in registry values. +These functions are exported on request: + + use Parse::Win32Registry qw( unpack_windows_time + unpack_unicode_string + unpack_sid + unpack_ace + unpack_acl + unpack_security_descriptor ); + +These unpack functions also return the length +of the packed object when called in a list context. + +For example, to extract one SID: + + my $sid = unpack_sid($data); + +To extract a series of SIDs: + + my $pos = 0; + while ($pos < length($data)) { + my ($sid, $packed_len) = unpack_sid(substr($data, $pos)); + last if !defined $sid; # abort if SID not defined + + # ...do something with $sid... + + $pos += $packed_len; # move past the packed SID + } + +=over 4 + +=item $time = unpack_windows_time( $data ) +=item ( $time, $packed_len ) = unpack_windows_time( $data ) + +Returns the epoch time for the Win32 FILETIME +contained in the supplied binary data. +A Win32 FILETIME is a 64-bit integer +containing the number of 100-nanosecond intervals since January 1st, 1601 +and can sometimes be found in Windows NT registry values. + +Returns nothing if the date is earlier than your computer's epoch. +The epoch begins at January 1st, 1970 on Unix and Windows machines. + +When called in a list context, it will also return the space used +in the supplied data by the windows time. + +(This function can also be called by its previous name of +convert_filetime_to_epoch_time.) + +=item $str = unpack_unicode_string( $data ) +=item ( $str, $packed_len ) = unpack_unicode_string( $data ) + +Extracts a Unicode (UCS-2LE) string from the supplied binary data. +Any terminating null characters are dropped. +Unicode (UCS-2LE) strings are sometimes encountered +in Windows NT registry REG_BINARY values. + +Note that Unicode strings contained in +REG_SZ, REG_EXPAND_SZ, and REG_MULTI_SZ values +are already automatically decoded +by the get_data method of a Value object. + +When called in a list context, it will also return the space used +in the supplied data by the Unicode string. + +=item $sid = unpack_sid( $data ) +=item ( $sid, $packed_len) = unpack_sid( $data ) + +Returns a SID Object representing the SID contained in the supplied data. +Returns nothing if the supplied data does not appear to contain a valid SID. + +When called in a list context, it will also return the space used +in the supplied data by the SID. + +=item $ace = unpack_ace( $data ) +=item ( $ace, $packed_len ) = unpack_ace( $data ) + +Returns an ACE Object representing the ACE contained in the supplied data. +Returns nothing if the supplied data does not appear to contain a valid ACE. + +When called in a list context, it will also return the space used +in the supplied data by the ACE. + +=item $acl = unpack_acl( $data ) +=item ( $acl, $packed_len ) = unpack_acl( $data ) + +Returns an ACL Object representing the ACL contained in the supplied data. +Returns nothing if the supplied data does not appear to contain a valid ACL. + +When called in a list context, it will also return the space used +in the supplied data by the ACL. + +=item $sd = unpack_security_descriptor( $data ) +=item ( $sd, $packed_len ) = unpack_security_descriptor( $data ) + +Returns a Security Descriptor Object representing +the security descriptor contained in the supplied data. +Returns nothing if the supplied data does not appear to contain +a valid security descriptor. + +When called in a list context, it will also return the space used +in the supplied data by the security descriptor. + +=back + +=head2 Formatting Data + +These functions are exported on request: + + use Parse::Win32Registry qw( iso8601 hexdump ); + +=over 4 + +=item $str = iso8601( $epoch_time ) + +Returns the ISO8601 string for the supplied $epoch_time, +for example, '2010-05-30T13:57:11Z'. +It assumes the supplied $epoch_time is in UTC, +and appends 'Z' to indicate this. + +The string '(undefined)' will be returned if the epoch time +is out of range. + + my $data = $reg_binary_value->get_data; + + # extract the Win32 FILETIME starting at the 9th byte of $data + my $time = unpack_windows_time( substr( $data, 8 ) ); + my $time_as_string = iso8601( $time ); + print "$time_as_string\n"; + +There are a number of ways of displaying a timestamp. For example: + + use Parse::Win32Registry qw(iso8601); + use POSIX qw(strftime); + print iso8601($key->get_timestamp); + print scalar(gmtime($key->get_timestamp)), " GMT\n"; + print scalar(localtime($key->get_timestamp)), " Local\n"; + print strftime("%Y-%m-%d %H:%M:%S GMT", + gmtime($key->get_timestamp)), "\n"; + print strftime("%Y-%m-%d %H:%M:%S Local", + localtime($key->get_timestamp)), "\n"; + +...might produce the following output: + + 2000-08-06T23:42:36Z + Sun Aug 6 23:42:36 2000 GMT + Mon Aug 7 07:42:36 2000 Local + 2000-08-06 23:42:36 GMT + 2000-08-07 07:42:36 Local + +=item $str = hexdump( $data ) + +Returns a multi-line string containing +a hexadecimal dump of the supplied data. +Each line will display 16 bytes in hexadecimal and ASCII, +and will be terminated by a newline character. + +=back + +=head2 Processing Multiple Registry Files Simultaneously + +There are three support functions +that create iterators for simultaneously +processing the keys and values +of multiple registry files. +These functions are exported on request: + + use Parse::Win32Registry qw( make_multiple_subkey_iterator + make_multiple_value_iterator + make_multiple_subtree_iterator ); + + +Handling lists of subkeys or values +should be done with a little care +as some of the processed registry files +might not contain the subkey or value being examined +and the list will contain missing entries: + + ($key1, $key2, undef, $key4) + +One way of handling this is to use map to check that a key is defined +and return undef if the subkey or value is not present. + + @subkeys = map { defined $_ && $_->get_subkey('subkey') || undef } @keys; + + @values = map { defined $_ && $_->get_value('value') || undef } @keys; + +=over 4 + +=item $iter = make_multiple_subkey_iterator( $key1, $key2, $key3, ... ) + +Returns an iterator for retrieving +the subkeys of the supplied Key objects. +Each call to the get_next method of the iterator +returns a reference to +a list of Key objects with the same name and path. +If any of the supplied Key objects +does not have a subkey with that name, +then that subkey will be undefined. + + my $subkey_iter = make_multiple_subkey_iterator($key1, $key2, ...); + while (my ($subkey1, $subkey2, ...) = $subkey_iter->get_next) { + ... + } + + my $subkey_iter = make_multiple_subkey_iterator($key1, $key2, ...); + while (my @subkeys = $subkey_iter->get_next) { + foreach my $subkey (@subkeys) { + if (defined $subkey) { + ... + } + } + } + +=item $iter = make_multiple_value_iterator( $key1, $key2, $key3, ... ) + +Returns an iterator for retrieving +the values of the supplied Key objects. +Each call to the get_next method of the iterator +returns a reference to +a list of Value objects with the same name. +If any of the supplied Key objects +does not have a value with that name, +then that value will be undefined. + + my $value_iter = make_multiple_value_iterator($key1, $key2, ...); + while (my ($value1, $value2, ...) = $value_iter->get_next) { + ... + } + +=item $iter = make_multiple_subtree_iterator( $key1, $key2, $key3, ... ) + +Returns an iterator for retrieving +the immediate subkeys and all descendant subkeys of the supplied Key objects. +Each call to the get_next method of the iterator +returns a list of Key objects with the same name and path. +If any of the supplied Key objects +does not have a subkey with that name, +then that subkey will be undefined. + +Each call to the get_next method of the iterator +returns it will return +either a reference to a list of Key objects +or a reference to a list of Key objects +and a reference to a list of a Value objects, +with each list of values accompanying the list of keys that they belong to. +Nothing is returned when the end of the list is reached. + + my $subtree_iter = make_multiple_subtree_iterator($key1, $key2, ...); + while (my $subkeys_ref = $tree_iter->get_next) { + # do something with @$subkeys_ref + } + + my $subtree_iter = make_multiple_subtree_iterator($key1, $key2, ...); + while (my ($subkeys_ref, $values_ref) = $tree_iter->get_next) { + if (defined $values_ref) { + # do something with @$subkeys_ref and @$values_ref + for (my $i = 0; $i < @$values_ref; $i++) { + print $values_ref->[$i]->as_string, "\n"; + } + ... + } + else { + # do something with @$subkeys_ref + my $first_defined_subkey = (grep { defined } @$subkeys_ref)[0]; + print $first_defined_subkey->as_string, "\n"; + ... + } + } + +=back + +=head2 Comparing Keys and Values + +These functions are exported on request: + + use Parse::Win32Registry qw( compare_multiple_keys + compare_multiple_values ); + +=over 4 + +=item @changes = compare_multiple_keys( $key1, $key2, ... ); + +Returns a list of strings +describing the differences found between the supplied keys. +The keys are compared in the order they are supplied. +If one of the supplied keys is undefined, +it is assumed to have been deleted. + +The possible changes are 'ADDED', and 'DELETED', +and for Windows NT registry keys (which have timestamps) +'NEWER', and 'OLDER'. + +For example, compare_multiple_keys($k1, $k2, $k3) +would return the list ('', 'NEWER', '') +if $k2 had a more recent timestamp than $k1, +but $k3 had the same timestamp as $k2. + +You can count the number of changed keys using the grep operator: + + my $num_changes = grep { $_ } @changes; + +=item @changes = compare_multiple_values( $value1, $value2, ... ); + +Returns a list of strings +describing the differences found between the supplied values. +The values are compared in the order they are supplied. +If one of the supplied values is undefined, +it is assumed to have been deleted. + +The possible changes are 'ADDED', 'DELETED', and 'CHANGED'. + +For example, compare_multiple_keys($v1, $v2, $v3) +would return the list ('', 'ADDED', 'CHANGED') +if $v2 exists but $v1 did not, +and $v3 had different data from $v2. + +You can count the number of changed values using the grep operator: + + my $num_changes = grep { $_ } @changes; + +=back + +=head1 HANDLING INVALID DATA + +The Parse::Win32Registry module will skip keys or values +that cannot be successfully parsed. + +If keys or values cannot be parsed, then +the get_subkey and get_value methods of Key objects +will return nothing. + +The get_list_of_subkeys and get_list_of_values methods of Key objects +will skip those keys or values that cannot be parsed. +If none of the keys or values can be parsed successfully, +an empty list will be returned. + +Additionally, values (in Windows NT registry files) +often store data in a separate area of the registry file. +If the value can be parsed, but the data cannot, +a Value object will be created, +but it will have no data. +The get_data method will return nothing. + +The most robust way of handling keys or values or data +is therefore to check that they are defined before processing them. +For example: + + my $key = $root_key->get_subkey( "Software\\Perl" ); + if ( defined $key ) { + print $key->as_string, "\n"; + my $value = $key->get_value( "Version" ); + if ( defined $value ) { + print $value->as_string, "\n"; + my $data = $value->get_data; + if ( defined $data ) { + # process $data in some way... + } + } + } + +You might not feel this robustness is necessary for your scripts. + +You can be alerted when there are problems parsing registry keys or values +by switching on warnings with: + + Parse::Win32Registry->enable_warnings; + +They can be switched off again with: + + Parse::Win32Registry->disable_warnings; + +=head1 LOW-LEVEL METHODS + +These methods are intended for those +who want to look at the structure of a registry file, +but with something a little more helpful than a hex editor. +They are not designed for pulling data out of keys and values: +they are designed to make it easier to look at the underlying +components of a registry file. + +Windows NT registry files are composed of one or more Hbin blocks. +Hbin blocks can contain a series of entries, +such as key, value, and security entries, +but also includes subkey lists, value lists, key class names, and value data. + +Windows 95 registry files are composed of an RGKN block, +followed by one or more RGDB blocks. +RGKN blocks contain the entries which link the registry keys +in the form of a tree. +RGDB blocks contain a corresponding entry for each key in the RGKN block. +This RGDB entry includes the name of the key and any associated values. +For convenience, when iterating the entries in an RGDB block, +each will be returned as a key entry followed by zero or more value entries. + +To see demonstrations of how these methods can be used, +look at the regscan.pl, gtkregscope.pl, and wxregscope.pl scripts. + +=head2 Registry Object Methods + +=over 4 + +=item $registry->get_block_iterator + +Returns an iterator for retrieving all the blocks in a registry file. +Each time the get_next method of the iterator is used, +it will return a single Block object. +Blocks will be returned one by one +until the end of the registry file is reached, +when nothing will be returned. + +Typically you would iterate over all the blocks in a registry file, +and iterate over all the entries in each block: + + my $block_iter = $registry->get_block_iterator; + while (my $block = $block_iter->get_next) { + my $entry_iter = $block->get_entry_iterator; + while (my $entry = $entry_iter->get_next) { + ... + } + } + +=item $registry->get_entry_iterator + +Returns an iterator for retrieving all the entries in a registry file. +Each time the get_next method of the iterator is used, +it will return a single Entry object. +Entries will be returned one by one +until the end of the registry file is reached, +when nothing will be returned. + +This is simply a more convenient method for retrieving +all the entries in a registry file, +which does not require you to iterate over each block. + +=back + +=head2 Block Object Methods + +=over 4 + +=item $block->get_entry_iterator + +Returns an iterator for retrieving all the entries in a block. +Each time the get_next method of the iterator is used, +it will return a single Entry object. +Entries will be returned one by one +until the end of the block is reached, +when nothing will be returned. + + my $entry_iter = $block->get_entry_iterator; + while (my $entry = $entry_iter->get_next) { + ... + } + +=item $block->get_offset + +Returns the position of the block relative to the start of the file. + +=item $block->get_length + +Returns the length of the block. + +=item $block->parse_info + +Returns a string containing a summary of the parser information +for the block. + +=item $block->unparsed + +Returns a string containing a hex dump +of the unparsed on-disk data for the block header. + +=item $block->get_raw_bytes + +Returns the unparsed on-disk data for the block header. + +=back + +=head2 Entry Object Methods + +In addition to the basic methods provided by all entries, +if an entry is a key, value, or security entry, +it will also provide the methods available to +Key, Value, or Security objects. +You might therefore find it useful to check what methods +are available so that you can use them: + + # use Entry object methods... + ... + if ($entry->can('get_subkey')) { + # use Key object methods... + } + elsif ($entry->can('get_data')) { + # use Value object methods... + } + elsif ($entry->can('get_security_descriptor')) { + # use Security object methods... + } + +=over 4 + +=item $entry->get_offset + +Returns the position of the entry relative to the start of the file. + +=item $entry->get_length + +Returns the length of the entry. + +=item $entry->get_tag + +Returns a string containing a descriptive tag for the entry. + +For Windows NT registry entries, the tags reflect the +signatures used to identify them. +These are: +'nk' for keys; +'vk' for values; +'sk' for security entries; +and 'lf', 'lh', 'li', or 'ri' for subkey lists. +Entries that do not have signatures will return an empty string. +Unidentified entries include +value lists, value data, and the class names of keys. + +For Windows 95 registry files, the tag +reflects which part of the registry file the entry is from, +and will be +'rgkn key', +'rgdb key', +or 'rgdb value'. + +=item $entry->is_allocated + +Returns a boolean value indicating the 'allocated' state of a +Windows NT registry entry. + +This value has no meaning for Windows 95 registry entries. + +=item $entry->as_string + +Returns a string representation of the entry. + +If the entry is a valid Key, Value, or Security object, +then as_string will call the as_string method of that object. + +=item $entry->parse_info + +Returns a string containing a summary of the parser information +for that entry. + +If the entry is a valid Key, Value, or Security object, +then parse_info will call the parse_info method of that object. + +=item $entry->unparsed + +Returns a string containing a hex dump +of the unparsed on-disk data for the entry. + +=item $entry->get_raw_bytes + +Returns the unparsed on-disk data for the entry. + +=back + +=head1 SCRIPTS + +All of the supplied scripts are intended to be used either as tools +or as examples for you to modify and develop. + +Try regdump.pl or regshell.pl to look at a registry file +from the command line, +or gtkregview.pl or wxregview.pl if you want a GUI. +If you want to compare registry files, +try regmultidiff.pl from the command line +or gtkregcompare.pl or wxregcompare.pl if you want a GUI. +You can edit the scripts to customize them for your own requirements. + +If you specify subkeys on the command line, note that you need to +quote the subkey on Windows if it contains spaces: + + regdump.pl ntuser.dat "software\microsoft\windows nt" + +You will also need to quote backslashes and spaces in Unix shells: + + regdump.pl ntuser.dat software\\microsoft\\windows\ nt + +or use single quotes: + + regdump.pl ntuser.dat 'software\microsoft\windows nt' + +=head2 gtkregcompare.pl + +gtkregcompare.pl is a GTK+ program for comparing multiple registry files. +It displays a tree of the registry keys and values +highlighting those that have changed. + +It requires Gtk2-Perl to be installed. + +Filenames of registry files to compare can be supplied on the command line: + + gtkregcompare.pl ... + +You can of course use wildcards when running from a Unix shell. + +=head2 gtkregscope.pl + +gtkregscope.pl is a GTK+ registry scanner. +It presents all the entries in a registry file returned by the +get_block_iterator and get_entry_iterator methods. +It uses color to highlight key, value, security, and subkey list entries, +and presents the block as a colored map. + +It requires Gtk2-Perl to be installed. + +A filename can also be supplied on the command line: + + gtkregscope.pl + +=head2 gtkregview.pl + +gtkregview.pl is a GTK+ registry viewer. +It displays a tree of registry keys on the left hand side, +a list of values on the right, +and a hex dump of the selected value data at the bottom. + +It requires Gtk2-Perl to be installed. + +A filename can also be supplied on the command line: + + gtkregview.pl + +=head2 regclassnames.pl + +regclassnames.pl will display registry keys that have class names. +Only a very few Windows NT registry key have class names. + +Type regclassnames.pl on its own to see the help: + + regclassnames.pl [subkey] + +=head2 regdump.pl + +regdump.pl is used to display the keys and values of a registry file. + +Type regdump.pl on its own to see the help: + + regdump.pl [subkey] [-r] [-v] [-x] [-c] [-s] [-o] + -r or --recurse traverse all child keys from the root key + or the subkey specified + -v or --values display values + -x or --hexdump display value data as a hex dump + -c or --class-name display the class name for the key (if present) + -s or --security display the security information for the key, + including the owner and group SIDs, + and the system and discretionary ACLs (if present) + -o or --owner display the owner SID for the key (if present) + +The contents of the root key will be displayed unless a subkey is +specified. Paths to subkeys are always specified relative to the root +key. By default, only the subkeys and values immediately underneath +the specified key will be displayed. To display all keys and values +beneath a key, use the -r or --recurse option. + +For example, regdump.pl ntuser.dat might display the following: + + $$$PROTO.HIV [2005-01-01T09:00:00Z] + ..\AppEvents + ..\Console + ..\Control Panel + ..\Environment + ..\Identities + ..\Keyboard Layout + ..\Printers + ..\Software + ..\UNICODE Program Groups + +From here, you can explore the subkeys to find those keys or values +you are interested in: + + regdump.pl ntuser.dat software + regdump.pl ntuser.dat software\microsoft + ... + +=head2 regexport.pl + +regexport.pl will display registry keys and values +in the Windows Registry Editor Version 5.00 format +used by REGEDIT on Windows 2000 and later. + +Type regexport.pl on its own to see the help: + + regexport.pl [subkey] [-r] + -r or --recurse traverse all child keys from the root key + or the subkey specified + +Values are always shown for each key displayed. + +Subkeys are displayed as comments when not recursing. +(Comments are preceded by the ';' character.) + +=head2 regfind.pl + +regfind.pl is used to search the keys, values, data, or types +of a registry file for a matching string. + +Type regfind.pl on its own to see the help: + + regfind.pl [-k] [-v] [-d] [-t] [-x] + -k or --key search key names for a match + -v or --value search value names for a match + -d or --data search value data for a match + -t or --type search value types for a match + -x or --hexdump display value data as a hex dump + +To search for the string "recent" in the names of any keys or values: + + regfind.pl ntuser.dat recent -kv + +To search for the string "administrator" in the data of any values: + + regfind.pl ntuser.dat administrator -d + +To list all REG_MULTI_SZ values: + + regfind.pl ntuser.dat -t multi_sz + +Search strings are not case-sensitive. + +=head2 regml.pl + +regml.pl will display those keys with explicit System Mandatory Label ACEs +set in the System ACL. +This feature was introduced with Windows Vista, and is used by applications +such as Internet Explorer running in Protected Mode. +Note that if a key does not have an explicit System Mandatory Label ACE, +it has Medium Integrity Level. +Only Windows NT registry files can contain System Mandatory Label ACEs. + +Type regml.pl on its own to see the help: + + regml.pl + +=head2 regmultidiff.pl + +regmultidiff.pl can be used to compare multiple registry files +and identify the differences between them. + +Type regmultidiff.pl on its own to see the help: + + regmultidiff.pl ... [] [-v] [-x] [-l] [-a] + -v or --values display values + -x or --hexdump display value data as a hex dump + -l or --long show each changed key or value instead of a summary + -a or --all show all keys and values before and after a change + +You can limit the comparison by specifying an initial subkey. + +=head2 regscan.pl + +regscan.pl dumps all the entries in a registry file. +This will include defunct keys and values that are no longer part +of the current active registry. + +Type regscan.pl on its own to see the help: + + regscan.pl [-k] [-v] [-s] [-a] [-p] [-u] [-w] + -k or --keys list only 'key' entries + -v or --values list only 'value' entries + -s or --security list only 'security' entries + -a or --allocated list only 'allocated' entries + -p or --parse-info show the technical information for an entry + instead of the string representation + -u or --unparsed show the unparsed on-disk entries as a hex dump + +=head2 regsecurity.pl + +regsecurity.pl will display the security information +contained in a registry files. +Only Windows NT registry files contain security information. + +Type regsecurity.pl on its own to see the help: + + regsecurity.pl + +=head2 regshell.pl + +Provides an interactive command shell +where you navigate through the keys +using 'cd' to change the current key +and 'ls' or 'dir' to list the contents of the current key. + +Tab completion of subkey and value names is available. +Names containing spaces are supported by quoting names with " characters. +Note that names are case sensitive. + +A filename should be supplied on the command line: + + regshell.pl + +Once regshell.pl is running, type help to see the available commands. + +It requires Term::ReadLine to be installed. + +=head2 regstats.pl + +regstats.pl counts the number of keys and values in a registry file. +It will also provide a count of each value type if requested. + +Type regstats.pl on its own to see the help: + + regstats.pl [-t] + -t or --types count value types + +=head2 regtimeline.pl + +regtimeline.pl displays keys and values in date order. + +As only Windows NT based registry keys provide timestamps, +this script only works on Windows NT registry files. + +You can limit the display to a given number of days +(counting back from the timestamp of the last key). + +Type regtimeline.pl on its own to see the help: + + regtimeline.pl [subkey] [-l ] [-v] [-x] + -l or --last display only the last days + of registry activity + -v or --values display values + -x or --hexdump display value data as a hex dump + +=head2 regtree.pl + +regtree.pl simply displays the registry as an indented tree, +optionally displaying the values of each key. + +Type regtree.pl on its own to see the help: + + regtree.pl [subkey] [-v] + -v or --values display values + +=head2 wxregcompare.pl + +wxregcompare.pl is a wxWidgets program for comparing multiple registry files. +It displays a tree of the registry keys and values, +highlighting those that have changed. + +It requires wxPerl to be installed. + +Filenames of registry files to compare can be supplied on the command line: + + wxregcompare.pl ... + +You can of course use wildcards when running from a Unix shell. + +=head2 wxregscope.pl + +wxregscope.pl is a wxWidgets registry scanner. +It presents all the entries in a registry file returned by the +get_block_iterator and get_entry_iterator methods. +It uses color to highlight key, value, security, and subkey list entries. + +It requires wxPerl to be installed. + +A filename can also be supplied on the command line: + + wxregscope.pl + +=head2 wxregview.pl + +wxregview.pl is a wxWidgets registry viewer. +It displays a tree of registry keys on the left hand side, +a list of values on the right, +and a hex dump of the selected value data at the bottom. +It can also provide a timeline view of all of the registry keys, +which can be used to navigate the main tree view +by clicking or double-clicking on a timeline key. + +It requires wxPerl to be installed. + +A filename can also be supplied on the command line: + + wxregview.pl + +=head1 ACKNOWLEDGEMENTS + +This would not have been possible without the work of those people who have +analysed and shared their knowledge of the structure of Windows Registry files, +primarily: +B.D. (WinReg.txt), +Petter Nordahl-Hagen (chntpw), +and Richard Sharpe and Jerry Carter (Samba 3). + +=head1 AUTHOR + +James Macfarlane, Ejmacfarla@cpan.orgE + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2006-2012 by James Macfarlane + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, +THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS +FOR A PARTICULAR PURPOSE. + +=cut diff --git a/thirdparty/rr-full/Parse/Win32Registry/Base.pm b/thirdparty/rr-full/Parse/Win32Registry/Base.pm new file mode 100644 index 0000000000..6598f37b11 --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/Base.pm @@ -0,0 +1,1107 @@ +package Parse::Win32Registry::Base; + +use strict; +use warnings; + +use base qw(Exporter); + +use Carp; +use Encode; +use Time::Local qw(timegm); + +our @EXPORT_OK = qw( + warnf + iso8601 + hexdump + format_octets + unpack_windows_time + unpack_string + unpack_unicode_string + unpack_guid + unpack_sid + unpack_ace + unpack_acl + unpack_security_descriptor + unpack_series + make_multiple_subkey_iterator + make_multiple_value_iterator + make_multiple_subtree_iterator + compare_multiple_keys + compare_multiple_values + REG_NONE + REG_SZ + REG_EXPAND_SZ + REG_BINARY + REG_DWORD + REG_DWORD_BIG_ENDIAN + REG_LINK + REG_MULTI_SZ + REG_RESOURCE_LIST + REG_FULL_RESOURCE_DESCRIPTOR + REG_RESOURCE_REQUIREMENTS_LIST + REG_QWORD +); + +our %EXPORT_TAGS = ( + all => [@EXPORT_OK], +); + +use constant REG_NONE => 0; +use constant REG_SZ => 1; +use constant REG_EXPAND_SZ => 2; +use constant REG_BINARY => 3; +use constant REG_DWORD => 4; +use constant REG_DWORD_BIG_ENDIAN => 5; +use constant REG_LINK => 6; +use constant REG_MULTI_SZ => 7; +use constant REG_RESOURCE_LIST => 8; +use constant REG_FULL_RESOURCE_DESCRIPTOR => 9; +use constant REG_RESOURCE_REQUIREMENTS_LIST => 10; +use constant REG_QWORD => 11; + +our $WARNINGS = 0; + +our $CODEPAGE = 'cp1252'; + +sub warnf { + my $message = shift; + warn sprintf "$message\n", @_ if $WARNINGS; +} + +sub hexdump { + my $data = shift; # packed binary data + my $start = shift || 0; # starting value for displayed offset + + return '' if !defined($data); + + my $output = ''; + + my $fake_start = $start & ~0xf; + my $end = length($data); + + my $pos = 0; + if ($fake_start < $start) { + $output .= sprintf '%8x ', $fake_start; + my $indent = $start - $fake_start; + $output .= ' ' x $indent; + my $row = substr($data, $pos, 16 - $indent); + my $len = length($row); + $output .= join(' ', unpack('H2' x $len, $row)); + if ($indent + $len < 16) { + my $padding = 16 - $len - $indent; + $output .= ' ' x $padding; + } + $output .= ' '; + $output .= ' ' x $indent; + $row =~ tr/\x20-\x7e/./c; + $output .= $row; + $output .= "\n"; + $pos += $len; + } + while ($pos < $end) { + $output .= sprintf '%8x ', $start + $pos; + my $row = substr($data, $pos, 16); + my $len = length($row); + $output .= join(' ', unpack('H2' x $len, $row)); + if ($len < 16) { + my $padding = 16 - $len; + $output .= ' ' x $padding; + } + $output .= ' '; + $row =~ tr/\x20-\x7e/./c; + $output .= $row; + $output .= "\n"; + $pos += 16; + } + + return $output; +} + +sub format_octets { + my $data = shift; # packed binary data + my $col = shift || 0; # starting column, e.g. length of initial string + + return "\n" if !defined($data); + + my $output = ''; + + $col = 76 if $col > 76; + my $max_octets = int((76 - $col) / 3) + 1; + + my $end = length($data); + my $pos = 0; + my $num_octets = $end - $pos; + $num_octets = $max_octets if $num_octets > $max_octets; + while ($pos < $end) { + $output .= join(',', unpack("x$pos(H2)$num_octets", $data)); + $pos += $num_octets; + $num_octets = $end - $pos; + $num_octets = 25 if $num_octets > 25; + if ($num_octets > 0) { + $output .= ",\\\n "; + } + } + $output .= "\n"; + return $output; +} + +sub unpack_windows_time { + my $data = shift; + + if (!defined $data) { + return; + } + + if (length($data) < 8) { + return; + } + + # The conversion uses real numbers + # as 32-bit perl does not provide 64-bit integers. + # The equation can be found in several places on the Net. + # My thanks go to Dan Sully for Audio::WMA's _fileTimeToUnixTime + # which shows a perl implementation of it. + my ($low, $high) = unpack("VV", $data); + my $filetime = $high * 2 ** 32 + $low; + my $epoch_time = int(($filetime - 116444736000000000) / 10000000); + + # adjust the UNIX epoch time to the local OS's epoch time + # (see perlport's Time and Date section) + my $epoch_offset = timegm(0, 0, 0, 1, 0, 70); + $epoch_time += $epoch_offset; + + if ($epoch_time < 0 || $epoch_time > 0x7fffffff) { + $epoch_time = undef; + } + + return wantarray ? ($epoch_time, 8) : $epoch_time; +} + +sub iso8601 { + my $time = shift; + my $tz = shift; + + if (!defined $time) { + return '(undefined)'; + } + + if (!defined $tz || $tz ne 'Z') { + $tz = 'Z' + } + + # On Windows, gmtime will return undef if $time < 0 or > 0x7fffffff + if ($time < 0 || $time > 0x7fffffff) { + return '(undefined)'; + } + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime $time; + + # The final 'Z' indicates UTC ("zero meridian") + return sprintf '%04d-%02d-%02dT%02d:%02d:%02d%s', + 1900+$year, 1+$mon, $mday, $hour, $min, $sec, $tz; +} + +sub unpack_string { + my $data = shift; + + if (!defined $data) { + return; + } + + my $str; + my $str_len; + if ((my $end = index($data, "\0")) != -1) { + $str = substr($data, 0, $end); + $str_len = $end + 1; # include the final null in the length + } + else { + $str = $data; + $str_len = length($data); + } + + return wantarray ? ($str, $str_len) : $str; +} + +sub unpack_unicode_string { + my $data = shift; + + if (!defined $data) { + return; + } + + my $str_len = 0; + foreach my $v (unpack('v*', $data)) { + $str_len += 2; + last if $v == 0; # include the final null in the length + } + my $str = decode('UCS-2LE', substr($data, 0, $str_len)); + + # The decode function from Encode may create invalid unicode characters + # which cause subsequent warnings (e.g. during regex matching). + # For example, characters in the 0xd800 to 0xdfff range of the + # basic multilingual plane (0x0000 to 0xffff) are 'surrogate pairs' + # and are expected to appear as a 'high surrogate' (0xd800 to 0xdbff) + # followed by a 'low surrogate' (0xdc00 to 0xdfff). + + # remove any final null + if (length($str) > 0 && substr($str, -1, 1) eq "\0") { + chop $str; + } + + return wantarray ? ($str, $str_len) : $str; +} + +sub unpack_guid { + my $guid = Parse::Win32Registry::GUID->new($_[0]); + return if !defined $guid; + return wantarray ? ($guid, $guid->get_length) : $guid; +} + +sub unpack_sid { + my $sid = Parse::Win32Registry::SID->new($_[0]); + return if !defined $sid; + return wantarray ? ($sid, $sid->get_length) : $sid; +} + +sub unpack_ace { + my $ace = Parse::Win32Registry::ACE->new($_[0]); + return if !defined $ace; + return wantarray ? ($ace, $ace->get_length) : $ace; +} + +sub unpack_acl { + my $acl = Parse::Win32Registry::ACL->new($_[0]); + return if !defined $acl; + return wantarray ? ($acl, $acl->get_length) : $acl; +} + +sub unpack_security_descriptor { + my $sd = Parse::Win32Registry::SecurityDescriptor->new($_[0]); + return if !defined $sd; + return wantarray ? ($sd, $sd->get_length) : $sd; +} + +sub unpack_series { + my $function = shift; + my $data = shift; + + if (!defined $function || !defined $data) { + croak "Usage: unpack_series(\\\&unpack_function, \$data)"; + } + + my $pos = 0; + my @items = (); + while (my ($item, $item_len) = $function->(substr($data, $pos))) { + push @items, $item; + $pos += $item_len; + } + return @items; +} + +sub make_multiple_subkey_iterator { + my @keys = @_; + + # check @keys contains keys + if (@keys == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Key') } + @keys) { + croak 'Usage: make_multiple_subkey_iterator($key1, $key2, ...)'; + } + + my %subkeys_seen = (); + my @subkeys_queue; + for (my $i = 0; $i < @keys; $i++) { + my $key = $keys[$i]; + next if !defined $key; + foreach my $subkey ($key->get_list_of_subkeys) { + my $name = $subkey->get_name; + $subkeys_seen{$name}[$i] = $subkey; + } + } + foreach my $name (sort keys %subkeys_seen) { + # make sure number of subkeys matches number of keys + if (@{$subkeys_seen{$name}} != @keys) { + @{$subkeys_seen{$name}}[@keys - 1] = undef; + } + push @subkeys_queue, $subkeys_seen{$name}; + } + + return Parse::Win32Registry::Iterator->new(sub { + my $subkeys = shift @subkeys_queue; + if (defined $subkeys) { + return $subkeys; + } + else { + return; + } + }); +} + +sub make_multiple_value_iterator { + my @keys = @_; + + # check @keys contains keys + if (@keys == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Key') } + @keys) { + croak 'Usage: make_multiple_value_iterator($key1, $key2, ...)'; + } + + my %values_seen = (); + my @values_queue; + for (my $i = 0; $i < @keys; $i++) { + my $key = $keys[$i]; + next if !defined $key; + foreach my $value ($key->get_list_of_values) { + my $name = $value->get_name; + $values_seen{$name}[$i] = $value; + } + } + foreach my $name (sort keys %values_seen) { + # make sure number of values matches number of keys + if (@{$values_seen{$name}} != @keys) { + @{$values_seen{$name}}[@keys - 1] = undef; + } + push @values_queue, $values_seen{$name}; + } + + return Parse::Win32Registry::Iterator->new(sub { + my $values = shift @values_queue; + if (defined $values) { + return $values; + } + else { + return; + } + }); +} + +sub make_multiple_subtree_iterator { + my @keys = @_; + + # check @keys contains keys + if (@keys == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Key') } + @keys) { + croak 'Usage: make_multiple_subtree_iterator($key1, $key2, ...)'; + } + + my @start_keys = (\@keys); + push my (@subkey_iters), Parse::Win32Registry::Iterator->new(sub { + return shift @start_keys; + }); + my $value_iter; + my $subkeys; # used to remember subkeys while iterating values + + return Parse::Win32Registry::Iterator->new(sub { + if (defined $value_iter && wantarray) { + my $values = $value_iter->(); + if (defined $values) { + return ($subkeys, $values); + } + } + while (@subkey_iters > 0) { + $subkeys = $subkey_iters[-1]->(); # depth-first + if (defined $subkeys) { + push @subkey_iters, make_multiple_subkey_iterator(@$subkeys); + $value_iter = make_multiple_value_iterator(@$subkeys); + return $subkeys; + } + pop @subkey_iters; # iter finished, so remove it + } + return; + }); +} + +sub compare_multiple_keys { + my @keys = @_; + + # check @keys contains keys + if (@keys == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Key') } + @keys) { + croak 'Usage: compare_multiple_keys($key1, $key2, ...)'; + } + + my @changes = (); + + my $benchmark_key; + foreach my $key (@keys) { + my $diff = ''; + # Skip comparison for the first value + if (@changes > 0) { + $diff = _compare_keys($benchmark_key, $key); + } + $benchmark_key = $key; + push @changes, $diff; + } + return @changes; +} + +sub compare_multiple_values { + my @values = @_; + + # check @values contains values + if (@values == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Value') } + @values) { + croak 'Usage: compare_multiple_values($value1, $value2, ...)'; + } + + my @changes = (); + + my $benchmark_value; + foreach my $value (@values) { + my $diff = ''; + # Skip comparison for the first value + if (@changes > 0) { + $diff = _compare_values($benchmark_value, $value); + } + $benchmark_value = $value; + push @changes, $diff; + } + return @changes; +} + +sub _compare_keys { + my ($key1, $key2) = @_; + + if (!defined $key1 && !defined $key2) { + return ''; # 'MISSING' + } + elsif (defined $key1 && !defined $key2) { + return 'DELETED'; + } + elsif (!defined $key1 && defined $key2) { + return 'ADDED'; + } + + my $timestamp1 = $key1->get_timestamp; + my $timestamp2 = $key2->get_timestamp; + if ($key1->get_name ne $key2->get_name) { + return 'CHANGED'; + } + elsif (defined $timestamp1 && defined $timestamp2) { + if ($timestamp1 < $timestamp2) { + return 'NEWER'; + } + elsif ($timestamp1 > $timestamp2) { + return 'OLDER'; + } + } + else { + return ''; # comment out to check values... + my $value_iter = make_multiple_value_iterator($key1, $key2); + while (my ($val1, $val2) = $value_iter->get_next) { + if (_compare_values($val1, $val2) ne '') { + return 'VALUES'; + } + } + return ''; + } +} + +sub _compare_values { + my ($val1, $val2) = @_; + + if (!defined $val1 && !defined $val2) { + return ''; # 'MISSING' + } + elsif (defined $val1 && !defined $val2) { + return 'DELETED'; + } + elsif (!defined $val1 && defined $val2) { + return 'ADDED'; + } + + my $data1 = $val1->get_data; + my $data2 = $val2->get_data; + if ($val1->get_name ne $val2->get_name || + $val1->get_type != $val2->get_type || + defined $data1 ne defined $data2 || + (defined $data1 && defined $data2 && $data1 ne $data2)) { + return 'CHANGED'; + } + else { + return ''; + } +} + + +package Parse::Win32Registry::Iterator; + +use Carp; + +sub new { + my $class = shift; + my $self = shift; + + my $type = ref $self; + croak 'Missing iterator subroutine' if $type ne 'CODE' + && $type ne __PACKAGE__; + + bless $self, $class; + return $self; +} + +sub get_next { + $_[0]->(); +} + + +package Parse::Win32Registry::GUID; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + if (length($data) < 16) { + return; + } + + my $guid = sprintf '{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}', + unpack('VvvC2C6', $data); + + my $self = { + _guid => $guid, + _length => 16, + }; + bless $self, $class; + + return $self; +} + +sub as_string { + my $self = shift; + + return $self->{_guid}; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + + +package Parse::Win32Registry::SID; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + # 0x00 byte = revision + # 0x01 byte = number of sub authorities + # 0x07 byte = identifier authority + # 0x08 dword = 1st sub authority + # 0x0c dword = 2nd sub authority + # ... + + if (length($data) < 8) { + return; + } + + my ($rev, $num_sub_auths, $id_auth) = unpack('CCx5C', $data); + + if ($num_sub_auths == 0) { + return; + } + + my $sid_len = 8 + 4 * $num_sub_auths; + + if (length($data) < $sid_len) { + return; + } + + my @sub_auths = unpack("x8V$num_sub_auths", $data); + my $sid = "S-$rev-$id_auth-" . join('-', @sub_auths); + + my $self = { + _sid => $sid, + _length => $sid_len, + }; + bless $self, $class; + + return $self; +} + +# See KB243330 for a list of well known sids +our %WellKnownSids = ( + 'S-1-0-0' => 'Nobody', + 'S-1-1-0' => 'Everyone', + 'S-1-3-0' => 'Creator Owner', + 'S-1-3-1' => 'Creator Group', + 'S-1-3-2' => 'Creator Owner Server', + 'S-1-3-3' => 'Creator Group Server', + 'S-1-5-1' => 'Dialup', + 'S-1-5-2' => 'Network', + 'S-1-5-3' => 'Batch', + 'S-1-5-4' => 'Interactive', + 'S-1-5-5-\\d+-\\d+' => 'Logon Session', + 'S-1-5-6' => 'Service', + 'S-1-5-7' => 'Anonymous', + 'S-1-5-8' => 'Proxy', + 'S-1-5-9' => 'Enterprise Domain Controllers', + 'S-1-5-10' => 'Principal Self', + 'S-1-5-11' => 'Authenticated Users', + 'S-1-5-12' => 'Restricted Code', + 'S-1-5-13' => 'Terminal Server Users', + 'S-1-5-18' => 'Local System', + 'S-1-5-19' => 'Local Service', + 'S-1-5-20' => 'Network Service', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-500' => 'Administrator', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-501' => 'Guest', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-502' => 'KRBTGT', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-512' => 'Domain Admins', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-513' => 'Domain Users', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-514' => 'Domain Guests', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-515' => 'Domain Computers', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-516' => 'Domain Controllers', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-517' => 'Cert Publishers', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-518' => 'Schema Admins', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-519' => 'Enterprise Admins', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-520' => 'Group Policy Creator Owners', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-533' => 'RAS and IAS Servers', + 'S-1-5-32-544' => 'Administrators', + 'S-1-5-32-545' => 'Users', + 'S-1-5-32-546' => 'Guest', + 'S-1-5-32-547' => 'Power Users', + 'S-1-5-32-548' => 'Account Operators', + 'S-1-5-32-549' => 'Server Operators', + 'S-1-5-32-550' => 'Print Operators', + 'S-1-5-32-551' => 'Backup Operators', + 'S-1-5-32-552' => 'Replicators', + 'S-1-16-4096' => 'Low Integrity Level', + 'S-1-16-8192' => 'Medium Integrity Level', + 'S-1-16-12288' => 'High Integrity Level', + 'S-1-16-16384' => 'System Integrity Level', +); + +sub get_name { + my $self = shift; + + my $sid = $self->{_sid}; + + foreach my $regexp (keys %WellKnownSids) { + if ($sid =~ m/^$regexp$/) { + return $WellKnownSids{$regexp}; + } + } + return; +} + +sub as_string { + my $self = shift; + + return $self->{_sid}; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + + +package Parse::Win32Registry::ACE; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + # 0x00 byte = type + # 0x01 byte = flags + # 0x02 word = length + + # Types: + # ACCESS_ALLOWED_ACE_TYPE = 0 + # ACCESS_DENIED_ACE_TYPE = 1 + # SYSTEM_AUDIT_ACE_TYPE = 2 + # SYSTEM_MANDATORY_LABEL_ACE_TYPE = x011 + + # Flags: + # OBJECT_INHERIT_ACE = 0x01 + # CONTAINER_INHERIT_ACE = 0x02 + # NO_PROPAGATE_INHERIT_ACE = 0x04 + # INHERIT_ONLY_ACE = 0x08 + # INHERITED_ACE = 0x10 + # SUCCESSFUL_ACCESS_ACE_FLAG = 0x40 (Audit Success) + # FAILED_ACCESS_ACE_FLAG = 0x80 (Audit Failure) + + if (length($data) < 4) { + return; + } + + my ($type, $flags, $ace_len) = unpack('CCv', $data); + + if (length($data) < $ace_len) { + return; + } + + # The data following the header varies depending on the type. + # For ACCESS_ALLOWED_ACE, ACCESS_DENIED_ACE, SYSTEM_AUDIT_ACE + # the header is followed by an access mask and a sid. + # 0x04 dword = access mask + # 0x08 = SID + + # Only the following types are currently unpacked: + # 0 (ACCESS_ALLOWED_ACE), 1 (ACCESS_DENIED_ACE), 2 (SYSTEM_AUDIT_ACE) + if ($type >= 0 && $type <= 2 || $type == 0x11) { + my $access_mask = unpack('x4V', $data); + my $sid = Parse::Win32Registry::SID->new(substr($data, 8, + $ace_len - 8)); + + # Abandon ace if sid is invalid + if (!defined $sid) { + return; + } + + # Abandon ace if not the expected length + if (($sid->get_length + 8) != $ace_len) { + return; + } + + my $self = { + _type => $type, + _flags => $flags, + _mask => $access_mask, + _trustee => $sid, + _length => $ace_len, + }; + bless $self, $class; + + return $self; + } + else { + return; + } +} + +our @Types = qw( + ACCESS_ALLOWED + ACCESS_DENIED + SYSTEM_AUDIT + SYSTEM_ALARM + ALLOWED_COMPOUND + ACCESS_ALLOWED_OBJECT + ACCESS_DENIED_OBJECT + SYSTEM_AUDIT_OBJECT + SYSTEM_ALARM_OBJECT + ACCESS_ALLOWED_CALLBACK + ACCESS_DENIED_CALLBACK + ACCESS_ALLOWED_CALLBACK_OBJECT + ACCESS_DENIED_CALLBACK_OBJECT + SYSTEM_AUDIT_CALLBACK + SYSTEM_ALARM_CALLBACK + SYSTEM_AUDIT_CALLBACK_OBJECT + SYSTEM_ALARM_CALLBACK_OBJECT + SYSTEM_MANDATORY_LABEL +); + +sub _look_up_ace_type { + my $type = shift; + + if (exists $Types[$type]) { + return $Types[$type]; + } + else { + return ''; + } +} + +sub get_type { + return $_[0]->{_type}; +} + +sub get_type_as_string { + return _look_up_ace_type($_[0]->{_type}); +} + +sub get_flags { + return $_[0]->{_flags}; +} + +sub get_access_mask { + return $_[0]->{_mask}; +} + +sub get_trustee { + return $_[0]->{_trustee}; +} + +sub as_string { + my $self = shift; + + my $sid = $self->{_trustee}; + my $string = sprintf '%s 0x%02x 0x%08x %s', + _look_up_ace_type($self->{_type}), + $self->{_flags}, + $self->{_mask}, + $sid->as_string; + my $name = $sid->get_name; + $string .= " [$name]" if defined $name; + return $string; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + + +package Parse::Win32Registry::ACL; + +use Carp; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + # 0x00 byte = revision + # 0x01 + # 0x02 word = length + # 0x04 word = number of aces + # 0x06 + # 0x08 = first ace (variable length) + # ... = second ace (variable length) + # ... + + if (length($data) < 8) { + return; + } + + my ($rev, $acl_len, $num_aces) = unpack('Cxvv', $data); + + if (length($data) < $acl_len) { + return; + } + + my $pos = 8; + my @acl = (); + foreach (my $num = 0; $num < $num_aces; $num++) { + my $ace = Parse::Win32Registry::ACE->new(substr($data, $pos, + $acl_len - $pos)); + # Abandon acl if any single ace is undefined + return if !defined $ace; + push @acl, $ace; + $pos += $ace->get_length; + } + + # Abandon acl if not expected length, but don't use + # $pos != $acl_len as some acls contain unused space. + if ($pos > $acl_len) { + return; + } + + my $self = { + _acl => \@acl, + _length => $acl_len, + }; + bless $self, $class; + + return $self; +} + +sub get_list_of_aces { + my $self = shift; + + return @{$self->{_acl}}; +} + +sub as_string { + croak 'Usage: ACLs do not have an as_string method; use as_stanza instead'; +} + +sub as_stanza { + my $self = shift; + + my $stanza = ''; + foreach my $ace (@{$self->{_acl}}) { + $stanza .= 'ACE: '. $ace->as_string . "\n"; + } + return $stanza; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + + +package Parse::Win32Registry::SecurityDescriptor; + +use Carp; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + # Unpacks "self-relative" security descriptors + + # 0x00 word = revision + # 0x02 word = control flags + # 0x04 dword = offset to owner sid + # 0x08 dword = offset to group sid + # 0x0c dword = offset to sacl + # 0x10 dword = offset to dacl + + # Offsets are relative to the start of the security descriptor + + # Control Flags: + # SE_OWNER_DEFAULTED 0x0001 + # SE_GROUP_DEFAULTED 0x0002 + # SE_DACL_PRESENT 0x0004 + # SE_DACL_DEFAULTED 0x0008 + # SE_SACL_PRESENT 0x0010 + # SE_SACL_DEFAULTED 0x0020 + # SE_DACL_AUTO_INHERIT_REQ 0x0100 + # SE_SACL_AUTO_INHERIT_REQ 0x0200 + # SE_DACL_AUTO_INHERITED 0x0400 + # SE_SACL_AUTO_INHERITED 0x0800 + # SE_DACL_PROTECTED 0x1000 + # SE_SACL_PROTECTED 0x2000 + # SE_RM_CONTROL_VALID 0x4000 + # SE_SELF_RELATIVE 0x8000 + + if (length($data) < 20) { + return; + } + + my ($rev, + $flags, + $offset_to_owner, + $offset_to_group, + $offset_to_sacl, + $offset_to_dacl) = unpack('vvVVVV', $data); + + my %sd = (); + my $sd_len = 20; + + my $self = {}; + if ($offset_to_owner > 0 && $offset_to_owner < length($data)) { + my $owner = Parse::Win32Registry::SID->new(substr($data, + $offset_to_owner)); + return if !defined $owner; + $self->{_owner} = $owner; + if ($offset_to_owner + $owner->get_length > $sd_len) { + $sd_len = $offset_to_owner + $owner->get_length; + } + } + if ($offset_to_group > 0 && $offset_to_group < length($data)) { + my $group = Parse::Win32Registry::SID->new(substr($data, + $offset_to_group)); + return if !defined $group; + $self->{_group} = $group; + if ($offset_to_group + $group->get_length > $sd_len) { + $sd_len = $offset_to_group + $group->get_length; + } + } + if ($offset_to_sacl > 0 && $offset_to_sacl < length($data)) { + my $sacl = Parse::Win32Registry::ACL->new(substr($data, + $offset_to_sacl)); + return if !defined $sacl; + $self->{_sacl} = $sacl; + if ($offset_to_sacl + $sacl->get_length > $sd_len) { + $sd_len = $offset_to_sacl + $sacl->get_length; + } + } + if ($offset_to_dacl > 0 && $offset_to_dacl < length($data)) { + my $dacl = Parse::Win32Registry::ACL->new(substr($data, + $offset_to_dacl)); + return if !defined $dacl; + $self->{_dacl} = $dacl; + if ($offset_to_dacl + $dacl->get_length > $sd_len) { + $sd_len = $offset_to_dacl + $dacl->get_length; + } + } + $self->{_length} = $sd_len; + bless $self, $class; + + return $self; +} + +sub get_owner { + my $self = shift; + + return $self->{_owner}; +} + +sub get_group { + my $self = shift; + + return $self->{_group}; +} + +sub get_sacl { + my $self = shift; + + return $self->{_sacl}; +} + +sub get_dacl { + my $self = shift; + + return $self->{_dacl}; +} + +sub as_string { + croak 'Usage: Security Descriptors do not have an as_string method; use as_stanza instead'; +} + +sub as_stanza { + my $self = shift; + + my $stanza = ''; + if (defined(my $owner = $self->{_owner})) { + $stanza .= 'Owner SID: ' . $owner->as_string; + my $name = $owner->get_name; + $stanza .= " [$name]" if defined $name; + $stanza .= "\n"; + } + if (defined(my $group = $self->{_group})) { + $stanza .= 'Group SID: ' . $group->as_string; + my $name = $group->get_name; + $stanza .= " [$name]" if defined $name; + $stanza .= "\n"; + } + if (defined(my $sacl = $self->{_sacl})) { + foreach my $ace ($sacl->get_list_of_aces) { + $stanza .= 'SACL ACE: ' . $ace->as_string . "\n"; + } + } + if (defined(my $dacl = $self->{_dacl})) { + foreach my $ace ($dacl->get_list_of_aces) { + $stanza .= 'DACL ACE: ' . $ace->as_string . "\n"; + } + } + return $stanza; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/Entry.pm b/thirdparty/rr-full/Parse/Win32Registry/Entry.pm new file mode 100644 index 0000000000..1e14a801a2 --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/Entry.pm @@ -0,0 +1,151 @@ +package Parse::Win32Registry::Entry; + +use strict; +use warnings; + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +sub get_regfile { + my $self = shift; + + return $self->{_regfile}; +} + +sub get_offset { + my $self = shift; + + return $self->{_offset}; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + +sub is_allocated { + my $self = shift; + + return $self->{_allocated}; +} + +sub get_tag { + my $self = shift; + + return $self->{_tag}; +} + +sub as_string { + my $self = shift; + + my $tag = $self->{_tag}; + $tag = 'unidentified entry' if !defined $tag; + return "($tag)"; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x %s len=0x%x', + $self->{_offset}, + $self->{_tag}, + $self->{_length}; + + return $info; +} + +sub unparsed { + my $self = shift; + + return hexdump($self->get_raw_bytes, $self->get_offset); +} + +sub get_raw_bytes { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $fh = $regfile->get_filehandle; + my $offset = $self->{_offset}; + my $length = $self->{_length}; + + if (defined $self->{_header_length}) { + $length = $self->{_header_length}; + } + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $buffer, $length); + if ($bytes_read == $length) { + return $buffer; + } + else { + return ''; + } +} + +sub looks_like_key { + return UNIVERSAL::isa($_[0], "Parse::Win32Registry::Key"); +} + +sub looks_like_value { + return UNIVERSAL::isa($_[0], "Parse::Win32Registry::Value"); +} + +sub looks_like_security { + return UNIVERSAL::isa($_[0], "Parse::Win32Registry::WinNT::Security"); +} + +sub _dumpvar { + my $self = shift; + my $depth = shift || 1; + + my $dumpvar = ''; + foreach (sort keys %$self) { + $dumpvar .= ' ' x ($depth*2); + $dumpvar .= "$_ => "; + my $var = $self->{$_}; + if (!defined $var) { + $dumpvar .= "undef\n"; + } + elsif (/offset/ || /_id$/ || /^_unk/) { + $dumpvar .= sprintf "0x%x\n", $var; + } + elsif (/_flags$/) { + $dumpvar .= sprintf "0x%x (0b%b)\n", $var, $var; + } + elsif (/length/ || /bytes_used/) { + $dumpvar .= sprintf "0x%x (%d)\n", $var, $var; + } + elsif (/_data$/) { + if (length($var) == 0) { + $dumpvar .= '(no data)'; + } + else { + $dumpvar .= join(' ', unpack('(H2)20', $var)); + if (length($var) > 20) { + $dumpvar .= '...'; + } + } + $dumpvar .= "\n"; + } + elsif (/timestamp$/) { + $dumpvar .= $var . " (" . iso8601($var) . ")\n"; + } + elsif ($var =~ /^\d+$/) { + $dumpvar .= sprintf "%d\n", $var; + } + elsif (ref($var)) { + $dumpvar .= "$var\n"; # stringify object ref + } + else { + $dumpvar .= qq{"$var"}; + $dumpvar .= ' '; + $dumpvar .= Encode::is_utf8($var) ? "(UTF8)" : "(BYTES)"; + $dumpvar .= "\n"; + } + } + + return $dumpvar; +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/File.pm b/thirdparty/rr-full/Parse/Win32Registry/File.pm new file mode 100644 index 0000000000..26dd2973b8 --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/File.pm @@ -0,0 +1,66 @@ +package Parse::Win32Registry::File; + +use strict; +use warnings; + +sub get_filehandle { + my $self = shift; + + return $self->{_filehandle}; +} + +sub get_filename { + my $self = shift; + + return $self->{_filename}; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + +sub get_entry_iterator { + my $self = shift; + + my $entry_iter; + my $block_iter = $self->get_block_iterator; + + return Parse::Win32Registry::Iterator->new(sub { + while (1) { + if (defined $entry_iter) { + my $entry = $entry_iter->(); + if (defined $entry) { + return $entry; + } + } + # entry iterator is undefined or finished + my $block = $block_iter->(); + if (!defined $block) { + return; # block iterator finished + } + $entry_iter = $block->get_entry_iterator; + } + }); +} + +# method provided for backwards compatibility +sub move_to_first_entry { + my $self = shift; + + $self->{_entry_iter} = undef; +} + +# method provided for backwards compatibility +sub get_next_entry { + my $self = shift; + + my $entry_iter = $self->{_entry_iter}; + if (!defined $entry_iter) { + $self->{_entry_iter} = $entry_iter = $self->get_entry_iterator; + } + return $entry_iter->(); +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/Key.pm b/thirdparty/rr-full/Parse/Win32Registry/Key.pm new file mode 100644 index 0000000000..2422a8131d --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/Key.pm @@ -0,0 +1,245 @@ +package Parse::Win32Registry::Key; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; + +sub get_name { + my $self = shift; + + # the root key of a windows 95 registry has no defined name + # but this should be set to '' when created + return $self->{_name}; +} + +sub get_path { + my $self = shift; + + return $self->{_key_path}; +} + +sub _look_up_subkey { + my $self = shift; + my $subkey_name = shift; + + croak 'Missing subkey name' if !defined $subkey_name; + + foreach my $subkey ($self->get_list_of_subkeys) { + if (uc $subkey_name eq uc $subkey->{_name}) { + return $subkey; + } + } + return; +} + +sub get_subkey { + my $self = shift; + my $subkey_path = shift; + + # check for definedness in case key name is '' or '0' + croak "Usage: get_subkey('key name')" if !defined $subkey_path; + + my $key = $self; + + # Current path component separator is '\' to match that used in Windows. + # split returns nothing if it is given an empty string, + # and without a limit of -1 drops trailing empty fields. + # The following returns a list with a single zero-length string ("") + # for an empty string, as split(/\\/, $subkey_path, -1) returns (), + # an empty list. + my @path_components = index($subkey_path, "\\") == -1 + ? ($subkey_path) + : split(/\\/, $subkey_path, -1); + + my %offsets_seen = (); + $offsets_seen{$key->get_offset} = undef; + + foreach my $subkey_name (@path_components) { + if (my $subkey = $key->_look_up_subkey($subkey_name)) { + if (exists $offsets_seen{$subkey->get_offset}) { + return; # found loop + } + $key = $subkey; + $offsets_seen{$key->get_offset} = undef; + } + else { # subkey name not found, abort look up + return; + } + } + return $key; +} + +sub get_value { + my $self = shift; + my $value_name = shift; + + # check for definedness in case value name is '' or '0' + croak "Usage: get_value('value name')" if !defined $value_name; + + foreach my $value ($self->get_list_of_values) { + if (uc $value_name eq uc $value->{_name}) { + return $value; + } + } + return undef; +} + +sub print_summary { + my $self = shift; + + print $self->as_string, "\n"; +} + +sub as_regedit_export { + my $self = shift; + + return "[" . $self->{_key_path} . "]\n"; +} + +sub regenerate_path { + my $self = shift; + + # ascend to the root + my $key = $self; + my @key_names = ($key->get_name); + + my %offsets_seen = (); + while (!$key->is_root) { + $offsets_seen{$key->get_offset}++; + $key = $key->get_parent; + if (!defined $key) { # found an undefined parent key + unshift @key_names, '(Invalid Parent Key)'; + last; + } + if (exists $offsets_seen{$key->get_offset}) { # found loop + unshift @key_names, '(Invalid Parent Key)'; + last; + } + unshift @key_names, $key->get_name; + } + + my $key_path = join('\\', @key_names); + $self->{_key_path} = $key_path; + return $key_path; +} + +sub get_value_data { + my $self = shift; + my $value_name = shift; + + croak "Usage: get_value_data('value name')" if !defined $value_name; + + if (my $value = $self->get_value($value_name)) { + return $value->get_data; + } + return; +} + +sub get_mru_list_of_values { + my $self = shift; + + my @values = (); + + if (my $mrulist = $self->get_value('MRUList')) { + foreach my $ch (split(//, $mrulist->get_data)) { + if (my $value = $self->get_value($ch)) { + push @values, $value; + } + } + } + elsif (my $mrulistex = $self->get_value('MRUListEx')) { + foreach my $item (unpack('V*', $mrulistex->get_data)) { + last if $item == 0xffffffff; + if (my $value = $self->get_value($item)) { + push @values, $value; + } + } + } + return @values; +} + +sub get_list_of_subkeys { + my $self = shift; + + my $subkey_iter = $self->get_subkey_iterator; + my @subkeys; + while (my $subkey = $subkey_iter->()) { + push @subkeys, $subkey; + } + return @subkeys; +} + +sub get_list_of_values { + my $self = shift; + + my $value_iter = $self->get_value_iterator; + my @values; + while (my $value = $value_iter->()) { + push @values, $value; + } + return @values; +} + +sub get_subtree_iterator { + my $self = shift; + + my @start_keys = ($self); + push my (@subkey_iters), Parse::Win32Registry::Iterator->new(sub { + return shift @start_keys; + }); + my $value_iter; + my $key; # used to remember key while iterating values + + return Parse::Win32Registry::Iterator->new(sub { + if (defined $value_iter && wantarray) { + my $value = $value_iter->(); + if (defined $value) { + return ($key, $value); + } + # $value_iter finished, so fetch a new one + # from the (current) $subkey_iter[-1] + } + while (@subkey_iters > 0) { + $key = $subkey_iters[-1]->(); # depth-first + if (defined $key) { + push @subkey_iters, $key->get_subkey_iterator; + $value_iter = $key->get_value_iterator; + return $key; + } + pop @subkey_iters; # $subkey_iter finished, so remove it + } + return; + }); +} + +sub walk { + my $self = shift; + my $key_enter_func = shift; + my $value_func = shift; + my $key_leave_func = shift; + + if (!defined $key_enter_func && + !defined $value_func && + !defined $key_leave_func) { + $key_enter_func = sub { print "+ ", $_[0]->get_path, "\n"; }; + $value_func = sub { print " '", $_[0]->get_name, "'\n"; }; + $key_leave_func = sub { print "- ", $_[0]->get_path, "\n"; }; + } + + $key_enter_func->($self) if ref $key_enter_func eq 'CODE'; + + foreach my $value ($self->get_list_of_values) { + $value_func->($value) if ref $value_func eq 'CODE'; + } + + foreach my $subkey ($self->get_list_of_subkeys) { + $subkey->walk($key_enter_func, $value_func, $key_leave_func); + } + + $key_leave_func->($self) if ref $key_leave_func eq 'CODE'; +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/Value.pm b/thirdparty/rr-full/Parse/Win32Registry/Value.pm new file mode 100644 index 0000000000..de8587a2c6 --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/Value.pm @@ -0,0 +1,101 @@ +package Parse::Win32Registry::Value; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +sub get_name { + my $self = shift; + + return $self->{_name}; +} + +sub get_type { + my $self = shift; + + return $self->{_type}; +} + +our @Types = qw( + REG_NONE + REG_SZ + REG_EXPAND_SZ + REG_BINARY + REG_DWORD + REG_DWORD_BIG_ENDIAN + REG_LINK + REG_MULTI_SZ + REG_RESOURCE_LIST + REG_FULL_RESOURCE_DESCRIPTOR + REG_RESOURCE_REQUIREMENTS_LIST + REG_QWORD +); + +sub get_type_as_string { + my $self = shift; + + my $type = $self->get_type; + if (exists $Types[$type]) { + return $Types[$type]; + } + else { + # Return unrecognised types as REG_ + # REGEDIT displays them as formatted hex numbers, e.g. 0x1f4 + return "REG_$type"; + } +} + +sub get_data_as_string { + my $self = shift; + + my $type = $self->get_type; + my $data = $self->get_data; + if (!defined($data)) { + return '(invalid data)'; + } + elsif (length($data) == 0) { + return '(no data)'; + } + elsif ($type == REG_SZ || $type == REG_EXPAND_SZ) { + return $data; + } + elsif ($type == REG_MULTI_SZ) { + my @data = $self->get_data; + my $i = 0; + return join(' ', map { "[" . $i++ . "] $_" } @data); + } + elsif ($type == REG_DWORD || $type == REG_DWORD_BIG_ENDIAN) { + return sprintf '0x%08x (%u)', $data, $data; + } + else { + return join(' ', unpack('(H2)*', $data)); + } +} + +sub get_raw_data { + my $self = shift; + + return $self->{_data}; +} + +sub as_string { + my $self = shift; + + my $name = $self->get_name; + $name = '(Default)' if $name eq ''; + my $type_as_string = $self->get_type_as_string; + my $data_as_string = $self->get_data_as_string; + return "$name ($type_as_string) = $data_as_string"; +} + +sub print_summary { + my $self = shift; + + print $self->as_string, "\n"; +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/Win95/File.pm b/thirdparty/rr-full/Parse/Win32Registry/Win95/File.pm new file mode 100644 index 0000000000..6b713f2f98 --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/Win95/File.pm @@ -0,0 +1,540 @@ +package Parse::Win32Registry::Win95::File; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::File); + +use Carp; +use File::Basename; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::Win95::Key; + +use constant CREG_HEADER_LENGTH => 0x20; +use constant OFFSET_TO_RGKN_BLOCK => 0x20; + +sub new { + my $class = shift; + my $filename = shift or croak 'No filename specified'; + + open my $fh, '<', $filename or croak "Unable to open '$filename': $!"; + + # CREG Header + # 0x00 dword = 'CREG' signature + # 0x04 + # 0x08 dword = offset to first rgdb block + # 0x0c + # 0x10 word = number of rgdb blocks + + my $bytes_read = sysread($fh, my $creg_header, CREG_HEADER_LENGTH); + if ($bytes_read != CREG_HEADER_LENGTH) { + warnf('Could not read registry file header'); + return; + } + + my ($creg_sig, + $offset_to_first_rgdb_block, + $num_rgdb_blocks) = unpack('a4x4Vx4v', $creg_header); + + if ($creg_sig ne 'CREG') { + warnf('Invalid registry file signature'); + return; + } + + my $self = {}; + $self->{_filehandle} = $fh; + $self->{_filename} = $filename; + $self->{_length} = (stat $fh)[7]; + $self->{_offset_to_first_rgdb_block} = $offset_to_first_rgdb_block; + $self->{_num_rgdb_blocks} = $num_rgdb_blocks; + bless $self, $class; + + # get_rgkn will cache the rgkn block for subsequent calls + my $rgkn_block = $self->get_rgkn; + return if !defined $rgkn_block; # warning will already have been made + + # Index the rgdb entries by id for faster look up + $self->_index_rgdb_entries; + + return $self; +} + +sub get_timestamp { + return undef; +} + +sub get_timestamp_as_string { + return iso8601(undef); +} + +sub get_embedded_filename { + return undef; +} + +sub get_root_key { + my $self = shift; + + return $self->get_rgkn->get_root_key; +} + +sub get_virtual_root_key { + my $self = shift; + my $fake_root = shift; + + my $root_key = $self->get_root_key; + return if !defined $root_key; + + if (!defined $fake_root) { + # guess virtual root from filename + my $filename = basename $self->{_filename}; + + if ($filename =~ /USER/i) { + $fake_root = 'HKEY_USERS'; + } + elsif ($filename =~ /SYSTEM/i) { + $fake_root = 'HKEY_LOCAL_MACHINE'; + } + else { + $fake_root = 'HKEY_UNKNOWN'; + } + } + + $root_key->{_name} = $fake_root; + $root_key->{_key_path} = $fake_root; + + return $root_key; +} + +sub _index_rgdb_entries { + my $self = shift; + + my %index = (); + + # Build index of rgdb key entries + # Entries are only included if $key_block_num matches $rgdb_block_num + my $rgdb_block_num = 0; + my $rgdb_iter = $self->get_rgdb_iterator; + while (my $rgdb = $rgdb_iter->()) { + my $rgdb_key_iter = $rgdb->get_key_iterator; + while (my $rgdb_key = $rgdb_key_iter->()) { + my $key_id = $rgdb_key->{_id}; + my $key_block_num = $key_id >> 16; + if ($rgdb_block_num == $key_block_num) { + $index{$key_id} = $rgdb_key; + } + } + $rgdb_block_num++; + } + + $self->{_rgdb_index} = \%index; +} + +sub _dump_rgdb_index { + my $self = shift; + + my $rgdb_index = $self->{_rgdb_index}; + + foreach my $key_id (sort { $a <=> $b } keys %$rgdb_index) { + my $rgdb_key = $rgdb_index->{$key_id}; + printf qq{id=0x%x 0x%x,%d/%d "%s" vals=%d\n}, + $key_id, + $rgdb_key->{_offset}, + $rgdb_key->{_length_used}, + $rgdb_key->{_length}, + $rgdb_key->{_name}, + $rgdb_key->{_num_values}; + } +} + +sub get_rgkn { + my $self = shift; + + # Return cached rgkn block if present + if (defined $self->{_rgkn}) { + return $self->{_rgkn}; + } + + my $offset = OFFSET_TO_RGKN_BLOCK; + my $rgkn_block = Parse::Win32Registry::Win95::RGKN->new($self, $offset); + $self->{_rgkn} = $rgkn_block; + return $rgkn_block; +} + +sub get_rgdb_iterator { + my $self = shift; + + my $offset_to_next_rgdb_block = $self->{_offset_to_first_rgdb_block}; + my $num_rgdb_blocks = $self->{_num_rgdb_blocks}; + + my $end_of_file = $self->{_length}; + + my $rgdb_block_num = 0; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_rgdb_block > $end_of_file) { + return; # no more rgdb blocks + } + if ($rgdb_block_num >= $num_rgdb_blocks) { + return; # no more rgdb blocks + } + $rgdb_block_num++; + if (my $rgdb_block = Parse::Win32Registry::Win95::RGDB->new($self, + $offset_to_next_rgdb_block)) + { + return unless $rgdb_block->get_length > 0; + $offset_to_next_rgdb_block += $rgdb_block->get_length; + return $rgdb_block; + } + }); +} + +sub get_block_iterator { + my $self = shift; + + my $rgdb_iter; + + return Parse::Win32Registry::Iterator->new(sub { + if (!defined $rgdb_iter) { + $rgdb_iter = $self->get_rgdb_iterator; + return $self->get_rgkn; + } + return $rgdb_iter->(); + }); +} + +*get_hbin_iterator = \&get_block_iterator; + + +package Parse::Win32Registry::Win95::RGKN; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +use constant RGKN_HEADER_LENGTH => 0x20; +use constant OFFSET_TO_RGKN_BLOCK => 0x20; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift || OFFSET_TO_RGKN_BLOCK; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGKN Block Header + # 0x0 dword = 'RGKN' signature + # 0x4 dword = length of rgkn block + # 0x8 dword = offset to root key entry (relative to start of rgkn block) + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgkn_header, RGKN_HEADER_LENGTH); + if ($bytes_read != RGKN_HEADER_LENGTH) { + warnf('Could not read RGKN header at 0x%x', $offset); + return; + } + + my ($sig, + $rgkn_block_length, + $offset_to_root_key) = unpack('a4VV', $rgkn_header); + + if ($sig ne 'RGKN') { + warnf('Invalid RGKN block signature at 0x%x', $offset); + return; + } + + $offset_to_root_key += $offset; + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $rgkn_block_length; + $self->{_header_length} = RGKN_HEADER_LENGTH; + $self->{_allocated} = 1; + $self->{_tag} = 'rgkn block'; + $self->{_offset_to_root_key} = $offset_to_root_key; + bless $self, $class; + + return $self; +} + +sub get_root_key { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset_to_root_key = $self->{_offset_to_root_key}; + + my $root_key = Parse::Win32Registry::Win95::Key->new($regfile, + $offset_to_root_key); + return $root_key; +} + +sub get_entry_iterator { + my $self = shift; + + my $root_key = $self->get_root_key; + + # In the unlikely event there is no root key, return an empty iterator + if (defined $root_key) { + return $root_key->get_subtree_iterator; + } + else { + return Parse::Win32Registry::Iterator->new(sub {}); + } +} + + +package Parse::Win32Registry::Win95::RGDB; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +use constant RGDB_HEADER_LENGTH => 0x20; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGDB Block Header + # 0x0 dword = 'RDGB' signature + # 0x4 dword = length of rgdb block + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgdb_header, RGDB_HEADER_LENGTH); + if ($bytes_read != RGDB_HEADER_LENGTH) { + return; + } + + my ($sig, + $rgdb_block_length) = unpack('a4V', $rgdb_header); + + if ($sig ne 'RGDB') { + return; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $rgdb_block_length; + $self->{_header_length} = RGDB_HEADER_LENGTH; + $self->{_allocated} = 1; + $self->{_tag} = 'rgdb block'; + bless $self, $class; + + return $self; +} + +sub get_key_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset = $self->{_offset}; + my $length = $self->{_length}; + + my $offset_to_next_rgdb_key = $offset + RGDB_HEADER_LENGTH; + my $end_of_rgdb_block = $offset + $length; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_rgdb_key >= $end_of_rgdb_block) { + return; + } + if (my $rgdb_key = Parse::Win32Registry::Win95::RGDBKey->new($regfile, + $offset_to_next_rgdb_key)) + { + return unless $rgdb_key->get_length > 0; + $offset_to_next_rgdb_key += $rgdb_key->get_length; + + # Check rgdb key has not run past end of rgdb block + if ($offset_to_next_rgdb_key > $end_of_rgdb_block) { + return; + } + return $rgdb_key; + } + }); +} + +sub get_entry_iterator { + my $self = shift; + + my $value_iter; + my $key_iter = $self->get_key_iterator; + + return Parse::Win32Registry::Iterator->new(sub { + if (defined $value_iter) { + my $value = $value_iter->(); + if (defined $value) { + return $value; + } + } + + my $key = $key_iter->(); + if (!defined $key) { + return; # key iterator finished + } + + $value_iter = $key->get_value_iterator; + return $key; + }); +} + + +package Parse::Win32Registry::Win95::RGDBKey; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); + +use constant RGDB_ENTRY_HEADER_LENGTH => 0x14; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGDB Key Entry + # 0x00 dword = length of rgdb entry / offset to next rgdb entry + # (this length includes any following value entries) + # 0x04 dword = id (top word = block num, bottom word = id) + # 0x08 dword = bytes used (unpacked, but not used) + # 0x0c word = key name length + # 0x0e word = number of values + # 0x10 dword + # 0x14 = key name [for key name length bytes] + # followed immediately by any RGDB Value Entries belonging to this key + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgdb_key_entry, RGDB_ENTRY_HEADER_LENGTH); + if ($bytes_read != RGDB_ENTRY_HEADER_LENGTH) { + return; + } + + my ($length, + $key_id, + $length_used, + $name_length, + $num_values) = unpack('VVVvv', $rgdb_key_entry); + + $bytes_read = sysread($fh, my $name, $name_length); + if ($bytes_read != $name_length) { + return; + } + $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name); + + # Calculate the length of the entry's key header + my $header_length = RGDB_ENTRY_HEADER_LENGTH + $name_length; + + # Check for invalid/unused entries + if ($key_id == 0xffffffff || $length_used == 0xffffffff + || $header_length > $length) + { + $name = ''; + $header_length = RGDB_ENTRY_HEADER_LENGTH; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_length_used} = $length_used; + $self->{_header_length} = $header_length; + $self->{_allocated} = 1; + $self->{_tag} = 'rgdb key'; + $self->{_id} = $key_id; + $self->{_name} = $name; + $self->{_name_length} = $name_length; + $self->{_num_values} = $num_values; + bless $self, $class; + + return $self; +} + +sub get_name { + my $self = shift; + + return $self->{_name}; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x rgdb key len=0x%x/0x%x "%s" id=0x%x vals=%d', + $self->{_offset}, + $self->{_length_used}, + $self->{_length}, + $self->{_name}, + $self->{_id}, + $self->{_num_values}; + + return $info; +} + +sub get_value_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + + my $num_values_remaining = $self->{_num_values}; + + my $offset = $self->{_offset}; + + # offset_to_next_rgdb_value can only be set to a valid offset + # if num_values_remaining > 0 + my $offset_to_next_rgdb_value = 0xffffffff; + if ($num_values_remaining > 0) { + $offset_to_next_rgdb_value = $offset + + $self->{_header_length}; + } + + my $end_of_rgdb_key = $offset + $self->{_length}; + + # don't attempt to return values if id is invalid... + if ($self->{_id} == 0xffffffff) { + $num_values_remaining = 0; + } + + return Parse::Win32Registry::Iterator->new(sub { + if ($num_values_remaining-- <= 0) { + return; + } + if ($offset_to_next_rgdb_value == 0xffffffff) { + return; + } + if ($offset_to_next_rgdb_value > $end_of_rgdb_key) { + return; + } + if (my $value = Parse::Win32Registry::Win95::Value->new($regfile, + $offset_to_next_rgdb_value)) + { + return unless $value->get_length > 0; + $offset_to_next_rgdb_value += $value->get_length; + return $value; + } + else { + return; # no more values + } + }); +} + +1; + diff --git a/thirdparty/rr-full/Parse/Win32Registry/Win95/Key.pm b/thirdparty/rr-full/Parse/Win32Registry/Win95/Key.pm new file mode 100644 index 0000000000..6bff102198 --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/Win95/Key.pm @@ -0,0 +1,207 @@ +package Parse::Win32Registry::Win95::Key; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Key); + +use Carp; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::Win95::Value; + +use constant RGKN_ENTRY_LENGTH => 0x1c; +use constant OFFSET_TO_RGKN_BLOCK => 0x20; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to RGKN key entry relative to start of RGKN + my $parent_key_path = shift; # parent key path (optional) + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGKN Key Entry + # 0x00 dword + # 0x04 dword + # 0x08 dword + # 0x0c dword = offset to parent RGKN entry + # 0x10 dword = offset to first child RGKN entry + # 0x14 dword = offset to next sibling RGKN entry + # 0x18 dword = entry id of RGDB entry + + # Extracted offsets are relative to the start of the RGKN block + + # Any offset of 0xffffffff marks the end of a list. + # An entry id of 0xffffffff means the RGKN entry has no RGDB entry. + # This occurs for the root key of the registry file. + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgkn_entry, RGKN_ENTRY_LENGTH); + if ($bytes_read != RGKN_ENTRY_LENGTH) { + warnf('Could not read RGKN key at 0x%x', $offset); + return; + } + + my ($offset_to_parent, + $offset_to_first_child, + $offset_to_next_sibling, + $key_id) = unpack('x12VVVV', $rgkn_entry); + + $offset_to_parent += OFFSET_TO_RGKN_BLOCK + if $offset_to_parent != 0xffffffff; + $offset_to_first_child += OFFSET_TO_RGKN_BLOCK + if $offset_to_first_child != 0xffffffff; + $offset_to_next_sibling += OFFSET_TO_RGKN_BLOCK + if $offset_to_next_sibling != 0xffffffff; + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = RGKN_ENTRY_LENGTH; + $self->{_allocated} = 1; + $self->{_tag} = 'rgkn key'; + $self->{_offset_to_parent} = $offset_to_parent; + $self->{_offset_to_first_child} = $offset_to_first_child; + $self->{_offset_to_next_sibling} = $offset_to_next_sibling; + $self->{_id} = $key_id; + bless $self, $class; + + # Look up corresponding rgdb entry + my $index = $regfile->{_rgdb_index}; + croak 'Missing rgdb index' if !defined $index; + if (exists $index->{$key_id}) { + my $rgdb_key = $index->{$key_id}; + $self->{_rgdb_key} = $rgdb_key; + $self->{_name} = $rgdb_key->get_name; + } + else { + $self->{_name} = ''; + # Only the root key should have no matching RGDB entry + if (!$self->is_root) { + warnf('Could not find RGDB entry for RGKN key at 0x%x', $offset); + } + } + + my $name = $self->{_name}; + $self->{_key_path} = defined($parent_key_path) + ? "$parent_key_path\\$name" + : $name; + + return $self; +} + +sub get_timestamp { + return undef; +} + +sub get_timestamp_as_string { + return iso8601(undef); +} + +sub get_class_name { + return undef; +} + +sub is_root { + my $self = shift; + + my $offset = $self->{_offset}; + my $regfile = $self->{_regfile}; + + my $rgkn_block = $regfile->get_rgkn; + my $offset_to_root_key = $rgkn_block->{_offset_to_root_key}; + + # This gives better results than checking id == 0xffffffff + return $offset == $offset_to_root_key; +} + +sub get_parent { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset_to_parent = $self->{_offset_to_parent}; + my $key_path = $self->{_key_path}; + + return if $self->is_root; + + my $grandparent_key_path; + my @keys = split(/\\/, $key_path, -1); + if (@keys > 2) { + $grandparent_key_path = join("\\", @keys[0..$#keys-2]); + } + + return Parse::Win32Registry::Win95::Key->new($regfile, + $offset_to_parent, + $grandparent_key_path); +} + +sub get_security { + return undef; +} + +sub as_string { + my $self = shift; + + return $self->get_path; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x rgkn key len=0x%x par=0x%x,child=0x%x,next=0x%x id=0x%x', + $self->{_offset}, + $self->{_length}, + $self->{_offset_to_parent}, + $self->{_offset_to_first_child}, + $self->{_offset_to_next_sibling}, + $self->{_id}; + return $info; +} + +sub get_subkey_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $key_path = $self->{_key_path}; + + my $offset_to_next_key = $self->{_offset_to_first_child}; + + my $end_of_file = $regfile->get_length; + my $rgkn_block = $regfile->get_rgkn; + my $end_of_rgkn_block = $rgkn_block->get_offset + $rgkn_block->get_length; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_key == 0xffffffff) { + return; # no more subkeys + } + if ($offset_to_next_key > $end_of_rgkn_block) { + return; + } + if (my $key = Parse::Win32Registry::Win95::Key->new($regfile, + $offset_to_next_key, $key_path)) + { + $offset_to_next_key = $key->{_offset_to_next_sibling}; + return $key; + } + else { + return; # no more subkeys + } + }); +} + +sub get_value_iterator { + my $self = shift; + + my $rgdb_key = $self->{_rgdb_key}; + if (defined $rgdb_key) { + return $rgdb_key->get_value_iterator; + } + else { + return Parse::Win32Registry::Iterator->new(sub {}); + } +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/Win95/Value.pm b/thirdparty/rr-full/Parse/Win32Registry/Win95/Value.pm new file mode 100644 index 0000000000..baffd13b59 --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/Win95/Value.pm @@ -0,0 +1,177 @@ +package Parse::Win32Registry::Win95::Value; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Value); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); + +use constant RGDB_VALUE_HEADER_LENGTH => 0xc; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to RGDB value entry + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGDB Value Entry + # 0x00 dword = value type + # 0x04 + # 0x08 word = value name length + # 0x0a word = value data length + # 0x0c = value name [for name length bytes] + # + value data [for data length bytes] + # Value type may just be a word, not a dword; + # following word always appears to be zero. + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgdb_value_entry, + RGDB_VALUE_HEADER_LENGTH); + if ($bytes_read != RGDB_VALUE_HEADER_LENGTH) { + warnf('Could not read RGDB value at 0x%x', $offset); + return; + } + + my ($type, + $name_length, + $data_length) = unpack('Vx4vv', $rgdb_value_entry); + + $bytes_read = sysread($fh, my $name, $name_length); + if ($bytes_read != $name_length) { + warnf('Could not read name for RGDB value at 0x%x', $offset); + return; + } + $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name); + + $bytes_read = sysread($fh, my $data, $data_length); + if ($bytes_read != $data_length) { + warnf('Could not read data for RGDB value at 0x%x', $offset); + return; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = RGDB_VALUE_HEADER_LENGTH + $name_length + $data_length; + $self->{_allocated} = 1; + $self->{_tag} = 'rgdb value'; + $self->{_name} = $name; + $self->{_name_length} = $name_length; + $self->{_type} = $type; + $self->{_data} = $data; + $self->{_data_length} = $data_length; + bless $self, $class; + + return $self; +} + +sub get_data { + my $self = shift; + + my $type = $self->get_type; + + my $data = $self->{_data}; + return if !defined $data; # actually, Win95 value data is always defined + + # apply decoding to appropriate data types + if ($type == REG_DWORD) { + if (length($data) == 4) { + $data = unpack('V', $data); + } + else { + # incorrect length for dword data + $data = undef; + } + } + elsif ($type == REG_DWORD_BIG_ENDIAN) { + if (length($data) == 4) { + $data = unpack('N', $data); + } + else { + # incorrect length for dword data + $data = undef; + } + } + elsif ($type == REG_SZ || $type == REG_EXPAND_SZ) { + # Snip off any terminating null. + # Typically, REG_SZ values will not have a terminating null, + # while REG_EXPAND_SZ values will have a terminating null + chop $data if substr($data, -1, 1) eq "\0"; + } + elsif ($type == REG_MULTI_SZ) { + # Snip off any terminating nulls + chop $data if substr($data, -1, 1) eq "\0"; + chop $data if substr($data, -1, 1) eq "\0"; + my @multi_sz = split("\0", $data, -1); + # Make sure there is at least one empty string + @multi_sz = ('') if @multi_sz == 0; + return wantarray ? @multi_sz : join($", @multi_sz); + } + + return $data; +} + +sub as_regedit_export { + my $self = shift; + my $version = shift || 5; + + my $name = $self->get_name; + my $export = $name eq '' ? '@=' : '"' . $name . '"='; + + my $type = $self->get_type; + + # XXX +# if (!defined $self->{_data}) { +# $name = $name eq '' ? '@' : qq{"$name"}; +# return qq{; $name=(invalid data)\n}; +# } + + if ($type == REG_SZ) { + $export .= '"' . $self->get_data . '"'; + $export .= "\n"; + } + elsif ($type == REG_BINARY) { + $export .= 'hex:'; + $export .= format_octets($self->{_data}, length($export)); + } + elsif ($type == REG_DWORD) { + my $data = $self->get_data; + $export .= defined($data) + ? sprintf("dword:%08x", $data) + : "dword:"; + $export .= "\n"; + } + elsif ($type == REG_EXPAND_SZ || $type == REG_MULTI_SZ) { + my $data = $version == 4 + ? $self->{_data} # raw data + : encode("UCS-2LE", $self->{_data}); # ansi->unicode + $export .= sprintf("hex(%x):", $type); + $export .= format_octets($data, length($export)); + } + else { + $export .= sprintf("hex(%x):", $type); + $export .= format_octets($self->{_data}, length($export)); + } + return $export; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x rgdb value len=0x%x "%s" type=%d data,len=0x%x', + $self->{_offset}, + $self->{_length}, + $self->{_name}, + $self->{_type}, + $self->{_data_length}; + return $info; +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/WinNT/Entry.pm b/thirdparty/rr-full/Parse/Win32Registry/WinNT/Entry.pm new file mode 100644 index 0000000000..545773838c --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/WinNT/Entry.pm @@ -0,0 +1,109 @@ +package Parse::Win32Registry::WinNT::Entry; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::WinNT::Key; +use Parse::Win32Registry::WinNT::Value; +use Parse::Win32Registry::WinNT::Security; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $entry_header, 8); + if ($bytes_read != 8) { + return; + } + + my ($length, + $tag) = unpack('Va2', $entry_header); + + my $allocated = 0; + if ($length > 0x7fffffff) { + $allocated = 1; + $length = (0xffffffff - $length) + 1; + } + + $tag = '' if $tag !~ /(nk|vk|lh|lf|li|ri|sk)/; + + if ($tag eq 'nk') { + if (my $key = Parse::Win32Registry::WinNT::Key->new($regfile, + $offset)) + { + $key->regenerate_path; + return $key; + } + } + elsif ($tag eq 'vk') { + if (my $value = Parse::Win32Registry::WinNT::Value->new($regfile, + $offset)) + { + return $value; + } + } + elsif ($tag eq 'sk') { + if (my $value = Parse::Win32Registry::WinNT::Security->new($regfile, + $offset)) + { + return $value; + } + } + + my $self = {}; + $self->{_regfile} = $regfile, + $self->{_offset} = $offset, + $self->{_length} = $length, + $self->{_tag} = $tag, + $self->{_allocated} = $allocated, + bless $self, $class; + + return $self; +} + +sub as_string { + my $self = shift; + + my $tag = $self->{_tag}; + if ($tag eq 'nk') { + return '(key entry)'; + } + elsif ($tag eq 'vk') { + return '(value entry)'; + } + elsif ($tag eq 'sk') { + return '(security entry)'; + } + elsif ($tag =~ /(lh|lf|li|ri)/) { + return '(subkey list entry)'; + } + return '(unidentified entry)'; +} + +sub parse_info { + my $self = shift; + + my $tag = $self->{_tag}; + $tag = defined($tag) && $tag ne '' + ? $tag . ' ' + : '.. '; + my $info = sprintf '0x%x %slen=0x%x alloc=%d', + $self->{_offset}, + $tag, + $self->{_length}, + $self->{_allocated}; + return $info; +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/WinNT/File.pm b/thirdparty/rr-full/Parse/Win32Registry/WinNT/File.pm new file mode 100644 index 0000000000..97a859b206 --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/WinNT/File.pm @@ -0,0 +1,297 @@ +package Parse::Win32Registry::WinNT::File; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::File); + +use Carp; +use Encode; +use File::Basename; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::WinNT::Key; + +use constant REGF_HEADER_LENGTH => 0x200; +use constant OFFSET_TO_FIRST_HBIN => 0x1000; + +sub new { + my $class = shift; + my $filename = shift or croak "No filename specified"; + + open my $fh, '<', $filename or croak "Unable to open '$filename': $!"; + + # 0x00 dword = 'regf' signature + # 0x04 dword = seq1 + # 0x08 dword = seq2 + # 0x0c qword = timestamp + # 0x14 dword = major version + # 0x18 dword = minor version + # 0x1c dword = type (0 = registry file, 1 = log file) + # 0x20 dword = (1) + # 0x24 dword = offset to root key + # 0x28 dword = total length of all hbins (excludes header) + # 0x2c dword = (1) + # 0x30 = embedded filename + + # Extracted offsets are always relative to first hbin + + my $bytes_read = sysread($fh, my $regf_header, REGF_HEADER_LENGTH); + if ($bytes_read != REGF_HEADER_LENGTH) { + warnf('Could not read registry file header'); + return; + } + + my ($regf_sig, + $seq1, + $seq2, + $timestamp, + $major_version, + $minor_version, + $type, + $offset_to_root_key, + $total_hbin_length, + $embedded_filename, + ) = unpack('a4VVa8VVVx4VVx4a64', $regf_header); + + $offset_to_root_key += OFFSET_TO_FIRST_HBIN; + + if ($regf_sig ne 'regf') { + warnf('Invalid registry file signature'); + return; + } + + $embedded_filename = unpack('Z*', decode('UCS-2LE', $embedded_filename)); + + # The header checksum is the xor of the first 127 dwords. + # The checksum is stored in the 128th dword, at offset 0x1fc (508). + my $checksum = 0; + foreach my $x (unpack('V127', $regf_header)) { + $checksum ^= $x; + } + my $embedded_checksum = unpack('x508V', $regf_header); + if ($checksum != $embedded_checksum) { + warnf('Invalid checksum for registry file header'); + } + + my $self = {}; + $self->{_filehandle} = $fh; + $self->{_filename} = $filename; + $self->{_length} = (stat $fh)[7]; + $self->{_offset_to_root_key} = $offset_to_root_key; + $self->{_timestamp} = unpack_windows_time($timestamp); + $self->{_embedded_filename} = $embedded_filename; + $self->{_seq1} = $seq1; + $self->{_seq2} = $seq2; + $self->{_version} = "$major_version.$minor_version"; + $self->{_type} = $type; + $self->{_total_hbin_length} = $total_hbin_length; + $self->{_embedded_checksum} = $embedded_checksum; + $self->{_security_cache} = {}; # comment out to disable cache + bless $self, $class; + + return $self; +} + +sub get_root_key { + my $self = shift; + + my $offset_to_root_key = $self->{_offset_to_root_key}; + + my $root_key = Parse::Win32Registry::WinNT::Key->new($self, + $offset_to_root_key); + return $root_key; +} + +sub get_virtual_root_key { + my $self = shift; + my $fake_root = shift; + + my $root_key = $self->get_root_key; + return if !defined $root_key; + + if (!defined $fake_root) { + # guess virtual root from filename + my $filename = basename $self->{_filename}; + + if ($filename =~ /NTUSER/i) { + $fake_root = 'HKEY_CURRENT_USER'; + } + elsif ($filename =~ /USRCLASS/i) { + $fake_root = 'HKEY_CLASSES_ROOT'; + } + elsif ($filename =~ /SOFTWARE/i) { + $fake_root = 'HKEY_LOCAL_MACHINE\SOFTWARE'; + } + elsif ($filename =~ /SYSTEM/i) { + $fake_root = 'HKEY_LOCAL_MACHINE\SYSTEM'; + } + elsif ($filename =~ /SAM/i) { + $fake_root = 'HKEY_LOCAL_MACHINE\SAM'; + } + elsif ($filename =~ /SECURITY/i) { + $fake_root = 'HKEY_LOCAL_MACHINE\SECURITY'; + } + else { + $fake_root = 'HKEY_UNKNOWN'; + } + } + + $root_key->{_name} = $fake_root; + $root_key->{_key_path} = $fake_root; + + return $root_key; +} + +sub get_timestamp { + my $self = shift; + + return $self->{_timestamp}; +} + +sub get_timestamp_as_string { + my $self = shift; + + return iso8601($self->{_timestamp}); +} + +sub get_embedded_filename { + my $self = shift; + + return $self->{_embedded_filename}; +} + +sub get_block_iterator { + my $self = shift; + + my $offset_to_next_hbin = OFFSET_TO_FIRST_HBIN; + my $end_of_file = $self->{_length}; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_hbin > $end_of_file) { + return; # no more hbins + } + if (my $hbin = Parse::Win32Registry::WinNT::Hbin->new($self, + $offset_to_next_hbin)) + { + return unless $hbin->get_length > 0; + $offset_to_next_hbin += $hbin->get_length; + return $hbin; + } + else { + return; # no more hbins + } + }); +} + +*get_hbin_iterator = \&get_block_iterator; + +sub _dump_security_cache { + my $self = shift; + + if (defined(my $cache = $self->{_security_cache})) { + foreach my $offset (sort { $a <=> $b } keys %$cache) { + my $security = $cache->{$offset}; + printf '0x%x %s\n', $offset, $security->as_string; + } + } +} + + +package Parse::Win32Registry::WinNT::Hbin; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::WinNT::Entry; + +use constant HBIN_HEADER_LENGTH => 0x20; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # 0x00 dword = 'hbin' signature + # 0x04 dword = offset from first hbin to this hbin + # 0x08 dword = length of this hbin / relative offset to next hbin + # 0x14 qword = timestamp (first hbin only) + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $hbin_header, HBIN_HEADER_LENGTH); + if ($bytes_read != HBIN_HEADER_LENGTH) { + return; + } + + my ($sig, + $offset_to_hbin, + $length, + $timestamp) = unpack('a4VVx8a8x4', $hbin_header); + + if ($sig ne 'hbin') { + return; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_header_length} = HBIN_HEADER_LENGTH; + $self->{_allocated} = 1; + $self->{_tag} = $sig; + $self->{_timestamp} = unpack_windows_time($timestamp); + bless $self, $class; + + return $self; +} + +sub get_timestamp { + my $self = shift; + + return $self->{_timestamp}; +} + +sub get_timestamp_as_string { + my $self = shift; + + return iso8601($self->{_timestamp}); +} + +sub get_entry_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset = $self->{_offset}; + my $length = $self->{_length}; + + my $offset_to_next_entry = $offset + HBIN_HEADER_LENGTH; + my $end_of_hbin = $offset + $length; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_entry >= $end_of_hbin) { + return; # no more entries + } + if (my $entry = Parse::Win32Registry::WinNT::Entry->new($regfile, + $offset_to_next_entry)) + { + return unless $entry->get_length > 0; + $offset_to_next_entry += $entry->get_length; + return $entry; + } + else { + return; # no more entries + } + }); +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/WinNT/Key.pm b/thirdparty/rr-full/Parse/Win32Registry/WinNT/Key.pm new file mode 100644 index 0000000000..d362086586 --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/WinNT/Key.pm @@ -0,0 +1,444 @@ +package Parse::Win32Registry::WinNT::Key; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Key); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::WinNT::Value; +use Parse::Win32Registry::WinNT::Security; + +use constant NK_HEADER_LENGTH => 0x50; +use constant OFFSET_TO_FIRST_HBIN => 0x1000; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to nk record relative to start of file + my $parent_key_path = shift; # parent key path (optional) + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # 0x00 dword = key length (negative = allocated) + # 0x04 word = 'nk' signature + # 0x06 word = flags + # 0x08 qword = timestamp + # 0x10 + # 0x14 dword = offset to parent + # 0x18 dword = number of subkeys + # 0x1c + # 0x20 dword = offset to subkey list (lf, lh, ri, li) + # 0x24 + # 0x28 dword = number of values + # 0x2c dword = offset to value list + # 0x30 dword = offset to security + # 0x34 dword = offset to class name + # 0x38 dword = max subkey name length + # 0x3c dword = max class name length + # 0x40 dword = max value name length + # 0x44 dword = max value data length + # 0x48 + # 0x4c word = key name length + # 0x4e word = class name length + # 0x50 = key name [for key name length bytes] + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $nk_header, NK_HEADER_LENGTH); + if ($bytes_read != NK_HEADER_LENGTH) { + warnf('Could not read key at 0x%x', $offset); + return; + } + + my ($length, + $sig, + $flags, + $timestamp, + $offset_to_parent, + $num_subkeys, + $offset_to_subkey_list, + $num_values, + $offset_to_value_list, + $offset_to_security, + $offset_to_class_name, + $name_length, + $class_name_length, + ) = unpack('Va2va8x4VVx4Vx4VVVVx20vv', $nk_header); + + $offset_to_parent += OFFSET_TO_FIRST_HBIN + if $offset_to_parent != 0xffffffff; + $offset_to_subkey_list += OFFSET_TO_FIRST_HBIN + if $offset_to_subkey_list != 0xffffffff; + $offset_to_value_list += OFFSET_TO_FIRST_HBIN + if $offset_to_value_list != 0xffffffff; + $offset_to_security += OFFSET_TO_FIRST_HBIN + if $offset_to_security != 0xffffffff; + $offset_to_class_name += OFFSET_TO_FIRST_HBIN + if $offset_to_class_name != 0xffffffff; + + my $allocated = 0; + if ($length > 0x7fffffff) { + $allocated = 1; + $length = (0xffffffff - $length) + 1; + } + # allocated should be true + + if ($length < NK_HEADER_LENGTH) { + warnf('Invalid value entry length at 0x%x', $offset); + return; + } + + if ($sig ne 'nk') { + warnf('Invalid signature for key at 0x%x', $offset); + return; + } + + $bytes_read = sysread($fh, my $name, $name_length); + if ($bytes_read != $name_length) { + warnf('Could not read name for key at 0x%x', $offset); + return; + } + + if ($flags & 0x20) { + $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name); + } + else { + $name = decode('UCS-2LE', $name); + } + + my $key_path = (defined $parent_key_path) + ? "$parent_key_path\\$name" + : "$name"; + + my $class_name; + if ($offset_to_class_name != 0xffffffff) { + sysseek($fh, $offset_to_class_name + 4, 0); + $bytes_read = sysread($fh, $class_name, $class_name_length); + if ($bytes_read != $class_name_length) { + warnf('Could not read class name at 0x%x', $offset_to_class_name); + $class_name = undef; + } + else { + $class_name = decode('UCS-2LE', $class_name); + } + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_allocated} = $allocated; + $self->{_tag} = $sig; + $self->{_name} = $name; + $self->{_name_length} = $name_length; + $self->{_key_path} = $key_path; + $self->{_flags} = $flags; + $self->{_offset_to_parent} = $offset_to_parent; + $self->{_num_subkeys} = $num_subkeys; + $self->{_offset_to_subkey_list} = $offset_to_subkey_list; + $self->{_num_values} = $num_values; + $self->{_offset_to_value_list} = $offset_to_value_list; + $self->{_timestamp} = unpack_windows_time($timestamp); + $self->{_offset_to_security} = $offset_to_security; + $self->{_offset_to_class_name} = $offset_to_class_name; + $self->{_class_name_length} = $class_name_length; + $self->{_class_name} = $class_name; + bless $self, $class; + + return $self; +} + +sub get_timestamp { + my $self = shift; + + return $self->{_timestamp}; +} + +sub get_timestamp_as_string { + my $self = shift; + + return iso8601($self->get_timestamp); +} + +sub get_class_name { + my $self = shift; + + return $self->{_class_name}; +} + +sub is_root { + my $self = shift; + + my $flags = $self->{_flags}; + return $flags & 4 || $flags & 8; +} + +sub get_parent { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset_to_parent = $self->{_offset_to_parent}; + my $key_path = $self->{_key_path}; + + return if $self->is_root; + + my $grandparent_key_path; + my @keys = split /\\/, $key_path, -1; + if (@keys > 2) { + $grandparent_key_path = join('\\', @keys[0..$#keys-2]); + } + + return Parse::Win32Registry::WinNT::Key->new($regfile, + $offset_to_parent, + $grandparent_key_path); +} + +sub get_security { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset_to_security = $self->{_offset_to_security}; + my $key_path = $self->{_key_path}; + + if ($offset_to_security == 0xffffffff) { + return; + } + + return Parse::Win32Registry::WinNT::Security->new($regfile, + $offset_to_security, + $key_path); +} + +sub as_string { + my $self = shift; + + my $string = $self->get_path . ' [' . $self->get_timestamp_as_string . ']'; + return $string; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x nk len=0x%x alloc=%d "%s" par=0x%x keys=%d,0x%x vals=%d,0x%x sec=0x%x class=0x%x', + $self->{_offset}, + $self->{_length}, + $self->{_allocated}, + $self->{_name}, + $self->{_offset_to_parent}, + $self->{_num_subkeys}, $self->{_offset_to_subkey_list}, + $self->{_num_values}, $self->{_offset_to_value_list}, + $self->{_offset_to_security}, + $self->{_offset_to_class_name}; + if (defined $self->{_class_name}) { + $info .= sprintf ',len=0x%x', $self->{_class_name_length}; + } + return $info; +} + +sub _get_offsets_to_subkeys { + my $self = shift; + + # Offset is passed as a parameter for recursive lists such as 'ri' + my $offset_to_subkey_list = shift || $self->{_offset_to_subkey_list}; + + my $regfile = $self->{_regfile}; + my $fh = $regfile->get_filehandle; + + return if $offset_to_subkey_list == 0xffffffff + || $self->{_num_subkeys} == 0; + + sysseek($fh, $offset_to_subkey_list, 0); + my $bytes_read = sysread($fh, my $subkey_list_header, 8); + if ($bytes_read != 8) { + warnf('Could not read subkey list header at 0x%x', + $offset_to_subkey_list); + return; + } + + # 0x00 dword = subkey list length (negative = allocated) + # 0x04 word = 'lf' signature + # 0x06 word = number of entries + # 0x08 dword = offset to 1st subkey + # 0x0c dword = first four characters of the key name + # 0x10 dword = offset to 2nd subkey + # 0x14 dword = first four characters of the key name + # ... + + # 0x00 dword = subkey list length (negative = allocated) + # 0x04 word = 'lh' signature + # 0x06 word = number of entries + # 0x08 dword = offset to 1st subkey + # 0x0c dword = hash of the key name + # 0x10 dword = offset to 2nd subkey + # 0x14 dword = hash of the key name + # ... + + # 0x00 dword = subkey list length (negative = allocated) + # 0x04 word = 'ri' signature + # 0x06 word = number of entries in ri list + # 0x08 dword = offset to 1st lf/lh/li list + # 0x0c dword = offset to 2nd lf/lh/li list + # 0x10 dword = offset to 3rd lf/lh/li list + # ... + + # 0x00 dword = subkey list length (negative = allocated) + # 0x04 word = 'li' signature + # 0x06 word = number of entries in li list + # 0x08 dword = offset to 1st subkey + # 0x0c dword = offset to 2nd subkey + # ... + + # Extracted offsets are always relative to first hbin + + my @offsets_to_subkeys = (); + + my ($length, + $sig, + $num_entries, + ) = unpack('Va2v', $subkey_list_header); + + my $subkey_list_length; + if ($sig eq 'lf' || $sig eq 'lh') { + $subkey_list_length = 2 * 4 * $num_entries; + } + elsif ($sig eq 'ri' || $sig eq 'li') { + $subkey_list_length = 4 * $num_entries; + } + else { + warnf('Invalid signature for subkey list at 0x%x', + $offset_to_subkey_list); + return; + } + + $bytes_read = sysread($fh, my $subkey_list, $subkey_list_length); + if ($bytes_read != $subkey_list_length) { + warnf('Could not read subkey list at 0x%x', + $offset_to_subkey_list); + return; + } + + if ($sig eq 'lf') { + foreach my $offset (unpack("(Vx4)$num_entries", $subkey_list)) { + push @offsets_to_subkeys, OFFSET_TO_FIRST_HBIN + $offset; + } + } + elsif ($sig eq 'lh') { + foreach my $offset (unpack("(Vx4)$num_entries", $subkey_list)) { + push @offsets_to_subkeys, OFFSET_TO_FIRST_HBIN + $offset; + } + } + elsif ($sig eq 'ri') { + foreach my $offset (unpack("V$num_entries", $subkey_list)) { + my $offsets_ref = + $self->_get_offsets_to_subkeys(OFFSET_TO_FIRST_HBIN + $offset); + if (defined $offsets_ref && ref $offsets_ref eq 'ARRAY') { + push @offsets_to_subkeys, @{ $offsets_ref }; + } + } + } + elsif ($sig eq 'li') { + foreach my $offset (unpack("V$num_entries", $subkey_list)) { + push @offsets_to_subkeys, OFFSET_TO_FIRST_HBIN + $offset; + } + } + + return \@offsets_to_subkeys; +} + +sub get_subkey_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $key_path = $self->{_key_path}; + + my @offsets_to_subkeys = (); + if ($self->{_num_subkeys} > 0) { + my $offsets_to_subkeys_ref = $self->_get_offsets_to_subkeys; + if (defined $offsets_to_subkeys_ref) { + @offsets_to_subkeys = @{$self->_get_offsets_to_subkeys}; + } + } + + return Parse::Win32Registry::Iterator->new(sub { + while (defined(my $offset_to_subkey = shift @offsets_to_subkeys)) { + my $subkey = Parse::Win32Registry::WinNT::Key->new($regfile, + $offset_to_subkey, $key_path); + if (defined $subkey) { + return $subkey; + } + } + return; # no more offsets to subkeys + }); +} + +sub _get_offsets_to_values { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $fh = $regfile->get_filehandle; + my $offset_to_value_list = $self->{_offset_to_value_list}; + + my $num_values = $self->{_num_values}; + return if $num_values == 0; + # Actually, this could probably just fall through + # as unpack("x4V0", ...) would return an empty array. + + my @offsets_to_values = (); + + # 0x00 dword = value list length (negative = allocated) + # 0x04 dword = 1st offset + # 0x08 dword = 2nd offset + # ... + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset_to_value_list, 0); + my $value_list_length = 0x4 + $num_values * 4; + my $bytes_read = sysread($fh, my $value_list, $value_list_length); + if ($bytes_read != $value_list_length) { + warnf("Could not read value list at 0x%x", + $offset_to_value_list); + return; + } + + foreach my $offset (unpack("x4V$num_values", $value_list)) { + push @offsets_to_values, OFFSET_TO_FIRST_HBIN + $offset; + } + + return \@offsets_to_values; +} + +sub get_value_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $key_path = $self->{_key_path}; + + my @offsets_to_values = (); + if ($self->{_num_values} > 0) { + my $offsets_to_values_ref = $self->_get_offsets_to_values; + if (defined $offsets_to_values_ref) { + @offsets_to_values = @{$self->_get_offsets_to_values}; + } + } + + return Parse::Win32Registry::Iterator->new(sub { + while (defined(my $offset_to_value = shift @offsets_to_values)) { + my $value = Parse::Win32Registry::WinNT::Value->new($regfile, + $offset_to_value); + if (defined $value) { + return $value; + } + } + return; # no more offsets to values + }); +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/WinNT/Security.pm b/thirdparty/rr-full/Parse/Win32Registry/WinNT/Security.pm new file mode 100644 index 0000000000..7311cbc8ac --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/WinNT/Security.pm @@ -0,0 +1,157 @@ +package Parse::Win32Registry::WinNT::Security; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +use constant SK_HEADER_LENGTH => 0x18; +use constant OFFSET_TO_FIRST_HBIN => 0x1000; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to sk record relative to start of file + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + if (defined(my $cache = $regfile->{_security_cache})) { + if (exists $cache->{$offset}) { + return $cache->{$offset}; + } + } + + my $fh = $regfile->get_filehandle; + + # 0x00 dword = security length (negative = allocated) + # 0x04 word = 'sk' signature + # 0x08 dword = offset to previous sk + # 0x0c dword = offset to next sk + # 0x10 dword = ref count + # 0x14 dword = length of security descriptor + # 0x18 = start of security descriptor + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $sk_header, SK_HEADER_LENGTH); + if ($bytes_read != SK_HEADER_LENGTH) { + warnf('Could not read security at 0x%x', $offset); + return; + } + + my ($length, + $sig, + $offset_to_previous, + $offset_to_next, + $ref_count, + $sd_length, + ) = unpack('Va2x2VVVV', $sk_header); + + $offset_to_previous += OFFSET_TO_FIRST_HBIN + if $offset_to_previous != 0xffffffff; + $offset_to_next += OFFSET_TO_FIRST_HBIN + if $offset_to_next != 0xffffffff; + + my $allocated = 0; + if ($length > 0x7fffffff) { + $allocated = 1; + $length = (0xffffffff - $length) + 1; + } + # allocated should be true + + if ($sig ne 'sk') { + warnf('Invalid signature for security at 0x%x', $offset); + return; + } + + $bytes_read = sysread($fh, my $sd_data, $sd_length); + if ($bytes_read != $sd_length) { + warnf('Could not read security descriptor for security at 0x%x', + $offset); + return; + } + + my $sd = unpack_security_descriptor($sd_data); + if (!defined $sd) { + warnf('Invalid security descriptor for security at 0x%x', + $offset); + # Abandon security object if security descriptor is invalid + return; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_allocated} = $allocated; + $self->{_tag} = $sig; + $self->{_offset_to_previous} = $offset_to_previous; + $self->{_offset_to_next} = $offset_to_next; + $self->{_ref_count} = $ref_count; + $self->{_security_descriptor_length} = $sd_length; + $self->{_security_descriptor} = $sd; + bless $self, $class; + + if (defined(my $cache = $regfile->{_security_cache})) { + $cache->{$offset} = $self; + } + + return $self; +} + +sub get_previous { + my $self = shift; + my $regfile = $self->{_regfile}; + my $offset_to_previous = $self->{_offset_to_previous}; + + return Parse::Win32Registry::WinNT::Security->new($regfile, + $offset_to_previous); +} + +sub get_next { + my $self = shift; + my $regfile = $self->{_regfile}; + my $offset_to_next = $self->{_offset_to_next}; + + return Parse::Win32Registry::WinNT::Security->new($regfile, + $offset_to_next); +} + +sub get_reference_count { + my $self = shift; + + return $self->{_ref_count}; +} + +sub get_security_descriptor { + my $self = shift; + + return $self->{_security_descriptor}; +} + +sub as_string { + my $self = shift; + + return '(security entry)'; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x sk len=0x%x alloc=%d prev=0x%x,next=0x%x refs=%d', + $self->{_offset}, + $self->{_length}, + $self->{_allocated}, + $self->{_offset_to_previous}, + $self->{_offset_to_next}, + $self->{_ref_count}; + + return $info; +} + +1; diff --git a/thirdparty/rr-full/Parse/Win32Registry/WinNT/Value.pm b/thirdparty/rr-full/Parse/Win32Registry/WinNT/Value.pm new file mode 100644 index 0000000000..b9e882b29c --- /dev/null +++ b/thirdparty/rr-full/Parse/Win32Registry/WinNT/Value.pm @@ -0,0 +1,332 @@ +package Parse::Win32Registry::WinNT::Value; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Value); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); + +use constant VK_HEADER_LENGTH => 0x18; +use constant OFFSET_TO_FIRST_HBIN => 0x1000; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to vk record relative to first hbin + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # 0x00 dword = value length (negative = allocated) + # 0x04 word = 'vk' signature + # 0x06 word = value name length + # 0x08 dword = value data length (bit 31 set => data stored inline) + # 0x0c dword = offset to data/inline data + # 0x10 dword = value type + # 0x14 word = flags (bit 1 set => compressed name) + # 0x16 word + # 0x18 = value name [for value name length bytes] + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $vk_header, VK_HEADER_LENGTH); + if ($bytes_read != VK_HEADER_LENGTH) { + warnf('Could not read value at 0x%x', $offset); + return; + } + + my ($length, + $sig, + $name_length, + $data_length, + $offset_to_data, + $type, + $flags, + ) = unpack('Va2vVVVv', $vk_header); + + my $allocated = 0; + if ($length > 0x7fffffff) { + $allocated = 1; + $length = (0xffffffff - $length) + 1; + } + # allocated should be true + + if ($length < VK_HEADER_LENGTH) { + warnf('Invalid value entry length at 0x%x', $offset); + return; + } + + if ($sig ne 'vk') { + warnf('Invalid signature for value at 0x%x', $offset); + return; + } + + $bytes_read = sysread($fh, my $name, $name_length); + if ($bytes_read != $name_length) { + warnf('Could not read name for value at 0x%x', $offset); + return; + } + + if ($flags & 1) { + $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name); + } + else { + $name = decode('UCS-2LE', $name); + }; + + # If the top bit of the data_length is set, then + # the value is inline and stored in the offset to data field (at 0xc). + my $data; + my $data_inline = $data_length >> 31; + if ($data_inline) { + # REG_DWORDs are always inline, but I've also seen + # REG_SZ, REG_BINARY, REG_EXPAND_SZ, and REG_NONE inline + $data_length &= 0x7fffffff; + if ($data_length > 4) { + warnf("Invalid inline data length for value '%s' at 0x%x", + $name, $offset); + $data = undef; + } + else { + # unpack inline data from header + $data = substr($vk_header, 0xc, $data_length); + } + } + else { + if ($offset_to_data != 0 && $offset_to_data != 0xffffffff) { + $offset_to_data += OFFSET_TO_FIRST_HBIN; + if ($offset_to_data < ($regfile->get_length - $data_length)) { + $data = _extract_data($fh, $offset_to_data, $data_length); + } + else { + warnf("Invalid offset to data for value '%s' at 0x%x", + $name, $offset); + } + } + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_allocated} = $allocated; + $self->{_tag} = $sig; + $self->{_name} = $name; + $self->{_name_length} = $name_length; + $self->{_type} = $type; + $self->{_data} = $data; + $self->{_data_length} = $data_length; + $self->{_data_inline} = $data_inline; + $self->{_offset_to_data} = $offset_to_data; + $self->{_flags} = $flags; + bless $self, $class; + + return $self; +} + +sub _extract_data { + my $fh = shift; + my $offset_to_data = shift; + my $data_length = shift; + + if ($offset_to_data == 0 || $offset_to_data == 0xffffffff) { + return undef; + } + + sysseek($fh, $offset_to_data, 0); + my $bytes_read = sysread($fh, my $data_header, 4); + if ($bytes_read != 4) { + warnf('Could not read data at 0x%x', $offset_to_data); + return undef; + } + + my ($max_data_length) = unpack('V', $data_header); + + my $data_allocated = 0; + if ($max_data_length > 0x7fffffff) { + $data_allocated = 1; + $max_data_length = (0xffffffff - $max_data_length) + 1; + } + # data_allocated should be true + + my $data; + + if ($data_length > $max_data_length) { + $bytes_read = sysread($fh, my $db_entry, 8); + if ($bytes_read != 8) { + warnf('Could not read data at 0x%x', $offset_to_data); + return undef; + } + + my ($sig, $num_data_blocks, $offset_to_data_block_list) + = unpack('a2vV', $db_entry); + if ($sig ne 'db') { + warnf('Invalid signature for big data at 0x%x', $offset_to_data); + return undef; + } + $offset_to_data_block_list += OFFSET_TO_FIRST_HBIN; + + sysseek($fh, $offset_to_data_block_list + 4, 0); + $bytes_read = sysread($fh, my $data_block_list, $num_data_blocks * 4); + if ($bytes_read != $num_data_blocks * 4) { + warnf('Could not read data block list at 0x%x', + $offset_to_data_block_list); + return undef; + } + + $data = ""; + my @offsets = map { OFFSET_TO_FIRST_HBIN + $_ } + unpack("V$num_data_blocks", $data_block_list); + foreach my $offset (@offsets) { + sysseek($fh, $offset, 0); + $bytes_read = sysread($fh, my $block_header, 4); + if ($bytes_read != 4) { + warnf('Could not read data block at 0x%x', $offset); + return undef; + } + my ($block_length) = unpack('V', $block_header); + if ($block_length > 0x7fffffff) { + $block_length = (0xffffffff - $block_length) + 1; + } + $bytes_read = sysread($fh, my $block_data, $block_length - 8); + if ($bytes_read != $block_length - 8) { + warnf('Could not read data block at 0x%x', $offset); + return undef; + } + $data .= $block_data; + } + if (length($data) < $data_length) { + warnf("Insufficient data blocks for data at 0x%x", $offset_to_data); + return undef; + } + $data = substr($data, 0, $data_length); + return $data; + } + else { + $bytes_read = sysread($fh, $data, $data_length); + if ($bytes_read != $data_length) { + warnf("Could not read data at 0x%x", $offset_to_data); + return undef; + } + } + return $data; +} + +sub get_data { + my $self = shift; + + my $type = $self->get_type; + + my $data = $self->{_data}; + return if !defined $data; + + # apply decoding to appropriate data types + if ($type == REG_DWORD) { + if (length($data) == 4) { + $data = unpack('V', $data); + } + else { + # incorrect length for dword data + $data = undef; + } + } + elsif ($type == REG_DWORD_BIG_ENDIAN) { + if (length($data) == 4) { + $data = unpack('N', $data); + } + else { + # incorrect length for dword data + $data = undef; + } + } + elsif ($type == REG_SZ || $type == REG_EXPAND_SZ) { + $data = decode('UCS-2LE', $data); + # snip off any terminating null + chop $data if substr($data, -1, 1) eq "\0"; + } + elsif ($type == REG_MULTI_SZ) { + $data = decode('UCS-2LE', $data); + # snip off any terminating nulls + chop $data if substr($data, -1, 1) eq "\0"; + chop $data if substr($data, -1, 1) eq "\0"; + my @multi_sz = split("\0", $data, -1); + # make sure there is at least one empty string + @multi_sz = ('') if @multi_sz == 0; + return wantarray ? @multi_sz : join($", @multi_sz); + } + + return $data; +} + +sub as_regedit_export { + my $self = shift; + my $version = shift || 5; + + my $name = $self->get_name; + my $export = $name eq '' ? '@=' : '"' . $name . '"='; + + my $type = $self->get_type; + + # XXX +# if (!defined $self->{_data}) { +# $name = $name eq '' ? '@' : qq{"$name"}; +# return qq{; $name=(invalid data)\n}; +# } + + if ($type == REG_SZ) { + $export .= '"' . $self->get_data . '"'; + $export .= "\n"; + } + elsif ($type == REG_BINARY) { + $export .= "hex:"; + $export .= format_octets($self->{_data}, length($export)); + } + elsif ($type == REG_DWORD) { + my $data = $self->get_data; + $export .= defined($data) + ? sprintf("dword:%08x", $data) + : "dword:"; + $export .= "\n"; + } + elsif ($type == REG_EXPAND_SZ || $type == REG_MULTI_SZ) { + my $data = $version == 4 + ? encode("ascii", $self->{_data}) # unicode->ascii + : $self->{_data}; # raw data + $export .= sprintf("hex(%x):", $type); + $export .= format_octets($data, length($export)); + } + else { + $export .= sprintf("hex(%x):", $type); + $export .= format_octets($self->{_data}, length($export)); + } + return $export; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x vk len=0x%x alloc=%d "%s" type=%d', + $self->{_offset}, + $self->{_length}, + $self->{_allocated}, + $self->{_name}, + $self->{_type}; + if ($self->{_data_inline}) { + $info .= sprintf ' data=inline,len=0x%x', + $self->{_data_length}; + } + else { + $info .= sprintf ' data=0x%x,len=0x%x', + $self->{_offset_to_data}, + $self->{_data_length}; + } + return $info; +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry.pm b/thirdparty/rr/Parse/Win32Registry.pm new file mode 100644 index 0000000000..1aa4556ffa --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry.pm @@ -0,0 +1,1834 @@ +package Parse::Win32Registry; + +use 5.008_001; +use strict; +use warnings; + +our $VERSION = '1.0'; + +use base qw(Exporter); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::Win95::File; +use Parse::Win32Registry::WinNT::File; + +our @EXPORT_OK = ( + # include old function names for backwards compatibility + 'convert_filetime_to_epoch_time', + 'formatted_octets', + @Parse::Win32Registry::Base::EXPORT_OK +); + +our %EXPORT_TAGS = ( + REG_ => [grep { /^REG_[A-Z_]*$/ } @EXPORT_OK], + all => [@EXPORT_OK], + functions => [grep { /^[a-z0-9_]*$/ } @EXPORT_OK], + constants => [grep { /^[A-Z_]*$/ } @EXPORT_OK], +); + +*convert_filetime_to_epoch_time = \&Parse::Win32Registry::unpack_windows_time; +*formatted_octets = \&Parse::Win32Registry::format_octets; + +sub enable_warnings { + $Parse::Win32Registry::Base::WARNINGS = 1; +} + +sub disable_warnings { + $Parse::Win32Registry::Base::WARNINGS = 0; +} + +sub set_codepage { + my $codepage = shift; + if (defined $codepage) { + $Parse::Win32Registry::Base::CODEPAGE = $codepage; + } +} + +sub get_codepage { + $Parse::Win32Registry::Base::CODEPAGE; +} + +sub new { + my $class = shift; + my $filename = shift or croak "No filename specified"; + + open my $regfile, "<", $filename or croak "Unable to open '$filename': $!"; + sysread($regfile, my $sig, 4); + if (!defined($sig) || length($sig) != 4) { + warnf("Could not read registry file header"); + return; + } + close $regfile; + + if ($sig eq "CREG") { + # attempt to parse this as a Windows 95 Registry File + return Parse::Win32Registry::Win95::File->new($filename); + } + elsif ($sig eq "regf") { + # attempt to parse this as a Windows NT Registry File + return Parse::Win32Registry::WinNT::File->new($filename); + } + else { + warnf("Invalid registry file header"); + return; + } +} + +1; + +__END__ + +=head1 NAME + +Parse::Win32Registry - Parse Windows Registry Files + +=head1 SYNOPSIS + + use strict; + use Parse::Win32Registry qw( :REG_ + unpack_windows_time + unpack_unicode_string ); + + my $filename = shift or die "Filename?"; + + my $registry = Parse::Win32Registry->new($filename) + or die "'$filename' is not a registry file\n"; + my $root_key = $registry->get_root_key + or die "Could not get root key of '$filename'\n"; + + # The following code works on USER.DAT or NTUSER.DAT files + + my $software_key = $root_key->get_subkey(".DEFAULT\\Software") + || $root_key->get_subkey("Software"); + + if (defined($software_key)) { + my @user_key_names = ( + "Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", + "Microsoft\\Windows\\CurrentVersion\\Explorer\\RunMRU", + ); + + foreach my $name (@user_key_names) { + if (my $key = $software_key->get_subkey($name)) { + print "\n", $key->as_string, "\n"; + foreach my $value ($key->get_list_of_values) { + print $value->as_string, "\n"; + } + } + } + + # This demonstrates how you can deal with a binary value + # that contains a Unicode string + foreach my $ver (qw(8.0 9.0 10.0 11.0)) { + my $key_name = "Microsoft\\Office\\$ver\\Common\\UserInfo"; + if (my $key = $software_key->get_subkey($key_name)) { + print "\n", $key->as_string, "\n"; + my @value_names = qw(UserName UserInitials Company); + foreach my $value_name (@value_names) { + if (my $value = $key->get_value($value_name)) { + print $value->as_string, "\n"; + my $data = $value->get_data; + my $string = unpack_unicode_string($data); + print "$value_name = '$string'\n"; + } + } + } + } + } + + # The following code works on SYSTEM.DAT or SOFTWARE files + + my $software_key = $root_key->get_subkey("Software") || $root_key; + + if (defined($software_key)) { + my @software_key_names = ( + "Microsoft\\Windows\\CurrentVersion", + "Microsoft\\Windows NT\\CurrentVersion", + ); + + foreach my $name (@software_key_names) { + if (my $key = $software_key->get_subkey($name)) { + print "\n", $key->as_string, "\n"; + foreach my $value ($key->get_list_of_values) { + print $value->as_string, "\n"; + } + } + } + + # This demonstrates how you can deal with a Unix date + # found in a registry value + my $key_name = "Microsoft\\Windows NT\\CurrentVersion"; + if (my $curver_key = $software_key->get_subkey($key_name)) { + print "\n", $curver_key->as_string, "\n"; + if (my $value = $curver_key->get_value("InstallDate")) { + print $value->as_string, "\n"; + my $time = $value->get_data; + print "InstallDate = ", + scalar gmtime $time, " GMT\n"; + print "InstallDate = ", + scalar localtime $time, " Local\n"; + } + } + } + + # The following code works on SYSTEM.DAT or SYSTEM files + + my $system_key = $root_key->get_subkey("System") || $root_key; + + my $ccs_name = "CurrentControlSet"; # default for Win95 + if (my $key = $system_key->get_subkey("Select")) { + my $current_value = $key->get_value("Current"); + $ccs_name = sprintf("ControlSet%03d", $current_value->get_data); + print "CurrentControlSet = $ccs_name\n"; + } + + my $ccs_key = $system_key->get_subkey($ccs_name); + + if (defined($ccs_key)) { + my @system_key_names = ( + "Control\\ComputerName\\ComputerName", + "Control\\TimeZoneInformation", + ); + + foreach my $name (@system_key_names) { + if (my $key = $ccs_key->get_subkey($name)) { + print "\n", $key->as_string, "\n"; + foreach my $value ($key->get_list_of_values) { + print $value->as_string, "\n"; + } + } + } + + # This demonstrates how you can deal with a Windows date + # found in a registry value + my $key_name = "Control\\Windows"; + if (my $windows_key = $ccs_key->get_subkey($key_name)) { + print "\n", $windows_key->as_string, "\n"; + if (my $value = $windows_key->get_value("ShutdownTime")) { + print $value->as_string, "\n"; + my $data = $value->get_data; + my $time = unpack_windows_time($data); + print "ShutdownTime = ", + scalar gmtime $time, " GMT\n"; + print "ShutdownTime = ", + scalar localtime $time, " Local\n"; + } + } + } + +=head1 DESCRIPTION + +Parse::Win32Registry is a module for parsing Windows Registry files, +allowing you to read the keys and values of a registry file +without going through the Windows API. + +It provides an object-oriented interface to the keys and values +in a registry file. Registry files are structured as trees of keys, +with each key containing further subkeys or values. + +The module is intended to be cross-platform, and run on those platforms +where Perl will run. + +It supports both +Windows NT registry files (Windows NT, 2000, XP, 2003, Vista, 7) +and Windows 95 registry files (Windows 95, 98, Millennium Edition). + +It is intended to be used to parse offline registry files. +If a registry file is currently in use, you will not be able to open it. +However, you can save part or all of a currently loaded registry file +using the Windows reg command +if you have the appropriate administrative access. + +=head1 DEPENDENCIES + +Requires Perl 5.8.1. All required modules are standard modules. + +=head1 METHODS + +Start by creating a Registry object from a valid registry file. +Use the Registry object's get_root_key method +to obtain the root key of that registry file. +This root key is your first Key object. +From this key, you can explore the Key and Value objects +that comprise the registry file using the methods described below. + +Data is read directly from a registry file when a Key or Value object +is created, and discarded when the Key or Value object is destroyed. +This avoids any delay in parsing an entire registry file to obtain a +Key or Value object as most code only looks at a subset of the keys +and values contained in a registry file. + +=head2 Registry Object Methods + +=over 4 + +=item $registry = Parse::Win32Registry->new( 'filename' ) + +Creates a new Registry object for the specified registry file. + +=item $registry->get_root_key + +Returns the root Key object of the registry file. + +The root key of a registry file is not the same as one of the virtual +roots of the registry (HKEY_LOCAL_MACHINE, HKEY_USERS, etc) that you +might be familiar with from using tools such as REGEDIT. + +The names of root keys vary by operating system and by file. +For example, the name of the root key of a Windows XP NTUSER.DAT file +is '$$$PROTO.HIV' and the name of the root key of a Windows 98 +USER.DAT file is an empty string. + +=item $registry->get_virtual_root_key + +=item $registry->get_virtual_root_key( 'virtual root key name' ) + +Returns the virtual root Key object of the registry file. + +In all respects this is exactly the same as the root Key object, +except that it pretends to be a virtual root by simply faking its name. +It guesses the virtual root key name +by looking at the filename of the registry file. +For example, if the filename contains 'SYSTEM' +the virtual root key will be named 'HKEY_LOCAL_MACHINE\\SYSTEM'. +If the guess fails (because the filename is not recognised) +the virtual root key will be named 'HKEY_UNKNOWN'. + +You can override the guess by supplying your own root key name. +You can use this to pass in your preferred root key name. +For example, you could pass the filename of the registry file in as +the virtual root key name, which would then cause the filename to +appear as part of each key's path. + +=item $registry->get_timestamp + +Returns the embedded timestamp for the registry file as a time value +(the number of seconds since your computer's epoch) +suitable for passing to gmtime or localtime. + +Only Windows NT registry files have an embedded timestamp. + +Returns nothing if the date is out of range +or if called on a Windows 95 registry file. + +=item $registry->get_timestamp_as_string + +Returns the timestamp as a ISO 8601 string, +for example, '2010-05-30T13:57:11Z'. +The Z indicates that the time is GMT ('Zero Meridian'). + +Returns the string '(undefined)' if the date is out of range +or if called on a Windows 95 registry file. + +=item $registry->get_embedded_filename + +Returns the embedded filename for the registry file. + +Only Windows NT registry files have an embedded filename. + +Returns nothing if called on a Windows 95 registry file. + +=item $registry->get_filename + +Returns the filename of the registry file. + +=item $registry->get_length + +Returns the length of the registry file. + +=back + +=head2 Key Object Methods + +=over 4 + +=item $key->get_name + +Returns the name of the key. The root key of a Windows 95 based +registry file does not have a name; this is returned as an empty +string. + +=item $key->get_path + +Returns the path to the key. This shows the all of the keys +from the root key to the current key, +joined by the path separator '\'. + +=item $key->get_subkey( 'key name' ) + +Returns a Key object for the specified subkey name. +If a key with that name does not exist, nothing will be returned. + +You can specify a path to a subkey by separating keys +using the path separator '\'. Remember +to quote any '\' characters with a preceding '\'. +For example: + + $key->get_subkey('Software\\Microsoft\\Windows') + +A path is always relative to the current key. +It should start with the name of the first subkey in the path, +not the current key. +If any key in the path does not exist, nothing will be returned. + +=item $key->get_value( 'value name' ) + +Returns a Value object for the specified value name. +If a value with that name does not exist, nothing will be returned. + +The default value (displayed as '(Default)' by REGEDIT) +does not actually have a name. It can obtained by supplying +an empty string, e.g. $key->get_value(''); + +=item $key->get_value_data( 'value name' ) + +Returns the data for the specified value name. +If either the value or the value's data does not exist, +nothing will be returned. + +This is simply a shortcut for accessing the data of a value +without creating an intermediate Value object. + +The following code: + + my $value = $key->get_value('value name'); + if (defined $value) { + my $data = $value->get_data; + if (defined $data) { + ...process data... + } + } + +can be replaced with: + + my $data = $key->get_value_data('value name'); + if (defined $data) { + ...process data... + } + +=item $key->get_list_of_subkeys + +Returns a list of Key objects representing the subkeys of the +current key. If a key has no subkeys, an empty list will be returned. + +=item $key->get_list_of_values + +Returns a list of Value objects representing the values of the +current key. If a key has no values, an empty list will be returned. + +=item $key->get_timestamp + +Returns the timestamp for the key as a time value +(the number of seconds since your computer's epoch) +suitable for passing to gmtime or localtime. + +Only Windows NT registry keys have a timestamp. + +Returns nothing if the date is out of range +or if called on a Windows 95 registry key. + +=item $key->get_timestamp_as_string + +Returns the timestamp as an ISO 8601 string, +for example, '2010-05-30T13:57:11Z'. +The Z indicates that the time is GMT ('Zero Meridian'). + +Returns the string '(undefined)' if the date is out of range +or if called on a Windows 95 registry key. + +=item $key->as_string + +Returns the path of the key as a string. +The timestamp will be appended for Windows NT registry keys. + +=item $key->as_regedit_export + +Returns the path of the key as a string +in the Windows Registry Editor Version 5.00 export format. +The string will be terminated with a newline character. + +If used in conjunction with the get_virtual_root_key method +of Registry objects this should generate key paths +interoperable with those exported by REGEDIT. + +=item $key->get_parent + +Returns a Key object for parent of the current key. +If the key does not have a valid parent key +(which will normally only occur for the root key) +nothing will be returned. + +=item $key->is_root + +Returns true if this key is the root key. + +=item $key->get_class_name + +Returns a string containing the class name associated with a key. +Only a very few Windows NT registry key have class names. + +Returns nothing if the key has no class name +or if called on a Windows 95 registry key. + +=item $key->get_security + +Returns a Security object containing the security information +for the key. Only Windows NT registry keys have security information. + +Returns nothing if called on a Windows 95 registry key. + +=item $key->get_subkey_iterator + +Returns an iterator for retrieving the subkeys of the current key. +Each time the get_next method of the iterator is used, +it will return a single Key object. +Keys will be returned one by one +until the end of the list is reached, +when nothing will be returned. + +It can be used as follows: + + my $subkey_iter = $key->get_subkey_iterator; + while (my $subkey = $subkey_iter->get_next) { + # do something with $subkey + ... + } + +Note that it is usually simpler to just use $key->get_list_of_subkeys. +An iterator might be useful when you need to +control the amount of processing you are performing, +such as programs that need to remain responsive to user actions. + +=item $key->get_value_iterator + +Returns an iterator for retrieving the values of the current key. +Each time the get_next method of the iterator is used, +it will return a single Value object. +Values will be returned one by one +until the end of the list is reached, +when nothing will be returned. + +It can be used as follows: + + my $value_iter = $key->get_value_iterator; + while (my $value = $value_iter->get_next) { + # do something with $value + ... + } + +Note that it is usually simpler to just use $key->get_list_of_values. + +=item $key->get_subtree_iterator + +Returns an iterator for retrieving the entire subtree +of keys and values beginning at the current key. +Each time the get_next method of the iterator is used, +it will return either a Key object +or a Key object and a Value object. +Each value accompanies the key that it belongs to. +Keys or Key/Value pairs will be returned one by one +until the end of the list is reached, +when nothing will be returned. + +It can be used as follows: + + my $subtree_iter = $key->get_subtree_iterator; + while (my ($key, $value) = $subtree_iter->get_next) { + if (defined $value) { + # do something with $key and $value + ... + } + else { + # do something with $key + ... + } + } + +Keys and values will be returned in the following order: + + root_key + root_key\key1 + root_key\key1, value1 + root_key\key1, value2 + root_key\key1\key2 + root_key\key1\key2, value3 + root_key\key1\key2, value4 + +If the iterator is used in a scalar context, +only Key objects will returned. + + my $subtree_iter = $key->get_subtree_iterator; + while (my $key = $subtree_iter->get_next) { + # do something with $key + ... + } + +Keys will be returned in the following order: + + root_key + root_key\key1 + root_key\key1\key2 + + +Note that it might be simpler to write a recursive function +to process the keys and values. + + sub traverse { + my $key = shift; + + # do something with $key + ... + + foreach my $value ($key->get_list_of_values) { + # do something with $value + ... + } + + foreach my $subkey ($key->get_list_of_subkeys) { + # recursively process $key + traverse($subkey); + } + } + + traverse($root_key); + +=item $key->walk( \&callback ); + +Performs a recursive descent of all the keys +in the subtree starting with the calling key, +and calls the callback function for each key reached. + +The callback function will be passed the current key. + + $key->walk( sub { + my $key = shift; + print $key->as_string, "\n"; + } ); + + $key->walk( sub { + my $key = shift; + print $key->as_regedit_export; + foreach my $value ($key->get_list_of_values) { + print $value->as_regedit_export; + } + } ); + +=back + +=head2 Value Object Methods + +=over 4 + +=item $value->get_name + +Returns the name of the value. +In both Windows NT and Windows 95 based registry files +you can get values without a name. +This is returned as an empty string. + +=item $value->get_type + +Returns the integer representing the type of the value +(where 1 is a REG_SZ, 2 is a REG_EXPAND_SZ, etc). +The constants for the value types can be imported from +the Parse::Win32Registry module with + + use Parse::Win32Registry qw( :REG_ ); + +=item $value->get_type_as_string + +Returns the type of the value as a string instead of an integer constant, +making it more suitable for printed output. + +=item $value->get_data + +Returns the data for the value. + +REG_SZ and REG_EXPAND_SZ values will be returned as strings. +String data will be converted from Unicode (UCS-2LE) for Windows +NT based registry files. +Any terminating null characters will be removed. + +REG_MULTI_SZ values will be returned as a list of strings when +called in a list context, +and as a string with each element separated by +the list separator $" when called in a scalar context. +(The list separator defaults to the space character. +See perlvar for further information.) +String data will be converted from Unicode (UCS-2LE) for Windows +NT based registry files. + + # get REG_MULTI_SZ data as a string + my $data = $multi_sz_value->get_data; + + # get REG_MULTI_SZ data as a list + my @data = $multi_sz_value->get_data; + +REG_DWORD values are unpacked and returned as unsigned integers. + +All other types are returned as packed binary strings. +To extract data from these packed binary strings, +you will need to use Perl's unpack function, +or one of the provided support functions. + +Nothing will be returned if the data is invalid. + +=item $value->get_data_as_string + +Returns the data for a value, making binary data safe for printed output. + +REG_SZ and REG_EXPAND_SZ values will be returned directly from get_data, +REG_MULTI_SZ values will have their component strings prefixed by +indices to more clearly show the number of elements, and +REG_DWORD values will be returned as a hexadecimal number followed +by its parenthesized decimal equivalent. +All other types of values will be returned as a string of hex octets. + +'(invalid data)' will be returned if the data is invalid +(i.e. when get_data returns undef). + +'(no data)' will be returned if get_data returns an empty string. + +=item $value->get_raw_data + +Returns the data for a value exactly as it was read from the registry, +without the processing normally performed by get_data. + +It is intended for those rare occasions +when you need to access binary data that has been +inappropriately stored in +a REG_SZ, REG_EXPAND_SZ, REG_MULTI_SZ, or REG_DWORD value. + +=item $value->as_string + +Returns the name, type, and data for the value as a string, +safe for printed output. + +'(Default)' will be used for the names of +those values that do not have names. + +=item $value->as_regedit_export + +Returns the name, type, and data for the value as a string, +in the Windows Registry Editor Version 5.00 export format. +The string will contain line breaks to ensure that +no line is longer than 80 characters. +Each line will be terminated with a newline character. + +'@' will be used for the names of +those values that do not have names. + +This should generate values +interoperable with those exported by REGEDIT. + +=back + +=head2 Security Object Methods + +Only Windows NT registry files contain security information +to control access to the registry keys. +This information is stored in security entries which are distributed +through the registry file separately from the keys that they apply to. +This allows the registry to share security information +amongst a large number of keys whilst unnecessary duplication. + +Security entries link to other security entries in a circular chain, +each entry linking to the one that precedes it and the one that follows it. + +=over 4 + +=item $security->get_security_descriptor + +Returns a Security Descriptor Object representing the security descriptor +contained in the security information registry entry. + +=item $security->get_next + +Returns the next security object. + +=item $security->get_previous + +Returns the previous security object. + +=item $security->get_reference_count + +Returns the reference count for the security object. + +=back + +=head2 Security Descriptor Object Methods + +A Security Descriptor object represents a security descriptor which +contains an owner SID, a primary group SID, +a System ACL, and a Discretionary ACL. + +=over 4 + +=item $security_descriptor->get_owner + +Returns a SID Object containing the Owner SID. + +=item $security_descriptor->get_group + +Returns a SID Object containing the primary group SID. + +=item $security_descriptor->get_sacl + +Returns an ACL Object containing the System ACL. +The System ACL contains those ACEs used for auditing. +Nothing will be returned if the security descriptor does not contain +a System ACL. + +=item $security_descriptor->get_dacl + +Returns an ACL Object containing the Discretionary ACL. +The Discretionary ACL contains those ACEs used for access control. +Nothing will be returned if the security descriptor does not contain +a Discretionary ACL. + +=item $security_descriptor->as_stanza + +Returns a multi-line string containing +the security descriptor formatted for presentation. +It will contain a line for the owner SID, +the group SID, +and each component ACE of the System ACL and the Discretionary ACL. +Each line will be terminated by a newline character. + +=back + +=head2 ACL Object Methods + +An ACL object represents an Access Control List, +which comprises a list of Access Control Entries. + +=over 4 + +=item $acl->get_list_of_aces + +Returns a list of ACE Objects representing the ACEs +in the order they appear in the ACL. +If the ACL contains no ACEs, nothing will be returned. + +=item $acl->as_stanza + +Returns a multi-line string containing +the ACL formatted for presentation. +It will contain a line for each component ACE of the ACL. +Each line will be terminated by a newline character. + +=back + +=head2 ACE Object Methods + +An ACE object represents an Access Control Entry. +An ACE describes the permissions assigned (the access mask) +to a Security Identifier (the trustee). + +=over 4 + +=item $ace->get_type + +Returns an integer containing the ACE type, +where 0 indicates an ACCESS_ALLOWED ACE, +1 an ACCESS_DENIED ACE, and +2 a SYSTEM_AUDIT ACE. +Typically you will encounter +ACCESS_ALLOWED and ACCESS_DENIED ACEs in Discretionary ACLs +and SYSTEM_AUDIT ACEs in System ACLs. + +=item $ace->get_type_as_string + +Returns the type as a string, rather than integer. + +=item $ace->get_flags + +Returns an integer containing the ACE flags. + +=item $ace->get_access_mask + +Returns an integer containing the ACE access mask. +The access mask controls what actions the trustee might perform with +the object the ACE applies to. + +=item $ace->get_trustee + +Returns a SID Object containing the trustee that this ACE +is associated with. + +=item $ace->as_string + +Returns a string containing +the ACE formatted for presentation. + +=back + +=head2 SID Object Methods + +A SID object represents a Security Identifier. + +=over 4 + +=item $sid->get_name + +Returns a string containing a name for the SID +(e.g. "Administrators" for S-1-5-32-544) +if it is a "well known" SID. +See Microsoft Knowledge Base Article KB243330. + +=item $sid->as_string + +Returns a string containing the SID formatted for presentation. + +=back + +=head1 EXPORTS + +=head2 Constants + +On request, Parse::Win32Registry will export the registry type constants: + + use Parse::Win32Registry qw( :REG_ ); + +The :REG_ tag exports all of the following constants: + + REG_NONE + REG_SZ + REG_EXPAND_SZ + REG_BINARY + REG_DWORD + REG_DWORD_BIG_ENDIAN + REG_LINK + REG_MULTI_SZ + REG_RESOURCE_LIST + REG_FULL_RESOURCE_DESCRIPTOR + REG_RESOURCE_REQUIREMENTS_LIST + REG_QWORD + +You can import individual types by specifying them, for example: + + use Parse::Win32Registry qw( REG_SZ REG_DWORD ); + +=head1 SUPPORT FUNCTIONS + +Parse::Win32Registry provides a number of support functions, +which are exported on request. All of the support functions can +be imported with: + + use Parse::Win32Registry qw( :functions ); + +=head2 Unpacking Binary Data + +There are a number of functions for assisting in unpacking binary data +found in registry values. +These functions are exported on request: + + use Parse::Win32Registry qw( unpack_windows_time + unpack_unicode_string + unpack_sid + unpack_ace + unpack_acl + unpack_security_descriptor ); + +These unpack functions also return the length +of the packed object when called in a list context. + +For example, to extract one SID: + + my $sid = unpack_sid($data); + +To extract a series of SIDs: + + my $pos = 0; + while ($pos < length($data)) { + my ($sid, $packed_len) = unpack_sid(substr($data, $pos)); + last if !defined $sid; # abort if SID not defined + + # ...do something with $sid... + + $pos += $packed_len; # move past the packed SID + } + +=over 4 + +=item $time = unpack_windows_time( $data ) +=item ( $time, $packed_len ) = unpack_windows_time( $data ) + +Returns the epoch time for the Win32 FILETIME +contained in the supplied binary data. +A Win32 FILETIME is a 64-bit integer +containing the number of 100-nanosecond intervals since January 1st, 1601 +and can sometimes be found in Windows NT registry values. + +Returns nothing if the date is earlier than your computer's epoch. +The epoch begins at January 1st, 1970 on Unix and Windows machines. + +When called in a list context, it will also return the space used +in the supplied data by the windows time. + +(This function can also be called by its previous name of +convert_filetime_to_epoch_time.) + +=item $str = unpack_unicode_string( $data ) +=item ( $str, $packed_len ) = unpack_unicode_string( $data ) + +Extracts a Unicode (UCS-2LE) string from the supplied binary data. +Any terminating null characters are dropped. +Unicode (UCS-2LE) strings are sometimes encountered +in Windows NT registry REG_BINARY values. + +Note that Unicode strings contained in +REG_SZ, REG_EXPAND_SZ, and REG_MULTI_SZ values +are already automatically decoded +by the get_data method of a Value object. + +When called in a list context, it will also return the space used +in the supplied data by the Unicode string. + +=item $sid = unpack_sid( $data ) +=item ( $sid, $packed_len) = unpack_sid( $data ) + +Returns a SID Object representing the SID contained in the supplied data. +Returns nothing if the supplied data does not appear to contain a valid SID. + +When called in a list context, it will also return the space used +in the supplied data by the SID. + +=item $ace = unpack_ace( $data ) +=item ( $ace, $packed_len ) = unpack_ace( $data ) + +Returns an ACE Object representing the ACE contained in the supplied data. +Returns nothing if the supplied data does not appear to contain a valid ACE. + +When called in a list context, it will also return the space used +in the supplied data by the ACE. + +=item $acl = unpack_acl( $data ) +=item ( $acl, $packed_len ) = unpack_acl( $data ) + +Returns an ACL Object representing the ACL contained in the supplied data. +Returns nothing if the supplied data does not appear to contain a valid ACL. + +When called in a list context, it will also return the space used +in the supplied data by the ACL. + +=item $sd = unpack_security_descriptor( $data ) +=item ( $sd, $packed_len ) = unpack_security_descriptor( $data ) + +Returns a Security Descriptor Object representing +the security descriptor contained in the supplied data. +Returns nothing if the supplied data does not appear to contain +a valid security descriptor. + +When called in a list context, it will also return the space used +in the supplied data by the security descriptor. + +=back + +=head2 Formatting Data + +These functions are exported on request: + + use Parse::Win32Registry qw( iso8601 hexdump ); + +=over 4 + +=item $str = iso8601( $epoch_time ) + +Returns the ISO8601 string for the supplied $epoch_time, +for example, '2010-05-30T13:57:11Z'. +It assumes the supplied $epoch_time is in UTC, +and appends 'Z' to indicate this. + +The string '(undefined)' will be returned if the epoch time +is out of range. + + my $data = $reg_binary_value->get_data; + + # extract the Win32 FILETIME starting at the 9th byte of $data + my $time = unpack_windows_time( substr( $data, 8 ) ); + my $time_as_string = iso8601( $time ); + print "$time_as_string\n"; + +There are a number of ways of displaying a timestamp. For example: + + use Parse::Win32Registry qw(iso8601); + use POSIX qw(strftime); + print iso8601($key->get_timestamp); + print scalar(gmtime($key->get_timestamp)), " GMT\n"; + print scalar(localtime($key->get_timestamp)), " Local\n"; + print strftime("%Y-%m-%d %H:%M:%S GMT", + gmtime($key->get_timestamp)), "\n"; + print strftime("%Y-%m-%d %H:%M:%S Local", + localtime($key->get_timestamp)), "\n"; + +...might produce the following output: + + 2000-08-06T23:42:36Z + Sun Aug 6 23:42:36 2000 GMT + Mon Aug 7 07:42:36 2000 Local + 2000-08-06 23:42:36 GMT + 2000-08-07 07:42:36 Local + +=item $str = hexdump( $data ) + +Returns a multi-line string containing +a hexadecimal dump of the supplied data. +Each line will display 16 bytes in hexadecimal and ASCII, +and will be terminated by a newline character. + +=back + +=head2 Processing Multiple Registry Files Simultaneously + +There are three support functions +that create iterators for simultaneously +processing the keys and values +of multiple registry files. +These functions are exported on request: + + use Parse::Win32Registry qw( make_multiple_subkey_iterator + make_multiple_value_iterator + make_multiple_subtree_iterator ); + + +Handling lists of subkeys or values +should be done with a little care +as some of the processed registry files +might not contain the subkey or value being examined +and the list will contain missing entries: + + ($key1, $key2, undef, $key4) + +One way of handling this is to use map to check that a key is defined +and return undef if the subkey or value is not present. + + @subkeys = map { defined $_ && $_->get_subkey('subkey') || undef } @keys; + + @values = map { defined $_ && $_->get_value('value') || undef } @keys; + +=over 4 + +=item $iter = make_multiple_subkey_iterator( $key1, $key2, $key3, ... ) + +Returns an iterator for retrieving +the subkeys of the supplied Key objects. +Each call to the get_next method of the iterator +returns a reference to +a list of Key objects with the same name and path. +If any of the supplied Key objects +does not have a subkey with that name, +then that subkey will be undefined. + + my $subkey_iter = make_multiple_subkey_iterator($key1, $key2, ...); + while (my ($subkey1, $subkey2, ...) = $subkey_iter->get_next) { + ... + } + + my $subkey_iter = make_multiple_subkey_iterator($key1, $key2, ...); + while (my @subkeys = $subkey_iter->get_next) { + foreach my $subkey (@subkeys) { + if (defined $subkey) { + ... + } + } + } + +=item $iter = make_multiple_value_iterator( $key1, $key2, $key3, ... ) + +Returns an iterator for retrieving +the values of the supplied Key objects. +Each call to the get_next method of the iterator +returns a reference to +a list of Value objects with the same name. +If any of the supplied Key objects +does not have a value with that name, +then that value will be undefined. + + my $value_iter = make_multiple_value_iterator($key1, $key2, ...); + while (my ($value1, $value2, ...) = $value_iter->get_next) { + ... + } + +=item $iter = make_multiple_subtree_iterator( $key1, $key2, $key3, ... ) + +Returns an iterator for retrieving +the immediate subkeys and all descendant subkeys of the supplied Key objects. +Each call to the get_next method of the iterator +returns a list of Key objects with the same name and path. +If any of the supplied Key objects +does not have a subkey with that name, +then that subkey will be undefined. + +Each call to the get_next method of the iterator +returns it will return +either a reference to a list of Key objects +or a reference to a list of Key objects +and a reference to a list of a Value objects, +with each list of values accompanying the list of keys that they belong to. +Nothing is returned when the end of the list is reached. + + my $subtree_iter = make_multiple_subtree_iterator($key1, $key2, ...); + while (my $subkeys_ref = $tree_iter->get_next) { + # do something with @$subkeys_ref + } + + my $subtree_iter = make_multiple_subtree_iterator($key1, $key2, ...); + while (my ($subkeys_ref, $values_ref) = $tree_iter->get_next) { + if (defined $values_ref) { + # do something with @$subkeys_ref and @$values_ref + for (my $i = 0; $i < @$values_ref; $i++) { + print $values_ref->[$i]->as_string, "\n"; + } + ... + } + else { + # do something with @$subkeys_ref + my $first_defined_subkey = (grep { defined } @$subkeys_ref)[0]; + print $first_defined_subkey->as_string, "\n"; + ... + } + } + +=back + +=head2 Comparing Keys and Values + +These functions are exported on request: + + use Parse::Win32Registry qw( compare_multiple_keys + compare_multiple_values ); + +=over 4 + +=item @changes = compare_multiple_keys( $key1, $key2, ... ); + +Returns a list of strings +describing the differences found between the supplied keys. +The keys are compared in the order they are supplied. +If one of the supplied keys is undefined, +it is assumed to have been deleted. + +The possible changes are 'ADDED', and 'DELETED', +and for Windows NT registry keys (which have timestamps) +'NEWER', and 'OLDER'. + +For example, compare_multiple_keys($k1, $k2, $k3) +would return the list ('', 'NEWER', '') +if $k2 had a more recent timestamp than $k1, +but $k3 had the same timestamp as $k2. + +You can count the number of changed keys using the grep operator: + + my $num_changes = grep { $_ } @changes; + +=item @changes = compare_multiple_values( $value1, $value2, ... ); + +Returns a list of strings +describing the differences found between the supplied values. +The values are compared in the order they are supplied. +If one of the supplied values is undefined, +it is assumed to have been deleted. + +The possible changes are 'ADDED', 'DELETED', and 'CHANGED'. + +For example, compare_multiple_keys($v1, $v2, $v3) +would return the list ('', 'ADDED', 'CHANGED') +if $v2 exists but $v1 did not, +and $v3 had different data from $v2. + +You can count the number of changed values using the grep operator: + + my $num_changes = grep { $_ } @changes; + +=back + +=head1 HANDLING INVALID DATA + +The Parse::Win32Registry module will skip keys or values +that cannot be successfully parsed. + +If keys or values cannot be parsed, then +the get_subkey and get_value methods of Key objects +will return nothing. + +The get_list_of_subkeys and get_list_of_values methods of Key objects +will skip those keys or values that cannot be parsed. +If none of the keys or values can be parsed successfully, +an empty list will be returned. + +Additionally, values (in Windows NT registry files) +often store data in a separate area of the registry file. +If the value can be parsed, but the data cannot, +a Value object will be created, +but it will have no data. +The get_data method will return nothing. + +The most robust way of handling keys or values or data +is therefore to check that they are defined before processing them. +For example: + + my $key = $root_key->get_subkey( "Software\\Perl" ); + if ( defined $key ) { + print $key->as_string, "\n"; + my $value = $key->get_value( "Version" ); + if ( defined $value ) { + print $value->as_string, "\n"; + my $data = $value->get_data; + if ( defined $data ) { + # process $data in some way... + } + } + } + +You might not feel this robustness is necessary for your scripts. + +You can be alerted when there are problems parsing registry keys or values +by switching on warnings with: + + Parse::Win32Registry->enable_warnings; + +They can be switched off again with: + + Parse::Win32Registry->disable_warnings; + +=head1 LOW-LEVEL METHODS + +These methods are intended for those +who want to look at the structure of a registry file, +but with something a little more helpful than a hex editor. +They are not designed for pulling data out of keys and values: +they are designed to make it easier to look at the underlying +components of a registry file. + +Windows NT registry files are composed of one or more Hbin blocks. +Hbin blocks can contain a series of entries, +such as key, value, and security entries, +but also includes subkey lists, value lists, key class names, and value data. + +Windows 95 registry files are composed of an RGKN block, +followed by one or more RGDB blocks. +RGKN blocks contain the entries which link the registry keys +in the form of a tree. +RGDB blocks contain a corresponding entry for each key in the RGKN block. +This RGDB entry includes the name of the key and any associated values. +For convenience, when iterating the entries in an RGDB block, +each will be returned as a key entry followed by zero or more value entries. + +To see demonstrations of how these methods can be used, +look at the regscan.pl, gtkregscope.pl, and wxregscope.pl scripts. + +=head2 Registry Object Methods + +=over 4 + +=item $registry->get_block_iterator + +Returns an iterator for retrieving all the blocks in a registry file. +Each time the get_next method of the iterator is used, +it will return a single Block object. +Blocks will be returned one by one +until the end of the registry file is reached, +when nothing will be returned. + +Typically you would iterate over all the blocks in a registry file, +and iterate over all the entries in each block: + + my $block_iter = $registry->get_block_iterator; + while (my $block = $block_iter->get_next) { + my $entry_iter = $block->get_entry_iterator; + while (my $entry = $entry_iter->get_next) { + ... + } + } + +=item $registry->get_entry_iterator + +Returns an iterator for retrieving all the entries in a registry file. +Each time the get_next method of the iterator is used, +it will return a single Entry object. +Entries will be returned one by one +until the end of the registry file is reached, +when nothing will be returned. + +This is simply a more convenient method for retrieving +all the entries in a registry file, +which does not require you to iterate over each block. + +=back + +=head2 Block Object Methods + +=over 4 + +=item $block->get_entry_iterator + +Returns an iterator for retrieving all the entries in a block. +Each time the get_next method of the iterator is used, +it will return a single Entry object. +Entries will be returned one by one +until the end of the block is reached, +when nothing will be returned. + + my $entry_iter = $block->get_entry_iterator; + while (my $entry = $entry_iter->get_next) { + ... + } + +=item $block->get_offset + +Returns the position of the block relative to the start of the file. + +=item $block->get_length + +Returns the length of the block. + +=item $block->parse_info + +Returns a string containing a summary of the parser information +for the block. + +=item $block->unparsed + +Returns a string containing a hex dump +of the unparsed on-disk data for the block header. + +=item $block->get_raw_bytes + +Returns the unparsed on-disk data for the block header. + +=back + +=head2 Entry Object Methods + +In addition to the basic methods provided by all entries, +if an entry is a key, value, or security entry, +it will also provide the methods available to +Key, Value, or Security objects. +You might therefore find it useful to check what methods +are available so that you can use them: + + # use Entry object methods... + ... + if ($entry->can('get_subkey')) { + # use Key object methods... + } + elsif ($entry->can('get_data')) { + # use Value object methods... + } + elsif ($entry->can('get_security_descriptor')) { + # use Security object methods... + } + +=over 4 + +=item $entry->get_offset + +Returns the position of the entry relative to the start of the file. + +=item $entry->get_length + +Returns the length of the entry. + +=item $entry->get_tag + +Returns a string containing a descriptive tag for the entry. + +For Windows NT registry entries, the tags reflect the +signatures used to identify them. +These are: +'nk' for keys; +'vk' for values; +'sk' for security entries; +and 'lf', 'lh', 'li', or 'ri' for subkey lists. +Entries that do not have signatures will return an empty string. +Unidentified entries include +value lists, value data, and the class names of keys. + +For Windows 95 registry files, the tag +reflects which part of the registry file the entry is from, +and will be +'rgkn key', +'rgdb key', +or 'rgdb value'. + +=item $entry->is_allocated + +Returns a boolean value indicating the 'allocated' state of a +Windows NT registry entry. + +This value has no meaning for Windows 95 registry entries. + +=item $entry->as_string + +Returns a string representation of the entry. + +If the entry is a valid Key, Value, or Security object, +then as_string will call the as_string method of that object. + +=item $entry->parse_info + +Returns a string containing a summary of the parser information +for that entry. + +If the entry is a valid Key, Value, or Security object, +then parse_info will call the parse_info method of that object. + +=item $entry->unparsed + +Returns a string containing a hex dump +of the unparsed on-disk data for the entry. + +=item $entry->get_raw_bytes + +Returns the unparsed on-disk data for the entry. + +=back + +=head1 SCRIPTS + +All of the supplied scripts are intended to be used either as tools +or as examples for you to modify and develop. + +Try regdump.pl or regshell.pl to look at a registry file +from the command line, +or gtkregview.pl or wxregview.pl if you want a GUI. +If you want to compare registry files, +try regmultidiff.pl from the command line +or gtkregcompare.pl or wxregcompare.pl if you want a GUI. +You can edit the scripts to customize them for your own requirements. + +If you specify subkeys on the command line, note that you need to +quote the subkey on Windows if it contains spaces: + + regdump.pl ntuser.dat "software\microsoft\windows nt" + +You will also need to quote backslashes and spaces in Unix shells: + + regdump.pl ntuser.dat software\\microsoft\\windows\ nt + +or use single quotes: + + regdump.pl ntuser.dat 'software\microsoft\windows nt' + +=head2 gtkregcompare.pl + +gtkregcompare.pl is a GTK+ program for comparing multiple registry files. +It displays a tree of the registry keys and values +highlighting those that have changed. + +It requires Gtk2-Perl to be installed. + +Filenames of registry files to compare can be supplied on the command line: + + gtkregcompare.pl ... + +You can of course use wildcards when running from a Unix shell. + +=head2 gtkregscope.pl + +gtkregscope.pl is a GTK+ registry scanner. +It presents all the entries in a registry file returned by the +get_block_iterator and get_entry_iterator methods. +It uses color to highlight key, value, security, and subkey list entries, +and presents the block as a colored map. + +It requires Gtk2-Perl to be installed. + +A filename can also be supplied on the command line: + + gtkregscope.pl + +=head2 gtkregview.pl + +gtkregview.pl is a GTK+ registry viewer. +It displays a tree of registry keys on the left hand side, +a list of values on the right, +and a hex dump of the selected value data at the bottom. + +It requires Gtk2-Perl to be installed. + +A filename can also be supplied on the command line: + + gtkregview.pl + +=head2 regclassnames.pl + +regclassnames.pl will display registry keys that have class names. +Only a very few Windows NT registry key have class names. + +Type regclassnames.pl on its own to see the help: + + regclassnames.pl [subkey] + +=head2 regdump.pl + +regdump.pl is used to display the keys and values of a registry file. + +Type regdump.pl on its own to see the help: + + regdump.pl [subkey] [-r] [-v] [-x] [-c] [-s] [-o] + -r or --recurse traverse all child keys from the root key + or the subkey specified + -v or --values display values + -x or --hexdump display value data as a hex dump + -c or --class-name display the class name for the key (if present) + -s or --security display the security information for the key, + including the owner and group SIDs, + and the system and discretionary ACLs (if present) + -o or --owner display the owner SID for the key (if present) + +The contents of the root key will be displayed unless a subkey is +specified. Paths to subkeys are always specified relative to the root +key. By default, only the subkeys and values immediately underneath +the specified key will be displayed. To display all keys and values +beneath a key, use the -r or --recurse option. + +For example, regdump.pl ntuser.dat might display the following: + + $$$PROTO.HIV [2005-01-01T09:00:00Z] + ..\AppEvents + ..\Console + ..\Control Panel + ..\Environment + ..\Identities + ..\Keyboard Layout + ..\Printers + ..\Software + ..\UNICODE Program Groups + +From here, you can explore the subkeys to find those keys or values +you are interested in: + + regdump.pl ntuser.dat software + regdump.pl ntuser.dat software\microsoft + ... + +=head2 regexport.pl + +regexport.pl will display registry keys and values +in the Windows Registry Editor Version 5.00 format +used by REGEDIT on Windows 2000 and later. + +Type regexport.pl on its own to see the help: + + regexport.pl [subkey] [-r] + -r or --recurse traverse all child keys from the root key + or the subkey specified + +Values are always shown for each key displayed. + +Subkeys are displayed as comments when not recursing. +(Comments are preceded by the ';' character.) + +=head2 regfind.pl + +regfind.pl is used to search the keys, values, data, or types +of a registry file for a matching string. + +Type regfind.pl on its own to see the help: + + regfind.pl [-k] [-v] [-d] [-t] [-x] + -k or --key search key names for a match + -v or --value search value names for a match + -d or --data search value data for a match + -t or --type search value types for a match + -x or --hexdump display value data as a hex dump + +To search for the string "recent" in the names of any keys or values: + + regfind.pl ntuser.dat recent -kv + +To search for the string "administrator" in the data of any values: + + regfind.pl ntuser.dat administrator -d + +To list all REG_MULTI_SZ values: + + regfind.pl ntuser.dat -t multi_sz + +Search strings are not case-sensitive. + +=head2 regml.pl + +regml.pl will display those keys with explicit System Mandatory Label ACEs +set in the System ACL. +This feature was introduced with Windows Vista, and is used by applications +such as Internet Explorer running in Protected Mode. +Note that if a key does not have an explicit System Mandatory Label ACE, +it has Medium Integrity Level. +Only Windows NT registry files can contain System Mandatory Label ACEs. + +Type regml.pl on its own to see the help: + + regml.pl + +=head2 regmultidiff.pl + +regmultidiff.pl can be used to compare multiple registry files +and identify the differences between them. + +Type regmultidiff.pl on its own to see the help: + + regmultidiff.pl ... [] [-v] [-x] [-l] [-a] + -v or --values display values + -x or --hexdump display value data as a hex dump + -l or --long show each changed key or value instead of a summary + -a or --all show all keys and values before and after a change + +You can limit the comparison by specifying an initial subkey. + +=head2 regscan.pl + +regscan.pl dumps all the entries in a registry file. +This will include defunct keys and values that are no longer part +of the current active registry. + +Type regscan.pl on its own to see the help: + + regscan.pl [-k] [-v] [-s] [-a] [-p] [-u] [-w] + -k or --keys list only 'key' entries + -v or --values list only 'value' entries + -s or --security list only 'security' entries + -a or --allocated list only 'allocated' entries + -p or --parse-info show the technical information for an entry + instead of the string representation + -u or --unparsed show the unparsed on-disk entries as a hex dump + +=head2 regsecurity.pl + +regsecurity.pl will display the security information +contained in a registry files. +Only Windows NT registry files contain security information. + +Type regsecurity.pl on its own to see the help: + + regsecurity.pl + +=head2 regshell.pl + +Provides an interactive command shell +where you navigate through the keys +using 'cd' to change the current key +and 'ls' or 'dir' to list the contents of the current key. + +Tab completion of subkey and value names is available. +Names containing spaces are supported by quoting names with " characters. +Note that names are case sensitive. + +A filename should be supplied on the command line: + + regshell.pl + +Once regshell.pl is running, type help to see the available commands. + +It requires Term::ReadLine to be installed. + +=head2 regstats.pl + +regstats.pl counts the number of keys and values in a registry file. +It will also provide a count of each value type if requested. + +Type regstats.pl on its own to see the help: + + regstats.pl [-t] + -t or --types count value types + +=head2 regtimeline.pl + +regtimeline.pl displays keys and values in date order. + +As only Windows NT based registry keys provide timestamps, +this script only works on Windows NT registry files. + +You can limit the display to a given number of days +(counting back from the timestamp of the last key). + +Type regtimeline.pl on its own to see the help: + + regtimeline.pl [subkey] [-l ] [-v] [-x] + -l or --last display only the last days + of registry activity + -v or --values display values + -x or --hexdump display value data as a hex dump + +=head2 regtree.pl + +regtree.pl simply displays the registry as an indented tree, +optionally displaying the values of each key. + +Type regtree.pl on its own to see the help: + + regtree.pl [subkey] [-v] + -v or --values display values + +=head2 wxregcompare.pl + +wxregcompare.pl is a wxWidgets program for comparing multiple registry files. +It displays a tree of the registry keys and values, +highlighting those that have changed. + +It requires wxPerl to be installed. + +Filenames of registry files to compare can be supplied on the command line: + + wxregcompare.pl ... + +You can of course use wildcards when running from a Unix shell. + +=head2 wxregscope.pl + +wxregscope.pl is a wxWidgets registry scanner. +It presents all the entries in a registry file returned by the +get_block_iterator and get_entry_iterator methods. +It uses color to highlight key, value, security, and subkey list entries. + +It requires wxPerl to be installed. + +A filename can also be supplied on the command line: + + wxregscope.pl + +=head2 wxregview.pl + +wxregview.pl is a wxWidgets registry viewer. +It displays a tree of registry keys on the left hand side, +a list of values on the right, +and a hex dump of the selected value data at the bottom. +It can also provide a timeline view of all of the registry keys, +which can be used to navigate the main tree view +by clicking or double-clicking on a timeline key. + +It requires wxPerl to be installed. + +A filename can also be supplied on the command line: + + wxregview.pl + +=head1 ACKNOWLEDGEMENTS + +This would not have been possible without the work of those people who have +analysed and shared their knowledge of the structure of Windows Registry files, +primarily: +B.D. (WinReg.txt), +Petter Nordahl-Hagen (chntpw), +and Richard Sharpe and Jerry Carter (Samba 3). + +=head1 AUTHOR + +James Macfarlane, Ejmacfarla@cpan.orgE + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2006-2012 by James Macfarlane + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, +THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS +FOR A PARTICULAR PURPOSE. + +=cut diff --git a/thirdparty/rr/Parse/Win32Registry/Base.pm b/thirdparty/rr/Parse/Win32Registry/Base.pm new file mode 100644 index 0000000000..6598f37b11 --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/Base.pm @@ -0,0 +1,1107 @@ +package Parse::Win32Registry::Base; + +use strict; +use warnings; + +use base qw(Exporter); + +use Carp; +use Encode; +use Time::Local qw(timegm); + +our @EXPORT_OK = qw( + warnf + iso8601 + hexdump + format_octets + unpack_windows_time + unpack_string + unpack_unicode_string + unpack_guid + unpack_sid + unpack_ace + unpack_acl + unpack_security_descriptor + unpack_series + make_multiple_subkey_iterator + make_multiple_value_iterator + make_multiple_subtree_iterator + compare_multiple_keys + compare_multiple_values + REG_NONE + REG_SZ + REG_EXPAND_SZ + REG_BINARY + REG_DWORD + REG_DWORD_BIG_ENDIAN + REG_LINK + REG_MULTI_SZ + REG_RESOURCE_LIST + REG_FULL_RESOURCE_DESCRIPTOR + REG_RESOURCE_REQUIREMENTS_LIST + REG_QWORD +); + +our %EXPORT_TAGS = ( + all => [@EXPORT_OK], +); + +use constant REG_NONE => 0; +use constant REG_SZ => 1; +use constant REG_EXPAND_SZ => 2; +use constant REG_BINARY => 3; +use constant REG_DWORD => 4; +use constant REG_DWORD_BIG_ENDIAN => 5; +use constant REG_LINK => 6; +use constant REG_MULTI_SZ => 7; +use constant REG_RESOURCE_LIST => 8; +use constant REG_FULL_RESOURCE_DESCRIPTOR => 9; +use constant REG_RESOURCE_REQUIREMENTS_LIST => 10; +use constant REG_QWORD => 11; + +our $WARNINGS = 0; + +our $CODEPAGE = 'cp1252'; + +sub warnf { + my $message = shift; + warn sprintf "$message\n", @_ if $WARNINGS; +} + +sub hexdump { + my $data = shift; # packed binary data + my $start = shift || 0; # starting value for displayed offset + + return '' if !defined($data); + + my $output = ''; + + my $fake_start = $start & ~0xf; + my $end = length($data); + + my $pos = 0; + if ($fake_start < $start) { + $output .= sprintf '%8x ', $fake_start; + my $indent = $start - $fake_start; + $output .= ' ' x $indent; + my $row = substr($data, $pos, 16 - $indent); + my $len = length($row); + $output .= join(' ', unpack('H2' x $len, $row)); + if ($indent + $len < 16) { + my $padding = 16 - $len - $indent; + $output .= ' ' x $padding; + } + $output .= ' '; + $output .= ' ' x $indent; + $row =~ tr/\x20-\x7e/./c; + $output .= $row; + $output .= "\n"; + $pos += $len; + } + while ($pos < $end) { + $output .= sprintf '%8x ', $start + $pos; + my $row = substr($data, $pos, 16); + my $len = length($row); + $output .= join(' ', unpack('H2' x $len, $row)); + if ($len < 16) { + my $padding = 16 - $len; + $output .= ' ' x $padding; + } + $output .= ' '; + $row =~ tr/\x20-\x7e/./c; + $output .= $row; + $output .= "\n"; + $pos += 16; + } + + return $output; +} + +sub format_octets { + my $data = shift; # packed binary data + my $col = shift || 0; # starting column, e.g. length of initial string + + return "\n" if !defined($data); + + my $output = ''; + + $col = 76 if $col > 76; + my $max_octets = int((76 - $col) / 3) + 1; + + my $end = length($data); + my $pos = 0; + my $num_octets = $end - $pos; + $num_octets = $max_octets if $num_octets > $max_octets; + while ($pos < $end) { + $output .= join(',', unpack("x$pos(H2)$num_octets", $data)); + $pos += $num_octets; + $num_octets = $end - $pos; + $num_octets = 25 if $num_octets > 25; + if ($num_octets > 0) { + $output .= ",\\\n "; + } + } + $output .= "\n"; + return $output; +} + +sub unpack_windows_time { + my $data = shift; + + if (!defined $data) { + return; + } + + if (length($data) < 8) { + return; + } + + # The conversion uses real numbers + # as 32-bit perl does not provide 64-bit integers. + # The equation can be found in several places on the Net. + # My thanks go to Dan Sully for Audio::WMA's _fileTimeToUnixTime + # which shows a perl implementation of it. + my ($low, $high) = unpack("VV", $data); + my $filetime = $high * 2 ** 32 + $low; + my $epoch_time = int(($filetime - 116444736000000000) / 10000000); + + # adjust the UNIX epoch time to the local OS's epoch time + # (see perlport's Time and Date section) + my $epoch_offset = timegm(0, 0, 0, 1, 0, 70); + $epoch_time += $epoch_offset; + + if ($epoch_time < 0 || $epoch_time > 0x7fffffff) { + $epoch_time = undef; + } + + return wantarray ? ($epoch_time, 8) : $epoch_time; +} + +sub iso8601 { + my $time = shift; + my $tz = shift; + + if (!defined $time) { + return '(undefined)'; + } + + if (!defined $tz || $tz ne 'Z') { + $tz = 'Z' + } + + # On Windows, gmtime will return undef if $time < 0 or > 0x7fffffff + if ($time < 0 || $time > 0x7fffffff) { + return '(undefined)'; + } + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime $time; + + # The final 'Z' indicates UTC ("zero meridian") + return sprintf '%04d-%02d-%02dT%02d:%02d:%02d%s', + 1900+$year, 1+$mon, $mday, $hour, $min, $sec, $tz; +} + +sub unpack_string { + my $data = shift; + + if (!defined $data) { + return; + } + + my $str; + my $str_len; + if ((my $end = index($data, "\0")) != -1) { + $str = substr($data, 0, $end); + $str_len = $end + 1; # include the final null in the length + } + else { + $str = $data; + $str_len = length($data); + } + + return wantarray ? ($str, $str_len) : $str; +} + +sub unpack_unicode_string { + my $data = shift; + + if (!defined $data) { + return; + } + + my $str_len = 0; + foreach my $v (unpack('v*', $data)) { + $str_len += 2; + last if $v == 0; # include the final null in the length + } + my $str = decode('UCS-2LE', substr($data, 0, $str_len)); + + # The decode function from Encode may create invalid unicode characters + # which cause subsequent warnings (e.g. during regex matching). + # For example, characters in the 0xd800 to 0xdfff range of the + # basic multilingual plane (0x0000 to 0xffff) are 'surrogate pairs' + # and are expected to appear as a 'high surrogate' (0xd800 to 0xdbff) + # followed by a 'low surrogate' (0xdc00 to 0xdfff). + + # remove any final null + if (length($str) > 0 && substr($str, -1, 1) eq "\0") { + chop $str; + } + + return wantarray ? ($str, $str_len) : $str; +} + +sub unpack_guid { + my $guid = Parse::Win32Registry::GUID->new($_[0]); + return if !defined $guid; + return wantarray ? ($guid, $guid->get_length) : $guid; +} + +sub unpack_sid { + my $sid = Parse::Win32Registry::SID->new($_[0]); + return if !defined $sid; + return wantarray ? ($sid, $sid->get_length) : $sid; +} + +sub unpack_ace { + my $ace = Parse::Win32Registry::ACE->new($_[0]); + return if !defined $ace; + return wantarray ? ($ace, $ace->get_length) : $ace; +} + +sub unpack_acl { + my $acl = Parse::Win32Registry::ACL->new($_[0]); + return if !defined $acl; + return wantarray ? ($acl, $acl->get_length) : $acl; +} + +sub unpack_security_descriptor { + my $sd = Parse::Win32Registry::SecurityDescriptor->new($_[0]); + return if !defined $sd; + return wantarray ? ($sd, $sd->get_length) : $sd; +} + +sub unpack_series { + my $function = shift; + my $data = shift; + + if (!defined $function || !defined $data) { + croak "Usage: unpack_series(\\\&unpack_function, \$data)"; + } + + my $pos = 0; + my @items = (); + while (my ($item, $item_len) = $function->(substr($data, $pos))) { + push @items, $item; + $pos += $item_len; + } + return @items; +} + +sub make_multiple_subkey_iterator { + my @keys = @_; + + # check @keys contains keys + if (@keys == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Key') } + @keys) { + croak 'Usage: make_multiple_subkey_iterator($key1, $key2, ...)'; + } + + my %subkeys_seen = (); + my @subkeys_queue; + for (my $i = 0; $i < @keys; $i++) { + my $key = $keys[$i]; + next if !defined $key; + foreach my $subkey ($key->get_list_of_subkeys) { + my $name = $subkey->get_name; + $subkeys_seen{$name}[$i] = $subkey; + } + } + foreach my $name (sort keys %subkeys_seen) { + # make sure number of subkeys matches number of keys + if (@{$subkeys_seen{$name}} != @keys) { + @{$subkeys_seen{$name}}[@keys - 1] = undef; + } + push @subkeys_queue, $subkeys_seen{$name}; + } + + return Parse::Win32Registry::Iterator->new(sub { + my $subkeys = shift @subkeys_queue; + if (defined $subkeys) { + return $subkeys; + } + else { + return; + } + }); +} + +sub make_multiple_value_iterator { + my @keys = @_; + + # check @keys contains keys + if (@keys == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Key') } + @keys) { + croak 'Usage: make_multiple_value_iterator($key1, $key2, ...)'; + } + + my %values_seen = (); + my @values_queue; + for (my $i = 0; $i < @keys; $i++) { + my $key = $keys[$i]; + next if !defined $key; + foreach my $value ($key->get_list_of_values) { + my $name = $value->get_name; + $values_seen{$name}[$i] = $value; + } + } + foreach my $name (sort keys %values_seen) { + # make sure number of values matches number of keys + if (@{$values_seen{$name}} != @keys) { + @{$values_seen{$name}}[@keys - 1] = undef; + } + push @values_queue, $values_seen{$name}; + } + + return Parse::Win32Registry::Iterator->new(sub { + my $values = shift @values_queue; + if (defined $values) { + return $values; + } + else { + return; + } + }); +} + +sub make_multiple_subtree_iterator { + my @keys = @_; + + # check @keys contains keys + if (@keys == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Key') } + @keys) { + croak 'Usage: make_multiple_subtree_iterator($key1, $key2, ...)'; + } + + my @start_keys = (\@keys); + push my (@subkey_iters), Parse::Win32Registry::Iterator->new(sub { + return shift @start_keys; + }); + my $value_iter; + my $subkeys; # used to remember subkeys while iterating values + + return Parse::Win32Registry::Iterator->new(sub { + if (defined $value_iter && wantarray) { + my $values = $value_iter->(); + if (defined $values) { + return ($subkeys, $values); + } + } + while (@subkey_iters > 0) { + $subkeys = $subkey_iters[-1]->(); # depth-first + if (defined $subkeys) { + push @subkey_iters, make_multiple_subkey_iterator(@$subkeys); + $value_iter = make_multiple_value_iterator(@$subkeys); + return $subkeys; + } + pop @subkey_iters; # iter finished, so remove it + } + return; + }); +} + +sub compare_multiple_keys { + my @keys = @_; + + # check @keys contains keys + if (@keys == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Key') } + @keys) { + croak 'Usage: compare_multiple_keys($key1, $key2, ...)'; + } + + my @changes = (); + + my $benchmark_key; + foreach my $key (@keys) { + my $diff = ''; + # Skip comparison for the first value + if (@changes > 0) { + $diff = _compare_keys($benchmark_key, $key); + } + $benchmark_key = $key; + push @changes, $diff; + } + return @changes; +} + +sub compare_multiple_values { + my @values = @_; + + # check @values contains values + if (@values == 0 || + grep { defined && !UNIVERSAL::isa($_, 'Parse::Win32Registry::Value') } + @values) { + croak 'Usage: compare_multiple_values($value1, $value2, ...)'; + } + + my @changes = (); + + my $benchmark_value; + foreach my $value (@values) { + my $diff = ''; + # Skip comparison for the first value + if (@changes > 0) { + $diff = _compare_values($benchmark_value, $value); + } + $benchmark_value = $value; + push @changes, $diff; + } + return @changes; +} + +sub _compare_keys { + my ($key1, $key2) = @_; + + if (!defined $key1 && !defined $key2) { + return ''; # 'MISSING' + } + elsif (defined $key1 && !defined $key2) { + return 'DELETED'; + } + elsif (!defined $key1 && defined $key2) { + return 'ADDED'; + } + + my $timestamp1 = $key1->get_timestamp; + my $timestamp2 = $key2->get_timestamp; + if ($key1->get_name ne $key2->get_name) { + return 'CHANGED'; + } + elsif (defined $timestamp1 && defined $timestamp2) { + if ($timestamp1 < $timestamp2) { + return 'NEWER'; + } + elsif ($timestamp1 > $timestamp2) { + return 'OLDER'; + } + } + else { + return ''; # comment out to check values... + my $value_iter = make_multiple_value_iterator($key1, $key2); + while (my ($val1, $val2) = $value_iter->get_next) { + if (_compare_values($val1, $val2) ne '') { + return 'VALUES'; + } + } + return ''; + } +} + +sub _compare_values { + my ($val1, $val2) = @_; + + if (!defined $val1 && !defined $val2) { + return ''; # 'MISSING' + } + elsif (defined $val1 && !defined $val2) { + return 'DELETED'; + } + elsif (!defined $val1 && defined $val2) { + return 'ADDED'; + } + + my $data1 = $val1->get_data; + my $data2 = $val2->get_data; + if ($val1->get_name ne $val2->get_name || + $val1->get_type != $val2->get_type || + defined $data1 ne defined $data2 || + (defined $data1 && defined $data2 && $data1 ne $data2)) { + return 'CHANGED'; + } + else { + return ''; + } +} + + +package Parse::Win32Registry::Iterator; + +use Carp; + +sub new { + my $class = shift; + my $self = shift; + + my $type = ref $self; + croak 'Missing iterator subroutine' if $type ne 'CODE' + && $type ne __PACKAGE__; + + bless $self, $class; + return $self; +} + +sub get_next { + $_[0]->(); +} + + +package Parse::Win32Registry::GUID; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + if (length($data) < 16) { + return; + } + + my $guid = sprintf '{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}', + unpack('VvvC2C6', $data); + + my $self = { + _guid => $guid, + _length => 16, + }; + bless $self, $class; + + return $self; +} + +sub as_string { + my $self = shift; + + return $self->{_guid}; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + + +package Parse::Win32Registry::SID; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + # 0x00 byte = revision + # 0x01 byte = number of sub authorities + # 0x07 byte = identifier authority + # 0x08 dword = 1st sub authority + # 0x0c dword = 2nd sub authority + # ... + + if (length($data) < 8) { + return; + } + + my ($rev, $num_sub_auths, $id_auth) = unpack('CCx5C', $data); + + if ($num_sub_auths == 0) { + return; + } + + my $sid_len = 8 + 4 * $num_sub_auths; + + if (length($data) < $sid_len) { + return; + } + + my @sub_auths = unpack("x8V$num_sub_auths", $data); + my $sid = "S-$rev-$id_auth-" . join('-', @sub_auths); + + my $self = { + _sid => $sid, + _length => $sid_len, + }; + bless $self, $class; + + return $self; +} + +# See KB243330 for a list of well known sids +our %WellKnownSids = ( + 'S-1-0-0' => 'Nobody', + 'S-1-1-0' => 'Everyone', + 'S-1-3-0' => 'Creator Owner', + 'S-1-3-1' => 'Creator Group', + 'S-1-3-2' => 'Creator Owner Server', + 'S-1-3-3' => 'Creator Group Server', + 'S-1-5-1' => 'Dialup', + 'S-1-5-2' => 'Network', + 'S-1-5-3' => 'Batch', + 'S-1-5-4' => 'Interactive', + 'S-1-5-5-\\d+-\\d+' => 'Logon Session', + 'S-1-5-6' => 'Service', + 'S-1-5-7' => 'Anonymous', + 'S-1-5-8' => 'Proxy', + 'S-1-5-9' => 'Enterprise Domain Controllers', + 'S-1-5-10' => 'Principal Self', + 'S-1-5-11' => 'Authenticated Users', + 'S-1-5-12' => 'Restricted Code', + 'S-1-5-13' => 'Terminal Server Users', + 'S-1-5-18' => 'Local System', + 'S-1-5-19' => 'Local Service', + 'S-1-5-20' => 'Network Service', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-500' => 'Administrator', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-501' => 'Guest', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-502' => 'KRBTGT', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-512' => 'Domain Admins', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-513' => 'Domain Users', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-514' => 'Domain Guests', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-515' => 'Domain Computers', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-516' => 'Domain Controllers', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-517' => 'Cert Publishers', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-518' => 'Schema Admins', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-519' => 'Enterprise Admins', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-520' => 'Group Policy Creator Owners', + 'S-1-5-\\d+-\\d+-\\d+-\\d+-533' => 'RAS and IAS Servers', + 'S-1-5-32-544' => 'Administrators', + 'S-1-5-32-545' => 'Users', + 'S-1-5-32-546' => 'Guest', + 'S-1-5-32-547' => 'Power Users', + 'S-1-5-32-548' => 'Account Operators', + 'S-1-5-32-549' => 'Server Operators', + 'S-1-5-32-550' => 'Print Operators', + 'S-1-5-32-551' => 'Backup Operators', + 'S-1-5-32-552' => 'Replicators', + 'S-1-16-4096' => 'Low Integrity Level', + 'S-1-16-8192' => 'Medium Integrity Level', + 'S-1-16-12288' => 'High Integrity Level', + 'S-1-16-16384' => 'System Integrity Level', +); + +sub get_name { + my $self = shift; + + my $sid = $self->{_sid}; + + foreach my $regexp (keys %WellKnownSids) { + if ($sid =~ m/^$regexp$/) { + return $WellKnownSids{$regexp}; + } + } + return; +} + +sub as_string { + my $self = shift; + + return $self->{_sid}; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + + +package Parse::Win32Registry::ACE; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + # 0x00 byte = type + # 0x01 byte = flags + # 0x02 word = length + + # Types: + # ACCESS_ALLOWED_ACE_TYPE = 0 + # ACCESS_DENIED_ACE_TYPE = 1 + # SYSTEM_AUDIT_ACE_TYPE = 2 + # SYSTEM_MANDATORY_LABEL_ACE_TYPE = x011 + + # Flags: + # OBJECT_INHERIT_ACE = 0x01 + # CONTAINER_INHERIT_ACE = 0x02 + # NO_PROPAGATE_INHERIT_ACE = 0x04 + # INHERIT_ONLY_ACE = 0x08 + # INHERITED_ACE = 0x10 + # SUCCESSFUL_ACCESS_ACE_FLAG = 0x40 (Audit Success) + # FAILED_ACCESS_ACE_FLAG = 0x80 (Audit Failure) + + if (length($data) < 4) { + return; + } + + my ($type, $flags, $ace_len) = unpack('CCv', $data); + + if (length($data) < $ace_len) { + return; + } + + # The data following the header varies depending on the type. + # For ACCESS_ALLOWED_ACE, ACCESS_DENIED_ACE, SYSTEM_AUDIT_ACE + # the header is followed by an access mask and a sid. + # 0x04 dword = access mask + # 0x08 = SID + + # Only the following types are currently unpacked: + # 0 (ACCESS_ALLOWED_ACE), 1 (ACCESS_DENIED_ACE), 2 (SYSTEM_AUDIT_ACE) + if ($type >= 0 && $type <= 2 || $type == 0x11) { + my $access_mask = unpack('x4V', $data); + my $sid = Parse::Win32Registry::SID->new(substr($data, 8, + $ace_len - 8)); + + # Abandon ace if sid is invalid + if (!defined $sid) { + return; + } + + # Abandon ace if not the expected length + if (($sid->get_length + 8) != $ace_len) { + return; + } + + my $self = { + _type => $type, + _flags => $flags, + _mask => $access_mask, + _trustee => $sid, + _length => $ace_len, + }; + bless $self, $class; + + return $self; + } + else { + return; + } +} + +our @Types = qw( + ACCESS_ALLOWED + ACCESS_DENIED + SYSTEM_AUDIT + SYSTEM_ALARM + ALLOWED_COMPOUND + ACCESS_ALLOWED_OBJECT + ACCESS_DENIED_OBJECT + SYSTEM_AUDIT_OBJECT + SYSTEM_ALARM_OBJECT + ACCESS_ALLOWED_CALLBACK + ACCESS_DENIED_CALLBACK + ACCESS_ALLOWED_CALLBACK_OBJECT + ACCESS_DENIED_CALLBACK_OBJECT + SYSTEM_AUDIT_CALLBACK + SYSTEM_ALARM_CALLBACK + SYSTEM_AUDIT_CALLBACK_OBJECT + SYSTEM_ALARM_CALLBACK_OBJECT + SYSTEM_MANDATORY_LABEL +); + +sub _look_up_ace_type { + my $type = shift; + + if (exists $Types[$type]) { + return $Types[$type]; + } + else { + return ''; + } +} + +sub get_type { + return $_[0]->{_type}; +} + +sub get_type_as_string { + return _look_up_ace_type($_[0]->{_type}); +} + +sub get_flags { + return $_[0]->{_flags}; +} + +sub get_access_mask { + return $_[0]->{_mask}; +} + +sub get_trustee { + return $_[0]->{_trustee}; +} + +sub as_string { + my $self = shift; + + my $sid = $self->{_trustee}; + my $string = sprintf '%s 0x%02x 0x%08x %s', + _look_up_ace_type($self->{_type}), + $self->{_flags}, + $self->{_mask}, + $sid->as_string; + my $name = $sid->get_name; + $string .= " [$name]" if defined $name; + return $string; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + + +package Parse::Win32Registry::ACL; + +use Carp; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + # 0x00 byte = revision + # 0x01 + # 0x02 word = length + # 0x04 word = number of aces + # 0x06 + # 0x08 = first ace (variable length) + # ... = second ace (variable length) + # ... + + if (length($data) < 8) { + return; + } + + my ($rev, $acl_len, $num_aces) = unpack('Cxvv', $data); + + if (length($data) < $acl_len) { + return; + } + + my $pos = 8; + my @acl = (); + foreach (my $num = 0; $num < $num_aces; $num++) { + my $ace = Parse::Win32Registry::ACE->new(substr($data, $pos, + $acl_len - $pos)); + # Abandon acl if any single ace is undefined + return if !defined $ace; + push @acl, $ace; + $pos += $ace->get_length; + } + + # Abandon acl if not expected length, but don't use + # $pos != $acl_len as some acls contain unused space. + if ($pos > $acl_len) { + return; + } + + my $self = { + _acl => \@acl, + _length => $acl_len, + }; + bless $self, $class; + + return $self; +} + +sub get_list_of_aces { + my $self = shift; + + return @{$self->{_acl}}; +} + +sub as_string { + croak 'Usage: ACLs do not have an as_string method; use as_stanza instead'; +} + +sub as_stanza { + my $self = shift; + + my $stanza = ''; + foreach my $ace (@{$self->{_acl}}) { + $stanza .= 'ACE: '. $ace->as_string . "\n"; + } + return $stanza; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + + +package Parse::Win32Registry::SecurityDescriptor; + +use Carp; + +sub new { + my $class = shift; + my $data = shift; + + if (!defined $data) { + return; + } + + # Unpacks "self-relative" security descriptors + + # 0x00 word = revision + # 0x02 word = control flags + # 0x04 dword = offset to owner sid + # 0x08 dword = offset to group sid + # 0x0c dword = offset to sacl + # 0x10 dword = offset to dacl + + # Offsets are relative to the start of the security descriptor + + # Control Flags: + # SE_OWNER_DEFAULTED 0x0001 + # SE_GROUP_DEFAULTED 0x0002 + # SE_DACL_PRESENT 0x0004 + # SE_DACL_DEFAULTED 0x0008 + # SE_SACL_PRESENT 0x0010 + # SE_SACL_DEFAULTED 0x0020 + # SE_DACL_AUTO_INHERIT_REQ 0x0100 + # SE_SACL_AUTO_INHERIT_REQ 0x0200 + # SE_DACL_AUTO_INHERITED 0x0400 + # SE_SACL_AUTO_INHERITED 0x0800 + # SE_DACL_PROTECTED 0x1000 + # SE_SACL_PROTECTED 0x2000 + # SE_RM_CONTROL_VALID 0x4000 + # SE_SELF_RELATIVE 0x8000 + + if (length($data) < 20) { + return; + } + + my ($rev, + $flags, + $offset_to_owner, + $offset_to_group, + $offset_to_sacl, + $offset_to_dacl) = unpack('vvVVVV', $data); + + my %sd = (); + my $sd_len = 20; + + my $self = {}; + if ($offset_to_owner > 0 && $offset_to_owner < length($data)) { + my $owner = Parse::Win32Registry::SID->new(substr($data, + $offset_to_owner)); + return if !defined $owner; + $self->{_owner} = $owner; + if ($offset_to_owner + $owner->get_length > $sd_len) { + $sd_len = $offset_to_owner + $owner->get_length; + } + } + if ($offset_to_group > 0 && $offset_to_group < length($data)) { + my $group = Parse::Win32Registry::SID->new(substr($data, + $offset_to_group)); + return if !defined $group; + $self->{_group} = $group; + if ($offset_to_group + $group->get_length > $sd_len) { + $sd_len = $offset_to_group + $group->get_length; + } + } + if ($offset_to_sacl > 0 && $offset_to_sacl < length($data)) { + my $sacl = Parse::Win32Registry::ACL->new(substr($data, + $offset_to_sacl)); + return if !defined $sacl; + $self->{_sacl} = $sacl; + if ($offset_to_sacl + $sacl->get_length > $sd_len) { + $sd_len = $offset_to_sacl + $sacl->get_length; + } + } + if ($offset_to_dacl > 0 && $offset_to_dacl < length($data)) { + my $dacl = Parse::Win32Registry::ACL->new(substr($data, + $offset_to_dacl)); + return if !defined $dacl; + $self->{_dacl} = $dacl; + if ($offset_to_dacl + $dacl->get_length > $sd_len) { + $sd_len = $offset_to_dacl + $dacl->get_length; + } + } + $self->{_length} = $sd_len; + bless $self, $class; + + return $self; +} + +sub get_owner { + my $self = shift; + + return $self->{_owner}; +} + +sub get_group { + my $self = shift; + + return $self->{_group}; +} + +sub get_sacl { + my $self = shift; + + return $self->{_sacl}; +} + +sub get_dacl { + my $self = shift; + + return $self->{_dacl}; +} + +sub as_string { + croak 'Usage: Security Descriptors do not have an as_string method; use as_stanza instead'; +} + +sub as_stanza { + my $self = shift; + + my $stanza = ''; + if (defined(my $owner = $self->{_owner})) { + $stanza .= 'Owner SID: ' . $owner->as_string; + my $name = $owner->get_name; + $stanza .= " [$name]" if defined $name; + $stanza .= "\n"; + } + if (defined(my $group = $self->{_group})) { + $stanza .= 'Group SID: ' . $group->as_string; + my $name = $group->get_name; + $stanza .= " [$name]" if defined $name; + $stanza .= "\n"; + } + if (defined(my $sacl = $self->{_sacl})) { + foreach my $ace ($sacl->get_list_of_aces) { + $stanza .= 'SACL ACE: ' . $ace->as_string . "\n"; + } + } + if (defined(my $dacl = $self->{_dacl})) { + foreach my $ace ($dacl->get_list_of_aces) { + $stanza .= 'DACL ACE: ' . $ace->as_string . "\n"; + } + } + return $stanza; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/Entry.pm b/thirdparty/rr/Parse/Win32Registry/Entry.pm new file mode 100644 index 0000000000..1e14a801a2 --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/Entry.pm @@ -0,0 +1,151 @@ +package Parse::Win32Registry::Entry; + +use strict; +use warnings; + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +sub get_regfile { + my $self = shift; + + return $self->{_regfile}; +} + +sub get_offset { + my $self = shift; + + return $self->{_offset}; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + +sub is_allocated { + my $self = shift; + + return $self->{_allocated}; +} + +sub get_tag { + my $self = shift; + + return $self->{_tag}; +} + +sub as_string { + my $self = shift; + + my $tag = $self->{_tag}; + $tag = 'unidentified entry' if !defined $tag; + return "($tag)"; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x %s len=0x%x', + $self->{_offset}, + $self->{_tag}, + $self->{_length}; + + return $info; +} + +sub unparsed { + my $self = shift; + + return hexdump($self->get_raw_bytes, $self->get_offset); +} + +sub get_raw_bytes { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $fh = $regfile->get_filehandle; + my $offset = $self->{_offset}; + my $length = $self->{_length}; + + if (defined $self->{_header_length}) { + $length = $self->{_header_length}; + } + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $buffer, $length); + if ($bytes_read == $length) { + return $buffer; + } + else { + return ''; + } +} + +sub looks_like_key { + return UNIVERSAL::isa($_[0], "Parse::Win32Registry::Key"); +} + +sub looks_like_value { + return UNIVERSAL::isa($_[0], "Parse::Win32Registry::Value"); +} + +sub looks_like_security { + return UNIVERSAL::isa($_[0], "Parse::Win32Registry::WinNT::Security"); +} + +sub _dumpvar { + my $self = shift; + my $depth = shift || 1; + + my $dumpvar = ''; + foreach (sort keys %$self) { + $dumpvar .= ' ' x ($depth*2); + $dumpvar .= "$_ => "; + my $var = $self->{$_}; + if (!defined $var) { + $dumpvar .= "undef\n"; + } + elsif (/offset/ || /_id$/ || /^_unk/) { + $dumpvar .= sprintf "0x%x\n", $var; + } + elsif (/_flags$/) { + $dumpvar .= sprintf "0x%x (0b%b)\n", $var, $var; + } + elsif (/length/ || /bytes_used/) { + $dumpvar .= sprintf "0x%x (%d)\n", $var, $var; + } + elsif (/_data$/) { + if (length($var) == 0) { + $dumpvar .= '(no data)'; + } + else { + $dumpvar .= join(' ', unpack('(H2)20', $var)); + if (length($var) > 20) { + $dumpvar .= '...'; + } + } + $dumpvar .= "\n"; + } + elsif (/timestamp$/) { + $dumpvar .= $var . " (" . iso8601($var) . ")\n"; + } + elsif ($var =~ /^\d+$/) { + $dumpvar .= sprintf "%d\n", $var; + } + elsif (ref($var)) { + $dumpvar .= "$var\n"; # stringify object ref + } + else { + $dumpvar .= qq{"$var"}; + $dumpvar .= ' '; + $dumpvar .= Encode::is_utf8($var) ? "(UTF8)" : "(BYTES)"; + $dumpvar .= "\n"; + } + } + + return $dumpvar; +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/File.pm b/thirdparty/rr/Parse/Win32Registry/File.pm new file mode 100644 index 0000000000..26dd2973b8 --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/File.pm @@ -0,0 +1,66 @@ +package Parse::Win32Registry::File; + +use strict; +use warnings; + +sub get_filehandle { + my $self = shift; + + return $self->{_filehandle}; +} + +sub get_filename { + my $self = shift; + + return $self->{_filename}; +} + +sub get_length { + my $self = shift; + + return $self->{_length}; +} + +sub get_entry_iterator { + my $self = shift; + + my $entry_iter; + my $block_iter = $self->get_block_iterator; + + return Parse::Win32Registry::Iterator->new(sub { + while (1) { + if (defined $entry_iter) { + my $entry = $entry_iter->(); + if (defined $entry) { + return $entry; + } + } + # entry iterator is undefined or finished + my $block = $block_iter->(); + if (!defined $block) { + return; # block iterator finished + } + $entry_iter = $block->get_entry_iterator; + } + }); +} + +# method provided for backwards compatibility +sub move_to_first_entry { + my $self = shift; + + $self->{_entry_iter} = undef; +} + +# method provided for backwards compatibility +sub get_next_entry { + my $self = shift; + + my $entry_iter = $self->{_entry_iter}; + if (!defined $entry_iter) { + $self->{_entry_iter} = $entry_iter = $self->get_entry_iterator; + } + return $entry_iter->(); +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/Key.pm b/thirdparty/rr/Parse/Win32Registry/Key.pm new file mode 100644 index 0000000000..2422a8131d --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/Key.pm @@ -0,0 +1,245 @@ +package Parse::Win32Registry::Key; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; + +sub get_name { + my $self = shift; + + # the root key of a windows 95 registry has no defined name + # but this should be set to '' when created + return $self->{_name}; +} + +sub get_path { + my $self = shift; + + return $self->{_key_path}; +} + +sub _look_up_subkey { + my $self = shift; + my $subkey_name = shift; + + croak 'Missing subkey name' if !defined $subkey_name; + + foreach my $subkey ($self->get_list_of_subkeys) { + if (uc $subkey_name eq uc $subkey->{_name}) { + return $subkey; + } + } + return; +} + +sub get_subkey { + my $self = shift; + my $subkey_path = shift; + + # check for definedness in case key name is '' or '0' + croak "Usage: get_subkey('key name')" if !defined $subkey_path; + + my $key = $self; + + # Current path component separator is '\' to match that used in Windows. + # split returns nothing if it is given an empty string, + # and without a limit of -1 drops trailing empty fields. + # The following returns a list with a single zero-length string ("") + # for an empty string, as split(/\\/, $subkey_path, -1) returns (), + # an empty list. + my @path_components = index($subkey_path, "\\") == -1 + ? ($subkey_path) + : split(/\\/, $subkey_path, -1); + + my %offsets_seen = (); + $offsets_seen{$key->get_offset} = undef; + + foreach my $subkey_name (@path_components) { + if (my $subkey = $key->_look_up_subkey($subkey_name)) { + if (exists $offsets_seen{$subkey->get_offset}) { + return; # found loop + } + $key = $subkey; + $offsets_seen{$key->get_offset} = undef; + } + else { # subkey name not found, abort look up + return; + } + } + return $key; +} + +sub get_value { + my $self = shift; + my $value_name = shift; + + # check for definedness in case value name is '' or '0' + croak "Usage: get_value('value name')" if !defined $value_name; + + foreach my $value ($self->get_list_of_values) { + if (uc $value_name eq uc $value->{_name}) { + return $value; + } + } + return undef; +} + +sub print_summary { + my $self = shift; + + print $self->as_string, "\n"; +} + +sub as_regedit_export { + my $self = shift; + + return "[" . $self->{_key_path} . "]\n"; +} + +sub regenerate_path { + my $self = shift; + + # ascend to the root + my $key = $self; + my @key_names = ($key->get_name); + + my %offsets_seen = (); + while (!$key->is_root) { + $offsets_seen{$key->get_offset}++; + $key = $key->get_parent; + if (!defined $key) { # found an undefined parent key + unshift @key_names, '(Invalid Parent Key)'; + last; + } + if (exists $offsets_seen{$key->get_offset}) { # found loop + unshift @key_names, '(Invalid Parent Key)'; + last; + } + unshift @key_names, $key->get_name; + } + + my $key_path = join('\\', @key_names); + $self->{_key_path} = $key_path; + return $key_path; +} + +sub get_value_data { + my $self = shift; + my $value_name = shift; + + croak "Usage: get_value_data('value name')" if !defined $value_name; + + if (my $value = $self->get_value($value_name)) { + return $value->get_data; + } + return; +} + +sub get_mru_list_of_values { + my $self = shift; + + my @values = (); + + if (my $mrulist = $self->get_value('MRUList')) { + foreach my $ch (split(//, $mrulist->get_data)) { + if (my $value = $self->get_value($ch)) { + push @values, $value; + } + } + } + elsif (my $mrulistex = $self->get_value('MRUListEx')) { + foreach my $item (unpack('V*', $mrulistex->get_data)) { + last if $item == 0xffffffff; + if (my $value = $self->get_value($item)) { + push @values, $value; + } + } + } + return @values; +} + +sub get_list_of_subkeys { + my $self = shift; + + my $subkey_iter = $self->get_subkey_iterator; + my @subkeys; + while (my $subkey = $subkey_iter->()) { + push @subkeys, $subkey; + } + return @subkeys; +} + +sub get_list_of_values { + my $self = shift; + + my $value_iter = $self->get_value_iterator; + my @values; + while (my $value = $value_iter->()) { + push @values, $value; + } + return @values; +} + +sub get_subtree_iterator { + my $self = shift; + + my @start_keys = ($self); + push my (@subkey_iters), Parse::Win32Registry::Iterator->new(sub { + return shift @start_keys; + }); + my $value_iter; + my $key; # used to remember key while iterating values + + return Parse::Win32Registry::Iterator->new(sub { + if (defined $value_iter && wantarray) { + my $value = $value_iter->(); + if (defined $value) { + return ($key, $value); + } + # $value_iter finished, so fetch a new one + # from the (current) $subkey_iter[-1] + } + while (@subkey_iters > 0) { + $key = $subkey_iters[-1]->(); # depth-first + if (defined $key) { + push @subkey_iters, $key->get_subkey_iterator; + $value_iter = $key->get_value_iterator; + return $key; + } + pop @subkey_iters; # $subkey_iter finished, so remove it + } + return; + }); +} + +sub walk { + my $self = shift; + my $key_enter_func = shift; + my $value_func = shift; + my $key_leave_func = shift; + + if (!defined $key_enter_func && + !defined $value_func && + !defined $key_leave_func) { + $key_enter_func = sub { print "+ ", $_[0]->get_path, "\n"; }; + $value_func = sub { print " '", $_[0]->get_name, "'\n"; }; + $key_leave_func = sub { print "- ", $_[0]->get_path, "\n"; }; + } + + $key_enter_func->($self) if ref $key_enter_func eq 'CODE'; + + foreach my $value ($self->get_list_of_values) { + $value_func->($value) if ref $value_func eq 'CODE'; + } + + foreach my $subkey ($self->get_list_of_subkeys) { + $subkey->walk($key_enter_func, $value_func, $key_leave_func); + } + + $key_leave_func->($self) if ref $key_leave_func eq 'CODE'; +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/Value.pm b/thirdparty/rr/Parse/Win32Registry/Value.pm new file mode 100644 index 0000000000..de8587a2c6 --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/Value.pm @@ -0,0 +1,101 @@ +package Parse::Win32Registry::Value; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +sub get_name { + my $self = shift; + + return $self->{_name}; +} + +sub get_type { + my $self = shift; + + return $self->{_type}; +} + +our @Types = qw( + REG_NONE + REG_SZ + REG_EXPAND_SZ + REG_BINARY + REG_DWORD + REG_DWORD_BIG_ENDIAN + REG_LINK + REG_MULTI_SZ + REG_RESOURCE_LIST + REG_FULL_RESOURCE_DESCRIPTOR + REG_RESOURCE_REQUIREMENTS_LIST + REG_QWORD +); + +sub get_type_as_string { + my $self = shift; + + my $type = $self->get_type; + if (exists $Types[$type]) { + return $Types[$type]; + } + else { + # Return unrecognised types as REG_ + # REGEDIT displays them as formatted hex numbers, e.g. 0x1f4 + return "REG_$type"; + } +} + +sub get_data_as_string { + my $self = shift; + + my $type = $self->get_type; + my $data = $self->get_data; + if (!defined($data)) { + return '(invalid data)'; + } + elsif (length($data) == 0) { + return '(no data)'; + } + elsif ($type == REG_SZ || $type == REG_EXPAND_SZ) { + return $data; + } + elsif ($type == REG_MULTI_SZ) { + my @data = $self->get_data; + my $i = 0; + return join(' ', map { "[" . $i++ . "] $_" } @data); + } + elsif ($type == REG_DWORD || $type == REG_DWORD_BIG_ENDIAN) { + return sprintf '0x%08x (%u)', $data, $data; + } + else { + return join(' ', unpack('(H2)*', $data)); + } +} + +sub get_raw_data { + my $self = shift; + + return $self->{_data}; +} + +sub as_string { + my $self = shift; + + my $name = $self->get_name; + $name = '(Default)' if $name eq ''; + my $type_as_string = $self->get_type_as_string; + my $data_as_string = $self->get_data_as_string; + return "$name ($type_as_string) = $data_as_string"; +} + +sub print_summary { + my $self = shift; + + print $self->as_string, "\n"; +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/Win95/File.pm b/thirdparty/rr/Parse/Win32Registry/Win95/File.pm new file mode 100644 index 0000000000..6b713f2f98 --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/Win95/File.pm @@ -0,0 +1,540 @@ +package Parse::Win32Registry::Win95::File; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::File); + +use Carp; +use File::Basename; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::Win95::Key; + +use constant CREG_HEADER_LENGTH => 0x20; +use constant OFFSET_TO_RGKN_BLOCK => 0x20; + +sub new { + my $class = shift; + my $filename = shift or croak 'No filename specified'; + + open my $fh, '<', $filename or croak "Unable to open '$filename': $!"; + + # CREG Header + # 0x00 dword = 'CREG' signature + # 0x04 + # 0x08 dword = offset to first rgdb block + # 0x0c + # 0x10 word = number of rgdb blocks + + my $bytes_read = sysread($fh, my $creg_header, CREG_HEADER_LENGTH); + if ($bytes_read != CREG_HEADER_LENGTH) { + warnf('Could not read registry file header'); + return; + } + + my ($creg_sig, + $offset_to_first_rgdb_block, + $num_rgdb_blocks) = unpack('a4x4Vx4v', $creg_header); + + if ($creg_sig ne 'CREG') { + warnf('Invalid registry file signature'); + return; + } + + my $self = {}; + $self->{_filehandle} = $fh; + $self->{_filename} = $filename; + $self->{_length} = (stat $fh)[7]; + $self->{_offset_to_first_rgdb_block} = $offset_to_first_rgdb_block; + $self->{_num_rgdb_blocks} = $num_rgdb_blocks; + bless $self, $class; + + # get_rgkn will cache the rgkn block for subsequent calls + my $rgkn_block = $self->get_rgkn; + return if !defined $rgkn_block; # warning will already have been made + + # Index the rgdb entries by id for faster look up + $self->_index_rgdb_entries; + + return $self; +} + +sub get_timestamp { + return undef; +} + +sub get_timestamp_as_string { + return iso8601(undef); +} + +sub get_embedded_filename { + return undef; +} + +sub get_root_key { + my $self = shift; + + return $self->get_rgkn->get_root_key; +} + +sub get_virtual_root_key { + my $self = shift; + my $fake_root = shift; + + my $root_key = $self->get_root_key; + return if !defined $root_key; + + if (!defined $fake_root) { + # guess virtual root from filename + my $filename = basename $self->{_filename}; + + if ($filename =~ /USER/i) { + $fake_root = 'HKEY_USERS'; + } + elsif ($filename =~ /SYSTEM/i) { + $fake_root = 'HKEY_LOCAL_MACHINE'; + } + else { + $fake_root = 'HKEY_UNKNOWN'; + } + } + + $root_key->{_name} = $fake_root; + $root_key->{_key_path} = $fake_root; + + return $root_key; +} + +sub _index_rgdb_entries { + my $self = shift; + + my %index = (); + + # Build index of rgdb key entries + # Entries are only included if $key_block_num matches $rgdb_block_num + my $rgdb_block_num = 0; + my $rgdb_iter = $self->get_rgdb_iterator; + while (my $rgdb = $rgdb_iter->()) { + my $rgdb_key_iter = $rgdb->get_key_iterator; + while (my $rgdb_key = $rgdb_key_iter->()) { + my $key_id = $rgdb_key->{_id}; + my $key_block_num = $key_id >> 16; + if ($rgdb_block_num == $key_block_num) { + $index{$key_id} = $rgdb_key; + } + } + $rgdb_block_num++; + } + + $self->{_rgdb_index} = \%index; +} + +sub _dump_rgdb_index { + my $self = shift; + + my $rgdb_index = $self->{_rgdb_index}; + + foreach my $key_id (sort { $a <=> $b } keys %$rgdb_index) { + my $rgdb_key = $rgdb_index->{$key_id}; + printf qq{id=0x%x 0x%x,%d/%d "%s" vals=%d\n}, + $key_id, + $rgdb_key->{_offset}, + $rgdb_key->{_length_used}, + $rgdb_key->{_length}, + $rgdb_key->{_name}, + $rgdb_key->{_num_values}; + } +} + +sub get_rgkn { + my $self = shift; + + # Return cached rgkn block if present + if (defined $self->{_rgkn}) { + return $self->{_rgkn}; + } + + my $offset = OFFSET_TO_RGKN_BLOCK; + my $rgkn_block = Parse::Win32Registry::Win95::RGKN->new($self, $offset); + $self->{_rgkn} = $rgkn_block; + return $rgkn_block; +} + +sub get_rgdb_iterator { + my $self = shift; + + my $offset_to_next_rgdb_block = $self->{_offset_to_first_rgdb_block}; + my $num_rgdb_blocks = $self->{_num_rgdb_blocks}; + + my $end_of_file = $self->{_length}; + + my $rgdb_block_num = 0; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_rgdb_block > $end_of_file) { + return; # no more rgdb blocks + } + if ($rgdb_block_num >= $num_rgdb_blocks) { + return; # no more rgdb blocks + } + $rgdb_block_num++; + if (my $rgdb_block = Parse::Win32Registry::Win95::RGDB->new($self, + $offset_to_next_rgdb_block)) + { + return unless $rgdb_block->get_length > 0; + $offset_to_next_rgdb_block += $rgdb_block->get_length; + return $rgdb_block; + } + }); +} + +sub get_block_iterator { + my $self = shift; + + my $rgdb_iter; + + return Parse::Win32Registry::Iterator->new(sub { + if (!defined $rgdb_iter) { + $rgdb_iter = $self->get_rgdb_iterator; + return $self->get_rgkn; + } + return $rgdb_iter->(); + }); +} + +*get_hbin_iterator = \&get_block_iterator; + + +package Parse::Win32Registry::Win95::RGKN; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +use constant RGKN_HEADER_LENGTH => 0x20; +use constant OFFSET_TO_RGKN_BLOCK => 0x20; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift || OFFSET_TO_RGKN_BLOCK; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGKN Block Header + # 0x0 dword = 'RGKN' signature + # 0x4 dword = length of rgkn block + # 0x8 dword = offset to root key entry (relative to start of rgkn block) + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgkn_header, RGKN_HEADER_LENGTH); + if ($bytes_read != RGKN_HEADER_LENGTH) { + warnf('Could not read RGKN header at 0x%x', $offset); + return; + } + + my ($sig, + $rgkn_block_length, + $offset_to_root_key) = unpack('a4VV', $rgkn_header); + + if ($sig ne 'RGKN') { + warnf('Invalid RGKN block signature at 0x%x', $offset); + return; + } + + $offset_to_root_key += $offset; + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $rgkn_block_length; + $self->{_header_length} = RGKN_HEADER_LENGTH; + $self->{_allocated} = 1; + $self->{_tag} = 'rgkn block'; + $self->{_offset_to_root_key} = $offset_to_root_key; + bless $self, $class; + + return $self; +} + +sub get_root_key { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset_to_root_key = $self->{_offset_to_root_key}; + + my $root_key = Parse::Win32Registry::Win95::Key->new($regfile, + $offset_to_root_key); + return $root_key; +} + +sub get_entry_iterator { + my $self = shift; + + my $root_key = $self->get_root_key; + + # In the unlikely event there is no root key, return an empty iterator + if (defined $root_key) { + return $root_key->get_subtree_iterator; + } + else { + return Parse::Win32Registry::Iterator->new(sub {}); + } +} + + +package Parse::Win32Registry::Win95::RGDB; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +use constant RGDB_HEADER_LENGTH => 0x20; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGDB Block Header + # 0x0 dword = 'RDGB' signature + # 0x4 dword = length of rgdb block + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgdb_header, RGDB_HEADER_LENGTH); + if ($bytes_read != RGDB_HEADER_LENGTH) { + return; + } + + my ($sig, + $rgdb_block_length) = unpack('a4V', $rgdb_header); + + if ($sig ne 'RGDB') { + return; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $rgdb_block_length; + $self->{_header_length} = RGDB_HEADER_LENGTH; + $self->{_allocated} = 1; + $self->{_tag} = 'rgdb block'; + bless $self, $class; + + return $self; +} + +sub get_key_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset = $self->{_offset}; + my $length = $self->{_length}; + + my $offset_to_next_rgdb_key = $offset + RGDB_HEADER_LENGTH; + my $end_of_rgdb_block = $offset + $length; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_rgdb_key >= $end_of_rgdb_block) { + return; + } + if (my $rgdb_key = Parse::Win32Registry::Win95::RGDBKey->new($regfile, + $offset_to_next_rgdb_key)) + { + return unless $rgdb_key->get_length > 0; + $offset_to_next_rgdb_key += $rgdb_key->get_length; + + # Check rgdb key has not run past end of rgdb block + if ($offset_to_next_rgdb_key > $end_of_rgdb_block) { + return; + } + return $rgdb_key; + } + }); +} + +sub get_entry_iterator { + my $self = shift; + + my $value_iter; + my $key_iter = $self->get_key_iterator; + + return Parse::Win32Registry::Iterator->new(sub { + if (defined $value_iter) { + my $value = $value_iter->(); + if (defined $value) { + return $value; + } + } + + my $key = $key_iter->(); + if (!defined $key) { + return; # key iterator finished + } + + $value_iter = $key->get_value_iterator; + return $key; + }); +} + + +package Parse::Win32Registry::Win95::RGDBKey; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); + +use constant RGDB_ENTRY_HEADER_LENGTH => 0x14; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGDB Key Entry + # 0x00 dword = length of rgdb entry / offset to next rgdb entry + # (this length includes any following value entries) + # 0x04 dword = id (top word = block num, bottom word = id) + # 0x08 dword = bytes used (unpacked, but not used) + # 0x0c word = key name length + # 0x0e word = number of values + # 0x10 dword + # 0x14 = key name [for key name length bytes] + # followed immediately by any RGDB Value Entries belonging to this key + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgdb_key_entry, RGDB_ENTRY_HEADER_LENGTH); + if ($bytes_read != RGDB_ENTRY_HEADER_LENGTH) { + return; + } + + my ($length, + $key_id, + $length_used, + $name_length, + $num_values) = unpack('VVVvv', $rgdb_key_entry); + + $bytes_read = sysread($fh, my $name, $name_length); + if ($bytes_read != $name_length) { + return; + } + $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name); + + # Calculate the length of the entry's key header + my $header_length = RGDB_ENTRY_HEADER_LENGTH + $name_length; + + # Check for invalid/unused entries + if ($key_id == 0xffffffff || $length_used == 0xffffffff + || $header_length > $length) + { + $name = ''; + $header_length = RGDB_ENTRY_HEADER_LENGTH; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_length_used} = $length_used; + $self->{_header_length} = $header_length; + $self->{_allocated} = 1; + $self->{_tag} = 'rgdb key'; + $self->{_id} = $key_id; + $self->{_name} = $name; + $self->{_name_length} = $name_length; + $self->{_num_values} = $num_values; + bless $self, $class; + + return $self; +} + +sub get_name { + my $self = shift; + + return $self->{_name}; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x rgdb key len=0x%x/0x%x "%s" id=0x%x vals=%d', + $self->{_offset}, + $self->{_length_used}, + $self->{_length}, + $self->{_name}, + $self->{_id}, + $self->{_num_values}; + + return $info; +} + +sub get_value_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + + my $num_values_remaining = $self->{_num_values}; + + my $offset = $self->{_offset}; + + # offset_to_next_rgdb_value can only be set to a valid offset + # if num_values_remaining > 0 + my $offset_to_next_rgdb_value = 0xffffffff; + if ($num_values_remaining > 0) { + $offset_to_next_rgdb_value = $offset + + $self->{_header_length}; + } + + my $end_of_rgdb_key = $offset + $self->{_length}; + + # don't attempt to return values if id is invalid... + if ($self->{_id} == 0xffffffff) { + $num_values_remaining = 0; + } + + return Parse::Win32Registry::Iterator->new(sub { + if ($num_values_remaining-- <= 0) { + return; + } + if ($offset_to_next_rgdb_value == 0xffffffff) { + return; + } + if ($offset_to_next_rgdb_value > $end_of_rgdb_key) { + return; + } + if (my $value = Parse::Win32Registry::Win95::Value->new($regfile, + $offset_to_next_rgdb_value)) + { + return unless $value->get_length > 0; + $offset_to_next_rgdb_value += $value->get_length; + return $value; + } + else { + return; # no more values + } + }); +} + +1; + diff --git a/thirdparty/rr/Parse/Win32Registry/Win95/Key.pm b/thirdparty/rr/Parse/Win32Registry/Win95/Key.pm new file mode 100644 index 0000000000..6bff102198 --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/Win95/Key.pm @@ -0,0 +1,207 @@ +package Parse::Win32Registry::Win95::Key; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Key); + +use Carp; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::Win95::Value; + +use constant RGKN_ENTRY_LENGTH => 0x1c; +use constant OFFSET_TO_RGKN_BLOCK => 0x20; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to RGKN key entry relative to start of RGKN + my $parent_key_path = shift; # parent key path (optional) + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGKN Key Entry + # 0x00 dword + # 0x04 dword + # 0x08 dword + # 0x0c dword = offset to parent RGKN entry + # 0x10 dword = offset to first child RGKN entry + # 0x14 dword = offset to next sibling RGKN entry + # 0x18 dword = entry id of RGDB entry + + # Extracted offsets are relative to the start of the RGKN block + + # Any offset of 0xffffffff marks the end of a list. + # An entry id of 0xffffffff means the RGKN entry has no RGDB entry. + # This occurs for the root key of the registry file. + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgkn_entry, RGKN_ENTRY_LENGTH); + if ($bytes_read != RGKN_ENTRY_LENGTH) { + warnf('Could not read RGKN key at 0x%x', $offset); + return; + } + + my ($offset_to_parent, + $offset_to_first_child, + $offset_to_next_sibling, + $key_id) = unpack('x12VVVV', $rgkn_entry); + + $offset_to_parent += OFFSET_TO_RGKN_BLOCK + if $offset_to_parent != 0xffffffff; + $offset_to_first_child += OFFSET_TO_RGKN_BLOCK + if $offset_to_first_child != 0xffffffff; + $offset_to_next_sibling += OFFSET_TO_RGKN_BLOCK + if $offset_to_next_sibling != 0xffffffff; + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = RGKN_ENTRY_LENGTH; + $self->{_allocated} = 1; + $self->{_tag} = 'rgkn key'; + $self->{_offset_to_parent} = $offset_to_parent; + $self->{_offset_to_first_child} = $offset_to_first_child; + $self->{_offset_to_next_sibling} = $offset_to_next_sibling; + $self->{_id} = $key_id; + bless $self, $class; + + # Look up corresponding rgdb entry + my $index = $regfile->{_rgdb_index}; + croak 'Missing rgdb index' if !defined $index; + if (exists $index->{$key_id}) { + my $rgdb_key = $index->{$key_id}; + $self->{_rgdb_key} = $rgdb_key; + $self->{_name} = $rgdb_key->get_name; + } + else { + $self->{_name} = ''; + # Only the root key should have no matching RGDB entry + if (!$self->is_root) { + warnf('Could not find RGDB entry for RGKN key at 0x%x', $offset); + } + } + + my $name = $self->{_name}; + $self->{_key_path} = defined($parent_key_path) + ? "$parent_key_path\\$name" + : $name; + + return $self; +} + +sub get_timestamp { + return undef; +} + +sub get_timestamp_as_string { + return iso8601(undef); +} + +sub get_class_name { + return undef; +} + +sub is_root { + my $self = shift; + + my $offset = $self->{_offset}; + my $regfile = $self->{_regfile}; + + my $rgkn_block = $regfile->get_rgkn; + my $offset_to_root_key = $rgkn_block->{_offset_to_root_key}; + + # This gives better results than checking id == 0xffffffff + return $offset == $offset_to_root_key; +} + +sub get_parent { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset_to_parent = $self->{_offset_to_parent}; + my $key_path = $self->{_key_path}; + + return if $self->is_root; + + my $grandparent_key_path; + my @keys = split(/\\/, $key_path, -1); + if (@keys > 2) { + $grandparent_key_path = join("\\", @keys[0..$#keys-2]); + } + + return Parse::Win32Registry::Win95::Key->new($regfile, + $offset_to_parent, + $grandparent_key_path); +} + +sub get_security { + return undef; +} + +sub as_string { + my $self = shift; + + return $self->get_path; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x rgkn key len=0x%x par=0x%x,child=0x%x,next=0x%x id=0x%x', + $self->{_offset}, + $self->{_length}, + $self->{_offset_to_parent}, + $self->{_offset_to_first_child}, + $self->{_offset_to_next_sibling}, + $self->{_id}; + return $info; +} + +sub get_subkey_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $key_path = $self->{_key_path}; + + my $offset_to_next_key = $self->{_offset_to_first_child}; + + my $end_of_file = $regfile->get_length; + my $rgkn_block = $regfile->get_rgkn; + my $end_of_rgkn_block = $rgkn_block->get_offset + $rgkn_block->get_length; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_key == 0xffffffff) { + return; # no more subkeys + } + if ($offset_to_next_key > $end_of_rgkn_block) { + return; + } + if (my $key = Parse::Win32Registry::Win95::Key->new($regfile, + $offset_to_next_key, $key_path)) + { + $offset_to_next_key = $key->{_offset_to_next_sibling}; + return $key; + } + else { + return; # no more subkeys + } + }); +} + +sub get_value_iterator { + my $self = shift; + + my $rgdb_key = $self->{_rgdb_key}; + if (defined $rgdb_key) { + return $rgdb_key->get_value_iterator; + } + else { + return Parse::Win32Registry::Iterator->new(sub {}); + } +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/Win95/Value.pm b/thirdparty/rr/Parse/Win32Registry/Win95/Value.pm new file mode 100644 index 0000000000..baffd13b59 --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/Win95/Value.pm @@ -0,0 +1,177 @@ +package Parse::Win32Registry::Win95::Value; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Value); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); + +use constant RGDB_VALUE_HEADER_LENGTH => 0xc; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to RGDB value entry + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # RGDB Value Entry + # 0x00 dword = value type + # 0x04 + # 0x08 word = value name length + # 0x0a word = value data length + # 0x0c = value name [for name length bytes] + # + value data [for data length bytes] + # Value type may just be a word, not a dword; + # following word always appears to be zero. + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $rgdb_value_entry, + RGDB_VALUE_HEADER_LENGTH); + if ($bytes_read != RGDB_VALUE_HEADER_LENGTH) { + warnf('Could not read RGDB value at 0x%x', $offset); + return; + } + + my ($type, + $name_length, + $data_length) = unpack('Vx4vv', $rgdb_value_entry); + + $bytes_read = sysread($fh, my $name, $name_length); + if ($bytes_read != $name_length) { + warnf('Could not read name for RGDB value at 0x%x', $offset); + return; + } + $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name); + + $bytes_read = sysread($fh, my $data, $data_length); + if ($bytes_read != $data_length) { + warnf('Could not read data for RGDB value at 0x%x', $offset); + return; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = RGDB_VALUE_HEADER_LENGTH + $name_length + $data_length; + $self->{_allocated} = 1; + $self->{_tag} = 'rgdb value'; + $self->{_name} = $name; + $self->{_name_length} = $name_length; + $self->{_type} = $type; + $self->{_data} = $data; + $self->{_data_length} = $data_length; + bless $self, $class; + + return $self; +} + +sub get_data { + my $self = shift; + + my $type = $self->get_type; + + my $data = $self->{_data}; + return if !defined $data; # actually, Win95 value data is always defined + + # apply decoding to appropriate data types + if ($type == REG_DWORD) { + if (length($data) == 4) { + $data = unpack('V', $data); + } + else { + # incorrect length for dword data + $data = undef; + } + } + elsif ($type == REG_DWORD_BIG_ENDIAN) { + if (length($data) == 4) { + $data = unpack('N', $data); + } + else { + # incorrect length for dword data + $data = undef; + } + } + elsif ($type == REG_SZ || $type == REG_EXPAND_SZ) { + # Snip off any terminating null. + # Typically, REG_SZ values will not have a terminating null, + # while REG_EXPAND_SZ values will have a terminating null + chop $data if substr($data, -1, 1) eq "\0"; + } + elsif ($type == REG_MULTI_SZ) { + # Snip off any terminating nulls + chop $data if substr($data, -1, 1) eq "\0"; + chop $data if substr($data, -1, 1) eq "\0"; + my @multi_sz = split("\0", $data, -1); + # Make sure there is at least one empty string + @multi_sz = ('') if @multi_sz == 0; + return wantarray ? @multi_sz : join($", @multi_sz); + } + + return $data; +} + +sub as_regedit_export { + my $self = shift; + my $version = shift || 5; + + my $name = $self->get_name; + my $export = $name eq '' ? '@=' : '"' . $name . '"='; + + my $type = $self->get_type; + + # XXX +# if (!defined $self->{_data}) { +# $name = $name eq '' ? '@' : qq{"$name"}; +# return qq{; $name=(invalid data)\n}; +# } + + if ($type == REG_SZ) { + $export .= '"' . $self->get_data . '"'; + $export .= "\n"; + } + elsif ($type == REG_BINARY) { + $export .= 'hex:'; + $export .= format_octets($self->{_data}, length($export)); + } + elsif ($type == REG_DWORD) { + my $data = $self->get_data; + $export .= defined($data) + ? sprintf("dword:%08x", $data) + : "dword:"; + $export .= "\n"; + } + elsif ($type == REG_EXPAND_SZ || $type == REG_MULTI_SZ) { + my $data = $version == 4 + ? $self->{_data} # raw data + : encode("UCS-2LE", $self->{_data}); # ansi->unicode + $export .= sprintf("hex(%x):", $type); + $export .= format_octets($data, length($export)); + } + else { + $export .= sprintf("hex(%x):", $type); + $export .= format_octets($self->{_data}, length($export)); + } + return $export; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x rgdb value len=0x%x "%s" type=%d data,len=0x%x', + $self->{_offset}, + $self->{_length}, + $self->{_name}, + $self->{_type}, + $self->{_data_length}; + return $info; +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/WinNT/Entry.pm b/thirdparty/rr/Parse/Win32Registry/WinNT/Entry.pm new file mode 100644 index 0000000000..545773838c --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/WinNT/Entry.pm @@ -0,0 +1,109 @@ +package Parse::Win32Registry::WinNT::Entry; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::WinNT::Key; +use Parse::Win32Registry::WinNT::Value; +use Parse::Win32Registry::WinNT::Security; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $entry_header, 8); + if ($bytes_read != 8) { + return; + } + + my ($length, + $tag) = unpack('Va2', $entry_header); + + my $allocated = 0; + if ($length > 0x7fffffff) { + $allocated = 1; + $length = (0xffffffff - $length) + 1; + } + + $tag = '' if $tag !~ /(nk|vk|lh|lf|li|ri|sk)/; + + if ($tag eq 'nk') { + if (my $key = Parse::Win32Registry::WinNT::Key->new($regfile, + $offset)) + { + $key->regenerate_path; + return $key; + } + } + elsif ($tag eq 'vk') { + if (my $value = Parse::Win32Registry::WinNT::Value->new($regfile, + $offset)) + { + return $value; + } + } + elsif ($tag eq 'sk') { + if (my $value = Parse::Win32Registry::WinNT::Security->new($regfile, + $offset)) + { + return $value; + } + } + + my $self = {}; + $self->{_regfile} = $regfile, + $self->{_offset} = $offset, + $self->{_length} = $length, + $self->{_tag} = $tag, + $self->{_allocated} = $allocated, + bless $self, $class; + + return $self; +} + +sub as_string { + my $self = shift; + + my $tag = $self->{_tag}; + if ($tag eq 'nk') { + return '(key entry)'; + } + elsif ($tag eq 'vk') { + return '(value entry)'; + } + elsif ($tag eq 'sk') { + return '(security entry)'; + } + elsif ($tag =~ /(lh|lf|li|ri)/) { + return '(subkey list entry)'; + } + return '(unidentified entry)'; +} + +sub parse_info { + my $self = shift; + + my $tag = $self->{_tag}; + $tag = defined($tag) && $tag ne '' + ? $tag . ' ' + : '.. '; + my $info = sprintf '0x%x %slen=0x%x alloc=%d', + $self->{_offset}, + $tag, + $self->{_length}, + $self->{_allocated}; + return $info; +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/WinNT/File.pm b/thirdparty/rr/Parse/Win32Registry/WinNT/File.pm new file mode 100644 index 0000000000..97a859b206 --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/WinNT/File.pm @@ -0,0 +1,297 @@ +package Parse::Win32Registry::WinNT::File; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::File); + +use Carp; +use Encode; +use File::Basename; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::WinNT::Key; + +use constant REGF_HEADER_LENGTH => 0x200; +use constant OFFSET_TO_FIRST_HBIN => 0x1000; + +sub new { + my $class = shift; + my $filename = shift or croak "No filename specified"; + + open my $fh, '<', $filename or croak "Unable to open '$filename': $!"; + + # 0x00 dword = 'regf' signature + # 0x04 dword = seq1 + # 0x08 dword = seq2 + # 0x0c qword = timestamp + # 0x14 dword = major version + # 0x18 dword = minor version + # 0x1c dword = type (0 = registry file, 1 = log file) + # 0x20 dword = (1) + # 0x24 dword = offset to root key + # 0x28 dword = total length of all hbins (excludes header) + # 0x2c dword = (1) + # 0x30 = embedded filename + + # Extracted offsets are always relative to first hbin + + my $bytes_read = sysread($fh, my $regf_header, REGF_HEADER_LENGTH); + if ($bytes_read != REGF_HEADER_LENGTH) { + warnf('Could not read registry file header'); + return; + } + + my ($regf_sig, + $seq1, + $seq2, + $timestamp, + $major_version, + $minor_version, + $type, + $offset_to_root_key, + $total_hbin_length, + $embedded_filename, + ) = unpack('a4VVa8VVVx4VVx4a64', $regf_header); + + $offset_to_root_key += OFFSET_TO_FIRST_HBIN; + + if ($regf_sig ne 'regf') { + warnf('Invalid registry file signature'); + return; + } + + $embedded_filename = unpack('Z*', decode('UCS-2LE', $embedded_filename)); + + # The header checksum is the xor of the first 127 dwords. + # The checksum is stored in the 128th dword, at offset 0x1fc (508). + my $checksum = 0; + foreach my $x (unpack('V127', $regf_header)) { + $checksum ^= $x; + } + my $embedded_checksum = unpack('x508V', $regf_header); + if ($checksum != $embedded_checksum) { + warnf('Invalid checksum for registry file header'); + } + + my $self = {}; + $self->{_filehandle} = $fh; + $self->{_filename} = $filename; + $self->{_length} = (stat $fh)[7]; + $self->{_offset_to_root_key} = $offset_to_root_key; + $self->{_timestamp} = unpack_windows_time($timestamp); + $self->{_embedded_filename} = $embedded_filename; + $self->{_seq1} = $seq1; + $self->{_seq2} = $seq2; + $self->{_version} = "$major_version.$minor_version"; + $self->{_type} = $type; + $self->{_total_hbin_length} = $total_hbin_length; + $self->{_embedded_checksum} = $embedded_checksum; + $self->{_security_cache} = {}; # comment out to disable cache + bless $self, $class; + + return $self; +} + +sub get_root_key { + my $self = shift; + + my $offset_to_root_key = $self->{_offset_to_root_key}; + + my $root_key = Parse::Win32Registry::WinNT::Key->new($self, + $offset_to_root_key); + return $root_key; +} + +sub get_virtual_root_key { + my $self = shift; + my $fake_root = shift; + + my $root_key = $self->get_root_key; + return if !defined $root_key; + + if (!defined $fake_root) { + # guess virtual root from filename + my $filename = basename $self->{_filename}; + + if ($filename =~ /NTUSER/i) { + $fake_root = 'HKEY_CURRENT_USER'; + } + elsif ($filename =~ /USRCLASS/i) { + $fake_root = 'HKEY_CLASSES_ROOT'; + } + elsif ($filename =~ /SOFTWARE/i) { + $fake_root = 'HKEY_LOCAL_MACHINE\SOFTWARE'; + } + elsif ($filename =~ /SYSTEM/i) { + $fake_root = 'HKEY_LOCAL_MACHINE\SYSTEM'; + } + elsif ($filename =~ /SAM/i) { + $fake_root = 'HKEY_LOCAL_MACHINE\SAM'; + } + elsif ($filename =~ /SECURITY/i) { + $fake_root = 'HKEY_LOCAL_MACHINE\SECURITY'; + } + else { + $fake_root = 'HKEY_UNKNOWN'; + } + } + + $root_key->{_name} = $fake_root; + $root_key->{_key_path} = $fake_root; + + return $root_key; +} + +sub get_timestamp { + my $self = shift; + + return $self->{_timestamp}; +} + +sub get_timestamp_as_string { + my $self = shift; + + return iso8601($self->{_timestamp}); +} + +sub get_embedded_filename { + my $self = shift; + + return $self->{_embedded_filename}; +} + +sub get_block_iterator { + my $self = shift; + + my $offset_to_next_hbin = OFFSET_TO_FIRST_HBIN; + my $end_of_file = $self->{_length}; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_hbin > $end_of_file) { + return; # no more hbins + } + if (my $hbin = Parse::Win32Registry::WinNT::Hbin->new($self, + $offset_to_next_hbin)) + { + return unless $hbin->get_length > 0; + $offset_to_next_hbin += $hbin->get_length; + return $hbin; + } + else { + return; # no more hbins + } + }); +} + +*get_hbin_iterator = \&get_block_iterator; + +sub _dump_security_cache { + my $self = shift; + + if (defined(my $cache = $self->{_security_cache})) { + foreach my $offset (sort { $a <=> $b } keys %$cache) { + my $security = $cache->{$offset}; + printf '0x%x %s\n', $offset, $security->as_string; + } + } +} + + +package Parse::Win32Registry::WinNT::Hbin; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::WinNT::Entry; + +use constant HBIN_HEADER_LENGTH => 0x20; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # 0x00 dword = 'hbin' signature + # 0x04 dword = offset from first hbin to this hbin + # 0x08 dword = length of this hbin / relative offset to next hbin + # 0x14 qword = timestamp (first hbin only) + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $hbin_header, HBIN_HEADER_LENGTH); + if ($bytes_read != HBIN_HEADER_LENGTH) { + return; + } + + my ($sig, + $offset_to_hbin, + $length, + $timestamp) = unpack('a4VVx8a8x4', $hbin_header); + + if ($sig ne 'hbin') { + return; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_header_length} = HBIN_HEADER_LENGTH; + $self->{_allocated} = 1; + $self->{_tag} = $sig; + $self->{_timestamp} = unpack_windows_time($timestamp); + bless $self, $class; + + return $self; +} + +sub get_timestamp { + my $self = shift; + + return $self->{_timestamp}; +} + +sub get_timestamp_as_string { + my $self = shift; + + return iso8601($self->{_timestamp}); +} + +sub get_entry_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset = $self->{_offset}; + my $length = $self->{_length}; + + my $offset_to_next_entry = $offset + HBIN_HEADER_LENGTH; + my $end_of_hbin = $offset + $length; + + return Parse::Win32Registry::Iterator->new(sub { + if ($offset_to_next_entry >= $end_of_hbin) { + return; # no more entries + } + if (my $entry = Parse::Win32Registry::WinNT::Entry->new($regfile, + $offset_to_next_entry)) + { + return unless $entry->get_length > 0; + $offset_to_next_entry += $entry->get_length; + return $entry; + } + else { + return; # no more entries + } + }); +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/WinNT/Key.pm b/thirdparty/rr/Parse/Win32Registry/WinNT/Key.pm new file mode 100644 index 0000000000..d362086586 --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/WinNT/Key.pm @@ -0,0 +1,444 @@ +package Parse::Win32Registry::WinNT::Key; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Key); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); +use Parse::Win32Registry::WinNT::Value; +use Parse::Win32Registry::WinNT::Security; + +use constant NK_HEADER_LENGTH => 0x50; +use constant OFFSET_TO_FIRST_HBIN => 0x1000; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to nk record relative to start of file + my $parent_key_path = shift; # parent key path (optional) + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # 0x00 dword = key length (negative = allocated) + # 0x04 word = 'nk' signature + # 0x06 word = flags + # 0x08 qword = timestamp + # 0x10 + # 0x14 dword = offset to parent + # 0x18 dword = number of subkeys + # 0x1c + # 0x20 dword = offset to subkey list (lf, lh, ri, li) + # 0x24 + # 0x28 dword = number of values + # 0x2c dword = offset to value list + # 0x30 dword = offset to security + # 0x34 dword = offset to class name + # 0x38 dword = max subkey name length + # 0x3c dword = max class name length + # 0x40 dword = max value name length + # 0x44 dword = max value data length + # 0x48 + # 0x4c word = key name length + # 0x4e word = class name length + # 0x50 = key name [for key name length bytes] + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $nk_header, NK_HEADER_LENGTH); + if ($bytes_read != NK_HEADER_LENGTH) { + warnf('Could not read key at 0x%x', $offset); + return; + } + + my ($length, + $sig, + $flags, + $timestamp, + $offset_to_parent, + $num_subkeys, + $offset_to_subkey_list, + $num_values, + $offset_to_value_list, + $offset_to_security, + $offset_to_class_name, + $name_length, + $class_name_length, + ) = unpack('Va2va8x4VVx4Vx4VVVVx20vv', $nk_header); + + $offset_to_parent += OFFSET_TO_FIRST_HBIN + if $offset_to_parent != 0xffffffff; + $offset_to_subkey_list += OFFSET_TO_FIRST_HBIN + if $offset_to_subkey_list != 0xffffffff; + $offset_to_value_list += OFFSET_TO_FIRST_HBIN + if $offset_to_value_list != 0xffffffff; + $offset_to_security += OFFSET_TO_FIRST_HBIN + if $offset_to_security != 0xffffffff; + $offset_to_class_name += OFFSET_TO_FIRST_HBIN + if $offset_to_class_name != 0xffffffff; + + my $allocated = 0; + if ($length > 0x7fffffff) { + $allocated = 1; + $length = (0xffffffff - $length) + 1; + } + # allocated should be true + + if ($length < NK_HEADER_LENGTH) { + warnf('Invalid value entry length at 0x%x', $offset); + return; + } + + if ($sig ne 'nk') { + warnf('Invalid signature for key at 0x%x', $offset); + return; + } + + $bytes_read = sysread($fh, my $name, $name_length); + if ($bytes_read != $name_length) { + warnf('Could not read name for key at 0x%x', $offset); + return; + } + + if ($flags & 0x20) { + $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name); + } + else { + $name = decode('UCS-2LE', $name); + } + + my $key_path = (defined $parent_key_path) + ? "$parent_key_path\\$name" + : "$name"; + + my $class_name; + if ($offset_to_class_name != 0xffffffff) { + sysseek($fh, $offset_to_class_name + 4, 0); + $bytes_read = sysread($fh, $class_name, $class_name_length); + if ($bytes_read != $class_name_length) { + warnf('Could not read class name at 0x%x', $offset_to_class_name); + $class_name = undef; + } + else { + $class_name = decode('UCS-2LE', $class_name); + } + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_allocated} = $allocated; + $self->{_tag} = $sig; + $self->{_name} = $name; + $self->{_name_length} = $name_length; + $self->{_key_path} = $key_path; + $self->{_flags} = $flags; + $self->{_offset_to_parent} = $offset_to_parent; + $self->{_num_subkeys} = $num_subkeys; + $self->{_offset_to_subkey_list} = $offset_to_subkey_list; + $self->{_num_values} = $num_values; + $self->{_offset_to_value_list} = $offset_to_value_list; + $self->{_timestamp} = unpack_windows_time($timestamp); + $self->{_offset_to_security} = $offset_to_security; + $self->{_offset_to_class_name} = $offset_to_class_name; + $self->{_class_name_length} = $class_name_length; + $self->{_class_name} = $class_name; + bless $self, $class; + + return $self; +} + +sub get_timestamp { + my $self = shift; + + return $self->{_timestamp}; +} + +sub get_timestamp_as_string { + my $self = shift; + + return iso8601($self->get_timestamp); +} + +sub get_class_name { + my $self = shift; + + return $self->{_class_name}; +} + +sub is_root { + my $self = shift; + + my $flags = $self->{_flags}; + return $flags & 4 || $flags & 8; +} + +sub get_parent { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset_to_parent = $self->{_offset_to_parent}; + my $key_path = $self->{_key_path}; + + return if $self->is_root; + + my $grandparent_key_path; + my @keys = split /\\/, $key_path, -1; + if (@keys > 2) { + $grandparent_key_path = join('\\', @keys[0..$#keys-2]); + } + + return Parse::Win32Registry::WinNT::Key->new($regfile, + $offset_to_parent, + $grandparent_key_path); +} + +sub get_security { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $offset_to_security = $self->{_offset_to_security}; + my $key_path = $self->{_key_path}; + + if ($offset_to_security == 0xffffffff) { + return; + } + + return Parse::Win32Registry::WinNT::Security->new($regfile, + $offset_to_security, + $key_path); +} + +sub as_string { + my $self = shift; + + my $string = $self->get_path . ' [' . $self->get_timestamp_as_string . ']'; + return $string; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x nk len=0x%x alloc=%d "%s" par=0x%x keys=%d,0x%x vals=%d,0x%x sec=0x%x class=0x%x', + $self->{_offset}, + $self->{_length}, + $self->{_allocated}, + $self->{_name}, + $self->{_offset_to_parent}, + $self->{_num_subkeys}, $self->{_offset_to_subkey_list}, + $self->{_num_values}, $self->{_offset_to_value_list}, + $self->{_offset_to_security}, + $self->{_offset_to_class_name}; + if (defined $self->{_class_name}) { + $info .= sprintf ',len=0x%x', $self->{_class_name_length}; + } + return $info; +} + +sub _get_offsets_to_subkeys { + my $self = shift; + + # Offset is passed as a parameter for recursive lists such as 'ri' + my $offset_to_subkey_list = shift || $self->{_offset_to_subkey_list}; + + my $regfile = $self->{_regfile}; + my $fh = $regfile->get_filehandle; + + return if $offset_to_subkey_list == 0xffffffff + || $self->{_num_subkeys} == 0; + + sysseek($fh, $offset_to_subkey_list, 0); + my $bytes_read = sysread($fh, my $subkey_list_header, 8); + if ($bytes_read != 8) { + warnf('Could not read subkey list header at 0x%x', + $offset_to_subkey_list); + return; + } + + # 0x00 dword = subkey list length (negative = allocated) + # 0x04 word = 'lf' signature + # 0x06 word = number of entries + # 0x08 dword = offset to 1st subkey + # 0x0c dword = first four characters of the key name + # 0x10 dword = offset to 2nd subkey + # 0x14 dword = first four characters of the key name + # ... + + # 0x00 dword = subkey list length (negative = allocated) + # 0x04 word = 'lh' signature + # 0x06 word = number of entries + # 0x08 dword = offset to 1st subkey + # 0x0c dword = hash of the key name + # 0x10 dword = offset to 2nd subkey + # 0x14 dword = hash of the key name + # ... + + # 0x00 dword = subkey list length (negative = allocated) + # 0x04 word = 'ri' signature + # 0x06 word = number of entries in ri list + # 0x08 dword = offset to 1st lf/lh/li list + # 0x0c dword = offset to 2nd lf/lh/li list + # 0x10 dword = offset to 3rd lf/lh/li list + # ... + + # 0x00 dword = subkey list length (negative = allocated) + # 0x04 word = 'li' signature + # 0x06 word = number of entries in li list + # 0x08 dword = offset to 1st subkey + # 0x0c dword = offset to 2nd subkey + # ... + + # Extracted offsets are always relative to first hbin + + my @offsets_to_subkeys = (); + + my ($length, + $sig, + $num_entries, + ) = unpack('Va2v', $subkey_list_header); + + my $subkey_list_length; + if ($sig eq 'lf' || $sig eq 'lh') { + $subkey_list_length = 2 * 4 * $num_entries; + } + elsif ($sig eq 'ri' || $sig eq 'li') { + $subkey_list_length = 4 * $num_entries; + } + else { + warnf('Invalid signature for subkey list at 0x%x', + $offset_to_subkey_list); + return; + } + + $bytes_read = sysread($fh, my $subkey_list, $subkey_list_length); + if ($bytes_read != $subkey_list_length) { + warnf('Could not read subkey list at 0x%x', + $offset_to_subkey_list); + return; + } + + if ($sig eq 'lf') { + foreach my $offset (unpack("(Vx4)$num_entries", $subkey_list)) { + push @offsets_to_subkeys, OFFSET_TO_FIRST_HBIN + $offset; + } + } + elsif ($sig eq 'lh') { + foreach my $offset (unpack("(Vx4)$num_entries", $subkey_list)) { + push @offsets_to_subkeys, OFFSET_TO_FIRST_HBIN + $offset; + } + } + elsif ($sig eq 'ri') { + foreach my $offset (unpack("V$num_entries", $subkey_list)) { + my $offsets_ref = + $self->_get_offsets_to_subkeys(OFFSET_TO_FIRST_HBIN + $offset); + if (defined $offsets_ref && ref $offsets_ref eq 'ARRAY') { + push @offsets_to_subkeys, @{ $offsets_ref }; + } + } + } + elsif ($sig eq 'li') { + foreach my $offset (unpack("V$num_entries", $subkey_list)) { + push @offsets_to_subkeys, OFFSET_TO_FIRST_HBIN + $offset; + } + } + + return \@offsets_to_subkeys; +} + +sub get_subkey_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $key_path = $self->{_key_path}; + + my @offsets_to_subkeys = (); + if ($self->{_num_subkeys} > 0) { + my $offsets_to_subkeys_ref = $self->_get_offsets_to_subkeys; + if (defined $offsets_to_subkeys_ref) { + @offsets_to_subkeys = @{$self->_get_offsets_to_subkeys}; + } + } + + return Parse::Win32Registry::Iterator->new(sub { + while (defined(my $offset_to_subkey = shift @offsets_to_subkeys)) { + my $subkey = Parse::Win32Registry::WinNT::Key->new($regfile, + $offset_to_subkey, $key_path); + if (defined $subkey) { + return $subkey; + } + } + return; # no more offsets to subkeys + }); +} + +sub _get_offsets_to_values { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $fh = $regfile->get_filehandle; + my $offset_to_value_list = $self->{_offset_to_value_list}; + + my $num_values = $self->{_num_values}; + return if $num_values == 0; + # Actually, this could probably just fall through + # as unpack("x4V0", ...) would return an empty array. + + my @offsets_to_values = (); + + # 0x00 dword = value list length (negative = allocated) + # 0x04 dword = 1st offset + # 0x08 dword = 2nd offset + # ... + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset_to_value_list, 0); + my $value_list_length = 0x4 + $num_values * 4; + my $bytes_read = sysread($fh, my $value_list, $value_list_length); + if ($bytes_read != $value_list_length) { + warnf("Could not read value list at 0x%x", + $offset_to_value_list); + return; + } + + foreach my $offset (unpack("x4V$num_values", $value_list)) { + push @offsets_to_values, OFFSET_TO_FIRST_HBIN + $offset; + } + + return \@offsets_to_values; +} + +sub get_value_iterator { + my $self = shift; + + my $regfile = $self->{_regfile}; + my $key_path = $self->{_key_path}; + + my @offsets_to_values = (); + if ($self->{_num_values} > 0) { + my $offsets_to_values_ref = $self->_get_offsets_to_values; + if (defined $offsets_to_values_ref) { + @offsets_to_values = @{$self->_get_offsets_to_values}; + } + } + + return Parse::Win32Registry::Iterator->new(sub { + while (defined(my $offset_to_value = shift @offsets_to_values)) { + my $value = Parse::Win32Registry::WinNT::Value->new($regfile, + $offset_to_value); + if (defined $value) { + return $value; + } + } + return; # no more offsets to values + }); +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/WinNT/Security.pm b/thirdparty/rr/Parse/Win32Registry/WinNT/Security.pm new file mode 100644 index 0000000000..7311cbc8ac --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/WinNT/Security.pm @@ -0,0 +1,157 @@ +package Parse::Win32Registry::WinNT::Security; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Entry); + +use Carp; +use Parse::Win32Registry::Base qw(:all); + +use constant SK_HEADER_LENGTH => 0x18; +use constant OFFSET_TO_FIRST_HBIN => 0x1000; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to sk record relative to start of file + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + if (defined(my $cache = $regfile->{_security_cache})) { + if (exists $cache->{$offset}) { + return $cache->{$offset}; + } + } + + my $fh = $regfile->get_filehandle; + + # 0x00 dword = security length (negative = allocated) + # 0x04 word = 'sk' signature + # 0x08 dword = offset to previous sk + # 0x0c dword = offset to next sk + # 0x10 dword = ref count + # 0x14 dword = length of security descriptor + # 0x18 = start of security descriptor + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $sk_header, SK_HEADER_LENGTH); + if ($bytes_read != SK_HEADER_LENGTH) { + warnf('Could not read security at 0x%x', $offset); + return; + } + + my ($length, + $sig, + $offset_to_previous, + $offset_to_next, + $ref_count, + $sd_length, + ) = unpack('Va2x2VVVV', $sk_header); + + $offset_to_previous += OFFSET_TO_FIRST_HBIN + if $offset_to_previous != 0xffffffff; + $offset_to_next += OFFSET_TO_FIRST_HBIN + if $offset_to_next != 0xffffffff; + + my $allocated = 0; + if ($length > 0x7fffffff) { + $allocated = 1; + $length = (0xffffffff - $length) + 1; + } + # allocated should be true + + if ($sig ne 'sk') { + warnf('Invalid signature for security at 0x%x', $offset); + return; + } + + $bytes_read = sysread($fh, my $sd_data, $sd_length); + if ($bytes_read != $sd_length) { + warnf('Could not read security descriptor for security at 0x%x', + $offset); + return; + } + + my $sd = unpack_security_descriptor($sd_data); + if (!defined $sd) { + warnf('Invalid security descriptor for security at 0x%x', + $offset); + # Abandon security object if security descriptor is invalid + return; + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_allocated} = $allocated; + $self->{_tag} = $sig; + $self->{_offset_to_previous} = $offset_to_previous; + $self->{_offset_to_next} = $offset_to_next; + $self->{_ref_count} = $ref_count; + $self->{_security_descriptor_length} = $sd_length; + $self->{_security_descriptor} = $sd; + bless $self, $class; + + if (defined(my $cache = $regfile->{_security_cache})) { + $cache->{$offset} = $self; + } + + return $self; +} + +sub get_previous { + my $self = shift; + my $regfile = $self->{_regfile}; + my $offset_to_previous = $self->{_offset_to_previous}; + + return Parse::Win32Registry::WinNT::Security->new($regfile, + $offset_to_previous); +} + +sub get_next { + my $self = shift; + my $regfile = $self->{_regfile}; + my $offset_to_next = $self->{_offset_to_next}; + + return Parse::Win32Registry::WinNT::Security->new($regfile, + $offset_to_next); +} + +sub get_reference_count { + my $self = shift; + + return $self->{_ref_count}; +} + +sub get_security_descriptor { + my $self = shift; + + return $self->{_security_descriptor}; +} + +sub as_string { + my $self = shift; + + return '(security entry)'; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x sk len=0x%x alloc=%d prev=0x%x,next=0x%x refs=%d', + $self->{_offset}, + $self->{_length}, + $self->{_allocated}, + $self->{_offset_to_previous}, + $self->{_offset_to_next}, + $self->{_ref_count}; + + return $info; +} + +1; diff --git a/thirdparty/rr/Parse/Win32Registry/WinNT/Value.pm b/thirdparty/rr/Parse/Win32Registry/WinNT/Value.pm new file mode 100644 index 0000000000..b9e882b29c --- /dev/null +++ b/thirdparty/rr/Parse/Win32Registry/WinNT/Value.pm @@ -0,0 +1,332 @@ +package Parse::Win32Registry::WinNT::Value; + +use strict; +use warnings; + +use base qw(Parse::Win32Registry::Value); + +use Carp; +use Encode; +use Parse::Win32Registry::Base qw(:all); + +use constant VK_HEADER_LENGTH => 0x18; +use constant OFFSET_TO_FIRST_HBIN => 0x1000; + +sub new { + my $class = shift; + my $regfile = shift; + my $offset = shift; # offset to vk record relative to first hbin + + croak 'Missing registry file' if !defined $regfile; + croak 'Missing offset' if !defined $offset; + + my $fh = $regfile->get_filehandle; + + # 0x00 dword = value length (negative = allocated) + # 0x04 word = 'vk' signature + # 0x06 word = value name length + # 0x08 dword = value data length (bit 31 set => data stored inline) + # 0x0c dword = offset to data/inline data + # 0x10 dword = value type + # 0x14 word = flags (bit 1 set => compressed name) + # 0x16 word + # 0x18 = value name [for value name length bytes] + + # Extracted offsets are always relative to first hbin + + sysseek($fh, $offset, 0); + my $bytes_read = sysread($fh, my $vk_header, VK_HEADER_LENGTH); + if ($bytes_read != VK_HEADER_LENGTH) { + warnf('Could not read value at 0x%x', $offset); + return; + } + + my ($length, + $sig, + $name_length, + $data_length, + $offset_to_data, + $type, + $flags, + ) = unpack('Va2vVVVv', $vk_header); + + my $allocated = 0; + if ($length > 0x7fffffff) { + $allocated = 1; + $length = (0xffffffff - $length) + 1; + } + # allocated should be true + + if ($length < VK_HEADER_LENGTH) { + warnf('Invalid value entry length at 0x%x', $offset); + return; + } + + if ($sig ne 'vk') { + warnf('Invalid signature for value at 0x%x', $offset); + return; + } + + $bytes_read = sysread($fh, my $name, $name_length); + if ($bytes_read != $name_length) { + warnf('Could not read name for value at 0x%x', $offset); + return; + } + + if ($flags & 1) { + $name = decode($Parse::Win32Registry::Base::CODEPAGE, $name); + } + else { + $name = decode('UCS-2LE', $name); + }; + + # If the top bit of the data_length is set, then + # the value is inline and stored in the offset to data field (at 0xc). + my $data; + my $data_inline = $data_length >> 31; + if ($data_inline) { + # REG_DWORDs are always inline, but I've also seen + # REG_SZ, REG_BINARY, REG_EXPAND_SZ, and REG_NONE inline + $data_length &= 0x7fffffff; + if ($data_length > 4) { + warnf("Invalid inline data length for value '%s' at 0x%x", + $name, $offset); + $data = undef; + } + else { + # unpack inline data from header + $data = substr($vk_header, 0xc, $data_length); + } + } + else { + if ($offset_to_data != 0 && $offset_to_data != 0xffffffff) { + $offset_to_data += OFFSET_TO_FIRST_HBIN; + if ($offset_to_data < ($regfile->get_length - $data_length)) { + $data = _extract_data($fh, $offset_to_data, $data_length); + } + else { + warnf("Invalid offset to data for value '%s' at 0x%x", + $name, $offset); + } + } + } + + my $self = {}; + $self->{_regfile} = $regfile; + $self->{_offset} = $offset; + $self->{_length} = $length; + $self->{_allocated} = $allocated; + $self->{_tag} = $sig; + $self->{_name} = $name; + $self->{_name_length} = $name_length; + $self->{_type} = $type; + $self->{_data} = $data; + $self->{_data_length} = $data_length; + $self->{_data_inline} = $data_inline; + $self->{_offset_to_data} = $offset_to_data; + $self->{_flags} = $flags; + bless $self, $class; + + return $self; +} + +sub _extract_data { + my $fh = shift; + my $offset_to_data = shift; + my $data_length = shift; + + if ($offset_to_data == 0 || $offset_to_data == 0xffffffff) { + return undef; + } + + sysseek($fh, $offset_to_data, 0); + my $bytes_read = sysread($fh, my $data_header, 4); + if ($bytes_read != 4) { + warnf('Could not read data at 0x%x', $offset_to_data); + return undef; + } + + my ($max_data_length) = unpack('V', $data_header); + + my $data_allocated = 0; + if ($max_data_length > 0x7fffffff) { + $data_allocated = 1; + $max_data_length = (0xffffffff - $max_data_length) + 1; + } + # data_allocated should be true + + my $data; + + if ($data_length > $max_data_length) { + $bytes_read = sysread($fh, my $db_entry, 8); + if ($bytes_read != 8) { + warnf('Could not read data at 0x%x', $offset_to_data); + return undef; + } + + my ($sig, $num_data_blocks, $offset_to_data_block_list) + = unpack('a2vV', $db_entry); + if ($sig ne 'db') { + warnf('Invalid signature for big data at 0x%x', $offset_to_data); + return undef; + } + $offset_to_data_block_list += OFFSET_TO_FIRST_HBIN; + + sysseek($fh, $offset_to_data_block_list + 4, 0); + $bytes_read = sysread($fh, my $data_block_list, $num_data_blocks * 4); + if ($bytes_read != $num_data_blocks * 4) { + warnf('Could not read data block list at 0x%x', + $offset_to_data_block_list); + return undef; + } + + $data = ""; + my @offsets = map { OFFSET_TO_FIRST_HBIN + $_ } + unpack("V$num_data_blocks", $data_block_list); + foreach my $offset (@offsets) { + sysseek($fh, $offset, 0); + $bytes_read = sysread($fh, my $block_header, 4); + if ($bytes_read != 4) { + warnf('Could not read data block at 0x%x', $offset); + return undef; + } + my ($block_length) = unpack('V', $block_header); + if ($block_length > 0x7fffffff) { + $block_length = (0xffffffff - $block_length) + 1; + } + $bytes_read = sysread($fh, my $block_data, $block_length - 8); + if ($bytes_read != $block_length - 8) { + warnf('Could not read data block at 0x%x', $offset); + return undef; + } + $data .= $block_data; + } + if (length($data) < $data_length) { + warnf("Insufficient data blocks for data at 0x%x", $offset_to_data); + return undef; + } + $data = substr($data, 0, $data_length); + return $data; + } + else { + $bytes_read = sysread($fh, $data, $data_length); + if ($bytes_read != $data_length) { + warnf("Could not read data at 0x%x", $offset_to_data); + return undef; + } + } + return $data; +} + +sub get_data { + my $self = shift; + + my $type = $self->get_type; + + my $data = $self->{_data}; + return if !defined $data; + + # apply decoding to appropriate data types + if ($type == REG_DWORD) { + if (length($data) == 4) { + $data = unpack('V', $data); + } + else { + # incorrect length for dword data + $data = undef; + } + } + elsif ($type == REG_DWORD_BIG_ENDIAN) { + if (length($data) == 4) { + $data = unpack('N', $data); + } + else { + # incorrect length for dword data + $data = undef; + } + } + elsif ($type == REG_SZ || $type == REG_EXPAND_SZ) { + $data = decode('UCS-2LE', $data); + # snip off any terminating null + chop $data if substr($data, -1, 1) eq "\0"; + } + elsif ($type == REG_MULTI_SZ) { + $data = decode('UCS-2LE', $data); + # snip off any terminating nulls + chop $data if substr($data, -1, 1) eq "\0"; + chop $data if substr($data, -1, 1) eq "\0"; + my @multi_sz = split("\0", $data, -1); + # make sure there is at least one empty string + @multi_sz = ('') if @multi_sz == 0; + return wantarray ? @multi_sz : join($", @multi_sz); + } + + return $data; +} + +sub as_regedit_export { + my $self = shift; + my $version = shift || 5; + + my $name = $self->get_name; + my $export = $name eq '' ? '@=' : '"' . $name . '"='; + + my $type = $self->get_type; + + # XXX +# if (!defined $self->{_data}) { +# $name = $name eq '' ? '@' : qq{"$name"}; +# return qq{; $name=(invalid data)\n}; +# } + + if ($type == REG_SZ) { + $export .= '"' . $self->get_data . '"'; + $export .= "\n"; + } + elsif ($type == REG_BINARY) { + $export .= "hex:"; + $export .= format_octets($self->{_data}, length($export)); + } + elsif ($type == REG_DWORD) { + my $data = $self->get_data; + $export .= defined($data) + ? sprintf("dword:%08x", $data) + : "dword:"; + $export .= "\n"; + } + elsif ($type == REG_EXPAND_SZ || $type == REG_MULTI_SZ) { + my $data = $version == 4 + ? encode("ascii", $self->{_data}) # unicode->ascii + : $self->{_data}; # raw data + $export .= sprintf("hex(%x):", $type); + $export .= format_octets($data, length($export)); + } + else { + $export .= sprintf("hex(%x):", $type); + $export .= format_octets($self->{_data}, length($export)); + } + return $export; +} + +sub parse_info { + my $self = shift; + + my $info = sprintf '0x%x vk len=0x%x alloc=%d "%s" type=%d', + $self->{_offset}, + $self->{_length}, + $self->{_allocated}, + $self->{_name}, + $self->{_type}; + if ($self->{_data_inline}) { + $info .= sprintf ' data=inline,len=0x%x', + $self->{_data_length}; + } + else { + $info .= sprintf ' data=0x%x,len=0x%x', + $self->{_offset_to_data}, + $self->{_data_length}; + } + return $info; +} + +1;