diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21a1ff8..92f4179 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: else base='${{ github.event.before }}' fi - if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data release-tooling testdata README.md todo.md; then + if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data release-tooling testdata vulnerability-filing README.md todo.md; then echo "code=false" >> "$GITHUB_OUTPUT" else echo "code=true" >> "$GITHUB_OUTPUT" @@ -82,7 +82,7 @@ jobs: run: docker build --no-cache-filter vulnerabilities --target vulnerability-report --output "$RUNNER_TEMP/vulnerabilities" . - name: File each one in todo.md id: file - run: python3 release-tooling/file_vulnerabilities.py "$RUNNER_TEMP/vulnerabilities/report.json" todo.md + run: python3 vulnerability-filing/file_vulnerabilities.py "$RUNNER_TEMP/vulnerabilities/report.json" todo.md - name: Propose the filed items if: failure() && steps.file.outcome == 'failure' && github.ref == 'refs/heads/main' env: @@ -97,7 +97,7 @@ jobs: git commit -qam "File the known vulnerabilities govulncheck reports" git push -qf "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" vulnerabilities gh pr create --head vulnerabilities --title "File the known vulnerabilities govulncheck reports" \ - --body "govulncheck reports known vulnerabilities in the Go version or a required module, and no release is cut until they are gone. This files them atop the next release in todo.md; each item names the bump that fixes it." + --body "govulncheck reports known vulnerabilities in the Go version or a required module, and no release is cut until they are gone. This files them atop the next release in todo.md, scored 0.0 until rank_todo.py scores them; each item names the bump that fixes it." # A pull request the workflow's token opens starts no workflow. gh workflow run test.yml --ref vulnerabilities diff --git a/Dockerfile b/Dockerfile index 25c6caf..767d5c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,9 +21,13 @@ COPY --from=vulnerabilities /report.json / FROM python:3.14.8-slim AS release-tooling COPY release-tooling/ /release-tooling/ +RUN python3 -m unittest discover -s /release-tooling -p '*_test.py' && touch /tested + +FROM python:3.14.8-slim AS vulnerability-filing +COPY vulnerability-filing/ /vulnerability-filing/ # The filer's test files an item into the plan itself. COPY todo.md /todo.md -RUN python3 -m unittest discover -s /release-tooling -p '*_test.py' && touch /tested +RUN python3 -m unittest discover -s /vulnerability-filing -p '*_test.py' && touch /tested FROM goreleaser/goreleaser:v2.18.3 AS packaging WORKDIR /check @@ -33,6 +37,7 @@ RUN git init -q && git remote add origin https://github.com/larvit/fejkdata.git FROM portable # BuildKit skips a stage nothing copies from. COPY --from=release-tooling /tested /tmp/release-tooling-tested +COPY --from=vulnerability-filing /tested /tmp/vulnerability-filing-tested COPY --from=packaging /tested /tmp/packaging-tested RUN unformatted="$(gofmt -l .)"; test -z "$unformatted" || \ { echo "unformatted files:"; echo "$unformatted"; exit 1; } diff --git a/README.md b/README.md index 9e4340d..907fb28 100644 --- a/README.md +++ b/README.md @@ -1170,8 +1170,9 @@ internal/rows/ a table's rows: the TSV, the options proved over it, the links b data/ the shipped modules, each a Go package embedding its categories (JSON, and a TSV per table) and its manifest; and Modules, which lists them data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md), and the modules they share docs/ the decision log, the register research behind the shipped data, the survey of other libraries, panels' scores and rulings on how readable the code is, and what each chunk cost an agent to read -release-tooling/ the release CI publishes from the changelog heading, and the binaries, packages, image and package repositories built for it; and the filing of known vulnerabilities +release-tooling/ the release CI publishes from the changelog heading, and the binaries, packages, image and package repositories built for it testdata/ the pinned shipped shape (see Versioning) +vulnerability-filing/ the script CI runs to file each known vulnerability govulncheck reports as a `todo.md` item ``` ## Development diff --git a/docs/decisions.md b/docs/decisions.md index 7889b25..ae8a058 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -1001,9 +1001,9 @@ protection does not require. So a vulnerability published in Go, or an outage of vuln.go.dev, holds the release and leaves unrelated pull requests mergeable. Each vulnerability is filed as a `security` item atop the next release in `todo.md`, by a pull request CI opens, so the plan names it before anyone reads a red run. -`release-tooling/file_vulnerabilities.py` writes that item in `todo.md`'s shape and scores it -by `todo.md`'s formula, because CI cannot reach `rank_todo.py`; a test files one into the -repository's own `todo.md`. When the next release has no item left and waits to be cut, +`vulnerability-filing/file_vulnerabilities.py` writes that item in `todo.md`'s shape with a +Score of 0.0, since CI cannot reach `rank_todo.py`, which scores it on the next commit to the +plan; a test files one into the repository's own `todo.md`. When the next release has no item left and waits to be cut, `todo.md` names no version for it, so the item lands in the release after; the maintainer moves it. Where no Go version fixes a vulnerability yet, govulncheck has no ignore list: the maintainer drops the job from the release's `needs` until one does, and records why here. diff --git a/release-tooling/file_vulnerabilities.py b/vulnerability-filing/file_vulnerabilities.py similarity index 70% rename from release-tooling/file_vulnerabilities.py rename to vulnerability-filing/file_vulnerabilities.py index ede4260..37a2c22 100644 --- a/release-tooling/file_vulnerabilities.py +++ b/vulnerability-filing/file_vulnerabilities.py @@ -4,25 +4,24 @@ Usage: file_vulnerabilities.py Exits 1 while any vulnerability is known, filed now or before, and 0 on a clean report. -The item is written the way ~/.claude/bin/rank_todo.py writes one, which CI cannot run: -todo.md's `Next ID = N` gives its ID, the release of the plan's top row (the next release) -gives its release, and its Score is todo.md's `Score = …` formula over the scores below and -the weight todo.md's goal table gives goal 1.1. The test that files into the repository's -own todo.md fails the gate when the plan's shape drifts from what this script reads. +todo.md's `Next ID = N` gives the item's ID, and the release of the plan's top row, the next +release, gives its release. Its Score is 0.0 until rank_todo.py, which CI cannot run, scores +it; a security item ranks first in its release whatever it scores. The test that files into +the repository's own todo.md fails the gate when the plan's shape drifts from what this +script reads. -A security item scores R, S and C 1, since the fix is a version bump, and A and G 8, since a -known vulnerability in the binaries works against README goal 1.1: installing fejkdata the -way one installs other tools, which image scanners then flag. +Scores, as todo.md's table spells them: R (risk), S (size) and C (difficulty) are 1, since +the fix is a version bump; A (value to the audience) and G (value to the goal) are 8, the goal +being README goal 1.1, installing fejkdata the way one installs other tools, which a flagged +binary works against. """ -import ast import json import re import sys from dataclasses import dataclass, field ROW = re.compile(r"^\| (\d+) \| ([^|]+?) \| [^|]*\| \*\*", re.M) -WEIGHT = re.compile(r"^\| ([\d.]+) \| ([\d.]+) \|$", re.M) NEXT_ID = re.compile(r"`Next ID = (\d+)`") SCORES = {"R": 1, "S": 1, "C": 1, "A": 8, "G": 8} GOAL = "1.1" @@ -81,32 +80,6 @@ def version_key(version: str): return [int(part) for part in re.findall(r"\d+", version)] -def evaluate(node, values): - """The value of todo.md's score formula: numbers, letters, unary minus and + - * /.""" - match node: - case ast.Constant(value=value): - return value - case ast.Name(id=name): - return values[name] - case ast.UnaryOp(op=ast.USub(), operand=operand): - return -evaluate(operand, values) - case ast.BinOp(left=left, op=ast.Add(), right=right): - return evaluate(left, values) + evaluate(right, values) - case ast.BinOp(left=left, op=ast.Sub(), right=right): - return evaluate(left, values) - evaluate(right, values) - case ast.BinOp(left=left, op=ast.Mult(), right=right): - return evaluate(left, values) * evaluate(right, values) - case ast.BinOp(left=left, op=ast.Div(), right=right): - return evaluate(left, values) / evaluate(right, values) - raise ValueError(f"todo.md's formula holds {ast.dump(node)}, which this script cannot evaluate") - - -def score(todo: str) -> str: - formula = re.search(r"`Score = (.+?)`", todo).group(1) - values = dict(SCORES, W=float(dict(WEIGHT.findall(todo))[GOAL])) - return f"{evaluate(ast.parse(formula, mode='eval').body, values):.1f}" - - def tagline(module: Module) -> str: if module.unfixed: return f"Ship no release while fejkdata carries {module.carried}, some of whose known vulnerabilities no version fixes yet." @@ -137,7 +110,7 @@ def file_items(modules: list[Module], todo: str) -> tuple[str, list[int]]: next_id = int(NEXT_ID.search(todo).group(1)) line = tagline(module) scores = " | ".join(str(SCORES[k]) for k in "RSCAG") - row = f"| {next_id} | {first.group(2)} | security | **{line}** | {scores} | {GOAL} | {score(todo)} |\n" + row = f"| {next_id} | {first.group(2)} | security | **{line}** | {scores} | {GOAL} | 0.0 |\n" todo = todo[: first.start()] + row + todo[first.start() :] todo = todo.replace(f"`Next ID = {next_id}`", f"`Next ID = {next_id + 1}`", 1) todo = todo.replace("## Details\n\n", f"## Details\n\n### {next_id}. {line}\n\n{detail(module)}\n", 1) diff --git a/release-tooling/file_vulnerabilities_test.py b/vulnerability-filing/file_vulnerabilities_test.py similarity index 98% rename from release-tooling/file_vulnerabilities_test.py rename to vulnerability-filing/file_vulnerabilities_test.py index be5fe99..6e7c5e9 100644 --- a/release-tooling/file_vulnerabilities_test.py +++ b/vulnerability-filing/file_vulnerabilities_test.py @@ -77,7 +77,7 @@ class FileItems(unittest.TestCase): todo, filed = file(TODO, *STDLIB) self.assertEqual(filed, [7]) self.assertIn("`Next ID = 8`", todo) - self.assertEqual(rows(todo)[0], f"| 7 | 0.2.0 | security | **{GO_TAGLINE}** | 1 | 1 | 1 | 8 | 8 | 1.1 | 29.1 |") + self.assertEqual(rows(todo)[0], f"| 7 | 0.2.0 | security | **{GO_TAGLINE}** | 1 | 1 | 1 | 8 | 8 | 1.1 | 0.0 |") self.assertTrue(rows(todo)[1].startswith("| 5 |")) self.assertTrue( todo.split("## Details\n\n", 1)[1].startswith( @@ -133,7 +133,7 @@ class FileItems(unittest.TestCase): row = rows(todo)[0] self.assertEqual(row.count("|"), header.count("|")) self.assertEqual(row.split(" | ")[1], rows(plan)[0].split(" | ")[1]) - self.assertRegex(row, rf"^\| {filed[0]} \| .* \| \d+\.\d \|$") + self.assertRegex(row, rf"^\| {filed[0]} \| .* \| 0\.0 \|$") self.assertIn(f"`Next ID = {filed[0] + 1}`", todo)