Spaces:
Sleeping
Sleeping
Deploy ReguAI: Neuro-Symbolic AI GRC & Automated Conformity Assessment Engine
Browse files- .github/workflows/reguai-gate.yml +70 -0
- README.md +18 -1
- api.py +39 -0
- app.py +16 -1
- data/active_learning_triplets.jsonl +5 -0
- src/cli.py +190 -0
- src/core/models.py +1 -0
- src/extraction/gliner_extractor.py +7 -0
- src/ontology/builder.py +3 -0
- src/ontology/shacl/transparency_shapes.ttl +53 -0
- src/triage/bom_generator.py +157 -0
- src/triage/report_generator.py +5 -0
- src/triage/sarif_exporter.py +198 -0
- src/triage/train_triplets.py +174 -0
- tests/test_cli_and_bom.py +148 -0
.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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 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()
|