fullcourtdefense-cli 1.34.10 → 1.34.12

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.
@@ -6458,7 +6458,9 @@ var OPERATION_MATCH_SKIP_FIELDS = /* @__PURE__ */ new Set([
6458
6458
  function buildOperationMatchText(_toolName, context) {
6459
6459
  const parts = [];
6460
6460
  for (const [key, value] of Object.entries(context)) {
6461
- if (OPERATION_MATCH_SKIP_FIELDS.has(key)) continue;
6461
+ const fieldWords = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_.-]/);
6462
+ const identifierField = /^(?:id|ids|uuid|uuids|guid|guids)$/.test(fieldWords[fieldWords.length - 1]);
6463
+ if (OPERATION_MATCH_SKIP_FIELDS.has(key) || identifierField) continue;
6462
6464
  if (typeof value !== "string" || value.length === 0 || value.length > 2e3) continue;
6463
6465
  const text = COMMAND_LINE_FIELDS.has(key) ? shellVerbWords(value) : value;
6464
6466
  parts.push(`${key} ${text}`);
@@ -6645,13 +6647,65 @@ function extractUrl(args, _argsText) {
6645
6647
  const direct = args.url || args.uri || args.endpoint || args.webhookUrl || args.callbackUrl || args.href || args.link;
6646
6648
  if (typeof direct === "string" && direct.trim()) return direct.trim();
6647
6649
  for (const field of URL_ACTION_FIELDS) {
6648
- const value = args[field];
6649
- if (typeof value !== "string") continue;
6650
- const found = value.match(/https?:\/\/[^\s"'<>]+/i)?.[0] || value.match(/\b(?:s3|gs|ftp|sftp|smb):\/\/[^\s"'<>]+/i)?.[0];
6650
+ const raw = args[field];
6651
+ if (typeof raw !== "string") continue;
6652
+ const shellField = ["command", "cmd", "script", "input"].includes(field);
6653
+ const value = shellField ? shellUrlActionText(raw) : raw;
6654
+ const firstUrl = (text) => text.match(/https?:\/\/[^\s"'<>]+/i)?.[0] || text.match(/\b(?:s3|gs|ftp|sftp|smb):\/\/[^\s"'<>]+/i)?.[0];
6655
+ 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)) : [];
6656
+ const externalOutbound = outboundUrls.find((url) => {
6657
+ try {
6658
+ return !isInternalHost(new URL(url).hostname);
6659
+ } catch {
6660
+ return false;
6661
+ }
6662
+ });
6663
+ const found = externalOutbound || outboundUrls[0] || firstUrl(value);
6651
6664
  if (found) return found;
6652
6665
  }
6653
6666
  return "";
6654
6667
  }
6668
+ function shellUrlActionText(command) {
6669
+ const segments = splitShellSegments(command);
6670
+ const filtered = segments.map((segment) => {
6671
+ const literal2 = /^(?:@'\r?\n[\s\S]*?\r?\n'@|@"\r?\n[^$`]*?\r?\n"@|'(?:[^']|'')*'|"[^"$`]*")/;
6672
+ const piped = segment.match(literal2);
6673
+ let tail = piped ? segment.slice(piped[0].length).replace(/^\s*\|\s*/, "") : "";
6674
+ if (!piped || !/^\s*\|/.test(segment.slice(piped[0].length))) {
6675
+ const argument = segment.match(/^(Set-Content|Add-Content|Out-File)\s+([\s\S]*?)\s+-(Value|InputObject)\s+([\s\S]+)$/i);
6676
+ const value = argument?.[4].match(literal2);
6677
+ if (!argument || !value) return segment;
6678
+ tail = `${argument[1]} ${argument[2]} ${argument[4].slice(value[0].length)}`;
6679
+ }
6680
+ const writer = tail.match(/^(?:Set-Content|Add-Content|Out-File)\s+([\s\S]+)$/i);
6681
+ if (!writer) return segment;
6682
+ const tokens = writer[1].match(/'[^']*'|"[^"$`]*"|[^\s'"$`|;&(){}]+/g) || [];
6683
+ if (tokens.join(" ").replace(/\s/g, "") !== writer[1].replace(/\s/g, "")) return segment;
6684
+ let target = "";
6685
+ for (let i2 = 0; i2 < tokens.length; i2++) {
6686
+ const token = tokens[i2];
6687
+ if (/^-(?:LiteralPath|Path|FilePath)$/i.test(token)) {
6688
+ if (target || !tokens[i2 + 1]) return segment;
6689
+ target = tokens[++i2];
6690
+ } else if (/^-Encoding$/i.test(token)) {
6691
+ if (!/^(?:utf8|utf8BOM|utf8NoBOM|unicode|ascii|default|oem|bigendianunicode|utf32)$/i.test(tokens[++i2] || "")) return segment;
6692
+ } else if (/^-(?:Force|NoNewline|Append)$/i.test(token)) {
6693
+ continue;
6694
+ } else if (!target && !token.startsWith("-")) target = token;
6695
+ else return segment;
6696
+ }
6697
+ target = target.replace(/^['"]|['"]$/g, "");
6698
+ if (!target || /^[\\/]{2}|[\r\n*?\[\]<>]/.test(target) || /:/.test(target.replace(/^[A-Za-z]:[\\/]/, ""))) return segment;
6699
+ return tail;
6700
+ });
6701
+ const onlyStoredData = segments.every((segment, index) => {
6702
+ if (filtered[index] !== segment) return true;
6703
+ const words = maskQuotedSpans(segment);
6704
+ if (!/[$`|;&(){}<>]/.test(segment) && /^(?:(?:cd|pushd|set-location)\s+[^\r\n]+|(?:pwd|popd|get-location)|git\s+status(?:\s+[^\r\n]+)?)$/i.test(segment)) return true;
6705
+ return !/[$`]/.test(segment) && /^\(Get-Content\s+(?:[A-Za-z]:[\\/])?[\w./\\-]+\s+-Raw\)\.Replace\("",\s*""\)\s*\|\s*Set-Content\s+(?:[A-Za-z]:[\\/])?[\w./\\-]+\s+-Encoding\s+utf8\s*$/i.test(words);
6706
+ });
6707
+ return onlyStoredData && filtered.some((value, index) => value !== segments[index]) ? filtered.join("\n") : command;
6708
+ }
6655
6709
  function splitShellSegments(command, alsoOnPipe = false) {
6656
6710
  const out = [];
6657
6711
  let cur = "";
@@ -6941,7 +6995,7 @@ function classifyShellWords(masked, raw, lead) {
6941
6995
  if (lead === "gh" && /\bgh\s+(?:pr|issue|repo|run|release|workflow)\s+(?:view|list|status|checks|diff|download)\b/.test(value)) return "read";
6942
6996
  if (lead === "gh" && /\bgh\s+auth\s+status\b/.test(value) && !/--show-token|\s-t\b/.test(value)) return "read";
6943
6997
  if (/^(?:gcloud\s+run\s+services\s+(?:describe|logs\s+read)|kubectl\s+(?:get|describe|logs|top|config\s+(?:current-context|get-contexts|view(?!\s+--raw)))|docker\s+(?:ps|images|logs|inspect|version|info)|terraform\s+(?:plan|validate|fmt|show|state\s+list|output)|aws\s+(?:s3\s+ls|sts\s+get-caller-identity)|az\s+account\s+show|npm\s+(?:ls|list|view|outdated|--version)|node\s+--version|python3?\s+--version)\b/.test(value)) return "read";
6944
- if (/\b(rm\s+-[a-z]*r[a-z]*f|rm\s+-[a-z]*f[a-z]*r|del\s+\/[fqs]|remove-item\b.*\b-recurse\b|rmdir\s+\/s)\b/.test(value)) {
6998
+ if (/\b(rm\s+-[a-z]*r[a-z]*f|rm\s+-[a-z]*f[a-z]*r|del\s+\/[fqs]|remove-item\b.*\s-recurse\b|rmdir\s+\/s)\b/.test(value)) {
6945
6999
  return isWorkspaceScopedRemoval(segment) ? "write" : "DELETE";
6946
7000
  }
6947
7001
  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)) {
@@ -6971,10 +7025,49 @@ function classifyShellWords(masked, raw, lead) {
6971
7025
  return "SHELL";
6972
7026
  }
6973
7027
  function classifyShellCommandOperation(command, depth = 0) {
7028
+ if (shellUrlActionText(command) !== command) return "write";
6974
7029
  const segments = splitShellSegments(command);
6975
7030
  if (segments.length === 0) return "SHELL";
6976
7031
  return worstShellOp(segments.map((segment) => classifyShellSegment(segment, depth)), "read");
6977
7032
  }
7033
+ function provenCompoundDownload(command, detectedUrl) {
7034
+ const segments = splitShellSegments(command).filter((segment) => /\b(?:https?|s3|gs|ftp|sftp|smb):\/\//i.test(segment));
7035
+ if (!segments.length || !command.includes(detectedUrl)) return false;
7036
+ return segments.every((segment) => {
7037
+ if (/[$`{}()<>;&]/.test(segment)) return false;
7038
+ const pipeline = splitShellSegments(segment, true);
7039
+ if (pipeline.slice(1).some((part) => !/^(?:Select-Object|Format-Table|Format-List)\s+[\w., *-]+$|^Out-Null$/i.test(part))) return false;
7040
+ const raw = pipeline[0];
7041
+ const pieces = raw.match(/"[^"\r\n]*"|'[^'\r\n]*'|[^\s"']+/g) || [];
7042
+ if (pieces.join("").replace(/\s/g, "") !== raw.replace(/\s/g, "")) return false;
7043
+ const tokens = pieces.map((piece) => piece.replace(/^["']|["']$/g, ""));
7044
+ if (!/^(?:curl(?:\.exe)?|wget(?:\.exe)?|Invoke-RestMethod|Invoke-WebRequest|irm|iwr)$/i.test(tokens.shift() || "")) return false;
7045
+ let urls = 0;
7046
+ for (let i2 = 0; i2 < tokens.length; i2++) {
7047
+ const token = tokens[i2];
7048
+ if (/^https?:\/\//i.test(token)) {
7049
+ urls++;
7050
+ continue;
7051
+ }
7052
+ if (/^-Uri$/i.test(token) && /^https?:\/\//i.test(tokens[i2 + 1] || "")) {
7053
+ urls++;
7054
+ i2++;
7055
+ continue;
7056
+ }
7057
+ if (/^(?:-X|--request|-Method)$/.test(token) && /^(?:GET|HEAD|OPTIONS)$/i.test(tokens[i2 + 1] || "")) {
7058
+ i2++;
7059
+ continue;
7060
+ }
7061
+ if (/^-[sfSLI]+$|^--(?:silent|fail|location|head)$|^-UseBasicParsing$/.test(token)) continue;
7062
+ if (/^-TimeoutSec$/i.test(token) && /^\d+$/.test(tokens[i2 + 1] || "")) {
7063
+ i2++;
7064
+ continue;
7065
+ }
7066
+ return false;
7067
+ }
7068
+ return urls === 1;
7069
+ });
7070
+ }
6978
7071
  function isInternalHost(hostname) {
6979
7072
  const host = hostname.toLowerCase();
6980
7073
  if (!host || host === "localhost" || host === "::1" || host.endsWith(".local") || host.endsWith(".internal")) return true;
@@ -7132,6 +7225,7 @@ function inferToolContext(toolName, args) {
7132
7225
  let operation = toolName;
7133
7226
  const toolNameLower = toolName.toLowerCase();
7134
7227
  const argsText = stringifyArgs(args);
7228
+ const commandText = String(args.command || args.cmd || args.code || args.script || args.input || "");
7135
7229
  const retryCount = Number(args.retryCount ?? args.retries ?? args.attempts ?? 0);
7136
7230
  const fanoutCount = Number(args.fanoutCount ?? args.recipientCount ?? args.childCalls ?? (Array.isArray(args.recipients) ? args.recipients.length : 0));
7137
7231
  const burstCount = Math.max(
@@ -7171,7 +7265,7 @@ function inferToolContext(toolName, args) {
7171
7265
  }
7172
7266
  }
7173
7267
  if (operation === toolName && CODE_EXEC_TOOL_NAME_RE.test(nameWords)) {
7174
- operation = classifyShellCommandOperation(String(args.command || args.code || args.script || args.input || ""));
7268
+ operation = classifyShellCommandOperation(commandText);
7175
7269
  }
7176
7270
  const pathArg = args.path || args.filepath || args.file || args.filename || "";
7177
7271
  if (typeof pathArg === "string" && pathArg.length > 0) {
@@ -7194,7 +7288,7 @@ function inferToolContext(toolName, args) {
7194
7288
  try {
7195
7289
  const parsed = new URL(detectedUrl);
7196
7290
  const hostType = isInternalHost(parsed.hostname) ? "internal" : "external";
7197
- 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);
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);
7198
7292
  context["destination.domain"] = parsed.hostname;
7199
7293
  context["url.risk"] = hostType;
7200
7294
  if (inbound) {
@@ -84,7 +84,7 @@ const FACT_WORDS = [
84
84
  ? `a secret-shaped value was found in the arguments${f['toolArgs.secretKind'] && f['toolArgs.secretKind'] !== 'none' ? ` (${f['toolArgs.secretKind'].replace(/_/g, ' ')})` : ''}`
85
85
  : undefined],
86
86
  ['toolArgs.containsPii', v => v === 'true' ? 'personal data (PII) was found in the arguments' : undefined],
87
- ['destination.type', (v, f) => v === 'external' ? `the destination ${f['destination.domain'] ? `"${f['destination.domain']}" ` : ''}is outside your organisation` : undefined],
87
+ ['destination.type', (v, f) => v === 'external' ? `the destination ${f['destination.domain'] ? `"${f['destination.domain']}" ` : ''}is classified as external; this does not establish whether your organisation trusts it` : undefined],
88
88
  ['destination.domain', (v, f) => f['destination.type'] === 'external' ? undefined : `destination host: ${v}`],
89
89
  ['url.risk', v => v && v !== 'none' && v !== 'low' ? `the URL was rated ${v} risk` : undefined],
90
90
  ['path.sensitive', v => v === 'true' ? 'the path is a sensitive location' : undefined],
@@ -830,7 +830,7 @@ function prepareDeveloperConfirmation(event, call, payload, source) {
830
830
  // Name the rule in the prompt itself: a developer deciding y/n must see WHICH
831
831
  // rule is asking and where to change it — same builder as every block.
832
832
  const ceilingNote = source.askedInsteadOfBlocked
833
- ? 'This policy is set to Block. On a developer workstation it asks you instead of blocking — a wrong Ask costs one click, a wrong block costs your afternoon. An admin can mark the policy "Hard block" in the console to stop this without asking.'
833
+ ? 'This policy is set to Block. Because it is not marked "Hard block", your workstation requires your approval before continuing. An admin can review the matching rule and its scope in the console.'
834
834
  : undefined;
835
835
  const explained = source.where === 'local-safety' && source.localFinding
836
836
  ? explainLocalSafetyStop('ask', event, call, source.localFinding)
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.34.10"
2
+ "version": "1.34.12"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.34.10",
3
+ "version": "1.34.12",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {