claude-crap 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +43 -23
  3. package/dist/index.js +79 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/scanner/auto-scan.d.ts +57 -0
  6. package/dist/scanner/auto-scan.d.ts.map +1 -0
  7. package/dist/scanner/auto-scan.js +138 -0
  8. package/dist/scanner/auto-scan.js.map +1 -0
  9. package/dist/scanner/detector.d.ts +53 -0
  10. package/dist/scanner/detector.d.ts.map +1 -0
  11. package/dist/scanner/detector.js +173 -0
  12. package/dist/scanner/detector.js.map +1 -0
  13. package/dist/scanner/index.d.ts +22 -0
  14. package/dist/scanner/index.d.ts.map +1 -0
  15. package/dist/scanner/index.js +22 -0
  16. package/dist/scanner/index.js.map +1 -0
  17. package/dist/scanner/runner.d.ts +59 -0
  18. package/dist/scanner/runner.d.ts.map +1 -0
  19. package/dist/scanner/runner.js +159 -0
  20. package/dist/scanner/runner.js.map +1 -0
  21. package/dist/schemas/tool-schemas.d.ts +11 -0
  22. package/dist/schemas/tool-schemas.d.ts.map +1 -1
  23. package/dist/schemas/tool-schemas.js +11 -0
  24. package/dist/schemas/tool-schemas.js.map +1 -1
  25. package/package.json +5 -1
  26. package/plugin/.claude-plugin/plugin.json +1 -1
  27. package/plugin/bundle/mcp-server.mjs +452 -0
  28. package/plugin/bundle/mcp-server.mjs.map +4 -4
  29. package/plugin/package.json +1 -1
  30. package/src/index.ts +98 -0
  31. package/src/scanner/auto-scan.ts +212 -0
  32. package/src/scanner/detector.ts +224 -0
  33. package/src/scanner/index.ts +22 -0
  34. package/src/scanner/runner.ts +212 -0
  35. package/src/schemas/tool-schemas.ts +13 -0
  36. package/src/tests/auto-scan.test.ts +137 -0
  37. package/src/tests/integration/mcp-server.integration.test.ts +2 -1
  38. package/src/tests/scanner-detector.test.ts +181 -0
  39. package/src/tests/scanner-runner.test.ts +63 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Execute a single scanner CLI and capture its raw output.
