Skip to main content

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​

KeyRequiredWhat it is
sourceyesA tree-sitter Apex query; its @source capture is the tainted value.
sinkyesA tree-sitter Apex query; its @sink capture is the argument that must not be tainted.
source_capture, sink_capturenoOther capture names (defaults @source, @sink).
propagatornoA 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