fullcourtdefense-cli 1.34.12 → 1.34.15

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.
@@ -37,6 +37,13 @@ export interface ResolvedActionPolicyApproval {
37
37
  onTimeout: ActionPolicyApprovalOnTimeout;
38
38
  }
39
39
  export interface ActionPolicyRule {
40
+ /** Explicit, narrowly scoped permission to relax named role actions only. */
41
+ roleException?: {
42
+ roleIds: string[];
43
+ verbs: string[];
44
+ };
45
+ /** Set only by the role compiler; never accepted from policy authors. */
46
+ roleVerb?: string;
40
47
  operations: string[];
41
48
  verdict: ActionPolicyVerdict;
42
49
  constraints?: ActionPolicyConstraint[];
@@ -45,6 +52,7 @@ export interface ActionPolicyRule {
45
52
  }
46
53
  /** Reject ambiguous/broad approval exceptions at authoring time; runtime also fails closed on them. */
47
54
  export declare function approvalExceptionError(rule: ActionPolicyRule): string | undefined;
55
+ export declare function roleExceptionError(rule: ActionPolicyRule): string | undefined;
48
56
  /**
49
57
  * The three kinds of machine a fleet policy can be written for:
50
58
  * - `workstation` — laptops / desktops a person uses (default for any machine that does not say otherwise)
@@ -35,6 +35,7 @@ __export(actionPolicyEngine_exports, {
35
35
  normalizeMachineKind: () => normalizeMachineKind,
36
36
  policyTargetsMachineKind: () => policyTargetsMachineKind,
37
37
  resolveApprovalOptions: () => resolveApprovalOptions,
38
+ roleExceptionError: () => roleExceptionError,
38
39
  ruleNeedsContentFact: () => ruleNeedsContentFact,
39
40
  toolCapabilities: () => toolCapabilities
40
41
  });
@@ -5738,6 +5739,14 @@ function parse3(input, options) {
5738
5739
  }
5739
5740
 
5740
5741
  // src/actionPolicyEngine.ts
5742
+ function isFirestoreQueryUrl(target) {
5743
+ try {
5744
+ const url = new URL(target);
5745
+ return url.origin === "https://firestore.googleapis.com" && !url.username && !url.password && !url.search && !url.hash && /^\/v1\/projects\/[^/]+\/databases\/[^/]+\/documents(?:\/[^/]+)*:runQuery$/.test(url.pathname);
5746
+ } catch {
5747
+ return false;
5748
+ }
5749
+ }
5741
5750
  function provenJavaScriptReadUrls(code) {
5742
5751
  if (code.length > 32768) return [];
5743
5752
  const literal2 = (value) => ({ kind: "literal", value });
@@ -5758,6 +5767,8 @@ function provenJavaScriptReadUrls(code) {
5758
5767
  const readProperty = (value, key) => {
5759
5768
  if (["constructor", "__proto__", "prototype"].includes(key)) return fail();
5760
5769
  if (value.kind === "object") return value.fields.get(key) || literal2(void 0);
5770
+ if (value.kind === "array" && key === "map") return { kind: "arrayMap", items: value.items };
5771
+ if (value.kind === "promise" && key === "catch") return callable("promise.catch");
5761
5772
  if (value.kind === "response") return ["json", "text"].includes(key) ? callable(`response.${key}`) : { kind: "data" };
5762
5773
  if (value.kind === "credential" && key === "trim") return callable("credential.trim");
5763
5774
  if (value.kind === "string" || value.kind === "literal" && typeof value.value === "string") {
@@ -5822,24 +5833,97 @@ function provenJavaScriptReadUrls(code) {
5822
5833
  const left = ev(node.left);
5823
5834
  const right = ev(node.right);
5824
5835
  if (node.operator !== "+") return fail();
5825
- if (left.kind === "literal" && right.kind === "literal" && typeof left.value === "string" && typeof right.value === "string") return literal2(left.value + right.value);
5836
+ if (left.kind === "literal" && right.kind === "literal" && typeof left.value === "string" && typeof right.value === "string") {
5837
+ if (left.value.length + right.value.length > 32768) return fail();
5838
+ return literal2(left.value + right.value);
5839
+ }
5826
5840
  if (left.kind === "literal" && left.value === "Bearer " && right.kind === "credential") return { kind: "authorization" };
5827
5841
  return fail();
5828
5842
  }
5829
5843
  case "ArrowFunctionExpression":
5830
5844
  case "FunctionExpression":
5831
- if (node.params.length || node.generator) return fail();
5845
+ if (node.params.some((param) => param.type !== "Identifier") || node.generator) return fail();
5832
5846
  return { kind: "function", node, env: new Map(env) };
5847
+ case "ForOfStatement": {
5848
+ const items = ev(node.right);
5849
+ if (node.await || items.kind !== "array" || items.items.length > 100 || node.left.type !== "VariableDeclaration" || node.left.kind !== "const" || node.left.declarations.length !== 1) return fail();
5850
+ for (const item of items.items) {
5851
+ const scope = new Map(env);
5852
+ const id = node.left.declarations[0].id;
5853
+ if (id.type === "Identifier") scope.set(id.name, item);
5854
+ else if (id.type === "ArrayPattern" && item.kind === "array" && id.elements.length === item.items.length) {
5855
+ id.elements.forEach((element, index) => {
5856
+ if (element?.type !== "Identifier") fail();
5857
+ scope.set(element.name, item.items[index]);
5858
+ });
5859
+ } else return fail();
5860
+ evaluate(node.body, scope, depth + 1);
5861
+ }
5862
+ return literal2(void 0);
5863
+ }
5864
+ case "AssignmentExpression": {
5865
+ if (node.operator !== "=" || node.left.type !== "MemberExpression" || node.left.computed || node.left.object.type !== "Identifier" || node.left.object.name !== "process" || env.has("process") || node.left.property.name !== "exitCode" || ev(node.right).kind !== "literal" || node.right.type !== "Literal" || node.right.value !== 1) return fail();
5866
+ return literal2(void 0);
5867
+ }
5868
+ case "NewExpression": {
5869
+ const constructor = ev(node.callee);
5870
+ if (constructor.kind !== "call" || constructor.name !== "GoogleAuth" || node.arguments.length !== 1) return fail();
5871
+ const options = ev(node.arguments[0]);
5872
+ if (options.kind !== "object" || options.fields.size !== 1) return fail();
5873
+ const scopes = options.fields.get("scopes");
5874
+ if (scopes?.kind !== "array" || scopes.items.length !== 1 || string(scopes.items[0]) !== "https://www.googleapis.com/auth/datastore") return fail();
5875
+ return object([["getClient", callable("google.getClient")]]);
5876
+ }
5833
5877
  case "ReturnStatement":
5834
5878
  return node.argument ? ev(node.argument) : literal2(void 0);
5835
5879
  case "CallExpression": {
5836
5880
  const callee = ev(node.callee);
5837
5881
  const args = node.arguments.map(ev);
5838
5882
  if (callee.kind === "function") {
5839
- if (args.length) return fail();
5840
- return evaluate(callee.node.body, new Map(callee.env), depth + 1);
5883
+ if (args.length !== callee.node.params.length) return fail();
5884
+ const scope = new Map(callee.env);
5885
+ args.forEach((value, index) => scope.set(callee.node.params[index].name, value));
5886
+ const result = evaluate(callee.node.body, scope, depth + 1);
5887
+ return callee.node.async ? { kind: "promise" } : result;
5888
+ }
5889
+ if (callee.kind === "arrayMap") {
5890
+ if (args.length !== 1 || args[0].kind !== "function" || args[0].node.params.length !== 1 || args[0].node.async) return fail();
5891
+ const fn = args[0];
5892
+ return { kind: "array", items: callee.items.map((item) => {
5893
+ const scope = new Map(fn.env);
5894
+ scope.set(fn.node.params[0].name, item);
5895
+ return evaluate(fn.node.body, scope, depth + 1);
5896
+ }) };
5841
5897
  }
5842
5898
  if (callee.kind !== "call") return fail();
5899
+ if (callee.name === "google.getClient") {
5900
+ if (args.length) return fail();
5901
+ return object([["request", callable("google.request")]]);
5902
+ }
5903
+ if (callee.name === "google.request") {
5904
+ if (args.length !== 1 || args[0].kind !== "object") return fail();
5905
+ const fields = args[0].fields;
5906
+ if ([...fields.keys()].some((key) => !["url", "method", "data"].includes(key))) return fail();
5907
+ const target = string(fields.get("url") || fail());
5908
+ if (!isFirestoreQueryUrl(target) || string(fields.get("method") || fail()).toUpperCase() !== "POST") return fail();
5909
+ const body = fields.get("data");
5910
+ const passive = (value) => {
5911
+ if (--budget < 0) return false;
5912
+ if (value.kind === "literal") return true;
5913
+ if (value.kind === "object") return [...value.fields.values()].every(passive);
5914
+ return value.kind === "array" && value.items.every(passive);
5915
+ };
5916
+ if (body?.kind !== "object" || !body.fields.has("structuredQuery") || [...body.fields.keys()].some((key) => !["structuredQuery", "readTime", "transaction"].includes(key)) || !passive(body)) return fail();
5917
+ urls.push(target);
5918
+ return { kind: "data" };
5919
+ }
5920
+ if (callee.name === "promise.catch") {
5921
+ if (args.length !== 1 || args[0].kind !== "function" || args[0].node.params.length !== 1) return fail();
5922
+ const scope = new Map(args[0].env);
5923
+ scope.set(args[0].node.params[0].name, { kind: "data" });
5924
+ evaluate(args[0].node.body, scope, depth + 1);
5925
+ return { kind: "data" };
5926
+ }
5843
5927
  if (callee.name === "fetch") {
5844
5928
  if (args.length < 1 || args.length > 2) return fail();
5845
5929
  const target = string(args[0]);
@@ -5866,6 +5950,7 @@ function provenJavaScriptReadUrls(code) {
5866
5950
  return { kind: "response" };
5867
5951
  }
5868
5952
  if (callee.name === "require") {
5953
+ if (args.length === 1 && string(args[0]) === "google-auth-library") return object([["GoogleAuth", callable("GoogleAuth")]]);
5869
5954
  if (args.length !== 1 || !["node:child_process", "child_process"].includes(string(args[0]))) return fail();
5870
5955
  return object([["execFileSync", callable("execFileSync")]]);
5871
5956
  }
@@ -5888,7 +5973,7 @@ function provenJavaScriptReadUrls(code) {
5888
5973
  if (args.length !== 1 || !jsonSafe(args[0])) return fail();
5889
5974
  return { kind: "string" };
5890
5975
  }
5891
- if (callee.name === "console.log") return literal2(void 0);
5976
+ if (callee.name === "console.log" || callee.name === "console.error") return literal2(void 0);
5892
5977
  if (callee.name === "string.trim") {
5893
5978
  if (args.length) return fail();
5894
5979
  return { kind: "string" };
@@ -5913,14 +5998,14 @@ function provenJavaScriptReadUrls(code) {
5913
5998
  ["fetch", callable("fetch")],
5914
5999
  ["require", callable("require")],
5915
6000
  ["JSON", object([["stringify", callable("JSON.stringify")]])],
5916
- ["console", object([["log", callable("console.log")]])]
6001
+ ["console", object([["log", callable("console.log")], ["error", callable("console.error")]])]
5917
6002
  ]));
5918
6003
  return urls;
5919
6004
  } catch {
5920
6005
  return [];
5921
6006
  }
5922
6007
  }
5923
- function provenInlineJavaScriptRead(command, detectedUrl) {
6008
+ function provenInlineJavaScriptReadUrls(command) {
5924
6009
  const urls = [];
5925
6010
  for (const segment of splitShellSegments(command)) {
5926
6011
  const here = segment.match(/^\s*@'\r?\n([\s\S]*?)\r?\n'@\s*\|\s*node(?:\.exe)?(?:\s+-)?\s*$/i);
@@ -5928,16 +6013,201 @@ function provenInlineJavaScriptRead(command, detectedUrl) {
5928
6013
  const code = here?.[1] ?? inline?.[1];
5929
6014
  if (code !== void 0) {
5930
6015
  const proven = provenJavaScriptReadUrls(code);
5931
- if (!proven.length) return false;
6016
+ if (!proven.length) return [];
5932
6017
  urls.push(...proven);
5933
6018
  } else {
5934
6019
  const lead = shellSegmentLead(segment);
5935
- if (!(SHELL_READ_PROGRAMS.has(lead) || ["git", "gh", "cd", "set-location"].includes(lead)) || classifyShellCommandOperation(segment) !== "read" || /https?:\/\//i.test(segment)) return false;
6020
+ if (!(SHELL_READ_PROGRAMS.has(lead) || ["git", "gh", "cd", "set-location"].includes(lead)) || classifyShellCommandOperation(segment) !== "read" || /https?:\/\//i.test(segment)) return [];
5936
6021
  }
5937
6022
  }
5938
- return urls.includes(detectedUrl);
6023
+ return urls;
6024
+ }
6025
+ function provenPowerShellQueryUrls(command) {
6026
+ if (command.length > 32768) return [];
6027
+ const prefix = /^\s*(?:git\s+status(?:\s+--short)?(?:\s+--branch)?|(?:cd|Set-Location)\s+(?:[A-Za-z]:[\\/][A-Za-z0-9_./\\-]+|'[A-Za-z]:[\\/][A-Za-z0-9_ ./\\-]+'))\s*(?:&&|;)\s*/i;
6028
+ for (let count = 0; count < 8 && prefix.test(command); count++) command = command.replace(prefix, "");
6029
+ const credential = Object.freeze({ credential: 1 });
6030
+ const authorization = Object.freeze({ authorization: 1 });
6031
+ const urls = [];
6032
+ const env = /* @__PURE__ */ new Map();
6033
+ const tokens = [];
6034
+ const fail = () => {
6035
+ throw new Error("Unsupported PowerShell query");
6036
+ };
6037
+ const lexer = /[ \t]+|\r?\n|@\{|@\(|'(?:[^']|'')*'|"[^"`]*"|\$[A-Za-z_][A-Za-z0-9_]*|-[A-Za-z]+|[A-Za-z_][A-Za-z0-9_-]*|\d+|[{}();=+|,]/gy;
6038
+ let offset2 = 0;
6039
+ let budget = 8e3;
6040
+ let pos = 0;
6041
+ const take = () => {
6042
+ if (--budget < 0 || pos >= tokens.length) return fail();
6043
+ return tokens[pos++];
6044
+ };
6045
+ const peek = () => tokens[pos];
6046
+ const expect = (token) => {
6047
+ if (take().toLowerCase() !== token.toLowerCase()) fail();
6048
+ };
6049
+ const separators = () => {
6050
+ while (peek() === ";") take();
6051
+ };
6052
+ const passive = (value) => {
6053
+ if (--budget < 0 || value === credential || value === authorization) return false;
6054
+ if (typeof value !== "object") return true;
6055
+ return Object.values(value).every(passive);
6056
+ };
6057
+ const expression = () => {
6058
+ const token = take();
6059
+ let value;
6060
+ if (token.startsWith("'")) value = token.slice(1, -1).replace(/''/g, "'");
6061
+ else if (token.startsWith('"')) {
6062
+ const text = token.slice(1, -1);
6063
+ const bearer = text.match(/^Bearer (\$[A-Za-z_][A-Za-z0-9_]*)$/);
6064
+ if (bearer && env.get(bearer[1].toLowerCase()) === credential) value = authorization;
6065
+ else {
6066
+ if (/[$`]/.test(text)) return fail();
6067
+ value = text;
6068
+ }
6069
+ } else if (token.startsWith("$")) value = env.get(token.toLowerCase()) ?? fail();
6070
+ else if (/^\d+$/.test(token)) value = Number(token);
6071
+ else if (token === "(") {
6072
+ value = expression();
6073
+ expect(")");
6074
+ } else if (token === "@(") {
6075
+ const items = [];
6076
+ separators();
6077
+ while (peek() !== ")") {
6078
+ items.push(expression());
6079
+ separators();
6080
+ if (peek() !== ")") expect(",");
6081
+ }
6082
+ expect(")");
6083
+ value = items;
6084
+ } else if (token === "@{") {
6085
+ const fields = /* @__PURE__ */ Object.create(null);
6086
+ separators();
6087
+ while (peek() !== "}") {
6088
+ const key = take();
6089
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || Object.hasOwn(fields, key)) return fail();
6090
+ expect("=");
6091
+ fields[key] = expression();
6092
+ if (peek() !== "}") expect(";");
6093
+ separators();
6094
+ }
6095
+ expect("}");
6096
+ value = fields;
6097
+ } else if (token.toLowerCase() === "if") {
6098
+ expect("(");
6099
+ const left = expression();
6100
+ expect("-eq");
6101
+ const right = expression();
6102
+ expect(")");
6103
+ if (typeof left === "object" || typeof right === "object") return fail();
6104
+ expect("{");
6105
+ const yes = expression();
6106
+ expect("}");
6107
+ expect("else");
6108
+ expect("{");
6109
+ const no = expression();
6110
+ expect("}");
6111
+ value = left === right ? yes : no;
6112
+ } else if (token.toLowerCase() === "gcloud") {
6113
+ expect("auth");
6114
+ expect("print-access-token");
6115
+ value = credential;
6116
+ } else return fail();
6117
+ while (peek() === "+") {
6118
+ take();
6119
+ const right = expression();
6120
+ if (typeof value !== "string" || typeof right !== "string" || value.length + right.length > 32768) return fail();
6121
+ value += right;
6122
+ }
6123
+ if (peek() === "|") {
6124
+ take();
6125
+ expect("ConvertTo-Json");
6126
+ expect("-Depth");
6127
+ const depth = take();
6128
+ if (!/^\d+$/.test(depth) || Number(depth) > 100 || !passive(value)) return fail();
6129
+ }
6130
+ return value;
6131
+ };
6132
+ const statement = () => {
6133
+ if (peek()?.startsWith("$")) {
6134
+ const name = take().toLowerCase();
6135
+ expect("=");
6136
+ if (["$env", "$psdefaultparametervalues", "$executioncontext", "$erroractionpreference", "$home", "$profile"].includes(name)) return fail();
6137
+ env.set(name, expression());
6138
+ return;
6139
+ }
6140
+ if (peek()?.toLowerCase() === "foreach") {
6141
+ take();
6142
+ expect("(");
6143
+ const name = take().toLowerCase();
6144
+ if (!/^\$[a-z_][a-z0-9_]*$/.test(name)) return fail();
6145
+ expect("in");
6146
+ const items = expression();
6147
+ expect(")");
6148
+ expect("{");
6149
+ if (!Array.isArray(items) || !items.length || items.length > 100) return fail();
6150
+ const start = pos;
6151
+ for (const item of items) {
6152
+ env.set(name, item);
6153
+ pos = start;
6154
+ separators();
6155
+ while (peek() !== "}") {
6156
+ statement();
6157
+ if (peek() !== "}") expect(";");
6158
+ separators();
6159
+ }
6160
+ expect("}");
6161
+ }
6162
+ return;
6163
+ }
6164
+ expect("Invoke-RestMethod");
6165
+ const options = /* @__PURE__ */ new Map();
6166
+ while (peek()?.startsWith("-")) {
6167
+ const flag = take().toLowerCase();
6168
+ if (options.has(flag)) return fail();
6169
+ options.set(flag, flag === "-method" ? take() : expression());
6170
+ }
6171
+ if ([...options.keys()].some((key) => !["-method", "-uri", "-headers", "-contenttype", "-body"].includes(key))) return fail();
6172
+ const target = options.get("-uri");
6173
+ if (typeof target !== "string") return fail();
6174
+ if (!isFirestoreQueryUrl(target) || String(options.get("-method")).toUpperCase() !== "POST" || options.get("-contenttype") !== "application/json") return fail();
6175
+ const headers = options.get("-headers");
6176
+ const body = options.get("-body");
6177
+ if (!headers || typeof headers !== "object" || Array.isArray(headers) || Object.keys(headers).length !== 1 || headers.Authorization !== authorization) return fail();
6178
+ if (!body || typeof body !== "object" || Array.isArray(body) || !Object.hasOwn(body, "structuredQuery") || Object.keys(body).some((key) => !["structuredQuery", "transaction", "readTime"].includes(key)) || !passive(body)) return fail();
6179
+ urls.push(target);
6180
+ if (peek() === "|") {
6181
+ take();
6182
+ expect("ConvertTo-Json");
6183
+ expect("-Depth");
6184
+ if (!/^\d+$/.test(take())) fail();
6185
+ }
6186
+ };
6187
+ try {
6188
+ while (offset2 < command.length) {
6189
+ lexer.lastIndex = offset2;
6190
+ const match = lexer.exec(command);
6191
+ if (!match) return [];
6192
+ offset2 = lexer.lastIndex;
6193
+ const token = match[0];
6194
+ if (/^[ \t]+$/.test(token)) continue;
6195
+ tokens.push(/\r?\n/.test(token) && token.length <= 2 ? ";" : token);
6196
+ }
6197
+ separators();
6198
+ while (pos < tokens.length) {
6199
+ statement();
6200
+ if (pos < tokens.length) expect(";");
6201
+ separators();
6202
+ }
6203
+ return urls;
6204
+ } catch {
6205
+ return [];
6206
+ }
5939
6207
  }
5940
6208
  function approvalExceptionError(rule) {
6209
+ const roleError = roleExceptionError(rule);
6210
+ if (roleError) return roleError;
5941
6211
  const ids = rule.approval?.replacesPolicyIds;
5942
6212
  if (ids === void 0) return void 0;
5943
6213
  if (!Array.isArray(ids) || ids.length > 50 || ids.some((id) => typeof id !== "string" || !id.trim())) {
@@ -5952,6 +6222,19 @@ function approvalExceptionError(rule) {
5952
6222
  }
5953
6223
  return void 0;
5954
6224
  }
6225
+ function roleExceptionError(rule) {
6226
+ const exception = rule.roleException;
6227
+ if (exception === void 0) return void 0;
6228
+ if (!exception || rule.verdict !== "allow") return "A role exception must use Allow.";
6229
+ const valid = (values, permitted) => Array.isArray(values) && values.length > 0 && values.length <= permitted.length && values.every((value) => typeof value === "string" && permitted.includes(value));
6230
+ if (!valid(exception.roleIds, ["read_only", "standard", "power", "unrestricted"]) || !valid(exception.verbs, ["read", "write", "delete", "drop", "deploy", "export", "send", "payment"])) {
6231
+ return "Select valid roles and role actions for the exception.";
6232
+ }
6233
+ if (!["agentName", "toolName"].every((field) => rule.constraints?.some((c) => c.field === field && c.operator === "equals" && typeof c.value === "string" && c.value.trim() && !/[?*]/.test(c.value)))) {
6234
+ return "A role exception must identify an exact agent and exact tool.";
6235
+ }
6236
+ return void 0;
6237
+ }
5955
6238
  var MACHINE_KINDS = ["workstation", "ci", "workload"];
5956
6239
  function normalizeMachineKind(value) {
5957
6240
  const kind = (value || "").trim().toLowerCase();
@@ -6445,6 +6728,7 @@ var OPERATION_MATCH_SKIP_FIELDS = /* @__PURE__ */ new Set([
6445
6728
  "destination.domain",
6446
6729
  "destination.host",
6447
6730
  "destination.type",
6731
+ "destination.repository",
6448
6732
  "destinationHost",
6449
6733
  "host",
6450
6734
  "hostname",
@@ -6643,6 +6927,63 @@ function hasPiiLikeValue(raw) {
6643
6927
  return false;
6644
6928
  }
6645
6929
  var URL_ACTION_FIELDS = ["command", "cmd", "script", "input", "target", "address", "host", "server", "baseUrl", "base_url", "apiUrl", "api_url", "remote", "repo", "repository", "source", "destination"];
6930
+ function explicitGitHubWriteRepository(command) {
6931
+ if (command.length > 65536 || /[%!]/.test(command)) return void 0;
6932
+ const segments = splitShellSegments(command);
6933
+ if (segments.length !== 1) return void 0;
6934
+ const segment = segments[0];
6935
+ const tokens = [];
6936
+ const token = /[ \t]*(?:'([^'\r\n]*)'|"([^"$`\\\r\n]*)"|([^\s'"$`\\|;&<>(){}\r\n]+))/gy;
6937
+ let offset2 = 0;
6938
+ while (offset2 < segment.length) {
6939
+ token.lastIndex = offset2;
6940
+ const match = token.exec(segment);
6941
+ if (!match || token.lastIndex < segment.length && !/[ \t]/.test(segment[token.lastIndex])) return void 0;
6942
+ if (match[3] !== void 0 && (!/^[a-z0-9_./:@=+-]+$/i.test(match[3]) || match[3].startsWith("@"))) return void 0;
6943
+ if (match[1] !== void 0 && /[|;&<>()$`^]/.test(match[1])) return void 0;
6944
+ tokens.push(match[1] ?? match[2] ?? match[3]);
6945
+ offset2 = token.lastIndex;
6946
+ }
6947
+ if (!/^gh(?:\.exe)?$/i.test(tokens[0] || "")) return void 0;
6948
+ let repository;
6949
+ let resource;
6950
+ let operation;
6951
+ let positional;
6952
+ const canonical = (value) => {
6953
+ const match = /^([a-z0-9][a-z0-9.-]*\.[a-z0-9-]+)\/([a-z0-9][a-z0-9-]*)\/([a-z0-9_.-]+)$/i.exec(value);
6954
+ if (!match || /^(?:\.|\.\.)$/.test(match[3]) || /\.git$/i.test(match[3])) return void 0;
6955
+ return `${match[1]}/${match[2]}/${match[3]}`.toLowerCase();
6956
+ };
6957
+ for (let i2 = 1; i2 < tokens.length; i2++) {
6958
+ const value = tokens[i2];
6959
+ if (value === "-R" || value === "--repo" || value.startsWith("--repo=") || /^-R.+/.test(value)) {
6960
+ if (repository) return void 0;
6961
+ const selector = value === "-R" || value === "--repo" ? tokens[++i2] : value.replace(/^(?:--repo=|-R)/, "");
6962
+ repository = canonical(selector || "");
6963
+ if (!repository) return void 0;
6964
+ } else if (["--body", "-b", "--body-file", "-F", "--title", "-t"].includes(value)) {
6965
+ if (!tokens[++i2]) return void 0;
6966
+ } else if (/^--(?:body|body-file|title)=.+/.test(value)) {
6967
+ continue;
6968
+ } else if (["--edit-last", "--create-if-none", "--delete-last", "--yes"].includes(value)) {
6969
+ continue;
6970
+ } else if (value.startsWith("-")) {
6971
+ return void 0;
6972
+ } else if (!resource) resource = value;
6973
+ else if (!operation) operation = value;
6974
+ else if (!positional) positional = value;
6975
+ else return void 0;
6976
+ }
6977
+ if (!["pr", "issue"].includes(resource || "") || !["comment", "create", "edit"].includes(operation || "")) return void 0;
6978
+ if (positional?.startsWith("https://")) {
6979
+ const target = /^https:\/\/([^/]+)\/([^/]+)\/([^/]+)\/(pull|issues)\/[1-9][0-9]*\/?$/.exec(positional);
6980
+ if (!target || (resource === "pr" ? target[4] !== "pull" : target[4] !== "issues")) return void 0;
6981
+ const fromUrl = canonical(`${target[1]}/${target[2]}/${target[3]}`);
6982
+ if (!fromUrl || repository && repository !== fromUrl) return void 0;
6983
+ repository = fromUrl;
6984
+ } else if (positional && !/^[1-9][0-9]*$/.test(positional)) return void 0;
6985
+ return repository;
6986
+ }
6646
6987
  function extractUrl(args, _argsText) {
6647
6988
  const direct = args.url || args.uri || args.endpoint || args.webhookUrl || args.callbackUrl || args.href || args.link;
6648
6989
  if (typeof direct === "string" && direct.trim()) return direct.trim();
@@ -7118,9 +7459,13 @@ function evaluateActionPolicies(policies, toolName, operation, context = {}, dec
7118
7459
  const ruleMatches = (rule) => candidateOperations.some((candidate) => operationMatchesRule(candidate, rule.operations, matchText, toolCaps)) && (!rule.constraints?.length || rule.constraints.every((c) => evaluateConstraint(c, context)));
7119
7460
  const replacedApprovalPolicies = /* @__PURE__ */ new Set();
7120
7461
  const appliedApprovalExceptions = /* @__PURE__ */ new Set();
7462
+ const roleExceptions = [];
7463
+ const appliedRoleExceptions = /* @__PURE__ */ new Set();
7464
+ let grantingRoleException;
7121
7465
  for (const policy of policies) {
7122
7466
  if (!policyApplies(policy) || policy.stage === "monitor") continue;
7123
7467
  for (const rule of policy.rules) {
7468
+ if (rule.roleException && !roleExceptionError(rule) && ruleMatches(rule)) roleExceptions.push({ policy, rule });
7124
7469
  if (rule.verdict !== "require_approval" || rule.approval?.scope !== "developer" || !ruleMatches(rule)) continue;
7125
7470
  if (approvalExceptionError(rule) || !Array.isArray(rule.approval.replacesPolicyIds)) continue;
7126
7471
  for (const id of rule.approval.replacesPolicyIds) {
@@ -7138,6 +7483,15 @@ function evaluateActionPolicies(policies, toolName, operation, context = {}, dec
7138
7483
  const isMonitorStage = policy.stage === "monitor";
7139
7484
  for (const rule of policy.rules) {
7140
7485
  if (!ruleMatches(rule)) continue;
7486
+ if (!isMonitorStage && policy.resourceType === "machine_role" && policy.enforcement !== "hard" && rule.roleVerb && (rule.verdict === "block" || rule.verdict === "require_approval")) {
7487
+ const roleId = policy.id.startsWith("machine-role:") ? policy.id.split(":")[1] : "";
7488
+ const exception = roleExceptions.find((item) => item.rule.roleException.roleIds.includes(roleId) && item.rule.roleException.verbs.includes(rule.roleVerb));
7489
+ if (exception) {
7490
+ grantingRoleException = exception.policy;
7491
+ appliedRoleExceptions.add(`${exception.policy.name} (${exception.policy.id}) relaxes ${policy.name}: ${rule.roleVerb}`);
7492
+ continue;
7493
+ }
7494
+ }
7141
7495
  if (!isMonitorStage && rule.verdict === "require_approval" && rule.approval?.scope !== "developer" && policy.enforcement !== "hard" && replacedApprovalPolicies.has(policy.id)) {
7142
7496
  appliedApprovalExceptions.add(`${policy.name} (${policy.id})`);
7143
7497
  continue;
@@ -7215,6 +7569,13 @@ function evaluateActionPolicies(policies, toolName, operation, context = {}, dec
7215
7569
  if (monitorMatches.length > 0) {
7216
7570
  worstResult = { ...worstResult, monitorMatches };
7217
7571
  }
7572
+ if (appliedRoleExceptions.size) {
7573
+ worstResult = {
7574
+ ...worstResult,
7575
+ ...worstResult.verdict === "allow" && grantingRoleException ? { policyId: grantingRoleException.id, policyName: grantingRoleException.name, resourceType: grantingRoleException.resourceType } : {},
7576
+ reason: `${worstResult.reason || worstResult.verdict}; explicit role exception: ${[...appliedRoleExceptions].sort().join("; ")}`
7577
+ };
7578
+ }
7218
7579
  if (skippedOrgPolicies.length > 0) {
7219
7580
  worstResult = { ...worstResult, skippedOrgPolicies };
7220
7581
  }
@@ -7282,13 +7643,14 @@ function inferToolContext(toolName, args) {
7282
7643
  operation = methodArg;
7283
7644
  if (args.url) context.url = args.url;
7284
7645
  }
7285
- const detectedUrl = extractUrl(args, argsText);
7646
+ const provenReadUrls = isCodeExecTool && !hasSecretLikeValue(argsText) ? [...provenInlineJavaScriptReadUrls(commandText), ...provenPowerShellQueryUrls(commandText)] : [];
7647
+ const detectedUrl = provenReadUrls[0] || extractUrl(args, argsText);
7286
7648
  if (detectedUrl) {
7287
7649
  context.url = detectedUrl;
7288
7650
  try {
7289
7651
  const parsed = new URL(detectedUrl);
7290
7652
  const hostType = isInternalHost(parsed.hostname) ? "internal" : "external";
7291
- const inbound = operation === "read" || ["GET", "HEAD", "OPTIONS", "SELECT", "SHOW", "DESCRIBE", "EXPLAIN"].includes(operation) || isCodeExecTool && !hasSecretLikeValue(argsText) && provenCompoundDownload(commandText, detectedUrl) || operation === "SHELL" && isCodeExecTool && !hasSecretLikeValue(argsText) && provenInlineJavaScriptRead(commandText, 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);
7653
+ 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);
7292
7654
  context["destination.domain"] = parsed.hostname;
7293
7655
  context["url.risk"] = hostType;
7294
7656
  if (inbound) {
@@ -7316,6 +7678,15 @@ function inferToolContext(toolName, args) {
7316
7678
  context.amount = String(args.amount ?? args.price ?? args.total);
7317
7679
  }
7318
7680
  context["action.kind"] = isCodeExecTool || toolNameLower === "shell" ? "shell" : context.query !== void 0 && operation !== toolName && /^(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE|SHOW|DESCRIBE|EXPLAIN)$/.test(operation) ? "database" : ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].includes(methodArg) ? "http" : context.path !== void 0 && (operation === "read" || operation === "write" || LOCAL_FILE_ACTION_RE.test(toolName) || FILE_REMOVAL_TOOL_RE.test(toolName)) ? "file" : "tool";
7681
+ delete context["destination.repository"];
7682
+ if (isCodeExecTool) {
7683
+ const repository = explicitGitHubWriteRepository(String(args.command || args.cmd || args.code || args.script || args.input || ""));
7684
+ if (repository) {
7685
+ context["destination.repository"] = repository;
7686
+ context["destination.domain"] = repository.split("/")[0];
7687
+ context["destination.type"] = isInternalHost(context["destination.domain"]) ? "internal" : "external";
7688
+ }
7689
+ }
7319
7690
  return { operation, context };
7320
7691
  }
7321
7692
  function inferInventoryToolOperations(toolName, toolActions = []) {
@@ -7357,6 +7728,7 @@ function inventoryToolMatchesActionPolicy(toolName, toolActions, policies) {
7357
7728
  normalizeMachineKind,
7358
7729
  policyTargetsMachineKind,
7359
7730
  resolveApprovalOptions,
7731
+ roleExceptionError,
7360
7732
  ruleNeedsContentFact,
7361
7733
  toolCapabilities
7362
7734
  });
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.34.12"
2
+ "version": "1.34.15"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.34.12",
3
+ "version": "1.34.15",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {