tractatus/scripts/check-attack-surface.js
TheFlow 51fd0bb6a3
Some checks are pending
CI / Run Tests (push) Waiting to run
CI / Lint Code (push) Waiting to run
CI / CSP Compliance Check (push) Waiting to run
chore(license): Phase B follow-on — relicense 3 scripts/ source files from Apache 2.0 to EUPL-1.2
Missed by Phase B (d600f6ed) which swept src/ headers but not scripts/ headers.
All 3 follow the Phase B precedent pattern:

  - scripts/check-attack-surface.js (the inst_084 validator hook itself)
  - scripts/sync-prod-audit-logs.js
  - scripts/migrate-to-schema-v3.js

Two header formats encountered:
  - Standard Apache 2.0 JS block header (first two files): full block swap to
    EUPL-1.2 equivalent with Licence/British spelling and EC canonical URL.
  - Brief JSDoc-style reference (migrate-to-schema-v3.js): short-form swap
    with Licence reference + URL line.

Other scripts/ files with Apache text references NOT in scope here:
  - scripts/relicense-apache-to-eupl.js (DATA: Apache patterns are search
    targets for the relicense tool itself)
  - scripts/fix-markdown-licences.js (DATA: Apache regex patterns for a
    migration script's find-and-replace)
  - scripts/migrate-licence-to-cc-by-4.js (DATA: Apache source patterns
    for a different migration workflow)
  - scripts/upload-document.js (DATA: Apache-2.0 is a valid SPDX tag for
    uploadable documents; retained as valid metadata option)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:54:10 +12:00

144 lines
4.3 KiB
JavaScript
Executable file

#!/usr/bin/env node
/*
* Copyright 2025 John G Stroh
*
* Licensed under the European Union Public Licence, Version 1.2 (EUPL-1.2);
* you may not use this file except in compliance with the Licence.
*
* You may obtain a copy of the Licence at:
* https://interoperable-europe.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the Licence is distributed on an "AS IS" basis,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Licence for the specific language governing permissions and
* limitations under the Licence.
*/
/**
* 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);
}