|
| 1 | +# Security Policy |
| 2 | + |
| 3 | +## Supported Versions |
| 4 | + |
| 5 | +| Version | Supported | |
| 6 | +|---------|-----------| |
| 7 | +| 1.2.x | ✅ Active (current) | |
| 8 | +| 1.1.x | ✅ Security fixes | |
| 9 | +| 1.0.x | ✅ Security fixes | |
| 10 | +| < 1.0.0 | ❌ Unsupported — upgrade to 1.2.x | |
| 11 | + |
| 12 | +## Reporting a Vulnerability |
| 13 | + |
| 14 | +**This is a security-critical tool.** We take every report seriously. |
| 15 | + |
| 16 | +**Do NOT open a public GitHub issue for security vulnerabilities.** |
| 17 | + |
| 18 | +Instead, report vulnerabilities via: |
| 19 | + |
| 20 | +- **Email**: [noah@invariantsystems.io](mailto:noah@invariantsystems.io) |
| 21 | +- **Subject prefix**: `[VULN] aiir: <brief description>` |
| 22 | + |
| 23 | +### What to include |
| 24 | + |
| 25 | +1. Description of the vulnerability |
| 26 | +2. Steps to reproduce |
| 27 | +3. Affected versions |
| 28 | +4. Severity assessment (if known) |
| 29 | +5. Suggested fix (if any) |
| 30 | + |
| 31 | +### Response timeline |
| 32 | + |
| 33 | +| Stage | Target | |
| 34 | +|-------|--------| |
| 35 | +| Acknowledgment | 24 hours | |
| 36 | +| Initial triage | 48 hours | |
| 37 | +| Fix for Critical/High | 7 days | |
| 38 | +| Fix for Medium/Low | 30 days | |
| 39 | +| Public disclosure | After fix is released | |
| 40 | + |
| 41 | +### Scope |
| 42 | + |
| 43 | +The following are in scope: |
| 44 | + |
| 45 | +- **aiir/cli.py** — public API shell and CLI entry point |
| 46 | +- **aiir/_core.py** — constants, encoding helpers, git operations, hashing |
| 47 | +- **aiir/_detect.py** — AI signal detection and commit metadata parsing |
| 48 | +- **aiir/_receipt.py** — receipt building, generation, formatting, writing |
| 49 | +- **aiir/_verify.py** — receipt content-addressed integrity verification |
| 50 | +- **aiir/_sign.py** — Sigstore signing and verification |
| 51 | +- **aiir/_ledger.py** — append-only JSONL ledger with auto-index |
| 52 | +- **aiir/_stats.py** — badge, stats dashboard, policy checks |
| 53 | +- **aiir/_github.py** — GitHub Actions integration |
| 54 | +- **aiir/mcp_server.py** — MCP server for AI assistants (path-restricted, error-sanitized) |
| 55 | +- **action.yml** — GitHub Actions composite action |
| 56 | +- **Receipt integrity** — content-addressed hashing chain |
| 57 | +- **Output injection** — GitHub Actions output/summary manipulation |
| 58 | +- **Supply chain** — dependency pinning and integrity |
| 59 | + |
| 60 | +### Out of scope |
| 61 | + |
| 62 | +- AI detection bypass (this is a known limitation of heuristic detection — see README) |
| 63 | +- Issues in upstream dependencies (`actions/setup-python`, `actions/upload-artifact`, etc.) |
| 64 | +- Denial of service via extremely large repositories (mitigated by `--max-count` limit) |
| 65 | + |
| 66 | +## Security Design |
| 67 | + |
| 68 | +### Threat model |
| 69 | + |
| 70 | +This tool processes untrusted input from: |
| 71 | +1. **Git commit metadata** — author names, emails, subjects, message bodies |
| 72 | +2. **GitHub Actions inputs** — `commit-range`, `ai-only`, `output-dir` |
| 73 | +3. **Diff content** — full diffs are hashed but not stored in receipts |
| 74 | + |
| 75 | +### Security properties |
| 76 | + |
| 77 | +- **Content-addressed receipts**: The `receipt_id` and `content_hash` are derived from SHA-256 of the canonical JSON receipt core. Modifying any field invalidates both. |
| 78 | +- **Sigstore keyless signing** (optional): Receipts can be cryptographically signed using Sigstore, providing non-repudiation and a public transparency log entry (Rekor). Uses OIDC keyless signing — no key management required. In GitHub Actions, ambient credentials are used automatically when `id-token: write` is set. |
| 79 | +- **NUL-byte delimited parsing**: Git metadata fields are parsed using `%x00` delimiters to prevent field injection via pipes or other characters in author names. |
| 80 | +- **Ref validation**: All user-provided git refs are validated to reject option-like strings (e.g., `--all`), preventing git argument injection. |
| 81 | +- **Heredoc output pattern**: GitHub Actions outputs use the heredoc delimiter pattern to prevent output injection via multiline values. |
| 82 | +- **Pinned dependencies**: All action dependencies are pinned to full commit SHAs, not mutable version tags. |
| 83 | +- **Markdown sanitization**: Commit subjects in step summaries are sanitized to prevent image beacon, phishing link, and HTML injection. |
| 84 | +- **Zero dependencies**: Only Python standard library. No supply chain attack surface from pip packages. |
| 85 | +- **PEP 740 digital attestations**: Every wheel and sdist published to PyPI includes in-toto-style digital attestations (SLSA provenance + PyPI Publish predicates), cryptographically signed via short-lived OIDC identities from Trusted Publishing. No static keys to compromise. |
| 86 | +- **SLSA provenance**: GitHub Actions `attest-build-provenance` generates SLSA provenance attestations for both wheel and sdist, binding each artifact to the specific build invocation. |
| 87 | +- **PyPI Integrity API verification**: Post-publish CI verifies that attestations are retrievable via PyPI's Integrity API (`GET /integrity/aiir/<version>/<file>/provenance`). A standalone verification script (`scripts/verify-pypi-provenance.py`) is provided for consumers. |
| 88 | +- **Trusted Publishing (OIDC)**: PyPI uploads use GitHub's OIDC identity provider — short-lived, scoped tokens instead of long-lived API tokens. No `PYPI_TOKEN` secret to rotate or leak. |
| 89 | + |
| 90 | +### Verifying AIIR release provenance |
| 91 | + |
| 92 | +Consumers can verify that any AIIR release was built by the official CI pipeline: |
| 93 | + |
| 94 | +```bash |
| 95 | +# Verify the latest release attestations (zero dependencies) |
| 96 | +python scripts/verify-pypi-provenance.py |
| 97 | + |
| 98 | +# Verify a specific version |
| 99 | +python scripts/verify-pypi-provenance.py 1.2.2 |
| 100 | + |
| 101 | +# Strict mode — fail if any artifact lacks attestations |
| 102 | +python scripts/verify-pypi-provenance.py --strict |
| 103 | +``` |
| 104 | + |
| 105 | +Alternatively, query the PyPI Integrity API directly: |
| 106 | + |
| 107 | +```bash |
| 108 | +# Fetch attestations for a specific file |
| 109 | +curl -s https://pypi.org/integrity/aiir/1.2.2/aiir-1.2.2-py3-none-any.whl/provenance | python3 -m json.tool |
| 110 | +``` |
| 111 | + |
| 112 | +## Secret rotation |
| 113 | + |
| 114 | +The AIIR project minimises long-lived secrets through OIDC Trusted Publishing |
| 115 | +(PyPI), Sigstore keyless signing, and the automatic `GITHUB_TOKEN`. Three |
| 116 | +repository-level secrets remain: |
| 117 | + |
| 118 | +| Secret | Type | Scope | Rotation cadence | |
| 119 | +|--------|------|-------|------------------| |
| 120 | +| `GITLAB_TOKEN` | GitLab PAT | `write_repository` on the GitLab mirror | 90 days | |
| 121 | +| `NPM_TOKEN` | npm granular token | `publish` on `@invariantsystems/aiir` | 90 days | |
| 122 | +| `WEBSITE_DISPATCH_TOKEN` | GitHub fine-grained PAT | `contents:read` on `invariantsystems.io` | 90 days | |
| 123 | + |
| 124 | +**Rotation procedure:** |
| 125 | +1. Generate a new token with the scopes listed above. |
| 126 | +2. Update the secret in **Settings → Secrets and variables → Actions**. |
| 127 | +3. Trigger a test workflow run to verify the new token works. |
| 128 | +4. Revoke the old token immediately after confirming the new one. |
| 129 | + |
| 130 | +**Design intent:** If any secret expires or is revoked, the failure mode is |
| 131 | +graceful — GitLab sync, npm publish, and website dispatch all have fallback |
| 132 | +paths (CI mirror cron, manual publish, 6-hour self-heal schedule). |
| 133 | + |
| 134 | +## Acknowledgments |
| 135 | + |
| 136 | +We gratefully acknowledge security researchers who report vulnerabilities responsibly. |
0 commit comments