gitmodelmujtaba commited on
Commit
4ec721f
·
verified ·
1 Parent(s): 440a59c

Deploy ReguAI: Neuro-Symbolic AI GRC & Automated Conformity Assessment Engine

Browse files
.github/workflows/reguai-gate.yml ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: ReguAI Deterministic Regulatory Gate
2
+
3
+ on:
4
+ pull_request:
5
+ paths:
6
+ - '**.md'
7
+ - '**.json'
8
+ - '**.yaml'
9
+ - '**.yml'
10
+ - 'data/**'
11
+ - 'src/**'
12
+ push:
13
+ branches:
14
+ - main
15
+
16
+ permissions:
17
+ contents: read
18
+ security-events: write
19
+ pull-requests: write
20
+
21
+ jobs:
22
+ regulatory-audit:
23
+ name: EU AI Act & GRC Normative Verification
24
+ runs-on: ubuntu-latest
25
+
26
+ steps:
27
+ - name: Checkout Codebase
28
+ uses: actions/checkout@v4
29
+
30
+ - name: Set up Python 3.11
31
+ uses: actions/setup-python@v5
32
+ with:
33
+ python-version: "3.11"
34
+ cache: "pip"
35
+
36
+ - name: Install Dependencies
37
+ run: |
38
+ python -m pip install --upgrade pip
39
+ pip install -r requirements.txt
40
+
41
+ - name: Execute ReguAI Audit Gate
42
+ id: reguai_audit
43
+ continue-on-error: true
44
+ run: |
45
+ mkdir -p reports
46
+ python -m src.cli audit data/synthetic_systems/ \
47
+ --format text \
48
+ --sarif-out reports/reguai-results.sarif \
49
+ --fail-on-violation
50
+
51
+ - name: Upload SARIF to GitHub Code Scanning
52
+ uses: github/codeql-action/upload-sarif@v3
53
+ if: always()
54
+ with:
55
+ sarif_file: reports/reguai-results.sarif
56
+ category: ReguAI-EU-AI-Act
57
+
58
+ - name: Comment PR Conformity Summary
59
+ if: github.event_name == 'pull_request' && always()
60
+ uses: actions/github-script@v7
61
+ with:
62
+ script: |
63
+ const fs = require('fs');
64
+ let summary = '### 🏛️ ReguAI Regulatory Conformity Audit Complete\n\nDeterministic W3C SHACL verification results have been uploaded to GitHub Code Scanning.';
65
+ github.rest.issues.createComment({
66
+ issue_number: context.issue.number,
67
+ owner: context.repo.owner,
68
+ repo: context.repo.repo,
69
+ body: summary
70
+ });
README.md CHANGED
@@ -159,11 +159,28 @@ markdown_report = engine.report_generator.generate_markdown_report(report)
159
  # Export Machine-Readable JSON-LD Certificate
160
  json_ld_cert = engine.report_generator.generate_json_ld(report)
161
 
 
 
 
162
  # Generate Print-Ready HTML Attestation Certificate
163
  html_cert = engine.report_generator.generate_html_certificate(report)
164
  ```
165
 
166
- ### 5. Running the Test Suite
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  ```bash
169
  pytest tests/ -v
 
159
  # Export Machine-Readable JSON-LD Certificate
160
  json_ld_cert = engine.report_generator.generate_json_ld(report)
161
 
162
+ # Export CycloneDX 1.6 Machine-Readable AI-BOM
163
+ aibom_json = engine.report_generator.generate_cyclonedx_bom(report)
164
+
165
  # Generate Print-Ready HTML Attestation Certificate
166
  html_cert = engine.report_generator.generate_html_certificate(report)
167
  ```
168
 
169
+ ### 5. Automated CI/CD Regulatory Gate (CLI & SARIF)
170
+
171
+ ```bash
172
+ # Audit a model card or directory with exit code gate
173
+ python -m src.cli audit data/synthetic_systems/compliant_clinical_samd.json \
174
+ --format text \
175
+ --sarif-out reports/audit.sarif \
176
+ --bom-out reports/cyclonedx_aibom.json \
177
+ --fail-on-violation
178
+
179
+ # Train active learning triplet metric learner on auditor feedback
180
+ python -m src.triage.train_triplets
181
+ ```
182
+
183
+ ### 6. Running the Test Suite
184
 
185
  ```bash
186
  pytest tests/ -v
api.py CHANGED
@@ -245,6 +245,45 @@ def get_certificate_html(token: str):
245
  return HTMLResponse(content=html)
246
 
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  @app.post("/api/v1/triage/feedback", tags=["Active Learning"])
249
  def submit_auditor_feedback(feedback: TripletFeedbackRequest):
250
  """
 
245
  return HTMLResponse(content=html)
246
 
247
 
248
+ @app.get("/api/v1/certificates/{token}/bom", tags=["Conformity Assessment"])
249
+ def get_certificate_bom(token: str):
250
+ """Retrieves machine-readable CycloneDX 1.6 AI Bill of Materials (AIBOM) for an audit."""
251
+ report = CERTIFICATE_CACHE.get(token)
252
+ if not report:
253
+ samd_path = SYNTHETIC_DIR / "compliant_clinical_samd.json"
254
+ report = engine.evaluate_system(samd_path)
255
+
256
+ return engine.report_generator.generate_cyclonedx_bom(report)
257
+
258
+
259
+ @app.get("/api/v1/certificates/{token}/sarif", tags=["Conformity Assessment"])
260
+ def get_certificate_sarif(token: str):
261
+ """Retrieves OASIS SARIF 2.1.0 security & compliance report for an audit."""
262
+ from src.triage.sarif_exporter import SarifExporter
263
+ report = CERTIFICATE_CACHE.get(token)
264
+ if not report:
265
+ samd_path = SYNTHETIC_DIR / "compliant_clinical_samd.json"
266
+ report = engine.evaluate_system(samd_path)
267
+
268
+ return SarifExporter.generate_sarif(report)
269
+
270
+
271
+ @app.post("/api/v1/audit/bom", tags=["Conformity Assessment"])
272
+ def generate_audit_bom(request: AuditRequest):
273
+ """Generates a CycloneDX 1.6 AI-BOM directly from specification text."""
274
+ if not request.specification_text.strip():
275
+ raise HTTPException(status_code=400, detail="Specification text cannot be empty.")
276
+
277
+ report = engine.evaluate_system(
278
+ request.specification_text,
279
+ auditor_id=request.auditor_id,
280
+ annual_turnover_eur=request.annual_turnover_eur or 0.0,
281
+ is_sme=request.is_sme or False,
282
+ )
283
+ CERTIFICATE_CACHE[report.provenance.digital_signature] = report
284
+ return engine.report_generator.generate_cyclonedx_bom(report)
285
+
286
+
287
  @app.post("/api/v1/triage/feedback", tags=["Active Learning"])
288
  def submit_auditor_feedback(feedback: TripletFeedbackRequest):
289
  """
app.py CHANGED
@@ -19,6 +19,7 @@ from src.core.models import AssertionStatus, EntityCategory
19
  from src.ui.graph_view import RegulatoryGraphView
20
 
21
  from src.core.case_catalog import CaseStudyCatalog
 
22
 
23
  # Initialize ReguAI Engine & Graph Visualizer & Case Study Catalog
24
  engine = ReguAIEngine()
