#!/usr/bin/perl -w
use strict;

# if you change anything, also notify SAP: vas@sap.com

my $dn = `dirname $0`;chomp($dn);
my $pwd = `pwd`;chomp($pwd);
if ($dn !~ /^\//) { $dn = $pwd . "/" . $dn; }
push @INC,$dn;
my $cverepobase=`dirname $dn`;
chomp($cverepobase);

#my $osvdir="/mounts/mirror/SuSE/ftp.suse.com/pub/projects/security/osv-cve/";
my $osvdir="$cverepobase/osv-cve";

use POSIX qw/strftime setlocale LC_TIME/;

require CanDBReader;
require CVEListReader;
require CPE;
require UpdateInfoReader;
UpdateInfoReader->import_product_updates();
#UpdateInfoReader->import_images();
require SMASHData;
&read_all_cached_issues();

use JSON;

my $jsoncoder = JSON->new->allow_nonref;
$jsoncoder->canonical(1);


require PInt;
require ModuleContained;

use IO::File;
use XML::Writer;

my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();

$year += 1900;

my @cves = ();
if (%CanDBReader::allcans) { # superflous, but avoid single use warning
	@cves = sort keys %CanDBReader::allcans;
}

sub
map_cvss_severity($) {
	my ($score) = @_;

	return "CRITICAL"	if ($score >= 9.0);
	return "HIGH"		if ($score >= 7.0);
	return "MEDIUM"		if ($score >= 4.0);
	return "LOW"		if ($score >= 0.1);
	return "NONE";
}

sub
filter_score($) {
	my ($score) = @_;

	$score =~ s/^(\d*\.\d)\d*$/$1/;
	return eval($score);	# must be number, not string in json
}

sub needs_page($) {
	my ($cve) = @_;

	# if we released stuff, embargoed no longer triggers
	if (defined($CanDBReader::advisoryids{$cve})		||
	    defined($CanDBReader::note{$cve})			||
	    defined($UpdateInfoReader::products{$cve})
	) {
		return 1;
	}

	# if we did not yet release stuff, and the CVE is tagged embargoed, do not generate a page.
	if (defined($CanDBReader::embargoed{$cve}) || defined($SMASHData::embargoedcves{$cve})) {
		print STDERR "$cve is embargoed\n" if (-t STDERR);
		return 0;
	}

	# generate pages if we have stuff in QA, or have bugzillas
	if (defined($CanDBReader::bugzillas{$cve}) || defined($UpdateInfoReader::productsinqa{$cve}) || defined($SMASHData::pkgstate{$cve})) {
		return 1;
	} else {
		return 0;
	}
}

sub cvesortfun($$) {
	my ($xa,$xb) = @_;
	my ($ya,$na,$yb,$nb);
	if ($xa =~ /-(\d*)-(\d*)$/) {
		$ya = $1; $na = $2;
		#print "$xa - $ya / $na\n";
	} else {
		print "$xa did not parse\n";
		$ya = 9999;
	}
	if ($xb =~ /-(\d*)-(\d*)$/) {
		$yb = $1; $nb = $2;
		#print "$xb - $yb / $nb\n";
	} else {
		print "$xb did not parse\n";
		$yb = 9999;
	}
	return $ya <=> $yb if ($ya != $yb);
	return $na <=> $nb;
}

my %printedcves;
foreach my $cve (@cves) { if (needs_page($cve)) { $printedcves{$cve} = 1; } }
foreach my $cve (keys %UpdateInfoReader::productsinqa) {
	next if ($CanDBReader::embargoed{$cve});
	next if ($SMASHData::embargoedcves{$cve});
	$printedcves{$cve} = 1;
}

foreach my $cve (keys %SMASHData::pkgstate) {
	next if ($CanDBReader::embargoed{$cve});
	next if ($SMASHData::embargoedcves{$cve});
	$printedcves{$cve} = 1;
}
foreach my $cve (keys %UpdateInfoReader::products) { $printedcves{$cve} = 1; }

delete $printedcves{'NOT-SECURITY'};

open(SUPPRESSLIST,"$cverepobase/data/suppress");
while (<SUPPRESSLIST>) {
	chomp;
	delete $printedcves{$_};
}
close(SUPPRESSLIST);

my @printedcves = sort cvesortfun keys %printedcves;

print "generating CVRF / CVE pages at " . localtime() . "\n";

# @printedcves = ('CVE-2021-33200','CVE-2021-37623', 'CVE-2022-32886', 'CVE-2022-32891', 'CVE-2022-32912', 'CVE-2022-36056', 'CVE-2022-40674') ;

while (my $cve = pop @printedcves) {
	my %xx;
	my @xx;
	my $str;
	my $released_products = "";
	my %products;
	my %osv;

	print STDERR "$cve\n" if (-t STDERR);

	next unless ($cve =~ /^(CAN|CVE)-\d*-\d*$/);

	if (!&needs_page($cve)) {
		print STDERR "unlinked/unreleased CVE id $cve, skipping.\n";
		next;
	}
	# fetch the content of state and severity etc.
	read_smash_issue($cve,0);

	############ OUTPUT the stuff ##################################

	my $fn = "$osvdir/$cve.json";

	my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat $fn;
	my $cvemtime;
	($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$cvemtime,$ctime,$blksize,$blocks) = stat "$cverepobase/cve/$cve.html";

	#if ($cvemtime < $mtime) {
	#	print "skipping $cve as CVE page is older than CVRF-CVE file.\n";
	#	next;
	#}

	# read old JSON, if present.

	my $oldosv;
	my $oldjson;

	if (open(OLDJSON,"<$fn")) {
		$oldjson = join("",<OLDJSON>);
		close(OLDJSON);

		eval {
			$oldosv = decode_json($oldjson);
		} or do {
			die "json invalid: $oldjson\n";
		};
	}

	my $dt = DateTime->now;
	my $nowdate = $dt->iso8601()."Z";

	my $initial_release_date = $dt->iso8601()."Z";
	my $oldmodifieddate = $dt->iso8601()."Z";

	if (defined($oldjson) && defined($oldosv->{"published"})) {
		$initial_release_date = $oldosv->{"published"};
		$oldmodifieddate = $oldosv->{"modified"};
	}

	####### determine OSV affected array 

	my @affected = ();
	if (defined($UpdateInfoReader::productsinqa{$cve})) {
		my %xproducts = %{$UpdateInfoReader::productsinqa{$cve}};

		foreach my $product (sort keys %xproducts) {
			next if (UpdateInfoReader::blacklistedproduct($product));
			next unless (defined($xproducts{$product}->{'packages'}));

			my %pkgs = %{$xproducts{$product}->{'packages'}};

			foreach my $pkg (sort keys %pkgs) {
				my %osvaffected = (
					"package" => {
						"ecosystem" => $product,
						"name" => $pkg,
					},
					"ranges" => [ {
						"type" => "ECOSYSTEM",
						"events" => [
							{
								"introduced" => "0",
							}
							# , {   	### we are not yet fixed on customer side, even if we know the version
							#	"fixed" => "$pkgs{$pkg}",
							#}
						]
					} ]
				);
				push @affected,\%osvaffected;
			}
		}
	}

	if (defined($UpdateInfoReader::products{$cve})) {
		my %xproducts = %{$UpdateInfoReader::products{$cve}};

		foreach my $product (sort keys %xproducts) {
			if (!defined($xproducts{$product}->{'packages'})) {
				print STDERR "$cve: no packages for $product, but entry?\n";
				next;
			}
			my %pkgs = %{$xproducts{$product}->{'packages'}};

			foreach my $pkg (sort keys %pkgs) {
				my %osvaffected = (
					"package" => {
						"ecosystem" => $product,
						"name" => $pkg,
					},
					"ranges" => [ {
						"type" => "ECOSYSTEM",
						"events" => [
							{
								"introduced" => "0",
							} , {   
								"fixed" => "$pkgs{$pkg}",
							}
						]
					} ]
				);
				push @affected,\%osvaffected;
			}
		}
	}

	# list affected ones

	my %smashprods = ();
	if (defined ($SMASHData::pkgstate{$cve})) {
		%smashprods= %{$SMASHData::pkgstate{$cve}};
	}
	foreach my $product (sort keys %smashprods) {
		next if (UpdateInfoReader::blacklistedproduct($product));

		my %pkgs = %{$smashprods{$product}};
		foreach my $pkg (sort keys %pkgs) {
			if ($pkgs{$pkg} eq "Affected") {
				print STDERR "$cve - $product - $pkg - $pkgs{$pkg}\n" if -t STDERR;
				my $havepkg = 0;
				foreach my $binpkg (sort keys %{$UpdateInfoReader::product2packages{$product}->{$pkg}}) {
					$havepkg = 1 if ($binpkg eq $pkg);

					my %osvaffected = (
						"package" => {
							"ecosystem" => $product,
							"name" => $binpkg,
						},
						"ranges" => [ {
							"type" => "ECOSYSTEM",
							"events" => [
								{
									"introduced" => "0",
								}
							]
						} ]
					);
					push @affected,\%osvaffected;
				}
				if (!$havepkg) {
					my %osvaffected = (
						"package" => {
							"ecosystem" => $product,
							"name" => $pkg,
						},
						"ranges" => [ {
							"type" => "ECOSYSTEM",
							"events" => [
								{
									"introduced" => "0",
								}
							]
						} ]
					);
					push @affected,\%osvaffected;
				}
			}
		}
	}

	####### 

	my $cvss3 = get_cvssv3_issue($cve);
	my @severity = ();
	# only if we have a score, and we have fixed products to list.
	if (defined($cvss3))  {
		my $basescore;
		my $basevector;
		if (defined($cvss3->{'SUSE'})) {
			$basescore = filter_score($cvss3->{'SUSE'}->{base_score});
			$basevector = $cvss3->{'SUSE'}->{base_vector};
		} else {
			if (defined($cvss3->{'National Vulnerability Database'})) {
				$basescore = filter_score($cvss3->{'National Vulnerability Database'}->{base_score});
				$basevector = $cvss3->{'National Vulnerability Database'}->{base_vector};
			}
		}
		if (defined($basescore)) {
			$basevector =~ /^CVSS:(\d.\d)\//;

			my %vulscore = (
				"type"	=> "CVSS_V3",
				"score"	=> "$basevector",
			);
			push @severity,\%vulscore;
		}
	}


	my @references = (
		{
		"type"	=> "WEB",
		"url"	=> "https://www.suse.com/security/cve/$cve",
		}
	);

	if (defined($CanDBReader::bugzillas{$cve})) {
		my %bugs = map { $_ => 1 } split (/,/,$CanDBReader::bugzillas{$cve});
		foreach my $bug (sort keys %bugs) {
			my %bugreference = (
					"type"	=> "REPORT",
					"url"	=> "https://bugzilla.suse.com/$bug",
					);
			push @references,\%bugreference;
		}
	}
	my @related;
	if (defined($CanDBReader::advisoryids{$cve})) {
		my %xx = map { $_ => 1 } split(/,/,$CanDBReader::advisoryids{$cve});
		foreach my $said (sort keys %xx) {
			push @related,$said;
			my $urlnotice = "\L$said";
			$urlnotice =~ /su-(\d*):/;
			my $noticeyear = $1;
			$urlnotice =~ s/://;
			$urlnotice = "https://www.suse.com/support/update/announcement/$noticeyear/$urlnotice/";

			if ($said !~ /^SUSE/) {       # only SUSE notices are uploaded to suse.com/support/update, otherwise use imported one
				$urlnotice = $CanDBReader::advisoryid2url{$said};
			}

			my %reference = (
				"type"	=> "ADVISORY",
				"url"	=> $urlnotice,
			);
			push @references,\%reference;
		}
	}

	$osv{"published"}	= $initial_release_date;
	$osv{"modified"}	= $oldmodifieddate;
	$osv{"id"}		= $cve;
	$osv{"summary"}		= "SUSE $cve";
	$osv{"details"}		= get_description($cve) || "unknown";
	$osv{"related"}		= \@related;
	$osv{"aliases"}		= [];	# 1<->1 transient relationship, currently not available.
	$osv{"references"}	= \@references;
	$osv{"severity"}	= \@severity;
	$osv{"affected"}	= \@affected;


	my $newjson = $jsoncoder->encode(\%osv);

	if ($newjson ne $oldjson) {
		print "CHANGED: $cve json got changed\n";
		$osv{"modified"} = $nowdate;
	} else {
		print "UNCHANGED: $cve json is unchanged\n";
	}

	open(OSV,">$fn.new");
	binmode(OSV,":utf8");
	print OSV $jsoncoder->encode(\%osv);
	close(OSV);

	my $newosvcontent;
	my $oldosvcontent = "<empty>";
	if (open(OSV,"<$fn.new")) {
		binmode(OSV,":utf8");
		$newosvcontent = join("",<OSV>);
		close(OSV);
	}
	if (open(OSV,"<$fn")) {
		binmode(OSV,":utf8");
		$oldosvcontent = join("",<OSV>);
		close(OSV);
	}
	if ($oldosvcontent ne $newosvcontent) {
		if (system("jq . <$fn >$fn.formatted ; jq . <$fn.new >$fn.new.formatted ; diff -uN $fn.formatted $fn.new.formatted")) {
			rename("$fn.new",$fn);
			unlink("$fn.new.formatted");
			unlink("$fn.formatted");
		} else {
			print STDERR "diff for $fn not detected by reader?\n";
			unlink("$fn.new");
		}
	} else {
		unlink("$fn.new");
	}
}
print "SUCCESS\n";
