fix(scripts): drop shell:true from slop-diff npx invocations

spawnSync('npx', [...], { shell: true }) invokes /bin/sh -c
with the args concatenated, subjecting them to shell parsing
(word splitting, glob expansion, metacharacter interpretation).
No user input reaches these calls today, so not exploitable —
but the posture is wrong: npx + shell args should be direct.

Fix: scope shell:true to process.platform === 'win32' where
npx is actually a .cmd requiring the shell. POSIX runs the
npx binary directly with array-form args.

Also includes Prettier reformatting (single→double quotes,
trailing commas, line wrapping) applied by the repo's
PostToolUse formatter hook. Security-relevant change is just
the two shell:true -> shell: process.platform === 'win32'
lines; everything else is whitespace/style.
This commit is contained in:
Mohammed Qazi
2026-04-18 15:59:56 -07:00
committed by Garry Tan
parent de4832ba1a
commit 202b4308b1
+60 -32
View File
@@ -11,48 +11,55 @@
* bun run slop:diff origin/release # diff against another base
*/
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { spawnSync } from "child_process";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
const base = process.argv[2] || 'main';
const base = process.argv[2] || "main";
// 1. Find changed files
const diffResult = spawnSync('git', ['diff', '--name-only', `${base}...HEAD`], {
encoding: 'utf-8', timeout: 10000,
const diffResult = spawnSync("git", ["diff", "--name-only", `${base}...HEAD`], {
encoding: "utf-8",
timeout: 10000,
});
const changedFiles = new Set(
(diffResult.stdout || '').trim().split('\n').filter(Boolean)
(diffResult.stdout || "").trim().split("\n").filter(Boolean),
);
if (changedFiles.size === 0) {
console.log('No files changed vs', base, '— nothing to check.');
console.log("No files changed vs", base, "— nothing to check.");
process.exit(0);
}
// 2. Run slop-scan on HEAD
const scanHead = spawnSync('npx', ['slop-scan', 'scan', '.', '--json'], {
encoding: 'utf-8', timeout: 120000, shell: true,
const scanHead = spawnSync("npx", ["slop-scan", "scan", ".", "--json"], {
encoding: "utf-8",
timeout: 120000,
shell: process.platform === "win32",
});
if (!scanHead.stdout) {
console.log('slop-scan not available. Install: npm i -g slop-scan');
console.log("slop-scan not available. Install: npm i -g slop-scan");
process.exit(0);
}
let headReport: any;
try { headReport = JSON.parse(scanHead.stdout); } catch {
console.log('slop-scan returned invalid JSON.'); process.exit(0);
try {
headReport = JSON.parse(scanHead.stdout);
} catch {
console.log("slop-scan returned invalid JSON.");
process.exit(0);
}
// 3. Get base branch findings using git stash approach
// Check out base versions of changed files, scan, then restore
const mergeBase = spawnSync('git', ['merge-base', base, 'HEAD'], {
encoding: 'utf-8', timeout: 5000,
const mergeBase = spawnSync("git", ["merge-base", base, "HEAD"], {
encoding: "utf-8",
timeout: 5000,
}).stdout?.trim();
// Fingerprint: strip line numbers so shifting code doesn't create false positives
// "line 142: empty catch, boundary=none" -> "empty catch, boundary=none"
function stripLineNum(evidence: string): string {
return evidence.replace(/^line \d+: /, '').replace(/ at line \d+ /, ' ');
return evidence.replace(/^line \d+: /, "").replace(/ at line \d+ /, " ");
}
// Count evidence items per (rule, file, stripped-evidence) for the base
@@ -61,27 +68,40 @@ const baseCounts = new Map<string, number>();
if (mergeBase) {
// Create temp worktree for base scan
const tmpWorktree = path.join(os.tmpdir(), `slop-base-${Date.now()}`);
const wtResult = spawnSync('git', ['worktree', 'add', '--detach', tmpWorktree, mergeBase], {
encoding: 'utf-8', timeout: 30000,
});
const wtResult = spawnSync(
"git",
["worktree", "add", "--detach", tmpWorktree, mergeBase],
{
encoding: "utf-8",
timeout: 30000,
},
);
if (wtResult.status === 0) {
// Copy slop-scan config if it exists
const configFile = 'slop-scan.config.json';
const configFile = "slop-scan.config.json";
if (fs.existsSync(configFile)) {
try { fs.copyFileSync(configFile, path.join(tmpWorktree, configFile)); } catch {}
try {
fs.copyFileSync(configFile, path.join(tmpWorktree, configFile));
} catch {}
}
const scanBase = spawnSync('npx', ['slop-scan', 'scan', tmpWorktree, '--json'], {
encoding: 'utf-8', timeout: 120000, shell: true,
});
const scanBase = spawnSync(
"npx",
["slop-scan", "scan", tmpWorktree, "--json"],
{
encoding: "utf-8",
timeout: 120000,
shell: process.platform === "win32",
},
);
if (scanBase.stdout) {
try {
const baseReport = JSON.parse(scanBase.stdout);
for (const f of baseReport.findings) {
// Remap worktree paths back to repo-relative
const realPath = f.path.replace(tmpWorktree + '/', '');
const realPath = f.path.replace(tmpWorktree + "/", "");
if (!changedFiles.has(realPath)) continue;
for (const ev of f.evidence || []) {
const key = `${f.ruleId}|${realPath}|${stripLineNum(ev)}`;
@@ -92,7 +112,7 @@ if (mergeBase) {
}
// Clean up worktree
spawnSync('git', ['worktree', 'remove', '--force', tmpWorktree], {
spawnSync("git", ["worktree", "remove", "--force", tmpWorktree], {
timeout: 10000,
});
}
@@ -102,7 +122,9 @@ if (mergeBase) {
// For each evidence item on HEAD, check if the base had the same (rule, file, stripped-evidence).
// Use counts to handle duplicates: if base had 2 and HEAD has 3, that's 1 new.
const headCounts = new Map<string, { count: number; evidence: string[] }>();
const headFindings = headReport.findings.filter((f: any) => changedFiles.has(f.path));
const headFindings = headReport.findings.filter((f: any) =>
changedFiles.has(f.path),
);
for (const f of headFindings) {
for (const ev of f.evidence || []) {
@@ -123,7 +145,7 @@ for (const [key, entry] of headCounts) {
const baseCount = baseCounts.get(key) || 0;
const netNew = entry.count - baseCount;
if (netNew > 0) {
const [ruleId, filePath] = key.split('|');
const [ruleId, filePath] = key.split("|");
// Take the last N evidence items as the "new" ones
for (const ev of entry.evidence.slice(-netNew)) {
newFindings.push({ ruleId, filePath, evidence: ev });
@@ -139,14 +161,20 @@ for (const [key, baseCount] of baseCounts) {
// 5. Print results
if (newFindings.length === 0) {
if (removedCount > 0) {
console.log(`\n slop-scan: no new findings. Removed ${removedCount} pre-existing findings.\n`);
console.log(
`\n slop-scan: no new findings. Removed ${removedCount} pre-existing findings.\n`,
);
} else {
console.log(`\n slop-scan: no new findings in ${changedFiles.size} changed files.\n`);
console.log(
`\n slop-scan: no new findings in ${changedFiles.size} changed files.\n`,
);
}
process.exit(0);
}
console.log(`\n── slop-scan: ${newFindings.length} new findings (+${newFindings.length} / -${removedCount}) ──\n`);
console.log(
`\n── slop-scan: ${newFindings.length} new findings (+${newFindings.length} / -${removedCount}) ──\n`,
);
// Group by file, then by rule
const grouped = new Map<string, Map<string, string[]>>();