mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-07-01 11:05:36 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a2f78c5d9 | |||
| 7abcc1d3e1 |
@@ -4,7 +4,7 @@ AI-powered penetration testing agent for defensive security analysis. Automates
|
|||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
**Prerequisites:** Docker, AI provider credentials (`.env` for local, `shn setup` or env vars for npx)
|
**Prerequisites:** Docker, AI provider credentials (`.env` for local, `npx @keygraph/shannon setup` or env vars for npx)
|
||||||
|
|
||||||
### Dual CLI
|
### Dual CLI
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@ Shannon supports two CLI modes, auto-detected based on the current working direc
|
|||||||
| **Install** | Zero-install via npm | Clone the repo |
|
| **Install** | Zero-install via npm | Clone the repo |
|
||||||
| **Image** | Pulled from Docker Hub (`keygraph/shannon:latest`) | Built locally (`shannon-worker`) |
|
| **Image** | Pulled from Docker Hub (`keygraph/shannon:latest`) | Built locally (`shannon-worker`) |
|
||||||
| **State** | `~/.shannon/` | Project directory |
|
| **State** | `~/.shannon/` | Project directory |
|
||||||
| **Credentials** | `~/.shannon/config.toml` (via `shn setup`) or env vars | `./.env` |
|
| **Credentials** | `~/.shannon/config.toml` (via `npx @keygraph/shannon setup`) or env vars | `./.env` |
|
||||||
| **Config** | `~/.shannon/config.toml` (via `shn setup`) | N/A |
|
| **Config** | `~/.shannon/config.toml` (via `npx @keygraph/shannon setup`) | N/A |
|
||||||
| **Prompts** | Bundled in Docker image | Mounted from `./apps/worker/prompts/` (live-editable) |
|
| **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.
|
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.
|
||||||
@@ -145,7 +145,7 @@ Durable workflow orchestration with crash recovery, queryable progress, intellig
|
|||||||
5. **Reporting** (`report`) — Executive-level security report
|
5. **Reporting** (`report`) — Executive-level security report
|
||||||
|
|
||||||
### 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 `npx @keygraph/shannon 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/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
|
- **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
|
- **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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* `shn setup` — interactive TUI wizard for one-time credential configuration.
|
* `npx @keygraph/shannon setup` — interactive TUI wizard for one-time credential configuration.
|
||||||
*
|
*
|
||||||
* Walks the user through selecting a provider and entering credentials,
|
* Walks the user through selecting a provider and entering credentials,
|
||||||
* then persists everything to ~/.shannon/config.toml with 0o600 permissions.
|
* then persists everything to ~/.shannon/config.toml with 0o600 permissions.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* `shn uninstall` command — remove ~/.shannon/ after confirmation (npx only).
|
* `npx @keygraph/shannon uninstall` command — remove ~/.shannon/ after confirmation (npx only).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export function resolveConfig(): void {
|
|||||||
for (const err of errors) {
|
for (const err of errors) {
|
||||||
console.error(` - ${err}`);
|
console.error(` - ${err}`);
|
||||||
}
|
}
|
||||||
console.error(`\nRun 'shn setup' to reconfigure.\n`);
|
console.error(`\nRun 'npx @keygraph/shannon setup' to reconfigure.\n`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
* - Spending cap check using isSpendingCapBehavior
|
* - Spending cap check using isSpendingCapBehavior
|
||||||
* - Handle failure (rollback, audit)
|
* - Handle failure (rollback, audit)
|
||||||
* - Validate output using AGENTS[agentName].deliverableFilename
|
* - Validate output using AGENTS[agentName].deliverableFilename
|
||||||
|
* - Render the deliverable to disk via the writeDeliverable hook (if provided)
|
||||||
* - Commit on success, log metrics
|
* - Commit on success, log metrics
|
||||||
*
|
*
|
||||||
* No Temporal dependencies - pure domain logic.
|
* No Temporal dependencies - pure domain logic.
|
||||||
@@ -55,6 +56,8 @@ export interface AgentExecutionInput {
|
|||||||
promptDir?: string | undefined;
|
promptDir?: string | undefined;
|
||||||
providerConfig?: import('../types/config.js').ProviderConfig | undefined;
|
providerConfig?: import('../types/config.js').ProviderConfig | undefined;
|
||||||
mcpServers?: Record<string, import('@anthropic-ai/claude-agent-sdk').McpServerConfig>;
|
mcpServers?: Record<string, import('@anthropic-ai/claude-agent-sdk').McpServerConfig>;
|
||||||
|
// Renders the deliverable to disk; invoked after validation, before the success commit.
|
||||||
|
writeDeliverable?: (deliverablesPath: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FailAgentOpts {
|
interface FailAgentOpts {
|
||||||
@@ -110,6 +113,7 @@ export class AgentExecutionService {
|
|||||||
promptDir,
|
promptDir,
|
||||||
providerConfig,
|
providerConfig,
|
||||||
mcpServers,
|
mcpServers,
|
||||||
|
writeDeliverable,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
// 1. Load config (pre-parsed configData → raw YAML → file path)
|
// 1. Load config (pre-parsed configData → raw YAML → file path)
|
||||||
@@ -236,7 +240,12 @@ export class AgentExecutionService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Success - commit deliverables, then capture checkpoint hash
|
// 10. Render the deliverable to disk so the success commit below stages it
|
||||||
|
if (writeDeliverable) {
|
||||||
|
await writeDeliverable(deliverablesPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. Success - commit deliverables, then capture checkpoint hash
|
||||||
await commitGitSuccess(deliverablesPath, agentName, logger);
|
await commitGitSuccess(deliverablesPath, agentName, logger);
|
||||||
const commitHash = await getGitCommitHash(deliverablesPath);
|
const commitHash = await getGitCommitHash(deliverablesPath);
|
||||||
|
|
||||||
|
|||||||
@@ -127,12 +127,11 @@ export const AGENT_PHASE_MAP: Readonly<Record<AgentName, PhaseName>> = Object.fr
|
|||||||
|
|
||||||
// Factory function for vulnerability queue validators.
|
// Factory function for vulnerability queue validators.
|
||||||
//
|
//
|
||||||
// Post-MCP-migration, the analysis_deliverable.md is rendered by the activity
|
// The analysis_deliverable.md is rendered via the writeDeliverable hook, which
|
||||||
// wrapper after validateAgentOutput runs, so the previous "both files exist"
|
// AgentExecutionService runs after validateAgentOutput but before the success
|
||||||
// check would race the renderer. The validator only checks the queue.json —
|
// commit — so a "both files exist" check here would race the renderer. The
|
||||||
// that file is written by the SDK structured-output path in agent-execution.ts
|
// validator only checks queue.json, written by the SDK structured-output path
|
||||||
// before this validator runs. The downstream checkExploitationQueue still
|
// in agent-execution.ts before this validator runs.
|
||||||
// renders the .md.
|
|
||||||
function createVulnValidator(vulnType: VulnType): AgentValidator {
|
function createVulnValidator(vulnType: VulnType): AgentValidator {
|
||||||
return async (sourceDir: string, logger: ActivityLogger): Promise<boolean> => {
|
return async (sourceDir: string, logger: ActivityLogger): Promise<boolean> => {
|
||||||
const queueFile = path.join(sourceDir, `${vulnType}_exploitation_queue.json`);
|
const queueFile = path.join(sourceDir, `${vulnType}_exploitation_queue.json`);
|
||||||
@@ -145,9 +144,9 @@ function createVulnValidator(vulnType: VulnType): AgentValidator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exploitation agents — validation lives in runExploitAgentWithCollector post-processing
|
// Exploitation agents — the evidence deliverable is rendered via the writeDeliverable
|
||||||
// (collector harvest + renderer write). The deliverable file is written by the renderer
|
// hook after the agent succeeds (before the success commit), so a file-existence check
|
||||||
// after the agent succeeds, so a file-existence check here would race the renderer.
|
// here would race the renderer.
|
||||||
//
|
//
|
||||||
// VulnType is kept in the import surface for createVulnValidator above; this factory
|
// VulnType is kept in the import surface for createVulnValidator above; this factory
|
||||||
// returns a no-op validator parameterized only for symmetry with the vuln-side factory.
|
// returns a no-op validator parameterized only for symmetry with the vuln-side factory.
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ async function runAgentActivity(
|
|||||||
agentName: AgentName,
|
agentName: AgentName,
|
||||||
input: ActivityInput,
|
input: ActivityInput,
|
||||||
mcpServers?: Record<string, import('@anthropic-ai/claude-agent-sdk').McpServerConfig>,
|
mcpServers?: Record<string, import('@anthropic-ai/claude-agent-sdk').McpServerConfig>,
|
||||||
|
writeDeliverable?: (deliverablesPath: string) => Promise<void>,
|
||||||
): Promise<AgentMetrics> {
|
): Promise<AgentMetrics> {
|
||||||
const { repoPath, configPath, pipelineTestingMode = false, workflowId, webUrl } = input;
|
const { repoPath, configPath, pipelineTestingMode = false, workflowId, webUrl } = input;
|
||||||
|
|
||||||
@@ -192,6 +193,7 @@ async function runAgentActivity(
|
|||||||
...(input.promptDir !== undefined && { promptDir: input.promptDir }),
|
...(input.promptDir !== undefined && { promptDir: input.promptDir }),
|
||||||
...(input.configYAML !== undefined && { configYAML: input.configYAML }),
|
...(input.configYAML !== undefined && { configYAML: input.configYAML }),
|
||||||
...(mcpServers && { mcpServers }),
|
...(mcpServers && { mcpServers }),
|
||||||
|
...(writeDeliverable && { writeDeliverable }),
|
||||||
},
|
},
|
||||||
auditSession,
|
auditSession,
|
||||||
logger,
|
logger,
|
||||||
@@ -255,28 +257,21 @@ export async function runPreReconAgent(input: ActivityInput): Promise<AgentMetri
|
|||||||
const { renderPreRecon } = await import('../services/pre-recon-renderer.js');
|
const { renderPreRecon } = await import('../services/pre-recon-renderer.js');
|
||||||
|
|
||||||
const collector = createPreReconCollectorServer();
|
const collector = createPreReconCollectorServer();
|
||||||
const metrics = await runAgentActivity('pre-recon', input, { 'pre-recon-collector': collector.server });
|
|
||||||
|
|
||||||
// On resume, the agent is skipped and the collector is never populated.
|
const writeDeliverable = async (deliverablesPath: string): Promise<void> => {
|
||||||
// The cached deliverable from the prior run is the source of truth.
|
const logger = createActivityLogger();
|
||||||
if (metrics.skipped) {
|
// Skipped tools surface as renderer placeholders, not as activity failures.
|
||||||
return metrics;
|
const callStatus = collector.getCallStatus();
|
||||||
}
|
logger.info('Pre-recon tool call status', { callStatus });
|
||||||
|
|
||||||
const logger = createActivityLogger();
|
const collected = collector.getAll();
|
||||||
const dir = deliverablesDir(input.repoPath, input.deliverablesSubdir);
|
const markdown = renderPreRecon(collected);
|
||||||
|
const mdPath = path.join(deliverablesPath, 'pre_recon_deliverable.md');
|
||||||
|
await atomicWrite(mdPath, markdown);
|
||||||
|
logger.info(`Wrote pre_recon_deliverable.md from structured data (${markdown.length} bytes)`);
|
||||||
|
};
|
||||||
|
|
||||||
// Skipped tools surface as renderer placeholders, not as activity failures.
|
return runAgentActivity('pre-recon', input, { 'pre-recon-collector': collector.server }, writeDeliverable);
|
||||||
const callStatus = collector.getCallStatus();
|
|
||||||
logger.info('Pre-recon tool call status', { callStatus });
|
|
||||||
|
|
||||||
const collected = collector.getAll();
|
|
||||||
const markdown = renderPreRecon(collected);
|
|
||||||
const mdPath = path.join(dir, 'pre_recon_deliverable.md');
|
|
||||||
await atomicWrite(mdPath, markdown);
|
|
||||||
logger.info(`Wrote pre_recon_deliverable.md from structured data (${markdown.length} bytes)`);
|
|
||||||
|
|
||||||
return metrics;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runReconAgent(input: ActivityInput): Promise<AgentMetrics> {
|
export async function runReconAgent(input: ActivityInput): Promise<AgentMetrics> {
|
||||||
@@ -284,28 +279,21 @@ export async function runReconAgent(input: ActivityInput): Promise<AgentMetrics>
|
|||||||
const { renderRecon } = await import('../services/recon-renderer.js');
|
const { renderRecon } = await import('../services/recon-renderer.js');
|
||||||
|
|
||||||
const collector = createReconCollectorServer();
|
const collector = createReconCollectorServer();
|
||||||
const metrics = await runAgentActivity('recon', input, { 'recon-collector': collector.server });
|
|
||||||
|
|
||||||
// On resume, the agent is skipped and the collector is never populated.
|
const writeDeliverable = async (deliverablesPath: string): Promise<void> => {
|
||||||
// The cached deliverable from the prior run is the source of truth.
|
const logger = createActivityLogger();
|
||||||
if (metrics.skipped) {
|
// Skipped tools surface as renderer placeholders, not as activity failures.
|
||||||
return metrics;
|
const callStatus = collector.getCallStatus();
|
||||||
}
|
logger.info('Recon tool call status', { callStatus });
|
||||||
|
|
||||||
const logger = createActivityLogger();
|
const collected = collector.getAll();
|
||||||
const dir = deliverablesDir(input.repoPath, input.deliverablesSubdir);
|
const markdown = renderRecon(collected);
|
||||||
|
const mdPath = path.join(deliverablesPath, 'recon_deliverable.md');
|
||||||
|
await atomicWrite(mdPath, markdown);
|
||||||
|
logger.info(`Wrote recon_deliverable.md from structured data (${markdown.length} bytes)`);
|
||||||
|
};
|
||||||
|
|
||||||
// Skipped tools surface as renderer placeholders, not as activity failures.
|
return runAgentActivity('recon', input, { 'recon-collector': collector.server }, writeDeliverable);
|
||||||
const callStatus = collector.getCallStatus();
|
|
||||||
logger.info('Recon tool call status', { callStatus });
|
|
||||||
|
|
||||||
const collected = collector.getAll();
|
|
||||||
const markdown = renderRecon(collected);
|
|
||||||
const mdPath = path.join(dir, 'recon_deliverable.md');
|
|
||||||
await atomicWrite(mdPath, markdown);
|
|
||||||
logger.info(`Wrote recon_deliverable.md from structured data (${markdown.length} bytes)`);
|
|
||||||
|
|
||||||
return metrics;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runVulnAgentWithCollector(
|
async function runVulnAgentWithCollector(
|
||||||
@@ -317,28 +305,21 @@ async function runVulnAgentWithCollector(
|
|||||||
const { renderVulnDeliverable } = await import('../services/vuln-renderer.js');
|
const { renderVulnDeliverable } = await import('../services/vuln-renderer.js');
|
||||||
|
|
||||||
const collector = createVulnCollector(vulnClass);
|
const collector = createVulnCollector(vulnClass);
|
||||||
const metrics = await runAgentActivity(agentName, input, { 'vuln-collector': collector.server });
|
|
||||||
|
|
||||||
// On resume, the agent is skipped and the collector is never populated.
|
const writeDeliverable = async (deliverablesPath: string): Promise<void> => {
|
||||||
// The cached deliverable from the prior run is the source of truth.
|
const logger = createActivityLogger();
|
||||||
if (metrics.skipped) {
|
// Skipped tools surface as renderer placeholders, not as activity failures.
|
||||||
return metrics;
|
const callStatus = collector.getCallStatus();
|
||||||
}
|
logger.info(`${vulnClass} vuln tool call status`, { callStatus });
|
||||||
|
|
||||||
const logger = createActivityLogger();
|
const collected = collector.getAll();
|
||||||
const dir = deliverablesDir(input.repoPath, input.deliverablesSubdir);
|
const markdown = renderVulnDeliverable(vulnClass, collected);
|
||||||
|
const mdPath = path.join(deliverablesPath, `${vulnClass}_analysis_deliverable.md`);
|
||||||
|
await atomicWrite(mdPath, markdown);
|
||||||
|
logger.info(`Wrote ${vulnClass}_analysis_deliverable.md from structured data (${markdown.length} bytes)`);
|
||||||
|
};
|
||||||
|
|
||||||
// Skipped tools surface as renderer placeholders, not as activity failures.
|
return runAgentActivity(agentName, input, { 'vuln-collector': collector.server }, writeDeliverable);
|
||||||
const callStatus = collector.getCallStatus();
|
|
||||||
logger.info(`${vulnClass} vuln tool call status`, { callStatus });
|
|
||||||
|
|
||||||
const collected = collector.getAll();
|
|
||||||
const markdown = renderVulnDeliverable(vulnClass, collected);
|
|
||||||
const mdPath = path.join(dir, `${vulnClass}_analysis_deliverable.md`);
|
|
||||||
await atomicWrite(mdPath, markdown);
|
|
||||||
logger.info(`Wrote ${vulnClass}_analysis_deliverable.md from structured data (${markdown.length} bytes)`);
|
|
||||||
|
|
||||||
return metrics;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runInjectionVulnAgent(input: ActivityInput): Promise<AgentMetrics> {
|
export async function runInjectionVulnAgent(input: ActivityInput): Promise<AgentMetrics> {
|
||||||
@@ -398,34 +379,29 @@ async function runExploitAgentWithCollector(
|
|||||||
const { validIds, idToType } = await readExploitQueue(queuePath);
|
const { validIds, idToType } = await readExploitQueue(queuePath);
|
||||||
|
|
||||||
const collector = createExploitCollector({ vulnClass, validIds });
|
const collector = createExploitCollector({ vulnClass, validIds });
|
||||||
const metrics = await runAgentActivity(agentName, input, { 'exploit-collector': collector.server });
|
|
||||||
|
|
||||||
// On resume, the agent is skipped and the collector is never populated.
|
const writeDeliverable = async (deliverablesPath: string): Promise<void> => {
|
||||||
// The cached deliverable from the prior run is the source of truth.
|
const logger = createActivityLogger();
|
||||||
if (metrics.skipped) {
|
const collected = collector.getAll();
|
||||||
return metrics;
|
const emittedIds = new Set(collected.map((e) => e.vulnerability_id));
|
||||||
}
|
const missingIds = [...validIds].filter((id) => !emittedIds.has(id));
|
||||||
|
const exploitedCount = collected.filter((e) => e.status === 'exploited').length;
|
||||||
|
const blockedCount = collected.filter((e) => e.status === 'blocked').length;
|
||||||
|
|
||||||
const logger = createActivityLogger();
|
logger.info(`${vulnClass} exploit tool call metrics`, {
|
||||||
const collected = collector.getAll();
|
queueSize: validIds.size,
|
||||||
const emittedIds = new Set(collected.map((e) => e.vulnerability_id));
|
exploited: exploitedCount,
|
||||||
const missingIds = [...validIds].filter((id) => !emittedIds.has(id));
|
blocked: blockedCount,
|
||||||
const exploitedCount = collected.filter((e) => e.status === 'exploited').length;
|
missing: missingIds.length,
|
||||||
const blockedCount = collected.filter((e) => e.status === 'blocked').length;
|
});
|
||||||
|
|
||||||
logger.info(`${vulnClass} exploit tool call metrics`, {
|
const markdown = renderExploitDeliverable(vulnClass, collected, idToType);
|
||||||
queueSize: validIds.size,
|
const mdPath = path.join(deliverablesPath, `${vulnClass}_exploitation_evidence.md`);
|
||||||
exploited: exploitedCount,
|
await atomicWrite(mdPath, markdown);
|
||||||
blocked: blockedCount,
|
logger.info(`Wrote ${vulnClass}_exploitation_evidence.md from structured data (${markdown.length} bytes)`);
|
||||||
missing: missingIds.length,
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const markdown = renderExploitDeliverable(vulnClass, collected, idToType);
|
return runAgentActivity(agentName, input, { 'exploit-collector': collector.server }, writeDeliverable);
|
||||||
const mdPath = path.join(dir, `${vulnClass}_exploitation_evidence.md`);
|
|
||||||
await atomicWrite(mdPath, markdown);
|
|
||||||
logger.info(`Wrote ${vulnClass}_exploitation_evidence.md from structured data (${markdown.length} bytes)`);
|
|
||||||
|
|
||||||
return metrics;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runInjectionExploitAgent(input: ActivityInput): Promise<AgentMetrics> {
|
export async function runInjectionExploitAgent(input: ActivityInput): Promise<AgentMetrics> {
|
||||||
|
|||||||
Reference in New Issue
Block a user