Skip to content

Commit e42d095

Browse files
authored
Merge branch 'main' into 1228-fixed-affected-version-matching
2 parents 94cd01b + a114deb commit e42d095

52 files changed

Lines changed: 2084 additions & 1018 deletions

Some content is hidden

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

.gitignore

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,14 @@ coverage.xml
4646
*.log
4747
local_settings.py
4848

49-
# Sphinx documentation
50-
docs/_build/
49+
# Sphinx
50+
docs/_build
51+
docs/bin
52+
docs/build
53+
docs/include
54+
docs/Lib
55+
doc/pyvenv.cfg
56+
pyvenv.cfg
5157

5258
# PyBuilder
5359
target/
@@ -103,3 +109,13 @@ Pipfile
103109
*.bak
104110
/.cache/
105111
/tmp/
112+
113+
# pyenv
114+
/.python-version
115+
/man/
116+
/.pytest_cache/
117+
lib64
118+
tcl
119+
120+
# Ignore Jupyter Notebook related temp files
121+
.ipynb_checkpoints/

.readthedocs.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,25 @@
55
# Required
66
version: 2
77

8+
# Build in latest ubuntu/python
9+
build:
10+
os: ubuntu-22.04
11+
tools:
12+
python: "3.11"
13+
14+
# Build PDF & ePub
15+
formats:
16+
- epub
17+
- pdf
18+
819
# Where the Sphinx conf.py file is located
920
sphinx:
1021
configuration: docs/source/conf.py
1122

12-
# Setting the doc build requirements
23+
# Setting the python version and doc build requirements
1324
python:
14-
version: "3.7"
1525
install:
16-
- requirements: docs/requirements.txt
26+
- method: pip
27+
path: .
28+
extra_requirements:
29+
- dev

apache-2.0.LICENSE

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,28 @@
174174
of your accepting any such warranty or additional liability.
175175

176176
END OF TERMS AND CONDITIONS
177+
178+
APPENDIX: How to apply the Apache License to your work.
179+
180+
To apply the Apache License to your work, attach the following
181+
boilerplate notice, with the fields enclosed by brackets "[]"
182+
replaced with your own identifying information. (Don't include
183+
the brackets!) The text should be enclosed in the appropriate
184+
comment syntax for the file format. We also recommend that a
185+
file or class name and description of purpose be included on the
186+
same "printed page" as the copyright notice for easier
187+
identification within third-party archives.
188+
189+
Copyright [yyyy] [name of copyright owner]
190+
191+
Licensed under the Apache License, Version 2.0 (the "License");
192+
you may not use this file except in compliance with the License.
193+
You may obtain a copy of the License at
194+
195+
http://www.apache.org/licenses/LICENSE-2.0
196+
197+
Unless required by applicable law or agreed to in writing, software
198+
distributed under the License is distributed on an "AS IS" BASIS,
199+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200+
See the License for the specific language governing permissions and
201+
limitations under the License.

vulnerabilities/import_runner.py

Lines changed: 78 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -245,69 +245,97 @@ def create_valid_vulnerability_reference(url, reference_id=None):
245245
return reference
246246

247247

248-
def get_or_create_vulnerability_and_aliases(alias_names, vulnerability_id=None, summary=None):
248+
def get_or_create_vulnerability_and_aliases(
249+
aliases: List[str], vulnerability_id=None, summary=None
250+
):
249251
"""
250252
Get or create vulnerabilitiy and aliases such that all existing and new
251253
aliases point to the same vulnerability
252254
"""
253-
existing_vulns = set()
254-
alias_names = set(alias_names)
255-
new_alias_names = set()
256-
for alias_name in alias_names:
257-
try:
258-
alias = Alias.objects.get(alias=alias_name)
259-
existing_vulns.add(alias.vulnerability)
260-
except Alias.DoesNotExist:
261-
new_alias_names.add(alias_name)
262-
263-
# If given set of aliases point to different vulnerabilities in the
264-
# database, request is malformed
265-
# TODO: It is possible that all those vulnerabilities are actually
266-
# the same at data level, figure out a way to merge them
267-
if len(existing_vulns) > 1:
268-
logger.warning(
269-
f"Given aliases {alias_names} already exist and do not point "
270-
f"to a single vulnerability. Cannot improve. Skipped."
271-
)
272-
return
255+
aliases = set(alias.strip() for alias in aliases if alias and alias.strip())
256+
new_alias_names, existing_vulns = get_vulns_for_aliases_and_get_new_aliases(aliases)
257+
258+
# All aliases must point to the same vulnerability
259+
vulnerability = None
260+
if existing_vulns:
261+
if len(existing_vulns) != 1:
262+
vcids = ", ".join(v.vulnerability_id for v in existing_vulns)
263+
logger.error(
264+
f"Cannot create vulnerability. "
265+
f"Aliases {aliases} already exist and point "
266+
f"to multiple vulnerabilities {vcids}."
267+
)
268+
return
269+
else:
270+
vulnerability = existing_vulns.pop()
273271

