fullcourtdefense-cli 1.34.25 → 1.35.0

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.
@@ -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
- if (!word) return "";
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 = /--?(?:password|passwd|pwd|token|api-?key|secret|client-secret|access-token)(?:=|\s+)[\\"']*([^\s\\"',;&|)}\]<>]{8,})/gi;
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|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;
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;
@@ -7076,8 +7153,8 @@ function extractUrl(args, _argsText) {
7076
7153
  const raw = args[field];
7077
7154
  if (typeof raw !== "string") continue;
7078
7155
  const shellField = ["command", "cmd", "script", "input"].includes(field);
7079
- const value = shellField ? splitShellSegments(shellUrlActionText(raw)).map(withoutUnattributedJavaScriptUrls).join("\n") : raw;
7080
- const firstUrl = (text) => text.match(/https?:\/\/[^\s"'<>]+/i)?.[0] || text.match(/\b(?:s3|gs|ftp|sftp|smb):\/\/[^\s"'<>]+/i)?.[0];
7156
+ const value = shellField ? splitShellSegments(shellUrlActionText(raw)).map(withoutUnattributedJavaScriptUrls).map(withoutGhMetadataProse).join("\n") : raw;
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,225 @@ 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"@|'(?:[^']|'')*'|-?\d+(?:\.\d+)?|@\((?:[\s,]|'(?:[^']|'')*'|"[^"$`]*"|@'\r?\n[\s\S]*?\r?\n'@|-?\d+(?:\.\d+)?)*\))\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
+ return parseGhMetadataWrite(segment)?.bodyFile;
7351
+ }
7352
+ function parseGhMetadataWrite(segment) {
7353
+ 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);
7354
+ if (!ghBody) return void 0;
7355
+ const tokens = ghBody[2].match(/'[^'\r\n]*'|"[^"\r\n]*"|[^\s'"]+/g) || [];
7356
+ if (tokens.join("").replace(/\s/g, "") !== ghBody[2].replace(/\s/g, "")) return void 0;
7357
+ let bodyFile;
7358
+ let bodySeen = false;
7359
+ const kept = [];
7360
+ for (let i2 = 0; i2 < tokens.length; i2 += 2) {
7361
+ const flag = tokens[i2];
7362
+ const value = tokens[i2 + 1];
7363
+ if (!/^--(?:base|head|title|label|assignee|milestone|reviewer|body-file|body)$/i.test(flag) || !value || !GH_FLAG_VALUE_RE.test(value)) return void 0;
7364
+ if (/^--body-file$/i.test(flag)) {
7365
+ if (bodySeen) return void 0;
7366
+ bodySeen = true;
7367
+ bodyFile = value;
7368
+ }
7369
+ if (/^--body$/i.test(flag)) {
7370
+ if (bodySeen) return void 0;
7371
+ bodySeen = true;
7372
+ }
7373
+ const prose = /^--(?:body|title)$/i.test(flag);
7374
+ kept.push(flag, prose ? '"fcd-gh-metadata"' : value);
7375
+ }
7376
+ return { bodyFile, masked: `${ghBody[1]}${kept.join(" ")}${ghBody[3]}` };
7377
+ }
7378
+ function withoutGhMetadataProse(segment) {
7379
+ return parseGhMetadataWrite(segment)?.masked ?? segment;
7380
+ }
7094
7381
  function shellUrlActionText(command) {
7095
7382
  if (command.length > 65536) return command;
7096
7383
  if (/\$env:(?:TEMP|TMP)\s*=/i.test(maskQuotedSpans(command))) return command;
7097
- const fileSyntax = (text) => text.replace(/"\$env:(?:TEMP|TMP)([\\/][A-Za-z0-9_ ./\\-]+)"/gi, '"fcd-temporary$1"').replace(/\(Join-Path\s+\$env:(?:TEMP|TMP)\s+'([A-Za-z0-9_ ./\\-]+)'\)/gi, '"fcd-temporary/$1"');
7384
+ const fileSyntax = temporaryFileSyntax;
7098
7385
  const originalSegments = splitShellSegments(command);
7099
7386
  const localPaths = /* @__PURE__ */ new Map();
7100
7387
  const literalValues = /* @__PURE__ */ new Map();
@@ -7111,7 +7398,7 @@ function shellUrlActionText(command) {
7111
7398
  return value;
7112
7399
  };
7113
7400
  const segments = originalSegments.map((segment, index) => {
7114
- const dataAssignment = /^\$([A-Za-z_][\w]*)\s*=\s*(@'\r?\n[\s\S]*?\r?\n'@|'(?:[^']|'')*')$/.exec(segment);
7401
+ const dataAssignment = /^\$([A-Za-z_][\w]*)\s*=\s*(@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$]*?\r?\n"@|'(?:[^']|'')*')$/.exec(segment);
7115
7402
  if (dataAssignment && !literalValues.has(dataAssignment[1].toLowerCase()) && literalValues.size < 32) {
7116
7403
  literalValues.set(dataAssignment[1].toLowerCase(), dataAssignment[2]);
7117
7404
  dataAssignments.add(index);
@@ -7125,7 +7412,7 @@ function shellUrlActionText(command) {
7125
7412
  pathAssignments.add(index);
7126
7413
  return segment;
7127
7414
  }
7128
- const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$`]*?\r?\n"@|'(?:[^']|'')*'|"[^"$`]*")/.exec(segment);
7415
+ const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$]*?\r?\n"@|'(?:[^']|'')*'|"[^"$`]*")/.exec(segment);
7129
7416
  const prefix = literal2?.[0] || "";
7130
7417
  const tail = segment.slice(prefix.length).replace(
7131
7418
  /"\$([A-Za-z_][\w]*)([\\/][\w ./\\-]*)?"/g,
@@ -7147,8 +7434,9 @@ function shellUrlActionText(command) {
7147
7434
  );
7148
7435
  });
