From e172804dd28d3c1d6515ddc4da4ba291c5a1f756 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Wed, 23 Sep 2026 16:46:51 +0200 Subject: [PATCH] feat: open security-wg VEX entry PR when an issue is closed with a justification label Closing an issue with one of the five OpenVEX `not_affected` justification labels now runs the `vex-entry` workflow, which generates a `vuln/deps/.json` entry and opens a pull request against nodejs/security-wg. CVEs already recorded in `vuln/deps` are skipped, so the per-release-line duplicates of the same CVE do not produce duplicate entries. The README documents the labels, the affected path (`confirmed`), the legacy labels, and the `SECURITY_WG_TOKEN` secret the workflow needs. Supersedes #225. Closes #224 Co-Authored-By: sddhantjaiii <122254271+sddhantjaiii@users.noreply.github.com> Assisted-By: Claude Fable 5.1 --- .github/workflows/test.yml | 23 ++++ .github/workflows/vex-entry.yml | 102 ++++++++++++++ .gitignore | 2 + README.md | 57 ++++++++ dep_checker/test_vex_entry.py | 225 ++++++++++++++++++++++++++++++ dep_checker/vex_entry.py | 236 ++++++++++++++++++++++++++++++++ 6 files changed, 645 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .github/workflows/vex-entry.yml create mode 100644 .gitignore create mode 100644 dep_checker/test_vex_entry.py create mode 100644 dep_checker/vex_entry.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..326f9a7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Setup Python 3.9 + uses: actions/setup-python@v6 + with: + python-version: '3.9' + - name: Run unit tests + working-directory: ./dep_checker + run: python -m unittest discover -p 'test_*.py' -v diff --git a/.github/workflows/vex-entry.yml b/.github/workflows/vex-entry.yml new file mode 100644 index 0000000..e35a5fb --- /dev/null +++ b/.github/workflows/vex-entry.yml @@ -0,0 +1,102 @@ +name: Create VEX entry in nodejs/security-wg + +# When an issue is closed with one of the OpenVEX justification labels, open a +# pull request against nodejs/security-wg adding a `vuln/deps/.json` entry. +# See README.md, section "VEX labels", for the label meanings. + +on: + issues: + types: [closed] + +permissions: + contents: read + issues: write + +jobs: + vex-entry: + if: | + contains(github.event.issue.labels.*.name, 'component_not_present') || + contains(github.event.issue.labels.*.name, 'vulnerable_code_not_present') || + contains(github.event.issue.labels.*.name, 'vulnerable_code_not_in_execute_path') || + contains(github.event.issue.labels.*.name, 'vulnerable_code_cannot_be_controlled_by_adversary') || + contains(github.event.issue.labels.*.name, 'inline_mitigations_already_exist') + runs-on: ubuntu-latest + steps: + - name: Checkout current repository + uses: actions/checkout@v7 + + - name: Checkout nodejs/security-wg + uses: actions/checkout@v7 + with: + repository: nodejs/security-wg + path: security-wg + token: ${{ secrets.SECURITY_WG_TOKEN }} + + - name: Setup Python 3.9 + uses: actions/setup-python@v6 + with: + python-version: '3.9' + + - name: Fetch issue comments + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api --paginate "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments" > comments.json + + - name: Generate VEX entry + id: entry + working-directory: ./dep_checker + env: + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_URL: ${{ github.event.issue.html_url }} + ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, ',') }} + CLOSED_BY: ${{ github.event.sender.login }} + run: | + python vex_entry.py \ + --issue-title "$ISSUE_TITLE" \ + --issue-url "$ISSUE_URL" \ + --labels "$ISSUE_LABELS" \ + --closed-by "$CLOSED_BY" \ + --comments-file ../comments.json \ + --deps-dir ../security-wg/vuln/deps + + - name: Create pull request in nodejs/security-wg + id: pr + if: steps.entry.outputs.skipped == 'false' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.SECURITY_WG_TOKEN }} + path: security-wg + branch: ${{ steps.entry.outputs.branch }} + delete-branch: true + commit-message: 'vuln: add deps entry for ${{ steps.entry.outputs.cves }}' + title: 'vuln: add deps entry for ${{ steps.entry.outputs.cves }}' + body: | + Adds `vuln/deps/${{ steps.entry.outputs.entry_file }}` marking ${{ steps.entry.outputs.cves }} as `not_affected`. + + Source: ${{ github.event.issue.html_url }} + Closed by: @${{ github.event.sender.login }} + + Please review the `overview` text before merging. cc: @nodejs/security-wg + labels: security-wg-agenda + + - name: Comment on the issue + if: always() && steps.entry.outcome != 'skipped' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE: ${{ github.event.issue.number }} + RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + ENTRY_OUTCOME: ${{ steps.entry.outcome }} + SKIPPED: ${{ steps.entry.outputs.skipped }} + PR_URL: ${{ steps.pr.outputs.pull-request-url }} + run: | + if [ "$ENTRY_OUTCOME" != "success" ]; then + body="The VEX entry could not be generated, see $RUN_URL. Check that the title contains a CVE id and that exactly one VEX justification label is set." + elif [ "$SKIPPED" = "true" ]; then + body="All CVEs in this issue are already recorded in nodejs/security-wg \`vuln/deps\`, no VEX entry created." + elif [ -n "$PR_URL" ]; then + body="VEX entry opened in nodejs/security-wg: $PR_URL" + else + body="VEX entry already pending in nodejs/security-wg, see $RUN_URL." + fi + gh issue comment "$ISSUE" --repo "${{ github.repository }}" --body "$body" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md index 3fe6e12..b4aea72 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,63 @@ This repo is used to Automated checks are currently run through a GitHub action using [dep_checker](https://github.com/nodejs/nodejs-dependency-vuln-assessments/tree/main/dep_checker). +## Triage labels + +Every issue opened by the scanner must be closed with a label that records +the outcome of the triage. The labels below map one to one onto the +[OpenVEX status justifications](https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-justifications) +and are the only ones that feed the Node.js VEX document. + +### Not affected + +Closing an issue with exactly one of these labels triggers the +[vex-entry](.github/workflows/vex-entry.yml) workflow, which opens a pull +request in [nodejs/security-wg](https://github.com/nodejs/security-wg/tree/main/vuln/deps) +adding a `vuln/deps/.json` entry with `status: not_affected` and the label +as `reason`. Once that pull request is merged, `node.openvex.json` is +regenerated and scanners consuming it stop reporting the CVE. + +| Label | Use when | +| --- | --- | +| `component_not_present` | The vulnerable component (library, module, or file) is not shipped in Node.js at all. | +| `vulnerable_code_not_present` | The dependency is shipped, but the vulnerable code is removed or compiled out of the Node.js build. | +| `vulnerable_code_not_in_execute_path` | The vulnerable code is compiled in, but Node.js never calls it and does not expose it through its APIs. | +| `vulnerable_code_cannot_be_controlled_by_adversary` | Node.js calls the vulnerable code, but an attacker cannot control the inputs that trigger the bug. | +| `inline_mitigations_already_exist` | Node.js calls the vulnerable code, but a mitigation elsewhere in Node.js prevents exploitation. | + +The workflow builds the entry as follows: + +- `cve`: every CVE id found in the issue title. CVEs already present in + `vuln/deps` are skipped, so closing the per-release-line duplicates of the + same CVE does not create duplicate entries. +- `description`: the CVE record title from cve.org. +- `overview`: the most recent comment on the issue written by the person who + closed it, or a generic sentence for the label if they left no comment. + Write the reasoning as a comment before closing, it becomes the public + `impact_statement`. +- `ref`: the issue URL. + +Review the `overview` text in the security-wg pull request before merging. +If the issue has no CVE id in its title, or more than one of these labels, +the workflow fails and comments on the issue. + +### Affected + +| Label | Use when | +| --- | --- | +| `confirmed` | The vulnerability affects Node.js. The fix ships in a security release and the VEX statement is produced from `vuln/core` by the security release process, not by this repository. | + +### Legacy labels + +`dont-believe-affects-nodejs` and `dont-fall-in-threat-model` predate the VEX +integration. They do not trigger any automation. Use one of the +`not_affected` labels above instead. + +### Setup + +The workflow needs a `SECURITY_WG_TOKEN` repository secret holding a token +with `contents: write` and `pull-requests: write` on nodejs/security-wg. + **DO NOT REPORT OR DISCUSS VULNERABILITIES THAT ARE NOT ALREADY PUBLIC IN THIS REPO**. Please report new vulnerabilities either to the projects for a specific dependency or report to the Node.js project diff --git a/dep_checker/test_vex_entry.py b/dep_checker/test_vex_entry.py new file mode 100644 index 0000000..c9d09b0 --- /dev/null +++ b/dep_checker/test_vex_entry.py @@ -0,0 +1,225 @@ +import json +import os +import tempfile +import unittest +from pathlib import Path + +import vex_entry + + +class ParseCveIds(unittest.TestCase): + def test_extracts_single_cve_from_scanner_title(self): + self.assertEqual( + vex_entry.parse_cve_ids("CVE-2026-78227 (ngtcp2) found on main"), + ["CVE-2026-78227"], + ) + + def test_extracts_multiple_cves_in_order_without_duplicates(self): + title = "cve-2025-9230, CVE-2025-9231, CVE-2025-9230, CVE-2025-9232 (OpenSSL)" + self.assertEqual( + vex_entry.parse_cve_ids(title), + ["CVE-2025-9230", "CVE-2025-9231", "CVE-2025-9232"], + ) + + def test_returns_empty_list_when_no_cve(self): + self.assertEqual(vex_entry.parse_cve_ids("GHSA-xxxx (foo) found on main"), []) + + +class JustificationFromLabels(unittest.TestCase): + def test_returns_the_single_vex_label(self): + self.assertEqual( + vex_entry.justification_from_labels( + ["v24.x", "vulnerable_code_not_in_execute_path"] + ), + "vulnerable_code_not_in_execute_path", + ) + + def test_raises_when_no_vex_label(self): + with self.assertRaises(vex_entry.VexEntryError): + vex_entry.justification_from_labels(["v24.x", "dont-believe-affects-nodejs"]) + + def test_raises_when_multiple_vex_labels(self): + with self.assertRaises(vex_entry.VexEntryError): + vex_entry.justification_from_labels( + ["component_not_present", "inline_mitigations_already_exist"] + ) + + +class DepsDirectory(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.deps = Path(self._tmp.name) + (self.deps / "1.json").write_text( + json.dumps({"cve": ["CVE-2023-45853"], "reason": "vulnerable_code_not_present"}) + ) + (self.deps / "6.json").write_text( + json.dumps({"cve": ["CVE-2025-9230", "CVE-2025-9231"], "reason": "x"}) + ) + (self.deps / "index.json").write_text("{}") + + def tearDown(self): + self._tmp.cleanup() + + def test_existing_cves_maps_cve_to_file(self): + self.assertEqual( + vex_entry.existing_cves(self.deps), + { + "CVE-2023-45853": "1.json", + "CVE-2025-9230": "6.json", + "CVE-2025-9231": "6.json", + }, + ) + + def test_next_entry_number_is_max_plus_one(self): + self.assertEqual(vex_entry.next_entry_number(self.deps), 7) + + def test_next_entry_number_starts_at_one_for_empty_dir(self): + with tempfile.TemporaryDirectory() as empty: + self.assertEqual(vex_entry.next_entry_number(Path(empty)), 1) + + +class Overview(unittest.TestCase): + comments = [ + {"user": {"login": "alice"}, "body": "first"}, + {"user": {"login": "bob"}, "body": "bob says no"}, + {"user": {"login": "alice"}, "body": "Node.js never calls this code path."}, + {"user": {"login": "carol"}, "body": "later"}, + ] + + def test_uses_latest_comment_by_closer(self): + self.assertEqual( + vex_entry.overview_from_comments(self.comments, "alice"), + "Node.js never calls this code path.", + ) + + def test_falls_back_to_default_when_closer_has_no_comment(self): + self.assertEqual( + vex_entry.overview_from_comments(self.comments, "dave"), + None, + ) + + def test_default_overview_mentions_reason(self): + text = vex_entry.default_overview("vulnerable_code_not_in_execute_path") + self.assertIn("not in the execution path", text) + + +class BuildEntry(unittest.TestCase): + def test_entry_matches_security_wg_schema(self): + entry = vex_entry.build_entry( + cves=["CVE-2025-9230"], + description="OpenSSL bug", + overview="Not reachable from Node.js.", + ref="https://github.com/nodejs/nodejs-dependency-vuln-assessments/issues/213", + reason="vulnerable_code_not_in_execute_path", + ) + self.assertEqual( + entry, + { + "cve": ["CVE-2025-9230"], + "description": "OpenSSL bug", + "overview": "Not reachable from Node.js.", + "ref": "https://github.com/nodejs/nodejs-dependency-vuln-assessments/issues/213", + "reason": "vulnerable_code_not_in_execute_path", + }, + ) + + +class DescriptionFromCveRecord(unittest.TestCase): + def test_prefers_title_then_first_english_description(self): + record = { + "containers": { + "cna": { + "title": "Out-of-bounds read in CMS", + "descriptions": [{"lang": "en", "value": "long text"}], + } + } + } + self.assertEqual( + vex_entry.description_from_cve_record(record), "Out-of-bounds read in CMS" + ) + + def test_uses_description_when_title_missing(self): + record = { + "containers": { + "cna": {"descriptions": [{"lang": "en", "value": "long text"}]} + } + } + self.assertEqual(vex_entry.description_from_cve_record(record), "long text") + + +class MainEndToEnd(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + root = Path(self._tmp.name) + self.deps = root / "deps" + self.deps.mkdir() + (self.deps / "3.json").write_text( + json.dumps({"cve": ["CVE-2025-9230"], "reason": "vulnerable_code_not_in_execute_path"}) + ) + self.comments = root / "comments.json" + self.comments.write_text( + json.dumps([{"user": {"login": "rafael"}, "body": "Node.js does not use CMS."}]) + ) + self.github_output = root / "out.txt" + self.github_output.write_text("") + self._old_env = os.environ.get("GITHUB_OUTPUT") + os.environ["GITHUB_OUTPUT"] = str(self.github_output) + self._old_fetch = vex_entry.fetch_cve_record + vex_entry.fetch_cve_record = lambda cve: { + "containers": {"cna": {"title": f"Title for {cve}"}} + } + + def tearDown(self): + vex_entry.fetch_cve_record = self._old_fetch + if self._old_env is None: + os.environ.pop("GITHUB_OUTPUT", None) + else: + os.environ["GITHUB_OUTPUT"] = self._old_env + self._tmp.cleanup() + + def run_main(self, title, labels): + return vex_entry.main( + [ + "--issue-title", title, + "--issue-url", "https://github.com/nodejs/nodejs-dependency-vuln-assessments/issues/400", + "--labels", labels, + "--closed-by", "rafael", + "--comments-file", str(self.comments), + "--deps-dir", str(self.deps), + ] + ) + + def test_writes_next_file_for_new_cves_only(self): + code = self.run_main( + "CVE-2025-9230, CVE-2025-9231 (OpenSSL)", "v24.x,vulnerable_code_not_in_execute_path" + ) + self.assertEqual(code, 0) + written = json.loads((self.deps / "4.json").read_text()) + self.assertEqual(written["cve"], ["CVE-2025-9231"]) + self.assertEqual(written["description"], "Title for CVE-2025-9231") + self.assertEqual(written["overview"], "Node.js does not use CMS.") + self.assertEqual(written["reason"], "vulnerable_code_not_in_execute_path") + self.assertIn("entry_file=4.json", self.github_output.read_text()) + self.assertIn("skipped=false", self.github_output.read_text()) + self.assertIn("branch=vex/cve-2025-9231", self.github_output.read_text()) + + def test_skips_when_all_cves_already_recorded(self): + code = self.run_main("CVE-2025-9230 (OpenSSL) found on main", "vulnerable_code_not_in_execute_path") + self.assertEqual(code, 0) + self.assertFalse((self.deps / "4.json").exists()) + self.assertIn("skipped=true", self.github_output.read_text()) + + def test_fails_without_cve_in_title(self): + code = self.run_main("GHSA-abcd (foo) found on main", "component_not_present") + self.assertEqual(code, 1) + self.assertFalse((self.deps / "4.json").exists()) + + def test_fails_with_conflicting_labels(self): + code = self.run_main( + "CVE-2025-9231 (OpenSSL)", "component_not_present,inline_mitigations_already_exist" + ) + self.assertEqual(code, 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/dep_checker/vex_entry.py b/dep_checker/vex_entry.py new file mode 100644 index 0000000..873ad91 --- /dev/null +++ b/dep_checker/vex_entry.py @@ -0,0 +1,236 @@ +"""Generate a nodejs/security-wg `vuln/deps/.json` entry from a closed issue. + +When an issue in this repository is closed with one of the OpenVEX +`not_affected` justification labels, the `vex-entry` workflow runs this script +to produce the JSON file that feeds `vuln/deps/index.json` in +nodejs/security-wg, which in turn feeds the generated `node.openvex.json`. + +The five accepted labels are the OpenVEX justification values, see +https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-justifications +""" + +from argparse import ArgumentParser +from pathlib import Path +from typing import Callable, Dict, List, Optional +from urllib.request import Request, urlopen + +import json +import os +import re +import sys + +JUSTIFICATIONS = ( + "component_not_present", + "vulnerable_code_not_present", + "vulnerable_code_not_in_execute_path", + "vulnerable_code_cannot_be_controlled_by_adversary", + "inline_mitigations_already_exist", +) + +DEFAULT_OVERVIEWS = { + "component_not_present": ( + "The vulnerable component is not shipped with Node.js." + ), + "vulnerable_code_not_present": ( + "The dependency is shipped with Node.js but the vulnerable code is not " + "included in the Node.js build." + ), + "vulnerable_code_not_in_execute_path": ( + "The vulnerable code is present in the bundled dependency but is not in " + "the execution path of Node.js." + ), + "vulnerable_code_cannot_be_controlled_by_adversary": ( + "The vulnerable code is reachable in Node.js but its inputs cannot be " + "controlled by an adversary." + ), + "inline_mitigations_already_exist": ( + "Node.js already includes mitigations that prevent this vulnerability " + "from being exploited." + ), +} + +CVE_RECORD_API = "https://cveawg.mitre.org/api/cve/{cve}" +CVE_PATTERN = re.compile(r"CVE-\d{4}-\d{4,}", re.IGNORECASE) + + +class VexEntryError(Exception): + """Raised when an issue cannot be turned into a VEX entry.""" + + +def parse_cve_ids(text: str) -> List[str]: + """Return the distinct CVE ids found in `text`, uppercased, in order.""" + seen: Dict[str, None] = {} + for match in CVE_PATTERN.findall(text): + seen.setdefault(match.upper(), None) + return list(seen) + + +def justification_from_labels(labels: List[str]) -> str: + """Return the single OpenVEX justification label present in `labels`.""" + found = [label for label in labels if label in JUSTIFICATIONS] + if not found: + raise VexEntryError( + "no VEX justification label found; expected one of: " + + ", ".join(JUSTIFICATIONS) + ) + if len(found) > 1: + raise VexEntryError( + "multiple VEX justification labels found: " + ", ".join(found) + ) + return found[0] + + +def _entry_files(deps_dir: Path) -> Dict[int, Path]: + files = {} + for path in deps_dir.glob("*.json"): + if path.stem.isdigit(): + files[int(path.stem)] = path + return files + + +def existing_cves(deps_dir: Path) -> Dict[str, str]: + """Map every CVE already recorded under `deps_dir` to its file name.""" + result: Dict[str, str] = {} + for _, path in sorted(_entry_files(deps_dir).items()): + data = json.loads(path.read_text()) + for cve in data.get("cve", []): + result[cve.upper()] = path.name + return result + + +def next_entry_number(deps_dir: Path) -> int: + numbers = _entry_files(deps_dir) + return max(numbers) + 1 if numbers else 1 + + +def overview_from_comments(comments: List[dict], closer_login: str) -> Optional[str]: + """Return the most recent comment body written by `closer_login`, if any.""" + for comment in reversed(comments): + if comment.get("user", {}).get("login") == closer_login: + body = comment.get("body", "").strip() + if body: + return body + return None + + +def default_overview(reason: str) -> str: + return DEFAULT_OVERVIEWS[reason] + + +def build_entry( + cves: List[str], description: str, overview: str, ref: str, reason: str +) -> dict: + return { + "cve": cves, + "description": description, + "overview": overview, + "ref": ref, + "reason": reason, + } + + +def description_from_cve_record(record: dict) -> str: + cna = record.get("containers", {}).get("cna", {}) + title = cna.get("title", "").strip() + if title: + return title + for item in cna.get("descriptions", []): + if item.get("lang", "en").startswith("en") and item.get("value"): + return item["value"].strip() + return "" + + +def fetch_cve_record(cve: str) -> dict: + request = Request( + CVE_RECORD_API.format(cve=cve), + headers={"User-Agent": "nodejs-dependency-vuln-assessments"}, + ) + with urlopen(request, timeout=30) as response: + return json.load(response) + + +def fetch_description(cves: List[str], fetch: Optional[Callable[[str], dict]] = None) -> str: + fetch = fetch or fetch_cve_record + parts = [] + for cve in cves: + try: + text = description_from_cve_record(fetch(cve)) + except Exception as error: # network or parse problem, reviewer can fix + print(f"warning: could not fetch {cve}: {error}", file=sys.stderr) + text = "" + parts.append(text or f"{cve} (description unavailable)") + return " ".join(parts) + + +def write_github_output(values: Dict[str, str]) -> None: + output_path = os.environ.get("GITHUB_OUTPUT") + if not output_path: + return + with open(output_path, "a") as output: + for key, value in values.items(): + output.write(f"{key}={value}\n") + + +def main(argv: Optional[List[str]] = None) -> int: + parser = ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--issue-title", required=True) + parser.add_argument("--issue-url", required=True) + parser.add_argument("--labels", required=True, help="comma separated label names") + parser.add_argument("--closed-by", required=True, help="login of the user who closed the issue") + parser.add_argument("--comments-file", required=True, help="JSON array of issue comments from the GitHub API") + parser.add_argument("--deps-dir", required=True, help="path to security-wg/vuln/deps") + args = parser.parse_args(argv) + + deps_dir = Path(args.deps_dir) + labels = [label.strip() for label in args.labels.split(",") if label.strip()] + + try: + reason = justification_from_labels(labels) + except VexEntryError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + + cves = parse_cve_ids(args.issue_title) + if not cves: + print(f"error: no CVE id found in issue title: {args.issue_title!r}", file=sys.stderr) + return 1 + + already = existing_cves(deps_dir) + new_cves = [cve for cve in cves if cve not in already] + skipped = {cve: already[cve] for cve in cves if cve in already} + for cve, filename in skipped.items(): + print(f"skipping {cve}: already recorded in {filename}") + + if not new_cves: + write_github_output({"entry_file": "", "cves": "", "skipped": "true"}) + print("nothing to do: every CVE is already in vuln/deps") + return 0 + + comments = json.loads(Path(args.comments_file).read_text()) + overview = overview_from_comments(comments, args.closed_by) or default_overview(reason) + + entry = build_entry( + cves=new_cves, + description=fetch_description(new_cves), + overview=overview, + ref=args.issue_url, + reason=reason, + ) + + target = deps_dir / f"{next_entry_number(deps_dir)}.json" + target.write_text(json.dumps(entry, indent=4) + "\n") + print(f"wrote {target}") + + write_github_output( + { + "entry_file": target.name, + "cves": " ".join(new_cves), + "branch": f"vex/{new_cves[0].lower()}", + "skipped": "false", + } + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())