mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-09-26 19:42:07 +02:00
Merge remote-tracking branch 'origin/main' into feat/pi-harness-migration
# Conflicts: # CLAUDE.md # README.md # apps/cli/src/commands/start.ts # apps/cli/src/commands/uninstall.ts # apps/worker/src/services/agent-execution.ts # apps/worker/src/services/preflight.ts # apps/worker/src/session-manager.ts # apps/worker/src/temporal/activities.ts # apps/worker/src/temporal/shared.ts # apps/worker/src/temporal/workflows.ts # docs/ai-providers.md # llms-full.txt
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* `shn setup` — interactive TUI wizard for one-time credential configuration.
|
||||
* `npx @keygraph/shannon setup` — interactive TUI wizard for one-time credential configuration.
|
||||
*
|
||||
* Walks the user through selecting a provider and entering credentials,
|
||||
* then persists everything to ~/.shannon/config.toml with 0o600 permissions.
|
||||
@@ -40,7 +40,7 @@ export async function setup(): Promise<void> {
|
||||
|
||||
const configPath = path.join(SHANNON_HOME, 'config.toml');
|
||||
p.log.success(`Configuration saved to ${configPath}`);
|
||||
p.outro('Run `npx @keygraph/shannon@beta start` to begin a scan.');
|
||||
p.outro('Run `npx @keygraph/shannon start` to begin a scan.');
|
||||
}
|
||||
|
||||
async function setupProvider(provider: Provider): Promise<ShannonConfig> {
|
||||
|
||||
@@ -246,7 +246,7 @@ function printInfo(
|
||||
repoPath: string,
|
||||
workspacesDir: string,
|
||||
): void {
|
||||
const logsCmd = isLocal() ? `./shannon logs ${workspace}` : `npx @keygraph/shannon@beta logs ${workspace}`;
|
||||
const logsCmd = isLocal() ? `./shannon logs ${workspace}` : `npx @keygraph/shannon logs ${workspace}`;
|
||||
const reportPath = path.join(workspacesDir, workspace, FINAL_REPORT_FILENAME);
|
||||
|
||||
console.log(' Scan started — it runs in the background, so you can close this terminal.');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* `shn uninstall` command — remove ~/.shannon/ after confirmation (npx only).
|
||||
* `npx @keygraph/shannon uninstall` command — remove ~/.shannon/ after confirmation (npx only).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -44,7 +44,7 @@ export async function uninstall(yes: boolean): Promise<void> {
|
||||
fs.rmSync(SHANNON_HOME, { recursive: true, force: true });
|
||||
|
||||
const done = 'All Shannon data has been removed.';
|
||||
const hint = 'Shannon has been uninstalled. Run `npx @keygraph/shannon@beta setup` to start fresh.';
|
||||
const hint = 'Shannon has been uninstalled. Run `npx @keygraph/shannon setup` to start fresh.';
|
||||
if (interactive) {
|
||||
p.log.success(done);
|
||||
p.outro(hint);
|
||||
|
||||
@@ -94,7 +94,7 @@ function loadTOML(): TOMLConfig | null {
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
console.error(`\nFailed to parse ${configPath}: ${message}`);
|
||||
console.error(`\nRun 'npx @keygraph/shannon@beta setup' to reconfigure.\n`);
|
||||
console.error(`\nRun 'npx @keygraph/shannon setup' to reconfigure.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -255,7 +255,7 @@ export function resolveConfig(): void {
|
||||
for (const err of errors) {
|
||||
console.error(` - ${err}`);
|
||||
}
|
||||
console.error(`\nRun 'shn setup' to reconfigure.\n`);
|
||||
console.error(`\nRun 'npx @keygraph/shannon setup' to reconfigure.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ export function validateCredentials(): CredentialValidation {
|
||||
const hint =
|
||||
getMode() === 'local'
|
||||
? `No credentials found. Set ANTHROPIC_API_KEY in .env or export it.`
|
||||
: `Authentication not configured. Export variables or run 'npx @keygraph/shannon@beta setup'.`;
|
||||
: `Authentication not configured. Export variables or run 'npx @keygraph/shannon setup'.`;
|
||||
return {
|
||||
valid: false,
|
||||
mode: 'api-key',
|
||||
|
||||
@@ -41,7 +41,7 @@ function blockSudo(): void {
|
||||
|
||||
function showHelp(): void {
|
||||
const mode = getMode();
|
||||
const prefix = mode === 'local' ? './shannon' : 'npx @keygraph/shannon@beta';
|
||||
const prefix = mode === 'local' ? './shannon' : 'npx @keygraph/shannon';
|
||||
|
||||
console.log(`
|
||||
Shannon - AI Penetration Testing Framework
|
||||
@@ -158,7 +158,7 @@ function parseStartArgs(argv: string[]): ParsedStartArgs {
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown option: ${arg}`);
|
||||
console.error(`Run "${getMode() === 'local' ? './shannon' : 'npx @keygraph/shannon@beta'} help" for usage`);
|
||||
console.error(`Run "${getMode() === 'local' ? './shannon' : 'npx @keygraph/shannon'} help" for usage`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,7 @@ function parseStartArgs(argv: string[]): ParsedStartArgs {
|
||||
if (!url || !repo) {
|
||||
console.error('ERROR: --url and --repo are required');
|
||||
console.error(
|
||||
`Usage: ${getMode() === 'local' ? './shannon' : 'npx @keygraph/shannon@beta'} start -u <url> -r <path>`,
|
||||
`Usage: ${getMode() === 'local' ? './shannon' : 'npx @keygraph/shannon'} start -u <url> -r <path>`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -202,7 +202,7 @@ switch (command) {
|
||||
const workspaceId = args[1];
|
||||
if (!workspaceId) {
|
||||
console.error('ERROR: Workspace ID is required');
|
||||
console.error(`Usage: ${getMode() === 'local' ? './shannon' : 'npx @keygraph/shannon@beta'} logs <workspace>`);
|
||||
console.error(`Usage: ${getMode() === 'local' ? './shannon' : 'npx @keygraph/shannon'} logs <workspace>`);
|
||||
process.exit(1);
|
||||
}
|
||||
logs(workspaceId);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* time and API costs compared to failing mid-pipeline.
|
||||
*
|
||||
* Checks run sequentially, cheapest first:
|
||||
* 1. Repository path exists and contains .git
|
||||
* 1. Repository path exists and is a directory
|
||||
* 2. Config file parses and validates (if provided)
|
||||
* 3. code_path rules match real entries in the repo (filesystem only)
|
||||
* 4. Credentials validate via a minimal pi session (API key, OAuth, or Bedrock)
|
||||
@@ -85,14 +85,12 @@ function pinnedLookup(addresses: LookupAddress[]): LookupFunction {
|
||||
|
||||
// === Repository Validation ===
|
||||
|
||||
async function validateRepo(
|
||||
repoPath: string,
|
||||
logger: ActivityLogger,
|
||||
skipGitCheck?: boolean,
|
||||
): Promise<Result<void, PentestError>> {
|
||||
async function validateRepo(repoPath: string, logger: ActivityLogger): Promise<Result<void, PentestError>> {
|
||||
logger.info('Checking repository path...', { repoPath });
|
||||
|
||||
// 1. Check repo directory exists
|
||||
// Check repo directory exists. The repo is not required to be a git repository:
|
||||
// multi-repo targets (a parent directory containing several repos) have no top-level
|
||||
// .git, and git-based checkpoint/rollback in git-manager already no-ops on non-git dirs.
|
||||
try {
|
||||
const stats = await fs.stat(repoPath);
|
||||
if (!stats.isDirectory()) {
|
||||
@@ -118,36 +116,6 @@ async function validateRepo(
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check .git directory exists (skipped when consumer removes .git after clone)
|
||||
if (!skipGitCheck) {
|
||||
try {
|
||||
const gitStats = await fs.stat(`${repoPath}/.git`);
|
||||
if (!gitStats.isDirectory()) {
|
||||
return err(
|
||||
new PentestError(
|
||||
`Not a git repository (no .git directory): ${repoPath}`,
|
||||
'config',
|
||||
false,
|
||||
{ repoPath },
|
||||
ErrorCode.REPO_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
return err(
|
||||
new PentestError(
|
||||
`Not a git repository (no .git directory): ${repoPath}`,
|
||||
'config',
|
||||
false,
|
||||
{ repoPath },
|
||||
ErrorCode.REPO_NOT_FOUND,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.info('Skipping .git check (skipGitCheck enabled)');
|
||||
}
|
||||
|
||||
logger.info('Repository path OK');
|
||||
return ok(undefined);
|
||||
}
|
||||
@@ -540,7 +508,7 @@ async function validateTargetUrl(targetUrl: string, logger: ActivityLogger): Pro
|
||||
/**
|
||||
* Run all preflight checks sequentially (cheapest first).
|
||||
*
|
||||
* 1. Repository path exists and contains .git
|
||||
* 1. Repository path exists and is a directory
|
||||
* 2. Config file parses and validates (if configPath provided)
|
||||
* 3. code_path rules match at least one entry in the repo (skipped without config)
|
||||
* 4. Credentials validate (API key, OAuth, or Bedrock)
|
||||
@@ -553,10 +521,9 @@ export async function runPreflightChecks(
|
||||
repoPath: string,
|
||||
configPath: string | undefined,
|
||||
logger: ActivityLogger,
|
||||
skipGitCheck?: boolean,
|
||||
): Promise<Result<void, PentestError>> {
|
||||
// 1. Repository check (free — filesystem only)
|
||||
const repoResult = await validateRepo(repoPath, logger, skipGitCheck);
|
||||
const repoResult = await validateRepo(repoPath, logger);
|
||||
if (!repoResult.ok) {
|
||||
return repoResult;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@ export interface ActivityInput {
|
||||
auditDir?: string;
|
||||
promptDir?: string;
|
||||
sastSarifPath?: string;
|
||||
skipGitCheck?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,7 +440,7 @@ export async function runReportAgent(input: ActivityInput): Promise<AgentMetrics
|
||||
* Preflight validation activity.
|
||||
*
|
||||
* Runs cheap checks before any agent execution:
|
||||
* 1. Repository path exists with .git
|
||||
* 1. Repository path exists and is a directory
|
||||
* 2. Config file validates (if provided)
|
||||
* 3. Credential validation (API key, OAuth, or Bedrock)
|
||||
* 4. Target URL reachable from the container
|
||||
@@ -461,7 +460,7 @@ export async function runPreflightValidation(input: ActivityInput): Promise<void
|
||||
const logger = createActivityLogger();
|
||||
logger.info('Running preflight validation...', { attempt: attemptNumber });
|
||||
|
||||
const result = await runPreflightChecks(input.webUrl, input.repoPath, input.configPath, logger, input.skipGitCheck);
|
||||
const result = await runPreflightChecks(input.webUrl, input.repoPath, input.configPath, logger);
|
||||
|
||||
if (isErr(result)) {
|
||||
const classified = classifyErrorForTemporal(result.error);
|
||||
|
||||
@@ -26,7 +26,6 @@ export interface PipelineInput {
|
||||
promptDir?: string; // Override prompt template directory
|
||||
sastSarifPath?: string; // Optional path for consumer-supplied findings input
|
||||
checkpointsEnabled?: boolean; // Enable checkpoint activities (default: false)
|
||||
skipGitCheck?: boolean; // Skip .git directory validation in preflight (e.g. when .git is removed after clone)
|
||||
vulnClasses?: VulnClass[]; // omitted = all five
|
||||
exploit?: boolean; // false skips the exploitation phase
|
||||
}
|
||||
|
||||
@@ -253,7 +253,6 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
...(input.auditDir !== undefined && { auditDir: input.auditDir }),
|
||||
...(input.promptDir !== undefined && { promptDir: input.promptDir }),
|
||||
...(input.sastSarifPath !== undefined && { sastSarifPath: input.sastSarifPath }),
|
||||
...(input.skipGitCheck !== undefined && { skipGitCheck: input.skipGitCheck }),
|
||||
};
|
||||
|
||||
const selectedVulnClasses: readonly VulnClass[] =
|
||||
|
||||
Reference in New Issue
Block a user