Process-Qualified Rules
How to write “only this binary may reach that application” — the pattern behind the Shadow AI demo. Every API behaviour on this page was measured against a live PCE, because several of them differ from what the object model suggests.
The shape of it
Three objects, in this order:
- A service qualified by process —
windows_egress_services. - An allow rule carrying the port in
ingress_servicesand the process service inegress_services. - A broad deny below it —
{"service": "All Services"}.
// 1. the process qualifier
{ "name": "S-VDI-chrome-egress",
"windows_egress_services": [
{ "process_name": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" }
] }
// 2. the allow: port on the provider side, process on the consumer side
{ "providers": ["app=payment"], "consumers": ["app=vdi"],
"ingress_services": [{ "port": 443, "proto": "tcp" }],
"egress_services": [{ "service": "S-VDI-chrome-egress" }],
"unscoped_consumers": true }
// 3. the deny, evaluated after allow rules
{ "providers": ["app=payment"], "consumers": ["ams"],
"ingress_services": [{ "service": "All Services" }] }
Three things that are not obvious
A service carries an OS type, and the qualifier lists are mutually exclusive. service_ports, windows_services and windows_egress_services cannot be combined. Sending two, the PCE keeps one, nulls the other, and still answers 201 — so “chrome.exe on 443” is not one service object. Editing one into another answers cannot change OS type of service. The tools refuse the combination up front rather than letting a half-discarded object look like success.
windows_egress_services takes process_name / service_name only. A port there is rejected outright. The port lives on the rule, which is why the pattern above splits them across ingress_services and egress_services.
A Windows egress service cannot go in ingress_services. The PCE answers ingress_service_cannot_be_windows_egress_service without saying where it belongs. It belongs in egress_services: that field qualifies which process on the consumer may use the rule, while ingress_services stays the provider-side port. The two together express “this binary, to that port”.
Process path matching
process_name is passed through exactly as given — no case folding, no slash rewriting, because the VEN matches it literally.
| Value | Matches |
|---|---|
C:\Program Files\Google\Chrome\Application\chrome.exe | that binary at that path |
chrome.exe | any binary of that name, in any directory |
The bare form is wider than it looks: anything a user renames to chrome.exe matches it.
Windows VEN required
windows_egress_services is evaluated on the consumer and needs a Windows VEN there. A Linux consumer ignores the process qualifier and matches on the port alone — so the rule is silently wider than it reads: “chrome.exe on 443” becomes “any process on 443” for those workloads.
The tools check the consumers in scope and return a policy_widening_warning naming the non-Windows workloads when this applies.
Deny rules cannot be process-qualified
The PCE evaluates a deny without process context, so a process-qualified service in a deny would apply to its ports alone — broader than intended. The tools reject it with that explanation.
The supported shape is always a qualified allow above a broad deny: allow rules are evaluated first, so the narrow allow wins and everything else falls through to the deny.
Referring to services
Anywhere a rule takes services, each entry is one of:
| Form | Use |
|---|---|
{"port": 443, "proto": "tcp"} | inline port |
{"href": "/orgs/1/sec_policy/draft/services/42"} | a specific object |
{"service": "All Services"} | by exact name |
Mixing those keys in one entry is an error — it used to be resolved by silently dropping the href, which stored 0/tcp and reported success.
Name resolution is exact. The PCE matches ?name= as a substring, so S-HTTP also returns S-HTTPS and S-HTTPS-UDP; the tools filter to the exact match and report ambiguity rather than guessing.