@xagents_in/unmask 0.1.3 → 0.1.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 (50) hide show
  1. package/README.md +86 -4
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/cache.mjs +488 -0
  5. package/dist/cli/compare.mjs +261 -0
  6. package/dist/cli/config.mjs +141 -0
  7. package/dist/cli/discovery.mjs +372 -0
  8. package/dist/cli/doctor.mjs +354 -0
  9. package/dist/cli/exceptions.mjs +170 -0
  10. package/dist/cli/file.mjs +271 -0
  11. package/dist/cli/files.mjs +141 -0
  12. package/dist/cli/history.mjs +232 -0
  13. package/dist/cli/hosted.mjs +251 -43
  14. package/dist/cli/info.mjs +125 -0
  15. package/dist/cli/init.mjs +57 -0
  16. package/dist/cli/install-guard.mjs +896 -0
  17. package/dist/cli/lockfile.mjs +330 -0
  18. package/dist/cli/main.mjs +97 -14
  19. package/dist/cli/preflight.mjs +388 -0
  20. package/dist/cli/sarif.mjs +100 -0
  21. package/dist/cli/shell-integration.mjs +122 -0
  22. package/dist/cli/terminal.mjs +396 -0
  23. package/dist/cli/watch.mjs +233 -0
  24. package/dist/cli/why.mjs +256 -0
  25. package/dist/core/advisories.mjs +101 -24
  26. package/dist/core/advisory-cache.mjs +295 -0
  27. package/dist/core/behavior-signals.mjs +1088 -0
  28. package/dist/core/dependency-graph.mjs +332 -0
  29. package/dist/core/evidence-exports.mjs +469 -0
  30. package/dist/core/guard-config.mjs +463 -0
  31. package/dist/core/guard-policy.mjs +308 -0
  32. package/dist/core/package-details.mjs +42 -6
  33. package/dist/core/report.mjs +7 -0
  34. package/dist/core/sarif.mjs +556 -0
  35. package/dist/core/syntax.mjs +296 -15
  36. package/dist/model/client.mjs +204 -56
  37. package/dist/model/diff-evidence.mjs +177 -0
  38. package/dist/model/summary.mjs +183 -0
  39. package/dist/server/advisory-result.mjs +137 -0
  40. package/dist/server/app.mjs +60 -2
  41. package/dist/server/dependency-screening.mjs +386 -0
  42. package/dist/server/exports.mjs +63 -0
  43. package/dist/server/guard-decision.mjs +37 -0
  44. package/dist/server/main.mjs +25 -7
  45. package/dist/server/queue.mjs +95 -53
  46. package/dist/server/store.mjs +21 -1
  47. package/dist/version.json +1 -1
  48. package/dist/viewer/render.mjs +10 -2
  49. package/dist/viewer/shell.mjs +1 -1
  50. package/package.json +1 -1
package/README.md CHANGED
@@ -49,12 +49,61 @@ type an exact release such as `chalk@6.0.1`, and press **check it**.
49
49
  **From a terminal:**
50
50
 
51
51
  ```bash
52
- npx @xagents_in/unmask check chalk@6.0.1 --out ./unmask-report
52
+ npx @xagents_in/unmask check chalk@6.0.1
53
53
  ```
54
54
 
55
- You get `report.json` and an escaped `report.html` in `./unmask-report`. Only the
55
+ The terminal shows the result, coverage, findings, known advisories, the AI
56
+ explanation status and next steps. Add `--out ./unmask-report` to also save
57
+ `report.json` and an escaped `report.html`; nothing is written without it. Only the
56
58
  package name and version leave your machine; the hosted service does the rest.
57
59
 
