Skip to content

Commit 5ca7a52

Browse files
authored
Merge pull request #1986 from tisnik/lcore-2631-docstrings
LCORE-2631: Better docstrings
2 parents c9871df + 72acc17 commit 5ca7a52

1 file changed

Lines changed: 143 additions & 40 deletions

File tree

scripts/vulnerability_report.py

Lines changed: 143 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@
3131
alerts needs to be provided
3232
"""
3333

34-
from statistics import median
35-
from datetime import datetime
36-
from dateutil import parser
37-
from collections import Counter
38-
from argparse import ArgumentParser, Namespace
3934
import json
40-
35+
from argparse import ArgumentParser, Namespace
36+
from collections import Counter
37+
from datetime import datetime
38+
from statistics import median
4139
from typing import Any
4240

41+
from dateutil import parser
42+
4343
type DependabotAlert = dict[str, Any]
4444
type DependabotAlerts = list[DependabotAlert]
4545

@@ -50,11 +50,12 @@ def create_argument_parser() -> ArgumentParser:
5050
The parser includes the following options:
5151
5252
Returns:
53-
Configured ArgumentParser for parsing the command line options.
53+
An ArgumentParser configured with command-line options for
54+
organization, repository, and report generation settings.
5455
"""
55-
parser = ArgumentParser(description="Vulnerability report tool")
56+
cli_parser = ArgumentParser(description="Vulnerability report tool")
5657

