mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-08-31 13:40:50 +03:00
Adds comprehensive GDPR compliance assessment skill covering Article 30 records, lawful basis validation, data subject rights, DPIAs, breach notification, international transfers, and technical/organizational measures. Features: - 295-line skill body (under 500-line cap) - 1008-char description with negative triggers (under 1024 limit) - 9 files total, all within skill directory - 3 production scripts: article30_parser.py, article30_validator.py, generate_ropa_report.py - Detailed workflow, templates, and references Negative triggers direct users to: - implementing-gdpr-data-protection-controls for Article 32 technical controls - implementing-gdpr-data-subject-access-request for DSAR automation Legal basis: EU Regulation 2016/679, UK GDPR as amended by Data Protection Act 2018 and Data (Use and Access) Act 2025. Effective date: August 2026. Validation: validate-skill.py PASS, lint-descriptions.py PASS
198 lines
7.4 KiB
Python
198 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
GDPR Article 30 RoPA Report Generator.
|
|
|
|
Generates a formatted Article 30 Records of Processing Activities report
|
|
in Markdown format suitable for supervisory authority submission.
|
|
|
|
Usage:
|
|
python generate_ropa_report.py --input ropa.json --output Article30_Register.md
|
|
python generate_ropa_report.py --input ropa.json --output report.pdf --format pdf
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
def format_list(items):
|
|
"""Format a list of items as bullet points."""
|
|
if not items:
|
|
return "*[None specified]*"
|
|
if isinstance(items, str):
|
|
return items
|
|
return "\n".join(f"- {item}" for item in items)
|
|
|
|
def generate_markdown(ropa):
|
|
"""Generate Markdown report from RoPA JSON."""
|
|
org = ropa.get("organization", {})
|
|
activities = ropa.get("processing_activities", [])
|
|
|
|
lines = []
|
|
lines.append("# Article 30 Records of Processing Activities")
|
|
lines.append("")
|
|
lines.append(f"**Organization**: {org.get('name', '[Organization Name]')}")
|
|
lines.append(f"**Controller Contact**: {org.get('controller_contact', '[Contact Details]')}")
|
|
if org.get('dpo_contact'):
|
|
lines.append(f"**Data Protection Officer**: {org.get('dpo_contact')}")
|
|
lines.append(f"**Generated**: {org.get('generated_date', datetime.now(timezone.utc).isoformat())}")
|
|
lines.append(f"**Total Processing Activities**: {len(activities)}")
|
|
lines.append("")
|
|
lines.append("---")
|
|
lines.append("")
|
|
|
|
for idx, activity in enumerate(activities, 1):
|
|
lines.append(f"## Processing Activity {idx}: {activity.get('source_file', 'Unnamed Activity')}")
|
|
lines.append("")
|
|
|
|
# Purposes
|
|
lines.append("### Purposes of Processing")
|
|
lines.append(format_list(activity.get('purposes', [])))
|
|
lines.append("")
|
|
|
|
# Data Subjects
|
|
lines.append("### Categories of Data Subjects")
|
|
lines.append(format_list(activity.get('data_subjects', [])))
|
|
lines.append("")
|
|
|
|
# Personal Data
|
|
lines.append("### Categories of Personal Data")
|
|
lines.append(format_list(activity.get('personal_data_categories', [])))
|
|
lines.append("")
|
|
|
|
# Recipients
|
|
lines.append("### Categories of Recipients")
|
|
lines.append(format_list(activity.get('recipients', [])))
|
|
lines.append("")
|
|
|
|
# Retention
|
|
lines.append("### Retention Period")
|
|
retention = activity.get('retention_period', '*[Not specified]*')
|
|
lines.append(f"{retention}")
|
|
lines.append("")
|
|
|
|
# International Transfers
|
|
lines.append("### International Transfers")
|
|
transfers = activity.get('international_transfers', [])
|
|
if transfers:
|
|
lines.append(format_list(transfers))
|
|
else:
|
|
lines.append("*No international transfers*")
|
|
lines.append("")
|
|
|
|
# Security Measures
|
|
lines.append("### Technical and Organizational Security Measures")
|
|
lines.append(format_list(activity.get('security_measures', [])))
|
|
lines.append("")
|
|
|
|
lines.append("---")
|
|
lines.append("")
|
|
|
|
# Completion Notes
|
|
if ropa.get('completion_notes'):
|
|
lines.append("## Completion Notes")
|
|
lines.append("")
|
|
for note in ropa['completion_notes']:
|
|
lines.append(f"- {note}")
|
|
lines.append("")
|
|
|
|
# Footer
|
|
lines.append("---")
|
|
lines.append("*This document fulfills the requirements of GDPR Article 30 (Records of Processing Activities).*")
|
|
lines.append("*Controllers must maintain this register and make it available to supervisory authorities upon request.*")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def generate_html(markdown_content):
|
|
"""Convert Markdown to basic HTML."""
|
|
html_header = """<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Article 30 RoPA</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
|
|
h1 { color: #1a5490; border-bottom: 3px solid #1a5490; }
|
|
h2 { color: #2563eb; margin-top: 30px; }
|
|
h3 { color: #4b5563; margin-top: 20px; }
|
|
hr { margin: 30px 0; border: none; border-top: 1px solid #ddd; }
|
|
ul { line-height: 1.6; }
|
|
em { color: #6b7280; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
"""
|
|
html_footer = """
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Simple Markdown to HTML conversion
|
|
html_body = markdown_content
|
|
html_body = html_body.replace("# ", "<h1>").replace("\n\n", "</h1>\n\n")
|
|
html_body = html_body.replace("## ", "<h2>").replace("\n\n", "</h2>\n\n")
|
|
html_body = html_body.replace("### ", "<h3>").replace("\n\n", "</h3>\n\n")
|
|
html_body = html_body.replace("**", "<strong>").replace("**", "</strong>")
|
|
html_body = html_body.replace("*[", "<em>[").replace("]*", "]</em>")
|
|
html_body = html_body.replace("---\n", "<hr>\n")
|
|
html_body = html_body.replace("- ", "<li>").replace("\n", "</li>\n")
|
|
|
|
return html_header + html_body + html_footer
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Generate Article 30 RoPA Report")
|
|
parser.add_argument("--input", "-i", required=True, help="Input RoPA JSON file")
|
|
parser.add_argument("--output", "-o", required=True, help="Output file path")
|
|
parser.add_argument("--format", choices=["markdown", "html", "pdf"], default="markdown",
|
|
help="Output format (default: markdown)")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
with open(args.input, 'r') as f:
|
|
ropa = json.load(f)
|
|
except (OSError, json.JSONDecodeError) as e:
|
|
print(f"ERROR: Could not read RoPA file: {e}", file=sys.stderr)
|
|
return 2
|
|
|
|
markdown_content = generate_markdown(ropa)
|
|
|
|
if args.format == "markdown":
|
|
with open(args.output, 'w') as f:
|
|
f.write(markdown_content)
|
|
print(f"[✓] Markdown report written to: {args.output}", file=sys.stderr)
|
|
|
|
elif args.format == "html":
|
|
html_content = generate_html(markdown_content)
|
|
with open(args.output, 'w') as f:
|
|
f.write(html_content)
|
|
print(f"[✓] HTML report written to: {args.output}", file=sys.stderr)
|
|
|
|
elif args.format == "pdf":
|
|
try:
|
|
# Try to use markdown2pdf if available
|
|
import subprocess
|
|
md_temp = args.output.replace('.pdf', '.md')
|
|
with open(md_temp, 'w') as f:
|
|
f.write(markdown_content)
|
|
|
|
# Try pandoc first, fall back to instructions
|
|
result = subprocess.run(['pandoc', md_temp, '-o', args.output],
|
|
capture_output=True)
|
|
if result.returncode == 0:
|
|
print(f"[✓] PDF report written to: {args.output}", file=sys.stderr)
|
|
import os
|
|
os.remove(md_temp)
|
|
else:
|
|
print(f"[!] pandoc not found. Markdown saved to: {md_temp}", file=sys.stderr)
|
|
print(f"[!] Install pandoc to generate PDF: apt install pandoc", file=sys.stderr)
|
|
except Exception as e:
|
|
print(f"[!] PDF generation requires pandoc: {e}", file=sys.stderr)
|
|
print(f"[!] Generating Markdown instead: {args.output.replace('.pdf', '.md')}", file=sys.stderr)
|
|
with open(args.output.replace('.pdf', '.md'), 'w') as f:
|
|
f.write(markdown_content)
|
|
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|