codiedev 0.10.0-beta.5 → 0.10.0-beta.7
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 +4 -4
- package/dist/captureSignals.js +2 -0
- package/dist/cli.js +38 -50
- package/dist/codexHookTrust.js +67 -4
- package/dist/codie.js +23 -74
- package/dist/codieLauncher.js +18 -22
- package/dist/commands/agentTemplates.generated.js +136 -0
- package/dist/commands/agents.js +1068 -0
- package/dist/commands/agentsSdk.generated.js +7 -0
- package/dist/commands/doctor.js +12 -38
- package/dist/commands/evals.js +187 -0
- package/dist/commands/pack.js +1 -1
- package/dist/commands/traces.js +79 -15
- package/dist/commands/tracesLabel.js +348 -0
- package/dist/connect.js +11 -52
- package/dist/copilotHooks.js +0 -4
- package/dist/cursorHooks.js +0 -15
- package/dist/harnessDirs.js +4 -0
- package/dist/hook.js +33 -197
- package/dist/limitEventReporter.js +1 -1
- package/dist/managedPi.js +240 -0
- package/dist/mcp.js +0 -307
- package/dist/packs/harnesses.js +40 -3
- package/dist/packs/install.js +2 -1
- package/dist/piHooks.js +216 -0
- package/dist/piRuntime.js +120 -0
- package/dist/selfUpdate.js +47 -2
- package/dist/utils.js +232 -115
- package/package.json +5 -7
- package/dist/codieStartup.js +0 -27
- package/dist/commands/promote.js +0 -53
- package/dist/commands/react.js +0 -38
- package/dist/commands/reverseTicket.js +0 -444
- package/dist/commands/security.js +0 -156
- package/dist/prUrl.js +0 -28
- package/dist/security.js +0 -417
- package/dist/securityDeepScan.js +0 -235
- package/dist/securityDoctor.js +0 -61
- package/dist/securityFeature.js +0 -6
- package/dist/securityInstall.js +0 -232
- package/dist/securityInventory.js +0 -566
- package/dist/securityPilot.js +0 -59
- package/dist/securityPolicy.js +0 -181
- package/dist/securitySession.js +0 -72
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runSecurityCommand = runSecurityCommand;
|
|
4
|
-
const securityInstall_1 = require("../securityInstall");
|
|
5
|
-
const securityPilot_1 = require("../securityPilot");
|
|
6
|
-
const securityPolicy_1 = require("../securityPolicy");
|
|
7
|
-
const securityInventory_1 = require("../securityInventory");
|
|
8
|
-
const securityDeepScan_1 = require("../securityDeepScan");
|
|
9
|
-
const utils_1 = require("../utils");
|
|
10
|
-
const securityDoctor_1 = require("../securityDoctor");
|
|
11
|
-
const securityFeature_1 = require("../securityFeature");
|
|
12
|
-
const SECURITY_USAGE = "Usage: codiedev security <doctor|pilot|inventory|scan|sync|rollback> [--json] [--deep] [--connect-mcp] [--supply-chain] [--no-upload] [--yes]";
|
|
13
|
-
function printPilot(json) {
|
|
14
|
-
const report = (0, securityPilot_1.runSyntheticSecurityPilot)();
|
|
15
|
-
if (json) {
|
|
16
|
-
console.log(JSON.stringify(report, null, 2));
|
|
17
|
-
return report.ok;
|
|
18
|
-
}
|
|
19
|
-
console.log("\nCodiedev security pilot — synthetic, side-effect-free validation\n");
|
|
20
|
-
for (const item of report.cases) {
|
|
21
|
-
console.log(`${item.passed ? "✓" : "✗"} ${item.surface}: ${item.name} (${item.actual})`);
|
|
22
|
-
}
|
|
23
|
-
console.log(`\n${report.ok ? "PASS" : "FAIL"}: ${report.cases.filter((item) => item.passed).length}/${report.cases.length} policy checks matched.`);
|
|
24
|
-
console.log("No commands were executed and no real credentials were used.\n");
|
|
25
|
-
return report.ok;
|
|
26
|
-
}
|
|
27
|
-
function printDoctor(json, companyId) {
|
|
28
|
-
const result = (0, securityDoctor_1.collectSecurityDoctorReport)(companyId);
|
|
29
|
-
if (json) {
|
|
30
|
-
console.log(JSON.stringify(result, null, 2));
|
|
31
|
-
return result.ok;
|
|
32
|
-
}
|
|
33
|
-
console.log("\nCodiedev doctor — Agent Security details\n");
|
|
34
|
-
for (const check of (0, securityDoctor_1.securityDoctorChecks)(result)) {
|
|
35
|
-
const mark = check.status === "pass" ? "✓" : check.status === "warn" ? "!" : "✗";
|
|
36
|
-
console.log(`${mark} ${check.name}: ${check.detail}`);
|
|
37
|
-
}
|
|
38
|
-
console.log(`\n${result.ok ? "PASS" : "FAIL"}: ${result.ok ? "endpoint security is healthy" : "run `codiedev connect`, then repeat this check"}.\n`);
|
|
39
|
-
return result.ok;
|
|
40
|
-
}
|
|
41
|
-
async function runSecurityCommand(args) {
|
|
42
|
-
const subcommand = args.find((arg) => !arg.startsWith("-")) ?? "doctor";
|
|
43
|
-
const json = args.includes("--json");
|
|
44
|
-
if (args.includes("--help") || args.includes("-h") || subcommand === "help") {
|
|
45
|
-
console.log(SECURITY_USAGE);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
const connectedConfig = (0, utils_1.readConfig)();
|
|
49
|
-
const requiresEntitlement = !["pilot", "self-test"].includes(subcommand);
|
|
50
|
-
if (requiresEntitlement && (!connectedConfig || !(0, securityFeature_1.isAgentSecurityEnabled)(connectedConfig))) {
|
|
51
|
-
const detail = connectedConfig
|
|
52
|
-
? `Agent Security is not enabled for ${connectedConfig.companyName}.`
|
|
53
|
-
: "Not connected. Run `codiedev connect` first.";
|
|
54
|
-
if (json)
|
|
55
|
-
console.log(JSON.stringify({ schemaVersion: 1, enabled: false, detail }, null, 2));
|
|
56
|
-
else
|
|
57
|
-
console.log(`\n${detail}\n`);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (subcommand === "inventory" || subcommand === "scan") {
|
|
61
|
-
const workspaceFlag = args.indexOf("--workspace");
|
|
62
|
-
const workspace = workspaceFlag >= 0 && args[workspaceFlag + 1]
|
|
63
|
-
? args[workspaceFlag + 1]
|
|
64
|
-
: process.cwd();
|
|
65
|
-
const inventory = (0, securityInventory_1.buildSecurityInventory)({ workspace });
|
|
66
|
-
const deep = subcommand === "scan" && args.includes("--deep")
|
|
67
|
-
? (0, securityDeepScan_1.runDeepSecurityScan)(inventory, {
|
|
68
|
-
workspace,
|
|
69
|
-
connectMcp: args.includes("--connect-mcp"),
|
|
70
|
-
supplyChain: args.includes("--supply-chain"),
|
|
71
|
-
})
|
|
72
|
-
: undefined;
|
|
73
|
-
const uploadableInventory = deep ? (0, securityDeepScan_1.mergeDeepScanFindings)(inventory, deep) : inventory;
|
|
74
|
-
let upload;
|
|
75
|
-
const config = (0, utils_1.readConfig)();
|
|
76
|
-
if (config && !args.includes("--no-upload")) {
|
|
77
|
-
try {
|
|
78
|
-
upload = await (0, securityInventory_1.uploadSecurityInventory)(uploadableInventory, config);
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
if (!json)
|
|
82
|
-
console.warn(`Inventory upload failed; local scan is still valid: ${error.message}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const output = { inventory: uploadableInventory, scanners: (0, securityInventory_1.scannerAvailability)(), ...(deep ? { deep } : {}), ...(upload ? { upload } : {}) };
|
|
86
|
-
if (json)
|
|
87
|
-
console.log(JSON.stringify(output, null, 2));
|
|
88
|
-
else {
|
|
89
|
-
const risky = inventory.components.filter((component) => component.findings.length > 0).length;
|
|
90
|
-
console.log(`\nCodiedev security inventory — ${inventory.components.length} components, ${risky} with local findings, ${inventory.drift.length} changed\n`);
|
|
91
|
-
const counts = new Map();
|
|
92
|
-
for (const component of inventory.components)
|
|
93
|
-
counts.set(component.kind, (counts.get(component.kind) ?? 0) + 1);
|
|
94
|
-
for (const [kind, count] of [...counts].sort())
|
|
95
|
-
console.log(` ${kind}: ${count}`);
|
|
96
|
-
if (deep)
|
|
97
|
-
for (const run of deep.runs)
|
|
98
|
-
console.log(` ${run.scanner}: ${run.status} — ${run.detail}`);
|
|
99
|
-
if (upload)
|
|
100
|
-
console.log(`\nUploaded ${upload.accepted} component records to the security workspace.`);
|
|
101
|
-
console.log("\nUse --json for machine-readable hashes and findings. Use scan --deep for installed OSS scanners; --connect-mcp and --supply-chain are explicit because they start or download third-party code.\n");
|
|
102
|
-
}
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (subcommand === "sync") {
|
|
106
|
-
const config = (0, utils_1.readConfig)();
|
|
107
|
-
if (!config) {
|
|
108
|
-
console.error("Not connected. Run `codiedev connect` first.");
|
|
109
|
-
process.exitCode = 1;
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
const policy = await (0, securityPolicy_1.syncEndpointSecurityPolicy)(config);
|
|
114
|
-
if (json)
|
|
115
|
-
console.log(JSON.stringify({ ok: true, policy }, null, 2));
|
|
116
|
-
else {
|
|
117
|
-
const verification = (0, securityPolicy_1.verifyEndpointSecurityPolicySignature)(policy);
|
|
118
|
-
console.log(`Security policy v${policy.policyVersion} synced (${policy.enforcementMode} mode; ${verification.detail}).`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
console.error(`Security policy sync failed: ${error.message}`);
|
|
123
|
-
process.exitCode = 1;
|
|
124
|
-
}
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (subcommand === "rollback") {
|
|
128
|
-
if (!args.includes("--yes")) {
|
|
129
|
-
console.error("Rollback overwrites the installed agent hook configurations. Re-run with `codiedev security rollback --yes` after confirming the snapshot shown by `codiedev doctor`.");
|
|
130
|
-
process.exitCode = 1;
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
const result = (0, securityInstall_1.restoreSecurityInstallSnapshot)((0, securityInstall_1.readSecurityInstallManifest)());
|
|
135
|
-
if (json)
|
|
136
|
-
console.log(JSON.stringify({ schemaVersion: 1, ok: true, ...result }, null, 2));
|
|
137
|
-
else
|
|
138
|
-
console.log(`Restored ${result.restored} hook configuration(s) from snapshot ${result.snapshotId}.`);
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
console.error(`Rollback failed: ${error.message}`);
|
|
142
|
-
process.exitCode = 1;
|
|
143
|
-
}
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const ok = subcommand === "pilot" || subcommand === "self-test"
|
|
147
|
-
? printPilot(json)
|
|
148
|
-
: subcommand === "doctor" || subcommand === "status"
|
|
149
|
-
? printDoctor(json, connectedConfig.companyId)
|
|
150
|
-
: false;
|
|
151
|
-
if (!["pilot", "self-test", "doctor", "status"].includes(subcommand)) {
|
|
152
|
-
console.error(SECURITY_USAGE);
|
|
153
|
-
}
|
|
154
|
-
if (!ok)
|
|
155
|
-
process.exitCode = 1;
|
|
156
|
-
}
|
package/dist/prUrl.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parsePrOrMrUrl = parsePrOrMrUrl;
|
|
4
|
-
function parsePrOrMrUrl(url) {
|
|
5
|
-
if (!url)
|
|
6
|
-
return null;
|
|
7
|
-
const gh = url.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:[/?#]|$)/);
|
|
8
|
-
if (gh && /github/i.test(gh[1])) {
|
|
9
|
-
return {
|
|
10
|
-
provider: "github",
|
|
11
|
-
host: gh[1],
|
|
12
|
-
owner: gh[2],
|
|
13
|
-
repo: gh[3],
|
|
14
|
-
number: Number(gh[4]),
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
const gl = url.match(/^https?:\/\/([^/]+)\/(.+)\/([^/]+)\/-\/merge_requests\/(\d+)(?:[/?#]|$)/);
|
|
18
|
-
if (gl) {
|
|
19
|
-
return {
|
|
20
|
-
provider: "gitlab",
|
|
21
|
-
host: gl[1],
|
|
22
|
-
owner: gl[2],
|
|
23
|
-
repo: gl[3],
|
|
24
|
-
number: Number(gl[4]),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
package/dist/security.js
DELETED
|
@@ -1,417 +0,0 @@
|
|
|
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.SECURITY_RUNTIME_VERSION = void 0;
|
|
37
|
-
exports.withEndpointIdentity = withEndpointIdentity;
|
|
38
|
-
exports.evaluatePromptSecurity = evaluatePromptSecurity;
|
|
39
|
-
exports.evaluateToolSecurity = evaluateToolSecurity;
|
|
40
|
-
exports.securityInputHasSensitivePath = securityInputHasSensitivePath;
|
|
41
|
-
exports.buildSecurityWorkflowEvent = buildSecurityWorkflowEvent;
|
|
42
|
-
exports.buildSecurityPromptEvent = buildSecurityPromptEvent;
|
|
43
|
-
exports.buildBlockedSecurityPromptEvent = buildBlockedSecurityPromptEvent;
|
|
44
|
-
const crypto = __importStar(require("crypto"));
|
|
45
|
-
exports.SECURITY_RUNTIME_VERSION = 1;
|
|
46
|
-
function withEndpointIdentity(event, identity) {
|
|
47
|
-
return {
|
|
48
|
-
...event,
|
|
49
|
-
...(identity.endpointId ? { endpointId: identity.endpointId.slice(0, 240) } : {}),
|
|
50
|
-
...(identity.endpointName ? { endpointName: identity.endpointName.slice(0, 240) } : {}),
|
|
51
|
-
...(identity.cliVersion ? { cliVersion: identity.cliVersion.slice(0, 80) } : {}),
|
|
52
|
-
...(typeof identity.securityRuntimeVersion === "number"
|
|
53
|
-
&& Number.isInteger(identity.securityRuntimeVersion)
|
|
54
|
-
&& identity.securityRuntimeVersion > 0
|
|
55
|
-
? { securityRuntimeVersion: identity.securityRuntimeVersion }
|
|
56
|
-
: {}),
|
|
57
|
-
...(typeof identity.securityPolicyVersion === "number"
|
|
58
|
-
&& Number.isInteger(identity.securityPolicyVersion)
|
|
59
|
-
&& identity.securityPolicyVersion > 0
|
|
60
|
-
? { securityPolicyVersion: identity.securityPolicyVersion }
|
|
61
|
-
: {}),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
function scopeMatches(value, candidate) {
|
|
65
|
-
const escaped = value.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
66
|
-
try {
|
|
67
|
-
return new RegExp(`^${escaped}$`, "i").test(candidate);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
function effectivePolicyAction(policy, category, fallback, source, input) {
|
|
74
|
-
if (!policy)
|
|
75
|
-
return fallback;
|
|
76
|
-
const toolName = input?.tool_name ?? input?.toolName ?? "";
|
|
77
|
-
const cwd = input?.cwd ?? "";
|
|
78
|
-
const excepted = policy.exceptions.some((exception) => {
|
|
79
|
-
if (exception.scope === "company" || exception.scope === "user")
|
|
80
|
-
return true;
|
|
81
|
-
if (exception.scope === "category")
|
|
82
|
-
return scopeMatches(exception.scopeValue, category);
|
|
83
|
-
if (exception.scope === "source")
|
|
84
|
-
return !!source && scopeMatches(exception.scopeValue, source);
|
|
85
|
-
if (exception.scope === "tool")
|
|
86
|
-
return !!toolName && scopeMatches(exception.scopeValue, toolName);
|
|
87
|
-
if (exception.scope === "repository")
|
|
88
|
-
return !!cwd && scopeMatches(exception.scopeValue, cwd);
|
|
89
|
-
return false;
|
|
90
|
-
});
|
|
91
|
-
if (excepted)
|
|
92
|
-
return "allow";
|
|
93
|
-
const configured = policy.rules[category] ?? fallback;
|
|
94
|
-
return policy.enforcementMode === "alert" && configured === "block" ? "alert" : configured;
|
|
95
|
-
}
|
|
96
|
-
function policyActionPrefix(action) {
|
|
97
|
-
if (action === "block")
|
|
98
|
-
return "Codie blocked";
|
|
99
|
-
if (action === "alert")
|
|
100
|
-
return "Codie detected";
|
|
101
|
-
return "Codie detected but allowed";
|
|
102
|
-
}
|
|
103
|
-
const SECRET_KEY = /(?:api[_-]?key|access[_-]?token|auth[_-]?token|password|passwd|secret|private[_-]?key|client[_-]?secret)/i;
|
|
104
|
-
const SECRET_VALUE_PATTERNS = [
|
|
105
|
-
/\b(?:sk|rk|pk)-(?:live|test|proj)?[-_A-Za-z0-9]{16,}\b/g,
|
|
106
|
-
/\bgh[opusr]_[A-Za-z0-9]{20,}\b/g,
|
|
107
|
-
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
108
|
-
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
|
|
109
|
-
/\bBearer\s+[A-Za-z0-9._~+\/-]{16,}=*\b/gi,
|
|
110
|
-
];
|
|
111
|
-
const SECRET_ASSIGNMENT = /\b([A-Z][A-Z0-9_]*(?:API_KEY|ACCESS_KEY|TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE_KEY|CLIENT_SECRET|DATABASE_URL))\s*[:=]\s*["']?([^\s"'`]{8,})/gi;
|
|
112
|
-
const SENSITIVE_PATH = /(?:~\/\.ssh\/(?:id_[a-z0-9_-]+)|(?:^|\/)\.env(?:\.[a-z0-9_-]+)?|(?:^|\/)\.aws\/credentials|(?:^|\/)\.config\/gcloud\/application_default_credentials\.json)(?=$|[\/\s"'])/i;
|
|
113
|
-
const PROTECTED_AGENT_CONFIG = /(?:^|[\/\s"'])\.(?:claude\/(?:settings(?:\.local)?\.json|hooks)|codex\/(?:config\.toml|hooks\.json)|cursor\/(?:mcp\.json|rules)|config\/opencode\/(?:opencode\.json|plugins))(?=$|[\s"'\/])/i;
|
|
114
|
-
function looksLikePlaceholder(value) {
|
|
115
|
-
return (/^\$\{?[A-Z][A-Z0-9_]*\}?$/.test(value) ||
|
|
116
|
-
/^(?:your|example|sample|dummy|fake|replace|redacted|changeme)[-_\[]/i.test(value) ||
|
|
117
|
-
/^\[(?:redacted|secret|value)\]$/i.test(value) ||
|
|
118
|
-
/^<[^>]+>$/.test(value));
|
|
119
|
-
}
|
|
120
|
-
function containsSecretValue(text) {
|
|
121
|
-
for (const pattern of SECRET_VALUE_PATTERNS) {
|
|
122
|
-
pattern.lastIndex = 0;
|
|
123
|
-
if (pattern.test(text)) {
|
|
124
|
-
pattern.lastIndex = 0;
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
pattern.lastIndex = 0;
|
|
128
|
-
}
|
|
129
|
-
SECRET_ASSIGNMENT.lastIndex = 0;
|
|
130
|
-
let match;
|
|
131
|
-
while ((match = SECRET_ASSIGNMENT.exec(text)) !== null) {
|
|
132
|
-
if (!looksLikePlaceholder(match[2])) {
|
|
133
|
-
SECRET_ASSIGNMENT.lastIndex = 0;
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
SECRET_ASSIGNMENT.lastIndex = 0;
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
function evaluatePromptSecurity(prompt, policy, source) {
|
|
141
|
-
if (!prompt || !containsSecretValue(prompt))
|
|
142
|
-
return { action: "allow" };
|
|
143
|
-
const action = effectivePolicyAction(policy, "secret_in_prompt", "block", source);
|
|
144
|
-
return {
|
|
145
|
-
action,
|
|
146
|
-
category: "secret_in_prompt",
|
|
147
|
-
reason: policyActionPrefix(action) + " this prompt because it appears to contain an API key, password, or credential. Remove the secret value and reference the environment variable by name instead (for example, $OPENAI_API_KEY).",
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function serializedToolInput(input) {
|
|
151
|
-
const value = input.tool_input ?? input.toolInput;
|
|
152
|
-
if (typeof value === "string")
|
|
153
|
-
return value;
|
|
154
|
-
try {
|
|
155
|
-
return JSON.stringify(value ?? "");
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
return "";
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
function toolCommand(input) {
|
|
162
|
-
const value = input.tool_input ?? input.toolInput;
|
|
163
|
-
if (value && typeof value === "object" && typeof value.command === "string") {
|
|
164
|
-
return value.command;
|
|
165
|
-
}
|
|
166
|
-
return typeof value === "string" ? value : "";
|
|
167
|
-
}
|
|
168
|
-
function isWriteOperation(input, serialized) {
|
|
169
|
-
const tool = String(input.tool_name ?? input.toolName ?? "").toLowerCase();
|
|
170
|
-
return /(?:^|[_-])(?:write|edit|patch|move|rename)(?:$|[_-])/.test(tool) ||
|
|
171
|
-
(/\b(?:tee|cp|mv|install)\b|(?:^|[^>])>{1,2}(?:[^>]|$)/.test(serialized) && /(?:bash|shell|terminal|exec)/.test(tool));
|
|
172
|
-
}
|
|
173
|
-
function networkHosts(text) {
|
|
174
|
-
const hosts = [];
|
|
175
|
-
for (const match of text.matchAll(/https?:\/\/([^\s/"'`]+)/gi))
|
|
176
|
-
hosts.push(match[1].replace(/:\d+$/, "").toLowerCase());
|
|
177
|
-
return [...new Set(hosts)];
|
|
178
|
-
}
|
|
179
|
-
function allowedByPatterns(candidate, patterns) {
|
|
180
|
-
return Boolean(patterns?.some((pattern) => scopeMatches(pattern, candidate)));
|
|
181
|
-
}
|
|
182
|
-
function mcpServerName(input) {
|
|
183
|
-
const tool = String(input.tool_name ?? input.toolName ?? "");
|
|
184
|
-
return tool.match(/^mcp__(.+?)__/)?.[1] ?? tool.match(/^mcp[.:/]([^.:/]+)/i)?.[1] ?? null;
|
|
185
|
-
}
|
|
186
|
-
function rootCommand(command) {
|
|
187
|
-
const cleaned = command.trim().replace(/^(?:sudo\s+|env\s+(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*)/, "");
|
|
188
|
-
return cleaned.match(/^([A-Za-z0-9_./+-]+)/)?.[1]?.split("/").pop() ?? null;
|
|
189
|
-
}
|
|
190
|
-
function destructiveTargetOutsideWorkspace(command, cwd) {
|
|
191
|
-
const match = command.match(/\brm\s+(?:-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*r[a-zA-Z]*)\s+(?:--\s+)?([^\s;&|]+)/);
|
|
192
|
-
if (!match)
|
|
193
|
-
return false;
|
|
194
|
-
const target = match[1].replace(/^['"]|['"]$/g, "");
|
|
195
|
-
if (["/", "~", "$HOME", "${HOME}", "..", "../"].includes(target))
|
|
196
|
-
return true;
|
|
197
|
-
if (target.startsWith("~/") || target.startsWith("../"))
|
|
198
|
-
return true;
|
|
199
|
-
if (!target.startsWith("/") || !cwd)
|
|
200
|
-
return false;
|
|
201
|
-
const normalizedCwd = cwd.replace(/\/+$/, "");
|
|
202
|
-
return target !== normalizedCwd && !target.startsWith(`${normalizedCwd}/`);
|
|
203
|
-
}
|
|
204
|
-
function evaluateToolSecurity(input, phase, policy, source, sessionContext) {
|
|
205
|
-
if (phase !== "pre")
|
|
206
|
-
return { action: "allow" };
|
|
207
|
-
const serialized = serializedToolInput(input);
|
|
208
|
-
if (!serialized)
|
|
209
|
-
return { action: "allow" };
|
|
210
|
-
if (destructiveTargetOutsideWorkspace(serialized, input.cwd)) {
|
|
211
|
-
const action = effectivePolicyAction(policy, "destructive_outside_workspace", "block", source, input);
|
|
212
|
-
return {
|
|
213
|
-
action,
|
|
214
|
-
category: "destructive_outside_workspace",
|
|
215
|
-
reason: policyActionPrefix(action) + " a destructive command targeting files outside the approved workspace.",
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
const hasNetworkEgress = /\b(?:curl|wget)\b|https?:\/\//i.test(serialized);
|
|
219
|
-
if (hasNetworkEgress && containsSecretValue(serialized)) {
|
|
220
|
-
const action = effectivePolicyAction(policy, "secret_exfiltration", "block", source, input);
|
|
221
|
-
return {
|
|
222
|
-
action,
|
|
223
|
-
category: "secret_exfiltration",
|
|
224
|
-
reason: policyActionPrefix(action) + " a command that would send credential material to an external destination.",
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
if (hasNetworkEgress &&
|
|
228
|
-
sessionContext?.sensitiveReadAt &&
|
|
229
|
-
(sessionContext.now ?? Date.now()) - sessionContext.sensitiveReadAt <= 5 * 60_000) {
|
|
230
|
-
return {
|
|
231
|
-
action: effectivePolicyAction(policy, "sensitive_read_then_egress", "alert", source, input),
|
|
232
|
-
category: "sensitive_read_then_egress",
|
|
233
|
-
reason: "Codie observed network egress shortly after this session read a credential-bearing file.",
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
const writeOperation = isWriteOperation(input, serialized);
|
|
237
|
-
if (writeOperation && SENSITIVE_PATH.test(serialized)) {
|
|
238
|
-
const action = effectivePolicyAction(policy, "sensitive_file_write", "block", source, input);
|
|
239
|
-
return {
|
|
240
|
-
action,
|
|
241
|
-
category: "sensitive_file_write",
|
|
242
|
-
reason: policyActionPrefix(action) + " a write to a credential-bearing file.",
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
if (writeOperation && PROTECTED_AGENT_CONFIG.test(serialized)) {
|
|
246
|
-
const action = effectivePolicyAction(policy, "protected_agent_config_write", "block", source, input);
|
|
247
|
-
return {
|
|
248
|
-
action,
|
|
249
|
-
category: "protected_agent_config_write",
|
|
250
|
-
reason: policyActionPrefix(action) + " an agent modifying its own hooks, policy, MCP, or instruction configuration.",
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
const server = mcpServerName(input);
|
|
254
|
-
if (server && policy?.constraints?.allowedMcpServers?.length && !allowedByPatterns(server, policy.constraints.allowedMcpServers)) {
|
|
255
|
-
return {
|
|
256
|
-
action: effectivePolicyAction(policy, "untrusted_mcp_tool", "alert", source, input),
|
|
257
|
-
category: "untrusted_mcp_tool",
|
|
258
|
-
reason: `Codie observed a tool call from MCP server “${server}”, which is not in the approved component list.`,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
const hosts = networkHosts(serialized);
|
|
262
|
-
const unapprovedHost = hosts.find((host) => policy?.constraints?.allowedHosts?.length && !allowedByPatterns(host, policy.constraints.allowedHosts));
|
|
263
|
-
if (unapprovedHost) {
|
|
264
|
-
return {
|
|
265
|
-
action: effectivePolicyAction(policy, "unapproved_network_egress", "alert", source, input),
|
|
266
|
-
category: "unapproved_network_egress",
|
|
267
|
-
reason: `Codie observed network egress to unapproved host “${unapprovedHost}”.`,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
const command = toolCommand(input);
|
|
271
|
-
if (/\b(?:base64\s+(?:-[a-z]*d|--decode)|openssl\s+enc\s+-d)[^|;&]*(?:\||&&)\s*(?:sh|bash|zsh)\b|\beval\s+["']?\$\(/i.test(command)) {
|
|
272
|
-
return {
|
|
273
|
-
action: effectivePolicyAction(policy, "obfuscated_shell_command", "alert", source, input),
|
|
274
|
-
category: "obfuscated_shell_command",
|
|
275
|
-
reason: "Codie observed an encoded or dynamically evaluated shell payload.",
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
if (/\b(?:npm|pnpm|yarn|pipx?|uv|gem|cargo)\s+(?:add|install|i)\b/i.test(command)) {
|
|
279
|
-
return {
|
|
280
|
-
action: effectivePolicyAction(policy, "package_install", "alert", source, input),
|
|
281
|
-
category: "package_install",
|
|
282
|
-
reason: "Codie observed a package installation. Verify the package source and pinned version.",
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
const commandName = rootCommand(command);
|
|
286
|
-
if (commandName && policy?.constraints?.allowedCommands?.length && !allowedByPatterns(commandName, policy.constraints.allowedCommands)) {
|
|
287
|
-
return {
|
|
288
|
-
action: effectivePolicyAction(policy, "unapproved_command", "alert", source, input),
|
|
289
|
-
category: "unapproved_command",
|
|
290
|
-
reason: `Codie observed command “${commandName}”, which is not in the approved command list.`,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
if (SENSITIVE_PATH.test(serialized)) {
|
|
294
|
-
return {
|
|
295
|
-
action: effectivePolicyAction(policy, "sensitive_file_read", "alert", source, input),
|
|
296
|
-
category: "sensitive_file_read",
|
|
297
|
-
reason: "Codie observed access to a sensitive credential file. This policy is alert-only.",
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
return { action: "allow" };
|
|
301
|
-
}
|
|
302
|
-
function securityInputHasSensitivePath(input) {
|
|
303
|
-
return SENSITIVE_PATH.test(serializedToolInput(input));
|
|
304
|
-
}
|
|
305
|
-
function fingerprint(value) {
|
|
306
|
-
return crypto.createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
307
|
-
}
|
|
308
|
-
function redactString(value) {
|
|
309
|
-
let count = 0;
|
|
310
|
-
let redacted = value;
|
|
311
|
-
for (const pattern of SECRET_VALUE_PATTERNS) {
|
|
312
|
-
redacted = redacted.replace(pattern, (match) => {
|
|
313
|
-
count += 1;
|
|
314
|
-
return `[REDACTED_SECRET:${fingerprint(match)}]`;
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
if (redacted.length > 16_384)
|
|
318
|
-
redacted = `${redacted.slice(0, 16_384)}[TRUNCATED]`;
|
|
319
|
-
return { value: redacted, count };
|
|
320
|
-
}
|
|
321
|
-
function redact(value, key = "", depth = 0) {
|
|
322
|
-
if (depth > 8)
|
|
323
|
-
return { value: "[TRUNCATED_DEPTH]", count: 0 };
|
|
324
|
-
if (SECRET_KEY.test(key) && value !== null && value !== undefined) {
|
|
325
|
-
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
326
|
-
return { value: `[REDACTED_SECRET:${fingerprint(text)}]`, count: 1 };
|
|
327
|
-
}
|
|
328
|
-
if (typeof value === "string")
|
|
329
|
-
return redactString(value);
|
|
330
|
-
if (Array.isArray(value)) {
|
|
331
|
-
let count = 0;
|
|
332
|
-
const next = value.slice(0, 200).map((item) => {
|
|
333
|
-
const result = redact(item, key, depth + 1);
|
|
334
|
-
count += result.count;
|
|
335
|
-
return result.value;
|
|
336
|
-
});
|
|
337
|
-
return { value: next, count };
|
|
338
|
-
}
|
|
339
|
-
if (value && typeof value === "object") {
|
|
340
|
-
let count = 0;
|
|
341
|
-
const next = {};
|
|
342
|
-
for (const [childKey, childValue] of Object.entries(value).slice(0, 200)) {
|
|
343
|
-
const result = redact(childValue, childKey, depth + 1);
|
|
344
|
-
count += result.count;
|
|
345
|
-
next[childKey] = result.value;
|
|
346
|
-
}
|
|
347
|
-
return { value: next, count };
|
|
348
|
-
}
|
|
349
|
-
return { value, count: 0 };
|
|
350
|
-
}
|
|
351
|
-
function buildSecurityWorkflowEvent(input, phase, source, now = Date.now) {
|
|
352
|
-
const sessionId = input.session_id ?? input.sessionId ?? "";
|
|
353
|
-
const toolName = input.tool_name ?? input.toolName ?? "";
|
|
354
|
-
if (!sessionId || !toolName)
|
|
355
|
-
return null;
|
|
356
|
-
const rawPayload = {
|
|
357
|
-
input: input.tool_input ?? input.toolInput,
|
|
358
|
-
...(phase === "post" ? { response: input.tool_response ?? input.toolResponse } : {}),
|
|
359
|
-
...(phase === "failure" ? { error: input.error ?? input.tool_response ?? input.toolResponse } : {}),
|
|
360
|
-
};
|
|
361
|
-
const sanitized = redact(rawPayload);
|
|
362
|
-
const duration = input.duration_ms ?? input.durationMs;
|
|
363
|
-
return {
|
|
364
|
-
schemaVersion: 1,
|
|
365
|
-
source,
|
|
366
|
-
phase,
|
|
367
|
-
sessionId,
|
|
368
|
-
toolName,
|
|
369
|
-
toolUseId: input.tool_use_id ?? input.toolUseId,
|
|
370
|
-
cwd: input.cwd,
|
|
371
|
-
observedAt: now(),
|
|
372
|
-
toolDurationMs: typeof duration === "number" && Number.isFinite(duration) ? duration : undefined,
|
|
373
|
-
payload: sanitized.value,
|
|
374
|
-
redactionCount: sanitized.count,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
function buildSecurityPromptEvent(input, source, now = Date.now, policy) {
|
|
378
|
-
const sessionId = input.session_id ?? input.sessionId ?? "";
|
|
379
|
-
if (!sessionId || !input.prompt) {
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
const decision = evaluatePromptSecurity(input.prompt, policy, source);
|
|
383
|
-
if (decision.action === "block")
|
|
384
|
-
return null;
|
|
385
|
-
const sanitized = redact({ prompt: input.prompt });
|
|
386
|
-
return {
|
|
387
|
-
schemaVersion: 1,
|
|
388
|
-
source,
|
|
389
|
-
phase: "pre",
|
|
390
|
-
sessionId,
|
|
391
|
-
toolName: "UserPrompt",
|
|
392
|
-
cwd: input.cwd,
|
|
393
|
-
observedAt: now(),
|
|
394
|
-
payload: sanitized.value,
|
|
395
|
-
redactionCount: sanitized.count,
|
|
396
|
-
localDecision: decision.action === "alert" ? "alert" : undefined,
|
|
397
|
-
localCategory: decision.action === "alert" ? decision.category : undefined,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
function buildBlockedSecurityPromptEvent(input, source, category, now = Date.now) {
|
|
401
|
-
const sessionId = input.session_id ?? input.sessionId ?? "";
|
|
402
|
-
if (!sessionId)
|
|
403
|
-
return null;
|
|
404
|
-
return {
|
|
405
|
-
schemaVersion: 1,
|
|
406
|
-
source,
|
|
407
|
-
phase: "pre",
|
|
408
|
-
sessionId,
|
|
409
|
-
toolName: "UserPrompt",
|
|
410
|
-
cwd: input.cwd,
|
|
411
|
-
observedAt: now(),
|
|
412
|
-
payload: { blocked: true, content: "[REDACTED_BLOCKED_PROMPT]" },
|
|
413
|
-
redactionCount: 1,
|
|
414
|
-
localDecision: "block",
|
|
415
|
-
localCategory: category,
|
|
416
|
-
};
|
|
417
|
-
}
|