TLDR
- Le reti di siti di propaganda si riconoscono dalle tracce tecniche che lasciano: contenuti copiati, link incrociati, account pubblicitari, certificati e infrastruttura condivisa.
- NarrativeScope è un tool open source in Python che raccoglie questi segnali da GDELT, dalle pagine, dai file ads.txt e dai log di Certificate Transparency, e li disegna in un grafo interattivo.
- Ogni collegamento porta con sé l'evidenza e un livello di forza: un account AdSense DIRECT condiviso pesa più di un IP condiviso, che dietro Cloudflare è la norma.
- Si può partire da un elenco già documentato, per esempio i domini Portal Kombat di VIGINUM, e cercare cosa li lega ad altri siti.
- Il downloader blocca SSRF e DNS rebinding risolvendo il nome una sola volta, dentro la connessione.
Perché conta ora
Le reti di propaganda non hanno più bisogno di lettori. Pubblicano migliaia di articoli al giorno su siti che quasi nessuno visita, perché il bersaglio è altrove: motori di ricerca, aggregatori e chatbot che pescano dal web. Ne abbiamo parlato in LLM grooming: milioni di articoli di propaganda per far parlare i chatbot come il Cremlino.
Chi gestisce cento siti, però, lascia tracce. Copia gli stessi testi, si linka da solo, incassa la pubblicità sullo stesso account, chiede certificati TLS per più domini insieme. Sono tracce tecniche, pubbliche e verificabili: con un po' di Python si possono raccogliere e mettere su una mappa.
NarrativeScope fa esattamente questo. È un tool OSINT open source che raccoglie questi segnali e li disegna in un grafo interattivo, dove ogni collegamento dice cosa è stato osservato e quanto pesa.
Il metodo: come VIGINUM ha smontato Portal Kombat
Il caso di scuola è Portal Kombat, la rete documentata nel febbraio 2024 da VIGINUM, il servizio francese contro le ingerenze digitali straniere. Almeno 193 "portali di informazione" in tre ecosistemi: i siti pravda rivolti all'Occidente, i siti -news.ru per i russofoni in Ucraina e un gruppo di siti locali "storici".
VIGINUM li ha collegati sommando segnali diversi:
- infrastruttura: i siti pravda condividevano un IP su un server in Russia; i -news.ru stavano nello stesso intervallo di indirizzi e nello stesso sistema autonomo;
- template: struttura HTML, rubriche, grafica e favicon identici;
- contenuti: ripubblicazione automatica da canali Telegram, agenzie russe e siti istituzionali, con errori tipici della traduzione automatica;
- ritmo: oltre 150.000 articoli dai cinque siti pravda in meno di tre mesi, con picchi di più di 1.700 al giorno su un solo sito.
Nel 2025 il Servizio europeo per l'azione esterna (EEAS) conta circa 200 testate in 35 lingue, alimentate da ripubblicazione automatica ad alta frequenza. VIGINUM pubblica anche gli indicatori tecnici su GitHub.
La lezione per chi scrive codice è chiara: nessun segnale da solo basta, la forza sta nella combinazione. NarrativeScope è costruito attorno a questa idea.
Come funziona NarrativeScope
Il flusso ha quattro fasi.
1. Sorgenti. Tre ingressi, anche combinati:
- una ricerca su GDELT DOC 2.0, l'API gratuita che indicizza le notizie online di tutto il mondo, con finestra mobile (
--timespan 7d) o intervallo di date (--since 2024-02-01 --until 2024-02-29); - un file con un URL per riga;
- un CSV di domini già documentati, con fonte, link e data: per esempio l'elenco di un rapporto VIGINUM.
2. Raccolta. Le pagine vengono scaricate con un client HTTP blindato (ci torniamo sotto). Da ogni pagina si estraggono testo e link in uscita.
3. Segnali. Ogni tipo di indizio diventa un tipo di arco nel grafo.
4. Grafo ed export. NetworkX costruisce il grafo, PyVis lo rende in HTML con menu di filtro; in parallelo escono un CSV con un arco per riga e un JSON importabile in Gephi.
I segnali, uno per uno
| Arco | Fonte | Forza | Cosa indica |
|---|---|---|---|
documented_in |
CSV importato | fact | il dominio è elencato da una fonte pubblica, con data |
links_to |
pagine | moderate | una pagina cita esplicitamente un altro sito del corpus |
similar_text |
pagine | moderate o weak | testi quasi identici (ripubblicazione o fonte comune) |
ads_direct |
ads.txt | moderate o weak | stesso account pubblicitario DIRECT su più domini |
ads_owner |
ads.txt | moderate | stesso proprietario dichiarato (OWNERDOMAIN) |
shared_certificate |
crt.sh | moderate o weak | un unico certificato TLS copre più domini, con date |
resolves_to |
DNS | weak | IP corrente, con data dell'osservazione |
Testi copiati: TF-IDF e similarità coseno
Le reti di ripubblicazione lasciano testi identici o quasi su domini diversi. NarrativeScope trasforma ogni articolo in un vettore TF-IDF su parole e coppie di parole, poi calcola la similarità coseno tra tutte le coppie di articoli di domini diversi:
vectorizer = TfidfVectorizer(ngram_range=(1, 2), sublinear_tf=True)
matrix = vectorizer.fit_transform(testi)
sims = cosine_similarity(matrix)
Sopra 0,80 l'arco è moderate: è quasi certamente lo stesso testo. Tra la soglia (default 0,60) e 0,80 è weak: riscrittura o fonte comune, per esempio lo stesso lancio d'agenzia. Il confronto è lessicale, quindi lavora tra articoli nella stessa lingua: per una rete multilingue conviene lanciare una ricerca per lingua.
ads.txt: chi incassa la pubblicità
Ogni sito che vende pubblicità programmatica pubblica /ads.txt, l'elenco dei venditori autorizzati. Una riga tipica è:
google.com, pub-1234567890123456, DIRECT, f08c47fec0942fa0
DIRECT significa che l'account appartiene all'editore stesso. Se lo stesso pub- compare come DIRECT su dieci domini, quei dieci domini incassano sullo stesso conto: è uno dei legami più concreti che si possano trovare senza accesso privilegiato. Con ads.txt 1.1 c'è anche OWNERDOMAIN=, il dominio del proprietario dichiarato dall'editore.
C'è però una trappola, e l'abbiamo trovata nei dati veri. Nel nostro test, NarrativeScope ha trovato 12 account DIRECT condivisi tra testate del tutto legittime come Columbia Journalism Review, Mirage News e CityWatch LA. Erano tutti account di piattaforme video e pubblicitarie (vi.ai, FreeWheel, Amazon Publisher Services, Rubicon): righe che la concessionaria fa copiare identiche a tutti i suoi clienti, marcate DIRECT anche se l'account non è dell'editore.
Per questo NarrativeScope dà peso moderate solo agli ID editore Google (pub-…), che identificano l'account AdSense o Ad Manager del singolo editore, e tratta gli altri DIRECT come segnali deboli. I record RESELLER vengono ignorati del tutto, e così le pagine HTML che molti siti servono al posto di un ads.txt mancante.
Certificate Transparency: la memoria dei certificati
Ogni certificato TLS pubblico finisce nei log di Certificate Transparency, consultabili su crt.sh, con date di validità e tutti i nomi coperti (SAN). Se un certificato copre sito-a.com e sito-b.com, qualcuno li gestiva insieme in quel periodo.
È uno dei pochi segnali storici gratuiti: anche se oggi i siti stanno su server diversi, il certificato emesso due anni fa resta nei log. NarrativeScope interroga crt.sh per ogni dominio, unisce precertificato e certificato finale e crea un nodo per ogni certificato condiviso, con emittente e date. I certificati con più di 25 nomi sono marcati come deboli: sono tipicamente certificati condivisi delle CDN.
DNS: utile, ma va datato
L'IP è il segnale più facile da raccogliere e il più fragile. Nel nostro test sui cinque siti pravda documentati da VIGINUM, oggi tutti rispondono da indirizzi Cloudflare, e pravda-fr.com e pravda-es.com condividono perfino gli stessi IP. Nel 2023, secondo VIGINUM, stavano su un unico server in Russia.
Lo stesso IP Cloudflare lo condividono migliaia di siti estranei. Per questo l'arco DNS è sempre weak e porta con sé la data dell'osservazione: diventa interessante solo quando si somma ad altro, o quando l'IP non appartiene a una CDN.
Il test: cinque siti pravda e una ricerca GDELT
Per vedere lo strumento su un caso vero abbiamo combinato una ricerca GDELT su "Portal Kombat" negli ultimi tre mesi con i cinque siti pravda elencati da VIGINUM, attivando ads.txt e certificati. Ecco cosa è emerso.
La cronologia dai certificati coincide con il rapporto. Il primo certificato registrato nei log CT per pravda-de.com è del 22 febbraio 2023; per pravda-fr.com, pravda-pl.com e pravda-en.com del 24 giugno 2023, tutti emessi nell'arco di poco più di un minuto. VIGINUM scrive che i siti pravda sono stati registrati nel giugno 2023, con pravda-de registrato a febbraio e attivato a giugno. Senza accedere ad alcun dato riservato, i log pubblici ricostruiscono la stessa linea del tempo.
L'infrastruttura è cambiata. Oggi i cinque siti rispondono da IP Cloudflare, e pravda-fr.com e pravda-es.com condividono gli stessi indirizzi. Nel 2023 stavano su un unico server in Russia. Nessun certificato attuale è condiviso tra loro: dietro Cloudflare ogni dominio ha il suo.
Niente pubblicità. Nessuno dei cinque siti pubblica un ads.txt. Coerente con una rete che non vive di traffico ma di volume e indicizzazione.
Testi gemelli tra fonti legittime. Tra gli articoli trovati da GDELT, una pagina di GlobalSecurity.org e una di Mirage News hanno similarità 0,95: entrambe riprendono la dichiarazione del G7 Rapid Response Mechanism sulle operazioni informative seguite alla violazione dello spazio aereo polacco. È l'esempio da manuale di "fonte comune": due siti indipendenti che riprendono lo stesso comunicato ufficiale.
Provarlo
Serve Python 3.9 o superiore.
git clone https://github.com/bithub-it/narrative-scope
cd narrative-scope
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
Notizie recenti su un tema, con pagine scaricate e confrontate:
python -m narrativescope --query '"Portal Kombat"' --timespan 3m --output output
Partire dai domini documentati e cercare legami infrastrutturali:
python -m narrativescope --known examples/portal-kombat.csv --ads --certs --output output
Il file d'esempio contiene i cinque siti pravda del rapporto VIGINUM, nel formato:
domain,source,url,date,note
pravda-fr.com,VIGINUM Portal Kombat,https://www.sgdsn.gouv.fr/...,2024-02-12,ecosistema pravda
Le due modalità si combinano, e --min-strength moderate toglie dal grafo i segnali deboli:
python -m narrativescope --query '"Portal Kombat"' --known examples/portal-kombat.csv \
--ads --certs --min-strength moderate --output output
Il risultato:
output/graph.html: il grafo. Domini ciano, articoli viola, IP grigi, account pubblicitari gialli, certificati verdi, fonti rosa; gli archi deboli sono tratteggiati e passando il mouse si legge l'evidenza. Il menu in alto filtra per tipo di nodo e di arco.output/edges.csv: un arco per riga con tipo, forza, data ed evidenza, comodo da ordinare e verificare in un foglio di calcolo.output/graph.json: il grafo completo, importabile in Gephi o in un notebook.
Sotto il cofano: niente SSRF, niente DNS rebinding
Un programma che scarica URL presi da GDELT o dai link delle pagine è un bersaglio classico di SSRF: basta un link a http://169.254.169.254/ o a http://192.168.1.1/ per trasformarlo in una sonda verso la rete interna.
Il controllo ingenuo è risolvere il nome, verificare che l'IP sia pubblico, poi passare l'URL a requests. Ma requests risolve il nome una seconda volta, e un DNS ostile con TTL zero può rispondere con un IP pubblico al controllo e con 127.0.0.1 alla connessione. È il DNS rebinding, e il cheat sheet OWASP sulla prevenzione SSRF lo cita esplicitamente.
NarrativeScope chiude la finestra spostando la risoluzione dentro la connessione. Una sottoclasse delle connessioni di urllib3 risolve il nome, valida ogni indirizzo e apre il socket verso l'IP appena validato:
class _PinnedMixin:
def _new_conn(self):
host = self._dns_host
ips = resolve_public(host, self.port) # risolve e rifiuta IP non pubblici
for ip in ips:
self._dns_host = ip # il socket va a questo IP, e solo a questo
try:
return super()._new_conn()
except Exception as exc:
last_exc = exc
finally:
self._dns_host = host
raise last_exc
Il nome originale resta in self.host, quindi SNI, verifica del certificato e intestazione Host funzionano come sempre. La classe viene montata in una requests.Session tramite un HTTPAdapter personalizzato, con trust_env = False per ignorare i proxy di sistema, che altrimenti farebbero la risoluzione al posto nostro.
Il resto del perimetro:
- solo
httpehttpssu porte 80 e 443, niente credenziali nell'URL; - rifiutati indirizzi privati, loopback, link-local, CGNAT, riservati, multicast e IPv6 che mappano IPv4 interni;
- redirect seguiti a mano, massimo tre, ognuno ricontrollato;
- limite di dimensione, solo contenuti testuali, ritardo tra richieste allo stesso host,
robots.txtrispettato.
La suite di test simula proprio l'attacco: un DNS finto risponde prima con un IP pubblico e poi con 127.0.0.1, e il test verifica che nessuna richiesta arrivi al server locale.
Leggere il grafo
Un grafo fitto colpisce, ma il valore sta nei singoli archi. Qualche regola pratica:
- Somma segnali indipendenti. Un account DIRECT condiviso più un certificato condiviso più testi identici è un cluster solido. Tre IP Cloudflare in comune non sono niente.
- Guarda le date. Certificati e osservazioni DNS hanno una data: un legame del 2023 racconta il 2023.
- Cerca la fonte comune. Cento archi di similarità verso lo stesso testo possono essere un lancio d'agenzia ripreso da cento testate.
- Parti da ciò che è documentato. Importare un elenco con fonte e data permette di distinguere sulla mappa ciò che è già attribuito da ciò che emerge dall'analisi.
- Usa i filtri.
--min-strength moderatee il menu digraph.htmltolgono il rumore e lasciano i collegamenti che contano.
Codice e approfondimenti
NarrativeScope è open source con licenza MIT: bithub-it/narrative-scope su GitHub. Se preferisci un archivio da scaricare al volo, c'è lo zip della versione 0.2.0.
Il lato motori di ricerca, cioè come queste reti sfruttano la SEO per farsi trovare, è raccontato su Bithub: Propaganda, SEO e motori di ricerca: come riconoscere le reti che amplificano le informazioni.
Nel repository trovi il codice, il README con tutte le opzioni, l'elenco d'esempio e la suite di test (python -m pytest -q).
FAQ
Che cos'è NarrativeScope?
È un tool OSINT open source in Python, con licenza MIT, che mappa le relazioni tra siti e articoli: link, testi ripubblicati, DNS, account pubblicitari in ads.txt, certificati TLS dai log di Certificate Transparency ed elenchi di domini documentati. Produce un grafo HTML interattivo, un CSV e un JSON.
Perché un IP condiviso conta poco?
Perché CDN e hosting condivisi servono migliaia di siti indipendenti dagli stessi indirizzi. I siti pravda di Portal Kombat nel 2023 stavano su un unico server in Russia; oggi rispondono da IP Cloudflare condivisi con chiunque. L'IP va letto con la data e insieme ad altri segnali.
Cosa rivela un file ads.txt?
Elenca chi può vendere la pubblicità di un sito. Un record DIRECT con lo stesso ID editore su più domini indica che gli incassi finiscono sullo stesso account. Il campo OWNERDOMAIN, introdotto con ads.txt 1.1, dichiara il dominio del proprietario del sito.
A cosa servono i log di Certificate Transparency?
Ogni certificato TLS pubblico viene registrato nei log CT, consultabili su crt.sh, con date di validità e nomi coperti. Se un certificato copre più domini sospetti, qualcuno li ha gestiti insieme in quel periodo. È uno dei pochi segnali storici disponibili gratuitamente.
Che cos'è GDELT DOC 2.0?
È un'API pubblica gratuita che cerca nelle notizie online di tutto il mondo e restituisce elenchi di articoli con URL, titolo, dominio, lingua e data. Copre una finestra mobile di tre mesi e si interroga con parole chiave, frasi esatte e intervalli di date.
Come si difende NarrativeScope dal DNS rebinding?
Risolve il nome host una sola volta, dentro la connessione: valida ogni IP restituito e apre il socket verso quell'indirizzo, mantenendo il nome originale per SNI e certificato. Non c'è una seconda risoluzione che un DNS ostile possa sfruttare.
Fonti
- NarrativeScope - repository GitHub
- Bithub - Propaganda, SEO e motori di ricerca: come riconoscere le reti che amplificano le informazioni
- SGDSN / VIGINUM - Portal Kombat: un réseau structuré et coordonné de propagande prorusse (febbraio 2024)
- SGDSN / VIGINUM - Portal Kombat: suite des investigations
- VIGINUM - Rapports techniques e indicatori pubblici (GitHub)
- EEAS - 3rd Report on Foreign Information Manipulation and Interference Threats (marzo 2025)
- GDELT Project - GDELT DOC 2.0 API Debuts
- IAB Tech Lab - ads.txt specification
- Google AdSense - ads.txt
- crt.sh - Certificate Transparency search
- Cloudflare - Cloudflare IP addresses
- OWASP - Server-Side Request Forgery Prevention Cheat Sheet



