Skip to content

Commit 5d1952e

Browse files
committed
add alpine improver
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 5218413 commit 5d1952e

8 files changed

Lines changed: 152 additions & 67 deletions

File tree

vulnerabilities/importer.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ def merge(cls, affected_packages: Iterable):
159159
fixed_versions = set()
160160
purls = set()
161161
for pkg in affected_packages:
162-
affected_version_ranges.add(pkg.affected_version_range)
162+
if pkg.affected_version_range:
163+
affected_version_ranges.add(pkg.affected_version_range)
163164
if pkg.fixed_version:
164165
fixed_versions.add(pkg.fixed_version)
165166
purls.add(pkg.package)
@@ -171,9 +172,12 @@ def to_dict(self):
171172
"""
172173
Return a serializable dict that can be converted back using self.from_dict
173174
"""
175+
affected_version_range = None
176+
if self.affected_version_range:
177+
affected_version_range = str(self.affected_version_range)
174178
return {
175179
"package": self.package.to_dict(),
176-
"affected_version_range": str(self.affected_version_range),
180+
"affected_version_range": affected_version_range,
177181
"fixed_version": str(self.fixed_version) if self.fixed_version else None,
178182
}
179183

@@ -183,9 +187,11 @@ def from_dict(cls, affected_pkg: dict):
183187
Return an AffectedPackage object from dict generated by self.to_dict
184188
"""
185189
package = PackageURL(**affected_pkg["package"])
186-
affected_version_range = VersionRange.from_string(affected_pkg["affected_version_range"])
190+
affected_version_range = None
191+
if affected_pkg["affected_version_range"]:
192+
affected_version_range = VersionRange.from_string(affected_pkg["affected_version_range"])
187193
fixed_version = affected_pkg["fixed_version"]
188-
if fixed_version:
194+
if fixed_version and affected_version_range:
189195
# TODO: revisit after https://github.com/nexB/univers/issues/10
190196
fixed_version = affected_version_range.version_class(fixed_version)
191197

@@ -224,6 +230,10 @@ class NoLicenseError(Exception):
224230
pass
225231

226232

233+
class InvalidSPDXLicense(Exception):
234+
pass
235+
236+
227237
class Importer:
228238
"""
229239
An Importer collects data from various upstreams and returns corresponding AdvisoryData objects
@@ -239,8 +249,8 @@ def __init__(self):
239249
licensing = Licensing()
240250
try:
241251
licensing.parse(self.spdx_license_expression)
242-
except Exception as e:
243-
raise Exception(
252+
except InvalidSPDXLicense as e:
253+
raise ValueError(
244254
f"{self.spdx_license_expression!r} is not a valid SPDX license expression"
245255
) from e
246256

vulnerabilities/importers/alpine_linux.py

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,22 @@
2525
from typing import Iterable
2626
from typing import List
2727
from typing import Mapping
28+
from urllib.parse import urljoin
2829

2930
import requests
3031
from bs4 import BeautifulSoup
32+
from django.db.models.query import QuerySet
3133
from packageurl import PackageURL
3234
from univers.versions import AlpineLinuxVersion
3335

3436
from vulnerabilities.helpers import is_cve
3537
from vulnerabilities.importer import AdvisoryData
3638
from vulnerabilities.importer import AffectedPackage
3739
from vulnerabilities.importer import Importer
40+
from vulnerabilities.improver import MAX_CONFIDENCE
41+
from vulnerabilities.improver import Improver
42+
from vulnerabilities.improver import Inference
43+
from vulnerabilities.models import Advisory
3844
from vulnerabilities.references import WireSharkReference
3945
from vulnerabilities.references import XsaReference
4046
from vulnerabilities.references import ZbxReference
@@ -60,7 +66,8 @@ def advisory_data(self) -> Iterable[AdvisoryData]:
6066
for link in advisory_links:
6167
record = fetch_response(link).json()
6268
if not record["packages"]:
63-
raise Exception(f'"packages" not found in {link}')
69+
LOGGER.error(f'"packages" not found in {link!r}')
70+
continue
6471
advisories.extend(process_record(record))
6572
return advisories
6673

@@ -72,50 +79,67 @@ def fetch_response(url):
7279
response = requests.get(url)
7380
if response.status_code == 200:
7481
return response
75-
raise Exception("Failed to fetch data from the URL")
82+
raise Exception(f"Failed to fetch data from {url!r}")
7683

7784

78-
def fetch_advisory_directory_links(page_response_content):
85+
def fetch_advisory_directory_links(page_response_content: str) -> List[str]:
7986
"""
80-
Return `advisory_directory_links` present in `index_page`
87+
Return a list of advisory directory links present in `page_response_content` html string
8188
"""
8289
index_page = BeautifulSoup(page_response_content, features="lxml")
8390
alpine_versions = [link.text for link in index_page.find_all("a") if link.text.startswith("v")]
8491

85-
assert alpine_versions, f"No versions found in {BASE_URL}"
92+
if not alpine_versions:
93+
LOGGER.error(f"No versions found in {BASE_URL!r}")
94+
return []
8695

87-
advisory_directory_links = [f"{BASE_URL}{version}" for version in alpine_versions]
96+
advisory_directory_links = [urljoin(BASE_URL, version) for version in alpine_versions]
8897

8998
return advisory_directory_links
9099

91100

92-
def fetch_advisory_links(advisory_directory_page, advisory_directory_link):
101+
def fetch_advisory_links(
102+
advisory_directory_page: str, advisory_directory_link: str
103+
) -> Iterable[str]:
93104
"""
94105
Yield json file urls present in `advisory_directory_page`
95106
"""
96107
advisory_directory_page = BeautifulSoup(advisory_directory_page, features="lxml")
97108
anchor_tags = advisory_directory_page.find_all("a")
98-
assert anchor_tags, f"No anchor tags found in {advisory_directory_link}"
109+
if not anchor_tags:
110+
LOGGER.error(f"No anchor tags found in {advisory_directory_link!r}")
111+
return iter([])
99112
for anchor_tag in anchor_tags:
100113
if anchor_tag.text.endswith("json"):
101114
yield f"{advisory_directory_link}{anchor_tag.text}"
102115

103116

117+
def check_for_attributes(record) -> bool:
118+
attributes = ["distroversion", "reponame", "archs"]
119+
for attribute in attributes:
120+
if attribute not in record:
121+
LOGGER.error(f'"{attribute!r}" not found in {record!r}')
122+
return False
123+
return True
124+
125+
104126
def process_record(record: dict) -> List[AdvisoryData]:
105127
"""
106128
Return a list of AdvisoryData objects by processing data
107129
present in that `record`
108130
"""
109131
if not record["packages"]:
110-
raise Exception(f'"packages" not found in this {record!r}')
132+
LOGGER.error(f'"packages" not found in this record {record!r}')
133+
return []
111134

112135
advisories: List[AdvisoryData] = []
113136

114137
for package in record["packages"]:
115-
assert package["pkg"]
116-
attributes = ["distroversion", "reponame", "archs"]
117-
for attribute in attributes:
118-
assert attribute in record
138+
if not package["pkg"]:
139+
LOGGER.error(f'"pkg" not found in this package {package!r}')
140+
continue
141+
if not check_for_attributes(record):
142+
continue
119143
loaded_advisories = load_advisories(
120144
package["pkg"],
121145
record["distroversion"],
@@ -133,18 +157,23 @@ def load_advisories(
133157
archs: List[str],
134158
) -> Iterable[AdvisoryData]:
135159
"""
136-
Yields AdvisoryData by mapping data from `pkg_infos`
160+
Yield AdvisoryData by mapping data from `pkg_infos`
137161
and form PURL for AffectedPackages by using
138162
`distroversion`, `reponame`, `archs`
139163
"""
140-
assert pkg_infos.get("name"), '"name" is not available in package'
164+
if not pkg_infos.get("name"):
165+
LOGGER.error(f'"name" is not available in package {pkg_infos!r}')
166+
return []
141167

142168
for version, fixed_vulns in pkg_infos["secfixes"].items():
143-
144-
assert fixed_vulns, f"No fixed vulnerabilities in {version}"
169+
if not fixed_vulns:
170+
LOGGER.error(f"No fixed vulnerabilities in version {version!r}")
171+
continue
145172

146173
for vuln_ids in fixed_vulns:
147-
assert isinstance(vuln_ids, str)
174+
if not isinstance(vuln_ids, str):
175+
LOGGER.error(f"{vuln_ids!r} is not of `str` instance")
176+
continue
148177
vuln_ids = vuln_ids.split()
149178
aliases = []
150179
vuln_id = vuln_ids[0]
@@ -176,8 +205,11 @@ def load_advisories(
176205
try:
177206
fixed_version = AlpineLinuxVersion(version)
178207
except Exception as e:
179-
raise Exception(f"{version!r} Not a valid Alpine Version") from e
180-
assert isinstance(archs, List)
208+
LOGGER.error(f"{version!r} is not a valid AlpineVersion {e!r}")
209+
continue
210+
if not isinstance(archs, List):
211+
LOGGER.error(f"{archs!r} is not of `List` instance")
212+
continue
181213
if archs:
182214
for arch in archs:
183215
qualifiers["arch"] = arch
@@ -209,3 +241,21 @@ def load_advisories(
209241
affected_packages=affected_packages,
210242
aliases=aliases,
211243
)
244+
245+
246+
class AlpineBasicImprover(Improver):
247+
@property
248+
def interesting_advisories(self) -> QuerySet:
249+
return Advisory.objects.filter(created_by=AlpineImporter.qualified_name)
250+
251+
def get_inferences(self, advisory_data: AdvisoryData) -> Iterable[Inference]:
252+
"""
253+
Generate and return Inferences for the given advisory data
254+
"""
255+
for affected_package in advisory_data.affected_packages:
256+
fixed_purl = affected_package.get_fixed_purl()
257+
yield Inference.from_advisory_data(
258+
advisory_data,
259+
confidence=MAX_CONFIDENCE,
260+
fixed_purl=fixed_purl,
261+
)

vulnerabilities/improve_runner.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,16 @@ def process_inferences(inferences: List[Inference], advisory: Advisory, improver
7575
)
7676
if updated:
7777
logger.info("Severity updated for reference {ref!r} to {severity.value!r}")
78-
79-
for pkg in inference.affected_purls:
80-
vulnerable_package, _ = _get_or_create_package(pkg)
81-
models.PackageRelatedVulnerability(
82-
vulnerability=vuln,
83-
package=vulnerable_package,
84-
created_by=improver_name,
85-
confidence=inference.confidence,
86-
fix=False,
87-
).update_or_create()
78+
if inference.affected_purls:
79+
for pkg in inference.affected_purls:
80+
vulnerable_package, _ = _get_or_create_package(pkg)
81+
models.PackageRelatedVulnerability(
82+
vulnerability=vuln,
83+
package=vulnerable_package,
84+
created_by=improver_name,
85+
confidence=inference.confidence,
86+
fix=False,
87+
).update_or_create()
8888

8989
if inference.fixed_purl:
9090
fixed_package, _ = _get_or_create_package(inference.fixed_purl)

vulnerabilities/improver.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ class Inference:
2727
"""
2828

2929
vulnerability_id: str = None
30-
aliases: List[str] = dataclasses.field(default_factory=list)
30+
aliases: Optional[List[str]] = None
3131
confidence: int = MAX_CONFIDENCE
3232
summary: Optional[str] = None
33-
affected_purls: List[PackageURL] = dataclasses.field(default_factory=list)
33+
affected_purls: Optional[List[PackageURL]] = None
3434
fixed_purl: PackageURL = None
3535
references: List[Reference] = dataclasses.field(default_factory=list)
3636

@@ -48,7 +48,12 @@ def __post_init__(self):
4848
)
4949

5050
versionless_purls = []
51-
for purl in self.affected_purls + [self.fixed_purl]:
51+
purls = []
52+
if self.fixed_purl:
53+
purls = [self.fixed_purl]
54+
if self.affected_purls:
55+
purls = purls + self.affected_purls
56+
for purl in purls:
5257
if purl and not purl.version:
5358
versionless_purls.append(purl)
5459

@@ -57,7 +62,7 @@ def __post_init__(self):
5762
), f"Version-less purls are not supported in an Inference: {versionless_purls}"
5863

5964
@classmethod
60-
def from_advisory_data(cls, advisory_data, confidence, affected_purls, fixed_purl):
65+
def from_advisory_data(cls, advisory_data, confidence, fixed_purl, affected_purls=None):
6166
"""
6267
Return an Inference object while keeping the same values as of advisory_data
6368
for vulnerability_id, summary and references
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from vulnerabilities import importers
22
from vulnerabilities.improvers import default
33

4-
IMPROVERS_REGISTRY = [default.DefaultImprover, importers.nginx.NginxBasicImprover]
4+
IMPROVERS_REGISTRY = [
5+
default.DefaultImprover,
6+
importers.nginx.NginxBasicImprover,
7+
importers.alpine_linux.AlpineBasicImprover,
8+
]
59

610
IMPROVERS_REGISTRY = {x.qualified_name: x for x in IMPROVERS_REGISTRY}

vulnerabilities/references.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ def from_id(cls, xsa_id):
1212
Return a new XsaReference from an XSA-XXXX id.
1313
"""
1414
if not xsa_id or not xsa_id.lower().startswith("xsa"):
15-
return ValueError(f"Not a Xen advisory. Does not start with XSA: {xsa_id!r}")
16-
numid = xsa_id.split("-")[-1]
15+
return ValueError(f"Not a Xen reference. Does not start with XSA: {xsa_id!r}")
16+
_, numid = xsa_id.rsplit("-")
1717
return cls(
1818
reference_id=xsa_id,
19-
url="https://xenbits.xen.org/xsa/advisory-{}.html".format(numid),
19+
url=f"https://xenbits.xen.org/xsa/advisory-{numid}.html",
2020
)
2121

2222

@@ -31,10 +31,10 @@ def from_id(cls, zbx_id):
3131
Return a new ZbxReference from an ZBX-XXXX id.
3232
"""
3333
if not zbx_id or not zbx_id.lower().startswith("zbx"):
34-
return ValueError(f"Not a Zabbix advisory. Does not start with ZBX: {zbx_id!r}")
34+
return ValueError(f"Not a Zabbix reference. Does not start with ZBX: {zbx_id!r}")
3535
return cls(
3636
reference_id=zbx_id,
37-
url="https://support.zabbix.com/browse/{}".format(zbx_id),
37+
url=f"https://support.zabbix.com/browse/{zbx_id}",
3838
)
3939

4040

@@ -50,9 +50,9 @@ def from_id(cls, wnpa_sec_id):
5050
"""
5151
if not wnpa_sec_id or not wnpa_sec_id.lower().startswith("wnpa-sec"):
5252
return ValueError(
53-
f"Not a WireShark advisory. Does not start with wnpa-sec: {wnpa_sec_id!r}"
53+
f"Not a WireShark reference. Does not start with wnpa-sec: {wnpa_sec_id!r}"
5454
)
5555
return cls(
5656
reference_id=wnpa_sec_id,
57-
url="https://www.wireshark.org/security/{}.html".format(wnpa_sec_id),
57+
url=f"https://www.wireshark.org/security/{wnpa_sec_id}.html",
5858
)

0 commit comments

Comments
 (0)