Skip to content

Commit d6832bd

Browse files
committed
Merge branch sbs2001:nginx in main
Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
2 parents a27fb3a + 0e760ad commit d6832bd

8 files changed

Lines changed: 233 additions & 22 deletions

File tree

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ ipython-genutils==0.2.0
2121
jedi==0.17.0
2222
lxml==4.3.3
2323
more-itertools==8.0.2
24-
packageurl-python==0.9.1
24+
packageurl-python==0.9.3
2525
packaging==19.2
2626
parso==0.7.0
2727
pexpect==4.8.0

vulnerabilities/import_runner.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -273,22 +273,16 @@ def _get_or_create_vulnerability(
273273

274274

275275
def _get_or_create_package(p: PackageURL) -> Tuple[models.Package, bool]:
276-
version = p.version
277276

278-
query_kwargs = {
279-
"name": packageurl.normalize_name(p.name, p.type, encode=True),
280-
"version": version,
281-
"type": packageurl.normalize_type(p.type, encode=True),
282-
}
283-
284-
if p.namespace:
285-
query_kwargs["namespace"] = packageurl.normalize_namespace(p.namespace, p.type, encode=True)
286-
287-
if p.qualifiers:
288-
query_kwargs["qualifiers"] = packageurl.normalize_qualifiers(p.qualifiers, encode=False)
289-
290-
if p.subpath:
291-
query_kwargs["subpath"] = packageurl.normalize_subpath(p.subpath, encode=True)
277+
query_kwargs = {}
278+
for key, val in p.to_dict().items():
279+
if not val:
280+
if key == "qualifiers":
281+
query_kwargs[key] = {}
282+
else:
283+
query_kwargs[key] = ""
284+
else:
285+
query_kwargs[key] = val
292286

293287
return models.Package.objects.get_or_create(**query_kwargs)
294288

vulnerabilities/importer_yielder.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,15 @@
210210
'branch': 'vulnerability-data'
211211
},
212212
},
213+
{
214+
'name': 'nginx',
215+
'license': '',
216+
'last_run': None,
217+
'data_source': 'NginxDataSource',
218+
'data_source_cfg': {
219+
'etag': {}
220+
},
221+
},
213222

214223
]
215224

vulnerabilities/importers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@
4141
from vulnerabilities.importers.project_kb_msr2019 import ProjectKBMSRDataSource
4242
from vulnerabilities.importers.apache_httpd import ApacheHTTPDDataSource
4343
from vulnerabilities.importers.kaybee import KaybeeDataSource
44+
from vulnerabilities.importers.nginx import NginxDataSource

vulnerabilities/importers/github.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
1818
# VulnerableCode should be considered or used as legal advice. Consult an Attorney
1919
# for any legal advice.
20-
# VulnerableCode is a free software code scanning tool from nexB Inc. and others.
20+
# VulnerableCode is a free software from nexB Inc. and others.
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2222

2323
import asyncio

