Build authorization-gated tenancy evidence harness
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0260c-4067-7052-9647-ad000d576e38
This commit is contained in:
parent
2c8e1d41ad
commit
beab2a04d1
32 changed files with 1816 additions and 11 deletions
83
src/whitehat_security/engagement.py
Normal file
83
src/whitehat_security/engagement.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
class AuthorizationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
REQUIRED = {
|
||||
"engagement_id", "authorization_id", "authorizer", "approved_at", "expires_at",
|
||||
"target", "target_owner", "environment", "source", "routes", "fixture_ids",
|
||||
"credential_lane", "credential_role", "credential_max_ttl_seconds", "techniques",
|
||||
"prohibited_techniques", "rate_limit_per_minute", "max_concurrency", "window_start",
|
||||
"window_end", "operator_contact", "abort_contact", "posture_claim", "attacker_model",
|
||||
"finding_destination", "target_owner_acknowledged_at",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Engagement:
|
||||
raw: dict[str, Any]
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | Path, *, now: datetime | None = None) -> "Engagement":
|
||||
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
missing = sorted(REQUIRED - data.keys())
|
||||
if missing:
|
||||
raise AuthorizationError(f"incomplete engagement; missing: {', '.join(missing)}")
|
||||
engagement = cls(data)
|
||||
engagement.validate(now=now)
|
||||
return engagement
|
||||
|
||||
def validate(self, *, now: datetime | None = None) -> None:
|
||||
current = now or datetime.now(UTC)
|
||||
start = _timestamp(self.raw["window_start"])
|
||||
end = _timestamp(self.raw["window_end"])
|
||||
expiry = _timestamp(self.raw["expires_at"])
|
||||
approved = _timestamp(self.raw["approved_at"])
|
||||
acknowledged = _timestamp(self.raw["target_owner_acknowledged_at"])
|
||||
if not approved <= current <= min(end, expiry):
|
||||
raise AuthorizationError("engagement is outside its approved time/expiry window")
|
||||
if current < start:
|
||||
raise AuthorizationError("engagement window has not started")
|
||||
if acknowledged < approved:
|
||||
raise AuthorizationError("target-owner acknowledgement predates approval")
|
||||
if acknowledged > current:
|
||||
raise AuthorizationError("target-owner acknowledgement is in the future")
|
||||
if start < approved:
|
||||
raise AuthorizationError("engagement window starts before approval")
|
||||
if self.raw["max_concurrency"] != 1:
|
||||
raise AuthorizationError("v0.1 permits exactly one in-flight API/database operation")
|
||||
if not 1 <= self.raw["rate_limit_per_minute"] <= 60:
|
||||
raise AuthorizationError("API rate ceiling must be between 1 and 60 per minute")
|
||||
if self.raw["environment"] == "production" and not self.raw.get("production_approval"):
|
||||
raise AuthorizationError("production requires production-specific approval")
|
||||
if self.raw["finding_destination"] != "risk-nexus":
|
||||
raise AuthorizationError("finding destination must be risk-nexus")
|
||||
parsed = urlparse(self.raw["target"])
|
||||
if parsed.scheme and parsed.scheme not in {"http", "https", "postgresql"}:
|
||||
raise AuthorizationError("unsupported target scheme")
|
||||
if not self.raw["fixture_ids"]:
|
||||
raise AuthorizationError("at least one disposable fixture id is required")
|
||||
if not 1 <= self.raw["credential_max_ttl_seconds"] <= 3600:
|
||||
raise AuthorizationError("credential TTL must be between 1 and 3600 seconds")
|
||||
|
||||
def permits(self, *, technique: str, route: str) -> None:
|
||||
if technique not in self.raw["techniques"]:
|
||||
raise AuthorizationError(f"technique not authorized: {technique}")
|
||||
if route not in self.raw["routes"]:
|
||||
raise AuthorizationError(f"route not authorized: {route}")
|
||||
|
||||
|
||||
def _timestamp(value: str) -> datetime:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
if parsed.tzinfo is None:
|
||||
raise AuthorizationError("engagement timestamps must include a timezone")
|
||||
return parsed.astimezone(UTC)
|
||||
Loading…
Add table
Add a link
Reference in a new issue