@xagents_in/unmask 0.1.2 → 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 +301 -85
- package/dist/cli/annotations.mjs +145 -0
- package/dist/cli/audit.mjs +223 -0
- package/dist/cli/batch.mjs +218 -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 +101 -14
- package/dist/cli/preflight.mjs +388 -0
- package/dist/cli/sarif.mjs +100 -0
- package/dist/cli/scan.mjs +128 -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/registry-facts.mjs +342 -0
- package/dist/core/report-exploration.mjs +328 -0
- 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 +66 -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/registry-facts.mjs +118 -0
- package/dist/server/report-exploration.mjs +87 -0
- 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,7 +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";
|
|
26
|
+
import { runScan } from "./scan.mjs";
|
|
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";
|
|
12
40
|
|
|
13
41
|
const names = {
|
|
14
42
|
"--tarball": "tarballPath",
|
|
@@ -33,7 +61,7 @@ function parse(argv) {
|
|
|
33
61
|
if (argv[0] !== "check")
|
|
34
62
|
throw new LocalInputError(
|
|
35
63
|
"usage",
|
|
36
|
-
"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",
|
|
37
65
|
);
|
|
38
66
|
const options = {};
|
|
39
67
|
let start = 1;
|
|
@@ -76,8 +104,6 @@ function parse(argv) {
|
|
|
76
104
|
);
|
|
77
105
|
options[names[key]] = value;
|
|
78
106
|
}
|
|
79
|
-
if (!options.out)
|
|
80
|
-
throw new LocalInputError("usage", "Check requires --out DIR");
|
|
81
107
|
if (options.registrySpec) {
|
|
82
108
|
if (
|
|
83
109
|
options.offline ||
|
|
@@ -103,7 +129,7 @@ function parse(argv) {
|
|
|
103
129
|
if (!options.registrySpec && !options.cacheDir)
|
|
104
130
|
throw new LocalInputError(
|
|
105
131
|
"usage",
|
|
106
|
-
"Offline check requires --cache-dir DIR
|
|
132
|
+
"Offline check requires --cache-dir DIR",
|
|
107
133
|
);
|
|
108
134
|
if (options.claim !== undefined) options.claim = exactClaim(options.claim);
|
|
109
135
|
return options;
|
|
@@ -161,19 +187,56 @@ async function configuredModel(allowModel, runtime) {
|
|
|
161
187
|
}
|
|
162
188
|
|
|
163
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;
|
|
164
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);
|
|
201
|
+
if (argv[0] === "scan") return await runScan(argv.slice(1), runtime);
|
|
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));
|
|
165
223
|
const { out, offline, allowModel, registrySpec, ...options } = parse(argv);
|
|
166
|
-
|
|
224
|
+
progress = createProgress(stderr, { env, subject: registrySpec });
|
|
225
|
+
let report, details;
|
|
167
226
|
if (registrySpec)
|
|
168
|
-
report = await
|
|
169
|
-
serverUrl:
|
|
227
|
+
({ report, details } = await getHostedDetails(registrySpec, {
|
|
228
|
+
serverUrl: env.UNMASK_SERVER_URL,
|
|
170
229
|
fetchImpl: runtime.fetchImpl,
|
|
171
230
|
signal: runtime.signal,
|
|
172
231
|
limits: runtime.hostedLimits,
|
|
173
|
-
|
|
232
|
+
optionalDetails: true,
|
|
233
|
+
onStatus: progress.update,
|
|
234
|
+
}));
|
|
174
235
|
else {
|
|
175
236
|
const model = await configuredModel(allowModel, runtime);
|
|
237
|
+
progress.update("local");
|
|
176
238
|
report = await analyzeLocal({ ...options, limits: runtime.limits });
|
|
239
|
+
if (allowModel) progress.update("model");
|
|
177
240
|
if (allowModel) report = await addConfiguredModel(report, model);
|
|
178
241
|
if (!allowModel)
|
|
179
242
|
report = {
|
|
@@ -181,16 +244,40 @@ export async function main(argv = process.argv.slice(2), runtime = {}) {
|
|
|
181
244
|
model: { status: "not-sent", reason: "consent-required" },
|
|
182
245
|
};
|
|
183
246
|
}
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
);
|
|
186
258
|
return report.policy.exitCode;
|
|
187
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
|
+
);
|
|
188
271
|
if (error instanceof LocalInputError) {
|
|
189
|
-
|
|
272
|
+
stderr.write(renderError(error.code, error.message, errorStyle));
|
|
190
273
|
return 2;
|
|
191
274
|
}
|
|
192
|
-
|
|
193
|
-
|
|
275
|
+
stderr.write(
|
|
276
|
+
renderError(
|
|
277
|
+
"internal",
|
|
278
|
+
"Internal analysis error; no completed result is claimed.",
|
|
279
|
+
errorStyle,
|
|
280
|
+
),
|
|
194
281
|
);
|
|
195
282
|
return 1;
|
|
196
283
|
}
|