@vibecheck-ai/mcp 24.6.11 → 25.0.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/dist/{ErrorHandlingEngine-VAHVDVFG-3GDGRN3U.js → ErrorHandlingEngine-FG65SFRB-4NEANCMF.js} +1 -1
- package/dist/{PhantomDepEngine-HQEXAS25-TRVEXBMF.js → PhantomDepEngine-5O7Z7MDE-4A37GGL4.js} +1 -1
- package/dist/{chunk-G3FQJC2H.js → chunk-FMRX5OVJ.js} +1 -1
- package/dist/{chunk-QFDZMUGO.js → chunk-FRK2XZX5.js} +12 -3
- package/dist/{chunk-RNFMO5GH.js → chunk-WUHPSW7M.js} +2606 -337
- package/dist/dist-Y2Z46SBD.js +22 -0
- package/dist/index.js +3075 -3047
- package/package.json +16 -14
- package/LICENSE +0 -21
- package/dist/dist-OUJKMVTB.js +0 -22
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
3
|
+
import * as path6__default from 'path';
|
|
4
|
+
import path6__default__default, { dirname } from 'path';
|
|
5
5
|
import { FrameworkPackEngine } from './chunk-MUP4JXOF.js';
|
|
6
6
|
import { LogicGapEngine } from './chunk-DDTUTWRY.js';
|
|
7
|
-
import { require_typescript, ErrorHandlingEngine } from './chunk-
|
|
7
|
+
import { require_typescript, ErrorHandlingEngine } from './chunk-FRK2XZX5.js';
|
|
8
8
|
import { BaseEngine } from './chunk-RR5ETBSV.js';
|
|
9
|
-
import { PhantomDepEngine } from './chunk-
|
|
9
|
+
import { PhantomDepEngine } from './chunk-FMRX5OVJ.js';
|
|
10
10
|
import { APITruthEngine } from './chunk-JZSHXEYP.js';
|
|
11
11
|
import { EnvVarEngine } from './chunk-QYXENOVK.js';
|
|
12
12
|
import { GhostRouteEngine } from './chunk-LQSBUKYZ.js';
|
|
@@ -14,9 +14,9 @@ import { CredentialsEngine } from './chunk-5DADZJ3D.js';
|
|
|
14
14
|
import { SecurityEngine } from './chunk-43XAAYST.js';
|
|
15
15
|
import { VersionHallucinationEngine } from './chunk-F34MHA6A.js';
|
|
16
16
|
import { __toESM } from './chunk-YWUMPN4Z.js';
|
|
17
|
+
import * as fs from 'fs/promises';
|
|
17
18
|
import * as fs2 from 'fs';
|
|
18
19
|
import fs2__default, { existsSync } from 'fs';
|
|
19
|
-
import * as fs from 'fs/promises';
|
|
20
20
|
import { setMaxListeners } from 'events';
|
|
21
21
|
import { z } from 'zod';
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ dirname(__filename$1);
|
|
|
27
27
|
// ../engines/dist/index.js
|
|
28
28
|
var import_typescript = __toESM(require_typescript(), 1);
|
|
29
29
|
|
|
30
|
-
// ../subscriptions/dist/chunk-
|
|
30
|
+
// ../subscriptions/dist/chunk-XDJH35I6.js
|
|
31
31
|
var PLAN_IDS = ["pro", "team", "enterprise"];
|
|
32
32
|
var PLAN_RANK = {
|
|
33
33
|
pro: 0,
|
|
@@ -1442,7 +1442,7 @@ Object.fromEntries(
|
|
|
1442
1442
|
|
|
1443
1443
|
// ../shared-types/dist/entitlements.js
|
|
1444
1444
|
var ENTITLEMENTS2 = {
|
|
1445
|
-
// === PRO
|
|
1445
|
+
// === PRO Tier Entitlements ===
|
|
1446
1446
|
REALITY_MODE: "reality_mode",
|
|
1447
1447
|
AUTOFIX: "autofix",
|
|
1448
1448
|
AUTOFIX_APPLY: "autofix_apply",
|
|
@@ -1454,7 +1454,7 @@ var ENTITLEMENTS2 = {
|
|
|
1454
1454
|
CLOUD_SYNC: "cloud_sync",
|
|
1455
1455
|
PRIORITY_SUPPORT: "priority_support",
|
|
1456
1456
|
SHAREABLE_REPORTS: "shareable_reports",
|
|
1457
|
-
// === TEAM
|
|
1457
|
+
// === TEAM Tier Entitlements ===
|
|
1458
1458
|
CONTEXT_ENGINE: "context_engine",
|
|
1459
1459
|
FIREWALL_AGENT: "firewall_agent",
|
|
1460
1460
|
FIREWALL_ENFORCE: "firewall_enforce",
|
|
@@ -1561,6 +1561,46 @@ var TEAM_ENTITLEMENTS = new Set(PRO_ENTITLEMENTS);
|
|
|
1561
1561
|
ENTITLEMENTS2.ENTERPRISE_SIGNED_BUNDLES,
|
|
1562
1562
|
ENTITLEMENTS2.ENTERPRISE_MULTI_REPO
|
|
1563
1563
|
]);
|
|
1564
|
+
function quotasToPlanLimits(q) {
|
|
1565
|
+
return {
|
|
1566
|
+
findingDetailLimit: q.findingDetailLimit,
|
|
1567
|
+
canAutoFix: q.canAutoFix,
|
|
1568
|
+
canHealPR: q.canHealPR,
|
|
1569
|
+
canModelFingerprint: q.canModelFingerprint,
|
|
1570
|
+
canRealityMode: q.canRealityMode,
|
|
1571
|
+
canISLGenerate: q.canISLGenerate,
|
|
1572
|
+
canCIBlock: q.canCIBlock,
|
|
1573
|
+
scanHistoryDays: q.scanHistoryDays,
|
|
1574
|
+
maxProjects: q.maxProjects,
|
|
1575
|
+
apiRequestsPerDay: q.apiRequestsPerDay,
|
|
1576
|
+
canExportCSV: q.canExportCSV,
|
|
1577
|
+
canSlackIntegration: q.canSlackIntegration,
|
|
1578
|
+
scansPerDay: q.scansPerDay
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
({
|
|
1582
|
+
free: quotasToPlanLimits(getQuotas(null)),
|
|
1583
|
+
pro: quotasToPlanLimits(PLAN_QUOTAS.pro),
|
|
1584
|
+
team: quotasToPlanLimits(PLAN_QUOTAS.team),
|
|
1585
|
+
enterprise: quotasToPlanLimits(PLAN_QUOTAS.enterprise)
|
|
1586
|
+
});
|
|
1587
|
+
({
|
|
1588
|
+
pro: {
|
|
1589
|
+
name: PLANS.pro.displayName,
|
|
1590
|
+
tagline: PLANS.pro.tagline,
|
|
1591
|
+
price: PLANS.pro.monthlyPriceUsd,
|
|
1592
|
+
priceLabel: PLANS.pro.priceLabel},
|
|
1593
|
+
team: {
|
|
1594
|
+
name: PLANS.team.displayName,
|
|
1595
|
+
tagline: PLANS.team.tagline,
|
|
1596
|
+
price: PLANS.team.monthlyPriceUsd,
|
|
1597
|
+
priceLabel: PLANS.team.priceLabel},
|
|
1598
|
+
enterprise: {
|
|
1599
|
+
name: PLANS.enterprise.displayName,
|
|
1600
|
+
tagline: PLANS.enterprise.tagline,
|
|
1601
|
+
price: PLANS.enterprise.monthlyPriceUsd,
|
|
1602
|
+
priceLabel: PLANS.enterprise.priceLabel}
|
|
1603
|
+
});
|
|
1564
1604
|
|
|
1565
1605
|
// ../shared-types/dist/risk-dimensions.js
|
|
1566
1606
|
var RISK_DIMENSIONS = [
|
|
@@ -1994,6 +2034,26 @@ function gateCanonicalScanReportFindings(report, planInput) {
|
|
|
1994
2034
|
};
|
|
1995
2035
|
}
|
|
1996
2036
|
|
|
2037
|
+
// ../shared-types/dist/billing.js
|
|
2038
|
+
function quotasToTierLimits(q) {
|
|
2039
|
+
return {
|
|
2040
|
+
scansPerMonth: q.scansPerMonth === Infinity ? -1 : q.scansPerMonth,
|
|
2041
|
+
projects: q.maxProjects === Infinity ? -1 : q.maxProjects,
|
|
2042
|
+
seats: q.maxSeats === Infinity ? -1 : q.maxSeats,
|
|
2043
|
+
apiCallsPerMinute: Math.ceil(q.apiRequestsPerDay / 1440),
|
|
2044
|
+
retentionDays: q.scanHistoryDays
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
({
|
|
2048
|
+
free: quotasToTierLimits(getQuotas(null)),
|
|
2049
|
+
pro: quotasToTierLimits(PLAN_QUOTAS.pro),
|
|
2050
|
+
team: quotasToTierLimits(PLAN_QUOTAS.team),
|
|
2051
|
+
enterprise: quotasToTierLimits(PLAN_QUOTAS.enterprise)
|
|
2052
|
+
});
|
|
2053
|
+
({
|
|
2054
|
+
...PLAN_PRICE_LABELS
|
|
2055
|
+
});
|
|
2056
|
+
|
|
1997
2057
|
// ../shared-types/dist/api-client.js
|
|
1998
2058
|
var PROOF_RUN_SOURCES = ["cli", "mcp", "extension", "github", "dashboard"];
|
|
1999
2059
|
new Set(PROOF_RUN_SOURCES);
|
|
@@ -2197,9 +2257,11 @@ var TypeContractEngine = class extends BaseEngine {
|
|
|
2197
2257
|
async scan(delta, signal) {
|
|
2198
2258
|
const findings = [];
|
|
2199
2259
|
const source = delta.fullText;
|
|
2200
|
-
const uri = delta.documentUri.replace(/^file:\/\//, "");
|
|
2260
|
+
const uri = delta.documentUri.replace(/^file:\/\//, "").replace(/\\/g, "/");
|
|
2201
2261
|
const lines = delta.lines ?? source.split("\n");
|
|
2202
|
-
if (/\.(test|spec)\.(ts|tsx)$/i.test(uri) || /__tests__|__mocks__|fixtures?/i.test(uri))
|
|
2262
|
+
if (/\.(test|spec)\.(ts|tsx)$/i.test(uri) || /\.bench\.(ts|tsx)$/i.test(uri) || /__tests__|__mocks__|fixtures?/i.test(uri) || /(?:^|[\\/])(?:bench|benchmarks?)(?:[\\/]|$)/i.test(uri)) {
|
|
2263
|
+
return findings;
|
|
2264
|
+
}
|
|
2203
2265
|
this.checkAbort(signal);
|
|
2204
2266
|
for (let i = 0; i < lines.length; i++) {
|
|
2205
2267
|
const line = lines[i];
|
|
@@ -3152,136 +3214,1202 @@ var PerformanceAntipatternEngine = class extends BaseEngine {
|
|
|
3152
3214
|
return findings;
|
|
3153
3215
|
}
|
|
3154
3216
|
};
|
|
3155
|
-
var
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
constructor(_threshold = 5, _cooldownMs = 3e4) {
|
|
3163
|
-
this._threshold = _threshold;
|
|
3164
|
-
this._cooldownMs = _cooldownMs;
|
|
3217
|
+
var TRUTHPACK_DIR = ".vibecheck/truthpack";
|
|
3218
|
+
async function loadTruthpack(workspaceRoot) {
|
|
3219
|
+
const dir = path6__default.join(workspaceRoot, TRUTHPACK_DIR);
|
|
3220
|
+
try {
|
|
3221
|
+
await fs.access(dir);
|
|
3222
|
+
} catch {
|
|
3223
|
+
return null;
|
|
3165
3224
|
}
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3225
|
+
try {
|
|
3226
|
+
const [routesData, envData, integrationsData] = await Promise.all([
|
|
3227
|
+
fs.readFile(path6__default.join(dir, "routes.json"), "utf-8").catch(() => '{"routes":[]}'),
|
|
3228
|
+
fs.readFile(path6__default.join(dir, "env.json"), "utf-8").catch(() => '{"variables":[]}'),
|
|
3229
|
+
fs.readFile(path6__default.join(dir, "integrations.json"), "utf-8").catch(() => '{"integrations":[]}')
|
|
3230
|
+
]);
|
|
3231
|
+
const routesJson = JSON.parse(routesData);
|
|
3232
|
+
const envJson = JSON.parse(envData);
|
|
3233
|
+
const integrationsJson = JSON.parse(integrationsData);
|
|
3234
|
+
return {
|
|
3235
|
+
routes: routesJson.routes ?? [],
|
|
3236
|
+
env: envJson.variables ?? [],
|
|
3237
|
+
integrations: integrationsJson.integrations ?? []
|
|
3238
|
+
};
|
|
3239
|
+
} catch {
|
|
3240
|
+
return null;
|
|
3171
3241
|
}
|
|
3172
|
-
|
|
3173
|
-
|
|
3242
|
+
}
|
|
3243
|
+
function extractKnownHostsFromIntegrations(integrations) {
|
|
3244
|
+
const out = /* @__PURE__ */ new Set([
|
|
3245
|
+
"localhost",
|
|
3246
|
+
"127.0.0.1",
|
|
3247
|
+
"0.0.0.0",
|
|
3248
|
+
"::1"
|
|
3249
|
+
]);
|
|
3250
|
+
const sdkApex = {
|
|
3251
|
+
stripe: "stripe.com",
|
|
3252
|
+
"@octokit/rest": "github.com",
|
|
3253
|
+
"posthog-node": "posthog.com",
|
|
3254
|
+
"posthog-js": "posthog.com",
|
|
3255
|
+
"@sentry/node": "sentry.io",
|
|
3256
|
+
resend: "resend.com",
|
|
3257
|
+
openai: "openai.com",
|
|
3258
|
+
"@anthropic-ai/sdk": "anthropic.com",
|
|
3259
|
+
ioredis: "redis.io",
|
|
3260
|
+
"drizzle-orm": "drizzle.team"
|
|
3261
|
+
};
|
|
3262
|
+
for (const integ of integrations) {
|
|
3263
|
+
if (integ.docsUrl) {
|
|
3264
|
+
const apex = apexFromUrl(integ.docsUrl);
|
|
3265
|
+
if (apex) out.add(apex);
|
|
3266
|
+
}
|
|
3267
|
+
if (integ.sdkPackage && sdkApex[integ.sdkPackage]) {
|
|
3268
|
+
out.add(sdkApex[integ.sdkPackage]);
|
|
3269
|
+
}
|
|
3174
3270
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3271
|
+
return out;
|
|
3272
|
+
}
|
|
3273
|
+
function apexFromUrl(url) {
|
|
3274
|
+
try {
|
|
3275
|
+
const u = new URL(url);
|
|
3276
|
+
return apexFromHost(u.hostname);
|
|
3277
|
+
} catch {
|
|
3278
|
+
return null;
|
|
3178
3279
|
}
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
}
|
|
3280
|
+
}
|
|
3281
|
+
function apexFromHost(hostname) {
|
|
3282
|
+
const host = hostname.toLowerCase();
|
|
3283
|
+
if (/^[\d.]+$/.test(host) || host.includes(":") || host === "localhost") {
|
|
3284
|
+
return host;
|
|
3185
3285
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3286
|
+
const parts = host.split(".");
|
|
3287
|
+
if (parts.length <= 2) return host;
|
|
3288
|
+
return parts.slice(-2).join(".");
|
|
3289
|
+
}
|
|
3290
|
+
function hostMatchesKnownSet(host, knownHosts) {
|
|
3291
|
+
const lower = host.toLowerCase();
|
|
3292
|
+
if (knownHosts.has(lower)) return true;
|
|
3293
|
+
const apex = apexFromHost(lower);
|
|
3294
|
+
if (knownHosts.has(apex)) return true;
|
|
3295
|
+
for (const known of knownHosts) {
|
|
3296
|
+
if (lower.endsWith(`.${known}`)) return true;
|
|
3297
|
+
}
|
|
3298
|
+
return false;
|
|
3299
|
+
}
|
|
3300
|
+
var TruthpackEnvIndex = class {
|
|
3301
|
+
_index;
|
|
3302
|
+
constructor(envVars, allowlistEnvVars) {
|
|
3303
|
+
this._index = new Set(envVars.map((v) => v.name));
|
|
3304
|
+
if (Array.isArray(allowlistEnvVars)) {
|
|
3305
|
+
for (const v of allowlistEnvVars) {
|
|
3306
|
+
if (typeof v === "string" && /^[A-Z_][A-Z0-9_]*$/.test(v)) this._index.add(v);
|
|
3307
|
+
}
|
|
3191
3308
|
}
|
|
3192
|
-
return false;
|
|
3193
3309
|
}
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
this._halfOpenTimer = setTimeout(() => {
|
|
3197
|
-
this._state = 2;
|
|
3198
|
-
this._halfOpenTimer = null;
|
|
3199
|
-
}, this._cooldownMs);
|
|
3310
|
+
get index() {
|
|
3311
|
+
return this._index;
|
|
3200
3312
|
}
|
|
3201
|
-
|
|
3202
|
-
|
|
3313
|
+
has(name) {
|
|
3314
|
+
return this._index.has(name);
|
|
3203
3315
|
}
|
|
3204
3316
|
};
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3317
|
+
function compileRoutePattern(routePath) {
|
|
3318
|
+
let isDynamic = false;
|
|
3319
|
+
let isCatchAll = false;
|
|
3320
|
+
let pattern = routePath.replace(/\[\[\.\.\.(\w+)\]\]/g, () => {
|
|
3321
|
+
isDynamic = true;
|
|
3322
|
+
isCatchAll = true;
|
|
3323
|
+
return "(?:\\/.*)?";
|
|
3324
|
+
}).replace(/\[\.\.\.(\w+)\]/g, () => {
|
|
3325
|
+
isDynamic = true;
|
|
3326
|
+
isCatchAll = true;
|
|
3327
|
+
return "\\/.*";
|
|
3328
|
+
}).replace(/\[(\w+)\]/g, () => {
|
|
3329
|
+
isDynamic = true;
|
|
3330
|
+
return "\\/[^/]+";
|
|
3331
|
+
}).replace(/:\w+/g, () => {
|
|
3332
|
+
isDynamic = true;
|
|
3333
|
+
return "\\/[^/]+";
|
|
3334
|
+
});
|
|
3335
|
+
pattern = pattern.replace(/\//g, "\\/").replace(/\\\\\//g, "\\/");
|
|
3336
|
+
return {
|
|
3337
|
+
regex: new RegExp(`^${pattern}$`),
|
|
3338
|
+
isDynamic,
|
|
3339
|
+
isCatchAll
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
function truthpackToRouteIndex(routes, workspaceRoot) {
|
|
3343
|
+
const entries = [];
|
|
3344
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3345
|
+
for (const r of routes) {
|
|
3346
|
+
const pathStr = r.path ?? "";
|
|
3347
|
+
if (!pathStr.startsWith("/")) continue;
|
|
3348
|
+
const pattern = pathStr.startsWith("/api") ? pathStr : pathStr;
|
|
3349
|
+
const key = `${pattern}:${r.method ?? "GET"}`;
|
|
3350
|
+
if (seen.has(key)) continue;
|
|
3351
|
+
seen.add(key);
|
|
3352
|
+
const compiled = compileRoutePattern(pattern);
|
|
3353
|
+
entries.push({
|
|
3354
|
+
pattern,
|
|
3355
|
+
regex: compiled.regex,
|
|
3356
|
+
isDynamic: compiled.isDynamic,
|
|
3357
|
+
isCatchAll: compiled.isCatchAll,
|
|
3358
|
+
filePath: r.file ? path6__default.resolve(workspaceRoot, r.file) : ""
|
|
3359
|
+
});
|
|
3241
3360
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3361
|
+
return {
|
|
3362
|
+
routes: entries,
|
|
3363
|
+
framework: "truthpack",
|
|
3364
|
+
builtAt: Date.now()
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
function isAIRulesFile(uri) {
|
|
3368
|
+
const path24 = uri.replace(/^file:\/\//, "").toLowerCase();
|
|
3369
|
+
const base = path24.split("/").pop() ?? "";
|
|
3370
|
+
if (base === ".cursorrules") return true;
|
|
3371
|
+
if (/\.cursor\/rules\/.+\.(md|mdc)$/.test(path24)) return true;
|
|
3372
|
+
if (base === "claude.md" || base === ".claude.md") return true;
|
|
3373
|
+
if (/\.claude\/(?:claude\.md|agents\/|commands\/|skills\/|memory\/)/i.test(path24)) return true;
|
|
3374
|
+
if (base === "agents.md") return true;
|
|
3375
|
+
if (base === "gemini.md") return true;
|
|
3376
|
+
if (/\.gemini\/(?!mcp\.json).+\.(md|toml)$/i.test(path24)) return true;
|
|
3377
|
+
if (base === ".windsurfrules" || base === ".codeiumrc") return true;
|
|
3378
|
+
if (/\.github\/copilot-instructions\.md$/.test(path24)) return true;
|
|
3379
|
+
if (base === ".aider.conf.yml" || base === "conventions.md") return true;
|
|
3380
|
+
if (/\.continue\/config\.(json|yaml|yml)$/.test(path24)) return true;
|
|
3381
|
+
if (base === ".clinerules" || base === ".roorules") return true;
|
|
3382
|
+
return false;
|
|
3383
|
+
}
|
|
3384
|
+
function isMCPConfigFile(uri) {
|
|
3385
|
+
const path24 = uri.replace(/^file:\/\//, "").toLowerCase();
|
|
3386
|
+
const base = path24.split("/").pop() ?? "";
|
|
3387
|
+
if (base === "mcp.json" || base === ".mcp.json") return true;
|
|
3388
|
+
if (base === "claude_desktop_config.json") return true;
|
|
3389
|
+
if (base === "mcp_servers.json") return true;
|
|
3390
|
+
if (/\/\.cursor\/mcp\.json$/.test(path24)) return true;
|
|
3391
|
+
if (/\/\.vscode\/mcp\.json$/.test(path24)) return true;
|
|
3392
|
+
if (/\/\.gemini\/(?:settings|mcp)\.json$/.test(path24)) return true;
|
|
3393
|
+
if (/\/\.mcp\/config\.json$/.test(path24)) return true;
|
|
3394
|
+
return false;
|
|
3395
|
+
}
|
|
3396
|
+
var INVISIBLE_UNICODE_RE = /[\u200B\u200C\u200D\u2060\uFEFF\u00AD\u200E\u200F]/u;
|
|
3397
|
+
var BIDI_OVERRIDE_RE = /[\u202A-\u202E\u2066-\u2069]/u;
|
|
3398
|
+
var TAG_CHARS_RE = /[\u{E0000}-\u{E007F}]/u;
|
|
3399
|
+
var VARIATION_SELECTORS_RE = /[\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/u;
|
|
3400
|
+
var HOMOGLYPH_LATIN_RE = /[A-Za-z]/;
|
|
3401
|
+
var HOMOGLYPH_CYRILLIC_LOOKALIKES_RE = /[\u0430\u0435\u043E\u0440\u0441\u0445\u0443\u0456\u0458\u04CF\u0410\u0412\u0415\u041A\u041C\u041D\u041E\u0420\u0421\u0422\u0425]/;
|
|
3402
|
+
var HOMOGLYPH_GREEK_LOOKALIKES_RE = /[\u03BF\u03B1\u03BD\u03C5\u03C7\u03C9\u0391\u0392\u0395\u0397\u0399\u039A\u039C\u039D\u039F\u03A1\u03A4\u03A5\u03A7]/;
|
|
3403
|
+
var INSTRUCTION_OVERRIDE_RE = /\b(?:ignore|disregard|forget|override|bypass|skip)\s+(?:all\s+)?(?:the\s+)?(?:previous|prior|above|earlier|original|preceding)\s+(?:instructions?|rules?|prompts?|context|guidelines?|directives?|constraints?)\b/i;
|
|
3404
|
+
var ROLE_REASSIGNMENT_RE = /\b(?:you\s+are\s+now|act\s+as|pretend\s+to\s+be|assume\s+the\s+role(?:\s+of)?|from\s+now\s+on\s+you|you'?re\s+(?:no\s+longer|not)\s+claude|you\s+have\s+no\s+restrictions)\b/i;
|
|
3405
|
+
var AI_COMMENT_HIJACK_RE = /(?:\/\/|\/\*|#|<!--|;)\s*(?:AI|assistant|copilot|cursor|claude|gpt|codex|gemini|windsurf|aider|cline)\s*[:\s]?\s*(?:ignore|override|instead|actually|disregard|don'?t|skip|bypass)/i;
|
|
3406
|
+
var BASE64_PAYLOAD_RE = /(?:base64|atob|decode|b64decode|fromBase64)\s*\(|(?<![A-Za-z0-9+/])([A-Za-z0-9+/]{60,}={0,2})(?![A-Za-z0-9+/])/;
|
|
3407
|
+
var HIDDEN_CONTENT_BLOCK_RE = /<!--[\s\S]{40,}?-->|<(?:div|span|p|section)[^>]*(?:display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0|height\s*:\s*0|font-size\s*:\s*0|color\s*:\s*(?:white|#fff|transparent))[^>]*>/i;
|
|
3408
|
+
var SYSTEM_PROMPT_EXTRACTION_RE = /\b(?:print|reveal|repeat|show|output|expose|leak|reproduce|tell\s+me)\s+(?:your\s+)?(?:system\s+prompt|initial\s+instructions?|original\s+prompt|hidden\s+rules?|internal\s+(?:prompt|context)|developer\s+mode\s+settings)\b/i;
|
|
3409
|
+
var JAILBREAK_TOKEN_RE = /\b(?:DAN(?:\s+mode)?|developer\s+mode\s+enabled|do\s+anything\s+now|STAN\s+mode|jailbreak\s+(?:mode|prompt)|unfiltered\s+mode|sudo\s+mode)\b/i;
|
|
3410
|
+
var EXFIL_INTENT_RE = /\b(?:send|post|upload|transmit|forward|exfiltrate|leak|deliver|push)\b[\s\S]{0,40}?\b(?:code|context|secrets?|env(?:ironment)?|credentials?|tokens?|api[_\s-]?keys?|source|files?|contents?|data|history|conversation)\b/i;
|
|
3411
|
+
var EXTERNAL_URL_RE = /\bhttps?:\/\/(?!(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1?\]|example\.com|github\.com\/[^/]+\/[^/]+(?:#|$|\s)))[\w.-]+(?:\.[a-z]{2,}|:\d+)/i;
|
|
3412
|
+
var WEBHOOK_VERB_RE = /\b(?:webhook|callback|fetch|request|curl|wget|http\.(?:get|post)|axios|XMLHttpRequest|navigator\.sendBeacon)\b/i;
|
|
3413
|
+
var TOOL_POISONING_RE = /\b(?:ALWAYS|MUST|REQUIRED|CRITICAL|IMPORTANT)\s+(?:use|call|invoke|run|execute|prefer)\s+this\s+tool|\b(?:before|prior\s+to)\s+(?:any|every|all)\s+(?:other|response|reply)|ignore\s+(?:previous|other)\s+tools?|do\s+not\s+(?:tell|inform)\s+the\s+user/i;
|
|
3414
|
+
var NPX_AUTO_YES_RE = /\b(?:npx|bunx|pnpx|pnpm\s+dlx|deno\s+run\s+--allow[^\s]*)\s+(?:-y|--yes|-Y)\b/i;
|
|
3415
|
+
var HTTP_NOT_HTTPS_RE = /["'`]http:\/\/(?!localhost|127\.0\.0\.1|\[::1?\]|0\.0\.0\.0)/i;
|
|
3416
|
+
var UNVERIFIED_GIT_URL_RE = /^git\+(?:https?|ssh):\/\/(?!.*#[a-f0-9]{7,40})/i;
|
|
3417
|
+
var DENO_BROAD_PERMS_RE = /--allow-all|--allow-net(?!=)|--allow-read(?!=)|--allow-write(?!=)|--allow-run(?!=)|--allow-env(?!=)|-A\b/;
|
|
3418
|
+
var PLAINTEXT_SECRET_VALUE_RE = /(?:sk-(?:live|test|proj|ant|or)[_-]|ghp_|gho_|ghu_|ghs_|github_pat_|xox[abprs]-|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35})/;
|
|
3419
|
+
function parseMCPDocument(content) {
|
|
3420
|
+
try {
|
|
3421
|
+
return JSON.parse(content);
|
|
3422
|
+
} catch {
|
|
3423
|
+
return null;
|
|
3247
3424
|
}
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3425
|
+
}
|
|
3426
|
+
function listMCPServers(doc) {
|
|
3427
|
+
const out = [];
|
|
3428
|
+
const collect = (root) => {
|
|
3429
|
+
const block = doc[root];
|
|
3430
|
+
if (!block || typeof block !== "object") return;
|
|
3431
|
+
for (const [name, entry] of Object.entries(block)) {
|
|
3432
|
+
if (entry && typeof entry === "object") {
|
|
3433
|
+
out.push({ name, entry, pointer: `${root}.${name}` });
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
};
|
|
3437
|
+
collect("mcpServers");
|
|
3438
|
+
collect("servers");
|
|
3439
|
+
return out;
|
|
3440
|
+
}
|
|
3441
|
+
function findLineNumber(source, needle) {
|
|
3442
|
+
const idx = source.indexOf(needle);
|
|
3443
|
+
if (idx === -1) return 1;
|
|
3444
|
+
let line = 1;
|
|
3445
|
+
for (let i = 0; i < idx; i++) {
|
|
3446
|
+
if (source.charCodeAt(i) === 10) line++;
|
|
3253
3447
|
}
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
return
|
|
3448
|
+
return line;
|
|
3449
|
+
}
|
|
3450
|
+
function arrayContainsWildcard(value) {
|
|
3451
|
+
if (!Array.isArray(value)) return false;
|
|
3452
|
+
return value.some((v) => typeof v === "string" && (v === "*" || v.trim() === "*"));
|
|
3453
|
+
}
|
|
3454
|
+
function objectHasWildcardKey(value) {
|
|
3455
|
+
if (!value || typeof value !== "object") return false;
|
|
3456
|
+
for (const [k, v] of Object.entries(value)) {
|
|
3457
|
+
if (k === "*") return true;
|
|
3458
|
+
if (typeof v === "string" && (v === "*" || v === "all")) return true;
|
|
3459
|
+
if (Array.isArray(v) && arrayContainsWildcard(v)) return true;
|
|
3264
3460
|
}
|
|
3461
|
+
return false;
|
|
3462
|
+
}
|
|
3463
|
+
function isAuthDisabled(entry) {
|
|
3464
|
+
const checkVal = (v) => {
|
|
3465
|
+
if (v === false) return true;
|
|
3466
|
+
if (typeof v === "string") {
|
|
3467
|
+
const s = v.toLowerCase();
|
|
3468
|
+
return s === "none" || s === "disabled" || s === "off" || s === "false";
|
|
3469
|
+
}
|
|
3470
|
+
if (v && typeof v === "object") {
|
|
3471
|
+
const obj = v;
|
|
3472
|
+
if (obj.required === false) return true;
|
|
3473
|
+
if (obj.enabled === false) return true;
|
|
3474
|
+
if (typeof obj.type === "string" && obj.type.toLowerCase() === "none") return true;
|
|
3475
|
+
}
|
|
3476
|
+
return false;
|
|
3477
|
+
};
|
|
3478
|
+
return checkVal(entry.auth) || checkVal(entry.authentication);
|
|
3479
|
+
}
|
|
3480
|
+
var SEVERITY_LADDER = ["info", "low", "medium", "high", "critical"];
|
|
3481
|
+
var AIRulesAttackEngine = class extends BaseEngine {
|
|
3482
|
+
id = "ai-rules-attack";
|
|
3483
|
+
name = "AI Rules File & MCP Attack Engine";
|
|
3484
|
+
version = "1.0.0";
|
|
3265
3485
|
/**
|
|
3266
|
-
*
|
|
3486
|
+
* `null` means "any extension". Rules files (`.cursorrules`, `.clinerules`,
|
|
3487
|
+
* `.windsurfrules`) have no extension, so we cannot use a Set here. The
|
|
3488
|
+
* scan() function does its own path-based gating in <1µs.
|
|
3267
3489
|
*/
|
|
3268
|
-
|
|
3269
|
-
|
|
3490
|
+
supportedExtensions = null;
|
|
3491
|
+
_knownHosts;
|
|
3492
|
+
_hasTruthpack;
|
|
3493
|
+
constructor(opts = {}) {
|
|
3494
|
+
super();
|
|
3495
|
+
this._knownHosts = opts.knownHosts ?? /* @__PURE__ */ new Set();
|
|
3496
|
+
this._hasTruthpack = (opts.knownHosts?.size ?? 0) > 0;
|
|
3270
3497
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3498
|
+
async scan(delta, signal) {
|
|
3499
|
+
const uri = delta.documentUri;
|
|
3500
|
+
const isRules = isAIRulesFile(uri);
|
|
3501
|
+
const isMCP = isMCPConfigFile(uri);
|
|
3502
|
+
if (!isRules && !isMCP) return [];
|
|
3503
|
+
this.checkAbort(signal);
|
|
3504
|
+
const source = delta.fullText;
|
|
3505
|
+
const cleanUri = uri.replace(/^file:\/\//, "");
|
|
3506
|
+
const pending = [];
|
|
3507
|
+
if (isRules) {
|
|
3508
|
+
this._checkRulesFile(source, pending);
|
|
3509
|
+
}
|
|
3510
|
+
if (isMCP) {
|
|
3511
|
+
this._checkMCPConfig(source, pending);
|
|
3512
|
+
this._checkRulesFile(source, pending);
|
|
3513
|
+
}
|
|
3514
|
+
if (pending.length === 0) return [];
|
|
3515
|
+
this._applyCoOccurrenceBoost(pending);
|
|
3516
|
+
return pending.map(
|
|
3517
|
+
(p) => this.createFinding({
|
|
3518
|
+
id: this.deterministicId(cleanUri, p.line, p.column, p.ruleId, p.evidence.slice(0, 32)),
|
|
3519
|
+
ruleId: p.ruleId,
|
|
3520
|
+
category: "ai_rules_attack",
|
|
3521
|
+
message: p.message,
|
|
3522
|
+
severity: p.severity,
|
|
3523
|
+
confidence: p.confidence,
|
|
3524
|
+
file: cleanUri,
|
|
3525
|
+
line: p.line,
|
|
3526
|
+
column: p.column,
|
|
3527
|
+
evidence: p.evidence,
|
|
3528
|
+
suggestion: p.suggestion,
|
|
3529
|
+
autoFixable: p.autoFixable
|
|
3530
|
+
})
|
|
3531
|
+
);
|
|
3280
3532
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3533
|
+
// ─── Rules-file checks ──────────────────────────────────────────────────
|
|
3534
|
+
_checkRulesFile(source, out) {
|
|
3535
|
+
const lines = source.split("\n");
|
|
3536
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3537
|
+
const raw = lines[i];
|
|
3538
|
+
const line = i + 1;
|
|
3539
|
+
if (INVISIBLE_UNICODE_RE.test(raw)) {
|
|
3540
|
+
out.push({
|
|
3541
|
+
ruleId: "AIRA001",
|
|
3542
|
+
category: "unicode",
|
|
3543
|
+
severity: "critical",
|
|
3544
|
+
confidence: 0.95,
|
|
3545
|
+
message: "Invisible Unicode character in rules file \u2014 may conceal hidden instructions [CWE-1007]",
|
|
3546
|
+
evidence: this._safeEvidence(raw),
|
|
3547
|
+
suggestion: "Strip zero-width and bidi-related characters from this file. Use a hex-aware editor to verify.",
|
|
3548
|
+
line,
|
|
3549
|
+
column: raw.search(INVISIBLE_UNICODE_RE),
|
|
3550
|
+
autoFixable: true
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3553
|
+
if (BIDI_OVERRIDE_RE.test(raw)) {
|
|
3554
|
+
out.push({
|
|
3555
|
+
ruleId: "AIRA002",
|
|
3556
|
+
category: "unicode",
|
|
3557
|
+
severity: "critical",
|
|
3558
|
+
confidence: 0.98,
|
|
3559
|
+
message: "Bidirectional text override in rules file (Trojan Source variant) [CVE-2021-42574]",
|
|
3560
|
+
evidence: this._safeEvidence(raw),
|
|
3561
|
+
suggestion: "Remove all bidi control characters (LRO/RLO/LRE/RLE/PDF/LRI/RLI/FSI/PDI). These flip the visual order of text and are almost never legitimate in agent rules.",
|
|
3562
|
+
line,
|
|
3563
|
+
column: raw.search(BIDI_OVERRIDE_RE),
|
|
3564
|
+
autoFixable: true
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
if (TAG_CHARS_RE.test(raw)) {
|
|
3568
|
+
out.push({
|
|
3569
|
+
ruleId: "AIRA003",
|
|
3570
|
+
category: "unicode",
|
|
3571
|
+
severity: "critical",
|
|
3572
|
+
confidence: 0.99,
|
|
3573
|
+
message: "Unicode tag characters detected \u2014 invisible ASCII payload encoding (Goodside-class attack)",
|
|
3574
|
+
evidence: this._safeEvidence(raw),
|
|
3575
|
+
suggestion: "Tag characters (U+E0000\u2013U+E007F) encode hidden ASCII text invisibly. Strip them entirely.",
|
|
3576
|
+
line,
|
|
3577
|
+
column: 0,
|
|
3578
|
+
autoFixable: true
|
|
3579
|
+
});
|
|
3580
|
+
}
|
|
3581
|
+
if (VARIATION_SELECTORS_RE.test(raw) && raw.length > 10) {
|
|
3582
|
+
out.push({
|
|
3583
|
+
ruleId: "AIRA004",
|
|
3584
|
+
category: "unicode",
|
|
3585
|
+
severity: "high",
|
|
3586
|
+
confidence: 0.7,
|
|
3587
|
+
message: "Variation selectors in rules file \u2014 may mask invisible payload",
|
|
3588
|
+
evidence: this._safeEvidence(raw),
|
|
3589
|
+
suggestion: "Variation selectors are rarely needed in agent instructions. Verify each one is legitimate (e.g., emoji styling) and strip the rest.",
|
|
3590
|
+
line,
|
|
3591
|
+
column: 0,
|
|
3592
|
+
autoFixable: false
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
if (this._hasHomoglyphMix(raw)) {
|
|
3596
|
+
out.push({
|
|
3597
|
+
ruleId: "AIRA005",
|
|
3598
|
+
category: "unicode",
|
|
3599
|
+
severity: "high",
|
|
3600
|
+
confidence: 0.85,
|
|
3601
|
+
message: "Homoglyph mixing \u2014 Cyrillic or Greek letters used inside Latin words to spoof tokens",
|
|
3602
|
+
evidence: this._safeEvidence(raw),
|
|
3603
|
+
suggestion: "Words mixing Latin with Cyrillic/Greek lookalikes can spoof package names, auth headers, or commands. Replace non-Latin lookalikes with their ASCII equivalents.",
|
|
3604
|
+
line,
|
|
3605
|
+
column: 0,
|
|
3606
|
+
autoFixable: false
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
if (INSTRUCTION_OVERRIDE_RE.test(raw)) {
|
|
3610
|
+
out.push({
|
|
3611
|
+
ruleId: "AIRA006",
|
|
3612
|
+
category: "prompt_injection",
|
|
3613
|
+
severity: "critical",
|
|
3614
|
+
confidence: 0.9,
|
|
3615
|
+
message: "Instruction-override pattern in rules file [CVE-2025-54135, OWASP LLM01]",
|
|
3616
|
+
evidence: raw.trim().slice(0, 80),
|
|
3617
|
+
suggestion: "Remove this instruction. Rules files should describe the project \u2014 they should never tell the AI to ignore prior context.",
|
|
3618
|
+
line,
|
|
3619
|
+
column: raw.search(INSTRUCTION_OVERRIDE_RE),
|
|
3620
|
+
autoFixable: false
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
if (ROLE_REASSIGNMENT_RE.test(raw)) {
|
|
3624
|
+
out.push({
|
|
3625
|
+
ruleId: "AIRA007",
|
|
3626
|
+
category: "prompt_injection",
|
|
3627
|
+
severity: "critical",
|
|
3628
|
+
confidence: 0.85,
|
|
3629
|
+
message: "Role-reassignment pattern in rules file [OWASP LLM01]",
|
|
3630
|
+
evidence: raw.trim().slice(0, 80),
|
|
3631
|
+
suggestion: "Rules files should not redefine the assistant's identity. Remove or rewrite this directive.",
|
|
3632
|
+
line,
|
|
3633
|
+
column: raw.search(ROLE_REASSIGNMENT_RE),
|
|
3634
|
+
autoFixable: false
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
if (AI_COMMENT_HIJACK_RE.test(raw)) {
|
|
3638
|
+
out.push({
|
|
3639
|
+
ruleId: "AIRA008",
|
|
3640
|
+
category: "prompt_injection",
|
|
3641
|
+
severity: "high",
|
|
3642
|
+
confidence: 0.8,
|
|
3643
|
+
message: "AI-targeted comment with override directive \u2014 hijack attempt",
|
|
3644
|
+
evidence: raw.trim().slice(0, 80),
|
|
3645
|
+
suggestion: 'Comments addressed to AI assistants that contain "ignore"/"override"/"actually" are an attack vector. Remove or rewrite.',
|
|
3646
|
+
line,
|
|
3647
|
+
column: raw.search(AI_COMMENT_HIJACK_RE),
|
|
3648
|
+
autoFixable: false
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
if (HIDDEN_CONTENT_BLOCK_RE.test(raw)) {
|
|
3652
|
+
out.push({
|
|
3653
|
+
ruleId: "AIRA010",
|
|
3654
|
+
category: "prompt_injection",
|
|
3655
|
+
severity: "high",
|
|
3656
|
+
confidence: 0.85,
|
|
3657
|
+
message: "Hidden content block \u2014 may conceal instructions from human reviewer",
|
|
3658
|
+
evidence: raw.trim().slice(0, 80),
|
|
3659
|
+
suggestion: "Visually hidden HTML / large MD comment blocks are a known prompt-injection vector. Remove or expose the content.",
|
|
3660
|
+
line,
|
|
3661
|
+
column: raw.search(HIDDEN_CONTENT_BLOCK_RE),
|
|
3662
|
+
autoFixable: false
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
if (SYSTEM_PROMPT_EXTRACTION_RE.test(raw)) {
|
|
3666
|
+
out.push({
|
|
3667
|
+
ruleId: "AIRA011",
|
|
3668
|
+
category: "prompt_injection",
|
|
3669
|
+
severity: "high",
|
|
3670
|
+
confidence: 0.85,
|
|
3671
|
+
message: "System-prompt extraction attempt in rules file [OWASP LLM07: System Prompt Leakage]",
|
|
3672
|
+
evidence: raw.trim().slice(0, 80),
|
|
3673
|
+
suggestion: "Rules files should never instruct the assistant to reveal its system prompt. Remove this directive.",
|
|
3674
|
+
line,
|
|
3675
|
+
column: raw.search(SYSTEM_PROMPT_EXTRACTION_RE),
|
|
3676
|
+
autoFixable: false
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
if (JAILBREAK_TOKEN_RE.test(raw)) {
|
|
3680
|
+
out.push({
|
|
3681
|
+
ruleId: "AIRA012",
|
|
3682
|
+
category: "prompt_injection",
|
|
3683
|
+
severity: "critical",
|
|
3684
|
+
confidence: 0.95,
|
|
3685
|
+
message: "Jailbreak / persona-override token in rules file",
|
|
3686
|
+
evidence: raw.trim().slice(0, 80),
|
|
3687
|
+
suggestion: 'Tokens like "DAN", "developer mode", "do anything now" are well-known jailbreak markers. Remove this content.',
|
|
3688
|
+
line,
|
|
3689
|
+
column: raw.search(JAILBREAK_TOKEN_RE),
|
|
3690
|
+
autoFixable: false
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3693
|
+
if (EXFIL_INTENT_RE.test(raw)) {
|
|
3694
|
+
out.push({
|
|
3695
|
+
ruleId: "AIRA013",
|
|
3696
|
+
category: "exfiltration",
|
|
3697
|
+
severity: "critical",
|
|
3698
|
+
confidence: 0.85,
|
|
3699
|
+
message: "Data-exfiltration directive in rules file [CVE-2025-55284, OWASP LLM02]",
|
|
3700
|
+
evidence: raw.trim().slice(0, 80),
|
|
3701
|
+
suggestion: "Rules files should never instruct the assistant to send code/secrets/context to external endpoints. Remove this directive.",
|
|
3702
|
+
line,
|
|
3703
|
+
column: raw.search(EXFIL_INTENT_RE),
|
|
3704
|
+
autoFixable: false
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
if (EXTERNAL_URL_RE.test(raw) && WEBHOOK_VERB_RE.test(raw)) {
|
|
3708
|
+
const urlMatch = raw.match(/https?:\/\/[\w.-]+(?::\d+)?/);
|
|
3709
|
+
const host = urlMatch ? this._extractHost(urlMatch[0]) : null;
|
|
3710
|
+
const isKnown = host && hostMatchesKnownSet(host, this._knownHosts);
|
|
3711
|
+
if (!isKnown) {
|
|
3712
|
+
const elevated = this._hasTruthpack;
|
|
3713
|
+
out.push({
|
|
3714
|
+
ruleId: "AIRA014",
|
|
3715
|
+
category: "exfiltration",
|
|
3716
|
+
severity: elevated ? "critical" : "high",
|
|
3717
|
+
confidence: elevated ? 0.99 : 0.8,
|
|
3718
|
+
message: elevated ? `External webhook / fetch URL to UNKNOWN host '${host ?? "?"}' (not in truthpack/integrations.json)` : "External webhook / fetch URL in rules file",
|
|
3719
|
+
evidence: raw.trim().slice(0, 80),
|
|
3720
|
+
suggestion: elevated ? `Host '${host ?? "?"}' is not a known integration for this project. If this is legitimate, add it to integrations.json and rerun \`vibecheck scan\`. Otherwise remove it.` : "Rules files should describe the project, not invoke external endpoints. Verify this URL is necessary; if so, document why.",
|
|
3721
|
+
line,
|
|
3722
|
+
column: raw.search(EXTERNAL_URL_RE),
|
|
3723
|
+
autoFixable: false
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
if (BASE64_PAYLOAD_RE.test(source)) {
|
|
3729
|
+
const m = BASE64_PAYLOAD_RE.exec(source);
|
|
3730
|
+
const evidence = m ? m[0].slice(0, 60) : "[base64 blob]";
|
|
3731
|
+
out.push({
|
|
3732
|
+
ruleId: "AIRA009",
|
|
3733
|
+
category: "prompt_injection",
|
|
3734
|
+
severity: "high",
|
|
3735
|
+
confidence: 0.7,
|
|
3736
|
+
message: "Base64-encoded payload or decoder call in rules file",
|
|
3737
|
+
evidence,
|
|
3738
|
+
suggestion: "Encoded payloads in rules files are an obfuscation vector. Decode the content and inline it in plaintext, or remove it.",
|
|
3739
|
+
line: findLineNumber(source, m?.[0] ?? ""),
|
|
3740
|
+
column: 0,
|
|
3741
|
+
autoFixable: false
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
// ─── MCP-config checks ──────────────────────────────────────────────────
|
|
3746
|
+
_checkMCPConfig(source, out) {
|
|
3747
|
+
const doc = parseMCPDocument(source);
|
|
3748
|
+
if (!doc) return;
|
|
3749
|
+
const servers = listMCPServers(doc);
|
|
3750
|
+
for (const { name, entry } of servers) {
|
|
3751
|
+
if (arrayContainsWildcard(entry.tools)) {
|
|
3752
|
+
out.push({
|
|
3753
|
+
ruleId: "AIRA016",
|
|
3754
|
+
category: "mcp_misconfig",
|
|
3755
|
+
severity: "critical",
|
|
3756
|
+
confidence: 0.99,
|
|
3757
|
+
message: `MCP server '${name}' exposes wildcard tool permissions ("*") [CWE-269; CVE-2025-54135]`,
|
|
3758
|
+
evidence: `tools: ["*"]`,
|
|
3759
|
+
suggestion: 'Replace "*" with an explicit tool allowlist, e.g. tools: ["read_file", "search"].',
|
|
3760
|
+
line: findLineNumber(source, `"${name}"`),
|
|
3761
|
+
column: 0,
|
|
3762
|
+
autoFixable: false
|
|
3763
|
+
});
|
|
3764
|
+
}
|
|
3765
|
+
const remoteUrl = typeof entry.url === "string" ? entry.url : void 0;
|
|
3766
|
+
if (remoteUrl && /^https?:\/\//i.test(remoteUrl)) {
|
|
3767
|
+
const host = this._extractHost(remoteUrl);
|
|
3768
|
+
const isKnown = host && hostMatchesKnownSet(host, this._knownHosts);
|
|
3769
|
+
const elevated = this._hasTruthpack && !isKnown;
|
|
3770
|
+
const downgraded = isKnown;
|
|
3771
|
+
out.push({
|
|
3772
|
+
ruleId: "AIRA017",
|
|
3773
|
+
category: "mcp_misconfig",
|
|
3774
|
+
severity: elevated ? "critical" : downgraded ? "low" : "high",
|
|
3775
|
+
confidence: elevated ? 0.99 : downgraded ? 0.5 : 0.9,
|
|
3776
|
+
message: elevated ? `MCP server '${name}' connects to UNKNOWN host '${host ?? "?"}' (not in truthpack/integrations.json) [CWE-829]` : downgraded ? `MCP server '${name}' connects to known integration '${host ?? "?"}' (informational)` : `MCP server '${name}' connects to a remote endpoint [CWE-829]`,
|
|
3777
|
+
evidence: remoteUrl.slice(0, 80),
|
|
3778
|
+
suggestion: elevated ? `Host '${host ?? "?"}' is not in your project's integrations. Confirm this is intentional; if legitimate, add it to truthpack/integrations.json.` : "Verify the remote MCP server is from a trusted operator. Prefer local stdio servers when possible.",
|
|
3779
|
+
line: findLineNumber(source, remoteUrl),
|
|
3780
|
+
column: 0,
|
|
3781
|
+
autoFixable: false
|
|
3782
|
+
});
|
|
3783
|
+
if (HTTP_NOT_HTTPS_RE.test(`"${remoteUrl}"`)) {
|
|
3784
|
+
out.push({
|
|
3785
|
+
ruleId: "AIRA024",
|
|
3786
|
+
category: "mcp_misconfig",
|
|
3787
|
+
severity: "critical",
|
|
3788
|
+
confidence: 0.99,
|
|
3789
|
+
message: `MCP server '${name}' uses unencrypted HTTP \u2014 MITM exposure`,
|
|
3790
|
+
evidence: remoteUrl.slice(0, 80),
|
|
3791
|
+
suggestion: "Use https:// for remote MCP servers. Reject any operator that requires plaintext HTTP.",
|
|
3792
|
+
line: findLineNumber(source, remoteUrl),
|
|
3793
|
+
column: 0,
|
|
3794
|
+
autoFixable: false
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
if (Array.isArray(entry.args)) {
|
|
3799
|
+
for (const arg of entry.args) {
|
|
3800
|
+
if (typeof arg === "string" && DENO_BROAD_PERMS_RE.test(arg)) {
|
|
3801
|
+
out.push({
|
|
3802
|
+
ruleId: "AIRA018",
|
|
3803
|
+
category: "mcp_misconfig",
|
|
3804
|
+
severity: "high",
|
|
3805
|
+
confidence: 0.95,
|
|
3806
|
+
message: `MCP server '${name}' uses overly permissive Deno args (${arg}) [CWE-732]`,
|
|
3807
|
+
evidence: arg,
|
|
3808
|
+
suggestion: "Replace --allow-all / -A with scoped permissions like --allow-read=/specific/path or --allow-net=api.example.com.",
|
|
3809
|
+
line: findLineNumber(source, arg),
|
|
3810
|
+
column: 0,
|
|
3811
|
+
autoFixable: false
|
|
3812
|
+
});
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
if (entry.env && typeof entry.env === "object") {
|
|
3817
|
+
for (const [k, v] of Object.entries(entry.env)) {
|
|
3818
|
+
if (typeof v !== "string") continue;
|
|
3819
|
+
if (/^\$\{?[A-Z_][A-Z0-9_]*\}?$/.test(v)) continue;
|
|
3820
|
+
if (PLAINTEXT_SECRET_VALUE_RE.test(v) || k.match(/key|secret|token|password/i) && v.length > 12 && !/^\$/.test(v)) {
|
|
3821
|
+
out.push({
|
|
3822
|
+
ruleId: "AIRA019",
|
|
3823
|
+
category: "mcp_misconfig",
|
|
3824
|
+
severity: "critical",
|
|
3825
|
+
confidence: 0.95,
|
|
3826
|
+
message: `MCP server '${name}' exposes a plaintext secret in env (${k}) [CWE-798]`,
|
|
3827
|
+
evidence: `${k}=[REDACTED]`,
|
|
3828
|
+
suggestion: `Reference the secret indirectly: env: { ${k}: "\${${k}}" } and set ${k} in your shell environment or OS keychain.`,
|
|
3829
|
+
line: findLineNumber(source, `"${k}"`),
|
|
3830
|
+
column: 0,
|
|
3831
|
+
autoFixable: false
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
if (isAuthDisabled(entry)) {
|
|
3837
|
+
out.push({
|
|
3838
|
+
ruleId: "AIRA020",
|
|
3839
|
+
category: "mcp_misconfig",
|
|
3840
|
+
severity: "high",
|
|
3841
|
+
confidence: 0.9,
|
|
3842
|
+
message: `MCP server '${name}' has authentication explicitly disabled [CWE-306]`,
|
|
3843
|
+
evidence: `auth: disabled`,
|
|
3844
|
+
suggestion: "Enable MCP server authentication. If the server cannot authenticate, do not expose it.",
|
|
3845
|
+
line: findLineNumber(source, `"${name}"`),
|
|
3846
|
+
column: 0,
|
|
3847
|
+
autoFixable: false
|
|
3848
|
+
});
|
|
3849
|
+
}
|
|
3850
|
+
if (objectHasWildcardKey(entry.permissions)) {
|
|
3851
|
+
out.push({
|
|
3852
|
+
ruleId: "AIRA021",
|
|
3853
|
+
category: "mcp_misconfig",
|
|
3854
|
+
severity: "critical",
|
|
3855
|
+
confidence: 0.95,
|
|
3856
|
+
message: `MCP server '${name}' has wildcard permissions [CWE-250]`,
|
|
3857
|
+
evidence: `permissions: { "*": ... }`,
|
|
3858
|
+
suggestion: "Replace wildcard with a scoped permission map.",
|
|
3859
|
+
line: findLineNumber(source, `"${name}"`),
|
|
3860
|
+
column: 0,
|
|
3861
|
+
autoFixable: false
|
|
3862
|
+
});
|
|
3863
|
+
}
|
|
3864
|
+
if (objectHasWildcardKey(entry.network) || typeof entry.network === "string" && entry.network === "*") {
|
|
3865
|
+
out.push({
|
|
3866
|
+
ruleId: "AIRA022",
|
|
3867
|
+
category: "mcp_misconfig",
|
|
3868
|
+
severity: "high",
|
|
3869
|
+
confidence: 0.9,
|
|
3870
|
+
message: `MCP server '${name}' allows unrestricted network access [CWE-284]`,
|
|
3871
|
+
evidence: `network: *`,
|
|
3872
|
+
suggestion: 'Restrict network: { allow: ["specific-host.example.com"] }.',
|
|
3873
|
+
line: findLineNumber(source, `"${name}"`),
|
|
3874
|
+
column: 0,
|
|
3875
|
+
autoFixable: false
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
if (typeof entry.command === "string" && NPX_AUTO_YES_RE.test(entry.command)) {
|
|
3879
|
+
out.push({
|
|
3880
|
+
ruleId: "AIRA023",
|
|
3881
|
+
category: "mcp_misconfig",
|
|
3882
|
+
severity: "high",
|
|
3883
|
+
confidence: 0.95,
|
|
3884
|
+
message: `MCP server '${name}' auto-confirms package install (npx -y / equivalent)`,
|
|
3885
|
+
evidence: entry.command.slice(0, 80),
|
|
3886
|
+
suggestion: "Pin the package + version (e.g. npx --package=@org/pkg@1.2.3) and remove -y. Consider installing as a dev dep instead.",
|
|
3887
|
+
line: findLineNumber(source, entry.command),
|
|
3888
|
+
column: 0,
|
|
3889
|
+
autoFixable: false
|
|
3890
|
+
});
|
|
3891
|
+
}
|
|
3892
|
+
if (Array.isArray(entry.args) && entry.args.some((a) => typeof a === "string" && /^-y$|^--yes$/.test(a))) {
|
|
3893
|
+
const cmd = typeof entry.command === "string" ? entry.command : "";
|
|
3894
|
+
if (!NPX_AUTO_YES_RE.test(cmd)) {
|
|
3895
|
+
out.push({
|
|
3896
|
+
ruleId: "AIRA023",
|
|
3897
|
+
category: "mcp_misconfig",
|
|
3898
|
+
severity: "high",
|
|
3899
|
+
confidence: 0.9,
|
|
3900
|
+
message: `MCP server '${name}' passes -y / --yes \u2014 auto-confirms package install`,
|
|
3901
|
+
evidence: `args: ["-y", ...]`,
|
|
3902
|
+
suggestion: "Remove -y. Pin the package version explicitly.",
|
|
3903
|
+
line: findLineNumber(source, `"${name}"`),
|
|
3904
|
+
column: 0,
|
|
3905
|
+
autoFixable: false
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
if (typeof entry.command === "string" && UNVERIFIED_GIT_URL_RE.test(entry.command)) {
|
|
3910
|
+
out.push({
|
|
3911
|
+
ruleId: "AIRA025",
|
|
3912
|
+
category: "mcp_misconfig",
|
|
3913
|
+
severity: "high",
|
|
3914
|
+
confidence: 0.9,
|
|
3915
|
+
message: `MCP server '${name}' installs from a Git URL with no commit pin`,
|
|
3916
|
+
evidence: entry.command.slice(0, 80),
|
|
3917
|
+
suggestion: "Pin to a specific commit: git+https://...#<full-sha>. A branch ref alone can change under you.",
|
|
3918
|
+
line: findLineNumber(source, entry.command),
|
|
3919
|
+
column: 0,
|
|
3920
|
+
autoFixable: false
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
const desc = entry.description ?? entry.description_for_model;
|
|
3924
|
+
if (typeof desc === "string" && TOOL_POISONING_RE.test(desc)) {
|
|
3925
|
+
out.push({
|
|
3926
|
+
ruleId: "AIRA015",
|
|
3927
|
+
category: "tool_poisoning",
|
|
3928
|
+
severity: "critical",
|
|
3929
|
+
confidence: 0.9,
|
|
3930
|
+
message: `MCP server '${name}' description contains tool-poisoning directives`,
|
|
3931
|
+
evidence: desc.slice(0, 80),
|
|
3932
|
+
suggestion: "Tool descriptions should describe what the tool does, not instruct the LLM to always call it or hide info from the user. Rewrite as a neutral description.",
|
|
3933
|
+
line: findLineNumber(source, `"${name}"`),
|
|
3934
|
+
column: 0,
|
|
3935
|
+
autoFixable: false
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
if (Array.isArray(doc.tools)) {
|
|
3940
|
+
for (const tool of doc.tools) {
|
|
3941
|
+
if (tool && typeof tool.description === "string" && TOOL_POISONING_RE.test(tool.description)) {
|
|
3942
|
+
out.push({
|
|
3943
|
+
ruleId: "AIRA015",
|
|
3944
|
+
category: "tool_poisoning",
|
|
3945
|
+
severity: "critical",
|
|
3946
|
+
confidence: 0.9,
|
|
3947
|
+
message: `Tool '${tool.name ?? "?"}' description contains tool-poisoning directives`,
|
|
3948
|
+
evidence: tool.description.slice(0, 80),
|
|
3949
|
+
suggestion: 'Rewrite tool descriptions as neutral capability statements. Remove "ALWAYS"/"MUST"/"hide from user" directives.',
|
|
3950
|
+
line: findLineNumber(source, tool.description),
|
|
3951
|
+
column: 0,
|
|
3952
|
+
autoFixable: false
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
3959
|
+
_hasHomoglyphMix(text) {
|
|
3960
|
+
if (!HOMOGLYPH_LATIN_RE.test(text)) return false;
|
|
3961
|
+
if (!HOMOGLYPH_CYRILLIC_LOOKALIKES_RE.test(text) && !HOMOGLYPH_GREEK_LOOKALIKES_RE.test(text)) {
|
|
3962
|
+
return false;
|
|
3963
|
+
}
|
|
3964
|
+
for (const word of text.split(/\s+/)) {
|
|
3965
|
+
if (word.length >= 3 && HOMOGLYPH_LATIN_RE.test(word) && (HOMOGLYPH_CYRILLIC_LOOKALIKES_RE.test(word) || HOMOGLYPH_GREEK_LOOKALIKES_RE.test(word))) {
|
|
3966
|
+
return true;
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
return false;
|
|
3970
|
+
}
|
|
3971
|
+
/**
|
|
3972
|
+
* Extract the lowercased hostname from a URL substring. Returns null if the
|
|
3973
|
+
* URL is malformed. Tolerant of trailing punctuation/quotes.
|
|
3974
|
+
*/
|
|
3975
|
+
_extractHost(url) {
|
|
3976
|
+
const cleaned = url.replace(/["'`,;).\]]+$/, "");
|
|
3977
|
+
try {
|
|
3978
|
+
return new URL(cleaned).hostname.toLowerCase();
|
|
3979
|
+
} catch {
|
|
3980
|
+
const m = cleaned.match(/^https?:\/\/([\w.-]+)/i);
|
|
3981
|
+
return m ? m[1].toLowerCase() : null;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
/**
|
|
3985
|
+
* Render evidence safely — strip the offending char so log/UI consumers
|
|
3986
|
+
* don't accidentally re-trigger downstream Unicode bugs. Cap at 80 chars.
|
|
3987
|
+
*/
|
|
3988
|
+
_safeEvidence(line) {
|
|
3989
|
+
return line.replace(INVISIBLE_UNICODE_RE, "\xB7").replace(BIDI_OVERRIDE_RE, "\xB7").replace(TAG_CHARS_RE, "\xB7").trim().slice(0, 80);
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Co-occurrence boost — when 3+ distinct attack categories fire in the
|
|
3993
|
+
* same file, every finding's severity advances one rung and confidence
|
|
3994
|
+
* floors at 0.99. Empirical signal: an attack file usually triggers
|
|
3995
|
+
* multiple categories at once; benign noise rarely does.
|
|
3996
|
+
*/
|
|
3997
|
+
_applyCoOccurrenceBoost(findings) {
|
|
3998
|
+
const categories = new Set(findings.map((f) => f.category));
|
|
3999
|
+
if (categories.size < 3) return;
|
|
4000
|
+
for (const f of findings) {
|
|
4001
|
+
const idx = SEVERITY_LADDER.indexOf(f.severity);
|
|
4002
|
+
if (idx >= 0 && idx < SEVERITY_LADDER.length - 1) {
|
|
4003
|
+
f.severity = SEVERITY_LADDER[idx + 1];
|
|
4004
|
+
}
|
|
4005
|
+
f.confidence = Math.max(f.confidence, 0.99);
|
|
4006
|
+
f.message = `${f.message} [BOOSTED: ${categories.size}-category co-occurrence]`;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
};
|
|
4010
|
+
var TEST_FILE_RE = /(?:\.(?:test|spec)\.(?:t|j)sx?$)|(?:^|\/)(?:__tests__|tests?|e2e|spec|cypress|playwright)\//i;
|
|
4011
|
+
var STORY_FILE_RE = /\.stories\.(?:ts|tsx|js|jsx|mdx)$/i;
|
|
4012
|
+
var EXPECT_CALL_RE = /\bexpect\s*\(/g;
|
|
4013
|
+
var IT_CALL_RE = /\b(?:it|test)\s*\(/g;
|
|
4014
|
+
var DESCRIBE_CALL_RE = /\bdescribe\s*\(/g;
|
|
4015
|
+
var SKIPPED_TEST_RE = /\b(?:it|test|describe)\.(?:skip|todo)\s*\(|\b(?:xit|xtest|xdescribe)\s*\(/g;
|
|
4016
|
+
var MOCK_CALL_RE = /\b(?:vi|jest)\.(?:mock|spyOn|fn)\s*\(|\b(?:mock|spyOn|stub|sinon\.(?:stub|spy|mock))\s*\(/g;
|
|
4017
|
+
var TAUTOLOGICAL_ASSERTIONS = [
|
|
4018
|
+
/expect\s*\(\s*true\s*\)\s*\.toBe\s*\(\s*true\s*\)/i,
|
|
4019
|
+
/expect\s*\(\s*false\s*\)\s*\.toBe\s*\(\s*false\s*\)/i,
|
|
4020
|
+
/expect\s*\(\s*1\s*\)\s*\.toBe\s*\(\s*1\s*\)/i,
|
|
4021
|
+
/expect\s*\(\s*['"]?\s*['"]?\s*\)\s*\.toBe\s*\(\s*['"]?\s*['"]?\s*\)/i,
|
|
4022
|
+
/expect\s*\(\s*null\s*\)\s*\.toBe\s*\(\s*null\s*\)/i,
|
|
4023
|
+
/expect\s*\(\s*undefined\s*\)\s*\.toBe\s*\(\s*undefined\s*\)/i,
|
|
4024
|
+
// expect(x).toBe(x) — backreference requires the variable to be captured.
|
|
4025
|
+
/expect\s*\(\s*([A-Za-z_$][\w$]*)\s*\)\s*\.toBe\s*\(\s*\1\s*\)/i,
|
|
4026
|
+
/expect\s*\(\s*([A-Za-z_$][\w$]*)\s*\)\s*\.toEqual\s*\(\s*\1\s*\)/i,
|
|
4027
|
+
/expect\s*\(\s*typeof\s+[\w$.]+\s*\)\s*\.toBe\s*\(\s*['"](?:string|number|boolean|object|function|undefined)['"]\s*\)/i
|
|
4028
|
+
];
|
|
4029
|
+
function extractTestBlocks(source) {
|
|
4030
|
+
const blocks = [];
|
|
4031
|
+
const re = /\b(?:it|test)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{/g;
|
|
4032
|
+
let m;
|
|
4033
|
+
while ((m = re.exec(source)) !== null) {
|
|
4034
|
+
const name = m[1] ?? "";
|
|
4035
|
+
const startIdx = m.index + m[0].length;
|
|
4036
|
+
const startLine = lineOf(source, m.index);
|
|
4037
|
+
let depth = 1;
|
|
4038
|
+
let i = startIdx;
|
|
4039
|
+
let inStr = null;
|
|
4040
|
+
let inTpl = false;
|
|
4041
|
+
while (i < source.length && depth > 0) {
|
|
4042
|
+
const c2 = source[i];
|
|
4043
|
+
if (inStr) {
|
|
4044
|
+
if (c2 === "\\") {
|
|
4045
|
+
i += 2;
|
|
4046
|
+
continue;
|
|
4047
|
+
}
|
|
4048
|
+
if (c2 === inStr) inStr = null;
|
|
4049
|
+
} else if (inTpl) {
|
|
4050
|
+
if (c2 === "\\") {
|
|
4051
|
+
i += 2;
|
|
4052
|
+
continue;
|
|
4053
|
+
}
|
|
4054
|
+
if (c2 === "`") inTpl = false;
|
|
4055
|
+
} else {
|
|
4056
|
+
if (c2 === '"' || c2 === "'") inStr = c2;
|
|
4057
|
+
else if (c2 === "`") inTpl = true;
|
|
4058
|
+
else if (c2 === "{") depth++;
|
|
4059
|
+
else if (c2 === "}") depth--;
|
|
4060
|
+
}
|
|
4061
|
+
i++;
|
|
4062
|
+
}
|
|
4063
|
+
const endLine = lineOf(source, i);
|
|
4064
|
+
blocks.push({
|
|
4065
|
+
startLine,
|
|
4066
|
+
endLine,
|
|
4067
|
+
name,
|
|
4068
|
+
body: source.slice(startIdx, i)
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
return blocks;
|
|
4072
|
+
}
|
|
4073
|
+
function lineOf(source, index) {
|
|
4074
|
+
let line = 1;
|
|
4075
|
+
for (let i = 0; i < index && i < source.length; i++) {
|
|
4076
|
+
if (source.charCodeAt(i) === 10) line++;
|
|
4077
|
+
}
|
|
4078
|
+
return line;
|
|
4079
|
+
}
|
|
4080
|
+
function countMatches(source, re) {
|
|
4081
|
+
const r = new RegExp(re.source, re.flags);
|
|
4082
|
+
let count = 0;
|
|
4083
|
+
while (r.exec(source) !== null) count++;
|
|
4084
|
+
return count;
|
|
4085
|
+
}
|
|
4086
|
+
var TestQualityEngine = class extends BaseEngine {
|
|
4087
|
+
id = "test-quality";
|
|
4088
|
+
name = "Test Quality Engine";
|
|
4089
|
+
version = "1.0.0";
|
|
4090
|
+
supportedExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"]);
|
|
4091
|
+
async scan(delta, signal) {
|
|
4092
|
+
const uri = delta.documentUri;
|
|
4093
|
+
const cleanUri = uri.replace(/^file:\/\//, "");
|
|
4094
|
+
if (!TEST_FILE_RE.test(cleanUri)) return [];
|
|
4095
|
+
if (STORY_FILE_RE.test(cleanUri)) return [];
|
|
4096
|
+
if (/\/tests?\/(?:setup|helpers?|utils?|fixtures?)[^/]*$/i.test(cleanUri)) return [];
|
|
4097
|
+
this.checkAbort(signal);
|
|
4098
|
+
const source = delta.fullText;
|
|
4099
|
+
const lines = source.split("\n");
|
|
4100
|
+
const pending = [];
|
|
4101
|
+
const expectCount = countMatches(source, EXPECT_CALL_RE);
|
|
4102
|
+
const itCount = countMatches(source, IT_CALL_RE);
|
|
4103
|
+
const describeCount = countMatches(source, DESCRIBE_CALL_RE);
|
|
4104
|
+
const mockCount = countMatches(source, MOCK_CALL_RE);
|
|
4105
|
+
if ((itCount > 0 || describeCount > 0) && expectCount === 0) {
|
|
4106
|
+
const usesAssert = /\bassert\s*\(|\bassert\.(?:ok|equal|deepEqual|strictEqual)/i.test(source);
|
|
4107
|
+
const usesChai = /\.\s*should\b|\bexpect\s*\([^)]+\)\s*\.\s*to\b/i.test(source);
|
|
4108
|
+
if (!usesAssert && !usesChai) {
|
|
4109
|
+
pending.push({
|
|
4110
|
+
ruleId: "TQ001",
|
|
4111
|
+
severity: "high",
|
|
4112
|
+
confidence: 0.9,
|
|
4113
|
+
message: `Test file has ${itCount} test block(s) but zero assertions`,
|
|
4114
|
+
evidence: `${itCount} it/test blocks, 0 expect()`,
|
|
4115
|
+
suggestion: "Add `expect(...)` (vitest/jest) or `assert(...)` (node:test) calls to verify behavior. A test without assertions cannot fail except by throwing.",
|
|
4116
|
+
line: 1,
|
|
4117
|
+
column: 0
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
{
|
|
4122
|
+
const r = new RegExp(SKIPPED_TEST_RE.source, SKIPPED_TEST_RE.flags);
|
|
4123
|
+
let m;
|
|
4124
|
+
while ((m = r.exec(source)) !== null) {
|
|
4125
|
+
pending.push({
|
|
4126
|
+
ruleId: "TQ005",
|
|
4127
|
+
severity: "medium",
|
|
4128
|
+
confidence: 0.95,
|
|
4129
|
+
message: `Skipped test left in source: ${m[0]}`,
|
|
4130
|
+
evidence: m[0],
|
|
4131
|
+
suggestion: "Remove `.skip` / `xit` / `xdescribe` once the test is fixed, or delete the test if it is no longer relevant. Skipped tests are silent dead weight in CI.",
|
|
4132
|
+
line: lineOf(source, m.index),
|
|
4133
|
+
column: 0
|
|
4134
|
+
});
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4138
|
+
const raw = lines[i];
|
|
4139
|
+
for (const re of TAUTOLOGICAL_ASSERTIONS) {
|
|
4140
|
+
if (re.test(raw)) {
|
|
4141
|
+
pending.push({
|
|
4142
|
+
ruleId: "TQ002",
|
|
4143
|
+
severity: "high",
|
|
4144
|
+
confidence: 0.95,
|
|
4145
|
+
message: "Tautological assertion \u2014 passes regardless of implementation",
|
|
4146
|
+
evidence: raw.trim().slice(0, 80),
|
|
4147
|
+
suggestion: "Replace with an assertion that compares actual behavior against an expected outcome (e.g. expect(result).toBe(expectedValue)).",
|
|
4148
|
+
line: i + 1,
|
|
4149
|
+
column: raw.search(re)
|
|
4150
|
+
});
|
|
4151
|
+
break;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4156
|
+
const raw = lines[i];
|
|
4157
|
+
if (!/\bconsole\.(?:log|info|debug)\s*\(/.test(raw)) continue;
|
|
4158
|
+
if (i === 0 && /^\s*(?:\/\/|\/\*)/.test(raw)) continue;
|
|
4159
|
+
const window = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join("\n");
|
|
4160
|
+
if (/\bexpect\s*\(/.test(window)) continue;
|
|
4161
|
+
pending.push({
|
|
4162
|
+
ruleId: "TQ007",
|
|
4163
|
+
severity: "medium",
|
|
4164
|
+
confidence: 0.7,
|
|
4165
|
+
message: "console.log in test without nearby expect() \u2014 likely debugging leftover or assertion stand-in",
|
|
4166
|
+
evidence: raw.trim().slice(0, 80),
|
|
4167
|
+
suggestion: "Remove the console.log if it was for debugging, or replace with an `expect()` call that verifies the condition.",
|
|
4168
|
+
line: i + 1,
|
|
4169
|
+
column: 0
|
|
4170
|
+
});
|
|
4171
|
+
}
|
|
4172
|
+
if (itCount > 0) {
|
|
4173
|
+
const blocks = extractTestBlocks(source);
|
|
4174
|
+
for (const block of blocks) {
|
|
4175
|
+
const blockExpects = countMatches(block.body, EXPECT_CALL_RE);
|
|
4176
|
+
const hasNotToThrow = /\.\s*not\s*\.\s*toThrow\s*\(/.test(block.body);
|
|
4177
|
+
const hasStrongAssertion = /\.\s*(?:toBe|toEqual|toMatch|toContain|toHaveLength|toHaveProperty|toHaveBeenCalled|toStrictEqual|toBeNull|toBeUndefined|toBeDefined|toBeTruthy|toBeFalsy|toBeGreaterThan|toBeLessThan|toBeCloseTo|toBeInstanceOf|toThrow)\b/.test(
|
|
4178
|
+
block.body
|
|
4179
|
+
);
|
|
4180
|
+
const hasNonNegatedStrongAssertion = hasStrongAssertion && /(?<!\.\s*not\s*)\.\s*(?:toBe|toEqual|toMatch|toContain|toHaveLength|toHaveProperty|toHaveBeenCalled|toStrictEqual|toBeNull|toBeUndefined|toBeDefined|toBeTruthy|toBeFalsy|toBeGreaterThan|toBeLessThan|toBeCloseTo|toBeInstanceOf|toThrow)\b/.test(
|
|
4181
|
+
block.body
|
|
4182
|
+
);
|
|
4183
|
+
const onlyNotToThrow = blockExpects > 0 && hasNotToThrow && !hasNonNegatedStrongAssertion;
|
|
4184
|
+
if (onlyNotToThrow && /^\s*should\b/i.test(block.name) && !/throw|error|reject|fail/i.test(block.name)) {
|
|
4185
|
+
pending.push({
|
|
4186
|
+
ruleId: "TQ006",
|
|
4187
|
+
severity: "medium",
|
|
4188
|
+
confidence: 0.8,
|
|
4189
|
+
message: `Test "${block.name}" only checks that nothing throws \u2014 name implies a stronger assertion`,
|
|
4190
|
+
evidence: `it("${block.name}", ...) { expect(...).not.toThrow() }`,
|
|
4191
|
+
suggestion: "Add an explicit assertion against the function output. `not.toThrow()` only catches exceptions; it does not verify return values, side effects, or state.",
|
|
4192
|
+
line: block.startLine,
|
|
4193
|
+
column: 0
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
const weakPresenceRe = /\.\s*(?:toBeDefined|toBeTruthy|not\s*\.\s*toBeUndefined|not\s*\.\s*toBeNull)\s*\(/;
|
|
4197
|
+
const hasWeakPresence = weakPresenceRe.test(block.body);
|
|
4198
|
+
const hasReallyStrong = /\.\s*(?:toBe|toEqual|toMatch|toContain|toHaveLength|toHaveProperty|toHaveBeenCalled|toStrictEqual|toBeGreaterThan|toBeLessThan|toBeCloseTo|toBeInstanceOf|toThrow)\b/.test(
|
|
4199
|
+
block.body
|
|
4200
|
+
);
|
|
4201
|
+
const onlyWeakPresence = blockExpects > 0 && hasWeakPresence && !hasReallyStrong && !hasNotToThrow;
|
|
4202
|
+
if (onlyWeakPresence && /^\s*should\b/i.test(block.name) && !/defined|exist|present|truthy|render|return|undefined|null/i.test(block.name)) {
|
|
4203
|
+
pending.push({
|
|
4204
|
+
ruleId: "TQ009",
|
|
4205
|
+
severity: "medium",
|
|
4206
|
+
confidence: 0.8,
|
|
4207
|
+
message: `Test "${block.name}" only checks presence \u2014 name implies a stronger assertion`,
|
|
4208
|
+
evidence: `it("${block.name}", ...) { expect(...).toBeDefined() }`,
|
|
4209
|
+
suggestion: "`.toBeDefined()` / `.toBeTruthy()` confirm the value exists but not that it is correct. Add an explicit assertion against the expected output.",
|
|
4210
|
+
line: block.startLine,
|
|
4211
|
+
column: 0
|
|
4212
|
+
});
|
|
4213
|
+
}
|
|
4214
|
+
const tryCatchSwallow = /try\s*\{[\s\S]*?expect\s*\([\s\S]*?\}\s*catch\s*\(\s*\w*\s*\)\s*\{[^}]*\}/.test(
|
|
4215
|
+
block.body
|
|
4216
|
+
) && !/throw|expect\.fail|done\s*\(/.test(block.body.split("catch")[1] ?? "");
|
|
4217
|
+
if (tryCatchSwallow) {
|
|
4218
|
+
pending.push({
|
|
4219
|
+
ruleId: "TQ004",
|
|
4220
|
+
severity: "critical",
|
|
4221
|
+
confidence: 0.9,
|
|
4222
|
+
message: `Test "${block.name}" swallows assertion failures inside try/catch`,
|
|
4223
|
+
evidence: "try { expect(...) } catch { /* swallowed */ }",
|
|
4224
|
+
suggestion: "Remove the try/catch around assertions, or have the catch block re-throw / call `expect.fail`. Assertion errors must propagate to the runner.",
|
|
4225
|
+
line: block.startLine,
|
|
4226
|
+
column: 0
|
|
4227
|
+
});
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
if (mockCount >= 4 && expectCount >= 1 && mockCount >= expectCount * 2) {
|
|
4231
|
+
pending.push({
|
|
4232
|
+
ruleId: "TQ003",
|
|
4233
|
+
severity: "medium",
|
|
4234
|
+
confidence: 0.7,
|
|
4235
|
+
message: `Mock-heavy test file: ${mockCount} mock calls vs ${expectCount} assertion(s)`,
|
|
4236
|
+
evidence: `mocks=${mockCount}, expects=${expectCount}`,
|
|
4237
|
+
suggestion: 'When mocks dominate assertions, the test usually verifies "the function called the mock", not real behavior. Reduce mocking surface or add behavioral assertions.',
|
|
4238
|
+
line: 1,
|
|
4239
|
+
column: 0
|
|
4240
|
+
});
|
|
4241
|
+
}
|
|
4242
|
+
const importRe = /^\s*import\s+(?:[\w*\s{},]+\s+from\s+)?['"]([^'"]+)['"]/gm;
|
|
4243
|
+
const projectImport = (() => {
|
|
4244
|
+
let m;
|
|
4245
|
+
while ((m = importRe.exec(source)) !== null) {
|
|
4246
|
+
const spec = m[1] ?? "";
|
|
4247
|
+
if (spec.startsWith(".") || spec.startsWith("@/") || spec.startsWith("~/")) return true;
|
|
4248
|
+
}
|
|
4249
|
+
return false;
|
|
4250
|
+
})();
|
|
4251
|
+
if (!projectImport && itCount >= 2 && expectCount >= 2) {
|
|
4252
|
+
pending.push({
|
|
4253
|
+
ruleId: "TQ008",
|
|
4254
|
+
severity: "medium",
|
|
4255
|
+
confidence: 0.65,
|
|
4256
|
+
message: "Test file imports nothing from the project \u2014 what is being tested?",
|
|
4257
|
+
evidence: "no relative or aliased imports",
|
|
4258
|
+
suggestion: "A test that does not import any project module either tests nothing or duplicates the production code inline. Either import the unit under test or delete this file.",
|
|
4259
|
+
line: 1,
|
|
4260
|
+
column: 0
|
|
4261
|
+
});
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
if (pending.length === 0) return [];
|
|
4265
|
+
return pending.map(
|
|
4266
|
+
(p) => this.createFinding({
|
|
4267
|
+
id: this.deterministicId(cleanUri, p.line, p.column, p.ruleId, p.evidence.slice(0, 32)),
|
|
4268
|
+
ruleId: p.ruleId,
|
|
4269
|
+
category: "test_quality",
|
|
4270
|
+
message: p.message,
|
|
4271
|
+
severity: p.severity,
|
|
4272
|
+
confidence: p.confidence,
|
|
4273
|
+
file: cleanUri,
|
|
4274
|
+
line: p.line,
|
|
4275
|
+
column: p.column,
|
|
4276
|
+
evidence: p.evidence,
|
|
4277
|
+
suggestion: p.suggestion,
|
|
4278
|
+
autoFixable: false
|
|
4279
|
+
})
|
|
4280
|
+
);
|
|
4281
|
+
}
|
|
4282
|
+
};
|
|
4283
|
+
var BreakerState = /* @__PURE__ */ ((BreakerState2) => {
|
|
4284
|
+
BreakerState2[BreakerState2["Closed"] = 0] = "Closed";
|
|
4285
|
+
BreakerState2[BreakerState2["Open"] = 1] = "Open";
|
|
4286
|
+
BreakerState2[BreakerState2["HalfOpen"] = 2] = "HalfOpen";
|
|
4287
|
+
return BreakerState2;
|
|
4288
|
+
})(BreakerState || {});
|
|
4289
|
+
var CircuitBreaker = class {
|
|
4290
|
+
constructor(_threshold = 5, _cooldownMs = 3e4) {
|
|
4291
|
+
this._threshold = _threshold;
|
|
4292
|
+
this._cooldownMs = _cooldownMs;
|
|
4293
|
+
}
|
|
4294
|
+
_state = 0;
|
|
4295
|
+
_failures = 0;
|
|
4296
|
+
_halfOpenTimer = null;
|
|
4297
|
+
get isOpen() {
|
|
4298
|
+
return this._state === 1;
|
|
4299
|
+
}
|
|
4300
|
+
get state() {
|
|
4301
|
+
return BreakerState[this._state];
|
|
4302
|
+
}
|
|
4303
|
+
recordSuccess() {
|
|
4304
|
+
this._failures = 0;
|
|
4305
|
+
this._state = 0;
|
|
4306
|
+
}
|
|
4307
|
+
recordFailure() {
|
|
4308
|
+
this._failures++;
|
|
4309
|
+
if (this._failures >= this._threshold) {
|
|
4310
|
+
this._state = 1;
|
|
4311
|
+
this._scheduleHalfOpen();
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
tryAllow() {
|
|
4315
|
+
if (this._state === 0) return true;
|
|
4316
|
+
if (this._state === 2) {
|
|
4317
|
+
this._state = 0;
|
|
4318
|
+
return true;
|
|
4319
|
+
}
|
|
4320
|
+
return false;
|
|
4321
|
+
}
|
|
4322
|
+
_scheduleHalfOpen() {
|
|
4323
|
+
if (this._halfOpenTimer) clearTimeout(this._halfOpenTimer);
|
|
4324
|
+
this._halfOpenTimer = setTimeout(() => {
|
|
4325
|
+
this._state = 2;
|
|
4326
|
+
this._halfOpenTimer = null;
|
|
4327
|
+
}, this._cooldownMs);
|
|
4328
|
+
}
|
|
4329
|
+
dispose() {
|
|
4330
|
+
if (this._halfOpenTimer) clearTimeout(this._halfOpenTimer);
|
|
4331
|
+
}
|
|
4332
|
+
};
|
|
4333
|
+
var EngineRegistry = class {
|
|
4334
|
+
_slots = /* @__PURE__ */ new Map();
|
|
4335
|
+
_breakers = /* @__PURE__ */ new Map();
|
|
4336
|
+
_registrationOrder = /* @__PURE__ */ new Map();
|
|
4337
|
+
_nextOrder = 0;
|
|
4338
|
+
_activeCache = null;
|
|
4339
|
+
/**
|
|
4340
|
+
* Register an engine. If an engine with the same ID already exists, it is replaced.
|
|
4341
|
+
*/
|
|
4342
|
+
register(engine, options = {}) {
|
|
4343
|
+
const slot = {
|
|
4344
|
+
engine,
|
|
4345
|
+
timeoutMs: options.timeoutMs ?? 200,
|
|
4346
|
+
priority: options.priority ?? 100,
|
|
4347
|
+
enabled: options.enabled ?? true,
|
|
4348
|
+
extensions: options.extensions ?? void 0
|
|
4349
|
+
};
|
|
4350
|
+
this._slots.set(engine.id, slot);
|
|
4351
|
+
this._breakers.set(engine.id, new CircuitBreaker());
|
|
4352
|
+
if (!this._registrationOrder.has(engine.id)) {
|
|
4353
|
+
this._registrationOrder.set(engine.id, this._nextOrder++);
|
|
4354
|
+
}
|
|
4355
|
+
this._activeCache = null;
|
|
4356
|
+
}
|
|
4357
|
+
/**
|
|
4358
|
+
* Deregister an engine by ID. Disposes the engine and its circuit breaker.
|
|
4359
|
+
*/
|
|
4360
|
+
deregister(id) {
|
|
4361
|
+
const slot = this._slots.get(id);
|
|
4362
|
+
if (!slot) return false;
|
|
4363
|
+
slot.engine.dispose?.();
|
|
4364
|
+
this._breakers.get(id)?.dispose();
|
|
4365
|
+
this._slots.delete(id);
|
|
4366
|
+
this._breakers.delete(id);
|
|
4367
|
+
this._activeCache = null;
|
|
4368
|
+
return true;
|
|
4369
|
+
}
|
|
4370
|
+
/**
|
|
4371
|
+
* Get a specific engine slot by ID.
|
|
4372
|
+
*/
|
|
4373
|
+
get(id) {
|
|
4374
|
+
return this._slots.get(id);
|
|
4375
|
+
}
|
|
4376
|
+
/**
|
|
4377
|
+
* Get the circuit breaker for an engine.
|
|
4378
|
+
*/
|
|
4379
|
+
getBreaker(id) {
|
|
4380
|
+
return this._breakers.get(id);
|
|
4381
|
+
}
|
|
4382
|
+
/**
|
|
4383
|
+
* Get all enabled engine slots, sorted by priority (ascending), then registration order.
|
|
4384
|
+
*/
|
|
4385
|
+
getActive() {
|
|
4386
|
+
if (this._activeCache) return this._activeCache;
|
|
4387
|
+
this._activeCache = [...this._slots.values()].filter((s) => s.enabled !== false).sort((a, b) => {
|
|
4388
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
4389
|
+
return (this._registrationOrder.get(a.engine.id) ?? 0) - (this._registrationOrder.get(b.engine.id) ?? 0);
|
|
4390
|
+
});
|
|
4391
|
+
return this._activeCache;
|
|
4392
|
+
}
|
|
4393
|
+
/**
|
|
4394
|
+
* Get all registered engine slots (including disabled).
|
|
4395
|
+
*/
|
|
4396
|
+
getAll() {
|
|
4397
|
+
return [...this._slots.values()];
|
|
4398
|
+
}
|
|
4399
|
+
/**
|
|
4400
|
+
* Enable or disable an engine by ID.
|
|
4401
|
+
*/
|
|
4402
|
+
setEnabled(id, enabled) {
|
|
4403
|
+
const slot = this._slots.get(id);
|
|
4404
|
+
if (!slot) return false;
|
|
4405
|
+
slot.enabled = enabled;
|
|
4406
|
+
this._activeCache = null;
|
|
4407
|
+
return true;
|
|
4408
|
+
}
|
|
4409
|
+
/**
|
|
4410
|
+
* Activate all registered engines. Call once at startup.
|
|
4411
|
+
*/
|
|
4412
|
+
async activateAll(onError) {
|
|
3285
4413
|
const tasks = [...this._slots.entries()].map(async ([id, slot]) => {
|
|
3286
4414
|
try {
|
|
3287
4415
|
await slot.engine.activate?.();
|
|
@@ -3312,7 +4440,7 @@ var EngineRegistry = class {
|
|
|
3312
4440
|
if (supportedExtensions) {
|
|
3313
4441
|
const uri = delta.documentUri;
|
|
3314
4442
|
const pathPart = uri.replace(/^file:\/\//, "");
|
|
3315
|
-
const ext =
|
|
4443
|
+
const ext = path6__default.extname(pathPart).toLowerCase() || ".ts";
|
|
3316
4444
|
if (!supportedExtensions.has(ext)) {
|
|
3317
4445
|
return {
|
|
3318
4446
|
findings: [],
|
|
@@ -3408,6 +4536,8 @@ function createDefaultRegistry() {
|
|
|
3408
4536
|
registry.register(new TypeContractEngine(), { priority: 50, timeoutMs: 120 });
|
|
3409
4537
|
registry.register(new SecurityPatternEngine(), { priority: 55, timeoutMs: 100 });
|
|
3410
4538
|
registry.register(new PerformanceAntipatternEngine(), { priority: 60, timeoutMs: 120 });
|
|
4539
|
+
registry.register(new AIRulesAttackEngine(), { priority: 45, timeoutMs: 60 });
|
|
4540
|
+
registry.register(new TestQualityEngine(), { priority: 47, timeoutMs: 30 });
|
|
3411
4541
|
return registry;
|
|
3412
4542
|
}
|
|
3413
4543
|
function isTestFile2(uri) {
|
|
@@ -4103,6 +5233,14 @@ function literalKey(expr) {
|
|
|
4103
5233
|
if (import_typescript.default.isBigIntLiteral(e)) return `bigint:${e.text}`;
|
|
4104
5234
|
return null;
|
|
4105
5235
|
}
|
|
5236
|
+
var IMPL001_SKIP_LONG_SAME_STRING = 200;
|
|
5237
|
+
function isLongSameLiteralReturn(expr) {
|
|
5238
|
+
const e = unwrap(expr);
|
|
5239
|
+
if (import_typescript.default.isStringLiteral(e) || import_typescript.default.isNoSubstitutionTemplateLiteral(e)) {
|
|
5240
|
+
return e.text.length >= IMPL001_SKIP_LONG_SAME_STRING;
|
|
5241
|
+
}
|
|
5242
|
+
return false;
|
|
5243
|
+
}
|
|
4106
5244
|
function getFunctionName(fn) {
|
|
4107
5245
|
if (import_typescript.default.isFunctionDeclaration(fn) && fn.name) return fn.name.text;
|
|
4108
5246
|
if (import_typescript.default.isFunctionExpression(fn) && fn.name) return fn.name.text;
|
|
@@ -4129,6 +5267,7 @@ function jsdocSuggestsStableLiteral(node) {
|
|
|
4129
5267
|
);
|
|
4130
5268
|
}
|
|
4131
5269
|
var IMPL001_NAME_ALLOW = /^(noop|identity|constant|tojson|valueof|jsonstringify)$/i;
|
|
5270
|
+
var IMPL008_EMPTY_BODY_ALLOW_NAMES = /* @__PURE__ */ new Set(["deactivate"]);
|
|
4132
5271
|
function isBooleanPredicateName(name) {
|
|
4133
5272
|
if (!name) return false;
|
|
4134
5273
|
return /^(is|has|can|must|should|will|did)[A-Z]/.test(name) || /^(is|has|can|must|should)_[a-z]/i.test(name);
|
|
@@ -4350,6 +5489,7 @@ var IncompleteImplEngine = class extends BaseEngine {
|
|
|
4350
5489
|
if (jsdocSuggestsStableLiteral(fn)) return;
|
|
4351
5490
|
const rets = collectReturns(fn);
|
|
4352
5491
|
if (rets.length === 0) return;
|
|
5492
|
+
if (rets.some((r) => isLongSameLiteralReturn(r))) return;
|
|
4353
5493
|
const keys = rets.map(literalKey);
|
|
4354
5494
|
if (keys.some((k) => k === null)) return;
|
|
4355
5495
|
const first = keys[0];
|
|
@@ -4391,13 +5531,15 @@ var IncompleteImplEngine = class extends BaseEngine {
|
|
|
4391
5531
|
ruleIMPL008(fn, filePath, uri, findings, lines) {
|
|
4392
5532
|
if (isAbstractOrOverloadStub(fn)) return;
|
|
4393
5533
|
if (!functionHasEmptyBody(fn)) return;
|
|
5534
|
+
const fnName = getFunctionName(fn);
|
|
5535
|
+
if (fnName && IMPL008_EMPTY_BODY_ALLOW_NAMES.has(fnName)) return;
|
|
4394
5536
|
const pos = fn.getStart(fn.getSourceFile());
|
|
4395
5537
|
const { line, character } = fn.getSourceFile().getLineAndCharacterOfPosition(pos);
|
|
4396
5538
|
const lineNum = line + 1;
|
|
4397
5539
|
if (this.lineHasEmptyBodySuppression(lines, lineNum)) return;
|
|
4398
5540
|
const name = getFunctionName(fn);
|
|
4399
|
-
const
|
|
4400
|
-
const sharedLibPath = /(?:^|\/)packages\/[^/]+\/src\//i.test(
|
|
5541
|
+
const normUri = uri.replace(/\\/g, "/");
|
|
5542
|
+
const sharedLibPath = /(?:^|\/)packages\/[^/]+\/src\//i.test(normUri) || /(?:^|\/)lib(?:s)?\/src\//i.test(normUri);
|
|
4401
5543
|
const confidence = sharedLibPath ? 0.72 : 0.78;
|
|
4402
5544
|
findings.push(
|
|
4403
5545
|
this.createFinding({
|
|
@@ -5137,12 +6279,31 @@ function hasDetachedAnnotation(sf, node) {
|
|
|
5137
6279
|
}
|
|
5138
6280
|
return false;
|
|
5139
6281
|
}
|
|
6282
|
+
var _sfLinesCache = /* @__PURE__ */ new WeakMap();
|
|
6283
|
+
function getSourceLines(sf) {
|
|
6284
|
+
let lines = _sfLinesCache.get(sf);
|
|
6285
|
+
if (!lines) {
|
|
6286
|
+
lines = sf.getFullText().split("\n");
|
|
6287
|
+
_sfLinesCache.set(sf, lines);
|
|
6288
|
+
}
|
|
6289
|
+
return lines;
|
|
6290
|
+
}
|
|
6291
|
+
var IGNORE_REGEXES = /* @__PURE__ */ new Map();
|
|
6292
|
+
function getIgnoreRegex(rule) {
|
|
6293
|
+
let re = IGNORE_REGEXES.get(rule);
|
|
6294
|
+
if (!re) {
|
|
6295
|
+
re = new RegExp(`@vibecheck-ignore(?:-${rule})?\\b`, "i");
|
|
6296
|
+
IGNORE_REGEXES.set(rule, re);
|
|
6297
|
+
}
|
|
6298
|
+
return re;
|
|
6299
|
+
}
|
|
5140
6300
|
function ignoreCommentNear(sf, pos, rule) {
|
|
5141
6301
|
const lc = sf.getLineAndCharacterOfPosition(pos);
|
|
5142
|
-
const lines = sf
|
|
6302
|
+
const lines = getSourceLines(sf);
|
|
6303
|
+
const re = getIgnoreRegex(rule);
|
|
5143
6304
|
for (let L = Math.max(0, lc.line - 3); L <= lc.line; L++) {
|
|
5144
6305
|
const row = lines[L] ?? "";
|
|
5145
|
-
if (
|
|
6306
|
+
if (re.test(row)) return true;
|
|
5146
6307
|
}
|
|
5147
6308
|
return false;
|
|
5148
6309
|
}
|
|
@@ -5474,13 +6635,893 @@ var OutcomeVerificationEngine = class extends BaseEngine {
|
|
|
5474
6635
|
file: ctx.uri,
|
|
5475
6636
|
line,
|
|
5476
6637
|
column,
|
|
5477
|
-
evidence: ret.getText().slice(0, 100),
|
|
5478
|
-
suggestion: "Await the request, validate the outcome, then return based on the actual response (or return the promise).",
|
|
6638
|
+
evidence: ret.getText().slice(0, 100),
|
|
6639
|
+
suggestion: "Await the request, validate the outcome, then return based on the actual response (or return the promise).",
|
|
6640
|
+
autoFixable: false
|
|
6641
|
+
})
|
|
6642
|
+
);
|
|
6643
|
+
}
|
|
6644
|
+
};
|
|
6645
|
+
function isNpmManifest(uri) {
|
|
6646
|
+
const path24 = uri.replace(/^file:\/\//, "").toLowerCase();
|
|
6647
|
+
const base = path24.split("/").pop() ?? "";
|
|
6648
|
+
if (base !== "package.json") return false;
|
|
6649
|
+
return !/\/(node_modules|dist|build|\.next|\.turbo|out)\//.test(path24);
|
|
6650
|
+
}
|
|
6651
|
+
function isPythonManifest(uri) {
|
|
6652
|
+
const path24 = uri.replace(/^file:\/\//, "").toLowerCase();
|
|
6653
|
+
const base = path24.split("/").pop() ?? "";
|
|
6654
|
+
if (base === "requirements.txt" || base === "pipfile" || base === "pipfile.lock") return true;
|
|
6655
|
+
if (base === "pyproject.toml") return true;
|
|
6656
|
+
if (/^requirements[-.][\w.-]+\.txt$/.test(base)) return true;
|
|
6657
|
+
return false;
|
|
6658
|
+
}
|
|
6659
|
+
var CANONICAL_AI_PACKAGES_NPM = /* @__PURE__ */ new Set([
|
|
6660
|
+
// Anthropic
|
|
6661
|
+
"@anthropic-ai/sdk",
|
|
6662
|
+
"@anthropic-ai/claude-code",
|
|
6663
|
+
"@anthropic-ai/bedrock-sdk",
|
|
6664
|
+
"@anthropic-ai/vertex-sdk",
|
|
6665
|
+
"@anthropic-ai/tokenizer",
|
|
6666
|
+
// OpenAI
|
|
6667
|
+
"openai",
|
|
6668
|
+
// Google
|
|
6669
|
+
"@google/genai",
|
|
6670
|
+
"@google/generative-ai",
|
|
6671
|
+
"@google-ai/generativelanguage",
|
|
6672
|
+
"@google-cloud/vertexai",
|
|
6673
|
+
"@google-cloud/aiplatform",
|
|
6674
|
+
// LangChain
|
|
6675
|
+
"langchain",
|
|
6676
|
+
"@langchain/core",
|
|
6677
|
+
"@langchain/anthropic",
|
|
6678
|
+
"@langchain/openai",
|
|
6679
|
+
"@langchain/google-genai",
|
|
6680
|
+
"@langchain/community",
|
|
6681
|
+
// MCP
|
|
6682
|
+
"@modelcontextprotocol/sdk",
|
|
6683
|
+
"@modelcontextprotocol/server-everything",
|
|
6684
|
+
"@modelcontextprotocol/server-filesystem",
|
|
6685
|
+
"@modelcontextprotocol/server-github",
|
|
6686
|
+
"@modelcontextprotocol/server-gitlab",
|
|
6687
|
+
"@modelcontextprotocol/server-memory",
|
|
6688
|
+
"@modelcontextprotocol/server-postgres",
|
|
6689
|
+
"@modelcontextprotocol/server-puppeteer",
|
|
6690
|
+
"@modelcontextprotocol/server-sequentialthinking",
|
|
6691
|
+
"@modelcontextprotocol/server-slack",
|
|
6692
|
+
"@modelcontextprotocol/server-time",
|
|
6693
|
+
"@modelcontextprotocol/inspector",
|
|
6694
|
+
// Vercel AI
|
|
6695
|
+
"ai",
|
|
6696
|
+
"@ai-sdk/anthropic",
|
|
6697
|
+
"@ai-sdk/openai",
|
|
6698
|
+
"@ai-sdk/google",
|
|
6699
|
+
"@ai-sdk/react",
|
|
6700
|
+
// Cohere
|
|
6701
|
+
"cohere-ai",
|
|
6702
|
+
// Mistral
|
|
6703
|
+
"@mistralai/mistralai"
|
|
6704
|
+
]);
|
|
6705
|
+
var CANONICAL_AI_PACKAGES_PYPI = /* @__PURE__ */ new Set([
|
|
6706
|
+
"anthropic",
|
|
6707
|
+
"openai",
|
|
6708
|
+
"google-generativeai",
|
|
6709
|
+
"google-cloud-aiplatform",
|
|
6710
|
+
"langchain",
|
|
6711
|
+
"langchain-core",
|
|
6712
|
+
"langchain-anthropic",
|
|
6713
|
+
"langchain-openai",
|
|
6714
|
+
"langchain-google-genai",
|
|
6715
|
+
"langchain-community",
|
|
6716
|
+
"mcp",
|
|
6717
|
+
"fastmcp",
|
|
6718
|
+
"mcp-server-fetch",
|
|
6719
|
+
"mcp-server-git",
|
|
6720
|
+
"mcp-server-time",
|
|
6721
|
+
"mistralai",
|
|
6722
|
+
"cohere",
|
|
6723
|
+
"tiktoken"
|
|
6724
|
+
]);
|
|
6725
|
+
var BRAND_KEYWORDS = [
|
|
6726
|
+
"claude",
|
|
6727
|
+
"anthropic",
|
|
6728
|
+
"openai",
|
|
6729
|
+
"chatgpt",
|
|
6730
|
+
"gpt-4",
|
|
6731
|
+
"gpt4",
|
|
6732
|
+
"gemini",
|
|
6733
|
+
"modelcontextprotocol"
|
|
6734
|
+
];
|
|
6735
|
+
var AI_HALLUCINATED_PACKAGES = /* @__PURE__ */ new Set([
|
|
6736
|
+
// npm — Anthropic / Claude
|
|
6737
|
+
"@anthropic/sdk",
|
|
6738
|
+
"@anthropic/claude",
|
|
6739
|
+
"@anthropic/claude-sdk",
|
|
6740
|
+
"@anthropic/api",
|
|
6741
|
+
"claude-sdk",
|
|
6742
|
+
"claude-api",
|
|
6743
|
+
"claude-ai",
|
|
6744
|
+
"anthropic-sdk",
|
|
6745
|
+
"anthropic-api",
|
|
6746
|
+
"anthropic-client",
|
|
6747
|
+
"anthropic-typescript",
|
|
6748
|
+
"claude-typescript",
|
|
6749
|
+
"claude-node",
|
|
6750
|
+
// npm — OpenAI
|
|
6751
|
+
"@openai/sdk",
|
|
6752
|
+
"@openai/api",
|
|
6753
|
+
"@openai/node",
|
|
6754
|
+
"openai-sdk",
|
|
6755
|
+
"openai-api",
|
|
6756
|
+
"openai-client",
|
|
6757
|
+
"openai-node-sdk",
|
|
6758
|
+
"gpt-sdk",
|
|
6759
|
+
"chatgpt-sdk",
|
|
6760
|
+
// npm — MCP
|
|
6761
|
+
"@mcp/sdk",
|
|
6762
|
+
"@mcp/server",
|
|
6763
|
+
"@mcp/client",
|
|
6764
|
+
"mcp-sdk",
|
|
6765
|
+
"mcp-protocol",
|
|
6766
|
+
"mcp-server-sdk",
|
|
6767
|
+
"modelcontext-sdk",
|
|
6768
|
+
// npm — Gemini / Google
|
|
6769
|
+
"@google/gemini",
|
|
6770
|
+
"gemini-sdk",
|
|
6771
|
+
"gemini-api",
|
|
6772
|
+
"google-gemini-sdk",
|
|
6773
|
+
// PyPI
|
|
6774
|
+
"claude-python",
|
|
6775
|
+
"anthropic-python",
|
|
6776
|
+
"anthropic-claude",
|
|
6777
|
+
"claude-ai-sdk",
|
|
6778
|
+
"openai-python-sdk",
|
|
6779
|
+
"gpt-python",
|
|
6780
|
+
"mcp-sdk-python",
|
|
6781
|
+
"modelcontextprotocol",
|
|
6782
|
+
"gemini-python"
|
|
6783
|
+
]);
|
|
6784
|
+
var WILDCARD_VERSION_RE = /^(?:\*|latest|x|>=?\s*0(?:\.0)?(?:\.0)?)$/i;
|
|
6785
|
+
var UNPINNED_GIT_URL_RE = /^git(?:\+(?:https?|ssh))?:\/\/[^#\s]+(?:\.git)?(?:#(?!(?:[a-f0-9]{7,40}\b|semver:|v?\d+\.\d+\.\d+)).*)?$/i;
|
|
6786
|
+
var HTTP_TARBALL_RE = /^http:\/\//i;
|
|
6787
|
+
var PYPI_LINE_RE = /^\s*([A-Za-z][A-Za-z0-9._-]*)\s*(?:\[[^\]]+\])?\s*(?:([<>=!~]=?|===)\s*([\w.+!*-]+))?\s*(?:;.*)?$/;
|
|
6788
|
+
function detectBrandImpersonationNpm(pkg) {
|
|
6789
|
+
const lower = pkg.toLowerCase();
|
|
6790
|
+
if (CANONICAL_AI_PACKAGES_NPM.has(lower)) return null;
|
|
6791
|
+
for (const brand of BRAND_KEYWORDS) {
|
|
6792
|
+
if (lower.includes(brand)) {
|
|
6793
|
+
if (lower.startsWith("@anthropic-ai/")) return null;
|
|
6794
|
+
if (lower.startsWith("@openai/")) return null;
|
|
6795
|
+
if (lower.startsWith("@modelcontextprotocol/")) return null;
|
|
6796
|
+
if (lower.startsWith("@langchain/")) return null;
|
|
6797
|
+
if (lower.startsWith("@ai-sdk/")) return null;
|
|
6798
|
+
if (lower.startsWith("@google-ai/") || lower.startsWith("@google-cloud/")) return null;
|
|
6799
|
+
return brand;
|
|
6800
|
+
}
|
|
6801
|
+
}
|
|
6802
|
+
return null;
|
|
6803
|
+
}
|
|
6804
|
+
function detectBrandImpersonationPypi(pkg) {
|
|
6805
|
+
const lower = pkg.toLowerCase().replace(/_/g, "-");
|
|
6806
|
+
if (CANONICAL_AI_PACKAGES_PYPI.has(lower)) return null;
|
|
6807
|
+
if (lower.startsWith("langchain-")) return null;
|
|
6808
|
+
for (const brand of BRAND_KEYWORDS) {
|
|
6809
|
+
if (lower.includes(brand)) return brand;
|
|
6810
|
+
}
|
|
6811
|
+
return null;
|
|
6812
|
+
}
|
|
6813
|
+
function detectMCPSlopsquatNpm(pkg) {
|
|
6814
|
+
const lower = pkg.toLowerCase();
|
|
6815
|
+
if (CANONICAL_AI_PACKAGES_NPM.has(lower)) return false;
|
|
6816
|
+
if (lower.startsWith("@modelcontextprotocol/")) return false;
|
|
6817
|
+
return /^mcp-server-|^@[\w-]+\/mcp-server-|^mcp-/.test(lower) || /-mcp-server$|-mcp$/.test(lower);
|
|
6818
|
+
}
|
|
6819
|
+
function detectMCPSlopsquatPypi(pkg) {
|
|
6820
|
+
const lower = pkg.toLowerCase().replace(/_/g, "-");
|
|
6821
|
+
if (CANONICAL_AI_PACKAGES_PYPI.has(lower)) return false;
|
|
6822
|
+
return /^mcp-server-|^mcp-/.test(lower);
|
|
6823
|
+
}
|
|
6824
|
+
var DEP_KEYS = [
|
|
6825
|
+
"dependencies",
|
|
6826
|
+
"devDependencies",
|
|
6827
|
+
"peerDependencies",
|
|
6828
|
+
"optionalDependencies"
|
|
6829
|
+
];
|
|
6830
|
+
var SlopsquatEngine = class extends BaseEngine {
|
|
6831
|
+
id = "slopsquat";
|
|
6832
|
+
name = "AI Slopsquat & Dependency Hygiene Engine";
|
|
6833
|
+
version = "1.0.0";
|
|
6834
|
+
supportedExtensions = null;
|
|
6835
|
+
async scan(delta, signal) {
|
|
6836
|
+
const uri = delta.documentUri;
|
|
6837
|
+
const isNpm = isNpmManifest(uri);
|
|
6838
|
+
const isPy = isPythonManifest(uri);
|
|
6839
|
+
if (!isNpm && !isPy) return [];
|
|
6840
|
+
this.checkAbort(signal);
|
|
6841
|
+
const source = delta.fullText;
|
|
6842
|
+
const cleanUri = uri.replace(/^file:\/\//, "");
|
|
6843
|
+
const pending = [];
|
|
6844
|
+
if (isNpm) {
|
|
6845
|
+
this._checkNpmManifest(source, pending);
|
|
6846
|
+
} else {
|
|
6847
|
+
this._checkPythonManifest(source, pending);
|
|
6848
|
+
}
|
|
6849
|
+
if (pending.length === 0) return [];
|
|
6850
|
+
return pending.map(
|
|
6851
|
+
(p) => this.createFinding({
|
|
6852
|
+
id: this.deterministicId(cleanUri, p.line, p.column, p.ruleId, p.evidence.slice(0, 32)),
|
|
6853
|
+
ruleId: p.ruleId,
|
|
6854
|
+
category: "slopsquat",
|
|
6855
|
+
message: p.message,
|
|
6856
|
+
severity: p.severity,
|
|
6857
|
+
confidence: p.confidence,
|
|
6858
|
+
file: cleanUri,
|
|
6859
|
+
line: p.line,
|
|
6860
|
+
column: p.column,
|
|
6861
|
+
evidence: p.evidence,
|
|
6862
|
+
suggestion: p.suggestion,
|
|
6863
|
+
autoFixable: false
|
|
6864
|
+
})
|
|
6865
|
+
);
|
|
6866
|
+
}
|
|
6867
|
+
// ─── npm ────────────────────────────────────────────────────────────────
|
|
6868
|
+
_checkNpmManifest(source, out) {
|
|
6869
|
+
let doc;
|
|
6870
|
+
try {
|
|
6871
|
+
doc = JSON.parse(source);
|
|
6872
|
+
} catch {
|
|
6873
|
+
return;
|
|
6874
|
+
}
|
|
6875
|
+
for (const depKey of DEP_KEYS) {
|
|
6876
|
+
const deps = doc[depKey];
|
|
6877
|
+
if (!deps || typeof deps !== "object") continue;
|
|
6878
|
+
for (const [pkg, rawVersion] of Object.entries(deps)) {
|
|
6879
|
+
if (typeof rawVersion !== "string") continue;
|
|
6880
|
+
const version = rawVersion.trim();
|
|
6881
|
+
const line = this._findLine(source, `"${pkg}"`);
|
|
6882
|
+
if (WILDCARD_VERSION_RE.test(version)) {
|
|
6883
|
+
out.push({
|
|
6884
|
+
ruleId: "SLOP001",
|
|
6885
|
+
severity: "high",
|
|
6886
|
+
confidence: 0.95,
|
|
6887
|
+
message: `Wildcard version for '${pkg}' \u2014 accepts any future release including malicious ones`,
|
|
6888
|
+
evidence: `"${pkg}": "${version}"`,
|
|
6889
|
+
suggestion: `Pin to a specific version: "${pkg}": "^X.Y.Z" (or exact "X.Y.Z").`,
|
|
6890
|
+
line,
|
|
6891
|
+
column: 0
|
|
6892
|
+
});
|
|
6893
|
+
}
|
|
6894
|
+
if (UNPINNED_GIT_URL_RE.test(version)) {
|
|
6895
|
+
out.push({
|
|
6896
|
+
ruleId: "SLOP006",
|
|
6897
|
+
severity: "high",
|
|
6898
|
+
confidence: 0.9,
|
|
6899
|
+
message: `Git dependency '${pkg}' is not pinned to a commit`,
|
|
6900
|
+
evidence: `"${pkg}": "${version}"`,
|
|
6901
|
+
suggestion: `Pin to a full commit SHA: "${pkg}": "git+https://...#<full-sha>". A branch reference can change under you.`,
|
|
6902
|
+
line,
|
|
6903
|
+
column: 0
|
|
6904
|
+
});
|
|
6905
|
+
}
|
|
6906
|
+
if (HTTP_TARBALL_RE.test(version)) {
|
|
6907
|
+
out.push({
|
|
6908
|
+
ruleId: "SLOP007",
|
|
6909
|
+
severity: "critical",
|
|
6910
|
+
confidence: 0.99,
|
|
6911
|
+
message: `Dependency '${pkg}' installs over plaintext HTTP \u2014 MITM exposure`,
|
|
6912
|
+
evidence: `"${pkg}": "${version}"`,
|
|
6913
|
+
suggestion: "Use https:// for tarball URLs.",
|
|
6914
|
+
line,
|
|
6915
|
+
column: 0
|
|
6916
|
+
});
|
|
6917
|
+
}
|
|
6918
|
+
if (AI_HALLUCINATED_PACKAGES.has(pkg.toLowerCase())) {
|
|
6919
|
+
out.push({
|
|
6920
|
+
ruleId: "SLOP004",
|
|
6921
|
+
severity: "critical",
|
|
6922
|
+
confidence: 0.99,
|
|
6923
|
+
message: `'${pkg}' is a known AI-hallucinated package name \u2014 does not exist in the official ecosystem`,
|
|
6924
|
+
evidence: `"${pkg}"`,
|
|
6925
|
+
suggestion: this._fixHintForHallucinated(pkg),
|
|
6926
|
+
line,
|
|
6927
|
+
column: 0
|
|
6928
|
+
});
|
|
6929
|
+
continue;
|
|
6930
|
+
}
|
|
6931
|
+
const brand = detectBrandImpersonationNpm(pkg);
|
|
6932
|
+
if (brand) {
|
|
6933
|
+
out.push({
|
|
6934
|
+
ruleId: "SLOP002",
|
|
6935
|
+
severity: "critical",
|
|
6936
|
+
confidence: 0.9,
|
|
6937
|
+
message: `'${pkg}' uses the '${brand}' brand but is not from the official organization`,
|
|
6938
|
+
evidence: `"${pkg}"`,
|
|
6939
|
+
suggestion: this._fixHintForBrand(brand),
|
|
6940
|
+
line,
|
|
6941
|
+
column: 0
|
|
6942
|
+
});
|
|
6943
|
+
}
|
|
6944
|
+
if (detectMCPSlopsquatNpm(pkg)) {
|
|
6945
|
+
out.push({
|
|
6946
|
+
ruleId: "SLOP003",
|
|
6947
|
+
severity: "high",
|
|
6948
|
+
confidence: 0.85,
|
|
6949
|
+
message: `'${pkg}' claims to be an MCP server but is not in the @modelcontextprotocol/ scope`,
|
|
6950
|
+
evidence: `"${pkg}"`,
|
|
6951
|
+
suggestion: "Verify the package author. Official MCP servers live under @modelcontextprotocol/. Vetted third-party MCP servers should be added to an explicit allowlist.",
|
|
6952
|
+
line,
|
|
6953
|
+
column: 0
|
|
6954
|
+
});
|
|
6955
|
+
}
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
// ─── PyPI ───────────────────────────────────────────────────────────────
|
|
6960
|
+
_checkPythonManifest(source, out) {
|
|
6961
|
+
const lines = source.split("\n");
|
|
6962
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6963
|
+
const raw = lines[i];
|
|
6964
|
+
const line = i + 1;
|
|
6965
|
+
const trimmed = raw.replace(/(^|[^\\])#.*$/, "$1").trim();
|
|
6966
|
+
if (!trimmed || trimmed.startsWith("-")) continue;
|
|
6967
|
+
const m = trimmed.match(PYPI_LINE_RE);
|
|
6968
|
+
if (!m) continue;
|
|
6969
|
+
const [, pkgRaw, op, version] = m;
|
|
6970
|
+
if (!pkgRaw) continue;
|
|
6971
|
+
const pkg = pkgRaw.toLowerCase();
|
|
6972
|
+
if (!op || version && (version === "*" || version.startsWith("0"))) {
|
|
6973
|
+
if (!op || version === "*") {
|
|
6974
|
+
out.push({
|
|
6975
|
+
ruleId: "SLOP001",
|
|
6976
|
+
severity: "medium",
|
|
6977
|
+
confidence: 0.85,
|
|
6978
|
+
message: `Unpinned PyPI dependency '${pkg}' \u2014 accepts any version`,
|
|
6979
|
+
evidence: trimmed.slice(0, 80),
|
|
6980
|
+
suggestion: `Pin to a specific version: ${pkg}==X.Y.Z (or use ~= for compatible release).`,
|
|
6981
|
+
line,
|
|
6982
|
+
column: 0
|
|
6983
|
+
});
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
if (AI_HALLUCINATED_PACKAGES.has(pkg)) {
|
|
6987
|
+
out.push({
|
|
6988
|
+
ruleId: "SLOP004",
|
|
6989
|
+
severity: "critical",
|
|
6990
|
+
confidence: 0.99,
|
|
6991
|
+
message: `'${pkg}' is a known AI-hallucinated PyPI package name \u2014 does not exist in the official ecosystem`,
|
|
6992
|
+
evidence: trimmed.slice(0, 80),
|
|
6993
|
+
suggestion: this._fixHintForHallucinated(pkg),
|
|
6994
|
+
line,
|
|
6995
|
+
column: 0
|
|
6996
|
+
});
|
|
6997
|
+
continue;
|
|
6998
|
+
}
|
|
6999
|
+
const brand = detectBrandImpersonationPypi(pkg);
|
|
7000
|
+
if (brand) {
|
|
7001
|
+
out.push({
|
|
7002
|
+
ruleId: "SLOP005",
|
|
7003
|
+
severity: "critical",
|
|
7004
|
+
confidence: 0.9,
|
|
7005
|
+
message: `'${pkg}' uses the '${brand}' brand on PyPI but is not the canonical package`,
|
|
7006
|
+
evidence: trimmed.slice(0, 80),
|
|
7007
|
+
suggestion: this._fixHintForBrand(brand),
|
|
7008
|
+
line,
|
|
7009
|
+
column: 0
|
|
7010
|
+
});
|
|
7011
|
+
}
|
|
7012
|
+
if (detectMCPSlopsquatPypi(pkg)) {
|
|
7013
|
+
out.push({
|
|
7014
|
+
ruleId: "SLOP003",
|
|
7015
|
+
severity: "high",
|
|
7016
|
+
confidence: 0.8,
|
|
7017
|
+
message: `'${pkg}' claims to be an MCP server on PyPI \u2014 verify the author`,
|
|
7018
|
+
evidence: trimmed.slice(0, 80),
|
|
7019
|
+
suggestion: "Official MCP packages on PyPI start with `mcp-server-` (e.g. mcp-server-fetch, mcp-server-git). Confirm the package author and source repository before installing.",
|
|
7020
|
+
line,
|
|
7021
|
+
column: 0
|
|
7022
|
+
});
|
|
7023
|
+
}
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
7027
|
+
_findLine(source, needle) {
|
|
7028
|
+
const idx = source.indexOf(needle);
|
|
7029
|
+
if (idx === -1) return 1;
|
|
7030
|
+
let line = 1;
|
|
7031
|
+
for (let i = 0; i < idx; i++) {
|
|
7032
|
+
if (source.charCodeAt(i) === 10) line++;
|
|
7033
|
+
}
|
|
7034
|
+
return line;
|
|
7035
|
+
}
|
|
7036
|
+
_fixHintForHallucinated(pkg) {
|
|
7037
|
+
const lower = pkg.toLowerCase();
|
|
7038
|
+
if (lower.includes("claude") || lower.includes("anthropic")) {
|
|
7039
|
+
return "Use the official package: @anthropic-ai/sdk (npm) or anthropic (PyPI).";
|
|
7040
|
+
}
|
|
7041
|
+
if (lower.includes("openai") || lower.includes("gpt") || lower.includes("chatgpt")) {
|
|
7042
|
+
return "Use the official package: openai (npm and PyPI).";
|
|
7043
|
+
}
|
|
7044
|
+
if (lower.includes("mcp") || lower.includes("modelcontext")) {
|
|
7045
|
+
return "Use the official MCP SDK: @modelcontextprotocol/sdk (npm) or mcp (PyPI).";
|
|
7046
|
+
}
|
|
7047
|
+
if (lower.includes("gemini") || lower.includes("google")) {
|
|
7048
|
+
return "Use the official package: @google/genai (npm) or google-generativeai (PyPI).";
|
|
7049
|
+
}
|
|
7050
|
+
return "Search npm/PyPI for the actual canonical package name. AI assistants frequently invent plausible-looking package names that do not exist.";
|
|
7051
|
+
}
|
|
7052
|
+
_fixHintForBrand(brand) {
|
|
7053
|
+
switch (brand) {
|
|
7054
|
+
case "claude":
|
|
7055
|
+
case "anthropic":
|
|
7056
|
+
return "Use the official @anthropic-ai/sdk (npm) or anthropic (PyPI). Anything else with the Anthropic/Claude brand in its name is likely impersonation.";
|
|
7057
|
+
case "openai":
|
|
7058
|
+
case "chatgpt":
|
|
7059
|
+
case "gpt-4":
|
|
7060
|
+
case "gpt4":
|
|
7061
|
+
return "Use the official openai (npm and PyPI). The OpenAI org does not publish other branded packages.";
|
|
7062
|
+
case "gemini":
|
|
7063
|
+
return "Use the official @google/genai (npm) or google-generativeai (PyPI). Google does not publish gemini-* packages from third-party scopes.";
|
|
7064
|
+
case "modelcontextprotocol":
|
|
7065
|
+
return "Official MCP packages are scoped under @modelcontextprotocol/ (npm) or named mcp / mcp-server-* (PyPI). Third-party MCP packages should be vetted against an explicit allowlist.";
|
|
7066
|
+
default:
|
|
7067
|
+
return `Verify '${brand}' is the official organization on the registry. AI brand impersonation packages are a common supply-chain attack.`;
|
|
7068
|
+
}
|
|
7069
|
+
}
|
|
7070
|
+
};
|
|
7071
|
+
var INITIAL_VERSION = "1.0.0";
|
|
7072
|
+
var baseLifecycle = {
|
|
7073
|
+
scan: "src/scan.ts"
|
|
7074
|
+
// engines export their scan via class.scan(); pointer is a stub for the manifest schema
|
|
7075
|
+
};
|
|
7076
|
+
var slopsquatManifest = {
|
|
7077
|
+
id: "slopsquat",
|
|
7078
|
+
version: INITIAL_VERSION,
|
|
7079
|
+
description: "Detects typo-squatted package names \u2014 imports that look real but are a known typo of a popular package.",
|
|
7080
|
+
capabilities: ["scan_files", "hallucination"],
|
|
7081
|
+
lifecycle: baseLifecycle,
|
|
7082
|
+
telemetry: {
|
|
7083
|
+
events: ["scan_started", "scan_completed", "slopsquat.match"]
|
|
7084
|
+
}
|
|
7085
|
+
};
|
|
7086
|
+
var fakeFeaturesManifest = {
|
|
7087
|
+
id: "fake_features",
|
|
7088
|
+
version: INITIAL_VERSION,
|
|
7089
|
+
description: "Detects code that simulates a feature without implementing it \u2014 placeholder UI bound to no backend, fake success returns, mock toggles in production.",
|
|
7090
|
+
capabilities: ["scan_files", "hallucination", "quality"],
|
|
7091
|
+
lifecycle: baseLifecycle,
|
|
7092
|
+
telemetry: {
|
|
7093
|
+
events: ["scan_started", "scan_completed", "fake_features.match"]
|
|
7094
|
+
}
|
|
7095
|
+
};
|
|
7096
|
+
var incompleteImplManifest = {
|
|
7097
|
+
id: "incomplete_impl",
|
|
7098
|
+
version: INITIAL_VERSION,
|
|
7099
|
+
description: "Detects partial implementations \u2014 exported functions never called, TODO throws, stub bodies, half-wired routes.",
|
|
7100
|
+
capabilities: ["scan_files", "hallucination", "quality"],
|
|
7101
|
+
lifecycle: baseLifecycle,
|
|
7102
|
+
telemetry: {
|
|
7103
|
+
events: ["scan_started", "scan_completed", "incomplete_impl.match"]
|
|
7104
|
+
}
|
|
7105
|
+
};
|
|
7106
|
+
var aiRulesAttackManifest = {
|
|
7107
|
+
id: "ai-rules-attack",
|
|
7108
|
+
version: INITIAL_VERSION,
|
|
7109
|
+
description: "Detects prompt-injection style patterns embedded in code or rules files that try to manipulate AI assistants downstream.",
|
|
7110
|
+
capabilities: ["scan_files", "hallucination", "security"],
|
|
7111
|
+
lifecycle: baseLifecycle,
|
|
7112
|
+
telemetry: {
|
|
7113
|
+
events: ["scan_started", "scan_completed", "ai_rules_attack.match"]
|
|
7114
|
+
}
|
|
7115
|
+
};
|
|
7116
|
+
var securityPatternManifest = {
|
|
7117
|
+
id: "security-pattern",
|
|
7118
|
+
version: INITIAL_VERSION,
|
|
7119
|
+
description: "Detects classic security antipatterns \u2014 eval on user input, weak crypto, unsafe URL construction, missing CSRF, etc.",
|
|
7120
|
+
capabilities: ["scan_files", "security"],
|
|
7121
|
+
lifecycle: baseLifecycle,
|
|
7122
|
+
telemetry: {
|
|
7123
|
+
events: ["scan_started", "scan_completed", "security_pattern.match"]
|
|
7124
|
+
}
|
|
7125
|
+
};
|
|
7126
|
+
var typeContractManifest = {
|
|
7127
|
+
id: "type-contract",
|
|
7128
|
+
version: INITIAL_VERSION,
|
|
7129
|
+
description: "Detects type-contract violations across module boundaries \u2014 return shape mismatches, missing required props, drift between consumer and producer.",
|
|
7130
|
+
capabilities: ["scan_files", "type_safety", "quality"],
|
|
7131
|
+
lifecycle: baseLifecycle,
|
|
7132
|
+
telemetry: {
|
|
7133
|
+
events: ["scan_started", "scan_completed", "type_contract.match"]
|
|
7134
|
+
}
|
|
7135
|
+
};
|
|
7136
|
+
var perfAntipatternManifest = {
|
|
7137
|
+
id: "perf-antipattern",
|
|
7138
|
+
version: INITIAL_VERSION,
|
|
7139
|
+
description: "Detects performance antipatterns \u2014 N+1 queries, sync I/O on hot paths, unbounded loops, accidental quadratic.",
|
|
7140
|
+
capabilities: ["scan_files", "quality"],
|
|
7141
|
+
lifecycle: baseLifecycle,
|
|
7142
|
+
telemetry: {
|
|
7143
|
+
events: ["scan_started", "scan_completed", "perf_antipattern.match"]
|
|
7144
|
+
}
|
|
7145
|
+
};
|
|
7146
|
+
var errorHandlingManifest = {
|
|
7147
|
+
id: "error_handling",
|
|
7148
|
+
version: INITIAL_VERSION,
|
|
7149
|
+
description: "Detects missing or swallowed error handling \u2014 empty catch, unawaited rejections, success-on-error, lost stack traces.",
|
|
7150
|
+
capabilities: ["scan_files", "quality"],
|
|
7151
|
+
lifecycle: baseLifecycle,
|
|
7152
|
+
telemetry: {
|
|
7153
|
+
events: ["scan_started", "scan_completed", "error_handling.match"]
|
|
7154
|
+
}
|
|
7155
|
+
};
|
|
7156
|
+
var logicGapManifest = {
|
|
7157
|
+
id: "logic_gap",
|
|
7158
|
+
version: INITIAL_VERSION,
|
|
7159
|
+
description: "Detects logical gaps \u2014 branches that never execute, conditions that always evaluate the same way, dead state, contradictory guards.",
|
|
7160
|
+
capabilities: ["scan_files", "quality"],
|
|
7161
|
+
lifecycle: baseLifecycle,
|
|
7162
|
+
telemetry: {
|
|
7163
|
+
events: ["scan_started", "scan_completed", "logic_gap.match"]
|
|
7164
|
+
}
|
|
7165
|
+
};
|
|
7166
|
+
var testQualityManifest = {
|
|
7167
|
+
id: "test-quality",
|
|
7168
|
+
version: INITIAL_VERSION,
|
|
7169
|
+
description: "Detects tests that pass without exercising real behavior \u2014 bare mocks, no assertions, expect-anything, snapshot rot.",
|
|
7170
|
+
capabilities: ["scan_files", "quality"],
|
|
7171
|
+
lifecycle: baseLifecycle,
|
|
7172
|
+
telemetry: {
|
|
7173
|
+
events: ["scan_started", "scan_completed", "test_quality.match"]
|
|
7174
|
+
}
|
|
7175
|
+
};
|
|
7176
|
+
var outcomeVerificationManifest = {
|
|
7177
|
+
id: "outcome_verification",
|
|
7178
|
+
version: INITIAL_VERSION,
|
|
7179
|
+
description: "Verifies that observable outcomes match declared intent \u2014 function signatures vs. callers, return shapes vs. consumers.",
|
|
7180
|
+
capabilities: ["scan_files", "quality"],
|
|
7181
|
+
lifecycle: baseLifecycle,
|
|
7182
|
+
telemetry: {
|
|
7183
|
+
events: ["scan_started", "scan_completed", "outcome_verification.match"]
|
|
7184
|
+
}
|
|
7185
|
+
};
|
|
7186
|
+
var CORE_ENGINE_MANIFESTS = [
|
|
7187
|
+
// Hallucination
|
|
7188
|
+
slopsquatManifest,
|
|
7189
|
+
fakeFeaturesManifest,
|
|
7190
|
+
incompleteImplManifest,
|
|
7191
|
+
aiRulesAttackManifest,
|
|
7192
|
+
// Security
|
|
7193
|
+
securityPatternManifest,
|
|
7194
|
+
// Type
|
|
7195
|
+
typeContractManifest,
|
|
7196
|
+
// Quality
|
|
7197
|
+
perfAntipatternManifest,
|
|
7198
|
+
errorHandlingManifest,
|
|
7199
|
+
logicGapManifest,
|
|
7200
|
+
testQualityManifest,
|
|
7201
|
+
outcomeVerificationManifest
|
|
7202
|
+
];
|
|
7203
|
+
var phantomDepManifest = {
|
|
7204
|
+
id: "phantom_dep",
|
|
7205
|
+
version: "1.0.0",
|
|
7206
|
+
description: "Detects hallucinated package imports \u2014 modules that do not exist in package.json or node_modules. The most undeniable AI-generated-code failure class.",
|
|
7207
|
+
capabilities: ["scan_files", "hallucination", "incremental"],
|
|
7208
|
+
lifecycle: baseLifecycle,
|
|
7209
|
+
telemetry: {
|
|
7210
|
+
events: ["scan_started", "scan_completed", "phantom_dep.match"]
|
|
7211
|
+
}
|
|
7212
|
+
};
|
|
7213
|
+
var ghostRouteManifest = {
|
|
7214
|
+
id: "ghost_route",
|
|
7215
|
+
version: "1.0.0",
|
|
7216
|
+
description: 'Detects API calls and links pointing at routes that do not exist in the workspace. The "this UI is wired to nothing" hallucination class.',
|
|
7217
|
+
capabilities: ["scan_files", "hallucination"],
|
|
7218
|
+
lifecycle: baseLifecycle,
|
|
7219
|
+
telemetry: {
|
|
7220
|
+
events: ["scan_started", "scan_completed", "ghost_route.match"]
|
|
7221
|
+
}
|
|
7222
|
+
};
|
|
7223
|
+
var WORKSPACE_ENGINE_MANIFESTS = [
|
|
7224
|
+
phantomDepManifest,
|
|
7225
|
+
ghostRouteManifest
|
|
7226
|
+
];
|
|
7227
|
+
function registerCoreEngines(registry) {
|
|
7228
|
+
registry.register(aiRulesAttackManifest, new AIRulesAttackEngine());
|
|
7229
|
+
registry.register(errorHandlingManifest, new ErrorHandlingEngine());
|
|
7230
|
+
registry.register(fakeFeaturesManifest, new FakeFeaturesEngine());
|
|
7231
|
+
registry.register(incompleteImplManifest, new IncompleteImplEngine());
|
|
7232
|
+
registry.register(logicGapManifest, new LogicGapEngine());
|
|
7233
|
+
registry.register(outcomeVerificationManifest, new OutcomeVerificationEngine());
|
|
7234
|
+
registry.register(perfAntipatternManifest, new PerformanceAntipatternEngine());
|
|
7235
|
+
registry.register(securityPatternManifest, new SecurityPatternEngine());
|
|
7236
|
+
registry.register(slopsquatManifest, new SlopsquatEngine());
|
|
7237
|
+
registry.register(testQualityManifest, new TestQualityEngine());
|
|
7238
|
+
registry.register(typeContractManifest, new TypeContractEngine());
|
|
7239
|
+
}
|
|
7240
|
+
var CORE_ENGINE_IDS = [
|
|
7241
|
+
aiRulesAttackManifest.id,
|
|
7242
|
+
errorHandlingManifest.id,
|
|
7243
|
+
fakeFeaturesManifest.id,
|
|
7244
|
+
incompleteImplManifest.id,
|
|
7245
|
+
logicGapManifest.id,
|
|
7246
|
+
outcomeVerificationManifest.id,
|
|
7247
|
+
perfAntipatternManifest.id,
|
|
7248
|
+
securityPatternManifest.id,
|
|
7249
|
+
slopsquatManifest.id,
|
|
7250
|
+
testQualityManifest.id,
|
|
7251
|
+
typeContractManifest.id
|
|
7252
|
+
];
|
|
7253
|
+
function registerWorkspaceEngines(registry, options) {
|
|
7254
|
+
registry.register(
|
|
7255
|
+
phantomDepManifest,
|
|
7256
|
+
new PhantomDepEngine(options.workspaceRoot)
|
|
7257
|
+
);
|
|
7258
|
+
registry.register(
|
|
7259
|
+
ghostRouteManifest,
|
|
7260
|
+
new GhostRouteEngine(
|
|
7261
|
+
options.workspaceRoot,
|
|
7262
|
+
options.ghostRouteConfidenceThreshold ?? 0.75,
|
|
7263
|
+
options.ghostRouteSafePrefixes ?? []
|
|
7264
|
+
)
|
|
7265
|
+
);
|
|
7266
|
+
}
|
|
7267
|
+
var WORKSPACE_ENGINE_IDS = [
|
|
7268
|
+
phantomDepManifest.id,
|
|
7269
|
+
ghostRouteManifest.id
|
|
7270
|
+
];
|
|
7271
|
+
function registerAllEngines(registry, options) {
|
|
7272
|
+
registerCoreEngines(registry);
|
|
7273
|
+
registerWorkspaceEngines(registry, options);
|
|
7274
|
+
}
|
|
7275
|
+
async function tryLoadNativeApi() {
|
|
7276
|
+
try {
|
|
7277
|
+
return await import('@vibecheck/native');
|
|
7278
|
+
} catch {
|
|
7279
|
+
return null;
|
|
7280
|
+
}
|
|
7281
|
+
}
|
|
7282
|
+
var VibecheckNativeEngine = class extends BaseEngine {
|
|
7283
|
+
id = "vibecheck-native";
|
|
7284
|
+
name = "Vibecheck Native Engine";
|
|
7285
|
+
version = "2.0.0";
|
|
7286
|
+
supportedExtensions = /* @__PURE__ */ new Set([
|
|
7287
|
+
".ts",
|
|
7288
|
+
".tsx",
|
|
7289
|
+
".js",
|
|
7290
|
+
".jsx",
|
|
7291
|
+
".cts",
|
|
7292
|
+
".mts",
|
|
7293
|
+
".cjs",
|
|
7294
|
+
".mjs"
|
|
7295
|
+
]);
|
|
7296
|
+
opts;
|
|
7297
|
+
constructor(opts = {}) {
|
|
7298
|
+
super();
|
|
7299
|
+
this.opts = {
|
|
7300
|
+
deadCode: true,
|
|
7301
|
+
duplication: true,
|
|
7302
|
+
health: true,
|
|
7303
|
+
...opts
|
|
7304
|
+
};
|
|
7305
|
+
}
|
|
7306
|
+
async scan(delta, signal) {
|
|
7307
|
+
this.checkAbort(signal);
|
|
7308
|
+
const native = await tryLoadNativeApi();
|
|
7309
|
+
if (!native) {
|
|
7310
|
+
return [];
|
|
7311
|
+
}
|
|
7312
|
+
const root = this.opts.root ?? rootFromDelta(delta) ?? process.cwd();
|
|
7313
|
+
const configPath = this.opts.configPath;
|
|
7314
|
+
const findings = [];
|
|
7315
|
+
if (this.opts.deadCode) {
|
|
7316
|
+
this.checkAbort(signal);
|
|
7317
|
+
const dcOpts = { root, configPath };
|
|
7318
|
+
const result = await native.detectDeadCode(dcOpts);
|
|
7319
|
+
findings.push(...this.mapIssues(result, root, "native-dead-code", "medium"));
|
|
7320
|
+
}
|
|
7321
|
+
if (this.opts.duplication) {
|
|
7322
|
+
this.checkAbort(signal);
|
|
7323
|
+
const dupOpts = { root, configPath };
|
|
7324
|
+
const result = await native.detectDuplication(dupOpts);
|
|
7325
|
+
findings.push(...this.mapIssues(result, root, "native-duplication", "low"));
|
|
7326
|
+
}
|
|
7327
|
+
if (this.opts.health) {
|
|
7328
|
+
this.checkAbort(signal);
|
|
7329
|
+
const compOpts = { root, configPath };
|
|
7330
|
+
const result = await native.computeHealth(compOpts);
|
|
7331
|
+
findings.push(...this.mapIssues(result, root, "native-health", "medium"));
|
|
7332
|
+
}
|
|
7333
|
+
return findings;
|
|
7334
|
+
}
|
|
7335
|
+
mapIssues(raw, root, defaultRuleId, defaultSeverity) {
|
|
7336
|
+
return readIssues(raw).map((issue) => {
|
|
7337
|
+
const file = fileOf(issue, root);
|
|
7338
|
+
const line = lineOf2(issue);
|
|
7339
|
+
const column = colOf(issue);
|
|
7340
|
+
const message = messageOf(issue);
|
|
7341
|
+
return this.createFinding({
|
|
7342
|
+
id: this.deterministicId(file, line, column, defaultRuleId, message),
|
|
7343
|
+
ruleId: kindOf(issue) ?? defaultRuleId,
|
|
7344
|
+
message,
|
|
7345
|
+
file,
|
|
7346
|
+
line,
|
|
7347
|
+
column,
|
|
7348
|
+
evidence: evidenceOf(issue, message),
|
|
7349
|
+
severity: severityOf(issue) ?? defaultSeverity,
|
|
7350
|
+
confidence: 0.9,
|
|
5479
7351
|
autoFixable: false
|
|
5480
|
-
})
|
|
5481
|
-
);
|
|
7352
|
+
});
|
|
7353
|
+
});
|
|
5482
7354
|
}
|
|
5483
7355
|
};
|
|
7356
|
+
function readIssues(raw) {
|
|
7357
|
+
if (!raw || typeof raw !== "object") return [];
|
|
7358
|
+
const obj = raw;
|
|
7359
|
+
for (const key of ["issues", "findings", "results", "items"]) {
|
|
7360
|
+
const v = obj[key];
|
|
7361
|
+
if (Array.isArray(v)) return v;
|
|
7362
|
+
}
|
|
7363
|
+
return [];
|
|
7364
|
+
}
|
|
7365
|
+
function rootFromDelta(delta) {
|
|
7366
|
+
const uri = delta.documentUri;
|
|
7367
|
+
if (typeof uri === "string" && uri.startsWith("file://")) {
|
|
7368
|
+
return void 0;
|
|
7369
|
+
}
|
|
7370
|
+
return void 0;
|
|
7371
|
+
}
|
|
7372
|
+
function fileOf(issue, root) {
|
|
7373
|
+
const f = issue.file ?? issue.path ?? issue.location_file;
|
|
7374
|
+
return f ?? root;
|
|
7375
|
+
}
|
|
7376
|
+
function lineOf2(issue) {
|
|
7377
|
+
const l = issue.line ?? issue.start_line ?? issue.row;
|
|
7378
|
+
return typeof l === "number" ? l : 0;
|
|
7379
|
+
}
|
|
7380
|
+
function colOf(issue) {
|
|
7381
|
+
const c2 = issue.column ?? issue.start_column ?? issue.col;
|
|
7382
|
+
return typeof c2 === "number" ? c2 : 0;
|
|
7383
|
+
}
|
|
7384
|
+
function messageOf(issue) {
|
|
7385
|
+
const m = issue.message ?? issue.title ?? issue.summary ?? issue.kind;
|
|
7386
|
+
return typeof m === "string" ? m : "Vibecheck native finding";
|
|
7387
|
+
}
|
|
7388
|
+
function kindOf(issue) {
|
|
7389
|
+
const k = issue.kind ?? issue.rule ?? issue.type;
|
|
7390
|
+
return typeof k === "string" ? k : void 0;
|
|
7391
|
+
}
|
|
7392
|
+
function severityOf(issue) {
|
|
7393
|
+
const s = issue.severity;
|
|
7394
|
+
if (s === "critical" || s === "high" || s === "medium" || s === "low" || s === "info") {
|
|
7395
|
+
return s;
|
|
7396
|
+
}
|
|
7397
|
+
return void 0;
|
|
7398
|
+
}
|
|
7399
|
+
function evidenceOf(issue, fallback) {
|
|
7400
|
+
const e = issue.evidence ?? issue.snippet ?? issue.code;
|
|
7401
|
+
return typeof e === "string" ? e : fallback;
|
|
7402
|
+
}
|
|
7403
|
+
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
7404
|
+
function buildClusters(findings, windowLines) {
|
|
7405
|
+
if (findings.length === 0) return [];
|
|
7406
|
+
const tagged = findings.map((finding, index) => ({ finding, index }));
|
|
7407
|
+
tagged.sort((a, b) => {
|
|
7408
|
+
if (a.finding.file !== b.finding.file) {
|
|
7409
|
+
return a.finding.file < b.finding.file ? -1 : 1;
|
|
7410
|
+
}
|
|
7411
|
+
return a.finding.line - b.finding.line;
|
|
7412
|
+
});
|
|
7413
|
+
const clusters = [];
|
|
7414
|
+
let current = [];
|
|
7415
|
+
let currentFile = "";
|
|
7416
|
+
let lastLine = -Infinity;
|
|
7417
|
+
for (const entry of tagged) {
|
|
7418
|
+
const startsNewCluster = entry.finding.file !== currentFile || entry.finding.line - lastLine > windowLines;
|
|
7419
|
+
if (startsNewCluster) {
|
|
7420
|
+
if (current.length > 0) clusters.push(current);
|
|
7421
|
+
current = [entry];
|
|
7422
|
+
currentFile = entry.finding.file;
|
|
7423
|
+
} else {
|
|
7424
|
+
current.push(entry);
|
|
7425
|
+
}
|
|
7426
|
+
lastLine = entry.finding.line;
|
|
7427
|
+
}
|
|
7428
|
+
if (current.length > 0) clusters.push(current);
|
|
7429
|
+
return clusters;
|
|
7430
|
+
}
|
|
7431
|
+
function fuseFindings(findings, options = {}) {
|
|
7432
|
+
const {
|
|
7433
|
+
windowLines = 5,
|
|
7434
|
+
minEnginesForBoost = 2,
|
|
7435
|
+
minEnginesForEscalation = 3,
|
|
7436
|
+
confidenceCeiling = 0.99
|
|
7437
|
+
} = options;
|
|
7438
|
+
if (findings.length === 0) return [];
|
|
7439
|
+
if (findings.length === 1) {
|
|
7440
|
+
const single = findings[0];
|
|
7441
|
+
return [
|
|
7442
|
+
{
|
|
7443
|
+
...single,
|
|
7444
|
+
relatedFindings: single.relatedFindings ?? []
|
|
7445
|
+
}
|
|
7446
|
+
];
|
|
7447
|
+
}
|
|
7448
|
+
const input = [...findings];
|
|
7449
|
+
const clusters = buildClusters(input, windowLines);
|
|
7450
|
+
const out = new Array(input.length);
|
|
7451
|
+
for (const cluster of clusters) {
|
|
7452
|
+
const distinctEngines = new Set(cluster.map((e) => e.finding.engine));
|
|
7453
|
+
const clusterSize = distinctEngines.size;
|
|
7454
|
+
const ids = cluster.map((e) => e.finding.id).filter((id) => Boolean(id));
|
|
7455
|
+
let maxSeverityIdx = -1;
|
|
7456
|
+
for (const e of cluster) {
|
|
7457
|
+
const sevIdx = SEVERITY_ORDER.indexOf(e.finding.severity);
|
|
7458
|
+
if (sevIdx > maxSeverityIdx) maxSeverityIdx = sevIdx;
|
|
7459
|
+
}
|
|
7460
|
+
const shouldBoost = clusterSize >= minEnginesForBoost;
|
|
7461
|
+
const shouldEscalate = clusterSize >= minEnginesForEscalation && maxSeverityIdx >= SEVERITY_ORDER.indexOf("high");
|
|
7462
|
+
for (const entry of cluster) {
|
|
7463
|
+
const f = entry.finding;
|
|
7464
|
+
const relatedFindings = ids.filter((id) => id !== f.id);
|
|
7465
|
+
let nextSeverity = f.severity;
|
|
7466
|
+
let nextConfidence = f.confidence;
|
|
7467
|
+
let reason;
|
|
7468
|
+
if (shouldBoost) {
|
|
7469
|
+
const extraEngines = clusterSize - 1;
|
|
7470
|
+
let conf = f.confidence;
|
|
7471
|
+
for (let i = 0; i < extraEngines; i++) {
|
|
7472
|
+
conf = conf + (confidenceCeiling - conf) * 0.5;
|
|
7473
|
+
}
|
|
7474
|
+
nextConfidence = Math.min(confidenceCeiling, conf);
|
|
7475
|
+
reason = `${clusterSize}-engine concurrence`;
|
|
7476
|
+
}
|
|
7477
|
+
if (shouldEscalate) {
|
|
7478
|
+
const idx = SEVERITY_ORDER.indexOf(f.severity);
|
|
7479
|
+
if (idx >= 0 && idx < SEVERITY_ORDER.length - 1) {
|
|
7480
|
+
nextSeverity = SEVERITY_ORDER[idx + 1];
|
|
7481
|
+
}
|
|
7482
|
+
reason = `${clusterSize}-engine cluster (escalated)`;
|
|
7483
|
+
}
|
|
7484
|
+
const fused = {
|
|
7485
|
+
...f,
|
|
7486
|
+
severity: nextSeverity,
|
|
7487
|
+
confidence: nextConfidence,
|
|
7488
|
+
relatedFindings,
|
|
7489
|
+
...shouldBoost || shouldEscalate ? {
|
|
7490
|
+
fusion: {
|
|
7491
|
+
...f.fusion ?? {},
|
|
7492
|
+
originalSeverity: f.fusion?.originalSeverity ?? f.severity,
|
|
7493
|
+
originalConfidence: f.fusion?.originalConfidence ?? f.confidence,
|
|
7494
|
+
clusterSize,
|
|
7495
|
+
reason
|
|
7496
|
+
}
|
|
7497
|
+
} : {}
|
|
7498
|
+
};
|
|
7499
|
+
out[entry.index] = fused;
|
|
7500
|
+
}
|
|
7501
|
+
}
|
|
7502
|
+
return out;
|
|
7503
|
+
}
|
|
7504
|
+
function computeFusionStats(findings, windowLines = 5) {
|
|
7505
|
+
const clusters = buildClusters([...findings], windowLines);
|
|
7506
|
+
let boosted = 0;
|
|
7507
|
+
let escalated = 0;
|
|
7508
|
+
let largest = 0;
|
|
7509
|
+
for (const f of findings) {
|
|
7510
|
+
if (f.fusion?.reason?.includes("escalated")) escalated++;
|
|
7511
|
+
else if (f.fusion?.reason) boosted++;
|
|
7512
|
+
}
|
|
7513
|
+
for (const cluster of clusters) {
|
|
7514
|
+
const distinctEngines = new Set(cluster.map((e) => e.finding.engine)).size;
|
|
7515
|
+
if (distinctEngines > largest) largest = distinctEngines;
|
|
7516
|
+
}
|
|
7517
|
+
return {
|
|
7518
|
+
totalFindings: findings.length,
|
|
7519
|
+
totalClusters: clusters.length,
|
|
7520
|
+
boostedFindings: boosted,
|
|
7521
|
+
escalatedFindings: escalated,
|
|
7522
|
+
largestClusterSize: largest
|
|
7523
|
+
};
|
|
7524
|
+
}
|
|
5484
7525
|
function isTestLikeScanPath(filePath) {
|
|
5485
7526
|
const n = filePath.replace(/\\/g, "/");
|
|
5486
7527
|
return /\.(test|spec)\.(ts|tsx|js|jsx)$/i.test(n) || /(?:^|\/)(?:__tests__|__mocks__|tests?|fixtures?|e2e|spec|cypress|playwright|__snapshots__|stubs?|mocks?)\//i.test(
|
|
@@ -5511,11 +7552,73 @@ function fnv1aId(input) {
|
|
|
5511
7552
|
}
|
|
5512
7553
|
return hash.toString(16).padStart(8, "0");
|
|
5513
7554
|
}
|
|
5514
|
-
function
|
|
7555
|
+
function normalizeFileKey(abs) {
|
|
7556
|
+
return path6__default__default.normalize(abs);
|
|
7557
|
+
}
|
|
7558
|
+
function resolveRelativeModule(fromFileAbs, specifier, knownFiles) {
|
|
7559
|
+
if (!specifier.startsWith(".")) return void 0;
|
|
7560
|
+
const baseDir = path6__default__default.dirname(fromFileAbs);
|
|
7561
|
+
const joined = normalizeFileKey(path6__default__default.join(baseDir, specifier));
|
|
7562
|
+
if (knownFiles.has(joined)) return joined;
|
|
7563
|
+
if (path6__default__default.extname(joined)) {
|
|
7564
|
+
return void 0;
|
|
7565
|
+
}
|
|
7566
|
+
for (const ext of [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]) {
|
|
7567
|
+
const c2 = normalizeFileKey(joined + ext);
|
|
7568
|
+
if (knownFiles.has(c2)) return c2;
|
|
7569
|
+
}
|
|
7570
|
+
for (const idx of ["index.ts", "index.tsx", "index.js", "index.jsx"]) {
|
|
7571
|
+
const c2 = normalizeFileKey(path6__default__default.join(joined, idx));
|
|
7572
|
+
if (knownFiles.has(c2)) return c2;
|
|
7573
|
+
}
|
|
7574
|
+
return void 0;
|
|
7575
|
+
}
|
|
7576
|
+
function moduleSpecifierText(m) {
|
|
7577
|
+
if (m && import_typescript.default.isStringLiteral(m)) return m.text;
|
|
7578
|
+
if (m && import_typescript.default.isNoSubstitutionTemplateLiteral(m)) return m.text;
|
|
7579
|
+
return void 0;
|
|
7580
|
+
}
|
|
7581
|
+
function isDynamicImportCall(n) {
|
|
7582
|
+
return n.expression.kind === import_typescript.default.SyntaxKind.ImportKeyword;
|
|
7583
|
+
}
|
|
7584
|
+
function addJsxTagNameToCalls(tag, into) {
|
|
7585
|
+
if (import_typescript.default.isIdentifier(tag)) {
|
|
7586
|
+
into.add(tag.text);
|
|
7587
|
+
} else if (import_typescript.default.isPropertyAccessExpression(tag)) {
|
|
7588
|
+
if (import_typescript.default.isIdentifier(tag.name)) into.add(tag.name.text);
|
|
7589
|
+
if (import_typescript.default.isIdentifier(tag.expression)) into.add(tag.expression.text);
|
|
7590
|
+
} else if (tag.kind === import_typescript.default.SyntaxKind.ThisKeyword) ;
|
|
7591
|
+
else if (import_typescript.default.isJsxNamespacedName(tag)) {
|
|
7592
|
+
into.add(tag.name.text);
|
|
7593
|
+
}
|
|
7594
|
+
}
|
|
7595
|
+
function addCreateElementComponentArg(n, into) {
|
|
7596
|
+
const e = n.expression;
|
|
7597
|
+
let isComponentFactory = false;
|
|
7598
|
+
if (import_typescript.default.isIdentifier(e)) {
|
|
7599
|
+
isComponentFactory = /^(createElement|h|jsx|jsxs|jsxDEV)$/.test(e.text);
|
|
7600
|
+
} else if (import_typescript.default.isPropertyAccessExpression(e) && import_typescript.default.isIdentifier(e.name)) {
|
|
7601
|
+
isComponentFactory = /^(createElement|jsx|jsxs|jsxDEV)$/.test(e.name.text);
|
|
7602
|
+
}
|
|
7603
|
+
if (!isComponentFactory) return;
|
|
7604
|
+
const first = n.arguments[0];
|
|
7605
|
+
if (first && import_typescript.default.isIdentifier(first)) {
|
|
7606
|
+
into.add(first.text);
|
|
7607
|
+
}
|
|
7608
|
+
}
|
|
7609
|
+
function collectCallAndNewNames(sf, fromFileAbs, knownFiles, dynamicImportTargets) {
|
|
5515
7610
|
const calls = /* @__PURE__ */ new Set();
|
|
5516
7611
|
const ctors = /* @__PURE__ */ new Set();
|
|
5517
7612
|
const visit = (n) => {
|
|
5518
7613
|
if (import_typescript.default.isCallExpression(n)) {
|
|
7614
|
+
if (isDynamicImportCall(n)) {
|
|
7615
|
+
const a0 = n.arguments[0];
|
|
7616
|
+
if (a0 && import_typescript.default.isStringLiteral(a0)) {
|
|
7617
|
+
const t = resolveRelativeModule(fromFileAbs, a0.text, knownFiles);
|
|
7618
|
+
if (t) dynamicImportTargets.add(t);
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7621
|
+
addCreateElementComponentArg(n, calls);
|
|
5519
7622
|
const e = n.expression;
|
|
5520
7623
|
if (import_typescript.default.isIdentifier(e)) {
|
|
5521
7624
|
calls.add(e.text);
|
|
@@ -5524,6 +7627,10 @@ function collectCallAndNewNames(sf) {
|
|
|
5524
7627
|
}
|
|
5525
7628
|
} else if (import_typescript.default.isNewExpression(n) && import_typescript.default.isIdentifier(n.expression)) {
|
|
5526
7629
|
ctors.add(n.expression.text);
|
|
7630
|
+
} else if (import_typescript.default.isJsxSelfClosingElement(n) || import_typescript.default.isJsxOpeningElement(n)) {
|
|
7631
|
+
addJsxTagNameToCalls(n.tagName, calls);
|
|
7632
|
+
} else if (import_typescript.default.isObjectLiteralExpression(n)) {
|
|
7633
|
+
collectObjectRouteStringNames(n, calls);
|
|
5527
7634
|
}
|
|
5528
7635
|
import_typescript.default.forEachChild(n, visit);
|
|
5529
7636
|
};
|
|
@@ -5591,6 +7698,83 @@ function collectExportedValueFunctions(sf, absolutePath) {
|
|
|
5591
7698
|
}
|
|
5592
7699
|
return out;
|
|
5593
7700
|
}
|
|
7701
|
+
function addReExportAliases(fromFileAbs, st, knownFiles, into) {
|
|
7702
|
+
const spec = st.moduleSpecifier && moduleSpecifierText(st.moduleSpecifier);
|
|
7703
|
+
if (!spec) return;
|
|
7704
|
+
const target = resolveRelativeModule(fromFileAbs, spec, knownFiles);
|
|
7705
|
+
if (!target) return;
|
|
7706
|
+
if (!st.exportClause || !import_typescript.default.isNamedExports(st.exportClause)) return;
|
|
7707
|
+
for (const el of st.exportClause.elements) {
|
|
7708
|
+
const local = (el.propertyName ?? el.name).text;
|
|
7709
|
+
const exportAs = el.name.text;
|
|
7710
|
+
let perFile = into.get(target);
|
|
7711
|
+
if (!perFile) {
|
|
7712
|
+
perFile = /* @__PURE__ */ new Map();
|
|
7713
|
+
into.set(target, perFile);
|
|
7714
|
+
}
|
|
7715
|
+
let set = perFile.get(local);
|
|
7716
|
+
if (!set) {
|
|
7717
|
+
set = /* @__PURE__ */ new Set();
|
|
7718
|
+
perFile.set(local, set);
|
|
7719
|
+
}
|
|
7720
|
+
set.add(local);
|
|
7721
|
+
set.add(exportAs);
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
function getNamesFromExportRecordList(records) {
|
|
7725
|
+
return records.map((r) => r.exportName);
|
|
7726
|
+
}
|
|
7727
|
+
var ROUTE_STRING_PROP = /^(component|element|page|screen|loader|pathcomponent)$/i;
|
|
7728
|
+
function collectObjectRouteStringNames(n, into) {
|
|
7729
|
+
if (!import_typescript.default.isObjectLiteralExpression(n)) {
|
|
7730
|
+
return;
|
|
7731
|
+
}
|
|
7732
|
+
for (const p of n.properties) {
|
|
7733
|
+
if (!import_typescript.default.isPropertyAssignment(p)) continue;
|
|
7734
|
+
const key = p.name;
|
|
7735
|
+
let keyText;
|
|
7736
|
+
if (import_typescript.default.isIdentifier(key)) {
|
|
7737
|
+
keyText = key.text;
|
|
7738
|
+
} else if (import_typescript.default.isStringLiteral(key) || import_typescript.default.isNoSubstitutionTemplateLiteral(key)) {
|
|
7739
|
+
keyText = key.text;
|
|
7740
|
+
}
|
|
7741
|
+
if (!keyText || !ROUTE_STRING_PROP.test(keyText)) continue;
|
|
7742
|
+
const v = p.initializer;
|
|
7743
|
+
if (import_typescript.default.isStringLiteral(v) || import_typescript.default.isNoSubstitutionTemplateLiteral(v)) {
|
|
7744
|
+
const s = v.text;
|
|
7745
|
+
if (/^[A-Z][A-Za-z0-9_]*$/.test(s) && s.length >= 2) {
|
|
7746
|
+
into.add(s);
|
|
7747
|
+
}
|
|
7748
|
+
}
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
function saturateReexportSeeds(mergedCalls, mergedCtors, reExportAliases) {
|
|
7752
|
+
const S = /* @__PURE__ */ new Set([...mergedCalls, ...mergedCtors]);
|
|
7753
|
+
const MAX_ITER = 100;
|
|
7754
|
+
let iter = 0;
|
|
7755
|
+
let changed = true;
|
|
7756
|
+
while (changed && iter < MAX_ITER) {
|
|
7757
|
+
iter++;
|
|
7758
|
+
changed = false;
|
|
7759
|
+
for (const perLocal of reExportAliases.values()) {
|
|
7760
|
+
for (const names of perLocal.values()) {
|
|
7761
|
+
if (![...names].some((n) => S.has(n))) continue;
|
|
7762
|
+
for (const m of names) {
|
|
7763
|
+
if (!S.has(m)) {
|
|
7764
|
+
S.add(m);
|
|
7765
|
+
changed = true;
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7768
|
+
}
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
if (iter >= MAX_ITER && changed) {
|
|
7772
|
+
console.debug(
|
|
7773
|
+
`[IMPL007] saturateReexportSeeds hit MAX_ITER=${MAX_ITER}; results may be incomplete`
|
|
7774
|
+
);
|
|
7775
|
+
}
|
|
7776
|
+
return S;
|
|
7777
|
+
}
|
|
5594
7778
|
var IMPL007_SKIP_NAMES = /* @__PURE__ */ new Set([
|
|
5595
7779
|
"handler",
|
|
5596
7780
|
"main",
|
|
@@ -5613,43 +7797,86 @@ function findUncalledExportedFunctions(files, options) {
|
|
|
5613
7797
|
const signal = options?.signal;
|
|
5614
7798
|
const findings = [];
|
|
5615
7799
|
if (files.length === 0) return findings;
|
|
5616
|
-
const
|
|
5617
|
-
const
|
|
7800
|
+
const knownFiles = /* @__PURE__ */ new Set();
|
|
7801
|
+
for (const f of files) {
|
|
7802
|
+
knownFiles.add(normalizeFileKey(f.absolutePath));
|
|
7803
|
+
}
|
|
7804
|
+
const sourceByAbs = /* @__PURE__ */ new Map();
|
|
5618
7805
|
for (const f of files) {
|
|
5619
|
-
if (signal?.aborted) return [];
|
|
5620
7806
|
if (f.relativePath.endsWith(".d.ts")) continue;
|
|
5621
|
-
|
|
7807
|
+
const key = normalizeFileKey(f.absolutePath);
|
|
5622
7808
|
try {
|
|
5623
|
-
sf = import_typescript.default.createSourceFile(
|
|
7809
|
+
const sf = import_typescript.default.createSourceFile(
|
|
5624
7810
|
f.absolutePath,
|
|
5625
7811
|
f.source,
|
|
5626
7812
|
import_typescript.default.ScriptTarget.Latest,
|
|
5627
7813
|
true,
|
|
5628
7814
|
scriptKind(f.absolutePath)
|
|
5629
7815
|
);
|
|
7816
|
+
sourceByAbs.set(key, sf);
|
|
5630
7817
|
} catch {
|
|
5631
|
-
continue;
|
|
5632
7818
|
}
|
|
5633
|
-
|
|
7819
|
+
}
|
|
7820
|
+
const reExportAliases = /* @__PURE__ */ new Map();
|
|
7821
|
+
for (const f of files) {
|
|
7822
|
+
if (signal?.aborted) return [];
|
|
7823
|
+
if (f.relativePath.endsWith(".d.ts")) continue;
|
|
7824
|
+
const k = normalizeFileKey(f.absolutePath);
|
|
7825
|
+
const sf = sourceByAbs.get(k);
|
|
7826
|
+
if (!sf) continue;
|
|
7827
|
+
for (const st of sf.statements) {
|
|
7828
|
+
if (import_typescript.default.isExportDeclaration(st) && st.moduleSpecifier) {
|
|
7829
|
+
if (st.exportClause && import_typescript.default.isNamedExports(st.exportClause)) {
|
|
7830
|
+
addReExportAliases(f.absolutePath, st, knownFiles, reExportAliases);
|
|
7831
|
+
}
|
|
7832
|
+
}
|
|
7833
|
+
}
|
|
7834
|
+
}
|
|
7835
|
+
const mergedCalls = /* @__PURE__ */ new Set();
|
|
7836
|
+
const mergedCtors = /* @__PURE__ */ new Set();
|
|
7837
|
+
const dynamicImportTargets = /* @__PURE__ */ new Set();
|
|
7838
|
+
const exportStarTargets = /* @__PURE__ */ new Set();
|
|
7839
|
+
for (const f of files) {
|
|
7840
|
+
if (signal?.aborted) return [];
|
|
7841
|
+
if (f.relativePath.endsWith(".d.ts")) continue;
|
|
7842
|
+
const k = normalizeFileKey(f.absolutePath);
|
|
7843
|
+
const sf = sourceByAbs.get(k);
|
|
7844
|
+
if (!sf) continue;
|
|
7845
|
+
for (const st of sf.statements) {
|
|
7846
|
+
if (import_typescript.default.isExportDeclaration(st) && st.moduleSpecifier && st.exportClause === void 0) {
|
|
7847
|
+
const spec = moduleSpecifierText(st.moduleSpecifier);
|
|
7848
|
+
if (spec) {
|
|
7849
|
+
const t = resolveRelativeModule(f.absolutePath, spec, knownFiles);
|
|
7850
|
+
if (t) exportStarTargets.add(t);
|
|
7851
|
+
}
|
|
7852
|
+
}
|
|
7853
|
+
}
|
|
7854
|
+
const { calls, ctors } = collectCallAndNewNames(
|
|
7855
|
+
sf,
|
|
7856
|
+
f.absolutePath,
|
|
7857
|
+
knownFiles,
|
|
7858
|
+
dynamicImportTargets
|
|
7859
|
+
);
|
|
5634
7860
|
for (const c2 of calls) mergedCalls.add(c2);
|
|
5635
7861
|
for (const c2 of ctors) mergedCtors.add(c2);
|
|
5636
7862
|
}
|
|
7863
|
+
for (const targetAbs of /* @__PURE__ */ new Set([...dynamicImportTargets, ...exportStarTargets])) {
|
|
7864
|
+
if (signal?.aborted) return [];
|
|
7865
|
+
const tKey = normalizeFileKey(targetAbs);
|
|
7866
|
+
const tSf = sourceByAbs.get(tKey);
|
|
7867
|
+
if (!tSf) continue;
|
|
7868
|
+
for (const n of getNamesFromExportRecordList(collectExportedValueFunctions(tSf, tKey))) {
|
|
7869
|
+
mergedCalls.add(n);
|
|
7870
|
+
}
|
|
7871
|
+
}
|
|
7872
|
+
const usageNames = saturateReexportSeeds(mergedCalls, mergedCtors, reExportAliases);
|
|
5637
7873
|
for (const f of files) {
|
|
5638
7874
|
if (signal?.aborted) return [];
|
|
5639
7875
|
if (f.isTest || f.relativePath.endsWith(".d.ts")) continue;
|
|
5640
7876
|
if (isIndexBarrel(f.relativePath)) continue;
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
f.absolutePath,
|
|
5645
|
-
f.source,
|
|
5646
|
-
import_typescript.default.ScriptTarget.Latest,
|
|
5647
|
-
true,
|
|
5648
|
-
scriptKind(f.absolutePath)
|
|
5649
|
-
);
|
|
5650
|
-
} catch {
|
|
5651
|
-
continue;
|
|
5652
|
-
}
|
|
7877
|
+
const k = normalizeFileKey(f.absolutePath);
|
|
7878
|
+
const sf = sourceByAbs.get(k);
|
|
7879
|
+
if (!sf) continue;
|
|
5653
7880
|
const exports$1 = collectExportedValueFunctions(sf, f.absolutePath);
|
|
5654
7881
|
const seen = /* @__PURE__ */ new Set();
|
|
5655
7882
|
for (const ex of exports$1) {
|
|
@@ -5659,9 +7886,9 @@ function findUncalledExportedFunctions(files, options) {
|
|
|
5659
7886
|
if (name.startsWith("_")) continue;
|
|
5660
7887
|
if (IMPL007_SKIP_NAMES.has(name)) continue;
|
|
5661
7888
|
if (name.length <= 1) continue;
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
7889
|
+
if (usageNames.has(name)) {
|
|
7890
|
+
continue;
|
|
7891
|
+
}
|
|
5665
7892
|
const id = fnv1aId(`${ex.absolutePath}::${ex.line}::${ex.column}::IMPL007::${name}`);
|
|
5666
7893
|
findings.push({
|
|
5667
7894
|
id,
|
|
@@ -5682,96 +7909,6 @@ function findUncalledExportedFunctions(files, options) {
|
|
|
5682
7909
|
}
|
|
5683
7910
|
return findings;
|
|
5684
7911
|
}
|
|
5685
|
-
var TRUTHPACK_DIR = ".vibecheck/truthpack";
|
|
5686
|
-
async function loadTruthpack(workspaceRoot) {
|
|
5687
|
-
const dir = path5__default.join(workspaceRoot, TRUTHPACK_DIR);
|
|
5688
|
-
try {
|
|
5689
|
-
await fs.access(dir);
|
|
5690
|
-
} catch {
|
|
5691
|
-
return null;
|
|
5692
|
-
}
|
|
5693
|
-
try {
|
|
5694
|
-
const [routesData, envData] = await Promise.all([
|
|
5695
|
-
fs.readFile(path5__default.join(dir, "routes.json"), "utf-8").catch(() => '{"routes":[]}'),
|
|
5696
|
-
fs.readFile(path5__default.join(dir, "env.json"), "utf-8").catch(() => '{"variables":[]}')
|
|
5697
|
-
]);
|
|
5698
|
-
const routesJson = JSON.parse(routesData);
|
|
5699
|
-
const envJson = JSON.parse(envData);
|
|
5700
|
-
return {
|
|
5701
|
-
routes: routesJson.routes ?? [],
|
|
5702
|
-
env: envJson.variables ?? []
|
|
5703
|
-
};
|
|
5704
|
-
} catch {
|
|
5705
|
-
return null;
|
|
5706
|
-
}
|
|
5707
|
-
}
|
|
5708
|
-
var TruthpackEnvIndex = class {
|
|
5709
|
-
_index;
|
|
5710
|
-
constructor(envVars, allowlistEnvVars) {
|
|
5711
|
-
this._index = new Set(envVars.map((v) => v.name));
|
|
5712
|
-
if (Array.isArray(allowlistEnvVars)) {
|
|
5713
|
-
for (const v of allowlistEnvVars) {
|
|
5714
|
-
if (typeof v === "string" && /^[A-Z_][A-Z0-9_]*$/.test(v)) this._index.add(v);
|
|
5715
|
-
}
|
|
5716
|
-
}
|
|
5717
|
-
}
|
|
5718
|
-
get index() {
|
|
5719
|
-
return this._index;
|
|
5720
|
-
}
|
|
5721
|
-
has(name) {
|
|
5722
|
-
return this._index.has(name);
|
|
5723
|
-
}
|
|
5724
|
-
};
|
|
5725
|
-
function compileRoutePattern(routePath) {
|
|
5726
|
-
let isDynamic = false;
|
|
5727
|
-
let isCatchAll = false;
|
|
5728
|
-
let pattern = routePath.replace(/\[\[\.\.\.(\w+)\]\]/g, () => {
|
|
5729
|
-
isDynamic = true;
|
|
5730
|
-
isCatchAll = true;
|
|
5731
|
-
return "(?:\\/.*)?";
|
|
5732
|
-
}).replace(/\[\.\.\.(\w+)\]/g, () => {
|
|
5733
|
-
isDynamic = true;
|
|
5734
|
-
isCatchAll = true;
|
|
5735
|
-
return "\\/.*";
|
|
5736
|
-
}).replace(/\[(\w+)\]/g, () => {
|
|
5737
|
-
isDynamic = true;
|
|
5738
|
-
return "\\/[^/]+";
|
|
5739
|
-
}).replace(/:\w+/g, () => {
|
|
5740
|
-
isDynamic = true;
|
|
5741
|
-
return "\\/[^/]+";
|
|
5742
|
-
});
|
|
5743
|
-
pattern = pattern.replace(/\//g, "\\/").replace(/\\\\\//g, "\\/");
|
|
5744
|
-
return {
|
|
5745
|
-
regex: new RegExp(`^${pattern}$`),
|
|
5746
|
-
isDynamic,
|
|
5747
|
-
isCatchAll
|
|
5748
|
-
};
|
|
5749
|
-
}
|
|
5750
|
-
function truthpackToRouteIndex(routes, workspaceRoot) {
|
|
5751
|
-
const entries = [];
|
|
5752
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5753
|
-
for (const r of routes) {
|
|
5754
|
-
const pathStr = r.path ?? "";
|
|
5755
|
-
if (!pathStr.startsWith("/")) continue;
|
|
5756
|
-
const pattern = pathStr.startsWith("/api") ? pathStr : pathStr;
|
|
5757
|
-
const key = `${pattern}:${r.method ?? "GET"}`;
|
|
5758
|
-
if (seen.has(key)) continue;
|
|
5759
|
-
seen.add(key);
|
|
5760
|
-
const compiled = compileRoutePattern(pattern);
|
|
5761
|
-
entries.push({
|
|
5762
|
-
pattern,
|
|
5763
|
-
regex: compiled.regex,
|
|
5764
|
-
isDynamic: compiled.isDynamic,
|
|
5765
|
-
isCatchAll: compiled.isCatchAll,
|
|
5766
|
-
filePath: r.file ? path5__default.resolve(workspaceRoot, r.file) : ""
|
|
5767
|
-
});
|
|
5768
|
-
}
|
|
5769
|
-
return {
|
|
5770
|
-
routes: entries,
|
|
5771
|
-
framework: "truthpack",
|
|
5772
|
-
builtAt: Date.now()
|
|
5773
|
-
};
|
|
5774
|
-
}
|
|
5775
7912
|
function deterministicId2(workspaceRoot, routePath, status) {
|
|
5776
7913
|
const input = `rtprobe:${workspaceRoot}::${routePath}::${status}::VRD007`;
|
|
5777
7914
|
let hash = 2166136261;
|
|
@@ -5789,20 +7926,20 @@ var DEFAULT_CONFIG = {
|
|
|
5789
7926
|
};
|
|
5790
7927
|
var probedWorkspaces = /* @__PURE__ */ new Set();
|
|
5791
7928
|
function getWorkspaceRoot(filePath) {
|
|
5792
|
-
let dir =
|
|
5793
|
-
const root =
|
|
7929
|
+
let dir = path6__default.dirname(filePath);
|
|
7930
|
+
const root = path6__default.parse(dir).root;
|
|
5794
7931
|
while (dir !== root) {
|
|
5795
7932
|
try {
|
|
5796
|
-
const pkg =
|
|
7933
|
+
const pkg = path6__default.join(dir, "package.json");
|
|
5797
7934
|
if (existsSync(pkg)) return dir;
|
|
5798
7935
|
} catch (err) {
|
|
5799
7936
|
if (process.env.VIBECHECK_DEBUG) {
|
|
5800
7937
|
console.warn("[runtime_probe] getWorkspaceRoot check failed:", err instanceof Error ? err.message : err);
|
|
5801
7938
|
}
|
|
5802
7939
|
}
|
|
5803
|
-
dir =
|
|
7940
|
+
dir = path6__default.dirname(dir);
|
|
5804
7941
|
}
|
|
5805
|
-
return
|
|
7942
|
+
return path6__default.dirname(filePath);
|
|
5806
7943
|
}
|
|
5807
7944
|
function matchGlob(routePath, pattern) {
|
|
5808
7945
|
const regex = pattern.replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/{{GLOBSTAR}}/g, ".*").replace(/\?/g, ".");
|
|
@@ -5946,7 +8083,7 @@ var EnvLoader = class {
|
|
|
5946
8083
|
let hasAnyEnvFile = false;
|
|
5947
8084
|
for (const f of envFiles) {
|
|
5948
8085
|
try {
|
|
5949
|
-
const content = fs2.readFileSync(
|
|
8086
|
+
const content = fs2.readFileSync(path6__default.join(workspaceRoot, f), "utf-8");
|
|
5950
8087
|
hasAnyEnvFile = true;
|
|
5951
8088
|
if (!EXAMPLE_ONLY_FILES.has(f)) {
|
|
5952
8089
|
hasRealEnvFile = true;
|
|
@@ -5957,7 +8094,7 @@ var EnvLoader = class {
|
|
|
5957
8094
|
}
|
|
5958
8095
|
} catch (err) {
|
|
5959
8096
|
if (err?.code === "EACCES") {
|
|
5960
|
-
process.stderr.write(`[vibecheck] Permission denied: ${
|
|
8097
|
+
process.stderr.write(`[vibecheck] Permission denied: ${path6__default.join(workspaceRoot, f)}
|
|
5961
8098
|
`);
|
|
5962
8099
|
}
|
|
5963
8100
|
}
|
|
@@ -5968,17 +8105,17 @@ var EnvLoader = class {
|
|
|
5968
8105
|
if (hasAnyEnvFile && !hasRealEnvFile) {
|
|
5969
8106
|
this.exampleOnly = true;
|
|
5970
8107
|
}
|
|
5971
|
-
const workflowsDir =
|
|
8108
|
+
const workflowsDir = path6__default.join(workspaceRoot, ".github", "workflows");
|
|
5972
8109
|
if (fs2.existsSync(workflowsDir)) {
|
|
5973
8110
|
for (const f of fs2.readdirSync(workflowsDir)) {
|
|
5974
8111
|
if (!f.endsWith(".yml") && !f.endsWith(".yaml")) continue;
|
|
5975
8112
|
try {
|
|
5976
|
-
const content = fs2.readFileSync(
|
|
8113
|
+
const content = fs2.readFileSync(path6__default.join(workflowsDir, f), "utf-8");
|
|
5977
8114
|
const matches = content.match(/([A-Z_][A-Z0-9_]*):/g) ?? [];
|
|
5978
8115
|
matches.forEach((v) => this._index.add(v.replace(":", "")));
|
|
5979
8116
|
} catch (err) {
|
|
5980
8117
|
if (err?.code === "EACCES") {
|
|
5981
|
-
process.stderr.write(`[vibecheck] Permission denied: ${
|
|
8118
|
+
process.stderr.write(`[vibecheck] Permission denied: ${path6__default.join(workflowsDir, f)}
|
|
5982
8119
|
`);
|
|
5983
8120
|
}
|
|
5984
8121
|
}
|
|
@@ -6534,10 +8671,11 @@ var DEFAULT_ENGINE_THRESHOLDS = {
|
|
|
6534
8671
|
ghost_route: 0.65,
|
|
6535
8672
|
phantom_dep: 0.55,
|
|
6536
8673
|
api_truth: 0.65,
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
8674
|
+
/** Slightly higher default confidence to reduce heuristic false positives. */
|
|
8675
|
+
logic_gap: 0.57,
|
|
8676
|
+
error_handling: 0.57,
|
|
8677
|
+
outcome_verification: 0.57,
|
|
8678
|
+
incomplete_impl: 0.52,
|
|
6541
8679
|
framework_packs: 0.55
|
|
6542
8680
|
};
|
|
6543
8681
|
var INLINE_SUPPRESS_RE = /\/\/\s*vibecheck-ignore-next-line(?:\s+([A-Z0-9]+(?:\s+[A-Z0-9]+)*))?/i;
|
|
@@ -6582,17 +8720,28 @@ function deduplicateFindings(findings) {
|
|
|
6582
8720
|
}
|
|
6583
8721
|
}
|
|
6584
8722
|
const byLocation = /* @__PURE__ */ new Map();
|
|
8723
|
+
const wordSetCache = /* @__PURE__ */ new WeakMap();
|
|
8724
|
+
const getWords = (f) => {
|
|
8725
|
+
let s = wordSetCache.get(f);
|
|
8726
|
+
if (!s) {
|
|
8727
|
+
const slice = f.message.slice(0, 40).toLowerCase();
|
|
8728
|
+
s = new Set(slice.split(/\W+/).filter((w) => w.length > 3));
|
|
8729
|
+
wordSetCache.set(f, s);
|
|
8730
|
+
}
|
|
8731
|
+
return s;
|
|
8732
|
+
};
|
|
6585
8733
|
for (const f of map.values()) {
|
|
6586
8734
|
const locKey = `${f.file}::${f.line}`;
|
|
6587
8735
|
const existing = byLocation.get(locKey);
|
|
6588
8736
|
if (!existing) {
|
|
6589
8737
|
byLocation.set(locKey, f);
|
|
6590
8738
|
} else {
|
|
6591
|
-
const
|
|
6592
|
-
const
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
8739
|
+
const fWords = getWords(f);
|
|
8740
|
+
const eWords = getWords(existing);
|
|
8741
|
+
let overlap = 0;
|
|
8742
|
+
for (const w of fWords) {
|
|
8743
|
+
if (eWords.has(w)) overlap++;
|
|
8744
|
+
}
|
|
6596
8745
|
if (overlap >= 2) {
|
|
6597
8746
|
if (f.confidence > existing.confidence) {
|
|
6598
8747
|
byLocation.set(locKey, f);
|
|
@@ -6605,7 +8754,7 @@ function deduplicateFindings(findings) {
|
|
|
6605
8754
|
return [...byLocation.values()];
|
|
6606
8755
|
}
|
|
6607
8756
|
function makeDelta(filePath, text) {
|
|
6608
|
-
const ext =
|
|
8757
|
+
const ext = path6__default.extname(filePath).toLowerCase();
|
|
6609
8758
|
const lines = text.split("\n");
|
|
6610
8759
|
return {
|
|
6611
8760
|
documentUri: filePath,
|
|
@@ -6724,9 +8873,9 @@ function normalizeGhostRoutePrefixes(routes) {
|
|
|
6724
8873
|
}
|
|
6725
8874
|
function toApiFinding(f, projectRoot) {
|
|
6726
8875
|
const abs = f.file.replace(/^file:\/\//, "");
|
|
6727
|
-
const rel =
|
|
8876
|
+
const rel = path6__default.relative(projectRoot, abs).replace(/\\/g, "/");
|
|
6728
8877
|
const fileOut = rel && !rel.startsWith("..") ? rel : abs;
|
|
6729
|
-
|
|
8878
|
+
const out = {
|
|
6730
8879
|
id: f.id,
|
|
6731
8880
|
ruleId: f.ruleId ?? f.engine,
|
|
6732
8881
|
engine: String(f.engine),
|
|
@@ -6738,8 +8887,21 @@ function toApiFinding(f, projectRoot) {
|
|
|
6738
8887
|
code: f.evidence ?? "",
|
|
6739
8888
|
message: f.message,
|
|
6740
8889
|
fix: f.suggestion ?? "",
|
|
6741
|
-
autoFixable: f.autoFixable
|
|
8890
|
+
autoFixable: f.autoFixable,
|
|
8891
|
+
confidence: f.confidence
|
|
6742
8892
|
};
|
|
8893
|
+
if (f.fusion) {
|
|
8894
|
+
out.fusion = {
|
|
8895
|
+
...f.fusion.reason ? { reason: f.fusion.reason } : {},
|
|
8896
|
+
...f.fusion.clusterSize != null ? { clusterSize: f.fusion.clusterSize } : {},
|
|
8897
|
+
...f.fusion.originalSeverity ? { originalSeverity: f.fusion.originalSeverity } : {},
|
|
8898
|
+
...f.fusion.originalConfidence != null ? { originalConfidence: f.fusion.originalConfidence } : {}
|
|
8899
|
+
};
|
|
8900
|
+
}
|
|
8901
|
+
if (f.relatedFindings && f.relatedFindings.length > 0) {
|
|
8902
|
+
out.relatedFindings = f.relatedFindings;
|
|
8903
|
+
}
|
|
8904
|
+
return out;
|
|
6743
8905
|
}
|
|
6744
8906
|
async function scan(opts) {
|
|
6745
8907
|
const {
|
|
@@ -6763,7 +8925,7 @@ async function scan(opts) {
|
|
|
6763
8925
|
const internalSignal = ac.signal;
|
|
6764
8926
|
setMaxListeners(64, internalSignal);
|
|
6765
8927
|
const absoluteFiles = rawFiles.map(
|
|
6766
|
-
(f) =>
|
|
8928
|
+
(f) => path6__default.isAbsolute(f) ? path6__default.normalize(f) : path6__default.normalize(path6__default.join(projectRoot, f))
|
|
6767
8929
|
);
|
|
6768
8930
|
if (internalSignal.aborted || absoluteFiles.length === 0) {
|
|
6769
8931
|
return {
|
|
@@ -6804,7 +8966,7 @@ async function scan(opts) {
|
|
|
6804
8966
|
registry.register(
|
|
6805
8967
|
new PhantomDepEngine(projectRoot, {
|
|
6806
8968
|
confidenceThreshold: getThreshold("phantom_dep"),
|
|
6807
|
-
registryCachePath:
|
|
8969
|
+
registryCachePath: path6__default.join(projectRoot, ".vibecheck", "registry-cache.json")
|
|
6808
8970
|
}),
|
|
6809
8971
|
{ timeoutMs: engineTimeouts["phantom-dep"] ?? 100, priority: 2 }
|
|
6810
8972
|
);
|
|
@@ -6848,6 +9010,19 @@ async function scan(opts) {
|
|
|
6848
9010
|
timeoutMs: engineTimeouts["incomplete-impl"] ?? 40,
|
|
6849
9011
|
priority: 11
|
|
6850
9012
|
});
|
|
9013
|
+
registry.register(new SlopsquatEngine(), {
|
|
9014
|
+
timeoutMs: engineTimeouts.slopsquat ?? 30,
|
|
9015
|
+
priority: 2.7
|
|
9016
|
+
});
|
|
9017
|
+
const knownHosts = truthpack ? extractKnownHostsFromIntegrations(truthpack.integrations) : /* @__PURE__ */ new Set();
|
|
9018
|
+
registry.register(new AIRulesAttackEngine({ knownHosts }), {
|
|
9019
|
+
timeoutMs: engineTimeouts["ai-rules-attack"] ?? 60,
|
|
9020
|
+
priority: 4.5
|
|
9021
|
+
});
|
|
9022
|
+
registry.register(new TestQualityEngine(), {
|
|
9023
|
+
timeoutMs: engineTimeouts["test-quality"] ?? 30,
|
|
9024
|
+
priority: 11.5
|
|
9025
|
+
});
|
|
6851
9026
|
applyEngineAllowlist(registry, expandEngineAllowlist(enginesOpt));
|
|
6852
9027
|
await registry.activateAll((id, err) => {
|
|
6853
9028
|
console.warn(`[VibeCheck] Engine "${id}" activation failed:`, err);
|
|
@@ -6865,11 +9040,11 @@ async function scan(opts) {
|
|
|
6865
9040
|
const rows = [];
|
|
6866
9041
|
for (const file of absoluteFiles) {
|
|
6867
9042
|
if (internalSignal.aborted) break;
|
|
6868
|
-
const ext =
|
|
9043
|
+
const ext = path6__default.extname(file).toLowerCase();
|
|
6869
9044
|
if (!CHECKED_EXTS.has(ext)) {
|
|
6870
9045
|
rows.push({
|
|
6871
9046
|
file,
|
|
6872
|
-
relativePath:
|
|
9047
|
+
relativePath: path6__default.relative(projectRoot, file).replace(/\\/g, "/"),
|
|
6873
9048
|
findings: [],
|
|
6874
9049
|
skipped: true,
|
|
6875
9050
|
skipReason: "Unsupported extension"
|
|
@@ -6882,7 +9057,7 @@ async function scan(opts) {
|
|
|
6882
9057
|
} catch {
|
|
6883
9058
|
rows.push({
|
|
6884
9059
|
file,
|
|
6885
|
-
relativePath:
|
|
9060
|
+
relativePath: path6__default.relative(projectRoot, file).replace(/\\/g, "/"),
|
|
6886
9061
|
findings: [],
|
|
6887
9062
|
skipped: true,
|
|
6888
9063
|
skipReason: "Stat error"
|
|
@@ -6892,7 +9067,7 @@ async function scan(opts) {
|
|
|
6892
9067
|
if (stat2.size > MAX_FILE_SIZE) {
|
|
6893
9068
|
rows.push({
|
|
6894
9069
|
file,
|
|
6895
|
-
relativePath:
|
|
9070
|
+
relativePath: path6__default.relative(projectRoot, file).replace(/\\/g, "/"),
|
|
6896
9071
|
findings: [],
|
|
6897
9072
|
skipped: true,
|
|
6898
9073
|
skipReason: "File too large"
|
|
@@ -6905,7 +9080,7 @@ async function scan(opts) {
|
|
|
6905
9080
|
} catch {
|
|
6906
9081
|
rows.push({
|
|
6907
9082
|
file,
|
|
6908
|
-
relativePath:
|
|
9083
|
+
relativePath: path6__default.relative(projectRoot, file).replace(/\\/g, "/"),
|
|
6909
9084
|
findings: [],
|
|
6910
9085
|
skipped: true,
|
|
6911
9086
|
skipReason: "Read error"
|
|
@@ -6915,7 +9090,7 @@ async function scan(opts) {
|
|
|
6915
9090
|
if (text.trim().length === 0) {
|
|
6916
9091
|
rows.push({
|
|
6917
9092
|
file,
|
|
6918
|
-
relativePath:
|
|
9093
|
+
relativePath: path6__default.relative(projectRoot, file).replace(/\\/g, "/"),
|
|
6919
9094
|
findings: [],
|
|
6920
9095
|
skipped: true,
|
|
6921
9096
|
skipReason: "Empty file"
|
|
@@ -6925,7 +9100,7 @@ async function scan(opts) {
|
|
|
6925
9100
|
if (incompleteWorkspaceEnabled && (ext === ".ts" || ext === ".tsx") && !file.endsWith(".d.ts")) {
|
|
6926
9101
|
workspaceTsFiles.push({
|
|
6927
9102
|
absolutePath: file,
|
|
6928
|
-
relativePath:
|
|
9103
|
+
relativePath: path6__default.relative(projectRoot, file).replace(/\\/g, "/"),
|
|
6929
9104
|
source: text,
|
|
6930
9105
|
isTest: isTestLikeScanPath(file)
|
|
6931
9106
|
});
|
|
@@ -6958,7 +9133,7 @@ async function scan(opts) {
|
|
|
6958
9133
|
}
|
|
6959
9134
|
rows.push({
|
|
6960
9135
|
file,
|
|
6961
|
-
relativePath:
|
|
9136
|
+
relativePath: path6__default.relative(projectRoot, file).replace(/\\/g, "/"),
|
|
6962
9137
|
findings: deduped,
|
|
6963
9138
|
skipped: false
|
|
6964
9139
|
});
|
|
@@ -6970,20 +9145,24 @@ async function scan(opts) {
|
|
|
6970
9145
|
if (extra007.length > 0) {
|
|
6971
9146
|
const byFile = /* @__PURE__ */ new Map();
|
|
6972
9147
|
for (const x of extra007) {
|
|
6973
|
-
const k =
|
|
9148
|
+
const k = path6__default.normalize(x.file);
|
|
6974
9149
|
const list = byFile.get(k) ?? [];
|
|
6975
9150
|
list.push(x);
|
|
6976
9151
|
byFile.set(k, list);
|
|
6977
9152
|
}
|
|
6978
9153
|
for (const row of rows) {
|
|
6979
9154
|
if (row.skipped) continue;
|
|
6980
|
-
const add = byFile.get(
|
|
9155
|
+
const add = byFile.get(path6__default.normalize(row.file));
|
|
6981
9156
|
if (add?.length) {
|
|
6982
9157
|
row.findings = deduplicateFindings([...row.findings, ...add]);
|
|
6983
9158
|
}
|
|
6984
9159
|
}
|
|
6985
9160
|
}
|
|
6986
9161
|
}
|
|
9162
|
+
for (const row of rows) {
|
|
9163
|
+
if (row.skipped || row.findings.length === 0) continue;
|
|
9164
|
+
row.findings = fuseFindings(row.findings);
|
|
9165
|
+
}
|
|
6987
9166
|
await registry.prepareDispose();
|
|
6988
9167
|
registry.dispose();
|
|
6989
9168
|
let filesScanned = 0;
|
|
@@ -7077,7 +9256,7 @@ function severityRank(s) {
|
|
|
7077
9256
|
return 4;
|
|
7078
9257
|
}
|
|
7079
9258
|
function relPath(workspaceRoot, file) {
|
|
7080
|
-
return
|
|
9259
|
+
return path6__default.relative(workspaceRoot, file).replace(/\\/g, "/");
|
|
7081
9260
|
}
|
|
7082
9261
|
function clusterWeight(f) {
|
|
7083
9262
|
if (f.severity === "critical") return 4;
|
|
@@ -7156,6 +9335,7 @@ function buildCiSummaryJson(findings, score, workspaceRoot, opts = {}) {
|
|
|
7156
9335
|
const fixableCount = findings.filter((f) => f.autoFixable).length;
|
|
7157
9336
|
const fixableWithSuggestion = findings.filter((f) => f.autoFixable && f.suggestion).length;
|
|
7158
9337
|
const shipBlockers = buildShipBlockers(findings, score, opts);
|
|
9338
|
+
const fusionStats = computeFusionStats(findings);
|
|
7159
9339
|
const md = formatCiSummaryMarkdownFromParts({
|
|
7160
9340
|
score,
|
|
7161
9341
|
findingsCount: findings.length,
|
|
@@ -7165,7 +9345,8 @@ function buildCiSummaryJson(findings, score, workspaceRoot, opts = {}) {
|
|
|
7165
9345
|
fixableWithSuggestion,
|
|
7166
9346
|
shipBlockers,
|
|
7167
9347
|
threshold: opts.threshold,
|
|
7168
|
-
failOn: opts.failOn
|
|
9348
|
+
failOn: opts.failOn,
|
|
9349
|
+
fusionStats
|
|
7169
9350
|
});
|
|
7170
9351
|
return {
|
|
7171
9352
|
summaryMarkdown: md,
|
|
@@ -7173,7 +9354,8 @@ function buildCiSummaryJson(findings, score, workspaceRoot, opts = {}) {
|
|
|
7173
9354
|
topFindings,
|
|
7174
9355
|
fixableCount,
|
|
7175
9356
|
fixableWithSuggestion,
|
|
7176
|
-
shipBlockers
|
|
9357
|
+
shipBlockers,
|
|
9358
|
+
fusionStats
|
|
7177
9359
|
};
|
|
7178
9360
|
}
|
|
7179
9361
|
function formatCiSummaryMarkdownFromParts(p) {
|
|
@@ -7228,6 +9410,25 @@ function formatCiSummaryMarkdownFromParts(p) {
|
|
|
7228
9410
|
);
|
|
7229
9411
|
}
|
|
7230
9412
|
lines.push("");
|
|
9413
|
+
if (p.fusionStats && p.fusionStats.totalFindings > 0 && (p.fusionStats.boostedFindings > 0 || p.fusionStats.escalatedFindings > 0)) {
|
|
9414
|
+
const s = p.fusionStats;
|
|
9415
|
+
lines.push("### Cross-engine fusion");
|
|
9416
|
+
lines.push("");
|
|
9417
|
+
const parts = [];
|
|
9418
|
+
if (s.escalatedFindings > 0) {
|
|
9419
|
+
parts.push(`**${s.escalatedFindings}** escalated`);
|
|
9420
|
+
}
|
|
9421
|
+
if (s.boostedFindings > 0) {
|
|
9422
|
+
parts.push(`**${s.boostedFindings}** confidence-boosted`);
|
|
9423
|
+
}
|
|
9424
|
+
parts.push(`largest cluster size: **${s.largestClusterSize}**`);
|
|
9425
|
+
lines.push(parts.join(" \xB7 ") + ".");
|
|
9426
|
+
lines.push("");
|
|
9427
|
+
lines.push(
|
|
9428
|
+
`_Across ${s.totalFindings} finding(s) in ${s.totalClusters} cluster(s), multiple engines independently flagged ${s.boostedFindings + s.escalatedFindings} location(s). Cross-engine concurrence is a strong independent signal._`
|
|
9429
|
+
);
|
|
9430
|
+
lines.push("");
|
|
9431
|
+
}
|
|
7231
9432
|
const gateParts = [];
|
|
7232
9433
|
if (typeof p.threshold === "number") {
|
|
7233
9434
|
gateParts.push(`threshold ${p.threshold}`);
|
|
@@ -7271,6 +9472,18 @@ function formatCiSummaryPlainLines(findings, score, _workspaceRoot, opts = {}) {
|
|
|
7271
9472
|
if (fixable > 0) {
|
|
7272
9473
|
out.push(` Auto-fixable: ${fixable}${withSug ? ` (${withSug} with suggestions)` : ""}`);
|
|
7273
9474
|
}
|
|
9475
|
+
const fusionStats = computeFusionStats(findings);
|
|
9476
|
+
if (fusionStats.boostedFindings > 0 || fusionStats.escalatedFindings > 0) {
|
|
9477
|
+
const parts = [];
|
|
9478
|
+
if (fusionStats.escalatedFindings > 0) {
|
|
9479
|
+
parts.push(`${fusionStats.escalatedFindings} escalated`);
|
|
9480
|
+
}
|
|
9481
|
+
if (fusionStats.boostedFindings > 0) {
|
|
9482
|
+
parts.push(`${fusionStats.boostedFindings} boosted`);
|
|
9483
|
+
}
|
|
9484
|
+
parts.push(`largest cluster ${fusionStats.largestClusterSize}`);
|
|
9485
|
+
out.push(` Cross-engine fusion: ${parts.join(", ")}`);
|
|
9486
|
+
}
|
|
7274
9487
|
return out;
|
|
7275
9488
|
}
|
|
7276
9489
|
var SARIF_SCHEMA_URL = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json";
|
|
@@ -7291,6 +9504,15 @@ function toSarif(input, workspaceRoot, options = {}) {
|
|
|
7291
9504
|
} = options;
|
|
7292
9505
|
const rulesMap = /* @__PURE__ */ new Map();
|
|
7293
9506
|
const sarifResults = [];
|
|
9507
|
+
const findingIndex = /* @__PURE__ */ new Map();
|
|
9508
|
+
for (const fileResult of input.files) {
|
|
9509
|
+
const relPathForFile = fileResult.relativePath ?? path6__default.relative(workspaceRoot, fileResult.file).replace(/\\/g, "/");
|
|
9510
|
+
for (const finding of fileResult.findings) {
|
|
9511
|
+
if (finding.id) {
|
|
9512
|
+
findingIndex.set(finding.id, { finding, relPath: relPathForFile });
|
|
9513
|
+
}
|
|
9514
|
+
}
|
|
9515
|
+
}
|
|
7294
9516
|
for (const fileResult of input.files) {
|
|
7295
9517
|
for (const finding of fileResult.findings) {
|
|
7296
9518
|
const ruleId = finding.ruleId ?? `${finding.engine}-${finding.category}`;
|
|
@@ -7300,7 +9522,7 @@ function toSarif(input, workspaceRoot, options = {}) {
|
|
|
7300
9522
|
shortDescription: { text: finding.message }
|
|
7301
9523
|
});
|
|
7302
9524
|
}
|
|
7303
|
-
const relPath2 = fileResult.relativePath ??
|
|
9525
|
+
const relPath2 = fileResult.relativePath ?? path6__default.relative(workspaceRoot, finding.file).replace(/\\/g, "/");
|
|
7304
9526
|
const region = {
|
|
7305
9527
|
startLine: finding.line
|
|
7306
9528
|
};
|
|
@@ -7313,9 +9535,12 @@ function toSarif(input, workspaceRoot, options = {}) {
|
|
|
7313
9535
|
if (finding.endColumn != null && finding.endColumn >= 0) {
|
|
7314
9536
|
region.endColumn = finding.endColumn + 1;
|
|
7315
9537
|
}
|
|
7316
|
-
const
|
|
9538
|
+
const fusionNote = finding.fusion?.reason ? `
|
|
9539
|
+
|
|
9540
|
+
[Fusion: ${finding.fusion.reason}` + (finding.fusion.originalSeverity ? ` \u2014 originally ${finding.fusion.originalSeverity}` : "") + "]" : "";
|
|
9541
|
+
const messageText = (finding.suggestion ? `${finding.message}
|
|
7317
9542
|
|
|
7318
|
-
Suggestion: ${finding.suggestion}` : finding.message;
|
|
9543
|
+
Suggestion: ${finding.suggestion}` : finding.message) + fusionNote;
|
|
7319
9544
|
const sarifResult = {
|
|
7320
9545
|
ruleId,
|
|
7321
9546
|
level: SEVERITY_TO_SARIF_LEVEL[finding.severity] ?? "warning",
|
|
@@ -7332,6 +9557,50 @@ Suggestion: ${finding.suggestion}` : finding.message;
|
|
|
7332
9557
|
if (finding.suggestion) {
|
|
7333
9558
|
sarifResult.fixes = [{ description: { text: finding.suggestion } }];
|
|
7334
9559
|
}
|
|
9560
|
+
const relatedIds = finding.relatedFindings ?? [];
|
|
9561
|
+
if (relatedIds.length > 0) {
|
|
9562
|
+
const relatedLocations = [];
|
|
9563
|
+
for (const relId of relatedIds) {
|
|
9564
|
+
const target = findingIndex.get(relId);
|
|
9565
|
+
if (!target) continue;
|
|
9566
|
+
relatedLocations.push({
|
|
9567
|
+
physicalLocation: {
|
|
9568
|
+
artifactLocation: { uri: target.relPath },
|
|
9569
|
+
region: {
|
|
9570
|
+
startLine: target.finding.line,
|
|
9571
|
+
...target.finding.column != null && target.finding.column >= 0 ? { startColumn: target.finding.column + 1 } : {}
|
|
9572
|
+
}
|
|
9573
|
+
},
|
|
9574
|
+
message: {
|
|
9575
|
+
text: `Related: ${target.finding.ruleId ?? target.finding.engine} \u2014 ${target.finding.message.slice(0, 100)}`
|
|
9576
|
+
}
|
|
9577
|
+
});
|
|
9578
|
+
}
|
|
9579
|
+
if (relatedLocations.length > 0) {
|
|
9580
|
+
sarifResult.relatedLocations = relatedLocations;
|
|
9581
|
+
}
|
|
9582
|
+
}
|
|
9583
|
+
const properties = {};
|
|
9584
|
+
if (typeof finding.confidence === "number") {
|
|
9585
|
+
properties.confidence = finding.confidence;
|
|
9586
|
+
}
|
|
9587
|
+
if (finding.id) {
|
|
9588
|
+
properties.fingerprint = finding.id;
|
|
9589
|
+
}
|
|
9590
|
+
if (finding.fusion) {
|
|
9591
|
+
properties.fusion = {
|
|
9592
|
+
...finding.fusion.reason ? { reason: finding.fusion.reason } : {},
|
|
9593
|
+
...finding.fusion.clusterSize != null ? { clusterSize: finding.fusion.clusterSize } : {},
|
|
9594
|
+
...finding.fusion.originalSeverity ? { originalSeverity: finding.fusion.originalSeverity } : {},
|
|
9595
|
+
...finding.fusion.originalConfidence != null ? { originalConfidence: finding.fusion.originalConfidence } : {}
|
|
9596
|
+
};
|
|
9597
|
+
}
|
|
9598
|
+
if (relatedIds.length > 0) {
|
|
9599
|
+
properties.relatedFindings = relatedIds;
|
|
9600
|
+
}
|
|
9601
|
+
if (Object.keys(properties).length > 0) {
|
|
9602
|
+
sarifResult.properties = properties;
|
|
9603
|
+
}
|
|
7335
9604
|
sarifResults.push(sarifResult);
|
|
7336
9605
|
}
|
|
7337
9606
|
}
|
|
@@ -7378,7 +9647,7 @@ function formatJson(result, options) {
|
|
|
7378
9647
|
return {
|
|
7379
9648
|
...f,
|
|
7380
9649
|
filePath: fr.file,
|
|
7381
|
-
relativePath: fr.relativePath ??
|
|
9650
|
+
relativePath: fr.relativePath ?? path6__default.relative(options.workspaceRoot, fr.file).replace(/\\/g, "/"),
|
|
7382
9651
|
riskDimension,
|
|
7383
9652
|
suggestion: improveRemediationText(f.suggestion, riskDimension)
|
|
7384
9653
|
};
|
|
@@ -7485,7 +9754,7 @@ async function findFile(dir, pattern, maxDepth = 5, currentDepth = 0) {
|
|
|
7485
9754
|
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") {
|
|
7486
9755
|
continue;
|
|
7487
9756
|
}
|
|
7488
|
-
const fullPath =
|
|
9757
|
+
const fullPath = path6__default__default.join(dir, entry.name);
|
|
7489
9758
|
if (entry.isDirectory()) {
|
|
7490
9759
|
const found = await findFile(fullPath, pattern, maxDepth, currentDepth + 1);
|
|
7491
9760
|
if (found) return found;
|
|
@@ -7507,7 +9776,7 @@ async function readFileSafe(filePath) {
|
|
|
7507
9776
|
}
|
|
7508
9777
|
async function seoEngine(projectPath) {
|
|
7509
9778
|
const issues = [];
|
|
7510
|
-
const hasRobots = await pathExists(
|
|
9779
|
+
const hasRobots = await pathExists(path6__default__default.join(projectPath, "public", "robots.txt"));
|
|
7511
9780
|
if (!hasRobots) {
|
|
7512
9781
|
issues.push({
|
|
7513
9782
|
id: "missing-robots",
|
|
@@ -7519,7 +9788,7 @@ async function seoEngine(projectPath) {
|
|
|
7519
9788
|
autoFixable: true
|
|
7520
9789
|
});
|
|
7521
9790
|
}
|
|
7522
|
-
const hasSitemap = await pathExists(
|
|
9791
|
+
const hasSitemap = await pathExists(path6__default__default.join(projectPath, "public", "sitemap.xml")) || await findFile(projectPath, /sitemap/i);
|
|
7523
9792
|
if (!hasSitemap) {
|
|
7524
9793
|
issues.push({
|
|
7525
9794
|
id: "missing-sitemap",
|
|
@@ -7543,7 +9812,7 @@ async function seoEngine(projectPath) {
|
|
|
7543
9812
|
autoFixable: true
|
|
7544
9813
|
});
|
|
7545
9814
|
}
|
|
7546
|
-
const hasOgImage = await pathExists(
|
|
9815
|
+
const hasOgImage = await pathExists(path6__default__default.join(projectPath, "public", "og-image.png")) || await pathExists(path6__default__default.join(projectPath, "public", "og.png")) || await findFile(path6__default__default.join(projectPath, "public"), /og[-_]?image/i);
|
|
7547
9816
|
if (!hasOgImage) {
|
|
7548
9817
|
issues.push({
|
|
7549
9818
|
id: "missing-og-image",
|
|
@@ -7559,7 +9828,7 @@ async function seoEngine(projectPath) {
|
|
|
7559
9828
|
}
|
|
7560
9829
|
async function securityEngine(projectPath) {
|
|
7561
9830
|
const issues = [];
|
|
7562
|
-
const hasEnvExample = await pathExists(
|
|
9831
|
+
const hasEnvExample = await pathExists(path6__default__default.join(projectPath, ".env.example")) || await pathExists(path6__default__default.join(projectPath, ".env.sample"));
|
|
7563
9832
|
if (!hasEnvExample) {
|
|
7564
9833
|
issues.push({
|
|
7565
9834
|
id: "missing-env-example",
|
|
@@ -7571,7 +9840,7 @@ async function securityEngine(projectPath) {
|
|
|
7571
9840
|
autoFixable: true
|
|
7572
9841
|
});
|
|
7573
9842
|
}
|
|
7574
|
-
const gitignore = await readFileSafe(
|
|
9843
|
+
const gitignore = await readFileSafe(path6__default__default.join(projectPath, ".gitignore"));
|
|
7575
9844
|
if (gitignore && !gitignore.includes(".env")) {
|
|
7576
9845
|
issues.push({
|
|
7577
9846
|
id: "env-not-gitignored",
|
|
@@ -7583,7 +9852,7 @@ async function securityEngine(projectPath) {
|
|
|
7583
9852
|
autoFixable: true
|
|
7584
9853
|
});
|
|
7585
9854
|
}
|
|
7586
|
-
const packageJson = await readFileSafe(
|
|
9855
|
+
const packageJson = await readFileSafe(path6__default__default.join(projectPath, "package.json"));
|
|
7587
9856
|
const hasHelmet = packageJson && /helmet|next-secure-headers/i.test(packageJson);
|
|
7588
9857
|
if (!hasHelmet) {
|
|
7589
9858
|
issues.push({
|
|
@@ -7597,7 +9866,7 @@ async function securityEngine(projectPath) {
|
|
|
7597
9866
|
});
|
|
7598
9867
|
}
|
|
7599
9868
|
const hasCors = packageJson && /cors|@fastify\/cors/i.test(packageJson);
|
|
7600
|
-
const nextConfig = await readFileSafe(
|
|
9869
|
+
const nextConfig = await readFileSafe(path6__default__default.join(projectPath, "next.config.js")) || await readFileSafe(path6__default__default.join(projectPath, "next.config.mjs")) || await readFileSafe(path6__default__default.join(projectPath, "next.config.ts"));
|
|
7601
9870
|
const hasNextCors = nextConfig && /headers|Access-Control/i.test(nextConfig ?? "");
|
|
7602
9871
|
if (!hasCors && !hasNextCors) {
|
|
7603
9872
|
issues.push({
|
|
@@ -7614,8 +9883,8 @@ async function securityEngine(projectPath) {
|
|
|
7614
9883
|
}
|
|
7615
9884
|
async function resilienceEngine(projectPath) {
|
|
7616
9885
|
const issues = [];
|
|
7617
|
-
const packageJson = await readFileSafe(
|
|
7618
|
-
const srcPath =
|
|
9886
|
+
const packageJson = await readFileSafe(path6__default__default.join(projectPath, "package.json"));
|
|
9887
|
+
const srcPath = path6__default__default.join(projectPath, "src");
|
|
7619
9888
|
const hasSrc = await pathExists(srcPath);
|
|
7620
9889
|
const searchPath = hasSrc ? srcPath : projectPath;
|
|
7621
9890
|
if (!(packageJson && /opossum|cockatiel|resilience4j|circuit.*breaker/i.test(packageJson))) {
|
|
@@ -7686,8 +9955,8 @@ async function resilienceEngine(projectPath) {
|
|
|
7686
9955
|
}
|
|
7687
9956
|
async function performanceEngine(projectPath) {
|
|
7688
9957
|
const issues = [];
|
|
7689
|
-
const packageJson = await readFileSafe(
|
|
7690
|
-
const nextConfig = await readFileSafe(
|
|
9958
|
+
const packageJson = await readFileSafe(path6__default__default.join(projectPath, "package.json"));
|
|
9959
|
+
const nextConfig = await readFileSafe(path6__default__default.join(projectPath, "next.config.js")) || await readFileSafe(path6__default__default.join(projectPath, "next.config.mjs"));
|
|
7691
9960
|
const hasImageOptimization = packageJson && /sharp|next\/image|@next\/image/i.test(packageJson) || nextConfig && /images:/i.test(nextConfig ?? "");
|
|
7692
9961
|
if (!hasImageOptimization) {
|
|
7693
9962
|
issues.push({
|
|
@@ -7728,8 +9997,8 @@ async function performanceEngine(projectPath) {
|
|
|
7728
9997
|
}
|
|
7729
9998
|
async function observabilityEngine(projectPath) {
|
|
7730
9999
|
const issues = [];
|
|
7731
|
-
const packageJson = await readFileSafe(
|
|
7732
|
-
const srcPath =
|
|
10000
|
+
const packageJson = await readFileSafe(path6__default__default.join(projectPath, "package.json"));
|
|
10001
|
+
const srcPath = path6__default__default.join(projectPath, "src");
|
|
7733
10002
|
const hasSrc = await pathExists(srcPath);
|
|
7734
10003
|
const searchPath = hasSrc ? srcPath : projectPath;
|
|
7735
10004
|
if (!(packageJson && /@opentelemetry|otel/i.test(packageJson))) {
|
|
@@ -7785,8 +10054,8 @@ async function observabilityEngine(projectPath) {
|
|
|
7785
10054
|
}
|
|
7786
10055
|
async function infrastructureEngine(projectPath) {
|
|
7787
10056
|
const issues = [];
|
|
7788
|
-
const packageJson = await readFileSafe(
|
|
7789
|
-
const hasDocker = await pathExists(
|
|
10057
|
+
const packageJson = await readFileSafe(path6__default__default.join(projectPath, "package.json"));
|
|
10058
|
+
const hasDocker = await pathExists(path6__default__default.join(projectPath, "Dockerfile")) || await pathExists(path6__default__default.join(projectPath, "docker-compose.yml"));
|
|
7790
10059
|
if (!hasDocker) {
|
|
7791
10060
|
issues.push({
|
|
7792
10061
|
id: "missing-docker",
|
|
@@ -7798,7 +10067,7 @@ async function infrastructureEngine(projectPath) {
|
|
|
7798
10067
|
autoFixable: true
|
|
7799
10068
|
});
|
|
7800
10069
|
}
|
|
7801
|
-
const hasCi = await pathExists(
|
|
10070
|
+
const hasCi = await pathExists(path6__default__default.join(projectPath, ".github", "workflows")) || await pathExists(path6__default__default.join(projectPath, ".gitlab-ci.yml")) || await pathExists(path6__default__default.join(projectPath, ".circleci"));
|
|
7802
10071
|
if (!hasCi) {
|
|
7803
10072
|
issues.push({
|
|
7804
10073
|
id: "missing-ci",
|
|
@@ -7810,7 +10079,7 @@ async function infrastructureEngine(projectPath) {
|
|
|
7810
10079
|
autoFixable: true
|
|
7811
10080
|
});
|
|
7812
10081
|
}
|
|
7813
|
-
const hasDeployConfig = await pathExists(
|
|
10082
|
+
const hasDeployConfig = await pathExists(path6__default__default.join(projectPath, "vercel.json")) || await pathExists(path6__default__default.join(projectPath, "netlify.toml")) || await pathExists(path6__default__default.join(projectPath, "railway.json"));
|
|
7814
10083
|
if (!hasDeployConfig && !hasDocker) {
|
|
7815
10084
|
issues.push({
|
|
7816
10085
|
id: "missing-deployment-config",
|
|
@@ -7838,7 +10107,7 @@ async function infrastructureEngine(projectPath) {
|
|
|
7838
10107
|
}
|
|
7839
10108
|
async function documentationEngine(projectPath) {
|
|
7840
10109
|
const issues = [];
|
|
7841
|
-
const hasReadme = await pathExists(
|
|
10110
|
+
const hasReadme = await pathExists(path6__default__default.join(projectPath, "README.md"));
|
|
7842
10111
|
if (!hasReadme) {
|
|
7843
10112
|
issues.push({
|
|
7844
10113
|
id: "missing-readme",
|
|
@@ -7850,7 +10119,7 @@ async function documentationEngine(projectPath) {
|
|
|
7850
10119
|
autoFixable: true
|
|
7851
10120
|
});
|
|
7852
10121
|
} else {
|
|
7853
|
-
const readme = await readFileSafe(
|
|
10122
|
+
const readme = await readFileSafe(path6__default__default.join(projectPath, "README.md"));
|
|
7854
10123
|
if (readme && readme.length < 500) {
|
|
7855
10124
|
issues.push({
|
|
7856
10125
|
id: "insufficient-readme",
|
|
@@ -7863,7 +10132,7 @@ async function documentationEngine(projectPath) {
|
|
|
7863
10132
|
});
|
|
7864
10133
|
}
|
|
7865
10134
|
}
|
|
7866
|
-
if (!await pathExists(
|
|
10135
|
+
if (!await pathExists(path6__default__default.join(projectPath, "CHANGELOG.md"))) {
|
|
7867
10136
|
issues.push({
|
|
7868
10137
|
id: "missing-changelog",
|
|
7869
10138
|
category: "Documentation",
|
|
@@ -7874,7 +10143,7 @@ async function documentationEngine(projectPath) {
|
|
|
7874
10143
|
autoFixable: true
|
|
7875
10144
|
});
|
|
7876
10145
|
}
|
|
7877
|
-
if (!await pathExists(
|
|
10146
|
+
if (!await pathExists(path6__default__default.join(projectPath, "CONTRIBUTING.md"))) {
|
|
7878
10147
|
issues.push({
|
|
7879
10148
|
id: "missing-contributing",
|
|
7880
10149
|
category: "Documentation",
|
|
@@ -7885,7 +10154,7 @@ async function documentationEngine(projectPath) {
|
|
|
7885
10154
|
autoFixable: true
|
|
7886
10155
|
});
|
|
7887
10156
|
}
|
|
7888
|
-
const hasLicense = await pathExists(
|
|
10157
|
+
const hasLicense = await pathExists(path6__default__default.join(projectPath, "LICENSE")) || await pathExists(path6__default__default.join(projectPath, "LICENSE.md"));
|
|
7889
10158
|
if (!hasLicense) {
|
|
7890
10159
|
issues.push({
|
|
7891
10160
|
id: "missing-license",
|
|
@@ -7901,7 +10170,7 @@ async function documentationEngine(projectPath) {
|
|
|
7901
10170
|
}
|
|
7902
10171
|
async function configurationEngine(projectPath) {
|
|
7903
10172
|
const issues = [];
|
|
7904
|
-
const tsConfig = await readFileSafe(
|
|
10173
|
+
const tsConfig = await readFileSafe(path6__default__default.join(projectPath, "tsconfig.json"));
|
|
7905
10174
|
if (tsConfig) {
|
|
7906
10175
|
try {
|
|
7907
10176
|
const parsed = JSON.parse(tsConfig);
|
|
@@ -7919,7 +10188,7 @@ async function configurationEngine(projectPath) {
|
|
|
7919
10188
|
} catch {
|
|
7920
10189
|
}
|
|
7921
10190
|
}
|
|
7922
|
-
const hasEslint = await pathExists(
|
|
10191
|
+
const hasEslint = await pathExists(path6__default__default.join(projectPath, ".eslintrc.json")) || await pathExists(path6__default__default.join(projectPath, ".eslintrc.js")) || await pathExists(path6__default__default.join(projectPath, "eslint.config.js"));
|
|
7923
10192
|
if (!hasEslint) {
|
|
7924
10193
|
issues.push({
|
|
7925
10194
|
id: "missing-eslint",
|
|
@@ -7931,7 +10200,7 @@ async function configurationEngine(projectPath) {
|
|
|
7931
10200
|
autoFixable: true
|
|
7932
10201
|
});
|
|
7933
10202
|
}
|
|
7934
|
-
const hasPrettier = await pathExists(
|
|
10203
|
+
const hasPrettier = await pathExists(path6__default__default.join(projectPath, ".prettierrc")) || await pathExists(path6__default__default.join(projectPath, ".prettierrc.json")) || await pathExists(path6__default__default.join(projectPath, "prettier.config.js"));
|
|
7935
10204
|
if (!hasPrettier) {
|
|
7936
10205
|
issues.push({
|
|
7937
10206
|
id: "missing-prettier",
|
|
@@ -7943,7 +10212,7 @@ async function configurationEngine(projectPath) {
|
|
|
7943
10212
|
autoFixable: true
|
|
7944
10213
|
});
|
|
7945
10214
|
}
|
|
7946
|
-
const hasEditorConfig = await pathExists(
|
|
10215
|
+
const hasEditorConfig = await pathExists(path6__default__default.join(projectPath, ".editorconfig"));
|
|
7947
10216
|
if (!hasEditorConfig) {
|
|
7948
10217
|
issues.push({
|
|
7949
10218
|
id: "missing-editorconfig",
|
|
@@ -7959,13 +10228,13 @@ async function configurationEngine(projectPath) {
|
|
|
7959
10228
|
}
|
|
7960
10229
|
async function backendEngine(projectPath) {
|
|
7961
10230
|
const issues = [];
|
|
7962
|
-
const apiPath =
|
|
7963
|
-
const apiAltPath =
|
|
7964
|
-
const pagesApiPath =
|
|
7965
|
-
const appApiPath =
|
|
10231
|
+
const apiPath = path6__default__default.join(projectPath, "src", "server");
|
|
10232
|
+
const apiAltPath = path6__default__default.join(projectPath, "api");
|
|
10233
|
+
const pagesApiPath = path6__default__default.join(projectPath, "pages", "api");
|
|
10234
|
+
const appApiPath = path6__default__default.join(projectPath, "app", "api");
|
|
7966
10235
|
const hasApi = await pathExists(apiPath) || await pathExists(apiAltPath) || await pathExists(pagesApiPath) || await pathExists(appApiPath);
|
|
7967
10236
|
if (!hasApi) return issues;
|
|
7968
|
-
const packageJson = await readFileSafe(
|
|
10237
|
+
const packageJson = await readFileSafe(path6__default__default.join(projectPath, "package.json"));
|
|
7969
10238
|
const hasHealth = await findFile(projectPath, /health|healthcheck|status/i);
|
|
7970
10239
|
if (!hasHealth) {
|
|
7971
10240
|
issues.push({
|
|
@@ -8020,7 +10289,7 @@ async function backendEngine(projectPath) {
|
|
|
8020
10289
|
}
|
|
8021
10290
|
async function accessibilityEngine(projectPath) {
|
|
8022
10291
|
const issues = [];
|
|
8023
|
-
const packageJson = await readFileSafe(
|
|
10292
|
+
const packageJson = await readFileSafe(path6__default__default.join(projectPath, "package.json"));
|
|
8024
10293
|
const hasA11yTesting = packageJson && /axe-core|@axe-core|jest-axe|cypress-axe|pa11y/i.test(packageJson);
|
|
8025
10294
|
if (!hasA11yTesting) {
|
|
8026
10295
|
issues.push({
|
|
@@ -8045,7 +10314,7 @@ async function accessibilityEngine(projectPath) {
|
|
|
8045
10314
|
autoFixable: true
|
|
8046
10315
|
});
|
|
8047
10316
|
}
|
|
8048
|
-
const globalCss = await readFileSafe(
|
|
10317
|
+
const globalCss = await readFileSafe(path6__default__default.join(projectPath, "src", "styles", "globals.css")) || await readFileSafe(path6__default__default.join(projectPath, "app", "globals.css")) || await readFileSafe(path6__default__default.join(projectPath, "styles", "globals.css"));
|
|
8049
10318
|
if (globalCss && /outline:\s*none|outline:\s*0/i.test(globalCss) && !/focus-visible/i.test(globalCss)) {
|
|
8050
10319
|
issues.push({
|
|
8051
10320
|
id: "focus-removed",
|
|
@@ -8083,9 +10352,9 @@ function hasLibrary(packageJsonContent, libraries) {
|
|
|
8083
10352
|
return null;
|
|
8084
10353
|
}
|
|
8085
10354
|
async function detectProjectType(projectPath, packageJsonContent) {
|
|
8086
|
-
const hasApp = await pathExists(
|
|
8087
|
-
const hasPages = await pathExists(
|
|
8088
|
-
const hasSrc = await pathExists(
|
|
10355
|
+
const hasApp = await pathExists(path6__default__default.join(projectPath, "app"));
|
|
10356
|
+
const hasPages = await pathExists(path6__default__default.join(projectPath, "pages"));
|
|
10357
|
+
const hasSrc = await pathExists(path6__default__default.join(projectPath, "src"));
|
|
8089
10358
|
const isNextJs = !!packageJsonContent && /["']next["']/.test(packageJsonContent);
|
|
8090
10359
|
const isRemix = !!packageJsonContent && /["']@remix-run\//.test(packageJsonContent);
|
|
8091
10360
|
const isVite = !!packageJsonContent && /["']vite["']/.test(packageJsonContent);
|
|
@@ -8176,7 +10445,7 @@ var ENGINES = [
|
|
|
8176
10445
|
];
|
|
8177
10446
|
async function runPolish(projectPath) {
|
|
8178
10447
|
const allIssues = [];
|
|
8179
|
-
const packageJson = await readFileSafe(
|
|
10448
|
+
const packageJson = await readFileSafe(path6__default__default.join(projectPath, "package.json"));
|
|
8180
10449
|
const projectType = await detectProjectType(projectPath, packageJson);
|
|
8181
10450
|
const engineResults = await Promise.allSettled(
|
|
8182
10451
|
ENGINES.map((engine) => engine(projectPath))
|
|
@@ -8591,13 +10860,13 @@ function estimateBlastRadius(steps, file) {
|
|
|
8591
10860
|
}
|
|
8592
10861
|
}
|
|
8593
10862
|
}
|
|
8594
|
-
const
|
|
8595
|
-
if (
|
|
8596
|
-
if (
|
|
10863
|
+
const path24 = file.toLowerCase();
|
|
10864
|
+
if (path24.includes("checkout") || path24.includes("payment")) flows.add("payment flow");
|
|
10865
|
+
if (path24.includes("auth") || path24.includes("login") || path24.includes("signup"))
|
|
8597
10866
|
flows.add("authentication");
|
|
8598
|
-
if (
|
|
8599
|
-
if (
|
|
8600
|
-
if (
|
|
10867
|
+
if (path24.includes("api/") || path24.includes("route")) flows.add("API endpoint");
|
|
10868
|
+
if (path24.includes("middleware")) flows.add("request pipeline");
|
|
10869
|
+
if (path24.includes("webhook")) flows.add("webhook processing");
|
|
8601
10870
|
if (flows.size === 0) return void 0;
|
|
8602
10871
|
const flowList = [...flows].slice(0, 3).join(", ");
|
|
8603
10872
|
const errorPct = Math.round(errors.length / Math.max(steps.length, 1) * 100);
|
|
@@ -8657,7 +10926,7 @@ async function loadEngines(workspaceRoot) {
|
|
|
8657
10926
|
})();
|
|
8658
10927
|
const ghostRouteIndex = truthpack ? truthpackToRouteIndex(truthpack.routes, workspaceRoot) : void 0;
|
|
8659
10928
|
const ghostRoutePrefixes = [];
|
|
8660
|
-
const { PhantomDepEngine: PhantomDepEngine2 } = await import('./PhantomDepEngine-
|
|
10929
|
+
const { PhantomDepEngine: PhantomDepEngine2 } = await import('./PhantomDepEngine-5O7Z7MDE-4A37GGL4.js');
|
|
8661
10930
|
const { APITruthEngine: APITruthEngine2 } = await import('./APITruthEngine-IZRR3NT5-LPFUOMLD.js');
|
|
8662
10931
|
const { EnvVarEngine: EnvVarEngine2 } = await import('./EnvVarEngine-ZFNW2XKP-6HRTZULP.js');
|
|
8663
10932
|
const { GhostRouteEngine: GhostRouteEngine2 } = await import('./GhostRouteEngine-UMYBCOCL-MSZOPVZY.js');
|
|
@@ -8666,7 +10935,7 @@ async function loadEngines(workspaceRoot) {
|
|
|
8666
10935
|
const { VersionHallucinationEngine: VersionHallucinationEngine2 } = await import('./VersionHallucinationEngine-673DJ26J-BD4SK6JX.js');
|
|
8667
10936
|
const { FrameworkPackEngine: FrameworkPackEngine2 } = await import('./FrameworkPackEngine-RRBJW4MC-KH7WRXXS.js');
|
|
8668
10937
|
const { LogicGapEngine: LogicGapEngine2 } = await import('./LogicGapEngine-OK5UKZQ5-YGXZDERB.js');
|
|
8669
|
-
const { ErrorHandlingEngine: ErrorHandlingEngine2 } = await import('./ErrorHandlingEngine-
|
|
10938
|
+
const { ErrorHandlingEngine: ErrorHandlingEngine2 } = await import('./ErrorHandlingEngine-FG65SFRB-4NEANCMF.js');
|
|
8670
10939
|
const confidenceThreshold = 0.75;
|
|
8671
10940
|
return [
|
|
8672
10941
|
new EnvVarEngine2(envIndex, {
|
|
@@ -8682,7 +10951,7 @@ async function loadEngines(workspaceRoot) {
|
|
|
8682
10951
|
),
|
|
8683
10952
|
new PhantomDepEngine2(workspaceRoot, {
|
|
8684
10953
|
confidenceThreshold: 0.55,
|
|
8685
|
-
registryCachePath:
|
|
10954
|
+
registryCachePath: path6__default.join(workspaceRoot, ".vibecheck", "registry-cache.json")
|
|
8686
10955
|
}),
|
|
8687
10956
|
new APITruthEngine2(0.65, void 0, workspaceRoot),
|
|
8688
10957
|
new CredentialsEngine2(),
|
|
@@ -8694,7 +10963,7 @@ async function loadEngines(workspaceRoot) {
|
|
|
8694
10963
|
];
|
|
8695
10964
|
}
|
|
8696
10965
|
function buildDelta(filePath, source) {
|
|
8697
|
-
const ext =
|
|
10966
|
+
const ext = path6__default.extname(filePath).toLowerCase();
|
|
8698
10967
|
const lines = source.split("\n");
|
|
8699
10968
|
return {
|
|
8700
10969
|
documentUri: filePath,
|
|
@@ -8797,10 +11066,10 @@ function findingsForFile(findings, relativePath) {
|
|
|
8797
11066
|
async function runGhostTrace(options) {
|
|
8798
11067
|
const { workspaceRoot, filePath, signal, engineTimeout = 1e4 } = options;
|
|
8799
11068
|
const start = Date.now();
|
|
8800
|
-
const absolutePath =
|
|
11069
|
+
const absolutePath = path6__default.isAbsolute(filePath) ? filePath : path6__default.resolve(workspaceRoot, filePath);
|
|
8801
11070
|
const source = fs2.readFileSync(absolutePath, "utf-8");
|
|
8802
11071
|
const lines = source.split("\n");
|
|
8803
|
-
const relativePath =
|
|
11072
|
+
const relativePath = path6__default.relative(workspaceRoot, absolutePath).replace(/\\/g, "/");
|
|
8804
11073
|
const callSites = parseCallSites(source);
|
|
8805
11074
|
const delta = buildDelta(absolutePath, source);
|
|
8806
11075
|
const engines = await loadEngines(workspaceRoot);
|
|
@@ -8858,4 +11127,4 @@ async function runGhostTrace(options) {
|
|
|
8858
11127
|
};
|
|
8859
11128
|
}
|
|
8860
11129
|
|
|
8861
|
-
export { CircuitBreaker, DAILY_SCAN_LIMIT_UPGRADE_URL, ENGINE_FOCUS_PRESETS, EngineRegistry, EnvLoader, FEATURE_NAMES, FakeFeaturesEngine, IncompleteImplEngine, OutcomeVerificationEngine, PerformanceAntipatternEngine, RULE_AUTOFIX_SAFETY_BY_ID, RULE_MATURITY_BY_ID, RULE_TRUST_IMPACT_BY_ID, RuntimeProbeEngine, SCAN_ENGINE_FOCUS_PRESET_NAMES, SecurityPatternEngine, TruthpackEnvIndex, TypeContractEngine, VIBECHECK_DEFAULT_ENGINE_IDS, accessibilityEngine, autofixSafetyForRule, backendEngine, buildCiSummaryJson, buildCliUpgradeBlock, buildGatedScanResponse, buildRiskClusters, buildShipBlockers, canAccessFeature, categoryIcons, computeTrustScore, configurationEngine, createDefaultRegistry, dashboardFindingUrl, diffScores, documentationEngine, engineTogglesAllowOnly, estimateBlastRadius, fetchCanonicalAccess, findUncalledExportedFunctions, formatCiSummaryMarkdown, formatCiSummaryPlainLines, formatDailyScanLimitMessage, formatFindingSeverityBreakdown, formatFindings, formatSummary, formatTraceAsJson, formatTraceForTerminal, formatTrustScoreMarkdown, formatTrustScoreMcp, gateCanonicalScanReportFindings, generateVerdict, getCheckoutUrl, getMinPlanForApiSurface, getMinimumPlanForFeature, getQuotas, getTrustScoreStatus, icons, infrastructureEngine, isTestLikeScanPath, isVibecheckFinding, loadTruthpack, maturityTierForRule, normalizeCanonicalScanReport, normalizePlanId, observabilityEngine, parseCallSites, performanceEngine, planHasApiSurface, resilienceEngine, runGhostTrace, runPolish, scan, securityEngine, seoEngine, toCanonicalFinding, toCanonicalFindingFromGuardrail, toCanonicalFindingFromLegacyFinding, toCanonicalFindingFromMissionFinding, toCanonicalFindingFromPolish, toCanonicalFindingFromReport, toCanonicalFindingFromScanApi, toCanonicalFindingFromScanFinding, toCanonicalFindings, toSarif, trustImpactForRule, trustPenaltyScaleForFinding, truthpackToRouteIndex };
|
|
11130
|
+
export { AIRulesAttackEngine, AI_HALLUCINATED_PACKAGES, CANONICAL_AI_PACKAGES_NPM, CORE_ENGINE_IDS, CORE_ENGINE_MANIFESTS, CircuitBreaker, DAILY_SCAN_LIMIT_UPGRADE_URL, ENGINE_FOCUS_PRESETS, EngineRegistry, EnvLoader, FEATURE_NAMES, FakeFeaturesEngine, IncompleteImplEngine, OutcomeVerificationEngine, PerformanceAntipatternEngine, RULE_AUTOFIX_SAFETY_BY_ID, RULE_MATURITY_BY_ID, RULE_TRUST_IMPACT_BY_ID, RuntimeProbeEngine, SCAN_ENGINE_FOCUS_PRESET_NAMES, SecurityPatternEngine, SlopsquatEngine, TestQualityEngine, TruthpackEnvIndex, TypeContractEngine, VIBECHECK_DEFAULT_ENGINE_IDS, VibecheckNativeEngine, WORKSPACE_ENGINE_IDS, WORKSPACE_ENGINE_MANIFESTS, accessibilityEngine, aiRulesAttackManifest, apexFromHost, apexFromUrl, autofixSafetyForRule, backendEngine, buildCiSummaryJson, buildCliUpgradeBlock, buildGatedScanResponse, buildRiskClusters, buildShipBlockers, canAccessFeature, categoryIcons, computeFusionStats, computeTrustScore, configurationEngine, createDefaultRegistry, dashboardFindingUrl, detectBrandImpersonationNpm, diffScores, documentationEngine, engineTogglesAllowOnly, errorHandlingManifest, estimateBlastRadius, extractKnownHostsFromIntegrations, fakeFeaturesManifest, fetchCanonicalAccess, findUncalledExportedFunctions, formatCiSummaryMarkdown, formatCiSummaryPlainLines, formatDailyScanLimitMessage, formatFindingSeverityBreakdown, formatFindings, formatSummary, formatTraceAsJson, formatTraceForTerminal, formatTrustScoreMarkdown, formatTrustScoreMcp, fuseFindings, gateCanonicalScanReportFindings, generateVerdict, getCheckoutUrl, getMinPlanForApiSurface, getMinimumPlanForFeature, getQuotas, getTrustScoreStatus, ghostRouteManifest, hostMatchesKnownSet, icons, incompleteImplManifest, infrastructureEngine, isTestLikeScanPath, isVibecheckFinding, loadTruthpack, logicGapManifest, maturityTierForRule, normalizeCanonicalScanReport, normalizePlanId, observabilityEngine, outcomeVerificationManifest, parseCallSites, perfAntipatternManifest, performanceEngine, phantomDepManifest, planHasApiSurface, registerAllEngines, registerCoreEngines, registerWorkspaceEngines, resilienceEngine, runGhostTrace, runPolish, scan, securityEngine, securityPatternManifest, seoEngine, slopsquatManifest, testQualityManifest, toCanonicalFinding, toCanonicalFindingFromGuardrail, toCanonicalFindingFromLegacyFinding, toCanonicalFindingFromMissionFinding, toCanonicalFindingFromPolish, toCanonicalFindingFromReport, toCanonicalFindingFromScanApi, toCanonicalFindingFromScanFinding, toCanonicalFindings, toSarif, trustImpactForRule, trustPenaltyScaleForFinding, truthpackToRouteIndex, typeContractManifest };
|