# Custom detections: Specification

Custom detections let admins write their own detection rules. Use them to identify browser-based threats specific to your organisation.

Rules are written in YAML. A configuration file may contain one or more rules, separated by --- (standard YAML multi-document syntax). Push validates the configuration against this specification when you save a custom detection.

## Rule structure

Each rule is a YAML document with the following top-level keys:

| Key  | Required  | Description  |
|  --- | --- | --- |
| `input` | Yes | The data source the rule operates on. |
| `metadata` | Yes | Indicator configuration for the rule. |
| `conditions` | Yes | Logic that determines when the rule fires. |
| `description` | No | A note for your own reference. Not used at runtime. |
| `min_spec` | No | Minimum specification version required to evaluate this rule. |
| `extension_version_constraints` | No | Restrict the rule to specific extension versions. |


## input

One of:

- `dom_content` — Match against the page DOM (CSS selectors, HTML comments, page URL, document title, JavaScript-set cookies).
- `web_request` — Match against outgoing HTTP requests.
- `web_response` — Match against incoming HTTP responses.


## metadata

| Key  | Required  | Description  |
|  --- | --- | --- |
| `indicator` | Yes | An `ENUM_STYLE` string labelling the specific indicator matched by this rule. Must match `^[A-Z0-9_]+$`. You may use any value, e.g. `TORRENT_MAGNET_LINK`. |


## conditions

Conditions define when the rule fires. The structure determines how they are combined:

- A **map** (key-value pairs at the same level) combines conditions with logical **AND** — all must match.
- A **list** combines conditions with logical **OR** — any must match.


These can be nested to express more complex logic. The following example requires method AND request_url, with request_url being evaluated as (path endswith /submit.php):


```yaml
conditions:
  method: POST
  request_url:
    path|endswith: /submit.php
```

## DOM content rules (input: dom_content)

| Key  | Description  |
|  --- | --- |
| `css_selectors` | Match elements in the page DOM. |
| `html_comments` | Match HTML comments in the document. |
| `url` | Match the page URL (`window.location.href`). See URL conditions. |
| `document_title` | Match against `document.title`. |
| `document_cookies` | Match cookies accessible via `document.cookie` (key-value test). |


### CSS selectors

A selector entry can be a plain string (used directly with `querySelector()`), or a map with the following keys:

| Key  | Description  |
|  --- | --- |
| `selector` | CSS selector using `querySelector()` — matches the first element. |
| `selector_all` | CSS selector using `querySelectorAll()` — matches all elements. |
| `text_content` | Test the element's `textContent`. |
| `text_nodes` | Test the concatenated value of child text nodes. |
| `inner_html` | Test the element's `innerHTML`. |
| `outer_html` | Test the element's `outerHTML`. |
| `condition` | For `selector_all` only. Use `all` to require every matched element to pass the test. Default is `any`. |



```yaml
# Match any <a> inside a <p> that contains "Create one!"
css_selectors:
  selector: p > a
  text_content|includes: Create one!
```


```yaml
# All <script> elements must contain "atob("
css_selectors:
  selector_all: script
  text_content|includes: atob(
  condition: all
```

### HTML comments

Match against HTML comment content. A plain string checks for exact equality. Use a named key with a modifier for other tests:


```yaml
# Match either comment (OR)
html_comments:
  - ex1|includes: example text
  - ex2|startswith: another example

# Both comments must match (AND)
html_comments:
  ex1|includes: example text
  ex2|startswith: another example
```

## HTTP request rules (input: web_request)

| Key  | Description  |
|  --- | --- |
| `method` | HTTP method, e.g. `GET`, `POST`. |
| `request_url` | Match the request URL. See URL conditions. |
| `tab_url` | Match the URL of the browser tab at the time of the request. See URL conditions. |
| `type` | Resource type, e.g. `script`, `xmlhttprequest`, `main_frame`. |
| `body` | Raw request body. |
| `form_data` | Parsed form data for `multipart/form-data` and `application/x-www-form-urlencoded` requests (key-value test). |
| `request_headers` | HTTP request headers (key-value test). |


> **Note:** A rule that tests `request_headers` cannot also test `body`. These come from different browser extension events.



```yaml
input: web_request
metadata:
  indicator: SUSPICIOUS_FORM_SUBMISSION
conditions:
  method: POST
  request_url:
    path|endswith: /collect
  form_data:
    - name: email
      value|exists: true
```

## HTTP response rules (input: web_response)

