Skip to content

Commit 6563693

Browse files
committed
migrate alpine importer to importer improver model
Current alpine importer models need to be refactored to give AdvisoryData instead of Advisory, also add some validation to parse license expression and make affected_version_range optional Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 4390b6a commit 6563693

5 files changed

Lines changed: 127 additions & 94 deletions

File tree

vulnerabilities/importer.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from packageurl import PackageURL
4242
from univers.version_range import VersionRange
4343
from univers.versions import Version
44+
from license_expression import Licensing
4445

4546
from vulnerabilities.helpers import classproperty
4647
from vulnerabilities.helpers import nearest_patched_package
@@ -117,22 +118,24 @@ class AffectedPackage:
117118
"""
118119
Contains a range of affected versions and a fixed version of a given package
119120
The PackageURL supplied must *not* have a version
121+
It must contain either `affected_version_range` or `fixed_version`
120122
"""
121123

122124
package: PackageURL
123-
affected_version_range: VersionRange
125+
affected_version_range: Optional[VersionRange]
124126
fixed_version: Optional[Version] = None
125127

126128
def __post_init__(self):
127-
if self.package.version:
129+
if self.package.version or not (self.affected_version_range or self.fixed_version):
128130
raise ValueError
129131

130132
def get_fixed_purl(self):
131133
"""
132134
Return PackageURL corresponding to object's fixed_version
133135
"""
134-
fixed_version = self.fixed_version
135-
fixed_purl = self.package._replace(version=str(fixed_version))
136+
if not self.fixed_version:
137+
raise ValueError
138+
fixed_purl = self.package._replace(version=str(self.fixed_version))
136139
return fixed_purl
137140

138141
@classmethod
@@ -228,6 +231,13 @@ class Importer:
228231
def __init__(self):
229232
if not self.spdx_license_expression:
230233
raise Exception(f"Cannot run importer {self!r} without a license")
234+
licensing = Licensing()
235+
try:
236+
licensing.parse(self.spdx_license_expression)
237+
except:
238+
raise Exception(
239+
f"{self.spdx_license_expression} is not a valid spdx license expression"
240+
)
231241

232242
@classproperty
233243
def qualified_name(cls):

vulnerabilities/importers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
# for any legal advice.
2020
# VulnerableCode is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
22+
from vulnerabilities.importers import alpine_linux
2223
from vulnerabilities.importers import nginx
2324

24-
IMPORTERS_REGISTRY = [nginx.NginxImporter]
25+
IMPORTERS_REGISTRY = [nginx.NginxImporter, alpine_linux.AlpineImporter]
2526

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

vulnerabilities/importers/alpine_linux.py

Lines changed: 108 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,124 +20,146 @@
2020
# for any legal advice.
2121
# VulnerableCode is a free software code scanning tool from nexB Inc. and others.
2222
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
23+
import json
2324
from typing import Any
25+
from typing import Iterable
2426
from typing import List
2527
from typing import Mapping
26-
from typing import Set
2728

2829
import requests
29-
import saneyaml
3030
from bs4 import BeautifulSoup
31+
from packageurl import PackageURL
32+
from univers.versions import AlpineLinuxVersion
3133

3234
from vulnerabilities.helpers import is_cve
33-
from vulnerabilities.importer import Advisory
35+
from vulnerabilities.importer import AdvisoryData
36+
from vulnerabilities.importer import AffectedPackage
3437
from vulnerabilities.importer import Importer
3538
from vulnerabilities.importer import Reference
3639

37-
BASE_URL = "https://secdb.alpinelinux.org/"
38-
3940

4041
class AlpineImporter(Importer):
41-
@staticmethod
42-
def fetch_advisory_links():
43-
index_page = BeautifulSoup(requests.get(BASE_URL).content, features="lxml")
44-
45-
alpine_versions = [
46-
link.text for link in index_page.find_all("a") if link.text.startswith("v")
47-
]
48-
49-
advisory_directory_links = [f"{BASE_URL}{version}" for version in alpine_versions]
50-
51-
advisory_links = []
52-
for advisory_directory_link in advisory_directory_links:
53-
advisory_directory_page = requests.get(advisory_directory_link).content
54-
advisory_directory_page = BeautifulSoup(advisory_directory_page, features="lxml")
55-
advisory_links.extend(
56-
[
57-
f"{advisory_directory_link}{anchore_tag.text}"
58-
for anchore_tag in advisory_directory_page.find_all("a")
59-
if anchore_tag.text.endswith("yaml")
60-
]
61-
)
62-
63-
return advisory_links
42+
spdx_license_expression = "CC-BY-SA-4.0"
6443

