Skip to the content.

How to configure policies

This guide shows you how to write a YAML policy file, load it into PolicyEngine, and verify it is seeding behavioral baselines correctly.


What policies do

Policies seed BehaviorProfile objects with known-good traffic before any telemetry is observed. A destination, port, or protocol listed as allowed in policy is treated as part of the baseline from the first event. This means traffic to your internal DNS server or corporate proxy will never fire a rare-destination finding, even on the very first run.

Policies do not suppress findings on their own. The PolicyViolationDetector actually fires the other way: it flags traffic that is outside an explicit allow-list. The two behaviours are complementary.


Policy file structure

groups:
  <group_name>:
    members: [<subject_id>, ...]
    allowed_destinations: [<ip_or_host>, ...]
    allowed_ports: [<int>, ...]
    allowed_protocols: [<proto>, ...]

subjects:
  <subject_id>:
    peer_group: <group_name>
    allowed_destinations: [...]
    allowed_ports: [...]
    allowed_protocols: [...]

All keys under both groups and subjects are optional. Omit allowed_destinations if you do not want to constrain destinations for that group or subject; the PolicyViolationDetector only fires when the allow-list is non-empty.


Minimal policy file

groups:
  engineering:
    members: [alice, bob]
    allowed_ports: [22, 80, 443]
    allowed_protocols: [tcp]

subjects:
  alice:
    peer_group: engineering
  bob:
    peer_group: engineering

This is enough to:


Full annotated example

This is an expanded version of examples/sensegnat.example.policies.yaml.

# Group rules apply to every member of the group.
# Subject rules ADD to group rules — they do not replace them.

groups:
  engineering:
    members: [alice, bob]
    allowed_destinations:
      - 203.0.113.10    # internal build server
      - 10.0.0.1        # corporate proxy
    allowed_ports: [22, 80, 443, 8080]
    allowed_protocols: [tcp]

  finance:
    members: [carol]
    allowed_destinations:
      - 203.0.113.50    # finance SaaS endpoint
    allowed_ports: [443]
    allowed_protocols: [tcp]

subjects:
  alice:
    peer_group: engineering
    # alice's additional exceptions on top of engineering group rules
    allowed_destinations:
      - 198.51.100.44   # approved external vendor
    allowed_ports: [8443]

  bob:
    peer_group: engineering
    # no subject-level exceptions — inherits engineering rules only

  carol:
    peer_group: finance
    # no subject-level exceptions — inherits finance rules only

How group inheritance works

Subject rules are additive. When you call policy_engine.allowed_destinations("alice"), the engine unions alice’s subject-level destinations with the destinations from her group:

alice's effective destinations
  = subjects.alice.allowed_destinations
  + groups.engineering.allowed_destinations
  = {198.51.100.44} ∪ {203.0.113.10, 10.0.0.1}
  = {198.51.100.44, 203.0.113.10, 10.0.0.1}

The same union logic applies to ports and protocols.

Subject Source Effective destinations
alice group + subject {203.0.113.10, 10.0.0.1, 198.51.100.44}
bob group only {203.0.113.10, 10.0.0.1}
carol group only {203.0.113.50}

A subject with peer_group set but no group-level or subject-level allowed_destinations gets an empty frozenset for destinations — PolicyViolationDetector will not fire for that subject on destination checks.


Loading the policy in code

from pathlib import Path
from sensegnat.policy.engine import PolicyEngine

engine = PolicyEngine.from_yaml(Path("policies.yaml"))

Verify the engine loaded correctly with quick assertions:

assert engine.peer_group("alice") == "engineering"
assert "203.0.113.10" in engine.allowed_destinations("alice")
assert "198.51.100.44" in engine.allowed_destinations("alice")
assert "198.51.100.44" not in engine.allowed_destinations("bob")
assert 443 in engine.allowed_ports("alice")
assert "tcp" in engine.allowed_protocols("carol")

For a subject that does not appear in the policy file at all:

assert engine.peer_group("ghost") is None
assert engine.allowed_destinations("ghost") == frozenset()

Wiring the engine into the service

Option 1 — via settings YAML (recommended for production)

Add policy_path to your sensegnat.yaml:

policy_path: ./policies.yaml

Then load it normally:

from pathlib import Path
from sensegnat.api.service import SenseGNATService
from sensegnat.config.settings import load_settings
from sensegnat.ingestion.csv_adapter import CsvEventAdapter

settings = load_settings(Path("sensegnat.yaml"))
service = SenseGNATService(adapter=CsvEventAdapter(Path("events.csv")), settings=settings)
service.run_once()

SenseGNATService.__init__ reads settings.policy_path and calls PolicyEngine.from_yaml automatically.

Option 2 — directly on the service instance

from sensegnat.policy.engine import PolicyEngine

service = SenseGNATService(adapter=adapter)
service.policy_engine = PolicyEngine.from_yaml(Path("policies.yaml"))

This is useful in tests or one-off scripts where you want to avoid the settings layer.


Common patterns

Allow-listing shared cloud infrastructure

Put widely used cloud endpoints in a group that all affected subjects belong to, rather than duplicating them in every subject block:

groups:
  all_staff:
    members: [alice, bob, carol, dave]
    allowed_destinations:
      - 8.8.8.8           # Google DNS
      - 1.1.1.1           # Cloudflare DNS
      - 169.254.169.254   # AWS metadata service
    allowed_ports: [53, 443, 80]
    allowed_protocols: [tcp, udp]

Segmenting by department

Give each department its own group with tightly scoped destinations. Subjects that belong to multiple logical groups cannot be in two peer_group values simultaneously; put shared rules in a parent group and document the exception:

groups:
  developers:
    members: [alice, bob]
    allowed_ports: [22, 80, 443, 8080, 8443, 5432]

  analysts:
    members: [carol, dave]
    allowed_ports: [443, 3306, 5432]

Handling one-off exceptions

Put per-subject exceptions directly in the subjects block rather than modifying group rules. This keeps the group definition clean and the exception auditable:

subjects:
  alice:
    peer_group: developers
    allowed_destinations:
      - 198.51.100.44   # temporary vendor access, expires 2026-06-01
    allowed_ports: [8443]

Subjects without a peer group

You can define allowed_destinations for a subject without assigning them a peer_group. PeerDeviationDetector will not fire for that subject (no peers to compare against), but PolicyViolationDetector will still enforce the allow-list:

subjects:
  service_account_x:
    allowed_destinations:
      - 10.10.0.5
    allowed_ports: [443]
    allowed_protocols: [tcp]

Verifying policy seeding suppresses rarity findings

The easiest way to confirm that policy seeding is working is to run two consecutive run_once() calls on the same service instance. On the first run, a policy-seeded destination is included in the built profile. On the second run, the same destination appears in the existing profile and the rarity detector stays silent:

from datetime import datetime, timezone
from sensegnat.api.service import SenseGNATService
from sensegnat.ingestion.sample_adapter import SampleEventAdapter
from sensegnat.policy.engine import PolicyEngine
from pathlib import Path

service = SenseGNATService(adapter=SampleEventAdapter())
service.policy_engine = PolicyEngine.from_yaml(Path("policies.yaml"))

service.run_once()   # builds profiles seeded with policy destinations

# Replace the adapter with events pointing at policy-allowed destinations
# and confirm no rare-destination findings are emitted.

If you see rare-destination findings for policy-listed destinations on the second run, the most likely cause is that the subject identifier in the event (source_user or source_host) does not match the subject key in the policy file. Print engine.peer_group(subject_id) to check.