Files
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

194 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""Agent for securing remote access to OT environments.
Manages remote access sessions with MFA verification, session
recording, role-based access policies, vendor co-attendance
requirements, and CIP-005 compliance auditing.
"""
import json
import hashlib
from pathlib import Path
from datetime import datetime
from enum import Enum
class SessionState(str, Enum):
PENDING = "pending_approval"
APPROVED = "approved"
ACTIVE = "active"
TERMINATED = "terminated"
EXPIRED = "expired"
DENIED = "denied"
class UserRole(str, Enum):
OT_OPERATOR = "ot_operator"
OT_ENGINEER = "ot_engineer"
VENDOR = "vendor"
SECURITY = "security_analyst"
class OTRemoteAccessAgent:
"""Manages secure remote access to OT environments."""
def __init__(self, output_dir="./ot_remote_access"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
self.sessions = {}
self.policies = {}
self.audit_log = []
def define_policy(self, role, allowed_targets, protocols, max_minutes):
"""Define access policy for a user role."""
self.policies[role] = {
"allowed_targets": allowed_targets,
"protocols": protocols,
"max_duration_minutes": max_minutes,
"requires_approval": role == UserRole.VENDOR,
"requires_co_attendance": role == UserRole.VENDOR,
}
def request_session(self, user_id, role, source_ip, target, target_ip,
protocol, purpose):
"""Request a new remote access session."""
sid = hashlib.sha256(
f"{user_id}{target_ip}{datetime.utcnow().isoformat()}".encode()
).hexdigest()[:16]
policy = self.policies.get(role)
if not policy:
self._log("DENIED", user_id, target, "No policy for role")
return None, "No policy defined for role"
if target not in policy["allowed_targets"]:
self._log("DENIED", user_id, target, "Target not authorized")
return None, f"Target {target} not authorized for {role}"
if protocol not in policy["protocols"]:
self._log("DENIED", user_id, target, f"Protocol {protocol} not allowed")
return None, f"Protocol {protocol} not authorized"
session = {
"session_id": sid,
"user_id": user_id,
"role": role,
"source_ip": source_ip,
"target": target,
"target_ip": target_ip,
"protocol": protocol,
"purpose": purpose,
"state": SessionState.PENDING if policy["requires_approval"] else SessionState.APPROVED,
"mfa_verified": False,
"created_at": datetime.utcnow().isoformat(),
"max_duration": policy["max_duration_minutes"],
}
self.sessions[sid] = session
self._log("REQUESTED", user_id, target, purpose)
return sid, "Session created"
def approve_session(self, sid, approver_id):
"""Approve a pending session."""
s = self.sessions.get(sid)
if not s or s["state"] != SessionState.PENDING:
return False, "Session not pending"
s["state"] = SessionState.APPROVED
s["approved_by"] = approver_id
self._log("APPROVED", approver_id, s["target"], f"Session {sid}")
return True, "Approved"
def activate_session(self, sid, mfa_verified=True):
"""Activate session after MFA verification."""
s = self.sessions.get(sid)
if not s or s["state"] != SessionState.APPROVED:
return False, "Not approved"
s["state"] = SessionState.ACTIVE
s["mfa_verified"] = mfa_verified
s["started_at"] = datetime.utcnow().isoformat()
s["recording_path"] = f"/recordings/{sid}_{datetime.utcnow().strftime('%Y%m%d')}.mp4"
self._log("ACTIVATED", s["user_id"], s["target"], f"MFA={'OK' if mfa_verified else 'FAIL'}")
return True, "Active"
def terminate_session(self, sid, reason="manual"):
"""Terminate an active session."""
s = self.sessions.get(sid)
if not s:
return False
s["state"] = SessionState.TERMINATED
s["ended_at"] = datetime.utcnow().isoformat()
self._log("TERMINATED", s["user_id"], s["target"], reason)
return True
def check_expired(self):
"""Find and terminate expired sessions."""
expired = []
now = datetime.utcnow()
for sid, s in self.sessions.items():
if s["state"] != SessionState.ACTIVE:
continue
started = datetime.fromisoformat(s["started_at"])
if (now - started).total_seconds() > s["max_duration"] * 60:
self.terminate_session(sid, "max_duration_exceeded")
expired.append(sid)
return expired
def audit_compliance(self):
"""Check CIP-005 remote access compliance."""
issues = []
for sid, s in self.sessions.items():
if s["state"] == SessionState.ACTIVE and not s["mfa_verified"]:
issues.append({"session": sid, "issue": "Active session without MFA (CIP-005-7 R2.4)"})
if s["role"] == UserRole.VENDOR:
policy = self.policies.get(UserRole.VENDOR, {})
if policy.get("requires_co_attendance") and "co_attendant" not in s:
issues.append({"session": sid, "issue": "Vendor session without co-attendance"})
return issues
def _log(self, event, user, target, detail):
self.audit_log.append({
"timestamp": datetime.utcnow().isoformat(),
"event": event,
"user": user,
"target": target,
"detail": detail,
})
def generate_report(self):
compliance = self.audit_compliance()
active = [s for s in self.sessions.values() if s["state"] == SessionState.ACTIVE]
report = {
"report_date": datetime.utcnow().isoformat(),
"total_sessions": len(self.sessions),
"active_sessions": len(active),
"compliance_issues": compliance,
"sessions": list(self.sessions.values()),
"audit_log": self.audit_log[-50:],
}
out = self.output_dir / "ot_remote_access_report.json"
with open(out, "w") as f:
json.dump(report, f, indent=2)
print(json.dumps(report, indent=2))
return report
def main():
agent = OTRemoteAccessAgent()
agent.define_policy(UserRole.OT_ENGINEER, ["HMI-01", "EWS-01", "HISTORIAN-01"],
["RDP", "SSH"], 240)
agent.define_policy(UserRole.VENDOR, ["DCS-EWS-01"], ["RDP"], 120)
sid, msg = agent.request_session("vendor_01", UserRole.VENDOR,
"203.0.113.50", "DCS-EWS-01",
"10.30.1.20", "RDP",
"DCS firmware update CR-2026-0045")
if sid:
agent.approve_session(sid, "ot_manager_01")
agent.activate_session(sid)
agent.generate_report()
if __name__ == "__main__":
main()