mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-26 19:54:37 +03:00
e8832748d3
- conducting-cyber-risk-assessment-with-nist-800-30 - executing-nist-rmf-authorization-to-operate - achieving-cmmc-level-2-compliance - implementing-hipaa-security-rule-safeguards - managing-third-party-vendor-risk Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
224 lines
7.7 KiB
Python
224 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
HIPAA Security Rule safeguard gap-assessment scorer.
|
|
|
|
Scores a safeguard-status inventory across the Administrative (164.308),
|
|
Physical (164.310), and Technical (164.312) safeguards. Required gaps are
|
|
weighted above addressable gaps, and any gap in the Risk Analysis or Risk
|
|
Management specifications is escalated (these are the most-cited OCR findings).
|
|
Emits a gap table, a weighted readiness score, and a prioritized remediation list.
|
|
|
|
Input JSON shape:
|
|
{
|
|
"org": {"name": "Northstar Clinic", "role": "Covered Entity"},
|
|
"safeguards": [
|
|
{
|
|
"id": "164.308(a)(1)(ii)(A)", "section": "308",
|
|
"name": "Risk Analysis", "requirement": "required",
|
|
"status": "gap"
|
|
},
|
|
{
|
|
"id": "164.312(a)(2)(iv)", "section": "312",
|
|
"name": "Encryption and Decryption", "requirement": "addressable",
|
|
"status": "partial",
|
|
"alternative_documented": false
|
|
}
|
|
]
|
|
}
|
|
|
|
requirement: required | addressable
|
|
status: implemented | partial | gap
|
|
|
|
Usage:
|
|
python process.py --input safeguards.json [--output gap.md]
|
|
python process.py --input safeguards.json --fail-on-required-gap
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
|
|
SECTION_NAMES = {
|
|
"308": "Administrative (§164.308)",
|
|
"310": "Physical (§164.310)",
|
|
"312": "Technical (§164.312)",
|
|
}
|
|
VALID_REQ = {"required", "addressable"}
|
|
VALID_STATUS = {"implemented", "partial", "gap"}
|
|
|
|
# status -> credit fraction toward "implemented"
|
|
CREDIT = {"implemented": 1.0, "partial": 0.5, "gap": 0.0}
|
|
# weighting: required safeguards count more toward the score
|
|
WEIGHT = {"required": 2, "addressable": 1}
|
|
|
|
# Specifications whose absence OCR cites most often -> escalate
|
|
HIGH_PRIORITY_KEYS = ("risk analysis", "risk management")
|
|
|
|
|
|
def score(data):
|
|
sgs = data.get("safeguards", [])
|
|
if not sgs:
|
|
raise ValueError("safeguards list is required")
|
|
|
|
total_weight = 0.0
|
|
earned_weight = 0.0
|
|
required_gaps = []
|
|
addressable_gaps = []
|
|
escalated = []
|
|
by_section = {}
|
|
rows = []
|
|
|
|
for s in sgs:
|
|
sid = s.get("id", "?")
|
|
req = s.get("requirement")
|
|
status = s.get("status")
|
|
if req not in VALID_REQ:
|
|
raise ValueError(f"{sid}: requirement '{req}' invalid (required|addressable)")
|
|
if status not in VALID_STATUS:
|
|
raise ValueError(f"{sid}: status '{status}' invalid (implemented|partial|gap)")
|
|
|
|
section = str(s.get("section", "")).replace("164.", "").strip()
|
|
sec_rec = by_section.setdefault(section, {"implemented": 0, "partial": 0, "gap": 0})
|
|
sec_rec[status] += 1
|
|
|
|
w = WEIGHT[req]
|
|
total_weight += w
|
|
earned_weight += w * CREDIT[status]
|
|
|
|
name = s.get("name", "")
|
|
is_high = any(k in name.lower() for k in HIGH_PRIORITY_KEYS)
|
|
|
|
# an addressable item with a documented equivalent alternative is acceptable
|
|
addressable_ok = (
|
|
req == "addressable"
|
|
and status != "implemented"
|
|
and s.get("alternative_documented") is True
|
|
)
|
|
|
|
if status in ("gap", "partial") and not addressable_ok:
|
|
if req == "required":
|
|
required_gaps.append(s)
|
|
else:
|
|
addressable_gaps.append(s)
|
|
if is_high:
|
|
escalated.append(s)
|
|
|
|
rows.append((sid, section, name, req, status, s.get("alternative_documented", None), is_high))
|
|
|
|
readiness = (100.0 * earned_weight / total_weight) if total_weight else 0.0
|
|
return {
|
|
"readiness": readiness,
|
|
"required_gaps": required_gaps,
|
|
"addressable_gaps": addressable_gaps,
|
|
"escalated": escalated,
|
|
"by_section": by_section,
|
|
"rows": rows,
|
|
}
|
|
|
|
|
|
def render(data, res):
|
|
org = data.get("org", {})
|
|
lines = []
|
|
lines.append(f"# HIPAA Security Rule Gap Assessment - {org.get('name','Organization')}")
|
|
lines.append("")
|
|
if org.get("role"):
|
|
lines.append(f"- **Role:** {org['role']}")
|
|
lines.append(f"- **Weighted readiness:** **{res['readiness']:.0f}%** "
|
|
"(required specifications weighted 2x addressable)")
|
|
lines.append(f"- **Required gaps:** {len(res['required_gaps'])} | "
|
|
f"Addressable gaps (no documented alternative): {len(res['addressable_gaps'])}")
|
|
lines.append("")
|
|
|
|
if res["escalated"]:
|
|
lines.append("> **OCR-priority gap detected:** Risk Analysis / Risk Management is "
|
|
"incomplete. This is the most-cited HIPAA finding - remediate first.")
|
|
lines.append("")
|
|
|
|
# status by section
|
|
lines.append("## Status by safeguard section")
|
|
lines.append("")
|
|
lines.append("| Section | Implemented | Partial | Gap |")
|
|
lines.append("|---|---|---|---|")
|
|
for sec in sorted(res["by_section"]):
|
|
r = res["by_section"][sec]
|
|
label = SECTION_NAMES.get(sec, sec)
|
|
lines.append(f"| {label} | {r['implemented']} | {r['partial']} | {r['gap']} |")
|
|
lines.append("")
|
|
|
|
# full table
|
|
lines.append("## Safeguard detail")
|
|
lines.append("")
|
|
lines.append("| Specification | Section | Requirement | Status | Alt. documented |")
|
|
lines.append("|---|---|---|---|---|")
|
|
for sid, sec, name, req, status, alt, _ in res["rows"]:
|
|
altdisp = "-" if alt is None else ("yes" if alt else "no")
|
|
secdisp = SECTION_NAMES.get(sec, sec)
|
|
lines.append(f"| {sid} {name} | {secdisp} | {req} | {status} | {altdisp} |")
|
|
lines.append("")
|
|
|
|
# remediation priority
|
|
lines.append("## Remediation priority")
|
|
lines.append("")
|
|
order = []
|
|
order += [(s, "ESCALATED (Risk Analysis/Mgmt)") for s in res["escalated"]]
|
|
order += [(s, "Required gap") for s in res["required_gaps"] if s not in res["escalated"]]
|
|
order += [(s, "Addressable - implement or document alternative") for s in res["addressable_gaps"]]
|
|
if not order:
|
|
lines.append("No outstanding gaps. Maintain documentation and re-evaluate on change.")
|
|
else:
|
|
seen = set()
|
|
i = 1
|
|
for s, why in order:
|
|
key = s.get("id")
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
lines.append(f"{i}. **{s.get('id')}** {s.get('name','')} - {why} "
|
|
f"(currently {s.get('status')}).")
|
|
i += 1
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="HIPAA Security Rule safeguard gap-assessment scorer")
|
|
ap.add_argument("--input", "-i", required=True, help="Path to safeguard-status JSON")
|
|
ap.add_argument("--output", "-o", help="Write Markdown gap assessment to this path")
|
|
ap.add_argument("--fail-on-required-gap", action="store_true",
|
|
help="Exit non-zero if any required specification is partial/gap")
|
|
args = ap.parse_args()
|
|
|
|
try:
|
|
with open(args.input) as f:
|
|
data = json.load(f)
|
|
except (OSError, json.JSONDecodeError) as e:
|
|
print(f"ERROR: could not read input JSON: {e}", file=sys.stderr)
|
|
return 2
|
|
|
|
try:
|
|
res = score(data)
|
|
md = render(data, res)
|
|
except ValueError as e:
|
|
print(f"ERROR: {e}", file=sys.stderr)
|
|
return 2
|
|
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
f.write(md + "\n")
|
|
print(f"Gap assessment written to {args.output}", file=sys.stderr)
|
|
else:
|
|
print(md)
|
|
|
|
print(f"Readiness {res['readiness']:.0f}%; required gaps {len(res['required_gaps'])}; "
|
|
f"escalated {len(res['escalated'])}.", file=sys.stderr)
|
|
|
|
if args.fail_on_required_gap and res["required_gaps"]:
|
|
ids = ", ".join(s.get("id", "?") for s in res["required_gaps"])
|
|
print(f"FAIL: {len(res['required_gaps'])} required specification gap(s): {ids}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|