Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build a vulnerability exception and risk acceptance tracking system with approval workflows, compensating controls documentation, and expiration management.
.claude/skills/building-vulnerability-exception-tracking-system/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-10 | ✗→✓ | ▲ Improved | — | — |
| case-14 | ✗→✓ | ▲ Improved | — | — |
A vulnerability exception tracking system manages cases where vulnerabilities cannot be remediated within SLA timelines. It provides structured workflows for requesting exceptions, documenting compensating controls, obtaining risk acceptance approvals, and automatically expiring exceptions when their validity period ends. This ensures organizations maintain visibility into accepted risks while complying with frameworks like PCI DSS, SOC 2, and NIST CSF.
flask, sqlalchemy, requests, jinja2| Category | Description | Max Duration | Approver Level | |----------|------------|-------------|----------------| | Remediation Delay | Patch available but deployment blocked | 30 days | Team Lead + Security | | No Fix Available | Vendor has not released a patch | 90 days | Security Director | | Business Critical | System cannot be patched without outage | 60 days | VP Engineering + CISO | | False Positive | Finding is not a real vulnerability | Permanent | Security Analyst | | Compensating Control | Alternative mitigation in place | 180 days | Security Architect |
pythonexception_schema = { "cve_id": "CVE-2024-XXXX", "finding_id": "unique-finding-reference", "asset_hostname": "prod-db-01.corp.local", "severity": "high", "cvss_score": 8.1, "category": "remediation_delay", "justification": "Database upgrade required before patch can be applied", "compensating_controls": [ "WAF rule blocking exploit pattern deployed", "Network segmentation restricting access to trusted VLANs only", "Enhanced monitoring via Splunk alert for exploitation indicators" ], "requested_expiration": "2024-06-15", "requestor_email": "dbadmin@company.com", "approver_emails": ["security-lead@company.com", "ciso@company.com"], "risk_rating": "medium", }
sqlCREATE TABLE vulnerability_exceptions ( id SERIAL PRIMARY KEY, cve_id VARCHAR(20) NOT NULL, finding_id VARCHAR(100) NOT NULL, asset_hostname VARCHAR(255), severity VARCHAR(20), cvss_score DECIMAL(3,1), category VARCHAR(50) NOT NULL, justification TEXT NOT NULL, compensating_controls TEXT, status VARCHAR(20) DEFAULT 'pending', requested_by VARCHAR(255) NOT NULL, approved_by VARCHAR(255), requested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, approved_at TIMESTAMP, expires_at TIMESTAMP NOT NULL, expired BOOLEAN DEFAULT FALSE, risk_rating VARCHAR(20), review_notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE exception_audit_log ( id SERIAL PRIMARY KEY, exception_id INTEGER REFERENCES vulnerability_exceptions(id), action VARCHAR(50) NOT NULL, actor VARCHAR(255) NOT NULL, details TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_exception_status ON vulnerability_exceptions(status); CREATE INDEX idx_exception_expires ON vulnerability_exceptions(expires_at); CREATE INDEX idx_exception_cve ON vulnerability_exceptions(cve_id);
pythonfrom flask import Flask, request, jsonify from datetime import datetime, timezone import json app = Flask(__name__) @app.route("/api/exceptions", methods=["POST"]) def create_exception(): data = request.json required = ["cve_id", "finding_id", "category", "justification", "expires_at", "requestor_email"] for field in required: if field not in data: return jsonify({"error": f"Missing required field: {field}"}), 400 # Validate expiration does not exceed category maximum max_days = {"remediation_delay": 30, "no_fix": 90, "business_critical": 60, "false_positive": 365, "compensating_control": 180} # Insert into database and notify approvers return jsonify({"status": "pending", "id": "exc-12345"}) @app.route("/api/exceptions/<exc_id>/approve", methods=["POST"]) def approve_exception(exc_id): approver = request.json.get("approver_email") notes = request.json.get("notes", "") # Update status to approved, record approver and timestamp return jsonify({"status": "approved"}) @app.route("/api/exceptions/<exc_id>/reject", methods=["POST"]) def reject_exception(exc_id): reviewer = request.json.get("reviewer_email") reason = request.json.get("reason") # Update status to rejected, record reviewer and reason return jsonify({"status": "rejected"})
bash# Check for expired exceptions daily python3 scripts/process.py --check-expirations # Generate monthly exception report python3 scripts/process.py --report --output exception_report.json
For each exception, compensating controls must address:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +50 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.