#!/usr/bin/perl use strict; use warnings; use Storable; # persistant storage #use LWP::Simple; # libwww-perl #use LWP::Debug qw(+); use LWP::UserAgent; use Config::Auto; # libconfig-auto-perl use Time::ParseDate; # libtime-modules-perl #use Linux::Distribution qw(distribution_name distribution_version); # liblinux-distribution-perl use IO::Uncompress::Bunzip2 qw(bunzip2 $Bunzip2Error); use IO::Uncompress::Gunzip qw(gunzip $GunzipError); use Digest::MD5 qw(md5_hex); use POSIX qw(mktime); use File::Path qw(make_path); use HTML::Parser; # mustn't load the huge SHA1 table into RAM.. use DBI; use Tie::File; use feature "switch"; eval { require "debian-security-advisory.pl"; require "ubuntu-security-advisory.pl"; require "common-vulnerability-entry.pl"; } or do { require "../debian-security-advisory.pl"; require "../ubuntu-security-advisory.pl"; require "../common-vulnerability-entry.pl"; }; # load user config, but don't parse it as perl $Config::Auto::DisablePerl = 1; my $config = Config::Auto::parse("apt-sec.conf"); # setup HTTP client my $lwp = LWP::UserAgent->new(ssl_opts => {SSL_ca_path => "/etc/ssl/certs/"}); $lwp->agent("apt-sec " . $lwp->agent); # global variables my $secperday = 60*60*24; my $now = mktime(gmtime()); # look, we're atomic! :-) my $verbosity = 1; # global lookup tables my %h_dsatable; # map dsa_id => dsa my %h_cvetable; # map cve_id => (rel-date, time-to-fix, score1, score2, score3)) my %h_src2dsa; # map src-name => dsa_id my %h_dsa2cve; # map dsa_id => cve_id my %h_src2mtbf; # map src-name => MTBFstats my %h_deb2pkg; # map deb-name => pkg-name my %h_pkg2src; # map pkg-name => src-name my %h_sha1map; # map sha1sum => file-path my %h_pkg2virt; # map pkg => virt. pkgs my %h_virt2pkg; # map virt. pkgs => pkg my %h_state; # remember what we've already parsed/downloaded my $dsatable = \%h_dsatable; my $cvetable = \%h_cvetable; my $src2dsa = \%h_src2dsa; my $dsa2cve = \%h_dsa2cve; my $src2mtbf = \%h_src2mtbf; my $deb2pkg = \%h_deb2pkg; my $pkg2src = \%h_pkg2src; my $pkg2virt = \%h_pkg2virt; my $virt2pkg = \%h_virt2pkg; my $db; # global SQL DB my $dbargs = {AutoCommit => 0, PrintError => 0}; my $state = \%h_state; ## logging # 1 fatal errors # 2 errors # 3 note # 4 trace # 5 debug sub msg { my $lvl = shift; my $msg = shift; if ($lvl <= $config->{"loglevel"}) { print $msg; } } sub debug { msg(5, shift); } sub trace { msg(4, shift); } sub note { msg(3, shift); } sub error { msg(2, shift); } sub fatal { msg(1, shift); } ## load state, different from DBs in that we always need it sub load_state { my $cache = $config->{cache_dir}; my $err = 0; eval { $state = retrieve($cache . "state"); } or do { ## init with default state $state->{"next_adv"} = 0; $state->{"next_fsa"} = 0; $state->{"Packages"} = ""; $state->{"Sources"} = ""; $state->{"Sha1Sums"} = ""; $state->{"vendor"} = ""; $err++; }; return 1-$err; } # helper function for getting HTTP(S) objects sub get { my $url = shift; #my $req = HTTP::Request->new(GET => $url); my $ret = $lwp->get($url); unless ($ret->is_success) { fatal("Failed: " . $ret->status_line . "\n"); return; } return $ret->content; } sub detect_distribution { my $dist = ""; #$dist = distribution_name(); $dist = "debian"; #$dist = "ubuntu"; if ( $dist eq "") { die "Error: Distribution unknown, not LSB conform, bailing out.\n"; } if ($state->{"vendor"} eq "" || $state->{"vendor"} ne $dist) { note "Detected $dist distribution\n"; $state->{"vendor"} = $dist; } } ## save state, different from DBs in that we always need it sub save_state { my $cache = $config->{cache_dir}; eval { store($state, $cache . "state"); return 1; } or do { error "Failed to save state: "; error "$@\n"; }; } sub load_sha1lists { my $cache = $config->{cache_dir}; $db = DBI->connect("dbi:SQLite:dbname=" . $cache . "sha1sums.db","","",$dbargs); eval { my @row = $db->selectrow_array("select * from SQLITE_MASTER where name='sha2pkg' and type='table'"); } or do { # create table and reset last-seen info on Sha1Sums file $db->do("create table sha2pkg (hash char(20) primary key, deb text, src text)"); die "$DBI::errstr\n" if ($db->err()); $state->{"Sha1Sums"} = ""; }; } sub save_sha1lists { my $cache = $config->{cache_dir}; $db->commit(); $db->disconnect(); die "$DBI::errstr\n" if ($db->err()); } ## persistant storage sub load_DBs { my $cache = $config->{cache_dir}; my $err = 0; eval { $dsatable = retrieve($cache . "dsatable"); $cvetable = retrieve($cache . "cvetable"); $src2dsa = retrieve($cache . "src2dsa"); $dsa2cve = retrieve($cache . "dsa2cve"); $src2mtbf = retrieve($cache . "src2mtbf"); $pkg2virt = retrieve($cache . "pkg2virt"); $virt2pkg = retrieve($cache . "virt2pkg"); } or do { $state->{"next_adv"} = 0; # restart parsing of DSAs.. %$dsatable = (); %$cvetable = (); %$src2dsa = (); %$dsa2cve = (); %$src2mtbf = (); $err++; }; eval { $deb2pkg = retrieve($cache . "deb2pkg"); } or do { # reset last-seen info on Packages $state->{"Packages"} = ""; %$deb2pkg=(); $err++; }; eval { $pkg2src = retrieve($cache . "pkg2src"); } or do { # reset last-seen info on Sources $state->{"Sources"} = ""; %$pkg2src=(); $err++; }; return 1-$err; } ## persistant storage sub save_DBs { my $cache = $config->{cache_dir}; eval { # program status data store($state, $cache . "state"); # parsed/evaluated security data store($dsatable, $cache . "dsatable"); store($cvetable, $cache . "cvetable"); store($src2dsa, $cache . "src2dsa"); store($dsa2cve, $cache . "dsa2cve"); store($src2mtbf, $cache . "src2mtbf"); # parsed Debian packages data store($deb2pkg, $cache . "deb2pkg"); store($pkg2src, $cache . "pkg2src"); store($pkg2virt, $cache . "pkg2virt"); store($virt2pkg, $cache . "virt2pkg"); return 1; } or do { error "Failed to save cache file(s): "; error "$@\n"; }; } ## Fetch current Packages, Sources and sha1sums files ## These are needed to find CVE stats by sha1sums/pkg-names ## Only Sha1Sums is custom generated, others are from Debian. ## FIXME: Server might do on-the-fly gzip (but should not for bzip2) ## Return: 1 on success, to signal that new parsing is needed. sub fetchMeta { my $file = shift; my $urlbase = $config->{"pkg_base_url"}; my $dir = $config->{"cache_dir"}; my $bzFile = $file . ".bz2"; my $url = $urlbase . $bzFile; debug "Checking meta file from $url\n"; my $req = HTTP::Request->new(GET => $urlbase . $bzFile); my $ret = $lwp->request($req, $dir . $bzFile); if ($ret->is_success) { # check if the file actally changed.. open(FILE, $dir . $bzFile) or die "Can't open '$dir$bzFile': $!"; binmode(FILE); my $stamp = Digest::MD5->new->addfile(*FILE)->hexdigest; if ($state->{$file} eq $stamp) { debug " unchanged..\n"; return 0; } else { $state->{$file} = $stamp; } # file seems new, unpack for parsing..(TODO: should keep in $tmp..) bunzip2 $dir . $bzFile => $dir . $file#, AutoClose => 1 or die "bunzip2 failed: $Bunzip2Error\n"; close(FILE); return 1; # file changed } else { error "Failed: " . $ret->status_line . "\n"; return 0; # no updates } } # Sources and Packages are not completely consistent, esp for debian-multimedia # He we store manual mappings for these.. sub addOrphanPkgs { my $pkg2src = shift; $pkg2src->{"liblame-dev"} = "lame"; $pkg2src->{"lame-extras"} = "lame"; $pkg2src->{"moonlight"} = "moon"; $pkg2src->{"libmoon0"} = "moon"; $pkg2src->{"xmms-mp4"} = "xmms2"; $pkg2src->{"xmms-mp4"} = "xmms2"; $pkg2src->{"lazarus-src-0.9.30"} = "lazarus"; $pkg2src->{"lazarus-ide-0.9.30"} = "lazarus"; $pkg2src->{"lcl-qt4-0.9.30"} = "lazarus"; $pkg2src->{"lazarus-ide-qt4-0.9.30"} = "lazarus"; $pkg2src->{"lcl-gtk2-0.9.30"} = "lazarus"; $pkg2src->{"lazarus-ide-gtk2-0.9.30"} = "lazarus"; $pkg2src->{"lcl-units-0.9.30"} = "lazarus"; $pkg2src->{"lazarus-0.9.30"} = "lazarus"; $pkg2src->{"lazarus-doc-0.9.30"} = "lazarus"; $pkg2src->{"lcl-0.9.30"} = "lazarus"; $pkg2src->{"lcl-utils-0.9.30"} = "lazarus"; $pkg2src->{"lcl-nogui-0.9.30"} = "lazarus"; $pkg2src->{"libx264-65"} = "x264"; $pkg2src->{"libx264-114"} = "x264"; $pkg2src->{"libx264-60"} = "x264"; # $pkg2src->{"libmlt3"} # $pkg2src->{"libgmerlin-avdec0"} # $pkg2src->{"libxul-dev"} # $pkg2src->{"libmyth-0.23.1-0"} # $pkg2src->{"libmpeg3hv"} # $pkg2src->{"libquicktimehv"} # $pkg2src->{"libxul0d"} # $pkg2src->{"acroread-fonts-kor"} } ## Parse dpkg Packages file, create map deb-name->pkg-name sub parsePackages { my $pkgfile = shift; my $dir = $config->{"cache_dir"}; my $pkgname; my @provides; my @lines; %$deb2pkg = (); %$pkg2virt = (); %$virt2pkg = (); trace "Parsing Packages file...\n"; $pkgfile = $dir . $pkgfile; tie @lines, 'Tie::File', $pkgfile or die "Unable to open file $pkgfile"; LINE: foreach my $line (@lines) { if ($line =~ /^Package:\ (.+)/) { $pkgname = $1; } elsif ($line =~ /^Filename:\ .*\/(\S+)/) { $deb2pkg->{$1} = $pkgname; } elsif ($line =~ /^Provides:\ +(.+)/) { @provides = split(/,\ /, $1); foreach my $virt (@provides) { $virt =~ s/(\s|,)//g; push @{$pkg2virt->{$pkgname}}, $virt unless ($virt ~~ @{$pkg2virt->{$pkgname}}); push @{$virt2pkg->{$virt}}, $pkgname unless ($pkgname ~~ @{$virt2pkg->{$virt}}); } } else { next LINE; } } untie @lines; # close file return; } ## Parse dpkg Sources file, create map pkg-name->src-name sub parseSources { my $srcfile = shift; my $dir = $config->{"cache_dir"}; my $srcname; my @pkgnames; my $checklinecontinuation=0; my @lines; %$pkg2src = (); trace "Parsing Sources file...\n"; $srcfile = $dir . $srcfile; tie @lines, 'Tie::File', $srcfile or die "Unable to open file $srcfile"; LINE: foreach my $line (@lines) { ## sometimes, list of binary pkgs has newline, so we need to check next line.. if ($checklinecontinuation == 1) { if ($line =~ /^[[:alpha:]]+:\ /) { $checklinecontinuation = 0; } else { @pkgnames = split(/,\ /, $line); foreach my $pkg (@pkgnames) { $pkg =~ s/(\s|,)//g; if ($pkg ne "") { $pkg2src->{$pkg} = unifySrcName($srcname); } } } next LINE; } if ($line =~ /^Package:\ (.+)/) { $srcname = $1; } elsif ($line =~ /^Binary:\ (.+)/) { @pkgnames = split(/,\ /, $1); foreach my $pkg (@pkgnames) { $pkg =~ s/(\s|,)//g; $pkg2src->{$pkg} = unifySrcName($srcname); } $checklinecontinuation = 1; } else { next LINE; } } untie @lines; # close file # Sources and Packages are sometimes not really consistent. Here we add some # manual entries and check for remaining pkgs without srcpkg.. addOrphanPkgs($pkg2src); foreach my $pkg (values %$deb2pkg) { print "Orphan Package: $pkg\n" unless (defined $pkg2src->{$pkg}); } return; } sub addSHA1 { my $hash = shift; my $deb = shift; my $src = shift; my ($thash,$tdeb,$tsrc) = getSHA1($hash); # if already recorded, extend and/or mark as non-unique if (defined $thash) { my @tdebs = split(/,/, $tdeb); my @tsrcs = split(/,/, $tsrc); if (! grep {$_ eq $deb} @tdebs) { #print "Adding deb $deb to $tdeb..\n"; my $tmp = $tdeb . "," . $deb; $db->do("update sha2pkg set deb='$tmp' where hash='$hash'"); die "$DBI::errstr\n" if ($db->err()); } if (! grep {$_ eq $src} @tsrcs) { print "Adding src $src to $tsrc..\n"; my $tmp = $tsrc . "," . $src; $db->do("update sha2pkg set src='$tmp' where hash='$hash'"); die "$DBI::errstr\n" if ($db->err()); } } else { $db->do("insert into sha2pkg (hash, deb, src) VALUES('$hash', '$deb', '$src')"); die "Error inserting $hash/$deb/$src: $DBI::errstr\n" if ($db->err()); } } sub getSHA1 { my $hash = shift; my @res = $db->selectrow_array("select * from sha2pkg where hash = '$hash'"); return @res; } ## Parse Sha1Sums file. Format: "sha1sum::deb-name::unix-file-path" ## Create 2 maps: sha1sum->file, file->deb-name sub parseSha1Sums { my $sha1file = shift; my $dir = $config->{"cache_dir"}; my @lines; trace "Parsing Sha1Sums file...\n"; # delete existing table first.. (or else everything will be collisions!) $db->do("delete from sha2pkg"); $sha1file = $dir . $sha1file; tie @lines, 'Tie::File', $sha1file or die "Unable to open file $sha1file"; LINE: foreach my $line (@lines) { unless ($line =~ /^(\w+)::(\S+)$/) { die "Sha1Sums parse error, line reads: \n>>$line<<"; } # ignore the hash of '\n' (many collisions due to empty files) next if ($1 eq 'da39a3ee5e6b4b0d3255bfef95601890afd80709'); # To avoid collisions in the SHA table we try to resolve to src pkg right away. # But that means we must resolve *all* packets, which often means that we # have to print errors and manually fix inconsistent meta-data. :-/ # To reduce this problem, addSHA1() keeps track of collisions and then we # only complain later on, when some actually measured SHA1 value has # ambigious security info. # The .deb package names are only stored for informational purposes. my $binpkg = $deb2pkg->{$2}; if ($binpkg) { my $srcpkg = $pkg2src->{$binpkg}; if ($srcpkg) { addSHA1 $1, $2, unifySrcName($srcpkg); } else { note "No srcpkg known for pkg $binpkg (inconsistent Packages/Sources data?)\n"; addSHA1 $1, $2, ""; } } else { note "No pkg known for deb $2 (stale entries in sha1sums?)\n"; } } untie @lines; # close file } ## Parse local dpkg status, return list of debs sub parseStatus { my $stsfile = shift; my @pkglist; my $pkgname; trace "Parsing dpkg status..\n"; open (PKG, "< $stsfile"); my @lines = ; LINE: foreach my $line (@lines) { if ($line =~ /^Package:\ (.+)/) { $pkgname = $1; } elsif ($line =~ /^Status:.*installed/) { push @pkglist, $pkgname; } else { next LINE; } } close(PKG); error "Could not find any installed packages in status file." if ($#pkglist <= 0); return \@pkglist; } sub parseAdvisory { my $adv = shift; given ($state->{"vendor"}) { when ($_ eq "debian") { return parseFSA $adv if $adv =~ /FreeBSD-SA/m; return parseDSAhtml($adv); } when ($_ eq "ubuntu") { return parseUSNhtml($adv); } # when ($_ eq "redhat") { return checkRHSA; } default { die "Unsupported distribution $_"; } }; } sub fixAdvisoryQuirks { my @arg = @_; given ($state->{"vendor"}) { when ($_ eq "debian") { return fixDSAquirks(@arg); } when ($_ eq "ubuntu") { return fixUSNquirks(@arg); } # when ($_ eq "redhat") { return checkRHSA; } default { die "Unsupported distribution $_"; } }; } ## Extract CVE ids from new advisories and print URL for mirror script sub printCVEs { my $id = shift; # Advisory ID my $adv = shift; # Advisory to scan my $url; my %cves; trace "Looking for CVEs in advisory.. \n"; my @dsastats = parseAdvisory($adv); return if !@dsastats; ## fix DSAs that don't contain correct CVE refs @dsastats = fixAdvisoryQuirks($id, \@dsastats); foreach my $cve_id (@{$dsastats[2]}) { $cve_id =~ s/^CAN/CVE/; $cves{$cve_id} = 1; } foreach my $cve (keys %cves) { print "NeedCVE: " . $config->{"cve_base_url"} . $cve . "\n"; print "NeedCVE: " . $config->{"cvss_base_url"} . $cve . "\n"; } } ## Update internal vuln. DB with new Advisory info ## Creates CVEtable for MTBF computation: ## ( cve-id => (date, delay, score1, score2, score3)) sub updateCVETables { my $id = shift; # Advisory to merge into tables my @cvestats; trace "Updating vulnerability database with advisory ".$state->{"vendor"}."/$id\n"; my $adv = $dsatable->{$id}; #print $dsatable->{$id} if $id eq "FSA-301"; my @dsastats = parseAdvisory($adv); return if !@dsastats; ## fix DSAs that don't contain correct CVE refs @dsastats = fixAdvisoryQuirks($id, \@dsastats); foreach my $srcpkg (@{$dsastats[0]}) { push @{$src2dsa->{$srcpkg}}, $id; push @{$dsa2cve->{$id}}, @{$dsastats[2]}; } foreach my $cve_id (@{$dsastats[2]}) { my $cve = fetchCVE($cve_id, $config->{"cve_base_url"}, $config->{"cvss_base_url"}); @cvestats = parseCVE($cve_id, $cve); if ($cvestats[0] > $dsastats[1] || $cvestats[0] == 0) { $cvestats[0] = $dsastats[1]; } my @cvedata = ( $cvestats[0], $dsastats[1]-$cvestats[0], $cvestats[1], $cvestats[2], $cvestats[3] ); $cvetable->{$cve_id} = \@cvedata; } } ## Check for updates on Package information sub aptsec_update { my $newAdv; unless ("--offline" ~~ @ARGV) { fetchMeta("Packages"); fetchMeta("Sources"); fetchMeta("Sha1Sums"); } unless ("--cves" ~~ @ARGV) { parsePackages("Packages"); parseSources("Sources"); unless ("--nosha1" ~~ @ARGV) { parseSha1Sums("Sha1Sums"); } } given ($state->{"vendor"}) { when ($_ eq "debian") { $newAdv = checkDSAs($state, $config); } when ($_ eq "ubuntu") { $newAdv = checkUSNs($state, $config); } #when ($_ eq "redhat") { ($id,$adv) = checkRHSA; } default { die "Unsupported distribution $_"; } }; foreach my $id (keys %$newAdv) { # if not known, process advisory if ($dsatable->{$id}) { note $state->{"vendor"} . " advisory $id already known.\n"; } elsif ("--cves" ~~ @ARGV) { ## scan for CVE urls only? printCVEs($id, $newAdv->{$id}); } else { ## store advisory and parse it $dsatable->{$id} = $newAdv->{$id}; updateCVETables($id); } } # recompute all pkg statistics foreach my $srcpkg (keys %$src2dsa) { processCVEs($srcpkg); } } ## find list of src pkgs from bin pkgs based on pkg2src sub resolvePkg2Src { my $pkglist = shift; my @srclist; my %tmp; my $srcpkg; foreach my $pkg (@$pkglist) { $srcpkg = $pkg2src->{$pkg}; if (defined $srcpkg) { # unique.. $tmp{$pkg2src->{$pkg}} = 1; } else { note "Could not find source package for: $pkg\n"; } } @srclist = keys %tmp; return \@srclist; } ## compute and store MTBF, MTBR and Scores of each src pkg ## output: %src2mtbf: ## (srcpkg=> (begin, num, delaysum, scoresum, maximpact, MTTF, MTTFl)) sub processCVEs { my $pkg = shift; my @stats = ($now, 0, 0, 0, 0); my %cvestats; my $lambda = $config->{"lambda"}; trace "Processing package $pkg"; ## @cvestats = (date base-score impact-score exploit-score) foreach my $dsa_id (@{$src2dsa->{$pkg}}) { foreach my $cve_id (@{$dsa2cve->{$dsa_id}}) { $cvestats{$cvetable->{$cve_id}[0]}++; $stats[1]++; $stats[2]+= $cvetable->{$cve_id}[1]; $stats[3]+= $cvetable->{$cve_id}[2]; if ($stats[4] < $cvetable->{$cve_id}[3]) { $stats[4] = $cvetable->{$cve_id}[3]; } } } # Ignore pkgs with less than one incident, should not happen.. return if ($stats[1] < 1); my $date; my $prev_date=0; my $delay; my $months; my $weight=0; my @dates = sort (keys %cvestats); $stats[0] = $dates[0]; $stats[5]=0; $stats[6]=0; foreach $date (@dates) { #print "$cvestats{$date} $date\n"; foreach (1..$cvestats{$date}) { if ($prev_date > 0) { $months = ($now - $date)/$secperday/30; $delay = ($date - $prev_date)/$secperday; #$delay = 0.0000001 if ($delay == 0); #print "delay: $delay\n"; $stats[5] += $delay; $stats[6] += $delay*exp(-$months/$lambda); $weight += exp(-$months/$lambda); } $prev_date = $date; } } ## correct stats in case that last vuln. is so long ago that the ## current reliability should be increased. ## only use if we have more than one complete interval already $delay = ($now - $dates[$#dates])/$secperday; return if ($stats[1] == 1 && $delay < 50); if ($delay > $stats[5]/$stats[1] || $delay > $stats[6]/$weight) { $stats[5] += $delay; $stats[5] /= $stats[1]; $stats[6] += $delay * exp(-0); $stats[6] /= $weight + exp(-0); } else { $stats[5] /= $stats[1]-1; # intervals = incidents-1 $stats[6] /= $weight; } # save if ($stats[5] > 0 && $stats[6] > 0) { $src2mtbf->{$pkg} = \@stats; } else { die "@stats"; } } # print some meta-info on internal data sub aptsec_about { my $num_dsa = keys %$dsatable; my $num_cve = keys %$cvetable; my $num_pkg = keys %$pkg2src; my $num_src = keys %$src2dsa; #printf("\nWorking DB has %d binary packages and SHA-1 file hashes.\n", printf("\nThe current database records %d binary packages and %d DSAs.\n", $num_pkg, $num_dsa); printf("%d CVEs are associated with %d source packages.\n", $num_cve, $num_src); } # use pkg provides info to suggest alternative programs sub aptsec_alternatives { my $pkg = shift; my @pkgs; my %provs; my %alts; my $lines; ## pkg is normally not src pkg. resolve it to src and then ## consider all bin pkgs created from it if ($pkg2src->{$pkg}) { print "\nResolving $pkg to $pkg2src->{$pkg}\n\n"; $pkg = $pkg2src->{$pkg}; } foreach (keys %$pkg2src) { if ($pkg2src->{$_} eq $pkg) { push @pkgs, $_; } } foreach $pkg (@pkgs) { #print "Provided by $pkg:\n"; foreach (@{$pkg2virt->{$pkg}}) { #print "$_\n"; $provs{$_} = 1; } } print "\nSimilar functions are provided by:\n\n"; foreach my $func (keys %provs) { #print "Function $func also is provided by:\n\t"; foreach (@{$virt2pkg->{$func}}) { $alts{$pkg2src->{$_}} = 1; } } $lines=0; foreach (keys %alts) { print "$_ "; $lines++; } print "-" unless $lines; print "\n\n"; } ## print overview for pkg high scores sub aptsec_hitlist { my @stats; my @pkg = (keys %$src2mtbf); # TODO: Some DSAs have to recognized CVEs, integrate security tracker # foreach my $pkg (@pkg) { # print "no dsa for $pkg\n" unless defined ($src2dsa->{$pkg}) # } print "\nOverall MTBF/MTTF:\n\n"; foreach my $pkg (@pkg) { my @stats = @{$src2mtbf->{$pkg}}; printf("MTTF:%6.1f, STP(12):%5.1f, MTTFl:%6.1f, STPl(12):%5.1f, Vuln:%3d, Pkg: %s\n", $stats[5], 365/$stats[5], $stats[6], 365/$stats[6], $stats[1], $pkg); } } ## evaluation function wrapper ## parameter is package or path to status file or nil, in which ## case we process all pkgs with recorded vulnerabilities sub aptsec_status_2010 { my $pkg = shift; my @stats; my @pkg = (keys %$src2dsa); my $year; my $num_dsa = keys %$dsatable; my $num_cve = keys %$cvetable; my $num_pkg = keys %$pkg2src; my $num_src = keys %$src2dsa; if (defined $pkg) { eval { my $srclist = resolvePkg2Src(parseStatus($pkg)); @pkg = @$srclist; #printSystemStats($srclist); } or do { print "Interpreting parameter $pkg as packet.."; @pkg = ($pkg); } } # individual package stats until end of given year $year=2009; print "\n Overall MTBF/MTTF until $year:\n"; simulate_stats($_,$year) foreach (@pkg); # actual vulnerabilities for a given year $year=2010; print "\n Stats of year $year:\n"; show_period($_,$year) foreach (@pkg); } sub show_period { my $pkg = shift; my $year = shift; #my $startdate = shift; #my $enddate = shift; my $start = parsedate("1.1.$year"); my $end = parsedate("31.12.$year"); my @cvelist; my %cvestats; my $num=0; ## @cvestats = (date base-score impact-score exploit-score) foreach my $dsa_id (@{$src2dsa->{$pkg}}) { foreach my $cve_id (@{$dsa2cve->{$dsa_id}}) { #my $yr = 1900 + (gmtime($cvetable->{$cve_id}[0]))[5]; #if ($startdate < $yr && $enddate >= $yr) { if ($start < $cvetable->{$cve_id}[0] && $end > $cvetable->{$cve_id}[0]) { push @{$cvestats{$cvetable->{$cve_id}[0]}}, $cve_id; $num++; } } } # Ignore pkgs with less than two incidents return if ($num < 1); foreach my $date (sort (keys %cvestats)) { #print "Date: $date, CVEs: @{$cvestats{$date}}\n"; } printf(" In %4d: MTBF: %3d, new vuln: %3d, Pkg:%s\n", $year, ($end-$start)/$secperday/$num, $num, $pkg); #foreach my $dsa_id (@{$src2dsa->{$pkg}}) { # foreach my $cve_id (@{$dsa2cve->{$dsa_id}}) { # my ($sec,$min,$hrs,$day,$mon,$yr) = gmtime($cvetable->{$cve_id}[0]); # printf "%s: Base Score: %04.1f, %02d.%02d.%04d\n", # $cve_id, $cvetable->{$cve_id}[2], $day, $mon+1, $yr+1900; #} } ## evaluation helper ## compute stats until date given in $2, then compute stats ## for the next year to check accuracy of the prediction. ## @cvestats = (date base-score impact-score exploit-score) sub simulate_stats { my $pkg = shift; my $year = shift; my $start = 0; my $end = parsedate("31.12.$year"); my $now = $end; my @stats = ($now, 0, 0, 0, 0); my %cvestats; my $lambda = $config->{"lambda"}; #unless (defined @{$src2dsa->{$pkg}}) { # print "No vulnerability record for package $pkg\n"; # return; #} foreach my $dsa_id (@{$src2dsa->{$pkg}}) { foreach my $cve_id (@{$dsa2cve->{$dsa_id}}) { #my $yr = 1900 + (gmtime($cvetable->{$cve_id}[0]))[5]; #if ($startdate <= $yr && $enddate > $yr) { if ($start < $cvetable->{$cve_id}[0] && $end > $cvetable->{$cve_id}[0]) { $cvestats{$cvetable->{$cve_id}[0]}++; $stats[1]++; $stats[2]+= $cvetable->{$cve_id}[1]; $stats[3]+= $cvetable->{$cve_id}[2]; if ($stats[4] < $cvetable->{$cve_id}[3]) { $stats[4] = $cvetable->{$cve_id}[3]; } } } } # Ignore pkgs with less than two incidents return if ($stats[1] < 2); my $date; my $prev_date=0; my $delay; my $months; my $weight=0; my @dates = sort (keys %cvestats); $stats[0] = $dates[0]; $stats[5]=0; $stats[6]=0; foreach $date (@dates) { #print "$cvestats{$date} $date\n"; foreach (1..$cvestats{$date}) { if ($prev_date > 0) { $months = ($now - $date)/$secperday/30; $delay = ($date - $prev_date)/$secperday; #$delay = 0.0000001 if ($delay == 0); #print "delay: $delay, age: $months\n"; $stats[5] += $delay; $stats[6] += $delay*exp(-$months/$lambda); $weight += exp(-$months/$lambda); } $prev_date = $date; } } ## correct stats in case that last vuln. is so long ago that the ## current reliability should be increased. $delay = ($now - $dates[$#dates])/$secperday; return if ($stats[1] == 1 && $delay < 50); if ($delay > $stats[5]/$stats[1] || $delay > $stats[6]/$weight) { $stats[5] += $delay; $stats[5] /= $stats[1]; $stats[6] += $delay * exp(-0); $stats[6] /= $weight + exp(-0); #print "delay: $delay, age: 0, weight: 1\n"; } else { #print "delay: $delay, age: 0, weight: 1\n"; $stats[5] /= $stats[1]-1; # intervals = incidents-1 $stats[6] /= $weight; } printf " Til $year "; #printf("MTTF:%6.1f, STP(12):%5.1f, MTTFl:%6.1f, STPl(12):%5.1f, MTRR:%6.1f, Vuln:%4d, Pkg: %s\n", printf("MTTF:%6.1f, STP(12):%5.1f, MTTFl:%6.1f, STPl(12):%5.1f, Vuln:%3d, Pkg: %s\n", $stats[5], 365/$stats[5], $stats[6], 365/$stats[6], #$stats[2]/$stats[1]/$secperday, $stats[1], $pkg); return @stats; } ## Use local system status(dpkg DB) for printing system status report sub aptsec_status { my $path; $path = shift or $path = "/var/lib/dpkg/status"; my $srclist = resolvePkg2Src(parseStatus($path)); printSystemStats($srclist); } ## Print 'trustworthiness' of a set(system) of src packages sub printSystemStats { my $srclist = shift; my $mtbf=0; my $mtrr=0; my $mttf=0; my $mttfl=0; my $num=0; # we can assume that pkgs are independent (right?) PKG: foreach my $pkg (@$srclist) { my $rstats = $src2mtbf->{$pkg} or next PKG; my @stats = @$rstats; $mtbf+=1/(($now-$stats[0])/$stats[1]/$secperday); $mttf+=1/$stats[5]; $mttfl+=1/$stats[6]; $mtrr+=$stats[2]/$stats[1]/$secperday; if ($verbosity > 0) { #printf("MTBF: %6.1f, MTTF: %6.1f, MTTFl: %6.1f, MTRR: %6.1f, \t%s\n", printf("MTTF: %6.1f, MTTFl: %6.1f, Vuln: %3d, Pkg: %s\n", #($now-$stats[0])/$stats[1]/$secperday, $stats[5], $stats[6], #$stats[2]/$stats[1]/$secperday, $stats[1], $pkg); } $num++; } printf "\n"; printf "Packages with past vulnerabilities installed: %d\n", $num; if ($num > 0) { #printf "System MTRR: %5.1f days\n", $mtrr/$num; #printf "System MTBF: %5.1f days per failure\n", 1/$mtbf; printf "System MTTF: %7.1f days per failure\n", 1/$mttf; printf "System MTTFl: %7.1f days per failure\n", 1/$mttfl; printf "System STP(12) %6.1f failures per year\n", 365*$mttf; printf "System STPl(12) %5.1f failures per year\n", 365*$mttfl; printf "\n"; } } ## show info on a single src pkg, resolv to src if needed sub aptsec_show { my $pkg = shift; my $ADV; my $lines; unless (defined $pkg) { aptsec_help(); exit; } given ($state->{"vendor"}) { when ($_ eq "debian") { $ADV = "DSA-"; } when ($_ eq "ubuntu") { $ADV = "USN-"; } default { die "Unsupported distribution $_"; } }; if (!($src2dsa->{$pkg}) && $pkg2src->{$pkg}) { print "\nResolving $pkg to $pkg2src->{$pkg}\n"; $pkg = $pkg2src->{$pkg}; } print "\nThe following binary packages are created from $pkg:\n\n"; $lines=0; foreach (keys %$pkg2src) { if ($pkg2src->{$_} eq $pkg) { print "$_\n"; $lines++; } } print "-\n" if ($lines < 1); unless ($src2dsa->{$pkg} && $src2mtbf->{$pkg}) { print "\nNo vulnerabilities recorded for source package $pkg.\n"; exit; } print "\nAdvisories on package $pkg:\n\n"; foreach my $dsa_id (sort @{$src2dsa->{$pkg}}) { print "$ADV$dsa_id\n"; foreach my $cve_id (@{$dsa2cve->{$dsa_id}}) { my ($sec,$min,$hrs,$day,$mon,$yr) = gmtime($cvetable->{$cve_id}[0]); printf "%s: Base Score: %04.1f, %02d.%02d.%04d\n", $cve_id, $cvetable->{$cve_id}[2], $day, $mon+1, $yr+1900; } } my @stats = @{$src2mtbf->{$pkg}}; my ($sec,$min,$hrs,$day,$mon,$yr) = gmtime($stats[0]); print "\nOverall vulnerability stats:\n\n"; printf " First vulnerability: %02d.%02d.%04d\n", $day, $mon+1, $yr+1900; printf " Total vulnerabilities: %d\n", $stats[1]; printf " Average Base Score: %04.2f/10\n", $stats[3]/$stats[1]; printf " Highest Impact Score: %d/10\n", $stats[4]; # printf " MTBF in days: %.2f\n", ($now-$stats[0])/$stats[1]/$secperday; printf " MTTF: %.1f days/vuln\n", $stats[5]; printf " MTTFl: %.1f days/vuln\n", $stats[6]; printf " STP(12): %.1f vuln/yr\n", 365/$stats[5]; printf " STPl(12): %.1f vuln/yr\n", 365/$stats[6]; # printf " MTRR in days: %.2f\n\n", $stats[2]/$stats[1]/$secperday; } sub aptsec_help { print "\n"; print "Usage:\n"; print "\n"; print "help This cruft\n"; print "update Update vulnerability databases\n"; print "status Compute expected failure rates for local system\n"; print "show Show failure rates and vulnerability statistics for \n"; print "\n"; } ## Print system status report from component(files) measurements (sha1sums) ## Expected input format is Linux IMA. We assume input was validated. ## ## Note: aptsec_status(), considers *reportedly installed* packages, while this ## one looks at *actually loaded* software that influenced the CPU since bootup. sub aptsec_attest { my $sha1file = shift; my %tmp; my @pkglist; my @res; $sha1file = "/sys/kernel/security/ima/ascii_runtime_measurements" unless ($sha1file); # don't use Tie here since we don't need and sometimes don't have write access.. open (SHA, "< $sha1file") or die "Unable to open file $sha1file"; my @lines = ; LINE: foreach my $line (@lines) { if ($line =~ /[0-9]{2}\ \w{40}\ ima\ (\w+)\ (\S+)/) { # IMA format? } elsif ($line =~ /(\w{40})\ (\S+)/) { # ' ' format? } else { print "Failed to parse attestation data input from $sha1file\n"; return; } unless (@res = getSHA1 $1) { print "Unknown measured file: $1 $2\n"; next LINE; } # if measured hash is ambigious, print warning and over-approximate if ($res[2] =~ /,/) { print "Ambigious hash $res[0]: $res[2]\n"; foreach my $pkg (split(/,/, $res[2])) { if ($src2dsa->{$pkg}) { print "\t$pkg has security record...\n" } $tmp{$pkg} = 1; } } else { $tmp{$res[2]} = 1; } } close(SHA); my @list = keys %tmp; my $srclist = \@list; printSystemStats($srclist); } my $action; $action = shift or $action = "help"; load_state(); detect_distribution(); make_path($state->{"cache_dir"}); given ($action) { when ($_ eq "update") { load_DBs; load_sha1lists; aptsec_update(); save_sha1lists; save_DBs; save_state; } when ($_ eq "status") { load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n"; aptsec_status(shift); } when ($_ eq "status2010") { load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n"; aptsec_status_2010(shift); } when ($_ eq "show") { load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n"; aptsec_show(shift); } when ($_ eq "attest") { load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n"; load_sha1lists; aptsec_attest(shift); } # when ($_ eq "altname") { # load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n"; # altname(shift); # } when ($_ eq "hits") { load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n"; aptsec_hitlist(); } when ($_ eq "about") { load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n"; aptsec_about(); } when ($_ eq "alt") { load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n"; aptsec_alternatives(shift); } default { aptsec_help(); } };