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

# 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 $csafdir="/mounts/mirror/SuSE/ftp.suse.com/pub/projects/security/csaf-vex/";
my $csafdir="/space/securitybot/csaf-vex/";

use POSIX qw/strftime setlocale LC_TIME/;

require Sign;
require CanDBReader;
require CVEListReader;
require CPE;
require UpdateInfoReader;
UpdateInfoReader->import_product_updates();
#UpdateInfoReader->import_images();
require SMASHData;
&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 %chostnames = (
	'amazon.*15-sp1'	=> "SLES 15 SP1 CHOST Images for Amazon EC2",
	'amazon.*15-sp2'	=> "SLES 15 SP2 CHOST Images for Amazon EC2",
	'amazon.*15-sp3'	=> "SLES 15 SP3 CHOST Images for Amazon EC2",
	'amazon.*15-sp4'	=> "SLES 15 SP4 CHOST Images for Amazon EC2",
	'amazon.*15-sp5'	=> "SLES 15 SP5 CHOST Images for Amazon EC2",
	'google.*15-sp1'	=> "SLES 15 SP1 CHOST Images for Google",
	'google.*15-sp2'	=> "SLES 15 SP2 CHOST Images for Google",
	'google.*15-sp3'	=> "SLES 15 SP3 CHOST Images for Google",
	'google.*15-sp4'	=> "SLES 15 SP4 CHOST Images for Google",
	'google.*15-sp5'	=> "SLES 15 SP5 CHOST Images for Google",
	'microsoft.*15-sp1'	=> "SLES 15 SP1 CHOST Images for Microsoft Azure",
	'microsoft.*15-sp2'	=> "SLES 15 SP2 CHOST Images for Microsoft Azure",
	'microsoft.*15-sp3'	=> "SLES 15 SP3 CHOST Images for Microsoft Azure",
	'microsoft.*15-sp4'	=> "SLES 15 SP4 CHOST Images for Microsoft Azure",
	'microsoft.*15-sp5'	=> "SLES 15 SP5 CHOST Images for Microsoft Azure",
);

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;
}

# Compares previous csaf and new csaf
sub
csaf_trees_same($$) {
	my ($oldcsaf,$newcsaf) = @_;

	# if there is no packagelist, there is no vulnerability set.
	if (defined($oldcsaf->{'vulnerabilities'}) && !defined($newcsaf->{'vulnerabilities'})) {
		print STDERR "compare: vulnerabilites defined in old, but not in new\n";
		return 0;
	}
	if (defined($newcsaf->{'vulnerabilities'}) && !defined($oldcsaf->{'vulnerabilities'})) {
		print STDERR "compare: vulnerabilites defined in new, but not in old\n";
		return 0;
	}

	if (defined($newcsaf->{'vulnerabilities'})) {
		# we should have just 1 vulnerability in
		my $oldvul = $oldcsaf->{'vulnerabilities'}->[0];
		my $newvul = $newcsaf->{'vulnerabilities'}->[0];

		# if we did not have scores, but now do ... not the same
		if (!defined($oldvul->{'scores'}) && defined($newvul->{'scores'})) {
			print STDERR "compare: score not in old, but present in old\n";
			return 0;
		}

		if (defined($oldvul->{'score'})) {	# only compare if we have scores
			if ($oldvul->{'scores'}->[0]->{'cvss_v3'}->{'vectorString'} ne $newvul->{'scores'}->[0]->{'cvss_v3'}->{'vectorString'}) {
				print STDERR "compare: cvss score changed\n";
				return 0;
			}

			my @oldprods = @{$oldvul->{'scores'}->[0]->{'products'}};
			my @newprods = @{$oldvul->{'scores'}->[0]->{'products'}};


			if ($#oldprods != $#newprods) {
				print STDERR "compare: product list length different.\n";
				return 0;
			}
		}
	}

	return 0 if (!defined($oldcsaf->{'document'}->{'references'}) && defined($newcsaf->{'document'}->{'references'}));

	if (defined($newcsaf->{'document'}->{'references'})) {
		my @oldrefs = @{$oldcsaf->{'document'}->{'references'}};
		my @newrefs = @{$newcsaf->{'document'}->{'references'}};
		if ($#oldrefs != $#newrefs) {
			print STDERR "compare: reference list length different.\n";
			return 0;
		}
	}

	# recursive descent?

	return 1;
}

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') ;

