← Files VeraARCHIVED FILE

modules/bilancio-xbrl-it/scripts/external_validation.py

3.29 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

#!/usr/bin/env python3
"""Secure intake and comparison of a user-controlled TEBENI result.

This module never sends an XBRL instance to an external service. It records a
report that the user chose to upload after the manual TEBENI step, then compares
declared external rule identifiers with the last local validation result.
"""

from __future__ import annotations

import hashlib
from pathlib import Path
from typing import Any, Mapping, Sequence

__all__ = ["record_external_validation_result"]

ALLOWED_SUFFIXES = {".html", ".htm", ".json", ".pdf", ".txt", ".xml"}
MAX_REPORT_BYTES = 20 * 1024 * 1024
RESULTS = {"PASS", "FAIL", "WARNING"}


def _sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def record_external_validation_result(
    case: Mapping[str, Any],
    report_path: Path,
    result: str,
    reported_issues: Sequence[Mapping[str, Any]],
    actor: str,
) -> dict[str, Any]:
    """Build a checksum-bound, non-authoritative external-validation record."""

    if not (case.get("validation") or {}).get("validated_revision_id"):
        raise ValueError("Local validation is required before external report intake")
    if report_path.is_symlink() or not report_path.is_file():
        raise ValueError("External validation report must be a regular local file")
    resolved = report_path.resolve()
    if resolved.suffix.lower() not in ALLOWED_SUFFIXES:
        raise ValueError("Unsupported external validation report type")
    size = resolved.stat().st_size
    if size > MAX_REPORT_BYTES:
        raise ValueError("External validation report exceeds the size limit")
    normalized_result = str(result).upper()
    if normalized_result not in RESULTS:
        raise ValueError("External validation result must be PASS, FAIL, or WARNING")
    normalized_issues: list[dict[str, str]] = []
    seen: set[str] = set()
    for issue in reported_issues:
        rule_id = str(issue["rule_id"]).strip()
        message = str(issue["message"]).strip()
        if not rule_id or not message or rule_id in seen:
            raise ValueError(
                "External issue IDs and messages must be present and unique"
            )
        seen.add(rule_id)
        normalized_issues.append({"rule_id": rule_id, "message": message})
    local_ids = {
        str(issue["rule_id"])
        for issue in (case.get("validation") or {}).get("issues", [])
    }
    external_ids = {item["rule_id"] for item in normalized_issues}
    return {
        "provider": "TEBENI",
        "route": "USER_CONTROLLED_MANUAL_UPLOAD",
        "authoritative_for_filing": False,
        "result": normalized_result,
        "report": {
            "file_name": resolved.name,
            "media_suffix": resolved.suffix.lower(),
            "sha256": _sha256_file(resolved),
            "size_bytes": size,
        },
        "reported_issues": normalized_issues,
        "comparison": {
            "matched_rule_ids": sorted(local_ids & external_ids),
            "external_only_rule_ids": sorted(external_ids - local_ids),
            "local_only_rule_ids": sorted(local_ids - external_ids),
        },
        "validated_revision_id": case["revision_id"],
        "recorded_by": actor,
    }

SHA-256: 9ca430dbbc87883876799ae7a7f51380067d79c25de4fb03b19bc33eb38725d1