diff --git a/README.md b/README.md index a9c453d..78aeaad 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,24 @@ Next step: use least-privilege workflow permissions or document an exception. Machine-readable artifacts preserve the same evidence for CI, dashboards, proof packs, SARIF, and AI review. +## Public Proof Artifacts + +The repository includes sanitized fixture scans so reviewers can inspect real output without running private code: + +| Fixture | Status | Purpose | +| --- | --- | --- | +| `safe-agent` | `approved` | Shows a clean repository passing the configured agent-access checks. | +| `risky-ci-agent` | `blocked` | Shows a privileged pull request workflow with write access being stopped. | +| `risky-mcp-agent` | `needs_review` | Shows an MCP server receiving a credential reference and requiring human review. | + +Start with [the public fixture manifest](docs/demo-artifacts/public-fixture-scans/manifest.json), then inspect the blocked CI [summary](docs/demo-artifacts/public-fixture-scans/risky-ci-agent/summary.md), [permit](docs/demo-artifacts/public-fixture-scans/risky-ci-agent/permit.yaml), and [raw findings](docs/demo-artifacts/public-fixture-scans/risky-ci-agent/raw-findings.json). + +Regenerate them with: + +```bash +uv run python tools/build_public_demo_artifacts.py +``` + ## What It Checks - MCP server configuration and tool boundaries @@ -223,6 +241,7 @@ Developer docs: - `docs-site/` - curated Fumadocs site for developers and AI agents - `AGENTS.md` - repo guidance for AI agents - `llms.txt` - compact AI-readable docs map +- [Public Demo Artifacts](docs-site/content/docs/public-demo-artifacts.mdx) - [AI Analysis Guide](docs/ai-analysis-guide.md) - [Artifact Reference](docs/artifact-reference.md) diff --git a/docs-site/content/docs/index.mdx b/docs-site/content/docs/index.mdx index 865fa95..f2a0dac 100644 --- a/docs-site/content/docs/index.mdx +++ b/docs-site/content/docs/index.mdx @@ -46,5 +46,6 @@ Deep Agent reports explain scanner artifacts. They do not replace scanner eviden ## Related pages - [Quickstart](/docs/quickstart) +- [Public Demo Artifacts](/docs/public-demo-artifacts) - [Agent Permit Model](/docs/agent-permit-model) - [Evidence and Artifacts](/docs/evidence-and-artifacts) diff --git a/docs-site/content/docs/meta.json b/docs-site/content/docs/meta.json index 4961178..742059f 100644 --- a/docs-site/content/docs/meta.json +++ b/docs-site/content/docs/meta.json @@ -5,6 +5,7 @@ "index", "quickstart", "first-scan-walkthrough", + "public-demo-artifacts", "---Core Concepts---", "agent-permit-model", "finding-statuses", diff --git a/docs-site/content/docs/public-demo-artifacts.mdx b/docs-site/content/docs/public-demo-artifacts.mdx new file mode 100644 index 0000000..89c1a59 --- /dev/null +++ b/docs-site/content/docs/public-demo-artifacts.mdx @@ -0,0 +1,94 @@ +--- +title: Public Demo Artifacts +description: Sanitized scan outputs that show PermitGraph working on safe fixture repositories. +--- + +## What this is + +Public demo artifacts are committed examples of real scanner output. + +They come from test fixtures, not private repositories. The generator runs PermitGraph, strips machine-local paths, and publishes selected artifacts under `docs/demo-artifacts/public-fixture-scans/`. + +## When to use it + +Use these artifacts when you need to show what PermitGraph produces before running it on a new repository. + +They are useful for: + +- README and documentation examples +- sales or investor demos +- issue reports about scanner behavior +- AI agents trying to understand artifact shape + +Do not treat them as customer audit evidence. They are fixture scans. + +## Command or example + +Regenerate the examples from the repository root: + +```bash +uv run python tools/build_public_demo_artifacts.py +``` + +The current demo set includes: + +| Fixture | Permit status | What it proves | +| --- | --- | --- | +| `safe-agent` | `approved` | A repository with no configured agent-access risk passes. | +| `risky-ci-agent` | `blocked` | A privileged pull request workflow with write access is stopped. | +| `risky-mcp-agent` | `needs_review` | A local MCP server receiving a credential reference requires human review. | + +## Output to expect + +The generator writes: + +```text +docs/demo-artifacts/public-fixture-scans/manifest.json +docs/demo-artifacts/public-fixture-scans//cli-output.txt +docs/demo-artifacts/public-fixture-scans//summary.md +docs/demo-artifacts/public-fixture-scans//permit.yaml +docs/demo-artifacts/public-fixture-scans//raw-findings.json +docs/demo-artifacts/public-fixture-scans//graph-paths.json +docs/demo-artifacts/public-fixture-scans//controls.json +docs/demo-artifacts/public-fixture-scans//run-metrics.json +``` + +Open the manifest first: + +[Public fixture scan manifest](https://github.com/IntelIP/agent-permit-office/blob/main/docs/demo-artifacts/public-fixture-scans/manifest.json) + +Then compare the blocked CI example: + +- [CLI output](https://github.com/IntelIP/agent-permit-office/blob/main/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/cli-output.txt) +- [Summary](https://github.com/IntelIP/agent-permit-office/blob/main/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/summary.md) +- [Permit](https://github.com/IntelIP/agent-permit-office/blob/main/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/permit.yaml) +- [Raw findings](https://github.com/IntelIP/agent-permit-office/blob/main/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/raw-findings.json) +- [Graph paths](https://github.com/IntelIP/agent-permit-office/blob/main/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/graph-paths.json) + +## How to interpret it + +Read the artifacts in this order: + +1. `cli-output.txt`: fast proof that the command ran and what status it returned. +2. `summary.md`: human-readable decision summary. +3. `permit.yaml`: the allow, forbid, and approval conditions. +4. `raw-findings.json`: exact scanner findings with evidence locations. +5. `graph-paths.json`: source-to-sink paths, such as credential to MCP server. +6. `controls.json`: review controls that must pass before approval. +7. `run-metrics.json`: counts used by dashboards and CI. + +The important pattern: scanner output owns the decision. Deep Agent review may explain these artifacts later, but it should not invent a different permit status. + +## Common mistakes + +- Presenting fixture artifacts as customer evidence. +- Editing the artifacts by hand instead of regenerating them. +- Publishing raw `.agent-permit/` folders from private repos. +- Showing only `summary.md` without `raw-findings.json` and `graph-paths.json`. + +## Related pages + +- [Quickstart](./quickstart) +- [Evidence and Artifacts](./evidence-and-artifacts) +- [Review Findings](./review-findings) +- [Artifact Reference](./artifact-reference) diff --git a/docs/demo-artifacts/public-fixture-scans/README.md b/docs/demo-artifacts/public-fixture-scans/README.md new file mode 100644 index 0000000..1d61e57 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/README.md @@ -0,0 +1,15 @@ +# Public Fixture Scans + +These sanitized artifacts are generated from test fixtures with: + +```bash +uv run python tools/build_public_demo_artifacts.py +``` + +They are safe demo evidence, not customer audit records. + +| Fixture | Status | Findings | Graph paths | Controls | +| --- | --- | ---: | ---: | ---: | +| safe-agent | approved | 0 | 0 | 0 | +| risky-ci-agent | blocked | 4 | 1 | 5 | +| risky-mcp-agent | needs_review | 2 | 1 | 3 | diff --git a/docs/demo-artifacts/public-fixture-scans/manifest.json b/docs/demo-artifacts/public-fixture-scans/manifest.json new file mode 100644 index 0000000..82adc10 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/manifest.json @@ -0,0 +1,37 @@ +{ + "description": "Sanitized scanner outputs generated from repository fixtures. These files show real PermitGraph decisions without publishing private repositories or generated local state.", + "fixtures": [ + { + "artifact_dir": "docs/demo-artifacts/public-fixture-scans/safe-agent", + "controls": 0, + "findings": 0, + "graph_paths": 0, + "id": "safe-agent", + "label": "Approved repository", + "permit_status": "approved", + "why": "No configured agent-access risk matched." + }, + { + "artifact_dir": "docs/demo-artifacts/public-fixture-scans/risky-ci-agent", + "controls": 5, + "findings": 4, + "graph_paths": 1, + "id": "risky-ci-agent", + "label": "Blocked CI automation", + "permit_status": "blocked", + "why": "A pull_request_target workflow grants write permissions and references a token." + }, + { + "artifact_dir": "docs/demo-artifacts/public-fixture-scans/risky-mcp-agent", + "controls": 3, + "findings": 2, + "graph_paths": 1, + "id": "risky-mcp-agent", + "label": "MCP credential review", + "permit_status": "needs_review", + "why": "A local MCP server receives a credential reference and uses an unpinned package command." + } + ], + "generator": "tools/build_public_demo_artifacts.py", + "name": "PermitGraph public fixture scans" +} diff --git a/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/cli-output.txt b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/cli-output.txt new file mode 100644 index 0000000..66d5208 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/cli-output.txt @@ -0,0 +1,22 @@ +Agent Permit Office +Status: scan_complete +Target: tests/fixtures/risky-ci-agent +Run ID: public-demo-risky-ci-agent +Artifacts: tests/fixtures/risky-ci-agent/.agent-permit/runs/public-demo-risky-ci-agent +Files indexed: 5 +High signal files: 2 +Skipped files/dirs: 1 +MCP servers: 0 +Credential refs: 0 +Prompt findings: 0 +CI findings: 4 +Findings: 4 +Graph nodes: 6 +Graph edges: 1 +Graph paths: 1 +Controls: 5 +Permit status: blocked +Summary: tests/fixtures/risky-ci-agent/.agent-permit/runs/public-demo-risky-ci-agent/summary.md +Metrics: tests/fixtures/risky-ci-agent/.agent-permit/runs/public-demo-risky-ci-agent/run-metrics.json +Events: tests/fixtures/risky-ci-agent/.agent-permit/analytics-events.jsonl +Next: review summary.md and risk-report.md diff --git a/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/controls.json b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/controls.json new file mode 100644 index 0000000..0d49d77 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/controls.json @@ -0,0 +1,144 @@ +{ + "controls": [ + { + "evidence": [ + { + "command": null, + "config_key": null, + "context_note": null, + "line_end": 4, + "line_start": 4, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": null, + "redacted_snippet": "pull_request_target:", + "secret_name": null, + "workflow_event": null, + "workflow_job": null + }, + { + "command": null, + "config_key": null, + "context_note": "pull_request_target", + "line_end": 7, + "line_start": 7, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": "write-all", + "redacted_snippet": "permissions: write-all", + "secret_name": null, + "workflow_event": "pull_request_target", + "workflow_job": null + } + ], + "id": "control:ci-least-privilege:finding:ci-pr-target-write-token:.github/workflows/agent.yml:7", + "name": "CI least-privilege workflow control", + "rationale": "Workflow trigger, token, or secret use is privileged.", + "recommendation": "Use trusted PR context and least-privilege workflow permissions.", + "related_finding_ids": [ + "finding:ci-pr-target-write-token:.github/workflows/agent.yml:7" + ], + "related_path_ids": [], + "status": "missing", + "target_id": "workflow:.github/workflows/agent.yml:4:ci-pr-target-write-token" + }, + { + "evidence": [ + { + "command": null, + "config_key": null, + "context_note": "pull_request_target", + "line_end": 4, + "line_start": 4, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": null, + "redacted_snippet": "pull_request_target:", + "secret_name": null, + "workflow_event": "pull_request_target", + "workflow_job": null + } + ], + "id": "control:ci-least-privilege:finding:ci-pull-request-target:.github/workflows/agent.yml:4", + "name": "CI least-privilege workflow control", + "rationale": "Workflow trigger, token, or secret use is privileged.", + "recommendation": "Use trusted PR context and least-privilege workflow permissions.", + "related_finding_ids": [ + "finding:ci-pull-request-target:.github/workflows/agent.yml:4" + ], + "related_path_ids": [], + "status": "missing", + "target_id": "workflow:.github/workflows/agent.yml:4:ci-pull-request-target" + }, + { + "evidence": [ + { + "command": null, + "config_key": null, + "context_note": "events=pull_request_target; job=agent-review", + "line_end": 16, + "line_start": 16, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": null, + "redacted_snippet": "GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}", + "secret_name": "GITHUB_TOKEN", + "workflow_event": "pull_request_target", + "workflow_job": "agent-review" + } + ], + "id": "control:ci-least-privilege:finding:ci-secret-reference:.github/workflows/agent.yml:16", + "name": "CI least-privilege workflow control", + "rationale": "Workflow trigger, token, or secret use is privileged.", + "recommendation": "Use trusted PR context and least-privilege workflow permissions.", + "related_finding_ids": [ + "finding:ci-secret-reference:.github/workflows/agent.yml:16" + ], + "related_path_ids": [], + "status": "missing", + "target_id": "workflow:.github/workflows/agent.yml:16:ci-secret-reference" + }, + { + "evidence": [ + { + "command": null, + "config_key": null, + "context_note": "events=pull_request_target", + "line_end": 7, + "line_start": 7, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": "write-all", + "redacted_snippet": "permissions: write-all", + "secret_name": null, + "workflow_event": "pull_request_target", + "workflow_job": null + } + ], + "id": "control:ci-least-privilege:finding:ci-write-all-permissions:.github/workflows/agent.yml:7", + "name": "CI least-privilege workflow control", + "rationale": "Workflow trigger, token, or secret use is privileged.", + "recommendation": "Use trusted PR context and least-privilege workflow permissions.", + "related_finding_ids": [ + "finding:ci-write-all-permissions:.github/workflows/agent.yml:7" + ], + "related_path_ids": [], + "status": "missing", + "target_id": "workflow:.github/workflows/agent.yml:7:ci-write-all-permissions" + }, + { + "evidence": [], + "id": "control:path-ci-privilege-boundary:path:a87e710d72ed", + "name": "Privileged CI boundary control", + "rationale": "Workflow file defines a privileged CI execution path.", + "recommendation": "Remove write permissions or privileged PR context before running agent workflows.", + "related_finding_ids": [], + "related_path_ids": [ + "path:a87e710d72ed" + ], + "status": "missing", + "target_id": "workflow:.github/workflows/agent.yml" + } + ], + "scan_run_id": "public-demo-risky-ci-agent" +} diff --git a/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/graph-paths.json b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/graph-paths.json new file mode 100644 index 0000000..9251858 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/graph-paths.json @@ -0,0 +1,44 @@ +{ + "paths": [ + { + "edge_ids": [ + "edge:file:.github/workflows/agent.yml:runs_in:workflow:.github/workflows/agent.yml" + ], + "id": "path:a87e710d72ed", + "node_ids": [ + "file:.github/workflows/agent.yml", + "workflow:.github/workflows/agent.yml" + ], + "rationale": "Workflow file defines a privileged CI execution context.", + "severity": "high", + "sink_category": "privileged_ci_workflow", + "sink_id": "workflow:.github/workflows/agent.yml", + "source_category": "workflow_file", + "source_id": "file:.github/workflows/agent.yml" + } + ], + "scan_run_id": "public-demo-risky-ci-agent", + "taxonomy": [ + { + "category": "workflow_file", + "label": ".github/workflows/agent.yml", + "node_id": "file:.github/workflows/agent.yml", + "rationale": "Workflow file controls CI execution context.", + "role": "source" + }, + { + "category": "instruction_file", + "label": "AGENTS.md", + "node_id": "file:AGENTS.md", + "rationale": "Agent instruction file can influence agent behavior.", + "role": "source" + }, + { + "category": "privileged_ci_workflow", + "label": ".github/workflows/agent.yml", + "node_id": "workflow:.github/workflows/agent.yml", + "rationale": "Workflow has privileged trigger, write, secret, or checkout risk.", + "role": "sink" + } + ] +} diff --git a/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/permit.yaml b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/permit.yaml new file mode 100644 index 0000000..681b92b --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/permit.yaml @@ -0,0 +1,30 @@ +scan_run_id: public-demo-risky-ci-agent +status: blocked +agent_name: repo-agent +discovered_tools: + [] +discovered_credentials: + [] +allowed_actions: + - "read repository files" + - "generate local report" +forbidden_actions: + - "enable agent tool execution" + - "grant credentials to agent or MCP runtime" + - "run agent workflow in privileged CI context" +required_approvals: + - "CI least-privilege workflow control: Use trusted PR context and least-privilege workflow permissions." + - "Privileged CI boundary control: Remove write permissions or privileged PR context before running agent workflows." +conditions: + - "Block agent execution until the workflow uses least-privilege permissions and trusted-code checkout semantics." + - "Remove write permissions or privileged PR context before running agent workflows." + - "Replace write-all with least-privilege permissions scoped to the job that needs them." + - "Use pull_request for untrusted code, or tightly gate all jobs that run under pull_request_target." + - "Use trusted PR context and least-privilege workflow permissions." + - "Verify the workflow only exposes secrets to trusted events and least-privilege jobs." + - "Workflow file defines a privileged CI execution context." +findings_summary: + critical: 1 + high: 2 + medium: 1 +evidence_bundle_path: tests/fixtures/risky-ci-agent/.agent-permit/runs/public-demo-risky-ci-agent diff --git a/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/raw-findings.json b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/raw-findings.json new file mode 100644 index 0000000..cb17c69 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/raw-findings.json @@ -0,0 +1,139 @@ +{ + "findings": [ + { + "category": "runtime_policy", + "confidence": "high", + "evidence": [ + { + "command": null, + "config_key": null, + "context_note": "pull_request_target", + "line_end": 4, + "line_start": 4, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": null, + "redacted_snippet": "pull_request_target:", + "secret_name": null, + "workflow_event": "pull_request_target", + "workflow_job": null + } + ], + "id": "finding:ci-pull-request-target:.github/workflows/agent.yml:4", + "recommendation": "Use pull_request for untrusted code, or tightly gate all jobs that run under pull_request_target.", + "requires_human_review": true, + "risk": "pull_request_target runs in the base repository context and can expose privileged tokens or secrets to automation that handles PRs.", + "rule_id": "ci-pull-request-target", + "severity": "high", + "source_fact_ids": [ + "workflow:.github/workflows/agent.yml:4:ci-pull-request-target" + ], + "title": "Workflow uses pull_request_target" + }, + { + "category": "runtime_policy", + "confidence": "high", + "evidence": [ + { + "command": null, + "config_key": null, + "context_note": "events=pull_request_target", + "line_end": 7, + "line_start": 7, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": "write-all", + "redacted_snippet": "permissions: write-all", + "secret_name": null, + "workflow_event": "pull_request_target", + "workflow_job": null + } + ], + "id": "finding:ci-write-all-permissions:.github/workflows/agent.yml:7", + "recommendation": "Replace write-all with least-privilege permissions scoped to the job that needs them.", + "requires_human_review": true, + "risk": "write-all grants broad repository mutation capability to workflow jobs.", + "rule_id": "ci-write-all-permissions", + "severity": "high", + "source_fact_ids": [ + "workflow:.github/workflows/agent.yml:7:ci-write-all-permissions" + ], + "title": "Workflow grants write-all permissions" + }, + { + "category": "credential_scope", + "confidence": "high", + "evidence": [ + { + "command": null, + "config_key": null, + "context_note": "events=pull_request_target; job=agent-review", + "line_end": 16, + "line_start": 16, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": null, + "redacted_snippet": "GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}", + "secret_name": "GITHUB_TOKEN", + "workflow_event": "pull_request_target", + "workflow_job": "agent-review" + } + ], + "id": "finding:ci-secret-reference:.github/workflows/agent.yml:16", + "recommendation": "Verify the workflow only exposes secrets to trusted events and least-privilege jobs.", + "requires_human_review": true, + "risk": "Workflow job references repository secret GITHUB_TOKEN, so event and permission controls determine whether automation can use it.", + "rule_id": "ci-secret-reference", + "severity": "medium", + "source_fact_ids": [ + "workflow:.github/workflows/agent.yml:16:ci-secret-reference" + ], + "title": "Workflow references repository secrets" + }, + { + "category": "runtime_policy", + "confidence": "high", + "evidence": [ + { + "command": null, + "config_key": null, + "context_note": null, + "line_end": 4, + "line_start": 4, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": null, + "redacted_snippet": "pull_request_target:", + "secret_name": null, + "workflow_event": null, + "workflow_job": null + }, + { + "command": null, + "config_key": null, + "context_note": "pull_request_target", + "line_end": 7, + "line_start": 7, + "package": null, + "path": ".github/workflows/agent.yml", + "permission_scope": "write-all", + "redacted_snippet": "permissions: write-all", + "secret_name": null, + "workflow_event": "pull_request_target", + "workflow_job": null + } + ], + "id": "finding:ci-pr-target-write-token:.github/workflows/agent.yml:7", + "recommendation": "Block agent execution until the workflow uses least-privilege permissions and trusted-code checkout semantics.", + "requires_human_review": true, + "risk": "A pull_request_target workflow with write permissions can run automation in a privileged repository context while processing PRs.", + "rule_id": "ci-pr-target-write-token", + "severity": "critical", + "source_fact_ids": [ + "workflow:.github/workflows/agent.yml:4:ci-pr-target-write-token" + ], + "title": "PR-target workflow has write token permissions" + } + ], + "scan_run_id": "public-demo-risky-ci-agent" +} diff --git a/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/risk-report.md b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/risk-report.md new file mode 100644 index 0000000..6fc07e7 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/risk-report.md @@ -0,0 +1,20 @@ +# Agent Permit Office Risk Report + +Status: blocked +Credentials: none +Findings: 4 +Graph paths: 1 +Controls: 5 + +## Top Findings +- [critical] ci-pr-target-write-token: PR-target workflow has write token permissions (.github/workflows/agent.yml:4) +- [high] ci-pull-request-target: Workflow uses pull_request_target (.github/workflows/agent.yml:4 (event=pull_request_target)) +- [high] ci-write-all-permissions: Workflow grants write-all permissions (.github/workflows/agent.yml:7 (event=pull_request_target, scope=write-all)) +- [medium] ci-secret-reference: Workflow references repository secrets (.github/workflows/agent.yml:16 (event=pull_request_target, job=agent-review, secret=GITHUB_TOKEN)) + +## Required Approvals +- CI least-privilege workflow control: Use trusted PR context and least-privilege workflow permissions. +- Privileged CI boundary control: Remove write permissions or privileged PR context before running agent workflows. + +## Top Paths +- [high] workflow_file -> privileged_ci_workflow: Workflow file defines a privileged CI execution context. diff --git a/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/run-metrics.json b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/run-metrics.json new file mode 100644 index 0000000..21dda44 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/run-metrics.json @@ -0,0 +1,61 @@ +{ + "aggregate_mismatches": 0, + "available_artifacts": [ + "agent-bom.json", + "codebase-map.json", + "controls.json", + "file-inventory.json", + "graph-paths.json", + "permit.yaml", + "raw-findings.json", + "risk-report.md", + "scan-input.json", + "scan-run.json", + "summary.md" + ], + "cache_hit_ratio": null, + "cache_write_tokens": 0, + "cached_tokens": 0, + "citation_check_status": "not_applicable", + "citation_supported": null, + "controls": 5, + "credentials": 0, + "files_indexed": 5, + "finding_severity_counts": { + "critical": 1, + "high": 2, + "info": 0, + "low": 0, + "medium": 1 + }, + "findings": 4, + "graph_edges": 1, + "graph_nodes": 6, + "graph_paths": 1, + "high_signal_files": 2, + "input_tokens": 0, + "investigation_exit_code": null, + "langsmith": null, + "mcp_servers": 0, + "missing_citation_rule_ids": 0, + "model": null, + "model_calls": 0, + "output_tokens": 0, + "permit_status": "blocked", + "phoenix": null, + "rule_counts": { + "ci-pr-target-write-token": 1, + "ci-pull-request-target": 1, + "ci-secret-reference": 1, + "ci-write-all-permissions": 1 + }, + "run_id": "public-demo-risky-ci-agent", + "run_type": "scan", + "scan_exit_code": null, + "skipped_files": 1, + "status": "completed", + "total_tokens": 0, + "unsupported_citations": 0, + "unsupported_rule_ids": 0, + "version": 1 +} diff --git a/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/summary.md b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/summary.md new file mode 100644 index 0000000..b5934b1 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-ci-agent/summary.md @@ -0,0 +1,30 @@ +# Agent Permit Office Summary + +Status: blocked +Findings: 4 +Graph paths: 1 +Controls: 5 +Credentials: none + +## Top Findings +- [critical] ci-pr-target-write-token at .github/workflows/agent.yml:4 +- [high] ci-pull-request-target at .github/workflows/agent.yml:4 (event=pull_request_target) +- [high] ci-write-all-permissions at .github/workflows/agent.yml:7 (event=pull_request_target, scope=write-all) +- [medium] ci-secret-reference at .github/workflows/agent.yml:16 (event=pull_request_target, job=agent-review, secret=GITHUB_TOKEN) + +## CI Workflow Groups +- `.github/workflows/agent.yml` / `agent-review` + - Rules: ci-secret-reference + - Secret refs: GITHUB_TOKEN +- `.github/workflows/agent.yml` / `workflow` + - Rules: ci-pr-target-write-token, ci-pull-request-target, ci-write-all-permissions + - Write scopes: write-all + +## Artifacts +- permit.yaml +- risk-report.md +- raw-findings.json +- agent-bom.json +- codebase-map.json +- graph-paths.json +- controls.json diff --git a/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/cli-output.txt b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/cli-output.txt new file mode 100644 index 0000000..60c5841 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/cli-output.txt @@ -0,0 +1,22 @@ +Agent Permit Office +Status: scan_complete +Target: tests/fixtures/risky-mcp-agent +Run ID: public-demo-risky-mcp-agent +Artifacts: tests/fixtures/risky-mcp-agent/.agent-permit/runs/public-demo-risky-mcp-agent +Files indexed: 5 +High signal files: 2 +Skipped files/dirs: 1 +MCP servers: 1 +Credential refs: 1 +Prompt findings: 0 +CI findings: 0 +Findings: 2 +Graph nodes: 7 +Graph edges: 2 +Graph paths: 1 +Controls: 3 +Permit status: needs_review +Summary: tests/fixtures/risky-mcp-agent/.agent-permit/runs/public-demo-risky-mcp-agent/summary.md +Metrics: tests/fixtures/risky-mcp-agent/.agent-permit/runs/public-demo-risky-mcp-agent/run-metrics.json +Events: tests/fixtures/risky-mcp-agent/.agent-permit/analytics-events.jsonl +Next: review summary.md and risk-report.md diff --git a/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/controls.json b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/controls.json new file mode 100644 index 0000000..f5f7e94 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/controls.json @@ -0,0 +1,74 @@ +{ + "controls": [ + { + "evidence": [ + { + "command": null, + "config_key": "mcpServers.github-tools.env", + "context_note": null, + "line_end": null, + "line_start": 3, + "package": null, + "path": ".mcp.json", + "permission_scope": null, + "redacted_snippet": "{\"command\": \"npx\", \"env\": [\"GITHUB_TOKEN\"], \"server\": \"github-tools\"}", + "secret_name": null, + "workflow_event": null, + "workflow_job": null + } + ], + "id": "control:mcp-credential-approval:finding:mcp-stdio-credential-ref:.mcp.json:github-tools", + "name": "MCP credential approval gate", + "rationale": "Credential reference is passed to a local MCP runtime.", + "recommendation": "Require human approval and least-privilege credential scope.", + "related_finding_ids": [ + "finding:mcp-stdio-credential-ref:.mcp.json:github-tools" + ], + "related_path_ids": [], + "status": "missing", + "target_id": "mcp-server:.mcp.json:github-tools" + }, + { + "evidence": [ + { + "command": "npx", + "config_key": null, + "context_note": null, + "line_end": null, + "line_start": 3, + "package": "github-mcp-server", + "path": ".mcp.json", + "permission_scope": null, + "redacted_snippet": "{\"command\": \"npx\", \"env\": [\"GITHUB_TOKEN\"], \"server\": \"github-tools\"}", + "secret_name": null, + "workflow_event": null, + "workflow_job": null + } + ], + "id": "control:mcp-package-pinning:finding:mcp-unpinned-package-command:.mcp.json:github-tools", + "name": "MCP package version pinning", + "rationale": "MCP server package is not pinned.", + "recommendation": "Pin MCP package versions before granting credentials.", + "related_finding_ids": [ + "finding:mcp-unpinned-package-command:.mcp.json:github-tools" + ], + "related_path_ids": [], + "status": "missing", + "target_id": "mcp-server:.mcp.json:github-tools" + }, + { + "evidence": [], + "id": "control:path-mcp-credential-boundary:path:961a20858a40", + "name": "Credential-to-MCP boundary control", + "rationale": "Credential can reach an MCP runtime through the graph.", + "recommendation": "Require package pinning, allowlist the MCP server, and use least-privilege credentials.", + "related_finding_ids": [], + "related_path_ids": [ + "path:961a20858a40" + ], + "status": "weak", + "target_id": "mcp-server:.mcp.json:github-tools" + } + ], + "scan_run_id": "public-demo-risky-mcp-agent" +} diff --git a/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/graph-paths.json b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/graph-paths.json new file mode 100644 index 0000000..707883f --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/graph-paths.json @@ -0,0 +1,51 @@ +{ + "paths": [ + { + "edge_ids": [ + "edge:mcp-server:.mcp.json:github-tools:receives_credential:credential-ref:GITHUB_TOKEN" + ], + "id": "path:961a20858a40", + "node_ids": [ + "credential-ref:GITHUB_TOKEN", + "mcp-server:.mcp.json:github-tools" + ], + "rationale": "Credential reference can reach an MCP tool runtime.", + "severity": "high", + "sink_category": "mcp_server", + "sink_id": "mcp-server:.mcp.json:github-tools", + "source_category": "credential", + "source_id": "credential-ref:GITHUB_TOKEN" + } + ], + "scan_run_id": "public-demo-risky-mcp-agent", + "taxonomy": [ + { + "category": "credential", + "label": "GITHUB_TOKEN", + "node_id": "credential-ref:GITHUB_TOKEN", + "rationale": "Credential variable can grant external or privileged access.", + "role": "source" + }, + { + "category": "repo_config", + "label": ".mcp.json", + "node_id": "file:.mcp.json", + "rationale": "MCP config controls external tool/runtime wiring.", + "role": "source" + }, + { + "category": "instruction_file", + "label": "AGENTS.md", + "node_id": "file:AGENTS.md", + "rationale": "Agent instruction file can influence agent behavior.", + "role": "source" + }, + { + "category": "mcp_server", + "label": "github-tools", + "node_id": "mcp-server:.mcp.json:github-tools", + "rationale": "MCP server is a tool runtime sink using stdio transport.", + "role": "sink" + } + ] +} diff --git a/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/permit.yaml b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/permit.yaml new file mode 100644 index 0000000..7b7c559 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/permit.yaml @@ -0,0 +1,27 @@ +scan_run_id: public-demo-risky-mcp-agent +status: needs_review +agent_name: repo-agent +discovered_tools: + - "github-tools" +discovered_credentials: + - "GITHUB_TOKEN" +allowed_actions: + - "read repository files" + - "generate local report" +forbidden_actions: + [] +required_approvals: + - "Credential-to-MCP boundary control: Require package pinning, allowlist the MCP server, and use least-privilege credentials." + - "MCP credential approval gate: Require human approval and least-privilege credential scope." + - "MCP package version pinning: Pin MCP package versions before granting credentials." +conditions: + - "Credential reference can reach an MCP tool runtime." + - "Pin MCP package versions before granting credentials." + - "Pin the MCP package version or vendor the server command before granting credentials or filesystem access." + - "Require human approval and least-privilege credential scope." + - "Require package pinning, allowlist the MCP server, and use least-privilege credentials." + - "Use least-privilege credentials, pin the MCP server package, and require human approval before enabling this server." +findings_summary: + high: 1 + medium: 1 +evidence_bundle_path: tests/fixtures/risky-mcp-agent/.agent-permit/runs/public-demo-risky-mcp-agent diff --git a/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/raw-findings.json b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/raw-findings.json new file mode 100644 index 0000000..c602515 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/raw-findings.json @@ -0,0 +1,65 @@ +{ + "findings": [ + { + "category": "credential_scope", + "confidence": "high", + "evidence": [ + { + "command": null, + "config_key": "mcpServers.github-tools.env", + "context_note": null, + "line_end": null, + "line_start": 3, + "package": null, + "path": ".mcp.json", + "permission_scope": null, + "redacted_snippet": "{\"command\": \"npx\", \"env\": [\"GITHUB_TOKEN\"], \"server\": \"github-tools\"}", + "secret_name": null, + "workflow_event": null, + "workflow_job": null + } + ], + "id": "finding:mcp-stdio-credential-ref:.mcp.json:github-tools", + "recommendation": "Use least-privilege credentials, pin the MCP server package, and require human approval before enabling this server.", + "requires_human_review": true, + "risk": "A locally launched MCP server can receive credential-bearing environment variables before its package, command, and permissions are reviewed.", + "rule_id": "mcp-stdio-credential-ref", + "severity": "high", + "source_fact_ids": [ + "mcp-server:.mcp.json:github-tools" + ], + "title": "Stdio MCP server receives credential references" + }, + { + "category": "supply_chain", + "confidence": "high", + "evidence": [ + { + "command": "npx", + "config_key": null, + "context_note": null, + "line_end": null, + "line_start": 3, + "package": "github-mcp-server", + "path": ".mcp.json", + "permission_scope": null, + "redacted_snippet": "{\"command\": \"npx\", \"env\": [\"GITHUB_TOKEN\"], \"server\": \"github-tools\"}", + "secret_name": null, + "workflow_event": null, + "workflow_job": null + } + ], + "id": "finding:mcp-unpinned-package-command:.mcp.json:github-tools", + "recommendation": "Pin the MCP package version or vendor the server command before granting credentials or filesystem access.", + "requires_human_review": true, + "risk": "Package-runner MCP commands can install a different server version over time, changing tool behavior without code review.", + "rule_id": "mcp-unpinned-package-command", + "severity": "medium", + "source_fact_ids": [ + "mcp-server:.mcp.json:github-tools" + ], + "title": "MCP server package is not version pinned" + } + ], + "scan_run_id": "public-demo-risky-mcp-agent" +} diff --git a/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/risk-report.md b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/risk-report.md new file mode 100644 index 0000000..418fce2 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/risk-report.md @@ -0,0 +1,19 @@ +# Agent Permit Office Risk Report + +Status: needs_review +Credentials: GITHUB_TOKEN +Findings: 2 +Graph paths: 1 +Controls: 3 + +## Top Findings +- [high] mcp-stdio-credential-ref: Stdio MCP server receives credential references (.mcp.json:3) +- [medium] mcp-unpinned-package-command: MCP server package is not version pinned (.mcp.json:3) + +## Required Approvals +- Credential-to-MCP boundary control: Require package pinning, allowlist the MCP server, and use least-privilege credentials. +- MCP credential approval gate: Require human approval and least-privilege credential scope. +- MCP package version pinning: Pin MCP package versions before granting credentials. + +## Top Paths +- [high] credential -> mcp_server: Credential reference can reach an MCP tool runtime. diff --git a/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/run-metrics.json b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/run-metrics.json new file mode 100644 index 0000000..e0a3bc2 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/run-metrics.json @@ -0,0 +1,59 @@ +{ + "aggregate_mismatches": 0, + "available_artifacts": [ + "agent-bom.json", + "codebase-map.json", + "controls.json", + "file-inventory.json", + "graph-paths.json", + "permit.yaml", + "raw-findings.json", + "risk-report.md", + "scan-input.json", + "scan-run.json", + "summary.md" + ], + "cache_hit_ratio": null, + "cache_write_tokens": 0, + "cached_tokens": 0, + "citation_check_status": "not_applicable", + "citation_supported": null, + "controls": 3, + "credentials": 1, + "files_indexed": 5, + "finding_severity_counts": { + "critical": 0, + "high": 1, + "info": 0, + "low": 0, + "medium": 1 + }, + "findings": 2, + "graph_edges": 2, + "graph_nodes": 7, + "graph_paths": 1, + "high_signal_files": 2, + "input_tokens": 0, + "investigation_exit_code": null, + "langsmith": null, + "mcp_servers": 1, + "missing_citation_rule_ids": 0, + "model": null, + "model_calls": 0, + "output_tokens": 0, + "permit_status": "needs_review", + "phoenix": null, + "rule_counts": { + "mcp-stdio-credential-ref": 1, + "mcp-unpinned-package-command": 1 + }, + "run_id": "public-demo-risky-mcp-agent", + "run_type": "scan", + "scan_exit_code": null, + "skipped_files": 1, + "status": "completed", + "total_tokens": 0, + "unsupported_citations": 0, + "unsupported_rule_ids": 0, + "version": 1 +} diff --git a/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/summary.md b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/summary.md new file mode 100644 index 0000000..f3b164d --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/risky-mcp-agent/summary.md @@ -0,0 +1,23 @@ +# Agent Permit Office Summary + +Status: needs_review +Findings: 2 +Graph paths: 1 +Controls: 3 +Credentials: GITHUB_TOKEN + +## Top Findings +- [high] mcp-stdio-credential-ref at .mcp.json:3 +- [medium] mcp-unpinned-package-command at .mcp.json:3 + +## CI Workflow Groups +No CI workflow findings. + +## Artifacts +- permit.yaml +- risk-report.md +- raw-findings.json +- agent-bom.json +- codebase-map.json +- graph-paths.json +- controls.json diff --git a/docs/demo-artifacts/public-fixture-scans/safe-agent/cli-output.txt b/docs/demo-artifacts/public-fixture-scans/safe-agent/cli-output.txt new file mode 100644 index 0000000..8200e7e --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/safe-agent/cli-output.txt @@ -0,0 +1,22 @@ +Agent Permit Office +Status: scan_complete +Target: tests/fixtures/safe-agent +Run ID: public-demo-safe-agent +Artifacts: tests/fixtures/safe-agent/.agent-permit/runs/public-demo-safe-agent +Files indexed: 4 +High signal files: 1 +Skipped files/dirs: 1 +MCP servers: 0 +Credential refs: 0 +Prompt findings: 0 +CI findings: 0 +Findings: 0 +Graph nodes: 4 +Graph edges: 0 +Graph paths: 0 +Controls: 0 +Permit status: approved +Summary: tests/fixtures/safe-agent/.agent-permit/runs/public-demo-safe-agent/summary.md +Metrics: tests/fixtures/safe-agent/.agent-permit/runs/public-demo-safe-agent/run-metrics.json +Events: tests/fixtures/safe-agent/.agent-permit/analytics-events.jsonl +Next: review summary.md and risk-report.md diff --git a/docs/demo-artifacts/public-fixture-scans/safe-agent/controls.json b/docs/demo-artifacts/public-fixture-scans/safe-agent/controls.json new file mode 100644 index 0000000..e3e7498 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/safe-agent/controls.json @@ -0,0 +1,4 @@ +{ + "controls": [], + "scan_run_id": "public-demo-safe-agent" +} diff --git a/docs/demo-artifacts/public-fixture-scans/safe-agent/graph-paths.json b/docs/demo-artifacts/public-fixture-scans/safe-agent/graph-paths.json new file mode 100644 index 0000000..efc3541 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/safe-agent/graph-paths.json @@ -0,0 +1,13 @@ +{ + "paths": [], + "scan_run_id": "public-demo-safe-agent", + "taxonomy": [ + { + "category": "instruction_file", + "label": "AGENTS.md", + "node_id": "file:AGENTS.md", + "rationale": "Agent instruction file can influence agent behavior.", + "role": "source" + } + ] +} diff --git a/docs/demo-artifacts/public-fixture-scans/safe-agent/permit.yaml b/docs/demo-artifacts/public-fixture-scans/safe-agent/permit.yaml new file mode 100644 index 0000000..cfb3d6f --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/safe-agent/permit.yaml @@ -0,0 +1,19 @@ +scan_run_id: public-demo-safe-agent +status: approved +agent_name: repo-agent +discovered_tools: + [] +discovered_credentials: + [] +allowed_actions: + - "read repository files" + - "generate local report" +forbidden_actions: + [] +required_approvals: + [] +conditions: + [] +findings_summary: + {} +evidence_bundle_path: tests/fixtures/safe-agent/.agent-permit/runs/public-demo-safe-agent diff --git a/docs/demo-artifacts/public-fixture-scans/safe-agent/raw-findings.json b/docs/demo-artifacts/public-fixture-scans/safe-agent/raw-findings.json new file mode 100644 index 0000000..035d134 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/safe-agent/raw-findings.json @@ -0,0 +1,4 @@ +{ + "findings": [], + "scan_run_id": "public-demo-safe-agent" +} diff --git a/docs/demo-artifacts/public-fixture-scans/safe-agent/risk-report.md b/docs/demo-artifacts/public-fixture-scans/safe-agent/risk-report.md new file mode 100644 index 0000000..b454946 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/safe-agent/risk-report.md @@ -0,0 +1,19 @@ +# Agent Permit Office Risk Report + +Status: approved +Credentials: none +Findings: 0 +Graph paths: 0 +Controls: 0 + +## Top Findings + +No deterministic findings. + +## Required Approvals + +None. + +## Top Paths + +No risky graph paths. diff --git a/docs/demo-artifacts/public-fixture-scans/safe-agent/run-metrics.json b/docs/demo-artifacts/public-fixture-scans/safe-agent/run-metrics.json new file mode 100644 index 0000000..b32e701 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/safe-agent/run-metrics.json @@ -0,0 +1,56 @@ +{ + "aggregate_mismatches": 0, + "available_artifacts": [ + "agent-bom.json", + "codebase-map.json", + "controls.json", + "file-inventory.json", + "graph-paths.json", + "permit.yaml", + "raw-findings.json", + "risk-report.md", + "scan-input.json", + "scan-run.json", + "summary.md" + ], + "cache_hit_ratio": null, + "cache_write_tokens": 0, + "cached_tokens": 0, + "citation_check_status": "not_applicable", + "citation_supported": null, + "controls": 0, + "credentials": 0, + "files_indexed": 4, + "finding_severity_counts": { + "critical": 0, + "high": 0, + "info": 0, + "low": 0, + "medium": 0 + }, + "findings": 0, + "graph_edges": 0, + "graph_nodes": 4, + "graph_paths": 0, + "high_signal_files": 1, + "input_tokens": 0, + "investigation_exit_code": null, + "langsmith": null, + "mcp_servers": 0, + "missing_citation_rule_ids": 0, + "model": null, + "model_calls": 0, + "output_tokens": 0, + "permit_status": "approved", + "phoenix": null, + "rule_counts": {}, + "run_id": "public-demo-safe-agent", + "run_type": "scan", + "scan_exit_code": null, + "skipped_files": 1, + "status": "completed", + "total_tokens": 0, + "unsupported_citations": 0, + "unsupported_rule_ids": 0, + "version": 1 +} diff --git a/docs/demo-artifacts/public-fixture-scans/safe-agent/summary.md b/docs/demo-artifacts/public-fixture-scans/safe-agent/summary.md new file mode 100644 index 0000000..cd8caa5 --- /dev/null +++ b/docs/demo-artifacts/public-fixture-scans/safe-agent/summary.md @@ -0,0 +1,22 @@ +# Agent Permit Office Summary + +Status: approved +Findings: 0 +Graph paths: 0 +Controls: 0 +Credentials: none + +## Top Findings +No deterministic findings. + +## CI Workflow Groups +No CI workflow findings. + +## Artifacts +- permit.yaml +- risk-report.md +- raw-findings.json +- agent-bom.json +- codebase-map.json +- graph-paths.json +- controls.json diff --git a/llms.txt b/llms.txt index f37344b..4964eed 100644 --- a/llms.txt +++ b/llms.txt @@ -7,6 +7,7 @@ PermitGraph is a local permit gate for AI agents. It scans repositories before a - Introduction: /docs - Quickstart: /docs/quickstart - First Scan Walkthrough: /docs/first-scan-walkthrough +- Public Demo Artifacts: /docs/public-demo-artifacts - Agent Permit Model: /docs/agent-permit-model - Finding Statuses: /docs/finding-statuses - Policy Checks: /docs/policy-checks @@ -35,6 +36,7 @@ PermitGraph is a local permit gate for AI agents. It scans repositories before a - Agent guide: AGENTS.md - Artifact guide: docs/artifact-reference.md - AI analysis guide: docs/ai-analysis-guide.md +- Public fixture scan artifacts: docs/demo-artifacts/public-fixture-scans/manifest.json - README quickstart: README.md ## Core Rule For AI Agents diff --git a/src/agent_permit/cli.py b/src/agent_permit/cli.py index 0f5ce03..28df263 100644 --- a/src/agent_permit/cli.py +++ b/src/agent_permit/cli.py @@ -1010,6 +1010,7 @@ def publish_scan_phase( findings=graph_result.findings, graph_paths=graph_path_report, controls=permit_evaluation.controls, + include_policy_evaluation=policy_evaluation is not None, ) if finding_diff is not None: summary_markdown += "\n" + build_finding_diff_markdown( diff --git a/src/agent_permit/reporting.py b/src/agent_permit/reporting.py index 97496f5..b413753 100644 --- a/src/agent_permit/reporting.py +++ b/src/agent_permit/reporting.py @@ -9,6 +9,7 @@ def build_summary_markdown( findings: list[Finding], graph_paths: GraphPathReport, controls: ControlReport, + include_policy_evaluation: bool = False, ) -> str: lines = [ "# Agent Permit Office Summary", @@ -50,7 +51,7 @@ def build_summary_markdown( lines.append(f" - Secret refs: {', '.join(group['secrets'])}") lines.extend(["", "## Artifacts"]) - for artifact_name in ( + artifact_names = [ "permit.yaml", "risk-report.md", "raw-findings.json", @@ -58,8 +59,10 @@ def build_summary_markdown( "codebase-map.json", "graph-paths.json", "controls.json", - "policy-evaluation.json", - ): + ] + if include_policy_evaluation: + artifact_names.append("policy-evaluation.json") + for artifact_name in artifact_names: lines.append(f"- {artifact_name}") return "\n".join(lines) + "\n" diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 979ad19..a7beecf 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -453,6 +453,7 @@ def test_scan_command_creates_run_artifacts(tmp_path) -> None: assert "`.github/workflows/agent.yml` / `workflow`" in summary_text assert "Rules: ci-pr-target-write-token" in summary_text assert "raw-findings.json" in summary_text + assert "policy-evaluation.json" not in summary_text assert len(raw_findings["findings"]) == 6 assert { finding["rule_id"] for finding in raw_findings["findings"] diff --git a/tests/unit/test_policy.py b/tests/unit/test_policy.py index cdf9918..e08ee36 100644 --- a/tests/unit/test_policy.py +++ b/tests/unit/test_policy.py @@ -45,6 +45,7 @@ def test_scan_auto_loads_default_policy_for_trusted_workflow_permission(tmp_path artifact_dir = tmp_path / ".agent-permit" / "runs" / "policy-ci" raw_findings = json.loads((artifact_dir / "raw-findings.json").read_text()) policy_eval = json.loads((artifact_dir / "policy-evaluation.json").read_text()) + summary_text = (artifact_dir / "summary.md").read_text() assert exit_code == 0 assert stderr.getvalue() == "" assert "Permit status: approved_with_conditions" in stdout.getvalue() @@ -52,6 +53,7 @@ def test_scan_auto_loads_default_policy_for_trusted_workflow_permission(tmp_path assert raw_findings["findings"][0]["severity"] == "low" assert raw_findings["findings"][0]["requires_human_review"] is False assert policy_eval["adjustments"][0]["action"] == "trusted_workflow_permission" + assert "policy-evaluation.json" in summary_text def test_scan_policy_allows_named_mcp_server_but_keeps_condition(tmp_path) -> None: diff --git a/tools/build_public_demo_artifacts.py b/tools/build_public_demo_artifacts.py new file mode 100644 index 0000000..df4106e --- /dev/null +++ b/tools/build_public_demo_artifacts.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +from contextlib import redirect_stderr +from io import StringIO +import json +from pathlib import Path +import shutil +import tempfile +from typing import Any + +from agent_permit.cli import run_scan + + +REPO_ROOT = Path(__file__).resolve().parents[1] +FIXTURE_ROOT = REPO_ROOT / "tests" / "fixtures" +OUTPUT_ROOT = REPO_ROOT / "docs" / "demo-artifacts" / "public-fixture-scans" +SELECTED_ARTIFACTS = ( + "summary.md", + "risk-report.md", + "permit.yaml", + "raw-findings.json", + "graph-paths.json", + "controls.json", + "run-metrics.json", +) +FIXTURES = ( + { + "id": "safe-agent", + "label": "Approved repository", + "why": "No configured agent-access risk matched.", + }, + { + "id": "risky-ci-agent", + "label": "Blocked CI automation", + "why": "A pull_request_target workflow grants write permissions and references a token.", + }, + { + "id": "risky-mcp-agent", + "label": "MCP credential review", + "why": "A local MCP server receives a credential reference and uses an unpinned package command.", + }, +) + + +def main() -> int: + if OUTPUT_ROOT.exists(): + shutil.rmtree(OUTPUT_ROOT) + OUTPUT_ROOT.mkdir(parents=True) + + manifest: dict[str, Any] = { + "name": "PermitGraph public fixture scans", + "description": ( + "Sanitized scanner outputs generated from repository fixtures. " + "These files show real PermitGraph decisions without publishing " + "private repositories or generated local state." + ), + "generator": "tools/build_public_demo_artifacts.py", + "fixtures": [], + } + + with tempfile.TemporaryDirectory(prefix="permitgraph-public-demo-") as tmp: + temp_root = Path(tmp) + for fixture in FIXTURES: + fixture_id = fixture["id"] + source = FIXTURE_ROOT / fixture_id + target = temp_root / fixture_id + shutil.copytree( + source, + target, + ignore=shutil.ignore_patterns(".agent-permit", "__pycache__"), + ) + + run_id = f"public-demo-{fixture_id}" + stdout = StringIO() + stderr = StringIO() + with redirect_stderr(stderr): + exit_code = run_scan( + target, + run_id=run_id, + stdout=stdout, + stderr=stderr, + ) + if exit_code != 0: + raise RuntimeError( + f"demo scan failed for {fixture_id}: {stderr.getvalue()}" + ) + + artifact_dir = target / ".agent-permit" / "runs" / run_id + public_dir = OUTPUT_ROOT / fixture_id + public_dir.mkdir() + + replacements = { + str(target): f"tests/fixtures/{fixture_id}", + str(target.resolve()): f"tests/fixtures/{fixture_id}", + str(artifact_dir): ( + f"tests/fixtures/{fixture_id}/.agent-permit/runs/{run_id}" + ), + str(artifact_dir.resolve()): ( + f"tests/fixtures/{fixture_id}/.agent-permit/runs/{run_id}" + ), + str(temp_root): "tests/fixtures", + str(temp_root.resolve()): "tests/fixtures", + } + + (public_dir / "cli-output.txt").write_text( + _sanitize_text(stdout.getvalue(), replacements), + encoding="utf-8", + ) + for artifact_name in SELECTED_ARTIFACTS: + src = artifact_dir / artifact_name + if artifact_name.endswith(".json"): + _write_sanitized_json(src, public_dir / artifact_name, replacements) + else: + (public_dir / artifact_name).write_text( + _sanitize_text(src.read_text(encoding="utf-8"), replacements), + encoding="utf-8", + ) + + metrics = json.loads((public_dir / "run-metrics.json").read_text()) + manifest["fixtures"].append( + { + "id": fixture_id, + "label": fixture["label"], + "why": fixture["why"], + "permit_status": metrics["permit_status"], + "findings": metrics["findings"], + "graph_paths": metrics["graph_paths"], + "controls": metrics["controls"], + "artifact_dir": str(public_dir.relative_to(REPO_ROOT)), + } + ) + + (OUTPUT_ROOT / "manifest.json").write_text( + json.dumps(manifest, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + (OUTPUT_ROOT / "README.md").write_text( + _build_readme(manifest), + encoding="utf-8", + ) + print(f"Wrote public demo artifacts: {OUTPUT_ROOT}") + return 0 + + +def _write_sanitized_json( + source: Path, + destination: Path, + replacements: dict[str, str], +) -> None: + payload = json.loads(source.read_text(encoding="utf-8")) + payload = _strip_volatile(_sanitize_json(payload, replacements)) + destination.write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def _sanitize_json(value: Any, replacements: dict[str, str]) -> Any: + if isinstance(value, str): + return _sanitize_text(value, replacements) + if isinstance(value, list): + return [_sanitize_json(item, replacements) for item in value] + if isinstance(value, dict): + return { + str(key): _sanitize_json(item, replacements) + for key, item in value.items() + } + return value + + +def _sanitize_text(value: str, replacements: dict[str, str]) -> str: + sanitized = value + for source, target in sorted(replacements.items(), key=lambda item: -len(item[0])): + sanitized = sanitized.replace(source, target) + return sanitized + + +def _strip_volatile(value: Any) -> Any: + if isinstance(value, list): + return [_strip_volatile(item) for item in value] + if isinstance(value, dict): + stripped = {} + for key, item in value.items(): + if key in {"duration_ms", "generated_at", "target_hash"}: + continue + stripped[key] = _strip_volatile(item) + return stripped + return value + + +def _build_readme(manifest: dict[str, Any]) -> str: + lines = [ + "# Public Fixture Scans", + "", + "These sanitized artifacts are generated from test fixtures with:", + "", + "```bash", + "uv run python tools/build_public_demo_artifacts.py", + "```", + "", + "They are safe demo evidence, not customer audit records.", + "", + "| Fixture | Status | Findings | Graph paths | Controls |", + "| --- | --- | ---: | ---: | ---: |", + ] + for fixture in manifest["fixtures"]: + lines.append( + "| {id} | {permit_status} | {findings} | {graph_paths} | {controls} |".format( + **fixture, + ) + ) + lines.append("") + return "\n".join(lines) + + +if __name__ == "__main__": + raise SystemExit(main())