@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/copilot-instructions.md +268 -8
- package/dist/{chunk-QQ7UKWTU.js → chunk-QQZJQ5DP.js} +75 -1
- package/dist/chunk-QQZJQ5DP.js.map +1 -0
- package/dist/{chunk-S4SJLY4M.js → chunk-YR6GFJMV.js} +33 -1
- package/dist/chunk-YR6GFJMV.js.map +1 -0
- package/dist/cli.js +612 -31
- package/dist/cli.js.map +1 -1
- package/dist/{config-PXL632ZX.js → config-3VKBM32K.js} +2 -2
- package/dist/{http-TID3W235.js → http-GVHZQCEY.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-QQ7UKWTU.js.map +0 -1
- package/dist/chunk-S4SJLY4M.js.map +0 -1
- /package/dist/{config-PXL632ZX.js.map → config-3VKBM32K.js.map} +0 -0
- /package/dist/{http-TID3W235.js.map → http-GVHZQCEY.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
setRepoConfigValue,
|
|
8
8
|
writeGlobalConfig,
|
|
9
9
|
writeRepoConfig
|
|
10
|
-
} from "./chunk-
|
|
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-
|
|
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/
|
|
2897
|
-
var
|
|
2898
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
`);
|