all-in-one security & code audit toolkit
point at a target → get a full security audit with remediation steps
Referrer-Policy: strict-origin-when-cross-origin.', evidence:'// ⚠ No Referrer-Policy header present' },
{ sev:'medium', title:'Server Version Disclosure', loc:'Server Header', desc:'The Server header reveals nginx version information, aiding targeted attacks.', fix:'Set server_tokens off; in nginx config.', evidence:'Server: nginx/1.24.0' },
{ sev:'medium', title:'Directory Listing Enabled', loc:'GET /assets/', desc:'Directory listing is enabled on the /assets/ path, exposing internal file structure.', fix:'Disable autoindex in nginx: autoindex off;', evidence:'GET /assets/\n→ 200 OK with directory listing\n→ Exposes: main.js, config.json, secrets.example.json' },
{ sev:'medium', title:'Subdomain Takeover Risk', loc:'staging.hermtica.com', desc:'CNAME points to a cloud service but the service endpoint is not claimed, creating a subdomain takeover risk.', fix:'Remove the dangling DNS record or claim the cloud service endpoint.', evidence:'staging.hermtica.com. CNAME hermtica-staging.vercel.app.\n// Vercel project not claimed → vulnerable to takeover' },
{ sev:'medium', title:'Missing Permissions-Policy', loc:'Response Headers', desc:'No Permissions-Policy header restricts browser features (camera, mic, geolocation).', fix:'Add Permissions-Policy: camera=(), microphone=(), geolocation=().', evidence:'// ⚠ No Permissions-Policy header present' },
{ sev:'medium', title:'Cookie Without Secure Flag', loc:'Set-Cookie Header', desc:'Session cookie is missing the Secure flag, allowing transmission over unencrypted HTTP.', fix:'Add Secure; HttpOnly; SameSite=Strict to all session cookies.', evidence:'Set-Cookie: session_id=abc123; Path=/\n// ⚠ Missing Secure, HttpOnly, and SameSite flags' },
{ sev:'medium', title:'CORS: Overly Permissive', loc:'Access-Control-Allow-Origin', desc:'CORS allows credentials from any origin when the Origin header matches, enabling CSRF-like attacks.', fix:'Use a whitelist of allowed origins instead of reflecting the Origin header.', evidence:'Access-Control-Allow-Origin: https://evil.com\nAccess-Control-Allow-Credentials: true' },
{ sev:'medium', title:'TLS 1.0/1.1 Enabled', loc:'SSL/TLS Config', desc:'Server accepts deprecated TLS 1.0 and 1.1 connections, which have known vulnerabilities (POODLE, BEAST).', fix:'Disable TLS 1.0/1.1. Require TLS 1.2 minimum.', evidence:'TLS 1.0: ACCEPTED (⚠ insecure)\nTLS 1.1: ACCEPTED (⚠ insecure)\nTLS 1.2: ACCEPTED\nTLS 1.3: ACCEPTED' },
{ sev:'low', title:'Missing X-Content-Type-Options', loc:'Response Headers', desc:'Without nosniff, browsers may MIME-sniff content, potentially executing malicious files.', fix:'Add X-Content-Type-Options: nosniff.', evidence:'// ⚠ No X-Content-Type-Options header present' },
{ sev:'low', title:'Exposed robots.txt', loc:'GET /robots.txt', desc:'robots.txt is accessible and reveals internal paths that should remain hidden.', fix:'Remove sensitive paths from robots.txt. Use authentication instead of obscurity.', evidence:'User-agent: *\nDisallow: /admin\nDisallow: /api/internal\nDisallow: /backups' },
{ sev:'low', title:'Cacheable HTTPS Response', loc:'Cache-Control Header', desc:'Sensitive pages lack cache-control headers, allowing them to be stored in browser/proxy caches.', fix:'Add Cache-Control: no-store, no-cache, must-revalidate to authenticated pages.', evidence:'// ⚠ No Cache-Control or Pragma headers on authenticated endpoints' },
{ sev:'low', title:'WebSocket Without TLS', loc:'ws:// connection', desc:'WebSocket connection uses unencrypted ws:// protocol instead of wss://.', fix:'Use wss:// for all WebSocket connections in production.', evidence:'ws://hermtica.com/ws\n→ Upgrade to wss:// required for production' },
]
},
code: {
target: '~/projects/api-server',
steps: [
{ pct: 10, label: 'indexing file tree…' },
{ pct: 20, label: 'scanning for hardcoded secrets…' },
{ pct: 35, label: 'detecting dangerous functions…' },
{ pct: 50, label: 'checking for XSS-prone patterns…' },
{ pct: 62, label: 'analyzing SQL injection patterns…' },
{ pct: 75, label: 'reviewing input validation…' },
{ pct: 85, label: 'auditing auth & session logic…' },
{ pct: 93, label: 'calculating complexity metrics…' },
{ pct: 100, label: 'generating report…' },
],
stats: { critical: 3, high: 4, medium: 6, low: 7 },
duration: '2.8',
findings: [
{ sev:'critical', title:'Hardcoded AWS Secret Key', loc:'src/config.js:12', desc:'An AWS secret access key is hardcoded in source code. Anyone with repository access can compromise the AWS account.', fix:'Use environment variables (process.env.AWS_SECRET_ACCESS_KEY) or AWS IAM instance roles. Rotate the exposed key immediately.', evidence:'const AWS_KEY = "AKIAIOSFODNN7EXAMPLE";\nconst AWS_SECRET = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";' },
{ sev:'critical', title:'Remote Code Execution via eval()', loc:'src/services/template.js:47', desc:'User-provided template string is passed directly to eval(), allowing arbitrary code execution on the server.', fix:'Never use eval() on user input. Use a sandboxed templating engine like Handlebars or Mustache.', evidence:'function renderTemplate(tmpl, data) {\n // ⚠ RCE VULNERABILITY\n return eval("`"+tmpl+"`");\n}' },
{ sev:'critical', title:'Shell Injection in File Upload', loc:'src/controllers/upload.js:34', desc:'Uploaded filename is passed unsanitized to exec(), enabling command injection via specially crafted filenames.', fix:'Use fs.rename() or a library like multer for file handling. Never pass user input to shell commands.', evidence:'app.post("/upload", (req, res) => {\n const { filename } = req.file;\n exec(`mv /tmp/upload /var/www/files/${filename}`);\n});' },
{ sev:'high', title:'SQL Injection via Raw Query', loc:'src/models/user.js:28', desc:'User email is interpolated directly into a raw SQL query, bypassing the ORM\'s protections.', fix:'Use parameterized queries or the ORM\'s built-in query builder.', evidence:'async function findByEmail(email) {\n const q = `SELECT * FROM users WHERE email = "${email}"`;\n return db.query(q);\n}' },
{ sev:'high', title:'NoSQL Injection in MongoDB Query', loc:'src/services/search.js:15', desc:'User search object is spread directly into the MongoDB query, allowing operator injection (e.g., $gt, $ne).', fix:'Sanitize query objects. Strip $-prefixed keys or use mongo-sanitize.', evidence:'async function searchUsers(filter) {\n // ⚠ NoSQL injection\n return User.find(filter);\n}' },
{ sev:'high', title:'JWT Secret Hardcoded & Weak', loc:'src/middleware/auth.js:5', desc:'JWT signing secret is hardcoded and easily guessable ("secret123"), allowing anyone to forge valid tokens.', fix:'Use a cryptographically random secret stored in environment variables. Minimum 256 bits.', evidence:'const JWT_SECRET = "secret123";\n// Anyone can generate valid tokens:\n// jwt.sign({admin:true}, "secret123")' },
{ sev:'high', title:'Password in Log Statement', loc:'src/controllers/auth.js:42', desc:'Plaintext passwords are logged during login attempts, exposing credentials in log files and monitoring systems.', fix:'Never log passwords, even on failed attempts. Log only usernames/email addresses.', evidence:'console.log(`Login attempt: ${email} / ${password}`);\n// Logs: Login attempt: user@example.com / hunter2' },
{ sev:'medium', title:'Missing Input Validation', loc:'src/routes/api.js:18', desc:'POST endpoint accepts raw JSON body with no validation, allowing malformed or malicious data into the system.', fix:'Use a validation library (Joi, Zod, Yup) to define and enforce input schemas.', evidence:'router.post("/users", async (req, res) => {\n // ⚠ No validation on req.body\n await User.create(req.body);\n});' },
{ sev:'medium', title:'Rate Limiting Disabled', loc:'src/app.js:12', desc:'No rate limiting is configured on the API, making it vulnerable to brute force and DDoS attacks.', fix:'Add express-rate-limit middleware with sensible defaults (e.g., 100 req/min per IP).', evidence:'// ⚠ No rate limiting configured\napp.use(express.json());\napp.use("/api", apiRoutes);' },
{ sev:'medium', title:'Debug Mode Enabled in Production', loc:'src/app.js:6', desc:'Express is running with debug error handling (NODE_ENV !== "production"), leaking stack traces to clients.', fix:'Set NODE_ENV=production in deployment. Use a custom error handler that returns sanitized messages.', evidence:'// NODE_ENV is "development"\n// Stack traces exposed on 500 errors' },
{ sev:'medium', title:'Weak Password Hashing (SHA-256)', loc:'src/services/hash.js:8', desc:'SHA-256 is used for password hashing instead of a slow, salted algorithm like bcrypt or argon2.', fix:'Replace with bcrypt (12+ salt rounds) or argon2id. Migrate existing hashes on next login.', evidence:'const crypto = require("crypto");\nfunction hashPassword(pw) {\n return crypto.createHash("sha256").update(pw).digest("hex");\n}' },
{ sev:'medium', title:'Insecure Random Token Generation', loc:'src/services/token.js:5', desc:'Math.random() is used for generating password reset tokens, which is not cryptographically secure.', fix:'Use crypto.randomBytes() for all security-sensitive token generation.', evidence:'function generateResetToken() {\n return Math.random().toString(36).slice(2);\n // Math.random() is NOT cryptographically secure\n}' },
{ sev:'medium', title:'Bare Except / Empty Catch', loc:'src/services/payment.js:67', desc:'All errors from the payment processing call are silently caught and ignored, masking potential failures.', fix:'Log the error at minimum. Handle known error types (network, auth, decline) differently.', evidence:'try {\n await stripe.charges.create(params);\n} catch(e) {\n // ⚠ silently ignoring all errors\n}' },
{ sev:'low', title:'Unused Dependencies', loc:'package.json', desc:'Several listed dependencies are not imported anywhere in the codebase, increasing supply chain attack surface.', fix:'Remove unused packages: uuid, moment, lodash.merge, request (deprecated).', evidence:'"dependencies": {\n "uuid": "^9.0.0", // ⚠ unused\n "moment": "^2.29.4", // ⚠ unused\n "request": "^2.88.2" // ⚠ deprecated + unused\n}' },
{ sev:'low', title:'Missing Helmet.js', loc:'src/app.js', desc:'Express app does not use helmet middleware, leaving several common HTTP security headers unset.', fix:'Add app.use(helmet()) as the first middleware. Helmet sets 11+ security headers automatically.', evidence:'// ⚠ No helmet() middleware\n// Headers missing: X-DNS-Prefetch-Control, X-Download-Options, X-Permitted-Cross-Domain-Policies' },
{ sev:'low', title:'High Cyclomatic Complexity', loc:'src/controllers/order.js:145', desc:'The processOrder() function has complexity 18, far above the recommended max of 10. Hard to test and maintain.', fix:'Refactor into smaller single-responsibility functions. Extract validation, payment, and notification logic.', evidence:'function processOrder(order) {\n // 18 branches, 145 lines\n // if/else chains × 12, nested switches × 3\n}' },
{ sev:'low', title:'console.log Left in Production', loc:'src/middleware/auth.js:22', desc:'Debug console.log statements remain in production code, potentially leaking sensitive info.', fix:'Use a proper logger (winston, pino) with log levels. Remove or gate debug logs behind environment check.', evidence:'console.log("Decoded token payload:", decoded);\n// leaks user id, email, role in production logs' },
{ sev:'low', title:'Missing .gitignore Entry', loc:'.env (tracked by git)', desc:'.env file is tracked in git history, potentially exposing secrets to anyone with repository access.', fix:'Add .env to .gitignore. Use git filter-branch or BFG to scrub from history. Rotate all exposed secrets.', evidence:'$ git log -- .env\ncommit a1b2c3d ...\n+DB_PASSWORD=supersecret\n+STRIPE_KEY=sk_live_xxx' },
{ sev:'low', title:'Large File (960 lines)', loc:'src/controllers/user.js', desc:'Single controller file exceeds 500 lines, making it difficult to navigate, review, and test.', fix:'Split into smaller modules: user.profile.js, user.settings.js, user.admin.js.', evidence:'src/controllers/user.js: 960 lines\n// Functions: 47, exports: 31' },
{ sev:'low', title:'Deprecated API Usage', loc:'src/routes/legacy.js:12', desc:'Uses the deprecated req.param() method which is removed in Express 5. Also uses old Promise API patterns.', fix:'Replace req.param("id") with req.params.id or req.query.id. Upgrade to async/await.', evidence:'router.get("/user", (req, res) => {\n const id = req.param("id"); // ⚠ deprecated\n User.findById(id, function(err, user) { // ⚠ callback pattern\n });\n});' },
]
},
github: {
target: 'https://github.com/expressjs/express',
steps: [
{ pct: 5, label: 'cloning repository (shallow)…' },
{ pct: 18, label: 'indexing files (24,831 files)…' },
{ pct: 30, label: 'scanning for secrets & credentials…' },
{ pct: 42, label: 'detecting dangerous patterns…' },
{ pct: 55, label: 'auditing dependencies (package.json)…' },
{ pct: 68, label: 'checking CI/CD pipeline security…' },
{ pct: 80, label: 'analyzing code quality & complexity…' },
{ pct: 90, label: 'running SAST ruleset…' },
{ pct: 100, label: 'generating report…' },
],
stats: { critical: 1, high: 3, medium: 7, low: 11 },
duration: '6.7',
findings: [
{ sev:'critical', title:'Prototype Pollution in qs Library', loc:'node_modules/qs@6.11.0', desc:'Dependency qs has a known prototype pollution vulnerability (CVE-2022-24999) in versions before 6.11.1. Allows attackers to inject properties into Object.prototype.', fix:'Update qs to >=6.11.1. Run npm audit fix or manually bump the version in package.json.', evidence:'"dependencies": {\n "qs": "6.11.0" // ⚠ CVE-2022-24999\n}' },
{ sev:'high', title:'Open Redirect in Path Handling', loc:'lib/utils.js:423', desc:'res.redirect() uses unsanitized user input for the redirect target, enabling phishing attacks via crafted URLs.', fix:'Validate redirect URLs against a whitelist. Reject or strip external URLs.', evidence:'app.get("/redirect", (req, res) => {\n // ⚠ unsanitized redirect\n res.redirect(req.query.url);\n});' },
{ sev:'high', title:'Regular Expression DoS (ReDoS)', loc:'lib/router/route.js:72', desc:'Route parameter regex uses nested quantifiers vulnerable to catastrophic backtracking on crafted inputs.', fix:'Rewrite regex to eliminate nested quantifiers. Use atomic groups (/(?>a+)+/) or rewrite as linear pattern.', evidence:'const paramRegex = /^([a-zA-Z]+)*$/;\n// Catastrophic backtracking on: "aaaaaaaaaaaaaaaaaaaaaa!"' },
{ sev:'high', title:'Sensitive Data in Test Fixtures', loc:'test/fixtures/users.json', desc:'Test fixtures contain what appear to be real email addresses and credentials, potentially exposing real user data.', fix:'Replace all test data with obviously fake values. Use @example.com domains and placeholder credentials.', evidence:'[\n {"email":"admin@company.com","password":"Spring2024!"},\n {"email":"dev@company.com","password":"DevPass123"}\n]' },
{ sev:'medium', title:'Outdated Dependency: body-parser', loc:'package.json', desc:'body-parser@1.20.1 has known moderate vulnerabilities. Version 1.20.2+ contains fixes.', fix:'Run npm update body-parser. Consider migrating to Express built-in body parsing.', evidence:'"dependencies": {\n "body-parser": "1.20.1" // ⚠ upgrade to 1.20.2+\n}' },
{ sev:'medium', title:'Missing security.md', loc:'Repository root', desc:'No SECURITY.md file exists, meaning there is no documented process for reporting security vulnerabilities.', fix:'Add a SECURITY.md with a clear vulnerability reporting process and expected response times.', evidence:'// ⚠ No SECURITY.md found in repository root' },
{ sev:'medium', title:'CI Runs with Write Permissions', loc:'.github/workflows/test.yml', desc:'GitHub Actions workflow has broad write permissions, increasing impact of a compromised workflow.', fix:'Set permissions: contents: read at the workflow level. Use fine-grained tokens for deploy jobs only.', evidence:'# ⚠ Default permissions: write-all\n# This job can push to main and modify releases' },
{ sev:'medium', title:'npm publish Without 2FA', loc:'CI/CD pipeline', desc:'The npm publish step does not enforce two-factor authentication, making the package vulnerable to account takeover.', fix:'Enable 2FA on the npm account. Use an automation token with publish-only scope in CI.', evidence:'// npm publish runs without --otp flag\n// No 2FA enforcement detected on npm account' },
{ sev:'medium', title:'Loose npm Version Ranges', loc:'package.json', desc:'Caret (^) version ranges allow minor/patch updates that could introduce compromised dependencies.', fix:'Pin dependency versions or use exact versions. Consider using npm-shrinkwrap with integrity hashes.', evidence:'"express": "^4.18.0", // accepts 4.18.0 - 4.x.x\n"debug": "^2.6.9", // accepts 2.6.9 - 2.x.x' },
{ sev:'medium', title:'Incomplete Input Sanitization', loc:'lib/middleware/query.js', desc:'Query string parser does not limit parameter count or depth, vulnerable to parameter pollution and resource exhaustion.', fix:'Add limits: qs.parse(str, { parameterLimit: 100, depth: 5 }).', evidence:'// qs.parse() called with default options\n// Max parameters: unlimited\n// Max depth: unlimited' },
{ sev:'medium', title:'Insecure Cookie Defaults', loc:'lib/response.js', desc:'Cookie flags (secure, httpOnly, sameSite) are not enabled by default, requiring developers to opt in.', fix:'Consider enabling secure defaults or documenting the security implications prominently.', evidence:'res.cookie("session", token);\n// Defaults: secure=false, httpOnly=false, sameSite=lax' },
{ sev:'low', title:'Debug Package in Production Deps', loc:'package.json', desc:'The debug package is a regular dependency rather than devDependency, increasing production bundle size.', fix:'Move debug to devDependencies if only used during development.', evidence:'"dependencies": {\n "debug": "2.6.9" // ⚠ should be devDependency\n}' },
{ sev:'low', title:'Missing .npmrc', loc:'Repository root', desc:'No .npmrc file with audit-level and engine-strict settings, leading to inconsistent installs across environments.', fix:'Create .npmrc with audit=true, audit-level=high, engine-strict=true.', evidence:'// ⚠ No .npmrc file present' },
{ sev:'low', title:'Unpinned GitHub Actions', loc:'.github/workflows/*.yml', desc:'Actions are referenced by branch/tag (e.g., actions/checkout@v3) without commit hash pinning.', fix:'Pin actions to full commit SHAs: actions/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675.', evidence:'- uses: actions/checkout@v3\n// ⚠ floating tag — could be repointed\n- uses: actions/setup-node@v3\n// ⚠ floating tag — could be repointed' },
{ sev:'low', title:'Long-Running TODO Marks', loc:'lib/router/index.js:34', desc:'TODO comment referencing a security concern has been present for 3+ years without resolution.', fix:'Either fix the issue or create a tracked issue. Remove the TODO if no action is planned.', evidence:'// TODO(CVE-2019-xxx): review path traversal edge case\n// Present since commit abc123 (May 2019)' },
{ sev:'low', title:'Missing Code Owners', loc:'Repository settings', desc:'No CODEOWNERS file defines who must review changes to security-sensitive paths.', fix:'Create .github/CODEOWNERS with rules for sensitive paths (*.js, /lib/, /middleware/).', evidence:'// ⚠ No CODEOWNERS file found' },
{ sev:'low', title:'Unused Polyfills', loc:'lib/utils.js:12', desc:'Polyfills for ES6 features (Object.assign, Array.from) remain despite the project requiring Node.js 14+.', fix:'Remove polyfills that are natively supported in the minimum Node.js version.', evidence:'// Polyfill for Object.assign — not needed in Node 14+\nif (!Object.assign) { /* ... */ }' },
{ sev:'low', title:'Large Test Files', loc:'test/acceptance/*.js', desc:'Several test files exceed 500 lines, making test failures hard to diagnose and tests hard to maintain.', fix:'Split large test suites by feature or endpoint. Use describe blocks for logical grouping.', evidence:'test/acceptance/routing.js: 847 lines\ntest/acceptance/middleware.js: 634 lines' },
{ sev:'low', title:'Commented-Out Code', loc:'lib/application.js:89', desc:'Large blocks of commented-out code remain, suggesting incomplete cleanup or uncertainty during refactors.', fix:'Remove commented code. If it serves as documentation, add a proper comment explaining intent.', evidence:'// Old init logic — removed in v4.17\n// function init() {\n// this._router = new Router();\n// ...\n// }' },
{ sev:'low', title:'Inconsistent Error Messages', loc:'Multiple files', desc:'Error messages leak internal paths in some places but not others, suggesting inconsistent sanitization.', fix:'Audit all error messages to ensure no path or internal detail leakage. Use error codes for client-facing errors.', evidence:'Error: ENOENT at /var/www/app/lib/middleware/auth.js:23\n// ⚠ internal path leaked vs. "Authentication error" elsewhere' },
{ sev:'low', title:'High Cognitive Complexity', loc:'lib/router/route.js', desc:'The route.dispatch() function has a cognitive complexity score of 25, well above the 15 threshold.', fix:'Refactor dispatch() into smaller functions: normalizePath(), matchParams(), execHandlers().', evidence:'function dispatch(req, res, done) {\n // Complexity: 25 (threshold: 15)\n // Nested callbacks × 4, conditionals × 8\n}' },
{ sev:'low', title:'Missing Changelog for Security Fixes', loc:'History.md', desc:'The changelog does not clearly mark security-related changes, making it hard for downstream users to assess risk.', fix:'Add [SECURITY] tags to entries that address vulnerabilities. Link to CVEs or advisories.', evidence:'// 4.18.2 / 2023-10-15\n// * Fixed edge case in path handling\n// * Updated dependencies\n// ⚠ No [SECURITY] labels on CVEs' },
]
}
};
// ---- DEMO RUNNER ----
let scanRunning = false;
async function runDemo(type) {
if (scanRunning) return;
scanRunning = true;
const demo = demos[type];
const btn = document.getElementById('btn-' + type);
// Hide previous results
document.getElementById('result').className = 'result';
document.getElementById('result').innerHTML = '';
document.getElementById('summary').style.display = 'none';
document.getElementById('status').className = 'status';
// Disable buttons
document.querySelectorAll('button').forEach(b => b.disabled = true);
// Show progress
const progressWrap = document.getElementById('progress-wrap');
const progressFill = document.getElementById('progress-fill');
const progressPct = document.getElementById('progress-pct');
const progressLabel = document.getElementById('progress-label');
progressWrap.classList.add('visible');
progressFill.style.width = '0%';
progressPct.textContent = '0%';
progressLabel.textContent = 'initializing scan engine…';
// Animate through steps
for (let i = 0; i < demo.steps.length; i++) {
const step = demo.steps[i];
await sleep(350 + Math.random() * 400);
progressFill.style.width = step.pct + '%';
progressPct.textContent = step.pct + '%';
progressLabel.textContent = step.label;
}
// Done
await sleep(300);
progressWrap.classList.remove('visible');
// Show status
const status = document.getElementById('status');
status.className = 'status visible done fade-in';
status.innerHTML = `✓ scan complete in ${demo.duration}s — ${demo.target}`;
// Populate summary
document.getElementById('s-critical').textContent = demo.stats.critical;
document.getElementById('s-high').textContent = demo.stats.high;
document.getElementById('s-medium').textContent = demo.stats.medium;
document.getElementById('s-low').textContent = demo.stats.low;
document.getElementById('summary').style.display = 'block';
// Populate results
const result = document.getElementById('result');
let html = '