From c98ce3189a3171f0594075fe17cc86a40760bee2 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Thu, 3 Feb 2022 23:25:49 -0500 Subject: add internal/cpedict --- internal/cpedict/cpedict.go | 46 +++++++++++ internal/cpedict/cpedict_test.go | 136 ++++++++++++++++++++++++++++++++ internal/cpedict/testdata/test-0.xml.gz | Bin 0 -> 1359 bytes 3 files changed, 182 insertions(+) create mode 100644 internal/cpedict/cpedict.go create mode 100644 internal/cpedict/cpedict_test.go create mode 100644 internal/cpedict/testdata/test-0.xml.gz diff --git a/internal/cpedict/cpedict.go b/internal/cpedict/cpedict.go new file mode 100644 index 0000000..0b5f77b --- /dev/null +++ b/internal/cpedict/cpedict.go @@ -0,0 +1,46 @@ +// CPE 2.3 dictionary parser. +// +// The official NVD CPE dictionary is available here: +// https://nvd.nist.gov/products/cpe +package cpedict + +import "time" + +// Dictionary generator information. +type Generator struct { + ProductName string `xml:"product_name"` // Product name. + ProductVersion string `xml:"product_version"` // Product version. + SchemaVersion string `xml:"schema_version"` // Schema version. + Timestamp time.Time `xml:"timestamp"` // Generation timestamp. +} + +// Dictionary item title. +type Title struct { + Lang string `xml:"lang,attr"` // language code + Text string `xml:",chardata"` // value +} + +// Dictionary item reference. +type Reference struct { + Href string `xml:"href,attr"` // Link + Text string `xml:",chardata"` // Text +} + +// CPE 2.3 item attributes. +type Cpe23Item struct { + Name string `xml:"name,attr"` // CPE 2.3 formatting string. +} + +// Dictionary item. +type Item struct { + CpeUri string `xml:"name,attr"` // CPE URI. + Cpe23Item Cpe23Item `xml:"cpe23-item"` // CPE 2.3 formatting string. + Titles []Title `xml:"title"` // Item titles. + References []Reference `xml:"references>reference"` // References. +} + +// CPE dictionary. +type Dictionary struct { + Generator Generator `xml:"generator"` // Dictionary generator. + Items []Item `xml:"cpe-item"` // Dictionary items. +} diff --git a/internal/cpedict/cpedict_test.go b/internal/cpedict/cpedict_test.go new file mode 100644 index 0000000..eca0aeb --- /dev/null +++ b/internal/cpedict/cpedict_test.go @@ -0,0 +1,136 @@ +package cpedict + +import ( + "compress/gzip" + "encoding/xml" + "os" + "testing" + "time" + "reflect" +) + +func TestDictionaryXMLUnmarshal(t *testing.T) { + // open test data + f, err := os.Open("testdata/test-0.xml.gz") + if err != nil { + t.Error(err) + return + } + defer f.Close() + + // create gzip reader + gz, err := gzip.NewReader(f) + if err != nil { + t.Error(err) + return + } + defer gz.Close() + + // create xml decoder, decode xml + d := xml.NewDecoder(gz) + var dict Dictionary + + if err := d.Decode(&dict); err != nil { + t.Error(err) + return + } + + // build expected time + var expTime time.Time + if err := expTime.UnmarshalText([]byte("2022-02-02T04:51:00.437Z")); err != nil { + t.Error(err) + return + } + + // expected generator + expGenerator := Generator { + ProductName: "National Vulnerability Database (NVD)", + ProductVersion: "4.9", + SchemaVersion: "2.3", + Timestamp: expTime, + } + + // compare generator + if !reflect.DeepEqual(dict.Generator, expGenerator) { + t.Errorf("got \"%v\", exp \"%v\"", dict.Generator, expGenerator) + return + } + + // check item count + gotNumItems := len(dict.Items) + expNumItems := 19 + if gotNumItems != expNumItems { + t.Errorf("item count: got %d, exp %d", gotNumItems, expNumItems) + return + } + + // build expected item + expItems := []Item { + // first item in the list + Item { + CpeUri: "cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~", + + Cpe23Item: Cpe23Item { + Name: "cpe:2.3:a:\\$0.99_kindle_books_project:\\$0.99_kindle_books:6:*:*:*:*:android:*:*", + }, + + Titles: []Title { + Title { + Lang: "en-US", + Text: "$0.99 Kindle Books project $0.99 Kindle Books (aka com.kindle.books.for99) for android 6.0", + }, + }, + + References: []Reference { + Reference { + Href: "https://play.google.com/store/apps/details?id=com.kindle.books.for99", + Text: "Product information", + }, + + Reference { + Href: "https://docs.google.com/spreadsheets/d/1t5GXwjw82SyunALVJb2w0zi3FoLRIkfGPc7AMjRF0r4/edit?pli=1#gid=1053404143", + Text: "Government Advisory", + }, + }, + }, + + // last item in the list + Item { + CpeUri: "cpe:/a:3com:3c16116-us:2.0", + + Cpe23Item: Cpe23Item { + Name: "cpe:2.3:a:3com:3c16116-us:2.0:*:*:*:*:*:*:*", + }, + + Titles: []Title { + Title { + Lang: "ja-JP", + Text: "スリーコム WebCache 3000 2.0", + }, + + Title { + Lang: "en-US", + Text: "3Com WebCache 3000 2.0", + }, + }, + }, + } + + // build item comparisons + compares := []struct { + name string + exp Item + got Item + } { + { "head", expItems[0], dict.Items[0] }, + { "tail", expItems[1], dict.Items[len(dict.Items) - 1] }, + } + + for _, row := range(compares) { + t.Run(row.name, func(t *testing.T) { + if !reflect.DeepEqual(row.got, row.exp) { + t.Errorf("got \"%v\", exp \"%v\"", row.got, row.exp) + } + }) + } +} diff --git a/internal/cpedict/testdata/test-0.xml.gz b/internal/cpedict/testdata/test-0.xml.gz new file mode 100644 index 0000000..110e965 Binary files /dev/null and b/internal/cpedict/testdata/test-0.xml.gz differ -- cgit v1.2.3