Verificare che una cartella non sia cambiata: un manifest di impronte in Python
Centoventiquattro righe di libreria standard per rispondere a una domanda a cui la data di ultima modifica non risponde: questi file sono ancora quelli di ieri?
«Questa cartella è ancora come l'avevo lasciata?» È una domanda che torna più spesso di quanto sembri: la copia di documenti acquisita per una perizia, il backup di cui non si è più sicuri, i file di un sito prima di caricarli via FTP, l'archivio delle fatture di un anno chiuso che non dovrebbe più muoversi.
La risposta che offre il sistema operativo — la data di ultima modifica — non è una risposta. Quella data è un attributo scrivibile: si riporta indietro con una riga di codice, e cambia anche quando il contenuto resta identico, per esempio dopo una copia fatta con lo strumento sbagliato. Nemmeno la dimensione basta: dentro una fattura, trasformare «1.250,00» in «1.950,00» non sposta un byte.
L'unico controllo che tiene è il contenuto: si leggono tutti i byte di ogni file e si riducono a un numero breve, l'impronta. Le impronte si registrano in un elenco, il manifest, e quando serve si ricalcolano e si confrontano. Qui sotto c'è lo script che lo fa, con la sola libreria standard di Python — senza installare nulla oltre a Python stesso.
Che cos'è un'impronta
Una funzione di hash crittografica prende una sequenza di byte di qualunque lunghezza e restituisce sempre lo stesso numero di bit: SHA-256 ne restituisce 256, cioè 64 cifre esadecimali. Tre proprietà la rendono utile qui: è deterministica, perciò lo stesso contenuto dà la stessa impronta su qualunque macchina; cambiare un solo bit produce un'impronta completamente diversa, non «simile»; e trovare due contenuti diversi con la stessa impronta — una collisione — non è alla portata di nessuno, allo stato delle conoscenze. SHA-256 è specificata dal NIST nel FIPS 180-4.
MD5 e SHA-1 stanno ancora nella libreria standard e per accorgersi di un errore di trasmissione o di un settore di disco andato male vanno benissimo. Per entrambe, però, sono state dimostrate collisioni costruite deliberatamente: se chi potrebbe aver toccato i file è una persona e non un disco che si guasta, non sono la scelta giusta. SHA-256 ha anche il vantaggio che chiunque la ricontrolla con gli strumenti che ha già: sha256sum su Linux e macOS, certutil -hashfile nomefile SHA256 su Windows.
Lo script
#!/usr/bin/env python3
"""impronte.py - crea e verifica un manifest di impronte SHA-256 di una cartella.
Uso:
python impronte.py crea CARTELLA
python impronte.py verifica CARTELLA
Codici di uscita: 0 nessuna differenza, 1 differenze trovate, 2 errore.
Solo libreria standard. Richiede Python 3.11 o successivo.
"""
import argparse
import hashlib
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
ALGORITMO = "sha256"
NOME_MANIFEST = "manifest-impronte.json"
def impronta(percorso: Path) -> str:
"""Impronta esadecimale del contenuto del file, letto a blocchi."""
with percorso.open("rb") as f:
return hashlib.file_digest(f, ALGORITMO).hexdigest()
def scansiona(radice: Path) -> tuple[dict, list]:
"""Percorre la cartella e restituisce {percorso relativo: {byte, impronta}}."""
voci, problemi = {}, []
for percorso in sorted(radice.rglob("*")):
if percorso.is_symlink() or not percorso.is_file():
continue
relativo = percorso.relative_to(radice).as_posix()
if relativo == NOME_MANIFEST:
continue
try:
voci[relativo] = {
"byte": percorso.stat().st_size,
ALGORITMO: impronta(percorso),
}
except OSError as errore:
problemi.append(f"{relativo}: {errore.strerror}")
return voci, problemi
def crea(radice: Path) -> int:
voci, problemi = scansiona(radice)
manifest = {
"creato": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"algoritmo": ALGORITMO,
"file": voci,
}
destinazione = radice / NOME_MANIFEST
destinazione.write_text(
json.dumps(manifest, indent=2, ensure_ascii=False, sort_keys=True) + "\n",
encoding="utf-8",
)
for problema in problemi:
print(f"non leggibile: {problema}", file=sys.stderr)
print(f"{len(voci)} file registrati in {destinazione}")
print(f"impronta del manifest: {impronta(destinazione)}")
return 2 if problemi else 0
def verifica(radice: Path) -> int:
origine = radice / NOME_MANIFEST
if not origine.is_file():
print(f"manifest assente: {origine}", file=sys.stderr)
return 2
manifest = json.loads(origine.read_text(encoding="utf-8"))
if manifest.get("algoritmo") != ALGORITMO:
print(
f"manifest prodotto con {manifest.get('algoritmo')!r}, "
f"questo script usa {ALGORITMO!r}",
file=sys.stderr,
)
return 2
attesi = manifest["file"]
attuali, problemi = scansiona(radice)
modificati = sorted(n for n in attesi if n in attuali and attuali[n] != attesi[n])
mancanti = sorted(n for n in attesi if n not in attuali)
aggiunti = sorted(n for n in attuali if n not in attesi)
for nome in modificati:
print(f"MODIFICATO {nome}")
print(f" atteso {attesi[nome][ALGORITMO]} {attesi[nome]['byte']} byte")
print(f" trovato {attuali[nome][ALGORITMO]} {attuali[nome]['byte']} byte")
for nome in mancanti:
print(f"MANCANTE {nome}")
for nome in aggiunti:
print(f"AGGIUNTO {nome}")
for problema in problemi:
print(f"ILLEGGIBILE {problema}", file=sys.stderr)
print(f"manifest del {manifest['creato']}, {len(attesi)} file registrati")
if not (modificati or mancanti or aggiunti or problemi):
print("nessuna differenza")
return 0
print(
f"differenze: {len(modificati)} modificati, {len(mancanti)} mancanti, "
f"{len(aggiunti)} aggiunti, {len(problemi)} illeggibili"
)
return 1
def main() -> int:
analizzatore = argparse.ArgumentParser(description=__doc__.splitlines()[0])
analizzatore.add_argument("comando", choices=["crea", "verifica"])
analizzatore.add_argument("cartella", type=Path)
argomenti = analizzatore.parse_args()
radice = argomenti.cartella.resolve()
if not radice.is_dir():
print(f"non e' una cartella: {radice}", file=sys.stderr)
return 2
return crea(radice) if argomenti.comando == "crea" else verifica(radice)
if __name__ == "__main__":
sys.exit(main())
Le scelte che contano
La riga che fa il lavoro è hashlib.file_digest(f, "sha256"), disponibile dalla versione 3.11 di Python. Risolve un problema su cui scrivendo a mano si sbaglia facilmente: un file non va letto tutto in memoria — su un archivio da qualche gigabyte il programma muore — ma a blocchi, aggiornando l'impronta a ogni blocco. file_digest lo fa da sé, con blocchi da 262.144 byte, e la sua documentazione avverte che «may bypass Python's I/O and use the file descriptor fileno directly»: può scavalcare i livelli di input/output di Python e leggere direttamente dal descrittore del file. Su Python 3.10 o precedenti la funzione non c'è e il ciclo va scritto a mano, con lo stesso risultato: h = hashlib.sha256(), poi while blocco := f.read(1024 * 1024): h.update(blocco), poi h.hexdigest().
Accanto all'impronta registro la dimensione in byte. Non serve come controllo — il controllo è l'impronta — ma è un'informazione diagnostica che arriva gratis dalla stessa chiamata a stat() e che in fase di verifica dice subito se un file è stato troncato o riscritto per intero. Il percorso di ogni file è relativo alla cartella e normalizzato con as_posix(), cioè con le barre in avanti: così un manifest creato su Windows si verifica su Linux e viceversa, senza che le barre rovesciate facciano sembrare diverso tutto l'elenco.
Il JSON viene scritto con le chiavi ordinate e l'indentazione: due manifest della stessa cartella sono così confrontabili con un normale diff o tenuti sotto controllo di versione. I collegamenti simbolici vengono saltati, perché possono puntare fuori dalla cartella e l'impronta del bersaglio non appartiene all'insieme che si sta certificando; il manifest stesso è escluso dalla scansione, altrimenti non potrebbe contenere la propria impronta.
Le differenze sono riportate in tre categorie, perché non significano la stessa cosa: un file modificato è il caso che preoccupa, uno mancante può essere una cancellazione o uno spostamento, uno aggiunto spesso è legittimo ma va comunque visto. Un file che non si riesce a leggere non scompare in silenzio: produce una riga su standard error e uscita 2, perché un manifest incompleto che sembra completo è peggio di un errore. I codici di uscita — 0 nessuna differenza, 1 differenze, 2 errore — servono a far girare lo script in un'attività pianificata che avvisa solo quando c'è qualcosa da guardare.
Che cosa fa, davvero
Lo script è stato eseguito nel container di questa redazione, Python 3.13.15 su Linux, su una cartella di prova con tre file di testo. Dopo la creazione del manifest ho cambiato un solo carattere nella prima fattura, cancellato un allegato e aggiunto un file. Questo è l'output reale, non ricostruito:
$ python3 impronte.py crea prova
3 file registrati in /.../prova/manifest-impronte.json
impronta del manifest: 0846cba063bc940e6add726de5aa7ed56835ab22fe9d3d32afb3c14bb707e794
$ python3 impronte.py verifica prova
manifest del 2026-10-02T06:40:23+00:00, 3 file registrati
nessuna differenza
[uscita 0]
$ python3 impronte.py verifica prova # dopo le modifiche
MODIFICATO fatture/f-2026-014.txt
atteso 62933e90223403acf471a2bf4ad90898e769eae477534e0402e4485c5443a6b3 36 byte
trovato 40fbb05522ea69ee3f18fe3066d5117c82fa099542ec61b61a3b7fb891c9c315 36 byte
MANCANTE allegati/verbale.txt
AGGIUNTO allegati/nota.txt
manifest del 2026-10-02T06:40:23+00:00, 3 file registrati
differenze: 1 modificati, 1 mancanti, 1 aggiunti, 0 illeggibili
[uscita 1]
Il punto da guardare sono i 36 byte che restano 36 byte: il file modificato ha esattamente la dimensione di prima, perché «1.250,00» e «1.950,00» occupano lo stesso spazio. Qualunque controllo basato su data e dimensione avrebbe detto che quella fattura non è stata toccata. Le impronte dicono il contrario, e sono verificabili da fuori: sha256sum sullo stesso file restituisce la stessa stringa di 64 cifre.
Sui tempi, due misure sulla stessa macchina. Duemila file per 196 MiB complessivi: 0,33 secondi per creare il manifest e 0,33 per verificarlo, con i file già nella cache del sistema, e un manifest di 268 KiB. Su un solo file da 256 MiB, col tempo migliore di tre prove, SHA-256 ha fatto 0,23 secondi — circa 1.090 MiB al secondo — contro 0,49 secondi e 524 MiB al secondo di BLAKE2b. È l'opposto di quello che si aspetterebbe chi conosce i due algoritmi, perché BLAKE2b è progettato per essere più veloce in software: la ragione è che il processore di questa macchina, un Intel Xeon, ha le istruzioni SHA-NI, che accelerano SHA-256 in hardware e non toccano BLAKE2b. Su una CPU priva di quelle istruzioni la graduatoria si rovescia: prima di cambiare algoritmo per guadagnare velocità, si misura sulla propria macchina.
Dove questo script non arriva
Il limite più importante è dove sta il manifest. Tenerlo nella cartella che descrive va bene contro i guasti e gli errori, non contro una persona: chi può riscrivere i file può riscrivere anche il manifest. Per questo crea stampa l'impronta del manifest stesso: sono 64 caratteri che si trascrivono a mano in un verbale, si mandano per posta elettronica, si annotano su un registro — e se quell'impronta corrisponde, tutto il resto dell'elenco è attendibile. Se serve di più, cioè che nessuno possa ricalcolare un manifest valido pur avendo i file, la libreria standard offre il modulo hmac: calcola l'impronta con una chiave segreta, che si custodisce separatamente.
Un'impronta non è una firma digitale né una marca temporale: dice che il contenuto è identico a quello di cui è stata calcolata, non chi l'ha prodotto né che esisteva a una certa data. Se il materiale è destinato a un giudizio, l'impronta serve, ma conta altrettanto il documento che la riporta e che le dà una data: su questo conviene sentire chi segue il procedimento. Allo stesso modo, calcolare impronte sui file di una cartella non è un'acquisizione forense di un supporto: sono attività diverse, con strumenti e cautele diverse.
Un dettaglio che fa perdere tempo a chi non lo conosce, verificato sulla macchina di prova: due nomi di file visivamente identici possono essere due file distinti. «perizia-città.txt» con il carattere «à» singolo e lo stesso nome con «a» più accento combinante sono sequenze di byte diverse, e sul filesystem di prova sono convissuti come due file separati. Poiché i sistemi operativi normalizzano i nomi in modo diverso, un manifest creato su una macchina e verificato su un'altra può segnalare differenze che riguardano il nome e non il contenuto.
L'impronta del manifest va conservata fuori dalla cartella: dentro, non protegge da chi ha accesso in scrittura. Lo script dichiara che il contenuto è lo stesso, non chi l'ha creato né quando: non sostituisce una firma digitale né una marca temporale. Su materiale destinato a un procedimento, le impronte si calcolano sulla copia e si verbalizzano insieme agli estremi dell'operazione.
Cosa puoi fare
Lo script è completo: si copia in un file impronte.py e funziona con Python 3.11 o successivo, senza installare nulla. Per cominciare basta scegliere una cartella che non dovrebbe più cambiare — un anno fiscale chiuso, una consegna fatta, i file di un sito appena pubblicato — lanciare crea una volta e annotare altrove l'impronta del manifest. Dal giorno dopo, verifica risponde in pochi secondi alla domanda con cui si è aperto questo articolo.
Da lì, due estensioni che costano poco. La prima: mettere la verifica in un'attività pianificata settimanale che avvisi solo quando il codice di uscita è diverso da zero, così il silenzio significa «tutto a posto» e non «nessuno ha controllato». La seconda: usare il manifest come documento di consegna. Si allega alla cartella consegnata al cliente o al collega e si comunica a parte la sua impronta; chi riceve esegue verifica e sa, senza dover fidarsi, che gli è arrivato quello che è partito.
Fonti
Da leggere
Articoli correlati
Verificare che una cartella non sia cambiata: un manifest di impronte in Python
La data di modifica di un file si riscrive in una riga di codice, e la dimensione non cambia se dentro una fattura 1.250,00 diventa 1.950,00. L'unico controllo che tiene è il contenuto: uno script Python di sola libreria standard che calcola le impronte SHA-256 di una cartella, le registra in un file JSON e le riverifica. Scritto, eseguito e misurato.
Leggi l'articolo →Aprire un file sospetto senza sporcare il PC: Windows Sandbox configurata a mano
Windows Sandbox apre una copia pulita di Windows che alla chiusura svanisce. Serve per provare un installatore o un allegato di dubbia provenienza, ma con le impostazioni di partenza la rete e gli appunti restano collegati al PC. Come si scrive il file .wsb che chiude tutto, e cosa la sandbox non fa.
Leggi l'articolo →Misurare una tensione con l'ESP32 senza prendere un abbaglio
Leggere la tensione di una batteria tampone con un ESP32 e un partitore resistivo e' il primo esercizio di chiunque si avvicini alle schede Wi-Fi. E' anche quello in cui si sbaglia di un volt senza accorgersene. Cosa dice la documentazione Espressif, perche' il fondo scala non e' 3,3 V e come si dimensiona il partitore.
Leggi l'articolo →