my %lastmodified = ();

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

	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.
	&SMASHData::read_smash_issue($cve,0);

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

	# Must match "id" (lowercase, replace invalid chars by _)
	my $fn = "$csafdir/\L$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 $json = "";
	my $oldcsaf;

	if (open(OLDJSON,"<$fn")) {
		binmode(OLDJSON,":utf8");
		$json = join("",<OLDJSON>);
		close(OLDJSON);

		eval {
			$oldcsaf = decode_json($json);
		} or do {
			warn "json invalid, deleting: $json\n";
			unlink($fn);
		};
	}

	my $dt = DateTime->now;


### here

	my %allproducts = ();
	my %packages = ();

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

		foreach my $product (keys %xproducts) {
			next if (UpdateInfoReader::blacklistedproduct($product));

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

			$allproducts{$product} = 1;
			foreach my $pkg (keys %pkgs) {
				next if ($pkg =~ /-debuginfo/);
				next if ($pkg =~ /-debugsource/);
				$packages{"$pkg-".$pkgs{$pkg}} = 1;
			}
		}
	}
	if (defined($UpdateInfoReader::productsinqa{$cve})) {
		my %xproducts = %{$UpdateInfoReader::productsinqa{$cve}};

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

			my %pkgs = %{$xproducts{$product}->{'packages'}};
			$allproducts{$product} = 1;
			foreach my $pkg (keys %pkgs) {
				next if ($pkg =~ /-debuginfo/);
				next if ($pkg =~ /-debugsource/);
				$packages{"$pkg-".$pkgs{$pkg}} = 1;
			}
		}
	}

	# calculate all smash products we have...
	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 (keys %pkgs) {
			if (	($pkgs{$pkg} eq "Not affected")     ||
				($pkgs{$pkg} eq "Affected")         ||
				($pkgs{$pkg} eq "Already fixed")
			) {
				print STDERR "$cve - $product - $pkg - $pkgs{$pkg}\n" if -t STDERR;
				$allproducts{$product} = 1;
				foreach my $binpkg (sort keys %{$UpdateInfoReader::product2packages{$product}->{$pkg}}) {
					$packages{"$binpkg"} = 1;	# no version
				}
				$packages{"$pkg"} = 1;	# no version ... just add source rpm too
			}
		}
	}

	my %containingproducts = ();

	foreach my $product (keys %allproducts) {
		next if (UpdateInfoReader::blacklistedproduct($product));
		my @contained = contained($product);
		if (@contained) {
			foreach my $contained (@contained) {
				$containingproducts{$contained}->{$product} = 1;
			}
		} else {
			$containingproducts{$product}->{$product} = 1;
		}
	}

	my @csafproductfamilies = ();
	foreach my $baseproduct (sort keys %containingproducts) {
		foreach my $product (sort keys %{$containingproducts{$baseproduct}}) {
			next if (UpdateInfoReader::blacklistedproduct($product));
			my %csafproduct = (
				"name"          => $product,
				"product_id"    => $product,
			);
			if (&SMASHData::get_cpe($product)) {
				$csafproduct{"product_identification_helper"} = {
					"cpe"   => &SMASHData::get_cpe($product)
				};
			} else {
				if (($product !~ /^Image/) && ($product !~ /^Container/)) {
					warn "no cpe found for $product in $cve\n";
				}
			}
			my %csafproductfamily = (
				"category"      => "product_name",
				"name"          => $product,
				"product"       => \%csafproduct,
			);
			push @csafproductfamilies, \%csafproductfamily;
		}
	}

	my @csafchostproductfamilies = ();

	# Find and declared affectedness for all chost images...

	my @allimages = keys %PInt::image2cves;
	my @cveallimages = keys %{$PInt::cve2images{$cve}};
	my %chostaffected = ();

	if (@cveallimages) {
		foreach my $image (keys %PInt::images) {
			my %ximage = %{$PInt::images{$image}};

			print STDERR "regexp of $image: " . $ximage{"regexp"} . "\n" if -t STDERR;
			my @cvechostimages = grep(/$ximage{"regexp"}/, @cveallimages);
			my @chostimages = grep(/$ximage{"regexp"}/, @allimages);

			my $fixed = 0;
			foreach my $chost (sort @chostimages) {
				if ($fixed) {
					$chostaffected{$chost} = "Fixed";
					print STDERR "$chost -> Fixed\n" if -t STDERR;
					next;
				}
				if (grep(/$chost/,@cvechostimages)) {
					print STDERR "$chost -> FIRST Fixed\n" if -t STDERR;
					$chostaffected{$chost} = "Fixed";	# First Fixed
					$fixed = 1;
					next;
				}
				print STDERR "$chost -> Known Affected\n" if -t STDERR;
				$chostaffected{$chost} = "Known Affected";
			}
		}

		# print STDERR "All chost states:\n" . Dumper(\%chostaffected) . "\n" if -t STDERR;

		###### new chost, lets try this tree:
		# CHOST Family
			# CHOST image name (SLES 15 SP2 CHOST for AWS)
				# CHOST image id

		if (%chostaffected) {
			foreach my $name (sort keys %chostnames) {
				if (grep(/$name/,keys %chostaffected)) {
					foreach my $chost (sort grep(/$name/,keys %chostaffected)) {
						my $cname = $chost;
						my $pname = $chost;
						my $sp = "";

						$pname =~ s/\/.*//;
						$cname =~ s/.*\///;

						if ($cname =~ /(sp\d)/) {
							$sp = "$1:";
						}

						my %csafproduct = (
							"name"          => $chostnames{$name},
							"product_id"    => "chost:$pname/$cname",
							"product_identification_helper" => {
								"cpe"   => "cpe:/o:suse:sles:15:$sp" . "chost-$pname:$cname"
							},
						);

						my %csafproductfamily = (
							"category"      => "product_name",
							"name"          => $chostnames{$name},
							"product"       => \%csafproduct,
						);
						push @csafchostproductfamilies, \%csafproductfamily;

						# $writer->dataElement(
						# 	"FullProductName",
						# 	"$chostnames{$name} $cname",
						# 	"ProductID" => "chost:$pname/$cname",
						# 	"CPE" => "cpe:/o:suse:sles:15:$sp" . "chost-$pname:$cname",
						#);
					}
				}
			}
		}
	}


	my @csafpackageversions = ();

	# Second lists all package-versions referenced...
	foreach my $pkgver (sort keys %packages) {
		my %csafpackage = (
			"name" => $pkgver,
			"product_id" => $pkgver,
		);
		if (package2cpe($pkgver)) {
			$csafpackage{"product_identification_helper"} = {
				"cpe" => package2cpe($pkgver)
			}
		}
		my %csafpackagefamily = (
			"category"	=> "product_version",
			"name"		=> $pkgver,
			"product"	=> \%csafpackage,
		);
		push @csafpackageversions,\%csafpackagefamily;
	}

	# FIXME: needs perhaps the architecture hierarchy....
	push @csafproductfamilies,@csafpackageversions;

	my @csafproductversions = ();

	if (@csafproductfamilies) {
		# might not be empty?
		my %csaffamilybranch = (
			"category"      => "product_family",
			"name"          => "SUSE Linux Enterprise",
			"branches"      => \@csafproductfamilies,
		);

		@csafproductversions = (
			\%csaffamilybranch,
		);
	}

	# could be empty
	if (@csafchostproductfamilies) {
		my %csafchostfamilybranch = (
		"category"      => "product_family",
			"name"          => "SUSE Linux Enterprise CHOST images",
			"branches"      => \@csafchostproductfamilies,
		);
		push @csafproductversions,\%csafchostfamilybranch;
	}

	my @csafproductrelationships = ();

	# Now list all products / package versions.
	if (defined($UpdateInfoReader::products{$cve})) {
		my %xproducts = %{$UpdateInfoReader::products{$cve}};

		foreach my $product (sort keys %xproducts) {
			next if (UpdateInfoReader::blacklistedproduct($product));
			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 $pkgver = "$pkg-".$pkgs{$pkg};

				my %csafrelationship = (
					"category"			=> "default_component_of",
					"product_reference"		=> $pkgver,
					"relates_to_product_reference"	=> $product,
					"full_product_name"     	=> {
							"name"                  => "$pkgver as component of $product",
							"product_id"            => "$product:$pkgver",
					},
				);
				push @csafproductrelationships,\%csafrelationship;
			}

		}
	}
	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 $pkgver = "$pkg-".$pkgs{$pkg};

				my %csafrelationship = (
					"category"			=> "default_component_of",
					"product_reference"		=> $pkgver,
					"relates_to_product_reference"	=> $product,
					"full_product_name"     	=> {
							"name"                  => "$pkgver as component of $product",
							"product_id"            => "$product:$pkgver",
					},
				);
				push @csafproductrelationships,\%csafrelationship;
			}
		}
	}

	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 "Not affected")     ||
				($pkgs{$pkg} eq "Affected")         ||
				($pkgs{$pkg} eq "Already fixed")
			) {
				print STDERR "$cve - $product - $pkg - $pkgs{$pkg}\n" if -t STDERR;

				my $havepkg = 0;
				foreach my $binpkg (sort keys %{$UpdateInfoReader::product2packages{$product}->{$pkg}}) {
					my %csafrelationship = (
						"category"			=> "default_component_of",
						"product_reference"		=> $binpkg,
						"relates_to_product_reference"	=> $product,
						"full_product_name"     	=> {
								"name"                  => "$binpkg as component of $product",
								"product_id"            => "$product:$binpkg",
						},
					);
					push @csafproductrelationships,\%csafrelationship;

					$havepkg = 1 if ($binpkg eq $pkg);
				}
				if (!$havepkg) {
					my %csafrelationship = (
						"category"			=> "default_component_of",
						"product_reference"		=> $pkg,
						"relates_to_product_reference"	=> $product,
						"full_product_name"     	=> {
								"name"                  => "$pkg as component of $product",
								"product_id"            => "$product:$pkg",
						},
					);
					push @csafproductrelationships,\%csafrelationship;
				}
			}
		}
	}

	# Emit relationships

	my %productstatus = ();	# { status -> { product:1 } }

	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 $pkgver = "$pkg-".$pkgs{$pkg};

				$productstatus{"recommended"}->{"$product:$pkgver"} = 1;
			}
		}
	}

	if (defined($UpdateInfoReader::productsinqa{$cve})) {
		my %xproducts = %{$UpdateInfoReader::productsinqa{$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 $pkgver = "$pkg-".$pkgs{$pkg};

				$productstatus{"first fixed"}->{"$product:$pkgver"} = 1;
			}
		}
	}

