Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rocky): support Rocky Linux #107

Merged
merged 12 commits into from
Jan 18, 2022
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)
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved
}

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))
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved
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 {
MaineK00n marked this conversation as resolved.
Show resolved Hide resolved
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