Skip to content

Commit ff9de46

Browse files
authored
Add JSON output to script and change issue template (#70)
This change adds a new `--json-output` flag to the script, which outputs the vulnerabilities found as a JSON string. It also adapts the GH Action to use it, instead of manually parsing the original output. It also changes the template for the created issues, which now contains the dependency name in its title, and a description that only mentions the relevant vulnerability (instead of the full output of the script).
1 parent f2a0a54 commit ff9de46

File tree

3 files changed

+51
-47
lines changed

3 files changed

+51
-47
lines changed

.github/ISSUE_TEMPLATE.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
---
2-
title: New vulnerability {{ env.VULN_ID }} found on {{ env.NODEJS_STREAM }}
2+
title: "{{ env.VULN_ID }} ({{ env.VULN_DEP_NAME }}) found on {{ env.NODEJS_STREAM }}"
33
asignees:
44
labels: "{{ env.NODEJS_STREAM }}"
55
---
6-
Failed run: {{ env.ACTION_URL }}
7-
Vulnerability ID: {{ env.VULN_ID }}
8-
9-
Full output:
10-
--------------------
11-
```
12-
{{ env.ERROR_MSG }}
13-
```
146

7+
A new vulnerability for {{ env.VULN_DEP_NAME }} {{ env.VULN_DEP_VERSION }} was found:
8+
Vulnerability ID: {{ env.VULN_ID }}
9+
Vulnerability URL: {{ env.VULN_URL }}
10+
Failed run: {{ env.ACTION_URL }}

.github/workflows/check-vulns.yml

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ jobs:
2525
runs-on: ubuntu-latest
2626
outputs:
2727
matrix: ${{ steps.set_matrix.outputs.matrix }}
28-
full_output: ${{ steps.collect_error.outputs.result }}
2928
steps:
3029
- name: Setup Python 3.9
3130
uses: actions/setup-python@v4
@@ -47,30 +46,16 @@ jobs:
4746
run: |
4847
(
4948
set -o pipefail
50-
python main.py --gh-token ${{ secrets.GITHUB_TOKEN }} --nvd-key=${{ secrets.NVD_API_KEY }} ../node ${{ inputs.nodejsStream }} 2>&1 | tee result.log
49+
python main.py --json-output --gh-token ${{ secrets.GITHUB_TOKEN }} --nvd-key=${{ secrets.NVD_API_KEY }} ../node ${{ inputs.nodejsStream }} 2>&1 | tee result.log
5150
)
5251
- name: build matrix
5352
id: set_matrix
5453
if: ${{ failure() }}
5554
working-directory: ./dep_checker
5655
run: |
57-
matrix=$((echo '{ "vulnerability" : ['
58-
cat result.log | sed -n 's/.*\(CVE-.*\|GHSA-.*\).*/"\1",/p' | sed '$s/,//'
59-
echo "]}"
60-
) | jq -c .)
56+
matrix=$(cat result.log | jq -c .)
6157
echo "matrix=$matrix" >> $GITHUB_OUTPUT
6258
63-
- name: collect error
64-
id: collect_error
65-
if: ${{ failure() }}
66-
working-directory: ./dep_checker
67-
run: |
68-
content=$(cat result.log)
69-
# New lines must be escaped since outputs cannot be multi-line
70-
content="${content//'%'/'%25'}"
71-
content="${content//$'\n'/'%0A'}"
72-
content="${content//$'\r'/'%0D'}"
73-
echo "result=$content" >> $GITHUB_OUTPUT
7459
create-issues:
7560
needs: check-vulns
7661
if: ${{ always() }}
@@ -85,7 +70,9 @@ jobs:
8570
search_existing: all
8671
env:
8772
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88-
ERROR_MSG: ${{ needs.check-vulns.outputs.full_output }}
89-
VULN_ID: ${{ matrix.vulnerability }}
73+
VULN_ID: ${{ matrix.vulnerabilities.id }}
74+
VULN_URL: ${{ matrix.vulnerabilities.url }}
75+
VULN_DEP_NAME: ${{ matrix.vulnerabilities.dependency }}
76+
VULN_DEP_VERSION: ${{ matrix.vulnerabilities.version }}
9077
NODEJS_STREAM: ${{ inputs.nodejsStream }}
9178
ACTION_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"

dep_checker/main.py

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,23 @@
2626
from typing import Optional
2727
from pathlib import Path
2828

29+
import json
30+
2931

3032
class Vulnerability:
31-
def __init__(self, id: str, url: str):
33+
def __init__(self, id: str, url: str, dependency: str, version: str):
3234
self.id = id
3335
self.url = url
36+
self.dependency = dependency
37+
self.version = version
38+
39+
40+
class VulnerabilityEncoder(json.JSONEncoder):
41+
def default(self, obj):
42+
if isinstance(obj, Vulnerability):
43+
return {"id": obj.id, "url": obj.url, "dependency": obj.dependency, "version": obj.version}
44+
# Let the base class default method raise the TypeError
45+
return json.JSONEncoder.default(self, obj)
3446

3547

3648
vulnerability_found_message = """For each dependency and vulnerability, check the following:
@@ -64,7 +76,7 @@ def __init__(self, id: str, url: str):
6476

6577
def query_ghad(
6678
dependencies: dict[str, Dependency], gh_token: str, repo_path: Path
67-
) -> dict[str, list[Vulnerability]]:
79+
) -> list[Vulnerability]:
6880
"""Queries the GitHub Advisory Database for vulnerabilities reported for Node's dependencies.
6981
7082
The database supports querying by package name in the NPM ecosystem, so we only send queries for the dependencies
@@ -86,7 +98,7 @@ def query_ghad(
8698
parse_results=True,
8799
)
88100

89-
found_vulnerabilities: dict[str, list[Vulnerability]] = defaultdict(list)
101+
found_vulnerabilities: list[Vulnerability] = list()
90102
for name, dep in deps_in_npm.items():
91103
variables_package = {
92104
"package_name": dep.npm_name,
@@ -103,10 +115,10 @@ def query_ghad(
103115
and v["advisory"]["ghsaId"] not in ignore_list
104116
]
105117
if matching_vulns:
106-
found_vulnerabilities[name].extend(
118+
found_vulnerabilities.extend(
107119
[
108120
Vulnerability(
109-
id=vuln["advisory"]["ghsaId"], url=vuln["advisory"]["permalink"]
121+
id=vuln["advisory"]["ghsaId"], url=vuln["advisory"]["permalink"], dependency=name, version=dep_version
110122
)
111123
for vuln in matching_vulns
112124
]
@@ -117,7 +129,7 @@ def query_ghad(
117129

118130
def query_nvd(
119131
dependencies: dict[str, Dependency], api_key: Optional[str], repo_path: Path
120-
) -> dict[str, list[Vulnerability]]:
132+
) -> list[Vulnerability]:
121133
"""Queries the National Vulnerability Database for vulnerabilities reported for Node's dependencies.
122134
123135
The database supports querying by CPE (Common Platform Enumeration) or by a keyword present in the CVE's
@@ -129,7 +141,7 @@ def query_nvd(
129141
for name, dep in dependencies.items()
130142
if dep.cpe is not None or dep.keyword is not None
131143
}
132-
found_vulnerabilities: dict[str, list[Vulnerability]] = defaultdict(list)
144+
found_vulnerabilities: list[Vulnerability] = list()
133145
for name, dep in deps_in_nvd.items():
134146
query_results = [
135147
cve
@@ -139,8 +151,9 @@ def query_nvd(
139151
if cve.id not in ignore_list
140152
]
141153
if query_results:
142-
found_vulnerabilities[name].extend(
143-
[Vulnerability(id=cve.id, url=cve.url) for cve in query_results]
154+
version = dep.version_parser(repo_path)
155+
found_vulnerabilities.extend(
156+
[Vulnerability(id=cve.id, url=cve.url, dependency=name, version=version) for cve in query_results]
144157
)
145158

146159
return found_vulnerabilities
@@ -170,10 +183,16 @@ def main() -> int:
170183
"--nvd-key",
171184
help="the NVD API key for querying the National Vulnerability Database",
172185
)
186+
parser.add_argument(
187+
"--json-output",
188+
action='store_true',
189+
help="the NVD API key for querying the National Vulnerability Database",
190+
)
173191
repo_path: Path = parser.parse_args().node_repo_path
174192
repo_branch: str = parser.parse_args().node_repo_branch
175193
gh_token = parser.parse_args().gh_token
176194
nvd_key = parser.parse_args().nvd_key
195+
json_output: bool = parser.parse_args().json_output
177196
if not repo_path.exists() or not (repo_path / ".git").exists():
178197
raise RuntimeError(
179198
"Invalid argument: '{repo_path}' is not a valid Node git repository"
@@ -196,25 +215,27 @@ def main() -> int:
196215
for name, dep in dependencies_info.items()
197216
if name in dependencies_per_branch[repo_branch]
198217
}
199-
ghad_vulnerabilities: dict[str, list[Vulnerability]] = (
218+
ghad_vulnerabilities: list[Vulnerability] = (
200219
{} if gh_token is None else query_ghad(dependencies, gh_token, repo_path)
201220
)
202-
nvd_vulnerabilities: dict[str, list[Vulnerability]] = query_nvd(
221+
nvd_vulnerabilities: list[Vulnerability] = query_nvd(
203222
dependencies, nvd_key, repo_path
204223
)
205224

206-
if not ghad_vulnerabilities and not nvd_vulnerabilities:
225+
all_vulnerabilities = {"vulnerabilities": ghad_vulnerabilities + nvd_vulnerabilities}
226+
no_vulnerabilities_found = not ghad_vulnerabilities and not nvd_vulnerabilities
227+
if json_output:
228+
print(json.dumps(all_vulnerabilities, cls=VulnerabilityEncoder))
229+
return 0 if no_vulnerabilities_found else 1
230+
elif no_vulnerabilities_found:
207231
print(f"No new vulnerabilities found ({len(ignore_list)} ignored)")
208232
return 0
209233
else:
210234
print("WARNING: New vulnerabilities found")
211-
for source in (ghad_vulnerabilities, nvd_vulnerabilities):
212-
for name, vulns in source.items():
213-
print(
214-
f"- {name} (version {dependencies[name].version_parser(repo_path)}) :"
215-
)
216-
for v in vulns:
217-
print(f"\t- {v.id}: {v.url}")
235+
for vuln in all_vulnerabilities["vulnerabilities"]:
236+
print(
237+
f"- {vuln.dependency} (version {vuln.version}) : {vuln.id} ({vuln.url})"
238+
)
218239
print(f"\n{vulnerability_found_message}")
219240
return 1
220241

0 commit comments

Comments
 (0)