Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add function to find product identification helpers inspecting the tree #505

Merged
merged 8 commits into from
Dec 1, 2023
61 changes: 61 additions & 0 deletions csaf/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
}
182 changes: 182 additions & 0 deletions csaf/util_test.go
Original file line number Diff line number Diff line change
@@ -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) <https://www.bsi.bund.de>
// Software-Engineering: 2022 Intevation GmbH <https://intevation.de>

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)
}
})
}
}
109 changes: 14 additions & 95 deletions examples/purls_searcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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()
}

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

// 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)
}
}
}
}
return nil
}
Loading