@@ -96,6 +97,8 @@ def run_assessment(doc_text: str, auditor_id: str, annual_turnover: float = 5000
96
  [],
97
  [],
98
  "<div style='padding:15px;'>No fine liability evaluated.</div>",
 
 
99
  )
100
 
101
  report = engine.evaluate_system(
@@ -243,6 +246,8 @@ def run_assessment(doc_text: str, auditor_id: str, annual_turnover: float = 5000
243
  cert_iframe_html = f"""
244
  <iframe srcdoc="{escaped_cert}" style="width: 100%; height: 680px; border: 1px solid #e2e8f0; border-radius: 8px;" frameborder="0"></iframe>
245
  """
 
 
246
 
247
  return (
248
  exec_html,
@@ -257,6 +262,8 @@ def run_assessment(doc_text: str, auditor_id: str, annual_turnover: float = 5000
257
  borderline_data,
258
  frameworks_data,
259
  fine_html,
 
 
260
  )
261
 
262
 
@@ -877,8 +884,12 @@ with gr.Blocks(title="ReguAI: Neuro-Symbolic AI GRC Engine") as demo:
877
  report_markdown = gr.Markdown(value=DEFAULT_ASSESSMENT[7])
878
  with gr.TabItem("Machine-Readable JSON-LD"):
879
  jsonld_display = gr.Code(value=DEFAULT_ASSESSMENT[6], language="json", label="W3C JSON-LD Digital Certificate")
 
 
 
 
880
 
881
- # Outputs list for 1-click preset execution (17 components)
882
  preset_outputs = [
883
  domain_dropdown,
884
  case_dropdown,
@@ -897,6 +908,8 @@ with gr.Blocks(title="ReguAI: Neuro-Symbolic AI GRC Engine") as demo:
897
  borderline_table,
898
  frameworks_table,
899
  fine_liability_output,
 
 
900
  ]
901
 
902
  # Wire 1-Click Preset Scenario Buttons
@@ -955,6 +968,8 @@ with gr.Blocks(title="ReguAI: Neuro-Symbolic AI GRC Engine") as demo:
955
  borderline_table,
956
  frameworks_table,
957
  fine_liability_output,
 
 
958
  ],
959
  )
960
 
 
19
  from src.ui.graph_view import RegulatoryGraphView
20
 
21
  from src.core.case_catalog import CaseStudyCatalog
22
+ from src.triage.sarif_exporter import SarifExporter
23
 
24
  # Initialize ReguAI Engine & Graph Visualizer & Case Study Catalog
25
  engine = ReguAIEngine()
 
97
  [],
98
  [],
99
  "<div style='padding:15px;'>No fine liability evaluated.</div>",
100
+ "{}",
101
+ "{}",
102
  )
103
 
104
  report = engine.evaluate_system(
 
246
  cert_iframe_html = f"""
247
  <iframe srcdoc="{escaped_cert}" style="width: 100%; height: 680px; border: 1px solid #e2e8f0; border-radius: 8px;" frameborder="0"></iframe>
248
  """
249
+ bom_json = json.dumps(engine.report_generator.generate_cyclonedx_bom(report), indent=2)
250
+ sarif_json = SarifExporter.export_sarif_json(report, indent=2)
251
 
252
  return (
253
  exec_html,
 
262
  borderline_data,
263
  frameworks_data,
264
  fine_html,
265
+ bom_json,
266
+ sarif_json,
267
  )
268
 
269
 
 
884
  report_markdown = gr.Markdown(value=DEFAULT_ASSESSMENT[7])
885
  with gr.TabItem("Machine-Readable JSON-LD"):
886
  jsonld_display = gr.Code(value=DEFAULT_ASSESSMENT[6], language="json", label="W3C JSON-LD Digital Certificate")
887
+ with gr.TabItem("📦 CycloneDX 1.6 AI-BOM"):
888
+ bom_display = gr.Code(value=DEFAULT_ASSESSMENT[12], language="json", label="CycloneDX 1.6 Machine-Readable AI-BOM")
889
+ with gr.TabItem("🛡️ OASIS SARIF 2.1.0 Report"):
890
+ sarif_display = gr.Code(value=DEFAULT_ASSESSMENT[13], language="json", label="OASIS SARIF 2.1.0 Static Analysis Report")
891
 
892
+ # Outputs list for 1-click preset execution (19 components)
893
  preset_outputs = [
894
  domain_dropdown,
895
  case_dropdown,
 
908
  borderline_table,
909
  frameworks_table,
910
  fine_liability_output,
911
+ bom_display,
912
+ sarif_display,
913
  ]
914
 
915
  # Wire 1-Click Preset Scenario Buttons
 
968
  borderline_table,
969
  frameworks_table,
970
  fine_liability_output,
971
+ bom_display,
972
+ sarif_display,
973
  ],
974
  )
975
 
