feat: add --validate-model to run the preflight model checks only

This commit is contained in:
ezl-keygraph committed 2026-10-05 01:53:34 +05:30
1 parent e90cdb5424
commit ed304bb812
11 files changed
+135 -27

No files matched your search

+21 -4
View File
@@ -120,6 +120,23 @@ function isAuthOnlyRun(): boolean {
return process.env.SHANNON_AUTH_ONLY === '1';
}
/** One scan per worker process; the worker sets this flag for a model-validation run (see worker.ts). */
function isModelOnlyRun(): boolean {
return process.env.SHANNON_VALIDATE_MODEL === '1';
}
/** Both validation-only modes share the terminal heading and drop the pentest-only lines. */
function isValidationOnlyRun(): boolean {
return isAuthOnlyRun() || isModelOnlyRun();
}
/** The log header title, framing a validation-only run by what it validated. */
function validationLogTitle(): string {
if (isAuthOnlyRun()) return 'Shannon - Authentication Validation Log';
if (isModelOnlyRun()) return 'Shannon - Model Validation Log';
return 'Shannon Pentest - Scan Log';
}
function safeAgenticSastStageLabel(label: string | undefined): string | undefined {
return label !== undefined && isCapellaTerminalStageLabel(label) ? label : undefined;
}
@@ -441,7 +458,7 @@ 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 title = validationLogTitle();
const header = [
'================================================================================',
title,
@@ -664,8 +681,8 @@ export class WorkflowLogger {
failed: 'FAILED',
};
const status = statusHeaders[summary.status];
const authOnly = isAuthOnlyRun();
const runLabel = authOnly ? 'Validation' : 'Scan';
const validationOnly = isValidationOnlyRun();
const runLabel = validationOnly ? 'Validation' : 'Scan';
const completedAgents = summary.completedAgents.filter(isLoggableAgentName);
const skippedAgents = (summary.skippedAgents ?? []).filter(isLoggableAgentName);
const operationalGroups = summarizeOperationalMetrics(summary.operationalMetrics, summary.operationalStages);
@@ -679,7 +696,7 @@ export class WorkflowLogger {
`Status: ${summary.status}`,
`Duration: ${formatDuration(Math.max(0, summary.totalDurationMs))}`,
`Total Cost: $${Math.max(0, summary.totalCostUsd).toFixed(4)}`,
...(authOnly ? [] : [`Agents: ${completedAgents.length} ran, ${skippedAgents.length} skipped`]),
...(validationOnly ? [] : [`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.');
+2
View File
@@ -109,6 +109,7 @@ export interface PipelineInput {
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)
validateModel?: boolean; // true stops the run after the preflight model checks (no pentest, no report)
}
/** What `loadResumeState` reconstructs from a prior workspace: independently verified, never assumed from session.json alone. */
@@ -186,6 +187,7 @@ export interface PipelineSummary {
export interface PipelineState {
status: 'running' | 'completed' | 'failed' | 'cancelled' | 'partial';
authOnly: boolean;
validateModel: boolean;
currentPhase: string | null;
currentAgent: string | null;
/** Agents that actually ran. Mutually exclusive from `skippedAgents`. */
+13 -3
View File
@@ -251,6 +251,7 @@ interface CliArgs {
customerOutputPath?: string;
pipelineTestingMode: boolean;
authOnly: boolean;
validateModel: boolean;
resumeFromWorkspace?: string;
}
@@ -266,7 +267,8 @@ function showUsage(): void {
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');
console.log(' --validate-auth Validate authentication only, then stop\n');
console.log(' --validate-auth Validate authentication only, then stop');
console.log(' --validate-model Validate the AI model only, then stop\n');
}
function parseCliArgs(argv: string[]): CliArgs {
@@ -283,6 +285,7 @@ function parseCliArgs(argv: string[]): CliArgs {
let customerOutputPath: string | undefined;
let pipelineTestingMode = false;
let authOnly = false;
let validateModel = false;
let resumeFromWorkspace: string | undefined;
for (let i = 0; i < argv.length; i++) {
@@ -321,6 +324,8 @@ function parseCliArgs(argv: string[]): CliArgs {
pipelineTestingMode = true;
} else if (arg === '--validate-auth') {
authOnly = true;
} else if (arg === '--validate-model') {
validateModel = true;
} else if (arg && !arg.startsWith('-')) {
if (!webUrl) {
webUrl = arg;
@@ -349,6 +354,7 @@ function parseCliArgs(argv: string[]): CliArgs {
...(workflowId && { workflowId }),
pipelineTestingMode,
authOnly,
validateModel,
...(configPath && { configPath }),
...(customerOutputPath && { customerOutputPath }),
...(resumeFromWorkspace && { resumeFromWorkspace }),
@@ -598,6 +604,7 @@ function buildPipelineInput(
...(orchestration.agenticSast !== undefined && { agenticSast: orchestration.agenticSast }),
...(orchestration.exploit !== undefined && { exploit: orchestration.exploit }),
...(args.authOnly && { authOnly: true }),
...(args.validateModel && { validateModel: true }),
};
}
@@ -654,6 +661,8 @@ async function waitForWorkflowResult(
console.log('\nScan cancelled before it finished.');
} else if (result.authOnly) {
console.log('\nAuthentication validated. No pentest was run (--validate-auth).');
} else if (result.validateModel) {
console.log('\nModel validated. No pentest was run (--validate-model).');
} else {
console.log('\nScan completed.');
}
@@ -766,9 +775,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.
// One scan per worker process, so an auth-only or model-validation run is a process-wide fact.
// The log writers read these to frame the log as a validation rather than a pentest.
if (args.authOnly) process.env.SHANNON_AUTH_ONLY = '1';
if (args.validateModel) process.env.SHANNON_VALIDATE_MODEL = '1';
// 2. Connect to Temporal server
const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233';
+11
View File
@@ -382,12 +382,14 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
const a = input.pipelineTestingMode ? testActs : acts;
const exploit = input.exploit ?? true;
const authOnly = input.authOnly ?? false;
const validateModel = input.validateModel ?? false;
const sessionId = input.sessionId || input.resumeFromWorkspace || workflowId;
const stateContext: 'fresh' | 'resume' = input.resumeFromWorkspace ? 'resume' : 'fresh';
const state: PipelineState = {
status: 'running',
authOnly,
validateModel,
currentPhase: null,
currentAgent: null,
completedAgents: [],
@@ -1344,6 +1346,15 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
await preflightActs.runPreflightValidation(activityInput);
// The probe gates the exploitation workload, which an auth-only run never reaches.
if (!authOnly) await preflightActs.runExploitReadinessProbe(activityInput);
if (validateModel) {
state.status = 'completed';
state.currentPhase = null;
state.summary = computeSummary(state, usageAccountingComplete());
await a.logWorkflowComplete(activityInput, toWorkflowSummary(state, 'completed'));
return state;
}
await preflightActs.syncPlaywrightStealthConfig(activityInput);
state.currentPhase = 'auth-validation';