@westbayberry/dg 1.0.53 → 1.0.56
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 +5 -1
- package/dist/index.mjs +249 -114
- package/dist/packages/cli/src/alt-screen.js +36 -0
- package/dist/packages/cli/src/api.js +322 -0
- package/dist/packages/cli/src/auth.js +218 -0
- package/dist/packages/cli/src/bin.js +386 -0
- package/dist/packages/cli/src/config.js +228 -0
- package/dist/packages/cli/src/discover.js +126 -0
- package/dist/packages/cli/src/first-run.js +135 -0
- package/dist/packages/cli/src/hook.js +360 -0
- package/dist/packages/cli/src/lockfile.js +303 -0
- package/dist/packages/cli/src/npm-wrapper.js +218 -0
- package/dist/packages/cli/src/pip-wrapper.js +273 -0
- package/dist/packages/cli/src/sanitize.js +38 -0
- package/dist/packages/cli/src/scan-core.js +144 -0
- package/dist/packages/cli/src/setup-status.js +46 -0
- package/dist/packages/cli/src/static-output.js +625 -0
- package/dist/packages/cli/src/telemetry.js +141 -0
- package/dist/packages/cli/src/ui/App.js +137 -0
- package/dist/packages/cli/src/ui/InitApp.js +391 -0
- package/dist/packages/cli/src/ui/LoginApp.js +51 -0
- package/dist/packages/cli/src/ui/NpmWrapperApp.js +73 -0
- package/dist/packages/cli/src/ui/PipWrapperApp.js +72 -0
- package/dist/packages/cli/src/ui/components/ConfirmPrompt.js +24 -0
- package/dist/packages/cli/src/ui/components/DemoScanAnimation.js +26 -0
- package/dist/packages/cli/src/ui/components/DurationLine.js +7 -0
- package/dist/packages/cli/src/ui/components/ErrorView.js +30 -0
- package/dist/packages/cli/src/ui/components/FileSavePrompt.js +210 -0
- package/dist/packages/cli/src/ui/components/InteractiveResultsView.js +557 -0
- package/dist/packages/cli/src/ui/components/Mascot.js +33 -0
- package/dist/packages/cli/src/ui/components/ProgressBar.js +51 -0
- package/dist/packages/cli/src/ui/components/ProgressDots.js +35 -0
- package/dist/packages/cli/src/ui/components/ProjectSelector.js +60 -0
- package/dist/packages/cli/src/ui/components/ResultsView.js +105 -0
- package/dist/packages/cli/src/ui/components/ScanResultCard.js +54 -0
- package/dist/packages/cli/src/ui/components/ScoreHeader.js +142 -0
- package/dist/packages/cli/src/ui/components/SetupBanner.js +17 -0
- package/dist/packages/cli/src/ui/components/Spinner.js +11 -0
- package/dist/packages/cli/src/ui/hooks/useExpandAnimation.js +44 -0
- package/dist/packages/cli/src/ui/hooks/useInit.js +341 -0
- package/dist/packages/cli/src/ui/hooks/useLogin.js +121 -0
- package/dist/packages/cli/src/ui/hooks/useNpmWrapper.js +192 -0
- package/dist/packages/cli/src/ui/hooks/usePipWrapper.js +195 -0
- package/dist/packages/cli/src/ui/hooks/useScan.js +202 -0
- package/dist/packages/cli/src/ui/hooks/useTerminalSize.js +29 -0
- package/dist/packages/cli/src/update-check.js +152 -0
- package/dist/packages/cli/src/wizard-demo-data.js +63 -0
- package/dist/src/ecosystem.js +2 -0
- package/dist/src/lockfile/diff.js +38 -0
- package/dist/src/lockfile/parse_package_json.js +41 -0
- package/dist/src/lockfile/parse_package_lock.js +55 -0
- package/dist/src/lockfile/parse_pipfile_lock.js +69 -0
- package/dist/src/lockfile/parse_pnpm_lock.js +62 -0
- package/dist/src/lockfile/parse_poetry_lock.js +71 -0
- package/dist/src/lockfile/parse_requirements.js +83 -0
- package/dist/src/lockfile/parse_yarn_lock.js +66 -0
- package/dist/src/logger.js +21 -0
- package/dist/src/npm/h2pool.js +161 -0
- package/dist/src/npm/registry.js +299 -0
- package/dist/src/npm/tarball.js +274 -0
- package/dist/src/pypi/registry.js +299 -0
- package/dist/src/pypi/tarball.js +361 -0
- package/dist/src/types.js +2 -0
- package/package.json +6 -3
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.renderResultStatic = renderResultStatic;
|
|
40
|
+
exports.runStatic = runStatic;
|
|
41
|
+
exports.runStaticNpm = runStaticNpm;
|
|
42
|
+
exports.runStaticPip = runStaticPip;
|
|
43
|
+
exports.runStaticLogin = runStaticLogin;
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
const node_fs_1 = require("node:fs");
|
|
46
|
+
const node_path_1 = require("node:path");
|
|
47
|
+
const api_1 = require("./api");
|
|
48
|
+
const auth_1 = require("./auth");
|
|
49
|
+
const lockfile_1 = require("./lockfile");
|
|
50
|
+
const npm_wrapper_1 = require("./npm-wrapper");
|
|
51
|
+
const pip_wrapper_1 = require("./pip-wrapper");
|
|
52
|
+
// ── JSON output handling ────────────────────────────────────
|
|
53
|
+
function writeJsonFile(filepath, json) {
|
|
54
|
+
const resolved = (0, node_path_1.resolve)(filepath);
|
|
55
|
+
try {
|
|
56
|
+
(0, node_fs_1.writeFileSync)(resolved, json + "\n");
|
|
57
|
+
process.stderr.write(chalk_1.default.green(` \u2713 Saved to ${resolved}\n\n`));
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
process.stderr.write(chalk_1.default.red(` Error saving file: ${msg}\n\n`));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function handleJsonOutput(result, config) {
|
|
65
|
+
if (!config.json)
|
|
66
|
+
return false;
|
|
67
|
+
const json = JSON.stringify(result, null, 2);
|
|
68
|
+
// Explicit --output: save directly (works in any context)
|
|
69
|
+
if (config.outputFile) {
|
|
70
|
+
writeJsonFile(config.outputFile, json);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// Non-TTY (piped): raw JSON to stdout
|
|
74
|
+
if (!process.stdout.isTTY) {
|
|
75
|
+
process.stdout.write(json + "\n");
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
// TTY interactive: launch file save TUI
|
|
79
|
+
const localDate = new Date().toLocaleDateString("sv-SE");
|
|
80
|
+
const defaultName = `dg-scan-${localDate}`;
|
|
81
|
+
const { render } = await Promise.resolve().then(() => __importStar(require("ink")));
|
|
82
|
+
const React = await Promise.resolve().then(() => __importStar(require("react")));
|
|
83
|
+
const { FileSavePrompt } = await Promise.resolve().then(() => __importStar(require("./ui/components/FileSavePrompt")));
|
|
84
|
+
await new Promise((resolve) => {
|
|
85
|
+
const inkInstance = render(React.createElement(FileSavePrompt, {
|
|
86
|
+
defaultName,
|
|
87
|
+
directory: process.cwd(),
|
|
88
|
+
score: result.score,
|
|
89
|
+
action: result.action,
|
|
90
|
+
packageCount: result.packages.length,
|
|
91
|
+
json,
|
|
92
|
+
onSaved: (filepath) => {
|
|
93
|
+
inkInstance.unmount();
|
|
94
|
+
process.stderr.write(chalk_1.default.green(` \u2713 Saved to ${filepath}\n\n`));
|
|
95
|
+
resolve();
|
|
96
|
+
},
|
|
97
|
+
onCancel: () => {
|
|
98
|
+
inkInstance.unmount();
|
|
99
|
+
process.stderr.write(chalk_1.default.dim(" Cancelled.\n\n"));
|
|
100
|
+
resolve();
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
// ── Helpers ─────────────────────────────────────────────────
|
|
107
|
+
function printTrialBanner(result) {
|
|
108
|
+
if (result.trialScansRemaining === undefined)
|
|
109
|
+
return;
|
|
110
|
+
process.stderr.write(chalk_1.default.dim(` Free tier \u00b7 Run \`dg login\` for higher scan limits.\n`));
|
|
111
|
+
}
|
|
112
|
+
function handleTrialExhausted(error, jsonMode = false) {
|
|
113
|
+
if (error instanceof api_1.TrialExhaustedError) {
|
|
114
|
+
// Check if user has stored credentials — if so, their key is likely invalid
|
|
115
|
+
let hasKey = false;
|
|
116
|
+
try {
|
|
117
|
+
hasKey = !!(0, auth_1.getStoredApiKey)();
|
|
118
|
+
}
|
|
119
|
+
catch { /* ignore — best-effort */ }
|
|
120
|
+
const message = hasKey
|
|
121
|
+
? "Your API key may be invalid or expired. Run `dg logout` then `dg login` to re-authenticate."
|
|
122
|
+
: "Monthly scan limit reached. Run `dg login` to create a free account for higher limits.";
|
|
123
|
+
if (jsonMode) {
|
|
124
|
+
process.stdout.write(JSON.stringify({
|
|
125
|
+
error: true,
|
|
126
|
+
code: "trial_exhausted",
|
|
127
|
+
message,
|
|
128
|
+
scansUsed: error.scansUsed,
|
|
129
|
+
maxScans: error.maxScans,
|
|
130
|
+
}, null, 2) + "\n");
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
if (hasKey) {
|
|
134
|
+
process.stderr.write(chalk_1.default.yellow(`\n ${message}\n\n`));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
process.stderr.write(chalk_1.default.yellow("\n Monthly scan limit reached (1,000 scans/month on free tier).\n") +
|
|
138
|
+
chalk_1.default.white(" Run `dg login` for higher limits, or upgrade at westbayberry.com/pricing\n\n"));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
process.exit(1);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
function actionColor(action) {
|
|
147
|
+
if (action === "block")
|
|
148
|
+
return chalk_1.default.red;
|
|
149
|
+
if (action === "warn")
|
|
150
|
+
return chalk_1.default.yellow;
|
|
151
|
+
return chalk_1.default.green;
|
|
152
|
+
}
|
|
153
|
+
function pad(s, len) {
|
|
154
|
+
return s + " ".repeat(Math.max(0, len - s.length));
|
|
155
|
+
}
|
|
156
|
+
function truncate(s, max) {
|
|
157
|
+
if (s.length <= max)
|
|
158
|
+
return s;
|
|
159
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
160
|
+
}
|
|
161
|
+
function groupPackages(packages) {
|
|
162
|
+
const map = new Map();
|
|
163
|
+
for (const pkg of packages) {
|
|
164
|
+
const fingerprint = pkg.findings.length === 0
|
|
165
|
+
? `__clean_${pkg.score}`
|
|
166
|
+
: pkg.findings
|
|
167
|
+
.map((f) => `${f.category ?? ""}:${f.severity}`)
|
|
168
|
+
.sort()
|
|
169
|
+
.join("|") + `|score:${pkg.score}`;
|
|
170
|
+
const group = map.get(fingerprint) ?? [];
|
|
171
|
+
group.push(pkg);
|
|
172
|
+
map.set(fingerprint, group);
|
|
173
|
+
}
|
|
174
|
+
return [...map.values()]
|
|
175
|
+
.map((pkgs) => ({ packages: pkgs, key: pkgs[0].name }))
|
|
176
|
+
.sort((a, b) => b.packages[0].score - a.packages[0].score);
|
|
177
|
+
}
|
|
178
|
+
function actionBadge(score) {
|
|
179
|
+
if (score >= 70)
|
|
180
|
+
return { label: "Block", color: chalk_1.default.red };
|
|
181
|
+
if (score >= 60)
|
|
182
|
+
return { label: "Warn", color: chalk_1.default.yellow };
|
|
183
|
+
return { label: "Pass", color: chalk_1.default.green };
|
|
184
|
+
}
|
|
185
|
+
function renderResultStatic(result, _config) {
|
|
186
|
+
const lines = [];
|
|
187
|
+
const actionStr = result.action.toUpperCase();
|
|
188
|
+
const colorAction = actionColor(result.action);
|
|
189
|
+
// Header
|
|
190
|
+
lines.push("");
|
|
191
|
+
lines.push(` ${chalk_1.default.bold("Dependency Guardian")}`);
|
|
192
|
+
lines.push(` Score: ${chalk_1.default.bold(String(result.score))}${" ".repeat(Math.max(1, 50 - String(result.score).length))}${colorAction(actionStr)}`);
|
|
193
|
+
lines.push("");
|
|
194
|
+
// Summary
|
|
195
|
+
const flagged = result.packages.filter((p) => p.score > 0);
|
|
196
|
+
const clean = result.packages.filter((p) => p.score === 0);
|
|
197
|
+
const total = result.packages.length;
|
|
198
|
+
if (flagged.length > 0) {
|
|
199
|
+
lines.push(` ${total} package${total !== 1 ? "s" : ""} scanned ${chalk_1.default.dim("\u2502")} ${chalk_1.default.yellow(`${flagged.length} flagged`)} \u2502 ${chalk_1.default.green(`${clean.length} clean`)}`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
lines.push(` ${total} package${total !== 1 ? "s" : ""} scanned ${chalk_1.default.dim("\u2502")} ${chalk_1.default.green("all clean")}`);
|
|
203
|
+
}
|
|
204
|
+
// License summary
|
|
205
|
+
const licensedPkgs = result.packages.filter((p) => p.license);
|
|
206
|
+
const licenseIssues = licensedPkgs.filter((p) => p.license && p.license.riskCategory !== "permissive");
|
|
207
|
+
if (licenseIssues.length > 0) {
|
|
208
|
+
lines.push(` ${chalk_1.default.dim("Licenses:")} ${chalk_1.default.yellow(`${licenseIssues.length} need review`)} \u2502 ${chalk_1.default.green(`${licensedPkgs.length - licenseIssues.length} permissive`)}`);
|
|
209
|
+
}
|
|
210
|
+
lines.push("");
|
|
211
|
+
if (total === 0) {
|
|
212
|
+
lines.push(" No packages to scan.");
|
|
213
|
+
lines.push("");
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
216
|
+
// Group flagged packages
|
|
217
|
+
const groups = groupPackages(flagged);
|
|
218
|
+
// Flagged packages table (only non-zero scores)
|
|
219
|
+
if (flagged.length > 0) {
|
|
220
|
+
lines.push(` ${chalk_1.default.bold("Flagged Packages")}`);
|
|
221
|
+
lines.push(` ${chalk_1.default.dim("\u2500".repeat(60))}`);
|
|
222
|
+
for (const group of groups) {
|
|
223
|
+
const rep = group.packages[0];
|
|
224
|
+
const { label, color } = actionBadge(rep.score);
|
|
225
|
+
const names = group.packages.length === 1
|
|
226
|
+
? rep.name
|
|
227
|
+
: group.packages.length <= 3
|
|
228
|
+
? group.packages.map((p) => p.name).join(", ")
|
|
229
|
+
: `${group.packages[0].name} + ${group.packages.length - 1} similar`;
|
|
230
|
+
const lcInfo = rep.license;
|
|
231
|
+
const lcStr = lcInfo ? truncate(lcInfo.spdx ?? lcInfo.raw ?? "", 14) : "";
|
|
232
|
+
const lcColor = !lcInfo ? chalk_1.default.dim
|
|
233
|
+
: lcInfo.riskCategory === "permissive" ? chalk_1.default.green
|
|
234
|
+
: (lcInfo.riskCategory === "no-license" || lcInfo.riskCategory === "unlicensed" || lcInfo.riskCategory === "network-copyleft") ? chalk_1.default.red
|
|
235
|
+
: chalk_1.default.yellow;
|
|
236
|
+
lines.push(` ${color(pad(label, 7))}${chalk_1.default.bold(pad(truncate(names, 36), 38))}${lcColor(pad(lcStr, 16))}${chalk_1.default.dim(`score ${rep.score}`)}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push("");
|
|
239
|
+
}
|
|
240
|
+
// Clean packages (collapsed to single line)
|
|
241
|
+
if (clean.length > 0) {
|
|
242
|
+
lines.push(` ${chalk_1.default.green("\u2713")} ${chalk_1.default.dim(`${clean.length} package${clean.length !== 1 ? "s" : ""} passed with score 0`)}`);
|
|
243
|
+
lines.push("");
|
|
244
|
+
}
|
|
245
|
+
// Duration
|
|
246
|
+
if (result.durationMs) {
|
|
247
|
+
lines.push(` ${chalk_1.default.dim(`Completed in ${(result.durationMs / 1000).toFixed(1)}s`)}`);
|
|
248
|
+
lines.push("");
|
|
249
|
+
}
|
|
250
|
+
return lines.join("\n");
|
|
251
|
+
}
|
|
252
|
+
async function runStatic(config) {
|
|
253
|
+
const dbg = (msg) => {
|
|
254
|
+
if (config.debug)
|
|
255
|
+
process.stderr.write(chalk_1.default.dim(` [debug] ${msg}\n`));
|
|
256
|
+
};
|
|
257
|
+
if (config.mode === "off") {
|
|
258
|
+
process.stderr.write(chalk_1.default.dim(" Dependency Guardian: mode is off — skipping.\n"));
|
|
259
|
+
process.exit(0);
|
|
260
|
+
}
|
|
261
|
+
dbg(`mode=${config.mode} max=${config.maxPackages}`);
|
|
262
|
+
dbg(`api=${config.apiUrl}`);
|
|
263
|
+
// Discover packages
|
|
264
|
+
process.stderr.write(chalk_1.default.dim(" Discovering package changes...\n"));
|
|
265
|
+
let discovery = (0, lockfile_1.discoverChanges)(process.cwd(), config);
|
|
266
|
+
dbg(`discovery method: ${discovery.method}`);
|
|
267
|
+
if (discovery.pythonPackages.length > 0) {
|
|
268
|
+
dbg(`python packages: ${discovery.pythonPackages.length}`);
|
|
269
|
+
}
|
|
270
|
+
if (discovery.packages.length === 0 && discovery.pythonPackages.length === 0) {
|
|
271
|
+
// Monorepo: auto-discover and scan all subprojects
|
|
272
|
+
const { discoverProjects } = await Promise.resolve().then(() => __importStar(require("./discover")));
|
|
273
|
+
const subProjects = discoverProjects(process.cwd());
|
|
274
|
+
if (subProjects.length === 0) {
|
|
275
|
+
process.stderr.write(chalk_1.default.dim(" No package changes detected.\n"));
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
const totalPkgs = subProjects.reduce((s, p) => s + p.packageCount, 0);
|
|
279
|
+
process.stderr.write(chalk_1.default.dim(` Found ${subProjects.length} projects (${totalPkgs} packages total):\n`));
|
|
280
|
+
for (const proj of subProjects) {
|
|
281
|
+
process.stderr.write(chalk_1.default.dim(` ${proj.ecosystem} ${proj.relativePath} (${proj.packageCount})\n`));
|
|
282
|
+
}
|
|
283
|
+
process.stderr.write("\n");
|
|
284
|
+
// Scan each project and merge results
|
|
285
|
+
const allNpmPkgs = [];
|
|
286
|
+
const allPyPkgs = [];
|
|
287
|
+
const seen = new Set();
|
|
288
|
+
for (const proj of subProjects) {
|
|
289
|
+
try {
|
|
290
|
+
const projConfig = { ...config, workspace: proj.relativePath };
|
|
291
|
+
const projDisc = (0, lockfile_1.discoverChanges)(process.cwd(), projConfig);
|
|
292
|
+
for (const pkg of projDisc.packages) {
|
|
293
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
294
|
+
if (!seen.has(key)) {
|
|
295
|
+
seen.add(key);
|
|
296
|
+
allNpmPkgs.push(pkg);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const pkg of projDisc.pythonPackages) {
|
|
300
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
301
|
+
if (!seen.has(key)) {
|
|
302
|
+
seen.add(key);
|
|
303
|
+
allPyPkgs.push(pkg);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Skip projects that fail to parse (e.g. corrupted lockfiles)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (allNpmPkgs.length === 0 && allPyPkgs.length === 0) {
|
|
312
|
+
process.stderr.write(chalk_1.default.dim(" No package changes detected across projects.\n"));
|
|
313
|
+
process.exit(0);
|
|
314
|
+
}
|
|
315
|
+
// Replace discovery with merged results and fall through to normal scan path
|
|
316
|
+
discovery = {
|
|
317
|
+
packages: allNpmPkgs,
|
|
318
|
+
pythonPackages: allPyPkgs,
|
|
319
|
+
method: "scan-all",
|
|
320
|
+
skipped: [],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const packages = discovery.packages;
|
|
324
|
+
const totalToScan = packages.length + discovery.pythonPackages.length;
|
|
325
|
+
process.stderr.write(chalk_1.default.dim(` Scanning ${totalToScan} package${totalToScan !== 1 ? "s" : ""}${discovery.pythonPackages.length > 0 ? ` (${packages.length} npm + ${discovery.pythonPackages.length} Python)` : ""} (${discovery.method})...\n`));
|
|
326
|
+
if (discovery.skipped.length > 0 && config.maxPackages < 10000) {
|
|
327
|
+
process.stderr.write(chalk_1.default.dim(` Note: ${discovery.skipped.length} package(s) not scanned (--max-packages=${config.maxPackages})\n`));
|
|
328
|
+
}
|
|
329
|
+
dbg(`packages to scan: ${packages
|
|
330
|
+
.map((p) => `${p.name}@${p.version}`)
|
|
331
|
+
.slice(0, 10)
|
|
332
|
+
.join(", ")}${packages.length > 10 ? ` (+${packages.length - 10} more)` : ""}`);
|
|
333
|
+
// Call Detection API (npm)
|
|
334
|
+
let result;
|
|
335
|
+
try {
|
|
336
|
+
const startMs = Date.now();
|
|
337
|
+
if (packages.length > 0) {
|
|
338
|
+
result = await (0, api_1.callAnalyzeAPI)(packages, config, (done, total) => {
|
|
339
|
+
process.stderr.write(chalk_1.default.dim(` Analyzed ${done}/${total}...\n`));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
result = { score: 0, action: "pass", packages: [], safeVersions: {}, durationMs: 0 };
|
|
344
|
+
}
|
|
345
|
+
// Scan Python packages if found
|
|
346
|
+
if (discovery.pythonPackages.length > 0) {
|
|
347
|
+
process.stderr.write(chalk_1.default.dim(` Scanning ${discovery.pythonPackages.length} Python package${discovery.pythonPackages.length !== 1 ? "s" : ""}...\n`));
|
|
348
|
+
const pyResult = await (0, api_1.callPyPIAnalyzeAPI)(discovery.pythonPackages, config);
|
|
349
|
+
// Merge Python results into npm results
|
|
350
|
+
result.packages.push(...pyResult.packages);
|
|
351
|
+
result.score = Math.max(result.score, pyResult.score);
|
|
352
|
+
if (pyResult.action === "block" || (pyResult.action === "warn" && result.action === "pass")) {
|
|
353
|
+
result.action = pyResult.action;
|
|
354
|
+
}
|
|
355
|
+
if (pyResult.trialScansRemaining !== undefined) {
|
|
356
|
+
result.trialScansRemaining = pyResult.trialScansRemaining;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
dbg(`API responded in ${Date.now() - startMs}ms, action=${result.action}, score=${result.score}`);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
if (handleTrialExhausted(error, config.json))
|
|
363
|
+
return;
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
// Handle JSON output (interactive prompt, pipe, or --output)
|
|
367
|
+
const jsonHandled = await handleJsonOutput(result, config);
|
|
368
|
+
if (jsonHandled) {
|
|
369
|
+
printTrialBanner(result);
|
|
370
|
+
if (result.action === "block" && config.mode === "block")
|
|
371
|
+
process.exit(2);
|
|
372
|
+
else if (result.action === "block" || result.action === "warn")
|
|
373
|
+
process.exit(1);
|
|
374
|
+
process.exit(0);
|
|
375
|
+
}
|
|
376
|
+
// Render human-readable output
|
|
377
|
+
const output = renderResultStatic(result, config);
|
|
378
|
+
process.stdout.write(output + "\n");
|
|
379
|
+
printTrialBanner(result);
|
|
380
|
+
// Exit code
|
|
381
|
+
if (result.action === "block" && config.mode === "block") {
|
|
382
|
+
process.exit(2);
|
|
383
|
+
}
|
|
384
|
+
else if (result.action === "block" || result.action === "warn") {
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
process.exit(0);
|
|
388
|
+
}
|
|
389
|
+
async function runStaticNpm(npmArgs, config) {
|
|
390
|
+
const parsed = (0, npm_wrapper_1.parseNpmArgs)(npmArgs);
|
|
391
|
+
// Non-install commands: pass through directly
|
|
392
|
+
if (!parsed.shouldScan) {
|
|
393
|
+
const code = await (0, npm_wrapper_1.runNpm)(parsed.rawArgs);
|
|
394
|
+
process.exit(code);
|
|
395
|
+
}
|
|
396
|
+
// No packages specified (e.g. bare `npm install` from package.json)
|
|
397
|
+
if (parsed.packages.length === 0) {
|
|
398
|
+
const bareSpecs = (0, npm_wrapper_1.readBareInstallPackages)(process.cwd());
|
|
399
|
+
if (bareSpecs.length === 0) {
|
|
400
|
+
process.stderr.write(chalk_1.default.dim(" Dependency Guardian: no packages to scan, passing through to npm.\n"));
|
|
401
|
+
const code = await (0, npm_wrapper_1.runNpm)(parsed.rawArgs);
|
|
402
|
+
process.exit(code);
|
|
403
|
+
}
|
|
404
|
+
process.stderr.write(chalk_1.default.dim(` Resolving ${bareSpecs.length} package${bareSpecs.length !== 1 ? "s" : ""} from package.json...\n`));
|
|
405
|
+
const { resolved, failed } = await (0, npm_wrapper_1.resolvePackages)(bareSpecs);
|
|
406
|
+
if (failed.length > 0 && config.debug) {
|
|
407
|
+
process.stderr.write(chalk_1.default.dim(` [debug] Could not resolve: ${failed
|
|
408
|
+
.slice(0, 5)
|
|
409
|
+
.join(", ")}${failed.length > 5 ? ` (+${failed.length - 5} more)` : ""}\n`));
|
|
410
|
+
}
|
|
411
|
+
return scanAndInstallStatic(resolved, parsed, config);
|
|
412
|
+
}
|
|
413
|
+
// Resolve versions
|
|
414
|
+
process.stderr.write(chalk_1.default.dim(` Resolving ${parsed.packages.length} package${parsed.packages.length !== 1 ? "s" : ""}...\n`));
|
|
415
|
+
const { resolved, failed } = await (0, npm_wrapper_1.resolvePackages)(parsed.packages);
|
|
416
|
+
if (failed.length > 0) {
|
|
417
|
+
process.stderr.write(chalk_1.default.yellow(` Warning: Could not resolve versions for: ${failed.join(", ")}\n`));
|
|
418
|
+
}
|
|
419
|
+
if (resolved.length === 0) {
|
|
420
|
+
process.stderr.write(chalk_1.default.dim(" No packages to scan. Passing through to npm.\n"));
|
|
421
|
+
const code = await (0, npm_wrapper_1.runNpm)(parsed.rawArgs);
|
|
422
|
+
process.exit(code);
|
|
423
|
+
}
|
|
424
|
+
return scanAndInstallStatic(resolved, parsed, config);
|
|
425
|
+
}
|
|
426
|
+
async function scanAndInstallStatic(resolved, parsed, config) {
|
|
427
|
+
// Scan
|
|
428
|
+
process.stderr.write(chalk_1.default.dim(` Scanning ${resolved.length} package${resolved.length !== 1 ? "s" : ""}...\n`));
|
|
429
|
+
let result;
|
|
430
|
+
try {
|
|
431
|
+
const startMs = Date.now();
|
|
432
|
+
result = await (0, api_1.callAnalyzeAPI)(resolved, config);
|
|
433
|
+
const elapsed = Date.now() - startMs;
|
|
434
|
+
if (config.debug) {
|
|
435
|
+
process.stderr.write(chalk_1.default.dim(` [debug] API responded in ${elapsed}ms, action=${result.action}, score=${result.score}\n`));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (handleTrialExhausted(error, config.json))
|
|
440
|
+
return;
|
|
441
|
+
// API unavailable — warn and proceed
|
|
442
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
443
|
+
process.stderr.write(chalk_1.default.yellow(` Warning: Scan failed (${msg}). Proceeding with install.\n`));
|
|
444
|
+
const code = await (0, npm_wrapper_1.runNpm)(parsed.rawArgs);
|
|
445
|
+
process.exit(code);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// Handle result
|
|
449
|
+
if (result.action === "pass") {
|
|
450
|
+
process.stderr.write(chalk_1.default.green(` ${chalk_1.default.bold("\u2713")} ${resolved.length} package${resolved.length !== 1 ? "s" : ""} scanned \u2014 all clear\n`));
|
|
451
|
+
printTrialBanner(result);
|
|
452
|
+
process.stderr.write("\n");
|
|
453
|
+
const code = await (0, npm_wrapper_1.runNpm)(parsed.rawArgs);
|
|
454
|
+
process.exit(code);
|
|
455
|
+
}
|
|
456
|
+
// Render findings
|
|
457
|
+
const output = renderResultStatic(result, config);
|
|
458
|
+
process.stdout.write(output + "\n");
|
|
459
|
+
printTrialBanner(result);
|
|
460
|
+
if (result.action === "warn") {
|
|
461
|
+
process.stderr.write(chalk_1.default.yellow(" Warnings detected. Proceeding with install.\n\n"));
|
|
462
|
+
const code = await (0, npm_wrapper_1.runNpm)(parsed.rawArgs);
|
|
463
|
+
process.exit(code);
|
|
464
|
+
}
|
|
465
|
+
// Block
|
|
466
|
+
if (result.action === "block") {
|
|
467
|
+
if (parsed.dgForce) {
|
|
468
|
+
process.stderr.write(chalk_1.default.yellow(chalk_1.default.bold(" --dg-force: Bypassing block. Install at your own risk.\n\n")));
|
|
469
|
+
const code = await (0, npm_wrapper_1.runNpm)(parsed.rawArgs);
|
|
470
|
+
process.exit(code);
|
|
471
|
+
}
|
|
472
|
+
process.stderr.write(chalk_1.default.red(chalk_1.default.bold(" BLOCKED: ")) +
|
|
473
|
+
chalk_1.default.red("High-risk packages detected. Install aborted.\n"));
|
|
474
|
+
process.stderr.write(chalk_1.default.dim(" Use --dg-force to bypass this check.\n\n"));
|
|
475
|
+
process.exit(2);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function runStaticPip(pipArgs, config) {
|
|
479
|
+
const parsed = (0, pip_wrapper_1.parsePipArgs)(pipArgs);
|
|
480
|
+
// Non-install commands: pass through directly
|
|
481
|
+
if (!parsed.shouldScan) {
|
|
482
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
483
|
+
process.exit(code);
|
|
484
|
+
}
|
|
485
|
+
// Collect package specs
|
|
486
|
+
let specs = parsed.packages;
|
|
487
|
+
if (specs.length === 0 && parsed.requirementsFile) {
|
|
488
|
+
specs = (0, pip_wrapper_1.parseRequirementsFile)(parsed.requirementsFile);
|
|
489
|
+
}
|
|
490
|
+
if (specs.length === 0) {
|
|
491
|
+
process.stderr.write(chalk_1.default.dim(" Dependency Guardian: no packages to scan, passing through to pip.\n"));
|
|
492
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
493
|
+
process.exit(code);
|
|
494
|
+
}
|
|
495
|
+
// Resolve versions via PyPI API
|
|
496
|
+
process.stderr.write(chalk_1.default.dim(` Resolving ${specs.length} package${specs.length !== 1 ? "s" : ""} from PyPI...\n`));
|
|
497
|
+
const { resolved, failed } = await (0, pip_wrapper_1.resolvePackages)(specs);
|
|
498
|
+
if (failed.length > 0) {
|
|
499
|
+
process.stderr.write(chalk_1.default.yellow(` Warning: Could not resolve versions for: ${failed.join(", ")}\n`));
|
|
500
|
+
}
|
|
501
|
+
if (resolved.length === 0) {
|
|
502
|
+
process.stderr.write(chalk_1.default.dim(" No packages to scan. Passing through to pip.\n"));
|
|
503
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
504
|
+
process.exit(code);
|
|
505
|
+
}
|
|
506
|
+
// Scan
|
|
507
|
+
process.stderr.write(chalk_1.default.dim(` Scanning ${resolved.length} Python package${resolved.length !== 1 ? "s" : ""}...\n`));
|
|
508
|
+
let result;
|
|
509
|
+
try {
|
|
510
|
+
result = await (0, api_1.callPyPIAnalyzeAPI)(resolved, config);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
if (handleTrialExhausted(error, config.json))
|
|
514
|
+
return;
|
|
515
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
516
|
+
process.stderr.write(chalk_1.default.yellow(` Warning: Scan failed (${msg}). Proceeding with install.\n`));
|
|
517
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
518
|
+
process.exit(code);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Handle result
|
|
522
|
+
if (result.action === "pass") {
|
|
523
|
+
process.stderr.write(chalk_1.default.green(` ${chalk_1.default.bold("\u2713")} ${resolved.length} package${resolved.length !== 1 ? "s" : ""} scanned \u2014 all clear\n`));
|
|
524
|
+
printTrialBanner(result);
|
|
525
|
+
process.stderr.write("\n");
|
|
526
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
527
|
+
process.exit(code);
|
|
528
|
+
}
|
|
529
|
+
const output = renderResultStatic(result, config);
|
|
530
|
+
process.stdout.write(output + "\n");
|
|
531
|
+
printTrialBanner(result);
|
|
532
|
+
if (result.action === "warn") {
|
|
533
|
+
process.stderr.write(chalk_1.default.yellow(" Warnings detected. Proceeding with install.\n\n"));
|
|
534
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
535
|
+
process.exit(code);
|
|
536
|
+
}
|
|
537
|
+
if (result.action === "block") {
|
|
538
|
+
if (parsed.dgForce) {
|
|
539
|
+
process.stderr.write(chalk_1.default.yellow(chalk_1.default.bold(" --dg-force: Bypassing block. Install at your own risk.\n\n")));
|
|
540
|
+
const code = await (0, pip_wrapper_1.runPip)(parsed.rawArgs);
|
|
541
|
+
process.exit(code);
|
|
542
|
+
}
|
|
543
|
+
process.stderr.write(chalk_1.default.red(chalk_1.default.bold(" BLOCKED: ")) +
|
|
544
|
+
chalk_1.default.red("High-risk packages detected. Install aborted.\n"));
|
|
545
|
+
process.stderr.write(chalk_1.default.dim(" Use --dg-force to bypass this check.\n\n"));
|
|
546
|
+
process.exit(2);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function runStaticLogin() {
|
|
550
|
+
const { createAuthSession, pollAuthSession, saveCredentials, openBrowser, getStoredApiKey, } = await Promise.resolve().then(() => __importStar(require("./auth")));
|
|
551
|
+
const apiUrl = process.env.DG_API_URL || "https://api.westbayberry.com";
|
|
552
|
+
// Check if already logged in with a valid key
|
|
553
|
+
const existing = getStoredApiKey();
|
|
554
|
+
if (existing) {
|
|
555
|
+
// Verify the key is still valid on the server
|
|
556
|
+
try {
|
|
557
|
+
const resp = await fetch(`${apiUrl}/v1/auth/status`, {
|
|
558
|
+
headers: { Authorization: `Bearer ${existing}` },
|
|
559
|
+
signal: AbortSignal.timeout(5000),
|
|
560
|
+
});
|
|
561
|
+
if (resp.ok) {
|
|
562
|
+
process.stderr.write(chalk_1.default.yellow(" Already authenticated.\n") +
|
|
563
|
+
chalk_1.default.dim(" Run `dg logout` first to re-authenticate.\n"));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch { }
|
|
568
|
+
// Key is invalid — clear it and proceed with fresh login
|
|
569
|
+
process.stderr.write(chalk_1.default.dim(" Stored key is invalid. Starting fresh login...\n"));
|
|
570
|
+
saveCredentials("");
|
|
571
|
+
}
|
|
572
|
+
process.stderr.write(chalk_1.default.dim(" Creating login session...\n"));
|
|
573
|
+
let session;
|
|
574
|
+
try {
|
|
575
|
+
session = await createAuthSession();
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
process.stderr.write(chalk_1.default.red(` Error: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
process.stderr.write(`\n Authenticate your account at:\n`);
|
|
582
|
+
process.stderr.write(` ${chalk_1.default.cyan(session.verifyUrl)}\n\n`);
|
|
583
|
+
process.stderr.write(chalk_1.default.dim(" Press ENTER to open in the browser...\n"));
|
|
584
|
+
await new Promise((resolve) => {
|
|
585
|
+
const onData = () => {
|
|
586
|
+
process.stdin.removeListener("data", onData);
|
|
587
|
+
if (process.stdin.unref)
|
|
588
|
+
process.stdin.unref();
|
|
589
|
+
resolve();
|
|
590
|
+
};
|
|
591
|
+
process.stdin.setEncoding("utf-8");
|
|
592
|
+
process.stdin.resume();
|
|
593
|
+
process.stdin.on("data", onData);
|
|
594
|
+
});
|
|
595
|
+
try {
|
|
596
|
+
openBrowser(session.verifyUrl);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
// Browser open is best-effort
|
|
600
|
+
}
|
|
601
|
+
process.stderr.write(chalk_1.default.dim(" Waiting for authorization...\n"));
|
|
602
|
+
const POLL_INTERVAL = 2000;
|
|
603
|
+
const MAX_ATTEMPTS = 150;
|
|
604
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
605
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
606
|
+
try {
|
|
607
|
+
const result = await pollAuthSession(session.sessionId);
|
|
608
|
+
if (result.status === "complete" && result.apiKey) {
|
|
609
|
+
saveCredentials(result.apiKey);
|
|
610
|
+
process.stderr.write(chalk_1.default.green(`\n Logged in as ${result.email ?? "unknown"}\n`) +
|
|
611
|
+
chalk_1.default.dim(" Credentials saved.\n\n"));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (result.status === "expired") {
|
|
615
|
+
process.stderr.write(chalk_1.default.yellow("\n Session expired. Run `dg login` to try again.\n"));
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
// Transient network error — continue polling
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
process.stderr.write(chalk_1.default.yellow("\n Timed out waiting for authorization.\n"));
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|