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>
144 lines
4.3 KiB
JavaScript
Executable file
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);
|
|
}
|