Files
shannon/apps/cli/src/scan/pipeline.ts
T
ezl-keygraph d41ae9c20d feat(cli): overhaul commands and add live scan status (#424)
* refactor(cli): list workspaces natively instead of via the worker image

* feat(cli): preflight that Docker is installed and running

* feat(cli): stop scans by workspace or --all, terminating their Temporal workflows

* fix(worker): abort the running agent on cancellation so Temporal cancel takes effect

* refactor(cli): split destructive teardown out of stop into a reset command

* refactor(cli): centralise flag parsing and confirmation across commands

* fix(cli): pass provider credentials to docker by name to keep secrets out of argv

* feat(cli): add per-command help via <command> --help/-h and help <command>

* feat(cli): replace raw docker output with clack spinners for infra and scan teardown

* fix(cli): verify scan stop by re-querying container and workflow state instead of assuming success

* fix(cli): resolve running state before prompting on stop and report no-op stops honestly

* refactor(cli): show splash first and drive start with one spinner resolving to a clean line

* fix(cli): validate --url up front so a bad value fails cleanly instead of a late crash

* refactor(cli): centralize error reporting with fail() for expected errors and a crash handler that logs the stack and links the issue tracker

* feat(cli): add --json/--plain machine-readable output to workspaces and status

* refactor(cli): remove the workspaces command

* refactor(cli): remove the status command

* feat(cli): add 'progress <workspace>' — live scan progress from Temporal

* fix(cli): mark metric-less agents as skipped in progress, not done

* feat(cli): animate running agents in progress with a clack-style spinner

* feat(cli): rename progress->status, reveal agents as they run, show live per-agent elapsed

* fix(cli): mark passed-over phases as skipped live, not pending

* style(cli): rename status footer 'Wall-clock' to 'Time Taken', drop the parenthetical

* style(cli): drop '(sum of agents)' from status total cost line

* style(cli): green filled circle for completed, Shannon gold for running

* style(cli): use Shannon gold in place of green in status

* feat(cli): suggest closest command or flag on typo

* refactor(cli): single-source start help and drop ./repos bare-name shortcut

* feat(cli): name providers and fix in multi-provider credential error

* feat(cli): support --flag=value syntax and expand leading ~ in paths

* refactor(cli): centralize ANSI color codes in colors.ts

* feat(cli): add scans command listing completed scans with cost and duration

* fix(cli): keep stdout clean off-TTY for logs and start

* feat(cli): add repo link to top-level help

* feat(worker): record auth-validation metrics and register resume attempts early

* refactor(cli): share resume-aware workflow-id resolution and surface root-cause failures

* feat(cli): add status --json, auth phase, dashboard link, and stable live redraw

* refactor(cli): drop cost from status and scans output

* feat(worker): surface both PDF and markdown report at run root

* refactor(cli): normalize error/warning prefixing through fail and warn

* feat(cli): add version --json for machine-readable output

* refactor(cli): rename start --debug to --keep-container

* refactor(cli): point start's progress hint at status instead of the Temporal dashboard

* refactor(cli): centralize the mode-aware command prefix

* refactor(cli): trim start and logs output to durable facts off-TTY

* feat(cli): require typed confirmation for reset instead of --yes

reset permanently wipes all Temporal data and volumes — a severe,
irreversible action. Replace its default y/N confirm (bypassable with
--yes) with a typed-word confirmation that has no bypass, so the wipe
can only be triggered by a deliberate interactive answer.

* feat(cli): surface logs and status hints after start on a TTY

* feat(cli): exit 2 on usage errors, distinct from operational failures

* feat(cli): add start --follow to stream logs and exit on scan outcome

* refactor(cli): redesign splash with sunset-gradient wordmark and truecolor

* refactor(cli): remove the uninstall command

* docs: sync CLI docs with removed uninstall/workspaces, new scans and --follow

* docs: fix reset confirmation — typed confirm, not --yes/-y

* style(cli): restructure status footer with divider, aligned Logs/Temporal rows

* feat(cli): show splash in the status command

* fix(worker): validate auth-state shape, not entry count

* docs: correct reset confirmation and add markdown report to run-root docs
2026-08-18 15:46:25 +05:30

124 lines
4.5 KiB
TypeScript

/**
* Static description of the Shannon scan pipeline, plus the worker types the CLI
* reads back from Temporal.
*
* The CLI cannot import from the worker package, so this mirrors it. Keep in sync with:
* - apps/worker/src/types/agents.ts (agent names / ordering)
* - apps/worker/src/session-manager.ts (phase membership)
* - apps/worker/src/temporal/activities.ts (the run*Agent activity names → `activityType`)
* - apps/worker/src/temporal/shared.ts (PipelineState / PipelineSummary)
* - apps/worker/src/types/metrics.ts (AgentMetrics)
*/
export interface AgentSpec {
/** Canonical agent name as it appears in PipelineState.completedAgents / agentMetrics. */
readonly name: string;
/** Short label for the progress tree. */
readonly label: string;
/** Temporal activity type name — how a running agent shows up in pendingActivities. */
readonly activityType: string;
}
export interface PhaseSpec {
readonly key: string;
readonly label: string;
readonly parallel: boolean;
readonly agents: readonly AgentSpec[];
}
/** The pipeline phases in execution order, each with its agents. */
export const PIPELINE: readonly PhaseSpec[] = [
{
// Preflight login check. Only authenticated scans record metrics here; a non-auth scan
// records none, so it renders as skipped — like Exploitation when nothing is exploitable.
key: 'auth-validation',
label: 'Authentication',
parallel: false,
agents: [{ name: 'validate-authentication', label: 'auth', activityType: 'runAuthenticationValidation' }],
},
{
key: 'pre-recon',
label: 'Pre-Recon',
parallel: false,
agents: [{ name: 'pre-recon', label: 'pre-recon', activityType: 'runPreReconAgent' }],
},
{
key: 'recon',
label: 'Recon',
parallel: false,
agents: [{ name: 'recon', label: 'recon', activityType: 'runReconAgent' }],
},
{
key: 'vulnerability-analysis',
label: 'Vulnerability Analysis',
parallel: true,
agents: [
{ name: 'injection-vuln', label: 'injection', activityType: 'runInjectionVulnAgent' },
{ name: 'xss-vuln', label: 'xss', activityType: 'runXssVulnAgent' },
{ name: 'auth-vuln', label: 'auth', activityType: 'runAuthVulnAgent' },
{ name: 'ssrf-vuln', label: 'ssrf', activityType: 'runSsrfVulnAgent' },
{ name: 'authz-vuln', label: 'authz', activityType: 'runAuthzVulnAgent' },
],
},
{
key: 'exploitation',
label: 'Exploitation',
parallel: true,
agents: [
{ name: 'injection-exploit', label: 'injection', activityType: 'runInjectionExploitAgent' },
{ name: 'xss-exploit', label: 'xss', activityType: 'runXssExploitAgent' },
{ name: 'auth-exploit', label: 'auth', activityType: 'runAuthExploitAgent' },
{ name: 'ssrf-exploit', label: 'ssrf', activityType: 'runSsrfExploitAgent' },
{ name: 'authz-exploit', label: 'authz', activityType: 'runAuthzExploitAgent' },
],
},
{
key: 'reporting',
label: 'Reporting',
parallel: false,
agents: [{ name: 'report', label: 'report', activityType: 'runReportAgent' }],
},
];
/** Temporal activity type name → canonical agent name, for mapping pendingActivities. */
export const ACTIVITY_TO_AGENT: Readonly<Record<string, string>> = Object.fromEntries(
PIPELINE.flatMap((phase) => phase.agents.map((agent) => [agent.activityType, agent.name])),
);
/** The vuln/exploit class of an agent (e.g. "authz-vuln" → "authz"), for failedPipelines matching. */
export function agentClass(name: string): string {
return name.replace(/-(vuln|exploit)$/, '');
}
// === Worker types read back from Temporal (mirror of shared.ts / metrics.ts) ===
export interface AgentMetrics {
readonly durationMs: number;
readonly costUsd: number | null;
readonly numTurns: number | null;
readonly model?: string;
readonly skipped?: boolean;
}
export interface PipelineSummary {
readonly totalCostUsd: number;
readonly totalDurationMs: number; // Wall-clock (end - start)
readonly totalTurns: number;
readonly agentCount: number;
}
export type PipelineStatus = 'running' | 'completed' | 'failed' | 'cancelled' | 'partial';
export interface PipelineState {
readonly status: PipelineStatus;
readonly currentPhase: string | null;
readonly currentAgent: string | null;
readonly completedAgents: string[];
readonly failedPipelines: { vulnType: string; error: string }[];
readonly failedAgent: string | null;
readonly error: string | null;
readonly startTime: number;
readonly agentMetrics: Record<string, AgentMetrics>;
readonly summary: PipelineSummary | null;
}