feat(advisories): add provisional GHSA feed (#242)

* feat(advisories): add provisional ghsa feed

* fix(workflows): include advisory signatures in checksums

* fix(workflows): mirror ghsa feed at release root

* feat(advisories): consolidate ghsa into agent feed

* ci(advisories): consolidate ghsa during nvd poll

* fix(advisories): retain unreplaced ghsa feed entries

* chore(skills): bump advisory feed consumers

* fix(release): resolve ts import closure dry run

* fix(release): preserve urls while stripping comments

* fix(release): ignore skill test-only changes

* fix(advisories): follow ghsa pagination links

* test(advisories): add nvd ghsa pipeline dry run
This commit is contained in:
davida-ps
2026-05-24 21:41:59 +03:00
committed by GitHub
parent 8a9bdfcd23
commit 4dbac421ab
34 changed files with 1944 additions and 81 deletions
@@ -46,6 +46,66 @@ class VerifySkillReleaseImportClosureTests(unittest.TestCase):
self.assertEqual(failures, [])
def test_ts_source_accepts_js_import_specifier(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "types.ts").write_text("export type Value = string;\n", encoding="utf-8")
(root / "main.ts").write_text("import type { Value } from './types.js';\n", encoding="utf-8")
failures = self.module.verify_import_closure(root)
self.assertEqual(failures, [])
def test_comment_import_examples_are_ignored(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "main.ts").write_text(
"/*\n"
" * Example integration:\n"
" * import { Missing } from '../external/project/file';\n"
" */\n"
"export {};\n",
encoding="utf-8",
)
failures = self.module.verify_import_closure(root)
self.assertEqual(failures, [])
def test_url_string_does_not_hide_following_relative_import(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "main.ts").write_text(
'const feedUrl = "https://example.test/feed.json"; import value from "./missing.js";\n',
encoding="utf-8",
)
failures = self.module.verify_import_closure(root)
self.assertEqual(len(failures), 1)
self.assertIn("main.ts imports ./missing.js", failures[0])
def test_remote_import_spec_survives_comment_stripping(self) -> None:
source = 'import remote from "https://example.test/module.mjs";\n'
stripped = self.module.strip_js_ts_comments(source)
specs = [match.group("spec") for match in self.module.IMPORT_RE.finditer(stripped)]
self.assertEqual(specs, ["https://example.test/module.mjs"])
def test_remote_runtime_import_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "main.mjs").write_text(
'import remote from "https://example.test/module.mjs";\n',
encoding="utf-8",
)
failures = self.module.verify_import_closure(root)
self.assertEqual(len(failures), 1)
self.assertIn("remote runtime import https://example.test/module.mjs", failures[0])
if __name__ == "__main__":
unittest.main()
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
"""Verify staged skill release JS/TS relative imports are self-contained.
"""Verify staged skill release JS/TS imports are self-contained.
The skill release workflow builds archives from `skill.json.sbom.files`. If a
runtime helper exists in the repo but is omitted from the SBOM, the staged
release can contain files whose relative imports point at missing files. This
script checks the staged payload, not the source tree, so it catches exactly
what would ship.
release can contain files whose relative imports point at missing files or
remote runtime imports. This script checks the staged payload, not the source
tree, so it catches exactly what would ship.
"""
from __future__ import annotations
@@ -22,18 +22,88 @@ IMPORT_RE = re.compile(
r"|\bimport\s*\(\s*"
r"|\brequire\s*\(\s*"
r")"
r"['\"](?P<spec>\.{1,2}/[^'\"]+)['\"]",
r"['\"](?P<spec>(?:\.{1,2}/|https?://)[^'\"]+)['\"]",
re.MULTILINE,
)
SOURCE_SUFFIXES = {".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"}
RESOLUTION_SUFFIXES = ["", ".mjs", ".js", ".cjs", ".mts", ".ts", ".cts", ".json"]
INDEX_FILENAMES = ["index.mjs", "index.js", "index.cjs", "index.mts", "index.ts", "index.cts", "index.json"]
TS_IMPORTER_SUFFIXES = {".ts", ".mts", ".cts"}
JS_TO_TS_SUFFIX = {".js": ".ts", ".mjs": ".mts", ".cjs": ".cts"}
def strip_js_ts_comments(text: str) -> str:
stripped: list[str] = []
state = "code"
i = 0
while i < len(text):
char = text[i]
next_char = text[i + 1] if i + 1 < len(text) else ""
if state == "line_comment":
if char in "\r\n":
stripped.append(char)
state = "code"
i += 1
continue
if state == "block_comment":
if char == "*" and next_char == "/":
state = "code"
i += 2
continue
if char in "\r\n":
stripped.append(char)
i += 1
continue
if state in {"single", "double", "template"}:
stripped.append(char)
if char == "\\" and i + 1 < len(text):
stripped.append(text[i + 1])
i += 2
continue
if (state == "single" and char == "'") or (state == "double" and char == '"') or (
state == "template" and char == "`"
):
state = "code"
i += 1
continue
if char == "/" and next_char == "/":
stripped.append(" ")
state = "line_comment"
i += 2
continue
if char == "/" and next_char == "*":
stripped.append(" ")
state = "block_comment"
i += 2
continue
stripped.append(char)
if char == "'":
state = "single"
elif char == '"':
state = "double"
elif char == "`":
state = "template"
i += 1
return "".join(stripped)
def is_remote_spec(spec: str) -> bool:
return spec.startswith(("http://", "https://"))
def candidate_paths(importer: Path, spec: str) -> list[Path]:
base = (importer.parent / spec).resolve()
candidates = [base]
if importer.suffix in TS_IMPORTER_SUFFIXES and base.suffix in JS_TO_TS_SUFFIX:
candidates.append(base.with_suffix(JS_TO_TS_SUFFIX[base.suffix]))
candidates.extend(base.with_suffix(suffix) for suffix in RESOLUTION_SUFFIXES if suffix and base.suffix == "")
candidates.extend(base / name for name in INDEX_FILENAMES)
return candidates
@@ -57,13 +127,18 @@ def verify_import_closure(root: Path) -> list[str]:
for source in sorted(p for p in root.rglob("*") if p.is_file() and p.suffix in SOURCE_SUFFIXES):
text = source.read_text(encoding="utf-8", errors="ignore")
text = strip_js_ts_comments(text)
for match in IMPORT_RE.finditer(text):
spec = match.group("spec")
rel_source = source.relative_to(root).as_posix()
if is_remote_spec(spec):
failures.append(f"{rel_source} imports remote runtime import {spec}")
continue
candidates = candidate_paths(source, spec)
if any(is_resolved_file(candidate, root) for candidate in candidates):
continue
rel_source = source.relative_to(root).as_posix()
display_target = (source.parent / spec).resolve()
try:
rel_target = display_target.relative_to(root).as_posix()