From 9073a8a282a4efd149acd88aeb23b3d1004cf1c7 Mon Sep 17 00:00:00 2001 From: Juan Ariza Toledano Date: Fri, 1 Dec 2023 15:31:25 +0100 Subject: [PATCH] feat: Add function to find product identification helpers inspecting the tree (#505) * feat: Add function to find product identification helpers inspecting the tree Signed-off-by: juan131 * fix: simplify unit tests Signed-off-by: juan131 * fix: also iterate over relationships Signed-off-by: juan131 * fix: adapt example to use new library function Signed-off-by: juan131 * Separate collecting and visiting of the product id helpers. --------- Signed-off-by: juan131 Co-authored-by: Sascha L. Teichmann --- csaf/util.go | 61 +++++++++++ csaf/util_test.go | 182 ++++++++++++++++++++++++++++++++ examples/purls_searcher/main.go | 111 +++---------------- 3 files changed, 258 insertions(+), 96 deletions(-) create mode 100644 csaf/util_test.go diff --git a/csaf/util.go b/csaf/util.go index f192f09..f8e34be 100644 --- a/csaf/util.go +++ b/csaf/util.go @@ -36,3 +36,64 @@ func ExtractProviderURL(r io.Reader, all bool) ([]string, error) { } return urls, nil } + +// CollectProductIdentificationHelpers returns a slice of all ProductIdentificationHelper +// for a given ProductID. +func (pt *ProductTree) CollectProductIdentificationHelpers(id ProductID) []*ProductIdentificationHelper { + var helpers []*ProductIdentificationHelper + pt.FindProductIdentificationHelpers( + id, func(helper *ProductIdentificationHelper) { + helpers = append(helpers, helper) + }) + return helpers +} + +// FindProductIdentificationHelpers calls visit on all ProductIdentificationHelper +// for a given ProductID by iterating over all full product names and branches +// recursively available in the ProductTree. +func (pt *ProductTree) FindProductIdentificationHelpers( + id ProductID, + visit func(*ProductIdentificationHelper), +) { + // Iterate over all full product names + if fpns := pt.FullProductNames; fpns != nil { + for _, fpn := range *fpns { + if fpn != nil && + fpn.ProductID != nil && *fpn.ProductID == id && + fpn.ProductIdentificationHelper != nil { + visit(fpn.ProductIdentificationHelper) + } + } + } + + // Iterate over branches recursively + var recBranch func(b *Branch) + recBranch = func(b *Branch) { + if b == nil { + return + } + if fpn := b.Product; fpn != nil && + fpn.ProductID != nil && *fpn.ProductID == id && + fpn.ProductIdentificationHelper != nil { + visit(fpn.ProductIdentificationHelper) + } + for _, c := range b.Branches { + recBranch(c) + } + } + for _, b := range pt.Branches { + recBranch(b) + } + + // Iterate over relationships + if rels := pt.RelationShips; rels != nil { + for _, rel := range *rels { + if rel != nil { + if fpn := rel.FullProductName; fpn != nil && fpn.ProductID != nil && + *fpn.ProductID == id && fpn.ProductIdentificationHelper != nil { + visit(fpn.ProductIdentificationHelper) + } + } + } + } +} diff --git a/csaf/util_test.go b/csaf/util_test.go new file mode 100644 index 0000000..0d5ff49 --- /dev/null +++ b/csaf/util_test.go @@ -0,0 +1,182 @@ +// This file is Free Software under the MIT License +// without warranty, see README.md and LICENSES/MIT.txt for details. +// +// SPDX-License-Identifier: MIT +// +// SPDX-FileCopyrightText: 2022 German Federal Office for Information Security (BSI) +// Software-Engineering: 2022 Intevation GmbH + +package csaf + +import ( + "reflect" + "testing" +) + +func TestProductTree_FindProductIdentificationHelpers(t *testing.T) { + type fields struct { + Branches Branches + FullProductNames *FullProductNames + RelationShips *Relationships + } + type args struct { + id ProductID + } + tests := []struct { + name string + fields fields + args args + want []*ProductIdentificationHelper + }{ + { + name: "empty product tree", + args: args{ + id: "CSAFPID-0001", + }, + want: nil, + }, + { + name: "product tree with matching full product names", + fields: fields{ + FullProductNames: &FullProductNames{{ + ProductID: &[]ProductID{"CSAFPID-0001"}[0], + ProductIdentificationHelper: &ProductIdentificationHelper{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta:*:*:*:*:*:*"}[0], + }, + }}, + }, + args: args{ + id: "CSAFPID-0001", + }, + want: []*ProductIdentificationHelper{{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta:*:*:*:*:*:*"}[0], + }}, + }, + { + name: "product tree with no matching full product names", + fields: fields{ + FullProductNames: &FullProductNames{{ + ProductID: &[]ProductID{"CSAFPID-0001"}[0], + ProductIdentificationHelper: &ProductIdentificationHelper{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta:*:*:*:*:*:*"}[0], + }, + }}, + }, + args: args{ + id: "CSAFPID-0002", + }, + want: nil, + }, + { + name: "product tree with matching branches", + fields: fields{ + Branches: Branches{{ + Name: &[]string{"beta"}[0], + Product: &FullProductName{ + ProductID: &[]ProductID{"CSAFPID-0001"}[0], + ProductIdentificationHelper: &ProductIdentificationHelper{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta:*:*:*:*:*:*"}[0], + }, + }, + Branches: Branches{{ + Name: &[]string{"beta-2"}[0], + Product: &FullProductName{ + ProductID: &[]ProductID{"CSAFPID-0001"}[0], + ProductIdentificationHelper: &ProductIdentificationHelper{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta-2:*:*:*:*:*:*"}[0], + }, + }, + }}, + }}, + }, + args: args{ + id: "CSAFPID-0001", + }, + want: []*ProductIdentificationHelper{{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta:*:*:*:*:*:*"}[0], + }, { + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta-2:*:*:*:*:*:*"}[0], + }}, + }, + { + name: "product tree with no matching branches", + fields: fields{ + Branches: Branches{{ + Name: &[]string{"beta"}[0], + Product: &FullProductName{ + ProductID: &[]ProductID{"CSAFPID-0001"}[0], + ProductIdentificationHelper: &ProductIdentificationHelper{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta:*:*:*:*:*:*"}[0], + }, + }, + Branches: Branches{{ + Name: &[]string{"beta-2"}[0], + Product: &FullProductName{ + ProductID: &[]ProductID{"CSAFPID-0001"}[0], + ProductIdentificationHelper: &ProductIdentificationHelper{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta-2:*:*:*:*:*:*"}[0], + }, + }, + }}, + }}, + }, + args: args{ + id: "CSAFPID-0002", + }, + want: nil, + }, + { + name: "product tree with matching relationships", + fields: fields{ + RelationShips: &Relationships{{ + FullProductName: &FullProductName{ + ProductID: &[]ProductID{"CSAFPID-0001"}[0], + ProductIdentificationHelper: &ProductIdentificationHelper{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta:*:*:*:*:*:*"}[0], + }, + }, + }}, + }, + args: args{ + id: "CSAFPID-0001", + }, + want: []*ProductIdentificationHelper{{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta:*:*:*:*:*:*"}[0], + }}, + }, + { + name: "product tree with no matching relationships", + fields: fields{ + RelationShips: &Relationships{{ + FullProductName: &FullProductName{ + ProductID: &[]ProductID{"CSAFPID-0001"}[0], + ProductIdentificationHelper: &ProductIdentificationHelper{ + CPE: &[]CPE{"cpe:2.3:a:microsoft:internet_explorer:1.0.0:beta:*:*:*:*:*:*"}[0], + }, + }, + }}, + }, + args: args{ + id: "CSAFPID-0002", + }, + want: nil, + }, + } + + t.Parallel() + for _, testToRun := range tests { + test := testToRun + t.Run(test.name, func(tt *testing.T) { + tt.Parallel() + pt := &ProductTree{ + Branches: test.fields.Branches, + FullProductNames: test.fields.FullProductNames, + RelationShips: test.fields.RelationShips, + } + if got := pt.CollectProductIdentificationHelpers(test.args.id); !reflect.DeepEqual(got, test.want) { + tt.Errorf("ProductTree.FindProductIdentificationHelpers() = %v, want %v", + got, test.want) + } + }) + } +} diff --git a/examples/purls_searcher/main.go b/examples/purls_searcher/main.go index a91470b..c1ec3e1 100644 --- a/examples/purls_searcher/main.go +++ b/examples/purls_searcher/main.go @@ -9,9 +9,8 @@ import ( "os" "strings" - "golang.org/x/exp/slices" - "github.com/csaf-poc/csaf_distribution/v3/csaf" + "github.com/csaf-poc/csaf_distribution/v3/util" ) func main() { @@ -35,106 +34,26 @@ func main() { // run prints PURLs belonging to the given Product IDs. func run(files []string, ids string) error { - - uf := newURLFinder(strings.Split(ids, ",")) - for _, file := range files { adv, err := csaf.LoadAdvisory(file) if err != nil { return fmt.Errorf("loading %q failed: %w", file, err) } - uf.findURLs(adv) - uf.dumpURLs() - uf.clear() + + for _, id := range strings.Split(ids, ",") { + already := util.Set[csaf.PURL]{} + i := 0 + adv.ProductTree.FindProductIdentificationHelpers( + csaf.ProductID(id), + func(h *csaf.ProductIdentificationHelper) { + if h.PURL != nil && !already.Contains(*h.PURL) { + already.Add(*h.PURL) + i++ + fmt.Printf("%d. %s\n", i, *h.PURL) + } + }) + } } return nil } - -// urlFinder helps to find the URLs of a set of product ids in advisories. -type urlFinder struct { - ids []csaf.ProductID - urls [][]csaf.PURL -} - -// newURLFinder creates a new urlFinder for given ids. -func newURLFinder(ids []string) *urlFinder { - uf := &urlFinder{ - ids: make([]csaf.ProductID, len(ids)), - urls: make([][]csaf.PURL, len(ids)), - } - for i := range uf.ids { - uf.ids[i] = csaf.ProductID(ids[i]) - } - return uf -} - -// clear resets the url finder after a run on an advisory. -func (uf *urlFinder) clear() { - for i := range uf.urls { - uf.urls[i] = uf.urls[i][:0] - } -} - -// dumpURLs dumps the found URLs to stdout. -func (uf *urlFinder) dumpURLs() { - for i, urls := range uf.urls { - if len(urls) == 0 { - continue - } - fmt.Printf("Found URLs for %s:\n", uf.ids[i]) - for j, url := range urls { - fmt.Printf("%d. %s\n", j+1, url) - } - } -} - -// findURLs find the URLs in an advisory. -func (uf *urlFinder) findURLs(adv *csaf.Advisory) { - tree := adv.ProductTree - if tree == nil { - return - } - - // If we have found it and we have a valid URL add unique. - add := func(idx int, h *csaf.ProductIdentificationHelper) { - if idx != -1 && h != nil && h.PURL != nil && - !slices.Contains(uf.urls[idx], *h.PURL) { - uf.urls[idx] = append(uf.urls[idx], *h.PURL) - } - } - - // First iterate over full product names. - if names := tree.FullProductNames; names != nil { - for _, name := range *names { - if name != nil && name.ProductID != nil { - add(slices.Index(uf.ids, *name.ProductID), name.ProductIdentificationHelper) - } - } - } - - // Second traverse the branches recursively. - var recBranch func(*csaf.Branch) - recBranch = func(b *csaf.Branch) { - if p := b.Product; p != nil && p.ProductID != nil { - add(slices.Index(uf.ids, *p.ProductID), p.ProductIdentificationHelper) - } - for _, c := range b.Branches { - recBranch(c) - } - } - for _, b := range tree.Branches { - recBranch(b) - } - - // Third iterate over relationships. - if tree.RelationShips != nil { - for _, rel := range *tree.RelationShips { - if rel != nil { - if fpn := rel.FullProductName; fpn != nil && fpn.ProductID != nil { - add(slices.Index(uf.ids, *fpn.ProductID), fpn.ProductIdentificationHelper) - } - } - } - } -}