diff --git a/scripts/audit-enforcement.js b/scripts/audit-enforcement.js index f9803bb7..dc9e68da 100755 --- a/scripts/audit-enforcement.js +++ b/scripts/audit-enforcement.js @@ -14,6 +14,12 @@ const INSTRUCTION_FILE = path.join(__dirname, '../.claude/instruction-history.js // Known enforcement mechanisms const ENFORCEMENT_MAP = { inst_008: ['.git/hooks/pre-commit', 'scripts/check-csp-violations.js'], + inst_008_CONSOLIDATED: ['.git/hooks/pre-commit', 'scripts/check-csp-violations.js'], + inst_012: ['scripts/check-confidential-docs.js', 'scripts/deploy.sh'], + inst_015: ['scripts/check-confidential-docs.js', 'scripts/deploy.sh'], + inst_016: ['scripts/check-prohibited-terms.js', '.git/hooks/pre-commit'], + inst_017: ['scripts/check-prohibited-terms.js', '.git/hooks/pre-commit'], + inst_018: ['scripts/check-prohibited-terms.js', '.git/hooks/pre-commit'], inst_023: ['scripts/track-background-process.js', 'scripts/session-init.js', 'scripts/session-closedown.js'], inst_027: ['.claude/hooks/framework-audit-hook.js'], inst_038: ['.claude/hooks/framework-audit-hook.js'], @@ -22,7 +28,8 @@ const ENFORCEMENT_MAP = { inst_065: ['scripts/session-init.js'], inst_066: ['.git/hooks/commit-msg'], inst_068: ['.git/hooks/pre-commit'], - inst_070: ['.git/hooks/pre-commit'], + inst_069: ['scripts/check-credential-exposure.js', '.git/hooks/pre-commit'], + inst_070: ['scripts/check-credential-exposure.js', '.git/hooks/pre-commit'], inst_071: ['scripts/deploy.sh'], inst_075: ['.claude/hooks/check-token-checkpoint.js'], inst_077: ['scripts/session-closedown.js'], diff --git a/scripts/check-confidential-docs.js b/scripts/check-confidential-docs.js new file mode 100755 index 00000000..4733e145 --- /dev/null +++ b/scripts/check-confidential-docs.js @@ -0,0 +1,153 @@ +#!/usr/bin/env node +/** + * Confidential Document Scanner - Enforces inst_012, inst_015 + * Prevents deployment of internal/confidential documents + */ + +const fs = require('fs'); +const path = require('path'); + +// File patterns that indicate confidential/internal documents +const CONFIDENTIAL_PATTERNS = [ + /session[-_]?handoff/i, + /phase[-_]?planning/i, + /cost[-_]?estimate/i, + /infrastructure[-_]?plan/i, + /progress[-_]?report/i, + /cover[-_]?letter/i, + /testing[-_]?checklist/i, + /internal/i, + /confidential/i, + /private/i, + /draft/i, + /wip[-_]/i, // work in progress + /todo/i +]; + +// Content markers that indicate confidential information +const CONFIDENTIAL_CONTENT_MARKERS = [ + /\[INTERNAL\]/i, + /\[CONFIDENTIAL\]/i, + /\[DRAFT\]/i, + /\[DO NOT PUBLISH\]/i, + /\[WIP\]/i, + /CONFIDENTIAL:/i, + /INTERNAL ONLY:/i +]; + +function checkFilePath(filePath) { + const basename = path.basename(filePath); + + for (const pattern of CONFIDENTIAL_PATTERNS) { + if (pattern.test(basename) || pattern.test(filePath)) { + return { + confidential: true, + reason: `Filename matches confidential pattern: ${pattern.source}` + }; + } + } + + return { confidential: false }; +} + +function checkFileContent(filePath) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + + for (let i = 0; i < Math.min(20, lines.length); i++) { + for (const marker of CONFIDENTIAL_CONTENT_MARKERS) { + if (marker.test(lines[i])) { + return { + confidential: true, + reason: `Content contains confidential marker at line ${i+1}: ${marker.source}`, + line: i + 1, + text: lines[i].trim() + }; + } + } + } + + return { confidential: false }; + } catch (err) { + // Can't read file (binary, etc.) - check by path only + return { confidential: false }; + } +} + +function scanFile(filePath) { + // Skip non-document files + const ext = path.extname(filePath).toLowerCase(); + if (!['.md', '.txt', '.pdf', '.doc', '.docx', '.html'].includes(ext)) { + return null; + } + + // Check filename + const pathCheck = checkFilePath(filePath); + if (pathCheck.confidential) { + return { file: filePath, ...pathCheck }; + } + + // Check content + const contentCheck = checkFileContent(filePath); + if (contentCheck.confidential) { + return { file: filePath, ...contentCheck }; + } + + return null; +} + +function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.log('Usage: check-confidential-docs.js [file2] ...'); + console.log(''); + console.log('Scans files to prevent deployment of internal/confidential documents'); + process.exit(0); + } + + console.log(`\nšŸ” Scanning ${args.length} file(s) for confidential markers...\n`); + + const findings = []; + + args.forEach(file => { + if (!fs.existsSync(file)) return; + + const result = scanFile(file); + if (result) { + findings.push(result); + } + }); + + if (findings.length === 0) { + console.log('āœ… No confidential documents detected\n'); + process.exit(0); + } + + // Report findings + console.log(`āŒ Found ${findings.length} confidential document(s):\n`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + findings.forEach(f => { + console.log(`šŸ”“ ${f.file}`); + console.log(` Reason: ${f.reason}`); + if (f.text) { + console.log(` Line ${f.line}: ${f.text.substring(0, 60)}...`); + } + console.log(''); + }); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + console.log('āš ļø DEPLOYMENT BLOCKED (inst_012/inst_015)\n'); + console.log('These documents are marked confidential/internal.'); + console.log(''); + console.log('Actions:'); + console.log(' 1. Remove confidential markers if approved for public release'); + console.log(' 2. Move to a non-public directory'); + console.log(' 3. Get explicit human approval before deploying\n'); + + process.exit(1); +} + +main(); diff --git a/scripts/check-credential-exposure.js b/scripts/check-credential-exposure.js new file mode 100755 index 00000000..396ddf61 --- /dev/null +++ b/scripts/check-credential-exposure.js @@ -0,0 +1,136 @@ +#!/usr/bin/env node +/** + * Credential Exposure Scanner - Enforces inst_069, inst_070 + * Detects real credentials in documentation and code + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// Patterns for real credentials (not example/redacted) +const CREDENTIAL_PATTERNS = [ + // API keys that look real (not EXAMPLE/REDACTED) + { pattern: /sk-ant-api03-[A-Za-z0-9_-]{95,}(?!EXAMPLE|REDACTED)/g, type: 'Anthropic API Key' }, + { pattern: /sk-[a-z0-9]{32,}(?!EXAMPLE|REDACTED|your-key)/gi, type: 'Stripe Secret Key' }, + { pattern: /pk-[a-z0-9]{32,}(?!EXAMPLE|REDACTED|your-key)/gi, type: 'Stripe Public Key' }, + + // Generic patterns that look suspicious + { pattern: /api[_-]?key[\s:=]+["']?([a-z0-9]{32,})["']?(?!EXAMPLE|REDACTED|your-|xxx)/gi, type: 'Generic API Key' }, + { pattern: /secret[\s:=]+["']?([a-z0-9]{32,})["']?(?!EXAMPLE|REDACTED|your-|xxx)/gi, type: 'Generic Secret' }, + { pattern: /password[\s:=]+["']?([^"'\s]{8,})["']?(?!REDACTED|your-|xxx|example|password123)/gi, type: 'Possible Password' }, + + // AWS + { pattern: /AKIA[0-9A-Z]{16}(?!EXAMPLE)/g, type: 'AWS Access Key' }, + + // JWT tokens (look for proper structure, not examples) + { pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?!EXAMPLE)/g, type: 'JWT Token' }, + + // Database connection strings with real passwords + { pattern: /mongodb:\/\/[^:]+:([^@\s]{8,})@(?!REDACTED|your-password|example)/gi, type: 'MongoDB Credential' }, + { pattern: /postgres:\/\/[^:]+:([^@\s]{8,})@(?!REDACTED|your-password|example)/gi, type: 'PostgreSQL Credential' } +]; + +function checkFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + const findings = []; + + lines.forEach((line, idx) => { + CREDENTIAL_PATTERNS.forEach(({ pattern, type }) => { + const matches = line.matchAll(pattern); + for (const match of matches) { + findings.push({ + file: filePath, + line: idx + 1, + type, + text: line.trim(), + match: match[0] + }); + } + }); + }); + + return findings; +} + +function scanFiles(files) { + const allFindings = []; + + files.forEach(file => { + if (!fs.existsSync(file)) return; + + // Skip binary files, node_modules, etc. + if (file.includes('node_modules') || file.includes('.git/') || file.includes('dist/')) return; + + const ext = path.extname(file).toLowerCase(); + if (['.png', '.jpg', '.jpeg', '.gif', '.pdf', '.zip'].includes(ext)) return; + + try { + const findings = checkFile(file); + allFindings.push(...findings); + } catch (err) { + // Binary file or encoding issue - skip + } + }); + + return allFindings; +} + +function main() { + const args = process.argv.slice(2); + + let files = []; + if (args.length === 0) { + // Scan staged git files + try { + const staged = execSync('git diff --cached --name-only --diff-filter=ACM', { + encoding: 'utf8' + }); + files = staged.trim().split('\n').filter(f => f.length > 0); + } catch (err) { + console.log('āš ļø Not in git repository - skipping credential scan'); + process.exit(0); + } + } else { + files = args; + } + + if (files.length === 0) { + console.log('āœ… No files to scan for credentials'); + process.exit(0); + } + + console.log(`\nšŸ” Scanning ${files.length} file(s) for credential exposure...\n`); + + const findings = scanFiles(files); + + if (findings.length === 0) { + console.log('āœ… No credentials detected\n'); + process.exit(0); + } + + // Report findings + console.log(`āŒ Found ${findings.length} potential credential(s):\n`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + findings.forEach(f => { + console.log(`šŸ”“ ${f.file}:${f.line}`); + console.log(` Type: ${f.type}`); + console.log(` Match: ${f.match.substring(0, 50)}${f.match.length > 50 ? '...' : ''}`); + console.log(` Line: ${f.text.substring(0, 80)}${f.text.length > 80 ? '...' : ''}`); + console.log(''); + }); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + console.log('āš ļø CRITICAL SECURITY ISSUE (inst_069/inst_070)\n'); + console.log('Replace real credentials with:'); + console.log(' • API keys: "sk-ant-api03-EXAMPLE-REDACTED-NEVER-USE"'); + console.log(' • Secrets: "REDACTED" or "your-secret-here"'); + console.log(' • Passwords: "your-password-here"\n'); + console.log('Use environment variables or secret management for real credentials.\n'); + + process.exit(1); +} + +main(); diff --git a/scripts/check-prohibited-terms.js b/scripts/check-prohibited-terms.js new file mode 100755 index 00000000..b401761b --- /dev/null +++ b/scripts/check-prohibited-terms.js @@ -0,0 +1,167 @@ +#!/usr/bin/env node +/** + * Prohibited Terms Scanner - Enforces inst_016, inst_017, inst_018 + * + * Scans files for prohibited language that violates governance rules + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// inst_017: Prohibited absolute assurance terms +const PROHIBITED_ABSOLUTE_TERMS = [ + /\bguarantee(s|d)?\b/i, + /\bensures?\s+(100%|complete|total|absolute)/i, + /\beliminates?\s+all\b/i, + /\bcompletely\s+(prevents?|eliminates?|removes?)\b/i, + /\bnever\s+fails?\b/i, + /\b100%\s+(safe|secure|reliable|accurate)\b/i, + /\babsolutely\s+(prevents?|guarantees?)\b/i +]; + +// inst_018: Prohibited maturity claims without evidence +const PROHIBITED_MATURITY_CLAIMS = [ + /\b(production-ready|battle-tested|enterprise-proven)\b/i, + /\bvalidated\s+by\s+\d+\s+(companies|organizations|teams)\b/i, + /\bwidely\s+adopted\b/i, + /\bmarket\s+(leader|validated)\b/i, + /\bcustomer\s+base\s+of\b/i +]; + +// inst_016: Requires citation or [NEEDS VERIFICATION] +const STATS_PATTERNS = [ + /\d+%\s+(improvement|increase|reduction|faster|better)/i, + /\d+x\s+(faster|better|more)/i, + /ROI\s+of\s+\d+/i, + /reduces?\s+(cost|time|effort)\s+by\s+\d+/i +]; + +function checkFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + const violations = []; + + lines.forEach((line, idx) => { + const lineNum = idx + 1; + + // Check inst_017: Absolute assurance terms + PROHIBITED_ABSOLUTE_TERMS.forEach(pattern => { + if (pattern.test(line)) { + violations.push({ + file: filePath, + line: lineNum, + type: 'inst_017', + severity: 'HIGH', + text: line.trim(), + rule: 'Prohibited absolute assurance term detected' + }); + } + }); + + // Check inst_018: Maturity claims + PROHIBITED_MATURITY_CLAIMS.forEach(pattern => { + if (pattern.test(line)) { + violations.push({ + file: filePath, + line: lineNum, + type: 'inst_018', + severity: 'HIGH', + text: line.trim(), + rule: 'Prohibited maturity claim without evidence' + }); + } + }); + + // Check inst_016: Statistics without citation + STATS_PATTERNS.forEach(pattern => { + if (pattern.test(line)) { + // Check if line has citation or [NEEDS VERIFICATION] + if (!line.includes('[') && !line.includes('(source:') && !line.includes('Citation:')) { + violations.push({ + file: filePath, + line: lineNum, + type: 'inst_016', + severity: 'MEDIUM', + text: line.trim(), + rule: 'Statistic without citation or [NEEDS VERIFICATION] marker' + }); + } + } + }); + }); + + return violations; +} + +function scanFiles(files) { + const allViolations = []; + + files.forEach(file => { + if (!fs.existsSync(file)) return; + + // Only scan text files (markdown, HTML, text) + const ext = path.extname(file).toLowerCase(); + if (!['.md', '.html', '.txt', '.json'].includes(ext)) return; + + const violations = checkFile(file); + allViolations.push(...violations); + }); + + return allViolations; +} + +function main() { + const args = process.argv.slice(2); + + let files = []; + if (args.length === 0) { + // Scan staged git files + try { + const staged = execSync('git diff --cached --name-only --diff-filter=ACM', { + encoding: 'utf8' + }); + files = staged.trim().split('\n').filter(f => f.length > 0); + } catch (err) { + console.error('Not in a git repository or no staged files'); + process.exit(0); + } + } else { + files = args; + } + + if (files.length === 0) { + console.log('āœ… No files to scan'); + process.exit(0); + } + + console.log(`\nšŸ” Scanning ${files.length} file(s) for prohibited terms...\n`); + + const violations = scanFiles(files); + + if (violations.length === 0) { + console.log('āœ… No prohibited terms detected\n'); + process.exit(0); + } + + // Report violations + console.log(`āŒ Found ${violations.length} violation(s):\n`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + violations.forEach(v => { + console.log(`${v.severity === 'HIGH' ? 'šŸ”“' : '🟔'} ${v.file}:${v.line}`); + console.log(` Rule: ${v.type} - ${v.rule}`); + console.log(` Text: ${v.text.substring(0, 80)}${v.text.length > 80 ? '...' : ''}`); + console.log(''); + }); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + console.log('Fix violations before committing/deploying:\n'); + console.log(' inst_016: Add citation or [NEEDS VERIFICATION] to statistics'); + console.log(' inst_017: Replace absolute terms with evidence-based language'); + console.log(' inst_018: Remove maturity claims or add documented evidence\n'); + + process.exit(1); +} + +main(); diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 033ae85f..cd38d52c 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -109,6 +109,24 @@ if [ "$FRONTEND_ONLY" = false ]; then echo -e " āœ“ .rsyncignore found" fi +# Check for confidential documents (inst_012/inst_015) +echo -e " Checking for confidential documents..." +if [ "$FRONTEND_ONLY" = true ]; then + PUBLIC_FILES=$(find public -type f \( -name "*.md" -o -name "*.html" -o -name "*.txt" \) 2>/dev/null || true) +else + PUBLIC_FILES=$(find public docs -type f \( -name "*.md" -o -name "*.html" -o -name "*.txt" \) 2>/dev/null || true) +fi + +if [ -n "$PUBLIC_FILES" ]; then + if ! node scripts/check-confidential-docs.js $PUBLIC_FILES 2>&1 | grep -q "No confidential"; then + echo -e "${RED}āœ— ERROR: Confidential documents detected - DEPLOYMENT BLOCKED (inst_012/inst_015)${NC}" + echo "" + node scripts/check-confidential-docs.js $PUBLIC_FILES + exit 1 + fi +fi +echo -e " āœ“ No confidential documents" + # Check local server is running if ! lsof -i :9000 >/dev/null 2>&1; then echo -e "${YELLOW} ⚠ WARNING: Local server not running on port 9000${NC}"