Files
OpenShell/python/openshell/prover/policy_parser.py
T
Alexander Watson b9fc0d3b8d refactor(prover): simplify to two Zelkova-like verification queries
Narrow OPP to two yes/no security questions:
1. Can data leave this sandbox? (exfiltration)
2. Can the agent write despite read-only intent? (write bypass)

Removes 4 advisory/lint queries (overpermissive methods, L4 gaps,
binary inheritance, inference relay), 3 output modes (Mermaid,
topology, HTML), and fixes 3 bugs (include_workdir not used in
exfil analysis, __all__ broken when protos missing, CLI PYTHONPATH).

~40% line reduction (2900 -> 1769 lines).

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

225 lines
6.7 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Parse OpenShell sandbox policy YAML into typed dataclasses."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING
import yaml
if TYPE_CHECKING:
from pathlib import Path
class PolicyIntent(Enum):
L4_ONLY = "l4_only"
READ_ONLY = "read_only"
READ_WRITE = "read_write"
FULL = "full"
CUSTOM = "custom"
@dataclass
class L7Rule:
method: str
path: str
command: str = ""
@dataclass
class Endpoint:
host: str
port: int
ports: list[int] = field(default_factory=list)
protocol: str = ""
tls: str = ""
enforcement: str = "audit"
access: str = ""
rules: list[L7Rule] = field(default_factory=list)
allowed_ips: list[str] = field(default_factory=list)
@property
def is_l7_enforced(self) -> bool:
return bool(self.protocol)
@property
def intent(self) -> PolicyIntent:
if not self.protocol:
return PolicyIntent.L4_ONLY
if self.access == "read-only":
return PolicyIntent.READ_ONLY
if self.access == "read-write":
return PolicyIntent.READ_WRITE
if self.access == "full":
return PolicyIntent.FULL
if self.rules:
methods = {r.method.upper() for r in self.rules}
if methods <= {"GET", "HEAD", "OPTIONS"}:
return PolicyIntent.READ_ONLY
if "DELETE" not in methods:
return PolicyIntent.READ_WRITE
return PolicyIntent.FULL
return PolicyIntent.CUSTOM
@property
def effective_ports(self) -> list[int]:
if self.ports:
return self.ports
if self.port:
return [self.port]
return []
@property
def allowed_methods(self) -> set[str]:
"""Return the set of HTTP methods this endpoint allows. Empty means all (L4-only)."""
if not self.protocol:
return set() # L4-only: all traffic passes
if self.access == "read-only":
return {"GET", "HEAD", "OPTIONS"}
if self.access == "read-write":
return {"GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"}
if self.access == "full":
return {"GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"}
if self.rules:
methods = set()
for r in self.rules:
m = r.method.upper()
if m == "*":
return {"GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"}
methods.add(m)
return methods
return set()
WRITE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
@dataclass
class Binary:
path: str
@dataclass
class NetworkPolicyRule:
name: str
endpoints: list[Endpoint] = field(default_factory=list)
binaries: list[Binary] = field(default_factory=list)
@dataclass
class FilesystemPolicy:
include_workdir: bool = True
workdir: str = "/sandbox"
read_only: list[str] = field(default_factory=list)
read_write: list[str] = field(default_factory=list)
@property
def readable_paths(self) -> list[str]:
paths = self.read_only + self.read_write
if self.include_workdir and self.workdir not in paths:
paths = [self.workdir, *paths]
return paths
@dataclass
class PolicyModel:
version: int = 1
filesystem_policy: FilesystemPolicy = field(default_factory=FilesystemPolicy)
network_policies: dict[str, NetworkPolicyRule] = field(default_factory=dict)
@property
def all_endpoints(self) -> list[tuple[str, Endpoint]]:
"""Return all (policy_name, endpoint) pairs."""
result = []
for name, rule in self.network_policies.items():
for ep in rule.endpoints:
result.append((name, ep))
return result
@property
def all_binaries(self) -> list[Binary]:
"""Return deduplicated list of all binaries across all policies."""
seen = set()
result = []
for rule in self.network_policies.values():
for b in rule.binaries:
if b.path not in seen:
seen.add(b.path)
result.append(b)
return result
@property
def binary_endpoint_pairs(self) -> list[tuple[Binary, str, Endpoint]]:
"""Return all (binary, policy_name, endpoint) triples."""
result = []
for name, rule in self.network_policies.items():
for b in rule.binaries:
for ep in rule.endpoints:
result.append((b, name, ep))
return result
def parse_policy(path: Path) -> PolicyModel:
"""Parse an OpenShell policy YAML file into a PolicyModel."""
with open(path) as f: # noqa: PTH123
raw = yaml.safe_load(f)
if not raw:
return PolicyModel()
fs_raw = raw.get("filesystem_policy", {}) or {}
fs = FilesystemPolicy(
include_workdir=fs_raw.get("include_workdir", True),
workdir=fs_raw.get("workdir", "/sandbox"),
read_only=fs_raw.get("read_only", []),
read_write=fs_raw.get("read_write", []),
)
network_policies = {}
for key, rule_raw in (raw.get("network_policies") or {}).items():
endpoints = []
for ep_raw in rule_raw.get("endpoints", []):
rules = []
for r in ep_raw.get("rules", []):
allow = r.get("allow", {})
rules.append(
L7Rule(
method=allow.get("method", ""),
path=allow.get("path", ""),
command=allow.get("command", ""),
)
)
endpoints.append(
Endpoint(
host=ep_raw.get("host", ""),
port=ep_raw.get("port", 0),
ports=ep_raw.get("ports", []),
protocol=ep_raw.get("protocol", ""),
tls=ep_raw.get("tls", ""),
enforcement=ep_raw.get("enforcement", "audit"),
access=ep_raw.get("access", ""),
rules=rules,
allowed_ips=ep_raw.get("allowed_ips", []),
)
)
binaries = []
for b_raw in rule_raw.get("binaries", []):
binaries.append(Binary(path=b_raw.get("path", "")))
network_policies[key] = NetworkPolicyRule(
name=rule_raw.get("name", key),
endpoints=endpoints,
binaries=binaries,
)
return PolicyModel(
version=raw.get("version", 1),
filesystem_policy=fs,
network_policies=network_policies,
)