fullcourtdefense-cli 1.35.1 → 1.35.3
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 +117 -12
- package/dist/approvalPrompt.d.ts +3 -0
- package/dist/approvalPrompt.js +20 -1
- package/dist/blockExplanation.js +21 -1
- package/dist/commandNarrative.d.ts +29 -0
- package/dist/commandNarrative.js +602 -0
- package/dist/commands/deterministicGuard.js +124 -6
- package/dist/commands/hook.js +8 -0
- package/dist/commands/workloadProtect.d.ts +10 -0
- package/dist/commands/workloadProtect.js +27 -4
- package/dist/detectorBaseline.d.ts +2 -2
- package/dist/detectorBaseline.js +2 -2
- package/dist/devConfirm.js +61 -15
- package/dist/index.js +4 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -6925,6 +6925,13 @@ function plausibleTokenBody(body) {
|
|
|
6925
6925
|
}
|
|
6926
6926
|
var PLACEHOLDER_WORD_RE = /example|sample|placeholder|redacted|dummy|changeme|fake|xxxx/i;
|
|
6927
6927
|
var REPEATED_CHUNK_RE = /(.{3,})\1{2,}/;
|
|
6928
|
+
var PUBLISHED_EXAMPLE_VALUES = /* @__PURE__ */ new Set([
|
|
6929
|
+
"JBSWY3DPEHPK3PXP",
|
|
6930
|
+
"GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
|
|
6931
|
+
"12345678901234567890",
|
|
6932
|
+
["AKIA", "IOSFODNN7", "EXAMPLE"].join(""),
|
|
6933
|
+
["wJalrXUtnFEMI/K7MDENG/bPxRfiCY", "EXAMPLEKEY"].join("")
|
|
6934
|
+
]);
|
|
6928
6935
|
function isHighEntropySecretToken(token, allowWordSlug = false) {
|
|
6929
6936
|
if (token.length < 24 || token.length > 512) return false;
|
|
6930
6937
|
const assignment = /^-{0,2}[A-Za-z_][\w.-]*=(.+)$/.exec(token);
|
|
@@ -6933,11 +6940,12 @@ function isHighEntropySecretToken(token, allowWordSlug = false) {
|
|
|
6933
6940
|
if (!/^[A-Za-z0-9._~+/=-]+$/.test(token)) return false;
|
|
6934
6941
|
if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(token)) return false;
|
|
6935
6942
|
if (/^[0-9a-f]+$/i.test(token)) return false;
|
|
6936
|
-
if (
|
|
6943
|
+
if (/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(token) && (token.length === 36 || /[^0-9a-f]/i.test(token[token.length - 37] || ""))) return false;
|
|
6937
6944
|
if (/^sha(?:1|256|384|512)-/i.test(token)) return false;
|
|
6938
6945
|
if (/^data:/i.test(token) || /^[A-Za-z0-9+/]{200,}={0,2}$/.test(token)) return false;
|
|
6939
6946
|
if (/^(?:https?|s3|gs|file):\/\//i.test(token) || /[\\/]{1}[\w.-]+[\\/]/.test(token)) return false;
|
|
6940
6947
|
if (allowWordSlug && /^[A-Za-z_][\w.-]*\/[A-Za-z_][\w.-]*\.[A-Za-z][A-Za-z0-9]{0,9}$/.test(token)) return false;
|
|
6948
|
+
if (allowWordSlug && /[/.]/.test(token) && token.split(/[/.]/).length >= 2 && token.split(/[/.]/).every((part) => /^[A-Za-z][a-z]*(?:[A-Z][a-z0-9]*)*$/.test(part))) return false;
|
|
6941
6949
|
if (/^[a-z]+(?:[A-Z][a-z0-9]+)+$/.test(token) || /^[a-z]+(?:_[a-z0-9]+)+$/.test(token)) return false;
|
|
6942
6950
|
if (allowWordSlug && /^[A-Z][a-z]+-(?:[A-Z][a-z]+){2,}$/.test(token)) return false;
|
|
6943
6951
|
if (allowWordSlug && /^[a-z]+(?:-[a-z]+)+(?:-\d+)?$/.test(token)) return false;
|
|
@@ -6959,6 +6967,7 @@ function looksLikeSecretValue(raw) {
|
|
|
6959
6967
|
if (value.length < 8) return false;
|
|
6960
6968
|
if (/^[A-Za-z_][A-Za-z0-9_.-]*\s*[:=]/.test(value)) return false;
|
|
6961
6969
|
if (/^--?[A-Za-z]/.test(value)) return false;
|
|
6970
|
+
if (PUBLISHED_EXAMPLE_VALUES.has(value)) return false;
|
|
6962
6971
|
if (SECRET_PLACEHOLDER_RE.test(value)) return false;
|
|
6963
6972
|
if (SECRET_CODE_EXPR_RE.test(value)) return false;
|
|
6964
6973
|
if (/^\/(?:[\[(^\\.]|.*\/[gimsuy]*$)/.test(value) || /[\[({]/.test(value)) return false;
|
|
@@ -7025,7 +7034,7 @@ function hasSecretLikeValue(value) {
|
|
|
7025
7034
|
return detectSecretKind(value) !== "none";
|
|
7026
7035
|
}
|
|
7027
7036
|
var PII_EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
|
7028
|
-
var PII_EMAIL_IGNORE_RE = /(?:@(?:example\.(?:com|org|net)|test\.[a-z]+|localhost(?:\.[a-z]+)?|[\w.-]*\.(?:invalid|test|local|localdomain|example)|users\.noreply\.github\.com|noreply\.[\w.-]
|
|
7037
|
+
var PII_EMAIL_IGNORE_RE = /(?:@(?:example\.(?:com|org|net)|test\.[a-z]+|localhost(?:\.[a-z]+)?|[\w.-]*\.(?:invalid|test|local|localdomain|example)|users\.noreply\.github\.com|noreply\.[\w.-]+|[\w.-]*\.gserviceaccount\.com|[\w.-]*\.svc\.id\.goog)$|^(?:no-?reply|noreply|do-?not-?reply|test\d*|example|user\d*|admin|root|foo|bar|baz|someone|somebody|you|me|name|first\.last|john\.doe|jane\.doe|email)@)/i;
|
|
7029
7038
|
var PII_PHONE_RE = /(?:^|[^\w.@/:-])(\+?\(?\d{1,4}\)?[\s-]\(?\d{2,4}\)?[\s-]\d{2,4}(?:[\s-]\d{2,4}){0,2})(?![\w.@/:-])/g;
|
|
7030
7039
|
function hasPiiLikeValue(raw) {
|
|
7031
7040
|
if (!raw) return false;
|
|
@@ -7163,7 +7172,9 @@ function extractUrl(args, _argsText) {
|
|
|
7163
7172
|
if (typeof raw !== "string") continue;
|
|
7164
7173
|
const shellField = ["command", "cmd", "script", "input"].includes(field);
|
|
7165
7174
|
const value = shellField ? splitShellSegments(shellUrlActionText(raw)).map(withoutUnattributedJavaScriptUrls).map(withoutGhMetadataProse).join("\n") : raw;
|
|
7166
|
-
const
|
|
7175
|
+
const firstWebUrl = (text) => text.match(/https?:\/\/[^\s"'<>`]+/i)?.[0];
|
|
7176
|
+
const firstStorageUrl = (text) => text.match(/\b(?:s3|gs|az|ftp|sftp|smb):\/\/[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[^\s"'<>`]*)?/i)?.[0];
|
|
7177
|
+
const firstUrl = (text) => firstWebUrl(text) || firstStorageUrl(text);
|
|
7167
7178
|
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)) : [];
|
|
7168
7179
|
const externalOutbound = outboundUrls.find((url) => {
|
|
7169
7180
|
try {
|
|
@@ -7172,7 +7183,8 @@ function extractUrl(args, _argsText) {
|
|
|
7172
7183
|
return false;
|
|
7173
7184
|
}
|
|
7174
7185
|
});
|
|
7175
|
-
const
|
|
7186
|
+
const networkSegments = shellField ? segmentsThatMayReachNetwork(splitShellSegments(value)) : [value];
|
|
7187
|
+
const found = externalOutbound || outboundUrls[0] || firstWebUrl(networkSegments.join("\n")) || (shellField ? firstStorageUrl(networkSegments.filter((segment, index, all) => storageUriIsDestination(segment, index, all)).join("\n")) : void 0) || firstStorageUrl(networkSegments.join("\n"));
|
|
7176
7188
|
if (found) return found;
|
|
7177
7189
|
}
|
|
7178
7190
|
return "";
|
|
@@ -7819,6 +7831,24 @@ function shellSegmentLead(segment) {
|
|
|
7819
7831
|
}
|
|
7820
7832
|
var PS_SESSION_VARIABLE_ASSIGN_RE = /^\s*\$(?:psdefaultparametervalues|psmodulepath|profile|executioncontext|env:(?:path|pathext|psmodulepath))\s*(?:\[[^\]]*\]\s*)?(?:\+?=)/i;
|
|
7821
7833
|
var PS_VALUE_EXECUTOR_RE = /\b(?:invoke-expression|iex|invoke-command|icm|start-process|saps|invoke-item|ii)\b|&\s*[$(]|\.\s*invoke\s*\(/i;
|
|
7834
|
+
var PS_VALUE_LEAD_RE = /^\$(?:env:)?\w+(?:\.\w+|\[\d+\])*$/;
|
|
7835
|
+
var PS_BLOCK_STAGE_LEADS = /* @__PURE__ */ new Set(["foreach-object", "foreach", "%", "where-object", "where", "?"]);
|
|
7836
|
+
function classifyPipelineStage(stage, depth) {
|
|
7837
|
+
const trimmed = stage.trim();
|
|
7838
|
+
if (!trimmed) return "read";
|
|
7839
|
+
if (isPowerShellReadFormatter(trimmed)) return "read";
|
|
7840
|
+
const lead = shellSegmentLead(trimmed);
|
|
7841
|
+
if (PS_BLOCK_STAGE_LEADS.has(lead) && depth < 3) {
|
|
7842
|
+
const open = trimmed.indexOf("{");
|
|
7843
|
+
const close = trimmed.lastIndexOf("}");
|
|
7844
|
+
if (open >= 0 && close > open) {
|
|
7845
|
+
const rest = maskQuotedSpans(trimmed.slice(0, open) + trimmed.slice(close + 1));
|
|
7846
|
+
if (!/^\s*(?:foreach-object|foreach|%|where-object|where|\?)\s*(?:-process\s*)?\s*$/i.test(rest)) return "SHELL";
|
|
7847
|
+
return classifyShellCommandOperation(trimmed.slice(open + 1, close), depth + 1);
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7850
|
+
return classifyShellSegment(trimmed, depth + 1);
|
|
7851
|
+
}
|
|
7822
7852
|
function isInertValueExpression(raw) {
|
|
7823
7853
|
const masked = maskQuotedSpans(raw);
|
|
7824
7854
|
if (/\$\(|`|&\s*[$(\w"']|[|<>]/.test(masked) || PS_VALUE_EXECUTOR_RE.test(masked)) return false;
|
|
@@ -7835,8 +7865,12 @@ function classifyShellSegment(segment, depth = 0) {
|
|
|
7835
7865
|
if (!raw) return "read";
|
|
7836
7866
|
if (PS_SESSION_VARIABLE_ASSIGN_RE.test(raw)) return "SHELL";
|
|
7837
7867
|
const lead = shellSegmentLead(segment);
|
|
7838
|
-
if (
|
|
7839
|
-
if (SHELL_NEUTRAL_BUILTINS.has(lead)
|
|
7868
|
+
if (PS_VALUE_LEAD_RE.test(lead) && PS_VALUE_EXECUTOR_RE.test(maskQuotedSpans(raw))) return "SHELL";
|
|
7869
|
+
if (SHELL_NEUTRAL_BUILTINS.has(lead)) return "read";
|
|
7870
|
+
if (PS_VALUE_LEAD_RE.test(lead)) {
|
|
7871
|
+
const stages = splitShellSegments(raw, true).slice(1);
|
|
7872
|
+
return worstShellOp(stages.map((stage) => classifyPipelineStage(stage, depth)), "read");
|
|
7873
|
+
}
|
|
7840
7874
|
if (isInertValueExpression(raw)) return "read";
|
|
7841
7875
|
const rawLower = raw.toLowerCase();
|
|
7842
7876
|
const sqlClient = SHELL_SQL_CLIENTS.has(lead) || /\|\s*(?:sudo\s+)?(?:psql|mysql|mariadb|sqlcmd|sqlite3|mongosh?|clickhouse-client|bq|cockroach|usql)\b/.test(rawLower);
|
|
@@ -7888,7 +7922,26 @@ function classifyShellWords(masked, raw, lead) {
|
|
|
7888
7922
|
if (lead === "git" && /\bgit\s+(?:status|diff|log|show|blame|clone|fetch|pull|ls-files|remote|branch(?!\s+-[dD])|describe|rev-parse|stash\s+(?:list|show)|tag$)\b/.test(value)) return "read";
|
|
7889
7923
|
if (lead === "gh" && /\bgh\s+(?:pr|issue|repo|run|release|workflow)\s+(?:view|list|status|checks|diff|download)\b/.test(value)) return "read";
|
|
7890
7924
|
if (lead === "gh" && /\bgh\s+auth\s+status\b/.test(value) && !/--show-token|\s-t\b/.test(value)) return "read";
|
|
7891
|
-
if (
|
|
7925
|
+
if (lead === "gh" && /\bgh\s+api(?=\s|$)/.test(value)) {
|
|
7926
|
+
const method = /(?:\s-x|\s--method)[\s=]+["']?([a-z]+)/i.exec(segment);
|
|
7927
|
+
const hasField = /\s(?:-f|-F|--field|--raw-field|--input)(?:[\s=]|$)/.test(segment);
|
|
7928
|
+
if (method && method[1].toUpperCase() !== "GET" || hasField) return "write";
|
|
7929
|
+
const header = /\s(?:-H|--header)(?:[\s=]|$)/.exec(segment);
|
|
7930
|
+
if (header && /\$\(|`|\$env:|\(\s*[A-Za-z][\w-]*\s/.test(segment.slice(header.index))) return "write";
|
|
7931
|
+
return pipesIntoInterpreter ? "SHELL" : "read";
|
|
7932
|
+
}
|
|
7933
|
+
const gcloudVerb = gcloudVerbWord(value);
|
|
7934
|
+
if (gcloudVerb) {
|
|
7935
|
+
if (GCLOUD_READ_VERB_RE.test(gcloudVerb)) {
|
|
7936
|
+
if (pipesIntoInterpreter) return "SHELL";
|
|
7937
|
+
const redirect = value.match(/(?:^|[^>0-9])>{1,2}\s*([^\s&|;]+)/);
|
|
7938
|
+
if (redirect && !/^\/dev\/null$|^nul$|^\$null$/i.test(redirect[1])) return "write";
|
|
7939
|
+
return "read";
|
|
7940
|
+
}
|
|
7941
|
+
if (gcloudVerb === "delete") return "DELETE";
|
|
7942
|
+
if (GCLOUD_WRITE_VERB_RE.test(gcloudVerb)) return pipesIntoInterpreter ? "SHELL" : "write";
|
|
7943
|
+
}
|
|
7944
|
+
if (/^(?: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";
|
|
7892
7945
|
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)) {
|
|
7893
7946
|
return isWorkspaceScopedRemoval(segment) ? "write" : "DELETE";
|
|
7894
7947
|
}
|
|
@@ -7919,6 +7972,35 @@ function classifyShellWords(masked, raw, lead) {
|
|
|
7919
7972
|
if (SHELL_BLOCK_STRUCTURE_RE.test(value)) return "read";
|
|
7920
7973
|
return "SHELL";
|
|
7921
7974
|
}
|
|
7975
|
+
var GCLOUD_READ_VERB_RE = /^(?:describe|list|ls|get-iam-policy|get-value|get-ancestors|read|check|versions)$/;
|
|
7976
|
+
var GCLOUD_WRITE_VERB_RE = /^(?:create|update|patch|replace|enable|disable|deploy|apply|import|export|start|stop|restart|resume|suspend|scale|promote|migrate|rollback|add-[a-z-]+|remove-[a-z-]+|set-[a-z-]+|unset|clear|activate|revoke|copy)$/;
|
|
7977
|
+
function gcloudVerbWord(value) {
|
|
7978
|
+
const tokens = value.split("|")[0].trim().split(/\s+/);
|
|
7979
|
+
if (!/^gcloud(?:\.cmd|\.exe)?$/.test(tokens.shift() || "")) return null;
|
|
7980
|
+
let depth = 0;
|
|
7981
|
+
for (const token of tokens) {
|
|
7982
|
+
if (token.startsWith("-")) continue;
|
|
7983
|
+
if (GCLOUD_READ_VERB_RE.test(token) || GCLOUD_WRITE_VERB_RE.test(token) || token === "delete") return token;
|
|
7984
|
+
if (!/^[a-z][a-z0-9-]*$/.test(token) || ++depth > 6) return null;
|
|
7985
|
+
}
|
|
7986
|
+
return null;
|
|
7987
|
+
}
|
|
7988
|
+
var CLOUD_CLI_LEAD_RE = /^(?:gcloud(?:\.cmd|\.exe)?|gsutil|aws|az)$/i;
|
|
7989
|
+
var STORAGE_TRANSFER_RE = /\b(?:aws\s+s3\s+(?:cp|sync|mv)|gsutil\s+(?:-m\s+)?(?:cp|rsync|mv)|gcloud(?:\.cmd|\.exe)?\s+storage\s+(?:cp|rsync|mv)|az\s+storage\s+blob\s+upload|scp|rsync|sftp)\b/i;
|
|
7990
|
+
var PIPES_INTO_INTERPRETER_RE = /\|\s*(?:sudo\s+)?(?:ba|z|da|k)?sh\b|\|\s*(?:pwsh|powershell|python3?|node|perl|ruby)\b/i;
|
|
7991
|
+
function storageUriIsDestination(segment, index = 0, all = []) {
|
|
7992
|
+
const lead = shellSegmentLead(segment);
|
|
7993
|
+
if (!CLOUD_CLI_LEAD_RE.test(lead)) return true;
|
|
7994
|
+
const words = maskQuotedSpans(segment).toLowerCase();
|
|
7995
|
+
if (PIPES_INTO_INTERPRETER_RE.test(words) || /\$\(|`|&\s*[$(]/.test(words) || PS_VALUE_EXECUTOR_RE.test(words)) return true;
|
|
7996
|
+
if (STORAGE_TRANSFER_RE.test(words)) return true;
|
|
7997
|
+
if (/^\s*(?:\$[\w:]+\s*=|[A-Za-z_]\w*=)/.test(segment) && all.slice(index + 1).some((later) => {
|
|
7998
|
+
const laterWords = maskQuotedSpans(later).toLowerCase();
|
|
7999
|
+
return PS_VALUE_EXECUTOR_RE.test(laterWords) || PIPES_INTO_INTERPRETER_RE.test(laterWords) || /(?:^|[\s;&|(])(?:sudo\s+)?(?:ba|z|da|k)?sh\s+-c\b|(?:^|[\s;&|(])(?:pwsh|powershell)(?:\.exe)?\s+-c(?:ommand)?\b|\beval\b|&\s*[$(]/.test(laterWords);
|
|
8000
|
+
})) return true;
|
|
8001
|
+
if (/^gcloud(?:\.cmd|\.exe)?\s+(?:--\S+\s+)*(?:alpha\s+|beta\s+)?[a-z-]+(?:\s+[a-z-]+){0,4}\s+export\b/.test(words.split("|")[0].trim())) return true;
|
|
8002
|
+
return false;
|
|
8003
|
+
}
|
|
7922
8004
|
var SHELL_BLOCK_STRUCTURE_RE = /^(?:\s|[{}()=\-\d]|\+=|""|\$[\w:.]+|\b(?:try|catch|finally|if|then|else|elseif|elif|fi|do|done|esac|end|exit|return|break|continue)\b)*$/i;
|
|
7923
8005
|
function classifyShellCommandOperation(command, depth = 0) {
|
|
7924
8006
|
if (provenPowerShellReadSequence(command)) return "read";
|
|
@@ -8093,6 +8175,16 @@ function provenPowerShellReadSequence(command) {
|
|
|
8093
8175
|
}
|
|
8094
8176
|
return requests > 0;
|
|
8095
8177
|
}
|
|
8178
|
+
function isLoopbackHost(hostname) {
|
|
8179
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
8180
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "::1" || host === "0.0.0.0" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
8181
|
+
}
|
|
8182
|
+
var RAW_NETWORK_PROGRAM_RE = /(?:^|[\s;&|(`])(?:nc|ncat|netcat|socat|ssh|scp|sftp|rsync|telnet|ftp|tftp|mail|mailx|sendmail|msmtp|openssl\s+s_client)\b/i;
|
|
8183
|
+
function commandStaysOnLoopback(command) {
|
|
8184
|
+
if (!command || RAW_NETWORK_PROGRAM_RE.test(command)) return false;
|
|
8185
|
+
const hosts = [...command.matchAll(/\b[a-z][a-z0-9+.-]*:\/\/(?:[^/\s@'"]*@)?([^/\s:'"?#]+)/gi)].map((match) => match[1]);
|
|
8186
|
+
return hosts.length > 0 && hosts.every(isLoopbackHost);
|
|
8187
|
+
}
|
|
8096
8188
|
function isInternalHost(hostname) {
|
|
8097
8189
|
const host = hostname.toLowerCase();
|
|
8098
8190
|
if (!host || host === "localhost" || host === "::1" || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
@@ -8133,6 +8225,9 @@ function evaluateActionPolicies(policies, toolName, operation, context = {}, dec
|
|
|
8133
8225
|
const matchText = buildOperationMatchText(toolName, context);
|
|
8134
8226
|
const toolCaps = toolCapabilities(toolName || context.toolName || "");
|
|
8135
8227
|
const localFileContentAction = isLocalFileContentAction(toolName, operation, context);
|
|
8228
|
+
const loopbackContentAction = context["destination.loopback"] === "true";
|
|
8229
|
+
const ruleNamesDestination = (rule) => (rule.constraints || []).some((c) => c.field.startsWith("destination."));
|
|
8230
|
+
const contentCeilingApplies = (rule) => ruleNeedsContentFact(rule) && (localFileContentAction || loopbackContentAction && !ruleNamesDestination(rule));
|
|
8136
8231
|
const isBareInvocation = !!operation && operation.toLowerCase() === (toolName || "").toLowerCase();
|
|
8137
8232
|
const isCodeExecTool = toolCaps.has("code_execution");
|
|
8138
8233
|
const candidateOperations = isBareInvocation && declaredOperations.length > 0 && !isCodeExecTool ? [operation, ...declaredOperations] : [operation];
|
|
@@ -8187,7 +8282,7 @@ function evaluateActionPolicies(policies, toolName, operation, context = {}, dec
|
|
|
8187
8282
|
const constraintDesc = rule.constraints?.map((c) => `${c.field} ${c.operator} ${c.value}`).join(", ");
|
|
8188
8283
|
const matchedRule = `${rule.operations.join("/")} \u2192 ${rule.verdict}${constraintDesc ? ` (${constraintDesc})` : ""}`;
|
|
8189
8284
|
if (isMonitorStage) {
|
|
8190
|
-
const wouldOnlyWarn = rule.verdict === "block" && policy.enforcement !== "hard" &&
|
|
8285
|
+
const wouldOnlyWarn = rule.verdict === "block" && policy.enforcement !== "hard" && contentCeilingApplies(rule);
|
|
8191
8286
|
if ((rule.verdict === "block" || rule.verdict === "require_approval") && !wouldOnlyWarn) {
|
|
8192
8287
|
const match = { policyId: policy.id, policyName: policy.name, verdict: rule.verdict, matchedRule };
|
|
8193
8288
|
monitorMatches.push(match);
|
|
@@ -8205,13 +8300,13 @@ function evaluateActionPolicies(policies, toolName, operation, context = {}, dec
|
|
|
8205
8300
|
reason: `Action policy "${policy.name}": ${rule.operations.join("/")} ${rule.verdict === "block" ? "blocked" : rule.verdict === "require_approval" ? "requires approval" : rule.verdict}`,
|
|
8206
8301
|
...rule.verdict === "require_approval" ? { approval: resolveApprovalOptions(rule.approval) } : {}
|
|
8207
8302
|
};
|
|
8208
|
-
if (rule.verdict === "block" && policy.enforcement !== "hard" &&
|
|
8303
|
+
if (rule.verdict === "block" && policy.enforcement !== "hard" && contentCeilingApplies(rule)) {
|
|
8209
8304
|
matched = {
|
|
8210
8305
|
...matched,
|
|
8211
8306
|
allowed: true,
|
|
8212
8307
|
verdict: "log",
|
|
8213
8308
|
loggedInsteadOfBlocked: true,
|
|
8214
|
-
reason: `${matched.reason} \u2014 recorded as a warning: this only touches a file on this machine and nothing leaves it (the same content leaving the machine still blocks; mark the policy Hard block to stop file edits too)`
|
|
8309
|
+
reason: localFileContentAction ? `${matched.reason} \u2014 recorded as a warning: this only touches a file on this machine and nothing leaves it (the same content leaving the machine still blocks; mark the policy Hard block to stop file edits too)` : `${matched.reason} \u2014 recorded as a warning: the destination is this machine (${context["destination.domain"]}) and nothing leaves it (the same content sent to an outside host still blocks; add a destination.type constraint to the rule to enforce on loopback too)`
|
|
8215
8310
|
};
|
|
8216
8311
|
}
|
|
8217
8312
|
if (matched.verdict === "require_approval") {
|
|
@@ -8365,11 +8460,17 @@ function inferBundledToolContext(toolName, args) {
|
|
|
8365
8460
|
context["destination.domain"] = parsed.hostname;
|
|
8366
8461
|
context["url.risk"] = hostType;
|
|
8367
8462
|
const requestHasSecret = recordHttpRequestFacts(context, args, detectedUrl);
|
|
8368
|
-
|
|
8463
|
+
const storageResourceOnly = isCodeExecTool && /^(?:gs|s3|az):\/\//i.test(detectedUrl) && (() => {
|
|
8464
|
+
const segments = splitShellSegments(commandText);
|
|
8465
|
+
const bucket = (detectedUrl.match(/^[a-z0-9]+:\/\/[^/\s"'<>`]+/i) || [detectedUrl])[0].toLowerCase();
|
|
8466
|
+
const carriers = segments.map((segment, index) => [segment, index]).filter(([segment]) => segment.toLowerCase().includes(bucket));
|
|
8467
|
+
return carriers.length > 0 && carriers.every(([segment, index]) => !storageUriIsDestination(segment, index, segments));
|
|
8468
|
+
})();
|
|
8469
|
+
if (inbound || storageResourceOnly) {
|
|
8369
8470
|
context["source.domain"] = parsed.hostname;
|
|
8370
8471
|
context["source.type"] = hostType;
|
|
8371
8472
|
}
|
|
8372
|
-
if (!inbound || requestHasSecret) {
|
|
8473
|
+
if (!inbound && !storageResourceOnly || requestHasSecret) {
|
|
8373
8474
|
context["destination.type"] = hostType;
|
|
8374
8475
|
}
|
|
8375
8476
|
} catch {
|
|
@@ -8388,6 +8489,10 @@ function inferBundledToolContext(toolName, args) {
|
|
|
8388
8489
|
context.amount = String(args.amount ?? args.price ?? args.total);
|
|
8389
8490
|
}
|
|
8390
8491
|
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";
|
|
8492
|
+
delete context["destination.loopback"];
|
|
8493
|
+
if (context["destination.domain"] && isLoopbackHost(context["destination.domain"]) && (!isCodeExecTool || commandStaysOnLoopback(commandText))) {
|
|
8494
|
+
context["destination.loopback"] = "true";
|
|
8495
|
+
}
|
|
8391
8496
|
delete context["destination.repository"];
|
|
8392
8497
|
if (isCodeExecTool) {
|
|
8393
8498
|
const repository = explicitGitHubWriteRepository(String(args.command || args.cmd || args.code || args.script || args.input || ""));
|
package/dist/approvalPrompt.d.ts
CHANGED
|
@@ -16,7 +16,10 @@ export interface NativeDialogView {
|
|
|
16
16
|
headline: string;
|
|
17
17
|
subtitle: string;
|
|
18
18
|
kind?: string;
|
|
19
|
+
/** The action: the whole script when the explanation carried it, else the short "What" line. */
|
|
19
20
|
action?: string;
|
|
21
|
+
/** Plain-words line per step of a shell script ("Exports Firestore data … to storage bucket …"). */
|
|
22
|
+
steps: string[];
|
|
20
23
|
policy?: string;
|
|
21
24
|
scope?: string;
|
|
22
25
|
agent?: string;
|
package/dist/approvalPrompt.js
CHANGED
|
@@ -12,6 +12,8 @@ function line(value, limit) {
|
|
|
12
12
|
const head = Math.ceil((limit - 3) * 0.6);
|
|
13
13
|
return `${text.slice(0, head)} … ${text.slice(-(limit - head - 3))}`;
|
|
14
14
|
}
|
|
15
|
+
/** The full script is shown in a scrolling box; this only guards against a runaway payload. */
|
|
16
|
+
const MAX_DIALOG_SCRIPT = 6000;
|
|
15
17
|
const CEILING_NOTE_RE = /\s*This policy is set to Block[.;]\s*(?:Because it is not marked "Hard block", your workstation requires your approval before continuing\.|on a developer workstation it asks you instead\.)?\s*(?:An admin can (?:review the matching rule and its scope in the console|mark it "Hard block" in the console to stop this without asking)\.)?/;
|
|
16
18
|
const CEILING_NOTE_SHORT = 'set to Block; on your workstation it asks you first.';
|
|
17
19
|
/** Parse the `Rule:` line the block explanation writes into the name the console lists and its scope. */
|
|
@@ -39,6 +41,20 @@ function promptFacts(reason) {
|
|
|
39
41
|
const facts = { agent, ide: field('IDE') };
|
|
40
42
|
if (rule)
|
|
41
43
|
Object.assign(facts, ruleParts(rule));
|
|
44
|
+
const script = field('Script');
|
|
45
|
+
if (script)
|
|
46
|
+
facts.script = script.length > MAX_DIALOG_SCRIPT ? `${script.slice(0, MAX_DIALOG_SCRIPT - 1)}…` : script;
|
|
47
|
+
const doesAt = rows.findIndex(row => /^Does:\s*$/.test(row));
|
|
48
|
+
if (doesAt >= 0) {
|
|
49
|
+
const steps = [];
|
|
50
|
+
for (const row of rows.slice(doesAt + 1)) {
|
|
51
|
+
if (!row.startsWith('- '))
|
|
52
|
+
break;
|
|
53
|
+
steps.push(row.slice(2).trim());
|
|
54
|
+
}
|
|
55
|
+
if (steps.length)
|
|
56
|
+
facts.steps = steps;
|
|
57
|
+
}
|
|
42
58
|
if (what) {
|
|
43
59
|
// summarizeAction ends with the kind in parentheses: "… (shell command)".
|
|
44
60
|
const kind = /^(.*)\s\(([a-z][a-z /-]{2,30})\)$/i.exec(what);
|
|
@@ -94,7 +110,9 @@ function nativeDialogView(reason, options = {}) {
|
|
|
94
110
|
? 'A security policy paused this action for an admin or manager.'
|
|
95
111
|
: 'A security policy paused this action. Review the details and choose what to do next.',
|
|
96
112
|
kind: f.kind,
|
|
97
|
-
|
|
113
|
+
// The whole script when we have it (the box scrolls); the short line otherwise.
|
|
114
|
+
action: f.script ? f.script.replace(/ ⏎ /g, '\n') : f.action ? line(f.action, 280) : undefined,
|
|
115
|
+
steps: (f.steps || []).slice(0, 14).map(step => line(step, 220)),
|
|
98
116
|
policy: f.policy ? line(f.policy, 160) : undefined,
|
|
99
117
|
scope: f.scope ? line(f.scope, 220) : undefined,
|
|
100
118
|
agent: f.agent ? line(f.agent, 80) : undefined,
|
|
@@ -117,6 +135,7 @@ function nativeApprovalMessage(reason, options = {}) {
|
|
|
117
135
|
f.policy ? `Policy: ${line(f.policy, 160)}` : '',
|
|
118
136
|
f.scope ? ` ${line(f.scope, 200)}` : '',
|
|
119
137
|
f.action ? `Action: ${f.kind ? `${f.kind} — ` : ''}${line(f.action, 230)}` : '',
|
|
138
|
+
...(f.steps || []).slice(0, 6).map((step, index) => `${index === 0 ? 'Does: ' : ' '}${index + 1}. ${line(step, 160)}`),
|
|
120
139
|
f.agent ? `Agent: ${line(f.agent, 80)}` : '',
|
|
121
140
|
f.ide ? `IDE: ${line(f.ide, 40)}` : '',
|
|
122
141
|
f.tool ? `Tool: ${line(f.tool, 230)}` : '',
|
package/dist/blockExplanation.js
CHANGED
|
@@ -19,7 +19,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
exports.summarizeAction = summarizeAction;
|
|
20
20
|
exports.buildBlockExplanation = buildBlockExplanation;
|
|
21
21
|
exports.oneLineExplanation = oneLineExplanation;
|
|
22
|
+
const commandNarrative_1 = require("./commandNarrative");
|
|
22
23
|
const MAX_WHAT = 160;
|
|
24
|
+
/** The full script travels to the approval dialog; anything longer is still cut, but far later. */
|
|
25
|
+
const MAX_SCRIPT = 6000;
|
|
26
|
+
/** Newlines flattened so the multi-line explanation grammar (one `Field:` per row) still parses. */
|
|
27
|
+
function scriptLine(command) {
|
|
28
|
+
const flat = command.replace(/\r?\n/g, ' ⏎ ').trim();
|
|
29
|
+
return flat.length > MAX_SCRIPT ? `${flat.slice(0, MAX_SCRIPT - 1)}…` : flat;
|
|
30
|
+
}
|
|
23
31
|
function str(v) {
|
|
24
32
|
return typeof v === 'string' && v.trim() ? v : undefined;
|
|
25
33
|
}
|
|
@@ -170,7 +178,10 @@ function buildBlockExplanation(input) {
|
|
|
170
178
|
// "Blocked ..." reason must not turn a pending approval into a final denial.
|
|
171
179
|
const reason = src.source === 'builtin' && (input.outcome === 'ask' || input.outcome === 'approval')
|
|
172
180
|
? src.reason.replace(/^Blocked\b/, 'Matched') : src.reason;
|
|
173
|
-
|
|
181
|
+
// Catalog findings often carry the same words as reason and explanation
|
|
182
|
+
// ("…: gcloud access-token print." / "gcloud access-token print"); say it once.
|
|
183
|
+
const explanation = src.explanation?.trim().replace(/\.$/, '');
|
|
184
|
+
why = explanation && !reason.toLowerCase().includes(explanation.toLowerCase()) ? `${reason} — ${src.explanation}` : reason;
|
|
174
185
|
unblock = [
|
|
175
186
|
`Open the rule in the console: ${consoleLink}`,
|
|
176
187
|
`An admin can set "${label}" to Ask the developer, Warn only, or Off — for everyone or only for ${input.developerName || 'this machine'}.`,
|
|
@@ -226,9 +237,18 @@ function buildBlockExplanation(input) {
|
|
|
226
237
|
if (input.note)
|
|
227
238
|
why = `${why} ${input.note}`.trim();
|
|
228
239
|
const headline = headlineFor(input.outcome, noun);
|
|
240
|
+
// Shell / terminal: say what each step of the script does, in plain words,
|
|
241
|
+
// and carry the whole script when the short "What" line had to cut it.
|
|
242
|
+
const command = (input.event === 'shell' || input.event === 'terminal')
|
|
243
|
+
? (str(input.toolArgs?.command) || str(input.toolArgs?.cmd) || str(input.toolArgs?.commandLine) || str(input.toolArgs?.script))
|
|
244
|
+
: undefined;
|
|
245
|
+
const steps = command ? (0, commandNarrative_1.describeShellCommandLines)(command) : [];
|
|
246
|
+
const script = command && command.replace(/\s+/g, ' ').trim().length > MAX_WHAT - 1 ? scriptLine(command) : undefined;
|
|
229
247
|
const userMessage = [
|
|
230
248
|
headline,
|
|
231
249
|
`What: ${what}`,
|
|
250
|
+
...(steps.length ? ['Does:', ...steps.map(step => ` - ${step}`)] : []),
|
|
251
|
+
...(script ? [`Script: ${script}`] : []),
|
|
232
252
|
`Rule: ${rule}`,
|
|
233
253
|
`Why: ${why}`,
|
|
234
254
|
stepsTitle(input.outcome),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a shell script DOES, one plain-words line per step — for the approval
|
|
3
|
+
* dialog and the IDE message. A developer asked to approve
|
|
4
|
+
*
|
|
5
|
+
* $prefix="gs://b/prod-export"; gcloud firestore export $prefix --project acme-prod; gcloud firestore import $prefix --project acme-staging
|
|
6
|
+
*
|
|
7
|
+
* should read "Exports Firestore data from project acme-prod to bucket b ·
|
|
8
|
+
* Imports Firestore data into project acme-staging from bucket b", not a
|
|
9
|
+
* truncated command line.
|
|
10
|
+
*
|
|
11
|
+
* Fixed templates only, keyed on the program and its verb — no model, no
|
|
12
|
+
* scoring, nothing that could decide anything. This module never influences a
|
|
13
|
+
* verdict; it only words one. A program the grammar does not know is reported
|
|
14
|
+
* as exactly that ("Runs `foo bar`"), never guessed. Pure: no I/O.
|
|
15
|
+
*/
|
|
16
|
+
export interface CommandStep {
|
|
17
|
+
/** The segment as written (trimmed). */
|
|
18
|
+
segment: string;
|
|
19
|
+
/** Plain-words description of what it does. */
|
|
20
|
+
text: string;
|
|
21
|
+
/** The grammar recognised the program and its verb. */
|
|
22
|
+
known: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Quote-aware split on `;`, `&&`, `||`, newlines. Pipes stay inside a step. */
|
|
25
|
+
export declare function splitCommandSteps(command: string): string[];
|
|
26
|
+
/** One plain-words line per step, in order. */
|
|
27
|
+
export declare function describeShellCommand(command: string): CommandStep[];
|
|
28
|
+
/** The step lines as the explanation prints them (`- …`). */
|
|
29
|
+
export declare function describeShellCommandLines(command: string): string[];
|