Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion csreview/src/detector.js
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,11 @@ const VULNERABILITY_PATTERNS = [
category: 'Configuration',
name: 'Default Credentials',
description: 'Default admin/password credentials.',
regex: /(?:admin|root|default|test)['"]?\s*[:=]\s*['"](?:admin|password|123456|root|default|changeme|test)['"]/gi,
// Requires a credential-context KEY (password/pwd/secret/credential) set to a
// weak/default value — not a bare admin|root|default|test key, which matched
// UI role-label maps like `{ admin: 'Admin' }` (a CRITICAL false positive).
regex:
/(?:pass(?:word|wd)?|pwd|secret|credential)['"]?\s*[:=]\s*['"](?:admin|password|123456|root|default|changeme|test|pass|secret|letmein|qwerty|admin123|password1|password123)['"]/gi,
cwe: 'CWE-798',
owasp: 'A07:2021-Identification and Authentication Failures',
fix: 'Change all default credentials.',
Expand Down
12 changes: 12 additions & 0 deletions csreview/src/ignore.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,18 @@ export const DEFAULT_IGNORE_DIRS = [
'.dart_tool', // Flutter/Dart build cache
'.gradle', // Gradle/Android cache
'.supabase', // Supabase CLI local runtime state (distinct from the `supabase/` source tree)
// JS/TS framework build outputs & deploy/cache dirs — compiled/bundled code,
// never first-party source. `.output` (Nuxt/Nitro) was the dominant false-
// positive source on a real monorepo: prototype-pollution in `_nitro.mjs`,
// JWTs in bundles, etc. (`.next`/`.nuxt` were already covered above.)
'.output', // Nuxt/Nitro build output
'out', // Next.js static export
'.vercel', // Vercel build output
'.netlify', // Netlify build output
'.svelte-kit', // SvelteKit build output
'.angular', // Angular cache/build
'.turbo', // Turborepo cache
'.parcel-cache', // Parcel cache
];

/** Built-in default file globs (minified/generated artifacts and prior reports). */
Expand Down
24 changes: 22 additions & 2 deletions csreview/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import { generateMarkdownReport } from './reports/markdown.js';
import { generateSarifReport } from './reports/sarif.js';
import { calculateSecurityScore } from './score.js';
import { normalizeLocalPath, safeResolveInside } from './pathSafety.js';
import { loadIgnore, applyIgnore, compileIgnorePatterns, DEFAULT_IGNORE_PATTERNS } from './ignore.js';
import {
loadIgnore,
applyIgnore,
compileIgnorePatterns,
DEFAULT_IGNORE_PATTERNS,
DEFAULT_IGNORE_DIRS,
} from './ignore.js';
import { loadBaseline, applyBaseline, writeBaseline } from './baseline.js';

/**
Expand Down Expand Up @@ -865,12 +871,26 @@ export function normalizeOsvScannerFindings(osvJson = {}, rootDir = process.cwd(
return findings;
}

/**
* Build Semgrep `--exclude <dir>` argument pairs from the canonical ignore-dir
* list so Semgrep skips the SAME build outputs / vendored dirs as the heuristic
* detector. Without this Semgrep scanned `.output`, `dist`, `.nuxt`, etc. and
* produced critical false positives from compiled bundles (e.g. prototype
* pollution in `_nitro.mjs`).
*
* @param {string[]} [dirs]
* @returns {string[]}
*/
export function semgrepExcludeArgs(dirs = DEFAULT_IGNORE_DIRS) {
return dirs.flatMap((dir) => ['--exclude', dir]);
}

async function runSemgrep(rootDir) {
try {
const versionResult = await execTool('semgrep', ['--version'], { timeout: VERSION_CHECK_TIMEOUT_MS });
const scanResult = await execTool(
'semgrep',
['--config', 'auto', '--json', '--quiet', '--exclude', 'node_modules', '--exclude', 'csreview-reports', rootDir],
['--config', 'auto', '--json', '--quiet', ...semgrepExcludeArgs(), rootDir],
{
cwd: rootDir,
timeout: 120000,
Expand Down
19 changes: 19 additions & 0 deletions csreview/test/detector-calibration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ test('findings in non-source paths (test/fixtures) are downgraded; real source k
assert.equal(inTest.confidence, 'LOW');
});

test('DEFAULT_CREDENTIALS does not fire on UI role-label maps but still catches weak password defaults', () => {
// FP (DeckMidia): a role->label map like { admin: 'Admin' } was flagged as
// CRITICAL Default Credentials because the rule matched admin: 'Admin'.
const fp = scan([
{ path: 'src/labels.js', code: "export const roleLabels = { owner: 'Owner', admin: 'Admin', test: 'Test' };\n" },
]);
assert.equal(
fp.filter((f) => String(f.id).startsWith('DEFAULT_CREDENTIALS')).length,
0,
`unexpected DEFAULT_CREDENTIALS FPs: ${fp.map((f) => f.file).join(', ')}`,
);
// TP: a credential field set to a weak/default value must still be caught.
const tp = scan([{ path: 'src/cfg.js', code: "export const cfg = { password: 'admin', pwd: 'changeme' };\n" }]);
assert.ok(
tp.some((f) => String(f.id).startsWith('DEFAULT_CREDENTIALS')),
'expected DEFAULT_CREDENTIALS to catch password: "admin"',
);
});

test('the shipped .csreview-ignore keeps a csreview self-audit free of rule-definition meta-FPs', async () => {
// A scanner must not flag its own rulebook: detector.js (regexes/descriptions/
// exploitation strings) and dumpGuide.js (sample connection strings) match the
Expand Down
5 changes: 5 additions & 0 deletions csreview/test/ignore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ test('DEFAULT_IGNORE_DIRS covers vendored deps, build output, and generated cach
'.dart_tool', // Flutter/Dart cache (the dominant external-tool noise source)
'.gradle', // Gradle/Android cache
'.supabase', // Supabase CLI local runtime state
'.output', // Nuxt/Nitro build output (the DeckMidia .output false-positive source)
'out', // Next.js static export
'csreview-reports',
'.csreview',
]) {
Expand All @@ -142,6 +144,9 @@ test('DEFAULT_IGNORE_PATTERNS are gitignore-syntax and scope external-tool findi
assert.ok(isIgnored('flutter/apps/operator/.dart_tool/chrome-device/Default/Preferences', compiled));
assert.ok(isIgnored('supabase-stuff/.supabase/postgres/data/x', compiled));
assert.ok(isIgnored('android/app/.gradle/cache/x', compiled));
// Nuxt/Nitro build output — the DeckMidia false positives (prototype pollution
// in _nitro.mjs, JWT in bundles) all came from here.
assert.ok(isIgnored('apps/web/.output/server/chunks/_nitro.mjs', compiled));
assert.ok(isIgnored('a/node_modules/pkg/index.js', compiled));
assert.ok(isIgnored('packages/web/dist/bundle.min.js', compiled));
// root-level cache dirs (no parent component) must also match — guards against
Expand Down
16 changes: 15 additions & 1 deletion csreview/test/tool-env.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check
import test from 'node:test';
import assert from 'node:assert/strict';
import { withToolEnv } from '../src/index.js';
import { withToolEnv, semgrepExcludeArgs } from '../src/index.js';

// Semgrep's update/version "phone home" check can hang the process (notably on
// Linux): `semgrep --version` prints the version then blocks on the check.
Expand Down Expand Up @@ -32,3 +32,17 @@ test('withToolEnv is a no-op for non-semgrep tools (returns options unchanged)',
assert.equal(opts, original);
assert.equal(opts.env, undefined);
});

test('semgrepExcludeArgs excludes build outputs and vendored dirs (the DeckMidia .output noise)', () => {
const args = semgrepExcludeArgs();
// shape: alternating --exclude <dir>
assert.ok(args.length >= 2 && args.length % 2 === 0);
const excluded = new Set();
for (let i = 0; i < args.length; i += 2) {
assert.equal(args[i], '--exclude');
excluded.add(args[i + 1]);
}
for (const dir of ['node_modules', 'dist', 'build', '.nuxt', '.output', 'csreview-reports', '.git']) {
assert.ok(excluded.has(dir), `semgrep must exclude ${dir}`);
}
});
Loading