Skip to content

Commit 5785017

Browse files
feat(suse): add suse-cvrf-cve feed updater
- Add suse/cvrf-cve package to fetch per-CVE CVRF from ftp.suse.com cvrf-cve - Wire target 'suse-cvrf-cve' in main.go and CI update workflow - Move CVE CVRF logic out of suse/cvrf; keep advisory CVRF in suse/cvrf Made-with: Cursor
1 parent a3423d9 commit 5785017

14 files changed

Lines changed: 341 additions & 130 deletions

.github/workflows/update.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ jobs:
7171
name: SUSE CVRF
7272
run: ./scripts/update.sh suse-cvrf "SUSE CVRF"
7373

74+
- if: always()
75+
name: SUSE CVE CVRF
76+
run: ./scripts/update.sh suse-cvrf-cve "SUSE CVE CVRF"
77+
7478
- if: always()
7579
name: GitLab Advisory Database
7680
run: ./scripts/update.sh glad "GitLab Advisory Database"

main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ import (
3232
"github.com/aquasecurity/vuln-list-update/rootio"
3333
"github.com/aquasecurity/vuln-list-update/seal"
3434
susecvrf "github.com/aquasecurity/vuln-list-update/suse/cvrf"
35+
susecvrfcve "github.com/aquasecurity/vuln-list-update/suse/cvrf-cve"
3536
"github.com/aquasecurity/vuln-list-update/ubuntu"
3637
"github.com/aquasecurity/vuln-list-update/utils"
3738
"github.com/aquasecurity/vuln-list-update/wolfi"
3839
)
3940

