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.
- package/dist/actionPolicyEngine.js +101 -7
- package/dist/blockExplanation.js +1 -1
- package/dist/commands/hook.js +1 -1
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
6649
|
-
if (typeof
|
|
6650
|
-
const
|
|
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.*\
|
|
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(
|
|
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(
|
|
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) {
|
package/dist/blockExplanation.js
CHANGED
|
@@ -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
|
|
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],
|
package/dist/commands/hook.js
CHANGED
|
@@ -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.
|
|
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