60
+ ### What the terminal shows
61
+
62
+ The examples below are real CLI output from a local test server with synthetic
63
+ fixtures and recorded OSV advisory data. They are not live-service results.
64
+
65
+ A completed check prints the result and exit code, coverage, findings, the OSV
66
+ advisories for that exact version, the AI status and next steps. The AI summary
67
+ appears only when the model actually returned one; otherwise the line names the
68
+ status, such as `unavailable (quota)`, and nothing stands in for it.
69
+
70
+ ```text
71
+ UNMASK fixture@1.0.0
72
+ Result Deterministic findings need review (exit 3)
73
+ Coverage complete: 1 file, 81 B
74
+ 0 byte-identical, 1 changed, 0 artifact-only, 0 not-compared
75
+ Source selected via user-full-SHA at dddddddddddd
76
+ Findings 1
77
+ - lifecycle-source-absent (script install)
78
+ Advisory none reported by OSV for this exact version
79
+ AI complete, 0 hypotheses (separate from deterministic evidence)
80
+ Summary: The artifact declares an install script that the selected source does not.
81
+ Scope: 1 evidence items supplied, 0 omitted; not checked: runtime-behavior
82
+ Uncertainty: The script was not executed, so its runtime effect is unknown.
83
+ Next
84
+ - Review each finding and its cited evidence before installing.
85
+ - AI suggests: Compare the install script with the upstream repository before installing.
86
+ - Add --out DIR to save the full JSON and HTML evidence.
87
+ ```
88
+
89
+ When the registry artifact cannot be analyzed, for example a removed release or
90
+ a timed-out run, the check still shows the advisories for that exact version. It
91
+ says that nothing was inspected and exits `2`:
92
+
93
+ ```text
94
+ UNMASK ua-parser-js@0.7.29
95
+ Result Advisory-only: artifact not inspected; no verdict is claimed (exit 2)
96
+ Artifact not inspected (execution-timeout); source comparison not performed
97
+ Advisory 1 known for this exact version (OSV)
98
+ - GHSA-pjwm-rvh2-c87w Embedded malware in ua-parser-js
99
+ AI not run; no artifact evidence to explain
100
+ ```
101
+
102
+ Progress and errors go to stderr, so stdout stays clean for pipes. Color appears
103
+ only on an interactive terminal; set `NO_COLOR=1` to turn it off. Exit `0` means
104
+ no review rule fired in the inspected scope. It does not mean safe, and a
105
+ missing advisory is not evidence of safety.
106
+
58
107
  ## What a check returns
59
108
 
60
109
  A real report for **`zod@4.6.7`**, from the live service:
@@ -183,8 +232,8 @@ npm run check:package
183
232
  node dist/cli/main.mjs check chalk@6.0.1 --out /tmp/unmask-chalk-report
184
233
  ```
185
234
 
186
- Choose a fresh output folder for each check; existing report files are never
187
- replaced. `UNMASK_SERVER_URL` selects another service: an HTTPS origin, or a
235
+ `--out` is optional. When given, choose a fresh output folder for each check;
236
+ existing report files are never replaced. `UNMASK_SERVER_URL` selects another service: an HTTPS origin, or a
188
237
  loopback HTTP origin for local testing. A hosted failure never falls back to local
189
238
  acquisition. See [CLI configuration and data flow](docs/cli.md).
190
239
 
@@ -199,6 +248,39 @@ folder instead. Leave out all source options for an artifact-only report.
199
248
  `--claim name@version` checks an identity claim against the package manifest.
200
249
  Excerpts leave the machine only if you add `--allow-model` and configure a provider.
201
250
 
251
+ **Commands in this build.** `node dist/cli/main.mjs --help` (or `unmask --help`
252
+ once installed) prints the same list, and `unmask help COMMAND` shows its usage.
253
+
254
+ | Command | What it does |
255
+ | ------------- | ------------------------------------------------------------------------------ |
256
+ | `check` | Check one exact version through the hosted service, or a local tarball offline |
257
+ | `install` | Refuse mutable npm install and explain the lockfile-first ci path |
258
+ | `ci` | Screen the exact lockfile plan before invoking npm ci |
259
+ | `enable` | Print explicit reversible shell integration |
260
+ | `disable` | Print how to remove the shell integration |
261
+ | `audit` | Check exact versions locked in a package-lock.json, with bounded coverage |
262
+ | `why` | Explain dependency paths recorded in the lockfile |
263
+ | `info` | Show hosted package metadata, scripts, dependencies and recorded assessment |
264
+ | `files` | List a bounded hosted package file inventory |
265
+ | `file` | Inspect recorded file evidence using a report-bound file selector |
266
+ | `compare` | Compare scripts, dependencies and files between two exact versions |
267
+ | `history` | Show a bounded registry release timeline with exact-version guidance |
268
+ | `config` | Manage project-local guard policy settings |
269
+ | `exceptions` | Manage explicit expiring exact-version guard exceptions |
270
+ | `init` | Create project guard configuration explicitly |
271
+ | `doctor` | Diagnose Node, service reachability and project guard configuration |
272
+ | `cache` | Import, inspect and explicitly prune local report snapshots |
273
+ | `sarif` | Export recorded findings as SARIF from an existing report |
274
+ | `annotations` | Produce escaped CI annotations and a summary from an existing report |
275
+ | `watch` | Opt in to watch a lockfile and check changes |
276
+ | `preflight` | Check changed lockfile entries against a baseline for CI |
277
+ | `scan` | Resolve a dist-tag to an exact version, then check it |
278
+ | `batch` | Check several explicit exact versions serially with duplicate coalescing |
279
+
280
+ The explicit install guard can block before npm when policy or incomplete evidence
281
+ requires it. Inspection commands report recorded evidence and coverage; exit `0`
282
+ never means a package is safe.
283
+
202
284
  **Run the website locally:**
203
285
 
204
286
  ```bash
