fullcourtdefense-cli 1.34.25 → 1.34.26
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/actionPolicyEngine.d.ts +13 -0
- package/dist/actionPolicyEngine.js +306 -16
- package/dist/approvalPrompt.d.ts +2 -0
- package/dist/approvalPrompt.js +1 -1
- package/dist/askDialog.d.ts +14 -1
- package/dist/askDialog.js +9 -7
- package/dist/commands/deterministicGuard.js +10 -3
- package/dist/commands/grants.d.ts +23 -0
- package/dist/commands/grants.js +79 -0
- package/dist/commands/hook.js +21 -8
- package/dist/commands/mcpGateway.js +32 -17
- package/dist/detectorBaseline.d.ts +2 -2
- package/dist/detectorBaseline.js +2 -2
- package/dist/devConfirm.d.ts +102 -5
- package/dist/devConfirm.js +135 -13
- package/dist/index.js +18 -0
- package/dist/machineActionVerify.d.ts +7 -0
- package/dist/machineActionVerify.js +7 -1
- package/dist/runtimeConfig.d.ts +22 -0
- package/dist/runtimeConfig.js +39 -0
- package/dist/telemetry.d.ts +4 -1
- package/dist/telemetry.js +9 -4
- package/dist/version.json +1 -1
- package/package.json +2 -1
|
@@ -29,13 +29,26 @@ export interface ActionPolicyApproval {
|
|
|
29
29
|
scope?: ActionPolicyApprovalScope;
|
|
30
30
|
remember?: ActionPolicyApprovalRemember;
|
|
31
31
|
onTimeout?: ActionPolicyApprovalOnTimeout;
|
|
32
|
+
/**
|
|
33
|
+
* Caps on a developer's "Always allow" under this rule: lifetime in days
|
|
34
|
+
* (1–30) and uses before the dialog returns (1–10000). Absent = the built-in
|
|
35
|
+
* 30-day lifetime, unlimited uses. The org ceiling delivered with the bundle
|
|
36
|
+
* can only tighten these, never widen them.
|
|
37
|
+
*/
|
|
38
|
+
maxDays?: number;
|
|
39
|
+
maxUses?: number;
|
|
32
40
|
}
|
|
33
41
|
/** Fully-resolved approval options (every field present) attached to a `require_approval` result. */
|
|
34
42
|
export interface ResolvedActionPolicyApproval {
|
|
35
43
|
scope: ActionPolicyApprovalScope;
|
|
36
44
|
remember: ActionPolicyApprovalRemember;
|
|
37
45
|
onTimeout: ActionPolicyApprovalOnTimeout;
|
|
46
|
+
/** "Always allow" caps (see `ActionPolicyApproval`); undefined = built-in default. */
|
|
47
|
+
maxDays?: number;
|
|
48
|
+
maxUses?: number;
|
|
38
49
|
}
|
|
50
|
+
export declare const ACTION_POLICY_GRANT_MAX_DAYS = 30;
|
|
51
|
+
export declare const ACTION_POLICY_GRANT_MAX_USES = 10000;
|
|
39
52
|
export interface ActionPolicyRule {
|
|
40
53
|
/** Explicit, narrowly scoped permission to relax named role actions only. */
|
|
41
54
|
roleException?: {
|
|
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/actionPolicyEngine.ts
|
|
21
21
|
var actionPolicyEngine_exports = {};
|
|
22
22
|
__export(actionPolicyEngine_exports, {
|
|
23
|
+
ACTION_POLICY_GRANT_MAX_DAYS: () => ACTION_POLICY_GRANT_MAX_DAYS,
|
|
24
|
+
ACTION_POLICY_GRANT_MAX_USES: () => ACTION_POLICY_GRANT_MAX_USES,
|
|
23
25
|
CONTENT_FACT_FIELDS: () => CONTENT_FACT_FIELDS,
|
|
24
26
|
DEFAULT_ACTION_POLICY_APPROVAL: () => DEFAULT_ACTION_POLICY_APPROVAL,
|
|
25
27
|
MACHINE_KINDS: () => MACHINE_KINDS,
|
|
@@ -6237,6 +6239,18 @@ function provenPowerShellQueryUrls(command) {
|
|
|
6237
6239
|
return [];
|
|
6238
6240
|
}
|
|
6239
6241
|
}
|
|
6242
|
+
var ACTION_POLICY_GRANT_MAX_DAYS = 30;
|
|
6243
|
+
var ACTION_POLICY_GRANT_MAX_USES = 1e4;
|
|
6244
|
+
function approvalCap(value, limit) {
|
|
6245
|
+
const n = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : NaN;
|
|
6246
|
+
if (!Number.isFinite(n) || n < 1) return void 0;
|
|
6247
|
+
return Math.min(limit, Math.floor(n));
|
|
6248
|
+
}
|
|
6249
|
+
function minCap(a, b) {
|
|
6250
|
+
if (a === void 0) return b;
|
|
6251
|
+
if (b === void 0) return a;
|
|
6252
|
+
return Math.min(a, b);
|
|
6253
|
+
}
|
|
6240
6254
|
function approvalExceptionError(rule) {
|
|
6241
6255
|
const roleError = roleExceptionError(rule);
|
|
6242
6256
|
if (roleError) return roleError;
|
|
@@ -6314,17 +6328,25 @@ function resolveApprovalOptions(approval) {
|
|
|
6314
6328
|
const scope = approval?.scope;
|
|
6315
6329
|
const remember = approval?.remember;
|
|
6316
6330
|
const onTimeout = approval?.onTimeout;
|
|
6331
|
+
const maxDays = approvalCap(approval?.maxDays, ACTION_POLICY_GRANT_MAX_DAYS);
|
|
6332
|
+
const maxUses = approvalCap(approval?.maxUses, ACTION_POLICY_GRANT_MAX_USES);
|
|
6317
6333
|
return {
|
|
6318
6334
|
scope: scope === "developer" || scope === "org" ? scope : DEFAULT_ACTION_POLICY_APPROVAL.scope,
|
|
6319
6335
|
remember: remember === "session" || remember === "1h" || remember === "none" ? remember : DEFAULT_ACTION_POLICY_APPROVAL.remember,
|
|
6320
|
-
onTimeout: onTimeout === "org" || onTimeout === "block" ? onTimeout : DEFAULT_ACTION_POLICY_APPROVAL.onTimeout
|
|
6336
|
+
onTimeout: onTimeout === "org" || onTimeout === "block" ? onTimeout : DEFAULT_ACTION_POLICY_APPROVAL.onTimeout,
|
|
6337
|
+
...maxDays !== void 0 ? { maxDays } : {},
|
|
6338
|
+
...maxUses !== void 0 ? { maxUses } : {}
|
|
6321
6339
|
};
|
|
6322
6340
|
}
|
|
6323
6341
|
function mergeApprovalOptions(a, b) {
|
|
6342
|
+
const maxDays = minCap(a.maxDays, b.maxDays);
|
|
6343
|
+
const maxUses = minCap(a.maxUses, b.maxUses);
|
|
6324
6344
|
return {
|
|
6325
6345
|
scope: APPROVAL_SCOPE_RANK[b.scope] > APPROVAL_SCOPE_RANK[a.scope] ? b.scope : a.scope,
|
|
6326
6346
|
remember: APPROVAL_REMEMBER_RANK[b.remember] > APPROVAL_REMEMBER_RANK[a.remember] ? b.remember : a.remember,
|
|
6327
|
-
onTimeout: APPROVAL_ON_TIMEOUT_RANK[b.onTimeout] > APPROVAL_ON_TIMEOUT_RANK[a.onTimeout] ? b.onTimeout : a.onTimeout
|
|
6347
|
+
onTimeout: APPROVAL_ON_TIMEOUT_RANK[b.onTimeout] > APPROVAL_ON_TIMEOUT_RANK[a.onTimeout] ? b.onTimeout : a.onTimeout,
|
|
6348
|
+
...maxDays !== void 0 ? { maxDays } : {},
|
|
6349
|
+
...maxUses !== void 0 ? { maxUses } : {}
|
|
6328
6350
|
};
|
|
6329
6351
|
}
|
|
6330
6352
|
function effectiveApprovalScope(approval, presence) {
|
|
@@ -6790,14 +6812,24 @@ var COMMAND_LINE_FIELDS = /* @__PURE__ */ new Set(["command", "cmd", "commandLin
|
|
|
6790
6812
|
var CODE_EXEC_TOOL_NAME_RE = /\b(node ?repl|repl|shell|exec|execute|terminal|command|cmd|run ?code|bash|zsh|sh|powershell|pwsh)\b/i;
|
|
6791
6813
|
function shellVerbWords(command) {
|
|
6792
6814
|
let lead = "";
|
|
6815
|
+
let first = true;
|
|
6816
|
+
let scriptArgs = false;
|
|
6793
6817
|
return shellCommandWords(command).split(/\s+/).filter(Boolean).map((token) => {
|
|
6794
6818
|
if (token === ";") {
|
|
6795
6819
|
lead = "";
|
|
6820
|
+
first = true;
|
|
6821
|
+
scriptArgs = false;
|
|
6796
6822
|
return token;
|
|
6797
6823
|
}
|
|
6824
|
+
if (scriptArgs) return "";
|
|
6798
6825
|
const flag = /^(-{1,2})([A-Za-z][A-Za-z0-9_-]*)(?:=.*)?$/.exec(token);
|
|
6799
6826
|
const word = flag ? flag[2] : /^\.?[A-Za-z][A-Za-z0-9_-]*$/.test(token) ? token : "";
|
|
6800
|
-
|
|
6827
|
+
const leading = first && !flag;
|
|
6828
|
+
if (leading) first = false;
|
|
6829
|
+
if (!word) {
|
|
6830
|
+
if (leading ? isScriptPath(token, true) : !flag && SCRIPT_RUNNER_LEADS.has(lead) && isScriptPath(token, false)) scriptArgs = true;
|
|
6831
|
+
return "";
|
|
6832
|
+
}
|
|
6801
6833
|
if (!flag && !lead) lead = word.toLowerCase();
|
|
6802
6834
|
if (flag) {
|
|
6803
6835
|
if (VCS_PROGRAMS.has(lead)) return "";
|
|
@@ -6812,6 +6844,50 @@ function shellVerbWords(command) {
|
|
|
6812
6844
|
}).filter(Boolean).join(" ");
|
|
6813
6845
|
}
|
|
6814
6846
|
var VCS_PROGRAMS = /* @__PURE__ */ new Set(["git", "gh", "hg", "svn", "glab"]);
|
|
6847
|
+
var SCRIPT_RUNNER_LEADS = /* @__PURE__ */ new Set([
|
|
6848
|
+
"bash",
|
|
6849
|
+
"sh",
|
|
6850
|
+
"zsh",
|
|
6851
|
+
"dash",
|
|
6852
|
+
"ksh",
|
|
6853
|
+
"fish",
|
|
6854
|
+
"cmd",
|
|
6855
|
+
"powershell",
|
|
6856
|
+
"pwsh",
|
|
6857
|
+
"sudo",
|
|
6858
|
+
"doas",
|
|
6859
|
+
"env",
|
|
6860
|
+
"nohup",
|
|
6861
|
+
"time",
|
|
6862
|
+
"timeout",
|
|
6863
|
+
"watch",
|
|
6864
|
+
"ssh",
|
|
6865
|
+
"xargs",
|
|
6866
|
+
"node",
|
|
6867
|
+
"nodejs",
|
|
6868
|
+
"deno",
|
|
6869
|
+
"bun",
|
|
6870
|
+
"python",
|
|
6871
|
+
"python3",
|
|
6872
|
+
"python2",
|
|
6873
|
+
"py",
|
|
6874
|
+
"perl",
|
|
6875
|
+
"ruby",
|
|
6876
|
+
"php",
|
|
6877
|
+
"lua",
|
|
6878
|
+
"rscript",
|
|
6879
|
+
"julia",
|
|
6880
|
+
"tsx",
|
|
6881
|
+
"ts-node",
|
|
6882
|
+
"npx"
|
|
6883
|
+
]);
|
|
6884
|
+
var SCRIPT_EXTENSION_RE = /\.(?:ps1|psm1|sh|bash|zsh|ksh|fish|py|pyw|js|mjs|cjs|ts|mts|cts|rb|pl|php|lua|r|jl|cmd|bat)$/i;
|
|
6885
|
+
function isScriptPath(token, leading) {
|
|
6886
|
+
const t = token.replace(/^["']|["']$/g, "");
|
|
6887
|
+
if (!t || /^[$%<>|&]/.test(t)) return false;
|
|
6888
|
+
if (SCRIPT_EXTENSION_RE.test(t)) return true;
|
|
6889
|
+
return leading ? /^\.{1,2}[\\/]/.test(t) : /[\\/]/.test(t);
|
|
6890
|
+
}
|
|
6815
6891
|
var DOT_COMMAND_PROGRAMS = /* @__PURE__ */ new Set(["sqlite3", "sqlite", "litecli", "duckdb"]);
|
|
6816
6892
|
function stringifyArgs(args) {
|
|
6817
6893
|
try {
|
|
@@ -6866,13 +6942,14 @@ function isHighEntropySecretToken(token, allowWordSlug = false) {
|
|
|
6866
6942
|
}
|
|
6867
6943
|
var SECRET_NAME = String.raw`(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|bearer[_-]?token|client[_-]?secret|secret[_-]?key|secret|password|passwd|pwd|credentials?|private[_-]?key|token)`;
|
|
6868
6944
|
var SECRET_ASSIGNMENT_RE = new RegExp(String.raw`(?:^|[^\w])[\w.-]*?${SECRET_NAME}[\\"']*\s*(?:=>|[:=])\s*[\\"']*([^\s\\"',;&|)}\]<>]{8,})`, "gi");
|
|
6869
|
-
var SECRET_FLAG_RE =
|
|
6870
|
-
var SECRET_PLACEHOLDER_RE = /^(?:<.*>|\$\{?[A-Za-z_][\w.]*\}?|%[A-Za-z_]+%|\{\{.*\}\}|\[.*\]|\*{3,}|x{4,}|X{4,}|\.{3,}|-{3,}|_{3,}|(?:your|my|our|the|a|an
|
|
6945
|
+
var SECRET_FLAG_RE = /(?:^|[\s"'`;(\[{,])--?(?:password|passwd|pwd|token|api-?key|secret|client-secret|access-token)(?:=|\s+)[\\"']*([^\s\\"',;&|)}\]<>-][^\s\\"',;&|)}\]<>]{7,})/gi;
|
|
6946
|
+
var SECRET_PLACEHOLDER_RE = /^(?:<.*>|\$\{?[A-Za-z_][\w.]*\}?|%[A-Za-z_]+%|\{\{.*\}\}|\[.*\]|\*{3,}|x{4,}|X{4,}|\.{3,}|-{3,}|_{3,}|(?:your|my|our|the|a|an)(?:[-_.][\w.-]*)?|(?:some|example|sample|dummy|fake|test|mock|placeholder|redacted|masked|hidden|changeme|change_me|replace_?me|todo|tbd|fixme|none|null|nil|undefined|true|false|required|optional|omitted|removed|unset|empty|secret|password|token|xxx+|abc+|123+)[\w.-]*)$/i;
|
|
6871
6947
|
var SECRET_CODE_EXPR_RE = /^(?:[A-Za-z_$][\w$]*(?:[.\[][^\s=]*)+|[A-Za-z_$][\w$]*\(.*\)?|process\.env\.\w+|os\.environ.*|env(?:iron)?\.\w+|new\s+\w+.*|await\s+.*|require\(.*)$/;
|
|
6872
6948
|
function looksLikeSecretValue(raw) {
|
|
6873
6949
|
const value = raw.replace(/[\\"'`]+$/, "");
|
|
6874
6950
|
if (value.length < 8) return false;
|
|
6875
6951
|
if (/^[A-Za-z_][A-Za-z0-9_.-]*\s*[:=]/.test(value)) return false;
|
|
6952
|
+
if (/^--?[A-Za-z]/.test(value)) return false;
|
|
6876
6953
|
if (SECRET_PLACEHOLDER_RE.test(value)) return false;
|
|
6877
6954
|
if (SECRET_CODE_EXPR_RE.test(value)) return false;
|
|
6878
6955
|
if (/^\/(?:[\[(^\\.]|.*\/[gimsuy]*$)/.test(value) || /[\[({]/.test(value)) return false;
|
|
@@ -7077,7 +7154,7 @@ function extractUrl(args, _argsText) {
|
|
|
7077
7154
|
if (typeof raw !== "string") continue;
|
|
7078
7155
|
const shellField = ["command", "cmd", "script", "input"].includes(field);
|
|
7079
7156
|
const value = shellField ? splitShellSegments(shellUrlActionText(raw)).map(withoutUnattributedJavaScriptUrls).join("\n") : raw;
|
|
7080
|
-
const firstUrl = (text) => text.match(/https?:\/\/[^\s"'
|
|
7157
|
+
const firstUrl = (text) => text.match(/https?:\/\/[^\s"'<>`]+/i)?.[0] || text.match(/\b(?:s3|gs|ftp|sftp|smb):\/\/[^\s"'<>`]+/i)?.[0];
|
|
7081
7158
|
const outboundUrls = shellField ? splitShellSegments(value).filter((segment) => /^(?:curl(?:\.exe)?|wget(?:\.exe)?|invoke-restmethod|invoke-webrequest|irm|iwr)$/i.test(shellSegmentLead(segment)) && classifyShellSegment(segment) === "write").map(firstUrl).filter((url) => Boolean(url)) : [];
|
|
7082
7159
|
const externalOutbound = outboundUrls.find((url) => {
|
|
7083
7160
|
try {
|
|
@@ -7086,15 +7163,210 @@ function extractUrl(args, _argsText) {
|
|
|
7086
7163
|
return false;
|
|
7087
7164
|
}
|
|
7088
7165
|
});
|
|
7089
|
-
const found = externalOutbound || outboundUrls[0] || firstUrl(value);
|
|
7166
|
+
const found = externalOutbound || outboundUrls[0] || firstUrl(shellField ? segmentsThatMayReachNetwork(splitShellSegments(value)).join("\n") : value);
|
|
7090
7167
|
if (found) return found;
|
|
7091
7168
|
}
|
|
7092
7169
|
return "";
|
|
7093
7170
|
}
|
|
7171
|
+
var LOCAL_DATA_LEADS = /* @__PURE__ */ new Set([
|
|
7172
|
+
// PowerShell text / file / pipeline cmdlets and their aliases
|
|
7173
|
+
"get-content",
|
|
7174
|
+
"gc",
|
|
7175
|
+
"type",
|
|
7176
|
+
"set-content",
|
|
7177
|
+
"sc",
|
|
7178
|
+
"add-content",
|
|
7179
|
+
"ac",
|
|
7180
|
+
"out-file",
|
|
7181
|
+
"out-string",
|
|
7182
|
+
"out-null",
|
|
7183
|
+
"write-host",
|
|
7184
|
+
"write-output",
|
|
7185
|
+
"write-verbose",
|
|
7186
|
+
"write-warning",
|
|
7187
|
+
"write-error",
|
|
7188
|
+
"write-information",
|
|
7189
|
+
"write-debug",
|
|
7190
|
+
"select-string",
|
|
7191
|
+
"sls",
|
|
7192
|
+
"select-object",
|
|
7193
|
+
"select",
|
|
7194
|
+
"where-object",
|
|
7195
|
+
"where",
|
|
7196
|
+
"foreach-object",
|
|
7197
|
+
"foreach",
|
|
7198
|
+
"%",
|
|
7199
|
+
"?",
|
|
7200
|
+
"sort-object",
|
|
7201
|
+
"sort",
|
|
7202
|
+
"group-object",
|
|
7203
|
+
"group",
|
|
7204
|
+
"measure-object",
|
|
7205
|
+
"measure",
|
|
7206
|
+
"compare-object",
|
|
7207
|
+
"compare",
|
|
7208
|
+
"format-table",
|
|
7209
|
+
"ft",
|
|
7210
|
+
"format-list",
|
|
7211
|
+
"fl",
|
|
7212
|
+
"format-wide",
|
|
7213
|
+
"fw",
|
|
7214
|
+
"convertto-json",
|
|
7215
|
+
"convertfrom-json",
|
|
7216
|
+
"convertto-csv",
|
|
7217
|
+
"convertfrom-csv",
|
|
7218
|
+
"test-path",
|
|
7219
|
+
"get-item",
|
|
7220
|
+
"gi",
|
|
7221
|
+
"get-childitem",
|
|
7222
|
+
"gci",
|
|
7223
|
+
"ls",
|
|
7224
|
+
"dir",
|
|
7225
|
+
"split-path",
|
|
7226
|
+
"join-path",
|
|
7227
|
+
"resolve-path",
|
|
7228
|
+
"new-item",
|
|
7229
|
+
"ni",
|
|
7230
|
+
"copy-item",
|
|
7231
|
+
"cpi",
|
|
7232
|
+
"copy",
|
|
7233
|
+
"move-item",
|
|
7234
|
+
"mi",
|
|
7235
|
+
"move",
|
|
7236
|
+
"rename-item",
|
|
7237
|
+
"rni",
|
|
7238
|
+
"ren",
|
|
7239
|
+
"remove-item",
|
|
7240
|
+
"ri",
|
|
7241
|
+
"del",
|
|
7242
|
+
"erase",
|
|
7243
|
+
"rd",
|
|
7244
|
+
"rmdir",
|
|
7245
|
+
"get-date",
|
|
7246
|
+
"set-location",
|
|
7247
|
+
"cd",
|
|
7248
|
+
"chdir",
|
|
7249
|
+
"push-location",
|
|
7250
|
+
"pushd",
|
|
7251
|
+
"pop-location",
|
|
7252
|
+
"popd",
|
|
7253
|
+
"get-location",
|
|
7254
|
+
"pwd",
|
|
7255
|
+
// POSIX text / file tools
|
|
7256
|
+
"echo",
|
|
7257
|
+
"printf",
|
|
7258
|
+
"cat",
|
|
7259
|
+
"grep",
|
|
7260
|
+
"egrep",
|
|
7261
|
+
"fgrep",
|
|
7262
|
+
"rg",
|
|
7263
|
+
"ag",
|
|
7264
|
+
"cut",
|
|
7265
|
+
"uniq",
|
|
7266
|
+
"head",
|
|
7267
|
+
"tail",
|
|
7268
|
+
"tr",
|
|
7269
|
+
"wc",
|
|
7270
|
+
"test",
|
|
7271
|
+
"mkdir",
|
|
7272
|
+
"touch",
|
|
7273
|
+
"cp",
|
|
7274
|
+
"mv",
|
|
7275
|
+
"rm",
|
|
7276
|
+
"ln",
|
|
7277
|
+
"basename",
|
|
7278
|
+
"dirname",
|
|
7279
|
+
"realpath",
|
|
7280
|
+
"jq",
|
|
7281
|
+
"yq",
|
|
7282
|
+
"diff",
|
|
7283
|
+
"date",
|
|
7284
|
+
"true",
|
|
7285
|
+
"false"
|
|
7286
|
+
]);
|
|
7287
|
+
var GIT_NETWORK_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
7288
|
+
"clone",
|
|
7289
|
+
"fetch",
|
|
7290
|
+
"pull",
|
|
7291
|
+
"push",
|
|
7292
|
+
"remote",
|
|
7293
|
+
"ls-remote",
|
|
7294
|
+
"submodule",
|
|
7295
|
+
"archive",
|
|
7296
|
+
"request-pull",
|
|
7297
|
+
"svn",
|
|
7298
|
+
"daemon",
|
|
7299
|
+
"bundle",
|
|
7300
|
+
"http-fetch",
|
|
7301
|
+
"http-push",
|
|
7302
|
+
"send-pack",
|
|
7303
|
+
"fetch-pack",
|
|
7304
|
+
"upload-pack",
|
|
7305
|
+
"receive-pack",
|
|
7306
|
+
"lfs"
|
|
7307
|
+
]);
|
|
7308
|
+
function segmentMayReachNetwork(segment) {
|
|
7309
|
+
if (/\$\(|`/.test(segment) || PS_VALUE_EXECUTOR_RE.test(segment)) return true;
|
|
7310
|
+
return splitShellSegments(segment, true).some((stage) => {
|
|
7311
|
+
if (/^\s*(?:\$[\w:]+|[A-Za-z_]\w*)\s*=/.test(stage)) return true;
|
|
7312
|
+
const masked = maskQuotedSpans(stage);
|
|
7313
|
+
for (const group of masked.matchAll(/\(\s*([A-Za-z][\w-]*)/g)) {
|
|
7314
|
+
if (!LOCAL_DATA_LEADS.has(group[1].toLowerCase())) return true;
|
|
7315
|
+
}
|
|
7316
|
+
const lead = shellSegmentLead(stage);
|
|
7317
|
+
if (!lead) return false;
|
|
7318
|
+
if (lead === "git") {
|
|
7319
|
+
const subcommand = gitSubcommand(stage);
|
|
7320
|
+
return !subcommand || GIT_NETWORK_SUBCOMMANDS.has(subcommand);
|
|
7321
|
+
}
|
|
7322
|
+
return !LOCAL_DATA_LEADS.has(lead);
|
|
7323
|
+
});
|
|
7324
|
+
}
|
|
7325
|
+
var LITERAL_ASSIGNMENT_RE = /^\s*(?:\$|(?=[A-Za-z_]))([A-Za-z_]\w*)\s*=\s*(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$`]*?\r?\n"@|'(?:[^']|'')*')\s*$/;
|
|
7326
|
+
function segmentsThatMayReachNetwork(segments) {
|
|
7327
|
+
const tempRedefined = segments.some((segment) => /\$env:(?:TEMP|TMP)\s*=/i.test(maskQuotedSpans(segment)));
|
|
7328
|
+
const remotePath = /(?:^|[\s"'=(])[\\/]{2}[^\s\\/"']+[\\/]/;
|
|
7329
|
+
return segments.filter((segment, index) => {
|
|
7330
|
+
if (!LITERAL_ASSIGNMENT_RE.test(segment)) return segmentMayReachNetwork(segment);
|
|
7331
|
+
return segments.slice(index + 1).some((later) => !LITERAL_ASSIGNMENT_RE.test(later) && (remotePath.test(later) || ghMetadataWriteBodyFile(tempRedefined ? later : temporaryFileSyntax(later)) === void 0 && segmentMayReachNetwork(later)));
|
|
7332
|
+
});
|
|
7333
|
+
}
|
|
7334
|
+
function gitSubcommand(stage) {
|
|
7335
|
+
const tokens = maskQuotedSpans(stage).trim().split(/\s+/);
|
|
7336
|
+
let i2 = tokens.findIndex((token) => /^(?:.*[\\/])?git(?:\.exe)?$/i.test(token)) + 1;
|
|
7337
|
+
if (i2 <= 0) return "";
|
|
7338
|
+
while (i2 < tokens.length && tokens[i2].startsWith("-")) {
|
|
7339
|
+
if (/^(?:-c|-C|--git-dir|--work-tree|--namespace|--exec-path|--config-env|--super-prefix|--list-cmds)$/i.test(tokens[i2])) i2++;
|
|
7340
|
+
i2++;
|
|
7341
|
+
}
|
|
7342
|
+
const subcommand = (tokens[i2] || "").toLowerCase();
|
|
7343
|
+
return /^[a-z][\w-]*$/.test(subcommand) ? subcommand : "";
|
|
7344
|
+
}
|
|
7345
|
+
function temporaryFileSyntax(text) {
|
|
7346
|
+
return text.replace(/"\$env:(?:TEMP|TMP)([\\/][A-Za-z0-9_ ./\\-]+)"/gi, '"fcd-temporary$1"').replace(/(?<=^|\s)\$env:(?:TEMP|TMP)([\\/][A-Za-z0-9_./\\-]+)(?=\s|$)/gi, '"fcd-temporary$1"').replace(/\(Join-Path\s+\$env:(?:TEMP|TMP)\s+'([A-Za-z0-9_ ./\\-]+)'\)/gi, '"fcd-temporary/$1"');
|
|
7347
|
+
}
|
|
7348
|
+
var GH_FLAG_VALUE_RE = /^(?:'[^'$`\r\n]*'|"[^"$`\r\n]*"|[^\s'"$`|;&(){}<>]+)$/;
|
|
7349
|
+
function ghMetadataWriteBodyFile(segment) {
|
|
7350
|
+
const ghBody = /^gh\s+(pr\s+create|issue\s+create|pr\s+(?:comment|edit)\s+\d+|issue\s+(?:comment|edit)\s+\d+)\s+(.+?)(?:\s+2>&1)?(?:\s*\|\s*Select-Object\s+-Last\s+\d{1,3})?$/i.exec(segment);
|
|
7351
|
+
if (!ghBody) return void 0;
|
|
7352
|
+
const tokens = ghBody[2].match(/'[^'\r\n]*'|"[^"\r\n]*"|[^\s'"]+/g) || [];
|
|
7353
|
+
if (tokens.join("").replace(/\s/g, "") !== ghBody[2].replace(/\s/g, "")) return void 0;
|
|
7354
|
+
let bodyFile;
|
|
7355
|
+
for (let i2 = 0; i2 < tokens.length; i2 += 2) {
|
|
7356
|
+
const flag = tokens[i2];
|
|
7357
|
+
const value = tokens[i2 + 1];
|
|
7358
|
+
if (!/^--(?:base|head|title|label|assignee|milestone|reviewer|body-file)$/i.test(flag) || !value || !GH_FLAG_VALUE_RE.test(value)) return void 0;
|
|
7359
|
+
if (/^--body-file$/i.test(flag)) {
|
|
7360
|
+
if (bodyFile !== void 0) return void 0;
|
|
7361
|
+
bodyFile = value;
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
return bodyFile;
|
|
7365
|
+
}
|
|
7094
7366
|
function shellUrlActionText(command) {
|
|
7095
7367
|
if (command.length > 65536) return command;
|
|
7096
7368
|
if (/\$env:(?:TEMP|TMP)\s*=/i.test(maskQuotedSpans(command))) return command;
|
|
7097
|
-
const fileSyntax =
|
|
7369
|
+
const fileSyntax = temporaryFileSyntax;
|
|
7098
7370
|
const originalSegments = splitShellSegments(command);
|
|
7099
7371
|
const localPaths = /* @__PURE__ */ new Map();
|
|
7100
7372
|
const literalValues = /* @__PURE__ */ new Map();
|
|
@@ -7111,7 +7383,7 @@ function shellUrlActionText(command) {
|
|
|
7111
7383
|
return value;
|
|
7112
7384
|
};
|
|
7113
7385
|
const segments = originalSegments.map((segment, index) => {
|
|
7114
|
-
const dataAssignment = /^\$([A-Za-z_][\w]*)\s*=\s*(@'\r?\n[\s\S]*?\r?\n'@|'(?:[^']|'')*')$/.exec(segment);
|
|
7386
|
+
const dataAssignment = /^\$([A-Za-z_][\w]*)\s*=\s*(@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$]*?\r?\n"@|'(?:[^']|'')*')$/.exec(segment);
|
|
7115
7387
|
if (dataAssignment && !literalValues.has(dataAssignment[1].toLowerCase()) && literalValues.size < 32) {
|
|
7116
7388
|
literalValues.set(dataAssignment[1].toLowerCase(), dataAssignment[2]);
|
|
7117
7389
|
dataAssignments.add(index);
|
|
@@ -7125,7 +7397,7 @@ function shellUrlActionText(command) {
|
|
|
7125
7397
|
pathAssignments.add(index);
|
|
7126
7398
|
return segment;
|
|
7127
7399
|
}
|
|
7128
|
-
const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[
|
|
7400
|
+
const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$]*?\r?\n"@|'(?:[^']|'')*'|"[^"$`]*")/.exec(segment);
|
|
7129
7401
|
const prefix = literal2?.[0] || "";
|
|
7130
7402
|
const tail = segment.slice(prefix.length).replace(
|
|
7131
7403
|
/"\$([A-Za-z_][\w]*)([\\/][\w ./\\-]*)?"/g,
|
|
@@ -7147,8 +7419,9 @@ function shellUrlActionText(command) {
|
|
|
7147
7419
|
);
|
|
7148
7420
|
});
|
|
7149
7421
|
if (expansionOverflow) return command;
|
|
7150
|
-
const
|
|
7151
|
-
|
|
7422
|
+
const storedTargets = /* @__PURE__ */ new Map();
|
|
7423
|
+
const filtered = segments.map((segment, index) => {
|
|
7424
|
+
const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$]*?\r?\n"@|'(?:[^']|'')*'|"[^"$`]*")/;
|
|
7152
7425
|
const piped = segment.match(literal2);
|
|
7153
7426
|
let tail = piped ? segment.slice(piped[0].length).replace(/^\s*\|\s*/, "") : "";
|
|
7154
7427
|
if (!piped || !/^\s*\|/.test(segment.slice(piped[0].length))) {
|
|
@@ -7176,8 +7449,15 @@ function shellUrlActionText(command) {
|
|
|
7176
7449
|
}
|
|
7177
7450
|
target = target.replace(/^['"]|['"]$/g, "");
|
|
7178
7451
|
if (!target || /^[\\/]{2}|[\r\n*?\[\]<>]/.test(target) || /:/.test(target.replace(/^[A-Za-z]:[\\/]/, ""))) return segment;
|
|
7452
|
+
const key = target.toLowerCase();
|
|
7453
|
+
if (!storedTargets.has(key)) storedTargets.set(key, index);
|
|
7179
7454
|
return tail;
|
|
7180
7455
|
});
|
|
7456
|
+
const isStoredTarget = (token, before) => {
|
|
7457
|
+
if (!token) return false;
|
|
7458
|
+
const written = storedTargets.get(token.replace(/^['"]|['"]$/g, "").toLowerCase());
|
|
7459
|
+
return written !== void 0 && written < before;
|
|
7460
|
+
};
|
|
7181
7461
|
const onlyStoredData = segments.every((segment, index) => {
|
|
7182
7462
|
if (dataAssignments.has(index)) return true;
|
|
7183
7463
|
if (pathAssignments.has(index)) return true;
|
|
@@ -7187,6 +7467,10 @@ function shellUrlActionText(command) {
|
|
|
7187
7467
|
if (/^if\s*\(\$([A-Za-z_][\w]*)\.Count\)\s*\{\s*throw\s+\$\1\[0\]\s*\}$/i.test(segment)) return true;
|
|
7188
7468
|
if (/^Write-Output\s+'(?:[^']|'')*'$/i.test(segment)) return true;
|
|
7189
7469
|
if (/^gh\s+pr\s+checks\s+\d+\s+--json\s+[a-z,]+\s+--jq\s+'(?:[^']|'')*'$/i.test(segment)) return true;
|
|
7470
|
+
const ghBodyFile = ghMetadataWriteBodyFile(segment);
|
|
7471
|
+
if (ghBodyFile !== void 0) return isStoredTarget(ghBodyFile, index);
|
|
7472
|
+
const removal = /^Remove-Item\s+(?:-(?:LiteralPath|Path)\s+)?('[^'\r\n]*'|"[^"\r\n]*"|[^\s'"$`|;&(){}<>]+)(?:\s+-Force)?$/i.exec(segment);
|
|
7473
|
+
if (removal && isStoredTarget(removal[1], index)) return true;
|
|
7190
7474
|
const read = /^Get-Content\s+(?:(?:-LiteralPath|-Path)\s+)?(?:'([^'$`\r\n]*)'|"([^"$`\r\n]*)"|([^\s'"$`|;&(){}<>]+))(?:\s+-(?:Tail|TotalCount)\s+\d{1,6})?(?:\s+-Raw)?\s*$/i.exec(segment);
|
|
7191
7475
|
if (read) {
|
|
7192
7476
|
const target = read[1] ?? read[2] ?? read[3];
|
|
@@ -7508,8 +7792,8 @@ function classifyShellWords(masked, raw, lead) {
|
|
|
7508
7792
|
if (/\b(drop\s+(database|table|schema)|truncate\s+table|kubectl\s+delete|terraform\s+destroy|gcloud\s+(?:sql\s+instances|compute\s+instances|projects|run\s+services)\s+delete|az\s+(?:group|vm|sql\s+server)\s+delete|aws\s+(?:s3\s+rb|rds\s+delete-db-instance|ec2\s+terminate-instances)|docker\s+(?:system\s+prune|volume\s+rm|rm\s+-f)|mkfs|dd\s+if=)\b/.test(value)) {
|
|
7509
7793
|
return "DELETE";
|
|
7510
7794
|
}
|
|
7511
|
-
const xfer = segment.toLowerCase().match(/\b(?:aws\s+s3\s+(?:cp|sync|mv)|gsutil\s+(?:-m\s+)?(?:cp|rsync|mv)|az\s+storage\s+blob\s+upload|scp|rsync|sftp)\s+(.+)$/);
|
|
7512
|
-
if (xfer && /\b(?:aws\s+s3\s+(?:cp|sync|mv)|gsutil\s+(?:-m\s+)?(?:cp|rsync|mv)|az\s+storage\s+blob\s+upload|scp|rsync|sftp)\b/.test(value)) {
|
|
7795
|
+
const xfer = segment.toLowerCase().match(/\b(?:aws\s+s3\s+(?:cp|sync|mv)|gsutil\s+(?:-m\s+)?(?:cp|rsync|mv)|gcloud(?:\.cmd|\.exe)?\s+storage\s+(?:cp|rsync|mv)|az\s+storage\s+blob\s+upload|scp|rsync|sftp)\s+(.+)$/);
|
|
7796
|
+
if (xfer && /\b(?:aws\s+s3\s+(?:cp|sync|mv)|gsutil\s+(?:-m\s+)?(?:cp|rsync|mv)|gcloud(?:\.cmd|\.exe)?\s+storage\s+(?:cp|rsync|mv)|az\s+storage\s+blob\s+upload|scp|rsync|sftp)\b/.test(value)) {
|
|
7513
7797
|
if (/az\s+storage\s+blob\s+upload/.test(value)) return "write";
|
|
7514
7798
|
const tokens = xfer[1].split(/\s+/).filter(Boolean).map((t) => t.replace(/^["']|["']$/g, ""));
|
|
7515
7799
|
const operands = [];
|
|
@@ -7707,6 +7991,10 @@ function isInternalHost(hostname) {
|
|
|
7707
7991
|
const private172 = host.match(/^172\.(\d+)\./);
|
|
7708
7992
|
return Boolean(private172 && Number(private172[1]) >= 16 && Number(private172[1]) <= 31);
|
|
7709
7993
|
}
|
|
7994
|
+
function isSingleLabelWebHost(parsed) {
|
|
7995
|
+
const host = parsed.hostname.toLowerCase();
|
|
7996
|
+
return /^(?:https?|wss?):$/.test(parsed.protocol) && host.length > 0 && !host.includes(".") && !host.includes(":");
|
|
7997
|
+
}
|
|
7710
7998
|
function policyTargetsCall(policy, context) {
|
|
7711
7999
|
if (!policyTargetsMachineKind(policy.appliesTo, normalizeMachineKind(context.machineKind))) return false;
|
|
7712
8000
|
const developerTargets = policy.appliesTo?.developerNames?.filter(Boolean) || [];
|
|
@@ -7962,7 +8250,7 @@ function inferBundledToolContext(toolName, args) {
|
|
|
7962
8250
|
context.url = detectedUrl;
|
|
7963
8251
|
try {
|
|
7964
8252
|
const parsed = new URL(detectedUrl);
|
|
7965
|
-
const hostType = isInternalHost(parsed.hostname) ? "internal" : "external";
|
|
8253
|
+
const hostType = isInternalHost(parsed.hostname) || isSingleLabelWebHost(parsed) ? "internal" : "external";
|
|
7966
8254
|
const inbound = operation === "read" || ["GET", "HEAD", "OPTIONS", "SELECT", "SHOW", "DESCRIBE", "EXPLAIN"].includes(operation) || isCodeExecTool && !hasSecretLikeValue(argsText) && provenCompoundDownload(commandText, detectedUrl) || provenReadUrls.includes(detectedUrl) || operation === toolName && /\b(fetch|get|read|search|list|lookup|browse|navigate|download|clone|pull|view|open|crawl|scrape)\b/.test(nameWords) && !/\b(post|put|send|upload|push|write|submit|publish|export|create|update|delete|patch)\b/.test(nameWords);
|
|
7967
8255
|
context["destination.domain"] = parsed.hostname;
|
|
7968
8256
|
context["url.risk"] = hostType;
|
|
@@ -8065,7 +8353,7 @@ function recordHttpRequestFacts(context, args, detectedUrl) {
|
|
|
8065
8353
|
if (!["http:", "https:"].includes(parsed.protocol)) return false;
|
|
8066
8354
|
const containsSecret = hasSecretLikeValue(stringifyArgs({ url: detectedUrl, headers: args.headers, body: args.body, data: args.data }));
|
|
8067
8355
|
context["request.destination.domain"] = parsed.hostname;
|
|
8068
|
-
context["request.destination.type"] = isInternalHost(parsed.hostname) ? "internal" : "external";
|
|
8356
|
+
context["request.destination.type"] = isInternalHost(parsed.hostname) || isSingleLabelWebHost(parsed) ? "internal" : "external";
|
|
8069
8357
|
context["request.containsSecret"] = containsSecret ? "true" : "false";
|
|
8070
8358
|
return containsSecret;
|
|
8071
8359
|
} catch {
|
|
@@ -8096,6 +8384,8 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
|
|
|
8096
8384
|
}
|
|
8097
8385
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8098
8386
|
0 && (module.exports = {
|
|
8387
|
+
ACTION_POLICY_GRANT_MAX_DAYS,
|
|
8388
|
+
ACTION_POLICY_GRANT_MAX_USES,
|
|
8099
8389
|
CONTENT_FACT_FIELDS,
|
|
8100
8390
|
DEFAULT_ACTION_POLICY_APPROVAL,
|
|
8101
8391
|
MACHINE_KINDS,
|
package/dist/approvalPrompt.d.ts
CHANGED
|
@@ -3,5 +3,7 @@ export interface ApprovalMessageOptions {
|
|
|
3
3
|
allowAlways?: boolean;
|
|
4
4
|
/** Human wording of the grant scope, e.g. "`gh` commands (write) to github.com". */
|
|
5
5
|
alwaysScopeLabel?: string;
|
|
6
|
+
/** How long it holds: "30 days" (default) or "7 days or 20 uses". */
|
|
7
|
+
alwaysLimitsLabel?: string;
|
|
6
8
|
}
|
|
7
9
|
export declare function nativeApprovalMessage(reason: string, options?: ApprovalMessageOptions): string;
|
package/dist/approvalPrompt.js
CHANGED
|
@@ -39,7 +39,7 @@ function nativeApprovalMessage(reason, options = {}) {
|
|
|
39
39
|
return [
|
|
40
40
|
'Allow this action?',
|
|
41
41
|
'', ...details, '',
|
|
42
|
-
...(options.alwaysScopeLabel ? [`Always allow covers: ${line(options.alwaysScopeLabel, 200)} — for 30 days on this machine, under this rule as it is written today.`, ''] : []),
|
|
42
|
+
...(options.alwaysScopeLabel ? [`Always allow covers: ${line(options.alwaysScopeLabel, 200)} — for ${options.alwaysLimitsLabel || '30 days'} on this machine, under this rule as it is written today.`, ''] : []),
|
|
43
43
|
'Allow once permits only this action. Deny or closing keeps it stopped.',
|
|
44
44
|
'Full details: the IDE message or this machine’s Activity in FullCourtDefense.',
|
|
45
45
|
].join('\n');
|
package/dist/askDialog.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Deterministic all the way: the rule that asked is named in the envelope, the
|
|
21
21
|
* grant is bound to that rule's hash, and nothing here re-evaluates the action.
|
|
22
22
|
*/
|
|
23
|
-
import { ConfirmRemember, GrantScope, NativeDialogOptions, NativeDialogOutcome } from './devConfirm';
|
|
23
|
+
import { ConfirmRemember, GrantLimits, GrantScope, NativeDialogOptions, NativeDialogOutcome } from './devConfirm';
|
|
24
24
|
/** Field on the verdict JSON that carries the envelope. Stripped before the IDE sees the verdict. */
|
|
25
25
|
export declare const ASK_ENVELOPE_FIELD = "fcdAsk";
|
|
26
26
|
/** Default wait for the person. Cursor's hook window is 90 s, Claude's 60 s by default; stay inside both. */
|
|
@@ -36,9 +36,22 @@ export interface AskEnvelope {
|
|
|
36
36
|
scope: GrantScope;
|
|
37
37
|
policyId?: string;
|
|
38
38
|
policyName?: string;
|
|
39
|
+
/** Bundle hash — goes on the recorded event. */
|
|
39
40
|
policyHash?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Identity of the rule as written (`actionPolicyRuleHash` / `localSafetyRuleHash`).
|
|
43
|
+
* A grant binds to this so it survives unrelated org edits and enrollments.
|
|
44
|
+
* Older hook builds omit it; the bundle hash is the fallback.
|
|
45
|
+
*/
|
|
46
|
+
ruleHash?: string;
|
|
40
47
|
matchedRule?: string;
|
|
41
48
|
remember: ConfirmRemember;
|
|
49
|
+
/**
|
|
50
|
+
* Caps an "Always allow" would be created under (rule + org ceiling).
|
|
51
|
+
* `null` = the org switched "Always allow" off (no third button).
|
|
52
|
+
* Absent (older hook build) = the built-in 30 days.
|
|
53
|
+
*/
|
|
54
|
+
grantLimits?: GrantLimits | null;
|
|
42
55
|
/** Text for the dialog (the same explanation the IDE card shows). */
|
|
43
56
|
reason: string;
|
|
44
57
|
}
|
package/dist/askDialog.js
CHANGED
|
@@ -138,15 +138,18 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
138
138
|
const canAsk = options.dialog ? (0, devConfirm_1.isHumanPresent)() : (0, devConfirm_1.nativeDialogAvailable)();
|
|
139
139
|
if (askChannel() === 'ide' || !canAsk)
|
|
140
140
|
return { ...clean, resolution: 'ide' };
|
|
141
|
-
const
|
|
141
|
+
const grantHash = envelope.ruleHash || envelope.policyHash;
|
|
142
|
+
const grantLimits = envelope.grantLimits === null ? undefined : (envelope.grantLimits || { ttlMs: devConfirm_1.GRANT_TTL_MS });
|
|
143
|
+
const allowAlways = envelope.remember !== 'none' && Boolean(grantHash) && Boolean(grantLimits)
|
|
142
144
|
&& (envelope.scope.event !== 'shell' || (envelope.scope.programs || []).length > 0);
|
|
143
145
|
const scopeLabel = allowAlways ? (0, devConfirm_1.describeGrantScope)(envelope.scope) : undefined;
|
|
146
|
+
const limitsLabel = allowAlways && grantLimits ? (0, devConfirm_1.describeGrantLimits)(grantLimits) : undefined;
|
|
144
147
|
const dialog = options.dialog || devConfirm_1.confirmNativeActionDetailed;
|
|
145
148
|
const reason = options.clientLabel ? `IDE: ${options.clientLabel}\n${envelope.reason}` : envelope.reason;
|
|
146
149
|
let outcome = 'unavailable';
|
|
147
150
|
let latencyMs = 0;
|
|
148
151
|
try {
|
|
149
|
-
({ outcome, latencyMs } = await dialog(reason, options.timeoutMs ?? exports.ASK_DIALOG_TIMEOUT_MS, { allowAlways, alwaysScopeLabel: scopeLabel }));
|
|
152
|
+
({ outcome, latencyMs } = await dialog(reason, options.timeoutMs ?? exports.ASK_DIALOG_TIMEOUT_MS, { allowAlways, alwaysScopeLabel: scopeLabel, alwaysLimitsLabel: limitsLabel }));
|
|
150
153
|
}
|
|
151
154
|
catch {
|
|
152
155
|
outcome = 'unavailable';
|
|
@@ -154,13 +157,12 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
154
157
|
if (outcome === 'unavailable')
|
|
155
158
|
return { ...clean, resolution: 'ide' };
|
|
156
159
|
const policyLabel = envelope.policyName ? ` (${envelope.policyName})` : '';
|
|
157
|
-
const rule = { policyId: envelope.policyId, policyName: envelope.policyName, matchedRule: envelope.matchedRule, policyHash: envelope.policyHash };
|
|
158
160
|
if (outcome === 'allow' || outcome === 'allow_always') {
|
|
159
161
|
// Same bookkeeping the post-execution hook would have done for an IDE Allow.
|
|
160
162
|
(0, devConfirm_1.settleDeveloperConfirmation)(envelope.event, envelope.toolName, envelope.argsDigest);
|
|
161
163
|
let granted = false;
|
|
162
|
-
if (outcome === 'allow_always' && allowAlways &&
|
|
163
|
-
granted = Boolean((0, devConfirm_1.recordDeveloperGrant)(envelope.scope, {
|
|
164
|
+
if (outcome === 'allow_always' && allowAlways && grantHash && grantLimits) {
|
|
165
|
+
granted = Boolean((0, devConfirm_1.recordDeveloperGrant)(envelope.scope, { policyId: envelope.policyId, policyName: envelope.policyName, matchedRule: envelope.matchedRule, policyHash: grantHash }, grantLimits));
|
|
164
166
|
}
|
|
165
167
|
// The MCP gateway sees this exact call next; do not ask the person twice.
|
|
166
168
|
if (envelope.event === 'mcp')
|
|
@@ -170,7 +172,7 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
170
172
|
toolName: envelope.toolName,
|
|
171
173
|
operation: envelope.operation,
|
|
172
174
|
reason: granted
|
|
173
|
-
? `developer chose Always allow in the FullCourtDefense dialog — ${scopeLabel} (
|
|
175
|
+
? `developer chose Always allow in the FullCourtDefense dialog — ${scopeLabel} (${limitsLabel}, this rule)${policyLabel}`
|
|
174
176
|
: `developer confirmed in the FullCourtDefense dialog${policyLabel}`,
|
|
175
177
|
ruleId: envelope.policyId,
|
|
176
178
|
policyHash: envelope.policyHash,
|
|
@@ -180,7 +182,7 @@ async function resolveAskWithDialog(verdict, options = {}) {
|
|
|
180
182
|
});
|
|
181
183
|
(0, telemetry_1.triggerFlush)(false);
|
|
182
184
|
const agentMsg = granted
|
|
183
|
-
? `FullCourtDefense: the developer allowed this ${envelope.event} and chose "Always allow" for ${scopeLabel} under this rule.`
|
|
185
|
+
? `FullCourtDefense: the developer allowed this ${envelope.event} and chose "Always allow" for ${scopeLabel} (${limitsLabel}) under this rule.`
|
|
184
186
|
: `FullCourtDefense: the developer allowed this ${envelope.event} once in the FullCourtDefense dialog.`;
|
|
185
187
|
return { ...clean, stdout: JSON.stringify(rewriteDecision(decision, true, '', agentMsg)), exitCode: 0, resolution: outcome };
|
|
186
188
|
}
|
|
@@ -568,7 +568,6 @@ function containsMetadataEndpoint(value) {
|
|
|
568
568
|
}
|
|
569
569
|
function containsSensitiveCredentialPath(value) {
|
|
570
570
|
const text = normalizeForPath(value);
|
|
571
|
-
const traversal = /(?:^|\/)\.\.(?:\/|$)/.test(text) || /%2e/i.test(value);
|
|
572
571
|
const checks = [
|
|
573
572
|
{ itemId: 'ssh_private_key', label: 'SSH private key', re: /(?:^|\/)\.ssh\/id_(?:rsa|ed25519|ecdsa|dsa)(?:$|[/?#\s'"])/ },
|
|
574
573
|
{ itemId: 'ssh_config', label: 'SSH config', re: /(?:^|\/)\.ssh\/config(?:$|[/?#\s'"])/ },
|
|
@@ -605,12 +604,20 @@ function containsSensitiveCredentialPath(value) {
|
|
|
605
604
|
if (check.re.test(text))
|
|
606
605
|
return { itemId: check.itemId, label: check.label };
|
|
607
606
|
}
|
|
608
|
-
|
|
607
|
+
// `.env` is a FILE, the last segment of its path: `../.env`, `.env.local`,
|
|
608
|
+
// `cat ../../.env`. A regex literal such as `process\.env\.\w*` normalizes to
|
|
609
|
+
// `process/.env/.\w*` — a directory named .env with more path after it — and
|
|
610
|
+
// is not a file read. Traversal and the file must sit in the SAME path token:
|
|
611
|
+
// `../.github/x.yml` on one argument and `.env` on another are two unrelated
|
|
612
|
+
// arguments, not a reach outside the workspace (recorded IDE false positive).
|
|
613
|
+
const envToken = text.split(/[\s'"]+/).find(token => /(?:^|\/)\.env(?:\.[a-z0-9_-]+)?(?:$|[?#])/.test(token));
|
|
614
|
+
if (envToken) {
|
|
609
615
|
// Traversal to a .env outside the workspace stays a hard block
|
|
610
616
|
// (env_traversal); a plain workspace .env is a separate warn-by-default
|
|
611
617
|
// item (env_workspace) — agents legitimately read it during development,
|
|
612
618
|
// but the fleet should still see every touch.
|
|
613
|
-
|
|
619
|
+
const reachesOutside = /(?:^|\/)\.\.(?:\/|$)/.test(envToken) || /%2e/i.test(envToken);
|
|
620
|
+
return reachesOutside
|
|
614
621
|
? { itemId: 'env_traversal', label: 'environment file via path traversal' }
|
|
615
622
|
: { itemId: 'env_workspace', label: 'workspace environment file' };
|
|
616
623
|
}
|