vulnerabilities/importers/nginx.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Copyright (c) nexB Inc. and others. All rights reserved.
2+
# http://nexb.com and https://github.com/nexB/vulnerablecode/
3+
# The VulnerableCode software is licensed under the Apache License version
4+
# Data generated with VulnerableCode require an acknowledgment.
5+
#
6+
# You may not use this software except in compliance with the License.
7+
# You may obtain a copy of the License at: http://apache.org/licenses/LICE
8+
# Unless required by applicable law or agreed to in writing, software dist
9+
# under the License is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
10+
# CONDITIONS OF ANY KIND, either express or implied. See the License for t
11+
# specific language governing permissions and limitations under the Licens
12+
#
13+
# When you publish or redistribute any data created with VulnerableCode or
14+
# derivative work, you must accompany this data with the following acknowl
15+
#
16+
# Generated with VulnerableCode and provided on an 'AS IS' BASIS, WITHOUT
17+
# OR CONDITIONS OF ANY KIND, either express or implied. No content create
18+
# VulnerableCode should be considered or used as legal advice. Consult an
19+
# for any legal advice.
20+
# VulnerableCode is a free software from nexB Inc. and others.
21+
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
22+
23+
import asyncio
24+
import dataclasses
25+
import json
26+
27+
import requests
28+
from packageurl import PackageURL
29+
from bs4 import BeautifulSoup
30+
from dephell_specifier import RangeSpecifier
31+
32+
from vulnerabilities.data_source import Advisory
33+
from vulnerabilities.data_source import DataSource
34+
from vulnerabilities.data_source import DataSourceConfiguration
35+
from vulnerabilities.data_source import Reference
36+
from vulnerabilities.package_managers import GitHubTagsAPI
37+
38+
39+
@dataclasses.dataclass
40+
class NginxDataSourceConfiguration(DataSourceConfiguration):
41+
etag: dict
42+
43+
44+
class NginxDataSource(DataSource):
45+
CONFIG_CLASS = NginxDataSourceConfiguration
46+
47+
url = "http://nginx.org/en/security_advisories.html"
48+
49+
def set_api(self):
50+
self.version_api = GitHubTagsAPI()
51+
asyncio.run(self.version_api.load_api(["nginx/nginx"]))
52+
53+
# For some reason nginx tags it's releases in the form of `release-1.2.3`
54+
# Chop off the `release-` part here.
55+
for index, version in enumerate(self.version_api.cache["nginx/nginx"]):
56+
self.version_api.cache["nginx/nginx"][index] = version.replace("release-", "")
57+
58+
def updated_advisories(self):
59+
advisories = []
60+
if self.create_etag():
61+
self.set_api()
62+
data = requests.get(self.url).content
63+
advisories.extend(self.to_advisories(data))
64+
return self.batch_advisories(advisories)
65+
66+
def create_etag(self):
67+
etag = requests.head(self.url).headers.get("ETag")
68+
if not etag:
69+
return True
70+
71+
elif self.url in self.config.etag:
72+
if self.config.etag[self.url] == etag:
73+
return False
74+
75+
self.config.etag[self.url] = etag
76+
return True
77+
78+
def to_advisories(self, data):
79+
advisories = []
80+
soup = BeautifulSoup(data)
81+
vuln_list = soup.select("li p")
82+
83+
# Example value of `vuln_list` :
84+
# ['Excessive CPU usage in HTTP/2 with small window updates',
85+
# <br/>,
86+
# 'Severity: medium',
87+
# <br/>,
88+
# <a href="http://mailman.nginx.org/pipermail/nginx-announce/2019/000249.html">Advisory</a>, # nopep8
89+
# <br/>,
90+
# <a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9511">CVE-2019-9511</a>,
91+
# <br/>,
92+
# 'Not vulnerable: 1.17.3+, 1.16.1+',
93+
# <br/>,
94+
# 'Vulnerable: 1.9.5-1.17.2']
95+
96+
for vuln_info in vuln_list:
97+
references = []
98+
for index, child in enumerate(vuln_info.children):
99+
if index == 0:
100+
# type of this child is bs4.element.NavigableString.
101+
# Hence cast it into standard string
102+
summary = str(child)
103+
continue
104+
105+
# hasattr(child, "attrs") == False for bs4.element.NavigableString
106+
if hasattr(child, "attrs") and child.attrs.get("href"):
107+
link = child.attrs["href"]
108+
references.append(Reference(url=link))
109+
if "cve.mitre.org" in link:
110+
cve_id = child.text
111+
continue
112+
113+
if "Not vulnerable" in child:
114+
fixed_packages = self.extract_fixed_pkgs(child)
115+
continue
116+
117+
if "Vulnerable" in child:
118+
vulnerable_packages = self.extract_vuln_pkgs(child)
119+
continue
120+
121+
advisories.append(
122+
Advisory(
123+
cve_id=cve_id,
124+
summary=summary,
125+
impacted_package_urls=vulnerable_packages,
126+
resolved_package_urls=fixed_packages,
127+
)
128+
)
129+
130+
return advisories
131+
132+
def extract_fixed_pkgs(self, vuln_info):
133+
vuln_status, version_info = vuln_info.split(": ")
134+
if "none" in version_info:
135+
return {}
136+
137+
raw_ranges = version_info.split(",")
138+
version_ranges = []
139+
for rng in raw_ranges:
140+
# Eg. "1.7.3+" gets converted to RangeSpecifier("^1.7.3")
141+
# The advisory in this case uses `+` in the sense that any version
142+
# with greater or equal `minor` version satisfies the range.
143+
# "1.7.4" satisifes "1.7.3+", but "1.8.4" does not. "1.7.3+" has same
144+
# semantics as that of "^1.7.3"
145+
146+
version_ranges.append(RangeSpecifier("^" + rng[:-1]))
147+
148+
valid_versions = find_valid_versions(self.version_api.get("nginx/nginx"), version_ranges)
149+
150+
return {
151+
PackageURL(type="generic", name="nginx", version=version) for version in valid_versions
152+
}
153+
154+
def extract_vuln_pkgs(self, vuln_info):
155+
vuln_status, version_infos = vuln_info.split(": ")
156+
if "none" in version_infos:
157+
return {}
158+
159+
version_ranges = []
160+
windows_only = False
161+
for version_info in version_infos.split(", "):
162+
if "-" not in version_info:
163+
# These are discrete versions
164+
version_ranges.append(RangeSpecifier(version_info[0]))
165+
continue
166+
167+
windows_only = "nginx/Windows" in version_info
168+
version_info = version_info.replace("nginx/Windows", "")
169+
lower_bound, upper_bound = version_info.split("-")
170+
171+
version_ranges.append(RangeSpecifier(f">={lower_bound},<={upper_bound}"))
172+
173+
valid_versions = find_valid_versions(self.version_api.get("nginx/nginx"), version_ranges)
174+
qualifiers = {}
175+
if windows_only:
176+
qualifiers["os"] = "windows"
177+
178+
return {
179+
PackageURL(type="generic", name="nginx", version=version, qualifiers=qualifiers)
180+
for version in valid_versions
181+
}
182+
183+
184+
def find_valid_versions(versions, version_ranges):
185+
valid_versions = set()
186+
for version in versions:
187+
if any([version in ver_range for ver_range in version_ranges]):
188+
valid_versions.add(version)
189+
190+
return valid_versions

