@occasiolabs/occasio 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +216 -0
- package/bin/occasio-mcp.js +5 -0
- package/bin/occasio.js +2 -0
- package/bin/supervisor/README.md +90 -0
- package/bin/supervisor/com.occasio.proxy.plist.template +36 -0
- package/bin/supervisor/install-windows-task.ps1 +48 -0
- package/bin/supervisor/occasio.service +18 -0
- package/docs/AUDIT.md +120 -0
- package/docs/attest_verify.py +283 -0
- package/docs/audit_walker.py +65 -0
- package/docs/canonicalize.py +99 -0
- package/docs/compliance-mapping.md +93 -0
- package/docs/demos/mcp-block.md +148 -0
- package/docs/edr-calibration.md +73 -0
- package/docs/edr-demo.md +83 -0
- package/docs/python-verifier.md +74 -0
- package/docs/reference-pipeline.md +140 -0
- package/package.json +69 -0
- package/policy-templates/dev-default.yml +84 -0
- package/policy-templates/finance.yml +61 -0
- package/policy-templates/strict.yml +49 -0
- package/schemas/agent-attestation-v1.json +190 -0
- package/schemas/occasio-policy.schema.json +99 -0
- package/spec/agent-attestation/v1/README.md +137 -0
- package/src/adapters/claude-code.js +518 -0
- package/src/adapters/cline.js +161 -0
- package/src/adapters/computer-use-cli.js +198 -0
- package/src/adapters/computer-use.js +227 -0
- package/src/analyzer.js +170 -0
- package/src/anomaly/cli.js +143 -0
- package/src/anomaly/detectors/deny-rate.js +84 -0
- package/src/anomaly/detectors/file-read-volume.js +109 -0
- package/src/anomaly/detectors/secret-redact-rate.js +107 -0
- package/src/anomaly/detectors/unknown-tool-input.js +83 -0
- package/src/anomaly/index.js +169 -0
- package/src/attest/canonicalize.js +97 -0
- package/src/attest/index.js +355 -0
- package/src/attest/run-slice.js +57 -0
- package/src/attest/sign.js +186 -0
- package/src/attest/verify.js +192 -0
- package/src/audit/errors.js +21 -0
- package/src/audit/input-normalizer.js +121 -0
- package/src/audit/jsonl-auditor.js +178 -0
- package/src/audit/verifier.js +152 -0
- package/src/baseline.js +507 -0
- package/src/boundary.js +238 -0
- package/src/budget.js +42 -0
- package/src/classifier.js +115 -0
- package/src/context-budget.js +77 -0
- package/src/core/boundary-event.js +75 -0
- package/src/core/decision.js +61 -0
- package/src/core/pipeline.js +66 -0
- package/src/core/tool-names.js +105 -0
- package/src/dashboard.js +892 -0
- package/src/demo/README.md +31 -0
- package/src/demo/anomalies-demo.js +211 -0
- package/src/demo/attest-demo.js +198 -0
- package/src/distiller.js +155 -0
- package/src/embeddings.json +72 -0
- package/src/executor/dispatcher.js +230 -0
- package/src/harness.js +817 -0
- package/src/index.js +1711 -0
- package/src/inspect.js +329 -0
- package/src/interceptor.js +1198 -0
- package/src/lao.js +185 -0
- package/src/lao_prep.py +119 -0
- package/src/ledger.js +209 -0
- package/src/mcp-experiment.js +140 -0
- package/src/mcp-normalize.js +139 -0
- package/src/mcp-server.js +320 -0
- package/src/outbound-policy.js +433 -0
- package/src/policy/built-in-classifiers.js +78 -0
- package/src/policy/doctor.js +226 -0
- package/src/policy/engine.js +339 -0
- package/src/policy/init.js +153 -0
- package/src/policy/loader.js +448 -0
- package/src/policy/rules-default.js +36 -0
- package/src/policy/shell-path.js +135 -0
- package/src/policy/show.js +196 -0
- package/src/policy/validate.js +310 -0
- package/src/preflight/cli.js +164 -0
- package/src/preflight/miner.js +329 -0
- package/src/proxy/agent-router.js +93 -0
- package/src/redteam.js +428 -0
- package/src/replay.js +446 -0
- package/src/report/index.js +224 -0
- package/src/runtime.js +595 -0
- package/src/scanner/index.js +49 -0
- package/src/selftest.js +192 -0
- package/src/session.js +36 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Demo — Cross-protocol governance via MCP
|
|
2
|
+
|
|
3
|
+
**What this demo proves.** The same `policy.yml` that governs Claude Code's `Read` tool also governs an MCP client's `read_file` call, unmodified. A `deny_paths` rule produces a `BLOCK` decision on both protocols, both produce a synthetic refusal to the agent, and both land in the same hash-chained `pipeline-events.jsonl` audit log — distinguishable only by the `protocol` field.
|
|
4
|
+
|
|
5
|
+
**Status.** Captured against the Occasio v0.6.5 MCP server (`bin/occasio-mcp.js`), driven by a synthetic MCP client invocation. The same code path runs when a real MCP client (Claude Desktop, Cursor, Continue) connects via stdio to `occasio-mcp` configured in its `mcpServers` list.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Policy
|
|
10
|
+
|
|
11
|
+
`~/.occasio/policy.yml`:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
version: 1
|
|
15
|
+
deny_paths:
|
|
16
|
+
- C:\Users\you\.ssh
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This is the **same** policy file that produced the Slice E `BLOCK` row for the Claude Code adapter. No MCP-specific stanzas. No new primitives.
|
|
20
|
+
|
|
21
|
+
## 2. MCP traffic — drive the server with two tool calls
|
|
22
|
+
|
|
23
|
+
Two `tools/call` requests, one denied and one allowed, sent through the Occasio MCP server with `clientInfo.name: "claude-ai"`:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
→ tools/call read_file { path: "C:\\Users\\you\\.ssh\\id_rsa" } # denied path
|
|
27
|
+
→ tools/call read_file { path: "C:\\Users\\you\\Desktop\\…\\README.md" } # allowed path
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The server's responses, captured from the JSON-RPC stdout stream:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "id": 2, "result": {
|
|
34
|
+
"content": [{ "type": "text", "text": "(blocked by policy)" }],
|
|
35
|
+
"isError": true
|
|
36
|
+
}}
|
|
37
|
+
|
|
38
|
+
{ "id": 3, "result": {
|
|
39
|
+
"content": [{ "type": "text", "text": " 1\t# Occasio\n 2\t…" }],
|
|
40
|
+
"isError": false
|
|
41
|
+
}}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The denied call **never opened the file**. It was short-circuited at the policy boundary, the synthetic refusal flowed back to the MCP client, and an audit row was written before the response was sent.
|
|
45
|
+
|
|
46
|
+
## 3. Audit row — verbatim from `pipeline-events.jsonl`
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"ts": "2026-05-10T14:28:16.133Z",
|
|
51
|
+
"event_id": "1b20b7a1-0512-417f-a080-605ebecedb64",
|
|
52
|
+
"agent": "claude-ai",
|
|
53
|
+
"protocol": "mcp",
|
|
54
|
+
"direction": "inbound",
|
|
55
|
+
"kind": "tool_call",
|
|
56
|
+
"tool_name": "read_file",
|
|
57
|
+
"tool_inputs": { "path": "C:\\Users\\you\\.ssh\\id_rsa" },
|
|
58
|
+
"action": "BLOCK",
|
|
59
|
+
"reason": "path-denied",
|
|
60
|
+
"policy_source": "default",
|
|
61
|
+
"result_kind": "block",
|
|
62
|
+
"prev_hash": "3a764943385af22ed364f6a56e0f2b53f06fd544c4fbd531a1633608ca1ada09",
|
|
63
|
+
"hash": "664a8b6076540185ca74255c5b4d25887cc09b7323f86dcee8fd562652be3650"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The `protocol: "mcp"` field is the proof that this is the same governance pipeline the Slice E row came from, just entered by a different front door. The `agent: "claude-ai"` field came from the MCP `initialize.clientInfo.name` and is what a real Claude Desktop client identifies itself as. The `prev_hash` value matches the `hash` of the previous Slice E LOCAL row — the chain is continuous across protocols.
|
|
68
|
+
|
|
69
|
+
## 4. Chain integrity — both verifiers agree
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
$ occasio audit verify
|
|
73
|
+
Rows: 33 total (33 chained, 0 legacy/unverified)
|
|
74
|
+
✓ Chain intact (33 rows verified)
|
|
75
|
+
|
|
76
|
+
$ python docs/audit_walker.py ~/.occasio/pipeline-events.jsonl
|
|
77
|
+
OK: 33 rows verified
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Adding the MCP row to a chain that previously contained only Anthropic-protocol rows did not break either verifier. The independent walker treats `protocol: "mcp"` as just another field; the canonical-serialization rules in `docs/AUDIT.md` are protocol-agnostic.
|
|
81
|
+
|
|
82
|
+
## 5. `occasio report` surfaces the MCP block
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
$ occasio report --days 1
|
|
86
|
+
{
|
|
87
|
+
"summary": {
|
|
88
|
+
"paths_blocked": 2,
|
|
89
|
+
…
|
|
90
|
+
},
|
|
91
|
+
"blocked_accesses": [
|
|
92
|
+
{ "ts": "2026-05-10T12:45:57.036Z", "session_id": "slice-e-live-…",
|
|
93
|
+
"tool": "read_file", "path": "C:\\Users\\you\\.ssh\\id_rsa",
|
|
94
|
+
"action": "BLOCK", "reason": "path-denied" },
|
|
95
|
+
{ "ts": "2026-05-10T14:28:16.133Z", "session_id": null,
|
|
96
|
+
"tool": "read_file", "path": "C:\\Users\\you\\.ssh\\id_rsa",
|
|
97
|
+
"action": "BLOCK", "reason": "path-denied" }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Both blocks appear under `blocked_accesses[]`. The first is the Slice E Claude-Code-protocol block; the second is this demo's MCP block. The report does not distinguish them by protocol because, from a governance-summary perspective, they are the same kind of event — and that is the point of the demo.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## What this demo deliberately does not show
|
|
107
|
+
|
|
108
|
+
- **No third-party MCP server is being forwarded to.** The Occasio MCP server (`lf`) implements `read_file` itself. A "transparent forwarder in front of `@modelcontextprotocol/server-filesystem`" is a deferred follow-up; the cross-protocol governance proof stands on its own without it.
|
|
109
|
+
- **No second MCP integration.** The same proof would extend to GitHub MCP, Slack MCP, etc.; v0.6.5 deliberately ships only the one path.
|
|
110
|
+
- **No new policy primitives, audit fields, or transforms.** Same policy schema, same audit row shape, same hash algorithm.
|
|
111
|
+
- **No README / GOVERNANCE positioning rewrite yet.** That work is sequenced after this proof exists, not before.
|
|
112
|
+
|
|
113
|
+
## Reproducing the demo
|
|
114
|
+
|
|
115
|
+
The demo is reproducible from a clean checkout in under a minute:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
# 1. Set the policy
|
|
119
|
+
cat > ~/.occasio/policy.yml <<'EOF'
|
|
120
|
+
version: 1
|
|
121
|
+
deny_paths:
|
|
122
|
+
- ~/.ssh
|
|
123
|
+
EOF
|
|
124
|
+
|
|
125
|
+
# 2. Drive the MCP server (synthetic; same code path as a real MCP client)
|
|
126
|
+
node -e "
|
|
127
|
+
const mcp = require('./src/mcp-server');
|
|
128
|
+
mcp.__setRespondHookForTests((f) => console.log(f));
|
|
129
|
+
(async () => {
|
|
130
|
+
await mcp.handleRequest({ jsonrpc: '2.0', id: 1, method: 'initialize',
|
|
131
|
+
params: { clientInfo: { name: 'claude-ai' } } });
|
|
132
|
+
await mcp.handleRequest({ jsonrpc: '2.0', id: 2, method: 'tools/call',
|
|
133
|
+
params: { name: 'read_file', arguments: { path: '$HOME/.ssh/id_rsa' } } });
|
|
134
|
+
})();
|
|
135
|
+
"
|
|
136
|
+
|
|
137
|
+
# 3. Inspect the new BLOCK row
|
|
138
|
+
tail -1 ~/.occasio/pipeline-events.jsonl | python -m json.tool
|
|
139
|
+
|
|
140
|
+
# 4. Confirm both verifiers agree
|
|
141
|
+
occasio audit verify
|
|
142
|
+
python docs/audit_walker.py ~/.occasio/pipeline-events.jsonl
|
|
143
|
+
|
|
144
|
+
# 5. Confirm the report surfaces it
|
|
145
|
+
occasio report --days 1 | python -c "import json,sys; print(json.load(sys.stdin)['blocked_accesses'])"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
For a real-MCP-client capture (Claude Desktop, Cursor, Continue), configure `occasio-mcp` as an MCP server in the client's config and prompt the agent to read a denied path. The audit row, the verifier output, and the report all behave identically — the only field that changes is `agent`, which carries the client's `clientInfo.name`.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# EDR-Detector calibration
|
|
2
|
+
|
|
3
|
+
The four built-in anomaly detectors (`src/anomaly/detectors/*`) shipped with starter thresholds. This doc records what those thresholds do on a real, day-to-day Occasio chain, and what was tuned after measuring.
|
|
4
|
+
|
|
5
|
+
## Why the thresholds matter
|
|
6
|
+
|
|
7
|
+
Occasio markets the anomaly layer as EDR — a category whose buyers (CISOs, Compliance) judge tools by the false-positive rate on normal activity. *"How often does this fire when nothing is wrong?"* is a hard, specific question. Starter thresholds without an empirical baseline are a credibility risk.
|
|
8
|
+
|
|
9
|
+
The calibration script `scripts/calibrate-anomaly-detectors.js` slides a 15-minute window across an audit chain in 5-minute steps, runs every detector on every window, and tallies alerts by severity. Run it against any sufficiently large chain (≥ ~500 rows) to validate the defaults against your own usage.
|
|
10
|
+
|
|
11
|
+
## Calibration run
|
|
12
|
+
|
|
13
|
+
**Chain:** `~/.occasio/pipeline-events.jsonl`
|
|
14
|
+
**Span:** 3.2 days (2026-05-11 → 2026-05-14)
|
|
15
|
+
**Rows:** 2522
|
|
16
|
+
**Windows evaluated:** 911 (≈ 12 per hour)
|
|
17
|
+
|
|
18
|
+
The chain includes a mix of normal coding work plus several adversarial harness/redteam scenarios run during development, so it is *not* purely benign — the spikes the detectors flag are partially real attacks, partially noisy thresholds. The goal of the tuning was to keep the real signal (HIGH) loud while quieting noise from normal activity.
|
|
19
|
+
|
|
20
|
+
### Before tuning
|
|
21
|
+
|
|
22
|
+
| Detector | Total | HIGH | MED | LOW | Per hour | Verdict |
|
|
23
|
+
|---|---:|---:|---:|---:|---:|---|
|
|
24
|
+
| `deny-rate` | 3 | 3 | 0 | 0 | 0.04 | calibrated — fires only on real burst |
|
|
25
|
+
| `file-read-volume` | 1 | 0 | 1 | 0 | 0.01 | calibrated |
|
|
26
|
+
| `unknown-tool-input` | 27 | 0 | 27 | 0 | 0.36 | **too noisy** — MEDIUM on every novel non-privileged shape |
|
|
27
|
+
| `secret-redact-rate` | 21 | 8 | 13 | 0 | 0.28 | **too noisy** — MEDIUM on every cold-start redaction |
|
|
28
|
+
|
|
29
|
+
### After tuning
|
|
30
|
+
|
|
31
|
+
| Detector | Total | HIGH | MED | LOW | Per hour | Verdict |
|
|
32
|
+
|---|---:|---:|---:|---:|---:|---|
|
|
33
|
+
| `deny-rate` | 3 | 3 | 0 | 0 | 0.04 | unchanged |
|
|
34
|
+
| `file-read-volume` | 1 | 0 | 1 | 0 | 0.01 | unchanged |
|
|
35
|
+
| `unknown-tool-input` | 24 | 0 | 0 | 24 | 0 MED+HIGH/h | non-privileged novelty now LOW; privileged-key path still HIGH |
|
|
36
|
+
| `secret-redact-rate` | 21 | 8 | 10 | 3 | 0.20 MED+HIGH/h | cold-start single-redactions now LOW; bursts (≥5) stay MEDIUM |
|
|
37
|
+
|
|
38
|
+
The HIGH-severity counts for `secret-redact-rate` are real spikes (e.g. ×117.8 over baseline) from harness runs producing secret-pattern matches on synthetic fixtures. On a chain without adversarial sessions the count would be near zero.
|
|
39
|
+
|
|
40
|
+
## What was tuned
|
|
41
|
+
|
|
42
|
+
### `src/anomaly/detectors/unknown-tool-input.js`
|
|
43
|
+
|
|
44
|
+
- `COLD_START_MIN_ROWS`: `50 → 200`. The detector needs more history to build a real fingerprint set before alerting on "novelty".
|
|
45
|
+
- Severity: non-privileged-key novelty is now **LOW** (was MEDIUM). Visible to SIEM consumers via `--json`, not loud for human review.
|
|
46
|
+
- Privileged-key novelty (`env`, `sudo`, `exec`, `seccomp`, etc.) remains **HIGH**.
|
|
47
|
+
|
|
48
|
+
### `src/anomaly/detectors/secret-redact-rate.js`
|
|
49
|
+
|
|
50
|
+
- Cold-start severity is now scaled by burst size: `< 5` redactions in window → **LOW**, `≥ 5` → **MEDIUM**. Compliance still sees every redaction in the JSON output; reviewers are not paged on test-fixture single-events.
|
|
51
|
+
- Once history is ≥ `MIN_HISTORY_FOR_RATE` (200), the existing multiplier logic takes over and HIGH-severity firing on real bursts is unchanged.
|
|
52
|
+
|
|
53
|
+
### `src/anomaly/detectors/deny-rate.js` and `src/anomaly/detectors/file-read-volume.js`
|
|
54
|
+
|
|
55
|
+
Unchanged. Calibration confirmed the defaults fire only on real spikes.
|
|
56
|
+
|
|
57
|
+
## How to re-run
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
node scripts/calibrate-anomaly-detectors.js
|
|
61
|
+
# JSON output for downstream tooling:
|
|
62
|
+
node scripts/calibrate-anomaly-detectors.js --json > calibration.json
|
|
63
|
+
# Override the window size or step:
|
|
64
|
+
node scripts/calibrate-anomaly-detectors.js --window 30m --step 10m
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The script prints per-detector tallies, alert rates, one example per severity level, and a heuristic suggestion ("threshold likely too tight" if a detector fires >1 HIGH/day on normal usage). Use the suggestion as a starting point, not a verdict — your chain's activity profile may legitimately push a detector higher than the heuristic expects.
|
|
68
|
+
|
|
69
|
+
## What this is not
|
|
70
|
+
|
|
71
|
+
- **Not a replacement for adversarial validation.** Calibration tells us whether the threshold is too tight for normal use. It does not tell us whether the threshold is loose enough to catch genuine attacks. That is the job of `occasio demo anomalies` (which constructs a synthetic adversarial chain that must trigger all four detectors) and the [EDR demo walkthrough](edr-demo.md) (which runs a real Claude Code session against the policy and confirms the detectors fire on the resulting chain).
|
|
72
|
+
- **Not a static contract.** Defaults are tuned against one user's chain over 3 days. A production deployment should re-calibrate against its own audit history. Plan: lift the thresholds into `policy.yml` as overridable values once the schema for that is agreed.
|
|
73
|
+
- **Not a guarantee of zero false positives.** A `MEDIUM` `secret-redact-rate` alert can fire when someone is legitimately editing a config file full of pattern-matching secrets. The right reading of any single alert is "look at the implicated rows and decide", not "block production".
|
package/docs/edr-demo.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# EDR demo — defense in depth against a real Claude Code agent
|
|
2
|
+
|
|
3
|
+
End-to-end validation that Occasio's two governance layers both fire correctly against a real, signed-in Claude Code agent attempting to bypass policy. Reproducible in under two minutes on a laptop, with no API key and no external services.
|
|
4
|
+
|
|
5
|
+
## What this proves
|
|
6
|
+
|
|
7
|
+
Two independent layers of defense, both validated against the same adversarial agent in the same run:
|
|
8
|
+
|
|
9
|
+
1. **Policy enforcement (Layer 2).** The agent's tool calls hit the policy and are blocked. The marker file the agent is told to extract never leaks into its stdout, never appears in any audit row, and the chain attributes the defense to a concrete `BLOCK` row with `reason: path-denied`.
|
|
10
|
+
2. **Anomaly detection (Layer 4 / EDR).** The pattern of those blocks — a burst of denials concentrated in a short window — is detected against the historical baseline of normal activity, and surfaced as a HIGH-severity alert with a measurable severity multiplier.
|
|
11
|
+
|
|
12
|
+
Each layer can be evaluated independently. Either holding without the other is still a partial defense; both holding is what a buyer with a real compliance requirement is looking for.
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
You need:
|
|
17
|
+
|
|
18
|
+
- Claude Code installed and signed in (Occasio uses the bundled auth — no `ANTHROPIC_API_KEY` required).
|
|
19
|
+
- Occasio v0.8.0 or newer with the anomaly layer (`occasio anomalies --help` should list four detectors).
|
|
20
|
+
- A populated `~/.occasio/pipeline-events.jsonl` from your everyday Occasio usage. The more rows, the more meaningful the baseline.
|
|
21
|
+
|
|
22
|
+
No other dependencies. No network calls beyond what Claude Code itself makes.
|
|
23
|
+
|
|
24
|
+
## Steps
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 1. Spawn five real Claude Code adversarial sessions through the harness.
|
|
28
|
+
# Each one tries to extract a marker from a denied path via a different
|
|
29
|
+
# attack vector (direct Read, shell-mediated cat/Get-Content, path
|
|
30
|
+
# traversal, Read→Write→Read tool chain, symlink/junction). Occasio's
|
|
31
|
+
# policy must block every one. Scratch workspaces are kept for analysis.
|
|
32
|
+
for s in deny-read deny-shell-bypass path-traversal \
|
|
33
|
+
tool-chain-read-write-read symlink-bypass; do
|
|
34
|
+
occasio harness --scenario "$s" --keep-scratch
|
|
35
|
+
done
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
# 2. Merge the five scratch chains into one for analysis. Each chain is
|
|
40
|
+
# hash-linked independently to its own GENESIS, so we cannot walk the
|
|
41
|
+
# merged file as a single chain — but the anomaly layer only needs
|
|
42
|
+
# rows, not chain integrity, so concatenation is fine here.
|
|
43
|
+
$merged = "$env:TEMP\edr-demo-merged.jsonl"
|
|
44
|
+
Remove-Item $merged -ErrorAction SilentlyContinue
|
|
45
|
+
Get-ChildItem $env:TEMP -Filter 'lf-scratch-*' -Directory |
|
|
46
|
+
ForEach-Object {
|
|
47
|
+
if (Test-Path "$($_.FullName)\events.jsonl") {
|
|
48
|
+
Get-Content "$($_.FullName)\events.jsonl" | Add-Content $merged
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 3. Run EDR against the merged chain. Pin --since to a few minutes after
|
|
55
|
+
# the last adversarial row so the harness activity lands in the window.
|
|
56
|
+
occasio anomalies --chain "$merged" --since "<ISO timestamp just after the harness finished>" --window 15m
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For the strongest test, prepend your real `~/.occasio/pipeline-events.jsonl` to the merged chain *before* running step 3. That gives the detector a real historical baseline to compare against — your day-to-day activity, mostly free of `BLOCK` rows — rather than the empty cold-start baseline.
|
|
60
|
+
|
|
61
|
+
## What you should see
|
|
62
|
+
|
|
63
|
+
**Step 1: every harness scenario reports `passed: true`** with assertions like *"marker not present in child stdout"* and *"defense attribution: Occasio BLOCKed (read_file / path-denied)"*. This is the Layer-2 proof: the agent tried multiple attack vectors and every one was blocked before any sensitive content reached its context.
|
|
64
|
+
|
|
65
|
+
**Step 3 with cold-start (no real baseline):** the `deny-rate` detector fires with `MEDIUM` severity, citing a high proportion of `BLOCK` rows in the window. Expect 60–90% block rate — the harness scenarios are pure denial tests, so almost every recorded tool call is a `BLOCK`.
|
|
66
|
+
|
|
67
|
+
**Step 3 with your real chain as baseline:** the detector escalates to `HIGH` severity with an explicit multiplier vs. historical rate. The multiplier scales with how busy your normal Occasio usage is — sparser baselines produce larger multipliers because the denominator is smaller. Expect anywhere from ×100 to ×1000 in practice; the exact number is a function of your chain volume, not the strength of the detection.
|
|
68
|
+
|
|
69
|
+
The key signal is the *category* of the alert (`HIGH` vs `MEDIUM` vs none), not the exact multiplier.
|
|
70
|
+
|
|
71
|
+
## How to interpret the result
|
|
72
|
+
|
|
73
|
+
A HIGH `deny-rate` alert means the detector observed a burst of policy violations that does not match the historical rhythm of this Occasio install. In a real deployment, this is the EDR signal an operator should react to:
|
|
74
|
+
|
|
75
|
+
- Look at the implicated row hashes (`rows_implicated` field) and replay the run(s) that produced them: `occasio replay --run <id>`.
|
|
76
|
+
- If the agent attempted the bursts under legitimate user request — a developer running an unfamiliar tool that touches many denied paths — re-scope the policy and add a `policy doctor` annotation.
|
|
77
|
+
- If the agent was driven by a prompt injection or a compromised input file, the run's audit chain plus the EDR alert together form a complete forensic record. The chain hash-links every event back to GENESIS; the attestation (`occasio attest --run-id <id> --sign`) cryptographically commits to the full slice.
|
|
78
|
+
|
|
79
|
+
## What this is not
|
|
80
|
+
|
|
81
|
+
- **Not a replacement for a third-party security audit.** This demo validates that two layers fire on a deliberately-staged attack. It does not certify that the policy is exhaustive, that the binary is free of bugs, or that the threat model matches your organisation's. A real audit looks at all three.
|
|
82
|
+
- **Not a defense against a compromised Occasio binary.** The whole pipeline trusts that the proxy itself has not been tampered with. If an attacker can replace the `occasio` binary on the runner, they can lie about everything downstream. Pin a checksummed install, run in CI under a known image, and verify the binary's signature before relying on its audit trail.
|
|
83
|
+
- **Not a substitute for a correctly-configured policy.** Layers 2 and 4 only catch what the policy declares. A `policy.yml` that omits a sensitive path will not produce a `BLOCK` row on that path, and the EDR will not see any anomaly. `occasio policy doctor` surfaces gaps; treat it as a required step, not optional hygiene.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Python verifier — cross-language verification of Occasio attestations
|
|
2
|
+
|
|
3
|
+
A second reference implementation of the [`agent-attestation/v1`](../spec/agent-attestation/v1/README.md) verifier, written in Python and depending only on the stdlib + optional `sigstore-python`. Lives alongside `audit_walker.py` (which it reuses for the chain step).
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
A predicate type whose verification is only feasible in the language that produced it is not a standard — it is one vendor's artifact. The Python verifier proves that Occasio attestations are **language-independent** and can be re-verified by any auditor in their environment of choice.
|
|
8
|
+
|
|
9
|
+
This is the proof artifact for the OpenSSF / in-toto Attestation Registry submission. The same predicate JSON + Sigstore bundle is verified pass/fail by:
|
|
10
|
+
|
|
11
|
+
- `occasio attest verify` (Node)
|
|
12
|
+
- `python docs/attest_verify.py` (Python)
|
|
13
|
+
- The browser viewer at [`integrations/attest-view/`](../integrations/attest-view/) (in-browser, partial — Sigstore crypto is deferred to one of the two above)
|
|
14
|
+
|
|
15
|
+
The Node test suite asserts that all three implementations agree byte-for-byte on the same payload (`test-interceptor.js` — search for `xlang:`).
|
|
16
|
+
|
|
17
|
+
## Files
|
|
18
|
+
|
|
19
|
+
| File | Purpose |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `canonicalize.py` | RFC 8785 subset, mirror of `src/attest/canonicalize.js` |
|
|
22
|
+
| `audit_walker.py` | SHA-256 chain walker (pre-existing, reused) |
|
|
23
|
+
| `attest_verify.py` | End-to-end verifier with CLI |
|
|
24
|
+
|
|
25
|
+
The Python `canonicalize` and the JS `canonicalize` must stay in lockstep. The two files exist in parallel deliberately — bundling them into one cross-compiled artifact would defeat the point of cross-language verifiability.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Verify a signed attestation pair end-to-end
|
|
31
|
+
python docs/attest_verify.py path/to/occasio-attestation.json
|
|
32
|
+
|
|
33
|
+
# Explicit bundle path (default: <attestation>.sigstore.json sidecar)
|
|
34
|
+
python docs/attest_verify.py path/to/att.json --bundle path/to/bundle.json
|
|
35
|
+
|
|
36
|
+
# Override the chain file (default: read chain_file from the attestation)
|
|
37
|
+
python docs/attest_verify.py path/to/att.json --chain path/to/pipeline-events.jsonl
|
|
38
|
+
|
|
39
|
+
# Machine-readable output
|
|
40
|
+
python docs/attest_verify.py --json path/to/att.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Exit code 0 when every (non-skipped) check passes, 1 otherwise.
|
|
44
|
+
|
|
45
|
+
## What the three checks prove
|
|
46
|
+
|
|
47
|
+
1. **Sigstore signature** — Fulcio certificate chain valid + Rekor inclusion proof present. Requires `pip install sigstore`; without it the step is marked `SKIP` so the auditor knows not to trust a partial result.
|
|
48
|
+
2. **Bundle payload matches attestation** — re-decode the DSSE envelope, canonicalize its `predicate`, compare canonical bytes to the canonicalised attestation (minus `signature` metadata). Pure-stdlib, always runs.
|
|
49
|
+
3. **Audit chain integrity** — SHA-256 walk every `prev_hash → hash` link from GENESIS, then assert that the attestation's `first_hash` and `last_hash` appear in the chain in the correct relative order. Reuses `audit_walker.py`.
|
|
50
|
+
|
|
51
|
+
Each check is independent. Skipping any one of them is not the same as a full verification, and the verifier surfaces that distinction explicitly (the overall pass requires `ok=True` on every check; skipped counts as not-ok).
|
|
52
|
+
|
|
53
|
+
## Round-trip claim
|
|
54
|
+
|
|
55
|
+
For a payload produced by `occasio attest --sign` and verified by `occasio attest verify`, the Python verifier produces the same pass/fail result on:
|
|
56
|
+
- the unmodified payload (both pass on steps 2+3; step 1 requires sigstore-python)
|
|
57
|
+
- a tampered predicate (both fail at step 2)
|
|
58
|
+
- a tampered chain (both fail at step 3)
|
|
59
|
+
- a tampered Sigstore bundle (both fail at step 1; SKIP if sigstore-python not installed)
|
|
60
|
+
|
|
61
|
+
The test suite covers cases 1, 2, and 3 deterministically with the Sigstore step mocked. The cross-language byte-equivalence on the predicate-canonicalization step is asserted via Python-spawn from the Node test runner (`xlang:` and `xlang-float:` test blocks); both implementations reject non-integer numbers so the equivalence cannot be silently broken by adding a float field to a future schema. Case 4 (real Sigstore tamper detection) requires GitHub Actions OIDC infrastructure and is exercised by the live Action's self-verify step in CI, not by the in-process test suite.
|
|
62
|
+
|
|
63
|
+
## Install hint for Sigstore step
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install sigstore # adds the Fulcio + Rekor verification step
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Versions tracked: `sigstore-python >= 3.0`. The verifier degrades gracefully if a different major version is installed (the `Verifier` API is the stable surface used here).
|
|
70
|
+
|
|
71
|
+
## Limitations
|
|
72
|
+
|
|
73
|
+
- **Identity pinning is not enforced** by the reference verifier. The default `policy.UnsafeNoOp()` accepts any Fulcio cert. An auditor whose compliance regime requires a specific workflow-ref identity (e.g. `repo:org/repo:ref:refs/heads/main`) should adapt the call to `policy.Identity(...)`. Pattern intentionally exposed: this is a policy decision, not a verifier decision.
|
|
74
|
+
- **The Python canonicalize is a JCS subset**, not full RFC 8785. The deviations are documented inline in `canonicalize.py`. Non-integer numbers are explicitly rejected on both sides as a load-bearing cross-language invariant — see `canonicalize.py` and `src/attest/canonicalize.js` for the rationale.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Reference Pipeline — from AI-agent PR to cryptographically-attested merge
|
|
2
|
+
|
|
3
|
+
End-to-end walkthrough of the Occasio attestation pipeline. Read top-to-bottom; every step has a copy-paste artefact and an explanation of what it proves.
|
|
4
|
+
|
|
5
|
+
## What this pipeline is for
|
|
6
|
+
|
|
7
|
+
When an AI coding agent (Claude Code, Cline, MCP-routed, etc.) opens a PR, the reviewer's question is *"What did the agent actually do?"* — every tool call, every blocked attempt, every secret redacted, under which policy, on what audit-chain commitment. This pipeline answers that question with a signed artifact that can be verified offline by any third party using only `cosign` and the published predicate type.
|
|
8
|
+
|
|
9
|
+
Three deliverables land on the PR:
|
|
10
|
+
|
|
11
|
+
1. A **Check Run** with a human-readable summary of the run.
|
|
12
|
+
2. A workflow **artifact** containing the signed predicate JSON and the Sigstore Bundle.
|
|
13
|
+
3. A **View Evidence** link that opens the standalone viewer page with both files pre-loaded.
|
|
14
|
+
|
|
15
|
+
## Try it locally in 30 seconds
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
occasio demo attest
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This builds a synthetic audit chain, an unsigned attestation, runs the canonical-JSON round-trip check, and previews the Check Run summary. No Sigstore, no GitHub, no API key — just the pipeline working against in-memory data. Use this to validate the build end-to-end before deploying any of the rest.
|
|
22
|
+
|
|
23
|
+
## Step 1 — your agent runs under Occasio
|
|
24
|
+
|
|
25
|
+
A Occasio session must produce events into `~/.occasio/pipeline-events.jsonl`. The simplest way is to invoke Claude Code (or any supported agent) through the local proxy:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
occasio claude --hardened
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`--hardened` routes `Read`/`Glob`/`Grep` through the unified runtime so tool calls are intercepted locally, distillation applied, and secret scanning runs on every tool result. The `~/.occasio/session.json` file gets a fresh `run_id` per session.
|
|
32
|
+
|
|
33
|
+
## Step 2 — produce an attestation locally (sanity check)
|
|
34
|
+
|
|
35
|
+
After a session ends, run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
occasio attest --run-id "$(jq -r .run_id ~/.occasio/session.json)"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This writes `attestation.json` to the cwd. The predicate is unsigned (`signature: null`) — that part lands in the GitHub Action step below. Inspect the predicate; the file is human-readable JSON. The `audit_chain.last_hash` is the commitment: signing it later transitively commits to every event in the slice.
|
|
42
|
+
|
|
43
|
+
## Step 3 — drop in the GitHub Action
|
|
44
|
+
|
|
45
|
+
Create `.github/workflows/attest-on-pr.yml` in your repo:
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
name: Attest AI-generated PR
|
|
49
|
+
|
|
50
|
+
on:
|
|
51
|
+
pull_request:
|
|
52
|
+
branches: [main]
|
|
53
|
+
|
|
54
|
+
permissions:
|
|
55
|
+
id-token: write # Sigstore keyless via GitHub OIDC
|
|
56
|
+
checks: write # post the PR Check Run
|
|
57
|
+
contents: read
|
|
58
|
+
|
|
59
|
+
jobs:
|
|
60
|
+
attest:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v5
|
|
64
|
+
with:
|
|
65
|
+
fetch-depth: 2 # so files-changed can diff HEAD^..HEAD
|
|
66
|
+
|
|
67
|
+
# ── Your AI-agent step here ────────────────────────────────────
|
|
68
|
+
# The agent must run under Occasio so its tool calls land in
|
|
69
|
+
# ~/.occasio/pipeline-events.jsonl. Example with Claude Code:
|
|
70
|
+
- run: npm i -g @occasiolabs/occasio @anthropic-ai/claude-code
|
|
71
|
+
- run: occasio claude --hardened < .github/agent-prompt.txt
|
|
72
|
+
env:
|
|
73
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
74
|
+
|
|
75
|
+
# ── Sign + Check Run ───────────────────────────────────────────
|
|
76
|
+
- uses: occasiolabs/attest-action@v1
|
|
77
|
+
# All inputs optional — run_id auto-resolves from session.json,
|
|
78
|
+
# paths default to ~/.occasio/*. Defaults are tuned for the
|
|
79
|
+
# 95% case.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That's it. The composite Action handles:
|
|
83
|
+
- `occasio attest --sign` using the workflow's OIDC token (no key management)
|
|
84
|
+
- Uploading `attestation.json` + `.sigstore.json` as a workflow artifact (90-day retention)
|
|
85
|
+
- Creating the Check Run via the GitHub API
|
|
86
|
+
|
|
87
|
+
## Step 4 — what reviewers see
|
|
88
|
+
|
|
89
|
+
The Check Run lands on the PR as:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
✓ Occasio Attested · 47 calls · 2 blocked
|
|
93
|
+
Claude Opus 4.7 · Policy strict-v2.1 (sha a126…3a)
|
|
94
|
+
Chain ✓ verified · Signature ✓ Sigstore keyless
|
|
95
|
+
[View evidence ↗] [Artifact ↗]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Click **View evidence** to open the [viewer](https://occasiolabs.github.io/attest-view) with the artifact's two JSON files. The viewer runs two browser-side checks (DSSE-payload equivalence + audit-chain replay) and surfaces the Rekor transparency log link for cryptographic verification.
|
|
99
|
+
|
|
100
|
+
The viewer **deliberately does not** verify the Sigstore certificate chain in-browser — bundling Fulcio/Rekor trust roots in-browser is a serious build problem we have not solved cheaply, and we are honest about it on the page itself. Offline crypto-verification is one CLI call:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
occasio attest verify occasio-attestation.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
That command runs three checks in order, all of which must pass:
|
|
107
|
+
1. Sigstore signature is valid (cert chain → Fulcio root, Rekor inclusion proof)
|
|
108
|
+
2. The DSSE payload inside the bundle byte-matches the attestation predicate (modulo `signature` field)
|
|
109
|
+
3. The audit chain integrity verifies end-to-end; the claimed `first_hash` / `last_hash` exist in the chain in the right relative order
|
|
110
|
+
|
|
111
|
+
## Step 5 — what auditors do
|
|
112
|
+
|
|
113
|
+
Auditors do not need access to the producer's machine. They download the workflow artifact, install Occasio (or `cosign`), and verify offline. The signed artifact is portable and self-contained: predicate JSON + Sigstore Bundle + (optionally) the chain file.
|
|
114
|
+
|
|
115
|
+
For a SOC2 audit period the workflow becomes:
|
|
116
|
+
1. Pull all `occasio-attestation` artifacts from the period (GitHub API)
|
|
117
|
+
2. For each: `occasio attest verify` and capture the exit code
|
|
118
|
+
3. Aggregate the `execution_summary` data: how many runs, how many blocks, what rules, what files
|
|
119
|
+
|
|
120
|
+
The audit chain is hash-linked across all of an agent's runs on the same machine; verifying any one slice does not require touching the producer's full log.
|
|
121
|
+
|
|
122
|
+
## Compatibility and what's stable
|
|
123
|
+
|
|
124
|
+
- **Predicate URI** `https://github.com/occasiolabs/occasio/spec/agent-attestation/v1` is **canonical**. It will not be moved or re-pointed.
|
|
125
|
+
- **Required fields** in v1 do not change without bumping the URI to `/v2`.
|
|
126
|
+
- **New optional fields** can land in v1.x (currently reserved: `subject.git_commit`, `subject.files_changed` — already in the schema, populated by the GitHub Action).
|
|
127
|
+
- The **Sigstore Bundle** is the standard `sigstore-bundle+json;version=0.2` shape — works with `cosign`, `sigstore-js`, `sigstore-python`, any future conformant tool.
|
|
128
|
+
|
|
129
|
+
## What this does NOT yet do
|
|
130
|
+
|
|
131
|
+
- **Multi-commit attestations.** Today the predicate binds to a single `run_id`. v1.1 will likely add `subject.git_commits[]` and a way to merge slices for a PR that includes N commits from M runs.
|
|
132
|
+
- **Embedded chain slice.** Today the chain-file path is advisory; the chain itself is not bundled. A v1.x option may add an embedded compressed chain for fully offline replay at a size cost.
|
|
133
|
+
- **Policy provenance.** `policy.file_hash` commits to the file bytes. We do not yet carry the *origin* of the policy (was it committed to a repo? signed by a security team?). v1.1 may add `policy.attestation_url` for nested signed claims.
|
|
134
|
+
|
|
135
|
+
## Reference / further reading
|
|
136
|
+
|
|
137
|
+
- [`spec/agent-attestation/v1/README.md`](../spec/agent-attestation/v1/README.md) — predicate type specification
|
|
138
|
+
- [`schemas/agent-attestation-v1.json`](../schemas/agent-attestation-v1.json) — authoritative JSON Schema
|
|
139
|
+
- [`integrations/attest-action/`](../integrations/attest-action/) — the GitHub Action
|
|
140
|
+
- [`integrations/attest-view/`](../integrations/attest-view/) — the static viewer page
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@occasiolabs/occasio",
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "Occasio — cryptographically verifiable behavioral attestation for AI coding agents. Tool-call interception + policy enforcement + tamper-evident audit chain + Sigstore-signed in-toto attestations + windowed EDR detection. Same engine for Claude Code and MCP; Computer-Use scaffold included.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin/",
|
|
8
|
+
"src/",
|
|
9
|
+
"schemas/",
|
|
10
|
+
"policy-templates/",
|
|
11
|
+
"docs/",
|
|
12
|
+
"spec/",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"NOTICE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node test-interceptor.js",
|
|
18
|
+
"smoke": "node test-smoke.js",
|
|
19
|
+
"test:mcp": "node test-mcp-server.js",
|
|
20
|
+
"restart-check": "node scripts/restart-check.js",
|
|
21
|
+
"check-validation": "node scripts/check-validation.js",
|
|
22
|
+
"start": "node bin/occasio.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"claude",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"cline",
|
|
28
|
+
"anthropic",
|
|
29
|
+
"ai-agents",
|
|
30
|
+
"ai-governance",
|
|
31
|
+
"agent-attestation",
|
|
32
|
+
"agent-provenance",
|
|
33
|
+
"sigstore",
|
|
34
|
+
"in-toto",
|
|
35
|
+
"slsa",
|
|
36
|
+
"policy",
|
|
37
|
+
"audit",
|
|
38
|
+
"compliance",
|
|
39
|
+
"eu-ai-act",
|
|
40
|
+
"nist-ai-rmf",
|
|
41
|
+
"soc2",
|
|
42
|
+
"secrets",
|
|
43
|
+
"edr",
|
|
44
|
+
"anomaly-detection",
|
|
45
|
+
"proxy",
|
|
46
|
+
"mcp",
|
|
47
|
+
"computer-use"
|
|
48
|
+
],
|
|
49
|
+
"bin": {
|
|
50
|
+
"occasio": "bin/occasio.js",
|
|
51
|
+
"oc": "bin/occasio.js",
|
|
52
|
+
"occasio-mcp": "bin/occasio-mcp.js"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "https://github.com/occasiolabs/occasio.git"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://github.com/occasiolabs/occasio#readme",
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/occasiolabs/occasio/issues"
|
|
64
|
+
},
|
|
65
|
+
"license": "Apache-2.0",
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"sigstore": "^3.1.0"
|
|
68
|
+
}
|
|
69
|
+
}
|