#
# Timeline functions
#
# $Date: 2005/10/13 17:02:34 $
#
# Brian Carrier [carrier@sleuthkit.org]
# Copyright (c) 2001-2005 by Brian Carrier. All rights reserved
#
# This file is part of the Autopsy Forensic Browser (Autopsy)
#
# Autopsy is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Autopsy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Autopsy; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR ANY PARTICULAR PURPOSE.
# IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, DATA, OR PROFITS OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package Timeline;
use POSIX; # needed for tzset
# Changing the order of this may affect the main function ordering
$Timeline::BLANK = 0;
$Timeline::FRAME = 1;
$Timeline::TABS = 2;
$Timeline::BODY_ENTER = 3;
$Timeline::BODY_RUN = 4;
$Timeline::TL_ENTER = 5;
$Timeline::TL_RUN = 6;
$Timeline::VIEW_FR = 7;
$Timeline::VIEW_MENU = 8;
$Timeline::VIEW_IDX = 9;
$Timeline::VIEW_SUM = 10;
$Timeline::VIEW = 11;
# Types of modes for fname (i.e. can we overwrite it if it exists)
my $FNAME_MODE_INIT = 0;
my $FNAME_MODE_OVER = 1;
sub main {
return if ($::LIVE == 1);
# By default, show the main frame
$Args::args{'view'} = $Args::enc_args{'view'} = $Timeline::FRAME
unless (exists $Args::args{'view'});
Args::check_view();
my $view = Args::get_view();
if ($view < $Timeline::VIEW_FR) {
if ($view == $Timeline::BLANK) {
return blank();
}
elsif ($view == $Timeline::FRAME) {
return frame();
}
elsif ($view == $Timeline::TABS) {
return tabs();
}
elsif ($view == $Timeline::BODY_ENTER) {
return body_enter();
}
elsif ($view == $Timeline::BODY_RUN) {
return body_run();
}
elsif ($view == $Timeline::TL_ENTER) {
return tl_enter();
}
elsif ($view == $Timeline::TL_RUN) {
return tl_run();
}
}
else {
if ($view == $Timeline::VIEW_FR) {
return view_fr();
}
elsif ($view == $Timeline::VIEW_MENU) {
return view_menu();
}
elsif ($view == $Timeline::VIEW_IDX) {
return view_idx();
}
elsif ($view == $Timeline::VIEW_SUM) {
return view_sum();
}
elsif ($view == $Timeline::VIEW) {
return view();
}
}
Print::print_check_err("Invalid Timeline View");
}
# Call the appropriate function based on the value of sort
sub frame {
Print::print_html_header_frameset(
"Timeline: $Args::args{'case'}:$Args::args{'host'}");
print "
\n";
return;
}
elsif ($submod == $Timeline::TL_ENTER) {
$str .= "&body=$Args::args{'body'}" if (exists $Args::args{'body'});
}
elsif ($submod == $Timeline::VIEW_FR) {
$str .= "&tl=$Args::args{'tl'}" if (exists $Args::args{'tl'});
}
print
"\n\n";
Print::print_html_footer_frameset();
return 0;
}
# The tabs / button images in timeline view
sub tabs {
Args::check_submod();
Print::print_html_header_tabs("Timeline Mode Tabs");
my $submod = Args::get_submod();
print "
\n";
Print::print_html_footer_tabs();
return 0;
}
sub body_enter {
Print::print_html_header("Enter Data to Make Body File");
my $i;
my %mnt2img;
# Cycle through each image we read from fsmorgue
foreach $i (keys %Caseman::vol2mnt) {
next
unless ($Caseman::vol2cat{$i} eq "part");
next
if ( ($Caseman::vol2ftype{$i} eq "swap")
|| ($Caseman::vol2ftype{$i} eq "raw"));
$mnt2vol{"$Caseman::vol2mnt{$i}--$i"} = $i;
}
# sort via parent volume, then starting location, and then mount point (which includes the name)
my @mnt = sort {
($Caseman::vol2par{$mnt2vol{$a}} cmp $Caseman::vol2par{$mnt2vol{$b}})
or ($Caseman::vol2start{$mnt2vol{$a}} <=>
$Caseman::vol2start{$mnt2vol{$b}})
or (lc($a) cmp lc($b))
} keys %mnt2vol;
print "\n";
Print::print_html_footer();
return 0;
}
sub body_run {
Args::check_fname();
Args::check_fname_mode();
Print::print_html_header("Make Body File");
my $fname_rel = Args::get_fname_rel();
my $fname = Args::get_fname();
my $fname_mode = $Args::args{'fname_mode'};
if ((-e "$fname") && ($FNAME_MODE_INIT == $fname_mode)) {
print "File Already Exists: $fname_rel\n";
my $hidden = Args::make_hidden();
$hidden .=
"\n"
. "\n";
my $i;
foreach $i (%Caseman::vol2mnt) {
$hidden .= "\n"
if (exists $Args::args{$i});
}
$hidden .=
"\n"
if (exists $Args::args{'al_file'});
$hidden .=
"\n"
if (exists $Args::args{'unal_file'});
$hidden .=
"\n"
if (exists $Args::args{'unal_meta'});
$hidden .=
"\n"
if (exists $Args::args{'md5'});
# Make a new name
print "
\n";
Print::print_html_footer();
return 0;
}
my $OTYPE_NORM = 1;
my $OTYPE_HOURLY = 2;
my $OTYPE_DAILY = 3;
sub tl_enter {
Print::print_html_header("Enter data for timeline");
my @body;
# Find the body files if we will be looking for them
unless ((exists $Args::args{'body'})
&& (exists $Caseman::vol2cat{$Args::args{'body'}}))
{
foreach my $k (keys %Caseman::vol2cat) {
if ( ($Caseman::vol2cat{$k} eq "timeline")
&& ($Caseman::vol2ftype{$k} eq "body"))
{
push @body, $k;
}
}
if (scalar(@body) == 0) {
print "There are currently no body files "
. "for this host. You must create the intermediate "
. "data file before you can perform this step \n"
. "
"
. ""
. "\n";
return 1;
}
}
print "Now we will sort the data and save it to a timeline.
\n"
. "
\n"
. "\n"
. "\n"
. Args::make_hidden()
. "1. Select the data input file (body):\n"
. "
";
# if the body file was specified then just print it
if (exists $Args::args{'body'}) {
print "
\n";
$chk = "";
}
}
my $cur_mon = 1 + (localtime())[4];
my $cur_year = 1900 + (localtime())[5];
# STARTING DATE
print "
\n"
. "
2. Enter the starting date: \n"
. "None: "
. "Specify: "
. ""
. ""
. "\n";
# END DATE
print "
3. Enter the ending date: \n"
. "None: \n"
. "Specify: \n"
. "\n"
. ""
. "\n";
# FILE NAME
print "
4. Enter the file name to save as: "
. "$::DATADIR/ \n"
. "\n";
# Get only the UNIX images - since only they have /etc/passwd and group
my @unix_imgs;
my $root_vol = "";
foreach my $i (keys %Caseman::vol2ftype) {
my $f = $Caseman::vol2ftype{$i};
next
unless (($f =~ /^ext/)
|| ($f =~ /^ufs/)
|| ($f =~ /^linux/)
|| ($f =~ /bsd$/)
|| ($f =~ /^solaris$/)
|| ($f =~ /^bsdi$/));
push @unix_vols, $i;
# Keep a reference to an image with '/' as the mounting point
$root_vol = $i
if ($Caseman::vol2mnt{$i} eq '/');
}
my $cnt = 5;
if (scalar @unix_vols > 0) {
print
"
$cnt. Select the UNIX image that contains the /etc/passwd and /etc/group files: \n";
$cnt++;
print "\n";
}
print "
$cnt. Choose the output format: \n";
$cnt++;
print
" Tabulated (normal) \n"
. " Comma delimited with hourly summary \n"
. " Comma delimited with daily summary \n";
print "
";
return 0;
}
my $mon;
my $day;
my $year;
my $date = "";
# Get the start date
unless ((exists $Args::args{'st_none'}) && ($Args::args{'st_none'} == 1)) {
if (exists $Args::args{'st_mon'}) {
Args::check_st_mon();
$mon = Args::get_st_mon();
}
if (exists $Args::args{'st_year'}) {
Args::check_st_year();
$year = Args::get_st_year();
}
if ( (exists $Args::args{'st_day'})
&& ($Args::args{'st_day'} =~ /^(\d\d?)$/))
{
$day = $1;
if (($day < 1) || ($day > 31)) {
print("Invalid start day\n");
return 1;
}
}
else {
print("Invalid start day\n");
return 1;
}
$date = "$mon/$day/$year";
}
unless ((exists $Args::args{'end_none'}) && ($Args::args{'end_none'} == 1))
{
if ($date eq "") {
print "Begin date must be given if ending date is given ";
return 1;
}
if ( (exists $Args::args{'end_mon'})
&& ($Args::args{'end_mon'} =~ /^(\d\d?)$/))
{
$mon = $1;
if (($mon < 1) || ($mon > 12)) {
print("Invalid end month\n");
return 1;
}
}
else {
print("Invalid end month\n");
return 1;
}
if ( (exists $Args::args{'end_year'})
&& ($Args::args{'end_year'} =~ /^(\d\d\d\d)$/))
{
$year = $1;
if (($year < 1970) || ($year > 2020)) {
print("Invalid ending year\n");
return 1;
}
}
else {
print("Invalid end year\n");
return 1;
}
if ( (exists $Args::args{'end_day'})
&& ($Args::args{'end_day'} =~ /^(\d\d?)$/))
{
$day = $1;
if (($day < 1) || ($day > 31)) {
print("Invalid end day\n");
return 1;
}
}
else {
print("Invalid end day\n");
return 1;
}
$date .= "-$mon/$day/$year";
}
# temp strings for the password and group files
my $pw_tmp = "";
my $gr_tmp = "";
my $mac_args = "";
my $log = "";
local *OUT;
# Password and Group Files
if ((exists $Args::args{'pw_vol'}) && ($Args::args{'pw_vol'} ne "")) {
Args::check_vol('pw_vol');
my $pw_vol = Args::get_vol('pw_vol');
my $ftype = $Caseman::vol2ftype{$pw_vol};
my $img = $Caseman::vol2path{$pw_vol};
my $offset = $Caseman::vol2start{$pw_vol};
my $imgtype = $Caseman::vol2itype{$pw_vol};
$log .= "Password & Group File ($pw_vol) ";
# Get the passwd file meta and copy the file
Exec::exec_pipe(*OUT,
"'$::TSKDIR/ifind' -f $ftype -n 'etc/passwd' -o $offset -i $imgtype $img"
);
my $pwi = Exec::read_pipe_line(*OUT);
close(OUT);
$pwi = "Error getting meta for passwd"
if ((!defined $pwi) || ($pwi eq ""));
# Do the Taint Checking
if ($pwi =~ /^($::REG_META)$/) {
$pwi = $1;
$log .= "Password Meta Address ($pwi) ";
# Find a temp name that we can call it
my $i;
for ($i = 0;; $i++) {
unless (-e "$fname.pw-$i") {
$pw_tmp = "$fname.pw-$i";
last;
}
}
Exec::exec_sys(
"'$::TSKDIR/icat' -f $ftype -o $offset -i $imgtype $img $pwi > '$pw_tmp'"
);
$mac_args .= " -p \'$pw_tmp\' ";
}
else {
print(
"Error finding /etc/passwd meta in $Caseman::vol2sname{$pw_vol} ($pwi) "
);
Print::log_host_inv(
"$Caseman::vol2sname{$pw_vol}: /etc/passwd file not found for timeline"
);
}
# Get the group file meta and copy the file
Exec::exec_pipe(*OUT,
"'$::TSKDIR/ifind' -f $ftype -n 'etc/group' -o $offset -i $imgtype $img"
);
my $gri = Exec::read_pipe_line(*OUT);
close(OUT);
$gri = "Error getting meta for group"
if ((!defined $gri) || ($gri eq ""));
# Do the Taint Checking
if ($gri =~ /^($::REG_META)$/) {
$gri = $1;
$log .= "Group Meta Address ($gri) ";
# Find a temp name that we can call it
my $i;
for ($i = 0;; $i++) {
unless (-e "$fname.gr-$i") {
$gr_tmp = "$fname.gr-$i";
last;
}
}
Exec::exec_sys(
"'$::TSKDIR/icat' -f $ftype -o $offset -i $imgtype $img $gri > '$gr_tmp'"
);
$mac_args .= " -g \'$gr_tmp\' ";
}
else {
print(
"Error finding /etc/group meta in $Caseman::vol2sname{$pw_vol} ($gri) "
);
Print::log_host_inv(
"$Caseman::vol2sname{$pw_vol}: /etc/group file not found for timeline"
);
}
}
if ($date eq "") {
print
"Creating Timeline using all dates (Time Zone: $Caseman::tz) \n";
Print::log_host_inv(
"$Caseman::vol2sname{$body}: Creating timeline using all dates (TZ: $Caseman::tz) ${log}to $fname_rel"
);
}
else {
print "Creating Timeline for $date (Time Zone: $Caseman::tz) \n";
Print::log_host_inv(
"$Caseman::vol2sname{$body}: Creating timeline for $date (TZ: $Caseman::tz) ${log}to $fname_rel"
);
}
my $tz = "";
$tz = "-z '$Caseman::tz'" unless ("$Caseman::tz" eq "");
# mactime needs the path to run the 'date' command
$ENV{PATH} = "/bin:/usr/bin";
local *OUT;
if ($otype == $OTYPE_NORM) {
Exec::exec_pipe(*OUT,
"LANG=C LC_ALL=C '$::TSKDIR/mactime' -b $Caseman::vol2path{$body} $tz -i day '${fname}.sum' $mac_args $date > '$fname'"
);
}
elsif ($otype == $OTYPE_HOURLY) {
Exec::exec_pipe(*OUT,
"LANG=C LC_ALL=C '$::TSKDIR/mactime' -b $Caseman::vol2path{$body} $tz -d -i hour '${fname}.sum' $mac_args $date > '$fname'"
);
}
elsif ($otype == $OTYPE_DAILY) {
Exec::exec_pipe(*OUT,
"LANG=C LC_ALL=C '$::TSKDIR/mactime' -b $Caseman::vol2path{$body} $tz -d -i day '${fname}.sum' $mac_args $date > '$fname'"
);
}
else {
Print::print_err("Unknown output type");
}
print "$_ \n" while ($_ = Exec::read_pipe_line(*OUT));
close(OUT);
$ENV{PATH} = "";
# Remove the password and group files
unlink("$pw_tmp") if ($pw_tmp ne "");
unlink("$gr_tmp") if ($gr_tmp ne "");
print " Timeline saved to $fname
\n";
# append to fsmorgue if a normal timeline
if ($otype == $OTYPE_NORM) {
my $tl_vol = Caseman::add_vol_host_config("timeline", $fname_rel);
print "Entry added to host config file
\n"
. "(NOTE: It is easier to view the timeline in a text editor than here)";
}
else {
print
"Comma delimited files cannot be viewed from within Autopsy. \n"
. "Open it in a spreadsheet or other data processing tool. \n";
}
Print::print_html_footer();
return 0;
}
sub view_menu {
Print::print_html_header("View Timeline Menu");
my @tl;
# Find the timelines in the images hash
foreach my $k (keys %Caseman::vol2cat) {
if ( ($Caseman::vol2cat{$k} eq "timeline")
&& ($Caseman::vol2ftype{$k} eq "timeline"))
{
push @tl, $k;
}
}
if (scalar(@tl) == 0) {
print "There are currently no timeline files in the "
. "host config file. One must first be created before you "
. "can view it \n";
print "
\n";
Print::print_html_footer();
return 0;
}
# Display the contents of the summary file (hits per day) and show
# it as hits per month
sub view_sum {
Args::check_tl();
Print::print_html_header("View Timeline Summary");
my $tl_vol = Args::get_tl();
my $tl = $Caseman::vol2path{$tl_vol};
$tl .= ".sum";
open(TL, "<$tl") or die "Can not open $tl";
my $url =
"$::PROGNAME?$Args::baseargs&mod=$::MOD_TL&"
. "view=$Timeline::VIEW_FR&tl=$tl_vol";
print "
This page provides a monthly summary of activity. "
. "Each day that has activity is noted with the number of events \n";
my $p_year = "";
my $p_mon = "";
print "
\n";
while () {
my @a = split(/ /, $_);
next unless (scalar(@a) == 5);
my $mon = $::m2d{$a[1]};
my $year = $a[3];
$year = $1 if ($year =~ /^(\d{4,4}):$/);
if (($p_year ne $year) || ($p_mon ne $mon)) {
print "
\n";
close(TL);
Print::print_html_footer();
return 0;
}
# display a given month of the timeline
sub view {
Args::check_tl();
Args::check_st_mon();
Args::check_st_year();
my $tl_vol = Args::get_tl();
my $tl = $Caseman::vol2path{$tl_vol};
my $st_mon = Args::get_st_mon();
my $st_year = Args::get_st_year();
Print::print_html_header("View $st_mon, $st_year of Timeline");
unless (open(TL, "$tl")) {
print("Error opening $tl");
return (1);
}
Print::log_host_inv(
"$Args::args{'tl'}: Viewing timeline for $::d2m[$st_mon] $st_year");
print "
\n";
# zone identifies if we should be printing or not
my $zone = 0;
my $row = 0;
while () {
if (
/^(?:(\w\w\w\s+)?(\w\w\w\s+\d\d\s+\d\d\d\d)\s+(\d\d:\d\d:\d\d))?\s+(\d+)\s+([mac\.]+)\s+([-\/\?\w]+)\s+([\d\w\/]+)\s+([\d\w\/]+)\s+($::REG_META)\s+(.*)$/o
)
{
my $day = "";
$day = $1 if (defined $1);
my $date = "";
$date = $2 if (defined $2);
my $time = "";
$time = $3 if (defined $3);
my $sz = $4;
my $mac = $5;
my $p = $6;
my $u = $7;
my $g = $8;
my $i = $9;
my $f = $10;
# we must break this down to see if we can skip it or not
if ($date ne "") {
if ($date =~ /^(\w\w\w)\s+\d\d\s+(\d\d\d\d)$/) {
if ($2 < $st_year) {
next;
}
elsif (($2 == $st_year) && ($::m2d{$1} < $st_mon)) {
next;
}
elsif ($2 > $st_year) {
last;
}
elsif (($2 == $st_year) && ($::m2d{$1} > $st_mon)) {
last;
}
else {
$zone = 1;
}
}
}
# we need to print this entry
if ($zone) {
# the deleted meta entries screw up in HTML
$f = "<$1 >" if ($f =~ /^<(.*?)>$/);
if (($row % 2) == 0) {
print "