vulnerabilities/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@
2727
from django.db import models
2828
import django.contrib.postgres.fields as pgfields
2929
from django.utils.translation import ugettext_lazy as _
30-
31-
from packageurl.contrib.django_models import PackageURLMixin
30+
from packageurl.contrib.django.models import PackageURLMixin
3231
from packageurl import PackageURL
3332

3433
from vulnerabilities.data_source import DataSource

vulnerabilities/package_managers.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,8 @@
2525
from typing import Mapping
2626
from typing import Set
2727
from typing import List
28-
from urllib.error import HTTPError
29-
from urllib.request import urlopen
3028
import xml.etree.ElementTree as ET
3129

32-
import requests
3330
from aiohttp import ClientSession
3431
from aiohttp.client_exceptions import ClientResponseError
3532

@@ -303,3 +300,24 @@ def extract_versions(resp: dict, pkg_name: str) -> Set[str]:
303300
# See https://github.com/composer/composer/blob/44a4429978d1b3c6223277b875762b2930e83e8c/doc/articles/versions.md#tags # nopep8
304301
# for explanation of removing 'v'
305302
return all_versions
303+
304+
305+
class GitHubTagsAPI(VersionAPI):
306+
async def load_api(self, repo_set):
307+
async with ClientSession(raise_for_status=True) as session:
308+
await asyncio.gather(
309+
*[
310+
self.fetch(owner_repo.lower(), session)
311+
for owner_repo in repo_set
312+
if owner_repo.lower() not in self.cache
313+
]
314+
)
315+
316+
async def fetch(self, owner_repo: str, session) -> None:
317+
# owner_repo is a string of format "{repo_owner}/{repo_name}"
318+
# Example value of owner_repo = "nexB/scancode-toolkit"
319+
endpoint = f"https://api.github.com/repos/{owner_repo}/git/refs/tags"
320+
resp = await session.request(method="GET", url=endpoint)
321+
resp = await resp.json()
322+
print(resp)
323+
self.cache[owner_repo] = [release["ref"].split("/")[-1] for release in resp]

0 commit comments

Comments
 (0)