apt-sec.mbox 26 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use Storable; # persistant storage
  5. use LWP::Simple; # libwww-perl
  6. use Config::Auto; # libconfig-auto-perl
  7. use Time::ParseDate; # libtime-modules-perl
  8. use Linux::Distribution qw(distribution_name distribution_version); # liblinux-distribution-perl
  9. #use IO::Compress::Bzip2 qw(bzip2 $Bzip2Error);
  10. use IO::Uncompress::Bunzip2 qw(bunzip2 $Bunzip2Error);
  11. #use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
  12. use Digest::MD5 qw(md5_hex);
  13. use POSIX qw(mktime);
  14. #use DB_File; # berkely DB backend for SHA1 list (2.3GB hash table...)
  15. use feature "switch";
  16. # don't try to parse perl in configs
  17. $Config::Auto::DisablePerl = 1;
  18. #print "This is libwww-perl-$LWP::VERSION\n";
  19. # load user config
  20. my $config = Config::Auto::parse("apt-sec.conf");
  21. # global variables
  22. my $secperday = 60*60*24;
  23. my $now = mktime(localtime()); # look, we're atomic! :-)
  24. my $verbosity = 1;
  25. my @months = ("January", "February", "March", "April", "May", "June", "July",
  26. "August", "September", "October", "November", "December");
  27. # global lookup tables
  28. my %h_dsatable; # map dsa_id => dsa
  29. my %h_cvetable; # map cve_id => (rel-date, time-to-fix, score1, score2, score3))
  30. my %h_src2dsa; # map src-name => dsa_id
  31. my %h_dsa2cve; # map dsa_id => cve_id
  32. my %h_src2mtbf; # map src-name => MTBFstats
  33. my %h_deb2pkg; # map deb-name => pkg-name
  34. my %h_pkg2src; # map pkg-name => src-name
  35. my %h_sha1map; # map sha1sum => file-path
  36. my %h_state; # remember what we've already parsed/downloaded
  37. my $dsatable = \%h_dsatable;
  38. my $cvetable = \%h_cvetable;
  39. my $src2dsa = \%h_src2dsa;
  40. my $dsa2cve = \%h_dsa2cve;
  41. my $src2mtbf = \%h_src2mtbf;
  42. my $deb2pkg = \%h_deb2pkg;
  43. my $pkg2src = \%h_pkg2src;
  44. my $sha1map = \%h_sha1map;
  45. my $state = \%h_state;
  46. sub detect_distribution {
  47. my $dist = "";
  48. my $version = "";
  49. $dist = distribution_name();
  50. $version = distribution_version();
  51. if ( $dist eq "") {
  52. print "Error: Distribution unknown, not LSB conform, bailing out.\n";
  53. exit;
  54. }
  55. if ($state->{"vendor"} eq "" || $state->{"vendor"} ne $dist) {
  56. print "Detected $dist distribution, version $version\n";
  57. $state->{"vendor"} = $dist;
  58. }
  59. }
  60. ## logging
  61. sub msg {
  62. my $lvl = shift;
  63. my $msg = shift;
  64. print "$msg\n" unless ($lvl > $config->{"loglevel"});
  65. }
  66. ## load state, different from DBs in that we always need it
  67. sub load_state {
  68. my $cache = $config->{cache_dir};
  69. my $err = 0;
  70. eval { $state = retrieve($cache . "state"); } or do {
  71. ## init with default state
  72. $state->{"next_dsa"} = 11; ## can't parse DSAs earlier than that
  73. $state->{"Packages"} = "";
  74. $state->{"Sources"} = "";
  75. $state->{"Sha1Sums"} = "";
  76. $state->{"vendor"} = "";
  77. $err++;
  78. };
  79. return 1-$err;
  80. }
  81. ## save state, different from DBs in that we always need it
  82. sub save_state {
  83. my $cache = $config->{cache_dir};
  84. eval {
  85. store($state, $cache . "state");
  86. return 1;
  87. } or do {
  88. print "Failed to save state, check permissions:\n";
  89. print "$@";
  90. };
  91. }
  92. ## persistant storage
  93. sub load_DBs {
  94. my $cache = $config->{cache_dir};
  95. my $err = 0;
  96. eval {
  97. $dsatable = retrieve($cache . "dsatable");
  98. $cvetable = retrieve($cache . "cvetable");
  99. $src2dsa = retrieve($cache . "src2dsa");
  100. $dsa2cve = retrieve($cache . "dsa2cve");
  101. $src2mtbf = retrieve($cache . "src2mtbf");
  102. } or do {
  103. #delete $dsatable->{$_} foreach (keys %$dsatable);
  104. #delete $cvetable->{$_} foreach (keys %$cvetable);
  105. #delete $src2dsa->{$_} foreach (keys %$src2dsa);
  106. #delete $dsa2cve->{$_} foreach (keys %$dsa2cve);
  107. #delete $src2mtbf->{$_} foreach (keys %$src2mtbf);
  108. %$dsatable = ();
  109. %$cvetable = ();
  110. %$src2dsa = ();
  111. %$dsa2cve = ();
  112. %$src2mtbf = ();
  113. $state->{"next_dsa"} = 11; ## can't parse DSAs earlier than that
  114. $err++;
  115. };
  116. eval { $deb2pkg = retrieve($cache . "deb2pkg");
  117. } or do {
  118. $state->{"Packages"} = "";
  119. %$deb2pkg=();
  120. $err++;
  121. };
  122. eval { $pkg2src = retrieve($cache . "pkg2src");
  123. } or do {
  124. $state->{"Sources"} = "";
  125. %$pkg2src=();
  126. $err++;
  127. };
  128. eval {
  129. $sha1map = retrieve($cache . "sha1map");
  130. } or do {
  131. $state->{"Sha1Sums"} = "";
  132. %$sha1map=();
  133. $err++;
  134. };
  135. return 1-$err;
  136. }
  137. ## persistant storage
  138. sub save_DBs {
  139. my $cache = $config->{cache_dir};
  140. eval {
  141. # program status data
  142. store($state, $cache . "state");
  143. # parsed/evaluated security data
  144. store($dsatable, $cache . "dsatable");
  145. store($cvetable, $cache . "cvetable");
  146. store($src2dsa, $cache . "src2dsa");
  147. store($dsa2cve, $cache . "dsa2cve");
  148. store($src2mtbf, $cache . "src2mtbf");
  149. # parsed Debian packages data
  150. store($deb2pkg, $cache . "deb2pkg");
  151. store($pkg2src, $cache . "pkg2src");
  152. store($sha1map, $cache . "sha1map");
  153. return 1;
  154. } or do {
  155. print "Failed to save cache file(s), check permissions:\n";
  156. print "$@";
  157. };
  158. }
  159. ## Parse DSA data and return array
  160. ## (src-pkg-name date (CVE-id)*)
  161. sub parseDSA {
  162. my $dsa = shift or die "No advisories to parse?!";
  163. my @dsa_names;
  164. my @dsa_CVEs;
  165. my $dsa_date;
  166. my $dsa_type;
  167. my @tmp;
  168. my $tmp_date;
  169. msg 10, "Trying to parse DSA..";
  170. my @lines = split(/\n/,$dsa);
  171. LINE: foreach my $line (@lines) {
  172. # Date Reported -> $dsa_date
  173. unless ($dsa_date) {
  174. #print "look for date in $line\n";
  175. if ($line =~ /^Date:\s+\w+,\s+(\d+)\s+(\w+)\s+(\d+)\s+\d+:\d+:\d+/) {
  176. $dsa_date = parsedate("$1 $2 $3");
  177. }
  178. # parse date from body: contains many flaws, mail header seems always accurate
  179. # if ($line =~ /^(\w+)\s+(.+),\s+(\d+)\s*$/ ||
  180. # $line =~ /^(\w+)\s+(.+),\s+(\d+)\s+http:\/\/www.debian.org\/security/) {
  181. # print "using body date: $1 $2, $3\n";
  182. # $dsa_date = parsedate("$1 $2, $3");
  183. # if (abs($tmp_date - $dsa_date) > 86401 && $1 . $2 ne "December1st" &&
  184. # $1 . $3 ne "January2004" && $1.$3 ne "Aug2005" && $1.$3 ne "July2005" && $1.$2 ne "August17th" &&
  185. # $1.$2 ne "August25th") {
  186. # $dsa_date = $tmp_date if ($1 eq "XXXXX");
  187. # next LINE if ($1 eq "XXXXX");
  188. # print "\nDate mismatch:\n";
  189. # print localtime($tmp_date);
  190. # print "\n";
  191. # print localtime($dsa_date);
  192. # print "\n";
  193. # die;
  194. # }
  195. # }
  196. next LINE;
  197. }
  198. # Affected Packages -> @dsa_names
  199. unless (@dsa_names) {
  200. #print "look for name in $line\n";
  201. if ($line =~ m/^Vulnerability\s*:/) {
  202. @dsa_names = @tmp;
  203. @tmp=();
  204. }
  205. if ( @tmp && $line =~ /^\s+(.*)/) {
  206. foreach my $w (split(/(,|\s+)/, $1)) {
  207. $w =~ s/,\s*//;
  208. push @tmp, $w;
  209. }
  210. }
  211. if ($line =~ /^Package\s*:\s+(.*)/) {
  212. foreach my $w (split(/(,|\s+)/, $1)) {
  213. $w =~ s/,\s*//;
  214. push @tmp, $w;
  215. }
  216. }
  217. next LINE;
  218. }
  219. # Security database references (CVEs) -> @dsa_CVEs
  220. unless (@dsa_CVEs) {
  221. #print "look for cve in $line\n";
  222. if ($line =~ m/^CVE\ IDs?\s+:\s(.*)/i ||
  223. $line =~ m/^CVE\ Id\(s\)\s+:\s(.*)/i) {
  224. foreach my $w (split(/\s+/, $1)) {
  225. $w =~ s/,\s*//;
  226. push @tmp, $w;
  227. }
  228. }
  229. if ( @tmp && $line =~ /^\s+(.*)/) {
  230. foreach my $w (split(/\s+/, $1)) {
  231. $w =~ s/,\s*//;
  232. push @tmp, $w;
  233. }
  234. }
  235. # anything else is considered end of entry
  236. @dsa_CVEs = @tmp;
  237. @tmp=();
  238. last if ($line =~ m/^$/);
  239. next LINE;
  240. }
  241. }
  242. print "\n\nNames: @dsa_names\n";
  243. print "Date: $dsa_date\n";
  244. print "CVEs: @dsa_CVEs\n";
  245. return (\@dsa_names, $dsa_date, \@dsa_CVEs);
  246. }
  247. ## Parse USN data and return array
  248. ## (src-pkg-name date (CVE-id)*)
  249. sub parseUSN {
  250. my $usn = shift or die "No advisories to parse?!";
  251. my @usn_names;
  252. my @usn_CVEs;
  253. my $usn_date;
  254. my $usn_type;
  255. my @tmp;
  256. msg 10, "Trying to parse USN..";
  257. my @lines = split(/\n/,$usn);
  258. LINE: foreach my $line (@lines) {
  259. # Date Reported -> $dsa_date
  260. unless ($usn_date) {
  261. #print "Looking for date\n";
  262. #print "Line: $line\n";
  263. if ($line =~ /^Ubuntu\ Security\ Notice\ .*\d+-\d+\s+(\w+)\s+(\d+),\s+(\d+)/) {
  264. $usn_date = parsedate("$1 $2, $3");
  265. }
  266. next LINE;
  267. }
  268. # Affected Packages -> @dsa_names
  269. unless (@usn_names) {
  270. #print "Looking for name\n";
  271. #print "Line: $line\n";
  272. if ($line =~ m/^CVE-/ || $line =~ m/^CAN-/ || $line =~ m/^http.*launchpad.net\/bugs/|| $line =~ m/^===========/) {
  273. @usn_names = @tmp;
  274. @tmp=();
  275. $usn_type = pop @usn_names;
  276. } else {
  277. foreach my $w (split(/\s+/, $line)) {
  278. $w =~ s/,\s*//;
  279. #print "Adding: $w\n";
  280. push @tmp, $w;
  281. }
  282. next LINE;
  283. }
  284. }
  285. # Security database references (CVEs) -> @dsa_CVEs
  286. unless (@usn_CVEs) {
  287. #print "Looking for cve\n";
  288. #print "Line: $line\n";
  289. if ($line =~ m/^===========/) {
  290. @usn_CVEs = @tmp;
  291. } else {
  292. foreach my $w (split(/\s+/, $line)) {
  293. $w =~ s/,\s*//;
  294. push @tmp, $w;
  295. }
  296. }
  297. }
  298. }
  299. #print "Names: @usn_names\n";
  300. #print "Date: $usn_date\n";
  301. #print "CVEs: @usn_CVEs\n";
  302. return (\@usn_names, $usn_date, \@usn_CVEs);
  303. }
  304. ## Get details of given CVE entry from NIST DB
  305. sub fetchCVE {
  306. my $cve_id = shift;
  307. ## print-only CVEs
  308. #print "CVE_ID: $cve_id\n";
  309. #return "";
  310. msg 10, " Fetching $cve_id";
  311. $cve_id =~ s/^CAN/CVE/;
  312. my $url= $config->{"cve_base_url"} . $cve_id;
  313. my $cve = get $url;
  314. #die " Unable to fetch CVE $cve_id" unless defined $cve;
  315. unless (defined $cve) {
  316. print "Failed to download CVE: $url\n";
  317. return "";
  318. }
  319. # Check for error pages: referenced but unpublished CVEs :-/
  320. if ($cve =~ /is\ valid\ CVE\ format,\ but\ CVE\ was\ not\ found/) {
  321. msg 5, " $cve_id does not exist in NIST DB\n";
  322. $cve = "";
  323. }
  324. return $cve;
  325. }
  326. ## Static map to correct errors in DSAs
  327. ## Return fixed list of CVE IDs or 0 to skip DSA
  328. sub fixDSAquirks {
  329. my $dsa_id = shift;
  330. my $dsa_state = shift;
  331. my @new_names = @{@$dsa_state[0]};
  332. my $new_date = @$dsa_state[1];
  333. my @new_cves = @{@$dsa_state[2]};
  334. ## These DSAs are totally screwed up..
  335. given ($dsa_id) {
  336. when ($_ eq "DSA 310-1") {
  337. @new_cves = ("CVE-2003-0385");
  338. }
  339. when ($_ eq "DSA-045-2") {
  340. @new_cves = ("CVE-2001-0414");
  341. }
  342. when ($_ eq "DSA-045-1") {
  343. @new_cves = ("CVE-2001-0414");
  344. }
  345. when ($_ eq "DSA 745-1") {
  346. @new_cves = ("CVE-2010-2155", "CVE-2009-4882");
  347. }
  348. when ($_ eq "DSA-1931-1") {
  349. @new_cves = ("CVE-2009-0689", "CVE-2009-2463");
  350. }
  351. when ($_ eq "DSA-1941-1") {
  352. @new_cves = ("CVE-2009-0755", "CVE-2009-3903", "CVE-2009-3904",
  353. "CVE-2009-3905", "CVE-2009-3606", "CVE-2009-3607",
  354. "CVE-2009-3608", "CVE-2009-3909", "CVE-2009-3938");
  355. }
  356. when ($_ eq "DSA-2056-1") {
  357. @new_cves = ("CAN-2005-1921", "CAN-2005-2106");
  358. }
  359. when ($_ eq "DSA 1095-1") {
  360. @new_cves = ("CVE-2006-0747", "CVE-2006-1861", "CVE-2006-2661");
  361. }
  362. when ($_ eq "DSA-2092-1") {
  363. @new_cves = ("CVE-2010-1625", "CVE-2010-1448", "CVE-2009-4497");
  364. }
  365. when ($_ eq "DSA 1284-1") {
  366. @new_cves = ("CVE-2007-1320", "CVE-2007-1321", "CVE-2007-1322", "CVE-2007-2893", "CVE-2007-1366");
  367. }
  368. when ($_ eq "DSA 1070-1") {
  369. $_ =~ s/CVE-2005-0528/CVE-2003-0985/ foreach (@new_cves);
  370. }
  371. };
  372. return (\@new_names, $new_date, \@new_cves);
  373. }
  374. ## static map to correct errors in USNs
  375. ## returns fixed list of CVE IDs or 0 to skip DSA
  376. sub fixUSNquirks {
  377. my $usn_id = shift;
  378. my $usn_state = shift;
  379. my @new_names = @{@$usn_state[0]};
  380. my $new_date = @$usn_state[1];
  381. my @new_cves = @{@$usn_state[2]};
  382. ## These DSAs are totally screwed up..
  383. given ($usn_id) {
  384. when ($_ eq "USN-6-1") {
  385. @new_names = ("postgresql");
  386. }
  387. };
  388. return (\@new_names, $new_date, \@new_cves);
  389. }
  390. ## Get CVE severity rating and report date and return
  391. ## (date base-score impact-score exploit-score)
  392. sub parseCVE {
  393. my $cve_id = shift;
  394. my $cve_date;
  395. my $cve_base;
  396. my $cve_impact;
  397. my $cve_exploit;
  398. my $cve = fetchCVE($cve_id);
  399. if ($cve eq "") {
  400. # No details means we assume highest score, but immediate fix.
  401. # There's not much justification for this, except CVSS also
  402. # assumes highest score..
  403. return (0, 10, 10, 10);
  404. }
  405. # Reporting Date -> $cve_date
  406. $cve =~ /<span class="label">Original\ release\ date:<\/span>(\d+)\/(\d+)\/(\d+)<\/div>/;
  407. $cve_date = parsedate("$3-$1-$2");
  408. die " Unable to extract date from CVE" unless defined $cve_date;
  409. # Base Score -> $cve_base
  410. unless ($cve =~ /CVSS\sv2\sBase\sScore:<\/span><a\shref=".*>(.*)<\/a>\s\(\w+\)/) {
  411. die " Unable to parse CVSSv2 Base Score for $cve_id";
  412. }
  413. $cve_base = $1;
  414. # Impact Subscore -> $cve_impact
  415. unless ($cve =~ /<div\ class="row"><span\ class="label">Impact\ Subscore:<\/span>\s+(.*)<\/div>/) {
  416. die " Unable to parse CVSSv2 Impact Score for $cve_id";
  417. }
  418. $cve_impact = $1;
  419. # Exploitability Subscore -> $cve_exploit
  420. unless ($cve =~ /<div\ class="row"><span\ class="label">Exploitability\ Subscore:<\/span>\s+(.*)<\/div>/) {
  421. die " Unable to parse CVSSv2 Exploitability Score for $cve_id";
  422. }
  423. $cve_exploit = $1;
  424. #print "$cve_base\n";
  425. #print "$cve_impact\n";
  426. #print "$cve_exploit\n";
  427. return ($cve_date, $cve_base, $cve_impact, $cve_exploit);
  428. }
  429. ## Fetch current Packages, Sources and sha1sums files
  430. ## These are needed to find CVE stats by sha1sums/pkg-names
  431. ## Only Sha1Sums is custom generated, others are from Debian.
  432. ## FIXME: Server might do on-the-fly gzip (but should not for bzip2)
  433. ## Return: 1 on success, to signal that new parsing is needed.
  434. sub fetchMeta {
  435. my $file = shift;
  436. my $urlbase = $config->{"pkg_base_url"};
  437. my $dir = $config->{"cache_dir"};
  438. msg 10, "Checking meta file $file..";
  439. my $bzFile = $file . ".bz2";
  440. my $url = $urlbase . $bzFile;
  441. my $ret = mirror $url, $dir . $bzFile; # download if new
  442. if ($ret == 200) {
  443. # check if the file actally changed..
  444. open(FILE, $dir . $bzFile) or die "Can't open '$dir$bzFile': $!";
  445. binmode(FILE);
  446. my $stamp = Digest::MD5->new->addfile(*FILE)->hexdigest;
  447. if ($state->{$file} eq $stamp) {
  448. print " unchanged..\n";
  449. return 0;
  450. } else {
  451. $state->{$file} = $stamp;
  452. }
  453. # file seems new, unpack for parsing..(TODO: should keep in $tmp..)
  454. bunzip2 $dir . $bzFile => $dir . $file#, AutoClose => 1
  455. or die "bunzip2 failed: $Bunzip2Error\n";
  456. return 1; # file changed
  457. } elsif ($ret != RC_NOT_MODIFIED ) {
  458. print "HTTP error code: $ret when fetching $url\n";
  459. return 0; # no updates
  460. }
  461. }
  462. ## Parse dpkg Packages file, create map deb-name->pkg-name
  463. sub parsePackages {
  464. my $pkgfile = shift;
  465. my %deb2pkg;
  466. my $pkgname;
  467. my $dir = $config->{"cache_dir"};
  468. msg 10, "Parsing Packages file...";
  469. $pkgfile = $dir . $pkgfile;
  470. open (PKG, "< $pkgfile");
  471. my @lines = <PKG>;
  472. LINE: foreach my $line (@lines) {
  473. if ($line =~ /^Package:\ (.+)/) {
  474. $pkgname = $1;
  475. } elsif ($line =~ /^Filename:\ .*\/(\S+)/) {
  476. $deb2pkg{$1} = $pkgname;
  477. } else {
  478. next LINE;
  479. }
  480. }
  481. #print map { "$_ => $deb2pkg{$_}\n" } keys %deb2pkg;
  482. return \%deb2pkg;
  483. }
  484. ## Parse dpkg Sources file, create map pkg-name->src-name
  485. sub parseSources {
  486. my $srcfile = shift;
  487. my $dir = $config->{"cache_dir"};
  488. my %pkg2src;
  489. my $srcname;
  490. my @pkgnames;
  491. my $checklinecontinuation=0;
  492. msg 10, "Parsing Sources file...";
  493. $srcfile = $dir . $srcfile;
  494. open (SRC, "< $srcfile");
  495. my @lines = <SRC>;
  496. LINE: foreach my $line (@lines) {
  497. ## sometimes, list of binary pkgs has newline, so we need to check next line..
  498. if ($checklinecontinuation == 1) {
  499. unless ($line =~ /^[[:alpha:]]+:\ /) {
  500. @pkgnames = split(/,\ /, $line);
  501. foreach my $pkg (@pkgnames) {
  502. $pkg2src{$pkg} = $srcname;
  503. }
  504. } else {
  505. $checklinecontinuation = 0;
  506. }
  507. next LINE;
  508. }
  509. if ($line =~ /^Package:\ (.+)/) {
  510. $srcname = $1;
  511. } elsif ($line =~ /^Binary:\ (.+)/) {
  512. @pkgnames = split(/,\ /, $1);
  513. foreach my $pkg (@pkgnames) {
  514. $pkg2src{$pkg} = $srcname;
  515. }
  516. $checklinecontinuation = 1;
  517. } else {
  518. next LINE;
  519. }
  520. }
  521. #print map { "$_ => $pkg2src{$_}\n" } keys %pkg2src;
  522. return \%pkg2src;
  523. }
  524. ## Parse Sha1Sums file. Format: "sha1sum::deb-name::unix-file-path"
  525. ## Create 2 maps: sha1sum->file, file->deb-name
  526. sub parseSha1Sums {
  527. my $sha1file = shift;
  528. my %sha1map;
  529. my $dir = $config->{"cache_dir"};
  530. msg 10, "Parsing Sha1Sums file...";
  531. $sha1file = $dir . $sha1file;
  532. open (SHA, "< $sha1file");
  533. my @lines = <SHA>;
  534. LINE: foreach my $line (@lines) {
  535. if ($line =~ /^(\w+)::(\S+)$/) {
  536. # It seems Perl can't handle that, needs around 4GB RAM :-/
  537. #$sha1map->{$1} = $3;
  538. #$filemap->{$3} = $2;
  539. $sha1map{$1} = $2;
  540. } else {
  541. die "Sha1Sums parse error, line reads: \n>>$line<<";
  542. }
  543. }
  544. return \%sha1map;
  545. }
  546. ## Parse local dpkg status, return list of debs
  547. sub parseStatus {
  548. my $stsfile = shift;
  549. my @pkglist;
  550. my $pkgname;
  551. msg 10, "Parsing dpkg status..";
  552. open (PKG, "< $stsfile");
  553. my @lines = <PKG>;
  554. LINE: foreach my $line (@lines) {
  555. if ($line =~ /^Package:\ (.+)/) {
  556. $pkgname = $1;
  557. } elsif ($line =~ /^Status:.*installed/) {
  558. push @pkglist, $pkgname;
  559. } else {
  560. next LINE;
  561. }
  562. }
  563. return \@pkglist;
  564. }
  565. sub parseAdvisory {
  566. my $adv = shift;
  567. given ($state->{"vendor"}) {
  568. when ($_ eq "debian") { return parseDSA($adv); }
  569. when ($_ eq "ubuntu") { return parseUSN($adv); }
  570. # when ($_ eq "redhat") { return checkRHSA; }
  571. default { die "Unsupported distribution $_"; }
  572. };
  573. }
  574. sub fixAdvisoryQuirks {
  575. my @arg = @_;
  576. given ($state->{"vendor"}) {
  577. when ($_ eq "debian") { return fixDSAquirks(@arg); }
  578. when ($_ eq "ubuntu") { return fixUSNquirks(@arg); }
  579. # when ($_ eq "redhat") { return checkRHSA; }
  580. default { die "Unsupported distribution $_"; }
  581. };
  582. }
  583. ## Update internal vuln. DB with new Advisory info
  584. ## Creates CVEtable for MTBF computation:
  585. ## ( cve-id => (date, delay, score1, score2, score3))
  586. sub updateCVETables {
  587. my $id = shift; # Advisory to merge into tables
  588. my @cvestats;
  589. msg 10, "Updating vulnerability database with advisory ".$state->{"vendor"}."/$id\n";
  590. my $adv = $dsatable->{$id};
  591. my @dsastats = parseAdvisory($adv);
  592. #print "\nADV-Stats:\n";
  593. #print "@{$dsastats[0]}\n";
  594. #print "@{$dsastats[2]}\n";
  595. ## fix DSAs that don't contain correct CVE refs
  596. @dsastats = fixAdvisoryQuirks($id, \@dsastats);
  597. #my $cve_ids = $dsastats[2];
  598. #print "\nADV-Stats:\n";
  599. #print "@{$dsastats[0]}\n";
  600. #print "@{$dsastats[2]}\n";
  601. foreach my $srcpkg (@{$dsastats[0]}) {
  602. push @{$src2dsa->{$srcpkg}}, $id;
  603. push @{$dsa2cve->{$id}}, @{$dsastats[2]};
  604. }
  605. foreach my $cve_id (@{$dsastats[2]}) {
  606. @cvestats = parseCVE($cve_id);
  607. if ($cvestats[0] > $dsastats[1] || $cvestats[0] == 0) {
  608. $cvestats[0] = $dsastats[1];
  609. }
  610. my @cvedata = ( $cvestats[0], $dsastats[1]-$cvestats[0],
  611. $cvestats[1], $cvestats[2], $cvestats[3] );
  612. $cvetable->{$cve_id} = \@cvedata;
  613. }
  614. }
  615. ## Check for updates on Package information
  616. sub aptsec_update {
  617. if (fetchMeta("Packages")) {
  618. $deb2pkg = parsePackages("Packages");
  619. }
  620. if (fetchMeta("Sources")) {
  621. $pkg2src = parseSources("Sources");
  622. }
  623. if (fetchMeta("Sha1Sums")) {
  624. $sha1map = parseSha1Sums("Sha1Sums");
  625. }
  626. #readAdvisories($last)
  627. }
  628. ## find list of src pkgs from bin pkgs based on pkg2src
  629. sub resolvePkg2Src {
  630. my $pkglist = shift;
  631. my @srclist;
  632. my %tmp;
  633. my $srcpkg;
  634. foreach my $pkg (@$pkglist) {
  635. $srcpkg = $pkg2src->{$pkg};
  636. if (defined $srcpkg) {
  637. $tmp{$pkg2src->{$pkg}} = 1;
  638. } else {
  639. msg 5, "Could not find source package for: $pkg";
  640. }
  641. }
  642. @srclist = keys %tmp;
  643. return \@srclist;
  644. }
  645. ## compute and store MTBF, MTBR and Scores of each src pkg
  646. ## uses: @cvestats = (date base-score impact-score exploit-score)
  647. ## output: %src2mtbf = (srcpkg=> (begin, num, delaysum, scoresum, maximpact))
  648. sub processCVEs {
  649. my @pkgs = shift;
  650. @pkgs = keys %$src2mtbf unless @pkgs;
  651. foreach my $pkg (@pkgs) {
  652. my @stats = ($now, 0, 0, 0, 0);
  653. foreach my $dsa_id (@{$src2dsa->{$pkg}}) {
  654. foreach my $cve_id (@{$dsa2cve->{$dsa_id}}) {
  655. if ($stats[0] > $cvetable->{$cve_id}[0]) {
  656. $stats[0] = $cvetable->{$cve_id}[0];
  657. }
  658. $stats[1]++;
  659. $stats[2]+= $cvetable->{$cve_id}[1];
  660. $stats[3]+= $cvetable->{$cve_id}[2];
  661. if ($stats[4] < $cvetable->{$cve_id}[3]) {
  662. $stats[4] = $cvetable->{$cve_id}[3];
  663. }
  664. }
  665. }
  666. # TODO: can only save for >=1?
  667. if ($stats[1] > 1) {
  668. $src2mtbf->{$pkg} = \@stats;
  669. }
  670. }
  671. }
  672. ## Use local system status(dpkg DB) for printing system status report
  673. sub aptsec_system {
  674. my $pkglist = parseStatus "/var/lib/dpkg/status";
  675. my $srclist = resolvePkg2Src $pkglist;
  676. printSystemStats($srclist);
  677. }
  678. ## Print 'trustworthiness' of a set(system) of src packages
  679. sub printSystemStats {
  680. my $srclist = shift;
  681. my $mtbf=0;
  682. my $mtrr=0;
  683. my $num=0;
  684. # we can assume that pkgs are independent (right?)
  685. PKG: foreach my $pkg (@$srclist) {
  686. my $rstats = $src2mtbf->{$pkg} or next PKG;
  687. my @stats = @$rstats;
  688. $mtbf+=1/(($now-$stats[0])/$stats[1]/$secperday);
  689. $mtrr+=$stats[2]/$stats[1]/$secperday;
  690. if ($verbosity > 0) {
  691. printf "MTBF: %4d, MTRR: %4d,\tPkg: %s\n",
  692. ($now-$stats[0])/$stats[1]/$secperday,
  693. $stats[2]/$stats[1]/$secperday,
  694. $pkg;
  695. }
  696. $num++;
  697. }
  698. printf "\n";
  699. printf "Packages with past vulnerabilities installed: %d\n", $num;
  700. printf "System MTBF: %5.1f days per failure\n", 1/$mtbf;
  701. printf "System MTRR: %5.1f days\n", $mtrr/$num;
  702. printf "\n";
  703. }
  704. ## show info on a single src pkg, resolv to src if needed
  705. sub aptsec_show {
  706. my $pkg = shift;
  707. unless (defined $pkg) {
  708. aptsec_help();
  709. exit;
  710. }
  711. if (!($src2dsa->{$pkg}) && $pkg2src->{$pkg}) {
  712. print "Resolving $pkg to $pkg2src->{$pkg}\n";
  713. $pkg = $pkg2src->{$pkg};
  714. }
  715. print "The following binary packages are created from $pkg:\n";
  716. foreach (keys %$pkg2src) {
  717. print "$_\n" if ($pkg2src->{$_} eq $pkg);
  718. }
  719. unless ($src2dsa->{$pkg} || @{$src2mtbf->{$pkg}}) {
  720. print "No vulnerabilities found for source package $pkg.";
  721. exit;
  722. }
  723. print "\nInformation on package $pkg:\n\n";
  724. foreach my $dsa_id (sort @{$src2dsa->{$pkg}}) {
  725. print "DSA: DSA-$dsa_id\n";
  726. foreach my $cve_id (@{$dsa2cve->{$dsa_id}}) {
  727. my ($sec,$min,$hrs,$day,$mon,$yr) = localtime($cvetable->{$cve_id}[0]);
  728. printf "%s: Base Score: %04.1f, %02d.%02d.%04d\n",
  729. $cve_id, $cvetable->{$cve_id}[2], $day, $mon, $yr+1900;
  730. }
  731. }
  732. my @stats = @{$src2mtbf->{$pkg}};
  733. my ($sec,$min,$hrs,$day,$mon,$yr) = localtime($stats[0]);
  734. print "\nOverall vulnerability stats: \n";
  735. printf " First one reported: %02d.%02d.%04d\n", $day, $mon, $yr+1900;
  736. printf " Total vulnerabilities: %d\n", $stats[1];
  737. printf " Average Base Score: %04.2f\n", $stats[3]/$stats[1];
  738. printf " Highest Impact Score: %d\n", $stats[4];
  739. printf " MTBF in days: %.2f\n", ($now-$stats[0])/$stats[1]/$secperday;
  740. printf " MTRR in days: %.2f\n\n", $stats[2]/$stats[1]/$secperday;
  741. }
  742. sub aptsec_help {
  743. print "\n";
  744. print "Usage:\n";
  745. print "\n";
  746. print "help This cruft\n";
  747. print "update Update vulnerability databases\n";
  748. print "system Compute expected failure rates for local system\n";
  749. print "show <pkg> Show failure rates and vulnerability statistics for <pkg>\n";
  750. print "\n";
  751. }
  752. ## Print system status report from component(files) measurements (sha1sums)
  753. ## Expected input format is Linux IMA. We assume input was validated.
  754. ##
  755. ## Note: aptsec_system(), considers *reportedly installed* packages, while this
  756. ## one looks at *actually loaded* software that influenced the CPU since bootup.
  757. sub aptsec_attest {
  758. my $sha1file = shift;
  759. my %tmp;
  760. my $pkg;
  761. my @pkglist=();
  762. $sha1file = "/sys/kernel/security/ima/ascii_runtime_measurements" unless ($sha1file);
  763. open (SHA, "< $sha1file") or die "Unable to open file $sha1file";
  764. my @lines = <SHA>;
  765. LINE: foreach my $line (@lines) {
  766. if ($line =~ /[0-9]{2}\ \w{40}\ ima\ (\w+)\ (\S+)/) {
  767. if ($sha1map->{$1}) {
  768. $tmp{$sha1map->{$1}} = 1;
  769. } else {
  770. print "Unknown measured file: $1 $2\n";
  771. }
  772. } else {
  773. print "Failed to parse attestation data input from $sha1file\n";
  774. }
  775. }
  776. foreach my $deb (keys %tmp) {
  777. if ($pkg = $deb2pkg->{$deb}) {
  778. push @pkglist, $pkg;
  779. }
  780. }
  781. my $srclist = resolvePkg2Src (\@pkglist);
  782. printSystemStats($srclist);
  783. }
  784. # read USN from stdin, assuming mbox format (separated by "^From ")
  785. sub readDSA {
  786. my $adv="";
  787. my $id="";
  788. my %newAdv;
  789. msg 5, "Reading new DSA advisory...";
  790. my @lines = <STDIN>;
  791. LINE: foreach my $line (@lines) {
  792. unless ($line =~ m/^From\ /) {
  793. if ($line =~ /^Debian\ Security\ Advisory\ (.*)\s\s+/) {
  794. $id = $1;
  795. $id =~ s/\s+$//;
  796. }
  797. $adv = join("", $adv,$line) ;#if ($id ne "");
  798. next LINE;
  799. }
  800. if ($adv && $id ) {
  801. $newAdv{$id} = $adv;
  802. #printf "\n\nNew Advisory:\n%s", $newAdv{$id};
  803. $adv="";
  804. $id="";
  805. }
  806. }
  807. if ($adv && $id ) {
  808. $newAdv{$id} = $adv;
  809. #printf "\n\nNew Advisory:\n%s", $newAdv{$id};
  810. $adv="";
  811. $id="";
  812. }
  813. return \%newAdv;
  814. }
  815. # read USN from stdin, assuming mbox format (separated by "^From ")
  816. sub readUSN {
  817. my $adv="";
  818. my $id="";
  819. my %newAdv;
  820. msg 15, "Reading in Ubuntu Security Notice..";
  821. my @lines = <STDIN>;
  822. LINE: foreach my $line (@lines) {
  823. unless ($line =~ m/^From\ /) {
  824. if ($line =~ /^Ubuntu\ Security\ Notice\ (.*)\s\s+/) {
  825. $id=$1;
  826. $id =~ s/\s+$//;
  827. }
  828. $adv = join("", $adv,$line) ;#if ($id ne "");
  829. next LINE;
  830. }
  831. if ($adv && $id) {
  832. $newAdv{$id} = $adv;
  833. $adv="";
  834. $id="";
  835. #printf "\n\nNew Advisory:\n%s", $newAdv{$id};
  836. }
  837. }
  838. if ($adv && $id ) {
  839. $newAdv{$id} = $adv;
  840. #printf "\n\nNew Advisory:\n%s", $newAdv{$id};
  841. $adv="";
  842. $id="";
  843. }
  844. return \%newAdv;
  845. }
  846. ## Should this advisory be skipped?
  847. sub blacklistedAdvisory {
  848. my $dsa_id = shift or die "DSAquirks: no id given..";
  849. ## check if DSA is blacklisted..
  850. my @id_blacklist = ("DSA-1975-1");
  851. grep ($_ eq $dsa_id, @id_blacklist) and return "true";
  852. return; # FALSE
  853. }
  854. ## read in new security advisories from STDIN
  855. ## one by one, slow slow slow.. :-/
  856. sub aptsec_newadv {
  857. my $newAdv;
  858. my $dist;
  859. given ($state->{"vendor"}) {
  860. when ($_ eq "debian") { $newAdv = readDSA; }
  861. when ($_ eq "ubuntu") { $newAdv = readUSN; }
  862. # when ($_ eq "redhat") { ($id,$adv) = checkRHSA; }
  863. default { die "Unsupported distribution $_"; }
  864. };
  865. ADV: foreach my $id (keys %$newAdv) {
  866. # skip this advisory?
  867. next ADV if (blacklistedAdvisory($id));
  868. # if not known, process advisory
  869. if ($dsatable->{$id}) {
  870. print $state->{"vendor"} . " advisory $id already known.\n";
  871. } else {
  872. $dsatable->{$id} = $newAdv->{$id};
  873. updateCVETables($id);
  874. }
  875. }
  876. # recompute all pkg statistics
  877. foreach my $srcpkg (keys %$src2dsa) {
  878. #print "srcpkg: $srcpkg\n";
  879. processCVEs($srcpkg);
  880. }
  881. }
  882. my $action;
  883. $action = shift or $action = "help";
  884. load_state();
  885. detect_distribution();
  886. given ($action) {
  887. when ($_ eq "update") {
  888. load_DBs;
  889. aptsec_update();
  890. save_DBs;
  891. }
  892. when ($_ eq "newadv") {
  893. load_DBs; #or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n";
  894. #$state->{"vendor"} = "ubuntu";
  895. aptsec_newadv();
  896. save_DBs;
  897. }
  898. when ($_ eq "system" || $_ eq "status") {
  899. load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n";
  900. aptsec_system();
  901. }
  902. when ($_ eq "show") {
  903. load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n";
  904. aptsec_show(shift);
  905. }
  906. when ($_ eq "attest") {
  907. load_DBs or die "Failed to load some cached file(s), please rebuild with 'apt-sec update'\n";
  908. aptsec_attest(shift);
  909. }
  910. default {
  911. aptsec_help();
  912. }
  913. };
  914. save_state();;