@westbayberry/dg 1.3.2 → 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 -54141
- 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,153 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { verifyLockfile } from "../verify/preflight.js";
|
|
4
|
+
export const LOCKFILE_ECOSYSTEMS = {
|
|
5
|
+
"package-lock.json": "npm",
|
|
6
|
+
"npm-shrinkwrap.json": "npm",
|
|
7
|
+
"yarn.lock": "npm",
|
|
8
|
+
"pnpm-lock.yaml": "npm",
|
|
9
|
+
"Pipfile.lock": "pypi",
|
|
10
|
+
"poetry.lock": "pypi",
|
|
11
|
+
"requirements.txt": "pypi"
|
|
12
|
+
};
|
|
13
|
+
export function isLockfileName(name) {
|
|
14
|
+
return Object.prototype.hasOwnProperty.call(LOCKFILE_ECOSYSTEMS, name);
|
|
15
|
+
}
|
|
16
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
17
|
+
"node_modules",
|
|
18
|
+
".git",
|
|
19
|
+
"dist",
|
|
20
|
+
"build",
|
|
21
|
+
"vendor",
|
|
22
|
+
"__pycache__",
|
|
23
|
+
".venv",
|
|
24
|
+
"venv",
|
|
25
|
+
"target",
|
|
26
|
+
"coverage"
|
|
27
|
+
]);
|
|
28
|
+
const MAX_DISCOVERY_DEPTH = 8;
|
|
29
|
+
function readDirents(directory) {
|
|
30
|
+
try {
|
|
31
|
+
return readdirSync(directory, { withFileTypes: true });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function firstLockfile(entries) {
|
|
38
|
+
for (const [lockfile, ecosystem] of Object.entries(LOCKFILE_ECOSYSTEMS)) {
|
|
39
|
+
if (entries.some((entry) => entry.name === lockfile && entry.isFile())) {
|
|
40
|
+
return [lockfile, ecosystem];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function shouldDescend(entry) {
|
|
46
|
+
return entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name) && !entry.name.startsWith(".");
|
|
47
|
+
}
|
|
48
|
+
export function discoverScanProjects(root) {
|
|
49
|
+
const projects = [];
|
|
50
|
+
walk(root, 0);
|
|
51
|
+
return projects;
|
|
52
|
+
function walk(directory, depth) {
|
|
53
|
+
if (depth > MAX_DISCOVERY_DEPTH) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const entries = readDirents(directory);
|
|
57
|
+
const match = firstLockfile(entries);
|
|
58
|
+
if (match) {
|
|
59
|
+
projects.push({
|
|
60
|
+
path: directory,
|
|
61
|
+
relativePath: relative(root, directory) || ".",
|
|
62
|
+
ecosystem: match[1],
|
|
63
|
+
depFile: match[0],
|
|
64
|
+
packageCount: countLockfilePackages(join(directory, match[0]))
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (shouldDescend(entry)) {
|
|
69
|
+
walk(join(directory, entry.name), depth + 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function discoverScanProjectsAsync(root, onProgress) {
|
|
75
|
+
const projects = [];
|
|
76
|
+
let lastYield = Date.now();
|
|
77
|
+
await walk(root, 0);
|
|
78
|
+
return projects;
|
|
79
|
+
async function walk(directory, depth) {
|
|
80
|
+
if (depth > MAX_DISCOVERY_DEPTH) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (Date.now() - lastYield > 8) {
|
|
84
|
+
await yieldToEventLoop();
|
|
85
|
+
lastYield = Date.now();
|
|
86
|
+
}
|
|
87
|
+
const entries = readDirents(directory);
|
|
88
|
+
const match = firstLockfile(entries);
|
|
89
|
+
if (match) {
|
|
90
|
+
const relativePath = relative(root, directory) || ".";
|
|
91
|
+
projects.push({
|
|
92
|
+
path: directory,
|
|
93
|
+
relativePath,
|
|
94
|
+
ecosystem: match[1],
|
|
95
|
+
depFile: match[0],
|
|
96
|
+
packageCount: countLockfilePackages(join(directory, match[0]))
|
|
97
|
+
});
|
|
98
|
+
onProgress?.({ path: relativePath === "." ? match[0] : `${relativePath}/${match[0]}`, found: projects.length });
|
|
99
|
+
}
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (shouldDescend(entry)) {
|
|
102
|
+
await walk(join(directory, entry.name), depth + 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function yieldToEventLoop() {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
setImmediate(resolve);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function countLockfilePackages(lockfilePath) {
|
|
113
|
+
try {
|
|
114
|
+
return verifyLockfile(lockfilePath).packages.length;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function collectScanPackages(projects) {
|
|
121
|
+
const byEcosystem = new Map();
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
let skipped = 0;
|
|
124
|
+
for (const project of projects) {
|
|
125
|
+
let identities;
|
|
126
|
+
try {
|
|
127
|
+
identities = verifyLockfile(join(project.path, project.depFile)).packages;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
skipped += 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const identity of identities) {
|
|
134
|
+
if (identity.ecosystem !== "npm" && identity.ecosystem !== "pypi") {
|
|
135
|
+
skipped += 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!identity.version) {
|
|
139
|
+
skipped += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const key = `${identity.ecosystem}:${identity.name}@${identity.version}`;
|
|
143
|
+
if (seen.has(key)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
seen.add(key);
|
|
147
|
+
const list = byEcosystem.get(identity.ecosystem) ?? [];
|
|
148
|
+
list.push({ name: identity.name, version: identity.version });
|
|
149
|
+
byEcosystem.set(identity.ecosystem, list);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { byEcosystem, skipped };
|
|
153
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { scanProject } from "./discovery.js";
|
|
4
|
+
import { renderJsonReport, renderSarifReport, renderTextReport } from "./render.js";
|
|
5
|
+
import { resolvePresentation } from "../presentation/mode.js";
|
|
6
|
+
import { createTheme } from "../presentation/theme.js";
|
|
7
|
+
import { launchScanTui, shouldLaunchScanTui } from "../scan-ui/launch.js";
|
|
8
|
+
import { tryScannerScan } from "./scanner-report.js";
|
|
9
|
+
import { runStagedScan } from "./staged.js";
|
|
10
|
+
import { scanExitCode } from "../scan-ui/shims.js";
|
|
11
|
+
import { loadUserConfig } from "../config/settings.js";
|
|
12
|
+
import { EXIT_USAGE } from "../commands/types.js";
|
|
13
|
+
export function runScanCommand(context) {
|
|
14
|
+
const parsed = parseScanArgs(context.args);
|
|
15
|
+
if ("error" in parsed) {
|
|
16
|
+
return usageError(parsed.error);
|
|
17
|
+
}
|
|
18
|
+
if (parsed.staged) {
|
|
19
|
+
return runStagedScan({ hook: parsed.hook });
|
|
20
|
+
}
|
|
21
|
+
if (shouldLaunchScanTui({
|
|
22
|
+
targetPath: parsed.targetPath,
|
|
23
|
+
format: parsed.format,
|
|
24
|
+
outputPath: parsed.outputPath ?? undefined
|
|
25
|
+
})) {
|
|
26
|
+
void launchScanTui().catch((error) => {
|
|
27
|
+
process.stderr.write(`dg scan TUI failed: ${error instanceof Error ? error.message : "unknown error"}\n`);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
exitCode: 0,
|
|
32
|
+
stdout: "",
|
|
33
|
+
stderr: ""
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
let report;
|
|
37
|
+
try {
|
|
38
|
+
report = scanProject({
|
|
39
|
+
targetPath: parsed.targetPath
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
stdout: "",
|
|
46
|
+
stderr: `dg scan failed: ${error instanceof Error ? error.message : "unknown scan error"}\n`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const scannerReport = tryScannerScan(parsed.targetPath, report);
|
|
50
|
+
if (scannerReport) {
|
|
51
|
+
report = scannerReport;
|
|
52
|
+
}
|
|
53
|
+
const scannerUnavailable = scannerReport === null && report.summary.projectCount > 0;
|
|
54
|
+
const rendered = renderReport(report, parsed.format, scannerUnavailable);
|
|
55
|
+
if (parsed.outputPath) {
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(resolve(parsed.outputPath), rendered, "utf8");
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
exitCode: 1,
|
|
62
|
+
stdout: "",
|
|
63
|
+
stderr: `dg scan could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
exitCode: exitCodeForReport(report),
|
|
68
|
+
stdout: `Wrote ${parsed.format} scan report to ${parsed.outputPath}\n`,
|
|
69
|
+
stderr: ""
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
exitCode: exitCodeForReport(report),
|
|
74
|
+
stdout: rendered,
|
|
75
|
+
stderr: ""
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function parseScanArgs(args) {
|
|
79
|
+
let format = "text";
|
|
80
|
+
let outputPath = null;
|
|
81
|
+
let targetPath = ".";
|
|
82
|
+
let sawTarget = false;
|
|
83
|
+
let staged = false;
|
|
84
|
+
let hook = false;
|
|
85
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
86
|
+
const arg = args[index];
|
|
87
|
+
if (!arg) {
|
|
88
|
+
return { error: "empty argument" };
|
|
89
|
+
}
|
|
90
|
+
if (arg === "--staged") {
|
|
91
|
+
staged = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg === "--hook") {
|
|
95
|
+
hook = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (arg === "--json") {
|
|
99
|
+
if (format !== "text") {
|
|
100
|
+
return { error: "choose only one output format" };
|
|
101
|
+
}
|
|
102
|
+
format = "json";
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (arg === "--sarif") {
|
|
106
|
+
if (format !== "text") {
|
|
107
|
+
return { error: "choose only one output format" };
|
|
108
|
+
}
|
|
109
|
+
format = "sarif";
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (arg === "--output" || arg === "-o") {
|
|
113
|
+
const next = args[index + 1];
|
|
114
|
+
if (!next) {
|
|
115
|
+
return { error: `${arg} requires a path` };
|
|
116
|
+
}
|
|
117
|
+
outputPath = next;
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (arg.startsWith("-")) {
|
|
122
|
+
return { error: `unknown option '${arg}'` };
|
|
123
|
+
}
|
|
124
|
+
if (sawTarget) {
|
|
125
|
+
return { error: "scan accepts at most one path" };
|
|
126
|
+
}
|
|
127
|
+
targetPath = arg;
|
|
128
|
+
sawTarget = true;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
format,
|
|
132
|
+
outputPath,
|
|
133
|
+
targetPath,
|
|
134
|
+
staged,
|
|
135
|
+
hook
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function usageError(message) {
|
|
139
|
+
return {
|
|
140
|
+
exitCode: EXIT_USAGE,
|
|
141
|
+
stdout: "",
|
|
142
|
+
stderr: `dg scan: ${message}. Usage: dg scan [path] [--json|--sarif] [--output <path>]\n`
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function renderReport(report, format, scannerUnavailable) {
|
|
146
|
+
if (format === "json") {
|
|
147
|
+
return renderJsonReport(report, scannerUnavailable);
|
|
148
|
+
}
|
|
149
|
+
if (format === "sarif") {
|
|
150
|
+
return renderSarifReport(report);
|
|
151
|
+
}
|
|
152
|
+
return renderTextReport(report, undefined, createTheme(resolvePresentation().color), scannerUnavailable);
|
|
153
|
+
}
|
|
154
|
+
function exitCodeForReport(report) {
|
|
155
|
+
if (report.scanner) {
|
|
156
|
+
return scanExitCode(report.scanner.action, loadUserConfig().policy.mode);
|
|
157
|
+
}
|
|
158
|
+
return report.status === "block" || report.status === "error" ? 1 : 0;
|
|
159
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
3
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
4
|
+
".git",
|
|
5
|
+
".hg",
|
|
6
|
+
".svn",
|
|
7
|
+
"coverage",
|
|
8
|
+
"dist",
|
|
9
|
+
"node_modules",
|
|
10
|
+
"vendor"
|
|
11
|
+
]);
|
|
12
|
+
const DEPENDENCY_SECTIONS = [
|
|
13
|
+
"dependencies",
|
|
14
|
+
"devDependencies",
|
|
15
|
+
"optionalDependencies",
|
|
16
|
+
"peerDependencies"
|
|
17
|
+
];
|
|
18
|
+
const RISKY_SCRIPT_NAMES = new Set(["preinstall", "install", "postinstall", "prepare"]);
|
|
19
|
+
const MAX_DISCOVERY_DEPTH = 8;
|
|
20
|
+
export function scanProject(options = {}) {
|
|
21
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
22
|
+
const requestedTarget = options.targetPath ?? ".";
|
|
23
|
+
const absoluteTarget = resolve(cwd, requestedTarget);
|
|
24
|
+
const targetInfo = statSync(absoluteTarget);
|
|
25
|
+
const root = targetInfo.isFile() ? dirname(absoluteTarget) : absoluteTarget;
|
|
26
|
+
const manifestPaths = targetInfo.isFile() && basename(absoluteTarget) === "package.json"
|
|
27
|
+
? [absoluteTarget]
|
|
28
|
+
: discoverPackageManifests(root);
|
|
29
|
+
const projects = [];
|
|
30
|
+
const errors = [];
|
|
31
|
+
for (const manifestPath of manifestPaths) {
|
|
32
|
+
const result = readPackageProject(root, manifestPath);
|
|
33
|
+
if ("error" in result) {
|
|
34
|
+
errors.push(result.error);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
projects.push(result.project);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const findings = projects.flatMap((project) => [...project.findings]);
|
|
41
|
+
const warnCount = findings.filter((finding) => finding.severity === "warn").length;
|
|
42
|
+
const blockCount = findings.filter((finding) => finding.severity === "block").length;
|
|
43
|
+
const status = resolveStatus({
|
|
44
|
+
blockCount,
|
|
45
|
+
errorCount: errors.length,
|
|
46
|
+
warnCount
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
target: displayPath(cwd, absoluteTarget),
|
|
50
|
+
status,
|
|
51
|
+
projects,
|
|
52
|
+
findings,
|
|
53
|
+
errors,
|
|
54
|
+
summary: {
|
|
55
|
+
projectCount: projects.length,
|
|
56
|
+
dependencyCount: projects.reduce((total, project) => total + project.dependencyCount, 0),
|
|
57
|
+
findingCount: findings.length,
|
|
58
|
+
warnCount,
|
|
59
|
+
blockCount,
|
|
60
|
+
errorCount: errors.length
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function resolveStatus(counts) {
|
|
65
|
+
if (counts.errorCount > 0) {
|
|
66
|
+
return "error";
|
|
67
|
+
}
|
|
68
|
+
if (counts.blockCount > 0) {
|
|
69
|
+
return "block";
|
|
70
|
+
}
|
|
71
|
+
if (counts.warnCount > 0) {
|
|
72
|
+
return "warn";
|
|
73
|
+
}
|
|
74
|
+
return "pass";
|
|
75
|
+
}
|
|
76
|
+
function discoverPackageManifests(root) {
|
|
77
|
+
const manifests = [];
|
|
78
|
+
walk(root, 0, manifests);
|
|
79
|
+
return manifests.sort((left, right) => displayPath(root, left).localeCompare(displayPath(root, right)));
|
|
80
|
+
}
|
|
81
|
+
function walk(directory, depth, manifests) {
|
|
82
|
+
if (depth > MAX_DISCOVERY_DEPTH) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const entries = readdirSync(directory, {
|
|
86
|
+
withFileTypes: true
|
|
87
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const absolutePath = resolve(directory, entry.name);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
92
|
+
walk(absolutePath, depth + 1, manifests);
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (entry.isFile() && entry.name === "package.json") {
|
|
97
|
+
manifests.push(absolutePath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function readPackageProject(root, manifestPath) {
|
|
102
|
+
const manifestDisplayPath = displayPath(root, manifestPath);
|
|
103
|
+
let manifest;
|
|
104
|
+
try {
|
|
105
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
error: {
|
|
110
|
+
location: manifestDisplayPath,
|
|
111
|
+
message: error instanceof Error ? error.message : "package.json could not be parsed"
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (!isRecord(manifest)) {
|
|
116
|
+
return {
|
|
117
|
+
error: {
|
|
118
|
+
location: manifestDisplayPath,
|
|
119
|
+
message: "package.json must contain an object"
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const name = typeof manifest.name === "string" && manifest.name.length > 0
|
|
124
|
+
? manifest.name
|
|
125
|
+
: basename(dirname(manifestPath));
|
|
126
|
+
const version = typeof manifest.version === "string" ? manifest.version : null;
|
|
127
|
+
const license = typeof manifest.license === "string" ? manifest.license : null;
|
|
128
|
+
const findings = [
|
|
129
|
+
...scriptFindings(manifest, manifestDisplayPath, name),
|
|
130
|
+
...dependencyFindings(manifest, manifestDisplayPath, name)
|
|
131
|
+
];
|
|
132
|
+
return {
|
|
133
|
+
project: {
|
|
134
|
+
name,
|
|
135
|
+
version,
|
|
136
|
+
license,
|
|
137
|
+
manifestPath: manifestDisplayPath,
|
|
138
|
+
dependencyCount: dependencyCount(manifest),
|
|
139
|
+
findings
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function scriptFindings(manifest, manifestPath, project) {
|
|
144
|
+
const scripts = isRecord(manifest.scripts) ? manifest.scripts : {};
|
|
145
|
+
return Object.keys(scripts)
|
|
146
|
+
.filter((scriptName) => RISKY_SCRIPT_NAMES.has(scriptName))
|
|
147
|
+
.sort()
|
|
148
|
+
.map((scriptName) => ({
|
|
149
|
+
id: "npm-lifecycle-script",
|
|
150
|
+
severity: "warn",
|
|
151
|
+
title: "Install lifecycle script present",
|
|
152
|
+
message: `script '${scriptName}' can execute during package manager installs`,
|
|
153
|
+
project,
|
|
154
|
+
location: `${manifestPath}:scripts.${scriptName}`
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
function dependencyFindings(manifest, manifestPath, project) {
|
|
158
|
+
const findings = [];
|
|
159
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
160
|
+
const dependencies = isRecord(manifest[section]) ? manifest[section] : {};
|
|
161
|
+
for (const [dependencyName, specifier] of Object.entries(dependencies).sort(([left], [right]) => left.localeCompare(right))) {
|
|
162
|
+
if (typeof specifier !== "string") {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const severity = dependencySpecifierSeverity(specifier);
|
|
166
|
+
if (!severity) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
findings.push({
|
|
170
|
+
id: severity === "block" ? "unverified-network-dependency" : "local-artifact-dependency",
|
|
171
|
+
severity,
|
|
172
|
+
title: severity === "block" ? "Unverified network dependency" : "Local artifact dependency",
|
|
173
|
+
message: `${dependencyName} uses '${specifier}', which should be verified before install`,
|
|
174
|
+
project,
|
|
175
|
+
location: `${manifestPath}:${section}.${dependencyName}`
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return findings;
|
|
180
|
+
}
|
|
181
|
+
function dependencySpecifierSeverity(specifier) {
|
|
182
|
+
const normalized = specifier.trim().toLowerCase();
|
|
183
|
+
if (normalized.startsWith("http://")
|
|
184
|
+
|| normalized.startsWith("https://")
|
|
185
|
+
|| normalized.startsWith("git+")
|
|
186
|
+
|| normalized.startsWith("git://")
|
|
187
|
+
|| normalized.startsWith("ssh://")
|
|
188
|
+
|| normalized.startsWith("github:")) {
|
|
189
|
+
return "block";
|
|
190
|
+
}
|
|
191
|
+
if (normalized.startsWith("file:")) {
|
|
192
|
+
return "warn";
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
function dependencyCount(manifest) {
|
|
197
|
+
return DEPENDENCY_SECTIONS.reduce((total, section) => {
|
|
198
|
+
const dependencies = isRecord(manifest[section]) ? manifest[section] : {};
|
|
199
|
+
return total + Object.keys(dependencies).length;
|
|
200
|
+
}, 0);
|
|
201
|
+
}
|
|
202
|
+
function isRecord(value) {
|
|
203
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
204
|
+
}
|
|
205
|
+
function displayPath(root, path) {
|
|
206
|
+
const relativePath = relative(root, path);
|
|
207
|
+
const display = relativePath.length === 0 ? "." : relativePath;
|
|
208
|
+
return display.split(sep).join("/");
|
|
209
|
+
}
|