diff --git a/changes/11666-add-nvd-resolved-version b/changes/11666-add-nvd-resolved-version new file mode 100644 index 000000000..dbebcfc9d --- /dev/null +++ b/changes/11666-add-nvd-resolved-version @@ -0,0 +1 @@ +- (premium only) adds `resolved_in_version` to `/fleet/software` APIs pulled from NVD feed \ No newline at end of file diff --git a/server/datastore/mysql/migrations/tables/20230918132351_AddResolvedInVersionToSoftwareCVE.go b/server/datastore/mysql/migrations/tables/20230918132351_AddResolvedInVersionToSoftwareCVE.go new file mode 100644 index 000000000..45c436078 --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230918132351_AddResolvedInVersionToSoftwareCVE.go @@ -0,0 +1,27 @@ +package tables + +import ( + "database/sql" + "fmt" +) + +func init() { + MigrationClient.AddMigration(Up_20230918132351, Down_20230918132351) +} + +func Up_20230918132351(tx *sql.Tx) error { + stmt := ` + ALTER TABLE software_cve + ADD COLUMN resolved_in_version VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL; + ` + + if _, err := tx.Exec(stmt); err != nil { + return fmt.Errorf("add resolved_in_version column to software_cve: %w", err) + } + + return nil +} + +func Down_20230918132351(tx *sql.Tx) error { + return nil +} diff --git a/server/datastore/mysql/migrations/tables/20230918132351_AddResolvedInVersionToSoftwareCVE_test.go b/server/datastore/mysql/migrations/tables/20230918132351_AddResolvedInVersionToSoftwareCVE_test.go new file mode 100644 index 000000000..aee7202ed --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20230918132351_AddResolvedInVersionToSoftwareCVE_test.go @@ -0,0 +1,80 @@ +package tables + +import ( + "testing" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/require" +) + +func TestUp_20230918132351(t *testing.T) { + db := applyUpToPrev(t) + + insertStmt := ` + INSERT INTO software_cve ( + software_id, + source, + cve + ) + VALUES (?, ?, ?) + ` + args := []interface{}{ + 1, + 0, + "CVE-2021-1234", + } + + execNoErr(t, db, insertStmt, args...) + + // Apply current migration. + applyNext(t, db) + + // check for null resolved_in_version default + selectAndAssert(t, db, 1, 0, "CVE-2021-1234", nil) + + // Update the resolved_in_version and verify + updateVersion := "6.0.2-76060002.202210150739~1666289067~22.04~fe0ce53" // long string to test the capacity of the new column + updateStmt := ` + UPDATE software_cve + SET resolved_in_version = ? + WHERE software_id = ? AND source = ? AND cve = ? + ` + + execNoErr(t, db, updateStmt, updateVersion, 1, 0, "CVE-2021-1234") + selectAndAssert(t, db, 1, 0, "CVE-2021-1234", &updateVersion) + + // Insert a new record and verify + insertStmt = ` + INSERT INTO software_cve ( + software_id, + source, + cve, + resolved_in_version + ) + VALUES (?, ?, ?, ?) + ` + + execNoErr(t, db, insertStmt, 1, 0, "CVE-2021-1235", updateVersion) + selectAndAssert(t, db, 1, 0, "CVE-2021-1235", &updateVersion) +} + +func selectAndAssert(t *testing.T, db *sqlx.DB, softwareID uint, source uint, cve string, resolvedInVersion *string) { + var softwareCVE struct { + SoftwareID uint `db:"software_id"` + Source uint `db:"source"` + CVE string `db:"cve"` + ResolvedInVersion *string `db:"resolved_in_version"` + } + + selectStmt := ` + SELECT software_id, source, cve, resolved_in_version + FROM software_cve + WHERE software_id = ? AND source = ? AND cve = ? + ` + + require.NoError(t, db.Get(&softwareCVE, selectStmt, softwareID, source, cve)) + require.Equal(t, softwareID, softwareCVE.SoftwareID) + require.Equal(t, source, softwareCVE.Source) + require.Equal(t, cve, softwareCVE.CVE) + require.Equal(t, resolvedInVersion, softwareCVE.ResolvedInVersion) +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index 29a214d15..93c1e2703 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -685,9 +685,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=208 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB AUTO_INCREMENT=209 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `mobile_device_management_solutions` ( @@ -1196,6 +1196,7 @@ CREATE TABLE `software_cve` ( `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `source` int(11) DEFAULT '0', `software_id` bigint(20) unsigned DEFAULT NULL, + `resolved_in_version` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `unq_software_id_cve` (`software_id`,`cve`), KEY `software_cve_software_id` (`software_id`) diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index c095aad1f..63bca52ef 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -588,6 +588,7 @@ func listSoftwareDB( cve.CISAKnownExploit = &result.CISAKnownExploit cve.CVEPublished = &result.CVEPublished cve.Description = &result.Description + cve.ResolvedInVersion = &result.ResolvedInVersion } softwares[idx].Vulnerabilities = append(softwares[idx].Vulnerabilities, cve) } @@ -597,14 +598,33 @@ func listSoftwareDB( } // softwareCVE is used for left joins with cve +// +// + type softwareCVE struct { fleet.Software - CVE *string `db:"cve"` - CVSSScore *float64 `db:"cvss_score"` - EPSSProbability *float64 `db:"epss_probability"` - CISAKnownExploit *bool `db:"cisa_known_exploit"` - CVEPublished *time.Time `db:"cve_published"` - Description *string `db:"description"` + + // CVE is the CVE identifier pulled from the NVD json (e.g. CVE-2019-1234) + CVE *string `db:"cve"` + + // CVSSScore is the CVSS score pulled from the NVD json (premium only) + CVSSScore *float64 `db:"cvss_score"` + + // EPSSProbability is the EPSS probability pulled from FIRST (premium only) + EPSSProbability *float64 `db:"epss_probability"` + + // CISAKnownExploit is the CISAKnownExploit pulled from CISA (premium only) + CISAKnownExploit *bool `db:"cisa_known_exploit"` + + // CVEPublished is the CVE published date pulled from the NVD json (premium only) + CVEPublished *time.Time `db:"cve_published"` + + // Description is the CVE description field pulled from the NVD json + Description *string `db:"description"` + + // ResolvedInVersion is the version of software where the CVE is no longer applicable. + // This is pulled from the versionEndExcluding field in the NVD json + ResolvedInVersion *string `db:"resolved_in_version"` } func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, error) { @@ -694,11 +714,12 @@ func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, e goqu.On(goqu.I("c.cve").Eq(goqu.I("scv.cve"))), ). SelectAppend( - goqu.MAX("c.cvss_score").As("cvss_score"), // for ordering - goqu.MAX("c.epss_probability").As("epss_probability"), // for ordering - goqu.MAX("c.cisa_known_exploit").As("cisa_known_exploit"), // for ordering - goqu.MAX("c.published").As("cve_published"), // for ordering - goqu.MAX("c.description").As("description"), // for ordering + goqu.MAX("c.cvss_score").As("cvss_score"), // for ordering + goqu.MAX("c.epss_probability").As("epss_probability"), // for ordering + goqu.MAX("c.cisa_known_exploit").As("cisa_known_exploit"), // for ordering + goqu.MAX("c.published").As("cve_published"), // for ordering + goqu.MAX("c.description").As("description"), // for ordering + goqu.MAX("scv.resolved_in_version").As("resolved_in_version"), // for ordering ) } @@ -768,6 +789,7 @@ func selectSoftwareSQL(opts fleet.SoftwareListOptions) (string, []interface{}, e "c.cisa_known_exploit", "c.description", goqu.I("c.published").As("cve_published"), + "scv.resolved_in_version", ) } @@ -1068,6 +1090,7 @@ func (ds *Datastore) SoftwareByID(ctx context.Context, id uint, includeCVEScores "c.cisa_known_exploit", "c.description", goqu.I("c.published").As("cve_published"), + "scv.resolved_in_version", ) } @@ -1109,6 +1132,7 @@ func (ds *Datastore) SoftwareByID(ctx context.Context, id uint, includeCVEScores cve.EPSSProbability = &result.EPSSProbability cve.CISAKnownExploit = &result.CISAKnownExploit cve.CVEPublished = &result.CVEPublished + cve.ResolvedInVersion = &result.ResolvedInVersion } software.Vulnerabilities = append(software.Vulnerabilities, cve) } @@ -1408,8 +1432,15 @@ func (ds *Datastore) InsertSoftwareVulnerability( var args []interface{} - stmt := `INSERT INTO software_cve (cve, source, software_id) VALUES (?,?,?) ON DUPLICATE KEY UPDATE updated_at=?` - args = append(args, vuln.CVE, source, vuln.SoftwareID, time.Now().UTC()) + stmt := ` + INSERT INTO software_cve (cve, source, software_id, resolved_in_version) + VALUES (?,?,?,?) + ON DUPLICATE KEY UPDATE + source = VALUES(source), + resolved_in_version = VALUES(resolved_in_version), + updated_at=? + ` + args = append(args, vuln.CVE, source, vuln.SoftwareID, vuln.ResolvedInVersion, time.Now().UTC()) res, err := ds.writer(ctx).ExecContext(ctx, stmt, args...) if err != nil { @@ -1444,6 +1475,7 @@ func (ds *Datastore) ListSoftwareVulnerabilitiesByHostIDsSource( goqu.I("hs.host_id"), goqu.I("sc.software_id"), goqu.I("sc.cve"), + goqu.I("sc.resolved_in_version"), ). Where( goqu.I("hs.host_id").In(hostIDs), diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go index 40acd2d2b..2f4405c88 100644 --- a/server/datastore/mysql/software_test.go +++ b/server/datastore/mysql/software_test.go @@ -548,9 +548,9 @@ func testSoftwareList(t *testing.T, ds *Datastore) { }) vulns := []fleet.SoftwareVulnerability{ - {SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0001"}, - {SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0002"}, - {SoftwareID: host3.Software[0].ID, CVE: "CVE-2022-0003"}, + {SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0001", ResolvedInVersion: "2.0.0"}, + {SoftwareID: host1.Software[0].ID, CVE: "CVE-2022-0002", ResolvedInVersion: "2.0.0"}, + {SoftwareID: host3.Software[0].ID, CVE: "CVE-2022-0003", ResolvedInVersion: "2.0.0"}, } for _, v := range vulns { @@ -595,22 +595,24 @@ func testSoftwareList(t *testing.T, ds *Datastore) { GenerateCPE: "somecpe", Vulnerabilities: fleet.Vulnerabilities{ { - CVE: "CVE-2022-0001", - DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0001", - CVSSScore: ptr.Float64Ptr(2.0), - EPSSProbability: ptr.Float64Ptr(0.01), - CISAKnownExploit: ptr.BoolPtr(false), - CVEPublished: ptr.TimePtr(now.Add(-2 * time.Hour)), - Description: ptr.StringPtr("this is a description for CVE-2022-0001"), + CVE: "CVE-2022-0001", + DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0001", + CVSSScore: ptr.Float64Ptr(2.0), + EPSSProbability: ptr.Float64Ptr(0.01), + CISAKnownExploit: ptr.BoolPtr(false), + CVEPublished: ptr.TimePtr(now.Add(-2 * time.Hour)), + Description: ptr.StringPtr("this is a description for CVE-2022-0001"), + ResolvedInVersion: ptr.StringPtr("2.0.0"), }, { - CVE: "CVE-2022-0002", - DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0002", - CVSSScore: ptr.Float64Ptr(1.0), - EPSSProbability: ptr.Float64Ptr(0.99), - CISAKnownExploit: ptr.BoolPtr(false), - CVEPublished: ptr.TimePtr(now), - Description: ptr.StringPtr("this is a description for CVE-2022-0002"), + CVE: "CVE-2022-0002", + DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0002", + CVSSScore: ptr.Float64Ptr(1.0), + EPSSProbability: ptr.Float64Ptr(0.99), + CISAKnownExploit: ptr.BoolPtr(false), + CVEPublished: ptr.TimePtr(now), + Description: ptr.StringPtr("this is a description for CVE-2022-0002"), + ResolvedInVersion: ptr.StringPtr("2.0.0"), }, }, } @@ -624,13 +626,14 @@ func testSoftwareList(t *testing.T, ds *Datastore) { GenerateCPE: "somecpe2", Vulnerabilities: fleet.Vulnerabilities{ { - CVE: "CVE-2022-0003", - DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0003", - CVSSScore: ptr.Float64Ptr(3.0), - EPSSProbability: ptr.Float64Ptr(0.98), - CISAKnownExploit: ptr.BoolPtr(true), - CVEPublished: ptr.TimePtr(now.Add(-1 * time.Hour)), - Description: ptr.StringPtr("this is a description for CVE-2022-0003"), + CVE: "CVE-2022-0003", + DetailsLink: "https://nvd.nist.gov/vuln/detail/CVE-2022-0003", + CVSSScore: ptr.Float64Ptr(3.0), + EPSSProbability: ptr.Float64Ptr(0.98), + CISAKnownExploit: ptr.BoolPtr(true), + CVEPublished: ptr.TimePtr(now.Add(-1 * time.Hour)), + Description: ptr.StringPtr("this is a description for CVE-2022-0003"), + ResolvedInVersion: ptr.StringPtr("2.0.0"), }, }, } @@ -1810,6 +1813,46 @@ func testInsertSoftwareVulnerability(t *testing.T, ds *Datastore) { require.Equal(t, 1, occurrence["cve-1"]) require.Equal(t, 1, occurrence["cve-2"]) }) + + t.Run("vulnerability includes version range", func(t *testing.T) { + // new host + host := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now()) + + // new software + software := fleet.Software{ + Name: "host3software", Version: "0.0.1", Source: "chrome_extensions", + } + + _, err := ds.UpdateHostSoftware(ctx, host.ID, []fleet.Software{software}) + require.NoError(t, err) + require.NoError(t, ds.LoadHostSoftware(ctx, host, false)) + + // new software cpe + cpes := []fleet.SoftwareCPE{ + {SoftwareID: host.Software[0].ID, CPE: "cpe:2.3:a:foo:foo:0.0.1:*:*:*:*:*:*:*"}, + } + + _, err = ds.UpsertSoftwareCPEs(ctx, cpes) + require.NoError(t, err) + + // new vulnerability + vuln := fleet.SoftwareVulnerability{ + SoftwareID: host.Software[0].ID, + CVE: "cve-3", + ResolvedInVersion: "1.2.3", + } + + inserted, err := ds.InsertSoftwareVulnerability(ctx, vuln, fleet.UbuntuOVALSource) + require.NoError(t, err) + require.True(t, inserted) + + storedVulns, err := ds.ListSoftwareVulnerabilitiesByHostIDsSource(ctx, []uint{host.ID}, fleet.UbuntuOVALSource) + require.NoError(t, err) + + require.Len(t, storedVulns[host.ID], 1) + require.Equal(t, "cve-3", storedVulns[host.ID][0].CVE) + require.Equal(t, "1.2.3", storedVulns[host.ID][0].ResolvedInVersion) + }) } func testListCVEs(t *testing.T, ds *Datastore) { diff --git a/server/fleet/vulnerabilities.go b/server/fleet/vulnerabilities.go index 53626aee5..5449d270a 100644 --- a/server/fleet/vulnerabilities.go +++ b/server/fleet/vulnerabilities.go @@ -12,11 +12,12 @@ type CVE struct { // 1. omitted when using the free tier // 2. null when using the premium tier, but there is no value available. This may be due to an issue with syncing cve scores. // 3. non-null when using the premium tier, and value is available. - CVSSScore **float64 `json:"cvss_score,omitempty" db:"cvss_score"` - EPSSProbability **float64 `json:"epss_probability,omitempty" db:"epss_probability"` - CISAKnownExploit **bool `json:"cisa_known_exploit,omitempty" db:"cisa_known_exploit"` - CVEPublished **time.Time `json:"cve_published,omitempty" db:"cve_published"` - Description **string `json:"cve_description,omitempty" db:"description"` + CVSSScore **float64 `json:"cvss_score,omitempty" db:"cvss_score"` + EPSSProbability **float64 `json:"epss_probability,omitempty" db:"epss_probability"` + CISAKnownExploit **bool `json:"cisa_known_exploit,omitempty" db:"cisa_known_exploit"` + CVEPublished **time.Time `json:"cve_published,omitempty" db:"cve_published"` + Description **string `json:"cve_description,omitempty" db:"description"` + ResolvedInVersion **string `json:"resolved_in_version,omitempty" db:"resolved_in_version"` } type CVEMeta struct { @@ -48,8 +49,9 @@ type SoftwareCPE struct { // SoftwareVulnerability is a vulnerability on a software. // Represents an entry in the `software_cve` table. type SoftwareVulnerability struct { - SoftwareID uint `db:"software_id"` - CVE string `db:"cve"` + SoftwareID uint `db:"software_id"` + CVE string `db:"cve"` + ResolvedInVersion string `db:"resolved_in_version"` } // String implements fmt.Stringer. diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index d1068cb68..a4c0c9cd6 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -2912,8 +2912,9 @@ func (s *integrationEnterpriseTestSuite) TestListSoftware() { inserted, err := s.ds.InsertSoftwareVulnerability( ctx, fleet.SoftwareVulnerability{ - SoftwareID: bar.ID, - CVE: "cve-123", + SoftwareID: bar.ID, + CVE: "cve-123", + ResolvedInVersion: "1.2.3", }, fleet.NVDSource, ) require.NoError(t, err) @@ -2955,6 +2956,7 @@ func (s *integrationEnterpriseTestSuite) TestListSoftware() { require.NotNil(t, barPayload.Vulnerabilities[0].CISAKnownExploit, ptr.BoolPtr(true)) require.Equal(t, barPayload.Vulnerabilities[0].CVEPublished, ptr.TimePtr(now)) require.Equal(t, barPayload.Vulnerabilities[0].Description, ptr.StringPtr("a long description of the cve")) + require.Equal(t, barPayload.Vulnerabilities[0].ResolvedInVersion, ptr.StringPtr("1.2.3")) } // TestGitOpsUserActions tests the permissions listed in ../../docs/Using-Fleet/Permissions.md. diff --git a/server/vulnerabilities/nvd/cve.go b/server/vulnerabilities/nvd/cve.go index ac538fc01..72133525c 100644 --- a/server/vulnerabilities/nvd/cve.go +++ b/server/vulnerabilities/nvd/cve.go @@ -9,17 +9,28 @@ import ( "path/filepath" "regexp" "runtime" + "strings" "sync" "time" + "github.com/Masterminds/semver" "github.com/facebookincubator/nvdtools/cvefeed" + feednvd "github.com/facebookincubator/nvdtools/cvefeed/nvd" + "github.com/facebookincubator/nvdtools/cvefeed/nvd/schema" "github.com/facebookincubator/nvdtools/providers/nvd" "github.com/facebookincubator/nvdtools/wfn" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" ) +// Define a regex pattern for semver (simplified) +var semverPattern = regexp.MustCompile(`^v?(\d+\.\d+\.\d+)`) + +// Define a regex pattern for splitting version strings into subparts +var nonNumericPartRegex = regexp.MustCompile(`(\d+)(\D.*)`) + // DownloadNVDCVEFeed downloads the NVD CVE feed. Skips downloading if the cve feed has not changed since the last time. func DownloadNVDCVEFeed(vulnPath string, cveFeedPrefixURL string) error { cve := nvd.SupportedCVE["cve-1.1.json.gz"] @@ -110,11 +121,13 @@ func TranslateCPEToCVE( return nil, nil } + // get all the software CPEs from the database CPEs, err := ds.ListSoftwareCPEs(ctx) if err != nil { return nil, err } + // hydrate the CPEs with the meta data var parsed []softwareCPEWithNVDMeta for _, CPE := range CPEs { attr, err := wfn.Parse(CPE.CPE) @@ -243,9 +256,15 @@ func checkCVEs( } } + resolvedVersion, err := getMatchingVersionEndExcluding(ctx, matches.CVE.ID(), softwareCPE.meta, dict, logger) + if err != nil { + level.Debug(logger).Log(logKey, "error", "err", err) + } + vuln := fleet.SoftwareVulnerability{ - SoftwareID: softwareCPE.SoftwareID, - CVE: matches.CVE.ID(), + SoftwareID: softwareCPE.SoftwareID, + CVE: matches.CVE.ID(), + ResolvedInVersion: resolvedVersion, } mu.Lock() @@ -272,3 +291,166 @@ func checkCVEs( return foundVulns, nil } + +// Returns the versionEndExcluding string for the given CVE and host software meta +// data, if it exists in the NVD feed. This effectively gives us the version of the +// software it needs to upgrade to in order to address the CVE. +func getMatchingVersionEndExcluding(ctx context.Context, cve string, hostSoftwareMeta *wfn.Attributes, dict cvefeed.Dictionary, logger kitlog.Logger) (string, error) { + vuln, ok := dict[cve].(*feednvd.Vuln) + if !ok { + return "", nil + } + + // Schema() maps to the JSON schema of the NVD feed for a given CVE + vulnSchema := vuln.Schema() + if vulnSchema == nil { + level.Error(logger).Log("msg", "error getting schema for CVE", "cve", cve) + return "", nil + } + + config := vulnSchema.Configurations + if config == nil { + return "", nil + } + + nodes := config.Nodes + if len(nodes) == 0 { + return "", nil + } + + cpeMatch := findCPEMatch(nodes) + if len(cpeMatch) == 0 { + return "", nil + } + + // convert the host software version to semver for later comparison + formattedVersion := preprocessVersion(wfn.StripSlashes(hostSoftwareMeta.Version)) + softwareVersion, err := semver.NewVersion(formattedVersion) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "parsing software version", hostSoftwareMeta.Product, hostSoftwareMeta.Version) + } + + // Check if the host software version matches any of the CPEMatch rules. + // CPEMatch rules can include version strings for the following: + // - versionStartIncluding + // - versionStartExcluding + // - versionEndExcluding + // - versionEndIncluding - not used in this function as we don't want to assume the resolved version + for _, rule := range cpeMatch { + if rule.VersionEndExcluding == "" { + continue + } + + // convert the NVD cpe23URi to wfn.Attributes for later comparison + attr, err := wfn.Parse(rule.Cpe23Uri) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "parsing cpe23Uri") + } + + // ensure the product and vendor match + if attr.Product != hostSoftwareMeta.Product || attr.Vendor != hostSoftwareMeta.Vendor { + continue + } + + // versionEnd is the version string that the vulnerable host software version must be less than + versionEnd, err := checkVersion(ctx, rule, softwareVersion, cve) + if err != nil { + return "", ctxerr.Wrap(ctx, err, "checking version") + } + if versionEnd != "" { + return versionEnd, nil + } + } + + return "", nil +} + +// CPEMatch can be nested in Children nodes. Recursively search the nodes for a CPEMatch +func findCPEMatch(nodes []*schema.NVDCVEFeedJSON10DefNode) []*schema.NVDCVEFeedJSON10DefCPEMatch { + for _, node := range nodes { + if len(node.CPEMatch) > 0 { + return node.CPEMatch + } + + if len(node.Children) > 0 { + match := findCPEMatch(node.Children) + if match != nil { + return match + } + } + } + return nil +} + +// checkVersion checks if the host software version matches the CPEMatch rule +func checkVersion(ctx context.Context, rule *schema.NVDCVEFeedJSON10DefCPEMatch, softwareVersion *semver.Version, cve string) (string, error) { + for _, condition := range []struct { + startIncluding string + startExcluding string + }{ + {rule.VersionStartIncluding, ""}, + {"", rule.VersionStartExcluding}, + } { + constraintStr := buildConstraintString(condition.startIncluding, condition.startExcluding, rule.VersionEndExcluding) + if constraintStr == "" { + return rule.VersionEndExcluding, nil + } + + constraint, err := semver.NewConstraint(constraintStr) + if err != nil { + return "", ctxerr.Wrapf(ctx, err, "parsing constraint: %s for cve: %s", constraintStr, cve) + } + + if constraint.Check(softwareVersion) { + return rule.VersionEndExcluding, nil + } + } + + return "", nil +} + +// buildConstraintString builds a semver constraint string from the startIncluding, +// startExcluding, and endExcluding strings +func buildConstraintString(startIncluding, startExcluding, endExcluding string) string { + if startIncluding == "" && startExcluding == "" { + return "" + } + startIncluding = preprocessVersion(startIncluding) + startExcluding = preprocessVersion(startExcluding) + endExcluding = preprocessVersion(endExcluding) + + if startIncluding != "" { + return fmt.Sprintf(">= %s, < %s", startIncluding, endExcluding) + } + return fmt.Sprintf("> %s, < %s", startExcluding, endExcluding) +} + +// Products using 4 part versioning scheme (ie. docker desktop) +// need to be converted to 3 part versioning scheme (2.3.0.2 -> 2.3.0+3) for use with +// the semver library. +func preprocessVersion(version string) string { + // If "+" is already present, validate the part before "+" as a semver + if strings.Contains(version, "+") { + parts := strings.Split(version, "+") + if semverPattern.MatchString(parts[0]) { + return version + } + } + + // If the version string contains more than 3 parts, convert it to 3 parts + parts := strings.Split(version, ".") + if len(parts) > 3 { + return parts[0] + "." + parts[1] + "." + parts[2] + "+" + strings.Join(parts[3:], ".") + } + + // If the version string ends with a non-numeric character (like '1.0.0b'), replace + // it with '+' (like '1.0.0+b') + if len(parts) == 3 { + matches := nonNumericPartRegex.FindStringSubmatch(parts[2]) + if len(matches) > 2 { + parts[2] = matches[1] + "+" + matches[2] + } + } + + return strings.Join(parts, ".") +} diff --git a/server/vulnerabilities/nvd/cve_test.go b/server/vulnerabilities/nvd/cve_test.go index 05baec7c7..2953a795c 100644 --- a/server/vulnerabilities/nvd/cve_test.go +++ b/server/vulnerabilities/nvd/cve_test.go @@ -12,6 +12,8 @@ import ( "testing" "time" + "github.com/facebookincubator/nvdtools/cvefeed" + "github.com/facebookincubator/nvdtools/wfn" "github.com/fleetdm/fleet/v4/pkg/nettest" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" @@ -337,3 +339,100 @@ func TestSyncsCVEFromURL(t *testing.T) { fmt.Sprintf("1 synchronisation error:\n\tunexpected size for \"%s/feeds/json/cve/1.1/nvdcve-1.1-2002.json.gz\" (200 OK): want 1453293, have 0", ts.URL), ) } + +// This test is using real data from the 2022 NVD feed +func TestGetMatchingVersionEndExcluding(t *testing.T) { + ctx := context.Background() + testDict := loadDict(t, "../testdata/nvdcve-1.1-2022.json.gz") + + tests := []struct { + name string + cve string + meta *wfn.Attributes + want string + wantErr bool + }{ + { + name: "happy path with version with no Version Start", + cve: "CVE-2022-40897", + meta: &wfn.Attributes{ + Vendor: "python", + Product: "setuptools", + Version: "64", + }, + want: "65.5.1", + wantErr: false, + }, + { + name: "CVE matches multiple products", + cve: "CVE-2022-40956", + meta: &wfn.Attributes{ + Vendor: "mozilla", + Product: "firefox", + Version: "93.0.100", + }, + want: "105.0", + wantErr: false, + }, + { + name: "Nodes has nested Children", + cve: "CVE-2022-40961", + meta: &wfn.Attributes{ + Vendor: "mozilla", + Product: "firefox", + Version: "93.0.100", + }, + want: "105.0", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getMatchingVersionEndExcluding(ctx, tt.cve, tt.meta, testDict, nil) + if (err != nil) != tt.wantErr { + t.Errorf("getMatchingVersionEndExcluding() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("getMatchingVersionEndExcluding() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPreprocessVersion(t *testing.T) { + testCases := []struct { + input string + expected string + }{ + {"2.3.0.2", "2.3.0+2"}, + {"2.3.0+2", "2.3.0+2"}, + {"v2.3.0+2", "v2.3.0+2"}, + {"2.3.0.2.5", "2.3.0+2.5"}, + {"2.3.0", "2.3.0"}, + {"2.3", "2.3"}, + {"v2.3.0", "v2.3.0"}, + {"notAVersion", "notAVersion"}, + {"2.0.0+svn315-7fakesync1ubuntu0.22.04.1", "2.0.0+svn315-7fakesync1ubuntu0.22.04.1"}, + {"1.21.1ubuntu2", "1.21.1+ubuntu2"}, + } + + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + output := preprocessVersion(tc.input) + if output != tc.expected { + t.Fatalf("expected: %s, got: %s", tc.expected, output) + } + }) + } +} + +// loadDict loads a cvefeed.Dictionary from a JSON NVD feed file. +func loadDict(t *testing.T, path string) cvefeed.Dictionary { + dict, err := cvefeed.LoadJSONDictionary(path) + if err != nil { + t.Fatal(err) + } + return dict +}