4041
var (
4142
target = flag.String("target", "", "update target (nvd, alpine, alpine-unfixed, redhat, redhat-oval, "+
42-
"redhat-csaf-vex, debian, ubuntu, amazon, oracle-oval, suse-cvrf, photon, arch-linux, glad, cwe, osvdev, mariner, kevc, wolfi, "+
43+
"redhat-csaf-vex, debian, ubuntu, amazon, oracle-oval, suse-cvrf, suse-cvrf-cve, photon, arch-linux, glad, cwe, osvdev, mariner, kevc, wolfi, "+
4344
"chainguard, azure, openeuler, echo, minimos, eoldates, rootio)")
4445
vulnListDir = flag.String("vuln-list-dir", "", "vuln-list dir")
4546
targetUri = flag.String("target-uri", "", "alternative repository URI (only glad)")
@@ -113,6 +114,11 @@ func run() error {
113114
if err := sc.Update(); err != nil {
114115
return xerrors.Errorf("SUSE CVRF update error: %w", err)
115116
}
117+
case "suse-cvrf-cve":
118+
sc := susecvrfcve.NewConfig()
119+
if err := sc.Update(); err != nil {
120+
return xerrors.Errorf("SUSE CVE CVRF update error: %w", err)
121+
}
116122
case "photon":
117123
pc := photon.NewConfig()
118124
if err := pc.Update(); err != nil {

suse/cvrf-cve/cvrf_cve.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package cvrfcve
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"encoding/xml"
7+
"fmt"
8+
"log"
9+
"path/filepath"
10+
"regexp"
11+
"strings"
12+
"unicode/utf8"
13+
14+
"github.com/cheggaaa/pb"
15+
"github.com/spf13/afero"
16+
"golang.org/x/xerrors"
17+
18+
"github.com/aquasecurity/vuln-list-update/utils"
19+
)
20+
21+
var (
22+
cvrfCVEURL = "http://ftp.suse.com/pub/projects/security/cvrf-cve/"
23+
fileRegexp = regexp.MustCompile(`<a href="(cvrf-(CVE-\d{4}-\d+)\.xml)">.*`)
24+
retry = 5
25+
concurrency = 20
26+
wait = 1
27+
cvrfDir = "cvrf"
28+
suseCVEDir = "suse-cves"
29+
)
30+
31+
type Config struct {
32+
VulnListDir string
33+
URL string
34+
AppFs afero.Fs
35+
Retry int
36+
}
37+
38+
func NewConfig() Config {
39+
return Config{
40+
VulnListDir: utils.VulnListDir(),
41+
URL: cvrfCVEURL,
42+
AppFs: afero.NewOsFs(),
43+
Retry: retry,
44+
}
45+
}
46+
47+
func (c Config) Update() error {
48+
log.Print("Fetching SUSE CVE CVRF data...")
49+
50+
res, err := utils.FetchURL(c.URL, "", c.Retry)
51+
if err != nil {
52+
return xerrors.Errorf("cannot download SUSE CVE CVRF list: %w", err)
53+
}
54+
55+
cveURLs := make(map[string]string)
56+
scanner := bufio.NewScanner(bytes.NewReader(res))
57+
for scanner.Scan() {
58+
line := scanner.Text()
59+
if match := fileRegexp.FindStringSubmatch(line); len(match) != 0 {
60+
cveURLs[match[2]] = c.URL + match[1]
61+
}
62+
}
63+
if err := scanner.Err(); err != nil {
64+
return xerrors.Errorf("failed reading SUSE CVE CVRF list: %w", err)
65+
}
66+
67+
urls := make([]string, 0, len(cveURLs))
68+
for _, u := range cveURLs {
69+
urls = append(urls, u)
70+
}
71+
72+
cvrfXMLs, err := utils.FetchConcurrently(urls, concurrency, wait, c.Retry)
73+
if err != nil {
74+
log.Printf("failed to fetch CVE CVRF data from SUSE. err: %s", err)
75+
}
76+
77+
log.Printf("Saving SUSE CVE CVRF data...")
78+
bar := pb.StartNew(len(cvrfXMLs))
79+
for _, cvrfXML := range cvrfXMLs {
80+
var cv Cvrf
81+
if len(cvrfXML) == 0 {
82+
log.Println("empty CVE CVRF xml")
83+
bar.Increment()
84+
continue
85+
}
86+
87+
if !utf8.Valid(cvrfXML) {
88+
log.Println("invalid UTF-8")
89+
cvrfXML = []byte(strings.ToValidUTF8(string(cvrfXML), ""))
90+
}
91+
92+
if err = xml.Unmarshal(cvrfXML, &cv); err != nil {
93+
return xerrors.Errorf("failed to decode SUSE CVE CVRF XML: %w", err)
94+
}
95+
96+
cveID := extractCVEID(cv)
97+
if cveID == "" {
98+
log.Printf("invalid CVE CVRF document ID: %s", cv.Tracking.ID)
99+
bar.Increment()
100+
continue
101+
}
102+
103+
if err = c.saveCVEPerYear(cveID, cv); err != nil {
104+
return xerrors.Errorf("failed to save SUSE CVE CVRF: %w", err)
105+
}
106+
bar.Increment()
107+
}
108+
bar.Finish()
109+
return nil
110+
}
111+
112+
func extractCVEID(cv Cvrf) string {
113+
for _, v := range cv.Vulnerabilities {
114+
cve := strings.TrimSpace(v.CVE)
115+
if cve != "" {
116+
return cve
117+
}
118+
}
119+
return strings.TrimSpace(cv.Title)
120+
}
121+
122+
func (c Config) saveCVEPerYear(cveID string, data interface{}) error {
123+
s := strings.Split(cveID, "-")
124+
if len(s) < 3 {
125+
return nil
126+
}
127+
128+
yearDir := filepath.Join(c.VulnListDir, cvrfDir, suseCVEDir, s[1])
129+
fileName := fmt.Sprintf("%s.json", cveID)
130+
if err := utils.WriteJSON(c.AppFs, yearDir, fileName, data); err != nil {
131+
return xerrors.Errorf("failed to write file: %w", err)
132+
}
133+
return nil
134+
}

suse/cvrf-cve/cvrf_cve_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package cvrfcve_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"os"
7+
"testing"
8+
9+
"github.com/spf13/afero"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
cvrfcve "github.com/aquasecurity/vuln-list-update/suse/cvrf-cve"
14+
)
15+
16+
func TestConfig_Update(t *testing.T) {
17+
testCases := []struct {
18+
name string
19+
appFs afero.Fs
20+
xmlFileNames map[string]string
21+
expectedFile string
22+
expectedErrorMsg string
23+
}{
24+
{
25+
name: "positive test",
26+
appFs: afero.NewMemMapFs(),
27+
xmlFileNames: map[string]string{
28+
"/pub/projects/security/cvrf-cve/": "testdata/cvrf-cve-list.html",
29+
"/pub/projects/security/cvrf-cve/cvrf-CVE-2014-6271.xml": "testdata/cvrf-CVE-2014-6271.xml",
30+
"/pub/projects/security/cvrf-cve/cvrf-CVE-1234-12345.xml": "testdata/cvrf-CVE-1234-12345.xml",
31+
},
32+
expectedFile: "/tmp/cvrf/suse-cves/2014/CVE-2014-6271.json",
33+
},
34+
{
35+
name: "broken XML",
36+
appFs: afero.NewMemMapFs(),
37+
xmlFileNames: map[string]string{
38+
"/pub/projects/security/cvrf-cve/": "testdata/cvrf-cve-list-invalid-single.html",
39+
"/pub/projects/security/cvrf-cve/cvrf-CVE-2014-6271.xml": "testdata/broken-cvrf-data.xml",
40+
},
41+
expectedErrorMsg: "failed to decode SUSE CVE CVRF XML",
42+
},
43+
}
44+
for _, tc := range testCases {
45+
t.Run(tc.name, func(t *testing.T) {
46+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
47+
filePath, ok := tc.xmlFileNames[r.URL.Path]
48+
if !ok {
49+
http.NotFound(w, r)
50+
return
51+
}
52+
b, err := os.ReadFile(filePath)
53+
assert.NoError(t, err, tc.name)
54+
_, err = w.Write(b)
55+
assert.NoError(t, err, tc.name)
56+
}))
57+
defer ts.Close()
58+
59+
c := cvrfcve.Config{
60+
VulnListDir: "/tmp",
61+
URL: ts.URL + "/pub/projects/security/cvrf-cve/",
62+
AppFs: tc.appFs,
63+
Retry: 0,
64+
}
65+
err := c.Update()
66+
if tc.expectedErrorMsg != "" {
67+
require.Error(t, err, tc.name)
68+
assert.Contains(t, err.Error(), tc.expectedErrorMsg, tc.name)
69+
return
70+
}
71+
require.NoError(t, err, tc.name)
72+
73+
b, err := afero.ReadFile(c.AppFs, tc.expectedFile)
74+
require.NoError(t, err, tc.name)
75+
assert.Contains(t, string(b), `"CVE": "CVE-2014-6271"`)
76+
assert.Contains(t, string(b), `"BaseScoreV3": "9.8"`)
77+
})
78+
}
79+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<cvrfdoc xmlns="http://docs.oasis-open.org/csaf/ns/csaf-cvrf/v1.2/cvrf">
3+
<DocumentTitle xml:lang="en">CVE-2014-6271</DocumentTitle>
4+
<Vulnerability xmlns="http://docs.oasis-open.org/csaf/ns/csaf-cvrf/v1.2/vuln" Ordinal="1">
5+
<CVE>CVE-2014-6271</CVE>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<cvrfdoc xmlns="http://docs.oasis-open.org/csaf/ns/csaf-cvrf/v1.2/cvrf">
3+
<DocumentTitle xml:lang="en">CVE-1234-12345</DocumentTitle>
4+
<DocumentTracking>
5+
<Identification>
6+
<ID>SUSE CVE-1234-12345</ID>
7+
</Identification>
8+
</DocumentTracking>
9+
<Vulnerability xmlns="http://docs.oasis-open.org/csaf/ns/csaf-cvrf/v1.2/vuln" Ordinal="1">
10+
<CVE>CVE-1234-12345</CVE>
11+
<CVSSScoreSets>
12+
<ScoreSetV3>
13+
<BaseScoreV3>6.5</BaseScoreV3>
14+
<VectorV3>CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N</VectorV3>
15+
</ScoreSetV3>
16+
</CVSSScoreSets>
17+
</Vulnerability>
18+
</cvrfdoc>

