fullcourtdefense-cli 1.21.7 → 1.21.8
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.
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scanDeterministicClipboard = scanDeterministicClipboard;
|
|
4
|
+
const deterministicGuard_1 = require("./deterministicGuard");
|
|
5
|
+
/**
|
|
6
|
+
* Clipboard scan for advisory paste protection (e.g. Claude Desktop, where the
|
|
7
|
+
* native chat has no hook and the MCP gateway can't see typed prompts). A
|
|
8
|
+
* recognizable credential sitting on the clipboard is a finding on its own —
|
|
9
|
+
* the very next action is a paste the guard cannot observe.
|
|
10
|
+
*
|
|
11
|
+
* This composes the SAME deterministic engine used everywhere else via its
|
|
12
|
+
* public API, so detection patterns and per-rule Local Safety toggles are
|
|
13
|
+
* identical. No new detection logic and no keyword list lives here.
|
|
14
|
+
*
|
|
15
|
+
* - Prefixed secrets (sk-, ghp_, AKIA…) and keyword-adjacent high-entropy
|
|
16
|
+
* strings are caught directly by scanDeterministicTextResponse.
|
|
17
|
+
* - A clipboard that is ONE bare credential-shaped token has, by definition,
|
|
18
|
+
* no surrounding keyword. We re-run the same contextual detector with a
|
|
19
|
+
* synthetic keyword so its entropy + shape gate still decides — a lone hex
|
|
20
|
+
* app secret (the exact WhatsApp-secret scenario) is caught, while a hash
|
|
21
|
+
* embedded in a sentence or diff stays silent because it is never a lone
|
|
22
|
+
* token.
|
|
23
|
+
*/
|
|
24
|
+
const LONE_TOKEN = /^[A-Za-z0-9_\-+/=]{24,256}$/;
|
|
25
|
+
function scanDeterministicClipboard(text, options) {
|
|
26
|
+
const trimmed = (text || '').trim();
|
|
27
|
+
if (!trimmed)
|
|
28
|
+
return undefined;
|
|
29
|
+
const direct = (0, deterministicGuard_1.scanDeterministicTextResponse)(trimmed, options);
|
|
30
|
+
if (direct)
|
|
31
|
+
return direct;
|
|
32
|
+
if (LONE_TOKEN.test(trimmed)) {
|
|
33
|
+
// Inject a canonical keyword so the contextual detector's proximity check
|
|
34
|
+
// passes; its high-entropy gate still does the real work. The synthetic
|
|
35
|
+
// prefix is never surfaced — the finding's evidence comes from the token.
|
|
36
|
+
return (0, deterministicGuard_1.scanDeterministicTextResponse)(`api key ${trimmed}`, options);
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
@@ -12,14 +12,24 @@ export declare function desktopChatGuardSupported(): boolean;
|
|
|
12
12
|
export declare function desktopChatGuardHealthy(withinMs?: number): boolean;
|
|
13
13
|
/**
|
|
14
14
|
* The PowerShell watcher. Compiles a tiny MSAA reader (C# via Add-Type using
|
|
15
|
-
* the .NET Framework compiler built into Windows), then —
|
|
16
|
-
*
|
|
17
|
-
* `FCD:<base64 utf8>`
|
|
18
|
-
*
|
|
15
|
+
* the .NET Framework compiler built into Windows), then — while the "Claude"
|
|
16
|
+
* process is running — reads the composer's editable text (emitting
|
|
17
|
+
* `FCD:<base64 utf8>`) and, only while Claude Desktop is the FOREGROUND window,
|
|
18
|
+
* the clipboard contents (emitting `FCDCLIP:<base64 utf8>`) when either
|
|
19
|
+
* changes. Everything is wrapped in try/catch — it must never crash the host,
|
|
20
|
+
* and it never writes to the clipboard or synthesizes input.
|
|
19
21
|
*/
|
|
20
22
|
export declare function desktopChatWatcherScript(): string;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
export type WatcherSource = 'composer' | 'clipboard';
|
|
24
|
+
/**
|
|
25
|
+
* Decode one watcher line into { source, text } (undefined if not ours).
|
|
26
|
+
* `FCD:` lines are composer reads; `FCDCLIP:` lines are clipboard reads while
|
|
27
|
+
* Claude Desktop is focused.
|
|
28
|
+
*/
|
|
29
|
+
export declare function decodeWatcherLine(line: string): {
|
|
30
|
+
source: WatcherSource;
|
|
31
|
+
text: string;
|
|
32
|
+
} | undefined;
|
|
23
33
|
export interface DesktopChatGuardHandle {
|
|
24
34
|
stop(): void;
|
|
25
35
|
}
|
|
@@ -47,6 +47,7 @@ const readline = __importStar(require("readline"));
|
|
|
47
47
|
const config_1 = require("../config");
|
|
48
48
|
const localSafetySnapshot_1 = require("../localSafetySnapshot");
|
|
49
49
|
const deterministicGuard_1 = require("./deterministicGuard");
|
|
50
|
+
const clipboardScan_1 = require("./clipboardScan");
|
|
50
51
|
const telemetry_1 = require("../telemetry");
|
|
51
52
|
const notify_1 = require("../notify");
|
|
52
53
|
const machineIdentity_1 = require("../machineIdentity");
|
|
@@ -78,21 +79,27 @@ const discoverPaths_1 = require("./discoverPaths");
|
|
|
78
79
|
* The SUPPORTED protection surface for Claude Desktop is its MCP tool calls
|
|
79
80
|
* through the FCD gateway — that is where enforcement happens.
|
|
80
81
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
82
|
+
* Paste protection (clipboard) — the reliable surface:
|
|
83
|
+
* Because reading the composer is best-effort, the guard ALSO watches the
|
|
84
|
+
* Windows clipboard, but ONLY while Claude Desktop is the foreground window.
|
|
85
|
+
* When a developer pastes a secret into Claude (the exact scenario we keep
|
|
86
|
+
* testing), the value is on the clipboard at that moment — the guard runs it
|
|
87
|
+
* through the SAME deterministic engine and warns before it reaches the model.
|
|
88
|
+
* Scoping to the Claude foreground window means a secret copied for any other
|
|
89
|
+
* app is never inspected. This is app-agnostic detection reused only here.
|
|
87
90
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
91
|
+
* Advisory ONLY: no keyboard hook, no input mutation, no keystroke capture, and
|
|
92
|
+
* it never writes to the clipboard. It reads the composer text and (only when
|
|
93
|
+
* Claude is focused) the clipboard, scans in-process, and — on a finding —
|
|
94
|
+
* shows a native warning window and reports a monitor event to the fleet. Text
|
|
95
|
+
* is never uploaded; only finding metadata (item id + masked value) is spooled.
|
|
96
|
+
* It cannot break typing and presents no keylogger surface.
|
|
92
97
|
*/
|
|
93
98
|
const WATCHER_PS1_PATH = path.join(os.homedir(), '.fullcourtdefense-claude-desktop-watcher.ps1');
|
|
94
99
|
const STATUS_PATH = path.join(os.homedir(), '.fullcourtdefense', 'claude-desktop-guard.json');
|
|
95
100
|
const STDOUT_PREFIX = 'FCD:';
|
|
101
|
+
/** Prefix for clipboard payloads (distinct from composer reads). */
|
|
102
|
+
const CLIP_PREFIX = 'FCDCLIP:';
|
|
96
103
|
/** Poll cadence for the composer read (ms). 800ms lost the race against a
|
|
97
104
|
* fast paste+Enter (the composer clears before the next read); 250ms is still
|
|
98
105
|
* negligible CPU for one MSAA tree walk. */
|
|
@@ -135,10 +142,12 @@ function desktopChatGuardHealthy(withinMs = 15 * 60_000) {
|
|
|
135
142
|
}
|
|
136
143
|
/**
|
|
137
144
|
* The PowerShell watcher. Compiles a tiny MSAA reader (C# via Add-Type using
|
|
138
|
-
* the .NET Framework compiler built into Windows), then —
|
|
139
|
-
*
|
|
140
|
-
* `FCD:<base64 utf8>`
|
|
141
|
-
*
|
|
145
|
+
* the .NET Framework compiler built into Windows), then — while the "Claude"
|
|
146
|
+
* process is running — reads the composer's editable text (emitting
|
|
147
|
+
* `FCD:<base64 utf8>`) and, only while Claude Desktop is the FOREGROUND window,
|
|
148
|
+
* the clipboard contents (emitting `FCDCLIP:<base64 utf8>`) when either
|
|
149
|
+
* changes. Everything is wrapped in try/catch — it must never crash the host,
|
|
150
|
+
* and it never writes to the clipboard or synthesizes input.
|
|
142
151
|
*/
|
|
143
152
|
function desktopChatWatcherScript() {
|
|
144
153
|
return [
|
|
@@ -163,8 +172,15 @@ function desktopChatWatcherScript() {
|
|
|
163
172
|
' [DllImport("user32.dll")] static extern int GetWindowText(IntPtr h, StringBuilder sb, int max);',
|
|
164
173
|
' [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);',
|
|
165
174
|
' [DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr h, uint msg, IntPtr wParam, IntPtr lParam);',
|
|
175
|
+
' [DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();',
|
|
166
176
|
' const uint WM_GETOBJECT = 0x003D;',
|
|
167
177
|
' public delegate bool EnumProc(IntPtr h, IntPtr p);',
|
|
178
|
+
' public static bool ClaudeIsForeground(HashSet<uint> pids) {',
|
|
179
|
+
' IntPtr fg = GetForegroundWindow();',
|
|
180
|
+
' if (fg == IntPtr.Zero) return false;',
|
|
181
|
+
' uint pid; GetWindowThreadProcessId(fg, out pid);',
|
|
182
|
+
' return pids.Contains(pid);',
|
|
183
|
+
' }',
|
|
168
184
|
' public static IntPtr FindMain(HashSet<uint> pids) {',
|
|
169
185
|
' IntPtr result = IntPtr.Zero;',
|
|
170
186
|
' EnumWindows((h, p) => {',
|
|
@@ -227,6 +243,7 @@ function desktopChatWatcherScript() {
|
|
|
227
243
|
'} catch { }',
|
|
228
244
|
'',
|
|
229
245
|
'$last = ""',
|
|
246
|
+
'$lastClip = ""',
|
|
230
247
|
'$lastWake = [DateTime]::MinValue',
|
|
231
248
|
'while ($true) {',
|
|
232
249
|
' Start-Sleep -Milliseconds ' + POLL_MS,
|
|
@@ -234,6 +251,19 @@ function desktopChatWatcherScript() {
|
|
|
234
251
|
' $pids = New-Object \'System.Collections.Generic.HashSet[uint32]\'',
|
|
235
252
|
" Get-Process -Name 'Claude' -ErrorAction SilentlyContinue | ForEach-Object { [void]$pids.Add([uint32]$_.Id) }",
|
|
236
253
|
' if ($pids.Count -eq 0) { continue }',
|
|
254
|
+
// Clipboard paste guard: only while Claude Desktop is the foreground app, so
|
|
255
|
+
// a copy staged for any OTHER application is never inspected or reported.
|
|
256
|
+
// The deterministic engine (parent process) decides if the text is a secret.
|
|
257
|
+
' if ([FcdMsaa]::ClaudeIsForeground($pids)) {',
|
|
258
|
+
' $clip = ""',
|
|
259
|
+
' try { $clip = Get-Clipboard -Raw -ErrorAction SilentlyContinue } catch { }',
|
|
260
|
+
' if (-not [string]::IsNullOrEmpty($clip) -and $clip.Length -le 8000 -and $clip -ne $lastClip) {',
|
|
261
|
+
' $lastClip = $clip',
|
|
262
|
+
' $cb = [System.Text.Encoding]::UTF8.GetBytes($clip)',
|
|
263
|
+
" [Console]::Out.WriteLine('" + CLIP_PREFIX + "' + [Convert]::ToBase64String($cb))",
|
|
264
|
+
' [Console]::Out.Flush()',
|
|
265
|
+
' }',
|
|
266
|
+
' }',
|
|
237
267
|
' $main = [FcdMsaa]::FindMain($pids)',
|
|
238
268
|
' if ($main -eq [IntPtr]::Zero) { continue }',
|
|
239
269
|
' if (([DateTime]::UtcNow - $lastWake).TotalSeconds -ge 30) { [FcdMsaa]::Wake($main); $lastWake = [DateTime]::UtcNow }',
|
|
@@ -260,12 +290,21 @@ function desktopChatWatcherScript() {
|
|
|
260
290
|
'',
|
|
261
291
|
].join('\n');
|
|
262
292
|
}
|
|
263
|
-
/**
|
|
293
|
+
/**
|
|
294
|
+
* Decode one watcher line into { source, text } (undefined if not ours).
|
|
295
|
+
* `FCD:` lines are composer reads; `FCDCLIP:` lines are clipboard reads while
|
|
296
|
+
* Claude Desktop is focused.
|
|
297
|
+
*/
|
|
264
298
|
function decodeWatcherLine(line) {
|
|
265
|
-
|
|
299
|
+
const prefix = line.startsWith(CLIP_PREFIX)
|
|
300
|
+
? { p: CLIP_PREFIX, source: 'clipboard' }
|
|
301
|
+
: line.startsWith(STDOUT_PREFIX)
|
|
302
|
+
? { p: STDOUT_PREFIX, source: 'composer' }
|
|
303
|
+
: undefined;
|
|
304
|
+
if (!prefix)
|
|
266
305
|
return undefined;
|
|
267
306
|
try {
|
|
268
|
-
return Buffer.from(line.slice(
|
|
307
|
+
return { source: prefix.source, text: Buffer.from(line.slice(prefix.p.length), 'base64').toString('utf8') };
|
|
269
308
|
}
|
|
270
309
|
catch {
|
|
271
310
|
return undefined;
|
|
@@ -312,10 +351,12 @@ function startDesktopChatGuard(runtime) {
|
|
|
312
351
|
startedAt,
|
|
313
352
|
heartbeatAt: new Date().toISOString(),
|
|
314
353
|
});
|
|
315
|
-
const handleText = (text) => {
|
|
354
|
+
const handleText = (text, source) => {
|
|
316
355
|
if (!text.trim())
|
|
317
356
|
return;
|
|
318
|
-
const finding =
|
|
357
|
+
const finding = source === 'clipboard'
|
|
358
|
+
? (0, clipboardScan_1.scanDeterministicClipboard)(text, scanOptions)
|
|
359
|
+
: (0, deterministicGuard_1.scanDeterministicPrompt)(text, scanOptions);
|
|
319
360
|
if (!finding)
|
|
320
361
|
return;
|
|
321
362
|
findings += 1;
|
|
@@ -327,7 +368,7 @@ function startDesktopChatGuard(runtime) {
|
|
|
327
368
|
(0, telemetry_1.spoolEvent)({
|
|
328
369
|
decision: 'allow',
|
|
329
370
|
toolName: 'claude_desktop_chat',
|
|
330
|
-
operation: 'prompt',
|
|
371
|
+
operation: source === 'clipboard' ? 'clipboard_paste' : 'prompt',
|
|
331
372
|
reason: `would block: ${finding.reason}`,
|
|
332
373
|
ruleId: finding.ruleId,
|
|
333
374
|
category: finding.category,
|
|
@@ -339,21 +380,24 @@ function startDesktopChatGuard(runtime) {
|
|
|
339
380
|
policyHash: finding.policyHash,
|
|
340
381
|
});
|
|
341
382
|
(0, telemetry_1.triggerFlush)();
|
|
342
|
-
const key = `${finding.itemId}:${masked}`;
|
|
383
|
+
const key = `${source}:${finding.itemId}:${masked}`;
|
|
343
384
|
const now = Date.now();
|
|
344
385
|
const last = lastToastAt.get(key) || 0;
|
|
345
386
|
if (!runtime.quiet && now - last > TOAST_DEBOUNCE_MS) {
|
|
346
387
|
lastToastAt.set(key, now);
|
|
388
|
+
const message = source === 'clipboard'
|
|
389
|
+
? `${finding.reason} You just copied this — do NOT paste it into Claude Desktop; its chat is not protected.`
|
|
390
|
+
: `${finding.reason}. Remove it before sending — this text has not been protected.`;
|
|
347
391
|
(0, notify_1.notifyOs)({
|
|
348
392
|
title: 'FullCourtDefense: secret in Claude Desktop',
|
|
349
|
-
message
|
|
393
|
+
message,
|
|
350
394
|
// Security warning: use a top-most window, not a toast. Toasts are
|
|
351
395
|
// silently swallowed by Focus Assist / Do-Not-Disturb and per-app
|
|
352
396
|
// banner settings — verified in the field — which would hide the alert.
|
|
353
397
|
forceWindow: true,
|
|
354
398
|
});
|
|
355
399
|
}
|
|
356
|
-
runtime.log(`Claude Desktop
|
|
400
|
+
runtime.log(`Claude Desktop ${source} finding: ${finding.itemId} (${finding.reason}).`);
|
|
357
401
|
};
|
|
358
402
|
const spawnWatcher = () => {
|
|
359
403
|
if (stopped)
|
|
@@ -376,11 +420,11 @@ function startDesktopChatGuard(runtime) {
|
|
|
376
420
|
}
|
|
377
421
|
const rl = readline.createInterface({ input: child.stdout });
|
|
378
422
|
rl.on('line', (line) => {
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
423
|
+
const decoded = decodeWatcherLine(line);
|
|
424
|
+
if (decoded === undefined)
|
|
381
425
|
return;
|
|
382
426
|
restartDelay = RESTART_MIN_MS; // healthy output resets backoff
|
|
383
|
-
handleText(text);
|
|
427
|
+
handleText(decoded.text, decoded.source);
|
|
384
428
|
});
|
|
385
429
|
child.on('exit', () => { rl.close(); if (!stopped)
|
|
386
430
|
scheduleRestart(); });
|
package/dist/version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.8",
|
|
4
4
|
"description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"test:posture-overhaul": "npm run build && node scripts/test-posture-overhaul.js",
|
|
36
36
|
"test:dpapi-config": "npm run build && node scripts/test-dpapi-config.js",
|
|
37
37
|
"test:desktop-chat-guard": "npm run build && node scripts/test-desktop-chat-guard.js",
|
|
38
|
+
"test:clipboard-scan": "npm run build && node scripts/test-clipboard-scan.js",
|
|
38
39
|
"build:msi": "powershell -NoProfile -ExecutionPolicy Bypass -File installer/windows/Build-Msi.ps1",
|
|
39
40
|
"prepublishOnly": "npm run build"
|
|
40
41
|
},
|