Files
shannon/apps/worker/src/scripts/save-deliverable.ts
T

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();