Continuously Verifying Least Privilege in Agent-Generated Code
The Problem
Coding agents are fast. Claude Code, Copilot, Cursor — they write functional code in seconds. But "functional" isn't "secure." When an agent adds a database connection, does it use a least-privilege service account? When it creates an API endpoint, does it scope authorization to the caller's workspace? When it generates a Dockerfile, does it run as non-root?
The traditional answer is code review. A human reads the PR, checks for security properties, approves or rejects. This works at 10 PRs a week. It collapses at 100 PRs a day — which is where agent-assisted teams are headed.
What's needed is a system that can verify security properties on every PR, at agent speed, without requiring a human to read every diff.
Verification Through Assertions
The approach: instead of scanning code for vulnerabilities (what SAST tools do), define what should be true about the code and verify those claims on every push.
An assertion is a typed, machine-checkable claim about a codebase property:
{
"type": "pattern_matches",
"params": {
"file": "src/middleware/auth.py",
"pattern": "current_user\\.tenant_id\\s*!=\\s*resource\\.tenant_id"
},
"description": "Tenant-scoped authorization rejects requests where tenant IDs don't match"
}
This says: "this pattern exists in this file, and it proves this aspect of the control." A developer or coding agent constructs the claim by reading the code. But a claim isn't proof — anyone can write a claim. That's where two-tier verification comes in.
Tier 1 (Mechanical) — does the pattern actually exist in the file? Does the function exist? Is the config value set? Deterministic checks: regex matches, AST lookups, file existence. Fast, no AI required.
Tier 2 (Semantic) — does the matched code actually prove the control? A function_exists check might find check_permissions — but is it a real implementation or an empty stub? Tier 2 feeds the matched code snippet to an LLM and asks: "Does this implementation prove the stated aspect of the control? YES or NO."
Sufficiency — do all assertions for a control, taken together, cover everything that single control requires?
Consider a control: "Enforce rate limiting on authentication endpoints — max 5 attempts per 60-second window per IP, returning HTTP 429 when exceeded." A team might submit:
decorator_present:rate_limitdecorator applied tologinendpointconfig_value_matches:RATE_LIMIT_MAX=5in configconfig_value_matches:RATE_LIMIT_WINDOW=60in config
Three passing assertions — but sufficiency flags the gap: nothing proves the endpoint returns 429 when the limit is hit, and nothing proves the limit is per-IP rather than global. The control is one responsibility (rate limiting on auth), but proving it requires covering all its specified aspects.
Applying This to Least Privilege
"Principle of Least Privilege" isn't one control — it's a family of controls that appear across a threat model wherever authorization decisions are made. In a typical web application threat model, Mipiti generates controls like:
- CTRL-06: Enforce per-API-key authorization for CRUD operations — a developer API key may only access resources within its org/project scope
- CTRL-11: Prevent path traversal in file parameters — resolve to an absolute path under project root and reject escapes
- CTRL-14: Run verification in a hardened CI job context — non-root user, no Docker socket, restricted filesystem mounts
- CTRL-25: Enforce MFA for all administrative access — disallow shared admin accounts
Each control is derived from a control objective in the threat model — the cross-product of an asset (what you're protecting) and an attacker (what capability they have). Least privilege isn't assumed — it's derived from the threat model and verified through assertions.
What the Agent Does
When a developer asks Claude Code to implement a feature, the MCP integration triggers generate_threat_model before any code is written. The agent receives controls — specific security requirements — and implements the feature with those controls in mind.
After implementation, the agent scans the codebase for evidence that each control is satisfied. For a tenant authorization control, it might find:
def check_tenant_access(resource, current_user):
if current_user.tenant_id != resource.tenant_id:
raise HTTPException(status_code=404)
The agent constructs an assertion: pattern_matches on current_user\.tenant_id\s*!=\s*resource\.tenant_id in src/middleware/auth.py, with a description explaining that this enforces tenant-scoped authorization. It verifies the assertion locally using mipiti-verify verify pattern_matches -p file=src/middleware/auth.py -p pattern="..." — confirming the pattern actually matches before submitting.
This happens for every control. The agent doesn't just write code — it proves the code satisfies security requirements.
What CI Does
On every push, the CI pipeline runs mipiti-verify:
- uses: Mipiti/mipiti-verify@163ecd50193df7a276ebc54cde67bbdd567eeadb # v0.17.0
with:
api-key: ${{ secrets.MIPITI_API_KEY }}
all: true
tier2-provider: openai
tier2-model: gpt-4o-mini
tier2-api-key: ${{ secrets.OPENAI_API_KEY }}
For each assertion:
- Tier 1 reads the file, runs the regex/AST check, and reports pass/fail
- Tier 2 reads the matched code snippet (never the full file — minimized to 16K chars centered on the match) and asks the LLM: "Does this code prove the stated aspect of the control?"
If a developer changes check_tenant_access to always return without checking (disabling authorization), tier 1 still passes (the pattern exists) but tier 2 catches it: "The function exists but does not implement meaningful authorization — it always allows access."
If someone deletes the function entirely, tier 1 catches it: "Pattern not found."
If someone adds a new endpoint without authorization, the sufficiency check catches it: "The assertion proves tenant authorization exists in check_tenant_access, but endpoint /api/new-feature is not shown to call this function."
Why the Platform Doesn't See the Code
For many teams — especially in regulated industries — sending source code to a third-party platform isn't an option. Compliance frameworks, customer contracts, and internal security policies restrict where code can go.
Mipiti is designed so that verification runs entirely in CI — where the code already lives. The platform tracks assertion definitions, control descriptions, and verification results, but never the code itself. This minimizes what the platform stores and simplifies compliance for both sides.
This isn't a limitation — it's what makes the architecture work. Verification happens at the source. The platform evaluates whether the verified assertions collectively cover the security controls. Each party operates on what it naturally has access to.
Lessons
Assertions drift, and that's the point. When code changes break an existing assertion, the CI run fails. This is the system working correctly — it caught a security regression that a human reviewer might miss in a 500-line diff. The assertion is the canary.
Sufficiency is harder than verification. Proving a function exists is easy. Proving that a set of assertions collectively covers every aspect a control specifies — that's where the AI evaluation earns its keep. A rate limiting control with three passing assertions can still be flagged as incomplete because none of them prove what happens when the limit is exceeded.
Agents submit better assertions than humans. Human-submitted assertions tend to be vague ("auth exists in auth.py"). Agent-submitted assertions are specific ("pattern current_user\.tenant_id\s*!=\s*resource\.tenant_id in src/middleware/auth.py proves tenant-scoped authorization rejects cross-tenant requests"). The specificity makes tier 2 evaluation more accurate.
Developer keys reduce friction. Developers need to test assertions locally without recording results. Separating developer keys (mk_ prefix, auto-skip submission) from verifier keys (mv_ prefix, submits results) lets developers iterate freely while CI remains the authority.
Continuous, Not Point-in-Time
Least privilege isn't a checkbox. It's a property that must hold on every commit, in every endpoint, across every authorization boundary. Agents write the code. CI verifies the assertions. The platform evaluates coverage. When something drifts, the next push catches it.
Mipiti generates security controls from feature descriptions and verifies them in CI. Try it with Claude Code or explore the documentation.