Files
T

374 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Just-In-Time Access Provisioning Engine
Manages JIT access requests, approval workflows, time-bound grants,
automatic revocation, and audit logging for zero-standing-privilege
implementations.
"""
import json
import datetime
import secrets
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from enum import Enum
class RiskLevel(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class RequestStatus(Enum):
PENDING = "pending"
APPROVED = "approved"
DENIED = "denied"
ACTIVE = "active"
EXPIRED = "expired"
REVOKED = "revoked"
EMERGENCY = "emergency"
@dataclass
class JITAccessRequest:
"""A just-in-time access request."""
request_id: str
requester: str
target_resource: str
resource_type: str # server, database, application, cloud_role
requested_duration_minutes: int
justification: str
risk_level: RiskLevel = RiskLevel.MEDIUM
status: RequestStatus = RequestStatus.PENDING
approvers: List[str] = field(default_factory=list)
approved_by: List[str] = field(default_factory=list)
denied_by: str = ""
created_at: str = ""
granted_at: str = ""
expires_at: str = ""
revoked_at: str = ""
is_emergency: bool = False
ticket_id: str = ""
@dataclass
class ResourcePolicy:
"""Policy for a protected resource."""
resource_pattern: str
resource_type: str
max_duration_minutes: int
risk_level: RiskLevel
auto_approve: bool = False
required_approvals: int = 1
approver_roles: List[str] = field(default_factory=list)
mfa_required: bool = True
session_recording: bool = False
class JITAccessEngine:
"""Manages the full JIT access lifecycle."""
def __init__(self):
self.requests: Dict[str, JITAccessRequest] = {}
self.policies: List[ResourcePolicy] = []
self.audit_log: List[Dict] = []
def add_policy(self, policy: ResourcePolicy):
"""Register a resource access policy."""
self.policies.append(policy)
def _get_policy(self, resource: str, resource_type: str) -> Optional[ResourcePolicy]:
"""Find matching policy for a resource."""
for policy in self.policies:
if policy.resource_type == resource_type:
if policy.resource_pattern == "*" or policy.resource_pattern in resource:
return policy
return None
def _generate_request_id(self) -> str:
return f"JIT-{datetime.datetime.now().strftime('%Y%m%d')}-{secrets.token_hex(4).upper()}"
def _log_event(self, event_type: str, request_id: str, details: Dict):
"""Record audit event."""
self.audit_log.append({
"timestamp": datetime.datetime.now().isoformat(),
"event_type": event_type,
"request_id": request_id,
**details
})
def submit_request(self, requester: str, target_resource: str,
resource_type: str, duration_minutes: int,
justification: str, is_emergency: bool = False,
ticket_id: str = "") -> JITAccessRequest:
"""Submit a new JIT access request."""
policy = self._get_policy(target_resource, resource_type)
if not policy:
raise ValueError(f"No policy found for resource type: {resource_type}")
# Enforce maximum duration
actual_duration = min(duration_minutes, policy.max_duration_minutes)
if is_emergency:
actual_duration = min(actual_duration, 120) # 2-hour max for emergency
request = JITAccessRequest(
request_id=self._generate_request_id(),
requester=requester,
target_resource=target_resource,
resource_type=resource_type,
requested_duration_minutes=actual_duration,
justification=justification,
risk_level=policy.risk_level,
created_at=datetime.datetime.now().isoformat(),
is_emergency=is_emergency,
ticket_id=ticket_id
)
if is_emergency:
# Emergency: grant immediately, require post-facto review
request.status = RequestStatus.EMERGENCY
now = datetime.datetime.now()
request.granted_at = now.isoformat()
request.expires_at = (now + datetime.timedelta(minutes=actual_duration)).isoformat()
self._log_event("EMERGENCY_GRANT", request.request_id, {
"requester": requester,
"resource": target_resource,
"duration_minutes": actual_duration,
"justification": justification
})
elif policy.auto_approve and policy.risk_level == RiskLevel.LOW:
# Auto-approve low-risk
request.status = RequestStatus.APPROVED
request.approved_by = ["AUTO"]
self._log_event("AUTO_APPROVED", request.request_id, {
"requester": requester,
"resource": target_resource,
"reason": "Low-risk auto-approve policy"
})
self._activate_access(request)
else:
# Route for approval
request.approvers = policy.approver_roles
request.status = RequestStatus.PENDING
self._log_event("REQUEST_SUBMITTED", request.request_id, {
"requester": requester,
"resource": target_resource,
"required_approvals": policy.required_approvals,
"approvers": policy.approver_roles
})
self.requests[request.request_id] = request
return request
def approve_request(self, request_id: str, approver: str) -> JITAccessRequest:
"""Approve a JIT access request."""
request = self.requests.get(request_id)
if not request:
raise ValueError(f"Request not found: {request_id}")
if request.status not in (RequestStatus.PENDING,):
raise ValueError(f"Request {request_id} is not pending approval")
request.approved_by.append(approver)
policy = self._get_policy(request.target_resource, request.resource_type)
required = policy.required_approvals if policy else 1
self._log_event("APPROVAL_RECORDED", request_id, {
"approver": approver,
"approvals_count": len(request.approved_by),
"required": required
})
if len(request.approved_by) >= required:
request.status = RequestStatus.APPROVED
self._activate_access(request)
return request
def deny_request(self, request_id: str, denier: str, reason: str = "") -> JITAccessRequest:
"""Deny a JIT access request."""
request = self.requests.get(request_id)
if not request:
raise ValueError(f"Request not found: {request_id}")
request.status = RequestStatus.DENIED
request.denied_by = denier
self._log_event("REQUEST_DENIED", request_id, {
"denied_by": denier,
"reason": reason
})
return request
def _activate_access(self, request: JITAccessRequest):
"""Activate the approved access grant."""
now = datetime.datetime.now()
request.status = RequestStatus.ACTIVE
request.granted_at = now.isoformat()
request.expires_at = (now + datetime.timedelta(
minutes=request.requested_duration_minutes
)).isoformat()
self._log_event("ACCESS_ACTIVATED", request.request_id, {
"requester": request.requester,
"resource": request.target_resource,
"granted_at": request.granted_at,
"expires_at": request.expires_at
})
def check_expirations(self) -> List[JITAccessRequest]:
"""Check and revoke expired access grants."""
now = datetime.datetime.now()
expired = []
for request in self.requests.values():
if request.status in (RequestStatus.ACTIVE, RequestStatus.EMERGENCY):
if request.expires_at:
expiry = datetime.datetime.fromisoformat(request.expires_at)
if now >= expiry:
request.status = RequestStatus.EXPIRED
request.revoked_at = now.isoformat()
expired.append(request)
self._log_event("ACCESS_EXPIRED", request.request_id, {
"requester": request.requester,
"resource": request.target_resource,
"expired_at": request.revoked_at
})
return expired
def revoke_access(self, request_id: str, reason: str = "") -> JITAccessRequest:
"""Manually revoke an active access grant."""
request = self.requests.get(request_id)
if not request:
raise ValueError(f"Request not found: {request_id}")
request.status = RequestStatus.REVOKED
request.revoked_at = datetime.datetime.now().isoformat()
self._log_event("ACCESS_REVOKED", request_id, {
"requester": request.requester,
"resource": request.target_resource,
"reason": reason
})
return request
def get_active_grants(self) -> List[JITAccessRequest]:
"""List all currently active access grants."""
return [r for r in self.requests.values()
if r.status in (RequestStatus.ACTIVE, RequestStatus.EMERGENCY)]
def get_metrics(self) -> Dict:
"""Calculate JIT access metrics."""
all_requests = list(self.requests.values())
total = len(all_requests)
if total == 0:
return {"total": 0}
by_status = {}
for r in all_requests:
by_status[r.status.value] = by_status.get(r.status.value, 0) + 1
emergency_count = sum(1 for r in all_requests if r.is_emergency)
# Calculate mean time to access
approved = [r for r in all_requests if r.granted_at and r.created_at]
if approved:
total_wait = sum(
(datetime.datetime.fromisoformat(r.granted_at) -
datetime.datetime.fromisoformat(r.created_at)).total_seconds()
for r in approved
)
mean_tta = total_wait / len(approved) / 60 # minutes
else:
mean_tta = 0
return {
"total_requests": total,
"by_status": by_status,
"emergency_grants": emergency_count,
"active_grants": len(self.get_active_grants()),
"mean_time_to_access_minutes": round(mean_tta, 1),
"audit_events": len(self.audit_log)
}
def generate_report(self) -> str:
"""Generate JIT access report."""
metrics = self.get_metrics()
active = self.get_active_grants()
lines = [
"=" * 70,
"JUST-IN-TIME ACCESS PROVISIONING REPORT",
"=" * 70,
f"Report Date: {datetime.datetime.now().isoformat()}",
f"Total Requests: {metrics['total_requests']}",
f"Active Grants: {metrics['active_grants']}",
f"Emergency Grants: {metrics['emergency_grants']}",
f"Mean Time to Access: {metrics['mean_time_to_access_minutes']} minutes",
f"Audit Events: {metrics['audit_events']}",
"-" * 70,
"",
"STATUS BREAKDOWN:",
]
for status, count in metrics.get("by_status", {}).items():
lines.append(f" {status}: {count}")
lines.append("")
if active:
lines.append("ACTIVE GRANTS:")
lines.append("-" * 40)
for r in active:
flag = " [EMERGENCY]" if r.is_emergency else ""
lines.append(f" {r.request_id}: {r.requester} -> {r.target_resource}{flag}")
lines.append(f" Expires: {r.expires_at}")
lines.append(f" Justification: {r.justification}")
lines.append("")
lines.append("=" * 70)
return "\n".join(lines)
def main():
"""Demo JIT access engine."""
engine = JITAccessEngine()
# Define resource policies
engine.add_policy(ResourcePolicy(
resource_pattern="*", resource_type="read_only",
max_duration_minutes=60, risk_level=RiskLevel.LOW,
auto_approve=True, required_approvals=0
))
engine.add_policy(ResourcePolicy(
resource_pattern="*", resource_type="production_server",
max_duration_minutes=240, risk_level=RiskLevel.HIGH,
auto_approve=False, required_approvals=2,
approver_roles=["manager", "security_team"],
session_recording=True
))
engine.add_policy(ResourcePolicy(
resource_pattern="*", resource_type="database_admin",
max_duration_minutes=120, risk_level=RiskLevel.CRITICAL,
auto_approve=False, required_approvals=2,
approver_roles=["dba_lead", "security_team"],
mfa_required=True, session_recording=True
))
# Submit requests
r1 = engine.submit_request("alice", "docs-server", "read_only", 30,
"Need to check documentation")
r2 = engine.submit_request("bob", "prod-web-01", "production_server", 120,
"Deploy hotfix for CVE-2026-1234", ticket_id="INC-5678")
r3 = engine.submit_request("charlie", "prod-db-01", "database_admin", 60,
"Critical production outage", is_emergency=True)
# Approve prod server access
engine.approve_request(r2.request_id, "manager_dave")
engine.approve_request(r2.request_id, "security_eve")
print(engine.generate_report())
if __name__ == "__main__":
main()