Skip to content

Commit ccf9963

Browse files
committed
Refactor VulnerabilityIssue class & functions to be more explicit & with docstrings
- Alter & add missing unit tests
1 parent 74d9f89 commit ccf9963

File tree

5 files changed

+239
-156
lines changed

5 files changed

+239
-156
lines changed

doc/github_actions/security_issues.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ Ideas
106106

107107
.. todo::
108108

109-
Add additional details to the :code:`security.Issue` type
109+
Add additional details to the :code:`VulnerabilityIssue` type
110110

111111

112112
.. todo::
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from collections.abc import Generator
5+
from dataclasses import (
6+
asdict,
7+
dataclass,
8+
)
9+
10+
import typer
11+
12+
13+
@dataclass(frozen=True)
14+
class VulnerabilityIssue:
15+
"""
16+
Dataclass for a vulnerability to be submitted to GitHub as an issue.
17+
18+
This format does not reflect the official CVE JSON schema:
19+
https://github.com/CVEProject/cve-schema/blob/master/schema/v5.0/CVE_JSON_5.0_schema.json
20+
Additionally, it is a known that some vulnerabilities may not initially or ever be
21+
assigned a CVE, which is meant to act as a unique identifier. In such cases, they
22+
instead have another kind of vulnerability ID associated with them.
23+
"""
24+
25+
cve: str
26+
cwe: str
27+
description: str
28+
coordinates: str
29+
references: tuple
30+
31+
@staticmethod
32+
def extract_from_jsonl(jsonl: typer.FileText) -> Generator[VulnerabilityIssue]:
33+
"""Converts the lines of a JSONL file into a generator of VulnerabilityIssue"""
34+
lines = (l for l in jsonl if l.strip() != "")
35+
for line in lines:
36+
obj = json.loads(line)
37+
obj["references"] = tuple(obj["references"])
38+
yield VulnerabilityIssue(**obj)
39+
40+
@property
41+
def json_str(self) -> str:
42+
"""Converts to a string-encoded JSON"""
43+
issue = asdict(self)
44+
return json.dumps(issue)
45+
46+
47+
@dataclass(frozen=True)
48+
class GitHubVulnerabilityIssue:
49+
"""Dataclass for an existing GitHub Issue associated with a vulnerability."""
50+
51+
cve: str
52+
cwe: str
53+
description: str
54+
coordinates: str
55+
references: tuple
56+
issue_url: str
57+
58+
@staticmethod
59+
def from_vulnerability_issue(
60+
issue: VulnerabilityIssue, issue_url: str
61+
) -> GitHubVulnerabilityIssue:
62+
"""Converts VulnerabilityIssue to GitHubVulnerabilityIssue"""
63+
return GitHubVulnerabilityIssue(**asdict(issue), issue_url=issue_url.strip())
64+
65+
@staticmethod
66+
def extract_from_jsonl(
67+
jsonl: typer.FileText,
68+
) -> Generator[GitHubVulnerabilityIssue]:
69+
"""Converts the lines of a JSONL file into a generator of GitHubVulnerabilityIssue"""
70+
lines = (l for l in jsonl if l.strip() != "")
71+
for line in lines:
72+
obj = json.loads(line)
73+
obj["references"] = tuple(obj["references"])
74+
yield GitHubVulnerabilityIssue(**obj)
75+
76+
@property
77+
def json_str(self) -> str:
78+
"""Converts to a string-encoded JSON"""
79+
issue_json = asdict(self)
80+
return json.dumps(issue_json)

exasol/toolbox/tools/security.py

Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,45 +10,23 @@
1010
Generator,
1111
Iterable,
1212
)
13-
from dataclasses import (
14-
asdict,
15-
dataclass,
16-
)
13+
from dataclasses import dataclass
1714
from enum import Enum
1815
from functools import partial
1916
from inspect import cleandoc
2017
from pathlib import Path
2118

2219
import typer
2320

21+
from exasol.toolbox.security import (
22+
GitHubVulnerabilityIssue,
23+
VulnerabilityIssue,
24+
)
25+
2426
stdout = print
2527
stderr = partial(print, file=sys.stderr)
2628

2729

