Files
OpenShell/python/openshell/prover/queries.py
T
Alexander Watson d7f1f8052d feat(prover): add OpenShell Policy Prover (OPP) to Python SDK
Integrate Z3-based formal verification for sandbox policies as
openshell.prover. Detects data exfiltration paths, write bypass
attacks, L7 enforcement gaps, overpermissive methods, and privilege
escalation via binary capability chains.

Closes #699

Signed-off-by: Alexander Watson <zredlined@gmail.com>
2026-03-31 08:31:00 -07:00

609 lines
23 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Verification queries that ask reachability questions against the Z3 model."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING
import z3
from .policy_parser import PolicyIntent
if TYPE_CHECKING:
from .z3_model import ReachabilityModel
class RiskLevel(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
ADVISORY = "advisory"
@dataclass
class ExfilPath:
"""A concrete path through which data can be exfiltrated."""
binary: str
endpoint_host: str
endpoint_port: int
mechanism: str
policy_name: str
l7_status: str # "l4_only", "l7_allows_write", "l7_bypassed"
@dataclass
class WriteBypassPath:
"""A path that allows writing despite read-only intent."""
binary: str
endpoint_host: str
endpoint_port: int
policy_name: str
policy_intent: str
bypass_reason: str # "l4_only", "l7_bypass_protocol", "credential_write_scope"
credential_actions: list[str] = field(default_factory=list)
@dataclass
class InheritancePath:
"""A binary that inherits network access through the ancestor chain."""
parent: str
child: str
endpoint_host: str
endpoint_port: int
child_capabilities: list[str]
@dataclass
class OverpermissiveMethodPath:
"""An endpoint where wildcard or overly broad HTTP methods are allowed."""
endpoint_host: str
endpoint_port: int
policy_name: str
binary: str
allowed_methods: str # e.g. "*" or "GET, POST, PUT, PATCH, DELETE"
needed_methods: str # e.g. "POST" — what the endpoint likely needs
excess_methods: str # e.g. "DELETE, PUT, PATCH" — what's unnecessary
risk_detail: str # why the excess matters
@dataclass
class L4PolicyGapPath:
"""An endpoint missing L7 enforcement where HTTP-capable binaries have access."""
endpoint_host: str
endpoint_port: int
policy_name: str
binary: str
binary_can_http: bool
binary_bypasses_l7: bool
has_sibling_l7: bool # True if other endpoints in same policy group have L7
detail: str
@dataclass
class Finding:
"""A single verification finding."""
query: str
title: str
description: str
risk: RiskLevel
paths: list[
ExfilPath
| WriteBypassPath
| InheritancePath
| OverpermissiveMethodPath
| L4PolicyGapPath
] = field(default_factory=list)
remediation: list[str] = field(default_factory=list)
accepted: bool = False
accepted_reason: str = ""
def check_data_exfiltration(model: ReachabilityModel) -> list[Finding]:
"""Check for data exfiltration paths from readable filesystem to writable egress channels."""
findings = []
if not model.policy.filesystem_policy.readable_paths:
return findings
exfil_paths: list[ExfilPath] = []
for bpath in model.binary_paths:
cap = model.binary_registry.get_or_unknown(bpath)
if not cap.can_exfiltrate:
continue
for eid in model.endpoints:
expr = model.can_exfil_via_endpoint(bpath, eid)
# Use a fresh solver to check each path independently
s = z3.Solver()
s.add(model.solver.assertions())
s.add(expr)
if s.check() == z3.sat:
# Determine the mechanism
ek = eid.key
bypass = cap.bypasses_l7
if bypass:
l7_status = "l7_bypassed"
mechanism = f"{cap.description} — uses non-HTTP protocol, bypasses L7 inspection"
elif ek not in model.l7_enforced:
l7_status = "l4_only"
mechanism = f"L4-only endpoint — no HTTP inspection, {bpath} can send arbitrary data"
else:
ep_is_l7 = False
for _pn, rule in model.policy.network_policies.items():
for ep in rule.endpoints:
if ep.host == eid.host and eid.port in ep.effective_ports:
ep_is_l7 = ep.is_l7_enforced
if not ep_is_l7:
l7_status = "l4_only"
mechanism = f"L4-only endpoint — no HTTP inspection, {bpath} can send arbitrary data"
else:
l7_status = "l7_allows_write"
mechanism = (
f"L7 allows write methods — {bpath} can POST/PUT data"
)
if cap.exfil_mechanism:
mechanism += f". Exfil via: {cap.exfil_mechanism}"
exfil_paths.append(
ExfilPath(
binary=bpath,
endpoint_host=eid.host,
endpoint_port=eid.port,
mechanism=mechanism,
policy_name=eid.policy_name,
l7_status=l7_status,
)
)
if exfil_paths:
readable = model.policy.filesystem_policy.readable_paths
has_l4_only = any(p.l7_status == "l4_only" for p in exfil_paths)
has_bypass = any(p.l7_status == "l7_bypassed" for p in exfil_paths)
risk = RiskLevel.CRITICAL if (has_l4_only or has_bypass) else RiskLevel.HIGH
remediation = []
if has_l4_only:
remediation.append(
"Add `protocol: rest` with specific L7 rules to L4-only endpoints "
"to enable HTTP inspection and restrict to safe methods/paths."
)
if has_bypass:
remediation.append(
"Binaries using non-HTTP protocols (git, ssh, nc) bypass L7 inspection. "
"Remove these binaries from the policy if write access is not intended, "
"or restrict credential scopes to read-only."
)
remediation.append(
"Restrict filesystem read access to only the paths the agent needs."
)
findings.append(
Finding(
query="data_exfiltration",
title="Data Exfiltration Paths Detected",
description=(
f"{len(exfil_paths)} exfiltration path(s) found from "
f"{len(readable)} readable filesystem path(s) to external endpoints."
),
risk=risk,
paths=exfil_paths,
remediation=remediation,
)
)
return findings
def check_write_bypass(model: ReachabilityModel) -> list[Finding]:
"""Check for write capabilities that bypass read-only policy intent."""
findings = []
bypass_paths: list[WriteBypassPath] = []
for policy_name, rule in model.policy.network_policies.items():
for ep in rule.endpoints:
if ep.intent not in (PolicyIntent.READ_ONLY, PolicyIntent.L4_ONLY):
continue
for port in ep.effective_ports:
for b in rule.binaries:
cap = model.binary_registry.get_or_unknown(b.path)
# Check: binary bypasses L7 and can write
if cap.bypasses_l7 and cap.can_write:
creds = model.credentials.credentials_for_host(ep.host)
api = model.credentials.api_for_host(ep.host)
cred_actions = []
for cred in creds:
if api:
for wa in api.write_actions_for_scopes(cred.scopes):
cred_actions.append(
f"{wa.method} {wa.path} ({wa.action})"
)
else:
cred_actions.append(
f"credential '{cred.name}' has scopes: {cred.scopes}"
)
if cred_actions or not creds:
bypass_paths.append(
WriteBypassPath(
binary=b.path,
endpoint_host=ep.host,
endpoint_port=port,
policy_name=policy_name,
policy_intent=ep.intent.value,
bypass_reason="l7_bypass_protocol",
credential_actions=cred_actions,
)
)
# Check: L4-only endpoint + binary can construct HTTP + credential has write
if not ep.is_l7_enforced and cap.can_construct_http:
creds = model.credentials.credentials_for_host(ep.host)
api = model.credentials.api_for_host(ep.host)
cred_actions = []
for cred in creds:
if api:
for wa in api.write_actions_for_scopes(cred.scopes):
cred_actions.append(
f"{wa.method} {wa.path} ({wa.action})"
)
else:
cred_actions.append(
f"credential '{cred.name}' has scopes: {cred.scopes}"
)
if cred_actions:
bypass_paths.append(
WriteBypassPath(
binary=b.path,
endpoint_host=ep.host,
endpoint_port=port,
policy_name=policy_name,
policy_intent=ep.intent.value,
bypass_reason="l4_only",
credential_actions=cred_actions,
)
)
if bypass_paths:
findings.append(
Finding(
query="write_bypass",
title="Write Bypass Detected — Read-Only Intent Violated",
description=(
f"{len(bypass_paths)} path(s) allow write operations despite "
f"read-only policy intent."
),
risk=RiskLevel.HIGH,
paths=bypass_paths,
remediation=[
"For L4-only endpoints: add `protocol: rest` with `access: read-only` "
"to enable HTTP method filtering.",
"For L7-bypassing binaries (git, ssh, nc): remove them from the policy's "
"binary list if write access is not intended.",
"Restrict credential scopes to read-only where possible.",
],
)
)
return findings
def check_binary_inheritance(model: ReachabilityModel) -> list[Finding]:
"""Check for binaries that inherit network access through the ancestor chain."""
findings = []
inheritance_paths: list[InheritancePath] = []
for bpath in model.binary_paths:
children = model.binary_registry.transitive_spawns(bpath)
if not children:
continue
for eid in model.endpoints:
access_key = f"{bpath}:{eid.key}"
if access_key not in model.policy_allows:
continue
for child in children:
child_cap = model.binary_registry.get_or_unknown(child)
capabilities = []
if child_cap.can_write:
capabilities.append("can_write")
if child_cap.bypasses_l7:
capabilities.append("bypasses_l7")
if child_cap.can_exfiltrate:
capabilities.append("can_exfiltrate")
if child_cap.can_construct_http:
capabilities.append("can_construct_http")
if capabilities:
inheritance_paths.append(
InheritancePath(
parent=bpath,
child=child,
endpoint_host=eid.host,
endpoint_port=eid.port,
child_capabilities=capabilities,
)
)
if inheritance_paths:
seen = set()
deduped = []
for ip in inheritance_paths:
key = (ip.parent, ip.child)
if key not in seen:
seen.add(key)
deduped.append(ip)
findings.append(
Finding(
query="binary_inheritance",
title="Binary Ancestor Chain — Transitive Access Inheritance",
description=(
f"{len(deduped)} binary pair(s) where child processes inherit "
f"network access from parent binaries via ancestor matching."
),
risk=RiskLevel.MEDIUM,
paths=deduped,
remediation=[
"Review which child processes are spawned by allowed binaries.",
"If a child binary (e.g., git, curl) has write or L7-bypass capabilities "
"that exceed the policy intent, consider removing the parent binary or "
"restricting it to a more specific binary path.",
],
)
)
return findings
def check_inference_relay(_model: ReachabilityModel) -> list[Finding]:
"""Flag inference.local as a potential side channel for policy bypass."""
return [
Finding(
query="inference_relay",
title="Inference Relay — inference.local Bypasses Policy",
description=(
"inference.local is always reachable from within the sandbox and bypasses "
"OPA policy evaluation entirely. If the backing model supports tool use "
"or function calling, the agent may instruct it to access external services, "
"creating a side channel outside the policy boundary."
),
risk=RiskLevel.ADVISORY,
paths=[],
remediation=[
"Verify that the inference backend does not support unrestricted tool use "
"or code execution.",
"If using tool-use models, ensure the inference router restricts which tools "
"are available and what external access they can perform.",
"Consider auditing inference.local traffic for unexpected patterns.",
],
)
]
def check_overpermissive_methods(model: ReachabilityModel) -> list[Finding]:
"""Flag endpoints using wildcard or overly broad HTTP method rules."""
endpoint_hints: dict[str, tuple[set[str], str]] = {
"integrate.api.nvidia.com": (
{"POST"},
"NVIDIA Cloud Functions management APIs (DELETE /v2/nvcf/assets, "
"DELETE /v2/nvcf/deployments) share this host",
),
"inference-api.nvidia.com": (
{"POST"},
"Inference endpoint — only POST to completion paths is needed",
),
"api.anthropic.com": (
{"POST"},
"Anthropic management APIs (DELETE /v1/files, DELETE /v1/skills) share this host",
),
"statsig.anthropic.com": (
{"POST"},
"Telemetry endpoint — only POST to ingest paths is needed",
),
"sentry.io": (
{"POST"},
"Error reporting — only POST to ingest paths is needed; "
"Sentry management APIs (DELETE projects, releases) share this host",
),
"registry.npmjs.org": (
{"GET"},
"Package installation is read-only (GET); wildcard allows npm publish (PUT)",
),
}
paths: list[OverpermissiveMethodPath] = []
for policy_name, rule in model.policy.network_policies.items():
for ep in rule.endpoints:
if not ep.is_l7_enforced:
continue
allowed = ep.allowed_methods
if not allowed:
continue
has_wildcard = any(r.method == "*" for r in ep.rules)
has_delete = "DELETE" in allowed
if not has_wildcard and not has_delete:
continue
hint = endpoint_hints.get(ep.host)
if hint:
needed, risk_detail = hint
excess = allowed - needed - {"HEAD", "OPTIONS"}
else:
if not has_delete:
continue
needed = allowed - {"DELETE"}
excess = {"DELETE"}
risk_detail = "DELETE method allowed but likely not required"
if not excess:
continue
for b in rule.binaries:
for port in ep.effective_ports:
paths.append(
OverpermissiveMethodPath(
endpoint_host=ep.host,
endpoint_port=port,
policy_name=policy_name,
binary=b.path,
allowed_methods="*"
if has_wildcard
else ", ".join(sorted(allowed)),
needed_methods=", ".join(sorted(needed)),
excess_methods=", ".join(sorted(excess)),
risk_detail=risk_detail,
)
)
if not paths:
return []
unique_endpoints = {
(p.endpoint_host, p.endpoint_port, p.policy_name) for p in paths
}
return [
Finding(
query="overpermissive_methods",
title="Overpermissive HTTP Methods — Destructive Operations Allowed",
description=(
f"{len(unique_endpoints)} endpoint(s) allow HTTP methods beyond what is needed. "
f'Wildcard (method: "*") or explicit DELETE on endpoints that share hosts with '
f"management APIs enables destructive operations."
),
risk=RiskLevel.HIGH,
paths=paths,
remediation=[
'Replace `method: "*"` with explicit method lists matching actual usage.',
"Inference endpoints: restrict to `POST` on `/v1/chat/completions`, "
"`/v1/messages`, `/v1/responses`.",
"Telemetry endpoints: restrict to `POST` on known ingest paths.",
"Package registries: restrict to `GET` for installation (no publish).",
],
)
]
def check_l4_policy_gaps(model: ReachabilityModel) -> list[Finding]:
"""Flag endpoints without L7 enforcement where HTTP-capable binaries have access."""
paths: list[L4PolicyGapPath] = []
for policy_name, rule in model.policy.network_policies.items():
has_any_l7 = any(ep.is_l7_enforced for ep in rule.endpoints)
for ep in rule.endpoints:
if ep.is_l7_enforced:
continue
for b in rule.binaries:
cap = model.binary_registry.get_or_unknown(b.path)
if not (
cap.can_construct_http or cap.bypasses_l7 or cap.can_exfiltrate
):
continue
detail_parts = []
if has_any_l7:
l7_siblings = [
f"{sib.host}:{sib.effective_ports[0]}"
for sib in rule.endpoints
if sib.is_l7_enforced and sib.effective_ports
]
detail_parts.append(
f"Inconsistent: sibling endpoint(s) {', '.join(l7_siblings)} "
f"in '{policy_name}' have L7, but this one does not"
)
if cap.can_construct_http:
detail_parts.append(
f"{b.path} can construct arbitrary HTTP — all methods/paths "
f"pass uninspected"
)
if cap.bypasses_l7:
detail_parts.append(
f"{b.path} uses non-HTTP protocol — even with L7, "
f"traffic would bypass inspection"
)
for port in ep.effective_ports:
paths.append(
L4PolicyGapPath(
endpoint_host=ep.host,
endpoint_port=port,
policy_name=policy_name,
binary=b.path,
binary_can_http=cap.can_construct_http,
binary_bypasses_l7=cap.bypasses_l7,
has_sibling_l7=has_any_l7,
detail="; ".join(detail_parts),
)
)
if not paths:
return []
has_inconsistent = any(p.has_sibling_l7 for p in paths)
unique_endpoints = {(p.endpoint_host, p.endpoint_port) for p in paths}
return [
Finding(
query="l4_policy_gaps",
title="L4-Only Endpoints — No HTTP Inspection",
description=(
f"{len(unique_endpoints)} endpoint(s) lack `protocol: rest` — all traffic "
f"passes without HTTP method or path enforcement. "
+ (
"Some share a policy group with L7-enforced siblings, indicating "
"inconsistent enforcement."
if has_inconsistent
else "HTTP-capable binaries have unrestricted access."
)
),
risk=RiskLevel.HIGH if has_inconsistent else RiskLevel.MEDIUM,
paths=paths,
remediation=[
"Add `protocol: rest` with `enforcement: enforce` and `tls: terminate` "
"to enable HTTP inspection.",
'Define explicit method and path rules (e.g., `allow: { method: GET, path: "/**" }`) '
"to restrict what traffic is permitted.",
"If L4-only is intentional (e.g., for binary protocols like git), "
"document the rationale and restrict the binary list tightly.",
],
)
]
def run_all_queries(model: ReachabilityModel) -> list[Finding]:
"""Run all verification queries and return findings."""
findings = []
findings.extend(check_data_exfiltration(model))
findings.extend(check_write_bypass(model))
findings.extend(check_overpermissive_methods(model))
findings.extend(check_l4_policy_gaps(model))
findings.extend(check_binary_inheritance(model))
findings.extend(check_inference_relay(model))
return findings