@kolatts/pncli 1.8.0 → 1.9.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/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  setRepoConfigValue,
8
8
  writeGlobalConfig,
9
9
  writeRepoConfig
10
- } from "./chunk-S4SJLY4M.js";
10
+ } from "./chunk-YR6GFJMV.js";
11
11
  import {
12
12
  ExitCode,
13
13
  PncliError,
@@ -18,13 +18,13 @@ import {
18
18
  setGlobalUser,
19
19
  success,
20
20
  warn
21
- } from "./chunk-QQ7UKWTU.js";
21
+ } from "./chunk-QQZJQ5DP.js";
22
22
  import {
23
23
  AdoCoreClient
24
24
  } from "./chunk-VDL32PSG.js";
25
25
 
26
26
  // src/cli.ts
27
- import { Command } from "commander";
27
+ import { Command as Command2 } from "commander";
28
28
  import { createRequire } from "module";
29
29
 
30
30
  // src/services/git/client.ts
@@ -771,7 +771,7 @@ function registerJiraCommands(program2) {
771
771
  fail(err, "jira", "get-issue", start);
772
772
  }
773
773
  });
774
- jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").requiredOption("--summary <text>", "Issue summary").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--field <Name=value>", "Custom field value (repeatable)", (val, acc) => [...acc, val], []).action(async (opts) => {
774
+ jira.command("create-issue").description("Create a Jira issue").option("--project <key>", "Project key").option("--type <type>", "Issue type (Bug, Story, Task, ...)").requiredOption("--summary <text>", "Issue summary").option("--description <text>", "Issue description").option("--priority <name>", "Priority name").option("--assignee <accountId>", "Assignee account ID").option("--labels <labels>", "Comma-separated labels").option("--parent <key>", 'Parent issue key \u2014 creates an "is child of" link after creation').option("--field <Name=value>", "Custom field value (repeatable)", (val, acc) => [...acc, val], []).action(async (opts) => {
775
775
  const start = Date.now();
776
776
  try {
777
777
  const { client, fieldMap } = getClientAndFields(program2);
@@ -784,6 +784,14 @@ function registerJiraCommands(program2) {
784
784
  const customFieldValues = parseFieldArgs(opts.field, fieldMap);
785
785
  const data = await client.createIssue({ project, issueType, summary: opts.summary, description: opts.description, priority, assignee: opts.assignee, labels, customFieldValues });
786
786
  success(data, "jira", "create-issue", start);
787
+ if (opts.parent) {
788
+ try {
789
+ await client.linkIssue({ key: data.key, linkType: "is child of", target: opts.parent });
790
+ } catch (linkErr) {
791
+ warn(`Created ${data.key} but failed to link to parent ${opts.parent}: ${linkErr instanceof Error ? linkErr.message : linkErr}`);
792
+ }
793
+ }
794
+ return;
787
795
  } catch (err) {
788
796
  fail(err, "jira", "create-issue", start);
789
797
  }
@@ -2039,6 +2047,9 @@ function registerSdeCommands(program2) {
2039
2047
  });
2040
2048
  }
2041
2049
 
2050
+ // src/services/deps/commands.ts
2051
+ import { Option } from "commander";
2052
+
2042
2053
  // src/services/deps/parsers/index.ts
2043
2054
  import fs from "fs";
2044
2055
  import path4 from "path";
@@ -2893,11 +2904,128 @@ async function checkPackagesForVulns(packages) {
2893
2904
  return allVulnerable;
2894
2905
  }
2895
2906
 
2896
- // src/services/deps/clients/artifactory.ts
2897
- var TIMEOUT_MS2 = 15e3;
2898
- async function fetchWithTimeout2(url, token) {
2907
+ // src/services/deps/clients/sonatype.ts
2908
+ var SONATYPE_URL = "https://ossindex.sonatype.org";
2909
+ var BATCH_SIZE2 = 128;
2910
+ var TIMEOUT_MS2 = 3e4;
2911
+ async function fetchWithTimeout2(url, init) {
2899
2912
  const controller = new AbortController();
2900
2913
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS2);
2914
+ try {
2915
+ return await fetch(url, { ...init, signal: controller.signal });
2916
+ } finally {
2917
+ clearTimeout(timer);
2918
+ }
2919
+ }
2920
+ function toPurl(pkg2) {
2921
+ switch (pkg2.ecosystem) {
2922
+ case "npm": {
2923
+ const encodedName = pkg2.name.startsWith("@") ? pkg2.name.replace("@", "%40") : pkg2.name;
2924
+ return `pkg:npm/${encodedName}@${pkg2.version}`;
2925
+ }
2926
+ case "nuget":
2927
+ return `pkg:nuget/${pkg2.name}@${pkg2.version}`;
2928
+ case "maven": {
2929
+ const colonIdx = pkg2.name.indexOf(":");
2930
+ if (colonIdx !== -1) {
2931
+ const groupId = pkg2.name.slice(0, colonIdx);
2932
+ const artifactId = pkg2.name.slice(colonIdx + 1);
2933
+ return `pkg:maven/${encodeURIComponent(groupId)}/${encodeURIComponent(artifactId)}@${pkg2.version}`;
2934
+ }
2935
+ return `pkg:maven/${encodeURIComponent(pkg2.name)}@${pkg2.version}`;
2936
+ }
2937
+ default:
2938
+ return `pkg:generic/${encodeURIComponent(pkg2.name)}@${pkg2.version}`;
2939
+ }
2940
+ }
2941
+ function inferSeverityFromCvss2(score) {
2942
+ if (score >= 9) return "CRITICAL";
2943
+ if (score >= 7) return "HIGH";
2944
+ if (score >= 4) return "MEDIUM";
2945
+ if (score > 0) return "LOW";
2946
+ return "UNKNOWN";
2947
+ }
2948
+ function mapSonatypeVuln(v, pkg2) {
2949
+ const refs = [v.reference, ...v.externalReferences ?? []].filter(Boolean);
2950
+ const advice = `See ${v.reference} for remediation details for ${pkg2.name}@${pkg2.version}.`;
2951
+ const cvePattern = /CVE-\d{4}-\d+/gi;
2952
+ const searchText = [v.displayName, v.title, v.description].filter(Boolean).join(" ");
2953
+ const aliases = [...new Set([...searchText.matchAll(cvePattern)].map((m) => m[0].toUpperCase()))];
2954
+ return {
2955
+ id: v.displayName || v.id,
2956
+ summary: v.title || v.description,
2957
+ severity: inferSeverityFromCvss2(v.cvssScore),
2958
+ cvss: v.cvssScore > 0 ? v.cvssScore : null,
2959
+ aliases,
2960
+ remediation: {
2961
+ fixAvailable: false,
2962
+ fixedVersions: [],
2963
+ advice
2964
+ },
2965
+ references: refs
2966
+ };
2967
+ }
2968
+ async function queryBatch2(purls, packages) {
2969
+ const res = await fetchWithTimeout2(`${SONATYPE_URL}/api/v3/component-report`, {
2970
+ method: "POST",
2971
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2972
+ body: JSON.stringify({ coordinates: purls })
2973
+ });
2974
+ if (res.status === 429) {
2975
+ throw new Error(
2976
+ "Sonatype OSS Index is rate-limiting requests from this IP. Create a free account at ossindex.sonatype.org and add your credentials to your pncli config to raise the limit."
2977
+ );
2978
+ }
2979
+ if (!res.ok) {
2980
+ throw new Error(`Sonatype OSS Index returned HTTP ${res.status}`);
2981
+ }
2982
+ const reports = await res.json();
2983
+ const vulnerable = [];
2984
+ const purlToPackage = /* @__PURE__ */ new Map();
2985
+ for (let i = 0; i < purls.length; i++) {
2986
+ purlToPackage.set(purls[i], packages[i]);
2987
+ }
2988
+ for (const report of reports) {
2989
+ if (!report?.coordinates) continue;
2990
+ const pkg2 = purlToPackage.get(report.coordinates);
2991
+ if (!pkg2) continue;
2992
+ const vulns = report.vulnerabilities ?? [];
2993
+ if (vulns.length === 0) continue;
2994
+ vulnerable.push({ ...pkg2, vulnerabilities: vulns.map((v) => mapSonatypeVuln(v, pkg2)) });
2995
+ }
2996
+ return vulnerable;
2997
+ }
2998
+ async function checkSonatypeConnectivity() {
2999
+ try {
3000
+ const res = await fetchWithTimeout2(`${SONATYPE_URL}/api/v3/component-report`, {
3001
+ method: "POST",
3002
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
3003
+ body: JSON.stringify({ coordinates: [] })
3004
+ });
3005
+ return { reachable: res.ok || res.status === 400 };
3006
+ } catch (err) {
3007
+ return {
3008
+ reachable: false,
3009
+ error: err instanceof Error ? err.message : String(err)
3010
+ };
3011
+ }
3012
+ }
3013
+ async function checkPackagesForVulnsViaSonatype(packages) {
3014
+ const allVulnerable = [];
3015
+ for (let i = 0; i < packages.length; i += BATCH_SIZE2) {
3016
+ const chunk = packages.slice(i, i + BATCH_SIZE2);
3017
+ const purls = chunk.map(toPurl);
3018
+ const results = await queryBatch2(purls, chunk);
3019
+ allVulnerable.push(...results);
3020
+ }
3021
+ return allVulnerable;
3022
+ }
3023
+
3024
+ // src/services/deps/clients/artifactory.ts
3025
+ var TIMEOUT_MS3 = 15e3;
3026
+ async function fetchWithTimeout3(url, token) {
3027
+ const controller = new AbortController();
3028
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS3);
2901
3029
  try {
2902
3030
  return await fetch(url, {
2903
3031
  headers: { Authorization: `Bearer ${token}` },
@@ -2933,7 +3061,7 @@ async function checkArtifactoryConnectivity(config) {
2933
3061
  };
2934
3062
  }
2935
3063
  try {
2936
- const res = await fetchWithTimeout2(`${config.baseUrl}/api/system/ping`, config.token);
3064
+ const res = await fetchWithTimeout3(`${config.baseUrl}/api/system/ping`, config.token);
2937
3065
  if (res.status === 401 || res.status === 403) {
2938
3066
  return {
2939
3067
  reachable: true,
@@ -2983,7 +3111,7 @@ function repoForEcosystem(config, eco) {
2983
3111
  async function getLatestNpm(baseUrl, token, repoName, packageName) {
2984
3112
  try {
2985
3113
  const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
2986
- const res = await fetchWithTimeout2(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3114
+ const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
2987
3115
  if (!res.ok) return null;
2988
3116
  const data = await res.json();
2989
3117
  return data["dist-tags"]?.latest ?? null;
@@ -2994,7 +3122,7 @@ async function getLatestNpm(baseUrl, token, repoName, packageName) {
2994
3122
  async function getLatestNuget(baseUrl, token, repoName, packageName) {
2995
3123
  try {
2996
3124
  const lower = packageName.toLowerCase();
2997
- const res = await fetchWithTimeout2(
3125
+ const res = await fetchWithTimeout3(
2998
3126
  `${baseUrl}/api/nuget/v3/${repoName}/flatcontainer/${lower}/index.json`,
2999
3127
  token
3000
3128
  );
@@ -3010,7 +3138,7 @@ async function getLatestMaven(baseUrl, token, repoName, packageName) {
3010
3138
  const [groupId, artifactId] = packageName.split(":");
3011
3139
  if (!groupId || !artifactId) return null;
3012
3140
  try {
3013
- const res = await fetchWithTimeout2(
3141
+ const res = await fetchWithTimeout3(
3014
3142
  `${baseUrl}/api/search/latestVersion?g=${encodeURIComponent(groupId)}&a=${encodeURIComponent(artifactId)}&repos=${repoName}`,
3015
3143
  token
3016
3144
  );
@@ -3060,7 +3188,7 @@ async function getOutdatedPackages(packages, config, filterType) {
3060
3188
  async function getLicenseNpm(baseUrl, token, repoName, packageName) {
3061
3189
  try {
3062
3190
  const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
3063
- const res = await fetchWithTimeout2(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3191
+ const res = await fetchWithTimeout3(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
3064
3192
  if (!res.ok) return null;
3065
3193
  const data = await res.json();
3066
3194
  if (typeof data.license === "string") return data.license;
@@ -3073,7 +3201,7 @@ async function getLicenseNpm(baseUrl, token, repoName, packageName) {
3073
3201
  async function getLicenseNuget(baseUrl, token, repoName, packageName, version) {
3074
3202
  try {
3075
3203
  const lower = packageName.toLowerCase();
3076
- const res = await fetchWithTimeout2(
3204
+ const res = await fetchWithTimeout3(
3077
3205
  `${baseUrl}/api/nuget/v3/${repoName}/registration/${lower}/${version}.json`,
3078
3206
  token
3079
3207
  );
@@ -3125,15 +3253,20 @@ async function detectTier(config) {
3125
3253
  cachedTier = { tier, osvReachable, artifactoryReachable };
3126
3254
  return cachedTier;
3127
3255
  }
3256
+ async function checkSonatypeReachable() {
3257
+ const result = await checkSonatypeConnectivity();
3258
+ return result.reachable;
3259
+ }
3128
3260
  async function buildConnectivityData(config) {
3129
- const [osvResult, artResult] = await Promise.all([
3261
+ const [osvResult, artResult, sonatypeResult] = await Promise.all([
3130
3262
  checkOsvConnectivity(),
3131
- checkArtifactoryConnectivity(config.artifactory)
3263
+ checkArtifactoryConnectivity(config.artifactory),
3264
+ checkSonatypeConnectivity()
3132
3265
  ]);
3133
3266
  const artCfg = config.artifactory;
3134
3267
  let tier = "local";
3135
3268
  if (artResult.reachable && artResult.authenticated) tier = "artifactory";
3136
- if (osvResult.reachable) tier = "full";
3269
+ if (osvResult.reachable || sonatypeResult.reachable) tier = "full";
3137
3270
  cachedTier = {
3138
3271
  tier,
3139
3272
  osvReachable: osvResult.reachable,
@@ -3156,46 +3289,134 @@ async function buildConnectivityData(config) {
3156
3289
  url: "https://api.osv.dev",
3157
3290
  ...osvResult.error ? { error: osvResult.error } : {}
3158
3291
  },
3292
+ sonatype: {
3293
+ reachable: sonatypeResult.reachable,
3294
+ url: "https://ossindex.sonatype.org",
3295
+ ...sonatypeResult.error ? { error: sonatypeResult.error } : {}
3296
+ },
3159
3297
  tier,
3160
3298
  capabilities: {
3161
3299
  scan: true,
3162
3300
  diff: true,
3163
3301
  outdated: artResult.reachable && artResult.authenticated,
3164
3302
  licenseCheck: artResult.reachable && artResult.authenticated,
3165
- cveCheck: osvResult.reachable
3303
+ cveCheck: osvResult.reachable || sonatypeResult.reachable
3166
3304
  }
3167
3305
  };
3168
3306
  }
3169
3307
 
3170
3308
  // src/services/deps/frisk.ts
3171
- async function runFrisk(config, opts) {
3309
+ function toErrorMessage(err) {
3310
+ return err instanceof Error ? err.message : String(err);
3311
+ }
3312
+ async function runFrisk(config, opts, source = "osv") {
3172
3313
  const repoRoot = getRepoRoot();
3173
3314
  if (!repoRoot) {
3174
3315
  throw new PncliError("Not inside a git repository.", 1);
3175
3316
  }
3176
- const { tier, osvReachable } = await detectTier(config);
3177
- if (!osvReachable) {
3317
+ const { tier: baseTier, osvReachable } = await detectTier(config);
3318
+ const sonatypeReachable = source !== "osv" ? await checkSonatypeReachable() : false;
3319
+ const tier = sonatypeReachable ? "full" : baseTier;
3320
+ if (source === "osv" && !osvReachable) {
3178
3321
  throw new PncliError(
3179
3322
  "deps frisk requires OSV.dev access but api.osv.dev is not reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
3180
3323
  503
3181
3324
  );
3182
3325
  }
3326
+ if (source === "sonatype" && !sonatypeReachable) {
3327
+ throw new PncliError(
3328
+ "deps frisk --source sonatype requires Sonatype OSS Index access but ossindex.sonatype.org is not reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
3329
+ 503
3330
+ );
3331
+ }
3332
+ if (source === "all" && !osvReachable && !sonatypeReachable) {
3333
+ throw new PncliError(
3334
+ "deps frisk requires either OSV.dev or Sonatype OSS Index access, but neither is reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
3335
+ 503
3336
+ );
3337
+ }
3338
+ const sourceErrors = [];
3339
+ let sourcesQueried;
3340
+ if (source === "all") {
3341
+ if (!osvReachable) sourceErrors.push({ source: "osv", error: "api.osv.dev is not reachable \u2014 run pncli deps connectivity to diagnose." });
3342
+ if (!sonatypeReachable) sourceErrors.push({ source: "sonatype", error: "ossindex.sonatype.org is not reachable \u2014 run pncli deps connectivity to diagnose." });
3343
+ sourcesQueried = [
3344
+ ...osvReachable ? ["osv"] : [],
3345
+ ...sonatypeReachable ? ["sonatype"] : []
3346
+ ];
3347
+ } else {
3348
+ sourcesQueried = [source];
3349
+ }
3183
3350
  const scanOpts = {
3184
3351
  ...opts,
3185
3352
  includeTransitive: opts.includeTransitive ?? true
3186
3353
  };
3187
3354
  const scan = scanRepo(repoRoot, scanOpts);
3188
3355
  if (scan.packages.length === 0) {
3189
- return { tier, scanned: 0, vulnerable: 0, packages: [] };
3356
+ return { tier, source, sourcesQueried, sourceErrors, scanned: 0, vulnerable: 0, packages: [] };
3357
+ }
3358
+ let vulnerable;
3359
+ if (source === "sonatype") {
3360
+ vulnerable = await checkPackagesForVulnsViaSonatype(scan.packages);
3361
+ } else if (source === "all") {
3362
+ const [osvSettled, sonatypeSettled] = await Promise.allSettled([
3363
+ osvReachable ? checkPackagesForVulns(scan.packages) : Promise.resolve([]),
3364
+ sonatypeReachable ? checkPackagesForVulnsViaSonatype(scan.packages) : Promise.resolve([])
3365
+ ]);
3366
+ if (osvReachable) {
3367
+ if (osvSettled.status === "rejected") {
3368
+ sourceErrors.push({ source: "osv", error: toErrorMessage(osvSettled.reason) });
3369
+ sourcesQueried = sourcesQueried.filter((s) => s !== "osv");
3370
+ }
3371
+ }
3372
+ if (sonatypeReachable) {
3373
+ if (sonatypeSettled.status === "rejected") {
3374
+ sourceErrors.push({ source: "sonatype", error: toErrorMessage(sonatypeSettled.reason) });
3375
+ sourcesQueried = sourcesQueried.filter((s) => s !== "sonatype");
3376
+ }
3377
+ }
3378
+ if (sourcesQueried.length === 0) {
3379
+ throw new PncliError(
3380
+ "All vulnerability sources failed during scanning. Run 'pncli deps connectivity' to diagnose.",
3381
+ 503
3382
+ );
3383
+ }
3384
+ vulnerable = mergeVulnerablePackages(
3385
+ osvSettled.status === "fulfilled" ? osvSettled.value : [],
3386
+ sonatypeSettled.status === "fulfilled" ? sonatypeSettled.value : []
3387
+ );
3388
+ } else {
3389
+ vulnerable = await checkPackagesForVulns(scan.packages);
3190
3390
  }
3191
- const vulnerable = await checkPackagesForVulns(scan.packages);
3192
3391
  return {
3193
3392
  tier,
3393
+ source,
3394
+ sourcesQueried,
3395
+ sourceErrors,
3194
3396
  scanned: scan.packages.length,
3195
3397
  vulnerable: vulnerable.length,
3196
3398
  packages: vulnerable
3197
3399
  };
3198
3400
  }
3401
+ function mergeVulnerablePackages(osvResults, sonatypeResults) {
3402
+ const byKey = /* @__PURE__ */ new Map();
3403
+ for (const pkg2 of osvResults) {
3404
+ const key = `${pkg2.ecosystem}:${pkg2.name}@${pkg2.version}`;
3405
+ byKey.set(key, { ...pkg2 });
3406
+ }
3407
+ for (const pkg2 of sonatypeResults) {
3408
+ const key = `${pkg2.ecosystem}:${pkg2.name}@${pkg2.version}`;
3409
+ const existing = byKey.get(key);
3410
+ if (existing) {
3411
+ const existingIds = new Set(existing.vulnerabilities.map((v) => v.id));
3412
+ const newVulns = pkg2.vulnerabilities.filter((v) => !existingIds.has(v.id));
3413
+ existing.vulnerabilities = [...existing.vulnerabilities, ...newVulns];
3414
+ } else {
3415
+ byKey.set(key, { ...pkg2 });
3416
+ }
3417
+ }
3418
+ return [...byKey.values()];
3419
+ }
3199
3420
 
3200
3421
  // src/services/deps/outdated.ts
3201
3422
  async function runOutdated(config, opts, filterType) {
@@ -3270,7 +3491,7 @@ async function runLicenseCheck(config, opts) {
3270
3491
  // src/services/deps/commands.ts
3271
3492
  function registerDepsCommands(program2) {
3272
3493
  const deps = program2.command("deps").description("Dependency scanning, CVE detection, and license auditing");
3273
- deps.command("frisk").description("Scan all dependencies for CVEs and output remediation paths (requires OSV.dev)").option("--ecosystem <ecosystem>", "Filter to one ecosystem: npm, nuget, maven, all", "all").option("--direct-only", "Only scan direct dependencies (default: include transitive)", false).option("--include-dev", "Include dev/test dependencies", false).action(async (opts, cmd) => {
3494
+ deps.command("frisk").description("Scan all dependencies for CVEs and output remediation paths (requires OSV.dev or Sonatype OSS Index)").option("--ecosystem <ecosystem>", "Filter to one ecosystem: npm, nuget, maven, all", "all").option("--direct-only", "Only scan direct dependencies (default: include transitive)", false).option("--include-dev", "Include dev/test dependencies", false).addOption(new Option("--source <source>", "Vulnerability source").choices(["osv", "sonatype", "all"]).default("osv")).action(async (opts, cmd) => {
3274
3495
  const startTime = Date.now();
3275
3496
  try {
3276
3497
  const globalOpts = cmd.optsWithGlobals();
@@ -3280,7 +3501,7 @@ function registerDepsCommands(program2) {
3280
3501
  includeTransitive: !opts.directOnly,
3281
3502
  includeDev: opts.includeDev
3282
3503
  };
3283
- const data = await runFrisk(config, scanOpts);
3504
+ const data = await runFrisk(config, scanOpts, opts.source);
3284
3505
  success(data, "deps", "frisk", startTime);
3285
3506
  } catch (err) {
3286
3507
  fail(err, "deps", "frisk", startTime);
@@ -3346,7 +3567,7 @@ function registerDepsCommands(program2) {
3346
3567
  fail(err, "deps", "license-check", startTime);
3347
3568
  }
3348
3569
  });
3349
- deps.command("connectivity").description("Test network access to Artifactory and OSV.dev, report available tier").action(async (_opts, cmd) => {
3570
+ deps.command("connectivity").description("Test network access to Artifactory, OSV.dev, and Sonatype OSS Index, report available tier").action(async (_opts, cmd) => {
3350
3571
  const startTime = Date.now();
3351
3572
  try {
3352
3573
  const globalOpts = cmd.optsWithGlobals();
@@ -3652,6 +3873,27 @@ function registerConfigCommands(program2) {
3652
3873
  } else {
3653
3874
  results.checkmarx = { ok: null, message: "not configured" };
3654
3875
  }
3876
+ const snConfigured = cfg.servicenow.baseUrl && cfg.servicenow.username && (cfg.servicenow.password || cfg.servicenow.apiToken);
3877
+ if (snConfigured) {
3878
+ try {
3879
+ await http.servicenow("/api/now/table/change_request", { params: { sysparm_limit: 1 } });
3880
+ results.servicenow = { ok: true, message: "connected" };
3881
+ } catch (err) {
3882
+ results.servicenow = { ok: false, message: err instanceof Error ? err.message : String(err) };
3883
+ }
3884
+ } else {
3885
+ results.servicenow = { ok: null, message: "not configured" };
3886
+ }
3887
+ if (cfg.contrast.apiKey && cfg.contrast.serviceKey && cfg.contrast.username && cfg.contrast.orgUuid) {
3888
+ try {
3889
+ await http.contrast(`/Contrast/api/ng/${cfg.contrast.orgUuid}/applications`, { params: { limit: 1 } });
3890
+ results.contrast = { ok: true, message: "connected" };
3891
+ } catch (err) {
3892
+ results.contrast = { ok: false, message: err instanceof Error ? err.message : String(err) };
3893
+ }
3894
+ } else {
3895
+ results.contrast = { ok: null, message: "not configured" };
3896
+ }
3655
3897
  success(results, "config", "test", start);
3656
3898
  } catch (err) {
3657
3899
  fail(err, "config", "test", start);
@@ -3798,11 +4040,36 @@ function registerConfigCommands(program2) {
3798
4040
  results.checkmarx = categorize2(err);
3799
4041
  }
3800
4042
  }
4043
+ const snCreds = cfg.servicenow.username && (cfg.servicenow.password || cfg.servicenow.apiToken);
4044
+ if (!snCreds) {
4045
+ results.servicenow = { status: "blank", message: "not configured" };
4046
+ } else if (!cfg.servicenow.baseUrl) {
4047
+ results.servicenow = { status: "error", message: "baseUrl not configured" };
4048
+ } else {
4049
+ try {
4050
+ await http.servicenow("/api/now/table/change_request", { params: { sysparm_limit: 1 }, timeoutMs: 1e4 });
4051
+ results.servicenow = { status: "valid", message: "ok" };
4052
+ } catch (err) {
4053
+ results.servicenow = categorize2(err);
4054
+ }
4055
+ }
4056
+ if (!cfg.contrast.apiKey || !cfg.contrast.serviceKey || !cfg.contrast.username) {
4057
+ results.contrast = { status: "blank", message: "not configured" };
4058
+ } else if (!cfg.contrast.orgUuid) {
4059
+ results.contrast = { status: "error", message: "orgUuid not configured" };
4060
+ } else {
4061
+ try {
4062
+ await http.contrast(`/Contrast/api/ng/${cfg.contrast.orgUuid}/applications`, { params: { limit: 1 }, timeoutMs: 1e4 });
4063
+ results.contrast = { status: "valid", message: "ok" };
4064
+ } catch (err) {
4065
+ results.contrast = categorize2(err);
4066
+ }
4067
+ }
3801
4068
  const statuses = Object.values(results).map((r) => r.status);
3802
4069
  if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
3803
4070
  else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
3804
4071
  if (cmdOpts.output === "table") {
3805
- const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx"];
4072
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast"];
3806
4073
  const labelWidth = 14;
3807
4074
  const statusWidth = 9;
3808
4075
  for (const svc of services) {
@@ -3816,7 +4083,7 @@ function registerConfigCommands(program2) {
3816
4083
  process.stdout.write("\n");
3817
4084
  } else {
3818
4085
  if (opts.pretty) {
3819
- const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx"];
4086
+ const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast"];
3820
4087
  const labelWidth = 14;
3821
4088
  const statusWidth = 9;
3822
4089
  for (const svc of services) {
@@ -4149,6 +4416,114 @@ async function initGlobalConfig(start) {
4149
4416
  }
4150
4417
  }
4151
4418
  }
4419
+ process.stderr.write("\n\u2500\u2500 ServiceNow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
4420
+ const useServiceNow = await confirm({
4421
+ message: "Configure ServiceNow for change management?",
4422
+ default: false
4423
+ });
4424
+ let servicenowBaseUrl = "";
4425
+ let servicenowUsername = "";
4426
+ let servicenowPassword = "";
4427
+ let servicenowApiToken = "";
4428
+ if (useServiceNow) {
4429
+ servicenowBaseUrl = await input({
4430
+ message: "ServiceNow instance URL (e.g. https://mycompany.service-now.com):",
4431
+ default: ""
4432
+ });
4433
+ servicenowUsername = await input({
4434
+ message: "ServiceNow username:",
4435
+ default: ""
4436
+ });
4437
+ const snUseToken = await confirm({
4438
+ message: "Authenticate with an API token instead of a password?",
4439
+ default: false
4440
+ });
4441
+ if (snUseToken) {
4442
+ servicenowApiToken = await password({
4443
+ message: "ServiceNow API token:",
4444
+ validate: (v) => v.length > 0 || "Token cannot be blank"
4445
+ });
4446
+ } else {
4447
+ servicenowPassword = await password({
4448
+ message: "ServiceNow password:",
4449
+ validate: (v) => v.length > 0 || "Password cannot be blank"
4450
+ });
4451
+ }
4452
+ const snHasCreds = servicenowBaseUrl && servicenowUsername && (servicenowPassword || servicenowApiToken);
4453
+ if (snHasCreds) {
4454
+ process.stderr.write("\n Verifying connection...\n");
4455
+ try {
4456
+ const tempConfig = {
4457
+ ...loadConfig(),
4458
+ servicenow: {
4459
+ baseUrl: servicenowBaseUrl,
4460
+ username: servicenowUsername,
4461
+ password: servicenowPassword || void 0,
4462
+ apiToken: servicenowApiToken || void 0
4463
+ }
4464
+ };
4465
+ const tempHttp = createHttpClient(tempConfig);
4466
+ await tempHttp.servicenow("/api/now/table/change_request", { params: { sysparm_limit: 1 } });
4467
+ process.stderr.write(" Connected.\n");
4468
+ } catch (err) {
4469
+ warn(`Could not connect to ServiceNow: ${err instanceof Error ? err.message : String(err)}`);
4470
+ warn("Config will be saved anyway. Check your URL and credentials and re-run pncli config init or pncli config test.");
4471
+ }
4472
+ }
4473
+ }
4474
+ process.stderr.write("\n\u2500\u2500 Contrast IAST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
4475
+ const useContrast = await confirm({
4476
+ message: "Configure Contrast Security IAST for vulnerability findings?",
4477
+ default: false
4478
+ });
4479
+ let contrastBaseUrl = "";
4480
+ let contrastOrgUuid = "";
4481
+ let contrastUsername = "";
4482
+ let contrastApiKey = "";
4483
+ let contrastServiceKey = "";
4484
+ if (useContrast) {
4485
+ process.stderr.write(" Find your credentials in Contrast under User Settings \u2192 Your Keys.\n");
4486
+ contrastBaseUrl = await input({
4487
+ message: "Contrast base URL (leave blank for cloud: https://app.contrastsecurity.com):",
4488
+ default: ""
4489
+ });
4490
+ contrastOrgUuid = await input({
4491
+ message: "Organization UUID:",
4492
+ default: ""
4493
+ });
4494
+ contrastUsername = await input({
4495
+ message: "Username (email):",
4496
+ default: ""
4497
+ });
4498
+ contrastApiKey = await password({
4499
+ message: "API key:"
4500
+ });
4501
+ contrastServiceKey = await password({
4502
+ message: "Service key:"
4503
+ });
4504
+ const contrastHasCreds = contrastOrgUuid && contrastUsername && contrastApiKey && contrastServiceKey;
4505
+ if (contrastHasCreds) {
4506
+ process.stderr.write("\n Verifying connection...\n");
4507
+ try {
4508
+ const tempConfig = {
4509
+ ...loadConfig(),
4510
+ contrast: {
4511
+ baseUrl: contrastBaseUrl || void 0,
4512
+ orgUuid: contrastOrgUuid,
4513
+ username: contrastUsername,
4514
+ apiKey: contrastApiKey,
4515
+ serviceKey: contrastServiceKey
4516
+ }
4517
+ };
4518
+ const tempHttp = createHttpClient(tempConfig);
4519
+ await tempHttp.contrast(`/Contrast/api/ng/${contrastOrgUuid}/applications`, { params: { limit: 1 } });
4520
+ process.stderr.write(" Connected.\n");
4521
+ } catch (err) {
4522
+ warn(`Could not connect to Contrast: ${err instanceof Error ? err.message : String(err)}`);
4523
+ warn("Config will be saved anyway. Check your credentials and re-run pncli config init or pncli config test.");
4524
+ }
4525
+ }
4526
+ }
4152
4527
  process.stderr.write("\n\u2500\u2500 Defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
4153
4528
  const jiraProject = await input({
4154
4529
  message: "Default Jira project key (optional):",
@@ -4242,6 +4617,23 @@ async function initGlobalConfig(start) {
4242
4617
  password: checkmarxPassword || void 0
4243
4618
  }
4244
4619
  } : {},
4620
+ ...useServiceNow && servicenowBaseUrl ? {
4621
+ servicenow: {
4622
+ baseUrl: servicenowBaseUrl,
4623
+ username: servicenowUsername || void 0,
4624
+ ...servicenowApiToken ? { apiToken: servicenowApiToken } : {},
4625
+ ...servicenowPassword ? { password: servicenowPassword } : {}
4626
+ }
4627
+ } : {},
4628
+ ...useContrast && contrastOrgUuid ? {
4629
+ contrast: {
4630
+ ...contrastBaseUrl ? { baseUrl: contrastBaseUrl } : {},
4631
+ orgUuid: contrastOrgUuid,
4632
+ username: contrastUsername || void 0,
4633
+ apiKey: contrastApiKey || void 0,
4634
+ serviceKey: contrastServiceKey || void 0
4635
+ }
4636
+ } : {},
4245
4637
  defaults: {
4246
4638
  jira: {
4247
4639
  project: jiraProject || void 0
@@ -4697,7 +5089,7 @@ function registerAdoWorkCommands(ado) {
4697
5089
  fail(err, "ado", "work-get", start);
4698
5090
  }
4699
5091
  });
4700
- work.command("create").description("Create a work item").requiredOption("--type <type>", "Work item type (e.g. Bug, Task, User Story)").requiredOption("--title <title>", "Work item title").option("--description <text>", "Description").option("--assignee <user>", "Assigned to (display name or email)").option("--priority <n>", "Priority (1-4)").option("--field <name=value>", "Additional field (repeatable)", (v, acc) => {
5092
+ work.command("create").description("Create a work item").requiredOption("--type <type>", "Work item type (e.g. Bug, Task, User Story)").requiredOption("--title <title>", "Work item title").option("--description <text>", "Description").option("--assignee <user>", "Assigned to (display name or email)").option("--priority <n>", "Priority (1-4)").option("--parent <id>", "Parent work item ID \u2014 creates a parent link after creation").option("--field <name=value>", "Additional field (repeatable)", (v, acc) => {
4701
5093
  acc.push(v);
4702
5094
  return acc;
4703
5095
  }, []).action(async (opts) => {
@@ -4719,6 +5111,17 @@ function registerAdoWorkCommands(ado) {
4719
5111
  ];
4720
5112
  const data = await workClient.createWorkItem(collection, project, opts.type, patch);
4721
5113
  success(data, "ado", "work-create", start);
5114
+ if (opts.parent) {
5115
+ try {
5116
+ const baseUrl = config.ado.baseUrl?.replace(/\/$/, "");
5117
+ const targetUrl = `${baseUrl}/${encodeURIComponent(collection)}/_apis/wit/workitems/${opts.parent}`;
5118
+ const linkPatch = [{ op: "add", path: "/relations/-", value: { rel: "System.LinkTypes.Hierarchy-Reverse", url: targetUrl } }];
5119
+ await workClient.updateWorkItem(collection, data.id, linkPatch);
5120
+ } catch (linkErr) {
5121
+ warn(`Created #${data.id} but failed to link to parent ${opts.parent}: ${linkErr instanceof Error ? linkErr.message : linkErr}`);
5122
+ }
5123
+ }
5124
+ return;
4722
5125
  } catch (err) {
4723
5126
  fail(err, "ado", "work-create", start);
4724
5127
  }
@@ -5135,11 +5538,11 @@ function registerAdoRepoCommands(ado) {
5135
5538
  const start = Date.now();
5136
5539
  try {
5137
5540
  const globalOpts = ado.optsWithGlobals();
5138
- const { loadConfig: lc } = await import("./config-PXL632ZX.js");
5541
+ const { loadConfig: lc } = await import("./config-3VKBM32K.js");
5139
5542
  const config = lc({ configPath: globalOpts.config });
5140
5543
  const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
5141
5544
  const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
5142
- const { createHttpClient: createHttpClient2 } = await import("./http-TID3W235.js");
5545
+ const { createHttpClient: createHttpClient2 } = await import("./http-GVHZQCEY.js");
5143
5546
  const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
5144
5547
  const coreClient = new AdoCoreClient2(http);
5145
5548
  const me = await coreClient.getConnectionData(collection);
@@ -6007,6 +6410,180 @@ function registerCheckmarxCommands(program2) {
6007
6410
  });
6008
6411
  }
6009
6412
 
6413
+ // src/services/servicenow/commands.ts
6414
+ function getHttp(program2) {
6415
+ const opts = program2.optsWithGlobals();
6416
+ const config = loadConfig({ configPath: opts.config });
6417
+ if (!config.servicenow.baseUrl) throw new PncliError("ServiceNow not configured. Run: pncli config init");
6418
+ return createHttpClient(config, Boolean(opts.dryRun));
6419
+ }
6420
+ function registerServiceNowCommands(program2) {
6421
+ const sn = program2.command("servicenow").description("ServiceNow operations");
6422
+ const change = sn.command("change").description("Change request operations");
6423
+ change.command("list").description("List change requests").option("--state <state>", "Filter by state (e.g. -1=Pending, 1=Open, 2=Work in Progress, 3=Closed Complete)").option("--assigned-to <user>", "Filter by assigned user sys_id or display name").option("--limit <n>", "Maximum number of results", "25").option("--fields <fields>", "Comma-separated field names to return").action(async (opts) => {
6424
+ const start = Date.now();
6425
+ try {
6426
+ const http = getHttp(program2);
6427
+ const params = {
6428
+ sysparm_limit: opts.limit ? parseInt(opts.limit, 10) : 25,
6429
+ sysparm_display_value: "true"
6430
+ };
6431
+ const sanitize = (v) => v.replace(/\^/g, "");
6432
+ const queries = [];
6433
+ if (opts.state !== void 0) queries.push(`state=${sanitize(opts.state)}`);
6434
+ if (opts.assignedTo) queries.push(`assigned_to=${sanitize(opts.assignedTo)}`);
6435
+ if (queries.length) params["sysparm_query"] = queries.join("^");
6436
+ if (opts.fields) params["sysparm_fields"] = opts.fields;
6437
+ const data = await http.servicenow("/api/now/table/change_request", { params });
6438
+ success(data.result, "servicenow", "change list", start);
6439
+ } catch (err) {
6440
+ fail(err, "servicenow", "change list", start);
6441
+ }
6442
+ });
6443
+ change.command("get").description("Get a change request by sys_id or number").requiredOption("--id <id>", "Change request sys_id or number (e.g. CHG0001234)").action(async (opts) => {
6444
+ const start = Date.now();
6445
+ try {
6446
+ const http = getHttp(program2);
6447
+ if (opts.id.toUpperCase().startsWith("CHG")) {
6448
+ const list = await http.servicenow("/api/now/table/change_request", {
6449
+ params: { sysparm_query: `number=${opts.id}`, sysparm_display_value: "true", sysparm_limit: 1 }
6450
+ });
6451
+ if (!list.result.length) throw new PncliError(`Change request not found: ${opts.id}`, 1);
6452
+ success(list.result[0], "servicenow", "change get", start);
6453
+ return;
6454
+ }
6455
+ const data = await http.servicenow(`/api/now/table/change_request/${opts.id}`, {
6456
+ params: { sysparm_display_value: "true" }
6457
+ });
6458
+ success(data.result, "servicenow", "change get", start);
6459
+ } catch (err) {
6460
+ fail(err, "servicenow", "change get", start);
6461
+ }
6462
+ });
6463
+ change.command("create").description("Create a change request").requiredOption("--short-description <text>", "Short description (title)").option("--description <text>", "Full description").option("--type <type>", "Change type: normal, standard, or emergency", "normal").option("--priority <n>", "Priority: 1=Critical, 2=High, 3=Moderate, 4=Low").option("--assigned-to <user>", "Assigned user sys_id").option("--assignment-group <group>", "Assignment group sys_id").option("--start-date <datetime>", "Planned start date (yyyy-MM-dd HH:mm:ss)").option("--end-date <datetime>", "Planned end date (yyyy-MM-dd HH:mm:ss)").action(async (opts) => {
6464
+ const start = Date.now();
6465
+ try {
6466
+ const http = getHttp(program2);
6467
+ const body = {
6468
+ short_description: opts.shortDescription,
6469
+ type: opts.type ?? "normal"
6470
+ };
6471
+ if (opts.description) body["description"] = opts.description;
6472
+ if (opts.priority) body["priority"] = opts.priority;
6473
+ if (opts.assignedTo) body["assigned_to"] = opts.assignedTo;
6474
+ if (opts.assignmentGroup) body["assignment_group"] = opts.assignmentGroup;
6475
+ if (opts.startDate) body["start_date"] = opts.startDate;
6476
+ if (opts.endDate) body["end_date"] = opts.endDate;
6477
+ const data = await http.servicenow("/api/now/table/change_request", {
6478
+ method: "POST",
6479
+ body
6480
+ });
6481
+ success(data.result, "servicenow", "change create", start);
6482
+ } catch (err) {
6483
+ fail(err, "servicenow", "change create", start);
6484
+ }
6485
+ });
6486
+ change.command("update").description("Update a change request").requiredOption("--id <sys_id>", "Change request sys_id").option("--short-description <text>", "New short description").option("--description <text>", "New description").option("--state <state>", "New state value").option("--priority <n>", "New priority").option("--assigned-to <user>", "New assigned user sys_id").option("--start-date <datetime>", "New planned start date").option("--end-date <datetime>", "New planned end date").action(async (opts) => {
6487
+ const start = Date.now();
6488
+ try {
6489
+ const http = getHttp(program2);
6490
+ const body = {};
6491
+ if (opts.shortDescription) body["short_description"] = opts.shortDescription;
6492
+ if (opts.description) body["description"] = opts.description;
6493
+ if (opts.state !== void 0) body["state"] = opts.state;
6494
+ if (opts.priority) body["priority"] = opts.priority;
6495
+ if (opts.assignedTo) body["assigned_to"] = opts.assignedTo;
6496
+ if (opts.startDate) body["start_date"] = opts.startDate;
6497
+ if (opts.endDate) body["end_date"] = opts.endDate;
6498
+ if (Object.keys(body).length === 0) throw new PncliError("No fields to update. Provide at least one option besides --id.", 1);
6499
+ const data = await http.servicenow(`/api/now/table/change_request/${opts.id}`, {
6500
+ method: "PATCH",
6501
+ body
6502
+ });
6503
+ success(data.result, "servicenow", "change update", start);
6504
+ } catch (err) {
6505
+ fail(err, "servicenow", "change update", start);
6506
+ }
6507
+ });
6508
+ change.command("close").description("Close a change request (state=3)").requiredOption("--id <sys_id>", "Change request sys_id").option("--close-notes <text>", "Closure notes").option("--close-code <code>", "Close code (e.g. successful, unsuccessful)").action(async (opts) => {
6509
+ const start = Date.now();
6510
+ try {
6511
+ const http = getHttp(program2);
6512
+ const body = { state: 3 };
6513
+ if (opts.closeNotes) body["close_notes"] = opts.closeNotes;
6514
+ if (opts.closeCode) body["close_code"] = opts.closeCode;
6515
+ const data = await http.servicenow(`/api/now/table/change_request/${opts.id}`, {
6516
+ method: "PATCH",
6517
+ body
6518
+ });
6519
+ success(data.result, "servicenow", "change close", start);
6520
+ } catch (err) {
6521
+ fail(err, "servicenow", "change close", start);
6522
+ }
6523
+ });
6524
+ }
6525
+
6526
+ // src/services/contrast/commands.ts
6527
+ function getHttpAndOrg(program2) {
6528
+ const opts = program2.optsWithGlobals();
6529
+ const config = loadConfig({ configPath: opts.config });
6530
+ if (!config.contrast.apiKey || !config.contrast.serviceKey || !config.contrast.username) throw new PncliError("Contrast not configured. Run: pncli config init");
6531
+ if (!config.contrast.orgUuid) throw new PncliError("Contrast org UUID not configured. Run: pncli config set contrast.orgUuid <uuid>");
6532
+ const http = createHttpClient(config, Boolean(opts.dryRun));
6533
+ return { http, orgUuid: config.contrast.orgUuid };
6534
+ }
6535
+ function registerContrastCommands(program2) {
6536
+ const contrast = program2.command("contrast").description("Contrast IAST operations");
6537
+ const apps = contrast.command("apps").description("Application operations");
6538
+ apps.command("list").description("List applications in the organization").option("--limit <n>", "Maximum number of results", "25").option("--offset <n>", "Pagination offset", "0").action(async (opts) => {
6539
+ const start = Date.now();
6540
+ try {
6541
+ const { http, orgUuid } = getHttpAndOrg(program2);
6542
+ const data = await http.contrast(`/Contrast/api/ng/${orgUuid}/applications`, {
6543
+ params: {
6544
+ limit: opts.limit ? parseInt(opts.limit, 10) : 25,
6545
+ offset: opts.offset ? parseInt(opts.offset, 10) : 0
6546
+ }
6547
+ });
6548
+ success(data, "contrast", "apps list", start);
6549
+ } catch (err) {
6550
+ fail(err, "contrast", "apps list", start);
6551
+ }
6552
+ });
6553
+ const findings = contrast.command("findings").description("Vulnerability findings operations");
6554
+ findings.command("list").description("List vulnerability findings for an application").requiredOption("--app <app-id>", "Application ID (UUID)").option("--severity <level>", "Filter by severity: CRITICAL, HIGH, MEDIUM, LOW, NOTE").option("--status <status>", "Filter by status: REPORTED, CONFIRMED, SUSPICIOUS, NOT_A_PROBLEM, REMEDIATED, FIXED").option("--limit <n>", "Maximum number of results", "25").option("--offset <n>", "Pagination offset", "0").action(async (opts) => {
6555
+ const start = Date.now();
6556
+ try {
6557
+ const { http, orgUuid } = getHttpAndOrg(program2);
6558
+ const params = {
6559
+ limit: opts.limit ? parseInt(opts.limit, 10) : 25,
6560
+ offset: opts.offset ? parseInt(opts.offset, 10) : 0
6561
+ };
6562
+ if (opts.severity) params["severities"] = opts.severity;
6563
+ if (opts.status) params["statuses"] = opts.status;
6564
+ const data = await http.contrast(
6565
+ `/Contrast/api/ng/${orgUuid}/traces/${opts.app}/filter`,
6566
+ { params }
6567
+ );
6568
+ success(data, "contrast", "findings list", start);
6569
+ } catch (err) {
6570
+ fail(err, "contrast", "findings list", start);
6571
+ }
6572
+ });
6573
+ findings.command("get").description("Get details for a specific vulnerability finding").requiredOption("--app <app-id>", "Application ID (UUID)").requiredOption("--trace <trace-id>", "Trace/finding UUID").action(async (opts) => {
6574
+ const start = Date.now();
6575
+ try {
6576
+ const { http, orgUuid } = getHttpAndOrg(program2);
6577
+ const data = await http.contrast(
6578
+ `/Contrast/api/ng/${orgUuid}/traces/${opts.app}/trace/${opts.trace}`
6579
+ );
6580
+ success(data, "contrast", "findings get", start);
6581
+ } catch (err) {
6582
+ fail(err, "contrast", "findings get", start);
6583
+ }
6584
+ });
6585
+ }
6586
+
6010
6587
  // src/services/skills/commands.ts
6011
6588
  import fs3 from "fs";
6012
6589
  import path5 from "path";
@@ -6165,7 +6742,7 @@ if (!process.env.PNCLI_VERIFY_TLS) {
6165
6742
  var require2 = createRequire(import.meta.url);
6166
6743
  var pkg = require2("../package.json");
6167
6744
  var TAGLINE = "One command does what three meetings couldn't.";
6168
- var program = new Command();
6745
+ var program = new Command2();
6169
6746
  program.name("pncli").description(`The Paperwork Nightmare CLI \u2014 ${TAGLINE}`).version(`${pkg.version} \u2014 ${TAGLINE}`, "-v, --version").option("--pretty", "Human-readable formatted output", false).option("--verbose", "Include full response metadata", false).option("--dry-run", "Print API requests without executing", false).option("--config <path>", "Override global config file location").option("--output-file <path>", "Write JSON output to file instead of stdout");
6170
6747
  program.hook("preAction", (thisCommand) => {
6171
6748
  const opts = thisCommand.optsWithGlobals();
@@ -6193,6 +6770,8 @@ registerJenkinsCommands(program);
6193
6770
  registerArtifactoryCommands(program);
6194
6771
  registerUdeployCommands(program);
6195
6772
  registerCheckmarxCommands(program);
6773
+ registerServiceNowCommands(program);
6774
+ registerContrastCommands(program);
6196
6775
  registerSkillsCommands(program);
6197
6776
  program.addHelpText("after", `
6198
6777
  Services:
@@ -6208,6 +6787,8 @@ Services:
6208
6787
  artifactory Artifactory (repos, artifact search, build info, properties)
6209
6788
  udeploy IBM UrbanCode Deploy (component versions, deployment processes)
6210
6789
  checkmarx Checkmarx CxSAST (projects, scans, scan statistics)
6790
+ servicenow ServiceNow (change requests)
6791
+ contrast Contrast IAST (applications, vulnerability findings)
6211
6792
  config Manage pncli configuration
6212
6793
  skills Download and manage Claude Code skills
6213
6794
  `);