Skip to content

Commit 2d24cce

Browse files
authored
Merge pull request #20 from decksoftware/fix/engine-build-output-exclusion
fix(engine): exclude build outputs (.output) from detector + Semgrep; tighten DEFAULT_CREDENTIALS
2 parents 414a814 + f1ef5d3 commit 2d24cce

6 files changed

Lines changed: 78 additions & 4 deletions

File tree

csreview/src/detector.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,11 @@ const VULNERABILITY_PATTERNS = [
851851
category: 'Configuration',
852852
name: 'Default Credentials',
853853
description: 'Default admin/password credentials.',
854-
regex: /(?:admin|root|default|test)['"]?\s*[:=]\s*['"](?:admin|password|123456|root|default|changeme|test)['"]/gi,
854+
// Requires a credential-context KEY (password/pwd/secret/credential) set to a
855+
// weak/default value — not a bare admin|root|default|test key, which matched
856+
// UI role-label maps like `{ admin: 'Admin' }` (a CRITICAL false positive).
857+
regex:
858+
/(?:pass(?:word|wd)?|pwd|secret|credential)['"]?\s*[:=]\s*['"](?:admin|password|123456|root|default|changeme|test|pass|secret|letmein|qwerty|admin123|password1|password123)['"]/gi,
855859
cwe: 'CWE-798',
856860
owasp: 'A07:2021-Identification and Authentication Failures',
857861
fix: 'Change all default credentials.',

csreview/src/ignore.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ export const DEFAULT_IGNORE_DIRS = [
219219
'.dart_tool', // Flutter/Dart build cache
220220
'.gradle', // Gradle/Android cache
221221
'.supabase', // Supabase CLI local runtime state (distinct from the `supabase/` source tree)
222+
// JS/TS framework build outputs & deploy/cache dirs — compiled/bundled code,
223+
// never first-party source. `.output` (Nuxt/Nitro) was the dominant false-
224+
// positive source on a real monorepo: prototype-pollution in `_nitro.mjs`,
225+
// JWTs in bundles, etc. (`.next`/`.nuxt` were already covered above.)
226+
'.output', // Nuxt/Nitro build output
227+
'out', // Next.js static export
228+
'.vercel', // Vercel build output
229+
'.netlify', // Netlify build output
230+
'.svelte-kit', // SvelteKit build output
231+
'.angular', // Angular cache/build
232+
'.turbo', // Turborepo cache
233+
'.parcel-cache', // Parcel cache
222234
];
223235

224236
/** Built-in default file globs (minified/generated artifacts and prior reports). */

csreview/src/index.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import { generateMarkdownReport } from './reports/markdown.js';
1010
import { generateSarifReport } from './reports/sarif.js';
1111
import { calculateSecurityScore } from './score.js';
1212
import { normalizeLocalPath, safeResolveInside } from './pathSafety.js';
13-
import { loadIgnore, applyIgnore, compileIgnorePatterns, DEFAULT_IGNORE_PATTERNS } from './ignore.js';
13+
import {
14+
loadIgnore,
15+
applyIgnore,
16+
compileIgnorePatterns,
17+
DEFAULT_IGNORE_PATTERNS,
18+
DEFAULT_IGNORE_DIRS,
19+
} from './ignore.js';
1420
import { loadBaseline, applyBaseline, writeBaseline } from './baseline.js';
1521

1622
/**
@@ -865,12 +871,26 @@ export function normalizeOsvScannerFindings(osvJson = {}, rootDir = process.cwd(
865871
return findings;
866872
}
867873

874+
/**
875+
* Build Semgrep `--exclude <dir>` argument pairs from the canonical ignore-dir
876+
* list so Semgrep skips the SAME build outputs / vendored dirs as the heuristic
877+
* detector. Without this Semgrep scanned `.output`, `dist`, `.nuxt`, etc. and
878+
* produced critical false positives from compiled bundles (e.g. prototype
879+
* pollution in `_nitro.mjs`).
880+
*
881+
* @param {string[]} [dirs]
882+
* @returns {string[]}
883+
*/
884+
export function semgrepExcludeArgs(dirs = DEFAULT_IGNORE_DIRS) {
885+
return dirs.flatMap((dir) => ['--exclude', dir]);
886+
}
887+
868888
async function runSemgrep(rootDir) {
869889
try {
870890
const versionResult = await execTool('semgrep', ['--version'], { timeout: VERSION_CHECK_TIMEOUT_MS });
871891
const scanResult = await execTool(
872892
'semgrep',
873-
['--config', 'auto', '--json', '--quiet', '--exclude', 'node_modules', '--exclude', 'csreview-reports', rootDir],
893+
['--config', 'auto', '--json', '--quiet', ...semgrepExcludeArgs(), rootDir],
874894
{
875895
cwd: rootDir,
876896
timeout: 120000,

csreview/test/detector-calibration.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,25 @@ test('findings in non-source paths (test/fixtures) are downgraded; real source k
8787
assert.equal(inTest.confidence, 'LOW');
8888
});
8989

90+
test('DEFAULT_CREDENTIALS does not fire on UI role-label maps but still catches weak password defaults', () => {
91+
// FP (DeckMidia): a role->label map like { admin: 'Admin' } was flagged as
92+
// CRITICAL Default Credentials because the rule matched admin: 'Admin'.
93+
const fp = scan([
94+
{ path: 'src/labels.js', code: "export const roleLabels = { owner: 'Owner', admin: 'Admin', test: 'Test' };\n" },
95+
]);
96+
assert.equal(
97+
fp.filter((f) => String(f.id).startsWith('DEFAULT_CREDENTIALS')).length,
98+
0,
99+
`unexpected DEFAULT_CREDENTIALS FPs: ${fp.map((f) => f.file).join(', ')}`,
100+
);
101+
// TP: a credential field set to a weak/default value must still be caught.
102+
const tp = scan([{ path: 'src/cfg.js', code: "export const cfg = { password: 'admin', pwd: 'changeme' };\n" }]);
103+
assert.ok(
104+
tp.some((f) => String(f.id).startsWith('DEFAULT_CREDENTIALS')),
105+
'expected DEFAULT_CREDENTIALS to catch password: "admin"',
106+
);
107+
});
108+
90109
test('the shipped .csreview-ignore keeps a csreview self-audit free of rule-definition meta-FPs', async () => {
91110
// A scanner must not flag its own rulebook: detector.js (regexes/descriptions/
92111
// exploitation strings) and dumpGuide.js (sample connection strings) match the

csreview/test/ignore.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ test('DEFAULT_IGNORE_DIRS covers vendored deps, build output, and generated cach
129129
'.dart_tool', // Flutter/Dart cache (the dominant external-tool noise source)
130130
'.gradle', // Gradle/Android cache
131131
'.supabase', // Supabase CLI local runtime state
132+
'.output', // Nuxt/Nitro build output (the DeckMidia .output false-positive source)
133+
'out', // Next.js static export
132134
'csreview-reports',
133135
'.csreview',
134136
]) {
@@ -142,6 +144,9 @@ test('DEFAULT_IGNORE_PATTERNS are gitignore-syntax and scope external-tool findi
142144
assert.ok(isIgnored('flutter/apps/operator/.dart_tool/chrome-device/Default/Preferences', compiled));
143145
assert.ok(isIgnored('supabase-stuff/.supabase/postgres/data/x', compiled));
144146
assert.ok(isIgnored('android/app/.gradle/cache/x', compiled));
147+
// Nuxt/Nitro build output — the DeckMidia false positives (prototype pollution
148+
// in _nitro.mjs, JWT in bundles) all came from here.
149+
assert.ok(isIgnored('apps/web/.output/server/chunks/_nitro.mjs', compiled));
145150
assert.ok(isIgnored('a/node_modules/pkg/index.js', compiled));
146151
assert.ok(isIgnored('packages/web/dist/bundle.min.js', compiled));
147152
// root-level cache dirs (no parent component) must also match — guards against

csreview/test/tool-env.test.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-check
22
import test from 'node:test';
33
import assert from 'node:assert/strict';
4-
import { withToolEnv } from '../src/index.js';
4+
import { withToolEnv, semgrepExcludeArgs } from '../src/index.js';
55

66
// Semgrep's update/version "phone home" check can hang the process (notably on
77
// Linux): `semgrep --version` prints the version then blocks on the check.
@@ -32,3 +32,17 @@ test('withToolEnv is a no-op for non-semgrep tools (returns options unchanged)',
3232
assert.equal(opts, original);
3333
assert.equal(opts.env, undefined);
3434
});
35+
36+
test('semgrepExcludeArgs excludes build outputs and vendored dirs (the DeckMidia .output noise)', () => {
37+
const args = semgrepExcludeArgs();
38+
// shape: alternating --exclude <dir>
39+
assert.ok(args.length >= 2 && args.length % 2 === 0);
40+
const excluded = new Set();
41+
for (let i = 0; i < args.length; i += 2) {
42+
assert.equal(args[i], '--exclude');
43+
excluded.add(args[i + 1]);
44+
}
45+
for (const dir of ['node_modules', 'dist', 'build', '.nuxt', '.output', 'csreview-reports', '.git']) {
46+
assert.ok(excluded.has(dir), `semgrep must exclude ${dir}`);
47+
}
48+
});

0 commit comments

Comments
 (0)