274-
existing_alias_vuln = existing_vulns.pop() if existing_vulns else None
275-
276-
if (
277-
existing_alias_vuln
278-
and vulnerability_id
279-
and existing_alias_vuln.vulnerability_id != vulnerability_id
280-
):
281-
logger.warning(
282-
f"Given aliases {alias_names!r} already exist and point to existing"
283-
f"vulnerability {existing_alias_vuln}. Unable to create Vulnerability "
284-
f"with vulnerability_id {vulnerability_id}. Skipped"
285-
)
286-
return
272+
if vulnerability_id and vulnerability.vulnerability_id != vulnerability_id:
273+
logger.error(
274+
f"Cannot create vulnerability. "
275+
f"Aliases {aliases} already exist and point to a different "
276+
f"vulnerability {vulnerability} than the requested "
277+
f"vulnerability {vulnerability_id}."
278+
)
279+
return
287280

288-
if existing_alias_vuln:
289-
vulnerability = existing_alias_vuln
290-
elif vulnerability_id:
281+
if vulnerability_id and not vulnerability:
291282
try:
292283
vulnerability = Vulnerability.objects.get(vulnerability_id=vulnerability_id)
293284
except Vulnerability.DoesNotExist:
294-
logger.warning(
295-
f"Given vulnerability_id: {vulnerability_id} does not exist in the database"
296-
)
285+
logger.error(f"Cannot get requested vulnerability {vulnerability_id}.")
297286
return
287+
if vulnerability:
288+
# TODO: We should keep multiple summaries, one for each advisory
289+
# if summary and summary != vulnerability.summary:
290+
# logger.warning(
291+
# f"Inconsistent summary for {vulnerability.vulnerability_id}. "
292+
# f"Existing: {vulnerability.summary!r}, provided: {summary!r}"
293+
# )
294+
associate_vulnerability_with_aliases(vulnerability=vulnerability, aliases=new_alias_names)
298295
else:
299-
vulnerability = Vulnerability(summary=summary)
300-
vulnerability.save()
296+
try:
297+
vulnerability = create_vulnerability_and_add_aliases(
298+
aliases=new_alias_names, summary=summary
299+
)
300+
except Exception as e:
301+
logger.error(
302+
f"Cannot create vulnerability with summary {summary!r} and {new_alias_names!r} {e!r}.\n{traceback_format_exc()}."
303+
)
304+
return
305+
306+
return vulnerability
307+
308+
309+
def get_vulns_for_aliases_and_get_new_aliases(aliases):
310+
"""
311+
Return ``new_aliases`` that are not in the database and
312+
``existing_vulns`` that point to the given ``aliases``.
313+
"""
314+
new_aliases = set(aliases)
315+
existing_vulns = set()
316+
for alias in Alias.objects.filter(alias__in=aliases):
317+
existing_vulns.add(alias.vulnerability)
318+
new_aliases.remove(alias.alias)
319+
return new_aliases, existing_vulns
301320

302-
if summary and summary != vulnerability.summary:
303-
logger.warning(
304-
f"Inconsistent summary for {vulnerability!r}. "
305-
f"Existing: {vulnerability.summary}, provided: {summary}"
306-
)
307321

308-
for alias_name in new_alias_names:
322+
@transaction.atomic
323+
def create_vulnerability_and_add_aliases(aliases, summary):
324+
"""
325+
Return a new ``vulnerability`` created with ``summary``
326+
and associate the ``vulnerability`` with ``aliases``.
327+
Raise exception if no alias is associated with the ``vulnerability``.
328+
"""
329+
vulnerability = Vulnerability(summary=summary)
330+
vulnerability.save()
331+
associate_vulnerability_with_aliases(aliases, vulnerability)
332+
if not vulnerability.aliases.count():
333+
raise Exception(f"Vulnerability {vulnerability.vcid} must have one or more aliases")
334+
return vulnerability
335+
336+
337+
def associate_vulnerability_with_aliases(aliases, vulnerability):
338+
for alias_name in aliases:
309339
alias = Alias(alias=alias_name, vulnerability=vulnerability)
310340
alias.save()
311341
logger.info(f"New alias for {vulnerability!r}: {alias_name}")
312-
313-
return vulnerability

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from vulnerabilities.importers import npm
2626
from vulnerabilities.importers import nvd
2727
from vulnerabilities.importers import openssl
28+
from vulnerabilities.importers import oss_fuzz
2829
from vulnerabilities.importers import postgresql
2930
from vulnerabilities.importers import project_kb_msr2019
3031
from vulnerabilities.importers import pypa
@@ -65,6 +66,7 @@
6566
ubuntu_usn.UbuntuUSNImporter,
6667
fireeye.FireyeImporter,
6768
apache_kafka.ApacheKafkaImporter,
69+
oss_fuzz.OSSFuzzImporter,
6870
]
6971

7072
IMPORTERS_REGISTRY = {x.qualified_name: x for x in IMPORTERS_REGISTRY}

vulnerabilities/importers/github.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import Iterable
1212
from typing import Optional
1313

