256 lines
6.9 KiB
Go
256 lines
6.9 KiB
Go
package rocky
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/xml"
|
|
"errors"
|
|
"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"`
|
|
Src string `xml:"src,attr" json:"src,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 {
|
|
if errors.Is(err, ErrorNoUpdateInfoField) && repo == "extras" {
|
|
log.Printf("skip extras repository because updateinfo field is not in repomd.xml: %s", err)
|
|
return 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
|
|
}
|
|
|
|
var ErrorNoUpdateInfoField = xerrors.New("no updateinfo field in the repomd")
|
|
|
|
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" {
|
|
return repo.Location.Href, nil
|
|
}
|
|
}
|
|
return "", ErrorNoUpdateInfoField
|
|
}
|
|
|
|
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
|
|
}
|