@grwnd/pi-governance 3.0.2 → 3.1.1

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