AiAnonymize_v3 / layers /rules /validators.py
Alessandro Tomassini
deploy(hf): overlay README/Dockerfile da huggingface/, senza docs/binari/model
c42a5a1
Raw
History Blame Contribute Delete
5.58 kB
"""Validatori di formato/checksum per ridurre i falsi positivi.
Ogni validatore riceve la stringa gia' matchata e ritorna True se rispetta
lo standard. Sono funzioni pure, senza stato.
"""
from __future__ import annotations
import re
_CF_ODD = {
"0": 1, "1": 0, "2": 5, "3": 7, "4": 9, "5": 13, "6": 15, "7": 17,
"8": 19, "9": 21, "A": 1, "B": 0, "C": 5, "D": 7, "E": 9, "F": 13,
"G": 15, "H": 17, "I": 19, "J": 21, "K": 2, "L": 4, "M": 18, "N": 20,
"O": 11, "P": 3, "Q": 6, "R": 8, "S": 12, "T": 14, "U": 16, "V": 10,
"W": 22, "X": 25, "Y": 24, "Z": 23,
}
_CF_EVEN = {
"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8,
"9": 9, "A": 0, "B": 1, "C": 2, "D": 3, "E": 4, "F": 5, "G": 6, "H": 7,
"I": 8, "J": 9, "K": 10, "L": 11, "M": 12, "N": 13, "O": 14, "P": 15,
"Q": 16, "R": 17, "S": 18, "T": 19, "U": 20, "V": 21, "W": 22, "X": 23,
"Y": 24, "Z": 25,
}
_CF_REMAINDER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# Codici paese ISO 3166-1 alpha-2: nel BIC/SWIFT occupano i caratteri 5-6 e
# sono il discriminante piu' forte contro i falsi positivi (parole maiuscole
# di 8 lettere come "SPECIALE" hanno "IA" in quella posizione, inesistente).
_ISO_3166_ALPHA2 = frozenset(
"AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI "
"BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN "
"CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK "
"FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM "
"HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN "
"KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK "
"ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP "
"NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW "
"SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF "
"TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI "
"VN VU WF WS YE YT ZA ZM ZW".split()
)
def validate_codice_fiscale(value: str) -> bool:
"""Valida il codice fiscale persona fisica (16 caratteri, char di controllo)."""
cf = value.strip().upper()
if not re.fullmatch(r"[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]", cf):
return False
total = 0
for i, ch in enumerate(cf[:15]):
total += _CF_ODD[ch] if (i % 2 == 0) else _CF_EVEN[ch]
return _CF_REMAINDER[total % 26] == cf[15]
def _luhn_check(digits: str) -> bool:
total = 0
reverse = digits[::-1]
for i, ch in enumerate(reverse):
d = int(ch)
if i % 2 == 1:
d *= 2
if d > 9:
d -= 9
total += d
return total % 10 == 0
def validate_partita_iva(value: str) -> bool:
"""Valida la partita IVA italiana (11 cifre, checksum di Luhn pari)."""
piva = re.sub(r"\D", "", value)
if len(piva) != 11:
return False
return _luhn_check(piva)
def validate_carta_credito(value: str) -> bool:
"""Valida una carta di credito tramite Luhn (13-19 cifre)."""
digits = re.sub(r"\D", "", value)
if not 13 <= len(digits) <= 19:
return False
return _luhn_check(digits)
def validate_iban(value: str) -> bool:
"""Valida IBAN tramite resto mod-97 (ISO 13616)."""
iban = re.sub(r"\s", "", value).upper()
if not re.fullmatch(r"[A-Z]{2}\d{2}[A-Z0-9]{11,30}", iban):
return False
rearranged = iban[4:] + iban[:4]
converted = "".join(
str(ord(ch) - 55) if ch.isalpha() else ch for ch in rearranged
)
try:
return int(converted) % 97 == 1
except ValueError:
return False
def validate_cig(value: str) -> bool:
"""CIG: 10 caratteri alfanumerici (validazione di formato)."""
return bool(re.fullmatch(r"[0-9A-Za-z]{10}", value.strip()))
def validate_cup(value: str) -> bool:
"""CUP: 15 caratteri alfanumerici (validazione di formato)."""
return bool(re.fullmatch(r"[0-9A-Za-z]{15}", value.strip()))
def validate_cap(value: str) -> bool:
return bool(re.fullmatch(r"\d{5}", value.strip()))
def validate_bic(value: str) -> bool:
"""Valida la struttura di un BIC/SWIFT (ISO 9362).
Formato: 4 lettere (banca) + 2 lettere (paese ISO 3166-1) + 2 alfanumerici
(localita') + 3 alfanumerici opzionali (filiale). Il vincolo del codice
paese ISO scarta i falsi positivi su parole maiuscole di 8 lettere
(es. "SPECIALE", "ORDINARI"), il cui codice paese non esiste.
"""
v = value.strip().upper()
if len(v) not in (8, 11):
return False
if not v[:6].isalpha():
return False
if v[4:6] not in _ISO_3166_ALPHA2:
return False
return v[6:].isalnum()
def always_valid(value: str) -> bool: # noqa: ARG001
return True
def validate_codice_fiscale_any(value: str) -> bool:
"""CF persona (16 char, checksum) OPPURE CF azienda (11 cifre = P.IVA, Luhn)."""
v = value.strip()
if re.fullmatch(r"\d{11}", v):
return validate_partita_iva(v)
return validate_codice_fiscale(v)
# Validatori applicati come FILTRO sui rilevamenti finali (qualunque layer li
# abbia prodotti, incluso il NER che non controlla i checksum): uno span di
# questi tipi con checksum/formato non valido viene scartato. Riduce i falsi
# positivi sui codici a formato verificabile.
FORMAT_VALIDATORS: dict = {
"CODICE_FISCALE": validate_codice_fiscale_any,
"PARTITA_IVA": validate_partita_iva,
"IBAN": validate_iban,
"CARTA_CREDITO": validate_carta_credito,
"BIC": validate_bic,
}