From 523a94732daee5a4c3323dfac551bb659c4c718f Mon Sep 17 00:00:00 2001 From: crozzy Date: Fri, 8 Sep 2023 10:03:44 -0700 Subject: [PATCH] enricher: add RHCC enricher This change introduces a new enricher that reports where rhcc packages exist (if at all), it allows callers to discount vulnerabilities / packages that come from the same layers. This approach helps to keep the index report unchanged and therefore state is less of an issue, it also builds on existing machinary. Signed-off-by: crozzy --- enricher/rhcc/rhcc.go | 49 ++++++++ enricher/rhcc/rhcc_test.go | 226 +++++++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 enricher/rhcc/rhcc.go create mode 100644 enricher/rhcc/rhcc_test.go diff --git a/enricher/rhcc/rhcc.go b/enricher/rhcc/rhcc.go new file mode 100644 index 000000000..5f55dff3d --- /dev/null +++ b/enricher/rhcc/rhcc.go @@ -0,0 +1,49 @@ +package rhcc + +import ( + "context" + "encoding/json" + + "github.com/quay/claircore" + "github.com/quay/claircore/libvuln/driver" + "github.com/quay/claircore/rhel/rhcc" +) + +type Enricher struct{} + +var ( + _ driver.Enricher = (*Enricher)(nil) +) + +const ( + // Type is the type of data returned from the Enricher's Enrich method. + Type = `message/vnd.clair.map.layer; enricher=clair.rhcc` +) + +func (e *Enricher) Name() string { return "rhcc" } + +func (e *Enricher) Enrich(ctx context.Context, g driver.EnrichmentGetter, r *claircore.VulnerabilityReport) (string, []json.RawMessage, error) { + rhccPkgs := make(map[string]string) + for id, p := range r.Packages { + if envs, ok := r.Environments[id]; ok && p.Kind == claircore.BINARY { + for _, e := range envs { + for _, repoID := range e.RepositoryIDs { + repo := r.Repositories[repoID] + if repo.Name == rhcc.GoldRepo.Name { + rhccPkgs[id] = e.IntroducedIn.String() + break + } + } + } + } + } + + if len(rhccPkgs) == 0 { + return Type, nil, nil + } + b, err := json.Marshal(rhccPkgs) + if err != nil { + return Type, nil, err + } + return Type, []json.RawMessage{b}, nil +} diff --git a/enricher/rhcc/rhcc_test.go b/enricher/rhcc/rhcc_test.go new file mode 100644 index 000000000..38d80b46b --- /dev/null +++ b/enricher/rhcc/rhcc_test.go @@ -0,0 +1,226 @@ +package rhcc + +import ( + "context" + "crypto/sha256" + "encoding/json" + "io" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/quay/zlog" + + "github.com/quay/claircore" + "github.com/quay/claircore/libvuln/driver" +) + +func Digest(name string) claircore.Digest { + h := sha256.New() + if _, err := io.WriteString(h, name); err != nil { + panic(err) + } + d, err := claircore.NewDigest("sha256", h.Sum(nil)) + if err != nil { + panic(err) + } + return d +} + +func TestEnrich(t *testing.T) { + t.Parallel() + ctx := zlog.Test(context.Background(), t) + firstLayerHash := Digest("first layer") + secondLayerHash := Digest("second layer") + tests := []struct { + name string + vr *claircore.VulnerabilityReport + layers []*claircore.Layer + want map[string]string + }{ + { + name: "one package that is a layer one that isn't", + vr: &claircore.VulnerabilityReport{ + Packages: map[string]*claircore.Package{ + "1": { + Name: "some-rh-package-slash-image", + Version: "v1.0.0", + Kind: claircore.BINARY, + }, + "2": { + Name: "grafana", + Version: "v4.7.0", + Kind: claircore.BINARY, + }, + }, + Environments: map[string][]*claircore.Environment{ + "1": {{IntroducedIn: firstLayerHash, RepositoryIDs: []string{"1"}}}, + "2": {{IntroducedIn: secondLayerHash}}, + }, + Repositories: map[string]*claircore.Repository{ + "1": { + ID: "1", + Name: "Red Hat Container Catalog", + URI: "https://catalog.redhat.com/software/containers/explore", + }, + }, + }, + layers: []*claircore.Layer{ + {Hash: firstLayerHash}, + {Hash: secondLayerHash}, + }, + want: map[string]string{"1": firstLayerHash.String()}, + }, + { + name: "two packages, neither are layers", + vr: &claircore.VulnerabilityReport{ + Packages: map[string]*claircore.Package{ + "1": { + Name: "cool app", + Version: "v1.0.0", + Kind: claircore.BINARY, + }, + "2": { + Name: "grafana", + Version: "v4.7.0", + Kind: claircore.BINARY, + }, + }, + Environments: map[string][]*claircore.Environment{ + "1": {{IntroducedIn: firstLayerHash}}, + "2": {{IntroducedIn: firstLayerHash}}, + }, + }, + layers: []*claircore.Layer{ + {Hash: firstLayerHash}, + {Hash: secondLayerHash}, + }, + want: nil, + }, + { + name: "multiple rhcc packages in different layers", + vr: &claircore.VulnerabilityReport{ + Packages: map[string]*claircore.Package{ + "1": { + Name: "some-rh-package-slash-image", + RepositoryHint: "rhcc", + Version: "v1.0.0", + Kind: claircore.BINARY, + }, + "2": { + Name: "some-other-rh-package-slash-image", + RepositoryHint: "rhcc", + Version: "v1.0.0", + Kind: claircore.BINARY, + }, + "3": { + Name: "grafana", + Version: "v4.7.0", + Kind: claircore.BINARY, + }, + }, + Environments: map[string][]*claircore.Environment{ + "1": {{IntroducedIn: firstLayerHash, RepositoryIDs: []string{"1"}}}, + "2": {{IntroducedIn: secondLayerHash, RepositoryIDs: []string{"1"}}}, + "3": {{IntroducedIn: firstLayerHash}}, + }, + Repositories: map[string]*claircore.Repository{ + "1": { + ID: "1", + Name: "Red Hat Container Catalog", + URI: "https://catalog.redhat.com/software/containers/explore", + }, + }, + }, + layers: []*claircore.Layer{ + {Hash: firstLayerHash}, + {Hash: secondLayerHash}, + }, + want: map[string]string{"1": firstLayerHash.String(), "2": secondLayerHash.String()}, + }, + { + name: "multiple rhcc packages in same layers (source and binary)", + vr: &claircore.VulnerabilityReport{ + Packages: map[string]*claircore.Package{ + "1": { + Name: "some-rh-package-slash-image-binary", + Version: "v1.0.0", + Kind: claircore.BINARY, + Source: &claircore.Package{ + Name: "some-rh-package-slash-image-source", + Version: "v1.0.0", + Kind: claircore.SOURCE, + }, + }, + "2": { + Name: "some-rh-package-slash-image-source", + Version: "v1.0.0", + Kind: claircore.SOURCE, + }, + "3": { + Name: "grafana", + Version: "v4.7.0", + Kind: claircore.BINARY, + }, + }, + Environments: map[string][]*claircore.Environment{ + "1": {{IntroducedIn: firstLayerHash, RepositoryIDs: []string{"1"}}}, + "2": {{IntroducedIn: firstLayerHash, RepositoryIDs: []string{"1"}}}, + "3": {{IntroducedIn: secondLayerHash}}, + }, + Repositories: map[string]*claircore.Repository{ + "1": { + ID: "1", + Name: "Red Hat Container Catalog", + URI: "https://catalog.redhat.com/software/containers/explore", + }, + }, + }, + layers: []*claircore.Layer{ + {Hash: firstLayerHash}, + {Hash: secondLayerHash}, + }, + want: map[string]string{"1": firstLayerHash.String()}, + }, + } + + e := &Enricher{} + nog := &noopGetter{} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tp, data, err := e.Enrich(ctx, nog, tc.vr) + if err != nil { + t.Fatal(err) + } + if tc.want == nil { + if data != nil { + t.Fatal("unexpected data") + } + return + } + if tp != "message/vnd.clair.map.layer; enricher=clair.rhcc" { + t.Fatal("wrong type") + } + got := make(map[string]string) + if err := json.Unmarshal(data[0], &got); err != nil { + t.Error(err) + } + if !cmp.Equal(got, tc.want) { + t.Error(cmp.Diff(got, tc.want)) + } + }) + + } +} + +func TestName(t *testing.T) { + e := &Enricher{} + if e.Name() != "rhcc" { + t.Fatal("name should be rhcc") + } +} + +type noopGetter struct{} + +func (f *noopGetter) GetEnrichment(ctx context.Context, tags []string) ([]driver.EnrichmentRecord, error) { + return nil, nil +}