mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-12 02:47:08 +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
@@ -18,26 +18,28 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { ApplicationFailure, Context, heartbeat } from '@temporalio/activity';
|
||||
import { writeUserSettingsForCodePathAvoids } from '../ai/settings-writer.js';
|
||||
import { AuditSession } from '../audit/index.js';
|
||||
import type { ResumeAttempt } from '../audit/metrics-tracker.js';
|
||||
import type { SessionMetadata } from '../audit/utils.js';
|
||||
import { generateSessionJsonPath, type SessionMetadata } from '../audit/utils.js';
|
||||
import type { WorkflowSummary } from '../audit/workflow-logger.js';
|
||||
import type { ContainerConfig, ProviderConfig } from '../types/config.js';
|
||||
import type { CheckpointContext } from '../interfaces/checkpoint-provider.js';
|
||||
import { DEFAULT_DELIVERABLES_SUBDIR, deliverablesDir } from '../paths.js';
|
||||
import { getContainer, getOrCreateContainer, removeContainer } from '../services/container.js';
|
||||
import { classifyErrorForTemporal, PentestError } from '../services/error-handling.js';
|
||||
import { ExploitationCheckerService } from '../services/exploitation-checker.js';
|
||||
import { executeGitCommandWithRetry } from '../services/git-manager.js';
|
||||
import { runPreflightChecks } from '../services/preflight.js';
|
||||
import type { ExploitationDecision, VulnType } from '../services/queue-validation.js';
|
||||
import { renderFindingsFromQueues } from '../services/findings-renderer.js';
|
||||
import { assembleFinalReport, injectModelIntoReport } from '../services/reporting.js';
|
||||
import { AGENTS } from '../session-manager.js';
|
||||
import type { AgentName } from '../types/agents.js';
|
||||
import { ALL_AGENTS } from '../types/agents.js';
|
||||
import type { ContainerConfig, ProviderConfig, VulnClass } from '../types/config.js';
|
||||
import { ErrorCode } from '../types/errors.js';
|
||||
import { isErr } from '../types/result.js';
|
||||
import { DEFAULT_DELIVERABLES_SUBDIR, deliverablesDir } from '../paths.js';
|
||||
import { fileExists, readJson } from '../utils/file-io.js';
|
||||
import { atomicWrite, fileExists, readJson } from '../utils/file-io.js';
|
||||
import { createActivityLogger } from './activity-logger.js';
|
||||
import type { AgentMetrics, PipelineState, ResumeState } from './shared.js';
|
||||
|
||||
@@ -135,7 +137,8 @@ async function runAgentActivity(agentName: AgentName, input: ActivityInput): Pro
|
||||
|
||||
// Skip guard: the checkpoint provider decides whether to run the agent.
|
||||
// The default NoOp provider always returns { skip: false }.
|
||||
const skipContainer = getContainer(workflowId) ??
|
||||
const skipContainer =
|
||||
getContainer(workflowId) ??
|
||||
getOrCreateContainer(workflowId, buildSessionMetadata(input), buildContainerConfig(input));
|
||||
const decision = await skipContainer.checkpointProvider.shouldSkipAgent(
|
||||
agentName,
|
||||
@@ -321,7 +324,15 @@ export async function runPreflightValidation(input: ActivityInput): Promise<void
|
||||
const logger = createActivityLogger();
|
||||
logger.info('Running preflight validation...', { attempt: attemptNumber });
|
||||
|
||||
const result = await runPreflightChecks(input.webUrl, input.repoPath, input.configPath, logger, input.skipGitCheck, input.apiKey, input.providerConfig);
|
||||
const result = await runPreflightChecks(
|
||||
input.webUrl,
|
||||
input.repoPath,
|
||||
input.configPath,
|
||||
logger,
|
||||
input.skipGitCheck,
|
||||
input.apiKey,
|
||||
input.providerConfig,
|
||||
);
|
||||
|
||||
if (isErr(result)) {
|
||||
const classified = classifyErrorForTemporal(result.error);
|
||||
@@ -390,11 +401,50 @@ export async function initDeliverableGit(input: ActivityInput): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the final report by concatenating exploitation evidence files.
|
||||
* Sync code_path avoid rules into Claude's user-scope settings.json so the
|
||||
* SDK enforces them at the tool layer for every agent in this run.
|
||||
*
|
||||
* Runs once per workflow before any agent fires. Config is fixed for the
|
||||
* lifetime of the workflow, so writing once avoids the parallel-agent race
|
||||
* on the global ~/.claude/settings.json file.
|
||||
*/
|
||||
export async function assembleReportActivity(input: ActivityInput): Promise<void> {
|
||||
export async function syncCodePathDenyRules(input: ActivityInput): Promise<void> {
|
||||
const logger = createActivityLogger();
|
||||
const container = getOrCreateContainer(input.workflowId, buildSessionMetadata(input), buildContainerConfig(input));
|
||||
|
||||
const configResult = await container.configLoader.loadOptional(input.configPath, undefined, input.configYAML);
|
||||
if (isErr(configResult)) {
|
||||
logger.warn(`syncCodePathDenyRules: skipping (config load failed: ${configResult.error.message})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = configResult.value;
|
||||
const denyCount = (config?.avoid ?? []).filter((r) => r.type === 'code_path').length;
|
||||
await writeUserSettingsForCodePathAvoids(config);
|
||||
logger.info(`Synced code_path deny rules to user settings (${denyCount} entries)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the final report by concatenating per-class deliverables.
|
||||
*
|
||||
* Under exploit=true, each exploit agent has produced `*_exploitation_evidence.md`
|
||||
* directly. Under exploit=false, exploit agents didn't run; we deterministically
|
||||
* render `*_findings.md` from each `*_exploitation_queue.json` first, then assemble.
|
||||
*/
|
||||
export async function assembleReportActivity(input: ActivityInput, exploit: boolean): Promise<void> {
|
||||
const { repoPath, deliverablesSubdir } = input;
|
||||
const logger = createActivityLogger();
|
||||
|
||||
if (!exploit) {
|
||||
logger.info('Rendering per-class findings from analysis queues...');
|
||||
try {
|
||||
await renderFindingsFromQueues(repoPath, deliverablesSubdir, logger);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
logger.warn(`Error rendering findings from queues: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Assembling deliverables from specialist agents...');
|
||||
try {
|
||||
await assembleFinalReport(repoPath, deliverablesSubdir, logger);
|
||||
@@ -438,6 +488,11 @@ export async function checkExploitationQueue(input: ActivityInput, vulnType: Vul
|
||||
return checker.checkQueue(vulnType, delivPath, logger);
|
||||
}
|
||||
|
||||
interface RunScope {
|
||||
vulnClasses: VulnClass[];
|
||||
exploit: boolean;
|
||||
}
|
||||
|
||||
interface SessionJson {
|
||||
session: {
|
||||
id: string;
|
||||
@@ -445,6 +500,7 @@ interface SessionJson {
|
||||
repoPath?: string;
|
||||
originalWorkflowId?: string;
|
||||
resumeAttempts?: ResumeAttempt[];
|
||||
scope?: RunScope;
|
||||
};
|
||||
metrics: {
|
||||
agents: Record<
|
||||
@@ -562,6 +618,42 @@ export async function loadResumeState(
|
||||
};
|
||||
}
|
||||
|
||||
/** First run records scope into session.json; resume runs throw if it differs. */
|
||||
export async function persistOrValidateRunScope(
|
||||
input: ActivityInput,
|
||||
vulnClasses: VulnClass[],
|
||||
exploit: boolean,
|
||||
): Promise<void> {
|
||||
const sessionMetadata = buildSessionMetadata(input);
|
||||
const auditSession = new AuditSession(sessionMetadata);
|
||||
await auditSession.initialize(input.workflowId);
|
||||
|
||||
const sessionPath = generateSessionJsonPath(sessionMetadata);
|
||||
const session = await readJson<SessionJson>(sessionPath);
|
||||
|
||||
if (session.session.scope) {
|
||||
const recorded = session.session.scope;
|
||||
const sameClasses =
|
||||
recorded.vulnClasses.length === vulnClasses.length &&
|
||||
recorded.vulnClasses.every((c) => vulnClasses.includes(c)) &&
|
||||
vulnClasses.every((c) => recorded.vulnClasses.includes(c));
|
||||
|
||||
if (!sameClasses || recorded.exploit !== exploit) {
|
||||
throw ApplicationFailure.nonRetryable(
|
||||
`Resume scope mismatch for workspace ${input.sessionId}.\n` +
|
||||
` Original: vuln_classes=[${recorded.vulnClasses.join(', ')}], exploit=${recorded.exploit}\n` +
|
||||
` Provided: vuln_classes=[${vulnClasses.join(', ')}], exploit=${exploit}\n` +
|
||||
`Resume requires the same scope as the original run. Start a new workspace if you want different scope.`,
|
||||
'ScopeMismatchError',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
session.session.scope = { vulnClasses: [...vulnClasses], exploit };
|
||||
await atomicWrite(sessionPath, session);
|
||||
}
|
||||
|
||||
async function findLatestCommit(gitDir: string, commitHashes: string[]): Promise<string> {
|
||||
if (commitHashes.length === 1) {
|
||||
const hash = commitHashes[0];
|
||||
@@ -605,7 +697,7 @@ export async function restoreGitCheckpoint(
|
||||
await executeGitCommandWithRetry(
|
||||
['git', 'rev-parse', '--verify', checkpointHash],
|
||||
repoPath,
|
||||
'verify checkpoint hash exists'
|
||||
'verify checkpoint hash exists',
|
||||
);
|
||||
} catch {
|
||||
logger.info(`Checkpoint hash not found in clone, skipping git reset: ${checkpointHash}`);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineQuery } from '@temporalio/workflow';
|
||||
|
||||
export type { AgentMetrics } from '../types/metrics.js';
|
||||
|
||||
import type { DistributedConfig, PipelineConfig, ProviderConfig } from '../types/config.js';
|
||||
import type { DistributedConfig, PipelineConfig, ProviderConfig, VulnClass } from '../types/config.js';
|
||||
import type { ErrorCode } from '../types/errors.js';
|
||||
import type { AgentMetrics } from '../types/metrics.js';
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface PipelineInput {
|
||||
checkpointsEnabled?: boolean; // Enable checkpoint activities (default: false)
|
||||
skipGitCheck?: boolean; // Skip .git directory validation in preflight (e.g. when .git is removed after clone)
|
||||
providerConfig?: ProviderConfig; // LLM provider configuration (Bedrock, Vertex, etc.)
|
||||
vulnClasses?: VulnClass[]; // omitted = all five
|
||||
exploit?: boolean; // false skips the exploitation phase
|
||||
}
|
||||
|
||||
export interface ResumeState {
|
||||
|
||||
@@ -36,7 +36,7 @@ import dotenv from 'dotenv';
|
||||
import { sanitizeHostname } from '../audit/utils.js';
|
||||
import { parseConfig } from '../config-parser.js';
|
||||
import { deliverablesDir } from '../paths.js';
|
||||
import type { PipelineConfig } from '../types/config.js';
|
||||
import type { PipelineConfig, VulnClass } from '../types/config.js';
|
||||
import { fileExists, readJson } from '../utils/file-io.js';
|
||||
import * as activities from './activities.js';
|
||||
import type { PipelineInput, PipelineProgress, PipelineState } from './shared.js';
|
||||
@@ -275,30 +275,39 @@ async function resolveWorkspace(client: Client, args: CliArgs): Promise<Workspac
|
||||
|
||||
// === Pipeline Input Construction ===
|
||||
|
||||
async function loadPipelineConfig(configPath: string | undefined): Promise<PipelineConfig> {
|
||||
if (!configPath) return {};
|
||||
interface OrchestrationConfig {
|
||||
pipelineConfig: PipelineConfig;
|
||||
vulnClasses?: VulnClass[];
|
||||
exploit?: boolean;
|
||||
}
|
||||
|
||||
async function loadOrchestrationConfig(configPath: string | undefined): Promise<OrchestrationConfig> {
|
||||
if (!configPath) return { pipelineConfig: {} };
|
||||
try {
|
||||
const config = await parseConfig(configPath);
|
||||
const raw = config.pipeline;
|
||||
if (!raw) return {};
|
||||
|
||||
const result: PipelineConfig = {};
|
||||
if (raw.retry_preset !== undefined) {
|
||||
result.retry_preset = raw.retry_preset;
|
||||
const pipelineConfig: PipelineConfig = {};
|
||||
if (config.pipeline?.retry_preset !== undefined) {
|
||||
pipelineConfig.retry_preset = config.pipeline.retry_preset;
|
||||
}
|
||||
if (raw.max_concurrent_pipelines !== undefined) {
|
||||
result.max_concurrent_pipelines = Number(raw.max_concurrent_pipelines);
|
||||
if (config.pipeline?.max_concurrent_pipelines !== undefined) {
|
||||
pipelineConfig.max_concurrent_pipelines = Number(config.pipeline.max_concurrent_pipelines);
|
||||
}
|
||||
return result;
|
||||
|
||||
return {
|
||||
pipelineConfig,
|
||||
...(config.vuln_classes && config.vuln_classes.length > 0 && { vulnClasses: [...config.vuln_classes] }),
|
||||
...(config.exploit !== undefined && { exploit: config.exploit === 'true' }),
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
return { pipelineConfig: {} };
|
||||
}
|
||||
}
|
||||
|
||||
function buildPipelineInput(
|
||||
args: CliArgs,
|
||||
workspace: WorkspaceResolution,
|
||||
pipelineConfig: PipelineConfig,
|
||||
orchestration: OrchestrationConfig,
|
||||
): PipelineInput {
|
||||
return {
|
||||
webUrl: args.webUrl,
|
||||
@@ -309,7 +318,9 @@ function buildPipelineInput(
|
||||
...(args.pipelineTestingMode && { pipelineTestingMode: args.pipelineTestingMode }),
|
||||
...(workspace.isResume && args.resumeFromWorkspace && { resumeFromWorkspace: args.resumeFromWorkspace }),
|
||||
...(workspace.terminatedWorkflows.length > 0 && { terminatedWorkflows: workspace.terminatedWorkflows }),
|
||||
...(Object.keys(pipelineConfig).length > 0 && { pipelineConfig }),
|
||||
...(Object.keys(orchestration.pipelineConfig).length > 0 && { pipelineConfig: orchestration.pipelineConfig }),
|
||||
...(orchestration.vulnClasses && { vulnClasses: orchestration.vulnClasses }),
|
||||
...(orchestration.exploit !== undefined && { exploit: orchestration.exploit }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -417,8 +428,8 @@ async function run(): Promise<void> {
|
||||
|
||||
// 4. Resolve workspace and build pipeline input
|
||||
const workspace = await resolveWorkspace(client, args);
|
||||
const pipelineConfig = await loadPipelineConfig(args.configPath);
|
||||
const input = buildPipelineInput(args, workspace, pipelineConfig);
|
||||
const orchestration = await loadOrchestrationConfig(args.configPath);
|
||||
const input = buildPipelineInput(args, workspace, orchestration);
|
||||
|
||||
// 5. Start worker polling in the background
|
||||
const workerDone = worker.run();
|
||||
|
||||
@@ -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