@coworker-jp/aidr 0.1.9 → 0.1.11
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/package.json +1 -1
- package/src/binary-fetcher.mjs +27 -4
- package/src/cli.mjs +13 -0
- package/src/scheduled.mjs +19 -3
- package/src/sentinel.mjs +85 -8
package/package.json
CHANGED
package/src/binary-fetcher.mjs
CHANGED
|
@@ -14,6 +14,7 @@ export function detectPlatform() {
|
|
|
14
14
|
if (os === "linux" && arch === "x64") return "linux-x86_64";
|
|
15
15
|
if (os === "linux" && arch === "arm64") return "linux-aarch64";
|
|
16
16
|
if (os === "darwin" && arch === "arm64") return "darwin-aarch64";
|
|
17
|
+
if (os === "win32" && arch === "x64") return "windows-x86_64";
|
|
17
18
|
if (os === "darwin" && arch === "x64") {
|
|
18
19
|
throw new Error("Intel Mac (darwin-x86_64) is not supported. Apple Silicon only.");
|
|
19
20
|
}
|
|
@@ -162,28 +163,50 @@ export async function fetchOpengrep(destPath) {
|
|
|
162
163
|
* otherwise falls back to the licensed download endpoint.
|
|
163
164
|
* Returns { path, platform, sha256, verified }.
|
|
164
165
|
*/
|
|
165
|
-
export async function fetchSentinel(
|
|
166
|
+
export async function fetchSentinel(
|
|
167
|
+
destPath,
|
|
168
|
+
{ accessKey, presignedUrl, dllPresignedUrl, env = "prod" } = {},
|
|
169
|
+
) {
|
|
166
170
|
const platform = detectPlatform();
|
|
167
171
|
|
|
168
172
|
let binUrl;
|
|
173
|
+
let binAuthKey = accessKey;
|
|
169
174
|
if (presignedUrl) {
|
|
170
175
|
// Use presigned URL from verify response (no auth header needed)
|
|
171
176
|
binUrl = presignedUrl;
|
|
172
|
-
|
|
177
|
+
binAuthKey = undefined;
|
|
173
178
|
} else {
|
|
174
179
|
const base = downloadBase(env);
|
|
175
180
|
binUrl = `${base}/sentinel/${platform}`;
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
await fsp.mkdir(path.dirname(destPath), { recursive: true });
|
|
179
|
-
const buf = await fetchBuffer(binUrl,
|
|
184
|
+
const buf = await fetchBuffer(binUrl, binAuthKey);
|
|
180
185
|
const sha256 = crypto.createHash("sha256").update(buf).digest("hex");
|
|
181
186
|
|
|
182
187
|
const tmp = `${destPath}.tmp-${process.pid}`;
|
|
183
188
|
await fsp.writeFile(tmp, buf);
|
|
184
|
-
await fsp.chmod(tmp, 0o755);
|
|
189
|
+
await fsp.chmod(tmp, 0o755); // no-op on Windows; harmless
|
|
185
190
|
await fsp.rename(tmp, destPath);
|
|
186
191
|
await clearQuarantine(destPath);
|
|
187
192
|
|
|
193
|
+
// Windows also needs the sibling WinDivert.dll — a PE import the loader
|
|
194
|
+
// resolves before the exe's own code runs, so it can't be embedded (unlike
|
|
195
|
+
// WinDivert64.sys, which is embedded and self-extracts at first run).
|
|
196
|
+
if (platform === "windows-x86_64") {
|
|
197
|
+
const dllPath = path.join(path.dirname(destPath), "WinDivert.dll");
|
|
198
|
+
let dllUrl = dllPresignedUrl;
|
|
199
|
+
let dllAuthKey;
|
|
200
|
+
if (dllUrl) {
|
|
201
|
+
dllAuthKey = undefined;
|
|
202
|
+
} else {
|
|
203
|
+
const base = downloadBase(env);
|
|
204
|
+
dllUrl = `${base}/sentinel/${platform}/WinDivert.dll`;
|
|
205
|
+
dllAuthKey = accessKey;
|
|
206
|
+
}
|
|
207
|
+
const dllBuf = await fetchBuffer(dllUrl, dllAuthKey);
|
|
208
|
+
await fsp.writeFile(dllPath, dllBuf);
|
|
209
|
+
}
|
|
210
|
+
|
|
188
211
|
return { path: destPath, platform, sha256, verified: false };
|
|
189
212
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -154,6 +154,15 @@ async function cmdInstall(opts) {
|
|
|
154
154
|
opts.scheduled = true;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
// On Windows there is no ai-scanner binary and no cron/systemd/launchd, so
|
|
158
|
+
// the scheduled endpoint-info scan cannot run — the standalone install is
|
|
159
|
+
// coworker-sentinel-only there (`--with-sentinel` installs the daemon below).
|
|
160
|
+
// Force scheduled off so it doesn't abort the install before sentinel.
|
|
161
|
+
if (process.platform === "win32" && opts.scheduled) {
|
|
162
|
+
console.error("Note: scheduled scanning is not available on Windows; installing coworker-sentinel only.");
|
|
163
|
+
opts.scheduled = false;
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
// --scheduled installs a system-wide cron / systemd / launchd entry that
|
|
158
167
|
// runs `ai-scanner scan endpoint-info` periodically as root, against an
|
|
159
168
|
// agent-independent binary at /opt/coworker/aidr/bin/ai-scanner. Auto-add
|
|
@@ -327,11 +336,15 @@ async function cmdInstall(opts) {
|
|
|
327
336
|
try {
|
|
328
337
|
const platform = (await import("./binary-fetcher.mjs")).detectPlatform();
|
|
329
338
|
const presignedUrl = verifyInfo?.sentinel_download_urls?.[platform] || undefined;
|
|
339
|
+
// Windows additionally needs the sibling WinDivert.dll presigned URL.
|
|
340
|
+
const dllPresignedUrl =
|
|
341
|
+
verifyInfo?.sentinel_download_urls?.[`${platform}-windivert-dll`] || undefined;
|
|
330
342
|
await installSentinel({
|
|
331
343
|
accessKey: opts.key,
|
|
332
344
|
dryRun: opts.dryRun,
|
|
333
345
|
skipBinary: opts.skipBinary,
|
|
334
346
|
presignedUrl,
|
|
347
|
+
dllPresignedUrl,
|
|
335
348
|
env: opts.env || DEFAULT_ENV,
|
|
336
349
|
noActivate: process.env.AI_SCANNER_NO_SCHEDULED_ACTIVATE === "1",
|
|
337
350
|
});
|
package/src/scheduled.mjs
CHANGED
|
@@ -15,11 +15,22 @@ const execFileP = promisify(execFile);
|
|
|
15
15
|
// without ever touching real /etc or /opt. Production should never set this.
|
|
16
16
|
const ROOT_PREFIX = process.env.AIDR_SCHEDULED_ROOT || "";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Windows uses %ProgramFiles% / %ProgramData% instead of the FHS paths. Only
|
|
19
|
+
// the constants coworker-sentinel needs are Windows-aware; the POSIX values
|
|
20
|
+
// (and the launchd/cron/logrotate constants below) are unchanged off-Windows.
|
|
21
|
+
const IS_WIN = process.platform === "win32";
|
|
22
|
+
const WIN_PROGRAMDATA = process.env.ProgramData || "C:\\ProgramData";
|
|
23
|
+
const WIN_PROGRAMFILES = process.env.ProgramFiles || "C:\\Program Files";
|
|
24
|
+
|
|
25
|
+
export const SYSTEM_BIN_DIR = IS_WIN
|
|
26
|
+
? path.join(WIN_PROGRAMFILES, "coworker", "aidr", "bin")
|
|
27
|
+
: ROOT_PREFIX + "/opt/coworker/aidr/bin";
|
|
19
28
|
export const SYSTEM_BIN = path.join(SYSTEM_BIN_DIR, "ai-scanner");
|
|
20
29
|
export const SYSTEM_OPENGREP = path.join(SYSTEM_BIN_DIR, "opengrep");
|
|
21
30
|
export const SYSTEM_USR_LOCAL_BIN = ROOT_PREFIX + "/usr/local/bin/ai-scanner";
|
|
22
|
-
const SYSTEM_ENV_DIR =
|
|
31
|
+
const SYSTEM_ENV_DIR = IS_WIN
|
|
32
|
+
? path.join(WIN_PROGRAMDATA, "coworker")
|
|
33
|
+
: ROOT_PREFIX + "/etc/aidr";
|
|
23
34
|
export const SYSTEM_ENV_FILE = path.join(SYSTEM_ENV_DIR, "aidr.env");
|
|
24
35
|
|
|
25
36
|
// macOS launchd (LaunchDaemons run as root, available pre-login)
|
|
@@ -35,7 +46,9 @@ export const CRON_FILE = ROOT_PREFIX + "/etc/cron.d/aidr-scheduled";
|
|
|
35
46
|
// Mode 0755 root:root, files mode 0644. launchd's `StandardOutPath` (macOS)
|
|
36
47
|
// redirects daemon stderr/stdout here; for Linux cron, the entry uses a
|
|
37
48
|
// literal `>> ... 2>&1` redirect.
|
|
38
|
-
export const SYSTEM_LOG_DIR =
|
|
49
|
+
export const SYSTEM_LOG_DIR = IS_WIN
|
|
50
|
+
? path.join(WIN_PROGRAMDATA, "coworker", "logs")
|
|
51
|
+
: ROOT_PREFIX + "/var/log/coworker";
|
|
39
52
|
export const SCHEDULED_LOG_FILE = path.join(SYSTEM_LOG_DIR, "scheduled.log");
|
|
40
53
|
export const SENTINEL_LOG_FILE = path.join(SYSTEM_LOG_DIR, "sentinel.log");
|
|
41
54
|
export const LOGROTATE_FILE = ROOT_PREFIX + "/etc/logrotate.d/coworker";
|
|
@@ -75,6 +88,9 @@ export async function ensureSystemLogDir(logFile, { dryRun = false } = {}) {
|
|
|
75
88
|
* without logrotate should configure their own rotation.
|
|
76
89
|
*/
|
|
77
90
|
export async function writeLogrotateConfig({ dryRun = false } = {}) {
|
|
91
|
+
// logrotate is a Unix tool; Windows has no equivalent here (the service can
|
|
92
|
+
// size-rotate its own audit log). No-op on Windows.
|
|
93
|
+
if (process.platform === "win32") return;
|
|
78
94
|
const body = `# Generated by aidr install. Covers both aidr-scheduled (weekly) and
|
|
79
95
|
# coworker-sentinel (daily, more chatty under load).
|
|
80
96
|
/var/log/coworker/scheduled.log {
|
package/src/sentinel.mjs
CHANGED
|
@@ -29,12 +29,24 @@ const execFileP = promisify(execFile);
|
|
|
29
29
|
|
|
30
30
|
const ROOT_PREFIX = process.env.AIDR_SCHEDULED_ROOT || "";
|
|
31
31
|
|
|
32
|
-
export const SENTINEL_BIN = path.join(
|
|
32
|
+
export const SENTINEL_BIN = path.join(
|
|
33
|
+
SYSTEM_BIN_DIR,
|
|
34
|
+
process.platform === "win32" ? "coworker-sentinel.exe" : "coworker-sentinel",
|
|
35
|
+
);
|
|
33
36
|
export const SENTINEL_PLIST = ROOT_PREFIX + "/Library/LaunchDaemons/jp.coworker.sentinel.plist";
|
|
34
37
|
export const SENTINEL_SERVICE = ROOT_PREFIX + "/etc/systemd/system/coworker-sentinel.service";
|
|
38
|
+
export const SENTINEL_WIN_SERVICE = "CoworkerSentinel";
|
|
35
39
|
|
|
36
40
|
function isRoot() {
|
|
37
41
|
if (process.env.AI_SCANNER_FAKE_ROOT === "1") return true;
|
|
42
|
+
if (process.platform === "win32") {
|
|
43
|
+
// No uid on Windows; `net session` succeeds only from an elevated shell.
|
|
44
|
+
try {
|
|
45
|
+
return spawnSync("net", ["session"], { stdio: "ignore" }).status === 0;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
38
50
|
return Boolean(process.getuid && process.getuid() === 0);
|
|
39
51
|
}
|
|
40
52
|
|
|
@@ -43,6 +55,13 @@ async function exists(p) {
|
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
export async function isSentinelInstalled() {
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
try {
|
|
60
|
+
return spawnSync("sc", ["query", SENTINEL_WIN_SERVICE], { stdio: "ignore" }).status === 0;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
46
65
|
return (
|
|
47
66
|
(await exists(SENTINEL_PLIST)) ||
|
|
48
67
|
(await exists(SENTINEL_SERVICE))
|
|
@@ -111,12 +130,15 @@ WantedBy=multi-user.target
|
|
|
111
130
|
* env — "dev" | "prod"
|
|
112
131
|
*/
|
|
113
132
|
export async function installSentinel(opts = {}) {
|
|
114
|
-
const { accessKey, dryRun = false, skipBinary = false, noActivate = false, presignedUrl, env = "prod" } = opts;
|
|
133
|
+
const { accessKey, dryRun = false, skipBinary = false, noActivate = false, presignedUrl, dllPresignedUrl, env = "prod" } = opts;
|
|
115
134
|
|
|
116
135
|
if (!isRoot()) {
|
|
117
136
|
throw new Error(
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
process.platform === "win32"
|
|
138
|
+
? "installSentinel requires an elevated (Administrator) shell.\n" +
|
|
139
|
+
"Re-run from an Administrator PowerShell/cmd: npx @coworker-jp/aidr install --key <key> --with-sentinel"
|
|
140
|
+
: "installSentinel requires root privileges.\n" +
|
|
141
|
+
"Re-run with: sudo -E npx @coworker-jp/aidr install --key <key> --with-sentinel",
|
|
120
142
|
);
|
|
121
143
|
}
|
|
122
144
|
|
|
@@ -127,7 +149,7 @@ export async function installSentinel(opts = {}) {
|
|
|
127
149
|
if (!dryRun && !skipBinary) {
|
|
128
150
|
await fs.mkdir(SYSTEM_BIN_DIR, { recursive: true });
|
|
129
151
|
console.error(`[sentinel] Downloading binary → ${SENTINEL_BIN}`);
|
|
130
|
-
await fetchSentinel(SENTINEL_BIN, { accessKey, presignedUrl, env });
|
|
152
|
+
await fetchSentinel(SENTINEL_BIN, { accessKey, presignedUrl, dllPresignedUrl, env });
|
|
131
153
|
} else if (skipBinary) {
|
|
132
154
|
console.error(`[sentinel] Skipping binary download (--skip-binary)`);
|
|
133
155
|
} else {
|
|
@@ -168,8 +190,10 @@ export async function installSentinel(opts = {}) {
|
|
|
168
190
|
}
|
|
169
191
|
}
|
|
170
192
|
|
|
171
|
-
// 4. Write service/plist
|
|
172
|
-
if (process.platform === "
|
|
193
|
+
// 4. Write service/plist (or register the Windows service)
|
|
194
|
+
if (process.platform === "win32") {
|
|
195
|
+
await _installWindows(dryRun, noActivate);
|
|
196
|
+
} else if (process.platform === "darwin") {
|
|
173
197
|
await _installMacos(dryRun, noActivate);
|
|
174
198
|
} else {
|
|
175
199
|
await _installLinux(dryRun, noActivate);
|
|
@@ -178,6 +202,44 @@ export async function installSentinel(opts = {}) {
|
|
|
178
202
|
console.error("[sentinel] Installation complete.");
|
|
179
203
|
}
|
|
180
204
|
|
|
205
|
+
async function _installWindows(dryRun, noActivate) {
|
|
206
|
+
if (dryRun) {
|
|
207
|
+
console.error(`[sentinel] DRY-RUN: would run \`${SENTINEL_BIN} install-ca\` + install-service`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
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
|
+
|
|
224
|
+
async function _uninstallWindows(dryRun) {
|
|
225
|
+
if (dryRun) {
|
|
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);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
181
243
|
async function _installMacos(dryRun, noActivate) {
|
|
182
244
|
const plistContent = buildLaunchdPlist(SYSTEM_ENV_FILE);
|
|
183
245
|
if (!dryRun) {
|
|
@@ -230,7 +292,9 @@ export async function uninstallSentinel({ dryRun = false } = {}) {
|
|
|
230
292
|
|
|
231
293
|
console.error("[sentinel] Uninstalling coworker-sentinel…");
|
|
232
294
|
|
|
233
|
-
if (process.platform === "
|
|
295
|
+
if (process.platform === "win32") {
|
|
296
|
+
await _uninstallWindows(dryRun);
|
|
297
|
+
} else if (process.platform === "darwin") {
|
|
234
298
|
if (await exists(SENTINEL_PLIST)) {
|
|
235
299
|
if (!dryRun) {
|
|
236
300
|
// Modern launchctl API; mirrors scheduled.mjs's uninstallScheduled.
|
|
@@ -263,6 +327,19 @@ export async function uninstallSentinel({ dryRun = false } = {}) {
|
|
|
263
327
|
}
|
|
264
328
|
}
|
|
265
329
|
|
|
330
|
+
// Windows ships a sibling WinDivert.dll next to the exe — remove it too.
|
|
331
|
+
if (process.platform === "win32") {
|
|
332
|
+
const dll = path.join(path.dirname(SENTINEL_BIN), "WinDivert.dll");
|
|
333
|
+
if (await exists(dll)) {
|
|
334
|
+
if (!dryRun) {
|
|
335
|
+
await fs.unlink(dll);
|
|
336
|
+
console.error(`[sentinel] Removed: ${dll}`);
|
|
337
|
+
} else {
|
|
338
|
+
console.error(`[sentinel] DRY-RUN: would remove ${dll}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
266
343
|
// Drop the sentinel log file. removeSystemLog also rmdirs SYSTEM_LOG_DIR
|
|
267
344
|
// and deletes /etc/logrotate.d/coworker if scheduled.log is also gone
|
|
268
345
|
// (i.e. the other daemon is uninstalled).
|