@coworker-jp/aidr 0.1.11 → 0.1.75
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 +66 -14
- package/package.json +1 -1
- package/src/agents/standalone.mjs +3 -3
- package/src/binary-fetcher.mjs +6 -58
- package/src/cli.mjs +198 -160
- package/src/env-gate.mjs +208 -0
- package/src/red-team.mjs +169 -0
- package/src/scheduled.mjs +83 -222
- package/src/sentinel.mjs +143 -266
- package/src/templates.mjs +7 -6
- package/src/verify.mjs +3 -4
- package/src/browser-extension.mjs +0 -83
- package/src/invoker-resolve.mjs +0 -63
package/src/sentinel.mjs
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* coworker-sentinel
|
|
2
|
+
* LEGACY coworker-sentinel cleanup helpers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* aidr no longer installs coworker-sentinel (the `--with-sentinel` flag is
|
|
5
|
+
* retired). Sentinel now ships via OS-native installers:
|
|
6
|
+
* Windows: coworker-sentinel-setup.exe (Inno Setup, own uninstaller)
|
|
7
|
+
* macOS: CoworkerSentinel.dmg (the app self-installs/uninstalls)
|
|
8
|
+
* Linux: coworker-sentinel.deb (dpkg postrm owns the cleanup;
|
|
9
|
+
* unit lives at /lib/systemd/system/coworker-sentinel.service)
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* What remains here is uninstall-only: `aidr uninstall` sweeps the artifacts
|
|
12
|
+
* an old npx-era `aidr install --with-sentinel` wrote on Linux —
|
|
13
|
+
* /etc/systemd/system/coworker-sentinel.service
|
|
14
|
+
* /opt/coworker/aidr/bin/coworker-sentinel (+ ai-scanner)
|
|
15
|
+
* /etc/aidr/sentinel-settings.json, /etc/aidr/aidr.env
|
|
16
|
+
* /var/log/coworker/sentinel.log (+ logrotate config)
|
|
17
|
+
*
|
|
18
|
+
* It must NEVER touch a deb-owned install: the deb's systemd unit is under
|
|
19
|
+
* /lib/systemd/system (not /etc/systemd/system), and when a deb install is
|
|
20
|
+
* present the shared files under /etc/aidr and the OS-trusted root CA are
|
|
21
|
+
* left alone (the deb daemon keeps using them; dpkg purge removes them).
|
|
12
22
|
*/
|
|
13
23
|
import fs from "node:fs/promises";
|
|
14
24
|
import path from "node:path";
|
|
15
25
|
import { execFile, spawnSync } from "node:child_process";
|
|
16
|
-
import { resolveInvokerUser } from "./invoker-resolve.mjs";
|
|
17
26
|
import { promisify } from "node:util";
|
|
18
|
-
import { fetchSentinel, detectPlatform } from "./binary-fetcher.mjs";
|
|
19
27
|
import {
|
|
20
28
|
SYSTEM_BIN_DIR,
|
|
29
|
+
SYSTEM_BIN,
|
|
21
30
|
SYSTEM_ENV_FILE,
|
|
22
31
|
SENTINEL_LOG_FILE,
|
|
23
|
-
ensureSystemLogDir,
|
|
24
|
-
writeLogrotateConfig,
|
|
25
32
|
removeSystemLog,
|
|
26
33
|
} from "./scheduled.mjs";
|
|
27
34
|
|
|
@@ -29,13 +36,30 @@ const execFileP = promisify(execFile);
|
|
|
29
36
|
|
|
30
37
|
const ROOT_PREFIX = process.env.AIDR_SCHEDULED_ROOT || "";
|
|
31
38
|
|
|
39
|
+
// Legacy npx-era binary location (never used by the deb / setup.exe / DMG).
|
|
32
40
|
export const SENTINEL_BIN = path.join(
|
|
33
41
|
SYSTEM_BIN_DIR,
|
|
34
42
|
process.platform === "win32" ? "coworker-sentinel.exe" : "coworker-sentinel",
|
|
35
43
|
);
|
|
44
|
+
// Daemon-owned settings file (endpoint-info collection toggle etc.). Shared
|
|
45
|
+
// between the legacy npx daemon and a deb-installed daemon — only removed
|
|
46
|
+
// when no deb install is present.
|
|
47
|
+
export const SENTINEL_SETTINGS_FILE = path.join(
|
|
48
|
+
path.dirname(SYSTEM_ENV_FILE),
|
|
49
|
+
"sentinel-settings.json",
|
|
50
|
+
);
|
|
36
51
|
export const SENTINEL_PLIST = ROOT_PREFIX + "/Library/LaunchDaemons/jp.coworker.sentinel.plist";
|
|
52
|
+
// Legacy npx-written unit. The deb ships its unit at DEB_SENTINEL_SERVICE —
|
|
53
|
+
// the two paths are how we tell the layouts apart.
|
|
37
54
|
export const SENTINEL_SERVICE = ROOT_PREFIX + "/etc/systemd/system/coworker-sentinel.service";
|
|
38
|
-
export const
|
|
55
|
+
export const DEB_SENTINEL_SERVICE = ROOT_PREFIX + "/lib/systemd/system/coworker-sentinel.service";
|
|
56
|
+
|
|
57
|
+
export const MACOS_DMG_HINT =
|
|
58
|
+
"[sentinel] macOS の coworker-sentinel は DMG (CoworkerSentinel.app) から導入します。\n" +
|
|
59
|
+
"[sentinel] ユーザーポータルの Download ページ、または GET /download/setup/macos から\n" +
|
|
60
|
+
"[sentinel] CoworkerSentinel.dmg をダウンロードし、Applications にドラッグ → 起動 →\n" +
|
|
61
|
+
"[sentinel] アクセスキーを入力 → 管理者パスワードを入力してください。npx 経由の\n" +
|
|
62
|
+
"[sentinel] インストール/アンインストールは macOS では行われません。";
|
|
39
63
|
|
|
40
64
|
function isRoot() {
|
|
41
65
|
if (process.env.AI_SCANNER_FAKE_ROOT === "1") return true;
|
|
@@ -54,270 +78,109 @@ async function exists(p) {
|
|
|
54
78
|
try { await fs.access(p); return true; } catch { return false; }
|
|
55
79
|
}
|
|
56
80
|
|
|
81
|
+
/**
|
|
82
|
+
* True only for the LEGACY npx-installed layout that aidr itself once wrote
|
|
83
|
+
* (Linux systemd unit under /etc/systemd/system). A deb install (unit under
|
|
84
|
+
* /lib/systemd/system) must NOT match — its cleanup belongs to dpkg postrm.
|
|
85
|
+
* macOS (DMG-owned) and Windows (setup.exe-owned, and indistinguishable from
|
|
86
|
+
* the npx-era layout — same %ProgramFiles%\coworker\aidr\bin + service name)
|
|
87
|
+
* always report false: aidr never uninstalls sentinel there.
|
|
88
|
+
*/
|
|
57
89
|
export async function isSentinelInstalled() {
|
|
58
|
-
if (process.platform === "win32")
|
|
59
|
-
|
|
60
|
-
return spawnSync("sc", ["query", SENTINEL_WIN_SERVICE], { stdio: "ignore" }).status === 0;
|
|
61
|
-
} catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return (
|
|
66
|
-
(await exists(SENTINEL_PLIST)) ||
|
|
67
|
-
(await exists(SENTINEL_SERVICE))
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function buildLaunchdPlist(envFile) {
|
|
72
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
73
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
74
|
-
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
75
|
-
<plist version="1.0">
|
|
76
|
-
<dict>
|
|
77
|
-
<key>Label</key>
|
|
78
|
-
<string>jp.coworker.sentinel</string>
|
|
79
|
-
<key>ProgramArguments</key>
|
|
80
|
-
<array>
|
|
81
|
-
<string>/bin/sh</string>
|
|
82
|
-
<string>-c</string>
|
|
83
|
-
<string>set -a; . ${envFile}; set +a; exec ${SENTINEL_BIN}</string>
|
|
84
|
-
</array>
|
|
85
|
-
<key>RunAtLoad</key>
|
|
86
|
-
<true/>
|
|
87
|
-
<key>KeepAlive</key>
|
|
88
|
-
<true/>
|
|
89
|
-
<key>StandardOutPath</key>
|
|
90
|
-
<string>${SENTINEL_LOG_FILE}</string>
|
|
91
|
-
<key>StandardErrorPath</key>
|
|
92
|
-
<string>${SENTINEL_LOG_FILE}</string>
|
|
93
|
-
<key>ThrottleInterval</key>
|
|
94
|
-
<integer>10</integer>
|
|
95
|
-
</dict>
|
|
96
|
-
</plist>
|
|
97
|
-
`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function buildSystemdService(envFile) {
|
|
101
|
-
return `[Unit]
|
|
102
|
-
Description=coworker-sentinel Host EDR Daemon
|
|
103
|
-
After=network.target
|
|
104
|
-
StartLimitIntervalSec=60
|
|
105
|
-
StartLimitBurst=5
|
|
106
|
-
|
|
107
|
-
[Service]
|
|
108
|
-
Type=simple
|
|
109
|
-
EnvironmentFile=${envFile}
|
|
110
|
-
ExecStart=${SENTINEL_BIN}
|
|
111
|
-
Restart=on-failure
|
|
112
|
-
RestartSec=5
|
|
113
|
-
StandardOutput=append:${SENTINEL_LOG_FILE}
|
|
114
|
-
StandardError=append:${SENTINEL_LOG_FILE}
|
|
115
|
-
SyslogIdentifier=coworker-sentinel
|
|
116
|
-
|
|
117
|
-
[Install]
|
|
118
|
-
WantedBy=multi-user.target
|
|
119
|
-
`;
|
|
90
|
+
if (process.platform === "darwin" || process.platform === "win32") return false;
|
|
91
|
+
return exists(SENTINEL_SERVICE);
|
|
120
92
|
}
|
|
121
93
|
|
|
122
94
|
/**
|
|
123
|
-
*
|
|
95
|
+
* Remove a LEGACY npx-installed sentinel daemon (Linux only).
|
|
124
96
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
97
|
+
* Deb coexistence rules:
|
|
98
|
+
* - the deb unit (/lib/systemd/system) and deb-owned files are never touched;
|
|
99
|
+
* - when a deb install is present, the shared /etc/aidr files, the system
|
|
100
|
+
* ai-scanner copy, the root CA, and the log file are all preserved
|
|
101
|
+
* (the deb daemon still uses them) — only the stale legacy unit and the
|
|
102
|
+
* legacy /opt binary are swept;
|
|
103
|
+
* - `systemctl disable --now` is only issued when no deb unit exists, so a
|
|
104
|
+
* running deb service is never stopped.
|
|
131
105
|
*/
|
|
132
|
-
export async function
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
: "installSentinel requires root privileges.\n" +
|
|
141
|
-
"Re-run with: sudo -E npx @coworker-jp/aidr install --key <key> --with-sentinel",
|
|
106
|
+
export async function uninstallSentinel({ dryRun = false } = {}) {
|
|
107
|
+
// macOS: DMG-based. Uninstall is done from the CoworkerSentinel.app's
|
|
108
|
+
// Uninstall button (which unloads the daemon, runs uninstall-ca, and
|
|
109
|
+
// deactivates the system extension behind one admin prompt). No-op + hint.
|
|
110
|
+
if (process.platform === "darwin") {
|
|
111
|
+
console.error(MACOS_DMG_HINT);
|
|
112
|
+
console.error(
|
|
113
|
+
"[sentinel] アンインストールは CoworkerSentinel.app の「Uninstall」ボタンから行ってください。",
|
|
142
114
|
);
|
|
115
|
+
return;
|
|
143
116
|
}
|
|
144
117
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
if (!dryRun && !skipBinary) {
|
|
150
|
-
await fs.mkdir(SYSTEM_BIN_DIR, { recursive: true });
|
|
151
|
-
console.error(`[sentinel] Downloading binary → ${SENTINEL_BIN}`);
|
|
152
|
-
await fetchSentinel(SENTINEL_BIN, { accessKey, presignedUrl, dllPresignedUrl, env });
|
|
153
|
-
} else if (skipBinary) {
|
|
154
|
-
console.error(`[sentinel] Skipping binary download (--skip-binary)`);
|
|
155
|
-
} else {
|
|
156
|
-
console.error(`[sentinel] DRY-RUN: would download → ${SENTINEL_BIN}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 2. Create the world-readable log dir + touch sentinel.log + drop the
|
|
160
|
-
// shared logrotate config (covering both daemons; idempotent).
|
|
161
|
-
await ensureSystemLogDir(SENTINEL_LOG_FILE, { dryRun });
|
|
162
|
-
await writeLogrotateConfig({ dryRun });
|
|
163
|
-
|
|
164
|
-
// 3. Write env file (append AI_SCANNER_ACCESS_KEY + AI_SCANNER_INVOKER_USER
|
|
165
|
-
// if missing). When --with-sentinel runs alongside --scheduled,
|
|
166
|
-
// scheduled.mjs's writeEnvFile has already written both lines and
|
|
167
|
-
// we no-op. When --with-sentinel runs standalone (no --scheduled),
|
|
168
|
-
// we are the only writer of aidr.env and must capture SUDO_USER
|
|
169
|
-
// ourselves; otherwise the daemon (running as root with no useful
|
|
170
|
-
// git/Apple/USER) reports `unknown`. Identity resolution itself
|
|
171
|
-
// (Apple ID / git email / username) happens at scan time inside
|
|
172
|
-
// the Rust binary using `sudo -u <invoker>`.
|
|
173
|
-
if (accessKey && !dryRun) {
|
|
174
|
-
let content = "";
|
|
175
|
-
try { content = await fs.readFile(SYSTEM_ENV_FILE, "utf8"); } catch { /* first run */ }
|
|
176
|
-
let appended = "";
|
|
177
|
-
if (!content.includes("AI_SCANNER_ACCESS_KEY")) {
|
|
178
|
-
appended += `\nAI_SCANNER_ACCESS_KEY=${accessKey}\n`;
|
|
179
|
-
}
|
|
180
|
-
if (!content.includes("AI_SCANNER_INVOKER_USER")) {
|
|
181
|
-
const invoker = resolveInvokerUser();
|
|
182
|
-
if (invoker) {
|
|
183
|
-
appended += `AI_SCANNER_INVOKER_USER=${invoker}\n`;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (appended) {
|
|
187
|
-
await fs.mkdir(path.dirname(SYSTEM_ENV_FILE), { recursive: true });
|
|
188
|
-
await fs.appendFile(SYSTEM_ENV_FILE, appended);
|
|
189
|
-
await fs.chmod(SYSTEM_ENV_FILE, 0o600);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// 4. Write service/plist (or register the Windows service)
|
|
118
|
+
// Windows: setup.exe-based. Its uninstaller (Add/Remove Programs) runs
|
|
119
|
+
// uninstall-service + uninstall-ca itself; the npx-era layout is identical
|
|
120
|
+
// (same bin dir + service name), so touching it here would corrupt a
|
|
121
|
+
// setup.exe install. No-op + hint.
|
|
194
122
|
if (process.platform === "win32") {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
await _installLinux(dryRun, noActivate);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
console.error("[sentinel] Installation complete.");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function _installWindows(dryRun, noActivate) {
|
|
206
|
-
if (dryRun) {
|
|
207
|
-
console.error(`[sentinel] DRY-RUN: would run \`${SENTINEL_BIN} install-ca\` + install-service`);
|
|
123
|
+
console.error(
|
|
124
|
+
"[sentinel] Windows の coworker-sentinel は setup.exe で管理されます。\n" +
|
|
125
|
+
"[sentinel] アンインストールは「設定 > アプリ」(Add/Remove Programs) から行ってください。",
|
|
126
|
+
);
|
|
208
127
|
return;
|
|
209
128
|
}
|
|
210
|
-
// The service (LocalSystem) reads AI_SCANNER_ACCESS_KEY etc. from
|
|
211
|
-
// %ProgramData%\coworker\aidr.env, written by installSentinel step 3.
|
|
212
|
-
// install-ca adds the DLP root to the OS trust store; install-service
|
|
213
|
-
// registers the auto-start service and starts it immediately.
|
|
214
|
-
await execFileP(SENTINEL_BIN, ["install-ca"]);
|
|
215
|
-
console.error("[sentinel] Root CA installed into the Windows trust store");
|
|
216
|
-
if (!noActivate && process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE !== "1") {
|
|
217
|
-
await execFileP(SENTINEL_BIN, ["install-service"]);
|
|
218
|
-
console.error(`[sentinel] Service registered and started (${SENTINEL_WIN_SERVICE})`);
|
|
219
|
-
} else {
|
|
220
|
-
console.error("[sentinel] --no-activate: skipping service registration");
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
129
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
console.error("[sentinel] DRY-RUN: would run uninstall-service + uninstall-ca");
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
// Order mirrors the CLI dispatch: stop/remove the service (so the exe isn't
|
|
230
|
-
// in use), then drop the MITM root CA so no dangling trust anchor remains.
|
|
231
|
-
try {
|
|
232
|
-
await execFileP(SENTINEL_BIN, ["uninstall-service"]);
|
|
233
|
-
} catch (e) {
|
|
234
|
-
console.warn("[sentinel] uninstall-service:", e.message);
|
|
235
|
-
}
|
|
236
|
-
try {
|
|
237
|
-
await execFileP(SENTINEL_BIN, ["uninstall-ca"]);
|
|
238
|
-
} catch (e) {
|
|
239
|
-
console.warn("[sentinel] uninstall-ca:", e.message);
|
|
130
|
+
if (!isRoot()) {
|
|
131
|
+
throw new Error("uninstallSentinel requires root privileges.");
|
|
240
132
|
}
|
|
241
|
-
}
|
|
242
133
|
|
|
243
|
-
|
|
244
|
-
const plistContent = buildLaunchdPlist(SYSTEM_ENV_FILE);
|
|
245
|
-
if (!dryRun) {
|
|
246
|
-
await fs.mkdir(path.dirname(SENTINEL_PLIST), { recursive: true });
|
|
247
|
-
await fs.writeFile(SENTINEL_PLIST, plistContent, { mode: 0o644 });
|
|
248
|
-
console.error(`[sentinel] Written: ${SENTINEL_PLIST}`);
|
|
134
|
+
console.error("[sentinel] Removing legacy npx-installed coworker-sentinel…");
|
|
249
135
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
await execFileP("launchctl", ["bootstrap", "system", SENTINEL_PLIST]);
|
|
257
|
-
console.error("[sentinel] Daemon loaded via launchctl");
|
|
258
|
-
} catch (e) {
|
|
259
|
-
console.warn("[sentinel] launchctl bootstrap failed — daemon may need manual start:", e.message);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
console.error(`[sentinel] DRY-RUN: would write ${SENTINEL_PLIST}`);
|
|
136
|
+
const debInstalled = await exists(DEB_SENTINEL_SERVICE);
|
|
137
|
+
if (debInstalled) {
|
|
138
|
+
console.error(
|
|
139
|
+
`[sentinel] deb install detected (${DEB_SENTINEL_SERVICE}) — removing only the stale legacy unit/binary; deb-owned files are untouched.`,
|
|
140
|
+
);
|
|
264
141
|
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function _installLinux(dryRun, noActivate) {
|
|
268
|
-
const serviceContent = buildSystemdService(SYSTEM_ENV_FILE);
|
|
269
|
-
const serviceDir = path.dirname(SENTINEL_SERVICE);
|
|
270
|
-
if (!dryRun) {
|
|
271
|
-
await fs.mkdir(serviceDir, { recursive: true });
|
|
272
|
-
await fs.writeFile(SENTINEL_SERVICE, serviceContent, { mode: 0o644 });
|
|
273
|
-
console.error(`[sentinel] Written: ${SENTINEL_SERVICE}`);
|
|
274
142
|
|
|
275
|
-
|
|
143
|
+
if (await exists(SENTINEL_SERVICE)) {
|
|
144
|
+
if (!dryRun) {
|
|
145
|
+
// Only stop/disable when no deb unit exists: `disable --now` acts on
|
|
146
|
+
// the service name and would stop a running deb daemon.
|
|
147
|
+
if (!debInstalled) {
|
|
148
|
+
spawnSync("systemctl", ["disable", "--now", "coworker-sentinel"], { stdio: "ignore" });
|
|
149
|
+
}
|
|
150
|
+
await fs.unlink(SENTINEL_SERVICE);
|
|
276
151
|
spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
|
|
277
|
-
|
|
278
|
-
|
|
152
|
+
console.error(`[sentinel] Removed: ${SENTINEL_SERVICE}`);
|
|
153
|
+
} else {
|
|
154
|
+
console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_SERVICE}`);
|
|
279
155
|
}
|
|
280
|
-
} else {
|
|
281
|
-
console.error(`[sentinel] DRY-RUN: would write ${SENTINEL_SERVICE}`);
|
|
282
156
|
}
|
|
283
|
-
}
|
|
284
157
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
console.error(`[sentinel] Removed: ${SENTINEL_PLIST}`);
|
|
304
|
-
} else {
|
|
305
|
-
console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_PLIST}`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
if (await exists(SENTINEL_SERVICE)) {
|
|
310
|
-
if (!dryRun) {
|
|
311
|
-
spawnSync("systemctl", ["disable", "--now", "coworker-sentinel"], { stdio: "ignore" });
|
|
312
|
-
await fs.unlink(SENTINEL_SERVICE);
|
|
313
|
-
spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
|
|
314
|
-
console.error(`[sentinel] Removed: ${SENTINEL_SERVICE}`);
|
|
315
|
-
} else {
|
|
316
|
-
console.error(`[sentinel] DRY-RUN: would remove ${SENTINEL_SERVICE}`);
|
|
158
|
+
// The legacy daemon auto-installed the DLP root CA into the OS trust store
|
|
159
|
+
// at startup, so uninstall must drop that trust anchor too — otherwise a
|
|
160
|
+
// MITM-capable root stays trusted after the daemon is gone. Must run
|
|
161
|
+
// BEFORE the binary is deleted (the binary is the uninstaller), and
|
|
162
|
+
// best-effort: a never-elevated install has no CA, and a pre-DLP binary
|
|
163
|
+
// doesn't know the subcommand and would start the daemon instead — the
|
|
164
|
+
// timeout kills that instead of hanging the uninstall. Skipped when a deb
|
|
165
|
+
// install is present: the deb daemon relies on that same trust anchor.
|
|
166
|
+
if (!debInstalled && (await exists(SENTINEL_BIN))) {
|
|
167
|
+
if (!dryRun) {
|
|
168
|
+
try {
|
|
169
|
+
await execFileP(SENTINEL_BIN, ["uninstall-ca"], {
|
|
170
|
+
timeout: 15_000,
|
|
171
|
+
killSignal: "SIGKILL",
|
|
172
|
+
});
|
|
173
|
+
console.error("[sentinel] Root CA removed from the OS trust store");
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.warn("[sentinel] uninstall-ca:", e.message);
|
|
317
176
|
}
|
|
177
|
+
} else {
|
|
178
|
+
console.error(`[sentinel] DRY-RUN: would run \`${SENTINEL_BIN} uninstall-ca\``);
|
|
318
179
|
}
|
|
319
180
|
}
|
|
320
181
|
|
|
182
|
+
// The legacy binary itself is npx-owned — always removed (the deb installs
|
|
183
|
+
// its binary elsewhere).
|
|
321
184
|
if (await exists(SENTINEL_BIN)) {
|
|
322
185
|
if (!dryRun) {
|
|
323
186
|
await fs.unlink(SENTINEL_BIN);
|
|
@@ -327,23 +190,37 @@ export async function uninstallSentinel({ dryRun = false } = {}) {
|
|
|
327
190
|
}
|
|
328
191
|
}
|
|
329
192
|
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
193
|
+
// Shared artifacts the legacy daemon owned: its settings file
|
|
194
|
+
// (sentinel-settings.json — endpoint-info collection toggle), the
|
|
195
|
+
// ai-scanner binary in SYSTEM_BIN_DIR that it either self-downloaded or
|
|
196
|
+
// that the old `aidr install --with-sentinel` placed for it, and the shared
|
|
197
|
+
// env file /etc/aidr/aidr.env (root-owned 0600 access key). With no daemon
|
|
198
|
+
// left, nothing consumes them — leaving the key behind is worse hygiene
|
|
199
|
+
// than re-entering it on reinstall. With a deb install present they are
|
|
200
|
+
// still live (the deb daemon reads them) and must be preserved; dpkg purge
|
|
201
|
+
// removes them when the deb goes. Best-effort removal.
|
|
202
|
+
if (!debInstalled) {
|
|
203
|
+
for (const f of [SENTINEL_SETTINGS_FILE, SYSTEM_BIN, SYSTEM_ENV_FILE]) {
|
|
204
|
+
if (await exists(f)) {
|
|
205
|
+
if (!dryRun) {
|
|
206
|
+
try {
|
|
207
|
+
await fs.unlink(f);
|
|
208
|
+
console.error(`[sentinel] Removed: ${f}`);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
console.warn(`[sentinel] could not remove ${f}: ${e.message}`);
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
console.error(`[sentinel] DRY-RUN: would remove ${f}`);
|
|
214
|
+
}
|
|
339
215
|
}
|
|
340
216
|
}
|
|
341
|
-
}
|
|
342
217
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
218
|
+
// Drop the sentinel log file. removeSystemLog also rmdirs SYSTEM_LOG_DIR
|
|
219
|
+
// and deletes /etc/logrotate.d/coworker if scheduled.log is also gone
|
|
220
|
+
// (i.e. the other legacy daemon is uninstalled). A deb daemon may still
|
|
221
|
+
// be appending to the log, so this too is deb-gated.
|
|
222
|
+
await removeSystemLog(SENTINEL_LOG_FILE, { dryRun });
|
|
223
|
+
}
|
|
347
224
|
|
|
348
|
-
console.error("[sentinel]
|
|
225
|
+
console.error("[sentinel] Legacy cleanup complete.");
|
|
349
226
|
}
|
package/src/templates.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export function getStartScannerSh(downloadBase = "https://pytd2jd5p7.execute-api
|
|
|
18
18
|
return `#!/bin/bash
|
|
19
19
|
# SessionStart hook: Setup ai-scanner for the current platform
|
|
20
20
|
# - Linux (x86_64 / aarch64): download native binary from S3
|
|
21
|
-
# - macOS (arm64
|
|
21
|
+
# - macOS (arm64 + x86_64): download the universal native binary from S3 (ad-hoc signed)
|
|
22
22
|
# - Binary is unsigned by 3rd party dev cert — quarantine xattr is stripped after download
|
|
23
23
|
# - Fail-open: never block Claude Code startup
|
|
24
24
|
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
@@ -84,11 +84,12 @@ case "$OS" in
|
|
|
84
84
|
;;
|
|
85
85
|
Darwin)
|
|
86
86
|
case "$ARCH" in
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
# Both arches map to the SAME slug on purpose: the object under
|
|
88
|
+
# darwin-aarch64 is a universal (fat) Mach-O with an arm64 and an
|
|
89
|
+
# x86_64 slice, so one download serves Apple Silicon and Intel. The
|
|
90
|
+
# slug is a wire contract with the verify Lambda's SCANNER_PLATFORMS
|
|
91
|
+
# — only its contents became universal, never its name.
|
|
92
|
+
arm64|x86_64) PLATFORM_SLUG="darwin-aarch64" ;;
|
|
92
93
|
*)
|
|
93
94
|
echo "Warning: Unsupported macOS architecture: $ARCH" >&2
|
|
94
95
|
exit 0
|
package/src/verify.mjs
CHANGED
|
@@ -38,14 +38,13 @@ export async function verifyKey(accessKey, env = "prod") {
|
|
|
38
38
|
if (body.status !== "active") {
|
|
39
39
|
throw new Error(`access key status is '${body.status || "unknown"}' (expected 'active')`);
|
|
40
40
|
}
|
|
41
|
+
// The server may also return sentinel-related fields (edr_enabled,
|
|
42
|
+
// sentinel_download_urls, …) for the OS-native installers; aidr is
|
|
43
|
+
// hooks-only and ignores them (tolerant parsing — unknown fields are fine).
|
|
41
44
|
return {
|
|
42
45
|
status: body.status,
|
|
43
46
|
plan: body.plan || "standard",
|
|
44
47
|
expires_at: body.expires_at || null,
|
|
45
|
-
edr_enabled: body.edr_enabled || false,
|
|
46
|
-
latest_sentinel_version: body.latest_sentinel_version || null,
|
|
47
|
-
sentinel_download_urls: body.sentinel_download_urls || {},
|
|
48
|
-
sentinel_sha256: body.sentinel_sha256 || {},
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
50
|
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, mkdirSync, readFileSync, chmodSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
|
|
4
|
-
// Extension ID will be determined after Chrome Web Store publication.
|
|
5
|
-
// For now use a placeholder that operators can override via AIDR_EXTENSION_ID env var.
|
|
6
|
-
const DEFAULT_EXTENSION_ID = "AIDR_EXTENSION_ID_PLACEHOLDER";
|
|
7
|
-
|
|
8
|
-
export async function installBrowserExtension(homeDir, binaryPath) {
|
|
9
|
-
const extensionId = process.env.AIDR_EXTENSION_ID || DEFAULT_EXTENSION_ID;
|
|
10
|
-
const platform = process.platform;
|
|
11
|
-
|
|
12
|
-
// 1. Write Native Messaging Host manifests for Chrome/Edge/Brave/Chromium
|
|
13
|
-
|
|
14
|
-
// Generate a wrapper script so the binary is invoked in native-messaging-host mode
|
|
15
|
-
const binDir = join(homeDir, ".claude", "bin");
|
|
16
|
-
mkdirSync(binDir, { recursive: true });
|
|
17
|
-
const wrapperPath = join(binDir, "ai-scanner-nativehost.sh");
|
|
18
|
-
const wrapperContent = `#!/bin/sh\nexec "${binaryPath}" native-messaging-host "$@"\n`;
|
|
19
|
-
try {
|
|
20
|
-
writeFileSync(wrapperPath, wrapperContent, { mode: 0o755 });
|
|
21
|
-
} catch (_e) {
|
|
22
|
-
// fall back to binary path directly
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const nativeManifest = {
|
|
26
|
-
name: "jp.coworker.aidr.browser",
|
|
27
|
-
description: "AIDR Shadow IT Monitor bridge",
|
|
28
|
-
path: wrapperPath,
|
|
29
|
-
type: "stdio",
|
|
30
|
-
allowed_origins: [`chrome-extension://${extensionId}/`],
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const chromePaths = getNativeMessagingPaths(platform, homeDir);
|
|
34
|
-
for (const dir of chromePaths) {
|
|
35
|
-
try {
|
|
36
|
-
mkdirSync(dir, { recursive: true });
|
|
37
|
-
writeFileSync(
|
|
38
|
-
join(dir, "jp.coworker.aidr.browser.json"),
|
|
39
|
-
JSON.stringify(nativeManifest, null, 2)
|
|
40
|
-
);
|
|
41
|
-
} catch (_e) {
|
|
42
|
-
// fail-open: skip paths we can't write
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// 2. Record in ~/.claude/ai-scanner-config.json
|
|
47
|
-
const configPath = join(homeDir, ".claude", "ai-scanner-config.json");
|
|
48
|
-
let config = {};
|
|
49
|
-
try {
|
|
50
|
-
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
51
|
-
} catch (_e) {
|
|
52
|
-
// file doesn't exist yet or is unreadable; start fresh
|
|
53
|
-
}
|
|
54
|
-
config.browser_extension = true;
|
|
55
|
-
config.extension_id = extensionId;
|
|
56
|
-
try {
|
|
57
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
58
|
-
} catch (_e) {
|
|
59
|
-
// fail-open: config write is best-effort
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log("✓ Browser extension native messaging host installed");
|
|
63
|
-
console.log(` Extension ID: ${extensionId}`);
|
|
64
|
-
console.log(" Open Chrome and install the AIDR Shadow IT Monitor extension to complete setup.");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getNativeMessagingPaths(platform, homeDir) {
|
|
68
|
-
if (platform === "darwin") {
|
|
69
|
-
return [
|
|
70
|
-
join(homeDir, "Library/Application Support/Google/Chrome/NativeMessagingHosts"),
|
|
71
|
-
join(homeDir, "Library/Application Support/Microsoft Edge/NativeMessagingHosts"),
|
|
72
|
-
join(homeDir, "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts"),
|
|
73
|
-
join(homeDir, "Library/Application Support/Chromium/NativeMessagingHosts"),
|
|
74
|
-
];
|
|
75
|
-
} else {
|
|
76
|
-
return [
|
|
77
|
-
join(homeDir, ".config/google-chrome/NativeMessagingHosts"),
|
|
78
|
-
join(homeDir, ".config/microsoft-edge/NativeMessagingHosts"),
|
|
79
|
-
join(homeDir, ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts"),
|
|
80
|
-
join(homeDir, ".config/chromium/NativeMessagingHosts"),
|
|
81
|
-
];
|
|
82
|
-
}
|
|
83
|
-
}
|