@grwnd/pi-governance 3.0.2 → 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.
@@ -1853,6 +1853,32 @@ var DlpConfig = import_typebox.Type.Object({
1853
1853
  allowlist: import_typebox.Type.Optional(import_typebox.Type.Array(DlpAllowlistEntryConfig)),
1854
1854
  role_overrides: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), DlpRoleOverrideConfig))
1855
1855
  });
1856
+ var DependencyGuardianChecksConfig = import_typebox.Type.Object({
1857
+ existence: import_typebox.Type.Boolean({ default: true }),
1858
+ reputation: import_typebox.Type.Boolean({ default: true }),
1859
+ typosquatting: import_typebox.Type.Boolean({ default: true }),
1860
+ install_scripts: import_typebox.Type.Boolean({ default: true }),
1861
+ vulnerabilities: import_typebox.Type.Boolean({ default: true })
1862
+ });
1863
+ var DependencyGuardianConfig = import_typebox.Type.Object({
1864
+ enabled: import_typebox.Type.Boolean({ default: true }),
1865
+ checks: import_typebox.Type.Optional(DependencyGuardianChecksConfig),
1866
+ risk_thresholds: import_typebox.Type.Optional(
1867
+ import_typebox.Type.Object({
1868
+ min_age_days: import_typebox.Type.Number({ default: 30, minimum: 0 }),
1869
+ min_weekly_downloads: import_typebox.Type.Number({ default: 100, minimum: 0 })
1870
+ })
1871
+ ),
1872
+ on_risk: import_typebox.Type.Optional(
1873
+ import_typebox.Type.Union([import_typebox.Type.Literal("escalate"), import_typebox.Type.Literal("block"), import_typebox.Type.Literal("audit")], {
1874
+ default: "escalate"
1875
+ })
1876
+ ),
1877
+ allowlist: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
1878
+ blocklist: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
1879
+ blocklist_patterns: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
1880
+ custom_registry_bypass: import_typebox.Type.Boolean({ default: true })
1881
+ });
1856
1882
  var GovernanceConfigSchema = import_typebox.Type.Object({
1857
1883
  auth: import_typebox.Type.Optional(AuthConfig),
1858
1884
  policy: import_typebox.Type.Optional(PolicyConfig),
@@ -1860,6 +1886,7 @@ var GovernanceConfigSchema = import_typebox.Type.Object({
1860
1886
  hitl: import_typebox.Type.Optional(HitlConfig),
1861
1887
  audit: import_typebox.Type.Optional(AuditConfig),
1862
1888
  dlp: import_typebox.Type.Optional(DlpConfig),
1889
+ dependency_guardian: import_typebox.Type.Optional(DependencyGuardianConfig),
1863
1890
  org_units: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), OrgUnitOverride))
1864
1891
  });
1865
1892
 
@@ -2870,6 +2897,1015 @@ var DlpMasker = class {
2870
2897
  }
2871
2898
  };
2872
2899
 
