diff --git a/rhel/rhcc/coalescer.go b/rhel/rhcc/coalescer.go new file mode 100644 index 000000000..dab27d9ca --- /dev/null +++ b/rhel/rhcc/coalescer.go @@ -0,0 +1,48 @@ +package rhcc + +import ( + "context" + + "github.com/quay/claircore" + "github.com/quay/claircore/internal/indexer" +) + +// coalescer takes individual layer artifacts and coalesces them to form the final image's +// package results +type coalescer struct{} + +func (c *coalescer) Coalesce(ctx context.Context, ls []*indexer.LayerArtifacts) (*claircore.IndexReport, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + ir := &claircore.IndexReport{ + Environments: map[string][]*claircore.Environment{}, + Packages: map[string]*claircore.Package{}, + Repositories: map[string]*claircore.Repository{}, + } + + for _, l := range ls { + if len(l.Repos) == 0 { + continue + } + rs := make([]string, len(l.Repos)) + for i, r := range l.Repos { + rs[i] = r.ID + ir.Repositories[r.ID] = r + } + for _, pkg := range l.Pkgs { + if pkg.RepositoryHint != `rhcc` { + continue + } + ir.Packages[pkg.ID] = pkg + ir.Environments[pkg.ID] = []*claircore.Environment{ + { + PackageDB: pkg.PackageDB, + IntroducedIn: l.Hash, + RepositoryIDs: rs, + }, + } + } + } + return ir, nil +} diff --git a/rhel/rhcc/coalescer_test.go b/rhel/rhcc/coalescer_test.go new file mode 100644 index 000000000..f7139ec5f --- /dev/null +++ b/rhel/rhcc/coalescer_test.go @@ -0,0 +1,74 @@ +package rhcc + +import ( + "context" + "strconv" + "testing" + + "github.com/quay/zlog" + + "github.com/quay/claircore" + "github.com/quay/claircore/internal/indexer" + "github.com/quay/claircore/test" +) + +func TestCoalescer(t *testing.T) { + ctx := zlog.Test(context.Background(), t) + coalescer := &coalescer{} + pkgs := test.GenUniquePackages(6) + for _, p := range pkgs { + // Mark them as if they came from this package's package scanner + p.RepositoryHint = `rhcc` + } + repo := []*claircore.Repository{&goldRepo} + layerArtifacts := []*indexer.LayerArtifacts{ + { + Hash: test.RandomSHA256Digest(t), + Pkgs: pkgs[:1], + }, + { + Hash: test.RandomSHA256Digest(t), + Pkgs: pkgs[:2], + }, + { + Hash: test.RandomSHA256Digest(t), + Pkgs: pkgs[:3], + Repos: repo, + }, + { + Hash: test.RandomSHA256Digest(t), + Pkgs: pkgs[:4], + }, + { + Hash: test.RandomSHA256Digest(t), + Pkgs: pkgs[:5], + Repos: repo, + }, + { + Hash: test.RandomSHA256Digest(t), + Pkgs: pkgs, + }, + } + ir, err := coalescer.Coalesce(ctx, layerArtifacts) + if err != nil { + t.Fatalf("received error from coalesce method: %v", err) + } + // Expect 0-5 to have gotten associated with the repository. + for i := range pkgs { + es, ok := ir.Environments[strconv.Itoa(i)] + if !ok && i == 5 { + // Left out the last package. + continue + } + e := es[0] + if len(e.RepositoryIDs) == 0 { + t.Error("expected some repositories") + } + for _, id := range e.RepositoryIDs { + r := ir.Repositories[id] + if got, want := r.Name, goldRepo.Name; got != want { + t.Errorf("got: %q, want: %q", got, want) + } + } + } +} diff --git a/rhel/rhcc/ecosystem.go b/rhel/rhcc/ecosystem.go new file mode 100644 index 000000000..de6a81ecb --- /dev/null +++ b/rhel/rhcc/ecosystem.go @@ -0,0 +1,24 @@ +package rhcc + +import ( + "context" + + "github.com/quay/claircore/internal/indexer" +) + +func NewEcosystem(_ context.Context) *indexer.Ecosystem { + return &indexer.Ecosystem{ + PackageScanners: func(_ context.Context) ([]indexer.PackageScanner, error) { + return []indexer.PackageScanner{&scanner{}}, nil + }, + DistributionScanners: func(_ context.Context) ([]indexer.DistributionScanner, error) { + return nil, nil + }, + RepositoryScanners: func(_ context.Context) ([]indexer.RepositoryScanner, error) { + return []indexer.RepositoryScanner{&reposcanner{}}, nil + }, + Coalescer: func(_ context.Context) (indexer.Coalescer, error) { + return &coalescer{}, nil + }, + } +} diff --git a/rhel/rhcc/fetcher_test.go b/rhel/rhcc/fetcher_test.go new file mode 100644 index 000000000..82ec1e63c --- /dev/null +++ b/rhel/rhcc/fetcher_test.go @@ -0,0 +1,50 @@ +package rhcc + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/quay/zlog" + + "github.com/quay/claircore/libvuln/driver" +) + +func TestFetcher(t *testing.T) { + const serveFile = "testdata/cve-2021-3762.xml" + ctx := zlog.Test(context.Background(), t) + + fi, err := os.Stat(serveFile) + if err != nil { + t.Fatal(err) + } + tag := fmt.Sprintf(`"%d"`, fi.ModTime().UnixNano()) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("if-none-match") == tag { + w.WriteHeader(http.StatusNotModified) + return + } + w.Header().Set("etag", tag) + http.ServeFile(w, r, serveFile) + })) + defer srv.Close() + + u := &updater{ + url: srv.URL, + client: srv.Client(), + } + rd, hint, err := u.Fetch(ctx, "") + if err != nil { + t.Error(err) + } + if rd != nil { + rd.Close() + } + _, _, err = u.Fetch(ctx, driver.Fingerprint(hint)) + if got, want := err, driver.Unchanged; got != want { + t.Errorf("got: %v, want: %v", got, want) + } +} diff --git a/rhel/rhcc/mapper.go b/rhel/rhcc/mapper.go new file mode 100644 index 000000000..e5f4941c4 --- /dev/null +++ b/rhel/rhcc/mapper.go @@ -0,0 +1,137 @@ +package rhcc + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "sync" + "sync/atomic" + "time" + + "github.com/quay/zlog" + "golang.org/x/time/rate" +) + +// MappingFile is a struct for mapping file between container NAME label and +// container registry repository location. +type mappingFile struct { + Data map[string][]string `json:"data"` +} + +func (m *mappingFile) Get(ctx context.Context, _ *http.Client, name string) []string { + if repos, ok := m.Data[name]; ok { + zlog.Debug(ctx).Str("name", name). + Msg("name present in mapping file") + return repos + } + return []string{} +} + +// UpdatingMapper provides local container name -> repos mapping via a +// continually updated local mapping file. +type updatingMapper struct { + URL string + // an atomic value holding the latest + // parsed MappingFile + mapping atomic.Value + + // Machinery for updating the mapping file. + reqRate *rate.Limiter + mu sync.Mutex // protects lastModified + lastModified string +} + +// NewUpdatingMapper returns an UpdatingMapper. +// +// The update period is unconfigurable. The first caller after the period loses +// and must update the mapping file. +func newUpdatingMapper(url string, init *mappingFile) *updatingMapper { + lu := &updatingMapper{ + URL: url, + reqRate: rate.NewLimiter(rate.Every(10*time.Minute), 1), + } + lu.mapping.Store(init) + // If we were provided an initial mapping, pull the first token. + if init != nil { + lu.reqRate.Allow() + } + return lu +} + +// Get translates container names to repos using a mapping file. +// +// Get is safe for concurrent usage. +func (u *updatingMapper) Get(ctx context.Context, c *http.Client, name string) []string { + ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/name2repos/UpdatingMapper.Get") + if name == "" { + return []string{} + } + if u.reqRate.Allow() { + zlog.Debug(ctx).Msg("got unlucky, updating mapping file") + if err := u.do(ctx, c); err != nil { + zlog.Error(ctx). + Err(err). + Msg("error updating mapping file") + } + } + + // interface conversion guaranteed to pass, see + // constructor. + m := u.mapping.Load().(*mappingFile) + if m == nil { + return []string{} + } + return m.Get(ctx, nil, name) +} + +func (u *updatingMapper) Fetch(ctx context.Context, c *http.Client) error { + return u.do(ctx, c) +} + +// Do is an internal method called to perform an atomic update of the mapping +// file. +// +// This method may be ran concurrently. +func (u *updatingMapper) do(ctx context.Context, c *http.Client) error { + ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/UpdatingMapper.do", "url", u.URL) + zlog.Debug(ctx).Msg("attempting fetch of name2repos mapping file") + + u.mu.Lock() + defer u.mu.Unlock() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.URL, nil) + if err != nil { + return err + } + if u.lastModified != "" { + req.Header.Set("if-modified-since", u.lastModified) + } + + resp, err := c.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNotModified: + zlog.Debug(ctx). + Str("since", u.lastModified). + Msg("response not modified; no update necessary") + return nil + default: + return fmt.Errorf("received status code %q querying mapping url", resp.StatusCode) + } + + var mapping mappingFile + err = json.NewDecoder(resp.Body).Decode(&mapping) + if err != nil { + return fmt.Errorf("failed to decode mapping file: %w", err) + } + u.lastModified = resp.Header.Get("last-modified") + // atomic store of mapping file + u.mapping.Store(&mapping) + zlog.Debug(ctx).Msg("atomic update of local mapping file complete") + return nil +} diff --git a/rhel/rhcc/matcher.go b/rhel/rhcc/matcher.go new file mode 100644 index 000000000..4041a8959 --- /dev/null +++ b/rhel/rhcc/matcher.go @@ -0,0 +1,47 @@ +package rhcc + +import ( + "context" + + rpmVersion "github.com/knqyf263/go-rpm-version" + "github.com/quay/zlog" + + "github.com/quay/claircore" + "github.com/quay/claircore/libvuln/driver" +) + +var Matcher driver.Matcher = &matcher{} + +type matcher struct{} + +var _ driver.Matcher = (*matcher)(nil) + +func (*matcher) Name() string { + return "rhel-container-matcher" +} + +func (*matcher) Filter(r *claircore.IndexRecord) bool { + return r.Repository != nil && + r.Repository.Name == goldRepo.Name +} + +func (*matcher) Query() []driver.MatchConstraint { + return []driver.MatchConstraint{driver.RepositoryName} +} + +func (*matcher) Vulnerable(ctx context.Context, record *claircore.IndexRecord, vuln *claircore.Vulnerability) (bool, error) { + pkgVer, fixedInVer := rpmVersion.NewVersion(record.Package.Version), rpmVersion.NewVersion(vuln.FixedInVersion) + zlog.Debug(ctx). + Str("record", record.Package.Version). + Str("vulnerability", vuln.FixedInVersion). + Msg("comparing versions") + return pkgVer.LessThan(fixedInVer), nil +} + +// Implement version filtering to have the database only return results for the +// same minor version. Marking the results as not authoritative means the +// Vulnerable method is still called. + +func (*matcher) VersionFilter() {} + +func (*matcher) VersionAuthoritative() bool { return false } diff --git a/rhel/rhcc/matcher_integration_test.go b/rhel/rhcc/matcher_integration_test.go new file mode 100644 index 000000000..104b944ea --- /dev/null +++ b/rhel/rhcc/matcher_integration_test.go @@ -0,0 +1,135 @@ +package rhcc + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/quay/zlog" + + "github.com/quay/claircore" + match_engine "github.com/quay/claircore/internal/matcher" + vulnstore "github.com/quay/claircore/internal/vulnstore/postgres" + "github.com/quay/claircore/libvuln/driver" + "github.com/quay/claircore/libvuln/updates" + "github.com/quay/claircore/pkg/ctxlock" + "github.com/quay/claircore/test/integration" +) + +func TestMain(m *testing.M) { + var c int + defer func() { os.Exit(c) }() + defer integration.DBSetup()() + c = m.Run() +} + +func TestMatcherIntegration(t *testing.T) { + table := []struct { + cvemap string + indexReport string + cveID string + match bool + }{ + { + cvemap: "cve-2021-3762", + indexReport: "clair-rhel8-v3.5.5-4", + cveID: "CVE-2021-3762", + match: true, + }, + { + cvemap: "cve-2020-8565", + indexReport: "rook-ceph-operator-container-4.6-115.d1788e1.release_4.6", + cveID: "CVE-2020-8565", + match: true, + }, + { + cvemap: "cve-2020-8565", + indexReport: "rook-ceph-operator-container-4.7-159.76b9b11.release_4.7", + cveID: "CVE-2020-8565", + match: false, + }, + } + + for _, tt := range table { + t.Run(tt.indexReport, func(t *testing.T) { + integration.NeedDB(t) + ctx := zlog.Test(context.Background(), t) + pool := vulnstore.TestDB(ctx, t) + store := vulnstore.NewVulnStore(pool) + m := &matcher{} + + serveFile := fmt.Sprintf("testdata/%s.xml", tt.cvemap) + + fi, err := os.Stat(serveFile) + if err != nil { + t.Fatal(err) + } + tag := fmt.Sprintf(`"%d"`, fi.ModTime().UnixNano()) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("etag", tag) + http.ServeFile(w, r, serveFile) + })) + defer srv.Close() + u := &updater{ + url: srv.URL, + client: srv.Client(), + } + s := driver.NewUpdaterSet() + if err := s.Add(u); err != nil { + t.Error(err) + } + + locks, err := ctxlock.New(ctx, pool) + if err != nil { + t.Error(err) + } + defer locks.Close(ctx) + + facs := make(map[string]driver.UpdaterSetFactory, 1) + facs[u.Name()] = driver.StaticSet(s) + mgr, err := updates.NewManager(ctx, store, locks, http.DefaultClient, updates.WithFactories(facs)) + if err != nil { + t.Error(err) + } + + // force update + if err := mgr.Run(ctx); err != nil { + t.Error(err) + } + + f, err := os.Open(filepath.Join("testdata", fmt.Sprintf("%s-indexreport.json", tt.indexReport))) + if err != nil { + t.Fatalf("%v", err) + } + defer f.Close() + var ir claircore.IndexReport + if err := json.NewDecoder(f).Decode(&ir); err != nil { + t.Fatalf("failed to decode IndexReport: %v", err) + } + vr, err := match_engine.Match(ctx, &ir, []driver.Matcher{m}, store) + if err != nil { + t.Fatal(err) + } + found := false + vulns := vr.Vulnerabilities + for _, vuln := range vulns { + t.Log(vuln.Name) + if vuln.Name == tt.cveID { + found = true + } + } + if found != tt.match { + t.Fatalf("Expected to find %s in vulnerability report", tt.cveID) + } + if err := json.NewEncoder(ioutil.Discard).Encode(&vr); err != nil { + t.Fatalf("failed to marshal VR: %v", err) + } + }) + } +} diff --git a/rhel/rhcc/parser_test.go b/rhel/rhcc/parser_test.go new file mode 100644 index 000000000..4f9ca1ca2 --- /dev/null +++ b/rhel/rhcc/parser_test.go @@ -0,0 +1,274 @@ +package rhcc + +import ( + "context" + "fmt" + "math" + "os" + "path/filepath" + "sort" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/quay/zlog" + + "github.com/quay/claircore" + "github.com/quay/claircore/pkg/cpe" +) + +func TestDB(t *testing.T) { + cve20213762issued, _ := time.Parse(time.RFC3339, "2021-09-28T00:00:00Z") + + date_2021_12_14, _ := time.Parse(time.RFC3339, "2021-12-14T00:00:00Z") + date_2021_12_16, _ := time.Parse(time.RFC3339, "2021-12-16T00:00:00Z") + date_2021_05_19, _ := time.Parse(time.RFC3339, "2021-05-19T00:00:00Z") + date_2021_08_03, _ := time.Parse(time.RFC3339, "2021-08-03T00:00:00Z") + + tt := []dbTestcase{ + { + Name: "cve-2021-3762", + Want: []*claircore.Vulnerability{ + { + Name: "CVE-2021-3762", + Description: "A directory traversal vulnerability was found in the ClairCore engine of Clair. An attacker can exploit this by supplying a crafted container image which, when scanned by Clair, allows for arbitrary file write on the filesystem, potentially allowing for remote code execution.", + Package: &claircore.Package{Name: "quay/clair-rhel8", Kind: claircore.BINARY}, + Updater: "rhel-container-updater", + Issued: cve20213762issued, + Severity: "important", + Links: "https://access.redhat.com/errata/RHSA-2021:3665", + NormalizedSeverity: claircore.High, + FixedInVersion: "v3.5.7-8", + Repo: &goldRepo, + Range: &claircore.Range{ + Lower: claircore.Version{ + Kind: "rhctag", + V: [10]int32{}, + }, + Upper: claircore.Version{ + Kind: "rhctag", + V: [10]int32{ + 3, + 5, + math.MaxInt32, + }, + }, + }, + }, + }, + }, + { + Name: "cve-2021-44228-ose-metering-hive", + Want: []*claircore.Vulnerability{ + { + Name: "CVE-2021-44228", + Description: "A flaw was found in the Apache Log4j logging library in versions from 2.0.0 and before 2.15.0. A remote attacker who can control log messages or log message parameters, can execute arbitrary code on the server via JNDI LDAP endpoint.", + Package: &claircore.Package{Name: "openshift4/ose-metering-hive", Kind: claircore.BINARY}, + Updater: "rhel-container-updater", + Issued: date_2021_12_16, + Severity: "moderate", + Links: "https://access.redhat.com/errata/RHSA-2021:5106", + NormalizedSeverity: claircore.Medium, + Range: &claircore.Range{ + Lower: claircore.Version{ + Kind: "rhctag", + V: [10]int32{}, + }, + Upper: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 6, math.MaxInt32}, + }, + }, + FixedInVersion: "v4.6.0-202112140546.p0.g8b9da97.assembly.stream", + Repo: &goldRepo, + }, + { + Name: "CVE-2021-44228", + Description: "A flaw was found in the Apache Log4j logging library in versions from 2.0.0 and before 2.15.0. A remote attacker who can control log messages or log message parameters, can execute arbitrary code on the server via JNDI LDAP endpoint.", + Package: &claircore.Package{Name: "openshift4/ose-metering-hive", Kind: claircore.BINARY}, + Updater: "rhel-container-updater", + Issued: date_2021_12_16, + Severity: "Critical", + Links: "https://access.redhat.com/errata/RHSA-2021:5107", + NormalizedSeverity: claircore.Critical, + Range: &claircore.Range{ + Lower: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 7}, + }, + Upper: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 7, math.MaxInt32}, + }, + }, + FixedInVersion: "v4.7.0-202112140553.p0.g091bb99.assembly.stream", + Repo: &goldRepo, + }, + { + Name: "CVE-2021-44228", + Description: "A flaw was found in the Apache Log4j logging library in versions from 2.0.0 and before 2.15.0. A remote attacker who can control log messages or log message parameters, can execute arbitrary code on the server via JNDI LDAP endpoint.", + Package: &claircore.Package{Name: "openshift4/ose-metering-hive", Kind: claircore.BINARY}, + Updater: "rhel-container-updater", + Issued: date_2021_12_14, + Severity: "Critical", + Links: "https://access.redhat.com/errata/RHSA-2021:5108", + NormalizedSeverity: claircore.Critical, + Range: &claircore.Range{ + Lower: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 8}, + }, + Upper: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 8, math.MaxInt32}, + }, + }, + FixedInVersion: "v4.8.0-202112132154.p0.g57dd03a.assembly.stream", + Repo: &goldRepo, + }, + }, + }, + { + Name: "cve-2021-44228-openshift-logging", + Want: []*claircore.Vulnerability{ + { + Name: "CVE-2021-44228", + Description: "A flaw was found in the Apache Log4j logging library in versions from 2.0.0 and before 2.15.0. A remote attacker who can control log messages or log message parameters, can execute arbitrary code on the server via JNDI LDAP endpoint.", + Package: &claircore.Package{Name: "openshift-logging/elasticsearch6-rhel8", Kind: claircore.BINARY}, + Updater: "rhel-container-updater", + Issued: date_2021_12_14, + Severity: "moderate", + Links: "https://access.redhat.com/errata/RHSA-2021:5137", + NormalizedSeverity: claircore.Medium, + Range: &claircore.Range{ + Lower: claircore.Version{ + Kind: "rhctag", + V: [10]int32{}, + }, + Upper: claircore.Version{ + Kind: "rhctag", + V: [10]int32{5, 0, math.MaxInt32}, + }, + }, + FixedInVersion: "v5.0.10-1", + Repo: &goldRepo, + }, + { + Name: "CVE-2021-44228", + Description: "A flaw was found in the Apache Log4j logging library in versions from 2.0.0 and before 2.15.0. A remote attacker who can control log messages or log message parameters, can execute arbitrary code on the server via JNDI LDAP endpoint.", + Package: &claircore.Package{Name: "openshift-logging/elasticsearch6-rhel8", Kind: claircore.BINARY}, + Updater: "rhel-container-updater", + Issued: date_2021_12_14, + Severity: "Critical", + NormalizedSeverity: claircore.Critical, + Links: "https://access.redhat.com/errata/RHSA-2021:5129", + Range: &claircore.Range{ + Lower: claircore.Version{ + Kind: "rhctag", + V: [10]int32{6, 8}, + }, + Upper: claircore.Version{ + Kind: "rhctag", + V: [10]int32{6, 8, math.MaxInt32}, + }, + }, + FixedInVersion: "v6.8.1-65", + Repo: &goldRepo, + }, + }, + }, + { + Name: "cve-2020-8565", + Want: []*claircore.Vulnerability{ + { + Name: "CVE-2020-8565", + Description: "A flaw was found in kubernetes. In Kubernetes, if the logging level is to at least 9, authorization and bearer tokens will be written to log files. This can occur both in API server logs and client tool output like `kubectl`. Previously, CVE-2019-11250 was assigned for the same issue for logging levels of at least 4.", + Package: &claircore.Package{Name: "ocs4/rook-ceph-rhel8-operator", Kind: claircore.BINARY}, + Updater: "rhel-container-updater", + Issued: date_2021_05_19, + Severity: "Moderate", + Links: "https://access.redhat.com/errata/RHSA-2021:2041", + NormalizedSeverity: claircore.Medium, + Range: &claircore.Range{ + Lower: claircore.Version{ + Kind: "rhctag", + V: [10]int32{}, + }, + Upper: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 7, math.MaxInt32}, + }, + }, + FixedInVersion: "4.7-140.49a6fcf.release_4.7", + Repo: &goldRepo, + }, + { + Name: "CVE-2020-8565", + Description: "A flaw was found in kubernetes. In Kubernetes, if the logging level is to at least 9, authorization and bearer tokens will be written to log files. This can occur both in API server logs and client tool output like `kubectl`. Previously, CVE-2019-11250 was assigned for the same issue for logging levels of at least 4.", + Package: &claircore.Package{Name: "ocs4/rook-ceph-rhel8-operator", Kind: claircore.BINARY}, + Updater: "rhel-container-updater", + Issued: date_2021_08_03, + Severity: "Moderate", + NormalizedSeverity: claircore.Medium, + Links: "https://access.redhat.com/errata/RHBA-2021:3003", + Range: &claircore.Range{ + Lower: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 8}, + }, + Upper: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 8, math.MaxInt32}, + }, + }, + FixedInVersion: "4.8-167.9a9db5f.release_4.8", + Repo: &goldRepo, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.Name, tc.Run) + } +} + +type dbTestcase struct { + Name string + Want []*claircore.Vulnerability +} + +func (tc dbTestcase) filename() string { + return filepath.Join("testdata", fmt.Sprintf("%s.xml", tc.Name)) +} + +func cpeUnbind(cpeValue string) cpe.WFN { + wfn, _ := cpe.Unbind(cpeValue) + return wfn +} + +func (tc dbTestcase) Run(t *testing.T) { + ctx := zlog.Test(context.Background(), t) + + f, err := os.Open(tc.filename()) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + u := &updater{} + got, err := u.Parse(ctx, f) + if err != nil { + t.Fatal(err) + } + t.Logf("found %d vulnerabilties", len(got)) + if len(got) != len(tc.Want) { + t.Fatalf("got: %d vulnerabilities, want %d vulnerabilities", len(got), len(tc.Want)) + } + // Sort for the comparison, because the Vulnerabilities method can return + // the slice in any order. + sort.SliceStable(got, func(i, j int) bool { return got[i].Name < got[j].Name }) + if !cmp.Equal(tc.Want, got) { + t.Error(cmp.Diff(tc.Want, got)) + } +} diff --git a/rhel/rhcc/rhcc.go b/rhel/rhcc/rhcc.go new file mode 100644 index 000000000..7c8ee6207 --- /dev/null +++ b/rhel/rhcc/rhcc.go @@ -0,0 +1,109 @@ +package rhcc + +import ( + "encoding/xml" + "strings" + "time" + + "github.com/quay/claircore" + "github.com/quay/claircore/pkg/cpe" + "github.com/quay/claircore/pkg/rhctag" +) + +var goldRepo = claircore.Repository{ + Name: "Red Hat Container Catalog", + URI: `https://catalog.redhat.com/software/containers/explore`, +} + +type cveMap struct { + XMLName xml.Name `xml:"cvemap"` + RedHatVulnerabilities []redHatVulnerability `xml:"Vulnerability"` +} + +type redHatVulnerability struct { + XMLName xml.Name `xml:"Vulnerability"` + Name string `xml:"name,attr"` + ThreatSeverity string `xml:"ThreatSeverity"` + AffectedReleases []affectedRelease `xml:"AffectedRelease"` + Details []details `xml:"Details"` +} + +type affectedRelease struct { + XMLName xml.Name `xml:"AffectedRelease"` + Cpe string `xml:"cpe,attr"` + ReleaseDate customTime `xml:"ReleaseDate"` + Package string `xml:"Package"` + Impact string `xml:"impact,attr"` + Advisory advisory `xml:"Advisory"` +} + +type advisory struct { + XMLName xml.Name `xml:"Advisory"` + URL string `xml:"url,attr"` +} + +type details struct { + XMLName xml.Name `xml:"Details"` + Text string `xml:",cdata"` + Source string `xml:"source,attr"` +} + +type customTime struct { + time time.Time +} + +type consolidatedRelease struct { + Issued time.Time + FixedInVersions *rhctag.Versions + Severity string + AdvisoryLink string + Cpe cpe.WFN +} + +func (c *customTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + const shortForm = "2006-01-02" + var v string + d.DecodeElement(&v, &start) + date := strings.Split(v, "T") + parse, err := time.Parse(shortForm, date[0]) + if err != nil { + return err + } + *c = customTime{parse} + return nil +} + +func parseContainerPackage(p string) (bool, string, string) { + parts := strings.Split(p, ":") + if len(parts) != 2 { + return false, "", "" + } + if !strings.ContainsAny(parts[0], "/") { + return false, "", "" + } + return true, parts[0], parts[1] +} + +// Prefer Red Hat descriptions over Mitre ones +func getDescription(ds []details) string { + rhDetailsIdx := -1 + mitreDetailsIdx := -1 + result := "" + for idx, d := range ds { + if d.Source == "Red Hat" { + rhDetailsIdx = idx + break + } else if d.Source == "Mitre" { + mitreDetailsIdx = idx + } + } + if rhDetailsIdx != -1 { + result = ds[rhDetailsIdx].Text + return strings.TrimSpace(result) + } + if mitreDetailsIdx != -1 { + result = ds[mitreDetailsIdx].Text + return strings.TrimSpace(result) + } + return strings.TrimSpace(result) +} diff --git a/rhel/rhcc/scanner.go b/rhel/rhcc/scanner.go new file mode 100644 index 000000000..fadfe45e1 --- /dev/null +++ b/rhel/rhcc/scanner.go @@ -0,0 +1,258 @@ +package rhcc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/fs" + "net/http" + "os" + "strings" + "time" + + "github.com/quay/zlog" + + "github.com/quay/claircore" + "github.com/quay/claircore/internal/indexer" + "github.com/quay/claircore/pkg/rhctag" + "github.com/quay/claircore/pkg/tarfs" + "github.com/quay/claircore/rhel/dockerfile" +) + +var _ indexer.PackageScanner = (*scanner)(nil) + +type nameReposMapper interface { + Get(context.Context, *http.Client, string) []string +} + +type scanner struct { + mapper nameReposMapper + client *http.Client + cfg ScannerConfig +} + +type ScannerConfig struct { + Name2ReposMappingURL string `json:"name2repos_mapping_url" yaml:"name2repos_mapping_url"` + Name2ReposMappingFile string `json:"name2repos_mapping_file" yaml:"name2repos_mapping_file"` + Timeout time.Duration `json:"timeout" yaml:"timeout"` +} + +// DefaultRepo2CPEMappingURL is default URL with a mapping file provided by Red +// Hat. +const DefaultName2ReposMappingURL = "https://access.redhat.com/security/data/metrics/container-name-repos-map.json" + +// Configure implements the RPCScanner interface. +func (s *scanner) Configure(ctx context.Context, f indexer.ConfigDeserializer, c *http.Client) error { + ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/scanner.Configure") + s.client = c + if err := f(&s.cfg); err != nil { + return err + } + // Set defaults if not set via passed function. + switch { + case s.cfg.Name2ReposMappingURL == "" && s.cfg.Name2ReposMappingFile == "": + // defaults + s.cfg.Name2ReposMappingURL = DefaultName2ReposMappingURL + fallthrough + case s.cfg.Name2ReposMappingURL != "" && s.cfg.Name2ReposMappingFile == "": + // remote only + u := newUpdatingMapper(s.cfg.Name2ReposMappingURL, nil) + if err := u.Fetch(ctx, s.client); err != nil { + return err + } + s.mapper = u + case s.cfg.Name2ReposMappingURL == "" && s.cfg.Name2ReposMappingFile != "": + // local only + f, err := os.Open(s.cfg.Name2ReposMappingFile) + if err != nil { + return err + } + defer f.Close() + var mf mappingFile + if err := json.NewDecoder(f).Decode(&mf); err != nil { + return err + } + s.mapper = &mf + case s.cfg.Name2ReposMappingURL != "" && s.cfg.Name2ReposMappingFile != "": + // load, then fetch later + f, err := os.Open(s.cfg.Name2ReposMappingFile) + if err != nil { + return err + } + defer f.Close() + var mf mappingFile + if err := json.NewDecoder(f).Decode(&mf); err != nil { + return err + } + s.mapper = newUpdatingMapper(s.cfg.Name2ReposMappingURL, &mf) + } + if s.cfg.Timeout == 0 { + s.cfg.Timeout = 30 * time.Second + } + return nil +} + +func (s *scanner) Name() string { return "rhel_containerscanner" } + +func (s *scanner) Version() string { return "1" } + +func (s *scanner) Kind() string { return "package" } + +// Scan performs a package scan on the given layer and returns all +// the RHEL container identifying metadata +func (s *scanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Package, error) { + ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/scanner.Scan") + const ( + compLabel = `com.redhat.component` + nameLabel = `name` + archLabel = `architecture` + ) + if err := ctx.Err(); err != nil { + return nil, err + } + + // add source package from component label + labels, p, err := findLabels(ctx, l) + switch { + case errors.Is(err, nil): + case errors.Is(err, errNotFound): + return nil, nil + default: + return nil, err + } + + vr := getVR(p) + rhctagVersion, err := rhctag.Parse(vr) + if err != nil { + // This can happen for containers which don't use semantic versioning, + // such as UBI. + return nil, nil + } + buildName, ok := labels[compLabel] + if !ok { + return nil, fmt.Errorf("expected label %s not found in dockerfile", compLabel) + } + arch, ok := labels[archLabel] + if !ok { + return nil, fmt.Errorf("expected label %s not found in dockerfile", archLabel) + } + name, ok := labels[nameLabel] + if !ok { + return nil, fmt.Errorf("expected label %s not found in dockerfile", nameLabel) + } + + minorRange := rhctagVersion.MinorStart() + src := claircore.Package{ + Kind: claircore.SOURCE, + Name: buildName, + Version: vr, + NormalizedVersion: minorRange.Version(true), + PackageDB: p, + Arch: arch, + RepositoryHint: `rhcc`, + } + pkgs := []*claircore.Package{&src} + + repos := s.mapper.Get(ctx, s.client, name) + if len(repos) == 0 { + // Didn't find external_repos in mapping, use name label as package + // name. + repos = []string{name} + } + for _, name := range repos { + // Add each external repo as a binary package. The same container image + // can ship to multiple repos eg. `"toolbox-container": + // ["rhel8/toolbox", "ubi8/toolbox"]`. Therefore, we want a binary + // package entry for each. + pkgs = append(pkgs, &claircore.Package{ + Kind: claircore.BINARY, + Name: name, + Version: vr, + NormalizedVersion: minorRange.Version(true), + Source: &src, + PackageDB: p, + Arch: arch, + RepositoryHint: `rhcc`, + }) + } + return pkgs, nil +} + +func findLabels(ctx context.Context, layer *claircore.Layer) (map[string]string, string, error) { + r, err := layer.Reader() + if err != nil { + return nil, "", err + } + defer r.Close() + sys, err := tarfs.New(r) + if err != nil { + return nil, "", err + } + ms, err := fs.Glob(sys, "root/buildinfo/Dockerfile-*") + if err != nil { // Can only return ErrBadPattern. + panic("progammer error") + } + if len(ms) == 0 { + return nil, "", errNotFound + } + p := ms[0] + f, err := sys.Open(p) + if err != nil { + return nil, "", err + } + defer f.Close() + labels, err := dockerfile.GetLabels(ctx, f) + if err != nil { + return nil, "", err + } + return labels, p, nil +} + +var errNotFound = errors.New("not found") + +// GetVR extracts the version-release string from the provided string ending in +// an NVR. +// +// Panics if passed malformed input. +func getVR(nvr string) string { + if strings.Count(nvr, "-") < 2 { + panic("programmer error") + } + i := strings.LastIndexByte(nvr, '-') + i = strings.LastIndexByte(nvr[:i], '-') + return nvr[i+1:] +} + +type reposcanner struct{} + +var _ indexer.RepositoryScanner = (*reposcanner)(nil) + +func (s *reposcanner) Name() string { return "rhel_containerscanner" } + +func (s *reposcanner) Version() string { return "1" } + +func (s *reposcanner) Kind() string { return "repository" } + +func (s *reposcanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Repository, error) { + ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/reposcanner.Scan") + r, err := l.Reader() + if err != nil { + return nil, err + } + defer r.Close() + sys, err := tarfs.New(r) + if err != nil { + return nil, err + } + ms, err := fs.Glob(sys, "root/buildinfo/Dockerfile-*") + if err != nil { // Can only return ErrBadPattern. + panic("progammer error") + } + if len(ms) == 0 { + return nil, nil + } + zlog.Debug(ctx). + Msg("found buildinfo Dockerfile") + return []*claircore.Repository{&goldRepo}, nil +} diff --git a/rhel/rhcc/scanner_test.go b/rhel/rhcc/scanner_test.go new file mode 100644 index 000000000..91cccbc40 --- /dev/null +++ b/rhel/rhcc/scanner_test.go @@ -0,0 +1,196 @@ +package rhcc + +import ( + "archive/tar" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "path" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/quay/zlog" + + "github.com/quay/claircore" +) + +func TestContainerScanner(t *testing.T) { + clairSourceContainer := &claircore.Package{ + Name: "quay-clair-container", + Version: "v3.5.5-4", + NormalizedVersion: claircore.Version{ + Kind: "rhctag", + V: [10]int32{3, 5}, + }, + Kind: claircore.SOURCE, + PackageDB: "root/buildinfo/Dockerfile-quay-clair-rhel8-v3.5.5-4", + RepositoryHint: "rhcc", + Arch: "x86_64", + } + + quaySourceContainer := &claircore.Package{ + Name: "quay-registry-container", + Version: "v3.5.6-4", + NormalizedVersion: claircore.Version{ + Kind: "rhctag", + V: [10]int32{3, 5}, + }, + Kind: claircore.SOURCE, + PackageDB: "root/buildinfo/Dockerfile-quay-quay-rhel8-v3.5.6-4", + RepositoryHint: "rhcc", + Arch: "x86_64", + } + + loggingSourceContainer := &claircore.Package{ + Name: "logging-elasticsearch6-container", + Version: "v4.6.0-202112132021.p0.g2a13a81.assembly.stream", + NormalizedVersion: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 6}, + }, + Kind: claircore.SOURCE, + PackageDB: "root/buildinfo/Dockerfile-openshift-ose-logging-elasticsearch6-v4.6.0-202112132021.p0.g2a13a81.assembly.stream", + RepositoryHint: "rhcc", + Arch: "x86_64", + } + name2reposData := map[string]map[string][]string{ + "data": {"openshift/ose-logging-elasticsearch6": {"openshift4/ose-logging-elasticsearch6"}}, + } + + table := []struct { + dockerfile string + want []*claircore.Package + }{ + { + dockerfile: "testdata/Dockerfile-quay-quay-rhel8-v3.5.6-4", + want: []*claircore.Package{ + quaySourceContainer, + { + Name: "quay/quay-rhel8", + Version: "v3.5.6-4", + NormalizedVersion: claircore.Version{ + Kind: "rhctag", + V: [10]int32{3, 5}, + }, + Kind: claircore.BINARY, + Source: quaySourceContainer, + PackageDB: "root/buildinfo/Dockerfile-quay-quay-rhel8-v3.5.6-4", + RepositoryHint: "rhcc", + Arch: "x86_64", + }, + }, + }, + { + dockerfile: "testdata/Dockerfile-quay-clair-rhel8-v3.5.5-4", + want: []*claircore.Package{ + clairSourceContainer, + { + Name: "quay/clair-rhel8", + Version: "v3.5.5-4", + NormalizedVersion: claircore.Version{ + Kind: "rhctag", + V: [10]int32{3, 5}, + }, + Kind: claircore.BINARY, + Source: clairSourceContainer, + PackageDB: "root/buildinfo/Dockerfile-quay-clair-rhel8-v3.5.5-4", + RepositoryHint: "rhcc", + Arch: "x86_64", + }, + }, + }, + { + dockerfile: "testdata/Dockerfile-openshift-ose-logging-elasticsearch6-v4.6.0-202112132021.p0.g2a13a81.assembly.stream", + want: []*claircore.Package{ + loggingSourceContainer, + { + Name: "openshift4/ose-logging-elasticsearch6", + Version: "v4.6.0-202112132021.p0.g2a13a81.assembly.stream", + NormalizedVersion: claircore.Version{ + Kind: "rhctag", + V: [10]int32{4, 6}, + }, + Kind: claircore.BINARY, + Source: loggingSourceContainer, + PackageDB: "root/buildinfo/Dockerfile-openshift-ose-logging-elasticsearch6-v4.6.0-202112132021.p0.g2a13a81.assembly.stream", + RepositoryHint: "rhcc", + Arch: "x86_64", + }, + }, + }, + } + ctx := zlog.Test(context.Background(), t) + mux := http.NewServeMux() + mux.HandleFunc("/container-name-repos-map.json", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("last-modified", "Mon, 02 Jan 2006 15:04:05 MST") + if err := json.NewEncoder(w).Encode(name2reposData); err != nil { + t.Fatal(err) + } + }) + srv := httptest.NewServer(mux) + defer srv.Close() + var cs scanner + cf := func(v interface{}) error { + cfg := v.(*ScannerConfig) + cfg.Name2ReposMappingURL = srv.URL + "/container-name-repos-map.json" + return nil + } + if err := cs.Configure(ctx, cf, srv.Client()); err != nil { + t.Error(err) + } + + for _, tt := range table { + t.Run(tt.dockerfile, func(t *testing.T) { + dockerfile, err := os.Open(tt.dockerfile) + if err != nil { + t.Fatal(err) + } + defer dockerfile.Close() + fi, err := dockerfile.Stat() + if err != nil { + t.Fatal(err) + } + tmpdir := t.TempDir() + // Write a tarball with the binary. + tarname := filepath.Join(tmpdir, "tar") + tf, err := os.Create(tarname) + if err != nil { + t.Fatal(err) + } + defer tf.Close() + tw := tar.NewWriter(tf) + hdr, err := tar.FileInfoHeader(fi, "") + if err != nil { + t.Fatal(err) + } + hdr.Name = path.Join("root/buildinfo", path.Base(tt.dockerfile)) + if err := tw.WriteHeader(hdr); err != nil { + t.Error(err) + } + if _, err := io.Copy(tw, dockerfile); err != nil { + t.Error(err) + } + if err := tw.Close(); err != nil { + t.Error(err) + } + t.Logf("wrote tar to: %s", tf.Name()) + + // Make a fake layer with the tarball. + l := claircore.Layer{} + l.SetLocal(tf.Name()) + + got, err := cs.Scan(ctx, &l) + if err != nil { + t.Error(err) + } + t.Logf("found %d packages", len(got)) + if !cmp.Equal(got, tt.want) { + t.Error(cmp.Diff(got, tt.want)) + } + }) + } +} diff --git a/rhel/rhcc/testdata/Dockerfile-openshift-ose-logging-elasticsearch6-v4.6.0-202112132021.p0.g2a13a81.assembly.stream b/rhel/rhcc/testdata/Dockerfile-openshift-ose-logging-elasticsearch6-v4.6.0-202112132021.p0.g2a13a81.assembly.stream new file mode 100644 index 000000000..20c3227be --- /dev/null +++ b/rhel/rhcc/testdata/Dockerfile-openshift-ose-logging-elasticsearch6-v4.6.0-202112132021.p0.g2a13a81.assembly.stream @@ -0,0 +1,91 @@ +FROM sha256:99132805a8fba15728bd1f32feb2c326beff18ea30d82ad82cc7e9a7c04da356 +ENV __doozer=update BUILD_RELEASE=202112132021.p0.g2a13a81.assembly.stream BUILD_VERSION=v4.6.0 OS_GIT_MAJOR=4 OS_GIT_MINOR=6 OS_GIT_PATCH=0 OS_GIT_TREE_STATE=clean OS_GIT_VERSION=4.6.0-202112132021.p0.g2a13a81.assembly.stream SOURCE_GIT_TREE_STATE=clean +ENV __doozer=merge OS_GIT_COMMIT=2a13a81 OS_GIT_VERSION=4.6.0-202112132021.p0.g2a13a81.assembly.stream-2a13a81 SOURCE_DATE_EPOCH=1639424735 SOURCE_GIT_COMMIT=2a13a81f7fcbb01db50add1430a08e5ec4d38be6 SOURCE_GIT_TAG=v3.11.0-alpha.0-1010-g2a13a81f SOURCE_GIT_URL=https://github.com/openshift/origin-aggregated-logging + +MAINTAINER OpenShift Development + + +EXPOSE 9200 +EXPOSE 9300 +USER 0 + +ARG ES_ARCHIVE_URL +ARG PROMETHEUS_EXPORTER_URL +ARG OPENDISTRO_URL +ARG OPENSHIFT_CI + +ENV ES_PATH_CONF=/etc/elasticsearch/ \ + ES_HOME=/usr/share/elasticsearch \ + ES_VER=6.8.1.redhat-00012 \ + HOME=/opt/app-root/src \ + INSTANCE_RAM=512G \ + JAVA_VER=11 \ + JAVA_HOME=/usr/lib/jvm/jre \ + NODE_QUORUM=1 \ + PROMETHEUS_EXPORTER_VER=6.8.1.0 \ + OPENDISTRO_VER=0.10.1.2-redhat-00006 \ + PLUGIN_LOGLEVEL=INFO \ + RECOVER_AFTER_NODES=1 \ + RECOVER_EXPECTED_NODES=1 \ + RECOVER_AFTER_TIME=5m \ + DHE_TMP_KEY_SIZE=2048 \ + RELEASE_STREAM=prod \ + OPENSHIFT_CI=${OPENSHIFT_CI:-false} + +ARG MAVEN_REPO_URL=${MAVEN_REPO_URL:-file:///artifacts/} + +RUN packages="java-${JAVA_VER}-openjdk-headless \ + python36 python3-pyyaml \ + hostname \ + openssl \ + zip \ + unzip" && \ + yum install -y --setopt=tsflags=nodocs ${packages} && \ + rpm -V ${packages} && \ + alternatives --set python /usr/bin/python3 && \ + yum clean all + +ADD extra-jvm.options install-es.sh ci-env.sh /var/tmp + +# Since artifacts does not exist during CI build, include README.MD +# which will prevent the copy from raising an error. +RUN mkdir /artifacts +# In an OSBS build, this will COPY artifacts from fetch-artifacts-koji.yaml. In a CI build, it will just +# copy the README.MD. +COPY artifacts/* /artifacts +COPY *.zip / +RUN /var/tmp/install-es.sh + +ADD sgconfig/ ${HOME}/sgconfig/ +ADD index_templates/ ${ES_HOME}/index_templates/ +ADD index_patterns/ ${ES_HOME}/index_patterns/ +ADD init/ ${ES_HOME}/init/ +ADD probe/ ${ES_HOME}/probe/ +ADD init.sh run.sh ci-env.sh install.sh ${HOME}/ +COPY utils/** /usr/local/bin/ + +RUN ${HOME}/install.sh && rm -rf /artifacts + +WORKDIR ${HOME} +USER 1000 +CMD ["sh", "/opt/app-root/src/run.sh"] + +LABEL \ + License="GPLv2+" \ + io.k8s.description="Elasticsearch container for EFK aggregated logging storage" \ + io.k8s.display-name="Elasticsearch 6" \ + io.openshift.tags="logging,elk,elasticsearch" \ + vendor="Red Hat" \ + name="openshift/ose-logging-elasticsearch6" \ + com.redhat.component="logging-elasticsearch6-container" \ + io.openshift.maintainer.product="OpenShift Container Platform" \ + io.openshift.maintainer.component="Logging" \ + release="202112132021.p0.g2a13a81.assembly.stream" \ + io.openshift.build.commit.id="2a13a81f7fcbb01db50add1430a08e5ec4d38be6" \ + io.openshift.build.source-location="https://github.com/openshift/origin-aggregated-logging" \ + io.openshift.build.commit.url="https://github.com/openshift/origin-aggregated-logging/commit/2a13a81f7fcbb01db50add1430a08e5ec4d38be6" \ + version="v4.6.0" + + +ADD logging-elasticsearch6-container-v4.6.0-202112132021.p0.g2a13a81.assembly.stream.json /root/buildinfo/content_manifests/logging-elasticsearch6-container-v4.6.0-202112132021.p0.g2a13a81.assembly.stream.json +LABEL "com.redhat.license_terms"="https://www.redhat.com/agreements" "distribution-scope"="public" "vendor"="Red Hat, Inc." "build-date"="2021-12-13T20:25:40.077527" "architecture"="x86_64" "vcs-type"="git" "vcs-ref"="aed8119d2cc4255488e2fc623dfbea9faa4e997c" "com.redhat.build-host"="cpt-1003.osbs.prod.upshift.rdu2.redhat.com" "description"="Elasticsearch container for EFK aggregated logging storage" "url"="https://access.redhat.com/containers/#/registry.access.redhat.com/openshift/ose-logging-elasticsearch6/images/v4.6.0-202112132021.p0.g2a13a81.assembly.stream" \ No newline at end of file diff --git a/rhel/rhcc/testdata/Dockerfile-quay-clair-rhel8-v3.5.5-4 b/rhel/rhcc/testdata/Dockerfile-quay-clair-rhel8-v3.5.5-4 new file mode 100644 index 000000000..8d36a8406 --- /dev/null +++ b/rhel/rhcc/testdata/Dockerfile-quay-clair-rhel8-v3.5.5-4 @@ -0,0 +1,68 @@ +FROM sha256:0ced1c7c9b23d0e107c7b15d5a0017abbbcf7e64e49a4c9f9efa1b9589ca8b68 AS build-pip + +COPY $REMOTE_SOURCE $REMOTE_SOURCE_DIR +WORKDIR $REMOTE_SOURCE_DIR/app + +RUN INSTALL_PKGS="\ + python3 \ + gcc-c++ \ + " && \ + yum -y --setopt=tsflags=nodocs --setopt=skip_missing_names_on_install=False install $INSTALL_PKGS && \ + yum -y update && \ + yum -y clean all + +RUN alternatives --set python /usr/bin/python3 && \ + python -m pip install --no-cache-dir --upgrade setuptools pip && \ + python -m pip install --no-cache-dir -r requirements.txt --no-cache && \ + python -m pip freeze + + +FROM sha256:320c4c36df196f57b67205292ec1514cce5d6c742cbdc01443f465d931d2bbd4 AS build-gomod + +COPY --from=build-pip $REMOTE_SOURCE_DIR $REMOTE_SOURCE_DIR +WORKDIR $REMOTE_SOURCE_DIR/app + +ARG CLAIR_VERSION=v4.1.1 + +# RUN go mod vendor +RUN go build \ + -ldflags="-X main.Version=${CLAIR_VERSION}" \ + ./cmd/clair +RUN go build \ + ./cmd/clairctl + +FROM sha256:0ced1c7c9b23d0e107c7b15d5a0017abbbcf7e64e49a4c9f9efa1b9589ca8b68 + +LABEL com.redhat.component="quay-clair-container" +LABEL name="quay/clair-rhel8" +LABEL version="v3.5.5" +LABEL io.k8s.display-name="Red Hat Quay - Clair" +LABEL io.k8s.description="Red Hat Quay container image scanner" +LABEL summary="Red Hat Quay container image scanner" +LABEL maintainer="support@redhat.com" +LABEL io.openshift.tags="quay" + +RUN INSTALL_PKGS="\ + rpm \ + tar \ + " && \ + yum -y --setopt=tsflags=nodocs --setopt=skip_missing_names_on_install=False install $INSTALL_PKGS && \ + yum -y update && \ + yum -y clean all + +VOLUME /config +EXPOSE 6060 +WORKDIR /run +ENV CLAIR_CONF=/config/config.yaml +ENV CLAIR_MODE=combo +ENV SSL_CERT_DIR="/etc/ssl/certs:/etc/pki/tls/certs:/var/run/certs" +USER nobody:nobody + +COPY --from=build-pip /usr/local/bin/dumb-init /usr/bin/dumb-init +COPY --from=build-gomod $REMOTE_SOURCE_DIR/app/clair /bin/clair +COPY --from=build-gomod $REMOTE_SOURCE_DIR/app/clairctl /bin/clairctl + +ENTRYPOINT ["/usr/bin/dumb-init", "--", "/bin/clair"] + +ADD quay-clair-container-v3.5.5-4.json /root/buildinfo/content_manifests/quay-clair-container-v3.5.5-4.json +LABEL "release"="4" "com.redhat.license_terms"="https://www.redhat.com/agreements" "distribution-scope"="public" "vendor"="Red Hat, Inc." "build-date"="2021-08-02T15:56:16.824320" "architecture"="x86_64" "vcs-type"="git" "vcs-ref"="60e980fc47a5b270aa1118213135d951b875d43d" "com.redhat.build-host"="cpt-1007.osbs.prod.upshift.rdu2.redhat.com" "description"="Red Hat Quay container image scanner" "url"="https://access.redhat.com/containers/#/registry.access.redhat.com/quay/clair-rhel8/images/v3.5.5-4" diff --git a/rhel/rhcc/testdata/Dockerfile-quay-quay-rhel8-v3.5.6-4 b/rhel/rhcc/testdata/Dockerfile-quay-quay-rhel8-v3.5.6-4 new file mode 100644 index 000000000..0bcba7172 --- /dev/null +++ b/rhel/rhcc/testdata/Dockerfile-quay-quay-rhel8-v3.5.6-4 @@ -0,0 +1,156 @@ + +FROM sha256:ad42391b9b4670e68a759d4cdb8780896071a1cb1ed519b474f901e597bf3b3d AS build-npm + +COPY $REMOTE_SOURCE $REMOTE_SOURCE_DIR +WORKDIR $REMOTE_SOURCE_DIR/app + +RUN INSTALL_PKGS="\ + nodejs \ + " && \ + yum -y --setopt=tsflags=nodocs --setopt=skip_missing_names_on_install=False install $INSTALL_PKGS + +RUN cd source/config-tool/pkg/lib/editor && \ + npm config list && \ + npm install --ignore-engines --loglevel verbose && \ + npm run build + +RUN cd source/quay && \ + npm config list && \ + npm install --ignore-engines --loglevel verbose && \ + npm run build + + +FROM sha256:320c4c36df196f57b67205292ec1514cce5d6c742cbdc01443f465d931d2bbd4 AS build-gomod + +COPY --from=build-npm $REMOTE_SOURCE_DIR/app $REMOTE_SOURCE_DIR/app +WORKDIR $REMOTE_SOURCE_DIR/app + +COPY --from=build-npm $REMOTE_SOURCE_DIR/app/source/config-tool/pkg/lib/editor/static/build $REMOTE_SOURCE_DIR/app/source/config-tool/pkg/lib/editor/static/build + +# https://projects.engineering.redhat.com/browse/CLOUDBLD-1611 +# Until above is fixed, "go mod vendor" can't be run since the go.mod must be +# in the root dir (ie. no multiples). Once fixed, add "go mod vendor" as a step +# here and remove vendor dirs from quay-osbs repo +# +RUN cd source/config-tool && \ + go build ./cmd/config-tool + +RUN cd source/jwtproxy && \ + go build ./cmd/jwtproxy + +RUN cd source/pushgateway && \ + go build + + +FROM sha256:ad42391b9b4670e68a759d4cdb8780896071a1cb1ed519b474f901e597bf3b3d + +LABEL com.redhat.component="quay-registry-container" +LABEL name="quay/quay-rhel8" +LABEL version="v3.5.6" +LABEL io.k8s.display-name="Red Hat Quay" +LABEL io.k8s.description="Red Hat Quay" +LABEL summary="Red Hat Quay" +LABEL maintainer="support@redhat.com" +LABEL io.openshift.tags="quay" + +ENV PYTHON_VERSION=3.8 \ + PYTHON_ROOT=/usr/local/lib/python3.8 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LANG=en_US.utf8 + +ENV QUAYDIR=/quay-registry \ + QUAYCONF=/quay-registry/conf \ + QUAYPATH="." + +RUN mkdir $QUAYDIR +WORKDIR $QUAYDIR + +ARG PIP_CERT +COPY --from=build-npm $REMOTE_SOURCE_DIR $REMOTE_SOURCE_DIR +COPY --from=build-npm $PIP_CERT $PIP_CERT +RUN cp -Rp $REMOTE_SOURCE_DIR/app/source/quay/* $QUAYDIR + +COPY --from=build-gomod $REMOTE_SOURCE_DIR/app/source/config-tool/config-tool /usr/local/bin/config-tool +COPY --from=build-gomod $REMOTE_SOURCE_DIR/app/source/jwtproxy/jwtproxy /usr/local/bin/jwtproxy +COPY --from=build-gomod $REMOTE_SOURCE_DIR/app/source/config-tool/pkg/lib/editor $QUAYDIR/config_app +COPY --from=build-gomod $REMOTE_SOURCE_DIR/app/source/pushgateway/pushgateway /usr/local/bin/pushgateway + +RUN INSTALL_PKGS="\ + python38 \ + nginx \ + openldap \ + postgresql \ + gcc-c++ git \ + openldap-devel \ + dnsmasq \ + memcached \ + openssl \ + skopeo \ + python38-devel \ + libffi-devel \ + openssl-devel \ + postgresql-devel \ + libjpeg-devel \ + " && \ + yum -y --setopt=tsflags=nodocs --setopt=skip_missing_names_on_install=False install $INSTALL_PKGS && \ + yum -y update && \ + yum -y clean all + +RUN alternatives --set python /usr/bin/python3 && \ + python -m pip install --no-cache-dir --upgrade setuptools pip && \ + python -m pip install --no-cache-dir wheel && \ + python -m pip install --no-cache-dir -r requirements-osbs.txt --no-cache && \ + python -m pip freeze + +RUN ln -s $QUAYCONF /conf && \ + ln -sf /dev/stdout /var/log/nginx/access.log && \ + ln -sf /dev/stdout /var/log/nginx/error.log && \ + chmod -R a+rwx /var/log/nginx + +# Cleanup +RUN UNINSTALL_PKGS="\ + gcc-c++ git \ + openldap-devel \ + python38-devel \ + libffi-devel \ + openssl-devel \ + postgresql-devel \ + libjpeg-devel \ + kernel-headers \ + " && \ + yum remove -y $UNINSTALL_PKGS && \ + yum clean all && \ + rm -rf /var/cache/yum /tmp/* /var/tmp/* /root/.cache && \ + rm -rf $REMOTE_SOURCE_DIR + +EXPOSE 8080 8443 7443 + +RUN chgrp -R 0 $QUAYDIR && \ + chmod -R g=u $QUAYDIR + +RUN mkdir /datastorage && chgrp 0 /datastorage && chmod g=u /datastorage && \ + mkdir -p /var/log/nginx && chgrp 0 /var/log/nginx && chmod g=u /var/log/nginx && \ + mkdir -p /conf/stack && chgrp 0 /conf/stack && chmod g=u /conf/stack && \ + mkdir -p /tmp && chgrp 0 /tmp && chmod g=u /tmp && \ + chmod g=u /etc/passwd + +RUN chgrp 0 /var/log/nginx && \ + chmod g=u /var/log/nginx && \ + chgrp -R 0 /etc/pki/ca-trust/extracted && \ + chmod -R g=u /etc/pki/ca-trust/extracted && \ + chgrp -R 0 /etc/pki/ca-trust/source/anchors && \ + chmod -R g=u /etc/pki/ca-trust/source/anchors && \ + chgrp -R 0 /usr/local/lib/python3.8/site-packages/certifi && \ + chmod -R g=u /usr/local/lib/python3.8/site-packages/certifi + +VOLUME ["/var/log", "/datastorage", "/tmp", "/conf/stack"] + +USER 1001 + +ENTRYPOINT ["dumb-init", "--", "/quay-registry/quay-entrypoint.sh"] +CMD ["registry"] + +ADD quay-registry-container-v3.5.6-4.json /root/buildinfo/content_manifests/quay-registry-container-v3.5.6-4.json +LABEL "release"="4" "com.redhat.license_terms"="https://www.redhat.com/agreements" "distribution-scope"="public" "vendor"="Red Hat, Inc." "build-date"="2021-08-17T21:16:14.144538" "architecture"="x86_64" "vcs-type"="git" "vcs-ref"="0e033c625b6a775f5be730ce5a938aa91cc46d29" "com.redhat.build-host"="example.com" "description"="Red Hat Quay" "url"="https://access.redhat.com/containers/#/registry.access.redhat.com/quay/quay-rhel8/images/v3.5.6-4" diff --git a/rhel/rhcc/testdata/clair-rhel8-v3.5.5-4-indexreport.json b/rhel/rhcc/testdata/clair-rhel8-v3.5.5-4-indexreport.json new file mode 100644 index 000000000..bb82b90ef --- /dev/null +++ b/rhel/rhcc/testdata/clair-rhel8-v3.5.5-4-indexreport.json @@ -0,0 +1,120 @@ +{ + "err": "", + "state": "IndexFinished", + "success": true, + "packages": { + "32": { + "id": "32", + "cpe": "", + "arch": "x86_64", + "kind": "source", + "name": "quay-clair-container", + "source": { + "id": "1", + "cpe": "", + "name": "", + "version": "", + "normalized_version": "" + }, + "version": "v3.5.5-4", + "normalized_version": "rhctag:3.5.0.0.0.0.0.0.0.0" + }, + "34": { + "id": "34", + "cpe": "", + "arch": "x86_64", + "kind": "binary", + "name": "quay/clair-rhel8", + "source": { + "id": "32", + "cpe": "", + "arch": "x86_64", + "kind": "source", + "name": "quay-clair-container", + "version": "v3.5.5-4", + "normalized_version": "" + }, + "version": "v3.5.5-4", + "normalized_version": "rhctag:3.5.0.0.0.0.0.0.0.0" + }, + "36": { + "id": "36", + "cpe": "", + "arch": "x86_64", + "kind": "source", + "name": "ubi8-container", + "source": { + "id": "1", + "cpe": "", + "name": "", + "version": "", + "normalized_version": "" + }, + "version": "8.4-206.1626828523", + "normalized_version": "rhctag:8.4.0.0.0.0.0.0.0.0" + }, + "38": { + "id": "38", + "cpe": "", + "arch": "x86_64", + "kind": "binary", + "name": "ubi8", + "source": { + "id": "36", + "cpe": "", + "arch": "x86_64", + "kind": "source", + "name": "ubi8-container", + "version": "8.4-206.1626828523", + "normalized_version": "" + }, + "version": "8.4-206.1626828523", + "normalized_version": "rhctag:8.4.0.0.0.0.0.0.0.0" + } + }, + "repository": { + "1": { + "name": "Red Hat Container Catalog", + "uri": "https://catalog.redhat.com/software/containers/explore" + } + }, + "environments": { + "32": [ + { + "package_db": "root/buildinfo/Dockerfile-quay-clair-rhel8-v3.5.5-4", + "introduced_in": "sha256:a5ac7bbd6645d6b98e41600a1510d7378d74e6b0b858622b57fce2a8a05a87e5", + "repository_ids": [ + "1" + ] + } + ], + "34": [ + { + "package_db": "root/buildinfo/Dockerfile-quay-clair-rhel8-v3.5.5-4", + "introduced_in": "sha256:a5ac7bbd6645d6b98e41600a1510d7378d74e6b0b858622b57fce2a8a05a87e5", + "repository_ids": [ + "1" + ] + } + ], + "36": [ + { + "package_db": "root/buildinfo/Dockerfile-ubi8-8.4-206.1626828523", + "introduced_in": "sha256:a50df8fd88fecefc26fd331f832672108deb08cf9d2b303a5b86156a7f51b5d8", + "repository_ids": [ + "1" + ] + } + ], + "38": [ + { + "package_db": "root/buildinfo/Dockerfile-ubi8-8.4-206.1626828523", + "introduced_in": "sha256:a50df8fd88fecefc26fd331f832672108deb08cf9d2b303a5b86156a7f51b5d8", + "repository_ids": [ + "1" + ] + } + ] + }, + "manifest_hash": "sha256:e7a9c9de4b2375d2d846b6b3d3e476f9a180d5d2a27282f62ff8f5d6fb96889c" +} diff --git a/rhel/rhcc/testdata/cve-2020-8565.xml b/rhel/rhcc/testdata/cve-2020-8565.xml new file mode 100644 index 000000000..72c6c6b29 --- /dev/null +++ b/rhel/rhcc/testdata/cve-2020-8565.xml @@ -0,0 +1,44 @@ + + + + Moderate + 2020-10-14T00:00:00 + +CVE-2020-8565 kubernetes: Incomplete fix for CVE-2019-11250 allows for token leak in logs when logLevel >= 9 + + + 5.3 + CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N + + CWE-117 +
+In Kubernetes, if the logging level is set to at least 9, authorization and bearer tokens will be written to log files. This can occur both in API server logs and client tool output like kubectl. This affects <= v1.19.3, <= v1.18.10, <= v1.17.13, < v1.20.0-alpha2. +
+
+A flaw was found in kubernetes. In Kubernetes, if the logging level is to at least 9, authorization and bearer tokens will be written to log files. This can occur both in API server logs and client tool output like `kubectl`. Previously, CVE-2019-11250 was assigned for the same issue for logging levels of at least 4. +
+ +OpenShift Container Platform 4 does not support LogLevels higher than 8 (via 'TraceAll'), and is therefore not affected by this vulnerability. + + +Red Hat would like to thank the Kubernetes Product Security Committee for reporting this issue. Upstream acknowledges Patrick Rhomberg (purelyapplied) as the original reporter. + + + Red Hat OpenShift Container Storage 4.7.0 on RHEL-8 + 2021-05-19T00:00:00 + RHSA-2021:2041 + ocs4/rook-ceph-rhel8-operator:4.7-140.49a6fcf.release_4.7 + + + Red Hat OpenShift Container Storage 4.8.0 on RHEL-8 + 2021-08-03T00:00:00 + RHBA-2021:3003 + ocs4/rook-ceph-rhel8-operator:4.8-167.9a9db5f.release_4.8 + + kubernetes 1.20.0, kubernetes 1.19.6, kubernetes 1.18.14, kubernetes 1.17.16 + +https://github.com/kubernetes/kubernetes/issues/95623 +https://groups.google.com/g/kubernetes-announce/c/ScdmyORnPDk + +
+
\ No newline at end of file diff --git a/rhel/rhcc/testdata/cve-2021-3762.xml b/rhel/rhcc/testdata/cve-2021-3762.xml new file mode 100644 index 000000000..fdb57161e --- /dev/null +++ b/rhel/rhcc/testdata/cve-2021-3762.xml @@ -0,0 +1,133 @@ + + + + +
+Buffer overflow in NFS mountd gives root access to remote attackers, mostly in Linux systems. +
+ +This issue has been addressed in nfs-server packages as shipped in Red Hat Linux since version nfs-server-2.2beta37. + +
+ + + Low + 2004-09-30T00:00:00 + +CVE-2004-0976 security flaw + +
+Multiple scripts in the perl package in Trustix Secure Linux 1.5 through 2.1 and other operating systems allows local users to overwrite files via a symlink attack on temporary files. +
+ +Red Hat is aware of this issue and is tracking it via the following bug: +https://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=140058 + +The Red Hat Security Response Team has rated this issue as having low security impact, a future update may address this flaw. More information regarding issue severity can be found here: +http://www.redhat.com/security/updates/classification/ + +Red Hat Enterprise Linux 5 is not vulnerable to this issue as it contains a backported patch. + + + Red Hat Enterprise Linux 3 + 2005-12-20T00:00:00 + RHSA-2005:881 + perl-2:5.8.0-90.4 + +
+ + + Critical + 2013-06-25T00:00:00 + +CVE-2013-1690 Mozilla: Execution of unmapped memory through onreadystatechange event (MFSA 2013-53) + + + 6.8 + AV:N/AC:M/Au:N/C:P/I:P/A:P + +
+Mozilla Firefox before 22.0, Firefox ESR 17.x before 17.0.7, Thunderbird before 17.0.7, and Thunderbird ESR 17.x before 17.0.7 do not properly handle onreadystatechange events in conjunction with page reloading, which allows remote attackers to cause a denial of service (application crash) or possibly execute arbitrary code via a crafted web site that triggers an attempt to execute data at an unmapped memory location. +
+ + Red Hat Enterprise Linux 5 + 2013-06-25T00:00:00 + RHSA-2013:0981 + firefox-0:17.0.7-1.el5_9 + + + Red Hat Enterprise Linux 5 + 2013-06-25T00:00:00 + RHSA-2013:0981 + xulrunner-0:17.0.7-1.el5_9 + + + Red Hat Enterprise Linux 5 + 2013-06-25T00:00:00 + RHSA-2013:0982 + thunderbird-0:17.0.7-1.el5_9 + + + Red Hat Enterprise Linux 6 + 2013-06-25T00:00:00 + RHSA-2013:0981 + firefox-0:17.0.7-1.el6_4 + + + Red Hat Enterprise Linux 6 + 2013-06-25T00:00:00 + RHSA-2013:0981 + xulrunner-0:17.0.7-1.el6_4 + + + Red Hat Enterprise Linux 6 + 2013-06-25T00:00:00 + RHSA-2013:0982 + thunderbird-0:17.0.7-1.el6_4 + + +http://www.mozilla.org/security/announce/2013/mfsa2013-53.html + +
+ + + + + Critical + 2021-09-28T12:00:00 + +CVE-2021-3762 quay/claircore: directory traversal when scanning crafted container image layer allows for arbitrary file write + + + 9.8 + CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + + CWE-22 +
+A directory traversal vulnerability was found in the ClairCore engine of Clair. An attacker can exploit this by supplying a crafted container image which, when scanned by Clair, allows for arbitrary file write on the filesystem, potentially allowing for remote code execution. +
+ +Only a single version of Red Hat Quay, 3.5.6 is affected by this vulnerability. All previous released versions of Red Hat Quay are not affected by this vulnerability. + +The overall vulnerability is rated as Critical for the ClairCore engine, but only rated Important for the Red Hat Quay product. In Red Hat Quay, Clair runs as the 'nobody' user in an unprivileged container, limiting the impact to modification of non-sensitives files in that container. + +Red Hat Advanced Cluster Security is not affected by this vulnerability. + +Quay.io is not affected by this vulnerability. + + +Red Hat would like to thank Yanir Tsarimi (Orca Security) for reporting this issue. + + +Mitigation for this issue is either not available or the currently available options do not meet the Red Hat Product Security criteria comprising ease of use and deployment, applicability to widespread installation base or stability. + + + Red Hat Quay 3 + 2021-09-28T00:00:00 + RHSA-2021:3665 + quay/clair-rhel8:v3.5.7-8 + + quay/claircore 0.5.5, quay/claircore 0.4.8 +
+ +
diff --git a/rhel/rhcc/testdata/cve-2021-44228-openshift-logging.xml b/rhel/rhcc/testdata/cve-2021-44228-openshift-logging.xml new file mode 100644 index 000000000..fac478f50 --- /dev/null +++ b/rhel/rhcc/testdata/cve-2021-44228-openshift-logging.xml @@ -0,0 +1,146 @@ + + + + + Critical + 2021-12-10T02:01:00 + +CVE-2021-44228 log4j-core: Remote code execution in Log4j 2.x when logs contain an attacker-controlled string value + + + 9.8 + CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + + CWE-20 +
+Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0 (along with 2.12.2, 2.12.3, and 2.3.1), this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects. +
+
+A flaw was found in the Apache Log4j logging library in versions from 2.0.0 and before 2.15.0. A remote attacker who can control log messages or log message parameters, can execute arbitrary code on the server via JNDI LDAP endpoint. +
+ +This issue only affects log4j versions between 2.0 and 2.14.1. In order to exploit this flaw you need: +- A remotely accessible endpoint with any protocol (HTTP, TCP, etc) that allows an attacker to send arbitrary data, +- A log statement in the endpoint that logs the attacker controlled data. + +In Red Hat OpenShift Logging the vulnerable log4j library is shipped in the Elasticsearch components. Because Elasticsearch is not susceptible to remote code execution with this vulnerability due to use of the Java Security Manager and because access to these components is limited, the impact by this vulnerability is reduced to Moderate. + +As per upstream applications using Log4j 1.x may be impacted by this flaw if their configuration uses JNDI. However, the risk is much lower. This flaw in Log4j 1.x is tracked via https://access.redhat.com/security/cve/CVE-2021-4104 and has been rated as having Moderate security impact. + +The following products are NOT affected by this flaw and have been explicitly listed here for the benefit of our customers. +- Red Hat Enterprise Linux +- Red Hat Advanced Cluster Management for Kubernetes +- Red Hat Advanced Cluster Security for Kubernetes +- Red Hat Ansible Automation Platform (Engine and Tower) +- Red Hat Certificate System +- Red Hat Directory Server +- Red Hat Identity Management +- Red Hat CloudForms +- Red Hat Update Infrastructure +- Red Hat Satellite +- Red Hat Ceph Storage +- Red Hat Gluster Storage +- Red Hat OpenShift Data Foundation +- Red Hat OpenStack Platform +- Red Hat Virtualization +- Red Hat Single Sign-On + + +For Log4j versions >=2.10 +set the system property log4j2.formatMsgNoLookups or the environment variable LOG4J_FORMAT_MSG_NO_LOOKUPS to true + +For Log4j versions >=2.7 and <=2.14.1 +all PatternLayout patterns can be modified to specify the message converter as %m{nolookups} instead of just %m + +For Log4j versions >=2.0-beta9 and <=2.10.0 +remove the JndiLookup class from the classpath. For example: +``` +zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class +``` + +On OpenShift 4 and in OpenShift Logging, the above mitigation can be applied by following the steps in this article: https://access.redhat.com/solutions/6578421 + +On OpenShift 3.11, mitigation to the affected Elasticsearch component can be applied by following the steps in this article: https://access.redhat.com/solutions/6578441 + + + OpenShift Logging 5.0 + 2021-12-14T00:00:00 + RHSA-2021:5137 + openshift-logging/elasticsearch6-rhel8:v5.0.10-1 + + + OpenShift Logging 5.1 + 2021-12-14T00:00:00 + RHSA-2021:5128 + openshift-logging/elasticsearch6-rhel8:v6.8.1-67 + + + OpenShift Logging 5.2 + 2021-12-14T00:00:00 + RHSA-2021:5127 + openshift-logging/elasticsearch6-rhel8:v6.8.1-66 + + + OpenShift Logging 5.3 + 2021-12-14T00:00:00 + RHSA-2021:5129 + openshift-logging/elasticsearch6-rhel8:v6.8.1-65 + + + log4j 2.15.0 + +https://github.com/advisories/GHSA-jfh8-c2jp-5v3q +https://logging.apache.org/log4j/2.x/security.html +https://www.lunasec.io/docs/blog/log4j-zero-day/ + + True +
+ +
diff --git a/rhel/rhcc/testdata/cve-2021-44228-ose-metering-hive.xml b/rhel/rhcc/testdata/cve-2021-44228-ose-metering-hive.xml new file mode 100644 index 000000000..be586a62f --- /dev/null +++ b/rhel/rhcc/testdata/cve-2021-44228-ose-metering-hive.xml @@ -0,0 +1,146 @@ + + + + + Critical + 2021-12-10T02:01:00 + +CVE-2021-44228 log4j-core: Remote code execution in Log4j 2.x when logs contain an attacker-controlled string value + + + 9.8 + CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + + CWE-20 +
+Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0 (along with 2.12.2, 2.12.3, and 2.3.1), this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects. +
+
+A flaw was found in the Apache Log4j logging library in versions from 2.0.0 and before 2.15.0. A remote attacker who can control log messages or log message parameters, can execute arbitrary code on the server via JNDI LDAP endpoint. +
+ +This issue only affects log4j versions between 2.0 and 2.14.1. In order to exploit this flaw you need: +- A remotely accessible endpoint with any protocol (HTTP, TCP, etc) that allows an attacker to send arbitrary data, +- A log statement in the endpoint that logs the attacker controlled data. + +In Red Hat OpenShift Logging the vulnerable log4j library is shipped in the Elasticsearch components. Because Elasticsearch is not susceptible to remote code execution with this vulnerability due to use of the Java Security Manager and because access to these components is limited, the impact by this vulnerability is reduced to Moderate. + +As per upstream applications using Log4j 1.x may be impacted by this flaw if their configuration uses JNDI. However, the risk is much lower. This flaw in Log4j 1.x is tracked via https://access.redhat.com/security/cve/CVE-2021-4104 and has been rated as having Moderate security impact. + +The following products are NOT affected by this flaw and have been explicitly listed here for the benefit of our customers. +- Red Hat Enterprise Linux +- Red Hat Advanced Cluster Management for Kubernetes +- Red Hat Advanced Cluster Security for Kubernetes +- Red Hat Ansible Automation Platform (Engine and Tower) +- Red Hat Certificate System +- Red Hat Directory Server +- Red Hat Identity Management +- Red Hat CloudForms +- Red Hat Update Infrastructure +- Red Hat Satellite +- Red Hat Ceph Storage +- Red Hat Gluster Storage +- Red Hat OpenShift Data Foundation +- Red Hat OpenStack Platform +- Red Hat Virtualization +- Red Hat Single Sign-On + + +For Log4j versions >=2.10 +set the system property log4j2.formatMsgNoLookups or the environment variable LOG4J_FORMAT_MSG_NO_LOOKUPS to true + +For Log4j versions >=2.7 and <=2.14.1 +all PatternLayout patterns can be modified to specify the message converter as %m{nolookups} instead of just %m + +For Log4j versions >=2.0-beta9 and <=2.10.0 +remove the JndiLookup class from the classpath. For example: +``` +zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class +``` + +On OpenShift 4 and in OpenShift Logging, the above mitigation can be applied by following the steps in this article: https://access.redhat.com/solutions/6578421 + +On OpenShift 3.11, mitigation to the affected Elasticsearch component can be applied by following the steps in this article: https://access.redhat.com/solutions/6578441 + + + + + Red Hat OpenShift Container Platform 4.6 + 2021-12-16T00:00:00 + RHSA-2021:5106 + openshift4/ose-metering-hive:v4.6.0-202112140546.p0.g8b9da97.assembly.stream + + + + Red Hat OpenShift Container Platform 4.7 + 2021-12-16T00:00:00 + RHSA-2021:5107 + openshift4/ose-metering-hive:v4.7.0-202112140553.p0.g091bb99.assembly.stream + + + + Red Hat OpenShift Container Platform 4.8 + 2021-12-14T00:00:00 + RHSA-2021:5108 + openshift4/ose-metering-hive:v4.8.0-202112132154.p0.g57dd03a.assembly.stream + + + log4j 2.15.0 + +https://github.com/advisories/GHSA-jfh8-c2jp-5v3q +https://logging.apache.org/log4j/2.x/security.html +https://www.lunasec.io/docs/blog/log4j-zero-day/ + + True +
+ +
diff --git a/rhel/rhcc/testdata/rook-ceph-operator-container-4.6-115.d1788e1.release_4.6-indexreport.json b/rhel/rhcc/testdata/rook-ceph-operator-container-4.6-115.d1788e1.release_4.6-indexreport.json new file mode 100644 index 000000000..bd859a881 --- /dev/null +++ b/rhel/rhcc/testdata/rook-ceph-operator-container-4.6-115.d1788e1.release_4.6-indexreport.json @@ -0,0 +1,68 @@ +{ + "err": "", + "state": "IndexFinished", + "success": true, + "packages": { + "4538": { + "id": "4538", + "cpe": "", + "arch": "x86_64", + "kind": "source", + "name": "rook-ceph-operator-container", + "source": { + "id": "1", + "cpe": "", + "name": "", + "version": "", + "normalized_version": "" + }, + "version": "4.6-115.d1788e1.release_4.6", + "normalized_version": "rhctag:4.6.0.0.0.0.0.0.0.0" + }, + "4540": { + "id": "4540", + "cpe": "", + "arch": "x86_64", + "kind": "binary", + "name": "ocs4/rook-ceph-rhel8-operator", + "source": { + "id": "4538", + "cpe": "", + "arch": "x86_64", + "kind": "source", + "name": "rook-ceph-operator-container", + "version": "4.6-115.d1788e1.release_4.6", + "normalized_version": "" + }, + "version": "4.6-115.d1788e1.release_4.6", + "normalized_version": "rhctag:4.6.0.0.0.0.0.0.0.0" + } + }, + "repository": { + "1": { + "name": "Red Hat Container Catalog", + "uri": "https://catalog.redhat.com/software/containers/explore" + } + }, + "environments": { + "4538": [ + { + "package_db": "root/buildinfo/Dockerfile-rook-ceph-4.6-115.d1788e1.release_4.6", + "introduced_in": "sha256:b922d7e2891ae5710b2e2f6dec59751e1ed086df761bc1ec37440f1f7b6a0247", + "repository_ids": [ + "1" + ] + } + ], + "4540": [ + { + "package_db": "root/buildinfo/Dockerfile-rook-ceph-4.6-115.d1788e1.release_4.6", + "introduced_in": "sha256:b922d7e2891ae5710b2e2f6dec59751e1ed086df761bc1ec37440f1f7b6a0247", + "repository_ids": [ + "1" + ] + } + ] + }, + "manifest_hash": "sha256:c71f151c22429e4833cb512dcab641a9cbc6400d95d0c24b577e1ab97bda5f33" +} diff --git a/rhel/rhcc/testdata/rook-ceph-operator-container-4.7-159.76b9b11.release_4.7-indexreport.json b/rhel/rhcc/testdata/rook-ceph-operator-container-4.7-159.76b9b11.release_4.7-indexreport.json new file mode 100644 index 000000000..db2e6708e --- /dev/null +++ b/rhel/rhcc/testdata/rook-ceph-operator-container-4.7-159.76b9b11.release_4.7-indexreport.json @@ -0,0 +1,68 @@ +{ + "err": "", + "state": "IndexFinished", + "success": true, + "packages": { + "4538": { + "id": "4538", + "cpe": "", + "arch": "x86_64", + "kind": "source", + "name": "rook-ceph-operator-container", + "source": { + "id": "1", + "cpe": "", + "name": "", + "version": "", + "normalized_version": "" + }, + "version": "4.7-159.76b9b11.release_4.7", + "normalized_version": "rhctag:4.7.0.0.0.0.0.0.0.0" + }, + "4540": { + "id": "4540", + "cpe": "", + "arch": "x86_64", + "kind": "binary", + "name": "ocs4/rook-ceph-rhel8-operator", + "source": { + "id": "4538", + "cpe": "", + "arch": "x86_64", + "kind": "source", + "name": "rook-ceph-operator-container", + "version": "4.7-159.76b9b11.release_4.7", + "normalized_version": "" + }, + "version": "4.7-159.76b9b11.release_4.7", + "normalized_version": "rhctag:4.7.0.0.0.0.0.0.0.0" + } + }, + "repository": { + "1": { + "name": "Red Hat Container Catalog", + "uri": "https://catalog.redhat.com/software/containers/explore" + } + }, + "environments": { + "4538": [ + { + "package_db": "root/buildinfo/Dockerfile-rook-ceph-4.7-159.76b9b11.release_4.7", + "introduced_in": "sha256:b922d7e2891ae5710b2e2f6dec59751e1ed086df761bc1ec37440f1f7b6a0247", + "repository_ids": [ + "1" + ] + } + ], + "4540": [ + { + "package_db": "root/buildinfo/Dockerfile-rook-ceph-4.7-159.76b9b11.release_4.7", + "introduced_in": "sha256:b922d7e2891ae5710b2e2f6dec59751e1ed086df761bc1ec37440f1f7b6a0247", + "repository_ids": [ + "1" + ] + } + ] + }, + "manifest_hash": "sha256:c71f151c22429e4833cb512dcab641a9cbc6400d95d0c24b577e1ab97bda5f33" +} diff --git a/rhel/rhcc/updater.go b/rhel/rhcc/updater.go new file mode 100644 index 000000000..8b0eb56c1 --- /dev/null +++ b/rhel/rhcc/updater.go @@ -0,0 +1,251 @@ +package rhcc + +import ( + "context" + "encoding/xml" + "fmt" + "io" + "net/http" + "sort" + + "github.com/quay/zlog" + + "github.com/quay/claircore" + "github.com/quay/claircore/libvuln/driver" + "github.com/quay/claircore/pkg/cpe" + "github.com/quay/claircore/pkg/rhctag" + "github.com/quay/claircore/pkg/tmp" + "github.com/quay/claircore/rhel" +) + +const ( + dbURL = "https://access.redhat.com/security/data/metrics/cvemap.xml" +) + +var ( + _ driver.Updater = (*updater)(nil) + _ driver.Configurable = (*updater)(nil) +) + +// updater fetches and parses cvemap.xml +type updater struct { + client *http.Client + url string +} + +type UpdaterConfig struct { + URL string `json:"url" yaml:"url"` +} + +const updaterName = "rhel-container-updater" + +func (*updater) Name() string { + return updaterName +} + +func UpdaterSet(_ context.Context) (driver.UpdaterSet, error) { + us := driver.NewUpdaterSet() + if err := us.Add(&updater{}); err != nil { + return us, err + } + return us, nil +} + +func (u *updater) Configure(ctx context.Context, f driver.ConfigUnmarshaler, c *http.Client) error { + u.url = dbURL + u.client = c + var cfg UpdaterConfig + if err := f(&cfg); err != nil { + return err + } + if cfg.URL != "" { + u.url = cfg.URL + } + return nil +} + +func (u *updater) Fetch(ctx context.Context, hint driver.Fingerprint) (io.ReadCloser, driver.Fingerprint, error) { + ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/Updater.Fetch") + + zlog.Info(ctx).Str("database", u.url).Msg("starting fetch") + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.url, nil) + if err != nil { + return nil, hint, fmt.Errorf("rhcc: unable to construct request: %w", err) + } + + if hint != "" { + zlog.Debug(ctx). + Str("hint", string(hint)). + Msg("using hint") + req.Header.Set("if-none-match", string(hint)) + } + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, hint, fmt.Errorf("rhcc: error making request: %w", err) + } + defer res.Body.Close() + + switch res.StatusCode { + case http.StatusOK: + if t := string(hint); t == "" || t != res.Header.Get("etag") { + break + } + fallthrough + case http.StatusNotModified: + zlog.Info(ctx).Msg("database unchanged since last fetch") + return nil, hint, driver.Unchanged + default: + return nil, hint, fmt.Errorf("rhcc: http response error: %s %d", res.Status, res.StatusCode) + } + zlog.Debug(ctx).Msg("successfully requested database") + + tf, err := tmp.NewFile("", updaterName+".") + if err != nil { + return nil, hint, fmt.Errorf("rhcc: unable to open tempfile: %w", err) + } + zlog.Debug(ctx). + Str("name", tf.Name()). + Msg("created tempfile") + + var r io.Reader = res.Body + if _, err := io.Copy(tf, r); err != nil { + tf.Close() + return nil, hint, fmt.Errorf("rhcc: unable to copy resp body to tempfile: %w", err) + } + if n, err := tf.Seek(0, io.SeekStart); err != nil || n != 0 { + tf.Close() + return nil, hint, fmt.Errorf("rhcc: unable to seek database to start: %w", err) + } + zlog.Debug(ctx).Msg("decompressed and buffered database") + + hint = driver.Fingerprint(res.Header.Get("etag")) + zlog.Debug(ctx). + Str("hint", string(hint)). + Msg("using new hint") + + return tf, hint, nil +} + +func (u *updater) Parse(ctx context.Context, r io.ReadCloser) ([]*claircore.Vulnerability, error) { + ctx = zlog.ContextWithValues(ctx, "component", "rhel/rhcc/Updater.Parse") + zlog.Info(ctx).Msg("parse start") + defer r.Close() + defer zlog.Info(ctx).Msg("parse done") + + var cvemap cveMap + if err := xml.NewDecoder(r).Decode(&cvemap); err != nil { + return nil, fmt.Errorf("rhel: unable to decode cvemap: %w", err) + } + zlog.Debug(ctx).Msg("xml decoded") + + zlog.Debug(ctx). + Int("count", len(cvemap.RedHatVulnerabilities)).Msg("found raw entries") + + vs := []*claircore.Vulnerability{} + for _, vuln := range cvemap.RedHatVulnerabilities { + description := getDescription(vuln.Details) + versionsByContainer := make(map[string]map[rhctag.Version]*consolidatedRelease) + for _, release := range vuln.AffectedReleases { + match, packageName, version := parseContainerPackage(release.Package) + if !match { + continue + } + // parse version + v, err := rhctag.Parse(version) + if err != nil { + zlog.Debug(ctx). + Str("package", packageName). + Str("version", version). + Err(err). + Msgf("tag parse error") + continue + } + // parse severity + var severity string + if release.Impact != "" { + severity = release.Impact + } else { + severity = vuln.ThreatSeverity + } + // parse cpe + cpe, err := cpe.Unbind(release.Cpe) + if err != nil { + zlog.Warn(ctx). + Err(err). + Str("cpe", release.Cpe). + Msg("could not unbind cpe") + continue + } + // collect minor keys + minorKey := v.MinorStart() + // initialize and update the minorKey to consolidated release map + if versionsByContainer[packageName] == nil { + versionsByContainer[packageName] = make(map[rhctag.Version]*consolidatedRelease) + } + versionsByContainer[packageName][minorKey] = &consolidatedRelease{ + Cpe: cpe, + Issued: release.ReleaseDate.time, + Severity: severity, + AdvisoryLink: release.Advisory.URL, + } + // initialize and update the fixed in versions slice + if versionsByContainer[packageName][minorKey].FixedInVersions == nil { + vs := make(rhctag.Versions, 0) + versionsByContainer[packageName][minorKey].FixedInVersions = &vs + } + newVersions := versionsByContainer[packageName][minorKey].FixedInVersions.Append(v) + versionsByContainer[packageName][minorKey].FixedInVersions = &newVersions + } + + // Build the Vulnerability slice + for pkg, releasesByMinor := range versionsByContainer { + p := &claircore.Package{ + Name: pkg, + Kind: claircore.BINARY, + } + // sort minor keys + minorKeys := make(rhctag.Versions, 0) + for k := range releasesByMinor { + minorKeys = append(minorKeys, k) + } + sort.Sort(minorKeys) + // iterate minor key map in order + for idx, minor := range minorKeys { + // sort the fixed in versions + sort.Sort(releasesByMinor[minor].FixedInVersions) + // The first minor version range should match all previous versions + start := minor + if idx == 0 { + start = rhctag.Version{} + } + // For containers such as openshift-logging/elasticsearch6-rhel8 we need to match + // the first Fixed in Version here. + // Most of the time this will return the only Fixed In Version for minor version + firstPatch, _ := releasesByMinor[minor].FixedInVersions.First() + r := &claircore.Range{ + Lower: start.Version(true), + Upper: firstPatch.Version(false), + } + v := &claircore.Vulnerability{ + Updater: updaterName, + Name: vuln.Name, + Description: description, + Issued: releasesByMinor[minor].Issued, + Severity: releasesByMinor[minor].Severity, + NormalizedSeverity: rhel.NormalizeSeverity(releasesByMinor[minor].Severity), + Package: p, + Repo: &goldRepo, + Links: releasesByMinor[minor].AdvisoryLink, + FixedInVersion: firstPatch.Original, + Range: r, + } + vs = append(vs, v) + } + } + } + zlog.Debug(ctx). + Int("count", len(vs)). + Msg("found vulnerabilities") + return vs, nil +}