mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-07-01 02:55:37 +02:00
Compare commits
250 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00c517c7b9 | |||
| 8b956c9972 | |||
| 3d1a3c75f8 | |||
| ac6db3b52e | |||
| 0a1a2eb1c1 | |||
| a6f004cd25 | |||
| 4a12918448 | |||
| 35f59f30f6 | |||
| 7813baf16a | |||
| 8f5d639f0d | |||
| 32c01a39b1 | |||
| 72c424f687 | |||
| 1af42339b9 | |||
| ca86c839cc | |||
| 0a57b062fd | |||
| 46be49c175 | |||
| 95998d1a44 | |||
| 6c8135d031 | |||
| 03a3d764af | |||
| 79caada539 | |||
| dcabe6e82e | |||
| ccb5303106 | |||
| 581c208b84 | |||
| 01644ff2ed | |||
| 0ce34c9c27 | |||
| 671d41699e | |||
| 8ca34dad69 | |||
| a111863778 | |||
| 3f83a51e22 | |||
| c78ae0b3b6 | |||
| c0794bccf6 | |||
| 1f6dfd7e17 | |||
| f6fd1edad6 | |||
| 77e300d52a | |||
| 99629c2b66 | |||
| 2a433f090f | |||
| 6a0c8ce710 | |||
| bc8fd203ed | |||
| 0d172f5e32 | |||
| 3324c01b83 | |||
| 601fbe7756 | |||
| ae4bd45a30 | |||
| 629c52ed3b | |||
| 3dd4056dc3 | |||
| 17df89a48f | |||
| 58afb767c6 | |||
| 023cc953db | |||
| 01165382ed | |||
| 4c6750541b | |||
| 2feff83b6e | |||
| 96b2728318 | |||
| 595b2ada78 | |||
| c68ee44103 | |||
| fdd7d0af64 | |||
| 03377de469 | |||
| 477ccd71aa | |||
| 43aa6386a2 | |||
| 6ad2c9d5c1 | |||
| 53bb10c450 | |||
| ce98c749f5 | |||
| ba8f737d02 | |||
| a01b130281 | |||
| ff7874815a | |||
| c5f13235da | |||
| 528dced335 | |||
| cdf0f13cc6 | |||
| e69ce6f51e | |||
| ab2c400daf | |||
| 9b0e64944b | |||
| f3f4e44ccd | |||
| 6b68bb40f8 | |||
| d3de8e13fb | |||
| 57d1141f4a | |||
| 1aafc0c3d0 | |||
| a8afe98518 | |||
| 395b2bd187 | |||
| e29d5b88a0 | |||
| 6a76df2f4c | |||
| 3ec491b30b | |||
| b62abfea4c | |||
| 98e3446448 | |||
| a03bc7506c | |||
| d67c07dc55 | |||
| 91f03242a5 | |||
| 17d12be2ab | |||
| 6b403d59a7 | |||
| 742b74c86f | |||
| eaa817ea64 | |||
| 839686c23c | |||
| e8e830c9f8 | |||
| 7ecf5abb35 | |||
| c0d46cb6b9 | |||
| afa0e9b701 | |||
| 7fb0c30769 | |||
| 1e3f709423 | |||
| a960ad1182 | |||
| d696a7584b | |||
| 413c47af5c | |||
| 16de74e0be | |||
| b208949345 | |||
| 9074149778 | |||
| bb89d6f458 | |||
| d3816a29fa | |||
| ae69478541 | |||
| 8e4fafba99 | |||
| 13731f5ebf | |||
| 3a07f8a81f | |||
| 45e9f305ea | |||
| 539bd873cc | |||
| c8bc29c011 | |||
| 759c8d8093 | |||
| e85f6e0c73 | |||
| 2cf237d638 | |||
| 1b696cac1b | |||
| 7f9c5cc496 | |||
| dbcb4587ee | |||
| f017a41436 | |||
| ee5d7b80a0 | |||
| f932fad2ed | |||
| ce2628f6f0 | |||
| c169b0d0a6 | |||
| 80bc8e3a44 | |||
| 30b5522647 | |||
| 2f4fa89e7b | |||
| 2e1fe3454a | |||
| a5daa07178 | |||
| efb5368b3c | |||
| 3c13a9a7e6 | |||
| 24bcd29d97 | |||
| 77c5b26a94 | |||
| 9809c769e3 | |||
| 2e9ee2a11e | |||
| 4aee8db3d0 | |||
| 9ed5327561 | |||
| 3a63624ff7 | |||
| 7cb0a0ae5e | |||
| 1c5a61e05f | |||
| 8f42eb64fa | |||
| d05eaf2ff7 | |||
| a15408e23f | |||
| 534b24901e | |||
| cdb7d165ca | |||
| 65aa5625f6 | |||
| 25fde5240a | |||
| f85c1bd193 | |||
| 63741d780e | |||
| 9606ffcf70 | |||
| cd04c7a6d2 | |||
| d01980ce4b | |||
| d925c4942b | |||
| 914860a6bd | |||
| 20b5939e35 | |||
| 51e621d0d5 | |||
| 45acb16711 | |||
| 8381198c41 | |||
| 3ac07a4718 | |||
| 7d91373fdb | |||
| 82fbf55843 | |||
| 8e9f6c3a0f | |||
| 11fdb69826 | |||
| 37157244ee | |||
| 0068b34859 | |||
| 98974d48cc | |||
| 10e602ec87 | |||
| dce9578a8e | |||
| b0cd70b67c | |||
| c9ee50123a | |||
| 39766d0afc | |||
| 515ade8302 | |||
| 26b42ecd67 | |||
| 37409a24fb | |||
| 42687d30fb | |||
| ad0d1a04e9 | |||
| 0d3812cdd2 | |||
| cecb64729f | |||
| c7de6636d9 | |||
| 7c2edeb4c0 | |||
| 9d20d94dda | |||
| a804c94834 | |||
| 20cdf0b026 | |||
| 7e0b2b28fe | |||
| a52c1ab7c3 | |||
| 719bf03293 | |||
| 23618f1fd1 | |||
| 68ec5ccc5a | |||
| f4f320dcb5 | |||
| 614caa1787 | |||
| acc4a1b032 | |||
| 323720f3b0 | |||
| 98e79d0125 | |||
| e4eb59870a | |||
| 6e7a7ec1cd | |||
| b5c286fc80 | |||
| fe351604f9 | |||
| bfaffe89e6 | |||
| 5f24311a4e | |||
| 236c4d2a2f | |||
| ce0d7b96c2 | |||
| b45e3e2844 | |||
| a909572596 | |||
| bb4aa03dd1 | |||
| abfc4eba82 | |||
| d5b064e0c0 | |||
| e1f369b233 | |||
| ca5515c23c | |||
| 92db01bd2d | |||
| 34850477a2 | |||
| d82d1fa753 | |||
| 0b9580a99a | |||
| cc36fe933d | |||
| 5b92ff52c4 | |||
| d8efd78ac0 | |||
| a099500d9b | |||
| f0b8c3aa6e | |||
| 258830b030 | |||
| d85b6af5f5 | |||
| f40f52f118 | |||
| f2870e3340 | |||
| f13c7421f4 | |||
| 9be2e71ff2 | |||
| 2966157596 | |||
| d649fccfdb | |||
| ef3ae0aead | |||
| eae0b8d654 | |||
| cfe8dc8bc8 | |||
| 255956d113 | |||
| 95a4639d90 | |||
| a8b4e6899a | |||
| 27334a4dd6 | |||
| a9e00ca19f | |||
| e1237416f5 | |||
| ac682b0172 | |||
| 66c549f3b7 | |||
| 3a8b7ae496 | |||
| e0ff1453a5 | |||
| 46a30fd8c9 | |||
| 80747a0204 | |||
| bbd9db2a61 | |||
| 770dae387a | |||
| 0c446382e6 | |||
| d72222dcb9 | |||
| 851752bcc1 | |||
| d30553d7dd | |||
| 8490196c78 | |||
| 7e0ca8c49d | |||
| 1fe4c1f828 | |||
| 59e7e3c586 | |||
| 7c4559d4aa | |||
| 96eee1c3b6 | |||
| 8f52722d56 |
+28
-21
@@ -8,41 +8,44 @@ You are debugging an issue. Follow this structured approach to avoid spinning in
|
||||
- Read the full error message and stack trace
|
||||
- Identify the layer where the error originated:
|
||||
- **CLI/Args** - Input validation, path resolution
|
||||
- **Config Parsing** - YAML parsing, JSON Schema validation
|
||||
- **Session Management** - Mutex, session.json, lock files
|
||||
- **Audit System** - Logging, metrics tracking, atomic writes
|
||||
- **Claude SDK** - Agent execution, MCP servers, turn handling
|
||||
- **Git Operations** - Checkpoints, rollback, commit
|
||||
- **Tool Execution** - nmap, subfinder, whatweb
|
||||
- **Validation** - Deliverable checks, queue validation
|
||||
- **Config Parsing** - YAML parsing, JSON Schema validation (`src/config-parser.ts`)
|
||||
- **Session Management** - Agent definitions (`src/session-manager.ts`), mutex (`src/utils/concurrency.ts`)
|
||||
- **DI Container** - Container initialization/lookup (`src/services/container.ts`)
|
||||
- **Services** - AgentExecutionService, ConfigLoaderService, ExploitationCheckerService, error-handling (`src/services/`)
|
||||
- **Audit System** - Logging, metrics tracking, atomic writes (`src/audit/`)
|
||||
- **Claude SDK** - Agent execution, MCP servers, turn handling (`src/ai/claude-executor.ts`)
|
||||
- **Git Operations** - Checkpoints, rollback, commit (`src/services/git-manager.ts`)
|
||||
- **Validation** - Deliverable checks, queue validation (`src/services/queue-validation.ts`)
|
||||
|
||||
## Step 2: Check Relevant Logs
|
||||
|
||||
**Session audit logs:**
|
||||
```bash
|
||||
# Find most recent session
|
||||
ls -lt audit-logs/ | head -5
|
||||
ls -lt workspaces/ | head -5
|
||||
|
||||
# Check session metrics and errors
|
||||
cat audit-logs/<session>/session.json | jq '.errors, .agentMetrics'
|
||||
cat workspaces/<session>/session.json | jq '.errors, .agentMetrics'
|
||||
|
||||
# Check agent execution logs
|
||||
ls -lt audit-logs/<session>/agents/
|
||||
cat audit-logs/<session>/agents/<latest>.log
|
||||
ls -lt workspaces/<session>/agents/
|
||||
cat workspaces/<session>/agents/<latest>.log
|
||||
```
|
||||
|
||||
## Step 3: Trace the Call Path
|
||||
|
||||
For Shannon, trace through these layers:
|
||||
|
||||
1. **Temporal Client** → `src/temporal/client.ts` - Workflow initiation
|
||||
1. **Worker + Client** → `src/temporal/worker.ts` - Combined worker + workflow submission
|
||||
2. **Workflow** → `src/temporal/workflows.ts` - Pipeline orchestration
|
||||
3. **Activities** → `src/temporal/activities.ts` - Agent execution with heartbeats
|
||||
4. **Config** → `src/config-parser.ts` - YAML loading, schema validation
|
||||
5. **Session** → `src/session-manager.ts` - Agent definitions, execution order
|
||||
6. **Audit** → `src/audit/audit-session.ts` - Logging facade, metrics tracking
|
||||
7. **Executor** → `src/ai/claude-executor.ts` - SDK calls, MCP setup, retry logic
|
||||
8. **Validation** → `src/queue-validation.ts` - Deliverable checks
|
||||
3. **Activities** → `src/temporal/activities.ts` - Thin wrappers: heartbeat, error classification
|
||||
4. **Container** → `src/services/container.ts` - Per-workflow DI
|
||||
5. **Services** → `src/services/agent-execution.ts` - Agent lifecycle
|
||||
6. **Config** → `src/config-parser.ts` via `src/services/config-loader.ts`
|
||||
7. **Prompts** → `src/services/prompt-manager.ts`
|
||||
8. **Audit** → `src/audit/audit-session.ts` - Logging facade, metrics tracking
|
||||
9. **Executor** → `src/ai/claude-executor.ts` - SDK calls, MCP setup, retry logic
|
||||
10. **Validation** → `src/services/queue-validation.ts` - Deliverable checks
|
||||
|
||||
## Step 4: Identify Root Cause
|
||||
|
||||
@@ -58,7 +61,10 @@ For Shannon, trace through these layers:
|
||||
| Cost/timing not tracked | Metrics not reloaded before update | Add `metricsTracker.reload()` before updates |
|
||||
| session.json corrupted | Partial write during crash | Delete and restart, or restore from backup |
|
||||
| YAML config rejected | Invalid schema or unsafe content | Run through AJV validator manually |
|
||||
| Prompt variable not replaced | Missing `{{VARIABLE}}` in context | Check `prompt-manager.ts` interpolation |
|
||||
| Prompt variable not replaced | Missing `{{VARIABLE}}` in context | Check `src/services/prompt-manager.ts` interpolation |
|
||||
| Service returns Err result | Check `ErrorCode` in Result | Trace through `classifyErrorForTemporal()` in `src/services/error-handling.ts` |
|
||||
| Container not found | `getOrCreateContainer()` not called | Check activity setup code in `src/temporal/activities.ts` |
|
||||
| ActivityLogger undefined | `createActivityLogger()` not called | Must be called at top of each activity function |
|
||||
|
||||
**MCP Server Issues:**
|
||||
```bash
|
||||
@@ -66,7 +72,7 @@ For Shannon, trace through these layers:
|
||||
npx playwright install chromium
|
||||
|
||||
# Check MCP server startup (look for connection errors)
|
||||
grep -i "mcp\|playwright" audit-logs/<session>/agents/*.log
|
||||
grep -i "mcp\|playwright" workspaces/<session>/agents/*.log
|
||||
```
|
||||
|
||||
**Git State Issues:**
|
||||
@@ -123,11 +129,12 @@ shannon <URL> <REPO> --pipeline-testing
|
||||
|
||||
## Quick Reference: Error Types
|
||||
|
||||
`ErrorCode` enum in `src/types/errors.ts` provides finer-grained classification used by `classifyErrorForTemporal()` in `src/services/error-handling.ts`.
|
||||
|
||||
| PentestError Type | Meaning | Retryable? |
|
||||
|-------------------|---------|------------|
|
||||
| `config` | Configuration file issues | No |
|
||||
| `network` | Connection/timeout issues | Yes |
|
||||
| `tool` | External tool (nmap, etc.) failed | Yes |
|
||||
| `prompt` | Claude SDK/API issues | Sometimes |
|
||||
| `filesystem` | File read/write errors | Sometimes |
|
||||
| `validation` | Deliverable validation failed | Yes (via retry) |
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
description: Create a PR to main branch using conventional commit style for the title
|
||||
---
|
||||
|
||||
Create a pull request from the current branch to the `main` branch.
|
||||
|
||||
## Arguments
|
||||
|
||||
The user may provide issue numbers that this PR fixes: `$ARGUMENTS`
|
||||
|
||||
- If provided (e.g., `123` or `123,456`), use these issue numbers
|
||||
- If not provided, check the branch name for issue numbers (e.g., `fix/123-bug` or `issue-456-feature` → extract `123` or `456`)
|
||||
- If no issues are found, omit the "Closes" section
|
||||
|
||||
## Steps
|
||||
|
||||
First, analyze the current branch to understand what changes have been made:
|
||||
1. Run `git log --oneline -10` to see recent commit history and understand commit style
|
||||
2. Run `git log main..HEAD --oneline` to see all commits on this branch that will be included in the PR
|
||||
3. Run `git diff main...HEAD --stat` to see a summary of file changes
|
||||
4. Run `git branch --show-current` to get the branch name for issue detection (if no explicit issues provided)
|
||||
|
||||
Then generate a PR title that:
|
||||
- Follows conventional commit format (e.g., `fix:`, `feat:`, `chore:`, `refactor:`)
|
||||
- Is concise and accurately describes the changes
|
||||
- Matches the style of recent commits in the repository
|
||||
|
||||
Generate a PR body with:
|
||||
- A `## Summary` section using rich bullets with bold action leads
|
||||
- A `Closes #X` line for each issue number (if any were provided or detected from branch name)
|
||||
|
||||
Each Summary bullet must follow this format:
|
||||
- **Bold action phrase** (imperative verb: "Add X", "Replace Y", "Fix Z") — followed by em dash and a 1-2 sentence conceptual description of what changed and why
|
||||
- Keep descriptions conceptual — no inline code references (no backticks for function/file names). The diff shows the code
|
||||
- Use 2-5 bullets, scaling with PR size. Group related changes into single bullets rather than listing every file touched
|
||||
|
||||
Example:
|
||||
```
|
||||
## Summary
|
||||
|
||||
- **Add preflight validation** — validates repo path, config, and credentials before agent execution. Fails fast with actionable errors
|
||||
- **Replace error strings** — pipe-delimited segments rendered as multi-line blocks with phase context, type, message, and remediation hint
|
||||
- **Add error classification** — new error codes for repo, auth, and billing failures with proper retry classification
|
||||
```
|
||||
|
||||
Finally, create the PR using the gh CLI:
|
||||
```
|
||||
gh pr create --base main --title "<generated title>" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
<rich bullets>
|
||||
|
||||
Closes #<issue1>
|
||||
Closes #<issue2>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
Note: Omit the "Closes" lines entirely if no issues are associated with this PR.
|
||||
|
||||
IMPORTANT:
|
||||
- Do NOT include any Claude Code attribution in the PR
|
||||
- Use the conventional commit prefix that best matches the changes (fix, feat, chore, refactor, docs, etc.)
|
||||
- The `Closes #X` syntax will automatically close the referenced issues when the PR is merged
|
||||
@@ -19,6 +19,8 @@ git diff HEAD
|
||||
- [ ] **Retryable flag matches behavior** - If error will be retried, set `retryable: true`
|
||||
- [ ] **Context includes debugging info** - Add relevant paths, tool names, error codes to context object
|
||||
- [ ] **Never swallow errors silently** - Always log or propagate errors
|
||||
- [ ] **Use ErrorCode enum** - Prefer `ErrorCode.CONFIG_INVALID` over string matching for classification
|
||||
- [ ] **Result<T,E> for service returns** - Services return `Result`, not throw
|
||||
|
||||
### Audit System & Concurrency (CRITICAL)
|
||||
- [ ] **Mutex protection for parallel operations** - Use `sessionMutex.lock()` when updating `session.json` during parallel agent execution
|
||||
@@ -41,6 +43,13 @@ git diff HEAD
|
||||
- [ ] **Duplicate rule detection** - Same `type:url_path` cannot appear twice
|
||||
- [ ] **JSON Schema validation before use** - Config must pass AJV validation
|
||||
|
||||
### Services Layer & DI Container (CRITICAL)
|
||||
- [ ] **Business logic in services, not activities** — Activities: heartbeat loop, error classification, container calls only. Domain logic → `src/services/`
|
||||
- [ ] **Services accept ActivityLogger** — Never import `@temporalio/*` in services. Use `ActivityLogger` interface from `src/types/`
|
||||
- [ ] **Result type for fallible operations** — Service methods return `Result<T, PentestError>`, unwrap with `isOk()`/`isErr()`. Activities call `executeOrThrow()` at the boundary
|
||||
- [ ] **Container lifecycle** — `getOrCreateContainer()` at activity start, `removeContainer()` only in workflow cleanup
|
||||
- [ ] **AuditSession not in container** — Must be passed per-agent call (parallel safety)
|
||||
|
||||
### Session & Agent Management (CRITICAL)
|
||||
- [ ] **Deliverable dependencies respected** - Exploitation agents only run if vulnerability queue exists AND has items
|
||||
- [ ] **Queue validation before exploitation** - Use `safeValidateQueueAndDeliverable()` to check eligibility
|
||||
@@ -91,6 +100,8 @@ git diff HEAD
|
||||
- [ ] **Duplicate retry logic** - Don't implement retry at both caller and callee level
|
||||
- [ ] **Hardcoded error message matching** - Prefer error codes over regex on error.message
|
||||
- [ ] **Missing timeout on long operations** - Git operations and API calls should have timeouts
|
||||
- [ ] **Console.log in services** — Use `ActivityLogger`. Only CLI display code (`client.ts`, `worker.ts`, `output-formatters.ts`) uses console.log
|
||||
- [ ] **Temporal imports in services** — Services must stay Temporal-agnostic. If you need Temporal APIs, it belongs in activities
|
||||
|
||||
### Code Quality
|
||||
- [ ] **No dead code added** - Remove unused imports, functions, variables
|
||||
|
||||
+9
-1
@@ -1,5 +1,5 @@
|
||||
# Node.js
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -46,6 +46,14 @@ temp/
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# CLI package (runs on host, not in container)
|
||||
# Keep apps/cli/package.json so pnpm workspaces resolve
|
||||
apps/cli/src/
|
||||
**/dist/
|
||||
apps/cli/infra/
|
||||
apps/cli/tsconfig.json
|
||||
apps/cli/tsdown.config.ts
|
||||
|
||||
# Docker files (avoid recursive copying)
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
|
||||
+59
-1
@@ -1,8 +1,66 @@
|
||||
# Shannon Environment Configuration
|
||||
# Copy this file to .env and fill in your credentials
|
||||
|
||||
# Anthropic API Key (required - choose one)
|
||||
# Recommended output token configuration for larger tool outputs
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000
|
||||
|
||||
# Adaptive thinking is enabled automatically on Opus 4.6/4.7/4.8. Set to false to disable.
|
||||
# CLAUDE_ADAPTIVE_THINKING=false
|
||||
|
||||
# Shannon forwards your machine's /etc/hosts entries into the worker container. Set to false to disable.
|
||||
# SHANNON_FORWARD_HOSTS=false
|
||||
|
||||
# =============================================================================
|
||||
# OPTION 1: Direct Anthropic
|
||||
# =============================================================================
|
||||
ANTHROPIC_API_KEY=your-api-key-here
|
||||
|
||||
# OR use OAuth token instead
|
||||
# CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token-here
|
||||
|
||||
# =============================================================================
|
||||
# OPTION 2: Custom Base URL (compatible proxies, gateways, etc.)
|
||||
# =============================================================================
|
||||
# Point the SDK at an alternative Anthropic-compatible endpoint.
|
||||
# ANTHROPIC_BASE_URL=https://your-proxy.example.com
|
||||
# ANTHROPIC_AUTH_TOKEN=your-auth-token # Auth token for the custom endpoint
|
||||
|
||||
# =============================================================================
|
||||
# Model Tier Overrides (Anthropic API / OAuth / Custom Base URL / Bedrock)
|
||||
# =============================================================================
|
||||
# Override which model is used for each tier. Defaults are used if not set.
|
||||
# Optional for direct Anthropic and custom base URL modes. Required for Bedrock/Vertex.
|
||||
# ANTHROPIC_SMALL_MODEL=... # Small tier (default: claude-haiku-4-5-20251001)
|
||||
# ANTHROPIC_MEDIUM_MODEL=... # Medium tier (default: claude-sonnet-4-6)
|
||||
# ANTHROPIC_LARGE_MODEL=... # Large tier (default: claude-opus-4-8)
|
||||
|
||||
# =============================================================================
|
||||
# OPTION 3: AWS Bedrock
|
||||
# =============================================================================
|
||||
# https://aws.amazon.com/blogs/machine-learning/accelerate-ai-development-with-amazon-bedrock-api-keys/
|
||||
# Requires the model tier overrides above to be set with Bedrock-specific model IDs.
|
||||
# Example Bedrock model IDs for us-east-1:
|
||||
# ANTHROPIC_SMALL_MODEL=us.anthropic.claude-haiku-4-5-20251001-v1:0
|
||||
# ANTHROPIC_MEDIUM_MODEL=us.anthropic.claude-sonnet-4-6
|
||||
# ANTHROPIC_LARGE_MODEL=us.anthropic.claude-opus-4-8
|
||||
|
||||
# CLAUDE_CODE_USE_BEDROCK=1
|
||||
# AWS_REGION=us-east-1
|
||||
# AWS_BEARER_TOKEN_BEDROCK=your-bearer-token
|
||||
|
||||
# =============================================================================
|
||||
# OPTION 4: Google Vertex AI
|
||||
# =============================================================================
|
||||
# https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-partner-models
|
||||
# Requires a GCP service account with roles/aiplatform.user.
|
||||
# Download the SA key JSON from GCP Console (IAM > Service Accounts > Keys).
|
||||
# Requires the model tier overrides above to be set with Vertex AI model IDs.
|
||||
# Example Vertex AI model IDs:
|
||||
# ANTHROPIC_SMALL_MODEL=claude-haiku-4-5@20251001
|
||||
# ANTHROPIC_MEDIUM_MODEL=claude-sonnet-4-6
|
||||
# ANTHROPIC_LARGE_MODEL=claude-opus-4-8
|
||||
|
||||
# CLAUDE_CODE_USE_VERTEX=1
|
||||
# CLOUD_ML_REGION=us-east5
|
||||
# ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
# GOOGLE_APPLICATION_CREDENTIALS=./credentials/google-sa-key.json
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
@@ -0,0 +1,162 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
title: "[BUG]: "
|
||||
labels: []
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: describe-the-bug
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: Provide a clear and concise description of the issue.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behaviour
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
description: Describe what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behaviour
|
||||
attributes:
|
||||
label: Actual behaviour
|
||||
description: Describe what actually happened.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: pre-submission-checklist
|
||||
attributes:
|
||||
label: Pre-submission checklist (required)
|
||||
options:
|
||||
- label: I have searched the existing open issues and confirmed this bug has not already been reported.
|
||||
required: true
|
||||
- label: I am running the latest released version of `shannon`.
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: applicable-checklist
|
||||
attributes:
|
||||
label: If applicable
|
||||
options:
|
||||
- label: I have included relevant error messages, stack traces, or failure details.
|
||||
- label: I have checked the workspaces folder for logs and pasted the relevant errors.
|
||||
- label: I have inspected the failed Temporal workflow run and included the failure reason.
|
||||
- label: I have included clear steps to reproduce the issue.
|
||||
- label: I have redacted any sensitive information (tokens, URLs, repo names).
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### Debugging checklist (required)
|
||||
|
||||
Please include any **error messages, stack traces, or failure details** you find from the steps below.
|
||||
|
||||
Issues without this information may be difficult to triage.
|
||||
|
||||
- Check the workflow log:
|
||||
- **npx mode:** `~/.shannon/workspaces/<workspace>/workflow.log`
|
||||
- **Local mode:** `./workspaces/<workspace>/workflow.log`
|
||||
Use `grep` or search to identify errors.
|
||||
Paste the relevant error output below.
|
||||
- Temporal:
|
||||
- Open the Temporal UI: http://localhost:8233/namespaces/default/workflows
|
||||
- Navigate to failed workflow runs
|
||||
- Open the failed workflow run
|
||||
- In Event History, click on the failed event
|
||||
Copy the error message or failure reason here.
|
||||
|
||||
- type: textarea
|
||||
id: debugging-details
|
||||
attributes:
|
||||
label: Debugging details
|
||||
description: Paste any error messages, stack traces, or failure details from the workspace logs or Temporal UI.
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots of the workspace logs or Temporal failure details.
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### CLI details
|
||||
|
||||
Provide the following information (redact sensitive data such as repository names, URLs, and tokens):
|
||||
|
||||
- type: dropdown
|
||||
id: cli-mode
|
||||
attributes:
|
||||
label: CLI mode
|
||||
options:
|
||||
- "npx (@keygraph/shannon)"
|
||||
- "Local (./shannon)"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: provider
|
||||
attributes:
|
||||
label: Provider
|
||||
options:
|
||||
- "Anthropic (API key)"
|
||||
- "Anthropic (OAuth token)"
|
||||
- "Custom base URL (proxy/gateway)"
|
||||
- "AWS Bedrock"
|
||||
- "Google Vertex AI"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: shannon-command
|
||||
attributes:
|
||||
label: Full command with all flags used (with redactions)
|
||||
placeholder: "e.g. npx @keygraph/shannon start -u <url> -r my-repo OR ./shannon start -u <url> -r my-repo"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os-version
|
||||
attributes:
|
||||
label: "OS (with version)"
|
||||
placeholder: "e.g. macOS 26.2"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: node-version
|
||||
attributes:
|
||||
label: "Node.js version ('node -v')"
|
||||
placeholder: "e.g. 22.12.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: docker-version
|
||||
attributes:
|
||||
label: "Docker version ('docker -v')"
|
||||
placeholder: "e.g. 25.0.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context that may help us analyze the root cause.
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "[FEATURE]: "
|
||||
labels: []
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: desired-solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: cli-mode
|
||||
attributes:
|
||||
label: Which CLI mode does this apply to?
|
||||
options:
|
||||
- Both
|
||||
- "npx (@keygraph/shannon)"
|
||||
- "Local (./shannon)"
|
||||
|
||||
- type: textarea
|
||||
id: alternatives-considered
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,199 @@
|
||||
name: Release (Beta)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-beta
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Compute next beta version
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BASE="2.0.0"
|
||||
LATEST=$(npm view "@keygraph/shannon" dist-tags.beta 2>/dev/null || echo "")
|
||||
|
||||
if [[ "$LATEST" == "$BASE-beta."* ]]; then
|
||||
# Same base version — increment the beta counter (e.g. 2.0.0-beta.2 -> 2.0.0-beta.3)
|
||||
N=$(echo "$LATEST" | grep -oE 'beta\.([0-9]+)' | grep -oE '[0-9]+')
|
||||
NEXT=$((N + 1))
|
||||
echo "version=$BASE-beta.$NEXT" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# No prior beta, or a different base (e.g. last beta was 1.0.0-beta.N) — start over.
|
||||
echo "version=$BASE-beta.1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Print version
|
||||
run: 'echo "Next beta version: ${{ steps.version.outputs.version }}"'
|
||||
|
||||
build-docker:
|
||||
name: Build Docker (${{ matrix.platform }})
|
||||
needs: preflight
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
outputs: type=image,name=keygraph/shannon,push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge-docker:
|
||||
name: Push Docker manifests
|
||||
needs: [preflight, build-docker]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
outputs:
|
||||
digest: ${{ steps.inspect.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag "keygraph/shannon:${{ needs.preflight.outputs.version }}" \
|
||||
$(printf 'keygraph/shannon@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
id: inspect
|
||||
run: |
|
||||
docker buildx imagetools inspect "keygraph/shannon:${{ needs.preflight.outputs.version }}"
|
||||
DIGEST="sha256:$(docker buildx imagetools inspect --raw "keygraph/shannon:${{ needs.preflight.outputs.version }}" | sha256sum | cut -d' ' -f1)"
|
||||
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||
|
||||
- name: Sign Docker image
|
||||
run: cosign sign --yes "keygraph/shannon@${{ steps.inspect.outputs.digest }}"
|
||||
|
||||
- name: Verify Docker image signature
|
||||
run: |
|
||||
sleep 10
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity https://github.com/${{ github.repository }}/.github/workflows/release-beta.yml@${{ github.ref }} \
|
||||
"keygraph/shannon@${{ steps.inspect.outputs.digest }}"
|
||||
|
||||
publish-npm:
|
||||
name: Publish npm (beta)
|
||||
needs: [preflight, merge-docker]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
|
||||
- name: Configure npm registry
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set CLI package version
|
||||
run: cd apps/cli && npm version "${{ needs.preflight.outputs.version }}" --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Sync lockfile with bumped version
|
||||
run: pnpm install --lockfile-only
|
||||
|
||||
- name: Build CLI
|
||||
run: pnpm --filter @keygraph/shannon run build
|
||||
|
||||
- name: Publish npm package
|
||||
working-directory: apps/cli
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
if npm view "@keygraph/shannon@${{ needs.preflight.outputs.version }}" version 2>/dev/null; then
|
||||
echo "Version already published, skipping"
|
||||
else
|
||||
pnpm publish --access public --no-git-checks --tag beta
|
||||
fi
|
||||
@@ -0,0 +1,241 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-main
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
should_release: ${{ steps.probe.outputs.should_release }}
|
||||
version: ${{ steps.probe.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Probe semantic-release
|
||||
id: probe
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
npx semantic-release@25 --dry-run --no-ci 2>&1 | tee semantic-release.log
|
||||
|
||||
if grep -qi "the next release version is" semantic-release.log; then
|
||||
echo "should_release=true" >> "$GITHUB_OUTPUT"
|
||||
VERSION=$(grep -oiE "the next release version is [0-9]+\.[0-9]+\.[0-9]+" semantic-release.log | grep -oE "[0-9]+\.[0-9]+\.[0-9]+")
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build-docker:
|
||||
name: Build Docker (${{ matrix.platform }})
|
||||
needs: preflight
|
||||
if: needs.preflight.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.platform }}
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
outputs: type=image,name=keygraph/shannon,push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge-docker:
|
||||
name: Push Docker manifests
|
||||
needs: [preflight, build-docker]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
outputs:
|
||||
digest: ${{ steps.inspect.outputs.digest }}
|
||||
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag "keygraph/shannon:${{ needs.preflight.outputs.version }}" \
|
||||
--tag "keygraph/shannon:latest" \
|
||||
$(printf 'keygraph/shannon@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
id: inspect
|
||||
run: |
|
||||
docker buildx imagetools inspect "keygraph/shannon:${{ needs.preflight.outputs.version }}"
|
||||
DIGEST="sha256:$(docker buildx imagetools inspect --raw "keygraph/shannon:${{ needs.preflight.outputs.version }}" | sha256sum | cut -d' ' -f1)"
|
||||
echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||
|
||||
- name: Sign Docker image
|
||||
run: cosign sign --yes "keygraph/shannon@${{ steps.inspect.outputs.digest }}"
|
||||
|
||||
- name: Verify Docker image signature
|
||||
run: |
|
||||
sleep 10
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity https://github.com/${{ github.repository }}/.github/workflows/release.yml@${{ github.ref }} \
|
||||
"keygraph/shannon@${{ steps.inspect.outputs.digest }}"
|
||||
|
||||
publish-npm:
|
||||
name: Publish npm
|
||||
needs: [preflight, merge-docker]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
|
||||
- name: Configure npm registry
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set CLI package version
|
||||
run: cd apps/cli && npm version "${{ needs.preflight.outputs.version }}" --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Sync lockfile with bumped version
|
||||
run: pnpm install --lockfile-only
|
||||
|
||||
- name: Build CLI
|
||||
run: pnpm --filter @keygraph/shannon run build
|
||||
|
||||
- name: Publish npm package
|
||||
working-directory: apps/cli
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
if npm view "@keygraph/shannon@${{ needs.preflight.outputs.version }}" version 2>/dev/null; then
|
||||
echo "Version already published, skipping"
|
||||
else
|
||||
pnpm publish --access public --no-git-checks
|
||||
fi
|
||||
|
||||
release:
|
||||
name: Create GitHub release
|
||||
needs: [preflight, publish-npm]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Create GitHub release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npx semantic-release@25
|
||||
@@ -0,0 +1,71 @@
|
||||
name: Rollback (Beta)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Beta version to roll back to (example: 2.0.0-beta.2)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: rollback-beta-${{ github.event.inputs.version }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
rollback:
|
||||
name: Roll back npm beta dist-tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate target version
|
||||
id: target
|
||||
shell: bash
|
||||
env:
|
||||
RAW_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${RAW_VERSION#v}"
|
||||
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]]; then
|
||||
echo "Version must be in format X.Y.Z-beta.N (e.g. 2.0.0-beta.2)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Verify npm package version exists
|
||||
run: npm view "@keygraph/shannon@${{ steps.target.outputs.version }}" version
|
||||
|
||||
- name: Show current npm dist-tags
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm dist-tag ls @keygraph/shannon
|
||||
|
||||
- name: Move npm beta tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm dist-tag add "@keygraph/shannon@${{ steps.target.outputs.version }}" beta
|
||||
|
||||
- name: Show final npm dist-tags
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm dist-tag ls @keygraph/shannon
|
||||
|
||||
- name: Write summary
|
||||
run: |
|
||||
{
|
||||
echo "## Rollback beta"
|
||||
echo ""
|
||||
echo "- Target version: \`${{ steps.target.outputs.version }}\`"
|
||||
echo "- npm package: \`@keygraph/shannon\` (beta tag moved)"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,129 @@
|
||||
name: Rollback
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to move npm latest and Docker latest to (example: 1.4.2)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: rollback-latest-${{ github.event.inputs.version }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
rollback:
|
||||
name: Roll back npm, Docker, and GitHub release latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout tags
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch all tags
|
||||
run: git fetch --force --tags
|
||||
|
||||
- name: Validate target version
|
||||
id: target
|
||||
shell: bash
|
||||
env:
|
||||
RAW_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${RAW_VERSION#v}"
|
||||
|
||||
case "$VERSION" in
|
||||
''|*[!0-9.]*)
|
||||
echo "Invalid version: $VERSION"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Version must be in semver format X.Y.Z"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git rev-parse "refs/tags/v$VERSION" >/dev/null 2>&1; then
|
||||
echo "Git tag v$VERSION does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Verify npm package version exists
|
||||
run: npm view "@keygraph/shannon@${{ steps.target.outputs.version }}" version
|
||||
|
||||
- name: Show current npm dist-tags
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm dist-tag ls @keygraph/shannon
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Verify Docker image tag exists
|
||||
run: docker buildx imagetools inspect "keygraph/shannon:${{ steps.target.outputs.version }}"
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||
|
||||
- name: Verify Docker image signature before rollback
|
||||
run: |
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity "https://github.com/${{ github.repository }}/.github/workflows/release.yml@refs/heads/main" \
|
||||
"keygraph/shannon:${{ steps.target.outputs.version }}"
|
||||
|
||||
- name: Move Docker latest
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag "keygraph/shannon:latest" \
|
||||
"keygraph/shannon:${{ steps.target.outputs.version }}"
|
||||
|
||||
- name: Move npm latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm dist-tag add "@keygraph/shannon@${{ steps.target.outputs.version }}" latest
|
||||
|
||||
- name: Mark GitHub release as latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release edit "v${{ steps.target.outputs.version }}" --latest
|
||||
|
||||
- name: Show final npm dist-tags
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm dist-tag ls @keygraph/shannon
|
||||
|
||||
- name: Verify Docker latest now points to target
|
||||
run: docker buildx imagetools inspect "keygraph/shannon:latest"
|
||||
|
||||
- name: Write summary
|
||||
run: |
|
||||
{
|
||||
echo "## Rollback latest"
|
||||
echo ""
|
||||
echo "- Target version: \`${{ steps.target.outputs.version }}\`"
|
||||
echo "- npm package: \`@keygraph/shannon\`"
|
||||
echo "- Docker image: \`keygraph/shannon\`"
|
||||
echo "- GitHub release: \`v${{ steps.target.outputs.version }}\` marked as latest"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
+4
-1
@@ -1,4 +1,7 @@
|
||||
node_modules/
|
||||
.env
|
||||
audit-logs/
|
||||
workspaces/
|
||||
credentials/
|
||||
dist/
|
||||
repos/
|
||||
.turbo/
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
minimum-release-age=10080
|
||||
ignore-scripts=true
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"branches": ["main"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"npmPublish": false
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"successCommentCondition": false,
|
||||
"failCommentCondition": false,
|
||||
"releasedLabels": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,291 +1,248 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
This is an AI-powered penetration testing agent designed for defensive security analysis. The tool automates vulnerability assessment by combining external reconnaissance tools with AI-powered code analysis to identify security weaknesses in web applications and their source code.
|
||||
AI-powered penetration testing agent for defensive security analysis. Automates vulnerability assessment by combining reconnaissance tools with AI-powered code analysis.
|
||||
|
||||
## Commands
|
||||
|
||||
### Prerequisites
|
||||
- **Docker** - Container runtime
|
||||
- **Anthropic API key** - Set in `.env` file
|
||||
**Prerequisites:** Docker, AI provider credentials (`.env` for local, `shn setup` or env vars for npx)
|
||||
|
||||
### Dual CLI
|
||||
|
||||
Shannon supports two CLI modes, auto-detected based on the current working directory:
|
||||
|
||||
| | **npx** (`npx @keygraph/shannon`) | **Local** (`./shannon`) |
|
||||
|---|---|---|
|
||||
| **Install** | Zero-install via npm | Clone the repo |
|
||||
| **Image** | Pulled from Docker Hub (`keygraph/shannon:latest`) | Built locally (`shannon-worker`) |
|
||||
| **State** | `~/.shannon/` | Project directory |
|
||||
| **Credentials** | `~/.shannon/config.toml` (via `shn setup`) or env vars | `./.env` |
|
||||
| **Config** | `~/.shannon/config.toml` (via `shn setup`) | N/A |
|
||||
| **Prompts** | Bundled in Docker image | Mounted from `./apps/worker/prompts/` (live-editable) |
|
||||
|
||||
Mode auto-detection: local mode activates when env var `SHANNON_LOCAL=1` is set by the `./shannon` entry point (`apps/cli/src/mode.ts`). Otherwise npx mode.
|
||||
|
||||
### npx Quick Start
|
||||
|
||||
### Running the Penetration Testing Agent (Docker + Temporal)
|
||||
```bash
|
||||
# Configure credentials
|
||||
cp .env.example .env
|
||||
# Edit .env:
|
||||
# ANTHROPIC_API_KEY=your-key
|
||||
# CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 # Prevents token limits during long reports
|
||||
# Configure credentials (interactive wizard)
|
||||
npx @keygraph/shannon setup
|
||||
|
||||
# Start a pentest workflow
|
||||
./shannon start URL=<url> REPO=<path>
|
||||
# Or export env vars directly (non-interactive / CI)
|
||||
export ANTHROPIC_API_KEY=your-key
|
||||
|
||||
# Run
|
||||
npx @keygraph/shannon start -u <url> -r /path/to/repo
|
||||
```
|
||||
|
||||
Examples:
|
||||
### Local (Development) Quick Start
|
||||
|
||||
```bash
|
||||
./shannon start URL=https://example.com REPO=/path/to/repo
|
||||
./shannon start URL=https://example.com REPO=/path/to/repo CONFIG=./configs/my-config.yaml
|
||||
./shannon start URL=https://example.com REPO=/path/to/repo OUTPUT=./my-reports
|
||||
# Setup
|
||||
echo "ANTHROPIC_API_KEY=your-key" > .env
|
||||
|
||||
# Build (auto-runs if image missing)
|
||||
./shannon build
|
||||
|
||||
# Run
|
||||
./shannon start -u <url> -r my-repo
|
||||
./shannon start -u <url> -r my-repo -c ./apps/worker/configs/my-config.yaml
|
||||
./shannon start -u <url> -r /any/path/to/repo
|
||||
```
|
||||
|
||||
### Monitoring Progress
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
./shannon logs # View real-time worker logs
|
||||
./shannon query ID=<workflow-id> # Query specific workflow progress
|
||||
# Temporal Web UI available at http://localhost:8233
|
||||
# Setup (npx mode only — one-time credential configuration)
|
||||
npx @keygraph/shannon setup
|
||||
|
||||
# Workspaces & Resume
|
||||
./shannon start -u <url> -r my-repo -w my-audit # New named workspace
|
||||
./shannon start -u <url> -r my-repo -w my-audit # Resume (same command)
|
||||
./shannon workspaces # List all workspaces
|
||||
|
||||
# Monitor
|
||||
./shannon logs <workspace> # Tail workflow log
|
||||
./shannon status # Show running workers
|
||||
# Temporal Web UI: http://localhost:8233
|
||||
|
||||
# Stop
|
||||
./shannon stop # Preserves workflow data
|
||||
./shannon stop --clean # Full cleanup including volumes (confirms first)
|
||||
|
||||
# Image management
|
||||
./shannon build [--no-cache] # Local mode: build worker image
|
||||
npx @keygraph/shannon uninstall # npx mode: remove ~/.shannon/ (confirms first)
|
||||
|
||||
# Build TypeScript (development)
|
||||
pnpm run build # Build all packages via Turborepo
|
||||
pnpm run check # Type-check all packages
|
||||
pnpm biome # Biome lint + format + import sorting check
|
||||
pnpm biome:fix # Auto-fix lint, format, and import sorting
|
||||
```
|
||||
|
||||
### Stopping Shannon
|
||||
```bash
|
||||
./shannon stop # Stop containers (preserves workflow data)
|
||||
./shannon stop CLEAN=true # Full cleanup including volumes
|
||||
**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), `--debug` (preserve worker container after exit for log inspection)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Monorepo Layout
|
||||
|
||||
```
|
||||
apps/cli/ — @keygraph/shannon (published to npm, bundled with tsdown)
|
||||
apps/worker/ — @shannon/worker (private, Temporal worker + pipeline logic)
|
||||
```
|
||||
|
||||
### Options
|
||||
```bash
|
||||
CONFIG=<file> YAML configuration file for authentication and testing parameters
|
||||
OUTPUT=<path> Custom output directory for session folder (default: ./audit-logs/)
|
||||
PIPELINE_TESTING=true Use minimal prompts and fast retry intervals (10s instead of 5min)
|
||||
REBUILD=true Force Docker rebuild with --no-cache (use when code changes aren't picked up)
|
||||
```
|
||||
### CLI Package (`apps/cli/`)
|
||||
Published as `@keygraph/shannon` on npm. Contains only Docker orchestration logic — no Temporal SDK, business logic, or prompts. Bundled with tsdown for single-file ESM output.
|
||||
|
||||
### Generate TOTP for Authentication
|
||||
TOTP generation is handled automatically via the `generate_totp` MCP tool during authentication flows.
|
||||
- `apps/cli/src/index.ts` — CLI dispatcher (`setup`, `start`, `stop`, `logs`, `workspaces`, `status`, `build`, `uninstall`, `info`)
|
||||
- `apps/cli/src/mode.ts` — Auto-detection: local mode if `SHANNON_LOCAL=1` env var is set
|
||||
- `apps/cli/src/docker.ts` — Compose lifecycle, image pull/build, ephemeral `docker run` worker spawning
|
||||
- `apps/cli/src/home.ts` — State directory management (`~/.shannon/` for npx, `./` for local)
|
||||
- `apps/cli/src/env.ts` — `.env` loading, TOML fallback (npx only) via `apps/cli/src/config/resolver.ts`, credential validation, env flag building
|
||||
- `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 (bare name → `./repos/<name>`, or any absolute/relative path)
|
||||
- `apps/cli/src/commands/` — Command handlers
|
||||
- `apps/cli/infra/compose.yml` — Bundled Temporal compose file for npx mode
|
||||
- `apps/cli/tsdown.config.ts` — tsdown bundler config
|
||||
- `shannon` — Node.js entry point (`#!/usr/bin/env node`) that delegates to `apps/cli/dist/index.mjs`
|
||||
|
||||
### Development Commands
|
||||
```bash
|
||||
# Build TypeScript
|
||||
npm run build
|
||||
### Docker Architecture
|
||||
Infra (Temporal) runs via `docker-compose.yml`. Workers are ephemeral `docker run --rm` containers, one per scan, each with a unique task queue and isolated volume mounts.
|
||||
|
||||
# Run with pipeline testing mode (fast, minimal deliverables)
|
||||
./shannon start URL=<url> REPO=<path> PIPELINE_TESTING=true
|
||||
```
|
||||
- `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).
|
||||
|
||||
## Architecture & Components
|
||||
### Worker Package (`apps/worker/`)
|
||||
- `apps/worker/src/paths.ts` — Centralized path constants (`PROMPTS_DIR`, `CONFIGS_DIR`, `WORKSPACES_DIR`)
|
||||
- `apps/worker/src/session-manager.ts` — Agent definitions (`AGENTS` record). Agent types in `apps/worker/src/types/agents.ts`
|
||||
- `apps/worker/src/config-parser.ts` — YAML config parsing with JSON Schema validation
|
||||
- `apps/worker/src/ai/claude-executor.ts` — Claude Agent SDK integration with retry logic
|
||||
- `apps/worker/src/services/` — Business logic layer (Temporal-agnostic). Activities delegate here. Key: `agent-execution.ts`, `error-handling.ts`, `container.ts`
|
||||
- `apps/worker/src/types/` — Consolidated types: `Result<T,E>`, `ErrorCode`, `AgentName`, `ActivityLogger`, etc.
|
||||
- `apps/worker/src/utils/` — Shared utilities (file I/O, formatting, concurrency)
|
||||
|
||||
### Core Modules
|
||||
- `src/config-parser.ts` - Handles YAML configuration parsing, validation, and distribution to agents
|
||||
- `src/error-handling.ts` - Comprehensive error handling with retry logic and categorized error types
|
||||
- `src/tool-checker.ts` - Validates availability of external security tools before execution
|
||||
- `src/session-manager.ts` - Agent definitions, execution order, and parallel groups
|
||||
- `src/queue-validation.ts` - Validates deliverables and agent prerequisites
|
||||
### Temporal Orchestration
|
||||
Durable workflow orchestration with crash recovery, queryable progress, intelligent retry, and parallel execution (5 concurrent agents in vuln/exploit phases).
|
||||
|
||||
### Temporal Orchestration Layer
|
||||
Shannon uses Temporal for durable workflow orchestration:
|
||||
- `src/temporal/shared.ts` - Types, interfaces, query definitions
|
||||
- `src/temporal/workflows.ts` - Main workflow (pentestPipelineWorkflow)
|
||||
- `src/temporal/activities.ts` - Activity implementations with heartbeats
|
||||
- `src/temporal/worker.ts` - Worker process entry point
|
||||
- `src/temporal/client.ts` - CLI client for starting workflows
|
||||
- `src/temporal/query.ts` - Query tool for progress inspection
|
||||
- `apps/worker/src/temporal/workflows.ts` — Main workflow (`pentestPipelineWorkflow`)
|
||||
- `apps/worker/src/temporal/activities.ts` — Thin wrappers — heartbeat loop, error classification, container lifecycle. Business logic delegated to `apps/worker/src/services/`
|
||||
- `apps/worker/src/temporal/activity-logger.ts` — `TemporalActivityLogger` implementation of `ActivityLogger` interface
|
||||
- `apps/worker/src/temporal/summary-mapper.ts` — Maps `PipelineSummary` to `WorkflowSummary`
|
||||
- `apps/worker/src/temporal/worker.ts` — Combined worker + client entry point (per-invocation task queue, submits workflow, waits for result)
|
||||
- `apps/worker/src/temporal/shared.ts` — Types, interfaces, query definitions
|
||||
### Five-Phase Pipeline
|
||||
|
||||
Key features:
|
||||
- **Crash recovery** - Workflows resume automatically after worker restart
|
||||
- **Queryable progress** - Real-time status via `./shannon query` or Temporal Web UI
|
||||
- **Intelligent retry** - Distinguishes transient vs permanent errors
|
||||
- **Parallel execution** - 5 concurrent agents in vulnerability/exploitation phases
|
||||
|
||||
### Five-Phase Testing Workflow
|
||||
|
||||
1. **Pre-Reconnaissance** (`pre-recon`) - External tool scans (nmap, subfinder, whatweb) + source code analysis
|
||||
2. **Reconnaissance** (`recon`) - Analysis of initial findings and attack surface mapping
|
||||
3. **Vulnerability Analysis** (5 agents run in parallel)
|
||||
- `injection-vuln` - SQL injection, command injection
|
||||
- `xss-vuln` - Cross-site scripting
|
||||
- `auth-vuln` - Authentication bypasses
|
||||
- `authz-vuln` - Authorization flaws
|
||||
- `ssrf-vuln` - Server-side request forgery
|
||||
4. **Exploitation** (5 agents run in parallel, only if vulnerabilities found)
|
||||
- `injection-exploit` - Exploit injection vulnerabilities
|
||||
- `xss-exploit` - Exploit XSS vulnerabilities
|
||||
- `auth-exploit` - Exploit authentication issues
|
||||
- `authz-exploit` - Exploit authorization flaws
|
||||
- `ssrf-exploit` - Exploit SSRF vulnerabilities
|
||||
5. **Reporting** (`report`) - Executive-level security report generation
|
||||
|
||||
### Configuration System
|
||||
The agent supports YAML configuration files with JSON Schema validation:
|
||||
- `configs/config-schema.json` - JSON Schema for configuration validation
|
||||
- `configs/example-config.yaml` - Template configuration file
|
||||
- `configs/juice-shop-config.yaml` - Example configuration for OWASP Juice Shop
|
||||
- `configs/keygraph-config.yaml` - Configuration for Keygraph applications
|
||||
- `configs/chatwoot-config.yaml` - Configuration for Chatwoot applications
|
||||
- `configs/metabase-config.yaml` - Configuration for Metabase applications
|
||||
- `configs/cal-com-config.yaml` - Configuration for Cal.com applications
|
||||
|
||||
Configuration includes:
|
||||
- Authentication settings (form, SSO, API, basic auth)
|
||||
- Multi-factor authentication with TOTP support
|
||||
- Custom login flow instructions
|
||||
- Application-specific testing parameters
|
||||
|
||||
### Prompt Templates
|
||||
The `prompts/` directory contains specialized prompt templates for each testing phase:
|
||||
- `pre-recon-code.txt` - Initial code analysis prompts
|
||||
- `recon.txt` - Reconnaissance analysis prompts
|
||||
- `vuln-*.txt` - Vulnerability assessment prompts (injection, XSS, auth, authz, SSRF)
|
||||
- `exploit-*.txt` - Exploitation attempt prompts
|
||||
- `report-executive.txt` - Executive report generation prompts
|
||||
|
||||
### Claude Agent SDK Integration
|
||||
The agent uses the `@anthropic-ai/claude-agent-sdk` with maximum autonomy configuration:
|
||||
- `maxTurns: 10_000` - Allows extensive autonomous analysis
|
||||
- `permissionMode: 'bypassPermissions'` - Full system access for thorough testing
|
||||
- Playwright MCP integration for web browser automation
|
||||
- Working directory set to target local repository
|
||||
- Configuration context injection for authenticated testing
|
||||
|
||||
### Authentication & Login Resources
|
||||
- `prompts/shared/login-instructions.txt` - Login flow template for all agents
|
||||
- TOTP token generation via MCP `generate_totp` tool
|
||||
- Support for multi-factor authentication workflows
|
||||
- Configurable authentication mechanisms (form, SSO, API, basic)
|
||||
|
||||
### Output & Deliverables
|
||||
All analysis results are saved to the `deliverables/` directory within the target local repository, including:
|
||||
- Pre-reconnaissance reports with external scan results
|
||||
- Vulnerability assessment findings
|
||||
- Exploitation attempt results
|
||||
- Executive-level security reports with business impact analysis
|
||||
|
||||
### External Tool Dependencies
|
||||
The agent integrates with external security tools:
|
||||
- `nmap` - Network port scanning
|
||||
- `subfinder` - Subdomain discovery
|
||||
- `whatweb` - Web technology fingerprinting
|
||||
|
||||
Tools are validated for availability before execution using the tool-checker module.
|
||||
|
||||
### Audit & Metrics System
|
||||
The agent implements a crash-safe audit system with the following features:
|
||||
|
||||
**Architecture:**
|
||||
- **audit-logs/** (or custom `--output` path): Centralized metrics and forensic logs
|
||||
- `{hostname}_{sessionId}/session.json` - Comprehensive metrics with attempt-level detail
|
||||
- `{hostname}_{sessionId}/prompts/` - Exact prompts used for reproducibility
|
||||
- `{hostname}_{sessionId}/agents/` - Turn-by-turn execution logs
|
||||
- `{hostname}_{sessionId}/deliverables/` - Security reports and findings
|
||||
|
||||
**Crash Safety:**
|
||||
- Append-only logging with immediate flush (survives kill -9)
|
||||
- Atomic writes for session.json (no partial writes)
|
||||
- Event-based logging (tool_start, tool_end, llm_response)
|
||||
|
||||
**Concurrency Safety:**
|
||||
- SessionMutex prevents race conditions during parallel agent execution
|
||||
- 5x faster execution with parallel vulnerability and exploitation phases
|
||||
|
||||
**Metrics & Reporting:**
|
||||
- Phase-level and agent-level timing/cost aggregations
|
||||
- Validation results integrated with metrics
|
||||
1. **Pre-Recon** (`pre-recon`) — Source code analysis to build the architectural baseline
|
||||
2. **Recon** (`recon`) — Attack surface mapping from initial findings
|
||||
3. **Vulnerability Analysis** (5 parallel agents) — injection, xss, auth, authz, ssrf
|
||||
4. **Exploitation** (5 parallel agents, conditional) — Exploits confirmed vulnerabilities
|
||||
5. **Reporting** (`report`) — Executive-level security report
|
||||
|
||||
### Supporting Systems
|
||||
- **Configuration** — YAML configs in `apps/worker/configs/` with JSON Schema validation (`config-schema.json`). Supports auth settings (MFA/TOTP), URL/code rule scoping (`rules.avoid`/`rules.focus`), run-scope steering (`vuln_classes`, `exploit`), free-form `rules_of_engagement`, and post-hoc `report` filters (`min_severity`, `min_confidence`, `guidance`). `code_path` avoid rules are written into `~/.claude/settings.json` `permissions.deny` (`Read`/`Edit`) once per workflow by `apps/worker/src/temporal/activities.ts:syncCodePathDenyRules` so the SDK enforces them at the tool layer even in `bypassPermissions` mode. `vuln_classes`/`exploit` scope is locked into `session.json` on first run; resumes with a different scope fail fast (`persistOrValidateRunScope`). Credential resolution — local mode: env vars → `./.env`; npx mode: env vars → `~/.shannon/config.toml` (via `shn setup`)
|
||||
- **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
|
||||
- **SDK Integration** — Uses `@anthropic-ai/claude-agent-sdk` with `maxTurns: 10_000` and `bypassPermissions` mode. Adaptive thinking is enabled by default on Opus 4.6/4.7/4.8 (`supportsAdaptiveThinking` in `apps/worker/src/ai/models.ts`); disable per-scan via `CLAUDE_ADAPTIVE_THINKING=false` (env) or `core.adaptive_thinking = false` (npx TOML). Browser automation via `playwright-cli` with session isolation (`-s=<session>`). TOTP generation via `generate-totp` CLI tool. Login flow template at `apps/worker/prompts/shared/login-instructions.txt` supports form, SSO, API, and basic auth. On authenticated whitebox scans, the `validate-authentication` preflight performs the single real login and saves the browser session to `auth-state.json` in the per-session audit directory (path from `authStateFile()` in `apps/worker/src/audit/utils.ts`, derived from `generateAuditPath()`). The validation activity (`apps/worker/src/services/validate-authentication.ts`) removes any stale file from a prior run before the agent runs and verifies the file parses and contains cookies or storage before the preflight is marked complete; `logWorkflowComplete` deletes it when the workflow ends so authenticated cookies don't sit on disk between scans. Agent prompts opt in to session reuse by `@include(shared/_shared-session.txt)` before their `<login_instructions>` block — the partial restores the session and falls through to the full login flow if verification fails. `vuln-auth`/`exploit-auth` omit the include and own their own login
|
||||
- **Audit System** — Crash-safe append-only logging in `workspaces/{hostname}_{sessionId}/`. Tracks session metrics, per-agent logs, prompts, and deliverables. 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
|
||||
- **Deliverables** — Saved to `deliverables/` in the target repo via the `save-deliverable` CLI script (`apps/worker/src/scripts/save-deliverable.ts`)
|
||||
- **Workspaces & Resume** — Named workspaces via `-w <name>` or auto-named from URL+timestamp. Resume detects completed agents via `session.json`. `loadResumeState()` in `apps/worker/src/temporal/activities.ts` validates deliverable existence, restores git checkpoints, and cleans up incomplete deliverables. Workspace listing via `apps/worker/src/temporal/workspaces.ts`
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Learning from Reference Implementations
|
||||
|
||||
A working POC exists at `/Users/arjunmalleswaran/Code/shannon-pocs` that demonstrates the ideal Temporal + Claude Agent SDK integration. When implementing Temporal features, agents can ask questions in the chat, and the user will relay them to another Claude Code session working in that POC directory.
|
||||
|
||||
**How to use this approach:**
|
||||
1. When stuck or unsure about Temporal patterns, write a specific question in the chat
|
||||
2. The user will ask an agent working on the POC to answer
|
||||
3. The user relays the answer (code snippets, patterns, explanations) back
|
||||
4. Apply the learned patterns to Shannon's codebase
|
||||
|
||||
**Example questions to ask:**
|
||||
- "How does the POC structure its workflow to handle parallel activities?"
|
||||
- "Show me how heartbeats are implemented in the POC's activities"
|
||||
- "What retry configuration does the POC use for long-running agent activities?"
|
||||
- "How does the POC integrate Claude Agent SDK calls within Temporal activities?"
|
||||
|
||||
**Reference implementation:**
|
||||
- **Temporal + Claude Agent SDK**: `/Users/arjunmalleswaran/Code/shannon-pocs` - working implementation demonstrating workflows, activities, worker setup, and SDK integration
|
||||
|
||||
### Adding a New Agent
|
||||
1. Define the agent in `src/session-manager.ts` (add to `AGENT_QUEUE` and appropriate parallel group)
|
||||
2. Create prompt template in `prompts/` (e.g., `vuln-newtype.txt` or `exploit-newtype.txt`)
|
||||
3. Add activity function in `src/temporal/activities.ts`
|
||||
4. Register activity in `src/temporal/workflows.ts` within the appropriate phase
|
||||
1. Define agent in `apps/worker/src/session-manager.ts` (add to `AGENTS` record). `ALL_AGENTS`/`AgentName` types live in `apps/worker/src/types/agents.ts`
|
||||
2. Create prompt template in `apps/worker/prompts/` (e.g., `vuln-newtype.txt`)
|
||||
3. Two-layer pattern: add a thin activity wrapper in `apps/worker/src/temporal/activities.ts` (heartbeat + error classification). `AgentExecutionService` in `apps/worker/src/services/agent-execution.ts` handles the agent lifecycle automatically via the `AGENTS` registry
|
||||
4. Register activity in `apps/worker/src/temporal/workflows.ts` within the appropriate phase
|
||||
|
||||
### Modifying Prompts
|
||||
- Prompt templates use variable substitution: `{{TARGET_URL}}`, `{{CONFIG_CONTEXT}}`, `{{LOGIN_INSTRUCTIONS}}`
|
||||
- Shared partials in `prompts/shared/` are included via `prompt-manager.ts`
|
||||
- Test changes with `PIPELINE_TESTING=true` for faster iteration
|
||||
- Variable substitution: `{{TARGET_URL}}`, `{{CONFIG_CONTEXT}}`, `{{LOGIN_INSTRUCTIONS}}`
|
||||
- Shared partials in `apps/worker/prompts/shared/` included via `apps/worker/src/services/prompt-manager.ts`
|
||||
- Test with `--pipeline-testing` for fast iteration
|
||||
|
||||
### Key Design Patterns
|
||||
- **Configuration-Driven Architecture**: YAML configs with JSON Schema validation
|
||||
- **Modular Error Handling**: Categorized error types with retry logic
|
||||
- **SDK-First Approach**: Heavy reliance on Claude Agent SDK for autonomous AI operations
|
||||
- **Progressive Analysis**: Each phase builds on previous phase results
|
||||
- **Configuration-Driven** — YAML configs with JSON Schema validation
|
||||
- **Progressive Analysis** — Each phase builds on previous results
|
||||
- **SDK-First** — Claude Agent SDK handles autonomous analysis
|
||||
- **Modular Error Handling** — `ErrorCode` enum, `Result<T,E>` for explicit error propagation, automatic retry (3 attempts per agent)
|
||||
- **Services Boundary** — Activities are thin Temporal wrappers; `apps/worker/src/services/` owns business logic, accepts `ActivityLogger`, returns `Result<T,E>`. No Temporal imports in services
|
||||
- **DI Container** — Per-workflow in `apps/worker/src/services/container.ts`. `AuditSession` excluded (parallel safety)
|
||||
- **Ephemeral Workers** — Each scan runs in its own `docker run --rm` container with a per-invocation task queue. Temporal routes activities by queue name, so per-scan queues ensure activities never land on a worker with the wrong repo mounted
|
||||
|
||||
### Error Handling Strategy
|
||||
The application uses a comprehensive error handling system with:
|
||||
- Categorized error types (PentestError, ConfigError, NetworkError, etc.)
|
||||
- Automatic retry logic for transient failures (3 attempts per agent)
|
||||
- Graceful degradation when external tools are unavailable
|
||||
- Detailed error logging and user-friendly error messages
|
||||
### Security
|
||||
Defensive security tool only. Use only on systems you own or have explicit permission to test.
|
||||
|
||||
### Testing Mode
|
||||
The agent includes a testing mode that skips external tool execution for faster development cycles:
|
||||
```bash
|
||||
./shannon start URL=<url> REPO=<path> PIPELINE_TESTING=true
|
||||
```
|
||||
## Code Style Guidelines
|
||||
|
||||
### Security Focus
|
||||
This is explicitly designed as a **defensive security tool** for:
|
||||
- Vulnerability assessment
|
||||
- Security analysis
|
||||
- Penetration testing
|
||||
- Security report generation
|
||||
### Formatting
|
||||
Biome handles formatting and linting. Run `pnpm biome:fix` to auto-fix. Config in `biome.json`: single quotes, semicolons, trailing commas, 2-space indent, 120 char line width.
|
||||
|
||||
The tool should only be used on systems you own or have explicit permission to test.
|
||||
### Clarity Over Brevity
|
||||
- Optimize for readability, not line count — three clear lines beat one dense expression
|
||||
- Use descriptive names that convey intent
|
||||
- Prefer explicit logic over clever one-liners
|
||||
|
||||
## Key Files & Directories
|
||||
### Structure
|
||||
- Keep functions focused on a single responsibility
|
||||
- Use early returns and guard clauses instead of deep nesting
|
||||
- Never use nested ternary operators — use if/else or switch
|
||||
- Extract complex conditions into well-named boolean variables
|
||||
|
||||
**Entry Points:**
|
||||
- `src/temporal/workflows.ts` - Temporal workflow definition
|
||||
- `src/temporal/activities.ts` - Activity implementations with heartbeats
|
||||
- `src/temporal/worker.ts` - Worker process entry point
|
||||
- `src/temporal/client.ts` - CLI client for starting workflows
|
||||
### TypeScript Conventions
|
||||
- Use `function` keyword for top-level functions (not arrow functions)
|
||||
- Explicit return type annotations on exported/top-level functions
|
||||
- Prefer `readonly` for data that shouldn't be mutated
|
||||
- `exactOptionalPropertyTypes` is enabled — use spread for optional props, not direct `undefined` assignment
|
||||
|
||||
**Core Logic:**
|
||||
- `src/session-manager.ts` - Agent definitions, execution order, parallel groups
|
||||
- `src/ai/claude-executor.ts` - Claude Agent SDK integration
|
||||
- `src/config-parser.ts` - YAML config parsing with JSON Schema validation
|
||||
- `src/audit/` - Crash-safe logging and metrics system
|
||||
### Avoid
|
||||
- Combining multiple concerns into a single function to "save lines"
|
||||
- Dense callback chains when sequential logic is clearer
|
||||
- Sacrificing readability for DRY — some repetition is fine if clearer
|
||||
- Abstractions for one-time operations
|
||||
- Backwards-compatibility shims, deprecated wrappers, or re-exports for removed code — delete the old code, don't preserve it
|
||||
|
||||
**Configuration:**
|
||||
- `shannon` - CLI script for running pentests
|
||||
- `docker-compose.yml` - Temporal server + worker containers
|
||||
- `configs/` - YAML configs with `config-schema.json` for validation
|
||||
- `prompts/` - AI prompt templates (`vuln-*.txt`, `exploit-*.txt`, etc.)
|
||||
### Comments
|
||||
Comments must be **timeless** — no references to this conversation, refactoring history, or the AI.
|
||||
|
||||
**Output:**
|
||||
- `audit-logs/{hostname}_{sessionId}/` - Session metrics, agent logs, deliverables
|
||||
**Patterns used in this codebase:**
|
||||
- `/** JSDoc */` — file headers (after license) and exported functions/interfaces
|
||||
- `// N. Description` — numbered sequential steps inside function bodies. Use when a
|
||||
function has 3+ distinct phases where at least one isn't immediately obvious from the
|
||||
code. Each step marks the start of a logical phase. Reference: `AgentExecutionService.execute`
|
||||
(steps 1-9) and `injectModelIntoReport` (steps 1-5)
|
||||
- `// === Section ===` — high-level dividers between groups of functions in long files,
|
||||
or to label major branching/classification blocks (e.g., `// === SPENDING CAP SAFEGUARD ===`).
|
||||
Not for sequential steps inside function bodies — use numbered steps for that
|
||||
- `// NOTE:` / `// WARNING:` / `// IMPORTANT:` — gotchas and constraints
|
||||
|
||||
**Never:** obvious comments, conversation references ("as discussed"), history ("moved from X")
|
||||
|
||||
## Key Files
|
||||
|
||||
**CLI:** `shannon` (entry point), `apps/cli/src/index.ts` (dispatcher), `apps/cli/src/docker.ts` (orchestration), `apps/cli/src/mode.ts` (auto-detection)
|
||||
|
||||
**Entry Points:** `apps/worker/src/temporal/workflows.ts`, `apps/worker/src/temporal/activities.ts`, `apps/worker/src/temporal/worker.ts`
|
||||
|
||||
**Core Logic:** `apps/worker/src/session-manager.ts`, `apps/worker/src/ai/claude-executor.ts`, `apps/worker/src/ai/settings-writer.ts` (writes `code_path` deny rules to `~/.claude/settings.json`), `apps/worker/src/config-parser.ts`, `apps/worker/src/services/` (incl. `preflight.ts`, `findings-renderer.ts`, `reporting.ts`), `apps/worker/src/audit/`
|
||||
|
||||
**Config:** `docker-compose.yml`, `apps/cli/infra/compose.yml`, `apps/worker/configs/`, `apps/worker/prompts/`, `tsconfig.base.json` (shared compiler options), `turbo.json`, `biome.json`
|
||||
|
||||
**CI/CD:** `.github/workflows/release.yml` (Docker Hub push + npm publish + GitHub release, manual dispatch)
|
||||
|
||||
## Package Installation
|
||||
|
||||
Package managers are configured with a minimum release age (7 days). Requires pnpm >= 10.16.0. If `pnpm install` fails due to a package being too new, **do not attempt to bypass it** — report the blocked package to the user and stop.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
- **"Repository not found"**: Ensure target local directory exists and is accessible
|
||||
|
||||
### Temporal & Docker Issues
|
||||
- **"Temporal not ready"**: Wait for health check or run `docker compose logs temporal`
|
||||
- **Worker not processing**: Ensure worker container is running with `docker compose ps`
|
||||
- **Reset workflow state**: `./shannon stop CLEAN=true` removes all Temporal data and volumes
|
||||
- **Local apps unreachable**: Use `host.docker.internal` instead of `localhost` for URLs
|
||||
- **Container permissions**: On Linux, may need `sudo` for docker commands
|
||||
|
||||
### External Tool Dependencies
|
||||
Missing tools can be skipped using `PIPELINE_TESTING=true` mode during development:
|
||||
- `nmap` - Network scanning
|
||||
- `subfinder` - Subdomain discovery
|
||||
- `whatweb` - Web technology detection
|
||||
|
||||
### Diagnostic & Utility Scripts
|
||||
```bash
|
||||
# View Temporal workflow history
|
||||
open http://localhost:8233
|
||||
```
|
||||
- **"Repository not found"** — Pass a bare name (`-r my-repo`) for `./repos/my-repo`, or a path (`-r /path/to/repo`) for any directory
|
||||
- **"Temporal not ready"** — Wait for health check or `docker compose logs temporal`
|
||||
- **Worker not processing** — Check `docker ps --filter "name=shannon-worker-"`
|
||||
- **Reset state** — `./shannon stop --clean`
|
||||
- **Local apps unreachable** — Use `host.docker.internal` instead of `localhost`
|
||||
- **Container permissions** — On Linux, may need `sudo` for docker commands
|
||||
|
||||
+46
-73
@@ -13,42 +13,32 @@ RUN apk update && apk add --no-cache \
|
||||
curl \
|
||||
wget \
|
||||
ca-certificates \
|
||||
# Network libraries for Go tools
|
||||
libpcap-dev \
|
||||
linux-headers \
|
||||
# Language runtimes
|
||||
go \
|
||||
nodejs-22 \
|
||||
npm \
|
||||
python3 \
|
||||
py3-pip \
|
||||
ruby \
|
||||
ruby-dev \
|
||||
# Security tools available in Wolfi
|
||||
nmap \
|
||||
# Additional utilities
|
||||
bash
|
||||
|
||||
# Set environment variables for Go
|
||||
ENV GOPATH=/go
|
||||
ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
ENV CGO_ENABLED=1
|
||||
# Install pnpm
|
||||
RUN npm install -g --ignore-scripts pnpm@10.33.0
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p $GOPATH/bin
|
||||
# Build Node.js application in builder to avoid QEMU emulation failures in CI
|
||||
WORKDIR /app
|
||||
|
||||
# Install Go-based security tools
|
||||
RUN go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest
|
||||
# Install WhatWeb from GitHub (Ruby-based tool)
|
||||
RUN git clone --depth 1 https://github.com/urbanadventurer/WhatWeb.git /opt/whatweb && \
|
||||
chmod +x /opt/whatweb/whatweb && \
|
||||
gem install addressable && \
|
||||
echo '#!/bin/bash' > /usr/local/bin/whatweb && \
|
||||
echo 'cd /opt/whatweb && exec ./whatweb "$@"' >> /usr/local/bin/whatweb && \
|
||||
chmod +x /usr/local/bin/whatweb
|
||||
# Copy workspace manifests for install layer caching
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./
|
||||
COPY apps/worker/package.json ./apps/worker/
|
||||
COPY apps/cli/package.json ./apps/cli/
|
||||
|
||||
# Install Python-based tools
|
||||
RUN pip3 install --no-cache-dir schemathesis
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
# Build worker. CLI not needed in Docker
|
||||
RUN pnpm --filter @shannon/worker run build
|
||||
|
||||
# Production-only deps (pnpm recommends install --prod over prune in monorepos)
|
||||
RUN rm -rf node_modules apps/*/node_modules && pnpm install --frozen-lockfile --prod
|
||||
|
||||
# Runtime stage - Minimal production image
|
||||
FROM cgr.dev/chainguard/wolfi-base:latest AS runtime
|
||||
@@ -61,15 +51,11 @@ RUN apk update && apk add --no-cache \
|
||||
bash \
|
||||
curl \
|
||||
ca-certificates \
|
||||
# Network libraries (runtime)
|
||||
libpcap \
|
||||
# Security tools
|
||||
nmap \
|
||||
shadow \
|
||||
# Language runtimes (minimal)
|
||||
nodejs-22 \
|
||||
npm \
|
||||
python3 \
|
||||
ruby \
|
||||
# Chromium browser and dependencies for Playwright
|
||||
chromium \
|
||||
# Additional libraries Chromium needs
|
||||
@@ -87,71 +73,58 @@ RUN apk update && apk add --no-cache \
|
||||
# Font rendering
|
||||
fontconfig
|
||||
|
||||
# Copy Go binaries from builder
|
||||
COPY --from=builder /go/bin/subfinder /usr/local/bin/
|
||||
|
||||
# Copy WhatWeb from builder
|
||||
COPY --from=builder /opt/whatweb /opt/whatweb
|
||||
COPY --from=builder /usr/local/bin/whatweb /usr/local/bin/whatweb
|
||||
|
||||
# Install WhatWeb Ruby dependencies in runtime stage
|
||||
RUN gem install addressable
|
||||
|
||||
# Copy Python packages from builder
|
||||
COPY --from=builder /usr/lib/python3.*/site-packages /usr/lib/python3.12/site-packages
|
||||
COPY --from=builder /usr/bin/schemathesis /usr/bin/
|
||||
|
||||
# Create non-root user for security
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 pentest && \
|
||||
adduser -u 1001 -G pentest -s /bin/bash -D pentest
|
||||
|
||||
# System-level git config (survives UID remapping in entrypoint)
|
||||
RUN git config --system user.email "agent@localhost" && \
|
||||
git config --system user.name "Pentest Agent" && \
|
||||
git config --system --add safe.directory '*'
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files first for better caching
|
||||
COPY package*.json ./
|
||||
COPY mcp-server/package*.json ./mcp-server/
|
||||
# Copy only what the worker needs (skip CLI source, infra, tsdown artifacts)
|
||||
COPY --from=builder /app/package.json /app/pnpm-workspace.yaml /app/pnpm-lock.yaml /app/.npmrc /app/
|
||||
COPY --from=builder /app/node_modules /app/node_modules
|
||||
COPY --from=builder /app/apps/worker /app/apps/worker
|
||||
COPY --from=builder /app/apps/cli/package.json /app/apps/cli/package.json
|
||||
|
||||
# Install Node.js dependencies (including devDependencies for TypeScript build)
|
||||
RUN npm ci && \
|
||||
cd mcp-server && npm ci && cd .. && \
|
||||
npm cache clean --force
|
||||
RUN npm install -g --ignore-scripts @anthropic-ai/claude-code@2.1.84 @playwright/cli@0.1.1
|
||||
RUN mkdir -p /tmp/.claude/skills && \
|
||||
playwright-cli install --skills && \
|
||||
cp -r .claude/skills/playwright-cli /tmp/.claude/skills/ && \
|
||||
rm -rf .claude
|
||||
|
||||
# Copy application source code
|
||||
COPY . .
|
||||
|
||||
# Build TypeScript (mcp-server first, then main project)
|
||||
RUN cd mcp-server && npm run build && cd .. && npm run build
|
||||
|
||||
# Remove devDependencies after build to reduce image size
|
||||
RUN npm prune --production && \
|
||||
cd mcp-server && npm prune --production
|
||||
# Symlink CLI tools onto PATH
|
||||
RUN ln -s /app/apps/worker/dist/scripts/save-deliverable.js /usr/local/bin/save-deliverable && \
|
||||
chmod +x /app/apps/worker/dist/scripts/save-deliverable.js && \
|
||||
ln -s /app/apps/worker/dist/scripts/generate-totp.js /usr/local/bin/generate-totp && \
|
||||
chmod +x /app/apps/worker/dist/scripts/generate-totp.js
|
||||
|
||||
# Create directories for session data and ensure proper permissions
|
||||
RUN mkdir -p /app/sessions /app/deliverables /app/repos /app/configs && \
|
||||
RUN mkdir -p /app/sessions /app/repos /app/workspaces && \
|
||||
mkdir -p /tmp/.cache /tmp/.config /tmp/.npm && \
|
||||
chmod 777 /app && \
|
||||
chmod 777 /tmp/.cache && \
|
||||
chmod 777 /tmp/.config && \
|
||||
chmod 777 /tmp/.npm && \
|
||||
chown -R pentest:pentest /app
|
||||
chown -R pentest:pentest /app /tmp/.claude
|
||||
|
||||
# Switch to non-root user
|
||||
USER pentest
|
||||
|
||||
# Configure Git to trust all directories
|
||||
RUN git config --global --add safe.directory '*'
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
# Set environment variables
|
||||
ENV NODE_ENV=production
|
||||
ENV PATH="/usr/local/bin:$PATH"
|
||||
ENV SHANNON_DOCKER=true
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
ENV PLAYWRIGHT_MCP_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
ENV npm_config_cache=/tmp/.npm
|
||||
ENV HOME=/tmp
|
||||
ENV XDG_CACHE_HOME=/tmp/.cache
|
||||
ENV XDG_CONFIG_HOME=/tmp/.config
|
||||
|
||||
# Set entrypoint
|
||||
ENTRYPOINT ["node", "dist/shannon.js"]
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
CMD ["node", "apps/worker/dist/temporal/worker.js"]
|
||||
|
||||
@@ -1,526 +1,251 @@
|
||||
> [!NOTE]
|
||||
> **[Shannon Lite achieves a 96.15% success rate on a hint-free, source-aware XBOW benchmark. →](https://github.com/KeygraphHQ/shannon/tree/main/xben-benchmark-results/README.md)**
|
||||
|
||||
>[!NOTE]
|
||||
> **[Better Steerability, Authentication Improvements, and the Migration to the Pi Harness](https://github.com/KeygraphHQ/shannon/discussions/348)**
|
||||
|
||||
<div align="center">
|
||||
|
||||
<img src="./assets/shannon-screen.png" alt="Shannon Screen" width="100%">
|
||||
<img src="./assets/github-banner.png" alt="Shannon - AI Pentester for Web Applications and APIs" width="100%">
|
||||
|
||||
# Shannon is your fully autonomous AI pentester.
|
||||
# Shannon - AI Pentester by Keygraph
|
||||
|
||||
Shannon’s job is simple: break your web app before anyone else does. <br />
|
||||
The Red Team to your vibe-coding Blue team. <br />
|
||||
Every Claude (coder) deserves their Shannon.
|
||||
<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, white-box AI pentester for web applications and APIs. <br />
|
||||
It analyzes your source code, identifies attack paths, and executes real exploits to prove vulnerabilities before they reach production.
|
||||
|
||||
---
|
||||
|
||||
[Website](https://keygraph.io) • [Discord](https://discord.gg/KAqzSHHpRt)
|
||||
<a href="https://discord.gg/9ZqQPuhJB7"><img src="./assets/discord.png" height="40" alt="Join Discord"></a>
|
||||
<a href="https://keygraph.io/"><img src="./assets/Keygraph_Button.png" height="40" alt="Visit Keygraph.io"></a>
|
||||
|
||||
---
|
||||
</div>
|
||||
|
||||
## 🎯 What is Shannon?
|
||||
> [!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.
|
||||
|
||||
Shannon is an AI pentester that delivers actual exploits, not just alerts.
|
||||
## Table of Contents
|
||||
|
||||
Shannon's goal is to break your web app before someone else does. It autonomously hunts for attack vectors in your code, then uses its built-in browser to execute real exploits, such as injection attacks, and auth bypass, to prove the vulnerability is actually exploitable.
|
||||
- [What is Shannon?](#what-is-shannon)
|
||||
- [Product Line](#product-line)
|
||||
- [Shannon Lite in Action](#shannon-lite-in-action)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Key Capabilities](#key-capabilities)
|
||||
- [Shannon Lite and Shannon Pro](#shannon-lite-and-shannon-pro)
|
||||
- [Architecture](#architecture)
|
||||
- [Documentation](#documentation)
|
||||
- [Safety, Scope, and Limitations](#safety-scope-and-limitations)
|
||||
- [License and Enterprise Licensing](#license-and-enterprise-licensing)
|
||||
- [Community and Support](#community-and-support)
|
||||
|
||||
**What Problem Does Shannon Solve?**
|
||||
## What is Shannon?
|
||||
|
||||
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 is an AI pentester developed by [Keygraph](https://keygraph.io). It performs white-box security testing of web applications and their underlying APIs by combining source-code analysis with live exploitation.
|
||||
|
||||
Shannon closes this gap by acting as your on-demand whitebox pentester. It doesn't just find potential issues. It executes real exploits, providing concrete proof of vulnerabilities. This lets you ship with confidence, knowing every build can be secured.
|
||||
Shannon analyzes your web application's source code to identify potential attack vectors, then uses browser automation and command-line tools to execute real exploits against the running application and its APIs. Only vulnerabilities with a working proof-of-concept are included in the final report.
|
||||
|
||||
> [!NOTE]
|
||||
> **From Autonomous Pentesting to Automated Compliance**
|
||||
>
|
||||
> Shannon is a core component of the **Keygraph Security and Compliance Platform**.
|
||||
>
|
||||
> While Shannon automates the critical task of penetration testing for your application, our broader platform automates your entire compliance journey—from evidence collection to audit readiness. We're building the "Rippling for Cybersecurity," a single platform to manage your security posture and streamline compliance frameworks like SOC 2 and HIPAA.
|
||||
>
|
||||
> ➡️ **[Learn more about the Keygraph Platform](https://keygraph.io)**
|
||||
### Why Shannon Exists
|
||||
|
||||
## 🎬 See Shannon in Action
|
||||
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.
|
||||
|
||||
**Real Results**: Shannon discovered 20+ critical vulnerabilities in OWASP Juice Shop, including complete auth bypass and database exfiltration. [See full report →](sample-reports/shannon-report-juice-shop.md)
|
||||
Shannon closes that gap by providing on-demand, automated penetration testing that can run against every build or release.
|
||||
|
||||

|
||||
## Product Line
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Fully Autonomous Operation**: Launch the pentest with a single command. The AI handles everything from advanced 2FA/TOTP logins (including sign in with Google) and browser navigation to the final report with zero intervention.
|
||||
- **Pentester-Grade Reports with Reproducible Exploits**: Delivers a final report focused on proven, exploitable findings, complete with copy-and-paste Proof-of-Concepts to eliminate false positives and provide actionable results.
|
||||
- **Critical OWASP Vulnerability Coverage**: Currently identifies and validates the following critical vulnerabilities: Injection, XSS, SSRF, and Broken Authentication/Authorization, with more types in development.
|
||||
- **Code-Aware Dynamic Testing**: Analyzes your source code to intelligently guide its attack strategy, then performs live, browser and command line based exploits on the running application to confirm real-world risk.
|
||||
- **Powered by Integrated Security Tools**: Enhances its discovery phase by leveraging leading reconnaissance and testing tools—including **Nmap, Subfinder, WhatWeb, and Schemathesis**—for deep analysis of the target environment.
|
||||
- **Parallel Processing for Faster Results**: Get your report faster. The system parallelizes the most time-intensive phases, running analysis and exploitation for all vulnerability types concurrently.
|
||||
|
||||
## 📦 Product Line
|
||||
|
||||
Shannon is available in two editions:
|
||||
Shannon is developed by [Keygraph](https://keygraph.io) and available in two editions:
|
||||
|
||||
| Edition | License | Best For |
|
||||
|---------|---------|----------|
|
||||
| **Shannon Lite** | AGPL-3.0 | Security teams, independent researchers, testing your own applications |
|
||||
| **Shannon Pro** | Commercial | Enterprises requiring advanced features, CI/CD integration, and dedicated support |
|
||||
| --- | --- | --- |
|
||||
| **Shannon Lite** | AGPL-3.0 | Local, strictly white-box testing of applications you own or are authorized to test. |
|
||||
| **Shannon Pro** | Commercial | Organizations needing a continuous pentesting and AppSec platform with black-box and white-box pentesting, parsed-code SAST, CI/CD gating, verified remediation, SLA tracking, and enterprise deployment. |
|
||||
|
||||
> **This repository contains Shannon Lite,** which utilizes our core autonomous AI pentesting framework. **Shannon Pro** enhances this foundation with an advanced, LLM-powered data flow analysis engine (inspired by the [LLMDFA paper](https://arxiv.org/abs/2402.10754)) for enterprise-grade code analysis and deeper vulnerability detection.
|
||||
## Shannon Lite in Action
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **White-box only.** Shannon Lite is designed for **white-box (source-available)** application security testing.
|
||||
> It expects access to your application's source code and repository layout.
|
||||
<p align="center">
|
||||
<img src="assets/shannon-action.gif" alt="Shannon Lite running an autonomous pentest" width="100%">
|
||||
</p>
|
||||
|
||||
[See feature comparison](./SHANNON-PRO.md)
|
||||
## 📑 Table of Contents
|
||||
Sample Shannon Lite penetration test reports from intentionally vulnerable applications:
|
||||
|
||||
- [What is Shannon?](#-what-is-shannon)
|
||||
- [See Shannon in Action](#-see-shannon-in-action)
|
||||
- [Features](#-features)
|
||||
- [Product Line](#-product-line)
|
||||
- [Setup & Usage Instructions](#-setup--usage-instructions)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Monitoring Progress](#monitoring-progress)
|
||||
- [Stopping Shannon](#stopping-shannon)
|
||||
- [Usage Examples](#usage-examples)
|
||||
- [Configuration (Optional)](#configuration-optional)
|
||||
- [Output and Results](#output-and-results)
|
||||
- [Sample Reports & Benchmarks](#-sample-reports--benchmarks)
|
||||
- [Architecture](#-architecture)
|
||||
- [Coverage and Roadmap](#-coverage-and-roadmap)
|
||||
- [Disclaimers](#-disclaimers)
|
||||
- [Telemetry](#-telemetry)
|
||||
- [License](#-license)
|
||||
- [Community & Support](#-community--support)
|
||||
- [Get in Touch](#-get-in-touch)
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Setup & Usage Instructions
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Docker** - Container runtime ([Install Docker](https://docs.docker.com/get-docker/))
|
||||
- **Anthropic API key or Claude Code OAuth token** - Get from [Anthropic Console](https://console.anthropic.com)
|
||||
- **Docker** - required for the worker container.
|
||||
- **Node.js 18+** - required for the recommended `npx` workflow.
|
||||
- **AI provider credentials** - Anthropic is recommended; AWS Bedrock, Google Vertex AI, and compatible proxy setups are documented separately.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Clone Shannon
|
||||
git clone https://github.com/KeygraphHQ/shannon.git
|
||||
cd shannon
|
||||
|
||||
# 2. Configure credentials (choose one method)
|
||||
|
||||
# Option A: Export environment variables
|
||||
export ANTHROPIC_API_KEY="your-api-key" # or CLAUDE_CODE_OAUTH_TOKEN
|
||||
export CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 # recommended
|
||||
|
||||
# Option B: Create a .env file
|
||||
cat > .env << 'EOF'
|
||||
ANTHROPIC_API_KEY=your-api-key
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000
|
||||
EOF
|
||||
|
||||
# 3. Run a pentest
|
||||
./shannon start URL=https://your-app.com REPO=/path/to/your/repo
|
||||
```
|
||||
|
||||
Shannon will build the containers, start the workflow, and return a workflow ID. The pentest runs in the background.
|
||||
|
||||
### Monitoring Progress
|
||||
|
||||
```bash
|
||||
# View real-time worker logs
|
||||
./shannon logs
|
||||
|
||||
# Query a specific workflow's progress
|
||||
./shannon query ID=shannon-1234567890
|
||||
|
||||
# Open the Temporal Web UI for detailed monitoring
|
||||
open http://localhost:8233
|
||||
```
|
||||
|
||||
### Stopping Shannon
|
||||
|
||||
```bash
|
||||
# Stop all containers (preserves workflow data)
|
||||
./shannon stop
|
||||
|
||||
# Full cleanup (removes all data)
|
||||
./shannon stop CLEAN=true
|
||||
```
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```bash
|
||||
# Basic pentest
|
||||
./shannon start URL=https://example.com REPO=/path/to/repo
|
||||
|
||||
# With a configuration file
|
||||
./shannon start URL=https://example.com REPO=/path/to/repo CONFIG=./configs/my-config.yaml
|
||||
|
||||
# Custom output directory
|
||||
./shannon start URL=https://example.com REPO=/path/to/repo OUTPUT=./my-reports
|
||||
```
|
||||
|
||||
### Prepare Your Repository
|
||||
|
||||
Shannon is designed for **web application security testing** and expects all application code to be available in a single directory structure. This works well for:
|
||||
|
||||
- **Monorepos** - Single repository containing all components
|
||||
- **Consolidated setups** - Multiple repositories organized in a shared folder
|
||||
|
||||
**For monorepos:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-org/your-monorepo.git /path/to/your-app
|
||||
```
|
||||
|
||||
**For multi-repository applications** (e.g., separate frontend/backend):
|
||||
|
||||
```bash
|
||||
mkdir /path/to/your-app
|
||||
cd /path/to/your-app
|
||||
git clone https://github.com/your-org/frontend.git
|
||||
git clone https://github.com/your-org/backend.git
|
||||
git clone https://github.com/your-org/api.git
|
||||
```
|
||||
|
||||
### Platform-Specific Instructions
|
||||
|
||||
**For Linux (Native Docker):**
|
||||
|
||||
You may need to run commands with `sudo` depending on your Docker setup. If you encounter permission issues with output files, ensure your user has access to the Docker socket.
|
||||
|
||||
**For macOS:**
|
||||
|
||||
Works out of the box with Docker Desktop installed.
|
||||
|
||||
**Testing Local Applications:**
|
||||
|
||||
Docker containers cannot reach `localhost` on your host machine. Use `host.docker.internal` in place of `localhost`:
|
||||
|
||||
```bash
|
||||
./shannon start URL=http://host.docker.internal:3000 REPO=/path/to/repo
|
||||
```
|
||||
|
||||
### Configuration (Optional)
|
||||
|
||||
While you can run without a config file, creating one enables authenticated testing and customized analysis.
|
||||
|
||||
#### Create Configuration File
|
||||
|
||||
Copy and modify the example configuration:
|
||||
|
||||
```bash
|
||||
cp configs/example-config.yaml configs/my-app-config.yaml
|
||||
```
|
||||
|
||||
#### Basic Configuration Structure
|
||||
|
||||
```yaml
|
||||
authentication:
|
||||
login_type: form
|
||||
login_url: "https://your-app.com/login"
|
||||
credentials:
|
||||
username: "test@example.com"
|
||||
password: "yourpassword"
|
||||
totp_secret: "LB2E2RX7XFHSTGCK" # Optional for 2FA
|
||||
|
||||
login_flow:
|
||||
- "Type $username into the email field"
|
||||
- "Type $password into the password field"
|
||||
- "Click the 'Sign In' button"
|
||||
|
||||
success_condition:
|
||||
type: url_contains
|
||||
value: "/dashboard"
|
||||
|
||||
rules:
|
||||
avoid:
|
||||
- description: "AI should avoid testing logout functionality"
|
||||
type: path
|
||||
url_path: "/logout"
|
||||
|
||||
focus:
|
||||
- description: "AI should emphasize testing API endpoints"
|
||||
type: path
|
||||
url_path: "/api"
|
||||
```
|
||||
|
||||
#### TOTP Setup for 2FA
|
||||
|
||||
If your application uses two-factor authentication, simply add the TOTP secret to your config file. The AI will automatically generate the required codes during testing.
|
||||
|
||||
### Output and Results
|
||||
|
||||
All results are saved to `./audit-logs/{hostname}_{sessionId}/` by default. Use `--output <path>` to specify a custom directory.
|
||||
|
||||
Output structure:
|
||||
```
|
||||
audit-logs/{hostname}_{sessionId}/
|
||||
├── session.json # Metrics and session data
|
||||
├── agents/ # Per-agent execution logs
|
||||
├── prompts/ # Prompt snapshots for reproducibility
|
||||
└── deliverables/
|
||||
└── comprehensive_security_assessment_report.md # Final comprehensive security report
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Sample Reports
|
||||
|
||||
> **Looking for quantitative benchmarks?** [See full benchmark methodology and results →](./xben-benchmark-results/README.md)
|
||||
|
||||
See Shannon's capabilities in action with penetration test results from industry-standard vulnerable applications:
|
||||
|
||||
#### 🧃 **OWASP Juice Shop** • [GitHub](https://github.com/juice-shop/juice-shop)
|
||||
|
||||
*A notoriously insecure web application maintained by OWASP, designed to test a tool's ability to uncover a wide range of modern vulnerabilities.*
|
||||
|
||||
**Performance**: Identified **over 20 high-impact vulnerabilities** across targeted OWASP categories in a single automated run.
|
||||
|
||||
**Key Accomplishments**:
|
||||
|
||||
- **Achieved complete authentication bypass** and exfiltrated the entire user database via Injection attack
|
||||
- **Executed a full privilege escalation** by creating a new administrator account through a registration workflow bypass
|
||||
- **Identified and exploited systemic authorization flaws (IDOR)** to access and modify any user's private data and shopping cart
|
||||
- **Discovered a Server-Side Request Forgery (SSRF)** vulnerability, enabling internal network reconnaissance
|
||||
|
||||
📄 **[View Complete Report →](sample-reports/shannon-report-juice-shop.md)**
|
||||
|
||||
---
|
||||
|
||||
#### 🔗 **c{api}tal API** • [GitHub](https://github.com/Checkmarx/capital)
|
||||
|
||||
*An intentionally vulnerable API from Checkmarx, designed to test a tool's ability to uncover the OWASP API Security Top 10.*
|
||||
|
||||
**Performance**: Identified **nearly 15 critical and high-severity vulnerabilities**, leading to full application compromise.
|
||||
|
||||
**Key Accomplishments**:
|
||||
|
||||
- **Executed a root-level Injection attack** by bypassing a denylist via command chaining in a hidden debug endpoint
|
||||
- **Achieved complete authentication bypass** by discovering and targeting a legacy, unpatched v1 API endpoint
|
||||
- **Escalated a regular user to full administrator privileges** by exploiting a Mass Assignment vulnerability in the user profile update function
|
||||
- **Demonstrated high accuracy** by correctly confirming the application's robust XSS defenses, reporting zero false positives
|
||||
|
||||
📄 **[View Complete Report →](sample-reports/shannon-report-capital-api.md)**
|
||||
|
||||
---
|
||||
|
||||
#### 🚗 **OWASP crAPI** • [GitHub](https://github.com/OWASP/crAPI)
|
||||
|
||||
*A modern, intentionally vulnerable API from OWASP, designed to benchmark a tool's effectiveness against the OWASP API Security Top 10.*
|
||||
|
||||
**Performance**: Identified **over 15 critical and high-severity vulnerabilities**, achieving full application compromise.
|
||||
|
||||
**Key Accomplishments**:
|
||||
|
||||
- **Bypassed authentication using multiple advanced JWT attacks**, including Algorithm Confusion, alg:none, and weak key (kid) injection
|
||||
- **Achieved full database compromise via Injection attacks**, exfiltrating user credentials from the PostgreSQL database
|
||||
- **Executed a critical Server-Side Request Forgery (SSRF) attack** that successfully forwarded internal authentication tokens to an external service
|
||||
- **Demonstrated high accuracy** by correctly identifying the application's robust XSS defenses, reporting zero false positives
|
||||
|
||||
📄 **[View Complete Report →](sample-reports/shannon-report-crapi.md)**
|
||||
|
||||
---
|
||||
|
||||
*These results demonstrate Shannon's ability to move beyond simple scanning, performing deep contextual exploitation with minimal false positives and actionable proof-of-concepts.*
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
Shannon emulates a human penetration tester's methodology using a sophisticated multi-agent architecture. It combines white-box source code analysis with black-box dynamic exploitation across four distinct phases:
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Reconnaissance │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┴───────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Vuln Analysis │ │ Vuln Analysis │ │ ... │
|
||||
│ (Injection) │ │ (XSS) │ │ │
|
||||
└─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Exploitation │ │ Exploitation │ │ ... │
|
||||
│ (Injection) │ │ (XSS) │ │ │
|
||||
└─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘
|
||||
│ │ │
|
||||
└─────────┬─────────┴───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Reporting │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Architectural Overview
|
||||
|
||||
Shannon is engineered to emulate the methodology of a human penetration tester. It leverages Anthropic's Claude Agent SDK as its core reasoning engine, but its true strength lies in the sophisticated multi-agent architecture built around it. This architecture combines the deep context of **white-box source code analysis** with the real-world validation of **black-box dynamic exploitation**, managed by an orchestrator through four distinct phases to ensure a focus on minimal false positives and intelligent context management.
|
||||
|
||||
---
|
||||
|
||||
#### **Phase 1: Reconnaissance**
|
||||
|
||||
The first phase builds a comprehensive map of the application's attack surface. Shannon analyzes the source code and integrates with tools like Nmap and Subfinder to understand the tech stack and infrastructure. Simultaneously, it performs live application exploration via browser automation to correlate code-level insights with real-world behavior, producing a detailed map of all entry points, API endpoints, and authentication mechanisms for the next phase.
|
||||
|
||||
#### **Phase 2: Vulnerability Analysis**
|
||||
|
||||
To maximize efficiency, this phase operates in parallel. Using the reconnaissance data, specialized agents for each OWASP category hunt for potential flaws in parallel. For vulnerabilities like Injection and SSRF, agents perform a structured data flow analysis, tracing user input to dangerous sinks. This phase produces a key deliverable: a list of **hypothesized exploitable paths** that are passed on for validation.
|
||||
|
||||
#### **Phase 3: Exploitation**
|
||||
|
||||
Continuing the parallel workflow to maintain speed, this phase is dedicated entirely to turning hypotheses into proof. Dedicated exploit agents receive the hypothesized paths and attempt to execute real-world attacks using browser automation, command-line tools, and custom scripts. This phase enforces a strict **"No Exploit, No Report"** policy: if a hypothesis cannot be successfully exploited to demonstrate impact, it is discarded as a false positive.
|
||||
|
||||
#### **Phase 4: Reporting**
|
||||
|
||||
The final phase compiles all validated findings into a professional, actionable report. An agent consolidates the reconnaissance data and the successful exploit evidence, cleaning up any noise or hallucinated artifacts. Only verified vulnerabilities are included, complete with **reproducible, copy-and-paste Proof-of-Concepts**, delivering a final pentest-grade report focused exclusively on proven risks.
|
||||
|
||||
|
||||
## 📋 Coverage and Roadmap
|
||||
|
||||
For detailed information about Shannon's security testing coverage and development roadmap, see our [Coverage and Roadmap](./COVERAGE.md) documentation.
|
||||
|
||||
## ⚠️ Disclaimers
|
||||
|
||||
### Important Usage Guidelines & Disclaimers
|
||||
|
||||
Please review the following guidelines carefully before using Shannon (Lite). As a user, you are responsible for your actions and assume all liability.
|
||||
|
||||
#### **1. Potential for Mutative Effects & Environment Selection**
|
||||
|
||||
This is not a passive scanner. The exploitation agents are designed to **actively execute attacks** to confirm vulnerabilities. This process can have mutative effects on the target application and its data.
|
||||
### Run Shannon Lite
|
||||
|
||||
> [!WARNING]
|
||||
> **⚠️ DO NOT run Shannon on production environments.**
|
||||
>
|
||||
> - It is intended exclusively for use on sandboxed, staging, or local development environments where data integrity is not a concern.
|
||||
> - Potential mutative effects include, but are not limited to: creating new users, modifying or deleting data, compromising test accounts, and triggering unintended side effects from injection attacks.
|
||||
|
||||
#### **2. Legal & Ethical Use**
|
||||
|
||||
Shannon is designed for legitimate security auditing purposes only.
|
||||
|
||||
> [!CAUTION]
|
||||
> **You must have explicit, written authorization** from the owner of the target system before running Shannon.
|
||||
>
|
||||
> Unauthorized scanning and exploitation of systems you do not own is illegal and can be prosecuted under laws such as the Computer Fraud and Abuse Act (CFAA). Keygraph is not responsible for any misuse of Shannon.
|
||||
|
||||
#### **3. LLM & Automation Caveats**
|
||||
|
||||
- **Verification is Required**: While significant engineering has gone into our "proof-by-exploitation" methodology to eliminate false positives, the underlying LLMs can still generate hallucinated or weakly-supported content in the final report. **Human oversight is essential** to validate the legitimacy and severity of all reported findings.
|
||||
- **Comprehensiveness**: The analysis in Shannon Lite may not be exhaustive due to the inherent limitations of LLM context windows. For a more comprehensive, graph-based analysis of your entire codebase, **Shannon Pro** leverages its advanced data flow analysis engine to ensure deeper and more thorough coverage.
|
||||
|
||||
#### **4. Scope of Analysis**
|
||||
|
||||
- **Targeted Vulnerabilities**: The current version of Shannon Lite specifically targets the following classes of *exploitable* vulnerabilities:
|
||||
- Broken Authentication & Authorization
|
||||
- Injection
|
||||
- Cross-Site Scripting (XSS)
|
||||
- Server-Side Request Forgery (SSRF)
|
||||
- **What Shannon Lite Does Not Cover**: This list is not exhaustive of all potential security risks. Shannon Lite's "proof-by-exploitation" model means it will not report on issues it cannot actively exploit, such as vulnerable third-party libraries or insecure configurations. These types of deep static-analysis findings are a core focus of the advanced analysis engine in **Shannon Pro**.
|
||||
|
||||
#### **5. Cost & Performance**
|
||||
|
||||
- **Time**: As of the current version, a full test run typically takes **1 to 1.5 hours** to complete.
|
||||
- **Cost**: Running the full test using Anthropic's Claude 4.5 Sonnet model may incur costs of approximately **$50 USD**. Please note that costs are subject to change based on model pricing and the complexity of the target application.
|
||||
|
||||
#### **6. Windows Antivirus False Positives**
|
||||
|
||||
Windows Defender may flag files in `xben-benchmark-results/` or `deliverables/` as malware. These are false positives caused by exploit code in the reports. Add an exclusion for the Shannon directory in Windows Defender, or use Docker/WSL2.
|
||||
|
||||
|
||||
## 📊 Telemetry
|
||||
|
||||
Shannon collects anonymous usage telemetry to help improve the tool.
|
||||
|
||||
### What We Collect
|
||||
|
||||
- Workflow and agent lifecycle events (start, complete, fail)
|
||||
- Timing and cost metrics (duration, API costs)
|
||||
- Error types (NOT error messages or stack traces)
|
||||
|
||||
### What We DO NOT Collect
|
||||
|
||||
- Target URLs, repository paths, or configuration
|
||||
- Vulnerability findings or security reports
|
||||
- Error messages, stack traces, or debugging info
|
||||
- Any personally identifiable information (PII)
|
||||
|
||||
### Opting Out
|
||||
|
||||
Telemetry is enabled by default. To disable it, set one of:
|
||||
> Shannon Lite actively executes exploits. Run it only against applications and environments you own or have explicit written authorization to test. Do not run Shannon Lite against production systems.
|
||||
|
||||
```bash
|
||||
# Standard opt-out
|
||||
export DO_NOT_TRACK=1
|
||||
# Configure credentials with the interactive wizard.
|
||||
npx @keygraph/shannon setup
|
||||
|
||||
# Shannon-specific opt-out
|
||||
export SHANNON_TELEMETRY=off
|
||||
# Run a pentest against a source-available target.
|
||||
npx @keygraph/shannon start -u https://your-app.com -r /path/to/your-repo
|
||||
```
|
||||
|
||||
Or add `DO_NOT_TRACK=1` to your `.env` file.
|
||||
Shannon Lite pulls the worker image from Docker Hub, starts the required local infrastructure, mounts the target repository read-only inside an ephemeral worker container, and writes results to a local workspace.
|
||||
|
||||
For source builds, authenticated scans, provider-specific setup, and platform notes, see [Documentation](#documentation).
|
||||
|
||||
## 📜 License
|
||||
## Key Capabilities
|
||||
|
||||
Shannon Lite is released under the [GNU Affero General Public License v3.0 (AGPL-3.0)](LICENSE).
|
||||
- **Proof-by-exploitation reports**: Shannon Lite reports validated findings with reproducible proof-of-concept steps instead of speculative warnings.
|
||||
- **White-box attack planning**: Shannon Lite uses source-code analysis to guide dynamic testing and focus on realistic attack paths.
|
||||
- **Autonomous execution**: Shannon Lite launches reconnaissance, vulnerability analysis, exploitation, and report generation from a single command.
|
||||
- **Authenticated testing**: Shannon Lite configuration files can describe login flows, test credentials, TOTP, email-based login flows, focus areas, and rules of engagement.
|
||||
- **OWASP-focused coverage**: Shannon Lite targets exploitable Injection, XSS, SSRF, Broken Authentication, and Broken Authorization issues.
|
||||
- **Resumable workspaces**: Shannon Lite can resume interrupted runs without re-running completed agents.
|
||||
|
||||
Shannon is open source (AGPL v3). This license allows you to:
|
||||
- Use it freely for all internal security testing.
|
||||
- Modify the code privately for internal use without sharing your changes.
|
||||
## Shannon Lite and Shannon Pro
|
||||
|
||||
The AGPL's sharing requirements primarily apply to organizations offering Shannon as a public or managed service (such as a SaaS platform). In those specific cases, any modifications made to the core software must be open-sourced.
|
||||
This repository contains **Shannon Lite**, the AGPL-3.0 open-source CLI for strictly white-box, proof-by-exploitation testing of web applications and APIs you own or are authorized to test. Shannon Lite requires access to the target application's source code and repository layout.
|
||||
|
||||
**Shannon Pro** is Keygraph's commercial continuous pentesting and AppSec platform for teams running security across many repositories, services, and environments. While Shannon Lite is a local white-box pentesting CLI, Shannon Pro is a full platform: it combines parsed-code SAST, source-to-sink analysis, black-box and white-box agentic pentesting, verified remediation, CI/CD gating, SLA tracking, and reporting for security and compliance teams.
|
||||
|
||||
## 👥 Community & Support
|
||||
Shannon Pro supports both **white-box and black-box agentic pentesting**: use source-aware testing when code is available, or run autonomous black-box testing against deployed applications and APIs when source access is unavailable or unnecessary.
|
||||
|
||||
### Community Resources
|
||||
Shannon Pro covers the full vulnerability lifecycle: finding exploitable issues, deduplicating and prioritizing them, syncing work into developer workflows, generating verified remediations, re-testing fixes, tracking SLAs, and producing dashboards for security reporting and compliance.
|
||||
|
||||
**Contributing:** At this time, we’re not accepting external code contributions (PRs).
|
||||
Issues are welcome for bug reports and feature requests.
|
||||
For enterprise deployments, Shannon Pro supports self-hosted and air-gapped environments, strict bring-your-own-key model access, and customer-controlled LLM gateway patterns. Deployments can be designed so source code, scan results, prompts, completions, and model traffic remain inside your security perimeter.
|
||||
|
||||
- 🐛 **Report bugs** via [GitHub Issues](https://github.com/KeygraphHQ/shannon/issues)
|
||||
- 💡 **Suggest features** in [Discussions](https://github.com/KeygraphHQ/shannon/discussions)
|
||||
- 💬 **Join our [Discord](https://discord.gg/KAqzSHHpRt)** for real-time community support
|
||||
Shannon Lite is a strong fit for local and project-level white-box testing. Shannon Pro is intended for organizations that need continuous AppSec coverage, black-box and white-box pentesting, centralized triage, verified remediation workflows, compliance-ready reporting, enterprise integrations, and commercial support.
|
||||
|
||||
### Stay Connected
|
||||
| Need | Shannon Lite | Shannon Pro |
|
||||
| --- | --- | --- |
|
||||
| License | AGPL-3.0 | Commercial |
|
||||
| White-box pentesting | Yes; source code required | Yes; source-aware testing with platform workflows |
|
||||
| Black-box pentesting | No | Yes; autonomous testing without source-code access |
|
||||
| Code analysis / SAST | Prompting and source pass-through to guide pentesting | Actual code parsing, Code Property Graph analysis, source-to-sink path analysis, and agentic SAST |
|
||||
| AppSec coverage | OWASP-focused agentic pentesting | Agentic pentesting, SAST, SCA, secrets, IaC, containers, and business logic testing |
|
||||
| CI/CD and gating | Manual/local CLI runs | Headless commercial CLI for CI/CD gating across enterprise CI/CD platforms |
|
||||
| Finding lifecycle | Local Markdown reports | Canonical findings, deduplication, ownership, status, SLA tracking, workflow sync, and reporting dashboards |
|
||||
| Remediation | Manual | User-initiated remediation with verification before delivery |
|
||||
| Fix verification | None; manual reruns only | Targeted verification without rerunning the entire scan, completing the remediation lifecycle |
|
||||
| Enterprise deployment | Local CLI and Docker worker | Self-hosted, air-gapped, BYOK, and customer-controlled LLM gateway options |
|
||||
| Support | Community | Commercial support |
|
||||
|
||||
- 🐦 **Twitter**: [@KeygraphHQ](https://twitter.com/KeygraphHQ)
|
||||
- 💼 **LinkedIn**: [Keygraph](https://linkedin.com/company/keygraph)
|
||||
- 🌐 **Website**: [keygraph.io](https://keygraph.io)
|
||||
Learn more on the [Keygraph website](https://keygraph.io), read the [Shannon Pro technical overview](docs/shannon-pro.md), start a free trial or book a [Shannon Pro demo](https://cal.com/team/keygraph/shannon-pro), or contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||
|
||||
## Architecture
|
||||
|
||||
Shannon Lite uses a multi-agent workflow that combines source-code analysis with live exploitation:
|
||||
|
||||
## 💬 Get in Touch
|
||||
```text
|
||||
┌──────────────────────┐
|
||||
│ Pre-Reconnaissance │
|
||||
│ (source code scan) │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Reconnaissance │
|
||||
│ (attack surface │
|
||||
│ mapping) │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┴───────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ Vuln │ │ Vuln │ │ ... │
|
||||
│(Injection)│ │ (XSS) │ │ │
|
||||
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ Exploit │ │ Exploit │ │ ... │
|
||||
│(Injection)│ │ (XSS) │ │ │
|
||||
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
|
||||
│ │ │
|
||||
└──────┬───────┴─────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Reporting │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### Interested in Shannon Pro?
|
||||
At a high level:
|
||||
|
||||
Shannon Pro is designed for organizations serious about application security. It offers enterprise-grade features, dedicated support, and seamless CI/CD integration, all powered by our most advanced LLM-based analysis engine. Find and fix complex vulnerabilities deep in your codebase before they ever reach production.
|
||||
- **Pre-reconnaissance** identifies frameworks, entry points, data flows, and likely attack surfaces from the repository.
|
||||
- **Reconnaissance** explores the live application and correlates runtime behavior with code-level context.
|
||||
- **Vulnerability analysis** runs specialized agents for Injection, XSS, SSRF, Authentication, and Authorization.
|
||||
- **Exploitation** attempts real proof-of-concept attacks and discards hypotheses that cannot be proven.
|
||||
- **Reporting** compiles validated findings, evidence, and remediation guidance into a final Markdown report.
|
||||
|
||||
For a detailed breakdown of features, technical differences, and enterprise use cases, see our [complete comparison guide](./SHANNON-PRO.md).
|
||||
Each scan runs in an ephemeral Docker container with an isolated workspace and per-invocation orchestration.
|
||||
|
||||
## Documentation
|
||||
|
||||
Use these guides for operational detail:
|
||||
|
||||
| Guide | Use it for |
|
||||
| --- | --- |
|
||||
| [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, report filters, and rate-limit settings. |
|
||||
| [AI providers](docs/ai-providers.md) | Anthropic, AWS Bedrock, Google Vertex AI, and custom Anthropic-compatible endpoints. |
|
||||
| [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. |
|
||||
| [Coverage and roadmap](docs/coverage-roadmap.md) | Current vulnerability coverage and planned work. |
|
||||
| [Shannon Pro](docs/shannon-pro.md) | Commercial platform, black-box and white-box pentesting, full lifecycle workflows, and enterprise deployment. |
|
||||
|
||||
## Safety, Scope, and Limitations
|
||||
|
||||
Shannon Lite is not a passive scanner. Its exploitation agents can create users, submit forms, mutate application state, trigger outbound requests, and otherwise affect the target system. Use sandboxed, staging, or local development environments with disposable data.
|
||||
|
||||
You are responsible for using Shannon Lite legally and ethically. Do not point Shannon Lite at systems, repositories, or applications you do not own or do not have explicit authorization to test.
|
||||
|
||||
Important limitations:
|
||||
|
||||
- Shannon Lite focuses on actively exploitable issues such as Injection, XSS, SSRF, Broken Authentication, and Broken Authorization. Broader static-analysis findings, including vulnerable dependencies and insecure configurations, are a core focus of Shannon Pro.
|
||||
- Findings still require human review. LLM-generated reports can contain weakly supported or incorrect details.
|
||||
- Shannon Lite is officially supported with Claude models. Smaller, alternative, or proxied non-Claude models may be incomplete or unstable.
|
||||
- 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.
|
||||
|
||||
Read the full [Safety and limitations](docs/safety.md) guide before running Shannon Lite in a new environment.
|
||||
|
||||
## License and Enterprise Licensing
|
||||
|
||||
Shannon Lite is licensed under the [GNU Affero General Public License v3.0](LICENSE).
|
||||
|
||||
Commercial and enterprise licensing is available for organizations that need different license terms, commercial support, private redistribution, managed-service use, or broader deployment options.
|
||||
|
||||
For commercial licensing, contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
|
||||
|
||||
## Community and Support
|
||||
|
||||
**Community office hours** are available for hands-on help with bugs, deployments, and configuration questions.
|
||||
|
||||
- US/EU: Thursday, 10:00 AM PT
|
||||
- Asia: Thursday, 2:00 PM IST
|
||||
- [Book a slot](https://cal.com/george-flores-keygraph/shannon-community-office-hours)
|
||||
|
||||
[Join Discord](https://discord.gg/cmctpMBXwE) to ask questions, share feedback, and connect with other Shannon Lite users.
|
||||
|
||||
At this time, Keygraph is not accepting external code contributions. Issues are welcome for bug reports and feature requests:
|
||||
|
||||
- [Report bugs](https://github.com/KeygraphHQ/shannon/issues)
|
||||
- [Suggest features](https://github.com/KeygraphHQ/shannon/discussions)
|
||||
|
||||
Stay connected:
|
||||
|
||||
- [Keygraph website](https://keygraph.io)
|
||||
- [Twitter/X: @KeygraphHQ](https://twitter.com/KeygraphHQ)
|
||||
- [LinkedIn: Keygraph](https://linkedin.com/company/keygraph)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-cPZcWjlfBJ3TCT8AaWpf8ztsw3FaHzJE4urr55KdlQs6cQ/viewform?usp=header" target="_blank">
|
||||
<img src="https://img.shields.io/badge/📋%20Express%20Interest%20in%20Shannon%20Pro-4285F4?style=for-the-badge&logo=google&logoColor=white" alt="Express Interest">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**Or contact us directly:**
|
||||
|
||||
📧 **Email**: [shannon@keygraph.io](mailto:shannon@keygraph.io)
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<b>Built with ❤️ by the Keygraph team</b><br>
|
||||
<i>Making application security accessible to everyone</i>
|
||||
<b>Built by <a href="https://keygraph.io">Keygraph</a></b>
|
||||
</p>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# Shannon Pro vs Shannon Lite
|
||||
|
||||
## Technical Differences
|
||||
|
||||
**Shannon Pro** is built on advanced, LLM-powered data flow analysis inspired by the ideas of the [LLM-driven Data-Flow Analysis paper](https://arxiv.org/abs/2402.10754). It traces data flows to identify complex, exploitable vulnerabilities with high precision. It's cloud-based with native CI/CD integration (GitHub Actions, GitLab CI, Jenkins) and supports self-hosted deployment.
|
||||
|
||||
### Feature Comparison
|
||||
|
||||
| Feature | Shannon Lite<br>(AGPL-3.0) | Shannon Pro<br>(Commercial) |
|
||||
|---------|:-------------------------:|:---------------------------:|
|
||||
| **Core Scanning** |
|
||||
| Source-Sink Analysis | Basic | LLM-powered data flow analysis for high-precision, source-to-sink vulnerability detection |
|
||||
| CVSS Scoring | ❌ | ✅ |
|
||||
| Remediation Guidance | Basic | Code-level fixes |
|
||||
| **Integration** |
|
||||
| CI/CD Pipeline Support | ❌ | ✅ |
|
||||
| API Access | ❌ | ✅ |
|
||||
| Jira/Linear/ServiceNow/Slack | ❌ | ✅ |
|
||||
| **Deployment** |
|
||||
| Hosting | Self-hosted | Cloud or Self-hosted |
|
||||
| **Enterprise** |
|
||||
| Multi-user & RBAC | ❌ | ✅ |
|
||||
| SSO/SAML | ❌ | ✅ |
|
||||
| Audit Logs | ❌ | ✅ |
|
||||
| Compliance Reporting | ❌ | ✅ (OWASP, PCI-DSS, SOC2) |
|
||||
| **Support** |
|
||||
| Support | Community | Dedicated + SLA |
|
||||
| **Cost** | Free + API costs | Contact Us |
|
||||
|
||||
## Which to Choose?
|
||||
|
||||
**Shannon Lite**: Individual researchers, small teams, or testing personal projects
|
||||
**Shannon Pro**: Designed for organizations that want to "shift-left" and integrate security directly into their development lifecycle. Its _advanced LLM-powered data flow analysis engine_ is ideal for catching deep-seated vulnerabilities before they ever reach production, complemented by full CI/CD integration and enterprise support.
|
||||
|
||||
## Interested in Shannon Pro?
|
||||
|
||||
Shannon Pro offers enterprise-grade features, dedicated support, and seamless CI/CD integration for organizations serious about application security.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-cPZcWjlfBJ3TCT8AaWpf8ztsw3FaHzJE4urr55KdlQs6cQ/viewform?usp=header" target="_blank">
|
||||
<img src="https://img.shields.io/badge/📋%20Express%20Interest%20in%20Shannon%20Pro-4285F4?style=for-the-badge&logo=google&logoColor=white" alt="Express Interest">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**Or contact us directly:**
|
||||
|
||||
📧 **Email**: [shannon@keygraph.io](mailto:shannon@keygraph.io)
|
||||
@@ -0,0 +1,3 @@
|
||||
src/
|
||||
tsconfig.json
|
||||
node_modules/
|
||||
@@ -0,0 +1,22 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/github-banner.png" alt="Shannon — AI Pentester for Web Applications and APIs" width="100%">
|
||||
|
||||
# Shannon — AI Pentester by Keygraph
|
||||
|
||||
Shannon is an autonomous, white-box AI pentester for web applications and APIs. <br />
|
||||
It analyzes your source code, identifies attack vectors, and executes real exploits to prove vulnerabilities before they reach production.
|
||||
|
||||
---
|
||||
|
||||
<a href="https://github.com/KeygraphHQ/shannon/discussions/categories/announcements"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/announcements.png" height="40" alt="Announcements"></a>
|
||||
<a href="https://discord.gg/9ZqQPuhJB7"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/discord.png" height="40" alt="Join Discord"></a>
|
||||
<a href="https://keygraph.io/"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/Keygraph_Button.png" height="40" alt="Visit Keygraph.io"></a>
|
||||
<a href="https://www.linkedin.com/company/keygraph/"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/linkedin.png" height="40" alt="Follow Us on Linkedin"></a>
|
||||
|
||||
---
|
||||
|
||||
**Full README and usage guide**
|
||||
[https://github.com/KeygraphHQ/shannon#readme](https://github.com/KeygraphHQ/shannon#readme)
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
networks:
|
||||
default:
|
||||
name: shannon-net
|
||||
|
||||
services:
|
||||
temporal:
|
||||
image: temporalio/temporal:1.7.0
|
||||
container_name: shannon-temporal
|
||||
command: ["server", "start-dev", "--db-filename", "/home/temporal/temporal.db", "--ip", "0.0.0.0"]
|
||||
ports:
|
||||
- "127.0.0.1:7233:7233"
|
||||
- "127.0.0.1:8233:8233"
|
||||
volumes:
|
||||
- temporal-data:/home/temporal
|
||||
healthcheck:
|
||||
test: ["CMD", "temporal", "operator", "cluster", "health", "--address", "localhost:7233"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
temporal-data:
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@keygraph/shannon",
|
||||
"version": "0.0.0",
|
||||
"description": "Shannon - Autonomous white-box AI pentester for web applications and APIs by Keygraph",
|
||||
"type": "module",
|
||||
"main": "dist/index.mjs",
|
||||
"bin": {
|
||||
"shannon": "dist/index.mjs"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"infra"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.1.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"smol-toml": "^1.6.1"
|
||||
},
|
||||
"keywords": [
|
||||
"security",
|
||||
"pentest",
|
||||
"penetration-testing",
|
||||
"vulnerability-assessment",
|
||||
"ai",
|
||||
"white-box",
|
||||
"owasp",
|
||||
"exploitation",
|
||||
"appsec",
|
||||
"keygraph"
|
||||
],
|
||||
"author": "",
|
||||
"license": "AGPL-3.0-only",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/KeygraphHQ/shannon.git",
|
||||
"directory": "apps/cli"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "^0.21.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* `shannon build` command — build the worker Docker image locally.
|
||||
* Only available in local mode (running from cloned repository).
|
||||
*/
|
||||
|
||||
import { buildImage } from '../docker.js';
|
||||
import { isLocal } from '../mode.js';
|
||||
|
||||
export function build(noCache: boolean): void {
|
||||
if (!isLocal()) {
|
||||
console.error('ERROR: Build is only available when running from the Shannon repository');
|
||||
console.error(' (Dockerfile not found in current directory)');
|
||||
console.error('');
|
||||
console.error('For npx usage, run: shannon update');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
buildImage(noCache);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* `shannon logs` command — tail a workspace's workflow log.
|
||||
*
|
||||
* Uses chokidar for reliable cross-platform file watching and
|
||||
* bounded synchronous reads to prevent duplicate output.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { watch } from 'chokidar';
|
||||
import { getWorkspacesDir } from '../home.js';
|
||||
|
||||
// Match the exact line the worker writes — anchored to prevent false positives from agent output
|
||||
const COMPLETION_PATTERN = /^Workflow (COMPLETED|FAILED)$/m;
|
||||
|
||||
/** Read a byte range from a file and return it as a UTF-8 string. */
|
||||
function readRange(filePath: string, start: number, end: number): string {
|
||||
const length = end - start;
|
||||
const buffer = Buffer.alloc(length);
|
||||
const fd = fs.openSync(filePath, 'r');
|
||||
try {
|
||||
fs.readSync(fd, buffer, 0, length, start);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
return buffer.toString('utf-8');
|
||||
}
|
||||
|
||||
/** Resolve a workspace ID to its workflow.log path, or exit with an error. */
|
||||
function resolveLogFile(workspaceId: string): string {
|
||||
const workspacesDir = getWorkspacesDir();
|
||||
|
||||
// 1. Direct match
|
||||
const directPath = path.join(workspacesDir, workspaceId, 'workflow.log');
|
||||
if (fs.existsSync(directPath)) return directPath;
|
||||
|
||||
// 2. Resume workflow ID (e.g. workspace_resume_123)
|
||||
const resumeBase = workspaceId.replace(/_resume_\d+$/, '');
|
||||
if (resumeBase !== workspaceId) {
|
||||
const resumePath = path.join(workspacesDir, resumeBase, 'workflow.log');
|
||||
if (fs.existsSync(resumePath)) return resumePath;
|
||||
}
|
||||
|
||||
// 3. Named workspace ID (e.g. workspace_shannon-123)
|
||||
const namedBase = workspaceId.replace(/_shannon-\d+$/, '');
|
||||
if (namedBase !== workspaceId) {
|
||||
const namedPath = path.join(workspacesDir, namedBase, 'workflow.log');
|
||||
if (fs.existsSync(namedPath)) return namedPath;
|
||||
}
|
||||
|
||||
console.error(`ERROR: Workflow log not found for: ${workspaceId}`);
|
||||
console.error('');
|
||||
console.error('Possible causes:');
|
||||
console.error(" - Workflow hasn't started yet");
|
||||
console.error(' - Workspace ID is incorrect');
|
||||
console.error('');
|
||||
console.error('Check the Temporal Web UI at http://localhost:8233 for workflow details');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export function logs(workspaceId: string): void {
|
||||
const logFile = resolveLogFile(workspaceId);
|
||||
let position = 0;
|
||||
|
||||
/**
|
||||
* Output any new content appended since the last read.
|
||||
* Returns true when the workflow completion marker is detected.
|
||||
*/
|
||||
function flush(): boolean {
|
||||
try {
|
||||
const { size } = fs.statSync(logFile);
|
||||
if (size <= position) return false;
|
||||
|
||||
const data = readRange(logFile, position, size);
|
||||
process.stdout.write(data);
|
||||
position = size;
|
||||
|
||||
return COMPLETION_PATTERN.test(data);
|
||||
} catch {
|
||||
// File deleted or unreadable — treat as done
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Tailing workflow log: ${logFile}`);
|
||||
|
||||
// 1. Output existing content
|
||||
if (flush()) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 2. Watch for appended content via chokidar
|
||||
const watcher = watch(logFile, { persistent: true });
|
||||
|
||||
const shutdown = (): void => {
|
||||
watcher.close().finally(() => process.exit(0));
|
||||
// Safety net — force exit if watcher.close() stalls
|
||||
setTimeout(() => process.exit(0), 1000).unref();
|
||||
};
|
||||
|
||||
watcher.on('change', () => {
|
||||
if (flush()) shutdown();
|
||||
});
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* `shn setup` — interactive TUI wizard for one-time credential configuration.
|
||||
*
|
||||
* Walks the user through selecting a provider and entering credentials,
|
||||
* then persists everything to ~/.shannon/config.toml with 0o600 permissions.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { type ShannonConfig, saveConfig } from '../config/writer.js';
|
||||
|
||||
const SHANNON_HOME = path.join(os.homedir(), '.shannon');
|
||||
|
||||
type Provider = 'anthropic' | 'custom_base_url' | 'bedrock' | 'vertex';
|
||||
|
||||
export async function setup(): Promise<void> {
|
||||
p.intro('Shannon Setup');
|
||||
|
||||
// 1. Select provider
|
||||
const provider = await p.select({
|
||||
message: 'Select your AI provider',
|
||||
options: [
|
||||
{ value: 'anthropic' as const, label: 'Claude Direct', hint: 'recommended' },
|
||||
{ value: 'custom_base_url' as const, label: 'Custom Base URL', hint: 'proxies, gateways' },
|
||||
{ value: 'bedrock' as const, label: 'Claude via AWS Bedrock' },
|
||||
{ value: 'vertex' as const, label: 'Claude via Google Vertex AI' },
|
||||
],
|
||||
});
|
||||
if (p.isCancel(provider)) return cancelAndExit();
|
||||
|
||||
const config = await setupProvider(provider as Provider);
|
||||
|
||||
// 2. Adaptive thinking
|
||||
await maybePromptAdaptiveThinking(config);
|
||||
|
||||
// 3. Save config
|
||||
saveConfig(config);
|
||||
|
||||
const configPath = path.join(SHANNON_HOME, 'config.toml');
|
||||
p.log.success(`Configuration saved to ${configPath}`);
|
||||
p.outro('Run `npx @keygraph/shannon start` to begin a scan.');
|
||||
}
|
||||
|
||||
async function setupProvider(provider: Provider): Promise<ShannonConfig> {
|
||||
switch (provider) {
|
||||
case 'anthropic':
|
||||
return setupAnthropic();
|
||||
case 'custom_base_url':
|
||||
return setupCustomBaseUrl();
|
||||
case 'bedrock':
|
||||
return setupBedrock();
|
||||
case 'vertex':
|
||||
return setupVertex();
|
||||
}
|
||||
}
|
||||
|
||||
// === Provider Setup Flows ===
|
||||
|
||||
async function setupAnthropic(): Promise<ShannonConfig> {
|
||||
const authMethod = await p.select({
|
||||
message: 'Authentication method',
|
||||
options: [
|
||||
{ value: 'api_key' as const, label: 'API Key' },
|
||||
{ value: 'oauth' as const, label: 'OAuth Token' },
|
||||
],
|
||||
});
|
||||
if (p.isCancel(authMethod)) return cancelAndExit();
|
||||
|
||||
const config: ShannonConfig = {};
|
||||
|
||||
if (authMethod === 'oauth') {
|
||||
const token = await promptSecret('Enter your OAuth token');
|
||||
config.anthropic = { oauth_token: token };
|
||||
} else {
|
||||
const apiKey = await promptSecret('Enter your Anthropic API key');
|
||||
config.anthropic = { api_key: apiKey };
|
||||
}
|
||||
|
||||
const customizeModels = await p.confirm({
|
||||
message:
|
||||
'Do you want to change the default models?\n' +
|
||||
' Small - claude-haiku-4-5-20251001\n' +
|
||||
' Medium - claude-sonnet-4-6\n' +
|
||||
' Large - claude-opus-4-8',
|
||||
initialValue: false,
|
||||
});
|
||||
if (p.isCancel(customizeModels)) return cancelAndExit();
|
||||
|
||||
if (customizeModels) {
|
||||
const small = await p.text({
|
||||
message: 'Small model ID',
|
||||
initialValue: 'claude-haiku-4-5-20251001',
|
||||
validate: required('Small model ID is required'),
|
||||
});
|
||||
if (p.isCancel(small)) return cancelAndExit();
|
||||
|
||||
const medium = await p.text({
|
||||
message: 'Medium model ID',
|
||||
initialValue: 'claude-sonnet-4-6',
|
||||
validate: required('Medium model ID is required'),
|
||||
});
|
||||
if (p.isCancel(medium)) return cancelAndExit();
|
||||
|
||||
const large = await p.text({
|
||||
message: 'Large model ID',
|
||||
initialValue: 'claude-opus-4-8',
|
||||
validate: required('Large model ID is required'),
|
||||
});
|
||||
if (p.isCancel(large)) return cancelAndExit();
|
||||
|
||||
config.models = { small, medium, large };
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function setupCustomBaseUrl(): Promise<ShannonConfig> {
|
||||
const baseUrl = await p.text({
|
||||
message: 'Endpoint URL',
|
||||
placeholder: 'https://your-proxy.example.com',
|
||||
validate: (value) => {
|
||||
if (!value) return 'Endpoint URL is required';
|
||||
try {
|
||||
new URL(value);
|
||||
} catch {
|
||||
return 'Must be a valid URL';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
if (p.isCancel(baseUrl)) return cancelAndExit();
|
||||
|
||||
const authToken = await promptSecret('Enter the auth token for the custom endpoint');
|
||||
|
||||
const config: ShannonConfig = {
|
||||
custom_base_url: { base_url: baseUrl, auth_token: authToken },
|
||||
};
|
||||
|
||||
const customizeModels = await p.confirm({
|
||||
message:
|
||||
'Do you want to change the default models?\n' +
|
||||
' Small - claude-haiku-4-5-20251001\n' +
|
||||
' Medium - claude-sonnet-4-6\n' +
|
||||
' Large - claude-opus-4-8',
|
||||
initialValue: false,
|
||||
});
|
||||
if (p.isCancel(customizeModels)) return cancelAndExit();
|
||||
|
||||
if (customizeModels) {
|
||||
const small = await p.text({
|
||||
message: 'Small model ID',
|
||||
initialValue: 'claude-haiku-4-5-20251001',
|
||||
validate: required('Small model ID is required'),
|
||||
});
|
||||
if (p.isCancel(small)) return cancelAndExit();
|
||||
|
||||
const medium = await p.text({
|
||||
message: 'Medium model ID',
|
||||
initialValue: 'claude-sonnet-4-6',
|
||||
validate: required('Medium model ID is required'),
|
||||
});
|
||||
if (p.isCancel(medium)) return cancelAndExit();
|
||||
|
||||
const large = await p.text({
|
||||
message: 'Large model ID',
|
||||
initialValue: 'claude-opus-4-8',
|
||||
validate: required('Large model ID is required'),
|
||||
});
|
||||
if (p.isCancel(large)) return cancelAndExit();
|
||||
|
||||
config.models = { small, medium, large };
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function setupBedrock(): Promise<ShannonConfig> {
|
||||
const region = await p.text({
|
||||
message: 'AWS Region',
|
||||
placeholder: 'us-east-1',
|
||||
validate: required('AWS Region is required'),
|
||||
});
|
||||
if (p.isCancel(region)) return cancelAndExit();
|
||||
|
||||
const token = await promptSecret('Enter your AWS Bearer Token');
|
||||
|
||||
const small = await p.text({
|
||||
message: 'Small model ID',
|
||||
placeholder: 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
|
||||
validate: required('Small model ID is required'),
|
||||
});
|
||||
if (p.isCancel(small)) return cancelAndExit();
|
||||
|
||||
const medium = await p.text({
|
||||
message: 'Medium model ID',
|
||||
placeholder: 'us.anthropic.claude-sonnet-4-6',
|
||||
validate: required('Medium model ID is required'),
|
||||
});
|
||||
if (p.isCancel(medium)) return cancelAndExit();
|
||||
|
||||
const large = await p.text({
|
||||
message: 'Large model ID',
|
||||
placeholder: 'us.anthropic.claude-opus-4-8',
|
||||
validate: required('Large model ID is required'),
|
||||
});
|
||||
if (p.isCancel(large)) return cancelAndExit();
|
||||
|
||||
return {
|
||||
bedrock: { use: true, region, token },
|
||||
models: { small, medium, large },
|
||||
};
|
||||
}
|
||||
|
||||
async function setupVertex(): Promise<ShannonConfig> {
|
||||
// 1. Collect region and project ID
|
||||
const region = await p.text({
|
||||
message: 'Google Cloud region',
|
||||
placeholder: 'us-east5',
|
||||
validate: required('Region is required'),
|
||||
});
|
||||
if (p.isCancel(region)) return cancelAndExit();
|
||||
|
||||
const projectId = await p.text({
|
||||
message: 'GCP Project ID',
|
||||
validate: required('Project ID is required'),
|
||||
});
|
||||
if (p.isCancel(projectId)) return cancelAndExit();
|
||||
|
||||
// 2. File picker for service account key
|
||||
p.log.info('Select the path to your GCP Service Account JSON key file.');
|
||||
const keySourcePath = await p.path({
|
||||
message: 'Service Account JSON key file',
|
||||
validate: (value) => {
|
||||
if (!value) return 'Path is required';
|
||||
if (!fs.existsSync(value)) return 'File not found';
|
||||
if (!value.endsWith('.json')) return 'Must be a .json file';
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
if (p.isCancel(keySourcePath)) return cancelAndExit();
|
||||
|
||||
// 3. Copy key to ~/.shannon/ and lock permissions
|
||||
const destPath = path.join(SHANNON_HOME, 'google-sa-key.json');
|
||||
fs.mkdirSync(SHANNON_HOME, { recursive: true });
|
||||
fs.copyFileSync(keySourcePath, destPath);
|
||||
fs.chmodSync(destPath, 0o600);
|
||||
p.log.success(`Key copied to ${destPath} (permissions: 0600)`);
|
||||
|
||||
// 4. Model tiers
|
||||
const models = await p.group({
|
||||
small: () =>
|
||||
p.text({
|
||||
message: 'Small model ID',
|
||||
placeholder: 'claude-haiku-4-5@20251001',
|
||||
validate: required('Small model ID is required'),
|
||||
}),
|
||||
medium: () =>
|
||||
p.text({
|
||||
message: 'Medium model ID',
|
||||
placeholder: 'claude-sonnet-4-6',
|
||||
validate: required('Medium model ID is required'),
|
||||
}),
|
||||
large: () =>
|
||||
p.text({
|
||||
message: 'Large model ID',
|
||||
placeholder: 'claude-opus-4-8',
|
||||
validate: required('Large model ID is required'),
|
||||
}),
|
||||
});
|
||||
if (p.isCancel(models)) return cancelAndExit();
|
||||
|
||||
return {
|
||||
vertex: {
|
||||
use: true,
|
||||
region,
|
||||
project_id: projectId,
|
||||
key_path: destPath,
|
||||
},
|
||||
models: { small: models.small, medium: models.medium, large: models.large },
|
||||
};
|
||||
}
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
async function maybePromptAdaptiveThinking(config: ShannonConfig): Promise<void> {
|
||||
const m = config.models;
|
||||
const hasAdaptiveModel = !m || [m.small, m.medium, m.large].some((v) => v && /opus-4-[678]/.test(v));
|
||||
if (!hasAdaptiveModel) return;
|
||||
|
||||
const enable = await p.confirm({
|
||||
message: 'Enable adaptive thinking on Opus 4.6/4.7/4.8? Claude decides when and how deeply to reason.',
|
||||
initialValue: true,
|
||||
});
|
||||
if (p.isCancel(enable)) return cancelAndExit();
|
||||
|
||||
config.core = { ...config.core, adaptive_thinking: enable };
|
||||
}
|
||||
|
||||
async function promptSecret(message: string): Promise<string> {
|
||||
const value = await p.password({
|
||||
message,
|
||||
validate: required(`${message.replace(/^Enter /, '')} is required`),
|
||||
});
|
||||
if (p.isCancel(value)) return cancelAndExit();
|
||||
return value;
|
||||
}
|
||||
|
||||
function required(errorMessage: string): (value: string | undefined) => string | undefined {
|
||||
return (value) => {
|
||||
if (!value) return errorMessage;
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
function cancelAndExit(): never {
|
||||
p.cancel('Setup cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* `shannon start` command — launch a pentest scan.
|
||||
*
|
||||
* Handles both local mode (local build, ./workspaces/, mounted prompts)
|
||||
* and npx mode (Docker Hub pull, ~/.shannon/).
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { ensureImage, ensureInfra, randomSuffix, spawnWorker } from '../docker.js';
|
||||
import { buildEnvFlags, loadEnv, validateCredentials } from '../env.js';
|
||||
import { getCredentialsPath, getWorkspacesDir, initHome } from '../home.js';
|
||||
import { isLocal } from '../mode.js';
|
||||
import { resolveConfig, resolveRepo } from '../paths.js';
|
||||
import { displaySplash } from '../splash.js';
|
||||
|
||||
export interface StartArgs {
|
||||
url: string;
|
||||
repo: string;
|
||||
config?: string;
|
||||
workspace?: string;
|
||||
output?: string;
|
||||
pipelineTesting: boolean;
|
||||
debug: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export async function start(args: StartArgs): Promise<void> {
|
||||
// 1. Initialize state directories and load env
|
||||
initHome();
|
||||
loadEnv();
|
||||
|
||||
// 2. Validate credentials
|
||||
const creds = validateCredentials();
|
||||
if (!creds.valid) {
|
||||
console.error(`ERROR: ${creds.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 3. Resolve paths
|
||||
const repo = resolveRepo(args.repo);
|
||||
const config = args.config ? resolveConfig(args.config) : undefined;
|
||||
|
||||
// 4. Ensure workspaces dir is writable by container user (UID 1001)
|
||||
const workspacesDir = getWorkspacesDir();
|
||||
fs.mkdirSync(workspacesDir, { recursive: true });
|
||||
fs.chmodSync(workspacesDir, 0o777);
|
||||
|
||||
// 5. Ensure image (auto-build in dev, pull in npx) and start infra
|
||||
ensureImage(args.version);
|
||||
await ensureInfra();
|
||||
|
||||
// 6. Generate unique task queue and container name
|
||||
const suffix = randomSuffix();
|
||||
const taskQueue = `shannon-${suffix}`;
|
||||
const containerName = `shannon-worker-${suffix}`;
|
||||
|
||||
// 7. Generate workspace name if not provided
|
||||
const workspace =
|
||||
args.workspace ?? `${new URL(args.url).hostname.replace(/[^a-zA-Z0-9-]/g, '-')}_shannon-${Date.now()}`;
|
||||
|
||||
// 8. Create writable overlay directories (mounted over :ro repo paths inside container)
|
||||
// Workspace dir must be 0o777 so the container user (UID 1001) can create audit subdirs
|
||||
const workspacePath = path.join(workspacesDir, workspace);
|
||||
fs.mkdirSync(workspacePath, { recursive: true });
|
||||
fs.chmodSync(workspacePath, 0o777);
|
||||
for (const dir of ['deliverables', 'scratchpad', '.playwright-cli', '.playwright']) {
|
||||
const dirPath = path.join(workspacePath, dir);
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
fs.chmodSync(dirPath, 0o777);
|
||||
}
|
||||
|
||||
// 9. Pre-create overlay mount points (:ro mounts can't auto-create them)
|
||||
const shannonDir = path.join(repo.hostPath, '.shannon');
|
||||
for (const dir of ['deliverables', 'scratchpad', '.playwright-cli']) {
|
||||
fs.mkdirSync(path.join(shannonDir, dir), { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(path.join(repo.hostPath, '.playwright'), { recursive: true });
|
||||
|
||||
const credentialsPath = getCredentialsPath();
|
||||
const hasCredentials = fs.existsSync(credentialsPath);
|
||||
|
||||
if (hasCredentials) {
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS = '/app/credentials/google-sa-key.json';
|
||||
}
|
||||
|
||||
// 10. Resolve output directory
|
||||
const outputDir = args.output ? path.resolve(args.output) : undefined;
|
||||
if (outputDir) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 11. Resolve prompts directory (local mode only)
|
||||
const promptsDir = isLocal() ? path.resolve('apps/worker/prompts') : undefined;
|
||||
|
||||
// 12. Display splash screen
|
||||
displaySplash(isLocal() ? undefined : args.version);
|
||||
|
||||
// 13. Spawn worker container
|
||||
const proc = spawnWorker({
|
||||
version: args.version,
|
||||
url: args.url,
|
||||
repo,
|
||||
workspacesDir,
|
||||
taskQueue,
|
||||
containerName,
|
||||
envFlags: buildEnvFlags(),
|
||||
...(config && { config }),
|
||||
...(hasCredentials && { credentials: credentialsPath }),
|
||||
...(promptsDir && { promptsDir }),
|
||||
...(outputDir && { outputDir }),
|
||||
workspace,
|
||||
...(args.pipelineTesting && { pipelineTesting: true }),
|
||||
...(args.debug && { debug: true }),
|
||||
});
|
||||
|
||||
// 14. Bail if `docker run -d` itself fails (mount error, image missing, etc.)
|
||||
const dockerExitCode = await new Promise<number>((resolve) => {
|
||||
proc.once('exit', (code) => resolve(code ?? 1));
|
||||
proc.once('error', (err) => {
|
||||
console.error(`Failed to start worker: ${err.message}`);
|
||||
resolve(1);
|
||||
});
|
||||
});
|
||||
|
||||
if (dockerExitCode !== 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Detect whether this is a fresh workspace or a resume by checking session.json existence
|
||||
const sessionJson = path.join(workspacesDir, workspace, 'session.json');
|
||||
const isResume = fs.existsSync(sessionJson);
|
||||
let initialResumeCount = 0;
|
||||
if (isResume) {
|
||||
try {
|
||||
const session = JSON.parse(fs.readFileSync(sessionJson, 'utf-8'));
|
||||
initialResumeCount = session.session?.resumeAttempts?.length ?? 0;
|
||||
} catch {
|
||||
// Corrupted file — worker will handle validation
|
||||
}
|
||||
}
|
||||
|
||||
// Poll for workflow to register in session.json
|
||||
process.stdout.write('Waiting for workflow to start...');
|
||||
let workflowId = '';
|
||||
let started = false;
|
||||
let attempts = 0;
|
||||
const pollInterval = setInterval(() => {
|
||||
attempts++;
|
||||
if (attempts > 60) {
|
||||
clearInterval(pollInterval);
|
||||
process.stdout.write('\n');
|
||||
console.error('Timeout waiting for workflow to start');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const session = JSON.parse(fs.readFileSync(sessionJson, 'utf-8'));
|
||||
const resumeAttempts: { workflowId: string }[] = session.session?.resumeAttempts ?? [];
|
||||
|
||||
// Fresh: session.json appears with originalWorkflowId. Resume: new resumeAttempts entry.
|
||||
const ready = isResume ? resumeAttempts.length > initialResumeCount : !!session.session?.originalWorkflowId;
|
||||
|
||||
if (ready) {
|
||||
clearInterval(pollInterval);
|
||||
started = true;
|
||||
|
||||
// Latest workflow ID: last resume attempt, or originalWorkflowId for fresh scans
|
||||
workflowId = resumeAttempts.at(-1)?.workflowId ?? session.session?.originalWorkflowId ?? '';
|
||||
|
||||
// Clear waiting line and show info
|
||||
process.stdout.write('\r\x1b[K');
|
||||
printInfo(args, workspace, workflowId, repo.hostPath, workspacesDir);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist yet
|
||||
}
|
||||
process.stdout.write('.');
|
||||
}, 2000);
|
||||
|
||||
// Stop the worker container only if it hasn't started yet
|
||||
let cleaned = false;
|
||||
const cleanup = (): void => {
|
||||
if (cleaned || started) return;
|
||||
cleaned = true;
|
||||
clearInterval(pollInterval);
|
||||
console.log(`\nStopping worker ${containerName}...`);
|
||||
try {
|
||||
execFileSync('docker', ['stop', containerName], { stdio: 'pipe' });
|
||||
} catch {
|
||||
// Container may have already exited
|
||||
}
|
||||
if (args.debug) {
|
||||
printDebugHint(containerName);
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('exit', cleanup);
|
||||
}
|
||||
|
||||
function printDebugHint(containerName: string): void {
|
||||
console.log('');
|
||||
console.log(` Worker container preserved: ${containerName}`);
|
||||
console.log(` Inspect logs: docker logs ${containerName}`);
|
||||
console.log(` Remove: docker rm ${containerName}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
function printInfo(
|
||||
args: StartArgs,
|
||||
workspace: string,
|
||||
workflowId: string,
|
||||
repoPath: string,
|
||||
workspacesDir: string,
|
||||
): void {
|
||||
const logsCmd = isLocal() ? `./shannon logs ${workspace}` : `npx @keygraph/shannon logs ${workspace}`;
|
||||
const reportsPath = path.join(workspacesDir, workspace);
|
||||
|
||||
console.log(` Target: ${args.url}`);
|
||||
console.log(` Repository: ${repoPath}`);
|
||||
console.log(` Workspace: ${workspace}`);
|
||||
if (args.config) {
|
||||
console.log(` Config: ${path.resolve(args.config)}`);
|
||||
}
|
||||
if (args.pipelineTesting) {
|
||||
console.log(' Mode: Pipeline Testing');
|
||||
}
|
||||
|
||||
// Surface Fable usage: its safety classifiers route cybersecurity tasks to
|
||||
// Opus 4.8, so those phases run on Opus 4.8 regardless of the tier setting.
|
||||
const fableTiers = (
|
||||
[
|
||||
['small', process.env.ANTHROPIC_SMALL_MODEL],
|
||||
['medium', process.env.ANTHROPIC_MEDIUM_MODEL],
|
||||
['large', process.env.ANTHROPIC_LARGE_MODEL],
|
||||
] as const
|
||||
).filter(([, model]) => model && /fable/i.test(model));
|
||||
if (fableTiers.length > 0) {
|
||||
const tierList = fableTiers.map(([tier, model]) => `${tier} (${model})`).join(', ');
|
||||
console.log(` Note: ${tierList} set to a Fable model. Fable's safety classifiers`);
|
||||
console.log(' route cybersecurity tasks to Opus 4.8, so those phases run on Opus 4.8.');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(' Monitor:');
|
||||
if (workflowId) {
|
||||
console.log(` Web UI: http://localhost:8233/namespaces/default/workflows/${workflowId}`);
|
||||
} else {
|
||||
console.log(' Web UI: http://localhost:8233');
|
||||
}
|
||||
console.log(` Logs: ${logsCmd}`);
|
||||
console.log('');
|
||||
console.log(' Output:');
|
||||
console.log(` Reports: ${reportsPath}/`);
|
||||
console.log('');
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* `shannon status` command — show running workers and Temporal health.
|
||||
*/
|
||||
|
||||
import { isTemporalReady, listRunningWorkers } from '../docker.js';
|
||||
|
||||
export function status(): void {
|
||||
// 1. Temporal health
|
||||
const temporalUp = isTemporalReady();
|
||||
console.log(`Temporal: ${temporalUp ? 'running' : 'not running'}`);
|
||||
if (temporalUp) {
|
||||
console.log(' Web UI: http://localhost:8233');
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// 2. Running workers
|
||||
const workers = listRunningWorkers();
|
||||
if (workers) {
|
||||
console.log('Workers:');
|
||||
console.log(workers);
|
||||
} else {
|
||||
console.log('Workers: none running');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* `shannon stop` command — stop workers and infrastructure.
|
||||
*/
|
||||
|
||||
import * as p from '@clack/prompts';
|
||||
import { stopInfra, stopWorkers } from '../docker.js';
|
||||
|
||||
export async function stop(clean: boolean): Promise<void> {
|
||||
if (clean) {
|
||||
const confirmed = await p.confirm({
|
||||
message: 'This will stop all running scans and remove the Temporal data. Continue?',
|
||||
});
|
||||
if (p.isCancel(confirmed) || !confirmed) {
|
||||
p.cancel('Aborted.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
stopWorkers();
|
||||
stopInfra(clean);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* `shn uninstall` command — remove ~/.shannon/ after confirmation (npx only).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import * as p from '@clack/prompts';
|
||||
import { stopInfra, stopWorkers } from '../docker.js';
|
||||
|
||||
const SHANNON_HOME = path.join(os.homedir(), '.shannon');
|
||||
|
||||
export async function uninstall(): Promise<void> {
|
||||
p.intro('Shannon Uninstall');
|
||||
|
||||
if (!fs.existsSync(SHANNON_HOME)) {
|
||||
p.log.info('Nothing to remove. Shannon is not configured on this machine.');
|
||||
p.outro('Done.');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await p.confirm({
|
||||
message: 'This will permanently remove all past scan data, saved configurations, and API keys. Continue?',
|
||||
});
|
||||
if (p.isCancel(confirmed) || !confirmed) {
|
||||
p.cancel('Aborted.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Stop any running containers first
|
||||
stopWorkers();
|
||||
stopInfra(false);
|
||||
|
||||
fs.rmSync(SHANNON_HOME, { recursive: true, force: true });
|
||||
p.log.success('All Shannon data has been removed.');
|
||||
p.outro('Shannon has been uninstalled. Run `npx @keygraph/shannon setup` to start fresh.');
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* `shannon workspaces` command — list all workspaces.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
import { getWorkerImage } from '../docker.js';
|
||||
import { getWorkspacesDir } from '../home.js';
|
||||
|
||||
export function workspaces(version: string): void {
|
||||
const workspacesDir = getWorkspacesDir();
|
||||
const image = getWorkerImage(version);
|
||||
|
||||
try {
|
||||
execFileSync(
|
||||
'docker',
|
||||
[
|
||||
'run',
|
||||
'--rm',
|
||||
'-v',
|
||||
`${workspacesDir}:/app/workspaces`,
|
||||
'-e',
|
||||
'WORKSPACES_DIR=/app/workspaces',
|
||||
image,
|
||||
'node',
|
||||
'apps/worker/dist/temporal/workspaces.js',
|
||||
],
|
||||
{ stdio: 'inherit', ...(os.platform() === 'win32' && { env: { ...process.env, MSYS_NO_PATHCONV: '1' } }) },
|
||||
);
|
||||
} catch {
|
||||
console.error('ERROR: Failed to list workspaces. Is the Docker image available?');
|
||||
console.error(` Run: docker pull ${image}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Configuration resolver with environment-first, TOML-fallback precedence.
|
||||
*
|
||||
* Priority: process.env > ~/.shannon/config.toml
|
||||
* Env var names match .env.example exactly; TOML uses nested sections.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { parse as parseTOML } from 'smol-toml';
|
||||
import { getConfigFile } from '../home.js';
|
||||
import { getMode } from '../mode.js';
|
||||
|
||||
// === TOML ↔ Env Mapping ===
|
||||
|
||||
type TOMLType = 'string' | 'number' | 'boolean';
|
||||
|
||||
interface ConfigMapping {
|
||||
readonly env: string;
|
||||
readonly toml: string;
|
||||
readonly type: TOMLType;
|
||||
readonly boolFormat?: 'numeric' | 'literal';
|
||||
}
|
||||
|
||||
/** Maps every supported env var to its TOML path (section.key) and expected type. */
|
||||
const CONFIG_MAP: readonly ConfigMapping[] = [
|
||||
// Core
|
||||
{ env: 'CLAUDE_CODE_MAX_OUTPUT_TOKENS', toml: 'core.max_tokens', type: 'number' },
|
||||
{ env: 'CLAUDE_ADAPTIVE_THINKING', toml: 'core.adaptive_thinking', type: 'boolean', boolFormat: 'literal' },
|
||||
|
||||
// Anthropic
|
||||
{ env: 'ANTHROPIC_API_KEY', toml: 'anthropic.api_key', type: 'string' },
|
||||
{ env: 'CLAUDE_CODE_OAUTH_TOKEN', toml: 'anthropic.oauth_token', type: 'string' },
|
||||
|
||||
// Bedrock
|
||||
{ env: 'CLAUDE_CODE_USE_BEDROCK', toml: 'bedrock.use', type: 'boolean' },
|
||||
{ env: 'AWS_REGION', toml: 'bedrock.region', type: 'string' },
|
||||
{ env: 'AWS_BEARER_TOKEN_BEDROCK', toml: 'bedrock.token', type: 'string' },
|
||||
|
||||
// Vertex
|
||||
{ env: 'CLAUDE_CODE_USE_VERTEX', toml: 'vertex.use', type: 'boolean' },
|
||||
{ env: 'CLOUD_ML_REGION', toml: 'vertex.region', type: 'string' },
|
||||
{ env: 'ANTHROPIC_VERTEX_PROJECT_ID', toml: 'vertex.project_id', type: 'string' },
|
||||
{ env: 'GOOGLE_APPLICATION_CREDENTIALS', toml: 'vertex.key_path', type: 'string' },
|
||||
|
||||
// Custom Base URL
|
||||
{ env: 'ANTHROPIC_BASE_URL', toml: 'custom_base_url.base_url', type: 'string' },
|
||||
{ env: 'ANTHROPIC_AUTH_TOKEN', toml: 'custom_base_url.auth_token', type: 'string' },
|
||||
|
||||
// Model tiers
|
||||
{ env: 'ANTHROPIC_SMALL_MODEL', toml: 'models.small', type: 'string' },
|
||||
{ env: 'ANTHROPIC_MEDIUM_MODEL', toml: 'models.medium', type: 'string' },
|
||||
{ env: 'ANTHROPIC_LARGE_MODEL', toml: 'models.large', type: 'string' },
|
||||
] as const;
|
||||
|
||||
// === TOML Parsing ===
|
||||
|
||||
type TOMLValue = string | number | boolean;
|
||||
type TOMLSection = Record<string, TOMLValue>;
|
||||
type TOMLConfig = Record<string, TOMLSection>;
|
||||
|
||||
/** Read a nested TOML value for a given mapping. */
|
||||
function getTomlValue(config: TOMLConfig, mapping: ConfigMapping): string | undefined {
|
||||
const [section, key] = mapping.toml.split('.');
|
||||
if (!section || !key) return undefined;
|
||||
|
||||
const sectionObj = config[section];
|
||||
if (!sectionObj || typeof sectionObj !== 'object') return undefined;
|
||||
|
||||
const value = sectionObj[key];
|
||||
if (value === undefined || value === null) return undefined;
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
if (mapping.boolFormat === 'literal') return value ? 'true' : 'false';
|
||||
return value ? '1' : '0';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/** Parse the global TOML config file, returning null if it doesn't exist. */
|
||||
function loadTOML(): TOMLConfig | null {
|
||||
const configPath = getConfigFile();
|
||||
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');
|
||||
console.error(`\nInsecure permissions (${actual}) on ${configPath}. Run: chmod 600 ${configPath}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
return parseTOML(content) as TOMLConfig;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error(`\nFailed to parse ${configPath}: ${message}`);
|
||||
console.error(`\nRun 'npx @keygraph/shannon setup' to reconfigure.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// === Validation ===
|
||||
|
||||
/** Build a lookup of allowed keys per section from CONFIG_MAP. */
|
||||
function buildSchema(): Map<string, Map<string, TOMLType>> {
|
||||
const schema = new Map<string, Map<string, TOMLType>>();
|
||||
for (const mapping of CONFIG_MAP) {
|
||||
const [section, key] = mapping.toml.split('.');
|
||||
if (!section || !key) continue;
|
||||
|
||||
let keys = schema.get(section);
|
||||
if (!keys) {
|
||||
keys = new Map();
|
||||
schema.set(section, keys);
|
||||
}
|
||||
keys.set(key, mapping.type);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
/** Check that a provider section has all required fields and dependencies. */
|
||||
function validateProviderFields(config: TOMLConfig, provider: string, errors: string[]): void {
|
||||
const section = config[provider] as Record<string, unknown> | undefined;
|
||||
if (!section) return;
|
||||
const keys = Object.keys(section);
|
||||
|
||||
switch (provider) {
|
||||
case 'anthropic':
|
||||
if (!keys.includes('api_key') && !keys.includes('oauth_token')) {
|
||||
errors.push('[anthropic] requires either api_key or oauth_token');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'custom_base_url': {
|
||||
const required = ['base_url', 'auth_token'];
|
||||
const missing = required.filter((k) => !keys.includes(k));
|
||||
if (missing.length > 0) {
|
||||
errors.push(`[custom_base_url] missing required keys: ${missing.join(', ')}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'bedrock': {
|
||||
const required = ['use', 'region', 'token'];
|
||||
const missing = required.filter((k) => !keys.includes(k));
|
||||
if (missing.length > 0) {
|
||||
errors.push(`[bedrock] missing required keys: ${missing.join(', ')}`);
|
||||
}
|
||||
validateModelTiers(config, 'bedrock', errors);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'vertex': {
|
||||
const required = ['use', 'region', 'project_id', 'key_path'];
|
||||
const missing = required.filter((k) => !keys.includes(k));
|
||||
if (missing.length > 0) {
|
||||
errors.push(`[vertex] missing required keys: ${missing.join(', ')}`);
|
||||
}
|
||||
validateModelTiers(config, 'vertex', errors);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Bedrock and Vertex require a [models] section with all three tiers. */
|
||||
function validateModelTiers(config: TOMLConfig, provider: string, errors: string[]): void {
|
||||
const models = config.models as Record<string, unknown> | undefined;
|
||||
if (!models || typeof models !== 'object') {
|
||||
errors.push(`[${provider}] requires a [models] section with small, medium, and large`);
|
||||
return;
|
||||
}
|
||||
|
||||
const required = ['small', 'medium', 'large'];
|
||||
const missing = required.filter((k) => !Object.keys(models).includes(k));
|
||||
if (missing.length > 0) {
|
||||
errors.push(`[models] missing required keys for ${provider}: ${missing.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a parsed TOML config against the known schema.
|
||||
* Returns an array of human-readable error messages (empty = valid).
|
||||
*/
|
||||
function validateConfig(config: TOMLConfig): string[] {
|
||||
const schema = buildSchema();
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const [section, sectionObj] of Object.entries(config)) {
|
||||
// 1. Reject unknown sections
|
||||
const allowedKeys = schema.get(section);
|
||||
if (!allowedKeys) {
|
||||
const known = [...schema.keys()].join(', ');
|
||||
errors.push(`Unknown section [${section}]. Valid sections: ${known}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Section value must be a table
|
||||
if (!sectionObj || typeof sectionObj !== 'object') {
|
||||
errors.push(`[${section}] must be a table, got ${typeof sectionObj}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Validate each key in the section
|
||||
for (const [key, value] of Object.entries(sectionObj as Record<string, unknown>)) {
|
||||
const expectedType = allowedKeys.get(key);
|
||||
if (!expectedType) {
|
||||
const known = [...allowedKeys.keys()].join(', ');
|
||||
errors.push(`Unknown key "${key}" in [${section}]. Valid keys: ${known}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value !== expectedType) {
|
||||
errors.push(`[${section}].${key} must be ${expectedType}, got ${typeof value}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reject empty strings — they pass type checks but are never useful
|
||||
if (typeof value === 'string' && value.trim() === '') {
|
||||
errors.push(`[${section}].${key} must not be empty`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Only one provider section allowed (ignore empty sections)
|
||||
const PROVIDER_SECTIONS = ['anthropic', 'custom_base_url', 'bedrock', 'vertex'] as const;
|
||||
const present = PROVIDER_SECTIONS.filter((s) => {
|
||||
const section = config[s];
|
||||
return section && typeof section === 'object' && Object.keys(section).length > 0;
|
||||
});
|
||||
if (present.length > 1) {
|
||||
errors.push(
|
||||
`Multiple providers configured: [${present.join('], [')}]. Only one provider section is allowed at a time`,
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Required fields per provider
|
||||
const singleProvider = present.length === 1 ? present[0] : undefined;
|
||||
if (singleProvider) {
|
||||
validateProviderFields(config, singleProvider, errors);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// === Public API ===
|
||||
|
||||
/**
|
||||
* Resolve all config values into process.env (npx mode only).
|
||||
*
|
||||
* For each mapped variable: if not already set in the environment,
|
||||
* look it up in ~/.shannon/config.toml and inject it into process.env.
|
||||
* Local mode uses .env exclusively — TOML is skipped.
|
||||
* Exits with an error if the TOML contains unknown or invalid keys.
|
||||
*/
|
||||
export function resolveConfig(): void {
|
||||
if (getMode() === 'local') return;
|
||||
|
||||
const toml = loadTOML();
|
||||
if (!toml) return;
|
||||
|
||||
// Validate before injecting
|
||||
const errors = validateConfig(toml);
|
||||
if (errors.length > 0) {
|
||||
console.error('\nInvalid configuration:');
|
||||
for (const err of errors) {
|
||||
console.error(` - ${err}`);
|
||||
}
|
||||
console.error(`\nRun 'shn setup' to reconfigure.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const mapping of CONFIG_MAP) {
|
||||
if (process.env[mapping.env]) continue;
|
||||
|
||||
const value = getTomlValue(toml, mapping);
|
||||
if (value) {
|
||||
process.env[mapping.env] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/** TOML config writer for ~/.shannon/config.toml. */
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { stringify } from 'smol-toml';
|
||||
import { getConfigFile } from '../home.js';
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface ShannonConfig {
|
||||
core?: { max_tokens?: number; adaptive_thinking?: boolean };
|
||||
anthropic?: { api_key?: string; oauth_token?: string };
|
||||
custom_base_url?: { base_url?: string; auth_token?: string };
|
||||
bedrock?: { use?: boolean; region?: string; token?: string };
|
||||
vertex?: { use?: boolean; region?: string; project_id?: string; key_path?: string };
|
||||
models?: { small?: string; medium?: string; large?: string };
|
||||
}
|
||||
|
||||
// === File Operations ===
|
||||
|
||||
/** Write the config to ~/.shannon/config.toml with 0o600 permissions. */
|
||||
export function saveConfig(config: ShannonConfig): void {
|
||||
const configPath = getConfigFile();
|
||||
const dir = path.dirname(configPath);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const content = stringify(config);
|
||||
fs.writeFileSync(configPath, content, { mode: 0o600 });
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* Docker orchestration — compose lifecycle, network, image pull/build, worker spawning.
|
||||
*
|
||||
* Local mode: builds locally, uses docker-compose.yml from repo root, mounts prompts.
|
||||
* NPX mode: pulls from Docker Hub, uses bundled compose.yml.
|
||||
*/
|
||||
|
||||
import { type ChildProcess, execFileSync, spawn } from 'node:child_process';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { getMode } from './mode.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const NPX_IMAGE_REPO = 'keygraph/shannon';
|
||||
const DEV_IMAGE = 'shannon-worker';
|
||||
|
||||
export function getWorkerImage(version: string): string {
|
||||
return getMode() === 'local' ? DEV_IMAGE : `${NPX_IMAGE_REPO}:${version}`;
|
||||
}
|
||||
|
||||
function getComposeFile(): string {
|
||||
return getMode() === 'local'
|
||||
? path.resolve('docker-compose.yml')
|
||||
: path.resolve(__dirname, '..', 'infra', 'compose.yml');
|
||||
}
|
||||
|
||||
/** Generate an 8-char random hex suffix for container/queue names. */
|
||||
export function randomSuffix(): string {
|
||||
return crypto.randomBytes(4).toString('hex');
|
||||
}
|
||||
|
||||
/** Run a command silently, return true if it succeeds. */
|
||||
function runQuiet(cmd: string, args: string[]): boolean {
|
||||
try {
|
||||
execFileSync(cmd, args, { stdio: 'pipe' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Run a command and return stdout, or empty string on failure. */
|
||||
function runOutput(cmd: string, args: string[]): string {
|
||||
try {
|
||||
return execFileSync(cmd, args, { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Temporal is running and healthy.
|
||||
*/
|
||||
export function isTemporalReady(): boolean {
|
||||
const output = runOutput('docker', [
|
||||
'exec',
|
||||
'shannon-temporal',
|
||||
'temporal',
|
||||
'operator',
|
||||
'cluster',
|
||||
'health',
|
||||
'--address',
|
||||
'localhost:7233',
|
||||
]);
|
||||
return output.includes('SERVING');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure Temporal is running via compose.
|
||||
*/
|
||||
export async function ensureInfra(): Promise<void> {
|
||||
if (isTemporalReady()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const composeFile = getComposeFile();
|
||||
console.log('Starting Shannon infrastructure...');
|
||||
execFileSync('docker', ['compose', '-f', composeFile, 'up', '-d'], { stdio: 'inherit' });
|
||||
|
||||
console.log('Waiting for Temporal to be ready...');
|
||||
for (let i = 0; i < 30; i++) {
|
||||
if (isTemporalReady()) {
|
||||
console.log('Temporal is ready!');
|
||||
return;
|
||||
}
|
||||
await sleep(2000);
|
||||
}
|
||||
console.error('Timeout waiting for Temporal');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the worker image locally (local mode only).
|
||||
*/
|
||||
export function buildImage(noCache: boolean): void {
|
||||
console.log(`Building ${DEV_IMAGE}...`);
|
||||
const args = ['build'];
|
||||
if (noCache) args.push('--no-cache');
|
||||
args.push('-t', DEV_IMAGE, '.');
|
||||
execFileSync('docker', args, { stdio: 'inherit' });
|
||||
console.log(`Build complete: ${DEV_IMAGE}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the worker image is available.
|
||||
* Local mode: auto-builds if missing. NPX mode: pulls from Docker Hub.
|
||||
*/
|
||||
export function ensureImage(version: string): void {
|
||||
const image = getWorkerImage(version);
|
||||
const exists = runQuiet('docker', ['image', 'inspect', image]);
|
||||
if (exists) return;
|
||||
|
||||
if (getMode() === 'local') {
|
||||
console.log('Worker image not found, building...');
|
||||
buildImage(false);
|
||||
} else {
|
||||
console.log(`Pulling ${image}...`);
|
||||
try {
|
||||
execFileSync('docker', ['pull', image], { stdio: 'inherit' });
|
||||
} catch {
|
||||
console.error(`\nERROR: Failed to pull ${image}`);
|
||||
console.error('The image may not be available for your platform yet.');
|
||||
console.error('Check https://hub.docker.com/r/keygraph/shannon for available tags.');
|
||||
process.exit(1);
|
||||
}
|
||||
pruneOldImages(version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if --add-host is needed (Linux without Podman).
|
||||
* macOS has host.docker.internal built in.
|
||||
*/
|
||||
function addHostFlag(): string[] {
|
||||
if (os.platform() === 'linux') {
|
||||
const hasPodman = runQuiet('which', ['podman']);
|
||||
if (!hasPodman) {
|
||||
return ['--add-host', 'host.docker.internal:host-gateway'];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Names whose standard IPs aren't covered by `shouldSkipHostsIp`. Loopback names
|
||||
* stay because their IPs (127.x, ::1) get rewritten — not skipped. Others like
|
||||
* `broadcasthost` and `ip6-mcastprefix` are intentionally omitted: their IPs
|
||||
* (255.255.255.255, ff00::/8) are already dropped at the IP filter.
|
||||
*/
|
||||
const HOSTS_SKIP_NAMES = new Set([
|
||||
'localhost',
|
||||
'ip6-localhost',
|
||||
'ip6-loopback',
|
||||
'ip6-localnet',
|
||||
'host.docker.internal',
|
||||
'gateway.docker.internal',
|
||||
'kubernetes.docker.internal',
|
||||
]);
|
||||
|
||||
function isLoopbackIp(ip: string): boolean {
|
||||
return ip.startsWith('127.') || ip === '::1';
|
||||
}
|
||||
|
||||
function shouldSkipHostsIp(ip: string): boolean {
|
||||
if (ip === '0.0.0.0' || ip === '255.255.255.255') return true;
|
||||
// Cloud metadata range — consistent with Shannon's SSRF guard
|
||||
if (ip.startsWith('169.254.')) return true;
|
||||
const lower = ip.toLowerCase();
|
||||
if (lower.startsWith('fe80:') || lower.startsWith('ff')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldSkipHostsName(name: string, hostname: string): boolean {
|
||||
const lower = name.toLowerCase();
|
||||
if (HOSTS_SKIP_NAMES.has(lower)) return true;
|
||||
if (lower === hostname.toLowerCase()) return true;
|
||||
if (lower.endsWith('.localhost')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the host's /etc/hosts and emit --add-host flags so the worker resolves
|
||||
* user-added entries the same way. Loopback IPs (127.x, ::1) are rewritten to
|
||||
* `host-gateway` so they target the host's loopback instead of the container's.
|
||||
*/
|
||||
function forwardEtcHostsFlags(): string[] {
|
||||
if (process.env.SHANNON_FORWARD_HOSTS === 'false') return [];
|
||||
if (os.platform() === 'win32') return [];
|
||||
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync('/etc/hosts', 'utf-8');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hostname = os.hostname();
|
||||
const flags: string[] = [];
|
||||
|
||||
for (const rawLine of content.split('\n')) {
|
||||
const hashIdx = rawLine.indexOf('#');
|
||||
const line = (hashIdx >= 0 ? rawLine.slice(0, hashIdx) : rawLine).trim();
|
||||
if (!line) continue;
|
||||
|
||||
const tokens = line
|
||||
.split(' ')
|
||||
.flatMap((t) => t.split('\t'))
|
||||
.filter(Boolean);
|
||||
const ip = tokens[0];
|
||||
const names = tokens.slice(1);
|
||||
if (!ip || names.length === 0) continue;
|
||||
if (shouldSkipHostsIp(ip)) continue;
|
||||
|
||||
const targetIp = isLoopbackIp(ip) ? 'host-gateway' : ip;
|
||||
const formattedIp = targetIp.includes(':') ? `[${targetIp}]` : targetIp;
|
||||
for (const name of names) {
|
||||
if (shouldSkipHostsName(name, hostname)) continue;
|
||||
flags.push('--add-host', `${name}:${formattedIp}`);
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
export interface WorkerOptions {
|
||||
version: string;
|
||||
url: string;
|
||||
repo: { hostPath: string; containerPath: string };
|
||||
workspacesDir: string;
|
||||
taskQueue: string;
|
||||
containerName: string;
|
||||
envFlags: string[];
|
||||
config?: { hostPath: string; containerPath: string };
|
||||
credentials?: string;
|
||||
promptsDir?: string;
|
||||
outputDir?: string;
|
||||
workspace: string;
|
||||
pipelineTesting?: boolean;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn the worker container in detached mode and return the process.
|
||||
* When `opts.debug` is true, omits `--rm` so the container persists for log inspection.
|
||||
*/
|
||||
export function spawnWorker(opts: WorkerOptions): ChildProcess {
|
||||
const args = ['run', '-d'];
|
||||
if (!opts.debug) {
|
||||
args.push('--rm');
|
||||
}
|
||||
args.push('--name', opts.containerName, '--network', 'shannon-net');
|
||||
|
||||
// Add host flag for Linux
|
||||
args.push(...addHostFlag());
|
||||
|
||||
// Forward user-added /etc/hosts entries into the worker
|
||||
args.push(...forwardEtcHostsFlags());
|
||||
|
||||
// UID remapping for Linux bind mounts
|
||||
if (os.platform() === 'linux' && process.getuid && process.getgid) {
|
||||
args.push('-e', `SHANNON_HOST_UID=${process.getuid()}`, '-e', `SHANNON_HOST_GID=${process.getgid()}`);
|
||||
}
|
||||
|
||||
// Volume mounts
|
||||
args.push('-v', `${opts.workspacesDir}:/app/workspaces`);
|
||||
args.push('-v', `${opts.repo.hostPath}:${opts.repo.containerPath}:ro`);
|
||||
|
||||
// Writable overlays: shadow .shannon/ and .playwright/ inside the :ro repo with workspace-backed dirs
|
||||
const workspacePath = path.join(opts.workspacesDir, opts.workspace);
|
||||
args.push('-v', `${path.join(workspacePath, 'deliverables')}:${opts.repo.containerPath}/.shannon/deliverables`);
|
||||
args.push('-v', `${path.join(workspacePath, 'scratchpad')}:${opts.repo.containerPath}/.shannon/scratchpad`);
|
||||
args.push('-v', `${path.join(workspacePath, '.playwright-cli')}:${opts.repo.containerPath}/.shannon/.playwright-cli`);
|
||||
args.push('-v', `${path.join(workspacePath, '.playwright')}:${opts.repo.containerPath}/.playwright`);
|
||||
|
||||
// Local mode: mount prompts for live editing
|
||||
if (opts.promptsDir) {
|
||||
args.push('-v', `${opts.promptsDir}:/app/apps/worker/prompts:ro`);
|
||||
}
|
||||
|
||||
if (opts.config) {
|
||||
args.push('-v', `${opts.config.hostPath}:${opts.config.containerPath}:ro`);
|
||||
}
|
||||
|
||||
// Output directory for deliverables copy
|
||||
if (opts.outputDir) {
|
||||
args.push('-v', `${opts.outputDir}:/app/output`);
|
||||
}
|
||||
|
||||
// Mount credentials file to fixed container path
|
||||
if (opts.credentials) {
|
||||
args.push('-v', `${opts.credentials}:/app/credentials/google-sa-key.json:ro`);
|
||||
}
|
||||
|
||||
// Environment
|
||||
args.push(...opts.envFlags);
|
||||
|
||||
// Container settings
|
||||
args.push('--shm-size', '2gb', '--security-opt', 'seccomp=unconfined');
|
||||
|
||||
// Image
|
||||
args.push(getWorkerImage(opts.version));
|
||||
|
||||
// Worker command
|
||||
args.push('node', 'apps/worker/dist/temporal/worker.js', opts.url, opts.repo.containerPath);
|
||||
args.push('--task-queue', opts.taskQueue);
|
||||
if (opts.config) {
|
||||
args.push('--config', opts.config.containerPath);
|
||||
}
|
||||
if (opts.outputDir) {
|
||||
args.push('--output', '/app/output');
|
||||
}
|
||||
args.push('--workspace', opts.workspace);
|
||||
if (opts.pipelineTesting) {
|
||||
args.push('--pipeline-testing');
|
||||
}
|
||||
|
||||
// Inherit stderr so `docker run` daemon errors surface to the user;
|
||||
// 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' } }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all running shannon-worker-* containers.
|
||||
*/
|
||||
export function stopWorkers(): void {
|
||||
const workers = runOutput('docker', ['ps', '-q', '--filter', 'name=shannon-worker-']);
|
||||
if (!workers) return;
|
||||
|
||||
const ids = workers.split('\n').filter(Boolean);
|
||||
console.log('Stopping worker containers...');
|
||||
execFileSync('docker', ['stop', ...ids], { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down the compose stack.
|
||||
*/
|
||||
export function stopInfra(clean: boolean): void {
|
||||
const composeFile = getComposeFile();
|
||||
const args = ['compose', '-f', composeFile, 'down'];
|
||||
if (clean) args.push('-v');
|
||||
execFileSync('docker', args, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove old keygraph/shannon images that don't match the current version.
|
||||
*/
|
||||
function pruneOldImages(currentVersion: string): void {
|
||||
const output = runOutput('docker', ['images', NPX_IMAGE_REPO, '--format', '{{.Tag}}']);
|
||||
if (!output) return;
|
||||
|
||||
const currentTag = currentVersion;
|
||||
const stale = output.split('\n').filter((tag) => tag && tag !== currentTag);
|
||||
for (const tag of stale) {
|
||||
runQuiet('docker', ['rmi', `${NPX_IMAGE_REPO}:${tag}`]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List running worker containers.
|
||||
*/
|
||||
export function listRunningWorkers(): string {
|
||||
return runOutput('docker', [
|
||||
'ps',
|
||||
'--filter',
|
||||
'name=shannon-worker-',
|
||||
'--format',
|
||||
'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}',
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Environment variable loading and credential validation.
|
||||
*
|
||||
* Local mode: loads ./.env via dotenv.
|
||||
* NPX mode: fills gaps from ~/.shannon/config.toml (no .env).
|
||||
*/
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import { resolveConfig } from './config/resolver.js';
|
||||
import { getMode } from './mode.js';
|
||||
|
||||
/** Environment variables forwarded to worker containers. */
|
||||
const FORWARD_VARS = [
|
||||
'ANTHROPIC_API_KEY',
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'ANTHROPIC_AUTH_TOKEN',
|
||||
'CLAUDE_CODE_OAUTH_TOKEN',
|
||||
'CLAUDE_CODE_USE_BEDROCK',
|
||||
'AWS_REGION',
|
||||
'AWS_BEARER_TOKEN_BEDROCK',
|
||||
'CLAUDE_CODE_USE_VERTEX',
|
||||
'CLOUD_ML_REGION',
|
||||
'ANTHROPIC_VERTEX_PROJECT_ID',
|
||||
'GOOGLE_APPLICATION_CREDENTIALS',
|
||||
'ANTHROPIC_SMALL_MODEL',
|
||||
'ANTHROPIC_MEDIUM_MODEL',
|
||||
'ANTHROPIC_LARGE_MODEL',
|
||||
'CLAUDE_CODE_MAX_OUTPUT_TOKENS',
|
||||
'CLAUDE_ADAPTIVE_THINKING',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Load credentials into process.env.
|
||||
* Local mode: loads ./.env via dotenv.
|
||||
* NPX mode: fills gaps from ~/.shannon/config.toml.
|
||||
* Exported env vars always take precedence in both modes.
|
||||
*/
|
||||
export function loadEnv(): void {
|
||||
if (getMode() === 'local') {
|
||||
dotenv.config({ path: '.env', quiet: true });
|
||||
} else {
|
||||
resolveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build `-e KEY=VALUE` flags for docker run, only for set variables.
|
||||
*/
|
||||
export function buildEnvFlags(): string[] {
|
||||
const flags: string[] = ['-e', 'TEMPORAL_ADDRESS=shannon-temporal:7233'];
|
||||
|
||||
for (const key of FORWARD_VARS) {
|
||||
const value = process.env[key];
|
||||
if (value) {
|
||||
flags.push('-e', `${key}=${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
interface CredentialValidation {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
mode: 'api-key' | 'oauth' | 'custom-base-url' | 'bedrock' | 'vertex';
|
||||
}
|
||||
|
||||
/** Check if a custom Anthropic-compatible base URL is configured. */
|
||||
function isCustomBaseUrlConfigured(): boolean {
|
||||
return !!(process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_AUTH_TOKEN);
|
||||
}
|
||||
|
||||
/** Detect which providers are configured via environment variables. */
|
||||
function detectProviders(): string[] {
|
||||
const providers: string[] = [];
|
||||
if (process.env.ANTHROPIC_API_KEY) providers.push('Anthropic API key');
|
||||
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) providers.push('Anthropic OAuth');
|
||||
if (isCustomBaseUrlConfigured()) providers.push('Custom Base URL');
|
||||
if (process.env.CLAUDE_CODE_USE_BEDROCK === '1') providers.push('AWS Bedrock');
|
||||
if (process.env.CLAUDE_CODE_USE_VERTEX === '1') providers.push('Google Vertex');
|
||||
return providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that exactly one authentication method is configured.
|
||||
*/
|
||||
export function validateCredentials(): CredentialValidation {
|
||||
// Reject multiple providers
|
||||
const providers = detectProviders();
|
||||
if (providers.length > 1) {
|
||||
return {
|
||||
valid: false,
|
||||
mode: 'api-key',
|
||||
error: `Multiple providers detected: ${providers.join(', ')}. Only one provider can be active at a time.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.ANTHROPIC_API_KEY) {
|
||||
return { valid: true, mode: 'api-key' };
|
||||
}
|
||||
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||
return { valid: true, mode: 'oauth' };
|
||||
}
|
||||
if (isCustomBaseUrlConfigured()) {
|
||||
return { valid: true, mode: 'custom-base-url' };
|
||||
}
|
||||
if (process.env.CLAUDE_CODE_USE_BEDROCK === '1') {
|
||||
const missing: string[] = [];
|
||||
if (!process.env.AWS_REGION) missing.push('AWS_REGION');
|
||||
if (!process.env.AWS_BEARER_TOKEN_BEDROCK) missing.push('AWS_BEARER_TOKEN_BEDROCK');
|
||||
if (!process.env.ANTHROPIC_SMALL_MODEL) missing.push('ANTHROPIC_SMALL_MODEL');
|
||||
if (!process.env.ANTHROPIC_MEDIUM_MODEL) missing.push('ANTHROPIC_MEDIUM_MODEL');
|
||||
if (!process.env.ANTHROPIC_LARGE_MODEL) missing.push('ANTHROPIC_LARGE_MODEL');
|
||||
if (missing.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
mode: 'bedrock',
|
||||
error: `Bedrock mode requires: ${missing.join(', ')}`,
|
||||
};
|
||||
}
|
||||
return { valid: true, mode: 'bedrock' };
|
||||
}
|
||||
if (process.env.CLAUDE_CODE_USE_VERTEX === '1') {
|
||||
const missing: string[] = [];
|
||||
if (!process.env.CLOUD_ML_REGION) missing.push('CLOUD_ML_REGION');
|
||||
if (!process.env.ANTHROPIC_VERTEX_PROJECT_ID) missing.push('ANTHROPIC_VERTEX_PROJECT_ID');
|
||||
if (!process.env.ANTHROPIC_SMALL_MODEL) missing.push('ANTHROPIC_SMALL_MODEL');
|
||||
if (!process.env.ANTHROPIC_MEDIUM_MODEL) missing.push('ANTHROPIC_MEDIUM_MODEL');
|
||||
if (!process.env.ANTHROPIC_LARGE_MODEL) missing.push('ANTHROPIC_LARGE_MODEL');
|
||||
if (missing.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
mode: 'vertex',
|
||||
error: `Vertex AI mode requires: ${missing.join(', ')}`,
|
||||
};
|
||||
}
|
||||
if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
||||
return {
|
||||
valid: false,
|
||||
mode: 'vertex',
|
||||
error: 'Vertex AI mode requires GOOGLE_APPLICATION_CREDENTIALS',
|
||||
};
|
||||
}
|
||||
return { valid: true, mode: 'vertex' };
|
||||
}
|
||||
|
||||
const hint =
|
||||
getMode() === 'local'
|
||||
? `No credentials found. Set ANTHROPIC_API_KEY in .env or export it.`
|
||||
: `Authentication not configured. Export variables or run 'npx @keygraph/shannon setup'.`;
|
||||
return {
|
||||
valid: false,
|
||||
mode: 'api-key',
|
||||
error: hint,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Shannon state directory management.
|
||||
*
|
||||
* Local mode (cloned repo): uses ./workspaces/, ./credentials/
|
||||
* NPX mode: uses ~/.shannon/workspaces/, ~/.shannon/
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { getMode } from './mode.js';
|
||||
|
||||
const SHANNON_HOME = path.join(os.homedir(), '.shannon');
|
||||
|
||||
export function getConfigFile(): string {
|
||||
return path.join(SHANNON_HOME, 'config.toml');
|
||||
}
|
||||
|
||||
export function getWorkspacesDir(): string {
|
||||
return getMode() === 'local' ? path.resolve('workspaces') : path.join(SHANNON_HOME, 'workspaces');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Vertex credentials file path.
|
||||
*
|
||||
* Checks GOOGLE_APPLICATION_CREDENTIALS env var first (may be set by TOML resolver),
|
||||
* then falls back to mode-appropriate default location.
|
||||
*/
|
||||
export function getCredentialsPath(): string {
|
||||
const envPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||
if (envPath && fs.existsSync(envPath)) return path.resolve(envPath);
|
||||
|
||||
if (getMode() === 'local') {
|
||||
return path.resolve('credentials', 'google-sa-key.json');
|
||||
}
|
||||
|
||||
return path.join(SHANNON_HOME, 'google-sa-key.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize state directories.
|
||||
* Local mode: creates ./workspaces/ and ./credentials/
|
||||
* NPX mode: creates ~/.shannon/workspaces/
|
||||
*/
|
||||
export function initHome(): void {
|
||||
if (getMode() === 'local') {
|
||||
fs.mkdirSync(path.resolve('workspaces'), { recursive: true });
|
||||
fs.mkdirSync(path.resolve('credentials'), { recursive: true });
|
||||
} else {
|
||||
fs.mkdirSync(path.join(SHANNON_HOME, 'workspaces'), { recursive: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Shannon CLI — AI Penetration Testing Framework
|
||||
*
|
||||
* Unified CLI supporting two modes:
|
||||
* Local mode: Run from cloned repo — builds locally, mounts prompts, uses ./workspaces/
|
||||
* NPX mode: Run via npx — pulls from Docker Hub, uses ~/.shannon/
|
||||
*
|
||||
* Mode is auto-detected based on presence of Dockerfile + docker-compose.yml + prompts/
|
||||
* in the current working directory.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { build } from './commands/build.js';
|
||||
import { logs } from './commands/logs.js';
|
||||
import { setup } from './commands/setup.js';
|
||||
import { start } from './commands/start.js';
|
||||
import { status } from './commands/status.js';
|
||||
import { stop } from './commands/stop.js';
|
||||
import { uninstall } from './commands/uninstall.js';
|
||||
import { workspaces } from './commands/workspaces.js';
|
||||
import { getMode } from './mode.js';
|
||||
import { displaySplash } from './splash.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function blockSudo(): void {
|
||||
const isSudo = !!process.env.SUDO_USER;
|
||||
const isRoot = process.geteuid?.() === 0;
|
||||
if (!isSudo && !isRoot) return;
|
||||
|
||||
if (isSudo) {
|
||||
console.error('ERROR: Shannon must not be run with sudo.');
|
||||
console.error('Re-run this command as your normal user.');
|
||||
} else {
|
||||
console.error('ERROR: Shannon must not be run as the root user.');
|
||||
console.error('Switch to a regular user account and re-run this command.');
|
||||
}
|
||||
if (process.platform === 'linux') {
|
||||
console.error('Configure Docker to run without sudo first:');
|
||||
console.error('https://docs.docker.com/engine/install/linux-postinstall');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function getVersion(): string {
|
||||
try {
|
||||
const pkgPath = path.join(__dirname, '..', 'package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version?: string };
|
||||
return pkg.version || '1.0.0';
|
||||
} catch {
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
function showHelp(): void {
|
||||
const mode = getMode();
|
||||
const prefix = mode === 'local' ? './shannon' : 'npx @keygraph/shannon';
|
||||
|
||||
console.log(`
|
||||
Shannon - AI Penetration Testing Framework
|
||||
|
||||
Usage:${
|
||||
mode === 'local'
|
||||
? ''
|
||||
: `
|
||||
${prefix} setup Configure credentials`
|
||||
}
|
||||
${prefix} start --url <url> --repo <path> [options] Start a pentest scan
|
||||
${prefix} stop [--clean] Stop all containers
|
||||
${prefix} workspaces List all workspaces
|
||||
${prefix} logs <workspace> Tail workflow log
|
||||
${prefix} status Show running workers${
|
||||
mode === 'local'
|
||||
? `
|
||||
${prefix} build [--no-cache] Build worker image`
|
||||
: `
|
||||
${prefix} uninstall Remove ~/.shannon/ and all data`
|
||||
}
|
||||
${prefix} info Show splash screen
|
||||
${prefix} help Show this help
|
||||
|
||||
Options for 'start':
|
||||
-u, --url <url> Target URL (required)
|
||||
-r, --repo <path> Repository path${mode === 'local' ? ' or bare name' : ''} (required)
|
||||
-c, --config <path> Configuration file (YAML)
|
||||
-o, --output <path> Copy deliverables to this directory after run
|
||||
-w, --workspace <name> Named workspace (auto-resumes if exists)
|
||||
--pipeline-testing Use minimal prompts for fast testing
|
||||
--debug Preserve worker container after exit for log inspection
|
||||
|
||||
Examples:
|
||||
${prefix} start -u https://example.com -r ${mode === 'local' ? 'my-repo' : './my-repo'}
|
||||
${prefix} start -u https://example.com -r /path/to/repo -c config.yaml -w q1-audit
|
||||
${prefix} logs q1-audit
|
||||
${prefix} stop --clean
|
||||
${
|
||||
mode === 'local'
|
||||
? `
|
||||
State directory: ./workspaces/`
|
||||
: `
|
||||
State directory: ~/.shannon/`
|
||||
}
|
||||
Monitor workflows at http://localhost:8233
|
||||
`);
|
||||
}
|
||||
|
||||
interface ParsedStartArgs {
|
||||
url: string;
|
||||
repo: string;
|
||||
config?: string;
|
||||
workspace?: string;
|
||||
output?: string;
|
||||
pipelineTesting: boolean;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
function parseStartArgs(argv: string[]): ParsedStartArgs {
|
||||
let url = '';
|
||||
let repo = '';
|
||||
let config: string | undefined;
|
||||
let workspace: string | undefined;
|
||||
let output: string | undefined;
|
||||
let pipelineTesting = false;
|
||||
let debug = false;
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
const next = argv[i + 1];
|
||||
|
||||
switch (arg) {
|
||||
case '-u':
|
||||
case '--url':
|
||||
if (next && !next.startsWith('-')) {
|
||||
url = next;
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case '-r':
|
||||
case '--repo':
|
||||
if (next && !next.startsWith('-')) {
|
||||
repo = next;
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case '-c':
|
||||
case '--config':
|
||||
if (next && !next.startsWith('-')) {
|
||||
config = next;
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case '-w':
|
||||
case '--workspace':
|
||||
if (next && !next.startsWith('-')) {
|
||||
workspace = next;
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case '-o':
|
||||
case '--output':
|
||||
if (next && !next.startsWith('-')) {
|
||||
output = next;
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case '--pipeline-testing':
|
||||
pipelineTesting = true;
|
||||
break;
|
||||
case '--debug':
|
||||
debug = true;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown option: ${arg}`);
|
||||
console.error(`Run "${getMode() === 'local' ? './shannon' : 'npx @keygraph/shannon'} help" for usage`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!url || !repo) {
|
||||
console.error('ERROR: --url and --repo are required');
|
||||
console.error(`Usage: ${getMode() === 'local' ? './shannon' : 'npx @keygraph/shannon'} start -u <url> -r <path>`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
repo,
|
||||
pipelineTesting,
|
||||
debug,
|
||||
...(config && { config }),
|
||||
...(workspace && { workspace }),
|
||||
...(output && { output }),
|
||||
};
|
||||
}
|
||||
|
||||
// === Main Dispatch ===
|
||||
|
||||
blockSudo();
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
switch (command) {
|
||||
case 'start': {
|
||||
const parsed = parseStartArgs(args.slice(1));
|
||||
await start({ ...parsed, version: getVersion() });
|
||||
break;
|
||||
}
|
||||
case 'stop':
|
||||
stop(args.includes('--clean'));
|
||||
break;
|
||||
case 'logs': {
|
||||
const workspaceId = args[1];
|
||||
if (!workspaceId) {
|
||||
console.error('ERROR: Workspace ID is required');
|
||||
console.error(`Usage: ${getMode() === 'local' ? './shannon' : 'npx @keygraph/shannon'} logs <workspace>`);
|
||||
process.exit(1);
|
||||
}
|
||||
logs(workspaceId);
|
||||
break;
|
||||
}
|
||||
case 'workspaces':
|
||||
workspaces(getVersion());
|
||||
break;
|
||||
case 'status':
|
||||
status();
|
||||
break;
|
||||
case 'setup':
|
||||
if (getMode() === 'local') {
|
||||
console.error('ERROR: setup is only available in npx mode. In local mode, use .env');
|
||||
process.exit(1);
|
||||
}
|
||||
setup();
|
||||
break;
|
||||
case 'build':
|
||||
build(args.includes('--no-cache'));
|
||||
break;
|
||||
case 'uninstall':
|
||||
if (getMode() === 'local') {
|
||||
console.error('ERROR: uninstall is only available in npx mode.');
|
||||
process.exit(1);
|
||||
}
|
||||
uninstall();
|
||||
break;
|
||||
case 'info':
|
||||
displaySplash(getMode() === 'local' ? undefined : getVersion());
|
||||
break;
|
||||
case 'help':
|
||||
case '--help':
|
||||
case '-h':
|
||||
case undefined:
|
||||
showHelp();
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
showHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Runtime mode detection — local (build from source) vs npx (Docker Hub).
|
||||
*
|
||||
* The root `./shannon` entry point sets SHANNON_LOCAL=1 before importing.
|
||||
* When run via npx, `cli/dist/index.js` is executed directly without it.
|
||||
*/
|
||||
|
||||
export type Mode = 'local' | 'npx';
|
||||
|
||||
let cachedMode: Mode | undefined;
|
||||
|
||||
export function getMode(): Mode {
|
||||
if (cachedMode !== undefined) return cachedMode;
|
||||
|
||||
cachedMode = process.env.SHANNON_LOCAL === '1' ? 'local' : 'npx';
|
||||
return cachedMode;
|
||||
}
|
||||
|
||||
export function setMode(mode: Mode): void {
|
||||
cachedMode = mode;
|
||||
}
|
||||
|
||||
export function isLocal(): boolean {
|
||||
return getMode() === 'local';
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Path resolution for --repo and --config arguments.
|
||||
*
|
||||
* Local mode supports bare repo names (e.g. "my-repo" → ./repos/my-repo).
|
||||
* Both modes resolve relative paths against CWD.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { isLocal } from './mode.js';
|
||||
|
||||
export interface MountPair {
|
||||
hostPath: string;
|
||||
containerPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve --repo to absolute path and container mount.
|
||||
* Dev mode: bare names (no / or . prefix) check ./repos/<name> first.
|
||||
*/
|
||||
export function resolveRepo(repoArg: string): MountPair {
|
||||
let hostPath: string;
|
||||
|
||||
if (isLocal() && !repoArg.startsWith('/') && !repoArg.startsWith('.')) {
|
||||
// Bare name — check ./repos/<name> for backward compatibility
|
||||
const barePath = path.resolve('repos', repoArg);
|
||||
if (fs.existsSync(barePath)) {
|
||||
hostPath = barePath;
|
||||
} else {
|
||||
console.error(`ERROR: Repository not found at ./repos/${repoArg}`);
|
||||
console.error('');
|
||||
console.error('Place your target repository under the ./repos/ directory,');
|
||||
console.error('or pass an absolute/relative path: -r /path/to/repo');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
hostPath = path.resolve(repoArg);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(hostPath)) {
|
||||
console.error(`ERROR: Repository not found: ${hostPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.statSync(hostPath).isDirectory()) {
|
||||
console.error(`ERROR: Not a directory: ${hostPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const basename = path.basename(hostPath);
|
||||
return {
|
||||
hostPath,
|
||||
containerPath: `/repos/${basename}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve --config to absolute path and container mount.
|
||||
*/
|
||||
export function resolveConfig(configArg: string): MountPair {
|
||||
const hostPath = path.resolve(configArg);
|
||||
|
||||
if (!fs.existsSync(hostPath)) {
|
||||
console.error(`ERROR: Config file not found: ${hostPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.statSync(hostPath).isFile()) {
|
||||
console.error(`ERROR: Not a file: ${hostPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const basename = path.basename(hostPath);
|
||||
return {
|
||||
hostPath,
|
||||
containerPath: `/app/configs/${basename}`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Splash screen display — pure terminal output, no npm dependencies.
|
||||
*/
|
||||
|
||||
export function displaySplash(version?: string): void {
|
||||
const GOLD = '\x1b[38;2;244;197;66m';
|
||||
const CYAN = '\x1b[36;1m';
|
||||
const WHITE = '\x1b[1;37m';
|
||||
const GRAY = '\x1b[0;37m';
|
||||
const YELLOW = '\x1b[1;33m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
const B = `${CYAN}\u2551${RESET}`;
|
||||
const S67 = ' '.repeat(67);
|
||||
const HR = '\u2550'.repeat(67);
|
||||
|
||||
const lines = [
|
||||
'',
|
||||
` ${CYAN}\u2554${HR}\u2557${RESET}`,
|
||||
` ${B}${S67}${B}`,
|
||||
` ${B} ${GOLD}\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557${RESET} ${B}`,
|
||||
` ${B} ${GOLD}\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551${RESET} ${B}`,
|
||||
` ${B} ${GOLD}\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551${RESET} ${B}`,
|
||||
` ${B} ${GOLD}\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551${RESET} ${B}`,
|
||||
` ${B} ${GOLD}\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551${RESET} ${B}`,
|
||||
` ${B} ${GOLD}\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D${RESET} ${B}`,
|
||||
` ${B}${S67}${B}`,
|
||||
` ${B} ${CYAN}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${RESET} ${B}`,
|
||||
` ${B} ${CYAN}\u2551${RESET} ${WHITE}AI Penetration Testing Framework${RESET} ${CYAN}\u2551${RESET} ${B}`,
|
||||
` ${B} ${CYAN}\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${RESET} ${B}`,
|
||||
` ${B}${S67}${B}`,
|
||||
];
|
||||
|
||||
if (version) {
|
||||
const verStr = `v${version}`;
|
||||
const verPadLeft = Math.floor((67 - verStr.length) / 2);
|
||||
const verPadRight = 67 - verStr.length - verPadLeft;
|
||||
lines.push(` ${B}${' '.repeat(verPadLeft)}${GRAY}${verStr}${RESET}${' '.repeat(verPadRight)}${B}`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
` ${B}${S67}${B}`,
|
||||
` ${B} ${YELLOW}\uD83D\uDD10 DEFENSIVE SECURITY ONLY \uD83D\uDD10${RESET} ${B}`,
|
||||
` ${B}${S67}${B}`,
|
||||
` ${CYAN}\u255A${HR}\u255D${RESET}`,
|
||||
'',
|
||||
);
|
||||
|
||||
console.log(lines.join('\n'));
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: 'esm',
|
||||
target: 'node18',
|
||||
outDir: 'dist',
|
||||
clean: true,
|
||||
deps: { neverBundle: ['@clack/prompts', 'dotenv', 'smol-toml'] },
|
||||
banner: { js: '#!/usr/bin/env node' },
|
||||
});
|
||||
@@ -39,9 +39,33 @@
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z2-7]+=*$",
|
||||
"description": "TOTP secret for two-factor authentication (Base32 encoded, case insensitive)"
|
||||
},
|
||||
"email_login": {
|
||||
"type": "object",
|
||||
"description": "Email account credentials for magic-link or OTP follow-through flows",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "Email address used to receive magic links or OTPs"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 255,
|
||||
"description": "Password for the email account"
|
||||
},
|
||||
"totp_secret": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z2-7]+=*$",
|
||||
"description": "TOTP secret for the email account's two-factor authentication (Base32 encoded)"
|
||||
}
|
||||
},
|
||||
"required": ["address", "password"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["username", "password"],
|
||||
"required": ["username"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"login_flow": {
|
||||
@@ -78,6 +102,23 @@
|
||||
"required": ["login_type", "login_url", "credentials", "success_condition"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"pipeline": {
|
||||
"type": "object",
|
||||
"description": "Pipeline execution settings for retry behavior and concurrency",
|
||||
"properties": {
|
||||
"retry_preset": {
|
||||
"type": "string",
|
||||
"enum": ["default", "subscription"],
|
||||
"description": "Retry preset. 'subscription' extends timeouts for Anthropic subscription rate limit windows (5h+)."
|
||||
},
|
||||
"max_concurrent_pipelines": {
|
||||
"type": "string",
|
||||
"pattern": "^[1-5]$",
|
||||
"description": "Max concurrent vulnerability pipelines (1-5, default: 5)"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rules": {
|
||||
"type": "object",
|
||||
"description": "Testing rules that define what to focus on or avoid during penetration testing",
|
||||
@@ -101,16 +142,73 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"vuln_classes": {
|
||||
"type": "array",
|
||||
"description": "Vulnerability classes to test. When omitted, all five classes run. When set, only listed classes run; their vuln+exploit agents and report sections are included.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["injection", "xss", "auth", "authz", "ssrf"]
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 5,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"exploit": {
|
||||
"type": "string",
|
||||
"enum": ["true", "false"],
|
||||
"description": "Whether to run the exploitation phase (default true). Set false to run only analysis."
|
||||
},
|
||||
"report": {
|
||||
"type": "object",
|
||||
"description": "Report filtering and guidance applied by the report agent.",
|
||||
"properties": {
|
||||
"min_severity": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high", "critical"],
|
||||
"description": "Minimum severity threshold; findings below are dropped by the report agent."
|
||||
},
|
||||
"min_confidence": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"],
|
||||
"description": "Minimum confidence threshold; findings below are dropped by the report agent."
|
||||
},
|
||||
"guidance": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 500,
|
||||
"description": "Free-text guidance to the report agent (e.g., 'Drop findings about missing security headers')."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"rules_of_engagement": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 1000,
|
||||
"description": "Free-text instructions to the agent that render into every prompt."
|
||||
},
|
||||
"login": {
|
||||
"type": "object",
|
||||
"description": "Deprecated: Use 'authentication' section instead",
|
||||
"deprecated": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the target environment, its deployment context, and any information that helps guide the security assessment",
|
||||
"minLength": 1,
|
||||
"maxLength": 500,
|
||||
"pattern": "\\S"
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{"required": ["authentication"]},
|
||||
{"required": ["rules"]},
|
||||
{"required": ["authentication", "rules"]}
|
||||
{ "required": ["authentication"] },
|
||||
{ "required": ["rules"] },
|
||||
{ "required": ["authentication", "rules"] },
|
||||
{ "required": ["description"] },
|
||||
{ "required": ["vuln_classes"] },
|
||||
{ "required": ["exploit"] },
|
||||
{ "required": ["report"] },
|
||||
{ "required": ["rules_of_engagement"] }
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
@@ -126,18 +224,18 @@
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["path", "subdomain", "domain", "method", "header", "parameter"],
|
||||
"description": "Type of rule (what aspect of requests to match against)"
|
||||
"enum": ["url_path", "subdomain", "domain", "method", "header", "parameter", "code_path"],
|
||||
"description": "Type of rule (what aspect of requests or source code to match against)"
|
||||
},
|
||||
"url_path": {
|
||||
"value": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 1000,
|
||||
"description": "URL path pattern or value to match"
|
||||
"description": "Value to match"
|
||||
}
|
||||
},
|
||||
"required": ["description", "type", "url_path"],
|
||||
"required": ["description", "type", "value"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
# Example configuration file for pentest-agent
|
||||
# Copy this file and modify it for your specific testing needs
|
||||
|
||||
# Description of the target environment (optional, max 500 chars)
|
||||
description: "Next.js e-commerce app on PostgreSQL. Local dev environment — .env files contain local-only credentials, not deployed to production."
|
||||
|
||||
# Limit which vulnerability classes run end-to-end (optional, default: all five)
|
||||
# vuln_classes: [injection, xss, auth, authz, ssrf]
|
||||
|
||||
# Skip the exploitation phase (optional, default: "true")
|
||||
# exploit: "false"
|
||||
|
||||
# Free-form engagement rules applied to analysis and exploitation agents (optional).
|
||||
# Example below is illustrative; edit, remove, or add sections as needed.
|
||||
# rules_of_engagement: |
|
||||
# Forbidden techniques:
|
||||
# - No password brute-force or credential stuffing. Cap login attempts at 5 per account.
|
||||
# - ...
|
||||
#
|
||||
# Operational:
|
||||
# - Throttle to under 5 requests per second per endpoint. Back off 60 seconds on any 429 response.
|
||||
# - ...
|
||||
#
|
||||
# Data handling:
|
||||
# - Do not include actual values in deliverables — use placeholders like [order_id] or [user_email].
|
||||
# - ...
|
||||
|
||||
authentication:
|
||||
login_type: form # Options: 'form' or 'sso'
|
||||
login_url: "https://example.com/login"
|
||||
credentials:
|
||||
username: "testuser"
|
||||
password: "testpassword"
|
||||
totp_secret: "JBSWY3DPEHPK3PXP" # Optional TOTP secret for 2FA
|
||||
|
||||
# Optional mailbox credentials for magic-link / email-OTP flows.
|
||||
# email_login:
|
||||
# address: "inbox@example.com"
|
||||
# password: "mailbox-password"
|
||||
# totp_secret: "JBSWY3DPEHPK3PXP"
|
||||
|
||||
# Natural language instructions for login flow
|
||||
login_flow:
|
||||
- "Type $username into the email field"
|
||||
- "Type $password into the password field"
|
||||
- "Click the 'Sign In' button"
|
||||
- "Enter $totp in the verification code field"
|
||||
- "Click 'Verify'"
|
||||
|
||||
success_condition:
|
||||
type: url_contains # Options: 'url_contains' or 'element_present'
|
||||
value: "/dashboard"
|
||||
|
||||
rules:
|
||||
# Supported types: url_path, subdomain, domain, method, header, parameter, code_path
|
||||
avoid:
|
||||
- description: "Do not test the marketing site subdomain"
|
||||
type: subdomain
|
||||
value: "www"
|
||||
|
||||
- description: "Skip logout functionality"
|
||||
type: url_path
|
||||
value: "/logout"
|
||||
|
||||
- description: "No DELETE operations on user API"
|
||||
type: url_path
|
||||
value: "/api/v1/users/*"
|
||||
|
||||
# code_path values are repo-relative file paths or globs (e.g. "src/auth.ts", "test/**").
|
||||
# - description: "Test fixtures and specs (not production code)"
|
||||
# type: code_path
|
||||
# value: "test/**"
|
||||
#
|
||||
# - description: "Generated migrations"
|
||||
# type: code_path
|
||||
# value: "db/migrations/**"
|
||||
|
||||
focus:
|
||||
- description: "Prioritize beta admin panel subdomain"
|
||||
type: subdomain
|
||||
value: "beta-admin"
|
||||
|
||||
- description: "Focus on user profile updates"
|
||||
type: url_path
|
||||
value: "/api/v2/user-profile"
|
||||
|
||||
# code_path values are repo-relative file paths or globs (e.g. "src/auth.ts", "routes/*.ts").
|
||||
# - description: "Express route handlers"
|
||||
# type: code_path
|
||||
# value: "routes/*.ts"
|
||||
#
|
||||
# - description: "Sequelize ORM model definitions"
|
||||
# type: code_path
|
||||
# value: "models/*.ts"
|
||||
|
||||
# Report filters applied by the report agent when assembling the final report (optional).
|
||||
# Example below is illustrative; edit, remove, or add sections as needed.
|
||||
# report:
|
||||
# min_severity: low
|
||||
# min_confidence: low
|
||||
# guidance: |
|
||||
# Drop findings about missing security headers and rate-limit gaps.
|
||||
# ...
|
||||
|
||||
# Pipeline execution settings (optional)
|
||||
# pipeline:
|
||||
# retry_preset: subscription # 'default' or 'subscription' (6h max retry for rate limit recovery)
|
||||
# max_concurrent_pipelines: 2 # 1-5, default: 5 (reduce to lower API usage spikes)
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@shannon/worker",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./interfaces": "./dist/interfaces/index.js",
|
||||
"./types": "./dist/types/index.js",
|
||||
"./types/config": "./dist/types/config.js",
|
||||
"./types/agents": "./dist/types/agents.js",
|
||||
"./pipeline": "./dist/temporal/pipeline.js",
|
||||
"./activities": "./dist/temporal/activities.js",
|
||||
"./services": "./dist/services/index.js",
|
||||
"./config": "./dist/config-parser.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "catalog:",
|
||||
"@temporalio/activity": "^1.11.0",
|
||||
"@temporalio/client": "^1.11.0",
|
||||
"@temporalio/worker": "^1.11.0",
|
||||
"@temporalio/workflow": "^1.11.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"zod": "^4.3.6",
|
||||
"zx": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "^4.0.9"
|
||||
}
|
||||
}
|
||||
@@ -105,14 +105,16 @@ If you only confirmed a vulnerability without attempting to weaponize it, you ha
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<starting_context>
|
||||
- Your **actionable exploitation queue** is located at `deliverables/auth_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
- Your **actionable exploitation queue** is located at `.shannon/deliverables/auth_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
|
||||
**INTELLIGENCE GATHERING PHASE (MANDATORY):**
|
||||
Before beginning exploitation, read these strategic intelligence files in order:
|
||||
1. `deliverables/pre_recon_deliverable.md` - Architecture overview, technology stack, and authentication code locations.
|
||||
2. `deliverables/recon_deliverable.md` - Complete API inventory, user roles, and data flow maps.
|
||||
3. `deliverables/auth_analysis_deliverable.md` - Strategic context from the Auth analysis specialist, including notes on session mechanisms, password policies, and flawed logic paths.
|
||||
1. `.shannon/deliverables/pre_recon_deliverable.md` - Architecture overview, technology stack, and authentication code locations.
|
||||
2. `.shannon/deliverables/recon_deliverable.md` - Complete API inventory, user roles, and data flow maps.
|
||||
3. `.shannon/deliverables/auth_analysis_deliverable.md` - Strategic context from the Auth analysis specialist, including notes on session mechanisms, password policies, and flawed logic paths.
|
||||
|
||||
- You will manage your work using the **TodoWrite tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts.
|
||||
</starting_context>
|
||||
@@ -122,13 +124,13 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
|
||||
**Phase Sequence:** VULN ANALYSIS (Complete) → **AUTH EXPLOITATION (You)** → FINAL REPORT (next phase)
|
||||
|
||||
**Your Input:** `deliverables/auth_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `deliverables/auth_exploitation_evidence.md` (exploitation evidence)
|
||||
**Your Input:** `.shannon/deliverables/auth_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `.shannon/deliverables/auth_exploitation_evidence.md` (exploitation evidence)
|
||||
|
||||
**Supporting Intelligence:**
|
||||
- `deliverables/pre_recon_deliverable.md` (architecture & code context)
|
||||
- `deliverables/recon_deliverable.md` (API inventory & user roles)
|
||||
- `deliverables/auth_analysis_deliverable.md` (strategic intel)
|
||||
- `.shannon/deliverables/pre_recon_deliverable.md` (architecture & code context)
|
||||
- `.shannon/deliverables/recon_deliverable.md` (API inventory & user roles)
|
||||
- `.shannon/deliverables/auth_analysis_deliverable.md` (strategic intel)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture and attack surfaces
|
||||
@@ -141,13 +143,8 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
You are the **Identity Compromise Specialist** - proving tangible impact of broken authentication through successful account takeover and session hijacking.
|
||||
</system_architecture>
|
||||
|
||||
<available_tools>
|
||||
- **{{MCP_SERVER}} (Playwright):** Essential for interacting with multi-step authentication flows, injecting stolen session cookies, and verifying account takeover in a real browser context.
|
||||
- **save_deliverable (MCP Tool):** Saves exploitation evidence files.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "AUTH_EVIDENCE" (required)
|
||||
- `content`: Your complete evidence report (markdown) (required)
|
||||
- **Returns:** `{ status: "success", filepath: "..." }` on success or `{ status: "error", message: "..." }` on failure
|
||||
<cli_tools>
|
||||
- **Browser Automation (playwright-cli skill):** Essential for interacting with multi-step authentication flows, injecting stolen session cookies, and verifying account takeover in a real browser context. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
- **Task Agent:** Mandatory coder-executor for brute force batches, credential stuffing, token replay automation, and any scripted workflow.
|
||||
- **TodoWrite tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow.
|
||||
@@ -156,7 +153,7 @@ You are the **Identity Compromise Specialist** - proving tangible impact of brok
|
||||
**CRITICAL TASK AGENT WORKFLOW:**
|
||||
- Delegate every custom script or multi-step automation to the Task Agent; do not hand-write scripts in Bash.
|
||||
- Keep requests ≤15 lines and use the template below so the Task Agent has clear inputs and success criteria.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
## Task Agent Scripting Requirements
|
||||
Use the Task Agent to execute all bulk or repetitive auth attacks. Manual commands are for quick verification only.
|
||||
@@ -189,7 +186,7 @@ Your primary goal is to achieve a definitive, evidence-backed conclusion for **e
|
||||
Your entire exploitation process will be managed through the TodoWrite tool. You MUST adhere to this process.
|
||||
|
||||
**1. Initialization:**
|
||||
- Read `deliverables/auth_exploitation_queue.json`.
|
||||
- Read `.shannon/deliverables/auth_exploitation_queue.json`.
|
||||
- Use the TodoWrite tool to create tasks for each vulnerability in the queue.
|
||||
- Each task should include the vulnerability ID and the hypothesis to test.
|
||||
Example tasks:
|
||||
@@ -307,92 +304,19 @@ Prioritize these based on the intelligence from your queue and vulnerability ana
|
||||
</attack_patterns>
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
When your attack plan is fully explored (all checkboxes are marked `[x]`), your mission is complete. You MUST generate two final files.
|
||||
<mcp_tools>
|
||||
You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/auth_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly.
|
||||
|
||||
**1. Your Specialist Deliverable**
|
||||
**When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission.
|
||||
|
||||
First, synthesize all evidence into your specialist section and save it using the save_deliverable script with type AUTH_EVIDENCE. This section will be combined with other specialist reports to form the complete security assessment.
|
||||
**Status discriminator.** Set `status: "exploited"` only when you've reached Level 3+ with concrete impact evidence (account takeover demonstrated, session hijacked end-to-end, password reset abused, MFA bypassed). Set `status: "blocked"` only for findings that are real vulnerabilities but where external factors — NOT security defenses — prevented full exploitation. See the Classification Decision Framework in this prompt. Do NOT call `add_exploit` for findings classified FALSE POSITIVE; those go in your `workspace/auth_false_positives.md` tracking file, not the deliverable.
|
||||
|
||||
**Section Ordering & Format Requirements:**
|
||||
- Use the exact vulnerability IDs from `deliverables/auth_exploitation_queue.json`
|
||||
- Within "Successfully Exploited" section: Order by severity of impact (most critical first)
|
||||
- Within "Confirmed Vulnerabilities (Attempted Exploitation)" section: Order by severity (most critical first)
|
||||
- Within "Potential" section: Order by confidence level (highest confidence first)
|
||||
**ID alignment.** `vulnerability_id` must match an ID from `.shannon/deliverables/auth_exploitation_queue.json` exactly (e.g. `AUTH-VULN-03`). The collector will reject IDs not in the queue with a list of valid IDs; if you get that error, you either typo'd an ID or imagined one — fix and retry.
|
||||
|
||||
**Critical Documentation Requirements:**
|
||||
**Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting.
|
||||
|
||||
**REPRODUCIBILITY IS MANDATORY:**
|
||||
- Every exploitation step must be completely self-contained and reproducible
|
||||
- Include full URLs with protocol, domain, port, path, and all parameters
|
||||
- Provide complete payloads and commands exactly as they should be used
|
||||
- Use clear placeholders for variable values like [SESSION_TOKEN], [PASSWORD], [USERNAME]
|
||||
- Write as if the reader has never seen the application before
|
||||
- Steps must be detailed enough that someone unfamiliar with the application can follow without additional research
|
||||
|
||||
---
|
||||
# Authentication Exploitation Evidence
|
||||
|
||||
## Successfully Exploited Vulnerabilities
|
||||
|
||||
### AUTH-VULN-01: Session Hijacking via Non-HttpOnly Cookie
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Authentication mechanism or endpoint affected]
|
||||
- **Overview:** [Breif Summary of the exploit itself]
|
||||
- **Impact:** [Business/security impact achieved]
|
||||
- **Severity:** [Critical/High/Medium/Low based on impact]
|
||||
|
||||
**Prerequisites:** (if any)
|
||||
[Required setup, tools, or conditions]
|
||||
|
||||
**Exploitation Steps:**
|
||||
[Each step must be completely reproducible with full details]
|
||||
|
||||
1. [Complete action with all necessary information]
|
||||
2. [Next action with full details]
|
||||
[Continue as needed...]
|
||||
|
||||
**Proof of Impact:**
|
||||
[Concrete evidence of successful exploitation]
|
||||
|
||||
**Notes:** (optional)
|
||||
[Any additional relevant information]
|
||||
|
||||
### AUTH-VULN-02: [Another Successfully Exploited Vulnerability]
|
||||
... (Repeat for each successfully exploited vulnerability) ...
|
||||
|
||||
## Potential Vulnerabilities (Validation Blocked)
|
||||
|
||||
### AUTH-VULN-03: Suspected Privilege Escalation via Role Manipulation
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Endpoint or mechanism where vulnerability exists]
|
||||
- **Current Blocker:** [What prevents full exploitation]
|
||||
- **Potential Impact:** [What could be achieved if blocker removed]
|
||||
- **Confidence:** [HIGH/MEDIUM/LOW]
|
||||
|
||||
**Evidence of Vulnerability:**
|
||||
[Code snippets, responses, or behavior proving vulnerability exists]
|
||||
|
||||
**Attempted Exploitation:**
|
||||
[What techniques were tried and why they didn't succeed]
|
||||
|
||||
**How This Would Be Exploited:**
|
||||
[If the blocker were removed, these steps would achieve exploitation]
|
||||
|
||||
If [blocker] were bypassed/removed:
|
||||
|
||||
1. [Complete action with all necessary details]
|
||||
2. [Next action with full information]
|
||||
[Continue as needed...]
|
||||
|
||||
**Expected Impact:**
|
||||
[Specific data or access that would be compromised]
|
||||
|
||||
---
|
||||
|
||||
</deliverable_instructions>
|
||||
**Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible.
|
||||
</mcp_tools>
|
||||
|
||||
<conclusion_trigger>
|
||||
### Evidence Completeness Verification
|
||||
@@ -405,10 +329,11 @@ If [blocker] were bypassed/removed:
|
||||
|
||||
COMPLETION REQUIREMENTS (ALL must be satisfied):
|
||||
1. Plan Completion: ALL tasks in your todo list must be marked as completed using the TodoWrite tool.
|
||||
2. Deliverable Generation: The required deliverable must be successfully saved using save_deliverable script:
|
||||
- Evidence report: Use `save_deliverable` MCP tool with `deliverable_type: "AUTH_EVIDENCE"` and your evidence report as `content`
|
||||
2. Evidence Emission: Every non-FALSE-POSITIVE vulnerability in `.shannon/deliverables/auth_exploitation_queue.json` has been recorded with one `add_exploit` call carrying the appropriate `status` ("exploited" or "blocked") and required fields. FALSE POSITIVE findings are recorded only in `workspace/auth_false_positives.md`, not via `add_exploit`.
|
||||
|
||||
CRITICAL WARNING: Announcing completion before every item in deliverables/auth_exploitation_queue.json has been pursued to a final, evidence-backed conclusion will be considered a mission failure.
|
||||
CRITICAL WARNING: Announcing completion before every item in .shannon/deliverables/auth_exploitation_queue.json has been pursued to a final, evidence-backed conclusion (either successfully exploited, recorded as potential-but-blocked, or verified false positive) will be considered a mission failure.
|
||||
|
||||
ONLY AFTER fulfilling these exhaustive requirements, announce "AUTH EXPLOITATION COMPLETE" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -88,18 +88,22 @@ After exhaustive bypass attempts, determine:
|
||||
@include(shared/_rules.txt)
|
||||
</rules>
|
||||
|
||||
@include(shared/_shared-session.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<starting_context>
|
||||
- Your **actionable exploitation queue** is located at `deliverables/authz_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
- Your **actionable exploitation queue** is located at `.shannon/deliverables/authz_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
|
||||
**INTELLIGENCE GATHERING PHASE (MANDATORY):**
|
||||
Before beginning exploitation, read these strategic intelligence files in order:
|
||||
1. `deliverables/pre_recon_deliverable.md` - Architecture overview, technology stack, and authorization code locations.
|
||||
2. `deliverables/recon_deliverable.md` - Complete API inventory, user roles, and permission models.
|
||||
3. `deliverables/authz_analysis_deliverable.md` - Strategic context from the Authz analysis specialist, including notes on access control patterns, role hierarchies, and flawed logic paths.
|
||||
1. `.shannon/deliverables/pre_recon_deliverable.md` - Architecture overview, technology stack, and authorization code locations.
|
||||
2. `.shannon/deliverables/recon_deliverable.md` - Complete API inventory, user roles, and permission models.
|
||||
3. `.shannon/deliverables/authz_analysis_deliverable.md` - Strategic context from the Authz analysis specialist, including notes on access control patterns, role hierarchies, and flawed logic paths.
|
||||
|
||||
- You will manage your work using the **TodoWrite tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts.
|
||||
</starting_context>
|
||||
@@ -109,13 +113,13 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
|
||||
**Phase Sequence:** VULN ANALYSIS (Complete) → **AUTHZ EXPLOITATION (You)** → FINAL REPORT (next phase)
|
||||
|
||||
**Your Input:** `deliverables/authz_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `deliverables/authz_exploitation_evidence.md` (exploitation evidence)
|
||||
**Your Input:** `.shannon/deliverables/authz_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `.shannon/deliverables/authz_exploitation_evidence.md` (exploitation evidence)
|
||||
|
||||
**Supporting Intelligence:**
|
||||
- `deliverables/pre_recon_deliverable.md` (architecture & code context)
|
||||
- `deliverables/recon_deliverable.md` (API inventory & user roles)
|
||||
- `deliverables/authz_analysis_deliverable.md` (strategic intel)
|
||||
- `.shannon/deliverables/pre_recon_deliverable.md` (architecture & code context)
|
||||
- `.shannon/deliverables/recon_deliverable.md` (API inventory & user roles)
|
||||
- `.shannon/deliverables/authz_analysis_deliverable.md` (strategic intel)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture and attack surfaces
|
||||
@@ -128,13 +132,8 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
You are the **Privilege Escalation Specialist** - proving tangible impact of broken authorization through access control bypass and unauthorized data access.
|
||||
</system_architecture>
|
||||
|
||||
<available_tools>
|
||||
- **{{MCP_SERVER}} (Playwright):** Essential for interacting with complex authorization flows, testing role-based access controls in browser contexts, and verifying privilege escalation through UI elements.
|
||||
- **save_deliverable (MCP Tool):** Saves exploitation evidence files.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "AUTHZ_EVIDENCE" (required)
|
||||
- `content`: Your complete evidence report (markdown) (required)
|
||||
- **Returns:** `{ status: "success", filepath: "..." }` on success or `{ status: "error", message: "..." }` on failure
|
||||
<cli_tools>
|
||||
- **Browser Automation (playwright-cli skill):** Essential for interacting with complex authorization flows, testing role-based access controls in browser contexts, and verifying privilege escalation through UI elements. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
- **Task Agent:** Mandatory coder-executor for IDOR sweeps, role escalation loops, and workflow bypass automation.
|
||||
- **TodoWrite tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow.
|
||||
@@ -143,7 +142,7 @@ You are the **Privilege Escalation Specialist** - proving tangible impact of bro
|
||||
**CRITICAL TASK AGENT WORKFLOW:**
|
||||
- Delegate every multi-user iteration, role toggle test, or workflow automation script to the Task Agent—never handcraft these scripts yourself.
|
||||
- Keep requests ≤15 lines and adhere to the template below so the Task Agent can act deterministically.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
## Task Agent Scripting Requirements
|
||||
All repeated authorization tests must run through the Task Agent.
|
||||
@@ -176,7 +175,7 @@ Your primary goal is to achieve a definitive, evidence-backed conclusion for **e
|
||||
Your entire exploitation process will be managed through the TodoWrite tool. You MUST adhere to this process.
|
||||
|
||||
**1. Initialization:**
|
||||
- Read `deliverables/authz_exploitation_queue.json`.
|
||||
- Read `.shannon/deliverables/authz_exploitation_queue.json`.
|
||||
- Use the TodoWrite tool to create tasks for each vulnerability in the queue.
|
||||
- Each task should include the vulnerability ID, type, and the hypothesis to test.
|
||||
Example tasks:
|
||||
@@ -313,88 +312,19 @@ Remember: The most effective attacks often come from understanding the specific
|
||||
</attack_patterns>
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
When your attack plan is fully explored (all checkboxes are marked `[x]`), your mission is complete. You MUST generate two final files.
|
||||
<mcp_tools>
|
||||
You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/authz_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly.
|
||||
|
||||
**1. Your Specialist Deliverable**
|
||||
**When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission.
|
||||
|
||||
First, synthesize all evidence into your specialist section and save it using the save_deliverable script with type AUTHZ_EVIDENCE. This section will be combined with other specialist reports to form the complete security assessment and MUST use the following structure:
|
||||
**Status discriminator.** Set `status: "exploited"` only when you've reached Level 3+ with concrete impact evidence (cross-tenant data access demonstrated, privilege escalation proven, unauthorized workflow transition executed). Set `status: "blocked"` only for findings that are real vulnerabilities but where external factors — NOT security defenses — prevented full exploitation. See the Classification Decision Framework in this prompt. Do NOT call `add_exploit` for findings classified FALSE POSITIVE; those go in your `workspace/authz_false_positives.md` tracking file, not the deliverable.
|
||||
|
||||
**Section Ordering & Format Requirements:**
|
||||
- Use the exact vulnerability IDs from `deliverables/authz_exploitation_queue.json`
|
||||
- Within "Successfully Exploited" section: Order by severity of impact (most critical first)
|
||||
- Within "Potential" section: Order by confidence level (highest confidence first)
|
||||
**ID alignment.** `vulnerability_id` must match an ID from `.shannon/deliverables/authz_exploitation_queue.json` exactly (e.g. `AUTHZ-VULN-03`). The collector will reject IDs not in the queue with a list of valid IDs; if you get that error, you either typo'd an ID or imagined one — fix and retry.
|
||||
|
||||
**Critical Documentation Requirements:**
|
||||
**Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting.
|
||||
|
||||
**REPRODUCIBILITY IS MANDATORY:**
|
||||
- Every exploitation step must be completely self-contained and reproducible
|
||||
- Include full URLs with protocol, domain, port, path, and all parameters
|
||||
- Provide complete payloads and commands exactly as they should be used
|
||||
- Use clear placeholders for variable values like [SESSION_TOKEN], [USER_ID], [ROLE]
|
||||
- Write as if the reader has never seen the application before
|
||||
- Steps must be detailed enough that someone unfamiliar with the application can follow without additional research
|
||||
|
||||
---
|
||||
# Authorization Exploitation Evidence
|
||||
|
||||
## Successfully Exploited Vulnerabilities
|
||||
|
||||
### AUTHZ-VULN-[ID]: [Vulnerability Title]
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Endpoint or mechanism where vulnerability exists]
|
||||
- **Overview:** [Breif Summary of the exploit itself]
|
||||
- **Impact:** [Business/security impact achieved]
|
||||
- **Severity:** [Critical/High/Medium/Low based on impact]
|
||||
|
||||
**Prerequisites:** (if any)
|
||||
[Required setup, tools, or conditions]
|
||||
|
||||
**Exploitation Steps:**
|
||||
[Each step must be completely reproducible with full details]
|
||||
|
||||
1. [Complete action with all necessary information]
|
||||
2. [Next action with full details]
|
||||
[Continue as needed...]
|
||||
|
||||
**Proof of Impact:**
|
||||
[Concrete evidence of successful exploitation]
|
||||
|
||||
**Notes:** (optional)
|
||||
[Any additional relevant information]
|
||||
|
||||
## Potential Vulnerabilities (Validation Blocked)
|
||||
|
||||
### AUTHZ-VULN-[ID]: [Vulnerability Title]
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Endpoint or mechanism where vulnerability exists]
|
||||
- **Current Blocker:** [What prevents full exploitation]
|
||||
- **Potential Impact:** [What could be achieved if blocker removed]
|
||||
- **Confidence:** [HIGH/MEDIUM/LOW]
|
||||
|
||||
**Evidence of Vulnerability:**
|
||||
[Code snippets, responses, or behavior proving vulnerability exists]
|
||||
|
||||
**Attempted Exploitation:**
|
||||
[What techniques were tried and why they didn't succeed]
|
||||
|
||||
**How This Would Be Exploited:**
|
||||
[If the blocker were removed, these steps would achieve exploitation]
|
||||
|
||||
If [blocker] were bypassed/removed:
|
||||
|
||||
1. [Complete action with all necessary details]
|
||||
2. [Next action with full information]
|
||||
[Continue as needed...]
|
||||
|
||||
**Expected Impact:**
|
||||
[Specific data or access that would be compromised]
|
||||
|
||||
---
|
||||
|
||||
</deliverable_instructions>
|
||||
**Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible.
|
||||
</mcp_tools>
|
||||
|
||||
<conclusion_trigger>
|
||||
### Evidence Completeness Verification
|
||||
@@ -407,10 +337,11 @@ If [blocker] were bypassed/removed:
|
||||
|
||||
COMPLETION REQUIREMENTS (ALL must be satisfied):
|
||||
1. Plan Completion: ALL tasks in your todo list must be marked as completed using the TodoWrite tool.
|
||||
2. Deliverable Generation: The required deliverable must be successfully saved using save_deliverable script:
|
||||
- Evidence report: Use `save_deliverable` MCP tool with `deliverable_type: "AUTHZ_EVIDENCE"` and your evidence report as `content`
|
||||
2. Evidence Emission: Every non-FALSE-POSITIVE vulnerability in `.shannon/deliverables/authz_exploitation_queue.json` has been recorded with one `add_exploit` call carrying the appropriate `status` ("exploited" or "blocked") and required fields. FALSE POSITIVE findings are recorded only in `workspace/authz_false_positives.md`, not via `add_exploit`.
|
||||
|
||||
CRITICAL WARNING: Announcing completion before every item in deliverables/authz_exploitation_queue.json has been pursued to a final, evidence-backed conclusion will be considered a mission failure.
|
||||
CRITICAL WARNING: Announcing completion before every item in .shannon/deliverables/authz_exploitation_queue.json has been pursued to a final, evidence-backed conclusion (either successfully exploited, recorded as potential-but-blocked, or verified false positive) will be considered a mission failure.
|
||||
|
||||
ONLY AFTER fulfilling these exhaustive requirements, announce "AUTHZ EXPLOITATION COMPLETE" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -65,18 +65,22 @@ Remember: An unproven vulnerability is worse than no finding at all - it wastes
|
||||
@include(shared/_rules.txt)
|
||||
</rules>
|
||||
|
||||
@include(shared/_shared-session.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<starting_context>
|
||||
- Your **actionable exploitation queue** is located at `deliverables/injection_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
- Your **actionable exploitation queue** is located at `.shannon/deliverables/injection_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
|
||||
**INTELLIGENCE GATHERING PHASE (MANDATORY):**
|
||||
Before beginning exploitation, read these strategic intelligence files in order:
|
||||
1. `deliverables/pre_recon_deliverable.md` - Architecture overview, exact vulnerable code snippets, database details, authentication mechanisms
|
||||
2. `deliverables/recon_deliverable.md` - Complete API inventory, input vectors, code pointers with file:line references
|
||||
3. `deliverables/injection_analysis_deliverable.md` - Strategic context from analysis specialist, WAF behavior, database technologies
|
||||
1. `.shannon/deliverables/pre_recon_deliverable.md` - Architecture overview, exact vulnerable code snippets, database details, authentication mechanisms
|
||||
2. `.shannon/deliverables/recon_deliverable.md` - Complete API inventory, input vectors, code pointers with file:line references
|
||||
3. `.shannon/deliverables/injection_analysis_deliverable.md` - Strategic context from analysis specialist, WAF behavior, database technologies
|
||||
|
||||
**INTELLIGENCE-INFORMED EXPLOITATION:**
|
||||
- Use exact query or command structures provided in intelligence reports to construct precise payloads.
|
||||
@@ -91,13 +95,13 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
|
||||
**Phase Sequence:** VULN ANALYSIS (Complete) → **INJECTION EXPLOITATION (You)** → FINAL REPORT (next phase)
|
||||
|
||||
**Your Input:** `deliverables/injection_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `deliverables/injection_exploitation_evidence.md` (exploitation evidence)
|
||||
**Your Input:** `.shannon/deliverables/injection_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `.shannon/deliverables/injection_exploitation_evidence.md` (exploitation evidence)
|
||||
|
||||
**Supporting Intelligence:**
|
||||
- `deliverables/pre_recon_deliverable.md` (architecture & code context)
|
||||
- `deliverables/recon_deliverable.md` (API inventory & input vectors)
|
||||
- `deliverables/injection_analysis_deliverable.md` (strategic intel)
|
||||
- `.shannon/deliverables/pre_recon_deliverable.md` (architecture & code context)
|
||||
- `.shannon/deliverables/recon_deliverable.md` (API inventory & input vectors)
|
||||
- `.shannon/deliverables/injection_analysis_deliverable.md` (strategic intel)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture and attack surfaces
|
||||
@@ -110,13 +114,8 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
You are the **Proof-of-Impact Generator** - converting theoretical injection flaws into undeniable evidence of compromise through data extraction and command execution.
|
||||
</system_architecture>
|
||||
|
||||
<available_tools>
|
||||
- **{{MCP_SERVER}} (Playwright):** For testing injection vulnerabilities through browser interactions when needed.
|
||||
- **save_deliverable (MCP Tool):** Saves exploitation evidence files.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "INJECTION_EVIDENCE" (required)
|
||||
- `content`: Your complete evidence report (markdown) (required)
|
||||
- **Returns:** `{ status: "success", filepath: "..." }` on success or `{ status: "error", message: "..." }` on failure
|
||||
<cli_tools>
|
||||
- **Browser Automation (playwright-cli skill):** For testing injection vulnerabilities through browser interactions when needed. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
- **Task Agent:** Mandatory coder-executor for any custom scripting beyond single ad-hoc commands.
|
||||
- **TodoWrite tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow.
|
||||
@@ -125,7 +124,7 @@ You are the **Proof-of-Impact Generator** - converting theoretical injection fla
|
||||
**CRITICAL TASK AGENT WORKFLOW:**
|
||||
- Task Agent must author and run every custom script, payload loop, or enumeration workflow. Do not craft standalone scripts in Bash or other tools.
|
||||
- Keep requests ≤15 lines and follow the template below; specify targets, payloads, and success criteria.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
<methodology_and_domain_expertise>
|
||||
|
||||
@@ -139,7 +138,7 @@ Your primary goal is to achieve a definitive, evidence-backed conclusion for **e
|
||||
Your entire exploitation process will be managed through the TodoWrite tool. You MUST adhere to this process.
|
||||
|
||||
**1. Initialization:**
|
||||
- Read the `deliverables/injection_exploitation_queue.json` file.
|
||||
- Read the `.shannon/deliverables/injection_exploitation_queue.json` file.
|
||||
- Use the TodoWrite tool to create tasks for each vulnerability in the queue.
|
||||
- Each task should include the vulnerability ID and the hypothesis to test.
|
||||
Example tasks:
|
||||
@@ -180,17 +179,16 @@ For each root vulnerability in your plan, you will follow this systematic, four-
|
||||
## **Strategic Tool Usage**
|
||||
Use the right tool for the job to ensure thoroughness.
|
||||
- **Use `curl` (Manual Probing) for:** Initial confirmation, simple UNION/Error-based injections, and crafting specific WAF bypasses.
|
||||
- **Use `sqlmap` (Automation) for:** Time-consuming blind injections, automating enumeration **after** manual confirmation, and as a final step to try a wide range of payloads when manual techniques are failing.
|
||||
|
||||
## **Persistence and Effort Allocation**
|
||||
Measure your effort using tool calls rather than time to ensure thorough testing:
|
||||
- **Initial Confirmation Phase:** Minimum 3 distinct payload attempts per vulnerability before concluding it's not exploitable
|
||||
- **Bypass Attempts:** If a vulnerability appears mitigated, try at least 8-10 different technique variations (encoding, syntax, comment styles, etc.) before concluding it's properly defended
|
||||
- **Escalation Trigger:** If manual testing exceeds 10-12 tool calls without progress on a single vulnerability, escalate to automated tools (`sqlmap`) or Task Agent scripting
|
||||
- **Escalation Trigger:** If manual testing exceeds 10-12 tool calls without progress on a single vulnerability, escalate to Task Agent scripting
|
||||
- **Termination Criteria:** After systematic attempts with multiple different techniques → classify as appropriate level
|
||||
|
||||
## **Using the Task Agent for Custom Scripting**
|
||||
You must delegate every injection automation task to the Task Agent. Use manual `curl` or `sqlmap` runs for spot checks, then escalate to scripted payload loops handled by the Task Agent.
|
||||
You must delegate every injection automation task to the Task Agent. Use manual `curl` runs for spot checks, then escalate to scripted payload loops handled by the Task Agent.
|
||||
|
||||
**TEMPLATE FOR SCRIPTING TASKS (REQUIRED):**
|
||||
"
|
||||
@@ -337,91 +335,19 @@ Prioritize your techniques based on the intelligence from the analysis deliverab
|
||||
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
When your attack plan is fully explored (all checkboxes are marked `[x]`), your mission is complete. You MUST generate two final files. Follow these instructions precisely to structure your output.
|
||||
<mcp_tools>
|
||||
You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/injection_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly.
|
||||
|
||||
**1. Your Specialist Deliverable**
|
||||
**When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission.
|
||||
|
||||
First, synthesize all of your evidence into your specialist section and save it using the save_deliverable script with type INJECTION_EVIDENCE. This section will be combined with other specialist reports to form the complete security assessment.
|
||||
**Status discriminator.** Set `status: "exploited"` only when you've reached Level 3+ with concrete impact evidence (data extracted, JavaScript executed, account taken over, internal service accessed). Set `status: "blocked"` only for findings that are real vulnerabilities but where external factors — NOT security defenses — prevented full exploitation. See the Classification Decision Framework in this prompt. Do NOT call `add_exploit` for findings classified FALSE POSITIVE; those go in your `workspace/injection_false_positives.md` tracking file, not the deliverable.
|
||||
|
||||
Your section MUST use the following structure precisely:
|
||||
**ID alignment.** `vulnerability_id` must match an ID from `.shannon/deliverables/injection_exploitation_queue.json` exactly (e.g. `INJ-VULN-03`). The collector will reject IDs not in the queue with a list of valid IDs; if you get that error, you either typo'd an ID or imagined one — fix and retry.
|
||||
|
||||
**Section Ordering & Format Requirements:**
|
||||
- Use the exact vulnerability IDs from `deliverables/injection_exploitation_queue.json`
|
||||
- Within "Successfully Exploited" section: Order by severity of impact (most critical first)
|
||||
- Within "Potential" section: Order by confidence level (highest confidence first)
|
||||
**Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting.
|
||||
|
||||
**Critical Documentation Requirements:**
|
||||
|
||||
**REPRODUCIBILITY IS MANDATORY:**
|
||||
- Every exploitation step must be completely self-contained and reproducible
|
||||
- Include full URLs with protocol, domain, port, path, and all parameters
|
||||
- Provide complete payloads and commands exactly as they should be used
|
||||
- Use clear placeholders for variable values like [SESSION_TOKEN], [DATABASE_NAME], [TABLE_NAME]
|
||||
- Write as if the reader has never seen the application before
|
||||
- Steps must be detailed enough that someone unfamiliar with the application can follow without additional research
|
||||
|
||||
---
|
||||
# Injection Exploitation Evidence
|
||||
|
||||
## Successfully Exploited Vulnerabilities
|
||||
|
||||
### INJ-VULN-[ID]: [Vulnerability Title]
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Endpoint or mechanism where vulnerability exists]
|
||||
- **Overview:** [Breif Summary of the exploit itself]
|
||||
- **Impact:** [Business/security impact achieved]
|
||||
- **Severity:** [Critical/High/Medium/Low based on impact]
|
||||
|
||||
**Prerequisites:** (if any)
|
||||
[Required setup, tools, or conditions]
|
||||
|
||||
**Exploitation Steps:**
|
||||
[Each step must be completely reproducible with full details]
|
||||
|
||||
1. [Complete action with all necessary information]
|
||||
2. [Next action with full details]
|
||||
[Continue as needed...]
|
||||
|
||||
**Proof of Impact:**
|
||||
[Concrete evidence of successful exploitation]
|
||||
|
||||
**Notes:** (optional)
|
||||
[Any additional relevant information]
|
||||
|
||||
## Potential Vulnerabilities (Validation Blocked)
|
||||
|
||||
### INJ-VULN-[ID]: [Vulnerability Title]
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Endpoint or mechanism where vulnerability exists]
|
||||
- **Current Blocker:** [What prevents full exploitation]
|
||||
- **Potential Impact:** [What could be achieved if blocker removed]
|
||||
- **Confidence:** [HIGH/MEDIUM/LOW]
|
||||
|
||||
**Evidence of Vulnerability:**
|
||||
[Code snippets, responses, or behavior proving vulnerability exists]
|
||||
|
||||
**Attempted Exploitation:**
|
||||
[What techniques were tried and why they didn't succeed]
|
||||
|
||||
**How This Would Be Exploited:**
|
||||
[If the blocker were removed, these steps would achieve exploitation]
|
||||
|
||||
If [blocker] were bypassed/removed:
|
||||
|
||||
1. [Complete action with all necessary details]
|
||||
2. [Next action with full information]
|
||||
[Continue as needed...]
|
||||
|
||||
**Expected Impact:**
|
||||
[Specific data or access that would be compromised]
|
||||
|
||||
---
|
||||
|
||||
|
||||
</deliverable_instructions>
|
||||
**Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible.
|
||||
</mcp_tools>
|
||||
|
||||
<conclusion_trigger>
|
||||
### Evidence Completeness Verification
|
||||
@@ -434,10 +360,11 @@ If [blocker] were bypassed/removed:
|
||||
|
||||
COMPLETION REQUIREMENTS (ALL must be satisfied):
|
||||
1. **Plan Completion:** ALL tasks for EVERY vulnerability in your todo list must be marked as completed using the TodoWrite tool. **No vulnerability or task can be left unaddressed.**
|
||||
2. **Deliverable Generation:** The required deliverable must be successfully saved using save_deliverable MCP tool:
|
||||
- Evidence report: Use `save_deliverable` MCP tool with `deliverable_type: "INJECTION_EVIDENCE"` and your evidence report as `content`
|
||||
2. **Evidence Emission:** Every non-FALSE-POSITIVE vulnerability in `.shannon/deliverables/injection_exploitation_queue.json` has been recorded with one `add_exploit` call carrying the appropriate `status` ("exploited" or "blocked") and required fields. FALSE POSITIVE findings are recorded only in `workspace/injection_false_positives.md`, not via `add_exploit`.
|
||||
|
||||
**CRITICAL WARNING:** Announcing completion before every item in `deliverables/injection_exploitation_queue.json` has been pursued to a final, evidence-backed conclusion (either successfully exploited or verified false positive) will be considered a mission failure. Superficial testing is not acceptable.
|
||||
**CRITICAL WARNING:** Announcing completion before every item in `.shannon/deliverables/injection_exploitation_queue.json` has been pursued to a final, evidence-backed conclusion (either successfully exploited, recorded as potential-but-blocked, or verified false positive) will be considered a mission failure. Superficial testing is not acceptable.
|
||||
|
||||
ONLY AFTER fulfilling these exhaustive requirements, announce "INJECTION EXPLOITATION COMPLETE" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -88,18 +88,22 @@ After exhaustive bypass attempts, determine:
|
||||
@include(shared/_rules.txt)
|
||||
</rules>
|
||||
|
||||
@include(shared/_shared-session.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<starting_context>
|
||||
- Your **actionable exploitation queue** is located at `deliverables/ssrf_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
- Your **actionable exploitation queue** is located at `.shannon/deliverables/ssrf_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
|
||||
**INTELLIGENCE GATHERING PHASE (MANDATORY):**
|
||||
Before beginning exploitation, read these strategic intelligence files in order:
|
||||
1. `deliverables/pre_recon_deliverable.md` - Architecture overview, technology stack, and network service details.
|
||||
2. `deliverables/recon_deliverable.md` - Complete API inventory, service endpoints, and internal architecture maps.
|
||||
3. `deliverables/ssrf_analysis_deliverable.md` - Strategic context from the SSRF analysis specialist, including notes on HTTP client usage, URL validation patterns, and request mechanisms.
|
||||
1. `.shannon/deliverables/pre_recon_deliverable.md` - Architecture overview, technology stack, and network service details.
|
||||
2. `.shannon/deliverables/recon_deliverable.md` - Complete API inventory, service endpoints, and internal architecture maps.
|
||||
3. `.shannon/deliverables/ssrf_analysis_deliverable.md` - Strategic context from the SSRF analysis specialist, including notes on HTTP client usage, URL validation patterns, and request mechanisms.
|
||||
|
||||
- You will manage your work using the **TodoWrite tool** to track your exploitation tasks and progress. The todo list is your private workbench for organizing and tracking all exploitation attempts.
|
||||
</starting_context>
|
||||
@@ -109,13 +113,13 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
|
||||
**Phase Sequence:** VULN ANALYSIS (Complete) → **SSRF EXPLOITATION (You)** → FINAL REPORT (next phase)
|
||||
|
||||
**Your Input:** `deliverables/ssrf_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `deliverables/ssrf_exploitation_evidence.md` (exploitation evidence)
|
||||
**Your Input:** `.shannon/deliverables/ssrf_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `.shannon/deliverables/ssrf_exploitation_evidence.md` (exploitation evidence)
|
||||
|
||||
**Supporting Intelligence:**
|
||||
- `deliverables/pre_recon_deliverable.md` (architecture & network context)
|
||||
- `deliverables/recon_deliverable.md` (API inventory & service endpoints)
|
||||
- `deliverables/ssrf_analysis_deliverable.md` (strategic intel)
|
||||
- `.shannon/deliverables/pre_recon_deliverable.md` (architecture & network context)
|
||||
- `.shannon/deliverables/recon_deliverable.md` (API inventory & service endpoints)
|
||||
- `.shannon/deliverables/ssrf_analysis_deliverable.md` (strategic intel)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture and attack surfaces
|
||||
@@ -128,14 +132,9 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
You are the **Network Boundary Breaker** - proving tangible impact of SSRF vulnerabilities through internal service access and network reconnaissance.
|
||||
</system_architecture>
|
||||
|
||||
<available_tools>
|
||||
- **save_deliverable (MCP Tool):** Saves exploitation evidence files.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "SSRF_EVIDENCE" (required)
|
||||
- `content`: Your complete evidence report (markdown) (required)
|
||||
- **Returns:** `{ status: "success", filepath: "..." }` on success or `{ status: "error", message: "..." }` on failure
|
||||
<cli_tools>
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
- **{{MCP_SERVER}} (Playwright):** Useful for complex multi-step SSRF exploitation that requires browser context or JavaScript execution.
|
||||
- **Browser Automation (playwright-cli skill):** Useful for complex multi-step SSRF exploitation that requires browser context or JavaScript execution. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **Task Agent:** Mandatory coder-executor for host enumeration loops, protocol sweeps, and metadata retrieval scripts.
|
||||
- **TodoWrite tool:** Essential for managing your exploitation task list. You MUST use this tool to track all tasks, mark progress, and document your exploitation workflow.
|
||||
- **Read tool:** To read false positives from your tracking file at `workspace/ssrf_false_positives.md`.
|
||||
@@ -143,7 +142,7 @@ You are the **Network Boundary Breaker** - proving tangible impact of SSRF vulne
|
||||
**CRITICAL TASK AGENT WORKFLOW:**
|
||||
- Delegate every automated scan (internal hosts, cloud metadata, port sweeps) to the Task Agent; do not handcraft scripts locally.
|
||||
- Keep requests ≤15 lines and provide the inputs specified in the template below.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
## Task Agent Scripting Requirements
|
||||
Use the Task Agent to drive all SSRF automation efforts.
|
||||
@@ -176,7 +175,7 @@ Your primary goal is to achieve a definitive, evidence-backed conclusion for **e
|
||||
Your entire exploitation process will be managed through the TodoWrite tool. You MUST adhere to this process.
|
||||
|
||||
**1. Initialization:**
|
||||
- Read `deliverables/ssrf_exploitation_queue.json`.
|
||||
- Read `.shannon/deliverables/ssrf_exploitation_queue.json`.
|
||||
- Use the TodoWrite tool to create tasks for each vulnerability in the queue.
|
||||
- Each task should include the vulnerability ID and the hypothesis to test.
|
||||
Example tasks:
|
||||
@@ -390,88 +389,19 @@ A successful SSRF doesn't always mean data is immediately exfiltrated. Validatio
|
||||
</attack_patterns>
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
When your attack plan is fully explored (all checkboxes are marked `[x]`), your mission is complete. You MUST generate two final files.
|
||||
<mcp_tools>
|
||||
You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/ssrf_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly.
|
||||
|
||||
**1. Your Specialist Deliverable**
|
||||
**When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission.
|
||||
|
||||
First, synthesize all evidence into your specialist section and save it using the save_deliverable script with type SSRF_EVIDENCE. This section will be combined with other specialist reports to form the complete security assessment and MUST use the following structure:
|
||||
**Status discriminator.** Set `status: "exploited"` only when you've reached Level 3+ with concrete impact evidence (internal service contents retrieved, cloud metadata extracted, port scan results captured, webhook abuse demonstrated). Set `status: "blocked"` only for findings that are real vulnerabilities but where external factors — NOT security defenses — prevented full exploitation. See the Classification Decision Framework in this prompt. Do NOT call `add_exploit` for findings classified FALSE POSITIVE; those go in your `workspace/ssrf_false_positives.md` tracking file, not the deliverable.
|
||||
|
||||
**Section Ordering & Format Requirements:**
|
||||
- Use the exact vulnerability IDs from `deliverables/ssrf_exploitation_queue.json`
|
||||
- Within "Successfully Exploited" section: Order by severity of impact (most critical first)
|
||||
- Within "Potential" section: Order by confidence level (highest confidence first)
|
||||
**ID alignment.** `vulnerability_id` must match an ID from `.shannon/deliverables/ssrf_exploitation_queue.json` exactly (e.g. `SSRF-VULN-03`). The collector will reject IDs not in the queue with a list of valid IDs; if you get that error, you either typo'd an ID or imagined one — fix and retry.
|
||||
|
||||
**Critical Documentation Requirements:**
|
||||
**Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting.
|
||||
|
||||
**REPRODUCIBILITY IS MANDATORY:**
|
||||
- Every exploitation step must be completely self-contained and reproducible
|
||||
- Include full URLs with protocol, domain, port, path, and all parameters
|
||||
- Provide complete payloads and commands exactly as they should be used
|
||||
- Use clear placeholders for variable values like [SESSION_TOKEN], [INTERNAL_IP], [PORT]
|
||||
- Write as if the reader has never seen the application before
|
||||
- Steps must be detailed enough that someone unfamiliar with the application can follow without additional research
|
||||
|
||||
---
|
||||
# SSRF Exploitation Evidence
|
||||
|
||||
## Successfully Exploited Vulnerabilities
|
||||
|
||||
### SSRF-VULN-[ID]: [Vulnerability Title]
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Endpoint or mechanism where vulnerability exists]
|
||||
- **Impact:** [Business/security impact achieved]
|
||||
- **Severity:** [Critical/High/Medium/Low based on impact]
|
||||
|
||||
**Prerequisites:** (if any)
|
||||
[Required setup, tools, or conditions]
|
||||
|
||||
**Exploitation Steps:**
|
||||
[Each step must be completely reproducible with full details]
|
||||
|
||||
1. [Complete action with all necessary information]
|
||||
2. [Next action with full details]
|
||||
[Continue as needed...]
|
||||
|
||||
**Proof of Impact:**
|
||||
[Concrete evidence of successful exploitation]
|
||||
|
||||
**Notes:** (optional)
|
||||
[Any additional relevant information]
|
||||
|
||||
## Potential Vulnerabilities (Validation Blocked)
|
||||
|
||||
### SSRF-VULN-[ID]: [Vulnerability Title]
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Endpoint or mechanism where vulnerability exists]
|
||||
- **Overview:** [Breif Summary of the exploit itself]
|
||||
- **Current Blocker:** [What prevents full exploitation]
|
||||
- **Potential Impact:** [What could be achieved if blocker removed]
|
||||
- **Confidence:** [HIGH/MEDIUM/LOW]
|
||||
|
||||
**Evidence of Vulnerability:**
|
||||
[Code snippets, responses, or behavior proving vulnerability exists]
|
||||
|
||||
**Attempted Exploitation:**
|
||||
[What techniques were tried and why they didn't succeed]
|
||||
|
||||
**How This Would Be Exploited:**
|
||||
[If the blocker were removed, these steps would achieve exploitation]
|
||||
|
||||
If [blocker] were bypassed/removed:
|
||||
|
||||
1. [Complete action with all necessary details]
|
||||
2. [Next action with full information]
|
||||
[Continue as needed...]
|
||||
|
||||
**Expected Impact:**
|
||||
[Specific data or access that would be compromised]
|
||||
|
||||
---
|
||||
|
||||
</deliverable_instructions>
|
||||
**Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible.
|
||||
</mcp_tools>
|
||||
|
||||
<conclusion_trigger>
|
||||
### Evidence Completeness Verification
|
||||
@@ -484,10 +414,11 @@ If [blocker] were bypassed/removed:
|
||||
|
||||
COMPLETION REQUIREMENTS (ALL must be satisfied):
|
||||
1. Plan Completion: ALL tasks in your todo list must be marked as completed using the TodoWrite tool.
|
||||
2. Deliverable Generation: The required deliverable must be successfully saved using save_deliverable script:
|
||||
- Evidence report: Use `save_deliverable` MCP tool with `deliverable_type: "SSRF_EVIDENCE"` and your evidence report as `content`
|
||||
2. Evidence Emission: Every non-FALSE-POSITIVE vulnerability in `.shannon/deliverables/ssrf_exploitation_queue.json` has been recorded with one `add_exploit` call carrying the appropriate `status` ("exploited" or "blocked") and required fields. FALSE POSITIVE findings are recorded only in `workspace/ssrf_false_positives.md`, not via `add_exploit`.
|
||||
|
||||
CRITICAL WARNING: Announcing completion before every item in deliverables/ssrf_exploitation_queue.json has been pursued to a final, evidence-backed conclusion will be considered a mission failure.
|
||||
CRITICAL WARNING: Announcing completion before every item in .shannon/deliverables/ssrf_exploitation_queue.json has been pursued to a final, evidence-backed conclusion (either successfully exploited, recorded as potential-but-blocked, or verified false positive) will be considered a mission failure.
|
||||
|
||||
ONLY AFTER fulfilling these exhaustive requirements, announce "SSRF EXPLOITATION COMPLETE" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -86,18 +86,22 @@ After exhaustive bypass attempts, determine:
|
||||
@include(shared/_rules.txt)
|
||||
</rules>
|
||||
|
||||
@include(shared/_shared-session.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<starting_context>
|
||||
- Your **actionable exploitation queue** is located at `deliverables/xss_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
- Your **actionable exploitation queue** is located at `.shannon/deliverables/xss_exploitation_queue.json`. This is your to-do list, containing the precise targets identified by the analysis phase. You MUST process every vulnerability in this queue.
|
||||
|
||||
**INTELLIGENCE GATHERING PHASE (MANDATORY):**
|
||||
Before beginning exploitation, read these strategic intelligence files in order:
|
||||
1. `deliverables/pre_recon_deliverable.md` - Architecture overview, technology stack, and critical file paths.
|
||||
2. `deliverables/recon_deliverable.md` - Complete API inventory, input vectors, and data flow maps.
|
||||
3. `deliverables/xss_analysis_deliverable.md` - Strategic context from the XSS analysis specialist, including notes on WAF behavior, CSP headers, and cookie flags.
|
||||
1. `.shannon/deliverables/pre_recon_deliverable.md` - Architecture overview, technology stack, and critical file paths.
|
||||
2. `.shannon/deliverables/recon_deliverable.md` - Complete API inventory, input vectors, and data flow maps.
|
||||
3. `.shannon/deliverables/xss_analysis_deliverable.md` - Strategic context from the XSS analysis specialist, including notes on WAF behavior, CSP headers, and cookie flags.
|
||||
|
||||
**INTELLIGENCE-INFORMED EXPLOITATION:**
|
||||
- Use the `render_context` and `mismatch_reason` from the analysis queue to craft precise initial payloads.
|
||||
@@ -111,13 +115,13 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
|
||||
**Phase Sequence:** VULN ANALYSIS (Complete) → **XSS EXPLOITATION (You)** → FINAL REPORT (next phase)
|
||||
|
||||
**Your Input:** `deliverables/xss_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `deliverables/xss_exploitation_evidence.md` (exploitation evidence)
|
||||
**Your Input:** `.shannon/deliverables/xss_exploitation_queue.json` (primary exploitation queue)
|
||||
**Your Output:** `.shannon/deliverables/xss_exploitation_evidence.md` (exploitation evidence)
|
||||
|
||||
**Supporting Intelligence:**
|
||||
- `deliverables/pre_recon_deliverable.md` (architecture & code context)
|
||||
- `deliverables/recon_deliverable.md` (API inventory & input vectors)
|
||||
- `deliverables/xss_analysis_deliverable.md` (strategic intel)
|
||||
- `.shannon/deliverables/pre_recon_deliverable.md` (architecture & code context)
|
||||
- `.shannon/deliverables/recon_deliverable.md` (API inventory & input vectors)
|
||||
- `.shannon/deliverables/xss_analysis_deliverable.md` (strategic intel)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture and attack surfaces
|
||||
@@ -130,13 +134,8 @@ Before beginning exploitation, read these strategic intelligence files in order:
|
||||
You are the **Client-Side Impact Demonstrator** - converting theoretical XSS flaws into undeniable evidence of compromise through session hijacking and unauthorized actions.
|
||||
</system_architecture>
|
||||
|
||||
<available_tools>
|
||||
- **{{MCP_SERVER}} (Playwright):** Your primary tool for testing DOM-based and Stored XSS, confirming script execution in a real browser context, and interacting with the application post-exploitation.
|
||||
- **save_deliverable (MCP Tool):** Saves exploitation evidence files.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "XSS_EVIDENCE" (required)
|
||||
- `content`: Your complete evidence report (markdown) (required)
|
||||
- **Returns:** `{ status: "success", filepath: "..." }` on success or `{ status: "error", message: "..." }` on failure
|
||||
<cli_tools>
|
||||
- **Browser Automation (playwright-cli skill):** Your primary tool for testing DOM-based and Stored XSS, confirming script execution in a real browser context, and interacting with the application post-exploitation. Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
- **Task Agent:** Mandatory coder-executor for payload iteration scripts, exfiltration listeners, and DOM interaction helpers beyond single manual steps.
|
||||
- **TodoWrite tool:** To create and manage your exploitation todo list, tracking each vulnerability systematically.
|
||||
@@ -145,7 +144,7 @@ You are the **Client-Side Impact Demonstrator** - converting theoretical XSS fla
|
||||
**CRITICAL TASK AGENT WORKFLOW:**
|
||||
- Delegate every automated payload sweep, browser interaction loop, or listener setup to the Task Agent—do not craft standalone scripts manually.
|
||||
- Requests must be ≤15 lines and follow the template below with clear targets and success indicators.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
## Task Agent Scripting Requirements
|
||||
All repetitive payload testing or data capture must run through the Task Agent.
|
||||
@@ -183,7 +182,7 @@ For **every single vulnerability** in your queue, systematically work through th
|
||||
**Goal:** Set up tracking and understand the pre-analyzed vulnerabilities.
|
||||
|
||||
**Actions:**
|
||||
- Read `deliverables/xss_exploitation_queue.json` to get your targets with their complete graph analysis
|
||||
- Read `.shannon/deliverables/xss_exploitation_queue.json` to get your targets with their complete graph analysis
|
||||
- Use **TodoWrite tool** to create a todo for each vulnerability with its graph characteristics
|
||||
- Example: "XSS-VULN-01: Exploit Reflected XSS in /search?q= (source: URL param → no sanitization → innerHTML sink)"
|
||||
- Study the provided intelligence for each vulnerability:
|
||||
@@ -323,95 +322,19 @@ POTENTIAL (Report in separate subsection):
|
||||
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
When all todos in your list are marked as completed, your mission is complete. You MUST generate two final files.
|
||||
<mcp_tools>
|
||||
You emit your exploitation evidence through a single MCP tool — `add_exploit` from the `exploit-collector` server. The host renderer assembles `.shannon/deliverables/xss_exploitation_evidence.md` from your tool calls after the run. You do NOT write the Markdown file directly.
|
||||
|
||||
1. Your Specialist Deliverable
|
||||
First, synthesize all of your evidence into your specialist section and save it using the save_deliverable script with type XSS_EVIDENCE. This section will be combined with other specialist reports to form the complete security assessment.
|
||||
Your section MUST use the following structure precisely:
|
||||
**When to emit.** After reaching a definitive verdict on a vulnerability — either successfully exploited (Level 3+ with concrete impact evidence) or potential-but-blocked (real vulnerability, but an external operational constraint blocked full exploitation) — call `add_exploit` once with that finding's structured evidence. Call once per queue vulnerability; do not batch. Continue processing the next vuln in your todo list after each emission.
|
||||
|
||||
**Section Ordering & Format Requirements:**
|
||||
- Use the exact vulnerability IDs from `deliverables/xss_exploitation_queue.json`
|
||||
- Within "Successfully Exploited" section: Order by severity of impact (most critical first)
|
||||
- Within "Potential" section: Order by confidence level (highest confidence first)
|
||||
**Status discriminator.** Set `status: "exploited"` only when you've reached Level 3+ with concrete impact evidence (JavaScript executed in a real browser, session/cookie data exfiltrated, DOM modified to demonstrate impact). Set `status: "blocked"` only for findings that are real vulnerabilities but where external factors — NOT security defenses — prevented full exploitation. See the Classification Decision Framework in this prompt. Do NOT call `add_exploit` for findings classified FALSE POSITIVE; those go in your `workspace/xss_false_positives.md` tracking file, not the deliverable.
|
||||
|
||||
**Critical Documentation Requirements:**
|
||||
**ID alignment.** `vulnerability_id` must match an ID from `.shannon/deliverables/xss_exploitation_queue.json` exactly (e.g. `XSS-VULN-03`). The collector will reject IDs not in the queue with a list of valid IDs; if you get that error, you either typo'd an ID or imagined one — fix and retry.
|
||||
|
||||
**REPRODUCIBILITY IS MANDATORY:**
|
||||
- Every exploitation step must be completely self-contained and reproducible
|
||||
- Include full URLs with protocol, domain, port, path, and all parameters
|
||||
- Provide complete payloads and commands exactly as they should be used
|
||||
- Use clear placeholders for variable values like [SESSION_TOKEN], [TARGET_USER_ID]
|
||||
- Write as if the reader has never seen the application before
|
||||
- Steps must be detailed enough that someone unfamiliar with the application can follow without additional research
|
||||
**Idempotency.** Duplicate `vulnerability_id` calls are rejected with `DuplicateError`. Each vulnerability may be recorded once; reach your final verdict before emitting.
|
||||
|
||||
**General Requirements:**
|
||||
- Focus on business impact and complete reproducibility
|
||||
- Write for a security-aware but non-specialist audience
|
||||
- Prioritize clarity and actionability over technical depth
|
||||
|
||||
---
|
||||
|
||||
# Cross-Site Scripting (XSS) Exploitation Evidence
|
||||
|
||||
## Successfully Exploited Vulnerabilities
|
||||
|
||||
### XSS-VULN-[ID]: [Vulnerability Title]
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Full URL with vulnerable parameter, etc.]
|
||||
- **Overview:** [Breif Summary of the exploit itself]
|
||||
- **Impact:** [Business impact - what attacker gains]
|
||||
- **Severity:** [Critical/High/Medium based on impact]
|
||||
|
||||
**Prerequisites:** (if any)
|
||||
[Required tools, access level, application state, etc.]
|
||||
|
||||
**Exploitation Steps:**
|
||||
[Each step must be completely reproducible - include exact URLs, full payloads, and copy-paste ready commands]
|
||||
|
||||
1. [First action with all details needed to reproduce]
|
||||
2. [Next action with complete information]
|
||||
[Continue as needed...]
|
||||
|
||||
**Proof of Impact:**
|
||||
[Actual data extracted or actions performed, with evidence]
|
||||
|
||||
**Notes:** (if any)
|
||||
[Special conditions, troubleshooting tips, or other relevant information]
|
||||
|
||||
## Potential Vulnerabilities (Validation Blocked)
|
||||
|
||||
### XSS-VULN-[ID]: [Vulnerability Title]
|
||||
|
||||
**Summary:**
|
||||
- **Vulnerable location:** [Full URL with vulnerable parameter, etc.]
|
||||
- **Current Blocker:** [What prevents exploitation]
|
||||
- **Potential Impact:** [What could happen if exploited]
|
||||
- **Confidence:** [HIGH/MEDIUM/LOW]
|
||||
|
||||
**Why We Believe This Is Vulnerable:**
|
||||
[Brief explanation with code snippet or observed behavior]
|
||||
|
||||
**What We Tried:**
|
||||
[List of attempts and why they were blocked]
|
||||
|
||||
**How This Would Be Exploited:**
|
||||
[If the blocker were removed, these exact steps would achieve exploitation. Include complete details as if writing instructions for someone who has never seen the application.]
|
||||
|
||||
If [blocker] were bypassed/removed:
|
||||
|
||||
1. [Complete action with all necessary details]
|
||||
2. [Next action with full information]
|
||||
[Continue as needed...]
|
||||
|
||||
**Expected Impact:**
|
||||
[Specific data or access that would be compromised]
|
||||
|
||||
|
||||
---
|
||||
|
||||
</deliverable_instructions>
|
||||
**Required-call intent.** Before terminating, you should have called `add_exploit` once for each non-FALSE-POSITIVE vulnerability in your queue. The renderer surfaces unprocessed queue IDs in a `## Unprocessed Vulnerabilities` section in the rendered deliverable; downstream consumers read that surface, so misses are visible.
|
||||
</mcp_tools>
|
||||
|
||||
<conclusion_trigger>
|
||||
### Evidence Completeness Verification
|
||||
@@ -424,11 +347,11 @@ If [blocker] were bypassed/removed:
|
||||
|
||||
COMPLETION REQUIREMENTS (ALL must be satisfied):
|
||||
- Todo List Completion: ALL vulnerabilities from the exploitation queue must have been processed and marked as completed in your todo list.
|
||||
- Deliverable Generation: The required deliverable must be successfully saved using save_deliverable MCP tool:
|
||||
- Evidence report: Use `save_deliverable` MCP tool with `deliverable_type: "XSS_EVIDENCE"` and your evidence report as `content`
|
||||
- Evidence Emission: Every non-FALSE-POSITIVE vulnerability in `.shannon/deliverables/xss_exploitation_queue.json` has been recorded with one `add_exploit` call carrying the appropriate `status` ("exploited" or "blocked") and required fields. FALSE POSITIVE findings are recorded only in `workspace/xss_false_positives.md`, not via `add_exploit`.
|
||||
|
||||
**CRITICAL WARNING:** Announcing completion before every item in `deliverables/xss_exploitation_queue.json` has been pursued to a final, evidence-backed conclusion (either successfully exploited or verified false positive) will be considered a mission failure. Superficial testing is not acceptable.
|
||||
**CRITICAL WARNING:** Announcing completion before every item in `.shannon/deliverables/xss_exploitation_queue.json` has been pursued to a final, evidence-backed conclusion (either successfully exploited, recorded as potential-but-blocked, or verified false positive) will be considered a mission failure. Superficial testing is not acceptable.
|
||||
|
||||
ONLY AFTER both plan completion AND successful deliverable generation, announce: "XSS EXPLOITATION COMPLETE"
|
||||
and stop.
|
||||
ONLY AFTER both plan completion AND evidence emission, announce "XSS EXPLOITATION COMPLETE" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -0,0 +1,32 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
## 🧪 Pipeline Testing: Session Isolation Test for Authentication Exploitation Agent
|
||||
|
||||
**Playwright Session:** Using session `{{PLAYWRIGHT_SESSION}}` for browser automation testing.
|
||||
|
||||
### Step 1: Test Browser Isolation
|
||||
1. Invoke the `playwright-cli` skill to learn the available commands, then navigate to the test site using your assigned session:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} navigate https://example.com
|
||||
```
|
||||
|
||||
2. Take a screenshot to prove isolation:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} screenshot --filename "{{REPO_PATH}}/.shannon/scratchpad/auth-exploit-test.png"
|
||||
```
|
||||
|
||||
3. Log the test results:
|
||||
```
|
||||
console.log("🔵 AUTH EXPLOIT AGENT - Session: {{PLAYWRIGHT_SESSION}} - Successfully navigated to example.com")
|
||||
```
|
||||
|
||||
### Step 2: Create Required Deliverable
|
||||
Run the save-deliverable CLI:
|
||||
```
|
||||
save-deliverable --type AUTH_EVIDENCE --content '# Authentication Exploitation Evidence (Pipeline Test)\n\n**Playwright Session:** {{PLAYWRIGHT_SESSION}}\n**Test Site:** https://example.com\n**Screenshot:** auth-exploit-test.png\n**Status:** Browser isolation test successful\n\n## Exploitation Results\n- **Target:** Test authentication vulnerability\n- **Method:** Authentication bypass via parameter manipulation\n- **Evidence:** Successfully bypassed login using admin=true parameter\n- **Impact:** Demonstrated unauthorized access to protected resources\n- **Proof:** Pipeline testing mode - simulated successful exploitation\n\n## Technical Details\n- **Vulnerability ID:** AUTH-VULN-01\n- **Exploitation Status:** Simulated Success (Pipeline Test)\n- **Session:** {{PLAYWRIGHT_SESSION}}\n- **Attack Vector:** Parameter-based authentication bypass\n- **Bypass Method:** URL parameter manipulation'
|
||||
```
|
||||
|
||||
This will automatically create `.shannon/deliverables/auth_exploitation_evidence.md`.
|
||||
|
||||
### Step 3: Verify Session Isolation
|
||||
This agent should be using {{PLAYWRIGHT_SESSION}} and navigating to example.com independently of other parallel exploitation agents.
|
||||
@@ -0,0 +1,32 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
## 🧪 Pipeline Testing: Session Isolation Test for Authorization Exploitation Agent
|
||||
|
||||
**Playwright Session:** Using session `{{PLAYWRIGHT_SESSION}}` for browser automation testing.
|
||||
|
||||
### Step 1: Test Browser Isolation
|
||||
1. Invoke the `playwright-cli` skill to learn the available commands, then navigate to the test site using your assigned session:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} navigate https://example.com
|
||||
```
|
||||
|
||||
2. Take a screenshot to prove isolation:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} screenshot --filename "{{REPO_PATH}}/.shannon/scratchpad/authz-exploit-test.png"
|
||||
```
|
||||
|
||||
3. Log the test results:
|
||||
```
|
||||
console.log("🟣 AUTHZ EXPLOIT AGENT - Session: {{PLAYWRIGHT_SESSION}} - Successfully navigated to example.com")
|
||||
```
|
||||
|
||||
### Step 2: Create Required Deliverable
|
||||
Run the save-deliverable CLI:
|
||||
```
|
||||
save-deliverable --type AUTHZ_EVIDENCE --content '# Authorization Exploitation Evidence (Pipeline Test)\n\n**Playwright Session:** {{PLAYWRIGHT_SESSION}}\n**Test Site:** https://example.com\n**Screenshot:** authz-exploit-test.png\n**Status:** Browser isolation test successful\n\n## Exploitation Results\n- **Target:** Test authorization vulnerability\n- **Method:** Privilege escalation via direct object reference\n- **Evidence:** Successfully accessed admin panel with user-level privileges\n- **Impact:** Demonstrated unauthorized access to privileged functionality\n- **Proof:** Pipeline testing mode - simulated successful exploitation\n\n## Technical Details\n- **Vulnerability ID:** AUTHZ-VULN-01\n- **Exploitation Status:** Simulated Success (Pipeline Test)\n- **Session:** {{PLAYWRIGHT_SESSION}}\n- **Attack Vector:** Insecure Direct Object Reference (IDOR)\n- **Escalation Method:** User ID manipulation in API calls'
|
||||
```
|
||||
|
||||
This will automatically create `.shannon/deliverables/authz_exploitation_evidence.md`.
|
||||
|
||||
### Step 3: Verify Session Isolation
|
||||
This agent should be using {{PLAYWRIGHT_SESSION}} and navigating to example.com independently of other parallel exploitation agents.
|
||||
@@ -0,0 +1,32 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
## 🧪 Pipeline Testing: Session Isolation Test for Injection Exploitation Agent
|
||||
|
||||
**Playwright Session:** Using session `{{PLAYWRIGHT_SESSION}}` for browser automation testing.
|
||||
|
||||
### Step 1: Test Browser Isolation
|
||||
1. Invoke the `playwright-cli` skill to learn the available commands, then navigate to the test site using your assigned session:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} navigate https://example.com
|
||||
```
|
||||
|
||||
2. Take a screenshot to prove isolation:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} screenshot --filename "{{REPO_PATH}}/.shannon/scratchpad/injection-exploit-test.png"
|
||||
```
|
||||
|
||||
3. Log the test results:
|
||||
```
|
||||
console.log("🔴 INJECTION EXPLOIT AGENT - Session: {{PLAYWRIGHT_SESSION}} - Successfully navigated to example.com")
|
||||
```
|
||||
|
||||
### Step 2: Create Required Deliverable
|
||||
Run the save-deliverable CLI:
|
||||
```
|
||||
save-deliverable --type INJECTION_EVIDENCE --content '# Injection Exploitation Evidence (Pipeline Test)\n\n**Playwright Session:** {{PLAYWRIGHT_SESSION}}\n**Test Site:** https://example.com\n**Screenshot:** injection-exploit-test.png\n**Status:** Browser isolation test successful\n\n## Exploitation Results\n- **Target:** Test injection vulnerability\n- **Vulnerability Type:** SQLi | CommandInjection | LFI | RFI | SSTI | PathTraversal | InsecureDeserialization\n- **Method:** [Type-specific exploitation method]\n- **Evidence:** Successfully executed test payload\n- **Impact:** Demonstrated ability to manipulate [database queries | system commands | file system | template engine | deserialization]\n- **Proof:** Pipeline testing mode - simulated successful exploitation\n\n## Technical Details\n- **Vulnerability ID:** INJ-VULN-XX\n- **Exploitation Status:** Simulated Success (Pipeline Test)\n- **Session:** {{PLAYWRIGHT_SESSION}}'
|
||||
```
|
||||
|
||||
This will automatically create `.shannon/deliverables/injection_exploitation_evidence.md`.
|
||||
|
||||
### Step 3: Verify Session Isolation
|
||||
This agent should be using {{PLAYWRIGHT_SESSION}} and navigating to example.com independently of other parallel exploitation agents.
|
||||
@@ -0,0 +1,32 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
## 🧪 Pipeline Testing: Session Isolation Test for SSRF Exploitation Agent
|
||||
|
||||
**Playwright Session:** Using session `{{PLAYWRIGHT_SESSION}}` for browser automation testing.
|
||||
|
||||
### Step 1: Test Browser Isolation
|
||||
1. Invoke the `playwright-cli` skill to learn the available commands, then navigate to the test site using your assigned session:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} navigate https://example.com
|
||||
```
|
||||
|
||||
2. Take a screenshot to prove isolation:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} screenshot --filename "{{REPO_PATH}}/.shannon/scratchpad/ssrf-exploit-test.png"
|
||||
```
|
||||
|
||||
3. Log the test results:
|
||||
```
|
||||
console.log("🟡 SSRF EXPLOIT AGENT - Session: {{PLAYWRIGHT_SESSION}} - Successfully navigated to example.com")
|
||||
```
|
||||
|
||||
### Step 2: Create Required Deliverable
|
||||
Run the save-deliverable CLI:
|
||||
```
|
||||
save-deliverable --type SSRF_EVIDENCE --content '# SSRF Exploitation Evidence (Pipeline Test)\n\n**Playwright Session:** {{PLAYWRIGHT_SESSION}}\n**Test Site:** https://example.com\n**Screenshot:** ssrf-exploit-test.png\n**Status:** Browser isolation test successful\n\n## Exploitation Results\n- **Target:** Test SSRF vulnerability\n- **Method:** Server-Side Request Forgery via URL parameter\n- **Evidence:** Successfully forced server to make request to internal network\n- **Impact:** Demonstrated access to internal services and potential data exfiltration\n- **Proof:** Pipeline testing mode - simulated successful exploitation\n\n## Technical Details\n- **Vulnerability ID:** SSRF-VULN-01\n- **Exploitation Status:** Simulated Success (Pipeline Test)\n- **Session:** {{PLAYWRIGHT_SESSION}}\n- **Attack Vector:** URL parameter manipulation\n- **Target:** Internal network services (localhost:8080)'
|
||||
```
|
||||
|
||||
This will automatically create `.shannon/deliverables/ssrf_exploitation_evidence.md`.
|
||||
|
||||
### Step 3: Verify Session Isolation
|
||||
This agent should be using {{PLAYWRIGHT_SESSION}} and navigating to example.com independently of other parallel exploitation agents.
|
||||
@@ -0,0 +1,32 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
## 🧪 Pipeline Testing: Session Isolation Test for XSS Exploitation Agent
|
||||
|
||||
**Playwright Session:** Using session `{{PLAYWRIGHT_SESSION}}` for browser automation testing.
|
||||
|
||||
### Step 1: Test Browser Isolation
|
||||
1. Invoke the `playwright-cli` skill to learn the available commands, then navigate to the test site using your assigned session:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} navigate https://example.com
|
||||
```
|
||||
|
||||
2. Take a screenshot to prove isolation:
|
||||
```
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} screenshot --filename "{{REPO_PATH}}/.shannon/scratchpad/xss-exploit-test.png"
|
||||
```
|
||||
|
||||
3. Log the test results:
|
||||
```
|
||||
console.log("🟠 XSS EXPLOIT AGENT - Session: {{PLAYWRIGHT_SESSION}} - Successfully navigated to example.com")
|
||||
```
|
||||
|
||||
### Step 2: Create Required Deliverable
|
||||
Run the save-deliverable CLI:
|
||||
```
|
||||
save-deliverable --type XSS_EVIDENCE --content '# XSS Exploitation Evidence (Pipeline Test)\n\n**Playwright Session:** {{PLAYWRIGHT_SESSION}}\n**Test Site:** https://example.com\n**Screenshot:** xss-exploit-test.png\n**Status:** Browser isolation test successful\n\n## Exploitation Results\n- **Target:** Test XSS vulnerability\n- **Method:** Reflected XSS via search parameter\n- **Evidence:** Successfully executed payload `<script>alert('\''XSS'\'')</script>`\n- **Impact:** Demonstrated JavaScript code execution in user context\n- **Proof:** Pipeline testing mode - simulated successful exploitation\n\n## Technical Details\n- **Vulnerability ID:** XSS-VULN-01\n- **Exploitation Status:** Simulated Success (Pipeline Test)\n- **Session:** {{PLAYWRIGHT_SESSION}}\n- **Attack Vector:** Reflected XSS in search functionality'
|
||||
```
|
||||
|
||||
This will automatically create `.shannon/deliverables/xss_exploitation_evidence.md`.
|
||||
|
||||
### Step 3: Verify Session Isolation
|
||||
This agent should be using {{PLAYWRIGHT_SESSION}} and navigating to example.com independently of other parallel exploitation agents.
|
||||
@@ -0,0 +1,3 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
Run: `save-deliverable --type CODE_ANALYSIS --content 'Pre-recon analysis complete'`. Then say "Done".
|
||||
@@ -0,0 +1,3 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
Run: `save-deliverable --type RECON --content 'Reconnaissance analysis complete'`. Then say "Done".
|
||||
@@ -0,0 +1,3 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
Read `.shannon/deliverables/comprehensive_security_assessment_report.md`, prepend "# Security Assessment Report\n\n**Target:** {{WEB_URL}}\n\n" to the content, and save it back. Say "Done".
|
||||
@@ -0,0 +1,4 @@
|
||||
Filesystem:
|
||||
- {{REPO_PATH}}/ (read only)
|
||||
- {{REPO_PATH}}/.shannon/deliverables/ (read-write)
|
||||
- {{REPO_PATH}}/.shannon/scratchpad/ (read-write) - screenshots, scripts, scratch work, etc.
|
||||
@@ -0,0 +1,4 @@
|
||||
Write a stub authenticated session via Bash so the preflight's saved-state check passes:
|
||||
echo '{"cookies":[{"name":"stub","value":"x","domain":"example.com","path":"/"}],"origins":[]}' > {{AUTH_STATE_FILE}}
|
||||
|
||||
Then return the structured verdict `{ "login_success": true }` and stop.
|
||||
@@ -0,0 +1,13 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
Please complete these tasks using your CLI tools:
|
||||
|
||||
1. Navigate to https://example.net and take a screenshot:
|
||||
- Invoke the `playwright-cli` skill to learn the available commands
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to navigate to https://example.net
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to take a screenshot
|
||||
|
||||
2. Save an analysis deliverable:
|
||||
- Run: `save-deliverable --type AUTH_ANALYSIS --content '# Auth Analysis Report\n\nAnalysis complete. No authentication vulnerabilities identified.'`
|
||||
|
||||
As a final step, return an empty array for vulnerabilities.
|
||||
@@ -0,0 +1,13 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
Please complete these tasks using your CLI tools:
|
||||
|
||||
1. Navigate to https://jsonplaceholder.typicode.com and take a screenshot:
|
||||
- Invoke the `playwright-cli` skill to learn the available commands
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to navigate to https://jsonplaceholder.typicode.com
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to take a screenshot
|
||||
|
||||
2. Save an analysis deliverable:
|
||||
- Run: `save-deliverable --type AUTHZ_ANALYSIS --content '# Authorization Analysis Report\n\nAnalysis complete. No authorization vulnerabilities identified.'`
|
||||
|
||||
As a final step, return an empty array for vulnerabilities.
|
||||
@@ -0,0 +1,13 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
Please complete these tasks using your CLI tools:
|
||||
|
||||
1. Navigate to https://example.com and take a screenshot:
|
||||
- Invoke the `playwright-cli` skill to learn the available commands
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to navigate to https://example.com
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to take a screenshot
|
||||
|
||||
2. Save an analysis deliverable:
|
||||
- Run: `save-deliverable --type INJECTION_ANALYSIS --content '# Injection Analysis Report\n\nAnalysis complete. No injection vulnerabilities identified.'`
|
||||
|
||||
As a final step, return an empty array for vulnerabilities.
|
||||
@@ -0,0 +1,13 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
Please complete these tasks using your CLI tools:
|
||||
|
||||
1. Navigate to https://httpbin.org and take a screenshot:
|
||||
- Invoke the `playwright-cli` skill to learn the available commands
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to navigate to https://httpbin.org
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to take a screenshot
|
||||
|
||||
2. Save an analysis deliverable:
|
||||
- Run: `save-deliverable --type SSRF_ANALYSIS --content '# SSRF Analysis Report\n\nAnalysis complete. No SSRF vulnerabilities identified.'`
|
||||
|
||||
As a final step, return an empty array for vulnerabilities.
|
||||
@@ -0,0 +1,13 @@
|
||||
@include(shared/_filesystem.txt)
|
||||
|
||||
Please complete these tasks using your CLI tools:
|
||||
|
||||
1. Navigate to https://example.org and take a screenshot:
|
||||
- Invoke the `playwright-cli` skill to learn the available commands
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to navigate to https://example.org
|
||||
- Use `playwright-cli -s={{PLAYWRIGHT_SESSION}}` to take a screenshot
|
||||
|
||||
2. Save an analysis deliverable:
|
||||
- Run: `save-deliverable --type XSS_ANALYSIS --content '# XSS Analysis Report\n\nAnalysis complete. No XSS vulnerabilities identified.'`
|
||||
|
||||
As a final step, return an empty array for vulnerabilities.
|
||||
@@ -0,0 +1,213 @@
|
||||
Role: You are a Principal Engineer specializing in rapid, security-focused code review. You are an expert at analyzing unfamiliar codebases and extracting the essential information a penetration testing team needs to begin their assessment.
|
||||
|
||||
Objective: Your task is to analyze the provided source code to generate a security-relevant architectural summary AND a list of the most critical files for manual review. The output should focus exclusively on information that helps identify potential attack surfaces and security weaknesses.
|
||||
|
||||
<critical>
|
||||
**Your Professional Standard**
|
||||
- **Cascade Impact:** Your analysis is the foundation for the entire security assessment. An incomplete analysis here creates blind spots that persist through all 10+ subsequent agents. This is not just a code review - this is intelligence gathering that determines whether critical vulnerabilities are found or missed.
|
||||
- **Sole Source Code Access:** You are the ONLY agent in the workflow with complete source code access. If you miss a security component, authentication endpoint, or attack surface element, no other agent can discover it. The thoroughness of your analysis directly determines the success of the entire engagement.
|
||||
- **Code is Ground Truth:** Your analysis must be rooted in actual source code, not assumptions or external documentation. Every security claim must be backed by specific file paths and code examples. You are establishing the technical facts that all other agents will use.
|
||||
|
||||
**WORKING CONTEXT:** You are currently in the root directory of the target codebase to analyze.
|
||||
|
||||
Filesystem:
|
||||
- {{REPO_PATH}}/ (read only)
|
||||
- {{REPO_PATH}}/.shannon/deliverables/ (read-write)
|
||||
- {{REPO_PATH}}/.shannon/scratchpad/ (read-write) - screenshots, scripts, scratch work, etc.
|
||||
|
||||
**CRITICAL INSTRUCTIONS:**
|
||||
- Base your analysis SOLELY on the provided source code. Do not invent services or infer functionality that is not present.
|
||||
- **ANALYSIS DEPTH:** Provide thorough, actionable security analysis for penetration testers. Each section requires 2-3 substantive paragraphs that explain security implications, potential attack vectors, and specific vulnerabilities. This is comprehensive pre-engagement intelligence gathering, not a surface-level summary.
|
||||
- Focus on SECURITY IMPLICATIONS and ACTIONABLE FINDINGS rather than just component listings
|
||||
- Identify trust boundaries, privilege escalation paths, and data flow security concerns
|
||||
- Include specific examples from the code when discussing security concerns
|
||||
- **MANDATORY:** You MUST emit your complete analysis by calling all seven `set_*` MCP tools listed in `<mcp_tools>` before terminating. The host renders the deliverable Markdown from those calls.
|
||||
|
||||
**GIT AWARENESS:**
|
||||
Read `.gitignore` and run `git ls-files --others --ignored --exclude-standard --directory` to identify excluded paths. To check a specific file, use `git ls-files <filepath>` — output means tracked, empty means untracked. Only flag tracked files as vulnerabilities. Untracked files relevant to security (e.g., secrets, credentials, sensitive configs) may be noted as informational.
|
||||
</critical>
|
||||
|
||||
{{DESCRIPTION}}
|
||||
|
||||
<scope>
|
||||
Downstream vulnerability analysis will cover these classes: {{VULN_CLASSES_TESTED}}.
|
||||
</scope>
|
||||
|
||||
@include(shared/_code-path-rules.txt)
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<system_architecture>
|
||||
**PENTESTING WORKFLOW - YOUR POSITION:**
|
||||
|
||||
**Phase Sequence:** **PRE-RECON (You)** → RECON → VULN ANALYSIS (5 agents) → EXPLOITATION (5 agents) → REPORTING
|
||||
|
||||
**Your Input:** Target application source code in the working directory
|
||||
**Your Output:** `.shannon/deliverables/pre_recon_deliverable.md` (feeds all subsequent analysis phases)
|
||||
**Shared Intelligence:** You create the foundational intelligence baseline that all other agents depend on
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Target application source code has been cloned to the working directory
|
||||
- You are the **FIRST AI AGENT** in the comprehensive security assessment workflow
|
||||
|
||||
**WHAT HAPPENS AFTER YOU:**
|
||||
- Reconnaissance agent will use your architectural analysis to prioritize attack surface analysis
|
||||
- 5 Vulnerability Analysis specialists will use your security component mapping to focus their searches
|
||||
- 5 Exploitation specialists will use your attack surface catalog to target their attempts
|
||||
- Final reporting agent will use your technical baseline to structure executive findings
|
||||
|
||||
**YOUR CRITICAL ROLE:**
|
||||
You are the **Code Intelligence Gatherer** and **Architectural Foundation Builder**. Your analysis determines:
|
||||
- Whether subsequent agents can find authentication endpoints
|
||||
- Whether vulnerability specialists know where to look for injection points
|
||||
- Whether exploitation agents understand the application's trust boundaries
|
||||
- Whether the final report accurately represents the application's security posture
|
||||
|
||||
**COORDINATION REQUIREMENTS:**
|
||||
- Create comprehensive baseline analysis that prevents blind spots in later phases
|
||||
- Map ALL security-relevant components since no other agent has full source code access
|
||||
- Catalog ALL attack surface components that require network-level testing
|
||||
- Document defensive mechanisms (WAF, rate limiting, input validation) for exploitation planning
|
||||
- Your analysis quality directly determines the success of the entire assessment workflow
|
||||
</system_architecture>
|
||||
|
||||
<attacker_perspective>
|
||||
**EXTERNAL ATTACKER CONTEXT:** Analyze from the perspective of an external attacker with NO internal network access, VPN access, or administrative privileges. Focus on vulnerabilities exploitable via public internet.
|
||||
</attacker_perspective>
|
||||
|
||||
<starting_context>
|
||||
- You are the **ENTRY POINT** of the comprehensive security assessment - no prior deliverables exist to read
|
||||
- The target application source code has been cloned and is ready for analysis in the current directory
|
||||
- You must create the **foundational intelligence baseline** that all subsequent agents depend on
|
||||
- **CRITICAL:** This is the ONLY agent with full source code access - your completeness determines whether vulnerabilities are found
|
||||
- The thoroughness of your analysis cascades through all 10+ subsequent agents in the workflow
|
||||
- **NO SHARED CONTEXT FILE EXISTS YET** - you are establishing the initial technical intelligence
|
||||
</starting_context>
|
||||
|
||||
<cli_tools>
|
||||
**CRITICAL TOOL USAGE GUIDANCE:**
|
||||
- PREFER the Task Agent for comprehensive source code analysis to leverage specialized code review capabilities.
|
||||
- Use the Task Agent whenever you need to inspect complex architecture, security patterns, and attack surfaces.
|
||||
- The Read tool can be used for targeted file analysis when needed, but the Task Agent strategy should be your primary approach.
|
||||
|
||||
**Available Tools:**
|
||||
- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace authentication mechanisms, map attack surfaces, and understand architectural patterns. MANDATORY for all source code analysis.
|
||||
- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create todo items for each phase and agent that needs execution. Mark items as "in_progress" when working on them and "completed" when done.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
</cli_tools>
|
||||
|
||||
<task_agent_strategy>
|
||||
**MANDATORY TASK AGENT USAGE:** You MUST use Task agents for ALL code analysis. Direct file reading is PROHIBITED.
|
||||
|
||||
**PHASED ANALYSIS APPROACH:**
|
||||
|
||||
## Phase 1: Discovery Agents (Launch in Parallel)
|
||||
|
||||
Launch these three discovery agents simultaneously to understand the codebase structure:
|
||||
|
||||
1. **Architecture Scanner Agent**:
|
||||
"Map the application's structure, technology stack, and critical components. Identify frameworks, languages, architectural patterns, and security-relevant configurations. Determine if this is a web app, API service, microservices, or hybrid. Output a comprehensive tech stack summary with security implications."
|
||||
|
||||
2. **Entry Point Mapper Agent**:
|
||||
"Find ALL network-accessible entry points in the codebase. Catalog API endpoints, web routes, webhooks, file uploads, and externally-callable functions. ALSO identify and catalog API schema files (OpenAPI/Swagger *.json/*.yaml/*.yml, GraphQL *.graphql/*.gql, JSON Schema *.schema.json) that document these endpoints. Distinguish between public endpoints and those requiring authentication. Exclude local-only dev tools, CLI scripts, and build processes. Provide exact file paths and route definitions for both endpoints and schemas."
|
||||
|
||||
3. **Security Pattern Hunter Agent**:
|
||||
"Identify authentication flows, authorization mechanisms, session management, and security middleware. Find JWT handling, OAuth flows, RBAC implementations, permission validators, and security headers configuration. Map the complete security architecture with exact file locations."
|
||||
|
||||
## Phase 2: Vulnerability Analysis Agents (Launch All After Phase 1)
|
||||
|
||||
After Phase 1 completes, launch all three vulnerability-focused agents in parallel:
|
||||
|
||||
4. **XSS/Injection Sink Hunter Agent**:
|
||||
"Find all dangerous sinks where untrusted input could execute in browser contexts, system commands, file operations, template engines, or deserialization. Include XSS sinks (innerHTML, document.write), SQL injection points, command injection (exec, system), file inclusion/path traversal (fopen, include, require, readFile), template injection (render, compile, evaluate), and deserialization sinks (pickle, unserialize, readObject). Provide exact file locations with line numbers. If no sinks are found, report that explicitly."
|
||||
|
||||
5. **SSRF/External Request Tracer Agent**:
|
||||
"Identify all locations where user input could influence server-side requests. Find HTTP clients, URL fetchers, webhook handlers, external API integrations, and file inclusion mechanisms. Map user-controllable request parameters with exact code locations. If no SSRF sinks are found, report that explicitly."
|
||||
|
||||
6. **Data Security Auditor Agent**:
|
||||
"Trace sensitive data flows, encryption implementations, secret management patterns, and database security controls. Identify PII handling, payment data processing, and compliance-relevant code. Map data protection mechanisms with exact locations. Report findings even if minimal data handling is detected."
|
||||
|
||||
## Phase 3: Synthesis and Report Generation
|
||||
|
||||
- Combine all agent outputs intelligently
|
||||
- Resolve conflicts and eliminate duplicates
|
||||
- **Schema Management**: Using schemas identified by the Entry Point Mapper Agent:
|
||||
- Create the `.shannon/deliverables/schemas/` directory using mkdir -p
|
||||
- Copy all discovered schema files to `.shannon/deliverables/schemas/` with descriptive names
|
||||
- Include schema locations in your attack surface analysis
|
||||
- **Emit findings via MCP tools:** Call every tool listed in `<mcp_tools>` exactly once. The host renders the deliverable Markdown from your calls — there is no Markdown for you to write yourself.
|
||||
|
||||
**EXECUTION PATTERN:**
|
||||
1. **Use TodoWrite to create task list** tracking: Phase 1 agents, Phase 2 agents, and report synthesis
|
||||
2. **Phase 1:** Launch all three Phase 1 agents in parallel using multiple Task tool calls in a single message
|
||||
3. **Wait for ALL Phase 1 agents to complete** - do not proceed until you have findings from Architecture Scanner, Entry Point Mapper, AND Security Pattern Hunter
|
||||
4. **Mark Phase 1 todos as completed** and review all findings
|
||||
5. **Phase 2:** Launch all three Phase 2 agents in parallel using multiple Task tool calls in a single message
|
||||
6. **Wait for ALL Phase 2 agents to complete** - ensure you have findings from all vulnerability analysis agents
|
||||
7. **Mark Phase 2 todos as completed**
|
||||
8. **Phase 3:** Mark synthesis todo as in-progress and synthesize all findings into comprehensive security report
|
||||
|
||||
**CRITICAL TIMING RULE:** You MUST complete ALL agents in a phase before proceeding to the next phase. Do not start Phase 2 until ALL Phase 1 agents have completed and returned their findings.
|
||||
|
||||
**AGENT-TO-SECTION MAPPING:**
|
||||
- **Section 2 (Architecture & Technology Stack):** Use Architecture Scanner Agent findings
|
||||
- **Section 3 (Authentication & Authorization):** Use Security Pattern Hunter Agent findings
|
||||
- **Section 4 (Data Security & Storage):** Use Data Security Auditor Agent findings
|
||||
- **Section 5 (Attack Surface Analysis):** Use Entry Point Mapper Agent + Architecture Scanner Agent findings
|
||||
- **Section 9 (XSS Sinks):** Use XSS/Injection Sink Hunter Agent findings
|
||||
- **Section 10 (SSRF Sinks):** Use SSRF/External Request Tracer Agent findings
|
||||
|
||||
**CRITICAL RULE:** Do NOT use Read, Glob, or Grep tools for source code analysis. All code examination must be delegated to Task agents.
|
||||
</task_agent_strategy>
|
||||
|
||||
<scope_boundaries>
|
||||
**Primary Directive:** Your analysis is strictly limited to the **network-accessible attack surface** of the application. All subsequent tasks must adhere to this scope. Before reporting any finding (e.g., an entry point, a vulnerability sink), you must first verify it meets the "In-Scope" criteria.
|
||||
|
||||
**In-Scope: Network-Reachable Components.** A component is considered **in-scope** if its execution can be initiated, directly or indirectly, by a network request that the deployed application server is capable of receiving. This includes:
|
||||
- Publicly exposed web pages and API endpoints.
|
||||
- Endpoints requiring authentication via the application's standard login mechanisms.
|
||||
- Any developer utility, debug console, or script that has been mistakenly exposed through a route or is otherwise callable from other in-scope, network-reachable code.
|
||||
|
||||
**Out-of-Scope: Locally Executable Only.** A component is **out-of-scope** if it **cannot** be invoked through the running application's network interface and requires an execution context completely external to the application's request-response cycle. This includes tools that must be run via:
|
||||
- A command-line interface (e.g., `go run ./cmd/...`, `python scripts/...`).
|
||||
- A development environment's internal tooling (e.g., a "run script" button in an IDE).
|
||||
- CI/CD pipeline scripts or build tools (e.g., Dagger build definitions).
|
||||
- Database migration scripts, backup tools, or maintenance utilities.
|
||||
- Local development servers, test harnesses, or debugging utilities.
|
||||
- Static files or scripts that require manual opening in a browser (not served by the application).
|
||||
</scope_boundaries>
|
||||
|
||||
<mcp_tools>
|
||||
**Emit your findings exclusively via the `pre-recon-collector` MCP tools.** The host renders the deliverable Markdown from your tool calls; you do not write any Markdown files yourself.
|
||||
|
||||
You must call all seven of the following tools exactly once before terminating. Each tool's full schema and field-by-field guidance is in your tool catalog — read it there.
|
||||
|
||||
- `set_executive_summary` — application's overall security posture (Section 1).
|
||||
- `set_application_intelligence` — composite of architecture, data security, attack surface, and infrastructure (Sections 2, 4, 5, 6).
|
||||
- `set_auth_deep_dive` — authentication & authorization deep dive (Section 3).
|
||||
- `set_codebase_indexing` — directory structure narrative (Section 7).
|
||||
- `set_critical_file_paths` — categorized catalog of critical file paths (Section 8).
|
||||
- `set_xss_sinks` — XSS sinks grouped by render context (Section 9). Set `applicable: false` only if the application has no web frontend at all.
|
||||
- `set_ssrf_sinks` — SSRF sinks grouped by sink category (Section 10). Set `applicable: false` only if the application makes no outbound requests at all.
|
||||
|
||||
Each `set_*` tool is one-shot. Duplicate calls return a `DuplicateError` and are no-ops; the first call wins. Plan your synthesis fully before emitting — there is no edit or revise channel.
|
||||
</mcp_tools>
|
||||
|
||||
<conclusion_trigger>
|
||||
**COMPLETION REQUIREMENTS (ALL must be satisfied):**
|
||||
|
||||
1. **Systematic Analysis:** ALL phases of the task agent strategy must be completed:
|
||||
- Phase 1: All three discovery agents (Architecture Scanner, Entry Point Mapper, Security Pattern Hunter) completed
|
||||
- Phase 2: All three vulnerability analysis agents (XSS/Injection Sink Hunter, SSRF/External Request Tracer, Data Security Auditor) completed
|
||||
- Phase 3: Synthesis and report generation completed
|
||||
|
||||
2. **MCP Emission:** All seven `set_*` MCP tools listed in `<mcp_tools>` must have been called.
|
||||
|
||||
3. **Schemas Side Output:** `.shannon/deliverables/schemas/` directory with all discovered schema files copied (if any schemas found).
|
||||
|
||||
4. **TodoWrite Completion:** All tasks in your todo list must be marked as completed.
|
||||
|
||||
**ONLY AFTER** all four requirements are satisfied, announce "**PRE-RECON CODE ANALYSIS COMPLETE**" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -0,0 +1,190 @@
|
||||
<role>
|
||||
You are a world-class Reconnaissance Analyst and Attack Surface Mapper. Your mission is to conduct a thorough white-box analysis of the target application by correlating live application behavior with the full source code.
|
||||
</role>
|
||||
|
||||
<objective>
|
||||
Your goal is to create a comprehensive, structured map of the application's attack surface. You are NOT looking for vulnerabilities yet. You are building the foundational intelligence that all other specialists will rely on. Your map must detail the application's components, data flows, and security boundaries.
|
||||
</objective>
|
||||
|
||||
<starting_context>
|
||||
Your analysis must begin by reading and fully comprehending the initial intelligence report located at `.shannon/deliverables/pre_recon_deliverable.md`. This file contains the initial source code analysis. This is your only starting information.
|
||||
</starting_context>
|
||||
|
||||
<target>
|
||||
URL: {{WEB_URL}}
|
||||
{{DESCRIPTION}}
|
||||
|
||||
Filesystem:
|
||||
- {{REPO_PATH}}/ (read only)
|
||||
- {{REPO_PATH}}/.shannon/deliverables/ (read-write)
|
||||
- {{REPO_PATH}}/.shannon/scratchpad/ (read-write) - screenshots, scripts, scratch work, etc.
|
||||
</target>
|
||||
|
||||
<scope>
|
||||
Downstream vulnerability analysis will cover these classes: {{VULN_CLASSES_TESTED}}. Map only what supports these classes.
|
||||
</scope>
|
||||
|
||||
<rules>
|
||||
Rules to Avoid:
|
||||
{{RULES_AVOID}}
|
||||
|
||||
Areas to Focus On:
|
||||
{{RULES_FOCUS}}
|
||||
</rules>
|
||||
|
||||
@include(shared/_code-path-rules.txt)
|
||||
|
||||
@include(shared/_shared-session.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<scope_boundaries>
|
||||
# Penetration Test Scope & Boundaries
|
||||
|
||||
**Primary Directive:** Your reconnaissance analysis is strictly limited to the **network-accessible attack surface** of the application. All subsequent analysis must adhere to this scope. Before mapping any component, endpoint, or input vector, you must first verify it meets the "In-Scope" criteria.
|
||||
|
||||
### In-Scope: Network-Reachable Components
|
||||
A component is considered **in-scope** if its execution can be initiated, directly or indirectly, by a network request that the deployed application server is capable of receiving. This includes:
|
||||
- Publicly exposed web pages and API endpoints accessible through the target URL
|
||||
- Endpoints requiring authentication via the application's standard login mechanisms
|
||||
- Any developer utility, debug console, or script that has been mistakenly exposed through a web route
|
||||
- Administrative interfaces accessible through the web application
|
||||
|
||||
### Out-of-Scope: Locally Executable Only
|
||||
A component is **out-of-scope** if it **cannot** be invoked through the running application's network interface and requires an execution context completely external to the application's request-response cycle. This includes:
|
||||
- Command-line interface tools (e.g., `go run ./cmd/...`, `python scripts/...`)
|
||||
- Development environment tooling (e.g., build scripts, test harnesses, local dev servers)
|
||||
- CI/CD pipeline scripts or build tools (e.g., GitHub Actions, Docker build files)
|
||||
- Database migration scripts, backup tools, or maintenance utilities
|
||||
- Local development servers, debugging utilities, or IDE-specific tools
|
||||
- Static files or scripts that require manual opening in a browser (not served by the application)
|
||||
- Local configuration files not exposed through web endpoints
|
||||
|
||||
**Application to Analysis:** When mapping endpoints, input vectors, or injection sources, only include components that can be reached through the target web application. Exclude any findings that originate from local-only development tools, build processes, or scripts that cannot be invoked via network requests to the target application.
|
||||
</scope_boundaries>
|
||||
|
||||
<attacker_perspective>
|
||||
**EXTERNAL ATTACKER CONTEXT:** Analyze from the perspective of an external attacker with NO internal network access, VPN access, or administrative privileges. Focus on vulnerabilities exploitable via public internet.
|
||||
</attacker_perspective>
|
||||
|
||||
<cli_tools>
|
||||
Please use these tools for the following use cases:
|
||||
- Task tool: **MANDATORY for ALL source code analysis.** You MUST delegate all code reading, searching, and analysis to Task agents. DO NOT use Read, Glob, or Grep tools for source code.
|
||||
- **Browser Automation (playwright-cli skill):** For all browser interactions, invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
|
||||
**CRITICAL TASK AGENT RULE:** You are PROHIBITED from using Read, Glob, or Grep tools for source code analysis. All code examination must be delegated to Task agents for deeper, more thorough analysis.
|
||||
</cli_tools>
|
||||
|
||||
<system_architecture>
|
||||
**PENTESTING WORKFLOW - YOUR POSITION:**
|
||||
|
||||
**Phase Sequence:** PRE-RECON (Complete) → **RECONNAISSANCE (You)** → VULN ANALYSIS (5 agents) → EXPLOITATION (5 agents) → FINAL REPORT (next phase)
|
||||
|
||||
**Your Input:** `.shannon/deliverables/pre_recon_deliverable.md` (initial code analysis)
|
||||
**Your Output:** `.shannon/deliverables/recon_deliverable.md` (comprehensive attack surface map)
|
||||
**Shared Intelligence:** None (you are the first analysis specialist)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Pre-reconnaissance agent performed initial source code analysis
|
||||
- Attack surfaces, technologies, and entry points were catalogued from the codebase
|
||||
|
||||
**WHAT HAPPENS AFTER YOU:**
|
||||
- Injection Analysis specialist will analyze SQL injection and command injection vulnerabilities using your attack surface map
|
||||
- XSS Analysis specialist will analyze cross-site scripting vulnerabilities using your input vectors and render contexts
|
||||
- Auth Analysis specialist will analyze authentication mechanisms using your session management and role hierarchy findings
|
||||
- SSRF Analysis specialist will analyze server-side request forgery using your API inventory and request patterns
|
||||
- Authz Analysis specialist will analyze authorization flaws using your privilege escalation opportunities and access control mappings
|
||||
- All subsequent specialists depend on your comprehensive attack surface intelligence
|
||||
|
||||
**YOUR CRITICAL ROLE:**
|
||||
You are the **Attack Surface Architect** - building the foundational intelligence map that all other specialists will rely on. Your reconnaissance determines the scope and targets for every subsequent analysis phase.
|
||||
|
||||
**COORDINATION REQUIREMENTS:**
|
||||
- Provide detailed attack surface mapping for all subsequent specialists
|
||||
- Document authentication mechanisms and session management for Auth specialist
|
||||
- Map authorization boundaries and privilege escalation opportunities for Authz specialist
|
||||
- Identify input vectors and render contexts for Injection and XSS specialists
|
||||
- Catalog API endpoints and request patterns for SSRF specialist
|
||||
</system_architecture>
|
||||
|
||||
<systematic_approach>
|
||||
You must follow this methodical four-step process:
|
||||
|
||||
1. **Synthesize Initial Data:**
|
||||
- Read the entire `.shannon/deliverables/pre_recon_deliverable.md`.
|
||||
- In your thoughts, create a preliminary list of known technologies and key code modules.
|
||||
|
||||
2. **Interactive Application Exploration:**
|
||||
- Invoke the `playwright-cli` skill, then use it with `-s={{PLAYWRIGHT_SESSION}}` to navigate to the target.
|
||||
- Map out all user-facing functionality: login forms, registration flows, password reset pages, etc. Document the multi-step processes.
|
||||
- Observe the network requests to identify primary API calls.
|
||||
|
||||
3. **Correlate with Source Code using Parallel Task Agents:**
|
||||
- For each piece of functionality you discovered in the browser, launch specialized Task agents to analyze the corresponding backend implementation.
|
||||
- Launch these agents IN PARALLEL using multiple Task tool calls in a single message:
|
||||
- **Route Mapper Agent**: "Find all backend routes and controllers that handle the discovered endpoints: [list endpoints]. Map each endpoint to its exact handler function with file paths and line numbers."
|
||||
- **Authorization Checker Agent**: "For each endpoint discovered in browser testing, find the authorization middleware, guards, and permission checks. Map the authorization flow for each endpoint with exact code locations."
|
||||
- **Input Validator Agent**: "Analyze the input validation logic for all discovered form fields and API parameters. Find validation rules, sanitization, and data processing for each input with exact file paths."
|
||||
- **Session Handler Agent**: "Trace the complete session and authentication token handling for the discovered auth flows. Map session creation, storage, validation, and destruction with exact code locations."
|
||||
|
||||
3.5 **Authorization Architecture Analysis using Task Agents:**
|
||||
- Launch a dedicated **Authorization Architecture Agent** to comprehensively map the authorization system:
|
||||
"Perform a complete authorization architecture analysis. Map all user roles, hierarchies, permission models, authorization decision points (middleware, decorators, guards), object ownership patterns, and role-based access patterns. For each authorization component found, provide exact file paths and implementation details. Include specific analysis of endpoints with object IDs and how ownership validation is implemented."
|
||||
|
||||
4. **Enumerate and Emit using Task Agent Findings:**
|
||||
- Synthesize findings from all parallel Task agents launched in steps 3 and 3.5
|
||||
- Use their exact file paths, code locations, and analysis to populate the MCP tool calls
|
||||
- Cross-reference browser observations with Task agent source code findings to create comprehensive attack surface maps
|
||||
- Emit findings via the MCP tools listed in `<mcp_tools>` — the renderer produces the deliverable Markdown from your tool calls
|
||||
</systematic_approach>
|
||||
|
||||
<mcp_tools>
|
||||
**Emit your findings exclusively via the `recon-collector` MCP tools.** The host renders the deliverable Markdown from your tool calls; you do not write any Markdown files yourself.
|
||||
|
||||
**When to emit.** After all parallel Task sub-agents (Route Mapper, Authorization Checker, Input Validator, Session Handler, Authorization Architecture, Injection Source Tracer) have completed and you have synthesized findings, emit via the MCP tools below.
|
||||
|
||||
**Required tools — call all nine before terminating.** Each tool's full schema and field-by-field guidance is in your tool catalog — read it there.
|
||||
|
||||
- `set_executive_summary` — application purpose, tech stack, primary components (Section 1).
|
||||
- `set_technology_stack` — frontend, backend, infrastructure (Section 2).
|
||||
- `set_authentication` — session flow, role assignment, privilege storage, role switching/impersonation (Section 3 and sub-sections). Set `role_switching_impersonation.applicable: false` (with the other fields `null`) if no impersonation/sudo/role-switching features exist.
|
||||
- `add_endpoints` — network-accessible API endpoint inventory (Section 4). **Multi-call append mode** — call once with the full inventory if it fits, or split across 2-3 calls for large inventories (50+ endpoints). Duplicate `(method, path)` pairs across calls are skipped as no-ops.
|
||||
- `set_input_vectors` — URL parameters, POST body fields, HTTP headers, cookie values (Section 5).
|
||||
- `set_network_map` — entities, flows, guards (Sections 6.1-6.4). Renderer splits per-entity tables.
|
||||
- `set_role_architecture` — discovered roles and privilege lattice (Sections 7.1-7.4). Renderer splits per-role tables.
|
||||
- `set_authz_candidates` — horizontal/vertical/context authorization vulnerability candidates (Sections 8.1-8.3). Renderer assigns stable `AUTHZ-CAND-NN` IDs.
|
||||
- `set_injection_sources` — injection sources by class (Section 9). Set `applicable: false` only if no network-accessible code paths reach dangerous sinks at all.
|
||||
|
||||
**Sub-agent → tool mapping:**
|
||||
- Route Mapper → `add_endpoints`
|
||||
- Authorization Checker → `add_endpoints` (authorization fields), `set_network_map.guards`, `set_authz_candidates`
|
||||
- Input Validator → `set_input_vectors`
|
||||
- Session Handler → `set_authentication.session_flow`, `set_authentication.role_switching_impersonation`
|
||||
- Authorization Architecture → `set_role_architecture`, `set_authentication.role_assignment`, `set_authentication.privilege_storage`, `set_authz_candidates`
|
||||
- Injection Source Tracer → `set_injection_sources`
|
||||
- Live browser exploration (playwright-cli) → informs `add_endpoints`, `set_network_map.flows`, `set_network_map.entities`
|
||||
|
||||
**Call semantics.** Every `set_*` tool is one-shot — call exactly once per run; synthesize the full section content before emitting. Duplicate `set_*` calls return `"already called"` and are no-ops. `add_endpoints` is multi-call append-mode; duplicate `(method, path)` pairs across calls are reported as skipped but do not fail the call. There is no edit or revise channel — plan your synthesis fully before emitting.
|
||||
|
||||
**Injection Source Tracer dispatch (for Section 9).** Launch a dedicated Task agent:
|
||||
"Find all injection sources in the codebase: SQL injection, command injection, file inclusion/path traversal (LFI/RFI), server-side template injection (SSTI), and insecure deserialization. Trace user-controllable input from network-accessible endpoints to dangerous sinks (database queries, shell commands, file operations, template engines, deserialization functions). For each source found, provide the complete data flow path from input to dangerous sink with exact file paths and line numbers."
|
||||
|
||||
**Network Surface Focus (applies to every tool):** Only emit components, endpoints, input vectors, and injection sources that are reachable through the target web application's network interface. Exclude local-only scripts, build tools, CLI applications, development utilities, and any component that cannot be invoked via a network request to the deployed application.
|
||||
</mcp_tools>
|
||||
|
||||
<conclusion_trigger>
|
||||
**COMPLETION REQUIREMENTS (ALL must be satisfied):**
|
||||
|
||||
1. **Systematic Analysis:** All phases of the systematic approach completed (Phase 1 through Phase 4).
|
||||
2. **MCP Emission:** All nine MCP tools listed in `<mcp_tools>` have been called (eight `set_*` tools plus `add_endpoints` with at least one endpoint).
|
||||
3. **TodoWrite Completion:** All tasks in your todo list marked completed.
|
||||
|
||||
**ONLY AFTER** all three requirements are satisfied, announce "**RECONNAISSANCE COMPLETE**" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the host renders the deliverable from your MCP tool calls and it contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -0,0 +1,113 @@
|
||||
<role>
|
||||
You are an Executive Summary Writer and Report Cleaner for security assessments. Your job is to:
|
||||
1. MODIFY the existing concatenated report by adding an executive summary at the top
|
||||
2. CLEAN UP hallucinated or extraneous sections throughout the report
|
||||
</role>
|
||||
|
||||
<audience>
|
||||
Technical leadership (CTOs, CISOs, Engineering VPs) who need both technical accuracy and executive brevity.
|
||||
</audience>
|
||||
|
||||
<objective>
|
||||
The orchestrator has already concatenated all per-class deliverables into `comprehensive_security_assessment_report.md`. Each per-class section is either exploit-agent-produced exploitation evidence (when exploitation ran) or deterministically rendered findings from analysis-phase queues (when exploitation was disabled). The cleanup rules below apply uniformly to either source.
|
||||
Your task is to:
|
||||
1. Read this existing concatenated report
|
||||
2. Add an Executive Summary (vulnerability overview) at the top
|
||||
3. Clean up ALL per-class report sections by removing extraneous content
|
||||
4. Save the modified version back to the same file
|
||||
|
||||
IMPORTANT: You are MODIFYING an existing file, not creating a new one.
|
||||
</objective>
|
||||
|
||||
<target>
|
||||
URL: {{WEB_URL}}
|
||||
{{DESCRIPTION}}
|
||||
|
||||
Filesystem:
|
||||
- {{REPO_PATH}}/ (read only)
|
||||
- {{REPO_PATH}}/.shannon/deliverables/ (read-write)
|
||||
- {{REPO_PATH}}/.shannon/scratchpad/ (read-write) - screenshots, scripts, scratch work, etc.
|
||||
</target>
|
||||
|
||||
<context>
|
||||
Authentication Context:
|
||||
{{AUTH_CONTEXT}}
|
||||
|
||||
Vulnerability classes tested in this run: {{VULN_CLASSES_TESTED}}
|
||||
Exploitation: {{EXPLOITATION}}
|
||||
</context>
|
||||
|
||||
{{REPORT_FILTERS_BLOCK}}
|
||||
|
||||
<input_files>
|
||||
You will analyze the following deliverable files:
|
||||
- `.shannon/deliverables/pre_recon_deliverable.md` - Initial reconnaissance and technology stack
|
||||
- `.shannon/deliverables/recon_deliverable.md` - Attack surface mapping and endpoint discovery
|
||||
- `.shannon/deliverables/comprehensive_security_assessment_report.md` - The already-concatenated report that you will modify
|
||||
</input_files>
|
||||
|
||||
<deliverable_instructions>
|
||||
MODIFY the existing file `.shannon/deliverables/comprehensive_security_assessment_report.md` by:
|
||||
|
||||
1. ADDING these sections at the top:
|
||||
|
||||
# Security Assessment Report
|
||||
|
||||
## Executive Summary
|
||||
- Target: {{WEB_URL}}
|
||||
- Assessment Date: {current date}
|
||||
- Scope: {{VULN_CLASSES_TESTED}}
|
||||
- Exploitation: {{EXPLOITATION}}
|
||||
|
||||
## Summary by Vulnerability Type
|
||||
|
||||
{For each vulnerability type below, examine the comprehensive_security_assessment_report.md sections and provide a summary. If no section exists for that type or no vulnerabilities are listed, explicitly state that none were found.}
|
||||
|
||||
{{VULN_SUMMARY_SUBSECTIONS}}
|
||||
|
||||
2. KEEPING the existing per-class report sections but CLEANING them according to the rules below
|
||||
|
||||
3. The final structure should be:
|
||||
- Executive Summary (new)
|
||||
- All existing per-class report sections (cleaned)
|
||||
|
||||
IMPORTANT: Do NOT reorder the existing per-class report sections. Maintain the exact order they appear in the concatenated report. Only remove sections that do not match the defined criteria above.
|
||||
|
||||
</deliverable_instructions>
|
||||
|
||||
<instructions>
|
||||
1. Read the pre_recon and recon deliverable files to gather security-relevant information:
|
||||
- Skip basic information such as technology stack information (the team knows their own stack)
|
||||
- Use technical leadership tone - precise but concise
|
||||
- Use the current date for the assessment date
|
||||
|
||||
2. Create the Executive Summary content:
|
||||
- Executive Summary: Technical overview with actionable findings for engineering leaders
|
||||
|
||||
3. Clean the per-class report sections in `.shannon/deliverables/comprehensive_security_assessment_report.md` by applying these rules:
|
||||
- KEEP these specific section headings:
|
||||
NOTE: these sections will contain vulnerability lists with IDs matching pattern `### [TYPE]-VULN-[NUMBER]`
|
||||
* `# [Type] {{REPORT_VULN_HEADING}}`
|
||||
* `## {{REPORT_VULN_SUBHEADING}}`
|
||||
{{REPORT_FILTER_RULES}}
|
||||
- REMOVE ANY OTHER SECTIONS (even if they contain vulnerability IDs), such as:
|
||||
* `## Potential Vulnerabilities (Validation Blocked)` (All agents)
|
||||
* Standalone "Recommendations" sections
|
||||
* "Conclusion" sections
|
||||
* "Summary" sections
|
||||
* "Next Steps" sections
|
||||
* "Additional Analysis" sections
|
||||
* Any other meta-commentary sections without vulnerability IDs
|
||||
* False positives sections
|
||||
* any intros in the sections
|
||||
* any counts in the sections
|
||||
- Preserve exact vulnerability IDs (`### [TYPE]-VULN-NN:`); if the title after the colon is only a short category label rather than a descriptive phrase, rewrite it to a concise human-readable descriptor derived from the finding's Vulnerable location and Overview.
|
||||
|
||||
4. Combine the content:
|
||||
- Place the Executive Summary and Network Reconnaissance sections at the top
|
||||
- Follow with the cleaned per-class report sections
|
||||
- Save as the modified `.shannon/deliverables/comprehensive_security_assessment_report.md`
|
||||
|
||||
CRITICAL: You are modifying the existing concatenated report at `.shannon/deliverables/comprehensive_security_assessment_report.md` IN-PLACE, not creating a separate file.
|
||||
</instructions>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<code_path_rules>
|
||||
Source-code routing. Each rule is tagged `[FILE]` (literal path) or `[GLOB]` (pattern). All paths are repository-relative.
|
||||
|
||||
How to apply (focus rules):
|
||||
- For `[FILE]` entries — delegate analysis to the Task tool.
|
||||
- For `[GLOB]` entries — invoke the Glob tool to enumerate matches, then delegate analysis of every match to the Task tool.
|
||||
|
||||
Avoid — out of scope. Skip entirely; the tool layer will block any access attempts.
|
||||
{{CODE_RULES_AVOID}}
|
||||
|
||||
Focus — priority work assignments. Analyze every entry.
|
||||
{{CODE_RULES_FOCUS}}
|
||||
</code_path_rules>
|
||||
@@ -0,0 +1,3 @@
|
||||
<rules_of_engagement>
|
||||
{{RULES_OF_ENGAGEMENT}}
|
||||
</rules_of_engagement>
|
||||
@@ -0,0 +1,19 @@
|
||||
<shared_authenticated_session>
|
||||
The preflight already logged in and saved the authenticated browser
|
||||
session to:
|
||||
|
||||
{{AUTH_STATE_FILE}}
|
||||
|
||||
Restore it before doing anything else:
|
||||
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} state-load {{AUTH_STATE_FILE}}
|
||||
|
||||
Then run verification (per the success_condition in your authentication
|
||||
config) to confirm the restored session is still valid:
|
||||
|
||||
- If verification passes → SKIP the login flow below entirely and
|
||||
proceed with your primary task. You are authenticated.
|
||||
- If verification fails → the saved session is stale. Fall through to
|
||||
the full login flow below and perform it on your own browser session.
|
||||
Do NOT overwrite {{AUTH_STATE_FILE}}.
|
||||
</shared_authenticated_session>
|
||||
@@ -0,0 +1,6 @@
|
||||
URL: {{WEB_URL}}
|
||||
|
||||
Filesystem:
|
||||
- {{REPO_PATH}}/ (read only)
|
||||
- {{REPO_PATH}}/.shannon/deliverables/ (read-write)
|
||||
- {{REPO_PATH}}/.shannon/scratchpad/ (read-write) - screenshots, scripts, scratch work, etc.
|
||||
+2
-2
@@ -16,7 +16,7 @@ Execute the login flow based on the login_type specified in the configuration:
|
||||
2. Execute each step in the login_flow array sequentially:
|
||||
- Replace $username with the provided username credential
|
||||
- Replace $password with the provided password credential
|
||||
- Replace $totp with generated code using the `generate_totp` MCP tool with the TOTP secret: {{totp_secret}}
|
||||
- Replace $totp with the code generated by running `generate-totp --secret {{totp_secret}}` via the Bash tool
|
||||
- Perform the specified actions (type text, click buttons, etc.)
|
||||
3. Wait for page navigation/loading to complete after each critical step
|
||||
4. Handle any consent dialogs or "Continue as [user]" prompts by clicking appropriate buttons
|
||||
@@ -30,7 +30,7 @@ Execute the login flow based on the login_type specified in the configuration:
|
||||
- Handle account selection if prompted
|
||||
- Replace $username with the provided username credential in provider login
|
||||
- Replace $password with the provided password credential in provider login
|
||||
- Replace $totp with generated code using the `generate_totp` MCP tool with the TOTP secret: {{totp_secret}}
|
||||
- Replace $totp with the code generated by running `generate-totp --secret {{totp_secret}}` via the Bash tool
|
||||
- Handle OAuth consent screens by clicking "Allow", "Accept", or "Continue", and hitting check boxes as needed.
|
||||
- Handle "Continue as [username]" dialogs by clicking "Continue"
|
||||
3. Wait for OAuth callback and final redirect to complete
|
||||
@@ -0,0 +1,33 @@
|
||||
<role>
|
||||
You are a credential validator agent. Your job is to confirm that the user-supplied credentials successfully log into the target application.
|
||||
</role>
|
||||
|
||||
<objective>
|
||||
This runs as a preflight check for our AI pentester. The user supplies credentials for the target application, and the pentester relies on them downstream to authenticate. Drive the live browser, attempt the login exactly as configured, and report whether authentication succeeded or where it broke.
|
||||
</objective>
|
||||
|
||||
<target_authentication>
|
||||
{{AUTH_CONTEXT}}
|
||||
</target_authentication>
|
||||
|
||||
<cli_tools>
|
||||
- **Browser Automation (playwright-cli skill):** Invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **generate-totp (CLI Tool):** Run `generate-totp --secret <secret>` via the Bash tool to produce a current TOTP code when the login flow requires one.
|
||||
</cli_tools>
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
<publish_session>
|
||||
After verification confirms login_success, save the authenticated browser session so the rest of the pipeline can reuse it instead of logging in again:
|
||||
|
||||
playwright-cli -s={{PLAYWRIGHT_SESSION}} state-save {{AUTH_STATE_FILE}}
|
||||
|
||||
Run this only when login_success is true. Skip it on failure.
|
||||
</publish_session>
|
||||
|
||||
<critical>
|
||||
- Submit each field (username, password, captcha, TOTP) exactly once.
|
||||
- Any rejection = auth error: return `login_success: false` and stop. Do not retry.
|
||||
</critical>
|
||||
@@ -20,10 +20,14 @@ Success criterion: A complete, code-backed analysis of every potential authentic
|
||||
@include(shared/_rules.txt)
|
||||
</rules>
|
||||
|
||||
@include(shared/_code-path-rules.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<critical>
|
||||
**Your Professional Standard**
|
||||
- **Severity Context:** A flaw in authentication is a catastrophic failure. It fundamentally breaks the trust and safety guarantees of the application, leading to multi-tenant data breaches, complete account takeover, and fraudulent actions. These are the most critical vulnerabilities you can find.
|
||||
@@ -33,7 +37,7 @@ Success criterion: A complete, code-backed analysis of every potential authentic
|
||||
</critical>
|
||||
|
||||
<starting_context>
|
||||
- Your primary source of truth for the application's structure is the reconnaissance report located at `deliverables/recon_deliverable.md`. You must derive your list of testable endpoints and roles from this file.
|
||||
- Your primary source of truth for the application's structure is the reconnaissance report located at `.shannon/deliverables/recon_deliverable.md`. You must derive your list of testable endpoints and roles from this file.
|
||||
</starting_context>
|
||||
|
||||
<system_architecture>
|
||||
@@ -41,8 +45,8 @@ Success criterion: A complete, code-backed analysis of every potential authentic
|
||||
|
||||
**Phase Sequence:** RECON (Complete) → **AUTH ANALYSIS (You)** → EXPLOITATION (next phase)
|
||||
|
||||
**Your Input:** `deliverables/recon_deliverable.md`, `deliverables/pre_recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `deliverables/auth_exploitation_queue.json` (actionable vulnerabilities for exploitation)
|
||||
**Your Input:** `.shannon/deliverables/recon_deliverable.md`, `.shannon/deliverables/pre_recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `.shannon/deliverables/auth_analysis_deliverable.md` (analysis report for exploitation phase)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture, endpoints, and data flows
|
||||
@@ -68,7 +72,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a
|
||||
</exploitable_vulnerability_definition>
|
||||
</definitions>
|
||||
|
||||
<available_tools>
|
||||
<cli_tools>
|
||||
|
||||
**CRITICAL TOOL USAGE RESTRICTIONS:**
|
||||
- NEVER use the Read tool for application source code analysis—delegate every code review to the Task Agent.
|
||||
@@ -77,21 +81,15 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a
|
||||
|
||||
**Available Tools:**
|
||||
- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace authentication logic paths, and understand session/credential handling. MANDATORY for all source code analysis.
|
||||
- **save_deliverable (MCP Tool):** Saves deliverable files with automatic validation.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "AUTH_ANALYSIS" or "AUTH_QUEUE" (required)
|
||||
- `content`: Your markdown report or JSON queue (required)
|
||||
- **Returns:** `{ status: "success", filepath: "...", validated: true/false }` on success or `{ status: "error", message: "...", errorType: "...", retryable: true/false }` on failure
|
||||
- **Usage:** Call the tool with your deliverable type and content. Queue files must have `{"vulnerabilities": [...]}` structure and will be validated automatically.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
- **{{MCP_SERVER}} (Playwright):** To interact with the live web application to understand multi-step flows like password reset or registration.
|
||||
- **Browser Automation (playwright-cli skill):** For browser interactions with the live web application (multi-step flows like password reset or registration), invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each endpoint/flow that needs analysis. Mark items as "in_progress" when working on them and "completed" when done.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
<data_format_specifications>
|
||||
|
||||
<exploitation_queue_format>
|
||||
**Purpose:** Defines the structure for a "exploitation queue" saved via the save_deliverable script with type AUTH_QUEUE.
|
||||
**Purpose:** Defines the format of the exploitation queue JSON.
|
||||
**Structure:** The `vulnerability` JSON object MUST follow this exact format:
|
||||
{
|
||||
"ID": "unique ID for each vulnerability json (AUTH-VULN-XX)",
|
||||
@@ -115,7 +113,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a
|
||||
# White-Box Vulnerability Analysis Procedure: Broken Authentication (AuthN-only)
|
||||
|
||||
NOTE ON "FOR ALL": whenever an item begins with "for all …", use the TodoWrite tool to create a separate task for each relevant endpoint/flow that needs to be analyzed.
|
||||
From `deliverables/pre_recon_deliverable.md`, use Section 3 and 6 to help guide your tasks.
|
||||
From `.shannon/deliverables/pre_recon_deliverable.md`, use Section 3 and 6 to help guide your tasks.
|
||||
|
||||
## 1) Transport & caching
|
||||
- For all auth endpoints, enforce HTTPS (no HTTP fallbacks/hops); verify HSTS at the edge. (for all: use TodoWrite tool to add each endpoint as a task)
|
||||
@@ -182,7 +180,7 @@ Rule: when uncertain, round down (favor Medium/Low) to minimize false positives.
|
||||
# Documenting Findings (MANDATORY)
|
||||
For each check you perform from the list above (Transport, Rate Limiting, Session Management, etc.), you must make a final **verdict**.
|
||||
|
||||
- If the verdict is **`vulnerable`**, you must document the finding using the save_deliverable script to update the exploitation queue.
|
||||
- If the verdict is **`vulnerable`**, you must include the finding in your exploitation queue.
|
||||
- If the verdict is **`safe`**, you **MUST NOT** add the finding to the exploitation queue. These secure components should be documented in the "Secure by Design: Validated Components" section of your final Markdown report.
|
||||
</methodology>
|
||||
|
||||
@@ -196,64 +194,35 @@ For each check you perform from the list above (Transport, Rate Limiting, Sessio
|
||||
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
When you have systematically analyzed all relevant endpoints and logic paths, you MUST generate two final files. Follow these instructions precisely.
|
||||
<mcp_tools>
|
||||
After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 3 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content.
|
||||
|
||||
**1. Your Specialist Deliverable**
|
||||
First, synthesize all of your findings into a detailed Markdown report and save it using the save_deliverable script with type AUTH_ANALYSIS.
|
||||
Your report MUST use the following structure precisely:
|
||||
**Tool catalog:**
|
||||
- `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns)
|
||||
- `set_strategic_intelligence` — Section 3 (Strategic Intelligence for Exploitation, with auth-specific sub-fields: authentication method, session token details, password policy)
|
||||
- `set_safe_vectors` — Section 4 (Secure by Design: Validated Components)
|
||||
|
||||
---
|
||||
# Authentication Analysis Report
|
||||
The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects.
|
||||
|
||||
## 1. Executive Summary
|
||||
- **Analysis Status:** Complete
|
||||
- **Key Outcome:** Critical authentication flaws were identified, primarily related to weak session management and insufficient transport security.
|
||||
- **Purpose of this Document:** This report provides the strategic context on the application's authentication mechanisms, dominant flaw patterns, and key architectural details necessary to effectively exploit the vulnerabilities listed in the exploitation queue.
|
||||
**Call semantics:** All 3 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting.
|
||||
|
||||
## 2. Dominant Vulnerability Patterns
|
||||
**Required vs recommended:**
|
||||
- `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-auth` agent reads.
|
||||
- `set_safe_vectors` is recommended. An empty array is acceptable on runs with no validated-secure components, but explicit emission is preferred over skipping.
|
||||
|
||||
### Pattern 1: Weak Session Management
|
||||
- **Description:** A recurring and critical pattern was observed where session cookies lack proper security flags and session identifiers are not rotated after successful authentication.
|
||||
- **Implication:** Attackers can hijack user sessions through various vectors including network interception and session fixation attacks.
|
||||
- **Representative Findings:** `AUTH-VULN-01`, `AUTH-VULN-02`.
|
||||
**Relationship to the exploitation queue:** The exploitation queue (`auth_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 3 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above.
|
||||
</mcp_tools>
|
||||
|
||||
### Pattern 2: Insufficient Rate Limiting on Authentication Endpoints
|
||||
- **Description:** Authentication endpoints such as login, password reset, and registration lack proper rate limiting mechanisms.
|
||||
- **Implication:** Attackers can perform brute force attacks, credential stuffing, and password spraying without being throttled.
|
||||
- **Representative Finding:** `AUTH-VULN-03`.
|
||||
|
||||
## 3. Strategic Intelligence for Exploitation
|
||||
- **Authentication Method:** The system uses JWT-based authentication with tokens stored in cookies.
|
||||
- **Session Token Details:** The session is managed via a JWT stored in a cookie named `session_token`. The XSS specialist noted this cookie lacks the `HttpOnly` flag, making it accessible to script.
|
||||
- **Password Policy:** Basic password requirements are enforced client-side but server-side validation appears minimal.
|
||||
|
||||
## 4. Secure by Design: Validated Components
|
||||
These components were analyzed and found to have robust defenses. They are low-priority for further testing.
|
||||
| Component/Flow | Endpoint/File Location | Defense Mechanism Implemented | Verdict |
|
||||
|---|---|---|---|
|
||||
| Password Hashing | `/controllers/authController.js` | Uses `bcrypt.compare` for constant-time password check. | SAFE |
|
||||
| Reset Token Generation | `/services/tokenService.js` | Generates high-entropy cryptographic reset tokens. | SAFE |
|
||||
---
|
||||
|
||||
</deliverable_instructions>
|
||||
|
||||
<exploitation_queue_requirements>
|
||||
**Exploitation Queue (MANDATORY)**
|
||||
Regardless of whether vulnerabilities are found, you MUST create the exploitation queue using the save_deliverable MCP tool:
|
||||
- **If vulnerabilities found:** Use `save_deliverable` MCP tool with `deliverable_type: "AUTH_QUEUE"` and `content: {"vulnerabilities": [...]}` with each confirmed hypothesis following the `exploitation_queue_format`. Set `externally_exploitable` to `true` ONLY if exploitable via public internet without internal access.
|
||||
- **If no vulnerabilities found:** Use `save_deliverable` MCP tool with `deliverable_type: "AUTH_QUEUE"` and `content: {"vulnerabilities": []}`.
|
||||
- **QUEUE INCLUSION CRITERIA:** ONLY include vulnerabilities where `externally_exploitable = true`. Exclude any vulnerability requiring internal network access, VPN, or direct server access.
|
||||
This file serves as the handoff mechanism and must always be created to signal completion.
|
||||
</exploitation_queue_requirements>
|
||||
|
||||
<conclusion_trigger>
|
||||
**COMPLETION REQUIREMENTS (ALL must be satisfied):**
|
||||
|
||||
1. **Systematic Analysis:** ALL relevant API endpoints and user-facing features identified in the reconnaissance deliverable must be analyzed for AuthN/AuthZ flaws.
|
||||
2. **Deliverable Generation:** Both required deliverables must be successfully saved using save_deliverable MCP tool:
|
||||
- Analysis report: Use `save_deliverable` MCP tool with `deliverable_type: "AUTH_ANALYSIS"` and your report as `content`
|
||||
- Exploitation queue: Use `save_deliverable` MCP tool with `deliverable_type: "AUTH_QUEUE"` and `content: {"vulnerabilities": [...]}`
|
||||
2. **Deliverable Emission:** Call the 3 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` is recommended (an empty array is acceptable but explicit emission is preferred).
|
||||
|
||||
**ONLY AFTER** both systematic analysis AND successful deliverable generation, announce "**AUTH ANALYSIS COMPLETE**" and stop.
|
||||
**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls.
|
||||
|
||||
**ONLY AFTER** both systematic analysis AND the required MCP tool calls have been completed, announce "**AUTH ANALYSIS COMPLETE**" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -19,10 +19,16 @@ Success criterion: A complete, code-backed analysis of every potential authoriza
|
||||
@include(shared/_rules.txt)
|
||||
</rules>
|
||||
|
||||
@include(shared/_code-path-rules.txt)
|
||||
|
||||
@include(shared/_shared-session.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<critical>
|
||||
**Your Professional Standard**
|
||||
- **Severity Context:** A flaw in authorization is a critical security failure. It allows privilege escalation, horizontal and vertical access control bypass, and unauthorized data access. These vulnerabilities enable attackers to access resources they shouldn't, escalate privileges, and compromise multi-tenant data isolation.
|
||||
@@ -32,7 +38,7 @@ Success criterion: A complete, code-backed analysis of every potential authoriza
|
||||
</critical>
|
||||
|
||||
<starting_context>
|
||||
- Your **primary source of truth** for authorization test targets is the reconnaissance report at `deliverables/recon_deliverable.md`. Look specifically for:
|
||||
- Your **primary source of truth** for authorization test targets is the reconnaissance report at `.shannon/deliverables/recon_deliverable.md`. Look specifically for:
|
||||
- **"Horizontal" section:** Endpoints where users access resources by ID that might belong to other users
|
||||
- **"Vertical" section:** Admin/privileged endpoints that regular users shouldn't access
|
||||
- **"Context" section:** Multi-step workflows where order/state matters
|
||||
@@ -44,8 +50,8 @@ Success criterion: A complete, code-backed analysis of every potential authoriza
|
||||
|
||||
**Phase Sequence:** RECON (Complete) → **AUTHZ ANALYSIS (You)** → EXPLOITATION (next phase)
|
||||
|
||||
**Your Input:** `deliverables/recon_deliverable.md`, `deliverables/pre_recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `deliverables/authz_exploitation_queue.json` (actionable vulnerabilities for exploitation)
|
||||
**Your Input:** `.shannon/deliverables/recon_deliverable.md`, `.shannon/deliverables/pre_recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `.shannon/deliverables/authz_analysis_deliverable.md` (analysis report for exploitation phase)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture, endpoints, and data flows
|
||||
@@ -71,7 +77,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a
|
||||
</exploitable_vulnerability_definition>
|
||||
</definitions>
|
||||
|
||||
<available_tools>
|
||||
<cli_tools>
|
||||
|
||||
**CRITICAL TOOL USAGE RESTRICTIONS:**
|
||||
- NEVER use the Read tool for application source code analysis—delegate every code review to the Task Agent.
|
||||
@@ -80,21 +86,15 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a
|
||||
|
||||
**Available Tools:**
|
||||
- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace authorization logic paths, and understand permission models. MANDATORY for all source code analysis.
|
||||
- **save_deliverable (MCP Tool):** Saves deliverable files with automatic validation.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "AUTHZ_ANALYSIS" or "AUTHZ_QUEUE" (required)
|
||||
- `content`: Your markdown report or JSON queue (required)
|
||||
- **Returns:** `{ status: "success", filepath: "...", validated: true/false }` on success or `{ status: "error", message: "...", errorType: "...", retryable: true/false }` on failure
|
||||
- **Usage:** Call the tool with your deliverable type and content. Queue files must have `{"vulnerabilities": [...]}` structure and will be validated automatically.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
- **{{MCP_SERVER}} (Playwright):** To interact with the live web application to understand multi-step flows and role-based access controls.
|
||||
- **Browser Automation (playwright-cli skill):** For browser interactions with the live web application (multi-step flows and role-based access controls), invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each endpoint that needs authorization analysis. Mark items as "in_progress" when working on them and "completed" when done.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
<data_format_specifications>
|
||||
|
||||
<exploitation_queue_format>
|
||||
**Purpose:** Defines the structure for a "exploitation queue" saved via the save_deliverable script with type AUTHZ_QUEUE.
|
||||
**Purpose:** Defines the format of the exploitation queue JSON.
|
||||
**Structure:** The `vulnerability` JSON object MUST follow this exact format:
|
||||
{
|
||||
"ID": "unique ID for each vulnerability (e.g., AUTHZ-VULN-01)",
|
||||
@@ -126,7 +126,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a
|
||||
### 1) Horizontal Authorization Analysis
|
||||
|
||||
- **Create To Dos:**
|
||||
For each item listed under **`deliverables/recon_deliverable.md` → section 8 "Horizontal"*, use the TodoWrite tool to create a task entry.
|
||||
For each item listed under **`.shannon/deliverables/recon_deliverable.md` → section 8 "Horizontal"*, use the TodoWrite tool to create a task entry.
|
||||
|
||||
- **Process:**
|
||||
- Start at the identified endpoint.
|
||||
@@ -158,7 +158,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a
|
||||
### 2) Vertical Authorization Analysis
|
||||
|
||||
- **Create To Dos:**
|
||||
For each item listed under **`deliverables/recon_deliverable.md` → section 8 "Vertical"**, use the TodoWrite tool to create a task entry.
|
||||
For each item listed under **`.shannon/deliverables/recon_deliverable.md` → section 8 "Vertical"**, use the TodoWrite tool to create a task entry.
|
||||
|
||||
- **Process:**
|
||||
- Start at the identified endpoint.
|
||||
@@ -184,7 +184,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a
|
||||
### 3) Context / Workflow Authorization Analysis
|
||||
|
||||
- **Create To Dos:**
|
||||
For each item listed under **`deliverables/recon_deliverable.md` → section 8 "Context"**, use the TodoWrite tool to create a task entry.
|
||||
For each item listed under **`.shannon/deliverables/recon_deliverable.md` → section 8 "Context"**, use the TodoWrite tool to create a task entry.
|
||||
|
||||
- **Process:**
|
||||
- Start at the endpoint that represents a step in a workflow.
|
||||
@@ -239,7 +239,7 @@ An **exploitable vulnerability** is a logical flaw in the code that represents a
|
||||
|
||||
For each analysis you perform from the lists above, you must make a final **verdict**:
|
||||
|
||||
- If the verdict is **`vulnerable`**, you must document the finding using the save_deliverable script to update the exploitation queue.
|
||||
- If the verdict is **`vulnerable`**, you must include the finding in your exploitation queue.
|
||||
- If the verdict is **`safe`**, you **MUST NOT** add the finding to the exploitation queue. These secure components should be documented in the "Secure by Design: Validated Components" section of your final Markdown report.
|
||||
</methodology>
|
||||
|
||||
@@ -272,93 +272,38 @@ For each analysis you perform from the lists above, you must make a final **verd
|
||||
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
When you have systematically analyzed all relevant endpoints and logic paths, you MUST generate three final files. Follow these instructions precisely to structure your output.
|
||||
<mcp_tools>
|
||||
After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content.
|
||||
|
||||
**1. Your Specialist Deliverable**
|
||||
**Tool catalog:**
|
||||
- `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns)
|
||||
- `set_strategic_intelligence` — Section 3 (Strategic Intelligence for Exploitation, with authz-specific sub-fields: session management architecture, role/permission model, resource access patterns, workflow implementation)
|
||||
- `set_safe_vectors` — Section 4 (vectors confirmed secure)
|
||||
- `set_blind_spots` — Section 5 (analysis constraints and blind spots)
|
||||
|
||||
First, synthesize all of your findings into a single, detailed Markdown report and save it using the save_deliverable script with type AUTHZ_ANALYSIS. This report is the official record of your work.
|
||||
The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. For authz specifically, when populating `set_safe_vectors`, the renderer maps `subject` to the "Endpoint" column header and `location` to the "Guard Location" column header.
|
||||
|
||||
Your report MUST use the following structure precisely:
|
||||
**Call semantics:** All 4 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting.
|
||||
|
||||
---
|
||||
# Authorization Analysis Report
|
||||
**Required vs recommended:**
|
||||
- `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-authz` agent reads.
|
||||
- `set_safe_vectors` and `set_blind_spots` are recommended. Empty arrays are acceptable on runs with no validated-secure endpoints or no constraint gaps, but explicit emission is preferred over skipping.
|
||||
|
||||
## 1. Executive Summary
|
||||
**Relationship to the exploitation queue:** The exploitation queue (`authz_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 4 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above.
|
||||
</mcp_tools>
|
||||
|
||||
- **Analysis Status:** Complete
|
||||
- **Key Outcome:** ONLY high-confidence authorization vulnerabilities (horizontal, vertical, and context/workflow) were recorded. All findings have been passed to the exploitation phase via the machine-readable exploitation queue.
|
||||
- **Purpose of this Document:** This report provides the strategic context, dominant patterns, and architectural intelligence necessary to effectively exploit the vulnerabilities listed in the queue. It is intended to be read alongside the JSON deliverable.
|
||||
|
||||
## 2. Dominant Vulnerability Patterns
|
||||
|
||||
### Pattern 1: Missing Ownership Validation (Horizontal | Vertical | Context)
|
||||
- **Description:** Multiple endpoints accept resource IDs without verifying the requesting user owns or has access to that resource
|
||||
- **Implication:** Users can access and modify other users' private data by manipulating ID parameters
|
||||
- **Representative:** AUTHZ-VULN-01, AUTHZ-VULN-03, AUTHZ-VULN-07
|
||||
|
||||
etc...
|
||||
|
||||
## 3. Strategic Intelligence for Exploitation
|
||||
examples:
|
||||
- **Session Management Architecture:**
|
||||
- Sessions use JWT tokens stored in cookies with `httpOnly` flag
|
||||
- User ID is extracted from token but not consistently validated against resource ownership
|
||||
- **Critical Finding:** The application trusts the user ID from the token without additional checks
|
||||
|
||||
- **Role/Permission Model:**
|
||||
- Three roles identified: `user`, `moderator`, `admin`
|
||||
- Role is stored in JWT token and database
|
||||
- **Critical Finding:** Role checks are inconsistently applied; many admin routes only check for authentication
|
||||
|
||||
- **Resource Access Patterns:**
|
||||
- Most endpoints use path parameters for resource IDs (e.g., `/api/users/{id}`)
|
||||
- **Critical Finding:** ID parameters are directly passed to database queries without ownership validation
|
||||
|
||||
- **Workflow Implementation:**
|
||||
- Multi-step processes use status fields in database
|
||||
- **Critical Finding:** Status transitions don't verify prior state completion
|
||||
|
||||
## 4. Vectors Analyzed and Confirmed Secure
|
||||
|
||||
These authorization checks were traced and confirmed to have robust, properly-placed guards. They are **low-priority** for further testing.
|
||||
|
||||
| **Endpoint** | **Guard Location** | **Defense Mechanism** | **Verdict** |
|
||||
|--------------|-------------------|----------------------|-------------|
|
||||
| `POST /api/auth/logout` | middleware/auth.js:45 | Session validation only (appropriate for logout) | SAFE |
|
||||
| `GET /api/public/*` | routes/public.js:12 | Public endpoints, no auth required by design | SAFE |
|
||||
| `GET /api/users/me` | controllers/user.js:89 | Uses session user ID, no parameter manipulation possible | SAFE |
|
||||
|
||||
## 5. Analysis Constraints and Blind Spots
|
||||
examples:
|
||||
- **Untraced Microservice Calls:**
|
||||
Some endpoints make calls to internal microservices. Authorization checks within these services could not be analyzed without their source code.
|
||||
|
||||
- **Dynamic Permission System:**
|
||||
The application appears to have a dynamic permission system loaded from database. Runtime permission checks could not be fully validated through static analysis.
|
||||
|
||||
---
|
||||
|
||||
</deliverable_instructions>
|
||||
|
||||
<exploitation_queue_requirements>
|
||||
**Exploitation Queue (MANDATORY)**
|
||||
Regardless of whether vulnerabilities are found, you MUST create the exploitation queue using the save_deliverable MCP tool:
|
||||
- **If vulnerabilities found:** Use `save_deliverable` MCP tool with `deliverable_type: "AUTHZ_QUEUE"` and `content: {"vulnerabilities": [...]}` with each confirmed hypothesis following the `exploitation_queue_format`. Set `externally_exploitable` to `true` ONLY if exploitable via public internet without internal access.
|
||||
- **If no vulnerabilities found:** Use `save_deliverable` MCP tool with `deliverable_type: "AUTHZ_QUEUE"` and `content: {"vulnerabilities": []}`.
|
||||
- **QUEUE INCLUSION CRITERIA:** ONLY include vulnerabilities where `externally_exploitable = true`. Exclude any vulnerability requiring internal network access, VPN, or direct server access.
|
||||
This file serves as the handoff mechanism and must always be created to signal completion.
|
||||
</exploitation_queue_requirements>
|
||||
|
||||
<conclusion_trigger>
|
||||
**COMPLETION REQUIREMENTS (ALL must be satisfied):**
|
||||
|
||||
1. **Todo Completion:** ALL tasks in your TodoWrite list must be marked as "completed"
|
||||
2. **Deliverable Generation:** Both required deliverables must be successfully saved using save_deliverable MCP tool:
|
||||
- Analysis report: Use `save_deliverable` MCP tool with `deliverable_type: "AUTHZ_ANALYSIS"` and your report as `content`
|
||||
- Exploitation queue: Use `save_deliverable` MCP tool with `deliverable_type: "AUTHZ_QUEUE"` and `content: {"vulnerabilities": [...]}`
|
||||
1. **Todo Completion:** ALL tasks in your TodoWrite list must be marked as "completed".
|
||||
2. **Deliverable Emission:** Call the 4 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays acceptable but explicit emission is preferred).
|
||||
|
||||
**ONLY AFTER** both todo completion AND successful deliverable generation, announce "**AUTHORIZATION ANALYSIS COMPLETE**" and stop.
|
||||
**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls.
|
||||
|
||||
**FAILURE TO COMPLETE TODOS = INCOMPLETE ANALYSIS** - You will be considered to have failed the mission if you generate deliverables before completing comprehensive testing of all authorization vectors.
|
||||
**ONLY AFTER** both todo completion AND the required MCP tool calls have been completed, announce "**AUTHORIZATION ANALYSIS COMPLETE**" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
|
||||
**FAILURE TO COMPLETE TODOS = INCOMPLETE ANALYSIS** - You will be considered to have failed the mission if you emit MCP deliverable calls before completing comprehensive testing of all authorization vectors.
|
||||
</conclusion_trigger>
|
||||
@@ -20,10 +20,16 @@ Success criterion: Complete source-to-sink traces detailing path, sanitizers, si
|
||||
@include(shared/_rules.txt)
|
||||
</rules>
|
||||
|
||||
@include(shared/_code-path-rules.txt)
|
||||
|
||||
@include(shared/_shared-session.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<critical>
|
||||
**Your Professional Standard**
|
||||
- **Severity Context:** A structural flaw in a backend command, whether an SQL query or a shell command, is one of the most severe classes of vulnerability in a SaaS application. It undermines the foundational trust of the system by creating the *potential* for data exposure (SQLi) or direct server compromise (Command Injection).
|
||||
@@ -33,7 +39,7 @@ Success criterion: Complete source-to-sink traces detailing path, sanitizers, si
|
||||
</critical>
|
||||
|
||||
<starting_context>
|
||||
- Your **single source of truth** for the application's structure is the reconnaissance report located at `deliverables/recon_deliverable.md`. You must derive your list of testable targets from this file.
|
||||
- Your **single source of truth** for the application's structure is the reconnaissance report located at `.shannon/deliverables/recon_deliverable.md`. You must derive your list of testable targets from this file.
|
||||
|
||||
</starting_context>
|
||||
|
||||
@@ -42,8 +48,8 @@ Success criterion: Complete source-to-sink traces detailing path, sanitizers, si
|
||||
|
||||
**Phase Sequence:** RECON (Complete) → **INJECTION ANALYSIS (You)** → EXPLOITATION (next phase)
|
||||
|
||||
**Your Input:** `deliverables/recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `deliverables/injection_exploitation_queue.json` (actionable vulnerabilities for exploitation)
|
||||
**Your Input:** `.shannon/deliverables/recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `.shannon/deliverables/injection_analysis_deliverable.md` (analysis report for exploitation phase)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture, attack surfaces, endpoints, input vectors
|
||||
@@ -71,7 +77,7 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en
|
||||
</exploitable_vulnerability_definition>
|
||||
</definitions>
|
||||
|
||||
<available_tools>
|
||||
<cli_tools>
|
||||
|
||||
**CRITICAL TOOL USAGE RESTRICTIONS:**
|
||||
- NEVER use the Read tool for application source code analysis—delegate every code review to the Task Agent.
|
||||
@@ -80,21 +86,15 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en
|
||||
|
||||
**Available Tools:**
|
||||
- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, map query/command construction paths, and verify sanitization coverage. MANDATORY for all source code analysis.
|
||||
- **save_deliverable (MCP Tool):** Saves deliverable files with automatic validation.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "INJECTION_ANALYSIS" or "INJECTION_QUEUE" (required)
|
||||
- `content`: Your markdown report or JSON queue (required)
|
||||
- **Returns:** `{ status: "success", filepath: "...", validated: true/false }` on success or `{ status: "error", message: "...", errorType: "...", retryable: true/false }` on failure
|
||||
- **Usage:** Call the tool with your deliverable type and content. Queue files must have `{"vulnerabilities": [...]}` structure and will be validated automatically.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
- **{{MCP_SERVER}} (Playwright):** To interact with the live web application to understand multi-step flows like password reset or registration.
|
||||
- **Browser Automation (playwright-cli skill):** For browser interactions with the live web application (multi-step flows like password reset or registration), invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each injection source that needs analysis. Mark items as "in_progress" when working on them and "completed" when done.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
<data_format_specifications>
|
||||
|
||||
<exploitation_queue_format>
|
||||
**Purpose:** Defines the structure for a "exploitation queue" saved via the save_deliverable script with type INJECTION_QUEUE.
|
||||
**Purpose:** Defines the format of the exploitation queue JSON.
|
||||
|
||||
**Structure:** The `vulnerability` JSON object MUST follow this exact format:
|
||||
{
|
||||
@@ -125,7 +125,7 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en
|
||||
|
||||
- **Goal:** Prove whether untrusted input can influence the **structure** of a backend command (SQL or Shell) or reach sensitive **slots** without the correct defense. No live exploitation in this phase.
|
||||
- **1) Create a To Do for each Injection Source found in the Pre-Recon Deliverable
|
||||
- inside of deliverables/pre_recon_deliverable.md under the section "7. Injection Sources (Command Injection and SQL Injection)" use the TodoWrite tool to create a task for each discovered Injection Source.
|
||||
- inside of .shannon/deliverables/pre_recon_deliverable.md under the section "7. Injection Sources (Command Injection and SQL Injection)" use the TodoWrite tool to create a task for each discovered Injection Source.
|
||||
- Note: All sources are marked as Tainted until they Hit a Santiization that matches the sink context. normalizers (lowercasing, trimming, JSON parse, schema decode) — still **tainted**.
|
||||
- **2) Trace Data Flow Paths from Source to Sink**
|
||||
- For each source, your goal is to identify every unique "Data Flow Path" to a database sink. A path is a distinct route the data takes through the code.
|
||||
@@ -148,8 +148,8 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en
|
||||
- Include a short rationale (e.g., "context mismatch: regex escape on ORDER BY keyword slot").
|
||||
- If concat occurred **after** sanitization, treat that sanitization as **non-effective** for this path.
|
||||
- **6) Append to findings list (consistent fields)**
|
||||
- **If the verdict is `vulnerable`:** Use the Write tool to log the finding in `deliverables/injection_exploitation_queue.json`. Set `externally_exploitable` to `true` ONLY if exploitable via public internet without internal access. Ensure all fields in the `exploitation_queue_format`, including a minimal `witness_payload`, are populated.
|
||||
- **If the verdict is `safe`:** DO NOT add the finding to the exploitation queue. These secure vectors must be documented later in the "Vectors Analyzed and Confirmed Secure" section of your final Markdown report (`deliverables/injection_analysis_deliverable.md`).
|
||||
- **If the verdict is `vulnerable`:** Include the finding in your exploitation queue. Set `externally_exploitable` to `true` ONLY if exploitable via public internet without internal access. Ensure all fields in the `exploitation_queue_format`, including a minimal `witness_payload`, are populated.
|
||||
- **If the verdict is `safe`:** DO NOT add the finding to the exploitation queue. These secure vectors must be documented later in the "Vectors Analyzed and Confirmed Secure" section of your final Markdown report (`.shannon/deliverables/injection_analysis_deliverable.md`).
|
||||
- **If a single source is found to be vulnerable via multiple, distinct paths to different sinks, you must create a separate vulnerability entry in the exploitation queue for each unique vulnerable path.**
|
||||
- **QUEUE INCLUSION CRITERIA:** ONLY include vulnerabilities where `externally_exploitable = true`. Exclude any vulnerability requiring internal network access, VPN, or direct server access.
|
||||
|
||||
@@ -283,89 +283,38 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en
|
||||
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
When you have systematically analyzed all input vectors, you MUST generate two final files. Follow these instructions precisely to structure your output.
|
||||
<mcp_tools>
|
||||
After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content.
|
||||
|
||||
**1. Your Specialist Deliverable**
|
||||
**Tool catalog:**
|
||||
- `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns)
|
||||
- `set_strategic_intelligence` — Section 3 (Strategic Intelligence for Exploitation, with injection-specific sub-fields: defensive evasion / WAF analysis, error-based injection potential, confirmed database technology)
|
||||
- `set_safe_vectors` — Section 4 (vectors confirmed secure)
|
||||
- `set_blind_spots` — Section 5 (analysis constraints and blind spots)
|
||||
|
||||
First, synthesize all of your findings into a single, detailed Markdown report located at `deliverables/injection_analysis_deliverable.md`. This report is the official record of your work.
|
||||
The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects.
|
||||
|
||||
Your report MUST use the following structure precisely:
|
||||
**Call semantics:** All 4 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting.
|
||||
|
||||
---
|
||||
#Injection Analysis Report (SQLi & Command Injection)
|
||||
**Required vs recommended:**
|
||||
- `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-injection` agent reads.
|
||||
- `set_safe_vectors` and `set_blind_spots` are recommended. Empty arrays are acceptable on runs with no validated-secure vectors or no constraint gaps, but explicit emission is preferred over skipping.
|
||||
|
||||
## 1. Executive Summary
|
||||
**Relationship to the exploitation queue:** The exploitation queue (`injection_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 4 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above.
|
||||
</mcp_tools>
|
||||
|
||||
- **Analysis Status:** Complete
|
||||
- **Key Outcome:** Several high-confidence SQL injection injection vulnerabilities (both SQLi and Command Injection) were identified. All findings have been passed to the exploitation phase via the machine-readable queue at `deliverables/injection_exploitation_queue.json`.
|
||||
- **Purpose of this Document:** This report provides the strategic context, dominant patterns, and environmental intelligence necessary to effectively exploit the vulnerabilities listed in the queue. It is intended to be read alongside the JSON deliverable.
|
||||
|
||||
|
||||
## 2. Dominant Vulnerability Patterns
|
||||
-**Description:** descirption of the pattern found in the codebase
|
||||
-**Implication:** implication of the pattern
|
||||
-**representative:** ID of a vulnerbaility that exhibits this pattern
|
||||
|
||||
|
||||
## 3. Strategic Intelligence for Exploitation
|
||||
|
||||
- **Defensive Evasion (WAF Analysis):**
|
||||
- A Web Application Firewall is active and blocks common payloads containing boolean logic (`' OR '1'='1`) and `UNION SELECT`.
|
||||
- **Critical Bypass:** The WAF failed to block **time-based blind injection** payloads (e.g., `pg_sleep()`). It also does not inspect the structure of complex, nested queries.
|
||||
- **Recommendation:** Start with **time-based techniques** for exploitation.
|
||||
|
||||
- **Error-Based Injection Potential:**
|
||||
- The `/api/products` endpoint returns verbose PostgreSQL error messages to the client.
|
||||
- **Recommendation:** This endpoint is a prime target for **error-based exploitation** to rapidly extract schema and data.
|
||||
|
||||
- **Confirmed Database Technology:**
|
||||
- Database is **PostgreSQL**, confirmed via error syntax and function calls (`pg_sleep`).
|
||||
- All payloads should be PostgreSQL-specific.
|
||||
|
||||
## 4. Vectors Analyzed and Confirmed Secure
|
||||
|
||||
These input vectors were traced and confirmed to have robust, context-appropriate defenses. They are **low-priority** for further testing.
|
||||
|
||||
| **Source (Parameter/Key)** | **Endpoint/File Location** | **Defense Mechanism Implemented** | **Verdict** |
|
||||
|-----------------------------|--------------------------------|-------------------------------------------|-------------|
|
||||
| `username` | `/api/login` | Prepared Statement (Parameter Binding) | SAFE |
|
||||
| `user_id` | `/api/users/{user_id}` | Input correctly cast to an Integer | SAFE |
|
||||
| `status` | `/api/orders` | Strict Whitelist Validation | SAFE |
|
||||
|
||||
## 5. Analysis Constraints and Blind Spots
|
||||
|
||||
- **Untraced Asynchronous Flows:**
|
||||
Analysis of a background job triggered by file uploads was inconclusive. The data flow passes through **RabbitMQ**, and static analysis could not confirm whether tainted data reaches a DB sink. This remains a blind spot.
|
||||
|
||||
- **Limited Visibility into Stored Procedures:**
|
||||
The application calls a stored procedure: `sp_calculate_report`. Source code for this procedure was not available. While inputs appear correctly typed, injection inside the procedure cannot be ruled out.
|
||||
|
||||
---
|
||||
|
||||
|
||||
</deliverable_instructions>
|
||||
|
||||
<exploitation_queue_requirements>
|
||||
**Exploitation Queue (MANDATORY)**
|
||||
|
||||
Regardless of whether vulnerabilities are found, you MUST create the exploitation queue using the save_deliverable MCP tool:
|
||||
|
||||
- **If vulnerabilities found:** Use `save_deliverable` MCP tool with `deliverable_type: "INJECTION_QUEUE"` and `content: {"vulnerabilities": [...]}` with each exploitable injection vulnerability (verdict: "vulnerable") following the exploitation_queue_format
|
||||
- **If no vulnerabilities found:** Use `save_deliverable` MCP tool with `deliverable_type: "INJECTION_QUEUE"` and `content: {"vulnerabilities": []}`
|
||||
|
||||
This file serves as the handoff mechanism to the Exploitation phase and must always be created to signal completion of your analysis.
|
||||
</exploitation_queue_requirements>
|
||||
|
||||
<conclusion_trigger>
|
||||
**COMPLETION REQUIREMENTS (ALL must be satisfied):**
|
||||
|
||||
1. **Todo Completion:** ALL tasks in your TodoWrite list must be marked as "completed"
|
||||
2. **Deliverable Generation:** Both required deliverables must be successfully saved using save_deliverable MCP tool:
|
||||
- Analysis report: Use `save_deliverable` MCP tool with `deliverable_type: "INJECTION_ANALYSIS"` and your report as `content`
|
||||
- Exploitation queue: Use `save_deliverable` MCP tool with `deliverable_type: "INJECTION_QUEUE"` and `content: {"vulnerabilities": [...]}`
|
||||
1. **Todo Completion:** ALL tasks in your TodoWrite list must be marked as "completed".
|
||||
2. **Deliverable Emission:** Call the 4 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays acceptable but explicit emission is preferred).
|
||||
|
||||
**ONLY AFTER** both todo completion AND successful deliverable generation, announce "**INJECTION ANALYSIS COMPLETE**" and stop.
|
||||
**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls.
|
||||
|
||||
**FAILURE TO COMPLETE TODOS = INCOMPLETE ANALYSIS** - You will be considered to have failed the mission if you generate deliverables before completing comprehensive testing of all input vectors.
|
||||
**ONLY AFTER** both todo completion AND the required MCP tool calls have been completed, announce "**INJECTION ANALYSIS COMPLETE**" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
|
||||
**FAILURE TO COMPLETE TODOS = INCOMPLETE ANALYSIS** - You will be considered to have failed the mission if you emit MCP deliverable calls before completing comprehensive testing of all input vectors.
|
||||
</conclusion_trigger>
|
||||
@@ -19,10 +19,16 @@ Success criterion: A complete source-to-sink trace for every identified SSRF vul
|
||||
@include(shared/_rules.txt)
|
||||
</rules>
|
||||
|
||||
@include(shared/_code-path-rules.txt)
|
||||
|
||||
@include(shared/_shared-session.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<critical>
|
||||
**Your Professional Standard**
|
||||
- **Severity Context:** An SSRF flaw is a breach of network segmentation and trust boundaries. It allows attackers to leverage the application server as a proxy to access internal services, cloud metadata endpoints, or perform reconnaissance of internal networks. These vulnerabilities can lead to data exposure, privilege escalation, and complete network compromise.
|
||||
@@ -32,7 +38,7 @@ Success criterion: A complete source-to-sink trace for every identified SSRF vul
|
||||
</critical>
|
||||
|
||||
<starting_context>
|
||||
- Your primary source of truth for the application's structure is the reconnaissance report located at `deliverables/recon_deliverable.md`. You must derive your list of testable endpoints from this file.
|
||||
- Your primary source of truth for the application's structure is the reconnaissance report located at `.shannon/deliverables/recon_deliverable.md`. You must derive your list of testable endpoints from this file.
|
||||
</starting_context>
|
||||
|
||||
<system_architecture>
|
||||
@@ -40,8 +46,8 @@ Success criterion: A complete source-to-sink trace for every identified SSRF vul
|
||||
|
||||
**Phase Sequence:** RECON (Complete) → **SSRF ANALYSIS (You)** → EXPLOITATION (next phase)
|
||||
|
||||
**Your Input:** `deliverables/recon_deliverable.md`, `deliverables/pre_recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `deliverables/ssrf_exploitation_queue.json` (actionable vulnerabilities for exploitation)
|
||||
**Your Input:** `.shannon/deliverables/recon_deliverable.md`, `.shannon/deliverables/pre_recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `.shannon/deliverables/ssrf_analysis_deliverable.md` (analysis report for exploitation phase)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture, endpoints, and data flows
|
||||
@@ -67,7 +73,7 @@ An **exploitable vulnerability** is a data flow where user-controlled input infl
|
||||
</exploitable_vulnerability_definition>
|
||||
</definitions>
|
||||
|
||||
<available_tools>
|
||||
<cli_tools>
|
||||
|
||||
**CRITICAL TOOL USAGE RESTRICTIONS:**
|
||||
- NEVER use the Read tool for application source code analysis—delegate every code review to the Task Agent.
|
||||
@@ -76,21 +82,15 @@ An **exploitable vulnerability** is a data flow where user-controlled input infl
|
||||
|
||||
**Available Tools:**
|
||||
- **Task Agent (Code Analysis):** Your primary tool. Use it to ask targeted questions about the source code, trace data flows, and understand HTTP client usage. MANDATORY for all source code analysis.
|
||||
- **save_deliverable (MCP Tool):** Saves deliverable files with automatic validation.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "SSRF_ANALYSIS" or "SSRF_QUEUE" (required)
|
||||
- `content`: Your markdown report or JSON queue (required)
|
||||
- **Returns:** `{ status: "success", filepath: "...", validated: true/false }` on success or `{ status: "error", message: "...", errorType: "...", retryable: true/false }` on failure
|
||||
- **Usage:** Call the tool with your deliverable type and content. Queue files must have `{"vulnerabilities": [...]}` structure and will be validated automatically.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
- **{{MCP_SERVER}} (Playwright):** To interact with the live web application to understand multi-step flows that might involve URL redirection or proxy functionality.
|
||||
- **Browser Automation (playwright-cli skill):** For browser interactions with the live web application (multi-step flows involving URL redirection or proxy functionality), invoke the `playwright-cli` skill to learn available commands. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each SSRF sink that needs analysis. Mark items as "in_progress" when working on them and "completed" when done.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
<data_format_specifications>
|
||||
|
||||
<exploitation_queue_format>
|
||||
**Purpose:** Defines the structure for a "exploitation queue" saved via the save_deliverable script with type SSRF_QUEUE.
|
||||
**Purpose:** Defines the format of the exploitation queue JSON.
|
||||
**Structure:** The `vulnerability` JSON object MUST follow this exact format:
|
||||
{
|
||||
"ID": "unique ID for each vulnerability json (SSRF-VULN-XX)",
|
||||
@@ -115,7 +115,7 @@ An **exploitable vulnerability** is a data flow where user-controlled input infl
|
||||
# White-Box Vulnerability Analysis Procedure: Server-Side Request Forgery (SSRF)
|
||||
|
||||
NOTE ON "FOR ALL": whenever an item begins with "for all …", use the TodoWrite tool to create a separate task for each relevant endpoint/flow that needs to be analyzed.
|
||||
From `deliverables/pre_recon_deliverable.md`, use Section 10 (SSRF Sinks) to guide your tasks.
|
||||
From `.shannon/deliverables/pre_recon_deliverable.md`, use Section 10 (SSRF Sinks) to guide your tasks.
|
||||
|
||||
## 1) Identify HTTP Client Usage Patterns
|
||||
- For all endpoints that accept URL parameters, callback URLs, webhook URLs, or file paths, trace how these inputs are processed.
|
||||
@@ -167,7 +167,7 @@ From `deliverables/pre_recon_deliverable.md`, use Section 10 (SSRF Sinks) to gui
|
||||
|
||||
### **1) Create a To-Do Item for Each SSRF Sink**
|
||||
|
||||
Inside `deliverables/pre_recon_deliverable.md` under section `##10. SSRF Sinks##`.
|
||||
Inside `.shannon/deliverables/pre_recon_deliverable.md` under section `##10. SSRF Sinks##`.
|
||||
|
||||
Use the TodoWrite tool to create a task for each discovered sink (any server-side request composed even partially from user input).
|
||||
|
||||
@@ -229,7 +229,7 @@ Rule: when uncertain, round down (favor Medium/Low) to minimize false positives.
|
||||
# Documenting Findings (MANDATORY)
|
||||
For each check you perform from the list above, you must make a final **verdict**.
|
||||
|
||||
- If the verdict is **`vulnerable`**, you must document the finding using the save_deliverable script to update the exploitation queue.
|
||||
- If the verdict is **`vulnerable`**, you must include the finding in your exploitation queue.
|
||||
- If the verdict is **`safe`**, you **MUST NOT** add the finding to the exploitation queue. These secure components should be documented in the "Secure by Design: Validated Components" section of your final Markdown report.
|
||||
</methodology>
|
||||
|
||||
@@ -243,64 +243,35 @@ For each check you perform from the list above, you must make a final **verdict*
|
||||
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
When you have systematically analyzed all relevant endpoints and request-making functions, you MUST generate two final files. Follow these instructions precisely.
|
||||
<mcp_tools>
|
||||
After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 3 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content.
|
||||
|
||||
**1. Your Specialist Deliverable**
|
||||
First, synthesize all of your findings into a detailed Markdown report and save it using the save_deliverable script with type SSRF_ANALYSIS.
|
||||
Your report MUST use the following structure precisely:
|
||||
**Tool catalog:**
|
||||
- `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns)
|
||||
- `set_strategic_intelligence` — Section 3 (Strategic Intelligence for Exploitation, with SSRF-specific sub-fields: HTTP client library, request architecture, internal services)
|
||||
- `set_safe_vectors` — Section 4 (Secure by Design: Validated Components)
|
||||
|
||||
---
|
||||
# SSRF Analysis Report
|
||||
The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects.
|
||||
|
||||
## 1. Executive Summary
|
||||
- **Analysis Status:** Complete
|
||||
- **Key Outcome:** Several high-confidence server-side request forgery vulnerabilities were identified, primarily related to insufficient URL validation and internal service access.
|
||||
- **Purpose of this Document:** This report provides the strategic context on the application's outbound request mechanisms, dominant flaw patterns, and key architectural details necessary to effectively exploit the vulnerabilities listed in the exploitation queue.
|
||||
**Call semantics:** All 3 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting.
|
||||
|
||||
## 2. Dominant Vulnerability Patterns
|
||||
**Required vs recommended:**
|
||||
- `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-ssrf` agent reads.
|
||||
- `set_safe_vectors` is recommended. An empty array is acceptable on runs with no validated-secure components, but explicit emission is preferred over skipping.
|
||||
|
||||
### Pattern 1: Insufficient URL Validation
|
||||
- **Description:** A recurring and critical pattern was observed where user-supplied URLs are not properly validated before being used in outbound HTTP requests.
|
||||
- **Implication:** Attackers can force the server to make requests to internal services, cloud metadata endpoints, or arbitrary external resources.
|
||||
- **Representative Findings:** `SSRF-VULN-01`, `SSRF-VULN-02`.
|
||||
**Relationship to the exploitation queue:** The exploitation queue (`ssrf_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 3 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above.
|
||||
</mcp_tools>
|
||||
|
||||
### Pattern 2: Missing Protocol Restrictions
|
||||
- **Description:** Endpoints accepting URL parameters do not restrict the protocol schemes that can be used.
|
||||
- **Implication:** Attackers can use dangerous schemes like file:// or gopher:// to access local files or perform protocol smuggling.
|
||||
- **Representative Finding:** `SSRF-VULN-03`.
|
||||
|
||||
## 3. Strategic Intelligence for Exploitation
|
||||
- **HTTP Client Library:** The application uses [HTTP_CLIENT_LIBRARY] for outbound requests.
|
||||
- **Request Architecture:** [DETAILS_ABOUT_REQUEST_PATTERNS]
|
||||
- **Internal Services:** [DISCOVERED_INTERNAL_SERVICES_OR_ENDPOINTS]
|
||||
|
||||
## 4. Secure by Design: Validated Components
|
||||
These components were analyzed and found to have robust defenses. They are low-priority for further testing.
|
||||
| Component/Flow | Endpoint/File Location | Defense Mechanism Implemented | Verdict |
|
||||
|---|---|---|---|
|
||||
| Image Upload Processing | `/controllers/uploadController.js` | Uses strict allowlist for image URLs with protocol validation. | SAFE |
|
||||
| Webhook Configuration | `/services/webhookService.js` | Implements comprehensive IP address blocklist and timeout controls. | SAFE |
|
||||
---
|
||||
|
||||
</deliverable_instructions>
|
||||
|
||||
<exploitation_queue_requirements>
|
||||
**Exploitation Queue (MANDATORY)**
|
||||
Regardless of whether vulnerabilities are found, you MUST create the exploitation queue using the save_deliverable MCP tool:
|
||||
- **If vulnerabilities found:** Use `save_deliverable` MCP tool with `deliverable_type: "SSRF_QUEUE"` and `content: {"vulnerabilities": [...]}` with each confirmed hypothesis following the `exploitation_queue_format`. Set `externally_exploitable` to `true` ONLY if exploitable via public internet without internal access.
|
||||
- **If no vulnerabilities found:** Use `save_deliverable` MCP tool with `deliverable_type: "SSRF_QUEUE"` and `content: {"vulnerabilities": []}`.
|
||||
- **QUEUE INCLUSION CRITERIA:** ONLY include vulnerabilities where `externally_exploitable = true`. Exclude any vulnerability requiring internal network access, VPN, or direct server access.
|
||||
This file serves as the handoff mechanism and must always be created to signal completion.
|
||||
</exploitation_queue_requirements>
|
||||
|
||||
<conclusion_trigger>
|
||||
**COMPLETION REQUIREMENTS (ALL must be satisfied):**
|
||||
|
||||
1. **Systematic Analysis:** ALL relevant API endpoints and request-making features identified in the reconnaissance deliverable must be analyzed for SSRF vulnerabilities.
|
||||
2. **Deliverable Generation:** Both required deliverables must be successfully saved using save_deliverable MCP tool:
|
||||
- Analysis report: Use `save_deliverable` MCP tool with `deliverable_type: "SSRF_ANALYSIS"` and your report as `content`
|
||||
- Exploitation queue: Use `save_deliverable` MCP tool with `deliverable_type: "SSRF_QUEUE"` and `content: {"vulnerabilities": [...]}`
|
||||
2. **Deliverable Emission:** Call the 3 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` is recommended (an empty array is acceptable but explicit emission is preferred).
|
||||
|
||||
**ONLY AFTER** both systematic analysis AND successful deliverable generation, announce "**SSRF ANALYSIS COMPLETE**" and stop.
|
||||
**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls.
|
||||
|
||||
**ONLY AFTER** both systematic analysis AND the required MCP tool calls have been completed, announce "**SSRF ANALYSIS COMPLETE**" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -19,10 +19,16 @@ Success criterion: Live confirmation of XSS execution for every vulnerability th
|
||||
@include(shared/_rules.txt)
|
||||
</rules>
|
||||
|
||||
@include(shared/_code-path-rules.txt)
|
||||
|
||||
@include(shared/_shared-session.txt)
|
||||
|
||||
<login_instructions>
|
||||
{{LOGIN_INSTRUCTIONS}}
|
||||
</login_instructions>
|
||||
|
||||
@include(shared/_rules-of-engagement.txt)
|
||||
|
||||
<critical>
|
||||
**Your Professional Standard**
|
||||
- Severity Context: An XSS flaw is a breach of trust between the user and the application. It grants an attacker the ability to execute arbitrary code within a victim's browser, leading to session hijacking, credential theft, and complete compromise of the user's account and data within the application.
|
||||
@@ -32,7 +38,7 @@ Success criterion: Live confirmation of XSS execution for every vulnerability th
|
||||
</critical>
|
||||
|
||||
<starting_context>
|
||||
- Your primary source of truth for the application's structure is the reconnaissance report located at deliverables/recon_deliverable.md. You must derive your list of testable targets from this file.
|
||||
- Your primary source of truth for the application's structure is the reconnaissance report located at .shannon/deliverables/recon_deliverable.md. You must derive your list of testable targets from this file.
|
||||
</starting_context>
|
||||
|
||||
<system_architecture>
|
||||
@@ -40,8 +46,8 @@ Success criterion: Live confirmation of XSS execution for every vulnerability th
|
||||
|
||||
**Phase Sequence:** RECON (Complete) → **XSS ANALYSIS (You)** → EXPLOITATION (next phase)
|
||||
|
||||
**Your Input:** `deliverables/recon_deliverable.md`, `deliverables/pre_recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `deliverables/xss_exploitation_queue.json` (actionable vulnerabilities for exploitation)
|
||||
**Your Input:** `.shannon/deliverables/recon_deliverable.md`, `.shannon/deliverables/pre_recon_deliverable.md` (reconnaissance findings)
|
||||
**Your Output:** `.shannon/deliverables/xss_analysis_deliverable.md` (analysis report for exploitation phase)
|
||||
|
||||
**WHAT HAPPENED BEFORE YOU:**
|
||||
- Reconnaissance agent mapped application architecture
|
||||
@@ -68,32 +74,26 @@ An **exploitable vulnerability** is a confirmed source-to-sink path where the en
|
||||
</exploitable_vulnerability_definition>
|
||||
</definitions>
|
||||
|
||||
<available_tools>
|
||||
<cli_tools>
|
||||
|
||||
**CRITICAL TOOL USAGE RESTRICTIONS:**
|
||||
- NEVER use the Read tool for application source code analysis - ALWAYS delegate to Task agents for examining .js, .ts, .py, .php files and application logic. You MAY use Read
|
||||
tool directly for these files: `deliverables/pre_recon_deliverable.md`, `deliverables/recon_deliverable.md`
|
||||
tool directly for these files: `.shannon/deliverables/pre_recon_deliverable.md`, `.shannon/deliverables/recon_deliverable.md`
|
||||
- Direct the Task Agent to trace render contexts, sanitization coverage, and template/component boundaries before deciding on exploitability.
|
||||
- **ALWAYS delegate code analysis to Task agents**
|
||||
|
||||
**Available Tools:**
|
||||
- **Task Agent (Code Analysis):** MANDATORY for all source code analysis and data flow tracing. Use this instead of Read tool for examining application code, models, controllers, and templates.
|
||||
- **Terminal (curl):** MANDATORY for testing HTTP-based XSS vectors and observing raw HTML responses. Use for reflected XSS testing and JSONP injection testing.
|
||||
- **{{MCP_SERVER}} (Playwright):** MANDATORY for testing DOM-based XSS and form submission vectors. Use for stored XSS testing and client-side payload execution verification.
|
||||
- **Browser Automation (playwright-cli skill):** MANDATORY for testing DOM-based XSS and form submission vectors. Invoke the `playwright-cli` skill to learn available commands. Use for stored XSS testing and client-side payload execution verification. Always pass `-s={{PLAYWRIGHT_SESSION}}` to every command for session isolation.
|
||||
- **TodoWrite Tool:** Use this to create and manage your analysis task list. Create a todo item for each sink you need to analyze.
|
||||
- **save_deliverable (MCP Tool):** Saves deliverable files with automatic validation.
|
||||
- **Parameters:**
|
||||
- `deliverable_type`: "XSS_ANALYSIS" or "XSS_QUEUE" (required)
|
||||
- `content`: Your markdown report or JSON queue (required)
|
||||
- **Returns:** `{ status: "success", filepath: "...", validated: true/false }` on success or `{ status: "error", message: "...", errorType: "...", retryable: true/false }` on failure
|
||||
- **Usage:** Call the tool with your deliverable type and content. Queue files must have `{"vulnerabilities": [...]}` structure and will be validated automatically.
|
||||
- **Bash tool:** Use for creating directories, copying files, and other shell commands as needed.
|
||||
</available_tools>
|
||||
</cli_tools>
|
||||
|
||||
<data_format_specifications>
|
||||
|
||||
<exploitation_queue_format>
|
||||
Purpose: Defines the structure for a "exploitation queue" saved via the save_deliverable script with type XSS_QUEUE.
|
||||
Purpose: Defines the structure of the agent's final structured response.
|
||||
Structure: The vulnerability JSON object MUST follow this exact format:
|
||||
{
|
||||
"ID": "unique ID for each vulnerability json (XSS-VULN-XX)",
|
||||
@@ -125,7 +125,7 @@ Structure: The vulnerability JSON object MUST follow this exact format:
|
||||
- **Core Principle:** Data is assumed to be tainted until a context-appropriate output encoder (sanitization) is encountered on its path to the sink.
|
||||
|
||||
### **1) Create a todo item for each XSS sink using the TodoWrite tool**
|
||||
Read deliverables/pre_recon_deliverable.md section ##9. XSS Sinks and Render Contexts## and use the **TodoWrite tool** to create a todo item for each discovered sink-context pair that needs analysis.
|
||||
Read .shannon/deliverables/pre_recon_deliverable.md section ##9. XSS Sinks and Render Contexts## and use the **TodoWrite tool** to create a todo item for each discovered sink-context pair that needs analysis.
|
||||
|
||||
### **2) Trace Each Sink Backward (Backward Taint Analysis)**
|
||||
For each pending item in your todo list (managed via TodoWrite tool), trace the origin of the data variable backward from the sink through the application logic. Your goal is to find either a valid sanitizer or an untrusted source. Mark each todo item as completed after you've fully analyzed that sink.
|
||||
@@ -178,7 +178,7 @@ This rulebook is used for the **Early Termination** check in Step 2.
|
||||
- Include both safe and vulnerable paths to demonstrate **full coverage**.
|
||||
- Craft a minimal `witness_payload` that proves control over the render context.
|
||||
- For every path analyzed, you must document the outcome. The location of the documentation depends on the verdict:
|
||||
- If the verdict is 'vulnerable', you MUST use the save_deliverable script to save the finding to the exploitation queue, including complete source-to-sink information.
|
||||
- If the verdict is 'vulnerable', you MUST include the finding in your final structured response's exploitation queue, including complete source-to-sink information.
|
||||
- If the verdict is 'safe', you MUST NOT add it to the exploitation queue. Instead, you will document these secure paths in the "Vectors Analyzed and Confirmed Secure" table of your final analysis report.
|
||||
- For vulnerable findings, craft a minimal witness_payload that proves control over the render context.
|
||||
|
||||
@@ -205,91 +205,36 @@ This rulebook is used for the **Early Termination** check in Step 2.
|
||||
|
||||
</methodology_and_domain_expertise>
|
||||
|
||||
<deliverable_instructions>
|
||||
<mcp_tools>
|
||||
After completing your TodoWrite tasks and synthesizing findings, emit your specialist deliverable via 4 one-shot MCP tools provided by the `vuln-collector` server. Each tool maps to a section (or pair of sections) of the rendered Markdown deliverable; call each exactly once with that section's complete content.
|
||||
|
||||
When you have systematically analyzed all input vectors, you MUST generate two final files. Follow these instructions precisely.
|
||||
**Tool catalog:**
|
||||
- `set_findings_summary` — Section 1 (Executive Summary key outcome) and Section 2 (Dominant Vulnerability Patterns)
|
||||
- `set_strategic_intelligence` — Section 3 (Strategic Intelligence for Exploitation, with XSS-specific sub-fields: CSP analysis, cookie security)
|
||||
- `set_safe_vectors` — Section 4 (vectors confirmed secure)
|
||||
- `set_blind_spots` — Section 5 (analysis constraints and blind spots)
|
||||
|
||||
## 1. Your Specialist Deliverable
|
||||
The MCP SDK injects each tool's complete description and per-field guidance into your tool catalog — refer to the tool catalog for what each parameter expects. For XSS specifically, when populating `set_safe_vectors`, include the optional `render_context` field on each entry (HTML_BODY, HTML_ATTRIBUTE, JAVASCRIPT_STRING, URL_PARAM, or CSS_VALUE).
|
||||
|
||||
- First, synthesize all of your findings into a single, detailed Markdown report located at
|
||||
`deliverables/xss_analysis_deliverable.md`.
|
||||
**Call semantics:** All 4 tools are one-shot — each may be called exactly once with the section's complete content. Duplicate calls return `"already called"` and are no-ops. There is no incremental/append mode; synthesize each section's full content in working memory before emitting.
|
||||
|
||||
- Your report MUST use the following structure precisely:
|
||||
**Required vs recommended:**
|
||||
- `set_findings_summary` and `set_strategic_intelligence` are required — call both before terminating. They produce the load-bearing content the downstream `exploit-xss` agent reads.
|
||||
- `set_safe_vectors` and `set_blind_spots` are recommended. Empty arrays are acceptable on runs with no validated-secure vectors or no constraint gaps, but explicit emission is preferred over skipping.
|
||||
|
||||
**Relationship to the exploitation queue:** The exploitation queue (`xss_exploitation_queue.json`) is captured automatically from your final structured output at session end. The 4 MCP tools produce the analysis deliverable Markdown; the structured-output queue is separate and follows the `exploitation_queue_format` schema documented above.
|
||||
</mcp_tools>
|
||||
|
||||
# Cross-Site Scripting (XSS) Analysis Report
|
||||
|
||||
## 1. Executive Summary
|
||||
- **Analysis Status:** Complete
|
||||
- **Key Outcome:** Several high-confidence XSS vulnerabilities were identified. All findings have been passed to the exploitation phase via `deliverables/xss_exploitation_queue.json`.
|
||||
- **Purpose of this Document:** This report provides the strategic context, dominant patterns, and environmental intelligence necessary to effectively exploit the vulnerabilities.
|
||||
|
||||
## 2. Dominant Vulnerability Patterns
|
||||
|
||||
**Pattern 1: Reflected XSS in Search Parameter**
|
||||
- **Description:** A recurring pattern was observed where the main search parameter (`?q=`) is reflected directly into the HTML body without any output encoding.
|
||||
- **Implication:** Any page with a search bar is likely vulnerable to basic reflected XSS. This is the easiest vector for exploitation.
|
||||
- **Representative Findings:** XSS-VULN-01, XSS-VULN-03.
|
||||
|
||||
**Pattern 2: DOM-based XSS in URL Hash**
|
||||
- **Description:** Client-side JavaScript reads from `location.hash` and writes the value into a div using `innerHTML` to dynamically load content, without sanitization.
|
||||
- **Implication:** This allows for script execution without the payload ever being sent to the server, potentially bypassing server-side logs and WAFs.
|
||||
- **Representative Finding:** XSS-VULN-02.
|
||||
|
||||
## 3. Strategic Intelligence for Exploitation
|
||||
|
||||
**Content Security Policy (CSP) Analysis**
|
||||
- **Current CSP:** `script-src 'self' https://trusted-cdn.com;`
|
||||
- **Critical Bypass:** The trusted CDN hosts an old version of AngularJS, which is vulnerable to client-side template injection. This provides a clear path to bypass the CSP.
|
||||
- **Recommendation:** Exploitation should focus on crafting AngularJS sandbox escape payloads.
|
||||
|
||||
**Cookie Security**
|
||||
- **Observation:** The primary session cookie (`sessionid`) is missing the `HttpOnly` flag.
|
||||
- **Recommendation:** This is a high-impact finding. The primary goal of exploitation should be to steal this cookie via `document.cookie`.
|
||||
|
||||
## 4. Vectors Analyzed and Confirmed Secure
|
||||
|
||||
These input vectors were traced and confirmed to have robust, context-appropriate defenses.
|
||||
|
||||
| Source (Parameter/Key) | Endpoint/File Location | Defense Mechanism Implemented | Render Context | Verdict |
|
||||
|--------------------------|-------------------------|--------------------------------|----------------|---------|
|
||||
| `username` | `/profile` | HTML Entity Encoding | HTML_BODY | SAFE |
|
||||
| `redirect_url` | `/login` | Strict URL Whitelist Validation | URL_PARAM | SAFE |
|
||||
|
||||
## 5. Analysis Constraints and Blind Spots
|
||||
|
||||
- **Minified JavaScript:** Analysis of the primary client-side bundle (`app.min.js`) was difficult. Some DOM XSS vulnerabilities may have been missed due to obfuscated code.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
</deliverable_instructions>
|
||||
|
||||
<exploitation_queue_requirements>
|
||||
|
||||
## Exploitation Queue (MANDATORY)
|
||||
|
||||
Regardless of whether vulnerabilities are found, you MUST create the exploitation queue using the save_deliverable MCP tool.
|
||||
|
||||
- **If exploitable vulnerabilities found:**
|
||||
Use `save_deliverable` MCP tool with `deliverable_type: "XSS_QUEUE"` and `content: {"vulnerabilities": [...]}` with each exploitable XSS vulnerability (verdict: "vulnerable") following the `exploitation_queue_format`. Set `externally_exploitable` to `true` ONLY if exploitable via public internet without internal access.
|
||||
|
||||
- **If no exploitable vulnerabilities found:**
|
||||
Use `save_deliverable` MCP tool with `deliverable_type: "XSS_QUEUE"` and `content: {"vulnerabilities": []}`
|
||||
|
||||
- **QUEUE INCLUSION CRITERIA:** ONLY include vulnerabilities where `externally_exploitable = true`. Exclude any vulnerability requiring internal network access, VPN, or direct server access.
|
||||
|
||||
This file is the mandatory handoff to the Exploitation phase.
|
||||
</exploitation_queue_requirements>
|
||||
|
||||
<conclusion_trigger>
|
||||
COMPLETION REQUIREMENTS (ALL must be satisfied):
|
||||
|
||||
1. Systematic Analysis: ALL input vectors identified from the reconnaissance deliverable must be analyzed.
|
||||
2. Deliverable Generation: Both required deliverables must be successfully saved using save_deliverable MCP tool:
|
||||
- Analysis report: Use `save_deliverable` MCP tool with `deliverable_type: "XSS_ANALYSIS"` and your report as `content`
|
||||
- Exploitation queue: Use `save_deliverable` MCP tool with `deliverable_type: "XSS_QUEUE"` and `content: {"vulnerabilities": [...]}`
|
||||
2. Deliverable Emission: Call the 4 MCP tools (`set_findings_summary`, `set_strategic_intelligence`, `set_safe_vectors`, `set_blind_spots`) exactly once each with their respective section content. `set_findings_summary` and `set_strategic_intelligence` are required; `set_safe_vectors` and `set_blind_spots` are recommended (empty arrays acceptable but explicit emission is preferred).
|
||||
|
||||
ONLY AFTER both systematic analysis AND successful deliverable generation, announce "XSS ANALYSIS COMPLETE" and stop.
|
||||
**Note:** The exploitation queue is captured automatically from your final structured output at session end — separate from the MCP tools above. The analysis deliverable Markdown is rendered by the harness after your session ends from the MCP tool calls.
|
||||
|
||||
ONLY AFTER both systematic analysis AND the required MCP tool calls have been completed, announce "XSS ANALYSIS COMPLETE" and stop.
|
||||
|
||||
**CRITICAL:** After announcing completion, STOP IMMEDIATELY. Do NOT output summaries, recaps, or explanations of your work — the deliverable contains everything needed.
|
||||
</conclusion_trigger>
|
||||
@@ -14,6 +14,7 @@ export interface AuditLogger {
|
||||
logToolStart(toolName: string, parameters: unknown): Promise<void>;
|
||||
logToolEnd(result: unknown): Promise<void>;
|
||||
logError(error: Error, duration: number, turns: number): Promise<void>;
|
||||
logNote(category: string, message: string): Promise<void>;
|
||||
}
|
||||
|
||||
class RealAuditLogger implements AuditLogger {
|
||||
@@ -56,6 +57,10 @@ class RealAuditLogger implements AuditLogger {
|
||||
timestamp: formatTimestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
async logNote(category: string, message: string): Promise<void> {
|
||||
await this.auditSession.logWorkflowNote(category, message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Null Object implementation - all methods are safe no-ops */
|
||||
@@ -67,6 +72,8 @@ class NullAuditLogger implements AuditLogger {
|
||||
async logToolEnd(_result: unknown): Promise<void> {}
|
||||
|
||||
async logError(_error: Error, _duration: number, _turns: number): Promise<void> {}
|
||||
|
||||
async logNote(_category: string, _message: string): Promise<void> {}
|
||||
}
|
||||
|
||||
// Returns no-op when auditSession is null
|
||||
@@ -0,0 +1,404 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
// Production Claude agent execution with retry, git checkpoints, and audit logging
|
||||
|
||||
import { type JsonSchemaOutputFormat, query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { fs, path } from 'zx';
|
||||
import type { AuditSession } from '../audit/index.js';
|
||||
import { deliverablesDir } from '../paths.js';
|
||||
import { isRetryableError, PentestError } from '../services/error-handling.js';
|
||||
import { AGENT_VALIDATORS } from '../session-manager.js';
|
||||
import type { ActivityLogger } from '../types/activity-logger.js';
|
||||
import { isSpendingCapBehavior } from '../utils/billing-detection.js';
|
||||
import { formatTimestamp } from '../utils/formatting.js';
|
||||
import { Timer } from '../utils/metrics.js';
|
||||
import { createAuditLogger } from './audit-logger.js';
|
||||
import { dispatchMessage } from './message-handlers.js';
|
||||
import { type ModelTier, resolveModel, supportsAdaptiveThinking } from './models.js';
|
||||
import { detectExecutionContext, formatCompletionMessage, formatErrorOutput } from './output-formatters.js';
|
||||
import { createProgressManager } from './progress-manager.js';
|
||||
|
||||
declare global {
|
||||
var SHANNON_DISABLE_LOADER: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface ClaudePromptResult {
|
||||
result?: string | null | undefined;
|
||||
success: boolean;
|
||||
duration: number;
|
||||
turns?: number | undefined;
|
||||
cost: number;
|
||||
model?: string | undefined;
|
||||
partialCost?: number | undefined;
|
||||
apiErrorDetected?: boolean | undefined;
|
||||
error?: string | undefined;
|
||||
errorType?: string | undefined;
|
||||
prompt?: string | undefined;
|
||||
retryable?: boolean | undefined;
|
||||
structuredOutput?: unknown;
|
||||
}
|
||||
|
||||
function outputLines(lines: string[]): void {
|
||||
for (const line of lines) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeErrorLog(
|
||||
err: Error & { code?: string; status?: number },
|
||||
sourceDir: string,
|
||||
fullPrompt: string,
|
||||
duration: number,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const errorLog = {
|
||||
timestamp: formatTimestamp(),
|
||||
agent: 'claude-executor',
|
||||
error: {
|
||||
name: err.constructor.name,
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
status: err.status,
|
||||
stack: err.stack,
|
||||
},
|
||||
context: {
|
||||
sourceDir,
|
||||
prompt: `${fullPrompt.slice(0, 200)}...`,
|
||||
retryable: isRetryableError(err),
|
||||
},
|
||||
duration,
|
||||
};
|
||||
const logPath = path.join(deliverablesDir(sourceDir), 'error.log');
|
||||
await fs.appendFile(logPath, `${JSON.stringify(errorLog)}\n`);
|
||||
} catch {
|
||||
// Best-effort error log writing - don't propagate failures
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateAgentOutput(
|
||||
result: ClaudePromptResult,
|
||||
agentName: string | null,
|
||||
sourceDir: string,
|
||||
logger: ActivityLogger,
|
||||
): Promise<boolean> {
|
||||
logger.info(`Validating ${agentName} agent output`);
|
||||
|
||||
try {
|
||||
// Check if agent completed successfully (text result OR structured output)
|
||||
if (!result.success || (!result.result && result.structuredOutput === undefined)) {
|
||||
logger.error('Validation failed: Agent execution was unsuccessful');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get validator function for this agent
|
||||
const validator = agentName ? AGENT_VALIDATORS[agentName as keyof typeof AGENT_VALIDATORS] : undefined;
|
||||
|
||||
if (!validator) {
|
||||
logger.warn(`No validator found for agent "${agentName}" - assuming success`);
|
||||
logger.info('Validation passed: Unknown agent with successful result');
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.info(`Using validator for agent: ${agentName}`, { sourceDir });
|
||||
|
||||
// Apply validation function
|
||||
const validationResult = await validator(sourceDir, logger);
|
||||
|
||||
if (validationResult) {
|
||||
logger.info('Validation passed: Required files/structure present');
|
||||
} else {
|
||||
logger.error('Validation failed: Missing required deliverable files');
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
logger.error(`Validation failed with error: ${errMsg}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Low-level SDK execution. Handles message streaming, progress, and audit logging.
|
||||
// Exported for Temporal activities to call single-attempt execution.
|
||||
export async function runClaudePrompt(
|
||||
prompt: string,
|
||||
sourceDir: string,
|
||||
context: string = '',
|
||||
description: string = 'Claude analysis',
|
||||
_agentName: string | null = null,
|
||||
auditSession: AuditSession | null = null,
|
||||
logger: ActivityLogger,
|
||||
modelTier: ModelTier = 'medium',
|
||||
outputFormat?: JsonSchemaOutputFormat,
|
||||
apiKey?: string,
|
||||
deliverablesSubdir?: string,
|
||||
providerConfig?: import('../types/config.js').ProviderConfig,
|
||||
mcpServers?: Record<string, import('@anthropic-ai/claude-agent-sdk').McpServerConfig>,
|
||||
): Promise<ClaudePromptResult> {
|
||||
// 1. Initialize timing and prompt
|
||||
const timer = new Timer(`agent-${description.toLowerCase().replace(/\s+/g, '-')}`);
|
||||
const fullPrompt = context ? `${context}\n\n${prompt}` : prompt;
|
||||
|
||||
// 2. Set up progress and audit infrastructure
|
||||
const execContext = detectExecutionContext(description);
|
||||
const progress = createProgressManager(
|
||||
{ description, useCleanOutput: execContext.useCleanOutput },
|
||||
global.SHANNON_DISABLE_LOADER ?? false,
|
||||
);
|
||||
const auditLogger = createAuditLogger(auditSession);
|
||||
|
||||
logger.info(`Running Claude Code: ${description}...`);
|
||||
|
||||
// 3. Build env vars to pass to SDK subprocesses
|
||||
const sdkEnv: Record<string, string> = {
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || '64000',
|
||||
PLAYWRIGHT_MCP_OUTPUT_DIR: deliverablesSubdir
|
||||
? path.join(sourceDir, path.dirname(deliverablesSubdir), '.playwright-cli')
|
||||
: path.join(sourceDir, '.shannon', '.playwright-cli'),
|
||||
// apiKey from ContainerConfig takes precedence over process.env
|
||||
...(apiKey && { ANTHROPIC_API_KEY: apiKey }),
|
||||
// Deliverables subdir for save-deliverable CLI tool
|
||||
...(deliverablesSubdir && { SHANNON_DELIVERABLES_SUBDIR: deliverablesSubdir }),
|
||||
};
|
||||
|
||||
// 3a. Apply structured provider config directly to sdkEnv (no process.env mutation)
|
||||
if (providerConfig) {
|
||||
switch (providerConfig.providerType) {
|
||||
case 'bedrock':
|
||||
sdkEnv.CLAUDE_CODE_USE_BEDROCK = '1';
|
||||
if (providerConfig.awsRegion) sdkEnv.AWS_REGION = providerConfig.awsRegion;
|
||||
if (providerConfig.awsAccessKeyId) sdkEnv.AWS_ACCESS_KEY_ID = providerConfig.awsAccessKeyId;
|
||||
if (providerConfig.awsSecretAccessKey) sdkEnv.AWS_SECRET_ACCESS_KEY = providerConfig.awsSecretAccessKey;
|
||||
break;
|
||||
case 'vertex':
|
||||
sdkEnv.CLAUDE_CODE_USE_VERTEX = '1';
|
||||
if (providerConfig.gcpRegion) sdkEnv.CLOUD_ML_REGION = providerConfig.gcpRegion;
|
||||
if (providerConfig.gcpProjectId) sdkEnv.ANTHROPIC_VERTEX_PROJECT_ID = providerConfig.gcpProjectId;
|
||||
if (providerConfig.gcpCredentialsPath)
|
||||
sdkEnv.GOOGLE_APPLICATION_CREDENTIALS = providerConfig.gcpCredentialsPath;
|
||||
break;
|
||||
case 'litellm_router':
|
||||
if (providerConfig.baseUrl) sdkEnv.ANTHROPIC_BASE_URL = providerConfig.baseUrl;
|
||||
if (providerConfig.authToken) sdkEnv.ANTHROPIC_AUTH_TOKEN = providerConfig.authToken;
|
||||
break;
|
||||
default:
|
||||
// 'anthropic_api' or unset — apiKey already handled above
|
||||
if (providerConfig.apiKey && !apiKey) sdkEnv.ANTHROPIC_API_KEY = providerConfig.apiKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3b. Passthrough env vars not already set by providerConfig or apiKey
|
||||
const passthroughVars = [
|
||||
...(!sdkEnv.ANTHROPIC_API_KEY ? ['ANTHROPIC_API_KEY'] : []),
|
||||
'CLAUDE_CODE_OAUTH_TOKEN',
|
||||
...(!sdkEnv.ANTHROPIC_BASE_URL ? ['ANTHROPIC_BASE_URL'] : []),
|
||||
...(!sdkEnv.ANTHROPIC_AUTH_TOKEN ? ['ANTHROPIC_AUTH_TOKEN'] : []),
|
||||
...(!sdkEnv.CLAUDE_CODE_USE_BEDROCK ? ['CLAUDE_CODE_USE_BEDROCK'] : []),
|
||||
...(!sdkEnv.AWS_REGION ? ['AWS_REGION'] : []),
|
||||
'AWS_BEARER_TOKEN_BEDROCK',
|
||||
...(!sdkEnv.CLAUDE_CODE_USE_VERTEX ? ['CLAUDE_CODE_USE_VERTEX'] : []),
|
||||
...(!sdkEnv.CLOUD_ML_REGION ? ['CLOUD_ML_REGION'] : []),
|
||||
...(!sdkEnv.ANTHROPIC_VERTEX_PROJECT_ID ? ['ANTHROPIC_VERTEX_PROJECT_ID'] : []),
|
||||
...(!sdkEnv.GOOGLE_APPLICATION_CREDENTIALS ? ['GOOGLE_APPLICATION_CREDENTIALS'] : []),
|
||||
'HOME',
|
||||
'PATH',
|
||||
'PLAYWRIGHT_MCP_EXECUTABLE_PATH',
|
||||
];
|
||||
for (const name of passthroughVars) {
|
||||
const val = process.env[name];
|
||||
if (val) {
|
||||
sdkEnv[name] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Configure SDK options
|
||||
// Model override from providerConfig takes precedence over env-based resolveModel
|
||||
const model = providerConfig?.modelOverrides?.[modelTier] ?? resolveModel(modelTier);
|
||||
const adaptiveThinking = supportsAdaptiveThinking(model) && process.env.CLAUDE_ADAPTIVE_THINKING !== 'false';
|
||||
const options = {
|
||||
model,
|
||||
maxTurns: 10_000,
|
||||
cwd: sourceDir,
|
||||
permissionMode: 'bypassPermissions' as const,
|
||||
allowDangerouslySkipPermissions: true,
|
||||
settingSources: ['user'] as ('user' | 'project' | 'local')[],
|
||||
env: sdkEnv,
|
||||
...(adaptiveThinking && { thinking: { type: 'adaptive' as const } }),
|
||||
...(outputFormat && { outputFormat }),
|
||||
...(mcpServers && Object.keys(mcpServers).length > 0 && { mcpServers }),
|
||||
};
|
||||
|
||||
if (!execContext.useCleanOutput) {
|
||||
logger.info(`SDK Options: maxTurns=${options.maxTurns}, cwd=${sourceDir}, permissions=BYPASS`);
|
||||
}
|
||||
|
||||
let turnCount = 0;
|
||||
let result: string | null = null;
|
||||
let apiErrorDetected = false;
|
||||
let totalCost = 0;
|
||||
|
||||
progress.start();
|
||||
|
||||
try {
|
||||
// 6. Process the message stream
|
||||
const messageLoopResult = await processMessageStream(
|
||||
fullPrompt,
|
||||
options,
|
||||
{ execContext, description, progress, auditLogger, logger },
|
||||
timer,
|
||||
);
|
||||
|
||||
turnCount = messageLoopResult.turnCount;
|
||||
result = messageLoopResult.result;
|
||||
apiErrorDetected = messageLoopResult.apiErrorDetected;
|
||||
totalCost = messageLoopResult.cost;
|
||||
const model = messageLoopResult.model;
|
||||
|
||||
// === SPENDING CAP SAFEGUARD ===
|
||||
// 7. Defense-in-depth: Detect spending cap that slipped through detectApiError().
|
||||
// Uses consolidated billing detection from utils/billing-detection.ts
|
||||
if (isSpendingCapBehavior(turnCount, totalCost, result || '')) {
|
||||
throw new PentestError(
|
||||
`Spending cap likely reached (turns=${turnCount}, cost=$0): ${result?.slice(0, 100)}`,
|
||||
'billing',
|
||||
true, // Retryable - Temporal will use 5-30 min backoff
|
||||
);
|
||||
}
|
||||
|
||||
// 8. Finalize successful result
|
||||
const duration = timer.stop();
|
||||
|
||||
if (apiErrorDetected) {
|
||||
logger.warn(`API Error detected in ${description} - will validate deliverables before failing`);
|
||||
}
|
||||
|
||||
progress.finish(formatCompletionMessage(execContext, description, turnCount, duration));
|
||||
|
||||
return {
|
||||
result,
|
||||
success: true,
|
||||
duration,
|
||||
turns: turnCount,
|
||||
cost: totalCost,
|
||||
model,
|
||||
partialCost: totalCost,
|
||||
apiErrorDetected,
|
||||
...(messageLoopResult.structuredOutput !== undefined && {
|
||||
structuredOutput: messageLoopResult.structuredOutput,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
// 9. Handle errors — log, write error file, return failure
|
||||
const duration = timer.stop();
|
||||
|
||||
const err = error as Error & { code?: string; status?: number };
|
||||
|
||||
await auditLogger.logError(err, duration, turnCount);
|
||||
progress.stop();
|
||||
outputLines(formatErrorOutput(err, execContext, description, duration, sourceDir, isRetryableError(err)));
|
||||
await writeErrorLog(err, sourceDir, fullPrompt, duration);
|
||||
|
||||
return {
|
||||
error: err.message,
|
||||
errorType: err.constructor.name,
|
||||
prompt: `${fullPrompt.slice(0, 100)}...`,
|
||||
success: false,
|
||||
duration,
|
||||
cost: totalCost,
|
||||
retryable: isRetryableError(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface MessageLoopResult {
|
||||
turnCount: number;
|
||||
result: string | null;
|
||||
apiErrorDetected: boolean;
|
||||
cost: number;
|
||||
model?: string | undefined;
|
||||
structuredOutput?: unknown;
|
||||
}
|
||||
|
||||
interface MessageLoopDeps {
|
||||
execContext: ReturnType<typeof detectExecutionContext>;
|
||||
description: string;
|
||||
progress: ReturnType<typeof createProgressManager>;
|
||||
auditLogger: ReturnType<typeof createAuditLogger>;
|
||||
logger: ActivityLogger;
|
||||
}
|
||||
|
||||
async function processMessageStream(
|
||||
fullPrompt: string,
|
||||
options: NonNullable<Parameters<typeof query>[0]['options']>,
|
||||
deps: MessageLoopDeps,
|
||||
timer: Timer,
|
||||
): Promise<MessageLoopResult> {
|
||||
const { execContext, description, progress, auditLogger, logger } = deps;
|
||||
const HEARTBEAT_INTERVAL = 30000;
|
||||
|
||||
let turnCount = 0;
|
||||
let result: string | null = null;
|
||||
let apiErrorDetected = false;
|
||||
let cost = 0;
|
||||
let model: string | undefined;
|
||||
let structuredOutput: unknown | undefined;
|
||||
let lastHeartbeat = Date.now();
|
||||
|
||||
for await (const message of query({ prompt: fullPrompt, options })) {
|
||||
// Heartbeat logging when loader is disabled
|
||||
const now = Date.now();
|
||||
if (global.SHANNON_DISABLE_LOADER && now - lastHeartbeat > HEARTBEAT_INTERVAL) {
|
||||
logger.info(`[${Math.floor((now - timer.startTime) / 1000)}s] ${description} running... (Turn ${turnCount})`);
|
||||
lastHeartbeat = now;
|
||||
}
|
||||
|
||||
// Increment turn count for assistant messages
|
||||
if (message.type === 'assistant') {
|
||||
turnCount++;
|
||||
}
|
||||
|
||||
const dispatchResult = await dispatchMessage(message as { type: string; subtype?: string }, turnCount, {
|
||||
execContext,
|
||||
description,
|
||||
progress,
|
||||
auditLogger,
|
||||
logger,
|
||||
});
|
||||
|
||||
if (dispatchResult.type === 'throw') {
|
||||
throw dispatchResult.error;
|
||||
}
|
||||
|
||||
if (dispatchResult.type === 'complete') {
|
||||
result = dispatchResult.result;
|
||||
cost = dispatchResult.cost;
|
||||
if (dispatchResult.structuredOutput !== undefined) {
|
||||
structuredOutput = dispatchResult.structuredOutput;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (dispatchResult.type === 'continue') {
|
||||
if (dispatchResult.apiErrorDetected) {
|
||||
apiErrorDetected = true;
|
||||
}
|
||||
if (dispatchResult.model) {
|
||||
model = dispatchResult.model;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
turnCount,
|
||||
result,
|
||||
apiErrorDetected,
|
||||
cost,
|
||||
model,
|
||||
...(structuredOutput !== undefined && { structuredOutput }),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import type { SDKAssistantMessageError } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { PentestError } from '../services/error-handling.js';
|
||||
import type { ActivityLogger } from '../types/activity-logger.js';
|
||||
import { ErrorCode } from '../types/errors.js';
|
||||
import { matchesBillingTextPattern } from '../utils/billing-detection.js';
|
||||
import { formatTimestamp } from '../utils/formatting.js';
|
||||
import type { AuditLogger } from './audit-logger.js';
|
||||
import {
|
||||
filterJsonToolCalls,
|
||||
formatAssistantOutput,
|
||||
formatResultOutput,
|
||||
formatToolResultOutput,
|
||||
formatToolUseOutput,
|
||||
} from './output-formatters.js';
|
||||
import type { ProgressManager } from './progress-manager.js';
|
||||
import type {
|
||||
ApiErrorDetection,
|
||||
AssistantMessage,
|
||||
AssistantResult,
|
||||
ContentBlock,
|
||||
ExecutionContext,
|
||||
ModelRefusalFallbackMessage,
|
||||
ResultData,
|
||||
ResultMessage,
|
||||
SystemInitMessage,
|
||||
ToolResultData,
|
||||
ToolResultMessage,
|
||||
ToolUseData,
|
||||
ToolUseMessage,
|
||||
} from './types.js';
|
||||
|
||||
// Handles both array and string content formats from SDK
|
||||
function extractMessageContent(message: AssistantMessage): string {
|
||||
const messageContent = message.message;
|
||||
|
||||
if (Array.isArray(messageContent.content)) {
|
||||
return messageContent.content
|
||||
.filter((c: ContentBlock) => c.type !== 'thinking' && c.type !== 'redacted_thinking')
|
||||
.map((c: ContentBlock) => c.text || JSON.stringify(c))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return String(messageContent.content);
|
||||
}
|
||||
|
||||
// Extracts only text content (no tool_use JSON) to avoid false positives in error detection
|
||||
function extractTextOnlyContent(message: AssistantMessage): string {
|
||||
const messageContent = message.message;
|
||||
|
||||
if (Array.isArray(messageContent.content)) {
|
||||
return messageContent.content
|
||||
.filter((c: ContentBlock) => c.type === 'text' || c.text)
|
||||
.map((c: ContentBlock) => c.text || '')
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return String(messageContent.content);
|
||||
}
|
||||
|
||||
function detectApiError(content: string): ApiErrorDetection {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return { detected: false };
|
||||
}
|
||||
|
||||
const lowerContent = content.toLowerCase();
|
||||
|
||||
// === BILLING/SPENDING CAP ERRORS (Retryable with long backoff) ===
|
||||
// When Claude Code hits its spending cap, it returns a short message like
|
||||
// "Spending cap reached resets 8am" instead of throwing an error.
|
||||
// These should retry with 5-30 min backoff so workflows can recover when cap resets.
|
||||
if (matchesBillingTextPattern(content)) {
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Billing limit reached: ${content.slice(0, 100)}`,
|
||||
'billing',
|
||||
true, // RETRYABLE - Temporal will use 5-30 min backoff
|
||||
{},
|
||||
ErrorCode.SPENDING_CAP_REACHED,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// === SESSION LIMIT (Non-retryable) ===
|
||||
// Different from spending cap - usually means something is fundamentally wrong
|
||||
if (lowerContent.includes('session limit reached')) {
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError('Session limit reached', 'billing', false),
|
||||
};
|
||||
}
|
||||
|
||||
// Non-fatal API errors - detected but continue
|
||||
if (lowerContent.includes('api error') || lowerContent.includes('terminated')) {
|
||||
return { detected: true };
|
||||
}
|
||||
|
||||
return { detected: false };
|
||||
}
|
||||
|
||||
// Maps SDK structured error types to our error handling.
|
||||
function handleStructuredError(errorType: SDKAssistantMessageError, content: string): ApiErrorDetection {
|
||||
switch (errorType) {
|
||||
case 'billing_error':
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Billing error (structured): ${content.slice(0, 100)}`,
|
||||
'billing',
|
||||
true, // Retryable with backoff
|
||||
{},
|
||||
ErrorCode.INSUFFICIENT_CREDITS,
|
||||
),
|
||||
};
|
||||
case 'rate_limit':
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Rate limit hit (structured): ${content.slice(0, 100)}`,
|
||||
'network',
|
||||
true, // Retryable with backoff
|
||||
{},
|
||||
ErrorCode.API_RATE_LIMITED,
|
||||
),
|
||||
};
|
||||
case 'authentication_failed':
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Authentication failed: ${content.slice(0, 100)}`,
|
||||
'config',
|
||||
false, // Not retryable - needs API key fix
|
||||
),
|
||||
};
|
||||
case 'server_error':
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Server error (structured): ${content.slice(0, 100)}`,
|
||||
'network',
|
||||
true, // Retryable
|
||||
),
|
||||
};
|
||||
case 'invalid_request':
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Invalid request: ${content.slice(0, 100)}`,
|
||||
'config',
|
||||
false, // Not retryable - needs code fix
|
||||
),
|
||||
};
|
||||
case 'max_output_tokens':
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Max output tokens reached: ${content.slice(0, 100)}`,
|
||||
'billing',
|
||||
true, // Retryable - may succeed with different content
|
||||
),
|
||||
};
|
||||
case 'overloaded':
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Anthropic API overloaded (structured): ${content.slice(0, 100)}`,
|
||||
'network',
|
||||
true, // Retryable with backoff
|
||||
),
|
||||
};
|
||||
case 'model_not_found':
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Model not found: ${content.slice(0, 100)}`,
|
||||
'config',
|
||||
false, // Not retryable - model ID is misconfigured
|
||||
),
|
||||
};
|
||||
case 'oauth_org_not_allowed':
|
||||
return {
|
||||
detected: true,
|
||||
shouldThrow: new PentestError(
|
||||
`Organization not allowed for this credential: ${content.slice(0, 100)}`,
|
||||
'config',
|
||||
false, // Not retryable - needs credential/org fix
|
||||
),
|
||||
};
|
||||
default:
|
||||
return { detected: true };
|
||||
}
|
||||
}
|
||||
|
||||
function handleAssistantMessage(message: AssistantMessage, turnCount: number): AssistantResult {
|
||||
const content = extractMessageContent(message);
|
||||
const cleanedContent = filterJsonToolCalls(content);
|
||||
|
||||
// Prefer structured error field from SDK, fall back to text-sniffing
|
||||
// Use text-only content for error detection to avoid false positives
|
||||
// from tool_use JSON (e.g. security reports containing "usage limit")
|
||||
let errorDetection: ApiErrorDetection;
|
||||
if (message.error) {
|
||||
errorDetection = handleStructuredError(message.error, content);
|
||||
} else {
|
||||
const textOnlyContent = extractTextOnlyContent(message);
|
||||
errorDetection = detectApiError(textOnlyContent);
|
||||
}
|
||||
|
||||
const result: AssistantResult = {
|
||||
content,
|
||||
cleanedContent,
|
||||
apiErrorDetected: errorDetection.detected,
|
||||
logData: {
|
||||
turn: turnCount,
|
||||
content,
|
||||
timestamp: formatTimestamp(),
|
||||
},
|
||||
};
|
||||
|
||||
// Only add shouldThrow if it exists (exactOptionalPropertyTypes compliance)
|
||||
if (errorDetection.shouldThrow) {
|
||||
result.shouldThrow = errorDetection.shouldThrow;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Final message of a query with cost/duration info
|
||||
function handleResultMessage(message: ResultMessage): ResultData {
|
||||
const result: ResultData = {
|
||||
result: message.result || null,
|
||||
cost: message.total_cost_usd || 0,
|
||||
duration_ms: message.duration_ms || 0,
|
||||
permissionDenials: message.permission_denials?.length || 0,
|
||||
};
|
||||
|
||||
// Only add subtype if it exists (exactOptionalPropertyTypes compliance)
|
||||
if (message.subtype) {
|
||||
result.subtype = message.subtype;
|
||||
}
|
||||
|
||||
// Capture stop_reason for diagnostics (helps debug early stops, budget exceeded, etc.)
|
||||
if (message.stop_reason !== undefined) {
|
||||
result.stop_reason = message.stop_reason;
|
||||
if (message.stop_reason && message.stop_reason !== 'end_turn') {
|
||||
console.log(` Stop reason: ${message.stop_reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.structured_output !== undefined) {
|
||||
result.structuredOutput = message.structured_output;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function handleToolUseMessage(message: ToolUseMessage): ToolUseData {
|
||||
return {
|
||||
toolName: message.name,
|
||||
parameters: message.input || {},
|
||||
timestamp: formatTimestamp(),
|
||||
};
|
||||
}
|
||||
|
||||
// Truncates long results for display (500 char limit), preserves full content for logging
|
||||
function handleToolResultMessage(message: ToolResultMessage): ToolResultData {
|
||||
const content = message.content;
|
||||
const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
||||
|
||||
const displayContent =
|
||||
contentStr.length > 500
|
||||
? `${contentStr.slice(0, 500)}...\n[Result truncated - ${contentStr.length} total chars]`
|
||||
: contentStr;
|
||||
|
||||
return {
|
||||
content,
|
||||
displayContent,
|
||||
timestamp: formatTimestamp(),
|
||||
};
|
||||
}
|
||||
|
||||
function outputLines(lines: string[]): void {
|
||||
for (const line of lines) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
export type MessageDispatchAction =
|
||||
| { type: 'continue'; apiErrorDetected?: boolean | undefined; model?: string | undefined }
|
||||
| { type: 'complete'; result: string | null; cost: number; structuredOutput?: unknown }
|
||||
| { type: 'throw'; error: Error };
|
||||
|
||||
export interface MessageDispatchDeps {
|
||||
execContext: ExecutionContext;
|
||||
description: string;
|
||||
progress: ProgressManager;
|
||||
auditLogger: AuditLogger;
|
||||
logger: ActivityLogger;
|
||||
}
|
||||
|
||||
// Dispatches SDK messages to appropriate handlers and formatters
|
||||
export async function dispatchMessage(
|
||||
message: { type: string; subtype?: string },
|
||||
turnCount: number,
|
||||
deps: MessageDispatchDeps,
|
||||
): Promise<MessageDispatchAction> {
|
||||
const { execContext, description, progress, auditLogger, logger } = deps;
|
||||
|
||||
switch (message.type) {
|
||||
case 'assistant': {
|
||||
const assistantResult = handleAssistantMessage(message as AssistantMessage, turnCount);
|
||||
|
||||
if (assistantResult.shouldThrow) {
|
||||
return { type: 'throw', error: assistantResult.shouldThrow };
|
||||
}
|
||||
|
||||
if (assistantResult.cleanedContent.trim()) {
|
||||
progress.stop();
|
||||
outputLines(formatAssistantOutput(assistantResult.cleanedContent, execContext, turnCount, description));
|
||||
progress.start();
|
||||
}
|
||||
|
||||
await auditLogger.logLlmResponse(turnCount, assistantResult.content);
|
||||
|
||||
if (assistantResult.apiErrorDetected) {
|
||||
logger.warn('API Error detected in assistant response');
|
||||
return { type: 'continue', apiErrorDetected: true };
|
||||
}
|
||||
|
||||
return { type: 'continue' };
|
||||
}
|
||||
|
||||
case 'system': {
|
||||
if (message.subtype === 'init') {
|
||||
const initMsg = message as SystemInitMessage;
|
||||
if (!execContext.useCleanOutput) {
|
||||
logger.info(`Model: ${initMsg.model}, Permission: ${initMsg.permissionMode}`);
|
||||
}
|
||||
return { type: 'continue', model: initMsg.model };
|
||||
}
|
||||
if (message.subtype === 'model_refusal_fallback') {
|
||||
const fallback = message as ModelRefusalFallbackMessage;
|
||||
const category = fallback.api_refusal_category ?? 'policy';
|
||||
await auditLogger.logNote(
|
||||
'model-fallback',
|
||||
`Model refused (${category}); fell back ${fallback.original_model} → ${fallback.fallback_model}`,
|
||||
);
|
||||
return { type: 'continue' };
|
||||
}
|
||||
return { type: 'continue' };
|
||||
}
|
||||
|
||||
case 'user':
|
||||
case 'tool_progress':
|
||||
case 'tool_use_summary':
|
||||
case 'auth_status':
|
||||
return { type: 'continue' };
|
||||
|
||||
case 'tool_use': {
|
||||
const toolData = handleToolUseMessage(message as unknown as ToolUseMessage);
|
||||
outputLines(formatToolUseOutput(toolData.toolName, toolData.parameters));
|
||||
await auditLogger.logToolStart(toolData.toolName, toolData.parameters);
|
||||
return { type: 'continue' };
|
||||
}
|
||||
|
||||
case 'tool_result': {
|
||||
const toolResultData = handleToolResultMessage(message as unknown as ToolResultMessage);
|
||||
outputLines(formatToolResultOutput(toolResultData.displayContent));
|
||||
await auditLogger.logToolEnd(toolResultData.content);
|
||||
return { type: 'continue' };
|
||||
}
|
||||
|
||||
case 'result': {
|
||||
const resultData = handleResultMessage(message as ResultMessage);
|
||||
outputLines(formatResultOutput(resultData, !execContext.useCleanOutput));
|
||||
|
||||
if (resultData.subtype === 'error_max_structured_output_retries') {
|
||||
return {
|
||||
type: 'throw',
|
||||
error: new PentestError(
|
||||
'Structured output validation failed after max retries',
|
||||
'validation',
|
||||
true,
|
||||
{},
|
||||
ErrorCode.OUTPUT_VALIDATION_FAILED,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'complete' as const,
|
||||
result: resultData.result,
|
||||
cost: resultData.cost,
|
||||
...(resultData.structuredOutput !== undefined && { structuredOutput: resultData.structuredOutput }),
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
logger.info(`Unhandled message type: ${message.type}`);
|
||||
return { type: 'continue' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* Model tier definitions and resolution.
|
||||
*
|
||||
* Three tiers mapped to capability levels:
|
||||
* - "small" (Haiku — summarization, structured extraction)
|
||||
* - "medium" (Sonnet — tool use, general analysis)
|
||||
* - "large" (Opus — deep reasoning, complex analysis)
|
||||
*
|
||||
* Users override via ANTHROPIC_SMALL_MODEL / ANTHROPIC_MEDIUM_MODEL / ANTHROPIC_LARGE_MODEL,
|
||||
* which works across all providers (direct, Bedrock, Vertex).
|
||||
*/
|
||||
|
||||
export type ModelTier = 'small' | 'medium' | 'large';
|
||||
|
||||
const DEFAULT_MODELS: Readonly<Record<ModelTier, string>> = {
|
||||
small: 'claude-haiku-4-5-20251001',
|
||||
medium: 'claude-sonnet-4-6',
|
||||
large: 'claude-opus-4-8',
|
||||
};
|
||||
|
||||
/** Resolve a model tier to a concrete model ID. */
|
||||
export function resolveModel(tier: ModelTier = 'medium'): string {
|
||||
switch (tier) {
|
||||
case 'small':
|
||||
return process.env.ANTHROPIC_SMALL_MODEL || DEFAULT_MODELS.small;
|
||||
case 'large':
|
||||
return process.env.ANTHROPIC_LARGE_MODEL || DEFAULT_MODELS.large;
|
||||
default:
|
||||
return process.env.ANTHROPIC_MEDIUM_MODEL || DEFAULT_MODELS.medium;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a model supports adaptive thinking. Opus 4.6, 4.7, and 4.8 only. */
|
||||
export function supportsAdaptiveThinking(model: string): boolean {
|
||||
return /opus-4-[678]/.test(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a model is in the Fable family. Fable's safety classifiers flag
|
||||
* cybersecurity tasks and route them to Opus 4.8, so a security scan on Fable
|
||||
* largely runs on Opus 4.8 anyway.
|
||||
*/
|
||||
export function isFableModel(model: string): boolean {
|
||||
return /fable/i.test(model);
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import { AGENTS } from '../session-manager.js';
|
||||
import { extractAgentType, formatDuration } from '../utils/formatting.js';
|
||||
import type { ExecutionContext, ResultData } from './types.js';
|
||||
|
||||
interface ToolCallInput {
|
||||
url?: string;
|
||||
element?: string;
|
||||
key?: string;
|
||||
fields?: unknown[];
|
||||
text?: string;
|
||||
action?: string;
|
||||
description?: string;
|
||||
command?: string;
|
||||
todos?: Array<{
|
||||
status: string;
|
||||
content: string;
|
||||
}>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ToolCall {
|
||||
name: string;
|
||||
input?: ToolCallInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent prefix for parallel execution
|
||||
*/
|
||||
export function getAgentPrefix(description: string): string {
|
||||
// Map agent names to their prefixes
|
||||
const agentPrefixes: Record<string, string> = {
|
||||
'injection-vuln': '[Injection]',
|
||||
'xss-vuln': '[XSS]',
|
||||
'auth-vuln': '[Auth]',
|
||||
'authz-vuln': '[Authz]',
|
||||
'ssrf-vuln': '[SSRF]',
|
||||
'injection-exploit': '[Injection]',
|
||||
'xss-exploit': '[XSS]',
|
||||
'auth-exploit': '[Auth]',
|
||||
'authz-exploit': '[Authz]',
|
||||
'ssrf-exploit': '[SSRF]',
|
||||
};
|
||||
|
||||
// First try to match by agent name directly
|
||||
for (const [agentName, prefix] of Object.entries(agentPrefixes)) {
|
||||
const agent = AGENTS[agentName as keyof typeof AGENTS];
|
||||
if (agent && description.includes(agent.displayName)) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to partial matches for backwards compatibility
|
||||
if (description.includes('injection')) return '[Injection]';
|
||||
if (description.includes('xss')) return '[XSS]';
|
||||
if (description.includes('authz')) return '[Authz]'; // Check authz before auth
|
||||
if (description.includes('auth')) return '[Auth]';
|
||||
if (description.includes('ssrf')) return '[SSRF]';
|
||||
|
||||
return '[Agent]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domain from URL for display
|
||||
*/
|
||||
function extractDomain(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.hostname || url.slice(0, 30);
|
||||
} catch {
|
||||
return url.slice(0, 30);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format playwright-cli commands into clean progress indicators
|
||||
*/
|
||||
function formatBrowserAction(command: string): string | null {
|
||||
// Extract subcommand after optional session flag (e.g., "playwright-cli -s=session1 navigate https://example.com")
|
||||
const match = command.match(/playwright-cli\s+(?:-s=\S+\s+)?(\S+)(?:\s+(.*))?/);
|
||||
if (!match) return null;
|
||||
|
||||
const subcommand = match[1];
|
||||
const args = match[2] || '';
|
||||
|
||||
switch (subcommand) {
|
||||
case 'open':
|
||||
case 'goto': {
|
||||
const domain = args.trim() ? extractDomain(args.trim()) : '';
|
||||
return domain ? `🌐 Navigating to ${domain}` : '🌐 Opening browser';
|
||||
}
|
||||
case 'go-back':
|
||||
return '⬅️ Going back';
|
||||
case 'go-forward':
|
||||
return '➡️ Going forward';
|
||||
case 'reload':
|
||||
return '🔄 Reloading page';
|
||||
case 'click':
|
||||
case 'dblclick':
|
||||
return `🖱️ Clicking ${(args || 'element').slice(0, 25)}`;
|
||||
case 'hover':
|
||||
return `👆 Hovering over ${(args || 'element').slice(0, 20)}`;
|
||||
case 'type':
|
||||
return `⌨️ Typing ${(args || 'text').slice(0, 20)}`;
|
||||
case 'press':
|
||||
case 'keydown':
|
||||
case 'keyup':
|
||||
return `⌨️ Pressing ${args || 'key'}`;
|
||||
case 'fill':
|
||||
return `📝 Filling ${(args || 'field').slice(0, 25)}`;
|
||||
case 'select':
|
||||
return '📋 Selecting dropdown option';
|
||||
case 'check':
|
||||
case 'uncheck':
|
||||
return `☑️ ${subcommand === 'check' ? 'Checking' : 'Unchecking'} ${(args || 'element').slice(0, 20)}`;
|
||||
case 'upload':
|
||||
return '📁 Uploading file';
|
||||
case 'drag':
|
||||
return '🖱️ Dragging element';
|
||||
case 'snapshot':
|
||||
return '📸 Taking page snapshot';
|
||||
case 'screenshot':
|
||||
return '📸 Taking screenshot';
|
||||
case 'eval':
|
||||
case 'run-code':
|
||||
return '🔍 Running JavaScript analysis';
|
||||
case 'console':
|
||||
return '📜 Checking console logs';
|
||||
case 'network':
|
||||
return '🌐 Analyzing network traffic';
|
||||
case 'tab-list':
|
||||
case 'tab-new':
|
||||
case 'tab-close':
|
||||
case 'tab-select':
|
||||
return `🗂️ ${subcommand.replace('tab-', '')} browser tab`;
|
||||
case 'dialog-accept':
|
||||
return '💬 Accepting dialog';
|
||||
case 'dialog-dismiss':
|
||||
return '💬 Dismissing dialog';
|
||||
case 'pdf':
|
||||
return '📄 Saving page as PDF';
|
||||
case 'resize':
|
||||
return `🖥️ Resizing browser ${args || ''}`.trim();
|
||||
default:
|
||||
return `🌐 Browser: ${subcommand}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize TodoWrite updates into clean progress indicators
|
||||
*/
|
||||
function summarizeTodoUpdate(input: ToolCallInput | undefined): string | null {
|
||||
if (!input?.todos || !Array.isArray(input.todos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const todos = input.todos;
|
||||
const completed = todos.filter((t) => t.status === 'completed');
|
||||
const inProgress = todos.filter((t) => t.status === 'in_progress');
|
||||
|
||||
// Show recently completed tasks
|
||||
const recent = completed.at(-1);
|
||||
if (recent) {
|
||||
return `✅ ${recent.content}`;
|
||||
}
|
||||
|
||||
// Show current in-progress task
|
||||
const current = inProgress.at(0);
|
||||
if (current) {
|
||||
return `🔄 ${current.content}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out JSON tool calls from content, with special handling for Task calls
|
||||
*/
|
||||
export function filterJsonToolCalls(content: string | null | undefined): string {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return content || '';
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
const processedLines: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Skip empty lines
|
||||
if (trimmed === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a JSON tool call
|
||||
if (trimmed.startsWith('{"type":"tool_use"')) {
|
||||
try {
|
||||
const toolCall = JSON.parse(trimmed) as ToolCall;
|
||||
|
||||
// Special handling for Task tool calls
|
||||
if (toolCall.name === 'Task') {
|
||||
const description = toolCall.input?.description || 'analysis agent';
|
||||
processedLines.push(`🚀 Launching ${description}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special handling for TodoWrite tool calls
|
||||
if (toolCall.name === 'TodoWrite') {
|
||||
const summary = summarizeTodoUpdate(toolCall.input);
|
||||
if (summary) {
|
||||
processedLines.push(summary);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special handling for browser tool calls (playwright-cli via Bash)
|
||||
if (toolCall.name === 'Bash') {
|
||||
const command = toolCall.input?.command || '';
|
||||
if (command.includes('playwright-cli')) {
|
||||
const browserAction = formatBrowserAction(command);
|
||||
if (browserAction) {
|
||||
processedLines.push(browserAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If JSON parsing fails, treat as regular text
|
||||
processedLines.push(line);
|
||||
}
|
||||
} else {
|
||||
// Keep non-JSON lines (assistant text)
|
||||
processedLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return processedLines.join('\n');
|
||||
}
|
||||
|
||||
export function detectExecutionContext(description: string): ExecutionContext {
|
||||
const isParallelExecution = description.includes('vuln agent') || description.includes('exploit agent');
|
||||
|
||||
const useCleanOutput =
|
||||
description.includes('Pre-recon agent') ||
|
||||
description.includes('Recon agent') ||
|
||||
description.includes('Executive Summary and Report Cleanup') ||
|
||||
description.includes('vuln agent') ||
|
||||
description.includes('exploit agent');
|
||||
|
||||
const agentType = extractAgentType(description);
|
||||
|
||||
const agentKey = description.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
return { isParallelExecution, useCleanOutput, agentType, agentKey };
|
||||
}
|
||||
|
||||
export function formatAssistantOutput(
|
||||
cleanedContent: string,
|
||||
context: ExecutionContext,
|
||||
turnCount: number,
|
||||
description: string,
|
||||
): string[] {
|
||||
if (!cleanedContent.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
if (context.isParallelExecution) {
|
||||
// Compact output for parallel agents with prefixes
|
||||
const prefix = getAgentPrefix(description);
|
||||
lines.push(`${prefix} ${cleanedContent}`);
|
||||
} else {
|
||||
// Full turn output for sequential agents
|
||||
lines.push(`\n Turn ${turnCount} (${description}):`);
|
||||
lines.push(` ${cleanedContent}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function formatResultOutput(data: ResultData, showFullResult: boolean): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`\n COMPLETED:`);
|
||||
lines.push(` Duration: ${(data.duration_ms / 1000).toFixed(1)}s, Cost: $${data.cost.toFixed(4)}`);
|
||||
|
||||
if (data.subtype === 'error_max_turns') {
|
||||
lines.push(` Stopped: Hit maximum turns limit`);
|
||||
} else if (data.subtype === 'error_during_execution') {
|
||||
lines.push(` Stopped: Execution error`);
|
||||
}
|
||||
|
||||
if (data.permissionDenials > 0) {
|
||||
lines.push(` ${data.permissionDenials} permission denials`);
|
||||
}
|
||||
|
||||
if (showFullResult && data.result && typeof data.result === 'string') {
|
||||
if (data.result.length > 1000) {
|
||||
lines.push(` ${data.result.slice(0, 1000)}... [${data.result.length} total chars]`);
|
||||
} else {
|
||||
lines.push(` ${data.result}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function formatErrorOutput(
|
||||
error: Error & { code?: string; status?: number },
|
||||
context: ExecutionContext,
|
||||
description: string,
|
||||
duration: number,
|
||||
sourceDir: string,
|
||||
isRetryable: boolean,
|
||||
): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
if (context.isParallelExecution) {
|
||||
const prefix = getAgentPrefix(description);
|
||||
lines.push(`${prefix} Failed (${formatDuration(duration)})`);
|
||||
} else if (context.useCleanOutput) {
|
||||
lines.push(`${context.agentType} failed (${formatDuration(duration)})`);
|
||||
} else {
|
||||
lines.push(` Claude Code failed: ${description} (${formatDuration(duration)})`);
|
||||
}
|
||||
|
||||
lines.push(` Error Type: ${error.constructor.name}`);
|
||||
lines.push(` Message: ${error.message}`);
|
||||
lines.push(` Agent: ${description}`);
|
||||
lines.push(` Working Directory: ${sourceDir}`);
|
||||
lines.push(` Retryable: ${isRetryable ? 'Yes' : 'No'}`);
|
||||
|
||||
if (error.code) {
|
||||
lines.push(` Error Code: ${error.code}`);
|
||||
}
|
||||
if (error.status) {
|
||||
lines.push(` HTTP Status: ${error.status}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function formatCompletionMessage(
|
||||
context: ExecutionContext,
|
||||
description: string,
|
||||
turnCount: number,
|
||||
duration: number,
|
||||
): string {
|
||||
if (context.isParallelExecution) {
|
||||
const prefix = getAgentPrefix(description);
|
||||
return `${prefix} Complete (${turnCount} turns, ${formatDuration(duration)})`;
|
||||
}
|
||||
|
||||
if (context.useCleanOutput) {
|
||||
return `${context.agentType.charAt(0).toUpperCase() + context.agentType.slice(1)} complete! (${turnCount} turns, ${formatDuration(duration)})`;
|
||||
}
|
||||
|
||||
return ` Claude Code completed: ${description} (${turnCount} turns) in ${formatDuration(duration)}`;
|
||||
}
|
||||
|
||||
export function formatToolUseOutput(toolName: string, input: Record<string, unknown> | undefined): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`\n Using Tool: ${toolName}`);
|
||||
if (input && Object.keys(input).length > 0) {
|
||||
lines.push(` Input: ${JSON.stringify(input, null, 2)}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function formatToolResultOutput(displayContent: string): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(` Tool Result:`);
|
||||
if (displayContent) {
|
||||
lines.push(` ${displayContent}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* Writes <sourceDir>/.playwright/cli.config.json with stealth defaults so
|
||||
* `playwright-cli open` auto-loads them from the agent's cwd. Skipped when a
|
||||
* config already exists so user-provided files are never clobbered.
|
||||
*
|
||||
* NOTE: Playwright's MCP browser config treats `initScript` entries as file
|
||||
* paths, not inline source. The stealth script is written alongside the config
|
||||
* and referenced by absolute path. Inline strings silently fail the daemon.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
async function pathExists(p: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const STEALTH_INIT_SCRIPT = `delete Object.getPrototypeOf(navigator).webdriver;
|
||||
|
||||
Object.defineProperty(navigator, 'plugins', {
|
||||
get: () => {
|
||||
const arr = [
|
||||
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
||||
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
||||
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
|
||||
];
|
||||
arr.__proto__ = PluginArray.prototype;
|
||||
return arr;
|
||||
},
|
||||
});
|
||||
|
||||
window.chrome = window.chrome || {};
|
||||
window.chrome.runtime = window.chrome.runtime || {
|
||||
PlatformOs: { MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd' },
|
||||
PlatformArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
|
||||
PlatformNaclArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
|
||||
RequestUpdateCheckStatus: { THROTTLED: 'throttled', NO_UPDATE: 'no_update', UPDATE_AVAILABLE: 'update_available' },
|
||||
OnInstalledReason: { INSTALL: 'install', UPDATE: 'update', CHROME_UPDATE: 'chrome_update', SHARED_MODULE_UPDATE: 'shared_module_update' },
|
||||
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
|
||||
};
|
||||
`;
|
||||
|
||||
function buildStealthConfig(initScriptPath: string) {
|
||||
return {
|
||||
browser: {
|
||||
browserName: 'chromium',
|
||||
launchOptions: {
|
||||
headless: true,
|
||||
args: ['--disable-blink-features=AutomationControlled'],
|
||||
ignoreDefaultArgs: ['--enable-automation'],
|
||||
},
|
||||
contextOptions: {
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
locale: 'en-US',
|
||||
extraHTTPHeaders: { 'Accept-Language': 'en-US,en;q=0.9' },
|
||||
userAgent:
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
},
|
||||
initScript: [initScriptPath],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type StealthConfigWriteResult = 'wrote' | 'skipped-existing';
|
||||
|
||||
export async function writePlaywrightStealthConfig(
|
||||
sourceDir: string,
|
||||
): Promise<{ result: StealthConfigWriteResult; configPath: string }> {
|
||||
const playwrightDir = path.join(sourceDir, '.playwright');
|
||||
const configPath = path.join(playwrightDir, 'cli.config.json');
|
||||
if (await pathExists(configPath)) {
|
||||
return { result: 'skipped-existing', configPath };
|
||||
}
|
||||
const initScriptPath = path.join(playwrightDir, 'scripts', 'stealth.js');
|
||||
await fs.mkdir(path.dirname(initScriptPath), { recursive: true });
|
||||
await fs.writeFile(initScriptPath, STEALTH_INIT_SCRIPT);
|
||||
await fs.writeFile(configPath, JSON.stringify(buildStealthConfig(initScriptPath), null, 2));
|
||||
return { result: 'wrote', configPath };
|
||||
}
|
||||
@@ -63,10 +63,7 @@ class NullProgressManager implements ProgressManager {
|
||||
}
|
||||
|
||||
// Returns no-op when disabled
|
||||
export function createProgressManager(
|
||||
context: ProgressContext,
|
||||
disableLoader: boolean
|
||||
): ProgressManager {
|
||||
export function createProgressManager(context: ProgressContext, disableLoader: boolean): ProgressManager {
|
||||
if (!context.useCleanOutput || disableLoader) {
|
||||
return new NullProgressManager();
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* Zod schema definitions for vulnerability exploitation queue structured outputs.
|
||||
*
|
||||
* Each vuln agent returns a structured JSON response matching its schema.
|
||||
* The SDK validates the output against the JSON Schema generated from these Zod definitions.
|
||||
*/
|
||||
|
||||
import type { JsonSchemaOutputFormat } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { z } from 'zod';
|
||||
import type { AgentName } from '../types/agents.js';
|
||||
|
||||
// === Common Fields ===
|
||||
|
||||
const ANALYSIS_NOTES_DESCRIPTION = 'Plain context for defenders (caveats, scope, what is at risk). Not attack steps.';
|
||||
|
||||
function notesField(exploit: boolean) {
|
||||
const f = z.string().optional();
|
||||
return exploit ? f : f.describe(ANALYSIS_NOTES_DESCRIPTION);
|
||||
}
|
||||
|
||||
function makeBase(exploit: boolean) {
|
||||
return z.object({
|
||||
ID: z.string(),
|
||||
vulnerability_type: z.string(),
|
||||
externally_exploitable: z.boolean(),
|
||||
confidence: z.string(),
|
||||
notes: notesField(exploit),
|
||||
});
|
||||
}
|
||||
|
||||
// === Per-Vuln-Type Schemas (used for type inference; notes description is mode-agnostic for types) ===
|
||||
|
||||
const baseVulnerability = makeBase(true);
|
||||
|
||||
const InjectionVulnerability = baseVulnerability.extend({
|
||||
source: z.string().optional(),
|
||||
combined_sources: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
sink_call: z.string().optional(),
|
||||
slot_type: z.string().optional(),
|
||||
sanitization_observed: z.string().optional(),
|
||||
concat_occurrences: z.string().optional(),
|
||||
verdict: z.string().optional(),
|
||||
mismatch_reason: z.string().optional(),
|
||||
witness_payload: z.string().optional(),
|
||||
});
|
||||
|
||||
const XssVulnerability = baseVulnerability.extend({
|
||||
source: z.string().optional(),
|
||||
source_detail: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
sink_function: z.string().optional(),
|
||||
render_context: z.string().optional(),
|
||||
encoding_observed: z.string().optional(),
|
||||
verdict: z.string().optional(),
|
||||
mismatch_reason: z.string().optional(),
|
||||
witness_payload: z.string().optional(),
|
||||
});
|
||||
|
||||
const AuthVulnerability = baseVulnerability.extend({
|
||||
source_endpoint: z.string().optional(),
|
||||
vulnerable_code_location: z.string().optional(),
|
||||
missing_defense: z.string().optional(),
|
||||
exploitation_hypothesis: z.string().optional(),
|
||||
suggested_exploit_technique: z.string().optional(),
|
||||
});
|
||||
|
||||
const SsrfVulnerability = baseVulnerability.extend({
|
||||
source_endpoint: z.string().optional(),
|
||||
vulnerable_parameter: z.string().optional(),
|
||||
vulnerable_code_location: z.string().optional(),
|
||||
missing_defense: z.string().optional(),
|
||||
exploitation_hypothesis: z.string().optional(),
|
||||
suggested_exploit_technique: z.string().optional(),
|
||||
});
|
||||
|
||||
const AuthzVulnerability = baseVulnerability.extend({
|
||||
endpoint: z.string().optional(),
|
||||
vulnerable_code_location: z.string().optional(),
|
||||
role_context: z.string().optional(),
|
||||
guard_evidence: z.string().optional(),
|
||||
side_effect: z.string().optional(),
|
||||
reason: z.string().optional(),
|
||||
minimal_witness: z.string().optional(),
|
||||
});
|
||||
|
||||
// === Inferred Entry Types (consumed by renderer) ===
|
||||
|
||||
export type InjectionFinding = z.infer<typeof InjectionVulnerability>;
|
||||
export type XssFinding = z.infer<typeof XssVulnerability>;
|
||||
export type AuthFinding = z.infer<typeof AuthVulnerability>;
|
||||
export type SsrfFinding = z.infer<typeof SsrfVulnerability>;
|
||||
export type AuthzFinding = z.infer<typeof AuthzVulnerability>;
|
||||
|
||||
// === Convert to JSON Schema for SDK ===
|
||||
|
||||
// NOTE: The SDK's AJV validator expects draft-07. Zod defaults to draft-2020-12 which
|
||||
// causes the SDK to silently skip structured output.
|
||||
function toOutputFormat(zodSchema: z.ZodType): JsonSchemaOutputFormat {
|
||||
return { type: 'json_schema', schema: z.toJSONSchema(zodSchema, { target: 'draft-07' }) as Record<string, unknown> };
|
||||
}
|
||||
|
||||
// === Per-Mode Output Format Builders ===
|
||||
// Two maps cached at module load; the only per-mode difference is the
|
||||
// description on the `notes` field, which steers the LLM's writing.
|
||||
|
||||
function buildOutputFormats(exploit: boolean): Partial<Record<AgentName, JsonSchemaOutputFormat>> {
|
||||
const base = makeBase(exploit);
|
||||
return {
|
||||
'injection-vuln': toOutputFormat(
|
||||
z.object({
|
||||
vulnerabilities: z.array(
|
||||
base.extend({
|
||||
source: z.string().optional(),
|
||||
combined_sources: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
sink_call: z.string().optional(),
|
||||
slot_type: z.string().optional(),
|
||||
sanitization_observed: z.string().optional(),
|
||||
concat_occurrences: z.string().optional(),
|
||||
verdict: z.string().optional(),
|
||||
mismatch_reason: z.string().optional(),
|
||||
witness_payload: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
'xss-vuln': toOutputFormat(
|
||||
z.object({
|
||||
vulnerabilities: z.array(
|
||||
base.extend({
|
||||
source: z.string().optional(),
|
||||
source_detail: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
sink_function: z.string().optional(),
|
||||
render_context: z.string().optional(),
|
||||
encoding_observed: z.string().optional(),
|
||||
verdict: z.string().optional(),
|
||||
mismatch_reason: z.string().optional(),
|
||||
witness_payload: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
'auth-vuln': toOutputFormat(
|
||||
z.object({
|
||||
vulnerabilities: z.array(
|
||||
base.extend({
|
||||
source_endpoint: z.string().optional(),
|
||||
vulnerable_code_location: z.string().optional(),
|
||||
missing_defense: z.string().optional(),
|
||||
exploitation_hypothesis: z.string().optional(),
|
||||
suggested_exploit_technique: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
'ssrf-vuln': toOutputFormat(
|
||||
z.object({
|
||||
vulnerabilities: z.array(
|
||||
base.extend({
|
||||
source_endpoint: z.string().optional(),
|
||||
vulnerable_parameter: z.string().optional(),
|
||||
vulnerable_code_location: z.string().optional(),
|
||||
missing_defense: z.string().optional(),
|
||||
exploitation_hypothesis: z.string().optional(),
|
||||
suggested_exploit_technique: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
'authz-vuln': toOutputFormat(
|
||||
z.object({
|
||||
vulnerabilities: z.array(
|
||||
base.extend({
|
||||
endpoint: z.string().optional(),
|
||||
vulnerable_code_location: z.string().optional(),
|
||||
role_context: z.string().optional(),
|
||||
guard_evidence: z.string().optional(),
|
||||
side_effect: z.string().optional(),
|
||||
reason: z.string().optional(),
|
||||
minimal_witness: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const OUTPUT_FORMATS_EXPLOIT = buildOutputFormats(true);
|
||||
const OUTPUT_FORMATS_ANALYSIS = buildOutputFormats(false);
|
||||
|
||||
const VULN_AGENT_QUEUE_FILENAMES: Partial<Record<AgentName, string>> = {
|
||||
'injection-vuln': 'injection_exploitation_queue.json',
|
||||
'xss-vuln': 'xss_exploitation_queue.json',
|
||||
'auth-vuln': 'auth_exploitation_queue.json',
|
||||
'ssrf-vuln': 'ssrf_exploitation_queue.json',
|
||||
'authz-vuln': 'authz_exploitation_queue.json',
|
||||
};
|
||||
|
||||
/** Returns the structured output format for a vuln agent, or undefined for non-vuln agents. */
|
||||
export function getOutputFormat(agentName: AgentName, exploit = true): JsonSchemaOutputFormat | undefined {
|
||||
return (exploit ? OUTPUT_FORMATS_EXPLOIT : OUTPUT_FORMATS_ANALYSIS)[agentName];
|
||||
}
|
||||
|
||||
/** Returns the queue filename for a vuln agent, or undefined for non-vuln agents. */
|
||||
export function getQueueFilename(agentName: AgentName): string | undefined {
|
||||
return VULN_AGENT_QUEUE_FILENAMES[agentName];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* Writes ~/.claude/settings.json with permissions.deny rules derived from
|
||||
* `code_path` avoid patterns. The SDK reads this via `settingSources: ['user']`;
|
||||
* deny rules fire even in `bypassPermissions` mode.
|
||||
*/
|
||||
|
||||
import os from 'node:os';
|
||||
import { fs, path } from 'zx';
|
||||
import type { DistributedConfig } from '../types/config.js';
|
||||
|
||||
const FILE_TOOLS = ['Read', 'Edit'] as const;
|
||||
|
||||
function denyEntriesFor(pattern: string): string[] {
|
||||
const arg = `./${pattern.replace(/^[./]+/, '')}`;
|
||||
return FILE_TOOLS.map((tool) => `${tool}(${arg})`);
|
||||
}
|
||||
|
||||
export async function writeUserSettingsForCodePathAvoids(config: DistributedConfig | null): Promise<void> {
|
||||
const avoidPatterns = (config?.avoid ?? []).filter((r) => r.type === 'code_path').map((r) => r.value);
|
||||
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
||||
|
||||
if (avoidPatterns.length === 0) {
|
||||
await fs.remove(settingsPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = {
|
||||
permissions: {
|
||||
deny: avoidPatterns.flatMap(denyEntriesFor),
|
||||
},
|
||||
};
|
||||
|
||||
await fs.ensureDir(path.dirname(settingsPath));
|
||||
await fs.writeJson(settingsPath, settings, { spaces: 2 });
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
// Type definitions for Claude executor message processing pipeline
|
||||
|
||||
import type { SDKAssistantMessageError } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
export interface ExecutionContext {
|
||||
isParallelExecution: boolean;
|
||||
useCleanOutput: boolean;
|
||||
@@ -13,22 +15,6 @@ export interface ExecutionContext {
|
||||
agentKey: string;
|
||||
}
|
||||
|
||||
export interface ProcessingState {
|
||||
turnCount: number;
|
||||
result: string | null;
|
||||
apiErrorDetected: boolean;
|
||||
totalCost: number;
|
||||
partialCost: number;
|
||||
lastHeartbeat: number;
|
||||
}
|
||||
|
||||
export interface ProcessingResult {
|
||||
result: string | null;
|
||||
turnCount: number;
|
||||
apiErrorDetected: boolean;
|
||||
totalCost: number;
|
||||
}
|
||||
|
||||
export interface AssistantResult {
|
||||
content: string;
|
||||
cleanedContent: string;
|
||||
@@ -46,7 +32,9 @@ export interface ResultData {
|
||||
cost: number;
|
||||
duration_ms: number;
|
||||
subtype?: string;
|
||||
stop_reason?: string | null;
|
||||
permissionDenials: number;
|
||||
structuredOutput?: unknown;
|
||||
}
|
||||
|
||||
export interface ToolUseData {
|
||||
@@ -64,10 +52,13 @@ export interface ToolResultData {
|
||||
export interface ContentBlock {
|
||||
type?: string;
|
||||
text?: string;
|
||||
thinking?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface AssistantMessage {
|
||||
type: 'assistant';
|
||||
error?: SDKAssistantMessageError;
|
||||
message: {
|
||||
content: ContentBlock[] | string;
|
||||
};
|
||||
@@ -79,7 +70,9 @@ export interface ResultMessage {
|
||||
total_cost_usd?: number;
|
||||
duration_ms?: number;
|
||||
subtype?: string;
|
||||
stop_reason?: string | null;
|
||||
permission_denials?: unknown[];
|
||||
structured_output?: unknown;
|
||||
}
|
||||
|
||||
export interface ToolUseMessage {
|
||||
@@ -98,37 +91,22 @@ export interface ApiErrorDetection {
|
||||
shouldThrow?: Error;
|
||||
}
|
||||
|
||||
// Message types from SDK stream
|
||||
export type SdkMessage =
|
||||
| AssistantMessage
|
||||
| ResultMessage
|
||||
| ToolUseMessage
|
||||
| ToolResultMessage
|
||||
| SystemInitMessage
|
||||
| UserMessage;
|
||||
|
||||
export interface SystemInitMessage {
|
||||
type: 'system';
|
||||
subtype: 'init';
|
||||
model?: string;
|
||||
permissionMode?: string;
|
||||
mcp_servers?: Array<{ name: string; status: string }>;
|
||||
}
|
||||
|
||||
/** Emitted when a model refuses a request and the SDK falls back to another model (e.g. Fable 5 routing cybersecurity tasks to Opus 4.8). */
|
||||
export interface ModelRefusalFallbackMessage {
|
||||
type: 'system';
|
||||
subtype: 'model_refusal_fallback';
|
||||
original_model: string;
|
||||
fallback_model: string;
|
||||
api_refusal_category?: string | null;
|
||||
}
|
||||
|
||||
export interface UserMessage {
|
||||
type: 'user';
|
||||
}
|
||||
|
||||
// Dispatch result types for message processing
|
||||
export type MessageDispatchResult =
|
||||
| { action: 'continue' }
|
||||
| { action: 'break'; result: string | null; cost: number }
|
||||
| { action: 'throw'; error: Error };
|
||||
|
||||
export interface MessageDispatchContext {
|
||||
turnCount: number;
|
||||
execContext: ExecutionContext;
|
||||
description: string;
|
||||
colorFn: (text: string) => string;
|
||||
useCleanOutput: boolean;
|
||||
}
|
||||
@@ -11,31 +11,24 @@
|
||||
* crash-safe audit logging.
|
||||
*/
|
||||
|
||||
import { PentestError } from '../services/error-handling.js';
|
||||
import { ErrorCode } from '../types/errors.js';
|
||||
import type { AgentEndResult } from '../types/index.js';
|
||||
import { SessionMutex } from '../utils/concurrency.js';
|
||||
import { formatTimestamp } from '../utils/formatting.js';
|
||||
import { AgentLogger } from './logger.js';
|
||||
import { WorkflowLogger, type AgentLogDetails, type WorkflowSummary } from './workflow-logger.js';
|
||||
import { MetricsTracker } from './metrics-tracker.js';
|
||||
import { initializeAuditStructure, type SessionMetadata } from './utils.js';
|
||||
import { formatTimestamp } from '../utils/formatting.js';
|
||||
import { SessionMutex } from '../utils/concurrency.js';
|
||||
import { type AgentLogDetails, WorkflowLogger, type WorkflowSummary } from './workflow-logger.js';
|
||||
|
||||
// Global mutex instance
|
||||
const sessionMutex = new SessionMutex();
|
||||
|
||||
interface AgentEndResult {
|
||||
attemptNumber: number;
|
||||
duration_ms: number;
|
||||
cost_usd: number;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
checkpoint?: string;
|
||||
isFinalAttempt?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* AuditSession - Main audit system facade
|
||||
*/
|
||||
export class AuditSession {
|
||||
private sessionMetadata: SessionMetadata;
|
||||
readonly sessionMetadata: SessionMetadata;
|
||||
private sessionId: string;
|
||||
private metricsTracker: MetricsTracker;
|
||||
private workflowLogger: WorkflowLogger;
|
||||
@@ -49,10 +42,22 @@ export class AuditSession {
|
||||
|
||||
// Validate required fields
|
||||
if (!this.sessionId) {
|
||||
throw new Error('sessionMetadata.id is required');
|
||||
throw new PentestError(
|
||||
'sessionMetadata.id is required',
|
||||
'config',
|
||||
false,
|
||||
{ field: 'sessionMetadata.id' },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
if (!this.sessionMetadata.webUrl) {
|
||||
throw new Error('sessionMetadata.webUrl is required');
|
||||
throw new PentestError(
|
||||
'sessionMetadata.webUrl is required',
|
||||
'config',
|
||||
false,
|
||||
{ field: 'sessionMetadata.webUrl' },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
// Components
|
||||
@@ -63,8 +68,10 @@ export class AuditSession {
|
||||
/**
|
||||
* Initialize audit session (creates directories, session.json)
|
||||
* Idempotent and race-safe
|
||||
*
|
||||
* @param workflowId - Optional workflow ID for tracking original or resume workflows
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
async initialize(workflowId?: string): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return; // Already initialized
|
||||
}
|
||||
@@ -73,10 +80,10 @@ export class AuditSession {
|
||||
await initializeAuditStructure(this.sessionMetadata);
|
||||
|
||||
// Initialize metrics tracker (loads or creates session.json)
|
||||
await this.metricsTracker.initialize();
|
||||
await this.metricsTracker.initialize(workflowId);
|
||||
|
||||
// Initialize workflow logger
|
||||
await this.workflowLogger.initialize();
|
||||
// Initialize workflow logger with actual Temporal workflow ID
|
||||
await this.workflowLogger.initialize(workflowId);
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
@@ -93,36 +100,29 @@ export class AuditSession {
|
||||
/**
|
||||
* Start agent execution
|
||||
*/
|
||||
async startAgent(
|
||||
agentName: string,
|
||||
promptContent: string,
|
||||
attemptNumber: number = 1
|
||||
): Promise<void> {
|
||||
async startAgent(agentName: string, promptContent: string, attemptNumber: number = 1): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
// Save prompt snapshot (only on first attempt)
|
||||
// 1. Save prompt snapshot (only on first attempt)
|
||||
if (attemptNumber === 1) {
|
||||
await AgentLogger.savePrompt(this.sessionMetadata, agentName, promptContent);
|
||||
}
|
||||
|
||||
// Track current agent name for workflow logging
|
||||
// 2. Create and initialize the per-agent logger
|
||||
this.currentAgentName = agentName;
|
||||
|
||||
// Create and initialize logger for this attempt
|
||||
this.currentLogger = new AgentLogger(this.sessionMetadata, agentName, attemptNumber);
|
||||
await this.currentLogger.initialize();
|
||||
|
||||
// Start metrics tracking
|
||||
// 3. Start metrics timer
|
||||
this.metricsTracker.startAgent(agentName, attemptNumber);
|
||||
|
||||
// Log start event
|
||||
// 4. Log start event to both agent log and workflow log
|
||||
await this.currentLogger.logEvent('agent_start', {
|
||||
agentName,
|
||||
attemptNumber,
|
||||
timestamp: formatTimestamp(),
|
||||
});
|
||||
|
||||
// Log to unified workflow log
|
||||
await this.workflowLogger.logAgent(agentName, 'start', { attemptNumber });
|
||||
}
|
||||
|
||||
@@ -131,7 +131,13 @@ export class AuditSession {
|
||||
*/
|
||||
async logEvent(eventType: string, eventData: unknown): Promise<void> {
|
||||
if (!this.currentLogger) {
|
||||
throw new Error('No active logger. Call startAgent() first.');
|
||||
throw new PentestError(
|
||||
'No active logger. Call startAgent() first.',
|
||||
'validation',
|
||||
false,
|
||||
{},
|
||||
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
// Log to agent-specific log file (JSON format)
|
||||
@@ -142,29 +148,29 @@ export class AuditSession {
|
||||
const agentName = this.currentAgentName || 'unknown';
|
||||
switch (eventType) {
|
||||
case 'tool_start':
|
||||
await this.workflowLogger.logToolStart(
|
||||
agentName,
|
||||
String(data.toolName || ''),
|
||||
data.parameters
|
||||
);
|
||||
await this.workflowLogger.logToolStart(agentName, String(data.toolName || ''), data.parameters);
|
||||
break;
|
||||
case 'llm_response':
|
||||
await this.workflowLogger.logLlmResponse(
|
||||
agentName,
|
||||
Number(data.turn || 0),
|
||||
String(data.content || '')
|
||||
);
|
||||
await this.workflowLogger.logLlmResponse(agentName, Number(data.turn || 0), String(data.content || ''));
|
||||
break;
|
||||
// tool_end and error events are intentionally not logged to workflow log
|
||||
// to reduce noise - the agent completion message captures the outcome
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a human-readable note to the unified workflow log (e.g. a model
|
||||
* refusal fallback). Independent of agent event logging.
|
||||
*/
|
||||
async logWorkflowNote(category: string, message: string): Promise<void> {
|
||||
await this.workflowLogger.logEvent(category, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* End agent execution (mutex-protected)
|
||||
*/
|
||||
async endAgent(agentName: string, result: AgentEndResult): Promise<void> {
|
||||
// Log end event
|
||||
// 1. Finalize agent log and close the stream
|
||||
if (this.currentLogger) {
|
||||
await this.currentLogger.logEvent('agent_end', {
|
||||
agentName,
|
||||
@@ -174,15 +180,13 @@ export class AuditSession {
|
||||
timestamp: formatTimestamp(),
|
||||
});
|
||||
|
||||
// Close logger
|
||||
await this.currentLogger.close();
|
||||
this.currentLogger = null;
|
||||
}
|
||||
|
||||
// Reset current agent name
|
||||
// 2. Log completion to the unified workflow log
|
||||
this.currentAgentName = null;
|
||||
|
||||
// Log to unified workflow log
|
||||
const agentLogDetails: AgentLogDetails = {
|
||||
attemptNumber: result.attemptNumber,
|
||||
duration_ms: result.duration_ms,
|
||||
@@ -192,13 +196,11 @@ export class AuditSession {
|
||||
};
|
||||
await this.workflowLogger.logAgent(agentName, 'end', agentLogDetails);
|
||||
|
||||
// Mutex-protected update to session.json
|
||||
// 3. Acquire mutex before touching session.json
|
||||
const unlock = await sessionMutex.lock(this.sessionId);
|
||||
try {
|
||||
// Reload inside mutex to prevent lost updates during parallel exploitation phase
|
||||
// 4. Reload-then-write inside mutex to prevent lost updates during parallel phases
|
||||
await this.metricsTracker.reload();
|
||||
|
||||
// Update metrics
|
||||
await this.metricsTracker.endAgent(agentName, result);
|
||||
} finally {
|
||||
unlock();
|
||||
@@ -208,7 +210,7 @@ export class AuditSession {
|
||||
/**
|
||||
* Update session status
|
||||
*/
|
||||
async updateSessionStatus(status: 'in-progress' | 'completed' | 'failed'): Promise<void> {
|
||||
async updateSessionStatus(status: 'in-progress' | 'completed' | 'failed' | 'cancelled'): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const unlock = await sessionMutex.lock(this.sessionId);
|
||||
@@ -251,4 +253,38 @@ export class AuditSession {
|
||||
await this.ensureInitialized();
|
||||
await this.workflowLogger.logWorkflowComplete(summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a resume attempt to the session
|
||||
* Call this when a workflow is resuming from an existing workspace
|
||||
*
|
||||
* @param workflowId - The new workflow ID for this resume attempt
|
||||
* @param terminatedWorkflows - IDs of workflows that were terminated
|
||||
* @param checkpointHash - Git checkpoint hash that was restored
|
||||
*/
|
||||
async addResumeAttempt(workflowId: string, terminatedWorkflows: string[], checkpointHash?: string): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const unlock = await sessionMutex.lock(this.sessionId);
|
||||
try {
|
||||
await this.metricsTracker.reload();
|
||||
await this.metricsTracker.addResumeAttempt(workflowId, terminatedWorkflows, checkpointHash);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log resume header to workflow.log
|
||||
* Call this when a workflow is resuming to add a visual separator
|
||||
*/
|
||||
async logResumeHeader(resumeInfo: {
|
||||
previousWorkflowId: string;
|
||||
newWorkflowId: string;
|
||||
checkpointHash: string;
|
||||
completedAgents: string[];
|
||||
}): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
await this.workflowLogger.logResumeHeader(resumeInfo);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,3 @@
|
||||
*/
|
||||
|
||||
export { AuditSession } from './audit-session.js';
|
||||
export { AgentLogger } from './logger.js';
|
||||
export { WorkflowLogger } from './workflow-logger.js';
|
||||
export { MetricsTracker } from './metrics-tracker.js';
|
||||
export * as AuditUtils from './utils.js';
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* LogStream - Stream composition utility for append-only logging
|
||||
*
|
||||
* Encapsulates the common stream management pattern used by AgentLogger
|
||||
* and WorkflowLogger: opening streams in append mode, handling backpressure,
|
||||
* and proper cleanup.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { ensureDirectory } from '../utils/file-io.js';
|
||||
|
||||
/**
|
||||
* LogStream - Manages a single append-only log file stream
|
||||
*/
|
||||
export class LogStream {
|
||||
private readonly filePath: string;
|
||||
private stream: fs.WriteStream | null = null;
|
||||
private _isOpen: boolean = false;
|
||||
|
||||
constructor(filePath: string) {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the stream for writing (creates parent directories, opens in append mode)
|
||||
*/
|
||||
async open(): Promise<void> {
|
||||
if (this._isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
await ensureDirectory(path.dirname(this.filePath));
|
||||
|
||||
// Create write stream in append mode
|
||||
this.stream = fs.createWriteStream(this.filePath, {
|
||||
flags: 'a',
|
||||
encoding: 'utf8',
|
||||
autoClose: true,
|
||||
});
|
||||
|
||||
// Handle stream errors to prevent crashes (log and mark closed)
|
||||
this.stream.on('error', (err) => {
|
||||
console.error(`LogStream error for ${this.filePath}:`, err.message);
|
||||
this._isOpen = false;
|
||||
});
|
||||
|
||||
this._isOpen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write text to the stream with backpressure handling
|
||||
*/
|
||||
async write(text: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._isOpen || !this.stream) {
|
||||
reject(new Error('LogStream not open'));
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = this.stream;
|
||||
let drainHandler: (() => void) | null = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (drainHandler) {
|
||||
stream.removeListener('drain', drainHandler);
|
||||
drainHandler = null;
|
||||
}
|
||||
};
|
||||
|
||||
const needsDrain = !stream.write(text, 'utf8', (error) => {
|
||||
cleanup();
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else if (!needsDrain) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
if (needsDrain) {
|
||||
drainHandler = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
stream.once('drain', drainHandler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the stream (flush and close)
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (!this._isOpen || !this.stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.stream?.end(() => {
|
||||
this._isOpen = false;
|
||||
this.stream = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the stream is currently open
|
||||
*/
|
||||
get isOpen(): boolean {
|
||||
return this._isOpen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path this stream writes to
|
||||
*/
|
||||
get path(): string {
|
||||
return this.filePath;
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,13 @@
|
||||
* Append-Only Agent Logger
|
||||
*
|
||||
* Provides crash-safe, append-only logging for agent execution.
|
||||
* Uses file streams with immediate flush to prevent data loss.
|
||||
* Uses LogStream for stream management with backpressure handling.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import {
|
||||
generateLogPath,
|
||||
generatePromptPath,
|
||||
type SessionMetadata,
|
||||
} from './utils.js';
|
||||
import { atomicWrite } from '../utils/file-io.js';
|
||||
import { formatTimestamp } from '../utils/formatting.js';
|
||||
import { LogStream } from './log-stream.js';
|
||||
import { generateLogPath, generatePromptPath, type SessionMetadata } from './utils.js';
|
||||
|
||||
interface LogEvent {
|
||||
type: string;
|
||||
@@ -30,13 +26,11 @@ interface LogEvent {
|
||||
* AgentLogger - Manages append-only logging for a single agent execution
|
||||
*/
|
||||
export class AgentLogger {
|
||||
private sessionMetadata: SessionMetadata;
|
||||
private agentName: string;
|
||||
private attemptNumber: number;
|
||||
private timestamp: number;
|
||||
private logPath: string;
|
||||
private stream: fs.WriteStream | null = null;
|
||||
private isOpen: boolean = false;
|
||||
private readonly sessionMetadata: SessionMetadata;
|
||||
private readonly agentName: string;
|
||||
private readonly attemptNumber: number;
|
||||
private readonly timestamp: number;
|
||||
private readonly logStream: LogStream;
|
||||
|
||||
constructor(sessionMetadata: SessionMetadata, agentName: string, attemptNumber: number) {
|
||||
this.sessionMetadata = sessionMetadata;
|
||||
@@ -44,26 +38,19 @@ export class AgentLogger {
|
||||
this.attemptNumber = attemptNumber;
|
||||
this.timestamp = Date.now();
|
||||
|
||||
// Generate log file path
|
||||
this.logPath = generateLogPath(sessionMetadata, agentName, this.timestamp, attemptNumber);
|
||||
const logPath = generateLogPath(sessionMetadata, agentName, this.timestamp, attemptNumber);
|
||||
this.logStream = new LogStream(logPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the log stream (creates file and opens stream)
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.isOpen) {
|
||||
if (this.logStream.isOpen) {
|
||||
return; // Already initialized
|
||||
}
|
||||
|
||||
// Create write stream with append mode and auto-flush
|
||||
this.stream = fs.createWriteStream(this.logPath, {
|
||||
flags: 'a', // Append mode
|
||||
encoding: 'utf8',
|
||||
autoClose: true,
|
||||
});
|
||||
|
||||
this.isOpen = true;
|
||||
await this.logStream.open();
|
||||
|
||||
// Write header
|
||||
await this.writeHeader();
|
||||
@@ -83,29 +70,7 @@ export class AgentLogger {
|
||||
`========================================\n`,
|
||||
].join('\n');
|
||||
|
||||
return this.writeRaw(header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write raw text to log file with immediate flush
|
||||
*/
|
||||
private writeRaw(text: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isOpen || !this.stream) {
|
||||
reject(new Error('Logger not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
const needsDrain = !this.stream.write(text, 'utf8', (error) => {
|
||||
if (error) reject(error);
|
||||
});
|
||||
|
||||
if (needsDrain) {
|
||||
this.stream.once('drain', resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
return this.logStream.write(header);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,34 +85,21 @@ export class AgentLogger {
|
||||
};
|
||||
|
||||
const eventLine = `${JSON.stringify(event)}\n`;
|
||||
return this.writeRaw(eventLine);
|
||||
return this.logStream.write(eventLine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the log stream
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (!this.isOpen || !this.stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.stream!.end(() => {
|
||||
this.isOpen = false;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return this.logStream.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save prompt snapshot to prompts directory
|
||||
* Static method - doesn't require logger instance
|
||||
*/
|
||||
static async savePrompt(
|
||||
sessionMetadata: SessionMetadata,
|
||||
agentName: string,
|
||||
promptContent: string
|
||||
): Promise<void> {
|
||||
static async savePrompt(sessionMetadata: SessionMetadata, agentName: string, promptContent: string): Promise<void> {
|
||||
const promptPath = generatePromptPath(sessionMetadata, agentName);
|
||||
|
||||
// Create header with metadata
|
||||
@@ -11,14 +11,13 @@
|
||||
* Tracks attempt-level data for complete forensic trail.
|
||||
*/
|
||||
|
||||
import {
|
||||
generateSessionJsonPath,
|
||||
type SessionMetadata,
|
||||
} from './utils.js';
|
||||
import { atomicWrite, readJson, fileExists } from '../utils/file-io.js';
|
||||
import { formatTimestamp, calculatePercentage } from '../utils/formatting.js';
|
||||
import { PentestError } from '../services/error-handling.js';
|
||||
import { AGENT_PHASE_MAP, type PhaseName } from '../session-manager.js';
|
||||
import type { AgentName } from '../types/index.js';
|
||||
import { ErrorCode } from '../types/errors.js';
|
||||
import type { AgentEndResult, AgentName } from '../types/index.js';
|
||||
import { atomicWrite, fileExists, readJson } from '../utils/file-io.js';
|
||||
import { calculatePercentage, formatTimestamp } from '../utils/formatting.js';
|
||||
import { generateSessionJsonPath, type SessionMetadata } from './utils.js';
|
||||
|
||||
interface AttemptData {
|
||||
attempt_number: number;
|
||||
@@ -26,15 +25,17 @@ interface AttemptData {
|
||||
cost_usd: number;
|
||||
success: boolean;
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
model?: string | undefined;
|
||||
error?: string | undefined;
|
||||
}
|
||||
|
||||
interface AgentMetrics {
|
||||
interface AgentAuditMetrics {
|
||||
status: 'in-progress' | 'success' | 'failed';
|
||||
attempts: AttemptData[];
|
||||
final_duration_ms: number;
|
||||
total_cost_usd: number;
|
||||
checkpoint?: string;
|
||||
model?: string | undefined;
|
||||
checkpoint?: string | undefined;
|
||||
}
|
||||
|
||||
interface PhaseMetrics {
|
||||
@@ -44,33 +45,32 @@ interface PhaseMetrics {
|
||||
agent_count: number;
|
||||
}
|
||||
|
||||
export interface ResumeAttempt {
|
||||
workflowId: string;
|
||||
timestamp: string;
|
||||
terminatedPrevious?: string;
|
||||
resumedFromCheckpoint?: string;
|
||||
}
|
||||
|
||||
interface SessionData {
|
||||
session: {
|
||||
id: string;
|
||||
webUrl: string;
|
||||
repoPath?: string;
|
||||
status: 'in-progress' | 'completed' | 'failed';
|
||||
status: 'in-progress' | 'completed' | 'failed' | 'cancelled';
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
originalWorkflowId?: string; // First workflow that created this workspace
|
||||
resumeAttempts?: ResumeAttempt[]; // Track all resume attempts
|
||||
};
|
||||
metrics: {
|
||||
total_duration_ms: number;
|
||||
total_cost_usd: number;
|
||||
phases: Record<string, PhaseMetrics>;
|
||||
agents: Record<string, AgentMetrics>;
|
||||
agents: Record<string, AgentAuditMetrics>;
|
||||
};
|
||||
}
|
||||
|
||||
interface AgentEndResult {
|
||||
attemptNumber: number;
|
||||
duration_ms: number;
|
||||
cost_usd: number;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
checkpoint?: string;
|
||||
isFinalAttempt?: boolean;
|
||||
}
|
||||
|
||||
interface ActiveTimer {
|
||||
startTime: number;
|
||||
attemptNumber: number;
|
||||
@@ -92,8 +92,10 @@ export class MetricsTracker {
|
||||
|
||||
/**
|
||||
* Initialize session.json (idempotent)
|
||||
*
|
||||
* @param workflowId - Optional workflow ID to set as originalWorkflowId for new sessions
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
async initialize(workflowId?: string): Promise<void> {
|
||||
// Check if session.json already exists
|
||||
const exists = await fileExists(this.sessionJsonPath);
|
||||
|
||||
@@ -102,21 +104,24 @@ export class MetricsTracker {
|
||||
this.data = await readJson<SessionData>(this.sessionJsonPath);
|
||||
} else {
|
||||
// Create new session.json
|
||||
this.data = this.createInitialData();
|
||||
this.data = this.createInitialData(workflowId);
|
||||
await this.save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create initial session.json structure
|
||||
*
|
||||
* @param workflowId - Optional workflow ID to set as originalWorkflowId
|
||||
*/
|
||||
private createInitialData(): SessionData {
|
||||
private createInitialData(workflowId?: string): SessionData {
|
||||
const sessionData: SessionData = {
|
||||
session: {
|
||||
id: this.sessionMetadata.id,
|
||||
webUrl: this.sessionMetadata.webUrl,
|
||||
status: 'in-progress',
|
||||
createdAt: (this.sessionMetadata as { createdAt?: string }).createdAt || formatTimestamp(),
|
||||
resumeAttempts: [],
|
||||
},
|
||||
metrics: {
|
||||
total_duration_ms: 0,
|
||||
@@ -125,6 +130,12 @@ export class MetricsTracker {
|
||||
agents: {}, // Agent-level metrics
|
||||
},
|
||||
};
|
||||
|
||||
// Set originalWorkflowId if provided (for new workspaces)
|
||||
if (workflowId) {
|
||||
sessionData.session.originalWorkflowId = workflowId;
|
||||
}
|
||||
|
||||
// Only add repoPath if it exists
|
||||
if (this.sessionMetadata.repoPath) {
|
||||
sessionData.session.repoPath = this.sessionMetadata.repoPath;
|
||||
@@ -147,10 +158,16 @@ export class MetricsTracker {
|
||||
*/
|
||||
async endAgent(agentName: string, result: AgentEndResult): Promise<void> {
|
||||
if (!this.data) {
|
||||
throw new Error('MetricsTracker not initialized');
|
||||
throw new PentestError(
|
||||
'MetricsTracker not initialized',
|
||||
'validation',
|
||||
false,
|
||||
{},
|
||||
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize agent metrics if not exists
|
||||
// 1. Initialize agent metrics if first time seeing this agent
|
||||
const existingAgent = this.data.metrics.agents[agentName];
|
||||
const agent = existingAgent ?? {
|
||||
status: 'in-progress' as const,
|
||||
@@ -160,7 +177,7 @@ export class MetricsTracker {
|
||||
};
|
||||
this.data.metrics.agents[agentName] = agent;
|
||||
|
||||
// Add attempt to array
|
||||
// 2. Build attempt record with optional model/error fields
|
||||
const attempt: AttemptData = {
|
||||
attempt_number: result.attemptNumber,
|
||||
duration_ms: result.duration_ms,
|
||||
@@ -169,55 +186,111 @@ export class MetricsTracker {
|
||||
timestamp: formatTimestamp(),
|
||||
};
|
||||
|
||||
if (result.model) {
|
||||
attempt.model = result.model;
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
attempt.error = result.error;
|
||||
}
|
||||
|
||||
// 3. Append attempt to history
|
||||
agent.attempts.push(attempt);
|
||||
|
||||
// Update total cost (includes failed attempts)
|
||||
// 4. Recalculate total cost across all attempts (includes failures)
|
||||
agent.total_cost_usd = agent.attempts.reduce((sum, a) => sum + a.cost_usd, 0);
|
||||
|
||||
// If successful, update final metrics and status
|
||||
// 5. Update agent status based on outcome
|
||||
if (result.success) {
|
||||
agent.status = 'success';
|
||||
agent.final_duration_ms = result.duration_ms;
|
||||
|
||||
// 6. Attach model and checkpoint metadata on success
|
||||
if (result.model) {
|
||||
agent.model = result.model;
|
||||
}
|
||||
|
||||
if (result.checkpoint) {
|
||||
agent.checkpoint = result.checkpoint;
|
||||
}
|
||||
} else {
|
||||
// If this was the last attempt, mark as failed
|
||||
if (result.isFinalAttempt) {
|
||||
agent.status = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
// Clear active timer
|
||||
// 7. Clear active timer
|
||||
this.activeTimers.delete(agentName);
|
||||
|
||||
// Recalculate aggregations
|
||||
// 8. Recalculate phase and session-level aggregations
|
||||
this.recalculateAggregations();
|
||||
|
||||
// Save to disk
|
||||
// 9. Persist to session.json
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session status
|
||||
*/
|
||||
async updateSessionStatus(status: 'in-progress' | 'completed' | 'failed'): Promise<void> {
|
||||
async updateSessionStatus(status: 'in-progress' | 'completed' | 'failed' | 'cancelled'): Promise<void> {
|
||||
if (!this.data) return;
|
||||
|
||||
this.data.session.status = status;
|
||||
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
||||
this.data.session.completedAt = formatTimestamp();
|
||||
}
|
||||
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a resume attempt to the session
|
||||
*
|
||||
* @param workflowId - The new workflow ID for this resume attempt
|
||||
* @param terminatedWorkflows - IDs of workflows that were terminated
|
||||
* @param checkpointHash - Git checkpoint hash that was restored
|
||||
*/
|
||||
async addResumeAttempt(workflowId: string, terminatedWorkflows: string[], checkpointHash?: string): Promise<void> {
|
||||
if (!this.data) {
|
||||
throw new PentestError(
|
||||
'MetricsTracker not initialized',
|
||||
'validation',
|
||||
false,
|
||||
{},
|
||||
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure originalWorkflowId is set (backfill if missing from old sessions)
|
||||
if (!this.data.session.originalWorkflowId) {
|
||||
this.data.session.originalWorkflowId = this.data.session.id;
|
||||
}
|
||||
|
||||
// Ensure resumeAttempts array exists
|
||||
if (!this.data.session.resumeAttempts) {
|
||||
this.data.session.resumeAttempts = [];
|
||||
}
|
||||
|
||||
// Add new resume attempt
|
||||
const resumeAttempt: ResumeAttempt = {
|
||||
workflowId,
|
||||
timestamp: formatTimestamp(),
|
||||
};
|
||||
|
||||
if (terminatedWorkflows.length > 0) {
|
||||
resumeAttempt.terminatedPrevious = terminatedWorkflows.join(',');
|
||||
}
|
||||
|
||||
if (checkpointHash) {
|
||||
resumeAttempt.resumedFromCheckpoint = checkpointHash;
|
||||
}
|
||||
|
||||
this.data.session.resumeAttempts.push(resumeAttempt);
|
||||
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate aggregations (total duration, total cost, phases)
|
||||
*/
|
||||
@@ -227,15 +300,10 @@ export class MetricsTracker {
|
||||
const agents = this.data.metrics.agents;
|
||||
|
||||
// Only count successful agents
|
||||
const successfulAgents = Object.entries(agents).filter(
|
||||
([, data]) => data.status === 'success'
|
||||
);
|
||||
const successfulAgents = Object.entries(agents).filter(([, data]) => data.status === 'success');
|
||||
|
||||
// Calculate total duration and cost
|
||||
const totalDuration = successfulAgents.reduce(
|
||||
(sum, [, data]) => sum + data.final_duration_ms,
|
||||
0
|
||||
);
|
||||
const totalDuration = successfulAgents.reduce((sum, [, data]) => sum + data.final_duration_ms, 0);
|
||||
|
||||
const totalCost = successfulAgents.reduce((sum, [, data]) => sum + data.total_cost_usd, 0);
|
||||
|
||||
@@ -249,15 +317,13 @@ export class MetricsTracker {
|
||||
/**
|
||||
* Calculate phase-level metrics
|
||||
*/
|
||||
private calculatePhaseMetrics(
|
||||
successfulAgents: Array<[string, AgentMetrics]>
|
||||
): Record<string, PhaseMetrics> {
|
||||
const phases: Record<PhaseName, AgentMetrics[]> = {
|
||||
private calculatePhaseMetrics(successfulAgents: Array<[string, AgentAuditMetrics]>): Record<string, PhaseMetrics> {
|
||||
const phases: Record<PhaseName, AgentAuditMetrics[]> = {
|
||||
'pre-recon': [],
|
||||
'recon': [],
|
||||
recon: [],
|
||||
'vulnerability-analysis': [],
|
||||
'exploitation': [],
|
||||
'reporting': [],
|
||||
exploitation: [],
|
||||
reporting: [],
|
||||
};
|
||||
|
||||
// Group agents by phase using imported AGENT_PHASE_MAP
|
||||
@@ -270,6 +336,7 @@ export class MetricsTracker {
|
||||
|
||||
// Calculate metrics per phase
|
||||
const phaseMetrics: Record<string, PhaseMetrics> = {};
|
||||
// biome-ignore lint/style/noNonNullAssertion: called from recalculateAggregations which guards this.data
|
||||
const totalDuration = this.data!.metrics.total_duration_ms;
|
||||
|
||||
for (const [phaseName, agentList] of Object.entries(phases)) {
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* Audit System Utilities
|
||||
*
|
||||
* Core utility functions for path generation, atomic writes, and formatting.
|
||||
* All functions are pure and crash-safe.
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { WORKSPACES_DIR } from '../paths.js';
|
||||
import { ensureDirectory } from '../utils/file-io.js';
|
||||
|
||||
export type { SessionMetadata } from '../types/audit.js';
|
||||
|
||||
import type { SessionMetadata } from '../types/audit.js';
|
||||
|
||||
/**
|
||||
* Extract and sanitize hostname from URL for use in identifiers
|
||||
*/
|
||||
export function sanitizeHostname(url: string): string {
|
||||
return new URL(url).hostname.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate standardized session identifier from workflow ID
|
||||
* Workflow IDs already contain hostname, so we use them directly
|
||||
*/
|
||||
export function generateSessionIdentifier(sessionMetadata: SessionMetadata): string {
|
||||
return sessionMetadata.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate path to audit log directory for a session
|
||||
* Uses custom outputPath if provided, otherwise defaults to WORKSPACES_DIR
|
||||
*/
|
||||
export function generateAuditPath(sessionMetadata: SessionMetadata): string {
|
||||
const sessionIdentifier = generateSessionIdentifier(sessionMetadata);
|
||||
const baseDir = sessionMetadata.outputPath || WORKSPACES_DIR;
|
||||
return path.join(baseDir, sessionIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate path to agent log file
|
||||
*/
|
||||
export function generateLogPath(
|
||||
sessionMetadata: SessionMetadata,
|
||||
agentName: string,
|
||||
timestamp: number,
|
||||
attemptNumber: number,
|
||||
): string {
|
||||
const auditPath = generateAuditPath(sessionMetadata);
|
||||
const filename = `${timestamp}_${agentName}_attempt-${attemptNumber}.log`;
|
||||
return path.join(auditPath, 'agents', filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate path to prompt snapshot file
|
||||
*/
|
||||
export function generatePromptPath(sessionMetadata: SessionMetadata, agentName: string): string {
|
||||
const auditPath = generateAuditPath(sessionMetadata);
|
||||
return path.join(auditPath, 'prompts', `${agentName}.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate path to session.json file
|
||||
*/
|
||||
export function generateSessionJsonPath(sessionMetadata: SessionMetadata): string {
|
||||
const auditPath = generateAuditPath(sessionMetadata);
|
||||
return path.join(auditPath, 'session.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to the shared authenticated browser session saved by the preflight
|
||||
* validator and consumed by downstream agents via `_shared-session.txt`.
|
||||
*/
|
||||
export function authStateFile(sessionMetadata: SessionMetadata): string {
|
||||
return path.join(generateAuditPath(sessionMetadata), 'auth-state.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate path to workflow.log file
|
||||
*/
|
||||
export function generateWorkflowLogPath(sessionMetadata: SessionMetadata): string {
|
||||
const auditPath = generateAuditPath(sessionMetadata);
|
||||
return path.join(auditPath, 'workflow.log');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize audit directory structure for a session
|
||||
* Creates: workspaces/{sessionId}/, agents/, prompts/, deliverables/
|
||||
*/
|
||||
export async function initializeAuditStructure(sessionMetadata: SessionMetadata): Promise<void> {
|
||||
const auditPath = generateAuditPath(sessionMetadata);
|
||||
const agentsPath = path.join(auditPath, 'agents');
|
||||
const promptsPath = path.join(auditPath, 'prompts');
|
||||
const deliverablesPath = path.join(auditPath, 'deliverables');
|
||||
|
||||
await ensureDirectory(auditPath);
|
||||
await ensureDirectory(agentsPath);
|
||||
await ensureDirectory(promptsPath);
|
||||
await ensureDirectory(deliverablesPath);
|
||||
}
|
||||
@@ -11,10 +11,11 @@
|
||||
* Optimized for `tail -f` viewing during concurrent workflow execution.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { generateWorkflowLogPath, ensureDirectory, type SessionMetadata } from './utils.js';
|
||||
import fs from 'node:fs/promises';
|
||||
import { isFableModel, resolveModel } from '../ai/models.js';
|
||||
import { formatDuration, formatTimestamp } from '../utils/formatting.js';
|
||||
import { LogStream } from './log-stream.js';
|
||||
import { generateWorkflowLogPath, type SessionMetadata } from './utils.js';
|
||||
|
||||
export interface AgentLogDetails {
|
||||
attemptNumber?: number;
|
||||
@@ -30,7 +31,7 @@ export interface AgentMetricsSummary {
|
||||
}
|
||||
|
||||
export interface WorkflowSummary {
|
||||
status: 'completed' | 'failed';
|
||||
status: 'completed' | 'failed' | 'cancelled';
|
||||
totalDurationMs: number;
|
||||
totalCostUsd: number;
|
||||
completedAgents: string[];
|
||||
@@ -42,38 +43,32 @@ export interface WorkflowSummary {
|
||||
* WorkflowLogger - Manages the unified workflow log file
|
||||
*/
|
||||
export class WorkflowLogger {
|
||||
private sessionMetadata: SessionMetadata;
|
||||
private logPath: string;
|
||||
private stream: fs.WriteStream | null = null;
|
||||
private initialized: boolean = false;
|
||||
private readonly sessionMetadata: SessionMetadata;
|
||||
private readonly logStream: LogStream;
|
||||
private workflowId: string | undefined;
|
||||
|
||||
constructor(sessionMetadata: SessionMetadata) {
|
||||
this.sessionMetadata = sessionMetadata;
|
||||
this.logPath = generateWorkflowLogPath(sessionMetadata);
|
||||
const logPath = generateWorkflowLogPath(sessionMetadata);
|
||||
this.logStream = new LogStream(logPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the log stream (creates file and writes header)
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) {
|
||||
async initialize(workflowId?: string): Promise<void> {
|
||||
if (workflowId) {
|
||||
this.workflowId = workflowId;
|
||||
}
|
||||
|
||||
if (this.logStream.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
await ensureDirectory(path.dirname(this.logPath));
|
||||
|
||||
// Create write stream with append mode
|
||||
this.stream = fs.createWriteStream(this.logPath, {
|
||||
flags: 'a',
|
||||
encoding: 'utf8',
|
||||
autoClose: true,
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
await this.logStream.open();
|
||||
|
||||
// Write header only if file is new (empty)
|
||||
const stats = await fs.promises.stat(this.logPath).catch(() => null);
|
||||
const stats = await fs.stat(this.logStream.path).catch(() => null);
|
||||
if (!stats || stats.size === 0) {
|
||||
await this.writeHeader();
|
||||
}
|
||||
@@ -83,40 +78,59 @@ export class WorkflowLogger {
|
||||
* Write header to log file
|
||||
*/
|
||||
private async writeHeader(): Promise<void> {
|
||||
const header = [
|
||||
const lines = [
|
||||
`================================================================================`,
|
||||
`Shannon Pentest - Workflow Log`,
|
||||
`================================================================================`,
|
||||
`Workflow ID: ${this.sessionMetadata.id}`,
|
||||
`Workflow ID: ${this.workflowId ?? this.sessionMetadata.id}`,
|
||||
`Target URL: ${this.sessionMetadata.webUrl}`,
|
||||
`Started: ${formatTimestamp()}`,
|
||||
];
|
||||
|
||||
// Surface Fable usage: its safety classifiers route cybersecurity tasks to
|
||||
// Opus 4.8, so those phases run on Opus 4.8 regardless of the tier setting.
|
||||
const fableTiers = (['small', 'medium', 'large'] as const)
|
||||
.map((tier) => ({ tier, model: resolveModel(tier) }))
|
||||
.filter(({ model }) => isFableModel(model));
|
||||
if (fableTiers.length > 0) {
|
||||
const tierList = fableTiers.map(({ tier, model }) => `${tier} (${model})`).join(', ');
|
||||
lines.push(
|
||||
`Note: ${tierList} set to a Fable model. Fable's safety classifiers`,
|
||||
` route cybersecurity tasks to Opus 4.8, so those phases run on Opus 4.8.`,
|
||||
);
|
||||
}
|
||||
|
||||
lines.push(`================================================================================`, ``);
|
||||
|
||||
return this.logStream.write(lines.join('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write resume header to log file when workflow is resumed
|
||||
*/
|
||||
async logResumeHeader(resumeInfo: {
|
||||
previousWorkflowId: string;
|
||||
newWorkflowId: string;
|
||||
checkpointHash: string;
|
||||
completedAgents: string[];
|
||||
}): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const header = [
|
||||
``,
|
||||
`================================================================================`,
|
||||
`RESUMED`,
|
||||
`================================================================================`,
|
||||
`Previous Workflow ID: ${resumeInfo.previousWorkflowId}`,
|
||||
`New Workflow ID: ${resumeInfo.newWorkflowId}`,
|
||||
`Resumed At: ${formatTimestamp()}`,
|
||||
`Checkpoint: ${resumeInfo.checkpointHash}`,
|
||||
`Completed: ${resumeInfo.completedAgents.length} agents (${resumeInfo.completedAgents.join(', ')})`,
|
||||
`================================================================================`,
|
||||
``,
|
||||
].join('\n');
|
||||
|
||||
return this.writeRaw(header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write raw text to log file with immediate flush
|
||||
*/
|
||||
private writeRaw(text: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.initialized || !this.stream) {
|
||||
reject(new Error('WorkflowLogger not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
const needsDrain = !this.stream.write(text, 'utf8', (error) => {
|
||||
if (error) reject(error);
|
||||
});
|
||||
|
||||
if (needsDrain) {
|
||||
this.stream.once('drain', resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
return this.logStream.write(header);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,20 +152,16 @@ export class WorkflowLogger {
|
||||
|
||||
// Add blank line before phase start for readability
|
||||
if (event === 'start') {
|
||||
await this.writeRaw('\n');
|
||||
await this.logStream.write('\n');
|
||||
}
|
||||
|
||||
await this.writeRaw(line);
|
||||
await this.logStream.write(line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an agent event
|
||||
*/
|
||||
async logAgent(
|
||||
agentName: string,
|
||||
event: 'start' | 'end',
|
||||
details?: AgentLogDetails
|
||||
): Promise<void> {
|
||||
async logAgent(agentName: string, event: 'start' | 'end', details?: AgentLogDetails): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
let message: string;
|
||||
@@ -160,7 +170,7 @@ export class WorkflowLogger {
|
||||
const attempt = details?.attemptNumber ?? 1;
|
||||
message = `${agentName}: Starting (attempt ${attempt})`;
|
||||
} else {
|
||||
const parts: string[] = [agentName + ':'];
|
||||
const parts: string[] = [`${agentName}:`];
|
||||
|
||||
if (details?.success === false) {
|
||||
parts.push('Failed');
|
||||
@@ -184,7 +194,7 @@ export class WorkflowLogger {
|
||||
}
|
||||
|
||||
const line = `[${this.formatLogTime()}] [AGENT] ${message}\n`;
|
||||
await this.writeRaw(line);
|
||||
await this.logStream.write(line);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,7 +204,7 @@ export class WorkflowLogger {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const line = `[${this.formatLogTime()}] [${eventType.toUpperCase()}] ${message}\n`;
|
||||
await this.writeRaw(line);
|
||||
await this.logStream.write(line);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,7 +215,7 @@ export class WorkflowLogger {
|
||||
|
||||
const contextStr = context ? ` (${context})` : '';
|
||||
const line = `[${this.formatLogTime()}] [ERROR] ${error.message}${contextStr}\n`;
|
||||
await this.writeRaw(line);
|
||||
await this.logStream.write(line);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,7 +223,7 @@ export class WorkflowLogger {
|
||||
*/
|
||||
private truncate(str: string, maxLen: number): string {
|
||||
if (str.length <= maxLen) return str;
|
||||
return str.slice(0, maxLen - 3) + '...';
|
||||
return `${str.slice(0, maxLen - 3)}...`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,22 +274,6 @@ export class WorkflowLogger {
|
||||
return String(p.url);
|
||||
}
|
||||
break;
|
||||
case 'mcp__playwright__browser_navigate':
|
||||
if (p.url) {
|
||||
return String(p.url);
|
||||
}
|
||||
break;
|
||||
case 'mcp__playwright__browser_click':
|
||||
if (p.selector) {
|
||||
return this.truncate(String(p.selector), 60);
|
||||
}
|
||||
break;
|
||||
case 'mcp__playwright__browser_type':
|
||||
if (p.selector) {
|
||||
const text = p.text ? `: "${this.truncate(String(p.text), 30)}"` : '';
|
||||
return `${this.truncate(String(p.selector), 40)}${text}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Default: show first string-valued param truncated
|
||||
@@ -301,7 +295,7 @@ export class WorkflowLogger {
|
||||
const params = this.formatToolParams(toolName, parameters);
|
||||
const paramStr = params ? `: ${params}` : '';
|
||||
const line = `[${this.formatLogTime()}] [${agentName}] [TOOL] ${toolName}${paramStr}\n`;
|
||||
await this.writeRaw(line);
|
||||
await this.logStream.write(line);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,7 +307,23 @@ export class WorkflowLogger {
|
||||
// Show full content, replacing newlines with escaped version for single-line output
|
||||
const escaped = content.replace(/\n/g, '\\n');
|
||||
const line = `[${this.formatLogTime()}] [${agentName}] [LLM] Turn ${turn}: ${escaped}\n`;
|
||||
await this.writeRaw(line);
|
||||
await this.logStream.write(line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a pipe-delimited error string into indented multi-line display.
|
||||
*
|
||||
* Input: "phase context|ErrorType|message|Hint: ..."
|
||||
* Output: "Error: phase context\n ErrorType\n ..."
|
||||
*/
|
||||
private formatErrorBlock(errorString: string): string {
|
||||
const segments = errorString.split('|');
|
||||
const label = 'Error: ';
|
||||
const indent = ' '.repeat(label.length);
|
||||
|
||||
const lines = segments.map((segment, i) => (i === 0 ? `${label}${segment.trim()}` : `${indent}${segment.trim()}`));
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,42 +334,47 @@ export class WorkflowLogger {
|
||||
|
||||
const status = summary.status === 'completed' ? 'COMPLETED' : 'FAILED';
|
||||
|
||||
await this.writeRaw('\n');
|
||||
await this.writeRaw(`================================================================================\n`);
|
||||
await this.writeRaw(`Workflow ${status}\n`);
|
||||
await this.writeRaw(`────────────────────────────────────────\n`);
|
||||
await this.writeRaw(`Workflow ID: ${this.sessionMetadata.id}\n`);
|
||||
await this.writeRaw(`Status: ${summary.status}\n`);
|
||||
await this.writeRaw(`Duration: ${formatDuration(summary.totalDurationMs)}\n`);
|
||||
await this.writeRaw(`Total Cost: $${summary.totalCostUsd.toFixed(4)}\n`);
|
||||
await this.writeRaw(`Agents: ${summary.completedAgents.length} completed\n`);
|
||||
const lines: string[] = [
|
||||
'',
|
||||
'================================================================================',
|
||||
`Workflow ${status}`,
|
||||
'────────────────────────────────────────',
|
||||
`Workflow ID: ${this.workflowId ?? this.sessionMetadata.id}`,
|
||||
`Status: ${summary.status}`,
|
||||
`Duration: ${formatDuration(summary.totalDurationMs)}`,
|
||||
`Total Cost: $${summary.totalCostUsd.toFixed(4)}`,
|
||||
`Agents: ${summary.completedAgents.length} completed`,
|
||||
];
|
||||
|
||||
if (summary.error) {
|
||||
await this.writeRaw(`Error: ${summary.error}\n`);
|
||||
lines.push(this.formatErrorBlock(summary.error).trimEnd());
|
||||
}
|
||||
|
||||
await this.writeRaw(`\n`);
|
||||
await this.writeRaw(`Agent Breakdown:\n`);
|
||||
lines.push('');
|
||||
lines.push('Agent Breakdown:');
|
||||
|
||||
for (const agentName of summary.completedAgents) {
|
||||
const metrics = summary.agentMetrics[agentName];
|
||||
if (metrics) {
|
||||
const duration = formatDuration(metrics.durationMs);
|
||||
const cost = metrics.costUsd !== null ? `$${metrics.costUsd.toFixed(4)}` : 'N/A';
|
||||
await this.writeRaw(` - ${agentName} (${duration}, ${cost})\n`);
|
||||
lines.push(` - ${agentName} (${duration}, ${cost})`);
|
||||
} else {
|
||||
await this.writeRaw(` - ${agentName}\n`);
|
||||
lines.push(` - ${agentName}`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.writeRaw(`================================================================================\n`);
|
||||
lines.push('================================================================================');
|
||||
|
||||
// Single atomic write to prevent interleaved/duplicate output in log tailers
|
||||
await this.logStream.write(`${lines.join('\n')}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure initialized (helper for lazy initialization)
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
if (!this.logStream.isOpen) {
|
||||
await this.initialize();
|
||||
}
|
||||
}
|
||||
@@ -368,15 +383,6 @@ export class WorkflowLogger {
|
||||
* Close the log stream
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (!this.initialized || !this.stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.stream!.end(() => {
|
||||
this.initialized = false;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return this.logStream.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,721 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
import { Ajv, type ErrorObject, type ValidateFunction } from 'ajv';
|
||||
import type { FormatsPlugin } from 'ajv-formats';
|
||||
import yaml from 'js-yaml';
|
||||
import { fs } from 'zx';
|
||||
import { PentestError } from './services/error-handling.js';
|
||||
import {
|
||||
ALL_VULN_CLASSES,
|
||||
type Authentication,
|
||||
type Config,
|
||||
type DistributedConfig,
|
||||
type Rule,
|
||||
} from './types/config.js';
|
||||
import { ErrorCode } from './types/errors.js';
|
||||
|
||||
// Handle ESM/CJS interop for ajv-formats using require
|
||||
const require = createRequire(import.meta.url);
|
||||
const addFormats: FormatsPlugin = require('ajv-formats');
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, verbose: true });
|
||||
addFormats(ajv);
|
||||
|
||||
let configSchema: object;
|
||||
let validateSchema: ValidateFunction;
|
||||
|
||||
try {
|
||||
const schemaPath = new URL('../configs/config-schema.json', import.meta.url);
|
||||
const schemaContent = await fs.readFile(schemaPath, 'utf8');
|
||||
configSchema = JSON.parse(schemaContent) as object;
|
||||
validateSchema = ajv.compile(configSchema);
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
throw new PentestError(`Failed to load configuration schema: ${errMsg}`, 'config', false, {
|
||||
schemaPath: '../configs/config-schema.json',
|
||||
originalError: errMsg,
|
||||
});
|
||||
}
|
||||
|
||||
const DANGEROUS_PATTERNS: RegExp[] = [
|
||||
/\.\.\//, // Path traversal
|
||||
/[<>]/, // HTML/XML injection
|
||||
/javascript:/i, // JavaScript URLs
|
||||
/data:/i, // Data URLs
|
||||
/file:/i, // File URLs
|
||||
];
|
||||
|
||||
/**
|
||||
* Format a single AJV error into a human-readable message.
|
||||
* Translates AJV error keywords into plain English descriptions.
|
||||
*/
|
||||
function formatAjvError(error: ErrorObject): string {
|
||||
const path = error.instancePath || 'root';
|
||||
const params = error.params as Record<string, unknown>;
|
||||
|
||||
switch (error.keyword) {
|
||||
case 'required': {
|
||||
const missingProperty = params.missingProperty as string;
|
||||
return `Missing required field: "${missingProperty}" at ${path || 'root'}`;
|
||||
}
|
||||
|
||||
case 'type': {
|
||||
const expectedType = params.type as string;
|
||||
return `Invalid type at ${path}: expected ${expectedType}`;
|
||||
}
|
||||
|
||||
case 'enum': {
|
||||
const allowedValues = params.allowedValues as unknown[];
|
||||
const formattedValues = allowedValues.map((v) => `"${v}"`).join(', ');
|
||||
return `Invalid value at ${path}: must be one of [${formattedValues}]`;
|
||||
}
|
||||
|
||||
case 'additionalProperties': {
|
||||
const additionalProperty = params.additionalProperty as string;
|
||||
return `Unknown field at ${path}: "${additionalProperty}" is not allowed`;
|
||||
}
|
||||
|
||||
case 'minLength': {
|
||||
const limit = params.limit as number;
|
||||
return `Value at ${path} is too short: must have at least ${limit} character(s)`;
|
||||
}
|
||||
|
||||
case 'maxLength': {
|
||||
const limit = params.limit as number;
|
||||
return `Value at ${path} is too long: must have at most ${limit} character(s)`;
|
||||
}
|
||||
|
||||
case 'minimum': {
|
||||
const limit = params.limit as number;
|
||||
return `Value at ${path} is too small: must be >= ${limit}`;
|
||||
}
|
||||
|
||||
case 'maximum': {
|
||||
const limit = params.limit as number;
|
||||
return `Value at ${path} is too large: must be <= ${limit}`;
|
||||
}
|
||||
|
||||
case 'minItems': {
|
||||
const limit = params.limit as number;
|
||||
return `Array at ${path} has too few items: must have at least ${limit} item(s)`;
|
||||
}
|
||||
|
||||
case 'maxItems': {
|
||||
const limit = params.limit as number;
|
||||
return `Array at ${path} has too many items: must have at most ${limit} item(s)`;
|
||||
}
|
||||
|
||||
case 'pattern': {
|
||||
const pattern = params.pattern as string;
|
||||
return `Value at ${path} does not match required pattern: ${pattern}`;
|
||||
}
|
||||
|
||||
case 'format': {
|
||||
const format = params.format as string;
|
||||
return `Value at ${path} must be a valid ${format}`;
|
||||
}
|
||||
|
||||
case 'const': {
|
||||
const allowedValue = params.allowedValue as unknown;
|
||||
return `Value at ${path} must be exactly "${allowedValue}"`;
|
||||
}
|
||||
|
||||
case 'oneOf': {
|
||||
return `Value at ${path} must match exactly one schema (matched ${params.passingSchemas ?? 0})`;
|
||||
}
|
||||
|
||||
case 'anyOf': {
|
||||
return `Value at ${path} must match at least one of the allowed schemas`;
|
||||
}
|
||||
|
||||
case 'not': {
|
||||
return `Value at ${path} matches a schema it should not match`;
|
||||
}
|
||||
|
||||
case 'if': {
|
||||
return `Value at ${path} does not satisfy conditional schema requirements`;
|
||||
}
|
||||
|
||||
case 'uniqueItems': {
|
||||
const i = params.i as number;
|
||||
const j = params.j as number;
|
||||
return `Array at ${path} contains duplicate items at positions ${j} and ${i}`;
|
||||
}
|
||||
|
||||
case 'propertyNames': {
|
||||
const propertyName = params.propertyName as string;
|
||||
return `Invalid property name at ${path}: "${propertyName}" does not match naming requirements`;
|
||||
}
|
||||
|
||||
case 'dependencies':
|
||||
case 'dependentRequired': {
|
||||
const property = params.property as string;
|
||||
const missingProperty = params.missingProperty as string;
|
||||
return `Missing dependent field at ${path}: "${missingProperty}" is required when "${property}" is present`;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Fallback for any unhandled keywords - use AJV's message if available
|
||||
const message = error.message || `validation failed for keyword "${error.keyword}"`;
|
||||
return `${path}: ${message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format all AJV errors into a list of human-readable messages.
|
||||
* Returns an array of formatted error strings.
|
||||
*/
|
||||
function formatAjvErrors(errors: ErrorObject[]): string[] {
|
||||
return errors.map(formatAjvError);
|
||||
}
|
||||
|
||||
export const parseConfig = async (configPath: string): Promise<Config> => {
|
||||
try {
|
||||
// 1. Verify file exists
|
||||
if (!(await fs.pathExists(configPath))) {
|
||||
throw new PentestError(
|
||||
`Configuration file not found: ${configPath}`,
|
||||
'config',
|
||||
false,
|
||||
{ configPath },
|
||||
ErrorCode.CONFIG_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check file size
|
||||
const stats = await fs.stat(configPath);
|
||||
const maxFileSize = 1024 * 1024; // 1MB
|
||||
if (stats.size > maxFileSize) {
|
||||
throw new PentestError(
|
||||
`Configuration file too large: ${stats.size} bytes (maximum: ${maxFileSize} bytes)`,
|
||||
'config',
|
||||
false,
|
||||
{ configPath, fileSize: stats.size, maxFileSize },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Read and check for empty content
|
||||
const configContent = await fs.readFile(configPath, 'utf8');
|
||||
|
||||
if (!configContent.trim()) {
|
||||
throw new PentestError(
|
||||
'Configuration file is empty',
|
||||
'config',
|
||||
false,
|
||||
{ configPath },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Parse YAML with safe schema
|
||||
let config: unknown;
|
||||
try {
|
||||
config = yaml.load(configContent, {
|
||||
schema: yaml.FAILSAFE_SCHEMA, // Only basic YAML types, no JS evaluation
|
||||
json: false, // Don't allow JSON-specific syntax
|
||||
filename: configPath,
|
||||
});
|
||||
} catch (yamlError) {
|
||||
const errMsg = yamlError instanceof Error ? yamlError.message : String(yamlError);
|
||||
throw new PentestError(
|
||||
`YAML parsing failed: ${errMsg}`,
|
||||
'config',
|
||||
false,
|
||||
{ configPath, originalError: errMsg },
|
||||
ErrorCode.CONFIG_PARSE_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Guard against null/undefined parse result
|
||||
if (config === null || config === undefined) {
|
||||
throw new PentestError(
|
||||
'Configuration file resulted in null/undefined after parsing',
|
||||
'config',
|
||||
false,
|
||||
{ configPath },
|
||||
ErrorCode.CONFIG_PARSE_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Validate schema, security rules, and return
|
||||
validateConfig(config as Config);
|
||||
|
||||
return config as Config;
|
||||
} catch (error) {
|
||||
// PentestError instances are already well-formatted, re-throw as-is
|
||||
if (error instanceof PentestError) {
|
||||
throw error;
|
||||
}
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
throw new PentestError(
|
||||
`Failed to parse configuration file '${configPath}': ${errMsg}`,
|
||||
'config',
|
||||
false,
|
||||
{ configPath, originalError: errMsg },
|
||||
ErrorCode.CONFIG_PARSE_ERROR,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a raw YAML string into a validated Config object.
|
||||
*
|
||||
* Same validation as parseConfig but accepts a string instead of a file path.
|
||||
* Used when config YAML is passed inline (e.g., from a parent workflow).
|
||||
*/
|
||||
export const parseConfigYAML = (yamlContent: string): Config => {
|
||||
if (!yamlContent.trim()) {
|
||||
throw new PentestError(
|
||||
'Configuration YAML string is empty',
|
||||
'config',
|
||||
false,
|
||||
{},
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
let config: unknown;
|
||||
try {
|
||||
config = yaml.load(yamlContent, {
|
||||
schema: yaml.FAILSAFE_SCHEMA,
|
||||
json: false,
|
||||
});
|
||||
} catch (yamlError) {
|
||||
const errMsg = yamlError instanceof Error ? yamlError.message : String(yamlError);
|
||||
throw new PentestError(
|
||||
`YAML parsing failed: ${errMsg}`,
|
||||
'config',
|
||||
false,
|
||||
{ originalError: errMsg },
|
||||
ErrorCode.CONFIG_PARSE_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
if (config === null || config === undefined) {
|
||||
throw new PentestError(
|
||||
'Configuration YAML resulted in null/undefined after parsing',
|
||||
'config',
|
||||
false,
|
||||
{},
|
||||
ErrorCode.CONFIG_PARSE_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
validateConfig(config as Config);
|
||||
return config as Config;
|
||||
};
|
||||
|
||||
function checkDeprecatedFields(config: Config): void {
|
||||
const messages: string[] = [];
|
||||
|
||||
const checkRules = (rules: unknown, where: string): void => {
|
||||
if (!Array.isArray(rules)) return;
|
||||
rules.forEach((rule, idx) => {
|
||||
if (typeof rule !== 'object' || rule === null) return;
|
||||
const r = rule as Record<string, unknown>;
|
||||
if (r.type === 'path') {
|
||||
messages.push(`rules.${where}[${idx}].type: 'path' has been renamed to 'url_path'.`);
|
||||
}
|
||||
if ('url_path' in r && !('value' in r)) {
|
||||
messages.push(`rules.${where}[${idx}]: the rule field 'url_path' has been renamed to 'value'.`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const raw = config as Record<string, unknown>;
|
||||
const rules = raw.rules as { avoid?: unknown; focus?: unknown } | undefined;
|
||||
checkRules(rules?.avoid, 'avoid');
|
||||
checkRules(rules?.focus, 'focus');
|
||||
|
||||
if (messages.length > 0) {
|
||||
throw new PentestError(
|
||||
`Configuration uses deprecated fields. Please update:\n - ${messages.join('\n - ')}`,
|
||||
'config',
|
||||
false,
|
||||
{ deprecatedFields: messages },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const validateConfig = (config: Config): void => {
|
||||
if (!config || typeof config !== 'object') {
|
||||
throw new PentestError(
|
||||
'Configuration must be a valid object',
|
||||
'config',
|
||||
false,
|
||||
{},
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(config)) {
|
||||
throw new PentestError(
|
||||
'Configuration must be an object, not an array',
|
||||
'config',
|
||||
false,
|
||||
{},
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
checkDeprecatedFields(config);
|
||||
|
||||
const isValid = validateSchema(config);
|
||||
if (!isValid) {
|
||||
const errors = validateSchema.errors || [];
|
||||
const errorMessages = formatAjvErrors(errors);
|
||||
throw new PentestError(
|
||||
`Configuration validation failed:\n - ${errorMessages.join('\n - ')}`,
|
||||
'config',
|
||||
false,
|
||||
{ validationErrors: errorMessages },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
performSecurityValidation(config);
|
||||
|
||||
const hasAnySteering =
|
||||
!!config.rules ||
|
||||
!!config.authentication ||
|
||||
!!config.description ||
|
||||
!!config.vuln_classes ||
|
||||
config.exploit !== undefined ||
|
||||
!!config.report ||
|
||||
!!config.rules_of_engagement;
|
||||
if (!hasAnySteering) {
|
||||
console.warn('⚠️ Configuration file contains no steering fields. The pentest will run with all defaults.');
|
||||
} else if (config.rules && !config.rules.avoid && !config.rules.focus) {
|
||||
console.warn('⚠️ Configuration file contains no rules. The pentest will run without any scoping restrictions.');
|
||||
}
|
||||
};
|
||||
|
||||
const performSecurityValidation = (config: Config): void => {
|
||||
if (config.authentication) {
|
||||
const auth = config.authentication;
|
||||
|
||||
// Check login_url for dangerous patterns (AJV's "uri" format allows javascript: per RFC 3986)
|
||||
if (auth.login_url) {
|
||||
for (const pattern of DANGEROUS_PATTERNS) {
|
||||
if (pattern.test(auth.login_url)) {
|
||||
throw new PentestError(
|
||||
`authentication.login_url contains potentially dangerous pattern: ${pattern.source}`,
|
||||
'config',
|
||||
false,
|
||||
{ field: 'login_url', pattern: pattern.source },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auth.credentials) {
|
||||
for (const pattern of DANGEROUS_PATTERNS) {
|
||||
if (pattern.test(auth.credentials.username)) {
|
||||
throw new PentestError(
|
||||
`authentication.credentials.username contains potentially dangerous pattern: ${pattern.source}`,
|
||||
'config',
|
||||
false,
|
||||
{ field: 'credentials.username', pattern: pattern.source },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auth.login_flow) {
|
||||
auth.login_flow.forEach((step, index) => {
|
||||
for (const pattern of DANGEROUS_PATTERNS) {
|
||||
if (pattern.test(step)) {
|
||||
throw new PentestError(
|
||||
`authentication.login_flow[${index}] contains potentially dangerous pattern: ${pattern.source}`,
|
||||
'config',
|
||||
false,
|
||||
{ field: `login_flow[${index}]`, pattern: pattern.source },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (config.rules) {
|
||||
validateRulesSecurity(config.rules.avoid, 'avoid');
|
||||
validateRulesSecurity(config.rules.focus, 'focus');
|
||||
|
||||
checkForDuplicates(config.rules.avoid || [], 'avoid');
|
||||
checkForDuplicates(config.rules.focus || [], 'focus');
|
||||
checkForConflicts(config.rules.avoid, config.rules.focus);
|
||||
}
|
||||
|
||||
if (config.description) {
|
||||
for (const pattern of DANGEROUS_PATTERNS) {
|
||||
if (pattern.test(config.description)) {
|
||||
throw new PentestError(
|
||||
`description contains potentially dangerous pattern: ${pattern.source}`,
|
||||
'config',
|
||||
false,
|
||||
{ field: 'description', pattern: pattern.source },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.rules_of_engagement) {
|
||||
for (const pattern of DANGEROUS_PATTERNS) {
|
||||
if (pattern.test(config.rules_of_engagement)) {
|
||||
throw new PentestError(
|
||||
`rules_of_engagement contains potentially dangerous pattern: ${pattern.source}`,
|
||||
'config',
|
||||
false,
|
||||
{ field: 'rules_of_engagement', pattern: pattern.source },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.report?.guidance) {
|
||||
for (const pattern of DANGEROUS_PATTERNS) {
|
||||
if (pattern.test(config.report.guidance)) {
|
||||
throw new PentestError(
|
||||
`report.guidance contains potentially dangerous pattern: ${pattern.source}`,
|
||||
'config',
|
||||
false,
|
||||
{ field: 'report.guidance', pattern: pattern.source },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validateRulesSecurity = (rules: Rule[] | undefined, ruleType: string): void => {
|
||||
if (!rules) return;
|
||||
|
||||
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 (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`;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const checkForDuplicates = (rules: Rule[], ruleType: string): void => {
|
||||
const seen = new Set<string>();
|
||||
rules.forEach((rule, index) => {
|
||||
const key = `${rule.type}:${rule.value}`;
|
||||
if (seen.has(key)) {
|
||||
throw new PentestError(
|
||||
`Duplicate rule found in rules.${ruleType}[${index}]: ${rule.type} '${rule.value}'`,
|
||||
'config',
|
||||
false,
|
||||
{ field: `rules.${ruleType}[${index}]`, ruleType: rule.type, value: rule.value },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
seen.add(key);
|
||||
});
|
||||
};
|
||||
|
||||
const checkForConflicts = (avoidRules: Rule[] = [], focusRules: Rule[] = []): void => {
|
||||
const avoidSet = new Set(avoidRules.map((rule) => `${rule.type}:${rule.value}`));
|
||||
|
||||
focusRules.forEach((rule, index) => {
|
||||
const key = `${rule.type}:${rule.value}`;
|
||||
if (avoidSet.has(key)) {
|
||||
throw new PentestError(
|
||||
`Conflicting rule found: rules.focus[${index}] '${rule.value}' also exists in rules.avoid`,
|
||||
'config',
|
||||
false,
|
||||
{ field: `rules.focus[${index}]`, value: rule.value },
|
||||
ErrorCode.CONFIG_VALIDATION_FAILED,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sanitizeRule = (rule: Rule): Rule => {
|
||||
return {
|
||||
description: rule.description.trim(),
|
||||
type: rule.type.toLowerCase().trim() as Rule['type'],
|
||||
value: rule.value.trim(),
|
||||
};
|
||||
};
|
||||
|
||||
export const distributeConfig = (config: Config | null): DistributedConfig => {
|
||||
const avoid = config?.rules?.avoid || [];
|
||||
const focus = config?.rules?.focus || [];
|
||||
const authentication = config?.authentication || null;
|
||||
const description = config?.description?.trim() || '';
|
||||
|
||||
const vuln_classes =
|
||||
config?.vuln_classes && config.vuln_classes.length > 0 ? [...config.vuln_classes] : [...ALL_VULN_CLASSES];
|
||||
|
||||
const exploit = config?.exploit !== undefined ? config.exploit === 'true' : true;
|
||||
|
||||
const report = {
|
||||
...(config?.report?.min_severity && { min_severity: config.report.min_severity }),
|
||||
...(config?.report?.min_confidence && { min_confidence: config.report.min_confidence }),
|
||||
...(config?.report?.guidance && { guidance: config.report.guidance.trim() }),
|
||||
};
|
||||
|
||||
const rules_of_engagement = config?.rules_of_engagement?.trim() ?? '';
|
||||
|
||||
return {
|
||||
avoid: avoid.map(sanitizeRule),
|
||||
focus: focus.map(sanitizeRule),
|
||||
authentication: authentication ? sanitizeAuthentication(authentication) : null,
|
||||
description,
|
||||
vuln_classes,
|
||||
exploit,
|
||||
report,
|
||||
rules_of_engagement,
|
||||
};
|
||||
};
|
||||
|
||||
const sanitizeAuthentication = (auth: Authentication): Authentication => {
|
||||
return {
|
||||
login_type: auth.login_type.toLowerCase().trim() as Authentication['login_type'],
|
||||
login_url: auth.login_url.trim(),
|
||||
credentials: {
|
||||
username: auth.credentials.username.trim(),
|
||||
...(auth.credentials.password && { password: auth.credentials.password }),
|
||||
...(auth.credentials.totp_secret && { totp_secret: auth.credentials.totp_secret.trim() }),
|
||||
...(auth.credentials.email_login && {
|
||||
email_login: {
|
||||
address: auth.credentials.email_login.address.trim(),
|
||||
password: auth.credentials.email_login.password,
|
||||
...(auth.credentials.email_login.totp_secret && {
|
||||
totp_secret: auth.credentials.email_login.totp_secret.trim(),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
...(auth.login_flow && { login_flow: auth.login_flow.map((step) => step.trim()) }),
|
||||
success_condition: {
|
||||
type: auth.success_condition.type.toLowerCase().trim() as Authentication['success_condition']['type'],
|
||||
value: auth.success_condition.value.trim(),
|
||||
},
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user