mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-07-01 02:55:37 +02:00
167 lines
4.6 KiB
JavaScript
167 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Copyright (C) 2025 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.
|
|
|
|
/**
|
|
* save-deliverable CLI
|
|
*
|
|
* Standalone script to save deliverable files.
|
|
*
|
|
* Usage:
|
|
* node save-deliverable.js --type INJECTION_ANALYSIS --file-path deliverables/injection_analysis_deliverable.md
|
|
*/
|
|
|
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { join, resolve } from 'node:path';
|
|
import { DELIVERABLE_FILENAMES, type DeliverableType } from '../types/deliverables.js';
|
|
|
|
// === Help ===
|
|
|
|
function printHelp(): void {
|
|
const types = Object.keys(DELIVERABLE_FILENAMES).join(', ');
|
|
console.log(
|
|
`save-deliverable - save a Shannon pentest deliverable under its canonical filename.
|
|
|
|
Usage:
|
|
save-deliverable --type <TYPE> --file-path <path>
|
|
save-deliverable --type <TYPE> --content '<text>'
|
|
save-deliverable --help
|
|
|
|
Options:
|
|
--type Deliverable type (required). One of:
|
|
${types}
|
|
--file-path Path of a file whose contents to save (preferred for large content).
|
|
--content Inline content string to save.
|
|
-h, --help Show this help and exit.
|
|
|
|
Output:
|
|
JSON to stdout. On success: {"status":"success","filepath":"..."}.
|
|
On error: {"status":"error","message":"...","retryable":true|false} (exit 1).`,
|
|
);
|
|
}
|
|
|
|
// === Argument Parsing ===
|
|
|
|
interface ParsedArgs {
|
|
type: string;
|
|
content?: string;
|
|
filePath?: string;
|
|
}
|
|
|
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
const args: ParsedArgs = { type: '' };
|
|
|
|
for (let i = 2; i < argv.length; i++) {
|
|
const arg = argv[i];
|
|
const next = argv[i + 1];
|
|
|
|
if (arg === '--type' && next) {
|
|
args.type = next;
|
|
i++;
|
|
} else if (arg === '--content' && next) {
|
|
args.content = next;
|
|
i++;
|
|
} else if (arg === '--file-path' && next) {
|
|
args.filePath = next;
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
// === File Operations ===
|
|
|
|
function saveDeliverableFile(targetDir: string, filename: string, content: string): string {
|
|
const subdir = process.env.SHANNON_DELIVERABLES_SUBDIR || '.shannon/deliverables';
|
|
const deliverablesDir = join(targetDir, ...subdir.split('/'));
|
|
const filepath = join(deliverablesDir, filename);
|
|
|
|
try {
|
|
mkdirSync(deliverablesDir, { recursive: true });
|
|
} catch {
|
|
throw new Error(`Cannot create deliverables directory at ${deliverablesDir}`);
|
|
}
|
|
|
|
writeFileSync(filepath, content, 'utf8');
|
|
return filepath;
|
|
}
|
|
|
|
// === Main ===
|
|
|
|
function main(): void {
|
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
printHelp();
|
|
return;
|
|
}
|
|
|
|
const args = parseArgs(process.argv);
|
|
|
|
// 1. Validate --type
|
|
if (!args.type) {
|
|
console.log(JSON.stringify({ status: 'error', message: 'Missing required --type argument', retryable: false }));
|
|
process.exit(1);
|
|
}
|
|
|
|
const deliverableType = args.type as DeliverableType;
|
|
const filename = DELIVERABLE_FILENAMES[deliverableType];
|
|
|
|
if (!filename) {
|
|
console.log(
|
|
JSON.stringify({ status: 'error', message: `Unknown deliverable type: ${args.type}`, retryable: false }),
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// 2. Resolve content from --content or --file-path
|
|
let content: string;
|
|
|
|
if (args.content) {
|
|
content = args.content;
|
|
} else if (args.filePath) {
|
|
// Path traversal protection: must resolve inside cwd
|
|
const cwd = process.cwd();
|
|
const resolved = resolve(cwd, args.filePath);
|
|
if (!resolved.startsWith(`${cwd}/`) && resolved !== cwd) {
|
|
console.log(
|
|
JSON.stringify({ status: 'error', message: `Path traversal detected: ${args.filePath}`, retryable: false }),
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
content = readFileSync(resolved, 'utf8');
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
console.log(JSON.stringify({ status: 'error', message: `Failed to read file: ${msg}`, retryable: true }));
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
console.log(
|
|
JSON.stringify({
|
|
status: 'error',
|
|
message: 'Either --content or --file-path is required',
|
|
retryable: false,
|
|
}),
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// 3. Save the file
|
|
try {
|
|
const targetDir = process.cwd();
|
|
const filepath = saveDeliverableFile(targetDir, filename, content);
|
|
console.log(JSON.stringify({ status: 'success', filepath }));
|
|
} catch (error) {
|
|
const msg = error instanceof Error ? error.message : String(error);
|
|
console.log(JSON.stringify({ status: 'error', message: `Failed to save: ${msg}`, retryable: true }));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|