Compound (taint) custom rules
A compound rule matches a data flow, not a single line: it fires only when the Apex taint engine shows a value from your source pattern reaching your sink pattern in the same file.
vulkro-sf rules validate .vulkro-sf/rules/param-soql.yaml
vulkro-sf scan ./force-app
Rules in .vulkro-sf/rules/ (project) or ~/.vulkro/rules/ (user) load automatically on every scan.
When to use one
Use a compound rule when the risk depends on where data comes from: a request parameter reaching dynamic SOQL, a sensitive field reaching a debug log or callout, user input reaching a DOM write. If a construct is always wrong (a banned method, a forbidden metadata value), a simple regex, xml or apex-ast rule is enough.
Example
rule_id: SF-CUSTOM-PARAM-SOQL-001
name: "Request parameter reaches dynamic SOQL"
description: "A page parameter flows into Database.query"
severity: high
signal: custom-param-soql
language: apex
match:
type: compound
compound:
# @source captures the tainted local variable
source: '(local_variable_declaration (variable_declarator name: (identifier) @source value: (method_invocation name: (identifier) @g (#eq? @g "get"))))'
# @sink captures the argument of the dangerous call
sink: '(method_invocation object: (identifier) @o name: (identifier) @m arguments: (argument_list (identifier) @sink) (#eq? @o "Database") (#eq? @m "query"))'
message: "Request input {0} reaches Database.query({1})."
remediation: "Bind the value with a :bind variable, or escape it with String.escapeSingleQuotes."
This reports custom-param-soql when a value read with .get(...) (for example ApexPages.currentPage().getParameters().get('q')) flows into Database.query. The rule inherits the taint engine's sanitizer handling: if the value passes through String.escapeSingleQuotes first, the finding is still reported but at lower confidence.
The parts of match.compound
| Key | Required | What it is |
|---|---|---|
source | yes | A tree-sitter Apex query; its @source capture is the tainted value. |
sink | yes | A tree-sitter Apex query; its @sink capture is the argument that must not be tainted. |
source_capture, sink_capture | no | Other capture names (defaults @source, @sink). |
propagator | no | A method name the flow must pass through (the sink value was assigned from a call to it). Omit it to accept any path. |
In message, {0} is the source identifier and {1} the sink identifier. Compound rules run on .cls and .trigger files.
Tips: keep the source narrow enough that constants are not marked tainted; keep the sink specific to the dangerous call; write the message so it says what is wrong and what to do.
Importing existing queries
vulkro-sf rules import-checkmarx <export.xml> [--output <file>] translates an Apex query export that names a source and a sink into a compound rule (a sink-only query becomes a regex rule). Only the endpoints are carried over: intermediate sanitizer chains are not, so review the output before you use it.
Rules are data
Rules cannot run commands, open network connections or read files outside the project. Unknown YAML keys are rejected.
See also: vulkro-sf rules, Apex detectors