mirror of
https://github.com/anchore/syft.git
synced 2026-11-02 23:46:35 +01:00
feat: Record where CPEs come from (#2552)
Syft can get CPEs from several source, including generating them based on package data, finding them in the NVD CPE dictionary, or finding them declared in a manifest or existing SBOM. Record where Syft got CPEs so that consumers of SBOMs can reason about how trustworthy they are. Signed-off-by: Will Murphy <will.murphy@anchore.com>
This commit is contained in:
+69
-30
@@ -8,9 +8,29 @@ import (
|
||||
"github.com/facebookincubator/nvdtools/wfn"
|
||||
)
|
||||
|
||||
// CPE contains the attributes of an NVD Attributes and a string
|
||||
// describing where Syft got the Attributes, e.g. generated by heuristics
|
||||
// vs looked up in the NVD Attributes dictionary
|
||||
type CPE struct {
|
||||
Attributes Attributes
|
||||
Source Source
|
||||
}
|
||||
|
||||
type Source string
|
||||
|
||||
func (c Source) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
const (
|
||||
GeneratedSource Source = "syft-generated"
|
||||
NVDDictionaryLookupSource Source = "nvd-cpe-dictionary"
|
||||
DeclaredSource Source = "declared"
|
||||
)
|
||||
|
||||
const Any = ""
|
||||
|
||||
type CPE struct {
|
||||
type Attributes struct {
|
||||
Part string
|
||||
Vendor string
|
||||
Product string
|
||||
@@ -24,19 +44,19 @@ type CPE struct {
|
||||
Language string
|
||||
}
|
||||
|
||||
func (c CPE) asAttributes() wfn.Attributes {
|
||||
func (c Attributes) asAttributes() wfn.Attributes {
|
||||
return wfn.Attributes(c)
|
||||
}
|
||||
|
||||
func fromAttributes(a wfn.Attributes) CPE {
|
||||
return CPE(a)
|
||||
func fromAttributes(a wfn.Attributes) Attributes {
|
||||
return Attributes(a)
|
||||
}
|
||||
|
||||
func (c CPE) BindToFmtString() string {
|
||||
func (c Attributes) BindToFmtString() string {
|
||||
return c.asAttributes().BindToFmtString()
|
||||
}
|
||||
|
||||
func NewWithAny() CPE {
|
||||
func NewWithAny() Attributes {
|
||||
return fromAttributes(*(wfn.NewAttributesWithAny()))
|
||||
}
|
||||
|
||||
@@ -46,36 +66,55 @@ const (
|
||||
|
||||
// This regex string is taken from
|
||||
// https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd which has the official cpe spec
|
||||
// This first part matches CPE urls and the second part matches binding strings
|
||||
// This first part matches Attributes urls and the second part matches binding strings
|
||||
const cpeRegexString = ((`^([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})`) +
|
||||
// Or match the CPE binding string
|
||||
// Or match the Attributes binding string
|
||||
// Note that we had to replace '`' with '\x60' to escape the backticks
|
||||
`|(cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\*\-]))(:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&'\(\)\+,/:;<=>@\[\]\^\x60\{\|}~]))+(\?*|\*?))|[\*\-])){4})$`)
|
||||
|
||||
var cpeRegex = regexp.MustCompile(cpeRegexString)
|
||||
|
||||
// New will parse a formatted CPE string and return a CPE object. Some input, such as the existence of whitespace
|
||||
// characters is allowed, however, a more strict validation is done after this sanitization process.
|
||||
func New(cpeStr string) (CPE, error) {
|
||||
// get a CPE object based on the given string --don't validate yet since it may be possible to escape select cases on the callers behalf
|
||||
c, err := newWithoutValidation(cpeStr)
|
||||
func New(value string, source Source) (CPE, error) {
|
||||
attributes, err := NewAttributes(value)
|
||||
if err != nil {
|
||||
return CPE{}, fmt.Errorf("unable to parse CPE string: %w", err)
|
||||
}
|
||||
|
||||
// ensure that this CPE can be validated after being fully sanitized
|
||||
if ValidateString(c.String()) != nil {
|
||||
return CPE{}, err
|
||||
}
|
||||
return CPE{
|
||||
Attributes: attributes,
|
||||
Source: source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// we don't return the sanitized string, as this is a concern for later when creating CPE strings. In fact, since
|
||||
// NewAttributes will parse a formatted Attributes string and return a Attributes object. Some input, such as the existence of whitespace
|
||||
// characters is allowed, however, a more strict validation is done after this sanitization process.
|
||||
func NewAttributes(cpeStr string) (Attributes, error) {
|
||||
// get a Attributes object based on the given string --don't validate yet since it may be possible to escape select cases on the callers behalf
|
||||
c, err := newWithoutValidation(cpeStr)
|
||||
if err != nil {
|
||||
return Attributes{}, fmt.Errorf("unable to parse Attributes string: %w", err)
|
||||
}
|
||||
|
||||
// ensure that this Attributes can be validated after being fully sanitized
|
||||
if ValidateString(c.String()) != nil {
|
||||
return Attributes{}, err
|
||||
}
|
||||
|
||||
// we don't return the sanitized string, as this is a concern for later when creating Attributes strings. In fact, since
|
||||
// sanitization is lossy (whitespace is replaced, not escaped) it's important that the raw values are left as.
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Must returns a CPE or panics if the provided string is not valid
|
||||
func Must(cpeStr string) CPE {
|
||||
c, err := New(cpeStr)
|
||||
func Must(cpeStr string, source Source) CPE {
|
||||
c := MustAttributes(cpeStr)
|
||||
return CPE{
|
||||
Attributes: c,
|
||||
Source: source,
|
||||
}
|
||||
}
|
||||
|
||||
func MustAttributes(cpeStr string) Attributes {
|
||||
c, err := NewAttributes(cpeStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -83,22 +122,22 @@ func Must(cpeStr string) CPE {
|
||||
}
|
||||
|
||||
func ValidateString(cpeStr string) error {
|
||||
// We should filter out all CPEs that do not match the official CPE regex
|
||||
// The facebook nvdtools parser can sometimes incorrectly parse invalid CPE strings
|
||||
// We should filter out all CPEs that do not match the official Attributes regex
|
||||
// The facebook nvdtools parser can sometimes incorrectly parse invalid Attributes strings
|
||||
if !cpeRegex.MatchString(cpeStr) {
|
||||
return fmt.Errorf("failed to parse CPE=%q as it doesn't match the regex=%s", cpeStr, cpeRegexString)
|
||||
return fmt.Errorf("failed to parse Attributes=%q as it doesn't match the regex=%s", cpeStr, cpeRegexString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newWithoutValidation(cpeStr string) (CPE, error) {
|
||||
func newWithoutValidation(cpeStr string) (Attributes, error) {
|
||||
value, err := wfn.Parse(cpeStr)
|
||||
if err != nil {
|
||||
return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err)
|
||||
return Attributes{}, fmt.Errorf("failed to parse Attributes=%q: %w", cpeStr, err)
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return CPE{}, fmt.Errorf("failed to parse CPE=%q", cpeStr)
|
||||
return Attributes{}, fmt.Errorf("failed to parse Attributes=%q", cpeStr)
|
||||
}
|
||||
|
||||
syftCPE := fromAttributes(*value)
|
||||
@@ -120,7 +159,7 @@ func newWithoutValidation(cpeStr string) (CPE, error) {
|
||||
}
|
||||
|
||||
func normalizeField(field string) string {
|
||||
// replace spaces with underscores (per section 5.3.2 of the CPE spec v 2.3)
|
||||
// replace spaces with underscores (per section 5.3.2 of the Attributes spec v 2.3)
|
||||
field = strings.ReplaceAll(field, " ", "_")
|
||||
|
||||
// keep dashes and forward slashes unescaped
|
||||
@@ -144,8 +183,8 @@ func stripSlashes(s string) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (c CPE) String() string {
|
||||
output := CPE{}
|
||||
func (c Attributes) String() string {
|
||||
output := Attributes{}
|
||||
output.Vendor = sanitize(c.Vendor)
|
||||
output.Product = sanitize(c.Product)
|
||||
output.Language = sanitize(c.Language)
|
||||
|
||||
Reference in New Issue
Block a user