Skip to content

Commit 8801c90

Browse files
authored
Add advisory v2 (#1866)
Reference: VCIO-next: Design new Advisory -> Package -> Vulnerability models relationhips #1393 VCIO-next: Advisory model migration Batch 1 #1877 Add V2Advisory Model. V2Advisory Model should have relationships between other models like aliases, affected packages, references, severities and weaknesses. V2AdvisoyModel will have advisory ID. advisory ID will be a natural ID for example Redhat importer will have RHSA, NVD will have CVE, when there is no ID we will create one. For example NPM * Add AdvisoryV2 models Signed-off-by: Tushar Goel <[email protected]> * Do formatting changes Signed-off-by: Tushar Goel <[email protected]> * Add migrations Signed-off-by: Tushar Goel <[email protected]> * Add model changes and support new advisory ingestion Signed-off-by: Tushar Goel <[email protected]> * Add V2Pipelines Signed-off-by: Tushar Goel <[email protected]> * Revert alpine linux importer Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Refactor compute content ID Signed-off-by: Tushar Goel <[email protected]> * Formatting changes Signed-off-by: Tushar Goel <[email protected]> * Fix errors in compute content ID Signed-off-by: Tushar Goel <[email protected]> * Add github pipeline Signed-off-by: Tushar Goel <[email protected]> * Add V2 pipelines Signed-off-by: Tushar Goel <[email protected]> * Rename pipelines Signed-off-by: Tushar Goel <[email protected]> * Add V2 importer pipelines Signed-off-by: Tushar Goel <[email protected]> * Rename pipelines Signed-off-by: Tushar Goel <[email protected]> * Reorder importers in registry Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Add tests for apache HTTPD importer pipeline Signed-off-by: Tushar Goel <[email protected]> * Add tests for npm importer pipeline Signed-off-by: Tushar Goel <[email protected]> * Add tests for github importer pipeline Signed-off-by: Tushar Goel <[email protected]> * Add tests for pysec importer Signed-off-by: Tushar Goel <[email protected]> * Add license header files Signed-off-by: Tushar Goel <[email protected]> * Add tests for Pypa importer Signed-off-by: Tushar Goel <[email protected]> * Add tests for vulnrichment importer pipeline v2 Signed-off-by: Tushar Goel <[email protected]> * Add UI for V2 Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Add Advisory Detail View Signed-off-by: Tushar Goel <[email protected]> * Fix risk score pipeline Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Change API design Signed-off-by: Tushar Goel <[email protected]> * Add tests for gitlab importer Signed-off-by: Tushar Goel <[email protected]> * Test postgresql importer Signed-off-by: Tushar Goel <[email protected]> * Add tests for elixir security importer Signed-off-by: Tushar Goel <[email protected]> * Add tests for models Signed-off-by: Tushar Goel <[email protected]> * Merge changes Signed-off-by: Tushar Goel <[email protected]> * Add tests for compute package risk V2 Signed-off-by: Tushar Goel <[email protected]> * Add tests for compute package rank V2 Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Add tests for V2 Importer Pipeline Signed-off-by: Tushar Goel <[email protected]> * Add tests for exploits enhancement pipeline Signed-off-by: Tushar Goel <[email protected]> --------- Signed-off-by: Tushar Goel <[email protected]>
1 parent 11284ee commit 8801c90

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+9799
-126
lines changed

vulnerabilities/api_v2.py

Lines changed: 382 additions & 0 deletions
Large diffs are not rendered by default.

vulnerabilities/forms.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ class VulnerabilitySearchForm(forms.Form):
3636
)
3737

3838

