mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-04 15:26:55 +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
@@ -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[];
|
||||
|
||||
Reference in new issue
Block a user