Files
scan-receipts/scripts/adversarial-scan.mjs
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

198 lines
5.9 KiB
JavaScript

import fs from 'fs';
import path from 'path';
function walk(dir) {
let results = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results = results.concat(walk(fullPath));
} else if (/\.(tsx|ts|jsx|js|css|scss)$/.test(entry.name)) {
results.push(fullPath);
}
}
return results;
}
const files = walk('src');
console.log(`Found ${files.length} source files under src/ to inspect.`);
const findings = [];
// Patterns:
// 1. Any shadow class except shadow-none, shadow-[none], shadow-[0px]
const shadowPattern = /\bshadow(-[a-zA-Z0-9_/\[\]#.-]+)?\b/g;
// 2. Any rounded class except rounded-none, rounded-[0px], rounded-0, rounded-[0]
const roundedPattern = /\brounded(-[a-zA-Z0-9_/\[\]#.-]+)?\b/g;
// 3. bg-gradient-*
const bgGradientPattern = /\bbg-gradient(-[a-zA-Z0-9_-]+)?\b/g;
// 4. from-* gradient stop
const fromPattern = /\bfrom-[a-zA-Z0-9_/\[\]#.-]+\b/g;
// 5. via-* gradient stop
const viaPattern = /\bvia-[a-zA-Z0-9_/\[\]#.-]+\b/g;
// 6. to-* gradient stop (e.g., to-black, to-white, to-slate-900, to-[#...])
const toPattern = /\bto-(black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|transparent|current|\[#[a-fA-F0-9]+\])(-[0-9]+)?(\/[0-9]+)?\b/g;
// 7. CSS box-shadow
const cssBoxShadowPattern = /box-shadow\s*:\s*([^;]+)/gi;
// 8. CSS border-radius
const cssBorderRadiusPattern = /border-radius\s*:\s*([^;]+)/gi;
for (const filePath of files) {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
lines.forEach((line, index) => {
const lineNum = index + 1;
const trimmed = line.trim();
// Check CSS properties
let match;
while ((match = cssBoxShadowPattern.exec(line)) !== null) {
const val = match[1].trim();
if (val !== 'none' && val !== '0' && val !== '0px') {
findings.push({
type: 'CSS_BOX_SHADOW',
file: filePath,
line: lineNum,
matched: match[0],
raw: trimmed,
});
}
}
while ((match = cssBorderRadiusPattern.exec(line)) !== null) {
const val = match[1].trim();
if (val !== '0' && val !== '0px' && val !== '0 0 0 0') {
findings.push({
type: 'CSS_BORDER_RADIUS',
file: filePath,
line: lineNum,
matched: match[0],
raw: trimmed,
});
}
}
// Check Tailwind / Class matches
// Look for string literals or className definitions
// To be thorough, check any occurrence in the file
let rMatch;
while ((rMatch = roundedPattern.exec(line)) !== null) {
const cls = rMatch[0];
if (
cls !== 'rounded-none' &&
cls !== 'rounded-[0px]' &&
cls !== 'rounded-0' &&
cls !== 'rounded-[0]'
) {
// Exclude JS identifier words like roundedTotal, Math.round, etc. if not a class token
// A class token is typically in quotes, backticks, or preceded/followed by whitespace/quotes
const before = line[rMatch.index - 1] || ' ';
const after = line[rMatch.index + cls.length] || ' ';
if (
/['"`\s=:({[,>]/.test(before) &&
/['"`\s=:)}],<]/.test(after)
) {
findings.push({
type: 'FORBIDDEN_ROUNDED_CLASS',
file: filePath,
line: lineNum,
matched: cls,
raw: trimmed,
});
}
}
}
let sMatch;
while ((sMatch = shadowPattern.exec(line)) !== null) {
const cls = sMatch[0];
if (
cls !== 'shadow-none' &&
cls !== 'shadow-[none]' &&
cls !== 'shadow-[0px]'
) {
const before = line[sMatch.index - 1] || ' ';
const after = line[sMatch.index + cls.length] || ' ';
if (
/['"`\s=:({[,>]/.test(before) &&
/['"`\s=:)}],<]/.test(after)
) {
findings.push({
type: 'FORBIDDEN_SHADOW_CLASS',
file: filePath,
line: lineNum,
matched: cls,
raw: trimmed,
});
}
}
}
let gMatch;
while ((gMatch = bgGradientPattern.exec(line)) !== null) {
findings.push({
type: 'FORBIDDEN_BG_GRADIENT_CLASS',
file: filePath,
line: lineNum,
matched: gMatch[0],
raw: trimmed,
});
}
let fMatch;
while ((fMatch = fromPattern.exec(line)) !== null) {
// Exclude JS imports: import { ... } from '...'
if (!/\bimport\b|\bexport\b/.test(line)) {
findings.push({
type: 'FORBIDDEN_FROM_GRADIENT_CLASS',
file: filePath,
line: lineNum,
matched: fMatch[0],
raw: trimmed,
});
}
}
let vMatch;
while ((vMatch = viaPattern.exec(line)) !== null) {
findings.push({
type: 'FORBIDDEN_VIA_GRADIENT_CLASS',
file: filePath,
line: lineNum,
matched: vMatch[0],
raw: trimmed,
});
}
let tMatch;
while ((tMatch = toPattern.exec(line)) !== null) {
// Check if it's inside className or class attribute or tailwind string
findings.push({
type: 'FORBIDDEN_TO_GRADIENT_CLASS',
file: filePath,
line: lineNum,
matched: tMatch[0],
raw: trimmed,
});
}
});
}
console.log('=== ADVERSARIAL SCAN REPORT ===');
console.log(`Scanned files: ${files.length}`);
console.log(`Total violations detected: ${findings.length}`);
if (findings.length > 0) {
console.log('\n--- VIOLATIONS LIST ---');
findings.forEach((f, idx) => {
console.log(`${idx + 1}. [${f.type}] ${f.file}:${f.line}`);
console.log(` Token: "${f.matched}"`);
console.log(` Line: ${f.raw}`);
});
process.exit(1);
} else {
console.log('CLEAN: No forbidden rounded, shadow, gradient, or box-shadow tokens found in src/');
process.exit(0);
}