39+
class AdvisorySearchForm(forms.Form):
40+
41+
search = forms.CharField(
42+
required=True,
43+
widget=forms.TextInput(attrs={"placeholder": "Advisory id or alias such as CVE or GHSA"}),
44+
)
45+
46+
3947
class ApiUserCreationForm(forms.ModelForm):
4048
"""
4149
Support a simplified creation for API-only users directly from the UI.

vulnerabilities/importer.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class VulnerabilitySeverity:
5555
value: str
5656
scoring_elements: str = ""
5757
published_at: Optional[datetime.datetime] = None
58+
url: Optional[str] = None
5859

5960
def to_dict(self):
6061
data = {
@@ -145,6 +146,54 @@ def from_url(cls, url):
145146
return cls(url=url)
146147

147148

149+
@dataclasses.dataclass(eq=True)
150+
@functools.total_ordering
151+
class ReferenceV2:
152+
reference_id: str = ""
153+
reference_type: str = ""
154+
url: str = ""
155+
156+
def __post_init__(self):
157+
if not self.url:
158+
raise TypeError("Reference must have a url")
159+
if self.reference_id and not isinstance(self.reference_id, str):
160+
self.reference_id = str(self.reference_id)
161+
162+
def __lt__(self, other):
163+
if not isinstance(other, Reference):
164+
return NotImplemented
165+
return self._cmp_key() < other._cmp_key()
166+
167+
# TODO: Add cache
168+
def _cmp_key(self):
169+
return (self.reference_id, self.reference_type, self.url)
170+
171+
def to_dict(self):
172+
"""Return a normalized dictionary representation"""
173+
return {
174+
"reference_id": self.reference_id,
175+
"reference_type": self.reference_type,
176+
"url": self.url,
177+
}
178+
179+
@classmethod
180+
def from_dict(cls, ref: dict):
181+
return cls(
182+
reference_id=str(ref["reference_id"]),
183+
reference_type=ref.get("reference_type") or "",
184+
url=ref["url"],
185+
)
186+
187+
@classmethod
188+
def from_url(cls, url):
189+
reference_id = get_reference_id(url)
190+
if "GHSA-" in reference_id.upper():
191+
return cls(reference_id=reference_id, url=url)
192+
if is_cve(reference_id):
193+
return cls(url=url, reference_id=reference_id.upper())
194+
return cls(url=url)
195+
196+
148197
class UnMergeablePackageError(Exception):
149198
"""
150199
Raised when a package cannot be merged with another one.
@@ -302,10 +351,81 @@ class AdvisoryData:
302351
date_published must be aware datetime
303352
"""
304353

354+
advisory_id: str = ""
305355
aliases: List[str] = dataclasses.field(default_factory=list)
306356
summary: Optional[str] = ""
307357
affected_packages: List[AffectedPackage] = dataclasses.field(default_factory=list)
308358
references: List[Reference] = dataclasses.field(default_factory=list)
359+
references_v2: List[ReferenceV2] = dataclasses.field(default_factory=list)
360+
date_published: Optional[datetime.datetime] = None
361+
weaknesses: List[int] = dataclasses.field(default_factory=list)
362+
severities: List[VulnerabilitySeverity] = dataclasses.field(default_factory=list)
363+
url: Optional[str] = None
364+
365+
def __post_init__(self):
366+
if self.date_published and not self.date_published.tzinfo:
367+
logger.warning(f"AdvisoryData with no tzinfo: {self!r}")
368+
if self.summary:
369+
self.summary = self.clean_summary(self.summary)
370+
371+
def clean_summary(self, summary):
372+
# https://nvd.nist.gov/vuln/detail/CVE-2013-4314
373+
# https://github.com/cms-dev/cms/issues/888#issuecomment-516977572
374+
summary = summary.strip()
375+
if summary:
376+
summary = summary.replace("\x00", "\uFFFD")
377+
return summary
378+
379+
def to_dict(self):
380+
return {
381+
"aliases": self.aliases,
382+
"summary": self.summary,
383+
"affected_packages": [pkg.to_dict() for pkg in self.affected_packages],
384+
"references": [ref.to_dict() for ref in self.references],
385+
"date_published": self.date_published.isoformat() if self.date_published else None,
386+
"weaknesses": self.weaknesses,
387+
"url": self.url if self.url else "",
388+
}
389+
390+
@classmethod
391+
def from_dict(cls, advisory_data):
392+
date_published = advisory_data["date_published"]
393+
transformed = {
394+
"aliases": advisory_data["aliases"],
395+
"summary": advisory_data["summary"],
396+
"affected_packages": [
397+
AffectedPackage.from_dict(pkg)
398+
for pkg in advisory_data["affected_packages"]
399+
if pkg is not None
400+
],
401+
"references": [Reference.from_dict(ref) for ref in advisory_data["references"]],
402+
"date_published": datetime.datetime.fromisoformat(date_published)
403+
if date_published
404+
else None,
405+
"weaknesses": advisory_data["weaknesses"],
406+
"url": advisory_data.get("url") or None,
407+
}
408+
return cls(**transformed)
409+
410+
411+
@dataclasses.dataclass(order=True)
412+
class AdvisoryDataV2:
413+
"""
414+
This data class expresses the contract between data sources and the import runner.
415+
416+
If a vulnerability_id is present then:
417+
summary or affected_packages or references must be present
418+
otherwise
419+
either affected_package or references should be present
420+
421+
date_published must be aware datetime
422+
"""
423+
424+
advisory_id: str = ""
425+
aliases: List[str] = dataclasses.field(default_factory=list)
426+
summary: Optional[str] = ""
427+
affected_packages: List[AffectedPackage] = dataclasses.field(default_factory=list)
428+
references: List[ReferenceV2] = dataclasses.field(default_factory=list)
309429
date_published: Optional[datetime.datetime] = None
310430
weaknesses: List[int] = dataclasses.field(default_factory=list)
311431
url: Optional[str] = None

vulnerabilities/importers/__init__.py

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
from vulnerabilities.importers import ubuntu_usn
3434
from vulnerabilities.importers import vulnrichment
3535
from vulnerabilities.importers import xen
36-
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipeline
3736
from vulnerabilities.pipelines import alpine_linux_importer
3837
from vulnerabilities.pipelines import github_importer
3938
from vulnerabilities.pipelines import gitlab_importer
@@ -42,45 +41,59 @@
4241
from vulnerabilities.pipelines import nvd_importer
4342
from vulnerabilities.pipelines import pypa_importer
4443
from vulnerabilities.pipelines import pysec_importer
44+
from vulnerabilities.pipelines.v2_importers import apache_httpd_importer as apache_httpd_v2
45+
from vulnerabilities.pipelines.v2_importers import github_importer as github_importer_v2
46+
from vulnerabilities.pipelines.v2_importers import gitlab_importer as gitlab_importer_v2
47+
from vulnerabilities.pipelines.v2_importers import npm_importer as npm_importer_v2
48+
from vulnerabilities.pipelines.v2_importers import nvd_importer as nvd_importer_v2
49+
from vulnerabilities.pipelines.v2_importers import pypa_importer as pypa_importer_v2
50+
from vulnerabilities.pipelines.v2_importers import pysec_importer as pysec_importer_v2
51+
from vulnerabilities.pipelines.v2_importers import vulnrichment_importer as vulnrichment_importer_v2
52+
from vulnerabilities.utils import create_registry
4553

46-
IMPORTERS_REGISTRY = [
47-
nvd_importer.NVDImporterPipeline,
48-
github_importer.GitHubAPIImporterPipeline,
49-
gitlab_importer.GitLabImporterPipeline,
50-
github_osv.GithubOSVImporter,
51-
pypa_importer.PyPaImporterPipeline,
52-
npm_importer.NpmImporterPipeline,
53-
nginx_importer.NginxImporterPipeline,
54-
pysec_importer.PyPIImporterPipeline,
55-
apache_tomcat.ApacheTomcatImporter,
56-
postgresql.PostgreSQLImporter,
57-
debian.DebianImporter,
58-
curl.CurlImporter,
59-
epss.EPSSImporter,
60-
vulnrichment.VulnrichImporter,
61-
alpine_linux_importer.AlpineLinuxImporterPipeline,
62-
ruby.RubyImporter,
63-
apache_kafka.ApacheKafkaImporter,
64-
openssl.OpensslImporter,
65-
redhat.RedhatImporter,
66-
archlinux.ArchlinuxImporter,
67-
ubuntu.UbuntuImporter,
68-
debian_oval.DebianOvalImporter,
69-
retiredotnet.RetireDotnetImporter,
70-
apache_httpd.ApacheHTTPDImporter,
71-
mozilla.MozillaImporter,
72-
gentoo.GentooImporter,
73-
istio.IstioImporter,
74-
project_kb_msr2019.ProjectKBMSRImporter,
75-
suse_scores.SUSESeverityScoreImporter,
76-
elixir_security.ElixirSecurityImporter,
77-
xen.XenImporter,
78-
ubuntu_usn.UbuntuUSNImporter,
79-
fireeye.FireyeImporter,
80-
oss_fuzz.OSSFuzzImporter,
81-
]
82-
83-
IMPORTERS_REGISTRY = {
84-
x.pipeline_id if issubclass(x, VulnerableCodeBaseImporterPipeline) else x.qualified_name: x
85-
for x in IMPORTERS_REGISTRY
86-
}
54+
IMPORTERS_REGISTRY = create_registry(
55+
[
56+
nvd_importer_v2.NVDImporterPipeline,
57+
github_importer_v2.GitHubAPIImporterPipeline,
58+
npm_importer_v2.NpmImporterPipeline,
59+
vulnrichment_importer_v2.VulnrichImporterPipeline,
60+
apache_httpd_v2.ApacheHTTPDImporterPipeline,
61+
pypa_importer_v2.PyPaImporterPipeline,
62+
gitlab_importer_v2.GitLabImporterPipeline,
63+
pysec_importer_v2.PyPIImporterPipeline,
64+
nvd_importer.NVDImporterPipeline,
65+
github_importer.GitHubAPIImporterPipeline,
66+
gitlab_importer.GitLabImporterPipeline,
67+
github_osv.GithubOSVImporter,
68+
pypa_importer.PyPaImporterPipeline,
69+
npm_importer.NpmImporterPipeline,
70+
nginx_importer.NginxImporterPipeline,
71+
pysec_importer.PyPIImporterPipeline,
72+
apache_tomcat.ApacheTomcatImporter,
73+
postgresql.PostgreSQLImporter,
74+
debian.DebianImporter,
75+
curl.CurlImporter,
76+
epss.EPSSImporter,
77+
vulnrichment.VulnrichImporter,
78+
alpine_linux_importer.AlpineLinuxImporterPipeline,
79+
ruby.RubyImporter,
80+
apache_kafka.ApacheKafkaImporter,
81+
openssl.OpensslImporter,
82+
redhat.RedhatImporter,
83+
archlinux.ArchlinuxImporter,
84+
ubuntu.UbuntuImporter,
85+
debian_oval.DebianOvalImporter,
86+
retiredotnet.RetireDotnetImporter,
87+
apache_httpd.ApacheHTTPDImporter,
88+
mozilla.MozillaImporter,
89+
gentoo.GentooImporter,
90+
istio.IstioImporter,
91+
project_kb_msr2019.ProjectKBMSRImporter,
92+
suse_scores.SUSESeverityScoreImporter,
93+
elixir_security.ElixirSecurityImporter,
94+
xen.XenImporter,
95+
ubuntu_usn.UbuntuUSNImporter,
96+
fireeye.FireyeImporter,
97+
oss_fuzz.OSSFuzzImporter,
98+
]
99+
)

vulnerabilities/importers/curl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def parse_advisory_data(raw_data) -> AdvisoryData:
9797
... ]
9898
... }
9999
>>> parse_advisory_data(raw_data)
100-
AdvisoryData(aliases=['CVE-2024-2379'], summary='QUIC certificate check bypass with wolfSSL', affected_packages=[AffectedPackage(package=PackageURL(type='generic', namespace='curl.se', name='curl', version=None, qualifiers={}, subpath=None), affected_version_range=GenericVersionRange(constraints=(VersionConstraint(comparator='=', version=SemverVersion(string='8.6.0')),)), fixed_version=SemverVersion(string='8.7.0'))], references=[Reference(reference_id='', reference_type='', url='https://curl.se/docs/CVE-2024-2379.html', severities=[VulnerabilitySeverity(system=Cvssv3ScoringSystem(identifier='cvssv3.1', name='CVSSv3.1 Base Score', url='https://www.first.org/cvss/v3-1/', notes='CVSSv3.1 base score and vector'), value='Low', scoring_elements='', published_at=None)]), Reference(reference_id='', reference_type='', url='https://hackerone.com/reports/2410774', severities=[])], date_published=datetime.datetime(2024, 3, 27, 8, 0, tzinfo=datetime.timezone.utc), weaknesses=[297], url='https://curl.se/docs/CVE-2024-2379.json')
100+
AdvisoryData(advisory_id='', aliases=['CVE-2024-2379'], summary='QUIC certificate check bypass with wolfSSL', affected_packages=[AffectedPackage(package=PackageURL(type='generic', namespace='curl.se', name='curl', version=None, qualifiers={}, subpath=None), affected_version_range=GenericVersionRange(constraints=(VersionConstraint(comparator='=', version=SemverVersion(string='8.6.0')),)), fixed_version=SemverVersion(string='8.7.0'))], references=[Reference(reference_id='', reference_type='', url='https://curl.se/docs/CVE-2024-2379.html', severities=[VulnerabilitySeverity(system=Cvssv3ScoringSystem(identifier='cvssv3.1', name='CVSSv3.1 Base Score', url='https://www.first.org/cvss/v3-1/', notes='CVSSv3.1 base score and vector'), value='Low', scoring_elements='', published_at=None, url=None)]), Reference(reference_id='', reference_type='', url='https://hackerone.com/reports/2410774', severities=[])], references_v2=[], date_published=datetime.datetime(2024, 3, 27, 8, 0, tzinfo=datetime.timezone.utc), weaknesses=[297], severities=[], url='https://curl.se/docs/CVE-2024-2379.json')
101101
"""
102102