| Key  | Description  |
|  --- | --- |
| `method` | HTTP method. |
| `request_url` | Match the request URL. See URL conditions. |
| `tab_url` | Match the URL of the browser tab. See URL conditions. |
| `type` | Resource type. |
| `status_code` | HTTP response status code. |
| `response_headers` | HTTP response headers (key-value test). |
| `cookies` | Cookies from `set-cookie` response headers (key-value test). |



```yaml
input: web_response
metadata:
  indicator: TRACKING_COOKIE_SET
conditions:
  cookies:
    - name: tracking_id
      value|exists: true
```

## URL conditions

A URL condition can be:

1. A plain string — tested against the full URL (exact equality after trimming, or with a modifier).
2. A map targeting specific URL components.


| Component  | Description  | Example value  |
|  --- | --- | --- |
| `href` | Full URL | `https://a.b.example.co.uk:8080/login?x=y#h` |
| `scheme` | Protocol, without `:` | `https` |
| `origin` | Scheme + host + port | `https://a.b.example.co.uk:8080` |
| `host` | Hostname + port | `a.b.example.co.uk:8080` |
| `hostname` | Full hostname | `a.b.example.co.uk` |
| `subdomain` | Subdomain parts only | `a.b` |
| `sld` | Second-level domain | `example` |
| `tld` | Top-level domain | `co.uk` |
| `root` | Hostname without subdomain | `example.co.uk` |
| `path` | URL path | `/login` |
| `params` | Full query string | `?x=y` |
| `hash` | Fragment | `#h` |
| `port` | Port number (blank if the default for the protocol) | `8080` |
| `search_params` | Parsed query parameters (key-value test) | — |
| `hostname_parts` | Array of hostname parts split on `.` — matches if any part matches | — |



```yaml
# Path ends with /login
request_url:
  path|endswith: /login

# Any part of the hostname equals "ipfs"
request_url:
  hostname_parts: ipfs

# Query parameter "token" exists
request_url:
  search_params:
    - name: token
      value|exists: true
```

## Key-value tests

Conditions for `search_params`, `form_data`, `request_headers`, `response_headers`, `cookies`, and `document_cookies` are key-value tests. Use `name` and `value` sub-keys to match against entries:


```yaml
# Any query parameter starting "user_" with a numeric value
request_url:
  search_params:
    - name|startswith: user_
      value|re: ^[\d-]+$
```


```yaml
# Match a form submission containing an email at a specific domain
form_data:
  - name: email
    value|endswith: @example.com
  - name: password
    value|exists: true
```

## Test modifiers

Append modifiers to a condition key with `|`:

| Modifier  | Description  |
|  --- | --- |
| *(none)* | Trim the value and check exact equality. |
| `|includes` | String contains the match. |
| `|startswith` | String starts with the match. |
| `|endswith` | String ends with the match. |
| `|re` | String matches the regular expression. |
| `|normalize` | Collapse whitespace, remove zero-width characters, and lowercase before testing. |
| `|exists` | Boolean — whether the named property exists (`true`) or does not exist (`false`). |
| `|length` | Length of a string, or count of items in `cookies`, `form_data`, or `search_params`. |


Modifiers can be stacked. Unknown modifiers are ignored, which allows applying multiple tests of the same type to a single key — all must match:


```yaml
# Normalize then check equality
text_content|normalize: example text

# All three includes tests must match
css_selectors:
  selector_all: script
  text_content|includes|a: fetch(
  text_content|includes|b: atob(
  text_content|includes|c: document.write(
```

> **Note:** Wildcard tests (`includes` and `re`) are performed on values truncated to 500,000 characters.


## Extension version constraints

Use `extension_version_constraints` to restrict a rule to specific extension versions — for example, to require a minimum version for a feature the rule depends on, or to avoid a version with a known bug.

Specify a list of version ranges. Items in the list are combined with OR. Multiple constraints within a single item are combined with AND.


```yaml
extension_version_constraints:
  - ">=2.5.0"
  - ">=1.9.0 <2.0.0"
```

Supported operators: `<`, `<=`, `>`, `>=`, `!=`.

## Full example


```yaml
---
description: Detect requests using the magnet protocol
input: web_request
metadata:
  indicator: TORRENT_MAGNET_LINK_DETECTED
conditions:
  request_url:
    scheme: magnet
---
description: Detect form submissions to a known data collection endpoint
input: web_request
metadata:
  indicator: SUSPICIOUS_FORM_SUBMISSION
conditions:
  method: POST
  request_url:
    path|endswith: /collect
  form_data:
    - name: email
      value|exists: true
```