From f7156cc5db7d277268be76d1a532d65e747f8618 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 | 48 ++++++++ enricher/rhcc/rhcc_test.go | 218 ++++++++++++++++++++++++++++++++++++ rhel/rhcc/coalescer_test.go | 4 +- rhel/rhcc/matcher.go | 2 +- rhel/rhcc/parser_test.go | 16 +-- rhel/rhcc/rhcc.go | 2 +- rhel/rhcc/scanner.go | 2 +- rhel/rhcc/updater.go | 2 +- 8 files changed, 280 insertions(+), 14 deletions(-) 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..0e7118e26 --- /dev/null +++ b/enricher/rhcc/rhcc.go @@ -0,0 +1,48 @@ +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) { + problematicPkgs := make(map[string]string) + for id, _ := range r.Packages { + if envs, ok := r.Environments[id]; ok { + for _, e := range envs { + for _, repoID := range e.RepositoryIDs { + repo := r.Repositories[repoID] + if repo.Name == rhcc.GoldRepo.Name { + problematicPkgs[id] = e.IntroducedIn.String() + } + } + } + } + } + + if len(problematicPkgs) == 0 { + return Type, nil, nil + } + b, err := json.Marshal(problematicPkgs) + 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..74bb1156a --- /dev/null +++ b/enricher/rhcc/rhcc_test.go @@ -0,0 +1,218 @@ +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() + io.WriteString(h, name) + 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: map[string]string{}, + }, + { + 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(), "2": 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 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 +} diff --git a/rhel/rhcc/coalescer_test.go b/rhel/rhcc/coalescer_test.go index bc5ce91c9..21c2cbe7a 100644 --- a/rhel/rhcc/coalescer_test.go +++ b/rhel/rhcc/coalescer_test.go @@ -21,7 +21,7 @@ func TestCoalescer(t *testing.T) { // Mark them as if they came from this package's package scanner p.RepositoryHint = `rhcc` } - repo := []*claircore.Repository{&goldRepo} + repo := []*claircore.Repository{&GoldRepo} layerArtifacts := []*indexer.LayerArtifacts{ { Hash: test.RandomSHA256Digest(t), @@ -67,7 +67,7 @@ func TestCoalescer(t *testing.T) { } for _, id := range e.RepositoryIDs { r := ir.Repositories[id] - if got, want := r.Name, goldRepo.Name; got != want { + if got, want := r.Name, GoldRepo.Name; got != want { t.Errorf("got: %q, want: %q", got, want) } } diff --git a/rhel/rhcc/matcher.go b/rhel/rhcc/matcher.go index dd761ec0c..054859c2c 100644 --- a/rhel/rhcc/matcher.go +++ b/rhel/rhcc/matcher.go @@ -26,7 +26,7 @@ func (*matcher) Name() string { return "rhel-container-matcher" } // Filter implements [driver.Matcher]. func (*matcher) Filter(r *claircore.IndexRecord) bool { return r.Repository != nil && - r.Repository.Name == goldRepo.Name + r.Repository.Name == GoldRepo.Name } // Query implements [driver.Matcher]. diff --git a/rhel/rhcc/parser_test.go b/rhel/rhcc/parser_test.go index eab81c2b4..1633ad7b5 100644 --- a/rhel/rhcc/parser_test.go +++ b/rhel/rhcc/parser_test.go @@ -39,7 +39,7 @@ func TestDB(t *testing.T) { Links: "https://access.redhat.com/errata/RHSA-2021:3665 https://access.redhat.com/security/cve/CVE-2021-3762", NormalizedSeverity: claircore.High, FixedInVersion: "v3.5.7-8", - Repo: &goldRepo, + Repo: &GoldRepo, Range: &claircore.Range{ Lower: claircore.Version{ Kind: "rhctag", @@ -81,7 +81,7 @@ func TestDB(t *testing.T) { }, }, FixedInVersion: "v4.6.0-202112140546.p0.g8b9da97.assembly.stream", - Repo: &goldRepo, + Repo: &GoldRepo, }, { Name: "RHSA-2021:5107", @@ -103,7 +103,7 @@ func TestDB(t *testing.T) { }, }, FixedInVersion: "v4.7.0-202112140553.p0.g091bb99.assembly.stream", - Repo: &goldRepo, + Repo: &GoldRepo, }, { Name: "RHSA-2021:5108", @@ -125,7 +125,7 @@ func TestDB(t *testing.T) { }, }, FixedInVersion: "v4.8.0-202112132154.p0.g57dd03a.assembly.stream", - Repo: &goldRepo, + Repo: &GoldRepo, }, }, }, @@ -153,7 +153,7 @@ func TestDB(t *testing.T) { }, }, FixedInVersion: "v6.8.1-65", - Repo: &goldRepo, + Repo: &GoldRepo, }, { Name: "RHSA-2021:5137", @@ -175,7 +175,7 @@ func TestDB(t *testing.T) { }, }, FixedInVersion: "v5.0.10-1", - Repo: &goldRepo, + Repo: &GoldRepo, }, }, }, @@ -203,7 +203,7 @@ func TestDB(t *testing.T) { }, }, FixedInVersion: "4.8-167.9a9db5f.release_4.8", - Repo: &goldRepo, + Repo: &GoldRepo, }, { Name: "RHSA-2021:2041", @@ -225,7 +225,7 @@ func TestDB(t *testing.T) { }, }, FixedInVersion: "4.7-140.49a6fcf.release_4.7", - Repo: &goldRepo, + Repo: &GoldRepo, }, }, }, diff --git a/rhel/rhcc/rhcc.go b/rhel/rhcc/rhcc.go index 6390536da..38e876181 100644 --- a/rhel/rhcc/rhcc.go +++ b/rhel/rhcc/rhcc.go @@ -14,7 +14,7 @@ import ( "github.com/quay/claircore/toolkit/types/cpe" ) -var goldRepo = claircore.Repository{ +var GoldRepo = claircore.Repository{ Name: "Red Hat Container Catalog", URI: `https://catalog.redhat.com/software/containers/explore`, } diff --git a/rhel/rhcc/scanner.go b/rhel/rhcc/scanner.go index 727b07cce..247fc1c95 100644 --- a/rhel/rhcc/scanner.go +++ b/rhel/rhcc/scanner.go @@ -299,5 +299,5 @@ func (s *reposcanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircor } zlog.Debug(ctx). Msg("found buildinfo Dockerfile") - return []*claircore.Repository{&goldRepo}, nil + return []*claircore.Repository{&GoldRepo}, nil } diff --git a/rhel/rhcc/updater.go b/rhel/rhcc/updater.go index 111e84d6d..f68518b19 100644 --- a/rhel/rhcc/updater.go +++ b/rhel/rhcc/updater.go @@ -288,7 +288,7 @@ func (u *updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vuln Severity: releasesByMinor[minor].Severity, NormalizedSeverity: common.NormalizeSeverity(releasesByMinor[minor].Severity), Package: p, - Repo: &goldRepo, + Repo: &GoldRepo, Links: links, FixedInVersion: firstPatch.Original, Range: r,