65-
def updated_advisories(self) -> Set[Advisory]:
44+
def advisory_data(self) -> Iterable[AdvisoryData]:
6645
advisories = []
67-
advisory_links = self.fetch_advisory_links()
46+
advisory_links = fetch_advisory_links()
6847
for link in advisory_links:
69-
advisories.extend(self._process_link(link))
48+
advisories.extend(process_link(link))
7049

71-
return self.batch_advisories(advisories)
7250

73-
def _process_link(self, link) -> List[Advisory]:
74-
advisories = []
75-
yaml_response = requests.get(link).content
76-
record = saneyaml.load(yaml_response)
51+
def fetch_advisory_links():
52+
BASE_URL = "https://secdb.alpinelinux.org/"
53+
index_page = BeautifulSoup(requests.get(BASE_URL).content, features="lxml")
7754

78-
if record["packages"] is None:
79-
return advisories
55+
alpine_versions = [link.text for link in index_page.find_all("a") if link.text.startswith("v")]
8056

81-
for p in record["packages"]:
82-
advisories.extend(
83-
self._load_advisories(
84-
p["pkg"],
85-
)
86-
)
57+
advisory_directory_links = [f"{BASE_URL}{version}" for version in alpine_versions]
8758

59+
advisory_links = []
60+
for advisory_directory_link in advisory_directory_links:
61+
advisory_directory_page = requests.get(advisory_directory_link).content
62+
advisory_directory_page = BeautifulSoup(advisory_directory_page, features="lxml")
63+
advisory_links.extend(
64+
[
65+
f"{advisory_directory_link}{anchore_tag.text}"
66+
for anchore_tag in advisory_directory_page.find_all("a")
67+
if anchore_tag.text.endswith("json")
68+
]
69+
)
70+
71+
return advisory_links
72+
73+
74+
def process_link(link) -> List[AdvisoryData]:
75+
json_response = requests.get(link).content
76+
record = json.loads(json_response)
77+
advisories: List[AdvisoryData] = []
78+
79+
if record["packages"] is None:
8880
return advisories
8981

90-
@staticmethod
91-
def _load_advisories(
92-
pkg_infos: Mapping[str, Any],
93-
) -> List[Advisory]:
82+
for package in record["packages"]:
83+
assert isinstance(record["archs"], Iterable[str])
84+
assert isinstance(record["reponame"], str)
85+
assert isinstance(record["distroversion"], str)
86+
advisories.extend(
87+
load_advisories(
88+
package["pkg"],
89+
record["distroversion"],
90+
record["reponame"],
91+
record["archs"],
92+
)
93+
)
94+
return advisories
95+
9496

95-
advisories = []
97+
def load_advisories(
98+
pkg_infos: Mapping[str, Any],
99+
distroversion: str,
100+
reponame: str,
101+
archs: Iterable[str],
102+
) -> Iterable[AdvisoryData]:
96103

97-
for fixed_vulns in pkg_infos["secfixes"].values():
104+
if not (pkg_infos.get("name") and isinstance(pkg_infos["name"], str)):
105+
return []
98106

99-
if fixed_vulns is None:
100-
continue
107+
assert isinstance(pkg_infos["secfixes"], Mapping[str, List])
101108

102-
for vuln_ids in fixed_vulns:
103-
vuln_ids = vuln_ids.split()
104-
references = []
105-
for reference_id in vuln_ids[1:]:
109+
for version, fixed_vulns in pkg_infos["secfixes"].items():
106110

