# Writing controls

> Profile layout, control syntax, and the check blocks that make up a rule.

Source: https://refpoint.dev/docs/rules/writing-rules

This page defines the files in a profile and the HCL blocks inside them:
manifest, control, check, precondition, alternative group, and dependency.

## Files and layout

```
my-profile/
├── profile.rpp          manifest, marks the directory as a profile root
└── controls/
    ├── ssh.rpc
    └── users.rpc        every *.rpc file in controls/ is loaded
```

| Extension | Role |
|---|---|
| `.rpp` | Profile manifest |
| `.rpc` | Control file. Files under `controls/` without this extension are ignored |

Editors do not highlight these extensions as HCL until a file type association
is added.

## Profile manifest

A profile is the unit of distribution and execution. It is a directory with a
manifest and a set of control files, and it is what `refpoint run` is given. It
carries the metadata, platform constraint, variables, and dependencies shared by
every control inside it.

```hcl
profile "linux-baseline" {
  title       = "Linux Baseline"
  description = "Server hardening baseline"
  version     = "1.2.0"
  author      = "Platform Team"

  platform = ["linux"]

  allowed_commands = ["systemctl", "getent"]

  dependency {
    path    = "../shared/iso27001-annex-a"
    include = ["^ssh-", "^audit-"]
    exclude = ["ssh-legacy-*"]
  }
}
```

| Attribute | Required | Notes |
|-----------|----------|-------|
| label | yes | Profile ID |
| `title` | yes | |
| `description` | no | |
| `version` | no | |
| `author` | no | |
| `platform` | no | OS or distro tokens, applied to every control |
| `allowed_commands` | no | Executables this profile's `command` checks need |
| `depends_on` | no | List of paths, all controls taken |
| `dependency` block | no | Path plus `include` and `exclude` patterns |
| `variable` block | no | Repeatable |

Only the `profile` block is read from this file. Any other block is rejected.
It would parse as valid HCL and have no effect, so rejection is explicit.

## Control

A control is one requirement with a stable ID, such as "SSH root login is
disabled". It groups the checks that prove the requirement, carries the impact
and tags used for scoring and filtering, and is the unit that receives a status
in the report.

```hcl
control "ssh-01" {
  title       = "SSH root login is disabled"
  description = "Direct root login over SSH removes accountability."
  impact      = 0.9
  remediation = "Set PermitRootLogin no in /etc/ssh/sshd_config and reload sshd."

  platform = ["linux"]

  tags = {
    category = "ssh"
    iso27001 = "A.8.2"
    nis2     = "21(2)(i)"
  }

  ref {
    title = "ISO/IEC 27001:2022 Annex A 8.2 Privileged access rights"
    url   = "https://example.org/policy/iso27001-a-8-2"
  }

  only_if "package" {
    name      = "openssh-server"
    installed = true
  }

  check "root-login" "sshd_config" {
    option "PermitRootLogin" {
      value = "no"
    }
  }

  any_of "time-daemon" {
    check "chrony" "service" {
      name    = "chronyd"
      running = true
    }
    check "timesyncd" "service" {
      name    = "systemd-timesyncd"
      running = true
    }
  }
}
```

| Attribute | Required | Notes |
|-----------|----------|-------|
| label | yes | Control ID |
| `title` | yes | |
| `description` | no | |
| `remediation` | no | Free text, carried into results and reports |
| `impact` | no | 0.0 to 1.0. Used for weighting and filtering |
| `tags` | no | Object of string values. A value may be a comma-separated list |
| `platform` | no | Overrides the profile-level `platform` when non-empty |
| `ref` block | no | Repeatable. `title` and `url` |
| `check` block | no | Repeatable. Two labels: name, resource type |
| `only_if` block | no | At most one. One label: resource type |
| `any_of` block | no | Repeatable. One label: group name |

Attributes and blocks outside the schema are rejected. This includes
meta-arguments such as `for_each`, `count`, and `dynamic`, which would
otherwise parse and do nothing.

## Check syntax

A check is one assertion against one resource on the target: a file, a service,
a package, a config option. It is the smallest unit evaluated, and a control
passes only when every check in it passes.

A check has two labels, a name and a resource type, and a body. The body holds
an address for addressed resource types, followed by assertions. Assertions are
defined per resource type in the [resource reference](/docs/resources).

Addressed resource type:

```hcl
check "<name>" "<resource_type>" {
  <address_attribute> = "<value>"
  <assertion>         = <value>
}
```

Label-only resource type. No address attribute. The check name is the report
label:

```hcl
check "<name>" "<resource_type>" {
  <assertion_block> "<label>" {
    <assertion> = <value>
  }
}
```

In the control above, `sshd_config` is label-only. Its assertion is the
`option "<Name>"` block, and `value` is one of that block's attributes.

| Element | Rule |
|---|---|
| Address attribute | One of `path`, `cmd`, `name`, `port`, `param`, `key`, `url`, fixed per resource type |
| Address evaluation | Evaluated with the profile's variable context. Variables may be interpolated |
| Assertions | Attributes or blocks specific to the resource type. Decoded by the matching provider |
| Reserved names | An address attribute name cannot also be an assertion attribute for that type |
| Address length | 4096 characters maximum |
| Block label length | 128 characters maximum |
| Control characters | Not permitted in addresses or labels. Both appear verbatim in logs and reports |

## Preconditions: only_if

A precondition is a check that gates the control. It decides whether the
control applies to this target at all, so a control is skipped rather than
failed on a host where it is irrelevant.

| `only_if` outcome | Control status |
|---|---|
| Passes | Control runs |
| Does not pass | `skipped`. Reason names the resource and the first failing assertion |
| Errors | `error`, not `skipped`. A broken precondition must not hide a control on every run |

## Alternatives: any_of

An alternative group holds checks for the different ways a requirement can be
met. The group counts as one check of the control and passes when any member
passes.

| Rule | Detail |
|---|---|
| Members | At least two |
| Satisfied when | Any member passes |
| Execution | Every member runs. No short-circuit. Each member's state is reported |
| Use case | A requirement with more than one acceptable implementation, such as a choice of time daemon |

## Dependencies

A dependency pulls another profile's controls into this one. It lets a shared
baseline be maintained once and reused, with `include` and `exclude` selecting
which of its controls apply.

| Rule | Detail |
|---|---|
| Path resolution | Relative to the profile directory |
| Confinement | None. `../shared-baseline` is valid. A dependency can pull in controls from outside the tree under review |
| Cycles | Detected and refused. A diamond is not a cycle |
| Order | Dependency controls first. The depending profile's own controls override on an ID conflict |
| Filtering | `include` applied first, then `exclude`. Same pattern syntax as `--control` and `--skip` |
| Platform | A dependency's profile-level `platform` is baked into each of its controls that declares none. The constraint survives a merge into a parent with a different platform |

## Parser limits

Profiles are loaded by `check` and `list` before any run. Loading must stay
cheap for hostile input.

| Limit | Value |
|-------|-------|
| Single HCL file | 4 MiB |
| Bracket nesting depth | 200 |
| Evaluated string length | 256 KiB |

The nesting limit guards the parser's per-level recursion. Exceeding the stack
is a fatal error, not a recoverable panic.