fullcourtdefense-cli 1.21.10 → 1.21.12
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/dist/commands/daemon.js +20 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +7 -2
- package/dist/commands/onboard.js +1 -1
- package/dist/machineActionVerify.d.ts +45 -0
- package/dist/machineActionVerify.js +134 -0
- package/dist/runtimeConfig.d.ts +10 -0
- package/dist/version.json +1 -1
- package/package.json +2 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -51,6 +51,7 @@ const integrity_1 = require("../integrity");
|
|
|
51
51
|
const machineIdentity_1 = require("../machineIdentity");
|
|
52
52
|
const discoveryMarker_1 = require("../discoveryMarker");
|
|
53
53
|
const selfUpdate_1 = require("../selfUpdate");
|
|
54
|
+
const machineActionVerify_1 = require("../machineActionVerify");
|
|
54
55
|
const desktopChatGuard_1 = require("./desktopChatGuard");
|
|
55
56
|
const COLOR = {
|
|
56
57
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
@@ -531,6 +532,25 @@ async function runDaemon(args, config) {
|
|
|
531
532
|
if (executingActionIds.has(action.id))
|
|
532
533
|
return;
|
|
533
534
|
executingActionIds.add(action.id);
|
|
535
|
+
// Cryptographic gate: never execute an action the control plane didn't
|
|
536
|
+
// sign — a Firestore/backend compromise must not become fleet-wide RCE.
|
|
537
|
+
const verdict = (0, machineActionVerify_1.verifyMachineAction)(action);
|
|
538
|
+
if (!verdict.ok) {
|
|
539
|
+
log(`Remote action REJECTED: ${action.type} (${action.id}) — ${verdict.reason}`);
|
|
540
|
+
await reportMachineAction(action.id, 'failed', { error: `Signature verification failed: ${verdict.reason}` });
|
|
541
|
+
executingActionIds.delete(action.id);
|
|
542
|
+
await uploadLogTail();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (verdict.reason)
|
|
546
|
+
log(`Remote action signature notice: ${verdict.reason}`);
|
|
547
|
+
const localIdentity = (0, machineIdentity_1.getMachineIdentity)();
|
|
548
|
+
if (action.machineId && action.machineId !== localIdentity.machineId) {
|
|
549
|
+
// Warn-only for now: enrolled machines can carry legacy fingerprints that
|
|
550
|
+
// differ from the freshly computed identity. Tighten to reject once the
|
|
551
|
+
// fleet is fully on stable machine ids.
|
|
552
|
+
log(`Remote action machine-binding notice: action targets ${action.machineId}, local id is ${localIdentity.machineId}.`);
|
|
553
|
+
}
|
|
534
554
|
await reportMachineAction(action.id, 'running');
|
|
535
555
|
log(`Remote action started: ${action.type} (${action.id}).`);
|
|
536
556
|
await uploadLogTail();
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface LoginArgs {
|
|
|
11
11
|
*
|
|
12
12
|
* The machine authenticates to the org with a reusable fleet token (distributed
|
|
13
13
|
* by IT/MDM), self-enrolls under its stable machine id, and receives a dedicated
|
|
14
|
-
* per-machine Shield (
|
|
14
|
+
* per-machine Shield (enforcing by default). Credentials are written to
|
|
15
15
|
* ~/.fullcourtdefense.yml so no manual copy/paste of Shield IDs/keys is needed.
|
|
16
16
|
*/
|
|
17
17
|
export declare function loginCommand(args: LoginArgs, config: BotGuardConfig): Promise<void>;
|
package/dist/commands/login.js
CHANGED
|
@@ -8,7 +8,7 @@ const machineIdentity_1 = require("../machineIdentity");
|
|
|
8
8
|
*
|
|
9
9
|
* The machine authenticates to the org with a reusable fleet token (distributed
|
|
10
10
|
* by IT/MDM), self-enrolls under its stable machine id, and receives a dedicated
|
|
11
|
-
* per-machine Shield (
|
|
11
|
+
* per-machine Shield (enforcing by default). Credentials are written to
|
|
12
12
|
* ~/.fullcourtdefense.yml so no manual copy/paste of Shield IDs/keys is needed.
|
|
13
13
|
*/
|
|
14
14
|
async function loginCommand(args, config) {
|
|
@@ -58,7 +58,12 @@ async function loginCommand(args, config) {
|
|
|
58
58
|
});
|
|
59
59
|
console.log('');
|
|
60
60
|
console.log(`\x1b[32m✓ ${result.reused ? 'Re-enrolled' : 'Enrolled'}\x1b[0m ${result.shieldName}`);
|
|
61
|
-
|
|
61
|
+
const modeNote = result.mode === 'block'
|
|
62
|
+
? 'enforcing — dangerous actions and matching policies are blocked'
|
|
63
|
+
: result.mode === 'monitor'
|
|
64
|
+
? 'monitor — actions are logged but not blocked'
|
|
65
|
+
: 'shadow — evaluation only, nothing is blocked';
|
|
66
|
+
console.log(` Mode: ${result.mode} (${modeNote}). Change it anytime from the dashboard.`);
|
|
62
67
|
console.log(` Config: ${savedPath}`);
|
|
63
68
|
console.log('');
|
|
64
69
|
console.log('Next: \x1b[1mfullcourtdefense install-all\x1b[0m to wrap your MCP clients and install available runtime hooks.');
|
package/dist/commands/onboard.js
CHANGED
|
@@ -419,7 +419,7 @@ async function onboardCommand(args, config) {
|
|
|
419
419
|
const dashboard = (0, notify_1.consoleUrl)('/agent-security/users?view=desktop');
|
|
420
420
|
console.log('');
|
|
421
421
|
if (requiredFailures.length === 0) {
|
|
422
|
-
console.log(`${GREEN}${BOLD}This machine is protected.${RESET} It reports to your org's AI Fleet (
|
|
422
|
+
console.log(`${GREEN}${BOLD}This machine is protected.${RESET} It reports to your org's AI Fleet and enforces your policies (dangerous actions are blocked). An admin can switch it to monitor-only from the dashboard.`);
|
|
423
423
|
console.log(`${DIM}Fleet view: ${dashboard}${RESET}`);
|
|
424
424
|
console.log(`${DIM}Restart your AI clients (Cursor, Claude, VS Code, …) so they pick up the wrapped configs.${RESET}`);
|
|
425
425
|
mark('verification', 'completed');
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ed25519 verification for remote machine actions pulled from the control
|
|
3
|
+
* plane (GET /api/cli/bundle -> machineAction).
|
|
4
|
+
*
|
|
5
|
+
* The daemon must not execute an action just because the backend returned it:
|
|
6
|
+
* a compromised backend/Firestore or stolen admin session could queue actions
|
|
7
|
+
* for the whole fleet. Every action is signed server-side with a private key
|
|
8
|
+
* held in GCP Secret Manager; this module re-builds the canonical payload and
|
|
9
|
+
* verifies the signature against the public key pinned below.
|
|
10
|
+
*
|
|
11
|
+
* Enforcement: when a pinned/override public key is available, actions with a
|
|
12
|
+
* missing or invalid signature are REJECTED. `FCD_MACHINE_ACTION_UNSIGNED_OK=1`
|
|
13
|
+
* is an explicit, loudly-logged local escape hatch for rollout emergencies.
|
|
14
|
+
*
|
|
15
|
+
* Keep the canonical payload construction in sync with
|
|
16
|
+
* backend/src/modules/discovery/machine-action-signing.ts.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MACHINE_ACTION_SIGNATURE_VERSION = "fcd-machine-action-v1";
|
|
19
|
+
export interface VerifiableMachineAction {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
expiresAt: string;
|
|
25
|
+
organizationId?: string;
|
|
26
|
+
machineId?: string;
|
|
27
|
+
signature?: {
|
|
28
|
+
signature: string;
|
|
29
|
+
keyId: string;
|
|
30
|
+
alg: string;
|
|
31
|
+
version: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface MachineActionVerdict {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
reason?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Verify a machine action before execution. Rejects when:
|
|
40
|
+
* - the signature is missing (unless the local escape hatch is set),
|
|
41
|
+
* - the signature's keyId is unknown,
|
|
42
|
+
* - the Ed25519 verification fails (any signed field was tampered with),
|
|
43
|
+
* - the action is already expired.
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyMachineAction(action: VerifiableMachineAction): MachineActionVerdict;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MACHINE_ACTION_SIGNATURE_VERSION = void 0;
|
|
37
|
+
exports.verifyMachineAction = verifyMachineAction;
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
/**
|
|
40
|
+
* Ed25519 verification for remote machine actions pulled from the control
|
|
41
|
+
* plane (GET /api/cli/bundle -> machineAction).
|
|
42
|
+
*
|
|
43
|
+
* The daemon must not execute an action just because the backend returned it:
|
|
44
|
+
* a compromised backend/Firestore or stolen admin session could queue actions
|
|
45
|
+
* for the whole fleet. Every action is signed server-side with a private key
|
|
46
|
+
* held in GCP Secret Manager; this module re-builds the canonical payload and
|
|
47
|
+
* verifies the signature against the public key pinned below.
|
|
48
|
+
*
|
|
49
|
+
* Enforcement: when a pinned/override public key is available, actions with a
|
|
50
|
+
* missing or invalid signature are REJECTED. `FCD_MACHINE_ACTION_UNSIGNED_OK=1`
|
|
51
|
+
* is an explicit, loudly-logged local escape hatch for rollout emergencies.
|
|
52
|
+
*
|
|
53
|
+
* Keep the canonical payload construction in sync with
|
|
54
|
+
* backend/src/modules/discovery/machine-action-signing.ts.
|
|
55
|
+
*/
|
|
56
|
+
exports.MACHINE_ACTION_SIGNATURE_VERSION = 'fcd-machine-action-v1';
|
|
57
|
+
/**
|
|
58
|
+
* Production control-plane public key (keyId v1). Rotations ship a new CLI
|
|
59
|
+
* release with the additional key appended.
|
|
60
|
+
*/
|
|
61
|
+
const EMBEDDED_PUBLIC_KEYS_PEM = {
|
|
62
|
+
v1: `-----BEGIN PUBLIC KEY-----
|
|
63
|
+
MCowBQYDK2VwAyEAbzPhAyATO3n6FL0rfPmlmFa8xz7VABNlU9iOh0GwucI=
|
|
64
|
+
-----END PUBLIC KEY-----`,
|
|
65
|
+
};
|
|
66
|
+
function toIso(value) {
|
|
67
|
+
const parsed = new Date(value);
|
|
68
|
+
return Number.isNaN(parsed.getTime()) ? '' : parsed.toISOString();
|
|
69
|
+
}
|
|
70
|
+
function canonicalPayload(action) {
|
|
71
|
+
return Buffer.from(JSON.stringify([
|
|
72
|
+
exports.MACHINE_ACTION_SIGNATURE_VERSION,
|
|
73
|
+
action.id,
|
|
74
|
+
action.organizationId || '',
|
|
75
|
+
action.machineId || '',
|
|
76
|
+
action.type,
|
|
77
|
+
action.reason,
|
|
78
|
+
toIso(action.createdAt),
|
|
79
|
+
toIso(action.expiresAt),
|
|
80
|
+
]), 'utf8');
|
|
81
|
+
}
|
|
82
|
+
/** Resolve the verification key for a signature keyId (env override wins — staging/tests). */
|
|
83
|
+
function resolvePublicKeyPem(keyId) {
|
|
84
|
+
const override = process.env.FCD_MACHINE_ACTION_PUBLIC_KEY?.trim();
|
|
85
|
+
if (override) {
|
|
86
|
+
return override.includes('-----BEGIN')
|
|
87
|
+
? override
|
|
88
|
+
: Buffer.from(override, 'base64').toString('utf8');
|
|
89
|
+
}
|
|
90
|
+
return EMBEDDED_PUBLIC_KEYS_PEM[keyId];
|
|
91
|
+
}
|
|
92
|
+
function unsignedAllowed() {
|
|
93
|
+
return process.env.FCD_MACHINE_ACTION_UNSIGNED_OK === '1';
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Verify a machine action before execution. Rejects when:
|
|
97
|
+
* - the signature is missing (unless the local escape hatch is set),
|
|
98
|
+
* - the signature's keyId is unknown,
|
|
99
|
+
* - the Ed25519 verification fails (any signed field was tampered with),
|
|
100
|
+
* - the action is already expired.
|
|
101
|
+
*/
|
|
102
|
+
function verifyMachineAction(action) {
|
|
103
|
+
const expiresAt = Date.parse(action.expiresAt);
|
|
104
|
+
if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
|
|
105
|
+
return { ok: false, reason: 'Action is expired.' };
|
|
106
|
+
}
|
|
107
|
+
if (!action.signature?.signature) {
|
|
108
|
+
if (unsignedAllowed()) {
|
|
109
|
+
return { ok: true, reason: 'UNSIGNED action accepted via FCD_MACHINE_ACTION_UNSIGNED_OK=1.' };
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: 'Action is not signed by the control plane. Rejected: this CLI requires Ed25519-signed remote actions (provision MACHINE_ACTION_SIGNING_KEY on the backend).',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const { signature } = action;
|
|
117
|
+
if (signature.alg !== 'ed25519' || signature.version !== exports.MACHINE_ACTION_SIGNATURE_VERSION) {
|
|
118
|
+
return { ok: false, reason: `Unsupported signature scheme (${signature.alg}/${signature.version}).` };
|
|
119
|
+
}
|
|
120
|
+
const publicKeyPem = resolvePublicKeyPem(signature.keyId);
|
|
121
|
+
if (!publicKeyPem) {
|
|
122
|
+
return { ok: false, reason: `No pinned public key for signing keyId "${signature.keyId}" — upgrade the CLI or check FCD_MACHINE_ACTION_PUBLIC_KEY.` };
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const key = crypto.createPublicKey(publicKeyPem);
|
|
126
|
+
const valid = crypto.verify(null, canonicalPayload(action), key, Buffer.from(signature.signature, 'base64'));
|
|
127
|
+
return valid
|
|
128
|
+
? { ok: true }
|
|
129
|
+
: { ok: false, reason: 'Invalid signature — the action payload does not match what the control plane signed.' };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return { ok: false, reason: `Signature verification error: ${error.message}` };
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/runtimeConfig.d.ts
CHANGED
|
@@ -40,6 +40,16 @@ export interface RuntimeBundle {
|
|
|
40
40
|
reason: string;
|
|
41
41
|
createdAt: string;
|
|
42
42
|
expiresAt: string;
|
|
43
|
+
/** Signed fields — present so the daemon can re-build the canonical payload. */
|
|
44
|
+
organizationId?: string;
|
|
45
|
+
machineId?: string;
|
|
46
|
+
/** Ed25519 control-plane signature; the daemon rejects unsigned/invalid actions. */
|
|
47
|
+
signature?: {
|
|
48
|
+
signature: string;
|
|
49
|
+
keyId: string;
|
|
50
|
+
alg: string;
|
|
51
|
+
version: string;
|
|
52
|
+
};
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
export interface FetchBundleInput {
|
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.12",
|
|
4
4
|
"description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"test:posture-blast": "npm run build && node scripts/test-posture-blast-radius.js",
|
|
32
32
|
"test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
|
|
33
33
|
"test:daemon-e2e": "npm run build && node scripts/test-daemon-e2e.js",
|
|
34
|
+
"test:machine-action-signing": "npm run build && node scripts/test-machine-action-signing.js",
|
|
34
35
|
"test:integrity": "npm run build && node scripts/test-integrity.js",
|
|
35
36
|
"test:posture-overhaul": "npm run build && node scripts/test-posture-overhaul.js",
|
|
36
37
|
"test:dpapi-config": "npm run build && node scripts/test-dpapi-config.js",
|