suse/cvrf/testdata/cvrf-cve-CVE-2014-6271.xml renamed to suse/cvrf-cve/testdata/cvrf-CVE-2014-6271.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<cvrfdoc xmlns="http://docs.oasis-open.org/csaf/ns/csaf-cvrf/v1.2/cvrf">
3+
<DocumentTitle xml:lang="en">CVE-2014-6271</DocumentTitle>
4+
<DocumentTracking>
5+
<Identification>
6+
<ID>SUSE CVE-2014-6271</ID>
7+
</Identification>
8+
</DocumentTracking>
39
<Vulnerability xmlns="http://docs.oasis-open.org/csaf/ns/csaf-cvrf/v1.2/vuln" Ordinal="1">
410
<CVE>CVE-2014-6271</CVE>
511
<CVSSScoreSets>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<html>
2+
<head><title>Index of /pub/projects/security/cvrf-cve/</title></head>
3+
<body>
4+
<h1>Index of /pub/projects/security/cvrf-cve/</h1><hr><pre><a href="../">../</a>
5+
<a href="cvrf-CVE-2014-6271.xml">cvrf-CVE-2014-6271.xml</a> 06-Aug-2024 01:23 46K
6+
</pre><hr></body>
7+
</html>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<html>
2+
<head><title>Index of /pub/projects/security/cvrf-cve/</title></head>
3+
<body>
4+
<h1>Index of /pub/projects/security/cvrf-cve/</h1><hr><pre><a href="../">../</a>
5+
<a href="cvrf-CVE-2014-6271.xml">cvrf-CVE-2014-6271.xml</a> 06-Aug-2024 01:23 46K
6+
<a href="cvrf-CVE-1234-12345.xml">cvrf-CVE-1234-12345.xml</a> 01-May-2024 02:37 2774
7+
</pre><hr></body>
8+
</html>

