mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-09-30 21:39:46 +02:00
feat: add config-driven run scoping and report filtering (#326)
* feat(steerability): add config-driven profile with code_path avoid enforcement * fix(steerability): write SDK deny rules once per workflow to avoid parallel-agent race * fix(steerability): reference guidance by pointer in report DROP rules * fix(steerability): tighten code_path avoid enforcement * chore(steerability): use shared ALL_VULN_CLASSES const and tighten RunScope type * fix(steerability): validate run scope before resume short-circuit * fix(steerability): emit only documented Read/Edit deny rules for code_path * fix(steerability): assemble report from analysis deliverables when exploit is disabled * feat(steerability): preflight check that code_path rules match at least one repo entry * fix(steerability): tag missing code_path entries with avoid/focus kind * revert(steerability): assemble report from analysis deliverables when exploit is disabled * feat(steerability): render per-class findings from queue JSON when exploit is disabled * refactor(steerability): trim findings renderer to common mappable rows * feat(steerability): allow report agent to rewrite category-label finding titles * docs(steerability): document new config fields in README and CLAUDE.md * docs(steerability): comment out optional config sections in examples
This commit is contained in:
1 parent
6c8135d031
commit
95998d1a44
32 files changed
+1162
-181
No files matched your search
@@ -33,6 +33,7 @@ import {
|
||||
} from '@temporalio/workflow';
|
||||
import type { AgentName, VulnType } from '../types/agents.js';
|
||||
import { ALL_AGENTS } from '../types/agents.js';
|
||||
import { ALL_VULN_CLASSES, type VulnClass } from '../types/config.js';
|
||||
import type * as activities from './activities.js';
|
||||
import type { ActivityInput } from './activities.js';
|
||||
import {
|
||||
@@ -48,6 +49,19 @@ import {
|
||||
import { toWorkflowSummary } from './summary-mapper.js';
|
||||
import { classifyErrorCode, formatWorkflowError } from './workflow-errors.js';
|
||||
|
||||
/** Agents this run is expected to produce — drives the resume short-circuit. */
|
||||
function computeExpectedAgents(vulnClasses: readonly VulnClass[], exploit: boolean): string[] {
|
||||
const expected: string[] = ['pre-recon', 'recon'];
|
||||
for (const cls of vulnClasses) {
|
||||
expected.push(`${cls}-vuln`);
|
||||
if (exploit) {
|
||||
expected.push(`${cls}-exploit`);
|
||||
}
|
||||
}
|
||||
expected.push('report');
|
||||
return expected;
|
||||
}
|
||||
|
||||
// Retry configuration for production (long intervals for billing recovery)
|
||||
const PRODUCTION_RETRY = {
|
||||
initialInterval: '5 minutes',
|
||||
@@ -215,22 +229,42 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
...(input.providerConfig !== undefined && { providerConfig: input.providerConfig }),
|
||||
};
|
||||
|
||||
const selectedVulnClasses: readonly VulnClass[] =
|
||||
input.vulnClasses && input.vulnClasses.length > 0 ? input.vulnClasses : ALL_VULN_CLASSES;
|
||||
const selectedClassSet = new Set<VulnClass>(selectedVulnClasses);
|
||||
const exploit: boolean = input.exploit ?? true;
|
||||
const expectedAgents = computeExpectedAgents(selectedVulnClasses, exploit);
|
||||
|
||||
await a.persistOrValidateRunScope(activityInput, [...selectedVulnClasses], exploit);
|
||||
|
||||
let resumeState: ResumeState | null = null;
|
||||
|
||||
if (input.resumeFromWorkspace) {
|
||||
// 1. Load resume state (validates workspace, cross-checks deliverables)
|
||||
resumeState = await a.loadResumeState(input.resumeFromWorkspace, input.webUrl, input.repoPath, input.deliverablesSubdir);
|
||||
resumeState = await a.loadResumeState(
|
||||
input.resumeFromWorkspace,
|
||||
input.webUrl,
|
||||
input.repoPath,
|
||||
input.deliverablesSubdir,
|
||||
);
|
||||
|
||||
// 2. Restore git workspace and clean up incomplete deliverables
|
||||
const incompleteAgents = ALL_AGENTS.filter(
|
||||
(agentName) => !resumeState?.completedAgents.includes(agentName),
|
||||
) as AgentName[];
|
||||
|
||||
await a.restoreGitCheckpoint(input.repoPath, resumeState.checkpointHash, incompleteAgents, input.deliverablesSubdir);
|
||||
await a.restoreGitCheckpoint(
|
||||
input.repoPath,
|
||||
resumeState.checkpointHash,
|
||||
incompleteAgents,
|
||||
input.deliverablesSubdir,
|
||||
);
|
||||
|
||||
// 3. Short-circuit if all agents already completed
|
||||
if (resumeState.completedAgents.length === ALL_AGENTS.length) {
|
||||
log.info(`All ${ALL_AGENTS.length} agents already completed. Nothing to resume.`);
|
||||
// 3. Short-circuit when every agent expected by this run is done.
|
||||
// Uses dynamic expectedAgents (not ALL_AGENTS) so a class-scoped run completes sooner.
|
||||
const allExpectedDone = expectedAgents.every((a) => resumeState?.completedAgents.includes(a));
|
||||
if (allExpectedDone) {
|
||||
log.info(`All ${expectedAgents.length} expected agents already completed. Nothing to resume.`);
|
||||
state.status = 'completed';
|
||||
state.completedAgents = [...resumeState.completedAgents];
|
||||
state.summary = computeSummary(state);
|
||||
@@ -389,6 +423,11 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
// === Initialize Deliverables Git ===
|
||||
await a.initDeliverableGit(activityInput);
|
||||
|
||||
// === Sync SDK deny rules ===
|
||||
await a.syncCodePathDenyRules(activityInput);
|
||||
|
||||
log.info(`Run scope: vuln_classes=[${selectedVulnClasses.join(', ')}] exploit=${exploit}`);
|
||||
|
||||
// === Phase 1: Pre-Reconnaissance ===
|
||||
await runSequentialPhase('pre-recon', 'pre-recon', a.runPreReconAgent);
|
||||
|
||||
@@ -432,19 +471,17 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
// 2. Check exploitation queue for actionable findings
|
||||
const decision = await a.checkExploitationQueue(activityInput, vulnType);
|
||||
|
||||
// 3. Conditionally run exploitation agent
|
||||
// 3. Previously-completed exploits are preserved regardless of mode; new exploits gated by mode.
|
||||
let exploitMetrics: AgentMetrics | null = null;
|
||||
if (decision.shouldExploit) {
|
||||
if (!shouldSkip(exploitAgentName)) {
|
||||
exploitMetrics = await runExploitAgent();
|
||||
state.agentMetrics[exploitAgentName] = exploitMetrics;
|
||||
state.completedAgents.push(exploitAgentName);
|
||||
if (input.checkpointsEnabled) {
|
||||
await a.saveCheckpoint(activityInput, exploitAgentName, 'exploitation', state);
|
||||
}
|
||||
} else {
|
||||
log.info(`Skipping ${exploitAgentName} (already complete)`);
|
||||
state.completedAgents.push(exploitAgentName);
|
||||
if (shouldSkip(exploitAgentName)) {
|
||||
log.info(`Skipping ${exploitAgentName} (already complete)`);
|
||||
state.completedAgents.push(exploitAgentName);
|
||||
} else if (decision.shouldExploit && exploit) {
|
||||
exploitMetrics = await runExploitAgent();
|
||||
state.agentMetrics[exploitAgentName] = exploitMetrics;
|
||||
state.completedAgents.push(exploitAgentName);
|
||||
if (input.checkpointsEnabled) {
|
||||
await a.saveCheckpoint(activityInput, exploitAgentName, 'exploitation', state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,6 +503,11 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
const pipelineThunks: Array<() => Promise<VulnExploitPipelineResult>> = [];
|
||||
|
||||
for (const config of pipelineConfigs) {
|
||||
// Excluded classes drop entirely; any prior deliverables stay on disk but don't count this run.
|
||||
if (!selectedClassSet.has(config.vulnType)) {
|
||||
log.info(`Skipping ${config.vulnType} pipeline (class not selected this run)`);
|
||||
continue;
|
||||
}
|
||||
if (!shouldSkip(config.vulnAgent) || !shouldSkip(config.exploitAgent)) {
|
||||
pipelineThunks.push(() => runVulnExploitPipeline(config.vulnType, config.runVuln, config.runExploit));
|
||||
} else {
|
||||
@@ -487,8 +529,8 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
state.currentAgent = 'report';
|
||||
await a.logPhaseTransition(activityInput, 'reporting', 'start');
|
||||
|
||||
// First, assemble the concatenated report from exploitation evidence files
|
||||
await a.assembleReportActivity(activityInput);
|
||||
// First, assemble the concatenated report from per-class deliverables
|
||||
await a.assembleReportActivity(activityInput, exploit);
|
||||
|
||||
// Then run the report agent to add executive summary and clean up
|
||||
state.agentMetrics.report = await a.runReportAgent(activityInput);
|
||||
|
||||
Reference in new issue
Block a user