@westbayberry/dg 2.0.11 → 2.1.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/dist/api/analyze.js +5 -3
- package/dist/bin/dg.js +1 -1
- package/dist/commands/completion.js +2 -1
- package/dist/commands/config.js +11 -3
- package/dist/commands/decisions.js +155 -0
- package/dist/commands/explain.js +6 -2
- package/dist/commands/router.js +2 -0
- package/dist/commands/scan.js +2 -1
- package/dist/commands/status.js +5 -2
- package/dist/config/settings.js +144 -25
- package/dist/decisions/apply.js +128 -0
- package/dist/decisions/remember-prompt.js +97 -0
- package/dist/install-ui/block-render.js +21 -4
- package/dist/install-ui/prompt.js +14 -0
- package/dist/launcher/install-preflight.js +126 -13
- package/dist/launcher/preflight-prompt.js +29 -2
- package/dist/launcher/run.js +14 -3
- package/dist/policy/cooldown.js +104 -0
- package/dist/policy/evaluate.js +0 -15
- package/dist/presentation/provenance.js +23 -0
- package/dist/project/dgfile.js +307 -0
- package/dist/proxy/enforcement.js +2 -1
- package/dist/proxy/metadata-map.js +25 -1
- package/dist/proxy/server.js +31 -2
- package/dist/scan/command.js +35 -8
- package/dist/scan/render.js +35 -4
- package/dist/scan/scanner-report.js +31 -4
- package/dist/scan/staged.js +69 -10
- package/dist/scan-ui/LegacyApp.js +4 -4
- package/dist/scan-ui/components/InteractiveResultsView.js +64 -7
- package/dist/scan-ui/hooks/useScan.js +31 -3
- package/dist/scan-ui/shims.js +3 -0
- package/dist/scripts/detect.js +153 -0
- package/dist/scripts/gate.js +170 -0
- package/dist/scripts/rebuild.js +28 -0
- package/dist/setup/plan.js +36 -1
- package/dist/util/json-file.js +24 -0
- package/dist/util/tty-prompt.js +13 -6
- package/dist/verify/package-check.js +12 -0
- package/package.json +9 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const LIFECYCLE_HOOKS = ["preinstall", "install", "postinstall"];
|
|
5
|
+
export function computeScriptsHash(scripts, hasGyp) {
|
|
6
|
+
const canonical = JSON.stringify({
|
|
7
|
+
preinstall: lifecycleCommand(scripts, "preinstall"),
|
|
8
|
+
install: lifecycleCommand(scripts, "install"),
|
|
9
|
+
postinstall: lifecycleCommand(scripts, "postinstall"),
|
|
10
|
+
gyp: hasGyp
|
|
11
|
+
});
|
|
12
|
+
return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
|
|
13
|
+
}
|
|
14
|
+
export function detectScriptWanters(projectDir) {
|
|
15
|
+
const nodeModules = join(projectDir, "node_modules");
|
|
16
|
+
const fromLockfile = wantersFromHiddenLockfile(projectDir, nodeModules);
|
|
17
|
+
const wanters = fromLockfile ?? wantersFromWalk(nodeModules);
|
|
18
|
+
const byName = new Map();
|
|
19
|
+
for (const wanter of wanters) {
|
|
20
|
+
if (!byName.has(wanter.name)) {
|
|
21
|
+
byName.set(wanter.name, wanter);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
25
|
+
}
|
|
26
|
+
export function detectPnpmIgnoredBuilds(projectDir) {
|
|
27
|
+
const modulesYamlPath = join(projectDir, "node_modules", ".modules.yaml");
|
|
28
|
+
let content;
|
|
29
|
+
try {
|
|
30
|
+
content = readFileSync(modulesYamlPath, "utf8");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const lines = content.split("\n");
|
|
36
|
+
const startIndex = lines.findIndex((line) => /^ignoredBuilds:/.test(line));
|
|
37
|
+
if (startIndex === -1) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const startLine = lines[startIndex] ?? "";
|
|
41
|
+
if (/\[\s*\]\s*$/.test(startLine)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const names = [];
|
|
45
|
+
for (const line of lines.slice(startIndex + 1)) {
|
|
46
|
+
const match = /^\s+-\s+(.+?)\s*$/.exec(line);
|
|
47
|
+
if (!match || !match[1]) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
names.push(stripYamlQuotes(match[1]));
|
|
51
|
+
}
|
|
52
|
+
return names;
|
|
53
|
+
}
|
|
54
|
+
function stripYamlQuotes(value) {
|
|
55
|
+
if (value.length >= 2 && ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"')))) {
|
|
56
|
+
return value.slice(1, -1);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function wantersFromHiddenLockfile(projectDir, nodeModules) {
|
|
61
|
+
const lockfilePath = join(nodeModules, ".package-lock.json");
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(readFileSync(lockfilePath, "utf8"));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (!isPlainObject(parsed) || !isPlainObject(parsed.packages)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const wanters = [];
|
|
73
|
+
for (const [packagePath, entry] of Object.entries(parsed.packages)) {
|
|
74
|
+
if (!isPlainObject(entry) || entry.hasInstallScript !== true || !packagePath.startsWith("node_modules/")) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const wanter = wanterFromManifestDir(join(projectDir, packagePath));
|
|
78
|
+
if (wanter) {
|
|
79
|
+
wanters.push(wanter);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return wanters;
|
|
83
|
+
}
|
|
84
|
+
function wantersFromWalk(nodeModules) {
|
|
85
|
+
const wanters = [];
|
|
86
|
+
for (const dir of packageDirs(nodeModules)) {
|
|
87
|
+
const wanter = wanterFromManifestDir(dir);
|
|
88
|
+
if (wanter) {
|
|
89
|
+
wanters.push(wanter);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return wanters;
|
|
93
|
+
}
|
|
94
|
+
function packageDirs(nodeModules) {
|
|
95
|
+
const dirs = [];
|
|
96
|
+
for (const entry of safeReaddir(nodeModules)) {
|
|
97
|
+
if (entry.startsWith(".")) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (entry.startsWith("@")) {
|
|
101
|
+
for (const scoped of safeReaddir(join(nodeModules, entry))) {
|
|
102
|
+
if (!scoped.startsWith(".")) {
|
|
103
|
+
dirs.push(join(nodeModules, entry, scoped));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
dirs.push(join(nodeModules, entry));
|
|
109
|
+
}
|
|
110
|
+
return dirs;
|
|
111
|
+
}
|
|
112
|
+
function wanterFromManifestDir(dir) {
|
|
113
|
+
let manifest;
|
|
114
|
+
try {
|
|
115
|
+
manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (!isPlainObject(manifest) || typeof manifest.name !== "string" || manifest.name.length === 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const scripts = isPlainObject(manifest.scripts) ? manifest.scripts : {};
|
|
124
|
+
const hasGyp = existsSync(join(dir, "binding.gyp"));
|
|
125
|
+
const hooks = LIFECYCLE_HOOKS.filter((hook) => typeof lifecycleCommand(scripts, hook) === "string");
|
|
126
|
+
if (hasGyp) {
|
|
127
|
+
hooks.push("gyp");
|
|
128
|
+
}
|
|
129
|
+
if (hooks.length === 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
name: manifest.name,
|
|
134
|
+
version: typeof manifest.version === "string" ? manifest.version : "",
|
|
135
|
+
hooks,
|
|
136
|
+
scriptsHash: computeScriptsHash(scripts, hasGyp)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function lifecycleCommand(scripts, hook) {
|
|
140
|
+
const command = scripts[hook];
|
|
141
|
+
return typeof command === "string" && command.length > 0 ? command : null;
|
|
142
|
+
}
|
|
143
|
+
function safeReaddir(dir) {
|
|
144
|
+
try {
|
|
145
|
+
return readdirSync(dir);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function isPlainObject(value) {
|
|
152
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
153
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { loadUserConfig } from "../config/settings.js";
|
|
2
|
+
import { resolvePresentation } from "../presentation/mode.js";
|
|
3
|
+
import { createTheme } from "../presentation/theme.js";
|
|
4
|
+
import { loadDgFile, saveDgFile } from "../project/dgfile.js";
|
|
5
|
+
import { detectPnpmIgnoredBuilds, detectScriptWanters } from "./detect.js";
|
|
6
|
+
export function evaluateScriptGate(wanters, approvals) {
|
|
7
|
+
const approved = [];
|
|
8
|
+
const denied = [];
|
|
9
|
+
const pending = [];
|
|
10
|
+
const drifted = [];
|
|
11
|
+
for (const wanter of wanters) {
|
|
12
|
+
const entry = approvals[wanter.name];
|
|
13
|
+
if (!entry) {
|
|
14
|
+
pending.push(wanter);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (entry.scriptsHash !== wanter.scriptsHash) {
|
|
18
|
+
drifted.push({ wanter, priorHash: entry.scriptsHash });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (entry.decision === "allow") {
|
|
22
|
+
approved.push(wanter);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
denied.push(wanter);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { approved, denied, pending, drifted };
|
|
29
|
+
}
|
|
30
|
+
export function applyScriptDecisions(file, decisions, now) {
|
|
31
|
+
if (decisions.length === 0) {
|
|
32
|
+
return file;
|
|
33
|
+
}
|
|
34
|
+
const npm = { ...file.scriptApprovals.npm };
|
|
35
|
+
for (const input of decisions) {
|
|
36
|
+
npm[input.wanter.name] = {
|
|
37
|
+
decision: input.decision,
|
|
38
|
+
scriptsHash: input.wanter.scriptsHash,
|
|
39
|
+
hooks: input.wanter.hooks,
|
|
40
|
+
...(input.wanter.version ? { approvedVersion: input.wanter.version } : {}),
|
|
41
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
42
|
+
approvedAt: now.toISOString(),
|
|
43
|
+
provenance: input.provenance ?? "prompt"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
...file,
|
|
48
|
+
scriptApprovals: { ...file.scriptApprovals, npm }
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function recordScriptObservations(options) {
|
|
52
|
+
const file = loadDgFile(options.projectDir);
|
|
53
|
+
if (!file.readable || (!file.exists && !options.createIfMissing) || options.wanters.length === 0) {
|
|
54
|
+
return { written: false, path: file.path };
|
|
55
|
+
}
|
|
56
|
+
const observed = { ...file.scriptApprovals.observed };
|
|
57
|
+
let changed = false;
|
|
58
|
+
for (const wanter of options.wanters) {
|
|
59
|
+
const existing = observed[wanter.name];
|
|
60
|
+
if (existing &&
|
|
61
|
+
existing.version === wanter.version &&
|
|
62
|
+
existing.scriptsHash === wanter.scriptsHash &&
|
|
63
|
+
sameHooks(existing.hooks, wanter.hooks)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
observed[wanter.name] = {
|
|
67
|
+
version: wanter.version,
|
|
68
|
+
hooks: wanter.hooks,
|
|
69
|
+
scriptsHash: wanter.scriptsHash,
|
|
70
|
+
firstSeen: existing ? existing.firstSeen : options.now.toISOString()
|
|
71
|
+
};
|
|
72
|
+
changed = true;
|
|
73
|
+
}
|
|
74
|
+
if (!changed) {
|
|
75
|
+
return { written: false, path: file.path };
|
|
76
|
+
}
|
|
77
|
+
saveDgFile({ ...file, scriptApprovals: { ...file.scriptApprovals, observed } });
|
|
78
|
+
return { written: true, path: file.path };
|
|
79
|
+
}
|
|
80
|
+
function sameHooks(a, b) {
|
|
81
|
+
return a.length === b.length && a.every((hook, index) => hook === b[index]);
|
|
82
|
+
}
|
|
83
|
+
export function hasExplicitScriptPreference(args, env) {
|
|
84
|
+
if (args.some((arg) => arg === "--ignore-scripts" || arg.startsWith("--ignore-scripts="))) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return env.npm_config_ignore_scripts !== undefined && env.npm_config_ignore_scripts !== "";
|
|
88
|
+
}
|
|
89
|
+
export function scriptGateInstallArgs(options) {
|
|
90
|
+
if (options.mode !== "enforce") {
|
|
91
|
+
return options.args;
|
|
92
|
+
}
|
|
93
|
+
if (options.manager !== "npm" && options.manager !== "yarn") {
|
|
94
|
+
return options.args;
|
|
95
|
+
}
|
|
96
|
+
if (hasExplicitScriptPreference(options.args, options.env)) {
|
|
97
|
+
return options.args;
|
|
98
|
+
}
|
|
99
|
+
return [...options.args, "--ignore-scripts"];
|
|
100
|
+
}
|
|
101
|
+
export function scriptGateChildEnv(options) {
|
|
102
|
+
if (options.mode !== "enforce" || (options.manager !== "npm" && options.manager !== "yarn")) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
if (hasExplicitScriptPreference(options.args, options.env)) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
return { npm_config_ignore_scripts: "true" };
|
|
109
|
+
}
|
|
110
|
+
const REPORTED_NAME_LIMIT = 6;
|
|
111
|
+
export function scriptGateReportLine(options) {
|
|
112
|
+
const theme = createTheme(resolvePresentation().color);
|
|
113
|
+
if (options.manager === "pnpm") {
|
|
114
|
+
const ignored = options.pnpmIgnoredBuilds ?? [];
|
|
115
|
+
if (ignored.length === 0) {
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
return `\n ${theme.paint("muted", `dg scripts: pnpm natively blocked install scripts for ${formatNames(ignored)} — review with 'pnpm approve-builds'`)}\n`;
|
|
119
|
+
}
|
|
120
|
+
const wanters = options.wanters ?? [];
|
|
121
|
+
if (wanters.length === 0) {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
const names = wanters.map((wanter) => (wanter.version ? `${wanter.name}@${wanter.version}` : wanter.name));
|
|
125
|
+
const noun = wanters.length === 1 ? "package ran" : "packages ran";
|
|
126
|
+
return `\n ${theme.paint("muted", `dg scripts: ${wanters.length} ${noun} install scripts (${formatNames(names)}) — per-package approvals are coming; see 'dg explain script-gate'`)}\n`;
|
|
127
|
+
}
|
|
128
|
+
function formatNames(names) {
|
|
129
|
+
if (names.length <= REPORTED_NAME_LIMIT) {
|
|
130
|
+
return names.join(", ");
|
|
131
|
+
}
|
|
132
|
+
return `${names.slice(0, REPORTED_NAME_LIMIT).join(", ")}, +${names.length - REPORTED_NAME_LIMIT} more`;
|
|
133
|
+
}
|
|
134
|
+
const MUTATING_ACTIONS = {
|
|
135
|
+
npm: new Set(["install", "i", "ci", "add", "update", "dedupe"]),
|
|
136
|
+
yarn: new Set(["add", "install", "upgrade"]),
|
|
137
|
+
pnpm: new Set(["install", "i", "add", "update"])
|
|
138
|
+
};
|
|
139
|
+
export function runScriptGateAfterInstall(options) {
|
|
140
|
+
try {
|
|
141
|
+
const classification = options.classification;
|
|
142
|
+
if (classification.kind !== "protected" || classification.ecosystem !== "javascript") {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
const mutatingActions = MUTATING_ACTIONS[classification.manager];
|
|
146
|
+
if (!mutatingActions || !mutatingActions.has(classification.action)) {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
const config = loadUserConfig(options.env ?? process.env);
|
|
150
|
+
if (config.scriptGate.mode === "off") {
|
|
151
|
+
return "";
|
|
152
|
+
}
|
|
153
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
154
|
+
if (classification.manager === "pnpm") {
|
|
155
|
+
return scriptGateReportLine({ manager: "pnpm", pnpmIgnoredBuilds: detectPnpmIgnoredBuilds(projectDir) });
|
|
156
|
+
}
|
|
157
|
+
const wanters = detectScriptWanters(projectDir);
|
|
158
|
+
recordScriptObservations({
|
|
159
|
+
projectDir,
|
|
160
|
+
wanters,
|
|
161
|
+
createIfMissing: config.scriptGate.observe,
|
|
162
|
+
now: options.now ?? new Date()
|
|
163
|
+
});
|
|
164
|
+
return scriptGateReportLine({ manager: classification.manager, wanters });
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// observing must never fail or block an install that already succeeded
|
|
168
|
+
return "";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const ROOT_LIFECYCLE_HOOKS = ["preinstall", "install", "postinstall", "prepare"];
|
|
2
|
+
export function buildScriptRebuildPlan(manager, packages, includeRootLifecycle) {
|
|
3
|
+
const binaryName = manager === "pnpm" ? "pnpm" : "npm";
|
|
4
|
+
const commands = [];
|
|
5
|
+
if (packages.length > 0) {
|
|
6
|
+
commands.push(binaryName === "pnpm" ? ["rebuild", ...packages] : ["rebuild", ...packages, "--foreground-scripts"]);
|
|
7
|
+
}
|
|
8
|
+
if (includeRootLifecycle && binaryName === "npm") {
|
|
9
|
+
for (const hook of ROOT_LIFECYCLE_HOOKS) {
|
|
10
|
+
commands.push(["run", "--if-present", hook]);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return { binaryName, commands };
|
|
14
|
+
}
|
|
15
|
+
export async function runScriptRebuild(options) {
|
|
16
|
+
const failures = [];
|
|
17
|
+
let exitCode = 0;
|
|
18
|
+
for (const args of options.plan.commands) {
|
|
19
|
+
const result = await options.spawner({ binary: options.binaryPath, args, env: options.env });
|
|
20
|
+
if (result.exitCode !== 0) {
|
|
21
|
+
failures.push({ args, exitCode: result.exitCode });
|
|
22
|
+
if (exitCode === 0) {
|
|
23
|
+
exitCode = result.exitCode;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { exitCode, failures };
|
|
28
|
+
}
|
package/dist/setup/plan.js
CHANGED
|
@@ -8,6 +8,7 @@ import { dgVersion } from "../commands/version.js";
|
|
|
8
8
|
import { compareVersions, readLatestVersion } from "../commands/update.js";
|
|
9
9
|
import { AuthError, authStatus, displayTier, readAuthState } from "../auth/store.js";
|
|
10
10
|
import { ConfigError, loadUserConfig } from "../config/settings.js";
|
|
11
|
+
import { describeCooldownSettings } from "../policy/cooldown.js";
|
|
11
12
|
import { resolveRealBinary } from "../launcher/resolve-real-binary.js";
|
|
12
13
|
import { packageManagerNames } from "../launcher/classify.js";
|
|
13
14
|
import { readServiceState } from "../service/state.js";
|
|
@@ -42,6 +43,7 @@ const DOCTOR_GROUP_BY_NAME = {
|
|
|
42
43
|
"cleanup-registry-stale-entries": "setup",
|
|
43
44
|
config: "setup",
|
|
44
45
|
policy: "setup",
|
|
46
|
+
"script-gate": "setup",
|
|
45
47
|
telemetry: "setup",
|
|
46
48
|
shims: "setup",
|
|
47
49
|
"shell-rc": "setup",
|
|
@@ -275,6 +277,7 @@ export function doctorReport(options = {}) {
|
|
|
275
277
|
checks.push(configCheck(paths, env));
|
|
276
278
|
checks.push(authCheck(env));
|
|
277
279
|
checks.push(policyCheck(env));
|
|
280
|
+
checks.push(scriptGateCheck(env));
|
|
278
281
|
checks.push(telemetryCheck(env));
|
|
279
282
|
const missingShims = SHIM_COMMANDS.filter((command) => !validShim(join(shimDir, command), command));
|
|
280
283
|
checks.push({
|
|
@@ -1090,7 +1093,7 @@ function policyCheck(env) {
|
|
|
1090
1093
|
return {
|
|
1091
1094
|
name: "policy",
|
|
1092
1095
|
status: "pass",
|
|
1093
|
-
message: `Local policy mode ${config.policy.mode}; project allowlists trusted: ${config.policy.trustProjectAllowlists}`
|
|
1096
|
+
message: `Local policy mode ${config.policy.mode}; project allowlists trusted: ${config.policy.trustProjectAllowlists}; release cooldown ${describeCooldownSettings(config, env)}`
|
|
1094
1097
|
};
|
|
1095
1098
|
}
|
|
1096
1099
|
catch (error) {
|
|
@@ -1101,6 +1104,38 @@ function policyCheck(env) {
|
|
|
1101
1104
|
};
|
|
1102
1105
|
}
|
|
1103
1106
|
}
|
|
1107
|
+
function scriptGateCheck(env) {
|
|
1108
|
+
try {
|
|
1109
|
+
const mode = loadUserConfig(env).scriptGate.mode;
|
|
1110
|
+
if (mode === "off") {
|
|
1111
|
+
return {
|
|
1112
|
+
name: "script-gate",
|
|
1113
|
+
status: "pass",
|
|
1114
|
+
message: "Install-script gate is off; protected installs do not report script-running packages"
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
if (mode === "enforce") {
|
|
1118
|
+
return {
|
|
1119
|
+
name: "script-gate",
|
|
1120
|
+
status: "warn",
|
|
1121
|
+
message: "scriptGate.mode is enforce, but this dg release observes only — installs report script-running packages without blocking",
|
|
1122
|
+
fix: "enforcement ships in an upcoming release; dg config set scriptGate.mode observe clears this warning"
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
return {
|
|
1126
|
+
name: "script-gate",
|
|
1127
|
+
status: "pass",
|
|
1128
|
+
message: "Install-script gate observes: protected npm/yarn installs report packages that ran install scripts (pnpm blocks them natively)"
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
return {
|
|
1133
|
+
name: "script-gate",
|
|
1134
|
+
status: "fail",
|
|
1135
|
+
message: error instanceof ConfigError ? error.message : "Unable to read script gate config"
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1104
1139
|
function telemetryCheck(env) {
|
|
1105
1140
|
try {
|
|
1106
1141
|
const config = loadUserConfig(env);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
export function writeJsonAtomic(path, value, options = {}) {
|
|
5
|
+
mkdirSync(dirname(path), {
|
|
6
|
+
recursive: true,
|
|
7
|
+
mode: options.dirMode ?? 0o700
|
|
8
|
+
});
|
|
9
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
10
|
+
try {
|
|
11
|
+
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
flag: "wx",
|
|
14
|
+
mode: options.fileMode ?? 0o600
|
|
15
|
+
});
|
|
16
|
+
renameSync(tempPath, path);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
rmSync(tempPath, {
|
|
20
|
+
force: true
|
|
21
|
+
});
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/util/tty-prompt.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { closeSync, openSync, readSync } from "node:fs";
|
|
2
2
|
export function promptYesNo(question, defaultYes, out = process.stderr) {
|
|
3
|
+
const answer = promptLine(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"} `, out);
|
|
4
|
+
if (answer === null) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
const normalized = answer.trim().toLowerCase();
|
|
8
|
+
if (normalized === "") {
|
|
9
|
+
return defaultYes;
|
|
10
|
+
}
|
|
11
|
+
return normalized === "y" || normalized === "yes";
|
|
12
|
+
}
|
|
13
|
+
export function promptLine(question, out = process.stderr) {
|
|
3
14
|
let tty;
|
|
4
15
|
try {
|
|
5
16
|
tty = openSync("/dev/tty", "rs");
|
|
@@ -8,7 +19,7 @@ export function promptYesNo(question, defaultYes, out = process.stderr) {
|
|
|
8
19
|
return null;
|
|
9
20
|
}
|
|
10
21
|
try {
|
|
11
|
-
out.write(
|
|
22
|
+
out.write(question);
|
|
12
23
|
const byte = Buffer.alloc(1);
|
|
13
24
|
let answer = "";
|
|
14
25
|
for (;;) {
|
|
@@ -31,11 +42,7 @@ export function promptYesNo(question, defaultYes, out = process.stderr) {
|
|
|
31
42
|
}
|
|
32
43
|
answer += char;
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
if (normalized === "") {
|
|
36
|
-
return defaultYes;
|
|
37
|
-
}
|
|
38
|
-
return normalized === "y" || normalized === "yes";
|
|
45
|
+
return answer;
|
|
39
46
|
}
|
|
40
47
|
finally {
|
|
41
48
|
closeSync(tty);
|
|
@@ -2,6 +2,7 @@ import { existsSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { analyzePackages, AnalyzeError } from "../api/analyze.js";
|
|
4
4
|
import { createTheme } from "../presentation/theme.js";
|
|
5
|
+
import { provenanceLabel, provenanceDowngradeLine } from "../presentation/provenance.js";
|
|
5
6
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
6
7
|
import { isRemotePackageSpec, isSupportedLockfilePath } from "./preflight.js";
|
|
7
8
|
import { authStatus } from "../auth/store.js";
|
|
@@ -62,6 +63,16 @@ function renderResult(spec, version, result, theme, verbose) {
|
|
|
62
63
|
const action = result.action ?? "pass";
|
|
63
64
|
const badge = theme.badge(action);
|
|
64
65
|
const lines = [`${badge} ${result.name}@${version} (${spec.ecosystem}) ${theme.paint("muted", `score ${result.score}`)}`];
|
|
66
|
+
if (result.provenance) {
|
|
67
|
+
const label = verbose && result.provenance.predicateType
|
|
68
|
+
? `provenance ${provenanceLabel(result.provenance)} · ${result.provenance.predicateType}`
|
|
69
|
+
: `provenance ${provenanceLabel(result.provenance)}`;
|
|
70
|
+
lines.push(` ${theme.paint("muted", label)}`);
|
|
71
|
+
const downgrade = provenanceDowngradeLine(version, result.provenance);
|
|
72
|
+
if (downgrade) {
|
|
73
|
+
lines.push(` ${theme.paint("warn", `⚠ ${downgrade}`)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
65
76
|
const reasons = verbose ? result.reasons : result.reasons.slice(0, 6);
|
|
66
77
|
for (const reason of reasons) {
|
|
67
78
|
const glyph = action === "block" ? theme.paint("block", "✘") : action === "warn" ? theme.paint("warn", "⚠") : theme.paint("muted", "·");
|
|
@@ -157,6 +168,7 @@ export async function runPackageCheck(target, io = {}, options = {}) {
|
|
|
157
168
|
score: result.score,
|
|
158
169
|
reasons: result.reasons,
|
|
159
170
|
findings: result.findings,
|
|
171
|
+
...(result.provenance ? { provenance: result.provenance } : {}),
|
|
160
172
|
...(result.recommendation ? { recommendation: result.recommendation } : {})
|
|
161
173
|
}, null, 2)}\n`
|
|
162
174
|
: renderResult(parsed, version, result, theme, options.verbose ?? false);
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@westbayberry/dg",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Dependency Guardian supply-chain firewall CLI",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/WestBayBerry/DG_CLI.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/WestBayBerry/DG_CLI/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/WestBayBerry/DG_CLI#readme",
|
|
5
13
|
"type": "module",
|
|
6
14
|
"bin": {
|
|
7
15
|
"dg": "./dist/bin/dg.js"
|