agent-sanitizer 2.62.2 → 2.63.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sanitizer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.63.0",
|
|
4
4
|
"description": "Defend an agent against hidden-content injection: strip payload-capable invisible Unicode and ANSI, splice out human-invisible HTML, and flag data-exfil URLs in untrusted text before any model sees it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "The credential-noun vocabulary: the words that make an identifier name a secret. Published so every consumer derives its own matcher from ONE list — a newly recognized noun reaches them all through a version bump instead of N hand edits. Read it from Python via agent_sanitizer.secrets (credential_name_segments / credential_field_name_patterns / non_secret_name_segments) or from JavaScript via the npm subpath export `agent-sanitizer/credential-names`. `parts` are the lowercase words of the noun; a consumer renders them for its own matcher (underscore-joined `API_KEY` and bare-joined `APIKEY` for an env-var NAME, `api[_-]?key` for a `field = value` regex). `uses` says which matcher may use the noun, because the two are not interchangeable: `env-name` matches a variable NAME and never inspects a value, so a broad noun there costs nothing, while `field-value` redacts whatever follows `noun = ` and a broad noun there mangles ordinary text — `key = <20 chars>` is a false-positive flood, so `key`, `pat`, `credential`, `credentials` and `secrets` are env-name only — `credential`/`credentials` are everyday variable names, and an attribute chain (`credentials = service_account.Credentials.from_service_account_info(info)`) escapes every value-shape skip, so a field-value rendering would redact ordinary source lines. `passphrase` is the exception among them: it is as shape-specific as `password`, and detect-secrets' own KeywordDetector DENYLIST omits it, so name-only would let `passphrase = <secret>` reach the model in cleartext. `nonSecretSuffixes` are the trailing words that make a credential-shaped name hold a NON-secret
|
|
2
|
+
"$comment": "The credential-noun vocabulary: the words that make an identifier name a secret. Published so every consumer derives its own matcher from ONE list — a newly recognized noun reaches them all through a version bump instead of N hand edits. Read it from Python via agent_sanitizer.secrets (credential_name_segments / credential_field_name_patterns / non_secret_name_segments) or from JavaScript via the npm subpath export `agent-sanitizer/credential-names`. `parts` are the lowercase words of the noun; a consumer renders them for its own matcher (underscore-joined `API_KEY` and bare-joined `APIKEY` for an env-var NAME, `api[_-]?key` for a `field = value` regex). `uses` says which matcher may use the noun, because the two are not interchangeable: `env-name` matches a variable NAME and never inspects a value, so a broad noun there costs nothing, while `field-value` redacts whatever follows `noun = ` and a broad noun there mangles ordinary text — `key = <20 chars>` is a false-positive flood, so `key`, `pat`, `credential`, `credentials` and `secrets` are env-name only — `credential`/`credentials` are everyday variable names, and an attribute chain (`credentials = service_account.Credentials.from_service_account_info(info)`) escapes every value-shape skip, so a field-value rendering would redact ordinary source lines. `passphrase` is the exception among them: it is as shape-specific as `password`, and detect-secrets' own KeywordDetector DENYLIST omits it, so name-only would let `passphrase = <secret>` reach the model in cleartext. `nonSecretSuffixes` are the trailing words that make a credential-shaped name hold a NON-secret — a key's identifier, the public half of a keypair, or metadata ABOUT a secret (its type, display name, label, kind, or the path/file it lives at) — which a consumer must not redact. A bare `id` is deliberately absent: Vault's `secret_id` is itself a credential. Every part is restricted to a-z0-9 so it carries no regex metacharacter; the accessors enforce that and fail closed on a violation, an empty list, or an unknown `uses` value. It sits inside the Python package because a wheel can only ship data under its package directory, while npm's `files`/`exports` can name any path — so ONE physical file backs both ecosystems and there is no copy to drift.",
|
|
3
3
|
"nouns": [
|
|
4
4
|
{ "parts": ["api", "key"], "uses": ["env-name", "field-value"] },
|
|
5
5
|
{ "parts": ["access", "key"], "uses": ["env-name", "field-value"] },
|
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
],
|
|
25
25
|
"nonSecretSuffixes": [
|
|
26
26
|
["key", "id"],
|
|
27
|
-
["public", "key"]
|
|
27
|
+
["public", "key"],
|
|
28
|
+
["type"],
|
|
29
|
+
["name"],
|
|
30
|
+
["label"],
|
|
31
|
+
["keyword"],
|
|
32
|
+
["kind"],
|
|
33
|
+
["path"],
|
|
34
|
+
["file"]
|
|
28
35
|
]
|
|
29
36
|
}
|
package/src/credential-names.mjs
CHANGED
|
@@ -178,7 +178,8 @@ export function credentialNames() {
|
|
|
178
178
|
* See this module's header for why that choice belongs to the caller.
|
|
179
179
|
*
|
|
180
180
|
* `declineNonSecret` applies the vocabulary's `nonSecretSuffixes`: a name ending
|
|
181
|
-
* in one holds an identifier
|
|
181
|
+
* in one holds an identifier, a public key, or metadata about a secret
|
|
182
|
+
* (`AWS_ACCESS_KEY_ID`, `SECRET_TYPE`, `TOKEN_FILE`), not the secret itself.
|
|
182
183
|
* Leave it on for a redactor, where redacting an identifier out of output is a
|
|
183
184
|
* visible defect; turn it off for a scrub whose failure to strip is the worse
|
|
184
185
|
* error. It is applied to the name's trailing run under both scopes, because a
|
|
@@ -198,26 +199,29 @@ export function credentialNameMatcher(options = {}) {
|
|
|
198
199
|
const vocabulary = spec ? parseCredentialNames(spec) : credentialNames();
|
|
199
200
|
const nouns = new Set(vocabulary.segments);
|
|
200
201
|
const nonSecret = new Set(vocabulary.nonSecretSegments);
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
202
|
+
// Each walk is bounded by the longest entry of the set it probes: a run longer
|
|
203
|
+
// than that matches nothing there, and the bound is what keeps the walk linear
|
|
204
|
+
// in the name's segment count. The bounds are separate because a two-word
|
|
205
|
+
// non-secret suffix (`PUBLIC_KEY`) must still be seen when every noun is one word.
|
|
206
|
+
/** @param {Iterable<string>} segments */
|
|
207
|
+
const longestRun = (segments) =>
|
|
208
|
+
Math.max(...Array.from(segments, (s) => s.split("_").length));
|
|
209
|
+
const maxRun = longestRun(nouns);
|
|
210
|
+
const maxNonSecretRun = longestRun(nonSecret);
|
|
211
|
+
/** @param {string[]} words @param {number} bound @returns {string[]} */
|
|
212
|
+
const trailingRuns = (words, bound) =>
|
|
213
|
+
Array.from({ length: Math.min(bound, words.length) }, (_, i) =>
|
|
210
214
|
words.slice(words.length - (i + 1)).join("_"),
|
|
211
215
|
);
|
|
212
216
|
return (name) => {
|
|
213
217
|
const words = name.toUpperCase().split("_");
|
|
214
218
|
if (
|
|
215
219
|
declineNonSecret &&
|
|
216
|
-
trailingRuns(words).some((run) => nonSecret.has(run))
|
|
220
|
+
trailingRuns(words, maxNonSecretRun).some((run) => nonSecret.has(run))
|
|
217
221
|
)
|
|
218
222
|
return false;
|
|
219
223
|
if (scope === "trailing")
|
|
220
|
-
return trailingRuns(words).some((run) => nouns.has(run));
|
|
224
|
+
return trailingRuns(words, maxRun).some((run) => nouns.has(run));
|
|
221
225
|
for (let start = 0; start < words.length; start++)
|
|
222
226
|
for (let span = 1; span <= maxRun && start + span <= words.length; span++)
|
|
223
227
|
if (nouns.has(words.slice(start, start + span).join("_"))) return true;
|
|
@@ -24,7 +24,8 @@ export function credentialNames(): CredentialNames;
|
|
|
24
24
|
* See this module's header for why that choice belongs to the caller.
|
|
25
25
|
*
|
|
26
26
|
* `declineNonSecret` applies the vocabulary's `nonSecretSuffixes`: a name ending
|
|
27
|
-
* in one holds an identifier
|
|
27
|
+
* in one holds an identifier, a public key, or metadata about a secret
|
|
28
|
+
* (`AWS_ACCESS_KEY_ID`, `SECRET_TYPE`, `TOKEN_FILE`), not the secret itself.
|
|
28
29
|
* Leave it on for a redactor, where redacting an identifier out of output is a
|
|
29
30
|
* visible defect; turn it off for a scrub whose failure to strip is the worse
|
|
30
31
|
* error. It is applied to the name's trailing run under both scopes, because a
|