Skip to main content

Custom rule packs

Vulkro ships a large built-in detector set. You can also add your own patterns without waiting on a release. Custom rules are written in YAML and run alongside the built-in detectors on every scan. Use them for organisation-specific concerns: an internal token format, a banned API, a framework convention your team enforces.

Two pattern dialects

Every rule sets kind, which decides how pattern is read:

  • kind: regex (the default): pattern is a Rust regex crate pattern, applied line by line. It matches raw text, so it is cheap and good for secret formats and simple banned-API checks. The Rust regex crate has no lookaround and no backreferences.
  • kind: ast: pattern is a tree-sitter S-expression query compiled against the language grammar. It matches at the AST level, not the text level, so a comment or a string literal containing the same text does not fire. An ast rule needs exactly ONE entry in languages, because node names differ per grammar (JavaScript call_expression versus Python call). To cover several languages, write one rule each.

languages filters which files a rule is applied to, and for ast it also selects the grammar. There is no file-glob field: scope by language.

Custom rules accept only python (py), javascript (js) and typescript (ts). Any other value, including go, is a compile error for that rule: it is skipped with a warning on stderr and contributes nothing. The scanner itself covers more languages than custom rules can target.

vulkro-rules.yaml
rules:
# regex dialect: an internal token format committed to source.
- id: corp-internal-token
languages: [javascript, typescript, python]
kind: regex
pattern: "intsk_[A-Za-z0-9]{32}"
message: "Internal corp token committed to source"
remediation: "Rotate the token and read it from an environment variable instead."
severity: critical
owasp_category: SecurityMisconfiguration
confidence: high

# ast dialect: a real eval() CALL, not the word "eval" in a comment.
- id: deny-eval-js
languages: [javascript]
kind: ast
pattern: '(call_expression function: (identifier) @fn (#eq? @fn "eval"))'
message: "eval() call: arbitrary code execution sink"
remediation: "Parse the input instead, for example JSON.parse for JSON-shaped data."
severity: high
owasp_category: BrokenObjectPropertyAuth
confidence: medium

Both examples above were run against the scanner before publication and each produces exactly one finding on a matching fixture.

Field reference

FieldRequiredNotes
idyesUnique across every rule file a scan loads. Duplicates are a load-time error.
languagesfor astList of language names. Filters files; selects the grammar for ast.
kindnoregex (default) or ast.
patternyesA regex string, or a tree-sitter S-expression when kind: ast. A plain string, never a nested block.
messageyesShown on the finding, prefixed with [<id>].
remediationyesWhat to do next.
severitynocritical, high, medium (default), low, info.
owasp_categorynoAn OwaspCategory variant name, for example BrokenObjectPropertyAuth. Defaults to SecurityMisconfiguration.
confidencenohigh, medium (default), low.
cwenoA CWE number.

A rule that fails to parse is reported on stderr and then SKIPPED. A malformed file yields a warning and zero findings rather than an error exit. Check stderr after editing a rule file, and confirm the rule fires on a fixture before trusting a clean result.

Duplicate rule IDs are an error at load time, so keep IDs unique across every file that a scan loads.

Where rules come from

A scan merges rules from these sources:

  • A vulkro-rules.yaml file at the project root is auto-discovered. No flag is needed.
  • vulkro scan . --rules <PATH> adds an extra rule file or a directory of rule files for that run.
  • Installed rule packs under ~/.vulkro/rule-packs/<author>/<name>/<version>/ participate in every subsequent scan.

Test a rule locally

Point a scan at a small fixture project and iterate until each rule fires where you expect and stays quiet where you do not:

vulkro scan ./test-project --rules ./my-rules.yaml

Keep a directory of positive and negative fixtures next to your rules so you can re-run them as a regression check when you edit a pattern.

Rule packs from the registry

A rule pack is a versioned bundle of rules published under an <author>/<name> id. Manage packs with vulkro rules:

vulkro rules list # available packs in the registry index
vulkro rules add acme/api-security # fetch, verify signature, install
vulkro rules installed # what is installed locally
vulkro rules verify acme/api-security # re-check the on-disk signature
vulkro rules remove acme/api-security # uninstall every version
vulkro rules update # refresh the registry index

Packs are signed, and add installs a pack only after its signature verifies against the trust roots the binary ships with. verify re-checks an installed pack at any time. Pin a version with vulkro rules add acme/[email protected]; the default is the latest published version.

Reuse existing Semgrep rules

If you already maintain Semgrep rules, convert one into Vulkro's format:

vulkro rules import-semgrep my-semgrep-rules.yaml

The importer writes a vulkro-rules.yaml next to the input, which a later vulkro scan auto-discovers with no extra flag. Translation is best-effort: simple patterns become regexes, and compound shapes (pattern-either, pattern-not, patterns:) are skipped with a warning per rule, so review the output before relying on it. Exit code is 0 on success, 2 on a read or parse error.