suse/cvrf-cve/types.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package cvrfcve
2+
3+
type Cvrf struct {
4+
Title string `xml:"DocumentTitle"`
5+
Tracking DocumentTracking `xml:"DocumentTracking"`
6+
Notes []DocumentNote `xml:"DocumentNotes>Note"`
7+
References []Reference `xml:"DocumentReferences>Reference"`
8+
Vulnerabilities []Vulnerability `xml:"Vulnerability"`
9+
}
10+
11+
type DocumentTracking struct {
12+
ID string `xml:"Identification>ID"`
13+
Status string `xml:"Status"`
14+
Version string `xml:"Version"`
15+
InitialReleaseDate string `xml:"InitialReleaseDate"`
16+
CurrentReleaseDate string `xml:"CurrentReleaseDate"`
17+
RevisionHistory []Revision `xml:"RevisionHistory>Revision"`
18+
}
19+
20+
type DocumentNote struct {
21+
Text string `xml:",chardata"`
22+
Title string `xml:"Title,attr"`
23+
Type string `xml:"Type,attr"`
24+
}
25+
26+
type Revision struct {
27+
Number string `xml:"Number"`
28+
Date string `xml:"Date"`
29+
Description string `xml:"Description"`
30+
}
31+
32+
type Vulnerability struct {
33+
CVE string `xml:"CVE"`
34+
Description string `xml:"Notes>Note"`
35+
Threats []Threat `xml:"Threats>Threat"`
36+
References []Reference `xml:"References>Reference"`
37+
ProductStatuses []Status `xml:"ProductStatuses>Status"`
38+
CVSSScoreSets CVSSScoreSets `xml:"CVSSScoreSets" json:",omitempty"`
39+
}
40+
41+
type Threat struct {
42+
Type string `xml:"Type,attr"`
43+
Severity string `xml:"Description"`
44+
}
45+
46+
type Reference struct {
47+
URL string `xml:"URL"`
48+
Description string `xml:"Description"`
49+
}
50+
51+
type Status struct {
52+
Type string `xml:"Type,attr"`
53+
ProductID []string `xml:"ProductID"`
54+
}
55+
56+
type CVSSScoreSets struct {
57+
ScoreSetV2 []ScoreSetV2 `xml:"ScoreSetV2" json:",omitempty"`
58+
ScoreSetV3 []ScoreSetV3 `xml:"ScoreSetV3" json:",omitempty"`
59+
}
60+
61+
type ScoreSetV2 struct {
62+
BaseScoreV2 string `xml:"BaseScoreV2" json:",omitempty"`
63+
VectorV2 string `xml:"VectorV2" json:",omitempty"`
64+
}
65+
66+
type ScoreSetV3 struct {
67+
BaseScoreV3 string `xml:"BaseScoreV3" json:",omitempty"`
68+
VectorV3 string `xml:"VectorV3" json:",omitempty"`
69+
}

0 commit comments

Comments
 (0)