mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-02-12 17:22:50 +00:00
feat: add configurable output directory with --output flag (#41)
* feat: add configurable output directory with --output flag Add --output CLI flag to specify custom output directory for session folders containing audit logs, prompts, agent logs, and deliverables. Changes: - Add --output <path> CLI flag parsing - Update generateAuditPath() to use custom path when provided - Add consolidateOutputs() to copy deliverables to session folder - Update Docker examples with volume mounts for output directories - Default remains ./audit-logs/ when --output is not specified 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add configurable output directory with --output flag Add --output CLI flag to specify custom output directory for session folders containing audit logs, prompts, agent logs, and deliverables. Changes: - Add --output <path> CLI flag parsing - Store outputPath in Session interface for persistence - Update generateAuditPath() to use custom path when provided - Pass outputPath through pre-recon and checkpoint-manager - Add consolidateOutputs() to copy deliverables to session folder - Update Docker examples with volume mount instructions - Default remains ./audit-logs/ when --output is not specified 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: add gitkeep and fix formatting * fix: correct docker run command formatting in README Remove invalid inline comments after backslash continuations in docker run commands. Comments cannot appear after backslash line continuations in shell scripts, as the backslash escapes the newline character. Reorganized comments to appear on separate lines before or after the command block for better clarity and proper shell syntax. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
14
CLAUDE.md
14
CLAUDE.md
@@ -15,13 +15,14 @@ npm install
|
||||
|
||||
### Running the Penetration Testing Agent
|
||||
```bash
|
||||
shannon <WEB_URL> <REPO_PATH> --config <CONFIG_FILE>
|
||||
shannon <WEB_URL> <REPO_PATH> [--config <CONFIG_FILE>] [--output <OUTPUT_DIR>]
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
shannon "https://example.com" "/path/to/local/repo"
|
||||
shannon "https://juice-shop.herokuapp.com" "/home/user/juice-shop" --config juice-shop-config.yaml
|
||||
shannon "https://example.com" "/path/to/repo" --output /path/to/reports
|
||||
```
|
||||
|
||||
### Alternative Execution
|
||||
@@ -194,10 +195,11 @@ The agent implements a sophisticated checkpoint system using git:
|
||||
The agent implements a crash-safe, self-healing audit system (v3.0) with the following guarantees:
|
||||
|
||||
**Architecture:**
|
||||
- **audit-logs/**: Centralized metrics and forensic logs (source of truth)
|
||||
- **audit-logs/** (or custom `--output` path): Centralized metrics and forensic logs (source of truth)
|
||||
- `{hostname}_{sessionId}/session.json` - Comprehensive metrics with attempt-level detail
|
||||
- `{hostname}_{sessionId}/prompts/` - Exact prompts used for reproducibility
|
||||
- `{hostname}_{sessionId}/agents/` - Turn-by-turn execution logs
|
||||
- `{hostname}_{sessionId}/deliverables/` - Security reports and findings
|
||||
- **.shannon-store.json**: Minimal orchestration state (completedAgents, checkpoints)
|
||||
|
||||
**Crash Safety:**
|
||||
@@ -287,13 +289,15 @@ dist/ # Compiled JavaScript output
|
||||
└── ... # Other compiled files
|
||||
package.json # Node.js dependencies
|
||||
.shannon-store.json # Orchestration state (minimal)
|
||||
audit-logs/ # Centralized audit data (v3.0)
|
||||
audit-logs/ # Centralized audit data (default, or use --output)
|
||||
└── {hostname}_{sessionId}/
|
||||
├── session.json # Comprehensive metrics
|
||||
├── prompts/ # Prompt snapshots
|
||||
│ └── {agent}.md
|
||||
└── agents/ # Agent execution logs
|
||||
└── {timestamp}_{agent}_attempt-{N}.log
|
||||
├── agents/ # Agent execution logs
|
||||
│ └── {timestamp}_{agent}_attempt-{N}.log
|
||||
└── deliverables/ # Security reports and findings
|
||||
└── ...
|
||||
configs/ # Configuration files
|
||||
├── config-schema.json # JSON Schema validation
|
||||
├── example-config.yaml # Template configuration
|
||||
|
||||
17
README.md
17
README.md
@@ -169,10 +169,15 @@ docker run --rm -it \
|
||||
-e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||
-v "$(pwd)/repos:/app/repos" \
|
||||
-v "$(pwd)/configs:/app/configs" \
|
||||
# Comment below line if using custom output directory
|
||||
-v "$(pwd)/audit-logs:/app/audit-logs" \
|
||||
shannon:latest \
|
||||
"https://your-app.com/" \
|
||||
"/app/repos/your-app" \
|
||||
--config /app/configs/example-config.yaml
|
||||
# Optional: uncomment below for custom output directory
|
||||
# -v "$(pwd)/reports:/app/reports" \
|
||||
# --output /app/reports
|
||||
```
|
||||
|
||||
**With Anthropic API Key:**
|
||||
@@ -186,10 +191,15 @@ docker run --rm -it \
|
||||
-e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||
-v "$(pwd)/repos:/app/repos" \
|
||||
-v "$(pwd)/configs:/app/configs" \
|
||||
# Comment below line if using custom output directory
|
||||
-v "$(pwd)/audit-logs:/app/audit-logs" \
|
||||
shannon:latest \
|
||||
"https://your-app.com/" \
|
||||
"/app/repos/your-app" \
|
||||
--config /app/configs/example-config.yaml
|
||||
# Optional: uncomment below for custom output directory
|
||||
# -v "$(pwd)/reports:/app/reports" \
|
||||
# --output /app/reports
|
||||
```
|
||||
|
||||
#### Platform-Specific Instructions
|
||||
@@ -217,10 +227,15 @@ docker run --rm -it \
|
||||
-e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \
|
||||
-v "$(pwd)/repos:/app/repos" \
|
||||
-v "$(pwd)/configs:/app/configs" \
|
||||
# Comment below line if using custom output directory
|
||||
-v "$(pwd)/audit-logs:/app/audit-logs" \
|
||||
shannon:latest \
|
||||
"http://host.docker.internal:3000" \
|
||||
"/app/repos/your-app" \
|
||||
--config /app/configs/example-config.yaml
|
||||
# Optional: uncomment below for custom output directory
|
||||
# -v "$(pwd)/reports:/app/reports" \
|
||||
# --output /app/reports
|
||||
```
|
||||
|
||||
### Configuration (Optional)
|
||||
@@ -281,7 +296,7 @@ docker run --rm shannon:latest --status
|
||||
|
||||
### Output and Results
|
||||
|
||||
All analysis results are saved to the `deliverables/` directory:
|
||||
All results are saved to `./audit-logs/` by default. Use `--output <path>` to specify a custom directory. If using `--output`, ensure that path is mounted to an accessible host directory (e.g., `-v "$(pwd)/custom-directory:/app/reports"`).
|
||||
|
||||
- **Pre-reconnaissance reports** - External scan results
|
||||
- **Vulnerability assessments** - Potential vulnerabilities from thorough code analysis and network mapping
|
||||
|
||||
0
audit-logs/.gitkeep
Normal file
0
audit-logs/.gitkeep
Normal file
@@ -26,6 +26,7 @@ export interface SessionMetadata {
|
||||
id: string;
|
||||
webUrl: string;
|
||||
repoPath?: string;
|
||||
outputPath?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -40,10 +41,12 @@ export function generateSessionIdentifier(sessionMetadata: SessionMetadata): str
|
||||
|
||||
/**
|
||||
* Generate path to audit log directory for a session
|
||||
* Uses custom outputPath if provided, otherwise defaults to AUDIT_LOGS_DIR
|
||||
*/
|
||||
export function generateAuditPath(sessionMetadata: SessionMetadata): string {
|
||||
const sessionIdentifier = generateSessionIdentifier(sessionMetadata);
|
||||
return path.join(AUDIT_LOGS_DIR, sessionIdentifier);
|
||||
const baseDir = sessionMetadata.outputPath || AUDIT_LOGS_DIR;
|
||||
return path.join(baseDir, sessionIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -293,7 +293,7 @@ const runSingleAgent = async (
|
||||
AGENTS[agentName]!.displayName,
|
||||
agentName,
|
||||
getAgentColor(agentName),
|
||||
{ id: currentSession.id, webUrl: currentSession.webUrl, repoPath: currentSession.repoPath }
|
||||
{ id: currentSession.id, webUrl: currentSession.webUrl, repoPath: currentSession.repoPath, ...(currentSession.outputPath && { outputPath: currentSession.outputPath }) }
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
@@ -727,7 +727,8 @@ export const rollbackTo = async (targetAgent: string, session: Session): Promise
|
||||
const sessionMetadata: SessionMetadata = {
|
||||
id: session.id,
|
||||
webUrl: session.webUrl,
|
||||
repoPath: session.repoPath
|
||||
repoPath: session.repoPath,
|
||||
...(session.outputPath && { outputPath: session.outputPath })
|
||||
};
|
||||
const auditSession = new AuditSession(sessionMetadata);
|
||||
await auditSession.initialize();
|
||||
|
||||
@@ -33,6 +33,9 @@ export function showHelp(): void {
|
||||
console.log(
|
||||
' --config <file> YAML configuration file for authentication and testing parameters'
|
||||
);
|
||||
console.log(
|
||||
' --output <path> Custom output directory for session folder (default: ./audit-logs/)'
|
||||
);
|
||||
console.log(
|
||||
' --pipeline-testing Use minimal prompts for fast pipeline testing (creates minimal deliverables)'
|
||||
);
|
||||
@@ -55,6 +58,7 @@ export function showHelp(): void {
|
||||
console.log(' # Normal mode - create new session');
|
||||
console.log(' shannon "https://example.com" "/path/to/local/repo"');
|
||||
console.log(' shannon "https://example.com" "/path/to/local/repo" --config auth.yaml');
|
||||
console.log(' shannon "https://example.com" "/path/to/local/repo" --output /path/to/reports');
|
||||
console.log(
|
||||
' shannon "https://example.com" "/path/to/local/repo" --setup-only # Setup only\n'
|
||||
);
|
||||
|
||||
@@ -137,7 +137,8 @@ async function runPreReconWave1(
|
||||
variables: PromptVariables,
|
||||
config: DistributedConfig | null,
|
||||
pipelineTestingMode: boolean = false,
|
||||
sessionId: string | null = null
|
||||
sessionId: string | null = null,
|
||||
outputPath: string | null = null
|
||||
): Promise<Wave1Results> {
|
||||
console.log(chalk.blue(' → Launching Wave 1 operations in parallel...'));
|
||||
|
||||
@@ -155,7 +156,7 @@ async function runPreReconWave1(
|
||||
AGENTS['pre-recon'].displayName,
|
||||
'pre-recon', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: sessionId!, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
{ id: sessionId!, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
)
|
||||
);
|
||||
const [codeAnalysis] = await Promise.all(operations);
|
||||
@@ -178,7 +179,7 @@ async function runPreReconWave1(
|
||||
AGENTS['pre-recon'].displayName,
|
||||
'pre-recon', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: sessionId!, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
{ id: sessionId!, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -357,13 +358,14 @@ export async function executePreReconPhase(
|
||||
config: DistributedConfig | null,
|
||||
toolAvailability: ToolAvailability,
|
||||
pipelineTestingMode: boolean,
|
||||
sessionId: string | null = null
|
||||
sessionId: string | null = null,
|
||||
outputPath: string | null = null
|
||||
): Promise<PreReconResult> {
|
||||
console.log(chalk.yellow.bold('\n🔍 PHASE 1: PRE-RECONNAISSANCE'));
|
||||
const timer = new Timer('phase-1-pre-recon');
|
||||
|
||||
console.log(chalk.yellow('Wave 1: Initial footprinting...'));
|
||||
const wave1Results = await runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTestingMode, sessionId);
|
||||
const wave1Results = await runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTestingMode, sessionId, outputPath);
|
||||
console.log(chalk.green(' ✅ Wave 1 operations completed'));
|
||||
|
||||
console.log(chalk.yellow('Wave 2: Additional scanning...'));
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface Session {
|
||||
repoPath: string;
|
||||
configFile: string | null;
|
||||
targetRepo: string;
|
||||
outputPath: string | null;
|
||||
status: 'in-progress' | 'completed' | 'failed';
|
||||
completedAgents: AgentName[];
|
||||
failedAgents: AgentName[];
|
||||
@@ -265,7 +266,8 @@ export const createSession = async (
|
||||
webUrl: string,
|
||||
repoPath: string,
|
||||
configFile: string | null = null,
|
||||
targetRepo: string | null = null
|
||||
targetRepo: string | null = null,
|
||||
outputPath: string | null = null
|
||||
): Promise<Session> => {
|
||||
// Use targetRepo if provided, otherwise use repoPath
|
||||
const resolvedTargetRepo = targetRepo || repoPath;
|
||||
@@ -279,9 +281,12 @@ export const createSession = async (
|
||||
console.log(chalk.blue(`📝 Reusing existing session: ${existingSession.id.substring(0, 8)}...`));
|
||||
console.log(chalk.gray(` Progress: ${existingSession.completedAgents.length}/${Object.keys(AGENTS).length} agents completed`));
|
||||
|
||||
// Update last activity timestamp
|
||||
await updateSession(existingSession.id, { lastActivity: new Date().toISOString() });
|
||||
return existingSession;
|
||||
// Update last activity timestamp and outputPath if provided
|
||||
await updateSession(existingSession.id, {
|
||||
lastActivity: new Date().toISOString(),
|
||||
...(outputPath && { outputPath })
|
||||
});
|
||||
return { ...existingSession, ...(outputPath && { outputPath }) };
|
||||
}
|
||||
|
||||
// If completed, create a new session (allows re-running after completion)
|
||||
@@ -298,6 +303,7 @@ export const createSession = async (
|
||||
repoPath,
|
||||
configFile,
|
||||
targetRepo: resolvedTargetRepo,
|
||||
outputPath,
|
||||
status: 'in-progress',
|
||||
completedAgents: [],
|
||||
failedAgents: [],
|
||||
|
||||
@@ -80,6 +80,27 @@ interface MainResult {
|
||||
// Configure zx to disable timeouts (let tools run as long as needed)
|
||||
$.timeout = 0;
|
||||
|
||||
/**
|
||||
* Consolidate deliverables from target repo into the session folder
|
||||
* Copies deliverables directory from source repo to session audit path
|
||||
*/
|
||||
async function consolidateOutputs(sourceDir: string, sessionPath: string): Promise<void> {
|
||||
const srcDeliverables = path.join(sourceDir, 'deliverables');
|
||||
const destDeliverables = path.join(sessionPath, 'deliverables');
|
||||
|
||||
try {
|
||||
if (await fs.pathExists(srcDeliverables)) {
|
||||
await fs.copy(srcDeliverables, destDeliverables, { overwrite: true });
|
||||
console.log(chalk.gray(`📄 Deliverables copied to session folder`));
|
||||
} else {
|
||||
console.log(chalk.yellow(`⚠️ No deliverables directory found at ${srcDeliverables}`));
|
||||
}
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.log(chalk.yellow(`⚠️ Failed to consolidate deliverables: ${err.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Setup graceful cleanup on process signals
|
||||
process.on('SIGINT', async () => {
|
||||
console.log(chalk.yellow('\n⚠️ Received SIGINT, cleaning up...'));
|
||||
@@ -99,7 +120,8 @@ async function main(
|
||||
repoPath: string,
|
||||
configPath: string | null = null,
|
||||
pipelineTestingMode: boolean = false,
|
||||
disableLoader: boolean = false
|
||||
disableLoader: boolean = false,
|
||||
outputPath: string | null = null
|
||||
): Promise<MainResult> {
|
||||
// Set global flag for loader control
|
||||
global.SHANNON_DISABLE_LOADER = disableLoader;
|
||||
@@ -116,6 +138,9 @@ async function main(
|
||||
if (configPath) {
|
||||
console.log(chalk.cyan(`⚙️ Config: ${configPath}`));
|
||||
}
|
||||
if (outputPath) {
|
||||
console.log(chalk.cyan(`📂 Output: ${outputPath}`));
|
||||
}
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
|
||||
// Parse configuration if provided
|
||||
@@ -166,7 +191,7 @@ async function main(
|
||||
const variables: PromptVariables = { webUrl, repoPath, sourceDir };
|
||||
|
||||
// Create session for tracking (in normal mode)
|
||||
const session: Session = await createSession(webUrl, repoPath, configPath, sourceDir);
|
||||
const session: Session = await createSession(webUrl, repoPath, configPath, sourceDir, outputPath);
|
||||
console.log(chalk.blue(`📝 Session created: ${session.id.substring(0, 8)}...`));
|
||||
|
||||
// If setup-only mode, exit after session creation
|
||||
@@ -243,7 +268,8 @@ async function main(
|
||||
distributedConfig,
|
||||
toolAvailability,
|
||||
pipelineTestingMode,
|
||||
session.id // Pass session ID for logging
|
||||
session.id, // Pass session ID for logging
|
||||
outputPath // Pass output path for audit logging
|
||||
);
|
||||
timingResults.phases['pre-recon'] = preReconDuration;
|
||||
await updateSessionProgress('pre-recon');
|
||||
@@ -262,7 +288,7 @@ async function main(
|
||||
AGENTS['recon'].displayName,
|
||||
'recon', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: session.id, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
{ id: session.id, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
);
|
||||
const reconDuration = reconTimer.stop();
|
||||
timingResults.phases['recon'] = reconDuration;
|
||||
@@ -345,7 +371,7 @@ async function main(
|
||||
'Executive Summary and Report Cleanup',
|
||||
'report', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: session.id, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
{ id: session.id, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
);
|
||||
|
||||
const reportDuration = reportTimer.stop();
|
||||
@@ -380,7 +406,11 @@ async function main(
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
|
||||
// Calculate audit logs path
|
||||
const auditLogsPath = generateAuditPath({ id: session.id, webUrl: session.webUrl, repoPath: session.repoPath });
|
||||
const auditLogsPath = generateAuditPath({ id: session.id, webUrl: session.webUrl, repoPath: session.repoPath, ...(outputPath && { outputPath }) });
|
||||
|
||||
// Consolidate deliverables into the session folder
|
||||
await consolidateOutputs(sourceDir, auditLogsPath);
|
||||
console.log(chalk.green(`\n📂 All outputs consolidated: ${auditLogsPath}`));
|
||||
|
||||
// Return final report path and audit logs path for clickable output
|
||||
return {
|
||||
@@ -398,6 +428,7 @@ if (args[0] && args[0].includes('shannon')) {
|
||||
|
||||
// Parse flags and arguments
|
||||
let configPath: string | null = null;
|
||||
let outputPath: string | null = null;
|
||||
let pipelineTestingMode = false;
|
||||
let disableLoader = false;
|
||||
const nonFlagArgs: string[] = [];
|
||||
@@ -413,6 +444,14 @@ for (let i = 0; i < args.length; i++) {
|
||||
console.log(chalk.red('❌ --config flag requires a file path'));
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (args[i] === '--output') {
|
||||
if (i + 1 < args.length) {
|
||||
outputPath = path.resolve(args[i + 1]!);
|
||||
i++; // Skip the next argument
|
||||
} else {
|
||||
console.log(chalk.red('❌ --output flag requires a directory path'));
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (args[i] === '--pipeline-testing') {
|
||||
pipelineTestingMode = true;
|
||||
} else if (args[i] === '--disable-loader') {
|
||||
@@ -494,6 +533,9 @@ console.log(chalk.green('✅ Input validation passed:'));
|
||||
console.log(chalk.gray(` Target Web URL: ${webUrl}`));
|
||||
console.log(chalk.gray(` Target Repository: ${repoPathValidation.path}\n`));
|
||||
console.log(chalk.gray(` Config Path: ${configPath}\n`));
|
||||
if (outputPath) {
|
||||
console.log(chalk.gray(` Output Path: ${outputPath}\n`));
|
||||
}
|
||||
if (pipelineTestingMode) {
|
||||
console.log(chalk.yellow('⚡ PIPELINE TESTING MODE ENABLED - Using minimal test prompts for fast pipeline validation\n'));
|
||||
}
|
||||
@@ -502,7 +544,7 @@ if (disableLoader) {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await main(webUrl!, repoPathValidation.path!, configPath, pipelineTestingMode, disableLoader);
|
||||
const result = await main(webUrl!, repoPathValidation.path!, configPath, pipelineTestingMode, disableLoader, outputPath);
|
||||
console.log(chalk.green.bold('\n📄 FINAL REPORT AVAILABLE:'));
|
||||
console.log(chalk.cyan(result.reportPath));
|
||||
console.log(chalk.green.bold('\n📂 AUDIT LOGS AVAILABLE:'));
|
||||
|
||||
Reference in New Issue
Block a user