@kolatts/pncli 1.8.0 → 1.10.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-3XHTRFRX.js} +108 -1
- package/dist/chunk-3XHTRFRX.js.map +1 -0
- package/dist/{chunk-S4SJLY4M.js → chunk-O3LWAWSN.js} +45 -1
- package/dist/chunk-O3LWAWSN.js.map +1 -0
- package/dist/cli.js +1016 -60
- package/dist/cli.js.map +1 -1
- package/dist/{config-PXL632ZX.js → config-NCI2BTQG.js} +2 -2
- package/dist/{http-TID3W235.js → http-L2P23H2D.js} +2 -2
- package/package.json +2 -1
- package/skills/address-pr-feedback/SKILL.md +87 -0
- package/skills/code-review/SKILL.md +82 -0
- package/skills/local-setup/SKILL.md +225 -0
- package/skills/plan/SKILL.md +120 -0
- package/skills/security-review/SKILL.md +108 -0
- package/skills/ship/SKILL.md +170 -0
- package/dist/chunk-QQ7UKWTU.js.map +0 -1
- package/dist/chunk-S4SJLY4M.js.map +0 -1
- /package/dist/{config-PXL632ZX.js.map → config-NCI2BTQG.js.map} +0 -0
- /package/dist/{http-TID3W235.js.map → http-L2P23H2D.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-O3LWAWSN.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-3XHTRFRX.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,341 @@ 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/sonatypeiq.ts
|
|
3025
|
+
var TIMEOUT_MS3 = 6e4;
|
|
3026
|
+
var MAX_POLL_ATTEMPTS = 10;
|
|
3027
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
3028
|
+
var BATCH_SIZE3 = 128;
|
|
3029
|
+
async function fetchWithTimeout3(url, init) {
|
|
3030
|
+
const controller = new AbortController();
|
|
3031
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS3);
|
|
3032
|
+
try {
|
|
3033
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
3034
|
+
} finally {
|
|
3035
|
+
clearTimeout(timer);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
function buildBasicAuth(userCode, passcode) {
|
|
3039
|
+
return `Basic ${Buffer.from(`${userCode}:${passcode}`).toString("base64")}`;
|
|
3040
|
+
}
|
|
3041
|
+
function threatLevelToSeverity(level) {
|
|
3042
|
+
if (level >= 9) return "CRITICAL";
|
|
3043
|
+
if (level >= 7) return "HIGH";
|
|
3044
|
+
if (level >= 4) return "MEDIUM";
|
|
3045
|
+
if (level > 0) return "LOW";
|
|
3046
|
+
return "UNKNOWN";
|
|
3047
|
+
}
|
|
3048
|
+
function toPurl2(pkg2) {
|
|
3049
|
+
switch (pkg2.ecosystem) {
|
|
3050
|
+
case "npm": {
|
|
3051
|
+
const encodedName = pkg2.name.startsWith("@") ? pkg2.name.replace("@", "%40") : pkg2.name;
|
|
3052
|
+
return `pkg:npm/${encodedName}@${pkg2.version}`;
|
|
3053
|
+
}
|
|
3054
|
+
case "nuget":
|
|
3055
|
+
return `pkg:nuget/${pkg2.name}@${pkg2.version}`;
|
|
3056
|
+
case "maven": {
|
|
3057
|
+
const colonIdx = pkg2.name.indexOf(":");
|
|
3058
|
+
if (colonIdx !== -1) {
|
|
3059
|
+
const groupId = pkg2.name.slice(0, colonIdx);
|
|
3060
|
+
const artifactId = pkg2.name.slice(colonIdx + 1);
|
|
3061
|
+
return `pkg:maven/${encodeURIComponent(groupId)}/${encodeURIComponent(artifactId)}@${pkg2.version}`;
|
|
3062
|
+
}
|
|
3063
|
+
return `pkg:maven/${encodeURIComponent(pkg2.name)}@${pkg2.version}`;
|
|
3064
|
+
}
|
|
3065
|
+
default:
|
|
3066
|
+
return `pkg:generic/${encodeURIComponent(pkg2.name)}@${pkg2.version}`;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
function mapViolationToVuln(violation) {
|
|
3070
|
+
const cveRefs = violation.constraintViolations.flatMap(
|
|
3071
|
+
(cv) => cv.reasons.filter((r) => r.reference?.type === "VULNERABILITY").map((r) => r.reference.value)
|
|
3072
|
+
);
|
|
3073
|
+
const id = cveRefs[0] ?? violation.policyId;
|
|
3074
|
+
const severity = threatLevelToSeverity(violation.policyThreatLevel);
|
|
3075
|
+
return {
|
|
3076
|
+
id,
|
|
3077
|
+
summary: violation.policyName,
|
|
3078
|
+
severity,
|
|
3079
|
+
cvss: null,
|
|
3080
|
+
aliases: [...new Set(cveRefs)],
|
|
3081
|
+
remediation: {
|
|
3082
|
+
fixAvailable: false,
|
|
3083
|
+
fixedVersions: [],
|
|
3084
|
+
advice: `Policy violation: ${violation.policyName} (threat level ${violation.policyThreatLevel}). Check Sonatype IQ Server for details.`
|
|
3085
|
+
},
|
|
3086
|
+
references: []
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
function syntheticVulnsFromCounts(criticalCount, severeCount, moderateCount, pkgName) {
|
|
3090
|
+
const vulns = [];
|
|
3091
|
+
if (criticalCount > 0) {
|
|
3092
|
+
vulns.push({
|
|
3093
|
+
id: `iq-critical-${pkgName}`,
|
|
3094
|
+
summary: `${criticalCount} critical vulnerability(ies) detected by Sonatype IQ`,
|
|
3095
|
+
severity: "CRITICAL",
|
|
3096
|
+
cvss: null,
|
|
3097
|
+
aliases: [],
|
|
3098
|
+
remediation: { fixAvailable: false, fixedVersions: [], advice: "Check Sonatype IQ Server for details." },
|
|
3099
|
+
references: []
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
if (severeCount > 0) {
|
|
3103
|
+
vulns.push({
|
|
3104
|
+
id: `iq-high-${pkgName}`,
|
|
3105
|
+
summary: `${severeCount} severe vulnerability(ies) detected by Sonatype IQ`,
|
|
3106
|
+
severity: "HIGH",
|
|
3107
|
+
cvss: null,
|
|
3108
|
+
aliases: [],
|
|
3109
|
+
remediation: { fixAvailable: false, fixedVersions: [], advice: "Check Sonatype IQ Server for details." },
|
|
3110
|
+
references: []
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
if (moderateCount > 0) {
|
|
3114
|
+
vulns.push({
|
|
3115
|
+
id: `iq-medium-${pkgName}`,
|
|
3116
|
+
summary: `${moderateCount} moderate vulnerability(ies) detected by Sonatype IQ`,
|
|
3117
|
+
severity: "MEDIUM",
|
|
3118
|
+
cvss: null,
|
|
3119
|
+
aliases: [],
|
|
3120
|
+
remediation: { fixAvailable: false, fixedVersions: [], advice: "Check Sonatype IQ Server for details." },
|
|
3121
|
+
references: []
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
return vulns;
|
|
3125
|
+
}
|
|
3126
|
+
async function resolveApplicationInternalId(baseUrl, applicationId, auth) {
|
|
3127
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/v2/applications?publicId=${encodeURIComponent(applicationId)}`;
|
|
3128
|
+
const res = await fetchWithTimeout3(url, {
|
|
3129
|
+
method: "GET",
|
|
3130
|
+
headers: { "Authorization": auth, "Accept": "application/json" }
|
|
3131
|
+
});
|
|
3132
|
+
if (!res.ok) return applicationId;
|
|
3133
|
+
const data = await res.json();
|
|
3134
|
+
if (data.applications && data.applications.length > 0) {
|
|
3135
|
+
return data.applications[0].id;
|
|
3136
|
+
}
|
|
3137
|
+
return applicationId;
|
|
3138
|
+
}
|
|
3139
|
+
async function pollResults(baseUrl, resultsUrl, auth) {
|
|
3140
|
+
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
3141
|
+
if (attempt > 0) {
|
|
3142
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS * attempt));
|
|
3143
|
+
}
|
|
3144
|
+
const url = resultsUrl.startsWith("http") ? resultsUrl : `${baseUrl.replace(/\/$/, "")}${resultsUrl}`;
|
|
3145
|
+
const res = await fetchWithTimeout3(url, {
|
|
3146
|
+
method: "GET",
|
|
3147
|
+
headers: { "Authorization": auth, "Accept": "application/json" }
|
|
3148
|
+
});
|
|
3149
|
+
if (!res.ok) {
|
|
3150
|
+
throw new Error(`Sonatype IQ Server returned HTTP ${res.status} polling results`);
|
|
3151
|
+
}
|
|
3152
|
+
const data = await res.json();
|
|
3153
|
+
if (!data.isQueued) return data;
|
|
3154
|
+
}
|
|
3155
|
+
throw new Error("Sonatype IQ Server evaluation timed out waiting for results.");
|
|
3156
|
+
}
|
|
3157
|
+
async function evaluateBatch(purls, packages, baseUrl, applicationId, auth) {
|
|
3158
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/v2/evaluation/applications/${encodeURIComponent(applicationId)}`;
|
|
3159
|
+
const res = await fetchWithTimeout3(url, {
|
|
3160
|
+
method: "POST",
|
|
3161
|
+
headers: { "Authorization": auth, "Content-Type": "application/json", "Accept": "application/json" },
|
|
3162
|
+
body: JSON.stringify({ components: purls.map((p) => ({ packageUrl: p })) })
|
|
3163
|
+
});
|
|
3164
|
+
if (!res.ok) {
|
|
3165
|
+
throw new Error(`Sonatype IQ Server returned HTTP ${res.status}`);
|
|
3166
|
+
}
|
|
3167
|
+
let data = await res.json();
|
|
3168
|
+
if (data.isQueued && data.resultsUrl) {
|
|
3169
|
+
data = await pollResults(baseUrl, data.resultsUrl, auth);
|
|
3170
|
+
}
|
|
3171
|
+
const components = data.components ?? [];
|
|
3172
|
+
const vulnerable = [];
|
|
3173
|
+
const purlToPackage = /* @__PURE__ */ new Map();
|
|
3174
|
+
for (let i = 0; i < purls.length; i++) {
|
|
3175
|
+
purlToPackage.set(purls[i], packages[i]);
|
|
3176
|
+
}
|
|
3177
|
+
for (const result of components) {
|
|
3178
|
+
if (!result?.component?.packageUrl) continue;
|
|
3179
|
+
const pkg2 = purlToPackage.get(result.component.packageUrl);
|
|
3180
|
+
if (!pkg2) continue;
|
|
3181
|
+
const criticalCount = result.criticalVulnerabilityCount ?? 0;
|
|
3182
|
+
const severeCount = result.severeVulnerabilityCount ?? 0;
|
|
3183
|
+
const moderateCount = result.moderateVulnerabilityCount ?? 0;
|
|
3184
|
+
const totalVulns = criticalCount + severeCount + moderateCount;
|
|
3185
|
+
const violations = result.policyData?.policyViolations ?? [];
|
|
3186
|
+
const securityViolations = violations.filter((v) => v.policyThreatCategory === "SECURITY");
|
|
3187
|
+
if (securityViolations.length === 0 && totalVulns === 0) continue;
|
|
3188
|
+
const vulnerabilities = securityViolations.length > 0 ? securityViolations.map((v) => mapViolationToVuln(v)) : syntheticVulnsFromCounts(criticalCount, severeCount, moderateCount, pkg2.name);
|
|
3189
|
+
vulnerable.push({ ...pkg2, vulnerabilities });
|
|
3190
|
+
}
|
|
3191
|
+
return vulnerable;
|
|
3192
|
+
}
|
|
3193
|
+
async function checkPackagesViaIqServer(packages, applicationId, config) {
|
|
3194
|
+
const { baseUrl, userCode, passcode } = config.sonatypeiq;
|
|
3195
|
+
if (!baseUrl) throw new Error("Sonatype IQ Server baseUrl not configured.");
|
|
3196
|
+
if (!userCode || !passcode) throw new Error("Sonatype IQ Server credentials not configured.");
|
|
3197
|
+
const auth = buildBasicAuth(userCode, passcode);
|
|
3198
|
+
const internalId = await resolveApplicationInternalId(baseUrl, applicationId, auth);
|
|
3199
|
+
const allVulnerable = [];
|
|
3200
|
+
for (let i = 0; i < packages.length; i += BATCH_SIZE3) {
|
|
3201
|
+
const chunk = packages.slice(i, i + BATCH_SIZE3);
|
|
3202
|
+
const purls = chunk.map(toPurl2);
|
|
3203
|
+
const results = await evaluateBatch(purls, chunk, baseUrl, internalId, auth);
|
|
3204
|
+
allVulnerable.push(...results);
|
|
3205
|
+
}
|
|
3206
|
+
return allVulnerable;
|
|
3207
|
+
}
|
|
3208
|
+
async function checkSonatypeIqConnectivity(baseUrl, userCode, passcode) {
|
|
3209
|
+
try {
|
|
3210
|
+
const auth = buildBasicAuth(userCode, passcode);
|
|
3211
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/v2/applications?limit=1`;
|
|
3212
|
+
const controller = new AbortController();
|
|
3213
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
3214
|
+
let res;
|
|
3215
|
+
try {
|
|
3216
|
+
res = await fetch(url, {
|
|
3217
|
+
method: "GET",
|
|
3218
|
+
headers: { "Authorization": auth, "Accept": "application/json" },
|
|
3219
|
+
signal: controller.signal
|
|
3220
|
+
});
|
|
3221
|
+
} finally {
|
|
3222
|
+
clearTimeout(timer);
|
|
3223
|
+
}
|
|
3224
|
+
if (res.status === 401 || res.status === 403) {
|
|
3225
|
+
return { reachable: true, authenticated: false, error: `Authentication failed (HTTP ${res.status})` };
|
|
3226
|
+
}
|
|
3227
|
+
return { reachable: true, authenticated: res.ok };
|
|
3228
|
+
} catch (err) {
|
|
3229
|
+
return {
|
|
3230
|
+
reachable: false,
|
|
3231
|
+
authenticated: false,
|
|
3232
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
// src/services/deps/clients/artifactory.ts
|
|
3238
|
+
var TIMEOUT_MS4 = 15e3;
|
|
3239
|
+
async function fetchWithTimeout4(url, token) {
|
|
3240
|
+
const controller = new AbortController();
|
|
3241
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS4);
|
|
2901
3242
|
try {
|
|
2902
3243
|
return await fetch(url, {
|
|
2903
3244
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -2933,7 +3274,7 @@ async function checkArtifactoryConnectivity(config) {
|
|
|
2933
3274
|
};
|
|
2934
3275
|
}
|
|
2935
3276
|
try {
|
|
2936
|
-
const res = await
|
|
3277
|
+
const res = await fetchWithTimeout4(`${config.baseUrl}/api/system/ping`, config.token);
|
|
2937
3278
|
if (res.status === 401 || res.status === 403) {
|
|
2938
3279
|
return {
|
|
2939
3280
|
reachable: true,
|
|
@@ -2983,7 +3324,7 @@ function repoForEcosystem(config, eco) {
|
|
|
2983
3324
|
async function getLatestNpm(baseUrl, token, repoName, packageName) {
|
|
2984
3325
|
try {
|
|
2985
3326
|
const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
|
|
2986
|
-
const res = await
|
|
3327
|
+
const res = await fetchWithTimeout4(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
|
|
2987
3328
|
if (!res.ok) return null;
|
|
2988
3329
|
const data = await res.json();
|
|
2989
3330
|
return data["dist-tags"]?.latest ?? null;
|
|
@@ -2994,7 +3335,7 @@ async function getLatestNpm(baseUrl, token, repoName, packageName) {
|
|
|
2994
3335
|
async function getLatestNuget(baseUrl, token, repoName, packageName) {
|
|
2995
3336
|
try {
|
|
2996
3337
|
const lower = packageName.toLowerCase();
|
|
2997
|
-
const res = await
|
|
3338
|
+
const res = await fetchWithTimeout4(
|
|
2998
3339
|
`${baseUrl}/api/nuget/v3/${repoName}/flatcontainer/${lower}/index.json`,
|
|
2999
3340
|
token
|
|
3000
3341
|
);
|
|
@@ -3010,7 +3351,7 @@ async function getLatestMaven(baseUrl, token, repoName, packageName) {
|
|
|
3010
3351
|
const [groupId, artifactId] = packageName.split(":");
|
|
3011
3352
|
if (!groupId || !artifactId) return null;
|
|
3012
3353
|
try {
|
|
3013
|
-
const res = await
|
|
3354
|
+
const res = await fetchWithTimeout4(
|
|
3014
3355
|
`${baseUrl}/api/search/latestVersion?g=${encodeURIComponent(groupId)}&a=${encodeURIComponent(artifactId)}&repos=${repoName}`,
|
|
3015
3356
|
token
|
|
3016
3357
|
);
|
|
@@ -3060,7 +3401,7 @@ async function getOutdatedPackages(packages, config, filterType) {
|
|
|
3060
3401
|
async function getLicenseNpm(baseUrl, token, repoName, packageName) {
|
|
3061
3402
|
try {
|
|
3062
3403
|
const encoded = packageName.startsWith("@") ? packageName.replace("/", "%2F") : packageName;
|
|
3063
|
-
const res = await
|
|
3404
|
+
const res = await fetchWithTimeout4(`${baseUrl}/api/npm/${repoName}/${encoded}`, token);
|
|
3064
3405
|
if (!res.ok) return null;
|
|
3065
3406
|
const data = await res.json();
|
|
3066
3407
|
if (typeof data.license === "string") return data.license;
|
|
@@ -3073,7 +3414,7 @@ async function getLicenseNpm(baseUrl, token, repoName, packageName) {
|
|
|
3073
3414
|
async function getLicenseNuget(baseUrl, token, repoName, packageName, version) {
|
|
3074
3415
|
try {
|
|
3075
3416
|
const lower = packageName.toLowerCase();
|
|
3076
|
-
const res = await
|
|
3417
|
+
const res = await fetchWithTimeout4(
|
|
3077
3418
|
`${baseUrl}/api/nuget/v3/${repoName}/registration/${lower}/${version}.json`,
|
|
3078
3419
|
token
|
|
3079
3420
|
);
|
|
@@ -3125,15 +3466,24 @@ async function detectTier(config) {
|
|
|
3125
3466
|
cachedTier = { tier, osvReachable, artifactoryReachable };
|
|
3126
3467
|
return cachedTier;
|
|
3127
3468
|
}
|
|
3469
|
+
async function checkSonatypeReachable() {
|
|
3470
|
+
const result = await checkSonatypeConnectivity();
|
|
3471
|
+
return result.reachable;
|
|
3472
|
+
}
|
|
3128
3473
|
async function buildConnectivityData(config) {
|
|
3129
|
-
const
|
|
3474
|
+
const iqCfg = config.sonatypeiq;
|
|
3475
|
+
const iqConfigured = !!(iqCfg.baseUrl && iqCfg.userCode && iqCfg.passcode);
|
|
3476
|
+
const checks = await Promise.all([
|
|
3130
3477
|
checkOsvConnectivity(),
|
|
3131
|
-
checkArtifactoryConnectivity(config.artifactory)
|
|
3478
|
+
checkArtifactoryConnectivity(config.artifactory),
|
|
3479
|
+
checkSonatypeConnectivity(),
|
|
3480
|
+
iqConfigured ? checkSonatypeIqConnectivity(iqCfg.baseUrl, iqCfg.userCode, iqCfg.passcode) : Promise.resolve(null)
|
|
3132
3481
|
]);
|
|
3482
|
+
const [osvResult, artResult, sonatypeResult, iqResult] = checks;
|
|
3133
3483
|
const artCfg = config.artifactory;
|
|
3134
3484
|
let tier = "local";
|
|
3135
3485
|
if (artResult.reachable && artResult.authenticated) tier = "artifactory";
|
|
3136
|
-
if (osvResult.reachable) tier = "full";
|
|
3486
|
+
if (osvResult.reachable || sonatypeResult.reachable) tier = "full";
|
|
3137
3487
|
cachedTier = {
|
|
3138
3488
|
tier,
|
|
3139
3489
|
osvReachable: osvResult.reachable,
|
|
@@ -3156,46 +3506,158 @@ async function buildConnectivityData(config) {
|
|
|
3156
3506
|
url: "https://api.osv.dev",
|
|
3157
3507
|
...osvResult.error ? { error: osvResult.error } : {}
|
|
3158
3508
|
},
|
|
3509
|
+
sonatype: {
|
|
3510
|
+
reachable: sonatypeResult.reachable,
|
|
3511
|
+
url: "https://ossindex.sonatype.org",
|
|
3512
|
+
...sonatypeResult.error ? { error: sonatypeResult.error } : {}
|
|
3513
|
+
},
|
|
3514
|
+
...iqConfigured && iqResult ? {
|
|
3515
|
+
sonatypeiq: {
|
|
3516
|
+
reachable: iqResult.reachable,
|
|
3517
|
+
authenticated: iqResult.authenticated,
|
|
3518
|
+
url: iqCfg.baseUrl,
|
|
3519
|
+
...iqResult.error ? { error: iqResult.error } : {}
|
|
3520
|
+
}
|
|
3521
|
+
} : {},
|
|
3159
3522
|
tier,
|
|
3160
3523
|
capabilities: {
|
|
3161
3524
|
scan: true,
|
|
3162
3525
|
diff: true,
|
|
3163
3526
|
outdated: artResult.reachable && artResult.authenticated,
|
|
3164
3527
|
licenseCheck: artResult.reachable && artResult.authenticated,
|
|
3165
|
-
cveCheck: osvResult.reachable
|
|
3528
|
+
cveCheck: osvResult.reachable || sonatypeResult.reachable || (iqResult?.reachable && iqResult?.authenticated) === true
|
|
3166
3529
|
}
|
|
3167
3530
|
};
|
|
3168
3531
|
}
|
|
3169
3532
|
|
|
3170
3533
|
// src/services/deps/frisk.ts
|
|
3171
|
-
|
|
3534
|
+
function toErrorMessage(err) {
|
|
3535
|
+
return err instanceof Error ? err.message : String(err);
|
|
3536
|
+
}
|
|
3537
|
+
async function runFrisk(config, opts, source = "osv", applicationId) {
|
|
3172
3538
|
const repoRoot = getRepoRoot();
|
|
3173
3539
|
if (!repoRoot) {
|
|
3174
3540
|
throw new PncliError("Not inside a git repository.", 1);
|
|
3175
3541
|
}
|
|
3176
|
-
|
|
3177
|
-
|
|
3542
|
+
if (source === "sonatypeiq") {
|
|
3543
|
+
if (!config.sonatypeiq.baseUrl || !config.sonatypeiq.userCode || !config.sonatypeiq.passcode) {
|
|
3544
|
+
throw new PncliError(
|
|
3545
|
+
"deps frisk --source sonatypeiq requires Sonatype IQ Server to be configured. Run: pncli config init",
|
|
3546
|
+
1
|
|
3547
|
+
);
|
|
3548
|
+
}
|
|
3549
|
+
if (!applicationId) {
|
|
3550
|
+
throw new PncliError(
|
|
3551
|
+
"deps frisk --source sonatypeiq requires --application-id <id>. Use 'pncli sonatypeiq applications list' to find application IDs.",
|
|
3552
|
+
1
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
const { tier: baseTier, osvReachable } = await detectTier(config);
|
|
3557
|
+
const sonatypeReachable = source === "sonatype" || source === "all" ? await checkSonatypeReachable() : false;
|
|
3558
|
+
const tier = sonatypeReachable ? "full" : baseTier;
|
|
3559
|
+
if (source === "osv" && !osvReachable) {
|
|
3178
3560
|
throw new PncliError(
|
|
3179
3561
|
"deps frisk requires OSV.dev access but api.osv.dev is not reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
|
|
3180
3562
|
503
|
|
3181
3563
|
);
|
|
3182
3564
|
}
|
|
3565
|
+
if (source === "sonatype" && !sonatypeReachable) {
|
|
3566
|
+
throw new PncliError(
|
|
3567
|
+
"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.",
|
|
3568
|
+
503
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
if (source === "all" && !osvReachable && !sonatypeReachable) {
|
|
3572
|
+
throw new PncliError(
|
|
3573
|
+
"deps frisk requires either OSV.dev or Sonatype OSS Index access, but neither is reachable from this machine. Run 'pncli deps connectivity' to diagnose.",
|
|
3574
|
+
503
|
|
3575
|
+
);
|
|
3576
|
+
}
|
|
3577
|
+
const sourceErrors = [];
|
|
3578
|
+
let sourcesQueried;
|
|
3579
|
+
if (source === "all") {
|
|
3580
|
+
if (!osvReachable) sourceErrors.push({ source: "osv", error: "api.osv.dev is not reachable \u2014 run pncli deps connectivity to diagnose." });
|
|
3581
|
+
if (!sonatypeReachable) sourceErrors.push({ source: "sonatype", error: "ossindex.sonatype.org is not reachable \u2014 run pncli deps connectivity to diagnose." });
|
|
3582
|
+
sourcesQueried = [
|
|
3583
|
+
...osvReachable ? ["osv"] : [],
|
|
3584
|
+
...sonatypeReachable ? ["sonatype"] : []
|
|
3585
|
+
];
|
|
3586
|
+
} else {
|
|
3587
|
+
sourcesQueried = [source];
|
|
3588
|
+
}
|
|
3183
3589
|
const scanOpts = {
|
|
3184
3590
|
...opts,
|
|
3185
3591
|
includeTransitive: opts.includeTransitive ?? true
|
|
3186
3592
|
};
|
|
3187
3593
|
const scan = scanRepo(repoRoot, scanOpts);
|
|
3188
3594
|
if (scan.packages.length === 0) {
|
|
3189
|
-
return { tier, scanned: 0, vulnerable: 0, packages: [] };
|
|
3595
|
+
return { tier, source, sourcesQueried, sourceErrors, scanned: 0, vulnerable: 0, packages: [] };
|
|
3596
|
+
}
|
|
3597
|
+
let vulnerable;
|
|
3598
|
+
if (source === "sonatypeiq") {
|
|
3599
|
+
vulnerable = await checkPackagesViaIqServer(scan.packages, applicationId, config);
|
|
3600
|
+
} else if (source === "sonatype") {
|
|
3601
|
+
vulnerable = await checkPackagesForVulnsViaSonatype(scan.packages);
|
|
3602
|
+
} else if (source === "all") {
|
|
3603
|
+
const [osvSettled, sonatypeSettled] = await Promise.allSettled([
|
|
3604
|
+
osvReachable ? checkPackagesForVulns(scan.packages) : Promise.resolve([]),
|
|
3605
|
+
sonatypeReachable ? checkPackagesForVulnsViaSonatype(scan.packages) : Promise.resolve([])
|
|
3606
|
+
]);
|
|
3607
|
+
if (osvReachable) {
|
|
3608
|
+
if (osvSettled.status === "rejected") {
|
|
3609
|
+
sourceErrors.push({ source: "osv", error: toErrorMessage(osvSettled.reason) });
|
|
3610
|
+
sourcesQueried = sourcesQueried.filter((s) => s !== "osv");
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
if (sonatypeReachable) {
|
|
3614
|
+
if (sonatypeSettled.status === "rejected") {
|
|
3615
|
+
sourceErrors.push({ source: "sonatype", error: toErrorMessage(sonatypeSettled.reason) });
|
|
3616
|
+
sourcesQueried = sourcesQueried.filter((s) => s !== "sonatype");
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
if (sourcesQueried.length === 0) {
|
|
3620
|
+
throw new PncliError(
|
|
3621
|
+
"All vulnerability sources failed during scanning. Run 'pncli deps connectivity' to diagnose.",
|
|
3622
|
+
503
|
|
3623
|
+
);
|
|
3624
|
+
}
|
|
3625
|
+
vulnerable = mergeVulnerablePackages(
|
|
3626
|
+
osvSettled.status === "fulfilled" ? osvSettled.value : [],
|
|
3627
|
+
sonatypeSettled.status === "fulfilled" ? sonatypeSettled.value : []
|
|
3628
|
+
);
|
|
3629
|
+
} else {
|
|
3630
|
+
vulnerable = await checkPackagesForVulns(scan.packages);
|
|
3190
3631
|
}
|
|
3191
|
-
const vulnerable = await checkPackagesForVulns(scan.packages);
|
|
3192
3632
|
return {
|
|
3193
3633
|
tier,
|
|
3634
|
+
source,
|
|
3635
|
+
sourcesQueried,
|
|
3636
|
+
sourceErrors,
|
|
3194
3637
|
scanned: scan.packages.length,
|
|
3195
3638
|
vulnerable: vulnerable.length,
|
|
3196
3639
|
packages: vulnerable
|
|
3197
3640
|
};
|
|
3198
3641
|
}
|
|
3642
|
+
function mergeVulnerablePackages(osvResults, sonatypeResults) {
|
|
3643
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
3644
|
+
for (const pkg2 of osvResults) {
|
|
3645
|
+
const key = `${pkg2.ecosystem}:${pkg2.name}@${pkg2.version}`;
|
|
3646
|
+
byKey.set(key, { ...pkg2 });
|
|
3647
|
+
}
|
|
3648
|
+
for (const pkg2 of sonatypeResults) {
|
|
3649
|
+
const key = `${pkg2.ecosystem}:${pkg2.name}@${pkg2.version}`;
|
|
3650
|
+
const existing = byKey.get(key);
|
|
3651
|
+
if (existing) {
|
|
3652
|
+
const existingIds = new Set(existing.vulnerabilities.map((v) => v.id));
|
|
3653
|
+
const newVulns = pkg2.vulnerabilities.filter((v) => !existingIds.has(v.id));
|
|
3654
|
+
existing.vulnerabilities = [...existing.vulnerabilities, ...newVulns];
|
|
3655
|
+
} else {
|
|
3656
|
+
byKey.set(key, { ...pkg2 });
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
return [...byKey.values()];
|
|
3660
|
+
}
|
|
3199
3661
|
|
|
3200
3662
|
// src/services/deps/outdated.ts
|
|
3201
3663
|
async function runOutdated(config, opts, filterType) {
|
|
@@ -3270,7 +3732,7 @@ async function runLicenseCheck(config, opts) {
|
|
|
3270
3732
|
// src/services/deps/commands.ts
|
|
3271
3733
|
function registerDepsCommands(program2) {
|
|
3272
3734
|
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) => {
|
|
3735
|
+
deps.command("frisk").description("Scan all dependencies for CVEs and output remediation paths (requires OSV.dev, Sonatype OSS Index, or Sonatype IQ Server)").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", "sonatypeiq", "all"]).default("osv")).option("--application-id <id>", "Sonatype IQ Server application ID (required when --source sonatypeiq)").action(async (opts, cmd) => {
|
|
3274
3736
|
const startTime = Date.now();
|
|
3275
3737
|
try {
|
|
3276
3738
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -3280,7 +3742,7 @@ function registerDepsCommands(program2) {
|
|
|
3280
3742
|
includeTransitive: !opts.directOnly,
|
|
3281
3743
|
includeDev: opts.includeDev
|
|
3282
3744
|
};
|
|
3283
|
-
const data = await runFrisk(config, scanOpts);
|
|
3745
|
+
const data = await runFrisk(config, scanOpts, opts.source, opts.applicationId);
|
|
3284
3746
|
success(data, "deps", "frisk", startTime);
|
|
3285
3747
|
} catch (err) {
|
|
3286
3748
|
fail(err, "deps", "frisk", startTime);
|
|
@@ -3346,7 +3808,7 @@ function registerDepsCommands(program2) {
|
|
|
3346
3808
|
fail(err, "deps", "license-check", startTime);
|
|
3347
3809
|
}
|
|
3348
3810
|
});
|
|
3349
|
-
deps.command("connectivity").description("Test network access to Artifactory
|
|
3811
|
+
deps.command("connectivity").description("Test network access to Artifactory, OSV.dev, and Sonatype OSS Index, report available tier").action(async (_opts, cmd) => {
|
|
3350
3812
|
const startTime = Date.now();
|
|
3351
3813
|
try {
|
|
3352
3814
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -3652,6 +4114,37 @@ function registerConfigCommands(program2) {
|
|
|
3652
4114
|
} else {
|
|
3653
4115
|
results.checkmarx = { ok: null, message: "not configured" };
|
|
3654
4116
|
}
|
|
4117
|
+
const snConfigured = cfg.servicenow.baseUrl && cfg.servicenow.username && (cfg.servicenow.password || cfg.servicenow.apiToken);
|
|
4118
|
+
if (snConfigured) {
|
|
4119
|
+
try {
|
|
4120
|
+
await http.servicenow("/api/now/table/change_request", { params: { sysparm_limit: 1 } });
|
|
4121
|
+
results.servicenow = { ok: true, message: "connected" };
|
|
4122
|
+
} catch (err) {
|
|
4123
|
+
results.servicenow = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
4124
|
+
}
|
|
4125
|
+
} else {
|
|
4126
|
+
results.servicenow = { ok: null, message: "not configured" };
|
|
4127
|
+
}
|
|
4128
|
+
if (cfg.contrast.apiKey && cfg.contrast.serviceKey && cfg.contrast.username && cfg.contrast.orgUuid) {
|
|
4129
|
+
try {
|
|
4130
|
+
await http.contrast(`/Contrast/api/ng/${cfg.contrast.orgUuid}/applications`, { params: { limit: 1 } });
|
|
4131
|
+
results.contrast = { ok: true, message: "connected" };
|
|
4132
|
+
} catch (err) {
|
|
4133
|
+
results.contrast = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
4134
|
+
}
|
|
4135
|
+
} else {
|
|
4136
|
+
results.contrast = { ok: null, message: "not configured" };
|
|
4137
|
+
}
|
|
4138
|
+
if (cfg.sonatypeiq.baseUrl && cfg.sonatypeiq.userCode && cfg.sonatypeiq.passcode) {
|
|
4139
|
+
try {
|
|
4140
|
+
await http.sonatypeiq("/api/v2/applications", { params: { limit: 1 } });
|
|
4141
|
+
results.sonatypeiq = { ok: true, message: "connected" };
|
|
4142
|
+
} catch (err) {
|
|
4143
|
+
results.sonatypeiq = { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
4144
|
+
}
|
|
4145
|
+
} else {
|
|
4146
|
+
results.sonatypeiq = { ok: null, message: "not configured" };
|
|
4147
|
+
}
|
|
3655
4148
|
success(results, "config", "test", start);
|
|
3656
4149
|
} catch (err) {
|
|
3657
4150
|
fail(err, "config", "test", start);
|
|
@@ -3798,11 +4291,48 @@ function registerConfigCommands(program2) {
|
|
|
3798
4291
|
results.checkmarx = categorize2(err);
|
|
3799
4292
|
}
|
|
3800
4293
|
}
|
|
4294
|
+
const snCreds = cfg.servicenow.username && (cfg.servicenow.password || cfg.servicenow.apiToken);
|
|
4295
|
+
if (!snCreds) {
|
|
4296
|
+
results.servicenow = { status: "blank", message: "not configured" };
|
|
4297
|
+
} else if (!cfg.servicenow.baseUrl) {
|
|
4298
|
+
results.servicenow = { status: "error", message: "baseUrl not configured" };
|
|
4299
|
+
} else {
|
|
4300
|
+
try {
|
|
4301
|
+
await http.servicenow("/api/now/table/change_request", { params: { sysparm_limit: 1 }, timeoutMs: 1e4 });
|
|
4302
|
+
results.servicenow = { status: "valid", message: "ok" };
|
|
4303
|
+
} catch (err) {
|
|
4304
|
+
results.servicenow = categorize2(err);
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
if (!cfg.contrast.apiKey || !cfg.contrast.serviceKey || !cfg.contrast.username) {
|
|
4308
|
+
results.contrast = { status: "blank", message: "not configured" };
|
|
4309
|
+
} else if (!cfg.contrast.orgUuid) {
|
|
4310
|
+
results.contrast = { status: "error", message: "orgUuid not configured" };
|
|
4311
|
+
} else {
|
|
4312
|
+
try {
|
|
4313
|
+
await http.contrast(`/Contrast/api/ng/${cfg.contrast.orgUuid}/applications`, { params: { limit: 1 }, timeoutMs: 1e4 });
|
|
4314
|
+
results.contrast = { status: "valid", message: "ok" };
|
|
4315
|
+
} catch (err) {
|
|
4316
|
+
results.contrast = categorize2(err);
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
if (!cfg.sonatypeiq.userCode || !cfg.sonatypeiq.passcode) {
|
|
4320
|
+
results.sonatypeiq = { status: "blank", message: "not configured" };
|
|
4321
|
+
} else if (!cfg.sonatypeiq.baseUrl) {
|
|
4322
|
+
results.sonatypeiq = { status: "error", message: "baseUrl not configured" };
|
|
4323
|
+
} else {
|
|
4324
|
+
try {
|
|
4325
|
+
await http.sonatypeiq("/api/v2/applications", { params: { limit: 1 }, timeoutMs: 1e4 });
|
|
4326
|
+
results.sonatypeiq = { status: "valid", message: "ok" };
|
|
4327
|
+
} catch (err) {
|
|
4328
|
+
results.sonatypeiq = categorize2(err);
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
3801
4331
|
const statuses = Object.values(results).map((r) => r.status);
|
|
3802
4332
|
if (statuses.includes("invalid")) process.exitCode = ExitCode.AUTH_ERROR;
|
|
3803
4333
|
else if (statuses.includes("error")) process.exitCode = ExitCode.NETWORK_ERROR;
|
|
3804
4334
|
if (cmdOpts.output === "table") {
|
|
3805
|
-
const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx"];
|
|
4335
|
+
const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq"];
|
|
3806
4336
|
const labelWidth = 14;
|
|
3807
4337
|
const statusWidth = 9;
|
|
3808
4338
|
for (const svc of services) {
|
|
@@ -3816,7 +4346,7 @@ function registerConfigCommands(program2) {
|
|
|
3816
4346
|
process.stdout.write("\n");
|
|
3817
4347
|
} else {
|
|
3818
4348
|
if (opts.pretty) {
|
|
3819
|
-
const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx"];
|
|
4349
|
+
const services = ["jira", "bitbucket", "confluence", "sonar", "sde", "ado", "jenkins", "udeploy", "artifactory", "checkmarx", "servicenow", "contrast", "sonatypeiq"];
|
|
3820
4350
|
const labelWidth = 14;
|
|
3821
4351
|
const statusWidth = 9;
|
|
3822
4352
|
for (const svc of services) {
|
|
@@ -4149,6 +4679,155 @@ async function initGlobalConfig(start) {
|
|
|
4149
4679
|
}
|
|
4150
4680
|
}
|
|
4151
4681
|
}
|
|
4682
|
+
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");
|
|
4683
|
+
const useServiceNow = await confirm({
|
|
4684
|
+
message: "Configure ServiceNow for change management?",
|
|
4685
|
+
default: false
|
|
4686
|
+
});
|
|
4687
|
+
let servicenowBaseUrl = "";
|
|
4688
|
+
let servicenowUsername = "";
|
|
4689
|
+
let servicenowPassword = "";
|
|
4690
|
+
let servicenowApiToken = "";
|
|
4691
|
+
if (useServiceNow) {
|
|
4692
|
+
servicenowBaseUrl = await input({
|
|
4693
|
+
message: "ServiceNow instance URL (e.g. https://mycompany.service-now.com):",
|
|
4694
|
+
default: ""
|
|
4695
|
+
});
|
|
4696
|
+
servicenowUsername = await input({
|
|
4697
|
+
message: "ServiceNow username:",
|
|
4698
|
+
default: ""
|
|
4699
|
+
});
|
|
4700
|
+
const snUseToken = await confirm({
|
|
4701
|
+
message: "Authenticate with an API token instead of a password?",
|
|
4702
|
+
default: false
|
|
4703
|
+
});
|
|
4704
|
+
if (snUseToken) {
|
|
4705
|
+
servicenowApiToken = await password({
|
|
4706
|
+
message: "ServiceNow API token:",
|
|
4707
|
+
validate: (v) => v.length > 0 || "Token cannot be blank"
|
|
4708
|
+
});
|
|
4709
|
+
} else {
|
|
4710
|
+
servicenowPassword = await password({
|
|
4711
|
+
message: "ServiceNow password:",
|
|
4712
|
+
validate: (v) => v.length > 0 || "Password cannot be blank"
|
|
4713
|
+
});
|
|
4714
|
+
}
|
|
4715
|
+
const snHasCreds = servicenowBaseUrl && servicenowUsername && (servicenowPassword || servicenowApiToken);
|
|
4716
|
+
if (snHasCreds) {
|
|
4717
|
+
process.stderr.write("\n Verifying connection...\n");
|
|
4718
|
+
try {
|
|
4719
|
+
const tempConfig = {
|
|
4720
|
+
...loadConfig(),
|
|
4721
|
+
servicenow: {
|
|
4722
|
+
baseUrl: servicenowBaseUrl,
|
|
4723
|
+
username: servicenowUsername,
|
|
4724
|
+
password: servicenowPassword || void 0,
|
|
4725
|
+
apiToken: servicenowApiToken || void 0
|
|
4726
|
+
}
|
|
4727
|
+
};
|
|
4728
|
+
const tempHttp = createHttpClient(tempConfig);
|
|
4729
|
+
await tempHttp.servicenow("/api/now/table/change_request", { params: { sysparm_limit: 1 } });
|
|
4730
|
+
process.stderr.write(" Connected.\n");
|
|
4731
|
+
} catch (err) {
|
|
4732
|
+
warn(`Could not connect to ServiceNow: ${err instanceof Error ? err.message : String(err)}`);
|
|
4733
|
+
warn("Config will be saved anyway. Check your URL and credentials and re-run pncli config init or pncli config test.");
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
process.stderr.write("\n\u2500\u2500 Sonatype IQ Server \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");
|
|
4738
|
+
const useSonatypeIq = await confirm({
|
|
4739
|
+
message: "Configure Sonatype IQ Server for dependency vulnerability scanning?",
|
|
4740
|
+
default: false
|
|
4741
|
+
});
|
|
4742
|
+
let sonatypeIqBaseUrl = "";
|
|
4743
|
+
let sonatypeIqUserCode = "";
|
|
4744
|
+
let sonatypeIqPasscode = "";
|
|
4745
|
+
if (useSonatypeIq) {
|
|
4746
|
+
process.stderr.write(" User Token credentials are found in your IQ Server profile under User Token.\n");
|
|
4747
|
+
sonatypeIqBaseUrl = await input({
|
|
4748
|
+
message: "Sonatype IQ Server base URL (e.g. https://iq.your-company.com):",
|
|
4749
|
+
default: ""
|
|
4750
|
+
});
|
|
4751
|
+
sonatypeIqUserCode = await input({
|
|
4752
|
+
message: "User Code (from User Token):",
|
|
4753
|
+
default: ""
|
|
4754
|
+
});
|
|
4755
|
+
sonatypeIqPasscode = await password({
|
|
4756
|
+
message: "Passcode (from User Token):"
|
|
4757
|
+
});
|
|
4758
|
+
if (sonatypeIqBaseUrl && sonatypeIqUserCode && sonatypeIqPasscode) {
|
|
4759
|
+
process.stderr.write("\n Verifying connection...\n");
|
|
4760
|
+
try {
|
|
4761
|
+
const tempConfig = {
|
|
4762
|
+
...loadConfig(),
|
|
4763
|
+
sonatypeiq: {
|
|
4764
|
+
baseUrl: sonatypeIqBaseUrl,
|
|
4765
|
+
userCode: sonatypeIqUserCode,
|
|
4766
|
+
passcode: sonatypeIqPasscode
|
|
4767
|
+
}
|
|
4768
|
+
};
|
|
4769
|
+
const tempHttp = createHttpClient(tempConfig);
|
|
4770
|
+
await tempHttp.sonatypeiq("/api/v2/applications", { params: { limit: 1 } });
|
|
4771
|
+
process.stderr.write(" Connected.\n");
|
|
4772
|
+
} catch (err) {
|
|
4773
|
+
warn(`Could not connect to Sonatype IQ Server: ${err instanceof Error ? err.message : String(err)}`);
|
|
4774
|
+
warn("Config will be saved anyway. Check your URL and credentials and re-run pncli config init or pncli config test.");
|
|
4775
|
+
}
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
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");
|
|
4779
|
+
const useContrast = await confirm({
|
|
4780
|
+
message: "Configure Contrast Security IAST for vulnerability findings?",
|
|
4781
|
+
default: false
|
|
4782
|
+
});
|
|
4783
|
+
let contrastBaseUrl = "";
|
|
4784
|
+
let contrastOrgUuid = "";
|
|
4785
|
+
let contrastUsername = "";
|
|
4786
|
+
let contrastApiKey = "";
|
|
4787
|
+
let contrastServiceKey = "";
|
|
4788
|
+
if (useContrast) {
|
|
4789
|
+
process.stderr.write(" Find your credentials in Contrast under User Settings \u2192 Your Keys.\n");
|
|
4790
|
+
contrastBaseUrl = await input({
|
|
4791
|
+
message: "Contrast base URL (leave blank for cloud: https://app.contrastsecurity.com):",
|
|
4792
|
+
default: ""
|
|
4793
|
+
});
|
|
4794
|
+
contrastOrgUuid = await input({
|
|
4795
|
+
message: "Organization UUID:",
|
|
4796
|
+
default: ""
|
|
4797
|
+
});
|
|
4798
|
+
contrastUsername = await input({
|
|
4799
|
+
message: "Username (email):",
|
|
4800
|
+
default: ""
|
|
4801
|
+
});
|
|
4802
|
+
contrastApiKey = await password({
|
|
4803
|
+
message: "API key:"
|
|
4804
|
+
});
|
|
4805
|
+
contrastServiceKey = await password({
|
|
4806
|
+
message: "Service key:"
|
|
4807
|
+
});
|
|
4808
|
+
const contrastHasCreds = contrastOrgUuid && contrastUsername && contrastApiKey && contrastServiceKey;
|
|
4809
|
+
if (contrastHasCreds) {
|
|
4810
|
+
process.stderr.write("\n Verifying connection...\n");
|
|
4811
|
+
try {
|
|
4812
|
+
const tempConfig = {
|
|
4813
|
+
...loadConfig(),
|
|
4814
|
+
contrast: {
|
|
4815
|
+
baseUrl: contrastBaseUrl || void 0,
|
|
4816
|
+
orgUuid: contrastOrgUuid,
|
|
4817
|
+
username: contrastUsername,
|
|
4818
|
+
apiKey: contrastApiKey,
|
|
4819
|
+
serviceKey: contrastServiceKey
|
|
4820
|
+
}
|
|
4821
|
+
};
|
|
4822
|
+
const tempHttp = createHttpClient(tempConfig);
|
|
4823
|
+
await tempHttp.contrast(`/Contrast/api/ng/${contrastOrgUuid}/applications`, { params: { limit: 1 } });
|
|
4824
|
+
process.stderr.write(" Connected.\n");
|
|
4825
|
+
} catch (err) {
|
|
4826
|
+
warn(`Could not connect to Contrast: ${err instanceof Error ? err.message : String(err)}`);
|
|
4827
|
+
warn("Config will be saved anyway. Check your credentials and re-run pncli config init or pncli config test.");
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4152
4831
|
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
4832
|
const jiraProject = await input({
|
|
4154
4833
|
message: "Default Jira project key (optional):",
|
|
@@ -4242,6 +4921,30 @@ async function initGlobalConfig(start) {
|
|
|
4242
4921
|
password: checkmarxPassword || void 0
|
|
4243
4922
|
}
|
|
4244
4923
|
} : {},
|
|
4924
|
+
...useServiceNow && servicenowBaseUrl ? {
|
|
4925
|
+
servicenow: {
|
|
4926
|
+
baseUrl: servicenowBaseUrl,
|
|
4927
|
+
username: servicenowUsername || void 0,
|
|
4928
|
+
...servicenowApiToken ? { apiToken: servicenowApiToken } : {},
|
|
4929
|
+
...servicenowPassword ? { password: servicenowPassword } : {}
|
|
4930
|
+
}
|
|
4931
|
+
} : {},
|
|
4932
|
+
...useContrast && contrastOrgUuid ? {
|
|
4933
|
+
contrast: {
|
|
4934
|
+
...contrastBaseUrl ? { baseUrl: contrastBaseUrl } : {},
|
|
4935
|
+
orgUuid: contrastOrgUuid,
|
|
4936
|
+
username: contrastUsername || void 0,
|
|
4937
|
+
apiKey: contrastApiKey || void 0,
|
|
4938
|
+
serviceKey: contrastServiceKey || void 0
|
|
4939
|
+
}
|
|
4940
|
+
} : {},
|
|
4941
|
+
...useSonatypeIq && sonatypeIqBaseUrl ? {
|
|
4942
|
+
sonatypeiq: {
|
|
4943
|
+
baseUrl: sonatypeIqBaseUrl,
|
|
4944
|
+
userCode: sonatypeIqUserCode || void 0,
|
|
4945
|
+
passcode: sonatypeIqPasscode || void 0
|
|
4946
|
+
}
|
|
4947
|
+
} : {},
|
|
4245
4948
|
defaults: {
|
|
4246
4949
|
jira: {
|
|
4247
4950
|
project: jiraProject || void 0
|
|
@@ -4697,7 +5400,7 @@ function registerAdoWorkCommands(ado) {
|
|
|
4697
5400
|
fail(err, "ado", "work-get", start);
|
|
4698
5401
|
}
|
|
4699
5402
|
});
|
|
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) => {
|
|
5403
|
+
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
5404
|
acc.push(v);
|
|
4702
5405
|
return acc;
|
|
4703
5406
|
}, []).action(async (opts) => {
|
|
@@ -4719,6 +5422,17 @@ function registerAdoWorkCommands(ado) {
|
|
|
4719
5422
|
];
|
|
4720
5423
|
const data = await workClient.createWorkItem(collection, project, opts.type, patch);
|
|
4721
5424
|
success(data, "ado", "work-create", start);
|
|
5425
|
+
if (opts.parent) {
|
|
5426
|
+
try {
|
|
5427
|
+
const baseUrl = config.ado.baseUrl?.replace(/\/$/, "");
|
|
5428
|
+
const targetUrl = `${baseUrl}/${encodeURIComponent(collection)}/_apis/wit/workitems/${opts.parent}`;
|
|
5429
|
+
const linkPatch = [{ op: "add", path: "/relations/-", value: { rel: "System.LinkTypes.Hierarchy-Reverse", url: targetUrl } }];
|
|
5430
|
+
await workClient.updateWorkItem(collection, data.id, linkPatch);
|
|
5431
|
+
} catch (linkErr) {
|
|
5432
|
+
warn(`Created #${data.id} but failed to link to parent ${opts.parent}: ${linkErr instanceof Error ? linkErr.message : linkErr}`);
|
|
5433
|
+
}
|
|
5434
|
+
}
|
|
5435
|
+
return;
|
|
4722
5436
|
} catch (err) {
|
|
4723
5437
|
fail(err, "ado", "work-create", start);
|
|
4724
5438
|
}
|
|
@@ -5135,11 +5849,11 @@ function registerAdoRepoCommands(ado) {
|
|
|
5135
5849
|
const start = Date.now();
|
|
5136
5850
|
try {
|
|
5137
5851
|
const globalOpts = ado.optsWithGlobals();
|
|
5138
|
-
const { loadConfig: lc } = await import("./config-
|
|
5852
|
+
const { loadConfig: lc } = await import("./config-NCI2BTQG.js");
|
|
5139
5853
|
const config = lc({ configPath: globalOpts.config });
|
|
5140
5854
|
const { collection, project, repo: repo2, gitClient } = getAdoContext(ado, true);
|
|
5141
5855
|
const { AdoCoreClient: AdoCoreClient2 } = await import("./core-SWYVXOQ4.js");
|
|
5142
|
-
const { createHttpClient: createHttpClient2 } = await import("./http-
|
|
5856
|
+
const { createHttpClient: createHttpClient2 } = await import("./http-L2P23H2D.js");
|
|
5143
5857
|
const http = createHttpClient2(config, Boolean(globalOpts.dryRun));
|
|
5144
5858
|
const coreClient = new AdoCoreClient2(http);
|
|
5145
5859
|
const me = await coreClient.getConnectionData(collection);
|
|
@@ -5751,7 +6465,7 @@ var UdeployClient = class {
|
|
|
5751
6465
|
};
|
|
5752
6466
|
|
|
5753
6467
|
// src/services/udeploy/commands.ts
|
|
5754
|
-
var
|
|
6468
|
+
var POLL_INTERVAL_MS2 = 5e3;
|
|
5755
6469
|
var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
5756
6470
|
function getClient8(program2) {
|
|
5757
6471
|
const opts = program2.optsWithGlobals();
|
|
@@ -5862,7 +6576,7 @@ function registerUdeployCommands(program2) {
|
|
|
5862
6576
|
if (elapsed >= timeoutMs) {
|
|
5863
6577
|
throw new PncliError(`Timed out after ${timeoutMs}ms waiting for request ${requestId}`, 1);
|
|
5864
6578
|
}
|
|
5865
|
-
await sleep(
|
|
6579
|
+
await sleep(POLL_INTERVAL_MS2);
|
|
5866
6580
|
const status = await client.getRequestStatus(requestId);
|
|
5867
6581
|
if (status.status === "CLOSED") {
|
|
5868
6582
|
const outcome = { requestId, status: status.status, result: status.result, elapsed_ms: Date.now() - start };
|
|
@@ -6007,19 +6721,261 @@ function registerCheckmarxCommands(program2) {
|
|
|
6007
6721
|
});
|
|
6008
6722
|
}
|
|
6009
6723
|
|
|
6724
|
+
// src/services/servicenow/commands.ts
|
|
6725
|
+
function getHttp(program2) {
|
|
6726
|
+
const opts = program2.optsWithGlobals();
|
|
6727
|
+
const config = loadConfig({ configPath: opts.config });
|
|
6728
|
+
if (!config.servicenow.baseUrl) throw new PncliError("ServiceNow not configured. Run: pncli config init");
|
|
6729
|
+
return createHttpClient(config, Boolean(opts.dryRun));
|
|
6730
|
+
}
|
|
6731
|
+
function registerServiceNowCommands(program2) {
|
|
6732
|
+
const sn = program2.command("servicenow").description("ServiceNow operations");
|
|
6733
|
+
const change = sn.command("change").description("Change request operations");
|
|
6734
|
+
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) => {
|
|
6735
|
+
const start = Date.now();
|
|
6736
|
+
try {
|
|
6737
|
+
const http = getHttp(program2);
|
|
6738
|
+
const params = {
|
|
6739
|
+
sysparm_limit: opts.limit ? parseInt(opts.limit, 10) : 25,
|
|
6740
|
+
sysparm_display_value: "true"
|
|
6741
|
+
};
|
|
6742
|
+
const sanitize = (v) => v.replace(/\^/g, "");
|
|
6743
|
+
const queries = [];
|
|
6744
|
+
if (opts.state !== void 0) queries.push(`state=${sanitize(opts.state)}`);
|
|
6745
|
+
if (opts.assignedTo) queries.push(`assigned_to=${sanitize(opts.assignedTo)}`);
|
|
6746
|
+
if (queries.length) params["sysparm_query"] = queries.join("^");
|
|
6747
|
+
if (opts.fields) params["sysparm_fields"] = opts.fields;
|
|
6748
|
+
const data = await http.servicenow("/api/now/table/change_request", { params });
|
|
6749
|
+
success(data.result, "servicenow", "change list", start);
|
|
6750
|
+
} catch (err) {
|
|
6751
|
+
fail(err, "servicenow", "change list", start);
|
|
6752
|
+
}
|
|
6753
|
+
});
|
|
6754
|
+
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) => {
|
|
6755
|
+
const start = Date.now();
|
|
6756
|
+
try {
|
|
6757
|
+
const http = getHttp(program2);
|
|
6758
|
+
if (opts.id.toUpperCase().startsWith("CHG")) {
|
|
6759
|
+
const list = await http.servicenow("/api/now/table/change_request", {
|
|
6760
|
+
params: { sysparm_query: `number=${opts.id}`, sysparm_display_value: "true", sysparm_limit: 1 }
|
|
6761
|
+
});
|
|
6762
|
+
if (!list.result.length) throw new PncliError(`Change request not found: ${opts.id}`, 1);
|
|
6763
|
+
success(list.result[0], "servicenow", "change get", start);
|
|
6764
|
+
return;
|
|
6765
|
+
}
|
|
6766
|
+
const data = await http.servicenow(`/api/now/table/change_request/${opts.id}`, {
|
|
6767
|
+
params: { sysparm_display_value: "true" }
|
|
6768
|
+
});
|
|
6769
|
+
success(data.result, "servicenow", "change get", start);
|
|
6770
|
+
} catch (err) {
|
|
6771
|
+
fail(err, "servicenow", "change get", start);
|
|
6772
|
+
}
|
|
6773
|
+
});
|
|
6774
|
+
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) => {
|
|
6775
|
+
const start = Date.now();
|
|
6776
|
+
try {
|
|
6777
|
+
const http = getHttp(program2);
|
|
6778
|
+
const body = {
|
|
6779
|
+
short_description: opts.shortDescription,
|
|
6780
|
+
type: opts.type ?? "normal"
|
|
6781
|
+
};
|
|
6782
|
+
if (opts.description) body["description"] = opts.description;
|
|
6783
|
+
if (opts.priority) body["priority"] = opts.priority;
|
|
6784
|
+
if (opts.assignedTo) body["assigned_to"] = opts.assignedTo;
|
|
6785
|
+
if (opts.assignmentGroup) body["assignment_group"] = opts.assignmentGroup;
|
|
6786
|
+
if (opts.startDate) body["start_date"] = opts.startDate;
|
|
6787
|
+
if (opts.endDate) body["end_date"] = opts.endDate;
|
|
6788
|
+
const data = await http.servicenow("/api/now/table/change_request", {
|
|
6789
|
+
method: "POST",
|
|
6790
|
+
body
|
|
6791
|
+
});
|
|
6792
|
+
success(data.result, "servicenow", "change create", start);
|
|
6793
|
+
} catch (err) {
|
|
6794
|
+
fail(err, "servicenow", "change create", start);
|
|
6795
|
+
}
|
|
6796
|
+
});
|
|
6797
|
+
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) => {
|
|
6798
|
+
const start = Date.now();
|
|
6799
|
+
try {
|
|
6800
|
+
const http = getHttp(program2);
|
|
6801
|
+
const body = {};
|
|
6802
|
+
if (opts.shortDescription) body["short_description"] = opts.shortDescription;
|
|
6803
|
+
if (opts.description) body["description"] = opts.description;
|
|
6804
|
+
if (opts.state !== void 0) body["state"] = opts.state;
|
|
6805
|
+
if (opts.priority) body["priority"] = opts.priority;
|
|
6806
|
+
if (opts.assignedTo) body["assigned_to"] = opts.assignedTo;
|
|
6807
|
+
if (opts.startDate) body["start_date"] = opts.startDate;
|
|
6808
|
+
if (opts.endDate) body["end_date"] = opts.endDate;
|
|
6809
|
+
if (Object.keys(body).length === 0) throw new PncliError("No fields to update. Provide at least one option besides --id.", 1);
|
|
6810
|
+
const data = await http.servicenow(`/api/now/table/change_request/${opts.id}`, {
|
|
6811
|
+
method: "PATCH",
|
|
6812
|
+
body
|
|
6813
|
+
});
|
|
6814
|
+
success(data.result, "servicenow", "change update", start);
|
|
6815
|
+
} catch (err) {
|
|
6816
|
+
fail(err, "servicenow", "change update", start);
|
|
6817
|
+
}
|
|
6818
|
+
});
|
|
6819
|
+
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) => {
|
|
6820
|
+
const start = Date.now();
|
|
6821
|
+
try {
|
|
6822
|
+
const http = getHttp(program2);
|
|
6823
|
+
const body = { state: 3 };
|
|
6824
|
+
if (opts.closeNotes) body["close_notes"] = opts.closeNotes;
|
|
6825
|
+
if (opts.closeCode) body["close_code"] = opts.closeCode;
|
|
6826
|
+
const data = await http.servicenow(`/api/now/table/change_request/${opts.id}`, {
|
|
6827
|
+
method: "PATCH",
|
|
6828
|
+
body
|
|
6829
|
+
});
|
|
6830
|
+
success(data.result, "servicenow", "change close", start);
|
|
6831
|
+
} catch (err) {
|
|
6832
|
+
fail(err, "servicenow", "change close", start);
|
|
6833
|
+
}
|
|
6834
|
+
});
|
|
6835
|
+
}
|
|
6836
|
+
|
|
6837
|
+
// src/services/contrast/commands.ts
|
|
6838
|
+
function getHttpAndOrg(program2) {
|
|
6839
|
+
const opts = program2.optsWithGlobals();
|
|
6840
|
+
const config = loadConfig({ configPath: opts.config });
|
|
6841
|
+
if (!config.contrast.apiKey || !config.contrast.serviceKey || !config.contrast.username) throw new PncliError("Contrast not configured. Run: pncli config init");
|
|
6842
|
+
if (!config.contrast.orgUuid) throw new PncliError("Contrast org UUID not configured. Run: pncli config set contrast.orgUuid <uuid>");
|
|
6843
|
+
const http = createHttpClient(config, Boolean(opts.dryRun));
|
|
6844
|
+
return { http, orgUuid: config.contrast.orgUuid };
|
|
6845
|
+
}
|
|
6846
|
+
function registerContrastCommands(program2) {
|
|
6847
|
+
const contrast = program2.command("contrast").description("Contrast IAST operations");
|
|
6848
|
+
const apps = contrast.command("apps").description("Application operations");
|
|
6849
|
+
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) => {
|
|
6850
|
+
const start = Date.now();
|
|
6851
|
+
try {
|
|
6852
|
+
const { http, orgUuid } = getHttpAndOrg(program2);
|
|
6853
|
+
const data = await http.contrast(`/Contrast/api/ng/${orgUuid}/applications`, {
|
|
6854
|
+
params: {
|
|
6855
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : 25,
|
|
6856
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : 0
|
|
6857
|
+
}
|
|
6858
|
+
});
|
|
6859
|
+
success(data, "contrast", "apps list", start);
|
|
6860
|
+
} catch (err) {
|
|
6861
|
+
fail(err, "contrast", "apps list", start);
|
|
6862
|
+
}
|
|
6863
|
+
});
|
|
6864
|
+
const findings = contrast.command("findings").description("Vulnerability findings operations");
|
|
6865
|
+
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) => {
|
|
6866
|
+
const start = Date.now();
|
|
6867
|
+
try {
|
|
6868
|
+
const { http, orgUuid } = getHttpAndOrg(program2);
|
|
6869
|
+
const params = {
|
|
6870
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : 25,
|
|
6871
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : 0
|
|
6872
|
+
};
|
|
6873
|
+
if (opts.severity) params["severities"] = opts.severity;
|
|
6874
|
+
if (opts.status) params["statuses"] = opts.status;
|
|
6875
|
+
const data = await http.contrast(
|
|
6876
|
+
`/Contrast/api/ng/${orgUuid}/traces/${opts.app}/filter`,
|
|
6877
|
+
{ params }
|
|
6878
|
+
);
|
|
6879
|
+
success(data, "contrast", "findings list", start);
|
|
6880
|
+
} catch (err) {
|
|
6881
|
+
fail(err, "contrast", "findings list", start);
|
|
6882
|
+
}
|
|
6883
|
+
});
|
|
6884
|
+
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) => {
|
|
6885
|
+
const start = Date.now();
|
|
6886
|
+
try {
|
|
6887
|
+
const { http, orgUuid } = getHttpAndOrg(program2);
|
|
6888
|
+
const data = await http.contrast(
|
|
6889
|
+
`/Contrast/api/ng/${orgUuid}/traces/${opts.app}/trace/${opts.trace}`
|
|
6890
|
+
);
|
|
6891
|
+
success(data, "contrast", "findings get", start);
|
|
6892
|
+
} catch (err) {
|
|
6893
|
+
fail(err, "contrast", "findings get", start);
|
|
6894
|
+
}
|
|
6895
|
+
});
|
|
6896
|
+
}
|
|
6897
|
+
|
|
6898
|
+
// src/services/sonatypeiq/commands.ts
|
|
6899
|
+
function getHttpClient(program2) {
|
|
6900
|
+
const opts = program2.optsWithGlobals();
|
|
6901
|
+
const config = loadConfig({ configPath: opts.config });
|
|
6902
|
+
if (!config.sonatypeiq.baseUrl) throw new PncliError("Sonatype IQ Server baseUrl not configured. Run: pncli config init");
|
|
6903
|
+
if (!config.sonatypeiq.userCode || !config.sonatypeiq.passcode) throw new PncliError("Sonatype IQ Server credentials not configured. Run: pncli config init");
|
|
6904
|
+
const http = createHttpClient(config, Boolean(opts.dryRun));
|
|
6905
|
+
return http;
|
|
6906
|
+
}
|
|
6907
|
+
function registerSonatypeIqCommands(program2) {
|
|
6908
|
+
const sonatypeiq = program2.command("sonatypeiq").description("Sonatype IQ Server operations");
|
|
6909
|
+
const applications = sonatypeiq.command("applications").description("Application operations");
|
|
6910
|
+
applications.command("list").description("List all applications in Sonatype IQ Server").option("--organization-id <id>", "Filter by organization ID").action(async (opts) => {
|
|
6911
|
+
const startTime = Date.now();
|
|
6912
|
+
try {
|
|
6913
|
+
const http = getHttpClient(program2);
|
|
6914
|
+
const params = {};
|
|
6915
|
+
if (opts.organizationId) params["organizationId"] = opts.organizationId;
|
|
6916
|
+
const data = await http.sonatypeiq("/api/v2/applications", { params });
|
|
6917
|
+
success(data, "sonatypeiq", "applications list", startTime);
|
|
6918
|
+
} catch (err) {
|
|
6919
|
+
fail(err, "sonatypeiq", "applications list", startTime);
|
|
6920
|
+
}
|
|
6921
|
+
});
|
|
6922
|
+
applications.command("get").description("Get an application by its public ID").requiredOption("--public-id <id>", "Application public ID").action(async (opts) => {
|
|
6923
|
+
const startTime = Date.now();
|
|
6924
|
+
try {
|
|
6925
|
+
const http = getHttpClient(program2);
|
|
6926
|
+
const data = await http.sonatypeiq("/api/v2/applications", {
|
|
6927
|
+
params: { publicId: opts.publicId }
|
|
6928
|
+
});
|
|
6929
|
+
success(data, "sonatypeiq", "applications get", startTime);
|
|
6930
|
+
} catch (err) {
|
|
6931
|
+
fail(err, "sonatypeiq", "applications get", startTime);
|
|
6932
|
+
}
|
|
6933
|
+
});
|
|
6934
|
+
const organizations = sonatypeiq.command("organizations").description("Organization operations");
|
|
6935
|
+
organizations.command("list").description("List all organizations in Sonatype IQ Server").action(async () => {
|
|
6936
|
+
const startTime = Date.now();
|
|
6937
|
+
try {
|
|
6938
|
+
const http = getHttpClient(program2);
|
|
6939
|
+
const data = await http.sonatypeiq("/api/v2/organizations");
|
|
6940
|
+
success(data, "sonatypeiq", "organizations list", startTime);
|
|
6941
|
+
} catch (err) {
|
|
6942
|
+
fail(err, "sonatypeiq", "organizations list", startTime);
|
|
6943
|
+
}
|
|
6944
|
+
});
|
|
6945
|
+
const policies = sonatypeiq.command("policies").description("Policy operations");
|
|
6946
|
+
policies.command("list").description("List all policies").option("--organization-id <id>", "Filter by organization ID").action(async (opts) => {
|
|
6947
|
+
const startTime = Date.now();
|
|
6948
|
+
try {
|
|
6949
|
+
const http = getHttpClient(program2);
|
|
6950
|
+
const path6 = opts.organizationId ? `/api/v2/policies/organization/${encodeURIComponent(opts.organizationId)}` : "/api/v2/policies";
|
|
6951
|
+
const data = await http.sonatypeiq(path6);
|
|
6952
|
+
success(data, "sonatypeiq", "policies list", startTime);
|
|
6953
|
+
} catch (err) {
|
|
6954
|
+
fail(err, "sonatypeiq", "policies list", startTime);
|
|
6955
|
+
}
|
|
6956
|
+
});
|
|
6957
|
+
}
|
|
6958
|
+
|
|
6010
6959
|
// src/services/skills/commands.ts
|
|
6011
6960
|
import fs3 from "fs";
|
|
6012
6961
|
import path5 from "path";
|
|
6013
6962
|
import os from "os";
|
|
6014
|
-
|
|
6015
|
-
var RAW_BASE = "https://raw.githubusercontent.com/kolatts/pncli/main/skills";
|
|
6963
|
+
import { fileURLToPath } from "url";
|
|
6016
6964
|
var AGENT_PATHS = {
|
|
6017
6965
|
"github-copilot": { project: ".agents/skills", user: path5.join(os.homedir(), ".copilot/skills") },
|
|
6018
6966
|
"claude-code": { project: ".claude/skills", user: path5.join(os.homedir(), ".claude/skills") }
|
|
6019
6967
|
};
|
|
6968
|
+
function getBundledSkillsDir() {
|
|
6969
|
+
try {
|
|
6970
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
6971
|
+
return path5.resolve(path5.dirname(thisFile), "..", "skills");
|
|
6972
|
+
} catch {
|
|
6973
|
+
return "";
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6020
6976
|
function registerSkillsCommands(program2) {
|
|
6021
6977
|
const skills = program2.command("skills").description("Manage pncli Claude Code skills");
|
|
6022
|
-
skills.command("install").description("
|
|
6978
|
+
skills.command("install").description("Install pncli skills into the current repo").option("--agent <agent>", "Target agent host: github-copilot | claude-code", "github-copilot").option("--scope <scope>", "Installation scope: project | user", "project").option("--target <dir>", "Override install directory (ignores --agent and --scope)").action((opts) => {
|
|
6023
6979
|
const start = Date.now();
|
|
6024
6980
|
try {
|
|
6025
6981
|
let targetDir;
|
|
@@ -6033,19 +6989,22 @@ function registerSkillsCommands(program2) {
|
|
|
6033
6989
|
const scopePath = opts.scope === "user" ? agentConfig.user : agentConfig.project;
|
|
6034
6990
|
targetDir = path5.resolve(scopePath);
|
|
6035
6991
|
}
|
|
6036
|
-
|
|
6037
|
-
const
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6992
|
+
const resolvedTarget = path5.resolve(targetDir);
|
|
6993
|
+
const bundledDir = getBundledSkillsDir();
|
|
6994
|
+
let skillDirs = [];
|
|
6995
|
+
if (bundledDir !== "") {
|
|
6996
|
+
try {
|
|
6997
|
+
skillDirs = fs3.readdirSync(bundledDir).filter(
|
|
6998
|
+
(name) => fs3.existsSync(path5.join(bundledDir, name, "SKILL.md"))
|
|
6999
|
+
);
|
|
7000
|
+
} catch {
|
|
7001
|
+
}
|
|
6042
7002
|
}
|
|
6043
|
-
const entries = await listRes.json();
|
|
6044
|
-
const skillDirs = entries.filter((e) => e.type === "dir").map((e) => e.name);
|
|
6045
7003
|
if (skillDirs.length === 0) {
|
|
6046
|
-
throw new Error("No skills found
|
|
7004
|
+
throw new Error("No bundled skills found. Reinstall pncli to get the latest version: npm install -g @kolatts/pncli");
|
|
6047
7005
|
}
|
|
6048
|
-
const
|
|
7006
|
+
const installed = [];
|
|
7007
|
+
const failed = [];
|
|
6049
7008
|
for (const skillName of skillDirs) {
|
|
6050
7009
|
const existingDir = path5.resolve(targetDir, skillName);
|
|
6051
7010
|
if (!existingDir.startsWith(resolvedTarget + path5.sep)) continue;
|
|
@@ -6053,25 +7012,15 @@ function registerSkillsCommands(program2) {
|
|
|
6053
7012
|
fs3.rmSync(existingDir, { recursive: true, force: true });
|
|
6054
7013
|
}
|
|
6055
7014
|
}
|
|
6056
|
-
warn(`
|
|
6057
|
-
const installed = [];
|
|
6058
|
-
const failed = [];
|
|
7015
|
+
warn(`Installing ${skillDirs.length} bundled skill(s) to ${targetDir}...`);
|
|
6059
7016
|
for (const skillName of skillDirs) {
|
|
6060
7017
|
const skillDir = path5.resolve(targetDir, skillName);
|
|
6061
7018
|
if (!skillDir.startsWith(resolvedTarget + path5.sep)) {
|
|
6062
7019
|
failed.push(skillName);
|
|
6063
7020
|
continue;
|
|
6064
7021
|
}
|
|
6065
|
-
const skillUrl = `${RAW_BASE}/${skillName}/SKILL.md`;
|
|
6066
7022
|
try {
|
|
6067
|
-
const
|
|
6068
|
-
headers: { "User-Agent": "pncli" }
|
|
6069
|
-
});
|
|
6070
|
-
if (!res.ok) {
|
|
6071
|
-
failed.push(skillName);
|
|
6072
|
-
continue;
|
|
6073
|
-
}
|
|
6074
|
-
const content = await res.text();
|
|
7023
|
+
const content = fs3.readFileSync(path5.join(bundledDir, skillName, "SKILL.md"), "utf8");
|
|
6075
7024
|
fs3.mkdirSync(skillDir, { recursive: true });
|
|
6076
7025
|
fs3.writeFileSync(path5.join(skillDir, "SKILL.md"), content, "utf8");
|
|
6077
7026
|
installed.push(skillName);
|
|
@@ -6081,7 +7030,7 @@ function registerSkillsCommands(program2) {
|
|
|
6081
7030
|
}
|
|
6082
7031
|
warn(`Installed ${installed.length} skill(s) to ${targetDir}`);
|
|
6083
7032
|
if (failed.length > 0) {
|
|
6084
|
-
warn(`Failed to
|
|
7033
|
+
warn(`Failed to install: ${failed.join(", ")}`);
|
|
6085
7034
|
}
|
|
6086
7035
|
success({
|
|
6087
7036
|
installed,
|
|
@@ -6089,7 +7038,8 @@ function registerSkillsCommands(program2) {
|
|
|
6089
7038
|
target: targetDir,
|
|
6090
7039
|
total: installed.length,
|
|
6091
7040
|
agent: opts.target ? "custom" : opts.agent,
|
|
6092
|
-
scope: opts.target ? "custom" : opts.scope
|
|
7041
|
+
scope: opts.target ? "custom" : opts.scope,
|
|
7042
|
+
source: "bundled"
|
|
6093
7043
|
}, "skills", "install", start);
|
|
6094
7044
|
} catch (err) {
|
|
6095
7045
|
fail(err, "skills", "install", start);
|
|
@@ -6165,7 +7115,7 @@ if (!process.env.PNCLI_VERIFY_TLS) {
|
|
|
6165
7115
|
var require2 = createRequire(import.meta.url);
|
|
6166
7116
|
var pkg = require2("../package.json");
|
|
6167
7117
|
var TAGLINE = "One command does what three meetings couldn't.";
|
|
6168
|
-
var program = new
|
|
7118
|
+
var program = new Command2();
|
|
6169
7119
|
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
7120
|
program.hook("preAction", (thisCommand) => {
|
|
6171
7121
|
const opts = thisCommand.optsWithGlobals();
|
|
@@ -6193,6 +7143,9 @@ registerJenkinsCommands(program);
|
|
|
6193
7143
|
registerArtifactoryCommands(program);
|
|
6194
7144
|
registerUdeployCommands(program);
|
|
6195
7145
|
registerCheckmarxCommands(program);
|
|
7146
|
+
registerServiceNowCommands(program);
|
|
7147
|
+
registerContrastCommands(program);
|
|
7148
|
+
registerSonatypeIqCommands(program);
|
|
6196
7149
|
registerSkillsCommands(program);
|
|
6197
7150
|
program.addHelpText("after", `
|
|
6198
7151
|
Services:
|
|
@@ -6208,6 +7161,9 @@ Services:
|
|
|
6208
7161
|
artifactory Artifactory (repos, artifact search, build info, properties)
|
|
6209
7162
|
udeploy IBM UrbanCode Deploy (component versions, deployment processes)
|
|
6210
7163
|
checkmarx Checkmarx CxSAST (projects, scans, scan statistics)
|
|
7164
|
+
servicenow ServiceNow (change requests)
|
|
7165
|
+
contrast Contrast IAST (applications, vulnerability findings)
|
|
7166
|
+
sonatypeiq Sonatype IQ Server (applications, organizations, policies)
|
|
6211
7167
|
config Manage pncli configuration
|
|
6212
7168
|
skills Download and manage Claude Code skills
|
|
6213
7169
|
`);
|