@yiaany/ghostapi 0.1.9 → 0.2.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/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +132 -119
- package/LICENSE +21 -21
- package/PROVENANCE.md +13 -0
- package/README.md +78 -84
- package/SECURITY.md +56 -53
- package/dist/actions/gateway.js +320 -72
- package/dist/actions/gateway.js.map +1 -1
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +1 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/agentPrompt.js +7 -5
- package/dist/agents/agentPrompt.js.map +1 -1
- package/dist/ai/aiClient.js +5 -5
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.js +66 -26
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/genericInference.js +151 -36
- package/dist/ai/genericInference.js.map +1 -1
- package/dist/ai/prompts.js +55 -49
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.js +649 -133
- package/dist/approvals/approvalInbox.js.map +1 -1
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/behavior/behaviorStore.js +34 -14
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.js +14 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +226 -93
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.js +17 -3
- package/dist/cli/openUrl.js.map +1 -1
- package/dist/cli/parser.js +79 -18
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.js +4 -2
- package/dist/config/dataPaths.js.map +1 -1
- package/dist/config/localConfig.js +5 -2
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.js +11 -5
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.js +311 -83
- package/dist/contracts/contract.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/credentials/credentialBroker.js +391 -79
- package/dist/credentials/credentialBroker.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -2
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/index.js.map +1 -1
- package/dist/dashboard/app.js +823 -685
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/index.html +704 -267
- package/dist/dashboard/styles.css +1040 -349
- package/dist/egress/capabilities.js +55 -25
- package/dist/egress/capabilities.js.map +1 -1
- package/dist/egress/linuxBootstrap.js +47 -11
- package/dist/egress/linuxBootstrap.js.map +1 -1
- package/dist/egress/run.js +170 -31
- package/dist/egress/run.js.map +1 -1
- package/dist/errors/errorEngine.js +15 -3
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/errors/providerErrors.js.map +1 -1
- package/dist/evals/evals.js +304 -78
- package/dist/evals/evals.js.map +1 -1
- package/dist/evals/index.d.ts +2 -2
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/evidence/index.d.ts +2 -2
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/report.d.ts +1 -1
- package/dist/evidence/report.js +263 -79
- package/dist/evidence/report.js.map +1 -1
- package/dist/fault/faultLab.js +24 -9
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +1 -1
- package/dist/inventory/analysis.js +174 -50
- package/dist/inventory/analysis.js.map +1 -1
- package/dist/inventory/index.d.ts +4 -4
- package/dist/inventory/index.js +3 -3
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/inventoryController.js +362 -115
- package/dist/inventory/inventoryController.js.map +1 -1
- package/dist/inventory/types.js +680 -140
- package/dist/inventory/types.js.map +1 -1
- package/dist/ledger/actionLedger.js +355 -78
- package/dist/ledger/actionLedger.js.map +1 -1
- package/dist/ledger/index.d.ts +2 -2
- package/dist/ledger/index.js +1 -1
- package/dist/ledger/index.js.map +1 -1
- package/dist/mcp/server.js +26 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/policy.js +112 -32
- package/dist/policy/policy.js.map +1 -1
- package/dist/productTelemetry/index.js +71 -20
- package/dist/productTelemetry/index.js.map +1 -1
- package/dist/providers/conformance.js +10 -5
- package/dist/providers/conformance.js.map +1 -1
- package/dist/providers/discord.js +2 -2
- package/dist/providers/generic.js +3 -3
- package/dist/providers/github.js +2 -2
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/packs/resendPack.js +51 -25
- package/dist/providers/packs/resendPack.js.map +1 -1
- package/dist/providers/packs/stripePack.js +590 -121
- package/dist/providers/packs/stripePack.js.map +1 -1
- package/dist/providers/registry.js +4 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/runtime.js +7 -6
- package/dist/providers/runtime.js.map +1 -1
- package/dist/providers/stripeWebhook.js +39 -10
- package/dist/providers/stripeWebhook.js.map +1 -1
- package/dist/providers/twilio.js +2 -2
- package/dist/proxy/cacheKey.js +9 -2
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.js +22 -9
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +57 -22
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.js +4 -2
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.js +211 -49
- package/dist/reliability/costGovernance.js.map +1 -1
- package/dist/reliability/index.d.ts +8 -8
- package/dist/reliability/index.js +4 -4
- package/dist/reliability/index.js.map +1 -1
- package/dist/reliability/reconciliation.js +274 -65
- package/dist/reliability/reconciliation.js.map +1 -1
- package/dist/reliability/runtimeHealth.js +127 -35
- package/dist/reliability/runtimeHealth.js.map +1 -1
- package/dist/reliability/slo.js +257 -50
- package/dist/reliability/slo.js.map +1 -1
- package/dist/report/safetyReport.d.ts +6 -0
- package/dist/report/safetyReport.js +56 -12
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/rules/aiRules.js +5 -5
- package/dist/rules/aiRules.js.map +1 -1
- package/dist/safety/index.d.ts +2 -2
- package/dist/safety/index.js +1 -1
- package/dist/safety/index.js.map +1 -1
- package/dist/safety/safetyController.js +675 -165
- package/dist/safety/safetyController.js.map +1 -1
- package/dist/scenarios/scenarioBundle.js +204 -71
- package/dist/scenarios/scenarioBundle.js.map +1 -1
- package/dist/scenarios/scenarioStore.js +88 -29
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.js +52 -10
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +10 -5
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.js +37 -10
- package/dist/server/accessControl.js.map +1 -1
- package/dist/server/createServer.js +1 -1
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.js +15 -5
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +61 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.js +1 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/sdkDetector.js +7 -2
- package/dist/setup/sdkDetector.js.map +1 -1
- package/dist/setup/setupGenerator.js +136 -48
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateExtractor.js +3 -1
- package/dist/state/stateExtractor.js.map +1 -1
- package/dist/state/stateResolver.js +10 -7
- package/dist/state/stateResolver.js.map +1 -1
- package/dist/state/stateStore.js +13 -4
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.js +12 -4
- package/dist/storage/fileStore.js.map +1 -1
- package/dist/teamControl/controlPlane.js +846 -174
- package/dist/teamControl/controlPlane.js.map +1 -1
- package/dist/teamControl/deployment.js +19 -5
- package/dist/teamControl/deployment.js.map +1 -1
- package/dist/teamControl/index.d.ts +4 -4
- package/dist/teamControl/index.js +2 -2
- package/dist/teamControl/index.js.map +1 -1
- package/dist/tests/testGenerator.js +15 -7
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -2
- package/dist/trust/index.js +1 -1
- package/dist/trust/index.js.map +1 -1
- package/dist/trust/trustLadder.js +309 -63
- package/dist/trust/trustLadder.js.map +1 -1
- package/dist/worlds/index.d.ts +2 -2
- package/dist/worlds/index.js +1 -1
- package/dist/worlds/index.js.map +1 -1
- package/dist/worlds/worlds.js +364 -65
- package/dist/worlds/worlds.js.map +1 -1
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -49
- package/docs/github-actions.md +84 -84
- package/docs/mcp.md +58 -52
- package/docs/policy.md +63 -61
- package/docs/providers/authoring-packs.md +180 -175
- package/docs/providers/stripe-core-pack.md +84 -82
- package/docs/releases/README.md +13 -0
- package/docs/security/action-gateway-threat-model.md +34 -34
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
- package/docs/security/approval-inbox-threat-model.md +35 -35
- package/docs/security/credential-broker-threat-model.md +52 -52
- package/docs/security/egress-threat-model.md +126 -126
- package/docs/security/inventory-threat-model.md +104 -104
- package/docs/security/kill-switch-budgets-threat-model.md +37 -37
- package/docs/security/reliability-threat-model.md +111 -111
- package/docs/security/trust-ladder-threat-model.md +33 -33
- package/docs/telemetry.md +14 -0
- package/docs/usage.md +680 -571
- package/examples/README.md +10 -10
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -21
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
- package/examples/ci-smoke/package.json +9 -9
- package/examples/ci-smoke/production-egress.mjs +4 -2
- package/examples/ci-smoke/safe.mjs +21 -17
- package/examples/evals/README.md +24 -24
- package/examples/evals/retry-after.eval.json +71 -52
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +38 -35
- package/examples/openai-streaming/README.md +16 -16
- package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
- package/examples/policy/ghostapi.policy.yaml +27 -27
- package/examples/record-replay/README.md +19 -19
- package/examples/record-replay/replay-requests.json +22 -22
- package/examples/record-replay/stripe-checkout.har.json +30 -30
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -35
- package/examples/stripe-node/checkout-flow.mjs +87 -61
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -19
- package/examples/worlds/subscription-recovery.mjs +21 -12
- package/package.json +28 -6
- package/dist/landing/assets/index-BXR41fAD.css +0 -1
- package/dist/landing/assets/index-CxWsopzP.css +0 -1
- package/dist/landing/assets/index-D8_ggMRE.js +0 -55
- package/dist/landing/assets/index-DZlpyhqd.js +0 -55
- package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
- package/dist/landing/index.html +0 -15
- package/dist/landing/landing.d.ts +0 -3
- package/dist/landing/landing.js +0 -12
- package/dist/landing/landing.js.map +0 -1
- package/docs/commercial/README.md +0 -29
- package/docs/commercial/data-inventory.md +0 -36
- package/docs/commercial/manual-invoicing.md +0 -44
- package/docs/commercial/metrics.md +0 -51
- package/docs/commercial/pricing.md +0 -103
- package/docs/commercial/privacy-policy-draft.md +0 -41
- package/docs/commercial/terms-topics-for-counsel.md +0 -51
- package/docs/design-partners/README.md +0 -52
- package/docs/design-partners/demo-narratives.md +0 -33
- package/docs/design-partners/design-partner-offer.md +0 -39
- package/docs/design-partners/discovery-questions.md +0 -28
- package/docs/design-partners/feedback-capture-template.md +0 -48
- package/docs/design-partners/icp-one-pager.md +0 -43
- package/docs/design-partners/onboarding-checklist.md +0 -33
- package/docs/design-partners/pilot-success-criteria.md +0 -27
- package/docs/design-partners/pricing-interview-script.md +0 -18
- package/docs/design-partners/telemetry-plan.md +0 -48
- package/docs/development/baseline.md +0 -114
- package/docs/development/onboarding-smoke.md +0 -37
- package/docs/development/verification-0.1.8.md +0 -25
- package/docs/enterprise-product-roadmap-ru.md +0 -1879
- package/docs/fundraising/README.md +0 -87
- package/docs/fundraising/data-room-checklist.md +0 -72
- package/docs/fundraising/demo-script.md +0 -52
- package/docs/fundraising/design-partners-50.md +0 -74
- package/docs/fundraising/launch-posts.md +0 -37
- package/docs/fundraising/metrics-and-evidence.md +0 -30
- package/docs/fundraising/roadmap-12-month.md +0 -25
- package/docs/fundraising/technical-due-diligence-index.md +0 -37
- package/docs/fundraising/yc-application.md +0 -25
- package/docs/hosted-pilot.md +0 -111
- package/docs/operations/disaster-recovery-runbook.md +0 -81
- package/docs/operations/kill-switch-runbook.md +0 -32
- package/docs/release-checklist.md +0 -83
- package/docs/release-migration-and-rollback.md +0 -63
- package/docs/release-readiness.md +0 -39
- package/docs/team-control-plane.md +0 -72
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
# Egress Threat Model
|
|
2
|
-
|
|
3
|
-
Last verified: 2026-08-06.
|
|
4
|
-
|
|
5
|
-
## Security Claim
|
|
6
|
-
|
|
7
|
-
GhostAPI 0.1.x includes a Linux-only `ghostapi run` backend that starts a command in fresh user, mount, network and PID namespaces with loopback only. It provides a fail-closed default-deny network boundary only after local `unshare`/`ip` preflight succeeds. `ghostapi doctor --egress` reports `NO PROCESS LAUNCHED`: it is a capability report, not proof that a specific run was isolated.
|
|
8
|
-
|
|
9
|
-
Changing an SDK base URL, setting `HTTP_PROXY`, or instructing an agent to use GhostAPI is useful **accidental safety**. It is not a hostile-code sandbox and is not evidence that production egress was impossible.
|
|
10
|
-
|
|
11
|
-
No design in this document claims protection from an administrator/root-equivalent actor, a compromised host, or a malicious process with control of the same user account outside a future containment boundary.
|
|
12
|
-
|
|
13
|
-
## Assets And Security Goals
|
|
14
|
-
|
|
15
|
-
- Prevent accidental calls to production providers while developers and AI agents test code.
|
|
16
|
-
- Make attempted allowed and blocked connections attributable to one `ghostapi run` execution.
|
|
17
|
-
- Prevent direct IP, alternate DNS, UDP/QUIC, local-network and subprocess bypasses when a supported enforcement backend is active.
|
|
18
|
-
- Fail closed when the requested guarantee cannot be established.
|
|
19
|
-
- Avoid persistent, system-wide proxy, DNS, routing, firewall or daemon state.
|
|
20
|
-
|
|
21
|
-
## Threat Actors
|
|
22
|
-
|
|
23
|
-
| Actor or path
|
|
24
|
-
|
|
|
25
|
-
| Incorrect application code
|
|
26
|
-
| AI agent creates another HTTP client | Bypasses guidance.
|
|
27
|
-
| `curl`, shell command, or subprocess | Bypasses guidance.
|
|
28
|
-
| Direct IP address
|
|
29
|
-
| Custom DNS resolver or DoH
|
|
30
|
-
| UDP or QUIC
|
|
31
|
-
| Local-network target
|
|
32
|
-
| Malicious same-user process
|
|
33
|
-
|
|
34
|
-
## Guarantee Levels
|
|
35
|
-
|
|
36
|
-
### HTTP Proxy Guidance
|
|
37
|
-
|
|
38
|
-
GhostAPI configures or documents a local HTTP endpoint and can collect only traffic voluntarily sent there. It can reduce accidental provider calls, but cannot prevent a different HTTP library, an IP literal, custom DNS, subprocess, UDP/QUIC, or local-network request.
|
|
39
|
-
|
|
40
|
-
This is the active guarantee on platforms without the Linux backend and when a Linux preflight fails.
|
|
41
|
-
|
|
42
|
-
### Process-Level Enforcement
|
|
43
|
-
|
|
44
|
-
A process launcher creates an OS-supported restricted execution boundary and starts the target only after a local preflight passes. The future boundary must cover children and inherited handles, deny all network by default, and expose GhostAPI through an intentionally configured loopback or private transport.
|
|
45
|
-
|
|
46
|
-
On Linux, `ghostapi run -- <command>` now implements a loopback-only process boundary with fresh user, mount, network and PID namespaces. It does not transparently redirect provider TLS traffic and it does not support external network allowlist entries until a reviewed policy gateway exists.
|
|
47
|
-
|
|
48
|
-
### Container Or Network-Namespace Enforcement
|
|
49
|
-
|
|
50
|
-
The target runs in a separate container or network namespace with only loopback, no host networking, no privileged mode, and no mounted container-control socket. This can provide the strongest planned local guarantee, but still does not protect against the container runtime daemon, an administrator, a compromised host, or an explicitly reintroduced host-network path.
|
|
51
|
-
|
|
52
|
-
This is not implemented in GhostAPI 0.1.x.
|
|
53
|
-
|
|
54
|
-
### Unsupported Or Degraded
|
|
55
|
-
|
|
56
|
-
If a native primitive is absent, a required privilege is unavailable, or GhostAPI has no reviewed launcher, the command must not claim isolation. It returns a clear degraded or unsupported result and recommends a reviewed container/VM backend where appropriate.
|
|
57
|
-
|
|
58
|
-
## Platform Capability Model
|
|
59
|
-
|
|
60
|
-
### Linux
|
|
61
|
-
|
|
62
|
-
Linux network namespaces isolate network devices, IP stacks, routing tables, firewall state, sockets, and related networking resources. The Linux manual also documents that the kernel must be configured with `CONFIG_NET_NS`; namespace lifecycle can naturally clean up devices once the last process exits. [Linux network namespaces](https://man7.org/linux/man-pages/man7/network_namespaces.7.html)
|
|
63
|
-
|
|
64
|
-
Implemented backend: an unprivileged parent performs local preflight, writes sanitized run evidence, and launches the target with fresh user, mount, network and PID namespaces. Only loopback is brought up and GhostAPI starts inside the namespace. The parent never creates a veth, route, firewall rule, proxy rule or global daemon. `--pid --fork --kill-child=SIGTERM` makes bootstrap termination kill the namespace init; when PID 1 exits, the PID namespace cannot retain target descendants. A preflight failure starts no target command.
|
|
65
|
-
|
|
66
|
-
The boundary does not make arbitrary same-user code hostile-safe: a path-based UNIX socket already accessible to the same user, such as a Docker/Podman control socket, is not an IP route and can act as a privileged deputy. The backend does not mount a control socket itself, but it also does not yet construct a filesystem sandbox that hides every host socket.
|
|
67
|
-
|
|
68
|
-
### Containers
|
|
69
|
-
|
|
70
|
-
Docker documents `--network none` as a mode that creates only a loopback interface inside the container. [Docker none network driver](https://docs.docker.com/engine/network/drivers/none/)
|
|
71
|
-
|
|
72
|
-
Planned backend: use an OCI runtime only when a bounded image/mount policy is available. `--network host`, privileged containers, mounted Docker/Podman sockets, and host credentials invalidate the intended boundary. GhostAPI will not start or configure a privileged daemon.
|
|
73
|
-
|
|
74
|
-
### Windows
|
|
75
|
-
|
|
76
|
-
Microsoft documents AppContainer as an isolation environment with network access granted only for explicitly allocated Internet, intranet, or server capabilities. [AppContainer isolation](https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-isolation)
|
|
77
|
-
|
|
78
|
-
Planned backend: a per-run AppContainer or LPAC launcher with no network capabilities and a dedicated writable directory. GhostAPI does not create profiles or launch AppContainers today, and a normal Win32 child process is not constrained. A future implementation must clean up its profile deterministically without changing global Windows Firewall state.
|
|
79
|
-
|
|
80
|
-
### macOS
|
|
81
|
-
|
|
82
|
-
Apple's App Sandbox is entitlement-based and intended for signed application bundles, not a general-purpose wrapper for arbitrary developer shell commands. [App Sandbox](https://developer.apple.com/documentation/security/app-sandbox)
|
|
83
|
-
|
|
84
|
-
Planned backend: use an explicit container or VM backend before claiming egress enforcement for arbitrary child commands. GhostAPI must not report `isolated` solely because proxy environment variables or a deprecated sandbox wrapper exists.
|
|
85
|
-
|
|
86
|
-
### Node.js Runtime
|
|
87
|
-
|
|
88
|
-
Node's Permission Model is activated with `--permission`; a launched Node process without `--allow-net` is denied network access. Node explicitly describes the model as a seat belt for trusted code and says it does not provide security guarantees against malicious code. [Node.js Permissions](https://nodejs.org/api/permissions.html)
|
|
89
|
-
|
|
90
|
-
GhostAPI reports this as a degraded auxiliary control only. It cannot contain non-Node children, is not the future `ghostapi run` hostile-sandbox boundary, and does not replace OS-level isolation.
|
|
91
|
-
|
|
92
|
-
## Capability API And CLI Contract
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
ghostapi doctor --egress
|
|
96
|
-
ghostapi doctor --egress --json
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
The JSON report is offline and deterministic apart from local runtime facts. It contains:
|
|
100
|
-
|
|
101
|
-
- `schemaVersion` for machine consumers.
|
|
102
|
-
- `isolated`, which is currently always `false`.
|
|
103
|
-
- `currentGuarantee`, currently `http-proxy-guidance`.
|
|
104
|
-
- Platform/runtime facts and one record per possible backend.
|
|
105
|
-
- Backend status: `available`, `degraded`, `not-implemented`, or `unsupported`.
|
|
106
|
-
- Required privileges/setup and remaining bypasses.
|
|
107
|
-
- A `globalStateChanged: false` invariant.
|
|
108
|
-
|
|
109
|
-
The diagnostic command does not open a network connection, change proxy settings, add firewall rules, create network namespaces, create an AppContainer profile, or contact a container daemon. `ghostapi run` performs the Linux namespace preflight and creates only child-owned namespaces.
|
|
110
|
-
|
|
111
|
-
## Minimal Cross-Platform Architecture
|
|
112
|
-
|
|
113
|
-
1. A platform-neutral policy compiler canonicalizes the allow/deny policy, local GhostAPI transport, command arguments, and run ID.
|
|
114
|
-
2. An unprivileged parent creates evidence and validates the selected backend before spawning the target.
|
|
115
|
-
3. The Linux backend implements `prepare`, `spawn`, lifecycle evidence and namespace cleanup; Windows AppContainer/LPAC and an explicit OCI/VM runner remain future reviewed backends.
|
|
116
|
-
4. Each backend owns its resources by process or container lifetime. Crash cleanup relies on resource ownership, with bounded reconciliation only for leftovers that can be safely identified by a run-specific marker.
|
|
117
|
-
5. A per-run recorder emits attempted connection metadata and a signed/finalized local evidence artifact. It must distinguish a setup failure from a blocked request and an unknown outcome.
|
|
118
|
-
6. A backend that cannot prove the requested guarantee returns `unsupported` or `degraded`; it never silently falls back to proxy guidance for a command advertised as isolated.
|
|
119
|
-
|
|
120
|
-
## Non-Goals For The First `ghostapi run`
|
|
121
|
-
|
|
122
|
-
- Global firewall, global proxy, global DNS, hosts-file, routing-table, or system service changes.
|
|
123
|
-
- Transparent interception of another user's process.
|
|
124
|
-
- Protection from root/administrator, the host kernel, or a compromised container runtime.
|
|
125
|
-
- Hidden installation of a privileged daemon.
|
|
126
|
-
- A claim that Node permissions sandbox hostile code.
|
|
1
|
+
# Egress Threat Model
|
|
2
|
+
|
|
3
|
+
Last verified: 2026-08-06.
|
|
4
|
+
|
|
5
|
+
## Security Claim
|
|
6
|
+
|
|
7
|
+
GhostAPI 0.1.x includes a Linux-only `ghostapi run` backend that starts a command in fresh user, mount, network and PID namespaces with loopback only. It provides a fail-closed default-deny network boundary only after local `unshare`/`ip` preflight succeeds. `ghostapi doctor --egress` reports `NO PROCESS LAUNCHED`: it is a capability report, not proof that a specific run was isolated.
|
|
8
|
+
|
|
9
|
+
Changing an SDK base URL, setting `HTTP_PROXY`, or instructing an agent to use GhostAPI is useful **accidental safety**. It is not a hostile-code sandbox and is not evidence that production egress was impossible.
|
|
10
|
+
|
|
11
|
+
No design in this document claims protection from an administrator/root-equivalent actor, a compromised host, or a malicious process with control of the same user account outside a future containment boundary.
|
|
12
|
+
|
|
13
|
+
## Assets And Security Goals
|
|
14
|
+
|
|
15
|
+
- Prevent accidental calls to production providers while developers and AI agents test code.
|
|
16
|
+
- Make attempted allowed and blocked connections attributable to one `ghostapi run` execution.
|
|
17
|
+
- Prevent direct IP, alternate DNS, UDP/QUIC, local-network and subprocess bypasses when a supported enforcement backend is active.
|
|
18
|
+
- Fail closed when the requested guarantee cannot be established.
|
|
19
|
+
- Avoid persistent, system-wide proxy, DNS, routing, firewall or daemon state.
|
|
20
|
+
|
|
21
|
+
## Threat Actors
|
|
22
|
+
|
|
23
|
+
| Actor or path | Proxy guidance | Process/container enforcement target |
|
|
24
|
+
| ------------------------------------ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| Incorrect application code | Usually caught only when it uses the configured local base URL. | Deny all non-explicit network paths. |
|
|
26
|
+
| AI agent creates another HTTP client | Bypasses guidance. | Same child process remains constrained. |
|
|
27
|
+
| `curl`, shell command, or subprocess | Bypasses guidance. | Descendants must remain in the containment boundary. |
|
|
28
|
+
| Direct IP address | Bypasses hostname-based proxy/DNS policy. | No route/socket path unless explicitly allowed. |
|
|
29
|
+
| Custom DNS resolver or DoH | Bypasses configured resolver assumptions. | Resolver traffic is denied with all other network traffic. |
|
|
30
|
+
| UDP or QUIC | Often bypasses HTTP-only controls. | All socket families and protocols are denied by the boundary. |
|
|
31
|
+
| Local-network target | Often omitted from production-focused allowlists. | Deny by default; allow only deliberate loopback transport. |
|
|
32
|
+
| Malicious same-user process | Out of scope for guidance. | Cannot be contained by a boundary it does not enter; same-user hostile code is not a guarantee from Node permission flags. |
|
|
33
|
+
|
|
34
|
+
## Guarantee Levels
|
|
35
|
+
|
|
36
|
+
### HTTP Proxy Guidance
|
|
37
|
+
|
|
38
|
+
GhostAPI configures or documents a local HTTP endpoint and can collect only traffic voluntarily sent there. It can reduce accidental provider calls, but cannot prevent a different HTTP library, an IP literal, custom DNS, subprocess, UDP/QUIC, or local-network request.
|
|
39
|
+
|
|
40
|
+
This is the active guarantee on platforms without the Linux backend and when a Linux preflight fails.
|
|
41
|
+
|
|
42
|
+
### Process-Level Enforcement
|
|
43
|
+
|
|
44
|
+
A process launcher creates an OS-supported restricted execution boundary and starts the target only after a local preflight passes. The future boundary must cover children and inherited handles, deny all network by default, and expose GhostAPI through an intentionally configured loopback or private transport.
|
|
45
|
+
|
|
46
|
+
On Linux, `ghostapi run -- <command>` now implements a loopback-only process boundary with fresh user, mount, network and PID namespaces. It does not transparently redirect provider TLS traffic and it does not support external network allowlist entries until a reviewed policy gateway exists.
|
|
47
|
+
|
|
48
|
+
### Container Or Network-Namespace Enforcement
|
|
49
|
+
|
|
50
|
+
The target runs in a separate container or network namespace with only loopback, no host networking, no privileged mode, and no mounted container-control socket. This can provide the strongest planned local guarantee, but still does not protect against the container runtime daemon, an administrator, a compromised host, or an explicitly reintroduced host-network path.
|
|
51
|
+
|
|
52
|
+
This is not implemented in GhostAPI 0.1.x.
|
|
53
|
+
|
|
54
|
+
### Unsupported Or Degraded
|
|
55
|
+
|
|
56
|
+
If a native primitive is absent, a required privilege is unavailable, or GhostAPI has no reviewed launcher, the command must not claim isolation. It returns a clear degraded or unsupported result and recommends a reviewed container/VM backend where appropriate.
|
|
57
|
+
|
|
58
|
+
## Platform Capability Model
|
|
59
|
+
|
|
60
|
+
### Linux
|
|
61
|
+
|
|
62
|
+
Linux network namespaces isolate network devices, IP stacks, routing tables, firewall state, sockets, and related networking resources. The Linux manual also documents that the kernel must be configured with `CONFIG_NET_NS`; namespace lifecycle can naturally clean up devices once the last process exits. [Linux network namespaces](https://man7.org/linux/man-pages/man7/network_namespaces.7.html)
|
|
63
|
+
|
|
64
|
+
Implemented backend: an unprivileged parent performs local preflight, writes sanitized run evidence, and launches the target with fresh user, mount, network and PID namespaces. Only loopback is brought up and GhostAPI starts inside the namespace. The parent never creates a veth, route, firewall rule, proxy rule or global daemon. `--pid --fork --kill-child=SIGTERM` makes bootstrap termination kill the namespace init; when PID 1 exits, the PID namespace cannot retain target descendants. A preflight failure starts no target command.
|
|
65
|
+
|
|
66
|
+
The boundary does not make arbitrary same-user code hostile-safe: a path-based UNIX socket already accessible to the same user, such as a Docker/Podman control socket, is not an IP route and can act as a privileged deputy. The backend does not mount a control socket itself, but it also does not yet construct a filesystem sandbox that hides every host socket.
|
|
67
|
+
|
|
68
|
+
### Containers
|
|
69
|
+
|
|
70
|
+
Docker documents `--network none` as a mode that creates only a loopback interface inside the container. [Docker none network driver](https://docs.docker.com/engine/network/drivers/none/)
|
|
71
|
+
|
|
72
|
+
Planned backend: use an OCI runtime only when a bounded image/mount policy is available. `--network host`, privileged containers, mounted Docker/Podman sockets, and host credentials invalidate the intended boundary. GhostAPI will not start or configure a privileged daemon.
|
|
73
|
+
|
|
74
|
+
### Windows
|
|
75
|
+
|
|
76
|
+
Microsoft documents AppContainer as an isolation environment with network access granted only for explicitly allocated Internet, intranet, or server capabilities. [AppContainer isolation](https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-isolation)
|
|
77
|
+
|
|
78
|
+
Planned backend: a per-run AppContainer or LPAC launcher with no network capabilities and a dedicated writable directory. GhostAPI does not create profiles or launch AppContainers today, and a normal Win32 child process is not constrained. A future implementation must clean up its profile deterministically without changing global Windows Firewall state.
|
|
79
|
+
|
|
80
|
+
### macOS
|
|
81
|
+
|
|
82
|
+
Apple's App Sandbox is entitlement-based and intended for signed application bundles, not a general-purpose wrapper for arbitrary developer shell commands. [App Sandbox](https://developer.apple.com/documentation/security/app-sandbox)
|
|
83
|
+
|
|
84
|
+
Planned backend: use an explicit container or VM backend before claiming egress enforcement for arbitrary child commands. GhostAPI must not report `isolated` solely because proxy environment variables or a deprecated sandbox wrapper exists.
|
|
85
|
+
|
|
86
|
+
### Node.js Runtime
|
|
87
|
+
|
|
88
|
+
Node's Permission Model is activated with `--permission`; a launched Node process without `--allow-net` is denied network access. Node explicitly describes the model as a seat belt for trusted code and says it does not provide security guarantees against malicious code. [Node.js Permissions](https://nodejs.org/api/permissions.html)
|
|
89
|
+
|
|
90
|
+
GhostAPI reports this as a degraded auxiliary control only. It cannot contain non-Node children, is not the future `ghostapi run` hostile-sandbox boundary, and does not replace OS-level isolation.
|
|
91
|
+
|
|
92
|
+
## Capability API And CLI Contract
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
ghostapi doctor --egress
|
|
96
|
+
ghostapi doctor --egress --json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The JSON report is offline and deterministic apart from local runtime facts. It contains:
|
|
100
|
+
|
|
101
|
+
- `schemaVersion` for machine consumers.
|
|
102
|
+
- `isolated`, which is currently always `false`.
|
|
103
|
+
- `currentGuarantee`, currently `http-proxy-guidance`.
|
|
104
|
+
- Platform/runtime facts and one record per possible backend.
|
|
105
|
+
- Backend status: `available`, `degraded`, `not-implemented`, or `unsupported`.
|
|
106
|
+
- Required privileges/setup and remaining bypasses.
|
|
107
|
+
- A `globalStateChanged: false` invariant.
|
|
108
|
+
|
|
109
|
+
The diagnostic command does not open a network connection, change proxy settings, add firewall rules, create network namespaces, create an AppContainer profile, or contact a container daemon. `ghostapi run` performs the Linux namespace preflight and creates only child-owned namespaces.
|
|
110
|
+
|
|
111
|
+
## Minimal Cross-Platform Architecture
|
|
112
|
+
|
|
113
|
+
1. A platform-neutral policy compiler canonicalizes the allow/deny policy, local GhostAPI transport, command arguments, and run ID.
|
|
114
|
+
2. An unprivileged parent creates evidence and validates the selected backend before spawning the target.
|
|
115
|
+
3. The Linux backend implements `prepare`, `spawn`, lifecycle evidence and namespace cleanup; Windows AppContainer/LPAC and an explicit OCI/VM runner remain future reviewed backends.
|
|
116
|
+
4. Each backend owns its resources by process or container lifetime. Crash cleanup relies on resource ownership, with bounded reconciliation only for leftovers that can be safely identified by a run-specific marker.
|
|
117
|
+
5. A per-run recorder emits attempted connection metadata and a signed/finalized local evidence artifact. It must distinguish a setup failure from a blocked request and an unknown outcome.
|
|
118
|
+
6. A backend that cannot prove the requested guarantee returns `unsupported` or `degraded`; it never silently falls back to proxy guidance for a command advertised as isolated.
|
|
119
|
+
|
|
120
|
+
## Non-Goals For The First `ghostapi run`
|
|
121
|
+
|
|
122
|
+
- Global firewall, global proxy, global DNS, hosts-file, routing-table, or system service changes.
|
|
123
|
+
- Transparent interception of another user's process.
|
|
124
|
+
- Protection from root/administrator, the host kernel, or a compromised container runtime.
|
|
125
|
+
- Hidden installation of a privileged daemon.
|
|
126
|
+
- A claim that Node permissions sandbox hostile code.
|
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
# Agent Inventory And Attack-Path Graph Threat Model
|
|
2
|
-
|
|
3
|
-
Applies to the local agent inventory layer: the persisted inventory store, attack-path graph, blast-radius analysis, detections, remediation workflow, open export, removal analysis, and ROI report.
|
|
4
|
-
|
|
5
|
-
## Scope
|
|
6
|
-
|
|
7
|
-
This threat model covers the inventory module added in the "agent inventory, attack-path graph, and indispensability" session:
|
|
8
|
-
|
|
9
|
-
- `inventory.json` local persisted store under `.ghostapi/`, created by `getDataPaths().inventoryStore`.
|
|
10
|
-
- Import of agents, tools, identities, providers, resources, side effects, credentials, and policies from `config`, `ci`, `gateway`, and `cloud` sources, each record carrying provenance (source, import time, importer) and freshness.
|
|
11
|
-
- The attack-path graph (`agent -> identity -> tool -> provider -> resource -> side effect`) with persisted, provenance- and freshness-carrying edges.
|
|
12
|
-
- Blast-radius analysis, detections, remediation proposals/applications, open export, removal analysis, and the ROI report.
|
|
13
|
-
|
|
14
|
-
The inventory layer adds no provider, credential, network egress, or hosted control-plane surface. All of it is local-first and synthetic. ROI and removal numbers are derived from imported counters and local records only; nothing is invented, and the pilot entry gate (a real team/enterprise pilot) is not yet met.
|
|
15
|
-
|
|
16
|
-
## Trust Boundaries
|
|
17
|
-
|
|
18
|
-
| Boundary
|
|
19
|
-
|
|
|
20
|
-
| Operator identity | Every inventory action authenticates through an injectable authorizer and requires an explicit permission (`inventory.import` / `inventory.inspect` / `inventory.analyze` / `inventory.remediate` / `inventory.export`); the test authorizer binds issued identities by reference | Real authn/authz infrastructure
|
|
21
|
-
| Import sources
|
|
22
|
-
| Tenant isolation
|
|
23
|
-
| Store
|
|
24
|
-
|
|
25
|
-
## Assets
|
|
26
|
-
|
|
27
|
-
- Agent inventory records and graph edges (`inventory.json`).
|
|
28
|
-
- Provenance and freshness metadata attached to every record and edge.
|
|
29
|
-
- Findings and remediation state (proposals, applications, rejections).
|
|
30
|
-
- The open export artifact (inventory, policies, eval scenario references, evidence metadata, removal analysis, ROI).
|
|
31
|
-
- Import-run and import-source ledger.
|
|
32
|
-
|
|
33
|
-
## Threats And Mitigations
|
|
34
|
-
|
|
35
|
-
### T1. Unauthenticated or unauthorized inventory writes
|
|
36
|
-
|
|
37
|
-
Every mutating and reading method calls `authorize` first: `validateOperator` on the authorizer result and an explicit permission check. The default authorizer (`createDisabledInventoryOperatorAuthorizer`) throws "not configured", so a controller with no authorizer cannot be used. Test identities are reference-bound; a plain `{}` is rejected as "not authenticated".
|
|
38
|
-
|
|
39
|
-
### T2. A poisoned import poisons the graph
|
|
40
|
-
|
|
41
|
-
`validateImportPayload` enforces strict schema, allowed source types, identifier/scope/hash/timestamp formats, bounded array sizes, and per-kind id uniqueness before anything is stored. Cross-references (e.g. an agent referencing an unknown identity) are validated against both the current import and the persisted state, and a reference to a record that does not exist rejects the whole import. Import payloads are canonicalized to a digest recorded on the import run and source.
|
|
42
|
-
|
|
43
|
-
### T3. Tenant data leaks across the graph or export
|
|
44
|
-
|
|
45
|
-
Every graph edge, record, finding, remediation, import run, and source is filtered by `operator.tenantId` in `snapshotFor`, `graphEdges`, `findAttackPaths`, `computeBlastRadius`, `computeFindings`, `computeCoverage`, `computeRemovalAnalysis`, `computeRoiReport`, and `export`. An edge can only be created from a record that belongs to the importing tenant. Cross-tenant leakage is covered by tests.
|
|
46
|
-
|
|
47
|
-
### T4. Provenance or freshness is missing or forged
|
|
48
|
-
|
|
49
|
-
Every record and edge carries `provenance` (`sourceId`, `sourceType`, `sourceName`, `importedAt`, `importedBy`) and `freshness` (`firstSeenAt`, `lastSeenAt`) built from the import payload source and the authenticated operator's principal id — never from client-supplied strings. `validateState` re-validates provenance/freshness on every read and write. Edge ids are derived from source/target/relation so re-imports refresh `lastSeenAt` instead of duplicating edges.
|
|
50
|
-
|
|
51
|
-
Edges have a freshness lifecycle. Attack-path and blast-radius analysis only use edges whose `lastSeenAt` is within the configured `edgeStaleDays` window, so a long-unseen relationship cannot be cited as current reachability evidence. On every import the importing tenant's stale edges are garbage-collected from the store, keeping the graph from accumulating frozen relationships that were never refreshed.
|
|
52
|
-
|
|
53
|
-
### T5. A remediation expands permissions instead of reducing them
|
|
54
|
-
|
|
55
|
-
`reduce_scope` proposals are rejected at proposal time unless the reduced scope list is a strict subset of the credential's current grant scopes and removes at least one scope; the same invariant is enforced again at apply time. `revoke` only ever sets a credential to revoked. `assign_owner` only sets an owner id. `onboard_through_gateway` only sets the gateway-managed flag. `create_eval` only records an eval scenario reference — and, when an `evalScenarioExists` resolver is configured, the referenced scenario must actually exist before the proposal is accepted, so a remediation cannot paper over a finding by pointing at a deleted scenario. No remediation path adds scopes or relaxes a control.
|
|
56
|
-
|
|
57
|
-
Applied remediations are re-verified on every analysis pass: a finding stays resolved only while the remediation remains effective. If a later import re-expands a reduced scope, revokes nothing, clears an assigned owner, un-gates an agent, or deletes the referenced eval scenario, the finding is re-opened instead of staying permanently marked resolved.
|
|
58
|
-
|
|
59
|
-
Remediation targets are validated against the tenant's live data before a proposal is accepted: an `environment` target must actually be referenced by at least one agent, identity, provider, resource, credential, or policy of the tenant, so a remediation cannot be proposed against a stale or invented environment.
|
|
60
|
-
|
|
61
|
-
### T6. The store is poisoned or exploded
|
|
62
|
-
|
|
63
|
-
The store is validated on every read (`validateState`) and write (`atomicWriteJson(this.path, validateState(state))`). Store bytes are capped at 8 MiB; every collection is bounded (`maxAgents`, `maxEdges` 6,000, `maxFindings` 3,000, etc.), and `assertStoreBounds` rejects edges and findings over their caps on every mutation. Import runs are rotated per tenant at `maxImports` (256), keeping the oldest runs trimmed rather than failing the write. The store must be a regular non-symlink file. Oversized or non-JSON stores are rejected, not partially loaded.
|
|
64
|
-
|
|
65
|
-
### T7. Secret-shaped or control-character data enters the inventory
|
|
66
|
-
|
|
67
|
-
Identifiers, scopes, and text are validated by regexes and by `sanitizeSecretString`: any value that contains a recognizable secret shape (e.g. `sk_live_*`) or control characters is rejected. Evidence metadata and reasons are bounded text.
|
|
68
|
-
|
|
69
|
-
### T8. ROI or removal analysis invents savings
|
|
70
|
-
|
|
71
|
-
The ROI report is built only from imported counters (latest per source) and from actually applied remediations in the store; unmeasured counters are reported as `null` and listed in `notMeasured`, and `basis` is `local_inventory_data_only`. The removal analysis reports only numbers computed from the local store and states plainly when nothing verifiable depends on GhostAPI. The heuristic blast-radius classification is explicitly labeled advisory, not proof of exploitability.
|
|
72
|
-
|
|
73
|
-
### T9. Attack-path graph is unbounded or cross-tenant
|
|
74
|
-
|
|
75
|
-
Edges are created only from the importing tenant's records, are keyed by a derived edge id, and are bounded (`maxEdges` 6,000). Path finding is always scoped to the tenant and to a single agent id. Incomplete paths (a record reference missing from the store) are counted separately and reported as incomplete, not silently dropped.
|
|
76
|
-
|
|
77
|
-
## Permissions And Capabilities Summary
|
|
78
|
-
|
|
79
|
-
| Action
|
|
80
|
-
|
|
|
81
|
-
| Import
|
|
82
|
-
| Inspect / graph / attack paths / blast radius / list remediations | operator authorizer + `inventory.inspect`
|
|
83
|
-
| Analyze / removal analysis / ROI
|
|
84
|
-
| Propose / apply / reject remediations
|
|
85
|
-
| Export
|
|
86
|
-
|
|
87
|
-
## Limits
|
|
88
|
-
|
|
89
|
-
| Resource
|
|
90
|
-
|
|
|
91
|
-
| Store bytes
|
|
92
|
-
| Import sources / import runs
|
|
93
|
-
| Agents / tools / identities
|
|
94
|
-
| Providers / resources / side effects | 200 / 1,000 / 2,000 |
|
|
95
|
-
| Credentials / policies
|
|
96
|
-
| Graph edges
|
|
97
|
-
| Findings / remediations
|
|
98
|
-
| References / scopes per record
|
|
99
|
-
|
|
100
|
-
## Out Of Scope
|
|
101
|
-
|
|
102
|
-
- Live provider, CI, or gateway ingestion (the layer consumes locally-supplied payloads only).
|
|
103
|
-
- Hosted multi-tenant control plane and federated sources.
|
|
104
|
-
- Automated kill-switch or revocation execution against real providers (remediations mutate the local inventory state only).
|
|
1
|
+
# Agent Inventory And Attack-Path Graph Threat Model
|
|
2
|
+
|
|
3
|
+
Applies to the local agent inventory layer: the persisted inventory store, attack-path graph, blast-radius analysis, detections, remediation workflow, open export, removal analysis, and ROI report.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
This threat model covers the inventory module added in the "agent inventory, attack-path graph, and indispensability" session:
|
|
8
|
+
|
|
9
|
+
- `inventory.json` local persisted store under `.ghostapi/`, created by `getDataPaths().inventoryStore`.
|
|
10
|
+
- Import of agents, tools, identities, providers, resources, side effects, credentials, and policies from `config`, `ci`, `gateway`, and `cloud` sources, each record carrying provenance (source, import time, importer) and freshness.
|
|
11
|
+
- The attack-path graph (`agent -> identity -> tool -> provider -> resource -> side effect`) with persisted, provenance- and freshness-carrying edges.
|
|
12
|
+
- Blast-radius analysis, detections, remediation proposals/applications, open export, removal analysis, and the ROI report.
|
|
13
|
+
|
|
14
|
+
The inventory layer adds no provider, credential, network egress, or hosted control-plane surface. All of it is local-first and synthetic. ROI and removal numbers are derived from imported counters and local records only; nothing is invented, and the pilot entry gate (a real team/enterprise pilot) is not yet met.
|
|
15
|
+
|
|
16
|
+
## Trust Boundaries
|
|
17
|
+
|
|
18
|
+
| Boundary | In scope | Out of scope |
|
|
19
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
20
|
+
| Operator identity | Every inventory action authenticates through an injectable authorizer and requires an explicit permission (`inventory.import` / `inventory.inspect` / `inventory.analyze` / `inventory.remediate` / `inventory.export`); the test authorizer binds issued identities by reference | Real authn/authz infrastructure |
|
|
21
|
+
| Import sources | Imports declare a source (`config` / `ci` / `gateway` / `cloud`) that is recorded as provenance on every record and edge | Live provider or CI credentials; the layer never reaches out to a source |
|
|
22
|
+
| Tenant isolation | All reads and writes are filtered by `tenantId`; graph, findings, export, attack paths, removal, and ROI are tenant-scoped | Multi-tenant federation |
|
|
23
|
+
| Store | Regular non-symlink JSON file, owner-only on POSIX, bounded bytes and record counts | Remote/distributed storage |
|
|
24
|
+
|
|
25
|
+
## Assets
|
|
26
|
+
|
|
27
|
+
- Agent inventory records and graph edges (`inventory.json`).
|
|
28
|
+
- Provenance and freshness metadata attached to every record and edge.
|
|
29
|
+
- Findings and remediation state (proposals, applications, rejections).
|
|
30
|
+
- The open export artifact (inventory, policies, eval scenario references, evidence metadata, removal analysis, ROI).
|
|
31
|
+
- Import-run and import-source ledger.
|
|
32
|
+
|
|
33
|
+
## Threats And Mitigations
|
|
34
|
+
|
|
35
|
+
### T1. Unauthenticated or unauthorized inventory writes
|
|
36
|
+
|
|
37
|
+
Every mutating and reading method calls `authorize` first: `validateOperator` on the authorizer result and an explicit permission check. The default authorizer (`createDisabledInventoryOperatorAuthorizer`) throws "not configured", so a controller with no authorizer cannot be used. Test identities are reference-bound; a plain `{}` is rejected as "not authenticated".
|
|
38
|
+
|
|
39
|
+
### T2. A poisoned import poisons the graph
|
|
40
|
+
|
|
41
|
+
`validateImportPayload` enforces strict schema, allowed source types, identifier/scope/hash/timestamp formats, bounded array sizes, and per-kind id uniqueness before anything is stored. Cross-references (e.g. an agent referencing an unknown identity) are validated against both the current import and the persisted state, and a reference to a record that does not exist rejects the whole import. Import payloads are canonicalized to a digest recorded on the import run and source.
|
|
42
|
+
|
|
43
|
+
### T3. Tenant data leaks across the graph or export
|
|
44
|
+
|
|
45
|
+
Every graph edge, record, finding, remediation, import run, and source is filtered by `operator.tenantId` in `snapshotFor`, `graphEdges`, `findAttackPaths`, `computeBlastRadius`, `computeFindings`, `computeCoverage`, `computeRemovalAnalysis`, `computeRoiReport`, and `export`. An edge can only be created from a record that belongs to the importing tenant. Cross-tenant leakage is covered by tests.
|
|
46
|
+
|
|
47
|
+
### T4. Provenance or freshness is missing or forged
|
|
48
|
+
|
|
49
|
+
Every record and edge carries `provenance` (`sourceId`, `sourceType`, `sourceName`, `importedAt`, `importedBy`) and `freshness` (`firstSeenAt`, `lastSeenAt`) built from the import payload source and the authenticated operator's principal id — never from client-supplied strings. `validateState` re-validates provenance/freshness on every read and write. Edge ids are derived from source/target/relation so re-imports refresh `lastSeenAt` instead of duplicating edges.
|
|
50
|
+
|
|
51
|
+
Edges have a freshness lifecycle. Attack-path and blast-radius analysis only use edges whose `lastSeenAt` is within the configured `edgeStaleDays` window, so a long-unseen relationship cannot be cited as current reachability evidence. On every import the importing tenant's stale edges are garbage-collected from the store, keeping the graph from accumulating frozen relationships that were never refreshed.
|
|
52
|
+
|
|
53
|
+
### T5. A remediation expands permissions instead of reducing them
|
|
54
|
+
|
|
55
|
+
`reduce_scope` proposals are rejected at proposal time unless the reduced scope list is a strict subset of the credential's current grant scopes and removes at least one scope; the same invariant is enforced again at apply time. `revoke` only ever sets a credential to revoked. `assign_owner` only sets an owner id. `onboard_through_gateway` only sets the gateway-managed flag. `create_eval` only records an eval scenario reference — and, when an `evalScenarioExists` resolver is configured, the referenced scenario must actually exist before the proposal is accepted, so a remediation cannot paper over a finding by pointing at a deleted scenario. No remediation path adds scopes or relaxes a control.
|
|
56
|
+
|
|
57
|
+
Applied remediations are re-verified on every analysis pass: a finding stays resolved only while the remediation remains effective. If a later import re-expands a reduced scope, revokes nothing, clears an assigned owner, un-gates an agent, or deletes the referenced eval scenario, the finding is re-opened instead of staying permanently marked resolved.
|
|
58
|
+
|
|
59
|
+
Remediation targets are validated against the tenant's live data before a proposal is accepted: an `environment` target must actually be referenced by at least one agent, identity, provider, resource, credential, or policy of the tenant, so a remediation cannot be proposed against a stale or invented environment.
|
|
60
|
+
|
|
61
|
+
### T6. The store is poisoned or exploded
|
|
62
|
+
|
|
63
|
+
The store is validated on every read (`validateState`) and write (`atomicWriteJson(this.path, validateState(state))`). Store bytes are capped at 8 MiB; every collection is bounded (`maxAgents`, `maxEdges` 6,000, `maxFindings` 3,000, etc.), and `assertStoreBounds` rejects edges and findings over their caps on every mutation. Import runs are rotated per tenant at `maxImports` (256), keeping the oldest runs trimmed rather than failing the write. The store must be a regular non-symlink file. Oversized or non-JSON stores are rejected, not partially loaded.
|
|
64
|
+
|
|
65
|
+
### T7. Secret-shaped or control-character data enters the inventory
|
|
66
|
+
|
|
67
|
+
Identifiers, scopes, and text are validated by regexes and by `sanitizeSecretString`: any value that contains a recognizable secret shape (e.g. `sk_live_*`) or control characters is rejected. Evidence metadata and reasons are bounded text.
|
|
68
|
+
|
|
69
|
+
### T8. ROI or removal analysis invents savings
|
|
70
|
+
|
|
71
|
+
The ROI report is built only from imported counters (latest per source) and from actually applied remediations in the store; unmeasured counters are reported as `null` and listed in `notMeasured`, and `basis` is `local_inventory_data_only`. The removal analysis reports only numbers computed from the local store and states plainly when nothing verifiable depends on GhostAPI. The heuristic blast-radius classification is explicitly labeled advisory, not proof of exploitability.
|
|
72
|
+
|
|
73
|
+
### T9. Attack-path graph is unbounded or cross-tenant
|
|
74
|
+
|
|
75
|
+
Edges are created only from the importing tenant's records, are keyed by a derived edge id, and are bounded (`maxEdges` 6,000). Path finding is always scoped to the tenant and to a single agent id. Incomplete paths (a record reference missing from the store) are counted separately and reported as incomplete, not silently dropped.
|
|
76
|
+
|
|
77
|
+
## Permissions And Capabilities Summary
|
|
78
|
+
|
|
79
|
+
| Action | Check |
|
|
80
|
+
| ----------------------------------------------------------------- | ------------------------------------------- |
|
|
81
|
+
| Import | operator authorizer + `inventory.import` |
|
|
82
|
+
| Inspect / graph / attack paths / blast radius / list remediations | operator authorizer + `inventory.inspect` |
|
|
83
|
+
| Analyze / removal analysis / ROI | operator authorizer + `inventory.analyze` |
|
|
84
|
+
| Propose / apply / reject remediations | operator authorizer + `inventory.remediate` |
|
|
85
|
+
| Export | operator authorizer + `inventory.export` |
|
|
86
|
+
|
|
87
|
+
## Limits
|
|
88
|
+
|
|
89
|
+
| Resource | Limit |
|
|
90
|
+
| ------------------------------------ | ------------------- |
|
|
91
|
+
| Store bytes | 8 MiB |
|
|
92
|
+
| Import sources / import runs | 64 / 256 |
|
|
93
|
+
| Agents / tools / identities | 500 / 500 / 500 |
|
|
94
|
+
| Providers / resources / side effects | 200 / 1,000 / 2,000 |
|
|
95
|
+
| Credentials / policies | 500 / 100 |
|
|
96
|
+
| Graph edges | 6,000 |
|
|
97
|
+
| Findings / remediations | 3,000 / 1,000 |
|
|
98
|
+
| References / scopes per record | 64 / 32 |
|
|
99
|
+
|
|
100
|
+
## Out Of Scope
|
|
101
|
+
|
|
102
|
+
- Live provider, CI, or gateway ingestion (the layer consumes locally-supplied payloads only).
|
|
103
|
+
- Hosted multi-tenant control plane and federated sources.
|
|
104
|
+
- Automated kill-switch or revocation execution against real providers (remediations mutate the local inventory state only).
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
# Local Kill Switch, Budget, And Blast-Radius Threat Model
|
|
2
|
-
|
|
3
|
-
## Scope
|
|
4
|
-
|
|
5
|
-
The Session 27 safety controller is a local, persisted coordination layer for the existing `ghostapi-synthetic` action path. It implements local kill switches, budgets, circuit breakers, bounded queue/dead-letter state, authenticated emergency API calls, audit records, and a scheduled synthetic game-day test. It does not introduce a provider, provider SDK/client, provider account, vault, credential, external HTTP, webhook/email/Slack transport, hosted control plane, or production side effect.
|
|
6
|
-
|
|
7
|
-
## Emergency Authority
|
|
8
|
-
|
|
9
|
-
`stop()`, `reenable()`, `configureBudget()`, and `configureCircuit()` require an injected `SafetyEmergencyAuthorizer`. The API accepts only verifier-issued operator objects with a stable principal and explicit permission. `safety.stop`, `safety.reenable`, and `safety.configure` are separate permissions. Empty, secret-shaped, or oversized reasons fail closed and every emergency decision is chained into the bounded local audit log.
|
|
10
|
-
|
|
11
|
-
There is intentionally no unauthenticated HTTP emergency endpoint, no CLI token parser, and no notification transport. A future deployment needs a separately reviewed identity authority and authenticated transport before exposing this emergency API remotely.
|
|
12
|
-
|
|
13
|
-
## Enforcement Semantics
|
|
14
|
-
|
|
15
|
-
- Switch scopes are `global`, `organization`, `project`, `environment`, `agent`, `workload`, `provider`, `operation`, and `risk_class`.
|
|
16
|
-
- Admission serializes against the persisted controller store. Budgets reserve action costs before execution, so parallel callers cannot oversubscribe money, requests, messages, mutations, deletes, token cost, concurrency, or velocity limits.
|
|
17
|
-
- A replay with the same idempotency key and action hash returns a replay lease without a second reservation. Reusing the key with a different action hash is denied.
|
|
18
|
-
- The action gateway calls the controller after plan/simulate and before its attempt record. The synthetic world calls the controller's `commit()` while it holds its own world lock. `commit()` is two-phase: a first mutate re-checks the lease, expiry, and kill-switch/circuit state; only then is the operation executed; a second mutate re-checks the lease and persists the `action.final_check` audit record ("remained active through commit") before returning. A kill switch that wins before the first phase blocks the local mutation. If the lease expires between the phases, `commit()` throws even though the operation already ran — the caller must not treat the action as successfully admitted.
|
|
19
|
-
- Leases are time-bounded: a reserved lease expires after `DEFAULT_LEASE_TTL_MS` (60 s by default, overridable via `leaseTtlMs`). Expired leases are pruned on the next admission, and both `assertActive()` and the `commit()` phases reject an expired lease. `complete()` is tolerant: it still records the outcome for a lease that expired after the operation, so the audit chain reflects what actually happened instead of dropping the terminal record.
|
|
20
|
-
- Kill-switch denials carry the stable error code `SAFETY_KILL_SWITCH` (and admit/commit errors are `SafetyControllerError`), so callers — including the scheduled game day — detect a kill-switch block structurally instead of by parsing message text.
|
|
21
|
-
- Queued actions have a fixed maximum of 100. A full queue returns backpressure. Stopping a matching scope moves queued records to the bounded dead-letter queue; no queued action is auto-retried. The dead-letter queue is also bounded, and when full the oldest entry is evicted so the store never grows without limit.
|
|
22
|
-
- Already approved is not already executable: approval artifacts still pass current policy, identity, idempotency, safety admission, and final-commit checks. An in-flight action that has crossed the final commit cannot be undone; GhostAPI does not claim rollback. A real provider would require idempotency and reconciliation semantics before an equivalent boundary is enabled.
|
|
23
|
-
- Failures, policy violations, latency threshold breaches, and reconciliation mismatches can open persisted circuit breakers. Open breakers deny new admission; no retry loop is created by the controller.
|
|
24
|
-
|
|
25
|
-
## Persistence And Limits
|
|
26
|
-
|
|
27
|
-
Critical state lives in `.ghostapi/safety-controller.json`, not process memory. It is strict-schema validated, bounded to 1 MiB, guarded as a regular non-symlink file, serialized with the existing file lock, atomically replaced, and covered by a bounded SHA-256 audit chain. This is local coordination and tamper evidence on one filesystem, not a distributed lock, immutable audit ledger, or defense against a malicious same-user actor who can modify both state and runtime.
|
|
28
|
-
|
|
29
|
-
## Game Day And Runbook
|
|
30
|
-
|
|
31
|
-
The scheduled GitHub workflow runs only `test/safetyController.test.ts`, which performs a local synthetic stop/re-enable drill and never creates a provider side effect. The operational procedure is in [`docs/operations/kill-switch-runbook.md`](../operations/kill-switch-runbook.md).
|
|
32
|
-
|
|
33
|
-
## Remaining Limits
|
|
34
|
-
|
|
35
|
-
- The controller does not authorize production execution, reconcile a provider outcome, revoke a provider credential, or prove a provider-side stop.
|
|
36
|
-
- Budget units are caller-supplied typed action metadata for the one local synthetic action; a future real action needs independently derived and provider-verified amounts/costs.
|
|
37
|
-
- Local timestamps use the process clock. A real deployment needs a trusted clock, durable audit sink, shared atomic store, reconciliation, and provider-specific final-side-effect guards.
|
|
1
|
+
# Local Kill Switch, Budget, And Blast-Radius Threat Model
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
The Session 27 safety controller is a local, persisted coordination layer for the existing `ghostapi-synthetic` action path. It implements local kill switches, budgets, circuit breakers, bounded queue/dead-letter state, authenticated emergency API calls, audit records, and a scheduled synthetic game-day test. It does not introduce a provider, provider SDK/client, provider account, vault, credential, external HTTP, webhook/email/Slack transport, hosted control plane, or production side effect.
|
|
6
|
+
|
|
7
|
+
## Emergency Authority
|
|
8
|
+
|
|
9
|
+
`stop()`, `reenable()`, `configureBudget()`, and `configureCircuit()` require an injected `SafetyEmergencyAuthorizer`. The API accepts only verifier-issued operator objects with a stable principal and explicit permission. `safety.stop`, `safety.reenable`, and `safety.configure` are separate permissions. Empty, secret-shaped, or oversized reasons fail closed and every emergency decision is chained into the bounded local audit log.
|
|
10
|
+
|
|
11
|
+
There is intentionally no unauthenticated HTTP emergency endpoint, no CLI token parser, and no notification transport. A future deployment needs a separately reviewed identity authority and authenticated transport before exposing this emergency API remotely.
|
|
12
|
+
|
|
13
|
+
## Enforcement Semantics
|
|
14
|
+
|
|
15
|
+
- Switch scopes are `global`, `organization`, `project`, `environment`, `agent`, `workload`, `provider`, `operation`, and `risk_class`.
|
|
16
|
+
- Admission serializes against the persisted controller store. Budgets reserve action costs before execution, so parallel callers cannot oversubscribe money, requests, messages, mutations, deletes, token cost, concurrency, or velocity limits.
|
|
17
|
+
- A replay with the same idempotency key and action hash returns a replay lease without a second reservation. Reusing the key with a different action hash is denied.
|
|
18
|
+
- The action gateway calls the controller after plan/simulate and before its attempt record. The synthetic world calls the controller's `commit()` while it holds its own world lock. `commit()` is two-phase: a first mutate re-checks the lease, expiry, and kill-switch/circuit state; only then is the operation executed; a second mutate re-checks the lease and persists the `action.final_check` audit record ("remained active through commit") before returning. A kill switch that wins before the first phase blocks the local mutation. If the lease expires between the phases, `commit()` throws even though the operation already ran — the caller must not treat the action as successfully admitted.
|
|
19
|
+
- Leases are time-bounded: a reserved lease expires after `DEFAULT_LEASE_TTL_MS` (60 s by default, overridable via `leaseTtlMs`). Expired leases are pruned on the next admission, and both `assertActive()` and the `commit()` phases reject an expired lease. `complete()` is tolerant: it still records the outcome for a lease that expired after the operation, so the audit chain reflects what actually happened instead of dropping the terminal record.
|
|
20
|
+
- Kill-switch denials carry the stable error code `SAFETY_KILL_SWITCH` (and admit/commit errors are `SafetyControllerError`), so callers — including the scheduled game day — detect a kill-switch block structurally instead of by parsing message text.
|
|
21
|
+
- Queued actions have a fixed maximum of 100. A full queue returns backpressure. Stopping a matching scope moves queued records to the bounded dead-letter queue; no queued action is auto-retried. The dead-letter queue is also bounded, and when full the oldest entry is evicted so the store never grows without limit.
|
|
22
|
+
- Already approved is not already executable: approval artifacts still pass current policy, identity, idempotency, safety admission, and final-commit checks. An in-flight action that has crossed the final commit cannot be undone; GhostAPI does not claim rollback. A real provider would require idempotency and reconciliation semantics before an equivalent boundary is enabled.
|
|
23
|
+
- Failures, policy violations, latency threshold breaches, and reconciliation mismatches can open persisted circuit breakers. Open breakers deny new admission; no retry loop is created by the controller.
|
|
24
|
+
|
|
25
|
+
## Persistence And Limits
|
|
26
|
+
|
|
27
|
+
Critical state lives in `.ghostapi/safety-controller.json`, not process memory. It is strict-schema validated, bounded to 1 MiB, guarded as a regular non-symlink file, serialized with the existing file lock, atomically replaced, and covered by a bounded SHA-256 audit chain. This is local coordination and tamper evidence on one filesystem, not a distributed lock, immutable audit ledger, or defense against a malicious same-user actor who can modify both state and runtime.
|
|
28
|
+
|
|
29
|
+
## Game Day And Runbook
|
|
30
|
+
|
|
31
|
+
The scheduled GitHub workflow runs only `test/safetyController.test.ts`, which performs a local synthetic stop/re-enable drill and never creates a provider side effect. The operational procedure is in [`docs/operations/kill-switch-runbook.md`](../operations/kill-switch-runbook.md).
|
|
32
|
+
|
|
33
|
+
## Remaining Limits
|
|
34
|
+
|
|
35
|
+
- The controller does not authorize production execution, reconcile a provider outcome, revoke a provider credential, or prove a provider-side stop.
|
|
36
|
+
- Budget units are caller-supplied typed action metadata for the one local synthetic action; a future real action needs independently derived and provider-verified amounts/costs.
|
|
37
|
+
- Local timestamps use the process clock. A real deployment needs a trusted clock, durable audit sink, shared atomic store, reconciliation, and provider-specific final-side-effect guards.
|