mirror of
https://github.com/empayre/fleet.git
synced 2024-11-07 01:15:22 +00:00
812d3c85de
- Improved NVD CPE matching process. - Fixed bug with the 'software/<id>' endpoint not showing the generated_cpe value.
216 lines
4.8 KiB
Go
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 ®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:
|
|
//
|
|
// [
|
|
// {
|
|
// "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"`
|
|
}
|