mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-08 01:01:10 +02:00
feat: add --validate-auth to run authentication validation only
This commit is contained in:
1 parent
ad2d069563
commit
c81553271f
13 files changed
+121
-24
No files matched your search
@@ -115,6 +115,11 @@ function safeAgenticSastCode(code: string | undefined): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** One scan per worker process; the worker sets this flag for an auth-only run (see worker.ts). */
|
||||
function isAuthOnlyRun(): boolean {
|
||||
return process.env.SHANNON_AUTH_ONLY === '1';
|
||||
}
|
||||
|
||||
function safeAgenticSastStageLabel(label: string | undefined): string | undefined {
|
||||
return label !== undefined && isCapellaTerminalStageLabel(label) ? label : undefined;
|
||||
}
|
||||
@@ -436,9 +441,10 @@ export class WorkflowLogger {
|
||||
try {
|
||||
this.logStream = await LogStream.acquire(this.logPath);
|
||||
const workflowId = safeWorkflowIdentifier(this.workflowId ?? this.sessionMetadata.id);
|
||||
const title = isAuthOnlyRun() ? 'Shannon - Authentication Validation Log' : 'Shannon Pentest - Scan Log';
|
||||
const header = [
|
||||
'================================================================================',
|
||||
'Shannon Pentest - Scan Log',
|
||||
title,
|
||||
'================================================================================',
|
||||
`Workflow ID: ${workflowId}`,
|
||||
`Target URL: ${safeTargetUrl(this.sessionMetadata.webUrl)}`,
|
||||
@@ -447,7 +453,7 @@ export class WorkflowLogger {
|
||||
'',
|
||||
].join('\n');
|
||||
await this.logStream.appendIfAbsent(header, {
|
||||
marker: 'Shannon Pentest - Scan Log',
|
||||
marker: title,
|
||||
scope: 'whole-file',
|
||||
match: 'exact-line',
|
||||
});
|
||||
@@ -658,6 +664,8 @@ export class WorkflowLogger {
|
||||
failed: 'FAILED',
|
||||
};
|
||||
const status = statusHeaders[summary.status];
|
||||
const authOnly = isAuthOnlyRun();
|
||||
const runLabel = authOnly ? 'Validation' : 'Scan';
|
||||
const completedAgents = summary.completedAgents.filter(isLoggableAgentName);
|
||||
const skippedAgents = (summary.skippedAgents ?? []).filter(isLoggableAgentName);
|
||||
const operationalGroups = summarizeOperationalMetrics(summary.operationalMetrics, summary.operationalStages);
|
||||
@@ -665,13 +673,13 @@ export class WorkflowLogger {
|
||||
const lines = [
|
||||
'',
|
||||
'================================================================================',
|
||||
`Scan ${status}`,
|
||||
`${runLabel} ${status}`,
|
||||
'────────────────────────────────────────',
|
||||
`Workflow ID: ${safeWorkflowIdentifier(this.workflowId ?? this.sessionMetadata.id)}`,
|
||||
`Status: ${summary.status}`,
|
||||
`Duration: ${formatDuration(Math.max(0, summary.totalDurationMs))}`,
|
||||
`Total Cost: $${Math.max(0, summary.totalCostUsd).toFixed(4)}`,
|
||||
`Agents: ${completedAgents.length} ran, ${skippedAgents.length} skipped`,
|
||||
...(authOnly ? [] : [`Agents: ${completedAgents.length} ran, ${skippedAgents.length} skipped`]),
|
||||
];
|
||||
if (summary.usageAccountingComplete === false) {
|
||||
lines.push('Cost Note: Cost is incomplete — some background work is not included in this total.');
|
||||
@@ -741,7 +749,7 @@ export class WorkflowLogger {
|
||||
}
|
||||
lines.push('================================================================================');
|
||||
|
||||
const marker = `Scan ${status}`;
|
||||
const marker = `${runLabel} ${status}`;
|
||||
await this.withStream((stream) =>
|
||||
stream.appendIfAbsent(`${lines.join('\n')}\n`, {
|
||||
marker,
|
||||
|
||||
@@ -108,6 +108,7 @@ export interface PipelineInput {
|
||||
customerOutputPath?: string; // Stable mounted path for final customer copies only
|
||||
checkpointsEnabled?: boolean; // Enable checkpoint activities (default: false)
|
||||
exploit?: boolean; // false skips the exploitation phase
|
||||
authOnly?: boolean; // true stops the run after auth validation (no pentest, no report)
|
||||
}
|
||||
|
||||
/** What `loadResumeState` reconstructs from a prior workspace: independently verified, never assumed from session.json alone. */
|
||||
@@ -184,6 +185,7 @@ export interface PipelineSummary {
|
||||
*/
|
||||
export interface PipelineState {
|
||||
status: 'running' | 'completed' | 'failed' | 'cancelled' | 'partial';
|
||||
authOnly: boolean;
|
||||
currentPhase: string | null;
|
||||
currentAgent: string | null;
|
||||
/** Agents that actually ran. Mutually exclusive from `skippedAgents`. */
|
||||
|
||||
@@ -250,6 +250,7 @@ interface CliArgs {
|
||||
configPath?: string;
|
||||
customerOutputPath?: string;
|
||||
pipelineTestingMode: boolean;
|
||||
authOnly: boolean;
|
||||
resumeFromWorkspace?: string;
|
||||
}
|
||||
|
||||
@@ -264,7 +265,8 @@ function showUsage(): void {
|
||||
console.log(' --config <path> Configuration file path');
|
||||
console.log(' --workspace <name> Resume from existing workspace');
|
||||
console.log(' --output <path> Stable mounted path for final customer report copies');
|
||||
console.log(' --pipeline-testing Use minimal prompts for fast testing\n');
|
||||
console.log(' --pipeline-testing Use minimal prompts for fast testing');
|
||||
console.log(' --validate-auth Validate authentication only, then stop\n');
|
||||
}
|
||||
|
||||
function parseCliArgs(argv: string[]): CliArgs {
|
||||
@@ -280,6 +282,7 @@ function parseCliArgs(argv: string[]): CliArgs {
|
||||
let configPath: string | undefined;
|
||||
let customerOutputPath: string | undefined;
|
||||
let pipelineTestingMode = false;
|
||||
let authOnly = false;
|
||||
let resumeFromWorkspace: string | undefined;
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
@@ -316,6 +319,8 @@ function parseCliArgs(argv: string[]): CliArgs {
|
||||
}
|
||||
} else if (arg === '--pipeline-testing') {
|
||||
pipelineTestingMode = true;
|
||||
} else if (arg === '--validate-auth') {
|
||||
authOnly = true;
|
||||
} else if (arg && !arg.startsWith('-')) {
|
||||
if (!webUrl) {
|
||||
webUrl = arg;
|
||||
@@ -343,6 +348,7 @@ function parseCliArgs(argv: string[]): CliArgs {
|
||||
taskQueue,
|
||||
...(workflowId && { workflowId }),
|
||||
pipelineTestingMode,
|
||||
authOnly,
|
||||
...(configPath && { configPath }),
|
||||
...(customerOutputPath && { customerOutputPath }),
|
||||
...(resumeFromWorkspace && { resumeFromWorkspace }),
|
||||
@@ -591,6 +597,7 @@ function buildPipelineInput(
|
||||
...(args.customerOutputPath !== undefined && { customerOutputPath: args.customerOutputPath }),
|
||||
...(orchestration.agenticSast !== undefined && { agenticSast: orchestration.agenticSast }),
|
||||
...(orchestration.exploit !== undefined && { exploit: orchestration.exploit }),
|
||||
...(args.authOnly && { authOnly: true }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -645,6 +652,8 @@ async function waitForWorkflowResult(
|
||||
}
|
||||
} else if (result.status === 'cancelled') {
|
||||
console.log('\nScan cancelled before it finished.');
|
||||
} else if (result.authOnly) {
|
||||
console.log('\nAuthentication validated. No pentest was run (--validate-auth).');
|
||||
} else {
|
||||
console.log('\nScan completed.');
|
||||
}
|
||||
@@ -757,6 +766,10 @@ async function run(): Promise<void> {
|
||||
// 1. Parse CLI args
|
||||
const args = parseCliArgs(process.argv.slice(2));
|
||||
|
||||
// One scan per worker process, so an auth-only run is a process-wide fact. The log writers
|
||||
// read it to frame the log as a validation rather than a pentest.
|
||||
if (args.authOnly) process.env.SHANNON_AUTH_ONLY = '1';
|
||||
|
||||
// 2. Connect to Temporal server
|
||||
const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
|
||||
console.log(`Connecting to Temporal at ${address}...`);
|
||||
|
||||
@@ -381,11 +381,13 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
const { workflowId } = workflowInfo();
|
||||
const a = input.pipelineTestingMode ? testActs : acts;
|
||||
const exploit = input.exploit ?? true;
|
||||
const authOnly = input.authOnly ?? false;
|
||||
const sessionId = input.sessionId || input.resumeFromWorkspace || workflowId;
|
||||
const stateContext: 'fresh' | 'resume' = input.resumeFromWorkspace ? 'resume' : 'fresh';
|
||||
|
||||
const state: PipelineState = {
|
||||
status: 'running',
|
||||
authOnly,
|
||||
currentPhase: null,
|
||||
currentAgent: null,
|
||||
completedAgents: [],
|
||||
@@ -1289,7 +1291,7 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
const durable = await deterministicReportActs.initializeDurableScanState(activityInput, exploit, stateContext);
|
||||
applyDurableSummary(durable);
|
||||
|
||||
if (input.resumeFromWorkspace) {
|
||||
if (!authOnly && input.resumeFromWorkspace) {
|
||||
// The new workflow id lands in session.json before anything that can reject the resume, so a
|
||||
// validation or checkpoint-restore failure still leaves the CLI an attempt to follow.
|
||||
await deterministicReportActs.registerResumeAttempt(activityInput, input.terminatedWorkflows ?? []);
|
||||
@@ -1340,7 +1342,8 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
state.currentPhase = 'preflight';
|
||||
state.currentAgent = null;
|
||||
await preflightActs.runPreflightValidation(activityInput);
|
||||
await preflightActs.runExploitReadinessProbe(activityInput);
|
||||
// The probe gates the exploitation workload, which an auth-only run never reaches.
|
||||
if (!authOnly) await preflightActs.runExploitReadinessProbe(activityInput);
|
||||
await preflightActs.syncPlaywrightStealthConfig(activityInput);
|
||||
|
||||
state.currentPhase = 'auth-validation';
|
||||
@@ -1349,6 +1352,21 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
if (authMetrics !== null) state.agentMetrics['validate-authentication'] = authMetrics;
|
||||
state.currentAgent = null;
|
||||
|
||||
// Auth-only runs stop here; a null result means no authentication block, which is a misconfig.
|
||||
if (authOnly) {
|
||||
if (authMetrics === null) {
|
||||
throw ApplicationFailure.nonRetryable(
|
||||
'An auth-validation run needs an authentication block in the config. Add one, or drop --validate-auth.',
|
||||
'ConfigurationError',
|
||||
);
|
||||
}
|
||||
state.status = 'completed';
|
||||
state.currentPhase = null;
|
||||
state.summary = computeSummary(state, usageAccountingComplete());
|
||||
await a.logWorkflowComplete(activityInput, toWorkflowSummary(state, 'completed'));
|
||||
return state;
|
||||
}
|
||||
|
||||
await a.initDeliverableGit(activityInput);
|
||||
await a.syncCodePathDenyRules(activityInput);
|
||||
|
||||
|
||||
Reference in new issue
Block a user