diff --git a/cmd/cve/validate/main.go b/cmd/cve/validate/main.go new file mode 100644 index 0000000000..99ce0f815a --- /dev/null +++ b/cmd/cve/validate/main.go @@ -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) +} diff --git a/server/vulnerabilities/goval_dictionary/analyzer.go b/server/vulnerabilities/goval_dictionary/analyzer.go index d4b6414e53..6350c4e77f 100644 --- a/server/vulnerabilities/goval_dictionary/analyzer.go +++ b/server/vulnerabilities/goval_dictionary/analyzer.go @@ -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) } diff --git a/server/vulnerabilities/goval_dictionary/database.go b/server/vulnerabilities/goval_dictionary/database.go index 1cb8e46ef6..a7194c9095 100644 --- a/server/vulnerabilities/goval_dictionary/database.go +++ b/server/vulnerabilities/goval_dictionary/database.go @@ -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 { diff --git a/server/vulnerabilities/goval_dictionary/database_test.go b/server/vulnerabilities/goval_dictionary/database_test.go index ed00326cbb..4a18dbcbf8 100644 --- a/server/vulnerabilities/goval_dictionary/database_test.go +++ b/server/vulnerabilities/goval_dictionary/database_test.go @@ -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()) + }) } diff --git a/server/vulnerabilities/nvd/sync.go b/server/vulnerabilities/nvd/sync.go index 65174e721b..d5dbb9e5fe 100644 --- a/server/vulnerabilities/nvd/sync.go +++ b/server/vulnerabilities/nvd/sync.go @@ -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 } diff --git a/server/vulnerabilities/oval/oval_platform.go b/server/vulnerabilities/oval/oval_platform.go index c9fcc7ba1b..b89a069eba 100644 --- a/server/vulnerabilities/oval/oval_platform.go +++ b/server/vulnerabilities/oval/oval_platform.go @@ -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 } diff --git a/server/vulnerabilities/oval/oval_platform_test.go b/server/vulnerabilities/oval/oval_platform_test.go index 86f402f885..d95bb6d4a3 100644 --- a/server/vulnerabilities/oval/oval_platform_test.go +++ b/server/vulnerabilities/oval/oval_platform_test.go @@ -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()) + } + }) }