@grwnd/pi-governance 3.0.1 → 3.1.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/extensions/index.cjs +1106 -3
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.js +1106 -3
- package/dist/extensions/index.js.map +1 -1
- package/dist/index.cjs +1052 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -2
- package/dist/index.d.ts +206 -2
- package/dist/index.js +1044 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -1
package/dist/index.js
CHANGED
|
@@ -3203,6 +3203,32 @@ var DlpConfig = Type.Object({
|
|
|
3203
3203
|
allowlist: Type.Optional(Type.Array(DlpAllowlistEntryConfig)),
|
|
3204
3204
|
role_overrides: Type.Optional(Type.Record(Type.String(), DlpRoleOverrideConfig))
|
|
3205
3205
|
});
|
|
3206
|
+
var DependencyGuardianChecksConfig = Type.Object({
|
|
3207
|
+
existence: Type.Boolean({ default: true }),
|
|
3208
|
+
reputation: Type.Boolean({ default: true }),
|
|
3209
|
+
typosquatting: Type.Boolean({ default: true }),
|
|
3210
|
+
install_scripts: Type.Boolean({ default: true }),
|
|
3211
|
+
vulnerabilities: Type.Boolean({ default: true })
|
|
3212
|
+
});
|
|
3213
|
+
var DependencyGuardianConfig = Type.Object({
|
|
3214
|
+
enabled: Type.Boolean({ default: true }),
|
|
3215
|
+
checks: Type.Optional(DependencyGuardianChecksConfig),
|
|
3216
|
+
risk_thresholds: Type.Optional(
|
|
3217
|
+
Type.Object({
|
|
3218
|
+
min_age_days: Type.Number({ default: 30, minimum: 0 }),
|
|
3219
|
+
min_weekly_downloads: Type.Number({ default: 100, minimum: 0 })
|
|
3220
|
+
})
|
|
3221
|
+
),
|
|
3222
|
+
on_risk: Type.Optional(
|
|
3223
|
+
Type.Union([Type.Literal("escalate"), Type.Literal("block"), Type.Literal("audit")], {
|
|
3224
|
+
default: "escalate"
|
|
3225
|
+
})
|
|
3226
|
+
),
|
|
3227
|
+
allowlist: Type.Optional(Type.Array(Type.String())),
|
|
3228
|
+
blocklist: Type.Optional(Type.Array(Type.String())),
|
|
3229
|
+
blocklist_patterns: Type.Optional(Type.Array(Type.String())),
|
|
3230
|
+
custom_registry_bypass: Type.Boolean({ default: true })
|
|
3231
|
+
});
|
|
3206
3232
|
var GovernanceConfigSchema = Type.Object({
|
|
3207
3233
|
auth: Type.Optional(AuthConfig),
|
|
3208
3234
|
policy: Type.Optional(PolicyConfig),
|
|
@@ -3210,6 +3236,7 @@ var GovernanceConfigSchema = Type.Object({
|
|
|
3210
3236
|
hitl: Type.Optional(HitlConfig),
|
|
3211
3237
|
audit: Type.Optional(AuditConfig),
|
|
3212
3238
|
dlp: Type.Optional(DlpConfig),
|
|
3239
|
+
dependency_guardian: Type.Optional(DependencyGuardianConfig),
|
|
3213
3240
|
org_units: Type.Optional(Type.Record(Type.String(), OrgUnitOverride))
|
|
3214
3241
|
});
|
|
3215
3242
|
|
|
@@ -4030,6 +4057,1015 @@ var DlpMasker = class {
|
|
|
4030
4057
|
}
|
|
4031
4058
|
};
|
|
4032
4059
|
|
|
4060
|
+
// src/lib/deps/parser.ts
|
|
4061
|
+
var FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
4062
|
+
// npm/yarn/pnpm
|
|
4063
|
+
"--registry",
|
|
4064
|
+
"--save-prefix",
|
|
4065
|
+
"--tag",
|
|
4066
|
+
"--cache",
|
|
4067
|
+
"--prefix",
|
|
4068
|
+
// pip
|
|
4069
|
+
"-r",
|
|
4070
|
+
"--requirement",
|
|
4071
|
+
"-c",
|
|
4072
|
+
"--constraint",
|
|
4073
|
+
"-e",
|
|
4074
|
+
"--editable",
|
|
4075
|
+
"-t",
|
|
4076
|
+
"--target",
|
|
4077
|
+
"--index-url",
|
|
4078
|
+
"-i",
|
|
4079
|
+
"--extra-index-url",
|
|
4080
|
+
"--find-links",
|
|
4081
|
+
"-f",
|
|
4082
|
+
"--root",
|
|
4083
|
+
"--prefix",
|
|
4084
|
+
// cargo
|
|
4085
|
+
"--git",
|
|
4086
|
+
"--branch",
|
|
4087
|
+
"--rev",
|
|
4088
|
+
"--path",
|
|
4089
|
+
"--version"
|
|
4090
|
+
]);
|
|
4091
|
+
var CUSTOM_REGISTRY_FLAGS = /* @__PURE__ */ new Set(["--registry", "--index-url", "-i", "--extra-index-url"]);
|
|
4092
|
+
var LOCKFILE_PATTERNS = [
|
|
4093
|
+
/\bnpm\s+ci\b/,
|
|
4094
|
+
/\bpnpm\s+install\s+--frozen-lockfile\b/,
|
|
4095
|
+
/\byarn\s+install\s+--frozen-lockfile\b/,
|
|
4096
|
+
/\bpip\s+install\b.*--require-hashes\b/
|
|
4097
|
+
];
|
|
4098
|
+
var MANAGER_MATCHERS = [
|
|
4099
|
+
{ manager: "npm", ecosystem: "npm", subcommandPattern: /\bnpm\s+(install|i|add)\s/ },
|
|
4100
|
+
{ manager: "yarn", ecosystem: "npm", subcommandPattern: /\byarn\s+(add|install)\s/ },
|
|
4101
|
+
{ manager: "pnpm", ecosystem: "npm", subcommandPattern: /\bpnpm\s+(add|install|i)\s/ },
|
|
4102
|
+
{ manager: "pip", ecosystem: "pypi", subcommandPattern: /\bpip\s+install\s/ },
|
|
4103
|
+
{ manager: "cargo", ecosystem: "crates.io", subcommandPattern: /\bcargo\s+(add|install)\s/ }
|
|
4104
|
+
];
|
|
4105
|
+
function splitVersionFromName(raw, ecosystem) {
|
|
4106
|
+
if (ecosystem === "npm") {
|
|
4107
|
+
const atIdx = raw.lastIndexOf("@");
|
|
4108
|
+
if (atIdx > 0) {
|
|
4109
|
+
return { name: raw.slice(0, atIdx), version: raw.slice(atIdx + 1) };
|
|
4110
|
+
}
|
|
4111
|
+
return { name: raw };
|
|
4112
|
+
}
|
|
4113
|
+
if (ecosystem === "pypi") {
|
|
4114
|
+
const match = raw.match(/^([a-zA-Z0-9_.-]+)([=<>!~]+.+)?$/);
|
|
4115
|
+
if (match) {
|
|
4116
|
+
return { name: match[1], version: match[2] };
|
|
4117
|
+
}
|
|
4118
|
+
return { name: raw };
|
|
4119
|
+
}
|
|
4120
|
+
if (ecosystem === "crates.io") {
|
|
4121
|
+
const atIdx = raw.indexOf("@");
|
|
4122
|
+
if (atIdx > 0) {
|
|
4123
|
+
return { name: raw.slice(0, atIdx), version: raw.slice(atIdx + 1) };
|
|
4124
|
+
}
|
|
4125
|
+
return { name: raw };
|
|
4126
|
+
}
|
|
4127
|
+
return { name: raw };
|
|
4128
|
+
}
|
|
4129
|
+
function parseInstallCommand(command) {
|
|
4130
|
+
const trimmed = command.trim();
|
|
4131
|
+
const isLockfileInstall = LOCKFILE_PATTERNS.some((p) => p.test(trimmed));
|
|
4132
|
+
if (/\bnpm\s+ci\b/.test(trimmed)) {
|
|
4133
|
+
return {
|
|
4134
|
+
manager: "npm",
|
|
4135
|
+
packages: [],
|
|
4136
|
+
flags: [],
|
|
4137
|
+
raw: trimmed,
|
|
4138
|
+
isLockfileInstall: true,
|
|
4139
|
+
usesCustomRegistry: false
|
|
4140
|
+
};
|
|
4141
|
+
}
|
|
4142
|
+
for (const matcher of MANAGER_MATCHERS) {
|
|
4143
|
+
if (!matcher.subcommandPattern.test(trimmed)) continue;
|
|
4144
|
+
const subMatch = trimmed.match(matcher.subcommandPattern);
|
|
4145
|
+
if (!subMatch) continue;
|
|
4146
|
+
const afterSubcommand = trimmed.slice(subMatch.index + subMatch[0].length);
|
|
4147
|
+
const tokens = tokenize(afterSubcommand);
|
|
4148
|
+
const packages = [];
|
|
4149
|
+
const flags = [];
|
|
4150
|
+
let usesCustomRegistry = false;
|
|
4151
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
4152
|
+
const token = tokens[i];
|
|
4153
|
+
if (token.startsWith("-")) {
|
|
4154
|
+
flags.push(token);
|
|
4155
|
+
const flagName = token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
|
|
4156
|
+
if (CUSTOM_REGISTRY_FLAGS.has(flagName)) {
|
|
4157
|
+
usesCustomRegistry = true;
|
|
4158
|
+
}
|
|
4159
|
+
if (FLAGS_WITH_VALUE.has(flagName) || token.includes("=")) {
|
|
4160
|
+
if (!token.includes("=")) i++;
|
|
4161
|
+
}
|
|
4162
|
+
continue;
|
|
4163
|
+
}
|
|
4164
|
+
if (matcher.manager === "pip" && (token.endsWith(".txt") || token.endsWith(".cfg"))) {
|
|
4165
|
+
continue;
|
|
4166
|
+
}
|
|
4167
|
+
if (matcher.manager === "pip" && (token.startsWith("/") || token.startsWith("./") || token.startsWith("http://") || token.startsWith("https://") || token.startsWith("git+"))) {
|
|
4168
|
+
continue;
|
|
4169
|
+
}
|
|
4170
|
+
const { name, version } = splitVersionFromName(token, matcher.ecosystem);
|
|
4171
|
+
if (name) {
|
|
4172
|
+
packages.push({ name, version, ecosystem: matcher.ecosystem });
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
return {
|
|
4176
|
+
manager: matcher.manager,
|
|
4177
|
+
packages,
|
|
4178
|
+
flags,
|
|
4179
|
+
raw: trimmed,
|
|
4180
|
+
isLockfileInstall,
|
|
4181
|
+
usesCustomRegistry
|
|
4182
|
+
};
|
|
4183
|
+
}
|
|
4184
|
+
return void 0;
|
|
4185
|
+
}
|
|
4186
|
+
function tokenize(input) {
|
|
4187
|
+
const tokens = [];
|
|
4188
|
+
let current = "";
|
|
4189
|
+
let inSingleQuote = false;
|
|
4190
|
+
let inDoubleQuote = false;
|
|
4191
|
+
for (let i = 0; i < input.length; i++) {
|
|
4192
|
+
const ch = input[i];
|
|
4193
|
+
if (ch === "'" && !inDoubleQuote) {
|
|
4194
|
+
inSingleQuote = !inSingleQuote;
|
|
4195
|
+
continue;
|
|
4196
|
+
}
|
|
4197
|
+
if (ch === '"' && !inSingleQuote) {
|
|
4198
|
+
inDoubleQuote = !inDoubleQuote;
|
|
4199
|
+
continue;
|
|
4200
|
+
}
|
|
4201
|
+
if (ch === " " && !inSingleQuote && !inDoubleQuote) {
|
|
4202
|
+
if (current) tokens.push(current);
|
|
4203
|
+
current = "";
|
|
4204
|
+
} else {
|
|
4205
|
+
current += ch;
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
if (current) tokens.push(current);
|
|
4209
|
+
return tokens;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
// src/lib/deps/registry.ts
|
|
4213
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
4214
|
+
function withTimeout(ms) {
|
|
4215
|
+
return AbortSignal.timeout(ms);
|
|
4216
|
+
}
|
|
4217
|
+
async function fetchNpmMetadata(name) {
|
|
4218
|
+
const encodedName = name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
|
|
4219
|
+
const url = `https://registry.npmjs.org/${encodedName}`;
|
|
4220
|
+
let res;
|
|
4221
|
+
try {
|
|
4222
|
+
res = await fetch(url, { signal: withTimeout(REQUEST_TIMEOUT_MS) });
|
|
4223
|
+
} catch {
|
|
4224
|
+
return notFound(name, "npm");
|
|
4225
|
+
}
|
|
4226
|
+
if (!res.ok) return notFound(name, "npm");
|
|
4227
|
+
const data = await res.json();
|
|
4228
|
+
const latest = data["dist-tags"]?.["latest"];
|
|
4229
|
+
const latestVersion = latest ? data.versions?.[latest] : void 0;
|
|
4230
|
+
const scripts = latestVersion?.scripts ?? {};
|
|
4231
|
+
const hasInstallScripts = !!(scripts["preinstall"] || scripts["install"] || scripts["postinstall"]);
|
|
4232
|
+
let weeklyDownloads;
|
|
4233
|
+
try {
|
|
4234
|
+
const dlUrl = `https://api.npmjs.org/downloads/point/last-week/${encodedName}`;
|
|
4235
|
+
const dlRes = await fetch(dlUrl, { signal: withTimeout(REQUEST_TIMEOUT_MS) });
|
|
4236
|
+
if (dlRes.ok) {
|
|
4237
|
+
const dlData = await dlRes.json();
|
|
4238
|
+
weeklyDownloads = dlData.downloads;
|
|
4239
|
+
}
|
|
4240
|
+
} catch {
|
|
4241
|
+
}
|
|
4242
|
+
return {
|
|
4243
|
+
name,
|
|
4244
|
+
ecosystem: "npm",
|
|
4245
|
+
exists: true,
|
|
4246
|
+
createdAt: data.time?.["created"] ? new Date(data.time["created"]) : void 0,
|
|
4247
|
+
modifiedAt: data.time?.["modified"] ? new Date(data.time["modified"]) : void 0,
|
|
4248
|
+
latestVersion: latest,
|
|
4249
|
+
weeklyDownloads,
|
|
4250
|
+
maintainerCount: data.maintainers?.length,
|
|
4251
|
+
hasRepository: !!data.repository,
|
|
4252
|
+
hasReadme: !!(data.readme && data.readme.length > 10),
|
|
4253
|
+
hasInstallScripts,
|
|
4254
|
+
description: data.description,
|
|
4255
|
+
license: data.license
|
|
4256
|
+
};
|
|
4257
|
+
}
|
|
4258
|
+
async function fetchPyPIMetadata(name) {
|
|
4259
|
+
const url = `https://pypi.org/pypi/${encodeURIComponent(name)}/json`;
|
|
4260
|
+
let res;
|
|
4261
|
+
try {
|
|
4262
|
+
res = await fetch(url, { signal: withTimeout(REQUEST_TIMEOUT_MS) });
|
|
4263
|
+
} catch {
|
|
4264
|
+
return notFound(name, "pypi");
|
|
4265
|
+
}
|
|
4266
|
+
if (!res.ok) return notFound(name, "pypi");
|
|
4267
|
+
const data = await res.json();
|
|
4268
|
+
let earliest;
|
|
4269
|
+
for (const files of Object.values(data.releases)) {
|
|
4270
|
+
for (const file of files) {
|
|
4271
|
+
const d = new Date(file.upload_time);
|
|
4272
|
+
if (!earliest || d < earliest) earliest = d;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
const projectUrls = data.info.project_urls ?? {};
|
|
4276
|
+
const hasRepo = !!(data.info.home_page || projectUrls["Source"] || projectUrls["Repository"] || projectUrls["GitHub"] || projectUrls["Homepage"]);
|
|
4277
|
+
const hasMaintainer = !!(data.info.maintainer || data.info.author);
|
|
4278
|
+
let weeklyDownloads;
|
|
4279
|
+
try {
|
|
4280
|
+
const statsUrl = `https://pypistats.org/api/packages/${encodeURIComponent(name)}/recent`;
|
|
4281
|
+
const statsRes = await fetch(statsUrl, {
|
|
4282
|
+
signal: withTimeout(REQUEST_TIMEOUT_MS),
|
|
4283
|
+
headers: { Accept: "application/json" }
|
|
4284
|
+
});
|
|
4285
|
+
if (statsRes.ok) {
|
|
4286
|
+
const statsData = await statsRes.json();
|
|
4287
|
+
weeklyDownloads = statsData.data?.last_week;
|
|
4288
|
+
}
|
|
4289
|
+
} catch {
|
|
4290
|
+
}
|
|
4291
|
+
return {
|
|
4292
|
+
name,
|
|
4293
|
+
ecosystem: "pypi",
|
|
4294
|
+
exists: true,
|
|
4295
|
+
createdAt: earliest,
|
|
4296
|
+
modifiedAt: data.urls.length > 0 ? new Date(data.urls[data.urls.length - 1].upload_time) : void 0,
|
|
4297
|
+
latestVersion: data.info.version,
|
|
4298
|
+
weeklyDownloads,
|
|
4299
|
+
maintainerCount: hasMaintainer ? 1 : 0,
|
|
4300
|
+
hasRepository: hasRepo,
|
|
4301
|
+
hasReadme: !!(data.info.description && data.info.description.length > 10),
|
|
4302
|
+
hasInstallScripts: false,
|
|
4303
|
+
// PyPI doesn't have post-install scripts in the same way
|
|
4304
|
+
description: data.info.summary,
|
|
4305
|
+
license: data.info.license
|
|
4306
|
+
};
|
|
4307
|
+
}
|
|
4308
|
+
function notFound(name, ecosystem) {
|
|
4309
|
+
return {
|
|
4310
|
+
name,
|
|
4311
|
+
ecosystem,
|
|
4312
|
+
exists: false,
|
|
4313
|
+
hasRepository: false,
|
|
4314
|
+
hasReadme: false,
|
|
4315
|
+
hasInstallScripts: false
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
var cache = /* @__PURE__ */ new Map();
|
|
4319
|
+
var MAX_CACHE_SIZE = 200;
|
|
4320
|
+
function cacheKey(name, ecosystem) {
|
|
4321
|
+
return `${ecosystem}:${name}`;
|
|
4322
|
+
}
|
|
4323
|
+
async function fetchRegistryMetadata(name, ecosystem) {
|
|
4324
|
+
const key = cacheKey(name, ecosystem);
|
|
4325
|
+
const cached = cache.get(key);
|
|
4326
|
+
if (cached) return cached;
|
|
4327
|
+
let result;
|
|
4328
|
+
switch (ecosystem) {
|
|
4329
|
+
case "npm":
|
|
4330
|
+
result = await fetchNpmMetadata(name);
|
|
4331
|
+
break;
|
|
4332
|
+
case "pypi":
|
|
4333
|
+
result = await fetchPyPIMetadata(name);
|
|
4334
|
+
break;
|
|
4335
|
+
default:
|
|
4336
|
+
result = notFound(name, ecosystem);
|
|
4337
|
+
}
|
|
4338
|
+
if (cache.size >= MAX_CACHE_SIZE) {
|
|
4339
|
+
const firstKey = cache.keys().next().value;
|
|
4340
|
+
if (firstKey !== void 0) cache.delete(firstKey);
|
|
4341
|
+
}
|
|
4342
|
+
cache.set(key, result);
|
|
4343
|
+
return result;
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4346
|
+
// src/lib/deps/vulnerabilities.ts
|
|
4347
|
+
var REQUEST_TIMEOUT_MS2 = 5e3;
|
|
4348
|
+
function osvEcosystem(ecosystem) {
|
|
4349
|
+
switch (ecosystem) {
|
|
4350
|
+
case "npm":
|
|
4351
|
+
return "npm";
|
|
4352
|
+
case "pypi":
|
|
4353
|
+
return "PyPI";
|
|
4354
|
+
case "crates.io":
|
|
4355
|
+
return "crates.io";
|
|
4356
|
+
default:
|
|
4357
|
+
return ecosystem;
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
function parseSeverity(vuln) {
|
|
4361
|
+
const sevEntry = vuln.severity?.find((s) => s.type === "CVSS_V3");
|
|
4362
|
+
if (!sevEntry) return "medium";
|
|
4363
|
+
const scoreStr = sevEntry.score;
|
|
4364
|
+
const numericMatch = scoreStr.match(/(\d+\.\d+)/);
|
|
4365
|
+
if (numericMatch) {
|
|
4366
|
+
const score = parseFloat(numericMatch[1]);
|
|
4367
|
+
if (score >= 9) return "critical";
|
|
4368
|
+
if (score >= 7) return "high";
|
|
4369
|
+
if (score >= 4) return "medium";
|
|
4370
|
+
return "low";
|
|
4371
|
+
}
|
|
4372
|
+
return "medium";
|
|
4373
|
+
}
|
|
4374
|
+
function extractFixedVersion(vuln) {
|
|
4375
|
+
for (const affected of vuln.affected ?? []) {
|
|
4376
|
+
for (const range of affected.ranges ?? []) {
|
|
4377
|
+
for (const event of range.events ?? []) {
|
|
4378
|
+
if (event.fixed) return event.fixed;
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
return void 0;
|
|
4383
|
+
}
|
|
4384
|
+
function mapVuln(vuln) {
|
|
4385
|
+
return {
|
|
4386
|
+
id: vuln.id,
|
|
4387
|
+
summary: vuln.summary ?? vuln.details?.slice(0, 200) ?? "No description",
|
|
4388
|
+
severity: parseSeverity(vuln),
|
|
4389
|
+
fixedIn: extractFixedVersion(vuln),
|
|
4390
|
+
aliases: vuln.aliases ?? []
|
|
4391
|
+
};
|
|
4392
|
+
}
|
|
4393
|
+
async function queryVulnerabilities(name, ecosystem, version) {
|
|
4394
|
+
const body = {
|
|
4395
|
+
package: { name, ecosystem: osvEcosystem(ecosystem) }
|
|
4396
|
+
};
|
|
4397
|
+
if (version) body.version = version;
|
|
4398
|
+
try {
|
|
4399
|
+
const res = await fetch("https://api.osv.dev/v1/query", {
|
|
4400
|
+
method: "POST",
|
|
4401
|
+
headers: { "Content-Type": "application/json" },
|
|
4402
|
+
body: JSON.stringify(body),
|
|
4403
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
|
|
4404
|
+
});
|
|
4405
|
+
if (!res.ok) {
|
|
4406
|
+
return { package: name, ecosystem, vulnerabilities: [], error: `OSV HTTP ${res.status}` };
|
|
4407
|
+
}
|
|
4408
|
+
const data = await res.json();
|
|
4409
|
+
return {
|
|
4410
|
+
package: name,
|
|
4411
|
+
ecosystem,
|
|
4412
|
+
vulnerabilities: (data.vulns ?? []).map(mapVuln)
|
|
4413
|
+
};
|
|
4414
|
+
} catch (err) {
|
|
4415
|
+
return {
|
|
4416
|
+
package: name,
|
|
4417
|
+
ecosystem,
|
|
4418
|
+
vulnerabilities: [],
|
|
4419
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
4420
|
+
};
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
async function queryVulnerabilitiesBatch(packages) {
|
|
4424
|
+
if (packages.length === 0) return [];
|
|
4425
|
+
if (packages.length === 1) {
|
|
4426
|
+
const pkg = packages[0];
|
|
4427
|
+
return [await queryVulnerabilities(pkg.name, pkg.ecosystem, pkg.version)];
|
|
4428
|
+
}
|
|
4429
|
+
const queries = packages.map((pkg) => {
|
|
4430
|
+
const q = {
|
|
4431
|
+
package: { name: pkg.name, ecosystem: osvEcosystem(pkg.ecosystem) }
|
|
4432
|
+
};
|
|
4433
|
+
if (pkg.version) q.version = pkg.version;
|
|
4434
|
+
return q;
|
|
4435
|
+
});
|
|
4436
|
+
try {
|
|
4437
|
+
const res = await fetch("https://api.osv.dev/v1/querybatch", {
|
|
4438
|
+
method: "POST",
|
|
4439
|
+
headers: { "Content-Type": "application/json" },
|
|
4440
|
+
body: JSON.stringify({ queries }),
|
|
4441
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
|
|
4442
|
+
});
|
|
4443
|
+
if (!res.ok) {
|
|
4444
|
+
return packages.map((pkg) => ({
|
|
4445
|
+
package: pkg.name,
|
|
4446
|
+
ecosystem: pkg.ecosystem,
|
|
4447
|
+
vulnerabilities: [],
|
|
4448
|
+
error: `OSV HTTP ${res.status}`
|
|
4449
|
+
}));
|
|
4450
|
+
}
|
|
4451
|
+
const data = await res.json();
|
|
4452
|
+
return data.results.map((result, i) => ({
|
|
4453
|
+
package: packages[i].name,
|
|
4454
|
+
ecosystem: packages[i].ecosystem,
|
|
4455
|
+
vulnerabilities: (result.vulns ?? []).map(mapVuln)
|
|
4456
|
+
}));
|
|
4457
|
+
} catch (err) {
|
|
4458
|
+
const errMsg = err instanceof Error ? err.message : "Unknown error";
|
|
4459
|
+
return packages.map((pkg) => ({
|
|
4460
|
+
package: pkg.name,
|
|
4461
|
+
ecosystem: pkg.ecosystem,
|
|
4462
|
+
vulnerabilities: [],
|
|
4463
|
+
error: errMsg
|
|
4464
|
+
}));
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
// src/lib/deps/levenshtein.ts
|
|
4469
|
+
function levenshteinDistance(a, b) {
|
|
4470
|
+
if (a === b) return 0;
|
|
4471
|
+
if (a.length === 0) return b.length;
|
|
4472
|
+
if (b.length === 0) return a.length;
|
|
4473
|
+
if (a.length > b.length) [a, b] = [b, a];
|
|
4474
|
+
const m = a.length;
|
|
4475
|
+
const n = b.length;
|
|
4476
|
+
const row = new Array(m + 1);
|
|
4477
|
+
for (let j = 0; j <= m; j++) row[j] = j;
|
|
4478
|
+
for (let i = 1; i <= n; i++) {
|
|
4479
|
+
let corner = row[0];
|
|
4480
|
+
row[0] = i;
|
|
4481
|
+
for (let j = 1; j <= m; j++) {
|
|
4482
|
+
const temp = row[j];
|
|
4483
|
+
if (b[i - 1] === a[j - 1]) {
|
|
4484
|
+
row[j] = corner;
|
|
4485
|
+
} else {
|
|
4486
|
+
row[j] = 1 + Math.min(corner, temp, row[j - 1]);
|
|
4487
|
+
}
|
|
4488
|
+
corner = temp;
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
return row[m];
|
|
4492
|
+
}
|
|
4493
|
+
function normalizedSimilarity(a, b) {
|
|
4494
|
+
const maxLen = Math.max(a.length, b.length);
|
|
4495
|
+
if (maxLen === 0) return 1;
|
|
4496
|
+
return 1 - levenshteinDistance(a, b) / maxLen;
|
|
4497
|
+
}
|
|
4498
|
+
function normalizeName(name) {
|
|
4499
|
+
const stripped = name.replace(/^@[^/]+\//, "");
|
|
4500
|
+
return stripped.replace(/[-_.]/g, "").toLowerCase();
|
|
4501
|
+
}
|
|
4502
|
+
function detectTyposquat(name, corpus) {
|
|
4503
|
+
const normalized = normalizeName(name);
|
|
4504
|
+
let bestMatch;
|
|
4505
|
+
let bestDistance = Infinity;
|
|
4506
|
+
for (const target of corpus) {
|
|
4507
|
+
const normalizedTarget = normalizeName(target);
|
|
4508
|
+
if (normalizedTarget === normalized) return void 0;
|
|
4509
|
+
const distance = levenshteinDistance(normalized, normalizedTarget);
|
|
4510
|
+
const similarity = normalizedSimilarity(normalized, normalizedTarget);
|
|
4511
|
+
const shouldFlag = distance === 1 || distance === 2 && normalized.length >= 5 || similarity >= 0.85;
|
|
4512
|
+
if (shouldFlag && distance < bestDistance) {
|
|
4513
|
+
bestDistance = distance;
|
|
4514
|
+
bestMatch = { target, distance, similarity };
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
return bestMatch;
|
|
4518
|
+
}
|
|
4519
|
+
|
|
4520
|
+
// src/lib/deps/allowlist.ts
|
|
4521
|
+
var DEFAULT_NPM_ALLOWLIST = [
|
|
4522
|
+
// Frameworks
|
|
4523
|
+
"express",
|
|
4524
|
+
"react",
|
|
4525
|
+
"react-dom",
|
|
4526
|
+
"next",
|
|
4527
|
+
"vue",
|
|
4528
|
+
"angular",
|
|
4529
|
+
"svelte",
|
|
4530
|
+
"fastify",
|
|
4531
|
+
"koa",
|
|
4532
|
+
"hapi",
|
|
4533
|
+
"nest",
|
|
4534
|
+
"nuxt",
|
|
4535
|
+
// Build tools
|
|
4536
|
+
"typescript",
|
|
4537
|
+
"webpack",
|
|
4538
|
+
"vite",
|
|
4539
|
+
"esbuild",
|
|
4540
|
+
"tsup",
|
|
4541
|
+
"rollup",
|
|
4542
|
+
"parcel",
|
|
4543
|
+
"babel",
|
|
4544
|
+
"swc",
|
|
4545
|
+
// Quality tools
|
|
4546
|
+
"prettier",
|
|
4547
|
+
"eslint",
|
|
4548
|
+
"vitest",
|
|
4549
|
+
"jest",
|
|
4550
|
+
"mocha",
|
|
4551
|
+
"chai",
|
|
4552
|
+
"sinon",
|
|
4553
|
+
"husky",
|
|
4554
|
+
"lint-staged",
|
|
4555
|
+
// Utilities
|
|
4556
|
+
"lodash",
|
|
4557
|
+
"axios",
|
|
4558
|
+
"chalk",
|
|
4559
|
+
"commander",
|
|
4560
|
+
"debug",
|
|
4561
|
+
"async",
|
|
4562
|
+
"uuid",
|
|
4563
|
+
"semver",
|
|
4564
|
+
"glob",
|
|
4565
|
+
"fs-extra",
|
|
4566
|
+
"mkdirp",
|
|
4567
|
+
"rimraf",
|
|
4568
|
+
"minimist",
|
|
4569
|
+
"yargs",
|
|
4570
|
+
"dotenv",
|
|
4571
|
+
"nanoid",
|
|
4572
|
+
"date-fns",
|
|
4573
|
+
"dayjs",
|
|
4574
|
+
"moment",
|
|
4575
|
+
"rxjs",
|
|
4576
|
+
"immer",
|
|
4577
|
+
// Validation
|
|
4578
|
+
"zod",
|
|
4579
|
+
"joi",
|
|
4580
|
+
"ajv",
|
|
4581
|
+
"yup",
|
|
4582
|
+
// HTTP / API
|
|
4583
|
+
"cors",
|
|
4584
|
+
"body-parser",
|
|
4585
|
+
"cookie-parser",
|
|
4586
|
+
"morgan",
|
|
4587
|
+
"helmet",
|
|
4588
|
+
"jsonwebtoken",
|
|
4589
|
+
"bcrypt",
|
|
4590
|
+
"passport",
|
|
4591
|
+
// Database
|
|
4592
|
+
"mongoose",
|
|
4593
|
+
"sequelize",
|
|
4594
|
+
"prisma",
|
|
4595
|
+
"typeorm",
|
|
4596
|
+
"knex",
|
|
4597
|
+
"pg",
|
|
4598
|
+
"mysql2",
|
|
4599
|
+
"redis",
|
|
4600
|
+
"ioredis",
|
|
4601
|
+
"better-sqlite3",
|
|
4602
|
+
// Realtime
|
|
4603
|
+
"socket.io",
|
|
4604
|
+
"ws",
|
|
4605
|
+
// State management
|
|
4606
|
+
"zustand",
|
|
4607
|
+
"redux",
|
|
4608
|
+
"mobx",
|
|
4609
|
+
// CSS
|
|
4610
|
+
"tailwindcss",
|
|
4611
|
+
"postcss",
|
|
4612
|
+
"autoprefixer",
|
|
4613
|
+
"sass",
|
|
4614
|
+
"styled-components",
|
|
4615
|
+
"clsx",
|
|
4616
|
+
"classnames",
|
|
4617
|
+
// Media
|
|
4618
|
+
"sharp",
|
|
4619
|
+
"puppeteer",
|
|
4620
|
+
"cheerio",
|
|
4621
|
+
"marked",
|
|
4622
|
+
"highlight.js",
|
|
4623
|
+
// Types
|
|
4624
|
+
"tslib"
|
|
4625
|
+
];
|
|
4626
|
+
var DEFAULT_PYPI_ALLOWLIST = [
|
|
4627
|
+
// Web
|
|
4628
|
+
"requests",
|
|
4629
|
+
"flask",
|
|
4630
|
+
"django",
|
|
4631
|
+
"fastapi",
|
|
4632
|
+
"aiohttp",
|
|
4633
|
+
"httpx",
|
|
4634
|
+
"uvicorn",
|
|
4635
|
+
"gunicorn",
|
|
4636
|
+
"starlette",
|
|
4637
|
+
// Data
|
|
4638
|
+
"numpy",
|
|
4639
|
+
"pandas",
|
|
4640
|
+
"scipy",
|
|
4641
|
+
"matplotlib",
|
|
4642
|
+
"scikit-learn",
|
|
4643
|
+
"pillow",
|
|
4644
|
+
// Infrastructure
|
|
4645
|
+
"boto3",
|
|
4646
|
+
"botocore",
|
|
4647
|
+
"urllib3",
|
|
4648
|
+
"certifi",
|
|
4649
|
+
"charset-normalizer",
|
|
4650
|
+
"idna",
|
|
4651
|
+
// Core
|
|
4652
|
+
"setuptools",
|
|
4653
|
+
"pip",
|
|
4654
|
+
"wheel",
|
|
4655
|
+
"packaging",
|
|
4656
|
+
"typing-extensions",
|
|
4657
|
+
"six",
|
|
4658
|
+
"python-dateutil",
|
|
4659
|
+
"pyyaml",
|
|
4660
|
+
"tomli",
|
|
4661
|
+
"filelock",
|
|
4662
|
+
"attrs",
|
|
4663
|
+
"click",
|
|
4664
|
+
"importlib-metadata",
|
|
4665
|
+
"zipp",
|
|
4666
|
+
"platformdirs",
|
|
4667
|
+
// Crypto
|
|
4668
|
+
"cryptography",
|
|
4669
|
+
"cffi",
|
|
4670
|
+
"pycparser",
|
|
4671
|
+
"jmespath",
|
|
4672
|
+
"pyasn1",
|
|
4673
|
+
// DB
|
|
4674
|
+
"sqlalchemy",
|
|
4675
|
+
"psycopg2",
|
|
4676
|
+
"redis",
|
|
4677
|
+
"celery",
|
|
4678
|
+
// Template / Markup
|
|
4679
|
+
"jinja2",
|
|
4680
|
+
"markupsafe",
|
|
4681
|
+
"beautifulsoup4",
|
|
4682
|
+
"lxml",
|
|
4683
|
+
"scrapy",
|
|
4684
|
+
// Validation
|
|
4685
|
+
"pydantic",
|
|
4686
|
+
// Testing
|
|
4687
|
+
"pytest",
|
|
4688
|
+
"tox",
|
|
4689
|
+
"coverage",
|
|
4690
|
+
"mock"
|
|
4691
|
+
];
|
|
4692
|
+
var DEFAULT_BLOCKLIST_EXACT = [
|
|
4693
|
+
// npm typosquats (historically malicious)
|
|
4694
|
+
"crossenv",
|
|
4695
|
+
"cross-env.js",
|
|
4696
|
+
"d3.js",
|
|
4697
|
+
"fabric-js",
|
|
4698
|
+
"ffmpegs",
|
|
4699
|
+
"gruntcli",
|
|
4700
|
+
"http-proxy.js",
|
|
4701
|
+
"jquery.js",
|
|
4702
|
+
"mongose",
|
|
4703
|
+
"mssql-node",
|
|
4704
|
+
"nodecaffe",
|
|
4705
|
+
"nodefabric",
|
|
4706
|
+
"nodemailer-js",
|
|
4707
|
+
"noderequest",
|
|
4708
|
+
"nodesass",
|
|
4709
|
+
"opencv.js",
|
|
4710
|
+
"openssl.js",
|
|
4711
|
+
"shadowsock",
|
|
4712
|
+
"sqliter",
|
|
4713
|
+
"sqlserver",
|
|
4714
|
+
// npm compromised
|
|
4715
|
+
"flatmap-stream",
|
|
4716
|
+
// PyPI typosquats
|
|
4717
|
+
"colourama",
|
|
4718
|
+
"python3-dateutil",
|
|
4719
|
+
"requesocks",
|
|
4720
|
+
"requesst",
|
|
4721
|
+
"beautifulsup",
|
|
4722
|
+
"numppy",
|
|
4723
|
+
"numpys",
|
|
4724
|
+
"djanga",
|
|
4725
|
+
"urlib3"
|
|
4726
|
+
];
|
|
4727
|
+
var DEFAULT_BLOCKLIST_PATTERNS = [
|
|
4728
|
+
/-free-download$/,
|
|
4729
|
+
/-crack$/,
|
|
4730
|
+
/-keygen$/,
|
|
4731
|
+
/-license-key$/,
|
|
4732
|
+
/-hack$/,
|
|
4733
|
+
/-serial$/,
|
|
4734
|
+
/-activation$/,
|
|
4735
|
+
/-premium-free$/,
|
|
4736
|
+
/-generator-free$/
|
|
4737
|
+
];
|
|
4738
|
+
function buildAllowBlockLists(ecosystem, userAllowlist, userBlocklist, userBlocklistPatterns) {
|
|
4739
|
+
const base = ecosystem === "npm" ? DEFAULT_NPM_ALLOWLIST : ecosystem === "pypi" ? DEFAULT_PYPI_ALLOWLIST : [];
|
|
4740
|
+
return {
|
|
4741
|
+
allowlist: [...base, ...userAllowlist],
|
|
4742
|
+
blocklist: [...DEFAULT_BLOCKLIST_EXACT, ...userBlocklist],
|
|
4743
|
+
blocklistPatterns: [
|
|
4744
|
+
...DEFAULT_BLOCKLIST_PATTERNS,
|
|
4745
|
+
...userBlocklistPatterns.map((p) => new RegExp(p))
|
|
4746
|
+
]
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
function isAllowlisted(name, allowlist) {
|
|
4750
|
+
return allowlist.includes(name);
|
|
4751
|
+
}
|
|
4752
|
+
function isBlocklisted(name, blocklist, blocklistPatterns) {
|
|
4753
|
+
if (blocklist.includes(name)) return true;
|
|
4754
|
+
return blocklistPatterns.some((p) => p.test(name));
|
|
4755
|
+
}
|
|
4756
|
+
|
|
4757
|
+
// src/lib/deps/risk.ts
|
|
4758
|
+
var DEFAULT_THRESHOLDS = {
|
|
4759
|
+
minAgeDays: 30,
|
|
4760
|
+
minWeeklyDownloads: 100
|
|
4761
|
+
};
|
|
4762
|
+
var SEVERITY_ORDER2 = {
|
|
4763
|
+
info: 0,
|
|
4764
|
+
low: 1,
|
|
4765
|
+
medium: 2,
|
|
4766
|
+
high: 3,
|
|
4767
|
+
critical: 4
|
|
4768
|
+
};
|
|
4769
|
+
function maxSeverity(a, b) {
|
|
4770
|
+
return SEVERITY_ORDER2[a] >= SEVERITY_ORDER2[b] ? a : b;
|
|
4771
|
+
}
|
|
4772
|
+
function computeRiskReport(metadata, vulns, typosquatMatch, isBlocklisted2, isAllowlisted2, thresholds = DEFAULT_THRESHOLDS) {
|
|
4773
|
+
const signals = [];
|
|
4774
|
+
let overallRisk = "info";
|
|
4775
|
+
if (!metadata.exists) {
|
|
4776
|
+
signals.push({
|
|
4777
|
+
name: "package_not_found",
|
|
4778
|
+
severity: "critical",
|
|
4779
|
+
detail: `Package "${metadata.name}" does not exist on ${metadata.ecosystem}`
|
|
4780
|
+
});
|
|
4781
|
+
overallRisk = "critical";
|
|
4782
|
+
}
|
|
4783
|
+
if (isBlocklisted2) {
|
|
4784
|
+
signals.push({
|
|
4785
|
+
name: "on_blocklist",
|
|
4786
|
+
severity: "critical",
|
|
4787
|
+
detail: `Package "${metadata.name}" is on the blocklist`
|
|
4788
|
+
});
|
|
4789
|
+
overallRisk = "critical";
|
|
4790
|
+
}
|
|
4791
|
+
for (const vuln of vulns) {
|
|
4792
|
+
signals.push({
|
|
4793
|
+
name: "known_vulnerability",
|
|
4794
|
+
severity: vuln.severity,
|
|
4795
|
+
detail: `${vuln.id}: ${vuln.summary}${vuln.fixedIn ? ` (fixed in ${vuln.fixedIn})` : ""}`
|
|
4796
|
+
});
|
|
4797
|
+
overallRisk = maxSeverity(overallRisk, vuln.severity);
|
|
4798
|
+
}
|
|
4799
|
+
if (isAllowlisted2 && signals.length === 0) {
|
|
4800
|
+
return {
|
|
4801
|
+
package: metadata.name,
|
|
4802
|
+
ecosystem: metadata.ecosystem,
|
|
4803
|
+
overallRisk: "info",
|
|
4804
|
+
signals: [],
|
|
4805
|
+
vulnerabilities: vulns,
|
|
4806
|
+
recommendation: "allow",
|
|
4807
|
+
metadata
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
if (!isAllowlisted2 && metadata.exists) {
|
|
4811
|
+
if (metadata.createdAt) {
|
|
4812
|
+
const ageDays = (Date.now() - metadata.createdAt.getTime()) / (1e3 * 60 * 60 * 24);
|
|
4813
|
+
if (ageDays < 7) {
|
|
4814
|
+
signals.push({
|
|
4815
|
+
name: "very_new_package",
|
|
4816
|
+
severity: "high",
|
|
4817
|
+
detail: `Created ${Math.floor(ageDays)} days ago`
|
|
4818
|
+
});
|
|
4819
|
+
overallRisk = maxSeverity(overallRisk, "high");
|
|
4820
|
+
} else if (ageDays < thresholds.minAgeDays) {
|
|
4821
|
+
signals.push({
|
|
4822
|
+
name: "new_package",
|
|
4823
|
+
severity: "medium",
|
|
4824
|
+
detail: `Created ${Math.floor(ageDays)} days ago (threshold: ${thresholds.minAgeDays})`
|
|
4825
|
+
});
|
|
4826
|
+
overallRisk = maxSeverity(overallRisk, "medium");
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
if (metadata.weeklyDownloads !== void 0) {
|
|
4830
|
+
if (metadata.weeklyDownloads < thresholds.minWeeklyDownloads) {
|
|
4831
|
+
const sev = metadata.weeklyDownloads < 10 ? "high" : "medium";
|
|
4832
|
+
signals.push({
|
|
4833
|
+
name: "low_downloads",
|
|
4834
|
+
severity: sev,
|
|
4835
|
+
detail: `${metadata.weeklyDownloads} weekly downloads (threshold: ${thresholds.minWeeklyDownloads})`
|
|
4836
|
+
});
|
|
4837
|
+
overallRisk = maxSeverity(overallRisk, sev);
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
if (typosquatMatch) {
|
|
4841
|
+
signals.push({
|
|
4842
|
+
name: "typosquat_suspect",
|
|
4843
|
+
severity: "high",
|
|
4844
|
+
detail: `Similar to "${typosquatMatch.target}" (edit distance: ${typosquatMatch.distance})`
|
|
4845
|
+
});
|
|
4846
|
+
overallRisk = maxSeverity(overallRisk, "high");
|
|
4847
|
+
}
|
|
4848
|
+
if (metadata.hasInstallScripts) {
|
|
4849
|
+
signals.push({
|
|
4850
|
+
name: "has_install_scripts",
|
|
4851
|
+
severity: "medium",
|
|
4852
|
+
detail: "Package has preinstall/install/postinstall scripts"
|
|
4853
|
+
});
|
|
4854
|
+
overallRisk = maxSeverity(overallRisk, "medium");
|
|
4855
|
+
}
|
|
4856
|
+
if (!metadata.hasRepository) {
|
|
4857
|
+
signals.push({
|
|
4858
|
+
name: "no_repository",
|
|
4859
|
+
severity: "low",
|
|
4860
|
+
detail: "No source repository URL in metadata"
|
|
4861
|
+
});
|
|
4862
|
+
overallRisk = maxSeverity(overallRisk, "low");
|
|
4863
|
+
}
|
|
4864
|
+
if (!metadata.hasReadme) {
|
|
4865
|
+
signals.push({
|
|
4866
|
+
name: "no_readme",
|
|
4867
|
+
severity: "low",
|
|
4868
|
+
detail: "No README content"
|
|
4869
|
+
});
|
|
4870
|
+
overallRisk = maxSeverity(overallRisk, "low");
|
|
4871
|
+
}
|
|
4872
|
+
if (!metadata.license) {
|
|
4873
|
+
signals.push({
|
|
4874
|
+
name: "no_license",
|
|
4875
|
+
severity: "low",
|
|
4876
|
+
detail: "No license declared"
|
|
4877
|
+
});
|
|
4878
|
+
overallRisk = maxSeverity(overallRisk, "low");
|
|
4879
|
+
}
|
|
4880
|
+
if (metadata.maintainerCount !== void 0 && metadata.maintainerCount <= 1) {
|
|
4881
|
+
signals.push({
|
|
4882
|
+
name: "single_maintainer",
|
|
4883
|
+
severity: "info",
|
|
4884
|
+
detail: `${metadata.maintainerCount} maintainer(s)`
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
let recommendation;
|
|
4889
|
+
if (SEVERITY_ORDER2[overallRisk] >= SEVERITY_ORDER2["critical"]) {
|
|
4890
|
+
recommendation = "block";
|
|
4891
|
+
} else if (SEVERITY_ORDER2[overallRisk] >= SEVERITY_ORDER2["medium"]) {
|
|
4892
|
+
recommendation = "escalate";
|
|
4893
|
+
} else {
|
|
4894
|
+
recommendation = "allow";
|
|
4895
|
+
}
|
|
4896
|
+
return {
|
|
4897
|
+
package: metadata.name,
|
|
4898
|
+
ecosystem: metadata.ecosystem,
|
|
4899
|
+
overallRisk,
|
|
4900
|
+
signals,
|
|
4901
|
+
vulnerabilities: vulns,
|
|
4902
|
+
recommendation,
|
|
4903
|
+
metadata
|
|
4904
|
+
};
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
// src/lib/deps/guardian.ts
|
|
4908
|
+
var DEFAULT_CONFIG2 = {
|
|
4909
|
+
enabled: true,
|
|
4910
|
+
checks: {
|
|
4911
|
+
existence: true,
|
|
4912
|
+
reputation: true,
|
|
4913
|
+
typosquatting: true,
|
|
4914
|
+
install_scripts: true,
|
|
4915
|
+
vulnerabilities: true
|
|
4916
|
+
},
|
|
4917
|
+
risk_thresholds: {
|
|
4918
|
+
min_age_days: 30,
|
|
4919
|
+
min_weekly_downloads: 100
|
|
4920
|
+
},
|
|
4921
|
+
on_risk: "escalate",
|
|
4922
|
+
allowlist: [],
|
|
4923
|
+
blocklist: [],
|
|
4924
|
+
blocklist_patterns: [],
|
|
4925
|
+
custom_registry_bypass: true
|
|
4926
|
+
};
|
|
4927
|
+
async function evaluateInstall(command, config = DEFAULT_CONFIG2) {
|
|
4928
|
+
if (!config.enabled) {
|
|
4929
|
+
return skippedResult(command, "Dependency guardian is disabled");
|
|
4930
|
+
}
|
|
4931
|
+
const parsed = parseInstallCommand(command);
|
|
4932
|
+
if (!parsed) {
|
|
4933
|
+
return skippedResult(command, "Not a recognized install command");
|
|
4934
|
+
}
|
|
4935
|
+
if (parsed.isLockfileInstall) {
|
|
4936
|
+
return skippedResult(command, "Lock-file install (pinned dependencies)");
|
|
4937
|
+
}
|
|
4938
|
+
if (config.custom_registry_bypass && parsed.usesCustomRegistry) {
|
|
4939
|
+
return skippedResult(command, "Custom registry detected (bypass enabled)");
|
|
4940
|
+
}
|
|
4941
|
+
if (parsed.packages.length === 0) {
|
|
4942
|
+
return skippedResult(command, "No specific packages to validate");
|
|
4943
|
+
}
|
|
4944
|
+
const ecosystems = [...new Set(parsed.packages.map((p) => p.ecosystem))];
|
|
4945
|
+
const listsPerEcosystem = new Map(
|
|
4946
|
+
ecosystems.map((eco) => [
|
|
4947
|
+
eco,
|
|
4948
|
+
buildAllowBlockLists(eco, config.allowlist, config.blocklist, config.blocklist_patterns)
|
|
4949
|
+
])
|
|
4950
|
+
);
|
|
4951
|
+
const metadataPromises = parsed.packages.map(async (pkg) => {
|
|
4952
|
+
if (!config.checks.existence && !config.checks.reputation && !config.checks.install_scripts) {
|
|
4953
|
+
return void 0;
|
|
4954
|
+
}
|
|
4955
|
+
return fetchRegistryMetadata(pkg.name, pkg.ecosystem);
|
|
4956
|
+
});
|
|
4957
|
+
const vulnPromise = config.checks.vulnerabilities ? queryVulnerabilitiesBatch(
|
|
4958
|
+
parsed.packages.map((p) => ({
|
|
4959
|
+
name: p.name,
|
|
4960
|
+
ecosystem: p.ecosystem,
|
|
4961
|
+
version: p.version
|
|
4962
|
+
}))
|
|
4963
|
+
) : Promise.resolve(
|
|
4964
|
+
parsed.packages.map((p) => ({
|
|
4965
|
+
package: p.name,
|
|
4966
|
+
ecosystem: p.ecosystem,
|
|
4967
|
+
vulnerabilities: []
|
|
4968
|
+
}))
|
|
4969
|
+
);
|
|
4970
|
+
const [metadataResults, vulnResults] = await Promise.all([
|
|
4971
|
+
Promise.all(metadataPromises),
|
|
4972
|
+
vulnPromise
|
|
4973
|
+
]);
|
|
4974
|
+
const reports = [];
|
|
4975
|
+
for (let i = 0; i < parsed.packages.length; i++) {
|
|
4976
|
+
const pkg = parsed.packages[i];
|
|
4977
|
+
const lists = listsPerEcosystem.get(pkg.ecosystem);
|
|
4978
|
+
const allowed = isAllowlisted(pkg.name, lists.allowlist);
|
|
4979
|
+
const blocked = isBlocklisted(pkg.name, lists.blocklist, lists.blocklistPatterns);
|
|
4980
|
+
const metadata = metadataResults[i] ?? {
|
|
4981
|
+
name: pkg.name,
|
|
4982
|
+
ecosystem: pkg.ecosystem,
|
|
4983
|
+
exists: true,
|
|
4984
|
+
// assume exists if we didn't check
|
|
4985
|
+
hasRepository: true,
|
|
4986
|
+
hasReadme: true,
|
|
4987
|
+
hasInstallScripts: false
|
|
4988
|
+
};
|
|
4989
|
+
const vulns = vulnResults[i]?.vulnerabilities ?? [];
|
|
4990
|
+
const typosquatMatch = config.checks.typosquatting && !allowed ? detectTyposquat(pkg.name, lists.allowlist) : void 0;
|
|
4991
|
+
const report = computeRiskReport(metadata, vulns, typosquatMatch, blocked, allowed, {
|
|
4992
|
+
minAgeDays: config.risk_thresholds.min_age_days,
|
|
4993
|
+
minWeeklyDownloads: config.risk_thresholds.min_weekly_downloads
|
|
4994
|
+
});
|
|
4995
|
+
reports.push(report);
|
|
4996
|
+
}
|
|
4997
|
+
let overallRecommendation = "allow";
|
|
4998
|
+
for (const report of reports) {
|
|
4999
|
+
if (report.recommendation === "block") {
|
|
5000
|
+
overallRecommendation = "block";
|
|
5001
|
+
break;
|
|
5002
|
+
}
|
|
5003
|
+
if (report.recommendation === "escalate") {
|
|
5004
|
+
overallRecommendation = "escalate";
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
if (config.on_risk === "audit" && overallRecommendation === "escalate") {
|
|
5008
|
+
overallRecommendation = "allow";
|
|
5009
|
+
}
|
|
5010
|
+
if (config.on_risk === "block" && overallRecommendation === "escalate") {
|
|
5011
|
+
overallRecommendation = "block";
|
|
5012
|
+
}
|
|
5013
|
+
const summary = formatSummary(command, reports, overallRecommendation);
|
|
5014
|
+
return {
|
|
5015
|
+
command,
|
|
5016
|
+
packages: reports,
|
|
5017
|
+
overallRecommendation,
|
|
5018
|
+
summary,
|
|
5019
|
+
auditMetadata: {
|
|
5020
|
+
command,
|
|
5021
|
+
manager: parsed.manager,
|
|
5022
|
+
packages: reports.map((r) => ({
|
|
5023
|
+
name: r.package,
|
|
5024
|
+
ecosystem: r.ecosystem,
|
|
5025
|
+
risk: r.overallRisk,
|
|
5026
|
+
signals: r.signals.map((s) => s.name),
|
|
5027
|
+
vulnCount: r.vulnerabilities.length
|
|
5028
|
+
}))
|
|
5029
|
+
},
|
|
5030
|
+
skipped: false
|
|
5031
|
+
};
|
|
5032
|
+
}
|
|
5033
|
+
function skippedResult(command, reason) {
|
|
5034
|
+
return {
|
|
5035
|
+
command,
|
|
5036
|
+
packages: [],
|
|
5037
|
+
overallRecommendation: "allow",
|
|
5038
|
+
summary: reason,
|
|
5039
|
+
auditMetadata: { command, skipped: true, reason },
|
|
5040
|
+
skipped: true,
|
|
5041
|
+
skipReason: reason
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
5044
|
+
function formatSummary(command, reports, recommendation) {
|
|
5045
|
+
const lines = [];
|
|
5046
|
+
lines.push(`Command: ${command}`);
|
|
5047
|
+
lines.push("");
|
|
5048
|
+
for (const report of reports) {
|
|
5049
|
+
if (report.signals.length === 0 && report.vulnerabilities.length === 0) continue;
|
|
5050
|
+
lines.push(`Package: ${report.package} (${report.ecosystem})`);
|
|
5051
|
+
lines.push(`Risk: ${report.overallRisk.toUpperCase()}`);
|
|
5052
|
+
if (report.signals.length > 0) {
|
|
5053
|
+
lines.push("Signals:");
|
|
5054
|
+
for (const signal of report.signals) {
|
|
5055
|
+
const icon = signal.severity === "critical" || signal.severity === "high" ? "!!" : signal.severity === "medium" ? "! " : " ";
|
|
5056
|
+
lines.push(` ${icon} ${signal.name}: ${signal.detail}`);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
lines.push("");
|
|
5060
|
+
}
|
|
5061
|
+
if (recommendation === "block") {
|
|
5062
|
+
lines.push("Recommendation: BLOCK");
|
|
5063
|
+
} else if (recommendation === "escalate") {
|
|
5064
|
+
lines.push("Recommendation: Requires human approval");
|
|
5065
|
+
}
|
|
5066
|
+
return lines.join("\n");
|
|
5067
|
+
}
|
|
5068
|
+
|
|
4033
5069
|
// src/lib/budget/tracker.ts
|
|
4034
5070
|
var BudgetTracker = class {
|
|
4035
5071
|
_used = 0;
|
|
@@ -5988,7 +7024,15 @@ export {
|
|
|
5988
7024
|
createApprovalFlow,
|
|
5989
7025
|
createIdentityChain,
|
|
5990
7026
|
createPolicyEngine,
|
|
7027
|
+
detectTyposquat,
|
|
7028
|
+
evaluateInstall,
|
|
7029
|
+
fetchRegistryMetadata,
|
|
7030
|
+
levenshteinDistance,
|
|
5991
7031
|
loadConfig,
|
|
7032
|
+
normalizedSimilarity,
|
|
7033
|
+
parseInstallCommand,
|
|
7034
|
+
queryVulnerabilities,
|
|
7035
|
+
queryVulnerabilitiesBatch,
|
|
5992
7036
|
render as renderTemplate,
|
|
5993
7037
|
startWizardServer
|
|
5994
7038
|
};
|