Skip to main content

Supplemental Rules

Supplemental rules extend Bulwark’s built-in health checks with custom detections — no code required. Rules are plain-text files (YAML, JSON, or XML) that describe conditions to evaluate against the data Bulwark collects from an AD environment. Triggered rules appear in the supplemental_rule_results list in the output alongside built-in findings.

How It Works

When Bulwark completes data collection, it loads all supplemental rules from the path you specify. Each rule’s conditions are evaluated against the collected HealthCheckData. If a rule’s conditions are satisfied, it is added to the results. Rules that do not trigger are silently skipped. If a field referenced by a criterion is not present in the collected data, Bulwark attempts a live LDAP query to retrieve it (see LDAP Fallback).

Supplying Rules

CLI

# Single file
bulwark healthcheck --rules /path/to/my-rules.yaml ...

# Directory (loads all .yaml, .yml, .json, .xml files)
bulwark healthcheck --rules /path/to/rules-dir/ ...

TUI

From the main menu, select Rules — Ingest and manage supplemental rules, then type the path to a file or directory and press Apply.

Rule File Format

Each file must contain a top-level rules list. A single file may contain multiple rules.

YAML

rules:
  - id: SUPP-001
    name: Too Many Inactive Users
    severity: high
    conditions:
      match: all
      criteria:
        - field: user_account_data.number_inactive
          operator: gt
          value: "50"

JSON

{
  "rules": [
    {
      "id": "SUPP-001",
      "name": "Too Many Inactive Users",
      "severity": "high",
      "conditions": {
        "match": "all",
        "criteria": [
          { "field": "user_account_data.number_inactive", "operator": "gt", "value": "50" }
        ]
      }
    }
  ]
}

XML

<rules>
  <rule>
    <id>SUPP-001</id>
    <name>Too Many Inactive Users</name>
    <severity>high</severity>
    <conditions>
      <match>all</match>
      <criteria>
        <criterion>
          <field>user_account_data.number_inactive</field>
          <operator>gt</operator>
          <value>50</value>
        </criterion>
      </criteria>
    </conditions>
  </rule>
</rules>

Rule Fields

FieldRequiredDescription
idyesUnique identifier (e.g., SUPP-001)
nameyesShort human-readable name shown in output
descriptionnoLonger explanation of what the rule checks
categorynoGrouping label (e.g., accounts, gpo, pki)
severitynocritical, high, medium, low, info
solutionnoRecommended remediation text
conditionsyesEvaluation logic (see below)

Conditions

conditions:
  match: all        # "all" (AND) or "any" (OR)
  criteria:
    - field: <field-path>
      operator: <operator>
      value: <comparison-value>
FieldValuesDefaultDescription
matchall / anyallall = every criterion must be true (AND). any = at least one (OR)
criterialistOne or more criterion objects

Operators

OperatorDescriptionExample value
eqEqual to"0", "true", "Windows Server 2019"
neNot equal to"false"
ltLess than (numeric)"10"
gtGreater than (numeric)"50"
lteLess than or equal to"100"
gteGreater than or equal to"1"
containsString contains substring (case-insensitive)"2012"
existsField is present and non-empty/non-zero(no value needed)
count_gtList length greater than"5"
count_ltList length less than"2"
count_eqList length equals"0"
All values are strings. Numeric operators convert both sides automatically.

LDAP Fallback

If a field path cannot be resolved from collected data, Bulwark performs a live LDAP query using one of four structured notations — all prefixed with ldap..

Notation 1 — Attribute Lookup: ldap.<attr>

Searches the domain object for the attribute, falls back to subtree scan.
criteria:
  - field: ldap.ms-DS-MachineAccountQuota
    operator: gt
    value: "0"
  - field: ldap.minPwdLength
    operator: lt
    value: "14"

Notation 2 — Type Attribute Query: ldap.<type>.self.<attr>

Searches all objects of <type> and returns every value of <attr> as a list.
criteria:
  - field: ldap.user.self.msDS-SupportedEncryptionTypes
    operator: count_gt
    value: "0"

Notation 3 — Type + Filter Query: ldap.<type>.filter.<ldap-filter>

Finds objects of <type> matching the raw LDAP filter. Returns distinguished names as a list.
criteria:
  - field: ldap.computer.filter.(operatingSystem=Windows 10*)
    operator: count_gt
    value: "0"
  - field: ldap.user.filter.(adminCount=1)
    operator: count_gt
    value: "0"

Notation 4 — Raw Filter Query: ldap.filter.<ldap-filter>

Searches the entire domain with a raw LDAP filter. No type restriction.
criteria:
  - field: ldap.filter.(&(objectClass=user)(adminCount=1))
    operator: count_gt
    value: "0"

Supported Object Types

TokenLDAP Filter
user(&(objectClass=user)(objectCategory=person))
computer(objectCategory=computer)
group(objectClass=group)
gpo(objectClass=groupPolicyContainer)
ou(objectClass=organizationalUnit)
domain(objectClass=domain)
trust(objectClass=trustedDomain)
dc / domaincontrollerDomain controllers (UAC bit 8192)
pki / ca(objectClass=certificationAuthority)
template / certtemplate(objectClass=pKICertificateTemplate)
Unrecognized tokens fall back to (objectClass=<token>).

Examples

Multi-Rule YAML File

rules:
  - id: SUPP-001
    name: Excessive Inactive Users
    description: More than 50 enabled accounts have not logged in for 6 months.
    category: accounts
    severity: high
    solution: Disable or remove stale user accounts.
    conditions:
      match: all
      criteria:
        - field: user_account_data.number_inactive
          operator: gt
          value: "50"

  - id: SUPP-002
    name: DES Encryption Still In Use
    category: accounts
    severity: critical
    solution: Remove the DES encryption flag from all accounts.
    conditions:
      match: all
      criteria:
        - field: user_account_data.number_des_enabled
          operator: gt
          value: "0"

  - id: SUPP-003
    name: Domain Functional Level Below 2016
    category: hygiene
    severity: medium
    solution: Raise the domain functional level to 2016 or higher.
    conditions:
      match: all
      criteria:
        - field: domain_functional_level
          operator: lt
          value: "7"

OR Condition (XML)

<rules>
  <rule>
    <id>SUPP-020</id>
    <name>Insecure Delegation Configuration</name>
    <severity>critical</severity>
    <conditions>
      <match>any</match>
      <criteria>
        <criterion>
          <field>user_account_data.number_enabled_trusted_to_authenticate_for_delegation</field>
          <operator>gt</operator>
          <value>0</value>
        </criterion>
        <criterion>
          <field>user_account_data.number_not_aes_enabled</field>
          <operator>gt</operator>
          <value>0</value>
        </criterion>
      </criteria>
    </conditions>
  </rule>
</rules>