Skip to content

Commit 1219e97

Browse files
authored
Add AIMS SoA Tracker script
This script tracks the implementation status of Annex A controls for ISO/IEC 42001:2023, providing a progress report and options for exporting data.
1 parent 6db82c3 commit 1219e97

File tree

1 file changed

+319
-0
lines changed

1 file changed

+319
-0
lines changed

12-SCRIPTS/aims_soa_tracker.py

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#!/usr/bin/env python3
2+
"""
3+
AIMS Statement of Applicability (SoA) Tracker
4+
ISO/IEC 42001:2023 | Clause 6.1.3 GRC Automation Script
5+
6+
Purpose:
7+
Tracks the implementation status of all 38 Annex A controls across 9 domains.
8+
Generates a progress report showing overall AIMS implementation readiness.
9+
10+
Usage:
11+
python aims_soa_tracker.py
12+
python aims_soa_tracker.py --export-csv soa_report.csv
13+
python aims_soa_tracker.py --domain "AI System Lifecycle"
14+
15+
Author: ISO 42001 Lead Auditor | GRC Lead
16+
Toolkit: github.com/Ankit-Uniyal/iso-42001-ai-governance-toolkit
17+
License: MIT
18+
"""
19+
20+
import argparse
21+
import csv
22+
import json
23+
import sys
24+
from datetime import datetime
25+
from typing import Optional
26+
27+
28+
# ─────────────────────────────────────────────────────────────────────────────
29+
# ALL 38 ANNEX A CONTROLS — ISO/IEC 42001:2023
30+
# ─────────────────────────────────────────────────────────────────────────────
31+
32+
ANNEX_A_CONTROLS = [
33+
# Domain 1: Policies for AI (A.2)
34+
{"id": "A.2.2", "domain": "Policies for AI", "control": "AI policy", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
35+
{"id": "A.2.3", "domain": "Policies for AI", "control": "Allocation of roles and responsibilities", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
36+
{"id": "A.2.4", "domain": "Policies for AI", "control": "Reporting obligations", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
37+
{"id": "A.2.5", "domain": "Policies for AI", "control": "Addressing AI considerations in contracts", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
38+
{"id": "A.2.6", "domain": "Policies for AI", "control": "Records related to AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
39+
40+
# Domain 2: Human Oversight (A.3)
41+
{"id": "A.3.2", "domain": "Human Oversight of AI Systems", "control": "Establishment of human oversight mechanisms", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
42+
43+
# Domain 3: Responsibilities (A.4)
44+
{"id": "A.4.2", "domain": "Responsibilities Related to AI Systems", "control": "Intended use", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
45+
{"id": "A.4.3", "domain": "Responsibilities Related to AI Systems", "control": "Accuracy, reliability and performance of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
46+
{"id": "A.4.4", "domain": "Responsibilities Related to AI Systems", "control": "Safety of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
47+
{"id": "A.4.5", "domain": "Responsibilities Related to AI Systems", "control": "Security of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
48+
{"id": "A.4.6", "domain": "Responsibilities Related to AI Systems", "control": "Availability of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
49+
{"id": "A.4.7", "domain": "Responsibilities Related to AI Systems", "control": "Eliminating bias and promoting fairness", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
50+
{"id": "A.4.8", "domain": "Responsibilities Related to AI Systems", "control": "Transparency", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
51+
{"id": "A.4.9", "domain": "Responsibilities Related to AI Systems", "control": "Privacy", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
52+
{"id": "A.4.10", "domain": "Responsibilities Related to AI Systems", "control": "Accountability", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
53+
54+
# Domain 4: Impact Assessment (A.5)
55+
{"id": "A.5.2", "domain": "Impact Assessment for AI Systems", "control": "AI system impact assessment process", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
56+
{"id": "A.5.3", "domain": "Impact Assessment for AI Systems", "control": "Documentation of AI system impact assessments", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
57+
58+
# Domain 5: AI System Lifecycle (A.6)
59+
{"id": "A.6.1.2", "domain": "AI System Lifecycle", "control": "General lifecycle management", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
60+
{"id": "A.6.2.2", "domain": "AI System Lifecycle", "control": "Data for AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
61+
{"id": "A.6.2.3", "domain": "AI System Lifecycle", "control": "Acquisition of AI systems and components", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
62+
{"id": "A.6.2.4", "domain": "AI System Lifecycle", "control": "Design and development of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
63+
{"id": "A.6.2.5", "domain": "AI System Lifecycle", "control": "Testing of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
64+
{"id": "A.6.2.6", "domain": "AI System Lifecycle", "control": "AI system documentation", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
65+
{"id": "A.6.2.7", "domain": "AI System Lifecycle", "control": "Deployment of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
66+
{"id": "A.6.2.8", "domain": "AI System Lifecycle", "control": "Operation of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
67+
{"id": "A.6.2.9", "domain": "AI System Lifecycle", "control": "Human oversight of AI systems during operation", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
68+
{"id": "A.6.2.10", "domain": "AI System Lifecycle", "control": "Monitoring AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
69+
{"id": "A.6.2.11", "domain": "AI System Lifecycle", "control": "Change management of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
70+
{"id": "A.6.2.12", "domain": "AI System Lifecycle", "control": "Decommissioning of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
71+
{"id": "A.6.2.13", "domain": "AI System Lifecycle", "control": "Incident management for AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
72+
73+
# Domain 6: Responsible and Trustworthy AI (A.7)
74+
{"id": "A.7.2", "domain": "Responsible and Trustworthy AI", "control": "Responsible and ethical use", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
75+
76+
# Domain 7: AI System Suppliers (A.8)
77+
{"id": "A.8.2", "domain": "AI System Suppliers", "control": "Supplier relationships for AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
78+
79+
# Domain 8: Documentation and Information (A.9)
80+
{"id": "A.9.2", "domain": "Documentation and Information Related to AI Systems", "control": "Documentation of AI systems", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
81+
82+
# Domain 9: AI Standards and Sector-Specific Issues (A.10)
83+
{"id": "A.10.2", "domain": "AI Standards and Sector-Specific Issues", "control": "Compliance with applicable standards", "mandatory": True, "status": "not_started", "evidence": "", "owner": ""},
84+
]
85+
86+
STATUS_OPTIONS = ["implemented", "partial", "planned", "not_started", "excluded"]
87+
STATUS_LABELS = {
88+
"implemented": "IMPLEMENTED",
89+
"partial": "PARTIAL",
90+
"planned": "PLANNED",
91+
"not_started": "NOT STARTED",
92+
"excluded": "EXCLUDED",
93+
}
94+
STATUS_WEIGHTS = {
95+
"implemented": 1.0,
96+
"partial": 0.5,
97+
"planned": 0.1,
98+
"not_started": 0.0,
99+
"excluded": 0.0,
100+
}
101+
102+
103+
def load_soa_state(filepath: str) -> list:
104+
"""Load SoA state from a JSON file (previously saved progress)."""
105+
try:
106+
with open(filepath, "r") as f:
107+
saved = json.load(f)
108+
control_map = {c["id"]: c for c in saved}
109+
controls = []
110+
for control in ANNEX_A_CONTROLS:
111+
if control["id"] in control_map:
112+
controls.append({**control, **control_map[control["id"]]})
113+
else:
114+
controls.append(control.copy())
115+
print(f"[INFO] Loaded SoA state from {filepath}")
116+
return controls
117+
except FileNotFoundError:
118+
print(f"[INFO] No saved state found at {filepath}. Starting fresh.")
119+
return [c.copy() for c in ANNEX_A_CONTROLS]
120+
121+
122+
def save_soa_state(controls: list, filepath: str) -> None:
123+
"""Save current SoA state to a JSON file."""
124+
with open(filepath, "w") as f:
125+
json.dump(controls, f, indent=2)
126+
print(f"[INFO] SoA state saved to {filepath}")
127+
128+
129+
def calculate_completion(controls: list) -> dict:
130+
"""Calculate completion statistics."""
131+
total = len(controls)
132+
by_status = {}
133+
for status in STATUS_OPTIONS:
134+
count = sum(1 for c in controls if c["status"] == status)
135+
by_status[status] = count
136+
137+
weighted_score = sum(STATUS_WEIGHTS.get(c["status"], 0) for c in controls)
138+
completion_pct = (weighted_score / total) * 100 if total > 0 else 0
139+
140+
by_domain = {}
141+
for control in controls:
142+
domain = control["domain"]
143+
if domain not in by_domain:
144+
by_domain[domain] = {"total": 0, "implemented": 0, "partial": 0, "planned": 0, "not_started": 0, "excluded": 0}
145+
by_domain[domain]["total"] += 1
146+
by_domain[domain][control["status"]] += 1
147+
148+
return {
149+
"total_controls": total,
150+
"by_status": by_status,
151+
"completion_pct": round(completion_pct, 1),
152+
"by_domain": by_domain,
153+
"timestamp": datetime.now().isoformat(),
154+
}
155+
156+
157+
def print_report(controls: list, domain_filter: Optional[str] = None) -> None:
158+
"""Print a formatted SoA progress report to the console."""
159+
stats = calculate_completion(controls)
160+
161+
print("\n" + "=" * 80)
162+
print(" ISO/IEC 42001:2023 — STATEMENT OF APPLICABILITY TRACKER")
163+
print(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
164+
print("=" * 80)
165+
166+
# Overall progress
167+
print(f"\n OVERALL IMPLEMENTATION PROGRESS: {stats['completion_pct']}%")
168+
bar_len = 50
169+
filled = int(bar_len * stats["completion_pct"] / 100)
170+
bar = "█" * filled + "░" * (bar_len - filled)
171+
print(f" [{bar}]")
172+
173+
print(f"\n Controls: {stats['total_controls']} total")
174+
for status, count in stats["by_status"].items():
175+
pct = round(count / stats["total_controls"] * 100, 1)
176+
label = STATUS_LABELS[status]
177+
print(f" {label:<15} {count:>3} ({pct}%)")
178+
179+
# Domain breakdown
180+
print("\n" + "-" * 80)
181+
print(" PROGRESS BY DOMAIN")
182+
print("-" * 80)
183+
for domain, counts in stats["by_domain"].items():
184+
if domain_filter and domain_filter.lower() not in domain.lower():
185+
continue
186+
impl = counts["implemented"]
187+
partial = counts["partial"]
188+
total_d = counts["total"]
189+
domain_score = (impl + partial * 0.5) / total_d * 100 if total_d > 0 else 0
190+
bar_d = "█" * int(domain_score / 5) + "░" * (20 - int(domain_score / 5))
191+
print(f"\n {domain}")
192+
print(f" [{bar_d}] {round(domain_score, 0):.0f}% ({impl} impl / {partial} partial / {total_d} total)")
193+
194+
# Control detail
195+
print("\n" + "-" * 80)
196+
print(" CONTROL DETAIL")
197+
print("-" * 80)
198+
current_domain = None
199+
for control in controls:
200+
if domain_filter and domain_filter.lower() not in control["domain"].lower():
201+
continue
202+
if control["domain"] != current_domain:
203+
current_domain = control["domain"]
204+
print(f"\n Domain: {current_domain}")
205+
status_label = STATUS_LABELS.get(control["status"], "UNKNOWN")
206+
owner = f" | Owner: {control['owner']}" if control["owner"] else ""
207+
evidence = f" | Evidence: {control['evidence']}" if control["evidence"] else ""
208+
print(f" {control['id']:<12} [{status_label:<15}] {control['control']}{owner}{evidence}")
209+
210+
print("\n" + "=" * 80)
211+
212+
# Audit readiness assessment
213+
pct = stats["completion_pct"]
214+
if pct >= 95:
215+
readiness = "AUDIT READY — Excellent implementation coverage"
216+
elif pct >= 80:
217+
readiness = "NEAR READY — Complete remaining partial/planned controls before audit"
218+
elif pct >= 60:
219+
readiness = "IN PROGRESS — Significant work remaining before audit readiness"
220+
elif pct >= 40:
221+
readiness = "EARLY STAGE — Substantial implementation work required"
222+
else:
223+
readiness = "INITIAL STAGE — Full implementation programme needed"
224+
225+
print(f"\n AUDIT READINESS: {readiness}")
226+
print("=" * 80 + "\n")
227+
228+
229+
def export_csv(controls: list, filepath: str) -> None:
230+
"""Export SoA controls to CSV for use in Excel or GRC tools."""
231+
fieldnames = ["id", "domain", "control", "mandatory", "status", "evidence", "owner"]
232+
with open(filepath, "w", newline="", encoding="utf-8") as f:
233+
writer = csv.DictWriter(f, fieldnames=fieldnames)
234+
writer.writeheader()
235+
writer.writerows(controls)
236+
print(f"[INFO] SoA exported to CSV: {filepath}")
237+
238+
239+
def interactive_update(controls: list) -> list:
240+
"""Interactive CLI to update control status."""
241+
print("\nINTERACTIVE SOA UPDATE MODE")
242+
print("Enter control ID to update (e.g., A.4.7), or 'q' to quit, 'list' to show all IDs\n")
243+
244+
control_map = {c["id"]: i for i, c in enumerate(controls)}
245+
246+
while True:
247+
user_input = input("Control ID: ").strip()
248+
if user_input.lower() == "q":
249+
break
250+
if user_input.lower() == "list":
251+
for c in controls:
252+
print(f" {c['id']:<12} [{STATUS_LABELS.get(c['status'], 'UNKNOWN'):<15}] {c['control']}")
253+
continue
254+
if user_input not in control_map:
255+
print(f" [ERROR] Control ID '{user_input}' not found. Try 'list' to see all IDs.")
256+
continue
257+
258+
idx = control_map[user_input]
259+
control = controls[idx]
260+
print(f"\n Control: {control['control']}")
261+
print(f" Current status: {STATUS_LABELS.get(control['status'], 'UNKNOWN')}")
262+
print(f" Status options: {', '.join(STATUS_OPTIONS)}")
263+
264+
new_status = input(" New status: ").strip().lower()
265+
if new_status not in STATUS_OPTIONS:
266+
print(f" [ERROR] Invalid status. Choose from: {', '.join(STATUS_OPTIONS)}")
267+
continue
268+
269+
new_evidence = input(f" Evidence reference (current: '{control['evidence']}'): ").strip()
270+
new_owner = input(f" Owner (current: '{control['owner']}'): ").strip()
271+
272+
controls[idx]["status"] = new_status
273+
if new_evidence:
274+
controls[idx]["evidence"] = new_evidence
275+
if new_owner:
276+
controls[idx]["owner"] = new_owner
277+
print(f" [OK] Updated {user_input} to {STATUS_LABELS[new_status]}\n")
278+
279+
return controls
280+
281+
282+
def main():
283+
parser = argparse.ArgumentParser(
284+
description="ISO/IEC 42001:2023 Annex A SoA Tracker",
285+
formatter_class=argparse.RawDescriptionHelpFormatter,
286+
epilog="""
287+
Examples:
288+
python aims_soa_tracker.py
289+
python aims_soa_tracker.py --export-csv soa_report.csv
290+
python aims_soa_tracker.py --domain "AI System Lifecycle"
291+
python aims_soa_tracker.py --update --state soa_state.json
292+
python aims_soa_tracker.py --state soa_state.json --export-csv soa_report.csv
293+
""",
294+
)
295+
parser.add_argument("--state", default="soa_state.json", help="JSON file to load/save SoA state (default: soa_state.json)")
296+
parser.add_argument("--export-csv", metavar="FILE", help="Export SoA to CSV file")
297+
parser.add_argument("--domain", metavar="NAME", help="Filter report to a specific domain")
298+
parser.add_argument("--update", action="store_true", help="Interactively update control statuses")
299+
parser.add_argument("--version", action="version", version="AIMS SoA Tracker 1.0.0")
300+
args = parser.parse_args()
301+
302+
# Load or initialise controls
303+
controls = load_soa_state(args.state)
304+
305+
# Interactive update mode
306+
if args.update:
307+
controls = interactive_update(controls)
308+
save_soa_state(controls, args.state)
309+
310+
# Print report
311+
print_report(controls, domain_filter=args.domain)
312+
313+
# Export CSV
314+
if args.export_csv:
315+
export_csv(controls, args.export_csv)
316+
317+
318+
if __name__ == "__main__":
319+
main()

0 commit comments

Comments
 (0)