57-
parser.add_argument(
58+
cli_parser.add_argument(
5859
"-v",
5960
"--verbose",
6061
dest="verbose",
@@ -63,45 +64,45 @@ def create_argument_parser() -> ArgumentParser:
6364
default=False,
6465
)
6566

66-
parser.add_argument(
67+
cli_parser.add_argument(
6768
"--organization",
6869
default="",
6970
help="GitHub organization.",
7071
required=True,
7172
)
7273

73-
parser.add_argument(
74+
cli_parser.add_argument(
7475
"--repository",
7576
default="",
7677
help="GitHub repository.",
7778
required=True,
7879
)
7980

80-
parser.add_argument(
81+
cli_parser.add_argument(
8182
"-r",
8283
"--retrieve-issues",
8384
action="store_true",
8485
default=False,
8586
help="Retrieve issues",
8687
)
8788

88-
parser.add_argument(
89+
cli_parser.add_argument(
8990
"-g",
9091
"--generate-graphs",
9192
action="store_true",
9293
default=False,
9394
help="Generate graphs with vulnerabilities info",
9495
)
9596

96-
parser.add_argument(
97+
cli_parser.add_argument(
9798
"-p",
9899
"--generate-page",
99100
action="store_true",
100101
default=False,
101102
help="Generate page with vulnerabilities info",
102103
)
103104

104-
parser.add_argument(
105+
cli_parser.add_argument(
105106
"-c",
106107
"--comparison",
107108
required=False,
@@ -111,17 +112,33 @@ def create_argument_parser() -> ArgumentParser:
111112
"Multiple JSON files with Dependabot alerts needs to be provided",
112113
)
113114

114-
return parser
115+
return cli_parser
115116

116117

117118
def dependabot_file_name(args: Namespace) -> str:
118-
"""Construct file name containing Dependabot alerts."""
119+
"""Construct file name containing Dependabot alerts.
120+
121+
Build the expected input filename for Dependabot alerts.
122+
123+
Filename format: {organization}__{repository}.json
124+
"""
119125
return f"{args.organization}__{args.repository}.json"
120126

121127

122128
def load_dependabot_file(filename: str) -> Any:
123-
"""Load JSON file containing Dependabot alerts."""
124-
with open(filename, "r") as fin:
129+
"""
130+
Load and validate a JSON file containing Dependabot alerts.
131+
132+
Ensures the JSON is a list of objects, each containing a 'state' field and
133+
a nested 'security_advisory' object with a 'severity' field.
134+
135+
Returns:
136+
list: A list of validated alert dictionaries.
137+
138+
Raises:
139+
ValueError: If the JSON structure is invalid.
140+
"""
141+
with open(filename, "r", encoding="utf-8") as fin:
125142
data = json.load(fin)
126143

127144
# perform sanity check
@@ -141,21 +158,40 @@ def load_dependabot_file(filename: str) -> Any:
141158

142159

143160
def has_attribute_with_value(item: DependabotAlert, attribute: str, value: str) -> bool:
144-
"""Check if dictionary has attribute with given value."""
161+
"""
162+
Determine if an alert attribute matches a specified value.
163+
164+
Returns:
165+
`true` if the attribute value equals the specified value, `false` otherwise.
166+
"""
145167
return bool(item[attribute] == value)
146168

147169

148170
def has_deep_attribute_with_value(
149171
item: DependabotAlert, selector: str, attribute: str, value: str
150172
) -> bool:
151-
"""Check if dictionary has deep attribute with given value."""
173+
"""
174+
Determine if a nested dictionary attribute has a specific value.
175+
176+
Returns:
177+
true if item[selector][attribute] equals the given value, false otherwise.
178+
"""
152179
return bool(item[selector][attribute] == value)
153180

154181

155182
def count_attribute_with_value(
156183
items: DependabotAlerts, attribute: str, value: str
157184
) -> int:
158-
"""Count all attributes with given value."""
185+
"""
186+
Count the number of items where the specified attribute equals the given value.
187+
188+
Parameters:
189+
attribute (str): The attribute name to check in each item.
190+
value (str): The target value to match.
191+
192+
Returns:
193+
int: The number of items where the attribute equals the value.
194+
"""
159195
cnt: int = 0
160196
for item in items:
161197
if has_attribute_with_value(item, attribute, value):
@@ -166,7 +202,17 @@ def count_attribute_with_value(
166202
def count_deep_attribute_with_value(
167203
items: DependabotAlerts, selector: str, attribute: str, value: str
168204
) -> int:
169-
"""Count all deep attributes with given value."""
205+
"""
206+
Count alerts where a nested attribute equals a specified value.
207+
208+
Parameters:
209+
selector (str): The top-level key within each alert
210+
attribute (str): The key within the nested object accessed by selector
211+
value (str): The value to match against
212+
213+
Returns:
214+
int: Number of alerts where item[selector][attribute] equals value
215+
"""
170216
cnt: int = 0
171217
for item in items:
172218
if has_deep_attribute_with_value(item, selector, attribute, value):
@@ -180,12 +226,22 @@ def opened_cves(source_data: DependabotAlerts) -> int:
180226

181227

182228
def fixed_cves(source_data: DependabotAlerts) -> int:
183-
"""Compute how many CVEs has been fixed opened."""
229+
"""
230+
Count the number of fixed CVEs.
231+
232+
Returns:
233+
The count of fixed CVEs.
234+
"""
184235
return count_attribute_with_value(source_data, "state", "fixed")
185236

186237

187238
def with_severity(severity: str, source_data: DependabotAlerts) -> int:
188-
"""Count number of CVE having specified severity."""
239+
"""
240+
Count alerts with the specified severity level.
241+
242+
Returns:
243+
int: The number of alerts matching the specified severity
244+
"""
189245
return count_deep_attribute_with_value(
190246
source_data, "security_advisory", "severity", severity
191247
)
@@ -194,20 +250,38 @@ def with_severity(severity: str, source_data: DependabotAlerts) -> int:
194250
def filter_by(
195251
source_data: DependabotAlerts, attribute: str, value: str
196252
) -> DependabotAlerts:
197-
"""Filter source data: retrieve only attribute with give value."""
253+
"""
254+
Return alerts where a specified attribute equals a given value.
255+
256+
Returns:
257+
A list of alerts where the specified attribute matches the given value.
258+
"""
198259
return [item for item in source_data if item[attribute] == value]
199260

200261

201262
def fill_in_state(source_data: DependabotAlerts) -> dict[str, int]:
202-
"""Fill-in the overall vulnerabilities state."""
263+
"""
264+
Count the number of open and fixed vulnerabilities across all alerts.
265+
266+
Returns:
267+
dict[str, int]: A dictionary with 'open' and 'fixed' keys mapping to
268+
their respective alert counts.
269+
"""
203270
state = {}
204271
state["open"] = opened_cves(source_data)
205272
state["fixed"] = fixed_cves(source_data)
206273
return state
207274

208275

209276
def fill_in_severity(source_data: DependabotAlerts) -> dict[str, int]:
210-
"""Fill-in the severity statistic."""
277+
"""
278+
Compute the count of alerts for each severity level.
279+
280+
Returns:
281+
severity (dict[str, int]): A dict with keys 'critical', 'high',
282+
'medium', and 'low', each mapped to the count of alerts with that
283+
severity.
284+
"""
211285
severity = {}
212286
severity["critical"] = with_severity("critical", source_data)
213287
severity["high"] = with_severity("high", source_data)
@@ -217,7 +291,12 @@ def fill_in_severity(source_data: DependabotAlerts) -> dict[str, int]:
217291

218292

219293
def fill_in_severities_set(source_data: DependabotAlerts) -> set[str]:
220-
"""Fill-in the set with severities."""
294+
"""
295+
Collect all distinct severity levels from Dependabot alerts.
296+
297+
Returns:
298+
set[str]: A set of unique severity levels found across the alerts.
299+
"""
221300
# Severity can be set to:
222301
# - low
223302
# - medium
@@ -230,36 +309,59 @@ def fill_in_severities_set(source_data: DependabotAlerts) -> set[str]:
230309

231310

232311
def fill_in_days_statistic(source_data: DependabotAlerts) -> dict[str, Any]:
233-
"""Fill-in statistic about days needed to fix the CVEs."""
312+
"""
313+
Compute statistics on elapsed days from creation to fix for alerts in a fixed state.
314+
315+
Returns:
316+
dict[str, Any]: Dictionary with "days" (list of elapsed day values),
317+
"avg" (average days to fix), and "median" (median days to fix).
318+
"""
234319
fixed = filter_by(source_data, "state", "fixed")
235320
days = []
236321
for item in fixed:
237322
dt1 = parser.isoparse(item["created_at"])
238323
dt2 = parser.isoparse(item["fixed_at"])
239324
d = (dt2 - dt1).total_seconds() / 86400
240325
days.append(d)
241-
days_stat = {}
326+
days_stat: dict[str, Any] = {}
242327
days_stat["days"] = days
243-
days_stat["avg"] = sum(days) / len(days)
244-
days_stat["median"] = median(days)
328+
# avoid division by zero
329+
if not days:
330+
days_stat["avg"] = sum(days) / len(days)
331+
days_stat["median"] = median(days)
332+
else:
333+
days_stat["avg"] = 0
334+
days_stat["median"] = 0
245335
return days_stat
246336

247337

248338
def fill_in_vulnerable_packages(source_data: DependabotAlerts) -> Counter[Any]:
249-
"""Fill-in vulnerable packages with CVE frequency info."""
339+
"""
340+
Count the frequency of vulnerable packages across all alerts.
341+
342+
Returns:
343+
A Counter mapping package names to their occurrence counts.
344+
"""
250345
package_names = [item["dependency"]["package"]["name"] for item in source_data]
251346
return Counter(package_names)
252347

253348

254349
def fill_in_cve_created_dates(source_data: DependabotAlerts) -> Counter[datetime]:
255-
"""Fill-in dates (freq.) when new CVE was detected by Dependabot."""
350+
"""
351+
Count the number of CVE alerts by their detection date.
352+
353+
Returns:
354+
A Counter mapping each creation date to the number of alerts created on that date.
355+
"""
256356
dates_str = [item["created_at"][:10] for item in source_data]
257357
dates = [datetime.strptime(date_str, "%Y-%m-%d") for date_str in dates_str]
258358
return Counter(dates)
259359

260360

261-
def process_dependabot_file(dependabot_file: str, prefix: str) -> dict[str, Any]:
262-
"""Read Dependabot alerts and prepare statistic info."""
361+
def process_dependabot_file(dependabot_file: str) -> dict[str, Any]:
362+
"""
363+
Compute vulnerability statistics from Dependabot alerts stored in a JSON file.
364+
"""
263365
source_data = load_dependabot_file(dependabot_file)
264366

265367
# dictionary holding the whole statistic about vulnerabilities
@@ -293,12 +395,13 @@ def main() -> int:
293395
`0` indicates success,
294396
`1` indicates any failure
295397
"""
296-
parser = create_argument_parser()
297-
args = parser.parse_args()
398+
cli_parser = create_argument_parser()
399+
args = cli_parser.parse_args()
298400
dependabot_file = dependabot_file_name(args)
299401
prefix = args.repository
300-
stat = process_dependabot_file(dependabot_file, prefix)
402+
stat = process_dependabot_file(dependabot_file)
301403
print(stat)
404+
print(prefix)
302405

303406
return 0
304407

0 commit comments

Comments
 (0)