#!/usr/bin/env node // posture-scanner.mjs — Deterministic security posture assessment // Replaces the Opus-based posture-assessor-agent for quick posture checks. // 13 categories aligned with OWASP LLM Top 10 + Claude Code Security Baseline v1.0. // // Standalone CLI: node scanners/posture-scanner.mjs [path] → JSON stdout // Library: import { scan } from './posture-scanner.mjs' // // Scanner prefix: PST // Zero external dependencies — Node.js builtins only. import { readFile, readdir, stat, access } from 'node:fs/promises'; import { join, resolve, relative, extname } from 'node:path'; import { homedir } from 'node:os'; import { scanForInjection } from './lib/injection-patterns.mjs'; import { gradeFromPassRate, riskScore, riskBand, verdict, SEVERITY } from './lib/severity.mjs'; import { finding, scannerResult, resetCounter } from './lib/output.mjs'; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const VERSION = '7.3.1'; /** Minimum lines for a hook script to be considered non-stub */ const NON_STUB_THRESHOLD = 5; /** Status values */ const STATUS = Object.freeze({ PASS: 'PASS', PARTIAL: 'PARTIAL', FAIL: 'FAIL', NA: 'N_A' }); /** Category definitions */ const CATEGORIES = [ { id: 1, name: 'Deny-First Configuration', owasp: 'ASI02, ASI03' }, { id: 2, name: 'Secrets Protection', owasp: 'ASI03, ASI05' }, { id: 3, name: 'Path Guarding', owasp: 'ASI05, ASI10' }, { id: 4, name: 'MCP Server Trust', owasp: 'ASI04, ASI07' }, { id: 5, name: 'Destructive Command Blocking', owasp: 'ASI02, ASI05' }, { id: 6, name: 'Sandbox Configuration', owasp: 'ASI02, ASI05' }, { id: 7, name: 'Human Review Requirements', owasp: 'ASI09' }, { id: 8, name: 'Skill and Plugin Sources', owasp: 'ASI04' }, { id: 9, name: 'Session Isolation', owasp: 'ASI06, ASI08' }, { id: 10, name: 'Cognitive State Security', owasp: 'LLM01, ASI02' }, { id: 11, name: 'Prompt Injection Hardening', owasp: 'LLM01, ASI01' }, { id: 12, name: 'Rule of Two', owasp: 'ASI02, ASI05' }, { id: 13, name: 'Long-Horizon Monitoring', owasp: 'ASI06, ASI08' }, { id: 14, name: 'EU AI Act Compliance', owasp: 'Governance' }, { id: 15, name: 'NIST AI RMF Alignment', owasp: 'Governance' }, { id: 16, name: 'ISO 42001 Readiness', owasp: 'Governance' }, ]; // Critical categories: FAIL in these prevents Grade A const CRITICAL_CATEGORIES = new Set([1, 2, 5]); // --------------------------------------------------------------------------- // File reading helpers // --------------------------------------------------------------------------- async function readJson(filePath) { try { const raw = await readFile(filePath, 'utf-8'); return JSON.parse(raw); } catch { return null; } } async function readText(filePath) { try { return await readFile(filePath, 'utf-8'); } catch { return null; } } async function fileExists(filePath) { try { await access(filePath); return true; } catch { return false; } } async function listDir(dirPath) { try { return await readdir(dirPath); } catch { return []; } } /** * Count non-empty, non-comment lines in a file. * @param {string} filePath * @returns {Promise} */ async function countCodeLines(filePath) { const content = await readText(filePath); if (!content) return 0; return content.split('\n').filter(l => { const t = l.trim(); return t.length > 0 && !t.startsWith('//') && !t.startsWith('#') && !t.startsWith('