103103
affected = get_item(raw_data, "affected")[0] if len(get_item(raw_data, "affected")) > 0 else []

vulnerabilities/importers/osv.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,74 @@ def parse_advisory_data(
107107
)
108108

109109

110+
def parse_advisory_data_v2(
111+
raw_data: dict, supported_ecosystems, advisory_url: str
112+
) -> Optional[AdvisoryData]:
113+
"""
114+
Return an AdvisoryData build from a ``raw_data`` mapping of OSV advisory and
115+
a ``supported_ecosystem`` string.
116+
"""
117+
advisory_id = raw_data.get("id") or ""
118+
if not advisory_id:
119+
logger.error(f"Missing advisory id in OSV data: {raw_data}")
120+
return None
121+
summary = raw_data.get("summary") or ""
122+
details = raw_data.get("details") or ""
123+
summary = build_description(summary=summary, description=details)
124+
aliases = raw_data.get("aliases") or []
125+
126+
date_published = get_published_date(raw_data=raw_data)
127+
severities = list(get_severities(raw_data=raw_data))
128+
references = get_references_v2(raw_data=raw_data)
129+
130+
affected_packages = []
131+
132+
for affected_pkg in raw_data.get("affected") or []:
133+
purl = get_affected_purl(affected_pkg=affected_pkg, raw_id=advisory_id)
134+
135+
if not purl or purl.type not in supported_ecosystems:
136+
logger.error(f"Unsupported package type: {affected_pkg!r} in OSV: {advisory_id!r}")
137+
continue
138+
139+
affected_version_range = get_affected_version_range(
140+
affected_pkg=affected_pkg,
141+
raw_id=advisory_id,
142+
supported_ecosystem=purl.type,
143+
)
144+
145+
for fixed_range in affected_pkg.get("ranges") or []:
146+
fixed_version = get_fixed_versions(
147+
fixed_range=fixed_range, raw_id=advisory_id, supported_ecosystem=purl.type
148+
)
149+
150+
for version in fixed_version:
151+
affected_packages.append(
152+
AffectedPackage(
153+
package=purl,
154+
affected_version_range=affected_version_range,
155+
fixed_version=version,
156+
)
157+
)
158+
database_specific = raw_data.get("database_specific") or {}
159+
cwe_ids = database_specific.get("cwe_ids") or []
160+
weaknesses = list(map(get_cwe_id, cwe_ids))
161+
162+
if advisory_id in aliases:
163+
aliases.remove(advisory_id)
164+
165+
return AdvisoryData(
166+
advisory_id=advisory_id,
167+
aliases=aliases,
168+
summary=summary,
169+
references_v2=references,
170+
severities=severities,
171+
affected_packages=affected_packages,
172+
date_published=date_published,
173+
weaknesses=weaknesses,
174+
url=advisory_url,
175+
)
176+
177+
110178
def extract_fixed_versions(fixed_range) -> Iterable[str]:
111179
"""
112180
Return a list of fixed version strings given a ``fixed_range`` mapping of
@@ -187,6 +255,23 @@ def get_references(raw_data, severities) -> List[Reference]:
187255
return references
188256

189257

258+
def get_references_v2(raw_data) -> List[Reference]:
259+
"""
260+
Return a list Reference extracted from a mapping of OSV ``raw_data`` given a
261+
``severities`` list of VulnerabilitySeverity.
262+
"""
263+
references = []
264+
for ref in raw_data.get("references") or []:
265+
if not ref:
266+
continue
267+
url = ref["url"]
268+
if not url:
269+
logger.error(f"Reference without URL : {ref!r} for OSV id: {raw_data['id']!r}")
270+
continue
271+
references.append(Reference(url=ref["url"]))
272+
return references
273+
274+
190275
def get_affected_purl(affected_pkg, raw_id):
191276
"""
192277
Return an affected PackageURL or None given a mapping of ``affected_pkg``

0 commit comments

Comments
 (0)