@westbayberry/dg 1.3.3 → 2.0.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/LICENSE +1 -201
- package/NOTICE +1 -4
- package/README.md +293 -0
- package/dist/api/analyze.js +210 -0
- package/dist/audit/deep.js +180 -0
- package/dist/audit/detectors.js +247 -0
- package/dist/audit/events.js +41 -0
- package/dist/audit/rules.js +426 -0
- package/dist/audit-ui/AuditApp.js +39 -0
- package/dist/audit-ui/components/AuditHeader.js +24 -0
- package/dist/audit-ui/components/AuditResultsView.js +307 -0
- package/dist/audit-ui/components/DeepStatusRow.js +11 -0
- package/dist/audit-ui/export.js +85 -0
- package/dist/audit-ui/format.js +34 -0
- package/dist/audit-ui/launch.js +34 -0
- package/dist/auth/device-login.js +271 -0
- package/dist/auth/env-token.js +6 -0
- package/dist/auth/login-app.js +156 -0
- package/dist/auth/store.js +147 -0
- package/dist/bin/dg.js +71 -0
- package/dist/commands/audit.js +357 -0
- package/dist/commands/completion.js +116 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/doctor.js +39 -0
- package/dist/commands/explain.js +100 -0
- package/dist/commands/guard-commit.js +158 -0
- package/dist/commands/help.js +74 -0
- package/dist/commands/licenses.js +435 -0
- package/dist/commands/login.js +81 -0
- package/dist/commands/logout.js +37 -0
- package/dist/commands/router.js +98 -0
- package/dist/commands/scan.js +18 -0
- package/dist/commands/service.js +475 -0
- package/dist/commands/setup.js +302 -0
- package/dist/commands/status.js +115 -0
- package/dist/commands/suggest.js +35 -0
- package/dist/commands/types.js +4 -0
- package/dist/commands/unavailable.js +11 -0
- package/dist/commands/uninstall.js +111 -0
- package/dist/commands/update.js +210 -0
- package/dist/commands/verify.js +151 -0
- package/dist/commands/version.js +22 -0
- package/dist/commands/wrap.js +55 -0
- package/dist/config/settings.js +302 -0
- package/dist/install-ui/LiveInstall.js +24 -0
- package/dist/install-ui/block-render.js +83 -0
- package/dist/install-ui/live-install-app.js +48 -0
- package/dist/install-ui/prompt.js +24 -0
- package/dist/launcher/classify.js +116 -0
- package/dist/launcher/env.js +53 -0
- package/dist/launcher/live-install.js +50 -0
- package/dist/launcher/output-redaction.js +77 -0
- package/dist/launcher/preflight-prompt.js +139 -0
- package/dist/launcher/resolve-real-binary.js +73 -0
- package/dist/launcher/run.js +417 -0
- package/dist/policy/evaluate.js +128 -0
- package/dist/presentation/mode.js +52 -0
- package/dist/presentation/theme.js +29 -0
- package/dist/proxy/buffer-budget.js +64 -0
- package/dist/proxy/ca.js +126 -0
- package/dist/proxy/classify-host.js +26 -0
- package/dist/proxy/enforcement.js +102 -0
- package/dist/proxy/metadata-map.js +336 -0
- package/dist/proxy/server.js +909 -0
- package/dist/proxy/upstream-proxy.js +102 -0
- package/dist/proxy/worker.js +39 -0
- package/dist/publish-set/collect.js +51 -0
- package/dist/publish-set/no-exec-shell.js +19 -0
- package/dist/publish-set/npm.js +109 -0
- package/dist/publish-set/pack.js +36 -0
- package/dist/publish-set/pypi.js +59 -0
- package/dist/runtime/cli.js +17 -0
- package/dist/runtime/first-run.js +60 -0
- package/dist/runtime/node-version.js +58 -0
- package/dist/runtime/nudges.js +105 -0
- package/dist/scan/analyze-worker.js +21 -0
- package/dist/scan/collect.js +153 -0
- package/dist/scan/command.js +159 -0
- package/dist/scan/discovery.js +209 -0
- package/dist/scan/render.js +240 -0
- package/dist/scan/scanner-report.js +82 -0
- package/dist/scan/staged.js +173 -0
- package/dist/scan/types.js +1 -0
- package/dist/scan-ui/LegacyApp.js +156 -0
- package/dist/scan-ui/alt-screen.js +84 -0
- package/dist/scan-ui/api-aliases.js +1 -0
- package/dist/scan-ui/components/ErrorView.js +23 -0
- package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
- package/dist/scan-ui/components/ProgressBar.js +89 -0
- package/dist/scan-ui/components/ProjectSelector.js +62 -0
- package/dist/scan-ui/components/ScoreHeader.js +20 -0
- package/dist/scan-ui/components/SetupBanner.js +13 -0
- package/dist/scan-ui/components/Spinner.js +4 -0
- package/dist/scan-ui/format-helpers.js +40 -0
- package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
- package/dist/scan-ui/hooks/useScan.js +113 -0
- package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
- package/dist/scan-ui/launch.js +27 -0
- package/dist/scan-ui/logo.js +91 -0
- package/dist/scan-ui/shims.js +30 -0
- package/dist/security/sanitize.js +28 -0
- package/dist/service/state.js +837 -0
- package/dist/service/trust-store.js +234 -0
- package/dist/service/worker.js +88 -0
- package/dist/setup/git-hook.js +244 -0
- package/dist/setup/optional-support.js +58 -0
- package/dist/setup/plan.js +899 -0
- package/dist/state/cleanup-registry.js +60 -0
- package/dist/state/index.js +5 -0
- package/dist/state/locks.js +161 -0
- package/dist/state/paths.js +24 -0
- package/dist/state/sessions.js +170 -0
- package/dist/state/store.js +50 -0
- package/dist/telemetry/events.js +40 -0
- package/dist/util/git.js +20 -0
- package/dist/util/tty-prompt.js +43 -0
- package/dist/verify/local.js +400 -0
- package/dist/verify/package-check.js +240 -0
- package/dist/verify/preflight.js +698 -0
- package/dist/verify/render.js +184 -0
- package/dist/verify/types.js +1 -0
- package/package.json +33 -50
- package/dist/index.mjs +0 -54116
- package/dist/postinstall.mjs +0 -731
- package/dist/python-hook/dg_pip_hook.pth +0 -1
- package/dist/python-hook/dg_pip_hook.py +0 -130
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { loadUserConfig } from "../config/settings.js";
|
|
4
|
+
import { evaluatePackagePolicy, resolveEffectivePolicy } from "../policy/evaluate.js";
|
|
5
|
+
const LOCKFILE_NAMES = new Set([
|
|
6
|
+
"Cargo.lock",
|
|
7
|
+
"Pipfile.lock",
|
|
8
|
+
"package-lock.json",
|
|
9
|
+
"pnpm-lock.yaml",
|
|
10
|
+
"poetry.lock",
|
|
11
|
+
"requirements.txt",
|
|
12
|
+
"yarn.lock"
|
|
13
|
+
]);
|
|
14
|
+
const REMOTE_SPEC_PREFIXES = [
|
|
15
|
+
"http://",
|
|
16
|
+
"https://",
|
|
17
|
+
"git+",
|
|
18
|
+
"git://",
|
|
19
|
+
"ssh://",
|
|
20
|
+
"github:"
|
|
21
|
+
];
|
|
22
|
+
export function isSupportedLockfilePath(target) {
|
|
23
|
+
return LOCKFILE_NAMES.has(basename(target));
|
|
24
|
+
}
|
|
25
|
+
export function verifyPackageSpec(spec, options = {}) {
|
|
26
|
+
const parsed = parsePackageSpec(spec);
|
|
27
|
+
const observations = parsed
|
|
28
|
+
? [parsed]
|
|
29
|
+
: [blockedUnknownSpec(spec)];
|
|
30
|
+
return preflightReport({
|
|
31
|
+
target: spec,
|
|
32
|
+
inputKind: "package-spec",
|
|
33
|
+
preflight: {
|
|
34
|
+
advisory: true,
|
|
35
|
+
packageCount: observations.length,
|
|
36
|
+
identitySource: "package-spec",
|
|
37
|
+
message: "Package spec verification is advisory preflight; proxy enforcement remains authoritative for network fetches."
|
|
38
|
+
},
|
|
39
|
+
observations,
|
|
40
|
+
options
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function verifyLockfile(targetPath, options = {}) {
|
|
44
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
45
|
+
const absoluteTarget = resolve(cwd, targetPath);
|
|
46
|
+
const displayTarget = displayPath(cwd, absoluteTarget);
|
|
47
|
+
if (!existsSync(absoluteTarget)) {
|
|
48
|
+
return preflightReport({
|
|
49
|
+
target: displayTarget,
|
|
50
|
+
inputKind: "lockfile",
|
|
51
|
+
preflight: lockfileSummary(0),
|
|
52
|
+
observations: [],
|
|
53
|
+
options,
|
|
54
|
+
errors: [`lockfile does not exist: ${displayTarget}`]
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
let text;
|
|
58
|
+
try {
|
|
59
|
+
text = readFileSync(absoluteTarget, "utf8");
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return preflightReport({
|
|
63
|
+
target: displayTarget,
|
|
64
|
+
inputKind: "lockfile",
|
|
65
|
+
preflight: lockfileSummary(0),
|
|
66
|
+
observations: [],
|
|
67
|
+
options,
|
|
68
|
+
errors: [`could not read lockfile: ${error instanceof Error ? error.message : "unknown read error"}`]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const observations = parseLockfile(basename(absoluteTarget), text);
|
|
72
|
+
return preflightReport({
|
|
73
|
+
target: displayTarget,
|
|
74
|
+
inputKind: "lockfile",
|
|
75
|
+
preflight: lockfileSummary(observations.length),
|
|
76
|
+
observations,
|
|
77
|
+
options
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function preflightReport(input) {
|
|
81
|
+
const policy = resolveEffectivePolicy({
|
|
82
|
+
userConfig: loadUserConfig()
|
|
83
|
+
});
|
|
84
|
+
const allowlists = (input.options.allowPackages ?? []).map((packageName) => ({
|
|
85
|
+
packageName,
|
|
86
|
+
reason: "dg verify command allowlist",
|
|
87
|
+
trustedBy: "user"
|
|
88
|
+
}));
|
|
89
|
+
const deniedLicenses = new Set((input.options.denyLicenses ?? []).map(normalizeLicense));
|
|
90
|
+
const findings = [];
|
|
91
|
+
for (const observation of input.observations) {
|
|
92
|
+
const packageName = packageDisplayName(observation.identity);
|
|
93
|
+
const licenseFinding = deniedLicenseFinding(observation.identity, deniedLicenses);
|
|
94
|
+
const packageVerdict = strongerVerdict(observation.verdict, licenseFinding ? "block" : "pass");
|
|
95
|
+
const evaluation = evaluatePackagePolicy({
|
|
96
|
+
verdict: packageVerdict,
|
|
97
|
+
packageName,
|
|
98
|
+
policy,
|
|
99
|
+
allowlists
|
|
100
|
+
});
|
|
101
|
+
const baseFinding = licenseFinding ?? observation.finding;
|
|
102
|
+
if (baseFinding && evaluation.action !== "pass") {
|
|
103
|
+
findings.push({
|
|
104
|
+
...baseFinding,
|
|
105
|
+
severity: evaluation.action === "block" ? "block" : "warn",
|
|
106
|
+
message: `${baseFinding.message} (${evaluation.reason})`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const errors = [...(input.errors ?? [])];
|
|
111
|
+
return {
|
|
112
|
+
target: input.target,
|
|
113
|
+
inputKind: input.inputKind,
|
|
114
|
+
status: statusFor(policyActionFor(findings), errors),
|
|
115
|
+
sha256: null,
|
|
116
|
+
sizeBytes: null,
|
|
117
|
+
archive: null,
|
|
118
|
+
workspaceScan: null,
|
|
119
|
+
preflight: input.preflight,
|
|
120
|
+
packages: input.observations.map((observation) => observation.identity),
|
|
121
|
+
findings,
|
|
122
|
+
errors,
|
|
123
|
+
summary: summarize(findings, errors)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function parsePackageSpec(spec) {
|
|
127
|
+
const trimmed = spec.trim();
|
|
128
|
+
if (trimmed.length === 0 || /[\u0000-\u001f\u007f]/u.test(trimmed)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const lowered = trimmed.toLowerCase();
|
|
132
|
+
if (REMOTE_SPEC_PREFIXES.some((prefix) => lowered.startsWith(prefix)) || lowered.startsWith("file:")) {
|
|
133
|
+
return packageObservation({
|
|
134
|
+
ecosystem: "unknown",
|
|
135
|
+
name: trimmed,
|
|
136
|
+
version: null,
|
|
137
|
+
requested: spec,
|
|
138
|
+
sourceKind: "package-spec",
|
|
139
|
+
resolvedUrl: trimmed,
|
|
140
|
+
integrity: null,
|
|
141
|
+
license: null
|
|
142
|
+
}, "block", {
|
|
143
|
+
id: "unverified-network-spec",
|
|
144
|
+
title: "Unverified network package spec",
|
|
145
|
+
message: "direct URL, git, and file package specs require artifact verification before install",
|
|
146
|
+
location: spec
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (lowered.startsWith("npm:")) {
|
|
150
|
+
return parseNpmSpec(trimmed.slice(4), spec);
|
|
151
|
+
}
|
|
152
|
+
if (lowered.startsWith("pypi:")) {
|
|
153
|
+
return parsePypiSpec(trimmed.slice(5), spec);
|
|
154
|
+
}
|
|
155
|
+
if (lowered.startsWith("cargo:")) {
|
|
156
|
+
return parseCargoSpec(trimmed.slice(6), spec);
|
|
157
|
+
}
|
|
158
|
+
if (trimmed.includes("==")) {
|
|
159
|
+
return parsePypiSpec(trimmed, spec);
|
|
160
|
+
}
|
|
161
|
+
return parseNpmSpec(trimmed, spec);
|
|
162
|
+
}
|
|
163
|
+
function parseNpmSpec(value, requested) {
|
|
164
|
+
const parsed = splitNameVersion(value);
|
|
165
|
+
if (!parsed || !isValidPackageName(parsed.name)) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return packageObservation({
|
|
169
|
+
ecosystem: "npm",
|
|
170
|
+
name: parsed.name,
|
|
171
|
+
version: parsed.version,
|
|
172
|
+
requested,
|
|
173
|
+
sourceKind: "package-spec",
|
|
174
|
+
resolvedUrl: null,
|
|
175
|
+
integrity: null,
|
|
176
|
+
license: null
|
|
177
|
+
}, exactVersionVerdict(parsed.version), exactVersionFinding(parsed.name, parsed.version, requested));
|
|
178
|
+
}
|
|
179
|
+
function parsePypiSpec(value, requested) {
|
|
180
|
+
const match = /^([A-Za-z0-9_.-]+)(?:==([^<>=!~\s]+))?$/.exec(value.trim());
|
|
181
|
+
if (!match?.[1]) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const version = match[2] ?? null;
|
|
185
|
+
return packageObservation({
|
|
186
|
+
ecosystem: "pypi",
|
|
187
|
+
name: match[1],
|
|
188
|
+
version,
|
|
189
|
+
requested,
|
|
190
|
+
sourceKind: "package-spec",
|
|
191
|
+
resolvedUrl: null,
|
|
192
|
+
integrity: null,
|
|
193
|
+
license: null
|
|
194
|
+
}, exactVersionVerdict(version), exactVersionFinding(match[1], version, requested));
|
|
195
|
+
}
|
|
196
|
+
function parseCargoSpec(value, requested) {
|
|
197
|
+
const parsed = splitNameVersion(value);
|
|
198
|
+
if (!parsed || !/^[A-Za-z0-9_-]+$/.test(parsed.name)) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return packageObservation({
|
|
202
|
+
ecosystem: "cargo",
|
|
203
|
+
name: parsed.name,
|
|
204
|
+
version: parsed.version,
|
|
205
|
+
requested,
|
|
206
|
+
sourceKind: "package-spec",
|
|
207
|
+
resolvedUrl: null,
|
|
208
|
+
integrity: null,
|
|
209
|
+
license: null
|
|
210
|
+
}, exactVersionVerdict(parsed.version), exactVersionFinding(parsed.name, parsed.version, requested));
|
|
211
|
+
}
|
|
212
|
+
function parseLockfile(name, text) {
|
|
213
|
+
if (name === "package-lock.json") {
|
|
214
|
+
return parsePackageLock(text);
|
|
215
|
+
}
|
|
216
|
+
if (name === "yarn.lock") {
|
|
217
|
+
return parseYarnLock(text);
|
|
218
|
+
}
|
|
219
|
+
if (name === "pnpm-lock.yaml") {
|
|
220
|
+
return parsePnpmLock(text);
|
|
221
|
+
}
|
|
222
|
+
if (name === "requirements.txt") {
|
|
223
|
+
return parseRequirements(text);
|
|
224
|
+
}
|
|
225
|
+
if (name === "Cargo.lock") {
|
|
226
|
+
return parseCargoLock(text);
|
|
227
|
+
}
|
|
228
|
+
if (name === "poetry.lock") {
|
|
229
|
+
return parsePoetryLock(text);
|
|
230
|
+
}
|
|
231
|
+
if (name === "Pipfile.lock") {
|
|
232
|
+
return parsePipfileLock(text);
|
|
233
|
+
}
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
function parsePackageLock(text) {
|
|
237
|
+
let parsed;
|
|
238
|
+
try {
|
|
239
|
+
parsed = JSON.parse(text);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
return [malformedLockfileObservation("package-lock.json", error)];
|
|
243
|
+
}
|
|
244
|
+
if (!isRecord(parsed)) {
|
|
245
|
+
return [malformedLockfileObservation("package-lock.json", new Error("root must be an object"))];
|
|
246
|
+
}
|
|
247
|
+
const observations = [];
|
|
248
|
+
const packages = isRecord(parsed.packages) ? parsed.packages : {};
|
|
249
|
+
for (const [path, rawPackage] of Object.entries(packages)) {
|
|
250
|
+
if (path.length === 0 || !isRecord(rawPackage)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const name = typeof rawPackage.name === "string" ? rawPackage.name : packageNameFromNodeModulesPath(path);
|
|
254
|
+
const version = typeof rawPackage.version === "string" ? rawPackage.version : null;
|
|
255
|
+
if (!name) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
observations.push(lockfileObservation({
|
|
259
|
+
ecosystem: "npm",
|
|
260
|
+
name,
|
|
261
|
+
version,
|
|
262
|
+
requested: path,
|
|
263
|
+
sourceKind: "lockfile",
|
|
264
|
+
resolvedUrl: stringOrNull(rawPackage.resolved),
|
|
265
|
+
integrity: stringOrNull(rawPackage.integrity),
|
|
266
|
+
license: stringOrNull(rawPackage.license)
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
const dependencies = isRecord(parsed.dependencies) ? parsed.dependencies : {};
|
|
270
|
+
for (const [name, rawPackage] of Object.entries(dependencies)) {
|
|
271
|
+
if (!isRecord(rawPackage) || observations.some((observation) => observation.identity.name === name)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
observations.push(lockfileObservation({
|
|
275
|
+
ecosystem: "npm",
|
|
276
|
+
name,
|
|
277
|
+
version: stringOrNull(rawPackage.version),
|
|
278
|
+
requested: name,
|
|
279
|
+
sourceKind: "lockfile",
|
|
280
|
+
resolvedUrl: stringOrNull(rawPackage.resolved),
|
|
281
|
+
integrity: stringOrNull(rawPackage.integrity),
|
|
282
|
+
license: stringOrNull(rawPackage.license)
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
return observations;
|
|
286
|
+
}
|
|
287
|
+
function parseYarnLock(text) {
|
|
288
|
+
const observations = [];
|
|
289
|
+
const blocks = text.split(/\n(?=(?:"?@?[^"\s].*"?):\n)/u);
|
|
290
|
+
for (const block of blocks) {
|
|
291
|
+
const lines = block.split(/\r?\n/u);
|
|
292
|
+
const header = lines[0]?.trim().replace(/:$/u, "");
|
|
293
|
+
if (!header) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const requested = header.split(",")[0]?.trim().replace(/^"|"$/gu, "") ?? header;
|
|
297
|
+
const name = packageNameFromYarnDescriptor(requested);
|
|
298
|
+
if (!name) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const version = quotedValue(lines, "version");
|
|
302
|
+
const resolvedUrl = quotedValue(lines, "resolved");
|
|
303
|
+
const integrity = quotedValue(lines, "integrity");
|
|
304
|
+
observations.push(lockfileObservation({
|
|
305
|
+
ecosystem: "npm",
|
|
306
|
+
name,
|
|
307
|
+
version,
|
|
308
|
+
requested,
|
|
309
|
+
sourceKind: "lockfile",
|
|
310
|
+
resolvedUrl,
|
|
311
|
+
integrity,
|
|
312
|
+
license: null
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
return observations;
|
|
316
|
+
}
|
|
317
|
+
function parsePnpmLock(text) {
|
|
318
|
+
const observations = [];
|
|
319
|
+
const lines = text.split(/\r?\n/u);
|
|
320
|
+
let current = null;
|
|
321
|
+
for (const line of lines) {
|
|
322
|
+
const packageMatch = /^\s{2}['"]?(?:\/)?((?:@[^/\s]+\/)?[^@\s:'"]+)@([^:\s'"]+)['"]?:\s*$/u.exec(line);
|
|
323
|
+
if (packageMatch?.[1] && packageMatch[2]) {
|
|
324
|
+
if (current) {
|
|
325
|
+
observations.push(lockfileObservation(current));
|
|
326
|
+
}
|
|
327
|
+
current = {
|
|
328
|
+
ecosystem: "npm",
|
|
329
|
+
name: packageMatch[1],
|
|
330
|
+
version: packageMatch[2],
|
|
331
|
+
requested: `${packageMatch[1]}@${packageMatch[2]}`,
|
|
332
|
+
sourceKind: "lockfile",
|
|
333
|
+
resolvedUrl: null,
|
|
334
|
+
integrity: null,
|
|
335
|
+
license: null
|
|
336
|
+
};
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (!current) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const integrityMatch = /integrity:\s*([^,}\s]+)/u.exec(line);
|
|
343
|
+
const tarballMatch = /tarball:\s*([^,}\s]+)/u.exec(line);
|
|
344
|
+
if (integrityMatch?.[1]) {
|
|
345
|
+
current = {
|
|
346
|
+
...current,
|
|
347
|
+
integrity: stripQuotes(integrityMatch[1])
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (tarballMatch?.[1]) {
|
|
351
|
+
current = {
|
|
352
|
+
...current,
|
|
353
|
+
resolvedUrl: stripQuotes(tarballMatch[1])
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (current) {
|
|
358
|
+
observations.push(lockfileObservation(current));
|
|
359
|
+
}
|
|
360
|
+
return observations;
|
|
361
|
+
}
|
|
362
|
+
function parseRequirements(text) {
|
|
363
|
+
return text.split(/\r?\n/u)
|
|
364
|
+
.map((line) => line.trim())
|
|
365
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"))
|
|
366
|
+
.map((line) => {
|
|
367
|
+
if (REMOTE_SPEC_PREFIXES.some((prefix) => line.toLowerCase().startsWith(prefix))) {
|
|
368
|
+
return packageObservation({
|
|
369
|
+
ecosystem: "pypi",
|
|
370
|
+
name: line,
|
|
371
|
+
version: null,
|
|
372
|
+
requested: line,
|
|
373
|
+
sourceKind: "lockfile-url-fallback",
|
|
374
|
+
resolvedUrl: line,
|
|
375
|
+
integrity: null,
|
|
376
|
+
license: null
|
|
377
|
+
}, "block", {
|
|
378
|
+
id: "lockfile-url-fallback",
|
|
379
|
+
title: "Lockfile URL fallback identity",
|
|
380
|
+
message: "lockfile entry uses a URL without package identity or hash metadata",
|
|
381
|
+
location: line
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const hash = /--hash=([A-Za-z0-9:-]+)/u.exec(line)?.[1] ?? null;
|
|
385
|
+
const cleanLine = line.replace(/\s+--hash=[^\s]+/gu, "");
|
|
386
|
+
const match = /^([A-Za-z0-9_.-]+)(?:==([^;\s]+))?/u.exec(cleanLine);
|
|
387
|
+
if (!match?.[1]) {
|
|
388
|
+
return blockedUnknownSpec(line);
|
|
389
|
+
}
|
|
390
|
+
return lockfileObservation({
|
|
391
|
+
ecosystem: "pypi",
|
|
392
|
+
name: match[1],
|
|
393
|
+
version: match[2] ?? null,
|
|
394
|
+
requested: line,
|
|
395
|
+
sourceKind: "lockfile",
|
|
396
|
+
resolvedUrl: null,
|
|
397
|
+
integrity: hash,
|
|
398
|
+
license: null
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function parseCargoLock(text) {
|
|
403
|
+
return lockBlocks(text, "[[package]]").map((block) => {
|
|
404
|
+
const name = tomlString(block, "name") ?? "unknown";
|
|
405
|
+
const version = tomlString(block, "version");
|
|
406
|
+
return lockfileObservation({
|
|
407
|
+
ecosystem: "cargo",
|
|
408
|
+
name,
|
|
409
|
+
version,
|
|
410
|
+
requested: `${name}@${version ?? "unknown"}`,
|
|
411
|
+
sourceKind: "lockfile",
|
|
412
|
+
resolvedUrl: tomlString(block, "source"),
|
|
413
|
+
integrity: tomlString(block, "checksum"),
|
|
414
|
+
license: null
|
|
415
|
+
});
|
|
416
|
+
}).filter((observation) => observation.identity.name !== "unknown");
|
|
417
|
+
}
|
|
418
|
+
function parsePoetryLock(text) {
|
|
419
|
+
return lockBlocks(text, "[[package]]").map((block) => {
|
|
420
|
+
const name = tomlString(block, "name") ?? "unknown";
|
|
421
|
+
const version = tomlString(block, "version");
|
|
422
|
+
return lockfileObservation({
|
|
423
|
+
ecosystem: "pypi",
|
|
424
|
+
name,
|
|
425
|
+
version,
|
|
426
|
+
requested: `${name}==${version ?? "unknown"}`,
|
|
427
|
+
sourceKind: "lockfile",
|
|
428
|
+
resolvedUrl: null,
|
|
429
|
+
integrity: null,
|
|
430
|
+
license: tomlString(block, "license")
|
|
431
|
+
});
|
|
432
|
+
}).filter((observation) => observation.identity.name !== "unknown");
|
|
433
|
+
}
|
|
434
|
+
function parsePipfileLock(text) {
|
|
435
|
+
let parsed;
|
|
436
|
+
try {
|
|
437
|
+
parsed = JSON.parse(text);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
return [malformedLockfileObservation("Pipfile.lock", error)];
|
|
441
|
+
}
|
|
442
|
+
if (!isRecord(parsed)) {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
return ["default", "develop"].flatMap((section) => {
|
|
446
|
+
const packages = isRecord(parsed[section]) ? parsed[section] : {};
|
|
447
|
+
return Object.entries(packages).map(([name, rawPackage]) => {
|
|
448
|
+
const record = isRecord(rawPackage) ? rawPackage : {};
|
|
449
|
+
const version = stringOrNull(record.version)?.replace(/^==/u, "") ?? null;
|
|
450
|
+
const hashes = Array.isArray(record.hashes) ? record.hashes.filter((hash) => typeof hash === "string") : [];
|
|
451
|
+
return lockfileObservation({
|
|
452
|
+
ecosystem: "pypi",
|
|
453
|
+
name,
|
|
454
|
+
version,
|
|
455
|
+
requested: `${name}${version ? `==${version}` : ""}`,
|
|
456
|
+
sourceKind: "lockfile",
|
|
457
|
+
resolvedUrl: null,
|
|
458
|
+
integrity: hashes[0] ?? null,
|
|
459
|
+
license: null
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function lockfileObservation(identity) {
|
|
465
|
+
if (identity.resolvedUrl && isUnsafeResolvedUrl(identity.resolvedUrl)) {
|
|
466
|
+
return packageObservation(identity, "block", {
|
|
467
|
+
id: "unverified-lockfile-url",
|
|
468
|
+
title: "Unverified lockfile URL",
|
|
469
|
+
message: "lockfile resolved artifact uses a direct URL or git source that requires proxy hash verification",
|
|
470
|
+
location: identity.requested
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
const integrityFinding = integrityPolicyFinding(identity);
|
|
474
|
+
if (integrityFinding) {
|
|
475
|
+
return packageObservation(identity, "warn", integrityFinding);
|
|
476
|
+
}
|
|
477
|
+
return packageObservation(identity, "pass", null);
|
|
478
|
+
}
|
|
479
|
+
function integrityPolicyFinding(identity) {
|
|
480
|
+
if (identity.integrity && isSupportedIntegrity(identity.integrity)) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
id: "missing-artifact-integrity",
|
|
485
|
+
title: "Missing artifact integrity",
|
|
486
|
+
message: `${packageDisplayName(identity)} has no supported lockfile integrity or checksum metadata`,
|
|
487
|
+
location: identity.requested
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function exactVersionFinding(name, version, location) {
|
|
491
|
+
if (version && isExactVersion(version)) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
id: "unpinned-package-spec",
|
|
496
|
+
title: "Unpinned package spec",
|
|
497
|
+
message: `${name} is not pinned to an exact package version`,
|
|
498
|
+
location
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function exactVersionVerdict(version) {
|
|
502
|
+
return version && isExactVersion(version) ? "pass" : "warn";
|
|
503
|
+
}
|
|
504
|
+
function deniedLicenseFinding(identity, deniedLicenses) {
|
|
505
|
+
if (!identity.license || !deniedLicenses.has(normalizeLicense(identity.license))) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
id: "license-policy-denied",
|
|
510
|
+
title: "Denied package license",
|
|
511
|
+
message: `${packageDisplayName(identity)} declares denied license '${identity.license}'`,
|
|
512
|
+
location: identity.requested
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function malformedLockfileObservation(lockfile, error) {
|
|
516
|
+
return packageObservation({
|
|
517
|
+
ecosystem: "unknown",
|
|
518
|
+
name: lockfile,
|
|
519
|
+
version: null,
|
|
520
|
+
requested: lockfile,
|
|
521
|
+
sourceKind: "lockfile",
|
|
522
|
+
resolvedUrl: null,
|
|
523
|
+
integrity: null,
|
|
524
|
+
license: null
|
|
525
|
+
}, "block", {
|
|
526
|
+
id: "malformed-lockfile",
|
|
527
|
+
title: "Malformed lockfile",
|
|
528
|
+
message: error instanceof Error ? error.message : "lockfile could not be parsed",
|
|
529
|
+
location: lockfile
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
function blockedUnknownSpec(spec) {
|
|
533
|
+
return packageObservation({
|
|
534
|
+
ecosystem: "unknown",
|
|
535
|
+
name: spec,
|
|
536
|
+
version: null,
|
|
537
|
+
requested: spec,
|
|
538
|
+
sourceKind: "package-spec",
|
|
539
|
+
resolvedUrl: null,
|
|
540
|
+
integrity: null,
|
|
541
|
+
license: null
|
|
542
|
+
}, "block", {
|
|
543
|
+
id: "unsupported-package-spec",
|
|
544
|
+
title: "Unsupported package spec",
|
|
545
|
+
message: "package spec could not be parsed without guessing identity",
|
|
546
|
+
location: spec
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function packageObservation(identity, verdict, finding) {
|
|
550
|
+
return {
|
|
551
|
+
identity,
|
|
552
|
+
verdict,
|
|
553
|
+
finding
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function splitNameVersion(value) {
|
|
557
|
+
const trimmed = value.trim();
|
|
558
|
+
if (trimmed.startsWith("@")) {
|
|
559
|
+
const index = trimmed.lastIndexOf("@");
|
|
560
|
+
if (index <= 0) {
|
|
561
|
+
return {
|
|
562
|
+
name: trimmed,
|
|
563
|
+
version: null
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
name: trimmed.slice(0, index),
|
|
568
|
+
version: trimmed.slice(index + 1) || null
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const index = trimmed.lastIndexOf("@");
|
|
572
|
+
if (index > 0) {
|
|
573
|
+
return {
|
|
574
|
+
name: trimmed.slice(0, index),
|
|
575
|
+
version: trimmed.slice(index + 1) || null
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
name: trimmed,
|
|
580
|
+
version: null
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function isValidPackageName(name) {
|
|
584
|
+
return /^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+$/iu.test(name) && !/[\\\s]/u.test(name);
|
|
585
|
+
}
|
|
586
|
+
function isExactVersion(version) {
|
|
587
|
+
return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(version);
|
|
588
|
+
}
|
|
589
|
+
function isSupportedIntegrity(value) {
|
|
590
|
+
return /^(?:sha256|sha384|sha512)-[A-Za-z0-9+/=]+$/u.test(value)
|
|
591
|
+
|| /^(?:sha256:)?[a-f0-9]{64}$/iu.test(value);
|
|
592
|
+
}
|
|
593
|
+
function isUnsafeResolvedUrl(value) {
|
|
594
|
+
const lower = value.toLowerCase();
|
|
595
|
+
return lower.startsWith("git+")
|
|
596
|
+
|| lower.startsWith("git://")
|
|
597
|
+
|| lower.startsWith("ssh://")
|
|
598
|
+
|| lower.startsWith("github:");
|
|
599
|
+
}
|
|
600
|
+
function strongerVerdict(left, right) {
|
|
601
|
+
if (left === "block" || right === "block") {
|
|
602
|
+
return "block";
|
|
603
|
+
}
|
|
604
|
+
if (left === "warn" || right === "warn") {
|
|
605
|
+
return "warn";
|
|
606
|
+
}
|
|
607
|
+
return "pass";
|
|
608
|
+
}
|
|
609
|
+
function policyActionFor(findings) {
|
|
610
|
+
if (findings.some((finding) => finding.severity === "block")) {
|
|
611
|
+
return "block";
|
|
612
|
+
}
|
|
613
|
+
if (findings.some((finding) => finding.severity === "warn")) {
|
|
614
|
+
return "warn";
|
|
615
|
+
}
|
|
616
|
+
return "pass";
|
|
617
|
+
}
|
|
618
|
+
function statusFor(action, errors) {
|
|
619
|
+
if (errors.length > 0) {
|
|
620
|
+
return "error";
|
|
621
|
+
}
|
|
622
|
+
return action;
|
|
623
|
+
}
|
|
624
|
+
function summarize(findings, errors) {
|
|
625
|
+
return {
|
|
626
|
+
findingCount: findings.length,
|
|
627
|
+
warnCount: findings.filter((finding) => finding.severity === "warn").length,
|
|
628
|
+
blockCount: findings.filter((finding) => finding.severity === "block").length,
|
|
629
|
+
errorCount: errors.length
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function lockfileSummary(packageCount) {
|
|
633
|
+
return {
|
|
634
|
+
advisory: true,
|
|
635
|
+
packageCount,
|
|
636
|
+
identitySource: "lockfile",
|
|
637
|
+
message: "Lockfile verification maps package identity and integrity for preflight only; proxy enforcement remains authoritative for network fetches."
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function packageDisplayName(identity) {
|
|
641
|
+
const version = identity.version ? `@${identity.version}` : "";
|
|
642
|
+
return `${identity.ecosystem}:${identity.name}${version}`;
|
|
643
|
+
}
|
|
644
|
+
function packageNameFromNodeModulesPath(path) {
|
|
645
|
+
const parts = path.split("/");
|
|
646
|
+
const nodeModulesIndex = parts.lastIndexOf("node_modules");
|
|
647
|
+
if (nodeModulesIndex === -1) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const first = parts[nodeModulesIndex + 1];
|
|
651
|
+
if (!first) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
if (first.startsWith("@") && parts[nodeModulesIndex + 2]) {
|
|
655
|
+
return `${first}/${parts[nodeModulesIndex + 2]}`;
|
|
656
|
+
}
|
|
657
|
+
return first;
|
|
658
|
+
}
|
|
659
|
+
function packageNameFromYarnDescriptor(descriptor) {
|
|
660
|
+
const value = descriptor.startsWith("@") ? descriptor.slice(1) : descriptor;
|
|
661
|
+
const index = value.lastIndexOf("@");
|
|
662
|
+
const name = descriptor.startsWith("@") ? `@${value.slice(0, index)}` : value.slice(0, index);
|
|
663
|
+
return name || null;
|
|
664
|
+
}
|
|
665
|
+
function quotedValue(lines, key) {
|
|
666
|
+
const pattern = new RegExp(`^\\s*${key}\\s+\"?([^\"\\n]+)\"?\\s*$`, "u");
|
|
667
|
+
for (const line of lines) {
|
|
668
|
+
const match = pattern.exec(line);
|
|
669
|
+
if (match?.[1]) {
|
|
670
|
+
return match[1];
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
function lockBlocks(text, marker) {
|
|
676
|
+
return text.split(marker).slice(1).map((block) => `${marker}${block}`);
|
|
677
|
+
}
|
|
678
|
+
function tomlString(block, key) {
|
|
679
|
+
const match = new RegExp(`^${key}\\s*=\\s*\"([^\"]*)\"`, "mu").exec(block);
|
|
680
|
+
return match?.[1] ?? null;
|
|
681
|
+
}
|
|
682
|
+
function stringOrNull(value) {
|
|
683
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
684
|
+
}
|
|
685
|
+
function stripQuotes(value) {
|
|
686
|
+
return value.replace(/^["']|["']$/gu, "");
|
|
687
|
+
}
|
|
688
|
+
function normalizeLicense(value) {
|
|
689
|
+
return value.trim().toLowerCase();
|
|
690
|
+
}
|
|
691
|
+
function displayPath(root, path) {
|
|
692
|
+
const relativePath = relative(resolve(root), resolve(path));
|
|
693
|
+
const display = relativePath.length === 0 ? "." : relativePath;
|
|
694
|
+
return display.split(sep).join("/");
|
|
695
|
+
}
|
|
696
|
+
function isRecord(value) {
|
|
697
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
698
|
+
}
|