fix(agt-policies): ne/not_in fail open on missing fields, bypassing deny rules#3359
Conversation
) _rego_op_clause guarded every comparison operator with `_v != null`, which is correct for positive operators but inverts the intent for negative ones: a missing field made the ne/not_in guard itself false, so the rule body never matched and evaluation fell through to default-allow. A deny rule pinning a content hash or an allowlist could be bypassed simply by omitting the field. Drop the null guard for ne and not_in only; Rego's native != and not ... in already treat null correctly once the guard is gone. Adds OPA-backed regression tests (missing content_hash, missing region) that fail red against the unmodified renderer and pass green after the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
🤖 AI Agent: security-scanner — View details
No security issues found. |
🤖 AI Agent: test-generator — `agent-governance-python/agt-policies/src/agt/manifest_resolution/build.py`
|
🤖 AI Agent: breaking-change-detector — API Compatibility
API Compatibility
|
🤖 AI Agent: code-reviewer — View details
TL;DR: 0 blockers, 1 warning. Fix addresses a critical security issue effectively, but additional test coverage is recommended.
Action items:
Warnings:
|
🤖 AI Agent: docs-sync-checker — Docs Sync
Docs Sync
|
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
|
🟡 Contributor Check: MEDIUM
Automated check by AGT Contributor Check. |
|
Thanks for this fix and especially the OPA-backed scenario tests. I am closing this in favor of #3358, which supersedes it, for two reasons. First, dropping the _v != null guard unconditionally also affects allow rules, so an allow ne/not_in would fire on a missing field, which is fail-open and can preempt a later deny in the first-match chain; the guard should stay for allow and drop only for deny. Second, this does not cover the missing-intermediate case (#3360): test_region_missing_denied uses args={} rather than omitting args, so the chained-accessor undefined path is not exercised. #3358 handles both. Your OPA scenario tests are stronger than the string-level assertions there, so it would be great to port them into #3358. Appreciate the contribution. |
Summary
Fixes #3297.
_rego_op_clauseinagent-governance-python/agt-policies/src/agt/manifest_resolution/build.pyguarded every comparison operator with_v != nullbefore evaluating the operator. That guard is correct for positive operators (eq,gt,in, ...) — a missing field shouldn't satisfy a positive assertion. But the same guard was applied uniformly to the negative operatorsneandnot_in, which inverts the intended safety: when the referenced field is absent,_v != nullitself is false, so the rule body never matches and evaluation falls through todefault verdict := allow.Concretely, a deny rule like:
intended to block calls with an unregistered (or missing) content hash, was bypassed entirely by omitting
content_hash— trivial for a caller to do.Fix
Dropped the
_v != nullguard forneandnot_inonly (2 lines changed). Rego's native!=andnot ... inalready treatnullcorrectly once the guard is removed — no restructuring of the accessor or matcher needed:All other operators (
eq,gt,lt,gte,lte,in,exists,contains,startswith,endswith,matches/regex) are untouched.Tests
Added to
tests/scenarios/test_egress_content_hash_escalation_scenarios.py, run through the real OPA-backed scenario harness (agt._harness.opa_runner.run_scenario), not string-matching on generated Rego source:test_missing_content_hash_denied— the exactcontent_hash neexample from Declarative negative operators (ne,not_in) fail open on missing fields, bypassing deny rules #3297not_inregion-allowlist fixture withtest_region_in_allowlist_passes,test_region_outside_allowlist_denied, andtest_region_missing_deniedWatched both missing-field tests fail red (
decision: allow) against the unmodified code, then pass green after the fix.Verification
Confirmed via
git stashthat the 8 failures are identical (same tests, same errors) on unmodifiedupstream/main— none are introduced by this change. Also independently reproduced the issue's exactopa evalrepro command against_render_regooutput to confirm the fix.Test plan
agt-policiessuite has no new failures vs. baselineruff checkclean on changed files🤖 Generated with Claude Code