mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Fixes: #31989 # Adding sw_edition to CPE generation and translation This PR adds the ability to override sw_edition with cpe translations. This adds a new column to cpe.sqlite that is generated daily. Old versions of fleet will still work with the new cpe db and translations. Versions from this change forward will require the new cpe db for cpe translations to work. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## Backwards Compatibility Testing with physical machines and for Firefox ESR fix | Fleet version | cpe db | translations | vuln. soft. # | Firefox ESR cpe | Firefox ESR vuln. # | | ------- | ------ | ------------ | ------------- | ---------------- | ------------------- | | Updated | old | old | 58 | `:*:macos:*:*` | 168 | | Updated | new | new | 58 | `:esr:macos:*:*` | 92 | | 4.71.1 | old | old | 58 | `:*:macos:*:*` | 168 | | 4.71.1 | new | new | 58 | `:*:macos:*:*` | 168 | Testing with osquery-perf hosts | Fleet version | cpe db | translations | vuln. soft. # | Vulnerabilities | | ------- | ------ | ------------ | ------------- | --------------- | | Updated | old | old | 156/161 | 3136 | | Updated | new | new | 156/161 | 3136 | | 4.71.1 | old | old | 156/161 | 3951 | | 4.71.1 | new | new | 156/161 | 3951 | --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
227 lines
5.5 KiB
Go
227 lines
5.5 KiB
Go
package nvd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/download"
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/google/go-github/v37/github"
|
|
)
|
|
|
|
const cpeTranslationsFilename = "cpe_translations.json"
|
|
|
|
// Exported for testing.
|
|
func LoadCPETranslations(path string) (CPETranslations, error) {
|
|
return loadCPETranslations(path)
|
|
}
|
|
|
|
func loadCPETranslations(path string) (CPETranslations, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
var translations CPETranslations
|
|
if err := json.NewDecoder(f).Decode(&translations); err != nil {
|
|
return nil, fmt.Errorf("decode json: %w", err)
|
|
}
|
|
|
|
return translations, nil
|
|
}
|
|
|
|
// DownloadCPETranslationsFromGithub downloads the CPE translations to the given vulnPath. If cpeTranslationsURL is empty, attempts to download it
|
|
// from the latest release of github.com/fleetdm/nvd. Skips downloading if CPE translations is newer than the release.
|
|
func DownloadCPETranslationsFromGithub(vulnPath string, cpeTranslationsURL string) error {
|
|
path := filepath.Join(vulnPath, cpeTranslationsFilename)
|
|
|
|
if cpeTranslationsURL == "" {
|
|
stat, err := os.Stat(path)
|
|
switch {
|
|
case errors.Is(err, os.ErrNotExist):
|
|
// okay
|
|
case err != nil:
|
|
return err
|
|
case stat.ModTime().Truncate(24 * time.Hour).Equal(time.Now().Truncate(24 * time.Hour)):
|
|
// Vulnerability assets are published once per day - if the asset in question has a
|
|
// mod date of 'today', then we can assume that is already up to day.
|
|
return nil
|
|
}
|
|
|
|
release, asset, err := GetGithubNVDAsset(func(asset *github.ReleaseAsset) bool {
|
|
return cpeTranslationsFilename == asset.GetName()
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if asset == nil {
|
|
return errors.New("failed to find cpe translations in nvd release")
|
|
}
|
|
if stat != nil && stat.ModTime().After(release.CreatedAt.Time) {
|
|
// file is newer than release, do nothing
|
|
return nil
|
|
}
|
|
cpeTranslationsURL = asset.GetBrowserDownloadURL()
|
|
}
|
|
|
|
u, err := url.Parse(cpeTranslationsURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
client := fleethttp.NewGithubClient()
|
|
if err := download.Download(client, u, path); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// regexpCache caches compiled regular expressions. Not safe for concurrent use.
|
|
type regexpCache struct {
|
|
re map[string]*regexp.Regexp
|
|
}
|
|
|
|
func newRegexpCache() *regexpCache {
|
|
return ®expCache{re: make(map[string]*regexp.Regexp)}
|
|
}
|
|
|
|
func (r *regexpCache) Get(pattern string) (*regexp.Regexp, error) {
|
|
if re, ok := r.re[pattern]; ok {
|
|
return re, nil
|
|
}
|
|
|
|
re, err := regexp.Compile(pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.re[pattern] = re
|
|
return re, nil
|
|
}
|
|
|
|
// CPETranslations include special case translations for software that fail to match entries in the NVD CPE Dictionary
|
|
// using the standard logic. This may be due to unexpected vendor or product names.
|
|
//
|
|
// Example:
|
|
//
|
|
// [
|
|
// {
|
|
// "software": {
|
|
// "bundle_identifier": ["com.1password.1password"]
|
|
// },
|
|
// "filter": {
|
|
// "product": ["1password"],
|
|
// "vendor": ["agilebits"]
|
|
// }
|
|
// }
|
|
// ]
|
|
type CPETranslations []CPETranslationItem
|
|
|
|
func (c CPETranslations) Translate(reCache *regexpCache, s *fleet.Software) (CPETranslation, bool, error) {
|
|
for _, item := range c {
|
|
match, err := item.Software.Matches(reCache, s)
|
|
if err != nil {
|
|
return CPETranslation{}, false, err
|
|
}
|
|
if match {
|
|
return item.Filter, true, nil
|
|
}
|
|
}
|
|
|
|
return CPETranslation{}, false, nil
|
|
}
|
|
|
|
type CPETranslationItem struct {
|
|
Software CPETranslationSoftware `json:"software"`
|
|
Filter CPETranslation `json:"filter"`
|
|
}
|
|
|
|
// CPETranslationSoftware represents software match criteria for cpe translations.
|
|
type CPETranslationSoftware struct {
|
|
Name []string `json:"name"`
|
|
BundleIdentifier []string `json:"bundle_identifier"`
|
|
Source []string `json:"source"`
|
|
}
|
|
|
|
// Matches returns true if the software satifies all the match criteria.
|
|
func (c CPETranslationSoftware) Matches(reCache *regexpCache, s *fleet.Software) (bool, error) {
|
|
matches := func(a, b string) (bool, error) {
|
|
// check if its a regular expression enclosed in '/'
|
|
if len(a) > 2 && a[0] == '/' && a[len(a)-1] == '/' {
|
|
pattern := a[1 : len(a)-1]
|
|
re, err := reCache.Get(pattern)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return re.MatchString(b), nil
|
|
}
|
|
return a == b, nil
|
|
}
|
|
|
|
if len(c.Name) > 0 {
|
|
found := false
|
|
for _, name := range c.Name {
|
|
match, err := matches(name, s.Name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if match {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false, nil
|
|
}
|
|
}
|
|
if len(c.BundleIdentifier) > 0 {
|
|
found := false
|
|
for _, bundleID := range c.BundleIdentifier {
|
|
match, err := matches(bundleID, s.BundleIdentifier)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if match {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false, nil
|
|
}
|
|
}
|
|
if len(c.Source) > 0 {
|
|
found := false
|
|
for _, source := range c.Source {
|
|
match, err := matches(source, s.Source)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if match {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
type CPETranslation struct {
|
|
Product []string `json:"product"`
|
|
Vendor []string `json:"vendor"`
|
|
TargetSW []string `json:"target_sw"`
|
|
SWEdition []string `json:"sw_edition"`
|
|
Part string `json:"part"`
|
|
// If Skip is set, no NVD vulnerabilities will be reported for the matching software.
|
|
Skip bool `json:"skip"`
|
|
}
|