mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-07-05 12:47:57 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35f59f30f6 | |||
| 7813baf16a | |||
| 8f5d639f0d |
@@ -7,6 +7,9 @@ CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000
|
|||||||
# Adaptive thinking is enabled automatically on Opus 4.6/4.7. Set to false to disable.
|
# Adaptive thinking is enabled automatically on Opus 4.6/4.7. Set to false to disable.
|
||||||
# CLAUDE_ADAPTIVE_THINKING=false
|
# 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
|
# OPTION 1: Direct Anthropic
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ Infra (Temporal) runs via `docker-compose.yml`. Workers are ephemeral `docker ru
|
|||||||
- `docker-compose.yml` — Infra only: `shannon-temporal` (port 7233/8233). Network: `shannon-net`
|
- `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"]`
|
- `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
|
- 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).
|
||||||
|
|
||||||
### Worker Package (`apps/worker/`)
|
### Worker Package (`apps/worker/`)
|
||||||
- `apps/worker/src/paths.ts` — Centralized path constants (`PROMPTS_DIR`, `CONFIGS_DIR`, `WORKSPACES_DIR`)
|
- `apps/worker/src/paths.ts` — Centralized path constants (`PROMPTS_DIR`, `CONFIGS_DIR`, `WORKSPACES_DIR`)
|
||||||
@@ -146,7 +147,7 @@ Durable workflow orchestration with crash recovery, queryable progress, intellig
|
|||||||
### Supporting Systems
|
### 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`)
|
- **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
|
- **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 (`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
|
- **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 (`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
|
- **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`)
|
- **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`
|
- **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`
|
||||||
|
|||||||
@@ -685,6 +685,12 @@ npx @keygraph/shannon start -u http://host.docker.internal:3000 -r /path/to/repo
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
**Custom hostnames in `/etc/hosts`:**
|
||||||
|
|
||||||
|
If your local stack uses custom hostnames mapped in `/etc/hosts`, Shannon forwards those entries into the worker container at scan start:
|
||||||
|
|
||||||
|
To disable, add `SHANNON_FORWARD_HOSTS=false` to `.env` (local mode) or export it in your shell: `export SHANNON_FORWARD_HOSTS=false`. In npx mode, the shell export is the only option since there's no `.env`.
|
||||||
|
|
||||||
### Output and Results
|
### Output and Results
|
||||||
|
|
||||||
All results are saved to the workspaces directory: `./workspaces/` (local mode) or `~/.shannon/workspaces/` (npx mode). Use `-o <path>` to copy deliverables to a custom output directory after the run completes.
|
All results are saved to the workspaces directory: `./workspaces/` (local mode) or `~/.shannon/workspaces/` (npx mode). Use `-o <path>` to copy deliverables to a custom output directory after the run completes.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import { type ChildProcess, execFileSync, spawn } from 'node:child_process';
|
import { type ChildProcess, execFileSync, spawn } from 'node:child_process';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
import fs from 'node:fs';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { setTimeout as sleep } from 'node:timers/promises';
|
import { setTimeout as sleep } from 'node:timers/promises';
|
||||||
@@ -145,6 +146,87 @@ function addHostFlag(): string[] {
|
|||||||
return [];
|
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 {
|
export interface WorkerOptions {
|
||||||
version: string;
|
version: string;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -176,6 +258,9 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
|
|||||||
// Add host flag for Linux
|
// Add host flag for Linux
|
||||||
args.push(...addHostFlag());
|
args.push(...addHostFlag());
|
||||||
|
|
||||||
|
// Forward user-added /etc/hosts entries into the worker
|
||||||
|
args.push(...forwardEtcHostsFlags());
|
||||||
|
|
||||||
// UID remapping for Linux bind mounts
|
// UID remapping for Linux bind mounts
|
||||||
if (os.platform() === 'linux' && process.getuid && process.getgid) {
|
if (os.platform() === 'linux' && process.getuid && process.getgid) {
|
||||||
args.push('-e', `SHANNON_HOST_UID=${process.getuid()}`, '-e', `SHANNON_HOST_GID=${process.getgid()}`);
|
args.push('-e', `SHANNON_HOST_UID=${process.getuid()}`, '-e', `SHANNON_HOST_GID=${process.getgid()}`);
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ After exhaustive bypass attempts, determine:
|
|||||||
@include(shared/_rules.txt)
|
@include(shared/_rules.txt)
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
|
@include(shared/_shared-session.txt)
|
||||||
|
|
||||||
<login_instructions>
|
<login_instructions>
|
||||||
{{LOGIN_INSTRUCTIONS}}
|
{{LOGIN_INSTRUCTIONS}}
|
||||||
</login_instructions>
|
</login_instructions>
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ Remember: An unproven vulnerability is worse than no finding at all - it wastes
|
|||||||
@include(shared/_rules.txt)
|
@include(shared/_rules.txt)
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
|
@include(shared/_shared-session.txt)
|
||||||
|
|
||||||
<login_instructions>
|
<login_instructions>
|
||||||
{{LOGIN_INSTRUCTIONS}}
|
{{LOGIN_INSTRUCTIONS}}
|
||||||
</login_instructions>
|
</login_instructions>
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ After exhaustive bypass attempts, determine:
|
|||||||
@include(shared/_rules.txt)
|
@include(shared/_rules.txt)
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
|
@include(shared/_shared-session.txt)
|
||||||
|
|
||||||
<login_instructions>
|
<login_instructions>
|
||||||
{{LOGIN_INSTRUCTIONS}}
|
{{LOGIN_INSTRUCTIONS}}
|
||||||
</login_instructions>
|
</login_instructions>
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ After exhaustive bypass attempts, determine:
|
|||||||
@include(shared/_rules.txt)
|
@include(shared/_rules.txt)
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
|
@include(shared/_shared-session.txt)
|
||||||
|
|
||||||
<login_instructions>
|
<login_instructions>
|
||||||
{{LOGIN_INSTRUCTIONS}}
|
{{LOGIN_INSTRUCTIONS}}
|
||||||
</login_instructions>
|
</login_instructions>
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
Return the structured verdict `{ "login_success": true }` and stop.
|
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.
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ Areas to Focus On:
|
|||||||
|
|
||||||
@include(shared/_code-path-rules.txt)
|
@include(shared/_code-path-rules.txt)
|
||||||
|
|
||||||
|
@include(shared/_shared-session.txt)
|
||||||
|
|
||||||
<login_instructions>
|
<login_instructions>
|
||||||
{{LOGIN_INSTRUCTIONS}}
|
{{LOGIN_INSTRUCTIONS}}
|
||||||
</login_instructions>
|
</login_instructions>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -19,6 +19,14 @@ This runs as a preflight check for our AI pentester. The user supplies credentia
|
|||||||
{{LOGIN_INSTRUCTIONS}}
|
{{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>
|
<critical>
|
||||||
- Submit each field (username, password, captcha, TOTP) exactly once.
|
- Submit each field (username, password, captcha, TOTP) exactly once.
|
||||||
- Any rejection = auth error: return `login_success: false` and stop. Do not retry.
|
- Any rejection = auth error: return `login_success: false` and stop. Do not retry.
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ Success criterion: A complete, code-backed analysis of every potential authoriza
|
|||||||
|
|
||||||
@include(shared/_code-path-rules.txt)
|
@include(shared/_code-path-rules.txt)
|
||||||
|
|
||||||
|
@include(shared/_shared-session.txt)
|
||||||
|
|
||||||
<login_instructions>
|
<login_instructions>
|
||||||
{{LOGIN_INSTRUCTIONS}}
|
{{LOGIN_INSTRUCTIONS}}
|
||||||
</login_instructions>
|
</login_instructions>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ Success criterion: Complete source-to-sink traces detailing path, sanitizers, si
|
|||||||
|
|
||||||
@include(shared/_code-path-rules.txt)
|
@include(shared/_code-path-rules.txt)
|
||||||
|
|
||||||
|
@include(shared/_shared-session.txt)
|
||||||
|
|
||||||
<login_instructions>
|
<login_instructions>
|
||||||
{{LOGIN_INSTRUCTIONS}}
|
{{LOGIN_INSTRUCTIONS}}
|
||||||
</login_instructions>
|
</login_instructions>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ Success criterion: A complete source-to-sink trace for every identified SSRF vul
|
|||||||
|
|
||||||
@include(shared/_code-path-rules.txt)
|
@include(shared/_code-path-rules.txt)
|
||||||
|
|
||||||
|
@include(shared/_shared-session.txt)
|
||||||
|
|
||||||
<login_instructions>
|
<login_instructions>
|
||||||
{{LOGIN_INSTRUCTIONS}}
|
{{LOGIN_INSTRUCTIONS}}
|
||||||
</login_instructions>
|
</login_instructions>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ Success criterion: Live confirmation of XSS execution for every vulnerability th
|
|||||||
|
|
||||||
@include(shared/_code-path-rules.txt)
|
@include(shared/_code-path-rules.txt)
|
||||||
|
|
||||||
|
@include(shared/_shared-session.txt)
|
||||||
|
|
||||||
<login_instructions>
|
<login_instructions>
|
||||||
{{LOGIN_INSTRUCTIONS}}
|
{{LOGIN_INSTRUCTIONS}}
|
||||||
</login_instructions>
|
</login_instructions>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const sessionMutex = new SessionMutex();
|
|||||||
* AuditSession - Main audit system facade
|
* AuditSession - Main audit system facade
|
||||||
*/
|
*/
|
||||||
export class AuditSession {
|
export class AuditSession {
|
||||||
private sessionMetadata: SessionMetadata;
|
readonly sessionMetadata: SessionMetadata;
|
||||||
private sessionId: string;
|
private sessionId: string;
|
||||||
private metricsTracker: MetricsTracker;
|
private metricsTracker: MetricsTracker;
|
||||||
private workflowLogger: WorkflowLogger;
|
private workflowLogger: WorkflowLogger;
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ export function generateSessionJsonPath(sessionMetadata: SessionMetadata): strin
|
|||||||
return path.join(auditPath, 'session.json');
|
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
|
* Generate path to workflow.log file
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { fs, path } from 'zx';
|
|||||||
import { type ClaudePromptResult, runClaudePrompt, validateAgentOutput } from '../ai/claude-executor.js';
|
import { type ClaudePromptResult, runClaudePrompt, validateAgentOutput } from '../ai/claude-executor.js';
|
||||||
import { getOutputFormat, getQueueFilename } from '../ai/queue-schemas.js';
|
import { getOutputFormat, getQueueFilename } from '../ai/queue-schemas.js';
|
||||||
import type { AuditSession } from '../audit/index.js';
|
import type { AuditSession } from '../audit/index.js';
|
||||||
|
import { authStateFile } from '../audit/utils.js';
|
||||||
import { AGENTS } from '../session-manager.js';
|
import { AGENTS } from '../session-manager.js';
|
||||||
import type { ActivityLogger } from '../types/activity-logger.js';
|
import type { ActivityLogger } from '../types/activity-logger.js';
|
||||||
import type { AgentName } from '../types/agents.js';
|
import type { AgentName } from '../types/agents.js';
|
||||||
@@ -122,7 +123,7 @@ export class AgentExecutionService {
|
|||||||
try {
|
try {
|
||||||
prompt = await loadPrompt(
|
prompt = await loadPrompt(
|
||||||
promptTemplate,
|
promptTemplate,
|
||||||
{ webUrl, repoPath },
|
{ webUrl, repoPath, AUTH_STATE_FILE: authStateFile(auditSession.sessionMetadata) },
|
||||||
distributedConfig,
|
distributedConfig,
|
||||||
pipelineTestingMode,
|
pipelineTestingMode,
|
||||||
logger,
|
logger,
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ function renderReportFilterRules(report: ReportConfig | undefined): string {
|
|||||||
interface PromptVariables {
|
interface PromptVariables {
|
||||||
webUrl: string;
|
webUrl: string;
|
||||||
repoPath: string;
|
repoPath: string;
|
||||||
|
AUTH_STATE_FILE: string;
|
||||||
PLAYWRIGHT_SESSION?: string;
|
PLAYWRIGHT_SESSION?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +322,12 @@ async function interpolateVariables(
|
|||||||
result = result.replace(/<rules_of_engagement>[\s\S]*?<\/rules_of_engagement>\s*/g, '');
|
result = result.replace(/<rules_of_engagement>[\s\S]*?<\/rules_of_engagement>\s*/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!config?.authentication) {
|
||||||
|
result = result.replace(/<shared_authenticated_session>[\s\S]*?<\/shared_authenticated_session>\s*/g, '');
|
||||||
|
} else {
|
||||||
|
result = result.replace(/{{AUTH_STATE_FILE}}/g, variables.AUTH_STATE_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
if (config?.authentication?.login_flow) {
|
if (config?.authentication?.login_flow) {
|
||||||
const loginInstructions = await buildLoginInstructions(config.authentication, logger, promptsBaseDir);
|
const loginInstructions = await buildLoginInstructions(config.authentication, logger, promptsBaseDir);
|
||||||
result = result.replace(/{{LOGIN_INSTRUCTIONS}}/g, loginInstructions);
|
result = result.replace(/{{LOGIN_INSTRUCTIONS}}/g, loginInstructions);
|
||||||
|
|||||||
@@ -12,10 +12,12 @@
|
|||||||
* pipeline burns hours on broken auth.
|
* pipeline burns hours on broken auth.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { readFile, rm } from 'node:fs/promises';
|
||||||
import type { JsonSchemaOutputFormat } from '@anthropic-ai/claude-agent-sdk';
|
import type { JsonSchemaOutputFormat } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { runClaudePrompt } from '../ai/claude-executor.js';
|
import { runClaudePrompt } from '../ai/claude-executor.js';
|
||||||
import type { AuditSession } from '../audit/index.js';
|
import type { AuditSession } from '../audit/index.js';
|
||||||
|
import { authStateFile } from '../audit/utils.js';
|
||||||
import type { ActivityLogger } from '../types/activity-logger.js';
|
import type { ActivityLogger } from '../types/activity-logger.js';
|
||||||
import type { AgentEndResult } from '../types/audit.js';
|
import type { AgentEndResult } from '../types/audit.js';
|
||||||
import type { DistributedConfig, ProviderConfig } from '../types/config.js';
|
import type { DistributedConfig, ProviderConfig } from '../types/config.js';
|
||||||
@@ -93,9 +95,12 @@ export async function validateAuthentication(input: ValidateAuthInput): Promise<
|
|||||||
loginType: authentication.login_type,
|
loginType: authentication.login_type,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const stateFile = authStateFile(auditSession.sessionMetadata);
|
||||||
|
await rm(stateFile, { force: true });
|
||||||
|
|
||||||
const prompt = await loadPrompt(
|
const prompt = await loadPrompt(
|
||||||
AGENT_NAME,
|
AGENT_NAME,
|
||||||
{ webUrl, repoPath },
|
{ webUrl, repoPath, AUTH_STATE_FILE: stateFile },
|
||||||
distributedConfig,
|
distributedConfig,
|
||||||
pipelineTestingMode ?? false,
|
pipelineTestingMode ?? false,
|
||||||
logger,
|
logger,
|
||||||
@@ -120,7 +125,14 @@ export async function validateAuthentication(input: ValidateAuthInput): Promise<
|
|||||||
providerConfig,
|
providerConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
const classification = classifyResult(result, authentication);
|
let classification = classifyResult(result, authentication);
|
||||||
|
|
||||||
|
if (classification.ok) {
|
||||||
|
const sessionCheck = await verifySavedAuthState(stateFile, logger);
|
||||||
|
if (!sessionCheck.ok) {
|
||||||
|
classification = sessionCheck;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const endResult: AgentEndResult = {
|
const endResult: AgentEndResult = {
|
||||||
attemptNumber,
|
attemptNumber,
|
||||||
@@ -135,6 +147,62 @@ export async function validateAuthentication(input: ValidateAuthInput): Promise<
|
|||||||
return classification;
|
return classification;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function verifySavedAuthState(stateFile: string, logger: ActivityLogger): Promise<Result<void, PentestError>> {
|
||||||
|
let contents: string;
|
||||||
|
try {
|
||||||
|
contents = await readFile(stateFile, 'utf8');
|
||||||
|
} catch {
|
||||||
|
return err(
|
||||||
|
new PentestError(
|
||||||
|
`Preflight reported login success but did not save the authenticated session to ${stateFile}.`,
|
||||||
|
'validation',
|
||||||
|
true,
|
||||||
|
{ stateFile },
|
||||||
|
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(contents);
|
||||||
|
} catch (parseErr) {
|
||||||
|
const detail = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
||||||
|
return err(
|
||||||
|
new PentestError(
|
||||||
|
`Preflight saved an authenticated session to ${stateFile}, but the file is not valid JSON: ${detail}`,
|
||||||
|
'validation',
|
||||||
|
true,
|
||||||
|
{ stateFile, parseError: detail },
|
||||||
|
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookieCount = countStorageEntries(parsed, 'cookies');
|
||||||
|
const originCount = countStorageEntries(parsed, 'origins');
|
||||||
|
if (cookieCount === 0 && originCount === 0) {
|
||||||
|
return err(
|
||||||
|
new PentestError(
|
||||||
|
`Preflight saved an authenticated session to ${stateFile}, but it contains no cookies or origins — the browser was not actually logged in.`,
|
||||||
|
'validation',
|
||||||
|
true,
|
||||||
|
{ stateFile, cookieCount, originCount },
|
||||||
|
ErrorCode.AGENT_EXECUTION_FAILED,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Preflight authenticated session saved', { stateFile, cookieCount, originCount });
|
||||||
|
return ok(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countStorageEntries(parsed: unknown, key: 'cookies' | 'origins'): number {
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) return 0;
|
||||||
|
const value = (parsed as Record<string, unknown>)[key];
|
||||||
|
return Array.isArray(value) ? value.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
function classifyResult(
|
function classifyResult(
|
||||||
result: import('../ai/claude-executor.js').ClaudePromptResult,
|
result: import('../ai/claude-executor.js').ClaudePromptResult,
|
||||||
authentication: NonNullable<DistributedConfig['authentication']>,
|
authentication: NonNullable<DistributedConfig['authentication']>,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { writePlaywrightStealthConfig } from '../ai/playwright-config-writer.js'
|
|||||||
import { writeUserSettingsForCodePathAvoids } from '../ai/settings-writer.js';
|
import { writeUserSettingsForCodePathAvoids } from '../ai/settings-writer.js';
|
||||||
import { AuditSession } from '../audit/index.js';
|
import { AuditSession } from '../audit/index.js';
|
||||||
import type { ResumeAttempt } from '../audit/metrics-tracker.js';
|
import type { ResumeAttempt } from '../audit/metrics-tracker.js';
|
||||||
import { generateSessionJsonPath, type SessionMetadata } from '../audit/utils.js';
|
import { authStateFile, generateSessionJsonPath, type SessionMetadata } from '../audit/utils.js';
|
||||||
import type { WorkflowSummary } from '../audit/workflow-logger.js';
|
import type { WorkflowSummary } from '../audit/workflow-logger.js';
|
||||||
import type { CheckpointContext } from '../interfaces/checkpoint-provider.js';
|
import type { CheckpointContext } from '../interfaces/checkpoint-provider.js';
|
||||||
import { DEFAULT_DELIVERABLES_SUBDIR, deliverablesDir } from '../paths.js';
|
import { DEFAULT_DELIVERABLES_SUBDIR, deliverablesDir } from '../paths.js';
|
||||||
@@ -926,7 +926,15 @@ export async function logWorkflowComplete(input: ActivityInput, summary: Workflo
|
|||||||
// 5. Write completion entry to workflow.log
|
// 5. Write completion entry to workflow.log
|
||||||
await auditSession.logWorkflowComplete(cumulativeSummary);
|
await auditSession.logWorkflowComplete(cumulativeSummary);
|
||||||
|
|
||||||
// 6. Clean up container
|
// 6. Drop the authenticated browser session
|
||||||
|
try {
|
||||||
|
await fs.rm(authStateFile(sessionMetadata), { force: true });
|
||||||
|
} catch (error) {
|
||||||
|
const detail = error instanceof Error ? error.message : String(error);
|
||||||
|
console.warn(`Failed to clean up auth-state.json: ${detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Clean up container
|
||||||
removeContainer(workflowId);
|
removeContainer(workflowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+4
-4
@@ -1010,8 +1010,8 @@ packages:
|
|||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
fast-uri@3.1.0:
|
fast-uri@3.1.2:
|
||||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
@@ -2339,7 +2339,7 @@ snapshots:
|
|||||||
ajv@8.18.0:
|
ajv@8.18.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
fast-uri: 3.1.0
|
fast-uri: 3.1.2
|
||||||
json-schema-traverse: 1.0.0
|
json-schema-traverse: 1.0.0
|
||||||
require-from-string: 2.0.2
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
@@ -2560,7 +2560,7 @@ snapshots:
|
|||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
fast-uri@3.1.0: {}
|
fast-uri@3.1.2: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.4):
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user