Rulesets and rules: the GraphWarden data model

A Ruleset is a container. A Rule is a single policy that matches an endpoint, evaluates conditions, and applies an action. Every Graph request arriving at the proxy is evaluated against the ruleset bound to the caller's App Identity. Rules are walked top to bottom; the first rule whose match and conditions succeed decides the verdict.

Shipped schema — reference table planned for Phase 6

Prerequisites

- Familiarity with Microsoft Graph endpoints (`/v1.0/users`, `/v1.0/groups`, etc.) - Basic YAML comfort - An App Identity in GraphWarden App Admin so you have somewhere to attach a ruleset

Ruleset structure

A ruleset is a YAML document with one top-level rules array. Each entry defines a single rule. Rulesets live in GraphWarden App Admin (MySQL-backed) and are synced to each proxy instance on a timer (RuleSync:IntervalSeconds).

# A minimal ruleset with two rules.
rules:
  - id: hr-app-allow-user-basics           # Stable identifier used in audit logs
    match:
      app_id: "f63c4abc-..."              # Target one App Identity, or "*" for any
      methods: ["GET"]                    # HTTP methods this rule covers
      endpoint_pattern: "/users*"         # Graph path prefix to match
    action: filter                         # Apply response_filter below
    response_filter:
      allow_properties:
        - id
        - displayName
        - mail

  - id: block-privileged-endpoints
    match:
      app_id: "*"
      methods: ["DELETE", "PATCH"]
      endpoint_pattern: "/identityRiskDetections*"
    action: block                          # Return 403 without calling Graph

A rule's anatomy

A rule has three logical sections. Read them in this order when designing or reviewing a rule.

  • match — a gate on the request envelope. app_id narrows to one App Identity (or *), methods is a list of HTTP verbs, and endpoint_pattern is a glob against the Graph path. All three fields of match are required; a rule with no endpoint_pattern will be rejected by the loader.
  • conditions — zero or more predicates evaluated AFTER match passes. Conditions are AND-combined by default. For OR or NOT semantics a single condition exposes a logic_group: any_of | none_of plus a variants list. Conditions are what make GraphWarden more than a firewall: a rule can depend on the caller's group membership, a target object's attributes, the time of day, a rate budget, or the calling IP.
  • action — the verdict. One of allow, filter, block, log_only. filter requires a sibling response_filter with allow_properties (whitelist) or strip_properties (blacklist). block short-circuits before Graph is called and returns HTTP 403.

Condition types

Eleven condition types are shipped. Each has its own required fields; see the API reference (planned for Phase 6) for the full field schema per type.

Type Domain What it matches
group_membership Object Target user or group is a member of a specified Azure AD security group
object_attribute Object A field on the response object equals, contains, or matches a regex against a value
domain Object Target user's UPN or mail domain is in a whitelist
custom_attribute Object A Directory Extension attribute on the target object matches a value
administrative_unit Object Target user or group belongs to a specified Administrative Unit
assigned_license Object Target user has a specified license SKU assigned
caller_identity Request Calling App Identity equals a specified UUID
time_window Request Request arrives within a configured HH:MM window for a given UTC offset
rate_limit Request Caller is within max_requests per window_seconds budget
object_count_limit Response Result set size is at or below max_count
ip_range Request Source IP is within one of the supplied CIDR ranges

The Domain column tells you WHEN the proxy can evaluate the condition. Object-domain conditions need the Graph response in hand before they resolve, so they are evaluated on the outbound path. Request-domain conditions evaluate before the upstream call. One request-domain condition failing can therefore save a Graph call entirely.

Transform types

Nine transforms are shipped. Transforms operate on individual properties referenced by a response_filter. The transform rewrites the value in-place on the response body before it is returned to the caller.

Transform Input shape Output shape Typical use
mask_start string First N chars hidden Conceal given name on a full-name field
mask_end string Last N chars hidden Redact a local-part while preserving domain
redact any [redacted] sentinel Remove a field entirely from view
hash string SHA-256 hex digest Emit a stable pseudonym for analytics
truncate string String capped to N chars Cap long notes or descriptions
regex_replace string Template output Redact SSN-shaped substrings
domain_only email string Domain part only Keep example.com, drop alice@
initials string Initials only "John Doe" becomes "JD"
noise string String plus random suffix Break output-identity assumptions

Evaluation order

Rules in a ruleset are walked top-down. The first rule whose match and conditions both succeed decides the verdict — first match wins. Later rules are not consulted. If no rule matches, the proxy falls through to a default allow (requests pass to Graph unchanged).

First-match-wins is the shipped semantic. There is no accumulating mode; each rule's action is final. To compose policies — for example, filter AND log — place a `log_only` rule BEFORE a `filter` rule with the same `match`: the walker stops at the first match, so log-then-filter is not a supported pattern. Use a single rule whose `action: filter` + audit event captures both concerns at once.

A cache_ttl on a rule caches the VERDICT (match decision and filter result) for a given caller + target object for the specified seconds. It does not cache the raw Graph response body.

Troubleshooting

My rule does not match and I expected it to — walk the ruleset in order and find the first rule whose match envelope (app_id, methods, endpoint_pattern) succeeds on the request. If an earlier rule is matching with a broader app_id: "*", its verdict wins and the later rule never runs. Narrow the earlier rule or reorder.

A filter rule returns 403 instead of a redacted body — the rule is declared action: block (or is missing action: filter entirely and the default is hitting somewhere else). The YAML loader logs RuleLoader: rule {id} has no action at startup — check Kestrel stdout.

A condition appears valid but never passes — confirm the condition type is spelled exactly as the table above lists it. Typos (group_memberships, object_attributes) are silently ignored by some older proxy builds. Run php artisan doctor:rules in App Admin to surface loader warnings.

Transforms do not appear to run — transforms only apply when action: filter AND the property appears in response_filter.allow_properties or strip_properties. A transform declared outside that block is syntactically valid YAML but has no effect.

From model to rollout

The ruleset patterns above are illustrated against concrete scenarios on the use cases page — HR self-service, SIEM backfill, vendor isolation. To pressure-test the model against your own Graph estate, contact the GraphWarden team.

Last reviewed:

Last reviewed: .