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.
Prerequisites
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_idnarrows to one App Identity (or*),methodsis a list of HTTP verbs, andendpoint_patternis a glob against the Graph path. All three fields ofmatchare required; a rule with noendpoint_patternwill be rejected by the loader.conditions— zero or more predicates evaluated AFTERmatchpasses. Conditions are AND-combined by default. For OR or NOT semantics a single condition exposes alogic_group: any_of | none_ofplus avariantslist. 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 ofallow,filter,block,log_only.filterrequires a siblingresponse_filterwithallow_properties(whitelist) orstrip_properties(blacklist).blockshort-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).
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: