UNPKG

@lanonasis/cli

Version:

Professional CLI for LanOnasis Memory as a Service (MaaS) with MCP support, seamless inline editing, and enterprise-grade security

219 lines (218 loc) 10 kB
import { Command } from 'commander'; import chalk from 'chalk'; import { homedir } from 'os'; import { join } from 'path'; import { existsSync, readFileSync, readdirSync, statSync, mkdirSync } from 'fs'; // ───────────────────────────────────────────── // Color scheme (matching api-keys.ts style) // ───────────────────────────────────────────── const colors = { primary: chalk.blue.bold, success: chalk.green, warning: chalk.yellow, error: chalk.red, info: chalk.cyan, accent: chalk.magenta, muted: chalk.gray, highlight: chalk.white.bold, }; // ───────────────────────────────────────────── // Constants // ───────────────────────────────────────────── const PRESCAN_REPORT_DIR = join(homedir(), '.lanonasis', 'security', 'prescan'); // ───────────────────────────────────────────── // Helpers // ───────────────────────────────────────────── function ensureReportDir() { if (!existsSync(PRESCAN_REPORT_DIR)) { mkdirSync(PRESCAN_REPORT_DIR, { recursive: true, mode: 0o700 }); } } function formatTimestamp(iso) { const d = new Date(iso); return d.toLocaleString(); } function loadLatestReport() { if (!existsSync(PRESCAN_REPORT_DIR)) return null; let latest = null; for (const file of readdirSync(PRESCAN_REPORT_DIR)) { if (!file.endsWith('.json')) continue; const full = join(PRESCAN_REPORT_DIR, file); try { const { mtime } = statSync(full); if (!latest || mtime > latest.mtime) latest = { mtime, path: full }; } catch { // skip unreadable } } if (!latest) return null; try { return JSON.parse(readFileSync(latest.path, 'utf8')); } catch { return null; } } async function loadPrescanRuntime() { const [{ PrivacySDK }, secretPrescan] = await Promise.all([ import('@lanonasis/privacy-sdk'), import('@lanonasis/secret-prescan'), ]); return { PrivacySDK, prescan: secretPrescan.prescan, saveReport: secretPrescan.saveReport, printSummary: secretPrescan.printSummary, getFlaggedFiles: secretPrescan.getFlaggedFiles, getQuarantinedFiles: secretPrescan.getQuarantinedFiles, secretPatterns: Array.isArray(secretPrescan.SECRET_PATTERNS) ? (secretPrescan.SECRET_PATTERNS.length) : 0, }; } function validatePath(path) { try { if (!statSync(path).isDirectory()) { console.error(colors.error(`✖ Path is not a directory: ${path}`)); process.exit(1); } } catch (err) { const code = err instanceof Error ? err.message : String(err); console.error(colors.error(`✖ Path does not exist: ${path}`)); process.exit(1); } } // ───────────────────────────────────────────── // Prescan Command (parent) // ───────────────────────────────────────────── const prescanCommand = new Command('prescan') .name('prescan') .description(colors.info('🔍 Local filesystem prescan for secrets/PII before MIRA extraction.') + ' Reports are value-stripped — never exposes raw secrets.'); // ───────────────────────────────────────────── // Subcommand: prescan run <path> // ───────────────────────────────────────────── const runCommand = new Command('run') .description('Run prescan on a directory') .argument('<path>', 'Root directory to scan') .option('--exclude <patterns...>', 'Glob patterns to exclude (e.g. --exclude node_modules --exclude .git)') .option('--json', 'Output machine-parseable JSON summary') .option('--save', 'Write report to ~/.lanonasis/security/prescan/') .option('--fail-on <threshold>', 'Exit non-zero if classification meets threshold: none, quarantined, or flagged') .option('--ci', 'CI mode: equivalent to --fail-on quarantined --json') .action(async (path, options) => { validatePath(path); const ciMode = options.ci ?? false; const failOn = options.failOn ?? (ciMode ? 'quarantined' : 'none'); const config = { target_path: path, exclude_patterns: options.exclude ?? [], }; console.error(colors.info('🔍 Starting prescan...')); console.error(colors.muted(` Target: ${path}`)); if (options.exclude?.length) { console.error(colors.muted(` Exclude: ${options.exclude.join(', ')}`)); } let report; try { const { PrivacySDK, prescan, saveReport, printSummary, getFlaggedFiles, getQuarantinedFiles, } = await loadPrescanRuntime(); const sdk = new PrivacySDK(); report = prescan(sdk, config); printSummary(report); // --save: write to CLI-owned dir, no mutations/ quarantines let reportPath; if (options.save) { ensureReportDir(); reportPath = saveReport(report, PRESCAN_REPORT_DIR); console.error(colors.success(`✅ Report saved to ${reportPath}`)); } const flaggedFiles = getFlaggedFiles(report); const quarantinedFiles = getQuarantinedFiles(report); // JSON output for piping/CI if (options.json || ciMode) { const out = { total_files: report.total_files, safe: report.summary.safe, flagged: report.summary.flagged, quarantined: report.summary.quarantined, errors: report.summary.errors, total_detections: report.summary.total_detections, detection_types: report.summary.detection_types, flagged_files: flaggedFiles, quarantined_files: quarantinedFiles, scan_root: report.scan_root, scanned_at: report.scanned_at, ...(reportPath ? { report_path: reportPath } : {}), }; console.log(JSON.stringify(out, null, 2)); } // Exit codes for CI gating let exitCode = 0; if (failOn === 'quarantined' && quarantinedFiles.length > 0) { console.error(colors.error(`\n🔴 Quarantined files found — fail-on=quarantined`)); exitCode = 1; } else if (failOn === 'flagged' && (flaggedFiles.length > 0 || quarantinedFiles.length > 0)) { console.error(colors.warning(`\n⚠️ Flagged or quarantined files found — fail-on=flagged`)); exitCode = 1; } process.exit(exitCode); } catch (err) { console.error(colors.error(`✖ Prescan failed: ${err.message}`)); process.exit(1); } }); // ───────────────────────────────────────────── // Subcommand: prescan status // ───────────────────────────────────────────── const statusCommand = new Command('status') .description('Show last prescan state and statistics') .action(async () => { console.log(colors.primary('🔍 Prescan Status')); console.log(colors.info('─'.repeat(45))); // Report directory console.log(`${colors.highlight('Report dir:')} ${colors.muted(PRESCAN_REPORT_DIR)}`); const dirExists = existsSync(PRESCAN_REPORT_DIR); if (!dirExists) { console.log(colors.warning('⚠️ No scans recorded yet — run `lanonasis prescan run <path> --save` first')); return; } // Pattern count const { secretPatterns: patternCount } = await loadPrescanRuntime(); console.log(`${colors.highlight('Patterns:')} ${colors.accent(patternCount)} registered`); // Latest report const report = loadLatestReport(); if (!report) { console.log(colors.warning('⚠️ No reports found')); return; } console.log(`${colors.highlight('Last scan:')} ${colors.success(formatTimestamp(report.scanned_at))}`); console.log(`${colors.highlight('Last target:')} ${colors.muted(report.scan_root)}`); console.log(''); console.log(colors.info('Results:')); const summary = report.summary; console.log(` ${colors.success('✅ SAFE:')} ${String(summary.safe).padStart(6)}`); console.log(` ${colors.warning('⚠️ FLAGGED:')} ${String(summary.flagged).padStart(6)}`); console.log(` ${colors.error('🔴 QUARANTINED:')} ${String(summary.quarantined).padStart(6)}`); console.log(` ${colors.muted('❌ ERRORS:')} ${String(summary.errors).padStart(6)}`); console.log(''); console.log(`${colors.highlight('Total detections:')} ${colors.accent(summary.total_detections)}`); if (Object.keys(summary.detection_types).length > 0) { console.log(''); console.log(colors.info('Detection types:')); for (const [type, count] of Object.entries(summary.detection_types).sort((a, b) => b[1] - a[1])) { console.log(` ${colors.muted(type.padEnd(24))} ${colors.accent(String(count))}`); } } }); // Wire up prescanCommand.addCommand(runCommand); prescanCommand.addCommand(statusCommand); export default prescanCommand;