3
+ *
4
+ * Each scanner has a fixed invocation that produces the format its
5
+ * adapter expects:
6
+ *
7
+ * - ESLint → `npx eslint -f json .` (JSON array)
8
+ * - Semgrep → `semgrep --sarif --quiet .` (SARIF 2.1.0)
9
+ * - Bandit → `bandit -f json -r . -q` (JSON object)
10
+ * - Stryker → `npx stryker run` then read `reports/mutation/mutation.json`
11
+ *
12
+ * ESLint and Bandit exit non-zero when findings exist — that is
13
+ * expected, not an error. The runner captures stdout regardless of
14
+ * exit code for those scanners.
15
+ *
16
+ * Stryker is special: it writes to a file instead of stdout, so we
17
+ * read the output file after the process exits.
18
+ *
19
+ * @module scanner/runner
20
+ */
21
+
22
+ import { execFile } from "node:child_process";
23
+ import { readFileSync, existsSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import type { KnownScanner } from "../adapters/common.js";
26
+
27
+ // ── Types ──────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Result of executing a single scanner.
31
+ */
32
+ export interface ScannerRunResult {
33
+ /** Which scanner was executed. */
34
+ scanner: KnownScanner;
35
+ /** Whether execution completed and produced parseable output. */
36
+ success: boolean;
37
+ /** The scanner's raw output (stdout or file contents). */
38
+ rawOutput: string;
39
+ /** Error message when `success` is false. */
40
+ error?: string;
41
+ /** Wall-clock execution time in milliseconds. */
42
+ durationMs: number;
43
+ }
44
+
45
+ // ── Scanner command definitions ────────────────────────────────────
46
+
47
+ interface ScannerCommand {
48
+ /** Binary or npx command. */
49
+ command: string;
50
+ /** CLI arguments. */
51
+ args: string[];
52
+ /** Maximum execution time in ms. */
53
+ timeoutMs: number;
54
+ /** If true, non-zero exit is expected when findings exist. */
55
+ nonZeroIsNormal: boolean;
56
+ /** If set, read output from this file instead of stdout. */
57
+ outputFile?: string;
58
+ }
59
+
60
+ function getScannerCommand(
61
+ scanner: KnownScanner,
62
+ workspaceRoot: string,
63
+ ): ScannerCommand {
64
+ switch (scanner) {
65
+ case "eslint":
66
+ return {
67
+ command: "npx",
68
+ args: ["eslint", "-f", "json", "."],
69
+ timeoutMs: 120_000,
70
+ nonZeroIsNormal: true,
71
+ };
72
+ case "semgrep":
73
+ return {
74
+ command: "semgrep",
75
+ args: ["--sarif", "--quiet", "."],
76
+ timeoutMs: 120_000,
77
+ nonZeroIsNormal: false,
78
+ };
79
+ case "bandit":
80
+ return {
81
+ command: "bandit",
82
+ args: ["-f", "json", "-r", ".", "-q"],
83
+ timeoutMs: 120_000,
84
+ nonZeroIsNormal: true,
85
+ };
86
+ case "stryker":
87
+ return {
88
+ command: "npx",
89
+ args: ["stryker", "run"],
90
+ timeoutMs: 300_000,
91
+ nonZeroIsNormal: false,
92
+ outputFile: join(workspaceRoot, "reports", "mutation", "mutation.json"),
93
+ };
94
+ }
95
+ }
96
+
97
+ // ── Public API ──────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * Execute a scanner CLI and return its raw output.
101
+ *
102
+ * @param scanner Which scanner to run.
103
+ * @param workspaceRoot Absolute path to the project root (used as cwd).
104
+ * @returns A {@link ScannerRunResult} with stdout or file output.
105
+ */
106
+ export function runScanner(
107
+ scanner: KnownScanner,
108
+ workspaceRoot: string,
109
+ ): Promise<ScannerRunResult> {
110
+ const start = Date.now();
111
+ const cmd = getScannerCommand(scanner, workspaceRoot);
112
+
113
+ return new Promise((resolve) => {
114
+ execFile(
115
+ cmd.command,
116
+ cmd.args,
117
+ {
118
+ cwd: workspaceRoot,
119
+ timeout: cmd.timeoutMs,
120
+ maxBuffer: 50 * 1024 * 1024, // 50 MB — large codebases produce verbose output
121
+ env: { ...process.env, FORCE_COLOR: "0" }, // suppress ANSI in output
122
+ },
123
+ (err, stdout, stderr) => {
124
+ const durationMs = Date.now() - start;
125
+
126
+ // For scanners where non-zero exit means "findings exist",
127
+ // we still have valid output in stdout.
128
+ if (err && !cmd.nonZeroIsNormal) {
129
+ // Stryker: check if the output file was written despite the error
130
+ if (cmd.outputFile && existsSync(cmd.outputFile)) {
131
+ try {
132
+ const fileOutput = readFileSync(cmd.outputFile, "utf-8");
133
+ resolve({
134
+ scanner,
135
+ success: true,
136
+ rawOutput: fileOutput,
137
+ durationMs,
138
+ });
139
+ return;
140
+ } catch {
141
+ // Fall through to error path
142
+ }
143
+ }
144
+
145
+ resolve({
146
+ scanner,
147
+ success: false,
148
+ rawOutput: "",
149
+ error: stderr || (err as Error).message,
150
+ durationMs,
151
+ });
152
+ return;
153
+ }
154
+
155
+ // For file-based output (Stryker), read from file
156
+ if (cmd.outputFile) {
157
+ if (existsSync(cmd.outputFile)) {
158
+ try {
159
+ const fileOutput = readFileSync(cmd.outputFile, "utf-8");
160
+ resolve({
161
+ scanner,
162
+ success: true,
163
+ rawOutput: fileOutput,
164
+ durationMs,
165
+ });
166
+ return;
167
+ } catch (readErr) {
168
+ resolve({
169
+ scanner,
170
+ success: false,
171
+ rawOutput: "",
172
+ error: `Failed to read output file: ${(readErr as Error).message}`,
173
+ durationMs,
174
+ });
175
+ return;
176
+ }
177
+ }
178
+ resolve({
179
+ scanner,
180
+ success: false,
181
+ rawOutput: "",
182
+ error: `Scanner completed but output file not found: ${cmd.outputFile}`,
183
+ durationMs,
184
+ });
185
+ return;
186
+ }
187
+
188
+ // Stdout-based output
189
+ const output = stdout.trim();
190
+ if (!output) {
191
+ resolve({
192
+ scanner,
193
+ success: true,
194
+ rawOutput: "[]", // ESLint returns empty when no files match
195
+ durationMs,
196
+ });
197
+ return;
198
+ }
199
+
200
+ resolve({
201
+ scanner,
202
+ success: true,
203
+ rawOutput: output,
204
+ durationMs,
205
+ });
206
+ },
207
+ );
208
+ });
209
+ }
210
+
211
+ // Exported for testing
212
+ export { getScannerCommand, type ScannerCommand };
@@ -199,6 +199,19 @@ export const ingestScannerOutputSchema = {
199
199
  * from an external scanner, deduplicates against the internal store, and
200
200
  * normalizes the output into claude-crap's canonical format.
201
201
  */
202
+ /**
203
+ * Schema for the `auto_scan` tool. Auto-detects available scanners
204
+ * in the workspace, runs them, and ingests findings into the SARIF store.
205
+ */
206
+ export const autoScanSchema = {
207
+ type: "object",
208
+ description:
209
+ "Auto-detect available scanners (ESLint, Semgrep, Bandit, Stryker) in the workspace, execute them, and ingest findings into the SARIF store. Returns detection results, per-scanner execution stats, and total findings ingested. Call this to populate findings without manual scanner invocation.",
210
+ properties: {},
211
+ required: [],
212
+ additionalProperties: false,
213
+ } as const;
214
+
202
215
  export const ingestSarifSchema = {
203
216
  type: "object",
204
217
  description:
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Unit tests for the auto-scan orchestrator.
3
+ *
4
+ * These tests verify the orchestration logic: detection → run → ingest.
5
+ * They use a real (temporary) workspace with config files to trigger
6
+ * detection, but scanner execution will fail (binaries aren't installed
7
+ * in the test environment). This tests the graceful-failure path.
8
+ *
9
+ * @module tests/auto-scan.test
10
+ */
11
+
12
+ import { describe, it } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { tmpdir } from "node:os";
17
+ import pino from "pino";
18
+
19
+ import { autoScan, type AutoScanResult } from "../scanner/auto-scan.js";
20
+ import { SarifStore } from "../sarif/sarif-store.js";
21
+
22
+ const logger = pino({ level: "silent" });
23
+
24
+ function makeTmpDir(): string {
25
+ return mkdtempSync(join(tmpdir(), "crap-autoscan-"));
26
+ }
27
+
28
+ describe("autoScan", () => {
29
+ it("returns empty results when no scanners are detected", async () => {
30
+ const dir = makeTmpDir();
31
+ try {
32
+ const store = new SarifStore({
33
+ workspaceRoot: dir,
34
+ outputDir: join(dir, ".claude-crap/reports"),
35
+ });
36
+ const result = await autoScan(dir, store, logger);
37
+ assert.equal(result.detected.length, 4);
38
+ assert.ok(result.totalDurationMs >= 0);
39
+ // No scanners available means no results
40
+ // (unless the host has scanner binaries installed)
41
+ assert.ok(Array.isArray(result.results));
42
+ assert.equal(typeof result.totalFindings, "number");
43
+ } finally {
44
+ rmSync(dir, { recursive: true, force: true });
45
+ }
46
+ });
47
+
48
+ it("detects scanners from config files in workspace", async () => {
49
+ const dir = makeTmpDir();
50
+ try {
51
+ writeFileSync(join(dir, "eslint.config.mjs"), "export default [];");
52
+ writeFileSync(join(dir, ".semgrep.yml"), "rules: []");
53
+
54
+ const store = new SarifStore({
55
+ workspaceRoot: dir,
56
+ outputDir: join(dir, ".claude-crap/reports"),
57
+ });
58
+ const result = await autoScan(dir, store, logger);
59
+
60
+ // ESLint and Semgrep should be detected
61
+ const eslintDetection = result.detected.find((d) => d.scanner === "eslint");
62
+ const semgrepDetection = result.detected.find((d) => d.scanner === "semgrep");
63
+ assert.ok(eslintDetection);
64
+ assert.equal(eslintDetection.available, true);
65
+ assert.ok(semgrepDetection);
66
+ assert.equal(semgrepDetection.available, true);
67
+ } finally {
68
+ rmSync(dir, { recursive: true, force: true });
69
+ }
70
+ });
71
+
72
+ it("returns correct structure even when all scanners fail", async () => {
73
+ const dir = makeTmpDir();
74
+ try {
75
+ // Create config files so scanners are detected but will fail to run
76
+ // (the temp dir isn't a real project)
77
+ writeFileSync(join(dir, "eslint.config.mjs"), "export default [];");
78
+
79
+ const store = new SarifStore({
80
+ workspaceRoot: dir,
81
+ outputDir: join(dir, ".claude-crap/reports"),
82
+ });
83
+ const result = await autoScan(dir, store, logger);
84
+
85
+ // Structure must be valid regardless of scanner success
86
+ assert.ok(Array.isArray(result.detected));
87
+ assert.ok(Array.isArray(result.results));
88
+ assert.equal(typeof result.totalFindings, "number");
89
+ assert.equal(typeof result.totalDurationMs, "number");
90
+
91
+ // eslint was detected, so it should appear in results
92
+ const eslintResult = result.results.find((r) => r.scanner === "eslint");
93
+ if (eslintResult) {
94
+ assert.equal(typeof eslintResult.success, "boolean");
95
+ assert.equal(typeof eslintResult.durationMs, "number");
96
+ assert.equal(typeof eslintResult.findingsIngested, "number");
97
+ }
98
+ } finally {
99
+ rmSync(dir, { recursive: true, force: true });
100
+ }
101
+ });
102
+
103
+ it("totalFindings sums findings across all successful scanners", async () => {
104
+ const dir = makeTmpDir();
105
+ try {
106
+ const store = new SarifStore({
107
+ workspaceRoot: dir,
108
+ outputDir: join(dir, ".claude-crap/reports"),
109
+ });
110
+ const result = await autoScan(dir, store, logger);
111
+
112
+ // Sum of individual scanner findings should equal total
113
+ const sumFromResults = result.results
114
+ .filter((r) => r.success)
115
+ .reduce((sum, r) => sum + r.findingsIngested, 0);
116
+ assert.equal(result.totalFindings, sumFromResults);
117
+ } finally {
118
+ rmSync(dir, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ it("result has one ScannerDetection per known scanner", async () => {
123
+ const dir = makeTmpDir();
124
+ try {
125
+ const store = new SarifStore({
126
+ workspaceRoot: dir,
127
+ outputDir: join(dir, ".claude-crap/reports"),
128
+ });
129
+ const result = await autoScan(dir, store, logger);
130
+
131
+ const scannerNames = result.detected.map((d) => d.scanner).sort();
132
+ assert.deepEqual(scannerNames, ["bandit", "eslint", "semgrep", "stryker"]);
133
+ } finally {
134
+ rmSync(dir, { recursive: true, force: true });
135
+ }
136
+ });
137
+ });
@@ -232,13 +232,14 @@ describe("MCP server integration", { skip: !serverBuilt }, () => {
232
232
  assert.ok(child && !child.killed, "server child should be running");
233
233
  });
234
234
 
235
- it("exposes all seven tools via tools/list", async () => {
235
+ it("exposes all eight tools via tools/list", async () => {
236
236
  const response = await client!.request<{ result?: { tools?: Array<{ name: string }> } }>(
237
237
  "tools/list",
238
238
  );
239
239
  const names = (response.result?.tools ?? []).map((t) => t.name).sort();
240
240
  assert.deepEqual(names, [
241
241
  "analyze_file_ast",
242
+ "auto_scan",
242
243
  "compute_crap",
243
244
  "compute_tdr",
244
245
  "ingest_sarif",
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Unit tests for the scanner auto-detector.
3
+ *
4
+ * These tests probe the detection logic for each scanner type:
5
+ * config file detection, package.json dependency detection, and
6
+ * the fallback to binary availability.
7
+ *
8
+ * @module tests/scanner-detector.test
9
+ */
10
+
11
+ import { describe, it } from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+
17
+ import { detectScanners, SCANNER_SIGNALS } from "../scanner/detector.js";
18
+
19
+ function makeTmpDir(): string {
20
+ return mkdtempSync(join(tmpdir(), "crap-detect-"));
21
+ }
22
+
23
+ describe("detectScanners", () => {
24
+ it("detects eslint when eslint.config.mjs exists", async () => {
25
+ const dir = makeTmpDir();
26
+ try {
27
+ writeFileSync(join(dir, "eslint.config.mjs"), "export default [];");
28
+ const results = await detectScanners(dir);
29
+ const eslint = results.find((r) => r.scanner === "eslint");
30
+ assert.ok(eslint);
31
+ assert.equal(eslint.available, true);
32
+ assert.ok(eslint.reason.includes("eslint.config.mjs"));
33
+ assert.ok(eslint.configPath);
34
+ } finally {
35
+ rmSync(dir, { recursive: true, force: true });
36
+ }
37
+ });
38
+
39
+ it("detects eslint from .eslintrc.json", async () => {
40
+ const dir = makeTmpDir();
41
+ try {
42
+ writeFileSync(join(dir, ".eslintrc.json"), "{}");
43
+ const results = await detectScanners(dir);
44
+ const eslint = results.find((r) => r.scanner === "eslint");
45
+ assert.ok(eslint);
46
+ assert.equal(eslint.available, true);
47
+ assert.ok(eslint.reason.includes(".eslintrc.json"));
48
+ } finally {
49
+ rmSync(dir, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ it("detects semgrep when .semgrep.yml exists", async () => {
54
+ const dir = makeTmpDir();
55
+ try {
56
+ writeFileSync(join(dir, ".semgrep.yml"), "rules: []");
57
+ const results = await detectScanners(dir);
58
+ const semgrep = results.find((r) => r.scanner === "semgrep");
59
+ assert.ok(semgrep);
60
+ assert.equal(semgrep.available, true);
61
+ assert.ok(semgrep.reason.includes(".semgrep.yml"));
62
+ } finally {
63
+ rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ it("detects bandit when .bandit config exists", async () => {
68
+ const dir = makeTmpDir();
69
+ try {
70
+ writeFileSync(join(dir, ".bandit"), "");
71
+ const results = await detectScanners(dir);
72
+ const bandit = results.find((r) => r.scanner === "bandit");
73
+ assert.ok(bandit);
74
+ assert.equal(bandit.available, true);
75
+ } finally {
76
+ rmSync(dir, { recursive: true, force: true });
77
+ }
78
+ });
79
+
80
+ it("detects stryker when stryker.conf.js exists", async () => {
81
+ const dir = makeTmpDir();
82
+ try {
83
+ writeFileSync(join(dir, "stryker.conf.js"), "module.exports = {};");
84
+ const results = await detectScanners(dir);
85
+ const stryker = results.find((r) => r.scanner === "stryker");
86
+ assert.ok(stryker);
87
+ assert.equal(stryker.available, true);
88
+ } finally {
89
+ rmSync(dir, { recursive: true, force: true });
90
+ }
91
+ });
92
+
93
+ it("detects eslint from package.json devDependencies", async () => {
94
+ const dir = makeTmpDir();
95
+ try {
96
+ writeFileSync(
97
+ join(dir, "package.json"),
98
+ JSON.stringify({ devDependencies: { eslint: "^9.0.0" } }),
99
+ );
100
+ const results = await detectScanners(dir);
101
+ const eslint = results.find((r) => r.scanner === "eslint");
102
+ assert.ok(eslint);
103
+ assert.equal(eslint.available, true);
104
+ assert.ok(eslint.reason.includes("package.json"));
105
+ } finally {
106
+ rmSync(dir, { recursive: true, force: true });
107
+ }
108
+ });
109
+
110
+ it("detects stryker from package.json @stryker-mutator/core", async () => {
111
+ const dir = makeTmpDir();
112
+ try {
113
+ writeFileSync(
114
+ join(dir, "package.json"),
115
+ JSON.stringify({
116
+ devDependencies: { "@stryker-mutator/core": "^7.0.0" },
117
+ }),
118
+ );
119
+ const results = await detectScanners(dir);
120
+ const stryker = results.find((r) => r.scanner === "stryker");
121
+ assert.ok(stryker);
122
+ assert.equal(stryker.available, true);
123
+ assert.ok(stryker.reason.includes("package.json"));
124
+ } finally {
125
+ rmSync(dir, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ it("returns available:false for all scanners in an empty directory", async () => {
130
+ const dir = makeTmpDir();
131
+ try {
132
+ const results = await detectScanners(dir);
133
+ // Config and package.json probes will all fail.
134
+ // Binary probe results depend on the host — don't assert on those,
135
+ // but do assert the structure is correct.
136
+ assert.equal(results.length, 4);
137
+ for (const r of results) {
138
+ assert.ok(["eslint", "semgrep", "bandit", "stryker"].includes(r.scanner));
139
+ assert.equal(typeof r.available, "boolean");
140
+ assert.equal(typeof r.reason, "string");
141
+ }
142
+ } finally {
143
+ rmSync(dir, { recursive: true, force: true });
144
+ }
145
+ });
146
+
147
+ it("handles malformed package.json gracefully", async () => {
148
+ const dir = makeTmpDir();
149
+ try {
150
+ writeFileSync(join(dir, "package.json"), "not json at all");
151
+ // Should not throw — just skip the package.json probe
152
+ const results = await detectScanners(dir);
153
+ assert.equal(results.length, 4);
154
+ } finally {
155
+ rmSync(dir, { recursive: true, force: true });
156
+ }
157
+ });
158
+
159
+ it("short-circuits on config file — does not need binary", async () => {
160
+ const dir = makeTmpDir();
161
+ try {
162
+ writeFileSync(join(dir, "eslint.config.mjs"), "export default [];");
163
+ const results = await detectScanners(dir);
164
+ const eslint = results.find((r) => r.scanner === "eslint");
165
+ assert.ok(eslint);
166
+ assert.equal(eslint.available, true);
167
+ // Reason mentions config file, not binary
168
+ assert.ok(eslint.reason.includes("config file"));
169
+ assert.ok(!eslint.reason.includes("binary"));
170
+ } finally {
171
+ rmSync(dir, { recursive: true, force: true });
172
+ }
173
+ });
174
+
175
+ it("SCANNER_SIGNALS covers all four scanners", () => {
176
+ assert.deepEqual(
177
+ Object.keys(SCANNER_SIGNALS).sort(),
178
+ ["bandit", "eslint", "semgrep", "stryker"],
179
+ );
180
+ });
181
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Unit tests for the scanner runner.
3
+ *
4
+ * These tests verify the command definitions and error handling of the
5
+ * runner module. Actual scanner execution is not tested here — that
6
+ * requires the scanner binaries to be installed.
7
+ *
8
+ * @module tests/scanner-runner.test
9
+ */
10
+
11
+ import { describe, it } from "node:test";
12
+ import assert from "node:assert/strict";
13
+
14
+ import { getScannerCommand, type ScannerCommand } from "../scanner/runner.js";
15
+
16
+ describe("getScannerCommand", () => {
17
+ it("returns correct eslint command", () => {
18
+ const cmd = getScannerCommand("eslint", "/tmp/project");
19
+ assert.equal(cmd.command, "npx");
20
+ assert.deepEqual(cmd.args, ["eslint", "-f", "json", "."]);
21
+ assert.equal(cmd.nonZeroIsNormal, true);
22
+ assert.equal(cmd.outputFile, undefined);
23
+ });
24
+
25
+ it("returns correct semgrep command", () => {
26
+ const cmd = getScannerCommand("semgrep", "/tmp/project");
27
+ assert.equal(cmd.command, "semgrep");
28
+ assert.deepEqual(cmd.args, ["--sarif", "--quiet", "."]);
29
+ assert.equal(cmd.nonZeroIsNormal, false);
30
+ assert.equal(cmd.outputFile, undefined);
31
+ });
32
+
33
+ it("returns correct bandit command", () => {
34
+ const cmd = getScannerCommand("bandit", "/tmp/project");
35
+ assert.equal(cmd.command, "bandit");
36
+ assert.deepEqual(cmd.args, ["-f", "json", "-r", ".", "-q"]);
37
+ assert.equal(cmd.nonZeroIsNormal, true);
38
+ assert.equal(cmd.outputFile, undefined);
39
+ });
40
+
41
+ it("returns correct stryker command with output file", () => {
42
+ const cmd = getScannerCommand("stryker", "/tmp/project");
43
+ assert.equal(cmd.command, "npx");
44
+ assert.deepEqual(cmd.args, ["stryker", "run"]);
45
+ assert.equal(cmd.nonZeroIsNormal, false);
46
+ assert.ok(cmd.outputFile);
47
+ assert.ok(cmd.outputFile.includes("mutation.json"));
48
+ });
49
+
50
+ it("all scanners have reasonable timeouts", () => {
51
+ for (const scanner of ["eslint", "semgrep", "bandit", "stryker"] as const) {
52
+ const cmd = getScannerCommand(scanner, "/tmp");
53
+ assert.ok(cmd.timeoutMs >= 60_000, `${scanner} timeout should be >= 60s`);
54
+ assert.ok(cmd.timeoutMs <= 300_000, `${scanner} timeout should be <= 300s`);
55
+ }
56
+ });
57
+
58
+ it("stryker has a longer timeout than other scanners", () => {
59
+ const stryker = getScannerCommand("stryker", "/tmp");
60
+ const eslint = getScannerCommand("eslint", "/tmp");
61
+ assert.ok(stryker.timeoutMs > eslint.timeoutMs);
62
+ });
63
+ });