From 77ea72500e115d5b9e958f63845eaba5b99b3a1c Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Thu, 30 Mar 2023 04:30:22 -0400 Subject: [PATCH] Add support for Chainguard distro (#190) Signed-off-by: Dan Luhring --- .github/workflows/update.yml | 4 + README.md | 8 +- chainguard/chainguard.go | 73 ++++++++++++++ chainguard/chainguard_test.go | 115 +++++++++++++++++++++++ chainguard/testdata/golden/binutils.json | 20 ++++ chainguard/testdata/security.json | 26 +++++ main.go | 12 ++- 7 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 chainguard/chainguard.go create mode 100644 chainguard/chainguard_test.go create mode 100644 chainguard/testdata/golden/binutils.json create mode 100644 chainguard/testdata/security.json diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 8181215..4f4fbe7 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -111,6 +111,10 @@ jobs: name: Wolfi Secdb run: ./vuln-list-update -target wolfi + - if: always() + name: Chainguard Secdb + run: ./vuln-list-update -target chainguard + - if: always() name: Known Exploited Vulnerabilities Catalog run: ./vuln-list-update -target kevc diff --git a/README.md b/README.md index fb30634..ea09e63 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,13 @@ https://github.com/aquasecurity/vuln-list/ $ vuln-list-update -h Usage of vuln-list-update: -target string - update target (nvd, alpine, alpine-unfixed, redhat, redhat-oval, debian, debian-oval, ubuntu, amazon, oracle-oval, suse-cvrf, photon, arch-linux, ghsa, glad, cwe, osv, go-vulndb, mariner, kevc) + update target (nvd, alpine, alpine-unfixed, redhat, redhat-oval, debian, debian-oval, ubuntu, amazon, oracle-oval, suse-cvrf, photon, arch-linux, ghsa, glad, cwe, osv, go-vulndb, mariner, kevc, wolfi, chainguard) + -target-branch string + alternative repository branch (only glad) + -target-uri string + alternative repository URI (only glad) -years string - update years (only redhat) + update years (only redhat) ``` ## Author diff --git a/chainguard/chainguard.go b/chainguard/chainguard.go new file mode 100644 index 0000000..69d4f14 --- /dev/null +++ b/chainguard/chainguard.go @@ -0,0 +1,73 @@ +package chainguard + +import ( + "log" + "net/url" + "path/filepath" + + "github.com/spf13/afero" + "golang.org/x/xerrors" + + "github.com/aquasecurity/vuln-list-update/alpine" + "github.com/aquasecurity/vuln-list-update/utils" +) + +const ( + chainguardDir = "chainguard" + secdbURLBase = "https://packages.cgr.dev" + secdbURLPath = "chainguard/security.json" +) + +type option func(c *Updater) + +func WithVulnListDir(v string) option { + return func(c *Updater) { c.vulnListDir = v } +} + +func WithAppFs(v afero.Fs) option { + return func(c *Updater) { c.appFs = v } +} + +func WithBaseURL(v *url.URL) option { + return func(c *Updater) { c.baseURL = v } +} + +type Updater struct { + vulnListDir string + appFs afero.Fs + baseURL *url.URL +} + +func NewUpdater(options ...option) *Updater { + u, _ := url.Parse(secdbURLBase) + updater := &Updater{ + vulnListDir: utils.VulnListDir(), + appFs: afero.NewOsFs(), + baseURL: u, + } + for _, option := range options { + option(updater) + } + + return updater +} + +func (u *Updater) Update() error { + dir := filepath.Join(u.vulnListDir, chainguardDir) + log.Printf("Remove Chainguard directory %s", dir) + if err := u.appFs.RemoveAll(dir); err != nil { + return xerrors.Errorf("failed to remove Chainguard directory: %w", err) + } + if err := u.appFs.MkdirAll(dir, 0755); err != nil { + return xerrors.Errorf("Chainguard mkdir error: %w", err) + } + + log.Println("Fetching Chainguard data...") + + alpineUpdater := alpine.NewUpdater( + alpine.WithBaseURL(u.baseURL), + alpine.WithVulnListDir(u.vulnListDir), + alpine.WithAdvisoryDir(chainguardDir), + alpine.WithAppFs(u.appFs)) + return alpineUpdater.Save("", secdbURLPath) +} diff --git a/chainguard/chainguard_test.go b/chainguard/chainguard_test.go new file mode 100644 index 0000000..fc31589 --- /dev/null +++ b/chainguard/chainguard_test.go @@ -0,0 +1,115 @@ +package chainguard_test + +import ( + "flag" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/aquasecurity/vuln-list-update/chainguard" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var update = flag.Bool("update", false, "update golden files") + +func TestUpdater_Update(t *testing.T) { + type fields struct { + appFs afero.Fs + retry int + } + tests := []struct { + name string + fields fields + fileNames map[string]string + goldenFiles map[string]string + wantErr string + }{ + { + name: "happy path", + fields: fields{ + appFs: afero.NewMemMapFs(), + retry: 0, + }, + fileNames: map[string]string{ + "/chainguard/security.json": "testdata/security.json", + }, + goldenFiles: map[string]string{ + "/tmp/chainguard/chainguard/binutils.json": "testdata/golden/binutils.json", + }, + }, + { + name: "404", + fields: fields{ + appFs: afero.NewMemMapFs(), + retry: 0, + }, + fileNames: map[string]string{ + "/": "testdata/index.html", + }, + wantErr: "status code: 404", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fileName, ok := tt.fileNames[r.URL.Path] + if !ok { + http.NotFound(w, r) + return + } + http.ServeFile(w, r, fileName) + })) + defer ts.Close() + + baseURL, err := url.Parse(ts.URL) + require.NoError(t, err) + + u := chainguard.NewUpdater( + chainguard.WithVulnListDir("/tmp"), + chainguard.WithBaseURL(baseURL), + chainguard.WithAppFs(tt.fields.appFs)) + err = u.Update() + if tt.wantErr != "" { + require.NotNil(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } else { + assert.NoError(t, err) + } + + fileCount := 0 + err = afero.Walk(tt.fields.appFs, "/", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + fileCount++ + + actual, err := afero.ReadFile(tt.fields.appFs, path) + assert.NoError(t, err, path) + + goldenPath, ok := tt.goldenFiles[path] + require.True(t, ok, path) + if *update { + err = ioutil.WriteFile(goldenPath, actual, 0666) + require.NoError(t, err, goldenPath) + } + expected, err := ioutil.ReadFile(goldenPath) + assert.NoError(t, err, goldenPath) + + assert.JSONEq(t, string(expected), string(actual), path) + + return nil + }) + assert.Equal(t, len(tt.goldenFiles), fileCount) + assert.NoError(t, err) + }) + } +} diff --git a/chainguard/testdata/golden/binutils.json b/chainguard/testdata/golden/binutils.json new file mode 100644 index 0000000..9de0791 --- /dev/null +++ b/chainguard/testdata/golden/binutils.json @@ -0,0 +1,20 @@ +{ + "name": "binutils", + "secfixes": { + "2.39-r1": [ + "CVE-2022-38126" + ], + "2.39-r2": [ + "CVE-2022-38533" + ], + "2.39-r3": [ + "CVE-2022-38128" + ] + }, + "apkurl": "{{urlprefix}}/{{reponame}}/{{arch}}/{{pkg.name}}-{{pkg.ver}}.apk", + "archs": [ + "x86_64" + ], + "urlprefix": "https://packages.cgr.dev", + "reponame": "chainguard" +} \ No newline at end of file diff --git a/chainguard/testdata/security.json b/chainguard/testdata/security.json new file mode 100644 index 0000000..630b061 --- /dev/null +++ b/chainguard/testdata/security.json @@ -0,0 +1,26 @@ +{ + "apkurl": "{{urlprefix}}/{{reponame}}/{{arch}}/{{pkg.name}}-{{pkg.ver}}.apk", + "archs": [ + "x86_64" + ], + "reponame": "chainguard", + "urlprefix": "https://packages.cgr.dev", + "packages": [ + { + "pkg": { + "name": "binutils", + "secfixes": { + "2.39-r1": [ + "CVE-2022-38126" + ], + "2.39-r2": [ + "CVE-2022-38533" + ], + "2.39-r3": [ + "CVE-2022-38128" + ] + } + } + } + ] +} diff --git a/main.go b/main.go index 4911ef9..eaba1cb 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/aquasecurity/vuln-list-update/chainguard" "github.com/aquasecurity/vuln-list-update/kevc" "github.com/aquasecurity/vuln-list-update/wolfi" @@ -49,7 +50,7 @@ const ( var ( target = flag.String("target", "", "update target (nvd, alpine, alpine-unfixed, redhat, redhat-oval, "+ - "debian, debian-oval, ubuntu, amazon, oracle-oval, suse-cvrf, photon, arch-linux, ghsa, glad, cwe, osv, go-vulndb, mariner, kevc, wolfi)") + "debian, debian-oval, ubuntu, amazon, oracle-oval, suse-cvrf, photon, arch-linux, ghsa, glad, cwe, osv, go-vulndb, mariner, kevc, wolfi, chainguard)") years = flag.String("years", "", "update years (only redhat)") targetUri = flag.String("target-uri", "", "alternative repository URI (only glad)") targetBranch = flag.String("target-branch", "", "alternative repository branch (only glad)") @@ -75,6 +76,7 @@ func run() error { url := fmt.Sprintf(repoURL, githubToken, repoOwner, repoName) log.Printf("target repository is %s/%s\n", repoOwner, repoName) + log.Printf("cloning/pulling into %s", utils.VulnListDir()) if _, err := gc.CloneOrPull(url, utils.VulnListDir(), "main", debug); err != nil { return xerrors.Errorf("clone or pull error: %w", err) @@ -234,7 +236,13 @@ func run() error { if err := wu.Update(); err != nil { return xerrors.Errorf("Wolfi update error: %w", err) } - commitMsg = "Wolfi Issue Tracker" + commitMsg = "Wolfi Security Data" + case "chainguard": + cu := chainguard.NewUpdater() + if err := cu.Update(); err != nil { + return xerrors.Errorf("Chainguard update error: %w", err) + } + commitMsg = "Chainguard Security Data" default: return xerrors.New("unknown target") }