skalpel 3.4.19 → 4.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/README.md +29 -37
- package/auth.mjs +179 -0
- package/{prosumer-hooks/bootstrap.mjs → bootstrap.mjs} +14 -8
- package/{prosumer-hooks/incremental-ingest.mjs → incremental-ingest.mjs} +5 -10
- package/{prosumer-hooks/install.mjs → install.mjs} +6 -9
- package/login.mjs +164 -0
- package/package.json +17 -57
- package/postinstall.mjs +68 -0
- package/skalpel-setup.mjs +896 -0
- package/{prosumer-hooks/skalpel-statusline.mjs → skalpel-statusline.mjs} +1 -2
- package/INSTALL.md +0 -250
- package/LICENSE +0 -201
- package/design-tokens.json +0 -52
- package/npm-bin/colors.js +0 -125
- package/npm-bin/skalpel.bug-0039.test.js +0 -232
- package/npm-bin/skalpel.js +0 -517
- package/npm-bin/skalpeld.js +0 -20
- package/postinstall/index.js +0 -277
- package/postinstall/index.test.js +0 -147
- package/postinstall/launchd/com.skalpel.skalpeld.plist.tmpl +0 -46
- package/postinstall/lib/ca-install.js +0 -268
- package/postinstall/lib/ca-install.test.js +0 -327
- package/postinstall/lib/detect-prior.js +0 -62
- package/postinstall/lib/env-inject.js +0 -105
- package/postinstall/lib/integrations.js +0 -207
- package/postinstall/lib/launch.js +0 -38
- package/postinstall/lib/log.js +0 -31
- package/postinstall/lib/paths.js +0 -236
- package/postinstall/lib/prosumer-hooks.js +0 -146
- package/postinstall/lib/prosumer-hooks.test.js +0 -278
- package/postinstall/lib/rc-edit.js +0 -168
- package/postinstall/lib/rc-edit.test.js +0 -243
- package/postinstall/lib/run-tests.js +0 -59
- package/postinstall/lib/service-register.js +0 -248
- package/postinstall/lib/sign-in.js +0 -80
- package/postinstall/lib/template.js +0 -36
- package/postinstall/preinstall.js +0 -111
- package/postinstall/preinstall.test.js +0 -47
- package/postinstall/snippets/bash.sh.tmpl +0 -12
- package/postinstall/snippets/fish.fish.tmpl +0 -11
- package/postinstall/snippets/powershell.ps1.tmpl +0 -12
- package/postinstall/snippets/zsh.sh.tmpl +0 -13
- package/postinstall/systemd/skalpeld.service.tmpl +0 -34
- package/postinstall/uninstall.js +0 -98
- package/postinstall/windows/Task.xml.tmpl +0 -42
- package/postinstall/windows/register-task.ps1.tmpl +0 -45
- package/prosumer-hooks/auth.mjs +0 -93
- package/prosumer-hooks/bootstrap.test.mjs +0 -93
- package/prosumer-hooks/incremental-ingest.test.mjs +0 -320
- /package/{prosumer-hooks/insights.mjs → insights.mjs} +0 -0
- /package/{prosumer-hooks/metrics.mjs → metrics.mjs} +0 -0
- /package/{prosumer-hooks/skalpel-hook-session-end.mjs → skalpel-hook-session-end.mjs} +0 -0
- /package/{prosumer-hooks/skalpel-hook-session.mjs → skalpel-hook-session.mjs} +0 -0
- /package/{prosumer-hooks/skalpel-hook.mjs → skalpel-hook.mjs} +0 -0
- /package/{prosumer-hooks/stats.mjs → stats.mjs} +0 -0
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Platform trust-store install for the daemon's MITM CA.
|
|
4
|
-
//
|
|
5
|
-
// macOS: login keychain via `security add-trusted-cert`.
|
|
6
|
-
// Linux: /usr/local/share/ca-certificates + `update-ca-certificates`.
|
|
7
|
-
//
|
|
8
|
-
// Contract (installDaemonCA / installMacOSCA):
|
|
9
|
-
// { ok: true } — installed (or already trusted)
|
|
10
|
-
// { ok: true, skipped: true, reason: '...' } — idempotent skip / unsupported OS
|
|
11
|
-
// { ok: false, reason: '...' } — deferred / declined / failed
|
|
12
|
-
//
|
|
13
|
-
// Failure is NEVER fatal to postinstall.
|
|
14
|
-
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const os = require('os');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const childProcess = require('child_process');
|
|
19
|
-
|
|
20
|
-
const KEYCHAIN_LABEL = 'Skalpel Local Intercept CA';
|
|
21
|
-
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
22
|
-
const SENTINEL_BASENAME = '.ca-install-pending';
|
|
23
|
-
|
|
24
|
-
const LINUX_CA_DIR = '/usr/local/share/ca-certificates';
|
|
25
|
-
const LINUX_CA_BASENAME = 'skalpel-local-intercept-ca.crt';
|
|
26
|
-
|
|
27
|
-
function loginKeychainPath(homedir) {
|
|
28
|
-
return path.join(homedir, 'Library', 'Keychains', 'login.keychain-db');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function linuxTargetPath() {
|
|
32
|
-
return path.join(LINUX_CA_DIR, LINUX_CA_BASENAME);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function logMsg(stream, msg) {
|
|
36
|
-
if (stream && typeof stream.write === 'function') {
|
|
37
|
-
stream.write(`${msg}\n`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function caFilesMatch(leftPath, rightPath) {
|
|
42
|
-
try {
|
|
43
|
-
const a = fs.readFileSync(leftPath);
|
|
44
|
-
const b = fs.readFileSync(rightPath);
|
|
45
|
-
return a.length === b.length && a.equals(b);
|
|
46
|
-
} catch (_) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function writeDeferredSentinel(caPath, opts, stderr) {
|
|
52
|
-
const sentinelDir = opts.sentinelDir || path.dirname(caPath);
|
|
53
|
-
const sentinelPath = path.join(sentinelDir, SENTINEL_BASENAME);
|
|
54
|
-
try {
|
|
55
|
-
fs.mkdirSync(sentinelDir, { recursive: true });
|
|
56
|
-
fs.writeFileSync(sentinelPath, '');
|
|
57
|
-
} catch (err) {
|
|
58
|
-
logMsg(stderr, `Skalpel: could not write ca-install sentinel at ${sentinelPath}: ${err.message}`);
|
|
59
|
-
}
|
|
60
|
-
logMsg(
|
|
61
|
-
stderr,
|
|
62
|
-
'Skalpel: daemon CA not yet generated — will install on first `skalpel login` or `codex` invocation.'
|
|
63
|
-
);
|
|
64
|
-
return { ok: false, reason: 'deferred-no-ca', sentinelPath };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function isRootUser() {
|
|
68
|
-
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function runPrivileged(spawnSync, bin, args) {
|
|
72
|
-
const fn = spawnSync || childProcess.spawnSync;
|
|
73
|
-
if (isRootUser()) {
|
|
74
|
-
return fn(bin, args, { encoding: 'utf8' });
|
|
75
|
-
}
|
|
76
|
-
return fn('sudo', ['-n', bin, ...args], { encoding: 'utf8' });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function sudoAvailable(spawnSync) {
|
|
80
|
-
const fn = spawnSync || childProcess.spawnSync;
|
|
81
|
-
const probe = fn('sudo', ['-n', 'true'], { encoding: 'utf8' });
|
|
82
|
-
return probe.status === 0;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// installDaemonCA is the postinstall entrypoint for all platforms.
|
|
86
|
-
async function installDaemonCA(caPath, opts) {
|
|
87
|
-
const platform = (opts && opts.platform) || process.platform;
|
|
88
|
-
if (platform === 'darwin') {
|
|
89
|
-
return installMacOSCA(caPath, { ...opts, platform: 'darwin' });
|
|
90
|
-
}
|
|
91
|
-
if (platform === 'linux') {
|
|
92
|
-
return installLinuxCA(caPath, opts);
|
|
93
|
-
}
|
|
94
|
-
const stderr = (opts && opts.stderr) || process.stderr;
|
|
95
|
-
logMsg(
|
|
96
|
-
stderr,
|
|
97
|
-
`Skalpel: Codex MITM CA install skipped on ${platform}; trust-store install ` +
|
|
98
|
-
'not yet implemented for this OS.'
|
|
99
|
-
);
|
|
100
|
-
return { ok: true, skipped: true, reason: 'platform' };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// installLinuxCA copies the daemon CA into the distro trust bundle and
|
|
104
|
-
// refreshes the system store.
|
|
105
|
-
async function installLinuxCA(caPath, opts) {
|
|
106
|
-
const o = opts || {};
|
|
107
|
-
const stderr = o.stderr || process.stderr;
|
|
108
|
-
const spawnSync = o.spawnSync || childProcess.spawnSync;
|
|
109
|
-
const platform = o.platform || process.platform;
|
|
110
|
-
|
|
111
|
-
if (platform !== 'linux') {
|
|
112
|
-
return { ok: true, skipped: true, reason: 'platform' };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!fs.existsSync(caPath)) {
|
|
116
|
-
return writeDeferredSentinel(caPath, o, stderr);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const dest = o.linuxTargetPath || linuxTargetPath();
|
|
120
|
-
if (fs.existsSync(dest) && caFilesMatch(caPath, dest)) {
|
|
121
|
-
return { ok: true, skipped: true, reason: 'already-installed' };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (!isRootUser() && !sudoAvailable(spawnSync)) {
|
|
125
|
-
logMsg(
|
|
126
|
-
stderr,
|
|
127
|
-
'Skalpel: Linux CA trust install needs root or passwordless sudo. Run:\n' +
|
|
128
|
-
` sudo install -m 0644 ${caPath} ${dest}\n` +
|
|
129
|
-
' sudo update-ca-certificates'
|
|
130
|
-
);
|
|
131
|
-
return { ok: false, reason: 'no-sudo' };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const installRes = runPrivileged(spawnSync, 'install', ['-m', '0644', caPath, dest]);
|
|
135
|
-
if (installRes.status !== 0) {
|
|
136
|
-
const detail = (installRes.stderr || installRes.stdout || '').trim();
|
|
137
|
-
logMsg(
|
|
138
|
-
stderr,
|
|
139
|
-
`Skalpel: failed to copy CA to ${dest}${detail ? `: ${detail}` : ''}. ` +
|
|
140
|
-
'Codex may bypass skalpel until trust is configured.'
|
|
141
|
-
);
|
|
142
|
-
return { ok: false, reason: 'install-failed', exitCode: installRes.status, detail };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const updateRes = runPrivileged(spawnSync, 'update-ca-certificates', []);
|
|
146
|
-
if (updateRes.status !== 0) {
|
|
147
|
-
const detail = (updateRes.stderr || updateRes.stdout || '').trim();
|
|
148
|
-
logMsg(
|
|
149
|
-
stderr,
|
|
150
|
-
`Skalpel: update-ca-certificates failed${detail ? `: ${detail}` : ''}.`
|
|
151
|
-
);
|
|
152
|
-
return { ok: false, reason: 'update-failed', exitCode: updateRes.status, detail };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
logMsg(stderr, 'Skalpel: daemon CA trusted in Linux system store.');
|
|
156
|
-
return { ok: true };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// installMacOSCA installs into the login keychain (darwin only).
|
|
160
|
-
async function installMacOSCA(caPath, opts) {
|
|
161
|
-
const o = opts || {};
|
|
162
|
-
const spawn = o.spawn || childProcess.spawn;
|
|
163
|
-
const stderr = o.stderr || process.stderr;
|
|
164
|
-
const homedir = o.homedir || os.homedir();
|
|
165
|
-
const timeoutMs = o.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
166
|
-
const platform = o.platform || process.platform;
|
|
167
|
-
|
|
168
|
-
if (platform !== 'darwin') {
|
|
169
|
-
return installDaemonCA(caPath, opts);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!fs.existsSync(caPath)) {
|
|
173
|
-
return writeDeferredSentinel(caPath, o, stderr);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const loginKC = loginKeychainPath(homedir);
|
|
177
|
-
|
|
178
|
-
if (await findCertificate(spawn, loginKC, timeoutMs)) {
|
|
179
|
-
return { ok: true, skipped: true, reason: 'already-installed' };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return runAddTrustedCert(spawn, stderr, caPath, loginKC, timeoutMs);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function findCertificate(spawn, loginKC, timeoutMs) {
|
|
186
|
-
return new Promise((resolve) => {
|
|
187
|
-
const child = spawn('security', ['find-certificate', '-c', KEYCHAIN_LABEL, loginKC], {
|
|
188
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
189
|
-
});
|
|
190
|
-
let settled = false;
|
|
191
|
-
const timer = setTimeout(() => {
|
|
192
|
-
if (settled) return;
|
|
193
|
-
settled = true;
|
|
194
|
-
try { child.kill('SIGTERM'); } catch (_) { /* noop */ }
|
|
195
|
-
resolve(false);
|
|
196
|
-
}, timeoutMs);
|
|
197
|
-
child.on('error', () => {
|
|
198
|
-
if (settled) return;
|
|
199
|
-
settled = true;
|
|
200
|
-
clearTimeout(timer);
|
|
201
|
-
resolve(false);
|
|
202
|
-
});
|
|
203
|
-
child.on('exit', (code) => {
|
|
204
|
-
if (settled) return;
|
|
205
|
-
settled = true;
|
|
206
|
-
clearTimeout(timer);
|
|
207
|
-
resolve(code === 0);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function runAddTrustedCert(spawn, stderr, caPath, loginKC, timeoutMs) {
|
|
213
|
-
return new Promise((resolve) => {
|
|
214
|
-
const child = spawn(
|
|
215
|
-
'security',
|
|
216
|
-
['add-trusted-cert', '-r', 'trustRoot', '-k', loginKC, caPath],
|
|
217
|
-
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
218
|
-
);
|
|
219
|
-
let settled = false;
|
|
220
|
-
const timer = setTimeout(() => {
|
|
221
|
-
if (settled) return;
|
|
222
|
-
settled = true;
|
|
223
|
-
try { child.kill('SIGTERM'); } catch (_) { /* noop */ }
|
|
224
|
-
logMsg(stderr, 'Skalpel: user did not respond to keychain prompt within 60s — skipped.');
|
|
225
|
-
resolve({ ok: false, reason: 'timeout' });
|
|
226
|
-
}, timeoutMs);
|
|
227
|
-
|
|
228
|
-
child.on('error', (err) => {
|
|
229
|
-
if (settled) return;
|
|
230
|
-
settled = true;
|
|
231
|
-
clearTimeout(timer);
|
|
232
|
-
logMsg(stderr, `Skalpel: keychain install failed to start: ${err.message}`);
|
|
233
|
-
resolve({ ok: false, reason: 'spawn-error', error: err.message });
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
child.on('exit', (code) => {
|
|
237
|
-
if (settled) return;
|
|
238
|
-
settled = true;
|
|
239
|
-
clearTimeout(timer);
|
|
240
|
-
if (code === 0) {
|
|
241
|
-
logMsg(stderr, 'Skalpel: daemon CA trusted in login keychain.');
|
|
242
|
-
resolve({ ok: true });
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
logMsg(
|
|
246
|
-
stderr,
|
|
247
|
-
'Skalpel: Codex MITM CA not trusted. Codex traffic will bypass skalpel until ' +
|
|
248
|
-
'you re-run the prompt (next `skalpel login` or `codex` invocation) or ' +
|
|
249
|
-
'manually trust ~/.skalpel/mitm-ca.pem.'
|
|
250
|
-
);
|
|
251
|
-
resolve({ ok: false, reason: 'declined-or-failed', exitCode: code });
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
module.exports = {
|
|
257
|
-
installDaemonCA,
|
|
258
|
-
installMacOSCA,
|
|
259
|
-
installLinuxCA,
|
|
260
|
-
KEYCHAIN_LABEL,
|
|
261
|
-
SENTINEL_BASENAME,
|
|
262
|
-
LINUX_CA_DIR,
|
|
263
|
-
LINUX_CA_BASENAME,
|
|
264
|
-
linuxTargetPath,
|
|
265
|
-
loginKeychainPath,
|
|
266
|
-
caFilesMatch,
|
|
267
|
-
runPrivileged,
|
|
268
|
-
};
|
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// ca-install tests.
|
|
3
|
-
//
|
|
4
|
-
// Stdlib only: assert + fs + os + path + events. No mocha / no jest.
|
|
5
|
-
// Matches the testing style of rc-edit.test.js. spawn is injected so
|
|
6
|
-
// the tests never touch the real `security` binary or the user's
|
|
7
|
-
// login keychain.
|
|
8
|
-
//
|
|
9
|
-
// Test cases (the promise.txt names each one explicitly):
|
|
10
|
-
// - non-darwin — platform != 'darwin' short-circuits
|
|
11
|
-
// - already-installed — find-certificate returns 0 → skip
|
|
12
|
-
// - success — add-trusted-cert exits 0
|
|
13
|
-
// - declined — add-trusted-cert exits non-zero
|
|
14
|
-
// - timeout — add-trusted-cert never exits; killed
|
|
15
|
-
// - deferred — CA file missing → sentinel written
|
|
16
|
-
|
|
17
|
-
'use strict';
|
|
18
|
-
|
|
19
|
-
const fs = require('fs');
|
|
20
|
-
const os = require('os');
|
|
21
|
-
const path = require('path');
|
|
22
|
-
const { EventEmitter } = require('events');
|
|
23
|
-
const assert = require('assert');
|
|
24
|
-
|
|
25
|
-
const ca = require('./ca-install');
|
|
26
|
-
|
|
27
|
-
let pass = 0;
|
|
28
|
-
let fail = 0;
|
|
29
|
-
|
|
30
|
-
function test(name, fn) {
|
|
31
|
-
return Promise.resolve()
|
|
32
|
-
.then(fn)
|
|
33
|
-
.then(() => {
|
|
34
|
-
process.stdout.write(` PASS ${name}\n`);
|
|
35
|
-
pass += 1;
|
|
36
|
-
})
|
|
37
|
-
.catch((err) => {
|
|
38
|
-
process.stderr.write(` FAIL ${name}\n ${err && err.stack ? err.stack : err}\n`);
|
|
39
|
-
fail += 1;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// fakeChild implements the subset of ChildProcess that installMacOSCA
|
|
44
|
-
// consumes: an event emitter with `kill()`. Plug into makeSpawn().
|
|
45
|
-
function fakeChild() {
|
|
46
|
-
const ee = new EventEmitter();
|
|
47
|
-
ee.killed = false;
|
|
48
|
-
ee.kill = function () { ee.killed = true; };
|
|
49
|
-
return ee;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// makeSpawn returns a fake spawn that scripts each successive
|
|
53
|
-
// invocation. Each script entry is either:
|
|
54
|
-
// { cmd: 'find-certificate', exit: 0|N }
|
|
55
|
-
// { cmd: 'add-trusted-cert', exit: 0|N, async?: true (delay exit), error?: Error }
|
|
56
|
-
// If `async` is set, the child does NOT emit `exit` — the caller is
|
|
57
|
-
// responsible for triggering the timeout path.
|
|
58
|
-
function makeSpawn(script) {
|
|
59
|
-
const calls = [];
|
|
60
|
-
let i = 0;
|
|
61
|
-
function spawn(bin, args) {
|
|
62
|
-
const verb = args[0];
|
|
63
|
-
const step = script[i] || {};
|
|
64
|
-
i += 1;
|
|
65
|
-
calls.push({ bin, args, verb });
|
|
66
|
-
const child = fakeChild();
|
|
67
|
-
if (step.error) {
|
|
68
|
-
process.nextTick(() => child.emit('error', step.error));
|
|
69
|
-
return child;
|
|
70
|
-
}
|
|
71
|
-
if (step.async) {
|
|
72
|
-
// never emits exit — caller's setTimeout drives the timeout
|
|
73
|
-
return child;
|
|
74
|
-
}
|
|
75
|
-
const exitCode = typeof step.exit === 'number' ? step.exit : 0;
|
|
76
|
-
process.nextTick(() => child.emit('exit', exitCode));
|
|
77
|
-
return child;
|
|
78
|
-
}
|
|
79
|
-
spawn.calls = calls;
|
|
80
|
-
return spawn;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function captureStderr() {
|
|
84
|
-
const buf = [];
|
|
85
|
-
return {
|
|
86
|
-
write(s) { buf.push(s); },
|
|
87
|
-
text() { return buf.join(''); },
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function tmpdir() {
|
|
92
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'ca-install-test-'));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function run() {
|
|
96
|
-
process.stdout.write('ca-install tests:\n');
|
|
97
|
-
|
|
98
|
-
await test('win32 platform skip via installDaemonCA', async () => {
|
|
99
|
-
const stderr = captureStderr();
|
|
100
|
-
const result = await ca.installDaemonCA('/anything', {
|
|
101
|
-
stderr,
|
|
102
|
-
platform: 'win32',
|
|
103
|
-
});
|
|
104
|
-
assert.strictEqual(result.ok, true);
|
|
105
|
-
assert.strictEqual(result.skipped, true);
|
|
106
|
-
assert.strictEqual(result.reason, 'platform');
|
|
107
|
-
assert.ok(/win32/.test(stderr.text()), 'should mention current platform');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await test('linux deferred writes sentinel', async () => {
|
|
111
|
-
const stderr = captureStderr();
|
|
112
|
-
const dir = tmpdir();
|
|
113
|
-
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
114
|
-
const result = await ca.installLinuxCA(caPath, {
|
|
115
|
-
stderr,
|
|
116
|
-
platform: 'linux',
|
|
117
|
-
sentinelDir: dir,
|
|
118
|
-
spawnSync: () => ({ status: 0 }),
|
|
119
|
-
});
|
|
120
|
-
assert.strictEqual(result.ok, false);
|
|
121
|
-
assert.strictEqual(result.reason, 'deferred-no-ca');
|
|
122
|
-
const sentinel = path.join(dir, ca.SENTINEL_BASENAME);
|
|
123
|
-
assert.ok(fs.existsSync(sentinel));
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
await test('linux already-installed idempotency', async () => {
|
|
127
|
-
const stderr = captureStderr();
|
|
128
|
-
const dir = tmpdir();
|
|
129
|
-
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
130
|
-
const dest = path.join(dir, 'skalpel-local-intercept-ca.crt');
|
|
131
|
-
const body = 'test-pem';
|
|
132
|
-
fs.writeFileSync(caPath, body);
|
|
133
|
-
fs.writeFileSync(dest, body);
|
|
134
|
-
const spawnSync = () => {
|
|
135
|
-
throw new Error('spawnSync should not run when already installed');
|
|
136
|
-
};
|
|
137
|
-
const result = await ca.installLinuxCA(caPath, {
|
|
138
|
-
stderr,
|
|
139
|
-
platform: 'linux',
|
|
140
|
-
linuxTargetPath: dest,
|
|
141
|
-
spawnSync,
|
|
142
|
-
});
|
|
143
|
-
assert.strictEqual(result.ok, true);
|
|
144
|
-
assert.strictEqual(result.skipped, true);
|
|
145
|
-
assert.strictEqual(result.reason, 'already-installed');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
await test('linux success path', async () => {
|
|
149
|
-
const stderr = captureStderr();
|
|
150
|
-
const dir = tmpdir();
|
|
151
|
-
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
152
|
-
const dest = path.join(dir, 'skalpel-local-intercept-ca.crt');
|
|
153
|
-
fs.writeFileSync(caPath, 'pem');
|
|
154
|
-
const calls = [];
|
|
155
|
-
const spawnSync = (bin, args) => {
|
|
156
|
-
if (bin === 'sudo') {
|
|
157
|
-
calls.push({ bin: args[1], args: args.slice(2) });
|
|
158
|
-
} else {
|
|
159
|
-
calls.push({ bin, args });
|
|
160
|
-
}
|
|
161
|
-
if (calls[calls.length - 1].bin === 'install') {
|
|
162
|
-
fs.copyFileSync(caPath, dest);
|
|
163
|
-
}
|
|
164
|
-
return { status: 0, stdout: '', stderr: '' };
|
|
165
|
-
};
|
|
166
|
-
const origGetuid = process.getuid;
|
|
167
|
-
process.getuid = () => 1000;
|
|
168
|
-
try {
|
|
169
|
-
const result = await ca.installLinuxCA(caPath, {
|
|
170
|
-
stderr,
|
|
171
|
-
platform: 'linux',
|
|
172
|
-
linuxTargetPath: dest,
|
|
173
|
-
spawnSync,
|
|
174
|
-
});
|
|
175
|
-
assert.strictEqual(result.ok, true);
|
|
176
|
-
assert.ok(fs.existsSync(dest));
|
|
177
|
-
const bins = calls.map((c) => c.bin);
|
|
178
|
-
assert.ok(bins.includes('install'), `expected install in ${bins}`);
|
|
179
|
-
assert.ok(bins.includes('update-ca-certificates'), `expected update-ca-certificates in ${bins}`);
|
|
180
|
-
} finally {
|
|
181
|
-
process.getuid = origGetuid;
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
await test('linux no-sudo soft failure', async () => {
|
|
186
|
-
const stderr = captureStderr();
|
|
187
|
-
const dir = tmpdir();
|
|
188
|
-
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
189
|
-
const dest = path.join(dir, 'skalpel-local-intercept-ca.crt');
|
|
190
|
-
fs.writeFileSync(caPath, 'pem');
|
|
191
|
-
const spawnSync = (bin) => {
|
|
192
|
-
if (bin === 'sudo') return { status: 1, stdout: '', stderr: 'sorry' };
|
|
193
|
-
return { status: 0, stdout: '', stderr: '' };
|
|
194
|
-
};
|
|
195
|
-
const origGetuid = process.getuid;
|
|
196
|
-
process.getuid = () => 1000;
|
|
197
|
-
try {
|
|
198
|
-
const result = await ca.installLinuxCA(caPath, {
|
|
199
|
-
stderr,
|
|
200
|
-
platform: 'linux',
|
|
201
|
-
linuxTargetPath: dest,
|
|
202
|
-
spawnSync,
|
|
203
|
-
});
|
|
204
|
-
assert.strictEqual(result.ok, false);
|
|
205
|
-
assert.strictEqual(result.reason, 'no-sudo');
|
|
206
|
-
assert.ok(/passwordless sudo|sudo install/.test(stderr.text()));
|
|
207
|
-
} finally {
|
|
208
|
-
process.getuid = origGetuid;
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
await test('already-installed idempotency', async () => {
|
|
213
|
-
const stderr = captureStderr();
|
|
214
|
-
const dir = tmpdir();
|
|
215
|
-
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
216
|
-
fs.writeFileSync(caPath, 'dummy');
|
|
217
|
-
// find-certificate exits 0 → already trusted; no add-trusted-cert call.
|
|
218
|
-
const spawn = makeSpawn([{ cmd: 'find-certificate', exit: 0 }]);
|
|
219
|
-
const result = await ca.installMacOSCA(caPath, {
|
|
220
|
-
spawn,
|
|
221
|
-
stderr,
|
|
222
|
-
platform: 'darwin',
|
|
223
|
-
homedir: dir,
|
|
224
|
-
});
|
|
225
|
-
assert.strictEqual(result.ok, true);
|
|
226
|
-
assert.strictEqual(result.skipped, true);
|
|
227
|
-
assert.strictEqual(result.reason, 'already-installed');
|
|
228
|
-
assert.strictEqual(spawn.calls.length, 1);
|
|
229
|
-
assert.strictEqual(spawn.calls[0].verb, 'find-certificate');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
await test('success path', async () => {
|
|
233
|
-
const stderr = captureStderr();
|
|
234
|
-
const dir = tmpdir();
|
|
235
|
-
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
236
|
-
fs.writeFileSync(caPath, 'dummy');
|
|
237
|
-
const spawn = makeSpawn([
|
|
238
|
-
{ cmd: 'find-certificate', exit: 1 },
|
|
239
|
-
{ cmd: 'add-trusted-cert', exit: 0 },
|
|
240
|
-
]);
|
|
241
|
-
const result = await ca.installMacOSCA(caPath, {
|
|
242
|
-
spawn,
|
|
243
|
-
stderr,
|
|
244
|
-
platform: 'darwin',
|
|
245
|
-
homedir: dir,
|
|
246
|
-
});
|
|
247
|
-
assert.strictEqual(result.ok, true);
|
|
248
|
-
assert.strictEqual(result.skipped, undefined);
|
|
249
|
-
assert.strictEqual(spawn.calls.length, 2);
|
|
250
|
-
assert.strictEqual(spawn.calls[1].verb, 'add-trusted-cert');
|
|
251
|
-
assert.deepStrictEqual(
|
|
252
|
-
spawn.calls[1].args.slice(0, 4),
|
|
253
|
-
['add-trusted-cert', '-r', 'trustRoot', '-k']
|
|
254
|
-
);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
await test('declined path', async () => {
|
|
258
|
-
const stderr = captureStderr();
|
|
259
|
-
const dir = tmpdir();
|
|
260
|
-
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
261
|
-
fs.writeFileSync(caPath, 'dummy');
|
|
262
|
-
const spawn = makeSpawn([
|
|
263
|
-
{ cmd: 'find-certificate', exit: 1 },
|
|
264
|
-
{ cmd: 'add-trusted-cert', exit: 128 },
|
|
265
|
-
]);
|
|
266
|
-
const result = await ca.installMacOSCA(caPath, {
|
|
267
|
-
spawn,
|
|
268
|
-
stderr,
|
|
269
|
-
platform: 'darwin',
|
|
270
|
-
homedir: dir,
|
|
271
|
-
});
|
|
272
|
-
assert.strictEqual(result.ok, false);
|
|
273
|
-
assert.strictEqual(result.reason, 'declined-or-failed');
|
|
274
|
-
assert.strictEqual(result.exitCode, 128);
|
|
275
|
-
assert.ok(/CA not trusted|bypass skalpel/.test(stderr.text()), 'should warn user');
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
await test('timeout path', async () => {
|
|
279
|
-
const stderr = captureStderr();
|
|
280
|
-
const dir = tmpdir();
|
|
281
|
-
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
282
|
-
fs.writeFileSync(caPath, 'dummy');
|
|
283
|
-
const spawn = makeSpawn([
|
|
284
|
-
{ cmd: 'find-certificate', exit: 1 },
|
|
285
|
-
{ cmd: 'add-trusted-cert', async: true }, // never exits
|
|
286
|
-
]);
|
|
287
|
-
const result = await ca.installMacOSCA(caPath, {
|
|
288
|
-
spawn,
|
|
289
|
-
stderr,
|
|
290
|
-
platform: 'darwin',
|
|
291
|
-
homedir: dir,
|
|
292
|
-
timeoutMs: 30, // small so test is fast
|
|
293
|
-
});
|
|
294
|
-
assert.strictEqual(result.ok, false);
|
|
295
|
-
assert.strictEqual(result.reason, 'timeout');
|
|
296
|
-
assert.ok(/did not respond|skipped/.test(stderr.text()), 'should explain skip');
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
await test('deferred (no CA file yet) writes sentinel', async () => {
|
|
300
|
-
const stderr = captureStderr();
|
|
301
|
-
const dir = tmpdir();
|
|
302
|
-
const caPath = path.join(dir, 'mitm-ca.pem');
|
|
303
|
-
// Do NOT create caPath — simulate daemon hasn't booted yet.
|
|
304
|
-
const spawn = makeSpawn([]);
|
|
305
|
-
const result = await ca.installMacOSCA(caPath, {
|
|
306
|
-
spawn,
|
|
307
|
-
stderr,
|
|
308
|
-
platform: 'darwin',
|
|
309
|
-
homedir: dir,
|
|
310
|
-
sentinelDir: dir,
|
|
311
|
-
});
|
|
312
|
-
assert.strictEqual(result.ok, false);
|
|
313
|
-
assert.strictEqual(result.reason, 'deferred-no-ca');
|
|
314
|
-
assert.strictEqual(spawn.calls.length, 0, 'no security spawn on deferred path');
|
|
315
|
-
const sentinel = path.join(dir, ca.SENTINEL_BASENAME);
|
|
316
|
-
assert.ok(fs.existsSync(sentinel), `sentinel should exist at ${sentinel}`);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
process.stdout.write(`\n pass=${pass} fail=${fail}\n`);
|
|
320
|
-
return fail === 0 ? 0 : 1;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (require.main === module) {
|
|
324
|
-
run().then((code) => process.exit(code));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
module.exports = { run };
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// Step 1: detect prior install.
|
|
2
|
-
//
|
|
3
|
-
// v3.0.1 fix: `prior` now means specifically "user has auth state" —
|
|
4
|
-
// i.e. auth.json exists. v3.0.0 treated any of {auth.json, config.toml,
|
|
5
|
-
// lock file, service registration} as "prior" and skipped sign-in.
|
|
6
|
-
// That meant once a machine had ever installed skalpel, the systemd /
|
|
7
|
-
// launchd unit file persists outside configDir; every subsequent
|
|
8
|
-
// `npm install skalpel` would skip sign-in forever, and the user
|
|
9
|
-
// could never get logged in via the install flow on re-install.
|
|
10
|
-
//
|
|
11
|
-
// The other probes (config.toml / lock file / service registration)
|
|
12
|
-
// are informational only — they get reported in the log but no longer
|
|
13
|
-
// gate sign-in. SPEC.md §9.6: a fresh user with no auth.json must
|
|
14
|
-
// always be offered the sign-in step, regardless of mechanical
|
|
15
|
-
// install state (service units, rc snippets) that survived a prior
|
|
16
|
-
// install.
|
|
17
|
-
|
|
18
|
-
'use strict';
|
|
19
|
-
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const log = require('./log');
|
|
22
|
-
const paths = require('./paths');
|
|
23
|
-
|
|
24
|
-
function fileExists(p) {
|
|
25
|
-
try {
|
|
26
|
-
fs.accessSync(p, fs.constants.F_OK);
|
|
27
|
-
return true;
|
|
28
|
-
} catch (_e) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function run({ dryRun }) {
|
|
34
|
-
const probes = [
|
|
35
|
-
{ label: 'auth.json', path: paths.authFile(), gatesPrior: true },
|
|
36
|
-
{ label: 'config.toml', path: paths.configToml(), gatesPrior: false },
|
|
37
|
-
{ label: 'lock file', path: paths.lockFile(), gatesPrior: false },
|
|
38
|
-
{ label: 'service registration', path: paths.servicePath(), gatesPrior: false },
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const findings = probes.map((p) => ({ ...p, exists: fileExists(p.path) }));
|
|
42
|
-
// `prior` reflects only auth.json existence — see header comment.
|
|
43
|
-
const prior = findings.some((f) => f.gatesPrior && f.exists);
|
|
44
|
-
|
|
45
|
-
if (dryRun) {
|
|
46
|
-
log.dryRun(`step 1 detect-prior: would probe ${findings.length} paths`);
|
|
47
|
-
}
|
|
48
|
-
for (const f of findings) {
|
|
49
|
-
log.info(
|
|
50
|
-
` ${f.exists ? 'present' : 'absent '} — ${f.label}: ${f.path}`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
log.info(
|
|
55
|
-
prior
|
|
56
|
-
? 'detected prior auth (auth.json present) — sign-in will skip'
|
|
57
|
-
: 'no prior auth detected — sign-in will run'
|
|
58
|
-
);
|
|
59
|
-
return { prior, findings };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
module.exports = { run };
|