aicomply-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. aicomply/__init__.py +0 -0
  2. aicomply/_version.py +14 -0
  3. aicomply/classifier/__init__.py +0 -0
  4. aicomply/classifier/assess.py +220 -0
  5. aicomply/classifier/risk_tier.py +33 -0
  6. aicomply/cli.py +192 -0
  7. aicomply/config.py +49 -0
  8. aicomply/evidence/__init__.py +0 -0
  9. aicomply/evidence/hasher.py +47 -0
  10. aicomply/generator/__init__.py +0 -0
  11. aicomply/generator/annex_iv.py +226 -0
  12. aicomply/reporter/__init__.py +0 -0
  13. aicomply/reporter/json_report.py +11 -0
  14. aicomply/reporter/markdown_report.py +70 -0
  15. aicomply/reporter/sarif_reporter.py +98 -0
  16. aicomply/reporter/terminal.py +186 -0
  17. aicomply/rules/__init__.py +0 -0
  18. aicomply/rules/eu_ai_act/art10_data_gov.yaml +14 -0
  19. aicomply/rules/eu_ai_act/art11_tech_doc.yaml +12 -0
  20. aicomply/rules/eu_ai_act/art12_logging.yaml +18 -0
  21. aicomply/rules/eu_ai_act/art13_transparency.yaml +12 -0
  22. aicomply/rules/eu_ai_act/art14_oversight.yaml +14 -0
  23. aicomply/rules/eu_ai_act/art15_accuracy.yaml +38 -0
  24. aicomply/rules/eu_ai_act/art50_transparency.yaml +12 -0
  25. aicomply/rules/eu_ai_act/art5_prohibited.yaml +33 -0
  26. aicomply/rules/eu_ai_act/art9_risk_mgmt.yaml +12 -0
  27. aicomply/rules/gdpr/art005_principles.yaml +12 -0
  28. aicomply/rules/gdpr/art05_pii.yaml +31 -0
  29. aicomply/rules/gdpr/art09_special_data.yaml +14 -0
  30. aicomply/rules/gdpr/art22_automated_decisions.yaml +14 -0
  31. aicomply/rules/gdpr/art32_security.yaml +56 -0
  32. aicomply/rules/loader.py +99 -0
  33. aicomply/scanner/__init__.py +0 -0
  34. aicomply/scanner/ast_parser.py +306 -0
  35. aicomply/scanner/engine.py +179 -0
  36. aicomply/scanner/regex_matcher.py +96 -0
  37. aicomply/schemas.py +136 -0
  38. aicomply_cli-0.1.0.dist-info/METADATA +369 -0
  39. aicomply_cli-0.1.0.dist-info/RECORD +42 -0
  40. aicomply_cli-0.1.0.dist-info/WHEEL +5 -0
  41. aicomply_cli-0.1.0.dist-info/entry_points.txt +2 -0
  42. aicomply_cli-0.1.0.dist-info/top_level.txt +1 -0
