@westbayberry/dg 2.3.2 → 2.3.4

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.
Files changed (122) hide show
  1. package/README.md +95 -208
  2. package/dist/agents/copilot-cli.js +7 -2
  3. package/dist/agents/persistence.js +83 -8
  4. package/dist/agents/registry.js +5 -2
  5. package/dist/agents/routing.js +31 -14
  6. package/dist/api/analyze.js +138 -41
  7. package/dist/audit/deep.js +2 -1
  8. package/dist/audit/detectors.js +23 -2
  9. package/dist/audit/rules.js +4 -1
  10. package/dist/audit-ui/export.js +5 -4
  11. package/dist/auth/device-login.js +33 -11
  12. package/dist/auth/login-app.js +17 -12
  13. package/dist/auth/store.js +65 -9
  14. package/dist/bin/dg.js +1 -1
  15. package/dist/commands/audit.js +12 -20
  16. package/dist/commands/{guard-commit.js → commit-guard.js} +12 -12
  17. package/dist/commands/cooldown.js +2 -1
  18. package/dist/commands/decisions.js +8 -7
  19. package/dist/commands/help.js +11 -3
  20. package/dist/commands/licenses.js +17 -8
  21. package/dist/commands/login.js +58 -21
  22. package/dist/commands/router.js +12 -2
  23. package/dist/commands/sbom.js +20 -5
  24. package/dist/commands/scan.js +5 -4
  25. package/dist/commands/service.js +1 -1
  26. package/dist/commands/setup.js +12 -12
  27. package/dist/commands/status.js +4 -4
  28. package/dist/commands/uninstall.js +1 -1
  29. package/dist/commands/update.js +20 -0
  30. package/dist/commands/verify.js +4 -4
  31. package/dist/config/settings.js +81 -12
  32. package/dist/export-ui/ExportDialog.js +1 -1
  33. package/dist/gate/cooldown-request.js +22 -0
  34. package/dist/gate/egress-guard.js +185 -0
  35. package/dist/gate/host-match.js +48 -0
  36. package/dist/gate/verdict-core.js +101 -0
  37. package/dist/gate/verdict-fetch.js +214 -0
  38. package/dist/launcher/agent-check.js +1096 -42
  39. package/dist/launcher/agent-hook-exec.js +8 -1
  40. package/dist/launcher/agent-hook-io.js +5 -3
  41. package/dist/launcher/classify.js +110 -26
  42. package/dist/launcher/env.js +84 -18
  43. package/dist/launcher/install-preflight.js +26 -3
  44. package/dist/launcher/live-install.js +4 -2
  45. package/dist/launcher/manifest-screen.js +88 -7
  46. package/dist/launcher/output-redaction.js +153 -15
  47. package/dist/launcher/preflight-prompt.js +1 -1
  48. package/dist/launcher/resolve-real-binary.js +8 -3
  49. package/dist/launcher/run.js +237 -40
  50. package/dist/origin/artifact-cache.js +55 -0
  51. package/dist/origin/cargo-config.js +12 -0
  52. package/dist/origin/gate-mode.js +7 -0
  53. package/dist/origin/platform-paths.js +14 -0
  54. package/dist/origin/precedence.js +345 -0
  55. package/dist/origin/server.js +611 -0
  56. package/dist/origin/spike-worker.js +35 -0
  57. package/dist/origin/worker.js +116 -0
  58. package/dist/presentation/package-page.js +21 -1
  59. package/dist/presentation/safe-version.js +11 -0
  60. package/dist/presentation/sarif.js +109 -0
  61. package/dist/project/dgfile.js +54 -17
  62. package/dist/project/override-trust.js +0 -0
  63. package/dist/proxy/enforcement.js +27 -7
  64. package/dist/proxy/metadata-map.js +33 -7
  65. package/dist/proxy/preverified.js +0 -3
  66. package/dist/proxy/server.js +104 -446
  67. package/dist/proxy/upstream-proxy.js +23 -4
  68. package/dist/proxy/worker.js +3 -2
  69. package/dist/publish-set/collect.js +6 -0
  70. package/dist/runtime/fatal.js +2 -1
  71. package/dist/runtime/first-run.js +5 -1
  72. package/dist/runtime/node-version.js +51 -0
  73. package/dist/runtime/nudges.js +27 -1
  74. package/dist/sbom/cyclonedx.js +120 -33
  75. package/dist/sbom/graph.js +236 -0
  76. package/dist/sbom/spdx.js +53 -0
  77. package/dist/sbom-ui/SbomApp.js +5 -3
  78. package/dist/sbom-ui/inventory.js +8 -10
  79. package/dist/scan/analyze-worker.js +3 -1
  80. package/dist/scan/collect.js +38 -8
  81. package/dist/scan/command.js +77 -27
  82. package/dist/scan/discovery.js +18 -7
  83. package/dist/scan/installed.js +202 -0
  84. package/dist/scan/manifest-coverage.js +128 -0
  85. package/dist/scan/render.js +125 -40
  86. package/dist/scan/scanner-report.js +92 -22
  87. package/dist/scan/staged.js +68 -15
  88. package/dist/scan/types.js +7 -1
  89. package/dist/scan-ui/LegacyApp.js +3 -3
  90. package/dist/scan-ui/components/InteractiveResultsView.js +17 -14
  91. package/dist/scan-ui/hooks/useScan.js +37 -11
  92. package/dist/scan-ui/launch.js +2 -2
  93. package/dist/scan-ui/logo.js +6 -2
  94. package/dist/scan-ui/shims.js +6 -6
  95. package/dist/scripts/gate.js +53 -35
  96. package/dist/security/csv.js +9 -0
  97. package/dist/security/sanitize.js +11 -2
  98. package/dist/service/state.js +120 -37
  99. package/dist/service/trust-refresh.js +12 -1
  100. package/dist/service/trust-store.js +36 -5
  101. package/dist/service/worker.js +5 -7
  102. package/dist/setup/git-hook.js +2 -2
  103. package/dist/setup/install-method.js +36 -0
  104. package/dist/setup/plan.js +119 -57
  105. package/dist/setup/uninstall-standalone.js +7 -2
  106. package/dist/setup-ui/gate.js +1 -8
  107. package/dist/setup-ui/offer.js +14 -7
  108. package/dist/setup-ui/selector.js +1 -1
  109. package/dist/setup-ui/wizard.js +73 -78
  110. package/dist/standalone/uninstall.mjs +1114 -386
  111. package/dist/state/cleanup-registry.js +14 -3
  112. package/dist/state/cooldown-held.js +36 -13
  113. package/dist/state/locks.js +239 -34
  114. package/dist/state/sessions.js +2 -2
  115. package/dist/util/sh-escape.js +6 -0
  116. package/dist/util/tty-prompt.js +13 -4
  117. package/dist/verify/local.js +60 -10
  118. package/dist/verify/package-check.js +35 -49
  119. package/dist/verify/preflight.js +59 -33
  120. package/dist/verify/render.js +11 -12
  121. package/npm-shrinkwrap.json +714 -213
  122. package/package.json +4 -2
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
3
3
  import { analyzePackages, AnalyzeError } from "../api/analyze.js";
4
4
  import { createTheme } from "../presentation/theme.js";
5
5
  import { provenanceLabel, provenanceDowngradeLine } from "../presentation/provenance.js";
6
+ import { safeVersionLine } from "../presentation/safe-version.js";
6
7
  import { packagePageUrl } from "../presentation/package-page.js";
7
8
  import { resolvePresentation } from "../presentation/mode.js";
8
9
  import { isRemotePackageSpec, isSupportedLockfilePath } from "./preflight.js";
@@ -27,17 +28,23 @@ function parseSpec(target) {
27
28
  if (!rest) {
28
29
  return { error: `missing package name. ${DEEP_VERIFY_HINT}` };
29
30
  }
30
- const separator = ecosystem === "pypi" ? "==" : "@";
31
- const at = ecosystem === "npm" ? rest.lastIndexOf("@") : rest.indexOf("==");
32
- if (ecosystem === "npm" && (at <= 0)) {
33
- return { ecosystem, name: rest, version: null };
34
- }
35
- if (ecosystem === "pypi" && at < 0) {
31
+ if (ecosystem === "npm") {
32
+ const at = rest.lastIndexOf("@");
33
+ if (at <= 0) {
34
+ return { ecosystem, name: rest, version: null };
35
+ }
36
+ return { ecosystem, name: rest.slice(0, at), version: rest.slice(at + 1).trim() || null };
37
+ }
38
+ // PyPI names contain no '@' or '=='; accept either the registry-style '@' the
39
+ // help advertises or the PEP 508 '==' pin, whichever comes first.
40
+ const eq = rest.indexOf("==");
41
+ const at = rest.indexOf("@");
42
+ const cut = eq >= 0 && (at < 0 || eq < at) ? eq : at;
43
+ if (cut <= 0) {
36
44
  return { ecosystem, name: rest, version: null };
37
45
  }
38
- const name = rest.slice(0, at);
39
- const version = rest.slice(at + separator.length).trim() || null;
40
- return { ecosystem, name, version };
46
+ const version = rest.slice(cut + (cut === eq ? 2 : 1)).trim() || null;
47
+ return { ecosystem, name: rest.slice(0, cut), version };
41
48
  }
42
49
  export async function resolveLatest(ecosystem, name, fetchImpl) {
43
50
  try {
@@ -77,8 +84,7 @@ function provenanceLines(version, result, theme, withPredicate) {
77
84
  function reasonGlyph(action, theme) {
78
85
  return action === "block" ? theme.paint("block", "✘") : action === "warn" ? theme.paint("warn", "⚠") : theme.paint("muted", "·");
79
86
  }
80
- function renderResult(spec, version, result, theme, verbose) {
81
- const action = result.action ?? "pass";
87
+ function renderResult(spec, version, result, action, theme, verbose) {
82
88
  const badge = theme.badge(action);
83
89
  const lines = [`${badge} ${result.name}@${version} (${spec.ecosystem}) ${theme.paint("muted", `score ${result.score}`)}`];
84
90
  lines.push(...provenanceLines(version, result, theme, verbose));
@@ -100,40 +106,13 @@ function renderResult(spec, version, result, theme, verbose) {
100
106
  if (result.recommendation) {
101
107
  lines.push(` ${theme.paint("muted", result.recommendation)}`);
102
108
  }
109
+ lines.push(...safeVersionLine(result, theme, spec.ecosystem));
103
110
  const page = packagePageUrl(spec.ecosystem, result.name);
104
111
  if (page) {
105
112
  lines.push(` ${theme.paint("muted", `→ ${page}`)}`);
106
113
  }
107
114
  return `${lines.join("\n")}\n`;
108
115
  }
109
- const FREE_REASON_CAP = 3;
110
- const FREE_SCANS_FOOTER_THRESHOLD = 5_000;
111
- function renderFreeResult(spec, version, result, theme, freeScansRemaining) {
112
- const action = result.action ?? "pass";
113
- const lines = [`${theme.badge(action)} ${result.name}@${version} (${spec.ecosystem})`];
114
- lines.push(...provenanceLines(version, result, theme, false));
115
- if (action === "pass") {
116
- lines.push(` ${theme.paint("muted", result.reasons[0] ?? "no risk signals")}`);
117
- }
118
- else {
119
- const reasons = result.reasons.slice(0, FREE_REASON_CAP);
120
- for (const reason of reasons) {
121
- lines.push(` ${reasonGlyph(action, theme)} ${reason}`);
122
- }
123
- if (result.reasons.length > reasons.length) {
124
- lines.push(` ${theme.paint("muted", `… ${result.reasons.length - reasons.length} more — sign in to see all:`)} ${theme.paint("accent", "dg login")}`);
125
- }
126
- }
127
- const page = packagePageUrl(spec.ecosystem, result.name);
128
- if (page) {
129
- lines.push(` ${theme.paint("muted", `→ ${page}`)}`);
130
- }
131
- lines.push(` ${theme.paint("muted", "full findings, license, JSON output:")} ${theme.paint("accent", "dg login")}`);
132
- if (freeScansRemaining !== undefined && freeScansRemaining < FREE_SCANS_FOOTER_THRESHOLD) {
133
- lines.push(` ${theme.paint("muted", `${freeScansRemaining.toLocaleString()} free package checks left this month`)}`);
134
- }
135
- return `${lines.join("\n")}\n`;
136
- }
137
116
  function encodeNpmPackagePath(name) {
138
117
  return name.split("/").map((segment) => encodeURIComponent(segment)).join("%2f");
139
118
  }
@@ -155,12 +134,12 @@ export async function runPackageCheck(target, io = {}, options = {}) {
155
134
  const accent = (text) => theme.paint("accent", text);
156
135
  const muted = (text) => theme.paint("muted", text);
157
136
  const authenticated = authStatus(io.env).authenticated;
158
- if (!authenticated && (options.format === "json" || options.outputPath)) {
137
+ if (!authenticated) {
159
138
  return {
160
139
  exitCode: EXIT_UNAVAILABLE,
161
140
  stdout: "",
162
- stderr: `\n ${theme.paint("warn", "⚠")} ${muted("--json and --output for registry checks require sign-in.")}\n` +
163
- ` ${accent("dg login")}${muted(" · see plans at")} ${accent("westbayberry.com/pricing")}\n\n`
141
+ stderr: `\n ${theme.paint("warn", "⚠")} ${muted("Sign in to run dg verify.")}\n` +
142
+ ` ${muted("Run")} ${accent("dg login")}\n\n`
164
143
  };
165
144
  }
166
145
  const parsed = parseSpec(target);
@@ -186,7 +165,6 @@ export async function runPackageCheck(target, io = {}, options = {}) {
186
165
  }
187
166
  }
188
167
  let result;
189
- let freeScansRemaining;
190
168
  try {
191
169
  const response = await analyzePackages([{ name: parsed.name, version }], {
192
170
  ecosystem: parsed.ecosystem,
@@ -194,7 +172,6 @@ export async function runPackageCheck(target, io = {}, options = {}) {
194
172
  ...(io.env ? { env: io.env } : {})
195
173
  });
196
174
  result = response.packages.find((entry) => entry.name === parsed.name) ?? response.packages[0];
197
- freeScansRemaining = response.freeScansRemaining;
198
175
  }
199
176
  catch (error) {
200
177
  if (error instanceof AnalyzeError && error.code === "quota_exceeded") {
@@ -225,11 +202,10 @@ export async function runPackageCheck(target, io = {}, options = {}) {
225
202
  reasons: result.reasons,
226
203
  findings: result.findings,
227
204
  ...(result.provenance ? { provenance: result.provenance } : {}),
228
- ...(result.recommendation ? { recommendation: result.recommendation } : {})
205
+ ...(result.recommendation ? { recommendation: result.recommendation } : {}),
206
+ ...(result.safeVersion ? { safeVersion: result.safeVersion } : {})
229
207
  }, null, 2)}\n`
230
- : authenticated
231
- ? renderResult(parsed, version, result, theme, options.verbose ?? false)
232
- : renderFreeResult(parsed, version, result, theme, freeScansRemaining);
208
+ : renderResult(parsed, version, result, action, theme, options.verbose ?? false);
233
209
  if (options.outputPath) {
234
210
  try {
235
211
  writeFileSync(resolve(options.outputPath), rendered, "utf8");
@@ -262,6 +238,7 @@ export async function maybeVerifyPackage(args) {
262
238
  let outputPath = null;
263
239
  let outputFlagMissingPath;
264
240
  let target;
241
+ let extraPositional;
265
242
  let unknownFlag;
266
243
  for (let index = 0; index < rest.length; index += 1) {
267
244
  const arg = rest[index];
@@ -290,8 +267,11 @@ export async function maybeVerifyPackage(args) {
290
267
  else if (arg.startsWith("-")) {
291
268
  unknownFlag = unknownFlag ?? arg;
292
269
  }
270
+ else if (target === undefined) {
271
+ target = arg;
272
+ }
293
273
  else {
294
- target = target ?? arg;
274
+ extraPositional = extraPositional ?? arg;
295
275
  }
296
276
  }
297
277
  if (!target) {
@@ -315,6 +295,12 @@ export async function maybeVerifyPackage(args) {
315
295
  result: { exitCode: EXIT_USAGE_VERDICT, stdout: "", stderr: `dg verify: unknown option '${unknownFlag}'. Run 'dg verify --help'.\n` }
316
296
  };
317
297
  }
298
+ if (extraPositional) {
299
+ return {
300
+ handled: true,
301
+ result: { exitCode: EXIT_USAGE_VERDICT, stdout: "", stderr: `dg verify: unexpected argument '${extraPositional}' — verify accepts one target. Run 'dg verify --help'.\n` }
302
+ };
303
+ }
318
304
  if (sarif) {
319
305
  return {
320
306
  handled: true,
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
3
  import { loadUserConfig } from "../config/settings.js";
4
4
  import { evaluatePackagePolicy, resolveEffectivePolicy } from "../policy/evaluate.js";
5
5
  const LOCKFILE_NAMES = new Set([
@@ -124,6 +124,7 @@ export function parseLockfilePackages(targetPath) {
124
124
  }
125
125
  const context = createParseContext(fileName, absoluteTarget);
126
126
  const observations = parseLockfile(text, context);
127
+ recordBlockedObservationSkips(observations, context);
127
128
  return {
128
129
  packages: observations
129
130
  .filter((observation) => observation.verdict !== "block" && observation.identity.sourceKind === "lockfile")
@@ -132,24 +133,49 @@ export function parseLockfilePackages(targetPath) {
132
133
  parseError: context.errors[0] ?? null
133
134
  };
134
135
  }
136
+ // A block-verdict entry is excluded from the scannable set; without an itemized
137
+ // skip it would vanish from the scan with no verdict and no notice. Record one for
138
+ // each blocked entry the per-parser path did not already skip.
139
+ function recordBlockedObservationSkips(observations, context) {
140
+ const alreadySkipped = new Set(context.skipped.map((skip) => skip.location));
141
+ for (const observation of observations) {
142
+ if (observation.verdict !== "block" || observation.identity.ecosystem === "unknown") {
143
+ continue;
144
+ }
145
+ const location = observation.identity.requested;
146
+ if (alreadySkipped.has(location)) {
147
+ continue;
148
+ }
149
+ alreadySkipped.add(location);
150
+ recordSkip(context, packageDisplayName(observation.identity), blockedObservationReason(observation.identity), location);
151
+ }
152
+ }
153
+ function blockedObservationReason(identity) {
154
+ if (identity.resolvedUrl && isUnsafeResolvedUrl(identity.resolvedUrl)) {
155
+ return "git";
156
+ }
157
+ // Off-registry hosts and URL fallbacks are labeled direct-url, as the per-parser skip path already does.
158
+ return "direct-url";
159
+ }
135
160
  function createParseContext(fileName, filePath) {
136
161
  return {
137
162
  fileName,
138
163
  filePath,
139
- registryHosts: expectedRegistryHosts(fileName, filePath),
164
+ registryHosts: expectedRegistryHosts(fileName),
140
165
  skipped: [],
141
166
  errors: []
142
167
  };
143
168
  }
144
- function expectedRegistryHosts(fileName, filePath) {
169
+ // Only the canonical public registries are trusted here. A repo-local .npmrc is
170
+ // attacker-controlled in a cloned repo, so it must not widen this allowlist (a
171
+ // hostile repo must never relax screening); legit private registries are fronted
172
+ // by the proxy door at transport time, not blessed by a static preflight.
173
+ function expectedRegistryHosts(fileName) {
145
174
  const hosts = new Set();
146
175
  if (NPM_LOCKFILE_NAMES.has(fileName)) {
147
176
  for (const host of DEFAULT_NPM_REGISTRY_HOSTS) {
148
177
  hosts.add(host);
149
178
  }
150
- for (const host of npmrcRegistryHosts(filePath)) {
151
- hosts.add(host);
152
- }
153
179
  }
154
180
  else if (PYPI_LOCKFILE_NAMES.has(fileName)) {
155
181
  for (const host of DEFAULT_PYPI_REGISTRY_HOSTS) {
@@ -158,32 +184,6 @@ function expectedRegistryHosts(fileName, filePath) {
158
184
  }
159
185
  return hosts;
160
186
  }
161
- function npmrcRegistryHosts(lockfilePath) {
162
- if (!lockfilePath) {
163
- return [];
164
- }
165
- let text;
166
- try {
167
- text = readLockfileText(join(dirname(lockfilePath), ".npmrc"));
168
- }
169
- catch {
170
- return [];
171
- }
172
- const hosts = [];
173
- for (const line of text.split(/\r?\n/u)) {
174
- const value = /^\s*(?:@[^=\s:]+:)?registry\s*=\s*(\S+)/u.exec(line)?.[1];
175
- if (!value) {
176
- continue;
177
- }
178
- try {
179
- hosts.push(new URL(value).hostname.toLowerCase());
180
- }
181
- catch {
182
- continue;
183
- }
184
- }
185
- return hosts;
186
- }
187
187
  function untrustedResolvedHost(value, context) {
188
188
  if (context.registryHosts.size === 0 || !/^https?:\/\//iu.test(value)) {
189
189
  return null;
@@ -478,8 +478,13 @@ function npmAliasVersion(version) {
478
478
  }
479
479
  return { name: spec.slice(0, at), version: spec.slice(at + 1) || null };
480
480
  }
481
- function parseYarnLock(text, context) {
481
+ function parseYarnLock(rawText, context) {
482
482
  const observations = [];
483
+ // The block-split lookahead matches a header terminated by ":\n"; a yarn.lock
484
+ // written with CRLF endings terminates headers with ":\r\n", so without this
485
+ // normalization the whole file collapses to one unparsed block and every
486
+ // package silently vanishes.
487
+ const text = rawText.replace(/\r\n/gu, "\n");
483
488
  const blocks = text.split(/\n(?=(?:"?@?[^"\s].*"?):\n)/u);
484
489
  for (const block of blocks) {
485
490
  const lines = block.split(/\r?\n/u);
@@ -688,6 +693,27 @@ function collectRequirements(text, baseDir, state, context, observations) {
688
693
  recordSkip(context, target, "local", line);
689
694
  continue;
690
695
  }
696
+ const indexOverride = /^(?:-i|--index-url|--extra-index-url|--find-links)[=\s]+(\S+)/u.exec(line);
697
+ if (indexOverride?.[1]) {
698
+ const indexUrl = indexOverride[1];
699
+ recordSkip(context, indexUrl, "index-override", line);
700
+ observations.push(packageObservation({
701
+ ecosystem: "pypi",
702
+ name: indexUrl,
703
+ version: null,
704
+ requested: line,
705
+ sourceKind: "lockfile-url-fallback",
706
+ resolvedUrl: indexUrl,
707
+ integrity: null,
708
+ license: null
709
+ }, "warn", {
710
+ id: "unverified-package-index",
711
+ title: "Unverified package index override",
712
+ message: `requirements resolve from '${indexUrl}', a non-default index that dg does not verify against the canonical registry`,
713
+ location: line
714
+ }));
715
+ continue;
716
+ }
691
717
  if (line.startsWith("-")) {
692
718
  continue;
693
719
  }
@@ -1,5 +1,6 @@
1
1
  import { createTheme } from "../presentation/theme.js";
2
2
  import { packagePageUrl } from "../presentation/package-page.js";
3
+ import { automationCategory, findingFingerprint, sarifLocationEntry } from "../presentation/sarif.js";
3
4
  import { sanitizeDeep } from "../security/sanitize.js";
4
5
  const STATUS_ROLE = {
5
6
  pass: "pass",
@@ -160,30 +161,28 @@ export function renderVerifySarif(report) {
160
161
  rules
161
162
  }
162
163
  },
164
+ automationDetails: {
165
+ id: automationCategory(report.target)
166
+ },
167
+ properties: {
168
+ dgVerdict: report.status
169
+ },
163
170
  results: report.findings.map((finding) => ({
164
171
  ruleId: finding.id,
165
172
  level: finding.severity === "block" ? "error" : "warning",
166
173
  message: {
167
174
  text: finding.message
168
175
  },
169
- locations: [
170
- {
171
- physicalLocation: {
172
- artifactLocation: {
173
- uri: artifactUri(finding.location)
174
- }
175
- }
176
- }
177
- ]
176
+ partialFingerprints: {
177
+ dgFindingHash: findingFingerprint(`${finding.id} ${finding.location}`)
178
+ },
179
+ locations: [sarifLocationEntry(finding.location, report.target)]
178
180
  }))
179
181
  }
180
182
  ]
181
183
  };
182
184
  return `${JSON.stringify(sarif, null, 2)}\n`;
183
185
  }
184
- function artifactUri(location) {
185
- return location.replace(/:\d+$/, "");
186
- }
187
186
  function uniqueFindings(findings) {
188
187
  const seen = new Set();
189
188
  const unique = [];