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
3934import json
40-
35+ from argparse import ArgumentParser , Namespace
36+ from collections import Counter
37+ from datetime import datetime
38+ from statistics import median
4139from typing import Any
4240
41+ from dateutil import parser
42+
4343type DependabotAlert = dict [str , Any ]
4444type 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
117118def 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
122128def 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
143160def 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
148170def 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
155182def 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(
166202def 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
182228def 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
187238def 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:
194250def 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
201262def 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
209276def 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
219293def 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
232311def 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
248338def 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
254349def 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