/*
* Autopsy Forensic Browser
*
* Copyright 2011 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.util.Arrays;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Level;
import org.openide.nodes.AbstractNode;
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.core.UserPreferences;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ContentVisitor;
import org.sleuthkit.datamodel.DerivedFile;
import org.sleuthkit.datamodel.Directory;
import org.sleuthkit.datamodel.File;
import org.sleuthkit.datamodel.LayoutFile;
import org.sleuthkit.datamodel.LocalFile;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
/**
* Filters database results by file extension.
*/
public final class FileTypesByExtension implements AutopsyVisitableItem {
private final SleuthkitCase skCase;
public FileTypesByExtension(SleuthkitCase skCase) {
this.skCase = skCase;
}
public SleuthkitCase getSleuthkitCase() {
return this.skCase;
}
@Override
public <T> T accept(AutopsyItemVisitor<T> v) {
return v.visit(this);
}
/**
* Listens for case and ingest invest. Updates observers when events are
* fired. FileType and FileTypes nodes are all listening to this.
*/
private static class FileTypesByExtObservable extends Observable {
private FileTypesByExtObservable() {
super();
IngestManager.getInstance().addIngestJobEventListener(pcl);
IngestManager.getInstance().addIngestModuleEventListener(pcl);
Case.addPropertyChangeListener(pcl);
}
private void removeListeners() {
deleteObservers();
IngestManager.getInstance().removeIngestJobEventListener(pcl);
IngestManager.getInstance().removeIngestModuleEventListener(pcl);
Case.removePropertyChangeListener(pcl);
}
private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> {
String eventType = evt.getPropertyName();
if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString()) || eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString()) || eventType.equals(Case.Events.DATA_SOURCE_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();
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) {
removeListeners();
}
}
};
private void update() {
setChanged();
notifyObservers();
}
}
/**
* Node for root of file types view. Children are nodes for specific types.
*/
static class FileTypesByExtNode extends DisplayableItemNode {
private static final String FNAME = NbBundle.getMessage(FileTypesByExtNode.class, "FileTypesByExtNode.fname.text");
private final FileTypesByExtension.RootFilter filter;
/**
*
* @param skCase
* @param filter null to display root node of file type tree, pass in
* something to provide a sub-node.
*/
FileTypesByExtNode(SleuthkitCase skCase, FileTypesByExtension.RootFilter filter) {
super(Children.create(new FileTypesByExtNodeChildren(skCase, filter, null), true), Lookups.singleton(filter == null ? FNAME : filter.getName()));
this.filter = filter;
init();
}
/**
*
* @param skCase
* @param filter
* @param o Observable that was created by a higher-level node that
* provides updates on events
*/
private FileTypesByExtNode(SleuthkitCase skCase, FileTypesByExtension.RootFilter filter, Observable o) {
super(Children.create(new FileTypesByExtNodeChildren(skCase, filter, o), true), Lookups.singleton(filter == null ? FNAME : filter.getName()));
this.filter = filter;
init();
}
private void init() {
// root node of tree
if (filter == null) {
super.setName(FNAME);
super.setDisplayName(FNAME);
} // sub-node in file tree (i.e. documents, exec, etc.)
else {
super.setName(filter.getName());
super.setDisplayName(filter.getDisplayName());
}
this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file_types.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(), "FileTypesByExtNode.createSheet.name.name"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.name.displayName"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.name.desc"), getName()));
return s;
}
@Override
public String getItemType() {
/**
* Because Documents and Executable are further expandable, their
* column order settings should be stored separately.
*/
if (filter == null) {
return getClass().getName();
}
if (filter.equals(FileTypesByExtension.RootFilter.TSK_DOCUMENT_FILTER) || filter.equals(FileTypesByExtension.RootFilter.TSK_EXECUTABLE_FILTER)) {
return getClass().getName() + filter.getName();
}
return getClass().getName();
}
private static class FileTypesByExtNodeChildren extends ChildFactory<FileTypesByExtension.SearchFilterInterface> {
private final SleuthkitCase skCase;
private final FileTypesByExtension.RootFilter filter;
private final Observable notifier;
/**
*
* @param skCase
* @param filter Is null for root node
* @param o Observable that provides updates based on events
* being fired (or null if one needs to be created)
*/
private FileTypesByExtNodeChildren(SleuthkitCase skCase, FileTypesByExtension.RootFilter filter, Observable o) {
super();
this.skCase = skCase;
this.filter = filter;
if (o == null) {
this.notifier = new FileTypesByExtObservable();
} else {
this.notifier = o;
}
}
@Override
protected boolean createKeys(List<FileTypesByExtension.SearchFilterInterface> list) {
// root node
if (filter == null) {
list.addAll(Arrays.asList(FileTypesByExtension.RootFilter.values()));
} // document and executable has another level of nodes
else if (filter.equals(FileTypesByExtension.RootFilter.TSK_DOCUMENT_FILTER)) {
list.addAll(Arrays.asList(FileTypesByExtension.DocumentFilter.values()));
} else if (filter.equals(FileTypesByExtension.RootFilter.TSK_EXECUTABLE_FILTER)) {
list.addAll(Arrays.asList(FileTypesByExtension.ExecutableFilter.values()));
}
return true;
}
@Override
protected Node createNodeForKey(FileTypesByExtension.SearchFilterInterface key) {
// make new nodes for the sub-nodes
if (key.getName().equals(FileTypesByExtension.RootFilter.TSK_DOCUMENT_FILTER.getName())) {
return new FileTypesByExtNode(skCase, FileTypesByExtension.RootFilter.TSK_DOCUMENT_FILTER, notifier);
} else if (key.getName().equals(FileTypesByExtension.RootFilter.TSK_EXECUTABLE_FILTER.getName())) {
return new FileTypesByExtNode(skCase, FileTypesByExtension.RootFilter.TSK_EXECUTABLE_FILTER, notifier);
} else {
return new FileExtensionNode(key, skCase, notifier);
}
}
}
}
/**
* Node for a specific file type / extension. Children of it will be the
* files of that type.
*/
static class FileExtensionNode extends DisplayableItemNode {
FileTypesByExtension.SearchFilterInterface filter;
SleuthkitCase skCase;
/**
*
* @param filter Extensions that will be shown for this node
* @param skCase
* @param o Observable that sends updates when the child factories
* should refresh
*/
FileExtensionNode(FileTypesByExtension.SearchFilterInterface filter, SleuthkitCase skCase, Observable o) {
super(Children.create(new FileExtensionNodeChildren(filter, skCase, o), true), Lookups.singleton(filter.getDisplayName()));
this.filter = filter;
this.skCase = skCase;
init();
o.addObserver(new ByExtNodeObserver());
}
private void init() {
super.setName(filter.getName());
updateDisplayName();
this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file-filter-icon.png"); //NON-NLS
}
// update the display name when new events are fired
private class ByExtNodeObserver implements Observer {
@Override
public void update(Observable o, Object arg) {
updateDisplayName();
}
}
private void updateDisplayName() {
final long count = FileExtensionNodeChildren.calculateItems(skCase, filter);
super.setDisplayName(filter.getDisplayName() + " (" + count + ")");
}
@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(), "FileTypesByExtNode.createSheet.filterType.name"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.filterType.displayName"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.filterType.desc"), filter.getDisplayName()));
String extensions = "";
for (String ext : filter.getFilter()) {
extensions += "'" + ext + "', ";
}
extensions = extensions.substring(0, extensions.lastIndexOf(','));
ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.fileExt.name"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.fileExt.displayName"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.fileExt.desc"), extensions));
return s;
}
@Override
public boolean isLeafTypeNode() {
return true;
}
/**
* Consider allowing different configurations for Images, Videos, etc
* (in which case we'd return getClass().getName() + filter.getName()
* for all filters).
*/
@Override
public String getItemType() {
return DisplayableItemNode.FILE_PARENT_NODE_KEY;
}
/**
* Child node factory for a specific file type - does the database
* query.
*/
private static class FileExtensionNodeChildren extends ChildFactory.Detachable<Content> {
private final SleuthkitCase skCase;
private final FileTypesByExtension.SearchFilterInterface filter;
private static final Logger LOGGER = Logger.getLogger(FileExtensionNodeChildren.class.getName());
private final Observable notifier;
/**
*
* @param filter Extensions to display
* @param skCase
* @param o Observable that will notify when there could be new
* data to display
*/
private FileExtensionNodeChildren(FileTypesByExtension.SearchFilterInterface filter, SleuthkitCase skCase, Observable o) {
super();
this.filter = filter;
this.skCase = skCase;
notifier = o;
}
@Override
protected void addNotify() {
if (notifier != null) {
notifier.addObserver(observer);
}
}
@Override
protected void removeNotify() {
if (notifier != null) {
notifier.deleteObserver(observer);
}
}
private final Observer observer = new FileTypeChildFactoryObserver();
// Cause refresh of children if there are changes
private class FileTypeChildFactoryObserver implements Observer {
@Override
public void update(Observable o, Object arg) {
refresh(true);
}
}
/**
* Get children count without actually loading all nodes
*
* @return
*/
private static long calculateItems(SleuthkitCase sleuthkitCase, FileTypesByExtension.SearchFilterInterface filter) {
try {
return sleuthkitCase.countFilesWhere(createQuery(filter));
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Error getting file search view count", ex); //NON-NLS
return 0;
}
}
@Override
protected boolean createKeys(List<Content> list) {
try {
List<AbstractFile> files = skCase.findAllFilesWhere(createQuery(filter));
list.addAll(files);
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Couldn't get search results", ex); //NON-NLS
}
return true;
}
private static String createQuery(FileTypesByExtension.SearchFilterInterface filter) {
StringBuilder query = new StringBuilder();
query.append("(dir_type = ").append(TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue()).append(")"); //NON-NLS
if (UserPreferences.hideKnownFilesInViewsTree()) {
query.append(" AND (known IS NULL OR known != ").append(TskData.FileKnown.KNOWN.getFileKnownValue()).append(")"); //NON-NLS
}
query.append(" AND (NULL"); //NON-NLS
for (String s : filter.getFilter()) {
query.append(" OR LOWER(name) LIKE LOWER('%").append(s).append("')"); //NON-NLS
}
query.append(')');
return query.toString();
}
@Override
protected Node createNodeForKey(Content key) {
return key.accept(new ContentVisitor.Default<AbstractNode>() {
@Override
public FileNode visit(File f) {
return new FileNode(f, false);
}
@Override
public DirectoryNode visit(Directory d) {
return new DirectoryNode(d);
}
@Override
public LayoutFileNode visit(LayoutFile lf) {
return new LayoutFileNode(lf);
}
@Override
public LocalFileNode visit(DerivedFile df) {
return new LocalFileNode(df);
}
@Override
public LocalFileNode visit(LocalFile lf) {
return new LocalFileNode(lf);
}
@Override
protected AbstractNode defaultVisit(Content di) {
throw new UnsupportedOperationException(NbBundle.getMessage(this.getClass(), "FileTypeChildren.exception.notSupported.msg", di.toString()));
}
});
}
}
}
// root node filters
public static enum RootFilter implements AutopsyVisitableItem, SearchFilterInterface {
TSK_IMAGE_FILTER(0, "TSK_IMAGE_FILTER", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskImgFilter.text"),
FileTypeExtensions.getImageExtensions()),
TSK_VIDEO_FILTER(1, "TSK_VIDEO_FILTER", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskVideoFilter.text"),
FileTypeExtensions.getVideoExtensions()),
TSK_AUDIO_FILTER(2, "TSK_AUDIO_FILTER", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskAudioFilter.text"),
FileTypeExtensions.getAudioExtensions()),
TSK_ARCHIVE_FILTER(3, "TSK_ARCHIVE_FILTER", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskArchiveFilter.text"),
FileTypeExtensions.getArchiveExtensions()),
TSK_DOCUMENT_FILTER(3, "TSK_DOCUMENT_FILTER", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskDocumentFilter.text"),
Arrays.asList(".doc", ".docx", ".pdf", ".xls", ".rtf", ".txt")), //NON-NLS
TSK_EXECUTABLE_FILTER(3, "TSK_EXECUTABLE_FILTER", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskExecFilter.text"),
Arrays.asList(".exe", ".dll", ".bat", ".cmd", ".com")); //NON-NLS
private final int id;
private final String name;
private final String displayName;
private final List<String> filter;
private RootFilter(int id, String name, String displayName, List<String> filter) {
this.id = id;
this.name = name;
this.displayName = displayName;
this.filter = filter;
}
@Override
public <T> T accept(AutopsyItemVisitor<T> v) {
return v.visit(this);
}
@Override
public String getName() {
return this.name;
}
@Override
public int getId() {
return this.id;
}
@Override
public String getDisplayName() {
return this.displayName;
}
@Override
public List<String> getFilter() {
return this.filter;
}
}
// document sub-node filters
public static enum DocumentFilter implements AutopsyVisitableItem, SearchFilterInterface {
AUT_DOC_HTML(0, "AUT_DOC_HTML", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autDocHtmlFilter.text"),
Arrays.asList(".htm", ".html")), //NON-NLS
AUT_DOC_OFFICE(1, "AUT_DOC_OFFICE", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autDocOfficeFilter.text"),
Arrays.asList(".doc", ".docx", ".odt", ".xls", ".xlsx", ".ppt", ".pptx")), //NON-NLS
AUT_DOC_PDF(2, "AUT_DOC_PDF", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autoDocPdfFilter.text"),
Arrays.asList(".pdf")), //NON-NLS
AUT_DOC_TXT(3, "AUT_DOC_TXT", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autDocTxtFilter.text"),
Arrays.asList(".txt")), //NON-NLS
AUT_DOC_RTF(4, "AUT_DOC_RTF", //NON-NLS
NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autDocRtfFilter.text"),
Arrays.asList(".rtf")); //NON-NLS
private final int id;
private final String name;
private final String displayName;
private final List<String> filter;
private DocumentFilter(int id, String name, String displayName, List<String> filter) {
this.id = id;
this.name = name;
this.displayName = displayName;
this.filter = filter;
}
@Override
public <T> T accept(AutopsyItemVisitor<T> v) {
return v.visit(this);
}
@Override
public String getName() {
return this.name;
}
@Override
public int getId() {
return this.id;
}
@Override
public String getDisplayName() {
return this.displayName;
}
@Override
public List<String> getFilter() {
return this.filter;
}
}
// executable sub-node filters
public static enum ExecutableFilter implements AutopsyVisitableItem, SearchFilterInterface {
ExecutableFilter_EXE(0, "ExecutableFilter_EXE", ".exe", Arrays.asList(".exe")), //NON-NLS
ExecutableFilter_DLL(1, "ExecutableFilter_DLL", ".dll", Arrays.asList(".dll")), //NON-NLS
ExecutableFilter_BAT(2, "ExecutableFilter_BAT", ".bat", Arrays.asList(".bat")), //NON-NLS
ExecutableFilter_CMD(3, "ExecutableFilter_CMD", ".cmd", Arrays.asList(".cmd")), //NON-NLS
ExecutableFilter_COM(4, "ExecutableFilter_COM", ".com", Arrays.asList(".com")); //NON-NLS
private final int id;
private final String name;
private final String displayName;
private final List<String> filter;
private ExecutableFilter(int id, String name, String displayName, List<String> filter) {
this.id = id;
this.name = name;
this.displayName = displayName;
this.filter = filter;
}
@Override
public <T> T accept(AutopsyItemVisitor<T> v) {
return v.visit(this);
}
@Override
public String getName() {
return this.name;
}
@Override
public int getId() {
return this.id;
}
@Override
public String getDisplayName() {
return this.displayName;
}
@Override
public List<String> getFilter() {
return this.filter;
}
}
interface SearchFilterInterface {
public String getName();
public int getId();
public String getDisplayName();
public List<String> getFilter();
}
}