/* * Autopsy Forensic Browser * * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> 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.datamodel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Set; import java.util.logging.Level; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.nodes.Sheet; 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.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.SleuthkitCase.CaseDbQuery; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskException; /** * Keyword hits node support */ public class KeywordHits implements AutopsyVisitableItem { private SleuthkitCase skCase; private static final Logger logger = Logger.getLogger(KeywordHits.class.getName()); private static final String KEYWORD_HITS = NbBundle.getMessage(KeywordHits.class, "KeywordHits.kwHits.text"); public static final String NAME = BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getLabel(); public static final String SIMPLE_LITERAL_SEARCH = NbBundle .getMessage(KeywordHits.class, "KeywordHits.simpleLiteralSearch.text"); public static final String SIMPLE_REGEX_SEARCH = NbBundle .getMessage(KeywordHits.class, "KeywordHits.singleRegexSearch.text"); private final KeywordResults keywordResults; public KeywordHits(SleuthkitCase skCase) { this.skCase = skCase; keywordResults = new KeywordResults(); } private final class KeywordResults extends Observable { // Map from listName/Type to Map of keyword to set of artifact Ids // NOTE: the map can be accessed by multiple worker threads and needs to be synchronized private final Map<String, Map<String, Set<Long>>> topLevelMap = new LinkedHashMap<>(); KeywordResults() { update(); } List<String> getListNames() { synchronized (topLevelMap) { List<String> names = new ArrayList<>(topLevelMap.keySet()); // this causes the "Single ..." terms to be in the middle of the results, // which is wierd. Make a custom comparator or do something else to maek them on top //Collections.sort(names); return names; } } List<String> getKeywords(String listName) { List<String> keywords; synchronized (topLevelMap) { keywords = new ArrayList<>(topLevelMap.get(listName).keySet()); } Collections.sort(keywords); return keywords; } Set<Long> getArtifactIds(String listName, String keyword) { synchronized (topLevelMap) { return topLevelMap.get(listName).get(keyword); } } // populate maps based on artifactIds void populateMaps(Map<Long, Map<Long, String>> artifactIds) { synchronized (topLevelMap) { topLevelMap.clear(); // map of list name to keword to artifact IDs Map<String, Map<String, Set<Long>>> listsMap = new LinkedHashMap<>(); // Map from from literal keyword to artifact IDs Map<String, Set<Long>> literalMap = new LinkedHashMap<>(); // Map from regex keyword artifact IDs Map<String, Set<Long>> regexMap = new LinkedHashMap<>(); // top-level nodes topLevelMap.put(SIMPLE_LITERAL_SEARCH, literalMap); topLevelMap.put(SIMPLE_REGEX_SEARCH, regexMap); for (Map.Entry<Long, Map<Long, String>> art : artifactIds.entrySet()) { long id = art.getKey(); Map<Long, String> attributes = art.getValue(); // I think we can use attributes.remove(...) here? String listName = attributes.get(Long.valueOf(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID())); String word = attributes.get(Long.valueOf(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID())); String reg = attributes.get(Long.valueOf(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP.getTypeID())); // part of a list if (listName != null) { if (listsMap.containsKey(listName) == false) { listsMap.put(listName, new LinkedHashMap<String, Set<Long>>()); } Map<String, Set<Long>> listMap = listsMap.get(listName); if (listMap.containsKey(word) == false) { listMap.put(word, new HashSet<Long>()); } listMap.get(word).add(id); } // regular expression, single term else if (reg != null) { if (regexMap.containsKey(reg) == false) { regexMap.put(reg, new HashSet<Long>()); } regexMap.get(reg).add(id); } // literal, single term else { if (literalMap.containsKey(word) == false) { literalMap.put(word, new HashSet<Long>()); } literalMap.get(word).add(id); } topLevelMap.putAll(listsMap); } } setChanged(); notifyObservers(); } @SuppressWarnings("deprecation") public void update() { Map<Long, Map<Long, String>> artifactIds = new LinkedHashMap<>(); if (skCase == null) { return; } int setId = BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID(); int wordId = BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID(); int regexId = BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP.getTypeID(); int artId = BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID(); String query = "SELECT blackboard_attributes.value_text,blackboard_attributes.artifact_id," //NON-NLS + "blackboard_attributes.attribute_type_id FROM blackboard_attributes,blackboard_artifacts WHERE " //NON-NLS + "(blackboard_attributes.artifact_id=blackboard_artifacts.artifact_id AND " //NON-NLS + "blackboard_artifacts.artifact_type_id=" + artId //NON-NLS + ") AND (attribute_type_id=" + setId + " OR " //NON-NLS + "attribute_type_id=" + wordId + " OR " //NON-NLS + "attribute_type_id=" + regexId + ")"; //NON-NLS try (CaseDbQuery dbQuery = skCase.executeQuery(query)) { ResultSet resultSet = dbQuery.getResultSet(); while (resultSet.next()) { String value = resultSet.getString("value_text"); //NON-NLS long artifactId = resultSet.getLong("artifact_id"); //NON-NLS long typeId = resultSet.getLong("attribute_type_id"); //NON-NLS if (!artifactIds.containsKey(artifactId)) { artifactIds.put(artifactId, new LinkedHashMap<Long, String>()); } if (!value.equals("")) { artifactIds.get(artifactId).put(typeId, value); } } } catch (TskCoreException | SQLException ex) { logger.log(Level.WARNING, "SQL Exception occurred: ", ex); //NON-NLS } populateMaps(artifactIds); } } @Override public <T> T accept(AutopsyItemVisitor<T> v) { return v.visit(this); } // Created by CreateAutopsyNodeVisitor public class RootNode extends DisplayableItemNode { public RootNode() { super(Children.create(new ListFactory(), true), Lookups.singleton(KEYWORD_HITS)); super.setName(NAME); super.setDisplayName(KEYWORD_HITS); this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/keyword_hits.png"); //NON-NLS } @Override public boolean isLeafTypeNode() { return false; } @Override public <T> T accept(DisplayableItemNodeVisitor<T> v) { return v.visit(this); } @Override protected Sheet createSheet() { Sheet s = super.createSheet(); Sheet.Set ss = s.get(Sheet.PROPERTIES); if (ss == null) { ss = Sheet.createPropertiesSet(); s.put(ss); } ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.name.name"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.name.displayName"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.name.desc"), getName())); return s; } @Override public String getItemType() { return getClass().getName(); } } private class ListFactory extends ChildFactory.Detachable<String> implements Observer { private final PropertyChangeListener pcl = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { /** * Checking for a current case is a stop gap measure until a * different way of handling the closing of cases is worked * out. Currently, remote events may be received for a case * that is already closed. */ try { Case.getCurrentCase(); /** * Even with the check above, it is still possible that * the case will be closed in a different thread before * this code executes. If that happens, it is possible * for the event to have a null oldValue. */ ModuleDataEvent eventData = (ModuleDataEvent) evt.getOldValue(); if (null != eventData && eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) { keywordResults.update(); } } catch (IllegalStateException notUsed) { /** * Case is closed, do nothing. */ } } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { /** * Checking for a current case is a stop gap measure until a * different way of handling the closing of cases is worked * out. Currently, remote events may be received for a case * that is already closed. */ try { Case.getCurrentCase(); keywordResults.update(); } catch (IllegalStateException notUsed) { /** * Case is closed, do nothing. */ } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { // case was closed. Remove listeners so that we don't get called with a stale case handle if (evt.getNewValue() == null) { removeNotify(); skCase = null; } } } }; @Override protected void addNotify() { IngestManager.getInstance().addIngestJobEventListener(pcl); IngestManager.getInstance().addIngestModuleEventListener(pcl); Case.addPropertyChangeListener(pcl); keywordResults.update(); keywordResults.addObserver(this); } @Override protected void removeNotify() { IngestManager.getInstance().removeIngestJobEventListener(pcl); IngestManager.getInstance().removeIngestModuleEventListener(pcl); Case.removePropertyChangeListener(pcl); keywordResults.deleteObserver(this); } @Override protected boolean createKeys(List<String> list) { list.addAll(keywordResults.getListNames()); return true; } @Override protected Node createNodeForKey(String key) { return new ListNode(key); } @Override public void update(Observable o, Object arg) { refresh(true); } } public class ListNode extends DisplayableItemNode implements Observer { private final String listName; public ListNode(String listName) { super(Children.create(new TermFactory(listName), true), Lookups.singleton(listName)); super.setName(listName); this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/keyword_hits.png"); //NON-NLS this.listName = listName; updateDisplayName(); keywordResults.addObserver(this); } private void updateDisplayName() { int totalDescendants = 0; for (String word : keywordResults.getKeywords(listName)) { Set<Long> ids = keywordResults.getArtifactIds(listName, word); totalDescendants += ids.size(); } super.setDisplayName(listName + " (" + totalDescendants + ")"); } @Override protected Sheet createSheet() { Sheet s = super.createSheet(); Sheet.Set ss = s.get(Sheet.PROPERTIES); if (ss == null) { ss = Sheet.createPropertiesSet(); s.put(ss); } ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.listName.name"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.listName.displayName"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.listName.desc"), listName)); ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.numChildren.name"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.numChildren.displayName"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.numChildren.desc"), keywordResults.getKeywords(listName).size())); return s; } @Override public boolean isLeafTypeNode() { return false; } @Override public <T> T accept(DisplayableItemNodeVisitor<T> v) { return v.visit(this); } @Override public void update(Observable o, Object arg) { updateDisplayName(); } @Override public String getItemType() { return getClass().getName(); } } private class TermFactory extends ChildFactory.Detachable<String> implements Observer { private final String setName; private TermFactory(String setName) { super(); this.setName = setName; } @Override protected void addNotify() { keywordResults.addObserver(this); } @Override protected void removeNotify() { keywordResults.deleteObserver(this); } @Override protected boolean createKeys(List<String> list) { list.addAll(keywordResults.getKeywords(setName)); return true; } @Override protected Node createNodeForKey(String key) { return new TermNode(setName, key); } @Override public void update(Observable o, Object arg) { refresh(true); } } public class TermNode extends DisplayableItemNode implements Observer { private final String setName; private final String keyword; public TermNode(String setName, String keyword) { super(Children.create(new HitsFactory(setName, keyword), true), Lookups.singleton(keyword)); super.setName(keyword); this.setName = setName; this.keyword = keyword; this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/keyword_hits.png"); //NON-NLS updateDisplayName(); keywordResults.addObserver(this); } private void updateDisplayName() { super.setDisplayName(keyword + " (" + keywordResults.getArtifactIds(setName, keyword).size() + ")"); } @Override public void update(Observable o, Object arg) { updateDisplayName(); } @Override public boolean isLeafTypeNode() { return true; } @Override public <T> T accept(DisplayableItemNodeVisitor<T> v) { return v.visit(this); } @Override protected Sheet createSheet() { Sheet s = super.createSheet(); Sheet.Set ss = s.get(Sheet.PROPERTIES); if (ss == null) { ss = Sheet.createPropertiesSet(); s.put(ss); } ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.listName.name"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.listName.displayName"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.listName.desc"), getDisplayName())); ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.filesWithHits.name"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.filesWithHits.displayName"), NbBundle.getMessage(this.getClass(), "KeywordHits.createSheet.filesWithHits.desc"), keywordResults.getArtifactIds(setName, keyword).size())); return s; } @Override public String getItemType() { return getClass().getName(); } } public class HitsFactory extends ChildFactory.Detachable<Long> implements Observer { private final String keyword; private final String setName; public HitsFactory(String setName, String keyword) { super(); this.setName = setName; this.keyword = keyword; } @Override protected void addNotify() { keywordResults.addObserver(this); } @Override protected void removeNotify() { keywordResults.deleteObserver(this); } @Override protected boolean createKeys(List<Long> list) { list.addAll(keywordResults.getArtifactIds(setName, keyword)); return true; } @Override protected Node createNodeForKey(Long artifactId) { if (skCase == null) { return null; } try { BlackboardArtifact art = skCase.getBlackboardArtifact(artifactId); BlackboardArtifactNode n = new BlackboardArtifactNode(art); AbstractFile file; try { file = skCase.getAbstractFileById(art.getObjectID()); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "TskCoreException while constructing BlackboardArtifact Node from KeywordHitsKeywordChildren"); //NON-NLS return n; } // It is possible to get a keyword hit on artifacts generated // for the underlying image in which case MAC times are not // available/applicable/useful. if (file == null) { return n; } n.addNodeProperty(new NodeProperty<>( NbBundle.getMessage(this.getClass(), "KeywordHits.createNodeForKey.modTime.name"), NbBundle.getMessage(this.getClass(), "KeywordHits.createNodeForKey.modTime.displayName"), NbBundle.getMessage(this.getClass(), "KeywordHits.createNodeForKey.modTime.desc"), ContentUtils.getStringTime(file.getMtime(), file))); n.addNodeProperty(new NodeProperty<>( NbBundle.getMessage(this.getClass(), "KeywordHits.createNodeForKey.accessTime.name"), NbBundle.getMessage(this.getClass(), "KeywordHits.createNodeForKey.accessTime.displayName"), NbBundle.getMessage(this.getClass(), "KeywordHits.createNodeForKey.accessTime.desc"), ContentUtils.getStringTime(file.getAtime(), file))); n.addNodeProperty(new NodeProperty<>( NbBundle.getMessage(this.getClass(), "KeywordHits.createNodeForKey.chgTime.name"), NbBundle.getMessage(this.getClass(), "KeywordHits.createNodeForKey.chgTime.displayName"), NbBundle.getMessage(this.getClass(), "KeywordHits.createNodeForKey.chgTime.desc"), ContentUtils.getStringTime(file.getCtime(), file))); return n; } catch (TskException ex) { logger.log(Level.WARNING, "TSK Exception occurred", ex); //NON-NLS } return null; } @Override public void update(Observable o, Object arg) { refresh(true); } } }