aicomply/__init__.py ADDED
File without changes
aicomply/_version.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ AIComply - Version Resolution
3
+ Centraliza la versión del paquete usando importlib.metadata.
4
+ Fallback a '0.1.0-dev' si el paquete no está instalado.
5
+ """
6
+
7
+ try:
8
+ from importlib.metadata import version
9
+ try:
10
+ __version__ = version("aicomply-cli")
11
+ except Exception:
12
+ __version__ = version("aicomply")
13
+ except Exception:
14
+ __version__ = "0.1.0-dev"
File without changes
@@ -0,0 +1,220 @@
1
+ """
2
+ AIComply - Interactive Risk Classifier (Decision Tree)
3
+ Evalúa casos de uso y arquitecturas de IA mediante preguntas estructuradas
4
+ para clasificar el sistema según el EU AI Act y el RGPD.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import List, Optional
9
+ from rich import box
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.prompt import Confirm, Prompt
13
+ from rich.table import Table
14
+
15
+ from aicomply.schemas import RiskTier
16
+
17
+ @dataclass
18
+ class AssessmentResult:
19
+ """
20
+ Resultado estructurado de la evaluación guiada.
21
+ """
22
+ system_name: str
23
+ risk_tier: RiskTier
24
+ applicable_articles: List[str]
25
+ obligations: List[str]
26
+ compliance_deadline: str
27
+ rationale: str
28
+
29
+ def run_interactive_assessment(console: Optional[Console] = None) -> AssessmentResult:
30
+ """
31
+ Ejecuta el cuestionario interactivo en la terminal.
32
+ """
33
+ console= console or Console()
34
+
35
+ console.print(
36
+ Panel(
37
+ "[bold cyan]Asistente de Clasificación Regulatoria - EU AI Act & RGPD[/bold cyan]\n"
38
+ "[dim]Responde a las siguientes preguntas técnicas y operativas sobre tu sistema.[/dim]",
39
+ border_style="cyan",
40
+ box=box.ROUNDED,
41
+ )
42
+ )
43
+
44
+ system_name = Prompt.ask("\n[bold]1. Nombre del sistema o proyecto[/bold]", default="AI-System")
45
+
46
+ # PASO 1: Ámbito del EU AI Act
47
+ is_ai = Confirm.ask(
48
+ "\n[bold]2. ¿El sistema infiere salidas (predicciones, contenido, decisiones) a partir de entradas usando modelos autónomos (ej. LLM, ML)?", default=True,
49
+ )
50
+ if not is_ai:
51
+ return AssessmentResult(
52
+ system_name=system_name,
53
+ risk_tier=RiskTier.MINIMAL_RISK,
54
+ applicable_articles=["Fuera de ámbito (Art. 3(1))"],
55
+ obligations=["No sujeto a las obligaciones del EU AI Act."],
56
+ compliance_deadline="N/A",
57
+ rationale="El software no cumple la definición de 'sistema de IA' según el Art. 3(1).",
58
+ )
59
+
60
+ # PASO 2: Prácticas Prohibidas (Art. 5)
61
+ console.print("\n[bold yellow]── Evaluación de Prácticas Prohibidas (Art. 5) ──[/bold yellow]")
62
+ prohibited_checks = [
63
+ ("¿El sistema realiza puntuación social (social scoring) evaluando personas por su conducta?", "Art. 5(1)(c)"),
64
+ ("¿Utiliza técnicas subliminales o manipulativas para alterar el comportamiento causando daño?", "Art. 5(1)(a)"),
65
+ ("¿Infere emociones en lugares de trabajo o instituciones educativas (salvo fines médicos)?", "Art. 5(1)(f)"),
66
+ ("¿Realiza identificación biométrica remota en tiempo real en espacios públicos para fines policiales?", "Art. 5(1)(h)"),
67
+ ]
68
+
69
+ for question, ref in prohibited_checks:
70
+ if Confirm.ask(f"• {question}", default=False):
71
+ return AssessmentResult(
72
+ system_name=system_name,
73
+ risk_tier=RiskTier.PROHIBITED,
74
+ applicable_articles=["Art. 5", ref],
75
+ obligations=[
76
+ "PROHIBICIÓN TOTAL DE DESPLIEGUE Y COMERCIALIZACIÓN.",
77
+ "Retirada inmediata del mercado o cese de operaciones.",
78
+ "Sanciones de hasta 35M€ o el 7% de la facturación global anual.",
79
+ ],
80
+ compliance_deadline="Aplicable desde febrero de 2025",
81
+ rationale=f"El sistema incurre en una práctica expresamente prohibida bajo el {ref}.",
82
+ )
83
+
84
+ # PASO 3: Sistemas de Alto Riesgo (Anexo I y Anexo III)
85
+ console.print("\n[bold red]── Evaluación de Sistemas de Alto Riesgo (Art. 6 / Anexo III) ──[/bold red]")
86
+ high_risk_domains = [
87
+ "Biometría: categorización o identificación biométrica remota post-hoc.",
88
+ "Infraestructuras críticas: gestión de tráfico, redes de agua, gas, electricidad.",
89
+ "Educación y formación: admisión de alumnos o evaluación de estudiantes.",
90
+ "Empleo y RRHH: selección de personal, asignación de tareas o monitorización de rendimiento.",
91
+ "Servicios esenciales: evaluación de solvencia/scoring crediticio, acceso a ayudas públicas.",
92
+ "Fuerzas de seguridad, gestión de migración, asilo y control fronterizo.",
93
+ "Administración de justicia y procesos democráticos.",
94
+ ]
95
+ console.print("[dim]Dominios del Anexo III:[/dim]")
96
+ for domain in high_risk_domains:
97
+ console.print(f" [dim]• {domain}[/dim]")
98
+
99
+ in_high_risk = Confirm.ask(
100
+ "\n¿El sistema se utiliza en alguno de estos dominios con impacto directo en derechos o seguridad?",
101
+ default=False,
102
+ )
103
+
104
+ if in_high_risk:
105
+ is_narrow_exception = Confirm.ask(
106
+ "¿El sistema realiza únicamente una tarea procedimental estrecha sin influir materialmente en la decisión final (Art. 6(3))?",
107
+ default=False,
108
+ )
109
+ if not is_narrow_exception:
110
+ return AssessmentResult(
111
+ system_name=system_name,
112
+ risk_tier=RiskTier.HIGH_RISK,
113
+ applicable_articles=["Art. 6", "Capítulo III", "Arts. 9-15", "Anexo III"],
114
+ obligations=[
115
+ "Implantar un sistema continuo de gestión de riesgos (Art. 9).",
116
+ "Gobernanza y mitigación de sesgos en datos de entrenamiento (Art. 10).",
117
+ "Elaborar y mantener la documentación técnica (Art. 11 / Anexo IV).",
118
+ "Habilitar registro automático de eventos (logging) continuo (Art. 12).",
119
+ "Diseñar interfaces con supervisión humana efectiva y override (Art. 14).",
120
+ "Asegurar robustez, precisión técnica y ciberseguridad (Art. 15).",
121
+ "Registro obligatorio en la base de datos de la UE.",
122
+ ],
123
+ compliance_deadline="Agosto 2026 (Transparencia) / Diciembre 2027 (Alto Riesgo)",
124
+ rationale="El sistema opera dentro de los casos de uso críticos enumerados en el Anexo III.",
125
+ )
126
+
127
+ # PASO 4: Modelos de Propósito General (GPAI / LLMs)
128
+ console.print("\n[bold cyan]── Modelos de IA de Propósito General (Capítulo V) ──[/bold cyan]")
129
+ is_gpai = Confirm.ask(
130
+ "¿Desarrollas, entrenas o afinas directamente un modelo base o GPAI (ej. LLM propio)?",
131
+ default=False,
132
+ )
133
+ if is_gpai:
134
+ return AssessmentResult(
135
+ system_name=system_name,
136
+ risk_tier=RiskTier.HIGH_RISK,
137
+ applicable_articles=["Capítulo V", "Arts. 51-55"],
138
+ obligations=[
139
+ "Documentación técnica del modelo y resumen público del entrenamiento.",
140
+ "Cumplimiento de la directiva sobre derechos de autor.",
141
+ "Si supera 10^25 FLOPs: Evaluación de riesgo sistémico y pruebas adversarias.",
142
+ ],
143
+ compliance_deadline="Agosto 2025",
144
+ rationale="Clasificado como Proveedor de Modelos de IA de Propósito General (GPAI).",
145
+ )
146
+
147
+ # PASO 5: Riesgo Limitado / Transparencia (Art. 50)
148
+ console.print("\n[bold yellow]── Obligaciones de Transparencia (Art. 50) ──[/bold yellow]")
149
+ interacts_with_humans = Confirm.ask(
150
+ "¿El sistema interactúa directamente con usuarios (chatbots) o genera contenido sintético/deepfakes?",
151
+ default=True,
152
+ )
153
+ if interacts_with_humans:
154
+ return AssessmentResult(
155
+ system_name=system_name,
156
+ risk_tier=RiskTier.LIMITED_RISK,
157
+ applicable_articles=["Art. 50(1)", "Art. 50(2)", "RGPD Art. 13/14"],
158
+ obligations=[
159
+ "Notificar al usuario de forma clara e inmediata que interactúa con una IA.",
160
+ "Marcar el contenido sintético generado (audio, imagen, vídeo, texto) en formato legible por máquina.",
161
+ "Permitir el ejercicio de derechos RGPD sobre la información tratada.",
162
+ ],
163
+ compliance_deadline="Exigible desde agosto de 2026",
164
+ rationale="Sistemas que interactúan con personas o generan contenido sintético sujeto al Art. 50.",
165
+ )
166
+
167
+ # PASO 6: Riesgo Mínimo
168
+ return AssessmentResult(
169
+ system_name=system_name,
170
+ risk_tier=RiskTier.MINIMAL_RISK,
171
+ applicable_articles=["Art. 95 (Códigos de conducta voluntarios)"],
172
+ obligations=[
173
+ "Sin obligaciones regulatorias vinculantes.",
174
+ "Recomendada adhesión voluntaria a códigos de buenas prácticas éticas.",
175
+ ],
176
+ compliance_deadline="Sin límite vinculante",
177
+ rationale="Sistemas sin riesgo significativo para derechos fundamentales o seguridad física.",
178
+ )
179
+
180
+
181
+ def render_assessment_report(result: AssessmentResult, console: Optional[Console] = None) -> None:
182
+ """Presenta el dictamen de clasificación en la consola."""
183
+ console = console or Console()
184
+
185
+ tier_styles = {
186
+ RiskTier.PROHIBITED: ("PROHIBIDO (Art. 5)", "bold white on red"),
187
+ RiskTier.HIGH_RISK: ("ALTO RIESGO", "bold red"),
188
+ RiskTier.LIMITED_RISK: ("RIESGO LIMITADO", "bold yellow"),
189
+ RiskTier.MINIMAL_RISK: ("RIESGO MÍNIMO", "bold green"),
190
+ }
191
+ badge_label, badge_style = tier_styles[result.risk_tier]
192
+
193
+ table = Table(box=box.ROUNDED, expand=True)
194
+ table.add_column("Parámetro", style="bold cyan", width=24)
195
+ table.add_column("Detalle")
196
+
197
+ table.add_row("Sistema Auditado", f"[bold]{result.system_name}[/bold]")
198
+ table.add_row("Dictamen de Riesgo", f"[{badge_style}] {badge_label} [/{badge_style}]")
199
+ table.add_row("Artículos Clave", ", ".join(result.applicable_articles))
200
+ table.add_row("Entrada en Vigor", f"[bold yellow]{result.compliance_deadline}[/bold yellow]")
201
+ table.add_row("Fundamento Legal", result.rationale)
202
+
203
+ console.print()
204
+ console.print(
205
+ Panel(
206
+ table,
207
+ title="[bold blue]DICTAMEN DE CLASIFICACIÓN REGULATORIA (EU AI ACT)[/bold blue]",
208
+ border_style="bright_blue",
209
+ box=box.ROUNDED,
210
+ )
211
+ )
212
+
213
+ obs_table = Table(box=box.SIMPLE_HEAVY, expand=True)
214
+ obs_table.add_column("#", width=4, justify="center")
215
+ obs_table.add_column("Obligaciones Técnicas y Operativas Vinculantes")
216
+
217
+ for idx, ob in enumerate(result.obligations, start=1):
218
+ obs_table.add_row(str(idx), ob)
219
+
220
+ console.print(obs_table)
@@ -0,0 +1,33 @@
1
+ """
2
+ AIComply - Risk Tier Classifier
3
+ Calcula la postura global de riesgo del repositorio basándose en la severidad y artículo de los hallazgos.
4
+ """
5
+
6
+ from typing import List
7
+ from aicomply.schemas import Finding, RiskTier
8
+
9
+
10
+ # Jerarquía estricta de riesgo (de mayor a menor)
11
+ TIER_HIERARCHY = [
12
+ RiskTier.PROHIBITED,
13
+ RiskTier.HIGH_RISK,
14
+ RiskTier.LIMITED_RISK,
15
+ RiskTier.MINIMAL_RISK,
16
+ ]
17
+
18
+
19
+ def classify_overall_risk(findings: List[Finding]) -> RiskTier:
20
+ """
21
+ Determina el nivel de riesgo más restrictivo presente en el código escaneado.
22
+ Si no hay hallazgos, el sistema se cataloga como 'minimal_risk'.
23
+ """
24
+ if not findings:
25
+ return RiskTier.MINIMAL_RISK
26
+
27
+ present_tiers = {f.risk_tier for f in findings}
28
+
29
+ for tier in TIER_HIERARCHY:
30
+ if tier in present_tiers:
31
+ return tier
32
+
33
+ return RiskTier.MINIMAL_RISK
aicomply/cli.py ADDED
@@ -0,0 +1,192 @@
1
+ """
2
+ AIComply - CLI Entrypoint (Typer)
3
+ """
4
+
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Optional, Set
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from aicomply.reporter.json_report import generate_json_report
12
+ from aicomply.reporter.markdown_report import generate_markdown_report
13
+ from aicomply.reporter.terminal import render_terminal_report
14
+ from aicomply.reporter.sarif_reporter import generate_sarif_report
15
+ from aicomply.rules.loader import RuleLoadError, load_rules_from_dir
16
+ from aicomply.scanner.engine import ScanEngine
17
+
18
+ from aicomply.classifier.assess import (
19
+ render_assessment_report,
20
+ run_interactive_assessment,
21
+ )
22
+
23
+ from aicomply.generator.annex_iv import AnnexIVGenerator
24
+
25
+
26
+ app = typer.Typer(
27
+ name="aicomply",
28
+ help="CLI de cumplimiento técnico y análisis determinista del EU AI Act.",
29
+ add_completion=False,
30
+ )
31
+ console = Console()
32
+
33
+ @app.callback()
34
+ def main() -> None:
35
+ """CLI de cumplimiento técnico y análisis determinista del EU AI Act."""
36
+ pass
37
+
38
+ class OutputFormat(str, Enum):
39
+ TERMINAL = "terminal"
40
+ JSON = "json"
41
+ MARKDOWN = "markdown"
42
+ SARIF = "sarif"
43
+
44
+ def get_default_rules_dir() -> Path:
45
+ """
46
+ Obtiene la ruta base del catálogo de reglas
47
+ """
48
+ return Path(__file__).parent / "rules"
49
+
50
+
51
+ @app.command(name="scan")
52
+ def scan(
53
+ path: Path = typer.Argument(
54
+ ...,
55
+ help="Ruta al archivo o directorio a auditar.",
56
+ exists=True,
57
+ resolve_path=True,
58
+ ),
59
+ format: OutputFormat = typer.Option(
60
+ OutputFormat.TERMINAL,
61
+ "--format",
62
+ "-f",
63
+ help="Formato de salida del reporte (terminal, markdown, json).",
64
+ ),
65
+ articles: Optional[str] = typer.Option(
66
+ None,
67
+ "--articles",
68
+ "-a",
69
+ help="Filtrar por artículos separados por comas (ej. 5,12,13).",
70
+ ),
71
+ output: Optional[Path] = typer.Option(
72
+ None,
73
+ "--output",
74
+ "-o",
75
+ help="Ruta para guardar el reporte emitido en disco.",
76
+ ),
77
+ evidence: bool = typer.Option(
78
+ False,
79
+ "--evidence",
80
+ help="Incluir identificadores criptográficos SHA-256 por cada hallazgo.",
81
+ ),
82
+ rules_dir: Optional[Path] = typer.Option(
83
+ None,
84
+ "--rules-dir",
85
+ help="Directorio personalizado de reglas YAML.",
86
+ ),
87
+ ) -> None:
88
+ """Escanea el código fuente en busca de infracciones del EU AI Act."""
89
+ rules_path = rules_dir or get_default_rules_dir()
90
+
91
+ try:
92
+ catalog = load_rules_from_dir(rules_path)
93
+ except RuleLoadError as err:
94
+ console.print(f"[bold red]Error al cargar el catálogo de reglas:[/bold red] {err}")
95
+ raise typer.Exit(code=2)
96
+
97
+ target_articles: Optional[Set[str]] = None
98
+ if articles:
99
+ target_articles = {art.strip() for art in articles.split(",") if art.strip()}
100
+
101
+ engine = ScanEngine(catalog=catalog, target_articles=target_articles)
102
+
103
+ try:
104
+ report = engine.scan_path(path)
105
+ except Exception as exc:
106
+ console.print(f"[bold red]Error durante la ejecución del escaneo:[/bold red] {exc}")
107
+ raise typer.Exit(code=2)
108
+
109
+ # Gestión de salida según formato
110
+ if output:
111
+ if format == OutputFormat.JSON:
112
+ content = generate_json_report(report)
113
+ elif format == OutputFormat.SARIF:
114
+ content = generate_sarif_report(report)
115
+ else:
116
+ content = generate_markdown_report(report, include_evidence=evidence)
117
+
118
+ output.write_text(content, encoding="utf-8")
119
+ console.print(f"[green]Reporte guardado exitosamente en:[/green] {output}")
120
+ else:
121
+ if format == OutputFormat.JSON:
122
+ typer.echo(generate_json_report(report))
123
+ elif format == OutputFormat.SARIF:
124
+ typer.echo(generate_sarif_report(report))
125
+ elif format == OutputFormat.MARKDOWN:
126
+ typer.echo(generate_markdown_report(report, include_evidence=evidence))
127
+ else:
128
+ render_terminal_report(report, include_evidence=evidence, console=console)
129
+
130
+ if report.summary.total_findings > 0:
131
+ raise typer.Exit(code=1)
132
+ raise typer.Exit(code=0)
133
+
134
+ @app.command(name="assess")
135
+ def assess() -> None:
136
+ """Asistente interactivo guiado para clasificar el nivel de riesgo de un caso de uso."""
137
+ result = run_interactive_assessment(console=console)
138
+ render_assessment_report(result, console=console)
139
+
140
+ @app.command(name="docgen")
141
+ def docgen(
142
+ path: Path = typer.Argument(
143
+ ...,
144
+ help="Ruta al repositorio del proyecto para auditar y documentar.",
145
+ exists=True,
146
+ resolve_path=True,
147
+ ),
148
+ system_name: str = typer.Option(
149
+ "AI-Production-System",
150
+ "--name",
151
+ "-n",
152
+ help="Nombre formal del sistema de IA para el dossier.",
153
+ ),
154
+ system_version: str = typer.Option(
155
+ "1.0.0",
156
+ "--version",
157
+ "-v",
158
+ help="Versión del sistema de IA.",
159
+ ),
160
+ output: Path = typer.Option(
161
+ Path("ANNEX_IV_TECHNICAL_DOCS.md"),
162
+ "--output",
163
+ "-o",
164
+ help="Ruta del archivo Markdown donde se guardará el expediente.",
165
+ ),
166
+ rules_dir: Optional[Path] = typer.Option(
167
+ None,
168
+ "--rules-dir",
169
+ help="Directorio personalizado de reglas YAML.",
170
+ ),
171
+ ) -> None:
172
+ """Genera el Dossier de Documentación Técnica formal exigido por el Anexo IV del EU AI Act."""
173
+ rules_path = rules_dir or get_default_rules_dir()
174
+
175
+ try:
176
+ catalog = load_rules_from_dir(rules_path)
177
+ except RuleLoadError as err:
178
+ console.print(f"[bold red]Error al cargar catálogo de reglas:[/bold red] {err}")
179
+ raise typer.Exit(code=2)
180
+
181
+ engine = ScanEngine(catalog=catalog)
182
+
183
+ with console.status("[bold cyan]Analizando arquitectura y generando expediente Anexo IV...[/bold cyan]"):
184
+ report = engine.scan_path(path)
185
+ generator = AnnexIVGenerator(report, system_name=system_name, version=system_version)
186
+ dossier_md = generator.generate_markdown_dossier()
187
+ output.write_text(dossier_md, encoding="utf-8")
188
+
189
+ console.print(f"[bold green][OK] Expediente Anexo IV generado con éxito:[/bold green] {output.resolve()}")
190
+
191
+ if __name__ == "__main__":
192
+ app()
aicomply/config.py ADDED
@@ -0,0 +1,49 @@
1
+ """
2
+ AIComply - Project Configuration Loader (.aicomply.yaml)
3
+ Permite definir exclusiones de reglas, rutas ignoradas y umbrales de fallo en CI/CD.
4
+ """
5
+ from pathlib import Path
6
+ from typing import List, Optional, Set
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+ import yaml
9
+
10
+ class AIComplyConfig(BaseModel):
11
+ """Esquema de configuración de AIComply por repositorio"""
12
+ model_config = ConfigDict(frozen=True, extra="forbid")
13
+
14
+ exclude_paths: List[str] = Field(
15
+ default_factory=lambda: ["tests/**", "fixtures/**", "docs/**"],
16
+ description="Rutas o patrones glob a ignorar durante el escaneo."
17
+ )
18
+ ignore_rules: List[str] = Field(
19
+ default_factory=list,
20
+ description="IDs de reglas desactivadas globalmente (ej. ['EUAIA-ART15-001'])."
21
+ )
22
+ enforce_risk_tier: Optional[str] = Field(
23
+ default=None,
24
+ description="Nivel de riesgo máximo tolerado antes de fallar (ej. 'high_risk')."
25
+ )
26
+ custom_rules_dir: Optional[str] = Field(
27
+ default=None,
28
+ description="Ruta relativa a reglas personalizadas adicionales"
29
+ )
30
+
31
+ def load_project_config(target_dir: Path) -> AIComplyConfig:
32
+ """Busca y carga un archivo .aicomply.yaml o aicomply.yml en el directorio objetivo."""
33
+ candidate_files = [
34
+ target_dir / ".aicomply.yaml",
35
+ target_dir / ".aicomply.yml",
36
+ target_dir / "aicomply.yaml",
37
+ ]
38
+
39
+ for config_path in candidate_files:
40
+ if config_path.is_file():
41
+ try:
42
+ with open(config_path, "r", encoding="UTF-8") as f:
43
+ raw_data = yaml.safe_load(f) or {}
44
+ return AIComplyConfig.model_validate(raw_data)
45
+ except Exception:
46
+ # Si el archivo está corrupto, continuar con la configuración por efecto
47
+ return AIComplyConfig()
48
+
49
+ return AIComplyConfig()
File without changes
@@ -0,0 +1,47 @@
1
+ """
2
+ AIComply - Cryptographic Evidence & Hasher
3
+ Genera identificadores SHA-256 deterministas para hallazgos individuales
4
+ y para el conjunto del reporte de auditoría.
5
+ """
6
+
7
+ import hashlib
8
+ import json
9
+ from typing import Any, Dict, List
10
+ from aicomply.schemas import CodeLocation, Finding
11
+
12
+
13
+ def compute_finding_hash(
14
+ rule_id: str,
15
+ location: CodeLocation,
16
+ target: str,
17
+ snippet: str = ""
18
+ ) -> str:
19
+ """
20
+ Calcula un hash SHA-256 determinista para un hallazgo específico.
21
+ Normaliza rutas relativas y elimina variaciones de espacios en blanco.
22
+ """
23
+ normalized_snippet = snippet.replace("\r\n", "\n").replace("\r", "\n").strip()
24
+ canonical_payload = {
25
+ "rule_id": rule_id.strip().upper(),
26
+ "file_path": location.file_path.replace("\\", "/").strip(),
27
+ "start_line": location.start_line,
28
+ "end_line": location.end_line,
29
+ "target": target.strip(),
30
+ "snippet_normalized": normalized_snippet,
31
+ }
32
+
33
+ encoded = json.dumps(canonical_payload, sort_keys=True).encode("utf-8")
34
+ return hashlib.sha256(encoded).hexdigest()
35
+
36
+
37
+ def compute_scan_hash(findings: List[Finding]) -> str:
38
+ """
39
+ Calcula un hash SHA-256 consolidado para la ejecución completa del escaneo.
40
+ Se ordenan los IDs de los hallazgos para asegurar determinismo.
41
+ """
42
+ if not findings:
43
+ return hashlib.sha256(b"AIComply_EMPTY_CLEAN_SCAN").hexdigest()
44
+
45
+ sorted_hashes = sorted([f.id for f in findings])
46
+ consolidated_payload = "|".join(sorted_hashes).encode("utf-8")
47
+ return hashlib.sha256(consolidated_payload).hexdigest()
File without changes