class

Krikri::TemplateActionPlugin

Inherits Krikri::ActionPlugin < Reference < Object

Constants

FOR_TUPLE_PARENS = /(\{%-?\s*for\s+)\(([^)]+)\)(\s+in\s+)/

{% for (key, value) in dict.items() %} - the idiomatic real- Jinja2 way to iterate a dict's key/value pairs (mysql_hardening's own hardening.cnf.j2 writes it exactly this way). Only the parens around the loop variables need stripping - the vendored crinja fork's .items() is a real method on Hash values (see lib/crinja/src/runtime/python_hash_methods.cr), so .items() itself is left alone and evaluated for real rather than textually stripped. Was NOT always true: an earlier FOR_ITEMS_METHOD regex used to strip .items() out entirely and rely on Crinja's bare {% for k, v in dict %} already yielding (key, value) pairs - a real deviation from Python/Jinja2 (where a bare dict for-loop iterates keys only) that happened to produce the right pairs, but silently broke .items() | sort (jtyr.nsswitch's own nsswitch. conf.j2: {% for key, val in nsswitch_config.items() | sort %}) by sorting the raw dict instead of its item tuples. Removed once .items() support landed in the fork - verified live against both jtyr.nsswitch (.items() | sort) and jtyr.motd (.items() alone, item.motd.j2), byte-for-byte identical to real Ansible.

INLINE_TERNARY = /\ \{\{ # opening {{ ( # capture the whole expression (?:[^}\n]*?) # lazy: up to the ternary \s+if\s+ # the ` if ` keyword (?:[^}\n]*?) # condition (lazy) (?:\s+else\s+ # the ` else ` keyword (optional) (?:[^}\n]*?))? # else branch (lazy, optional) ) \}\} # closing }} /x

Rewrites Jinja2 inline conditional expressions {{ A if C else B }} into the Crinja-parseable {{ C | ternary(A, B) }} form. This is real Jinja2 (used by dev-sec os_hardening), which Crinja 0.9.0 cannot parse. Only {{ }} expression blocks are touched; {% %} statement blocks are left as-is.

Regex-based (not manual char indexing): matches a {{ ... }} block and rewrites an inline A if C else B ternary within it. Each match keeps the if / else as the top-level separator, so an operand that is itself a parenthesized ternary ((B if C2 else D)) is handled naturally by the nested ( ... ) captures. The else ... branch is optional - real Jinja2 permits {{ A if C }} on its own (renders as empty/Undefined when C is false; konstruktoid-hardening's sshd_config.j2 does this throughout, e.g. {{ 'Ciphers ' ~ sshd_ciphers | join(',') if sshd_ciphers }} to omit the whole config line entirely when the list is empty). #rewrite_ternary_expr treats a missing else branch as ''. [^}\n] (not just [^}]) in every lazy segment below is load- bearing, not cosmetic: a real inline ternary is always written on one line, but [^}]*? alone also matches newlines, so on a large template with sparse/mismatched {/} (mrlesmithjr.netdata's own 5934-line netdata.conf.j2 - only 26 {{ and 37 bare } total in the whole file, zero of them an actual ternary) each of the 26 {{ candidates would lazily scan for the next literal } ACROSS THE REST OF THE FILE, and the nested optional else group multiplies that against every already-scanned position - PCRE2's JIT match-time stack (a separate resource from its compile-time stack, and unrelated to true catastrophic backtracking) overflows ("Regex match error: JIT stack limit reached"), crashing the template: task outright on a file that never needed rewriting at all. Excluding \n bounds every candidate scan to a single line, matching how these templates are actually written and eliminating the cross-file scan entirely.

JOIN_METHOD = /("(?:[^"\\\n]|\\.)*")\s*\.join\(\s*([^)\n]*?)\s*\)/

Method-call .join( form that real Jinja2 permits but Crinja's parser rejects: {{ "SEP".join(LIST) }} is the standard sep.join(list) idiom (dev-sec os_hardening's securetty template uses it). Rewritten into the equivalent LIST | join("SEP") filter, which Crinja supports. $1 = the sep string literal, $2 = the list expression being joined. [^"\\] excludes \n too (not just cosmetic - see INLINE_TERNARY's own comment above for the general shape of this bug): a template with an ODD number of " characters total has no valid second quote to close a literal at all, so the unbounded (?:[^"\\]|\\.)* tries every possible split of the REST OF THE FILE between its two alternatives looking for one - mrlesmithjr.netdata's own 5934-line netdata.conf.j2 has exactly one stray " in the whole file, and this pattern alone (not INLINE_TERNARY, initially suspected first) was the one that actually overflowed PCRE2's JIT match-time stack. Real quoted string literals here are always single-line.

SPLIT_METHOD = /([A-Za-z_]\w*(?:\.[A-Za-z_]\w*|\[[^\]]*\])*)\.split\(([^)]*)\)(?:\[(\d+)\])?/

Method-call .split(...) - real Python's own str.split() method (not a Jinja2 filter - Jinja2 exposes native object methods directly), rejected by Crinja's parser the same way .items() was. dev-sec apache_hardening's own httpd.conf.j2 uses it to pick the minor version out of an already-parsed apache_version string: {% if apache_version.split('.')[1] == '4' %}. Rewritten to VAR | split(ARGS) (see jinja_filters.cr's own :split filter). $1 = the dotted/bracketed variable expression being split (apache_version, _apache_version.stdout, ...), $2 = the raw argument text (possibly empty, for Python's own no-arg whitespace- split form), $3 = an optional trailing literal numeric index (apache_version.split('.')[1] - split()'s result is almost always indexed immediately). Crinja can parse (EXPR).1 (dot- numeric indexing on a parenthesized expression) but NOT (EXPR)[1] (bracket indexing on one) - confirmed by direct testing, not assumed - so a captured trailing [N] is rewritten to .N rather than carried through as-is; #rewrite_inline_ternaries's own gsub call is responsible for making that substitution (see below).

Group 1 alternates .ident/[...] suffixes in any order - real bug found benchmarking githubixx.ansible_role_wireguard's own wg.conf.j2: the old pattern only allowed dot-segments before any bracket-index suffix (foo.bar[0]), so hostvars[host]. wireguard_address.split('/')[0] (a bracket, THEN more dots) never matched starting from "hostvars" at all - the regex engine instead found a match starting mid-expression, from "wireguard_address" alone, silently leaving the "hostvars[host]." prefix untouched and producing the syntactically invalid "hostvars[host].(wireguard_ address | split('/', 0))" (a stray .( Crinja's own parser rejects outright: "Expected IDENTIFIER, got LEFT_PAREN").

TAG_IF_ELIF = /\{%(-|\+?)\s*(if|elif)\s+(.*?)\s*(-|\+?)%\}/

A {% if EXPR %}/{% elif EXPR %} statement tag - $1/$4 are the optional whitespace-control markers, either the trim - or the keep + (both preserved as-is on rewrite - the + must survive so the vendored Crinja fork's native + handling still sees it; the + forms used to be pre-stripped out of the whole template, see #render_template's comment for why that's gone), $2 the keyword, $3 the condition. Used to find the same real-Jinja2 infix in/not in operator Crinja can't parse (see #rewrite_in_expr) when it's used directly in a statement condition rather than nested inside an inline ternary's own condition (already handled separately, since that one lives inside a {{ }} block). Deliberately does NOT match {% for %} - for x in list is valid Crinja syntax on its own and must never be touched.

Instance methods

execute

Execute action on controller Returns: modified params to send to remote plugin, or nil if action failed

Source