> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Enforcement

> Policy actions and enforcement mechanisms for blocking, masking, filtering, and rewriting queries

## Overview

Formal policies can enforce different actions at three evaluation stages: **session**, **request**, and **response**. Each stage has specific actions available based on when the policy is evaluated.

## Evaluation Stages

<CardGroup cols={3}>
  <Card title="Session" icon="plug">
    **When**: Connection establishment **Actions**: `allow`, `block`, `mfa` Use
    for: Authentication, connection-level access control
  </Card>

  <Card title="Request" icon="circle-arrow-right">
    **When**: Before query reaches resource **Actions**: `allow`, `block`,
    `rewrite` Use for: Query validation, SQL rewriting, blocking writes
  </Card>

  <Card title="Response" icon="circle-arrow-left">
    **When**: After data returns from resource **Actions**: `allow`, `filter`,
    `mask`, `decrypt` Use for: Data masking, PII redaction, filtering results
  </Card>
</CardGroup>

## Common Action Parameters

All actions support these parameters:

| Parameter         | Type       | Description                                                                             |
| ----------------- | ---------- | --------------------------------------------------------------------------------------- |
| `action`          | **String** | The enforcement action: `allow`, `block`, `filter`, `mask`, `decrypt`, `rewrite`, `mfa` |
| `reason`          | **String** | Explanation for the action (logged for compliance and auditing)                         |
| `contextual_data` | **String** | Additional context that influenced the decision (e.g., "Zendesk ticket #123")           |

## Block Action

Deny access and terminate the connection or query.

### Parameters

| Parameter | Type       | Description                                                       |
| --------- | ---------- | ----------------------------------------------------------------- |
| `type`    | **Enum**   | One of `block_with_formal_message` or `block_with_custom_message` |
| `message` | **String** | Custom message to show the user                                   |

### Example

```rego theme={null}
package formal.v2

import future.keywords.if

# Block all DELETE statements in production
request := {
  "action": "block",
  "type": "block_with_custom_message",
  "message": "DELETE operations are not allowed in production",
  "reason": "Production data protection policy"
} if {
  input.resource.environment == "production"
  input.query.statement_type == "DELETE"
}
```

## Allow Action

Explicitly permit an operation. Use in combination with default deny policies.

### Example

```rego theme={null}
package formal.v2

import future.keywords.if
import future.keywords.in

# Default deny
default session := {
  "action": "block",
  "type": "block_with_formal_message"
}

# Allow admins
session := {
  "action": "allow",
  "reason": "User is in admin group"
} if {
  "admin" in input.user.groups
}
```

## Rewrite Action

Modify the query before it reaches the resource.

### Parameters

| Parameter | Type        | Description                     |
| --------- | ----------- | ------------------------------- |
| `limit`   | **Integer** | The limit to apply to the query |
| `comment` | **String**  | The comment to add to the query |

### Example

```rego theme={null}
package formal.v2

import future.keywords.if

# Add LIMIT clause to unbounded queries
request := {
  "action": "rewrite",
  "limit": 1000,
  "comment": "Auto-added row limit for safety",
} if {
  startswith(lower(input.query.query), "select")
}
```

## Filter Action

Remove rows from the result set based on conditions.

### Example

```rego theme={null}
package formal.v2

import future.keywords.if

# Filter rows unless user has open Zendesk ticket
default response := {
  "action": "filter",
  "reason": "No open tickets for this data"
}

response := {
  "action": "allow",
  "contextual_data": filtered_tickets,
  "reason": "User has open ticket"
} if {
  col := input.row[_]
  col["path"] == "main.public.pii.email"

  filtered_tickets := [obj |
    obj := data.zendesk_tickets[_]
    obj.requester_email == col.value
    obj.status == "open"
  ]

  count(filtered_tickets) > 0
}
```

## Mask Action

Redact or obfuscate sensitive data in responses.

### Parameters

| Parameter           | Type          | Description                                                                |
| ------------------- | ------------- | -------------------------------------------------------------------------- |
| `type`              | **String**    | Masking type (e.g., `redact.partial`, `hash.with_salt`, `fake`, `nullify`) |
| `sub_type`          | **String**    | Specific masking method (e.g., `email_mask_username`)                      |
| `columns`           | **\[]Column** | List of columns to mask                                                    |
| `redact`            | **String**    | Replacement value for redaction                                            |
| `characters_count`  | **Integer**   | Number of characters to mask                                               |
| `typesafe`          | **Boolean**   | Maintain column data type (default: `false`)                               |
| `typesafe_fallback` | **String**    | `fallback_to_null` or `fallback_to_default`                                |

### Masking Types

| Type                         | Privacy Level | Description                                  |
| ---------------------------- | ------------- | -------------------------------------------- |
| `nullify`                    | 4 (Highest)   | Replace with NULL                            |
| `hash.with_salt`             | 4             | Hash with random salt                        |
| `fake`                       | 3             | Generate realistic fake data                 |
| `redact.constant_characters` | 3             | Replace with constant string                 |
| `hash.no_salt`               | 2             | Hash without salt (deterministic)            |
| `redact.partial`             | 1             | Partially redact (e.g., mask email username) |
| `none`                       | 0 (Lowest)    | No masking                                   |

### Masking Subtypes

