#!/usr/bin/env node /* * Copyright 2025 John G Stroh * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Pre-commit Attack Surface Check (inst_084) * * Scans staged files for attack surface exposures * Blocks commits that expose internal implementation details in public documents */ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const { validateFile } = require('../src/utils/attack-surface-validator.util'); function getStagedFiles() { try { const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }); return output.split('\n').filter(f => f.trim()); } catch (error) { return []; } } function checkStagedFiles() { const stagedFiles = getStagedFiles(); if (stagedFiles.length === 0) { console.log('āœ… No attack surface exposure check needed (no staged files)'); return { success: true, exposures: [] }; } // Filter for documents only const documentFiles = stagedFiles.filter(f => f.endsWith('.md') || f.includes('/docs/') ); if (documentFiles.length === 0) { console.log('āœ… No documents in staged files'); return { success: true, exposures: [] }; } console.log(`\nšŸ” Scanning ${documentFiles.length} document(s) for attack surface exposure...`); const allExposures = []; for (const file of documentFiles) { const filePath = path.join(process.cwd(), file); if (!fs.existsSync(filePath)) { continue; // File was deleted } const content = fs.readFileSync(filePath, 'utf8'); const result = validateFile(file, content); if (!result.allowed) { allExposures.push({ file, violations: result.violations }); } } return { success: allExposures.length === 0, exposures: allExposures }; } function printExposures(exposures) { console.log('\n' + '━'.repeat(80)); console.log('āŒ ATTACK SURFACE EXPOSURE DETECTED (inst_084)'); console.log('━'.repeat(80)); console.log(''); console.log('The following files expose internal implementation details:'); console.log(''); for (const { file, violations } of exposures) { console.log(`\nšŸ”“ ${file}`); console.log(''); for (const violation of violations) { console.log(` Line ${violation.line}: ${violation.description}`); console.log(` Found: "${violation.match}"`); console.log(` šŸ’” ${violation.suggestion}`); console.log(''); } } console.log('━'.repeat(80)); console.log('āš ļø SECURITY RISK: Internal architecture exposed in public documents'); console.log('━'.repeat(80)); console.log(''); console.log('Why this matters:'); console.log(' • Exact file paths → easier reconnaissance for attackers'); console.log(' • API endpoints → attack surface mapping'); console.log(' • Port numbers → network scanning targets'); console.log(' • Internal URLs → direct access attempts'); console.log(''); console.log('Fix: Use generalized component names instead of specific paths'); console.log(''); console.log('Examples:'); console.log(' āŒ "Dashboard at /admin/audit-analytics.html"'); console.log(' āœ… "Administrative Dashboard"'); console.log(' āŒ "GET /api/admin/audit-logs endpoint"'); console.log(' āœ… "Authenticated API for audit data"'); console.log(''); console.log('To bypass (NOT RECOMMENDED):'); console.log(' git commit --no-verify'); console.log(''); } // Main execution const result = checkStagedFiles(); if (!result.success) { printExposures(result.exposures); process.exit(1); } else { if (result.exposures.length === 0) { console.log('āœ… No attack surface exposures detected'); } process.exit(0); }