validate generate-cve.yml outputs (#26752)

https://github.com/fleetdm/fleet/issues/21300

- [x] Added/updated automated tests
- [x] A detailed QA plan exists on the associated ticket (if it isn't
there, work with the product group's QA engineer to add it)
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Konstantin Sykulev 2025-03-12 14:49:47 -05:00 committed by GitHub
parent 997adcebe0
commit bd2b2bcd3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 216 additions and 29 deletions

79
cmd/cve/validate/main.go Normal file
View file

@ -0,0 +1,79 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/goval_dictionary"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/nvd"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/oval"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
)
func main() {
dbDir := flag.String("db_dir", "/tmp/vulndbs", "Path to the vulnerability database")
debug := flag.Bool("debug", false, "Sets debug mode")
flag.Parse()
logger := log.NewJSONLogger(os.Stdout)
if *debug {
logger = level.NewFilter(logger, level.AllowDebug())
} else {
logger = level.NewFilter(logger, level.AllowInfo())
}
vulnPath := *dbDir
checkNVDVulnerabilities(vulnPath, logger)
checkGovalDictionaryVulnerabilities(vulnPath)
}
func checkNVDVulnerabilities(vulnPath string, logger log.Logger) {
metaMap := make(map[string]fleet.CVEMeta)
if err := nvd.CVEMetaFromNVDFeedFiles(metaMap, vulnPath, logger); err != nil {
panic(err)
}
}
func checkGovalDictionaryVulnerabilities(vulnPath string) {
for _, p := range oval.SupportedGovalPlatforms {
platform := platformFromString(p)
destFilename := platform.ToGovalDictionaryFilename()
filename := platform.ToGovalDatabaseFilename()
// Renaming these files from amzn_%d.sqlite3 to fleet_goval_dictionary_amzn_%d.sqlite3
// In the vulnerabilities repository the `goval-dictionary fetch` downloads these files with the shorter name
// However, the goval_dictionary/sync.go#Refresh method download these files, extracts them, and uses the longer name
// See in specific the `downloadDatabase` function where it sets the `dstPath` to use `platform.ToGovalDictionaryFilename`
// LoadDb then expect the path to include the `ToGovalDictionaryFilename`
err := os.Rename(fmt.Sprintf("%s/%s", vulnPath, filename), fmt.Sprintf("%s/%s", vulnPath, destFilename))
if err != nil {
panic(fmt.Sprintf("failed to move file from %s/%s to %s/%s: %v", vulnPath, filename, vulnPath, destFilename, err))
}
db, err := goval_dictionary.LoadDb(platform, vulnPath)
if err != nil {
panic(err)
}
err = db.Verfiy()
if err != nil {
panic(err)
}
err = os.Rename(fmt.Sprintf("%s/%s", vulnPath, destFilename), fmt.Sprintf("%s/%s", vulnPath, filename))
if err != nil {
panic(fmt.Sprintf("failed to move file from %s/%s to %s/%s: %v", vulnPath, destFilename, vulnPath, filename, err))
}
}
}
func platformFromString(platform string) oval.Platform {
parts := strings.Split(platform, "_")
host, version := parts[0], fmt.Sprintf("Amazon Linux %s.0", parts[1]) // this is due to how oval.Platform#getMajorMinorVer works
return oval.NewPlatform(host, version)
}

View file

@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/oval"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/utils"
@ -34,7 +35,7 @@ func Analyze(
if !platform.IsGovalDictionarySupported() {
return nil, ErrUnsupportedPlatform
}
db, err := loadDb(platform, vulnPath)
db, err := LoadDb(platform, vulnPath)
if err != nil {
return nil, err
}
@ -117,8 +118,8 @@ func Analyze(
return inserted, nil
}
// loadDb returns the latest goval_dictionary database for the given platform.
func loadDb(platform oval.Platform, vulnPath string) (*Database, error) {
// LoadDb returns the latest goval_dictionary database for the given platform.
func LoadDb(platform oval.Platform, vulnPath string) (*Database, error) {
if !platform.IsGovalDictionarySupported() {
return nil, fmt.Errorf("platform %q not supported", platform)
}

View file

@ -3,12 +3,13 @@ package goval_dictionary
import (
"database/sql"
"fmt"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/oval"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/utils"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"strings"
)
func NewDB(db *sql.DB, platform oval.Platform) *Database {
@ -20,13 +21,30 @@ type Database struct {
platform oval.Platform
}
const baseSearchStmt = `SELECT packages.version, cves.cve_id
FROM packages join definitions on definitions.id = packages.definition_id
JOIN advisories ON advisories.definition_id = definitions.id JOIN cves ON cves.advisory_id = advisories.id`
func (db Database) Verfiy() error {
searchStmt := fmt.Sprintf("%s LIMIT 1", baseSearchStmt)
affectedSoftwareRows, err := db.sqlite.Query(searchStmt)
if err != nil {
return fmt.Errorf("could not query database: %w", err)
}
defer affectedSoftwareRows.Close()
if affectedSoftwareRows.Err() != nil {
return affectedSoftwareRows.Err()
}
return nil
}
// Eval evaluates the current goval_dictionary database against an OS version and a list of installed software,
// returns all software vulnerabilities found. Logs on any errors so we return as many vulnerabilities as we can.
func (db Database) Eval(software []fleet.Software, logger kitlog.Logger) []fleet.SoftwareVulnerability {
searchStmt := `SELECT packages.version, cves.cve_id
FROM packages join definitions on definitions.id = packages.definition_id
JOIN advisories ON advisories.definition_id = definitions.id JOIN cves ON cves.advisory_id = advisories.id
WHERE packages.name = ? AND packages.arch = ? ORDER BY cve_id, version`
searchStmt := fmt.Sprintf("%s WHERE packages.name = ? AND packages.arch = ? ORDER BY cve_id, version", baseSearchStmt)
vulnerabilities := make([]fleet.SoftwareVulnerability, 0)
for _, swItem := range software {

View file

@ -2,13 +2,37 @@ package goval_dictionary
import (
"database/sql"
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/oval"
kitlog "github.com/go-kit/log"
"github.com/stretchr/testify/require"
"testing"
)
func TestVerify(t *testing.T) {
sqlite, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
// notice how we are missing the packages.version column
dbSetupQueries := []string{
"CREATE TABLE packages (name TEXT NOT NULL, arch TEXT NOT NULL, definition_id INTEGER NOT NULL)",
"CREATE TABLE definitions (id INTEGER NOT NULL PRIMARY KEY)",
"CREATE TABLE advisories (id INTEGER NOT NULL PRIMARY KEY, definition_id INTEGER NOT NULL)",
"CREATE TABLE cves (cve_id TEXT NOT NULL, advisory_id INTEGER NOT NULL)",
}
for _, query := range dbSetupQueries {
if _, err := sqlite.Exec(query); err != nil {
t.Fatal(err)
}
}
db := NewDB(sqlite, oval.NewPlatform("amzn", "Amazon Linux 2.0.0"))
t.Run("Verify alerts of error", func(t *testing.T) {
require.Error(t, db.Verfiy())
})
}
func TestDatabase(t *testing.T) {
// build minimal slice of goval-dictionary sqlite schema
sqlite, err := sql.Open("sqlite3", ":memory:")
@ -90,4 +114,8 @@ func TestDatabase(t *testing.T) {
require.Equal(t, uint(235), vulns[2].SoftwareID)
require.Equal(t, uint(235), vulns[3].SoftwareID)
})
t.Run("Verify returns no errors", func(t *testing.T) {
require.NoError(t, db.Verfiy())
})
}

View file

@ -185,21 +185,13 @@ func DownloadCISAKnownExploitsFeed(vulnPath string) error {
return nil
}
// LoadCVEMeta loads the cvss scores, epss scores, and known exploits from the previously downloaded feeds and saves
// them to the database.
func LoadCVEMeta(ctx context.Context, logger log.Logger, vulnPath string, ds fleet.Datastore) error {
if !license.IsPremium(ctx) {
level.Info(logger).Log("msg", "skipping cve_meta parsing due to license check")
return nil
}
func CVEMetaFromNVDFeedFiles(metaMap map[string]fleet.CVEMeta, vulnPath string, logger log.Logger) error {
// load cvss scores
files, err := getNVDCVEFeedFiles(vulnPath)
if err != nil {
return fmt.Errorf("get nvd cve feeds: %w", err)
}
metaMap := make(map[string]fleet.CVEMeta)
for _, file := range files {
// Load json files one at a time. Attempting to load them all uses too much memory, > 1 GB.
@ -236,6 +228,10 @@ func LoadCVEMeta(ctx context.Context, logger log.Logger, vulnPath string, ds fle
}
}
return nil
}
func CVEMetaFromEPSSFeedFiles(metaMap map[string]fleet.CVEMeta, vulnPath string, logger log.Logger) error {
// load epss scores
path := filepath.Join(vulnPath, strings.TrimSuffix(epssFilename, ".gz"))
@ -254,8 +250,12 @@ func LoadCVEMeta(ctx context.Context, logger log.Logger, vulnPath string, ds fle
metaMap[epssScore.CVE] = score
}
return nil
}
func CVEMetaFromCISAFeedFiles(metaMap map[string]fleet.CVEMeta, vulnPath string, logger log.Logger) error {
// load known exploits
path = filepath.Join(vulnPath, cisaKnownExploitsFilename)
path := filepath.Join(vulnPath, cisaKnownExploitsFilename)
b, err := os.ReadFile(path)
if err != nil {
return err
@ -283,6 +283,43 @@ func LoadCVEMeta(ctx context.Context, logger log.Logger, vulnPath string, ds fle
metaMap[cve] = meta
}
return nil
}
func CVEMetaFromFiles(vulnPath string, logger log.Logger) (map[string]fleet.CVEMeta, error) {
metaMap := make(map[string]fleet.CVEMeta)
err := CVEMetaFromNVDFeedFiles(metaMap, vulnPath, logger)
if err != nil {
return nil, fmt.Errorf("nvd meta: %w", err)
}
err = CVEMetaFromEPSSFeedFiles(metaMap, vulnPath, logger)
if err != nil {
return nil, fmt.Errorf("epss meta: %w", err)
}
err = CVEMetaFromCISAFeedFiles(metaMap, vulnPath, logger)
if err != nil {
return nil, fmt.Errorf("cisa meta: %w", err)
}
return metaMap, nil
}
// LoadCVEMeta loads the cvss scores, epss scores, and known exploits from the previously downloaded feeds and saves
// them to the database.
func LoadCVEMeta(ctx context.Context, logger log.Logger, vulnPath string, ds fleet.Datastore) error {
if !license.IsPremium(ctx) {
level.Info(logger).Log("msg", "skipping cve_meta parsing due to license check")
return nil
}
metaMap, err := CVEMetaFromFiles(vulnPath, logger)
if err != nil {
return err
}
if len(metaMap) == 0 {
return nil
}

View file

@ -12,12 +12,21 @@ import (
type Platform string
// OvalFilePrefix is the file prefix used when saving an OVAL artifact.
const OvalFilePrefix = "fleet_oval"
const GovalDictionaryFilePrefix = "fleet_goval_dictionary"
const (
OvalFilePrefix = "fleet_oval"
GovalDictionaryFilePrefix = "fleet_goval_dictionary"
)
// SupportedSoftwareSources are the software sources for which we are using OVAL or goval-dictionary for vulnerability detection.
var SupportedSoftwareSources = []string{"deb_packages", "rpm_packages"}
var SupportedGovalPlatforms = []string{
"amzn_01",
"amzn_02",
"amzn_2022",
"amzn_2023",
}
// getMajorMinorVer returns the major and minor version of an 'os_version'.
// ex: 'Ubuntu 20.4.0' => '(20, 04)'
func getMajorMinorVer(osVersion string) (string, string) {
@ -74,6 +83,12 @@ func (op Platform) ToGovalDictionaryFilename() string {
return fmt.Sprintf("%s_%s.sqlite3", GovalDictionaryFilePrefix, op)
}
// ToGovalDatabaseFilename returns the filename of the sqlite3 files downloaded using
// the goval-dictionary fetch method in the vulnerabilities generate-cve.yml workflow
func (op Platform) ToGovalDatabaseFilename() string {
return fmt.Sprintf("%s.sqlite3", op)
}
// IsSupported returns whether the given platform is currently supported.
func (op Platform) IsSupported() bool {
supported := []string{
@ -104,14 +119,7 @@ func (op Platform) IsSupported() bool {
}
func (op Platform) IsGovalDictionarySupported() bool {
supported := []string{
"amzn_01",
"amzn_02",
"amzn_2022",
"amzn_2023",
}
for _, p := range supported {
for _, p := range SupportedGovalPlatforms {
if strings.HasPrefix(string(op), p) {
return true
}

View file

@ -88,4 +88,20 @@ func TestOvalPlatform(t *testing.T) {
require.Equal(t, c.expected, plat.ToGovalDictionaryFilename())
}
})
t.Run("ToGovalDatabaseFilename", func(t *testing.T) {
cases := []struct {
version string
expected string
}{
{"Amazon Linux 1.0.0", "amzn_01.sqlite3"},
{"Amazon Linux 2.0.0", "amzn_02.sqlite3"},
{"Amazon Linux 2022.0.0", "amzn_2022.sqlite3"},
{"Amazon Linux 2023.0.0", "amzn_2023.sqlite3"},
}
for _, c := range cases {
plat := NewPlatform("amzn", c.version)
require.Equal(t, c.expected, plat.ToGovalDatabaseFilename())
}
})
}