mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 14:14:56 +03:00
c47eed6a64
- 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
200 lines
7.0 KiB
Python
200 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Historian server attack detection agent for ICS/SCADA environments."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import argparse
|
|
import socket
|
|
from datetime import datetime
|
|
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
print("Install: pip install requests")
|
|
sys.exit(1)
|
|
|
|
|
|
HISTORIAN_PORTS = {
|
|
5450: "OSIsoft PI AF",
|
|
5457: "OSIsoft PI Data Archive",
|
|
5459: "OSIsoft PI Web API",
|
|
1433: "SQL Server (Wonderware/FactoryTalk)",
|
|
3306: "MySQL (Ignition)",
|
|
8088: "Ignition Gateway",
|
|
443: "HTTPS (PI Web API / Ignition)",
|
|
}
|
|
|
|
|
|
def scan_historian_ports(host):
|
|
"""Scan for exposed historian service ports."""
|
|
results = []
|
|
for port, service in HISTORIAN_PORTS.items():
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(3)
|
|
status = sock.connect_ex((host, port)) == 0
|
|
sock.close()
|
|
result = {"host": host, "port": port, "service": service, "open": status}
|
|
if status:
|
|
result["finding"] = f"Historian port {port} ({service}) accessible"
|
|
result["severity"] = "HIGH"
|
|
results.append(result)
|
|
except socket.error:
|
|
pass
|
|
return results
|
|
|
|
|
|
def check_pi_web_api(host, username=None, password=None):
|
|
"""Check OSIsoft PI Web API for authentication and configuration issues."""
|
|
base = f"https://{host}/piwebapi"
|
|
auth = (username, password) if username else None
|
|
results = {"host": host, "checks": []}
|
|
|
|
try:
|
|
resp = requests.get(f"{base}/system", auth=auth,
|
|
verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true", timeout=10) # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
results["product_version"] = data.get("ProductTitle", "")
|
|
results["checks"].append({
|
|
"check": "PI Web API accessible",
|
|
"status": "PASS" if auth else "FAIL",
|
|
"detail": "Anonymous access enabled" if not auth and resp.status_code == 200 else "",
|
|
"severity": "CRITICAL" if not auth else "INFO",
|
|
})
|
|
except requests.exceptions.ConnectionError:
|
|
results["checks"].append({"check": "PI Web API", "status": "UNREACHABLE"})
|
|
except Exception as e:
|
|
results["error"] = str(e)
|
|
|
|
try:
|
|
resp = requests.get(f"{base}/points", auth=auth,
|
|
verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true",
|
|
params={"maxCount": 10}, timeout=10)
|
|
if resp.status_code == 200:
|
|
points = resp.json().get("Items", [])
|
|
results["exposed_points"] = len(points)
|
|
results["sample_points"] = [p.get("Name", "") for p in points[:5]]
|
|
if not auth:
|
|
results["checks"].append({
|
|
"check": "Point data accessible without auth",
|
|
"status": "FAIL",
|
|
"severity": "CRITICAL",
|
|
})
|
|
except Exception:
|
|
pass
|
|
|
|
return results
|
|
|
|
|
|
def check_ignition_gateway(host, port=8088):
|
|
"""Check Inductive Automation Ignition gateway status."""
|
|
results = {"host": host, "port": port}
|
|
try:
|
|
resp = requests.get(f"http://{host}:{port}/StatusPing", timeout=10)
|
|
if resp.status_code == 200:
|
|
results["gateway_accessible"] = True
|
|
results["response"] = resp.text[:200]
|
|
|
|
resp2 = requests.get(f"http://{host}:{port}/system/gwinfo", timeout=10)
|
|
if resp2.status_code == 200:
|
|
results["gateway_info_exposed"] = True
|
|
results["finding"] = "Ignition gateway info page accessible"
|
|
results["severity"] = "HIGH"
|
|
except Exception as e:
|
|
results["error"] = str(e)
|
|
return results
|
|
|
|
|
|
def analyze_historian_logs(log_entries):
|
|
"""Analyze historian access logs for attack indicators."""
|
|
findings = []
|
|
failed_logins = {}
|
|
bulk_reads = {}
|
|
|
|
for entry in log_entries:
|
|
if entry.get("event_type") == "login_failed":
|
|
src = entry.get("src_ip", "")
|
|
failed_logins[src] = failed_logins.get(src, 0) + 1
|
|
if entry.get("event_type") == "data_read" and entry.get("point_count", 0) > 1000:
|
|
src = entry.get("src_ip", "")
|
|
bulk_reads[src] = bulk_reads.get(src, 0) + entry["point_count"]
|
|
|
|
for ip, count in failed_logins.items():
|
|
if count > 5:
|
|
findings.append({
|
|
"ip": ip,
|
|
"issue": f"Brute force attempt: {count} failed logins",
|
|
"severity": "HIGH",
|
|
})
|
|
|
|
for ip, points in bulk_reads.items():
|
|
if points > 10000:
|
|
findings.append({
|
|
"ip": ip,
|
|
"issue": f"Bulk data exfiltration: {points} points read",
|
|
"severity": "CRITICAL",
|
|
})
|
|
|
|
return findings
|
|
|
|
|
|
def run_audit(args):
|
|
"""Execute historian server attack detection audit."""
|
|
print(f"\n{'='*60}")
|
|
print(f" HISTORIAN SERVER ATTACK DETECTION")
|
|
print(f" Generated: {datetime.utcnow().isoformat()} UTC")
|
|
print(f"{'='*60}\n")
|
|
|
|
report = {}
|
|
|
|
if args.host:
|
|
port_scan = scan_historian_ports(args.host)
|
|
open_ports = [p for p in port_scan if p.get("open")]
|
|
report["port_scan"] = port_scan
|
|
print(f"--- HISTORIAN PORT SCAN ({args.host}) ---")
|
|
for p in open_ports:
|
|
print(f" [{p.get('severity','INFO')}] Port {p['port']}: {p['service']}")
|
|
if not open_ports:
|
|
print(" No historian ports detected")
|
|
|
|
if args.pi_host:
|
|
pi = check_pi_web_api(args.pi_host, args.pi_user, args.pi_pass)
|
|
report["pi_web_api"] = pi
|
|
print(f"\n--- PI WEB API CHECK ---")
|
|
for c in pi.get("checks", []):
|
|
print(f" [{c.get('severity','INFO')}] {c['check']}: {c['status']}")
|
|
|
|
if args.ignition_host:
|
|
ign = check_ignition_gateway(args.ignition_host, args.ignition_port or 8088)
|
|
report["ignition_gateway"] = ign
|
|
print(f"\n--- IGNITION GATEWAY CHECK ---")
|
|
print(f" Accessible: {ign.get('gateway_accessible', False)}")
|
|
if ign.get("finding"):
|
|
print(f" [{ign['severity']}] {ign['finding']}")
|
|
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Historian Attack Detection Agent")
|
|
parser.add_argument("--host", help="Historian server to scan")
|
|
parser.add_argument("--pi-host", help="OSIsoft PI Web API host")
|
|
parser.add_argument("--pi-user", help="PI username")
|
|
parser.add_argument("--pi-pass", help="PI password")
|
|
parser.add_argument("--ignition-host", help="Ignition gateway host")
|
|
parser.add_argument("--ignition-port", type=int, default=8088)
|
|
parser.add_argument("--output", help="Save report to JSON file")
|
|
args = parser.parse_args()
|
|
|
|
report = run_audit(args)
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2, default=str)
|
|
print(f"\n[+] Report saved to {args.output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|