@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.
- package/README.md +86 -4
- package/dist/cli/annotations.mjs +145 -0
- package/dist/cli/audit.mjs +223 -0
- package/dist/cli/cache.mjs +488 -0
- package/dist/cli/compare.mjs +261 -0
- package/dist/cli/config.mjs +141 -0
- package/dist/cli/discovery.mjs +372 -0
- package/dist/cli/doctor.mjs +354 -0
- package/dist/cli/exceptions.mjs +170 -0
- package/dist/cli/file.mjs +271 -0
- package/dist/cli/files.mjs +141 -0
- package/dist/cli/history.mjs +232 -0
- package/dist/cli/hosted.mjs +251 -43
- package/dist/cli/info.mjs +125 -0
- package/dist/cli/init.mjs +57 -0
- package/dist/cli/install-guard.mjs +896 -0
- package/dist/cli/lockfile.mjs +330 -0
- package/dist/cli/main.mjs +97 -14
- package/dist/cli/preflight.mjs +388 -0
- package/dist/cli/sarif.mjs +100 -0
- package/dist/cli/shell-integration.mjs +122 -0
- package/dist/cli/terminal.mjs +396 -0
- package/dist/cli/watch.mjs +233 -0
- package/dist/cli/why.mjs +256 -0
- package/dist/core/advisories.mjs +101 -24
- package/dist/core/advisory-cache.mjs +295 -0
- package/dist/core/behavior-signals.mjs +1088 -0
- package/dist/core/dependency-graph.mjs +332 -0
- package/dist/core/evidence-exports.mjs +469 -0
- package/dist/core/guard-config.mjs +463 -0
- package/dist/core/guard-policy.mjs +308 -0
- package/dist/core/package-details.mjs +42 -6
- package/dist/core/report.mjs +7 -0
- package/dist/core/sarif.mjs +556 -0
- package/dist/core/syntax.mjs +296 -15
- package/dist/model/client.mjs +204 -56
- package/dist/model/diff-evidence.mjs +177 -0
- package/dist/model/summary.mjs +183 -0
- package/dist/server/advisory-result.mjs +137 -0
- package/dist/server/app.mjs +60 -2
- package/dist/server/dependency-screening.mjs +386 -0
- package/dist/server/exports.mjs +63 -0
- package/dist/server/guard-decision.mjs +37 -0
- package/dist/server/main.mjs +25 -7
- package/dist/server/queue.mjs +95 -53
- package/dist/server/store.mjs +21 -1
- package/dist/version.json +1 -1
- package/dist/viewer/render.mjs +10 -2
- package/dist/viewer/shell.mjs +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { constants, open } from "node:fs/promises";
|
|
2
|
+
import { LocalInputError } from "../core/pipeline.mjs";
|
|
3
|
+
|
|
4
|
+
export const LOCKFILE_LIMITS = Object.freeze({
|
|
5
|
+
maxBytes: 32 * 1024 * 1024,
|
|
6
|
+
maxEntries: 100000,
|
|
7
|
+
});
|
|
8
|
+
const EXACT =
|
|
9
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
10
|
+
const NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
11
|
+
const EDGE_TYPES = [
|
|
12
|
+
["dependencies", "prod"],
|
|
13
|
+
["optionalDependencies", "optional"],
|
|
14
|
+
["peerDependencies", "peer"],
|
|
15
|
+
["devDependencies", "dev"],
|
|
16
|
+
];
|
|
17
|
+
const invalid = (message) => new LocalInputError("lockfile-invalid", message);
|
|
18
|
+
const isObject = (value) =>
|
|
19
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
|
|
21
|
+
export const isExactVersion = (value) =>
|
|
22
|
+
typeof value === "string" &&
|
|
23
|
+
value.length <= 256 &&
|
|
24
|
+
EXACT.test(value) &&
|
|
25
|
+
!value
|
|
26
|
+
.split("+")[0]
|
|
27
|
+
.split("-")
|
|
28
|
+
.slice(1)
|
|
29
|
+
.join("-")
|
|
30
|
+
.split(".")
|
|
31
|
+
.some((part) => /^0\d+$/.test(part));
|
|
32
|
+
export const isPackageName = (value) =>
|
|
33
|
+
typeof value === "string" && value.length <= 214 && NAME.test(value);
|
|
34
|
+
|
|
35
|
+
export function nameFromKey(key) {
|
|
36
|
+
const index = key.lastIndexOf("node_modules/");
|
|
37
|
+
return index === -1 ? null : key.slice(index + "node_modules/".length);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parentKey(key) {
|
|
41
|
+
const index = key.lastIndexOf("/node_modules/");
|
|
42
|
+
return index === -1 ? "" : key.slice(0, index);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const NPM_REGISTRY_HOST = "registry.npmjs.org";
|
|
46
|
+
export function isNpmRegistryTarball(resolved, name, version) {
|
|
47
|
+
if (typeof resolved !== "string") return false;
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(resolved);
|
|
50
|
+
return (
|
|
51
|
+
url.protocol === "https:" &&
|
|
52
|
+
url.host === NPM_REGISTRY_HOST &&
|
|
53
|
+
!url.username &&
|
|
54
|
+
!url.password &&
|
|
55
|
+
!url.search &&
|
|
56
|
+
!url.hash &&
|
|
57
|
+
url.pathname === `/${name}/-/${name.split("/").pop()}-${version}.tgz`
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function classify(key, raw) {
|
|
65
|
+
if (key === "") return { kind: "root", reason: null };
|
|
66
|
+
if (raw.link === true) return { kind: "link", reason: "workspace-or-link" };
|
|
67
|
+
if (!key.includes("node_modules/"))
|
|
68
|
+
return { kind: "workspace", reason: "workspace-or-link" };
|
|
69
|
+
if (raw.inBundle === true)
|
|
70
|
+
return { kind: "bundled", reason: "bundled-dependency" };
|
|
71
|
+
const name = raw.name ?? nameFromKey(key);
|
|
72
|
+
if (!isPackageName(name)) return { kind: "invalid", reason: "invalid-name" };
|
|
73
|
+
if (typeof raw.version !== "string")
|
|
74
|
+
return { kind: "invalid", reason: "missing-version" };
|
|
75
|
+
if (/^(?:git\+|git:|github:|https?:|file:)/.test(raw.version))
|
|
76
|
+
return { kind: "url", reason: "non-registry-source" };
|
|
77
|
+
if (!isExactVersion(raw.version))
|
|
78
|
+
return { kind: "invalid", reason: "inexact-version" };
|
|
79
|
+
if (
|
|
80
|
+
raw.resolved !== undefined &&
|
|
81
|
+
!isNpmRegistryTarball(raw.resolved, name, raw.version)
|
|
82
|
+
)
|
|
83
|
+
return { kind: "url", reason: "non-registry-source" };
|
|
84
|
+
return { kind: "registry", reason: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function edges(raw) {
|
|
88
|
+
const result = [];
|
|
89
|
+
const peerMeta = isObject(raw.peerDependenciesMeta)
|
|
90
|
+
? raw.peerDependenciesMeta
|
|
91
|
+
: {};
|
|
92
|
+
for (const [field, type] of EDGE_TYPES) {
|
|
93
|
+
const value = raw[field];
|
|
94
|
+
if (value === undefined) continue;
|
|
95
|
+
if (!isObject(value)) throw invalid(`Lockfile ${field} must be an object.`);
|
|
96
|
+
for (const [name, range] of Object.entries(value)) {
|
|
97
|
+
// Legacy or unusual names stay as edges; only their own entries are classified invalid.
|
|
98
|
+
if (
|
|
99
|
+
typeof range !== "string" ||
|
|
100
|
+
!name ||
|
|
101
|
+
name.length > 214 ||
|
|
102
|
+
/[\0-\x1f\x7f\\]/.test(name)
|
|
103
|
+
)
|
|
104
|
+
throw invalid(`Lockfile ${field} contains an invalid entry.`);
|
|
105
|
+
result.push({
|
|
106
|
+
name,
|
|
107
|
+
range,
|
|
108
|
+
type:
|
|
109
|
+
type === "peer" && peerMeta[name]?.optional === true
|
|
110
|
+
? "peer-optional"
|
|
111
|
+
: type,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function parseLockfile(text, limits = LOCKFILE_LIMITS) {
|
|
119
|
+
if (typeof text !== "string" || Buffer.byteLength(text) > limits.maxBytes)
|
|
120
|
+
throw invalid("Lockfile is missing or exceeds the size limit.");
|
|
121
|
+
let data;
|
|
122
|
+
try {
|
|
123
|
+
data = JSON.parse(text);
|
|
124
|
+
} catch {
|
|
125
|
+
throw invalid("Lockfile is not valid JSON.");
|
|
126
|
+
}
|
|
127
|
+
if (!isObject(data)) throw invalid("Lockfile must be a JSON object.");
|
|
128
|
+
if (![2, 3].includes(data.lockfileVersion))
|
|
129
|
+
throw new LocalInputError(
|
|
130
|
+
"lockfile-unsupported",
|
|
131
|
+
"Only npm lockfileVersion 2 or 3 (with a packages map) is supported.",
|
|
132
|
+
);
|
|
133
|
+
// npm writes an empty packages map for a project with no dependencies.
|
|
134
|
+
if (isObject(data.packages) && Object.keys(data.packages).length === 0)
|
|
135
|
+
data.packages = { "": {} };
|
|
136
|
+
if (!isObject(data.packages) || !isObject(data.packages[""]))
|
|
137
|
+
throw invalid("Lockfile packages map with a root entry is required.");
|
|
138
|
+
const keys = Object.keys(data.packages);
|
|
139
|
+
if (keys.length > limits.maxEntries)
|
|
140
|
+
throw invalid("Lockfile exceeds the entry limit.");
|
|
141
|
+
const entries = new Map();
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
const value = data.packages[key];
|
|
144
|
+
const badKey =
|
|
145
|
+
key.length > 4096 ||
|
|
146
|
+
(key !== "" &&
|
|
147
|
+
key
|
|
148
|
+
.split("/")
|
|
149
|
+
.some(
|
|
150
|
+
(part) => ["", ".", ".."].includes(part) || part.includes("\\"),
|
|
151
|
+
));
|
|
152
|
+
// Malformed entries are kept as invalid so the rest of the graph still parses.
|
|
153
|
+
const raw = isObject(value) && !badKey ? value : {};
|
|
154
|
+
const { kind, reason } = badKey
|
|
155
|
+
? { kind: "invalid", reason: "invalid-location" }
|
|
156
|
+
: !isObject(value)
|
|
157
|
+
? { kind: "invalid", reason: "invalid-entry" }
|
|
158
|
+
: classify(key, raw);
|
|
159
|
+
let entryEdges = [];
|
|
160
|
+
let edgesValid = true;
|
|
161
|
+
try {
|
|
162
|
+
entryEdges = kind === "link" ? [] : edges(raw);
|
|
163
|
+
} catch {
|
|
164
|
+
edgesValid = false;
|
|
165
|
+
}
|
|
166
|
+
entries.set(key, {
|
|
167
|
+
key,
|
|
168
|
+
name:
|
|
169
|
+
key === ""
|
|
170
|
+
? (raw.name ?? data.name ?? null)
|
|
171
|
+
: (raw.name ?? nameFromKey(key) ?? key),
|
|
172
|
+
version: typeof raw.version === "string" ? raw.version : null,
|
|
173
|
+
kind,
|
|
174
|
+
reason,
|
|
175
|
+
dev: raw.dev === true,
|
|
176
|
+
optional: raw.optional === true || raw.devOptional === true,
|
|
177
|
+
bundled: raw.inBundle === true,
|
|
178
|
+
resolved: typeof raw.resolved === "string" ? raw.resolved : null,
|
|
179
|
+
integrity: typeof raw.integrity === "string" ? raw.integrity : null,
|
|
180
|
+
bound:
|
|
181
|
+
kind === "registry" &&
|
|
182
|
+
isNpmRegistryTarball(
|
|
183
|
+
raw.resolved,
|
|
184
|
+
raw.name ?? nameFromKey(key),
|
|
185
|
+
raw.version,
|
|
186
|
+
) &&
|
|
187
|
+
/^sha512-[A-Za-z0-9+/]{86}==$/.test(raw.integrity ?? ""),
|
|
188
|
+
edges: entryEdges,
|
|
189
|
+
edgesValid,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// npm omits name on workspace entries; the linking node_modules key carries it.
|
|
193
|
+
for (const entry of entries.values()) {
|
|
194
|
+
const target = entry.kind === "link" && entries.get(entry.resolved ?? "");
|
|
195
|
+
if (target?.kind === "workspace" && target.name === target.key)
|
|
196
|
+
target.name = nameFromKey(entry.key);
|
|
197
|
+
}
|
|
198
|
+
return { lockfileVersion: data.lockfileVersion, entries };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function readLockfile(filename, limits = LOCKFILE_LIMITS) {
|
|
202
|
+
const unreadable = () =>
|
|
203
|
+
new LocalInputError(
|
|
204
|
+
"lockfile-unreadable",
|
|
205
|
+
"The lockfile must be a readable regular file within the size limit; pass --lockfile PATH to package-lock.json.",
|
|
206
|
+
);
|
|
207
|
+
let handle;
|
|
208
|
+
try {
|
|
209
|
+
// Nonblocking open plus fstat rejects FIFOs/devices before any read.
|
|
210
|
+
handle = await open(filename, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
211
|
+
const info = await handle.stat();
|
|
212
|
+
if (!info.isFile() || info.size > limits.maxBytes) throw unreadable();
|
|
213
|
+
const buffer = Buffer.alloc(info.size + 1);
|
|
214
|
+
let length = 0;
|
|
215
|
+
while (length < buffer.length) {
|
|
216
|
+
const { bytesRead } = await handle.read(
|
|
217
|
+
buffer,
|
|
218
|
+
length,
|
|
219
|
+
buffer.length - length,
|
|
220
|
+
length,
|
|
221
|
+
);
|
|
222
|
+
if (bytesRead === 0) break;
|
|
223
|
+
length += bytesRead;
|
|
224
|
+
}
|
|
225
|
+
if (length !== info.size) throw unreadable();
|
|
226
|
+
return parseLockfile(buffer.subarray(0, length).toString("utf8"), limits);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof LocalInputError) throw error;
|
|
229
|
+
throw unreadable();
|
|
230
|
+
} finally {
|
|
231
|
+
await handle?.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function resolveEdge(lock, fromKey, name) {
|
|
236
|
+
let base = fromKey;
|
|
237
|
+
for (let step = 0; step < 4096; step++) {
|
|
238
|
+
const candidate = (base ? base + "/" : "") + "node_modules/" + name;
|
|
239
|
+
const entry = lock.entries.get(candidate);
|
|
240
|
+
if (entry) {
|
|
241
|
+
if (entry.kind !== "link") return entry;
|
|
242
|
+
return lock.entries.get(entry.resolved ?? "") ?? null;
|
|
243
|
+
}
|
|
244
|
+
if (base === "") return null;
|
|
245
|
+
base = parentKey(base);
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function scanEdges(lock, omitDev) {
|
|
251
|
+
const missing = [];
|
|
252
|
+
const omitted = { optional: 0, peerOptional: 0, devEdges: 0 };
|
|
253
|
+
for (const entry of lock.entries.values()) {
|
|
254
|
+
if (entry.kind === "link" || (omitDev && entry.dev)) continue;
|
|
255
|
+
const project = ["root", "workspace"].includes(entry.kind);
|
|
256
|
+
for (const edge of entry.edges) {
|
|
257
|
+
if (edge.type === "dev" && !project) continue;
|
|
258
|
+
const found = resolveEdge(lock, entry.key, edge.name);
|
|
259
|
+
if (edge.type === "dev" && omitDev) omitted.devEdges++;
|
|
260
|
+
else if (found) continue;
|
|
261
|
+
else if (edge.type === "optional") omitted.optional++;
|
|
262
|
+
else if (edge.type === "peer-optional") omitted.peerOptional++;
|
|
263
|
+
else missing.push({ from: entry.key, name: edge.name, type: edge.type });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { missing, omitted };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function unresolvedEdges(lock, { omitDev = false } = {}) {
|
|
270
|
+
return scanEdges(lock, omitDev).missing;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function exactTargets(lock, { omitDev = false } = {}) {
|
|
274
|
+
const unique = new Map();
|
|
275
|
+
const skipped = {};
|
|
276
|
+
let considered = 0;
|
|
277
|
+
const { missing: unresolved, omitted } = scanEdges(lock, omitDev);
|
|
278
|
+
omitted.devEntries = 0;
|
|
279
|
+
if (unresolved.length) skipped["unresolved-dependency"] = unresolved.length;
|
|
280
|
+
const badEdges = [...lock.entries.values()].filter(
|
|
281
|
+
(entry) => !entry.edgesValid && !(omitDev && entry.dev),
|
|
282
|
+
).length;
|
|
283
|
+
if (badEdges) skipped["invalid-dependencies"] = badEdges;
|
|
284
|
+
for (const entry of lock.entries.values()) {
|
|
285
|
+
if (omitDev && entry.dev) {
|
|
286
|
+
if (!["root", "workspace", "link"].includes(entry.kind))
|
|
287
|
+
omitted.devEntries++;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// A link whose target entry is absent means unlocked local code.
|
|
291
|
+
if (entry.kind === "link" && !lock.entries.has(entry.resolved ?? "")) {
|
|
292
|
+
skipped["dangling-link"] = (skipped["dangling-link"] ?? 0) + 1;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (["root", "workspace", "link"].includes(entry.kind)) continue;
|
|
296
|
+
considered++;
|
|
297
|
+
if (entry.kind !== "registry") {
|
|
298
|
+
skipped[entry.reason] = (skipped[entry.reason] ?? 0) + 1;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const spec = `${entry.name}@${entry.version}`;
|
|
302
|
+
const target = unique.get(spec) ?? {
|
|
303
|
+
spec,
|
|
304
|
+
name: entry.name,
|
|
305
|
+
version: entry.version,
|
|
306
|
+
locations: [],
|
|
307
|
+
bound: true,
|
|
308
|
+
integrity: [],
|
|
309
|
+
aliases: [],
|
|
310
|
+
};
|
|
311
|
+
target.locations.push(entry.key);
|
|
312
|
+
const alias = nameFromKey(entry.key);
|
|
313
|
+
if (alias !== entry.name && !target.aliases.includes(alias))
|
|
314
|
+
target.aliases.push(alias);
|
|
315
|
+
target.bound &&= entry.bound;
|
|
316
|
+
if (entry.integrity && !target.integrity.includes(entry.integrity))
|
|
317
|
+
target.integrity.push(entry.integrity);
|
|
318
|
+
if (target.integrity.length > 1) target.bound = false;
|
|
319
|
+
unique.set(spec, target);
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
considered,
|
|
323
|
+
targets: [...unique.values()].sort((a, b) =>
|
|
324
|
+
a.spec < b.spec ? -1 : a.spec > b.spec ? 1 : 0,
|
|
325
|
+
),
|
|
326
|
+
skipped,
|
|
327
|
+
omitted,
|
|
328
|
+
unresolved: unresolved.slice(0, 100),
|
|
329
|
+
};
|
|
330
|
+
}
|
package/dist/cli/main.mjs
CHANGED
|
@@ -8,9 +8,35 @@ import { renderReport } from "../viewer/render.mjs";
|
|
|
8
8
|
import { parsePackageSpec, AcquisitionError } from "../core/acquire.mjs";
|
|
9
9
|
import { addConfiguredModel } from "../core/online.mjs";
|
|
10
10
|
import { modelClientFromEnv } from "../model/config.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import { getHostedDetails } from "./hosted.mjs";
|
|
12
|
+
import { infoCommand } from "./info.mjs";
|
|
13
|
+
import { filesCommand } from "./files.mjs";
|
|
14
|
+
import { fileCommand } from "./file.mjs";
|
|
15
|
+
import {
|
|
16
|
+
createProgress,
|
|
17
|
+
renderAdvisoryOnly,
|
|
18
|
+
renderCheckSummary,
|
|
19
|
+
renderError,
|
|
20
|
+
terminalStyle,
|
|
21
|
+
} from "./terminal.mjs";
|
|
22
|
+
import { discoveryCommand, isDiscoveryRequest } from "./discovery.mjs";
|
|
23
|
+
import { runConfig } from "./config.mjs";
|
|
24
|
+
import { runExceptions } from "./exceptions.mjs";
|
|
25
|
+
import { runInit } from "./init.mjs";
|
|
12
26
|
import { runScan } from "./scan.mjs";
|
|
13
27
|
import { runBatch } from "./batch.mjs";
|
|
28
|
+
import { runInstallGuard } from "./install-guard.mjs";
|
|
29
|
+
import { runShellIntegration } from "./shell-integration.mjs";
|
|
30
|
+
import { runAudit } from "./audit.mjs";
|
|
31
|
+
import { runWhy } from "./why.mjs";
|
|
32
|
+
import { preflightMain } from "./preflight.mjs";
|
|
33
|
+
import { watchMain } from "./watch.mjs";
|
|
34
|
+
import { runCompare } from "./compare.mjs";
|
|
35
|
+
import { runHistory } from "./history.mjs";
|
|
36
|
+
import { doctorCommand } from "./doctor.mjs";
|
|
37
|
+
import { cacheCommand } from "./cache.mjs";
|
|
38
|
+
import { sarifCommand } from "./sarif.mjs";
|
|
39
|
+
import { annotationsCommand } from "./annotations.mjs";
|
|
14
40
|
|
|
15
41
|
const names = {
|
|
16
42
|
"--tarball": "tarballPath",
|
|
@@ -35,7 +61,7 @@ function parse(argv) {
|
|
|
35
61
|
if (argv[0] !== "check")
|
|
36
62
|
throw new LocalInputError(
|
|
37
63
|
"usage",
|
|
38
|
-
"Use check name@version or an explicit local tarball with --offline",
|
|
64
|
+
"Use check name@version, audit, why name, or an explicit local tarball with --offline",
|
|
39
65
|
);
|
|
40
66
|
const options = {};
|
|
41
67
|
let start = 1;
|
|
@@ -78,8 +104,6 @@ function parse(argv) {
|
|
|
78
104
|
);
|
|
79
105
|
options[names[key]] = value;
|
|
80
106
|
}
|
|
81
|
-
if (!options.out)
|
|
82
|
-
throw new LocalInputError("usage", "Check requires --out DIR");
|
|
83
107
|
if (options.registrySpec) {
|
|
84
108
|
if (
|
|
85
109
|
options.offline ||
|
|
@@ -105,7 +129,7 @@ function parse(argv) {
|
|
|
105
129
|
if (!options.registrySpec && !options.cacheDir)
|
|
106
130
|
throw new LocalInputError(
|
|
107
131
|
"usage",
|
|
108
|
-
"Offline check requires --cache-dir DIR
|
|
132
|
+
"Offline check requires --cache-dir DIR",
|
|
109
133
|
);
|
|
110
134
|
if (options.claim !== undefined) options.claim = exactClaim(options.claim);
|
|
111
135
|
return options;
|
|
@@ -163,21 +187,56 @@ async function configuredModel(allowModel, runtime) {
|
|
|
163
187
|
}
|
|
164
188
|
|
|
165
189
|
export async function main(argv = process.argv.slice(2), runtime = {}) {
|
|
190
|
+
const stdout = runtime.stdout ?? process.stdout;
|
|
191
|
+
const stderr = runtime.stderr ?? process.stderr;
|
|
192
|
+
const env = runtime.env ?? process.env;
|
|
193
|
+
const errorStyle = terminalStyle(stderr, env);
|
|
194
|
+
let progress;
|
|
166
195
|
try {
|
|
196
|
+
if (isDiscoveryRequest(argv)) return discoveryCommand(argv, runtime);
|
|
197
|
+
if (argv[0] === "install" || argv[0] === "ci")
|
|
198
|
+
return await runInstallGuard(argv[0], argv.slice(1), runtime);
|
|
199
|
+
if (argv[0] === "enable" || argv[0] === "disable")
|
|
200
|
+
return await runShellIntegration(argv[0], argv.slice(1), runtime);
|
|
167
201
|
if (argv[0] === "scan") return await runScan(argv.slice(1), runtime);
|
|
168
202
|
if (argv[0] === "batch") return await runBatch(argv.slice(1), runtime);
|
|
203
|
+
if (argv[0] === "config") return await runConfig(argv.slice(1), runtime);
|
|
204
|
+
if (argv[0] === "exceptions")
|
|
205
|
+
return await runExceptions(argv.slice(1), runtime);
|
|
206
|
+
if (argv[0] === "init") return await runInit(argv.slice(1), runtime);
|
|
207
|
+
if (argv[0] === "file") return await fileCommand(argv.slice(1), runtime);
|
|
208
|
+
if (argv[0] === "files") return await filesCommand(argv.slice(1), runtime);
|
|
209
|
+
if (argv[0] === "info") return await infoCommand(argv.slice(1), runtime);
|
|
210
|
+
if (argv[0] === "audit") return await runAudit(argv.slice(1), runtime);
|
|
211
|
+
if (argv[0] === "preflight")
|
|
212
|
+
return await preflightMain(argv.slice(1), runtime);
|
|
213
|
+
if (argv[0] === "watch") return await watchMain(argv.slice(1), runtime);
|
|
214
|
+
if (argv[0] === "why") return await runWhy(argv.slice(1));
|
|
215
|
+
if (argv[0] === "compare") return await runCompare(argv.slice(1), runtime);
|
|
216
|
+
if (argv[0] === "history") return await runHistory(argv.slice(1), runtime);
|
|
217
|
+
if (argv[0] === "doctor")
|
|
218
|
+
return await doctorCommand(argv.slice(1), runtime);
|
|
219
|
+
if (argv[0] === "cache") return await cacheCommand(argv.slice(1), runtime);
|
|
220
|
+
if (argv[0] === "sarif") return await sarifCommand(argv.slice(1));
|
|
221
|
+
if (argv[0] === "annotations")
|
|
222
|
+
return await annotationsCommand(argv.slice(1));
|
|
169
223
|
const { out, offline, allowModel, registrySpec, ...options } = parse(argv);
|
|
170
|
-
|
|
224
|
+
progress = createProgress(stderr, { env, subject: registrySpec });
|
|
225
|
+
let report, details;
|
|
171
226
|
if (registrySpec)
|
|
172
|
-
report = await
|
|
173
|
-
serverUrl:
|
|
227
|
+
({ report, details } = await getHostedDetails(registrySpec, {
|
|
228
|
+
serverUrl: env.UNMASK_SERVER_URL,
|
|
174
229
|
fetchImpl: runtime.fetchImpl,
|
|
175
230
|
signal: runtime.signal,
|
|
176
231
|
limits: runtime.hostedLimits,
|
|
177
|
-
|
|
232
|
+
optionalDetails: true,
|
|
233
|
+
onStatus: progress.update,
|
|
234
|
+
}));
|
|
178
235
|
else {
|
|
179
236
|
const model = await configuredModel(allowModel, runtime);
|
|
237
|
+
progress.update("local");
|
|
180
238
|
report = await analyzeLocal({ ...options, limits: runtime.limits });
|
|
239
|
+
if (allowModel) progress.update("model");
|
|
181
240
|
if (allowModel) report = await addConfiguredModel(report, model);
|
|
182
241
|
if (!allowModel)
|
|
183
242
|
report = {
|
|
@@ -185,16 +244,40 @@ export async function main(argv = process.argv.slice(2), runtime = {}) {
|
|
|
185
244
|
model: { status: "not-sent", reason: "consent-required" },
|
|
186
245
|
};
|
|
187
246
|
}
|
|
188
|
-
|
|
189
|
-
|
|
247
|
+
const outDir = out === undefined ? undefined : path.resolve(out);
|
|
248
|
+
if (outDir) progress.update("writing");
|
|
249
|
+
if (outDir) await writeReports(outDir, report);
|
|
250
|
+
progress.stop();
|
|
251
|
+
stdout.write(
|
|
252
|
+
renderCheckSummary(report, {
|
|
253
|
+
details,
|
|
254
|
+
outDir,
|
|
255
|
+
style: terminalStyle(stdout, env),
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
190
258
|
return report.policy.exitCode;
|
|
191
259
|
} catch (error) {
|
|
260
|
+
progress?.stop();
|
|
261
|
+
if (error instanceof LocalInputError && error.advisoryResult)
|
|
262
|
+
stdout.write(
|
|
263
|
+
renderAdvisoryOnly(
|
|
264
|
+
error.advisoryResult,
|
|
265
|
+
`${error.advisoryResult.package.name}@${error.advisoryResult.package.version}`,
|
|
266
|
+
{
|
|
267
|
+
style: terminalStyle(stdout, env),
|
|
268
|
+
},
|
|
269
|
+
),
|
|
270
|
+
);
|
|
192
271
|
if (error instanceof LocalInputError) {
|
|
193
|
-
|
|
272
|
+
stderr.write(renderError(error.code, error.message, errorStyle));
|
|
194
273
|
return 2;
|
|
195
274
|
}
|
|
196
|
-
|
|
197
|
-
|
|
275
|
+
stderr.write(
|
|
276
|
+
renderError(
|
|
277
|
+
"internal",
|
|
278
|
+
"Internal analysis error; no completed result is claimed.",
|
|
279
|
+
errorStyle,
|
|
280
|
+
),
|
|
198
281
|
);
|
|
199
282
|
return 1;
|
|
200
283
|
}
|