mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 22:24:56 +03:00
c21af3347e
- Add scripts/agent.py and references/api-reference.md to all remaining skills - Update all 648 LICENSE files: copyright now reads 'Mahipal' - Add implementing-security-monitoring-with-datadog (new skill with full anatomy) - All 649 skills now have: SKILL.md, LICENSE, scripts/agent.py, references/api-reference.md
216 lines
7.8 KiB
Python
216 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Agent for securing historian servers in OT environments.
|
|
|
|
Audits network exposure, authentication configuration, data
|
|
integrity protections, and DMZ replication architecture of
|
|
process historian servers (OSIsoft PI, AVEVA, Honeywell PHD).
|
|
"""
|
|
|
|
import json
|
|
import socket
|
|
import sys
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
|
|
HISTORIAN_PORTS = {
|
|
5450: ("PI Data Archive", "PI SDK/API connections"),
|
|
5457: ("PI AF Server", "PI Asset Framework"),
|
|
5459: ("PI Notifications", "Notification Service"),
|
|
443: ("HTTPS", "Web API / PI Vision"),
|
|
80: ("HTTP", "Unsecured web interface"),
|
|
1433: ("MS SQL Server", "Direct database access"),
|
|
5432: ("PostgreSQL", "Direct database access"),
|
|
3389: ("RDP", "Remote Desktop"),
|
|
135: ("RPC", "Windows RPC"),
|
|
445: ("SMB", "Windows File Sharing"),
|
|
8080: ("HTTP Alt", "Alternative web interface"),
|
|
502: ("Modbus", "Industrial protocol"),
|
|
}
|
|
|
|
UNNECESSARY_PORTS = {80, 135, 445, 3389, 8080}
|
|
|
|
|
|
class HistorianSecurityAgent:
|
|
"""Audits OT historian server security configuration."""
|
|
|
|
def __init__(self, historian_ip, historian_type="PI",
|
|
output_dir="./historian_audit"):
|
|
self.ip = historian_ip
|
|
self.hist_type = historian_type
|
|
self.output_dir = Path(output_dir)
|
|
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
self.findings = []
|
|
|
|
def check_network_exposure(self):
|
|
"""Scan historian for exposed network services."""
|
|
exposed = []
|
|
for port, (service, desc) in HISTORIAN_PORTS.items():
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(3)
|
|
result = sock.connect_ex((self.ip, port))
|
|
sock.close()
|
|
if result == 0:
|
|
exposed.append({
|
|
"port": port, "service": service, "description": desc,
|
|
"unnecessary": port in UNNECESSARY_PORTS,
|
|
})
|
|
except (socket.error, OSError):
|
|
pass
|
|
|
|
for svc in exposed:
|
|
if svc["unnecessary"]:
|
|
self.findings.append({
|
|
"severity": "high",
|
|
"category": "Network Exposure",
|
|
"title": f"Unnecessary service: {svc['service']} (port {svc['port']})",
|
|
"remediation": f"Disable {svc['service']} or restrict via firewall",
|
|
})
|
|
if any(s["port"] == 80 for s in exposed):
|
|
self.findings.append({
|
|
"severity": "high",
|
|
"category": "Encryption",
|
|
"title": "HTTP (unencrypted) web interface exposed",
|
|
"remediation": "Redirect HTTP to HTTPS and disable port 80",
|
|
})
|
|
return exposed
|
|
|
|
def check_authentication(self):
|
|
"""Evaluate historian authentication configuration risks."""
|
|
checks = [
|
|
{
|
|
"check": "PI Trust Authentication",
|
|
"severity": "critical",
|
|
"risk": "IP-based auth without credentials",
|
|
"remediation": "Migrate to Windows Integrated Security",
|
|
},
|
|
{
|
|
"check": "Default piadmin account",
|
|
"severity": "critical",
|
|
"risk": "Default admin may have weak password",
|
|
"remediation": "Disable piadmin, use named Windows accounts",
|
|
},
|
|
{
|
|
"check": "Anonymous SDK access",
|
|
"severity": "high",
|
|
"risk": "Unauthenticated PI SDK connections",
|
|
"remediation": "Require authentication for all connections",
|
|
},
|
|
{
|
|
"check": "Shared service accounts",
|
|
"severity": "medium",
|
|
"risk": "Non-attributable access to historian data",
|
|
"remediation": "Use individual named accounts with PI Mappings",
|
|
},
|
|
]
|
|
for c in checks:
|
|
self.findings.append({
|
|
"severity": c["severity"],
|
|
"category": "Authentication",
|
|
"title": f"Review: {c['check']}",
|
|
"detail": c["risk"],
|
|
"remediation": c["remediation"],
|
|
})
|
|
return checks
|
|
|
|
def check_data_integrity(self):
|
|
"""Evaluate data integrity protections."""
|
|
checks = [
|
|
{
|
|
"check": "Audit trail for data modifications",
|
|
"severity": "high",
|
|
"detail": "Historical data edits must be logged with before/after values",
|
|
"remediation": "Enable PI audit trail for all modifications",
|
|
},
|
|
{
|
|
"check": "Backup integrity verification",
|
|
"severity": "medium",
|
|
"detail": "Backups should be tested regularly for recovery",
|
|
"remediation": "Implement automated backup verification quarterly",
|
|
},
|
|
{
|
|
"check": "Tag security enforcement",
|
|
"severity": "high",
|
|
"detail": "Per-tag access control must restrict write access",
|
|
"remediation": "Configure tag-level security for critical process points",
|
|
},
|
|
]
|
|
for c in checks:
|
|
self.findings.append({
|
|
"severity": c["severity"],
|
|
"category": "Data Integrity",
|
|
"title": c["check"],
|
|
"detail": c["detail"],
|
|
"remediation": c["remediation"],
|
|
})
|
|
return checks
|
|
|
|
def check_dmz_architecture(self):
|
|
"""Evaluate historian DMZ replication architecture."""
|
|
checks = [
|
|
{
|
|
"check": "Data diode or unidirectional gateway",
|
|
"severity": "high",
|
|
"detail": "OT-to-IT replication should use hardware-enforced unidirectional flow",
|
|
"remediation": "Deploy Waterfall or equivalent data diode between OT and DMZ",
|
|
},
|
|
{
|
|
"check": "Enterprise direct access to OT historian",
|
|
"severity": "critical",
|
|
"detail": "Enterprise users must never connect directly to OT historian",
|
|
"remediation": "Route all access through DMZ historian replica",
|
|
},
|
|
]
|
|
for c in checks:
|
|
self.findings.append({
|
|
"severity": c["severity"],
|
|
"category": "DMZ Architecture",
|
|
"title": c["check"],
|
|
"detail": c["detail"],
|
|
"remediation": c["remediation"],
|
|
})
|
|
return checks
|
|
|
|
def generate_report(self):
|
|
exposed = self.check_network_exposure()
|
|
auth = self.check_authentication()
|
|
integrity = self.check_data_integrity()
|
|
dmz = self.check_dmz_architecture()
|
|
|
|
severity_counts = {}
|
|
for f in self.findings:
|
|
sev = f["severity"]
|
|
severity_counts[sev] = severity_counts.get(sev, 0) + 1
|
|
|
|
report = {
|
|
"report_date": datetime.utcnow().isoformat(),
|
|
"historian_ip": self.ip,
|
|
"historian_type": self.hist_type,
|
|
"exposed_services": exposed,
|
|
"severity_summary": severity_counts,
|
|
"total_findings": len(self.findings),
|
|
"findings": self.findings,
|
|
}
|
|
out = self.output_dir / "historian_audit_report.json"
|
|
with open(out, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
print(json.dumps(report, indent=2))
|
|
return report
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: agent.py <historian_ip> [--type PI|AVEVA|PHD]")
|
|
sys.exit(1)
|
|
ip = sys.argv[1]
|
|
hist_type = "PI"
|
|
if "--type" in sys.argv:
|
|
hist_type = sys.argv[sys.argv.index("--type") + 1]
|
|
agent = HistorianSecurityAgent(ip, hist_type)
|
|
agent.generate_report()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|