This commit is contained in:
U-BASIS\smoss 2013-06-18 16:20:02 -04:00
commit 7e81142c50
10 changed files with 194 additions and 121 deletions

View File

@ -68,9 +68,5 @@ abstract class ContentTypePanel extends JPanel {
*/
abstract public void select();
@Override
abstract public void addPropertyChangeListener(PropertyChangeListener pcl);
@Override
abstract public void removePropertyChangeListener(PropertyChangeListener pcl);
}

View File

@ -31,8 +31,8 @@ import javax.swing.event.DocumentListener;
* ImageTypePanel for adding an image file such as .img, .E0x, .00x, etc.
*/
public class ImageFilePanel extends ContentTypePanel implements DocumentListener {
private static ImageFilePanel instance;
private PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private static ImageFilePanel instance = null;
private PropertyChangeSupport pcs = null;
private JFileChooser fc = new JFileChooser();
/**
@ -47,19 +47,27 @@ public class ImageFilePanel extends ContentTypePanel implements DocumentListener
fc.addChoosableFileFilter(AddImageVisualPanel1.rawFilter);
fc.addChoosableFileFilter(AddImageVisualPanel1.encaseFilter);
fc.setFileFilter(AddImageVisualPanel1.allFilter);
pathTextField.getDocument().addDocumentListener(this);
}
/**
* Returns the default instance of a ImageFilePanel.
*/
public static ImageFilePanel getDefault() {
public static synchronized ImageFilePanel getDefault() {
if (instance == null) {
instance = new ImageFilePanel();
instance.postInit();
}
return instance;
}
//post-constructor initialization to properly initialize listener support
//without leaking references of uninitialized objects
private void postInit() {
pathTextField.getDocument().addDocumentListener(this);
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
@ -217,12 +225,20 @@ public class ImageFilePanel extends ContentTypePanel implements DocumentListener
}
@Override
public void addPropertyChangeListener(PropertyChangeListener pcl) {
public synchronized void addPropertyChangeListener(PropertyChangeListener pcl) {
super.addPropertyChangeListener(pcl);
if (pcs == null) {
pcs = new PropertyChangeSupport(this);
}
pcs.addPropertyChangeListener(pcl);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener pcl) {
super.removePropertyChangeListener(pcl);
pcs.removePropertyChangeListener(pcl);
}

View File

@ -45,7 +45,7 @@ import org.sleuthkit.autopsy.coreutils.PlatformUtil;
*/
public class LocalDiskPanel extends ContentTypePanel {
private static LocalDiskPanel instance;
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private PropertyChangeSupport pcs = null;
private List<LocalDisk> disks = new ArrayList<LocalDisk>();
private LocalDiskModel model;
private boolean enableNext = false;
@ -61,13 +61,14 @@ public class LocalDiskPanel extends ContentTypePanel {
/**
* Get the default instance of this panel.
*/
public static LocalDiskPanel getDefault() {
public static synchronized LocalDiskPanel getDefault() {
if (instance == null) {
instance = new LocalDiskPanel();
}
return instance;
}
private void customInit() {
model = new LocalDiskModel();
diskComboBox.setModel(model);
@ -189,12 +190,20 @@ public class LocalDiskPanel extends ContentTypePanel {
}
@Override
public void addPropertyChangeListener(PropertyChangeListener pcl) {
public synchronized void addPropertyChangeListener(PropertyChangeListener pcl) {
super.addPropertyChangeListener(pcl);
if (pcs == null) {
pcs = new PropertyChangeSupport(this);
}
pcs.addPropertyChangeListener(pcl);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener pcl) {
super.removePropertyChangeListener(pcl);
pcs.removePropertyChangeListener(pcl);
}

View File

@ -30,7 +30,7 @@ import javax.swing.JFileChooser;
*/
public class LocalFilesPanel extends ContentTypePanel {
private PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private PropertyChangeSupport pcs = null;
private Set<File> currentFiles = new TreeSet<File>(); //keep currents in a set to disallow duplicates per add
private boolean enableNext = false;
private static LocalFilesPanel instance;
@ -55,7 +55,6 @@ public class LocalFilesPanel extends ContentTypePanel {
localFileChooser.setMultiSelectionEnabled(true);
selectedPaths.setText("");
}
@Override
@ -103,15 +102,24 @@ public class LocalFilesPanel extends ContentTypePanel {
}
@Override
public void addPropertyChangeListener(PropertyChangeListener pcl) {
public synchronized void addPropertyChangeListener(PropertyChangeListener pcl) {
super.addPropertyChangeListener(pcl);
if (pcs == null) {
pcs = new PropertyChangeSupport(this);
}
pcs.addPropertyChangeListener(pcl);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener pcl) {
super.removePropertyChangeListener(pcl);
pcs.removePropertyChangeListener(pcl);
}
@Override
public String toString() {
return "Logical Files";

View File

@ -41,6 +41,7 @@ import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.SolrInputDocument;
import org.openide.util.Exceptions;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.ContentUtils;
import org.sleuthkit.autopsy.keywordsearch.Server.SolrServerNoPortException;
@ -52,10 +53,10 @@ import org.sleuthkit.datamodel.DerivedFile;
import org.sleuthkit.datamodel.Directory;
import org.sleuthkit.datamodel.File;
import org.sleuthkit.datamodel.FsContent;
import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.LayoutFile;
import org.sleuthkit.datamodel.LocalFile;
import org.sleuthkit.datamodel.ReadContentInputStream;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
/**
@ -184,6 +185,12 @@ public class Ingester {
private class GetContentFieldsV extends ContentVisitor.Default<Map<String, String>> {
private SleuthkitCase curCase = null;
GetContentFieldsV() {
curCase = Case.getCurrentCase().getSleuthkitCase();
}
@Override
protected Map<String, String> defaultVisit(Content cntnt) {
return new HashMap<String, String>();
@ -217,11 +224,7 @@ public class Ingester {
@Override
public Map<String, String> visit(LocalFile lf) {
final Map<String, String> params = new HashMap<String, String>();
params.put(Server.Schema.ID.toString(), Long.toString(lf.getId()));
params.put(Server.Schema.FILE_NAME.toString(), lf.getName());
params.put(Server.Schema.IMAGE_ID.toString(), Long.toString(-1));
return params;
return getCommonFields(lf);
}
private Map<String, String> getCommonFsContentFields(Map<String, String> params, FsContent fsContent) {
@ -235,15 +238,13 @@ public class Ingester {
private Map<String, String> getCommonFields(AbstractFile af) {
Map<String, String> params = new HashMap<String, String>();
params.put(Server.Schema.ID.toString(), Long.toString(af.getId()));
long imageId = -1;
long dataSourceId = -1;
try {
Image image = af.getImage();
if (image != null) {
imageId = image.getId();
}
params.put(Server.Schema.IMAGE_ID.toString(), Long.toString(imageId));
dataSourceId = curCase.getFileDataSource(af);
params.put(Server.Schema.IMAGE_ID.toString(), Long.toString(dataSourceId));
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Could not get image id to properly index the file " + af.getId());
logger.log(Level.SEVERE, "Could not get data source id to properly index the file " + af.getId());
params.put(Server.Schema.IMAGE_ID.toString(), Long.toString(-1));
}
params.put(Server.Schema.FILE_NAME.toString(), af.getName());

View File

@ -18,6 +18,10 @@
*/
package org.sleuthkit.autopsy.keywordsearch;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
*
* Filter to restrict query only specific files, chunks, images
@ -27,23 +31,23 @@ public class KeywordQueryFilter {
public static enum FilterType {
FILE, CHUNK, IMAGE
FILE, CHUNK, DATA_SOURCE
};
private long[] idFilters;
private Set<Long>idFilters;
private FilterType filterType;
public KeywordQueryFilter(FilterType filterType, long id) {
this.filterType = filterType;
this.idFilters = new long[1];
this.idFilters[0] = id;
this.idFilters = new HashSet<Long>();
this.idFilters.add(id);
}
public KeywordQueryFilter(FilterType filterType, long[] ids) {
public KeywordQueryFilter(FilterType filterType, Set<Long>ids) {
this.filterType = filterType;
this.idFilters = ids;
}
public long[] getIdFilters() {
public Set<Long> getIdFilters() {
return idFilters;
}
@ -55,12 +59,14 @@ public class KeywordQueryFilter {
public String toString() {
StringBuilder sb = new StringBuilder();
String id = null;
for (int i = 0; i < idFilters.length; ++i) {
Iterator<Long>it = idFilters.iterator();
for (int i = 0; it.hasNext(); ++i) {
if (i > 0) {
sb.append(" "); //OR
}
long idVal = idFilters[i];
if (filterType == FilterType.IMAGE) {
long idVal = it.next();
if (filterType == FilterType.DATA_SOURCE) {
id = Server.Schema.IMAGE_ID.toString();
} else {
id = Server.Schema.ID.toString();

View File

@ -44,6 +44,7 @@ import org.netbeans.api.progress.aggregate.AggregateProgressFactory;
import org.netbeans.api.progress.aggregate.AggregateProgressHandle;
import org.netbeans.api.progress.aggregate.ProgressContributor;
import org.openide.util.Cancellable;
import org.openide.util.Exceptions;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.EscapeUtil;
import org.sleuthkit.autopsy.coreutils.StopWatch;
@ -60,6 +61,7 @@ import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.ReadContentInputStream;
import org.sleuthkit.datamodel.SleuthkitCase;
@ -115,7 +117,7 @@ public final class KeywordSearchIngestModule extends IngestModuleAbstractFile {
private Map<Keyword, List<Long>> currentResults;
//only search images from current ingest, not images previously ingested/indexed
//accessed read-only by searcher thread
private Set<Long> curImageIds;
private Set<Long> curDataSourceIds;
private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy
private static final Lock searcherLock = rwLock.writeLock();
private volatile int messageID = 0;
@ -129,6 +131,7 @@ public final class KeywordSearchIngestModule extends IngestModuleAbstractFile {
private KeywordSearchConfigurationPanel panel;
private Tika tikaFormatDetector;
private enum IngestStatus {
INGESTED, EXTRACTED_INGESTED, SKIPPED, INGESTED_META
@ -160,12 +163,10 @@ public final class KeywordSearchIngestModule extends IngestModuleAbstractFile {
return ProcessResult.OK;
}
try {
//add image id of the file to the set, keeping track of images being ingested
final Image fileImage = abstractFile.getImage();
if (fileImage != null) {
//not all Content objects have an image associated (e.g. LocalFiles)
curImageIds.add(fileImage.getId());
}
//add data source id of the file to the set, keeping track of images being ingested
final long fileSourceId = caseHandle.getFileDataSource(abstractFile);
curDataSourceIds.add(fileSourceId);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error getting image id of file processed by keyword search: " + abstractFile.getName(), ex);
}
@ -288,7 +289,7 @@ public final class KeywordSearchIngestModule extends IngestModuleAbstractFile {
private void cleanup() {
ingestStatus.clear();
currentResults.clear();
curImageIds.clear();
curDataSourceIds.clear();
currentSearcher = null;
//finalSearcher = null; //do not collect, might be finalizing
@ -399,7 +400,7 @@ public final class KeywordSearchIngestModule extends IngestModuleAbstractFile {
//keeps track of all results per run not to repeat reporting the same hits
currentResults = new HashMap<Keyword, List<Long>>();
curImageIds = new HashSet<Long>();
curDataSourceIds = new HashSet<Long>();
indexer = new Indexer();
@ -930,15 +931,10 @@ public final class KeywordSearchIngestModule extends IngestModuleAbstractFile {
del = new TermComponentQuery(keywordQuery);
}
//limit search to currently ingested images
final long imageIds[] = new long[curImageIds.size()];
final Iterator<Long> it = curImageIds.iterator();
for (int imageI = 0; it.hasNext(); ++imageI) {
imageIds[imageI] = it.next();
}
//limit search to currently ingested data sources
//set up a filter with 1 or more image ids OR'ed
final KeywordQueryFilter imageFilter = new KeywordQueryFilter(KeywordQueryFilter.FilterType.IMAGE, imageIds);
del.addFilter(imageFilter);
final KeywordQueryFilter dataSourceFilter = new KeywordQueryFilter(KeywordQueryFilter.FilterType.DATA_SOURCE, curDataSourceIds);
del.addFilter(dataSourceFilter);
Map<String, List<ContentHit>> queryResult = null;

View File

@ -3,6 +3,7 @@
New features:
- Logical files and folders support
- New file views in directory tree to view: deleted, executable, archive files and files by size
- ext4 and yaffs2 support (via TSK 4.1.0)
Improvements:
- Improvements to tagging of files and keyword search results
@ -14,6 +15,7 @@ Bugfixes:
- fixed result viewer for "File Search by MD5 Hash"
- fix Solr, Timeline and RecentActivity issues with java 7.0.21
- Views->Recent Files showing inconsistent results when clicked many times
- reduced memory usage in Timeline
---------------- VERSION 3.0.5 --------------

View File

@ -77,13 +77,13 @@ import org.openide.awt.ActionReferences;
import org.openide.awt.ActionRegistration;
import org.openide.modules.InstalledFileLocator;
import org.openide.modules.ModuleInstall;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.actions.CallableSystemAction;
import org.openide.util.actions.Presenter;
import org.openide.util.lookup.Lookups;
import org.openide.windows.WindowManager;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.corecomponents.DataContentPanel;
@ -98,6 +98,7 @@ import org.sleuthkit.autopsy.datamodel.FileNode;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.coreutils.ExecUtil;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
@ -106,6 +107,11 @@ import org.sleuthkit.datamodel.TskCoreException;
@ActionReferences(value = {
@ActionReference(path = "Menu/Tools", position = 100)})
@NbBundle.Messages(value = "CTL_TimelineView=Generate Timeline")
/**
* The Timeline Action entry point. Collects data and pushes data to javafx
* widgets
*
*/
public class Timeline extends CallableSystemAction implements Presenter.Toolbar, PropertyChangeListener {
private static final Logger logger = Logger.getLogger(Timeline.class.getName());
@ -478,8 +484,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Day of Month");
yAxis.setLabel("Number of Events");
ObservableList<BarChart.Data<String,Number>> bcData
= makeObservableListByMonthAllDays(me, ye.getYear());
ObservableList<BarChart.Data<String, Number>> bcData = makeObservableListByMonthAllDays(me, ye.getYear());
BarChart.Series<String, Number> series = new BarChart.Series<String, Number>(bcData);
series.setName(me.getMonthName() + " " + ye.getYear());
@ -503,9 +508,18 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
@Override
public void handle(MouseEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
//reset the view and free the current nodes before loading new ones
final FileRootNode d = new FileRootNode("Empty Root", new ArrayList<Long>());
dataResultPanel.setNode(d);
dataResultPanel.setPath("Loading...");
}
});
final int day = (Integer.valueOf((barData.getXValue()).split("-")[1]));
final DayEpoch de = myme.getDay(day);
final List<AbstractFile> afs;
final List<Long> afs;
if (de != null) {
afs = de.getEvents();
} else {
@ -516,7 +530,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
final FileRootNode d = new FileRootNode("Test Root", afs);
final FileRootNode d = new FileRootNode("Root", afs);
dataResultPanel.setNode(d);
//set result viewer title path with the current date
String dateString = ye.getYear() + "-" + (1 + me.getMonthInt()) + "-" + +de.dayNum;
@ -693,7 +707,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
return month;
}
public void add(AbstractFile af, int month, int day) {
public void add(long fileId, int month, int day) {
// see if this month is in the list
MonthEpoch monthEpoch = null;
for (MonthEpoch me : months) {
@ -709,7 +723,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
}
// add the file the the MonthEpoch object
monthEpoch.add(af, day);
monthEpoch.add(fileId, day);
}
}
@ -752,7 +766,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
return de;
}
public void add(AbstractFile af, int day) {
public void add(long fileId, int day) {
DayEpoch dayEpoch = null;
for (DayEpoch de : days) {
if (de.getDayInt() == day) {
@ -766,7 +780,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
days.add(dayEpoch);
}
dayEpoch.add(af);
dayEpoch.add(fileId);
}
/**
@ -786,7 +800,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
private class DayEpoch extends Epoch {
private List<AbstractFile> files = new ArrayList<>();
private final List<Long> fileIds = new ArrayList<>();
int dayNum = 0; //Day of the month this Epoch represents, 1 indexed: 28=28.
DayEpoch(int dayOfMonth) {
@ -799,40 +813,68 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
@Override
public int getNumFiles() {
return files.size();
return fileIds.size();
}
public void add(AbstractFile af) {
files.add(af);
public void add(long fileId) {
fileIds.add(fileId);
}
List<AbstractFile> getEvents() {
return this.files;
List<Long> getEvents() {
return this.fileIds;
}
}
// The node factories used to make lists of files to send to the result viewer
private class FileNodeChildFactory extends ChildFactory<AbstractFile> {
// using the lazy loading (rather than background) loading option to facilitate
// loading a huge number of nodes for the given day
private class FileNodeChildFactory extends Children.Keys<Long> {
List<AbstractFile> l;
private List<Long> fileIds;
FileNodeChildFactory(List<AbstractFile> l) {
this.l = l;
FileNodeChildFactory(List<Long> fileIds) {
super(true);
this.fileIds = fileIds;
}
@Override
protected boolean createKeys(List<AbstractFile> list) {
list.addAll(l);
return true;
protected void addNotify() {
super.addNotify();
setKeys(fileIds);
}
@Override
protected Node createNodeForKey(AbstractFile file) {
protected void removeNotify() {
super.removeNotify();
setKeys(new ArrayList<Long>());
}
@Override
protected Node[] createNodes(Long t) {
return new Node[]{createNodeForKey(t)};
}
// @Override
// protected boolean createKeys(List<Long> list) {
// list.addAll(fileIds);
// return true;
// }
//@Override
protected Node createNodeForKey(Long fileId) {
AbstractFile af = null;
try {
af = skCase.getAbstractFileById(fileId);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error getting file by id and creating a node in Timeline: " + fileId, ex);
//no node will be shown for this object
return null;
}
Node wrapped;
if (file.isDir()) {
wrapped = new DirectoryNode(file, false);
if (af.isDir()) {
wrapped = new DirectoryNode(af, false);
} else {
wrapped = new FileNode(file, false);
wrapped = new FileNode(af, false);
}
return new FilterNodeLeaf(wrapped);
}
@ -840,8 +882,9 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
private class FileRootNode extends DisplayableItemNode {
FileRootNode(String NAME, List<AbstractFile> l) {
super(Children.create(new FileNodeChildFactory(l), true));
FileRootNode(String NAME, List<Long> fileIds) {
//super(Children.create(new FileNodeChildFactory(fileIds), true));
super(new FileNodeChildFactory(fileIds), Lookups.singleton(fileIds));
super.setName(NAME);
super.setDisplayName(NAME);
}
@ -887,16 +930,8 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
prevYear = year;
}
// create and add the file
AbstractFile file;
try {
file = skCase.getAbstractFileById(ObjId);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Could not find a file with ID " + ObjId, ex);
continue;
}
if (ye != null) {
ye.add(file, month, day);
ye.add(ObjId, month, day);
}
}
@ -923,11 +958,11 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
+ java.io.File.separator + currentCase.getName() + "-" + datenotime + ".txt";
// Run query to get all files
String filesAndDirs = "name != '.' "
final String filesAndDirs = "name != '.' "
+ "AND name != '..'";
List<AbstractFile> files = null;
List<Long> fileIds = null;
try {
files = skCase.findAllFilesWhere(filesAndDirs);
fileIds = skCase.findAllFileIdsWhere(filesAndDirs);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error querying image files to make a body file: " + bodyFilePath, ex);
return null;
@ -945,7 +980,8 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
BufferedWriter out = null;
try {
out = new BufferedWriter(fileWriter);
for (AbstractFile file : files) {
for (long fileId : fileIds) {
AbstractFile file = skCase.getAbstractFileById(fileId);
// try {
// MD5|name|inode|mode_as_string|ObjId|GID|size|atime|mtime|ctime|crtime
if (file.getMd5Hash() != null) {
@ -985,6 +1021,10 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
out.write(Long.toString(file.getCrtime()));
out.write("\n");
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error querying file by id", ex);
return null;
} catch (IOException ex) {
logger.log(Level.WARNING, "Error while trying to write data to the body file.", ex);
return null;
@ -1030,8 +1070,7 @@ public class Timeline extends CallableSystemAction implements Presenter.Toolbar,
} catch (IOException ioe) {
logger.log(Level.SEVERE, "Could not create mactime file, encountered error ", ioe);
return null;
}
finally {
} finally {
if (writer != null) {
try {
writer.close();

View File

@ -11,7 +11,7 @@
<body>
<p align="center" style="font-size: 145%;"><strong>Autopsy 3 Quick Start Guide</strong></p>
<p align="center" style="font-size: 120%;">June 2012</p>
<p align="center" style="font-size: 120%;">June 2013</p>
<p align="center"><a href="http://www.sleuthkit.org/autopsy/">www.sleuthkit.org/autopsy/</a></p>