claude-crap 0.1.2 → 0.3.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.
- package/CHANGELOG.md +68 -0
- package/README.md +44 -23
- package/dist/index.js +142 -1
- package/dist/index.js.map +1 -1
- package/dist/scanner/auto-scan.d.ts +57 -0
- package/dist/scanner/auto-scan.d.ts.map +1 -0
- package/dist/scanner/auto-scan.js +138 -0
- package/dist/scanner/auto-scan.js.map +1 -0
- package/dist/scanner/bootstrap.d.ts +89 -0
- package/dist/scanner/bootstrap.d.ts.map +1 -0
- package/dist/scanner/bootstrap.js +278 -0
- package/dist/scanner/bootstrap.js.map +1 -0
- package/dist/scanner/detector.d.ts +53 -0
- package/dist/scanner/detector.d.ts.map +1 -0
- package/dist/scanner/detector.js +173 -0
- package/dist/scanner/detector.js.map +1 -0
- package/dist/scanner/index.d.ts +23 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +23 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/runner.d.ts +59 -0
- package/dist/scanner/runner.d.ts.map +1 -0
- package/dist/scanner/runner.js +159 -0
- package/dist/scanner/runner.js.map +1 -0
- package/dist/schemas/tool-schemas.d.ts +23 -0
- package/dist/schemas/tool-schemas.d.ts.map +1 -1
- package/dist/schemas/tool-schemas.js +23 -0
- package/dist/schemas/tool-schemas.js.map +1 -1
- package/package.json +5 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/mcp-server.mjs +732 -0
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package.json +1 -1
- package/src/index.ts +176 -0
- package/src/scanner/auto-scan.ts +212 -0
- package/src/scanner/bootstrap.ts +383 -0
- package/src/scanner/detector.ts +224 -0
- package/src/scanner/index.ts +30 -0
- package/src/scanner/runner.ts +212 -0
- package/src/schemas/tool-schemas.ts +27 -0
- package/src/tests/auto-scan.test.ts +137 -0
- package/src/tests/integration/mcp-server.integration.test.ts +3 -1
- package/src/tests/scanner-bootstrap.test.ts +186 -0
- package/src/tests/scanner-detector.test.ts +181 -0
- package/src/tests/scanner-runner.test.ts +63 -0
|
@@ -8083,6 +8083,586 @@ function resolveWithinWorkspace(workspaceRoot, filePath) {
|
|
|
8083
8083
|
return candidate;
|
|
8084
8084
|
}
|
|
8085
8085
|
|
|
8086
|
+
// src/scanner/detector.ts
|
|
8087
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
8088
|
+
import { join as join6 } from "node:path";
|
|
8089
|
+
import { execFile } from "node:child_process";
|
|
8090
|
+
var SCANNER_SIGNALS = {
|
|
8091
|
+
eslint: {
|
|
8092
|
+
configFiles: [
|
|
8093
|
+
"eslint.config.js",
|
|
8094
|
+
"eslint.config.mjs",
|
|
8095
|
+
"eslint.config.cjs",
|
|
8096
|
+
"eslint.config.ts",
|
|
8097
|
+
"eslint.config.mts",
|
|
8098
|
+
"eslint.config.cts",
|
|
8099
|
+
".eslintrc.js",
|
|
8100
|
+
".eslintrc.cjs",
|
|
8101
|
+
".eslintrc.yaml",
|
|
8102
|
+
".eslintrc.yml",
|
|
8103
|
+
".eslintrc.json"
|
|
8104
|
+
],
|
|
8105
|
+
packageJsonKeys: ["eslint"],
|
|
8106
|
+
binaryNames: ["eslint"]
|
|
8107
|
+
},
|
|
8108
|
+
semgrep: {
|
|
8109
|
+
configFiles: [
|
|
8110
|
+
".semgrep.yml",
|
|
8111
|
+
".semgrep.yaml",
|
|
8112
|
+
".semgrep.json"
|
|
8113
|
+
],
|
|
8114
|
+
packageJsonKeys: [],
|
|
8115
|
+
binaryNames: ["semgrep"]
|
|
8116
|
+
},
|
|
8117
|
+
bandit: {
|
|
8118
|
+
configFiles: [
|
|
8119
|
+
".bandit",
|
|
8120
|
+
"bandit.yaml",
|
|
8121
|
+
"bandit.yml"
|
|
8122
|
+
],
|
|
8123
|
+
packageJsonKeys: [],
|
|
8124
|
+
binaryNames: ["bandit"]
|
|
8125
|
+
},
|
|
8126
|
+
stryker: {
|
|
8127
|
+
configFiles: [
|
|
8128
|
+
"stryker.conf.js",
|
|
8129
|
+
"stryker.conf.mjs",
|
|
8130
|
+
"stryker.conf.cjs",
|
|
8131
|
+
"stryker.conf.json",
|
|
8132
|
+
".strykerrc",
|
|
8133
|
+
".strykerrc.json"
|
|
8134
|
+
],
|
|
8135
|
+
packageJsonKeys: ["@stryker-mutator/core"],
|
|
8136
|
+
binaryNames: ["stryker"]
|
|
8137
|
+
}
|
|
8138
|
+
};
|
|
8139
|
+
function probeConfigFiles(workspaceRoot, scanner) {
|
|
8140
|
+
const signals = SCANNER_SIGNALS[scanner];
|
|
8141
|
+
for (const file of signals.configFiles) {
|
|
8142
|
+
const fullPath = join6(workspaceRoot, file);
|
|
8143
|
+
if (existsSync(fullPath)) {
|
|
8144
|
+
return { found: true, path: fullPath };
|
|
8145
|
+
}
|
|
8146
|
+
}
|
|
8147
|
+
return { found: false };
|
|
8148
|
+
}
|
|
8149
|
+
function probePackageJson(workspaceRoot, scanner) {
|
|
8150
|
+
const signals = SCANNER_SIGNALS[scanner];
|
|
8151
|
+
if (signals.packageJsonKeys.length === 0) return false;
|
|
8152
|
+
const pkgPath = join6(workspaceRoot, "package.json");
|
|
8153
|
+
if (!existsSync(pkgPath)) return false;
|
|
8154
|
+
try {
|
|
8155
|
+
const raw = readFileSync2(pkgPath, "utf-8");
|
|
8156
|
+
const pkg = JSON.parse(raw);
|
|
8157
|
+
const deps = {
|
|
8158
|
+
...typeof pkg.dependencies === "object" && pkg.dependencies !== null ? pkg.dependencies : {},
|
|
8159
|
+
...typeof pkg.devDependencies === "object" && pkg.devDependencies !== null ? pkg.devDependencies : {}
|
|
8160
|
+
};
|
|
8161
|
+
return signals.packageJsonKeys.some((key) => key in deps);
|
|
8162
|
+
} catch {
|
|
8163
|
+
return false;
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
function probeBinary(binaryName) {
|
|
8167
|
+
return new Promise((resolve6) => {
|
|
8168
|
+
execFile("which", [binaryName], { timeout: 5e3 }, (err) => {
|
|
8169
|
+
resolve6(err === null);
|
|
8170
|
+
});
|
|
8171
|
+
});
|
|
8172
|
+
}
|
|
8173
|
+
async function detectScanners(workspaceRoot) {
|
|
8174
|
+
const scanners = ["eslint", "semgrep", "bandit", "stryker"];
|
|
8175
|
+
const results = await Promise.all(
|
|
8176
|
+
scanners.map(async (scanner) => {
|
|
8177
|
+
const configProbe = probeConfigFiles(workspaceRoot, scanner);
|
|
8178
|
+
if (configProbe.found && configProbe.path) {
|
|
8179
|
+
return {
|
|
8180
|
+
scanner,
|
|
8181
|
+
available: true,
|
|
8182
|
+
reason: `config file found: ${configProbe.path.replace(workspaceRoot + "/", "")}`,
|
|
8183
|
+
configPath: configProbe.path
|
|
8184
|
+
};
|
|
8185
|
+
}
|
|
8186
|
+
if (probePackageJson(workspaceRoot, scanner)) {
|
|
8187
|
+
return {
|
|
8188
|
+
scanner,
|
|
8189
|
+
available: true,
|
|
8190
|
+
reason: `found in package.json dependencies`
|
|
8191
|
+
};
|
|
8192
|
+
}
|
|
8193
|
+
const signals = SCANNER_SIGNALS[scanner];
|
|
8194
|
+
for (const bin of signals.binaryNames) {
|
|
8195
|
+
if (await probeBinary(bin)) {
|
|
8196
|
+
return {
|
|
8197
|
+
scanner,
|
|
8198
|
+
available: true,
|
|
8199
|
+
reason: `binary "${bin}" found on PATH`
|
|
8200
|
+
};
|
|
8201
|
+
}
|
|
8202
|
+
}
|
|
8203
|
+
return {
|
|
8204
|
+
scanner,
|
|
8205
|
+
available: false,
|
|
8206
|
+
reason: "no config file, package.json entry, or binary found"
|
|
8207
|
+
};
|
|
8208
|
+
})
|
|
8209
|
+
);
|
|
8210
|
+
return results;
|
|
8211
|
+
}
|
|
8212
|
+
|
|
8213
|
+
// src/scanner/runner.ts
|
|
8214
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
8215
|
+
import { readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
|
|
8216
|
+
import { join as join7 } from "node:path";
|
|
8217
|
+
function getScannerCommand(scanner, workspaceRoot) {
|
|
8218
|
+
switch (scanner) {
|
|
8219
|
+
case "eslint":
|
|
8220
|
+
return {
|
|
8221
|
+
command: "npx",
|
|
8222
|
+
args: ["eslint", "-f", "json", "."],
|
|
8223
|
+
timeoutMs: 12e4,
|
|
8224
|
+
nonZeroIsNormal: true
|
|
8225
|
+
};
|
|
8226
|
+
case "semgrep":
|
|
8227
|
+
return {
|
|
8228
|
+
command: "semgrep",
|
|
8229
|
+
args: ["--sarif", "--quiet", "."],
|
|
8230
|
+
timeoutMs: 12e4,
|
|
8231
|
+
nonZeroIsNormal: false
|
|
8232
|
+
};
|
|
8233
|
+
case "bandit":
|
|
8234
|
+
return {
|
|
8235
|
+
command: "bandit",
|
|
8236
|
+
args: ["-f", "json", "-r", ".", "-q"],
|
|
8237
|
+
timeoutMs: 12e4,
|
|
8238
|
+
nonZeroIsNormal: true
|
|
8239
|
+
};
|
|
8240
|
+
case "stryker":
|
|
8241
|
+
return {
|
|
8242
|
+
command: "npx",
|
|
8243
|
+
args: ["stryker", "run"],
|
|
8244
|
+
timeoutMs: 3e5,
|
|
8245
|
+
nonZeroIsNormal: false,
|
|
8246
|
+
outputFile: join7(workspaceRoot, "reports", "mutation", "mutation.json")
|
|
8247
|
+
};
|
|
8248
|
+
}
|
|
8249
|
+
}
|
|
8250
|
+
function runScanner(scanner, workspaceRoot) {
|
|
8251
|
+
const start = Date.now();
|
|
8252
|
+
const cmd = getScannerCommand(scanner, workspaceRoot);
|
|
8253
|
+
return new Promise((resolve6) => {
|
|
8254
|
+
execFile2(
|
|
8255
|
+
cmd.command,
|
|
8256
|
+
cmd.args,
|
|
8257
|
+
{
|
|
8258
|
+
cwd: workspaceRoot,
|
|
8259
|
+
timeout: cmd.timeoutMs,
|
|
8260
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
8261
|
+
// 50 MB — large codebases produce verbose output
|
|
8262
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
8263
|
+
// suppress ANSI in output
|
|
8264
|
+
},
|
|
8265
|
+
(err, stdout, stderr) => {
|
|
8266
|
+
const durationMs = Date.now() - start;
|
|
8267
|
+
if (err && !cmd.nonZeroIsNormal) {
|
|
8268
|
+
if (cmd.outputFile && existsSync2(cmd.outputFile)) {
|
|
8269
|
+
try {
|
|
8270
|
+
const fileOutput = readFileSync3(cmd.outputFile, "utf-8");
|
|
8271
|
+
resolve6({
|
|
8272
|
+
scanner,
|
|
8273
|
+
success: true,
|
|
8274
|
+
rawOutput: fileOutput,
|
|
8275
|
+
durationMs
|
|
8276
|
+
});
|
|
8277
|
+
return;
|
|
8278
|
+
} catch {
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8281
|
+
resolve6({
|
|
8282
|
+
scanner,
|
|
8283
|
+
success: false,
|
|
8284
|
+
rawOutput: "",
|
|
8285
|
+
error: stderr || err.message,
|
|
8286
|
+
durationMs
|
|
8287
|
+
});
|
|
8288
|
+
return;
|
|
8289
|
+
}
|
|
8290
|
+
if (cmd.outputFile) {
|
|
8291
|
+
if (existsSync2(cmd.outputFile)) {
|
|
8292
|
+
try {
|
|
8293
|
+
const fileOutput = readFileSync3(cmd.outputFile, "utf-8");
|
|
8294
|
+
resolve6({
|
|
8295
|
+
scanner,
|
|
8296
|
+
success: true,
|
|
8297
|
+
rawOutput: fileOutput,
|
|
8298
|
+
durationMs
|
|
8299
|
+
});
|
|
8300
|
+
return;
|
|
8301
|
+
} catch (readErr) {
|
|
8302
|
+
resolve6({
|
|
8303
|
+
scanner,
|
|
8304
|
+
success: false,
|
|
8305
|
+
rawOutput: "",
|
|
8306
|
+
error: `Failed to read output file: ${readErr.message}`,
|
|
8307
|
+
durationMs
|
|
8308
|
+
});
|
|
8309
|
+
return;
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
8312
|
+
resolve6({
|
|
8313
|
+
scanner,
|
|
8314
|
+
success: false,
|
|
8315
|
+
rawOutput: "",
|
|
8316
|
+
error: `Scanner completed but output file not found: ${cmd.outputFile}`,
|
|
8317
|
+
durationMs
|
|
8318
|
+
});
|
|
8319
|
+
return;
|
|
8320
|
+
}
|
|
8321
|
+
const output = stdout.trim();
|
|
8322
|
+
if (!output) {
|
|
8323
|
+
resolve6({
|
|
8324
|
+
scanner,
|
|
8325
|
+
success: true,
|
|
8326
|
+
rawOutput: "[]",
|
|
8327
|
+
// ESLint returns empty when no files match
|
|
8328
|
+
durationMs
|
|
8329
|
+
});
|
|
8330
|
+
return;
|
|
8331
|
+
}
|
|
8332
|
+
resolve6({
|
|
8333
|
+
scanner,
|
|
8334
|
+
success: true,
|
|
8335
|
+
rawOutput: output,
|
|
8336
|
+
durationMs
|
|
8337
|
+
});
|
|
8338
|
+
}
|
|
8339
|
+
);
|
|
8340
|
+
});
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8343
|
+
// src/scanner/auto-scan.ts
|
|
8344
|
+
function ingestScannerRun(scanner, rawOutput, sarifStore) {
|
|
8345
|
+
let parsed;
|
|
8346
|
+
try {
|
|
8347
|
+
parsed = JSON.parse(rawOutput);
|
|
8348
|
+
} catch {
|
|
8349
|
+
parsed = rawOutput;
|
|
8350
|
+
}
|
|
8351
|
+
const adapted = adaptScannerOutput(scanner, parsed);
|
|
8352
|
+
const stats = sarifStore.ingestRun(adapted.document, adapted.sourceTool);
|
|
8353
|
+
return { accepted: stats.accepted };
|
|
8354
|
+
}
|
|
8355
|
+
async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
8356
|
+
const start = Date.now();
|
|
8357
|
+
const detected = await detectScanners(workspaceRoot);
|
|
8358
|
+
const available = detected.filter((d) => d.available);
|
|
8359
|
+
logger2.info(
|
|
8360
|
+
{
|
|
8361
|
+
detected: detected.map((d) => `${d.scanner}:${d.available}`),
|
|
8362
|
+
available: available.length
|
|
8363
|
+
},
|
|
8364
|
+
"auto-scan: detection complete"
|
|
8365
|
+
);
|
|
8366
|
+
if (available.length === 0) {
|
|
8367
|
+
return {
|
|
8368
|
+
detected,
|
|
8369
|
+
results: [],
|
|
8370
|
+
totalFindings: 0,
|
|
8371
|
+
totalDurationMs: Date.now() - start
|
|
8372
|
+
};
|
|
8373
|
+
}
|
|
8374
|
+
const runResults = await Promise.allSettled(
|
|
8375
|
+
available.map((d) => runScanner(d.scanner, workspaceRoot))
|
|
8376
|
+
);
|
|
8377
|
+
const results = [];
|
|
8378
|
+
let totalFindings = 0;
|
|
8379
|
+
let persistNeeded = false;
|
|
8380
|
+
for (let i = 0; i < available.length; i++) {
|
|
8381
|
+
const detection = available[i];
|
|
8382
|
+
const settled = runResults[i];
|
|
8383
|
+
if (settled.status === "rejected") {
|
|
8384
|
+
const error = String(settled.reason);
|
|
8385
|
+
logger2.warn(
|
|
8386
|
+
{ scanner: detection.scanner, error },
|
|
8387
|
+
"auto-scan: scanner execution rejected"
|
|
8388
|
+
);
|
|
8389
|
+
results.push({
|
|
8390
|
+
scanner: detection.scanner,
|
|
8391
|
+
success: false,
|
|
8392
|
+
findingsIngested: 0,
|
|
8393
|
+
durationMs: 0,
|
|
8394
|
+
error
|
|
8395
|
+
});
|
|
8396
|
+
continue;
|
|
8397
|
+
}
|
|
8398
|
+
const runResult = settled.value;
|
|
8399
|
+
if (!runResult.success) {
|
|
8400
|
+
logger2.warn(
|
|
8401
|
+
{ scanner: runResult.scanner, error: runResult.error },
|
|
8402
|
+
"auto-scan: scanner returned failure"
|
|
8403
|
+
);
|
|
8404
|
+
results.push({
|
|
8405
|
+
scanner: runResult.scanner,
|
|
8406
|
+
success: false,
|
|
8407
|
+
findingsIngested: 0,
|
|
8408
|
+
durationMs: runResult.durationMs,
|
|
8409
|
+
error: runResult.error ?? "unknown error"
|
|
8410
|
+
});
|
|
8411
|
+
continue;
|
|
8412
|
+
}
|
|
8413
|
+
try {
|
|
8414
|
+
const { accepted } = ingestScannerRun(
|
|
8415
|
+
runResult.scanner,
|
|
8416
|
+
runResult.rawOutput,
|
|
8417
|
+
sarifStore
|
|
8418
|
+
);
|
|
8419
|
+
totalFindings += accepted;
|
|
8420
|
+
persistNeeded = true;
|
|
8421
|
+
logger2.info(
|
|
8422
|
+
{ scanner: runResult.scanner, accepted, durationMs: runResult.durationMs },
|
|
8423
|
+
"auto-scan: scanner ingested"
|
|
8424
|
+
);
|
|
8425
|
+
results.push({
|
|
8426
|
+
scanner: runResult.scanner,
|
|
8427
|
+
success: true,
|
|
8428
|
+
findingsIngested: accepted,
|
|
8429
|
+
durationMs: runResult.durationMs
|
|
8430
|
+
});
|
|
8431
|
+
} catch (err) {
|
|
8432
|
+
const error = err.message;
|
|
8433
|
+
logger2.warn(
|
|
8434
|
+
{ scanner: runResult.scanner, error },
|
|
8435
|
+
"auto-scan: adapter/ingestion failed"
|
|
8436
|
+
);
|
|
8437
|
+
results.push({
|
|
8438
|
+
scanner: runResult.scanner,
|
|
8439
|
+
success: false,
|
|
8440
|
+
findingsIngested: 0,
|
|
8441
|
+
durationMs: runResult.durationMs,
|
|
8442
|
+
error
|
|
8443
|
+
});
|
|
8444
|
+
}
|
|
8445
|
+
}
|
|
8446
|
+
if (persistNeeded) {
|
|
8447
|
+
await sarifStore.persist();
|
|
8448
|
+
}
|
|
8449
|
+
return {
|
|
8450
|
+
detected,
|
|
8451
|
+
results,
|
|
8452
|
+
totalFindings,
|
|
8453
|
+
totalDurationMs: Date.now() - start
|
|
8454
|
+
};
|
|
8455
|
+
}
|
|
8456
|
+
|
|
8457
|
+
// src/scanner/bootstrap.ts
|
|
8458
|
+
import { existsSync as existsSync3, writeFileSync, readdirSync } from "node:fs";
|
|
8459
|
+
import { join as join8 } from "node:path";
|
|
8460
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
8461
|
+
function detectProjectType(workspaceRoot) {
|
|
8462
|
+
const has = (file) => existsSync3(join8(workspaceRoot, file));
|
|
8463
|
+
if (has("package.json")) {
|
|
8464
|
+
if (has("tsconfig.json")) return "typescript";
|
|
8465
|
+
return "javascript";
|
|
8466
|
+
}
|
|
8467
|
+
if (has("pyproject.toml") || has("setup.py") || has("requirements.txt")) {
|
|
8468
|
+
return "python";
|
|
8469
|
+
}
|
|
8470
|
+
if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) {
|
|
8471
|
+
return "java";
|
|
8472
|
+
}
|
|
8473
|
+
if (has("Directory.Build.props")) return "csharp";
|
|
8474
|
+
try {
|
|
8475
|
+
const entries = readdirSync(workspaceRoot);
|
|
8476
|
+
if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
|
|
8477
|
+
return "csharp";
|
|
8478
|
+
}
|
|
8479
|
+
} catch {
|
|
8480
|
+
}
|
|
8481
|
+
return "unknown";
|
|
8482
|
+
}
|
|
8483
|
+
function generateEslintConfig(isTypeScript) {
|
|
8484
|
+
if (isTypeScript) {
|
|
8485
|
+
return `import js from "@eslint/js";
|
|
8486
|
+
import tseslint from "typescript-eslint";
|
|
8487
|
+
|
|
8488
|
+
export default tseslint.config(
|
|
8489
|
+
js.configs.recommended,
|
|
8490
|
+
...tseslint.configs.recommended,
|
|
8491
|
+
{
|
|
8492
|
+
ignores: ["dist/", "node_modules/", "coverage/"],
|
|
8493
|
+
},
|
|
8494
|
+
);
|
|
8495
|
+
`;
|
|
8496
|
+
}
|
|
8497
|
+
return `import js from "@eslint/js";
|
|
8498
|
+
|
|
8499
|
+
export default [
|
|
8500
|
+
js.configs.recommended,
|
|
8501
|
+
{
|
|
8502
|
+
ignores: ["dist/", "node_modules/", "coverage/"],
|
|
8503
|
+
},
|
|
8504
|
+
];
|
|
8505
|
+
`;
|
|
8506
|
+
}
|
|
8507
|
+
function npmInstall(workspaceRoot, packages) {
|
|
8508
|
+
return new Promise((resolve6) => {
|
|
8509
|
+
execFile3(
|
|
8510
|
+
"npm",
|
|
8511
|
+
["install", "--save-dev", ...packages],
|
|
8512
|
+
{
|
|
8513
|
+
cwd: workspaceRoot,
|
|
8514
|
+
timeout: 12e4,
|
|
8515
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
8516
|
+
},
|
|
8517
|
+
(err, stdout, stderr) => {
|
|
8518
|
+
if (err) {
|
|
8519
|
+
resolve6({
|
|
8520
|
+
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8521
|
+
success: false,
|
|
8522
|
+
detail: stderr || err.message
|
|
8523
|
+
});
|
|
8524
|
+
return;
|
|
8525
|
+
}
|
|
8526
|
+
resolve6({
|
|
8527
|
+
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8528
|
+
success: true,
|
|
8529
|
+
detail: `installed ${packages.join(", ")}`
|
|
8530
|
+
});
|
|
8531
|
+
}
|
|
8532
|
+
);
|
|
8533
|
+
});
|
|
8534
|
+
}
|
|
8535
|
+
function writeEslintConfigFile(workspaceRoot, isTypeScript) {
|
|
8536
|
+
const configPath = join8(workspaceRoot, "eslint.config.mjs");
|
|
8537
|
+
if (existsSync3(configPath)) {
|
|
8538
|
+
return {
|
|
8539
|
+
action: "create eslint.config.mjs",
|
|
8540
|
+
success: true,
|
|
8541
|
+
detail: "eslint.config.mjs already exists \u2014 skipped"
|
|
8542
|
+
};
|
|
8543
|
+
}
|
|
8544
|
+
try {
|
|
8545
|
+
writeFileSync(configPath, generateEslintConfig(isTypeScript), "utf-8");
|
|
8546
|
+
return {
|
|
8547
|
+
action: "create eslint.config.mjs",
|
|
8548
|
+
success: true,
|
|
8549
|
+
detail: `created eslint.config.mjs (${isTypeScript ? "TypeScript" : "JavaScript"} template)`
|
|
8550
|
+
};
|
|
8551
|
+
} catch (err) {
|
|
8552
|
+
return {
|
|
8553
|
+
action: "create eslint.config.mjs",
|
|
8554
|
+
success: false,
|
|
8555
|
+
detail: err.message
|
|
8556
|
+
};
|
|
8557
|
+
}
|
|
8558
|
+
}
|
|
8559
|
+
function getRecommendation(projectType) {
|
|
8560
|
+
switch (projectType) {
|
|
8561
|
+
case "javascript":
|
|
8562
|
+
case "typescript":
|
|
8563
|
+
return {
|
|
8564
|
+
scanner: "eslint",
|
|
8565
|
+
canAutoInstall: true,
|
|
8566
|
+
installInstructions: "npm install --save-dev eslint @eslint/js"
|
|
8567
|
+
};
|
|
8568
|
+
case "python":
|
|
8569
|
+
return {
|
|
8570
|
+
scanner: "bandit",
|
|
8571
|
+
canAutoInstall: false,
|
|
8572
|
+
installInstructions: "pip install bandit (or: pipx install bandit, poetry add --group dev bandit)"
|
|
8573
|
+
};
|
|
8574
|
+
case "java":
|
|
8575
|
+
case "csharp":
|
|
8576
|
+
return {
|
|
8577
|
+
scanner: "semgrep",
|
|
8578
|
+
canAutoInstall: false,
|
|
8579
|
+
installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
|
|
8580
|
+
};
|
|
8581
|
+
case "unknown":
|
|
8582
|
+
return {
|
|
8583
|
+
scanner: "semgrep",
|
|
8584
|
+
canAutoInstall: false,
|
|
8585
|
+
installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
|
|
8586
|
+
};
|
|
8587
|
+
}
|
|
8588
|
+
}
|
|
8589
|
+
async function bootstrapScanner(workspaceRoot, sarifStore, logger2) {
|
|
8590
|
+
const detections = await detectScanners(workspaceRoot);
|
|
8591
|
+
const available = detections.filter((d) => d.available);
|
|
8592
|
+
if (available.length > 0) {
|
|
8593
|
+
const existingScanners = available.map((d) => d.scanner);
|
|
8594
|
+
logger2.info(
|
|
8595
|
+
{ existingScanners },
|
|
8596
|
+
"bootstrap: scanner(s) already configured \u2014 skipping"
|
|
8597
|
+
);
|
|
8598
|
+
return {
|
|
8599
|
+
projectType: detectProjectType(workspaceRoot),
|
|
8600
|
+
alreadyConfigured: true,
|
|
8601
|
+
existingScanners,
|
|
8602
|
+
steps: [],
|
|
8603
|
+
autoScanResult: null,
|
|
8604
|
+
success: true,
|
|
8605
|
+
summary: `Scanner(s) already configured: ${existingScanners.join(", ")}. Run auto_scan to ingest findings.`
|
|
8606
|
+
};
|
|
8607
|
+
}
|
|
8608
|
+
const projectType = detectProjectType(workspaceRoot);
|
|
8609
|
+
const recommendation = getRecommendation(projectType);
|
|
8610
|
+
const steps = [];
|
|
8611
|
+
logger2.info(
|
|
8612
|
+
{ projectType, scanner: recommendation.scanner },
|
|
8613
|
+
"bootstrap: detected project type"
|
|
8614
|
+
);
|
|
8615
|
+
if (recommendation.canAutoInstall) {
|
|
8616
|
+
const isTypeScript = projectType === "typescript";
|
|
8617
|
+
const packages = isTypeScript ? ["eslint", "@eslint/js", "typescript-eslint"] : ["eslint", "@eslint/js"];
|
|
8618
|
+
const installStep = await npmInstall(workspaceRoot, packages);
|
|
8619
|
+
steps.push(installStep);
|
|
8620
|
+
if (installStep.success) {
|
|
8621
|
+
const configStep = writeEslintConfigFile(workspaceRoot, isTypeScript);
|
|
8622
|
+
steps.push(configStep);
|
|
8623
|
+
}
|
|
8624
|
+
} else {
|
|
8625
|
+
steps.push({
|
|
8626
|
+
action: `suggest ${recommendation.scanner} install`,
|
|
8627
|
+
success: true,
|
|
8628
|
+
detail: recommendation.installInstructions
|
|
8629
|
+
});
|
|
8630
|
+
}
|
|
8631
|
+
const installSucceeded = steps.every((s) => s.success);
|
|
8632
|
+
let autoScanResult = null;
|
|
8633
|
+
if (installSucceeded && recommendation.canAutoInstall) {
|
|
8634
|
+
try {
|
|
8635
|
+
autoScanResult = await autoScan(workspaceRoot, sarifStore, logger2);
|
|
8636
|
+
} catch (err) {
|
|
8637
|
+
logger2.warn(
|
|
8638
|
+
{ err: err.message },
|
|
8639
|
+
"bootstrap: auto_scan after install failed"
|
|
8640
|
+
);
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
const findings = autoScanResult?.totalFindings ?? 0;
|
|
8644
|
+
const scannerInstalled = recommendation.canAutoInstall && installSucceeded;
|
|
8645
|
+
let summary;
|
|
8646
|
+
if (scannerInstalled && autoScanResult) {
|
|
8647
|
+
summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan found ${findings} finding(s).`;
|
|
8648
|
+
} else if (scannerInstalled) {
|
|
8649
|
+
summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan did not run.`;
|
|
8650
|
+
} else if (!recommendation.canAutoInstall) {
|
|
8651
|
+
summary = `Detected ${projectType} project. Install ${recommendation.scanner} manually: ${recommendation.installInstructions}`;
|
|
8652
|
+
} else {
|
|
8653
|
+
summary = `Failed to install ${recommendation.scanner}. Check the error details in the steps.`;
|
|
8654
|
+
}
|
|
8655
|
+
return {
|
|
8656
|
+
projectType,
|
|
8657
|
+
alreadyConfigured: false,
|
|
8658
|
+
existingScanners: [],
|
|
8659
|
+
steps,
|
|
8660
|
+
autoScanResult,
|
|
8661
|
+
success: installSucceeded,
|
|
8662
|
+
summary
|
|
8663
|
+
};
|
|
8664
|
+
}
|
|
8665
|
+
|
|
8086
8666
|
// src/schemas/tool-schemas.ts
|
|
8087
8667
|
var computeCrapSchema = {
|
|
8088
8668
|
type: "object",
|
|
@@ -8210,6 +8790,20 @@ var ingestScannerOutputSchema = {
|
|
|
8210
8790
|
required: ["scanner", "rawOutput"],
|
|
8211
8791
|
additionalProperties: false
|
|
8212
8792
|
};
|
|
8793
|
+
var bootstrapScannerSchema = {
|
|
8794
|
+
type: "object",
|
|
8795
|
+
description: "Detect the project type (JavaScript, TypeScript, Python, Java, C#), install the appropriate scanner (ESLint for JS/TS, Bandit for Python, Semgrep for Java/C#), create a minimal config file, and run auto_scan to verify. Skips installation if a scanner is already configured. Use this when auto_scan finds no scanners and quality grades are vacuously A.",
|
|
8796
|
+
properties: {},
|
|
8797
|
+
required: [],
|
|
8798
|
+
additionalProperties: false
|
|
8799
|
+
};
|
|
8800
|
+
var autoScanSchema = {
|
|
8801
|
+
type: "object",
|
|
8802
|
+
description: "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.",
|
|
8803
|
+
properties: {},
|
|
8804
|
+
required: [],
|
|
8805
|
+
additionalProperties: false
|
|
8806
|
+
};
|
|
8213
8807
|
var ingestSarifSchema = {
|
|
8214
8808
|
type: "object",
|
|
8215
8809
|
description: "Ingest a raw SARIF 2.1.0 report produced by an external scanner (Semgrep, ESLint, Bandit, Stryker, etc.), deduplicate it against the internal store, and return the normalized document. The agent should call this once per scanner invocation, not once per finding.",
|
|
@@ -8329,6 +8923,16 @@ async function main() {
|
|
|
8329
8923
|
name: "score_project",
|
|
8330
8924
|
description: "Aggregate the project score across Maintainability, Reliability, Security and Overall, returning a chat-friendly Markdown summary, the structured JSON, the local dashboard URL, and the consolidated SARIF report path.",
|
|
8331
8925
|
inputSchema: scoreProjectSchema
|
|
8926
|
+
},
|
|
8927
|
+
{
|
|
8928
|
+
name: "auto_scan",
|
|
8929
|
+
description: "Auto-detect available scanners (ESLint, Semgrep, Bandit, Stryker) in the workspace, run them, and ingest findings into the SARIF store.",
|
|
8930
|
+
inputSchema: autoScanSchema
|
|
8931
|
+
},
|
|
8932
|
+
{
|
|
8933
|
+
name: "bootstrap_scanner",
|
|
8934
|
+
description: "Detect project type, install the right scanner (ESLint for JS/TS, Bandit for Python, Semgrep for Java/C#), create minimal config, and run auto_scan to verify.",
|
|
8935
|
+
inputSchema: bootstrapScannerSchema
|
|
8332
8936
|
}
|
|
8333
8937
|
]
|
|
8334
8938
|
}));
|
|
@@ -8610,6 +9214,63 @@ async function main() {
|
|
|
8610
9214
|
};
|
|
8611
9215
|
}
|
|
8612
9216
|
}
|
|
9217
|
+
case "bootstrap_scanner": {
|
|
9218
|
+
logger.info({ tool: "bootstrap_scanner" }, "Tool call received");
|
|
9219
|
+
try {
|
|
9220
|
+
const result = await bootstrapScanner(config.pluginRoot, sarifStore, logger);
|
|
9221
|
+
const markdown = renderBootstrapMarkdown(result);
|
|
9222
|
+
return {
|
|
9223
|
+
content: [
|
|
9224
|
+
{ type: "text", text: markdown },
|
|
9225
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
9226
|
+
],
|
|
9227
|
+
isError: !result.success
|
|
9228
|
+
};
|
|
9229
|
+
} catch (err) {
|
|
9230
|
+
logger.error({ err }, "bootstrap_scanner failed");
|
|
9231
|
+
return {
|
|
9232
|
+
content: [
|
|
9233
|
+
{
|
|
9234
|
+
type: "text",
|
|
9235
|
+
text: JSON.stringify(
|
|
9236
|
+
{ tool: "bootstrap_scanner", status: "error", message: err.message },
|
|
9237
|
+
null,
|
|
9238
|
+
2
|
|
9239
|
+
)
|
|
9240
|
+
}
|
|
9241
|
+
],
|
|
9242
|
+
isError: true
|
|
9243
|
+
};
|
|
9244
|
+
}
|
|
9245
|
+
}
|
|
9246
|
+
case "auto_scan": {
|
|
9247
|
+
logger.info({ tool: "auto_scan" }, "Tool call received");
|
|
9248
|
+
try {
|
|
9249
|
+
const result = await autoScan(config.pluginRoot, sarifStore, logger);
|
|
9250
|
+
const markdown = renderAutoScanMarkdown(result);
|
|
9251
|
+
return {
|
|
9252
|
+
content: [
|
|
9253
|
+
{ type: "text", text: markdown },
|
|
9254
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
9255
|
+
]
|
|
9256
|
+
};
|
|
9257
|
+
} catch (err) {
|
|
9258
|
+
logger.error({ err }, "auto_scan failed");
|
|
9259
|
+
return {
|
|
9260
|
+
content: [
|
|
9261
|
+
{
|
|
9262
|
+
type: "text",
|
|
9263
|
+
text: JSON.stringify(
|
|
9264
|
+
{ tool: "auto_scan", status: "error", message: err.message },
|
|
9265
|
+
null,
|
|
9266
|
+
2
|
|
9267
|
+
)
|
|
9268
|
+
}
|
|
9269
|
+
],
|
|
9270
|
+
isError: true
|
|
9271
|
+
};
|
|
9272
|
+
}
|
|
9273
|
+
}
|
|
8613
9274
|
default:
|
|
8614
9275
|
throw new Error(`[claude-crap] Unknown tool: ${name}`);
|
|
8615
9276
|
}
|
|
@@ -8661,6 +9322,77 @@ async function main() {
|
|
|
8661
9322
|
const transport = new StdioServerTransport();
|
|
8662
9323
|
await server.connect(transport);
|
|
8663
9324
|
logger.info("claude-crap MCP server ready (stdio)");
|
|
9325
|
+
autoScan(config.pluginRoot, sarifStore, logger).then((result) => {
|
|
9326
|
+
const scanners = result.results.filter((r) => r.success).map((r) => r.scanner);
|
|
9327
|
+
logger.info(
|
|
9328
|
+
{
|
|
9329
|
+
scannersRun: scanners,
|
|
9330
|
+
totalFindings: result.totalFindings,
|
|
9331
|
+
durationMs: result.totalDurationMs
|
|
9332
|
+
},
|
|
9333
|
+
"auto-scan completed"
|
|
9334
|
+
);
|
|
9335
|
+
}).catch((err) => {
|
|
9336
|
+
logger.warn(
|
|
9337
|
+
{ err: err.message },
|
|
9338
|
+
"auto-scan failed \u2014 continuing without it"
|
|
9339
|
+
);
|
|
9340
|
+
});
|
|
9341
|
+
}
|
|
9342
|
+
function renderBootstrapMarkdown(result) {
|
|
9343
|
+
const lines = ["## claude-crap :: bootstrap scanner\n"];
|
|
9344
|
+
lines.push(`**Project type:** ${result.projectType}`);
|
|
9345
|
+
if (result.alreadyConfigured) {
|
|
9346
|
+
lines.push(`**Status:** Scanner(s) already configured: ${result.existingScanners.join(", ")}`);
|
|
9347
|
+
lines.push("\nNo installation needed. Run `auto_scan` to ingest findings.");
|
|
9348
|
+
return lines.join("\n");
|
|
9349
|
+
}
|
|
9350
|
+
lines.push("");
|
|
9351
|
+
if (result.steps.length > 0) {
|
|
9352
|
+
lines.push("### Steps\n");
|
|
9353
|
+
lines.push("| Action | Status | Detail |");
|
|
9354
|
+
lines.push("| ------ | :----: | ------ |");
|
|
9355
|
+
for (const s of result.steps) {
|
|
9356
|
+
const status = s.success ? "ok" : "failed";
|
|
9357
|
+
lines.push(`| ${s.action} | ${status} | ${s.detail} |`);
|
|
9358
|
+
}
|
|
9359
|
+
lines.push("");
|
|
9360
|
+
}
|
|
9361
|
+
if (result.autoScanResult) {
|
|
9362
|
+
const r = result.autoScanResult;
|
|
9363
|
+
const scanners = r.results.filter((s) => s.success).map((s) => s.scanner);
|
|
9364
|
+
lines.push(
|
|
9365
|
+
`**Auto-scan:** ${r.totalFindings} finding(s) ingested from ${scanners.join(", ") || "no scanners"} in ${(r.totalDurationMs / 1e3).toFixed(1)}s`
|
|
9366
|
+
);
|
|
9367
|
+
lines.push("");
|
|
9368
|
+
}
|
|
9369
|
+
lines.push(`**Summary:** ${result.summary}`);
|
|
9370
|
+
return lines.join("\n");
|
|
9371
|
+
}
|
|
9372
|
+
function renderAutoScanMarkdown(result) {
|
|
9373
|
+
const lines = ["## claude-crap :: auto-scan results\n"];
|
|
9374
|
+
lines.push("### Detected scanners\n");
|
|
9375
|
+
lines.push("| Scanner | Available | Reason |");
|
|
9376
|
+
lines.push("| ------- | :-------: | ------ |");
|
|
9377
|
+
for (const d of result.detected) {
|
|
9378
|
+
lines.push(`| ${d.scanner} | ${d.available ? "yes" : "no"} | ${d.reason} |`);
|
|
9379
|
+
}
|
|
9380
|
+
lines.push("");
|
|
9381
|
+
if (result.results.length > 0) {
|
|
9382
|
+
lines.push("### Execution results\n");
|
|
9383
|
+
lines.push("| Scanner | Status | Findings | Duration |");
|
|
9384
|
+
lines.push("| ------- | :----: | :------: | -------: |");
|
|
9385
|
+
for (const r of result.results) {
|
|
9386
|
+
const status = r.success ? "ok" : "failed";
|
|
9387
|
+
const duration = `${(r.durationMs / 1e3).toFixed(1)}s`;
|
|
9388
|
+
lines.push(`| ${r.scanner} | ${status} | ${r.findingsIngested} | ${duration} |`);
|
|
9389
|
+
}
|
|
9390
|
+
lines.push("");
|
|
9391
|
+
}
|
|
9392
|
+
lines.push(
|
|
9393
|
+
`**Total findings ingested:** ${result.totalFindings} in ${(result.totalDurationMs / 1e3).toFixed(1)}s`
|
|
9394
|
+
);
|
|
9395
|
+
return lines.join("\n");
|
|
8664
9396
|
}
|
|
8665
9397
|
function safeLoadStrictness(workspaceRoot, logger2) {
|
|
8666
9398
|
try {
|