diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index d0cc8a470..631dd14b4 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -7,10 +7,9 @@ repositories. # Usage: -usage: vulnerability_report.py [-h] [-v] [--organization ORGANIZATION] - [--repository REPOSITORY] [-r] - [-s {auto_dismissed,dismissed,fixed,open}] [-a] - [-g] [-svg] [-png] [-p] +usage: vulnerability_report.py [-h] [-v] [--organization ORGANIZATION] [--repository REPOSITORY] + [-r] [-s {auto_dismissed,dismissed,fixed,open}] [-a] [-g] + [-f FROM_DATE] [-svg] [-png] [-p] [-i] [-c COMPARISON [COMPARISON ...]] Vulnerability report tool @@ -32,6 +31,10 @@ -svg, --svg-output Generate graphs in SVG format -png, --png-output Generate graphs in PNG format -p, --generate-page Generate page with vulnerabilities info + -i, --generate-issues + Generate list of issues to be send to JIRA + -f, --from-date FROM_DATE + Date filter used when --generate-issues operation is enabled -c, --comparison COMPARISON [COMPARISON ...] Compare two or more repositories and generate comparison report. Multiple JSON files with Dependabot @@ -41,9 +44,10 @@ import json import os import re +import sys from argparse import ArgumentParser, Namespace from collections import Counter -from datetime import datetime +from datetime import datetime, timezone from statistics import median from typing import Any, Optional @@ -155,6 +159,22 @@ def create_argument_parser() -> ArgumentParser: help="Generate page with vulnerabilities info", ) + cli_parser.add_argument( + "-i", + "--generate-issues", + action="store_true", + default=False, + help="Generate list of issues to be send to JIRA", + ) + + cli_parser.add_argument( + "-f", + "--from-date", + action="store", + default=None, + help="Date filter used when --generate-issues operation is enabled", + ) + cli_parser.add_argument( "-c", "--comparison", @@ -727,6 +747,74 @@ def generate_page( fout.write(output) +def parse_datetime(value: str) -> datetime: + """Parse an ISO-format datetime and convert it to UTC. + + A value without a timezone is treated as UTC. Invalid values raise ValueError. + """ + dt = datetime.fromisoformat(value) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc) + + +def is_after(date_str: str, from_date: datetime) -> bool: + """Return whether the parsed date is strictly later than ``from_date``. + + Invalid date strings raise ValueError; a naive ``from_date`` raises TypeError. + """ + return parse_datetime(date_str) > from_date + + +def generate_list_of_issues( + dependabot_file: str, from_date_as_string: Optional[str] +) -> None: + """Print Jira issue lines for alerts, optionally filtered by creation time. + + ``from_date_as_string`` is an ISO-format datetime; only alerts created + strictly after it are printed. Without it, all alerts are considered. Titles + have quotes removed and are limited to 255 characters; occurrences of + ``.konflux/`` are removed from the manifest path. Malformed entries raising + KeyError, TypeError, or ValueError are reported to stderr and skipped. Other errors, such as AttributeError + for a non-string manifest path, propagate. + + Raises: + OSError: If the input file cannot be read. + ValueError: If JSON decoding or parsing the supplied filter date fails. + + """ + with open(dependabot_file, "r", encoding="utf-8") as fin: + issues = json.load(fin) + + from_date: Optional[datetime] = None + + if from_date_as_string is not None: + from_date = parse_datetime(from_date_as_string) + + for issue in issues: + try: + path = issue["dependency"]["manifest_path"] + path = path.replace(".konflux/", "") + title = f"{issue["security_advisory"]["summary"]} [{path}]" + title = title.replace('"', "") + title = title.replace("'", "") + # Jira Cloud rejects summaries longer than 255 characters. + title = title[:255] + url = issue["html_url"] + + if from_date is None: + # w/o filtering + print(f'ISSUE "{title}" "{url}"') + else: + # perform filtering + created_at = issue["created_at"] + if is_after(created_at, from_date): + print(f'ISSUE "{title}" "{url}"') + except (KeyError, TypeError, ValueError) as e: + # skip malformed entries + print(f"Error retrieving issue: {e}", file=sys.stderr) + + def is_valid_github_org_repo(name: str) -> bool: """ Check whether a GitHub organization or repository name is valid. @@ -766,10 +854,11 @@ def retrieve_issues( """ Retrieve all Dependabot alerts for a GitHub repository and save them to a JSON file. - The function validates the organization and repository names, checks that the - GITHUB_TOKEN environment variable is set, then calls the GitHub Dependabot alerts - API and follows pagination until all alerts are collected. The resulting alerts - are written to the specified output file in JSON format. + The function validates the organization and repository names, checks that + the GITHUB_TOKEN environment variable is set, then calls the GitHub + Dependabot alerts API and follows pagination until all alerts are + collected. The resulting alerts are written to the specified output file in + JSON format. Args: organization: GitHub organization name. @@ -879,19 +968,23 @@ def process_issues_for_repository( retrieve_issues_operation: bool = True, generate_graphs_operation: bool = True, generate_page_operation: bool = True, + generate_issues_operation: bool = True, + from_date: Optional[str] = None, ) -> int: """Retrieve alerts or process the local file and generate selected outputs. Parameters: organization (str): name of GitHub organization repository (str): name of GitHub repository - state_filter (str): used to filter issues with specified state + state_filter (Optional[str]): used to filter issues with specified state dependabot_file (str): file to store Dependabot issues svg_output (bool): if set to true, CVE statistic is exported into SVG graphs png_output (bool): if set to true, CVE statistic is exported into PNG graphs retrieve_issues_operation (bool): enable retrieving Dependabot issues generate_graphs_operation (bool): enable generating graphs in SVG or PNG format generate_page_operation (bool): enable generating page with overall status + generate_issues_operation (bool): enable generating list of issues to be send into JIRA + from_date (Optional[str]): date used to filter issues Retrieval takes precedence over report generation and returns its status. Otherwise return 0 after processing; file, data, and plotting errors propagate. @@ -918,6 +1011,9 @@ def process_issues_for_repository( if generate_page_operation: generate_page(stat, prefix, organization, repository) + if generate_issues_operation: + generate_list_of_issues(dependabot_file, from_date) + return 0 @@ -965,6 +1061,7 @@ def main() -> int: dependabot_file, args.svg_output, args.png_output, + from_date=args.from_date, ) return 1 if errors > 0 else 0 @@ -981,6 +1078,8 @@ def main() -> int: args.retrieve_issues, args.generate_graphs, args.generate_page, + args.generate_issues, + args.from_date, )