support/scripts/cve.py: fix CPE matching
Given the following criteria: `cpe:2.3🅰️oneidentitty:syslog-ng:*:*:*:*:-:*:*:*`. The former `cpe_matches` implementation would match with the following CPE: `cpe:2.3🅰️oneidentitty:syslog-ng:4.71:*:*:*:premium:*:*:*`. The 'hyphen' ('-') meaning is "Not Attributed" (NA) a criteria with no attributed software edition shouldn't match with a CPE with an attributed software edition: https://csrc.nist.gov/pubs/ir/7695/final This patch also create a distinct 'CPE' object that aggregate the function specifics to CPEs like it's done for 'CVE'. Signed-off-by: Thomas Perale <thomas.perale@mind.be> Signed-off-by: Peter Korsgaard <peter@korsgaard.com>
This commit is contained in:
committed by
Peter Korsgaard
parent
4b318dea17
commit
35f376d88e
@@ -39,22 +39,98 @@ ops = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Check if two CPE IDs match each other
|
class CPE:
|
||||||
def cpe_matches(cpe1, cpe2):
|
DISJOINT = 0
|
||||||
cpe1_elems = cpe1.split(":")
|
SUBSET = 1
|
||||||
cpe2_elems = cpe2.split(":")
|
SUPERSET = 2
|
||||||
|
EQUAL = 3
|
||||||
|
|
||||||
remains = filter(lambda x: x[0] not in ["*", "-"] and x[1] not in ["*", "-"] and x[0] != x[1],
|
ANY = '*'
|
||||||
zip(cpe1_elems, cpe2_elems))
|
NA = '-'
|
||||||
return len(list(remains)) == 0
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def compareAttribute(left, right):
|
||||||
|
"""
|
||||||
|
This static method compare two single attributes part of two CPE.
|
||||||
|
|
||||||
def cpe_product(cpe):
|
This is an implementation of table 6-2 of [1].
|
||||||
return cpe.split(':')[4]
|
|
||||||
|
|
||||||
|
Attribute that are empty will be matched to the '*' (ANY) attribute.
|
||||||
|
According to [2] section 6.1.2.1.1 the empty attribute is inherited
|
||||||
|
from CPE22 and now bind to ANY.
|
||||||
|
|
||||||
def cpe_version(cpe):
|
The hyphen '-' bind to the NA attribute (see [2]).
|
||||||
return cpe.split(':')[5]
|
|
||||||
|
[1] https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7696.pdf
|
||||||
|
[2] https://nvlpubs.nist.gov/nistpubs/Legacy/IR/nistir7695.pdf
|
||||||
|
"""
|
||||||
|
if left == '':
|
||||||
|
left = CPE.ANY
|
||||||
|
|
||||||
|
if right == '':
|
||||||
|
right = CPE.ANY
|
||||||
|
|
||||||
|
if left == right:
|
||||||
|
# 1 6 9 - equals
|
||||||
|
return CPE.EQUAL
|
||||||
|
elif left == CPE.ANY:
|
||||||
|
# 2 3 4 - superset
|
||||||
|
return CPE.SUPERSET
|
||||||
|
elif left == CPE.NA and right == CPE.ANY:
|
||||||
|
# 5 - subset
|
||||||
|
return CPE.SUBSET
|
||||||
|
elif left == CPE.NA:
|
||||||
|
# 12 16 - disjoint
|
||||||
|
return CPE.DISJOINT
|
||||||
|
elif right == CPE.ANY:
|
||||||
|
# 13 15 - subset
|
||||||
|
return CPE.SUBSET
|
||||||
|
return CPE.DISJOINT
|
||||||
|
|
||||||
|
def matches(self, target) -> bool:
|
||||||
|
"""
|
||||||
|
As an example let's take the example of CVE-2023-... for syslog-ng.
|
||||||
|
One of the node as the following CPE criteria matched with the Buildroot CPE:
|
||||||
|
|
||||||
|
cpe:2.3:a:oneidentitty:syslog-ng:*:*:*:*:-:*:*:*
|
||||||
|
cpe:2.3:a:oneidentitty:syslog-ng:4.71:*:*:*:*:*:*:*
|
||||||
|
|
||||||
|
vendor: EQUAL (3)
|
||||||
|
product: EQUAL (3)
|
||||||
|
version: SUPERSET (2)
|
||||||
|
update: EQUAL (3)
|
||||||
|
edition: EQUAL (3)
|
||||||
|
language: EQUAL (3)
|
||||||
|
sw_edition: SUBSET (1)
|
||||||
|
...
|
||||||
|
|
||||||
|
This operation results in the two CPE matching.
|
||||||
|
"""
|
||||||
|
if not isinstance(target, CPE):
|
||||||
|
target = CPE(target)
|
||||||
|
|
||||||
|
for selfAttribute, targetAttribute in zip(self.parts, target.parts):
|
||||||
|
if CPE.compareAttribute(selfAttribute, targetAttribute) == CPE.DISJOINT:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.cpe
|
||||||
|
|
||||||
|
def __init__(self, cpe):
|
||||||
|
self.cpe = cpe
|
||||||
|
self.parts = cpe.split(':')
|
||||||
|
self.vendor = self.parts[3]
|
||||||
|
self.product = self.parts[4]
|
||||||
|
self.version = self.parts[5]
|
||||||
|
self.update = self.parts[6]
|
||||||
|
self.edition = self.parts[7]
|
||||||
|
self.language = self.parts[8]
|
||||||
|
self.sw_edition = self.parts[9]
|
||||||
|
self.target_sw = self.parts[10]
|
||||||
|
self.target_hw = self.parts[11]
|
||||||
|
self.other = self.parts[12]
|
||||||
|
|
||||||
|
|
||||||
class CVE:
|
class CVE:
|
||||||
@@ -127,8 +203,9 @@ class CVE:
|
|||||||
for cpe in node.get('cpeMatch', ()):
|
for cpe in node.get('cpeMatch', ()):
|
||||||
if not cpe['vulnerable']:
|
if not cpe['vulnerable']:
|
||||||
return
|
return
|
||||||
product = cpe_product(cpe['criteria'])
|
cpeId = CPE(cpe['criteria'])
|
||||||
version = cpe_version(cpe['criteria'])
|
product = cpeId.product
|
||||||
|
version = cpeId.version
|
||||||
# ignore when product is '-', which means N/A
|
# ignore when product is '-', which means N/A
|
||||||
if product == '-':
|
if product == '-':
|
||||||
return
|
return
|
||||||
@@ -160,7 +237,7 @@ class CVE:
|
|||||||
v_end = cpe['versionEndExcluding']
|
v_end = cpe['versionEndExcluding']
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
'id': cpe['criteria'],
|
'id': cpeId,
|
||||||
'v_start': v_start,
|
'v_start': v_start,
|
||||||
'op_start': op_start,
|
'op_start': op_start,
|
||||||
'v_end': v_end,
|
'v_end': v_end,
|
||||||
@@ -181,30 +258,29 @@ class CVE:
|
|||||||
@property
|
@property
|
||||||
def affected_products(self):
|
def affected_products(self):
|
||||||
"""The set of CPE products referred by this CVE definition"""
|
"""The set of CPE products referred by this CVE definition"""
|
||||||
return set(cpe_product(p['id']) for p in self.each_cpe())
|
return set(p['id'].product for p in self.each_cpe())
|
||||||
|
|
||||||
def affects(self, name, version, cpeid=None):
|
def affects(self, name, version, cpeid=None):
|
||||||
"""
|
"""
|
||||||
True if the Buildroot Package object passed as argument is affected
|
True if the Buildroot Package object passed as argument is affected
|
||||||
by this CVE.
|
by this CVE.
|
||||||
"""
|
"""
|
||||||
|
if cpeid is None:
|
||||||
|
# if we don't have a cpeid, build one based on name and version
|
||||||
|
cpeid = CPE("cpe:2.3:*:*:%s:%s:*:*:*:*:*:*:*" % (name, version))
|
||||||
|
elif not isinstance(cpeid, CPE):
|
||||||
|
cpeid = CPE(cpeid)
|
||||||
|
|
||||||
pkg_version = distutils.version.LooseVersion(version)
|
# Always prefer the package version of the CPE ID.
|
||||||
|
pkg_version = distutils.version.LooseVersion(cpeid.version)
|
||||||
if not hasattr(pkg_version, "version"):
|
if not hasattr(pkg_version, "version"):
|
||||||
print("Cannot parse package '%s' version '%s'" % (name, version), file=sys.stderr)
|
print("Cannot parse package '%s' version '%s'" % (name, version), file=sys.stderr)
|
||||||
pkg_version = None
|
pkg_version = None
|
||||||
|
|
||||||
# if we don't have a cpeid, build one based on name and version
|
|
||||||
if not cpeid:
|
|
||||||
cpeid = "cpe:2.3:*:*:%s:%s:*:*:*:*:*:*:*" % (name, version)
|
|
||||||
# if we have a cpeid, use its version instead of the package
|
|
||||||
# version, as they might be different due to
|
|
||||||
# <pkg>_CPE_ID_VERSION
|
|
||||||
else:
|
|
||||||
pkg_version = distutils.version.LooseVersion(cpe_version(cpeid))
|
|
||||||
|
|
||||||
for cpe in self.each_cpe():
|
for cpe in self.each_cpe():
|
||||||
if not cpe_matches(cpe['id'], cpeid):
|
if not cpe['id'].matches(cpeid):
|
||||||
|
# If the node CPE id is not a subset of the target package we
|
||||||
|
# don't check for affect
|
||||||
continue
|
continue
|
||||||
if not cpe['v_start'] and not cpe['v_end']:
|
if not cpe['v_start'] and not cpe['v_end']:
|
||||||
return self.CVE_AFFECTS
|
return self.CVE_AFFECTS
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ def check_package_cves(nvd_path, packages):
|
|||||||
pkg.status['cve'] = ("na", "no version information available")
|
pkg.status['cve'] = ("na", "no version information available")
|
||||||
continue
|
continue
|
||||||
if pkg.cpeid:
|
if pkg.cpeid:
|
||||||
cpe_product = cvecheck.cpe_product(pkg.cpeid)
|
cpe_product = cvecheck.CPE(pkg.cpeid).product
|
||||||
cpe_product_pkgs[cpe_product].append(pkg)
|
cpe_product_pkgs[cpe_product].append(pkg)
|
||||||
else:
|
else:
|
||||||
cpe_product_pkgs[pkg.name].append(pkg)
|
cpe_product_pkgs[pkg.name].append(pkg)
|
||||||
|
|||||||
Reference in New Issue
Block a user