#!/usr/bin/perl
###############################################################################
##
## Functions for downloading and parsing Ubuntu Security Advisories (USNs)
##
###############################################################################
use strict;
use warnings;
#use Storable; # persistant storage
use LWP::UserAgent;
#use Config::Auto; # libconfig-auto-perl
use Time::ParseDate; # libtime-modules-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;
use feature "switch";
## Fetch USN from ubuntu archive. Can't use tracker since dates are missing.
sub fetchUSN {
my $id= shift;
my $usn_base_url = shift;
my $usn;
trace " Fetching USN-$id\n";
my $url= $usn_base_url . "usn-" . $id;
debug "USN URL: $url\n";
$usn = get $url;
unless (defined $usn) {
note " USN-$id is not available\n";
return "";
}
return $usn;
}
## Try to find new USNs by iteration, return table of USNs to process
## Ignores advisory revisions (not clear what to do with them, mostly junk)
sub checkUSNs($$) {
my $state = shift;
my $config = shift;
my %usntable;
my $next_usn = $state->{"next_adv"};
my $rev = 1;
my $id;
trace "Checking for new USNs..\n";
if ($next_usn < $config->{"first_usn"}) {
note "Cache was deleted, starting at USN $next_usn\n";
$next_usn = $config->{"first_usn"};
}
$rev = 0 if ($next_usn == 60); # fuck
$id = sprintf("%d-%d",$next_usn, $rev);
my $usn = fetchUSN($id, $config->{"usn_base_url"});
until ($usn eq "") {
debug " Got USN-$id\n";
# strip and save USN content
$usn =~ s/.*
//si;
$usn =~ s/<\/div>.*//si;
$usntable{$id} = $usn;
### check for advisory revisions?
#until ($usn eq "") {
# msg 10, " Got USN-$id";
# $usn =~ s/.*
//si;
# $usn =~ s/<\/div>.*//si;
# $usntable{$id} = $usn;
#
# $rev++;
# $rev++ unless (fixUSNquirks($next_usn,$rev)); # skip USN?
# $id = sprintf("%d-%d", $next_usn, $rev);
# $usn = fetchUSN($id);
#}
$rev = 1;
$next_usn++;
$next_usn++ if (blacklistedUSN("USN-".$next_usn));
$rev = 0 if ($next_usn == 60);
$id = sprintf("%d-%d", $next_usn, $rev);
$usn = fetchUSN($id, $config->{"usn_base_url"});
}
return \%usntable;
}
## static map to correct errors in USNs
## returns fixed list of CVE IDs or 0 to skip DSA
sub fixUSNquirks {
my $usn_id = shift;
my $usn_state = shift;
my @new_names = @{@$usn_state[0]};
my $new_date = @$usn_state[1];
my @new_cves = @{@$usn_state[2]};
## These DSAs are totally screwed up..
given ($usn_id) {
when ($_ eq "291") {
@new_cves = ("CVE-2006-0747", "CVE-2006-1861", "CVE-2006-2661");
}
when ($_ eq "853") {
@new_cves = (
"CVE-2009-0689", "CVE-2009-3274", "CVE-2009-3370", "CVE-2009-3371",
"CVE-2009-3372", "CVE-2009-3373", "CVE-2009-3374", "CVE-2009-3375",
"CVE-2009-3376", "CVE-2009-3377", "CVE-2009-3380", "CVE-2009-3381",
"CVE-2009-3382", "CVE-2009-3383");
}
};
return (\@new_names, $new_date, \@new_cves);
}
## Should this advisory be skipped?
sub blacklistedUSN {
my $id = shift or die "Advisory blacklist: no id given..";
## check if Adv is blacklisted..
my @id_blacklist = ("USN-667", "USN-718", "USN-910", "USN-935",
"USN-950", "USN-1034", "USN-1049", "USN-1050");
grep ($_ eq $id, @id_blacklist) and return "true";
return; # FALSE
}
## Parse USN data and return array
## (src-pkg-name date (CVE-id)*)
sub parseUSNhtml {
my $usn = shift;
my @usn_names;
my @usn_CVEs;
my $usn_date;
my $usn_type;
my @tmp;
my @lines = split(/\n/,$usn);
LINE: foreach my $line (@lines) {
# Date Reported -> $dsa_date
unless ($usn_date) {
#print "Looking for date\n";
#print "Line: $line\n";
if ($line =~ /^Ubuntu\ Security\ Notice\ .*\d+-\d+\s+(\w+)\s+(\d+),\s+(\d+)/) {
$usn_date = parsedate("$1 $2, $3");
}
next LINE;
}
# Affected Packages -> @dsa_names
unless (@usn_names) {
#print "Looking for name\n";
#print "Line: $line\n";
if ($line =~ m/^CVE-/ || $line =~ m/^CAN-/ || $line =~ m/^http.*launchpad.net\/bugs/|| $line =~ m/^===========/) {
@usn_names = @tmp;
@tmp=();
$usn_type = pop @usn_names;
} else {
foreach my $w (split(/\s+/, $line)) {
$w =~ s/,\s*//;
push @tmp, $w;
}
next LINE;
}
}
# Security database references (CVEs) -> @dsa_CVEs
unless (@usn_CVEs) {
#print "Looking for cve\n";
#print "Line: $line\n";
if ($line =~ m/^===========/) {
@usn_CVEs = @tmp;
} else {
foreach my $w (split(/\s+/, $line)) {
$w =~ s/,\s*//;
push @tmp, $w;
}
}
}
}
return (\@usn_names, $usn_date, \@usn_CVEs);
}
1;