fleet/server/vulnerabilities/nvd/cpe_translations.go
Juan Fernandez 812d3c85de
Fixes various bugs with NVD vulnerability detection (#7963)
- Improved NVD CPE matching process.
- Fixed bug with the 'software/<id>' endpoint not showing the generated_cpe value.
2022-10-04 07:04:48 -04:00

216 lines
4.8 KiB
Go

package nvd
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"github.com/fleetdm/fleet/v4/pkg/download"
"github.com/fleetdm/fleet/v4/server/fleet"
)
const cpeTranslationsFilename = "cpe_translations.json"
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
}
// DownloadCPETranslations 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 DownloadCPETranslations(vulnPath string, client *http.Client, cpeTranslationsURL string) error {
path := filepath.Join(vulnPath, cpeTranslationsFilename)
if cpeTranslationsURL == "" {
release, err := GetLatestNVDRelease(client)
if err != nil {
return err
}
stat, err := os.Stat(path)
switch {
case errors.Is(err, os.ErrNotExist):
// okay
case err != nil:
return err
default:
if stat.ModTime().After(release.CreatedAt.Time) {
// file is newer than release, do nothing
return nil
}
}
for _, asset := range release.Assets {
if cpeTranslationsFilename == asset.GetName() {
cpeTranslationsURL = asset.GetBrowserDownloadURL()
break
}
}
if cpeTranslationsURL == "" {
return errors.New("failed to find cpe translations in nvd release")
}
}
u, err := url.Parse(cpeTranslationsURL)
if err != nil {
return err
}
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 &regexpCache{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:
//
// [
// {
// "match": {
// "bundle_identifier": ["com.1password.1password"]
// },
// "translation": {
// "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"`
}