mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-03 14:56:50 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
327c10fd90 | ||
|
|
22b093aac5 | ||
|
|
25b90b0611 | ||
|
|
2786f9aa2d | ||
|
|
d41d52f17d | ||
|
|
4b8131fdd5 | ||
|
|
e92ee61c05 | ||
|
|
1f364522ca |
No files matched your search
+4
-8
@@ -22,21 +22,15 @@ SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
# SHANNON_AI_MODEL=amazon-bedrock:us.anthropic.claude-opus-4-8
|
||||
|
||||
# --- Custom Base URL ---------------------------------------------------------
|
||||
# Route through a proxy or gateway (LiteLLM, an internal endpoint).
|
||||
# Pick the block matching the API dialect your gateway speaks, and uncomment all
|
||||
# three lines. The provider prefix picks the dialect; the model id is whatever
|
||||
# name your gateway serves it under.
|
||||
|
||||
# Anthropic compatible - Anthropic Messages:
|
||||
# Anthropic Messages API:
|
||||
# SHANNON_AI_API_KEY=your-gateway-key-here
|
||||
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||
# SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
|
||||
# OpenAI compatible - Chat Completions (default) or Responses:
|
||||
# OpenAI Responses API:
|
||||
# SHANNON_AI_API_KEY=your-gateway-key-here
|
||||
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
||||
# SHANNON_AI_MODEL=openai:gpt-5.5
|
||||
# SHANNON_AI_OPENAI_FORMAT=responses
|
||||
|
||||
# --- Other provider ----------------------------------------------------------
|
||||
# Any other provider the Pi harness supports. Name it in SHANNON_AI_MODEL and
|
||||
@@ -44,6 +38,8 @@ SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
# and model at preflight.
|
||||
# SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3
|
||||
# SHANNON_AI_API_KEY=your-api-key-here
|
||||
# Optional: point that provider at a proxy or LLM gateway.
|
||||
# SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||
|
||||
# --- Misc --------------------------------------------------------------------
|
||||
# Forward /etc/hosts entries into the worker container.
|
||||
|
||||
@@ -121,8 +121,8 @@ body:
|
||||
- "xAI"
|
||||
- "AWS Bedrock"
|
||||
- "Custom base URL - Anthropic Messages"
|
||||
- "Custom base URL - OpenAI Chat Completions"
|
||||
- "Custom base URL - OpenAI Responses"
|
||||
- "Other provider (Pi catalogue)"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ pnpm biome:fix # Auto-fix lint, format, and import sorting
|
||||
|
||||
**Monorepo tooling:** pnpm workspaces, Turborepo for task orchestration, Biome for linting/formatting. TypeScript compiler options shared via `tsconfig.base.json` at the root. All packages extend it, overriding only `rootDir` and `outDir`. Shared devDependencies (`typescript`, `@types/node`, `turbo`, `@biomejs/biome`) are hoisted to the root workspace.
|
||||
|
||||
**Options:** `-c <file>` (YAML config), `-o <path>` (output directory), `-w <name>` (named workspace; auto-resumes if exists), `--pipeline-testing` (minimal prompts, 10s retries), `--keep-container` (preserve worker container after exit for log inspection), `--yes`/`-y` (skip the confirmation prompt on `stop`; required for non-interactive use; `reset` requires a typed `confirm` and cannot be skipped)
|
||||
**Options:** `-c <file>` (YAML config), `--models-config <file>` (pi `models.json` defining models pi's catalogue lacks), `-o <path>` (output directory), `-w <name>` (named workspace; auto-resumes if exists), `--pipeline-testing` (minimal prompts, 10s retries), `--keep-container` (preserve worker container after exit for log inspection), `--yes`/`-y` (skip the confirmation prompt on `stop`; required for non-interactive use; `reset` requires a typed `confirm` and cannot be skipped)
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -112,7 +112,7 @@ Published as `@keygraph/shannon` on npm. Contains Docker orchestration and a dir
|
||||
- `apps/cli/src/config/resolver.ts` — Cascading config (npx only): env vars → `~/.shannon/config.toml` (parsed with `smol-toml`)
|
||||
- `apps/cli/src/config/writer.ts` — TOML serialization and secure file persistence (0o600)
|
||||
- `apps/cli/src/commands/setup.ts` — Interactive TUI wizard (`@clack/prompts`) for provider credential setup (npx only)
|
||||
- `apps/cli/src/paths.ts` — Repo/config path resolution (any absolute or relative path)
|
||||
- `apps/cli/src/paths.ts` — Repo/config/models-config path resolution (any absolute or relative path). `MODELS_CONFIG_CONTAINER_PATH` is fixed at `/app/models.json` because the worker names it to pi rather than discovering it
|
||||
- `apps/cli/src/version.ts` — Version reporting (npx: `package.json` version; local: `git-<sha>`)
|
||||
- `apps/cli/src/tty.ts` — Terminal capability detection: `requireInteractive` guard (fails fast off-TTY instead of hanging on a prompt), `supportsColor` color gating (`NO_COLOR`/`FORCE_COLOR`), and `stdoutIsTerminal` for spinner/cursor output
|
||||
- `apps/cli/src/commands/` — Command handlers
|
||||
@@ -126,7 +126,7 @@ Infra (Temporal) runs via `docker-compose.yml`. Workers are ephemeral `docker ru
|
||||
- `docker-compose.yml` — Infra only: `shannon-temporal` (port 7233/8233). Network: `shannon-net`
|
||||
- `Dockerfile` — 2-stage build (builder + Chainguard Wolfi runtime). Uses pnpm. Entrypoint: `CMD ["node", "apps/worker/dist/temporal/worker.js"]`
|
||||
- No `docker-compose.docker.yml` — host gateway handled via `--add-host` flag in CLI
|
||||
- `/etc/hosts` forwarding — at worker spawn, `forwardEtcHostsFlags` in `apps/cli/src/docker.ts` reads the host's `/etc/hosts` and emits one `--add-host` flag per valid user-added entry. Loopback IPs (`127.x`, `::1`) are rewritten to `host-gateway`; IPv6 addresses are bracketed. Disable per-scan via `SHANNON_FORWARD_HOSTS=false`. No-op on Windows native (WSL2 reads its own `/etc/hosts` via the Linux path).
|
||||
- `/etc/hosts` forwarding — at worker spawn, `forwardEtcHostsFlags` in `apps/cli/src/docker.ts` reads the host's `/etc/hosts` and emits one `--add-host` flag per valid user-added entry. Loopback IPs (`127.x`, `::1`) are rewritten to `host-gateway`; IPv6 addresses are bracketed. Disable per-scan via `SHANNON_FORWARD_HOSTS=false`. Native Windows is refused at startup (`blockNativeWindows` in `apps/cli/src/index.ts`, pointing to the WSL2 guide in `docs/platforms.md`); WSL2 reads its own `/etc/hosts` via the Linux path.
|
||||
|
||||
### Worker Package (`apps/worker/`)
|
||||
- `apps/worker/src/paths.ts` — Centralized path constants (`PROMPTS_DIR`, `CONFIGS_DIR`, `WORKSPACES_DIR`)
|
||||
@@ -165,7 +165,7 @@ Around those phases:
|
||||
- **Configuration** — YAML configs in `apps/worker/configs/` use the closed JSON Schema in `config-schema.json`. Every fresh scan runs the fixed five analysis classes; there is no public class selector. `agentic_sast.enabled` is the only public agentic-SAST setting. Finding reconciliation runs on every scan and has no public setting of its own. Config also supports authentication (MFA/TOTP), URL/code rule scoping (`rules.avoid`/`rules.focus`), `exploit`, free-form `rules_of_engagement`, and post-hoc `report` options (`min_severity`, `min_confidence`, `guidance`, and exploit-only `sarif` output via `apps/worker/src/services/sarif-renderer.ts`, on by default for exploit runs and opt out with `report.sarif: "false"`). `code_path` avoid rules are enforced via the `@gotgenes/pi-permission-system` extension: `apps/worker/src/temporal/activities.ts:syncCodePathDenyRules` writes a global `path` deny config once per workflow (`apps/worker/src/ai/pi/permission-system.ts:syncPermissionSystemConfig`), and the executor loads the extension when that config is present (`apps/worker/src/ai/pi/pi-executor.ts`), so denies fire across every tool and child `task` session. Credential resolution — local mode: env vars → `./.env`; npx mode: env vars → `~/.shannon/config.toml` (via `npx @keygraph/shannon setup`)
|
||||
- **Agentic SAST progress** — Capella runs as a child workflow, so its activities are absent from the parent's `pendingActivities` and invisible to the CLI. The child signals each stage boundary up via `capellaStageProgress` (`apps/worker/src/temporal/shared.ts`); the parent's handler validates the payload and writes the child-supplied `startedAt` and `durationMs` directly to `operationalStages['agentic-sast:<stage>']`, so both the live `getProgress` query and the terminal result carry per-stage rows. Signalling is best-effort and every failure is swallowed — a closed or unreachable parent must never fail a SAST run. `CAPELLA_STAGE_LABELS` in `apps/worker/src/ai/sast/types.ts` is the one label table, shared by the scan log and the status tree; `CAPELLA_PROGRESS_STAGES` omits `export`, which runs no model and so never becomes a row. Scans predating the signal keep the aggregate `agentic-sast` span and render as a bare phase line
|
||||
- **Prompts** — Per-phase templates in `apps/worker/prompts/` with variable substitution (`{{TARGET_URL}}`, `{{CONFIG_CONTEXT}}`). Shared partials in `apps/worker/prompts/shared/` via `apps/worker/src/services/prompt-manager.ts`, including `_code-path-rules.txt` (focus/avoid `[FILE]`/`[GLOB]` routing) and `_rules-of-engagement.txt` (free-text engagement rules). When `exploit: false`, `apps/worker/src/services/findings-renderer.ts` deterministically converts each `*_exploitation_queue.json` into a `*_findings.md` for report assembly — no LLM in the loop
|
||||
- **Agent Harness (pi)** — Uses the **pi harness** (`@earendil-works/pi-coding-agent`, requires Node ≥ 22.19) via `apps/worker/src/ai/pi/pi-executor.ts` (`runPiPrompt` → `createAgentSession`). Retry is split in `apps/worker/src/ai/pi/retry-settings.ts`: pi's agent-level loop is off so Temporal owns agent restarts, while `provider.maxRetries` stays on — pi reads the `provider` block independently of the `enabled` flag — so transport faults are absorbed in-session rather than costing a full agent re-run. `maxRetryDelayMs` is left at pi's 60s default. One model runs every phase, named by `SHANNON_AI_MODEL=<provider>:<model-id>` (default `anthropic:claude-sonnet-4-6`). `apps/worker/src/ai/models.ts` parses the spec — splitting on the **first** colon only, so Bedrock IDs keep theirs — and resolves it through pi's `ModelRuntime`. pi ships the `CredentialStore` interface but no in-memory implementation (its own reads `auth.json` from disk), so `RuntimeCredentialStore` in that file supplies one: credentials arrive as env vars in an ephemeral container and must never touch disk. `createModelRuntime(providerId, apiKey)` builds the runtime; `allowModelNetwork` stays at its default `false` so a scan never blocks on a catalog refresh. `resolveModelSelection()` is **async** because `ModelRuntime.create()` is. Any pi-ai provider id is accepted — `parseModelSpec` no longer rejects against a hardcoded list, so pi's registry is the authority (an unknown provider/model surfaces as a clear "not found in pi registry" error at preflight, which points to the browsable catalogue at `pi.dev/models` — `PI_CATALOG_URL` in `apps/worker/src/ai/models.ts`, appended to the not-found errors and shown in the setup wizard's "Other provider" hint). Four providers are **curated** (`CURATED_PROVIDERS`: `anthropic`, `openai`, `xai`, `amazon-bedrock`) with their own credential variables, config sections, and setup flows; each provider's API key env var is declared once in `PROVIDER_API_KEY_ENV` — Shannon uses each vendor's own variable name (`OPENAI_API_KEY`, `XAI_API_KEY`, …), never an invented one; Bedrock's entry is `AWS_BEARER_TOKEN_BEDROCK`, paired with `AWS_REGION`, which preflight requires separately as provider config rather than a credential. Any other provider uses the **generic** credential path: `SHANNON_AI_API_KEY` (`GENERIC_API_KEY_ENV`) supplies the key for any provider whose credential is a plain API key. Curated providers' own variables take precedence over it, and it also works as a fallback for them — Bedrock is the sole exception (it authenticates through its AWS_ variables, so the generic key never stands in for it). The CLI forwards `SHANNON_AI_API_KEY` in `COMMON_FORWARD_VARS` (it is provider-neutral, binding to whatever `SHANNON_AI_MODEL` names, so the "only one provider configured" guard counts only named credentials), and stores it under a generic `[provider]` config.toml section (`provider.api_key`). `npx @keygraph/shannon setup` exposes this as the "Other provider" option: free-text provider id + model id + key (a curated provider id is rejected there, since it has its own option). `SHANNON_AI_BASE_URL` overrides the endpoint for any provider (proxies/gateways); the credential is unchanged. `pointAtGateway` (`apps/worker/src/ai/models.ts`) applies the one dialect change: behind a base URL, `openai` follows `SHANNON_AI_OPENAI_FORMAT` (`chat-completions` default, or `responses`). On `chat-completions` it switches the API to `openai-completions` and drops the catalogue's Responses-shaped `compat` block so pi's `detectCompat` derives completions settings; on `responses` the descriptor is unchanged but for the endpoint. `resolveGatewayFormat` rejects the variable when the provider is not `openai` or no base URL is set, since it cannot take effect there. All other providers keep their API. The CLI mirrors the accepted values in `apps/cli/src/model-spec.ts`, forwards the variable in `COMMON_FORWARD_VARS`, and maps it to `openai.format` in config.toml. `buildEnvFlags` forwards only the selected provider's credential into the worker container. The CLI mirrors the parse rule and the provider/credential tables in `apps/cli/src/model-spec.ts` (it cannot import from the worker package); the two must stay in sync. pi ships no JSON-schema output or `Task`/`TodoWrite` built-ins, so structured queues are captured via a `submit_exploitation_queue` custom tool (`apps/worker/src/ai/queue-schemas.ts`), and `task` (child sessions scoped to `read`, `grep`, `find`, `ls`, `write`, and `bash` — no nested `task` or collector tools; `CHILD_TOOLS` in `apps/worker/src/ai/pi/task-tool.ts`) + `todo_write` (`apps/worker/src/ai/pi/session-tools.ts`) are provided as custom tools; the per-phase collectors are pi custom tools (TypeBox `defineTool` in `apps/worker/src/collectors/`). Shannon sets no thinking configuration at all — no `thinkingLevel` is passed to any `createAgentSession` call, so pi's own default applies. There Line truncated
|
||||
- **Agent Harness (pi)** — Uses the **pi harness** (`@earendil-works/pi-coding-agent`, requires Node ≥ 22.19) via `apps/worker/src/ai/pi/pi-executor.ts` (`runPiPrompt` → `createAgentSession`). Retry is split in `apps/worker/src/ai/pi/retry-settings.ts`: pi's agent-level loop is off so Temporal owns agent restarts, while `provider.maxRetries` stays on — pi reads the `provider` block independently of the `enabled` flag — so transport faults are absorbed in-session rather than costing a full agent re-run. `maxRetryDelayMs` is left at pi's 60s default. One model runs every phase, named by `SHANNON_AI_MODEL=<provider>:<model-id>` (default `anthropic:claude-sonnet-4-6`). `apps/worker/src/ai/models.ts` parses the spec — splitting on the **first** colon only, so Bedrock IDs keep theirs — and resolves it through pi's `ModelRuntime`. pi ships the `CredentialStore` interface but no in-memory implementation (its own reads `auth.json` from disk), so `RuntimeCredentialStore` in that file supplies one: credentials arrive as env vars in an ephemeral container and must never touch disk. `createModelRuntime(providerId, apiKey)` builds the runtime with `allowModelNetwork: true`, so `ModelRuntime.create()` refreshes the model catalogue over the network at scan start and a freshly released model resolves without a `--models-config` file. The fetch is bounded (10s) and falls back to the static catalogue on timeout, so an unreachable catalogue endpoint cannot hang the scan. The refresh does not override a `--models-config`: pi reloads and re-applies that file as a config overlay on every refresh (it reloads `this.config` at the top of `refresh()`), so custom definitions still win over the fetched catalogue; the merge semantics below are unchanged, just layered over a fresher base. `resolveModelSelection()` is **async** because `ModelRuntime.create()` is. Any pi-ai provider id is accepted — `parseModelSpec` no longer rejects against a hardcoded list, so pi's registry is the authority (an unknown provider/model surfaces as a clear "not found in pi registry" error at preflight, which points to the browsable catalogue at `pi.dev/models` — `PI_CATALOG_URL` in `apps/worker/src/ai/models.ts`, appended to the not-found errors and shown in the setup wizard's "Other provider" hint). Four providers are **curated** (`CURATED_PROVIDERS`: `anthropic`, `openai`, `xai`, `amazon-bedrock`) with their own credential variables, config sections, and setup flows; each provider's API key env var is declared once in `PROVIDER_API_KEY_ENV` — Shannon uses each vendor's own variable name (`OPENAI_API_KEY`, `XAI_API_KEY`, …), never an invented one; Bedrock's entry is `AWS_BEARER_TOKEN_BEDROCK`, paired with `AWS_REGION`, which preflight requires separately as provider config rather than a credential. Any other provider uses the **generic** credential path: `SHANNON_AI_API_KEY` (`GENERIC_API_KEY_ENV`) supplies the key for any provider whose credential is a plain API key. Curated providers' own variables take precedence over it, and it also works as a fallback for them — Bedrock is the sole exception (it authenticates through its AWS_ variables, so the generic key never stands in for it). The CLI forwards `SHANNON_AI_API_KEY` in `COMMON_FORWARD_VARS` (it is provider-neutral, binding to whatever `SHANNON_AI_MODEL` names, so the "only one provider configured" guard counts only named credentials), and stores it under a generic `[provider]` config.toml section (`provider.api_key`). `npx @keygraph/shannon setup` exposes this as the "Other provider" option: free-text provider id + model id + key (a curated provider id is rejected there, since it has its own option). A model pi's catalogue does not carry, such as a self-hosted model, is reachable without an SDK bump: `--models-config <file>` mounts a pi `models.json` read-only at `/app/models.json`. The mount is the entire CLI→worker protocol: nothing is forwarded through the environment, and `modelsConfigPath()` detects the file at that fixed path, exactly as `piAuthPresent()` detects the pi auth mount whose flag is likewise not forwarded (`MODELS_CONFIG_CONTAINER_PATH` in the CLI and `MODELS_CONFIG_PATH` in `apps/worker/src/paths.ts` must stay in sync). `createModelRuntime` always names `modelsPath` explicitly — the mounted path, or **`null` when no config was supplied**, which switches models.json off outright. It is never left to pi's default of `<agent dir>/models.json`, because that dir is shared with the pi auth mount, so a file landing there must not silently contribute model definitions to a scan that did not ask for one. `modelsStorePath` is pinned to the agent dir alongside it, since pi otherwise derives it from `dirname(modelsPath)` and would try to write beside a read-only mount. Custom definitions merge over the built-in catalogue: a matching model id replaces the built-in entry, a new id is added alongside, and `modelOverrides` adjusts a built-in without replacing the proviLine truncated
|
||||
- **Pi Credential Reuse** — `SHANNON_USE_PI_AUTH=1` opts into reusing the host's Pi login, including an `openai-codex` ChatGPT Plus/Pro subscription (`SHANNON_AI_MODEL=openai-codex:<model-id>`) or an `xai` Grok subscription (`SHANNON_AI_MODEL=xai:<model-id>`); the mechanism is provider-agnostic and works for any Pi login. `apps/cli/src/env.ts` requires `~/.pi/agent/auth.json`; `start.ts` passes its path to `spawnWorker`, which mounts only that file read-write at `/tmp/.pi/agent/auth.json`. The flag itself is not forwarded: the worker detects the file with `piAuthPresent()` and passes its path to `ModelRuntime.create`. CLI and worker API-key presence checks are skipped on this path, but the normal preflight model probe still validates the credential. The image and UID-remapping entrypoint keep `/tmp/.pi/agent` owned by `pentest` so adjacent Pi/Shannon configuration remains writable. Refreshed OAuth state is persisted to the host for subsequent scans.
|
||||
- **Audit System** — Crash-safe append-only logging in `workspaces/{hostname}_{sessionId}/`. The run directory's top level holds the human-facing report in both formats (`Security-Assessment-Report.pdf` and `Security-Assessment-Report.md`, `FINAL_REPORT_PDF_FILENAME`/`FINAL_REPORT_MD_FILENAME` in `apps/worker/src/paths.ts`); everything else — deliverables, per-agent logs, prompts, `session.json`, `workflow.log`, and browser artifacts — is nested under a hidden `.shannon/` internals dir (`INTERNAL_DIR`) so a customer sees only the report. Audit path helpers route through `generateInternalPath` (`apps/worker/src/audit/utils.ts`); the CLI nests the overlay backing dirs under the same `.shannon/` (`apps/cli/src/docker.ts`, `start.ts`). `session.json`/`workflow.log` reads use dual-read resolvers (`resolveSessionJsonPath`, `resolveRunFile`) that prefer `.shannon/` and fall back to the legacy run-root layout, so pre-restructure workspaces stay listable (`scans`/`logs`) without migration. A pre-restructure workspace cannot be resumed: `classifyWorkspaceLaunch` (`apps/cli/src/commands/start.ts`) requires `.shannon/launch.json`, and its absence fails the launch as "created by an earlier version of Shannon" before anything on disk is touched. There is no in-place migration — the workspace's files and report are left untouched, and the operator starts a new scan under a different `-w` name. The report agent writes structured findings to `report.json`, from which `report-renderer.ts` renders the assembled markdown and `report-json-adapter.ts` produces the Typst-shaped JSON that `pdf-renderer.ts` compiles into `comprehensive_security_assessment_report.pdf` using the bundled `apps/worker/templates/typst/report.typ` template (the `typst` binary is installed in the worker image). `copyReportToRunRoot` (`apps/worker/src/services/reporting.ts`) surfaces both the PDF and the markdown to the run root as `Security-Assessment-Report.pdf` and `Security-Assessment-Report.md`; the deliverables-dir copies remain as the git-checkpointed sources. PDF compilation is best-effort — a failure is logged and the run still completes. WorkflowLogger (`apps/worker/src/audit/workflow-logger.ts`) provides unified human-readable per-workflow logs, backed by LogStream (`apps/worker/src/audit/log-stream.ts`) shared stream primitive. Every combined-log line is also projected into a per-agent file under `.shannon/agents/<slug>.log` (one per pipeline agent, one per Capella stage; subagents fold into the parent's file, and a stage's concurrent sessions share its file with an inline session label). The projection boundary is `apps/worker/src/audit/actor-projection.ts` (`projectActor` maps a `TraceActor` to its combined prefix and owning file slug — slugs come only from closed fields); fan-out is best-effort and never blocks the canonical combined log. A lifecycle owner holds a `LogStream` lease per agent file (the pipeline agent's `logAgent` span, or a Capella stage activity's `try/finally`) so per-line writes ride the reference count; `CapellaStageTrace.drain()` flushes a stage's trace queue before its activity returns. The CLI tails one file with `shannon logs --agent <name>` (`--list-agents` to enumerate); the default `shannon logs` path is unchanged
|
||||
- **Deliverables** — Saved to `.shannon/deliverables/` in the target repo via the `save-deliverable` CLI script (`apps/worker/src/scripts/save-deliverable.ts`)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
> [!NOTE]
|
||||
> **Shannon 3.0 is live:** deeper security code analysis, a rebuilt terminal experience, native CI/CD workflows, professional PDF reports, and SARIF—still fully open source, self-hosted, and bring-your-own-model.
|
||||
> **[Shannon 3.0 is live](https://github.com/KeygraphHQ/shannon/discussions/439):** deeper security code analysis, more thoroughly vetted findings, a rebuilt CLI, native CI/CD, professional PDF reports, and SARIF.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -17,6 +17,14 @@ It analyzes your source code, identifies attack paths, and executes real exploit
|
||||
|
||||
**This repository is Shannon Open Source: the full agent, run locally from your command line.**
|
||||
|
||||
<p><strong>Launch Shannon</strong></p>
|
||||
|
||||
```bash
|
||||
npx @keygraph/shannon@latest
|
||||
```
|
||||
|
||||
<sub>The interactive launcher will guide you through setup and your first pentest.</sub>
|
||||
|
||||
---
|
||||
|
||||
<a href="https://discord.gg/9ZqQPuhJB7"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/discord_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/discord_button_light.png"><img src="./assets/discord_button_light.png" height="40" alt="Join Discord"></picture></a> <a href="https://keygraph.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/keygraph_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/keygraph_button_light.png"><img src="./assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></picture></a>
|
||||
@@ -49,6 +57,7 @@ It analyzes your source code, identifies attack paths, and executes real exploit
|
||||
- [Documentation](#documentation)
|
||||
- [Safety, Scope, and Limitations](#safety-scope-and-limitations)
|
||||
- [License](#license)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [About Keygraph](#about-keygraph)
|
||||
- [Community and Support](#community-and-support)
|
||||
- [Common Questions](#common-questions)
|
||||
@@ -69,19 +78,29 @@ Shannon analyzes your web application's source code to identify potential attack
|
||||
|
||||
Shannon is the agent. This repository is Shannon Open Source, the standalone pentester you run yourself. The same Shannon also powers the [Keygraph platform](https://keygraph.io), Keygraph's commercial pentesting product. See [Editions](#editions) for how the two compare.
|
||||
|
||||
### Why Shannon Exists
|
||||
<a id="why-shannon-exists"></a>
|
||||
<details>
|
||||
<summary><strong>Why Shannon Exists</strong></summary>
|
||||
|
||||
Thanks to tools like Claude Code and Cursor, your team ships code non-stop. But your penetration test? That happens once a year. This creates a massive security gap. For the other 364 days, you could be unknowingly shipping vulnerabilities to production.
|
||||
|
||||
Shannon closes that gap by providing on-demand, automated penetration testing that can run against every build or release.
|
||||
|
||||
### Why "Shannon"?
|
||||
</details>
|
||||
|
||||
<a id="why-shannon"></a>
|
||||
<details>
|
||||
<summary><strong>Why "Shannon"?</strong></summary>
|
||||
|
||||
It's named after Claude Shannon, the father of information theory. At its core, pentesting is an information problem: every probe reduces uncertainty about a system's state. The best tools maximize the signal gained from every request, turning those bits of knowledge into an exploit path.
|
||||
|
||||
Also, we wanted you to be able to say, "Hey Claude, run Shannon" to find all the security flaws in your vibe-coded app.
|
||||
|
||||
### Not a replacement for human pentesters
|
||||
</details>
|
||||
|
||||
<a id="not-a-replacement-for-human-pentesters"></a>
|
||||
<details>
|
||||
<summary><strong>Not a replacement for human pentesters</strong></summary>
|
||||
|
||||
Shannon is built to work alongside expert pentesters and red teamers, not replace them. Great pentesters understand the business, chain attacks in ways nobody anticipated, and bring years of judgment that current models can't match.
|
||||
|
||||
@@ -89,11 +108,13 @@ Shannon solves a different problem: there is far more software to test than secu
|
||||
|
||||
Shannon shifts pentesting left into the software development lifecycle (SDLC). Use it to run exploitation-backed tests against staging environments and releases at the cadence they actually ship, and save expert human time for the risks that need someone who knows the organization.
|
||||
|
||||
</details>
|
||||
|
||||
## Shannon in Action
|
||||
|
||||

|
||||
|
||||
Penetration test reports from Shannon Open Source scanning Photoview 2.4.0. Read the [announcement][announcement] and the full [benchmark writeup][benchmark] for methodology, cost, and the comparison against Aikido and XBOW.
|
||||
These reports are from Shannon Open Source scans of Photoview 2.4.0, one of the applications in Doyensec's comparison of Aikido and XBOW. We ran Shannon against the same application version and evaluated its results separately. Read the [Doyensec study](https://doyensec.com/resources/ComparingAIApplicationSecurityTestingPlatforms_Doyensec.pdf) and our [Shannon follow-up comparison](docs/shannon-xbow-aikido-benchmark.md) for the methodology, limitations, costs, and results.
|
||||
|
||||
|
||||
| Model | Report | SARIF |
|
||||
@@ -102,12 +123,6 @@ Penetration test reports from Shannon Open Source scanning Photoview 2.4.0. Read
|
||||
| Grok 4.6 | [View report](benchmark/photoview-grok-4-6.pdf) | [SARIF](benchmark/photoview-grok-4-6.sarif) |
|
||||
| Claude Opus 5 | [View report](benchmark/photoview-opus-5.pdf) | [SARIF](benchmark/photoview-opus-5.sarif) |
|
||||
|
||||
[announcement]: https://github.com/KeygraphHQ/shannon/discussions/439
|
||||
[benchmark]: docs/shannon-xbow-aikido-benchmark.md
|
||||
|
||||
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -116,7 +131,7 @@ Penetration test reports from Shannon Open Source scanning Photoview 2.4.0. Read
|
||||
|
||||
- **Docker**: required for the worker container.
|
||||
- **Node.js 18+**: required for the recommended `npx` workflow.
|
||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, [any other provider](docs/ai-providers.md#any-other-provider) in the harness catalogue, and any endpoint that speaks the Anthropic Messages API or the OpenAI Chat Completions or Responses API through a [custom base URL](docs/ai-providers.md#custom-base-url). You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic. See [AI providers](docs/ai-providers.md#suggested-models) for suggested model IDs.
|
||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and [any other provider](docs/ai-providers.md#any-other-provider) in the harness catalogue — each of which you can point at a proxy or LLM gateway through a [custom base URL](docs/ai-providers.md#custom-base-url), and a model the catalogue does not carry can be described with a [custom model configuration](docs/ai-providers.md#custom-model-configuration). You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic. See [AI providers](docs/ai-providers.md#suggested-models) for suggested model IDs.
|
||||
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run - see [AI providers](docs/ai-providers.md#cyber-safeguards-do-this-before-your-first-scan).
|
||||
|
||||
|
||||
@@ -151,17 +166,17 @@ For source builds, authenticated scans, provider-specific setup, and platform no
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- **No exploit, no report**: Shannon includes a vulnerability only after validating it with a working, reproducible proof of concept—eliminating the speculative warnings typical of scanners.
|
||||
- **Advanced security code analysis**: Before it sends a single payload, Shannon reads the codebase and builds a picture of the application: architecture, trust boundaries, exposed interfaces, data flows, and the assets worth attacking. From there it opens targeted investigations and filters the candidates they turn up. What survives goes to the live pentesting agents.
|
||||
- **Autonomous execution**: Shannon launches reconnaissance, vulnerability analysis, exploitation, and report generation from a single command.
|
||||
- **Live terminal experience**: A rebuilt CLI makes scans easy to configure and shows agent progress and clean results without requiring operators to inspect the underlying orchestration logs.
|
||||
- **Authenticated testing**: configuration files can describe login flows, test credentials, TOTP, email-based login flows, focus areas, and rules of engagement.
|
||||
- **OWASP-focused coverage**: Shannon targets exploitable Injection, XSS, SSRF, Broken Authentication, and Broken Authorization issues.
|
||||
- **Resumable workspaces**: Shannon can resume interrupted runs without re-running completed agents.
|
||||
- **Native CI/CD integrations**: Run Shannon through the official GitHub Action or reusable GitLab CI/CD component. Preserve reports, SARIF, and logs as pipeline artifacts; publish findings into native security workflows; and gate releases only on vulnerabilities Shannon actually demonstrates.
|
||||
- **Professional and machine-readable reports**: Shannon generates evidence-rich PDF and Markdown reports plus structured JSON and SARIF 2.1.0. SARIF is enabled by default on exploit-mode scans and can be disabled with `report.sarif: "false"`.
|
||||
- **Bring your own key, provider-agnostic**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and any endpoint speaking the Anthropic Messages API or the OpenAI Chat Completions or Responses API, including self-hosted models served through Ollama, vLLM, or LM Studio and gateways such as OpenRouter and LiteLLM. You supply the credentials and choose exactly where model traffic goes. Local and self-hosted models are supported.
|
||||
- **Private by design**: Shannon runs inside your infrastructure and writes results to a local workspace. Model requests go straight to the provider or endpoint you configure, and they carry source and application context with them, so choose that endpoint deliberately. Point Shannon at a local model endpoint and nothing leaves your environment.
|
||||
- **No exploit, no report**: Reports only vulnerabilities confirmed with a reproducible proof of concept, reducing speculative scanner noise.
|
||||
- **Advanced code analysis**: Maps architecture, trust boundaries, interfaces, data flows, and critical assets before sending credible attack paths to live pentesting agents.
|
||||
- **Autonomous execution**: Runs reconnaissance, analysis, exploitation, and reporting from a single command.
|
||||
- **Live terminal experience**: Simplifies scan setup and shows agent progress and results without exposing orchestration logs.
|
||||
- **Authenticated testing**: Supports credentials, login flows, TOTP, email authentication, focus areas, and rules of engagement through configuration.
|
||||
- **OWASP-focused coverage**: Tests for exploitable injection, XSS, SSRF, broken authentication, and broken authorization.
|
||||
- **Resumable workspaces**: Resumes interrupted scans without repeating completed work.
|
||||
- **Native CI/CD integrations**: Runs through the official GitHub Action or GitLab CI/CD component, preserves artifacts, publishes findings, and gates releases on proven vulnerabilities.
|
||||
- **Multi-format reports**: Produces evidence-rich PDF and Markdown reports plus JSON and SARIF 2.1.0. SARIF is enabled by default for exploit-mode scans.
|
||||
- **Provider agnostic and BYOK**: Supports Anthropic, OpenAI, xAI, AWS Bedrock, compatible APIs and LLM gateways, and local models served through Ollama, vLLM, or LM Studio.
|
||||
- **Private by design**: Runs in your infrastructure, stores results locally, and sends model requests directly to your chosen endpoint. A local endpoint keeps data inside your environment.
|
||||
|
||||
|
||||
|
||||
@@ -220,24 +235,11 @@ See the [Shannon GitHub Action documentation](https://github.com/KeygraphHQ/shan
|
||||
|
||||
## Editions
|
||||
|
||||
**Shannon Open Source** is the complete autonomous pentester for developers and security teams. It is optimized for fast local and CI/CD runs: understand the application, execute real attacks, and report only proven vulnerabilities.
|
||||
**Shannon Open Source** is a complete autonomous pentester, especially well suited to individual developers and small teams running focused security tests locally or in CI/CD.
|
||||
|
||||
**Keygraph Enterprise Platform** turns Shannon's proof engine into an organization-wide AppSec program, adding exhaustive analysis, centralized vulnerability management, automated remediation, enterprise governance, and continuous operation at scale.
|
||||
**Keygraph Enterprise Platform** is for organizations that need a shared platform for continuous agentic pentesting/AppSec across many teams, repositories, and environments. It centralizes deeper analysis, vulnerability management, remediation, verification, governance, and reporting so teams do not have to assemble and maintain those workflows themselves.
|
||||
|
||||
|
||||
| | Shannon Open Source | Keygraph Enterprise Platform |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Best for | Local and CI/CD pentesting | Continuous AppSec across teams and repositories |
|
||||
| Security analysis | Multi-stage agentic review models architecture, trust boundaries, and data flows, filters candidate vulnerabilities, and hands the survivors to live pentesting agents | Exhaustive parsed-code agentic SAST: persistent Code Property Graphs, interprocedural source-to-sink and sanitizer modeling, cross-repository context, exploit-chain analysis, and business-logic testing |
|
||||
| Additional coverage | Not included | SCA with reachability, secrets scanning, and business-logic testing |
|
||||
| AppSec operations | N/A — standalone CLI | Canonical findings, deduplication, SLAs, analytics, automated remediation, and targeted verification |
|
||||
| Governance | N/A — local, single-operator CLI | SSO, SCIM, granular access control, APIs, and full audit logging |
|
||||
| Deployment | Self-hosted, air-gapped, BYOM, AGPL-3.0 | On-premises or air-gapped, granular model routing, commercial support |
|
||||
|
||||
|
||||
Shannon Open Source is not a trial edition. Choose Keygraph Enterprise when you need deeper analysis and a governed, closed-loop AppSec program.
|
||||
|
||||
[Explore the Keygraph Enterprise Platform →](docs/keygraph-platform.md)
|
||||
[Learn about the Keygraph Enterprise Platform and compare editions →](docs/keygraph-platform.md)
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -284,7 +286,7 @@ Use these guides for operational detail:
|
||||
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Source build and CLI commands](docs/development.md) | Cloning, building, common commands, output paths, and local development. |
|
||||
| [Configuration](docs/configuration.md) | Authenticated testing, login flows, rules of engagement, and report filters. |
|
||||
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock, and any other Pi-supported provider), and custom gateways. |
|
||||
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock, and any other Pi-supported provider), and custom LLM gateways. |
|
||||
| [Platforms and networking](docs/platforms.md) | Windows/WSL2, Linux, macOS, Docker networking, local apps, and custom hostnames. |
|
||||
| [Workspaces and resuming](docs/workspaces.md) | Naming workspaces, resuming interrupted scans, and workspace storage. |
|
||||
| [Safety and limitations](docs/safety.md) | Authorized-use requirements, non-production guidance, mutative effects, cost, and model caveats. |
|
||||
@@ -304,7 +306,7 @@ Important limitations:
|
||||
|
||||
- Shannon Open Source is tuned for fast, code-informed pentesting in everyday development and CI/CD. Exhaustive agentic SAST, broader scanner coverage, centralized governance, and full-lifecycle vulnerability management are delivered through the Keygraph Enterprise Platform.
|
||||
- Findings still require human review. LLM-generated reports can contain weakly supported or incorrect details.
|
||||
- Anthropic, OpenAI, xAI, and AWS Bedrock are built-in providers, and any Anthropic Messages API or OpenAI Chat Completions or Responses API endpoint works through a custom base URL. Model capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker results.
|
||||
- Anthropic, OpenAI, xAI, and AWS Bedrock are built-in providers, and any other provider in the harness catalogue works too — each reachable through a custom base URL that points it at a proxy or LLM gateway. Model capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker results.
|
||||
- A full run can take roughly 1 to 1.5 hours and may incur LLM API costs depending on model pricing and application complexity.
|
||||
- Do not scan untrusted or adversarial codebases. AI-powered tools that read source code can be exposed to prompt injection.
|
||||
|
||||
@@ -318,6 +320,14 @@ Commercial and enterprise licensing is available for organizations that need dif
|
||||
|
||||
For commercial licensing, contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Thanks to [Pi](https://github.com/earendil-works/pi),
|
||||
[Playwright CLI](https://github.com/microsoft/playwright-cli),
|
||||
and [Mantis](https://github.com/google/mantis).
|
||||
|
||||
See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for licensing and attribution details.
|
||||
|
||||
## About Keygraph
|
||||
|
||||
**Keygraph** is the company behind Shannon. It also builds the **Keygraph platform**, the commercial agentic pentesting product that closes the full AppSec lifecycle and runs an enhanced build of Shannon as its pentesting engine.
|
||||
@@ -365,14 +375,14 @@ Yes. Shannon emits SARIF 2.1.0, the OASIS standard format for static analysis re
|
||||
|
||||
### Which AI providers does Shannon support?
|
||||
|
||||
Anthropic, OpenAI, xAI, and AWS Bedrock are built in and configured directly by provider ID. Beyond those, Shannon runs on any endpoint that implements the Anthropic Messages API or the OpenAI Chat Completions or Responses API, reached through a custom base URL. The rule is the API format, not the vendor. Shannon uses a single unified model setting throughout a pentest.
|
||||
Anthropic, OpenAI, xAI, and AWS Bedrock are built in and configured directly by provider ID. Beyond those, Shannon runs on any provider in the Pi harness catalogue, named the same `<provider>:<model-id>` way. Any provider can be pointed at a proxy or LLM gateway through a custom base URL, which overrides only the endpoint and keeps that provider's API dialect. A model the catalogue does not carry, such as one a router or gateway serves under its own ID, or a self-hosted model, is described in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file and passed with `--models-config`. Shannon uses a single unified model setting throughout a pentest.
|
||||
|
||||
### Can I run Shannon on a local or self-hosted model?
|
||||
|
||||
Shannon works with local models served through Ollama, vLLM, or LM Studio, which expose an OpenAI-compatible endpoint, as well as routers such as OpenRouter and gateways such as LiteLLM. Point Shannon at the endpoint with a custom base URL. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves. See [AI providers](docs/ai-providers.md#custom-base-url).
|
||||
Shannon works with local models served through Ollama, vLLM, or LM Studio, which expose an OpenAI-compatible endpoint, as well as routers such as OpenRouter and LLM gateways such as LiteLLM. A model the harness catalogue does not carry, which most self-hosted models are, is described in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file passed with `--models-config`; routers and gateways can also be reached with a custom base URL. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves. See [Local and self-hosted models](docs/ai-providers.md#local-and-self-hosted-models).
|
||||
|
||||
### Does Shannon actually exploit vulnerabilities, or just scan?
|
||||
|
||||
Shannon executes real exploits. It reports a finding only when it has produced a working proof-of-concept, and discards hypotheses it cannot prove. It is a pentester, not a passive scanner.
|
||||
|
||||
**Built by [Keygraph](https://keygraph.io)**
|
||||
**Built by [Keygraph](https://keygraph.io)**
|
||||
+1
-1
@@ -22,7 +22,7 @@ It analyzes your source code, identifies attack paths, and executes real exploit
|
||||
|
||||
- **Docker**: required for the worker container.
|
||||
- **Node.js 18+**: required for the recommended `npx` workflow.
|
||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, any other provider in the harness catalogue, and any endpoint that speaks the Anthropic Messages API or the OpenAI Chat Completions or Responses API through a custom base URL. You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic.
|
||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and any other provider in the harness catalogue — each of which you can point at a proxy or LLM gateway through a custom base URL. You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic.
|
||||
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run.
|
||||
|
||||
### Run Shannon
|
||||
|
||||
@@ -10,7 +10,7 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { type ShannonConfig, saveConfig } from '../config/writer.js';
|
||||
import { CURATED_PROVIDERS, type CuratedProviderId, isCuratedProvider, type OpenAiFormat } from '../model-spec.js';
|
||||
import { CURATED_PROVIDERS, type CuratedProviderId, isCuratedProvider } from '../model-spec.js';
|
||||
import { displaySplash } from '../splash.js';
|
||||
import { requireInteractive } from '../tty.js';
|
||||
import { getVersion } from '../version.js';
|
||||
@@ -22,24 +22,16 @@ const CUSTOM_BASE_URL = '__custom_base_url__';
|
||||
const OTHER_PROVIDER = '__other_provider__';
|
||||
|
||||
/**
|
||||
* Wire formats reachable through the gateway route. The format picks the provider
|
||||
* that supplies the credential, and for OpenAI it also picks which of the two
|
||||
* OpenAI APIs Shannon calls.
|
||||
* API dialects reachable through the gateway route. The dialect picks the provider
|
||||
* that supplies the credential and names the wire protocol the endpoint must speak.
|
||||
*/
|
||||
const GATEWAY_DIALECTS: readonly {
|
||||
value: string;
|
||||
label: string;
|
||||
provider: 'anthropic' | 'openai';
|
||||
format?: OpenAiFormat;
|
||||
}[] = [
|
||||
{ value: 'anthropic', label: 'Anthropic Messages', provider: 'anthropic' },
|
||||
{
|
||||
value: 'openai-chat-completions',
|
||||
label: 'OpenAI Chat Completions',
|
||||
provider: 'openai',
|
||||
format: 'chat-completions',
|
||||
},
|
||||
{ value: 'openai-responses', label: 'OpenAI Responses', provider: 'openai', format: 'responses' },
|
||||
{ value: 'openai', label: 'OpenAI Responses', provider: 'openai' },
|
||||
];
|
||||
|
||||
/** Suggested models per curated provider, best-first. Free-text entry accepts any model in the provider's catalogue. */
|
||||
@@ -78,7 +70,11 @@ export async function setup(): Promise<void> {
|
||||
{ value: 'openai' as const, label: 'OpenAI', hint: 'GPT models' },
|
||||
{ value: 'xai' as const, label: 'xAI', hint: 'Grok models' },
|
||||
{ value: 'amazon-bedrock' as const, label: 'AWS Bedrock', hint: 'Claude models via AWS' },
|
||||
{ value: CUSTOM_BASE_URL as typeof CUSTOM_BASE_URL, label: 'Custom Base URL', hint: 'your own proxy or gateway' },
|
||||
{
|
||||
value: CUSTOM_BASE_URL as typeof CUSTOM_BASE_URL,
|
||||
label: 'Custom Base URL',
|
||||
hint: 'route through a proxy or LLM gateway',
|
||||
},
|
||||
{
|
||||
value: OTHER_PROVIDER as typeof OTHER_PROVIDER,
|
||||
label: 'Other provider',
|
||||
@@ -88,20 +84,21 @@ export async function setup(): Promise<void> {
|
||||
});
|
||||
if (p.isCancel(selected)) return cancelAndExit();
|
||||
|
||||
// 2. Credentials — and, on the gateway route, the endpoint and its dialect.
|
||||
const { provider, config, gateway } = await setupSelection(selected);
|
||||
// 2. Credentials, and any endpoint override. A base URL overrides the endpoint
|
||||
// for whichever provider is chosen — the curated gateway route names it via
|
||||
// the dialect, the "Other provider" route asks for it directly.
|
||||
const { provider, config, baseUrl } = await setupSelection(selected);
|
||||
|
||||
// 3. The model that runs every phase.
|
||||
const modelId = await promptModel(provider);
|
||||
config.core = { ...config.core, model: `${provider}:${modelId}` };
|
||||
if (gateway) config.core = { ...config.core, base_url: gateway.baseUrl };
|
||||
if (baseUrl) config.core = { ...config.core, base_url: baseUrl };
|
||||
|
||||
saveConfig(config);
|
||||
|
||||
const configPath = path.join(SHANNON_HOME, 'config.toml');
|
||||
const summary = [`Provider ${provider}`, `Model ${modelId}`];
|
||||
if (gateway) summary.push(`Endpoint ${gateway.baseUrl}`);
|
||||
if (gateway?.format) summary.push(`API ${gateway.format}`);
|
||||
if (baseUrl) summary.push(`Endpoint ${baseUrl}`);
|
||||
|
||||
p.log.success(`Configuration saved to ${configPath}`);
|
||||
p.log.info(summary.join('\n'));
|
||||
@@ -111,7 +108,7 @@ export async function setup(): Promise<void> {
|
||||
interface Selection {
|
||||
provider: string;
|
||||
config: ShannonConfig;
|
||||
gateway?: GatewaySetup;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
/** Resolve the provider selection into a provider id and its credential config. */
|
||||
@@ -120,7 +117,7 @@ async function setupSelection(
|
||||
): Promise<Selection> {
|
||||
if (selected === CUSTOM_BASE_URL) {
|
||||
const gateway = await setupGateway();
|
||||
return { provider: gateway.provider, config: gateway.config, gateway };
|
||||
return { provider: gateway.provider, config: gateway.config, baseUrl: gateway.baseUrl };
|
||||
}
|
||||
if (selected === OTHER_PROVIDER) {
|
||||
return setupOtherProvider();
|
||||
@@ -144,6 +141,8 @@ async function setupProvider(provider: CuratedProviderId): Promise<ShannonConfig
|
||||
/**
|
||||
* Any pi provider Shannon does not curate. The id is free text — the worker's
|
||||
* preflight validates it — and the key is stored generically as SHANNON_AI_API_KEY.
|
||||
* An optional base URL points that provider at a proxy or LLM gateway; left blank, the
|
||||
* provider's own endpoint is used.
|
||||
*/
|
||||
async function setupOtherProvider(): Promise<Selection> {
|
||||
p.log.info('Browse supported providers and models at https://pi.dev/models');
|
||||
@@ -159,7 +158,13 @@ async function setupOtherProvider(): Promise<Selection> {
|
||||
if (p.isCancel(provider)) return cancelAndExit();
|
||||
|
||||
const apiKey = await promptSecret('Enter the API key');
|
||||
return { provider: provider.trim(), config: { provider: { api_key: apiKey } } };
|
||||
const baseUrl = await promptOptionalBaseUrl();
|
||||
|
||||
return {
|
||||
provider: provider.trim(),
|
||||
config: { provider: { api_key: apiKey } },
|
||||
...(baseUrl && { baseUrl }),
|
||||
};
|
||||
}
|
||||
|
||||
// === Provider Setup Flows ===
|
||||
@@ -200,11 +205,10 @@ interface GatewaySetup {
|
||||
provider: CuratedProviderId;
|
||||
config: ShannonConfig;
|
||||
baseUrl: string;
|
||||
format?: OpenAiFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway route: the endpoint decides where requests go, but the format still
|
||||
* Gateway route: the endpoint decides where requests go, but the dialect still
|
||||
* picks a real provider, because that is what supplies the credential and the
|
||||
* wire protocol.
|
||||
*/
|
||||
@@ -236,11 +240,9 @@ async function setupGateway(): Promise<GatewaySetup> {
|
||||
|
||||
const authToken = await promptSecret('Enter the auth token for the endpoint');
|
||||
const config: ShannonConfig =
|
||||
provider === 'anthropic'
|
||||
? { anthropic: { api_key: authToken } }
|
||||
: { openai: { api_key: authToken, ...(dialect.format && { format: dialect.format }) } };
|
||||
provider === 'anthropic' ? { anthropic: { api_key: authToken } } : { openai: { api_key: authToken } };
|
||||
|
||||
return { provider, config, baseUrl, ...(dialect.format && { format: dialect.format }) };
|
||||
return { provider, config, baseUrl };
|
||||
}
|
||||
|
||||
// === Model Selection ===
|
||||
@@ -308,6 +310,31 @@ async function promptModelId(provider: string, placeholder?: string): Promise<st
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
/**
|
||||
* Optional endpoint override. Empty input means the provider's default endpoint;
|
||||
* any value must be a valid URL.
|
||||
*/
|
||||
async function promptOptionalBaseUrl(): Promise<string | undefined> {
|
||||
const baseUrl = await p.text({
|
||||
message: 'Custom base URL (optional, leave blank for the provider default)',
|
||||
placeholder: 'https://llm-gateway.example.com',
|
||||
validate: (value) => {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return undefined;
|
||||
try {
|
||||
new URL(trimmed);
|
||||
} catch {
|
||||
return 'Must be a valid URL';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
if (p.isCancel(baseUrl)) return cancelAndExit();
|
||||
|
||||
const trimmed = baseUrl?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
async function promptSecret(message: string): Promise<string> {
|
||||
const value = await p.password({
|
||||
message,
|
||||
|
||||
@@ -22,14 +22,16 @@ import {
|
||||
FINAL_REPORT_PDF_FILENAME,
|
||||
INTERNAL_DIR,
|
||||
resolveConfig,
|
||||
resolveModelsConfig,
|
||||
resolveRepo,
|
||||
resolveRunFile,
|
||||
STARTUP_ERROR_FILENAME,
|
||||
} from '../paths.js';
|
||||
import { clearPendingWorkflowIdentity, writePendingWorkflowIdentity } from '../pending-workflow.js';
|
||||
import { indentFailureSegments } from '../scan/failure.js';
|
||||
import { indentFailureSegments, parseFailureSegments } from '../scan/failure.js';
|
||||
import { resolveWorkflowId } from '../session.js';
|
||||
import { displayPlainBanner, displaySplash } from '../splash.js';
|
||||
import { getTerminalOutcome } from '../temporal-client.js';
|
||||
import { describeWorkflowLifecycle, getTerminalOutcome, queryProgress } from '../temporal-client.js';
|
||||
import { stdoutIsTerminal } from '../tty.js';
|
||||
import { tailUntilComplete } from './logs.js';
|
||||
|
||||
@@ -37,6 +39,7 @@ export interface StartArgs {
|
||||
url: string;
|
||||
repo: string;
|
||||
config?: string;
|
||||
modelsConfig?: string;
|
||||
workspace?: string;
|
||||
output?: string;
|
||||
pipelineTesting: boolean;
|
||||
@@ -231,6 +234,7 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
}
|
||||
const repo = resolveRepo(args.repo);
|
||||
const config = args.config ? resolveConfig(args.config) : undefined;
|
||||
const modelsConfig = args.modelsConfig ? resolveModelsConfig(args.modelsConfig) : undefined;
|
||||
const workspacesDir = getWorkspacesDir();
|
||||
const workspace =
|
||||
args.workspace ?? `${new URL(args.url).hostname.replace(/[^a-zA-Z0-9-]/g, '-')}_shannon-${Date.now()}`;
|
||||
@@ -311,6 +315,10 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Clear a stale startup-error from a previous launch so the poll reacts only to this worker's.
|
||||
const startupErrorPath = path.join(internalPath, STARTUP_ERROR_FILENAME);
|
||||
fs.rmSync(startupErrorPath, { force: true });
|
||||
|
||||
// 9. Spawn the worker container.
|
||||
const proc = spawnWorker({
|
||||
version: args.version,
|
||||
@@ -322,6 +330,7 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
containerName,
|
||||
envFlags: buildEnvFlags(),
|
||||
...(config && { config }),
|
||||
...(modelsConfig && { modelsConfig }),
|
||||
...(promptsDir && { promptsDir }),
|
||||
...(outputDir && { outputDir }),
|
||||
workspace,
|
||||
@@ -379,6 +388,16 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
// Poll for the workflow to register in session.json; the spinner resolves once it does.
|
||||
spinner.message('Waiting for the scan to start');
|
||||
for (let attempts = 0; attempts < 60; attempts++) {
|
||||
// A pre-workflow failure leaves its reason here (nothing reached Temporal); surface it
|
||||
// rather than polling out to a generic timeout.
|
||||
const startupError = readStartupError(startupErrorPath);
|
||||
if (startupError) {
|
||||
cleaned = true; // The worker already exited; nothing to stop.
|
||||
spinner.error('The scan could not start');
|
||||
printStartupError(startupError);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const session = JSON.parse(fs.readFileSync(sessionJson, 'utf-8'));
|
||||
const resumeAttempts: { workflowId: string }[] = session.session?.resumeAttempts ?? [];
|
||||
@@ -395,6 +414,17 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
} catch {
|
||||
warn(`Scan ${workspace} started, but its launch record could not be removed.`);
|
||||
}
|
||||
|
||||
// Hold until preflight clears, so an unreachable target or bad credential is reported here
|
||||
// rather than after "Scan started".
|
||||
spinner.message('Running preflight checks');
|
||||
const outcome = await awaitPreflightOutcome(workflowId);
|
||||
if (outcome.kind === 'failed') {
|
||||
spinner.error('The scan could not start');
|
||||
printScanStartFailure(outcome.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner.stop(`Scan started — ${workspace}`);
|
||||
printInfo(args, workspace, repo.hostPath, workspacesDir);
|
||||
if (args.follow) {
|
||||
@@ -438,6 +468,92 @@ export function classifyStartupTimeout(sessionJsonPath: string): 'unregistered'
|
||||
return 'scan-running';
|
||||
}
|
||||
|
||||
/** A pre-workflow failure the worker persisted; mirrors StartupErrorRecord in the worker. */
|
||||
interface StartupError {
|
||||
phase?: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the worker's pre-workflow failure record, if it wrote one. Undefined until the file exists
|
||||
* and parses, so a partial write is simply re-read on the next poll rather than treated as failure.
|
||||
*/
|
||||
function readStartupError(startupErrorPath: string): StartupError | undefined {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = fs.readFileSync(startupErrorPath, 'utf-8');
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return isRecord(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Outcome of waiting for the in-workflow preflight to clear. */
|
||||
type PreflightOutcome = { kind: 'passed' } | { kind: 'failed'; message: string } | { kind: 'unconfirmed' };
|
||||
|
||||
/**
|
||||
* Wait for the registered workflow's preflight to pass or fail: passed once `currentPhase` moves
|
||||
* beyond 'preflight' (or the scan already closed ok), failed when the workflow terminates with an
|
||||
* error. Bounded, so a Temporal query outage falls through as 'unconfirmed' rather than hanging.
|
||||
*/
|
||||
async function awaitPreflightOutcome(workflowId: string): Promise<PreflightOutcome> {
|
||||
for (let attempts = 0; attempts < 80; attempts++) {
|
||||
try {
|
||||
const lifecycle = await describeWorkflowLifecycle(workflowId);
|
||||
if (lifecycle.kind === 'terminal') {
|
||||
const outcome = await getTerminalOutcome(workflowId);
|
||||
return outcome.kind === 'failed' ? { kind: 'failed', message: outcome.message } : { kind: 'passed' };
|
||||
}
|
||||
|
||||
const progress = await queryProgress(workflowId);
|
||||
if (progress && progress.currentPhase !== null && progress.currentPhase !== 'preflight') {
|
||||
return { kind: 'passed' };
|
||||
}
|
||||
} catch {
|
||||
// Transient query failure; keep waiting within the bound.
|
||||
}
|
||||
await sleep(1500);
|
||||
}
|
||||
return { kind: 'unconfirmed' };
|
||||
}
|
||||
|
||||
/** Print a preflight failure: context line, then the indented reason and hint, then the reference code. */
|
||||
function printScanStartFailure(message: string): void {
|
||||
const segments = parseFailureSegments(message);
|
||||
const phaseContext = segments.shift() ?? 'The scan failed';
|
||||
const last = segments[segments.length - 1];
|
||||
const reference = last?.startsWith('Reference code:') ? segments.pop() : undefined;
|
||||
|
||||
const lines = [` ${phaseContext}`, '', ...segments.map((segment) => ` ${segment}`)];
|
||||
if (reference) {
|
||||
lines.push('', ` ${reference}`);
|
||||
}
|
||||
console.error(`\n${lines.join('\n')}\n`);
|
||||
}
|
||||
|
||||
/** Print the worker's persisted startup-failure reason, with its reference code when present. */
|
||||
function printStartupError(startupError: StartupError): void {
|
||||
const message =
|
||||
typeof startupError.message === 'string' && startupError.message.trim()
|
||||
? startupError.message.trim()
|
||||
: 'The worker rejected the scan before it could start. Check the configuration file passed with -c.';
|
||||
console.error('');
|
||||
for (const line of message.split('\n')) {
|
||||
console.error(line.length > 0 ? ` ${line}` : '');
|
||||
}
|
||||
if (typeof startupError.code === 'string' && startupError.code.trim()) {
|
||||
console.error('');
|
||||
console.error(` Reference code: ${startupError.code.trim()}`);
|
||||
}
|
||||
console.error('');
|
||||
}
|
||||
|
||||
/** Point the operator at a scan that is running but whose startup this CLI could not confirm. */
|
||||
function printUnconfirmedScanHint(workspace: string, taskQueue: string, containerName: string): void {
|
||||
console.log('');
|
||||
@@ -530,6 +646,10 @@ function printInfo(args: StartArgs, workspace: string, repoPath: string, workspa
|
||||
if (args.config) {
|
||||
console.log(` Config: ${interactive ? path.resolve(args.config) : path.basename(args.config)}`);
|
||||
}
|
||||
if (args.modelsConfig) {
|
||||
const shown = interactive ? path.resolve(args.modelsConfig) : path.basename(args.modelsConfig);
|
||||
console.log(` Models: ${shown}`);
|
||||
}
|
||||
if (args.pipelineTesting) {
|
||||
console.log(' Mode: Pipeline Testing');
|
||||
}
|
||||
|
||||
@@ -40,9 +40,8 @@ const CONFIG_MAP: readonly ConfigMapping[] = [
|
||||
{ env: 'ANTHROPIC_API_KEY', toml: 'anthropic.api_key', type: 'string' },
|
||||
{ env: 'CLAUDE_CODE_OAUTH_TOKEN', toml: 'anthropic.oauth_token', type: 'string' },
|
||||
|
||||
// OpenAI — format picks the wire API a gateway serves
|
||||
// OpenAI
|
||||
{ env: 'OPENAI_API_KEY', toml: 'openai.api_key', type: 'string' },
|
||||
{ env: 'SHANNON_AI_OPENAI_FORMAT', toml: 'openai.format', type: 'string' },
|
||||
|
||||
// xAI
|
||||
{ env: 'XAI_API_KEY', toml: 'xai.api_key', type: 'string' },
|
||||
@@ -97,15 +96,12 @@ function loadTOML(): TOMLConfig | null {
|
||||
if (!fs.existsSync(configPath)) return null;
|
||||
|
||||
// Config contains secrets — refuse to read if group or others have any access.
|
||||
// Skip on Windows where POSIX permissions are not supported.
|
||||
if (process.platform !== 'win32') {
|
||||
const mode = fs.statSync(configPath).mode;
|
||||
if (mode & 0o077) {
|
||||
const actual = (mode & 0o777).toString(8).padStart(3, '0');
|
||||
fail(
|
||||
`Your config file is readable by other users on this machine (${actual}). Lock it down: chmod 600 ${configPath}`,
|
||||
);
|
||||
}
|
||||
const mode = fs.statSync(configPath).mode;
|
||||
if (mode & 0o077) {
|
||||
const actual = (mode & 0o777).toString(8).padStart(3, '0');
|
||||
fail(
|
||||
`Your config file is readable by other users on this machine (${actual}). Lock it down: chmod 600 ${configPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getConfigFile } from '../home.js';
|
||||
export interface ShannonConfig {
|
||||
core?: { model?: string; base_url?: string };
|
||||
anthropic?: { api_key?: string; oauth_token?: string };
|
||||
openai?: { api_key?: string; format?: string };
|
||||
openai?: { api_key?: string };
|
||||
xai?: { api_key?: string };
|
||||
bedrock?: { region?: string; token?: string };
|
||||
/** Generic credential for any provider Shannon does not curate. Maps to SHANNON_AI_API_KEY. */
|
||||
|
||||
@@ -360,7 +360,6 @@ function shouldSkipHostsName(name: string, hostname: string): boolean {
|
||||
*/
|
||||
function forwardEtcHostsFlags(): string[] {
|
||||
if (!envBool('SHANNON_FORWARD_HOSTS', true)) return [];
|
||||
if (os.platform() === 'win32') return [];
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
@@ -407,6 +406,7 @@ export interface WorkerOptions {
|
||||
containerName: string;
|
||||
envFlags: string[];
|
||||
config?: { hostPath: string; containerPath: string };
|
||||
modelsConfig?: { hostPath: string; containerPath: string };
|
||||
promptsDir?: string;
|
||||
outputDir?: string;
|
||||
workspace: string;
|
||||
@@ -469,6 +469,12 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
|
||||
args.push('-v', `${opts.config.hostPath}:${opts.config.containerPath}:ro`);
|
||||
}
|
||||
|
||||
// pi model config. The mount is the only signal the worker gets: it detects the file at
|
||||
// this fixed path, so nothing about --models-config travels through the environment.
|
||||
if (opts.modelsConfig) {
|
||||
args.push('-v', `${opts.modelsConfig.hostPath}:${opts.modelsConfig.containerPath}:ro`);
|
||||
}
|
||||
|
||||
// Customer-copy destination. The workflow surfaces only final report artifacts here.
|
||||
if (opts.outputDir) {
|
||||
args.push('-v', `${opts.outputDir}:/app/output`);
|
||||
@@ -510,8 +516,6 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
|
||||
// ignore stdin/stdout (the container ID is noise).
|
||||
return spawn('docker', args, {
|
||||
stdio: ['ignore', 'ignore', 'inherit'],
|
||||
// Prevent MSYS/Git Bash from converting Unix paths on Windows
|
||||
...(os.platform() === 'win32' && { env: { ...process.env, MSYS_NO_PATHCONV: '1' } }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
const COMMON_FORWARD_VARS = [
|
||||
'SHANNON_AI_MODEL',
|
||||
'SHANNON_AI_BASE_URL',
|
||||
'SHANNON_AI_OPENAI_FORMAT',
|
||||
// Opt-in debug flag: when set, the worker persists a bounded, sanitized snippet of a failed
|
||||
// provider turn's raw error message to error.log. Off by default; provider prose stays out of
|
||||
// durable state unless an operator deliberately enables it for a diagnosis.
|
||||
|
||||
@@ -29,6 +29,7 @@ export const START_OPTIONS: readonly (readonly [string, string])[] = [
|
||||
['-u, --url <url>', 'Target URL (required)'],
|
||||
['-r, --repo <path>', 'Repository path (required)'],
|
||||
['-c, --config <path>', 'Configuration file (YAML)'],
|
||||
['--models-config <path>', "pi model config (models.json) defining models pi's catalogue lacks"],
|
||||
['-o, --output <path>', 'Copy deliverables to this directory after the run'],
|
||||
['-w, --workspace <name>', 'Named workspace (auto-resumes if it exists)'],
|
||||
['-f, --follow', 'Stream the scan log until it finishes'],
|
||||
|
||||
@@ -61,6 +61,18 @@ function blockSudo(): void {
|
||||
);
|
||||
}
|
||||
|
||||
/** Refuse to run on native Windows. WSL2 reports `linux`, so it is unaffected. */
|
||||
function blockNativeWindows(): void {
|
||||
if (process.platform !== 'win32') return;
|
||||
|
||||
failWith(
|
||||
'CLI_PRECONDITION_FAILED',
|
||||
'Shannon does not run on native Windows.',
|
||||
'Run Shannon inside WSL2. Setup instructions:',
|
||||
'https://github.com/KeygraphHQ/shannon/blob/main/docs/platforms.md',
|
||||
);
|
||||
}
|
||||
|
||||
/** Commands whose `--json` output contract extends to failures. */
|
||||
const JSON_CAPABLE_COMMANDS = new Set(['status', 'scans', 'version', '--version', '-v']);
|
||||
|
||||
@@ -171,6 +183,7 @@ interface ParsedStartArgs {
|
||||
url: string;
|
||||
repo: string;
|
||||
config?: string;
|
||||
modelsConfig?: string;
|
||||
workspace?: string;
|
||||
output?: string;
|
||||
pipelineTesting: boolean;
|
||||
@@ -184,6 +197,7 @@ function parseStartArgs(argv: string[]): ParsedStartArgs {
|
||||
url: ['-u', '--url'],
|
||||
repo: ['-r', '--repo'],
|
||||
config: ['-c', '--config'],
|
||||
modelsConfig: ['--models-config'],
|
||||
output: ['-o', '--output'],
|
||||
workspace: ['-w', '--workspace'],
|
||||
},
|
||||
@@ -213,6 +227,7 @@ function parseStartArgs(argv: string[]): ParsedStartArgs {
|
||||
keepContainer: !!flags.keepContainer,
|
||||
follow: !!flags.follow,
|
||||
...(values.config && { config: values.config }),
|
||||
...(values.modelsConfig && { modelsConfig: values.modelsConfig }),
|
||||
...(values.workspace && { workspace: values.workspace }),
|
||||
...(values.output && { output: values.output }),
|
||||
};
|
||||
@@ -259,6 +274,7 @@ async function main(): Promise<void> {
|
||||
enableJsonErrors();
|
||||
}
|
||||
|
||||
blockNativeWindows();
|
||||
blockSudo();
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
@@ -52,15 +52,6 @@ export const PROVIDER_CREDENTIAL_HINT: Readonly<Record<CuratedProviderId, string
|
||||
/** Model used when SHANNON_AI_MODEL is unset. */
|
||||
export const DEFAULT_MODEL_SPEC = 'anthropic:claude-sonnet-4-6';
|
||||
|
||||
/**
|
||||
* Values SHANNON_AI_OPENAI_FORMAT accepts, selecting the wire format an
|
||||
* OpenAI-compatible gateway serves. Mirrors OPENAI_FORMATS in
|
||||
* apps/worker/src/ai/models.ts; the worker validates and applies it.
|
||||
*/
|
||||
export const OPENAI_FORMATS = ['chat-completions', 'responses'] as const;
|
||||
|
||||
export type OpenAiFormat = (typeof OPENAI_FORMATS)[number];
|
||||
|
||||
export interface ModelSpec {
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
|
||||
+38
-2
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Path resolution for --repo and --config arguments.
|
||||
* Path resolution for --repo, --config and --models-config arguments.
|
||||
*
|
||||
* Both --repo and --config are filesystem paths, absolute or relative to CWD.
|
||||
* All three are filesystem paths, absolute or relative to CWD.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -48,6 +48,13 @@ export const FINAL_REPORT_PDF_FILENAME = 'Security-Assessment-Report.pdf';
|
||||
*/
|
||||
export const FINAL_REPORT_MD_FILENAME = 'Security-Assessment-Report.md';
|
||||
|
||||
/**
|
||||
* Reason for a pre-workflow failure, written by the worker under INTERNAL_DIR. The CLI reads it
|
||||
* during the startup poll to report the real cause instead of a generic timeout. Must match
|
||||
* STARTUP_ERROR_FILENAME in the worker package.
|
||||
*/
|
||||
export const STARTUP_ERROR_FILENAME = 'startup-error.json';
|
||||
|
||||
/**
|
||||
* Resolve a run-directory file (e.g. session.json, workflow.log), preferring the
|
||||
* current INTERNAL_DIR location and falling back to the legacy run-root location
|
||||
@@ -108,3 +115,32 @@ export function resolveConfig(configArg: string): MountPair {
|
||||
containerPath: `/app/configs/${basename}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Container path for a mounted pi model config. Fixed, not derived from the host filename:
|
||||
* the worker detects the file here to decide whether models.json is enabled at all. Must
|
||||
* match MODELS_CONFIG_PATH in the worker package.
|
||||
*/
|
||||
export const MODELS_CONFIG_CONTAINER_PATH = '/app/models.json';
|
||||
|
||||
/**
|
||||
* Resolve --models-config to an absolute path and container mount. Content is left
|
||||
* unparsed: pi's models.json permits comments, so JSON.parse would reject valid input,
|
||||
* and pi's own loader reports schema faults far better — the worker surfaces those.
|
||||
*/
|
||||
export function resolveModelsConfig(modelsConfigArg: string): MountPair {
|
||||
const hostPath = path.resolve(expandHome(modelsConfigArg));
|
||||
|
||||
if (!fs.existsSync(hostPath)) {
|
||||
fail(`Model config file not found: ${hostPath}`);
|
||||
}
|
||||
|
||||
if (!fs.statSync(hostPath).isFile()) {
|
||||
fail(`Not a file: ${hostPath}`);
|
||||
}
|
||||
|
||||
return {
|
||||
hostPath,
|
||||
containerPath: MODELS_CONFIG_CONTAINER_PATH,
|
||||
};
|
||||
}
|
||||
@@ -39,9 +39,9 @@
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-agent-core": "^0.84.2",
|
||||
"@earendil-works/pi-ai": "^0.84.2",
|
||||
"@earendil-works/pi-coding-agent": "^0.84.2",
|
||||
"@earendil-works/pi-agent-core": "^0.84.4",
|
||||
"@earendil-works/pi-ai": "^0.84.4",
|
||||
"@earendil-works/pi-coding-agent": "^0.84.4",
|
||||
"@gotgenes/pi-permission-system": "^10.9.0",
|
||||
"@temporalio/activity": "1.15.0",
|
||||
"@temporalio/client": "1.15.0",
|
||||
|
||||
+51
-107
@@ -20,6 +20,13 @@
|
||||
* Resolution returns a pi `Model` plus the `ModelRuntime` that owns its auth,
|
||||
* built over an in-memory credential store primed from the environment.
|
||||
*
|
||||
* The catalogue is refreshed over the network at scan start, so a newly released model
|
||||
* on a catalogue provider resolves on its own. A model the catalogue does not carry, such
|
||||
* as a router model under its own id, or a self-hosted server, is described in a
|
||||
* pi `models.json` (the CLI's `--models-config`), which merges over the catalogue. The
|
||||
* credential store below outranks any `apiKey` that file carries, so it describes the
|
||||
* model while the environment still supplies the secret.
|
||||
*
|
||||
* The CLI cannot import this module (it ships as a separate bundle), so
|
||||
* `apps/cli/src/model-spec.ts` mirrors the parse rule and the provider/credential
|
||||
* tables by hand for its own `status` rendering and setup wizard. The two copies
|
||||
@@ -32,6 +39,7 @@ import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { Api, Credential, CredentialInfo, CredentialStore, Model } from '@earendil-works/pi-ai';
|
||||
import { getAgentDir, ModelRuntime } from '@earendil-works/pi-coding-agent';
|
||||
import { MODELS_CONFIG_PATH } from '../paths.js';
|
||||
|
||||
/**
|
||||
* Providers Shannon curates with their own credential variables, config sections,
|
||||
@@ -78,42 +86,6 @@ export const DEFAULT_MODEL_SPEC = 'anthropic:claude-sonnet-4-6';
|
||||
/** Browsable pi model catalogue — the source of valid `<provider>:<model-id>` ids. */
|
||||
export const PI_CATALOG_URL = 'https://pi.dev/models';
|
||||
|
||||
/**
|
||||
* Wire formats an OpenAI-compatible gateway may serve, named by
|
||||
* SHANNON_AI_OPENAI_FORMAT. Only `openai` offers a choice: every other supported
|
||||
* provider has exactly one API in pi's registry.
|
||||
*/
|
||||
export const OPENAI_FORMATS = {
|
||||
'chat-completions': 'openai-completions',
|
||||
responses: 'openai-responses',
|
||||
} as const;
|
||||
|
||||
export type OpenAiFormat = keyof typeof OPENAI_FORMATS;
|
||||
|
||||
/** Format assumed when a gateway is configured but no format is named. */
|
||||
export const DEFAULT_OPENAI_FORMAT: OpenAiFormat = 'chat-completions';
|
||||
|
||||
function isOpenAiFormat(value: string): value is OpenAiFormat {
|
||||
return value in OPENAI_FORMATS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read SHANNON_AI_OPENAI_FORMAT. Unset returns undefined, which lets the caller
|
||||
* distinguish "not configured" from an explicit choice and reject the variable
|
||||
* where it has no effect.
|
||||
*/
|
||||
export function resolveOpenAiFormat(): OpenAiFormat | undefined {
|
||||
const raw = process.env.SHANNON_AI_OPENAI_FORMAT?.trim();
|
||||
if (!raw) return undefined;
|
||||
|
||||
if (!isOpenAiFormat(raw)) {
|
||||
throw new Error(
|
||||
`SHANNON_AI_OPENAI_FORMAT must be one of: ${Object.keys(OPENAI_FORMATS).join(', ')}. Got "${raw}".`,
|
||||
);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
export interface ModelSpec {
|
||||
providerId: string;
|
||||
modelId: string;
|
||||
@@ -232,20 +204,50 @@ export function piAuthPresent(): boolean {
|
||||
return existsSync(piAuthPath());
|
||||
}
|
||||
|
||||
/** Path of the mounted pi model config, or undefined when the scan supplied none. */
|
||||
export function modelsConfigPath(): string | undefined {
|
||||
return existsSync(MODELS_CONFIG_PATH) ? MODELS_CONFIG_PATH : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a ModelRuntime whose only credential is the one supplied. Model catalogs
|
||||
* stay offline (`allowModelNetwork` defaults to false) so a scan never blocks on
|
||||
* a catalog refresh.
|
||||
* Where pi persists remote model catalogues. Pinned to the writable agent dir because pi
|
||||
* otherwise derives it from `dirname(modelsPath)`, which is a read-only mount.
|
||||
*/
|
||||
function modelsStorePath(): string {
|
||||
return path.join(getAgentDir(), 'models-store.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a ModelRuntime whose only credential is the one supplied. `allowModelNetwork`
|
||||
* refreshes the model catalogue over the network at scan start, so the registry reflects
|
||||
* models the pinned pi build predates. The fetch is bounded and falls back to the static
|
||||
* catalogue on timeout, so an unreachable endpoint cannot hang the scan. A mounted
|
||||
* `--models-config` overlays the catalogue and is reloaded on every refresh, so its
|
||||
* definitions take precedence.
|
||||
*
|
||||
* `modelsPath` is always explicit, never pi's default of `<agent dir>/models.json`: with no
|
||||
* `--models-config` it is null, which switches models.json off outright, so a stray file in
|
||||
* that shared dir cannot feed model definitions to a run that did not ask for them.
|
||||
* `modelsStorePath` is pinned to the writable agent dir, replacing pi's default
|
||||
* `dirname(modelsPath)` (a read-only mount) as the fetched catalogue's store.
|
||||
*
|
||||
* When the host's pi auth.json is present, the runtime reads it instead: pi's
|
||||
* disk-backed store resolves the credential. The mount is writable so OAuth
|
||||
* refreshes persist to the host for subsequent runs.
|
||||
*/
|
||||
export async function createModelRuntime(providerId: string, apiKey: string | undefined): Promise<ModelRuntime> {
|
||||
const modelsPath = modelsConfigPath();
|
||||
const modelSources = {
|
||||
modelsPath: modelsPath ?? null,
|
||||
...(modelsPath ? { modelsStorePath: modelsStorePath() } : {}),
|
||||
allowModelNetwork: true,
|
||||
modelRefreshTimeoutMs: 10_000,
|
||||
};
|
||||
|
||||
if (piAuthPresent()) {
|
||||
return ModelRuntime.create({ authPath: piAuthPath() });
|
||||
return ModelRuntime.create({ ...modelSources, authPath: piAuthPath() });
|
||||
}
|
||||
return ModelRuntime.create({ credentials: new RuntimeCredentialStore(providerId, apiKey) });
|
||||
return ModelRuntime.create({ ...modelSources, credentials: new RuntimeCredentialStore(providerId, apiKey) });
|
||||
}
|
||||
|
||||
export interface ModelSelection {
|
||||
@@ -257,80 +259,23 @@ export interface ModelSelection {
|
||||
}
|
||||
|
||||
/**
|
||||
* Point a model descriptor at a gateway.
|
||||
* Resolve a model against a runtime, returning undefined when the id is unknown.
|
||||
*
|
||||
* An OpenAI gateway may serve either wire format, named by
|
||||
* SHANNON_AI_OPENAI_FORMAT and defaulting to chat completions, which is what
|
||||
* most gateway software exposes. Switching to completions also drops the stored
|
||||
* `compat` block: the catalogue's block describes Responses, and an explicit
|
||||
* entry outranks pi's `detectCompat`, so leaving it would apply Responses
|
||||
* settings to a completions request. Staying on Responses keeps it, since it
|
||||
* then describes the format in use. Every other provider has one API and only
|
||||
* changes address.
|
||||
*/
|
||||
function pointAtGateway(model: Model<Api>, providerId: string, baseUrl: string, format: OpenAiFormat): Model<Api> {
|
||||
if (providerId !== 'openai') return { ...model, baseUrl };
|
||||
if (format === 'responses') return { ...model, baseUrl, api: OPENAI_FORMATS.responses };
|
||||
|
||||
const { compat: _responsesCompat, ...withoutCompat } = model;
|
||||
return { ...withoutCompat, baseUrl, api: OPENAI_FORMATS['chat-completions'] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a model against a runtime.
|
||||
*
|
||||
* Direct to a provider, the model must exist in the catalogue. Behind a custom
|
||||
* endpoint it need not: a gateway may serve models under its own names, so an
|
||||
* unknown id is passed through on a descriptor borrowed from the provider's
|
||||
* catalogue for its API dialect. Cost and context window on such a descriptor
|
||||
* are the reference model's, so spend figures are approximate there.
|
||||
*
|
||||
* Returns undefined when the id is unresolvable — unknown with no endpoint
|
||||
* override, or a provider carrying no models at all.
|
||||
* The model must exist in the runtime's registry, whether or not an endpoint override
|
||||
* is in play — a base URL changes the address and nothing else. A gateway serving a
|
||||
* model under its own name is described in a `--models-config` file, which puts a real
|
||||
* descriptor in the registry rather than guessing one from an unrelated model.
|
||||
*/
|
||||
export function resolveModel(
|
||||
modelRuntime: ModelRuntime,
|
||||
providerId: string,
|
||||
modelId: string,
|
||||
baseUrl: string | undefined,
|
||||
format: OpenAiFormat = DEFAULT_OPENAI_FORMAT,
|
||||
): Model<Api> | undefined {
|
||||
const found = modelRuntime.getModel(providerId, modelId);
|
||||
if (found) {
|
||||
return baseUrl ? pointAtGateway(found, providerId, baseUrl, format) : found;
|
||||
}
|
||||
if (!baseUrl) return undefined;
|
||||
if (!found) return undefined;
|
||||
|
||||
const reference = modelRuntime.getModels(providerId)[0];
|
||||
if (!reference) return undefined;
|
||||
|
||||
return pointAtGateway({ ...reference, id: modelId, name: modelId }, providerId, baseUrl, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SHANNON_AI_OPENAI_FORMAT against the rest of the configuration and
|
||||
* return the format a gateway run should use.
|
||||
*
|
||||
* The variable only reaches a request when both an OpenAI model and a gateway
|
||||
* are configured, so it is rejected outside that combination rather than
|
||||
* silently ignored.
|
||||
*/
|
||||
export function resolveGatewayFormat(providerId: string, baseUrl: string | undefined): OpenAiFormat {
|
||||
const configured = resolveOpenAiFormat();
|
||||
if (!configured) return DEFAULT_OPENAI_FORMAT;
|
||||
|
||||
if (providerId !== 'openai') {
|
||||
throw new Error(
|
||||
`SHANNON_AI_OPENAI_FORMAT applies to openai models only, but SHANNON_AI_MODEL selects "${providerId}". ` +
|
||||
`${providerId} serves a single API, so there is no format to choose.`,
|
||||
);
|
||||
}
|
||||
if (!baseUrl) {
|
||||
throw new Error(
|
||||
'SHANNON_AI_OPENAI_FORMAT applies to gateway runs only. Set SHANNON_AI_BASE_URL, or unset the format to call OpenAI directly.',
|
||||
);
|
||||
}
|
||||
return configured;
|
||||
return baseUrl ? { ...found, baseUrl } : found;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,12 +285,11 @@ export function resolveGatewayFormat(providerId: string, baseUrl: string | undef
|
||||
export async function resolveModelSelection(): Promise<ModelSelection> {
|
||||
const { providerId, modelId } = resolveModelSpec();
|
||||
const credentials = resolveProviderCredentials(providerId);
|
||||
const format = resolveGatewayFormat(providerId, credentials.baseUrl);
|
||||
|
||||
const mountedPiAuth = piAuthPresent();
|
||||
const modelRuntime = await createModelRuntime(providerId, credentials.apiKey);
|
||||
|
||||
const model = resolveModel(modelRuntime, providerId, modelId, credentials.baseUrl, format);
|
||||
const model = resolveModel(modelRuntime, providerId, modelId, credentials.baseUrl);
|
||||
if (!model) {
|
||||
throw new Error(
|
||||
`Model not found in pi registry: provider="${providerId}" model="${modelId}". Browse valid providers and models at ${PI_CATALOG_URL}.`,
|
||||
|
||||
@@ -276,7 +276,7 @@ function policy(
|
||||
}
|
||||
|
||||
export const CAPELLA_ACTIVITY_POLICIES = Object.freeze({
|
||||
capellaArchitecture: policy('architecture', 60 * MINUTE_MS, 60 * MINUTE_MS, 5 * MINUTE_MS, 3, 'large'),
|
||||
capellaArchitecture: policy('architecture', 90 * MINUTE_MS, 90 * MINUTE_MS, 5 * MINUTE_MS, 3, 'large'),
|
||||
capellaThreatModel: policy('threat-model', 30 * MINUTE_MS, 30 * MINUTE_MS, 5 * MINUTE_MS, 2, 'medium'),
|
||||
capellaPlan: policy('plan', 30 * MINUTE_MS, 90 * MINUTE_MS, 5 * MINUTE_MS, 2, 'medium'),
|
||||
capellaResearch: policy('research', 3 * HOUR_MS, 4.5 * HOUR_MS, 5 * MINUTE_MS, 2, 'small + medium'),
|
||||
|
||||
@@ -34,6 +34,9 @@ const SAFE_ERROR_MESSAGES: Readonly<Record<ErrorCode, string>> = {
|
||||
[ErrorCode.TARGET_UNREACHABLE]: 'The target could not be reached.',
|
||||
[ErrorCode.AUTH_FAILED]: 'Authentication validation failed.',
|
||||
[ErrorCode.AUTH_LOGIN_FAILED]: 'The configured login could not be completed.',
|
||||
[ErrorCode.MODEL_NOT_FOUND]:
|
||||
'The selected model was not found in the harness catalogue. Check SHANNON_AI_MODEL, or supply the model with --models-config.',
|
||||
[ErrorCode.MODEL_CONFIG_INVALID]: 'The model configuration file could not be used.',
|
||||
};
|
||||
|
||||
const ERROR_CATEGORIES = new Set<PentestErrorType>([
|
||||
|
||||
@@ -462,8 +462,19 @@ const performSecurityValidation = (config: Config): void => {
|
||||
}
|
||||
|
||||
if (config.rules) {
|
||||
validateRulesSecurity(config.rules.avoid, 'avoid');
|
||||
validateRulesSecurity(config.rules.focus, 'focus');
|
||||
// Report every bad rule at once, so a config is fixed in one pass rather than one per re-run.
|
||||
const ruleErrors: string[] = [];
|
||||
collectRuleErrors(config.rules.avoid, 'avoid', ruleErrors);
|
||||
collectRuleErrors(config.rules.focus, 'focus', ruleErrors);
|
||||
if (ruleErrors.length > 0) {
|
||||
throw new PentestError(
|
||||
`Configuration validation failed:\n\n${ruleErrors.join('\n\n')}`,
|
||||
'config',
|
||||
false,
|
||||
{ validationErrors: ruleErrors },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
checkForDuplicates(config.rules.avoid || [], 'avoid');
|
||||
checkForDuplicates(config.rules.focus || [], 'focus');
|
||||
@@ -513,126 +524,108 @@ const performSecurityValidation = (config: Config): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const validateRulesSecurity = (rules: Rule[] | undefined, ruleType: string): void => {
|
||||
if (!rules) return;
|
||||
/** Human-readable rule label, e.g. "Focus rule 1" — 1-based to match how an operator counts them. */
|
||||
function ruleLabel(ruleType: string, index: number): string {
|
||||
const capitalized = `${ruleType.charAt(0).toUpperCase()}${ruleType.slice(1)}`;
|
||||
return `${capitalized} rule ${index + 1}`;
|
||||
}
|
||||
|
||||
rules.forEach((rule, index) => {
|
||||
for (const pattern of DANGEROUS_PATTERNS) {
|
||||
if (pattern.test(rule.value)) {
|
||||
throw new PentestError(
|
||||
`rules.${ruleType}[${index}].value contains potentially dangerous pattern: ${pattern.source}`,
|
||||
'config',
|
||||
false,
|
||||
{ field: `rules.${ruleType}[${index}].value`, pattern: pattern.source },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
if (rule.description !== undefined && pattern.test(rule.description)) {
|
||||
throw new PentestError(
|
||||
`rules.${ruleType}[${index}].description contains potentially dangerous pattern: ${pattern.source}`,
|
||||
'config',
|
||||
false,
|
||||
{ field: `rules.${ruleType}[${index}].description`, pattern: pattern.source },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validateRuleTypeSpecific(rule, ruleType, index);
|
||||
});
|
||||
};
|
||||
|
||||
const validateRuleTypeSpecific = (rule: Rule, ruleType: string, index: number): void => {
|
||||
const field = `rules.${ruleType}[${index}].value`;
|
||||
/** A rule error as an aligned label/Value/Problem block, so the offending value is easy to spot. */
|
||||
function ruleValueMessage(label: string, value: string, problem: string): string {
|
||||
return [`${label}:`, ` Value: ${value}`, ` Problem: ${problem}`].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* The type-specific constraint a rule value breaks, or undefined when valid. Returns rather than
|
||||
* throws so every bad rule can be collected and reported together.
|
||||
*/
|
||||
function ruleTypeProblem(rule: Rule): string | undefined {
|
||||
switch (rule.type) {
|
||||
case 'url_path':
|
||||
if (!rule.value.startsWith('/')) {
|
||||
throw new PentestError(
|
||||
`${field} for type 'url_path' must start with '/'`,
|
||||
'config',
|
||||
false,
|
||||
{ field, ruleType: rule.type },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
return "a 'url_path' rule matches the request path only, so it must begin with '/' (e.g. '/api/users')";
|
||||
}
|
||||
break;
|
||||
return undefined;
|
||||
|
||||
case 'code_path':
|
||||
if (rule.value.includes('://')) {
|
||||
throw new PentestError(
|
||||
`${field} for type 'code_path' must not contain a URL protocol (got '${rule.value}')`,
|
||||
'config',
|
||||
false,
|
||||
{ field, ruleType: rule.type },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
return "a 'code_path' rule points at source files, so it must not contain a URL protocol like 'http://' (e.g. 'src/api/users.ts' or 'src/**/*.ts')";
|
||||
}
|
||||
break;
|
||||
return undefined;
|
||||
|
||||
case 'subdomain':
|
||||
case 'domain':
|
||||
// Basic domain validation - no slashes allowed
|
||||
if (rule.value.includes('/')) {
|
||||
throw new PentestError(
|
||||
`${field} for type '${rule.type}' cannot contain '/' characters`,
|
||||
'config',
|
||||
false,
|
||||
{ field, ruleType: rule.type },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
return `a '${rule.type}' rule is a host name, so it cannot contain '/' (e.g. 'api.example.com')`;
|
||||
}
|
||||
// Must contain at least one dot for domains
|
||||
if (rule.type === 'domain' && !rule.value.includes('.')) {
|
||||
throw new PentestError(
|
||||
`${field} for type 'domain' must be a valid domain name`,
|
||||
'config',
|
||||
false,
|
||||
{ field, ruleType: rule.type },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
return "a 'domain' rule must be a full domain name, including the top-level domain (e.g. 'example.com')";
|
||||
}
|
||||
break;
|
||||
return undefined;
|
||||
|
||||
case 'method': {
|
||||
const allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
||||
if (!allowedMethods.includes(rule.value.toUpperCase())) {
|
||||
throw new PentestError(
|
||||
`${field} for type 'method' must be one of: ${allowedMethods.join(', ')}`,
|
||||
'config',
|
||||
false,
|
||||
{ field, ruleType: rule.type, allowedMethods },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
return `'${rule.value}' is not a recognized HTTP method — use one of: ${allowedMethods.join(', ')}`;
|
||||
}
|
||||
break;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
case 'header':
|
||||
if (!rule.value.match(/^[a-zA-Z0-9\-_]+$/)) {
|
||||
throw new PentestError(
|
||||
`${field} for type 'header' must be a valid header name (alphanumeric, hyphens, underscores only)`,
|
||||
'config',
|
||||
false,
|
||||
{ field, ruleType: rule.type },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
return "a header name may contain only letters, digits, hyphens, and underscores (e.g. 'Authorization' or 'X-Api-Key')";
|
||||
}
|
||||
break;
|
||||
return undefined;
|
||||
|
||||
case 'parameter':
|
||||
if (!rule.value.match(/^[a-zA-Z0-9\-_]+$/)) {
|
||||
throw new PentestError(
|
||||
`${field} for type 'parameter' must be a valid parameter name (alphanumeric, hyphens, underscores only)`,
|
||||
'config',
|
||||
false,
|
||||
{ field, ruleType: rule.type },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
return "a parameter name may contain only letters, digits, hyphens, and underscores (e.g. 'user_id' or 'redirect-url')";
|
||||
}
|
||||
return undefined;
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a block to `blocks` for every invalid rule — a dangerous pattern in the value or
|
||||
* description, or a broken type-specific constraint — so all bad rules can be reported together.
|
||||
*/
|
||||
function collectRuleErrors(rules: Rule[] | undefined, ruleType: string, blocks: string[]): void {
|
||||
if (!rules) return;
|
||||
|
||||
rules.forEach((rule, index) => {
|
||||
const label = ruleLabel(ruleType, index);
|
||||
const dangerousInValue = DANGEROUS_PATTERNS.find((pattern) => pattern.test(rule.value));
|
||||
if (dangerousInValue) {
|
||||
blocks.push(
|
||||
ruleValueMessage(label, rule.value, `contains a potentially dangerous pattern (${dangerousInValue.source})`),
|
||||
);
|
||||
} else {
|
||||
const problem = ruleTypeProblem(rule);
|
||||
if (problem) {
|
||||
blocks.push(ruleValueMessage(label, rule.value, problem));
|
||||
}
|
||||
}
|
||||
|
||||
const description = rule.description;
|
||||
if (description !== undefined) {
|
||||
const dangerousInDescription = DANGEROUS_PATTERNS.find((pattern) => pattern.test(description));
|
||||
if (dangerousInDescription) {
|
||||
blocks.push(
|
||||
ruleValueMessage(
|
||||
`${label} (description)`,
|
||||
description,
|
||||
`contains a potentially dangerous pattern (${dangerousInDescription.source})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const checkForDuplicates = (rules: Rule[], ruleType: string): void => {
|
||||
const seen = new Set<string>();
|
||||
|
||||
@@ -15,6 +15,12 @@ export const TYPST_TEMPLATE = path.join(WORKER_ROOT, 'templates', 'typst', 'repo
|
||||
/** Compiled pi extension dir that enforces bounded `bash` timeouts (resolved from dist/) */
|
||||
export const BASH_TIMEOUT_EXTENSION_DIR = path.join(import.meta.dirname, 'ai', 'extensions', 'bash-timeout');
|
||||
|
||||
/**
|
||||
* Where the CLI mounts a pi model config passed with `--models-config`; its presence is
|
||||
* what enables models.json. Must match MODELS_CONFIG_CONTAINER_PATH in the CLI package.
|
||||
*/
|
||||
export const MODELS_CONFIG_PATH = '/app/models.json';
|
||||
|
||||
/** Default deliverables subdirectory relative to repoPath */
|
||||
export const DEFAULT_DELIVERABLES_SUBDIR = '.shannon/deliverables';
|
||||
|
||||
@@ -49,6 +55,13 @@ export const SARIF_FILENAME = 'report.sarif';
|
||||
/** Deterministic receipt for the canonical report finalization commit. */
|
||||
export const REPORT_FINALIZATION_MANIFEST_FILENAME = 'report_finalization_manifest.json';
|
||||
|
||||
/**
|
||||
* Reason for a pre-workflow failure (bad config, resume mismatch, worker setup), written under
|
||||
* INTERNAL_DIR for the CLI to surface — at that point Temporal has no record of the run. Must
|
||||
* match STARTUP_ERROR_FILENAME in the CLI package.
|
||||
*/
|
||||
export const STARTUP_ERROR_FILENAME = 'startup-error.json';
|
||||
|
||||
/**
|
||||
* Resolve the session.json path for a run directory, preferring the current
|
||||
* `.shannon/` location and falling back to the legacy run-root location so
|
||||
|
||||
@@ -332,6 +332,14 @@ function classifyByErrorCode(code: ErrorCode, retryableFromError: boolean): { ty
|
||||
case ErrorCode.AUTH_FAILED:
|
||||
return { type: 'AuthenticationError', retryable: false };
|
||||
|
||||
// Not AuthenticationError: the credential is not in question, and the pipeline
|
||||
// appends an "is your API key valid" hint to anything classified that way.
|
||||
case ErrorCode.MODEL_NOT_FOUND:
|
||||
return { type: 'ModelNotFoundError', retryable: false };
|
||||
|
||||
case ErrorCode.MODEL_CONFIG_INVALID:
|
||||
return { type: 'ModelConfigError', retryable: false };
|
||||
|
||||
case ErrorCode.AUTH_LOGIN_FAILED:
|
||||
return { type: 'AuthLoginFailedError', retryable: false };
|
||||
|
||||
|
||||
@@ -40,10 +40,9 @@ import {
|
||||
createModelRuntime,
|
||||
GENERIC_API_KEY_ENV,
|
||||
type ModelSpec,
|
||||
type OpenAiFormat,
|
||||
modelsConfigPath,
|
||||
PI_CATALOG_URL,
|
||||
piAuthPresent,
|
||||
resolveGatewayFormat,
|
||||
resolveModel,
|
||||
resolveModelSpec,
|
||||
resolveProviderCredentials,
|
||||
@@ -319,7 +318,7 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
|
||||
'config',
|
||||
false,
|
||||
{},
|
||||
ErrorCode.AUTH_FAILED,
|
||||
ErrorCode.MODEL_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -329,24 +328,7 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
|
||||
// needs one API key.
|
||||
const credentials = resolveProviderCredentials(spec.providerId);
|
||||
|
||||
// 3. Wire format for an OpenAI gateway. Rejects a format named where it cannot
|
||||
// take effect, rather than letting the run proceed on the wrong API.
|
||||
let format: OpenAiFormat;
|
||||
try {
|
||||
format = resolveGatewayFormat(spec.providerId, credentials.baseUrl);
|
||||
} catch (error) {
|
||||
return err(
|
||||
new PentestError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
'config',
|
||||
false,
|
||||
{ providerId: spec.providerId },
|
||||
ErrorCode.AUTH_FAILED,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// With a mounted pi auth.json the env-var checks don't apply — step 5's probe validates it.
|
||||
// With a mounted pi auth.json the env-var checks don't apply — step 4's probe validates it.
|
||||
const isBedrock = spec.providerId === 'amazon-bedrock';
|
||||
const missing =
|
||||
isBedrock && !piAuthPresent() ? ['AWS_REGION', 'AWS_BEARER_TOKEN_BEDROCK'].filter((n) => !process.env[n]) : [];
|
||||
@@ -362,33 +344,49 @@ async function validateCredentials(logger: ActivityLogger): Promise<Result<void,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Model must exist in the registry, for every provider — Bedrock IDs are the
|
||||
// easiest to get wrong, since region prefixes and version suffixes differ per
|
||||
// model (`us.anthropic.claude-opus-5` exists, bare `anthropic.` does not).
|
||||
// A custom endpoint is exempt: it may serve models under its own names.
|
||||
// 3. Model must exist in the registry, for every provider and endpoint — Bedrock IDs
|
||||
// are the easiest to get wrong, since region prefixes and version suffixes differ
|
||||
// per model (`us.anthropic.claude-opus-5` exists, bare `anthropic.` does not).
|
||||
// An id the registry lacks is supplied by --models-config, not guessed at here.
|
||||
const modelRuntime = await createModelRuntime(spec.providerId, credentials.apiKey);
|
||||
const baseModel = resolveModel(modelRuntime, spec.providerId, spec.modelId, credentials.baseUrl, format);
|
||||
if (!baseModel) {
|
||||
|
||||
// A model config that fails to parse or compose leaves pi with an empty or fallback
|
||||
// provider, which would surface below as "model not found" and blame SHANNON_AI_MODEL
|
||||
// for the file's fault. Report the real cause first.
|
||||
const modelsConfig = modelsConfigPath();
|
||||
if (modelsConfig) {
|
||||
logger.info(`Model config: ${modelsConfig}`);
|
||||
}
|
||||
const modelConfigError = modelRuntime.getError();
|
||||
if (modelConfigError) {
|
||||
return err(
|
||||
new PentestError(
|
||||
`Model not found in pi registry: provider="${spec.providerId}" model="${spec.modelId}". Check SHANNON_AI_MODEL — browse valid providers and models at ${PI_CATALOG_URL}.`,
|
||||
`Model configuration is invalid:\n${modelConfigError}`,
|
||||
'config',
|
||||
false,
|
||||
{ providerId: spec.providerId, modelId: spec.modelId },
|
||||
ErrorCode.AUTH_FAILED,
|
||||
{ providerId: spec.providerId, ...(modelsConfig && { modelsConfig }) },
|
||||
ErrorCode.MODEL_CONFIG_INVALID,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!modelRuntime.getModel(spec.providerId, spec.modelId)) {
|
||||
logger.warn(
|
||||
`Model "${spec.modelId}" is not in the ${spec.providerId} catalogue; passing it to the custom endpoint as given. Cost figures will be approximate.`,
|
||||
|
||||
const baseModel = resolveModel(modelRuntime, spec.providerId, spec.modelId, credentials.baseUrl);
|
||||
if (!baseModel) {
|
||||
return err(
|
||||
new PentestError(
|
||||
`Model not found in pi registry: provider="${spec.providerId}" model="${spec.modelId}". Check SHANNON_AI_MODEL — browse valid providers and models at ${PI_CATALOG_URL}. A model the catalogue does not carry can be defined in a model config passed with --models-config.`,
|
||||
'config',
|
||||
false,
|
||||
{ providerId: spec.providerId, modelId: spec.modelId },
|
||||
ErrorCode.MODEL_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (credentials.baseUrl && spec.providerId === 'openai') {
|
||||
logger.info(`Gateway API: ${format} (${baseModel.api})`);
|
||||
logger.info(`Gateway API: ${baseModel.api}`);
|
||||
}
|
||||
|
||||
// 5. One real request, so a credential the account cannot use fails here
|
||||
// 4. One real request, so a credential the account cannot use fails here
|
||||
// rather than partway through the run. Bedrock included: pi resolves the
|
||||
// bearer token from the primed credential and the region from AWS_REGION,
|
||||
// so the probe exercises the same auth path the scan will.
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
* TEMPORAL_ADDRESS - Temporal server address (default: localhost:7233)
|
||||
*/
|
||||
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Client, Connection, type WorkflowHandle, WorkflowNotFoundError } from '@temporalio/client';
|
||||
@@ -40,7 +41,8 @@ import { CAPELLA_FORMAT_VERSION, CAPELLA_PROMPT_SET_VERSION } from '../ai/sast/c
|
||||
import { summarizeOperationalMetrics } from '../audit/operational-summary.js';
|
||||
import { sanitizeHostname } from '../audit/utils.js';
|
||||
import { distributeConfig, parseConfig } from '../config-parser.js';
|
||||
import { deliverablesDir, resolveSessionJsonPath } from '../paths.js';
|
||||
import { deliverablesDir, INTERNAL_DIR, resolveSessionJsonPath, STARTUP_ERROR_FILENAME } from '../paths.js';
|
||||
import { PentestError } from '../services/error-handling.js';
|
||||
import { isProviderFailureCategory } from '../types/errors.js';
|
||||
import {
|
||||
ACCEPTED_CAPELLA_FAILURE_STAGES,
|
||||
@@ -511,31 +513,61 @@ interface OrchestrationConfig {
|
||||
exploit?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the scan config into orchestration values, or throw on a broken config. Failing (rather
|
||||
* than falling back to defaults that quietly change scope) lets the caller persist parseConfig's
|
||||
* error for the CLI instead of running a misconfigured scan.
|
||||
*/
|
||||
async function loadOrchestrationConfig(configPath: string | undefined): Promise<OrchestrationConfig> {
|
||||
if (!configPath) return {};
|
||||
try {
|
||||
const config = await parseConfig(configPath);
|
||||
const distributed = distributeConfig(config);
|
||||
const codePathAvoids = distributed.avoid.filter((rule) => rule.type === 'code_path').map((rule) => rule.value);
|
||||
const codePathFocus = distributed.focus.filter((rule) => rule.type === 'code_path').map((rule) => rule.value);
|
||||
const config = await parseConfig(configPath);
|
||||
const distributed = distributeConfig(config);
|
||||
const codePathAvoids = distributed.avoid.filter((rule) => rule.type === 'code_path').map((rule) => rule.value);
|
||||
const codePathFocus = distributed.focus.filter((rule) => rule.type === 'code_path').map((rule) => rule.value);
|
||||
|
||||
return {
|
||||
...(distributed.agenticSast && {
|
||||
agenticSast: {
|
||||
codePathAvoids,
|
||||
codePathFocus,
|
||||
modelSpec: process.env.SHANNON_AI_MODEL?.trim() || DEFAULT_MODEL_SPEC,
|
||||
capellaFormatVersion: CAPELLA_FORMAT_VERSION,
|
||||
promptSetVersion: CAPELLA_PROMPT_SET_VERSION,
|
||||
},
|
||||
}),
|
||||
exploit: distributed.exploit,
|
||||
};
|
||||
} catch (error) {
|
||||
// A broken config must fail the run, not silently fall back to empty
|
||||
// defaults that quietly change scope (vuln classes, exploit, retries).
|
||||
console.error('Worker configuration could not be loaded. Reference code: CONFIG_VALIDATION_FAILED');
|
||||
process.exit(1);
|
||||
return {
|
||||
...(distributed.agenticSast && {
|
||||
agenticSast: {
|
||||
codePathAvoids,
|
||||
codePathFocus,
|
||||
modelSpec: process.env.SHANNON_AI_MODEL?.trim() || DEFAULT_MODEL_SPEC,
|
||||
capellaFormatVersion: CAPELLA_FORMAT_VERSION,
|
||||
promptSetVersion: CAPELLA_PROMPT_SET_VERSION,
|
||||
},
|
||||
}),
|
||||
exploit: distributed.exploit,
|
||||
};
|
||||
}
|
||||
|
||||
// === Startup Failure Persistence ===
|
||||
|
||||
/** Reason for a failure that happens before the workflow is created. */
|
||||
interface StartupErrorRecord {
|
||||
phase: string;
|
||||
code?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a pre-workflow failure to the bind-mounted workspace so the CLI can surface it. The
|
||||
* worker exits before the workflow exists, so Temporal has no record and `--rm` removes the
|
||||
* container; the file under INTERNAL_DIR outlives it on the host mount. `workspace` is the CLI's
|
||||
* `--workspace` name (the run directory); absent only when the worker is run off the CLI path.
|
||||
* Best-effort — a persist failure must not mask the original error.
|
||||
*/
|
||||
function persistStartupError(workspace: string | undefined, error: unknown, phase: string): void {
|
||||
if (!workspace) return;
|
||||
const record: StartupErrorRecord = {
|
||||
phase,
|
||||
...(error instanceof PentestError && error.code !== undefined && { code: error.code }),
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
try {
|
||||
const dir = path.join('./workspaces', workspace, INTERNAL_DIR);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(path.join(dir, STARTUP_ERROR_FILENAME), JSON.stringify(record, null, 2), 'utf8');
|
||||
} catch {
|
||||
// A broken bind mount must not compound the failure; the caller's console.error still fires.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,24 +687,26 @@ async function waitForWorkflowResult(
|
||||
|
||||
// === Main Entry Point ===
|
||||
|
||||
async function run(): Promise<void> {
|
||||
// 1. Parse CLI args
|
||||
const args = parseCliArgs(process.argv.slice(2));
|
||||
|
||||
// 2. Connect to Temporal server
|
||||
const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
|
||||
console.log(`Connecting to Temporal at ${address}...`);
|
||||
|
||||
const connection = await NativeConnection.connect({ address });
|
||||
const clientConnection = await Connection.connect({ address });
|
||||
const client = new Client({ connection: clientConnection });
|
||||
/** A scan whose workflow is durably submitted, with the handles run() needs to await it. */
|
||||
interface StartedScan {
|
||||
handle: WorkflowHandle<(input: PipelineInput) => Promise<PipelineState>>;
|
||||
workspace: WorkspaceResolution;
|
||||
worker: Worker;
|
||||
workerDone: Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run every step that precedes the durable creation of the workflow: config parsing, workspace
|
||||
* resolution, worker setup, and workflow submission. A failure anywhere here is a startup failure
|
||||
* — Temporal holds no record yet — so the reason is persisted for the CLI before it propagates.
|
||||
*/
|
||||
async function startScan(client: Client, connection: NativeConnection, args: CliArgs): Promise<StartedScan> {
|
||||
try {
|
||||
// 3. Validate orchestration and resume state before terminating any workflow.
|
||||
// 1. Validate orchestration and resume state before terminating any workflow.
|
||||
const orchestration = await loadOrchestrationConfig(args.configPath);
|
||||
const workspace = await resolveWorkspace(client, args, orchestration.exploit ?? true);
|
||||
|
||||
// 4. Bundle workflows and create the worker with the collision-checked activity registry.
|
||||
// 2. Bundle workflows and create the worker with the collision-checked activity registry.
|
||||
console.log('Preparing scan...');
|
||||
const workflowBundle = await bundleWorkflowCode({
|
||||
workflowsPath: path.join(__dirname, 'workflows.js'),
|
||||
@@ -695,13 +729,11 @@ async function run(): Promise<void> {
|
||||
maxConcurrentActivityTaskExecutions: 25,
|
||||
});
|
||||
|
||||
// 5. Build the fixed-scope pipeline input.
|
||||
// 3. Build the fixed-scope pipeline input and start worker polling in the background.
|
||||
const input = buildPipelineInput(args, workspace, orchestration);
|
||||
|
||||
// 6. Start worker polling in the background.
|
||||
const workerDone = worker.run();
|
||||
|
||||
// 7. Submit workflow to the same task queue.
|
||||
// 4. Submit workflow to the same task queue. Past this point the run exists in Temporal.
|
||||
const handle = await client.workflow.start<(input: PipelineInput) => Promise<PipelineState>>(
|
||||
'pentestPipelineWorkflow',
|
||||
{
|
||||
@@ -711,10 +743,33 @@ async function run(): Promise<void> {
|
||||
},
|
||||
);
|
||||
|
||||
// 8. Wait for workflow result.
|
||||
return { handle, workspace, worker, workerDone };
|
||||
} catch (startupError) {
|
||||
persistStartupError(args.resumeFromWorkspace, startupError, 'startup');
|
||||
throw startupError;
|
||||
}
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
// 1. Parse CLI args
|
||||
const args = parseCliArgs(process.argv.slice(2));
|
||||
|
||||
// 2. Connect to Temporal server
|
||||
const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
|
||||
console.log(`Connecting to Temporal at ${address}...`);
|
||||
|
||||
const connection = await NativeConnection.connect({ address });
|
||||
const clientConnection = await Connection.connect({ address });
|
||||
const client = new Client({ connection: clientConnection });
|
||||
|
||||
try {
|
||||
// 3. Start the scan: parse config, resolve the workspace, and submit the workflow.
|
||||
const { handle, workspace, worker, workerDone } = await startScan(client, connection, args);
|
||||
|
||||
// 4. Wait for workflow result.
|
||||
await waitForWorkflowResult(handle, workspace);
|
||||
|
||||
// 9. Shut down worker gracefully. Final customer copies are workflow-owned.
|
||||
// 5. Shut down worker gracefully. Final customer copies are workflow-owned.
|
||||
worker.shutdown();
|
||||
await workerDone;
|
||||
} finally {
|
||||
@@ -725,8 +780,10 @@ async function run(): Promise<void> {
|
||||
|
||||
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : undefined;
|
||||
if (invokedPath === fileURLToPath(import.meta.url)) {
|
||||
run().catch(() => {
|
||||
console.error('Worker failed. Reference code: WORKER_FAILED');
|
||||
run().catch((error) => {
|
||||
// startScan persists pre-workflow failures for the CLI; this also logs them in the container.
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Worker failed: ${message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import { ErrorCode } from '../types/errors.js';
|
||||
*/
|
||||
const ERROR_TYPE_TO_CODE: Record<string, ErrorCode> = {
|
||||
AuthenticationError: ErrorCode.AUTH_FAILED,
|
||||
ModelNotFoundError: ErrorCode.MODEL_NOT_FOUND,
|
||||
ModelConfigError: ErrorCode.MODEL_CONFIG_INVALID,
|
||||
ConfigurationError: ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
OutputValidationError: ErrorCode.OUTPUT_VALIDATION_FAILED,
|
||||
AgentExecutionError: ErrorCode.AGENT_EXECUTION_FAILED,
|
||||
@@ -54,6 +56,8 @@ export function classifyErrorCode(error: unknown): ErrorCode | undefined {
|
||||
*/
|
||||
const REMEDIATION_HINTS: Record<string, string> = {
|
||||
AuthenticationError: "Verify the selected provider's API key is valid and not expired.",
|
||||
ModelNotFoundError: 'Check SHANNON_AI_MODEL against pi.dev/models, or supply the model with --models-config.',
|
||||
ModelConfigError: 'Check the --models-config file parses and matches pi’s models.json schema.',
|
||||
ConfigurationError: 'Check your CONFIG file path and contents.',
|
||||
GitError: 'Check repository path and git state.',
|
||||
InvalidTargetError: 'Verify the target URL is correct and accessible.',
|
||||
@@ -69,6 +73,8 @@ const REMEDIATION_HINTS: Record<string, string> = {
|
||||
*/
|
||||
const SAFE_WORKFLOW_FAILURE_MESSAGES: Readonly<Record<string, string>> = {
|
||||
AuthenticationError: 'Provider authentication failed.',
|
||||
ModelNotFoundError: 'The selected model was not found in the harness catalogue.',
|
||||
ModelConfigError: 'The model configuration file could not be used.',
|
||||
ConfigurationError: 'The scan configuration is invalid.',
|
||||
OutputValidationError: 'A scan step returned an unusable result.',
|
||||
AgentExecutionError: 'An agent could not complete its work.',
|
||||
@@ -125,10 +131,6 @@ export function formatWorkflowError(error: unknown, currentPhase: string | null,
|
||||
|
||||
const segments: string[] = [phaseContext];
|
||||
|
||||
if (unwrapped.type) {
|
||||
segments.push(unwrapped.type);
|
||||
}
|
||||
|
||||
segments.push(
|
||||
unwrapped.type === null
|
||||
? 'The scan could not be completed.'
|
||||
@@ -140,6 +142,7 @@ export function formatWorkflowError(error: unknown, currentPhase: string | null,
|
||||
if (hint) {
|
||||
segments.push(`Hint: ${hint}`);
|
||||
}
|
||||
segments.push(`Reference code: ${unwrapped.type}`);
|
||||
}
|
||||
|
||||
return segments.join('|');
|
||||
|
||||
@@ -40,6 +40,8 @@ export enum ErrorCode {
|
||||
TARGET_UNREACHABLE = 'TARGET_UNREACHABLE',
|
||||
AUTH_FAILED = 'AUTH_FAILED',
|
||||
AUTH_LOGIN_FAILED = 'AUTH_LOGIN_FAILED',
|
||||
MODEL_NOT_FOUND = 'MODEL_NOT_FOUND',
|
||||
MODEL_CONFIG_INVALID = 'MODEL_CONFIG_INVALID',
|
||||
}
|
||||
|
||||
export type PentestErrorType = 'config' | 'network' | 'prompt' | 'filesystem' | 'validation' | 'unknown';
|
||||
|
||||
+127
-19
@@ -28,12 +28,15 @@ Shannon forwards only the selected provider's credential into the scan container
|
||||
Shannon accepts any provider and model present in the Pi harness catalogue. Browse them at [pi.dev/models](https://pi.dev/models).
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=your-api-key # the provider's API key
|
||||
export SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3 # <provider>:<model-id>
|
||||
export SHANNON_AI_API_KEY=your-api-key # the provider's key — or the gateway's when a base URL is set
|
||||
export SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3 # <provider>:<model-id>
|
||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com # optional: route through a proxy or LLM gateway
|
||||
```
|
||||
|
||||
This path covers providers whose credential is a single API key. Providers that need more than that are not currently supported.
|
||||
|
||||
A model the catalogue does not carry is reachable by describing it yourself. See [Custom model configuration](#custom-model-configuration).
|
||||
|
||||
`npx @keygraph/shannon setup` exposes this as the **Other provider** option.
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -48,7 +51,7 @@ Review each vendor's guidance and complete the verification or enrollment they a
|
||||
- Anthropic - [Real-time cyber safeguards on Claude Opus and Sonnet](https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet)
|
||||
- OpenAI - [Cyber](https://chatgpt.com/cyber)
|
||||
|
||||
This applies to the Anthropic and OpenAI providers, including when either is reached through a gateway. Bedrock serves Claude models and is subject to Anthropic's safeguards as well.
|
||||
This applies to the Anthropic and OpenAI providers, including when either is reached through an LLM gateway. Bedrock serves Claude models and is subject to Anthropic's safeguards as well.
|
||||
|
||||
## Suggested models
|
||||
|
||||
@@ -104,17 +107,18 @@ Bedrock uses bearer-token authentication only. IAM access keys, session tokens,
|
||||
|
||||
## Custom base URL
|
||||
|
||||
To route model traffic through your own infrastructure — a corporate proxy, an LLM gateway such as LiteLLM, or a regional endpoint — set a base URL alongside your normal model selection. The provider half of `SHANNON_AI_MODEL` decides which key is sent and which API Shannon speaks, so pick the one your gateway serves:
|
||||
`SHANNON_AI_BASE_URL` routes model traffic through a proxy or LLM gateway instead of the provider's default endpoint — an LLM gateway such as LiteLLM, a regional endpoint, or any other host you choose. It is a plain endpoint override: it changes only *where* requests go. The provider half of `SHANNON_AI_MODEL` still decides which credential is sent and which API dialect is spoken, and that is unchanged by the base URL.
|
||||
|
||||
| Gateway serves | Model prefix | API key |
|
||||
| --- | --- | --- |
|
||||
| Anthropic Messages | `anthropic:` | `SHANNON_AI_API_KEY` |
|
||||
| OpenAI Chat Completions | `openai:` | `SHANNON_AI_API_KEY` |
|
||||
| OpenAI Responses | `openai:` + `SHANNON_AI_OPENAI_FORMAT=responses` | `SHANNON_AI_API_KEY` |
|
||||
This works for **any** provider, curated or not, subject to two rules. A provider's dialect is fixed, so the endpoint you point at must speak that provider's dialect:
|
||||
|
||||
The model ID is whatever name your gateway serves it under; it does not have to exist in Shannon's catalogue.
|
||||
| Provider prefix | Dialect the endpoint must speak |
|
||||
| --- | --- |
|
||||
| `anthropic:` | Anthropic Messages |
|
||||
| `openai:` | OpenAI Responses |
|
||||
|
||||
Anthropic Messages:
|
||||
And the model ID must still resolve in the harness catalogue. A base URL changes only the address; it grants no exemption from that check. A gateway serving a model under its own name needs that name described in a [custom model configuration](#custom-model-configuration) file.
|
||||
|
||||
Anthropic Messages LLM gateway:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=sk-ant-...
|
||||
@@ -122,7 +126,7 @@ export SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||
```
|
||||
|
||||
OpenAI Chat Completions:
|
||||
OpenAI Responses LLM gateway:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=sk-...
|
||||
@@ -130,19 +134,122 @@ export SHANNON_AI_MODEL=openai:gpt-5.6-sol
|
||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
||||
```
|
||||
|
||||
`SHANNON_AI_MODEL` is always `<provider>:<model-id>`, gateway or not.
|
||||
`npx @keygraph/shannon setup` configures a base URL two ways: **Custom Base URL** covers the common Anthropic Messages and OpenAI Responses LLM gateways, and **Other provider** takes any provider ID plus an optional base URL of its own.
|
||||
|
||||
OpenAI is the one provider serving two APIs, so a gateway run picks one:
|
||||
## Custom model configuration
|
||||
|
||||
A custom model configuration is a Pi `models.json` file that describes a model the harness catalogue does not carry: one a router or gateway serves under its own ID, or a local server (see [Local and self-hosted models](#local-and-self-hosted-models)). You pass it with `--models-config`, and Shannon merges its definitions over the catalogue so `SHANNON_AI_MODEL` can then name the model like any other:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_OPENAI_FORMAT=responses # default: chat-completions
|
||||
npx @keygraph/shannon start -u https://example.com -r /path/to/repo --models-config ./models.json
|
||||
```
|
||||
|
||||
Chat Completions is the default because that is what most gateway software exposes. Set `responses` for a gateway that passes the Responses API through — it preserves reasoning state between turns, which Chat Completions cannot. `openai:gpt-5` with no base URL always calls OpenAI's Responses API directly.
|
||||
```bash
|
||||
./shannon start -u https://example.com -r ./my-repo --models-config ./models.json
|
||||
```
|
||||
|
||||
The variable is rejected in preflight where it cannot take effect: with a non-`openai` model, since Anthropic, xAI, and Bedrock each serve one API, and with no `SHANNON_AI_BASE_URL`, since a direct OpenAI run is always Responses.
|
||||
[pi.dev/models](https://pi.dev/models) supplies the file contents. Find the model under the provider you want, since the same model has a different ID per provider, then open its page and expand **Show configuration** for a ready-to-paste snippet:
|
||||
|
||||
`npx @keygraph/shannon setup` covers this under **Custom Base URL**, which asks which API your gateway serves and configures the matching provider for you.
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"apiKey": "YOUR_API_KEY",
|
||||
"models": [
|
||||
{
|
||||
"id": "z-ai/glm-5.3",
|
||||
"name": "Z.ai: GLM 5.3",
|
||||
"reasoning": true,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"thinkingLevelMap": {
|
||||
"off": null,
|
||||
"minimal": null,
|
||||
"low": "low",
|
||||
"medium": null,
|
||||
"high": "high",
|
||||
"xhigh": null,
|
||||
"max": "max"
|
||||
},
|
||||
"contextWindow": 1048576,
|
||||
"maxTokens": 943718,
|
||||
"cost": {
|
||||
"input": 1.4,
|
||||
"output": 4.4,
|
||||
"cacheRead": 0.26,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"thinkingFormat": "openrouter"
|
||||
}
|
||||
}
|
||||
],
|
||||
"api": "openai-completions",
|
||||
"baseUrl": "https://openrouter.ai/api/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then name the model the usual way:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=your-api-key
|
||||
export SHANNON_AI_MODEL=openrouter:z-ai/glm-5.3
|
||||
```
|
||||
|
||||
Leave `YOUR_API_KEY` exactly as it is. Shannon sends the credential from your environment, and that takes precedence over anything the file declares, so the file describes the model and never has to hold a secret.
|
||||
|
||||
Pi's [models documentation](https://pi.dev/docs/latest/models) describes the full format, including provider routing preferences and compatibility flags.
|
||||
|
||||
## Local and self-hosted models
|
||||
|
||||
Ollama, LM Studio, vLLM, and any other OpenAI-compatible server are reached through the same mechanism. Describe the server as a provider in a model config file, then name its model with `SHANNON_AI_MODEL`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Use `host.docker.internal`, not `localhost`. The scan runs inside a container, so `localhost` points at the container itself rather than at your machine.
|
||||
|
||||
A `models.json` for Ollama:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://host.docker.internal:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"models": [
|
||||
{ "id": "<model-id>" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then name the model and run:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=ollama # any value, see below
|
||||
export SHANNON_AI_MODEL=ollama:<model-id>
|
||||
./shannon start -u https://example.com -r ./my-repo --models-config ./models.json
|
||||
```
|
||||
|
||||
LM Studio and vLLM take the same shape on their own ports, `http://host.docker.internal:1234/v1` and `http://host.docker.internal:8000/v1` respectively. The provider name is yours to choose, and only has to match the prefix in `SHANNON_AI_MODEL`.
|
||||
|
||||
`SHANNON_AI_API_KEY` is still required even though a local server ignores it. Shannon checks that the selected provider has a credential before it starts, so set it to any placeholder value. It is sent to your server and discarded.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Shannon drives every phase through multi-turn tool use. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves.
|
||||
|
||||
Some servers need compatibility flags. If a reasoning-capable model is rejected, turn off the roles it does not understand, at either provider or model level:
|
||||
|
||||
```json
|
||||
"compat": { "supportsDeveloperRole": false, "supportsReasoningEffort": false }
|
||||
```
|
||||
|
||||
Pi's [models documentation](https://pi.dev/docs/latest/models) lists the full set of compatibility flags and local-runtime options.
|
||||
|
||||
## OpenAI Codex (ChatGPT Plus/Pro subscription)
|
||||
|
||||
@@ -209,7 +316,8 @@ These instructions apply only to `shannon-v1`.
|
||||
|
||||
Checks run before a scan starts, so mistakes fail immediately rather than partway through a run:
|
||||
|
||||
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). A custom base URL exempts the model ID, since a gateway may serve its own names.
|
||||
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). To run a model the catalogue does not carry, describe it with [`--models-config`](#custom-model-configuration).
|
||||
- **Model configuration** — when `--models-config` is passed, the file is parsed and schema-checked before the scan starts, and a fault fails preflight with the offending field named.
|
||||
- **Credential presence** — validated for the selected provider, or read from Pi when `SHANNON_USE_PI_AUTH=1`.
|
||||
- **Credential validity** — one minimal request against the model the scan will use, so a rejected key, an exhausted quota, or a model the account cannot reach fails before any agent runs. Bedrock included: its bearer token and region go through the same probe.
|
||||
|
||||
|
||||
+56
-88
@@ -1,133 +1,101 @@
|
||||
# Keygraph Enterprise Platform
|
||||
|
||||
Shannon 3.0 makes advanced, code-informed autonomous pentesting available to everyone. The open-source CLI maps routes and data flows, understands application architecture, executes real attacks, and produces PDF and SARIF results—locally, in CI/CD, or fully air-gapped with your own model.
|
||||
Shannon 3.0 is an open-source pentester. It reads your source, maps routes and data flows, runs real attacks against a live target, and writes PDF and SARIF reports. It runs locally, in CI, or air-gapped with your own model. Shannon Open Source is a complete pentester, not a trial edition.
|
||||
|
||||
The **Keygraph Enterprise Platform** is the commercial AppSec operating system for organizations that need to run that process continuously across many repositories, teams, and environments. It adds exhaustive agentic SAST, business-logic and source-to-sink analysis, broader scanner coverage, centralized vulnerability management, automated remediation and targeted verification, enterprise governance, and organization-wide reporting.
|
||||
Keygraph Enterprise runs an enterprise-hardened fork of Shannon continuously across hundreds of repositories and adds what a security team needs around it: audit-depth static analysis on a parsed code graph, business-logic testing, SCA and secrets scanning, one deduplicated record per vulnerability across scans and scanners, generated fixes, fix verification, and SSO, RBAC, and audit logs. It is for security teams that own vulnerability management across many engineering teams and need one place to triage, assign, fix, and verify.
|
||||
|
||||
> Shannon Open Source is a complete autonomous pentester, not a trial edition. Keygraph Enterprise is for teams that need greater analysis depth, shared control, and a closed-loop vulnerability-management program.
|
||||
Both editions are BYOK. Keygraph never receives your source and never proxies model traffic, open source or commercial. Shannon Open Source runs from your machine or CI runner. Keygraph Enterprise deploys as a platform inside your cloud or data center, including fully air-gapped.
|
||||
|
||||
## Who It Is For
|
||||
## Shannon Open Source vs. Keygraph Enterprise
|
||||
|
||||
Keygraph Enterprise is designed for organizations that need to:
|
||||
| | Shannon Open Source | Keygraph Enterprise |
|
||||
| --- | --- | --- |
|
||||
| Best for | Developers and teams running repository-level pentests locally or in CI | Security organizations running continuous AppSec across many teams and repositories |
|
||||
| Code analysis | Agent pass over architecture, entry points, and data flows to seed the pentest, sized to finish inside a CI run | Persistent code property graph plus a long-running analysis harness with interprocedural taint, sanitizer modeling, cross-repo context, exploit chains, and multi-pass review |
|
||||
| Pentesting | On-demand, source-aware white-box pentesting with optional authenticated testing, focused on injection, XSS, SSRF, broken authentication, and broken authorization, with proof by exploitation | Enterprise-hardened Shannon fork run continuously, with grey-box and black-box targets and business-logic invariant testing |
|
||||
| SCA and secrets | Not included | SCA with reachability and secrets scanning including history |
|
||||
| Findings | Per-run PDF, Markdown, JSON, and SARIF, with SARIF ingestion into GitHub code scanning | One record per vulnerability per repo across scans and scanners, plus ownership, SLAs, dashboards, and audit evidence |
|
||||
| Fixes and verification | Not included | Fix PRs with verification by re-analysis and exploit replay, with no full rescan required |
|
||||
| CI/CD and source control | GitHub Action and GitLab CI component for pull-request, release, and scheduled runs, with gates on `status: exploited` | GitHub, GitLab, Azure DevOps, and Bitbucket with organization-wide policy and centrally managed integrations |
|
||||
| Deployment and models | Runs locally or on a CI runner with BYOK to any Anthropic- or OpenAI-compatible endpoint or local model | Deployed in your AWS, GCP, Azure, or on-prem environment. Customer-hosted services and stored platform data remain inside your environment. Model requests go directly to the provider, private endpoint, gateway, or local model you configure. A local model supports fully disconnected deployments |
|
||||
| Governance, license, support | AGPL-3.0 and community support | SSO, SCIM, RBAC, and audit logs, plus a commercial license, enterprise support, and SOC 2 Type II |
|
||||
|
||||
- continuously test hundreds or thousands of repositories, services, applications, and APIs;
|
||||
- combine agentic pentesting, SAST, SCA, secrets, and business-logic findings in one system;
|
||||
- enforce security policy in GitHub Actions, GitLab CI, and enterprise delivery pipelines;
|
||||
- give developers one canonical, actionable record for each vulnerability instead of duplicate scanner alerts;
|
||||
- assign owners, apply SLAs, track status, and measure risk and remediation performance across the organization;
|
||||
- generate fixes and verify them without rerunning an entire scan;
|
||||
- enforce enterprise identity, authorization, audit, and API-access controls; and
|
||||
- deploy fully on-premises or air-gapped with customer-controlled models, keys, and routing.
|
||||
## How it fits your pipeline
|
||||
|
||||
## Close the Entire AppSec Loop
|
||||
1. Scans run on pull requests, releases, and a schedule against repositories in GitHub, GitLab, Azure DevOps, or Bitbucket.
|
||||
2. Pipelines gate on exploited severity. A code-analysis hypothesis never fails a build.
|
||||
3. Findings from every scanner and every run land as one record per vulnerability per repository, with an owner and an SLA. The same finding across ten runs is one record, not ten alerts.
|
||||
4. From a finding, Keygraph opens a fix PR into your normal review flow.
|
||||
5. Verification confirms the fix against the changed code and the original exploit. No full rescan is required.
|
||||
|
||||
The platform connects discovery, triage, remediation, and verification in one continuous workflow:
|
||||
## What is different technically
|
||||
|
||||
1. **Analyze** every repository with exhaustive agentic SAST and complementary scanners.
|
||||
2. **Prove** exploitability with source-aware white-box, black-box, and grey-box pentesting.
|
||||
3. **Normalize and deduplicate** results into a canonical finding per vulnerability and repository.
|
||||
4. **Prioritize and assign** using severity, reachability, exploit evidence, ownership, policy, and business context.
|
||||
5. **Remediate** with an AI-authored patch delivered as a reviewable pull request.
|
||||
6. **Verify** the specific fix with deterministic checks and adversarial agent reasoning—without rerunning the full scan.
|
||||
7. **Track and govern** status, exceptions, SLAs, audit history, trends, and compliance evidence until closure.
|
||||
### Static analysis on a code property graph
|
||||
|
||||
## Exhaustive Agentic SAST
|
||||
Shannon Open Source's code analysis is sized to finish inside a CI run: agents read the repository, map the attack surface, and hand candidates to the pentester. Enterprise is built for depth instead. It first parses each repository into a persistent code property graph, then runs an analysis harness derived from one built for long-running vulnerability audits, heavily adapted to query the graph rather than read files. The harness decomposes the application into risk, taint-flow, framework, and specialist tasks and supports longer-running audit workflows beyond typical CI job windows.
|
||||
|
||||
Shannon 3.0's open-source code analysis runs a multi-stage agentic workflow. It models application architecture, trust boundaries, exposed interfaces, and data flows, opens targeted investigations, reviews the candidates they turn up, and hands the survivors to live pentesting agents. That workflow is built for practical local and CI/CD runs.
|
||||
On the graph, it performs:
|
||||
|
||||
The Enterprise engine goes further, for audits at organization scale. It parses the codebase and builds persistent structural context before agents start reasoning about security:
|
||||
- Interprocedural taint tracking across functions, files, fields, containers, and framework request lifecycles.
|
||||
- Source, sink, and sanitizer modeling that records where validation, encoding, or authorization changes a path.
|
||||
- Cross-repository modeling of services, entry points, and trust boundaries.
|
||||
- Semantic deduplication of variants of the same defect, and exploit-chain analysis for combinations with higher impact than any single issue.
|
||||
- Multiple review passes per candidate, checking the agent's claim against the graph and available deployment and configuration context. Candidates that cannot be substantiated are not reported.
|
||||
|
||||
- **Repository and architecture modeling** identifies services, frameworks, entry points, assets, trust boundaries, and cross-repository relationships.
|
||||
- **Interprocedural call and data-flow analysis** traces values across functions, files, fields, containers, and framework-managed request lifecycles.
|
||||
- **Source, sink, and sanitizer modeling** follows untrusted input to sensitive operations and records where validation, encoding, authorization, or other controls alter the path.
|
||||
- **Threat-driven decomposition** breaks large applications into risk, taint-flow, framework, and specialist analysis tasks so deep scans remain systematic.
|
||||
- **Exhaustive adversarial verification** challenges candidates across multiple review passes, weighing structural evidence against what the agents found, then asks whether each one is viable in the application's production configuration.
|
||||
- **Semantic deduplication and exploit-chain analysis** consolidate variants of the same defect and identify combinations whose impact is greater than any isolated issue.
|
||||
- **Business-logic invariant testing** derives rules the code is supposed to preserve—such as tenant isolation, workflow order, approval limits, balances, and state transitions—then agents fuzz those invariants for application-specific flaws.
|
||||
### Business-logic invariants
|
||||
|
||||
The result is broad vulnerability hunting with precise paths back to the relevant code, not a flat list of pattern matches.
|
||||
Shannon Open Source focuses on injection, XSS, SSRF, and broken authentication and authorization. Enterprise adds testing for the bugs that do not fit a vulnerability class: it derives invariants the application is supposed to hold (tenant isolation, workflow ordering, approval limits, balance conservation, state transitions) and tests them against the running application. This is where application-specific vulnerabilities live and where pattern-based SAST often provides little or no signal.
|
||||
|
||||
### Proof by exploitation
|
||||
|
||||
The pentesting engine is a hardened fork of Shannon with the same rule: a pentest finding requires a working exploit. No exploit, no finding. Enterprise stores the exploit and replays it later to verify the fix.
|
||||
|
||||
SCA prioritizes vulnerable dependencies that application code actually reaches. Secrets scanning covers current source and repository history.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/agentic-sast-results.png" alt="Keygraph Enterprise SAST results grouped into business-logic issues, point issues, and secrets" width="100%">
|
||||
<img src="../assets/keygraph-platform/agentic-sast-results.png" alt="Keygraph Enterprise findings grouped into business-logic issues, point issues, and secrets" width="100%">
|
||||
</p>
|
||||
|
||||
## Complete Application-Security Coverage
|
||||
## Findings
|
||||
|
||||
Agentic SAST and pentesting work alongside additional first-class scanners:
|
||||
Shannon Open Source hands you a report per scan. Enterprise dedupes across runs and across scanners, deterministically and semantically, into one record per vulnerability per repository. Each record carries evidence, source location, severity, scan history, status, owner, resolution, and last-verified state.
|
||||
|
||||
- **SCA with reachability** prioritizes vulnerable dependencies that application code can actually reach.
|
||||
- **Full secrets scanning** detects credentials, tokens, and keys across source and repository history.
|
||||
- **Agentic pentesting** correlates code intelligence with live application behavior and attempts real exploitation. The core rule remains: no exploit, no pentest finding.
|
||||
Workflows cover assignment, triage, false-positive and risk-acceptance decisions, and SLA policies with escalation and aging. Dashboards report open risk, coverage, new versus resolved, SLA compliance, and MTTR, exportable as evidence for customers and auditors.
|
||||
|
||||
## One System of Record for Every Finding
|
||||
|
||||
Keygraph ingests results from every analysis source, correlates them, and maintains one canonical finding per vulnerability per repository. Security and engineering teams work from the same record, with evidence, source location, severity, scan history, status, assignee, resolution, and last-verification state.
|
||||
|
||||
The vulnerability-management layer provides:
|
||||
|
||||
- deterministic and semantic deduplication across scans and scanners;
|
||||
- ownership, assignment, triage, false-positive, risk-acceptance, and resolution workflows;
|
||||
- SLA policies, escalation, aging, and last-verified tracking;
|
||||
- bidirectional developer-workflow integrations and APIs;
|
||||
- dashboards for risk, coverage, trends, new versus resolved findings, SLA compliance, and MTTR; and
|
||||
- exportable evidence for customers, auditors, and compliance programs.
|
||||
Findings still require human review. Enterprise's extra review passes reduce weakly supported findings, but they do not eliminate them.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/canonical-findings.png" alt="Keygraph Enterprise canonical findings inventory with severity, status, source, and verification filters" width="100%">
|
||||
<img src="../assets/keygraph-platform/canonical-findings.png" alt="Keygraph Enterprise findings inventory with severity, status, source, and verification filters" width="100%">
|
||||
</p>
|
||||
|
||||
## Remediate, Then Verify the Fix
|
||||
### Fix and verify
|
||||
|
||||
From an individual finding, a user can ask Keygraph to produce a focused patch. The remediation agent reasons from the root cause and evidence, changes only the required code, and opens a pull request into the existing review process. It does not silently apply fixes to a protected branch.
|
||||
From a finding, Keygraph generates a patch scoped to that finding and opens a pull request. It never commits to a protected branch.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/automated-remediation.png" alt="Keygraph Enterprise remediation workflow for generating a fix and opening a pull request" width="100%">
|
||||
</p>
|
||||
|
||||
After a patch is available, targeted verification re-analyzes the affected code and, for dynamic pentest findings, re-tests the original proof of concept against the target. Deterministic checks and adversarial agent reasoning produce a clear verdict without the cost and delay of rerunning the entire scan.
|
||||
Verification re-analyzes the changed code and, for pentest findings, replays the original exploit against the patched target. The verdict comes from deterministic checks plus a review pass, without rerunning the full scan.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/targeted-verification.png" alt="Keygraph Enterprise targeted finding-verification workflow" width="100%">
|
||||
<img src="../assets/keygraph-platform/targeted-verification.png" alt="Keygraph Enterprise finding-verification workflow" width="100%">
|
||||
</p>
|
||||
|
||||
## Enterprise Governance and Integrations
|
||||
## Deployment and access control
|
||||
|
||||
Keygraph is built for shared operation across security, platform, and engineering teams:
|
||||
Keygraph Enterprise deploys entirely inside your AWS, GCP, Azure, or on-prem environment, including networks with no internet egress. There is no Keygraph-operated control plane. Customer-hosted services and stored platform data remain inside your environment for the life of the deployment.
|
||||
|
||||
- SAML 2.0 or OIDC single sign-on and SCIM provisioning;
|
||||
- organization, team, and user management;
|
||||
- built-in and custom roles with granular relationship-, attribute-, and role-based authorization (ReBAC, ABAC, and RBAC);
|
||||
- repository, pentest-profile, scanner, finding, and administration boundaries;
|
||||
- full audit logging and scoped API keys;
|
||||
- integrations with source control, CI/CD, ticketing, chat, and cloud environments; and
|
||||
- commercial support and enterprise onboarding.
|
||||
Model access is BYOK and BYOM. Route workloads to Anthropic, OpenAI, xAI, or Bedrock, a private cloud endpoint, your own gateway such as LiteLLM with your routing and policy applied, or local models on vLLM or Ollama. Model requests go directly to the endpoint you configure. Keygraph never receives or proxies them. A local model supports a fully disconnected deployment.
|
||||
|
||||
Access control: SAML/OIDC SSO, SCIM, roles with repository-scoped visibility (RBAC, plus attribute and relationship rules where needed), full audit log, scoped API keys.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/enterprise-access-control.png" alt="Keygraph Enterprise granular roles and repository visibility controls" width="100%">
|
||||
<img src="../assets/keygraph-platform/enterprise-access-control.png" alt="Keygraph Enterprise roles and repository visibility controls" width="100%">
|
||||
</p>
|
||||
|
||||
## On-Premises, Air-Gapped, and Customer-Controlled AI
|
||||
|
||||
Keygraph Enterprise can run entirely inside your AWS, GCP, Azure, or on-premises environment, including networks with no public internet access. Deployments can keep source code, scan artifacts, findings, prompts, completions, and model traffic inside your security perimeter.
|
||||
|
||||
AI access is bring-your-own-key and bring-your-own-model. Organizations can route workloads through approved commercial providers, private cloud endpoints, an internal LLM gateway, or local open-source models, with granular routing and policy controlled by the customer. There is no requirement for a Keygraph-operated control plane or model proxy.
|
||||
|
||||
Keygraph maintains a SOC 2 Type II audit and makes the current report available to customers under appropriate confidentiality terms.
|
||||
|
||||
## Shannon 3.0 vs. Keygraph Enterprise
|
||||
|
||||
| | Shannon Open Source | Keygraph Enterprise Platform |
|
||||
| --- | --- | --- |
|
||||
| Best for | Individual developers and teams running pentests locally or in CI/CD | Security organizations running a continuous AppSec program across many teams and repositories |
|
||||
| Code analysis | Multi-stage agentic review maps architecture, trust boundaries, exposed interfaces, and data flows, filters candidate vulnerabilities, and hands the survivors to live pentesting agents | Exhaustive parsed-code analysis: persistent Code Property Graphs, interprocedural source-to-sink and sanitizer modeling, cross-repository context, exploit-chain analysis, and business-logic invariant testing |
|
||||
| Pentesting | On-demand, source-aware white-box pentesting with proof by exploitation | Continuous white-box, black-box, and grey-box pentesting across applications and environments |
|
||||
| Additional AppSec coverage | Not included | SCA with reachability, secrets scanning, and business-logic invariant testing |
|
||||
| CI/CD and reporting | Official GitHub Action and reusable GitLab CI/CD component; staging, release, merge-request, and scheduled pentests; demonstrated-vulnerability severity gates; PDF, Markdown, JSON, SARIF, artifacts, and native security-workflow ingestion | Organization-wide policies and gating, centrally managed integrations, canonical findings, dashboards, analytics, SLA tracking, and compliance evidence |
|
||||
| Automated remediation and verification | Not included | AI-authored pull requests with targeted code and exploit verification |
|
||||
| Enterprise governance | N/A — local, single-operator CLI | SSO, SCIM, teams, ReBAC/ABAC/RBAC, audit logs, API keys, ownership, and SLA policies |
|
||||
| Deployment and AI | Self-hosted, no telemetry, BYOM, and fully air-gapped with a local model | Fully on-premises or air-gapped, BYOK/BYOM, and granular routing through customer-controlled gateways |
|
||||
| License and support | AGPL-3.0 and community support | Commercial license, enterprise support, and SOC 2 Type II controls |
|
||||
Keygraph maintains a SOC 2 Type II audit. The report is available to customers under NDA.
|
||||
|
||||
## Talk to Keygraph
|
||||
|
||||
Visit [keygraph.io](https://keygraph.io), book a [Keygraph demo](https://cal.com/team/keygraph/shannon-pro), or contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||
Visit [keygraph.io](https://keygraph.io), book a [demo](https://cal.com/team/keygraph/shannon-pro), or email [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||
@@ -132,6 +132,6 @@ Exploit-mode scans write SARIF 2.1.0 by default, so findings land in GitHub code
|
||||
|
||||
### Conclusion
|
||||
|
||||
The complaint we hear most from CISOs about AI pentesting isn't accuracy, it's cost. At $4,000 a scan, the tier Doyensec purchased from both XBOW and Aikido, the math works for an annual check-up. It doesn't work per release, and teams are shipping faster than ever. A large enterprise with 5,000 repositories is looking at $20 million for a single pass.
|
||||
The complaint we hear most from CISOs about AI pentesting is the economics of continuous coverage. Doyensec paid $4,000 for each Aikido and XBOW assessment. At that benchmark price, treating 5,000 repositories as separate assessment targets would imply $20 million for one portfolio-wide pass before enterprise discounts. That may be viable for selective annual testing, but not for testing an entire portfolio at release cadence.
|
||||
|
||||
Shannon v3 running DeepSeek v4 Flash scanned Photoview for $6.10 and caught the same critical SQL injection the $4,000 platforms caught. The same enterprise would pay about $30,000 for that pass. The cheap model doesn't catch everything: our Claude Opus 5 run found 6 of 7 patched vulnerabilities to DeepSeek's 3, for $115. That's the point. Run the cheap model on every change, run the heavy one on a schedule, and continuous pentesting becomes affordable.
|
||||
Shannon v3 running DeepSeek v4 Flash scanned Photoview for $6.10 and caught the same critical SQL injection the $4,000 platforms caught. At the observed DeepSeek model cost, 5,000 equivalent Shannon runs would consume about $30,500 in model tokens, before infrastructure and operational costs. The cheap model doesn't catch everything: our Claude Opus 5 run found 6 of 7 patched vulnerabilities to DeepSeek's 3, for $115. These results support a tiered testing strategy in which teams use lower-cost models for frequent scans and more capable models for periodic deeper assessments, making continuous pentesting economically practical.
|
||||
+272
-158
@@ -1,20 +1,23 @@
|
||||
# Shannon Full LLM Context
|
||||
|
||||
> Combined README and documentation for AI agents and LLMs. This file is a hand-maintained copy of the repository Markdown files, updated by hand whenever those files change. For the concise index, see [llms.txt](llms.txt).
|
||||
|
||||
> Combined README and documentation for AI agents and LLMs, generated verbatim from the local files referenced in llms.txt. For the concise index, see [llms.txt](llms.txt).
|
||||
|
||||
---
|
||||
|
||||
# File: README.md
|
||||
|
||||
> [!NOTE]
|
||||
> **Shannon 3.0 is live:** deeper security code analysis, a rebuilt terminal experience, native CI/CD workflows, professional PDF reports, and SARIF—still fully open source, self-hosted, and bring-your-own-model.
|
||||
> **[Shannon 3.0 is live](https://github.com/KeygraphHQ/shannon/discussions/439):** deeper security code analysis, more thoroughly vetted findings, a rebuilt CLI, native CI/CD, professional PDF reports, and SARIF.
|
||||
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./assets/github-banner-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="./assets/github-banner-light.png">
|
||||
<img src="./assets/github-banner-light.png" alt="Shannon, AI Pentester for Web Apps and APIs, by Keygraph" width="100%">
|
||||
</picture>
|
||||
|
||||

|
||||
|
||||

|
||||
<a href="https://trendshift.io/repositories/15604" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15604" alt="KeygraphHQ%2Fshannon | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
### Shannon is an autonomous, AI pentester for web applications and APIs.
|
||||
|
||||
@@ -22,13 +25,21 @@ It analyzes your source code, identifies attack paths, and executes real exploit
|
||||
|
||||
**This repository is Shannon Open Source: the full agent, run locally from your command line.**
|
||||
|
||||
---
|
||||
<p><strong>Launch Shannon</strong></p>
|
||||
|
||||
 
|
||||
```bash
|
||||
npx @keygraph/shannon@latest
|
||||
```
|
||||
|
||||
<sub>The interactive launcher will guide you through setup and your first pentest.</sub>
|
||||
|
||||
---
|
||||
|
||||
<a href="https://discord.gg/9ZqQPuhJB7"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/discord_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/discord_button_light.png"><img src="./assets/discord_button_light.png" height="40" alt="Join Discord"></picture></a> <a href="https://keygraph.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/keygraph_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/keygraph_button_light.png"><img src="./assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></picture></a>
|
||||
|
||||
---
|
||||
|
||||
</div>
|
||||
|
||||
> [!TIP]
|
||||
> **AI agents and LLMs:** start with [llms.txt](llms.txt) for a concise map of this repository, or use [llms-full.txt](llms-full.txt) for the README and docs combined into one file.
|
||||
@@ -37,19 +48,33 @@ It analyzes your source code, identifies attack paths, and executes real exploit
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [What is Shannon?](#what-is-shannon)
|
||||
- [Why Shannon Exists](#why-shannon-exists)
|
||||
- [Why "Shannon"?](#why-shannon)
|
||||
- [Not a replacement for human pentesters](#not-a-replacement-for-human-pentesters)
|
||||
- [Shannon in Action](#shannon-in-action)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Run Shannon](#run-shannon)
|
||||
- [Key Capabilities](#key-capabilities)
|
||||
- [CI/CD Integrations](#cicd-integrations)
|
||||
- [GitHub Actions](#github-actions)
|
||||
- [Editions](#editions)
|
||||
- [Architecture](#architecture)
|
||||
- [Documentation](#documentation)
|
||||
- [Safety, Scope, and Limitations](#safety-scope-and-limitations)
|
||||
- [License](#license)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [About Keygraph](#about-keygraph)
|
||||
- [Community and Support](#community-and-support)
|
||||
- [Common Questions](#common-questions)
|
||||
- [Can I self-host Shannon?](#can-i-self-host-shannon)
|
||||
- [Does Shannon support bring your own key (BYOK)?](#does-shannon-support-bring-your-own-key-byok)
|
||||
- [Does Shannon output SARIF?](#does-shannon-output-sarif)
|
||||
- [Which AI providers does Shannon support?](#which-ai-providers-does-shannon-support)
|
||||
- [Can I run Shannon on a local or self-hosted model?](#can-i-run-shannon-on-a-local-or-self-hosted-model)
|
||||
- [Does Shannon actually exploit vulnerabilities, or just scan?](#does-shannon-actually-exploit-vulnerabilities-or-just-scan)
|
||||
|
||||
|
||||
|
||||
@@ -61,33 +86,50 @@ Shannon analyzes your web application's source code to identify potential attack
|
||||
|
||||
Shannon is the agent. This repository is Shannon Open Source, the standalone pentester you run yourself. The same Shannon also powers the [Keygraph platform](https://keygraph.io), Keygraph's commercial pentesting product. See [Editions](#editions) for how the two compare.
|
||||
|
||||
### Why Shannon Exists
|
||||
<a id="why-shannon-exists"></a>
|
||||
<details>
|
||||
<summary><strong>Why Shannon Exists</strong></summary>
|
||||
|
||||
Thanks to tools like Claude Code and Cursor, your team ships code non-stop. But your penetration test? That happens once a year. This creates a massive security gap. For the other 364 days, you could be unknowingly shipping vulnerabilities to production.
|
||||
|
||||
Shannon closes that gap by providing on-demand, automated penetration testing that can run against every build or release.
|
||||
|
||||
### Why "Shannon"?
|
||||
</details>
|
||||
|
||||
<a id="why-shannon"></a>
|
||||
<details>
|
||||
<summary><strong>Why "Shannon"?</strong></summary>
|
||||
|
||||
It's named after Claude Shannon, the father of information theory. At its core, pentesting is an information problem: every probe reduces uncertainty about a system's state. The best tools maximize the signal gained from every request, turning those bits of knowledge into an exploit path.
|
||||
|
||||
Also, we wanted you to be able to say, "Hey Claude, run Shannon" to find all the security flaws in your vibe-coded app.
|
||||
|
||||
</details>
|
||||
|
||||
<a id="not-a-replacement-for-human-pentesters"></a>
|
||||
<details>
|
||||
<summary><strong>Not a replacement for human pentesters</strong></summary>
|
||||
|
||||
Shannon is built to work alongside expert pentesters and red teamers, not replace them. Great pentesters understand the business, chain attacks in ways nobody anticipated, and bring years of judgment that current models can't match.
|
||||
|
||||
Shannon solves a different problem: there is far more software to test than security teams have time to cover. Critical systems get periodic expert assessments, while the long tail of internal apps, APIs, and fast-moving services rarely gets tested at all.
|
||||
|
||||
Shannon shifts pentesting left into the software development lifecycle (SDLC). Use it to run exploitation-backed tests against staging environments and releases at the cadence they actually ship, and save expert human time for the risks that need someone who knows the organization.
|
||||
|
||||
</details>
|
||||
|
||||
## Shannon in Action
|
||||
|
||||

|
||||
|
||||
Sample penetration test reports from intentionally vulnerable applications, produced by Shannon Open Source:
|
||||
|
||||
|
||||
| Target | Summary | Report |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
|
||||
| OWASP Juice Shop | 20+ vulnerabilities, including authentication bypass, SQL injection, IDOR, and SSRF. | [View report](sample-reports/shannon-report-juice-shop.md) |
|
||||
| c{api}tal API | Approximately 15 critical and high-severity API findings, including command injection, auth bypass, and mass assignment. | [View report](sample-reports/shannon-report-capital-api.md) |
|
||||
| OWASP crAPI | 15+ critical and high-severity findings across JWT, injection, SSRF, and API authorization paths. | [View report](sample-reports/shannon-report-crapi.md) |
|
||||
|
||||
These reports are from Shannon Open Source scans of Photoview 2.4.0, one of the applications in Doyensec's comparison of Aikido and XBOW. We ran Shannon against the same application version and evaluated its results separately. Read the [Doyensec study](https://doyensec.com/resources/ComparingAIApplicationSecurityTestingPlatforms_Doyensec.pdf) and our [Shannon follow-up comparison](docs/shannon-xbow-aikido-benchmark.md) for the methodology, limitations, costs, and results.
|
||||
|
||||
|
||||
| Model | Report | SARIF |
|
||||
| ----------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
||||
| DeepSeek v4 Flash | [View report](benchmark/photoview-deepseek-v4-flash.pdf) | [SARIF](benchmark/photoview-deepseek-v4-flash.sarif) |
|
||||
| Grok 4.6 | [View report](benchmark/photoview-grok-4-6.pdf) | [SARIF](benchmark/photoview-grok-4-6.sarif) |
|
||||
| Claude Opus 5 | [View report](benchmark/photoview-opus-5.pdf) | [SARIF](benchmark/photoview-opus-5.sarif) |
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -97,7 +139,7 @@ Sample penetration test reports from intentionally vulnerable applications, prod
|
||||
|
||||
- **Docker**: required for the worker container.
|
||||
- **Node.js 18+**: required for the recommended `npx` workflow.
|
||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, [any other provider](docs/ai-providers.md#any-other-provider) in the harness catalogue, and any endpoint that speaks the Anthropic Messages API or the OpenAI Chat Completions or Responses API through a [custom base URL](docs/ai-providers.md#custom-base-url). You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic. See [AI providers](docs/ai-providers.md#suggested-models) for suggested model IDs.
|
||||
- **AI provider credentials**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and [any other provider](docs/ai-providers.md#any-other-provider) in the harness catalogue — each of which you can point at a proxy or LLM gateway through a [custom base URL](docs/ai-providers.md#custom-base-url), and a model the catalogue does not carry can be described with a [custom model configuration](docs/ai-providers.md#custom-model-configuration). You bring your own key, and Keygraph never proxies your model traffic. Shannon is provider-agnostic. See [AI providers](docs/ai-providers.md#suggested-models) for suggested model IDs.
|
||||
- **Cyber safeguards cleared with your provider**: Anthropic and OpenAI apply real-time safeguards to cyber-security workloads, which can interrupt a scan mid-run. Complete their guidance for legitimate security testers before your first run - see [AI providers](docs/ai-providers.md#cyber-safeguards-do-this-before-your-first-scan).
|
||||
|
||||
|
||||
@@ -132,17 +174,17 @@ For source builds, authenticated scans, provider-specific setup, and platform no
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- **No exploit, no report**: Shannon includes a vulnerability only after validating it with a working, reproducible proof of concept—eliminating the speculative warnings typical of scanners.
|
||||
- **Advanced security code analysis**: Before it sends a single payload, Shannon reads the codebase and builds a picture of the application: architecture, trust boundaries, exposed interfaces, data flows, and the assets worth attacking. From there it opens targeted investigations and filters the candidates they turn up. What survives goes to the live pentesting agents.
|
||||
- **Autonomous execution**: Shannon launches reconnaissance, vulnerability analysis, exploitation, and report generation from a single command.
|
||||
- **Live terminal experience**: A rebuilt CLI makes scans easy to configure and shows agent progress and clean results without requiring operators to inspect the underlying orchestration logs.
|
||||
- **Authenticated testing**: configuration files can describe login flows, test credentials, TOTP, email-based login flows, focus areas, and rules of engagement.
|
||||
- **OWASP-focused coverage**: Shannon targets exploitable Injection, XSS, SSRF, Broken Authentication, and Broken Authorization issues.
|
||||
- **Resumable workspaces**: Shannon can resume interrupted runs without re-running completed agents.
|
||||
- **Native CI/CD integrations**: Run Shannon through the official GitHub Action or reusable GitLab CI/CD component. Preserve reports, SARIF, and logs as pipeline artifacts; publish findings into native security workflows; and gate releases only on vulnerabilities Shannon actually demonstrates.
|
||||
- **Professional and machine-readable reports**: Shannon generates evidence-rich PDF and Markdown reports plus structured JSON and SARIF 2.1.0. SARIF is enabled by default on exploit-mode scans and can be disabled with `report.sarif: "false"`.
|
||||
- **Bring your own key, provider-agnostic**: Shannon runs on Anthropic, OpenAI, xAI, AWS Bedrock, and any endpoint speaking the Anthropic Messages API or the OpenAI Chat Completions or Responses API, including self-hosted models served through Ollama, vLLM, or LM Studio and gateways such as OpenRouter and LiteLLM. You supply the credentials and choose exactly where model traffic goes. Local and self-hosted models are supported.
|
||||
- **Private by design**: Shannon runs inside your infrastructure and writes results to a local workspace. Model requests go straight to the provider or endpoint you configure, and they carry source and application context with them, so choose that endpoint deliberately. Point Shannon at a local model endpoint and nothing leaves your environment.
|
||||
- **No exploit, no report**: Reports only vulnerabilities confirmed with a reproducible proof of concept, reducing speculative scanner noise.
|
||||
- **Advanced code analysis**: Maps architecture, trust boundaries, interfaces, data flows, and critical assets before sending credible attack paths to live pentesting agents.
|
||||
- **Autonomous execution**: Runs reconnaissance, analysis, exploitation, and reporting from a single command.
|
||||
- **Live terminal experience**: Simplifies scan setup and shows agent progress and results without exposing orchestration logs.
|
||||
- **Authenticated testing**: Supports credentials, login flows, TOTP, email authentication, focus areas, and rules of engagement through configuration.
|
||||
- **OWASP-focused coverage**: Tests for exploitable injection, XSS, SSRF, broken authentication, and broken authorization.
|
||||
- **Resumable workspaces**: Resumes interrupted scans without repeating completed work.
|
||||
- **Native CI/CD integrations**: Runs through the official GitHub Action or GitLab CI/CD component, preserves artifacts, publishes findings, and gates releases on proven vulnerabilities.
|
||||
- **Multi-format reports**: Produces evidence-rich PDF and Markdown reports plus JSON and SARIF 2.1.0. SARIF is enabled by default for exploit-mode scans.
|
||||
- **Provider agnostic and BYOK**: Supports Anthropic, OpenAI, xAI, AWS Bedrock, compatible APIs and LLM gateways, and local models served through Ollama, vLLM, or LM Studio.
|
||||
- **Private by design**: Runs in your infrastructure, stores results locally, and sends model requests directly to your chosen endpoint. A local endpoint keeps data inside your environment.
|
||||
|
||||
|
||||
|
||||
@@ -201,24 +243,11 @@ See the [Shannon GitHub Action documentation](https://github.com/KeygraphHQ/shan
|
||||
|
||||
## Editions
|
||||
|
||||
**Shannon Open Source** is the complete autonomous pentester for developers and security teams. It is optimized for fast local and CI/CD runs: understand the application, execute real attacks, and report only proven vulnerabilities.
|
||||
**Shannon Open Source** is a complete autonomous pentester, especially well suited to individual developers and small teams running focused security tests locally or in CI/CD.
|
||||
|
||||
**Keygraph Enterprise Platform** turns Shannon's proof engine into an organization-wide AppSec program, adding exhaustive analysis, centralized vulnerability management, automated remediation, enterprise governance, and continuous operation at scale.
|
||||
**Keygraph Enterprise Platform** is for organizations that need a shared platform for continuous agentic pentesting/AppSec across many teams, repositories, and environments. It centralizes deeper analysis, vulnerability management, remediation, verification, governance, and reporting so teams do not have to assemble and maintain those workflows themselves.
|
||||
|
||||
|
||||
| | Shannon Open Source | Keygraph Enterprise Platform |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Best for | Local and CI/CD pentesting | Continuous AppSec across teams and repositories |
|
||||
| Security analysis | Multi-stage agentic review models architecture, trust boundaries, and data flows, filters candidate vulnerabilities, and hands the survivors to live pentesting agents | Exhaustive parsed-code agentic SAST: persistent Code Property Graphs, interprocedural source-to-sink and sanitizer modeling, cross-repository context, exploit-chain analysis, and business-logic testing |
|
||||
| Additional coverage | Not included | SCA with reachability, secrets scanning, and business-logic testing |
|
||||
| AppSec operations | N/A — standalone CLI | Canonical findings, deduplication, SLAs, analytics, automated remediation, and targeted verification |
|
||||
| Governance | N/A — local, single-operator CLI | SSO, SCIM, granular access control, APIs, and full audit logging |
|
||||
| Deployment | Self-hosted, air-gapped, BYOM, AGPL-3.0 | On-premises or air-gapped, granular model routing, commercial support |
|
||||
|
||||
|
||||
Shannon Open Source is not a trial edition. Choose Keygraph Enterprise when you need deeper analysis and a governed, closed-loop AppSec program.
|
||||
|
||||
[Explore the Keygraph Enterprise Platform →](docs/keygraph-platform.md)
|
||||
[Learn about the Keygraph Enterprise Platform and compare editions →](docs/keygraph-platform.md)
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -265,7 +294,7 @@ Use these guides for operational detail:
|
||||
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Source build and CLI commands](docs/development.md) | Cloning, building, common commands, output paths, and local development. |
|
||||
| [Configuration](docs/configuration.md) | Authenticated testing, login flows, rules of engagement, and report filters. |
|
||||
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock, and any other Pi-supported provider), and custom gateways. |
|
||||
| [AI providers](docs/ai-providers.md) | Selecting the model, the supported providers (Anthropic, OpenAI, xAI, AWS Bedrock, and any other Pi-supported provider), and custom LLM gateways. |
|
||||
| [Platforms and networking](docs/platforms.md) | Windows/WSL2, Linux, macOS, Docker networking, local apps, and custom hostnames. |
|
||||
| [Workspaces and resuming](docs/workspaces.md) | Naming workspaces, resuming interrupted scans, and workspace storage. |
|
||||
| [Safety and limitations](docs/safety.md) | Authorized-use requirements, non-production guidance, mutative effects, cost, and model caveats. |
|
||||
@@ -285,7 +314,7 @@ Important limitations:
|
||||
|
||||
- Shannon Open Source is tuned for fast, code-informed pentesting in everyday development and CI/CD. Exhaustive agentic SAST, broader scanner coverage, centralized governance, and full-lifecycle vulnerability management are delivered through the Keygraph Enterprise Platform.
|
||||
- Findings still require human review. LLM-generated reports can contain weakly supported or incorrect details.
|
||||
- Anthropic, OpenAI, xAI, and AWS Bedrock are built-in providers, and any Anthropic Messages API or OpenAI Chat Completions or Responses API endpoint works through a custom base URL. Model capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker results.
|
||||
- Anthropic, OpenAI, xAI, and AWS Bedrock are built-in providers, and any other provider in the harness catalogue works too — each reachable through a custom base URL that points it at a proxy or LLM gateway. Model capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker results.
|
||||
- A full run can take roughly 1 to 1.5 hours and may incur LLM API costs depending on model pricing and application complexity.
|
||||
- Do not scan untrusted or adversarial codebases. AI-powered tools that read source code can be exposed to prompt injection.
|
||||
|
||||
@@ -299,6 +328,14 @@ Commercial and enterprise licensing is available for organizations that need dif
|
||||
|
||||
For commercial licensing, contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Thanks to [Pi](https://github.com/earendil-works/pi),
|
||||
[Playwright CLI](https://github.com/microsoft/playwright-cli),
|
||||
and [Mantis](https://github.com/google/mantis).
|
||||
|
||||
See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for licensing and attribution details.
|
||||
|
||||
## About Keygraph
|
||||
|
||||
**Keygraph** is the company behind Shannon. It also builds the **Keygraph platform**, the commercial agentic pentesting product that closes the full AppSec lifecycle and runs an enhanced build of Shannon as its pentesting engine.
|
||||
@@ -346,17 +383,18 @@ Yes. Shannon emits SARIF 2.1.0, the OASIS standard format for static analysis re
|
||||
|
||||
### Which AI providers does Shannon support?
|
||||
|
||||
Anthropic, OpenAI, xAI, and AWS Bedrock are built in and configured directly by provider ID. Beyond those, Shannon runs on any endpoint that implements the Anthropic Messages API or the OpenAI Chat Completions or Responses API, reached through a custom base URL. The rule is the API format, not the vendor. Shannon uses a single unified model setting throughout a pentest.
|
||||
Anthropic, OpenAI, xAI, and AWS Bedrock are built in and configured directly by provider ID. Beyond those, Shannon runs on any provider in the Pi harness catalogue, named the same `<provider>:<model-id>` way. Any provider can be pointed at a proxy or LLM gateway through a custom base URL, which overrides only the endpoint and keeps that provider's API dialect. A model the catalogue does not carry, such as one a router or gateway serves under its own ID, or a self-hosted model, is described in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file and passed with `--models-config`. Shannon uses a single unified model setting throughout a pentest.
|
||||
|
||||
### Can I run Shannon on a local or self-hosted model?
|
||||
|
||||
Shannon works with local models served through Ollama, vLLM, or LM Studio, which expose an OpenAI-compatible endpoint, as well as routers such as OpenRouter and gateways such as LiteLLM. Point Shannon at the endpoint with a custom base URL. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves. See [AI providers](docs/ai-providers.md#custom-base-url).
|
||||
Shannon works with local models served through Ollama, vLLM, or LM Studio, which expose an OpenAI-compatible endpoint, as well as routers such as OpenRouter and LLM gateways such as LiteLLM. A model the harness catalogue does not carry, which most self-hosted models are, is described in a [custom model configuration](docs/ai-providers.md#custom-model-configuration) file passed with `--models-config`; routers and gateways can also be reached with a custom base URL. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves. See [Local and self-hosted models](docs/ai-providers.md#local-and-self-hosted-models).
|
||||
|
||||
### Does Shannon actually exploit vulnerabilities, or just scan?
|
||||
|
||||
Shannon executes real exploits. It reports a finding only when it has produced a working proof-of-concept, and discards hypotheses it cannot prove. It is a pentester, not a passive scanner.
|
||||
|
||||
**Built by [Keygraph](https://keygraph.io)**
|
||||
|
||||
---
|
||||
|
||||
# File: docs/development.md
|
||||
@@ -747,12 +785,15 @@ Shannon forwards only the selected provider's credential into the scan container
|
||||
Shannon accepts any provider and model present in the Pi harness catalogue. Browse them at [pi.dev/models](https://pi.dev/models).
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=your-api-key # the provider's API key
|
||||
export SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3 # <provider>:<model-id>
|
||||
export SHANNON_AI_API_KEY=your-api-key # the provider's key — or the gateway's when a base URL is set
|
||||
export SHANNON_AI_MODEL=openrouter:moonshotai/kimi-k3 # <provider>:<model-id>
|
||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com # optional: route through a proxy or LLM gateway
|
||||
```
|
||||
|
||||
This path covers providers whose credential is a single API key. Providers that need more than that are not currently supported.
|
||||
|
||||
A model the catalogue does not carry is reachable by describing it yourself. See [Custom model configuration](#custom-model-configuration).
|
||||
|
||||
`npx @keygraph/shannon setup` exposes this as the **Other provider** option.
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -767,7 +808,7 @@ Review each vendor's guidance and complete the verification or enrollment they a
|
||||
- Anthropic - [Real-time cyber safeguards on Claude Opus and Sonnet](https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet)
|
||||
- OpenAI - [Cyber](https://chatgpt.com/cyber)
|
||||
|
||||
This applies to the Anthropic and OpenAI providers, including when either is reached through a gateway. Bedrock serves Claude models and is subject to Anthropic's safeguards as well.
|
||||
This applies to the Anthropic and OpenAI providers, including when either is reached through an LLM gateway. Bedrock serves Claude models and is subject to Anthropic's safeguards as well.
|
||||
|
||||
## Suggested models
|
||||
|
||||
@@ -823,17 +864,18 @@ Bedrock uses bearer-token authentication only. IAM access keys, session tokens,
|
||||
|
||||
## Custom base URL
|
||||
|
||||
To route model traffic through your own infrastructure — a corporate proxy, an LLM gateway such as LiteLLM, or a regional endpoint — set a base URL alongside your normal model selection. The provider half of `SHANNON_AI_MODEL` decides which key is sent and which API Shannon speaks, so pick the one your gateway serves:
|
||||
`SHANNON_AI_BASE_URL` routes model traffic through a proxy or LLM gateway instead of the provider's default endpoint — an LLM gateway such as LiteLLM, a regional endpoint, or any other host you choose. It is a plain endpoint override: it changes only *where* requests go. The provider half of `SHANNON_AI_MODEL` still decides which credential is sent and which API dialect is spoken, and that is unchanged by the base URL.
|
||||
|
||||
| Gateway serves | Model prefix | API key |
|
||||
| --- | --- | --- |
|
||||
| Anthropic Messages | `anthropic:` | `SHANNON_AI_API_KEY` |
|
||||
| OpenAI Chat Completions | `openai:` | `SHANNON_AI_API_KEY` |
|
||||
| OpenAI Responses | `openai:` + `SHANNON_AI_OPENAI_FORMAT=responses` | `SHANNON_AI_API_KEY` |
|
||||
This works for **any** provider, curated or not, subject to two rules. A provider's dialect is fixed, so the endpoint you point at must speak that provider's dialect:
|
||||
|
||||
The model ID is whatever name your gateway serves it under; it does not have to exist in Shannon's catalogue.
|
||||
| Provider prefix | Dialect the endpoint must speak |
|
||||
| --- | --- |
|
||||
| `anthropic:` | Anthropic Messages |
|
||||
| `openai:` | OpenAI Responses |
|
||||
|
||||
Anthropic Messages:
|
||||
And the model ID must still resolve in the harness catalogue. A base URL changes only the address; it grants no exemption from that check. A gateway serving a model under its own name needs that name described in a [custom model configuration](#custom-model-configuration) file.
|
||||
|
||||
Anthropic Messages LLM gateway:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=sk-ant-...
|
||||
@@ -841,7 +883,7 @@ export SHANNON_AI_MODEL=anthropic:claude-sonnet-4-6
|
||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com
|
||||
```
|
||||
|
||||
OpenAI Chat Completions:
|
||||
OpenAI Responses LLM gateway:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=sk-...
|
||||
@@ -849,19 +891,122 @@ export SHANNON_AI_MODEL=openai:gpt-5.6-sol
|
||||
export SHANNON_AI_BASE_URL=https://llm-gateway.example.com/v1
|
||||
```
|
||||
|
||||
`SHANNON_AI_MODEL` is always `<provider>:<model-id>`, gateway or not.
|
||||
`npx @keygraph/shannon setup` configures a base URL two ways: **Custom Base URL** covers the common Anthropic Messages and OpenAI Responses LLM gateways, and **Other provider** takes any provider ID plus an optional base URL of its own.
|
||||
|
||||
OpenAI is the one provider serving two APIs, so a gateway run picks one:
|
||||
## Custom model configuration
|
||||
|
||||
A custom model configuration is a Pi `models.json` file that describes a model the harness catalogue does not carry: one a router or gateway serves under its own ID, or a local server (see [Local and self-hosted models](#local-and-self-hosted-models)). You pass it with `--models-config`, and Shannon merges its definitions over the catalogue so `SHANNON_AI_MODEL` can then name the model like any other:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_OPENAI_FORMAT=responses # default: chat-completions
|
||||
npx @keygraph/shannon start -u https://example.com -r /path/to/repo --models-config ./models.json
|
||||
```
|
||||
|
||||
Chat Completions is the default because that is what most gateway software exposes. Set `responses` for a gateway that passes the Responses API through — it preserves reasoning state between turns, which Chat Completions cannot. `openai:gpt-5` with no base URL always calls OpenAI's Responses API directly.
|
||||
```bash
|
||||
./shannon start -u https://example.com -r ./my-repo --models-config ./models.json
|
||||
```
|
||||
|
||||
The variable is rejected in preflight where it cannot take effect: with a non-`openai` model, since Anthropic, xAI, and Bedrock each serve one API, and with no `SHANNON_AI_BASE_URL`, since a direct OpenAI run is always Responses.
|
||||
[pi.dev/models](https://pi.dev/models) supplies the file contents. Find the model under the provider you want, since the same model has a different ID per provider, then open its page and expand **Show configuration** for a ready-to-paste snippet:
|
||||
|
||||
`npx @keygraph/shannon setup` covers this under **Custom Base URL**, which asks which API your gateway serves and configures the matching provider for you.
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"apiKey": "YOUR_API_KEY",
|
||||
"models": [
|
||||
{
|
||||
"id": "z-ai/glm-5.3",
|
||||
"name": "Z.ai: GLM 5.3",
|
||||
"reasoning": true,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"thinkingLevelMap": {
|
||||
"off": null,
|
||||
"minimal": null,
|
||||
"low": "low",
|
||||
"medium": null,
|
||||
"high": "high",
|
||||
"xhigh": null,
|
||||
"max": "max"
|
||||
},
|
||||
"contextWindow": 1048576,
|
||||
"maxTokens": 943718,
|
||||
"cost": {
|
||||
"input": 1.4,
|
||||
"output": 4.4,
|
||||
"cacheRead": 0.26,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"thinkingFormat": "openrouter"
|
||||
}
|
||||
}
|
||||
],
|
||||
"api": "openai-completions",
|
||||
"baseUrl": "https://openrouter.ai/api/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then name the model the usual way:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=your-api-key
|
||||
export SHANNON_AI_MODEL=openrouter:z-ai/glm-5.3
|
||||
```
|
||||
|
||||
Leave `YOUR_API_KEY` exactly as it is. Shannon sends the credential from your environment, and that takes precedence over anything the file declares, so the file describes the model and never has to hold a secret.
|
||||
|
||||
Pi's [models documentation](https://pi.dev/docs/latest/models) describes the full format, including provider routing preferences and compatibility flags.
|
||||
|
||||
## Local and self-hosted models
|
||||
|
||||
Ollama, LM Studio, vLLM, and any other OpenAI-compatible server are reached through the same mechanism. Describe the server as a provider in a model config file, then name its model with `SHANNON_AI_MODEL`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Use `host.docker.internal`, not `localhost`. The scan runs inside a container, so `localhost` points at the container itself rather than at your machine.
|
||||
|
||||
A `models.json` for Ollama:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://host.docker.internal:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"models": [
|
||||
{ "id": "<model-id>" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then name the model and run:
|
||||
|
||||
```bash
|
||||
export SHANNON_AI_API_KEY=ollama # any value, see below
|
||||
export SHANNON_AI_MODEL=ollama:<model-id>
|
||||
./shannon start -u https://example.com -r ./my-repo --models-config ./models.json
|
||||
```
|
||||
|
||||
LM Studio and vLLM take the same shape on their own ports, `http://host.docker.internal:1234/v1` and `http://host.docker.internal:8000/v1` respectively. The provider name is yours to choose, and only has to match the prefix in `SHANNON_AI_MODEL`.
|
||||
|
||||
`SHANNON_AI_API_KEY` is still required even though a local server ignores it. Shannon checks that the selected provider has a credential before it starts, so set it to any placeholder value. It is sent to your server and discarded.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Shannon drives every phase through multi-turn tool use. Capability varies, and a model that does not follow Shannon's instructions or tool-use constraints reliably will produce weaker pentests than a frontier model, so take this path only if you know how your chosen model behaves.
|
||||
|
||||
Some servers need compatibility flags. If a reasoning-capable model is rejected, turn off the roles it does not understand, at either provider or model level:
|
||||
|
||||
```json
|
||||
"compat": { "supportsDeveloperRole": false, "supportsReasoningEffort": false }
|
||||
```
|
||||
|
||||
Pi's [models documentation](https://pi.dev/docs/latest/models) lists the full set of compatibility flags and local-runtime options.
|
||||
|
||||
## OpenAI Codex (ChatGPT Plus/Pro subscription)
|
||||
|
||||
@@ -928,7 +1073,8 @@ These instructions apply only to `shannon-v1`.
|
||||
|
||||
Checks run before a scan starts, so mistakes fail immediately rather than partway through a run:
|
||||
|
||||
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). A custom base URL exempts the model ID, since a gateway may serve its own names.
|
||||
- **Provider and model ID** — validated against the Pi harness catalogue. An unknown provider or model ID fails preflight with a pointer to [pi.dev/models](https://pi.dev/models). To run a model the catalogue does not carry, describe it with [`--models-config`](#custom-model-configuration).
|
||||
- **Model configuration** — when `--models-config` is passed, the file is parsed and schema-checked before the scan starts, and a fault fails preflight with the offending field named.
|
||||
- **Credential presence** — validated for the selected provider, or read from Pi when `SHANNON_USE_PI_AUTH=1`.
|
||||
- **Credential validity** — one minimal request against the model the scan will use, so a rejected key, an exhausted quota, or a model the account cannot reach fails before any agent runs. Bedrock included: its bearer token and region go through the same probe.
|
||||
|
||||
@@ -1189,134 +1335,102 @@ For organizations that need broader static and organizational coverage now, see
|
||||
|
||||
# Keygraph Enterprise Platform
|
||||
|
||||
Shannon 3.0 makes advanced, code-informed autonomous pentesting available to everyone. The open-source CLI maps routes and data flows, understands application architecture, executes real attacks, and produces PDF and SARIF results—locally, in CI/CD, or fully air-gapped with your own model.
|
||||
Shannon 3.0 is an open-source pentester. It reads your source, maps routes and data flows, runs real attacks against a live target, and writes PDF and SARIF reports. It runs locally, in CI, or air-gapped with your own model. Shannon Open Source is a complete pentester, not a trial edition.
|
||||
|
||||
The **Keygraph Enterprise Platform** is the commercial AppSec operating system for organizations that need to run that process continuously across many repositories, teams, and environments. It adds exhaustive agentic SAST, business-logic and source-to-sink analysis, broader scanner coverage, centralized vulnerability management, automated remediation and targeted verification, enterprise governance, and organization-wide reporting.
|
||||
Keygraph Enterprise runs an enterprise-hardened fork of Shannon continuously across hundreds of repositories and adds what a security team needs around it: audit-depth static analysis on a parsed code graph, business-logic testing, SCA and secrets scanning, one deduplicated record per vulnerability across scans and scanners, generated fixes, fix verification, and SSO, RBAC, and audit logs. It is for security teams that own vulnerability management across many engineering teams and need one place to triage, assign, fix, and verify.
|
||||
|
||||
> Shannon Open Source is a complete autonomous pentester, not a trial edition. Keygraph Enterprise is for teams that need greater analysis depth, shared control, and a closed-loop vulnerability-management program.
|
||||
Both editions are BYOK. Keygraph never receives your source and never proxies model traffic, open source or commercial. Shannon Open Source runs from your machine or CI runner. Keygraph Enterprise deploys as a platform inside your cloud or data center, including fully air-gapped.
|
||||
|
||||
## Who It Is For
|
||||
## Shannon Open Source vs. Keygraph Enterprise
|
||||
|
||||
Keygraph Enterprise is designed for organizations that need to:
|
||||
| | Shannon Open Source | Keygraph Enterprise |
|
||||
| --- | --- | --- |
|
||||
| Best for | Developers and teams running repository-level pentests locally or in CI | Security organizations running continuous AppSec across many teams and repositories |
|
||||
| Code analysis | Agent pass over architecture, entry points, and data flows to seed the pentest, sized to finish inside a CI run | Persistent code property graph plus a long-running analysis harness with interprocedural taint, sanitizer modeling, cross-repo context, exploit chains, and multi-pass review |
|
||||
| Pentesting | On-demand, source-aware white-box pentesting with optional authenticated testing, focused on injection, XSS, SSRF, broken authentication, and broken authorization, with proof by exploitation | Enterprise-hardened Shannon fork run continuously, with grey-box and black-box targets and business-logic invariant testing |
|
||||
| SCA and secrets | Not included | SCA with reachability and secrets scanning including history |
|
||||
| Findings | Per-run PDF, Markdown, JSON, and SARIF, with SARIF ingestion into GitHub code scanning | One record per vulnerability per repo across scans and scanners, plus ownership, SLAs, dashboards, and audit evidence |
|
||||
| Fixes and verification | Not included | Fix PRs with verification by re-analysis and exploit replay, with no full rescan required |
|
||||
| CI/CD and source control | GitHub Action and GitLab CI component for pull-request, release, and scheduled runs, with gates on `status: exploited` | GitHub, GitLab, Azure DevOps, and Bitbucket with organization-wide policy and centrally managed integrations |
|
||||
| Deployment and models | Runs locally or on a CI runner with BYOK to any Anthropic- or OpenAI-compatible endpoint or local model | Deployed in your AWS, GCP, Azure, or on-prem environment. Customer-hosted services and stored platform data remain inside your environment. Model requests go directly to the provider, private endpoint, gateway, or local model you configure. A local model supports fully disconnected deployments |
|
||||
| Governance, license, support | AGPL-3.0 and community support | SSO, SCIM, RBAC, and audit logs, plus a commercial license, enterprise support, and SOC 2 Type II |
|
||||
|
||||
- continuously test hundreds or thousands of repositories, services, applications, and APIs;
|
||||
- combine agentic pentesting, SAST, SCA, secrets, and business-logic findings in one system;
|
||||
- enforce security policy in GitHub Actions, GitLab CI, and enterprise delivery pipelines;
|
||||
- give developers one canonical, actionable record for each vulnerability instead of duplicate scanner alerts;
|
||||
- assign owners, apply SLAs, track status, and measure risk and remediation performance across the organization;
|
||||
- generate fixes and verify them without rerunning an entire scan;
|
||||
- enforce enterprise identity, authorization, audit, and API-access controls; and
|
||||
- deploy fully on-premises or air-gapped with customer-controlled models, keys, and routing.
|
||||
## How it fits your pipeline
|
||||
|
||||
## Close the Entire AppSec Loop
|
||||
1. Scans run on pull requests, releases, and a schedule against repositories in GitHub, GitLab, Azure DevOps, or Bitbucket.
|
||||
2. Pipelines gate on exploited severity. A code-analysis hypothesis never fails a build.
|
||||
3. Findings from every scanner and every run land as one record per vulnerability per repository, with an owner and an SLA. The same finding across ten runs is one record, not ten alerts.
|
||||
4. From a finding, Keygraph opens a fix PR into your normal review flow.
|
||||
5. Verification confirms the fix against the changed code and the original exploit. No full rescan is required.
|
||||
|
||||
The platform connects discovery, triage, remediation, and verification in one continuous workflow:
|
||||
## What is different technically
|
||||
|
||||
1. **Analyze** every repository with exhaustive agentic SAST and complementary scanners.
|
||||
2. **Prove** exploitability with source-aware white-box, black-box, and grey-box pentesting.
|
||||
3. **Normalize and deduplicate** results into a canonical finding per vulnerability and repository.
|
||||
4. **Prioritize and assign** using severity, reachability, exploit evidence, ownership, policy, and business context.
|
||||
5. **Remediate** with an AI-authored patch delivered as a reviewable pull request.
|
||||
6. **Verify** the specific fix with deterministic checks and adversarial agent reasoning—without rerunning the full scan.
|
||||
7. **Track and govern** status, exceptions, SLAs, audit history, trends, and compliance evidence until closure.
|
||||
### Static analysis on a code property graph
|
||||
|
||||
## Exhaustive Agentic SAST
|
||||
Shannon Open Source's code analysis is sized to finish inside a CI run: agents read the repository, map the attack surface, and hand candidates to the pentester. Enterprise is built for depth instead. It first parses each repository into a persistent code property graph, then runs an analysis harness derived from one built for long-running vulnerability audits, heavily adapted to query the graph rather than read files. The harness decomposes the application into risk, taint-flow, framework, and specialist tasks and supports longer-running audit workflows beyond typical CI job windows.
|
||||
|
||||
Shannon 3.0's open-source code analysis runs a multi-stage agentic workflow. It models application architecture, trust boundaries, exposed interfaces, and data flows, opens targeted investigations, reviews the candidates they turn up, and hands the survivors to live pentesting agents. That workflow is built for practical local and CI/CD runs.
|
||||
On the graph, it performs:
|
||||
|
||||
The Enterprise engine goes further, for audits at organization scale. It parses the codebase and builds persistent structural context before agents start reasoning about security:
|
||||
- Interprocedural taint tracking across functions, files, fields, containers, and framework request lifecycles.
|
||||
- Source, sink, and sanitizer modeling that records where validation, encoding, or authorization changes a path.
|
||||
- Cross-repository modeling of services, entry points, and trust boundaries.
|
||||
- Semantic deduplication of variants of the same defect, and exploit-chain analysis for combinations with higher impact than any single issue.
|
||||
- Multiple review passes per candidate, checking the agent's claim against the graph and available deployment and configuration context. Candidates that cannot be substantiated are not reported.
|
||||
|
||||
- **Repository and architecture modeling** identifies services, frameworks, entry points, assets, trust boundaries, and cross-repository relationships.
|
||||
- **Interprocedural call and data-flow analysis** traces values across functions, files, fields, containers, and framework-managed request lifecycles.
|
||||
- **Source, sink, and sanitizer modeling** follows untrusted input to sensitive operations and records where validation, encoding, authorization, or other controls alter the path.
|
||||
- **Threat-driven decomposition** breaks large applications into risk, taint-flow, framework, and specialist analysis tasks so deep scans remain systematic.
|
||||
- **Exhaustive adversarial verification** challenges candidates across multiple review passes, weighing structural evidence against what the agents found, then asks whether each one is viable in the application's production configuration.
|
||||
- **Semantic deduplication and exploit-chain analysis** consolidate variants of the same defect and identify combinations whose impact is greater than any isolated issue.
|
||||
- **Business-logic invariant testing** derives rules the code is supposed to preserve—such as tenant isolation, workflow order, approval limits, balances, and state transitions—then agents fuzz those invariants for application-specific flaws.
|
||||
### Business-logic invariants
|
||||
|
||||
The result is broad vulnerability hunting with precise paths back to the relevant code, not a flat list of pattern matches.
|
||||
Shannon Open Source focuses on injection, XSS, SSRF, and broken authentication and authorization. Enterprise adds testing for the bugs that do not fit a vulnerability class: it derives invariants the application is supposed to hold (tenant isolation, workflow ordering, approval limits, balance conservation, state transitions) and tests them against the running application. This is where application-specific vulnerabilities live and where pattern-based SAST often provides little or no signal.
|
||||
|
||||
### Proof by exploitation
|
||||
|
||||
The pentesting engine is a hardened fork of Shannon with the same rule: a pentest finding requires a working exploit. No exploit, no finding. Enterprise stores the exploit and replays it later to verify the fix.
|
||||
|
||||
SCA prioritizes vulnerable dependencies that application code actually reaches. Secrets scanning covers current source and repository history.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/agentic-sast-results.png" alt="Keygraph Enterprise SAST results grouped into business-logic issues, point issues, and secrets" width="100%">
|
||||
<img src="../assets/keygraph-platform/agentic-sast-results.png" alt="Keygraph Enterprise findings grouped into business-logic issues, point issues, and secrets" width="100%">
|
||||
</p>
|
||||
|
||||
## Complete Application-Security Coverage
|
||||
## Findings
|
||||
|
||||
Agentic SAST and pentesting work alongside additional first-class scanners:
|
||||
Shannon Open Source hands you a report per scan. Enterprise dedupes across runs and across scanners, deterministically and semantically, into one record per vulnerability per repository. Each record carries evidence, source location, severity, scan history, status, owner, resolution, and last-verified state.
|
||||
|
||||
- **SCA with reachability** prioritizes vulnerable dependencies that application code can actually reach.
|
||||
- **Full secrets scanning** detects credentials, tokens, and keys across source and repository history.
|
||||
- **Agentic pentesting** correlates code intelligence with live application behavior and attempts real exploitation. The core rule remains: no exploit, no pentest finding.
|
||||
Workflows cover assignment, triage, false-positive and risk-acceptance decisions, and SLA policies with escalation and aging. Dashboards report open risk, coverage, new versus resolved, SLA compliance, and MTTR, exportable as evidence for customers and auditors.
|
||||
|
||||
## One System of Record for Every Finding
|
||||
|
||||
Keygraph ingests results from every analysis source, correlates them, and maintains one canonical finding per vulnerability per repository. Security and engineering teams work from the same record, with evidence, source location, severity, scan history, status, assignee, resolution, and last-verification state.
|
||||
|
||||
The vulnerability-management layer provides:
|
||||
|
||||
- deterministic and semantic deduplication across scans and scanners;
|
||||
- ownership, assignment, triage, false-positive, risk-acceptance, and resolution workflows;
|
||||
- SLA policies, escalation, aging, and last-verified tracking;
|
||||
- bidirectional developer-workflow integrations and APIs;
|
||||
- dashboards for risk, coverage, trends, new versus resolved findings, SLA compliance, and MTTR; and
|
||||
- exportable evidence for customers, auditors, and compliance programs.
|
||||
Findings still require human review. Enterprise's extra review passes reduce weakly supported findings, but they do not eliminate them.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/canonical-findings.png" alt="Keygraph Enterprise canonical findings inventory with severity, status, source, and verification filters" width="100%">
|
||||
<img src="../assets/keygraph-platform/canonical-findings.png" alt="Keygraph Enterprise findings inventory with severity, status, source, and verification filters" width="100%">
|
||||
</p>
|
||||
|
||||
## Remediate, Then Verify the Fix
|
||||
### Fix and verify
|
||||
|
||||
From an individual finding, a user can ask Keygraph to produce a focused patch. The remediation agent reasons from the root cause and evidence, changes only the required code, and opens a pull request into the existing review process. It does not silently apply fixes to a protected branch.
|
||||
From a finding, Keygraph generates a patch scoped to that finding and opens a pull request. It never commits to a protected branch.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/automated-remediation.png" alt="Keygraph Enterprise remediation workflow for generating a fix and opening a pull request" width="100%">
|
||||
</p>
|
||||
|
||||
After a patch is available, targeted verification re-analyzes the affected code and, for dynamic pentest findings, re-tests the original proof of concept against the target. Deterministic checks and adversarial agent reasoning produce a clear verdict without the cost and delay of rerunning the entire scan.
|
||||
Verification re-analyzes the changed code and, for pentest findings, replays the original exploit against the patched target. The verdict comes from deterministic checks plus a review pass, without rerunning the full scan.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/targeted-verification.png" alt="Keygraph Enterprise targeted finding-verification workflow" width="100%">
|
||||
<img src="../assets/keygraph-platform/targeted-verification.png" alt="Keygraph Enterprise finding-verification workflow" width="100%">
|
||||
</p>
|
||||
|
||||
## Enterprise Governance and Integrations
|
||||
## Deployment and access control
|
||||
|
||||
Keygraph is built for shared operation across security, platform, and engineering teams:
|
||||
Keygraph Enterprise deploys entirely inside your AWS, GCP, Azure, or on-prem environment, including networks with no internet egress. There is no Keygraph-operated control plane. Customer-hosted services and stored platform data remain inside your environment for the life of the deployment.
|
||||
|
||||
- SAML 2.0 or OIDC single sign-on and SCIM provisioning;
|
||||
- organization, team, and user management;
|
||||
- built-in and custom roles with granular relationship-, attribute-, and role-based authorization (ReBAC, ABAC, and RBAC);
|
||||
- repository, pentest-profile, scanner, finding, and administration boundaries;
|
||||
- full audit logging and scoped API keys;
|
||||
- integrations with source control, CI/CD, ticketing, chat, and cloud environments; and
|
||||
- commercial support and enterprise onboarding.
|
||||
Model access is BYOK and BYOM. Route workloads to Anthropic, OpenAI, xAI, or Bedrock, a private cloud endpoint, your own gateway such as LiteLLM with your routing and policy applied, or local models on vLLM or Ollama. Model requests go directly to the endpoint you configure. Keygraph never receives or proxies them. A local model supports a fully disconnected deployment.
|
||||
|
||||
Access control: SAML/OIDC SSO, SCIM, roles with repository-scoped visibility (RBAC, plus attribute and relationship rules where needed), full audit log, scoped API keys.
|
||||
|
||||
<p align="center">
|
||||
<img src="../assets/keygraph-platform/enterprise-access-control.png" alt="Keygraph Enterprise granular roles and repository visibility controls" width="100%">
|
||||
<img src="../assets/keygraph-platform/enterprise-access-control.png" alt="Keygraph Enterprise roles and repository visibility controls" width="100%">
|
||||
</p>
|
||||
|
||||
## On-Premises, Air-Gapped, and Customer-Controlled AI
|
||||
|
||||
Keygraph Enterprise can run entirely inside your AWS, GCP, Azure, or on-premises environment, including networks with no public internet access. Deployments can keep source code, scan artifacts, findings, prompts, completions, and model traffic inside your security perimeter.
|
||||
|
||||
AI access is bring-your-own-key and bring-your-own-model. Organizations can route workloads through approved commercial providers, private cloud endpoints, an internal LLM gateway, or local open-source models, with granular routing and policy controlled by the customer. There is no requirement for a Keygraph-operated control plane or model proxy.
|
||||
|
||||
Keygraph maintains a SOC 2 Type II audit and makes the current report available to customers under appropriate confidentiality terms.
|
||||
|
||||
## Shannon 3.0 vs. Keygraph Enterprise
|
||||
|
||||
| | Shannon Open Source | Keygraph Enterprise Platform |
|
||||
| --- | --- | --- |
|
||||
| Best for | Individual developers and teams running pentests locally or in CI/CD | Security organizations running a continuous AppSec program across many teams and repositories |
|
||||
| Code analysis | Multi-stage agentic review maps architecture, trust boundaries, exposed interfaces, and data flows, filters candidate vulnerabilities, and hands the survivors to live pentesting agents | Exhaustive parsed-code analysis: persistent Code Property Graphs, interprocedural source-to-sink and sanitizer modeling, cross-repository context, exploit-chain analysis, and business-logic invariant testing |
|
||||
| Pentesting | On-demand, source-aware white-box pentesting with proof by exploitation | Continuous white-box, black-box, and grey-box pentesting across applications and environments |
|
||||
| Additional AppSec coverage | Not included | SCA with reachability, secrets scanning, and business-logic invariant testing |
|
||||
| CI/CD and reporting | Official GitHub Action and reusable GitLab CI/CD component; staging, release, merge-request, and scheduled pentests; demonstrated-vulnerability severity gates; PDF, Markdown, JSON, SARIF, artifacts, and native security-workflow ingestion | Organization-wide policies and gating, centrally managed integrations, canonical findings, dashboards, analytics, SLA tracking, and compliance evidence |
|
||||
| Automated remediation and verification | Not included | AI-authored pull requests with targeted code and exploit verification |
|
||||
| Enterprise governance | N/A — local, single-operator CLI | SSO, SCIM, teams, ReBAC/ABAC/RBAC, audit logs, API keys, ownership, and SLA policies |
|
||||
| Deployment and AI | Self-hosted, no telemetry, BYOM, and fully air-gapped with a local model | Fully on-premises or air-gapped, BYOK/BYOM, and granular routing through customer-controlled gateways |
|
||||
| License and support | AGPL-3.0 and community support | Commercial license, enterprise support, and SOC 2 Type II controls |
|
||||
Keygraph maintains a SOC 2 Type II audit. The report is available to customers under NDA.
|
||||
|
||||
## Talk to Keygraph
|
||||
|
||||
Visit [keygraph.io](https://keygraph.io), book a [Keygraph demo](https://cal.com/team/keygraph/shannon-pro), or contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||
Visit [keygraph.io](https://keygraph.io), book a [demo](https://cal.com/team/keygraph/shannon-pro), or email [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||
@@ -6,14 +6,13 @@ Use this file as the concise entry point for AI agents and LLMs reading this rep
|
||||
|
||||
## Start Here
|
||||
|
||||
- [README](README.md): Main project overview, editions, quick start, Shannon capabilities, Keygraph platform positioning, common questions, safety notes, licensing, and support links.
|
||||
- [Full Combined Context](llms-full.txt): README and documentation combined into one file for agents that need maximum local context.
|
||||
- [README](README.md): Main project overview, editions, quick start, Shannon capabilities, CI/CD integrations, common questions, safety notes, licensing, and support links.
|
||||
|
||||
## Shannon
|
||||
|
||||
- [Development](docs/development.md): Source-build workflow, common CLI commands, repository paths, and output locations.
|
||||
- [Configuration](docs/configuration.md): Authenticated testing, login flows, rules of engagement, report filters, credential precedence, and rate-limit settings.
|
||||
- [AI Providers](docs/ai-providers.md): Anthropic, OpenAI, xAI, AWS Bedrock, any other Pi-supported provider, and custom gateway setup.
|
||||
- [Configuration](docs/configuration.md): Authenticated testing, login flows, rules of engagement (including rate-limit guidance), report filters, and credential precedence.
|
||||
- [AI Providers](docs/ai-providers.md): Anthropic, OpenAI, xAI, AWS Bedrock, any other Pi-supported provider, custom LLM gateway setup, custom model configuration for models not yet in the Pi catalogue, and local self-hosted runtimes (Ollama, LM Studio, vLLM).
|
||||
- [Platforms and Networking](docs/platforms.md): Windows/WSL2, Linux, macOS, Docker networking, local applications, and custom hostnames.
|
||||
- [Workspaces and Resuming](docs/workspaces.md): Workspace storage, naming, resuming interrupted scans, and examples.
|
||||
- [Safety and Limitations](docs/safety.md): Authorized-use requirements, non-production guidance, mutative effects, model caveats, scope limits, cost, and performance.
|
||||
@@ -28,9 +27,3 @@ Use this file as the concise entry point for AI agents and LLMs reading this rep
|
||||
- [Keygraph website](https://keygraph.io): Company and commercial product information.
|
||||
- [Keygraph demo](https://cal.com/team/keygraph/shannon-pro): Demo and trial contact path.
|
||||
- [Community Discord](https://discord.gg/cmctpMBXwE): Community support and discussion.
|
||||
|
||||
## Optional
|
||||
|
||||
- [Sample Juice Shop report](sample-reports/shannon-report-juice-shop.md): Shannon sample report for OWASP Juice Shop.
|
||||
- [Sample c{api}tal API report](sample-reports/shannon-report-capital-api.md): Shannon sample report for c{api}tal API.
|
||||
- [Sample crAPI report](sample-reports/shannon-report-crapi.md): Shannon sample report for OWASP crAPI.
|
||||
Generated
+40
-73
@@ -46,17 +46,17 @@ importers:
|
||||
apps/worker:
|
||||
dependencies:
|
||||
'@earendil-works/pi-agent-core':
|
||||
specifier: ^0.84.2
|
||||
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
specifier: ^0.84.4
|
||||
version: 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-ai':
|
||||
specifier: ^0.84.2
|
||||
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
specifier: ^0.84.4
|
||||
version: 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-coding-agent':
|
||||
specifier: ^0.84.2
|
||||
version: 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
specifier: ^0.84.4
|
||||
version: 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@gotgenes/pi-permission-system':
|
||||
specifier: ^10.9.0
|
||||
version: 10.9.0(@earendil-works/pi-coding-agent@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.2)
|
||||
version: 10.9.0(@earendil-works/pi-coding-agent@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.4)
|
||||
'@temporalio/activity':
|
||||
specifier: 1.15.0
|
||||
version: 1.15.0
|
||||
@@ -295,34 +295,34 @@ packages:
|
||||
'@clack/prompts@1.1.0':
|
||||
resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==}
|
||||
|
||||
'@earendil-works/pi-agent-core@0.84.2':
|
||||
resolution: {integrity: sha512-8Pn3wSCxj0cfo5I6jxQYVB/3uuQRmHhAlEclyjqpOuMEdQMIODHizRogv56FLdbU+dTiGnybeHQ2N+sV1/L2YA==}
|
||||
'@earendil-works/pi-agent-core@0.84.4':
|
||||
resolution: {integrity: sha512-HyUnjaOXj6oN/6SNcr8A1J/ElRQA50FtIE0XUTSKAQVqmdlb9qdojOyUQwF/jULE5+yOEtGuVgi/N1RnBiNG+g==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@earendil-works/pi-ai@0.84.2':
|
||||
resolution: {integrity: sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==}
|
||||
'@earendil-works/pi-ai@0.84.4':
|
||||
resolution: {integrity: sha512-AClAZxf5+c4RRu44NJPS6wyQy+Nmq+Mzyyrdvm4ZVMNuixelO02RZX4G4Aq1F145Yzp43wnM5S+hLlSI7ypfVw==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
hasBin: true
|
||||
|
||||
'@earendil-works/pi-client@0.84.2':
|
||||
resolution: {integrity: sha512-/RFSPhD/bZbpOp1oJj+UneSUFSgZhWxzcSENUY+8+8xhoBrWXMYI2t77XNx4Yf+c8YK2qTHquForhNcelYpXvg==}
|
||||
'@earendil-works/pi-client@0.84.4':
|
||||
resolution: {integrity: sha512-q398WY/3ZQHTizk7IKxApzqFV0xt4yM9LkSkwyqeLK5Bj5RwRjOWxESt26z4LgNp4O+8hqhqFPf/8fj4H5rE4A==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@earendil-works/pi-coding-agent@0.84.2':
|
||||
resolution: {integrity: sha512-l4E+B7hgXKWddRo8bC/eSue2aWZjEgJ9xIpf5p0Og+lq8a2TArCwJ0HCoCPCgaBP/tN4zbYH/wOwvx9pJpeLCA==}
|
||||
'@earendil-works/pi-coding-agent@0.84.4':
|
||||
resolution: {integrity: sha512-jmOlrqUmvhh/siNWFRXjYLJzhKFIHNsAQaysRwzQPQFnPAaV/vhqHsLH/MBsIISA1Rjj7WTUFR3nJrpXoLx39w==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
hasBin: true
|
||||
|
||||
'@earendil-works/pi-protocol@0.84.2':
|
||||
resolution: {integrity: sha512-jbBh03fkeckWEroHpcZBr4w5/Ibat8WwdXFlXHivYQImrQNFtLpDeL0t1cku4hmK0q3pceIRQHkw4fwbM4YILQ==}
|
||||
'@earendil-works/pi-protocol@0.84.4':
|
||||
resolution: {integrity: sha512-acyE9ozxkMiWiz/xyWpU0O9vwnYv0hyG889Vniv6Sg9c9zfsX+8MePnDNphBacY2Fvm1rxdsGmiVDSZl9yuDFA==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@earendil-works/pi-telemetry@0.84.2':
|
||||
resolution: {integrity: sha512-wg5caea7uIv1BHRBm2Y116RvFG4oSAiP5qk9tA2463PDGIr4K8M1Ceyyg5DOpF/shUUl0gk826yQJAeAcHYB9g==}
|
||||
'@earendil-works/pi-telemetry@0.84.4':
|
||||
resolution: {integrity: sha512-8e2CuxM+ht+hedQXTZmi5JVl6/xDK9RpSDL2+MbITevKYQhMZ/z6lJOTFgox3HQyGxO8mOZEtYGVeQNaD4OzqA==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@earendil-works/pi-tui@0.84.2':
|
||||
resolution: {integrity: sha512-ds2TLihOnM5sLJB3VpXV6y0uR5efVuHf4MN7yDpsty6hA2DUO/EDVzjp/0od0G2JslzVLMjT8T8zavtxVb+qbg==}
|
||||
'@earendil-works/pi-tui@0.84.4':
|
||||
resolution: {integrity: sha512-nPUnwDkLtupPXnZQYrCwPFcuTydCDqTY6ZbFqhsL4S4kVq0AT418kPa/6uXwtaCD+MjBNBltb7ScTYX65yeE1w==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
'@emnapi/core@1.9.1':
|
||||
@@ -588,10 +588,6 @@ packages:
|
||||
'@nodable/entities@2.1.1':
|
||||
resolution: {integrity: sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==}
|
||||
|
||||
'@opentelemetry/api@1.9.0':
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@oxc-project/types@0.122.0':
|
||||
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
|
||||
|
||||
@@ -1360,10 +1356,6 @@ packages:
|
||||
glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
|
||||
glob@13.0.6:
|
||||
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
google-auth-library@10.7.0:
|
||||
resolution: {integrity: sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1575,10 +1567,6 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
minipass@7.1.3:
|
||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -1665,10 +1653,6 @@ packages:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-scurry@2.0.2:
|
||||
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
|
||||
path-to-regexp@8.4.2:
|
||||
resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
|
||||
|
||||
@@ -2456,10 +2440,10 @@ snapshots:
|
||||
'@clack/core': 1.1.0
|
||||
sisteransi: 1.0.5
|
||||
|
||||
'@earendil-works/pi-agent-core@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
'@earendil-works/pi-agent-core@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@earendil-works/pi-ai': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-telemetry': 0.84.2
|
||||
'@earendil-works/pi-ai': 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-telemetry': 0.84.4
|
||||
diff: 8.0.4
|
||||
ignore: 7.0.5
|
||||
typebox: 1.3.7
|
||||
@@ -2472,13 +2456,12 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-ai@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
'@earendil-works/pi-ai@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.91.1(zod@4.3.6)
|
||||
'@aws-sdk/client-bedrock-runtime': 3.1048.0
|
||||
'@earendil-works/pi-telemetry': 0.84.2
|
||||
'@earendil-works/pi-telemetry': 0.84.4
|
||||
'@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@smithy/node-http-handler': 4.7.3
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
@@ -2493,22 +2476,21 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-client@0.84.2':
|
||||
'@earendil-works/pi-client@0.84.4':
|
||||
dependencies:
|
||||
'@earendil-works/pi-protocol': 0.84.2
|
||||
'@earendil-works/pi-protocol': 0.84.4
|
||||
|
||||
'@earendil-works/pi-coding-agent@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
'@earendil-works/pi-coding-agent@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@earendil-works/pi-agent-core': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-ai': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-client': 0.84.2
|
||||
'@earendil-works/pi-protocol': 0.84.2
|
||||
'@earendil-works/pi-tui': 0.84.2
|
||||
'@earendil-works/pi-agent-core': 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-ai': 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-client': 0.84.4
|
||||
'@earendil-works/pi-protocol': 0.84.4
|
||||
'@earendil-works/pi-tui': 0.84.4
|
||||
'@silvia-odwyer/photon-node': 0.3.4
|
||||
chalk: 5.6.2
|
||||
cross-spawn: 7.0.6
|
||||
diff: 8.0.4
|
||||
glob: 13.0.6
|
||||
grok-mermaid: 0.2.2
|
||||
highlight.js: 10.7.3
|
||||
hosted-git-info: 9.0.3
|
||||
@@ -2530,13 +2512,13 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@earendil-works/pi-protocol@0.84.2':
|
||||
'@earendil-works/pi-protocol@0.84.4':
|
||||
dependencies:
|
||||
typebox: 1.3.7
|
||||
|
||||
'@earendil-works/pi-telemetry@0.84.2': {}
|
||||
'@earendil-works/pi-telemetry@0.84.4': {}
|
||||
|
||||
'@earendil-works/pi-tui@0.84.2':
|
||||
'@earendil-works/pi-tui@0.84.4':
|
||||
dependencies:
|
||||
get-east-asian-width: 1.6.0
|
||||
marked: 18.0.5
|
||||
@@ -2570,10 +2552,10 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@gotgenes/pi-permission-system@10.9.0(@earendil-works/pi-coding-agent@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.2)':
|
||||
'@gotgenes/pi-permission-system@10.9.0(@earendil-works/pi-coding-agent@0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6))(@earendil-works/pi-tui@0.84.4)':
|
||||
dependencies:
|
||||
'@earendil-works/pi-coding-agent': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-tui': 0.84.2
|
||||
'@earendil-works/pi-coding-agent': 0.84.4(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6)
|
||||
'@earendil-works/pi-tui': 0.84.4
|
||||
tree-sitter-bash: 0.25.1
|
||||
web-tree-sitter: 0.26.9
|
||||
transitivePeerDependencies:
|
||||
@@ -2820,8 +2802,6 @@ snapshots:
|
||||
|
||||
'@nodable/entities@2.1.1': {}
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@oxc-project/types@0.122.0': {}
|
||||
|
||||
'@protobufjs/aspromise@1.1.2': {}
|
||||
@@ -3599,12 +3579,6 @@ snapshots:
|
||||
|
||||
glob-to-regexp@0.4.1: {}
|
||||
|
||||
glob@13.0.6:
|
||||
dependencies:
|
||||
minimatch: 10.2.5
|
||||
minipass: 7.1.3
|
||||
path-scurry: 2.0.2
|
||||
|
||||
google-auth-library@10.7.0:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
@@ -3813,8 +3787,6 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@7.1.3: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
ms@3.0.0-canary.1: {}
|
||||
@@ -3877,11 +3849,6 @@ snapshots:
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-scurry@2.0.2:
|
||||
dependencies:
|
||||
lru-cache: 11.5.1
|
||||
minipass: 7.1.3
|
||||
|
||||
path-to-regexp@8.4.2:
|
||||
optional: true
|
||||
|
||||
|
||||
Reference in new issue
Block a user