feat(governance): second wave enforcement - 64% improvement (28% → 46%)

Implements 7 additional architectural enforcement mechanisms:

 Prohibited Terms Detection (inst_016/017/018):
- scripts/check-prohibited-terms.js
- Scans for absolute assurance terms ("guarantee", "100% secure")
- Detects maturity claims without evidence ("production-ready", "battle-tested")
- Checks statistics require citation or [NEEDS VERIFICATION]
- Integrated into .git/hooks/pre-commit (Check 2)

 Credential Exposure Prevention (inst_069/070):
- scripts/check-credential-exposure.js
- Detects real API keys, secrets, passwords in documentation
- Validates example credentials use proper patterns (EXAMPLE/REDACTED)
- CRITICAL: Runs first in pre-commit (Check 0)

 Confidential Document Protection (inst_012/015):
- scripts/check-confidential-docs.js
- Prevents deployment of internal/session-handoff documents
- Scans filenames and content for [CONFIDENTIAL]/[INTERNAL] markers
- Integrated into scripts/deploy.sh pre-flight checks

 Enhanced Pre-Commit Hook:
Now runs 4 checks in order:
0. Credential exposure (CRITICAL)
1. CSP compliance
2. Prohibited terms
3. Test requirements

 Enhanced Deployment Script:
- Added confidential document check to deploy.sh
- Scans public/ and docs/ before deployment
- Blocks deployment if confidential markers found

 Updated Enforcement Map:
- Added all new mechanisms to audit-enforcement.js
- Updated inst_008_CONSOLIDATED mapping
- New mappings: inst_012, inst_015, inst_016, inst_017, inst_018, inst_069, inst_070

📊 Enforcement Progress:
- Wave 1: 11/39 imperative instructions enforced (28%)
- Wave 2: 18/39 imperative instructions enforced (46%)
- Improvement: +7 instructions = +64% increase
- Remaining gaps: 21/39 (54%)

🎯 Next Priority Gaps:
- inst_013/043/045: API security validation
- inst_019: Context pressure comprehensive accounting
- inst_025: Deployment file mapping
- inst_039/040: Batch operation verification
- inst_079/080/081: Values/principles (process-based)

🔒 Security Posture:
- CRITICAL security checks now run first (credential exposure)
- All text files scanned before commit
- All deployment candidates scanned before rsync

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TheFlow 2025-10-25 13:26:33 +13:00
parent 436ca56cb0
commit 65b2c80be3
5 changed files with 482 additions and 1 deletions

View file

@ -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'],

View file

@ -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 <file1> [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();

View file

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

167
scripts/check-prohibited-terms.js Executable file
View file

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

View file

@ -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}"