@westbayberry/dg 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -226
- package/dist/agents/claude-code.js +113 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +113 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +285 -0
- package/dist/agents/registry.js +127 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +1 -1
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit-ui/AuditApp.js +2 -0
- package/dist/audit-ui/components/AuditResultsView.js +55 -92
- package/dist/audit-ui/export.js +0 -4
- package/dist/audit-ui/format.js +0 -3
- package/dist/audit-ui/launch.js +10 -1
- package/dist/auth/device-login.js +4 -5
- package/dist/auth/login-app.js +7 -7
- package/dist/auth/store.js +8 -3
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +231 -0
- package/dist/commands/audit.js +22 -3
- package/dist/commands/config.js +15 -7
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +23 -9
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/licenses.js +10 -22
- package/dist/commands/logout.js +4 -11
- package/dist/commands/router.js +6 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +197 -27
- package/dist/commands/types.js +1 -0
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +14 -44
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +1 -1
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/launcher/agent-check.js +466 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +31 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +17 -6
- package/dist/launcher/env.js +71 -24
- package/dist/launcher/install-preflight.js +51 -14
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +14 -1
- package/dist/launcher/run.js +89 -86
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +18 -5
- package/dist/policy/evaluate.js +5 -6
- package/dist/policy/pypi-name.js +17 -0
- package/dist/presentation/mode.js +3 -2
- package/dist/presentation/package-page.js +9 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +145 -6
- package/dist/proxy/auth.js +42 -0
- package/dist/proxy/ca.js +29 -9
- package/dist/proxy/cooldown-exemptions-file.js +33 -0
- package/dist/proxy/enforcement.js +55 -16
- package/dist/proxy/metadata-map.js +41 -3
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +448 -49
- package/dist/proxy/worker.js +16 -1
- package/dist/publish-set/collect.js +1 -4
- package/dist/publish-set/npm.js +8 -5
- package/dist/publish-set/pack.js +9 -3
- package/dist/runtime/cli.js +0 -4
- package/dist/runtime/fatal.js +31 -0
- package/dist/runtime/first-run.js +12 -11
- package/dist/runtime/node-version.js +43 -6
- package/dist/runtime/nudges.js +35 -2
- package/dist/sbom/cyclonedx.js +211 -0
- package/dist/sbom-ui/SbomApp.js +158 -0
- package/dist/sbom-ui/components/SbomHeader.js +32 -0
- package/dist/sbom-ui/components/SbomList.js +52 -0
- package/dist/sbom-ui/inventory.js +128 -0
- package/dist/sbom-ui/launch.js +51 -0
- package/dist/sbom-ui/run.js +55 -0
- package/dist/sbom-ui/store.js +26 -0
- package/dist/scan/collect.js +10 -6
- package/dist/scan/command.js +17 -10
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +28 -4
- package/dist/scan/scanner-report.js +15 -9
- package/dist/scan/staged.js +2 -1
- package/dist/scan-ui/LegacyApp.js +9 -13
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
- package/dist/scan-ui/components/ProgressBar.js +3 -14
- package/dist/scan-ui/components/ProjectSelector.js +1 -1
- package/dist/scan-ui/components/ScoreHeader.js +2 -3
- package/dist/scan-ui/components/SetupBanner.js +0 -6
- package/dist/scan-ui/format-helpers.js +61 -5
- package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
- package/dist/scan-ui/hooks/useScan.js +16 -2
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +11 -4
- package/dist/scripts/detect.js +7 -2
- package/dist/scripts/gate.js +1 -1
- package/dist/service/state.js +27 -8
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/worker.js +23 -1
- package/dist/setup/activate-shell.js +28 -0
- package/dist/setup/git-hook.js +49 -4
- package/dist/setup/plan.js +73 -39
- package/dist/setup-ui/gate.js +39 -0
- package/dist/setup-ui/offer.js +42 -0
- package/dist/setup-ui/selector.js +27 -0
- package/dist/setup-ui/tasks.js +56 -0
- package/dist/setup-ui/wizard.js +225 -0
- package/dist/state/cooldown-held.js +66 -0
- package/dist/state/index.js +1 -0
- package/dist/state/locks.js +4 -2
- package/dist/state/store.js +2 -1
- package/dist/util/external-tool.js +25 -0
- package/dist/util/git.js +10 -3
- package/dist/util/report-writer.js +57 -0
- package/dist/verify/local.js +240 -42
- package/dist/verify/package-check.js +84 -28
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +6 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -117
- package/dist/commands/explain.js +0 -236
- package/dist/commands/unavailable.js +0 -11
- package/dist/scripts/rebuild.js +0 -28
- package/dist/telemetry/events.js +0 -40
package/dist/verify/local.js
CHANGED
|
@@ -8,6 +8,10 @@ const MAX_UNPACKED_BYTES = 250 * 1024 * 1024;
|
|
|
8
8
|
const MAX_ARCHIVE_ENTRIES = 20000;
|
|
9
9
|
const MAX_ARCHIVE_PATH_LENGTH = 240;
|
|
10
10
|
const TAR_BLOCK_SIZE = 512;
|
|
11
|
+
const GNU_LONGNAME_TYPE = 0x4c;
|
|
12
|
+
const GNU_LONGLINK_TYPE = 0x4b;
|
|
13
|
+
const PAX_EXTENDED_TYPE = 0x78;
|
|
14
|
+
const PAX_GLOBAL_TYPE = 0x67;
|
|
11
15
|
export function verifyLocalTarget(targetPath, cwd = process.cwd()) {
|
|
12
16
|
const absoluteTarget = resolve(cwd, targetPath);
|
|
13
17
|
if (!existsSync(absoluteTarget)) {
|
|
@@ -115,23 +119,73 @@ function scanTarball(bytes, path) {
|
|
|
115
119
|
const entries = [];
|
|
116
120
|
let offset = 0;
|
|
117
121
|
let unpackedSizeBytes = 0;
|
|
122
|
+
let pendingLongName = null;
|
|
123
|
+
let pendingPax = null;
|
|
124
|
+
let globalPax = null;
|
|
118
125
|
while (offset + TAR_BLOCK_SIZE <= tarBytes.length) {
|
|
119
126
|
const header = tarBytes.subarray(offset, offset + TAR_BLOCK_SIZE);
|
|
120
127
|
if (header.every((byte) => byte === 0)) {
|
|
121
128
|
break;
|
|
122
129
|
}
|
|
123
|
-
const
|
|
130
|
+
const rawName = tarEntryName(header);
|
|
124
131
|
const size = tarEntrySize(header);
|
|
132
|
+
if (size === null) {
|
|
133
|
+
errors.push("tar archive has an unparseable entry size");
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
125
136
|
const bodyOffset = offset + TAR_BLOCK_SIZE;
|
|
126
137
|
const nextOffset = bodyOffset + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
|
|
127
|
-
if (
|
|
138
|
+
if (rawName.length === 0 || nextOffset > tarBytes.length) {
|
|
128
139
|
errors.push("tar archive has a malformed entry header");
|
|
129
140
|
break;
|
|
130
141
|
}
|
|
142
|
+
const body = tarBytes.subarray(bodyOffset, bodyOffset + size);
|
|
143
|
+
const typeflag = header.readUInt8(156);
|
|
144
|
+
if (typeflag === GNU_LONGNAME_TYPE) {
|
|
145
|
+
pendingLongName = readNullTerminated(body, 0, body.length);
|
|
146
|
+
if (pendingLongName.length === 0) {
|
|
147
|
+
errors.push("tar archive has an empty long-name header");
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
offset = nextOffset;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (typeflag === GNU_LONGLINK_TYPE) {
|
|
154
|
+
offset = nextOffset;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (typeflag === PAX_EXTENDED_TYPE || typeflag === PAX_GLOBAL_TYPE) {
|
|
158
|
+
const records = parsePaxRecords(body);
|
|
159
|
+
if (records === null) {
|
|
160
|
+
errors.push("tar archive has an unparseable PAX extended header");
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (typeflag === PAX_EXTENDED_TYPE) {
|
|
164
|
+
pendingPax = mergePaxRecords(pendingPax, records);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
globalPax = mergePaxRecords(globalPax, records);
|
|
168
|
+
}
|
|
169
|
+
offset = nextOffset;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const paxPath = pendingPax?.get("path") ?? globalPax?.get("path") ?? null;
|
|
173
|
+
if (paxPath !== null && pendingLongName !== null && paxPath !== pendingLongName) {
|
|
174
|
+
errors.push("tar entry name metadata is contradictory");
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
const paxSize = pendingPax?.get("size") ?? null;
|
|
178
|
+
if (paxSize !== null && Number.parseInt(paxSize, 10) !== size) {
|
|
179
|
+
errors.push("tar entry size metadata is contradictory");
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
const name = paxPath ?? pendingLongName ?? rawName;
|
|
183
|
+
pendingLongName = null;
|
|
184
|
+
pendingPax = null;
|
|
131
185
|
unpackedSizeBytes += size;
|
|
132
186
|
entries.push({
|
|
133
187
|
name,
|
|
134
|
-
body
|
|
188
|
+
body
|
|
135
189
|
});
|
|
136
190
|
findings.push(...pathSafetyFindings(name));
|
|
137
191
|
if (entries.length > MAX_ARCHIVE_ENTRIES) {
|
|
@@ -144,6 +198,9 @@ function scanTarball(bytes, path) {
|
|
|
144
198
|
}
|
|
145
199
|
offset = nextOffset;
|
|
146
200
|
}
|
|
201
|
+
if (errors.length === 0 && (pendingLongName !== null || pendingPax !== null)) {
|
|
202
|
+
errors.push("tar archive ends with an unapplied extended header");
|
|
203
|
+
}
|
|
147
204
|
findings.push(...packageManifestFindings(entries));
|
|
148
205
|
return {
|
|
149
206
|
findings,
|
|
@@ -155,56 +212,41 @@ function scanTarball(bytes, path) {
|
|
|
155
212
|
}
|
|
156
213
|
};
|
|
157
214
|
}
|
|
215
|
+
const ZIP_LOCAL_HEADER_SIG = 0x04034b50;
|
|
216
|
+
const ZIP_CENTRAL_HEADER_SIG = 0x02014b50;
|
|
217
|
+
const ZIP_EOCD_SIG = 0x06054b50;
|
|
218
|
+
const ZIP_DATA_DESCRIPTOR_SIG = 0x08074b50;
|
|
219
|
+
const ZIP_EOCD_MIN_BYTES = 22;
|
|
220
|
+
const ZIP_EOCD_SEARCH_BYTES = ZIP_EOCD_MIN_BYTES + 0xffff;
|
|
158
221
|
function scanZipLike(bytes, path) {
|
|
222
|
+
const central = readZipCentralDirectory(bytes);
|
|
223
|
+
if ("error" in central) {
|
|
224
|
+
return archiveError(`${basename(path)}: ${central.error}`);
|
|
225
|
+
}
|
|
226
|
+
const mismatch = zipLocalHeaderMismatch(bytes, central);
|
|
227
|
+
if (mismatch) {
|
|
228
|
+
return archiveError(`${basename(path)}: ${mismatch}`);
|
|
229
|
+
}
|
|
159
230
|
const findings = [];
|
|
160
231
|
const errors = [];
|
|
161
232
|
const entries = [];
|
|
162
|
-
let offset = 0;
|
|
163
233
|
let unpackedSizeBytes = 0;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
const flags = bytes.readUInt16LE(offset + 6);
|
|
170
|
-
const method = bytes.readUInt16LE(offset + 8);
|
|
171
|
-
const compressedSize = bytes.readUInt32LE(offset + 18);
|
|
172
|
-
const uncompressedSize = bytes.readUInt32LE(offset + 22);
|
|
173
|
-
const fileNameLength = bytes.readUInt16LE(offset + 26);
|
|
174
|
-
const extraLength = bytes.readUInt16LE(offset + 28);
|
|
175
|
-
const nameStart = offset + 30;
|
|
176
|
-
const dataStart = nameStart + fileNameLength + extraLength;
|
|
177
|
-
const dataEnd = dataStart + compressedSize;
|
|
178
|
-
if (dataEnd > bytes.length) {
|
|
179
|
-
errors.push(`${basename(path)} has a malformed zip entry`);
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
const name = bytes.subarray(nameStart, nameStart + fileNameLength).toString("utf8");
|
|
183
|
-
findings.push(...pathSafetyFindings(name));
|
|
184
|
-
if ((flags & 0x1) !== 0) {
|
|
234
|
+
for (const entry of central.entries) {
|
|
235
|
+
findings.push(...pathSafetyFindings(entry.name));
|
|
236
|
+
if ((entry.flags & 0x1) !== 0) {
|
|
185
237
|
findings.push({
|
|
186
238
|
id: "encrypted-archive-entry",
|
|
187
239
|
severity: "block",
|
|
188
240
|
title: "Encrypted archive entry",
|
|
189
241
|
message: "encrypted archive entries cannot be inspected locally",
|
|
190
|
-
location: name
|
|
242
|
+
location: entry.name
|
|
191
243
|
});
|
|
192
244
|
}
|
|
193
|
-
|
|
194
|
-
unpackedSizeBytes += uncompressedSize;
|
|
245
|
+
unpackedSizeBytes += entry.uncompressedSize;
|
|
195
246
|
entries.push({
|
|
196
|
-
name,
|
|
197
|
-
body
|
|
247
|
+
name: entry.name,
|
|
248
|
+
body: readZipEntryBody(bytes, entry, errors)
|
|
198
249
|
});
|
|
199
|
-
if ((flags & 0x8) !== 0) {
|
|
200
|
-
findings.push({
|
|
201
|
-
id: "zip-data-descriptor",
|
|
202
|
-
severity: "warn",
|
|
203
|
-
title: "Zip data descriptor",
|
|
204
|
-
message: "zip entry uses a data descriptor, so byte-size validation is conservative",
|
|
205
|
-
location: name
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
250
|
if (entries.length > MAX_ARCHIVE_ENTRIES) {
|
|
209
251
|
findings.push(limitFinding("archive has too many entries"));
|
|
210
252
|
break;
|
|
@@ -213,7 +255,6 @@ function scanZipLike(bytes, path) {
|
|
|
213
255
|
findings.push(limitFinding("archive expands beyond the local verification limit"));
|
|
214
256
|
break;
|
|
215
257
|
}
|
|
216
|
-
offset = dataEnd;
|
|
217
258
|
}
|
|
218
259
|
findings.push(...packageManifestFindings(entries));
|
|
219
260
|
return {
|
|
@@ -226,6 +267,121 @@ function scanZipLike(bytes, path) {
|
|
|
226
267
|
}
|
|
227
268
|
};
|
|
228
269
|
}
|
|
270
|
+
function readZipCentralDirectory(bytes) {
|
|
271
|
+
const eocdOffset = findZipEndOfCentralDirectory(bytes);
|
|
272
|
+
if (eocdOffset === null) {
|
|
273
|
+
return { error: "zip end of central directory record not found" };
|
|
274
|
+
}
|
|
275
|
+
const entryCount = bytes.readUInt16LE(eocdOffset + 10);
|
|
276
|
+
const centralDirectorySize = bytes.readUInt32LE(eocdOffset + 12);
|
|
277
|
+
const centralDirectoryOffset = bytes.readUInt32LE(eocdOffset + 16);
|
|
278
|
+
if (entryCount === 0xffff || centralDirectorySize === 0xffffffff || centralDirectoryOffset === 0xffffffff) {
|
|
279
|
+
return { error: "zip64 archives are not supported by local verification" };
|
|
280
|
+
}
|
|
281
|
+
if (centralDirectoryOffset + centralDirectorySize > eocdOffset) {
|
|
282
|
+
return { error: "zip central directory extends past its end record" };
|
|
283
|
+
}
|
|
284
|
+
const entries = [];
|
|
285
|
+
let offset = centralDirectoryOffset;
|
|
286
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
287
|
+
if (offset + 46 > eocdOffset || bytes.readUInt32LE(offset) !== ZIP_CENTRAL_HEADER_SIG) {
|
|
288
|
+
return { error: "zip central directory is truncated or malformed" };
|
|
289
|
+
}
|
|
290
|
+
const nameLength = bytes.readUInt16LE(offset + 28);
|
|
291
|
+
const extraLength = bytes.readUInt16LE(offset + 30);
|
|
292
|
+
const commentLength = bytes.readUInt16LE(offset + 32);
|
|
293
|
+
if (offset + 46 + nameLength + extraLength + commentLength > eocdOffset) {
|
|
294
|
+
return { error: "zip central directory is truncated or malformed" };
|
|
295
|
+
}
|
|
296
|
+
entries.push({
|
|
297
|
+
name: bytes.subarray(offset + 46, offset + 46 + nameLength).toString("utf8"),
|
|
298
|
+
flags: bytes.readUInt16LE(offset + 8),
|
|
299
|
+
method: bytes.readUInt16LE(offset + 10),
|
|
300
|
+
compressedSize: bytes.readUInt32LE(offset + 20),
|
|
301
|
+
uncompressedSize: bytes.readUInt32LE(offset + 24),
|
|
302
|
+
localHeaderOffset: bytes.readUInt32LE(offset + 42)
|
|
303
|
+
});
|
|
304
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
305
|
+
}
|
|
306
|
+
if (offset !== centralDirectoryOffset + centralDirectorySize) {
|
|
307
|
+
return { error: "zip central directory size disagrees with its entries" };
|
|
308
|
+
}
|
|
309
|
+
return { entries, centralDirectoryOffset };
|
|
310
|
+
}
|
|
311
|
+
function findZipEndOfCentralDirectory(bytes) {
|
|
312
|
+
const searchStart = Math.max(0, bytes.length - ZIP_EOCD_SEARCH_BYTES);
|
|
313
|
+
for (let offset = bytes.length - ZIP_EOCD_MIN_BYTES; offset >= searchStart; offset -= 1) {
|
|
314
|
+
if (bytes.readUInt32LE(offset) === ZIP_EOCD_SIG) {
|
|
315
|
+
const commentLength = bytes.readUInt16LE(offset + 20);
|
|
316
|
+
if (offset + ZIP_EOCD_MIN_BYTES + commentLength === bytes.length) {
|
|
317
|
+
return offset;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
function zipLocalHeaderMismatch(bytes, central) {
|
|
324
|
+
const { entries, centralDirectoryOffset } = central;
|
|
325
|
+
const claimed = new Map();
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (claimed.has(entry.localHeaderOffset)) {
|
|
328
|
+
return "zip central directory lists overlapping local entries";
|
|
329
|
+
}
|
|
330
|
+
claimed.set(entry.localHeaderOffset, entry);
|
|
331
|
+
}
|
|
332
|
+
for (const entry of entries) {
|
|
333
|
+
const offset = entry.localHeaderOffset;
|
|
334
|
+
if (offset + 30 > centralDirectoryOffset || bytes.readUInt32LE(offset) !== ZIP_LOCAL_HEADER_SIG) {
|
|
335
|
+
return "zip central directory points at a missing local file header";
|
|
336
|
+
}
|
|
337
|
+
const flags = bytes.readUInt16LE(offset + 6);
|
|
338
|
+
const compressedSize = bytes.readUInt32LE(offset + 18);
|
|
339
|
+
const nameLength = bytes.readUInt16LE(offset + 26);
|
|
340
|
+
const name = bytes.subarray(offset + 30, offset + 30 + nameLength).toString("utf8");
|
|
341
|
+
if (name !== entry.name) {
|
|
342
|
+
return "zip local file header name disagrees with the central directory";
|
|
343
|
+
}
|
|
344
|
+
if ((flags & 0x8) === 0 && compressedSize !== entry.compressedSize) {
|
|
345
|
+
return "zip local file header size disagrees with the central directory";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
let offset = bytes.length >= 4 && bytes.readUInt32LE(0) === ZIP_LOCAL_HEADER_SIG
|
|
349
|
+
? 0
|
|
350
|
+
: entries.length > 0
|
|
351
|
+
? Math.min(...entries.map((entry) => entry.localHeaderOffset))
|
|
352
|
+
: centralDirectoryOffset;
|
|
353
|
+
while (offset + 4 <= centralDirectoryOffset && bytes.readUInt32LE(offset) === ZIP_LOCAL_HEADER_SIG) {
|
|
354
|
+
const entry = claimed.get(offset);
|
|
355
|
+
if (!entry) {
|
|
356
|
+
return "zip has a local file entry the central directory does not list";
|
|
357
|
+
}
|
|
358
|
+
const nameLength = bytes.readUInt16LE(offset + 26);
|
|
359
|
+
const extraLength = bytes.readUInt16LE(offset + 28);
|
|
360
|
+
let next = offset + 30 + nameLength + extraLength + entry.compressedSize;
|
|
361
|
+
if ((entry.flags & 0x8) !== 0) {
|
|
362
|
+
next += next + 4 <= centralDirectoryOffset && bytes.readUInt32LE(next) === ZIP_DATA_DESCRIPTOR_SIG ? 16 : 12;
|
|
363
|
+
}
|
|
364
|
+
if (next > centralDirectoryOffset) {
|
|
365
|
+
return "zip local entry data extends into the central directory";
|
|
366
|
+
}
|
|
367
|
+
offset = next;
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
function readZipEntryBody(bytes, entry, errors) {
|
|
372
|
+
if ((entry.flags & 0x1) !== 0) {
|
|
373
|
+
return Buffer.alloc(0);
|
|
374
|
+
}
|
|
375
|
+
const headerOffset = entry.localHeaderOffset;
|
|
376
|
+
const nameLength = bytes.readUInt16LE(headerOffset + 26);
|
|
377
|
+
const extraLength = bytes.readUInt16LE(headerOffset + 28);
|
|
378
|
+
const dataStart = headerOffset + 30 + nameLength + extraLength;
|
|
379
|
+
if (dataStart + entry.compressedSize > bytes.length) {
|
|
380
|
+
errors.push(`${entry.name}: zip entry data is truncated`);
|
|
381
|
+
return Buffer.alloc(0);
|
|
382
|
+
}
|
|
383
|
+
return readZipBody(bytes.subarray(dataStart, dataStart + entry.compressedSize), entry.method, entry.name, errors);
|
|
384
|
+
}
|
|
229
385
|
function readZipBody(data, method, name, errors) {
|
|
230
386
|
if (method === 0) {
|
|
231
387
|
return data;
|
|
@@ -375,9 +531,51 @@ function tarEntryName(header) {
|
|
|
375
531
|
return prefix.length > 0 ? `${prefix}/${name}` : name;
|
|
376
532
|
}
|
|
377
533
|
function tarEntrySize(header) {
|
|
534
|
+
if ((header.readUInt8(124) & 0x80) !== 0) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
378
537
|
const value = readNullTerminated(header, 124, 12).trim();
|
|
379
|
-
|
|
380
|
-
|
|
538
|
+
if (value.length === 0) {
|
|
539
|
+
return 0;
|
|
540
|
+
}
|
|
541
|
+
if (!/^[0-7]+$/u.test(value)) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
return Number.parseInt(value, 8);
|
|
545
|
+
}
|
|
546
|
+
function parsePaxRecords(body) {
|
|
547
|
+
const records = new Map();
|
|
548
|
+
let offset = 0;
|
|
549
|
+
while (offset < body.length) {
|
|
550
|
+
const space = body.indexOf(0x20, offset);
|
|
551
|
+
if (space === -1 || space === offset) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
const lengthText = body.subarray(offset, space).toString("ascii");
|
|
555
|
+
if (!/^\d+$/u.test(lengthText)) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const length = Number.parseInt(lengthText, 10);
|
|
559
|
+
const recordEnd = offset + length;
|
|
560
|
+
if (length <= space - offset + 1 || recordEnd > body.length || body.readUInt8(recordEnd - 1) !== 0x0a) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
const text = body.subarray(space + 1, recordEnd - 1).toString("utf8");
|
|
564
|
+
const equals = text.indexOf("=");
|
|
565
|
+
if (equals <= 0) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
records.set(text.slice(0, equals), text.slice(equals + 1));
|
|
569
|
+
offset = recordEnd;
|
|
570
|
+
}
|
|
571
|
+
return records;
|
|
572
|
+
}
|
|
573
|
+
function mergePaxRecords(existing, incoming) {
|
|
574
|
+
const merged = new Map(existing ?? []);
|
|
575
|
+
for (const [key, value] of incoming) {
|
|
576
|
+
merged.set(key, value);
|
|
577
|
+
}
|
|
578
|
+
return merged;
|
|
381
579
|
}
|
|
382
580
|
function readNullTerminated(buffer, start, length) {
|
|
383
581
|
const slice = buffer.subarray(start, start + length);
|
|
@@ -3,10 +3,11 @@ 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 { packagePageUrl } from "../presentation/package-page.js";
|
|
6
7
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
7
8
|
import { isRemotePackageSpec, isSupportedLockfilePath } from "./preflight.js";
|
|
8
9
|
import { authStatus } from "../auth/store.js";
|
|
9
|
-
import { EXIT_TOOL_ERROR, EXIT_UNAVAILABLE } from "../commands/types.js";
|
|
10
|
+
import { EXIT_TOOL_ERROR, EXIT_UNAVAILABLE, EXIT_USAGE_VERDICT } from "../commands/types.js";
|
|
10
11
|
const REGISTRIES = { npm: "npm", pypi: "pypi" };
|
|
11
12
|
const DEEP_VERIFY_HINT = "deep verify supports npm and pypi: dg verify npm:<package> or pypi:<package>";
|
|
12
13
|
function parseSpec(target) {
|
|
@@ -38,10 +39,10 @@ function parseSpec(target) {
|
|
|
38
39
|
const version = rest.slice(at + separator.length).trim() || null;
|
|
39
40
|
return { ecosystem, name, version };
|
|
40
41
|
}
|
|
41
|
-
async function resolveLatest(ecosystem, name, fetchImpl) {
|
|
42
|
+
export async function resolveLatest(ecosystem, name, fetchImpl) {
|
|
42
43
|
try {
|
|
43
44
|
if (ecosystem === "npm") {
|
|
44
|
-
const response = await fetchImpl(`https://registry.npmjs.org/${name
|
|
45
|
+
const response = await fetchImpl(`https://registry.npmjs.org/${encodeNpmPackagePath(name)}`);
|
|
45
46
|
if (!response.ok) {
|
|
46
47
|
return null;
|
|
47
48
|
}
|
|
@@ -59,24 +60,31 @@ async function resolveLatest(ecosystem, name, fetchImpl) {
|
|
|
59
60
|
return null;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
function provenanceLines(version, result, theme, withPredicate) {
|
|
64
|
+
if (!result.provenance) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const label = withPredicate && result.provenance.predicateType
|
|
68
|
+
? `provenance ${provenanceLabel(result.provenance)} · ${result.provenance.predicateType}`
|
|
69
|
+
: `provenance ${provenanceLabel(result.provenance)}`;
|
|
70
|
+
const lines = [` ${theme.paint("muted", label)}`];
|
|
71
|
+
const downgrade = provenanceDowngradeLine(version, result.provenance);
|
|
72
|
+
if (downgrade) {
|
|
73
|
+
lines.push(` ${theme.paint("warn", `⚠ ${downgrade}`)}`);
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
function reasonGlyph(action, theme) {
|
|
78
|
+
return action === "block" ? theme.paint("block", "✘") : action === "warn" ? theme.paint("warn", "⚠") : theme.paint("muted", "·");
|
|
79
|
+
}
|
|
62
80
|
function renderResult(spec, version, result, theme, verbose) {
|
|
63
81
|
const action = result.action ?? "pass";
|
|
64
82
|
const badge = theme.badge(action);
|
|
65
83
|
const lines = [`${badge} ${result.name}@${version} (${spec.ecosystem}) ${theme.paint("muted", `score ${result.score}`)}`];
|
|
66
|
-
|
|
67
|
-
const label = verbose && result.provenance.predicateType
|
|
68
|
-
? `provenance ${provenanceLabel(result.provenance)} · ${result.provenance.predicateType}`
|
|
69
|
-
: `provenance ${provenanceLabel(result.provenance)}`;
|
|
70
|
-
lines.push(` ${theme.paint("muted", label)}`);
|
|
71
|
-
const downgrade = provenanceDowngradeLine(version, result.provenance);
|
|
72
|
-
if (downgrade) {
|
|
73
|
-
lines.push(` ${theme.paint("warn", `⚠ ${downgrade}`)}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
84
|
+
lines.push(...provenanceLines(version, result, theme, verbose));
|
|
76
85
|
const reasons = verbose ? result.reasons : result.reasons.slice(0, 6);
|
|
77
86
|
for (const reason of reasons) {
|
|
78
|
-
|
|
79
|
-
lines.push(` ${glyph} ${reason}`);
|
|
87
|
+
lines.push(` ${reasonGlyph(action, theme)} ${reason}`);
|
|
80
88
|
}
|
|
81
89
|
if (!verbose && result.reasons.length > reasons.length) {
|
|
82
90
|
lines.push(` ${theme.paint("muted", `… ${result.reasons.length - reasons.length} more — rerun with --verbose`)}`);
|
|
@@ -92,8 +100,43 @@ function renderResult(spec, version, result, theme, verbose) {
|
|
|
92
100
|
if (result.recommendation) {
|
|
93
101
|
lines.push(` ${theme.paint("muted", result.recommendation)}`);
|
|
94
102
|
}
|
|
103
|
+
const page = packagePageUrl(spec.ecosystem, result.name);
|
|
104
|
+
if (page) {
|
|
105
|
+
lines.push(` ${theme.paint("muted", `→ ${page}`)}`);
|
|
106
|
+
}
|
|
107
|
+
return `${lines.join("\n")}\n`;
|
|
108
|
+
}
|
|
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
|
+
}
|
|
95
135
|
return `${lines.join("\n")}\n`;
|
|
96
136
|
}
|
|
137
|
+
function encodeNpmPackagePath(name) {
|
|
138
|
+
return name.split("/").map((segment) => encodeURIComponent(segment)).join("%2f");
|
|
139
|
+
}
|
|
97
140
|
function exitCodeFor(action) {
|
|
98
141
|
if (action === "block") {
|
|
99
142
|
return 2;
|
|
@@ -109,39 +152,41 @@ function exitCodeFor(action) {
|
|
|
109
152
|
export async function runPackageCheck(target, io = {}, options = {}) {
|
|
110
153
|
const fetchImpl = io.fetchImpl ?? fetch;
|
|
111
154
|
const theme = createTheme(resolvePresentation().color);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
155
|
+
const accent = (text) => theme.paint("accent", text);
|
|
156
|
+
const muted = (text) => theme.paint("muted", text);
|
|
157
|
+
const authenticated = authStatus(io.env).authenticated;
|
|
158
|
+
if (!authenticated && (options.format === "json" || options.outputPath)) {
|
|
115
159
|
return {
|
|
116
160
|
exitCode: EXIT_UNAVAILABLE,
|
|
117
161
|
stdout: "",
|
|
118
|
-
stderr: `\n ${theme.paint("warn", "⚠")}
|
|
162
|
+
stderr: `\n ${theme.paint("warn", "⚠")} ${muted("--json and --output for registry checks require sign-in.")}\n` +
|
|
119
163
|
` ${accent("dg login")}${muted(" · see plans at")} ${accent("westbayberry.com/pricing")}\n\n`
|
|
120
164
|
};
|
|
121
165
|
}
|
|
122
166
|
const parsed = parseSpec(target);
|
|
123
167
|
if (parsed === null) {
|
|
124
168
|
return {
|
|
125
|
-
exitCode:
|
|
169
|
+
exitCode: EXIT_USAGE_VERDICT,
|
|
126
170
|
stdout: "",
|
|
127
171
|
stderr: `dg verify: add a registry, e.g. dg verify npm:${target} or dg verify pypi:${target}\n`
|
|
128
172
|
};
|
|
129
173
|
}
|
|
130
174
|
if ("error" in parsed) {
|
|
131
|
-
return { exitCode:
|
|
175
|
+
return { exitCode: EXIT_USAGE_VERDICT, stdout: "", stderr: `dg verify: ${parsed.error}\n` };
|
|
132
176
|
}
|
|
133
177
|
let version = parsed.version;
|
|
134
178
|
if (!version) {
|
|
135
179
|
version = await resolveLatest(parsed.ecosystem, parsed.name, fetchImpl);
|
|
136
180
|
if (!version) {
|
|
137
181
|
return {
|
|
138
|
-
exitCode:
|
|
182
|
+
exitCode: exitCodeFor("analysis_incomplete"),
|
|
139
183
|
stdout: "",
|
|
140
184
|
stderr: `dg verify: could not resolve the latest version of ${parsed.name} on ${parsed.ecosystem}\n`
|
|
141
185
|
};
|
|
142
186
|
}
|
|
143
187
|
}
|
|
144
188
|
let result;
|
|
189
|
+
let freeScansRemaining;
|
|
145
190
|
try {
|
|
146
191
|
const response = await analyzePackages([{ name: parsed.name, version }], {
|
|
147
192
|
ecosystem: parsed.ecosystem,
|
|
@@ -149,13 +194,22 @@ export async function runPackageCheck(target, io = {}, options = {}) {
|
|
|
149
194
|
...(io.env ? { env: io.env } : {})
|
|
150
195
|
});
|
|
151
196
|
result = response.packages.find((entry) => entry.name === parsed.name) ?? response.packages[0];
|
|
197
|
+
freeScansRemaining = response.freeScansRemaining;
|
|
152
198
|
}
|
|
153
199
|
catch (error) {
|
|
200
|
+
if (error instanceof AnalyzeError && error.code === "quota_exceeded") {
|
|
201
|
+
const loginLine = authenticated ? "" : ` ${muted("Run")} ${accent("dg login")} ${muted("to connect your account.")}\n`;
|
|
202
|
+
return {
|
|
203
|
+
exitCode: exitCodeFor("analysis_incomplete"),
|
|
204
|
+
stdout: "",
|
|
205
|
+
stderr: `dg verify: ${error.message}\n${loginLine} ${muted("see plans at")} ${accent("westbayberry.com/pricing")}\n`
|
|
206
|
+
};
|
|
207
|
+
}
|
|
154
208
|
const message = error instanceof AnalyzeError ? error.message : error instanceof Error ? error.message : "could not reach the scanner";
|
|
155
|
-
return { exitCode:
|
|
209
|
+
return { exitCode: exitCodeFor("analysis_incomplete"), stdout: "", stderr: `dg verify: ${message}\n` };
|
|
156
210
|
}
|
|
157
211
|
if (!result) {
|
|
158
|
-
return { exitCode:
|
|
212
|
+
return { exitCode: exitCodeFor("analysis_incomplete"), stdout: "", stderr: `dg verify: scanner returned no result for ${parsed.name}\n` };
|
|
159
213
|
}
|
|
160
214
|
const action = result.action ?? "pass";
|
|
161
215
|
const rendered = options.format === "json"
|
|
@@ -171,7 +225,9 @@ export async function runPackageCheck(target, io = {}, options = {}) {
|
|
|
171
225
|
...(result.provenance ? { provenance: result.provenance } : {}),
|
|
172
226
|
...(result.recommendation ? { recommendation: result.recommendation } : {})
|
|
173
227
|
}, null, 2)}\n`
|
|
174
|
-
:
|
|
228
|
+
: authenticated
|
|
229
|
+
? renderResult(parsed, version, result, theme, options.verbose ?? false)
|
|
230
|
+
: renderFreeResult(parsed, version, result, theme, freeScansRemaining);
|
|
175
231
|
if (options.outputPath) {
|
|
176
232
|
try {
|
|
177
233
|
writeFileSync(resolve(options.outputPath), rendered, "utf8");
|
|
@@ -248,20 +304,20 @@ export async function maybeVerifyPackage(args) {
|
|
|
248
304
|
if (outputFlagMissingPath) {
|
|
249
305
|
return {
|
|
250
306
|
handled: true,
|
|
251
|
-
result: { exitCode:
|
|
307
|
+
result: { exitCode: EXIT_USAGE_VERDICT, stdout: "", stderr: `dg verify: ${outputFlagMissingPath} requires a path. Run 'dg verify --help'.\n` }
|
|
252
308
|
};
|
|
253
309
|
}
|
|
254
310
|
if (unknownFlag) {
|
|
255
311
|
return {
|
|
256
312
|
handled: true,
|
|
257
|
-
result: { exitCode:
|
|
313
|
+
result: { exitCode: EXIT_USAGE_VERDICT, stdout: "", stderr: `dg verify: unknown option '${unknownFlag}'. Run 'dg verify --help'.\n` }
|
|
258
314
|
};
|
|
259
315
|
}
|
|
260
316
|
if (sarif) {
|
|
261
317
|
return {
|
|
262
318
|
handled: true,
|
|
263
319
|
result: {
|
|
264
|
-
exitCode:
|
|
320
|
+
exitCode: EXIT_USAGE_VERDICT,
|
|
265
321
|
stdout: "",
|
|
266
322
|
stderr: "dg verify: --sarif applies to local artifacts and lockfiles; registry package checks support --json.\n"
|
|
267
323
|
}
|