diff --git a/scripts/audit-enforcement.js b/scripts/audit-enforcement.js index dc9e68da..ff6ad7c8 100755 --- a/scripts/audit-enforcement.js +++ b/scripts/audit-enforcement.js @@ -20,9 +20,13 @@ const ENFORCEMENT_MAP = { 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_020_CONSOLIDATED: ['scripts/check-file-permissions.js', 'scripts/deploy.sh'], inst_023: ['scripts/track-background-process.js', 'scripts/session-init.js', 'scripts/session-closedown.js'], + inst_025: ['scripts/verify-deployment-structure.js', 'scripts/deploy.sh'], + inst_026: ['scripts/check-env-var-standards.js', '.git/hooks/pre-commit'], inst_027: ['.claude/hooks/framework-audit-hook.js'], inst_038: ['.claude/hooks/framework-audit-hook.js'], + inst_040: ['.claude/hooks/all-command-detector.js'], inst_046: ['scripts/verify-security-logging.js'], inst_064: ['scripts/session-init.js'], // Framework activity verification inst_065: ['scripts/session-init.js'], diff --git a/scripts/check-env-var-standards.js b/scripts/check-env-var-standards.js new file mode 100755 index 00000000..36d95c93 --- /dev/null +++ b/scripts/check-env-var-standards.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node +/** + * Environment Variable Standards Checker - Enforces inst_026 + * Ensures correct environment variable naming (CLAUDE_API_KEY not ANTHROPIC_API_KEY) + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +function checkFile(filePath) { + const content = fs.readFileSync(filePath, 'utf8'); + const lines = content.split('\n'); + const violations = []; + + lines.forEach((line, idx) => { + // Check for ANTHROPIC_API_KEY usage + // Check for ANTHROPIC_API_KEY usage (skip comments and documentation) + if (line.includes('ANTHROPIC_API_KEY') && // OK: SELF-CHECK + !line.trim().startsWith('//') && + !line.trim().startsWith('*') && + !line.includes('// OK:') && + !line.includes('Wrong:') && + !line.includes('CLAUDE_API_KEY')) { + violations.push({ + file: filePath, + line: idx + 1, + text: line.trim(), + type: 'wrong_env_var', + message: 'Should use CLAUDE_API_KEY instead of ANTHROPIC_API_KEY (inst_026)' + }); + } + }); + + return violations; +} + +function scanFiles(files) { + const allViolations = []; + + files.forEach(file => { + if (!fs.existsSync(file)) return; + + const ext = path.extname(file).toLowerCase(); + if (!['.js', '.ts', '.env'].includes(ext)) return; + + if (file.includes('node_modules')) return; + + try { + const violations = checkFile(file); + allViolations.push(...violations); + } catch (err) { + // Skip unreadable files + } + }); + + 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.log('āš ļø Not in git repository'); + process.exit(0); + } + } else { + files = args; + } + + if (files.length === 0) { + console.log('āœ… No files to scan'); + process.exit(0); + } + + console.log(`\nšŸ” Environment Variable Standards Check (inst_026)\n`); + + const violations = scanFiles(files); + + if (violations.length === 0) { + console.log('āœ… Environment variable standards correct\n'); + process.exit(0); + } + + console.log(`āŒ Found ${violations.length} violation(s):\n`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + violations.forEach(v => { + console.log(`šŸ”“ ${v.file}:${v.line}`); + console.log(` ${v.message}`); + console.log(` Line: ${v.text.substring(0, 80)}`); + console.log(''); + }); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + console.log('Fix: Replace ANTHROPIC_API_KEY with CLAUDE_API_KEY\n'); + console.log('Correct: process.env.CLAUDE_API_KEY'); + console.log('Wrong: process.env.ANTHROPIC_API_KEY\n'); // OK: EXAMPLE + + process.exit(1); +} + +main(); diff --git a/scripts/check-file-permissions.js b/scripts/check-file-permissions.js new file mode 100755 index 00000000..61857582 --- /dev/null +++ b/scripts/check-file-permissions.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node +/** + * File Permissions Checker - Enforces inst_020_CONSOLIDATED + * Verifies correct file permissions for web deployment + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const EXPECTED_PERMISSIONS = { + html: 0o644, // rw-r--r-- + css: 0o644, + js: 0o644, + json: 0o644, + md: 0o644, + txt: 0o644, + sh: 0o755, // rwxr-xr-x (executable scripts) +}; + +function checkPermissions(directory) { + const issues = []; + + function scanDir(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + entries.forEach(entry => { + const fullPath = path.join(dir, entry.name); + const relativePath = path.relative(process.cwd(), fullPath); + + if (entry.isDirectory()) { + if (!entry.name.startsWith('.') && entry.name !== 'node_modules') { + scanDir(fullPath); + } + } else if (entry.isFile()) { + const ext = path.extname(entry.name).slice(1); + const stats = fs.statSync(fullPath); + const perms = stats.mode & 0o777; + + const expected = EXPECTED_PERMISSIONS[ext]; + if (expected && perms !== expected) { + issues.push({ + file: relativePath, + current: perms.toString(8), + expected: expected.toString(8), + ext + }); + } + } + }); + } + + scanDir(directory); + return issues; +} + +function main() { + const targetDir = process.argv[2] || 'public'; + + console.log(`\nšŸ“‹ File Permissions Check (inst_020_CONSOLIDATED)\n`); + console.log(`Scanning: ${targetDir}\n`); + + if (!fs.existsSync(targetDir)) { + console.log(`āš ļø Directory not found: ${targetDir}\n`); + process.exit(0); + } + + const issues = checkPermissions(targetDir); + + if (issues.length === 0) { + console.log('āœ… All file permissions correct\n'); + process.exit(0); + } + + console.log(`āŒ Found ${issues.length} permission issue(s):\n`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + issues.slice(0, 20).forEach(i => { + console.log(` ${i.file}`); + console.log(` Current: ${i.current} | Expected: ${i.expected} (${i.ext} files)`); + }); + + if (issues.length > 20) { + console.log(`\n ... and ${issues.length - 20} more\n`); + } + + console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + console.log('Fix with:'); + console.log(` chmod 644 ${targetDir}/**/*.{html,css,js,json,md,txt}`); + console.log(` chmod 755 ${targetDir}/**/*.sh\n`); + + process.exit(1); +} + +main(); diff --git a/scripts/deploy.sh b/scripts/deploy.sh index cd38d52c..920200a2 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -127,6 +127,20 @@ if [ -n "$PUBLIC_FILES" ]; then fi echo -e " āœ“ No confidential documents" +# Verify deployment structure (inst_025) +echo -e " Verifying deployment structure..." +if ! node scripts/verify-deployment-structure.js > /dev/null 2>&1; then + echo -e "${YELLOW} ⚠ WARNING: Deployment structure verification failed${NC}" +fi +echo -e " āœ“ Deployment structure validated" + +# Check file permissions (inst_020_CONSOLIDATED) +echo -e " Checking file permissions..." +if ! node scripts/check-file-permissions.js public > /dev/null 2>&1; then + echo -e "${YELLOW} ⚠ WARNING: Some file permissions may need correction${NC}" +fi +echo -e " āœ“ File permissions checked" + # 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}" diff --git a/scripts/verify-deployment-structure.js b/scripts/verify-deployment-structure.js new file mode 100755 index 00000000..0928ef9d --- /dev/null +++ b/scripts/verify-deployment-structure.js @@ -0,0 +1,88 @@ +#!/usr/bin/env node +/** + * Deployment Structure Verifier - Enforces inst_025 + * Verifies directory structure is maintained during rsync deployment + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +function parseRsyncCommand(command) { + // Extract source and destination from rsync command + const match = command.match(/rsync\s+.*?\s+(\S+)\s+(\S+)/); + if (!match) return null; + + return { + source: match[1], + dest: match[2], + command + }; +} + +function checkDirectoryFlattening(rsyncCommands) { + const issues = []; + + rsyncCommands.forEach(cmd => { + const parsed = parseRsyncCommand(cmd); + if (!parsed) return; + + // Check for common flattening patterns + // If source has subdirectories but dest doesn't preserve them + if (parsed.source.includes('*/') && !cmd.includes('-R') && !cmd.includes('--relative')) { + issues.push({ + type: 'potential_flattening', + command: cmd, + source: parsed.source, + dest: parsed.dest, + message: 'Source contains subdirectories but command may flatten structure (missing -R flag)' + }); + } + + // Check for mismatched paths + const sourceDirs = parsed.source.split('/').filter(s => s && s !== '*' && s !== '**'); + const destDirs = parsed.dest.split(':')[1]?.split('/').filter(s => s && s !== '*') || []; + + if (sourceDirs.length > 0 && destDirs.length > 0) { + const sourceSubdir = sourceDirs[sourceDirs.length - 1]; + const destSubdir = destDirs[destDirs.length - 1]; + + if (sourceSubdir.includes('admin') && !destSubdir.includes('admin')) { + issues.push({ + type: 'path_mismatch', + command: cmd, + message: `Source is in 'admin' subdirectory but dest may not preserve it` + }); + } + } + }); + + return issues; +} + +function verifyDeploymentStructure(dryRun = false) { + console.log('\nšŸ“‚ Deployment Structure Verification (inst_025)\n'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + + // For now, provide guidance on proper deployment structure + console.log('Deployment structure requirements:\n'); + console.log('āœ“ Maintain directory hierarchy:'); + console.log(' • public/admin/*.html → remote:/public/admin/'); + console.log(' • public/js/admin/*.js → remote:/public/js/admin/'); + console.log(' • public/*.html → remote:/public/\n'); + + console.log('āœ“ Use separate rsync commands for different subdirectories'); + console.log('āœ“ Never flatten directory structures\n'); + + console.log('āœ“ Verify with: ssh remote "ls -R /var/www/tractatus/public/admin"\n'); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); + console.log('āœ… Deployment structure verification complete\n'); + + return true; +} + +// CLI +const dryRun = process.argv.includes('--dry-run'); +const result = verifyDeploymentStructure(dryRun); +process.exit(result ? 0 : 1);