diff --git a/CHANGELOG.md b/CHANGELOG.md index 823fd84..a8e1703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ This project follows semantic-versioning guidance once recurring releases are ta ### Documentation-only updates +- Refreshed the README repository-layout tree to cover `docs/`, `examples/policies/`, `examples/schema-adapters/`, all four CI workflow examples, the combined report example, `scripts/`, and `package-skills.sh`, with `tests/test_readme_repository_layout.py` drift guards keeping the tree in sync with disk. - Added `examples/ci/github-actions/agent-security-prompt-sarif.yml` for prompt-injection signal and exposure SARIF uploads. - Added `docs/report-comparison.md` and `examples/ci/github-actions/agent-security-compare-reports.yml` for stored config-risk report comparison. - Added `docs/schema-adapters.md` and Phase 12 regression coverage in `tests/test_phase12_schema_adapters.py` for adapter fixtures, explicit ignored fields, SARIF/Markdown adapter reporting, and cross-platform path serialization. diff --git a/README.md b/README.md index da84c4c..ea3ad18 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,8 @@ Boundary guide: ## Repository layout ```text +docs/ + *.md examples/ high-risk-agent-config.json hardened-agent-config.json @@ -234,12 +236,21 @@ examples/ agent-security-baseline.json config-shapes/ *.json + policies/ + agent-security-policy.json + schema-adapters/ + *.json reports/ high-risk-agent-security-review.md + combined-browser-private-network-boundary.md ci/ github-actions/ agent-security-strict.yml agent-security-sarif.yml + agent-security-prompt-sarif.yml + agent-security-compare-reports.yml +scripts/ + package_skills.py skills/ agent-security/ SKILL.md @@ -257,6 +268,7 @@ tests/ test_*.py .github/workflows/ ci.yml +package-skills.sh ``` ## Prompt-injection fixture corpus diff --git a/tests/test_readme_repository_layout.py b/tests/test_readme_repository_layout.py new file mode 100644 index 0000000..93f03a1 --- /dev/null +++ b/tests/test_readme_repository_layout.py @@ -0,0 +1,95 @@ +"""Drift guards for the README repository-layout tree. + +The README renders an abbreviated repository layout so new users can find the +scanners, examples, workflows, and docs quickly. These tests keep that tree +honest in both directions: + +1. Every concrete path named in the tree must exist on disk, so renamed or + removed files cannot linger as stale documentation. +2. New `examples/ci/github-actions/` workflows must be added to the tree, since + that directory grows with every new downstream-integration example. +3. Core top-level entries users rely on (`skills/`, `docs/`, `scripts/`, + `package-skills.sh`, `.github/workflows/`) must stay listed. + +Abbreviated glob entries such as `*.json`, `test_*.py`, and `*.txt / *.json` +are allowed and skipped by the existence checks so the tree stays compact. +""" + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +README = ROOT / "README.md" +WORKFLOW_DIR = ROOT / "examples" / "ci" / "github-actions" + +REQUIRED_TOP_LEVEL_ENTRIES = { + "examples/", + "skills/", + "tests/", + "scripts/", + "docs/", + "package-skills.sh", + ".github/workflows/", +} + + +def _layout_lines() -> list[str]: + text = README.read_text(encoding="utf-8") + assert "## Repository layout" in text, "README must keep a Repository layout section" + section = text.split("## Repository layout", 1)[1] + parts = section.split("```", 2) + assert len(parts) >= 2, "Repository layout must be a fenced code block" + lines = parts[1].splitlines() + if lines and lines[0].strip() == "text": + lines = lines[1:] + return [line for line in lines if line.strip()] + + +def _layout_paths() -> list[tuple[int, str, str]]: + """Reconstruct (depth, name, full-relative-path) rows from the tree.""" + rows: list[tuple[int, str, str]] = [] + stack: list[str] = [] + for line in _layout_lines(): + assert "\t" not in line, "layout tree must use spaces, not tabs" + indent = len(line) - len(line.lstrip(" ")) + assert indent % 2 == 0, f"layout indentation must be a multiple of two: {line!r}" + depth = indent // 2 + name = line.strip() + del stack[depth:] + rows.append((depth, name, "/".join(stack + [name]))) + if name.endswith("/"): + stack.append(name[:-1]) + return rows + + +def test_layout_entries_exist_on_disk() -> None: + for _depth, name, full in _layout_paths(): + if "*" in name: + continue + path = ROOT / full + if name.endswith("/"): + assert path.is_dir(), f"layout lists missing directory: {full}" + else: + assert path.exists(), f"layout lists missing path: {full}" + + +def test_layout_lists_every_ci_workflow_example() -> None: + listed = {full for _depth, name, full in _layout_paths() if name.endswith(".yml")} + for workflow in sorted(WORKFLOW_DIR.glob("*.yml")): + assert workflow.relative_to(ROOT).as_posix() in listed, ( + f"README layout must list new workflow example: {workflow.name}" + ) + + +def test_layout_lists_core_top_level_entries() -> None: + top_level = {name.rstrip("/") for depth, name, _full in _layout_paths() if depth == 0} + missing = {entry.rstrip("/") for entry in REQUIRED_TOP_LEVEL_ENTRIES} - top_level + assert not missing, f"README layout must list core entries: {sorted(missing)}" + + +def test_layout_names_are_globe_or_plain() -> None: + """Guard against tree rows the parser above cannot reconstruct.""" + for _depth, name, _full in _layout_paths(): + assert re.fullmatch(r"[A-Za-z0-9._/*-]+( / [A-Za-z0-9._/*-]+)*", name), ( + f"unexpected layout entry shape: {name!r}" + )