14+
from cwe2.database import Database
1415
from dateutil import parser as dateparser
1516
from packageurl import PackageURL
1617
from univers.version_range import RANGE_CLASS_BY_SCHEMES
@@ -24,11 +25,11 @@
2425
from vulnerabilities.importer import Reference
2526
from vulnerabilities.importer import VulnerabilitySeverity
2627
from vulnerabilities.utils import dedupe
28+
from vulnerabilities.utils import get_cwe_id
2729
from vulnerabilities.utils import get_item
2830

2931
logger = logging.getLogger(__name__)
3032

31-
3233
PACKAGE_TYPE_BY_GITHUB_ECOSYSTEM = {
3334
"MAVEN": "maven",
3435
"NUGET": "nuget",
@@ -63,6 +64,11 @@
6364
url
6465
}
6566
severity
67+
cwes(first: 10){
68+
nodes {
69+
cweId
70+
}
71+
}
6672
publishedAt
6773
}
6874
firstPatchedVersion{
@@ -227,10 +233,34 @@ def process_response(resp: dict, package_type: str) -> Iterable[AdvisoryData]:
227233
else:
228234
logger.error(f"Unknown identifier type {identifier_type!r} and value {value!r}")
229235

236+
weaknesses = get_cwes_from_github_advisory(advisory)
237+
230238
yield AdvisoryData(
231239
aliases=sorted(dedupe(aliases)),
232240
summary=summary,
233241
references=references,
234242
affected_packages=affected_packages,
235243
date_published=date_published,
244+
weaknesses=weaknesses,
236245
)
246+
247+
248+
def get_cwes_from_github_advisory(advisory) -> [int]:
249+
"""
250+
Return the cwe-id list from advisory ex: [ 522 ]
251+
by extracting the cwe_list from advisory ex: [{'cweId': 'CWE-522'}]
252+
then remove the CWE- from string and convert it to integer 522 and Check if the CWE in CWE-Database
253+
"""
254+
weaknesses = []
255+
db = Database()
256+
cwe_list = get_item(advisory, "cwes", "nodes") or []
257+
for cwe_item in cwe_list:
258+
cwe_string = get_item(cwe_item, "cweId")
259+
if cwe_string:
260+
cwe_id = get_cwe_id(cwe_string)
261+
try:
262+
db.get(cwe_id)
263+
weaknesses.append(cwe_id)
264+
except Exception:
265+
logger.error("Invalid CWE id")
266+
return weaknesses

vulnerabilities/importers/gitlab.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@
2828
from vulnerabilities.importer import Importer
2929
from vulnerabilities.importer import Reference
3030
from vulnerabilities.utils import build_description
31+
from vulnerabilities.utils import get_cwe_id
3132

3233
logger = logging.getLogger(__name__)
3334

34-
3535
PURL_TYPE_BY_GITLAB_SCHEME = {
3636
"conan": "conan",
3737
"gem": "gem",
@@ -44,7 +44,6 @@
4444
"pypi": "pypi",
4545
}
4646

47-
4847
GITLAB_SCHEME_BY_PURL_TYPE = {v: k for k, v in PURL_TYPE_BY_GITLAB_SCHEME.items()}
4948

5049

@@ -186,6 +185,10 @@ def parse_gitlab_advisory(file):
186185
summary = build_description(gitlab_advisory.get("title"), gitlab_advisory.get("description"))
187186
urls = gitlab_advisory.get("urls")
188187
references = [Reference.from_url(u) for u in urls]
188+
189+
cwe_ids = gitlab_advisory.get("cwe_ids") or []
190+
cwe_list = list(map(get_cwe_id, cwe_ids))
191+
189192
date_published = dateparser.parse(gitlab_advisory.get("pubdate"))
190193
date_published = date_published.replace(tzinfo=pytz.UTC)
191194
package_slug = gitlab_advisory.get("package_slug")
@@ -251,4 +254,5 @@ def parse_gitlab_advisory(file):
251254
references=references,
252255
date_published=date_published,
253256
affected_packages=affected_packages,
257+
weaknesses=cwe_list,
254258
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/nexB/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
import logging
10+
from pathlib import Path
11+
from typing import Iterable
12+
13+
import saneyaml
14+
15+
from vulnerabilities.importer import AdvisoryData
16+
from vulnerabilities.importer import Importer
17+
from vulnerabilities.importers.osv import parse_advisory_data
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
class OSSFuzzImporter(Importer):
23+
license_url = "https://github.com/google/oss-fuzz-vulns/blob/main/LICENSE"
24+
spdx_license_expression = "CC-BY-4.0"
25+
url = "git+https://github.com/google/oss-fuzz-vulns"
26+
27+
def advisory_data(self) -> Iterable[AdvisoryData]:
28+
try:
29+
self.clone(repo_url=self.url)
30+
path = Path(self.vcs_response.dest_dir) / "vulns"
31+
for file in path.glob("**/*.yaml"):
32+
with open(file) as f:
33+
yaml_data = saneyaml.load(f.read())
34+
yield parse_advisory_data(yaml_data, supported_ecosystem="oss-fuzz")
35+
finally:
36+
if self.vcs_response:
37+
self.vcs_response.delete()

0 commit comments

Comments
 (0)