28-
# Note: In the long term we may want to adapt the official CVE json schema
29-
# https://github.com/CVEProject/cve-schema/blob/master/schema/v5.0/CVE_JSON_5.0_schema.json
30-
@dataclass(frozen=True)
31-
class Issue:
32-
cve: str
33-
cwe: str
34-
description: str
35-
coordinates: str
36-
references: tuple
37-
38-
39-
def _issues(input) -> Generator[Issue]:
40-
lines = (l for l in input if l.strip() != "")
41-
for line in lines:
42-
obj = json.loads(line)
43-
yield Issue(**obj)
44-
45-
46-
def _issues_as_json_str(issues):
47-
for issue in issues:
48-
issue = asdict(issue)
49-
yield json.dumps(issue)
50-
51-
5230
def gh_security_issues() -> Generator[tuple[str, str]]:
5331
"""
5432
Yields issue-id, cve-id pairs for all (closed, open) issues associated with CVEs
@@ -87,14 +65,14 @@ def gh_security_issues() -> Generator[tuple[str, str]]:
8765
return issues
8866

8967

90-
def from_maven(report: str) -> Iterable[Issue]:
68+
def from_maven(report: str) -> Iterable[VulnerabilityIssue]:
9169
# Note: Consider adding warnings if there is the same cve with multiple coordinates
9270
report = json.loads(report)
9371
dependencies = report.get("vulnerable", {}) # type: ignore
9472
for dependency_name, dependency in dependencies.items(): # type: ignore
9573
for v in dependency["vulnerabilities"]: # type: ignore
9674
references = [v["reference"]] + v["externalReferences"]
97-
yield Issue(
75+
yield VulnerabilityIssue(
9876
cve=v["cve"],
9977
cwe=v["cwe"],
10078
description=v["description"],
@@ -145,10 +123,10 @@ def identify_pypi_references(
145123
)
146124

147125

148-
def from_pip_audit(report: str) -> Iterable[Issue]:
126+
def from_pip_audit(report: str) -> Iterable[VulnerabilityIssue]:
149127
"""
150128
Transforms the JSON output from `nox -s dependency:audit` into an iterable of
151-
`security.Issue` objects.
129+
`VulnerabilityIssue` objects.
152130
153131
This does not gracefully handle scenarios where:
154132
- a CVE is not initially associated with the vulnerability; however, the assumption
@@ -175,7 +153,7 @@ def from_pip_audit(report: str) -> Iterable[Issue]:
175153
references=refs, package_name=package
176154
)
177155
if cves:
178-
yield Issue(
156+
yield VulnerabilityIssue(
179157
cve=sorted(cves)[0],
180158
cwe="None" if not cwes else ", ".join(cwes),
181159
description=v["description"],
@@ -241,11 +219,11 @@ def _row(issue):
241219
return template.format(header=_header(), rows="\n".join(_row(i) for i in issues))
242220

243221

244-
def security_issue_title(issue: Issue) -> str:
222+
def security_issue_title(issue: VulnerabilityIssue) -> str:
245223
return f"🔐 {issue.cve}: {issue.coordinates}"
246224

247225

248-
def security_issue_body(issue: Issue) -> str:
226+
def security_issue_body(issue: VulnerabilityIssue) -> str:
249227
def as_markdown_listing(elements: Iterable[str]):
250228
return "\n".join(f"- {element}" for element in elements)
251229

@@ -269,7 +247,9 @@ def as_markdown_listing(elements: Iterable[str]):
269247
)
270248

271249

272-
def create_security_issue(issue: Issue, project: str | None = None) -> tuple[str, str]:
250+
def create_security_issue(
251+
issue: VulnerabilityIssue, project: str | None = None
252+
) -> tuple[str, str]:
273253
# fmt: off
274254
command = [
275255
"gh", "issue", "create",
@@ -322,14 +302,14 @@ def convert(
322302

323303
def _maven(infile):
324304
issues = from_maven(infile.read())
325-
for issue in _issues_as_json_str(issues):
326-
stdout(issue)
305+
for issue in issues:
306+
stdout(issue.json_str)
327307
raise typer.Exit(code=0)
328308

329309
def _pip_audit(infile):
330310
issues = from_pip_audit(infile.read())
331-
for issue in _issues_as_json_str(issues):
332-
stdout(issue)
311+
for issue in issues:
312+
stdout(issue.json_str)
333313
raise typer.Exit(code=0)
334314

335315
actions = {Format.Maven: _maven, Format.PipAudit: _pip_audit}
@@ -364,10 +344,12 @@ def _github(infile):
364344
for issue in to_be_filtered:
365345
stderr(f"{issue}")
366346
filtered_issues = [
367-
issue for issue in _issues(infile) if issue.cve not in to_be_filtered
347+
issue
348+
for issue in VulnerabilityIssue.extract_from_jsonl(infile)
349+
if issue.cve not in to_be_filtered
368350
]
369-
for issue in _issues_as_json_str(filtered_issues):
370-
stdout(issue)
351+
for issue in filtered_issues:
352+
stdout(issue.json_str)
371353
raise typer.Exit(code=0)
372354

373355
def _pass_through(infile):
@@ -398,10 +380,13 @@ def create(
398380
Output:
399381
{ "cve": "<cve-id>", "cwe": "<cwe-id>", "description": "<multiline string>", "coordinates": "<string>", "references": ["<url>", "<url>", ...], "issue_url": "<url>" }
400382
"""
401-
for issue in _issues(input_file):
383+
for issue in VulnerabilityIssue.extract_from_jsonl(input_file):
402384
std_err, issue_url = create_security_issue(issue, project)
403385
stderr(std_err)
404-
stdout(format_jsonl(issue_url, issue))
386+
ghvi = GitHubVulnerabilityIssue.from_vulnerability_issue(
387+
issue=issue, issue_url=issue_url
388+
)
389+
stdout(ghvi.json_str)
405390

406391

407392
class PPrintFormats(str, Enum):
@@ -421,11 +406,5 @@ def json_issue_to_markdown(
421406
print(issues_to_markdown(issues))
422407

423408

424-
def format_jsonl(issue_url: str, issue: Issue) -> str:
425-
issue_json = asdict(issue)
426-
issue_json["issue_url"] = issue_url.strip()
427-
return json.dumps(issue_json)
428-
429-
430409
if __name__ == "__main__":
431410
CLI()

test/unit/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
@pytest.fixture(scope="session")
99
def pip_audit_jinja2_issue():
10-
return security.Issue(
10+
return security.VulnerabilityIssue(
1111
cve="CVE-2025-27516",
1212
cwe="None",
1313
description=cleandoc(
@@ -33,7 +33,7 @@ def pip_audit_jinja2_issue():
3333

3434
@pytest.fixture(scope="session")
3535
def pip_audit_cryptography_issue():
36-
return security.Issue(
36+
return security.VulnerabilityIssue(
3737
cve="CVE-2024-12797",
3838
cwe="None",
3939
description=cleandoc(

0 commit comments

Comments
 (0)