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.
This commit is contained in:
ajmallesh
2026-08-26 20:19:41 -07:00
parent 85d5cbd657
commit 2469e6deac
97 changed files with 385 additions and 136 deletions
+6 -6
View File
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Mario Zechner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9
View File
@@ -39,6 +39,7 @@ It analyzes your source code, identifies attack paths, and executes real exploit
- [Documentation](#documentation)
- [Safety, Scope, and Limitations](#safety-scope-and-limitations)
- [License](#license)
- [Acknowledgements](#acknowledgements)
- [About Keygraph](#about-keygraph)
- [Community and Support](#community-and-support)
- [Common Questions](#common-questions)
@@ -230,6 +231,14 @@ Commercial and enterprise licensing is available for organizations that need dif
For commercial licensing, contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
## Acknowledgements
Thanks to [Pi](https://github.com/earendil-works/pi),
[Playwright CLI](https://github.com/microsoft/playwright-cli),
and [Mantis](https://github.com/google/mantis).
See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for licensing and attribution details.
## About Keygraph
**Keygraph** is the company behind Shannon. It also builds the **Keygraph platform**, the commercial agentic pentesting product that closes the full AppSec lifecycle and runs an enhanced build of Shannon as its pentesting engine.
+18 -8
View File
@@ -6,6 +6,17 @@ Shannon as a whole is distributed under the GNU Affero General Public License,
version 3.0 (see LICENSE). Third-party material incorporated into Shannon
remains subject to the attribution and notice requirements of its own license.
## Pi
Shannon uses Pi as part of its agent framework.
Project: https://github.com/earendil-works/pi
License: MIT
Copyright (c) 2025 Mario Zechner
The applicable license is reproduced at `LICENSES/MIT-Pi.txt`.
## Mantis
Portions of Shannon's Capella agentic SAST implementation, specifically the
@@ -24,13 +35,12 @@ reside under:
- apps/worker/prompts/partials/ (capella-*.hbs prompt partials)
- apps/worker/prompts/sast/capella/ (prompt templates)
The pinned upstream tree contains no NOTICE file, so no upstream NOTICE text is
reproduced here. The upstream LICENSE carries no copyright notice of its own, so
none is reproduced.
The Mantis-derived material has been substantially modified by Keygraph
for use within Shannon, including adaptation to Shannon's agent
architecture and the Pi agent framework.
The Mantis-derived material has been substantially modified by Keygraph for
Shannon. Material changes include adaptation to Shannon's agent architecture,
enforcing repository-relative paths and complete verdict sets, and providing
separate production and pipeline-testing prompt variants.
Copyright and attribution notices from the original Mantis material
remain the property of their respective copyright holders.
Modifications: Copyright © 2026 Keygraph, Inc.
Modifications:
Copyright © 2026 Keygraph, Inc.
+1 -1
View File
@@ -148,10 +148,10 @@ export function tailUntilComplete(logFile: string, opts: TailOptions = {}): Prom
return new Promise((resolve) => {
let position = 0;
const completion = new LogCompletionState();
const completionDecoder = new StringDecoder('utf8');
let done = false;
const controller = new AbortController();
let watcher: ReturnType<typeof watch> | undefined;
const completionDecoder = new StringDecoder('utf8');
/** Output any new content appended since the last read. */
function flush(): boolean {
+3 -2
View File
@@ -448,8 +448,9 @@ export function spawnWorker(opts: WorkerOptions): ChildProcess {
args.push(...opts.envFlags);
// Container settings. Chromium's own sandbox needs syscalls Docker's default seccomp
// profile blocks, so it is loosened for the in-container browser automation; the
// worker process itself is not granted any extra privilege by this.
// 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+26 -2
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -19,6 +19,13 @@
*
* Resolution returns a pi `Model` plus the `ModelRuntime` that owns its auth,
* built over an in-memory credential store primed from the environment.
*
* The CLI cannot import this module (it ships as a separate bundle), so
* `apps/cli/src/model-spec.ts` mirrors the parse rule and the provider/credential
* tables by hand for its own `status` rendering and setup wizard. The two copies
* have no shared compile-time link: a provider added or renamed on one side and
* not the other does not fail to build, it just makes the CLI's guidance or
* guard rails disagree with what the worker actually accepts at runtime.
*/
import { existsSync } from 'node:fs';
@@ -30,6 +37,11 @@ import { getAgentDir, ModelRuntime } from '@earendil-works/pi-coding-agent';
* Providers Shannon curates with their own credential variables, config sections,
* and setup flows. Each is a pi-ai provider id; any other pi provider is still
* reachable through the generic credential path below.
*
* Kept identical to the CLI's own copy of this list (`apps/cli/src/model-spec.ts`),
* which the CLI uses to decide whether "only one provider is configured" and to
* gate its "Other provider" setup option. A curated provider missing from one
* copy is silently treated as generic on that side.
*/
export const CURATED_PROVIDERS = ['anthropic', 'openai', 'xai', 'amazon-bedrock'] as const;
@@ -47,6 +59,11 @@ export const GENERIC_API_KEY_ENV = 'SHANNON_AI_API_KEY';
* does not invent credential names — these are the variables each provider's own
* tooling uses. Bedrock pairs its bearer token with AWS_REGION, which is provider
* config rather than a credential.
*
* Mirrored by the CLI's own table of the same name, used there to decide which
* env vars to forward into the worker container. A variable added here without
* its CLI counterpart never reaches the container: the worker looks for a
* credential the CLI never forwarded, and preflight reports it as absent.
*/
export const PROVIDER_API_KEY_ENV: Readonly<Record<CuratedProviderId, readonly string[]>> = {
anthropic: ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'],
@@ -335,11 +352,18 @@ export async function resolveModelSelection(): Promise<ModelSelection> {
);
}
let credentialSource: ModelSelection['credentialSource'] = 'ambient';
if (mountedPiAuth) {
credentialSource = 'pi-auth';
} else if (credentials.apiKey) {
credentialSource = 'api-key';
}
return {
model,
modelRuntime,
modelId,
providerId,
credentialSource: mountedPiAuth ? 'pi-auth' : credentials.apiKey ? 'api-key' : 'ambient',
credentialSource,
};
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -38,6 +38,9 @@ const MAX_TOOLS_PER_SESSION = 32;
const MAX_TURNS_PER_SESSION = 1_000;
const MAX_TIMEOUT_MS = 24 * 60 * 60 * 1_000;
// The closed set of stage-specific tools a caller is allowed to hand in alongside the confined
// repository tools. Anything not on this list, and not a repository tool, is rejected as unknown
// by validateCallerTools below.
const CAPELLA_COLLECTOR_TOOL_NAMES = new Set([
'report_finding',
'record_duplicates',
@@ -47,6 +50,12 @@ const CAPELLA_COLLECTOR_TOOL_NAMES = new Set([
'record_calibration',
]);
// A Capella stage reasons over a read-only, confined view of the repository; none of these may
// ever be offered to it. `bash`/`shell`/`network`/`browser`/`web_search` would give it an escape
// hatch out of the confined tool set entirely; `edit`/`write` would let a review agent change the
// code it is meant to only analyze; `task` would let it spawn further sessions outside this
// executor's bounded turn/timeout accounting; `glob`/`ls`/`todo`/`todo_write` duplicate tools the
// stage already gets from the confined factory or has no use for.
const FORBIDDEN_TOOL_NAMES = new Set([
'bash',
'browser',
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+17 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -13,6 +13,9 @@ import path from 'node:path';
import { ArtifactIntegrityError, ReconciliationIoError } from '../reconciliation/artifact-store.js';
const JAIL_PREFIX = 'shannon-task-formation-';
// Never copied into the model-readable jail: `.git` carries deliverables history, `.shannon` holds
// scan internals, and `.pi` holds provider credentials. Any of these reaching the jail would expose
// them to the tools the model drives. The post-copy verification re-checks their absence by name.
const ALWAYS_EXCLUDED_NAMES = Object.freeze(['.git', '.shannon', '.pi'] as const);
export interface SourceJailOptions {
@@ -44,6 +47,8 @@ function checkCancellation(signal: AbortSignal | undefined): void {
if (signal?.aborted === true) throw cancellationError(signal);
}
// Path-confinement predicate: true only when `candidate` is `root` itself or lies beneath it.
// A relative path that escapes upward (`..`) or is absolute means the candidate is outside the root.
function isWithin(root: string, candidate: string): boolean {
const relativePath = path.relative(root, candidate);
return (
@@ -76,6 +81,8 @@ async function relativeExclusion(
if (relativePath === undefined) return undefined;
if (relativePath === '') {
// An exclusion that resolves to the whole root would empty the jail. Fail closed rather than
// copy nothing and hand the model an empty tree.
throw new ArtifactIntegrityError('A task-formation exclusion resolves to the complete source root');
}
return relativePath;
@@ -119,11 +126,16 @@ async function copySourceTree(
throw new ReconciliationIoError('Unable to enumerate the task-formation source tree');
}
// Cancellation is checked before every top-level entry and inside the copy filter so an aborted
// scan stops promptly instead of copying a whole large tree first.
for (const entry of entries) {
checkCancellation(signal);
const source = path.join(sourceRoot, entry.name);
const destinationEntry = path.join(destination, entry.name);
try {
// verbatimSymlinks copies links as links rather than following them, so a link pointing
// outside the tree cannot pull external content in; the filter then drops any path that
// resolves outside the root, plus the always- and dynamically-excluded paths.
await cp(source, destinationEntry, {
recursive: true,
verbatimSymlinks: true,
@@ -183,6 +195,9 @@ async function assertDynamicExclusionsAbsent(
}
}
// Re-verify the copied tree independently of the copy filter: the jail root must be a real
// directory (not a symlink), and no excluded name or protected workspace path may survive. This
// catches a filter gap or a race during the copy before the model is allowed to read the tree.
async function verifyJail(
directory: string,
dynamicExclusions: readonly string[],
@@ -247,6 +262,7 @@ export async function materializeSourceJail(options: SourceJailOptions): Promise
} catch {
throw new ReconciliationIoError('Unable to resolve the task-formation temporary root');
}
// A temp root inside the source tree would make the copy try to copy the jail into itself.
if (isWithin(sourceRoot, tempRoot)) {
throw new ArtifactIntegrityError('The task-formation temporary root cannot be inside the source tree');
}
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -17,6 +17,10 @@ import { type CapturedSubmitTool, createGenericSubmitTool } from '../submit-tool
const ZERO_USAGE = { inputTokens: 0, outputTokens: 0, costUsd: 0 } as const;
// True only when this caller's own signal aborted and the error traces back to it. Walk a bounded,
// cycle-guarded cause chain so a cancellation wrapped several layers deep is still recognized as a
// cancellation and not misreported as a provider error. Without the `signal.aborted` gate an
// unrelated AbortError from the provider could be mistaken for our cancellation.
function isSignalCancellation(error: unknown, signal: AbortSignal | undefined): boolean {
if (signal?.aborted !== true) return false;
@@ -109,6 +113,8 @@ async function generate(host: ModelHost, request: StructuredGenerationRequest):
};
}
if (response.stopReason === 'aborted') {
// An abort with our signal set is a real cancellation. An abort without it is a provider-side
// stop we did not ask for, so classify it as an error the caller can retry on.
if (request.signal?.aborted === true) {
return { stopReason: 'aborted', toolCalls: [], usage: responseUsage(response) };
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+10 -2
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -9,6 +9,13 @@
import { ProgressIndicator } from '../progress-indicator.js';
import { extractAgentType } from '../utils/formatting.js';
/**
* `useCleanOutput` marks the phases that use the friendly "Running X..."
* spinner plus a one-line completion message (pre-recon, recon, report, and
* the vuln/exploit agents) as opposed to the verbose turn-by-turn fallback
* formatting used elsewhere. `createProgressManager` reads it to decide
* between a real spinner and the silent null one.
*/
export interface ProgressContext {
description: string;
useCleanOutput: boolean;
@@ -62,7 +69,8 @@ class NullProgressManager implements ProgressManager {
}
}
// Returns no-op when disabled
// Returns no-op when disabled. `disableLoader` lets a caller force the silent manager regardless
// of useCleanOutput, for a context where an animated spinner would be unwanted no matter the phase.
export function createProgressManager(context: ProgressContext, disableLoader: boolean): ProgressManager {
if (!context.useCleanOutput || disableLoader) {
return new NullProgressManager();
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -267,6 +267,9 @@ export async function runReviewStage(
);
let usage = primary.usage;
let salvagedTurnLimitCount = primary.salvagedTurnLimit ? 1 : 0;
// One bounded repair pass, scoped to only the findings the primary session skipped: this
// recovers a session that ran out of turns or omitted a few findings without re-running the
// full finding set, which would double the cost of every unaffected verdict alongside it.
const missing = missingFindings(source.findings, collector.getAcceptedIds());
if (missing.length > 0) {
const repair = await runCollectorSession(
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -717,6 +717,9 @@ export function createReconCollector(): ReconCollector {
'The renderer sorts by (path, method) before rendering, so emission order does not affect output.',
parameters: AddEndpointsInputSchema,
async execute(_toolCallId, input) {
// Unlike the one-shot set_* tools, repeated calls here are expected (the agent splits a
// large inventory across several), so a repeated (method, path) pair is silently skipped
// as a no-op rather than rejected as a DuplicateError.
addEndpointsCalls += 1;
const added: string[] = [];
const skipped: string[] = [];
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+3 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -404,6 +404,8 @@ export function createVulnCollector(vulnClass: VulnClass): VulnCollector {
parameters: intelSchema,
async execute(_toolCallId, input) {
if (state.strategic_intelligence) return alreadyCalled('set_strategic_intelligence');
// Safe: intelSchema was selected from STRATEGIC_INTEL_SCHEMAS by this collector's own
// vulnClass, so cleanInput's output shape always matches one arm of the union below.
state.strategic_intelligence = cleanInput(intelSchema, input) as unknown as StrategicIntelligenceInput;
return successResult({ set: 'set_strategic_intelligence' });
},
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+2
View File
@@ -93,4 +93,6 @@ function findRepoRoot(): string {
}
const REPO_ROOT = findRepoRoot();
/** Default root for named scan workspaces; each session's audit directory nests under here. */
export const WORKSPACES_DIR = path.join(REPO_ROOT, 'workspaces');
+3 -2
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -36,7 +36,8 @@ export class ProgressIndicator {
this.interval = null;
}
// Clear the spinner line
// Clear the spinner line: overwrite with spaces at least as wide as the last frame
// written (message plus the spinner glyph and separator), then return the cursor home.
process.stdout.write(`\r${' '.repeat(this.message.length + 5)}\r`);
this.isRunning = false;
}
+10 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -12,6 +12,11 @@
* Generates a TOTP code for the target's MFA.
* Based on RFC 6238 (TOTP) and RFC 4226 (HOTP).
*
* The login flow prompt has the agent run this via the `bash` tool with the TOTP secret
* substituted in, rather than asking the model to work out HOTP/TOTP arithmetic itself.
* The secret is only ever held in memory here; nothing is written to disk, and the
* result is emitted as JSON on stdout for the caller to parse.
*
* Usage:
* generate-totp --secret JBSWY3DPEHPK3PXP
*/
@@ -64,7 +69,10 @@ function generateHOTP(secret: string, counter: number, digits: number = 6): stri
hmac.update(counterBuffer);
const hash = hmac.digest();
// Dynamic truncation (SHA-1 always produces 20 bytes)
// Dynamic truncation (SHA-1 always produces 20 bytes). The low nibble of the last byte
// picks a 4-byte window anywhere in the hash; masking the top bit of that window's first
// byte (0x7f) keeps the result a positive 31-bit int per RFC 4226, regardless of JS's
// signed 32-bit bitwise operators.
const lastByte = hash[hash.length - 1] ?? 0;
const offset = lastByte & 0x0f;
const code =
+5 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -107,6 +107,10 @@ function main(): void {
process.exit(1);
}
// The --type argument only ever resolves to one of the fixed filenames in
// DELIVERABLE_FILENAMES; there is no path taking an agent-supplied filename directly
// to disk. This keeps the deliverables directory contents fully predictable regardless
// of what an agent passes for --type.
const deliverableType = args.type as DeliverableType;
const filename = DELIVERABLE_FILENAMES[deliverableType];
+5 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -58,6 +58,8 @@ function getFlag(argv: string[], flag: string): string | undefined {
return undefined;
}
// Reads the existing report.json (if any) so report_meta can be merged in without
// disturbing the findings array that other invocations of the report agent's tools append to.
function readReportFile(filePath: string): ReportFile {
if (!existsSync(filePath)) {
return { findings: [] };
@@ -66,6 +68,8 @@ function readReportFile(filePath: string): ReportFile {
return JSON.parse(raw) as ReportFile;
}
// Temp file + rename so a crash mid-write never leaves report.json truncated; the
// finding-collector's own writes to this file rely on the same guarantee.
function writeReportFile(filePath: string, data: ReportFile): void {
const tmpPath = `${filePath}.tmp`;
const payload = JSON.stringify(data, null, 2);
+6 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -90,6 +90,9 @@ function errorCodeFromResult(result: PiPromptResult): ErrorCode {
return ErrorCode.AGENT_EXECUTION_FAILED;
}
// Only the codes this service can itself raise from execution failure get a specific category;
// every other code (including provider-classified ones) falls through to 'validation' because
// this path only runs for a failed agent attempt, never for a config or preflight error.
function categoryForErrorCode(code: ErrorCode): PentestErrorType {
switch (code) {
case ErrorCode.GIT_CHECKPOINT_FAILED:
@@ -109,6 +112,8 @@ function gitFailureForAgent(
error: Error | undefined,
code: ErrorCode = ErrorCode.GIT_CHECKPOINT_FAILED,
): PentestError {
// An unclassified git failure is assumed transient (filesystem contention, not a permanent
// fault), so it does not cost the class its one shot at succeeding on retry.
const retryable = error instanceof PentestError ? error.retryable : true;
const message = error?.message ?? 'unknown git failure';
return new PentestError(
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -33,6 +33,11 @@ export interface RenumberErrorDetails {
readonly [key: string]: unknown;
}
/**
* Integrity failure shared by exact-path publication and the renumber/compaction transforms.
* `retryable` drives the Temporal wrapper's retry decision, and `details.checkCode` is a stable
* machine-readable identifier; both are part of the durable error contract.
*/
export class RenumberError extends Error {
readonly retryable: boolean;
readonly type: RenumberErrorType;
@@ -56,6 +61,7 @@ export interface ExactOutputFile {
export interface ExactOutputCommit {
readonly commitHash: string;
readonly changedPaths: readonly string[];
/** True when HEAD already held every declared byte and the existing commit was adopted. */
readonly alreadyCommitted: boolean;
}
@@ -71,6 +77,9 @@ function isErrno(error: unknown, code: string): boolean {
return error instanceof Error && (error as NodeJS.ErrnoException).code === code;
}
// WARNING: a symlink at a declared output path would redirect the atomic write outside the
// deliverables repo, so publication refuses to write through one. ENOENT is fine: the path
// simply has not been written yet.
async function rejectSymlinks(dir: string, relPaths: readonly string[]): Promise<void> {
for (const relPath of relPaths) {
try {
@@ -107,6 +116,11 @@ async function executeExactGitCommand(
}
}
/**
* Realign only the declared paths with HEAD, in both the worktree and the index: paths present
* at HEAD are restored, paths absent at HEAD are unstaged and deleted. Sibling files are never
* touched, so a failed publication cannot discard a concurrent agent's staged or dirty work.
*/
async function repairExactPathsFromHead(dir: string, relPaths: readonly string[]): Promise<void> {
const presentPaths: string[] = [];
const absentPaths: string[] = [];
@@ -141,6 +155,13 @@ async function repairExactPathsFromHead(dir: string, relPaths: readonly string[]
}
}
/**
* Commit the declared files through a scratch index seeded from HEAD's tree, so entries staged
* in the real index can never leak into the commit and a failed attempt leaves the real index
* untouched. Publication routes here whenever the file set declares a deletion.
* `update-ref HEAD <new> <old>` is a compare-and-swap: it fails instead of clobbering HEAD if
* anything else advanced the branch after the tree was read.
*/
async function commitExactFilesWithTemporaryIndex(
dir: string,
files: readonly ExactOutputFile[],
@@ -178,6 +199,9 @@ async function commitExactFilesWithTemporaryIndex(
dir,
'advancing HEAD to the exact-output commit',
);
// The commit was built in the scratch index, so the real index still reflects the old HEAD.
// Re-sync just the declared paths there; otherwise later status and commit calls would see
// phantom changes for files this commit already settled.
for (const file of files) {
if (file.contents === null) {
const listed = await executeExactGitCommand(
@@ -207,7 +231,16 @@ async function commitExactFilesWithTemporaryIndex(
}
}
/** Exact-path, lost-acknowledgement-safe publication used by both transforms. */
/**
* Publish an exact file set as one commit whose changed paths equal exactly the declared
* deltas, leaving every sibling path alone.
*
* The call is safe to re-drive after a lost acknowledgement: when HEAD already holds every
* declared byte it repairs worktree drift and adopts the existing state (`alreadyCommitted`)
* instead of creating a second commit. Committed bytes are re-read and verified before the
* result is returned, so a caller never acknowledges a publication the repo does not hold.
* On any failure the declared paths are rolled back to HEAD.
*/
export async function writeAndCommitExactFiles(
dir: string,
files: readonly ExactOutputFile[],
@@ -278,6 +311,8 @@ export async function writeAndCommitExactFiles(
throw new RenumberError('key-set-divergence', false, { checkCode: 'changed-path-set-mismatch' });
}
await options.afterCommit?.(committed);
// Verify the committed bytes before returning: acknowledgement must follow proof, and a
// mismatch here rolls back and surfaces as terminal rather than as a lying success.
for (const file of files) {
const verified = await readCommittedFile(dir, file.relPath);
const matches =
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+4 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -180,6 +180,9 @@ export async function withGitRepoLock<T>(operation: () => Promise<T>): Promise<T
}
}
// Git has no structured exit code for "another process holds the lock", so the only signal
// available is these known substrings in stderr. Matching here is scoped to lock contention only,
// never used to infer anything else about a command's outcome.
const GIT_LOCK_ERROR_PATTERNS = [
'index.lock',
'unable to lock',
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+4 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -162,6 +162,9 @@ async function validateConfig(configPath: string, logger: ActivityLogger): Promi
// === code_path Existence Validation ===
// .shannon/ holds this scan's own internal bookkeeping (deliverables, logs, checkpoints), not
// source the operator meant to scope. Without this exclusion, a code_path rule could match a
// path Shannon itself created rather than a real entry in the target repo.
const CODE_PATH_IGNORE = ['.git/**', '.shannon/**'];
async function patternMatchesAny(repoPath: string, pattern: string): Promise<boolean> {
+9 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -170,6 +170,10 @@ interface IncludeReplacement {
}
// Pure function: Build complete login instructions from config
//
// Username, password, TOTP secret, and email-login credentials are substituted directly into
// the returned string, which only ever lives in process memory on its way into the prompt sent
// to the model. Nothing in this function writes credentials to a file.
async function buildLoginInstructions(
authentication: Authentication,
logger: ActivityLogger,
@@ -306,6 +310,10 @@ function replaceLiteral(input: string, pattern: RegExp | string, replacement: st
return input.replace(pattern, () => replacement);
}
// Deliberately omits password, TOTP secret, and email-login credentials: this block is
// background context for the agent's prompt header, not the login mechanism itself. The
// actual secret values are only ever interpolated into {{LOGIN_INSTRUCTIONS}} via
// buildLoginInstructions, so a secret is never duplicated into this second location.
function buildAuthContext(config: DistributedConfig | null): string {
if (!config?.authentication) {
return 'No authentication configured - unauthenticated testing only';
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+4 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -176,6 +176,9 @@ function buildMessageMarkdown(finding: AddFindingInput): string {
parts.push('', '**Remediation**', '', finding.remediation);
// Exploitation steps and proof of impact are deliberately absent: SARIF has no structural home
// for them, and flattening them into prose would imply this file carries the evidence.
// NOTE: the filename below is not imported from paths.ts (FINAL_REPORT_PDF_FILENAME) because
// it is customer-facing prose, not a path this module reads or writes. It must be kept in sync
// by hand if that constant's value ever changes.
parts.push('', 'Full exploitation evidence: `Security-Assessment-Report.pdf`');
return parts.join('\n');
}
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -125,6 +125,12 @@ export async function validateAuthentication(
loginType: authentication.login_type,
});
// This is the one place in the pipeline that performs a real login and persists the resulting
// browser session (cookies/storage) to disk, so downstream agents can reuse it instead of
// logging in again. Remove any file left by a prior attempt first: verifySavedAuthState below
// trusts the file's mere presence as proof this run's login succeeded, so a stale leftover
// would let a failed attempt look like a success. The file itself is deleted again when the
// workflow ends, so an authenticated session never survives between scans.
const stateFile = authStateFile(auditSession.sessionMetadata);
await rm(stateFile, { force: true });
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+6 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -62,6 +62,11 @@ const REMEDIATION_HINTS: Record<string, string> = {
PipelineFailedError: 're-run the same -w to retry from the last checkpoint.',
};
/**
* Every message a terminal scan failure can show. Closed on purpose: an ApplicationFailure's
* own `.message` can carry raw activity or provider detail, so it is never surfaced directly.
* A type absent from this record falls back to one generic sentence instead.
*/
const SAFE_WORKFLOW_FAILURE_MESSAGES: Readonly<Record<string, string>> = {
AuthenticationError: 'Provider authentication failed.',
ConfigurationError: 'The scan configuration is invalid.',
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+3 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -61,7 +61,9 @@ export type VulnType = VulnClass;
* Decision returned by queue validation for exploitation phase.
*/
export interface ExploitationDecision {
/** True when the class's exploitation queue has at least one vulnerability to process. */
shouldExploit: boolean;
/** Currently always false; queue validation failures are surfaced as thrown errors instead. */
shouldRetry: boolean;
vulnerabilityCount: number;
vulnType: VulnType;
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+4 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -8,6 +8,9 @@
* Configuration type definitions
*/
// Every variant but `code_path` scopes network requests (URL/method/header/parameter matching).
// `code_path` is enforced by a different mechanism entirely: it becomes a permission-system deny
// rule so an avoided path is blocked from every tool and child session, not just outbound traffic.
export type RuleType = 'url_path' | 'subdomain' | 'domain' | 'method' | 'header' | 'parameter' | 'code_path';
export interface Rule {
+5 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -8,6 +8,10 @@
* Deliverable Type Definitions
*
* Maps deliverable types to their filenames for the save-deliverable CLI.
*
* This covers only the phases whose pipeline-testing prompts write a markdown deliverable
* by invoking that CLI directly. Exploitation and reporting have no entries here because their
* structured output goes through the collector tool contract instead, in every prompt mode.
*/
export enum DeliverableType {
+3 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -63,6 +63,8 @@ export function isProviderFailureCategory(value: unknown): value is ProviderFail
}
export interface ProviderFailure {
// The exhaustive set of shapes a model-provider failure can take at this boundary: bad
// credentials, bad provider/model configuration, or everything else the agent run raised.
readonly type: 'AuthenticationError' | 'ConfigurationError' | 'AgentExecutionError';
readonly category: ProviderFailureCategory;
readonly retryable: boolean;
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
+4 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -51,6 +51,9 @@ export class SessionMutex {
// 4. Return unlock that releases the next waiter in the chain
return () => {
// Only delete the map entry if it still points at this call's own promise. If a later
// lock() has already replaced it, this unlock is not the current tail, so leave the
// newer entry alone; deleting unconditionally would drop a still-pending waiter.
if (this.locks.get(sessionId) === promise) {
this.locks.delete(sessionId);
}
+7 -3
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -32,6 +32,8 @@ export async function ensureDirectory(dirPath: string): Promise<void> {
* Guarantees no partial writes or corruption on crash
*/
export async function atomicWrite(filePath: string, data: object | string): Promise<void> {
// The temp file must sit next to filePath so the rename below stays on one filesystem;
// POSIX only guarantees rename() is atomic within a single filesystem, not across mounts.
const tempPath = `${filePath}.tmp`;
const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
@@ -39,10 +41,12 @@ export async function atomicWrite(filePath: string, data: object | string): Prom
// Write to temp file
await fs.writeFile(tempPath, content, 'utf8');
// Atomic rename (POSIX guarantee: atomic on same filesystem)
// Atomic rename (POSIX guarantee: atomic on same filesystem). A reader can only ever
// observe the old complete file or the new complete file, never a truncated write.
await fs.rename(tempPath, filePath);
} catch (error) {
// Clean up temp file on failure
// Clean up temp file on failure. Errors here are swallowed so the original
// write/rename failure is what propagates, not a secondary cleanup failure.
try {
await fs.unlink(tempPath);
} catch {
+5 -2
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -44,7 +44,10 @@ export function calculatePercentage(part: number, total: number): number {
}
/**
* Extract agent type from description string for display purposes
* Extract agent type from description string for display purposes.
* Matches on literal substrings from agent display names, so it is cosmetic only:
* a miss just falls through to the generic 'analysis' label instead of affecting
* any pipeline behavior.
*/
export function extractAgentType(description: string): string {
if (description.includes('Pre-recon')) {
+5 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -16,6 +16,10 @@ type PipelineFunction = (x: any) => any | Promise<any>;
/**
* Async pipeline that passes result through a series of functions.
* Clearer than reduce-based pipe and easier to debug.
*
* There is no per-step try/catch: a thrown error or rejected promise from any function
* stops the pipeline immediately and propagates to the caller, skipping the remaining
* functions. Callers that need partial-failure handling must do it inside a step.
*/
export async function asyncPipe<TResult>(initial: unknown, ...fns: PipelineFunction[]): Promise<TResult> {
let result = initial;
+7 -1
View File
@@ -1,4 +1,4 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
@@ -6,6 +6,12 @@
import { glob } from 'zx';
/**
* Whether a code_path rule value contains wildcard syntax (e.g. `*`, `**`, `{a,b}`) rather
* than naming a single literal path. Used to tag rules as `[GLOB]` vs `[FILE]` when rendering
* them into agent prompts and permission-system deny config, since the two need different
* matching behavior downstream.
*/
export function isGlobPattern(value: string): boolean {
return glob.isDynamicPattern(value);
}
+4 -1
View File
@@ -1,9 +1,10 @@
// Copyright (C) 2025 Keygraph, Inc.
// Copyright (C) 2026 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.
/** Wall-clock timer. Safe to call duration() before stop() to read elapsed time so far. */
export class Timer {
name: string;
startTime: number;
@@ -19,6 +20,8 @@ export class Timer {
return this.duration();
}
// Falls back to the current time when the timer hasn't been stopped yet, so callers
// can poll an in-flight timer without needing to stop it first.
duration(): number {
const end = this.endTime || Date.now();
return end - this.startTime;
+1 -1
View File
@@ -144,7 +144,7 @@ report:
sarif: "false"
```
Each finding becomes one SARIF result, filed under a rule per vulnerability class (`shannon/injection`, `shannon/xss`, `shannon/auth`, `shannon/authz`, `shannon/ssrf`, and `shannon/other` for findings outside those classes) and tagged with its OWASP Top Ten 2025 category. Results are anchored to the code location the analysis phase recorded, falling back to the HTTP entry point when the finding names no file. Severity maps onto SARIF's three levels: `critical` and `high` become `error`, `medium` becomes `warning`, everything else becomes `note`.
Each finding becomes one SARIF result, filed under a rule per vulnerability class (`shannon/injection`, `shannon/xss`, `shannon/auth`, `shannon/authz`, `shannon/ssrf`, and `shannon/miscellaneous` for findings outside those classes) and tagged with its OWASP Top Ten 2025 category. Results are anchored to the code location the analysis phase recorded, falling back to the HTTP entry point when the finding names no file. Severity maps onto SARIF's three levels: `critical` and `high` become `error`, `medium` becomes `warning`, everything else becomes `note`.
If the SARIF log cannot be written, the JSON and Markdown reports are still produced and the scan finishes as "partial".
+7 -5
View File
@@ -57,12 +57,14 @@ The target repository is mounted read-only inside the worker container.
Monitor progress:
```bash
npx @keygraph/shannon logs <workspace>
npx @keygraph/shannon status <workspace>
npx @keygraph/shannon logs [<workspace>] # defaults to the single running scan, else the most recent
npx @keygraph/shannon status [<workspace>] # same default target; add --json for a machine-readable snapshot
npx @keygraph/shannon scans
npx @keygraph/shannon version
```
With no workspace, `logs` and `status` follow the single running scan; when several are running, name one.
Source-build equivalents:
```bash
@@ -89,7 +91,7 @@ open http://localhost:8233
Stop Shannon:
```bash
npx @keygraph/shannon stop <workspace> # stop one scan (confirms first; add --yes/-y to skip)
npx @keygraph/shannon stop [<workspace>] # stop one scan (defaults to the single running scan; confirms first; add --yes/-y to skip)
npx @keygraph/shannon stop --all # stop all scans (Temporal stays up)
npx @keygraph/shannon reset # stop everything and wipe all Temporal data (type 'confirm' to proceed; cannot be skipped)
```
@@ -97,7 +99,7 @@ npx @keygraph/shannon reset # stop everything and wipe all Temporal
Source-build equivalents:
```bash
./shannon stop <workspace> # stop one scan (confirms first; add --yes/-y to skip)
./shannon stop [<workspace>] # stop one scan (defaults to the single running scan; confirms first; add --yes/-y to skip)
./shannon stop --all # stop all scans (Temporal stays up)
./shannon reset # stop everything and wipe all Temporal data (type 'confirm' to proceed; cannot be skipped)
```
@@ -120,7 +122,7 @@ 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
# List completed scans.
# List running and completed scans.
npx @keygraph/shannon scans
```
+1 -1
View File
@@ -36,7 +36,7 @@ Resume an auto-named workspace:
npx @keygraph/shannon start -u https://example.com -r /path/to/repo -w example-com_shannon-1771007534808
```
List completed scans:
List running and completed scans:
```bash
npx @keygraph/shannon scans
+33 -19
View File
@@ -1,6 +1,6 @@
# Shannon Full LLM Context
> Combined README and documentation for AI agents and LLMs. Generated from repository Markdown files. For the concise index, see [llms.txt](llms.txt).
> Combined README and documentation for AI agents and LLMs. This file is a hand-maintained copy of the repository Markdown files, updated by hand whenever those files change. For the concise index, see [llms.txt](llms.txt).
---
@@ -48,6 +48,7 @@ It analyzes your source code, identifies attack paths, and executes real exploit
- [Documentation](#documentation)
- [Safety, Scope, and Limitations](#safety-scope-and-limitations)
- [License](#license)
- [Acknowledgements](#acknowledgements)
- [About Keygraph](#about-keygraph)
- [Community and Support](#community-and-support)
- [Common Questions](#common-questions)
@@ -239,6 +240,14 @@ Commercial and enterprise licensing is available for organizations that need dif
For commercial licensing, contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
## Acknowledgements
Thanks to [Pi](https://github.com/earendil-works/pi),
[Playwright CLI](https://github.com/microsoft/playwright-cli),
and [Mantis](https://github.com/google/mantis).
See [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for licensing and attribution details.
## About Keygraph
**Keygraph** is the company behind Shannon. It also builds the **Keygraph platform**, the commercial agentic pentesting product that closes the full AppSec lifecycle and runs an enhanced build of Shannon as its pentesting engine.
@@ -266,10 +275,6 @@ Stay connected:
## Common Questions
### Is Shannon free?
Yes. Shannon Open Source is free and licensed under AGPL-3.0. You run it yourself from the command line. Your only cost is the AI provider credits you supply.
### Can I self-host Shannon?
Yes. Shannon Open Source runs entirely on your own infrastructure in an ephemeral Docker container. Your source code is mounted read-only and never leaves your environment.
@@ -294,10 +299,6 @@ Technically yes, but it is not recommended. Shannon works with local models serv
Shannon executes real exploits. It reports a finding only when it has produced a working proof-of-concept, and discards hypotheses it cannot prove. It is a pentester, not a scanner.
### Is Shannon free for startups and nonprofits?
Shannon Open Source is free for everyone. In addition, the Keygraph Community Program gives eligible nonprofits and early-stage startups free access to the commercial Keygraph platform. See [keygraph.io](https://keygraph.io).
<p align="center">
<b>Built by <a href="https://keygraph.io">Keygraph</a></b>
</p>
@@ -365,21 +366,31 @@ The target repository is mounted read-only inside the worker container.
Monitor progress:
```bash
npx @keygraph/shannon logs <workspace>
npx @keygraph/shannon status <workspace>
npx @keygraph/shannon logs [<workspace>] # defaults to the single running scan, else the most recent
npx @keygraph/shannon status [<workspace>] # same default target; add --json for a machine-readable snapshot
npx @keygraph/shannon scans
npx @keygraph/shannon version
```
With no workspace, `logs` and `status` follow the single running scan; when several are running, name one.
Source-build equivalents:
```bash
./shannon logs <workspace>
./shannon status <workspace>
./shannon logs [<workspace>] # the combined live log (unchanged default)
./shannon logs [<workspace>] --agent <name> # tail one agent's own log
./shannon logs [<workspace>] --list-agents # list the agents with their own log
./shannon status [<workspace>]
./shannon scans
./shannon version
```
Every scan writes one combined `.shannon/workflow.log` and a per-agent projection of it under
`.shannon/agents/`: one file per pipeline agent (`recon.log`, `xss-vuln.log`, …) and one per Capella
stage (`agentic-sast-research.log`, …). Delegated subagents fold into their parent's file, and a
Capella stage's concurrent sessions share its file with an inline session label. The combined log
stays canonical; the per-agent files are best-effort projections.
Open the Temporal Web UI for detailed monitoring:
```bash
@@ -389,7 +400,7 @@ open http://localhost:8233
Stop Shannon:
```bash
npx @keygraph/shannon stop <workspace> # stop one scan (confirms first; add --yes/-y to skip)
npx @keygraph/shannon stop [<workspace>] # stop one scan (defaults to the single running scan; confirms first; add --yes/-y to skip)
npx @keygraph/shannon stop --all # stop all scans (Temporal stays up)
npx @keygraph/shannon reset # stop everything and wipe all Temporal data (type 'confirm' to proceed; cannot be skipped)
```
@@ -397,7 +408,7 @@ npx @keygraph/shannon reset # stop everything and wipe all Temporal
Source-build equivalents:
```bash
./shannon stop <workspace> # stop one scan (confirms first; add --yes/-y to skip)
./shannon stop [<workspace>] # stop one scan (defaults to the single running scan; confirms first; add --yes/-y to skip)
./shannon stop --all # stop all scans (Temporal stays up)
./shannon reset # stop everything and wipe all Temporal data (type 'confirm' to proceed; cannot be skipped)
```
@@ -420,7 +431,7 @@ 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
# List completed scans.
# List running and completed scans.
npx @keygraph/shannon scans
```
@@ -455,7 +466,7 @@ workspaces/{hostname}_{sessionId}/
|-- Security-Assessment-Report.md # the final report (Markdown)
`-- .shannon/ # internals
|-- deliverables/ # report source, per-phase analysis, queues
|-- agents/ # per-agent logs
|-- agents/ # per-agent log projections, one file per agent/Capella stage
|-- prompts/ # rendered prompts
|-- scratchpad/ # screenshots, scripts
|-- session.json # resume state
@@ -612,7 +623,9 @@ report:
sarif: "false"
```
Each finding becomes one SARIF result, filed under a rule per vulnerability class (`shannon/injection`, `shannon/xss`, `shannon/auth`, `shannon/authz`, `shannon/ssrf`) and tagged with its OWASP Top Ten 2025 category. Results are anchored to the code location the analysis phase recorded, falling back to the HTTP entry point when the finding names no file. Severity maps onto SARIF's three levels: `critical` and `high` become `error`, `medium` becomes `warning`, everything else becomes `note`.
Each finding becomes one SARIF result, filed under a rule per vulnerability class (`shannon/injection`, `shannon/xss`, `shannon/auth`, `shannon/authz`, `shannon/ssrf`, and `shannon/miscellaneous` for findings outside those classes) and tagged with its OWASP Top Ten 2025 category. Results are anchored to the code location the analysis phase recorded, falling back to the HTTP entry point when the finding names no file. Severity maps onto SARIF's three levels: `critical` and `high` become `error`, `medium` becomes `warning`, everything else becomes `note`.
If the SARIF log cannot be written, the JSON and Markdown reports are still produced and the scan finishes as "partial".
The log is written only for exploitative runs. `sarif` is ignored when `exploit` is `"false"`.
@@ -1001,7 +1014,7 @@ Resume an auto-named workspace:
npx @keygraph/shannon start -u https://example.com -r /path/to/repo -w example-com_shannon-1771007534808
```
List completed scans:
List running and completed scans:
```bash
npx @keygraph/shannon scans
@@ -1070,6 +1083,7 @@ For broader coverage, the Keygraph platform adds black-box and white-box agentic
A full test run typically takes roughly 1 to 1.5 hours. LLM API costs vary by model pricing, target complexity, selected provider, and concurrency.
---
# File: docs/coverage-roadmap.md