From 66e110ce4cc7bb5dece3295bc09e03994380d2d6 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Sat, 14 Feb 2026 10:53:03 +0000 Subject: [PATCH] Generate comprehensive ISO 27001 compliance documentation in Word format Add python-docx dependency and a new script to generate a Word document detailing the IDS features and mapping them to ISO 27001 Annex A controls. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 2744be16-afcd-406e-ae68-fcf62f19bcc3 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/RyXWGQA --- IDS_Conformita_ISO27001.docx | Bin 0 -> 44765 bytes generate_iso27001_doc.py | 622 +++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + uv.lock | 121 ++++++- 4 files changed, 743 insertions(+), 1 deletion(-) create mode 100644 IDS_Conformita_ISO27001.docx create mode 100644 generate_iso27001_doc.py diff --git a/IDS_Conformita_ISO27001.docx b/IDS_Conformita_ISO27001.docx new file mode 100644 index 0000000000000000000000000000000000000000..f1ec3c02c7d2c21f7721ddc6738e07f3180282e9 GIT binary patch literal 44765 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�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 80 sui router MikroTik via REST API", + "Integrazione con feed di threat intelligence pubblici (Spamhaus, Talos, AWS, GCP, Microsoft Azure, Meta/Facebook, Cloudflare)", + "Dashboard web interattiva con visualizzazioni in tempo reale", + "Gestione whitelist/blacklist con supporto completo CIDR", + "Geolocalizzazione automatica degli IP rilevati", + "Sistema di pulizia automatica dei dati obsoleti", + "Monitoraggio continuo dei servizi di sistema", +] +for f in features_main: + doc.add_paragraph(f, style='List Bullet') + +# --- 3. ARCHITETTURA --- +doc.add_heading('3. Architettura del Sistema', level=1) +doc.add_paragraph( + "Il sistema adotta un'architettura a microservizi composta da tre componenti principali:" +) + +arch_table = doc.add_table(rows=5, cols=3) +arch_table.style = 'Light Grid Accent 1' +arch_table.alignment = WD_TABLE_ALIGNMENT.CENTER + +headers = ['Componente', 'Tecnologia', 'Funzione'] +for i, h in enumerate(headers): + arch_table.rows[0].cells[i].text = h + for paragraph in arch_table.rows[0].cells[i].paragraphs: + for run in paragraph.runs: + run.bold = True + +arch_data = [ + ['Frontend Web', 'React, ShadCN UI, TanStack Query', 'Dashboard di monitoraggio, gestione whitelist/blacklist, visualizzazione rilevamenti'], + ['Backend API', 'Node.js, Express, Drizzle ORM', 'API REST, gestione database PostgreSQL, coordinamento servizi'], + ['Backend ML', 'Python, FastAPI, scikit-learn, XGBoost', 'Analisi anomalie con Isolation Forest e classificatore ensemble, blocco automatico IP'], + ['Database', 'PostgreSQL', 'Persistenza dati: log di rete, rilevamenti, whitelist, blacklist, configurazione router'], +] +for i, row_data in enumerate(arch_data): + for j, cell_text in enumerate(row_data): + arch_table.rows[i+1].cells[j].text = cell_text + +doc.add_paragraph("") +doc.add_paragraph( + "La comunicazione tra i componenti avviene tramite API REST protette da autenticazione " + "mediante API Key (header X-API-Key). Il database PostgreSQL è accessibile solo dai " + "backend tramite connessione autenticata." +) + +# --- 4. FUNZIONALITÀ --- +doc.add_heading('4. Funzionalità del Sistema', level=1) + +# 4.1 +doc.add_heading('4.1 Raccolta e Analisi Log (Syslog)', level=2) +doc.add_paragraph( + "Il componente syslog_parser.py riceve i log syslog via protocollo UDP sulla porta 514 " + "da tutti i router MikroTik configurati. I log vengono analizzati, normalizzati e " + "memorizzati nel database PostgreSQL nella tabella network_logs." +) +doc.add_heading('Caratteristiche:', level=3) +syslog_features = [ + "Ricezione syslog UDP sulla porta 514 (standard RFC 5424)", + "Parsing automatico dei messaggi syslog con estrazione di IP sorgente, destinazione, porte, protocollo", + "Auto-reconnect e recovery in caso di errori di connessione al database", + "Politica di retention dei dati: 3 giorni di conservazione dei log grezzi", + "Gestione di volumi elevati: oltre 186 milioni di record processati", +] +for f in syslog_features: + doc.add_paragraph(f, style='List Bullet') + +# 4.2 +doc.add_heading('4.2 Rilevamento Anomalie con Machine Learning', level=2) +doc.add_paragraph( + "Il cuore del sistema è il motore di Machine Learning che utilizza un approccio ibrido " + "per il rilevamento delle anomalie, combinando due algoritmi complementari per ridurre " + "i falsi positivi e migliorare l'accuratezza del rilevamento." +) +doc.add_heading('Algoritmi Utilizzati:', level=3) +ml_features = [ + "Extended Isolation Forest (EIF): algoritmo non supervisionato per il rilevamento di anomalie basato sull'isolamento dei punti dati anomali. Analizza 25 feature di rete estratte dai log.", + "Classificatore Ensemble con Voting Pesato: combina XGBoost e altri classificatori per una classificazione più precisa delle anomalie rilevate.", + "Risk Score (0-100): ogni IP riceve un punteggio di rischio su scala 0-100 distribuito su 5 livelli (Normale <40, Basso 40-59, Medio 60-69, Alto 70-84, Critico >=85).", + "Retraining automatico settimanale del modello ML per adattarsi ai nuovi pattern di traffico.", + "Analisi ogni 2 minuti dei log dell'ultima ora per rilevamento near-real-time.", +] +for f in ml_features: + doc.add_paragraph(f, style='List Bullet') + +doc.add_heading('Feature di Rete Analizzate (25 feature):', level=3) +doc.add_paragraph( + "Il modello analizza feature quali: frequenza delle connessioni per IP, distribuzione delle porte " + "di destinazione, rapporto pacchetti in/out, diversità dei protocolli, pattern temporali, " + "entropia delle connessioni, velocità di scansione porte, distribuzione geografica delle " + "connessioni, e altre metriche statistiche derivate dal traffico di rete." +) + +# 4.3 +doc.add_heading('4.3 Blocco Automatico degli IP Malevoli', level=2) +doc.add_paragraph( + "Gli IP identificati come critici (risk score >= 80) vengono automaticamente bloccati " + "su tutti i router MikroTik configurati tramite la REST API di RouterOS." +) +block_features = [ + "Blocco automatico ogni 2 minuti per IP con risk score >= 80", + "Comunicazione con router MikroTik tramite REST API (HTTP/HTTPS)", + "Blocco parallelo su tutti i router abilitati contemporaneamente", + "Verifica whitelist prima del blocco (gli IP in whitelist non vengono mai bloccati)", + "Blocco massivo retroattivo: endpoint /block-all-critical per bloccare tutti gli IP critici storici non ancora bloccati", + "Sblocco manuale disponibile dalla dashboard web (pulsante 'Sblocca Router')", + "Auto-sblocco quando un IP viene aggiunto alla whitelist", + "Timeout configurabile per le regole di blocco sul router", + "Tracciamento dello stato di blocco nel database (campo blocked e blocked_at)", +] +for f in block_features: + doc.add_paragraph(f, style='List Bullet') + +# 4.4 +doc.add_heading('4.4 Gestione Whitelist e Blacklist', level=2) +doc.add_paragraph( + "Il sistema gestisce whitelist e blacklist con supporto completo per singoli IP e " + "range CIDR, utilizzando i tipi nativi INET/CIDR di PostgreSQL per un matching efficiente." +) +wl_features = [ + "Whitelist manuale: aggiunta/rimozione IP dalla dashboard web con motivo e note", + "Blacklist automatica: alimentata da feed di threat intelligence pubblici", + "Supporto completo CIDR: matching di range di rete (es. 192.168.0.0/16) con operatori PostgreSQL <<=", + "Logica di priorità: Whitelist manuale > Whitelist pubblica > Blacklist", + "Auto-sblocco dai router quando un IP viene aggiunto alla whitelist", + "Paginazione server-side (50 record/pagina) e ricerca con debounce per performance", + "Campo source per tracciare l'origine di ogni entry (manuale, Spamhaus, AWS, ecc.)", +] +for f in wl_features: + doc.add_paragraph(f, style='List Bullet') + +# 4.5 +doc.add_heading('4.5 Liste Pubbliche e Threat Intelligence', level=2) +doc.add_paragraph( + "Il sistema integra automaticamente feed di threat intelligence da fonti pubbliche riconosciute, " + "sincronizzandoli ogni 10 minuti per mantenere aggiornate le liste di IP noti come malevoli " + "o appartenenti a provider cloud legittimi." +) + +lists_table = doc.add_table(rows=8, cols=3) +lists_table.style = 'Light Grid Accent 1' +lists_table.alignment = WD_TABLE_ALIGNMENT.CENTER + +list_headers = ['Feed', 'Tipo', 'Descrizione'] +for i, h in enumerate(list_headers): + lists_table.rows[0].cells[i].text = h + for paragraph in lists_table.rows[0].cells[i].paragraphs: + for run in paragraph.runs: + run.bold = True + +list_data = [ + ['Spamhaus DROP', 'Blacklist', "Lista di IP/CIDR noti per attività malevole (spam, botnet, C&C)"], + ['Talos Intelligence', 'Blacklist', 'Feed di threat intelligence di Cisco Talos'], + ['Amazon AWS', 'Whitelist', 'Range IP ufficiali dei servizi cloud AWS'], + ['Google Cloud/GCP', 'Whitelist', 'Range IP ufficiali dei servizi Google Cloud'], + ['Microsoft Azure', 'Whitelist', 'Range IP ufficiali dei servizi cloud Microsoft Azure'], + ['Meta (Facebook)', 'Whitelist', 'Range IP di Meta (Facebook, Instagram, WhatsApp)'], + ['Cloudflare', 'Whitelist', 'Range IP della CDN Cloudflare'], +] +for i, row_data in enumerate(list_data): + for j, cell_text in enumerate(row_data): + lists_table.rows[i+1].cells[j].text = cell_text + +doc.add_paragraph("") +doc.add_paragraph( + "La merge logic applica una priorità basata sul tipo: gli IP in whitelist manuale hanno " + "sempre la precedenza, seguiti dalla whitelist pubblica e infine dalla blacklist. Questo " + "previene il blocco accidentale di servizi cloud legittimi." +) + +# 4.6 +doc.add_heading('4.6 Dashboard e Monitoraggio in Tempo Reale', level=2) +doc.add_paragraph( + "La dashboard web fornisce una visione completa e in tempo reale dello stato della " + "sicurezza di rete, con aggiornamento automatico ogni 10 secondi." +) +dash_features = [ + "Panoramica generale: contatori di router attivi, rilevamenti totali, IP bloccati, IP critici", + "Pagina Rilevamenti: elenco paginato (50/pagina) con ricerca server-side su IP, paese, organizzazione", + "Pagina Whitelist: gestione paginata con ricerca e operazioni CRUD", + "Pagina Analytics: grafici e visualizzazioni del traffico normale vs attacco", + "Filtri avanzati: per tipo di anomalia (DDoS, Port Scan, Brute Force, Botnet), range di risk score", + "Indicatori visivi: badge colorati per livelli di rischio, stato di blocco, flag paese", + "Monitoraggio servizi: stato in tempo reale di ML Backend, Database, Syslog Parser", + "Operazioni dirette: pulsanti per aggiungere a whitelist, sbloccare router, avviare training ML", +] +for f in dash_features: + doc.add_paragraph(f, style='List Bullet') + +# 4.7 +doc.add_heading('4.7 Geolocalizzazione IP', level=2) +doc.add_paragraph( + "Integrazione con il servizio ip-api.com per arricchire ogni rilevamento con informazioni " + "geografiche e di rete, inclusi paese, città, ISP, numero AS e organizzazione. " + "Il sistema implementa caching intelligente per ridurre le chiamate API e rispettare i " + "rate limit del servizio." +) + +# 4.8 +doc.add_heading('4.8 Gestione Router MikroTik', level=2) +doc.add_paragraph( + "Il sistema comunica con i router MikroTik tramite la REST API di RouterOS, " + "supportando operazioni parallele su multipli router contemporaneamente." +) +router_features = [ + "Configurazione router: IP, porta API, credenziali, abilitazione/disabilitazione", + "Test di connettività automatico", + "Supporto HTTP (porta 80) e HTTPS (porta 443) con gestione certificati SSL/TLS", + "Operazioni parallele su tutti i router (blocco/sblocco simultaneo)", + "Gestione address-list di firewall (aggiunta, rimozione, lettura)", + "Compatibilità con RouterOS 7.x e versioni successive", +] +for f in router_features: + doc.add_paragraph(f, style='List Bullet') + +# 4.9 +doc.add_heading('4.9 Pulizia Automatica dei Dati', level=2) +doc.add_paragraph( + "Un timer systemd orario esegue lo script cleanup_detections.py che:" +) +cleanup_features = [ + "Rimuove rilevamenti più vecchi di 48 ore", + "Sblocca automaticamente gli IP bloccati da più di 2 ore", + "Mantiene la retention dei log di rete a 3 giorni", + "Registra statistiche di pulizia nei log di sistema", +] +for f in cleanup_features: + doc.add_paragraph(f, style='List Bullet') + +# 4.10 +doc.add_heading('4.10 Monitoraggio Servizi', level=2) +doc.add_paragraph( + "La dashboard fornisce monitoraggio in tempo reale dello stato dei servizi critici " + "con possibilità di riavvio tramite API protette." +) +service_features = [ + "Monitoraggio stato: ML Backend, Database PostgreSQL, Syslog Parser", + "API per gestione servizi (start/stop/restart) protette da API Key", + "Integrazione con systemd per il controllo dei servizi Python", + "Health check endpoint (/health) per verifica rapida dello stato del sistema", +] +for f in service_features: + doc.add_paragraph(f, style='List Bullet') + +# --- 5. MAPPATURA ISO 27001 --- +doc.add_heading('5. Mappatura Controlli ISO/IEC 27001:2022', level=1) +doc.add_paragraph( + "La seguente tabella mappa le funzionalità dell'IDS ai controlli dell'Annex A dello " + "standard ISO/IEC 27001:2022, evidenziando come il sistema contribuisce alla conformità." +) + +iso_table = doc.add_table(rows=1, cols=4) +iso_table.style = 'Light Grid Accent 1' +iso_table.alignment = WD_TABLE_ALIGNMENT.CENTER + +iso_headers = ['Controllo', 'Titolo', 'Funzionalità IDS', 'Copertura'] +for i, h in enumerate(iso_headers): + iso_table.rows[0].cells[i].text = h + for paragraph in iso_table.rows[0].cells[i].paragraphs: + for run in paragraph.runs: + run.bold = True + +iso_mappings = [ + ['A.5.1', 'Politiche per la sicurezza delle informazioni', + 'Politiche di blocco automatico, soglie di rischio configurabili, logica di priorità whitelist/blacklist', + 'Parziale'], + ['A.5.7', 'Threat intelligence', + 'Integrazione automatica con 7+ feed di threat intelligence (Spamhaus, Talos, AWS, GCP, Azure, Meta, Cloudflare)', + 'Completa'], + ['A.5.24', 'Pianificazione e preparazione della gestione degli incidenti', + 'Rilevamento automatico anomalie, classificazione per livello di rischio, workflow di risposta automatizzato', + 'Completa'], + ['A.5.25', 'Valutazione e decisione sugli eventi di sicurezza', + 'Risk scoring ML (0-100), classificazione automatica in 5 livelli, soglie configurabili', + 'Completa'], + ['A.5.26', 'Risposta agli incidenti di sicurezza', + 'Blocco automatico IP critici sui router, sblocco manuale, gestione whitelist', + 'Completa'], + ['A.5.28', 'Raccolta delle evidenze', + 'Log completi di tutte le operazioni, timestamp di rilevamento e blocco, geolocalizzazione', + 'Completa'], + ['A.8.1', 'Dispositivi endpoint utente', + 'Protezione della rete tramite blocco IP malevoli a livello router (perimetrale)', + 'Parziale'], + ['A.8.9', 'Gestione della configurazione', + 'Gestione centralizzata della configurazione dei router, migrazioni database versionante', + 'Parziale'], + ['A.8.15', 'Logging', + 'Raccolta syslog centralizzata da tutti i router, retention 3 giorni, 186M+ record', + 'Completa'], + ['A.8.16', 'Attività di monitoraggio', + 'Dashboard real-time, auto-refresh 10s, monitoraggio servizi, alert visivi per livelli di rischio', + 'Completa'], + ['A.8.20', 'Sicurezza delle reti', + 'Firewall automatico via address-list MikroTik, blocco parallelo su tutti i router', + 'Completa'], + ['A.8.21', 'Sicurezza dei servizi di rete', + 'Protezione contro DDoS, port scanning, brute force, botnet tramite rilevamento ML', + 'Completa'], + ['A.8.22', 'Segregazione delle reti', + 'Supporto multi-router con configurazioni indipendenti, blocco selettivo per router', + 'Parziale'], + ['A.8.23', 'Filtraggio web', + 'Blocco IP malevoli a livello di rete, integrazione blacklist/whitelist', + 'Parziale'], +] + +for mapping in iso_mappings: + row = iso_table.add_row() + for i, cell_text in enumerate(mapping): + row.cells[i].text = cell_text + +# --- 6. CONTROLLI ANNEX A --- +doc.add_heading('6. Controlli Annex A Coperti - Dettaglio', level=1) + +doc.add_heading('A.5.7 - Threat Intelligence', level=2) +doc.add_paragraph( + "Il sistema implementa un processo automatizzato di raccolta e integrazione di threat intelligence " + "da fonti pubbliche riconosciute a livello internazionale. La sincronizzazione avviene ogni 10 minuti " + "tramite il servizio ids-list-fetcher, che scarica, analizza e aggiorna le liste nel database. " + "La merge logic applica una gerarchia di priorità per evitare conflitti tra liste diverse." +) + +doc.add_heading('A.5.24/25/26 - Gestione Incidenti', level=2) +doc.add_paragraph( + "Il ciclo di vita degli incidenti di sicurezza è gestito automaticamente:\n" + "1. RILEVAMENTO: Il modello ML analizza i log ogni 2 minuti e identifica anomalie\n" + "2. CLASSIFICAZIONE: Ogni anomalia riceve un risk score e viene categorizzata (DDoS, Port Scan, Brute Force, Botnet)\n" + "3. RISPOSTA: Gli IP con score >= 80 vengono bloccati automaticamente sui router\n" + "4. DOCUMENTAZIONE: Ogni evento viene registrato con timestamp, geolocalizzazione, score e stato di blocco\n" + "5. RISOLUZIONE: Pulizia automatica dopo 48 ore, sblocco dopo 2 ore, possibilità di whitelist manuale" +) + +doc.add_heading('A.8.15/16 - Logging e Monitoraggio', level=2) +doc.add_paragraph( + "Il sistema fornisce capacità complete di logging e monitoraggio:\n" + "- Raccolta centralizzata dei log syslog da tutti i router via UDP:514\n" + "- Conservazione dei log per 3 giorni con retention policy automatica\n" + "- Dashboard di monitoraggio con aggiornamento ogni 10 secondi\n" + "- Monitoraggio dello stato dei servizi (ML Backend, Database, Syslog Parser)\n" + "- Visualizzazioni analitiche del traffico normale vs attacco\n" + "- Paginazione e ricerca server-side per gestire grandi volumi di dati" +) + +# --- 7. POLITICHE DI SICUREZZA --- +doc.add_heading('7. Politiche di Sicurezza Implementate', level=1) + +doc.add_heading('7.1 Autenticazione e Controllo Accessi', level=2) +sec_features = [ + "API Key authentication (header X-API-Key) per tutti gli endpoint del backend ML", + "Credenziali router cifrate nel database", + "Connessioni database autenticate con credenziali separate", + "Supporto HTTPS/TLS per la comunicazione con i router MikroTik", +] +for f in sec_features: + doc.add_paragraph(f, style='List Bullet') + +doc.add_heading('7.2 Protezione dei Dati', level=2) +data_features = [ + "Database PostgreSQL con accesso autenticato", + "Retention policy automatica: 3 giorni per log, 48 ore per rilevamenti", + "Nessun dato sensibile esposto tramite API pubblica", + "Validazione input con schema Zod per prevenire injection", + "Query parametrizzate per prevenire SQL injection", +] +for f in data_features: + doc.add_paragraph(f, style='List Bullet') + +doc.add_heading('7.3 Disponibilità e Resilienza', level=2) +avail_features = [ + "Auto-reconnect del parser syslog in caso di disconnessione dal database", + "Timer systemd con restart automatico dei servizi in caso di failure", + "Health check endpoint per monitoraggio esterno", + "Architettura a microservizi: il failure di un componente non blocca gli altri", +] +for f in avail_features: + doc.add_paragraph(f, style='List Bullet') + +# --- 8. GESTIONE INCIDENTI --- +doc.add_heading('8. Gestione degli Incidenti', level=1) + +doc.add_heading('8.1 Workflow di Risposta Automatica', level=2) +doc.add_paragraph( + "Il sistema implementa un workflow di risposta automatica agli incidenti di sicurezza:" +) + +incident_table = doc.add_table(rows=6, cols=3) +incident_table.style = 'Light Grid Accent 1' +incident_table.alignment = WD_TABLE_ALIGNMENT.CENTER + +inc_headers = ['Fase', 'Azione', 'Tempistica'] +for i, h in enumerate(inc_headers): + incident_table.rows[0].cells[i].text = h + for paragraph in incident_table.rows[0].cells[i].paragraphs: + for run in paragraph.runs: + run.bold = True + +inc_data = [ + ['1. Raccolta', 'Ricezione log syslog da router MikroTik', 'Tempo reale (UDP)'], + ['2. Analisi', 'Analisi ML con Isolation Forest + Ensemble', 'Ogni 2 minuti'], + ['3. Classificazione', 'Assegnazione risk score e tipo anomalia', 'Automatica'], + ['4. Risposta', 'Blocco automatico IP su tutti i router (score >= 80)', 'Immediato dopo analisi'], + ['5. Documentazione', 'Registrazione con geolocalizzazione e timestamp', 'Automatica'], +] +for i, row_data in enumerate(inc_data): + for j, cell_text in enumerate(row_data): + incident_table.rows[i+1].cells[j].text = cell_text + +doc.add_paragraph("") + +doc.add_heading('8.2 Tipi di Anomalie Rilevate', level=2) +anomaly_table = doc.add_table(rows=6, cols=2) +anomaly_table.style = 'Light Grid Accent 1' +anomaly_table.alignment = WD_TABLE_ALIGNMENT.CENTER + +anom_headers = ['Tipo', 'Descrizione'] +for i, h in enumerate(anom_headers): + anomaly_table.rows[0].cells[i].text = h + for paragraph in anomaly_table.rows[0].cells[i].paragraphs: + for run in paragraph.runs: + run.bold = True + +anom_data = [ + ['DDoS Attack', 'Attacco Distributed Denial of Service - elevato volume di traffico da singola sorgente'], + ['Port Scanning', "Scansione sistematica delle porte per identificare servizi vulnerabili"], + ['Brute Force', 'Tentativi ripetuti di autenticazione con credenziali diverse'], + ['Botnet Activity', 'Traffico riconducibile a reti di dispositivi compromessi (C&C)'], + ['Suspicious Activity', 'Comportamento anomalo non classificabile nelle categorie precedenti'], +] +for i, row_data in enumerate(anom_data): + for j, cell_text in enumerate(row_data): + anomaly_table.rows[i+1].cells[j].text = cell_text + +# --- 9. CONTINUITÀ --- +doc.add_heading('9. Continuità Operativa e Disponibilità', level=1) +doc.add_paragraph( + "Il sistema è progettato per operare in modo continuo e autonomo, minimizzando " + "l'intervento manuale e garantendo la disponibilità del servizio di monitoraggio." +) +continuity_features = [ + "Servizi gestiti da systemd con restart automatico in caso di failure (Restart=always)", + "Timer systemd per operazioni periodiche (analisi ML ogni 2 min, pulizia oraria, sync liste ogni 10 min)", + "Auto-reconnect del parser syslog in caso di perdita connessione al database", + "Architettura a microservizi: il failure del backend ML non blocca la raccolta log", + "Health check endpoint per integrazione con sistemi di monitoraggio esterni (Nagios, Zabbix, ecc.)", + "Database PostgreSQL con supporto a backup e recovery point-in-time", + "Migrazioni database versionante per aggiornamenti sicuri dello schema", +] +for f in continuity_features: + doc.add_paragraph(f, style='List Bullet') + +# --- 10. AUDIT --- +doc.add_heading('10. Audit e Tracciabilità', level=1) +doc.add_paragraph( + "Il sistema mantiene traccia completa di tutte le operazioni per supportare attività " + "di audit e conformità:" +) +audit_features = [ + "Log di tutti i rilevamenti con timestamp, risk score, tipo anomalia, geolocalizzazione", + "Registrazione delle operazioni di blocco/sblocco con timestamp (blocked_at)", + "Storico delle entry di whitelist con data di creazione e motivo", + "Log delle sincronizzazioni delle liste pubbliche con conteggio IP aggiunti/rimossi", + "Storico dei training del modello ML con parametri e risultati", + "Log di sistema via journald per tutti i servizi (accessibili con journalctl)", + "Database delle migrazioni con versioning per tracciare le modifiche allo schema", +] +for f in audit_features: + doc.add_paragraph(f, style='List Bullet') + +# --- 11. CONCLUSIONI --- +doc.add_heading('11. Conclusioni', level=1) +doc.add_paragraph( + "Il Sistema di Rilevamento Intrusioni (IDS) implementa un insieme completo di controlli " + "di sicurezza che contribuiscono significativamente alla conformità con lo standard " + "ISO/IEC 27001:2022. In particolare, il sistema copre in modo completo i controlli " + "relativi a:" +) +conclusions = [ + "Threat intelligence (A.5.7): integrazione automatica con 7+ feed pubblici", + "Gestione degli incidenti (A.5.24-A.5.28): workflow completo dalla rilevazione alla risoluzione", + "Logging e monitoraggio (A.8.15-A.8.16): raccolta centralizzata e dashboard real-time", + "Sicurezza delle reti (A.8.20-A.8.21): protezione attiva tramite blocco automatico", +] +for c in conclusions: + doc.add_paragraph(c, style='List Bullet') + +doc.add_paragraph("") +doc.add_paragraph( + "Il sistema richiede integrazione con ulteriori controlli organizzativi, procedurali e " + "tecnici per una conformità completa allo standard ISO 27001, tra cui: politiche formali " + "documentate, formazione del personale, gestione degli asset, controllo degli accessi " + "fisici e logici, e processi di audit interno periodico." +) + +doc.add_paragraph("") +p = doc.add_paragraph() +p.alignment = WD_ALIGN_PARAGRAPH.CENTER +run = p.add_run("--- Fine del Documento ---") +run.font.color.rgb = RGBColor(128, 128, 128) +run.font.size = Pt(10) + +# Save +output_path = "IDS_Conformita_ISO27001.docx" +doc.save(output_path) +print(f"Documento generato: {output_path}") diff --git a/pyproject.toml b/pyproject.toml index 49f0bca..2ba0498 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,4 +5,5 @@ description = "Add your description here" requires-python = ">=3.11" dependencies = [ "httpx>=0.28.1", + "python-docx>=1.2.0", ] diff --git a/uv.lock b/uv.lock index 1e086e8..994b094 100644 --- a/uv.lock +++ b/uv.lock @@ -71,16 +71,135 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "python-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + [[package]] name = "repl-nix-workspace" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "httpx" }, + { name = "python-docx" }, ] [package.metadata] -requires-dist = [{ name = "httpx", specifier = ">=0.28.1" }] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "python-docx", specifier = ">=1.2.0" }, +] [[package]] name = "sniffio"