@@ -0,0 +1,145 @@
1
+ import { constants } from "node:fs";
2
+ import { open } from "node:fs/promises";
3
+ import { LocalInputError } from "../core/pipeline.mjs";
4
+ import { collectFindings } from "../core/sarif.mjs";
5
+ import { parseExportOptions, readExportInputs } from "./sarif.mjs";
6
+
7
+ const MAX_SUMMARY_ITEMS = 50;
8
+ const commands = { error: "error", warning: "warning", note: "notice" };
9
+ const visible = (value) =>
10
+ String(value).replace(
11
+ /[\u0000-\u0009\u000b\u000c\u000e-\u001f\u007f-\u009f\u2028\u2029]/g,
12
+ (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`,
13
+ );
14
+
15
+ export function escapeData(value) {
16
+ return visible(value)
17
+ .replace(/%/g, "%25")
18
+ .replace(/\r/g, "%0D")
19
+ .replace(/\n/g, "%0A");
20
+ }
21
+
22
+ export function escapeProperty(value) {
23
+ return escapeData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
24
+ }
25
+
26
+ export function escapeMarkdown(value) {
27
+ return visible(value)
28
+ .replace(/[\r\n]+/g, " ")
29
+ .replace(/[\\`*_{}[\]()<>#+!|~&"'=-]/g, (char) => `\\${char}`);
30
+ }
31
+
32
+ function where(result) {
33
+ const spans = result.locations.map(
34
+ (item) =>
35
+ `${item.side} ${item.path} bytes ${item.byteOffset}-${item.byteOffset + item.byteLength}`,
36
+ );
37
+ return spans.length ? ` [${spans.join("; ")}]` : "";
38
+ }
39
+
40
+ export function annotationOutcome(collected) {
41
+ const { report, advisories, results } = collected;
42
+ let exitCode = report.policy.exitCode;
43
+ const knownAdvisory = results.some((item) =>
44
+ ["advisory/known-malware", "advisory/known-vulnerability"].includes(
45
+ item.ruleId,
46
+ ),
47
+ );
48
+ if (exitCode === 0 && knownAdvisory) exitCode = 3;
49
+ if (exitCode === 0 && advisories && !advisories.complete) exitCode = 2;
50
+ return exitCode;
51
+ }
52
+
53
+ export function renderAnnotations(collected) {
54
+ const lines = [];
55
+ for (const result of collected.results)
56
+ lines.push(
57
+ `::${commands[result.level]} title=${escapeProperty(`unmask ${result.ruleId}`)}::${escapeData(result.message + where(result))}`,
58
+ );
59
+ for (const item of collected.limitations)
60
+ lines.push(
61
+ `::${commands[item.level]} title=${escapeProperty(`unmask ${item.id}`)}::${escapeData(item.message)}`,
62
+ );
63
+ return lines.join("\n") + "\n";
64
+ }
65
+
66
+ export function renderSummary(collected, exitCode) {
67
+ const { report, identity, advisories, results, limitations } = collected;
68
+ const conflict = identity?.identityStatus === "claim-conflicts-with-manifest";
69
+ const name = identity
70
+ ? conflict
71
+ ? `${identity.name}@${identity.version} (claimed; artifact declares ${identity.declared.name}@${identity.declared.version})`
72
+ : `${identity.name}@${identity.version}`
73
+ : "unidentified artifact";
74
+ const count = (level) =>
75
+ results.filter((item) => item.level === level).length;
76
+ const lines = [
77
+ `## UNMASK check: ${escapeMarkdown(name)}`,
78
+ "",
79
+ "| Item | Value |",
80
+ "| --- | --- |",
81
+ `| Exit code | ${exitCode} |`,
82
+ `| Policy findings | ${report.policy.findings.length} |`,
83
+ `| Coverage complete | ${report.coverage.complete ? "yes" : "no"} |`,
84
+ `| Identity | ${escapeMarkdown(identity?.identityStatus ?? "unknown")} |`,
85
+ `| Advisory check | ${escapeMarkdown(advisories ? `${advisories.status}${advisories.boundTo === "claim" && identity.identityStatus !== "claim-matches-manifest" ? " (for requested identity)" : ""}` : "not supplied")} |`,
86
+ `| Results | ${count("error")} error, ${count("warning")} warning, ${count("note")} note |`,
87
+ `| Artifact sha256 | \`${report.inputs.artifact.sha256}\` |`,
88
+ "",
89
+ ];
90
+ if (results.length) {
91
+ lines.push("### Results", "");
92
+ for (const item of results.slice(0, MAX_SUMMARY_ITEMS))
93
+ lines.push(
94
+ `- **${item.level}** \`${item.ruleId}\`: ${escapeMarkdown(item.message + where(item))}`,
95
+ );
96
+ if (results.length > MAX_SUMMARY_ITEMS)
97
+ lines.push(
98
+ `- ${results.length - MAX_SUMMARY_ITEMS} more results omitted`,
99
+ );
100
+ lines.push("");
101
+ }
102
+ lines.push("### Limitations", "");
103
+ for (const item of limitations)
104
+ lines.push(`- ${item.level}: ${escapeMarkdown(item.message)}`);
105
+ return lines.join("\n") + "\n\n";
106
+ }
107
+
108
+ async function appendSummary(filename, markdown) {
109
+ let handle;
110
+ try {
111
+ handle = await open(
112
+ filename,
113
+ constants.O_WRONLY |
114
+ constants.O_APPEND |
115
+ constants.O_CREAT |
116
+ constants.O_NONBLOCK |
117
+ (constants.O_NOFOLLOW ?? 0),
118
+ 0o600,
119
+ );
120
+ if (!(await handle.stat()).isFile()) throw new Error("not regular");
121
+ await handle.writeFile(markdown);
122
+ } catch {
123
+ throw new LocalInputError(
124
+ "output-write-failed",
125
+ "Summary requires a regular, writable file path",
126
+ );
127
+ } finally {
128
+ await handle?.close();
129
+ }
130
+ }
131
+
132
+ export async function annotationsCommand(argv, stdout = process.stdout) {
133
+ const options = parseExportOptions(argv, {
134
+ "--report": "report",
135
+ "--details": "details",
136
+ "--summary": "summary",
137
+ });
138
+ const { report, details } = await readExportInputs(options);
139
+ const collected = collectFindings(report, details);
140
+ const exitCode = annotationOutcome(collected);
141
+ if (options.summary !== undefined)
142
+ await appendSummary(options.summary, renderSummary(collected, exitCode));
143
+ stdout.write(renderAnnotations(collected));
144
+ return exitCode;
145
+ }
@@ -0,0 +1,223 @@
1
+ import { LocalInputError } from "../core/pipeline.mjs";
2
+ import { AcquisitionError } from "../core/acquire.mjs";
3
+ import { checkHosted } from "./hosted.mjs";
4
+ import { exactTargets, readLockfile } from "./lockfile.mjs";
5
+
6
+ export const AUDIT_LIMITS = Object.freeze({
7
+ defaultMax: 6,
8
+ maxPackages: 50,
9
+ maxConcurrency: 2,
10
+ });
11
+ const STATUS_BY_EXIT = { 0: "no-findings", 2: "incomplete", 3: "findings" };
12
+
13
+ export function bindingStatus(target, report) {
14
+ if (!target.bound) return "unbound";
15
+ const actual = report?.inputs?.artifact?.sha512;
16
+ if (typeof actual !== "string" || !/^[a-f0-9]{128}$/.test(actual))
17
+ return "unavailable";
18
+ const locked = target.integrity
19
+ .flatMap((value) => value.split(/\s+/))
20
+ .filter((value) => value.startsWith("sha512-"))
21
+ .map((value) => Buffer.from(value.slice(7), "base64").toString("hex"));
22
+ return locked.length > 0 && locked.every((value) => value === actual)
23
+ ? "verified"
24
+ : "mismatch";
25
+ }
26
+
27
+ function positive(value, max, label) {
28
+ if (!/^[1-9]\d*$/.test(value ?? "") || Number(value) > max)
29
+ throw new LocalInputError(
30
+ "invalid-option",
31
+ `${label} must be an integer from 1 to ${max}.`,
32
+ );
33
+ return Number(value);
34
+ }
35
+
36
+ export function parseAuditArgs(argv) {
37
+ const options = {
38
+ lockfile: "package-lock.json",
39
+ max: AUDIT_LIMITS.defaultMax,
40
+ concurrency: 1,
41
+ omitDev: false,
42
+ json: false,
43
+ };
44
+ const seen = new Set();
45
+ for (let index = 0; index < argv.length; index++) {
46
+ const key = argv[index];
47
+ if (seen.has(key))
48
+ throw new LocalInputError(
49
+ "duplicate-option",
50
+ "Each option may be supplied only once",
51
+ );
52
+ seen.add(key);
53
+ if (key === "--json") options.json = true;
54
+ else if (key === "--omit-dev") options.omitDev = true;
55
+ else if (key === "--lockfile") {
56
+ const value = argv[++index];
57
+ if (!value || value.startsWith("--"))
58
+ throw new LocalInputError(
59
+ "missing-value",
60
+ "An option is missing its value",
61
+ );
62
+ options.lockfile = value;
63
+ } else if (key === "--max")
64
+ options.max = positive(argv[++index], AUDIT_LIMITS.maxPackages, "--max");
65
+ else if (key === "--concurrency")
66
+ options.concurrency = positive(
67
+ argv[++index],
68
+ AUDIT_LIMITS.maxConcurrency,
69
+ "--concurrency",
70
+ );
71
+ else
72
+ throw new LocalInputError(
73
+ "unknown-option",
74
+ "Use audit [--lockfile PATH] [--max N] [--concurrency N] [--omit-dev] [--json]",
75
+ );
76
+ }
77
+ return options;
78
+ }
79
+
80
+ async function checkOne(target, check) {
81
+ try {
82
+ const report = await check(target.spec);
83
+ const exitCode = report?.policy?.exitCode;
84
+ return {
85
+ ...target,
86
+ status: STATUS_BY_EXIT[exitCode] ?? "incomplete",
87
+ binding: bindingStatus(target, report),
88
+ exitCode,
89
+ findings: (report.policy.findings ?? []).map((item) => item.ruleId),
90
+ model: report.model?.status ?? "not-reported",
91
+ };
92
+ } catch (error) {
93
+ return {
94
+ ...target,
95
+ status: "failed",
96
+ error:
97
+ error instanceof LocalInputError
98
+ ? { code: error.code, message: error.message }
99
+ : error instanceof AcquisitionError
100
+ ? { code: error.code, message: "Invalid exact package identity." }
101
+ : { code: "internal", message: "Check failed unexpectedly." },
102
+ };
103
+ }
104
+ }
105
+
106
+ export async function auditLock(
107
+ lock,
108
+ { check, max = AUDIT_LIMITS.defaultMax, concurrency = 1, omitDev = false },
109
+ ) {
110
+ const { considered, targets, skipped, omitted } = exactTargets(lock, {
111
+ omitDev,
112
+ });
113
+ const selected = targets.slice(0, max);
114
+ const results = new Array(selected.length);
115
+ let next = 0;
116
+ // Bounded worker pool keeps hosted load within the shared rate limit.
117
+ await Promise.all(
118
+ Array.from({ length: Math.min(concurrency, selected.length) }, async () => {
119
+ while (next < selected.length) {
120
+ const index = next++;
121
+ results[index] = await checkOne(selected[index], check);
122
+ }
123
+ }),
124
+ );
125
+ const notChecked = targets
126
+ .slice(max)
127
+ .map((target) => ({ ...target, status: "not-checked" }));
128
+ const counts = {};
129
+ for (const item of [...results, ...notChecked])
130
+ counts[item.status] = (counts[item.status] ?? 0) + 1;
131
+ const skippedTotal = Object.values(skipped).reduce((a, b) => a + b, 0);
132
+ const binding = {};
133
+ for (const item of results)
134
+ if (item.binding) binding[item.binding] = (binding[item.binding] ?? 0) + 1;
135
+ const complete =
136
+ skippedTotal === 0 &&
137
+ notChecked.length === 0 &&
138
+ results.every(
139
+ (item) =>
140
+ ["no-findings", "findings"].includes(item.status) &&
141
+ item.binding === "verified",
142
+ );
143
+ return {
144
+ schemaVersion: "1",
145
+ lockfileVersion: lock.lockfileVersion,
146
+ scope: omitDev ? "production-and-optional" : "all-locked",
147
+ coverage: {
148
+ complete,
149
+ lockedEntries: considered,
150
+ uniqueExact: targets.length,
151
+ checked: results.length,
152
+ counts,
153
+ binding,
154
+ skipped,
155
+ omitted,
156
+ },
157
+ exitCode: counts.findings ? 3 : complete ? 0 : 2,
158
+ results: [...results, ...notChecked],
159
+ limitations: [
160
+ "No findings means no deterministic policy finding in the checked scope, not that a package is safe.",
161
+ "Removed or unpublished registry versions fail acquisition and are reported as failed, not clean.",
162
+ "Git, URL, file, workspace and invalid entries are not audited by exact registry identity.",
163
+ "Coverage requires each locked sha512 integrity to equal the hosted artifact sha512; unbound (no registry.npmjs.org resolved or sha512 integrity), mismatched or unreported hashes stay incomplete.",
164
+ "Unlocked optionalDependencies and optional peers are counted under coverage.omitted and not audited; npm does not require them, so they do not make coverage incomplete.",
165
+ "At most --max (default 6, matching the hosted limit of six new checks per ten minutes) unique versions are checked, in name order; the rest are not-checked.",
166
+ ],
167
+ };
168
+ }
169
+
170
+ function formatText(result) {
171
+ const lines = [];
172
+ const { coverage } = result;
173
+ lines.push(
174
+ `Audited ${coverage.checked} of ${coverage.uniqueExact} unique exact versions (${coverage.lockedEntries} locked entries, lockfile v${result.lockfileVersion}).`,
175
+ );
176
+ for (const item of result.results) {
177
+ const detail =
178
+ item.status === "failed"
179
+ ? ` ${item.error.code}`
180
+ : (item.findings?.length ? ` ${item.findings.join(",")}` : "") +
181
+ (item.binding && item.binding !== "verified"
182
+ ? ` [binding ${item.binding}]`
183
+ : "");
184
+ const alias = item.aliases?.length
185
+ ? ` (as ${item.aliases.join(", ")})`
186
+ : "";
187
+ lines.push(` ${item.status.padEnd(12)} ${item.spec}${alias}${detail}`);
188
+ }
189
+ for (const [reason, count] of Object.entries(coverage.skipped))
190
+ lines.push(` skipped ${count} entries: ${reason}`);
191
+ const { optional, peerOptional, devEntries, devEdges } = coverage.omitted;
192
+ if (optional + peerOptional + devEntries + devEdges > 0)
193
+ lines.push(
194
+ ` omitted ${optional} unlocked optional, ${peerOptional} unlocked optional peer edges; ${devEntries} dev entries, ${devEdges} dev edges (--omit-dev)`,
195
+ );
196
+ lines.push(
197
+ coverage.complete
198
+ ? "Coverage: complete for the checked scope."
199
+ : "Coverage: INCOMPLETE; unchecked, failed or skipped entries are not verified.",
200
+ );
201
+ lines.push(result.limitations[0]);
202
+ return lines.join("\n") + "\n";
203
+ }
204
+
205
+ export async function runAudit(argv, runtime = {}) {
206
+ const options = parseAuditArgs(argv);
207
+ const lock = await readLockfile(options.lockfile);
208
+ const env = runtime.env ?? process.env;
209
+ const check =
210
+ runtime.auditCheck ??
211
+ ((spec) =>
212
+ checkHosted(spec, {
213
+ serverUrl: env.UNMASK_SERVER_URL,
214
+ fetchImpl: runtime.fetchImpl,
215
+ signal: runtime.signal,
216
+ limits: runtime.hostedLimits,
217
+ }));
218
+ const result = await auditLock(lock, { ...options, check });
219
+ process.stdout.write(
220
+ options.json ? JSON.stringify(result, null, 2) + "\n" : formatText(result),
221
+ );
222
+ return result.exitCode;
223
+ }