
SOC 2 evidence that collects itself in GitLab CI: every merge emits a signed bundle, an OPA gate blocks releases with missing proof, and auditors get one link instead of screenshots.
SOC 2 evidence that collects itself in GitLab CI: every merge emits a signed bundle, an Open Policy Agent gate blocks releases with missing proof, and an auditor gets one packet link instead of screenshots.
Live auditor dashboard · GitLab project · Demo evidence · Roadmap
I built ShipProof because enterprise sales repeatedly stall during infosec review. When an auditor asks for SOC 2 Type II change-management evidence, engineers often spend two weeks hunting for screenshots across Jira, Slack, and Drive.
I wanted proof to live directly in the deployment pipeline. Every merge commit in ShipProof automatically creates a canonical, signed evidence bundle covering test runs, Semgrep SAST scans, Gitleaks secrets detection, Syft software bills of materials, and CODEOWNERS approvals.
My central design rule is strict: an LLM can summarize proofs or draft issue descriptions, but it can never approve a release. The release gate is an Open Policy Agent evaluation written in Rego. If an artifact is missing or an approval looks like a rubber stamp, the gate fails closed, halts the release, and files an issue with the fix path. When the gate allows, the auditor gets a single link to a static site or an authenticated API.
I mapped ShipProof against the GitLab DevOps lifecycle. It automates all nine stages natively:
| Stage | What I automated in the pipeline |
|---|---|
| plan | Closed-loop gap work item management. Gate denials auto-open structured tracking issues with recurrence counts; subsequent passing pipelines auto-resolve and close the issues with fix commit references. |
| create | Code review and source management. CODEOWNERS protects policy files; the gate rejects rubber-stamp approvals under five minutes; automated MR notes guide review; Duo flows narrate MR comments. |
| verify | pytest, Semgrep SAST, and Gitleaks run in the verify stage, recording artifact hashes. |
| package | Syft generates CycloneDX SBOMs in the package stage and hashes the artifact into the bundle. |
| secure | The deterministic OPA gate checks signatures, chain hashes, and rubber-stamp rules before release. |
| release | The compliance gate job blocks deployment to production; deploy jobs require an allow verdict. |
| configure | Cloud Run deployment manifests render from environment variables without committing secrets. |
| monitor | Post-deploy checks verify that running container image digests match the bundle-approved commit. |
| govern | An immutable hash-chained ledger feeds auditor packets, gap issues, and the GitLab Pages dashboard. |
I developed ShipProof with Python 3.11 and Node.js 20.
python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
Run my test suite and type checks:
OPA_BIN=/tmp/opa pytest -q && ruff check src tests demo-app scripts && mypy --strict src scripts
Collect a local bundle for any commit:
PYTHONPATH=src python -m comply.collect --sha aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --out /tmp/bundle.json
The collector outputs a single JSON line to stdout and writes the full bundle to disk alongside policy/inputs.json. If proofs are missing, the bundle records a deny status.
PYTHONPATH=src python -m comply.collect --sha <40-hex-sha> --out evidence/bundle.json \
--mr-iid 42 --pipeline-id 1001 --project group/project \
--pipeline-file pipeline.json --approvals-file approvals.json
In CI, scripts/gate_bridge.py prepares pipeline.json, while scripts/fetch_approvals.py pulls review data from the GitLab API. Missing files are recorded as missing status rather than assumed passes. Every stderr log includes the bundle correlation ID, and secrets are masked as ***.
PYTHONPATH=src python -m comply.gate --bundle evidence/bundle.json --ledger logs/ledger.jsonl
Exit code 0 indicates an allow verdict. Exit code 1 indicates a denial with a structured error naming the failed control. Exit code 2 indicates invalid arguments. When the gate blocks in CI, the pipeline triggers python -m comply.gap to open a tracking issue.
PYTHONPATH=src python -m comply.packet --ledger logs/ledger.jsonl --release all \
--out packet.json --csv packet.csv --pdf packet.pdf
The CSV export contains eleven columns: bundle_id,commit_sha,mr_iid,pipeline_id,control,proof_type,status,artifact_sha,approver_ids,policy_sha,created_at. The PDF renders from the same JSON source through print CSS.
AUDITOR_TOKEN=local-only PYTHONPATH=src uvicorn comply.api:app --port 8000
Query bundles using bearer authentication:
curl -H "Authorization: Bearer local-only" "localhost:8000/api/v1/bundles?mr_iid=42"
The API exposes endpoints for bundle queries, release packets, and background PDF exports, protected by bearer tokens and a rate limit of 60 requests per minute.
cd auditor && npm install && npm run build
The interface is built with React 19, TypeScript, and Tailwind CSS. It exports as a static site using hash routing, allowing it to run directly on GitLab Pages without server rewrites. When no backend API is present, it uses sample data from public/fallback/demo-packets.json.
flowchart LR
A["CI jobs (pytest, Semgrep, Gitleaks, Syft)"] --> B["gate_bridge.py (artifact summary)"]
B --> C["comply.collect (bundle.json + inputs.json)"]
C --> D["comply.sign (Cosign, HMAC fallback)"]
D --> E["ledger.jsonl (hash chain)"]
E --> F["comply.gate (OPA policy + signature + chain)"]
F -->|"deny"| G["comply.gap (gap issue + MR comment)"]
F -->|"allow"| H["deploy jobs (gated)"]
E --> I["comply.packet (JSON, CSV, PDF)"]
I --> J["GitLab Pages dashboard + read API"]
The pipeline executes unit tests and security scanners, bridges outputs into the evidence collector, signs the bundle, and appends a record to the ledger. The gate job evaluates policy/soc2-v1.rego against the bundle, the cryptographic signature, and the ledger chain. Denials notify the merge request and open a GitLab work item. Successful evaluations unblock deployment jobs and publish the auditor packet.