@mrciphersmith/keryx 0.4.4 → 0.4.6
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/cli.js +8698 -5275
- package/dist/core.js +1741 -518
- package/dist/proxy-worker.js +187 -31
- package/package.json +1 -1
package/dist/core.js
CHANGED
|
@@ -2496,7 +2496,7 @@ function detectPii(content) {
|
|
|
2496
2496
|
perRule[PHONE_RULE] = dropRescansInsideCards(perRule[PHONE_RULE], perRule[CARD_RULE]);
|
|
2497
2497
|
return perRule.flat();
|
|
2498
2498
|
}
|
|
2499
|
-
var UNICODE_EMAIL, ASCII_EMAIL, emailScanner, asciiEmailScanner, PHONE_FROM, PHONE_RUN_CHAR, PHONE_START_BLOCKER, DIGIT, PHONE_DIAL_GROUP, PHONE_FIRST_GROUP, PHONE_SEPARATOR, PHONE_NANP_SPACED, WHITESPACE, LINE_BREAK, PHONE_MAX_DIGITS = 15, PHONE_MIN_DIGITS = 9, rescanned, PHONE_JOINED_PIECE, PHONE_CARD_SHAPE, PHONE_GLUE, PHONE_GLUE_LETTER, PHONE_GLUE_EVIDENCE, phoneGlueScanner, phoneScanner, IBAN_START, IBAN_MAX_GROUPS = 8, IBAN_STATES, ibanFailed, ibanStamp = 0, ibanScanner, IBAN_LENGTHS, IBAN_GLUE_START, ibanGluedScanner, CARD_SEPARATOR = "[ \\t\\r\\n._/-]", CARD_EXTRA, CARD_GROUP_SEPARATOR, GLUE_LETTER_BEFORE, GLUE_LETTER_AFTER, NON_HEX_LETTER, GLUE_FLANK = 8, UUID_ANYWHERE, UUID_REACH = 36, SSN_BREAK_EXTRA, IPV4_EXTRA, RULES, CALENDAR_DATE, IDENTIFIER_CHAR, TOKEN_SCAN_LIMIT = 64, UUID_TOKEN, rescanTag, NORMALIZE_CHUNK, NORMALIZE_OVERLAP = 512, NORMALIZE_SKIP = 256, NON_ASCII, DECIMAL_DIGIT, digitValues, BREAK_CHAR, PHONE_RULE, CARD_RULE, NUMBER_RULES, FULLWIDTH_CAPITAL, SOFT_HYPHEN, NUMBER_BREAK;
|
|
2499
|
+
var UNICODE_EMAIL, ASCII_EMAIL, emailScanner, asciiEmailScanner, PHONE_FROM, PHONE_RUN_CHAR, PHONE_START_BLOCKER, DIGIT, PHONE_DIAL_GROUP, PHONE_FIRST_GROUP, PHONE_SEPARATOR, PHONE_NANP_SPACED, WHITESPACE, LINE_BREAK, PHONE_MAX_DIGITS = 15, PHONE_MIN_DIGITS = 9, rescanned, PHONE_JOINED_PIECE, PHONE_CARD_SHAPE, PHONE_GLUE, PHONE_GLUE_LETTER, PHONE_GLUE_EVIDENCE, phoneGlueScanner, phoneScanner, IBAN_START, IBAN_MAX_GROUPS = 8, IBAN_STATES, ibanFailed, ibanStamp = 0, ibanScanner, IBAN_LENGTHS, IBAN_GLUE_START, ibanGluedScanner, CARD_SEPARATOR = "[ \\t\\r\\n._/-]", CARD_EXTRA, CARD_GROUP_SEPARATOR, GLUE_LETTER_BEFORE, GLUE_LETTER_AFTER, NON_HEX_LETTER, GLUE_FLANK = 8, UUID_ANYWHERE, UUID_REACH = 36, SSN_BREAK_EXTRA, IPV4_EXTRA, IPV6_COMPRESSED, RULES, CALENDAR_DATE, IDENTIFIER_CHAR, TOKEN_SCAN_LIMIT = 64, UUID_TOKEN, rescanTag, NORMALIZE_CHUNK, NORMALIZE_OVERLAP = 512, NORMALIZE_SKIP = 256, NON_ASCII, DECIMAL_DIGIT, digitValues, BREAK_CHAR, PHONE_RULE, CARD_RULE, NUMBER_RULES, FULLWIDTH_CAPITAL, SOFT_HYPHEN, NUMBER_BREAK;
|
|
2500
2500
|
var init_pii = __esm(() => {
|
|
2501
2501
|
UNICODE_EMAIL = {
|
|
2502
2502
|
from: /(?:(?<=[\p{L}\p{N}\p{M}_])(?![\p{L}\p{N}\p{M}_])|(?<![\p{L}\p{N}\p{M}_])(?=[\p{L}\p{N}\p{M}_]))[\p{L}\p{N}\p{M}._%+-]+@[\p{L}\p{N}\p{M}.-]+\.[\p{L}\p{M}]{2,}(?![\p{L}\p{N}\p{M}_])/uy,
|
|
@@ -2794,6 +2794,7 @@ var init_pii = __esm(() => {
|
|
|
2794
2794
|
UUID_ANYWHERE = /[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}/g;
|
|
2795
2795
|
SSN_BREAK_EXTRA = /(?<![0-9])\d{3}(?:\r\n|[\t\r\n])\d{2}(?:\r\n|[\t\r\n])\d{4}(?![0-9])/g;
|
|
2796
2796
|
IPV4_EXTRA = /(?<![0-9.])(?<!\b[vV])\d{1,3}(?:[\t\r\n]{0,2}\.[\t\r\n]{0,2}\d{1,3}){3}(?![0-9])/g;
|
|
2797
|
+
IPV6_COMPRESSED = /(?<![0-9A-Fa-f:])(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6})?::(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4}){0,6})?(?![0-9A-Fa-f:])/g;
|
|
2797
2798
|
RULES = [
|
|
2798
2799
|
{
|
|
2799
2800
|
policyId: "pii.email",
|
|
@@ -2867,7 +2868,10 @@ var init_pii = __esm(() => {
|
|
|
2867
2868
|
severity: "low",
|
|
2868
2869
|
confidence: 0.6,
|
|
2869
2870
|
validate: isValidIp,
|
|
2870
|
-
also: [
|
|
2871
|
+
also: [
|
|
2872
|
+
{ regex: IPV4_EXTRA, validate: (value) => isValidIp(value.replace(/[\t\r\n]/g, "")) },
|
|
2873
|
+
{ regex: IPV6_COMPRESSED, validate: (value) => /[0-9]/.test(value) && isValidIp(value) }
|
|
2874
|
+
]
|
|
2871
2875
|
}
|
|
2872
2876
|
];
|
|
2873
2877
|
CALENDAR_DATE = /(?:19|20)\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])/;
|
|
@@ -2888,29 +2892,55 @@ var init_pii = __esm(() => {
|
|
|
2888
2892
|
});
|
|
2889
2893
|
|
|
2890
2894
|
// src/security/detect/secrets.ts
|
|
2895
|
+
function scanPrivateKeyBlocks(content) {
|
|
2896
|
+
const spans = [];
|
|
2897
|
+
PRIVATE_KEY_BEGIN.lastIndex = 0;
|
|
2898
|
+
let begin;
|
|
2899
|
+
while ((begin = PRIVATE_KEY_BEGIN.exec(content)) !== null) {
|
|
2900
|
+
PRIVATE_KEY_END.lastIndex = begin.index + begin[0].length;
|
|
2901
|
+
const end = PRIVATE_KEY_END.exec(content);
|
|
2902
|
+
if (end === null) {
|
|
2903
|
+
spans.push([begin.index, content.length]);
|
|
2904
|
+
break;
|
|
2905
|
+
}
|
|
2906
|
+
const stop = end.index + end[0].length;
|
|
2907
|
+
spans.push([begin.index, stop]);
|
|
2908
|
+
PRIVATE_KEY_BEGIN.lastIndex = stop;
|
|
2909
|
+
}
|
|
2910
|
+
return spans;
|
|
2911
|
+
}
|
|
2891
2912
|
function detectSecrets(content) {
|
|
2892
2913
|
const matches = [];
|
|
2914
|
+
const push = (rule, start, end) => {
|
|
2915
|
+
matches.push({
|
|
2916
|
+
category: "secret",
|
|
2917
|
+
policyId: rule.policyId,
|
|
2918
|
+
severity: rule.severity,
|
|
2919
|
+
confidence: rule.confidence,
|
|
2920
|
+
start,
|
|
2921
|
+
end,
|
|
2922
|
+
value: content.slice(start, end),
|
|
2923
|
+
mask: "secret",
|
|
2924
|
+
remediation: rule.remediation
|
|
2925
|
+
});
|
|
2926
|
+
};
|
|
2893
2927
|
for (const rule of RULES2) {
|
|
2928
|
+
if (rule.scan) {
|
|
2929
|
+
for (const [start, end] of rule.scan(content)) {
|
|
2930
|
+
push(rule, start, end);
|
|
2931
|
+
}
|
|
2932
|
+
continue;
|
|
2933
|
+
}
|
|
2894
2934
|
rule.regex.lastIndex = 0;
|
|
2895
2935
|
let m;
|
|
2896
2936
|
while ((m = rule.regex.exec(content)) !== null) {
|
|
2897
|
-
const group = rule
|
|
2898
|
-
const value = m[group];
|
|
2899
|
-
if (value === undefined || value.length === 0) {
|
|
2937
|
+
const group = valueGroupOf(rule, m);
|
|
2938
|
+
const value = group === undefined ? undefined : m[group];
|
|
2939
|
+
if (group === undefined || value === undefined || value.length === 0) {
|
|
2900
2940
|
continue;
|
|
2901
2941
|
}
|
|
2902
|
-
const start = group === 0 ? m.index : m.index + m[0].indexOf(value);
|
|
2903
|
-
|
|
2904
|
-
category: "secret",
|
|
2905
|
-
policyId: rule.policyId,
|
|
2906
|
-
severity: rule.severity,
|
|
2907
|
-
confidence: rule.confidence,
|
|
2908
|
-
start,
|
|
2909
|
-
end: start + value.length,
|
|
2910
|
-
value,
|
|
2911
|
-
mask: "secret",
|
|
2912
|
-
remediation: rule.remediation
|
|
2913
|
-
});
|
|
2942
|
+
const start = group === 0 ? m.index : m.indices?.[group]?.[0] ?? m.index + m[0].indexOf(value);
|
|
2943
|
+
push(rule, start, start + value.length);
|
|
2914
2944
|
if (m.index === rule.regex.lastIndex) {
|
|
2915
2945
|
rule.regex.lastIndex += 1;
|
|
2916
2946
|
}
|
|
@@ -2918,8 +2948,22 @@ function detectSecrets(content) {
|
|
|
2918
2948
|
}
|
|
2919
2949
|
return matches;
|
|
2920
2950
|
}
|
|
2921
|
-
|
|
2951
|
+
function valueGroupOf(rule, m) {
|
|
2952
|
+
if (rule.valueGroup === undefined) {
|
|
2953
|
+
return 0;
|
|
2954
|
+
}
|
|
2955
|
+
for (let group = rule.valueGroup;group < m.length; group += 1) {
|
|
2956
|
+
if (m[group] !== undefined) {
|
|
2957
|
+
return group;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
var PRIVATE_KEY_BEGIN, PRIVATE_KEY_END, ASSIGNED_VALUE, RULES2;
|
|
2922
2963
|
var init_secrets = __esm(() => {
|
|
2964
|
+
PRIVATE_KEY_BEGIN = /-----BEGIN (?:[A-Z0-9]{1,16} ){0,3}PRIVATE KEY(?: BLOCK)?-----/g;
|
|
2965
|
+
PRIVATE_KEY_END = /-----END (?:[A-Z0-9]{1,16} ){0,3}PRIVATE KEY(?: BLOCK)?-----/g;
|
|
2966
|
+
ASSIGNED_VALUE = String.raw`(?:"([^"\n]{6,})"|'([^'\n]{6,})'|["']?([^\s"'#]{6,}))`;
|
|
2923
2967
|
RULES2 = [
|
|
2924
2968
|
{
|
|
2925
2969
|
policyId: "secrets.aws-access-key",
|
|
@@ -2935,6 +2979,13 @@ var init_secrets = __esm(() => {
|
|
|
2935
2979
|
confidence: 0.97,
|
|
2936
2980
|
remediation: "Revoke the GitHub token and rotate it."
|
|
2937
2981
|
},
|
|
2982
|
+
{
|
|
2983
|
+
policyId: "secrets.github-token",
|
|
2984
|
+
regex: /\bgithub_pat_[A-Za-z0-9_]{50,255}\b/g,
|
|
2985
|
+
severity: "critical",
|
|
2986
|
+
confidence: 0.97,
|
|
2987
|
+
remediation: "Revoke the GitHub token and rotate it."
|
|
2988
|
+
},
|
|
2938
2989
|
{
|
|
2939
2990
|
policyId: "secrets.google-api-key",
|
|
2940
2991
|
regex: /\bAIza[0-9A-Za-z_-]{35}\b/g,
|
|
@@ -2965,14 +3016,15 @@ var init_secrets = __esm(() => {
|
|
|
2965
3016
|
},
|
|
2966
3017
|
{
|
|
2967
3018
|
policyId: "secrets.private-key-block",
|
|
2968
|
-
regex:
|
|
3019
|
+
regex: PRIVATE_KEY_BEGIN,
|
|
3020
|
+
scan: scanPrivateKeyBlocks,
|
|
2969
3021
|
severity: "critical",
|
|
2970
3022
|
confidence: 0.99,
|
|
2971
3023
|
remediation: "Remove the private key and rotate the corresponding key pair."
|
|
2972
3024
|
},
|
|
2973
3025
|
{
|
|
2974
3026
|
policyId: "secrets.url-credentials",
|
|
2975
|
-
regex:
|
|
3027
|
+
regex: /[a-z][a-z0-9+.-]{0,31}:\/\/[^/\s:@]+:([^/\s:@]+)@[^\s/]+/gi,
|
|
2976
3028
|
severity: "high",
|
|
2977
3029
|
confidence: 0.92,
|
|
2978
3030
|
remediation: "Remove inline credentials from URLs; use a secrets store."
|
|
@@ -3002,7 +3054,7 @@ var init_secrets = __esm(() => {
|
|
|
3002
3054
|
},
|
|
3003
3055
|
{
|
|
3004
3056
|
policyId: "secrets.credential-file-assignment",
|
|
3005
|
-
regex:
|
|
3057
|
+
regex: new RegExp(String.raw`\b((?:aws_)?secret_access_key|aws_session_token|aws_secret_key|client_secret)\s*[:=]\s*` + ASSIGNED_VALUE, "gid"),
|
|
3006
3058
|
severity: "critical",
|
|
3007
3059
|
confidence: 0.95,
|
|
3008
3060
|
remediation: "Rotate the credential; it was read from a credentials file into agent-visible output.",
|
|
@@ -3010,7 +3062,7 @@ var init_secrets = __esm(() => {
|
|
|
3010
3062
|
},
|
|
3011
3063
|
{
|
|
3012
3064
|
policyId: "secrets.env-assignment",
|
|
3013
|
-
regex:
|
|
3065
|
+
regex: new RegExp(String.raw`\b([A-Z0-9_]*(?:DATABASE_URL|JWT_SECRET|SECRET(?:_KEY)?|API_?KEY|ACCESS_?KEY|ACCESS_?TOKEN|AUTH_?TOKEN|PASSWORD|PRIVATE_?KEY|TOKEN))["']?\s*[:=]\s*` + ASSIGNED_VALUE, "gd"),
|
|
3014
3066
|
severity: "high",
|
|
3015
3067
|
confidence: 0.85,
|
|
3016
3068
|
remediation: "Move the secret to an untracked env file or secrets manager.",
|
|
@@ -3021,7 +3073,16 @@ var init_secrets = __esm(() => {
|
|
|
3021
3073
|
|
|
3022
3074
|
// src/security/detect/egress.ts
|
|
3023
3075
|
function hostOf(url) {
|
|
3024
|
-
|
|
3076
|
+
try {
|
|
3077
|
+
const host = new URL(url).hostname;
|
|
3078
|
+
if (host) {
|
|
3079
|
+
return host.toLowerCase();
|
|
3080
|
+
}
|
|
3081
|
+
} catch {}
|
|
3082
|
+
return literalHostOf(url);
|
|
3083
|
+
}
|
|
3084
|
+
function literalHostOf(url) {
|
|
3085
|
+
const m = /^https?:[/\\]{2}([^/?#\\]+)/i.exec(url);
|
|
3025
3086
|
if (!m || m[1] === undefined) {
|
|
3026
3087
|
return null;
|
|
3027
3088
|
}
|
|
@@ -3030,13 +3091,19 @@ function hostOf(url) {
|
|
|
3030
3091
|
if (at >= 0) {
|
|
3031
3092
|
authority = authority.slice(at + 1);
|
|
3032
3093
|
}
|
|
3033
|
-
if (
|
|
3034
|
-
const
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3094
|
+
if (authority.startsWith("[")) {
|
|
3095
|
+
const close = authority.indexOf("]");
|
|
3096
|
+
return close > 0 ? authority.slice(0, close + 1).toLowerCase() : null;
|
|
3097
|
+
}
|
|
3098
|
+
const colon = authority.indexOf(":");
|
|
3099
|
+
if (colon >= 0) {
|
|
3100
|
+
authority = authority.slice(0, colon);
|
|
3038
3101
|
}
|
|
3039
|
-
return authority.toLowerCase();
|
|
3102
|
+
return authority.toLowerCase() || null;
|
|
3103
|
+
}
|
|
3104
|
+
function hostCandidates(url) {
|
|
3105
|
+
const hosts = [hostOf(url), literalHostOf(url)].filter((h) => !!h);
|
|
3106
|
+
return [...new Set(hosts)];
|
|
3040
3107
|
}
|
|
3041
3108
|
function hostAllowed(host, allowlist) {
|
|
3042
3109
|
const h = host.toLowerCase();
|
|
@@ -3045,46 +3112,130 @@ function hostAllowed(host, allowlist) {
|
|
|
3045
3112
|
return h === e || h.endsWith(`.${e}`);
|
|
3046
3113
|
});
|
|
3047
3114
|
}
|
|
3048
|
-
function
|
|
3049
|
-
|
|
3050
|
-
if (h === "metadata.google.internal" || h === "localhost") {
|
|
3115
|
+
function isPrivateV4(a, b) {
|
|
3116
|
+
if (a === 0)
|
|
3051
3117
|
return true;
|
|
3052
|
-
|
|
3118
|
+
if (a === 10)
|
|
3119
|
+
return true;
|
|
3120
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
3121
|
+
return true;
|
|
3122
|
+
if (a === 127)
|
|
3123
|
+
return true;
|
|
3124
|
+
if (a === 169 && b === 254)
|
|
3125
|
+
return true;
|
|
3126
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
3127
|
+
return true;
|
|
3128
|
+
if (a === 192 && b === 168)
|
|
3129
|
+
return true;
|
|
3130
|
+
return false;
|
|
3131
|
+
}
|
|
3132
|
+
function decimalV4(h) {
|
|
3053
3133
|
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
|
|
3134
|
+
if (!v4)
|
|
3135
|
+
return null;
|
|
3136
|
+
const o = v4.slice(1, 5).map((n) => Number(n));
|
|
3137
|
+
if (o.some((n) => n > 255))
|
|
3138
|
+
return null;
|
|
3139
|
+
return [o[0], o[1]];
|
|
3140
|
+
}
|
|
3141
|
+
function whatwgV4(h) {
|
|
3142
|
+
const parts = h.split(".");
|
|
3143
|
+
if (parts.length > 1 && parts[parts.length - 1] === "")
|
|
3144
|
+
parts.pop();
|
|
3145
|
+
if (parts.length < 1 || parts.length > 4)
|
|
3146
|
+
return null;
|
|
3147
|
+
const nums = [];
|
|
3148
|
+
for (const p of parts) {
|
|
3149
|
+
let n;
|
|
3150
|
+
if (/^0x[0-9a-f]*$/i.test(p))
|
|
3151
|
+
n = p.length === 2 ? 0 : parseInt(p.slice(2), 16);
|
|
3152
|
+
else if (/^0[0-7]+$/.test(p))
|
|
3153
|
+
n = parseInt(p.slice(1), 8);
|
|
3154
|
+
else if (/^(0|[1-9]\d*)$/.test(p))
|
|
3155
|
+
n = Number(p);
|
|
3156
|
+
else
|
|
3157
|
+
return null;
|
|
3158
|
+
nums.push(n);
|
|
3159
|
+
}
|
|
3160
|
+
const last = nums.pop();
|
|
3161
|
+
if (nums.some((n) => n > 255) || last >= 256 ** (4 - nums.length))
|
|
3162
|
+
return null;
|
|
3163
|
+
let value = last;
|
|
3164
|
+
nums.forEach((n, i) => {
|
|
3165
|
+
value += n * 256 ** (3 - i);
|
|
3166
|
+
});
|
|
3167
|
+
return [Math.floor(value / 2 ** 24), Math.floor(value / 2 ** 16) % 256];
|
|
3168
|
+
}
|
|
3169
|
+
function parseV6(h) {
|
|
3170
|
+
let text = h;
|
|
3171
|
+
const tail = [];
|
|
3172
|
+
const v4 = /^(.*:)(\d{1,3}(?:\.\d{1,3}){3})$/.exec(text);
|
|
3054
3173
|
if (v4) {
|
|
3055
|
-
const o = v4.
|
|
3056
|
-
if (o.some((n) => n > 255))
|
|
3057
|
-
return
|
|
3174
|
+
const o = v4[2].split(".").map(Number);
|
|
3175
|
+
if (o.some((n) => n > 255))
|
|
3176
|
+
return null;
|
|
3177
|
+
tail.push(o[0] << 8 | o[1], o[2] << 8 | o[3]);
|
|
3178
|
+
text = `${v4[1]}0`;
|
|
3179
|
+
}
|
|
3180
|
+
const halves = text.split("::");
|
|
3181
|
+
if (halves.length > 2)
|
|
3182
|
+
return null;
|
|
3183
|
+
const read = (s) => {
|
|
3184
|
+
if (s === "")
|
|
3185
|
+
return [];
|
|
3186
|
+
const out = [];
|
|
3187
|
+
for (const g of s.split(":")) {
|
|
3188
|
+
if (!/^[0-9a-f]{1,4}$/.test(g))
|
|
3189
|
+
return null;
|
|
3190
|
+
out.push(parseInt(g, 16));
|
|
3058
3191
|
}
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3192
|
+
return out;
|
|
3193
|
+
};
|
|
3194
|
+
const head = read(halves[0]);
|
|
3195
|
+
const rest = halves.length === 2 ? read(halves[1]) : [];
|
|
3196
|
+
if (!head || !rest)
|
|
3197
|
+
return null;
|
|
3198
|
+
if (tail.length > 0)
|
|
3199
|
+
(rest.length > 0 ? rest : head).pop();
|
|
3200
|
+
const given = head.length + rest.length + tail.length;
|
|
3201
|
+
if (halves.length === 1 ? given !== 8 : given > 7)
|
|
3202
|
+
return null;
|
|
3203
|
+
return [...head, ...new Array(8 - given).fill(0), ...rest, ...tail];
|
|
3204
|
+
}
|
|
3205
|
+
function isSsrfHost(host) {
|
|
3206
|
+
const h = host.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
|
|
3207
|
+
if (h === "metadata.google.internal" || h === "localhost" || h.endsWith(".localhost")) {
|
|
3208
|
+
return true;
|
|
3209
|
+
}
|
|
3210
|
+
if (h.includes(":")) {
|
|
3211
|
+
const g = parseV6(h.replace(/%.*$/, ""));
|
|
3212
|
+
if (!g)
|
|
3213
|
+
return false;
|
|
3214
|
+
const [g0, , , , , g5, g6, g7] = g;
|
|
3215
|
+
if (g.slice(0, 7).every((x) => x === 0) && g7 <= 1)
|
|
3065
3216
|
return true;
|
|
3066
|
-
if (
|
|
3217
|
+
if ((g0 & 65024) === 64512)
|
|
3067
3218
|
return true;
|
|
3068
|
-
if (
|
|
3219
|
+
if ((g0 & 65472) === 65152)
|
|
3069
3220
|
return true;
|
|
3221
|
+
if (g.slice(0, 5).every((x) => x === 0) && g5 === 65535) {
|
|
3222
|
+
return isPrivateV4(g6 >> 8, g6 & 255);
|
|
3223
|
+
}
|
|
3070
3224
|
return false;
|
|
3071
3225
|
}
|
|
3072
|
-
|
|
3073
|
-
return true;
|
|
3074
|
-
}
|
|
3075
|
-
return false;
|
|
3226
|
+
return [decimalV4(h), whatwgV4(h)].some((v4) => v4 !== null && isPrivateV4(v4[0], v4[1]));
|
|
3076
3227
|
}
|
|
3077
3228
|
function detectEgress(content, allowlist = []) {
|
|
3078
3229
|
const matches = [];
|
|
3079
|
-
const ssrfSpans =
|
|
3230
|
+
const ssrfSpans = [];
|
|
3080
3231
|
EXTERNAL_URL.lastIndex = 0;
|
|
3081
3232
|
let m;
|
|
3082
3233
|
while ((m = EXTERNAL_URL.exec(content)) !== null) {
|
|
3083
3234
|
const url = m[0];
|
|
3084
3235
|
const start = m.index;
|
|
3085
3236
|
const end = start + url.length;
|
|
3086
|
-
const
|
|
3087
|
-
if (
|
|
3237
|
+
const hosts = hostCandidates(url);
|
|
3238
|
+
if (hosts.some(isSsrfHost)) {
|
|
3088
3239
|
matches.push({
|
|
3089
3240
|
category: "egress",
|
|
3090
3241
|
policyId: "egress.ssrf-metadata",
|
|
@@ -3095,8 +3246,8 @@ function detectEgress(content, allowlist = []) {
|
|
|
3095
3246
|
value: url,
|
|
3096
3247
|
remediation: "Block requests to private, loopback, link-local, or cloud-metadata hosts (SSRF)."
|
|
3097
3248
|
});
|
|
3098
|
-
ssrfSpans.
|
|
3099
|
-
} else if (
|
|
3249
|
+
ssrfSpans.push({ start, end });
|
|
3250
|
+
} else if (hosts.length > 0 && allowlist.length > 0 && hosts.some((h) => !hostAllowed(h, allowlist))) {
|
|
3100
3251
|
matches.push({
|
|
3101
3252
|
category: "egress",
|
|
3102
3253
|
policyId: "egress.non-allowlisted-domain",
|
|
@@ -3126,14 +3277,16 @@ function detectEgress(content, allowlist = []) {
|
|
|
3126
3277
|
}
|
|
3127
3278
|
}
|
|
3128
3279
|
BARE_SSRF_HOST.lastIndex = 0;
|
|
3280
|
+
let span = 0;
|
|
3129
3281
|
while ((m = BARE_SSRF_HOST.exec(content)) !== null) {
|
|
3130
3282
|
const value = m[0];
|
|
3131
3283
|
const start = m.index;
|
|
3132
3284
|
const end = start + value.length;
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3285
|
+
while (span < ssrfSpans.length && ssrfSpans[span].end <= start) {
|
|
3286
|
+
span += 1;
|
|
3287
|
+
}
|
|
3288
|
+
const cur = ssrfSpans[span];
|
|
3289
|
+
const covered = cur !== undefined && start >= cur.start && end <= cur.end;
|
|
3137
3290
|
if (!covered && isSsrfHost(value)) {
|
|
3138
3291
|
matches.push({
|
|
3139
3292
|
category: "egress",
|
|
@@ -3258,7 +3411,7 @@ function isExfilDestination(url, allowlist) {
|
|
|
3258
3411
|
return !(allowlist.length > 0 && hostAllowed(host, allowlist));
|
|
3259
3412
|
}
|
|
3260
3413
|
function considerUrl(hit, allowlist, out) {
|
|
3261
|
-
if (!isExfilDestination(hit.classify ?? hit.url, allowlist)) {
|
|
3414
|
+
if (!hit.unexamined && !isExfilDestination(hit.classify ?? hit.url, allowlist)) {
|
|
3262
3415
|
return;
|
|
3263
3416
|
}
|
|
3264
3417
|
out.push({
|
|
@@ -3649,27 +3802,181 @@ function spanHasNonWhitespace(content, index, budget, start, end) {
|
|
|
3649
3802
|
budget.work -= width;
|
|
3650
3803
|
return stripBlockContainerMarkers(content.slice(start, end)).trim().length > 0;
|
|
3651
3804
|
}
|
|
3805
|
+
function newInlineDestinationCache() {
|
|
3806
|
+
return { readings: new Map, heads: null, bare: new Map };
|
|
3807
|
+
}
|
|
3808
|
+
function isAsciiPunctuation(code) {
|
|
3809
|
+
return code >= 33 && code <= 47 || code >= 58 && code <= 64 || code >= 91 && code <= 96 || code >= 123 && code <= 126;
|
|
3810
|
+
}
|
|
3811
|
+
function classificationHeadEnd(content, from, cache) {
|
|
3812
|
+
const heads = cache.heads ??= new Int32Array(content.length + 1);
|
|
3813
|
+
let at = from;
|
|
3814
|
+
while (at < content.length) {
|
|
3815
|
+
const known = heads[at];
|
|
3816
|
+
if (known !== 0) {
|
|
3817
|
+
at = known - 1;
|
|
3818
|
+
break;
|
|
3819
|
+
}
|
|
3820
|
+
const code = content.charCodeAt(at);
|
|
3821
|
+
if (code === 41 || isCollapsibleSpace(code, content[at]))
|
|
3822
|
+
break;
|
|
3823
|
+
at += 1;
|
|
3824
|
+
}
|
|
3825
|
+
for (let fill = from;fill < at && heads[fill] === 0; fill += 1)
|
|
3826
|
+
heads[fill] = at + 1;
|
|
3827
|
+
return at;
|
|
3828
|
+
}
|
|
3829
|
+
function bareDestinationEnd(content, from, memo) {
|
|
3830
|
+
const known = memo.get(from);
|
|
3831
|
+
if (known !== undefined)
|
|
3832
|
+
return known;
|
|
3833
|
+
const opens = [];
|
|
3834
|
+
const fail = () => {
|
|
3835
|
+
for (const start of opens)
|
|
3836
|
+
memo.set(start, -1);
|
|
3837
|
+
memo.set(from, -1);
|
|
3838
|
+
return -1;
|
|
3839
|
+
};
|
|
3840
|
+
let at = from;
|
|
3841
|
+
for (;; ) {
|
|
3842
|
+
const code = at < content.length ? content.charCodeAt(at) : 0;
|
|
3843
|
+
if (code <= 32 || code === 127 || isCollapsibleSpace(code, content[at])) {
|
|
3844
|
+
if (opens.length === 0)
|
|
3845
|
+
break;
|
|
3846
|
+
memo.set(opens.pop(), at);
|
|
3847
|
+
return fail();
|
|
3848
|
+
}
|
|
3849
|
+
if (code === 92 && isAsciiPunctuation(content.charCodeAt(at + 1))) {
|
|
3850
|
+
at += 2;
|
|
3851
|
+
continue;
|
|
3852
|
+
}
|
|
3853
|
+
if (code === 40) {
|
|
3854
|
+
const inner = memo.get(at + 1);
|
|
3855
|
+
if (inner === undefined) {
|
|
3856
|
+
if (opens.length >= MAX_DESTINATION_PAREN_DEPTH)
|
|
3857
|
+
return fail();
|
|
3858
|
+
opens.push(at + 1);
|
|
3859
|
+
at += 1;
|
|
3860
|
+
continue;
|
|
3861
|
+
}
|
|
3862
|
+
if (inner === -1 || content.charCodeAt(inner) !== 41)
|
|
3863
|
+
return fail();
|
|
3864
|
+
at = inner + 1;
|
|
3865
|
+
continue;
|
|
3866
|
+
}
|
|
3867
|
+
if (code === 41) {
|
|
3868
|
+
if (opens.length === 0)
|
|
3869
|
+
break;
|
|
3870
|
+
memo.set(opens.pop(), at);
|
|
3871
|
+
}
|
|
3872
|
+
at += 1;
|
|
3873
|
+
}
|
|
3874
|
+
memo.set(from, at);
|
|
3875
|
+
return at;
|
|
3876
|
+
}
|
|
3877
|
+
function skipSpacesAndTabs(content, from) {
|
|
3878
|
+
let at = from;
|
|
3879
|
+
while (at < content.length) {
|
|
3880
|
+
const code = content.charCodeAt(at);
|
|
3881
|
+
if (code !== 32 && code !== 9)
|
|
3882
|
+
break;
|
|
3883
|
+
at += 1;
|
|
3884
|
+
}
|
|
3885
|
+
return at;
|
|
3886
|
+
}
|
|
3887
|
+
function resolvesAsInline(content, from, at, memo) {
|
|
3888
|
+
for (let gap = from;gap < at; gap += 1) {
|
|
3889
|
+
const code = content.charCodeAt(gap);
|
|
3890
|
+
if (code !== 32 && code !== 9)
|
|
3891
|
+
return false;
|
|
3892
|
+
}
|
|
3893
|
+
let cursor = at;
|
|
3894
|
+
if (content.charCodeAt(cursor) === 60) {
|
|
3895
|
+
for (cursor += 1;; cursor += 1) {
|
|
3896
|
+
if (cursor >= content.length)
|
|
3897
|
+
return false;
|
|
3898
|
+
const code = content.charCodeAt(cursor);
|
|
3899
|
+
if (code === 92 && isAsciiPunctuation(content.charCodeAt(cursor + 1))) {
|
|
3900
|
+
cursor += 1;
|
|
3901
|
+
continue;
|
|
3902
|
+
}
|
|
3903
|
+
if (code === 62)
|
|
3904
|
+
break;
|
|
3905
|
+
if (code === 60 || code === 10 || code === 13)
|
|
3906
|
+
return false;
|
|
3907
|
+
}
|
|
3908
|
+
cursor += 1;
|
|
3909
|
+
} else {
|
|
3910
|
+
cursor = bareDestinationEnd(content, cursor, memo);
|
|
3911
|
+
if (cursor === -1)
|
|
3912
|
+
return false;
|
|
3913
|
+
}
|
|
3914
|
+
const destinationEnd = cursor;
|
|
3915
|
+
cursor = skipSpacesAndTabs(content, cursor);
|
|
3916
|
+
if (content[cursor] === ")")
|
|
3917
|
+
return true;
|
|
3918
|
+
if (cursor === destinationEnd)
|
|
3919
|
+
return false;
|
|
3920
|
+
const opener = content[cursor];
|
|
3921
|
+
const closer = opener === "(" ? ")" : opener === '"' || opener === "'" ? opener : null;
|
|
3922
|
+
if (closer === null)
|
|
3923
|
+
return false;
|
|
3924
|
+
for (cursor += 1;; cursor += 1) {
|
|
3925
|
+
if (cursor >= content.length)
|
|
3926
|
+
return false;
|
|
3927
|
+
const code = content.charCodeAt(cursor);
|
|
3928
|
+
if (code === 92 && isAsciiPunctuation(content.charCodeAt(cursor + 1))) {
|
|
3929
|
+
cursor += 1;
|
|
3930
|
+
continue;
|
|
3931
|
+
}
|
|
3932
|
+
if (content[cursor] === closer)
|
|
3933
|
+
break;
|
|
3934
|
+
if (opener === "(" && code === 40)
|
|
3935
|
+
return false;
|
|
3936
|
+
if (LINE_TERMINATORS.has(code))
|
|
3937
|
+
return false;
|
|
3938
|
+
}
|
|
3939
|
+
return content[skipSpacesAndTabs(content, cursor + 1)] === ")";
|
|
3940
|
+
}
|
|
3652
3941
|
function readInlineDestination(content, index, descriptionEnd, cache) {
|
|
3653
|
-
const cached = cache.get(descriptionEnd);
|
|
3942
|
+
const cached = cache.readings.get(descriptionEnd);
|
|
3654
3943
|
if (cached !== undefined)
|
|
3655
3944
|
return cached;
|
|
3656
3945
|
let destination = null;
|
|
3657
3946
|
if (content[descriptionEnd] === "(" && firstAtOrAfter(index.closeParens, descriptionEnd) !== -1) {
|
|
3658
3947
|
const at = skipDestinationLeadingWhitespace(content, descriptionEnd + 1);
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
const url = match ? match[1] ?? match[2] ?? "" : "";
|
|
3662
|
-
if (match && url) {
|
|
3948
|
+
const head = readClassifiedHead(content, index, at, cache);
|
|
3949
|
+
if (head) {
|
|
3663
3950
|
destination = {
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
end: INLINE_DESTINATION.lastIndex
|
|
3951
|
+
...head,
|
|
3952
|
+
resolves: resolvesAsInline(content, descriptionEnd + 1, at, cache.bare)
|
|
3667
3953
|
};
|
|
3668
3954
|
}
|
|
3669
3955
|
}
|
|
3670
|
-
cache.set(descriptionEnd, destination);
|
|
3956
|
+
cache.readings.set(descriptionEnd, destination);
|
|
3671
3957
|
return destination;
|
|
3672
3958
|
}
|
|
3959
|
+
function readClassifiedHead(content, index, at, cache) {
|
|
3960
|
+
if (content[at] === "<") {
|
|
3961
|
+
let close = at + 1;
|
|
3962
|
+
while (close < content.length && !`<>
|
|
3963
|
+
`.includes(content[close])) {
|
|
3964
|
+
close += 1;
|
|
3965
|
+
}
|
|
3966
|
+
const paren = content[close] === ">" ? firstAtOrAfter(index.closeParens, close + 1) : -1;
|
|
3967
|
+
if (paren !== -1) {
|
|
3968
|
+
const url = content.slice(at + 1, close);
|
|
3969
|
+
return url ? { url, start: at + 1, end: paren + 1 } : null;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
const headEnd = classificationHeadEnd(content, at, cache);
|
|
3973
|
+
if (headEnd === at)
|
|
3974
|
+
return null;
|
|
3975
|
+
const paren = firstAtOrAfter(index.closeParens, headEnd);
|
|
3976
|
+
if (paren === -1)
|
|
3977
|
+
return null;
|
|
3978
|
+
return { url: content.slice(at, headEnd), start: at, end: paren + 1 };
|
|
3979
|
+
}
|
|
3673
3980
|
function readBracketConstructs(content, index, open, destinations, budget, wantReference) {
|
|
3674
3981
|
const constructs = [];
|
|
3675
3982
|
const { ends, blind } = descriptionEnds(index, open);
|
|
@@ -3684,7 +3991,8 @@ function readBracketConstructs(content, index, open, destinations, budget, wantR
|
|
|
3684
3991
|
start: inline.start,
|
|
3685
3992
|
descriptionEnd,
|
|
3686
3993
|
end: inline.end,
|
|
3687
|
-
escapeAware
|
|
3994
|
+
escapeAware,
|
|
3995
|
+
resolves: inline.resolves
|
|
3688
3996
|
});
|
|
3689
3997
|
continue;
|
|
3690
3998
|
}
|
|
@@ -3792,13 +4100,8 @@ function readStartTag(content, from) {
|
|
|
3792
4100
|
}
|
|
3793
4101
|
return { attributes, end: index };
|
|
3794
4102
|
}
|
|
3795
|
-
function isSensitiveDestination(url
|
|
3796
|
-
|
|
3797
|
-
if (cached !== undefined)
|
|
3798
|
-
return cached;
|
|
3799
|
-
const sensitive = SENSITIVE_URL_VALUE.test(url);
|
|
3800
|
-
cache.set(descriptionEnd, sensitive);
|
|
3801
|
-
return sensitive;
|
|
4103
|
+
function isSensitiveDestination(url) {
|
|
4104
|
+
return SENSITIVE_URL_VALUE.test(url) || SENSITIVE_URL_VALUE.test(renderableUrl(url));
|
|
3802
4105
|
}
|
|
3803
4106
|
function countCommas(value) {
|
|
3804
4107
|
let total = 0;
|
|
@@ -3811,6 +4114,8 @@ function countCommas(value) {
|
|
|
3811
4114
|
function detectExfil(content, allowlist = []) {
|
|
3812
4115
|
const matches = [];
|
|
3813
4116
|
let m;
|
|
4117
|
+
if (!content.includes("[") && !content.includes("<"))
|
|
4118
|
+
return matches;
|
|
3814
4119
|
const {
|
|
3815
4120
|
refs,
|
|
3816
4121
|
maxLength: maxLabelLength,
|
|
@@ -3824,11 +4129,12 @@ function detectExfil(content, allowlist = []) {
|
|
|
3824
4129
|
work: LABEL_WORK_FACTOR * content.length,
|
|
3825
4130
|
exhausted: false
|
|
3826
4131
|
};
|
|
3827
|
-
const destinations =
|
|
4132
|
+
const destinations = newInlineDestinationCache();
|
|
3828
4133
|
let destinationStart = -1;
|
|
3829
4134
|
let destinationEnd = -1;
|
|
3830
|
-
const sensitive = new Map;
|
|
3831
4135
|
const flaggedInlineStarts = new Set;
|
|
4136
|
+
const examinedLinkStarts = new Set;
|
|
4137
|
+
let classifyWork = INLINE_CLASSIFY_WORK_FACTOR * content.length;
|
|
3832
4138
|
BRACKET_OPEN.lastIndex = 0;
|
|
3833
4139
|
while ((m = BRACKET_OPEN.exec(content)) !== null) {
|
|
3834
4140
|
const isImage = m[1] === "!";
|
|
@@ -3841,7 +4147,7 @@ function detectExfil(content, allowlist = []) {
|
|
|
3841
4147
|
if (!inline || inline.kind !== "inline") {
|
|
3842
4148
|
continue;
|
|
3843
4149
|
}
|
|
3844
|
-
if (inline.escapeAware) {} else if (isImage) {
|
|
4150
|
+
if (inline.escapeAware || !inline.resolves) {} else if (isImage) {
|
|
3845
4151
|
BRACKET_OPEN.lastIndex = inline.end;
|
|
3846
4152
|
} else {
|
|
3847
4153
|
destinationStart = inline.descriptionEnd;
|
|
@@ -3849,15 +4155,26 @@ function detectExfil(content, allowlist = []) {
|
|
|
3849
4155
|
}
|
|
3850
4156
|
if (flaggedInlineStarts.has(inline.start))
|
|
3851
4157
|
continue;
|
|
3852
|
-
if (!isImage
|
|
3853
|
-
|
|
4158
|
+
if (!isImage) {
|
|
4159
|
+
if (examinedLinkStarts.has(inline.start))
|
|
4160
|
+
continue;
|
|
4161
|
+
examinedLinkStarts.add(inline.start);
|
|
3854
4162
|
}
|
|
3855
|
-
|
|
3856
|
-
considerUrl({
|
|
4163
|
+
const hit = {
|
|
3857
4164
|
url: inline.url,
|
|
3858
4165
|
start: inline.start,
|
|
3859
4166
|
policyId: isImage ? "egress.markdown-image-exfil" : "egress.markdown-link-sensitive-value"
|
|
3860
|
-
}
|
|
4167
|
+
};
|
|
4168
|
+
if (inline.url.length > classifyWork) {
|
|
4169
|
+
flaggedInlineStarts.add(inline.start);
|
|
4170
|
+
considerUrl({ ...hit, unexamined: true }, allowlist, matches);
|
|
4171
|
+
continue;
|
|
4172
|
+
}
|
|
4173
|
+
classifyWork -= inline.url.length;
|
|
4174
|
+
if (!isImage && !isSensitiveDestination(inline.url))
|
|
4175
|
+
continue;
|
|
4176
|
+
flaggedInlineStarts.add(inline.start);
|
|
4177
|
+
considerUrl(hit, allowlist, matches);
|
|
3861
4178
|
}
|
|
3862
4179
|
destinationStart = -1;
|
|
3863
4180
|
destinationEnd = -1;
|
|
@@ -3873,7 +4190,7 @@ function detectExfil(content, allowlist = []) {
|
|
|
3873
4190
|
const constructs = readBracketConstructs(content, index, open, destinations, budget, isImage);
|
|
3874
4191
|
const inline = constructs.find((construct) => construct.kind === "inline");
|
|
3875
4192
|
if (inline && inline.kind === "inline") {
|
|
3876
|
-
if (!inline.escapeAware) {
|
|
4193
|
+
if (!inline.escapeAware && inline.resolves) {
|
|
3877
4194
|
if (isImage)
|
|
3878
4195
|
BRACKET_OPEN.lastIndex = inline.end;
|
|
3879
4196
|
else {
|
|
@@ -4035,7 +4352,7 @@ function scanHtmlStartTags(source, content, origin, allowlist, out) {
|
|
|
4035
4352
|
}
|
|
4036
4353
|
}
|
|
4037
4354
|
}
|
|
4038
|
-
var EXFIL_CONFIDENCE = 0.85, NAMED_CHARACTER_REFERENCES, CHARACTER_REFERENCE, MAX_DECODE_PASSES = 3, MAX_CODE_POINT = 1114111, URL_STRIPPED_CHARACTERS, C0_OR_SPACE = 32, SYNTHETIC_BASE_PAIRS, FETCH_REMEDIATION = "Strip or allowlist auto-fetched destinations in rendered output \u2014 markdown images, <img>/<image>/<input type=image>/<video poster>/background, media and embedded-content elements, and any <base href> that re-points them; they exfiltrate context on render.", META_REFRESH_REMEDIATION = "Strip or allowlist this <meta http-equiv=refresh>. It is not a subresource fetch: on render it navigates the whole client to the destination, so the entire directive is masked rather than the URL alone.", BASE_REMEDIATION = "Strip or allowlist this <base href>. Unlike an <img src>, masking it re-points EVERY relative URL in the document to the reader's own origin, so a false positive here changes the whole document's resolution, not just one destination.", UNICODE_WHITESPACE, BRACKET_OPEN,
|
|
4355
|
+
var EXFIL_CONFIDENCE = 0.85, NAMED_CHARACTER_REFERENCES, CHARACTER_REFERENCE, MAX_DECODE_PASSES = 3, MAX_CODE_POINT = 1114111, URL_STRIPPED_CHARACTERS, C0_OR_SPACE = 32, SYNTHETIC_BASE_PAIRS, FETCH_REMEDIATION = "Strip or allowlist auto-fetched destinations in rendered output \u2014 markdown images, <img>/<image>/<input type=image>/<video poster>/background, media and embedded-content elements, and any <base href> that re-points them; they exfiltrate context on render.", META_REFRESH_REMEDIATION = "Strip or allowlist this <meta http-equiv=refresh>. It is not a subresource fetch: on render it navigates the whole client to the destination, so the entire directive is masked rather than the URL alone.", BASE_REMEDIATION = "Strip or allowlist this <base href>. Unlike an <img src>, masking it re-points EVERY relative URL in the document to the reader's own origin, so a false positive here changes the whole document's resolution, not just one destination.", UNICODE_WHITESPACE, BRACKET_OPEN, REFERENCE_DESTINATION, LABEL_WHITESPACE_RUN, LINE_TERMINATORS, LABEL_WORK_FACTOR = 8, MAX_DESTINATION_PAREN_DEPTH = 32, HTML_START_TAG, IMAGE_POLICY = "egress.html-image-exfil", MEDIA_POLICY = "egress.html-media-exfil", EMBEDDED_POLICY = "egress.html-embedded-document-exfil", IMAGE_URL, IMAGE_SRCSET, MEDIA_URL, MEDIA_SRCSET, EMBEDDED_URL, BACKGROUND_ONLY, FETCHING_ATTRIBUTES, META_REFRESH_CONTENT, SENSITIVE_URL_VALUE, INLINE_CLASSIFY_WORK_FACTOR = 8, SRCSET_DESCRIPTOR;
|
|
4039
4356
|
var init_exfil = __esm(() => {
|
|
4040
4357
|
init_egress();
|
|
4041
4358
|
NAMED_CHARACTER_REFERENCES = {
|
|
@@ -4085,7 +4402,6 @@ var init_exfil = __esm(() => {
|
|
|
4085
4402
|
];
|
|
4086
4403
|
UNICODE_WHITESPACE = /\s/;
|
|
4087
4404
|
BRACKET_OPEN = /(!?)\[/g;
|
|
4088
|
-
INLINE_DESTINATION = /(?:<([^<>\n]*)>|([^)\s]+))[^)]*\)/y;
|
|
4089
4405
|
REFERENCE_DESTINATION = /(?:<([^<>\n]*)>|(\S+))/y;
|
|
4090
4406
|
LABEL_WHITESPACE_RUN = /\s+/g;
|
|
4091
4407
|
LINE_TERMINATORS = new Set([10, 13, 8232, 8233]);
|
|
@@ -4127,6 +4443,356 @@ var init_exfil = __esm(() => {
|
|
|
4127
4443
|
SRCSET_DESCRIPTOR = /\s+/;
|
|
4128
4444
|
});
|
|
4129
4445
|
|
|
4446
|
+
// src/security/detect/entropy.ts
|
|
4447
|
+
function isAdjacentLabel(content, start) {
|
|
4448
|
+
const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
|
|
4449
|
+
if (ADJACENT_LABEL.test(before)) {
|
|
4450
|
+
return true;
|
|
4451
|
+
}
|
|
4452
|
+
if (before.trim().length > 0) {
|
|
4453
|
+
return false;
|
|
4454
|
+
}
|
|
4455
|
+
let lineStart = start - 1;
|
|
4456
|
+
while (lineStart >= 0 && content[lineStart] !== `
|
|
4457
|
+
` && /\s/.test(content[lineStart])) {
|
|
4458
|
+
lineStart -= 1;
|
|
4459
|
+
}
|
|
4460
|
+
if (lineStart < 0 || content[lineStart] !== `
|
|
4461
|
+
`) {
|
|
4462
|
+
return false;
|
|
4463
|
+
}
|
|
4464
|
+
const prevTail = lastLineOf(content.slice(Math.max(0, lineStart - CONTINUATION_WINDOW), lineStart)).trimEnd();
|
|
4465
|
+
return CONTINUATION_LABEL_LINE_RE.test(prevTail);
|
|
4466
|
+
}
|
|
4467
|
+
function isHexBlob(value) {
|
|
4468
|
+
return HEX_BLOB.test(value) && /[a-f]/i.test(value);
|
|
4469
|
+
}
|
|
4470
|
+
function isAllowShapedValue(value) {
|
|
4471
|
+
return FULL_GIT_SHA_RE.test(value) || UUID_RE.test(value) || INTEGRITY_RE.test(value);
|
|
4472
|
+
}
|
|
4473
|
+
function lastLineOf(before) {
|
|
4474
|
+
const newline = before.lastIndexOf(`
|
|
4475
|
+
`);
|
|
4476
|
+
return newline < 0 ? before : before.slice(newline + 1);
|
|
4477
|
+
}
|
|
4478
|
+
function isPlainCasedWord(letters) {
|
|
4479
|
+
return /^[a-z]+$/.test(letters) || /^[A-Z]+$/.test(letters) || /^[A-Z][a-z]*$/.test(letters);
|
|
4480
|
+
}
|
|
4481
|
+
function isSingleTransitionTag(segment) {
|
|
4482
|
+
const letterThenDigit = /^([A-Za-z]+)([0-9]+)$/.exec(segment);
|
|
4483
|
+
if (letterThenDigit !== null) {
|
|
4484
|
+
const letters = letterThenDigit[1];
|
|
4485
|
+
return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
|
|
4486
|
+
}
|
|
4487
|
+
const digitThenLetter = /^([0-9]+)([A-Za-z]+)$/.exec(segment);
|
|
4488
|
+
if (digitThenLetter !== null) {
|
|
4489
|
+
const letters = digitThenLetter[2];
|
|
4490
|
+
return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
|
|
4491
|
+
}
|
|
4492
|
+
return false;
|
|
4493
|
+
}
|
|
4494
|
+
function isSlugHexTail(segment) {
|
|
4495
|
+
return SLUG_HEX_TAIL_RE.test(segment);
|
|
4496
|
+
}
|
|
4497
|
+
function isWordSlug(value) {
|
|
4498
|
+
const segments = value.split(/[-_]/);
|
|
4499
|
+
if (segments.length < 3) {
|
|
4500
|
+
return false;
|
|
4501
|
+
}
|
|
4502
|
+
let words = 0;
|
|
4503
|
+
for (let i = 0;i < segments.length; i += 1) {
|
|
4504
|
+
const segment = segments[i];
|
|
4505
|
+
const isDigits = /^[0-9]+$/.test(segment);
|
|
4506
|
+
if (isDigits) {
|
|
4507
|
+
continue;
|
|
4508
|
+
}
|
|
4509
|
+
if (isPlainCasedWord(segment)) {
|
|
4510
|
+
if (segment.length >= 3) {
|
|
4511
|
+
words += 1;
|
|
4512
|
+
}
|
|
4513
|
+
continue;
|
|
4514
|
+
}
|
|
4515
|
+
if (isSingleTransitionTag(segment)) {
|
|
4516
|
+
continue;
|
|
4517
|
+
}
|
|
4518
|
+
if (i === segments.length - 1 && isSlugHexTail(segment)) {
|
|
4519
|
+
continue;
|
|
4520
|
+
}
|
|
4521
|
+
return false;
|
|
4522
|
+
}
|
|
4523
|
+
return words >= 1;
|
|
4524
|
+
}
|
|
4525
|
+
function shannonEntropy(value) {
|
|
4526
|
+
const counts = new Map;
|
|
4527
|
+
for (const ch of value) {
|
|
4528
|
+
counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
4529
|
+
}
|
|
4530
|
+
let entropy = 0;
|
|
4531
|
+
for (const count of counts.values()) {
|
|
4532
|
+
const p = count / value.length;
|
|
4533
|
+
entropy -= p * Math.log2(p);
|
|
4534
|
+
}
|
|
4535
|
+
return entropy;
|
|
4536
|
+
}
|
|
4537
|
+
function bareShapeQualifies(head) {
|
|
4538
|
+
if (!/[0-9]/.test(head) && !/[+=]/.test(head)) {
|
|
4539
|
+
return { qualifies: false, entropy: 0 };
|
|
4540
|
+
}
|
|
4541
|
+
if (isWordSlug(head)) {
|
|
4542
|
+
return { qualifies: false, entropy: 0 };
|
|
4543
|
+
}
|
|
4544
|
+
const entropy = shannonEntropy(head);
|
|
4545
|
+
const hexBlob = isHexBlob(head);
|
|
4546
|
+
if (entropy < ENTROPY_FLOOR_BITS && !hexBlob) {
|
|
4547
|
+
return { qualifies: false, entropy };
|
|
4548
|
+
}
|
|
4549
|
+
return { qualifies: true, entropy };
|
|
4550
|
+
}
|
|
4551
|
+
function shapeQualifiesAsSecret(value) {
|
|
4552
|
+
if (isAllowShapedValue(value)) {
|
|
4553
|
+
return { qualifies: false, entropy: 0 };
|
|
4554
|
+
}
|
|
4555
|
+
return bareShapeQualifies(value);
|
|
4556
|
+
}
|
|
4557
|
+
function looksSecretShaped(value) {
|
|
4558
|
+
return shapeQualifiesAsSecret(value).qualifies;
|
|
4559
|
+
}
|
|
4560
|
+
function buildMatch(start, end, value, entropy) {
|
|
4561
|
+
const confidence = Math.max(0.4, Math.min(0.7, 0.4 + (entropy - ENTROPY_FLOOR_BITS) * 0.21));
|
|
4562
|
+
return {
|
|
4563
|
+
category: "secret",
|
|
4564
|
+
policyId: "secrets.high-entropy",
|
|
4565
|
+
severity: "medium",
|
|
4566
|
+
confidence: Number(confidence.toFixed(2)),
|
|
4567
|
+
start,
|
|
4568
|
+
end,
|
|
4569
|
+
value,
|
|
4570
|
+
mask: "entropy",
|
|
4571
|
+
remediation: "Verify this high-entropy value is not a live credential."
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
function overlaps(a, start, end) {
|
|
4575
|
+
return start < a.end && end > a.start;
|
|
4576
|
+
}
|
|
4577
|
+
function isKnownDocumentHost(authority) {
|
|
4578
|
+
if (authority.includes("@")) {
|
|
4579
|
+
return false;
|
|
4580
|
+
}
|
|
4581
|
+
const host = authority.replace(/:[0-9]*$/, "").toLowerCase();
|
|
4582
|
+
return DOCUMENT_HOSTS.has(host) || NOTION_SITE_HOST_RE.test(host);
|
|
4583
|
+
}
|
|
4584
|
+
function findUrlSpans(content) {
|
|
4585
|
+
const spans = [];
|
|
4586
|
+
URL_SPAN_RE.lastIndex = 0;
|
|
4587
|
+
let m;
|
|
4588
|
+
while ((m = URL_SPAN_RE.exec(content)) !== null) {
|
|
4589
|
+
let raw = m[0];
|
|
4590
|
+
while (raw.length > 0 && URL_TRAILING_PUNCTUATION_RE.test(raw)) {
|
|
4591
|
+
raw = raw.slice(0, -1);
|
|
4592
|
+
}
|
|
4593
|
+
if (raw.length > 0) {
|
|
4594
|
+
spans.push({ start: m.index, end: m.index + raw.length, raw });
|
|
4595
|
+
}
|
|
4596
|
+
if (m.index === URL_SPAN_RE.lastIndex) {
|
|
4597
|
+
URL_SPAN_RE.lastIndex += 1;
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
return spans;
|
|
4601
|
+
}
|
|
4602
|
+
function urlComponentSpans(raw) {
|
|
4603
|
+
const spans = [];
|
|
4604
|
+
const schemeMatch = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(raw);
|
|
4605
|
+
if (schemeMatch === null) {
|
|
4606
|
+
return spans;
|
|
4607
|
+
}
|
|
4608
|
+
let i = schemeMatch[0].length;
|
|
4609
|
+
while (i < raw.length && raw[i] !== "/" && raw[i] !== "?" && raw[i] !== "#") {
|
|
4610
|
+
i += 1;
|
|
4611
|
+
}
|
|
4612
|
+
const documentHost = isKnownDocumentHost(raw.slice(schemeMatch[0].length, i));
|
|
4613
|
+
let pathEnd = i;
|
|
4614
|
+
while (pathEnd < raw.length && raw[pathEnd] !== "?" && raw[pathEnd] !== "#") {
|
|
4615
|
+
pathEnd += 1;
|
|
4616
|
+
}
|
|
4617
|
+
let segStart = i;
|
|
4618
|
+
for (let j = i;j <= pathEnd; j += 1) {
|
|
4619
|
+
if (j === pathEnd || raw[j] === "/") {
|
|
4620
|
+
if (j > segStart && !documentHost) {
|
|
4621
|
+
spans.push({ start: segStart, end: j, kind: "path" });
|
|
4622
|
+
}
|
|
4623
|
+
segStart = j + 1;
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
let cursor = pathEnd;
|
|
4627
|
+
if (cursor < raw.length && raw[cursor] === "?") {
|
|
4628
|
+
let queryEnd = cursor + 1;
|
|
4629
|
+
while (queryEnd < raw.length && raw[queryEnd] !== "#") {
|
|
4630
|
+
queryEnd += 1;
|
|
4631
|
+
}
|
|
4632
|
+
let pairStart = cursor + 1;
|
|
4633
|
+
for (let j = cursor + 1;j <= queryEnd; j += 1) {
|
|
4634
|
+
if (j === queryEnd || raw[j] === "&") {
|
|
4635
|
+
const pair = raw.slice(pairStart, j);
|
|
4636
|
+
const eq = pair.indexOf("=");
|
|
4637
|
+
if (eq !== -1 && eq + 1 < pair.length) {
|
|
4638
|
+
spans.push({ start: pairStart + eq + 1, end: j, kind: "query" });
|
|
4639
|
+
} else if (eq === -1 && pair.length > 0) {
|
|
4640
|
+
spans.push({ start: pairStart, end: j, kind: "query" });
|
|
4641
|
+
}
|
|
4642
|
+
pairStart = j + 1;
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
cursor = queryEnd;
|
|
4646
|
+
}
|
|
4647
|
+
if (cursor < raw.length && raw[cursor] === "#" && cursor + 1 < raw.length) {
|
|
4648
|
+
spans.push({ start: cursor + 1, end: raw.length, kind: "fragment" });
|
|
4649
|
+
}
|
|
4650
|
+
return spans;
|
|
4651
|
+
}
|
|
4652
|
+
function isQueryWord(word) {
|
|
4653
|
+
return /^(?:[A-Za-z]*|[0-9]+)$/.test(word) || isSingleTransitionTag(word);
|
|
4654
|
+
}
|
|
4655
|
+
function isQueryWordRun(run) {
|
|
4656
|
+
return run.includes("+") && run.split("+").every((piece) => piece.split(/[-_]/).every(isQueryWord));
|
|
4657
|
+
}
|
|
4658
|
+
function secretShapedCandidatesIn(text) {
|
|
4659
|
+
const normalized = text.replace(TOKEN_RUN, (run) => isQueryWordRun(run) ? run.replace(/\+/g, " ") : run);
|
|
4660
|
+
const out = [];
|
|
4661
|
+
TOKEN.lastIndex = 0;
|
|
4662
|
+
let m;
|
|
4663
|
+
while ((m = TOKEN.exec(normalized)) !== null) {
|
|
4664
|
+
const value = m[0];
|
|
4665
|
+
if (value.length < URL_COMPONENT_MIN_LENGTH) {
|
|
4666
|
+
continue;
|
|
4667
|
+
}
|
|
4668
|
+
const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
|
|
4669
|
+
const { qualifies, entropy } = shapeQualifiesAsSecret(head);
|
|
4670
|
+
if (!qualifies) {
|
|
4671
|
+
continue;
|
|
4672
|
+
}
|
|
4673
|
+
const start = m.index;
|
|
4674
|
+
const end = start + value.length;
|
|
4675
|
+
out.push({ start, end, value: text.slice(start, end), entropy });
|
|
4676
|
+
}
|
|
4677
|
+
return out;
|
|
4678
|
+
}
|
|
4679
|
+
function looksSecretShapedIn(text) {
|
|
4680
|
+
return secretShapedCandidatesIn(text).length > 0;
|
|
4681
|
+
}
|
|
4682
|
+
function labelledPieceQualifies(piece) {
|
|
4683
|
+
if (isAllowShapedValue(piece)) {
|
|
4684
|
+
return { entropy: shannonEntropy(piece) };
|
|
4685
|
+
}
|
|
4686
|
+
if (isHexBlob(piece)) {
|
|
4687
|
+
return { entropy: shannonEntropy(piece) };
|
|
4688
|
+
}
|
|
4689
|
+
if (!/[0-9]/.test(piece) && !/[+=]/.test(piece)) {
|
|
4690
|
+
return null;
|
|
4691
|
+
}
|
|
4692
|
+
const withoutSeparators = piece.replace(/[-_.+]/g, "");
|
|
4693
|
+
const entropy = shannonEntropy(withoutSeparators.length > 0 ? withoutSeparators : piece);
|
|
4694
|
+
if (entropy >= ENTROPY_FLOOR_BITS) {
|
|
4695
|
+
return { entropy };
|
|
4696
|
+
}
|
|
4697
|
+
return null;
|
|
4698
|
+
}
|
|
4699
|
+
function isPathWordPiece(piece) {
|
|
4700
|
+
return /^[A-Za-z]{3,}$/.test(piece) && isPlainCasedWord(piece) || isWordSlug(piece);
|
|
4701
|
+
}
|
|
4702
|
+
function labelledValueQualifies(head) {
|
|
4703
|
+
if (!head.includes("/")) {
|
|
4704
|
+
return labelledPieceQualifies(head);
|
|
4705
|
+
}
|
|
4706
|
+
let pathLike = false;
|
|
4707
|
+
for (const piece of head.split("/")) {
|
|
4708
|
+
if (piece.length === 0) {
|
|
4709
|
+
continue;
|
|
4710
|
+
}
|
|
4711
|
+
const found = labelledPieceQualifies(piece);
|
|
4712
|
+
if (found !== null) {
|
|
4713
|
+
return found;
|
|
4714
|
+
}
|
|
4715
|
+
pathLike ||= isPathWordPiece(piece);
|
|
4716
|
+
}
|
|
4717
|
+
return pathLike ? null : labelledPieceQualifies(head.replace(/\//g, ""));
|
|
4718
|
+
}
|
|
4719
|
+
function detectEntropy(content) {
|
|
4720
|
+
const matches = [];
|
|
4721
|
+
const urlSpans = findUrlSpans(content);
|
|
4722
|
+
for (const url of urlSpans) {
|
|
4723
|
+
for (const comp of urlComponentSpans(url.raw)) {
|
|
4724
|
+
const compText = url.raw.slice(comp.start, comp.end);
|
|
4725
|
+
for (const cand of secretShapedCandidatesIn(compText)) {
|
|
4726
|
+
const absStart = url.start + comp.start + cand.start;
|
|
4727
|
+
const absEnd = url.start + comp.start + cand.end;
|
|
4728
|
+
matches.push(buildMatch(absStart, absEnd, cand.value, cand.entropy));
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
LABEL_ASSIGNMENT_RE.lastIndex = 0;
|
|
4733
|
+
let am;
|
|
4734
|
+
while ((am = LABEL_ASSIGNMENT_RE.exec(content)) !== null) {
|
|
4735
|
+
const value = am[1];
|
|
4736
|
+
const end = am.index + am[0].length;
|
|
4737
|
+
const start = end - value.length;
|
|
4738
|
+
if (urlSpans.some((u) => overlaps(u, start, end))) {
|
|
4739
|
+
continue;
|
|
4740
|
+
}
|
|
4741
|
+
const found = labelledValueQualifies(value.split(".")[0]);
|
|
4742
|
+
if (found !== null) {
|
|
4743
|
+
matches.push(buildMatch(start, end, value, found.entropy));
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
TOKEN.lastIndex = 0;
|
|
4747
|
+
let m;
|
|
4748
|
+
while ((m = TOKEN.exec(content)) !== null) {
|
|
4749
|
+
const value = m[0];
|
|
4750
|
+
const start = m.index;
|
|
4751
|
+
const end = start + value.length;
|
|
4752
|
+
if (urlSpans.some((u) => overlaps(u, start, end))) {
|
|
4753
|
+
continue;
|
|
4754
|
+
}
|
|
4755
|
+
if (matches.some((existing) => overlaps(existing, start, end))) {
|
|
4756
|
+
continue;
|
|
4757
|
+
}
|
|
4758
|
+
if (!isAdjacentLabel(content, start)) {
|
|
4759
|
+
continue;
|
|
4760
|
+
}
|
|
4761
|
+
const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
|
|
4762
|
+
const found = labelledValueQualifies(head);
|
|
4763
|
+
if (found === null) {
|
|
4764
|
+
continue;
|
|
4765
|
+
}
|
|
4766
|
+
matches.push(buildMatch(start, end, value, found.entropy));
|
|
4767
|
+
}
|
|
4768
|
+
return matches.sort((a, b) => a.start - b.start);
|
|
4769
|
+
}
|
|
4770
|
+
var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", LABEL_CONTINUATION_MAX = 40, ENTROPY_FLOOR_BITS = 3.6, caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), FILLER_WORDS = "is|was|set|to|the|value|now", ADJACENT_LABEL, CONTINUATION_LABEL_LINE_RE, CONTINUATION_WINDOW, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, TAG_MAX_LETTER_RUN = 8, SLUG_HEX_TAIL_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20, DOCUMENT_HOSTS, NOTION_SITE_HOST_RE, TOKEN_RUN;
|
|
4771
|
+
var init_entropy = __esm(() => {
|
|
4772
|
+
ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]{0,${LABEL_CONTINUATION_MAX}}` + `(?:\\s+(?:${caseless(FILLER_WORDS)})){0,2}` + `["']?\\s*[:=]?\\s*["']?$`);
|
|
4773
|
+
CONTINUATION_LABEL_LINE_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]{0,${LABEL_CONTINUATION_MAX}}["']?\\s*[:=]?\\s*\\\\\\s*$`);
|
|
4774
|
+
CONTINUATION_WINDOW = 2 * LABEL_CONTINUATION_MAX;
|
|
4775
|
+
LABEL_ASSIGNMENT_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${SENSITIVE_LABEL_WORDS})[A-Za-z0-9_-]{0,${LABEL_CONTINUATION_MAX}}"?\\s*=\\s*"?` + `([A-Za-z0-9+/_=-]{6,}(?:\\.[A-Za-z0-9+/_=-]{6,})*)`, "gi");
|
|
4776
|
+
TOKEN = /[A-Za-z0-9+/=_-]{20,}(?:\.[A-Za-z0-9+/=_-]{6,})*/g;
|
|
4777
|
+
TOKEN_HEAD = /^[A-Za-z0-9+/=_-]+/;
|
|
4778
|
+
HEX_BLOB = /^[0-9a-f]{24,}$/i;
|
|
4779
|
+
FULL_GIT_SHA_RE = /^[0-9a-f]{40}$/i;
|
|
4780
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4781
|
+
INTEGRITY_RE = /^(?:sha256|sha512)-[A-Za-z0-9+/]+=*$/;
|
|
4782
|
+
SLUG_HEX_TAIL_RE = /^[0-9a-f]{10,16}$/i;
|
|
4783
|
+
URL_SPAN_RE = /\bhttps?:\/\/[^\s<>"'\\]+/g;
|
|
4784
|
+
URL_TRAILING_PUNCTUATION_RE = /[.,;:!?)\]}]$/;
|
|
4785
|
+
DOCUMENT_HOSTS = new Set([
|
|
4786
|
+
"docs.google.com",
|
|
4787
|
+
"sheets.google.com",
|
|
4788
|
+
"drive.google.com",
|
|
4789
|
+
"notion.so",
|
|
4790
|
+
"www.notion.so"
|
|
4791
|
+
]);
|
|
4792
|
+
NOTION_SITE_HOST_RE = /^[a-z0-9-]+\.notion\.site$/;
|
|
4793
|
+
TOKEN_RUN = /[A-Za-z0-9+/=_-]+/g;
|
|
4794
|
+
});
|
|
4795
|
+
|
|
4130
4796
|
// src/lib/contained-path.ts
|
|
4131
4797
|
import { lstat as lstat3, realpath as realpath3 } from "fs/promises";
|
|
4132
4798
|
import { existsSync as existsSync2, lstatSync, realpathSync } from "fs";
|
|
@@ -4157,7 +4823,7 @@ function isInside(root, candidate) {
|
|
|
4157
4823
|
function outsideProjectMessage(candidate, rootReal) {
|
|
4158
4824
|
return `refused: ${candidate} resolves outside the project root. Paths must stay inside ${rootReal}.`;
|
|
4159
4825
|
}
|
|
4160
|
-
async function resolveContainedPath(projectRoot, candidate) {
|
|
4826
|
+
async function resolveContainedPath(projectRoot, candidate, baseDir) {
|
|
4161
4827
|
try {
|
|
4162
4828
|
const rootStat = await lstat3(projectRoot);
|
|
4163
4829
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
@@ -4167,7 +4833,7 @@ async function resolveContainedPath(projectRoot, candidate) {
|
|
|
4167
4833
|
return { ok: false, reason: "not-found", message: `no such file or directory: ${candidate}` };
|
|
4168
4834
|
}
|
|
4169
4835
|
const rootReal = await realpath3(projectRoot).catch(() => path10.resolve(projectRoot));
|
|
4170
|
-
const absolute = path10.resolve(rootReal, candidate);
|
|
4836
|
+
const absolute = path10.resolve(baseDir ?? rootReal, candidate);
|
|
4171
4837
|
const real = await realpath3(absolute).catch(() => null);
|
|
4172
4838
|
const effective = real ?? absolute;
|
|
4173
4839
|
if (!isInside(rootReal, effective)) {
|
|
@@ -4771,330 +5437,6 @@ var init_config = __esm(() => {
|
|
|
4771
5437
|
]);
|
|
4772
5438
|
});
|
|
4773
5439
|
|
|
4774
|
-
// src/security/detect/entropy.ts
|
|
4775
|
-
function isAdjacentLabel(content, start) {
|
|
4776
|
-
const before = lastLineOf(content.slice(Math.max(0, start - LABEL_WINDOW), start));
|
|
4777
|
-
if (ADJACENT_LABEL.test(before)) {
|
|
4778
|
-
return true;
|
|
4779
|
-
}
|
|
4780
|
-
if (before.trim().length > 0) {
|
|
4781
|
-
return false;
|
|
4782
|
-
}
|
|
4783
|
-
const lineStart = content.lastIndexOf(`
|
|
4784
|
-
`, start - 1);
|
|
4785
|
-
if (lineStart < 0) {
|
|
4786
|
-
return false;
|
|
4787
|
-
}
|
|
4788
|
-
const prevLineStart = content.lastIndexOf(`
|
|
4789
|
-
`, lineStart - 1) + 1;
|
|
4790
|
-
const prevLine = content.slice(prevLineStart, lineStart);
|
|
4791
|
-
return CONTINUATION_LABEL_LINE_RE.test(prevLine);
|
|
4792
|
-
}
|
|
4793
|
-
function isHexBlob(value) {
|
|
4794
|
-
return HEX_BLOB.test(value) && /[a-f]/i.test(value);
|
|
4795
|
-
}
|
|
4796
|
-
function isAllowShapedValue(value) {
|
|
4797
|
-
return FULL_GIT_SHA_RE.test(value) || UUID_RE.test(value) || INTEGRITY_RE.test(value);
|
|
4798
|
-
}
|
|
4799
|
-
function lastLineOf(before) {
|
|
4800
|
-
const newline = before.lastIndexOf(`
|
|
4801
|
-
`);
|
|
4802
|
-
return newline < 0 ? before : before.slice(newline + 1);
|
|
4803
|
-
}
|
|
4804
|
-
function isPlainCasedWord(letters) {
|
|
4805
|
-
return /^[a-z]+$/.test(letters) || /^[A-Z]+$/.test(letters) || /^[A-Z][a-z]*$/.test(letters);
|
|
4806
|
-
}
|
|
4807
|
-
function isSingleTransitionTag(segment) {
|
|
4808
|
-
const letterThenDigit = /^([A-Za-z]+)([0-9]+)$/.exec(segment);
|
|
4809
|
-
if (letterThenDigit !== null) {
|
|
4810
|
-
const letters = letterThenDigit[1];
|
|
4811
|
-
return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
|
|
4812
|
-
}
|
|
4813
|
-
const digitThenLetter = /^([0-9]+)([A-Za-z]+)$/.exec(segment);
|
|
4814
|
-
if (digitThenLetter !== null) {
|
|
4815
|
-
const letters = digitThenLetter[2];
|
|
4816
|
-
return letters.length <= TAG_MAX_LETTER_RUN && isPlainCasedWord(letters);
|
|
4817
|
-
}
|
|
4818
|
-
return false;
|
|
4819
|
-
}
|
|
4820
|
-
function isSlugHexTail(segment) {
|
|
4821
|
-
return SLUG_HEX_TAIL_RE.test(segment);
|
|
4822
|
-
}
|
|
4823
|
-
function isWordSlug(value) {
|
|
4824
|
-
const segments = value.split(/[-_]/);
|
|
4825
|
-
if (segments.length < 3) {
|
|
4826
|
-
return false;
|
|
4827
|
-
}
|
|
4828
|
-
let words = 0;
|
|
4829
|
-
for (let i = 0;i < segments.length; i += 1) {
|
|
4830
|
-
const segment = segments[i];
|
|
4831
|
-
const isDigits = /^[0-9]+$/.test(segment);
|
|
4832
|
-
if (isDigits) {
|
|
4833
|
-
continue;
|
|
4834
|
-
}
|
|
4835
|
-
if (isPlainCasedWord(segment)) {
|
|
4836
|
-
if (segment.length >= 3) {
|
|
4837
|
-
words += 1;
|
|
4838
|
-
}
|
|
4839
|
-
continue;
|
|
4840
|
-
}
|
|
4841
|
-
if (isSingleTransitionTag(segment)) {
|
|
4842
|
-
continue;
|
|
4843
|
-
}
|
|
4844
|
-
if (i === segments.length - 1 && isSlugHexTail(segment)) {
|
|
4845
|
-
continue;
|
|
4846
|
-
}
|
|
4847
|
-
return false;
|
|
4848
|
-
}
|
|
4849
|
-
return words >= 1;
|
|
4850
|
-
}
|
|
4851
|
-
function shannonEntropy(value) {
|
|
4852
|
-
const counts = new Map;
|
|
4853
|
-
for (const ch of value) {
|
|
4854
|
-
counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
4855
|
-
}
|
|
4856
|
-
let entropy = 0;
|
|
4857
|
-
for (const count of counts.values()) {
|
|
4858
|
-
const p = count / value.length;
|
|
4859
|
-
entropy -= p * Math.log2(p);
|
|
4860
|
-
}
|
|
4861
|
-
return entropy;
|
|
4862
|
-
}
|
|
4863
|
-
function bareShapeQualifies(head) {
|
|
4864
|
-
if (!/[0-9]/.test(head) && !/[+=]/.test(head)) {
|
|
4865
|
-
return { qualifies: false, entropy: 0 };
|
|
4866
|
-
}
|
|
4867
|
-
if (isWordSlug(head)) {
|
|
4868
|
-
return { qualifies: false, entropy: 0 };
|
|
4869
|
-
}
|
|
4870
|
-
const entropy = shannonEntropy(head);
|
|
4871
|
-
const hexBlob = isHexBlob(head);
|
|
4872
|
-
if (entropy < 3.6 && !hexBlob) {
|
|
4873
|
-
return { qualifies: false, entropy };
|
|
4874
|
-
}
|
|
4875
|
-
return { qualifies: true, entropy };
|
|
4876
|
-
}
|
|
4877
|
-
function shapeQualifiesAsSecret(value) {
|
|
4878
|
-
if (isAllowShapedValue(value)) {
|
|
4879
|
-
return { qualifies: false, entropy: 0 };
|
|
4880
|
-
}
|
|
4881
|
-
return bareShapeQualifies(value);
|
|
4882
|
-
}
|
|
4883
|
-
function looksSecretShaped(value) {
|
|
4884
|
-
return shapeQualifiesAsSecret(value).qualifies;
|
|
4885
|
-
}
|
|
4886
|
-
function buildMatch(start, end, value, entropy) {
|
|
4887
|
-
const confidence = Math.max(0.4, Math.min(0.7, 0.4 + (entropy - 3.6) * 0.21));
|
|
4888
|
-
return {
|
|
4889
|
-
category: "secret",
|
|
4890
|
-
policyId: "secrets.high-entropy",
|
|
4891
|
-
severity: "medium",
|
|
4892
|
-
confidence: Number(confidence.toFixed(2)),
|
|
4893
|
-
start,
|
|
4894
|
-
end,
|
|
4895
|
-
value,
|
|
4896
|
-
mask: "entropy",
|
|
4897
|
-
remediation: "Verify this high-entropy value is not a live credential."
|
|
4898
|
-
};
|
|
4899
|
-
}
|
|
4900
|
-
function findUrlSpans(content) {
|
|
4901
|
-
const spans = [];
|
|
4902
|
-
URL_SPAN_RE.lastIndex = 0;
|
|
4903
|
-
let m;
|
|
4904
|
-
while ((m = URL_SPAN_RE.exec(content)) !== null) {
|
|
4905
|
-
let raw = m[0];
|
|
4906
|
-
while (raw.length > 0 && URL_TRAILING_PUNCTUATION_RE.test(raw)) {
|
|
4907
|
-
raw = raw.slice(0, -1);
|
|
4908
|
-
}
|
|
4909
|
-
if (raw.length > 0) {
|
|
4910
|
-
spans.push({ start: m.index, end: m.index + raw.length, raw });
|
|
4911
|
-
}
|
|
4912
|
-
if (m.index === URL_SPAN_RE.lastIndex) {
|
|
4913
|
-
URL_SPAN_RE.lastIndex += 1;
|
|
4914
|
-
}
|
|
4915
|
-
}
|
|
4916
|
-
return spans;
|
|
4917
|
-
}
|
|
4918
|
-
function urlComponentSpans(raw) {
|
|
4919
|
-
const spans = [];
|
|
4920
|
-
const schemeMatch = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.exec(raw);
|
|
4921
|
-
if (schemeMatch === null) {
|
|
4922
|
-
return spans;
|
|
4923
|
-
}
|
|
4924
|
-
let i = schemeMatch[0].length;
|
|
4925
|
-
while (i < raw.length && raw[i] !== "/" && raw[i] !== "?" && raw[i] !== "#") {
|
|
4926
|
-
i += 1;
|
|
4927
|
-
}
|
|
4928
|
-
let pathEnd = i;
|
|
4929
|
-
while (pathEnd < raw.length && raw[pathEnd] !== "?" && raw[pathEnd] !== "#") {
|
|
4930
|
-
pathEnd += 1;
|
|
4931
|
-
}
|
|
4932
|
-
let segStart = i;
|
|
4933
|
-
for (let j = i;j <= pathEnd; j += 1) {
|
|
4934
|
-
if (j === pathEnd || raw[j] === "/") {
|
|
4935
|
-
if (j > segStart) {
|
|
4936
|
-
spans.push({ start: segStart, end: j });
|
|
4937
|
-
}
|
|
4938
|
-
segStart = j + 1;
|
|
4939
|
-
}
|
|
4940
|
-
}
|
|
4941
|
-
let cursor = pathEnd;
|
|
4942
|
-
if (cursor < raw.length && raw[cursor] === "?") {
|
|
4943
|
-
let queryEnd = cursor + 1;
|
|
4944
|
-
while (queryEnd < raw.length && raw[queryEnd] !== "#") {
|
|
4945
|
-
queryEnd += 1;
|
|
4946
|
-
}
|
|
4947
|
-
let pairStart = cursor + 1;
|
|
4948
|
-
for (let j = cursor + 1;j <= queryEnd; j += 1) {
|
|
4949
|
-
if (j === queryEnd || raw[j] === "&") {
|
|
4950
|
-
const pair = raw.slice(pairStart, j);
|
|
4951
|
-
const eq = pair.indexOf("=");
|
|
4952
|
-
if (eq !== -1 && eq + 1 < pair.length) {
|
|
4953
|
-
spans.push({ start: pairStart + eq + 1, end: j });
|
|
4954
|
-
} else if (eq === -1 && pair.length > 0) {
|
|
4955
|
-
spans.push({ start: pairStart, end: j });
|
|
4956
|
-
}
|
|
4957
|
-
pairStart = j + 1;
|
|
4958
|
-
}
|
|
4959
|
-
}
|
|
4960
|
-
cursor = queryEnd;
|
|
4961
|
-
}
|
|
4962
|
-
if (cursor < raw.length && raw[cursor] === "#" && cursor + 1 < raw.length) {
|
|
4963
|
-
spans.push({ start: cursor + 1, end: raw.length });
|
|
4964
|
-
}
|
|
4965
|
-
return spans;
|
|
4966
|
-
}
|
|
4967
|
-
function secretShapedCandidatesIn(text) {
|
|
4968
|
-
const normalized = text.replace(/\+/g, " ");
|
|
4969
|
-
const out = [];
|
|
4970
|
-
TOKEN.lastIndex = 0;
|
|
4971
|
-
let m;
|
|
4972
|
-
while ((m = TOKEN.exec(normalized)) !== null) {
|
|
4973
|
-
const value = m[0];
|
|
4974
|
-
if (value.length < URL_COMPONENT_MIN_LENGTH) {
|
|
4975
|
-
continue;
|
|
4976
|
-
}
|
|
4977
|
-
const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
|
|
4978
|
-
const { qualifies, entropy } = shapeQualifiesAsSecret(head);
|
|
4979
|
-
if (!qualifies) {
|
|
4980
|
-
continue;
|
|
4981
|
-
}
|
|
4982
|
-
const start = m.index;
|
|
4983
|
-
const end = start + value.length;
|
|
4984
|
-
out.push({ start, end, value: text.slice(start, end), entropy });
|
|
4985
|
-
}
|
|
4986
|
-
return out;
|
|
4987
|
-
}
|
|
4988
|
-
function looksSecretShapedIn(text) {
|
|
4989
|
-
return secretShapedCandidatesIn(text).length > 0;
|
|
4990
|
-
}
|
|
4991
|
-
function labelledPieceQualifies(piece) {
|
|
4992
|
-
if (isAllowShapedValue(piece)) {
|
|
4993
|
-
return { entropy: shannonEntropy(piece) };
|
|
4994
|
-
}
|
|
4995
|
-
if (isHexBlob(piece)) {
|
|
4996
|
-
return { entropy: shannonEntropy(piece) };
|
|
4997
|
-
}
|
|
4998
|
-
if (!/[0-9]/.test(piece) && !/[+=]/.test(piece)) {
|
|
4999
|
-
return null;
|
|
5000
|
-
}
|
|
5001
|
-
const withoutSeparators = piece.replace(/[-_.+]/g, "");
|
|
5002
|
-
const entropy = shannonEntropy(withoutSeparators.length > 0 ? withoutSeparators : piece);
|
|
5003
|
-
if (entropy >= 3.6) {
|
|
5004
|
-
return { entropy };
|
|
5005
|
-
}
|
|
5006
|
-
return null;
|
|
5007
|
-
}
|
|
5008
|
-
function firstQualifyingSlashPiece(head) {
|
|
5009
|
-
let offset = 0;
|
|
5010
|
-
for (const piece of head.split("/")) {
|
|
5011
|
-
if (piece.length > 0) {
|
|
5012
|
-
const found = labelledPieceQualifies(piece);
|
|
5013
|
-
if (found !== null) {
|
|
5014
|
-
return { offset, length: piece.length, entropy: found.entropy };
|
|
5015
|
-
}
|
|
5016
|
-
}
|
|
5017
|
-
offset += piece.length + 1;
|
|
5018
|
-
}
|
|
5019
|
-
return null;
|
|
5020
|
-
}
|
|
5021
|
-
function detectEntropy(content) {
|
|
5022
|
-
const matches = [];
|
|
5023
|
-
const urlSpans = findUrlSpans(content);
|
|
5024
|
-
for (const url of urlSpans) {
|
|
5025
|
-
for (const comp of urlComponentSpans(url.raw)) {
|
|
5026
|
-
const compText = url.raw.slice(comp.start, comp.end);
|
|
5027
|
-
for (const cand of secretShapedCandidatesIn(compText)) {
|
|
5028
|
-
const absStart = url.start + comp.start + cand.start;
|
|
5029
|
-
const absEnd = url.start + comp.start + cand.end;
|
|
5030
|
-
matches.push(buildMatch(absStart, absEnd, cand.value, cand.entropy));
|
|
5031
|
-
}
|
|
5032
|
-
}
|
|
5033
|
-
}
|
|
5034
|
-
LABEL_ASSIGNMENT_RE.lastIndex = 0;
|
|
5035
|
-
let am;
|
|
5036
|
-
while ((am = LABEL_ASSIGNMENT_RE.exec(content)) !== null) {
|
|
5037
|
-
const value = am[1];
|
|
5038
|
-
const end = am.index + am[0].length;
|
|
5039
|
-
const start = end - value.length;
|
|
5040
|
-
if (urlSpans.some((u) => start < u.end && end > u.start)) {
|
|
5041
|
-
continue;
|
|
5042
|
-
}
|
|
5043
|
-
const found = labelledPieceQualifies(value);
|
|
5044
|
-
if (found !== null) {
|
|
5045
|
-
matches.push(buildMatch(start, end, value, found.entropy));
|
|
5046
|
-
}
|
|
5047
|
-
}
|
|
5048
|
-
TOKEN.lastIndex = 0;
|
|
5049
|
-
let m;
|
|
5050
|
-
while ((m = TOKEN.exec(content)) !== null) {
|
|
5051
|
-
const value = m[0];
|
|
5052
|
-
const start = m.index;
|
|
5053
|
-
const end = start + value.length;
|
|
5054
|
-
if (urlSpans.some((u) => start < u.end && end > u.start)) {
|
|
5055
|
-
continue;
|
|
5056
|
-
}
|
|
5057
|
-
if (matches.some((existing) => start < existing.end && end > existing.start)) {
|
|
5058
|
-
continue;
|
|
5059
|
-
}
|
|
5060
|
-
if (!isAdjacentLabel(content, start)) {
|
|
5061
|
-
continue;
|
|
5062
|
-
}
|
|
5063
|
-
const head = TOKEN_HEAD.exec(value)?.[0] ?? value;
|
|
5064
|
-
if (head.includes("/")) {
|
|
5065
|
-
const piece = firstQualifyingSlashPiece(head);
|
|
5066
|
-
if (piece === null) {
|
|
5067
|
-
continue;
|
|
5068
|
-
}
|
|
5069
|
-
const pieceStart = start + piece.offset;
|
|
5070
|
-
const pieceEnd = pieceStart + piece.length;
|
|
5071
|
-
matches.push(buildMatch(pieceStart, pieceEnd, content.slice(pieceStart, pieceEnd), piece.entropy));
|
|
5072
|
-
continue;
|
|
5073
|
-
}
|
|
5074
|
-
const found = labelledPieceQualifies(head);
|
|
5075
|
-
if (found === null) {
|
|
5076
|
-
continue;
|
|
5077
|
-
}
|
|
5078
|
-
matches.push(buildMatch(start, end, value, found.entropy));
|
|
5079
|
-
}
|
|
5080
|
-
return matches.sort((a, b) => a.start - b.start);
|
|
5081
|
-
}
|
|
5082
|
-
var SENSITIVE_LABEL_WORDS = "key|secret|token|password|passwd|api|credential|auth|bearer", caseless = (words) => words.replace(/[a-z]/g, (ch) => `[${ch}${ch.toUpperCase()}]`), FILLER_WORDS = "is|was|set|to|the|value|now", ADJACENT_LABEL, CONTINUATION_LABEL_LINE_RE, LABEL_ASSIGNMENT_RE, TOKEN, TOKEN_HEAD, LABEL_WINDOW = 40, HEX_BLOB, FULL_GIT_SHA_RE, UUID_RE, INTEGRITY_RE, TAG_MAX_LETTER_RUN = 8, SLUG_HEX_TAIL_RE, URL_SPAN_RE, URL_TRAILING_PUNCTUATION_RE, URL_COMPONENT_MIN_LENGTH = 20;
|
|
5083
|
-
var init_entropy = __esm(() => {
|
|
5084
|
-
ADJACENT_LABEL = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*` + `(?:\\s+(?:${caseless(FILLER_WORDS)})){0,2}` + `["']?\\s*[:=]?\\s*["']?$`);
|
|
5085
|
-
CONTINUATION_LABEL_LINE_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${caseless(SENSITIVE_LABEL_WORDS)})[A-Za-z0-9_-]*["']?\\s*[:=]?\\s*\\\\\\s*$`);
|
|
5086
|
-
LABEL_ASSIGNMENT_RE = new RegExp(`(?:^|[^A-Za-z0-9])(?:${SENSITIVE_LABEL_WORDS})[A-Za-z0-9_-]*"?\\s*=\\s*"?([A-Za-z0-9+/_=-]{6,})`, "gi");
|
|
5087
|
-
TOKEN = /[A-Za-z0-9+/=_-]{20,}(?:\.[A-Za-z0-9+/=_-]{6,})*/g;
|
|
5088
|
-
TOKEN_HEAD = /^[A-Za-z0-9+/=_-]+/;
|
|
5089
|
-
HEX_BLOB = /^[0-9a-f]{24,}$/i;
|
|
5090
|
-
FULL_GIT_SHA_RE = /^[0-9a-f]{40}$/i;
|
|
5091
|
-
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5092
|
-
INTEGRITY_RE = /^(?:sha256|sha512)-[A-Za-z0-9+/]+=*$/;
|
|
5093
|
-
SLUG_HEX_TAIL_RE = /^[0-9a-f]{10,16}$/i;
|
|
5094
|
-
URL_SPAN_RE = /\bhttps?:\/\/[^\s<>"'\\]+/g;
|
|
5095
|
-
URL_TRAILING_PUNCTUATION_RE = /[.,;:!?)\]}]$/;
|
|
5096
|
-
});
|
|
5097
|
-
|
|
5098
5440
|
// src/security/entropy-gate.ts
|
|
5099
5441
|
function ensureLoading() {
|
|
5100
5442
|
if (loadStarted) {
|
|
@@ -5274,12 +5616,12 @@ function isNumericContextKey(key) {
|
|
|
5274
5616
|
function isNumericText(value) {
|
|
5275
5617
|
return /^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(value);
|
|
5276
5618
|
}
|
|
5277
|
-
function
|
|
5619
|
+
function floorMatches(value, key, exemptExfil) {
|
|
5278
5620
|
const secretMatches = detectSecrets(value);
|
|
5279
5621
|
const piiMatches = detectPii(value);
|
|
5280
5622
|
const allExfilMatches = detectExfil(value);
|
|
5281
5623
|
const exfilMatches = exemptExfil ? allExfilMatches.filter((match) => !exemptExfil(match)) : allExfilMatches;
|
|
5282
|
-
const
|
|
5624
|
+
const entropyMatches = isEntropyBackendEnabled() ? detectEntropy(value) : [];
|
|
5283
5625
|
const contextualMatches = isSensitiveFieldKey(key) && value.length > 0 && secretMatches.length === 0 ? [
|
|
5284
5626
|
{
|
|
5285
5627
|
category: "secret",
|
|
@@ -5293,21 +5635,23 @@ function redactString(value, key, exemptExfil) {
|
|
|
5293
5635
|
remediation: "Keep credential fields out of transport output."
|
|
5294
5636
|
}
|
|
5295
5637
|
] : [];
|
|
5296
|
-
const
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
] : [...secretMatches, ...contextualMatches, ...piiMatches, ...exfilMatches];
|
|
5638
|
+
const pii = isNumericContextKey(key) && isNumericText(value) ? piiMatches.filter((match) => match.policyId !== "pii.phone") : piiMatches;
|
|
5639
|
+
return [...secretMatches, ...contextualMatches, ...pii, ...exfilMatches, ...entropyMatches];
|
|
5640
|
+
}
|
|
5641
|
+
function redactString(value, key, exemptExfil) {
|
|
5642
|
+
const matches = floorMatches(value, key, exemptExfil);
|
|
5302
5643
|
if (matches.length === 0) {
|
|
5303
5644
|
return { value, reasons: [] };
|
|
5304
5645
|
}
|
|
5305
|
-
const redacted = matches.length === secretMatches.length + piiMatches.length + exfilMatches.length && contextualMatches.length === 0 && exemptedCount === 0 ? redactSensitiveText(value) : applyRedaction(value, matches);
|
|
5306
5646
|
return {
|
|
5307
|
-
value:
|
|
5647
|
+
value: applyRedaction(value, matches),
|
|
5308
5648
|
reasons: matches.map((match) => match.policyId)
|
|
5309
5649
|
};
|
|
5310
5650
|
}
|
|
5651
|
+
function isSensitiveNumber(value, exemptExfil) {
|
|
5652
|
+
const text = String(value);
|
|
5653
|
+
return floorMatches(text, undefined, exemptExfil).some((match) => match.policyId !== "pii.phone" && (match.policyId !== "pii.credit-card" || CARD_LEADING_DIGIT.test(match.value)));
|
|
5654
|
+
}
|
|
5311
5655
|
function isSensitivePropertyName(name) {
|
|
5312
5656
|
if (name.length === 0) {
|
|
5313
5657
|
return false;
|
|
@@ -5335,6 +5679,9 @@ function sanitizeJsonValue(value, key, active, exemptExfil) {
|
|
|
5335
5679
|
reason: isSensitiveFieldKey(key) ? "sensitive-numeric-field" : "non-json-value"
|
|
5336
5680
|
};
|
|
5337
5681
|
}
|
|
5682
|
+
if (isSensitiveNumber(value, exemptExfil)) {
|
|
5683
|
+
return { ok: false, reason: "sensitive-numeric-value" };
|
|
5684
|
+
}
|
|
5338
5685
|
return { ok: true, value, reasons: [] };
|
|
5339
5686
|
}
|
|
5340
5687
|
if (typeof value !== "object") {
|
|
@@ -5705,6 +6052,9 @@ function validateSerializedContentForTransport(content, exemptExfil) {
|
|
|
5705
6052
|
} catch {
|
|
5706
6053
|
return validateOutputForTransport({ value: content, format: "text", ...exemption });
|
|
5707
6054
|
}
|
|
6055
|
+
if (typeof value === "number") {
|
|
6056
|
+
return validateOutputForTransport({ value: content, format: "text", ...exemption });
|
|
6057
|
+
}
|
|
5708
6058
|
const result = validateOutputForTransport({ value, format: "json", ...exemption });
|
|
5709
6059
|
if (!result.ok || result.redaction.state === "redacted") {
|
|
5710
6060
|
return result;
|
|
@@ -5714,11 +6064,13 @@ function validateSerializedContentForTransport(content, exemptExfil) {
|
|
|
5714
6064
|
redaction: { state: "redacted", reasons: [SERIALIZED_NORMALIZED_REASON] }
|
|
5715
6065
|
};
|
|
5716
6066
|
}
|
|
5717
|
-
var FORMAT_UNSAFE_OUTPUT_TEXT = "Output withheld: format-unsafe", SUPPORTED_SCHEMA_KEYS, JSON_TYPES, EXTERNAL_SCHEMA_REFS, SENSITIVE_NUMERIC_KEYS, REFERENCE_SAFE_SIBLINGS, SERIALIZED_NORMALIZED_REASON = "serialized-content-normalized", JSON_WHITESPACE, JSON_KEYWORDS;
|
|
6067
|
+
var FORMAT_UNSAFE_OUTPUT_TEXT = "Output withheld: format-unsafe", SUPPORTED_SCHEMA_KEYS, JSON_TYPES, EXTERNAL_SCHEMA_REFS, SENSITIVE_NUMERIC_KEYS, REFERENCE_SAFE_SIBLINGS, CARD_LEADING_DIGIT, SERIALIZED_NORMALIZED_REASON = "serialized-content-normalized", JSON_WHITESPACE, JSON_KEYWORDS;
|
|
5718
6068
|
var init_output_validation = __esm(() => {
|
|
5719
6069
|
init_pii();
|
|
5720
6070
|
init_secrets();
|
|
5721
6071
|
init_exfil();
|
|
6072
|
+
init_entropy();
|
|
6073
|
+
init_entropy_gate();
|
|
5722
6074
|
init_redact();
|
|
5723
6075
|
init_schemas();
|
|
5724
6076
|
SUPPORTED_SCHEMA_KEYS = new Set([
|
|
@@ -5769,6 +6121,7 @@ var init_output_validation = __esm(() => {
|
|
|
5769
6121
|
"token"
|
|
5770
6122
|
]);
|
|
5771
6123
|
REFERENCE_SAFE_SIBLINGS = new Set(["$ref", "$schema", "$id", "title", "$defs"]);
|
|
6124
|
+
CARD_LEADING_DIGIT = /^[2-6]/;
|
|
5772
6125
|
JSON_WHITESPACE = new Set([" ", "\t", `
|
|
5773
6126
|
`, "\r"]);
|
|
5774
6127
|
JSON_KEYWORDS = [
|
|
@@ -5980,24 +6333,42 @@ var init_seam = __esm(() => {
|
|
|
5980
6333
|
});
|
|
5981
6334
|
|
|
5982
6335
|
// src/security/detect/injection.ts
|
|
5983
|
-
function foldForMatching(content) {
|
|
6336
|
+
function foldForMatching(content, map = CONFUSABLES) {
|
|
5984
6337
|
let folded = "";
|
|
5985
6338
|
for (let i = 0;i < content.length; i += 1) {
|
|
5986
6339
|
const ch = content[i];
|
|
5987
|
-
folded +=
|
|
6340
|
+
folded += map[ch] ?? ch;
|
|
5988
6341
|
}
|
|
5989
6342
|
const nfkc = folded.normalize("NFKC");
|
|
5990
6343
|
return nfkc.length === folded.length ? nfkc : folded;
|
|
5991
6344
|
}
|
|
5992
6345
|
function detectInjection(content) {
|
|
5993
6346
|
const matches = [];
|
|
5994
|
-
const
|
|
6347
|
+
const seen = new Set;
|
|
6348
|
+
const views = [foldForMatching(content)];
|
|
6349
|
+
if (HAS_ALT_CONFUSABLE.test(content)) {
|
|
6350
|
+
views.push(foldForMatching(content, ALT_CONFUSABLES));
|
|
6351
|
+
}
|
|
6352
|
+
for (const folded of views) {
|
|
6353
|
+
scanView(content, folded, matches, seen);
|
|
6354
|
+
}
|
|
6355
|
+
return matches;
|
|
6356
|
+
}
|
|
6357
|
+
function scanView(content, folded, matches, seen) {
|
|
5995
6358
|
for (const pattern of PATTERNS) {
|
|
5996
6359
|
pattern.regex.lastIndex = 0;
|
|
5997
6360
|
let m;
|
|
5998
6361
|
while ((m = pattern.regex.exec(folded)) !== null) {
|
|
5999
6362
|
const start = Math.min(m.index, content.length);
|
|
6000
6363
|
const end = Math.min(m.index + m[0].length, content.length);
|
|
6364
|
+
const key = `${pattern.policyId}:${start}:${end}`;
|
|
6365
|
+
if (seen.has(key)) {
|
|
6366
|
+
if (m.index === pattern.regex.lastIndex) {
|
|
6367
|
+
pattern.regex.lastIndex += 1;
|
|
6368
|
+
}
|
|
6369
|
+
continue;
|
|
6370
|
+
}
|
|
6371
|
+
seen.add(key);
|
|
6001
6372
|
matches.push({
|
|
6002
6373
|
category: "prompt-injection",
|
|
6003
6374
|
policyId: pattern.policyId,
|
|
@@ -6013,9 +6384,8 @@ function detectInjection(content) {
|
|
|
6013
6384
|
}
|
|
6014
6385
|
}
|
|
6015
6386
|
}
|
|
6016
|
-
return matches;
|
|
6017
6387
|
}
|
|
6018
|
-
var CONFUSABLES, PATTERNS;
|
|
6388
|
+
var CONFUSABLES, ALT_CONFUSABLES, HAS_ALT_CONFUSABLE, PATTERNS;
|
|
6019
6389
|
var init_injection = __esm(() => {
|
|
6020
6390
|
CONFUSABLES = {
|
|
6021
6391
|
"\u0430": "a",
|
|
@@ -6034,8 +6404,8 @@ var init_injection = __esm(() => {
|
|
|
6034
6404
|
"\u0423": "Y",
|
|
6035
6405
|
"\u0455": "s",
|
|
6036
6406
|
"\u0405": "S",
|
|
6037
|
-
"\u0456": "
|
|
6038
|
-
"\u0406": "
|
|
6407
|
+
"\u0456": "i",
|
|
6408
|
+
"\u0406": "I",
|
|
6039
6409
|
"\u0458": "j",
|
|
6040
6410
|
"\u0408": "J",
|
|
6041
6411
|
"\u03B1": "a",
|
|
@@ -6059,6 +6429,8 @@ var init_injection = __esm(() => {
|
|
|
6059
6429
|
"\u03C7": "x",
|
|
6060
6430
|
"\u03A7": "X"
|
|
6061
6431
|
};
|
|
6432
|
+
ALT_CONFUSABLES = { ...CONFUSABLES, "\u0456": "o", "\u0406": "O" };
|
|
6433
|
+
HAS_ALT_CONFUSABLE = /[\u0456\u0406]/;
|
|
6062
6434
|
PATTERNS = [
|
|
6063
6435
|
{
|
|
6064
6436
|
policyId: "prompt-injection.ignore-instructions",
|
|
@@ -6289,16 +6661,48 @@ async function mergeAdapter(cwd, content, spec, into) {
|
|
|
6289
6661
|
} catch {}
|
|
6290
6662
|
}
|
|
6291
6663
|
function dedupeOverlaps(matches) {
|
|
6664
|
+
const trees = new Map;
|
|
6665
|
+
for (const match of matches) {
|
|
6666
|
+
const tree = trees.get(match.category) ?? { starts: [], maxEnd: [] };
|
|
6667
|
+
tree.starts.push(match.start);
|
|
6668
|
+
trees.set(match.category, tree);
|
|
6669
|
+
}
|
|
6670
|
+
for (const tree of trees.values()) {
|
|
6671
|
+
tree.starts = [...new Set(tree.starts)].sort((a, b) => a - b);
|
|
6672
|
+
tree.maxEnd = new Array(tree.starts.length + 1).fill(-Infinity);
|
|
6673
|
+
}
|
|
6292
6674
|
const kept = [];
|
|
6293
6675
|
const sorted = [...matches].sort((a, b) => b.confidence - a.confidence);
|
|
6294
6676
|
for (const candidate of sorted) {
|
|
6295
|
-
const
|
|
6296
|
-
|
|
6297
|
-
|
|
6677
|
+
const tree = trees.get(candidate.category);
|
|
6678
|
+
const position = upperBound(tree.starts, candidate.start);
|
|
6679
|
+
let furthest = -Infinity;
|
|
6680
|
+
for (let i = position;i > 0; i -= i & -i) {
|
|
6681
|
+
furthest = Math.max(furthest, tree.maxEnd[i]);
|
|
6682
|
+
}
|
|
6683
|
+
if (furthest >= candidate.end) {
|
|
6684
|
+
continue;
|
|
6685
|
+
}
|
|
6686
|
+
kept.push(candidate);
|
|
6687
|
+
for (let i = position;i < tree.maxEnd.length; i += i & -i) {
|
|
6688
|
+
tree.maxEnd[i] = Math.max(tree.maxEnd[i], candidate.end);
|
|
6298
6689
|
}
|
|
6299
6690
|
}
|
|
6300
6691
|
return kept.sort((a, b) => a.start - b.start);
|
|
6301
6692
|
}
|
|
6693
|
+
function upperBound(sorted, value) {
|
|
6694
|
+
let low = 0;
|
|
6695
|
+
let high = sorted.length;
|
|
6696
|
+
while (low < high) {
|
|
6697
|
+
const mid = low + high >> 1;
|
|
6698
|
+
if (sorted[mid] <= value) {
|
|
6699
|
+
low = mid + 1;
|
|
6700
|
+
} else {
|
|
6701
|
+
high = mid;
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6704
|
+
return low;
|
|
6705
|
+
}
|
|
6302
6706
|
var SECURITY_MODEL_RUNTIME = "";
|
|
6303
6707
|
var init_detect = __esm(() => {
|
|
6304
6708
|
init_seam();
|
|
@@ -7398,16 +7802,16 @@ function isSecretRun(run) {
|
|
|
7398
7802
|
function looksLikeSecret(text) {
|
|
7399
7803
|
if (BOT_TOKEN_SHAPE.test(text))
|
|
7400
7804
|
return true;
|
|
7401
|
-
for (const run of text.match(
|
|
7805
|
+
for (const run of text.match(TOKEN_RUN2) ?? []) {
|
|
7402
7806
|
if (isSecretRun(run))
|
|
7403
7807
|
return true;
|
|
7404
7808
|
}
|
|
7405
7809
|
return false;
|
|
7406
7810
|
}
|
|
7407
|
-
var BOT_TOKEN_SHAPE,
|
|
7811
|
+
var BOT_TOKEN_SHAPE, TOKEN_RUN2, RUN_PARTS;
|
|
7408
7812
|
var init_secret_shape = __esm(() => {
|
|
7409
7813
|
BOT_TOKEN_SHAPE = /\d{6,}:[A-Za-z0-9_-]{30,}/;
|
|
7410
|
-
|
|
7814
|
+
TOKEN_RUN2 = /[A-Za-z0-9_-]{32,}|[A-Za-z0-9+/]{40,}={0,2}/g;
|
|
7411
7815
|
RUN_PARTS = /[A-Z][a-z]+|[a-z]+|[A-Z]+(?![a-z])|\d+|[-_]/g;
|
|
7412
7816
|
});
|
|
7413
7817
|
|
|
@@ -7616,11 +8020,119 @@ function parseScalar(raw) {
|
|
|
7616
8020
|
return unescapeToml(match[1]);
|
|
7617
8021
|
return;
|
|
7618
8022
|
}
|
|
8023
|
+
function splitDepthAware(inner) {
|
|
8024
|
+
const parts = [];
|
|
8025
|
+
let quoted = false;
|
|
8026
|
+
let depth = 0;
|
|
8027
|
+
let start = 0;
|
|
8028
|
+
for (let i = 0;i < inner.length; i++) {
|
|
8029
|
+
const ch = inner[i];
|
|
8030
|
+
if (ch === '"' && inner[i - 1] !== "\\")
|
|
8031
|
+
quoted = !quoted;
|
|
8032
|
+
if (quoted)
|
|
8033
|
+
continue;
|
|
8034
|
+
if (ch === "[" || ch === "{")
|
|
8035
|
+
depth++;
|
|
8036
|
+
if (ch === "]" || ch === "}")
|
|
8037
|
+
depth--;
|
|
8038
|
+
if (ch === "," && depth === 0) {
|
|
8039
|
+
parts.push(inner.slice(start, i));
|
|
8040
|
+
start = i + 1;
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
8043
|
+
const tail = inner.slice(start).trim();
|
|
8044
|
+
if (tail !== "")
|
|
8045
|
+
parts.push(tail);
|
|
8046
|
+
return parts;
|
|
8047
|
+
}
|
|
8048
|
+
function parseKeyPath(text) {
|
|
8049
|
+
const segments = [];
|
|
8050
|
+
let rest = text.trim();
|
|
8051
|
+
for (;; ) {
|
|
8052
|
+
const m = /^(?:"((?:[^"\\]|\\.)*)"|([A-Za-z0-9_-]+))\s*/.exec(rest);
|
|
8053
|
+
if (m === null)
|
|
8054
|
+
return;
|
|
8055
|
+
segments.push(m[1] !== undefined ? unescapeToml(m[1]) : m[2]);
|
|
8056
|
+
rest = rest.slice(m[0].length);
|
|
8057
|
+
if (rest === "")
|
|
8058
|
+
return segments;
|
|
8059
|
+
if (!rest.startsWith("."))
|
|
8060
|
+
return;
|
|
8061
|
+
rest = rest.slice(1).trimStart();
|
|
8062
|
+
}
|
|
8063
|
+
}
|
|
8064
|
+
function findKeyEquals(line) {
|
|
8065
|
+
let quoted = false;
|
|
8066
|
+
for (let i = 0;i < line.length; i++) {
|
|
8067
|
+
const ch = line[i];
|
|
8068
|
+
if (ch === '"' && line[i - 1] !== "\\")
|
|
8069
|
+
quoted = !quoted;
|
|
8070
|
+
if (ch === "=" && !quoted)
|
|
8071
|
+
return i;
|
|
8072
|
+
}
|
|
8073
|
+
return -1;
|
|
8074
|
+
}
|
|
8075
|
+
function applyField(server, key, valueText) {
|
|
8076
|
+
const value = valueText.trim();
|
|
8077
|
+
if (value.startsWith("[")) {
|
|
8078
|
+
const closeAt = findUnquotedClose(value);
|
|
8079
|
+
if (closeAt === -1 || value.slice(closeAt + 1).trim() !== "")
|
|
8080
|
+
return `value for "${key}" is not a single-line array`;
|
|
8081
|
+
const parsed = parseStringArray(value.slice(1, closeAt));
|
|
8082
|
+
if (parsed === undefined)
|
|
8083
|
+
return `value for "${key}" is not an array of strings this reader supports`;
|
|
8084
|
+
if (key === "command")
|
|
8085
|
+
return '"command" must be a string, not an array';
|
|
8086
|
+
if (key === "args")
|
|
8087
|
+
server.args = parsed;
|
|
8088
|
+
return;
|
|
8089
|
+
}
|
|
8090
|
+
if (value.startsWith("{")) {
|
|
8091
|
+
if (key === "command" || key === "args")
|
|
8092
|
+
return `"${key}" must not be a table`;
|
|
8093
|
+
return value.endsWith("}") ? undefined : `value for "${key}" is an inline table this reader cannot read`;
|
|
8094
|
+
}
|
|
8095
|
+
const scalar = parseScalar(value);
|
|
8096
|
+
if (scalar === undefined)
|
|
8097
|
+
return `value for "${key}" is a TOML form this reader does not support (${value})`;
|
|
8098
|
+
if (key === "command") {
|
|
8099
|
+
if (typeof scalar !== "string")
|
|
8100
|
+
return '"command" must be a string';
|
|
8101
|
+
server.command = scalar;
|
|
8102
|
+
}
|
|
8103
|
+
return;
|
|
8104
|
+
}
|
|
8105
|
+
function applyInlineServer(server, table) {
|
|
8106
|
+
const text = table.trim();
|
|
8107
|
+
if (!text.startsWith("{") || !text.endsWith("}"))
|
|
8108
|
+
return `"${server.name}" is not an inline table this reader understands`;
|
|
8109
|
+
for (const entry of splitDepthAware(text.slice(1, -1))) {
|
|
8110
|
+
const eq = findKeyEquals(entry);
|
|
8111
|
+
const path = eq === -1 ? undefined : parseKeyPath(entry.slice(0, eq));
|
|
8112
|
+
if (path === undefined || path.length !== 1)
|
|
8113
|
+
return `cannot read "${entry.trim()}" as a key/value in "${server.name}"`;
|
|
8114
|
+
const error = applyField(server, path[0], entry.slice(eq + 1));
|
|
8115
|
+
if (error !== undefined)
|
|
8116
|
+
return error;
|
|
8117
|
+
}
|
|
8118
|
+
return;
|
|
8119
|
+
}
|
|
7619
8120
|
function parseCodexToml(text) {
|
|
7620
8121
|
const servers = new Map;
|
|
7621
8122
|
const order = [];
|
|
7622
8123
|
let current;
|
|
8124
|
+
let atRoot = true;
|
|
8125
|
+
const serverFor = (name, line) => {
|
|
8126
|
+
const existing = servers.get(name);
|
|
8127
|
+
if (existing)
|
|
8128
|
+
return existing;
|
|
8129
|
+
const created = { name, line };
|
|
8130
|
+
servers.set(name, created);
|
|
8131
|
+
order.push(name);
|
|
8132
|
+
return created;
|
|
8133
|
+
};
|
|
7623
8134
|
let pendingKey;
|
|
8135
|
+
let pendingServer;
|
|
7624
8136
|
let pendingBuffer = "";
|
|
7625
8137
|
let pendingStartLine = 0;
|
|
7626
8138
|
const lines = text.split(`
|
|
@@ -7647,13 +8159,14 @@ function parseCodexToml(text) {
|
|
|
7647
8159
|
error: `line ${pendingStartLine}: value for "${pendingKey}" is not an array of strings this reader supports`
|
|
7648
8160
|
};
|
|
7649
8161
|
}
|
|
7650
|
-
if (
|
|
8162
|
+
if (pendingServer && pendingKey === "command") {
|
|
7651
8163
|
return { ok: false, error: `line ${pendingStartLine}: "command" must be a string, not an array` };
|
|
7652
8164
|
}
|
|
7653
|
-
if (
|
|
7654
|
-
|
|
8165
|
+
if (pendingServer && pendingKey === "args") {
|
|
8166
|
+
pendingServer.args = parsedArray;
|
|
7655
8167
|
}
|
|
7656
8168
|
pendingKey = undefined;
|
|
8169
|
+
pendingServer = undefined;
|
|
7657
8170
|
pendingBuffer = "";
|
|
7658
8171
|
continue;
|
|
7659
8172
|
}
|
|
@@ -7661,6 +8174,7 @@ function parseCodexToml(text) {
|
|
|
7661
8174
|
if (trimmed === "")
|
|
7662
8175
|
continue;
|
|
7663
8176
|
if (trimmed.startsWith("[")) {
|
|
8177
|
+
atRoot = false;
|
|
7664
8178
|
const header = HEADER.exec(trimmed);
|
|
7665
8179
|
if (header !== null) {
|
|
7666
8180
|
const name = header[1] ?? header[2];
|
|
@@ -7690,19 +8204,62 @@ function parseCodexToml(text) {
|
|
|
7690
8204
|
current = undefined;
|
|
7691
8205
|
continue;
|
|
7692
8206
|
}
|
|
8207
|
+
let target = current;
|
|
8208
|
+
let kv;
|
|
7693
8209
|
if (current === undefined) {
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
8210
|
+
if (!atRoot)
|
|
8211
|
+
continue;
|
|
8212
|
+
const eq = findKeyEquals(trimmed);
|
|
8213
|
+
const path = eq === -1 ? undefined : parseKeyPath(trimmed.slice(0, eq));
|
|
8214
|
+
if (path === undefined) {
|
|
8215
|
+
if (/^"?mcp_servers"?\s*[.=]/.test(trimmed)) {
|
|
8216
|
+
return { ok: false, error: `line ${lineNo}: cannot read "${trimmed}" as a TOML key/value` };
|
|
8217
|
+
}
|
|
8218
|
+
continue;
|
|
8219
|
+
}
|
|
8220
|
+
if (path[0] !== "mcp_servers")
|
|
8221
|
+
continue;
|
|
8222
|
+
const valueText = trimmed.slice(eq + 1).trim();
|
|
8223
|
+
if (path.length === 1) {
|
|
8224
|
+
if (!valueText.startsWith("{") || !valueText.endsWith("}")) {
|
|
8225
|
+
return { ok: false, error: `line ${lineNo}: "mcp_servers" is not an inline table this reader understands` };
|
|
8226
|
+
}
|
|
8227
|
+
for (const entry of splitDepthAware(valueText.slice(1, -1))) {
|
|
8228
|
+
const innerEq = findKeyEquals(entry);
|
|
8229
|
+
const innerPath = innerEq === -1 ? undefined : parseKeyPath(entry.slice(0, innerEq));
|
|
8230
|
+
if (innerPath === undefined || innerPath.length !== 1) {
|
|
8231
|
+
return { ok: false, error: `line ${lineNo}: cannot read "${entry.trim()}" as an mcp_servers entry` };
|
|
8232
|
+
}
|
|
8233
|
+
const error = applyInlineServer(serverFor(innerPath[0], lineNo), entry.slice(innerEq + 1));
|
|
8234
|
+
if (error !== undefined)
|
|
8235
|
+
return { ok: false, error: `line ${lineNo}: ${error}` };
|
|
8236
|
+
}
|
|
8237
|
+
continue;
|
|
8238
|
+
}
|
|
8239
|
+
const server = serverFor(path[1], lineNo);
|
|
8240
|
+
if (path.length === 2) {
|
|
8241
|
+
const error = applyInlineServer(server, valueText);
|
|
8242
|
+
if (error !== undefined)
|
|
8243
|
+
return { ok: false, error: `line ${lineNo}: ${error}` };
|
|
8244
|
+
continue;
|
|
8245
|
+
}
|
|
8246
|
+
if (path.length > 3)
|
|
8247
|
+
continue;
|
|
8248
|
+
target = server;
|
|
8249
|
+
kv = { key: path[2], valueText };
|
|
8250
|
+
} else {
|
|
8251
|
+
const pair = /^([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(.+)$/.exec(trimmed);
|
|
8252
|
+
if (pair === null) {
|
|
8253
|
+
return { ok: false, error: `line ${lineNo}: cannot read "${trimmed}" as a TOML key/value` };
|
|
8254
|
+
}
|
|
8255
|
+
kv = { key: pair[1], valueText: pair[2].trim() };
|
|
7699
8256
|
}
|
|
7700
|
-
const key =
|
|
7701
|
-
const valueText = pair[2].trim();
|
|
8257
|
+
const { key, valueText } = kv;
|
|
7702
8258
|
if (valueText.startsWith("[")) {
|
|
7703
8259
|
const closeAt = findUnquotedClose(valueText);
|
|
7704
8260
|
if (closeAt === -1) {
|
|
7705
8261
|
pendingKey = key;
|
|
8262
|
+
pendingServer = target;
|
|
7706
8263
|
pendingBuffer = `${valueText.slice(1)}
|
|
7707
8264
|
`;
|
|
7708
8265
|
pendingStartLine = lineNo;
|
|
@@ -7716,8 +8273,8 @@ function parseCodexToml(text) {
|
|
|
7716
8273
|
if (parsedArray === undefined) {
|
|
7717
8274
|
return { ok: false, error: `line ${lineNo}: value for "${key}" is not an array of strings this reader supports` };
|
|
7718
8275
|
}
|
|
7719
|
-
if (key === "args")
|
|
7720
|
-
|
|
8276
|
+
if (key === "args" && target)
|
|
8277
|
+
target.args = parsedArray;
|
|
7721
8278
|
continue;
|
|
7722
8279
|
}
|
|
7723
8280
|
const scalar = parseScalar(valueText);
|
|
@@ -7728,7 +8285,8 @@ function parseCodexToml(text) {
|
|
|
7728
8285
|
if (typeof scalar !== "string") {
|
|
7729
8286
|
return { ok: false, error: `line ${lineNo}: "command" must be a string` };
|
|
7730
8287
|
}
|
|
7731
|
-
|
|
8288
|
+
if (target)
|
|
8289
|
+
target.command = scalar;
|
|
7732
8290
|
}
|
|
7733
8291
|
}
|
|
7734
8292
|
if (pendingKey !== undefined) {
|
|
@@ -13239,6 +13797,16 @@ function hashToolDefinition(tool) {
|
|
|
13239
13797
|
});
|
|
13240
13798
|
return createHash4("sha256").update(canonical).digest("hex");
|
|
13241
13799
|
}
|
|
13800
|
+
function hasCommentPayload(text) {
|
|
13801
|
+
const lower = text.toLowerCase();
|
|
13802
|
+
return [
|
|
13803
|
+
["<!--", "-->"],
|
|
13804
|
+
["<important>", "</important>"]
|
|
13805
|
+
].some(([open, close]) => {
|
|
13806
|
+
const at = lower.indexOf(open);
|
|
13807
|
+
return at !== -1 && lower.indexOf(close, at + open.length) !== -1;
|
|
13808
|
+
});
|
|
13809
|
+
}
|
|
13242
13810
|
function makeMatch(category, policyId, severity, confidence, token, remediation) {
|
|
13243
13811
|
return {
|
|
13244
13812
|
category,
|
|
@@ -13273,30 +13841,45 @@ function parseTools(manifest) {
|
|
|
13273
13841
|
}
|
|
13274
13842
|
function toolText(tool) {
|
|
13275
13843
|
const parts = [tool.description ?? ""];
|
|
13276
|
-
const
|
|
13277
|
-
|
|
13278
|
-
|
|
13844
|
+
const stack = [tool.inputSchema];
|
|
13845
|
+
let visited = 0;
|
|
13846
|
+
while (stack.length > 0) {
|
|
13847
|
+
const value = stack.pop();
|
|
13848
|
+
if (!value || typeof value !== "object") {
|
|
13849
|
+
continue;
|
|
13850
|
+
}
|
|
13851
|
+
visited += 1;
|
|
13852
|
+
if (visited > SCHEMA_NODE_BUDGET) {
|
|
13853
|
+
return { text: parts.join(`
|
|
13854
|
+
`), truncated: true };
|
|
13279
13855
|
}
|
|
13280
13856
|
for (const [key, nested] of Object.entries(value)) {
|
|
13281
|
-
if ((key
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13857
|
+
if (SCHEMA_TEXT_KEYS.has(key)) {
|
|
13858
|
+
if (typeof nested === "string") {
|
|
13859
|
+
parts.push(nested);
|
|
13860
|
+
} else if (Array.isArray(nested)) {
|
|
13861
|
+
parts.push(...nested.filter((item) => typeof item === "string"));
|
|
13862
|
+
}
|
|
13863
|
+
}
|
|
13864
|
+
if (nested && typeof nested === "object") {
|
|
13865
|
+
stack.push(nested);
|
|
13285
13866
|
}
|
|
13286
13867
|
}
|
|
13287
|
-
}
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
`);
|
|
13868
|
+
}
|
|
13869
|
+
return { text: parts.join(`
|
|
13870
|
+
`), truncated: false };
|
|
13291
13871
|
}
|
|
13292
13872
|
function scanMcpManifest(manifest, options = {}) {
|
|
13293
13873
|
const tools = parseTools(manifest);
|
|
13294
13874
|
const matches = [];
|
|
13295
13875
|
const seenNames = new Map;
|
|
13296
13876
|
for (const tool of tools) {
|
|
13297
|
-
const text = toolText(tool);
|
|
13877
|
+
const { text, truncated } = toolText(tool);
|
|
13878
|
+
if (truncated) {
|
|
13879
|
+
matches.push(makeMatch("prompt-injection", "mcp.schema-too-large", "medium", 0.5, `tool:${tool.name}`, `Tool "${tool.name}" input schema is too large to scan whole (over ${SCHEMA_NODE_BUDGET} nodes); review it by hand.`));
|
|
13880
|
+
}
|
|
13298
13881
|
for (const pattern of POISONING_PATTERNS) {
|
|
13299
|
-
if (pattern.regex.test(text)) {
|
|
13882
|
+
if (pattern.test ? pattern.test(text) : pattern.regex.test(text)) {
|
|
13300
13883
|
const category = pattern.policyId === "mcp.poisoning.embedded-credential" ? "secret" : pattern.policyId === "mcp.poisoning.exfil-verb" ? "egress" : "prompt-injection";
|
|
13301
13884
|
matches.push(makeMatch(category, pattern.policyId, "high", pattern.confidence, `tool:${tool.name}`, `Tool "${tool.name}" description contains a tool-poisoning signal; treat MCP tool metadata as untrusted.`));
|
|
13302
13885
|
}
|
|
@@ -13324,7 +13907,7 @@ function scanMcpManifest(manifest, options = {}) {
|
|
|
13324
13907
|
}
|
|
13325
13908
|
return matches;
|
|
13326
13909
|
}
|
|
13327
|
-
var POISONING_PATTERNS, LINE_JUMPING_PATTERNS, INVISIBLE_UNICODE;
|
|
13910
|
+
var POISONING_PATTERNS, LINE_JUMPING_PATTERNS, INVISIBLE_UNICODE, SCHEMA_TEXT_KEYS, SCHEMA_NODE_BUDGET = 1e4;
|
|
13328
13911
|
var init_mcp = __esm(() => {
|
|
13329
13912
|
POISONING_PATTERNS = [
|
|
13330
13913
|
{
|
|
@@ -13354,7 +13937,8 @@ var init_mcp = __esm(() => {
|
|
|
13354
13937
|
},
|
|
13355
13938
|
{
|
|
13356
13939
|
policyId: "mcp.poisoning.comment-payload",
|
|
13357
|
-
regex:
|
|
13940
|
+
regex: /<!--|<important>/i,
|
|
13941
|
+
test: hasCommentPayload,
|
|
13358
13942
|
confidence: 0.7
|
|
13359
13943
|
}
|
|
13360
13944
|
];
|
|
@@ -13376,8 +13960,12 @@ var init_mcp = __esm(() => {
|
|
|
13376
13960
|
}
|
|
13377
13961
|
];
|
|
13378
13962
|
INVISIBLE_UNICODE = new RegExp("[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\u2066-\\u206F\\uFEFF]" + "|[\\u{E0000}-\\u{E007F}]", "u");
|
|
13963
|
+
SCHEMA_TEXT_KEYS = new Set(["description", "title", "default", "const", "examples", "enum"]);
|
|
13379
13964
|
});
|
|
13380
13965
|
|
|
13966
|
+
// src/lib/args.ts
|
|
13967
|
+
var init_args = () => {};
|
|
13968
|
+
|
|
13381
13969
|
// src/lib/shell-syntax.ts
|
|
13382
13970
|
function splitSegments(command) {
|
|
13383
13971
|
const segments = [];
|
|
@@ -13494,7 +14082,7 @@ function stripAssignments(words) {
|
|
|
13494
14082
|
return words.slice(i);
|
|
13495
14083
|
}
|
|
13496
14084
|
function commandWord(words) {
|
|
13497
|
-
const first = stripAssignments(words)[0] ?? "";
|
|
14085
|
+
const first = (stripAssignments(words)[0] ?? "").replace(/^[({]+/, "").replace(/\\(.)/g, "$1");
|
|
13498
14086
|
const base = first.split("/").pop() ?? first;
|
|
13499
14087
|
return base.toLowerCase();
|
|
13500
14088
|
}
|
|
@@ -13526,13 +14114,197 @@ function isCatastrophicTarget(arg) {
|
|
|
13526
14114
|
function hasRecursive(words) {
|
|
13527
14115
|
return args(words).some((a) => RECURSIVE_FLAG.test(a));
|
|
13528
14116
|
}
|
|
13529
|
-
function
|
|
14117
|
+
function gitConfigEntry(spec, fromEnv) {
|
|
14118
|
+
const eq = spec.indexOf("=");
|
|
14119
|
+
return { key: (eq < 0 ? spec : spec.slice(0, eq)).toLowerCase(), value: eq < 0 ? undefined : spec.slice(eq + 1), opaque: fromEnv || wordIsOpaque(spec) };
|
|
14120
|
+
}
|
|
14121
|
+
function parseGitInvocation(input) {
|
|
14122
|
+
const bare = stripAssignments(input);
|
|
14123
|
+
let envConfig = input.slice(0, input.length - bare.length).some((w) => GIT_CONFIG_SOURCE_ASSIGNMENT.test(w));
|
|
14124
|
+
const words = bare.map(unescapeWord);
|
|
14125
|
+
const config = [];
|
|
14126
|
+
let unknownGlobal = false;
|
|
14127
|
+
let i = 1;
|
|
14128
|
+
for (;i < words.length; i += 1) {
|
|
14129
|
+
const w = words[i];
|
|
14130
|
+
if (!w.startsWith("-"))
|
|
14131
|
+
break;
|
|
14132
|
+
const eq = w.startsWith("--") ? w.indexOf("=") : -1;
|
|
14133
|
+
const name = eq > 0 ? w.slice(0, eq) : w;
|
|
14134
|
+
if (GIT_GLOBAL_VALUE_OPTIONS.has(name)) {
|
|
14135
|
+
if (GIT_REPOSITORY_OPTIONS.has(name))
|
|
14136
|
+
envConfig = true;
|
|
14137
|
+
const value = eq > 0 ? w.slice(eq + 1) : words[++i];
|
|
14138
|
+
if (name === "-c" || name === "--config-env")
|
|
14139
|
+
config.push(gitConfigEntry(value ?? "", name === "--config-env"));
|
|
14140
|
+
continue;
|
|
14141
|
+
}
|
|
14142
|
+
if (!GIT_GLOBAL_FLAGS.has(name))
|
|
14143
|
+
unknownGlobal = true;
|
|
14144
|
+
}
|
|
14145
|
+
let rest = words.slice(i);
|
|
14146
|
+
const invocation = (sub, opaqueSub, shellAlias) => ({
|
|
14147
|
+
sub,
|
|
14148
|
+
args: rest.slice(1),
|
|
14149
|
+
config,
|
|
14150
|
+
opaqueSub,
|
|
14151
|
+
unknownGlobal,
|
|
14152
|
+
envConfig,
|
|
14153
|
+
...shellAlias !== undefined ? { shellAlias } : {}
|
|
14154
|
+
});
|
|
14155
|
+
for (let guard = 0;guard < 8 && rest.length > 0; guard += 1) {
|
|
14156
|
+
const head = rest[0];
|
|
14157
|
+
if (wordIsOpaque(head))
|
|
14158
|
+
return invocation(undefined, true);
|
|
14159
|
+
if (GIT_COMMANDS.has(head.toLowerCase()))
|
|
14160
|
+
return invocation(head.toLowerCase(), false);
|
|
14161
|
+
const alias = [...config].reverse().find((c) => c.key === `alias.${head.toLowerCase()}`);
|
|
14162
|
+
if (alias === undefined)
|
|
14163
|
+
return invocation(head, false);
|
|
14164
|
+
if (alias.opaque || alias.value === undefined)
|
|
14165
|
+
return invocation(undefined, true);
|
|
14166
|
+
if (alias.value.trimStart().startsWith("!"))
|
|
14167
|
+
return invocation(undefined, true, [alias.value.trimStart().slice(1), ...rest.slice(1)].join(" "));
|
|
14168
|
+
const expanded = splitWords(alias.value);
|
|
14169
|
+
if (expanded.length === 0 || expanded[0].startsWith("-"))
|
|
14170
|
+
return invocation(undefined, true);
|
|
14171
|
+
rest = [...expanded, ...rest.slice(1)];
|
|
14172
|
+
}
|
|
14173
|
+
return invocation(undefined, rest.length > 0);
|
|
14174
|
+
}
|
|
14175
|
+
function resolvePushOption(name) {
|
|
14176
|
+
if (PUSH_LONG_OPTIONS.has(name))
|
|
14177
|
+
return name;
|
|
14178
|
+
const candidates = [...PUSH_LONG_OPTIONS.keys()].filter((option) => option.startsWith(name));
|
|
14179
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
14180
|
+
}
|
|
14181
|
+
function parsePushArgs(args) {
|
|
14182
|
+
const request = { force: false, del: false, mirror: false, all: false, prune: false, unknown: false, positionals: [] };
|
|
14183
|
+
const set = (option) => {
|
|
14184
|
+
if (option === "force" || option === "force-with-lease" || option === "force-if-includes")
|
|
14185
|
+
request.force = true;
|
|
14186
|
+
else if (option === "delete")
|
|
14187
|
+
request.del = true;
|
|
14188
|
+
else if (option === "mirror")
|
|
14189
|
+
request.mirror = true;
|
|
14190
|
+
else if (option === "all" || option === "branches")
|
|
14191
|
+
request.all = true;
|
|
14192
|
+
else if (option === "prune")
|
|
14193
|
+
request.prune = true;
|
|
14194
|
+
};
|
|
14195
|
+
let options = true;
|
|
14196
|
+
for (let i = 0;i < args.length; i += 1) {
|
|
14197
|
+
const w = args[i];
|
|
14198
|
+
if (!options || w === "-" || !w.startsWith("-")) {
|
|
14199
|
+
request.positionals.push(w);
|
|
14200
|
+
continue;
|
|
14201
|
+
}
|
|
14202
|
+
if (w === "--") {
|
|
14203
|
+
options = false;
|
|
14204
|
+
continue;
|
|
14205
|
+
}
|
|
14206
|
+
if (w.startsWith("--")) {
|
|
14207
|
+
const eq = w.indexOf("=");
|
|
14208
|
+
const name = eq > 0 ? w.slice(2, eq) : w.slice(2);
|
|
14209
|
+
if (name.startsWith("n"))
|
|
14210
|
+
continue;
|
|
14211
|
+
const option = resolvePushOption(name);
|
|
14212
|
+
if (option === undefined) {
|
|
14213
|
+
request.unknown = true;
|
|
14214
|
+
continue;
|
|
14215
|
+
}
|
|
14216
|
+
if (PUSH_LONG_OPTIONS.get(option) === "value" && eq < 0)
|
|
14217
|
+
i += 1;
|
|
14218
|
+
set(option);
|
|
14219
|
+
continue;
|
|
14220
|
+
}
|
|
14221
|
+
for (let at = 1;at < w.length; at += 1) {
|
|
14222
|
+
const ch = w[at];
|
|
14223
|
+
if (ch === "o") {
|
|
14224
|
+
if (at === w.length - 1)
|
|
14225
|
+
i += 1;
|
|
14226
|
+
break;
|
|
14227
|
+
}
|
|
14228
|
+
if (!PUSH_SHORT_FLAGS.has(ch)) {
|
|
14229
|
+
request.unknown = true;
|
|
14230
|
+
break;
|
|
14231
|
+
}
|
|
14232
|
+
if (ch === "f")
|
|
14233
|
+
request.force = true;
|
|
14234
|
+
if (ch === "d")
|
|
14235
|
+
request.del = true;
|
|
14236
|
+
}
|
|
14237
|
+
}
|
|
14238
|
+
return request;
|
|
14239
|
+
}
|
|
14240
|
+
function isDangerousDestination(destination) {
|
|
14241
|
+
const name = (destination.split("/").pop() ?? "").toLowerCase();
|
|
14242
|
+
return destination.includes("*") || PROTECTED_BRANCHES.includes(name) || name === "" || name === "head" || name === "@";
|
|
14243
|
+
}
|
|
14244
|
+
function refspecDestroys(spec, force, del) {
|
|
14245
|
+
if (wordIsOpaque(spec))
|
|
14246
|
+
return true;
|
|
14247
|
+
const plus = spec.startsWith("+");
|
|
14248
|
+
const body = plus ? spec.slice(1) : spec;
|
|
14249
|
+
if (del)
|
|
14250
|
+
return isDangerousDestination(body);
|
|
14251
|
+
if (body === ":")
|
|
14252
|
+
return force || plus;
|
|
14253
|
+
const deletes = body.startsWith(":");
|
|
14254
|
+
if (!(force || plus || deletes))
|
|
14255
|
+
return false;
|
|
14256
|
+
const destination = body.slice(body.lastIndexOf(":") + 1);
|
|
14257
|
+
if (deletes && destination !== "" && !destination.includes("*")) {
|
|
14258
|
+
return PROTECTED_BRANCHES.includes((destination.split("/").pop() ?? "").toLowerCase());
|
|
14259
|
+
}
|
|
14260
|
+
return isDangerousDestination(destination);
|
|
14261
|
+
}
|
|
14262
|
+
function isConfigTrue(value) {
|
|
14263
|
+
return value === undefined || !/^(?:false|no|off|0|)$/i.test(value.trim());
|
|
14264
|
+
}
|
|
14265
|
+
function configArmsPush(entry) {
|
|
14266
|
+
if (CONFIG_INCLUDE_KEY.test(entry.key))
|
|
14267
|
+
return true;
|
|
14268
|
+
const match = PUSH_SHAPING_KEY.exec(entry.key);
|
|
14269
|
+
if (match === null)
|
|
14270
|
+
return false;
|
|
14271
|
+
if (entry.opaque)
|
|
14272
|
+
return true;
|
|
14273
|
+
return match[1] === "mirror" ? isConfigTrue(entry.value) : refspecDestroys(entry.value ?? "", false, false);
|
|
14274
|
+
}
|
|
14275
|
+
function configWriteArmsPush(entry) {
|
|
14276
|
+
if (!entry.key.startsWith("alias."))
|
|
14277
|
+
return configArmsPush(entry);
|
|
14278
|
+
if (entry.opaque || entry.value === undefined)
|
|
14279
|
+
return true;
|
|
14280
|
+
const value = entry.value.trimStart();
|
|
14281
|
+
return /\bpush\b/i.test(value) || value.startsWith("!") && isDestructiveCommand(value.slice(1));
|
|
14282
|
+
}
|
|
14283
|
+
function gitWords(v) {
|
|
14284
|
+
if (v.cmd === "git")
|
|
14285
|
+
return v.words;
|
|
14286
|
+
if (!WRAPPER_COMMANDS.has(v.cmd))
|
|
14287
|
+
return;
|
|
14288
|
+
const peeled = peelWrappers(v.words);
|
|
14289
|
+
return peeled.inner === undefined && baseName(peeled.words[0]) === "git" ? peeled.words : undefined;
|
|
14290
|
+
}
|
|
14291
|
+
function assignsGitConfigSource(words) {
|
|
14292
|
+
const bare = stripAssignments(words);
|
|
14293
|
+
if (words.slice(0, words.length - bare.length + 1).some((w) => GIT_CONFIG_SOURCE_ASSIGNMENT.test(w)))
|
|
14294
|
+
return true;
|
|
14295
|
+
const cmd = commandWord(bare);
|
|
14296
|
+
if (!ASSIGNING_BUILTINS.has(cmd) && !WRAPPER_COMMANDS.has(cmd))
|
|
14297
|
+
return false;
|
|
14298
|
+
return bare.slice(1).some((w) => GIT_CONFIG_SOURCE_ASSIGNMENT.test(w));
|
|
14299
|
+
}
|
|
14300
|
+
function classifySegment(seg, configRelocated) {
|
|
13530
14301
|
const view = {
|
|
13531
14302
|
raw: seg.raw,
|
|
13532
14303
|
cmd: head(seg.words),
|
|
13533
14304
|
args: args(seg.words),
|
|
13534
14305
|
positionals: positionals(seg.words),
|
|
13535
|
-
words: seg.words
|
|
14306
|
+
words: seg.words,
|
|
14307
|
+
configRelocated
|
|
13536
14308
|
};
|
|
13537
14309
|
return RULES3.some((rule) => rule(view));
|
|
13538
14310
|
}
|
|
@@ -13540,8 +14312,10 @@ function classifyCommand(command) {
|
|
|
13540
14312
|
if (command.trim().length === 0)
|
|
13541
14313
|
return "shell";
|
|
13542
14314
|
const segments = splitSegments(command);
|
|
14315
|
+
let configRelocated = false;
|
|
13543
14316
|
for (const seg of segments) {
|
|
13544
|
-
|
|
14317
|
+
configRelocated ||= assignsGitConfigSource(seg.words);
|
|
14318
|
+
if (classifySegment(seg, configRelocated))
|
|
13545
14319
|
return "destructive";
|
|
13546
14320
|
}
|
|
13547
14321
|
for (let i = 1;i < segments.length; i++) {
|
|
@@ -13560,8 +14334,78 @@ function isDestructiveCommand(command) {
|
|
|
13560
14334
|
return classifyCommand(command) === "destructive";
|
|
13561
14335
|
}
|
|
13562
14336
|
function touchesAgentCredentials(command) {
|
|
13563
|
-
|
|
13564
|
-
|
|
14337
|
+
return textSpellings(command).some((text) => CREDENTIAL_MARKERS.some((marker) => text.includes(marker)));
|
|
14338
|
+
}
|
|
14339
|
+
function textSpellings(command) {
|
|
14340
|
+
return [command, collapseQuoting(joinContinuations(command))].map((text) => text.toLowerCase().replace(/\s+/g, " "));
|
|
14341
|
+
}
|
|
14342
|
+
function baseName(word) {
|
|
14343
|
+
return ((word ?? "").split("/").pop() ?? "").toLowerCase();
|
|
14344
|
+
}
|
|
14345
|
+
function unescapeWord(word) {
|
|
14346
|
+
return word.replace(/\\(.)/g, "$1");
|
|
14347
|
+
}
|
|
14348
|
+
function peelWrappers(input) {
|
|
14349
|
+
let words = stripAssignments(input.map(unescapeWord));
|
|
14350
|
+
for (let guard = 0;guard < 16 && words.length > 0; guard++) {
|
|
14351
|
+
const cmd = (words[0].split("/").pop() ?? "").toLowerCase();
|
|
14352
|
+
if (SHELLS.has(cmd)) {
|
|
14353
|
+
const at = words.findIndex((w, i) => i > 0 && /^-[a-z]*c[a-z]*$/i.test(w));
|
|
14354
|
+
const script = at > 0 ? words[at + 1] : undefined;
|
|
14355
|
+
if (script !== undefined)
|
|
14356
|
+
return { words, inner: script };
|
|
14357
|
+
return { words };
|
|
14358
|
+
}
|
|
14359
|
+
if (cmd === "eval")
|
|
14360
|
+
return { words, inner: words.slice(1).join(" ") };
|
|
14361
|
+
if (!WRAPPER_COMMANDS.has(cmd))
|
|
14362
|
+
return { words };
|
|
14363
|
+
let i = 1;
|
|
14364
|
+
while (i < words.length) {
|
|
14365
|
+
const w = words[i];
|
|
14366
|
+
if (w === "--") {
|
|
14367
|
+
i++;
|
|
14368
|
+
break;
|
|
14369
|
+
}
|
|
14370
|
+
if (w.startsWith("-")) {
|
|
14371
|
+
i += WRAPPER_FLAGS_WITH_VALUE.has(w) ? 2 : 1;
|
|
14372
|
+
continue;
|
|
14373
|
+
}
|
|
14374
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(w)) {
|
|
14375
|
+
i++;
|
|
14376
|
+
continue;
|
|
14377
|
+
}
|
|
14378
|
+
if ((cmd === "timeout" || cmd === "nice") && /^[0-9.]+[smhd]?$/.test(w)) {
|
|
14379
|
+
i++;
|
|
14380
|
+
continue;
|
|
14381
|
+
}
|
|
14382
|
+
break;
|
|
14383
|
+
}
|
|
14384
|
+
words = words.slice(i);
|
|
14385
|
+
}
|
|
14386
|
+
return { words };
|
|
14387
|
+
}
|
|
14388
|
+
function decodeAnsiC(body) {
|
|
14389
|
+
return body.replace(/\\(?:x([0-9a-fA-F]{1,2})|u([0-9a-fA-F]{1,4})|U([0-9a-fA-F]{1,8})|([0-7]{1,3})|(.))/gs, (_m, x, u, big, oct, ch) => {
|
|
14390
|
+
const code = x ?? u ?? big;
|
|
14391
|
+
if (code !== undefined)
|
|
14392
|
+
return String.fromCodePoint(Math.min(Number.parseInt(code, 16), 1114111));
|
|
14393
|
+
if (oct !== undefined)
|
|
14394
|
+
return String.fromCharCode(Number.parseInt(oct, 8) & 255);
|
|
14395
|
+
return ANSI_C_ESCAPES[ch ?? ""] ?? `\\${ch ?? ""}`;
|
|
14396
|
+
});
|
|
14397
|
+
}
|
|
14398
|
+
function joinContinuations(command) {
|
|
14399
|
+
return command.replace(/\\\r?\n/g, "");
|
|
14400
|
+
}
|
|
14401
|
+
function expandAnsiCQuotes(command) {
|
|
14402
|
+
return command.replace(/\$'((?:[^'\\]|\\.)*)'/gs, (_m, body) => `'${decodeAnsiC(body).replace(/'/g, "'\\''")}'`);
|
|
14403
|
+
}
|
|
14404
|
+
function collapseQuoting(command) {
|
|
14405
|
+
return expandAnsiCQuotes(command).replace(/["'\\]/g, "");
|
|
14406
|
+
}
|
|
14407
|
+
function wordIsOpaque(word) {
|
|
14408
|
+
return /[$`{]/.test(word);
|
|
13565
14409
|
}
|
|
13566
14410
|
var CATASTROPHIC_TARGETS, RECURSIVE_FLAG, PRIVILEGE, INTERPRETERS, DOWNLOADERS, CONTAINER_RUNTIMES, PROTECTED_BRANCHES, BLOCK_DEVICE, rulePrivilege = (v) => PRIVILEGE.has(v.cmd), ruleBlockDeviceRedirect = (v) => BLOCK_DEVICE.test(v.raw) && /(?:^|\s)(?:>|>>)\s*\/dev\//.test(v.raw), ruleRm = (v) => v.cmd === "rm" && v.positionals.some(isCatastrophicTarget), ruleDd = (v) => v.cmd === "dd" && v.args.some((x) => /^of=/i.test(x) && BLOCK_DEVICE.test(x)), rulePermissionSweep = (v) => (v.cmd === "chmod" || v.cmd === "chown" || v.cmd === "chgrp") && hasRecursive(v.words) && v.positionals.some(isCatastrophicTarget), ruleHostState = (v) => {
|
|
13567
14411
|
if (v.cmd === "shutdown" || v.cmd === "reboot" || v.cmd === "halt" || v.cmd === "poweroff")
|
|
@@ -13572,19 +14416,52 @@ var CATASTROPHIC_TARGETS, RECURSIVE_FLAG, PRIVILEGE, INTERPRETERS, DOWNLOADERS,
|
|
|
13572
14416
|
return false;
|
|
13573
14417
|
const joined = ` ${v.args.join(" ")}`;
|
|
13574
14418
|
return /--privileged\b/.test(joined) || /--pid[= ]host\b/.test(joined) || /--(?:userns|ipc|uts|network|net)[= ]host\b/.test(joined) || /docker\.sock/.test(joined) || /(?:^|\s)(?:-v|--volume)[= ]\/:/.test(joined) || /source=\/(?:,|\s|$)/.test(joined);
|
|
13575
|
-
}, ruleForcePush = (v) => {
|
|
13576
|
-
|
|
14419
|
+
}, GIT_GLOBAL_VALUE_OPTIONS, GIT_GLOBAL_FLAGS, GIT_COMMANDS, GIT_CONFIG_SOURCE_ASSIGNMENT, GIT_REPOSITORY_OPTIONS, PUSH_LONG_OPTIONS, PUSH_SHORT_FLAGS, PUSH_SHAPING_KEY, CONFIG_INCLUDE_KEY, ruleForcePush = (v) => {
|
|
14420
|
+
const words = gitWords(v);
|
|
14421
|
+
if (words === undefined)
|
|
13577
14422
|
return false;
|
|
13578
|
-
const
|
|
13579
|
-
if (
|
|
14423
|
+
const git = parseGitInvocation(words);
|
|
14424
|
+
if (git.shellAlias !== undefined)
|
|
14425
|
+
return isDestructiveCommand(git.shellAlias) || /\bpush\b/.test(git.shellAlias);
|
|
14426
|
+
if (git.opaqueSub)
|
|
14427
|
+
return true;
|
|
14428
|
+
if (git.unknownGlobal && positionals(words).includes("push"))
|
|
14429
|
+
return true;
|
|
14430
|
+
if (git.sub !== "push")
|
|
13580
14431
|
return false;
|
|
13581
|
-
|
|
13582
|
-
if (after.length === 0)
|
|
14432
|
+
if (git.envConfig || v.configRelocated === true || git.args.some(wordIsOpaque) || git.config.some(configArmsPush))
|
|
13583
14433
|
return true;
|
|
13584
|
-
const
|
|
13585
|
-
|
|
13586
|
-
|
|
14434
|
+
const push = parsePushArgs(git.args);
|
|
14435
|
+
if (push.unknown || push.mirror)
|
|
14436
|
+
return true;
|
|
14437
|
+
const refspecs = push.positionals.slice(1);
|
|
14438
|
+
if (push.all && (push.force || push.prune))
|
|
14439
|
+
return true;
|
|
14440
|
+
if (push.prune && (refspecs.length === 0 || refspecs.some((spec) => spec.includes("*"))))
|
|
14441
|
+
return true;
|
|
14442
|
+
if (refspecs.length === 0)
|
|
14443
|
+
return push.force;
|
|
14444
|
+
return refspecs.some((spec) => refspecDestroys(spec, push.force, push.del));
|
|
14445
|
+
}, ruleArmsForcePush = (v) => {
|
|
14446
|
+
const words = gitWords(v);
|
|
14447
|
+
if (words === undefined)
|
|
14448
|
+
return false;
|
|
14449
|
+
const git = parseGitInvocation(words);
|
|
14450
|
+
if (git.sub === "remote") {
|
|
14451
|
+
if (git.args[0] !== "add")
|
|
14452
|
+
return false;
|
|
14453
|
+
return git.args.some((w) => {
|
|
14454
|
+
const [name = "", value] = w.split("=", 2);
|
|
14455
|
+
return name.length > 3 && "--mirror".startsWith(name) && value !== "fetch";
|
|
14456
|
+
});
|
|
14457
|
+
}
|
|
14458
|
+
if (git.sub !== "config")
|
|
14459
|
+
return false;
|
|
14460
|
+
const operands = git.args.filter((w) => !w.startsWith("-"));
|
|
14461
|
+
return operands.some((w, i) => i + 1 < operands.length && configWriteArmsPush(gitConfigEntry(`${w}=${operands[i + 1]}`, false)));
|
|
14462
|
+
}, RULES3, ASSIGNING_BUILTINS, CREDENTIAL_MARKERS, SYSTEMCTL_CONTROL_VERBS, SCHEDULE_VERBS, WRAPPER_COMMANDS, WRAPPER_FLAGS_WITH_VALUE, SHELLS, KERYX_ENTRY_NAMES, JS_RUNNERS, TERMINAL_DRIVERS, ANSI_C_ESCAPES, OPERATOR_REVIEW_VERBS, UNATTENDED_REVIEW_VERBS, HUB_MACHINE_OPERATOR_VERBS, OPERATOR_COMMAND_PATHS, MAX_COMMAND_PATH, OPERATOR_PATH_WORDS;
|
|
13587
14463
|
var init_command_risk = __esm(() => {
|
|
14464
|
+
init_args();
|
|
13588
14465
|
init_shell_syntax();
|
|
13589
14466
|
CATASTROPHIC_TARGETS = new Set([
|
|
13590
14467
|
"/",
|
|
@@ -13631,6 +14508,255 @@ var init_command_risk = __esm(() => {
|
|
|
13631
14508
|
CONTAINER_RUNTIMES = new Set(["docker", "podman", "nerdctl"]);
|
|
13632
14509
|
PROTECTED_BRANCHES = ["main", "master", "develop", "development", "trunk"];
|
|
13633
14510
|
BLOCK_DEVICE = /\/dev\/(?:sd[a-z]|nvme\d|disk\d|hd[a-z]|vd[a-z]|mmcblk\d)/i;
|
|
14511
|
+
GIT_GLOBAL_VALUE_OPTIONS = new Set([
|
|
14512
|
+
"-C",
|
|
14513
|
+
"-c",
|
|
14514
|
+
"--git-dir",
|
|
14515
|
+
"--work-tree",
|
|
14516
|
+
"--namespace",
|
|
14517
|
+
"--super-prefix",
|
|
14518
|
+
"--config-env",
|
|
14519
|
+
"--attr-source"
|
|
14520
|
+
]);
|
|
14521
|
+
GIT_GLOBAL_FLAGS = new Set([
|
|
14522
|
+
"-p",
|
|
14523
|
+
"--paginate",
|
|
14524
|
+
"-P",
|
|
14525
|
+
"--no-pager",
|
|
14526
|
+
"--no-replace-objects",
|
|
14527
|
+
"--bare",
|
|
14528
|
+
"--literal-pathspecs",
|
|
14529
|
+
"--glob-pathspecs",
|
|
14530
|
+
"--noglob-pathspecs",
|
|
14531
|
+
"--icase-pathspecs",
|
|
14532
|
+
"--no-optional-locks",
|
|
14533
|
+
"--no-lazy-fetch",
|
|
14534
|
+
"--no-advice",
|
|
14535
|
+
"--html-path",
|
|
14536
|
+
"--man-path",
|
|
14537
|
+
"--info-path",
|
|
14538
|
+
"--exec-path",
|
|
14539
|
+
"--list-cmds",
|
|
14540
|
+
"-v",
|
|
14541
|
+
"--version",
|
|
14542
|
+
"-h",
|
|
14543
|
+
"--help"
|
|
14544
|
+
]);
|
|
14545
|
+
GIT_COMMANDS = new Set([
|
|
14546
|
+
"add",
|
|
14547
|
+
"am",
|
|
14548
|
+
"annotate",
|
|
14549
|
+
"apply",
|
|
14550
|
+
"archimport",
|
|
14551
|
+
"archive",
|
|
14552
|
+
"backfill",
|
|
14553
|
+
"bisect",
|
|
14554
|
+
"blame",
|
|
14555
|
+
"branch",
|
|
14556
|
+
"bugreport",
|
|
14557
|
+
"bundle",
|
|
14558
|
+
"cat-file",
|
|
14559
|
+
"check-attr",
|
|
14560
|
+
"check-ignore",
|
|
14561
|
+
"check-mailmap",
|
|
14562
|
+
"check-ref-format",
|
|
14563
|
+
"checkout",
|
|
14564
|
+
"checkout--worker",
|
|
14565
|
+
"checkout-index",
|
|
14566
|
+
"cherry",
|
|
14567
|
+
"cherry-pick",
|
|
14568
|
+
"clean",
|
|
14569
|
+
"clone",
|
|
14570
|
+
"column",
|
|
14571
|
+
"commit",
|
|
14572
|
+
"commit-graph",
|
|
14573
|
+
"commit-tree",
|
|
14574
|
+
"config",
|
|
14575
|
+
"count-objects",
|
|
14576
|
+
"credential",
|
|
14577
|
+
"credential-cache",
|
|
14578
|
+
"credential-cache--daemon",
|
|
14579
|
+
"credential-netrc",
|
|
14580
|
+
"credential-osxkeychain",
|
|
14581
|
+
"credential-store",
|
|
14582
|
+
"cvsexportcommit",
|
|
14583
|
+
"cvsimport",
|
|
14584
|
+
"cvsserver",
|
|
14585
|
+
"daemon",
|
|
14586
|
+
"describe",
|
|
14587
|
+
"diagnose",
|
|
14588
|
+
"diff",
|
|
14589
|
+
"diff-files",
|
|
14590
|
+
"diff-index",
|
|
14591
|
+
"diff-pairs",
|
|
14592
|
+
"diff-tree",
|
|
14593
|
+
"difftool",
|
|
14594
|
+
"difftool--helper",
|
|
14595
|
+
"env--helper",
|
|
14596
|
+
"fast-export",
|
|
14597
|
+
"fast-import",
|
|
14598
|
+
"fetch",
|
|
14599
|
+
"fetch-pack",
|
|
14600
|
+
"filter-branch",
|
|
14601
|
+
"fmt-merge-msg",
|
|
14602
|
+
"for-each-ref",
|
|
14603
|
+
"for-each-repo",
|
|
14604
|
+
"format-patch",
|
|
14605
|
+
"fsck",
|
|
14606
|
+
"fsck-objects",
|
|
14607
|
+
"fsmonitor--daemon",
|
|
14608
|
+
"gc",
|
|
14609
|
+
"get-tar-commit-id",
|
|
14610
|
+
"grep",
|
|
14611
|
+
"hash-object",
|
|
14612
|
+
"help",
|
|
14613
|
+
"hook",
|
|
14614
|
+
"http-backend",
|
|
14615
|
+
"http-fetch",
|
|
14616
|
+
"http-push",
|
|
14617
|
+
"imap-send",
|
|
14618
|
+
"index-pack",
|
|
14619
|
+
"init",
|
|
14620
|
+
"init-db",
|
|
14621
|
+
"instaweb",
|
|
14622
|
+
"interpret-trailers",
|
|
14623
|
+
"log",
|
|
14624
|
+
"ls-files",
|
|
14625
|
+
"ls-remote",
|
|
14626
|
+
"ls-tree",
|
|
14627
|
+
"mailinfo",
|
|
14628
|
+
"mailsplit",
|
|
14629
|
+
"maintenance",
|
|
14630
|
+
"merge",
|
|
14631
|
+
"merge-base",
|
|
14632
|
+
"merge-file",
|
|
14633
|
+
"merge-index",
|
|
14634
|
+
"merge-octopus",
|
|
14635
|
+
"merge-one-file",
|
|
14636
|
+
"merge-ours",
|
|
14637
|
+
"merge-recursive",
|
|
14638
|
+
"merge-recursive-ours",
|
|
14639
|
+
"merge-recursive-theirs",
|
|
14640
|
+
"merge-resolve",
|
|
14641
|
+
"merge-subtree",
|
|
14642
|
+
"merge-tree",
|
|
14643
|
+
"mergetool",
|
|
14644
|
+
"mktag",
|
|
14645
|
+
"mktree",
|
|
14646
|
+
"multi-pack-index",
|
|
14647
|
+
"mv",
|
|
14648
|
+
"name-rev",
|
|
14649
|
+
"notes",
|
|
14650
|
+
"p4",
|
|
14651
|
+
"pack-objects",
|
|
14652
|
+
"pack-redundant",
|
|
14653
|
+
"pack-refs",
|
|
14654
|
+
"patch-id",
|
|
14655
|
+
"pickaxe",
|
|
14656
|
+
"prune",
|
|
14657
|
+
"prune-packed",
|
|
14658
|
+
"pull",
|
|
14659
|
+
"push",
|
|
14660
|
+
"quiltimport",
|
|
14661
|
+
"range-diff",
|
|
14662
|
+
"read-tree",
|
|
14663
|
+
"rebase",
|
|
14664
|
+
"rebase--interactive",
|
|
14665
|
+
"receive-pack",
|
|
14666
|
+
"reflog",
|
|
14667
|
+
"refs",
|
|
14668
|
+
"remote",
|
|
14669
|
+
"remote-ext",
|
|
14670
|
+
"remote-fd",
|
|
14671
|
+
"remote-ftp",
|
|
14672
|
+
"remote-ftps",
|
|
14673
|
+
"remote-http",
|
|
14674
|
+
"remote-https",
|
|
14675
|
+
"repack",
|
|
14676
|
+
"replace",
|
|
14677
|
+
"replay",
|
|
14678
|
+
"request-pull",
|
|
14679
|
+
"rerere",
|
|
14680
|
+
"reset",
|
|
14681
|
+
"restore",
|
|
14682
|
+
"rev-list",
|
|
14683
|
+
"rev-parse",
|
|
14684
|
+
"revert",
|
|
14685
|
+
"rm",
|
|
14686
|
+
"send-email",
|
|
14687
|
+
"send-pack",
|
|
14688
|
+
"serve",
|
|
14689
|
+
"sh-i18n--envsubst",
|
|
14690
|
+
"shell",
|
|
14691
|
+
"shortlog",
|
|
14692
|
+
"show",
|
|
14693
|
+
"show-branch",
|
|
14694
|
+
"show-index",
|
|
14695
|
+
"show-ref",
|
|
14696
|
+
"sparse-checkout",
|
|
14697
|
+
"stage",
|
|
14698
|
+
"stash",
|
|
14699
|
+
"status",
|
|
14700
|
+
"stripspace",
|
|
14701
|
+
"submodule",
|
|
14702
|
+
"submodule--helper",
|
|
14703
|
+
"subtree",
|
|
14704
|
+
"switch",
|
|
14705
|
+
"symbolic-ref",
|
|
14706
|
+
"tag",
|
|
14707
|
+
"unpack-file",
|
|
14708
|
+
"unpack-objects",
|
|
14709
|
+
"update-index",
|
|
14710
|
+
"update-ref",
|
|
14711
|
+
"update-server-info",
|
|
14712
|
+
"upload-archive",
|
|
14713
|
+
"upload-archive--writer",
|
|
14714
|
+
"upload-pack",
|
|
14715
|
+
"var",
|
|
14716
|
+
"verify-commit",
|
|
14717
|
+
"verify-pack",
|
|
14718
|
+
"verify-tag",
|
|
14719
|
+
"version",
|
|
14720
|
+
"web--browse",
|
|
14721
|
+
"whatchanged",
|
|
14722
|
+
"worktree",
|
|
14723
|
+
"write-tree"
|
|
14724
|
+
]);
|
|
14725
|
+
GIT_CONFIG_SOURCE_ASSIGNMENT = /^[({]*(?:HOME|XDG_CONFIG_HOME|GIT_DIR|GIT_COMMON_DIR|GIT_WORK_TREE|GIT_CONFIG\w*)=/i;
|
|
14726
|
+
GIT_REPOSITORY_OPTIONS = new Set(["--git-dir", "--work-tree"]);
|
|
14727
|
+
PUSH_LONG_OPTIONS = new Map([
|
|
14728
|
+
["verbose", "flag"],
|
|
14729
|
+
["quiet", "flag"],
|
|
14730
|
+
["repo", "value"],
|
|
14731
|
+
["all", "flag"],
|
|
14732
|
+
["branches", "flag"],
|
|
14733
|
+
["mirror", "flag"],
|
|
14734
|
+
["delete", "flag"],
|
|
14735
|
+
["tags", "flag"],
|
|
14736
|
+
["dry-run", "flag"],
|
|
14737
|
+
["porcelain", "flag"],
|
|
14738
|
+
["force", "flag"],
|
|
14739
|
+
["force-with-lease", "optional"],
|
|
14740
|
+
["force-if-includes", "flag"],
|
|
14741
|
+
["recurse-submodules", "value"],
|
|
14742
|
+
["thin", "flag"],
|
|
14743
|
+
["receive-pack", "value"],
|
|
14744
|
+
["exec", "value"],
|
|
14745
|
+
["set-upstream", "flag"],
|
|
14746
|
+
["progress", "flag"],
|
|
14747
|
+
["prune", "flag"],
|
|
14748
|
+
["no-verify", "flag"],
|
|
14749
|
+
["verify", "flag"],
|
|
14750
|
+
["follow-tags", "flag"],
|
|
14751
|
+
["signed", "optional"],
|
|
14752
|
+
["atomic", "flag"],
|
|
14753
|
+
["push-option", "value"],
|
|
14754
|
+
["ipv4", "flag"],
|
|
14755
|
+
["ipv6", "flag"]
|
|
14756
|
+
]);
|
|
14757
|
+
PUSH_SHORT_FLAGS = new Set(["v", "q", "d", "n", "f", "u", "4", "6"]);
|
|
14758
|
+
PUSH_SHAPING_KEY = /^remote\..+\.(push|mirror)$/;
|
|
14759
|
+
CONFIG_INCLUDE_KEY = /^include(?:if\..*)?\.path$/;
|
|
13634
14760
|
RULES3 = [
|
|
13635
14761
|
rulePrivilege,
|
|
13636
14762
|
ruleBlockDeviceRedirect,
|
|
@@ -13640,8 +14766,10 @@ var init_command_risk = __esm(() => {
|
|
|
13640
14766
|
ruleHostState,
|
|
13641
14767
|
ruleMkfs,
|
|
13642
14768
|
ruleContainerEscape,
|
|
13643
|
-
ruleForcePush
|
|
14769
|
+
ruleForcePush,
|
|
14770
|
+
ruleArmsForcePush
|
|
13644
14771
|
];
|
|
14772
|
+
ASSIGNING_BUILTINS = new Set(["export", "declare", "typeset", "readonly", "local"]);
|
|
13645
14773
|
CREDENTIAL_MARKERS = [
|
|
13646
14774
|
"permissions.json",
|
|
13647
14775
|
"auth.json",
|
|
@@ -13696,6 +14824,21 @@ var init_command_risk = __esm(() => {
|
|
|
13696
14824
|
KERYX_ENTRY_NAMES = new Set(["keryx", "cli.ts", "cli.js"]);
|
|
13697
14825
|
JS_RUNNERS = new Set(["bun", "bunx", "node", "npx", "pnpx", "tsx", "deno"]);
|
|
13698
14826
|
TERMINAL_DRIVERS = new Set(["script", "screen", "tmux", "unbuffer", "dtach", "abduco", "expect", "socat", "zpty"]);
|
|
14827
|
+
ANSI_C_ESCAPES = { n: `
|
|
14828
|
+
`, t: "\t", r: "\r", a: "\x07", b: "\b", e: "\x1B", E: "\x1B", f: "\f", v: "\v", "\\": "\\", "'": "'", '"': '"', "?": "?" };
|
|
14829
|
+
OPERATOR_REVIEW_VERBS = new Set(["waive-mutation", "override-reviewers", "confirm-plan", "decide-premise"]);
|
|
14830
|
+
UNATTENDED_REVIEW_VERBS = new Set(["start", "attach", "plan"]);
|
|
14831
|
+
HUB_MACHINE_OPERATOR_VERBS = new Set(["pair", "allow", "configure", "unpair"]);
|
|
14832
|
+
OPERATOR_COMMAND_PATHS = [
|
|
14833
|
+
...["accept", "promote", "graduate"].map((verb) => ["learn", verb]),
|
|
14834
|
+
["agents", "external", "apply"],
|
|
14835
|
+
...[...OPERATOR_REVIEW_VERBS, ...UNATTENDED_REVIEW_VERBS, "estimate"].map((verb) => ["review", verb]),
|
|
14836
|
+
["review", "comments", "reply"],
|
|
14837
|
+
["review", "chunks", "estimate"],
|
|
14838
|
+
...[...HUB_MACHINE_OPERATOR_VERBS].map((verb) => ["hub", "machine", verb])
|
|
14839
|
+
];
|
|
14840
|
+
MAX_COMMAND_PATH = Math.max(...OPERATOR_COMMAND_PATHS.map((path) => path.length));
|
|
14841
|
+
OPERATOR_PATH_WORDS = new Set(OPERATOR_COMMAND_PATHS.flat());
|
|
13699
14842
|
});
|
|
13700
14843
|
|
|
13701
14844
|
// src/security/audit-harness/checks.ts
|
|
@@ -13736,6 +14879,9 @@ function foldFullWidth(ch) {
|
|
|
13736
14879
|
const code = ch.codePointAt(0);
|
|
13737
14880
|
return code >= 65281 && code <= 65374 ? String.fromCodePoint(code - 65248) : ch;
|
|
13738
14881
|
}
|
|
14882
|
+
function isCodePoint(code) {
|
|
14883
|
+
return Number.isInteger(code) && code >= 0 && code <= 1114111;
|
|
14884
|
+
}
|
|
13739
14885
|
function decodeEntityStep(text) {
|
|
13740
14886
|
const match = HTML_ENTITY_AT_RE.exec(text);
|
|
13741
14887
|
if (!match)
|
|
@@ -13743,11 +14889,11 @@ function decodeEntityStep(text) {
|
|
|
13743
14889
|
const body = match[1];
|
|
13744
14890
|
if (body.startsWith("#x")) {
|
|
13745
14891
|
const code = Number.parseInt(body.slice(2), 16);
|
|
13746
|
-
return
|
|
14892
|
+
return isCodePoint(code) ? String.fromCodePoint(code) : undefined;
|
|
13747
14893
|
}
|
|
13748
14894
|
if (body.startsWith("#")) {
|
|
13749
14895
|
const code = Number.parseInt(body.slice(1), 10);
|
|
13750
|
-
return
|
|
14896
|
+
return isCodePoint(code) ? String.fromCodePoint(code) : undefined;
|
|
13751
14897
|
}
|
|
13752
14898
|
return NAMED_HTML_ENTITIES[body];
|
|
13753
14899
|
}
|
|
@@ -13973,7 +15119,7 @@ function checkMissingDenyList(relativePath, settings) {
|
|
|
13973
15119
|
return [];
|
|
13974
15120
|
}
|
|
13975
15121
|
const coversCredentials = deny.some((pattern) => touchesAgentCredentials(pattern));
|
|
13976
|
-
if (coversCredentials
|
|
15122
|
+
if (coversCredentials) {
|
|
13977
15123
|
return [];
|
|
13978
15124
|
}
|
|
13979
15125
|
const pointer = "/permissions/deny";
|
|
@@ -14023,9 +15169,13 @@ function checkBypassFlagPresent(relativePath, settings) {
|
|
|
14023
15169
|
const findings = [];
|
|
14024
15170
|
const root = asRecord2(settings);
|
|
14025
15171
|
const permissions = asRecord2(root?.permissions);
|
|
14026
|
-
const
|
|
14027
|
-
|
|
14028
|
-
|
|
15172
|
+
const modeLocations = [
|
|
15173
|
+
{ pointer: "/permissions/defaultMode", value: permissions?.defaultMode },
|
|
15174
|
+
{ pointer: "/defaultMode", value: root?.defaultMode }
|
|
15175
|
+
];
|
|
15176
|
+
for (const { pointer, value } of modeLocations) {
|
|
15177
|
+
if (value !== "bypassPermissions")
|
|
15178
|
+
continue;
|
|
14029
15179
|
findings.push({
|
|
14030
15180
|
surface: "settings",
|
|
14031
15181
|
check: "bypass-flag-present",
|
|
@@ -14037,7 +15187,7 @@ function checkBypassFlagPresent(relativePath, settings) {
|
|
|
14037
15187
|
evidence: { category: "artifact-safety", matchedToken: "defaultMode:bypassPermissions" },
|
|
14038
15188
|
internalProposal: {
|
|
14039
15189
|
proposal: {
|
|
14040
|
-
id: `reset-default-mode-${relativePath}`,
|
|
15190
|
+
id: pointer === "/defaultMode" ? `reset-default-mode-root-${relativePath}` : `reset-default-mode-${relativePath}`,
|
|
14041
15191
|
rationale: 'Set defaultMode back to "default" so the approval gate applies.',
|
|
14042
15192
|
patch: `- "defaultMode": "bypassPermissions"
|
|
14043
15193
|
+ "defaultMode": "default"`
|
|
@@ -14069,18 +15219,11 @@ function checkBypassFlagPresent(relativePath, settings) {
|
|
|
14069
15219
|
function isPinnedPackageSpec(spec) {
|
|
14070
15220
|
if (spec.length === 0)
|
|
14071
15221
|
return false;
|
|
14072
|
-
|
|
14073
|
-
const secondAt = spec.indexOf("@", 1);
|
|
14074
|
-
if (secondAt === -1)
|
|
14075
|
-
return false;
|
|
14076
|
-
const version = spec.slice(secondAt + 1);
|
|
14077
|
-
return version.length > 0 && version !== "latest";
|
|
14078
|
-
}
|
|
14079
|
-
const at = spec.indexOf("@");
|
|
15222
|
+
const at = spec.indexOf("@", spec.startsWith("@") ? 1 : 0);
|
|
14080
15223
|
if (at === -1)
|
|
14081
15224
|
return false;
|
|
14082
15225
|
const version = spec.slice(at + 1);
|
|
14083
|
-
return version
|
|
15226
|
+
return EXACT_VERSION_RE.test(version) || COMMIT_SHA_RE.test(version) || INTEGRITY_HASH_RE.test(version);
|
|
14084
15227
|
}
|
|
14085
15228
|
function packageArgFromLauncher(command, argv) {
|
|
14086
15229
|
const base = command.split("/").pop() ?? command;
|
|
@@ -14498,7 +15641,7 @@ function checkAgentMissingModelTier(relativePath, content) {
|
|
|
14498
15641
|
}
|
|
14499
15642
|
];
|
|
14500
15643
|
}
|
|
14501
|
-
var DROPPED_SINGLE_CODEPOINTS, COMBINING_MARK_RANGES, MATH_ALPHANUMERIC_LETTER_STYLE_STARTS, MATH_ALPHANUMERIC_DIGIT_STYLE_STARTS, MATH_ALPHANUMERIC_EXCEPTIONS, HTML_ENTITY_AT_RE, NAMED_HTML_ENTITIES, HOMOGLYPH_FOLD, QUOTE_CHARS_ANY, SENTENCE_END_CHARS, REPORTED_SPEECH_RE, SEVERITY_RANK, AUTO_RUN_PATTERNS, WILDCARD_ALLOW_PATTERNS, BYPASS_FLAGS, TOOL_INPUT_VAR = "(?:TOOL_INPUT|CLAUDE_TOOL_INPUT|tool_input|ARGUMENTS|1)", INTERP_VAR_RE, INSIDE_SUBSHELL_RE, INSIDE_BACKTICK_RE, INSIDE_DQUOTE_RE, NETWORK_CLIENTS, SHELL_INTERPRETERS = "sh|bash|zsh|dash|ksh|python3?|node|perl|ruby", SHELL_LIKE_INTERPRETERS = "sh|bash|zsh|dash|ksh", SCRIPT_STDIN_INTERPRETERS = "python3?|node|perl|ruby", DOWNLOAD_TOOLS = "curl|wget", ABS_PATH_PREFIX = "(?:/usr/bin/|/bin/|/usr/local/bin/)?", WRAPPER_PREFIX, TEE_HOP = `(?:tee\\s+\\S+\\s*\\|\\s*)?`, BACKTICK = "\\u0060", PIPE_TO_SHELL_RE, PIPE_TO_SCRIPT_STDIN_RE, DASH_C_COMMAND_SUB_RE, PROCESS_SUB_RE, SOURCE_PROCESS_SUB_RE, EVAL_COMMAND_SUB_RE, DOWNLOAD_THEN_EXEC_RE, POWERSHELL_IEX_RE, POWERSHELL_PIPE_IEX_RE, POWERSHELL_DOWNLOADSTRING_RE, REMOTE_EXEC_SHAPES, INTERPRETER_EXEC_FLAG_RE, REVERSE_SHELL_RE, BASE64_PIPE_SHELL_RE, XARGS_TO_SHELL_C_RE, DOWNLOAD_CHMOD_EXEC_RE, BEHAVIOR_CLASS_SHAPES, MAX_FENCE_INDENT = 3, TRAILING_EXIT0_RE, EMPTY_CATCH_RE, SCRIPT_SHELL_SUPPRESSION_RE;
|
|
15644
|
+
var DROPPED_SINGLE_CODEPOINTS, COMBINING_MARK_RANGES, MATH_ALPHANUMERIC_LETTER_STYLE_STARTS, MATH_ALPHANUMERIC_DIGIT_STYLE_STARTS, MATH_ALPHANUMERIC_EXCEPTIONS, HTML_ENTITY_AT_RE, NAMED_HTML_ENTITIES, HOMOGLYPH_FOLD, QUOTE_CHARS_ANY, SENTENCE_END_CHARS, REPORTED_SPEECH_RE, SEVERITY_RANK, AUTO_RUN_PATTERNS, WILDCARD_ALLOW_PATTERNS, BYPASS_FLAGS, EXACT_VERSION_RE, COMMIT_SHA_RE, INTEGRITY_HASH_RE, TOOL_INPUT_VAR = "(?:TOOL_INPUT|CLAUDE_TOOL_INPUT|tool_input|ARGUMENTS|1)", INTERP_VAR_RE, INSIDE_SUBSHELL_RE, INSIDE_BACKTICK_RE, INSIDE_DQUOTE_RE, NETWORK_CLIENTS, SHELL_INTERPRETERS = "sh|bash|zsh|dash|ksh|python3?|node|perl|ruby", SHELL_LIKE_INTERPRETERS = "sh|bash|zsh|dash|ksh", SCRIPT_STDIN_INTERPRETERS = "python3?|node|perl|ruby", DOWNLOAD_TOOLS = "curl|wget", ABS_PATH_PREFIX = "(?:/usr/bin/|/bin/|/usr/local/bin/)?", WRAPPER_PREFIX, TEE_HOP = `(?:tee\\s+\\S+\\s*\\|\\s*)?`, BACKTICK = "\\u0060", PIPE_TO_SHELL_RE, PIPE_TO_SCRIPT_STDIN_RE, DASH_C_COMMAND_SUB_RE, PROCESS_SUB_RE, SOURCE_PROCESS_SUB_RE, EVAL_COMMAND_SUB_RE, DOWNLOAD_THEN_EXEC_RE, POWERSHELL_IEX_RE, POWERSHELL_PIPE_IEX_RE, POWERSHELL_DOWNLOADSTRING_RE, REMOTE_EXEC_SHAPES, INTERPRETER_EXEC_FLAG_RE, REVERSE_SHELL_RE, BASE64_PIPE_SHELL_RE, XARGS_TO_SHELL_C_RE, DOWNLOAD_CHMOD_EXEC_RE, BEHAVIOR_CLASS_SHAPES, MAX_FENCE_INDENT = 3, TRAILING_EXIT0_RE, EMPTY_CATCH_RE, SCRIPT_SHELL_SUPPRESSION_RE;
|
|
14502
15645
|
var init_checks = __esm(() => {
|
|
14503
15646
|
init_injection();
|
|
14504
15647
|
init_secrets();
|
|
@@ -14656,6 +15799,9 @@ var init_checks = __esm(() => {
|
|
|
14656
15799
|
"--yolo",
|
|
14657
15800
|
"--dangerously-bypass-approvals-and-sandbox"
|
|
14658
15801
|
];
|
|
15802
|
+
EXACT_VERSION_RE = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
15803
|
+
COMMIT_SHA_RE = /^[0-9a-f]{40}$/i;
|
|
15804
|
+
INTEGRITY_HASH_RE = /^sha(?:256|384|512)[-:][A-Za-z0-9+/=_-]+$/;
|
|
14659
15805
|
INTERP_VAR_RE = new RegExp(`\\$\\{?\\s*${TOOL_INPUT_VAR}\\s*\\}?`);
|
|
14660
15806
|
INSIDE_SUBSHELL_RE = new RegExp(`\\$\\([^)]*\\$${TOOL_INPUT_VAR}\\b[^)]*\\)`);
|
|
14661
15807
|
INSIDE_BACKTICK_RE = new RegExp(`\`[^\`]*\\$${TOOL_INPUT_VAR}\\b[^\`]*\``);
|
|
@@ -15753,12 +16899,15 @@ function ephemeralHashFn() {
|
|
|
15753
16899
|
const key = ephemeralHmacKey;
|
|
15754
16900
|
return (value) => hmacHash(value, key);
|
|
15755
16901
|
}
|
|
15756
|
-
async function
|
|
16902
|
+
async function loadAnalysisContext(cwd) {
|
|
15757
16903
|
const config = await loadSecurityConfig(cwd);
|
|
16904
|
+
return { config, hashFn: await hashFnFor(cwd) };
|
|
16905
|
+
}
|
|
16906
|
+
async function detectAndResolve(cwd, input, context, opts) {
|
|
16907
|
+
const { config, hashFn } = context;
|
|
15758
16908
|
const original = await runDetectorsAsync(cwd, input.content, config);
|
|
15759
16909
|
const contextual = opts.filesystemSourceScan && input.source === "trusted-project" && input.path ? await filterSourceMatches(input.content, input.path, original) : { matches: original };
|
|
15760
16910
|
const matches = contextual.matches;
|
|
15761
|
-
const hashFn = await hashFnFor(cwd);
|
|
15762
16911
|
const buildOpts = {
|
|
15763
16912
|
source: input.source,
|
|
15764
16913
|
content: input.content,
|
|
@@ -15771,6 +16920,9 @@ async function analyze(cwd, input, opts = {}) {
|
|
|
15771
16920
|
buildOpts.path = input.path;
|
|
15772
16921
|
}
|
|
15773
16922
|
const decision = resolveDecision(config, { ...buildOpts, matches });
|
|
16923
|
+
return contextual.warning !== undefined ? { decision, warning: contextual.warning } : { decision };
|
|
16924
|
+
}
|
|
16925
|
+
async function runSelfProtection(cwd, config) {
|
|
15774
16926
|
const previous = await readState(cwd);
|
|
15775
16927
|
const selfProtection = evaluateSelfProtection(config, previous);
|
|
15776
16928
|
if (selfProtection.incidents.length > 0) {
|
|
@@ -15779,6 +16931,13 @@ async function analyze(cwd, input, opts = {}) {
|
|
|
15779
16931
|
if (!config.configUnreadable) {
|
|
15780
16932
|
await writeState(cwd, currentState(config));
|
|
15781
16933
|
}
|
|
16934
|
+
return { findings: selfProtection.findings, warnings: selfProtection.warnings };
|
|
16935
|
+
}
|
|
16936
|
+
async function analyze(cwd, input, opts = {}) {
|
|
16937
|
+
const context = await loadAnalysisContext(cwd);
|
|
16938
|
+
const { config } = context;
|
|
16939
|
+
const { decision, warning } = await detectAndResolve(cwd, input, context, opts);
|
|
16940
|
+
const selfProtection = await runSelfProtection(cwd, config);
|
|
15782
16941
|
if (selfProtection.findings.length > 0) {
|
|
15783
16942
|
decision.findings.push(...selfProtection.findings);
|
|
15784
16943
|
decision.gate = computeGate(decision.findings, config).gate;
|
|
@@ -15786,7 +16945,7 @@ async function analyze(cwd, input, opts = {}) {
|
|
|
15786
16945
|
}
|
|
15787
16946
|
return {
|
|
15788
16947
|
decision,
|
|
15789
|
-
warnings: [...selfProtection.warnings, ...
|
|
16948
|
+
warnings: [...selfProtection.warnings, ...warning !== undefined ? [warning] : []],
|
|
15790
16949
|
config
|
|
15791
16950
|
};
|
|
15792
16951
|
}
|
|
@@ -15825,32 +16984,50 @@ async function runScanPath(cwd, input) {
|
|
|
15825
16984
|
});
|
|
15826
16985
|
const findings = [];
|
|
15827
16986
|
const warnings = [];
|
|
15828
|
-
let config;
|
|
15829
16987
|
const files = traversal.files.map((file) => ({ ...file }));
|
|
16988
|
+
const markIncomplete = () => {
|
|
16989
|
+
traversal.coverage.status = "incomplete";
|
|
16990
|
+
if (!traversal.coverage.reasons.includes("security analysis unavailable")) {
|
|
16991
|
+
traversal.coverage.reasons.push("security analysis unavailable");
|
|
16992
|
+
}
|
|
16993
|
+
};
|
|
16994
|
+
const context = await loadAnalysisContext(cwd).catch(() => {
|
|
16995
|
+
return;
|
|
16996
|
+
});
|
|
16997
|
+
let analyzed = 0;
|
|
15830
16998
|
for (const item of traversal.contents) {
|
|
15831
16999
|
try {
|
|
15832
|
-
|
|
17000
|
+
if (context === undefined)
|
|
17001
|
+
throw new Error("security analysis unavailable");
|
|
17002
|
+
const result = await detectAndResolve(cwd, {
|
|
15833
17003
|
content: item.content,
|
|
15834
17004
|
source: input.source,
|
|
15835
17005
|
path: item.path,
|
|
15836
17006
|
...input.target !== undefined ? { target: input.target } : {}
|
|
15837
|
-
}, { filesystemSourceScan: true });
|
|
15838
|
-
|
|
17007
|
+
}, context, { filesystemSourceScan: true });
|
|
17008
|
+
analyzed += 1;
|
|
15839
17009
|
findings.push(...result.decision.findings);
|
|
15840
|
-
|
|
17010
|
+
if (result.warning !== undefined)
|
|
17011
|
+
warnings.push(result.warning);
|
|
15841
17012
|
} catch {
|
|
15842
17013
|
const file = files.find((entry) => entry.path === item.path && entry.status === "scanned");
|
|
15843
17014
|
if (file !== undefined) {
|
|
15844
17015
|
file.status = "failed";
|
|
15845
17016
|
file.reason = "security analysis unavailable";
|
|
15846
17017
|
}
|
|
15847
|
-
|
|
15848
|
-
|
|
15849
|
-
|
|
15850
|
-
|
|
17018
|
+
markIncomplete();
|
|
17019
|
+
}
|
|
17020
|
+
}
|
|
17021
|
+
const config = context?.config ?? await loadSecurityConfig(cwd);
|
|
17022
|
+
if (analyzed > 0) {
|
|
17023
|
+
try {
|
|
17024
|
+
const selfProtection = await runSelfProtection(cwd, config);
|
|
17025
|
+
findings.push(...selfProtection.findings);
|
|
17026
|
+
warnings.unshift(...selfProtection.warnings);
|
|
17027
|
+
} catch {
|
|
17028
|
+
markIncomplete();
|
|
15851
17029
|
}
|
|
15852
17030
|
}
|
|
15853
|
-
config ??= await loadSecurityConfig(cwd);
|
|
15854
17031
|
const computed = computeGate(findings, config);
|
|
15855
17032
|
const gate = computed.gate === "pass" && traversal.coverage.status === "incomplete" ? "incomplete" : computed.gate;
|
|
15856
17033
|
const action = strongestAction(findings.map((finding) => finding.action));
|
|
@@ -26350,6 +27527,7 @@ __export(exports_service9, {
|
|
|
26350
27527
|
hashVerifiedTaskOutcome: () => hashVerifiedTaskOutcome,
|
|
26351
27528
|
isExternalSlateStale: () => isExternalSlateStale,
|
|
26352
27529
|
isSlateSeedKind: () => isSlateSeedKind,
|
|
27530
|
+
isWorkspaceId: () => isWorkspaceId,
|
|
26353
27531
|
listWorkspaceViews: () => listWorkspaceViews,
|
|
26354
27532
|
localWorkspaceAuthorizationServer: () => localWorkspaceAuthorizationServer,
|
|
26355
27533
|
lookupWorkspace: () => lookupWorkspace,
|
|
@@ -26359,6 +27537,7 @@ __export(exports_service9, {
|
|
|
26359
27537
|
normalizeProposalLifecycleResult: () => normalizeProposalLifecycleResult,
|
|
26360
27538
|
normalizeWorkspaceReference: () => normalizeWorkspaceReference,
|
|
26361
27539
|
proposalNotePath: () => proposalNotePath,
|
|
27540
|
+
proposeFromSession: () => proposeFromSession,
|
|
26362
27541
|
readExternalSlate: () => readExternalSlate,
|
|
26363
27542
|
reclaimStaleExternalSlates: () => reclaimStaleExternalSlates,
|
|
26364
27543
|
redactSensitiveText: () => redactSensitiveText,
|
|
@@ -42670,7 +43849,7 @@ class WorkspaceService {
|
|
|
42670
43849
|
}
|
|
42671
43850
|
}
|
|
42672
43851
|
async readManifest(id) {
|
|
42673
|
-
if (
|
|
43852
|
+
if (!isWorkspaceId(id))
|
|
42674
43853
|
throw new WorkspaceServiceError("not_found", "workspace not found");
|
|
42675
43854
|
let parsed;
|
|
42676
43855
|
try {
|
|
@@ -42708,6 +43887,9 @@ function currentRoleOrRevoked(manifest, subject) {
|
|
|
42708
43887
|
function localWorkspaceAuthorizationServer(subject = `user:local-${process.getuid?.() ?? process.pid}`) {
|
|
42709
43888
|
return createSacAuthorizationServer({ authenticateRequest: async () => ({ subject, authenticationMethod: "local-os", roleRevision: "local-os-v1" }) });
|
|
42710
43889
|
}
|
|
43890
|
+
function isWorkspaceId(id) {
|
|
43891
|
+
return /^[a-z][a-z0-9-]{2,63}$/.test(id);
|
|
43892
|
+
}
|
|
42711
43893
|
function newWorkspaceId() {
|
|
42712
43894
|
return `workspace-${randomUUID4().replace(/-/g, "").slice(0, 16)}`;
|
|
42713
43895
|
}
|
|
@@ -45676,9 +46858,44 @@ function createLocalFwkReadService(cwd, opts) {
|
|
|
45676
46858
|
function normalizeFwkResult(result) {
|
|
45677
46859
|
return JSON.parse(JSON.stringify(result));
|
|
45678
46860
|
}
|
|
46861
|
+
// src/sac/propose-from-session.ts
|
|
46862
|
+
init_guard();
|
|
46863
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
46864
|
+
import { writeFile as writeFile25 } from "fs/promises";
|
|
46865
|
+
async function proposeFromSession(input) {
|
|
46866
|
+
const { cwd, workspaceId, kind, sessionRef } = input;
|
|
46867
|
+
const session = findSession(cwd, sessionRef);
|
|
46868
|
+
if (!session)
|
|
46869
|
+
return { ok: false, reason: "session-not-found", message: `no session matching "${sessionRef}" in this project` };
|
|
46870
|
+
const proposalId = `proposal-${randomUUID10().replace(/-/g, "").slice(0, 16)}`;
|
|
46871
|
+
const notePath = proposalNotePath(cwd, workspaceId, proposalId);
|
|
46872
|
+
const materializedNote = input.note === undefined ? undefined : prepareOutputForPersistence(await guardOutput({ cwd, content: input.note, target: "task", source: "generated", path: notePath }), input.note);
|
|
46873
|
+
if (materializedNote !== undefined && !materializedNote.allowed) {
|
|
46874
|
+
return { ok: false, reason: "note-refused", message: materializedNote.reason };
|
|
46875
|
+
}
|
|
46876
|
+
const persistedNote = materializedNote?.allowed ? materializedNote.content : undefined;
|
|
46877
|
+
const { service, wrapUpAuthority, authorizationServer } = createHarnessProposalLifecycleService(cwd, { workspaceId, ...persistedNote ? { note: persistedNote } : {} });
|
|
46878
|
+
const requestCorrelationId = randomUUID10();
|
|
46879
|
+
const actor = await authorizationServer.actorContextFor(undefined, requestCorrelationId);
|
|
46880
|
+
if (!actor)
|
|
46881
|
+
return { ok: false, reason: "no-actor", message: "trusted ActorContext is required" };
|
|
46882
|
+
const wrapUp = await wrapUpAuthority.issue({ actor, source: "session", sourceRef: sessionEvidenceRef(workspaceId, session.id) });
|
|
46883
|
+
const proposal = await service.create({
|
|
46884
|
+
request: undefined,
|
|
46885
|
+
requestCorrelationId,
|
|
46886
|
+
workspaceId,
|
|
46887
|
+
id: proposalId,
|
|
46888
|
+
proposalRevision: input.proposalRevision ?? "1",
|
|
46889
|
+
kind,
|
|
46890
|
+
wrapUp
|
|
46891
|
+
});
|
|
46892
|
+
if (persistedNote)
|
|
46893
|
+
await writeFile25(notePath, persistedNote, "utf8");
|
|
46894
|
+
return { ok: true, result: normalizeProposalLifecycleResult(proposal) };
|
|
46895
|
+
}
|
|
45679
46896
|
// src/sac/collaboration-service.ts
|
|
45680
46897
|
import { appendFile as appendFile9, mkdir as mkdir37, readFile as readFile75 } from "fs/promises";
|
|
45681
|
-
import { randomUUID as
|
|
46898
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
45682
46899
|
import path145 from "path";
|
|
45683
46900
|
class CollaborationServiceError extends Error {
|
|
45684
46901
|
code;
|
|
@@ -45725,7 +46942,7 @@ class CollaborationService {
|
|
|
45725
46942
|
const activity = this.validate({
|
|
45726
46943
|
...supplied,
|
|
45727
46944
|
schemaVersion: "1.0",
|
|
45728
|
-
id: `activity-${
|
|
46945
|
+
id: `activity-${randomUUID11()}`,
|
|
45729
46946
|
workspaceId: manifest.id,
|
|
45730
46947
|
actorSubject: actor.subject,
|
|
45731
46948
|
occurredAt: (this.input.now ?? (() => new Date))().toISOString(),
|
|
@@ -45786,13 +47003,13 @@ import path147 from "path";
|
|
|
45786
47003
|
// src/sac/machine-wrap-up.ts
|
|
45787
47004
|
init_fs();
|
|
45788
47005
|
init_slate();
|
|
45789
|
-
import { createHash as createHash36, randomUUID as
|
|
47006
|
+
import { createHash as createHash36, randomUUID as randomUUID13 } from "crypto";
|
|
45790
47007
|
import { mkdir as mkdir38 } from "fs/promises";
|
|
45791
47008
|
import path146 from "path";
|
|
45792
47009
|
|
|
45793
47010
|
// src/sac/workspace-resolve.ts
|
|
45794
47011
|
init_redact();
|
|
45795
|
-
import { randomUUID as
|
|
47012
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
45796
47013
|
var DEFAULT_MODEL_TURN_TIMEOUT_MS = 15000;
|
|
45797
47014
|
var TOPIC_HINT_MAX_LENGTH = 200;
|
|
45798
47015
|
function workspaceStore(cwd) {
|
|
@@ -45820,7 +47037,7 @@ async function resolveOrCreateWorkspace(input) {
|
|
|
45820
47037
|
const store = workspaceStore(input.cwd);
|
|
45821
47038
|
let existing;
|
|
45822
47039
|
try {
|
|
45823
|
-
const listed = await store.list({ request: undefined, requestCorrelationId:
|
|
47040
|
+
const listed = await store.list({ request: undefined, requestCorrelationId: randomUUID12(), includeArchived: false });
|
|
45824
47041
|
existing = listed.filter((w) => w.status === "active").map((w) => ({ id: w.id, title: w.title, status: w.status }));
|
|
45825
47042
|
} catch {
|
|
45826
47043
|
return { ok: false, reason: "error" };
|
|
@@ -45886,7 +47103,7 @@ async function createWorkspace(store, title) {
|
|
|
45886
47103
|
try {
|
|
45887
47104
|
const workspace = await store.create({
|
|
45888
47105
|
request: undefined,
|
|
45889
|
-
requestCorrelationId:
|
|
47106
|
+
requestCorrelationId: randomUUID12(),
|
|
45890
47107
|
id: newWorkspaceId(),
|
|
45891
47108
|
title
|
|
45892
47109
|
});
|
|
@@ -46121,7 +47338,7 @@ async function proposeOneGroup(params) {
|
|
|
46121
47338
|
workspaceId: params.workspaceId,
|
|
46122
47339
|
now: params.now
|
|
46123
47340
|
});
|
|
46124
|
-
const requestCorrelationId =
|
|
47341
|
+
const requestCorrelationId = randomUUID13();
|
|
46125
47342
|
const actor = await authorizationServer.actorContextFor(undefined, requestCorrelationId);
|
|
46126
47343
|
if (!actor)
|
|
46127
47344
|
throw new Error("trusted ActorContext is required for a machine wrap-up propose");
|
|
@@ -47360,6 +48577,12 @@ var HELP_GROUPS = [
|
|
|
47360
48577
|
group: "External agents, ACP and MCP",
|
|
47361
48578
|
summary: "Install, audit and uninstall Keryx's hooks and instructions in another coding agent (see also /integrate, unrelated)."
|
|
47362
48579
|
},
|
|
48580
|
+
{
|
|
48581
|
+
kind: "cli",
|
|
48582
|
+
name: "herdr",
|
|
48583
|
+
group: "External agents, ACP and MCP",
|
|
48584
|
+
summary: "Register Keryx with the Herdr desktop app and herdr-core (opt-in install, remove, status)."
|
|
48585
|
+
},
|
|
47363
48586
|
{
|
|
47364
48587
|
kind: "cli",
|
|
47365
48588
|
name: "serve-mcp",
|