# list not affected ones
	my @pkgs = ();
	foreach my $product (sort keys %smashprods) {
		next if (UpdateInfoReader::blacklistedproduct($product));

		my %pkgs = %{$smashprods{$product}};
		foreach my $pkg (keys %pkgs) {

			if (	($pkgs{$pkg} eq "Not affected")     ||
					($pkgs{$pkg} eq "Already fixed")
			   ) {
				print STDERR "$cve - $product - $pkg - $pkgs{$pkg}\n" if -t STDERR;
				my $havepkg = 0;
				foreach my $binpkg (sort keys %{$UpdateInfoReader::product2packages{$product}->{$pkg}}) {
					$productstatus{"known_not_affected"}->{"$product:$binpkg"} = 1;
					$havepkg = 1 if ($binpkg eq $pkg);
				}
				if (!$havepkg) {
					$productstatus{"known_not_affected"}->{"$product:$pkg"} = 1;
				}
			}
		}
	}
# list affected ones
	@pkgs = ();
	foreach my $product (sort keys %smashprods) {
		next if (UpdateInfoReader::blacklistedproduct($product));

		my %pkgs = %{$smashprods{$product}};
		foreach my $pkg (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}}) {
					$productstatus{"known affected"}->{"$product:$binpkg"} = 1;
					$havepkg = 1 if ($binpkg eq $pkg);
				}
				if (!$havepkg) {
					$productstatus{"known_affected"}->{"$product:$pkg"} = 1;
				}
			}
		}
	}

	if (%chostaffected) {
		my @fixed = ();
		my @affected = ();
		foreach my $name (sort keys %chostnames) {
			foreach my $chost (sort grep(/$name/,keys %chostaffected)) {
				my $cname = $chost;
				my $pname = $chost;

				$pname =~ s/\/.*//;
				$cname =~ s/.*\///;

				if ($chostaffected{$chost} eq "Fixed") {
					push @fixed, "chost:$pname/$cname";
				} else {
					push @affected, "chost:$pname/$cname";
				}
			}
		}
		if (@fixed) {
			foreach my $chost (sort @fixed) {
				$productstatus{"recommended"}->{$chost} = 1;
			}
		}
		if (@affected) {
			foreach my $chost (sort @affected) {
				$productstatus{"affected"}->{$chost} = 1;
			}
		}
	}
	my %csafproducttree = ();
	# tree might be empty. if we have no branches, we also have no relationships
	if (@csafproductversions) {
		%csafproducttree = (
			"branches"      => [            # toplevel is just just the 1 vendor entry, rest is generated above
				{
					"branches"      => \@csafproductversions,
					"category"      => "vendor",
					"name"          => "SUSE",
				},
			],
			"relationships" => \@csafproductrelationships,
		);
	}

	my %csafproductstatus = ();	# { status -> ( product ) }

	# print STDERR "productstatus: " . Dumper(\%productstatus);

	foreach my $status (keys %productstatus) {
		my @products = sort keys %{$productstatus{$status}};
		$csafproductstatus{$status} = \@products;
	}

	my @csafproductsfixed = ();
	if (defined($csafproductstatus{"recommended"})) {
		@csafproductsfixed = sort @{$csafproductstatus{"recommended"}};
	}

	my @csafvulreferences = (
			{
			"category"      => "external",
			"summary"       => $cve,
			"url"           => "https://www.suse.com/security/cve/$cve",
			},
			{
			"category"      => "external",
			"summary"       => "SUSE Security Ratings",
			"url"           => "https://www.suse.com/support/security/rating/",
			},
			);

	if (defined($CanDBReader::bugzillas{$cve})) {
		my %bugs = map { $_ => 1 } split (/,/,$CanDBReader::bugzillas{$cve});
		foreach my $bug (sort keys %bugs) {
			my %csafbugreference = (
					"category"      => "external",
					"summary"       => "SUSE Bug $bug for $cve",
					"url"           => "https://bugzilla.suse.com/$bug",
					);
			push @csafvulreferences,\%csafbugreference;
		}
	}
	if (defined($CanDBReader::advisoryids{$cve})) {
		my %xx = map { $_ => 1 } split(/,/,$CanDBReader::advisoryids{$cve});
		foreach my $said (sort keys %xx) {
			if (defined($CanDBReader::advisoryid2url{$said})) {
				my %csafreference = (
						"category"	=> "external",
						"summary"	=> "Advisory link for $said",
						"url"		=> $CanDBReader::advisoryid2url{$said},
						);
				push @csafvulreferences,\%csafreference;
			}
		}
	}

	my $severity = $SMASHData::severity{$cve};
	if (!defined($severity)) {
		print STDERR "$cve - no severity?\n" if (-t STDERR);
		$severity = "moderate";
	}

	my @csafvulscores = ();

	my $basescore;
	my $basevector;

	my $cvss3 = &SMASHData::get_cvssv3_issue($cve);
	# only if we have a score, and we have fixed products to list.
	if (defined($cvss3) && @csafproductsfixed) {
		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 $basever = $1;

			my %csafvulscore = (
					"products"      => \@csafproductsfixed,         # FIXME check if we really need it here
					"cvss_v3"       => {
						"version"       => "$basever",
						"vectorString"  => $basevector,
						"baseSeverity"  => map_cvss_severity($basescore),
						"baseScore"     => $basescore,
					},
					);
			push @csafvulscores,\%csafvulscore;
		}
	}

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

	if (defined($oldcsaf) && defined($oldcsaf->{'document'}->{'tracking'}->{'initial_release_date'})) {
		$initial_release_date =  $oldcsaf->{'document'}->{'tracking'}->{'initial_release_date'};
	}

	my $threatdetected = $SMASHData::timestamps{$cve}->{"created"};
	if (!defined($threatdetected)) {
		warn "SKIPPING: no created stamp for $cve\n";
		next;
	}
	my $tddt = DateTime->from_epoch(epoch => $threatdetected);

	my %csafvulnerability = (
		"cve"   => $cve,
# FIXME "cwe" ... not yet handled by SUSE

		"ids"           => [
		{
		"system_name" => "SUSE CVE Page",
		"text" => "https://www.suse.com/security/cve/$cve",
		}
		],
		"notes"         => [
		{
		"category"      => "general",
		"title"         => "CVE description",
		"text"          => get_description($cve) || "unknown",
		}
		],
		"references"    => \@csafvulreferences,
		"threats"       => [
		{
			"category"      => "impact",
			"date"          => $tddt->iso8601()."Z",
			"details"       => $severity
		}
		],
		"product_status"=> \%csafproductstatus,
# discovery_date ... check?
# release_date          TODO
		"title"         => $cve,
	);

	if (@csafproductsfixed) {
		my @csafremediations = (
		{
			"category"      => "vendor_fix",
			"details"       => "To install this SUSE Security Update use the SUSE recommended installation methods like YaST online_update or \"zypper patch\".\n",
			"product_ids"   => \@csafproductsfixed
		},
		);
		$csafvulnerability{'remediations'} = \@csafremediations;
	}

	if (@csafvulscores) {   # empty score array is not allowed.
		$csafvulnerability{"scores"} = \@csafvulscores;
	}

	my @csafnotes = (
		{       "category"      => "summary",
			"title"         => "Title",
			"text"          => "SUSE $cve",
		},
		{       "category"      => "description",
			"title"         => "Description of the CVE",
			"text"          => get_description($cve) || "unknown",
		},
		{       "category"      => "legal_disclaimer",
			"title"         => "Terms of use",
			"text"          => "CSAF 2.0 data is provided by SUSE under the Creative Commons License 4.0 with Attribution (CC-BY-4.0)."
		}
	);
	if ($CANDBReader::note{$cve}) {
		my %note = (
				"category" 	=> "note",
				"title" 	=> "Note from SUSE Securit",
				"text" 		=> $CANDBReader::note{$cve}
			   );
		push @csafnotes,\%note;
	}

	my %csafdocument = (
		"category"              => "csaf_vex",
		"csaf_version"          => "2.0",
		"title"                 => "SUSE CVE $cve",
		"lang"                  => "en",
		"aggregate_severity"    => {
		"namespace"     => "https://www.suse.com/support/security/rating/",
			"text"          => $SMASHData::severity{$cve}
		},
		"distribution"          => {
			"text"  => "Copyright $year SUSE LLC. All rights reserved.",
			"tlp"   => {
				"label"         => "WHITE",
				"url"           => "https://www.first.org/tlp/"
			},
		},
		"notes"                 => \@csafnotes,
		"references"            => \@csafvulreferences,
		"publisher"             => {
			"category"              => "vendor",
			"contact_details"       => "https://www.suse.com/support/security/contact/",
			"name"                  => "SUSE Product Security Team",
			"namespace"             => "https://www.suse.com/",
		},
		"tracking"              => {
			# "current_release_date"  => $dt->iso8601()."Z",	# set later
			"id"                    => $cve,
			"initial_release_date"  => $initial_release_date,
			"generator"             => {
				"date" => $initial_release_date,
				"engine" => {
					"name"                  => "cve-database.git:bin/generate-csaf-vex.pl",
					"version"               => "1",
				}
			},
			"status"                => "interim",
			"version"               => "1",
		},
	);

	# This is really the toplevel CSAF
	my %csaf = (
		'document'              => \%csafdocument,
	);

	if (%csafproducttree) {
		$csaf{'product_tree'} 		= \%csafproducttree;
		$csaf{'vulnerabilities'}	= [ \%csafvulnerability ];
	}

	my @csafrevisionhistory = ();
	my $highestrev = 1;
	# Copy old releasedate, reset it if we have a new revision later on.
	if (defined($oldcsaf) && defined($oldcsaf->{'document'}->{'tracking'})) {
		$csafdocument{'tracking'}->{'current_release_date'} =  $oldcsaf->{'document'}->{'tracking'}->{'current_release_date'};
		@csafrevisionhistory = @{$oldcsaf->{'document'}->{'tracking'}->{'revision_history'}};
		$highestrev = $oldcsaf->{'document'}->{'tracking'}->{'version'};
		$csafdocument{'tracking'}->{'revision_history'} = \@csafrevisionhistory;
		$csafdocument{'tracking'}->{'version'} = "$highestrev";
	}
	foreach my $rev (@csafrevisionhistory) {
		if ($rev->{number} > $highestrev) {
			$highestrev = $rev->{number};
		}
	}

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

	if (system("cmp $fn $fn.new")) {
		print "difference detected:\n";
		system("jq . <$fn >$fn.formatted ; jq . <$fn.new >$fn.new.formatted ; diff -uN $fn.formatted $fn.new.formatted");
		unlink("$fn.new.formatted");
		unlink("$fn.formatted");

	# if (!csaf_trees_same($oldcsaf,\%csaf)) {
		$highestrev++;

		my %csafrev = (
			"date"                  =>      $dt->iso8601()."Z",
			"number"                =>      "$highestrev",
			"summary"               =>      "Current version",
		);
		push @csafrevisionhistory,\%csafrev;
		$csaf{'document'}->{'tracking'}->{'current_release_date'} = $dt->iso8601()."Z";
	}

	$csaf{'document'}->{'tracking'}->{'revision_history'} = \@csafrevisionhistory;
	$csaf{'document'}->{'tracking'}->{'version'} = "$highestrev";

	$lastmodified{"\L$cve.json"} = $csafdocument{'tracking'}->{'current_release_date'};

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

	my $newcsafcontent;
	my $oldcsafcontent = "<empty>";
	if (open(CSAF,"<$fn.new")) {
		binmode(CSAF,":utf8");
		$newcsafcontent = join("",<CSAF>);
		close(CSAF);
	}
	if (open(CSAF,"<$fn")) {
		binmode(CSAF,":utf8");
		$oldcsafcontent = join("",<CSAF>);
		close(CSAF);
	}
	if ($oldcsafcontent ne $newcsafcontent) {
		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");
			sign($fn);
			hash($fn);
		} else {
			print STDERR "diff for $fn not detected by reader?\n";
			unlink("$fn.new");
		}
	} else {
		unlink("$fn.new");
	}
}
chdir("/mounts/mirror/SuSE/ftp.suse.com/pub/projects/security/");
system("tar cjf csaf-vex.tar.bz2 csaf-vex/");

open(INDEXTXT,">csaf-vex/index.txt")||die "csaf-vex/index.txt: $!";
print INDEXTXT join("\n",sort keys %lastmodified) . "\n";
close(INDEXTXT);
open(CHANGESCSV,">csaf-vex/changes.csv")||die "csaf-vex/changes.csv: $!";
foreach my $entry (sort keys %lastmodified) {
	print CHANGESCSV "\"$entry\",\"$lastmodified{$entry}\"\n";
}
close(CHANGESCSV);

#sign("csaf-vex.tar.bz2");

print "SUCCESS\n";