2900
+ // src/lib/deps/parser.ts
2901
+ var FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
2902
+ // npm/yarn/pnpm
2903
+ "--registry",
2904
+ "--save-prefix",
2905
+ "--tag",
2906
+ "--cache",
2907
+ "--prefix",
2908
+ // pip
2909
+ "-r",
2910
+ "--requirement",
2911
+ "-c",
2912
+ "--constraint",
2913
+ "-e",
2914
+ "--editable",
2915
+ "-t",
2916
+ "--target",
2917
+ "--index-url",
2918
+ "-i",
2919
+ "--extra-index-url",
2920
+ "--find-links",
2921
+ "-f",
2922
+ "--root",
2923
+ "--prefix",
2924
+ // cargo
2925
+ "--git",
2926
+ "--branch",
2927
+ "--rev",
2928
+ "--path",
2929
+ "--version"
2930
+ ]);
2931
+ var CUSTOM_REGISTRY_FLAGS = /* @__PURE__ */ new Set(["--registry", "--index-url", "-i", "--extra-index-url"]);
2932
+ var LOCKFILE_PATTERNS = [
2933
+ /\bnpm\s+ci\b/,
2934
+ /\bpnpm\s+install\s+--frozen-lockfile\b/,
2935
+ /\byarn\s+install\s+--frozen-lockfile\b/,
2936
+ /\bpip\s+install\b.*--require-hashes\b/
2937
+ ];
2938
+ var MANAGER_MATCHERS = [
2939
+ { manager: "npm", ecosystem: "npm", subcommandPattern: /\bnpm\s+(install|i|add)\s/ },
2940
+ { manager: "yarn", ecosystem: "npm", subcommandPattern: /\byarn\s+(add|install)\s/ },
2941
+ { manager: "pnpm", ecosystem: "npm", subcommandPattern: /\bpnpm\s+(add|install|i)\s/ },
2942
+ { manager: "pip", ecosystem: "pypi", subcommandPattern: /\bpip\s+install\s/ },
2943
+ { manager: "cargo", ecosystem: "crates.io", subcommandPattern: /\bcargo\s+(add|install)\s/ }
2944
+ ];
2945
+ function splitVersionFromName(raw, ecosystem) {
2946
+ if (ecosystem === "npm") {
2947
+ const atIdx = raw.lastIndexOf("@");
2948
+ if (atIdx > 0) {
2949
+ return { name: raw.slice(0, atIdx), version: raw.slice(atIdx + 1) };
2950
+ }
2951
+ return { name: raw };
2952
+ }
2953
+ if (ecosystem === "pypi") {
2954
+ const match = raw.match(/^([a-zA-Z0-9_.-]+)([=<>!~]+.+)?$/);
2955
+ if (match) {
2956
+ return { name: match[1], version: match[2] };
2957
+ }
2958
+ return { name: raw };
2959
+ }
2960
+ if (ecosystem === "crates.io") {
2961
+ const atIdx = raw.indexOf("@");
2962
+ if (atIdx > 0) {
2963
+ return { name: raw.slice(0, atIdx), version: raw.slice(atIdx + 1) };
2964
+ }
2965
+ return { name: raw };
2966
+ }
2967
+ return { name: raw };
2968
+ }
2969
+ function parseInstallCommand(command) {
2970
+ const trimmed = command.trim();
2971
+ const isLockfileInstall = LOCKFILE_PATTERNS.some((p) => p.test(trimmed));
2972
+ if (/\bnpm\s+ci\b/.test(trimmed)) {
2973
+ return {
2974
+ manager: "npm",
2975
+ packages: [],
2976
+ flags: [],
2977
+ raw: trimmed,
2978
+ isLockfileInstall: true,
2979
+ usesCustomRegistry: false
2980
+ };
2981
+ }
2982
+ for (const matcher of MANAGER_MATCHERS) {
2983
+ if (!matcher.subcommandPattern.test(trimmed)) continue;
2984
+ const subMatch = trimmed.match(matcher.subcommandPattern);
2985
+ if (!subMatch) continue;
2986
+ const afterSubcommand = trimmed.slice(subMatch.index + subMatch[0].length);
2987
+ const tokens = tokenize(afterSubcommand);
2988
+ const packages = [];
2989
+ const flags = [];
2990
+ let usesCustomRegistry = false;
2991
+ for (let i = 0; i < tokens.length; i++) {
2992
+ const token = tokens[i];
2993
+ if (token.startsWith("-")) {
2994
+ flags.push(token);
2995
+ const flagName = token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
2996
+ if (CUSTOM_REGISTRY_FLAGS.has(flagName)) {
2997
+ usesCustomRegistry = true;
2998
+ }
2999
+ if (FLAGS_WITH_VALUE.has(flagName) || token.includes("=")) {
3000
+ if (!token.includes("=")) i++;
3001
+ }
3002
+ continue;
3003
+ }
3004
+ if (matcher.manager === "pip" && (token.endsWith(".txt") || token.endsWith(".cfg"))) {
3005
+ continue;
3006
+ }
3007
+ if (matcher.manager === "pip" && (token.startsWith("/") || token.startsWith("./") || token.startsWith("http://") || token.startsWith("https://") || token.startsWith("git+"))) {
3008
+ continue;
3009
+ }
3010
+ const { name, version } = splitVersionFromName(token, matcher.ecosystem);
3011
+ if (name) {
3012
+ packages.push({ name, version, ecosystem: matcher.ecosystem });
3013
+ }
3014
+ }
3015
+ return {
3016
+ manager: matcher.manager,
3017
+ packages,
3018
+ flags,
3019
+ raw: trimmed,
3020
+ isLockfileInstall,
3021
+ usesCustomRegistry
3022
+ };
3023
+ }
3024
+ return void 0;
3025
+ }
3026
+ function tokenize(input) {
3027
+ const tokens = [];
3028
+ let current = "";
3029
+ let inSingleQuote = false;
3030
+ let inDoubleQuote = false;
3031
+ for (let i = 0; i < input.length; i++) {
3032
+ const ch = input[i];
3033
+ if (ch === "'" && !inDoubleQuote) {
3034
+ inSingleQuote = !inSingleQuote;
3035
+ continue;
3036
+ }
3037
+ if (ch === '"' && !inSingleQuote) {
3038
+ inDoubleQuote = !inDoubleQuote;
3039
+ continue;
3040
+ }
3041
+ if (ch === " " && !inSingleQuote && !inDoubleQuote) {
3042
+ if (current) tokens.push(current);
3043
+ current = "";
3044
+ } else {
3045
+ current += ch;
3046
+ }
3047
+ }
3048
+ if (current) tokens.push(current);
3049
+ return tokens;
3050
+ }
3051
+
3052
+ // src/lib/deps/registry.ts
3053
+ var REQUEST_TIMEOUT_MS = 5e3;
3054
+ function withTimeout(ms) {
3055
+ return AbortSignal.timeout(ms);
3056
+ }
3057
+ async function fetchNpmMetadata(name) {
3058
+ const encodedName = name.startsWith("@") ? `@${encodeURIComponent(name.slice(1))}` : encodeURIComponent(name);
3059
+ const url = `https://registry.npmjs.org/${encodedName}`;
3060
+ let res;
3061
+ try {
3062
+ res = await fetch(url, { signal: withTimeout(REQUEST_TIMEOUT_MS) });
3063
+ } catch {
3064
+ return notFound(name, "npm");
3065
+ }
3066
+ if (!res.ok) return notFound(name, "npm");
3067
+ const data = await res.json();
3068
+ const latest = data["dist-tags"]?.["latest"];
3069
+ const latestVersion = latest ? data.versions?.[latest] : void 0;
3070
+ const scripts = latestVersion?.scripts ?? {};
3071
+ const hasInstallScripts = !!(scripts["preinstall"] || scripts["install"] || scripts["postinstall"]);
3072
+ let weeklyDownloads;
3073
+ try {
3074
+ const dlUrl = `https://api.npmjs.org/downloads/point/last-week/${encodedName}`;
3075
+ const dlRes = await fetch(dlUrl, { signal: withTimeout(REQUEST_TIMEOUT_MS) });
3076
+ if (dlRes.ok) {
3077
+ const dlData = await dlRes.json();
3078
+ weeklyDownloads = dlData.downloads;
3079
+ }
3080
+ } catch {
3081
+ }
3082
+ return {
3083
+ name,
3084
+ ecosystem: "npm",
3085
+ exists: true,
3086
+ createdAt: data.time?.["created"] ? new Date(data.time["created"]) : void 0,
3087
+ modifiedAt: data.time?.["modified"] ? new Date(data.time["modified"]) : void 0,
3088
+ latestVersion: latest,
3089
+ weeklyDownloads,
3090
+ maintainerCount: data.maintainers?.length,
3091
+ hasRepository: !!data.repository,
3092
+ hasReadme: !!(data.readme && data.readme.length > 10),
3093
+ hasInstallScripts,
3094
+ description: data.description,
3095
+ license: data.license
3096
+ };
3097
+ }
3098
+ async function fetchPyPIMetadata(name) {
3099
+ const url = `https://pypi.org/pypi/${encodeURIComponent(name)}/json`;
3100
+ let res;
3101
+ try {
3102
+ res = await fetch(url, { signal: withTimeout(REQUEST_TIMEOUT_MS) });
3103
+ } catch {
3104
+ return notFound(name, "pypi");
3105
+ }
3106
+ if (!res.ok) return notFound(name, "pypi");
3107
+ const data = await res.json();
3108
+ let earliest;
3109
+ for (const files of Object.values(data.releases)) {
3110
+ for (const file of files) {
3111
+ const d = new Date(file.upload_time);
3112
+ if (!earliest || d < earliest) earliest = d;
3113
+ }
3114
+ }
3115
+ const projectUrls = data.info.project_urls ?? {};
3116
+ const hasRepo = !!(data.info.home_page || projectUrls["Source"] || projectUrls["Repository"] || projectUrls["GitHub"] || projectUrls["Homepage"]);
3117
+ const hasMaintainer = !!(data.info.maintainer || data.info.author);
3118
+ let weeklyDownloads;
3119
+ try {
3120
+ const statsUrl = `https://pypistats.org/api/packages/${encodeURIComponent(name)}/recent`;
3121
+ const statsRes = await fetch(statsUrl, {
3122
+ signal: withTimeout(REQUEST_TIMEOUT_MS),
3123
+ headers: { Accept: "application/json" }
3124
+ });
3125
+ if (statsRes.ok) {
3126
+ const statsData = await statsRes.json();
3127
+ weeklyDownloads = statsData.data?.last_week;
3128
+ }
3129
+ } catch {
3130
+ }
3131
+ return {
3132
+ name,
3133
+ ecosystem: "pypi",
3134
+ exists: true,
3135
+ createdAt: earliest,
3136
+ modifiedAt: data.urls.length > 0 ? new Date(data.urls[data.urls.length - 1].upload_time) : void 0,
3137
+ latestVersion: data.info.version,
3138
+ weeklyDownloads,
3139
+ maintainerCount: hasMaintainer ? 1 : 0,
3140
+ hasRepository: hasRepo,
3141
+ hasReadme: !!(data.info.description && data.info.description.length > 10),
3142
+ hasInstallScripts: false,
3143
+ // PyPI doesn't have post-install scripts in the same way
3144
+ description: data.info.summary,
3145
+ license: data.info.license
3146
+ };
3147
+ }
3148
+ function notFound(name, ecosystem) {
3149
+ return {
3150
+ name,
3151
+ ecosystem,
3152
+ exists: false,
3153
+ hasRepository: false,
3154
+ hasReadme: false,
3155
+ hasInstallScripts: false
3156
+ };
3157
+ }
3158
+ var cache = /* @__PURE__ */ new Map();
3159
+ var MAX_CACHE_SIZE = 200;
3160
+ function cacheKey(name, ecosystem) {
3161
+ return `${ecosystem}:${name}`;
3162
+ }
3163
+ async function fetchRegistryMetadata(name, ecosystem) {
3164
+ const key = cacheKey(name, ecosystem);
3165
+ const cached = cache.get(key);
3166
+ if (cached) return cached;
3167
+ let result;
3168
+ switch (ecosystem) {
3169
+ case "npm":
3170
+ result = await fetchNpmMetadata(name);
3171
+ break;
3172
+ case "pypi":
3173
+ result = await fetchPyPIMetadata(name);
3174
+ break;
3175
+ default:
3176
+ result = notFound(name, ecosystem);
3177
+ }
3178
+ if (cache.size >= MAX_CACHE_SIZE) {
3179
+ const firstKey = cache.keys().next().value;
3180
+ if (firstKey !== void 0) cache.delete(firstKey);
3181
+ }
3182
+ cache.set(key, result);
3183
+ return result;
3184
+ }
3185
+
3186
+ // src/lib/deps/vulnerabilities.ts
3187
+ var REQUEST_TIMEOUT_MS2 = 5e3;
3188
+ function osvEcosystem(ecosystem) {
3189
+ switch (ecosystem) {
3190
+ case "npm":
3191
+ return "npm";
3192
+ case "pypi":
3193
+ return "PyPI";
3194
+ case "crates.io":
3195
+ return "crates.io";
3196
+ default:
3197
+ return ecosystem;
3198
+ }
3199
+ }
3200
+ function parseSeverity(vuln) {
3201
+ const sevEntry = vuln.severity?.find((s) => s.type === "CVSS_V3");
3202
+ if (!sevEntry) return "medium";
3203
+ const scoreStr = sevEntry.score;
3204
+ const numericMatch = scoreStr.match(/(\d+\.\d+)/);
3205
+ if (numericMatch) {
3206
+ const score = parseFloat(numericMatch[1]);
3207
+ if (score >= 9) return "critical";
3208
+ if (score >= 7) return "high";
3209
+ if (score >= 4) return "medium";
3210
+ return "low";
3211
+ }
3212
+ return "medium";
3213
+ }
3214
+ function extractFixedVersion(vuln) {
3215
+ for (const affected of vuln.affected ?? []) {
3216
+ for (const range of affected.ranges ?? []) {
3217
+ for (const event of range.events ?? []) {
3218
+ if (event.fixed) return event.fixed;
3219
+ }
3220
+ }
3221
+ }
3222
+ return void 0;
3223
+ }
3224
+ function mapVuln(vuln) {
3225
+ return {
3226
+ id: vuln.id,
3227
+ summary: vuln.summary ?? vuln.details?.slice(0, 200) ?? "No description",
3228
+ severity: parseSeverity(vuln),
3229
+ fixedIn: extractFixedVersion(vuln),
3230
+ aliases: vuln.aliases ?? []
3231
+ };
3232
+ }
3233
+ async function queryVulnerabilities(name, ecosystem, version) {
3234
+ const body = {
3235
+ package: { name, ecosystem: osvEcosystem(ecosystem) }
3236
+ };
3237
+ if (version) body.version = version;
3238
+ try {
3239
+ const res = await fetch("https://api.osv.dev/v1/query", {
3240
+ method: "POST",
3241
+ headers: { "Content-Type": "application/json" },
3242
+ body: JSON.stringify(body),
3243
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
3244
+ });
3245
+ if (!res.ok) {
3246
+ return { package: name, ecosystem, vulnerabilities: [], error: `OSV HTTP ${res.status}` };
3247
+ }
3248
+ const data = await res.json();
3249
+ return {
3250
+ package: name,
3251
+ ecosystem,
3252
+ vulnerabilities: (data.vulns ?? []).map(mapVuln)
3253
+ };
3254
+ } catch (err) {
3255
+ return {
3256
+ package: name,
3257
+ ecosystem,
3258
+ vulnerabilities: [],
3259
+ error: err instanceof Error ? err.message : "Unknown error"
3260
+ };
3261
+ }
3262
+ }
3263
+ async function queryVulnerabilitiesBatch(packages) {
3264
+ if (packages.length === 0) return [];
3265
+ if (packages.length === 1) {
3266
+ const pkg = packages[0];
3267
+ return [await queryVulnerabilities(pkg.name, pkg.ecosystem, pkg.version)];
3268
+ }
3269
+ const queries = packages.map((pkg) => {
3270
+ const q = {
3271
+ package: { name: pkg.name, ecosystem: osvEcosystem(pkg.ecosystem) }
3272
+ };
3273
+ if (pkg.version) q.version = pkg.version;
3274
+ return q;
3275
+ });
3276
+ try {
3277
+ const res = await fetch("https://api.osv.dev/v1/querybatch", {
3278
+ method: "POST",
3279
+ headers: { "Content-Type": "application/json" },
3280
+ body: JSON.stringify({ queries }),
3281
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
3282
+ });
3283
+ if (!res.ok) {
3284
+ return packages.map((pkg) => ({
3285
+ package: pkg.name,
3286
+ ecosystem: pkg.ecosystem,
3287
+ vulnerabilities: [],
3288
+ error: `OSV HTTP ${res.status}`
3289
+ }));
3290
+ }
3291
+ const data = await res.json();
3292
+ return data.results.map((result, i) => ({
3293
+ package: packages[i].name,
3294
+ ecosystem: packages[i].ecosystem,
3295
+ vulnerabilities: (result.vulns ?? []).map(mapVuln)
3296
+ }));
3297
+ } catch (err) {
3298
+ const errMsg = err instanceof Error ? err.message : "Unknown error";
3299
+ return packages.map((pkg) => ({
3300
+ package: pkg.name,
3301
+ ecosystem: pkg.ecosystem,
3302
+ vulnerabilities: [],
3303
+ error: errMsg
3304
+ }));
3305
+ }
3306
+ }
3307
+
3308
+ // src/lib/deps/levenshtein.ts
3309
+ function levenshteinDistance(a, b) {
3310
+ if (a === b) return 0;
3311
+ if (a.length === 0) return b.length;
3312
+ if (b.length === 0) return a.length;
3313
+ if (a.length > b.length) [a, b] = [b, a];
3314
+ const m = a.length;
3315
+ const n = b.length;
3316
+ const row = new Array(m + 1);
3317
+ for (let j = 0; j <= m; j++) row[j] = j;
3318
+ for (let i = 1; i <= n; i++) {
3319
+ let corner = row[0];
3320
+ row[0] = i;
3321
+ for (let j = 1; j <= m; j++) {
3322
+ const temp = row[j];
3323
+ if (b[i - 1] === a[j - 1]) {
3324
+ row[j] = corner;
3325
+ } else {
3326
+ row[j] = 1 + Math.min(corner, temp, row[j - 1]);
3327
+ }
3328
+ corner = temp;
3329
+ }
3330
+ }
3331
+ return row[m];
3332
+ }
3333
+ function normalizedSimilarity(a, b) {
3334
+ const maxLen = Math.max(a.length, b.length);
3335
+ if (maxLen === 0) return 1;
3336
+ return 1 - levenshteinDistance(a, b) / maxLen;
3337
+ }
3338
+ function normalizeName(name) {
3339
+ const stripped = name.replace(/^@[^/]+\//, "");
3340
+ return stripped.replace(/[-_.]/g, "").toLowerCase();
3341
+ }
3342
+ function detectTyposquat(name, corpus) {
3343
+ const normalized = normalizeName(name);
3344
+ let bestMatch;
3345
+ let bestDistance = Infinity;
3346
+ for (const target of corpus) {
3347
+ const normalizedTarget = normalizeName(target);
3348
+ if (normalizedTarget === normalized) return void 0;
3349
+ const distance = levenshteinDistance(normalized, normalizedTarget);
3350
+ const similarity = normalizedSimilarity(normalized, normalizedTarget);
3351
+ const shouldFlag = distance === 1 || distance === 2 && normalized.length >= 5 || similarity >= 0.85;
3352
+ if (shouldFlag && distance < bestDistance) {
3353
+ bestDistance = distance;
3354
+ bestMatch = { target, distance, similarity };
3355
+ }
3356
+ }
3357
+ return bestMatch;
3358
+ }
3359
+
3360
+ // src/lib/deps/allowlist.ts
3361
+ var DEFAULT_NPM_ALLOWLIST = [
3362
+ // Frameworks
3363
+ "express",
3364
+ "react",
3365
+ "react-dom",
3366
+ "next",
3367
+ "vue",
3368
+ "angular",
3369
+ "svelte",
3370
+ "fastify",
3371
+ "koa",
3372
+ "hapi",
3373
+ "nest",
3374
+ "nuxt",
3375
+ // Build tools
3376
+ "typescript",
3377
+ "webpack",
3378
+ "vite",
3379
+ "esbuild",
3380
+ "tsup",
3381
+ "rollup",
3382
+ "parcel",
3383
+ "babel",
3384
+ "swc",
3385
+ // Quality tools
3386
+ "prettier",
3387
+ "eslint",
3388
+ "vitest",
3389
+ "jest",
3390
+ "mocha",
3391
+ "chai",
3392
+ "sinon",
3393
+ "husky",
3394
+ "lint-staged",
3395
+ // Utilities
3396
+ "lodash",
3397
+ "axios",
3398
+ "chalk",
3399
+ "commander",
3400
+ "debug",
3401
+ "async",
3402
+ "uuid",
3403
+ "semver",
3404
+ "glob",
3405
+ "fs-extra",
3406
+ "mkdirp",
3407
+ "rimraf",
3408
+ "minimist",
3409
+ "yargs",
3410
+ "dotenv",
3411
+ "nanoid",
3412
+ "date-fns",
3413
+ "dayjs",
3414
+ "moment",
3415
+ "rxjs",
3416
+ "immer",
3417
+ // Validation
3418
+ "zod",
3419
+ "joi",
3420
+ "ajv",
3421
+ "yup",
3422
+ // HTTP / API
3423
+ "cors",
3424
+ "body-parser",
3425
+ "cookie-parser",
3426
+ "morgan",
3427
+ "helmet",
3428
+ "jsonwebtoken",
3429
+ "bcrypt",
3430
+ "passport",
3431
+ // Database
3432
+ "mongoose",
3433
+ "sequelize",
3434
+ "prisma",
3435
+ "typeorm",
3436
+ "knex",
3437
+ "pg",
3438
+ "mysql2",
3439
+ "redis",
3440
+ "ioredis",
3441
+ "better-sqlite3",
3442
+ // Realtime
3443
+ "socket.io",
3444
+ "ws",
3445
+ // State management
3446
+ "zustand",
3447
+ "redux",
3448
+ "mobx",
3449
+ // CSS
3450
+ "tailwindcss",
3451
+ "postcss",
3452
+ "autoprefixer",
3453
+ "sass",
3454
+ "styled-components",
3455
+ "clsx",
3456
+ "classnames",
3457
+ // Media
3458
+ "sharp",
3459
+ "puppeteer",
3460
+ "cheerio",
3461
+ "marked",
3462
+ "highlight.js",
3463
+ // Types
3464
+ "tslib"
3465
+ ];
3466
+ var DEFAULT_PYPI_ALLOWLIST = [
3467
+ // Web
3468
+ "requests",
3469
+ "flask",
3470
+ "django",
3471
+ "fastapi",
3472
+ "aiohttp",
3473
+ "httpx",
3474
+ "uvicorn",
3475
+ "gunicorn",
3476
+ "starlette",
3477
+ // Data
3478
+ "numpy",
3479
+ "pandas",
3480
+ "scipy",
3481
+ "matplotlib",
3482
+ "scikit-learn",
3483
+ "pillow",
3484
+ // Infrastructure
3485
+ "boto3",
3486
+ "botocore",
3487
+ "urllib3",
3488
+ "certifi",
3489
+ "charset-normalizer",
3490
+ "idna",
3491
+ // Core
3492
+ "setuptools",
3493
+ "pip",
3494
+ "wheel",
3495
+ "packaging",
3496
+ "typing-extensions",
3497
+ "six",
3498
+ "python-dateutil",
3499
+ "pyyaml",
3500
+ "tomli",
3501
+ "filelock",
3502
+ "attrs",
3503
+ "click",
3504
+ "importlib-metadata",
3505
+ "zipp",
3506
+ "platformdirs",
3507
+ // Crypto
3508
+ "cryptography",
3509
+ "cffi",
3510
+ "pycparser",
3511
+ "jmespath",
3512
+ "pyasn1",
3513
+ // DB
3514
+ "sqlalchemy",
3515
+ "psycopg2",
3516
+ "redis",
3517
+ "celery",
3518
+ // Template / Markup
3519
+ "jinja2",
3520
+ "markupsafe",
3521
+ "beautifulsoup4",
3522
+ "lxml",
3523
+ "scrapy",
3524
+ // Validation
3525
+ "pydantic",
3526
+ // Testing
3527
+ "pytest",
3528
+ "tox",
3529
+ "coverage",
3530
+ "mock"
3531
+ ];
3532
+ var DEFAULT_BLOCKLIST_EXACT = [
3533
+ // npm typosquats (historically malicious)
3534
+ "crossenv",
3535
+ "cross-env.js",
3536
+ "d3.js",
3537
+ "fabric-js",
3538
+ "ffmpegs",
3539
+ "gruntcli",
3540
+ "http-proxy.js",
3541
+ "jquery.js",
3542
+ "mongose",
3543
+ "mssql-node",
3544
+ "nodecaffe",
3545
+ "nodefabric",
3546
+ "nodemailer-js",
3547
+ "noderequest",
3548
+ "nodesass",
3549
+ "opencv.js",
3550
+ "openssl.js",
3551
+ "shadowsock",
3552
+ "sqliter",
3553
+ "sqlserver",
3554
+ // npm compromised
3555
+ "flatmap-stream",
3556
+ // PyPI typosquats
3557
+ "colourama",
3558
+ "python3-dateutil",
3559
+ "requesocks",
3560
+ "requesst",
3561
+ "beautifulsup",
3562
+ "numppy",
3563
+ "numpys",
3564
+ "djanga",
3565
+ "urlib3"
3566
+ ];
3567
+ var DEFAULT_BLOCKLIST_PATTERNS = [
3568
+ /-free-download$/,
3569
+ /-crack$/,
3570
+ /-keygen$/,
3571
+ /-license-key$/,
3572
+ /-hack$/,
3573
+ /-serial$/,
3574
+ /-activation$/,
3575
+ /-premium-free$/,
3576
+ /-generator-free$/
3577
+ ];
3578
+ function buildAllowBlockLists(ecosystem, userAllowlist, userBlocklist, userBlocklistPatterns) {
3579
+ const base = ecosystem === "npm" ? DEFAULT_NPM_ALLOWLIST : ecosystem === "pypi" ? DEFAULT_PYPI_ALLOWLIST : [];
3580
+ return {
3581
+ allowlist: [...base, ...userAllowlist],
3582
+ blocklist: [...DEFAULT_BLOCKLIST_EXACT, ...userBlocklist],
3583
+ blocklistPatterns: [
3584
+ ...DEFAULT_BLOCKLIST_PATTERNS,
3585
+ ...userBlocklistPatterns.map((p) => new RegExp(p))
3586
+ ]
3587
+ };
3588
+ }
3589
+ function isAllowlisted(name, allowlist) {
3590
+ return allowlist.includes(name);
3591
+ }
3592
+ function isBlocklisted(name, blocklist, blocklistPatterns) {
3593
+ if (blocklist.includes(name)) return true;
3594
+ return blocklistPatterns.some((p) => p.test(name));
3595
+ }
3596
+
3597
+ // src/lib/deps/risk.ts
3598
+ var DEFAULT_THRESHOLDS = {
3599
+ minAgeDays: 30,
3600
+ minWeeklyDownloads: 100
3601
+ };
3602
+ var SEVERITY_ORDER2 = {
3603
+ info: 0,
3604
+ low: 1,
3605
+ medium: 2,
3606
+ high: 3,
3607
+ critical: 4
3608
+ };
3609
+ function maxSeverity(a, b) {
3610
+ return SEVERITY_ORDER2[a] >= SEVERITY_ORDER2[b] ? a : b;
3611
+ }
3612
+ function computeRiskReport(metadata, vulns, typosquatMatch, isBlocklisted2, isAllowlisted2, thresholds = DEFAULT_THRESHOLDS) {
3613
+ const signals = [];
3614
+ let overallRisk = "info";
3615
+ if (!metadata.exists) {
3616
+ signals.push({
3617
+ name: "package_not_found",
3618
+ severity: "critical",
3619
+ detail: `Package "${metadata.name}" does not exist on ${metadata.ecosystem}`
3620
+ });
3621
+ overallRisk = "critical";
3622
+ }
3623
+ if (isBlocklisted2) {
3624
+ signals.push({
3625
+ name: "on_blocklist",
3626
+ severity: "critical",
3627
+ detail: `Package "${metadata.name}" is on the blocklist`
3628
+ });
3629
+ overallRisk = "critical";
3630
+ }
3631
+ for (const vuln of vulns) {
3632
+ signals.push({
3633
+ name: "known_vulnerability",
3634
+ severity: vuln.severity,
3635
+ detail: `${vuln.id}: ${vuln.summary}${vuln.fixedIn ? ` (fixed in ${vuln.fixedIn})` : ""}`
3636
+ });
3637
+ overallRisk = maxSeverity(overallRisk, vuln.severity);
3638
+ }
3639
+ if (isAllowlisted2 && signals.length === 0) {
3640
+ return {
3641
+ package: metadata.name,
3642
+ ecosystem: metadata.ecosystem,
3643
+ overallRisk: "info",
3644
+ signals: [],
3645
+ vulnerabilities: vulns,
3646
+ recommendation: "allow",
3647
+ metadata
3648
+ };
3649
+ }
3650
+ if (!isAllowlisted2 && metadata.exists) {
3651
+ if (metadata.createdAt) {
3652
+ const ageDays = (Date.now() - metadata.createdAt.getTime()) / (1e3 * 60 * 60 * 24);
3653
+ if (ageDays < 7) {
3654
+ signals.push({
3655
+ name: "very_new_package",
3656
+ severity: "high",
3657
+ detail: `Created ${Math.floor(ageDays)} days ago`
3658
+ });
3659
+ overallRisk = maxSeverity(overallRisk, "high");
3660
+ } else if (ageDays < thresholds.minAgeDays) {
3661
+ signals.push({
3662
+ name: "new_package",
3663
+ severity: "medium",
3664
+ detail: `Created ${Math.floor(ageDays)} days ago (threshold: ${thresholds.minAgeDays})`
3665
+ });
3666
+ overallRisk = maxSeverity(overallRisk, "medium");
3667
+ }
3668
+ }
3669
+ if (metadata.weeklyDownloads !== void 0) {
3670
+ if (metadata.weeklyDownloads < thresholds.minWeeklyDownloads) {
3671
+ const sev = metadata.weeklyDownloads < 10 ? "high" : "medium";
3672
+ signals.push({
3673
+ name: "low_downloads",
3674
+ severity: sev,
3675
+ detail: `${metadata.weeklyDownloads} weekly downloads (threshold: ${thresholds.minWeeklyDownloads})`
3676
+ });
3677
+ overallRisk = maxSeverity(overallRisk, sev);
3678
+ }
3679
+ }
3680
+ if (typosquatMatch) {
3681
+ signals.push({
3682
+ name: "typosquat_suspect",
3683
+ severity: "high",
3684
+ detail: `Similar to "${typosquatMatch.target}" (edit distance: ${typosquatMatch.distance})`
3685
+ });
3686
+ overallRisk = maxSeverity(overallRisk, "high");
3687
+ }
3688
+ if (metadata.hasInstallScripts) {
3689
+ signals.push({
3690
+ name: "has_install_scripts",
3691
+ severity: "medium",
3692
+ detail: "Package has preinstall/install/postinstall scripts"
3693
+ });
3694
+ overallRisk = maxSeverity(overallRisk, "medium");
3695
+ }
3696
+ if (!metadata.hasRepository) {
3697
+ signals.push({
3698
+ name: "no_repository",
3699
+ severity: "low",
3700
+ detail: "No source repository URL in metadata"
3701
+ });
3702
+ overallRisk = maxSeverity(overallRisk, "low");
3703
+ }
3704
+ if (!metadata.hasReadme) {
3705
+ signals.push({
3706
+ name: "no_readme",
3707
+ severity: "low",
3708
+ detail: "No README content"
3709
+ });
3710
+ overallRisk = maxSeverity(overallRisk, "low");
3711
+ }
3712
+ if (!metadata.license) {
3713
+ signals.push({
3714
+ name: "no_license",
3715
+ severity: "low",
3716
+ detail: "No license declared"
3717
+ });
3718
+ overallRisk = maxSeverity(overallRisk, "low");
3719
+ }
3720
+ if (metadata.maintainerCount !== void 0 && metadata.maintainerCount <= 1) {
3721
+ signals.push({
3722
+ name: "single_maintainer",
3723
+ severity: "info",
3724
+ detail: `${metadata.maintainerCount} maintainer(s)`
3725
+ });
3726
+ }
3727
+ }
3728
+ let recommendation;
3729
+ if (SEVERITY_ORDER2[overallRisk] >= SEVERITY_ORDER2["critical"]) {
3730
+ recommendation = "block";
3731
+ } else if (SEVERITY_ORDER2[overallRisk] >= SEVERITY_ORDER2["medium"]) {
3732
+ recommendation = "escalate";
3733
+ } else {
3734
+ recommendation = "allow";
3735
+ }
3736
+ return {
3737
+ package: metadata.name,
3738
+ ecosystem: metadata.ecosystem,
3739
+ overallRisk,
3740
+ signals,
3741
+ vulnerabilities: vulns,
3742
+ recommendation,
3743
+ metadata
3744
+ };
3745
+ }
3746
+
3747
+ // src/lib/deps/guardian.ts
3748
+ var DEFAULT_CONFIG2 = {
3749
+ enabled: true,
3750
+ checks: {
3751
+ existence: true,
3752
+ reputation: true,
3753
+ typosquatting: true,
3754
+ install_scripts: true,
3755
+ vulnerabilities: true
3756
+ },
3757
+ risk_thresholds: {
3758
+ min_age_days: 30,
3759
+ min_weekly_downloads: 100
3760
+ },
3761
+ on_risk: "escalate",
3762
+ allowlist: [],
3763
+ blocklist: [],
3764
+ blocklist_patterns: [],
3765
+ custom_registry_bypass: true
3766
+ };
3767
+ async function evaluateInstall(command, config = DEFAULT_CONFIG2) {
3768
+ if (!config.enabled) {
3769
+ return skippedResult(command, "Dependency guardian is disabled");
3770
+ }
3771
+ const parsed = parseInstallCommand(command);
3772
+ if (!parsed) {
3773
+ return skippedResult(command, "Not a recognized install command");
3774
+ }
3775
+ if (parsed.isLockfileInstall) {
3776
+ return skippedResult(command, "Lock-file install (pinned dependencies)");
3777
+ }
3778
+ if (config.custom_registry_bypass && parsed.usesCustomRegistry) {
3779
+ return skippedResult(command, "Custom registry detected (bypass enabled)");
3780
+ }
3781
+ if (parsed.packages.length === 0) {
3782
+ return skippedResult(command, "No specific packages to validate");
3783
+ }
3784
+ const ecosystems = [...new Set(parsed.packages.map((p) => p.ecosystem))];
3785
+ const listsPerEcosystem = new Map(
3786
+ ecosystems.map((eco) => [
3787
+ eco,
3788
+ buildAllowBlockLists(eco, config.allowlist, config.blocklist, config.blocklist_patterns)
3789
+ ])
3790
+ );
3791
+ const metadataPromises = parsed.packages.map(async (pkg) => {
3792
+ if (!config.checks.existence && !config.checks.reputation && !config.checks.install_scripts) {
3793
+ return void 0;
3794
+ }
3795
+ return fetchRegistryMetadata(pkg.name, pkg.ecosystem);
3796
+ });
3797
+ const vulnPromise = config.checks.vulnerabilities ? queryVulnerabilitiesBatch(
3798
+ parsed.packages.map((p) => ({
3799
+ name: p.name,
3800
+ ecosystem: p.ecosystem,
3801
+ version: p.version
3802
+ }))
3803
+ ) : Promise.resolve(
3804
+ parsed.packages.map((p) => ({
3805
+ package: p.name,
3806
+ ecosystem: p.ecosystem,
3807
+ vulnerabilities: []
3808
+ }))
3809
+ );
3810
+ const [metadataResults, vulnResults] = await Promise.all([
3811
+ Promise.all(metadataPromises),
3812
+ vulnPromise
3813
+ ]);
3814
+ const reports = [];
3815
+ for (let i = 0; i < parsed.packages.length; i++) {
3816
+ const pkg = parsed.packages[i];
3817
+ const lists = listsPerEcosystem.get(pkg.ecosystem);
3818
+ const allowed = isAllowlisted(pkg.name, lists.allowlist);
3819
+ const blocked = isBlocklisted(pkg.name, lists.blocklist, lists.blocklistPatterns);
3820
+ const metadata = metadataResults[i] ?? {
3821
+ name: pkg.name,
3822
+ ecosystem: pkg.ecosystem,
3823
+ exists: true,
3824
+ // assume exists if we didn't check
3825
+ hasRepository: true,
3826
+ hasReadme: true,
3827
+ hasInstallScripts: false
3828
+ };
3829
+ const vulns = vulnResults[i]?.vulnerabilities ?? [];
3830
+ const typosquatMatch = config.checks.typosquatting && !allowed ? detectTyposquat(pkg.name, lists.allowlist) : void 0;
3831
+ const report = computeRiskReport(metadata, vulns, typosquatMatch, blocked, allowed, {
3832
+ minAgeDays: config.risk_thresholds.min_age_days,
3833
+ minWeeklyDownloads: config.risk_thresholds.min_weekly_downloads
3834
+ });
3835
+ reports.push(report);
3836
+ }
3837
+ let overallRecommendation = "allow";
3838
+ for (const report of reports) {
3839
+ if (report.recommendation === "block") {
3840
+ overallRecommendation = "block";
3841
+ break;
3842
+ }
3843
+ if (report.recommendation === "escalate") {
3844
+ overallRecommendation = "escalate";
3845
+ }
3846
+ }
3847
+ if (config.on_risk === "audit" && overallRecommendation === "escalate") {
3848
+ overallRecommendation = "allow";
3849
+ }
3850
+ if (config.on_risk === "block" && overallRecommendation === "escalate") {
3851
+ overallRecommendation = "block";
3852
+ }
3853
+ const summary = formatSummary(command, reports, overallRecommendation);
3854
+ return {
3855
+ command,
3856
+ packages: reports,
3857
+ overallRecommendation,
3858
+ summary,
3859
+ auditMetadata: {
3860
+ command,
3861
+ manager: parsed.manager,
3862
+ packages: reports.map((r) => ({
3863
+ name: r.package,
3864
+ ecosystem: r.ecosystem,
3865
+ risk: r.overallRisk,
3866
+ signals: r.signals.map((s) => s.name),
3867
+ vulnCount: r.vulnerabilities.length
3868
+ }))
3869
+ },
3870
+ skipped: false
3871
+ };
3872
+ }
3873
+ function skippedResult(command, reason) {
3874
+ return {
3875
+ command,
3876
+ packages: [],
3877
+ overallRecommendation: "allow",
3878
+ summary: reason,
3879
+ auditMetadata: { command, skipped: true, reason },
3880
+ skipped: true,
3881
+ skipReason: reason
3882
+ };
3883
+ }
3884
+ function formatSummary(command, reports, recommendation) {
3885
+ const lines = [];
3886
+ lines.push(`Command: ${command}`);
3887
+ lines.push("");
3888
+ for (const report of reports) {
3889
+ if (report.signals.length === 0 && report.vulnerabilities.length === 0) continue;
3890
+ lines.push(`Package: ${report.package} (${report.ecosystem})`);
3891
+ lines.push(`Risk: ${report.overallRisk.toUpperCase()}`);
3892
+ if (report.signals.length > 0) {
3893
+ lines.push("Signals:");
3894
+ for (const signal of report.signals) {
3895
+ const icon = signal.severity === "critical" || signal.severity === "high" ? "!!" : signal.severity === "medium" ? "! " : " ";
3896
+ lines.push(` ${icon} ${signal.name}: ${signal.detail}`);
3897
+ }
3898
+ }
3899
+ lines.push("");
3900
+ }
3901
+ if (recommendation === "block") {
3902
+ lines.push("Recommendation: BLOCK");
3903
+ } else if (recommendation === "escalate") {
3904
+ lines.push("Recommendation: Requires human approval");
3905
+ }
3906
+ return lines.join("\n");
3907
+ }
3908
+
2873
3909
  // src/extensions/index.ts
2874
3910
  var PATH_TOOLS = {
2875
3911
  read: "path",
@@ -2973,6 +4009,28 @@ function resolveDlpConfig(dlpConfig, role) {
2973
4009
  pattern_overrides: patternOverrides
2974
4010
  };
2975
4011
  }
4012
+ function resolveGuardianConfig(cfg) {
4013
+ if (!cfg || cfg.enabled === false) return void 0;
4014
+ return {
4015
+ enabled: cfg.enabled ?? true,
4016
+ checks: {
4017
+ existence: cfg.checks?.existence ?? true,
4018
+ reputation: cfg.checks?.reputation ?? true,
4019
+ typosquatting: cfg.checks?.typosquatting ?? true,
4020
+ install_scripts: cfg.checks?.install_scripts ?? true,
4021
+ vulnerabilities: cfg.checks?.vulnerabilities ?? true
4022
+ },
4023
+ risk_thresholds: {
4024
+ min_age_days: cfg.risk_thresholds?.min_age_days ?? 30,
4025
+ min_weekly_downloads: cfg.risk_thresholds?.min_weekly_downloads ?? 100
4026
+ },
4027
+ on_risk: cfg.on_risk ?? "escalate",
4028
+ allowlist: cfg.allowlist ?? [],
4029
+ blocklist: cfg.blocklist ?? [],
4030
+ blocklist_patterns: cfg.blocklist_patterns ?? [],
4031
+ custom_registry_bypass: cfg.custom_registry_bypass ?? true
4032
+ };
4033
+ }
2976
4034
  var piGovernance = (pi) => {
2977
4035
  let config;
2978
4036
  let policyEngine;
@@ -2986,6 +4044,7 @@ var piGovernance = (pi) => {
2986
4044
  let configWatcher;
2987
4045
  let dlpScanner;
2988
4046
  let dlpMasker;
4047
+ let guardianConfig;
2989
4048
  let protectedPaths = /* @__PURE__ */ new Set();
2990
4049
  const stats = {
2991
4050
  configTampered: 0,
@@ -3081,6 +4140,7 @@ var piGovernance = (pi) => {
3081
4140
  dlpScanner = new DlpScanner(dlpCfg);
3082
4141
  dlpMasker = new DlpMasker(config.dlp?.masking);
3083
4142
  }
4143
+ guardianConfig = resolveGuardianConfig(config.dependency_guardian);
3084
4144
  if (loaded.source !== "built-in") {
3085
4145
  configWatcher = new ConfigWatcher(
3086
4146
  loaded.source,
@@ -3100,6 +4160,7 @@ var piGovernance = (pi) => {
3100
4160
  dlpScanner = void 0;
3101
4161
  dlpMasker = void 0;
3102
4162
  }
4163
+ guardianConfig = resolveGuardianConfig(newConfig.dependency_guardian);
3103
4164
  audit.log({
3104
4165
  sessionId,
3105
4166
  event: "config_reloaded",
@@ -3130,7 +4191,7 @@ var piGovernance = (pi) => {
3130
4191
  "info"
3131
4192
  );
3132
4193
  });
3133
- pi.on("tool_call", async (event, _ctx) => {
4194
+ pi.on("tool_call", async (event, ctx) => {
3134
4195
  if (!audit || !policyEngine || !identity) return void 0;
3135
4196
  const { toolName, input } = event;
3136
4197
  const params = summarizeParams(toolName, input);
@@ -3196,6 +4257,47 @@ var piGovernance = (pi) => {
3196
4257
  const command = typeof input["command"] === "string" ? input["command"] : "";
3197
4258
  const classification = bashClassifier.classify(command);
3198
4259
  if (classification === "dangerous") {
4260
+ if (guardianConfig && parseInstallCommand(command)) {
4261
+ const guardianResult = await evaluateInstall(command, guardianConfig);
4262
+ if (!guardianResult.skipped) {
4263
+ if (guardianResult.overallRecommendation === "block") {
4264
+ stats.denied++;
4265
+ await audit.log({
4266
+ ...baseRecord,
4267
+ event: "dep_blocked",
4268
+ decision: "denied",
4269
+ reason: guardianResult.summary,
4270
+ metadata: guardianResult.auditMetadata
4271
+ });
4272
+ return { block: true, reason: guardianResult.summary };
4273
+ }
4274
+ if (guardianResult.overallRecommendation === "escalate") {
4275
+ await audit.log({
4276
+ ...baseRecord,
4277
+ event: "dep_escalated",
4278
+ metadata: guardianResult.auditMetadata
4279
+ });
4280
+ const approved = await ctx.ui.confirm(
4281
+ "Dependency Review Required",
4282
+ guardianResult.summary
4283
+ );
4284
+ if (!approved) {
4285
+ stats.denied++;
4286
+ await audit.log({ ...baseRecord, event: "dep_rejected" });
4287
+ return { block: true, reason: "Dependency rejected by reviewer" };
4288
+ }
4289
+ stats.approvals++;
4290
+ await audit.log({ ...baseRecord, event: "dep_approved" });
4291
+ }
4292
+ await audit.log({
4293
+ ...baseRecord,
4294
+ event: "dep_allowed",
4295
+ metadata: guardianResult.auditMetadata
4296
+ });
4297
+ stats.allowed++;
4298
+ return void 0;
4299
+ }
4300
+ }
3199
4301
  stats.denied++;
3200
4302
  await audit.log({
3201
4303
  ...baseRecord,