107-
if reference_id.startswith("XSA"):
108-
xsa_id = reference_id.split("-")[-1]
109-
references.append(
110-
Reference(
111-
reference_id=reference_id,
112-
url="https://xenbits.xen.org/xsa/advisory-{}.html".format(xsa_id),
113-
)
114-
)
111+
if fixed_vulns is None:
112+
continue
115113

116-
elif reference_id.startswith("ZBX"):
117-
references.append(
118-
Reference(
119-
reference_id=reference_id,
120-
url="https://support.zabbix.com/browse/{}".format(reference_id),
121-
)
114+
for vuln_ids in fixed_vulns:
115+
assert isinstance(vuln_ids, str)
116+
vuln_ids = vuln_ids.split()
117+
references = []
118+
for reference_id in vuln_ids[1:]:
119+
120+
if reference_id.startswith("XSA"):
121+
xsa_id = reference_id.split("-")[-1]
122+
references.append(
123+
Reference(
124+
reference_id=reference_id,
125+
url="https://xenbits.xen.org/xsa/advisory-{}.html".format(xsa_id),
122126
)
127+
)
123128

124-
elif reference_id.startswith("wnpa-sec"):
125-
references.append(
126-
Reference(
127-
reference_id=reference_id,
128-
url="https://www.wireshark.org/security/{}.html".format(
129-
reference_id
130-
),
131-
)
129+
elif reference_id.startswith("ZBX"):
130+
references.append(
131+
Reference(
132+
reference_id=reference_id,
133+
url="https://support.zabbix.com/browse/{}".format(reference_id),
132134
)
135+
)
133136

134-
# TODO: Handle the CVE-????-????? case
135-
advisories.append(
136-
Advisory(
137-
summary="",
138-
references=references,
139-
vulnerability_id=vuln_ids[0] if is_cve(vuln_ids[0]) else "",
137+
elif reference_id.startswith("wnpa-sec"):
138+
references.append(
139+
Reference(
140+
reference_id=reference_id,
141+
url="https://www.wireshark.org/security/{}.html".format(reference_id),
142+
)
140143
)
141-
)
142144

143-
return advisories
145+
# TODO: Handle the CVE-????-????? case
146+
yield AdvisoryData(
147+
summary="",
148+
references=references,
149+
affected_packages=[
150+
AffectedPackage(
151+
package=PackageURL(
152+
type="alpine",
153+
name=pkg_infos["name"],
154+
qualifiers={
155+
"arch": arch,
156+
"distroversion": distroversion,
157+
"reponame": reponame,
158+
},
159+
),
160+
fixed_version=AlpineLinuxVersion(version),
161+
)
162+
for arch in archs
163+
],
164+
aliases=[vuln_ids[0] if is_cve(vuln_ids[0]) else ""],
165+
)

vulnerabilities/importers/nginx.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
# for any legal advice.
2020
# VulnerableCode is a free software tool from nexB Inc. and others.
2121
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
22-
2322
import asyncio
2423
import dataclasses
2524
import datetime

vulnerabilities/improvers/default.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from itertools import chain
22
from typing import Iterable
33
from typing import List
4+
from typing import Tuple
45

56
from django.db.models.query import QuerySet
67
from packageurl import PackageURL
@@ -38,7 +39,7 @@ def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
3839
)
3940

4041

41-
def get_exact_purls(affected_package: AffectedPackage) -> (List[PackageURL], PackageURL):
42+
def get_exact_purls(affected_package: AffectedPackage) -> Tuple[List[PackageURL], PackageURL]:
4243
"""
4344
Return a list of affected purls and the fixed package found in the ``affected_package``
4445
AffectedPackage disregarding any ranges.
@@ -67,7 +68,7 @@ def get_exact_purls(affected_package: AffectedPackage) -> (List[PackageURL], Pac
6768
range_versions = [c.version for c in vr.constraints if c]
6869
resolved_versions = [v for v in range_versions if v and v in vr]
6970

70-
affected_purls = []
71+
affected_purls: List[PackageURL] = []
7172
for version in resolved_versions:
7273
affected_purl = affected_package.package._replace(version=str(version))
7374
affected_purls.append(affected_purl)

0 commit comments

Comments
 (0)