<AccordionGroup>
  <Accordion title="Email Masking">
    * `email_mask_username`: `****@example.com`
    * `email_mask_domain_name`: `user@*****.***`
    * `email_mask_while_preserving`: `a****@e******.com`
    * `email_mask_with_fake`: `fake.email@example.com`
  </Accordion>

  <Accordion title="Personal Information">
    * `person_full_name_mask_with_fake`
    * `person_first_name_mask_with_fake`
    * `person_last_name_mask_with_fake`
    * `person_ssn_mask_with_fake`
  </Accordion>

  <Accordion title="Location Data">
    * `postal_address_mask_with_fake`
    * `city_mask_with_fake`
    * `state_mask_with_fake`
    * `zip_mask_with_fake`
    * `location_mask_except_state_country`
  </Accordion>

  <Accordion title="Payment Data">
    * `payment_credit_card_number_mask_with_fake`
    * `payment_credit_card_cvv_mask_with_fake`
    * `payment_credit_card_exp_mask_with_fake`
    * `payment_ach_routing_with_fake`
    * `payment_bitcoin_address_with_fake`
  </Accordion>

  <Accordion title="Network Data">
    * `network_url_mask_with_fake`
    * `network_ipv4_mask_with_fake`
    * `network_ipv6_mask_with_fake`
    * `network_mac_mask_with_fake`
  </Accordion>

  <Accordion title="General Redaction">
    * `redact.constant_characters`: Replace with custom string
    * `redact.first_n_characters`: Mask first N chars
    * `redact.last_n_characters`: Mask last N chars
    * `mask_everything_except_last`: Show only last N chars
  </Accordion>
</AccordionGroup>

### Examples

**Mask email usernames:**

```rego theme={null}
package formal.v2

import future.keywords.if

response := {
  "action": "mask",
  "type": "redact.partial",
  "sub_type": "email_mask_username",
  "columns": columns
} if {
  columns := [col |
    col := input.row[_]
    col["data_label"] == "email_address"
  ]
  count(columns) > 0
}
```

**Replace PII with fake data:**

```rego theme={null}
package formal.v2

import future.keywords.if

response := {
  "action": "mask",
  "type": "fake",
  "sub_type": "person_full_name_mask_with_fake",
  "columns": columns
} if {
  columns := [col |
    col := input.row[_]
    col["data_label"] == "name"
  ]
  count(columns) > 0
}
```

**Redact with constant value:**

```rego theme={null}
package formal.v2

import future.keywords.if

response := {
  "action": "mask",
  "type": "redact.constant_characters",
  "sub_type": "redact.constant_characters",
  "redact": "[REDACTED]",
  "columns": columns
} if {
  columns := [col |
    col := input.row[_]
    col["data_label"] == "ssn"
  ]
  count(columns) > 0
}
```

## Decrypt Action

Decrypt previously encrypted columns (requires encryption policy).

### Example

```rego theme={null}
package formal.v2

import future.keywords.if
import future.keywords.in

response := {
  "action": "decrypt",
  "columns": columns
} if {
  "decrypt_access" in input.user.groups
  columns := [col |
    col := input.row[_]
    col["name"] == "encrypted_ssn"
  ]
}
```

## MFA Action

Require multi-factor authentication before allowing access.

### Example

```rego theme={null}
package formal.v2

import future.keywords.if

session := {
  "action": "mfa",
  "reason": "Production access requires MFA"
} if {
  input.resource.environment == "production"
}
```

## Rule Conflicts and Precedence

When multiple policies apply to the same query, Formal resolves conflicts using **least privilege**:

| Scenario                     | Resolution                                                       |
| ---------------------------- | ---------------------------------------------------------------- |
| **Block vs Allow**           | Block wins                                                       |
| **Multiple Filter actions**  | Smallest row limit wins                                          |
| **Multiple Mask actions**    | Highest privacy level wins (nullify > constant > fake > partial) |
| **Multiple Rewrite actions** | Arbitrary (avoid conflicts with scoping)                         |

### Example of Conflict Resolution

```
Policy A: Mask email as ***@***.com (privacy level 3)
Policy B: Nullify email (privacy level 4)

Result: Email is nullified (higher privacy)
```

<Tip>
  Use `included_connectors` or `included_resources` to avoid unintended policy
  conflicts.
</Tip>

## Scoping Policies with Connectors and Resources

To prevent policy conflicts when using the `default` keyword, you can limit which Connectors or Resources a policy applies to.

### Include/Exclude Connectors

Use `included_connectors` to apply a policy only to specific connectors:

```rego theme={null}
package formal.v2

import future.keywords.if

# Only apply to these Connectors
included_connectors := ["production-connector", "staging-connector"]

session := {
  "action": "block",
  "type": "block_with_formal_message"
} if {
  input.resource.technology == "postgres"
}
```

Use `excluded_connectors` to exclude specific connectors:

```rego theme={null}
package formal.v2

import future.keywords.if

# Exclude these Connectors
excluded_connectors := ["dev-connector"]

session := {
  "action": "block",
  "type": "block_with_formal_message"
} if {
  input.resource.technology == "postgres"
}
```

<Warning>
  A single policy cannot use both `included_connectors` and
  `excluded_connectors`. Choose one or the other.
</Warning>

### Include/Exclude Resources

Use `included_resources` to apply a policy only to specific resources:

```rego theme={null}
package formal.v2

import future.keywords.if

# Only apply to specific resources
included_resources := ["production-postgres"]

session := {
  "action": "block",
  "type": "block_with_formal_message"
} if {
  input.user.type == "machine"
}
```

Use `excluded_resources` to exclude specific resources:

```rego theme={null}
package formal.v2

import future.keywords.if

# Exclude specific resources
excluded_resources := ["dev-postgres", "staging-postgres"]

session := {
  "action": "block",
  "type": "block_with_formal_message"
} if {
  "engineer" in input.user.groups
}
```

<Warning>
  A single policy cannot use both `included_resources` and `excluded_resources`.
  Choose one or the other.
</Warning>
