|
| 1 | +# Hits-to-Rules (false-positive mitigation) |
| 2 | + |
| 3 | +Reference for the provider feature that turns Wallarm **hit** data into |
| 4 | +false-positive **suppression rules** managed as Terraform state. Describes how |
| 5 | +the code behaves; for the operator procedure see the registry guide |
| 6 | +`docs/guides/hits_to_rules.md`. |
| 7 | + |
| 8 | +## 1. Overview |
| 9 | + |
| 10 | +Wallarm detects threats and records them as **hits**. Some hits are false |
| 11 | +positives - legitimate traffic flagged as an attack. The fix is a suppression |
| 12 | +rule that tells Wallarm to stop flagging that signature or attack type at that |
| 13 | +request point and scope. |
| 14 | + |
| 15 | +Hits are **ephemeral** (they age out of the API), so the suppression config |
| 16 | +cannot be derived from live hits on every plan - it must be captured into |
| 17 | +Terraform state once and then persist independently. This feature does that: a |
| 18 | +data source reads hits and transforms them into a scope (`action`) plus grouped |
| 19 | +signatures; a gating resource ensures each request is fetched only once; and two |
| 20 | +rule resources create the actual suppressions. The pieces are wired together in |
| 21 | +the reference module `examples/hits-to-rules/`. |
| 22 | + |
| 23 | +Scope of this doc: `data.wallarm_hits`, `wallarm_hits_index`, |
| 24 | +`wallarm_rule_disable_stamp`, `wallarm_rule_disable_attack_type`, and how they |
| 25 | +compose. The HCL generator's role in the flow is summarized here; its full |
| 26 | +behavior is in `hcl-generator.md`. |
| 27 | + |
| 28 | +## 2. Model |
| 29 | + |
| 30 | +A hit carries a detection **point** (where in the request the signature |
| 31 | +matched), one or more **stamps** (numeric signature IDs), an **attack type** |
| 32 | +(`sqli`, `xss`, ...), and request metadata (`domain`, `path`, `poolid`, |
| 33 | +`attack_id`, `request_id`). Hits of the same HTTP request share `request_id`; |
| 34 | +hits of the same campaign share `attack_id`. |
| 35 | + |
| 36 | +Suppression is expressed against an **action** (the match scope: host + URL path |
| 37 | ++ optionally the application instance) and a **point**. Two rule shapes exist: |
| 38 | + |
| 39 | +- **disable_stamp** - allow one specific signature (`stamp`) at a point. |
| 40 | +- **disable_attack_type** - allow a whole attack type at a point (the only |
| 41 | + option for stampless types, see § 6). |
| 42 | + |
| 43 | +```mermaid |
| 44 | +flowchart LR |
| 45 | + H[Hits in API<br/>ephemeral] -->|request_id| DS[data.wallarm_hits] |
| 46 | + IDX[wallarm_hits_index<br/>ready + cached_request_ids] -->|gates new ids only| DS |
| 47 | + DS -->|aggregated JSON| CACHE[terraform_data.cache<br/>persists in state] |
| 48 | + CACHE -->|dedup by action_hash in HCL locals| EXP{{expand per stamp / type}} |
| 49 | + EXP --> RS[wallarm_rule_disable_stamp] |
| 50 | + EXP --> RA[wallarm_rule_disable_attack_type] |
| 51 | + DS -.->|source=rules| GEN[wallarm_rule_generator<br/>standalone .tf + moved] |
| 52 | +``` |
| 53 | + |
| 54 | +The action scope produced by the data source uses the **exact same schema** as |
| 55 | +every `wallarm_rule_*` resource (`resourcerule.ScopeActionSchema()`), so |
| 56 | +`data.wallarm_hits.<x>.action` can be passed straight into a rule's `action` |
| 57 | +argument. |
| 58 | + |
| 59 | +## 3. Elements |
| 60 | + |
| 61 | +| Element | Kind | Responsibility | |
| 62 | +|---|---|---| |
| 63 | +| `data.wallarm_hits` | data source | Fetch hits for one `request_id`, validate their action is consistent, optionally expand by attack campaign, compute the action scope + hash, group signatures per point, emit the compact `aggregated` payload. | |
| 64 | +| `wallarm_hits_index` | resource | Persistent per-client index of already-fetched request IDs. Exposes `ready` and `cached_request_ids` so HCL can gate the data source to new IDs only. Holds no API state (`Delete` is state-only). | |
| 65 | +| `wallarm_rule_disable_stamp` | resource | Create/read/update/delete a `disable_stamp` rule (allow one `stamp` at a `point`/`action`). | |
| 66 | +| `wallarm_rule_disable_attack_type` | resource | Same lifecycle for a `disable_attack_type` rule (allow one `attack_type`). | |
| 67 | +| `terraform_data.cache` | Terraform built-in (module) | Persists each request's `aggregated` output in state with `ignore_changes`, so rules survive after hits expire. Not a provider resource. | |
| 68 | +| `wallarm_rule_generator` (`source = "rules"`) | resource | Optional: writes standalone `.tf` files plus `moved` blocks from the cached rules, for migration off `for_each`. Detailed in `hcl-generator.md`. | |
| 69 | + |
| 70 | +## 4. Behavior |
| 71 | + |
| 72 | +### 4.1 Data source read pipeline |
| 73 | + |
| 74 | +`dataSourceWallarmHitsRead` runs in phases: |
| 75 | + |
| 76 | +1. **Fetch direct hits** (`fetchDirectHits`) - `HitRead` filtered by |
| 77 | + `client_id` + `request_id`, time range, and noise filters (see § 6.3). |
| 78 | + Empty result -> `setEmptyHitsState` (empty `action`, `aggregated` with empty |
| 79 | + arrays, `hits_count = 0`) and return; this is what makes a re-fetch of |
| 80 | + expired hits destroy rules, and is why fetching is gated (§ 4.2). |
| 81 | +2. **Action-consistency check** - all direct hits must share `domain`, `path`, |
| 82 | + and `poolid`; otherwise the read fails with an `inconsistent hit data` error. |
| 83 | + The first hit is the reference (`refDomain`/`refPath`/`refPoolID`). |
| 84 | +3. **Attack expansion** (mode `attack` only) - |
| 85 | + `fetchRelatedHitsByAttackIDs` collects unique `attack_id`s, pages `HitRead` |
| 86 | + (batch `HitFetchBatchSize`) filtered to `attack_types`, keeps only hits whose |
| 87 | + action matches the reference, then `mergeHits` dedupes by hit ID. When |
| 88 | + `refPath` is `[multiple]`, matching is on `domain` + `poolid` only. |
| 89 | +4. **Build action + hashes** - `buildActionFromHit` turns |
| 90 | + domain/path/poolid into action conditions; `ConditionsHash` -> `action_hash`, |
| 91 | + `ActionDirName` -> `action_dir_name`. |
| 92 | +5. **API validation** - if the hit ID has >=2 elements, `ActionReadByHitID` |
| 93 | + fetches the API's own conditions and compares hashes. A fetch error is a |
| 94 | + `[WARN]` (read proceeds); a **hash mismatch is a hard error** with a |
| 95 | + full condition-by-condition diff. |
| 96 | +6. **Group + aggregate** - `groupHitsForRules` groups by `point_hash` + |
| 97 | + attack type, unions stamps, drops hits whose type is not in `attack_types`. |
| 98 | + `buildAggregatedJSON` filters by `rule_types`, truncates hashes to 16 chars, |
| 99 | + and marshals `{action_hash, action, groups[]}`. |
| 100 | + |
| 101 | +### 4.2 Gating and persistence |
| 102 | + |
| 103 | +`wallarm_hits_index.ready` is `false` on create and `true` afterwards, made |
| 104 | +known at plan time by `hitsIndexCustomizeDiff` (`SetNew`). `cached_request_ids` |
| 105 | +is empty on create and, on update, the diff **preserves the old state value** so |
| 106 | +newly added IDs read as uncached during plan - that is the signal the module |
| 107 | +uses to fetch only new IDs (`ready ? new_ids : all_ids`). `Create`/`Read`/ |
| 108 | +`Update` then sync `cached_request_ids` to the configured `request_ids`. |
| 109 | + |
| 110 | +On first apply with request IDs, `ready = false` fetches everything, caches it, |
| 111 | +and creates rules in a single apply. Subsequent applies fetch only new IDs. |
| 112 | +Deduplication by `action_hash` happens in the module's HCL locals (stamps |
| 113 | +unioned via `distinct(flatten(...))`), so identical rules from different request |
| 114 | +IDs collapse to one resource - this prevents drift loops where the API would |
| 115 | +merge duplicate rules. |
| 116 | + |
| 117 | +### 4.3 Rule create semantics |
| 118 | + |
| 119 | +Both rule resources build a `wallarm.ActionCreate` and call `HintCreate`: |
| 120 | + |
| 121 | +- `Type` = `"disable_stamp"` / `"disable_attack_type"`. |
| 122 | +- `VariativityDisabled` = **`true`** (hardcoded) - these rules are exact, not |
| 123 | + variative. |
| 124 | +- `Validated` = `false`. |
| 125 | +- `action` (scope) is expanded from the `action {}` blocks |
| 126 | + (`ExpandSetToActionDetailsList`); `point` from the `point` attribute |
| 127 | + (`ExpandPointsToTwoDimensionalArray`). |
| 128 | +- Resource ID is `clientID/actionID/ruleID`; `rule_id`, `action_id`, |
| 129 | + `rule_type` are set from the response. |
| 130 | + |
| 131 | +Update routes through `resourcerule.Update(apiClient, WithStamp | |
| 132 | +WithAttackType)`, Delete through `resourcerule.Delete`, Import through |
| 133 | +`resourcerule.Import("<type>")`, and both use |
| 134 | +`resourcerule.ActionScopeCustomizeDiff`. |
| 135 | + |
| 136 | +### 4.4 Action-condition construction |
| 137 | + |
| 138 | +`buildActionFromHit` + `locationToConditions` port the Ruby |
| 139 | +`LocationToConditions`: |
| 140 | + |
| 141 | +- **instance** condition emitted when `include_instance` is true and |
| 142 | + `poolid != 0` (`{instance: <poolid>}`, `equal`, empty value). |
| 143 | +- **HOST** header always `iequal` to `domain`. |
| 144 | +- **path** split on `/` into `equal` segment conditions, terminated by an |
| 145 | + `absent` condition one index past the last segment (fixes chain length). |
| 146 | +- final path segment splits into `action_name` + `action_ext` on the **first** |
| 147 | + dot, matching the API (`archive.tar.gz` -> name `archive`, ext `tar.gz`); no |
| 148 | + dot -> `action_name` = segment and `action_ext` `absent`. **Known bug (R-002):** |
| 149 | + the code currently splits on the *last* dot (`actionNameExtConditions` here and |
| 150 | + `parseLastSegment` on the `action_path` side); the fix is `strings.Index` at |
| 151 | + both sites. See `action.md §4.3`. |
| 152 | +- root path `/` -> `action_name` empty + `path[0]` absent. |
| 153 | +- `path == "[multiple]"` -> host-only wildcard scope (no path/action_name/ |
| 154 | + action_ext conditions). |
| 155 | + |
| 156 | +### 4.5 Failure and edge modes |
| 157 | + |
| 158 | +| Situation | Handling | |
| 159 | +|---|---| |
| 160 | +| Direct hits disagree on domain/path/poolid | Hard error (`inconsistent hit data`). | |
| 161 | +| Provider action hash != API hash | Hard error with per-condition diff. | |
| 162 | +| `ActionReadByHitID` call fails | `[WARN]`, read proceeds unvalidated. | |
| 163 | +| No hits (or all expired) | Empty state; downstream rules destroyed if not cached. | |
| 164 | +| Stampless attack type (`xxe`, `invalid_xml`) | No stamps; only `disable_attack_type` rules. With `rule_types=["disable_stamp"]` they yield nothing. | |
| 165 | +| `nil` stamps slice | Coerced to `[]` before marshal (JSON `null` breaks HCL). | |
| 166 | + |
| 167 | +## 5. Parameters |
| 168 | + |
| 169 | +### 5.1 `data.wallarm_hits` |
| 170 | + |
| 171 | +| input | type | req? | default | notes | |
| 172 | +|---|---|---|---|---| |
| 173 | +| `client_id` | int | optional | provider default | tenant scope; resolved via `retrieveClientID`. | |
| 174 | +| `request_id` | string | **required** | - | the request whose hits to fetch. | |
| 175 | +| `mode` | string | optional | `request` | `request` \| `attack` (validated). | |
| 176 | +| `attack_types` | list(string) | optional | 16 default types (§ 6.1) | filter; in `attack` mode also limits what is fetched. | |
| 177 | +| `rule_types` | list(string) | optional | both | `disable_stamp` \| `disable_attack_type` (validated). | |
| 178 | +| `include_instance` | bool | optional | `true` | include `instance`/poolid in action scope. | |
| 179 | +| `time` | list(int), max 2 | optional | [6 months ago, now] | `[from, to]` unix timestamps. | |
| 180 | +| `action` | set(block) | optional/computed | computed from hits | rule-compatible action scope. | |
| 181 | + |
| 182 | +Computed outputs: `action_hash` (16-char-truncated in keys/aggregated, full |
| 183 | +SHA256 in the `action_hash` attribute), `action_dir_name`, `action_conditions` |
| 184 | +(type/point/value list), `aggregated` (JSON, see § 6.4), `hits_count`, and |
| 185 | +`hits` (per-hit detail: `id`, `type`, `ip`, `statuscode`, `time`, `value`, |
| 186 | +`stamps`, `stamps_hash`, `point`, `point_wrapped`, `point_hash`, `poolid`, |
| 187 | +`attack_id`, `block_status`, `request_id`, `domain`, `path`, `protocol`, |
| 188 | +`known_attack`, `node_uuid`). |
| 189 | + |
| 190 | +### 5.2 `wallarm_hits_index` |
| 191 | + |
| 192 | +| attribute | type | req? | notes | |
| 193 | +|---|---|---|---| |
| 194 | +| `client_id` | int | optional | tenant scope. | |
| 195 | +| `request_ids` | set(string) | **required** | IDs to track. | |
| 196 | +| `ready` | bool | computed | `false` on create, `true` after. | |
| 197 | +| `cached_request_ids` | set(string) | computed | mirrors `request_ids` after apply; old value preserved during plan. | |
| 198 | + |
| 199 | +ID: `hits_index_<client_id>`. `Delete` clears the ID only (no API call). |
| 200 | + |
| 201 | +### 5.3 `wallarm_rule_disable_stamp` / `wallarm_rule_disable_attack_type` |
| 202 | + |
| 203 | +| input | type | req? | notes | |
| 204 | +|---|---|---|---| |
| 205 | +| `stamp` (stamp rule) | int | **required** | `>= 1`. | |
| 206 | +| `attack_type` (attack-type rule) | string | **required** | one of § 6.2 (includes `any`). | |
| 207 | +| `action` | set(block) | optional+computed | rule scope (`ScopeActionSchema`). | |
| 208 | +| `point` | list(list(string)) | **required**, ForceNew | detection point (`defaultPointSchema`). | |
| 209 | +| `comment`, `active`, `set`, `title` | common | optional | `commonResourceRuleFields`; `comment` defaults to `"Managed by Terraform"`. | |
| 210 | +| `variativity_disabled` | bool | - | forced `true` at create. | |
| 211 | + |
| 212 | +Computed: `rule_id`, `action_id`, `rule_type`. Import ID and lifecycle helpers |
| 213 | +per § 4.3. |
| 214 | + |
| 215 | +## 6. Reference data |
| 216 | + |
| 217 | +### 6.1 Data-source default attack-type filter (16) |
| 218 | + |
| 219 | +`xss`, `sqli`, `rce`, `ptrav`, `crlf`, `redir`, `nosqli`, `ldapi`, `scanner`, |
| 220 | +`mass_assignment`, `ssrf`, `ssi`, `mail_injection`, `ssti`, `xxe`, `invalid_xml` |
| 221 | +(`defaultAllowedAttackTypes`). |
| 222 | + |
| 223 | +### 6.2 `disable_attack_type.attack_type` allowed values (17) |
| 224 | + |
| 225 | +The 16 above plus `any` (the resource's `StringInSlice`; note `any` is not in |
| 226 | +the data-source default filter). |
| 227 | + |
| 228 | +### 6.3 Hit fetch filters |
| 229 | + |
| 230 | +| filter | direct | attack-related | |
| 231 | +|---|---|---| |
| 232 | +| `NotType` | `warn`, `infoleak` | - | |
| 233 | +| `Type` (allowlist) | - | `attack_types` | |
| 234 | +| `NotState` | `falsepositive` | `falsepositive` | |
| 235 | +| `NotExperimental` / `NotAasmEvent` | yes | yes | |
| 236 | +| `NotWallarmScanner` | - | yes | |
| 237 | +| batch / order | `HitFetchBatchSize`, `time` desc | same, paged by offset | |
| 238 | + |
| 239 | +`HitFetchBatchSize = 500` (`constants.go`). |
| 240 | + |
| 241 | +### 6.4 `aggregated` JSON shape |
| 242 | + |
| 243 | +```json |
| 244 | +{ |
| 245 | + "action_hash": "<16 hex>", |
| 246 | + "action": [ { "type": "...", "value": "...", "point": { "...": "..." } } ], |
| 247 | + "groups": [ |
| 248 | + { "key": "<point_hash16>_<attack_type>", "point": [["header","HOST"]], |
| 249 | + "stamps": [6961], "attack_type": "sqli", "disable_attack_type": true } |
| 250 | + ] |
| 251 | +} |
| 252 | +``` |
| 253 | + |
| 254 | +`groups` are filtered by `rule_types`: `stamps` populated only when |
| 255 | +`disable_stamp` is requested; `disable_attack_type` true only when that type is |
| 256 | +requested and an attack type is present. A group with neither is dropped. |
| 257 | + |
| 258 | +### 6.5 Hashes |
| 259 | + |
| 260 | +`action_hash` = Ruby-compatible `resourcerule.ConditionsHash` (SHA256 of sorted |
| 261 | +conditions); `point_hash` = `resourcerule.PointHash`. Both are truncated to 16 |
| 262 | +hex chars where used as `for_each`/group keys. |
| 263 | + |
| 264 | +## 7. References |
| 265 | + |
| 266 | +- `docs/guides/hits_to_rules.md` - operator how-to (the procedure). |
| 267 | +- `examples/hits-to-rules/` - the reference module wiring these together. |
| 268 | +- `hits.md` - domain notes on hits and the FP workflow. |
| 269 | +- `rules-core.md`, `action.md`, `point.md` - |
| 270 | + Action/Condition/Hint model, action path-expansion, point chaining. |
| 271 | +- `hcl-generator.md` - full `wallarm_rule_generator` behavior. |
0 commit comments