data/active_learning_triplets.jsonl CHANGED
@@ -15,3 +15,8 @@
15
  {"timestamp": "2026-09-20T20:00:35.196971+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
16
  {"timestamp": "2026-09-20T20:01:32.495427+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
17
  {"timestamp": "2026-09-20T20:12:47.133587+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
 
 
 
 
 
 
15
  {"timestamp": "2026-09-20T20:00:35.196971+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
16
  {"timestamp": "2026-09-20T20:01:32.495427+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
17
  {"timestamp": "2026-09-20T20:12:47.133587+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
18
+ {"timestamp": "2026-09-21T14:35:57.733905+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
19
+ {"timestamp": "2026-09-21T14:43:22.767001+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
20
+ {"timestamp": "2026-09-21T14:46:20.754528+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
21
+ {"timestamp": "2026-09-21T14:47:50.118531+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
22
+ {"timestamp": "2026-09-21T14:48:40.951594+00:00", "auditor_id": "compliance_lead_01", "claim_id": "clm_test_99", "anchor_text": "Verified operational override in production dashboard.", "positive_label": "HUMAN_OVERSIGHT", "negative_label": "IRRELEVANT_TEXT", "verified_assertion_status": "IMPLEMENTED", "auditor_notes": "Verified operational override in production dashboard."}
src/cli.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ReguAI CLI: Automated CI/CD Regulatory Conformity Gate & Audit Runner.
3
+ Evaluates model cards, system specifications, and documentation for EU AI Act,
4
+ NIST AI RMF, and ISO 42001 compliance, outputting SARIF, JSON, or Annex IV reports.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import argparse
10
+ import json
11
+ from pathlib import Path
12
+ from typing import List, Optional
13
+
14
+ if hasattr(sys.stdout, "reconfigure"):
15
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
16
+ if hasattr(sys.stderr, "reconfigure"):
17
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
18
+
19
+ from src.engine import ReguAIEngine
20
+ from src.core.models import ConformityReport
21
+ from src.triage.sarif_exporter import SarifExporter
22
+
23
+
24
+ def render_terminal_summary(report: ConformityReport, target_path: str) -> str:
25
+ """Renders an executive terminal compliance summary."""
26
+ status_symbol = "[PASS]" if report.overall_conforms else "[FAIL]"
27
+ lines = [
28
+ "=" * 78,
29
+ f" REGUAI REGULATORY AUDIT REPORT: {report.system_metadata.name}",
30
+ "=" * 78,
31
+ f"Target Specification : {target_path}",
32
+ f"System Identifier : {report.system_metadata.system_id} (v{report.system_metadata.version})",
33
+ f"Risk Classification : {report.system_metadata.eu_risk_classification}",
34
+ f"Conformity Status : {status_symbol} ({report.conformity_score:.1f}%)",
35
+ f"Requirements Passed : {report.passed_requirements_count} / {report.total_requirements_evaluated}",
36
+ f"Digital Audit Token : {report.provenance.digital_signature}",
37
+ "-" * 78,
38
+ ]
39
+
40
+ if report.violations:
41
+ lines.append(f"CRITICAL LEGAL VIOLATIONS ({len(report.violations)}):")
42
+ for idx, v in enumerate(report.violations, 1):
43
+ lines.append(f" {idx}. [{v.regulatory_article}] {v.message}")
44
+ lines.append(f" Remediation: {v.remediation_guidance}")
45
+ lines.append(f" Reference : {v.normative_reference}")
46
+ lines.append("-" * 78)
47
+
48
+ if report.warnings:
49
+ lines.append(f"ADVISORY WARNINGS ({len(report.warnings)}):")
50
+ for idx, w in enumerate(report.warnings, 1):
51
+ lines.append(f" {idx}. [{w.regulatory_article}] {w.message}")
52
+ lines.append("-" * 78)
53
+
54
+ if report.fine_exposure and report.fine_exposure.get("total_potential_fine_eur", 0) > 0:
55
+ fine = report.fine_exposure
56
+ lines.append(f"EU AI ACT ARTICLE 99 MAXIMUM FINE LIABILITY:")
57
+ lines.append(f" Statutory Fine Exposure: €{fine['total_potential_fine_eur']:,.2f}")
58
+ lines.append(f" Severity Bracket : {fine.get('severity_bracket', 'Standard')}")
59
+ lines.append("-" * 78)
60
+
61
+ return "\n".join(lines)
62
+
63
+
64
+ def audit_target(args: argparse.Namespace) -> int:
65
+ """Executes conformity audit on single file or directory."""
66
+ target = Path(args.target)
67
+ if not target.exists():
68
+ print(f"Error: Target path '{target}' does not exist.", file=sys.stderr)
69
+ return 2
70
+
71
+ files_to_audit: List[Path] = []
72
+ if target.is_dir():
73
+ for ext in ("*.json", "*.md", "*.yaml", "*.yml"):
74
+ files_to_audit.extend(target.glob(ext))
75
+ else:
76
+ files_to_audit.append(target)
77
+
78
+ if not files_to_audit:
79
+ print(f"Error: No JSON, Markdown, or YAML specifications found in '{target}'.", file=sys.stderr)
80
+ return 2
81
+
82
+ engine = ReguAIEngine()
83
+ overall_exit_code = 0
84
+ all_reports: List[ConformityReport] = []
85
+
86
+ for file_path in files_to_audit:
87
+ report = engine.evaluate_system(
88
+ input_data=file_path,
89
+ auditor_id=args.auditor or "reguai_ci_cd",
90
+ annual_turnover_eur=args.turnover,
91
+ is_sme=args.sme,
92
+ )
93
+ all_reports.append(report)
94
+
95
+ if not report.overall_conforms:
96
+ overall_exit_code = 1
97
+
98
+ # Format output
99
+ if args.format == "text":
100
+ print(render_terminal_summary(report, str(file_path)))
101
+ elif args.format == "json":
102
+ print(json.dumps(report.model_dump(), indent=2))
103
+ elif args.format == "markdown":
104
+ print(engine.report_generator.generate_markdown_report(report))
105
+ elif args.format == "sarif":
106
+ sarif_doc = SarifExporter.generate_sarif(report, target_file_path=str(file_path))
107
+ print(json.dumps(sarif_doc, indent=2))
108
+
109
+ # Output file artifacts if requested
110
+ if args.sarif_out:
111
+ SarifExporter.export_sarif_json(
112
+ report,
113
+ output_file_path=args.sarif_out,
114
+ target_file_path=str(file_path),
115
+ )
116
+ if args.format == "text":
117
+ print(f" [+] SARIF report written to: {args.sarif_out}")
118
+
119
+ if args.json_out:
120
+ p = Path(args.json_out)
121
+ p.parent.mkdir(parents=True, exist_ok=True)
122
+ p.write_text(json.dumps(report.model_dump(), indent=2), encoding="utf-8")
123
+ if args.format == "text":
124
+ print(f" [+] JSON report written to: {args.json_out}")
125
+
126
+ if args.md_out:
127
+ p = Path(args.md_out)
128
+ p.parent.mkdir(parents=True, exist_ok=True)
129
+ md_content = engine.report_generator.generate_markdown_report(report)
130
+ p.write_text(md_content, encoding="utf-8")
131
+ if args.format == "text":
132
+ print(f" [+] Annex IV Markdown written to: {args.md_out}")
133
+
134
+ if args.bom_out:
135
+ p = Path(args.bom_out)
136
+ p.parent.mkdir(parents=True, exist_ok=True)
137
+ bom_content = engine.report_generator.generate_cyclonedx_bom(report)
138
+ p.write_text(json.dumps(bom_content, indent=2), encoding="utf-8")
139
+ if args.format == "text":
140
+ print(f" [+] CycloneDX AI-BOM written to: {args.bom_out}")
141
+
142
+ if args.fail_on_violation and overall_exit_code != 0:
143
+ return 1
144
+ return 0
145
+
146
+
147
+ def main():
148
+ parser = argparse.ArgumentParser(
149
+ prog="reguai",
150
+ description="ReguAI: Neuro-Symbolic AI GRC & Deterministic Conformity Assessment Engine",
151
+ )
152
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
153
+
154
+ # Audit sub-command
155
+ audit_parser = subparsers.add_parser("audit", help="Audit model card or system spec against EU AI Act")
156
+ audit_parser.add_argument("target", help="Path to specification file or directory")
157
+ audit_parser.add_argument("--format", choices=["text", "json", "markdown", "sarif"], default="text", help="Output format")
158
+ audit_parser.add_argument("--fail-on-violation", action="store_true", help="Exit with code 1 if violations are detected")
159
+ audit_parser.add_argument("--auditor", default="ci_cd_automated_gate", help="Auditor / pipeline identifier")
160
+ audit_parser.add_argument("--turnover", type=float, default=0.0, help="Annual turnover in EUR for fine calculation")
161
+ audit_parser.add_argument("--sme", action="store_true", help="Whether developer is SME under Art 99(6)")
162
+ audit_parser.add_argument("--sarif-out", help="File path to write SARIF 2.1.0 output")
163
+ audit_parser.add_argument("--json-out", help="File path to write JSON report output")
164
+ audit_parser.add_argument("--md-out", help="File path to write Annex IV Markdown output")
165
+ audit_parser.add_argument("--bom-out", help="File path to write CycloneDX AI-BOM output")
166
+
167
+ # Benchmark sub-command
168
+ benchmark_parser = subparsers.add_parser("benchmark", help="Run regulatory extraction and triage benchmark")
169
+
170
+ # Version sub-command
171
+ subparsers.add_parser("version", help="Print ReguAI version")
172
+
173
+ args = parser.parse_args()
174
+
175
+ if args.command == "audit":
176
+ sys.exit(audit_target(args))
177
+ elif args.command == "benchmark":
178
+ from evaluate_benchmark import run_benchmark
179
+ run_benchmark()
180
+ sys.exit(0)
181
+ elif args.command == "version":
182
+ print("ReguAI Neuro-Symbolic GRC Engine v0.1.0")
183
+ sys.exit(0)
184
+ else:
185
+ parser.print_help()
186
+ sys.exit(0)
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
src/core/models.py CHANGED
@@ -32,6 +32,7 @@ class EntityCategory(str, Enum):
32
  ACCURACY_ROBUSTNESS = "ACCURACY_ROBUSTNESS" # Art 15(1)
33
  CYBERSECURITY = "CYBERSECURITY" # Art 15(4)
34
  FAIL_SAFE = "FAIL_SAFE" # Art 14(4)(e) / Art 15
 
35
 
36
 
37
  class ExtractedClaim(BaseModel):
 
32
  ACCURACY_ROBUSTNESS = "ACCURACY_ROBUSTNESS" # Art 15(1)
33
  CYBERSECURITY = "CYBERSECURITY" # Art 15(4)
34
  FAIL_SAFE = "FAIL_SAFE" # Art 14(4)(e) / Art 15
35
+ WATERMARKING_CONTROL = "WATERMARKING_CONTROL" # Art 50(2) / Synthetic Content Marking
36
 
37
 
38
  class ExtractedClaim(BaseModel):
src/extraction/gliner_extractor.py CHANGED
@@ -100,6 +100,13 @@ class RegulatoryClaimExtractor:
100
  r"\b(conformity\s+assessment\s+file|design\s+specification)\b",
101
  ],
102
  },
 
 
 
 
 
 
 
103
  }
104
 
105
  def extract_claims(self, text: str) -> List[ExtractedClaim]:
 
100
  r"\b(conformity\s+assessment\s+file|design\s+specification)\b",
101
  ],
102
  },
103
+ EntityCategory.WATERMARKING_CONTROL: {
104
+ "article": "Article 50(2)",
105
+ "keywords": [
106
+ r"\b(watermark(?:ing)?|c2pa|machine-readable\s+provenance|synthetic\s+content\s+marking)\b",
107
+ r"\b(steganograph(?:y|ic)|ai-generated\s+disclosure|deepfake\s+detection)\b",
108
+ ],
109
+ },
110
  }
111
 
112
  def extract_claims(self, text: str) -> List[ExtractedClaim]:
src/ontology/builder.py CHANGED
@@ -31,6 +31,7 @@ class NormativeGraphBuilder:
31
  EntityCategory.ACCURACY_ROBUSTNESS: (REGU.hasRobustnessControl, REGU.RobustnessControl),
32
  EntityCategory.CYBERSECURITY: (REGU.hasCybersecurityControl, REGU.CybersecurityControl),
33
  EntityCategory.FAIL_SAFE: (REGU.hasEmergencyStop, REGU.StopMechanism),
 
34
  }
35
 
36
  def _bind_namespaces(self, g: Graph) -> None:
@@ -70,6 +71,8 @@ class NormativeGraphBuilder:
70
  g.add((sys_uri, RDF.type, REGU.ProhibitedAISystem))
71
  g.add((sys_uri, REGU.prohibitionStatus, REGU.ProhibitedPracticeDetected))
72
  g.add((sys_uri, REGU.hasProhibitedPracticeType, REGU.ProhibitedPracticeDetected))
 
 
73
  elif "general purpose" in risk_class_lower or "gpai" in risk_class_lower:
74
  if "systemic" in risk_class_lower or "article 51" in risk_class_lower:
75
  g.add((sys_uri, RDF.type, REGU.GPAISystemicRiskModel))
 
31
  EntityCategory.ACCURACY_ROBUSTNESS: (REGU.hasRobustnessControl, REGU.RobustnessControl),
32
  EntityCategory.CYBERSECURITY: (REGU.hasCybersecurityControl, REGU.CybersecurityControl),
33
  EntityCategory.FAIL_SAFE: (REGU.hasEmergencyStop, REGU.StopMechanism),
34
+ EntityCategory.WATERMARKING_CONTROL: (REGU.hasWatermarkingControl, REGU.WatermarkingControl),
35
  }
36
 
37
  def _bind_namespaces(self, g: Graph) -> None:
 
71
  g.add((sys_uri, RDF.type, REGU.ProhibitedAISystem))
72
  g.add((sys_uri, REGU.prohibitionStatus, REGU.ProhibitedPracticeDetected))
73
  g.add((sys_uri, REGU.hasProhibitedPracticeType, REGU.ProhibitedPracticeDetected))
74
+ elif "generative" in risk_class_lower or "genai" in risk_class_lower or "synthetic" in risk_class_lower or "article 50" in risk_class_lower:
75
+ g.add((sys_uri, RDF.type, REGU.GenerativeAISystem))
76
  elif "general purpose" in risk_class_lower or "gpai" in risk_class_lower:
77
  if "systemic" in risk_class_lower or "article 51" in risk_class_lower:
78
  g.add((sys_uri, RDF.type, REGU.GPAISystemicRiskModel))
src/ontology/shacl/transparency_shapes.ttl ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
2
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
3
+ @prefix sh: <http://www.w3.org/ns/shacl#> .
4
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
5
+ @prefix regu: <http://regu.ai/schema#> .
6
+ @prefix eu: <http://data.europa.eu/eli/reg/2024/1689#> .
7
+
8
+ # =========================================================================
9
+ # ReguAI Normative SHACL Shape Validation
10
+ # Regulation (EU) 2024/1689 - Article 50 (Transparency Obligations for Certain AI Systems)
11
+ # =========================================================================
12
+
13
+ regu:GenerativeAITransparencyShape
14
+ a sh:NodeShape ;
15
+ sh:targetClass regu:GenerativeAISystem ;
16
+ sh:name "EU AI Act Article 50 Generative AI Synthetic Content Transparency Shape" ;
17
+ sh:description "Enforces watermarking, machine-readable detection, and interaction disclosures under Article 50." ;
18
+
19
+ # Article 50(1): Direct Interaction Disclosure
20
+ sh:property [
21
+ sh:path regu:hasTransparencySpecification ;
22
+ sh:minCount 1 ;
23
+ sh:severity sh:Violation ;
24
+ sh:message "EU AI Act Art 50(1) Non-Conformity: Systems directly interacting with natural persons must disclose that the user is interacting with an AI system." ;
25
+ ] ;
26
+
27
+ # Article 50(2): Synthetic Content Watermarking & Machine-Readable Provenance
28
+ sh:property [
29
+ sh:path regu:hasWatermarkingControl ;
30
+ sh:minCount 1 ;
31
+ sh:severity sh:Violation ;
32
+ sh:message "EU AI Act Art 50(2) Non-Conformity: AI systems generating synthetic text, image, audio, or video must implement machine-readable watermarking or detection metadata." ;
33
+ ] ;
34
+ sh:property [
35
+ sh:path ( regu:hasWatermarkingControl regu:implementationStatus ) ;
36
+ sh:hasValue regu:Implemented ;
37
+ sh:severity sh:Violation ;
38
+ sh:message "EU AI Act Art 50(2) Non-Conformity: Synthetic content watermarking control must be 'Implemented'." ;
39
+ ] .
40
+
41
+ regu:GPAICopyrightTransparencyShape
42
+ a sh:NodeShape ;
43
+ sh:targetClass regu:GPAIModel ;
44
+ sh:name "EU AI Act Article 53 GPAI Copyright & Training Data Disclosure Shape" ;
45
+ sh:description "Enforces copyright reservation compliance and training data summary disclosure under Article 53." ;
46
+
47
+ # Article 53(1)(c): Copyright Policy Compliance
48
+ sh:property [
49
+ sh:path regu:hasDataGovernance ;
50
+ sh:minCount 1 ;
51
+ sh:severity sh:Violation ;
52
+ sh:message "EU AI Act Art 53(1)(c) Non-Conformity: Providers of GPAI models must maintain and document copyright reservation policies." ;
53
+ ] .
src/triage/bom_generator.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CycloneDX 1.6 AI Bill of Materials (AIBOM) Generator.
3
+ Produces machine-readable AI Bill of Materials adhering to OWASP CycloneDX v1.6
4
+ specifications for EU AI Act Annex IV, NIST AI RMF, and enterprise GRC ingestion.
5
+ """
6
+
7
+ from typing import Dict, Any, List, Optional
8
+ import uuid
9
+ from datetime import datetime, timezone
10
+ import json
11
+
12
+ from src.core.models import ConformityReport, EntityCategory, AssertionStatus
13
+
14
+
15
+ class AIBOMGenerator:
16
+ """Generates official CycloneDX 1.6 AI Bill of Materials (AIBOM) documents."""
17
+
18
+ CYCLONEDX_VERSION = "1.6"
19
+ SPEC_SCHEMA = f"http://cyclonedx.org/schema/bom-{CYCLONEDX_VERSION}.schema.json"
20
+
21
+ @classmethod
22
+ def generate_bom(cls, report: ConformityReport) -> Dict[str, Any]:
23
+ """Translates a ConformityReport into a structured CycloneDX 1.6 AIBOM dictionary."""
24
+ meta = report.system_metadata
25
+ prov = report.provenance
26
+ serial_uuid = f"urn:uuid:{uuid.uuid4()}"
27
+
28
+ # Extract datasets and governance components from claims
29
+ sub_components: List[Dict[str, Any]] = []
30
+ for idx, claim in enumerate(report.claims_analyzed):
31
+ comp_type = "data" if claim.category == EntityCategory.DATA_GOVERNANCE else "framework"
32
+ sub_components.append({
33
+ "type": comp_type,
34
+ "bom-ref": f"component-{claim.claim_id}",
35
+ "name": claim.category.value,
36
+ "description": claim.entity_text,
37
+ "properties": [
38
+ {"name": "reguai:assertionStatus", "value": claim.assertion_status.value},
39
+ {"name": "reguai:confidence", "value": str(round(claim.confidence, 3))},
40
+ {"name": "reguai:normativeArticle", "value": claim.normative_article},
41
+ {"name": "reguai:evidenceQuote", "value": claim.evidence_quote[:200]},
42
+ ]
43
+ })
44
+
45
+ # Base CycloneDX 1.6 AI model component
46
+ model_component: Dict[str, Any] = {
47
+ "type": "machine-learning-model",
48
+ "bom-ref": f"model-{meta.system_id}",
49
+ "name": meta.name,
50
+ "version": meta.version,
51
+ "description": meta.intended_purpose,
52
+ "supplier": {
53
+ "name": meta.developer_name,
54
+ },
55
+ "modelCard": {
56
+ "modelParameters": {
57
+ "task": meta.domain,
58
+ "architecture": meta.eu_risk_classification,
59
+ },
60
+ "quantitativeAnalysis": {
61
+ "performanceMetrics": [
62
+ {
63
+ "type": "ConformityScore",
64
+ "value": f"{report.conformity_score:.1f}%",
65
+ "description": "Deterministic EU AI Act SHACL Normative Conformity Index"
66
+ },
67
+ {
68
+ "type": "PassedRequirements",
69
+ "value": f"{report.passed_requirements_count}/{report.total_requirements_evaluated}",
70
+ "description": "Total satisfied legal shape constraints"
71
+ }
72
+ ]
73
+ },
74
+ "considerations": {
75
+ "users": [meta.deployment_context],
76
+ "useCases": [meta.intended_purpose],
77
+ "limitations": [
78
+ v.message for v in report.violations
79
+ ] if report.violations else ["None identified under verified constraints."]
80
+ }
81
+ },
82
+ "properties": [
83
+ {"name": "reguai:riskClassification", "value": meta.eu_risk_classification},
84
+ {"name": "reguai:conformityStatus", "value": "CONFORMS" if report.overall_conforms else "NON_CONFORMANT"},
85
+ {"name": "reguai:digitalSignature", "value": prov.digital_signature},
86
+ {"name": "reguai:inputDocSha256", "value": prov.input_doc_sha256},
87
+ {"name": "reguai:rulesetSha256", "value": prov.ruleset_sha256},
88
+ ]
89
+ }
90
+
91
+ # Regulatory compliance declarations under CycloneDX 1.6 declarations
92
+ declarations = {
93
+ "standards": [
94
+ {
95
+ "name": "Regulation (EU) 2024/1689 (EU AI Act)",
96
+ "version": "2024/1689",
97
+ "description": "European Union Artificial Intelligence Act",
98
+ "status": "passed" if report.overall_conforms else "failed",
99
+ "requirements": [
100
+ {
101
+ "identifier": v.regulatory_article,
102
+ "title": v.regulatory_article,
103
+ "conformance": {
104
+ "score": 0.0,
105
+ "rationale": v.message
106
+ }
107
+ } for v in report.violations
108
+ ]
109
+ },
110
+ {
111
+ "name": "NIST AI RMF 1.0",
112
+ "version": "1.0",
113
+ "description": "NIST Artificial Intelligence Risk Management Framework",
114
+ "status": "passed" if report.overall_conforms else "warning"
115
+ },
116
+ {
117
+ "name": "ISO/IEC 42001:2023",
118
+ "version": "2023",
119
+ "description": "Artificial Intelligence Management System",
120
+ "status": "passed" if report.overall_conforms else "warning"
121
+ }
122
+ ]
123
+ }
124
+
125
+ bom_doc: Dict[str, Any] = {
126
+ "$schema": cls.SPEC_SCHEMA,
127
+ "bomFormat": "CycloneDX",
128
+ "specVersion": cls.CYCLONEDX_VERSION,
129
+ "serialNumber": serial_uuid,
130
+ "version": 1,
131
+ "metadata": {
132
+ "timestamp": report.generated_at_utc,
133
+ "tools": [
134
+ {
135
+ "vendor": "ReguAI Governance",
136
+ "name": "ReguAI Neuro-Symbolic GRC Core",
137
+ "version": "0.1.0"
138
+ }
139
+ ],
140
+ "component": model_component,
141
+ },
142
+ "components": sub_components,
143
+ "declarations": declarations,
144
+ "vulnerabilities": [
145
+ {
146
+ "id": f"EU-AI-ACT-{v.regulatory_article.replace(' ', '-')}",
147
+ "description": v.message,
148
+ "recommendation": v.remediation_guidance,
149
+ "analysis": {
150
+ "state": "exploitable" if v.severity == "Violation" else "in_triage",
151
+ "detail": v.normative_reference
152
+ }
153
+ } for v in report.violations
154
+ ]
155
+ }
156
+
157
+ return bom_doc
src/triage/report_generator.py CHANGED
@@ -399,3 +399,8 @@ class ConformityReportGenerator:
399
  </html>
400
  """
401
  return html
 
 
 
 
 
 
399
  </html>
400
  """
401
  return html
402
+
403
+ def generate_cyclonedx_bom(self, report: ConformityReport) -> Dict[str, Any]:
404
+ """Generates an official CycloneDX 1.6 AI Bill of Materials (AIBOM) dictionary."""
405
+ from src.triage.bom_generator import AIBOMGenerator
406
+ return AIBOMGenerator.generate_bom(report)
src/triage/sarif_exporter.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OASIS SARIF 2.1.0 (Static Analysis Results Interchange Format) Exporter.
3
+ Exports ReguAI Conformity Reports to standard SARIF JSON for GitHub Code Scanning,
4
+ GitLab SAST, and CI/CD security/compliance dashboards.
5
+ """
6
+
7
+ from typing import Dict, Any, List, Optional
8
+ import json
9
+ import re
10
+ from pathlib import Path
11
+
12
+ from src.core.models import ConformityReport, ValidationViolation
13
+
14
+
15
+ class SarifExporter:
16
+ """Exports ConformityReport objects to OASIS SARIF v2.1.0 JSON format."""
17
+
18
+ SCHEMA_URI = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
19
+ SARIF_VERSION = "2.1.0"
20
+ TOOL_NAME = "ReguAI"
21
+ TOOL_VERSION = "0.1.0"
22
+ INFORMATION_URI = "https://github.com/gitcodemujtaba/reguAI"
23
+
24
+ @classmethod
25
+ def _normalize_rule_id(cls, regulatory_article: str, component: str = "") -> str:
26
+ """Converts regulatory article strings into canonical SARIF rule identifiers."""
27
+ clean_art = re.sub(r"[^\w\s-]", "", regulatory_article).strip()
28
+ clean_art = re.sub(r"\s+", "-", clean_art).upper()
29
+ if not clean_art.startswith("REGU-"):
30
+ clean_art = f"REGU-{clean_art}"
31
+ return clean_art
32
+
33
+ @classmethod
34
+ def generate_sarif(
35
+ cls,
36
+ report: ConformityReport,
37
+ target_file_path: Optional[str] = None,
38
+ ) -> Dict[str, Any]:
39
+ """
40
+ Generates a valid SARIF 2.1.0 dictionary from a ConformityReport.
41
+ """
42
+ file_uri = target_file_path or f"{report.system_metadata.system_id}.json"
43
+
44
+ rules_dict: Dict[str, Dict[str, Any]] = {}
45
+ results: List[Dict[str, Any]] = []
46
+
47
+ # Process hard violations (SARIF error level)
48
+ for v in report.violations:
49
+ rule_id = cls._normalize_rule_id(v.regulatory_article, v.source_constraint_component)
50
+ if rule_id not in rules_dict:
51
+ rules_dict[rule_id] = {
52
+ "id": rule_id,
53
+ "name": f"EU_AI_Act_{v.regulatory_article.replace(' ', '_')}",
54
+ "shortDescription": {
55
+ "text": f"Mandatory compliance check: {v.regulatory_article}"
56
+ },
57
+ "fullDescription": {
58
+ "text": v.message
59
+ },
60
+ "help": {
61
+ "text": f"Remediation: {v.remediation_guidance}\nNormative Reference: {v.normative_reference}",
62
+ "markdown": f"### Remediation Guidance\n{v.remediation_guidance}\n\n**Normative Reference:** `{v.normative_reference}`"
63
+ },
64
+ "defaultConfiguration": {
65
+ "level": "error"
66
+ },
67
+ "helpUri": "https://data.europa.eu/eli/reg/2024/1689/oj",
68
+ "properties": {
69
+ "tags": ["compliance", "eu-ai-act", "governance", "shacl"],
70
+ "precision": "very-high"
71
+ }
72
+ }
73
+
74
+ result_obj = {
75
+ "ruleId": rule_id,
76
+ "level": "error",
77
+ "message": {
78
+ "text": f"[{v.regulatory_article}] {v.message}. Remediation: {v.remediation_guidance}"
79
+ },
80
+ "locations": [
81
+ {
82
+ "physicalLocation": {
83
+ "artifactLocation": {
84
+ "uri": file_uri,
85
+ "uriBaseId": "%SRCROOT%"
86
+ },
87
+ "region": {
88
+ "startLine": 1,
89
+ "startColumn": 1
90
+ }
91
+ }
92
+ }
93
+ ],
94
+ "properties": {
95
+ "focusNode": v.focus_node,
96
+ "resultPath": v.result_path,
97
+ "remediationGuidance": v.remediation_guidance,
98
+ "normativeReference": v.normative_reference
99
+ }
100
+ }
101
+ results.append(result_obj)
102
+
103
+ # Process warnings (SARIF warning level)
104
+ for w in report.warnings:
105
+ rule_id = cls._normalize_rule_id(w.regulatory_article, w.source_constraint_component)
106
+ if rule_id not in rules_dict:
107
+ rules_dict[rule_id] = {
108
+ "id": rule_id,
109
+ "name": f"Advisory_{w.regulatory_article.replace(' ', '_')}",
110
+ "shortDescription": {
111
+ "text": f"Regulatory Advisory: {w.regulatory_article}"
112
+ },
113
+ "fullDescription": {
114
+ "text": w.message
115
+ },
116
+ "defaultConfiguration": {
117
+ "level": "warning"
118
+ },
119
+ "helpUri": "https://data.europa.eu/eli/reg/2024/1689/oj",
120
+ "properties": {
121
+ "tags": ["advisory", "eu-ai-act", "governance"],
122
+ "precision": "high"
123
+ }
124
+ }
125
+
126
+ result_obj = {
127
+ "ruleId": rule_id,
128
+ "level": "warning",
129
+ "message": {
130
+ "text": f"[{w.regulatory_article}] {w.message}"
131
+ },
132
+ "locations": [
133
+ {
134
+ "physicalLocation": {
135
+ "artifactLocation": {
136
+ "uri": file_uri,
137
+ "uriBaseId": "%SRCROOT%"
138
+ },
139
+ "region": {
140
+ "startLine": 1,
141
+ "startColumn": 1
142
+ }
143
+ }
144
+ }
145
+ ],
146
+ "properties": {
147
+ "focusNode": w.focus_node,
148
+ "resultPath": w.result_path,
149
+ }
150
+ }
151
+ results.append(result_obj)
152
+
153
+ sarif_doc = {
154
+ "$schema": cls.SCHEMA_URI,
155
+ "version": cls.SARIF_VERSION,
156
+ "runs": [
157
+ {
158
+ "tool": {
159
+ "driver": {
160
+ "name": cls.TOOL_NAME,
161
+ "version": cls.TOOL_VERSION,
162
+ "informationUri": cls.INFORMATION_URI,
163
+ "rules": list(rules_dict.values()),
164
+ }
165
+ },
166
+ "invocations": [
167
+ {
168
+ "executionSuccessful": True,
169
+ "endTimeUtc": report.generated_at_utc,
170
+ "properties": {
171
+ "conformityScore": report.conformity_score,
172
+ "overallConforms": report.overall_conforms,
173
+ "digitalSignature": report.provenance.digital_signature,
174
+ }
175
+ }
176
+ ],
177
+ "results": results,
178
+ }
179
+ ]
180
+ }
181
+ return sarif_doc
182
+
183
+ @classmethod
184
+ def export_sarif_json(
185
+ cls,
186
+ report: ConformityReport,
187
+ output_file_path: Optional[str] = None,
188
+ target_file_path: Optional[str] = None,
189
+ indent: int = 2,
190
+ ) -> str:
191
+ """Serializes SARIF document to JSON string and optionally writes to file."""
192
+ sarif_data = cls.generate_sarif(report, target_file_path=target_file_path)
193
+ json_str = json.dumps(sarif_data, indent=indent)
194
+ if output_file_path:
195
+ out_p = Path(output_file_path)
196
+ out_p.parent.mkdir(parents=True, exist_ok=True)
197
+ out_p.write_text(json_str, encoding="utf-8")
198
+ return json_str
src/triage/train_triplets.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Active Learning Triplet Metric Learning & Confidence Calibrator.
3
+ Consumes auditor-in-the-loop triplet instances (anchor, positive, negative),
4
+ evaluates semantic margin separation, and trains/updates category prototype embeddings
5
+ and confidence calibration thresholds.
6
+ """
7
+
8
+ from typing import Dict, Any, List, Tuple, Optional
9
+ import sys
10
+ import json
11
+ import math
12
+ from collections import defaultdict
13
+ import re
14
+ from pathlib import Path
15
+
16
+ # Ensure project root is in sys.path
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
18
+
19
+ from src.core.config import DATA_DIR
20
+ from src.core.models import EntityCategory, AssertionStatus
21
+
22
+
23
+ class TripletMetricLearner:
24
+ """
25
+ Contrastive metric learner and prototype calibrator for regulatory claim assertions.
26
+ Enforces that anchor quotes are closer to auditor-approved positive labels than rejected negative labels.
27
+ """
28
+
29
+ def __init__(self, data_path: Optional[Path] = None, margin: float = 0.2):
30
+ self.data_path = data_path or (DATA_DIR / "active_learning_triplets.jsonl")
31
+ self.margin = margin
32
+ self.category_prototypes: Dict[str, Dict[str, float]] = defaultdict(lambda: defaultdict(float))
33
+ self.vocabulary: Dict[str, int] = {}
34
+
35
+ def _tokenize(self, text: str) -> List[str]:
36
+ """Simple linguistic tokenizer converting text to normalized unigram and bigram tokens."""
37
+ clean = re.sub(r"[^\w\s]", " ", text.lower())
38
+ tokens = [t for t in clean.split() if len(t) > 2]
39
+ bigrams = [f"{tokens[i]}_{tokens[i+1]}" for i in range(len(tokens) - 1)]
40
+ return tokens + bigrams
41
+
42
+ def _compute_bow_vector(self, text: str) -> Dict[str, float]:
43
+ """Builds an L2-normalized bag-of-words vector."""
44
+ tokens = self._tokenize(text)
45
+ if not tokens:
46
+ return {}
47
+ counts: Dict[str, float] = defaultdict(float)
48
+ for t in tokens:
49
+ counts[t] += 1.0
50
+
51
+ norm = math.sqrt(sum(v * v for v in counts.values()))
52
+ if norm > 0:
53
+ return {k: v / norm for k, v in counts.items()}
54
+ return counts
55
+
56
+ def _cosine_similarity(self, vec_a: Dict[str, float], vec_b: Dict[str, float]) -> float:
57
+ """Computes sparse cosine similarity between two normalized vectors."""
58
+ dot = 0.0
59
+ for k, val_a in vec_a.items():
60
+ if k in vec_b:
61
+ dot += val_a * vec_b[k]
62
+ return max(0.0, min(1.0, dot))
63
+
64
+ def load_triplets(self) -> List[Dict[str, Any]]:
65
+ """Loads all recorded active learning triplets from disk."""
66
+ if not self.data_path.exists():
67
+ return []
68
+ records = []
69
+ with open(self.data_path, "r", encoding="utf-8") as f:
70
+ for line in f:
71
+ line = line.strip()
72
+ if line:
73
+ records.append(json.loads(line))
74
+ return records
75
+
76
+ def train_prototypes(self, learning_rate: float = 0.1, epochs: int = 5) -> Dict[str, Any]:
77
+ """
78
+ Fits category prototype vectors using gradient-inspired updates over the triplets.
79
+ Pulls positive category prototype toward anchor; pushes negative category prototype away.
80
+ """
81
+ triplets = self.load_triplets()
82
+ if not triplets:
83
+ return {
84
+ "triplets_count": 0,
85
+ "status": "NO_DATA",
86
+ "loss": 0.0,
87
+ "separation_accuracy": 0.0,
88
+ }
89
+
90
+ # Initialize prototypes with positive anchors
91
+ for item in triplets:
92
+ anchor_vec = self._compute_bow_vector(item["anchor_text"])
93
+ pos_label = item["positive_label"]
94
+ for token, weight in anchor_vec.items():
95
+ self.category_prototypes[pos_label][token] += weight
96
+
97
+ # Normalize prototypes
98
+ for cat, tokens in self.category_prototypes.items():
99
+ norm = math.sqrt(sum(v * v for v in tokens.values()))
100
+ if norm > 0:
101
+ self.category_prototypes[cat] = {k: v / norm for k, v in tokens.items()}
102
+
103
+ # Iterative contrastive margin optimization
104
+ history = []
105
+ for epoch in range(epochs):
106
+ total_loss = 0.0
107
+ correct_pairs = 0
108
+
109
+ for item in triplets:
110
+ anchor_vec = self._compute_bow_vector(item["anchor_text"])
111
+ pos_proto = self.category_prototypes[item["positive_label"]]
112
+ neg_proto = self.category_prototypes.get(item["negative_label"], {})
113
+
114
+ sim_pos = self._cosine_similarity(anchor_vec, pos_proto)
115
+ sim_neg = self._cosine_similarity(anchor_vec, neg_proto)
116
+
117
+ # Triplet ranking margin loss: max(0, sim_neg - sim_pos + margin)
118
+ loss = max(0.0, sim_neg - sim_pos + self.margin)
119
+ total_loss += loss
120
+
121
+ if sim_pos > sim_neg:
122
+ correct_pairs += 1
123
+
124
+ # Gradient step if violation occurs
125
+ if loss > 0:
126
+ for k, val in anchor_vec.items():
127
+ self.category_prototypes[item["positive_label"]][k] += learning_rate * val
128
+ if item["negative_label"] in self.category_prototypes:
129
+ self.category_prototypes[item["negative_label"]][k] -= learning_rate * val * 0.5
130
+
131
+ avg_loss = total_loss / len(triplets)
132
+ acc = (correct_pairs / len(triplets)) * 100.0
133
+ history.append({"epoch": epoch + 1, "loss": round(avg_loss, 4), "accuracy": round(acc, 1)})
134
+
135
+ return {
136
+ "triplets_count": len(triplets),
137
+ "status": "CONVERGED",
138
+ "final_loss": history[-1]["loss"],
139
+ "separation_accuracy": history[-1]["accuracy"],
140
+ "training_history": history,
141
+ "trained_categories": list(self.category_prototypes.keys()),
142
+ }
143
+
144
+ def predict_category(self, query_text: str) -> Tuple[str, float]:
145
+ """Predicts the most aligned regulatory category and returns confidence score."""
146
+ q_vec = self._compute_bow_vector(query_text)
147
+ best_cat = "UNKNOWN"
148
+ best_sim = 0.0
149
+
150
+ for cat, proto in self.category_prototypes.items():
151
+ sim = self._cosine_similarity(q_vec, proto)
152
+ if sim > best_sim:
153
+ best_sim = sim
154
+ best_cat = cat
155
+
156
+ return best_cat, best_sim
157
+
158
+
159
+ def main():
160
+ print("=" * 70)
161
+ print(" REGUAI ACTIVE LEARNING TRIPLET METRIC LEARNER & CALIBRATOR")
162
+ print("=" * 70)
163
+ learner = TripletMetricLearner()
164
+ results = learner.train_prototypes(epochs=5)
165
+ print(f"Loaded Active Learning Feedback Triplets : {results['triplets_count']}")
166
+ print(f"Training Status : {results['status']}")
167
+ print(f"Final Triplet Margin Loss : {results.get('final_loss', 0.0)}")
168
+ print(f"Anchor Separation Accuracy : {results.get('separation_accuracy', 0.0)}%")
169
+ print(f"Calibrated Categories : {results.get('trained_categories', [])}")
170
+ print("=" * 70)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
tests/test_cli_and_bom.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Unit and integration tests for ReguAI CLI, SARIF Exporter, CycloneDX 1.6 AI-BOM,
3
+ and Active Learning Triplet Metric Learner.
4
+ """
5
+
6
+ import pytest
7
+ import json
8
+ from pathlib import Path
9
+ from fastapi.testclient import TestClient
10
+
11
+ from src.engine import ReguAIEngine
12
+ from src.core.config import SYNTHETIC_DIR
13
+ from src.triage.sarif_exporter import SarifExporter
14
+ from src.triage.bom_generator import AIBOMGenerator
15
+ from src.triage.train_triplets import TripletMetricLearner
16
+ from api import app
17
+
18
+
19
+ @pytest.fixture
20
+ def engine():
21
+ return ReguAIEngine()
22
+
23
+
24
+ @pytest.fixture
25
+ def compliant_report(engine):
26
+ path = SYNTHETIC_DIR / "compliant_clinical_samd.json"
27
+ return engine.evaluate_system(path)
28
+
29
+
30
+ @pytest.fixture
31
+ def non_compliant_report(engine):
32
+ path = SYNTHETIC_DIR / "non_compliant_hr_recruitment.json"
33
+ return engine.evaluate_system(path)
34
+
35
+
36
+ def test_sarif_export_structure(compliant_report, non_compliant_report):
37
+ # Compliant system SARIF
38
+ sarif_pass = SarifExporter.generate_sarif(compliant_report)
39
+ assert sarif_pass["version"] == "2.1.0"
40
+ assert sarif_pass["$schema"] == SarifExporter.SCHEMA_URI
41
+ assert len(sarif_pass["runs"]) == 1
42
+ assert sarif_pass["runs"][0]["tool"]["driver"]["name"] == "ReguAI"
43
+ assert len(sarif_pass["runs"][0]["results"]) == 0
44
+
45
+ # Non-compliant system SARIF
46
+ sarif_fail = SarifExporter.generate_sarif(non_compliant_report)
47
+ assert len(sarif_fail["runs"][0]["results"]) > 0
48
+ first_result = sarif_fail["runs"][0]["results"][0]
49
+ assert first_result["level"] == "error"
50
+ assert "ruleId" in first_result
51
+ assert first_result["ruleId"].startswith("REGU-")
52
+ assert "locations" in first_result
53
+
54
+
55
+ def test_cyclonedx_bom_structure(compliant_report, non_compliant_report):
56
+ bom = AIBOMGenerator.generate_bom(compliant_report)
57
+ assert bom["bomFormat"] == "CycloneDX"
58
+ assert bom["specVersion"] == "1.6"
59
+ assert "serialNumber" in bom
60
+ assert bom["serialNumber"].startswith("urn:uuid:")
61
+ assert bom["metadata"]["component"]["type"] == "machine-learning-model"
62
+ assert bom["metadata"]["component"]["name"] == compliant_report.system_metadata.name
63
+ assert "declarations" in bom
64
+ assert len(bom["declarations"]["standards"]) >= 1
65
+ assert bom["declarations"]["standards"][0]["name"] == "Regulation (EU) 2024/1689 (EU AI Act)"
66
+ assert bom["declarations"]["standards"][0]["status"] == "passed"
67
+
68
+ # Non-compliant BOM has vulnerabilities and failed standards
69
+ bom_fail = AIBOMGenerator.generate_bom(non_compliant_report)
70
+ assert bom_fail["declarations"]["standards"][0]["status"] == "failed"
71
+ assert len(bom_fail["vulnerabilities"]) > 0
72
+
73
+
74
+ def test_triplet_metric_learner():
75
+ learner = TripletMetricLearner()
76
+ results = learner.train_prototypes(epochs=3)
77
+ assert results["status"] in ("CONVERGED", "NO_DATA")
78
+ if results["status"] == "CONVERGED":
79
+ assert results["separation_accuracy"] >= 80.0
80
+ # Test category prediction
81
+ cat, conf = learner.predict_category("Clinician manual override and emergency kill switch.")
82
+ assert cat in ("HUMAN_OVERSIGHT", "FAIL_SAFE", "UNKNOWN")
83
+
84
+
85
+ def test_api_bom_and_sarif_endpoints():
86
+ client = TestClient(app)
87
+
88
+ # 1. Evaluate to register token in cache
89
+ eval_resp = client.post(
90
+ "/api/v1/audit/evaluate",
91
+ json={"specification_text": "Clinical diagnostic assistant with continuous risk management, bias mitigation, human-in-the-loop review and automated logging."}
92
+ )
93
+ assert eval_resp.status_code == 200
94
+ token = eval_resp.json()["certificate_token"]
95
+
96
+ # 2. Retrieve CycloneDX BOM
97
+ bom_resp = client.get(f"/api/v1/certificates/{token}/bom")
98
+ assert bom_resp.status_code == 200
99
+ bom_data = bom_resp.json()
100
+ assert bom_data["bomFormat"] == "CycloneDX"
101
+ assert bom_data["specVersion"] == "1.6"
102
+
103
+ # 3. Retrieve SARIF report
104
+ sarif_resp = client.get(f"/api/v1/certificates/{token}/sarif")
105
+ assert sarif_resp.status_code == 200
106
+ sarif_data = sarif_resp.json()
107
+ assert sarif_data["version"] == "2.1.0"
108
+ assert sarif_data["runs"][0]["tool"]["driver"]["name"] == "ReguAI"
109
+
110
+ # 4. Direct audit to BOM endpoint
111
+ direct_bom = client.post(
112
+ "/api/v1/audit/bom",
113
+ json={"specification_text": "Autonomous triage system with verified operational data governance and testing."}
114
+ )
115
+ assert direct_bom.status_code == 200
116
+ assert direct_bom.json()["bomFormat"] == "CycloneDX"
117
+
118
+
119
+ def test_cli_audit_execution(tmp_path):
120
+ from src.cli import main
121
+ import sys
122
+
123
+ sarif_file = tmp_path / "test.sarif"
124
+ json_file = tmp_path / "test.json"
125
+ bom_file = tmp_path / "test.bom.json"
126
+ target = str(SYNTHETIC_DIR / "compliant_clinical_samd.json")
127
+
128
+ test_args = [
129
+ "reguai", "audit", target,
130
+ "--format", "text",
131
+ "--sarif-out", str(sarif_file),
132
+ "--json-out", str(json_file),
133
+ "--bom-out", str(bom_file),
134
+ "--fail-on-violation"
135
+ ]
136
+
137
+ old_argv = sys.argv
138
+ try:
139
+ sys.argv = test_args
140
+ with pytest.raises(SystemExit) as exc_info:
141
+ main()
142
+ assert exc_info.value.code == 0
143
+ finally:
144
+ sys.argv = old_argv
145
+
146
+ assert sarif_file.exists()
147
+ assert json_file.exists()
148
+ assert bom_file.exists()