mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
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:
parent
997adcebe0
commit
bd2b2bcd3b
7 changed files with 216 additions and 29 deletions
79
cmd/cve/validate/main.go
Normal file
79
cmd/cve/validate/main.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue