mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 13:44:56 +03:00
230 lines
8.3 KiB
Python
230 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Vulnerability Exception Tracking System.
|
|
|
|
Manages vulnerability exception requests, approvals, expiration tracking,
|
|
and compensating controls documentation.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
|
|
DB_PATH = os.environ.get("EXCEPTION_DB_PATH", "vulnerability_exceptions.db")
|
|
|
|
|
|
def init_db(db_path=DB_PATH):
|
|
conn = sqlite3.connect(db_path)
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS vulnerability_exceptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
cve_id TEXT NOT NULL,
|
|
finding_id TEXT NOT NULL,
|
|
asset_hostname TEXT,
|
|
severity TEXT,
|
|
cvss_score REAL,
|
|
category TEXT NOT NULL,
|
|
justification TEXT NOT NULL,
|
|
compensating_controls TEXT,
|
|
status TEXT DEFAULT 'pending',
|
|
requested_by TEXT NOT NULL,
|
|
approved_by TEXT,
|
|
requested_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
approved_at TEXT,
|
|
expires_at TEXT NOT NULL,
|
|
risk_rating TEXT,
|
|
review_notes TEXT,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS exception_audit_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
exception_id INTEGER,
|
|
action TEXT NOT NULL,
|
|
actor TEXT NOT NULL,
|
|
details TEXT,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (exception_id) REFERENCES vulnerability_exceptions(id)
|
|
)
|
|
""")
|
|
conn.commit()
|
|
return conn
|
|
|
|
|
|
def create_exception(conn, data):
|
|
max_days = {
|
|
"remediation_delay": 30,
|
|
"no_fix": 90,
|
|
"business_critical": 60,
|
|
"false_positive": 365,
|
|
"compensating_control": 180,
|
|
}
|
|
category = data.get("category", "remediation_delay")
|
|
expires = data.get("expires_at")
|
|
if expires:
|
|
exp_date = datetime.fromisoformat(expires)
|
|
max_exp = datetime.now(timezone.utc) + timedelta(days=max_days.get(category, 30))
|
|
if exp_date.replace(tzinfo=timezone.utc) > max_exp:
|
|
print(f"[-] Expiration exceeds maximum {max_days[category]} days for {category}")
|
|
return None
|
|
|
|
cursor = conn.execute(
|
|
"""INSERT INTO vulnerability_exceptions
|
|
(cve_id, finding_id, asset_hostname, severity, cvss_score, category,
|
|
justification, compensating_controls, requested_by, expires_at, risk_rating)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(
|
|
data["cve_id"], data["finding_id"], data.get("asset_hostname", ""),
|
|
data.get("severity", ""), data.get("cvss_score", 0),
|
|
category, data["justification"],
|
|
json.dumps(data.get("compensating_controls", [])),
|
|
data["requestor_email"], expires, data.get("risk_rating", "medium"),
|
|
),
|
|
)
|
|
exc_id = cursor.lastrowid
|
|
conn.execute(
|
|
"INSERT INTO exception_audit_log (exception_id, action, actor, details) VALUES (?, ?, ?, ?)",
|
|
(exc_id, "created", data["requestor_email"], f"Exception request for {data['cve_id']}"),
|
|
)
|
|
conn.commit()
|
|
print(f"[+] Exception created: ID {exc_id} for {data['cve_id']}")
|
|
return exc_id
|
|
|
|
|
|
def approve_exception(conn, exc_id, approver_email, notes=""):
|
|
conn.execute(
|
|
"""UPDATE vulnerability_exceptions
|
|
SET status = 'approved', approved_by = ?, approved_at = ?, review_notes = ?
|
|
WHERE id = ?""",
|
|
(approver_email, datetime.now(timezone.utc).isoformat(), notes, exc_id),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO exception_audit_log (exception_id, action, actor, details) VALUES (?, ?, ?, ?)",
|
|
(exc_id, "approved", approver_email, notes),
|
|
)
|
|
conn.commit()
|
|
print(f"[+] Exception {exc_id} approved by {approver_email}")
|
|
|
|
|
|
def reject_exception(conn, exc_id, reviewer_email, reason):
|
|
conn.execute(
|
|
"UPDATE vulnerability_exceptions SET status = 'rejected', review_notes = ? WHERE id = ?",
|
|
(reason, exc_id),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO exception_audit_log (exception_id, action, actor, details) VALUES (?, ?, ?, ?)",
|
|
(exc_id, "rejected", reviewer_email, reason),
|
|
)
|
|
conn.commit()
|
|
print(f"[+] Exception {exc_id} rejected by {reviewer_email}: {reason}")
|
|
|
|
|
|
def check_expirations(conn, slack_webhook=None):
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
warn_date = (datetime.now(timezone.utc) + timedelta(days=14)).isoformat()
|
|
|
|
expiring_soon = conn.execute(
|
|
"SELECT * FROM vulnerability_exceptions WHERE status = 'approved' AND expires_at BETWEEN ? AND ?",
|
|
(now, warn_date),
|
|
).fetchall()
|
|
|
|
expired = conn.execute(
|
|
"SELECT * FROM vulnerability_exceptions WHERE status = 'approved' AND expires_at < ?",
|
|
(now,),
|
|
).fetchall()
|
|
|
|
columns = [d[0] for d in conn.execute("SELECT * FROM vulnerability_exceptions LIMIT 0").description]
|
|
|
|
for row in expired:
|
|
record = dict(zip(columns, row))
|
|
conn.execute("UPDATE vulnerability_exceptions SET status = 'expired' WHERE id = ?", (record["id"],))
|
|
conn.execute(
|
|
"INSERT INTO exception_audit_log (exception_id, action, actor, details) VALUES (?, ?, ?, ?)",
|
|
(record["id"], "expired", "system", f"Exception expired on {record['expires_at']}"),
|
|
)
|
|
print(f"[!] Exception {record['id']} ({record['cve_id']}) EXPIRED")
|
|
|
|
conn.commit()
|
|
|
|
print(f"\n[*] Expiration Check Results:")
|
|
print(f" Expired: {len(expired)}")
|
|
print(f" Expiring within 14 days: {len(expiring_soon)}")
|
|
|
|
if slack_webhook and (expired or expiring_soon):
|
|
payload = {
|
|
"text": f"Vulnerability Exception Alert: {len(expired)} expired, {len(expiring_soon)} expiring soon"
|
|
}
|
|
requests.post(slack_webhook, json=payload, timeout=10)
|
|
|
|
return {"expired": len(expired), "expiring_soon": len(expiring_soon)}
|
|
|
|
|
|
def generate_report(conn, output_path):
|
|
report = {
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"summary": {},
|
|
"exceptions": [],
|
|
}
|
|
cursor = conn.execute(
|
|
"""SELECT status, COUNT(*) as count FROM vulnerability_exceptions GROUP BY status"""
|
|
)
|
|
for row in cursor.fetchall():
|
|
report["summary"][row[0]] = row[1]
|
|
|
|
cursor = conn.execute(
|
|
"SELECT * FROM vulnerability_exceptions ORDER BY CASE status "
|
|
"WHEN 'expired' THEN 1 WHEN 'pending' THEN 2 WHEN 'approved' THEN 3 ELSE 4 END"
|
|
)
|
|
columns = [d[0] for d in cursor.description]
|
|
for row in cursor.fetchall():
|
|
report["exceptions"].append(dict(zip(columns, row)))
|
|
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
json.dump(report, f, indent=2)
|
|
print(f"[+] Exception report written to {output_path}")
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Vulnerability Exception Tracking System")
|
|
parser.add_argument("--db", default=DB_PATH)
|
|
parser.add_argument("--create", help="JSON file with exception request")
|
|
parser.add_argument("--approve", type=int, help="Exception ID to approve")
|
|
parser.add_argument("--reject", type=int, help="Exception ID to reject")
|
|
parser.add_argument("--approver", help="Approver email")
|
|
parser.add_argument("--reason", help="Rejection reason")
|
|
parser.add_argument("--notes", default="", help="Approval notes")
|
|
parser.add_argument("--check-expirations", action="store_true")
|
|
parser.add_argument("--report", action="store_true")
|
|
parser.add_argument("--output", default="exception_report.json")
|
|
parser.add_argument("--slack-webhook", help="Slack webhook for notifications")
|
|
args = parser.parse_args()
|
|
|
|
conn = init_db(args.db)
|
|
|
|
if args.create:
|
|
with open(args.create, "r") as f:
|
|
data = json.load(f)
|
|
create_exception(conn, data)
|
|
elif args.approve and args.approver:
|
|
approve_exception(conn, args.approve, args.approver, args.notes)
|
|
elif args.reject and args.approver and args.reason:
|
|
reject_exception(conn, args.reject, args.approver, args.reason)
|
|
elif args.check_expirations:
|
|
check_expirations(conn, args.slack_webhook)
|
|
elif args.report:
|
|
generate_report(conn, args.output)
|
|
else:
|
|
parser.print_help()
|
|
|
|
conn.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|