@blamejs/core 0.7.52 → 0.7.61

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.
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ /**
3
+ * guard-shell — Shell metacharacter identifier-safety primitive
4
+ * (b.guardShell).
5
+ *
6
+ * Validates user-input strings BEFORE they're handed to a
7
+ * child-process spawn (regardless of operator's `shell:` opt). The
8
+ * canonical defense is "use array args + shell:false", but operators
9
+ * still receive operator-untrusted strings that flow through path-
10
+ * arg or arg-list shapes — guardShell refuses obvious shell-injection
11
+ * shapes before the spawn call. KIND="identifier" — consumes
12
+ * ctx.identifier (or ctx.arg).
13
+ *
14
+ * Threat catalog:
15
+ * - POSIX shell metacharacters — `;`, `&`, `|`, `<`, `>`, `(`, `)`,
16
+ * `{`, `}`, `[`, `]`, `*`, `?`, `~`, `!`, `#`, `\`, single + double
17
+ * quotes.
18
+ * - Backtick command substitution.
19
+ * - `$(...)` command substitution and `${VAR}` parameter expansion.
20
+ * - Process substitution `<(...)` / `>(...)`.
21
+ * - cmd.exe metacharacters — `&`, `|`, `<`, `>`, `^`, `%`, `"`, `'`,
22
+ * `(`, `)`, `,`, `;`, `=`, ` `, tabs, newlines.
23
+ * - Newline / NUL injection (line splitting in scripts).
24
+ * - Variable expansion `$VAR`.
25
+ * - Operator may opt-in to `argHyphenPolicy` to refuse leading `-`
26
+ * arguments (defense against `-rf` / `--exec` / etc.).
27
+ * - BIDI / zero-width / control / null-byte universal refuse.
28
+ *
29
+ * var rv = b.guardShell.validate("file with spaces.txt",
30
+ * { profile: "strict" });
31
+ * var g = b.guardShell.gate({ profile: "strict" });
32
+ */
33
+
34
+ var codepointClass = require("./codepoint-class");
35
+ var lazyRequire = require("./lazy-require");
36
+ var gateContract = require("./gate-contract");
37
+ var C = require("./constants");
38
+ var numericBounds = require("./numeric-bounds");
39
+ var { GuardShellError } = require("./framework-error");
40
+
41
+ var observability = lazyRequire(function () { return require("./observability"); });
42
+ void observability;
43
+
44
+ var _err = GuardShellError.factory;
45
+
46
+ // POSIX shell metachars (excluding whitespace which is per-policy).
47
+ var POSIX_META_RE = /[;&|<>$`\\()[\]{}*?~!#'"]/;
48
+
49
+ // cmd.exe metachars.
50
+ var CMD_META_RE = /[&|<>^%"',;=]/;
51
+
52
+ // $(...) and ${...} command/param substitution.
53
+ var DOLLAR_PAREN_RE = /\$\(/;
54
+ var DOLLAR_BRACE_RE = /\$\{/;
55
+
56
+ // $VAR parameter expansion (bare).
57
+ var DOLLAR_VAR_RE = /\$[A-Za-z_][A-Za-z0-9_]*/;
58
+
59
+ // Process substitution.
60
+ var PROCESS_SUBST_RE = /[<>]\(/;
61
+
62
+ // Newline (any).
63
+ var NEWLINE_RE = /[\r\n]/;
64
+
65
+ // ---- Profile presets ----
66
+
67
+ var PROFILES = Object.freeze({
68
+ "strict": {
69
+ bidiPolicy: "reject",
70
+ controlPolicy: "reject",
71
+ nullBytePolicy: "reject",
72
+ zeroWidthPolicy: "reject",
73
+ posixMetaPolicy: "reject",
74
+ cmdMetaPolicy: "reject",
75
+ dollarSubstPolicy: "reject",
76
+ processSubstPolicy: "reject",
77
+ backtickPolicy: "reject",
78
+ newlinePolicy: "reject",
79
+ argHyphenPolicy: "reject",
80
+ maxBytes: C.BYTES.kib(2),
81
+ maxRuntimeMs: C.TIME.seconds(2),
82
+ },
83
+ "balanced": {
84
+ bidiPolicy: "reject",
85
+ controlPolicy: "reject",
86
+ nullBytePolicy: "reject",
87
+ zeroWidthPolicy: "reject",
88
+ posixMetaPolicy: "audit",
89
+ cmdMetaPolicy: "audit",
90
+ dollarSubstPolicy: "reject",
91
+ processSubstPolicy: "reject",
92
+ backtickPolicy: "reject",
93
+ newlinePolicy: "reject",
94
+ argHyphenPolicy: "audit",
95
+ maxBytes: C.BYTES.kib(2),
96
+ maxRuntimeMs: C.TIME.seconds(2),
97
+ },
98
+ "permissive": {
99
+ bidiPolicy: "reject", // BIDI refused at every profile
100
+ controlPolicy: "reject", // controls refused at every profile
101
+ nullBytePolicy: "reject", // null refused at every profile
102
+ zeroWidthPolicy: "reject", // zero-width refused at every profile
103
+ posixMetaPolicy: "audit",
104
+ cmdMetaPolicy: "audit",
105
+ dollarSubstPolicy: "reject", // command substitution refused at every profile
106
+ processSubstPolicy: "reject", // process substitution refused at every profile
107
+ backtickPolicy: "reject", // backtick substitution refused at every profile
108
+ newlinePolicy: "reject", // newline refused at every profile
109
+ argHyphenPolicy: "allow",
110
+ maxBytes: C.BYTES.kib(8),
111
+ maxRuntimeMs: C.TIME.seconds(2),
112
+ },
113
+ });
114
+
115
+ var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
116
+ mode: "enforce",
117
+ }));
118
+
119
+ var COMPLIANCE_POSTURES = Object.freeze({
120
+ "hipaa": Object.assign({}, PROFILES["strict"], {
121
+ forensicSnippetBytes: C.BYTES.bytes(256),
122
+ }),
123
+ "pci-dss": Object.assign({}, PROFILES["strict"], {
124
+ forensicSnippetBytes: C.BYTES.bytes(256),
125
+ }),
126
+ "gdpr": Object.assign({}, PROFILES["balanced"], {
127
+ forensicSnippetBytes: C.BYTES.bytes(128),
128
+ }),
129
+ "soc2": Object.assign({}, PROFILES["strict"], {
130
+ forensicSnippetBytes: C.BYTES.bytes(512),
131
+ }),
132
+ });
133
+
134
+ function _resolveOpts(opts) {
135
+ return gateContract.resolveProfileAndPosture(opts, {
136
+ profiles: PROFILES,
137
+ compliancePostures: COMPLIANCE_POSTURES,
138
+ defaults: DEFAULTS,
139
+ errorClass: GuardShellError,
140
+ errCodePrefix: "shell",
141
+ });
142
+ }
143
+
144
+ function _detectIssues(input, opts) {
145
+ var issues = [];
146
+ if (typeof input !== "string") {
147
+ return [{ kind: "bad-input", severity: "high",
148
+ ruleId: "shell.bad-input",
149
+ snippet: "shell arg is not a string" }];
150
+ }
151
+ if (input.length === 0) {
152
+ // Empty arg is not necessarily a threat (legit blank args exist).
153
+ return [];
154
+ }
155
+ if (Buffer.byteLength(input, "utf8") > opts.maxBytes) {
156
+ return [{ kind: "shell-cap", severity: "high",
157
+ ruleId: "shell.shell-cap",
158
+ snippet: "shell arg exceeds maxBytes " + opts.maxBytes }];
159
+ }
160
+
161
+ var charThreats = codepointClass.detectCharThreats(input, opts, "shell");
162
+ for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
163
+
164
+ // $(...) / ${...} / backtick — universal refuse.
165
+ if (opts.dollarSubstPolicy !== "allow" &&
166
+ (DOLLAR_PAREN_RE.test(input) || DOLLAR_BRACE_RE.test(input))) { // allow:regex-no-length-cap — input bounded by maxBytes
167
+ issues.push({
168
+ kind: "dollar-substitution", severity: "critical",
169
+ ruleId: "shell.dollar-substitution",
170
+ snippet: "argument contains `$(` or `${` — POSIX shell command / " +
171
+ "parameter substitution",
172
+ });
173
+ }
174
+ if (opts.backtickPolicy !== "allow" && input.indexOf("`") !== -1) {
175
+ issues.push({
176
+ kind: "backtick", severity: "critical",
177
+ ruleId: "shell.backtick",
178
+ snippet: "argument contains backtick — POSIX shell command " +
179
+ "substitution",
180
+ });
181
+ }
182
+ if (opts.processSubstPolicy !== "allow" && PROCESS_SUBST_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
183
+ issues.push({
184
+ kind: "process-substitution", severity: "critical",
185
+ ruleId: "shell.process-substitution",
186
+ snippet: "argument contains `<(` or `>(` — Bash process " +
187
+ "substitution",
188
+ });
189
+ }
190
+ if (opts.dollarSubstPolicy !== "allow" && DOLLAR_VAR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
191
+ issues.push({
192
+ kind: "dollar-var",
193
+ severity: opts.dollarSubstPolicy === "reject" ? "high" : "warn",
194
+ ruleId: "shell.dollar-var",
195
+ snippet: "argument contains `$VAR` parameter expansion",
196
+ });
197
+ }
198
+ if (opts.newlinePolicy !== "allow" && NEWLINE_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
199
+ issues.push({
200
+ kind: "newline", severity: "high",
201
+ ruleId: "shell.newline",
202
+ snippet: "argument contains CR / LF — line-splitting in shell " +
203
+ "scripts",
204
+ });
205
+ }
206
+ if (opts.posixMetaPolicy !== "allow" && POSIX_META_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
207
+ issues.push({
208
+ kind: "posix-metachar",
209
+ severity: opts.posixMetaPolicy === "reject" ? "high" : "warn",
210
+ ruleId: "shell.posix-metachar",
211
+ snippet: "argument contains POSIX shell metacharacter " +
212
+ "(`;|&<>()[]{}*?~!#`'\"\\`)",
213
+ });
214
+ }
215
+ if (opts.cmdMetaPolicy !== "allow" && CMD_META_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
216
+ issues.push({
217
+ kind: "cmd-metachar",
218
+ severity: opts.cmdMetaPolicy === "reject" ? "high" : "warn",
219
+ ruleId: "shell.cmd-metachar",
220
+ snippet: "argument contains cmd.exe metacharacter " +
221
+ "(`&|<>^%\"',;=`)",
222
+ });
223
+ }
224
+ if (opts.argHyphenPolicy !== "allow" && input.charAt(0) === "-") {
225
+ issues.push({
226
+ kind: "arg-hyphen-leading",
227
+ severity: opts.argHyphenPolicy === "reject" ? "high" : "warn",
228
+ ruleId: "shell.arg-hyphen-leading",
229
+ snippet: "argument begins with `-` — would be parsed as an " +
230
+ "option flag by the target binary (`-rf` / `--exec` " +
231
+ "class)",
232
+ });
233
+ }
234
+
235
+ return issues;
236
+ }
237
+
238
+ function validate(input, opts) {
239
+ opts = _resolveOpts(opts);
240
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
241
+ ["maxBytes"],
242
+ "guardShell.validate", GuardShellError, "shell.bad-opt");
243
+ return gateContract.aggregateIssues(_detectIssues(input, opts));
244
+ }
245
+
246
+ function sanitize(input, opts) {
247
+ opts = _resolveOpts(opts);
248
+ if (typeof input !== "string") {
249
+ throw _err("shell.bad-input", "sanitize requires string input");
250
+ }
251
+ // Shell args can't be repaired — sanitize either passes through
252
+ // valid input or throws.
253
+ var issues = _detectIssues(input, opts);
254
+ for (var i = 0; i < issues.length; i += 1) {
255
+ if (issues[i].severity === "critical" || issues[i].severity === "high") {
256
+ throw _err(issues[i].ruleId || "shell.refused",
257
+ "guardShell.sanitize: " + issues[i].snippet);
258
+ }
259
+ }
260
+ return input;
261
+ }
262
+
263
+ function gate(opts) {
264
+ opts = _resolveOpts(opts);
265
+ return gateContract.buildGuardGate(
266
+ opts.name || "guardShell:" + (opts.profile || "default"),
267
+ opts,
268
+ async function (ctx) {
269
+ var arg = ctx && (ctx.identifier || ctx.arg);
270
+ if (arg === undefined || arg === null) return { ok: true, action: "serve" };
271
+ var rv = validate(arg, opts);
272
+ if (rv.issues.length === 0) return { ok: true, action: "serve" };
273
+ var hasCritical = rv.issues.some(function (i) {
274
+ return i.severity === "critical";
275
+ });
276
+ var hasHigh = rv.issues.some(function (i) {
277
+ return i.severity === "high";
278
+ });
279
+ if (!hasCritical && !hasHigh) {
280
+ return { ok: true, action: "audit-only", issues: rv.issues };
281
+ }
282
+ return { ok: false, action: "refuse", issues: rv.issues };
283
+ });
284
+ }
285
+
286
+ var buildProfile = gateContract.makeProfileBuilder(PROFILES);
287
+
288
+ function compliancePosture(name) {
289
+ return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
290
+ _err, "shell");
291
+ }
292
+
293
+ var _shellRulePacks = gateContract.makeRulePackLoader(GuardShellError, "shell");
294
+ var loadRulePack = _shellRulePacks.load;
295
+
296
+ module.exports = {
297
+ // ---- guard-* family registry exports ----
298
+ NAME: "shell",
299
+ KIND: "identifier",
300
+ INTEGRATION_FIXTURES: Object.freeze({
301
+ kind: "identifier",
302
+ benignBytes: Buffer.from("safe-arg-value", "utf8"),
303
+ hostileBytes: Buffer.from("safe; rm -rf /", "utf8"),
304
+ benignIdentifier: "safe-arg-value",
305
+ // Hostile: command-injection via metacharacter chain.
306
+ hostileIdentifier: "safe; rm -rf /",
307
+ }),
308
+ // ---- primitive surface ----
309
+ validate: validate,
310
+ sanitize: sanitize,
311
+ gate: gate,
312
+ buildProfile: buildProfile,
313
+ compliancePosture: compliancePosture,
314
+ loadRulePack: loadRulePack,
315
+ PROFILES: PROFILES,
316
+ DEFAULTS: DEFAULTS,
317
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
318
+ GuardShellError: GuardShellError,
319
+ };
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ /**
3
+ * guard-template — Template-injection identifier-safety primitive
4
+ * (b.guardTemplate).
5
+ *
6
+ * Detects Server-Side Template Injection (SSTI) shapes in user-input
7
+ * strings before they're rendered through any template engine. Refused
8
+ * by default at every profile — operator-untrusted input rarely
9
+ * legitimately contains template-engine syntax. KIND="identifier" —
10
+ * consumes ctx.identifier (or ctx.text).
11
+ *
12
+ * Threat catalog (engine-shape detection):
13
+ * - Jinja2 / Django / Twig / Liquid — `{{...}}` and `{%...%}`.
14
+ * Recent CVEs: CVE-2024-22195 (Jinja xml_attr filter),
15
+ * CVE-2024-26139 (Bottle), CVE-2024-23348 (Pyrogram).
16
+ * - Handlebars — `{{...}}` (same shape as Jinja; flagged together).
17
+ * - ERB / Tornado — `<%...%>` and `<%=...%>`.
18
+ * - Pug — `#{...}` interpolation, `!{...}` raw-HTML interpolation.
19
+ * - Mako / Velocity / Tornado — `${...}` interpolation.
20
+ * - Velocity directive — `#set(...)`, `#if(...)`, `#foreach(...)`.
21
+ * - AngularJS — `{{...}}` (covered by Jinja shape; legacy).
22
+ * - BIDI / null / control / zero-width universal refuse.
23
+ *
24
+ * var rv = b.guardTemplate.validate("Hello {{ name }}",
25
+ * { profile: "strict" });
26
+ * var g = b.guardTemplate.gate({ profile: "strict" });
27
+ */
28
+
29
+ var codepointClass = require("./codepoint-class");
30
+ var lazyRequire = require("./lazy-require");
31
+ var gateContract = require("./gate-contract");
32
+ var C = require("./constants");
33
+ var numericBounds = require("./numeric-bounds");
34
+ var { GuardTemplateError } = require("./framework-error");
35
+
36
+ var observability = lazyRequire(function () { return require("./observability"); });
37
+ void observability;
38
+
39
+ var _err = GuardTemplateError.factory;
40
+
41
+ // Engine-shape detectors.
42
+ var JINJA_EXPR_RE = /\{\{[\s\S]*?\}\}/;
43
+ var JINJA_STMT_RE = /\{%[\s\S]*?%\}/;
44
+ var ERB_EXPR_RE = /<%[\s\S]*?%>/;
45
+ var PUG_INTERP_RE = /[#!]\{[\s\S]*?\}/;
46
+ var DOLLAR_BRACE_RE = /\$\{[\s\S]*?\}/;
47
+ var VELOCITY_DIR_RE = /#(?:set|if|else|elseif|end|foreach|parse|include|stop)\b/i;
48
+
49
+ // ---- Profile presets ----
50
+
51
+ var PROFILES = Object.freeze({
52
+ "strict": {
53
+ bidiPolicy: "reject",
54
+ controlPolicy: "reject",
55
+ nullBytePolicy: "reject",
56
+ zeroWidthPolicy: "reject",
57
+ jinjaPolicy: "reject",
58
+ erbPolicy: "reject",
59
+ pugPolicy: "reject",
60
+ dollarBracePolicy: "reject",
61
+ velocityDirectivePolicy: "reject",
62
+ maxBytes: C.BYTES.kib(64),
63
+ maxRuntimeMs: C.TIME.seconds(2),
64
+ },
65
+ "balanced": {
66
+ bidiPolicy: "reject",
67
+ controlPolicy: "reject",
68
+ nullBytePolicy: "reject",
69
+ zeroWidthPolicy: "reject",
70
+ jinjaPolicy: "reject", // SSTI class — refused at every profile
71
+ erbPolicy: "reject",
72
+ pugPolicy: "reject",
73
+ dollarBracePolicy: "audit", // ${...} can also be JS template literal — audit
74
+ velocityDirectivePolicy: "reject",
75
+ maxBytes: C.BYTES.kib(128),
76
+ maxRuntimeMs: C.TIME.seconds(2),
77
+ },
78
+ "permissive": {
79
+ bidiPolicy: "reject", // BIDI refused at every profile
80
+ controlPolicy: "reject", // controls refused at every profile
81
+ nullBytePolicy: "reject", // null refused at every profile
82
+ zeroWidthPolicy: "reject", // zero-width refused at every profile
83
+ jinjaPolicy: "reject", // SSTI class refused at every profile
84
+ erbPolicy: "reject", // SSTI class refused at every profile
85
+ pugPolicy: "reject", // SSTI class refused at every profile
86
+ dollarBracePolicy: "audit",
87
+ velocityDirectivePolicy: "audit",
88
+ maxBytes: C.BYTES.kib(512),
89
+ maxRuntimeMs: C.TIME.seconds(2),
90
+ },
91
+ });
92
+
93
+ var DEFAULTS = Object.freeze(Object.assign({}, PROFILES["strict"], {
94
+ mode: "enforce",
95
+ }));
96
+
97
+ var COMPLIANCE_POSTURES = Object.freeze({
98
+ "hipaa": Object.assign({}, PROFILES["strict"], {
99
+ forensicSnippetBytes: C.BYTES.bytes(512),
100
+ }),
101
+ "pci-dss": Object.assign({}, PROFILES["strict"], {
102
+ forensicSnippetBytes: C.BYTES.bytes(512),
103
+ }),
104
+ "gdpr": Object.assign({}, PROFILES["balanced"], {
105
+ forensicSnippetBytes: C.BYTES.bytes(256),
106
+ }),
107
+ "soc2": Object.assign({}, PROFILES["strict"], {
108
+ forensicSnippetBytes: C.BYTES.bytes(1024),
109
+ }),
110
+ });
111
+
112
+ function _resolveOpts(opts) {
113
+ return gateContract.resolveProfileAndPosture(opts, {
114
+ profiles: PROFILES,
115
+ compliancePostures: COMPLIANCE_POSTURES,
116
+ defaults: DEFAULTS,
117
+ errorClass: GuardTemplateError,
118
+ errCodePrefix: "template",
119
+ });
120
+ }
121
+
122
+ function _detectIssues(input, opts) {
123
+ var issues = [];
124
+ if (typeof input !== "string") {
125
+ return [{ kind: "bad-input", severity: "high",
126
+ ruleId: "template.bad-input",
127
+ snippet: "template input is not a string" }];
128
+ }
129
+ if (input.length === 0) return [];
130
+ if (Buffer.byteLength(input, "utf8") > opts.maxBytes) {
131
+ return [{ kind: "input-cap", severity: "high",
132
+ ruleId: "template.input-cap",
133
+ snippet: "template input exceeds maxBytes " + opts.maxBytes }];
134
+ }
135
+
136
+ var charThreats = codepointClass.detectCharThreats(input, opts, "template");
137
+ for (var ci = 0; ci < charThreats.length; ci += 1) issues.push(charThreats[ci]);
138
+
139
+ if (opts.jinjaPolicy !== "allow") {
140
+ if (JINJA_EXPR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
141
+ issues.push({
142
+ kind: "jinja-expression", severity: "high",
143
+ ruleId: "template.jinja-expression",
144
+ snippet: "input contains `{{...}}` template-engine expression " +
145
+ "syntax — Jinja / Django / Twig / Liquid / Handlebars / " +
146
+ "AngularJS SSTI shape (CVE-2024-22195 / 26139 / 23348 " +
147
+ "class)",
148
+ });
149
+ }
150
+ if (JINJA_STMT_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
151
+ issues.push({
152
+ kind: "jinja-statement", severity: "high",
153
+ ruleId: "template.jinja-statement",
154
+ snippet: "input contains `{%...%}` template-engine statement " +
155
+ "syntax — SSTI shape",
156
+ });
157
+ }
158
+ }
159
+ if (opts.erbPolicy !== "allow" && ERB_EXPR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
160
+ issues.push({
161
+ kind: "erb-expression", severity: "high",
162
+ ruleId: "template.erb-expression",
163
+ snippet: "input contains `<%...%>` template-engine expression " +
164
+ "syntax — ERB / Tornado SSTI shape",
165
+ });
166
+ }
167
+ if (opts.pugPolicy !== "allow" && PUG_INTERP_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
168
+ issues.push({
169
+ kind: "pug-interpolation", severity: "high",
170
+ ruleId: "template.pug-interpolation",
171
+ snippet: "input contains `#{...}` or `!{...}` template-engine " +
172
+ "interpolation — Pug SSTI shape",
173
+ });
174
+ }
175
+ if (opts.dollarBracePolicy !== "allow" && DOLLAR_BRACE_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
176
+ issues.push({
177
+ kind: "dollar-brace",
178
+ severity: opts.dollarBracePolicy === "reject" ? "high" : "warn",
179
+ ruleId: "template.dollar-brace",
180
+ snippet: "input contains `${...}` interpolation — Mako / " +
181
+ "Velocity / Tornado / JS template-literal SSTI shape",
182
+ });
183
+ }
184
+ if (opts.velocityDirectivePolicy !== "allow" &&
185
+ VELOCITY_DIR_RE.test(input)) { // allow:regex-no-length-cap — input bounded by maxBytes
186
+ issues.push({
187
+ kind: "velocity-directive",
188
+ severity: opts.velocityDirectivePolicy === "reject" ? "high" : "warn",
189
+ ruleId: "template.velocity-directive",
190
+ snippet: "input contains Velocity directive (`#set` / `#if` / " +
191
+ "`#foreach` / etc.) — SSTI shape",
192
+ });
193
+ }
194
+
195
+ return issues;
196
+ }
197
+
198
+ function validate(input, opts) {
199
+ opts = _resolveOpts(opts);
200
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
201
+ ["maxBytes"],
202
+ "guardTemplate.validate", GuardTemplateError, "template.bad-opt");
203
+ return gateContract.aggregateIssues(_detectIssues(input, opts));
204
+ }
205
+
206
+ function sanitize(input, opts) {
207
+ opts = _resolveOpts(opts);
208
+ if (typeof input !== "string") {
209
+ throw _err("template.bad-input", "sanitize requires string input");
210
+ }
211
+ var issues = _detectIssues(input, opts);
212
+ for (var i = 0; i < issues.length; i += 1) {
213
+ if (issues[i].severity === "critical" || issues[i].severity === "high") {
214
+ throw _err(issues[i].ruleId || "template.refused",
215
+ "guardTemplate.sanitize: " + issues[i].snippet);
216
+ }
217
+ }
218
+ return input;
219
+ }
220
+
221
+ function gate(opts) {
222
+ opts = _resolveOpts(opts);
223
+ return gateContract.buildGuardGate(
224
+ opts.name || "guardTemplate:" + (opts.profile || "default"),
225
+ opts,
226
+ async function (ctx) {
227
+ var text = ctx && (ctx.identifier || ctx.text);
228
+ if (text === undefined || text === null) {
229
+ return { ok: true, action: "serve" };
230
+ }
231
+ var rv = validate(text, opts);
232
+ if (rv.issues.length === 0) return { ok: true, action: "serve" };
233
+ var hasCritical = rv.issues.some(function (i) {
234
+ return i.severity === "critical";
235
+ });
236
+ var hasHigh = rv.issues.some(function (i) {
237
+ return i.severity === "high";
238
+ });
239
+ if (!hasCritical && !hasHigh) {
240
+ return { ok: true, action: "audit-only", issues: rv.issues };
241
+ }
242
+ return { ok: false, action: "refuse", issues: rv.issues };
243
+ });
244
+ }
245
+
246
+ var buildProfile = gateContract.makeProfileBuilder(PROFILES);
247
+
248
+ function compliancePosture(name) {
249
+ return gateContract.lookupCompliancePosture(name, COMPLIANCE_POSTURES,
250
+ _err, "template");
251
+ }
252
+
253
+ var _tplRulePacks = gateContract.makeRulePackLoader(GuardTemplateError, "template");
254
+ var loadRulePack = _tplRulePacks.load;
255
+
256
+ module.exports = {
257
+ // ---- guard-* family registry exports ----
258
+ NAME: "template",
259
+ KIND: "identifier",
260
+ INTEGRATION_FIXTURES: Object.freeze({
261
+ kind: "identifier",
262
+ benignBytes: Buffer.from("Hello world", "utf8"),
263
+ hostileBytes: Buffer.from("Hello {{7*7}}", "utf8"),
264
+ benignIdentifier: "Hello world",
265
+ // Hostile: Jinja-shape SSTI probe.
266
+ hostileIdentifier: "Hello {{7*7}}",
267
+ }),
268
+ // ---- primitive surface ----
269
+ validate: validate,
270
+ sanitize: sanitize,
271
+ gate: gate,
272
+ buildProfile: buildProfile,
273
+ compliancePosture: compliancePosture,
274
+ loadRulePack: loadRulePack,
275
+ PROFILES: PROFILES,
276
+ DEFAULTS: DEFAULTS,
277
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
278
+ GuardTemplateError: GuardTemplateError,
279
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.7.52",
3
+ "version": "0.7.61",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:5f6f1fab-e230-4b2c-ad06-d5b9143d8fe2",
5
+ "serialNumber": "urn:uuid:a193a2a6-fe98-4457-b5e9-e5ffa6a1d0ab",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-05T22:47:38.526Z",
8
+ "timestamp": "2026-05-06T00:18:55.633Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.7.52",
22
+ "bom-ref": "@blamejs/core@0.7.61",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.7.52",
25
+ "version": "0.7.61",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.7.52",
29
+ "purl": "pkg:npm/%40blamejs/core@0.7.61",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.7.52",
57
+ "ref": "@blamejs/core@0.7.61",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]