mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 00:18:27 +00:00
Loom explaining changes (hit 5 min limit):
https://www.loom.com/share/e59b63bf638e4d9cad7984ef589b878d?sid=111fff75-115a-4a44-ae4f-6f25fede0d51
#14887
- [x] Need to merge fleetdm/nvd PR
https://github.com/fleetdm/nvd/pull/25 before this one.
# Checklist for submitter
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
- Manually tested (with corresponding fleetdm/fleet changes) in my
personal fork: https://github.com/getvictor/nvd/releases
# QA Plan (must be done before merging this PR, and after merging the
nvd PR)
- [ ] Fork https://github.com/fleetdm/nvd and point `generate.yml` to
this branch.
[example](9d8e54930b/.github/workflows/generate.yml (L26))
- [ ] Add NVD_API_KEY to nvd secrets, and run the the nvd generate
GitHub action. Get key:
https://nvd.nist.gov/developers/request-an-api-key
- [ ] Compare the generated `cpe-###.sqlite.gz` to the previous one. One
way is to open it up with sqlite3 and `select * from cpe_2 order by
cpe23;` and dump results to a CSV file. Known differences are:
- New file has ~2,500 more records
- Backslashes are handled differently for `Backpack\CRUD` and `Philips
In.Sight B120\37` products -- not a new issue since we do not support
those products right now
- `cpe:2.3🅰️moodle:moodle:4.2.0:*:*:*:*:*:*:*` -- this appears OK.
Also, it is a PHP plugin, and we don't support these currently.
- [ ] Record the existing vulnerabilities of current hosts.
- [ ] Stop any running fleet server. Delete `/tmp/vulndbs/cpe.sqlite`.
Can also delete other files there, or not delete this file -- it should
be overwritten by the new file. Also delete all rows in software_cpe and
software_cve DB tables. (Or can just spin up a fresh fleet server with
fresh DB, and re-enroll hosts (after setting the new env variable
below))
- [ ] Find the path to the generated `cpe-###.sqlite.gz` file
- [ ] Set `FLEET_VULNERABILITIES_CPE_DATABASE_URL` environment variable
to the above path, and start fleet server.
- [ ] After server's vulnerabilities cron job runs, the new
vulnerabilities should match the previous vulnerabilities
184 lines
4.6 KiB
Go
184 lines
4.6 KiB
Go
package nvd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/facebookincubator/nvdtools/cpedict"
|
|
"github.com/facebookincubator/nvdtools/wfn"
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
func sqliteDB(dbPath string) (*sqlx.DB, error) {
|
|
db, err := sqlx.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return db, nil
|
|
}
|
|
|
|
func applyCPEDatabaseSchema(db *sqlx.DB) error {
|
|
// Use a new table cpe_2 containing new columns vendor, product. view cpe used for backwards compatibility
|
|
// with old fleet versions that use "select * from cpe ...". When creating the view, we need to
|
|
// select rowid because it is used for joins between the cpe and cpe_search tables
|
|
_, err := db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS cpe_2 (
|
|
cpe23 TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
vendor TEXT,
|
|
product TEXT,
|
|
version TEXT,
|
|
target_sw TEXT,
|
|
deprecated BOOLEAN DEFAULT FALSE
|
|
);
|
|
CREATE VIEW IF NOT EXISTS cpe AS
|
|
SELECT
|
|
rowid,
|
|
cpe23,
|
|
title,
|
|
version,
|
|
target_sw,
|
|
deprecated
|
|
FROM cpe_2;
|
|
CREATE TABLE IF NOT EXISTS deprecated_by (
|
|
cpe_id INTEGER,
|
|
cpe23 TEXT NOT NULL,
|
|
FOREIGN KEY(cpe_id) REFERENCES cpe(rowid)
|
|
);
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS cpe_search USING fts5(title, target_sw);
|
|
CREATE INDEX IF NOT EXISTS idx_cpe_2_cpe23 ON cpe_2 (cpe23);
|
|
CREATE INDEX IF NOT EXISTS idx_cpe_2_vendor ON cpe_2 (vendor);
|
|
CREATE INDEX IF NOT EXISTS idx_cpe_2_product ON cpe_2 (product);
|
|
CREATE INDEX IF NOT EXISTS idx_cpe_2_version ON cpe_2 (version);
|
|
CREATE INDEX IF NOT EXISTS idx_cpe_2_target_sw ON cpe_2 (target_sw);
|
|
CREATE INDEX IF NOT EXISTS idx_deprecated_by ON deprecated_by (cpe23);
|
|
`)
|
|
return err
|
|
}
|
|
|
|
func generateCPEItem(item cpedict.CPEItem) ([]interface{}, map[string]string, error) {
|
|
var cpes []interface{}
|
|
deprecations := make(map[string]string)
|
|
|
|
cpe23 := wfn.Attributes(item.CPE23.Name).BindToFmtString()
|
|
title := item.Title["en-US"]
|
|
vendor := wfn.StripSlashes(item.CPE23.Name.Vendor)
|
|
product := wfn.StripSlashes(item.CPE23.Name.Product)
|
|
version := wfn.StripSlashes(item.CPE23.Name.Version)
|
|
targetSW := wfn.StripSlashes(item.CPE23.Name.TargetSW)
|
|
|
|
cpes = append(cpes, cpe23, title, vendor, product, version, targetSW, item.Deprecated)
|
|
|
|
if item.CPE23.Deprecation != nil {
|
|
for _, deprecatedBy := range item.CPE23.Deprecation.DeprecatedBy {
|
|
deprecatedByCPE23 := wfn.Attributes(deprecatedBy.Name).BindToFmtString()
|
|
deprecations[cpe23] = deprecatedByCPE23
|
|
}
|
|
}
|
|
|
|
return cpes, deprecations, nil
|
|
}
|
|
|
|
const batchSize = 800
|
|
|
|
func GenerateCPEDB(path string, items []cpedict.CPEItem) error {
|
|
err := os.Remove(path)
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
db, err := sqliteDB(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
|
|
err = applyCPEDatabaseSchema(db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cpesCount := 0
|
|
var cpesBatch []interface{}
|
|
deprecationsCount := 0
|
|
var deprecationsBatch []interface{}
|
|
|
|
for _, item := range items {
|
|
cpes, deprecations, err := generateCPEItem(item)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cpesBatch = append(cpesBatch, cpes...)
|
|
cpesCount++
|
|
if len(deprecations) > 0 {
|
|
deprecationsCount++
|
|
}
|
|
for key, val := range deprecations {
|
|
deprecationsBatch = append(deprecationsBatch, key, val)
|
|
}
|
|
if cpesCount > batchSize {
|
|
err = bulkInsertCPEs(cpesCount, db, cpesBatch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cpesBatch = []interface{}{}
|
|
cpesCount = 0
|
|
}
|
|
if deprecationsCount > batchSize {
|
|
err := bulkInsertDeprecations(deprecationsCount, db, deprecationsBatch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
deprecationsBatch = []interface{}{}
|
|
deprecationsCount = 0
|
|
}
|
|
}
|
|
if cpesCount > 0 {
|
|
err = bulkInsertCPEs(cpesCount, db, cpesBatch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if deprecationsCount > 0 {
|
|
err := bulkInsertDeprecations(deprecationsCount, db, deprecationsBatch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err = db.Exec(`INSERT INTO cpe_search (rowid, title, target_sw) select rowid, title, target_sw from cpe`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func bulkInsertDeprecations(deprecationsCount int, db *sqlx.DB, allDeprecations []interface{}) error {
|
|
values := strings.TrimSuffix(strings.Repeat("((SELECT rowid FROM CPE where cpe23 = ?), ?),", deprecationsCount), ",")
|
|
_, err := db.Exec(
|
|
fmt.Sprintf(`INSERT INTO deprecated_by(cpe_id, cpe23) VALUES %s`, values),
|
|
allDeprecations...,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func bulkInsertCPEs(cpesCount int, db *sqlx.DB, allCPEs []interface{}) error {
|
|
values := strings.TrimSuffix(strings.Repeat("(?, ?, ?, ?, ?, ?, ?), ", cpesCount), ", ")
|
|
_, err := db.Exec(
|
|
fmt.Sprintf(`
|
|
INSERT INTO cpe_2 (
|
|
cpe23,
|
|
title,
|
|
vendor,
|
|
product,
|
|
version,
|
|
target_sw,
|
|
deprecated
|
|
)
|
|
VALUES %s`, values),
|
|
allCPEs...,
|
|
)
|
|
return err
|
|
}
|