feat: Shannon 3.0 Agentic SAST (#433)

* feat(worker): add agentic static analysis

Add the ten-stage Agentic SAST pipeline, confined repository tools, model runtime, prompt templates, and SARIF export.

Make retries, repair sessions, reduced coverage, usage accounting, and model-output drift durable across Temporal replay and resume. Keep retry diagnostics in their actionable closed vocabulary. Package the Mantis-derived license material with the prompts that require it.

* feat(worker): deduplicate static and runtime findings before exploitation

Parse Agentic SAST SARIF into typed observations, enrich and route those observations, and reconcile them with pentest findings before exploitation.

Publish deterministic exploitation queues with stable lineage, exact-path Git commits, retry-safe manifests, named drop reasons, and confined task formation. Reject duplicate producer IDs before commit and adopt either legal provenance shape after a lost acknowledgement.

* feat(config)!: replace vuln_classes with agentic_sast

Wire Agentic SAST and reconciliation into the main pipeline, persist their durable state, and add the Miscellaneous finding and exploitation lane.

Make scan completion, cancellation, partial outcomes, resume identity, and report recovery use the integrated final workflow contract. Introduce the atomic finalization, ordering, renumbering, compaction, and output services that workflow calls. Keep completed Miscellaneous work and report drafts idempotent across resume, preserve public main's default-on exploit SARIF behavior, and describe stage-fallback candidates without claiming they were exported.

BREAKING CHANGE: `vuln_classes` has been removed. Configs containing it now fail validation, and all five core pentest classes run on every scan.

Workspaces created by Shannon 2.x cannot be resumed. Finish or discard in-flight scans before upgrading, then start a new workspace name.

* perf: overlap static analysis and the Miscellaneous lane with the pentest

Run Agentic SAST alongside vulnerability analysis and run Miscellaneous exploitation alongside the specialist exploitation lanes.

Keep reconciliation dependent on the completed static-analysis result while preserving parallel work everywhere that has no data dependency.

* feat(cli)!: default the scan target and add a JSON error contract

List local scans, resolve the active or most recent workspace automatically, and make logs, status, and stop use one canonical scan identity.

Add stable machine-readable failures, richer status output, explicit help errors, and seven-day Temporal retention. Treat absent Temporal pending-activity failures as absent whether the decoder represents them as `null` or missing.

BREAKING CHANGE: `status --json` now returns a fixed `failureMessage`. Read `partialReasons`, `agenticSast`, and `workflow.log` for diagnostic detail.

* feat(logging): trace tool calls and write a log per agent

Record complete tool-call arguments in the workflow log and project each agent's events into its own durable log.

Add agent listing and agent-specific log tailing while preserving byte-exact output and draining log handles before
activities return.

* feat(worker): standardize severity and reporting guidance in exploit prompts

Give every exploit agent the same status, confidence, severity-reasoning, report-writing, credential-handling, and
scope contract.

Apply the same task-formation and SAST-enrichment procedure to the Miscellaneous lane.

* feat(worker): disclose scan coverage and make reporting auditable

Build on the retry-safe finalization foundation to preserve correct identities, source locations, scan dates,
partial-coverage limitations, and consistent report JSON, Markdown, SARIF, and PDF output.

Report Agentic SAST, reconciliation wall-clock time, stage usage, retry spend, and background work without duplicate
or hardcoded totals. Keep report findings canonical, drop cross-class restatements, name enrichment losses, and render
the executive-summary narrative in the PDF.

* chore(license): attribute Mantis and Pi and refresh the docs

Add the final Mantis and Pi notices, license copies, acknowledgements, and residual copyright updates.

Update the README, maintained documentation, contributor guidance, and hand-maintained mirrors to describe Agentic
SAST, reconciliation, the Miscellaneous lane, current CLI behavior, and the final release contract. Correct stale
workspace and container guidance and annotate long-standing internals for maintainers.

* fix(logging): treat a slash as a word separator in agent labels

* feat(cli)!: rebuild scan status around model work

- show Capella stages beneath the concurrent Agentic SAST phase
- attach reconciliation time to the class row it feeds
- hide completed bookkeeping and the duplicate miscellaneous wrapper
- carry validated child-workflow progress into durable parent state
- derive the terminal tree and status JSON from the same phase shape

BREAKING CHANGE: `status --json` replaces phase `parallel` with `children` and `meta`, adds phase summaries and notes plus agent attachment fields, and removes the `analysis-engines` and `operational-work` phases.

* fix(report): drop the empty Critical Findings section from the PDF summary

* fix(sast): align Capella export with the submit-time code-path contract

The export gate required every code_paths entry to be file:line, but submit only
requires the primary sink to be file:line and accepts bare trace steps. A single
malformed trace step therefore dropped an otherwise-valid finding at export.

- add isValidPrimaryCodePath as the one shared primary-sink contract
- validate only the primary at export; buildResult already drops unusable steps
- route the submit-time validator through the same helper so the two cannot drift

* feat(sast): tolerate hygiene-only Capella reductions instead of going partial

A reduction only makes a run partial when it loses real coverage or a whole
finding. Malformed model output, salvaged turn-limit work, and rejected duplicate
verdicts are recorded as evidence but no longer flip the run to partial.

- add reductionIsTolerable: partial only when genuine-loss counts are nonzero
- drive runCapella's partial reasons and display coverage off non-tolerable ones
- keep every reduction in agenticSast.reductions so nothing is lost as evidence

* feat(logging): record the provider reason for a failed agent turn

A failed provider turn collapsed to AGENT_EXECUTION_FAILED/unknown with the
underlying reason discarded, so a model-side rejection or safeguard was
indistinguishable from a transport fault in the error log.

- add safeProviderTurnDetails: write bounded, non-sensitive fields (provider,
  model, responseId, stop reason, tool-in-flight, category, retryable) to error.log
- gate a sanitized errorMessage snippet behind SHANNON_DEBUG_PROVIDER_ERRORS, off by default
- forward SHANNON_DEBUG_PROVIDER_ERRORS from the CLI into the worker container

* fix(cli): keep shannon logs tailing through a Temporal blip

- End the interactive tail on the log's own terminal marker or Ctrl-C, so a
  transient Temporal outage no longer aborts the command with exit 1.
- Rebuild the memoized Temporal client after a failed poll: a wedged gRPC
  channel was cached forever, so "retrying…" could never reconnect.
- Keep start --follow (CI) bounded — a genuinely dead Temporal still fails
  the run instead of hanging.

* fix(worker): correct PDF finding reporting

- Render OWASP category, authentication state, and remediation
- Omit the redundant per-finding exploited status
- Preserve canonical category and field ordering across report modes
- Continue Proof of Impact numbering across embedded code blocks
- Wrap long PDF code lines without changing canonical report content

* fix: attribute a reconciliation failure to exploitation only

- Stop marking a class's vulnerability-analysis agent failed when that agent
  succeeded and only reconciliation failed; the status tree now renders the
  analysis row completed and the exploitation row failed
- Consume the worker's failedReconciliations signal in the CLI, which the
  mirrored PipelineState already declared but never read
- Correct the class_reconciliation_failed message, which claimed the class's
  analysis results were still in the report when the class is excluded from it

* fix(pi): give each task sub-session its own resource loader to prevent stale extension ctx

* fix(prompts): scope exploit agents to in-band proof, mark OOB-only findings blocked

* fix(cli): reject a shell credential that shadows a gateway config.toml key

* fix(cli): make scan shutdown verifiable

- preselect and persist workflow identity before worker launch
- cancel first, then verify bounded Temporal termination
- reconcile Docker workers with Temporal open workflows
- fail closed on stale images and unavailable lifecycle state
- mark cancellation only after confirmed shutdown

* feat(cli): prompt for setup on a bare npx invocation with no credentials

* fix(cli): don't blame anthropic when no credentials are configured at all

* chore(release): bump beta base version to 3.0.0

* feat(cli): show a 'start your first scan' box in help on a TTY

* docs: refresh README and platform overview for Shannon 3.0

- lead with the 3.0 launch note and rewrite key capabilities around security
  code analysis, the rebuilt terminal experience, native CI/CD, and PDF/SARIF
- recast the editions table as Shannon Open Source against the Keygraph
  Enterprise Platform, stating open source is not a trial edition
- rewrite the platform overview around exhaustive agentic SAST, canonical
  findings, automated remediation, targeted verification, and governance
- add five product screenshots under assets/keygraph-platform/, referenced
  relative to docs/

* docs: add the Shannon naming section and swap in the 3.0 demo GIF

- explain the Claude Shannon information-theory origin under "What is Shannon?"
- point "Shannon in Action" at the 3.0 recording in assets/Shannon3GIF.gif

Both taken from the README half of #438.

* docs: document CI/CD integrations and the reconciled analysis pipeline

- add a CI/CD Integrations section covering the official GitHub Action and
  GitLab component, pipeline artifacts, and exploit-only severity gates
- redraw the architecture section as a Mermaid flow: agentic code analysis
  and recon feed finding reconciliation, then exploitation and reporting
- describe open-source code analysis as a multi-stage agentic workflow and
  reserve parsed-code CPGs and exhaustive verification for Enterprise
- sharpen the privacy wording: results stay local, but model requests carry
  source context to whichever endpoint you configure
- drop the "not recommended" framing on local models and add a section on
  why Shannon complements rather than replaces human pentesters
- regenerate llms-full.txt from the updated README and docs

* docs: add the Photoview benchmark across three models

- Add a "Shannon in Action" table for Photoview 2.4.0 runs on
  DeepSeek v4 Flash, Grok 4.6, and Claude Opus 5, each linking its
  PDF report and SARIF output
- Store the per-model reports under benchmark/
- Link the (forthcoming) benchmark writeup from the section intro

* docs: add the Shannon vs XBOW/Aikido Photoview benchmark writeup

- Add docs/shannon-xbow-aikido-benchmark.md with methodology, per-model
  cost/coverage tables, and links to each model's report and SARIF
- Link the writeup from the README "Shannon in Action" section

* docs: link the benchmark announcement discussion from the README

* fix(readme): restore theme-aware banner, badge, and buttons

* feat!: trigger the Shannon 3.0 major release

---------

Co-authored-by: ezl-keygraph <ezhil@keygraph.io>
This commit is contained in:
Arjun Malleswaranandezl-keygraph authored and GitHub committed 2026-09-02 14:34:26 +05:30
1 parent 6108de3cfc
commit 9767ebe633
267 files changed
+127389 -3268

No files matched your search

+231 -56
View File
@@ -14,7 +14,7 @@ import { setTimeout as sleep } from 'node:timers/promises';
import { fileURLToPath } from 'node:url';
import type { SpinnerResult } from '@clack/prompts';
import { envBool, PI_AUTH_CONTAINER_PATH } from './env.js';
import { fail } from './errors.js';
import { fail, warn } from './errors.js';
import { getMode, isDevMode } from './mode.js';
import { INTERNAL_DIR } from './paths.js';
import { runStep, spawnCaptured, surfaceOutput } from './ui.js';
@@ -27,6 +27,16 @@ const DEV_IMAGE = 'shannon-worker';
/** Docker label stamped on each worker container, mapping it back to its workspace so a single scan can be stopped by name. */
const WORKSPACE_LABEL = 'shannon.workspace';
/** Docker label that joins a worker container to the Temporal workflow polling its unique task queue. */
const TASK_QUEUE_LABEL = 'shannon.task-queue';
/** Docker label carrying the workflow ID selected before the worker starts. */
const WORKFLOW_ID_LABEL = 'shannon.workflow-id';
/** Image/container protocol proving that the worker honors the preselected workflow ID. */
const WORKER_PROTOCOL_LABEL = 'shannon.worker-protocol';
export const WORKFLOW_ID_PROTOCOL = 'workflow-id-v1';
export function getWorkerImage(version: string): string {
return getMode() === 'local' ? DEV_IMAGE : `${NPX_IMAGE_REPO}:${version}`;
}
@@ -84,9 +94,6 @@ function spawnQuiet(cmd: string, args: string[]): Promise<boolean> {
const TEMPORAL_CONTAINER = 'shannon-temporal';
const TEMPORAL_ADDRESS = 'localhost:7233';
/** Query matching every running pentest scan workflow. */
const RUNNING_SCAN_QUERY = "ExecutionStatus = 'Running' AND WorkflowType = 'pentestPipelineWorkflow'";
/** Build `docker exec` args for a `temporal` CLI command run inside the Temporal container. */
function temporalCmd(...args: string[]): string[] {
return ['exec', TEMPORAL_CONTAINER, 'temporal', ...args, '--address', TEMPORAL_ADDRESS];
@@ -116,10 +123,8 @@ export function isTemporalReady(): boolean {
return output.includes('SERVING');
}
/**
* Ensure Temporal is running via compose.
*/
export async function ensureInfra(spinner: SpinnerResult): Promise<void> {
/** Start (or find) Temporal via compose and wait until it serves; exits the process on failure. */
async function ensureTemporalHealthy(spinner: SpinnerResult): Promise<void> {
if (isTemporalReady()) {
return;
}
@@ -146,6 +151,97 @@ export async function ensureInfra(spinner: SpinnerResult): Promise<void> {
process.exit(1);
}
const DEFAULT_RETENTION_HOURS = 168;
const RETENTION_ENV = 'SHANNON_TEMPORAL_RETENTION';
const RETENTION_NAMESPACE = 'default';
/**
* Desired retention in whole hours: unset or empty env → 168 (7 days); a positive
* whole-hour override like `72h`; anything else warns and returns null (leave unchanged).
*/
function desiredRetentionHours(): number | null {
const raw = process.env[RETENTION_ENV];
if (raw === undefined || raw.trim() === '') {
return DEFAULT_RETENTION_HOURS;
}
const match = raw.trim().match(/^([1-9][0-9]*)h$/);
if (!match) {
warn(
`Ignoring invalid ${RETENTION_ENV} "${raw}" — Temporal retention left unchanged.`,
'Use a positive whole number of hours, e.g. "168h".',
);
return null;
}
return Number(match[1]);
}
/** Convert a Go duration such as "24h0m0s" or "168h" to whole seconds, or null when it doesn't parse. */
function parseGoDurationSeconds(text: string): number | null {
const match = text.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
if (!match || (match[1] === undefined && match[2] === undefined && match[3] === undefined)) {
return null;
}
const hours = Number(match[1] ?? 0);
const minutes = Number(match[2] ?? 0);
const seconds = Number(match[3] ?? 0);
return hours * 3600 + minutes * 60 + seconds;
}
/**
* Current retention of the `default` namespace in seconds, or null when it can't be read.
* `runOutput` returns '' on a failed describe, so a failed read and an unparseable one both
* collapse to null — either way the live value is unknown, which the caller handles the same way.
*/
function readCurrentRetentionSeconds(): number | null {
const output = runOutput('docker', temporalCmd('operator', 'namespace', 'describe', RETENTION_NAMESPACE));
const match = output.match(/WorkflowExecutionRetentionTtl\s+(\S+)/);
if (!match || match[1] === undefined) {
return null;
}
return parseGoDurationSeconds(match[1]);
}
/**
* Converge the `default` namespace's retention to the CLI-owned value after Temporal is
* healthy. The CLI is the authority: a manual change is replaced on the next start unless
* the operator sets the matching override. A describe or update failure warns once that the
* requested value wasn't applied and never blocks the scan.
*/
function convergeNamespaceRetention(): void {
const hours = desiredRetentionHours();
if (hours === null) {
return;
}
const currentSeconds = readCurrentRetentionSeconds();
if (currentSeconds === null) {
warn(
`Could not read Temporal retention for namespace "${RETENTION_NAMESPACE}" — the requested value (${hours}h) was not applied.`,
);
return;
}
if (currentSeconds === hours * 3600) {
return;
}
const updated = runQuiet(
'docker',
temporalCmd('operator', 'namespace', 'update', '--namespace', RETENTION_NAMESPACE, '--retention', `${hours}h`),
);
if (!updated) {
warn(`Could not update Temporal retention to ${hours}h — the requested value was not applied.`);
}
}
/**
* Ensure Temporal is running via compose, then converge its scan-history retention.
*/
export async function ensureInfra(spinner: SpinnerResult): Promise<void> {
await ensureTemporalHealthy(spinner);
convergeNamespaceRetention();
}
/**
* Build the worker image from the repository, tagged with the name this mode
* resolves at run time.
@@ -167,7 +263,10 @@ export function buildImage(noCache: boolean, version: string): void {
export function ensureImage(version: string): void {
const image = getWorkerImage(version);
const exists = runQuiet('docker', ['image', 'inspect', image]);
if (exists) return;
if (exists) {
ensureWorkerImageProtocol(image);
return;
}
if (canBuildImage()) {
console.log('Shannon image not found, building...');
@@ -185,6 +284,22 @@ export function ensureImage(version: string): void {
}
pruneOldImages(version);
}
ensureWorkerImageProtocol(image);
}
/** Refuse a stale worker image that would ignore the CLI-selected workflow ID. */
function ensureWorkerImageProtocol(image: string): void {
const protocol = runOutput('docker', [
'image',
'inspect',
image,
'--format',
`{{ index .Config.Labels "${WORKER_PROTOCOL_LABEL}" }}`,
]);
if (protocol === WORKFLOW_ID_PROTOCOL) return;
const hint = canBuildImage() ? 'Run ./shannon build, then retry.' : 'Reinstall this Shannon version, then retry.';
fail('The Shannon worker image is incompatible with this CLI.', hint);
}
/**
@@ -288,6 +403,7 @@ export interface WorkerOptions {
repo: { hostPath: string; containerPath: string };
workspacesDir: string;
taskQueue: string;
workflowId: string;
containerName: string;
envFlags: string[];
config?: { hostPath: string; containerPath: string };
@@ -310,8 +426,16 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
}
args.push('--name', opts.containerName, '--network', 'shannon-net');
// Tag with the workspace so `stop <workspace>` can target this scan's container
args.push('--label', `${WORKSPACE_LABEL}=${opts.workspace}`);
// Keep the launch identity on the container before session.json exists. The fixed workflow
// ID lets stop verify the pre-registration window without trusting visibility timing.
args.push(
'--label',
`${WORKSPACE_LABEL}=${opts.workspace}`,
'--label',
`${TASK_QUEUE_LABEL}=${opts.taskQueue}`,
'--label',
`${WORKFLOW_ID_LABEL}=${opts.workflowId}`,
);
// Add host flag for Linux
args.push(...addHostFlag());
@@ -345,7 +469,7 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
args.push('-v', `${opts.config.hostPath}:${opts.config.containerPath}:ro`);
}
// Output directory for deliverables copy
// Customer-copy destination. The workflow surfaces only final report artifacts here.
if (opts.outputDir) {
args.push('-v', `${opts.outputDir}:/app/output`);
}
@@ -358,7 +482,10 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
// Environment
args.push(...opts.envFlags);
// Container settings
// Container settings. Chromium's own sandbox needs syscalls Docker's default seccomp
// profile blocks, which is why the profile is dropped. `seccomp=unconfined` is a
// container-wide setting, not a per-process one: every process here runs unfiltered,
// the worker included — not just the browser automation that motivates it.
args.push('--shm-size', '2gb', '--security-opt', 'seccomp=unconfined');
// Image
@@ -367,6 +494,7 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
// Worker command
args.push('node', 'apps/worker/dist/temporal/worker.js', opts.url, opts.repo.containerPath);
args.push('--task-queue', opts.taskQueue);
args.push('--workflow-id', opts.workflowId);
if (opts.config) {
args.push('--config', opts.config.containerPath);
}
@@ -390,6 +518,18 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
/** `docker ps --filter` args matching every running worker container. */
export const WORKER_FILTER: readonly string[] = ['--filter', 'name=shannon-worker-'];
/** Result of a command-backed query whose unavailable state must not be mistaken for an empty result. */
export type CommandQueryResult<T> = { kind: 'ok'; value: T } | { kind: 'unavailable' };
/** Identity carried by a running scan worker container. Older workers may lack the newer labels. */
export interface RunningScanContainer {
readonly id: string;
readonly workspace?: string;
readonly taskQueue?: string;
readonly workflowId?: string;
readonly workerProtocol?: string;
}
/** `docker ps --filter` args matching one scan's worker container(s), by workspace label. */
export function scanFilter(workspace: string): readonly string[] {
return ['--filter', `label=${WORKSPACE_LABEL}=${workspace}`];
@@ -400,9 +540,85 @@ export function scanFilter(workspace: string): readonly string[] {
* the authoritative check for whether containers actually stopped — `docker stop`'s
* exit code can't distinguish "already gone" from "failed to stop".
*/
export function runningContainersChecked(filter: readonly string[]): CommandQueryResult<string[]> {
try {
const output = execFileSync('docker', ['ps', '-q', ...filter], { stdio: 'pipe', encoding: 'utf-8' }).trim();
return { kind: 'ok', value: output.split('\n').filter(Boolean) };
} catch {
return { kind: 'unavailable' };
}
}
/**
* Best-effort counterpart for callers where Docker unavailability is intentionally
* presented as no local running containers.
*/
export function runningContainers(filter: readonly string[]): string[] {
const output = runOutput('docker', ['ps', '-q', ...filter]);
return output.split('\n').filter(Boolean);
const result = runningContainersChecked(filter);
return result.kind === 'ok' ? result.value : [];
}
function normalizedLabel(value: string | undefined): string | undefined {
const normalized = value?.trim();
return normalized && normalized !== '<no value>' ? normalized : undefined;
}
/**
* Running scan containers with the labels needed to correlate a worker to its Temporal
* workflow. A successful query keeps unlabeled legacy workers in the result by ID.
*/
export function runningScanContainersChecked(
filter: readonly string[] = WORKER_FILTER,
): CommandQueryResult<RunningScanContainer[]> {
try {
const format = `{{.ID}}\t{{ index .Labels "${WORKSPACE_LABEL}" }}\t{{ index .Labels "${TASK_QUEUE_LABEL}" }}\t{{ index .Labels "${WORKFLOW_ID_LABEL}" }}\t{{ index .Labels "${WORKER_PROTOCOL_LABEL}" }}`;
const output = execFileSync('docker', ['ps', ...filter, '--format', format], {
stdio: 'pipe',
encoding: 'utf-8',
}).trim();
if (!output) return { kind: 'ok', value: [] };
const containers: RunningScanContainer[] = [];
for (const line of output.split('\n')) {
const [rawId, rawWorkspace, rawTaskQueue, rawWorkflowId, rawWorkerProtocol] = line.split('\t');
const id = rawId?.trim();
if (!id) return { kind: 'unavailable' };
const workspace = normalizedLabel(rawWorkspace);
const taskQueue = normalizedLabel(rawTaskQueue);
const workflowId = normalizedLabel(rawWorkflowId);
const workerProtocol = normalizedLabel(rawWorkerProtocol);
containers.push({
id,
...(workspace !== undefined && { workspace }),
...(taskQueue !== undefined && { taskQueue }),
...(workflowId !== undefined && { workflowId }),
...(workerProtocol !== undefined && { workerProtocol }),
});
}
return { kind: 'ok', value: containers };
} catch {
return { kind: 'unavailable' };
}
}
/**
* Workspace names of every running worker container, read from the shannon.workspace
* label each scan is stamped with at spawn. The checked form preserves Docker query
* failures so lifecycle commands do not mistake an unavailable daemon for an empty list.
*/
export function runningScanWorkspacesChecked(): CommandQueryResult<string[]> {
const result = runningScanContainersChecked();
if (result.kind === 'unavailable') return result;
return {
kind: 'ok',
value: result.value.flatMap((container) => (container.workspace === undefined ? [] : [container.workspace])),
};
}
/** Best-effort counterpart for callers that only need the local scan list. */
export function runningScanWorkspaces(): string[] {
const result = runningScanWorkspacesChecked();
return result.kind === 'ok' ? result.value : [];
}
/**
@@ -414,47 +630,6 @@ export async function stopContainers(ids: string[]): Promise<void> {
await Promise.all(ids.map((id) => spawnQuiet('docker', ['stop', id])));
}
/**
* Terminate a Temporal workflow so a stopped scan doesn't linger as a running
* workflow with no worker. Best-effort: returns false if Temporal is unreachable
* or the workflow already closed. Requires Temporal to be up (guard with isTemporalReady).
*/
export function terminateWorkflow(workflowId: string, reason: string): boolean {
return runQuiet('docker', temporalCmd('workflow', 'terminate', '--workflow-id', workflowId, '--reason', reason));
}
/**
* Terminate every running pentest workflow in one batch, so `stop --all` doesn't
* leave workflows running with no worker. Best-effort: returns false if Temporal
* is unreachable. Requires Temporal to be up (guard with isTemporalReady).
*/
export function terminateAllWorkflows(reason: string): boolean {
return runQuiet(
'docker',
temporalCmd('workflow', 'terminate', '--query', RUNNING_SCAN_QUERY, '--reason', reason, '--yes'),
);
}
/**
* Whether a specific workflow is still in the Running state. Re-querying this after
* a terminate verifies it actually took effect, rather than trusting the terminate
* command's exit code. Requires Temporal to be up (guard with isTemporalReady).
*/
export function isWorkflowRunning(workflowId: string): boolean {
const query = `WorkflowId = '${workflowId}' AND ExecutionStatus = 'Running'`;
const output = runOutput('docker', temporalCmd('workflow', 'list', '--query', query));
return output.includes(workflowId);
}
/**
* Whether any pentest scan workflow is still Running — the `stop --all` counterpart
* to isWorkflowRunning. Requires Temporal to be up (guard with isTemporalReady).
*/
export function anyRunningScanWorkflow(): boolean {
const output = runOutput('docker', temporalCmd('workflow', 'list', '--query', RUNNING_SCAN_QUERY));
return output.includes('pentestPipelineWorkflow');
}
/**
* Tear down the compose stack. When `clean` is set, volumes are removed too.
*/