@blamejs/core 0.18.46 → 0.18.48
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/CHANGELOG.md +40 -0
- package/lib/codepoint-class.js +31 -3
- package/lib/gate-contract.js +256 -2
- package/lib/guard-archive.js +18 -0
- package/lib/guard-auth.js +28 -0
- package/lib/guard-cidr.js +19 -0
- package/lib/guard-country.js +4 -4
- package/lib/guard-csv.js +31 -2
- package/lib/guard-domain.js +22 -0
- package/lib/guard-email.js +18 -0
- package/lib/guard-filename.js +46 -11
- package/lib/guard-graphql.js +16 -0
- package/lib/guard-html.js +21 -0
- package/lib/guard-image.js +19 -0
- package/lib/guard-json.js +24 -0
- package/lib/guard-jsonpath.js +14 -1
- package/lib/guard-jwt.js +20 -2
- package/lib/guard-markdown.js +26 -0
- package/lib/guard-mime.js +15 -5
- package/lib/guard-oauth.js +23 -0
- package/lib/guard-pdf.js +19 -1
- package/lib/guard-regex.js +16 -1
- package/lib/guard-shell.js +14 -1
- package/lib/guard-svg.js +18 -0
- package/lib/guard-template.js +14 -1
- package/lib/guard-text.js +21 -1
- package/lib/guard-time.js +20 -4
- package/lib/guard-uuid.js +28 -4
- package/lib/guard-xml.js +20 -0
- package/lib/guard-yaml.js +29 -2
- package/lib/network-dns.js +25 -12
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,46 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.18.x
|
|
10
10
|
|
|
11
|
+
- v0.18.48 (2026-08-22) — **A guard option you set and a guard option that exists were not the same set.** Every `*Policy` option across the guard family now names the values it takes, and refuses everything else where options are resolved rather than at the first request that trips the scan. Holding the options to their vocabularies surfaced four places where what a guard advertised and what it did had come apart: an option documented under a name nothing read, a value that disabled a critical check without appearing in any opts block, two policies whose settings the gate's floor overrode anyway, and one declared in all three profiles and read nowhere. **Fixed:** *A guard policy refuses a value outside its vocabulary* — The two hundred and sixty-two `*Policy` options across the guard family each declare the values they accept, checked where options are resolved — boot, not the first hostile input — and on every entry point, including the `gate()` and `validate()` of guards that bind their own resolver.
|
|
12
|
+
|
|
13
|
+
The failure it ends is quiet, because policies are read leniently. `duplicateKeyPolicy: "rejct"` is not `allow`, so the check still runs; it is not `reject` either, so the finding drops from critical to a warning. The operator asked to refuse a duplicate key and silently got an audit line. Nothing was let through that a stricter reading would have caught, so this was never a hole — but a setting that means something other than what it says is worth a restart to learn about.
|
|
14
|
+
|
|
15
|
+
`b.gateContract.policyVocabulary(names, values, overrides?)` builds these tables: most of a guard's policies answer the same question — refuse the construct, record it, or let it through — and one or two do not. Written out per option instead, a family of nine ends up with eight that accept `audit-only` and one that does not. Each guard also exports its `POLICY_VOCABULARY`, so a caller can ask what an option takes instead of reading the source. · *An option name the guard does not have is refused rather than ignored* — A `*Policy` key that no guard option matches was kept by the option merge, read by nothing, and left the profile default in force. `b.guardMime` had shipped that way: its opts block named `riskyTypesPolicy` while the code reads `riskyTypePolicy`, so an operator following the documentation and setting it to `reject` kept the balanced profile's `audit` and had risky content types reported rather than refused, with nothing to see but the documentation that was wrong to begin with.
|
|
16
|
+
|
|
17
|
+
A guard's policy set is fully described by its profiles, postures and defaults, so a near-miss is decidable and now refused by name. The rest of the option surface stays open — callbacks, caches and host wiring are not enumerable in advance.
|
|
18
|
+
|
|
19
|
+
The two statements a guard makes about an option — the vocabulary it enforces and the opts block an operator reads — are now compared to each other, which is what surfaced this one. · *`kidTraversalPolicy: "allow"` turned off JWT key-path traversal detection* — `b.guardJwt` documents the `kid` traversal check as unconditional, and the header field it inspects is the one that steers key lookup — a `kid` of `../../etc/passwd` reaching a file-backed resolver is the point of the check. The code consulted `kidTraversalPolicy` for an `allow` its opts block never offered, and that value skipped the check entirely.
|
|
20
|
+
|
|
21
|
+
The check no longer reads a policy, and the option accepts `reject` alone. `algNonePolicy` is likewise fixed at `reject`: `alg=none` was already refused unconditionally and the option had never been read. · *Two guardFilename policies were settings the floor already overrode* — `traversalPolicy` and `nullBytePolicy` accepted `allow` and `audit`, and both were overridden by the refusal floor the gate applies to a leaf filename. An operator setting either believed they had turned a class of finding down; the refusal stood regardless, and the two disagreed in the audit record rather than in the verdict. Both now accept `reject` alone, so the floor is visible in what the guard will take rather than only in what it does.
|
|
22
|
+
|
|
23
|
+
`adsPolicy` keeps `allow` for the one place that honours it — `verifyExtractionPath`, for an operator deliberately extracting stream-suffixed names — and no longer accepts an `audit` that did nothing. · *`svgzPolicy` was declared in every profile and read nowhere* — A gzip-compressed SVG is refused on its byte signature before any parse, and `b.guardSvg.sanitize` throws on one outright. `svgzPolicy` sat in all three profiles without being consulted, so an operator writing `svgzPolicy: "allow"` got a silent no-op and a reasonable belief that SVGZ now passed. The option takes `reject` alone, which is what the guard has always done. · *Three policies do not take `audit-only`, because their code reads the literal `audit`* — `audit-only` is the family's documented synonym for `audit`, and most policies treat them identically because the disposition mapper asks one predicate. Three conditions test for the literal spelling instead, and there the two words took different branches with nothing in the option's name to say which you had picked: `pkcePolicy: "audit-only"` refused every OAuth flow missing a PKCE challenge while `"audit"` served it with a finding; `pollutionPolicy: "audit-only"` removed `__proto__` from the parsed object while `"audit"` preserved it, a difference in the data rather than in a finding; and `pathSeparatorsPolicy: "audit-only"` failed a path-shaped name that `"audit"` accepted.
|
|
24
|
+
|
|
25
|
+
`b.guardOauth.pkcePolicy`, `b.guardJson.pollutionPolicy` and `b.guardFilename.pathSeparatorsPolicy` now refuse `audit-only` rather than accepting it and meaning something else by it — as `b.guardArchive.nestedArchivePolicy`, `b.guardDomain.trailingDotPolicy` and `b.guardYaml.tagPolicy` already did, for the same reason. Write `audit` on those six.
|
|
26
|
+
|
|
27
|
+
A detector holds the pairing: a policy compared against the literal `"audit"` may not also advertise the synonym, so the next one is a failing gate rather than two spellings that quietly disagree. · *Which guards refuse `strip` on a character policy, in full* — 0.18.47 made repair capability a declaration rather than something inferred from the presence of a `sanitize`, and named four guards the inference had been wrong about. Fourteen refuse `strip` on the shared character policies; the list is here because the boot error lands on a caller who had a working configuration, and four names is not the set.
|
|
28
|
+
|
|
29
|
+
Refuse it: `b.guardAuth`, `b.guardCidr`, `b.guardCountry`, `b.guardDomain`, `b.guardGraphql`, `b.guardJsonpath`, `b.guardJwt`, `b.guardMime`, `b.guardOauth`, `b.guardRegex`, `b.guardShell`, `b.guardTemplate`, `b.guardTime`, `b.guardUuid`. Accept it: `b.guardEmail`, `b.guardFilename`, `b.guardHtml`, `b.guardJson`, `b.guardMarkdown`, `b.guardSvg`, `b.guardText`, `b.guardXml`, `b.guardYaml`.
|
|
30
|
+
|
|
31
|
+
All fourteen refuse the character rather than removing it. Feed `b.guardUuid.sanitize` an identifier with a zero-width character in it and it throws `uuid.zero-width`, because a UUID with the character removed is a different identifier — and the same holds for a CIDR block, a country code, a domain, a JSONPath, a media type, a timestamp, a GraphQL document, an OAuth flow and an auth bundle. There is no repaired form to route a `strip` to.
|
|
32
|
+
|
|
33
|
+
`b.guardAuth`, `b.guardOauth` and `b.guardGraphql` are the clearest case for making capability a declaration rather than an inference: each carries a `sanitizeTransform` that is the identity function, because an auth bundle and an OAuth flow are composed of values the framework must not rewrite — forging a JWT `alg` or editing a `state` parameter would disarm the token the guard exists to catch. Inferring "can repair" from the presence of a `sanitize` read all three as repairing.
|
|
34
|
+
|
|
35
|
+
`b.guardCsv`, `b.guardImage` and `b.guardPdf` expose no shared character policy at all. guardCsv repairs characters through its own `bidiCharPolicy`, `controlCharPolicy` and `homoglyphPolicy`, and those still take `strip`. · *Twenty opts-block lines offered a repair the guard cannot perform* — `b.guardUuid`, `b.guardCountry`, `b.guardTime`, `b.guardMime`, `b.guardShell`, `b.guardRegex`, `b.guardJsonpath` and `b.guardTemplate` documented `strip` on their character policies. None of them repairs a character — a UUID with a zero-width character in it is not a UUID with the character removed, it is a different identifier — so the value was refused at construction while the documentation offered it. The lines now name what the guards accept.
|
|
36
|
+
|
|
37
|
+
- v0.18.47 (2026-08-22) — **A misspelled character policy was accepted at boot and quietly meant something else.** `bidiPolicy: "rejct"` was accepted by every door the framework offers — the guard's own option resolver and the gate built from it — and then read leniently at runtime: the scan runs because the value is not `allow`, an issue is raised, and the disposition falls through to refuse. Nothing is let through, so it was never a hole; the operator simply asked for one disposition, got another, and was told nothing at the moment a boot error would have cost them a restart. Ninety-three settings across the guard family behaved that way. **Fixed:** *A character policy refuses a value outside its vocabulary* — The five character-threat policies every guard shares — `bidiPolicy`, `nullBytePolicy`, `controlPolicy`, `zeroWidthPolicy`, `tagsPolicy` — now accept only `allow`, `audit`, `audit-only`, `reject`, and `strip` where the guard can carry a repair out. Anything else is refused where options are resolved, which is boot, not the first request that trips the scan. Every entry point takes the check: the generated resolver, and the `gate()` and `validate()` of the guards that bind their own.
|
|
38
|
+
|
|
39
|
+
The check is derived from each guard's own declared defaults rather than hand-listed per guard, for the same reason the size caps beside it are. Hand-listing is what produced the gap: `b.guardCountry` added this for its own three options after `{ reservedPolicy: "rejcet" }` served a reserved country code and reported success, and the other two hundred and fifty-nine policy options across the family never followed. A guard that declares its own vocabulary explicitly still wins, so that one is unchanged.
|
|
40
|
+
|
|
41
|
+
Whether a guard can perform a repair is now DECLARED rather than inferred from whether it exports a `sanitize`. An identifier guard exports one that validates and throws — a UUID with a zero-width character in it is not a UUID, and there is nothing to repair — so inferring the capability let `b.guardRegex`, `b.guardJwt`, `b.guardShell` and `b.guardTemplate` accept `strip` at construction and then refuse at runtime, which is the same silent substitution. Guards that repair say so, and the claim is held from both ends: `b.gateContract.defineGuard` refuses a spec that declares repair without a `sanitize` — or a `sanitizeTransform` for one to be built from — and the family sweep separately checks that a guard claiming it actually removes the character, while one that does not claim it refuses the instruction.
|
|
42
|
+
|
|
43
|
+
What this does not yet cover: the guard-specific policies — formula injection, homoglyphs, DOCTYPE, aliases, and the rest — still accept anything. Their vocabularies genuinely differ from one another, and the set a profile happens to select is not the set the code accepts, so inferring them would refuse values that work today. Each needs its own list, read from the guard rather than guessed. · *`audit-only` did not mean what `audit` means* — The two have been synonyms wherever a policy is turned into a disposition. The severity calculation recognised only the literal `audit`, so on the public validation path they disagreed: `zeroWidthPolicy: "audit"` returned a pass with a warning, while `audit-only` returned a failure with a high-severity issue. The same split affected the control-character and Unicode Tags policies.
|
|
44
|
+
|
|
45
|
+
One predicate decides it now, in the module that owns the character scanning, and the disposition mapper asks the same one. Two spellings of a single setting is how they come to disagree; asking one question is how they stop. · *The DNSSEC algorithm table called RSASHA512 current* — RFC 9904 replaced RFC 8624 as the DNSSEC algorithm requirements, and it reads RSASHA512 as NOT RECOMMENDED for signing while still MUST for validation. `b.network.dns.classifyDnskeyAlgorithm(10)` reported it as `current`, so an operator asking which algorithm to sign a zone with got the opposite of what the current document says.
|
|
46
|
+
|
|
47
|
+
It is not marked deprecated, and deliberately: validation remains mandatory, and a caller using that flag to decide whether to ACCEPT a zone must keep accepting it. One boolean cannot carry a requirement that differs between signing and validation, so the reason names both levels and the flag follows the side an operator can still choose.
|
|
48
|
+
|
|
49
|
+
The citations moved with it — DSA, DSA-NSEC3-SHA1 and ECC-GOST to RFC 9904's Table 2, the DS digest vocabulary and GOST R 34.11-94 to Table 3, and RSAMD5 off RFC 6944, which had been obsoleted twice over, onto RFC 6725. Nothing asserted on algorithm 10 before, which is how a stale classification sat in a table everything else in the file kept current.
|
|
50
|
+
|
|
11
51
|
- v0.18.46 (2026-08-22) — **A failing test reported the handle it leaked instead of the check that failed.** Thirty test files ran their assertions inside a `try` whose `finally` drained open handles. A throw from a `finally` replaces the error the body raised, so when a test failed and its skipped teardown left a socket behind, what surfaced was "a handle leaked" — the consequence — while the check that actually failed was discarded. That is the shape of failure that reads as an unexplained flake, and it was reachable in every suite that opens a server. **Fixed:** *A drain no longer replaces the failure it followed* — `try { ...tests... } finally { await drain() }` reads as correct. It is not. When the body throws, every teardown after the throw is skipped, so the drain finds the servers those teardowns would have closed and throws too — and a throw from a `finally` replaces the body's error. The run reports a leaked handle, which is true and useless: the check that failed is gone, and the leak looks like the bug.
|
|
12
52
|
|
|
13
53
|
The drain now runs through one helper that keeps the body's error and appends the others to it, so a run that failed for a reason reports that reason, and a run that only leaked still reports the leak. Thirty files moved onto it.
|
package/lib/codepoint-class.js
CHANGED
|
@@ -207,6 +207,33 @@ function firstInRanges(text, ranges, from) {
|
|
|
207
207
|
// The shape the detectors want: `{ index, char, codePoint }` for the first
|
|
208
208
|
// codepoint of `text` in `ranges`, or null. `char` is the WHOLE codepoint,
|
|
209
209
|
// which for an astral hit is a surrogate pair.
|
|
210
|
+
/**
|
|
211
|
+
* @primitive b.codepointClass.isAuditPolicy
|
|
212
|
+
* @signature b.codepointClass.isAuditPolicy(policy)
|
|
213
|
+
* @since 0.18.47
|
|
214
|
+
* @status stable
|
|
215
|
+
* @related b.gateContract.policyDisposition, b.gateContract.charPolicyEnums
|
|
216
|
+
*
|
|
217
|
+
* Whether a policy value asks for an AUDIT — reporting the finding rather than
|
|
218
|
+
* refusing or repairing. `audit-only` is the framework's own synonym for
|
|
219
|
+
* `audit`, and this is the one place that decides so.
|
|
220
|
+
*
|
|
221
|
+
* It exists because the two spellings disagreed. The disposition mapper treated
|
|
222
|
+
* them as the same thing while the severity calculation recognised only the
|
|
223
|
+
* literal `audit`, so `zeroWidthPolicy: "audit-only"` produced a high-severity
|
|
224
|
+
* issue and a failed validation where `audit` produced a warning and a pass.
|
|
225
|
+
* Two spellings of one setting is how they come to disagree; asking one
|
|
226
|
+
* predicate is how they stop.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* b.codepointClass.isAuditPolicy("audit"); // → true
|
|
230
|
+
* b.codepointClass.isAuditPolicy("audit-only"); // → true
|
|
231
|
+
* b.codepointClass.isAuditPolicy("reject"); // → false
|
|
232
|
+
*/
|
|
233
|
+
function isAuditPolicy(policy) {
|
|
234
|
+
return policy === "audit" || policy === "audit-only";
|
|
235
|
+
}
|
|
236
|
+
|
|
210
237
|
function _firstHit(text, ranges) {
|
|
211
238
|
var i = firstInRanges(text, ranges);
|
|
212
239
|
if (i === -1) return null;
|
|
@@ -817,7 +844,7 @@ function detectCharThreats(text, opts, codePrefix) {
|
|
|
817
844
|
// reported, so the audit setting did nothing an operator could
|
|
818
845
|
// observe except the thing they asked it not to do.
|
|
819
846
|
severity: opts.controlPolicy === "reject" ? "high"
|
|
820
|
-
: opts.controlPolicy
|
|
847
|
+
: isAuditPolicy(opts.controlPolicy) ? "warn" : "high",
|
|
821
848
|
ruleId: codePrefix + ".control",
|
|
822
849
|
location: ctrlMatch.index,
|
|
823
850
|
// DEL is a control character here but is not in the C0 block, so the
|
|
@@ -849,7 +876,7 @@ function detectCharThreats(text, opts, codePrefix) {
|
|
|
849
876
|
issues.push({
|
|
850
877
|
kind: "zero-width",
|
|
851
878
|
severity: opts.zeroWidthPolicy === "reject" ? "critical"
|
|
852
|
-
: opts.zeroWidthPolicy
|
|
879
|
+
: isAuditPolicy(opts.zeroWidthPolicy) ? "warn" : "high",
|
|
853
880
|
ruleId: codePrefix + ".zero-width",
|
|
854
881
|
location: zwMatch.index,
|
|
855
882
|
snippet: "zero-width / invisible-formatting char U+" +
|
|
@@ -877,7 +904,7 @@ function detectCharThreats(text, opts, codePrefix) {
|
|
|
877
904
|
issues.push({
|
|
878
905
|
kind: "unicode-tags",
|
|
879
906
|
severity: tagsPolicy === "reject" ? "critical"
|
|
880
|
-
: tagsPolicy
|
|
907
|
+
: isAuditPolicy(tagsPolicy) ? "warn" : "high",
|
|
881
908
|
ruleId: codePrefix + ".unicode-tags",
|
|
882
909
|
location: tagMatch.index,
|
|
883
910
|
snippet: "Unicode Tags block char U+" +
|
|
@@ -1724,6 +1751,7 @@ module.exports = {
|
|
|
1724
1751
|
hex4: hex4,
|
|
1725
1752
|
charClass: charClass,
|
|
1726
1753
|
inRanges: inRanges,
|
|
1754
|
+
isAuditPolicy: isAuditPolicy,
|
|
1727
1755
|
firstInRanges: firstInRanges,
|
|
1728
1756
|
stripRanges: stripRanges,
|
|
1729
1757
|
replaceRanges: replaceRanges,
|
package/lib/gate-contract.js
CHANGED
|
@@ -2143,13 +2143,166 @@ function buildContentGate(spec) {
|
|
|
2143
2143
|
* b.gateContract.policyDisposition("audit-only"); // → "audit"
|
|
2144
2144
|
* b.gateContract.policyDisposition("rejet"); // → "refuse" (fail closed)
|
|
2145
2145
|
*/
|
|
2146
|
+
// The character-threat policies every guard in the family shares, and the
|
|
2147
|
+
// vocabulary each accepts. A value outside it is a typo, and a typo is a
|
|
2148
|
+
// CONFIG-TIME error: read leniently at runtime it takes whichever branch is not
|
|
2149
|
+
// the strict one — the scan runs because the value is not "allow", an issue is
|
|
2150
|
+
// raised, and policyDisposition falls through to "refuse". That fails closed,
|
|
2151
|
+
// so it is not a hole, but the operator asked for one disposition and silently
|
|
2152
|
+
// got another. This turns it into a boot error instead.
|
|
2153
|
+
//
|
|
2154
|
+
// `strip` is an instruction to repair, so it is legal only where the guard has
|
|
2155
|
+
// a `sanitize` to repair with — the same rule the family's performable-actions
|
|
2156
|
+
// invariant asserts. An `entries` guard has none by design: a hostile archive
|
|
2157
|
+
// member cannot be made safe.
|
|
2158
|
+
var CHAR_POLICY_KEYS = Object.freeze([
|
|
2159
|
+
"bidiPolicy", "nullBytePolicy", "controlPolicy", "zeroWidthPolicy", "tagsPolicy",
|
|
2160
|
+
]);
|
|
2161
|
+
var CHAR_POLICY_BASE = Object.freeze(["allow", "audit", "audit-only", "reject"]);
|
|
2162
|
+
|
|
2163
|
+
// The vocabularies a guard's own policies keep reaching for. Named because
|
|
2164
|
+
// most of a guard's policies answer the same question — refuse this construct,
|
|
2165
|
+
// record it, or let it through — and because a hand-written array per option
|
|
2166
|
+
// is where a family of nine drops `audit-only` from one of them.
|
|
2167
|
+
var POLICY_VALUES = Object.freeze({
|
|
2168
|
+
// Refuse the construct, record it, or let it through. `audit-only` is the
|
|
2169
|
+
// family's documented synonym for `audit`.
|
|
2170
|
+
rejectAuditAllow: Object.freeze(["reject", "audit", "audit-only", "allow"]),
|
|
2171
|
+
// The same, where the guard can also remove the construct and still produce
|
|
2172
|
+
// a usable document. Only for a finding the sanitizer genuinely repairs:
|
|
2173
|
+
// routing one to a repair that never happens serves the input as sanitized.
|
|
2174
|
+
rejectStripAuditAllow: Object.freeze(["reject", "strip", "audit", "audit-only", "allow"]),
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* @primitive b.gateContract.policyVocabulary
|
|
2179
|
+
* @signature b.gateContract.policyVocabulary(names, values, overrides?)
|
|
2180
|
+
* @since 0.18.48
|
|
2181
|
+
* @status stable
|
|
2182
|
+
* @related b.gateContract.charPolicyEnums, b.gateContract.defineGuard
|
|
2183
|
+
*
|
|
2184
|
+
* An `enumOpts` table where `names` all take `values`, with `overrides` merged
|
|
2185
|
+
* over the top for the options that differ. Most of a guard's policies share
|
|
2186
|
+
* one vocabulary and one or two do not, and writing the shared one out per
|
|
2187
|
+
* option is how a family of nine ends up with eight that accept `audit-only`
|
|
2188
|
+
* and one that does not.
|
|
2189
|
+
*
|
|
2190
|
+
* @example
|
|
2191
|
+
* var POLICY_ENUM = gateContract.policyVocabulary(
|
|
2192
|
+
* ["doctypePolicy", "entityPolicy", "cdataPolicy"],
|
|
2193
|
+
* gateContract.POLICY_VALUES.rejectAuditAllow,
|
|
2194
|
+
* { xmlDsigPolicy: ["audit", "audit-only", "allow"] });
|
|
2195
|
+
* POLICY_ENUM.entityPolicy; // → ["reject", "audit", "audit-only", "allow"]
|
|
2196
|
+
* POLICY_ENUM.xmlDsigPolicy; // → ["audit", "audit-only", "allow"]
|
|
2197
|
+
*/
|
|
2198
|
+
function policyVocabulary(names, values, overrides) {
|
|
2199
|
+
if (!Array.isArray(names) || !Array.isArray(values) || values.length === 0) {
|
|
2200
|
+
throw GateContractError.factory("gate-contract/bad-opt",
|
|
2201
|
+
"gateContract.policyVocabulary: names and values must both be arrays " +
|
|
2202
|
+
"and values must be non-empty");
|
|
2203
|
+
}
|
|
2204
|
+
// Frozen a level down, and from a copy. A guard publishes this table as its
|
|
2205
|
+
// `POLICY_VOCABULARY` while the resolver checks against the same arrays, so
|
|
2206
|
+
// an introspection caller that pushed onto one would be editing the rule, not
|
|
2207
|
+
// reading it: `POLICY_VOCABULARY.symlinkPolicy.push("bogus")` and the guard
|
|
2208
|
+
// accepts "bogus". Copying also keeps the caller's own array theirs.
|
|
2209
|
+
var out = {};
|
|
2210
|
+
var shared = Object.freeze(values.slice());
|
|
2211
|
+
names.forEach(function (name) {
|
|
2212
|
+
out[name] = shared;
|
|
2213
|
+
});
|
|
2214
|
+
if (overrides && typeof overrides === "object") {
|
|
2215
|
+
Object.keys(overrides).forEach(function (name) {
|
|
2216
|
+
// A non-array override would be dropped by the resolver's own
|
|
2217
|
+
// `Array.isArray` test and leave the option unconstrained — the option
|
|
2218
|
+
// would read as declared while accepting anything, which is the state
|
|
2219
|
+
// this whole mechanism exists to end. It is an author mistake, so it
|
|
2220
|
+
// belongs where the author is looking.
|
|
2221
|
+
if (!Array.isArray(overrides[name]) || overrides[name].length === 0) {
|
|
2222
|
+
throw GateContractError.factory("gate-contract/bad-opt",
|
|
2223
|
+
"gateContract.policyVocabulary: override for " + JSON.stringify(name) +
|
|
2224
|
+
" must be a non-empty array of values");
|
|
2225
|
+
}
|
|
2226
|
+
out[name] = Object.freeze(overrides[name].slice());
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
return Object.freeze(out);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
/**
|
|
2233
|
+
* @primitive b.gateContract.charPolicyEnums
|
|
2234
|
+
* @signature b.gateContract.charPolicyEnums(defaults, opts?)
|
|
2235
|
+
* @since 0.18.47
|
|
2236
|
+
* @status stable
|
|
2237
|
+
* @related b.gateContract.resolveProfileAndPosture, b.gateContract.defineGuard
|
|
2238
|
+
*
|
|
2239
|
+
* The `enumOpts` entry for every character-threat policy present in `defaults`,
|
|
2240
|
+
* so a guard binding its own resolver holds callers to the same vocabulary the
|
|
2241
|
+
* generated one does. `opts.canRepair` adds `strip`, which is an instruction to
|
|
2242
|
+
* repair and so legal only where the guard has a `sanitize` to perform it.
|
|
2243
|
+
*
|
|
2244
|
+
* Pass the result as `enumOpts` to `resolveProfileAndPosture`. A guard whose
|
|
2245
|
+
* gate or validate binds its own resolver and omits this accepts a misspelled
|
|
2246
|
+
* policy on that entry point while the generated path refuses it — the two
|
|
2247
|
+
* doors disagreeing is how the check came to cover only one of them.
|
|
2248
|
+
*
|
|
2249
|
+
* @opts
|
|
2250
|
+
* canRepair: boolean, // default: false — adds `strip` to every entry
|
|
2251
|
+
*
|
|
2252
|
+
* @example
|
|
2253
|
+
* gateContract.resolveProfileAndPosture(opts, {
|
|
2254
|
+
* profiles: PROFILES,
|
|
2255
|
+
* defaults: DEFAULTS,
|
|
2256
|
+
* enumOpts: gateContract.charPolicyEnums(DEFAULTS, { canRepair: true }),
|
|
2257
|
+
* });
|
|
2258
|
+
*/
|
|
2259
|
+
function charPolicyEnums(defaults, opts) {
|
|
2260
|
+
var canRepair = !!(opts && opts.canRepair);
|
|
2261
|
+
var out = null;
|
|
2262
|
+
for (var i = 0; i < CHAR_POLICY_KEYS.length; i += 1) {
|
|
2263
|
+
var key = CHAR_POLICY_KEYS[i];
|
|
2264
|
+
// `tagsPolicy` is usually ABSENT from a guard's defaults because it
|
|
2265
|
+
// INHERITS from zeroWidthPolicy (resolveTagsPolicy reads one when the other
|
|
2266
|
+
// is unset). It is still a supported inline override, so a guard exposing
|
|
2267
|
+
// the inherited behaviour must hold a typo in it to the same vocabulary —
|
|
2268
|
+
// keying only on presence left `{ tagsPolicy: "strp" }` unchecked on every
|
|
2269
|
+
// guard that inherits, and the Tags detector then read the typo as a
|
|
2270
|
+
// high-severity refusal instead of the promised boot error.
|
|
2271
|
+
var governs = defaults && (typeof defaults[key] === "string" ||
|
|
2272
|
+
(key === "tagsPolicy" && typeof defaults.zeroWidthPolicy === "string"));
|
|
2273
|
+
if (!governs) continue;
|
|
2274
|
+
if (!out) out = {};
|
|
2275
|
+
out[key] = canRepair ? CHAR_POLICY_BASE.concat(["strip"]) : CHAR_POLICY_BASE.slice();
|
|
2276
|
+
}
|
|
2277
|
+
return out;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function _withCharPolicyEnums(defaults, spec) {
|
|
2281
|
+
var declared = spec.enumOpts && typeof spec.enumOpts === "object" ? spec.enumOpts : null;
|
|
2282
|
+
// DECLARED, not inferred from the presence of a `sanitize`. An identifier
|
|
2283
|
+
// guard exports one that validates and throws — there is nothing to repair,
|
|
2284
|
+
// the value either is a UUID or is not — so inferring the capability let
|
|
2285
|
+
// guardRegex, guardJwt, guardShell and guardTemplate accept `strip` at
|
|
2286
|
+
// construction and refuse at runtime, which is the silent substitution this
|
|
2287
|
+
// check exists to stop. A guard says whether it can repair a character, and
|
|
2288
|
+
// the family sweep holds it to the claim.
|
|
2289
|
+
var derived = charPolicyEnums(defaults, { canRepair: spec.charRepair === true });
|
|
2290
|
+
if (!derived) return declared || null;
|
|
2291
|
+
var out = declared ? Object.assign({}, declared) : {};
|
|
2292
|
+
Object.keys(derived).forEach(function (key) {
|
|
2293
|
+
if (declared && declared[key]) return; // an explicit declaration wins
|
|
2294
|
+
out[key] = derived[key];
|
|
2295
|
+
});
|
|
2296
|
+
return out;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2146
2299
|
var MITIGATION_POLICIES = Object.freeze({
|
|
2147
2300
|
strip: true, "prefix-tab": true, "prefix-quote": true,
|
|
2148
2301
|
"wrap-with-quotes-and-prefix": true, allowlist: true, redact: true, trim: true,
|
|
2149
2302
|
});
|
|
2150
2303
|
function policyDisposition(policy) {
|
|
2151
2304
|
if (policy === "reject") return "refuse";
|
|
2152
|
-
if (policy
|
|
2305
|
+
if (codepointClass.isAuditPolicy(policy)) return "audit";
|
|
2153
2306
|
if (MITIGATION_POLICIES[policy] === true) return "sanitize";
|
|
2154
2307
|
return "refuse";
|
|
2155
2308
|
}
|
|
@@ -2598,10 +2751,77 @@ function resolveProfileAndPosture(opts, cfg) {
|
|
|
2598
2751
|
}
|
|
2599
2752
|
});
|
|
2600
2753
|
}
|
|
2754
|
+
// The same failure with the NAME wrong instead of the value, which is
|
|
2755
|
+
// quieter because there is no value to inspect: the merge keeps the key,
|
|
2756
|
+
// nothing reads it, and the profile default stays in force. An operator who
|
|
2757
|
+
// sets `riskyTypesPolicy: "reject"` on a guard whose option is
|
|
2758
|
+
// `riskyTypePolicy` keeps the balanced profile's "audit" and gets no sign
|
|
2759
|
+
// the setting was dropped.
|
|
2760
|
+
//
|
|
2761
|
+
// Only `*Policy` keys are decidable this way: a guard's policy set is fully
|
|
2762
|
+
// described by its profiles, postures and defaults, so anything else with
|
|
2763
|
+
// that suffix is a near-miss. The rest of the opts surface stays open by
|
|
2764
|
+
// design — callbacks, caches and host wiring are not enumerable in advance.
|
|
2765
|
+
_refuseUnknownPolicyKeys(opts, resolved, cfg, ErrorClass, prefix);
|
|
2601
2766
|
return resolved;
|
|
2602
2767
|
}
|
|
2603
2768
|
|
|
2604
2769
|
|
|
2770
|
+
// A published copy of an enumOpts table, frozen a level down so a caller
|
|
2771
|
+
// reading it cannot reorder the guard's own vocabulary underneath it.
|
|
2772
|
+
function _freezeVocabulary(enumOpts) {
|
|
2773
|
+
var out = {};
|
|
2774
|
+
if (enumOpts && typeof enumOpts === "object") {
|
|
2775
|
+
Object.keys(enumOpts).forEach(function (k) {
|
|
2776
|
+
out[k] = Array.isArray(enumOpts[k]) ? Object.freeze(enumOpts[k].slice()) : enumOpts[k];
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
return Object.freeze(out);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// Every `*Policy` name this guard answers to: whatever its defaults carry, plus
|
|
2783
|
+
// whatever any profile or posture overlay can introduce, plus the vocabularies
|
|
2784
|
+
// declared for it. The union matters — a policy that only a strict profile sets
|
|
2785
|
+
// is still a real option, and `tagsPolicy` is governed through the family's
|
|
2786
|
+
// derivation without appearing in any defaults block.
|
|
2787
|
+
function _knownPolicyKeys(cfg) {
|
|
2788
|
+
var known = Object.create(null);
|
|
2789
|
+
function absorb(o) {
|
|
2790
|
+
if (!o || typeof o !== "object") return;
|
|
2791
|
+
Object.keys(o).forEach(function (k) {
|
|
2792
|
+
if (/Policy$/.test(k)) known[k] = true;
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
absorb(cfg.defaults);
|
|
2796
|
+
absorb(cfg.enumOpts);
|
|
2797
|
+
[cfg.profiles, cfg.compliancePostures].forEach(function (table) {
|
|
2798
|
+
if (!table || typeof table !== "object") return;
|
|
2799
|
+
Object.keys(table).forEach(function (name) { absorb(table[name]); });
|
|
2800
|
+
});
|
|
2801
|
+
return known;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
// Refuses a caller-supplied `*Policy` the guard has no such option for. Reads
|
|
2805
|
+
// the caller's own opts rather than the merged result, so a guard's internal
|
|
2806
|
+
// wiring is never the thing that fails, and stays silent for a guard that has
|
|
2807
|
+
// no policies at all — there, the suffix carries no meaning to trade on.
|
|
2808
|
+
function _refuseUnknownPolicyKeys(opts, resolved, cfg, ErrorClass, prefix) {
|
|
2809
|
+
if (!opts || typeof opts !== "object") return;
|
|
2810
|
+
var supplied = Object.keys(opts).filter(function (k) {
|
|
2811
|
+
return /Policy$/.test(k) && opts[k] !== undefined;
|
|
2812
|
+
});
|
|
2813
|
+
if (!supplied.length) return;
|
|
2814
|
+
var known = _knownPolicyKeys(cfg);
|
|
2815
|
+
var names = Object.keys(known);
|
|
2816
|
+
if (!names.length) return;
|
|
2817
|
+
for (var i = 0; i < supplied.length; i += 1) {
|
|
2818
|
+
if (known[supplied[i]]) continue;
|
|
2819
|
+
throw ErrorClass.factory(prefix + ".bad-opt",
|
|
2820
|
+
prefix + ": no option named " + JSON.stringify(supplied[i]) +
|
|
2821
|
+
" — this guard's policies are " + names.sort().join(", "));
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2605
2825
|
// The own enumerable properties of `o` that were actually given a value. A key
|
|
2606
2826
|
// present with the value `undefined` is treated as absent, so it cannot erase
|
|
2607
2827
|
// a default it was never meant to touch.
|
|
@@ -3168,7 +3388,12 @@ function defineGuard(spec) {
|
|
|
3168
3388
|
// validate() below.
|
|
3169
3389
|
nonNegativeOpts: _capKeys(defaults, spec.intOpts),
|
|
3170
3390
|
// Options restricted to a fixed vocabulary, checked at the same funnel.
|
|
3171
|
-
|
|
3391
|
+
// The character policies are derived rather than hand-listed, for the
|
|
3392
|
+
// reason the caps above are: declared per guard, they were declared by
|
|
3393
|
+
// one. guard-country wrote `enumOpts` for its own three opts after
|
|
3394
|
+
// `{ reservedPolicy: "rejcet" }` served a reserved code and reported ok,
|
|
3395
|
+
// and the other 259 policy opts across the family never followed.
|
|
3396
|
+
enumOpts: _withCharPolicyEnums(defaults, spec),
|
|
3172
3397
|
});
|
|
3173
3398
|
};
|
|
3174
3399
|
if (typeof spec.detect === "function") {
|
|
@@ -3360,11 +3585,23 @@ function defineGuard(spec) {
|
|
|
3360
3585
|
var out = {
|
|
3361
3586
|
NAME: spec.name,
|
|
3362
3587
|
KIND: spec.kind,
|
|
3588
|
+
// Surfaced so a caller — and the family sweep — can ask whether `strip` is
|
|
3589
|
+
// an instruction this guard can carry out, rather than inferring it from
|
|
3590
|
+
// the presence of a `sanitize` that may only validate and throw.
|
|
3591
|
+
CHAR_REPAIR: spec.charRepair === true,
|
|
3363
3592
|
// The options this guard DECLARES to be caps, where zero is refused. Every
|
|
3364
3593
|
// other numeric option is derived from the defaults and held only to
|
|
3365
3594
|
// "non-negative integer", because derivation cannot tell a cap from a
|
|
3366
3595
|
// tolerance. Exposed so the family tests can tell the two apart.
|
|
3367
3596
|
INT_OPTS: Object.freeze(Array.isArray(spec.intOpts) ? spec.intOpts.slice() : []),
|
|
3597
|
+
// The value each policy option accepts, after the family's character
|
|
3598
|
+
// policies are folded in. Exposed for the same reason as INT_OPTS: the
|
|
3599
|
+
// family sweep has to ask each guard what it takes rather than assume one
|
|
3600
|
+
// vocabulary for all of them. Where a guard pins a policy to a single
|
|
3601
|
+
// value — guardFilename's traversal, guardJwt's alg=none — the answer is a
|
|
3602
|
+
// one-element list, and a sweep that assumed otherwise would report the
|
|
3603
|
+
// pinning as a regression.
|
|
3604
|
+
POLICY_VOCABULARY: _freezeVocabulary(_withCharPolicyEnums(defaults, spec)),
|
|
3368
3605
|
validate: spec.validate,
|
|
3369
3606
|
resolveOpts: _resolveGuardOpts,
|
|
3370
3607
|
buildProfile: buildProfileFn,
|
|
@@ -3379,6 +3616,20 @@ function defineGuard(spec) {
|
|
|
3379
3616
|
out.EXTENSIONS = Object.freeze((spec.extensions || []).slice());
|
|
3380
3617
|
}
|
|
3381
3618
|
if (spec.integrationFixtures) out.INTEGRATION_FIXTURES = spec.integrationFixtures;
|
|
3619
|
+
// A declaration has to be backed by something. `charRepair: true` says the
|
|
3620
|
+
// guard can carry out `strip`, which puts that value in its character-policy
|
|
3621
|
+
// vocabulary — so a spec that declares it without a `sanitize` (or a
|
|
3622
|
+
// `sanitizeTransform` for one to be built from) would accept the instruction
|
|
3623
|
+
// at boot and refuse it at runtime, which is the substitution the declaration
|
|
3624
|
+
// was introduced to stop. Checked HERE rather than earlier because
|
|
3625
|
+
// sanitizeTransform generates the sanitize further up, and checking before
|
|
3626
|
+
// that would refuse a spec that does supply a repair path.
|
|
3627
|
+
if (spec.charRepair === true && typeof spec.sanitize !== "function") {
|
|
3628
|
+
throw ErrorClass.factory(prefix + ".bad-spec",
|
|
3629
|
+
prefix + ": charRepair is declared but the guard has no sanitize — " +
|
|
3630
|
+
"`strip` on a character policy is an instruction to repair, so declare a " +
|
|
3631
|
+
"sanitize or a sanitizeTransform, or drop charRepair.");
|
|
3632
|
+
}
|
|
3382
3633
|
if (typeof spec.sanitize === "function") out.sanitize = spec.sanitize;
|
|
3383
3634
|
out.gate = gateFn;
|
|
3384
3635
|
// Error class exported under its own constructor name (GuardCsvError etc.)
|
|
@@ -3713,6 +3964,9 @@ module.exports = {
|
|
|
3713
3964
|
// defineGuard: the cap keys to declare, derived from its defaults so the
|
|
3714
3965
|
// list cannot drift away from them.
|
|
3715
3966
|
capKeysOf: capKeysOf,
|
|
3967
|
+
charPolicyEnums: charPolicyEnums,
|
|
3968
|
+
policyVocabulary: policyVocabulary,
|
|
3969
|
+
POLICY_VALUES: POLICY_VALUES,
|
|
3716
3970
|
identitySanitize: identitySanitize,
|
|
3717
3971
|
ctxValueFrom: ctxValueFrom,
|
|
3718
3972
|
runIssueValidator: runIssueValidator,
|
package/lib/guard-archive.js
CHANGED
|
@@ -197,6 +197,20 @@ var INT_OPTS = ["maxEntries", "maxTotalBytes", "maxEntryBytes",
|
|
|
197
197
|
// DERIVED from DEFAULTS rather than written out, because a hand-kept list is
|
|
198
198
|
// what drifted away from the defaults it was meant to mirror and left limits
|
|
199
199
|
// unchecked across the family.
|
|
200
|
+
// Each policy's vocabulary, so a misspelling is a boot error rather than a
|
|
201
|
+
// runtime surprise. Read leniently, a typo takes whichever branch is not the
|
|
202
|
+
// strict one: `symlinkPolicy: "rejct"` is not "allow", so the check runs, and
|
|
203
|
+
// it is not "reject" either, so the finding drops from critical to warn.
|
|
204
|
+
//
|
|
205
|
+
// `audit-only` is deliberately absent, unlike elsewhere in the family:
|
|
206
|
+
// nestedArchivePolicy tests for "audit" exactly, so the synonym would fall
|
|
207
|
+
// past both branches and record nothing at all.
|
|
208
|
+
var POLICY_ENUM = gateContract.policyVocabulary([
|
|
209
|
+
"traversalPolicy", "absolutePathPolicy", "symlinkPolicy", "hardlinkPolicy",
|
|
210
|
+
"nestedArchivePolicy", "duplicateNamePolicy", "caseInsensitiveCollisionPolicy",
|
|
211
|
+
"encryptionPolicy", "sparseEntryPolicy",
|
|
212
|
+
], ["reject", "audit", "allow"]);
|
|
213
|
+
|
|
200
214
|
function _resolveOpts(opts) {
|
|
201
215
|
return gateContract.resolveProfileAndPosture(opts, {
|
|
202
216
|
profiles: PROFILES,
|
|
@@ -206,6 +220,7 @@ function _resolveOpts(opts) {
|
|
|
206
220
|
errCodePrefix: "archive",
|
|
207
221
|
intOpts: INT_OPTS,
|
|
208
222
|
nonNegativeOpts: gateContract.capKeysOf(DEFAULTS),
|
|
223
|
+
enumOpts: POLICY_ENUM,
|
|
209
224
|
});
|
|
210
225
|
}
|
|
211
226
|
|
|
@@ -866,6 +881,9 @@ module.exports = {
|
|
|
866
881
|
// ---- guard-* family registry exports (consumed by b.guardAll) ----
|
|
867
882
|
NAME: "archive",
|
|
868
883
|
KIND: "entries", // archive-entries guard (consumes ctx.entries)
|
|
884
|
+
// The value each policy accepts. Named here rather than derived, because
|
|
885
|
+
// this guard binds its own resolver instead of going through defineGuard.
|
|
886
|
+
POLICY_VOCABULARY: POLICY_ENUM,
|
|
869
887
|
INTEGRATION_FIXTURES: Object.freeze({
|
|
870
888
|
kind: "entries",
|
|
871
889
|
contentType: "application/zip",
|
package/lib/guard-auth.js
CHANGED
|
@@ -393,7 +393,35 @@ var INTEGRATION_FIXTURES = Object.freeze({
|
|
|
393
393
|
// surface (validate / sanitize / bespoke gate) passed through verbatim.
|
|
394
394
|
// The custom KIND ("auth-bundle") is accepted because the bespoke gate
|
|
395
395
|
// reads its own ctx fields (ctx.authBundle / ctx.auth).
|
|
396
|
+
// The policies this wrapper accepts on the wrapped guard's behalf, with the
|
|
397
|
+
// wrapped guard's own vocabulary for each. Derived rather than restated: a
|
|
398
|
+
// second copy of guardOauth's values here would be free to drift from the ones
|
|
399
|
+
// the child actually enforces, and the operator would meet whichever of the
|
|
400
|
+
// two happened to be wrong.
|
|
401
|
+
//
|
|
402
|
+
// Without this, forwarding `codeReusePolicy` through the wrapper failed at the
|
|
403
|
+
// wrapper's own resolver, because the name belongs to the child and this
|
|
404
|
+
// guard's profiles never mention it.
|
|
405
|
+
//
|
|
406
|
+
// The character policies are excluded even though they are forwarded: their
|
|
407
|
+
// vocabulary is derived per guard from whether that guard can repair a
|
|
408
|
+
// character, and this guard cannot. Taking the child's answer would hand this
|
|
409
|
+
// one a `strip` it has no way to carry out the moment the two differ — the
|
|
410
|
+
// substitution that accepting-then-refusing was introduced to stop.
|
|
411
|
+
var CHILD_OWNED_POLICIES = Object.freeze([
|
|
412
|
+
"bidiPolicy", "controlPolicy", "nullBytePolicy", "zeroWidthPolicy", "tagsPolicy",
|
|
413
|
+
]);
|
|
414
|
+
var FORWARDED_POLICY_ENUM = Object.freeze(
|
|
415
|
+
OAUTH_FORWARDED_OPTS.reduce(function (acc, key) {
|
|
416
|
+
if (CHILD_OWNED_POLICIES.indexOf(key) !== -1) return acc;
|
|
417
|
+
var vocabulary = guardOauth.POLICY_VOCABULARY &&
|
|
418
|
+
guardOauth.POLICY_VOCABULARY[key];
|
|
419
|
+
if (Array.isArray(vocabulary)) acc[key] = vocabulary;
|
|
420
|
+
return acc;
|
|
421
|
+
}, {}));
|
|
422
|
+
|
|
396
423
|
var _guard = module.exports = gateContract.defineGuard({
|
|
424
|
+
enumOpts: FORWARDED_POLICY_ENUM,
|
|
397
425
|
name: "auth",
|
|
398
426
|
kind: "auth-bundle",
|
|
399
427
|
errorClass: GuardAuthError,
|
package/lib/guard-cidr.js
CHANGED
|
@@ -494,10 +494,29 @@ var INTEGRATION_FIXTURES = gateContract.identifierFixtures("8.8.8.0/24", "10.0.0
|
|
|
494
494
|
// standard serve -> audit-only -> refuse chain — reading ctx.identifier ||
|
|
495
495
|
// ctx.cidr via ctxFields. No sanitize action: an allowlist gate never
|
|
496
496
|
// rewrites the operator's stored network range.
|
|
497
|
+
// Each policy's vocabulary, so a misspelling is a boot error rather than a
|
|
498
|
+
// runtime surprise. Read leniently, a typo takes whichever branch is not the
|
|
499
|
+
// strict one: `reservedRangesPolicy: "rejct"` is not "allow", so the check
|
|
500
|
+
// runs, and it is not "reject" either, so the finding drops to a warning — the
|
|
501
|
+
// operator asked to refuse a reserved range and silently got an audit.
|
|
502
|
+
//
|
|
503
|
+
// requireMaskPolicy carries its own spellings because the question is about the
|
|
504
|
+
// mask rather than a threat disposition. `audit-only` rides with `audit` on the
|
|
505
|
+
// three that take one, matching how the framework treats the two everywhere
|
|
506
|
+
// else.
|
|
507
|
+
var POLICY_ENUM = gateContract.policyVocabulary([
|
|
508
|
+
"networkAlignmentPolicy", "reservedRangesPolicy", "ipv4MappedIpv6Policy",
|
|
509
|
+
], gateContract.POLICY_VALUES.rejectAuditAllow, {
|
|
510
|
+
// Names its own values because the finding is an absence — a bare address
|
|
511
|
+
// where a mask was expected — so "reject" alone would not say what of.
|
|
512
|
+
requireMaskPolicy: ["allow-bare-ip", "audit-bare-ip", "reject-bare-ip"],
|
|
513
|
+
});
|
|
514
|
+
|
|
497
515
|
module.exports = gateContract.defineGuard({
|
|
498
516
|
name: "cidr",
|
|
499
517
|
kind: "identifier",
|
|
500
518
|
errorClass: GuardCidrError,
|
|
519
|
+
enumOpts: POLICY_ENUM,
|
|
501
520
|
profiles: PROFILES,
|
|
502
521
|
base: 128,
|
|
503
522
|
integrationFixtures: INTEGRATION_FIXTURES,
|
package/lib/guard-country.js
CHANGED
|
@@ -348,10 +348,10 @@ function _detectIssues(input, opts) {
|
|
|
348
348
|
* @opts
|
|
349
349
|
* profile: "strict"|"balanced"|"permissive",
|
|
350
350
|
* compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
|
|
351
|
-
* bidiPolicy: "reject"|"
|
|
352
|
-
* controlPolicy: "reject"|"
|
|
353
|
-
* nullBytePolicy: "reject"|"
|
|
354
|
-
* zeroWidthPolicy: "reject"|"
|
|
351
|
+
* bidiPolicy: "reject"|"audit"|"allow",
|
|
352
|
+
* controlPolicy: "reject"|"audit"|"allow",
|
|
353
|
+
* nullBytePolicy: "reject"|"audit"|"allow",
|
|
354
|
+
* zeroWidthPolicy: "reject"|"audit"|"allow",
|
|
355
355
|
* reservedPolicy: "reject"|"audit"|"allow",
|
|
356
356
|
* userAssignedPolicy: "reject"|"audit"|"allow",
|
|
357
357
|
* formerlyUsedPolicy: "reject"|"audit"|"allow",
|
package/lib/guard-csv.js
CHANGED
|
@@ -639,7 +639,7 @@ function _stripIssues(text, opts) {
|
|
|
639
639
|
* twice does not stack prefixes.
|
|
640
640
|
*
|
|
641
641
|
* @opts
|
|
642
|
-
* formulaInjectionPolicy: "prefix-tab"|"prefix-quote"|"wrap-with-quotes-and-prefix"|"reject"|"allowlist",
|
|
642
|
+
* formulaInjectionPolicy: "prefix-tab"|"prefix-quote"|"wrap-with-quotes-and-prefix"|"reject"|"allowlist"|"audit-only"|"allow",
|
|
643
643
|
* formulasAllowlist: string[], // when policy === "allowlist"
|
|
644
644
|
* bidiCharPolicy: "reject"|"strip"|"audit"|"allow",
|
|
645
645
|
* controlCharPolicy: "reject"|"strip"|"allow",
|
|
@@ -1047,7 +1047,7 @@ function serialize(rows, opts) {
|
|
|
1047
1047
|
* controlCharPolicy: "reject"|"strip"|"allow",
|
|
1048
1048
|
* nullByteHandling: "reject"|"strip"|"allow",
|
|
1049
1049
|
* homoglyphPolicy: "audit"|"strip"|"allow",
|
|
1050
|
-
* formulaInjectionPolicy: "prefix-tab"|"prefix-quote"|"wrap-with-quotes-and-prefix"|"reject"|"audit-only"|"allow",
|
|
1050
|
+
* formulaInjectionPolicy: "prefix-tab"|"prefix-quote"|"wrap-with-quotes-and-prefix"|"reject"|"allowlist"|"audit-only"|"allow",
|
|
1051
1051
|
* dangerousFunctions: string[],
|
|
1052
1052
|
* dialectPolicy: "strict"|"permissive",
|
|
1053
1053
|
*
|
|
@@ -1409,7 +1409,36 @@ var INTEGRATION_FIXTURES = Object.freeze({
|
|
|
1409
1409
|
// extras (serialize / escapeCell / detect / schema / FORMULA_PREFIXES /
|
|
1410
1410
|
// DANGEROUS_FUNCTIONS) passed through verbatim. The bespoke `gate` carries
|
|
1411
1411
|
// CSV's sanitize-reparse-reserialize chain unchanged.
|
|
1412
|
+
// Each policy's vocabulary, so a misspelling is a boot error rather than a
|
|
1413
|
+
// runtime surprise. Read leniently, a typo takes whichever branch is not the
|
|
1414
|
+
// strict one: `formulaInjectionPolicy: "prefix-tabb"` is neither "allow" nor
|
|
1415
|
+
// "audit-only", so a mitigation is attempted, and it is not a spelling the
|
|
1416
|
+
// mitigation recognises — the operator asked for a tab prefix and got something
|
|
1417
|
+
// else without being told.
|
|
1418
|
+
//
|
|
1419
|
+
// The sets are the UNION of what the code compares, what the opts blocks
|
|
1420
|
+
// document and what the shipped profiles select, because no single source was
|
|
1421
|
+
// complete: the two opts blocks disagreed with each other, one listing
|
|
1422
|
+
// `allowlist` and the other `audit-only` / `allow`, while the code compares all
|
|
1423
|
+
// three. Both blocks now carry the full set.
|
|
1424
|
+
//
|
|
1425
|
+
// The last four are MODE settings rather than threat dispositions — a dialect,
|
|
1426
|
+
// a numeric rendering, whether whitespace is kept — so they share nothing with
|
|
1427
|
+
// the disposition vocabulary and are listed on their own terms.
|
|
1428
|
+
var POLICY_ENUM = Object.freeze({
|
|
1429
|
+
formulaInjectionPolicy: ["prefix-tab", "prefix-quote", "wrap-with-quotes-and-prefix",
|
|
1430
|
+
"reject", "allowlist", "audit-only", "allow"],
|
|
1431
|
+
bidiCharPolicy: ["reject", "strip", "audit", "allow"],
|
|
1432
|
+
controlCharPolicy: ["reject", "strip", "allow"],
|
|
1433
|
+
homoglyphPolicy: ["audit", "strip", "allow"],
|
|
1434
|
+
trailingWhitespacePolicy: ["trim", "preserve", "reject"],
|
|
1435
|
+
dialectPolicy: ["strict", "permissive"],
|
|
1436
|
+
numericPrecisionPolicy: ["decimal-string-above-safe-int", "scientific", "reject-bigint"],
|
|
1437
|
+
piiPolicy: ["preserve", "redact"],
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1412
1440
|
module.exports = gateContract.defineGuard({
|
|
1441
|
+
enumOpts: POLICY_ENUM,
|
|
1413
1442
|
name: "csv",
|
|
1414
1443
|
kind: "content",
|
|
1415
1444
|
errorClass: GuardCsvError,
|