@coworker-jp/aidr 0.0.1
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 +55 -0
- package/bin/aidr.js +2 -0
- package/package.json +29 -0
- package/src/agents/_stub.mjs +18 -0
- package/src/agents/aider.mjs +5 -0
- package/src/agents/amazonq.mjs +5 -0
- package/src/agents/amp.mjs +5 -0
- package/src/agents/antigravity.mjs +5 -0
- package/src/agents/claude.mjs +92 -0
- package/src/agents/cline.mjs +5 -0
- package/src/agents/codex.mjs +225 -0
- package/src/agents/continue.mjs +5 -0
- package/src/agents/copilot.mjs +5 -0
- package/src/agents/crush.mjs +5 -0
- package/src/agents/cursor.mjs +109 -0
- package/src/agents/gemini.mjs +66 -0
- package/src/agents/index.mjs +36 -0
- package/src/agents/jetbrains.mjs +5 -0
- package/src/agents/kiro.mjs +79 -0
- package/src/agents/opencode.mjs +5 -0
- package/src/agents/qwen.mjs +5 -0
- package/src/agents/roo.mjs +5 -0
- package/src/agents/standalone.mjs +43 -0
- package/src/agents/trae.mjs +5 -0
- package/src/agents/windsurf.mjs +105 -0
- package/src/binary-fetcher.mjs +157 -0
- package/src/browser-extension.mjs +83 -0
- package/src/cli.mjs +494 -0
- package/src/detect.mjs +39 -0
- package/src/fs-utils.mjs +247 -0
- package/src/merge.mjs +412 -0
- package/src/scheduled.mjs +219 -0
- package/src/templates.mjs +759 -0
- package/src/toml-merge.mjs +167 -0
- package/src/verify.mjs +51 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
// SOURCE: docker/user-frontend/src/lib/scanner-templates.ts
|
|
2
|
+
// ESM port of the scanner hook templates. Keep in sync with the TS source.
|
|
3
|
+
// Templates are byte-identical to the canonical .{agent}/ files in ai-scanner
|
|
4
|
+
// repo (Claude / Cursor / Windsurf / Kiro) — only the access key value differs.
|
|
5
|
+
|
|
6
|
+
import { buildClaudeSettings, buildGeminiSettings } from "./merge.mjs";
|
|
7
|
+
|
|
8
|
+
export function getSettingsJson(accessKey, scope = "project") {
|
|
9
|
+
return JSON.stringify(buildClaudeSettings(accessKey, scope), null, 2) + "\n";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// start_scanner.sh — parameterized by the verify endpoint (prod / dev).
|
|
13
|
+
// `agentType` is currently unused inside the script (endpoint-info has moved
|
|
14
|
+
// to schedule-only invocation via aidr --scheduled), but the parameter
|
|
15
|
+
// is kept for signature stability with callers and the wider TS template.
|
|
16
|
+
// eslint-disable-next-line no-unused-vars
|
|
17
|
+
export function getStartScannerSh(downloadBase = "https://pytd2jd5p7.execute-api.ap-northeast-1.amazonaws.com/download", agentType = "claude-code") {
|
|
18
|
+
return `#!/bin/bash
|
|
19
|
+
# SessionStart hook: Setup ai-scanner for the current platform
|
|
20
|
+
# - Linux (x86_64 / aarch64): download native binary from S3
|
|
21
|
+
# - macOS (arm64 / Apple Silicon): download native binary from S3 (ad-hoc signed)
|
|
22
|
+
# - Binary is unsigned by 3rd party dev cert — quarantine xattr is stripped after download
|
|
23
|
+
# - Fail-open: never block Claude Code startup
|
|
24
|
+
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
25
|
+
BIN_DIR="$(dirname "$BIN")"
|
|
26
|
+
PULL_STAMP="$HOME/.claude/.ai-scanner-last-pull"
|
|
27
|
+
PULL_INTERVAL=300
|
|
28
|
+
DOWNLOAD_BASE="${downloadBase}"
|
|
29
|
+
|
|
30
|
+
mkdir -p "$BIN_DIR" "$HOME/.claude/quarantine" "$HOME/.claude/ai-scanner"
|
|
31
|
+
|
|
32
|
+
# macOS has no base \`timeout\` (it's in GNU coreutils). Shim it so hooks work
|
|
33
|
+
# on fresh Macs without brew install coreutils.
|
|
34
|
+
if ! command -v timeout >/dev/null 2>&1; then
|
|
35
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
36
|
+
timeout() { gtimeout "$@"; }
|
|
37
|
+
else
|
|
38
|
+
timeout() { shift; "$@"; }
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Generic throttle: true (0) if stamp missing or older than PULL_INTERVAL
|
|
43
|
+
should_check_update() {
|
|
44
|
+
[ ! -f "$PULL_STAMP" ] && return 0
|
|
45
|
+
local last=$(cat "$PULL_STAMP" 2>/dev/null || echo 0)
|
|
46
|
+
[ $(($(date +%s) - last)) -ge "$PULL_INTERVAL" ]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Download binary from $1 to $BIN, strip quarantine (macOS), chmod +x.
|
|
50
|
+
# Uses curl's native --max-time so no external \`timeout\` dependency.
|
|
51
|
+
# Access key is required by the /download endpoint.
|
|
52
|
+
download_binary() {
|
|
53
|
+
local url="$1"
|
|
54
|
+
local tmp=$(mktemp "\${BIN_DIR}/ai-scanner.XXXXXX")
|
|
55
|
+
local auth_args=()
|
|
56
|
+
if [ -n "\${AI_SCANNER_ACCESS_KEY:-}" ]; then
|
|
57
|
+
auth_args=(-H "x-access-key: \${AI_SCANNER_ACCESS_KEY}")
|
|
58
|
+
fi
|
|
59
|
+
if curl -sfL --max-time 60 "\${auth_args[@]}" -o "$tmp" "$url" 2>/dev/null; then
|
|
60
|
+
chmod +x "$tmp"
|
|
61
|
+
# macOS: strip Gatekeeper quarantine so unsigned binary can execute
|
|
62
|
+
# (no-op on Linux where xattr isn't typically present)
|
|
63
|
+
xattr -d com.apple.quarantine "$tmp" 2>/dev/null || true
|
|
64
|
+
mv -f "$tmp" "$BIN"
|
|
65
|
+
return 0
|
|
66
|
+
fi
|
|
67
|
+
rm -f "$tmp"
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
OS=$(uname -s)
|
|
72
|
+
ARCH=$(uname -m)
|
|
73
|
+
|
|
74
|
+
case "$OS" in
|
|
75
|
+
Linux)
|
|
76
|
+
case "$ARCH" in
|
|
77
|
+
x86_64) PLATFORM_SLUG="linux-x86_64" ;;
|
|
78
|
+
aarch64|arm64) PLATFORM_SLUG="linux-aarch64" ;;
|
|
79
|
+
*)
|
|
80
|
+
echo "Warning: Unsupported Linux architecture: $ARCH (expected x86_64 or aarch64)" >&2
|
|
81
|
+
exit 0
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
;;
|
|
85
|
+
Darwin)
|
|
86
|
+
case "$ARCH" in
|
|
87
|
+
arm64) PLATFORM_SLUG="darwin-aarch64" ;;
|
|
88
|
+
x86_64)
|
|
89
|
+
echo "Warning: ai-scanner does not support Intel Mac (x86_64). Apple Silicon required." >&2
|
|
90
|
+
exit 0
|
|
91
|
+
;;
|
|
92
|
+
*)
|
|
93
|
+
echo "Warning: Unsupported macOS architecture: $ARCH" >&2
|
|
94
|
+
exit 0
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
;;
|
|
98
|
+
*)
|
|
99
|
+
echo "Warning: Unsupported OS for ai-scanner: $OS" >&2
|
|
100
|
+
exit 0
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
|
|
104
|
+
# Binary missing → fetch it
|
|
105
|
+
if [ ! -x "$BIN" ]; then
|
|
106
|
+
if download_binary "\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}"; then
|
|
107
|
+
echo "ai-scanner binary installed (\${PLATFORM_SLUG})" >&2
|
|
108
|
+
else
|
|
109
|
+
echo "Warning: Failed to download ai-scanner binary from \${DOWNLOAD_BASE}/\${PLATFORM_SLUG}" >&2
|
|
110
|
+
fi
|
|
111
|
+
exit 0
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Binary exists — throttled self-update check
|
|
115
|
+
if should_check_update; then
|
|
116
|
+
"$BIN" update 2>/dev/null || true
|
|
117
|
+
date +%s > "$PULL_STAMP"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
exit 0
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const STOP_SCANNER_SH = `#!/bin/bash
|
|
125
|
+
# Stop hook: intentionally does nothing.
|
|
126
|
+
# Container stays running for next session startup speed.
|
|
127
|
+
exit 0
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
// Wraps every ai-scanner invocation in a timeout so a hung binary cannot
|
|
131
|
+
// indefinitely block the editor. Exit 124 from GNU `timeout` means "timed
|
|
132
|
+
// out" — we map that to 0 (fail-open), matching the project-wide policy
|
|
133
|
+
// that hook failures never block user work.
|
|
134
|
+
//
|
|
135
|
+
// Portability: Linux ships `timeout` in coreutils; Homebrew macOS exposes
|
|
136
|
+
// it as `gtimeout`; a bare macOS (no coreutils) has neither, in which case
|
|
137
|
+
// we exec without a timeout. This keeps the editor-level timeout as the
|
|
138
|
+
// only safety net on that platform, which is acceptable — installing
|
|
139
|
+
// coreutils is the documented remedy.
|
|
140
|
+
//
|
|
141
|
+
// Default 30s: the v1.4.x scanner's cold-start path does rules-decrypt +
|
|
142
|
+
// advisory_client DDB round-trip + YARA + Quarantine (typosquat + registry
|
|
143
|
+
// metadata) + Semgrep, measured ~8.5s on admin@coworker.co.jp's Linux box
|
|
144
|
+
// with a warm cache — cold-start + slow network can push that past 12s and
|
|
145
|
+
// exhaust the previous default, causing a known-malicious file to slip
|
|
146
|
+
// through as a false-negative (timeout → 124 → fail-open 0). 30s is
|
|
147
|
+
// generous enough to absorb cold-start variance while still bounding a
|
|
148
|
+
// truly hung scanner. AI_SCANNER_HOOK_TIMEOUT (seconds) lets operators
|
|
149
|
+
// tune further per-environment.
|
|
150
|
+
const SCAN_HELPERS = `run_scanner() {
|
|
151
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
152
|
+
timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT:-30}" "$BIN" "$@"
|
|
153
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
154
|
+
gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT:-30}" "$BIN" "$@"
|
|
155
|
+
else
|
|
156
|
+
"$BIN" "$@"
|
|
157
|
+
fi
|
|
158
|
+
local rc=$?
|
|
159
|
+
[ $rc -eq 124 ] && return 0
|
|
160
|
+
return $rc
|
|
161
|
+
}`;
|
|
162
|
+
|
|
163
|
+
export const SCAN_BASH_SH = `#!/bin/bash
|
|
164
|
+
# PostToolUse hook for Bash: Scan command output.
|
|
165
|
+
# Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
|
|
166
|
+
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
167
|
+
[ ! -x "$BIN" ] && exit 0
|
|
168
|
+
|
|
169
|
+
export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
|
|
170
|
+
|
|
171
|
+
${SCAN_HELPERS}
|
|
172
|
+
|
|
173
|
+
INPUT=$(cat)
|
|
174
|
+
COMMAND=$(jq -r '.tool_input.command // ""' <<<"$INPUT")
|
|
175
|
+
jq -r '.tool_response // ""' <<<"$INPUT" \\
|
|
176
|
+
| run_scanner scan bash-output --command "$COMMAND"
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
export const SCAN_BASH_INPUT_SH = `#!/bin/bash
|
|
180
|
+
# PreToolUse hook for Bash: Scan command before execution.
|
|
181
|
+
# Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
|
|
182
|
+
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
183
|
+
[ ! -x "$BIN" ] && exit 0
|
|
184
|
+
|
|
185
|
+
export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
|
|
186
|
+
|
|
187
|
+
${SCAN_HELPERS}
|
|
188
|
+
|
|
189
|
+
CONTENT=$(jq -r '.tool_input.command // ""')
|
|
190
|
+
[ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
|
|
191
|
+
|
|
192
|
+
printf '%s' "$CONTENT" | run_scanner scan bash-input
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
export const SCAN_EDIT_SH = `#!/bin/bash
|
|
196
|
+
# PreToolUse hook for Edit/MultiEdit: Scan new content before write.
|
|
197
|
+
# Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
|
|
198
|
+
# Only the Edit-vs-MultiEdit content extraction is Claude Code specific.
|
|
199
|
+
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
200
|
+
[ ! -x "$BIN" ] && exit 0
|
|
201
|
+
|
|
202
|
+
export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
|
|
203
|
+
|
|
204
|
+
${SCAN_HELPERS}
|
|
205
|
+
|
|
206
|
+
INPUT=$(cat)
|
|
207
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
208
|
+
TOOL_NAME=$(jq -r '.tool_name // "unknown"' <<<"$INPUT")
|
|
209
|
+
|
|
210
|
+
case "$TOOL_NAME" in
|
|
211
|
+
Edit) CONTENT=$(jq -r '.tool_input.new_string // ""' <<<"$INPUT") ;;
|
|
212
|
+
MultiEdit) CONTENT=$(jq -r '[.tool_input.edits[]?.new_string // ""] | join("\\n")' <<<"$INPUT") ;;
|
|
213
|
+
*) CONTENT=$(jq -r '.tool_input.new_string // .tool_input.content // ""' <<<"$INPUT") ;;
|
|
214
|
+
esac
|
|
215
|
+
|
|
216
|
+
[ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
|
|
217
|
+
|
|
218
|
+
printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
219
|
+
`;
|
|
220
|
+
|
|
221
|
+
export const SCAN_READ_SH = `#!/bin/bash
|
|
222
|
+
# PostToolUse hook for Read: Scan file content Claude just read.
|
|
223
|
+
# Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
|
|
224
|
+
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
225
|
+
[ ! -x "$BIN" ] && exit 0
|
|
226
|
+
|
|
227
|
+
export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
|
|
228
|
+
|
|
229
|
+
${SCAN_HELPERS}
|
|
230
|
+
|
|
231
|
+
INPUT=$(cat)
|
|
232
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
233
|
+
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
234
|
+
|
|
235
|
+
# tool_response is an object: {type, file: {filePath, content, ...}}.
|
|
236
|
+
jq -r '.tool_response.file.content // .tool_response // ""' <<<"$INPUT" \\
|
|
237
|
+
| run_scanner scan read-output --file "$FILE_PATH"
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
export const SCAN_EDIT_OUTPUT_SH = `#!/bin/bash
|
|
241
|
+
# PostToolUse hook for Edit/MultiEdit/Write: Scan final file content.
|
|
242
|
+
# Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
|
|
243
|
+
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
244
|
+
[ ! -x "$BIN" ] && exit 0
|
|
245
|
+
|
|
246
|
+
export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
|
|
247
|
+
|
|
248
|
+
${SCAN_HELPERS}
|
|
249
|
+
|
|
250
|
+
INPUT=$(cat)
|
|
251
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
252
|
+
|
|
253
|
+
# tool_response for Edit/Write: {filePath, content, ...} or nested {file: {content}}.
|
|
254
|
+
jq -r '.tool_response.file.content // .tool_response.content // .tool_response // ""' <<<"$INPUT" \\
|
|
255
|
+
| run_scanner scan edit-output --file "$FILE_PATH"
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
export const SCAN_READ_INPUT_SH = `#!/bin/bash
|
|
259
|
+
# PreToolUse hook for Read: Scan file content before Claude reads it.
|
|
260
|
+
# This is a thin Claude Code adapter — all scanning logic lives in the Rust
|
|
261
|
+
# binary, including actor resolution, exclude-dir filtering, content cap, and
|
|
262
|
+
# detection-log writing.
|
|
263
|
+
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
264
|
+
[ ! -x "$BIN" ] && exit 0
|
|
265
|
+
|
|
266
|
+
export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
|
|
267
|
+
|
|
268
|
+
${SCAN_HELPERS}
|
|
269
|
+
|
|
270
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // ""')
|
|
271
|
+
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
272
|
+
|
|
273
|
+
# Rust reads the file itself (no need to pipe content). Exit code 2 = detection.
|
|
274
|
+
run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
export const SCAN_WEBFETCH_SH = `#!/bin/bash
|
|
278
|
+
# PreToolUse hook for WebFetch: Scan URL before Claude fetches it.
|
|
279
|
+
# Thin Claude Code adapter — Rust handles phishing scoring + YARA + detection log.
|
|
280
|
+
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
281
|
+
[ ! -x "$BIN" ] && exit 0
|
|
282
|
+
|
|
283
|
+
export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
|
|
284
|
+
|
|
285
|
+
${SCAN_HELPERS}
|
|
286
|
+
|
|
287
|
+
URL=$(jq -r '.tool_input.url // ""')
|
|
288
|
+
[ -z "$URL" ] || [ "$URL" = "null" ] && exit 0
|
|
289
|
+
|
|
290
|
+
run_scanner scan webfetch --url "$URL" </dev/null
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
export const SCAN_WEBSEARCH_SH = `#!/bin/bash
|
|
294
|
+
# PostToolUse hook for WebSearch: Scan search results for phishing URLs.
|
|
295
|
+
# Thin Claude Code adapter — Rust handles cap, phishing scoring, detection log.
|
|
296
|
+
BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
|
|
297
|
+
[ ! -x "$BIN" ] && exit 0
|
|
298
|
+
|
|
299
|
+
export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
|
|
300
|
+
|
|
301
|
+
${SCAN_HELPERS}
|
|
302
|
+
|
|
303
|
+
jq -r '.tool_response // ""' | run_scanner scan websearch`;
|
|
304
|
+
|
|
305
|
+
export const LOG_WEB_URLS_SH = `#!/bin/bash
|
|
306
|
+
# Log URLs from WebSearch and WebFetch tool calls
|
|
307
|
+
# Hook input is received via stdin as JSON
|
|
308
|
+
|
|
309
|
+
LOG_DIR="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs"
|
|
310
|
+
LOG_FILE="$LOG_DIR/web_urls.log"
|
|
311
|
+
|
|
312
|
+
mkdir -p "$LOG_DIR"
|
|
313
|
+
|
|
314
|
+
INPUT=$(cat)
|
|
315
|
+
|
|
316
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
|
|
317
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
318
|
+
|
|
319
|
+
if [ "$TOOL_NAME" = "WebSearch" ]; then
|
|
320
|
+
QUERY=$(echo "$INPUT" | jq -r '.tool_input.query // "unknown"')
|
|
321
|
+
echo "\${TIMESTAMP} [WebSearch] query=\\"\${QUERY}\\"" >> "$LOG_FILE"
|
|
322
|
+
elif [ "$TOOL_NAME" = "WebFetch" ]; then
|
|
323
|
+
URL=$(echo "$INPUT" | jq -r '.tool_input.url // "unknown"')
|
|
324
|
+
echo "\${TIMESTAMP} [WebFetch] url=\\"\${URL}\\"" >> "$LOG_FILE"
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
exit 0
|
|
328
|
+
`;
|
|
329
|
+
|
|
330
|
+
// env.sh shared by Cursor/Windsurf/Kiro/Codex (byte-identical to canonical,
|
|
331
|
+
// only the access key value differs).
|
|
332
|
+
//
|
|
333
|
+
// Exposes a `run_scanner` bash function that every hook adapter uses to
|
|
334
|
+
// invoke the scanner binary under a timeout. A hung scanner must never
|
|
335
|
+
// stall the editor's tool call (fail-open is the project policy).
|
|
336
|
+
export function agentEnvSh(accessKey, agentDir) {
|
|
337
|
+
return `#!/bin/bash
|
|
338
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
339
|
+
export AI_SCANNER_BIN="\${AI_SCANNER_BIN:-$HOME/${agentDir}/bin/ai-scanner}"
|
|
340
|
+
export AI_SCANNER_ACCESS_KEY="${accessKey}"
|
|
341
|
+
export AI_SCANNER_LEVEL="\${AI_SCANNER_LEVEL:-high}"
|
|
342
|
+
export AI_SCANNER_EXCLUDE_DIRS="\${AI_SCANNER_EXCLUDE_DIRS:-}"
|
|
343
|
+
export AI_SCANNER_QUARANTINE_DAYS="\${AI_SCANNER_QUARANTINE_DAYS:-3}"
|
|
344
|
+
export AI_SCANNER_QUARANTINE_MODE="\${AI_SCANNER_QUARANTINE_MODE:-scan}"
|
|
345
|
+
export AI_SCANNER_EXFIL_MODE="\${AI_SCANNER_EXFIL_MODE:-on}"
|
|
346
|
+
export AI_SCANNER_WEBFETCH_YARA_THRESHOLD="\${AI_SCANNER_WEBFETCH_YARA_THRESHOLD:-50}"
|
|
347
|
+
export AI_SCANNER_HOOK_TIMEOUT="\${AI_SCANNER_HOOK_TIMEOUT:-30}"
|
|
348
|
+
export AI_SCANNER_DETECTION_LOG="\${AI_SCANNER_DETECTION_LOG:-$SCRIPT_DIR/../logs/detections.log}"
|
|
349
|
+
export CLAUDE_PROJECT_DIR="\${CLAUDE_PROJECT_DIR:-.}"
|
|
350
|
+
|
|
351
|
+
# Wraps "$AI_SCANNER_BIN \$*" in an optional timeout. Exit 124 from GNU
|
|
352
|
+
# \`timeout\` (timed out) is mapped to 0 so a hung scanner never blocks the
|
|
353
|
+
# tool call. Bare macOS (no coreutils) falls through without a timeout —
|
|
354
|
+
# install coreutils (\`brew install coreutils\`) to get the safety net.
|
|
355
|
+
run_scanner() {
|
|
356
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
357
|
+
timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_SCANNER_BIN}" "$@"
|
|
358
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
359
|
+
gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_SCANNER_BIN}" "$@"
|
|
360
|
+
else
|
|
361
|
+
"\${AI_SCANNER_BIN}" "$@"
|
|
362
|
+
fi
|
|
363
|
+
local rc=$?
|
|
364
|
+
[ $rc -eq 124 ] && return 0
|
|
365
|
+
return $rc
|
|
366
|
+
}
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Cursor ────────────────────────────────────────────────────────────
|
|
371
|
+
// hooks.json shape moved to merge.mjs (buildCursorHooksJson) so cursor.mjs can
|
|
372
|
+
// merge with any user-edited existing hooks.json instead of overwriting.
|
|
373
|
+
|
|
374
|
+
export function getCursorSessionInitSh(accessKey) {
|
|
375
|
+
return `#!/bin/bash
|
|
376
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
377
|
+
source "$SCRIPT_DIR/env.sh"
|
|
378
|
+
cat > /dev/null
|
|
379
|
+
bash "$SCRIPT_DIR/start_scanner.sh" 2>/dev/null || true
|
|
380
|
+
cat <<EOF
|
|
381
|
+
{"env":{"AI_SCANNER_BIN":"\${AI_SCANNER_BIN}","AI_SCANNER_ACCESS_KEY":"${accessKey}","AI_SCANNER_LEVEL":"high"}}
|
|
382
|
+
EOF
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export const CURSOR_PRE_SHELL_SH = `#!/bin/bash
|
|
387
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
388
|
+
source "$SCRIPT_DIR/env.sh"
|
|
389
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
390
|
+
export AI_SCANNER_DETECTION_LOG
|
|
391
|
+
jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
392
|
+
`;
|
|
393
|
+
|
|
394
|
+
export const CURSOR_PRE_READ_SH = `#!/bin/bash
|
|
395
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
396
|
+
source "$SCRIPT_DIR/env.sh"
|
|
397
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
398
|
+
export AI_SCANNER_DETECTION_LOG
|
|
399
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // ""')
|
|
400
|
+
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
401
|
+
run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
402
|
+
`;
|
|
403
|
+
|
|
404
|
+
export const CURSOR_PRE_EDIT_SH = `#!/bin/bash
|
|
405
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
406
|
+
source "$SCRIPT_DIR/env.sh"
|
|
407
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
408
|
+
export AI_SCANNER_DETECTION_LOG
|
|
409
|
+
INPUT=$(cat)
|
|
410
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
411
|
+
TOOL_NAME=$(jq -r '.tool_name // "unknown"' <<<"$INPUT")
|
|
412
|
+
case "$TOOL_NAME" in
|
|
413
|
+
Edit) CONTENT=$(jq -r '.tool_input.new_string // ""' <<<"$INPUT") ;;
|
|
414
|
+
MultiEdit) CONTENT=$(jq -r '[.tool_input.edits[]?.new_string // ""] | join("\\n")' <<<"$INPUT") ;;
|
|
415
|
+
*) CONTENT=$(jq -r '.tool_input.new_string // .tool_input.content // ""' <<<"$INPUT") ;;
|
|
416
|
+
esac
|
|
417
|
+
[ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
|
|
418
|
+
printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
419
|
+
`;
|
|
420
|
+
|
|
421
|
+
export const CURSOR_POST_SHELL_SH = `#!/bin/bash
|
|
422
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
423
|
+
source "$SCRIPT_DIR/env.sh"
|
|
424
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
425
|
+
export AI_SCANNER_DETECTION_LOG
|
|
426
|
+
INPUT=$(cat)
|
|
427
|
+
COMMAND=$(jq -r '.tool_output // ""' <<<"$INPUT" | head -1 && jq -r '.tool_input.command // ""' <<<"$INPUT")
|
|
428
|
+
jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan bash-output --command "$(jq -r '.tool_input.command // ""' <<<"$INPUT")"
|
|
429
|
+
`;
|
|
430
|
+
|
|
431
|
+
export const CURSOR_POST_READ_SH = `#!/bin/bash
|
|
432
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
433
|
+
source "$SCRIPT_DIR/env.sh"
|
|
434
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
435
|
+
export AI_SCANNER_DETECTION_LOG
|
|
436
|
+
INPUT=$(cat)
|
|
437
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
438
|
+
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
439
|
+
jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan read-output --file "$FILE_PATH"
|
|
440
|
+
`;
|
|
441
|
+
|
|
442
|
+
export const CURSOR_POST_WRITE_SH = `#!/bin/bash
|
|
443
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
444
|
+
source "$SCRIPT_DIR/env.sh"
|
|
445
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
446
|
+
export AI_SCANNER_DETECTION_LOG
|
|
447
|
+
INPUT=$(cat)
|
|
448
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
|
|
449
|
+
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
450
|
+
[ ! -f "$FILE_PATH" ] && exit 0
|
|
451
|
+
run_scanner scan edit-output --file "$FILE_PATH" < "$FILE_PATH"
|
|
452
|
+
`;
|
|
453
|
+
|
|
454
|
+
// ── Windsurf ─────────────────────────────────────────────────────────
|
|
455
|
+
// hooks.json shape moved to merge.mjs (buildWindsurfHooksJson).
|
|
456
|
+
|
|
457
|
+
export const WINDSURF_ENSURE_SCANNER_SH = `#!/bin/bash
|
|
458
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
459
|
+
source "$SCRIPT_DIR/env.sh"
|
|
460
|
+
BIN="$AI_SCANNER_BIN"
|
|
461
|
+
LOCK_FILE="/tmp/.ai-scanner-ensure.lock"
|
|
462
|
+
if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
|
|
463
|
+
return 0 2>/dev/null || true
|
|
464
|
+
fi
|
|
465
|
+
(
|
|
466
|
+
flock -w 120 200 || exit 0
|
|
467
|
+
if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then exit 0; fi
|
|
468
|
+
bash "$SCRIPT_DIR/start_scanner.sh"
|
|
469
|
+
) 200>"$LOCK_FILE"
|
|
470
|
+
return 0 2>/dev/null || true
|
|
471
|
+
`;
|
|
472
|
+
|
|
473
|
+
export const WINDSURF_PRE_COMMAND_SH = `#!/bin/bash
|
|
474
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
475
|
+
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
476
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
477
|
+
export AI_SCANNER_DETECTION_LOG
|
|
478
|
+
CONTENT=$(jq -r '.tool_info.command_line // ""')
|
|
479
|
+
[ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
|
|
480
|
+
printf '%s' "$CONTENT" | run_scanner scan bash-input
|
|
481
|
+
`;
|
|
482
|
+
|
|
483
|
+
export const WINDSURF_PRE_READ_SH = `#!/bin/bash
|
|
484
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
485
|
+
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
486
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
487
|
+
export AI_SCANNER_DETECTION_LOG
|
|
488
|
+
FILE_PATH=$(jq -r '.tool_info.file_path // ""')
|
|
489
|
+
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
490
|
+
run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
491
|
+
`;
|
|
492
|
+
|
|
493
|
+
export const WINDSURF_PRE_WRITE_SH = `#!/bin/bash
|
|
494
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
495
|
+
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
496
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
497
|
+
export AI_SCANNER_DETECTION_LOG
|
|
498
|
+
INPUT=$(cat)
|
|
499
|
+
FILE_PATH=$(jq -r '.tool_info.file_path // ""' <<<"$INPUT")
|
|
500
|
+
CONTENT=$(jq -r '[.tool_info.edits[]?.new_string // ""] | join("\\n")' <<<"$INPUT")
|
|
501
|
+
[ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
|
|
502
|
+
printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
503
|
+
`;
|
|
504
|
+
|
|
505
|
+
export const WINDSURF_POST_READ_SH = `#!/bin/bash
|
|
506
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
507
|
+
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
508
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
509
|
+
export AI_SCANNER_DETECTION_LOG
|
|
510
|
+
FILE_PATH=$(jq -r '.tool_info.file_path // ""')
|
|
511
|
+
[ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
|
|
512
|
+
run_scanner scan read-output --file "$FILE_PATH" < "$FILE_PATH"
|
|
513
|
+
`;
|
|
514
|
+
|
|
515
|
+
export const WINDSURF_POST_WRITE_SH = `#!/bin/bash
|
|
516
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
517
|
+
source "$SCRIPT_DIR/ensure_scanner.sh"
|
|
518
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
519
|
+
export AI_SCANNER_DETECTION_LOG
|
|
520
|
+
FILE_PATH=$(jq -r '.tool_info.file_path // ""')
|
|
521
|
+
[ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
|
|
522
|
+
run_scanner scan edit-output --file "$FILE_PATH" < "$FILE_PATH"
|
|
523
|
+
`;
|
|
524
|
+
|
|
525
|
+
// ── Kiro ─────────────────────────────────────────────────────────────
|
|
526
|
+
export function getKiroAgentJson(scope = "project", absHome = "") {
|
|
527
|
+
const p = scope === "user" ? `${absHome}/.kiro/coworker-ai` : ".kiro/coworker-ai";
|
|
528
|
+
// timeout_ms is Kiro's native field. Default is 30000ms (30s); explicitly
|
|
529
|
+
// pin to 15000ms so a stalled scanner is killed by the editor in time,
|
|
530
|
+
// and 60000ms for agentSpawn to give the binary fetch room. The
|
|
531
|
+
// shell-side run_scanner cap (12s) fires first under normal conditions.
|
|
532
|
+
return JSON.stringify({
|
|
533
|
+
name: "ai-scanner",
|
|
534
|
+
description: "CoWorker AIDR security scanner",
|
|
535
|
+
tools: ["*"],
|
|
536
|
+
hooks: {
|
|
537
|
+
agentSpawn: [{ command: `${p}/start.sh`, timeout_ms: 60000 }],
|
|
538
|
+
preToolUse: [
|
|
539
|
+
{ matcher: "shell", command: `${p}/pre_shell.sh`, timeout_ms: 15000 },
|
|
540
|
+
{ matcher: "read", command: `${p}/pre_read.sh`, timeout_ms: 15000 },
|
|
541
|
+
{ matcher: "write", command: `${p}/pre_write.sh`, timeout_ms: 15000 },
|
|
542
|
+
],
|
|
543
|
+
postToolUse: [
|
|
544
|
+
{ matcher: "shell", command: `${p}/post_shell.sh`, timeout_ms: 15000 },
|
|
545
|
+
{ matcher: "read", command: `${p}/post_read.sh`, timeout_ms: 15000 },
|
|
546
|
+
{ matcher: "write", command: `${p}/post_write.sh`, timeout_ms: 15000 },
|
|
547
|
+
],
|
|
548
|
+
},
|
|
549
|
+
}, null, 2);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export const KIRO_START_SH = `#!/bin/bash
|
|
553
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
554
|
+
source "$SCRIPT_DIR/env.sh"
|
|
555
|
+
exec "$SCRIPT_DIR/start_scanner.sh"
|
|
556
|
+
`;
|
|
557
|
+
|
|
558
|
+
export const KIRO_PRE_SHELL_SH = `#!/bin/bash
|
|
559
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
560
|
+
source "$SCRIPT_DIR/env.sh"
|
|
561
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
562
|
+
export AI_SCANNER_DETECTION_LOG
|
|
563
|
+
jq -r '.tool_input.command // ""' | run_scanner scan bash-input
|
|
564
|
+
`;
|
|
565
|
+
|
|
566
|
+
export const KIRO_PRE_READ_SH = `#!/bin/bash
|
|
567
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
568
|
+
source "$SCRIPT_DIR/env.sh"
|
|
569
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
570
|
+
export AI_SCANNER_DETECTION_LOG
|
|
571
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""')
|
|
572
|
+
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
573
|
+
run_scanner scan read-input --file "$FILE_PATH" </dev/null
|
|
574
|
+
`;
|
|
575
|
+
|
|
576
|
+
export const KIRO_PRE_WRITE_SH = `#!/bin/bash
|
|
577
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
578
|
+
source "$SCRIPT_DIR/env.sh"
|
|
579
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
580
|
+
export AI_SCANNER_DETECTION_LOG
|
|
581
|
+
INPUT=$(cat)
|
|
582
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
|
|
583
|
+
CONTENT=$(jq -r '.tool_input.new_string // .tool_input.new_str // .tool_input.file_text // .tool_input.content // ""' <<<"$INPUT")
|
|
584
|
+
[ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
|
|
585
|
+
printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
|
|
586
|
+
`;
|
|
587
|
+
|
|
588
|
+
export const KIRO_POST_SHELL_SH = `#!/bin/bash
|
|
589
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
590
|
+
source "$SCRIPT_DIR/env.sh"
|
|
591
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
592
|
+
export AI_SCANNER_DETECTION_LOG
|
|
593
|
+
INPUT=$(cat)
|
|
594
|
+
COMMAND=$(jq -r '.tool_input.command // ""' <<<"$INPUT")
|
|
595
|
+
RESPONSE=$(jq -r '.tool_response | if type=="object" then (.result[0].stdout // tostring) else . end' <<<"$INPUT")
|
|
596
|
+
printf '%s' "$RESPONSE" | run_scanner scan bash-output --command "$COMMAND"
|
|
597
|
+
`;
|
|
598
|
+
|
|
599
|
+
export const KIRO_POST_READ_SH = `#!/bin/bash
|
|
600
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
601
|
+
source "$SCRIPT_DIR/env.sh"
|
|
602
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
603
|
+
export AI_SCANNER_DETECTION_LOG
|
|
604
|
+
INPUT=$(cat)
|
|
605
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""' <<<"$INPUT")
|
|
606
|
+
[ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
|
|
607
|
+
RESPONSE=$(jq -r '.tool_response | if type=="object" then (.result | if type=="array" then join("\\n") else tostring end) else . end' <<<"$INPUT")
|
|
608
|
+
printf '%s' "$RESPONSE" | run_scanner scan read-output --file "$FILE_PATH"
|
|
609
|
+
`;
|
|
610
|
+
|
|
611
|
+
export const KIRO_POST_WRITE_SH = `#!/bin/bash
|
|
612
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
613
|
+
source "$SCRIPT_DIR/env.sh"
|
|
614
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
615
|
+
export AI_SCANNER_DETECTION_LOG
|
|
616
|
+
INPUT=$(cat)
|
|
617
|
+
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
|
|
618
|
+
RESPONSE=$(jq -r '.tool_response | if type=="object" then (.result | if type=="array" then join("\\n") else tostring end) else . end' <<<"$INPUT")
|
|
619
|
+
printf '%s' "$RESPONSE" | run_scanner scan edit-output --file "$FILE_PATH"
|
|
620
|
+
`;
|
|
621
|
+
|
|
622
|
+
// ── Gemini / Codex ────────────────────────────────────────────────────
|
|
623
|
+
// Thin wrappers that shell out to ai-scanner via generic bash-input scan.
|
|
624
|
+
|
|
625
|
+
export function getGeminiSettingsJson(accessKey, binary) {
|
|
626
|
+
return JSON.stringify(buildGeminiSettings(accessKey, binary), null, 2) + "\n";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function getGeminiPreHookSh(binary) {
|
|
630
|
+
return `#!/bin/bash
|
|
631
|
+
# Gemini CLI BeforeTool hook wrapper.
|
|
632
|
+
# Doesn't source env.sh (Gemini binds the binary path at install time), so
|
|
633
|
+
# the run_scanner timeout helper is inlined here. See agentEnvSh for the
|
|
634
|
+
# canonical version and the rationale for fail-open on exit 124.
|
|
635
|
+
BIN="${binary}"
|
|
636
|
+
[ ! -x "$BIN" ] && exit 0
|
|
637
|
+
|
|
638
|
+
run_scanner() {
|
|
639
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
640
|
+
timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT:-30}" "$BIN" "$@"
|
|
641
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
642
|
+
gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT:-30}" "$BIN" "$@"
|
|
643
|
+
else
|
|
644
|
+
"$BIN" "$@"
|
|
645
|
+
fi
|
|
646
|
+
local rc=$?
|
|
647
|
+
[ $rc -eq 124 ] && return 0
|
|
648
|
+
return $rc
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
INPUT=$(cat)
|
|
652
|
+
TOOL=$(jq -r '.tool_name // ""' <<<"$INPUT" 2>/dev/null || echo "")
|
|
653
|
+
case "$TOOL" in
|
|
654
|
+
shell|run_shell_command)
|
|
655
|
+
CMD=$(jq -r '.tool_input.command // .tool_input.cmd // ""' <<<"$INPUT" 2>/dev/null)
|
|
656
|
+
[ -z "$CMD" ] && exit 0
|
|
657
|
+
printf '%s' "$CMD" | run_scanner scan bash-input
|
|
658
|
+
;;
|
|
659
|
+
read_file|read)
|
|
660
|
+
FP=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT" 2>/dev/null)
|
|
661
|
+
[ -z "$FP" ] && exit 0
|
|
662
|
+
run_scanner scan read-input --file "$FP" </dev/null
|
|
663
|
+
;;
|
|
664
|
+
write_file|edit|replace)
|
|
665
|
+
FP=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT" 2>/dev/null)
|
|
666
|
+
CONTENT=$(jq -r '.tool_input.content // .tool_input.new_string // ""' <<<"$INPUT" 2>/dev/null)
|
|
667
|
+
[ -z "$CONTENT" ] && exit 0
|
|
668
|
+
printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FP"
|
|
669
|
+
;;
|
|
670
|
+
*) exit 0 ;;
|
|
671
|
+
esac
|
|
672
|
+
`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ── Codex CLI ────────────────────────────────────────────────────────
|
|
676
|
+
// Codex only emits Bash tool events today (spec: https://developers.openai.com/codex/hooks).
|
|
677
|
+
// We wire three hooks:
|
|
678
|
+
// - SessionStart (startup|resume) → ensures scanner binary is present & healthy
|
|
679
|
+
// - PreToolUse (Bash) → scan command before execution, return block JSON on detection
|
|
680
|
+
// - PostToolUse (Bash) → scan output, return feedback JSON on detection
|
|
681
|
+
// hooks.json shape lives in merge.mjs (buildCodexHooksJson).
|
|
682
|
+
|
|
683
|
+
export const CODEX_SESSION_INIT_SH = `#!/bin/bash
|
|
684
|
+
# Codex SessionStart: guarantee binary is ready before the first tool runs.
|
|
685
|
+
# flock prevents two concurrent sessions from racing on the download.
|
|
686
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
687
|
+
source "$SCRIPT_DIR/env.sh"
|
|
688
|
+
cat > /dev/null # drain stdin so Codex doesn't block on a write
|
|
689
|
+
BIN="$AI_SCANNER_BIN"
|
|
690
|
+
LOCK_FILE="/tmp/.ai-scanner-codex-init.lock"
|
|
691
|
+
if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
|
|
692
|
+
exit 0
|
|
693
|
+
fi
|
|
694
|
+
(
|
|
695
|
+
flock -w 120 200 || exit 0
|
|
696
|
+
if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then exit 0; fi
|
|
697
|
+
bash "$SCRIPT_DIR/start_scanner.sh" 2>/dev/null || true
|
|
698
|
+
) 200>"$LOCK_FILE"
|
|
699
|
+
exit 0
|
|
700
|
+
`;
|
|
701
|
+
|
|
702
|
+
export const CODEX_PRE_SHELL_SH = `#!/bin/bash
|
|
703
|
+
# Codex PreToolUse Bash hook: scan command and return block JSON to Codex.
|
|
704
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
705
|
+
source "$SCRIPT_DIR/env.sh"
|
|
706
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
707
|
+
export AI_SCANNER_DETECTION_LOG
|
|
708
|
+
INPUT=$(cat)
|
|
709
|
+
CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)
|
|
710
|
+
[ -z "$CMD" ] || [ "$CMD" = "null" ] && exit 0
|
|
711
|
+
SCAN_STDOUT="$(mktemp)"
|
|
712
|
+
SCAN_STDERR="$(mktemp)"
|
|
713
|
+
cleanup() {
|
|
714
|
+
rm -f "$SCAN_STDOUT" "$SCAN_STDERR"
|
|
715
|
+
}
|
|
716
|
+
trap cleanup EXIT
|
|
717
|
+
|
|
718
|
+
printf '%s' "$CMD" | run_scanner scan bash-input >"$SCAN_STDOUT" 2>"$SCAN_STDERR"
|
|
719
|
+
RC=$?
|
|
720
|
+
[ "$RC" -eq 0 ] && exit 0
|
|
721
|
+
[ "$RC" -ne 2 ] && exit 0
|
|
722
|
+
|
|
723
|
+
REASON="$(jq -r '.message // empty' <"$SCAN_STDOUT" 2>/dev/null)"
|
|
724
|
+
[ -n "$REASON" ] || REASON="$(cat "$SCAN_STDERR" 2>/dev/null)"
|
|
725
|
+
[ -n "$REASON" ] || REASON="AIDR blocked the Bash command."
|
|
726
|
+
|
|
727
|
+
jq -cn \
|
|
728
|
+
--arg reason "$REASON" \
|
|
729
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$reason}}'
|
|
730
|
+
`;
|
|
731
|
+
|
|
732
|
+
export const CODEX_POST_SHELL_SH = `#!/bin/bash
|
|
733
|
+
# Codex PostToolUse Bash hook: scan command output and return feedback JSON.
|
|
734
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
|
|
735
|
+
source "$SCRIPT_DIR/env.sh"
|
|
736
|
+
BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
|
|
737
|
+
export AI_SCANNER_DETECTION_LOG
|
|
738
|
+
INPUT=$(cat)
|
|
739
|
+
CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)
|
|
740
|
+
OUTPUT=$(jq -r '.tool_response.output // .tool_output // ""' <<<"$INPUT" 2>/dev/null)
|
|
741
|
+
[ -z "$OUTPUT" ] || [ "$OUTPUT" = "null" ] && exit 0
|
|
742
|
+
SCAN_STDOUT="$(mktemp)"
|
|
743
|
+
SCAN_STDERR="$(mktemp)"
|
|
744
|
+
cleanup() {
|
|
745
|
+
rm -f "$SCAN_STDOUT" "$SCAN_STDERR"
|
|
746
|
+
}
|
|
747
|
+
trap cleanup EXIT
|
|
748
|
+
|
|
749
|
+
printf '%s' "$OUTPUT" | run_scanner scan bash-output --command "$CMD" >"$SCAN_STDOUT" 2>"$SCAN_STDERR"
|
|
750
|
+
RC=$?
|
|
751
|
+
[ "$RC" -eq 0 ] && exit 0
|
|
752
|
+
[ "$RC" -ne 2 ] && exit 0
|
|
753
|
+
|
|
754
|
+
REASON="$(jq -r '.message // empty' <"$SCAN_STDOUT" 2>/dev/null)"
|
|
755
|
+
[ -n "$REASON" ] || REASON="$(cat "$SCAN_STDERR" 2>/dev/null)"
|
|
756
|
+
[ -n "$REASON" ] || REASON="AIDR found risky Bash output."
|
|
757
|
+
|
|
758
|
+
jq -cn --arg reason "$REASON" '{decision:"block",reason:$reason}'
|
|
759
|
+
`;
|