fullcourtdefense-cli 1.34.11 → 1.34.14

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.
@@ -5738,6 +5738,14 @@ function parse3(input, options) {
5738
5738
  }
5739
5739
 
5740
5740
  // src/actionPolicyEngine.ts
5741
+ function isFirestoreQueryUrl(target) {
5742
+ try {
5743
+ const url = new URL(target);
5744
+ return url.origin === "https://firestore.googleapis.com" && !url.username && !url.password && !url.search && !url.hash && /^\/v1\/projects\/[^/]+\/databases\/[^/]+\/documents(?:\/[^/]+)*:runQuery$/.test(url.pathname);
5745
+ } catch {
5746
+ return false;
5747
+ }
5748
+ }
5741
5749
  function provenJavaScriptReadUrls(code) {
5742
5750
  if (code.length > 32768) return [];
5743
5751
  const literal2 = (value) => ({ kind: "literal", value });
@@ -5758,6 +5766,8 @@ function provenJavaScriptReadUrls(code) {
5758
5766
  const readProperty = (value, key) => {
5759
5767
  if (["constructor", "__proto__", "prototype"].includes(key)) return fail();
5760
5768
  if (value.kind === "object") return value.fields.get(key) || literal2(void 0);
5769
+ if (value.kind === "array" && key === "map") return { kind: "arrayMap", items: value.items };
5770
+ if (value.kind === "promise" && key === "catch") return callable("promise.catch");
5761
5771
  if (value.kind === "response") return ["json", "text"].includes(key) ? callable(`response.${key}`) : { kind: "data" };
5762
5772
  if (value.kind === "credential" && key === "trim") return callable("credential.trim");
5763
5773
  if (value.kind === "string" || value.kind === "literal" && typeof value.value === "string") {
@@ -5822,24 +5832,97 @@ function provenJavaScriptReadUrls(code) {
5822
5832
  const left = ev(node.left);
5823
5833
  const right = ev(node.right);
5824
5834
  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);
5835
+ if (left.kind === "literal" && right.kind === "literal" && typeof left.value === "string" && typeof right.value === "string") {
5836
+ if (left.value.length + right.value.length > 32768) return fail();
5837
+ return literal2(left.value + right.value);
5838
+ }
5826
5839
  if (left.kind === "literal" && left.value === "Bearer " && right.kind === "credential") return { kind: "authorization" };
5827
5840
  return fail();
5828
5841
  }
5829
5842
  case "ArrowFunctionExpression":
5830
5843
  case "FunctionExpression":
5831
- if (node.params.length || node.generator) return fail();
5844
+ if (node.params.some((param) => param.type !== "Identifier") || node.generator) return fail();
5832
5845
  return { kind: "function", node, env: new Map(env) };
5846
+ case "ForOfStatement": {
5847
+ const items = ev(node.right);
5848
+ 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();
5849
+ for (const item of items.items) {
5850
+ const scope = new Map(env);
5851
+ const id = node.left.declarations[0].id;
5852
+ if (id.type === "Identifier") scope.set(id.name, item);
5853
+ else if (id.type === "ArrayPattern" && item.kind === "array" && id.elements.length === item.items.length) {
5854
+ id.elements.forEach((element, index) => {
5855
+ if (element?.type !== "Identifier") fail();
5856
+ scope.set(element.name, item.items[index]);
5857
+ });
5858
+ } else return fail();
5859
+ evaluate(node.body, scope, depth + 1);
5860
+ }
5861
+ return literal2(void 0);
5862
+ }
5863
+ case "AssignmentExpression": {
5864
+ 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();
5865
+ return literal2(void 0);
5866
+ }
5867
+ case "NewExpression": {
5868
+ const constructor = ev(node.callee);
5869
+ if (constructor.kind !== "call" || constructor.name !== "GoogleAuth" || node.arguments.length !== 1) return fail();
5870
+ const options = ev(node.arguments[0]);
5871
+ if (options.kind !== "object" || options.fields.size !== 1) return fail();
5872
+ const scopes = options.fields.get("scopes");
5873
+ if (scopes?.kind !== "array" || scopes.items.length !== 1 || string(scopes.items[0]) !== "https://www.googleapis.com/auth/datastore") return fail();
5874
+ return object([["getClient", callable("google.getClient")]]);
5875
+ }
5833
5876
  case "ReturnStatement":
5834
5877
  return node.argument ? ev(node.argument) : literal2(void 0);
5835
5878
  case "CallExpression": {
5836
5879
  const callee = ev(node.callee);
5837
5880
  const args = node.arguments.map(ev);
5838
5881
  if (callee.kind === "function") {
5839
- if (args.length) return fail();
5840
- return evaluate(callee.node.body, new Map(callee.env), depth + 1);
5882
+ if (args.length !== callee.node.params.length) return fail();
5883
+ const scope = new Map(callee.env);
5884
+ args.forEach((value, index) => scope.set(callee.node.params[index].name, value));
5885
+ const result = evaluate(callee.node.body, scope, depth + 1);
5886
+ return callee.node.async ? { kind: "promise" } : result;
5887
+ }
5888
+ if (callee.kind === "arrayMap") {
5889
+ if (args.length !== 1 || args[0].kind !== "function" || args[0].node.params.length !== 1 || args[0].node.async) return fail();
5890
+ const fn = args[0];
5891
+ return { kind: "array", items: callee.items.map((item) => {
5892
+ const scope = new Map(fn.env);
5893
+ scope.set(fn.node.params[0].name, item);
5894
+ return evaluate(fn.node.body, scope, depth + 1);
5895
+ }) };
5841
5896
  }
5842
5897
  if (callee.kind !== "call") return fail();
5898
+ if (callee.name === "google.getClient") {
5899
+ if (args.length) return fail();
5900
+ return object([["request", callable("google.request")]]);
5901
+ }
5902
+ if (callee.name === "google.request") {
5903
+ if (args.length !== 1 || args[0].kind !== "object") return fail();
5904
+ const fields = args[0].fields;
5905
+ if ([...fields.keys()].some((key) => !["url", "method", "data"].includes(key))) return fail();
5906
+ const target = string(fields.get("url") || fail());
5907
+ if (!isFirestoreQueryUrl(target) || string(fields.get("method") || fail()).toUpperCase() !== "POST") return fail();
5908
+ const body = fields.get("data");
5909
+ const passive = (value) => {
5910
+ if (--budget < 0) return false;
5911
+ if (value.kind === "literal") return true;
5912
+ if (value.kind === "object") return [...value.fields.values()].every(passive);
5913
+ return value.kind === "array" && value.items.every(passive);
5914
+ };
5915
+ if (body?.kind !== "object" || !body.fields.has("structuredQuery") || [...body.fields.keys()].some((key) => !["structuredQuery", "readTime", "transaction"].includes(key)) || !passive(body)) return fail();
5916
+ urls.push(target);
5917
+ return { kind: "data" };
5918
+ }
5919
+ if (callee.name === "promise.catch") {
5920
+ if (args.length !== 1 || args[0].kind !== "function" || args[0].node.params.length !== 1) return fail();
5921
+ const scope = new Map(args[0].env);
5922
+ scope.set(args[0].node.params[0].name, { kind: "data" });
5923
+ evaluate(args[0].node.body, scope, depth + 1);
5924
+ return { kind: "data" };
5925
+ }
5843
5926
  if (callee.name === "fetch") {
5844
5927
  if (args.length < 1 || args.length > 2) return fail();
5845
5928
  const target = string(args[0]);
@@ -5866,6 +5949,7 @@ function provenJavaScriptReadUrls(code) {
5866
5949
  return { kind: "response" };
5867
5950
  }
5868
5951
  if (callee.name === "require") {
5952
+ if (args.length === 1 && string(args[0]) === "google-auth-library") return object([["GoogleAuth", callable("GoogleAuth")]]);
5869
5953
  if (args.length !== 1 || !["node:child_process", "child_process"].includes(string(args[0]))) return fail();
5870
5954
  return object([["execFileSync", callable("execFileSync")]]);
5871
5955
  }
@@ -5888,7 +5972,7 @@ function provenJavaScriptReadUrls(code) {
5888
5972
  if (args.length !== 1 || !jsonSafe(args[0])) return fail();
5889
5973
  return { kind: "string" };
5890
5974
  }
5891
- if (callee.name === "console.log") return literal2(void 0);
5975
+ if (callee.name === "console.log" || callee.name === "console.error") return literal2(void 0);
5892
5976
  if (callee.name === "string.trim") {
5893
5977
  if (args.length) return fail();
5894
5978
  return { kind: "string" };
@@ -5913,14 +5997,14 @@ function provenJavaScriptReadUrls(code) {
5913
5997
  ["fetch", callable("fetch")],
5914
5998
  ["require", callable("require")],
5915
5999
  ["JSON", object([["stringify", callable("JSON.stringify")]])],
5916
- ["console", object([["log", callable("console.log")]])]
6000
+ ["console", object([["log", callable("console.log")], ["error", callable("console.error")]])]
5917
6001
  ]));
5918
6002
  return urls;
5919
6003
  } catch {
5920
6004
  return [];
5921
6005
  }
5922
6006
  }
5923
- function provenInlineJavaScriptRead(command, detectedUrl) {
6007
+ function provenInlineJavaScriptReadUrls(command) {
5924
6008
  const urls = [];
5925
6009
  for (const segment of splitShellSegments(command)) {
5926
6010
  const here = segment.match(/^\s*@'\r?\n([\s\S]*?)\r?\n'@\s*\|\s*node(?:\.exe)?(?:\s+-)?\s*$/i);
@@ -5928,14 +6012,197 @@ function provenInlineJavaScriptRead(command, detectedUrl) {
5928
6012
  const code = here?.[1] ?? inline?.[1];
5929
6013
  if (code !== void 0) {
5930
6014
  const proven = provenJavaScriptReadUrls(code);
5931
- if (!proven.length) return false;
6015
+ if (!proven.length) return [];
5932
6016
  urls.push(...proven);
5933
6017
  } else {
5934
6018
  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;
6019
+ if (!(SHELL_READ_PROGRAMS.has(lead) || ["git", "gh", "cd", "set-location"].includes(lead)) || classifyShellCommandOperation(segment) !== "read" || /https?:\/\//i.test(segment)) return [];
6020
+ }
6021
+ }
6022
+ return urls;
6023
+ }
6024
+ function provenPowerShellQueryUrls(command) {
6025
+ if (command.length > 32768) return [];
6026
+ 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;
6027
+ for (let count = 0; count < 8 && prefix.test(command); count++) command = command.replace(prefix, "");
6028
+ const credential = Object.freeze({ credential: 1 });
6029
+ const authorization = Object.freeze({ authorization: 1 });
6030
+ const urls = [];
6031
+ const env = /* @__PURE__ */ new Map();
6032
+ const tokens = [];
6033
+ const fail = () => {
6034
+ throw new Error("Unsupported PowerShell query");
6035
+ };
6036
+ const lexer = /[ \t]+|\r?\n|@\{|@\(|'(?:[^']|'')*'|"[^"`]*"|\$[A-Za-z_][A-Za-z0-9_]*|-[A-Za-z]+|[A-Za-z_][A-Za-z0-9_-]*|\d+|[{}();=+|,]/gy;
6037
+ let offset2 = 0;
6038
+ let budget = 8e3;
6039
+ let pos = 0;
6040
+ const take = () => {
6041
+ if (--budget < 0 || pos >= tokens.length) return fail();
6042
+ return tokens[pos++];
6043
+ };
6044
+ const peek = () => tokens[pos];
6045
+ const expect = (token) => {
6046
+ if (take().toLowerCase() !== token.toLowerCase()) fail();
6047
+ };
6048
+ const separators = () => {
6049
+ while (peek() === ";") take();
6050
+ };
6051
+ const passive = (value) => {
6052
+ if (--budget < 0 || value === credential || value === authorization) return false;
6053
+ if (typeof value !== "object") return true;
6054
+ return Object.values(value).every(passive);
6055
+ };
6056
+ const expression = () => {
6057
+ const token = take();
6058
+ let value;
6059
+ if (token.startsWith("'")) value = token.slice(1, -1).replace(/''/g, "'");
6060
+ else if (token.startsWith('"')) {
6061
+ const text = token.slice(1, -1);
6062
+ const bearer = text.match(/^Bearer (\$[A-Za-z_][A-Za-z0-9_]*)$/);
6063
+ if (bearer && env.get(bearer[1].toLowerCase()) === credential) value = authorization;
6064
+ else {
6065
+ if (/[$`]/.test(text)) return fail();
6066
+ value = text;
6067
+ }
6068
+ } else if (token.startsWith("$")) value = env.get(token.toLowerCase()) ?? fail();
6069
+ else if (/^\d+$/.test(token)) value = Number(token);
6070
+ else if (token === "(") {
6071
+ value = expression();
6072
+ expect(")");
6073
+ } else if (token === "@(") {
6074
+ const items = [];
6075
+ separators();
6076
+ while (peek() !== ")") {
6077
+ items.push(expression());
6078
+ separators();
6079
+ if (peek() !== ")") expect(",");
6080
+ }
6081
+ expect(")");
6082
+ value = items;
6083
+ } else if (token === "@{") {
6084
+ const fields = /* @__PURE__ */ Object.create(null);
6085
+ separators();
6086
+ while (peek() !== "}") {
6087
+ const key = take();
6088
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || Object.hasOwn(fields, key)) return fail();
6089
+ expect("=");
6090
+ fields[key] = expression();
6091
+ if (peek() !== "}") expect(";");
6092
+ separators();
6093
+ }
6094
+ expect("}");
6095
+ value = fields;
6096
+ } else if (token.toLowerCase() === "if") {
6097
+ expect("(");
6098
+ const left = expression();
6099
+ expect("-eq");
6100
+ const right = expression();
6101
+ expect(")");
6102
+ if (typeof left === "object" || typeof right === "object") return fail();
6103
+ expect("{");
6104
+ const yes = expression();
6105
+ expect("}");
6106
+ expect("else");
6107
+ expect("{");
6108
+ const no = expression();
6109
+ expect("}");
6110
+ value = left === right ? yes : no;
6111
+ } else if (token.toLowerCase() === "gcloud") {
6112
+ expect("auth");
6113
+ expect("print-access-token");
6114
+ value = credential;
6115
+ } else return fail();
6116
+ while (peek() === "+") {
6117
+ take();
6118
+ const right = expression();
6119
+ if (typeof value !== "string" || typeof right !== "string" || value.length + right.length > 32768) return fail();
6120
+ value += right;
6121
+ }
6122
+ if (peek() === "|") {
6123
+ take();
6124
+ expect("ConvertTo-Json");
6125
+ expect("-Depth");
6126
+ const depth = take();
6127
+ if (!/^\d+$/.test(depth) || Number(depth) > 100 || !passive(value)) return fail();
6128
+ }
6129
+ return value;
6130
+ };
6131
+ const statement = () => {
6132
+ if (peek()?.startsWith("$")) {
6133
+ const name = take().toLowerCase();
6134
+ expect("=");
6135
+ if (["$env", "$psdefaultparametervalues", "$executioncontext", "$erroractionpreference", "$home", "$profile"].includes(name)) return fail();
6136
+ env.set(name, expression());
6137
+ return;
6138
+ }
6139
+ if (peek()?.toLowerCase() === "foreach") {
6140
+ take();
6141
+ expect("(");
6142
+ const name = take().toLowerCase();
6143
+ if (!/^\$[a-z_][a-z0-9_]*$/.test(name)) return fail();
6144
+ expect("in");
6145
+ const items = expression();
6146
+ expect(")");
6147
+ expect("{");
6148
+ if (!Array.isArray(items) || !items.length || items.length > 100) return fail();
6149
+ const start = pos;
6150
+ for (const item of items) {
6151
+ env.set(name, item);
6152
+ pos = start;
6153
+ separators();
6154
+ while (peek() !== "}") {
6155
+ statement();
6156
+ if (peek() !== "}") expect(";");
6157
+ separators();
6158
+ }
6159
+ expect("}");
6160
+ }
6161
+ return;
6162
+ }
6163
+ expect("Invoke-RestMethod");
6164
+ const options = /* @__PURE__ */ new Map();
6165
+ while (peek()?.startsWith("-")) {
6166
+ const flag = take().toLowerCase();
6167
+ if (options.has(flag)) return fail();
6168
+ options.set(flag, flag === "-method" ? take() : expression());
6169
+ }
6170
+ if ([...options.keys()].some((key) => !["-method", "-uri", "-headers", "-contenttype", "-body"].includes(key))) return fail();
6171
+ const target = options.get("-uri");
6172
+ if (typeof target !== "string") return fail();
6173
+ if (!isFirestoreQueryUrl(target) || String(options.get("-method")).toUpperCase() !== "POST" || options.get("-contenttype") !== "application/json") return fail();
6174
+ const headers = options.get("-headers");
6175
+ const body = options.get("-body");
6176
+ if (!headers || typeof headers !== "object" || Array.isArray(headers) || Object.keys(headers).length !== 1 || headers.Authorization !== authorization) return fail();
6177
+ 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();
6178
+ urls.push(target);
6179
+ if (peek() === "|") {
6180
+ take();
6181
+ expect("ConvertTo-Json");
6182
+ expect("-Depth");
6183
+ if (!/^\d+$/.test(take())) fail();
6184
+ }
6185
+ };
6186
+ try {
6187
+ while (offset2 < command.length) {
6188
+ lexer.lastIndex = offset2;
6189
+ const match = lexer.exec(command);
6190
+ if (!match) return [];
6191
+ offset2 = lexer.lastIndex;
6192
+ const token = match[0];
6193
+ if (/^[ \t]+$/.test(token)) continue;
6194
+ tokens.push(/\r?\n/.test(token) && token.length <= 2 ? ";" : token);
6195
+ }
6196
+ separators();
6197
+ while (pos < tokens.length) {
6198
+ statement();
6199
+ if (pos < tokens.length) expect(";");
6200
+ separators();
5936
6201
  }
6202
+ return urls;
6203
+ } catch {
6204
+ return [];
5937
6205
  }
5938
- return urls.includes(detectedUrl);
5939
6206
  }
5940
6207
  function approvalExceptionError(rule) {
5941
6208
  const ids = rule.approval?.replacesPolicyIds;
@@ -6445,6 +6712,7 @@ var OPERATION_MATCH_SKIP_FIELDS = /* @__PURE__ */ new Set([
6445
6712
  "destination.domain",
6446
6713
  "destination.host",
6447
6714
  "destination.type",
6715
+ "destination.repository",
6448
6716
  "destinationHost",
6449
6717
  "host",
6450
6718
  "hostname",
@@ -6458,7 +6726,9 @@ var OPERATION_MATCH_SKIP_FIELDS = /* @__PURE__ */ new Set([
6458
6726
  function buildOperationMatchText(_toolName, context) {
6459
6727
  const parts = [];
6460
6728
  for (const [key, value] of Object.entries(context)) {
6461
- if (OPERATION_MATCH_SKIP_FIELDS.has(key)) continue;
6729
+ const fieldWords = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_.-]/);
6730
+ const identifierField = /^(?:id|ids|uuid|uuids|guid|guids)$/.test(fieldWords[fieldWords.length - 1]);
6731
+ if (OPERATION_MATCH_SKIP_FIELDS.has(key) || identifierField) continue;
6462
6732
  if (typeof value !== "string" || value.length === 0 || value.length > 2e3) continue;
6463
6733
  const text = COMMAND_LINE_FIELDS.has(key) ? shellVerbWords(value) : value;
6464
6734
  parts.push(`${key} ${text}`);
@@ -6641,14 +6911,81 @@ function hasPiiLikeValue(raw) {
6641
6911
  return false;
6642
6912
  }
6643
6913
  var URL_ACTION_FIELDS = ["command", "cmd", "script", "input", "target", "address", "host", "server", "baseUrl", "base_url", "apiUrl", "api_url", "remote", "repo", "repository", "source", "destination"];
6914
+ function explicitGitHubWriteRepository(command) {
6915
+ if (command.length > 65536 || /[%!]/.test(command)) return void 0;
6916
+ const segments = splitShellSegments(command);
6917
+ if (segments.length !== 1) return void 0;
6918
+ const segment = segments[0];
6919
+ const tokens = [];
6920
+ const token = /[ \t]*(?:'([^'\r\n]*)'|"([^"$`\\\r\n]*)"|([^\s'"$`\\|;&<>(){}\r\n]+))/gy;
6921
+ let offset2 = 0;
6922
+ while (offset2 < segment.length) {
6923
+ token.lastIndex = offset2;
6924
+ const match = token.exec(segment);
6925
+ if (!match || token.lastIndex < segment.length && !/[ \t]/.test(segment[token.lastIndex])) return void 0;
6926
+ if (match[3] !== void 0 && (!/^[a-z0-9_./:@=+-]+$/i.test(match[3]) || match[3].startsWith("@"))) return void 0;
6927
+ if (match[1] !== void 0 && /[|;&<>()$`^]/.test(match[1])) return void 0;
6928
+ tokens.push(match[1] ?? match[2] ?? match[3]);
6929
+ offset2 = token.lastIndex;
6930
+ }
6931
+ if (!/^gh(?:\.exe)?$/i.test(tokens[0] || "")) return void 0;
6932
+ let repository;
6933
+ let resource;
6934
+ let operation;
6935
+ let positional;
6936
+ const canonical = (value) => {
6937
+ const match = /^([a-z0-9][a-z0-9.-]*\.[a-z0-9-]+)\/([a-z0-9][a-z0-9-]*)\/([a-z0-9_.-]+)$/i.exec(value);
6938
+ if (!match || /^(?:\.|\.\.)$/.test(match[3]) || /\.git$/i.test(match[3])) return void 0;
6939
+ return `${match[1]}/${match[2]}/${match[3]}`.toLowerCase();
6940
+ };
6941
+ for (let i2 = 1; i2 < tokens.length; i2++) {
6942
+ const value = tokens[i2];
6943
+ if (value === "-R" || value === "--repo" || value.startsWith("--repo=") || /^-R.+/.test(value)) {
6944
+ if (repository) return void 0;
6945
+ const selector = value === "-R" || value === "--repo" ? tokens[++i2] : value.replace(/^(?:--repo=|-R)/, "");
6946
+ repository = canonical(selector || "");
6947
+ if (!repository) return void 0;
6948
+ } else if (["--body", "-b", "--body-file", "-F", "--title", "-t"].includes(value)) {
6949
+ if (!tokens[++i2]) return void 0;
6950
+ } else if (/^--(?:body|body-file|title)=.+/.test(value)) {
6951
+ continue;
6952
+ } else if (["--edit-last", "--create-if-none", "--delete-last", "--yes"].includes(value)) {
6953
+ continue;
6954
+ } else if (value.startsWith("-")) {
6955
+ return void 0;
6956
+ } else if (!resource) resource = value;
6957
+ else if (!operation) operation = value;
6958
+ else if (!positional) positional = value;
6959
+ else return void 0;
6960
+ }
6961
+ if (!["pr", "issue"].includes(resource || "") || !["comment", "create", "edit"].includes(operation || "")) return void 0;
6962
+ if (positional?.startsWith("https://")) {
6963
+ const target = /^https:\/\/([^/]+)\/([^/]+)\/([^/]+)\/(pull|issues)\/[1-9][0-9]*\/?$/.exec(positional);
6964
+ if (!target || (resource === "pr" ? target[4] !== "pull" : target[4] !== "issues")) return void 0;
6965
+ const fromUrl = canonical(`${target[1]}/${target[2]}/${target[3]}`);
6966
+ if (!fromUrl || repository && repository !== fromUrl) return void 0;
6967
+ repository = fromUrl;
6968
+ } else if (positional && !/^[1-9][0-9]*$/.test(positional)) return void 0;
6969
+ return repository;
6970
+ }
6644
6971
  function extractUrl(args, _argsText) {
6645
6972
  const direct = args.url || args.uri || args.endpoint || args.webhookUrl || args.callbackUrl || args.href || args.link;
6646
6973
  if (typeof direct === "string" && direct.trim()) return direct.trim();
6647
6974
  for (const field of URL_ACTION_FIELDS) {
6648
6975
  const raw = args[field];
6649
6976
  if (typeof raw !== "string") continue;
6650
- const value = ["command", "cmd", "script", "input"].includes(field) ? shellUrlActionText(raw) : raw;
6651
- const found = value.match(/https?:\/\/[^\s"'<>]+/i)?.[0] || value.match(/\b(?:s3|gs|ftp|sftp|smb):\/\/[^\s"'<>]+/i)?.[0];
6977
+ const shellField = ["command", "cmd", "script", "input"].includes(field);
6978
+ const value = shellField ? shellUrlActionText(raw) : raw;
6979
+ const firstUrl = (text) => text.match(/https?:\/\/[^\s"'<>]+/i)?.[0] || text.match(/\b(?:s3|gs|ftp|sftp|smb):\/\/[^\s"'<>]+/i)?.[0];
6980
+ 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)) : [];
6981
+ const externalOutbound = outboundUrls.find((url) => {
6982
+ try {
6983
+ return !isInternalHost(new URL(url).hostname);
6984
+ } catch {
6985
+ return false;
6986
+ }
6987
+ });
6988
+ const found = externalOutbound || outboundUrls[0] || firstUrl(value);
6652
6989
  if (found) return found;
6653
6990
  }
6654
6991
  return "";
@@ -7018,6 +7355,44 @@ function classifyShellCommandOperation(command, depth = 0) {
7018
7355
  if (segments.length === 0) return "SHELL";
7019
7356
  return worstShellOp(segments.map((segment) => classifyShellSegment(segment, depth)), "read");
7020
7357
  }
7358
+ function provenCompoundDownload(command, detectedUrl) {
7359
+ const segments = splitShellSegments(command).filter((segment) => /\b(?:https?|s3|gs|ftp|sftp|smb):\/\//i.test(segment));
7360
+ if (!segments.length || !command.includes(detectedUrl)) return false;
7361
+ return segments.every((segment) => {
7362
+ if (/[$`{}()<>;&]/.test(segment)) return false;
7363
+ const pipeline = splitShellSegments(segment, true);
7364
+ if (pipeline.slice(1).some((part) => !/^(?:Select-Object|Format-Table|Format-List)\s+[\w., *-]+$|^Out-Null$/i.test(part))) return false;
7365
+ const raw = pipeline[0];
7366
+ const pieces = raw.match(/"[^"\r\n]*"|'[^'\r\n]*'|[^\s"']+/g) || [];
7367
+ if (pieces.join("").replace(/\s/g, "") !== raw.replace(/\s/g, "")) return false;
7368
+ const tokens = pieces.map((piece) => piece.replace(/^["']|["']$/g, ""));
7369
+ if (!/^(?:curl(?:\.exe)?|wget(?:\.exe)?|Invoke-RestMethod|Invoke-WebRequest|irm|iwr)$/i.test(tokens.shift() || "")) return false;
7370
+ let urls = 0;
7371
+ for (let i2 = 0; i2 < tokens.length; i2++) {
7372
+ const token = tokens[i2];
7373
+ if (/^https?:\/\//i.test(token)) {
7374
+ urls++;
7375
+ continue;
7376
+ }
7377
+ if (/^-Uri$/i.test(token) && /^https?:\/\//i.test(tokens[i2 + 1] || "")) {
7378
+ urls++;
7379
+ i2++;
7380
+ continue;
7381
+ }
7382
+ if (/^(?:-X|--request|-Method)$/.test(token) && /^(?:GET|HEAD|OPTIONS)$/i.test(tokens[i2 + 1] || "")) {
7383
+ i2++;
7384
+ continue;
7385
+ }
7386
+ if (/^-[sfSLI]+$|^--(?:silent|fail|location|head)$|^-UseBasicParsing$/.test(token)) continue;
7387
+ if (/^-TimeoutSec$/i.test(token) && /^\d+$/.test(tokens[i2 + 1] || "")) {
7388
+ i2++;
7389
+ continue;
7390
+ }
7391
+ return false;
7392
+ }
7393
+ return urls === 1;
7394
+ });
7395
+ }
7021
7396
  function isInternalHost(hostname) {
7022
7397
  const host = hostname.toLowerCase();
7023
7398
  if (!host || host === "localhost" || host === "::1" || host.endsWith(".local") || host.endsWith(".internal")) return true;
@@ -7175,6 +7550,7 @@ function inferToolContext(toolName, args) {
7175
7550
  let operation = toolName;
7176
7551
  const toolNameLower = toolName.toLowerCase();
7177
7552
  const argsText = stringifyArgs(args);
7553
+ const commandText = String(args.command || args.cmd || args.code || args.script || args.input || "");
7178
7554
  const retryCount = Number(args.retryCount ?? args.retries ?? args.attempts ?? 0);
7179
7555
  const fanoutCount = Number(args.fanoutCount ?? args.recipientCount ?? args.childCalls ?? (Array.isArray(args.recipients) ? args.recipients.length : 0));
7180
7556
  const burstCount = Math.max(
@@ -7214,7 +7590,7 @@ function inferToolContext(toolName, args) {
7214
7590
  }
7215
7591
  }
7216
7592
  if (operation === toolName && CODE_EXEC_TOOL_NAME_RE.test(nameWords)) {
7217
- operation = classifyShellCommandOperation(String(args.command || args.code || args.script || args.input || ""));
7593
+ operation = classifyShellCommandOperation(commandText);
7218
7594
  }
7219
7595
  const pathArg = args.path || args.filepath || args.file || args.filename || "";
7220
7596
  if (typeof pathArg === "string" && pathArg.length > 0) {
@@ -7231,13 +7607,14 @@ function inferToolContext(toolName, args) {
7231
7607
  operation = methodArg;
7232
7608
  if (args.url) context.url = args.url;
7233
7609
  }
7234
- const detectedUrl = extractUrl(args, argsText);
7610
+ const provenReadUrls = isCodeExecTool && !hasSecretLikeValue(argsText) ? [...provenInlineJavaScriptReadUrls(commandText), ...provenPowerShellQueryUrls(commandText)] : [];
7611
+ const detectedUrl = provenReadUrls[0] || extractUrl(args, argsText);
7235
7612
  if (detectedUrl) {
7236
7613
  context.url = detectedUrl;
7237
7614
  try {
7238
7615
  const parsed = new URL(detectedUrl);
7239
7616
  const hostType = isInternalHost(parsed.hostname) ? "internal" : "external";
7240
- const inbound = operation === "read" || ["GET", "HEAD", "OPTIONS", "SELECT", "SHOW", "DESCRIBE", "EXPLAIN"].includes(operation) || operation === "SHELL" && isCodeExecTool && !hasSecretLikeValue(argsText) && provenInlineJavaScriptRead(String(args.command || args.code || args.script || args.input || ""), 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);
7617
+ 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);
7241
7618
  context["destination.domain"] = parsed.hostname;
7242
7619
  context["url.risk"] = hostType;
7243
7620
  if (inbound) {
@@ -7265,6 +7642,15 @@ function inferToolContext(toolName, args) {
7265
7642
  context.amount = String(args.amount ?? args.price ?? args.total);
7266
7643
  }
7267
7644
  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";
7645
+ delete context["destination.repository"];
7646
+ if (isCodeExecTool) {
7647
+ const repository = explicitGitHubWriteRepository(String(args.command || args.cmd || args.code || args.script || args.input || ""));
7648
+ if (repository) {
7649
+ context["destination.repository"] = repository;
7650
+ context["destination.domain"] = repository.split("/")[0];
7651
+ context["destination.type"] = isInternalHost(context["destination.domain"]) ? "internal" : "external";
7652
+ }
7653
+ }
7268
7654
  return { operation, context };
7269
7655
  }
7270
7656
  function inferInventoryToolOperations(toolName, toolActions = []) {
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.34.11"
2
+ "version": "1.34.14"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.34.11",
3
+ "version": "1.34.14",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {