Answer architecture round 3: 0 🔴, 2 🟡; a plan-only change runs the test that pins the plan's shape, and a module record carries its own path
This commit is contained in:
@@ -30,7 +30,7 @@ jobs:
|
|||||||
else
|
else
|
||||||
base='${{ github.event.before }}'
|
base='${{ github.event.before }}'
|
||||||
fi
|
fi
|
||||||
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data release-tooling testdata README.md; then
|
if git diff --quiet "$base"...HEAD -- '*.go' go.mod go.sum Dockerfile .dockerignore .github data release-tooling testdata README.md todo.md; then
|
||||||
echo "code=false" >> "$GITHUB_OUTPUT"
|
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|||||||
@@ -20,15 +20,21 @@ GOAL = "1.1"
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Module:
|
class Module:
|
||||||
|
path: str
|
||||||
found_in: str
|
found_in: str
|
||||||
fixed: list[str] = field(default_factory=list)
|
fixed: list[str] = field(default_factory=list)
|
||||||
osvs: dict[str, dict] = field(default_factory=dict)
|
osvs: dict[str, dict] = field(default_factory=dict)
|
||||||
|
|
||||||
def name(self, path: str) -> str:
|
@property
|
||||||
return "Go" if path == "stdlib" else f"`{path}`"
|
def name(self) -> str:
|
||||||
|
return "Go" if self.path == "stdlib" else f"`{self.path}`"
|
||||||
|
|
||||||
def version(self, path: str, version: str) -> str:
|
def version(self, version: str) -> str:
|
||||||
return version.removeprefix("v") if path == "stdlib" else version
|
return version.removeprefix("v") if self.path == "stdlib" else version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def carried(self) -> str:
|
||||||
|
return f"{self.name} {self.version(self.found_in)}"
|
||||||
|
|
||||||
|
|
||||||
def messages(report: str):
|
def messages(report: str):
|
||||||
@@ -41,19 +47,20 @@ def messages(report: str):
|
|||||||
yield message
|
yield message
|
||||||
|
|
||||||
|
|
||||||
def vulnerable_modules(report: str) -> dict[str, Module]:
|
def vulnerable_modules(report: str) -> list[Module]:
|
||||||
"""Each module govulncheck found a vulnerability in, by module path, stdlib being Go."""
|
"""Each module govulncheck found a vulnerability in, Go (stdlib) first, then by path."""
|
||||||
osvs, modules = {}, {}
|
osvs, modules = {}, {}
|
||||||
for message in messages(report):
|
for message in messages(report):
|
||||||
if "osv" in message:
|
if "osv" in message:
|
||||||
osvs[message["osv"]["id"]] = message["osv"]
|
osv = message["osv"]
|
||||||
|
osvs[osv["id"]] = {"id": osv["id"], "aliases": osv.get("aliases", []), "summary": osv.get("summary", "")}
|
||||||
elif "finding" in message:
|
elif "finding" in message:
|
||||||
finding = message["finding"]
|
finding = message["finding"]
|
||||||
frame = finding["trace"][0]
|
frame = finding["trace"][0]
|
||||||
module = modules.setdefault(frame["module"], Module(frame["version"]))
|
module = modules.setdefault(frame["module"], Module(frame["module"], frame["version"]))
|
||||||
module.osvs[finding["osv"]] = osvs[finding["osv"]]
|
module.osvs[finding["osv"]] = osvs[finding["osv"]]
|
||||||
module.fixed.append(finding.get("fixed_version", ""))
|
module.fixed.append(finding.get("fixed_version", ""))
|
||||||
return modules
|
return sorted(modules.values(), key=lambda m: (m.path != "stdlib", m.path))
|
||||||
|
|
||||||
|
|
||||||
def version_key(version: str):
|
def version_key(version: str):
|
||||||
@@ -85,42 +92,39 @@ def score(todo: str) -> str:
|
|||||||
return f"{evaluate(ast.parse(formula, mode='eval').body, values):.1f}"
|
return f"{evaluate(ast.parse(formula, mode='eval').body, values):.1f}"
|
||||||
|
|
||||||
|
|
||||||
def tagline(path: str, module: Module) -> str:
|
def tagline(module: Module) -> str:
|
||||||
carried = f"{module.name(path)} {module.version(path, module.found_in)}"
|
|
||||||
if "" in module.fixed:
|
if "" in module.fixed:
|
||||||
return f"Ship no release while fejkdata carries {carried}, some of whose known vulnerabilities no version fixes yet."
|
return f"Ship no release while fejkdata carries {module.carried}, some of whose known vulnerabilities no version fixes yet."
|
||||||
fixed = module.version(path, max(module.fixed, key=version_key))
|
fixed = module.version(max(module.fixed, key=version_key))
|
||||||
if path == "stdlib":
|
if module.path == "stdlib":
|
||||||
return f"Raise Go to {fixed}, so the release binaries carry none of {carried}'s known vulnerabilities."
|
return f"Raise Go to {fixed}, so the release binaries carry none of {module.carried}'s known vulnerabilities."
|
||||||
return f"Raise {module.name(path)} to {fixed}, so fejkdata requires no version of it with a known vulnerability."
|
return f"Raise {module.name} to {fixed}, so fejkdata requires no version of it with a known vulnerability."
|
||||||
|
|
||||||
|
|
||||||
def detail(path: str, module: Module) -> str:
|
def detail(module: Module) -> str:
|
||||||
lines = []
|
lines = []
|
||||||
for osv in sorted(module.osvs.values(), key=lambda o: o["id"]):
|
for osv in sorted(module.osvs.values(), key=lambda o: o["id"]):
|
||||||
aliases = f" ({', '.join(osv['aliases'])})" if osv["aliases"] else ""
|
aliases = f" ({', '.join(osv['aliases'])})" if osv["aliases"] else ""
|
||||||
lines.append(f"- {osv['id']}{aliases}: {osv['summary']}")
|
lines.append(f"- {osv['id']}{aliases}: {osv['summary']}")
|
||||||
carried = f"{module.name(path)} {module.version(path, module.found_in)}"
|
return f"govulncheck found these in {module.carried}:\n\n" + "\n".join(lines) + "\n"
|
||||||
return f"govulncheck found these in {carried}:\n\n" + "\n".join(lines) + "\n"
|
|
||||||
|
|
||||||
|
|
||||||
def file_items(modules: dict[str, Module], todo: str) -> tuple[str, list[int]]:
|
def file_items(modules: list[Module], todo: str) -> tuple[str, list[int]]:
|
||||||
"""Return todo.md with an item for each module whose vulnerabilities it does not yet name, and the items' IDs."""
|
"""Return todo.md with an item for each module whose vulnerabilities it does not yet name, and the items' IDs."""
|
||||||
filed = []
|
filed = []
|
||||||
for path in sorted(modules, key=lambda m: (m != "stdlib", m)):
|
for module in modules:
|
||||||
module = modules[path]
|
|
||||||
if all(osv in todo for osv in module.osvs):
|
if all(osv in todo for osv in module.osvs):
|
||||||
continue
|
continue
|
||||||
first = ROW.search(todo)
|
first = ROW.search(todo)
|
||||||
if first is None:
|
if first is None:
|
||||||
raise ValueError("todo.md has no item to take the next release from")
|
raise ValueError("todo.md has no item to take the next release from")
|
||||||
next_id = int(NEXT_ID.search(todo).group(1))
|
next_id = int(NEXT_ID.search(todo).group(1))
|
||||||
line = tagline(path, module)
|
line = tagline(module)
|
||||||
scores = " | ".join(str(SCORES[k]) for k in "RSCAG")
|
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} | {score(todo)} |\n"
|
||||||
todo = todo[: first.start()] + row + todo[first.start() :]
|
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(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(path, module)}\n", 1)
|
todo = todo.replace("## Details\n\n", f"## Details\n\n### {next_id}. {line}\n\n{detail(module)}\n", 1)
|
||||||
filed.append(next_id)
|
filed.append(next_id)
|
||||||
return todo, filed
|
return todo, filed
|
||||||
|
|
||||||
@@ -132,7 +136,7 @@ def main(argv: list[str]) -> int:
|
|||||||
if not modules:
|
if not modules:
|
||||||
print("No known vulnerabilities.")
|
print("No known vulnerabilities.")
|
||||||
return 0
|
return 0
|
||||||
count = sum(len(m.osvs) for m in modules.values())
|
count = sum(len(m.osvs) for m in modules)
|
||||||
known = f"{count} known {'vulnerability' if count == 1 else 'vulnerabilities'}"
|
known = f"{count} known {'vulnerability' if count == 1 else 'vulnerabilities'}"
|
||||||
with open(todo_path, encoding="utf-8") as f:
|
with open(todo_path, encoding="utf-8") as f:
|
||||||
todo, filed = file_items(modules, f.read())
|
todo, filed = file_items(modules, f.read())
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ Why.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
OSVS = (
|
OSVS = (
|
||||||
{"id": "GO-2026-0002", "summary": "Denial of service in net/http", "aliases": ["CVE-2026-2"]},
|
{"id": "GO-2026-0002", "summary": "Denial of service in net/http", "aliases": ["GHSA-2222", "CVE-2026-2"]},
|
||||||
{"id": "GO-2026-0001", "summary": "Cross-site scripting in html/template", "aliases": ["CVE-2026-1"]},
|
{"id": "GO-2026-0001", "summary": "Cross-site scripting in html/template", "aliases": ["CVE-2026-1"]},
|
||||||
{"id": "GO-2026-0003", "summary": "Panic in x/text", "aliases": []},
|
{"id": "GO-2026-0003", "summary": "Panic in x/text"},
|
||||||
)
|
)
|
||||||
STDLIB = (
|
STDLIB = (
|
||||||
("GO-2026-0002", "stdlib", "v1.27.1", "v1.27.2"),
|
("GO-2026-0002", "stdlib", "v1.27.1", "v1.27.2"),
|
||||||
@@ -83,7 +83,7 @@ class FileItems(unittest.TestCase):
|
|||||||
f"### 7. {GO_TAGLINE}\n\n"
|
f"### 7. {GO_TAGLINE}\n\n"
|
||||||
"govulncheck found these in Go 1.27.1:\n\n"
|
"govulncheck found these in Go 1.27.1:\n\n"
|
||||||
"- GO-2026-0001 (CVE-2026-1): Cross-site scripting in html/template\n"
|
"- GO-2026-0001 (CVE-2026-1): Cross-site scripting in html/template\n"
|
||||||
"- GO-2026-0002 (CVE-2026-2): Denial of service in net/http\n\n"
|
"- GO-2026-0002 (GHSA-2222, CVE-2026-2): Denial of service in net/http\n\n"
|
||||||
"### 5. Add a thing."
|
"### 5. Add a thing."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user