7149
7436
  if (expansionOverflow) return command;
7150
- const filtered = segments.map((segment) => {
7151
- const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$`]*?\r?\n"@|'(?:[^']|'')*'|"[^"$`]*")/;
7437
+ const storedTargets = /* @__PURE__ */ new Map();
7438
+ const filtered = segments.map((segment, index) => {
7439
+ const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$]*?\r?\n"@|'(?:[^']|'')*'|"[^"$`]*")/;
7152
7440
  const piped = segment.match(literal2);
7153
7441
  let tail = piped ? segment.slice(piped[0].length).replace(/^\s*\|\s*/, "") : "";
7154
7442
  if (!piped || !/^\s*\|/.test(segment.slice(piped[0].length))) {
@@ -7176,8 +7464,15 @@ function shellUrlActionText(command) {
7176
7464
  }
7177
7465
  target = target.replace(/^['"]|['"]$/g, "");
7178
7466
  if (!target || /^[\\/]{2}|[\r\n*?\[\]<>]/.test(target) || /:/.test(target.replace(/^[A-Za-z]:[\\/]/, ""))) return segment;
7467
+ const key = target.toLowerCase();
7468
+ if (!storedTargets.has(key)) storedTargets.set(key, index);
7179
7469
  return tail;
7180
7470
  });
7471
+ const isStoredTarget = (token, before) => {
7472
+ if (!token) return false;
7473
+ const written = storedTargets.get(token.replace(/^['"]|['"]$/g, "").toLowerCase());
7474
+ return written !== void 0 && written < before;
7475
+ };
7181
7476
  const onlyStoredData = segments.every((segment, index) => {
7182
7477
  if (dataAssignments.has(index)) return true;
7183
7478
  if (pathAssignments.has(index)) return true;
@@ -7187,6 +7482,10 @@ function shellUrlActionText(command) {
7187
7482
  if (/^if\s*\(\$([A-Za-z_][\w]*)\.Count\)\s*\{\s*throw\s+\$\1\[0\]\s*\}$/i.test(segment)) return true;
7188
7483
  if (/^Write-Output\s+'(?:[^']|'')*'$/i.test(segment)) return true;
7189
7484
  if (/^gh\s+pr\s+checks\s+\d+\s+--json\s+[a-z,]+\s+--jq\s+'(?:[^']|'')*'$/i.test(segment)) return true;
7485
+ const ghBodyFile = ghMetadataWriteBodyFile(segment);
7486
+ if (ghBodyFile !== void 0) return isStoredTarget(ghBodyFile, index);
7487
+ const removal = /^Remove-Item\s+(?:-(?:LiteralPath|Path)\s+)?('[^'\r\n]*'|"[^"\r\n]*"|[^\s'"$`|;&(){}<>]+)(?:\s+-Force)?$/i.exec(segment);
7488
+ if (removal && isStoredTarget(removal[1], index)) return true;
7190
7489
  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
7490
  if (read) {
7192
7491
  const target = read[1] ?? read[2] ?? read[3];
@@ -7223,6 +7522,22 @@ function splitShellSegments(command, alsoOnPipe = false) {
7223
7522
  continue;
7224
7523
  }
7225
7524
  }
7525
+ if (ch === "@" && command[i2 + 1] === "{") {
7526
+ const end = hashtableEnd(command, i2);
7527
+ if (end > i2) {
7528
+ cur += command.slice(i2, end);
7529
+ i2 = end - 1;
7530
+ continue;
7531
+ }
7532
+ }
7533
+ if (ch === "@" && command[i2 + 1] === "(") {
7534
+ const end = literalArrayEnd(command, i2);
7535
+ if (end > i2) {
7536
+ cur += command.slice(i2, end);
7537
+ i2 = end - 1;
7538
+ continue;
7539
+ }
7540
+ }
7226
7541
  if (ch === '"' || ch === "'") {
7227
7542
  quote = ch;
7228
7543
  cur += ch;
@@ -7263,6 +7578,47 @@ ${q}@`;
7263
7578
  if (idx < 0) return -1;
7264
7579
  return idx + close.length;
7265
7580
  }
7581
+ function hashtableEnd(command, start) {
7582
+ let depth = 0;
7583
+ let quote = null;
7584
+ for (let i2 = start + 1; i2 < command.length; i2++) {
7585
+ const ch = command[i2];
7586
+ if (quote) {
7587
+ if (ch === "\\" && quote === '"' && i2 + 1 < command.length) {
7588
+ i2++;
7589
+ continue;
7590
+ }
7591
+ if (ch === quote) quote = null;
7592
+ continue;
7593
+ }
7594
+ if (ch === '"' || ch === "'") {
7595
+ quote = ch;
7596
+ continue;
7597
+ }
7598
+ if (ch === "{") depth++;
7599
+ else if (ch === "}" && --depth === 0) return i2 + 1;
7600
+ }
7601
+ return -1;
7602
+ }
7603
+ var LITERAL_ELEMENT_RE = /^(?:'(?:[^']|'')*'|"[^"$`]*"|@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$`]*?\r?\n"@|-?\d+(?:\.\d+)?)/;
7604
+ function literalArrayEnd(command, start) {
7605
+ let i2 = start + 2;
7606
+ let elements = 0;
7607
+ while (i2 < command.length) {
7608
+ const rest = command.slice(i2);
7609
+ const skip = /^[\s,]+/.exec(rest);
7610
+ if (skip) {
7611
+ i2 += skip[0].length;
7612
+ continue;
7613
+ }
7614
+ if (command[i2] === ")") return elements > 0 ? i2 + 1 : -1;
7615
+ const element = LITERAL_ELEMENT_RE.exec(rest);
7616
+ if (!element || elements >= 256) return -1;
7617
+ i2 += element[0].length;
7618
+ elements++;
7619
+ }
7620
+ return -1;
7621
+ }
7266
7622
  var QUOTED_LITERAL_RE = /@'[\s\S]*?\r?\n'@|@"[\s\S]*?\r?\n"@|"((?:[^"\\]|\\.)*)"|'([^']*)'/g;
7267
7623
  function quotedSpans(segment) {
7268
7624
  const spans = [];
@@ -7445,6 +7801,16 @@ function shellSegmentLead(segment) {
7445
7801
  }
7446
7802
  var PS_SESSION_VARIABLE_ASSIGN_RE = /^\s*\$(?:psdefaultparametervalues|psmodulepath|profile|executioncontext|env:(?:path|pathext|psmodulepath))\s*(?:\[[^\]]*\]\s*)?(?:\+?=)/i;
7447
7803
  var PS_VALUE_EXECUTOR_RE = /\b(?:invoke-expression|iex|invoke-command|icm|start-process|saps|invoke-item|ii)\b|&\s*[$(]|\.\s*invoke\s*\(/i;
7804
+ function isInertValueExpression(raw) {
7805
+ const masked = maskQuotedSpans(raw);
7806
+ if (/\$\(|`|&\s*[$(\w"']|[|<>]/.test(masked) || PS_VALUE_EXECUTOR_RE.test(masked)) return false;
7807
+ if (/(?:::|\.)\s*(?:invoke|start|run|exec|create|load|download|upload|send|open)\w*\s*\(/i.test(masked)) return false;
7808
+ for (const group of masked.matchAll(/\(\s*([A-Za-z][\w-]*)/g)) if (!LOCAL_DATA_LEADS.has(group[1].toLowerCase())) return false;
7809
+ const body = masked.replace(/^\s*\$[\w]+\s*(?:\+|-|\*|\/)?=\s*/, "").trim();
7810
+ if (/^\$\w+\s*(?:\+\+|--)$/.test(body)) return true;
7811
+ if (/^(?:@\{|@\()/.test(body)) return !/[$(]/.test(body.slice(2)) && /^@[{(](?:[\s;,=]|""|-?\d+(?:\.\d+)?|[\w-]+|@\{|\}|\))*$/.test(body);
7812
+ return /^(?:""|-?\d)/.test(body);
7813
+ }
7448
7814
  var worstShellOp = (ops, floor = "read") => ops.reduce((w, op) => (SHELL_OP_RANK[op] || 0) > (SHELL_OP_RANK[w] || 0) ? op : w, floor);
7449
7815
  function classifyShellSegment(segment, depth = 0) {
7450
7816
  const raw = segment.trim();
@@ -7453,6 +7819,7 @@ function classifyShellSegment(segment, depth = 0) {
7453
7819
  const lead = shellSegmentLead(segment);
7454
7820
  if ((/^\$env:[\w]+$/.test(lead) || /^\$\w+$/.test(lead)) && PS_VALUE_EXECUTOR_RE.test(maskQuotedSpans(raw))) return "SHELL";
7455
7821
  if (SHELL_NEUTRAL_BUILTINS.has(lead) || /^\$env:[\w]+$/.test(lead) || /^\$\w+$/.test(lead)) return "read";
7822
+ if (isInertValueExpression(raw)) return "read";
7456
7823
  const rawLower = raw.toLowerCase();
7457
7824
  const sqlClient = SHELL_SQL_CLIENTS.has(lead) || /\|\s*(?:sudo\s+)?(?:psql|mysql|mariadb|sqlcmd|sqlite3|mongosh?|clickhouse-client|bq|cockroach|usql)\b/.test(rawLower);
7458
7825
  if (sqlClient) {
@@ -7484,6 +7851,8 @@ function classifyShellWords(masked, raw, lead) {
7484
7851
  const explicitMethod = /(?:\s-x|\s--request|\s-method)[\s=]+["']?(post|put|patch|delete)\b/i.test(segment);
7485
7852
  const hasBody = /\s(?:-d|--data(?:-raw|-binary|-urlencode)?|-F|--form|-T|--upload-file|--json|-body|-infile)\b/i.test(segment);
7486
7853
  if (explicitMethod || hasBody) return "write";
7854
+ const credentialArgument = /\s(?:-h|--header|-headers|-u|--user|-token|-credential|-authentication|--oauth2-bearer|-proxycredential)(?:[\s=]|$)/i.exec(segment);
7855
+ if (credentialArgument && /\$\(|`|\$env:|\(\s*[A-Za-z][\w-]*\s/.test(segment.slice(credentialArgument.index))) return "write";
7487
7856
  return pipesIntoInterpreter ? "SHELL" : "read";
7488
7857
  }
7489
7858
  if (SHELL_READ_PROGRAMS.has(lead)) {
@@ -7508,8 +7877,8 @@ function classifyShellWords(masked, raw, lead) {
7508
7877
  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
7878
  return "DELETE";
7510
7879
  }
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)) {
7880
+ 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+(.+)$/);
7881
+ 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
7882
  if (/az\s+storage\s+blob\s+upload/.test(value)) return "write";
7514
7883
  const tokens = xfer[1].split(/\s+/).filter(Boolean).map((t) => t.replace(/^["']|["']$/g, ""));
7515
7884
  const operands = [];
@@ -7640,7 +8009,7 @@ function isPowerShellResponseProjection(segment, responses) {
7640
8009
  const object = /^\[pscustomobject\]\s*@\{([^{}]+)\}\s*(?:\|\s*(.+))?$/i.exec(segment);
7641
8010
  if (!object) return false;
7642
8011
  if (object[2] && object[2].split("|").some((part) => !isPowerShellReadFormatter(part.trim()))) return false;
7643
- const fields = object[1].trim().replace(/;\s*$/, "").split(";");
8012
+ const fields = object[1].trim().replace(/[;\s]*$/, "").split(/;|\r?\n/).filter((field) => field.trim());
7644
8013
  if (fields.length > 32) return false;
7645
8014
  const labels = /* @__PURE__ */ new Set();
7646
8015
  return fields.every((field) => {
@@ -7707,6 +8076,10 @@ function isInternalHost(hostname) {
7707
8076
  const private172 = host.match(/^172\.(\d+)\./);
7708
8077
  return Boolean(private172 && Number(private172[1]) >= 16 && Number(private172[1]) <= 31);
7709
8078
  }
8079
+ function isSingleLabelWebHost(parsed) {
8080
+ const host = parsed.hostname.toLowerCase();
8081
+ return /^(?:https?|wss?):$/.test(parsed.protocol) && host.length > 0 && !host.includes(".") && !host.includes(":");
8082
+ }
7710
8083
  function policyTargetsCall(policy, context) {
7711
8084
  if (!policyTargetsMachineKind(policy.appliesTo, normalizeMachineKind(context.machineKind))) return false;
7712
8085
  const developerTargets = policy.appliesTo?.developerNames?.filter(Boolean) || [];
@@ -7962,7 +8335,7 @@ function inferBundledToolContext(toolName, args) {
7962
8335
  context.url = detectedUrl;
7963
8336
  try {
7964
8337
  const parsed = new URL(detectedUrl);
7965
- const hostType = isInternalHost(parsed.hostname) ? "internal" : "external";
8338
+ const hostType = isInternalHost(parsed.hostname) || isSingleLabelWebHost(parsed) ? "internal" : "external";
7966
8339
  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
8340
  context["destination.domain"] = parsed.hostname;
7968
8341
  context["url.risk"] = hostType;
@@ -8065,7 +8438,7 @@ function recordHttpRequestFacts(context, args, detectedUrl) {
8065
8438
  if (!["http:", "https:"].includes(parsed.protocol)) return false;
8066
8439
  const containsSecret = hasSecretLikeValue(stringifyArgs({ url: detectedUrl, headers: args.headers, body: args.body, data: args.data }));
8067
8440
  context["request.destination.domain"] = parsed.hostname;
8068
- context["request.destination.type"] = isInternalHost(parsed.hostname) ? "internal" : "external";
8441
+ context["request.destination.type"] = isInternalHost(parsed.hostname) || isSingleLabelWebHost(parsed) ? "internal" : "external";
8069
8442
  context["request.containsSecret"] = containsSecret ? "true" : "false";
8070
8443
  return containsSecret;
8071
8444
  } catch {
@@ -8096,6 +8469,8 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
8096
8469
  }
8097
8470
  // Annotate the CommonJS export names for ESM import in node:
8098
8471
  0 && (module.exports = {
8472
+ ACTION_POLICY_GRANT_MAX_DAYS,
8473
+ ACTION_POLICY_GRANT_MAX_USES,
8099
8474
  CONTENT_FACT_FIELDS,
8100
8475
  DEFAULT_ACTION_POLICY_APPROVAL,
8101
8476
  MACHINE_KINDS,
@@ -3,5 +3,9 @@ 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
  }
9
+ /** Window title: the kind of action and the policy that stopped it, by name. */
10
+ export declare function nativeApprovalTitle(reason: string): string;
7
11
  export declare function nativeApprovalMessage(reason: string, options?: ApprovalMessageOptions): string;