Skip to content

Commit

Permalink
feat(rocky): support Rocky Linux (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
MaineK00n authored Jan 18, 2022
1 parent 9b3980a commit 4d919c3
Show file tree
Hide file tree
Showing 11 changed files with 749 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
251 changes: 251 additions & 0 deletions rocky/rocky.go
Original file line number Diff line number Diff line change
@@ -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
}
74 changes: 74 additions & 0 deletions rocky/rocky_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
}
Loading

0 comments on commit 4d919c3

Please sign in to comment.