From 4d919c3b2af2f9029baca037b6ae06bb3a2e52ec Mon Sep 17 00:00:00 2001 From: MaineK00n Date: Tue, 18 Jan 2022 22:45:06 +0900 Subject: [PATCH] feat(rocky): support Rocky Linux (#107) --- .github/workflows/update.yml | 4 + main.go | 7 + rocky/rocky.go | 251 ++++++++++++++++++ rocky/rocky_test.go | 74 ++++++ rocky/testdata/fixtures/happy/repomd.xml | 77 ++++++ .../testdata/fixtures/happy/updateinfo.xml.gz | Bin 0 -> 2585 bytes .../fixtures/repomd_invalid/repomd.xml | 3 + .../fixtures/updateinfo_invalid/repomd.xml | 77 ++++++ .../updateinfo_invalid/updateinfo.xml.gz | Bin 0 -> 1380 bytes .../testdata/golden/2021/RLSA-2021:2575.json | 65 +++++ .../testdata/golden/2021/RLSA-2021:3057.json | 191 +++++++++++++ 11 files changed, 749 insertions(+) create mode 100644 rocky/rocky.go create mode 100644 rocky/rocky_test.go create mode 100644 rocky/testdata/fixtures/happy/repomd.xml create mode 100644 rocky/testdata/fixtures/happy/updateinfo.xml.gz create mode 100644 rocky/testdata/fixtures/repomd_invalid/repomd.xml create mode 100644 rocky/testdata/fixtures/updateinfo_invalid/repomd.xml create mode 100644 rocky/testdata/fixtures/updateinfo_invalid/updateinfo.xml.gz create mode 100644 rocky/testdata/golden/2021/RLSA-2021:2575.json create mode 100644 rocky/testdata/golden/2021/RLSA-2021:3057.json diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 498f953..9fe9591 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -91,6 +91,10 @@ jobs: name: AlmaLinux Security Advisory run: ./vuln-list-update -target alma + - if: always() + name: Rocky Linux Security Advisory + run: ./vuln-list-update -target rocky + - if: always() name: OSV Database run: ./vuln-list-update -target osv diff --git a/main.go b/main.go index 6308495..e61d7fb 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( "github.com/aquasecurity/vuln-list-update/photon" redhatoval "github.com/aquasecurity/vuln-list-update/redhat/oval" "github.com/aquasecurity/vuln-list-update/redhat/securitydataapi" + "github.com/aquasecurity/vuln-list-update/rocky" susecvrf "github.com/aquasecurity/vuln-list-update/suse/cvrf" "github.com/aquasecurity/vuln-list-update/ubuntu" "github.com/aquasecurity/vuln-list-update/utils" @@ -195,6 +196,12 @@ func run() error { return xerrors.Errorf("AlmaLinux update error: %w", err) } commitMsg = "AlmaLinux Security Advisory" + case "rocky": + rc := rocky.NewConfig() + if err := rc.Update(); err != nil { + return xerrors.Errorf("Rocky Linux update error: %w", err) + } + commitMsg = "Rocky Linux Security Advisory" case "osv": p := osv.NewOsv() if err := p.Update(); err != nil { diff --git a/rocky/rocky.go b/rocky/rocky.go new file mode 100644 index 0000000..657219d --- /dev/null +++ b/rocky/rocky.go @@ -0,0 +1,251 @@ +package rocky + +import ( + "bytes" + "compress/gzip" + "encoding/xml" + "fmt" + "log" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/aquasecurity/vuln-list-update/utils" + "github.com/cheggaaa/pb/v3" + "golang.org/x/xerrors" +) + +const ( + retry = 3 + rockyDir = "rocky" +) + +var ( + urlFormat = "https://download.rockylinux.org/pub/rocky/%s/%s/%s/os/" + defaultReleases = []string{"8"} + defaultRepos = []string{"BaseOS", "AppStream", "extras"} + defaultArches = []string{"x86_64", "aarch64"} +) + +// RepoMd has repomd data +type RepoMd struct { + RepoList []Repo `xml:"data"` +} + +// Repo has a repo data +type Repo struct { + Type string `xml:"type,attr"` + Location Location `xml:"location"` +} + +// Location has a location of repomd +type Location struct { + Href string `xml:"href,attr"` +} + +// UpdateInfo has a list +type UpdateInfo struct { + RLSAList []RLSA `xml:"update"` +} + +// RLSA has detailed data of RLSA +type RLSA struct { + ID string `xml:"id" json:"id,omitempty"` + Title string `xml:"title" json:"title,omitempty"` + Issued Date `xml:"issued" json:"issued,omitempty"` + Updated Date `xml:"updated" json:"updated,omitempty"` + Severity string `xml:"severity" json:"severity,omitempty"` + Description string `xml:"description" json:"description,omitempty"` + Packages []Package `xml:"pkglist>collection>package" json:"packages,omitempty"` + References []Reference `xml:"references>reference" json:"references,omitempty"` + CveIDs []string `json:"cveids,omitempty"` +} + +// Date has time information +type Date struct { + Date string `xml:"date,attr" json:"date,omitempty"` +} + +// Reference has reference information +type Reference struct { + Href string `xml:"href,attr" json:"href,omitempty"` + ID string `xml:"id,attr" json:"id,omitempty"` + Title string `xml:"title,attr" json:"title,omitempty"` + Type string `xml:"type,attr" json:"type,omitempty"` +} + +// Package has affected package information +type Package struct { + Name string `xml:"name,attr" json:"name,omitempty"` + Epoch string `xml:"epoch,attr" json:"epoch,omitempty"` + Version string `xml:"version,attr" json:"version,omitempty"` + Release string `xml:"release,attr" json:"release,omitempty"` + Arch string `xml:"arch,attr" json:"arch,omitempty"` + Filename string `xml:"filename" json:"filename,omitempty"` +} + +type options struct { + url string + dir string + retry int + releases []string + repos []string + arches []string +} + +type option func(*options) + +func With(url, dir string, retry int, releases, repos, arches []string) option { + return func(opts *options) { + opts.url = url + opts.dir = dir + opts.retry = retry + opts.releases = releases + opts.repos = repos + opts.arches = arches + } +} + +type Config struct { + *options +} + +func NewConfig(opts ...option) Config { + o := &options{ + url: urlFormat, + dir: filepath.Join(utils.VulnListDir(), rockyDir), + retry: retry, + releases: defaultReleases, + repos: defaultRepos, + arches: defaultArches, + } + for _, opt := range opts { + opt(o) + } + + return Config{ + options: o, + } +} + +func (c Config) Update() error { + for _, release := range c.releases { + for _, repo := range c.repos { + for _, arch := range c.arches { + log.Printf("Fetching Rocky Linux %s %s %s data...", release, repo, arch) + if err := c.update(release, repo, arch); err != nil { + return xerrors.Errorf("failed to update security advisories of Rocky Linux %s %s %s: %w", release, repo, arch, err) + } + } + } + } + return nil +} + +func (c Config) update(release, repo, arch string) error { + dirPath := filepath.Join(c.dir, release, repo, arch) + log.Printf("Remove Rocky Linux %s %s %s directory %s", release, repo, arch, dirPath) + if err := os.RemoveAll(dirPath); err != nil { + return xerrors.Errorf("failed to remove Rocky Linux %s %s %s directory: %w", release, repo, arch, err) + } + if err := os.MkdirAll(dirPath, os.ModePerm); err != nil { + return xerrors.Errorf("failed to mkdir: %w", err) + } + + u, err := url.Parse(fmt.Sprintf(c.url, release, repo, arch)) + if err != nil { + return xerrors.Errorf("failed to parse root url: %w", err) + } + rootPath := u.Path + u.Path = path.Join(rootPath, "repodata/repomd.xml") + updateInfoPath, err := c.fetchUpdateInfoPath(u.String()) + if err != nil { + return xerrors.Errorf("failed to fetch updateInfo path from repomd.xml: %w", err) + } + u.Path = path.Join(rootPath, updateInfoPath) + uinfo, err := c.fetchUpdateInfo(u.String()) + if err != nil { + return xerrors.Errorf("failed to fetch updateInfo: %w", err) + } + + secErrata := map[string][]RLSA{} + for _, rlsa := range uinfo.RLSAList { + if !strings.HasPrefix(rlsa.ID, "RLSA-") { + continue + } + y := strings.Split(strings.TrimPrefix(rlsa.ID, "RLSA-"), ":")[0] + secErrata[y] = append(secErrata[y], rlsa) + } + + for year, errata := range secErrata { + log.Printf("Write Errata for Rocky Linux %s %s %s %s", release, repo, arch, year) + + if err := os.MkdirAll(filepath.Join(dirPath, year), os.ModePerm); err != nil { + return xerrors.Errorf("failed to mkdir: %w", err) + } + + bar := pb.StartNew(len(errata)) + for _, erratum := range errata { + jsonPath := filepath.Join(dirPath, year, fmt.Sprintf("%s.json", erratum.ID)) + if err := utils.Write(jsonPath, erratum); err != nil { + return xerrors.Errorf("failed to write Rocky Linux CVE details: %w", err) + } + bar.Increment() + } + bar.Finish() + } + + return nil +} + +func (c Config) fetchUpdateInfoPath(repomdURL string) (updateInfoPath string, err error) { + res, err := utils.FetchURL(repomdURL, "", c.retry) + if err != nil { + return "", xerrors.Errorf("failed to fetch %s: %w", repomdURL, err) + } + + var repoMd RepoMd + if err := xml.NewDecoder(bytes.NewBuffer(res)).Decode(&repoMd); err != nil { + return "", xerrors.Errorf("failed to decode repomd.xml: %w", err) + } + + for _, repo := range repoMd.RepoList { + if repo.Type == "updateinfo" { + updateInfoPath = repo.Location.Href + break + } + } + if updateInfoPath == "" { + return "", xerrors.New("no updateinfo field in the repomd") + } + return updateInfoPath, nil +} + +func (c Config) fetchUpdateInfo(url string) (*UpdateInfo, error) { + res, err := utils.FetchURL(url, "", c.retry) + if err != nil { + return nil, xerrors.Errorf("failed to fetch updateInfo: %w", err) + } + r, err := gzip.NewReader(bytes.NewBuffer(res)) + if err != nil { + return nil, xerrors.Errorf("failed to decompress updateInfo: %w", err) + } + defer r.Close() + + var updateInfo UpdateInfo + if err := xml.NewDecoder(r).Decode(&updateInfo); err != nil { + return nil, err + } + for i, alas := range updateInfo.RLSAList { + var cveIDs []string + for _, ref := range alas.References { + if ref.Type == "cve" { + cveIDs = append(cveIDs, ref.ID) + } + } + updateInfo.RLSAList[i].CveIDs = cveIDs + } + return &updateInfo, nil +} diff --git a/rocky/rocky_test.go b/rocky/rocky_test.go new file mode 100644 index 0000000..219b7a0 --- /dev/null +++ b/rocky/rocky_test.go @@ -0,0 +1,74 @@ +package rocky_test + +import ( + "errors" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/aquasecurity/vuln-list-update/rocky" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" +) + +func Test_Update(t *testing.T) { + tests := []struct { + name string + rootDir string + expectedError error + }{ + { + name: "happy path", + rootDir: "testdata/fixtures/happy", + expectedError: nil, + }, + { + name: "bad repomd response", + rootDir: "testdata/fixtures/repomd_invalid", + expectedError: xerrors.Errorf("failed to update security advisories of Rocky Linux 8 BaseOS x86_64: %w", errors.New("failed to fetch updateInfo path from repomd.xml")), + }, + { + name: "bad updateInfo response", + rootDir: "testdata/fixtures/updateinfo_invalid", + expectedError: xerrors.Errorf("failed to update security advisories of Rocky Linux 8 BaseOS x86_64: %w", errors.New("failed to fetch updateInfo")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tsUpdateInfoURL := httptest.NewServer(http.StripPrefix("/pub/rocky/8/BaseOS/x86_64/os/repodata/", http.FileServer(http.Dir(tt.rootDir)))) + defer tsUpdateInfoURL.Close() + + dir := t.TempDir() + rc := rocky.NewConfig(rocky.With(tsUpdateInfoURL.URL+"/pub/rocky/%s/%s/%s/os/", dir, 0, []string{"8"}, []string{"BaseOS"}, []string{"x86_64"})) + if err := rc.Update(); tt.expectedError != nil { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError.Error()) + return + } + + err := filepath.Walk(dir, func(path string, info os.FileInfo, errfp error) error { + if errfp != nil { + return errfp + } + if info.IsDir() { + return nil + } + + dir, file := filepath.Split(path) + want, err := os.ReadFile(filepath.Join("testdata", "golden", filepath.Base(dir), file)) + assert.NoError(t, err, "failed to open the golden file") + + got, err := os.ReadFile(path) + assert.NoError(t, err, "failed to open the result file") + + assert.JSONEq(t, string(want), string(got)) + + return nil + }) + assert.Nil(t, err, "filepath walk error") + }) + } +} diff --git a/rocky/testdata/fixtures/happy/repomd.xml b/rocky/testdata/fixtures/happy/repomd.xml new file mode 100644 index 0000000..5e652e5 --- /dev/null +++ b/rocky/testdata/fixtures/happy/repomd.xml @@ -0,0 +1,77 @@ + + + 8.4 + + Rocky Linux 8 + + + 9d25370cf8f2bdf046145fa51ef3d0229ecc6862cbe35281a70184cc39089f54 + 554780b39c8a31f3b92eb2356f38099bd6135834c51542c8ffb889aa6d37c1a0 + + 1632166291 + 4136440 + 29944727 + + + 3f9875964fcb58abd0c3b88ae317450d124020d81e873f1db05c695e84fc1c3b + 483a4f0494e31ae0d100714ddae8eab680c408ff354979636d7bec849c1b6e4d + + 1632166291 + 3284862 + 45704869 + + + 201204bd642f240caaa2d8cd8b8fcf0bf0071fdf7ba67c68b79c209163995057 + ab9351e393e2f08997754411263a7b51114209668d453a62ad998981789c091d + + 1632166291 + 621783 + 6152523 + + + 2e35bd95b02d3bf3d99b82f3bbb6b0381f55b671226771d9d7db975b5d0f205b + b1af8fb023566905067e07bfcffed71ce73ebc6e4ed0af2a010ac7ea6bbfbefa + + 1632166306 + 3599636 + 34222080 + + + 06510a9c700387c4c670654f6440386d6e91e0628116492a4a38228f67ec4d61 + 19df9e2e5a6e66214ddf95569d50ce1c73cfed99c109e453b043a68b8609fd95 + + 1632166298 + 2665240 + 24879104 + + + dddb998b6aca861c3b0724e13ee6f99a74e516b659299ec47c682a08f414cf25 + 46d0a6ae8562e93c729361fa1b28ccf10d26bed3d9d2ff1e464ff807b6201688 + + 1632166293 + 423132 + 6062080 + + + 5eedac6f334681aa51e154d77025db287c33ce1491b14368be9b477ff8208152 + + 1632166276 + 297208 + + + 32e04847f7cc2872db5ac9e92ea540ef2a7999d1c2be0c8c3d47a359b3e2d613 + 5eedac6f334681aa51e154d77025db287c33ce1491b14368be9b477ff8208152 + + 1632166291 + 56668 + 297208 + + + c14b2249cc128fa8c565d0b79986daffc4e1c0a430c84a3ebccc1a016d59bda1 + 3655bacbf1e119d2fd5d27c25acc98a5f8e27ba64d9b86c6b181c20e67b0f7e7 + + 1632172541 + 24366 + 184021 + + \ No newline at end of file diff --git a/rocky/testdata/fixtures/happy/updateinfo.xml.gz b/rocky/testdata/fixtures/happy/updateinfo.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..eaad18e12edc331fa666a3972c44f336b17fbc16 GIT binary patch literal 2585 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 literal 0 HcmV?d00001 diff --git a/rocky/testdata/fixtures/repomd_invalid/repomd.xml b/rocky/testdata/fixtures/repomd_invalid/repomd.xml new file mode 100644 index 0000000..40d3d84 --- /dev/null +++ b/rocky/testdata/fixtures/repomd_invalid/repomd.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/rocky/testdata/fixtures/updateinfo_invalid/repomd.xml b/rocky/testdata/fixtures/updateinfo_invalid/repomd.xml new file mode 100644 index 0000000..5e652e5 --- /dev/null +++ b/rocky/testdata/fixtures/updateinfo_invalid/repomd.xml @@ -0,0 +1,77 @@ + + + 8.4 + + Rocky Linux 8 + + + 9d25370cf8f2bdf046145fa51ef3d0229ecc6862cbe35281a70184cc39089f54 + 554780b39c8a31f3b92eb2356f38099bd6135834c51542c8ffb889aa6d37c1a0 + + 1632166291 + 4136440 + 29944727 + + + 3f9875964fcb58abd0c3b88ae317450d124020d81e873f1db05c695e84fc1c3b + 483a4f0494e31ae0d100714ddae8eab680c408ff354979636d7bec849c1b6e4d + + 1632166291 + 3284862 + 45704869 + + + 201204bd642f240caaa2d8cd8b8fcf0bf0071fdf7ba67c68b79c209163995057 + ab9351e393e2f08997754411263a7b51114209668d453a62ad998981789c091d + + 1632166291 + 621783 + 6152523 + + + 2e35bd95b02d3bf3d99b82f3bbb6b0381f55b671226771d9d7db975b5d0f205b + b1af8fb023566905067e07bfcffed71ce73ebc6e4ed0af2a010ac7ea6bbfbefa + + 1632166306 + 3599636 + 34222080 + + + 06510a9c700387c4c670654f6440386d6e91e0628116492a4a38228f67ec4d61 + 19df9e2e5a6e66214ddf95569d50ce1c73cfed99c109e453b043a68b8609fd95 + + 1632166298 + 2665240 + 24879104 + + + dddb998b6aca861c3b0724e13ee6f99a74e516b659299ec47c682a08f414cf25 + 46d0a6ae8562e93c729361fa1b28ccf10d26bed3d9d2ff1e464ff807b6201688 + + 1632166293 + 423132 + 6062080 + + + 5eedac6f334681aa51e154d77025db287c33ce1491b14368be9b477ff8208152 + + 1632166276 + 297208 + + + 32e04847f7cc2872db5ac9e92ea540ef2a7999d1c2be0c8c3d47a359b3e2d613 + 5eedac6f334681aa51e154d77025db287c33ce1491b14368be9b477ff8208152 + + 1632166291 + 56668 + 297208 + + + c14b2249cc128fa8c565d0b79986daffc4e1c0a430c84a3ebccc1a016d59bda1 + 3655bacbf1e119d2fd5d27c25acc98a5f8e27ba64d9b86c6b181c20e67b0f7e7 + + 1632172541 + 24366 + 184021 + + \ No newline at end of file diff --git a/rocky/testdata/fixtures/updateinfo_invalid/updateinfo.xml.gz b/rocky/testdata/fixtures/updateinfo_invalid/updateinfo.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..a7ba8f6c04105f52be50f62457ac5535dbfd9d9d GIT binary patch literal 1380 zcmV-q1)KUGiwFpcw3%H119fm@VRU6_Zf0*^X>N95Y-waJcx`L|?O083<2DdI*S}(* z0!0>xB-`)iyK!>rAvQ&kGjHC^ zBge_!-PVeh!dR)BSu{$95xtuavaZdU6L!8C5bIo}%`8%~>26~ycxR+r|G;W?r<;W9 zI-4SGhW5SY)WHhFj_P0!l!?xs1QER03eyC;qhLwUgffd@&jWCT+S=!sqq_v&M z$-kaO!PIaRk47{eO^1_+(k&?umYmaYG#y`tB~~CF_?vGLWa+1=EEg8jtwXr;-}dlW zVg_t4nN)0{dUyxHx{5g}EOqYKYJoB;>pG2Y&!U6 zG#Oo8T#lx+|Awj+XllDD*%rA_%*mx7af`9XN3Ae581ch`b;?<)Sd$~o<>0y@+2oyd zW*x6&E+{ZZD`tVKYjV5|AXjWnHEZypb5KpS^dMkwTnbZ@g@+=aBB z`WY~QXT-5jlRc#zNQ*^s9DadQct=6~|GH?;!(4x0mPf`PpGPBy|1DPfxrH72asKG1MnF zgcAxo8SEYbqX`a&|E2s@QXWmV0{c9Wg8h169zA<9lzmsZ+3TKED-ZvEwpH-}39YH1Jp8+W*WWPqsaR^@o1sg({fE#M{f{;woYFT{GV4 zV9%1PuLTt&VYe1Us*T7VIpKayIW+do|=oD3r%7zQVbE9M)fl?_)B3M2;Knzn?~} m`7!Wn{ut8U_tQwz`^L&oZ`-Z3|A*B)8~g(!