mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-02 22:36:49 +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
@@ -87,7 +87,7 @@ pnpm biome:fix # Auto-fix lint, format, and import sorting
|
||||
|
||||
**Monorepo tooling:** pnpm workspaces, Turborepo for task orchestration, Biome for linting/formatting. TypeScript compiler options shared via `tsconfig.base.json` at the root. All packages extend it, overriding only `rootDir` and `outDir`. Shared devDependencies (`typescript`, `@types/node`, `turbo`, `@biomejs/biome`) are hoisted to the root workspace.
|
||||
|
||||
**Options:** `-c <file>` (YAML config), `--models-config <file>` (pi `models.json` defining models pi's catalogue lacks), `-o <path>` (output directory), `-w <name>` (named workspace; auto-resumes if exists), `--pipeline-testing` (minimal prompts, 10s retries), `--keep-container` (preserve worker container after exit for log inspection), `--yes`/`-y` (skip the confirmation prompt on `stop`; required for non-interactive use; `reset` requires a typed `confirm` and cannot be skipped)
|
||||
**Options:** `-c <file>` (YAML config), `--models-config <file>` (pi `models.json` defining models pi's catalogue lacks), `-o <path>` (output directory), `-w <name>` (named workspace; auto-resumes if exists), `--validate-auth` (run preflight and auth validation only, then stop; no pentest or report; requires a fresh workspace and an `authentication` block in the config), `--pipeline-testing` (minimal prompts, 10s retries), `--keep-container` (preserve worker container after exit for log inspection), `--yes`/`-y` (skip the confirmation prompt on `stop`; required for non-interactive use; `reset` requires a typed `confirm` and cannot be skipped)
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -21,7 +21,15 @@ import { resolveWorkflowId } from '../session.js';
|
||||
import { waitForWorkflowClose } from '../temporal-client.js';
|
||||
import { stdoutIsTerminal } from '../tty.js';
|
||||
|
||||
const TERMINAL_HEADINGS = new Set(['Scan COMPLETED', 'Scan PARTIAL', 'Scan FAILED', 'Scan CANCELLED']);
|
||||
const TERMINAL_HEADINGS = new Set([
|
||||
'Scan COMPLETED',
|
||||
'Scan PARTIAL',
|
||||
'Scan FAILED',
|
||||
'Scan CANCELLED',
|
||||
'Validation COMPLETED',
|
||||
'Validation FAILED',
|
||||
'Validation CANCELLED',
|
||||
]);
|
||||
|
||||
// The combined log resets completion on the bare `RESUMED` heading; a per-agent file carries the
|
||||
// distinct `--- RESUMED (<workflow id>) ---` boundary that WorkflowLogger.logResumeBoundary writes
|
||||
@@ -48,7 +56,7 @@ export class LogCompletionState {
|
||||
this.failureIsLastMarker = false;
|
||||
} else if (TERMINAL_HEADINGS.has(line)) {
|
||||
this.terminalIsLastMarker = true;
|
||||
this.failureIsLastMarker = line === 'Scan FAILED';
|
||||
this.failureIsLastMarker = line.endsWith('FAILED');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface StartArgs {
|
||||
pipelineTesting: boolean;
|
||||
keepContainer: boolean;
|
||||
follow: boolean;
|
||||
authOnly: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
@@ -225,6 +226,9 @@ export function createWorkflowId(workspace: string, isResume: boolean, timestamp
|
||||
}
|
||||
|
||||
export async function start(args: StartArgs): Promise<void> {
|
||||
// Auth-only runs are short and have no report to come back for, so they always stream to the end.
|
||||
if (args.authOnly) args.follow = true;
|
||||
|
||||
// 1. Resolve non-mutating inputs and classify the workspace before changing it.
|
||||
initHome();
|
||||
loadEnv();
|
||||
@@ -242,6 +246,13 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
const requestedOutputDir = args.output ? path.resolve(expandHome(args.output)) : undefined;
|
||||
const launchDecision = classifyWorkspaceLaunch(workspacePath, args.url, requestedOutputDir);
|
||||
|
||||
// Auth-only runs write no resumable state, so they always run fresh; reusing a workspace would resume it.
|
||||
if (args.authOnly && launchDecision.isResume) {
|
||||
fail(
|
||||
'An auth-validation run needs a fresh workspace. Omit -w to auto-name one, or choose a -w name that is not in use.',
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Inputs are valid; identify the run before initializing shared infrastructure.
|
||||
const bannerVersion = isLocal() ? undefined : args.version;
|
||||
if (stdoutIsTerminal()) {
|
||||
@@ -254,7 +265,7 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
ensureDocker();
|
||||
ensureImage(args.version);
|
||||
const spinner = p.spinner();
|
||||
spinner.start('Starting scan');
|
||||
spinner.start(args.authOnly ? 'Starting authentication validation' : 'Starting scan');
|
||||
await ensureInfra(spinner);
|
||||
|
||||
// 3. Generate the invocation identity.
|
||||
@@ -336,6 +347,7 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
workspace,
|
||||
...(args.pipelineTesting && { pipelineTesting: true }),
|
||||
...(args.keepContainer && { keepContainer: true }),
|
||||
...(args.authOnly && { authOnly: true }),
|
||||
...(shouldUsePiAuth() && { piAuthHostPath: resolveHostPiAuthPath() }),
|
||||
});
|
||||
|
||||
@@ -386,7 +398,7 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
});
|
||||
|
||||
// Poll for the workflow to register in session.json; the spinner resolves once it does.
|
||||
spinner.message('Waiting for the scan to start');
|
||||
spinner.message(args.authOnly ? 'Waiting for authentication validation to start' : 'Waiting for the scan to start');
|
||||
for (let attempts = 0; attempts < 60; attempts++) {
|
||||
// A pre-workflow failure leaves its reason here (nothing reached Temporal); surface it
|
||||
// rather than polling out to a generic timeout.
|
||||
@@ -420,15 +432,15 @@ export async function start(args: StartArgs): Promise<void> {
|
||||
spinner.message('Running preflight checks');
|
||||
const outcome = await awaitPreflightOutcome(workflowId);
|
||||
if (outcome.kind === 'failed') {
|
||||
spinner.error('The scan could not start');
|
||||
spinner.error(args.authOnly ? 'Authentication validation could not start' : 'The scan could not start');
|
||||
printScanStartFailure(outcome.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner.stop(`Scan started — ${workspace}`);
|
||||
spinner.stop(args.authOnly ? `Validating authentication — ${workspace}` : `Scan started — ${workspace}`);
|
||||
printInfo(args, workspace, repo.hostPath, workspacesDir);
|
||||
if (args.follow) {
|
||||
await followScan(workspace, workspacesDir);
|
||||
await followScan(workspace, workspacesDir, args.authOnly);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -576,7 +588,7 @@ function printUnconfirmedScanHint(workspace: string, taskQueue: string, containe
|
||||
* That tracks whether the pipeline ran, not whether vulnerabilities were found. On failure the
|
||||
* root-cause message is printed so a red CI build says why.
|
||||
*/
|
||||
async function followScan(workspace: string, workspacesDir: string): Promise<never> {
|
||||
async function followScan(workspace: string, workspacesDir: string, authOnly = false): Promise<never> {
|
||||
const logFile = resolveRunFile(path.join(workspacesDir, workspace), 'workflow.log');
|
||||
const workflowId = resolveWorkflowId(workspace);
|
||||
|
||||
@@ -587,7 +599,8 @@ async function followScan(workspace: string, workspacesDir: string): Promise<nev
|
||||
}
|
||||
|
||||
if (stdoutIsTerminal()) {
|
||||
console.error('\n Following scan log (Ctrl-C to stop watching):\n');
|
||||
const what = authOnly ? 'validation' : 'scan';
|
||||
console.error(`\n Following ${what} log (Ctrl-C to stop watching):\n`);
|
||||
}
|
||||
|
||||
let temporalUnreachable = false;
|
||||
@@ -675,10 +688,12 @@ function printInfo(args: StartArgs, workspace: string, repoPath: string, workspa
|
||||
console.log(` Progress: ${prefix} status ${workspace}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(' Report (when the scan finishes):');
|
||||
console.log(` ${reportDir}${path.sep}`);
|
||||
console.log(` ${FINAL_REPORT_PDF_FILENAME}`);
|
||||
console.log(` ${FINAL_REPORT_MD_FILENAME}`);
|
||||
console.log('');
|
||||
if (!args.authOnly) {
|
||||
console.log('');
|
||||
console.log(' Report (when the scan finishes):');
|
||||
console.log(` ${reportDir}${path.sep}`);
|
||||
console.log(` ${FINAL_REPORT_PDF_FILENAME}`);
|
||||
console.log(` ${FINAL_REPORT_MD_FILENAME}`);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
@@ -412,6 +412,7 @@ export interface WorkerOptions {
|
||||
workspace: string;
|
||||
pipelineTesting?: boolean;
|
||||
keepContainer?: boolean;
|
||||
authOnly?: boolean;
|
||||
piAuthHostPath?: string;
|
||||
}
|
||||
|
||||
@@ -511,6 +512,9 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
|
||||
if (opts.pipelineTesting) {
|
||||
args.push('--pipeline-testing');
|
||||
}
|
||||
if (opts.authOnly) {
|
||||
args.push('--validate-auth');
|
||||
}
|
||||
|
||||
// Inherit stderr so `docker run` daemon errors surface to the user;
|
||||
// ignore stdin/stdout (the container ID is noise).
|
||||
|
||||
@@ -33,6 +33,7 @@ export const START_OPTIONS: readonly (readonly [string, string])[] = [
|
||||
['-o, --output <path>', 'Copy deliverables to this directory after the run'],
|
||||
['-w, --workspace <name>', 'Named workspace (auto-resumes if it exists)'],
|
||||
['-f, --follow', 'Stream the scan log until it finishes'],
|
||||
['--validate-auth', 'Validate authentication only, then stop (no pentest)'],
|
||||
['--pipeline-testing', 'Use minimal prompts for fast testing'],
|
||||
['--keep-container', 'Preserve the worker container after exit for log inspection'],
|
||||
];
|
||||
@@ -45,6 +46,7 @@ const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
||||
'start -u https://example.com -r ./my-repo',
|
||||
'start -u https://example.com -r /path/to/repo -c config.yaml -w q1-audit',
|
||||
'start -u https://example.com -r ./my-repo --follow',
|
||||
'start -u https://example.com -r ./my-repo -c config.yaml --validate-auth',
|
||||
],
|
||||
},
|
||||
stop: {
|
||||
|
||||
@@ -189,6 +189,7 @@ interface ParsedStartArgs {
|
||||
pipelineTesting: boolean;
|
||||
keepContainer: boolean;
|
||||
follow: boolean;
|
||||
authOnly: boolean;
|
||||
}
|
||||
|
||||
function parseStartArgs(argv: string[]): ParsedStartArgs {
|
||||
@@ -205,6 +206,7 @@ function parseStartArgs(argv: string[]): ParsedStartArgs {
|
||||
pipelineTesting: ['--pipeline-testing'],
|
||||
keepContainer: ['--keep-container'],
|
||||
follow: ['-f', '--follow'],
|
||||
authOnly: ['--validate-auth'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -220,12 +222,20 @@ function parseStartArgs(argv: string[]): ParsedStartArgs {
|
||||
failUsage(`invalid --url: ${url}`);
|
||||
}
|
||||
|
||||
if (flags.authOnly && !values.config) {
|
||||
failUsage(
|
||||
'--validate-auth needs a config file with an authentication block',
|
||||
`Usage: ${commandPrefix()} start -u <url> -r <path> -c <config.yaml> --validate-auth`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
repo,
|
||||
pipelineTesting: !!flags.pipelineTesting,
|
||||
keepContainer: !!flags.keepContainer,
|
||||
follow: !!flags.follow,
|
||||
authOnly: !!flags.authOnly,
|
||||
...(values.config && { config: values.config }),
|
||||
...(values.modelsConfig && { modelsConfig: values.modelsConfig }),
|
||||
...(values.workspace && { workspace: values.workspace }),
|
||||
|
||||
@@ -108,6 +108,7 @@ const MISCELLANEOUS_EXPLOIT_AGENT: AgentSpec = {
|
||||
* available guess.
|
||||
*/
|
||||
export function pipelineForState(state: PipelineState | null): readonly PhaseSpec[] {
|
||||
if (state?.authOnly === true) return PIPELINE.filter((phase) => phase.key === 'auth-validation');
|
||||
if (state?.expectedAgents === undefined) return PIPELINE;
|
||||
const expected = new Set(state.expectedAgents);
|
||||
return PIPELINE.map((phase) => {
|
||||
@@ -353,6 +354,7 @@ export type PipelineStatus = 'running' | 'completed' | 'failed' | 'cancelled' |
|
||||
|
||||
export interface PipelineState {
|
||||
readonly status: PipelineStatus;
|
||||
readonly authOnly?: boolean;
|
||||
readonly currentPhase: string | null;
|
||||
readonly currentAgent: string | null;
|
||||
readonly completedAgents: string[];
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -179,3 +179,14 @@ login_flow:
|
||||
- "If prompted for 2FA, type $totp in <exact code field label or placeholder>"
|
||||
- "Click <exact button text>"
|
||||
```
|
||||
|
||||
### Validating Authentication Only
|
||||
|
||||
To confirm your login flow works before committing to a full scan, add `--validate-auth` to `start`:
|
||||
|
||||
```bash
|
||||
npx @keygraph/shannon start -u https://your-app.com -r /path/to/repo -c config.yaml --validate-auth
|
||||
```
|
||||
|
||||
The run performs preflight and the single real login, then stops. No pentest, reconciliation, or report
|
||||
is produced. It requires an `authentication` block in the config.
|
||||
@@ -122,6 +122,9 @@ npx @keygraph/shannon start -u https://example.com -r /path/to/repo -w q1-audit
|
||||
# Stream the log until the scan finishes, then exit on its outcome (useful in CI).
|
||||
npx @keygraph/shannon start -u https://example.com -r /path/to/repo --follow
|
||||
|
||||
# Validate the configured login only, then stop (no pentest or report).
|
||||
npx @keygraph/shannon start -u https://example.com -r /path/to/repo -c /path/to/my-config.yaml --validate-auth --follow
|
||||
|
||||
# List running and completed scans.
|
||||
npx @keygraph/shannon scans
|
||||
```
|
||||
@@ -134,6 +137,7 @@ Source-build examples:
|
||||
./shannon start -u https://example.com -r /path/to/repo -o ./my-reports
|
||||
./shannon start -u https://example.com -r /path/to/repo -w q1-audit
|
||||
./shannon start -u https://example.com -r /path/to/repo --follow
|
||||
./shannon start -u https://example.com -r /path/to/repo -c /path/to/my-config.yaml --validate-auth
|
||||
./shannon scans
|
||||
|
||||
# Rebuild the worker image.
|
||||
|
||||
Reference in new issue
Block a user