Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions rootio/rootio.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,22 @@ import (
const (
rootioDir = "rootio"
cveFeedURLBase = "https://api.root.io"
cveFeedPath = "external/cve_feed"
retry = 3
)

// feedInfo contains the information for each feed type
type feedInfo struct {
path string // API path
subDir string // subdirectory under rootio
fileName string // output filename
}

// feeds defines the feeds to fetch
var feeds = []feedInfo{
{path: "external/cve_feed", subDir: "", fileName: "cve_feed.json"}, // OS packages feed (legacy endpoint)
{path: "external/app_feed", subDir: "app", fileName: "cve_feed.json"}, // Application packages feed
}

type option func(c *Updater)

func WithVulnListDir(v string) option {
Expand Down Expand Up @@ -63,25 +75,48 @@ func (u *Updater) Update() error {
return xerrors.Errorf("Root.io mkdir error: %w", err)
}

log.Println("Fetching Root.io CVE data...")
// Fetch and save feeds
for _, feed := range feeds {
if err := u.fetchAndSaveFeed(feed); err != nil {
return err
}
}

return nil
}

// fetchAndSaveFeed fetches a feed from the given path and saves it to the specified file
func (u *Updater) fetchAndSaveFeed(feed feedInfo) error {
feedURL := u.baseURL.JoinPath(feed.path)
log.Printf("Fetching Root.io data from %s...", feedURL.String())

feedURL := u.baseURL.JoinPath(cveFeedPath)
data, err := utils.FetchURL(feedURL.String(), "", u.retry)
if err != nil {
return xerrors.Errorf("Failed to fetch Root.io CVE feed from %s: %w", feedURL.String(), err)
return xerrors.Errorf("Failed to fetch Root.io feed from %s: %w", feedURL.String(), err)
}

var feedData CVEFeed
if err := json.Unmarshal(data, &feedData); err != nil {
return xerrors.Errorf("failed to parse Root.io feed JSON: %w", err)
}

var cveFeed CVEFeed
if err := json.Unmarshal(data, &cveFeed); err != nil {
return xerrors.Errorf("failed to parse Root.io CVE feed JSON: %w", err)
// Determine the target directory
var targetDir string
if feed.subDir != "" {
targetDir = filepath.Join(u.vulnListDir, rootioDir, feed.subDir)
if err := os.MkdirAll(targetDir, 0755); err != nil {
return xerrors.Errorf("failed to create directory %s: %w", targetDir, err)
}
} else {
targetDir = filepath.Join(u.vulnListDir, rootioDir)
}

// Save the entire feed as a single JSON file
feedFilePath := filepath.Join(dir, "cve_feed.json")
if err := utils.Write(feedFilePath, cveFeed); err != nil {
return xerrors.Errorf("failed to write Root.io CVE feed to %s: %w", feedFilePath, err)
// Save feed
feedFilePath := filepath.Join(targetDir, feed.fileName)
if err := utils.Write(feedFilePath, feedData); err != nil {
return xerrors.Errorf("failed to write Root.io feed to %s: %w", feedFilePath, err)
}
log.Printf("Root.io data updated successfully in %s", feedFilePath)

log.Printf("Root.io CVE data updated successfully in %s", feedFilePath)
return nil
}
80 changes: 54 additions & 26 deletions rootio/rootio_test.go
Original file line number Diff line number Diff line change
@@ -1,53 +1,79 @@
package rootio

import (
"flag"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

var update = flag.Bool("update", false, "update golden files")

func TestUpdater_Update(t *testing.T) {
tests := []struct {
name string
testFile string
wantErr string
name string
osTestFile string
appTestFile string
wantErr string
}{
{
name: "valid response",
testFile: "testdata/valid.json",
name: "valid response",
osTestFile: "testdata/os_feed.json",
appTestFile: "testdata/app_feed.json",
},
{
name: "invalid OS JSON response",
osTestFile: "testdata/invalid.json",
appTestFile: "testdata/app_feed.json",
wantErr: "failed to parse Root.io feed JSON",
},
{
name: "invalid JSON response",
testFile: "testdata/invalid.json",
wantErr: "failed to parse Root.io CVE feed JSON",
name: "invalid app JSON response",
osTestFile: "testdata/os_feed.json",
appTestFile: "testdata/invalid.json",
wantErr: "failed to parse Root.io feed JSON",
},
{
name: "requesting non-existent file",
testFile: "testdata/non-existent.json",
wantErr: "status code: 404",
name: "OS feed not found",
osTestFile: "testdata/non-existent.json",
appTestFile: "testdata/app_feed.json",
wantErr: "status code: 404",
},
{
name: "empty test file",
name: "server error",
wantErr: "status code: 500",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tt.testFile != "" {
http.ServeFile(w, r, tt.testFile)
} else {
path := strings.TrimPrefix(r.URL.Path, "/")

if tt.osTestFile == "" && tt.appTestFile == "" {
w.WriteHeader(http.StatusInternalServerError)
return
}

switch path {
case "external/cve_feed":
if tt.osTestFile != "" {
http.ServeFile(w, r, tt.osTestFile)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
case "external/app_feed":
if tt.appTestFile != "" {
http.ServeFile(w, r, tt.appTestFile)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
default:
http.NotFound(w, r)
}
}))
defer ts.Close()
Expand All @@ -68,19 +94,21 @@ func TestUpdater_Update(t *testing.T) {
}
assert.NoError(t, err)

actual, err := os.ReadFile(filepath.Join(tmpDir, rootioDir, "cve_feed.json"))
// Verify OS feed exists and is valid
osActual, err := os.ReadFile(filepath.Join(tmpDir, rootioDir, "cve_feed.json"))
require.NoError(t, err)

wantFile := filepath.Join("testdata", "happy", "cve_feed.json")
if *update {
err = os.WriteFile(wantFile, actual, 0666)
require.NoError(t, err, wantFile)
}
osExpected, err := os.ReadFile(tt.osTestFile)
require.NoError(t, err)
assert.JSONEq(t, string(osExpected), string(osActual))

expected, err := os.ReadFile(wantFile)
// Verify app feed exists and is valid
appActual, err := os.ReadFile(filepath.Join(tmpDir, rootioDir, "app", "cve_feed.json"))
require.NoError(t, err)

assert.JSONEq(t, string(expected), string(actual))
appExpected, err := os.ReadFile(tt.appTestFile)
require.NoError(t, err)
assert.JSONEq(t, string(appExpected), string(appActual))
})
}
}
104 changes: 104 additions & 0 deletions rootio/testdata/app_feed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{
"pip": [
{
"distroversion": "",
"packages": [
{
"pkg": {
"name": "django",
"cves": {
"CVE-2023-36053": {
"vulnerable_ranges": [
">=3.2,<3.2.20",
">=4.0,<4.1.10",
">=4.2,<4.2.3"
],
"fixed_versions": [
"3.2.20",
"4.1.10",
"4.2.3"
]
}
}
}
},
{
"pkg": {
"name": "requests",
"cves": {
"CVE-2023-32681": {
"vulnerable_ranges": [
"<2.31.0"
],
"fixed_versions": [
"2.31.0"
]
}
}
}
}
]
}
],
"npm": [
{
"distroversion": "",
"packages": [
{
"pkg": {
"name": "express",
"cves": {
"CVE-2022-24999": {
"vulnerable_ranges": [
"<4.17.3",
">=5.0.0-alpha.1,<5.0.0-alpha.8"
],
"fixed_versions": [
"4.17.3",
"5.0.0-alpha.8"
]
}
}
}
},
{
"pkg": {
"name": "lodash",
"cves": {
"CVE-2021-23337": {
"vulnerable_ranges": [
"<4.17.21"
],
"fixed_versions": [
"4.17.21"
]
}
}
}
}
]
}
],
"go": [
{
"distroversion": "",
"packages": [
{
"pkg": {
"name": "golang.org/x/crypto",
"cves": {
"CVE-2022-27191": {
"vulnerable_ranges": [
"<0.0.0-20220314234659-1baeb1ce4c0b"
],
"fixed_versions": [
"0.0.0-20220314234659-1baeb1ce4c0b"
]
}
}
}
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"cves": {
"CVE-2023-46853": {
"vulnerable_ranges": [
"\u003c1.6.17-r00071"
"<1.6.17-r00071"
],
"fixed_versions": [
"1.6.17-r00071"
Expand All @@ -31,8 +31,8 @@
"cves": {
"CVE-2022-49043": {
"vulnerable_ranges": [
"\u003c2.9.14+dfsg-1.3~deb12u1.root.io.3",
"\u003e2.9.14+dfsg-1.3~deb12u1.root.io.3 \u003c2.9.14+dfsg-1.3~deb12u2"
"<2.9.14+dfsg-1.3~deb12u1.root.io.3",
">2.9.14+dfsg-1.3~deb12u1.root.io.3 <2.9.14+dfsg-1.3~deb12u2"
],
"fixed_versions": [
"2.9.14+dfsg-1.3~deb12u1.root.io.3",
Expand All @@ -55,7 +55,7 @@
"cves": {
"CVE-2023-29383": {
"vulnerable_ranges": [
"\u003c1:4.8.1-2ubuntu2.2.root.io.2"
"<1:4.8.1-2ubuntu2.2.root.io.2"
],
"fixed_versions": [
"1:4.8.1-2ubuntu2.2.root.io.2"
Expand Down