Security Monitor v2 · Microsoft 365 Security Platform → SISAlerts + Jira

Security Monitor

Plataforma de monitorización de seguridad M365. Consume la Graph Security API (alertas e incidentes Defender), enriquece con contexto de usuario y dispositivo, y notifica a través de SISAlerts y Jira.

👤 Autor: JM Fernandez + Claude (Anthropic)
📅 Fecha: Mayo 2026
🌐 Acceso: Entra ID App Proxy (SSO)
Estado: Producción
Python 3 Flask + Waitress SQLite WAL Microsoft Graph Security API Microsoft Defender Jira Cloud SISAlerts Entra ID App Proxy WinSW Windows Server

Plataforma de seguridad M365 en producción

Security Monitor v2 es un servicio Python que consume la Graph Security API (alertas e incidentes Defender), enriquece con contexto de usuario y dispositivo, y notifica a través de SISAlerts y Jira.

🛡️

Graph Security API (flujo principal)

Polling cada 120s de security/alerts_v2 e incidents. Prefiltro por severidad mínima global. Enriquecimiento con perfil de usuario, manager, grupos y datos de dispositivo.

📋

Filtros de severidad configurables

CRUD de filtros con lógica “first match wins”. Cada filtro determina si se notifica a SISAlerts y/o si se crea ticket Jira, con qué proyecto y tipo de issue.

🔖

Jira dual project

Cada alerta o incidente puede generar tickets en dos proyectos Jira distintos simultáneamente. Los enlaces entre alertas e incidentes se gestionan independientemente por proyecto.

📊

Informe mensual de protección

Informe HTML de postura M365 generado los días 1 y 15. Cubre sign-ins bloqueados, risk detections, alertas por categoría y usuarios en riesgo. Se adjunta a un ticket Jira.

🔒

Idempotencia garantizada

Tabla notification_log con canales sisalerts, jira_create, jira_create_2, jira_link:{key}. Previene duplicados tras reinicios o errores transitorios.

120
Segundos entre polls Graph Security API
N
Filtros de severidad configurables (CRUD)
2
Proyectos Jira simultáneos por alerta/incidente
9
Tablas SQLite (alerts, incidents, filters, notif_log…)
8083
Puerto de la webapp
1+15
Días del mes en que se genera el informe de protección

Arquitectura del flujo de seguridad

El scheduler gestiona el polling de la Graph Security API y el informe de protección en el mismo proceso Python. Comparten DB, config y servicios de notificación.

🛡️ Flujo principal — Graph Security API

Graph Security
alerts_v2 + incidents
Microsoft Defender, MCAS, MDI…
120s poll
🔄
Scheduler
Python thread
prefiltro severidad, enrich, persist
notifier
🔔
SISAlerts
port 8082
ingest_security_alert()
jira_client
🔖
Jira Cloud
REST v3
Tickets proyecto 1 y/o 2
Enrichment: alert evidence Graph User (profile + manager + groups) Graph Device cache TTL 5min
Idempotencia: notification_log canales: sisalerts, jira_create, jira_create_2, jira_link:{key} CHECK antes de notificar
Dual Jira: severity_filter con jira_project_key_2 create en proyecto 1 Y proyecto 2 links sin cruzar proyectos
Protection report: scheduler días 1+15 graph_protection.py HTML + Chart.js attach Jira ticket

Pipeline detallado: alerta Defender → Jira + SISAlerts

Cada ciclo de 120 segundos el scheduler obtiene alertas e incidentes nuevos, los enriquece, los persiste en SQLite y los notifica según los filtros de severidad configurados.

1

Poll Graph Security API

GET /security/alerts_v2 y GET /security/incidents con filtro por createdDateTime ge {last_poll}. Auth con certificado (App Registration 250c7cb4).

2

Prefiltro min_severity_global

Descarta alertas por debajo del umbral mínimo global de config antes de persistir. Reduce ruido de alertas informational no deseadas.

3

Enriquecimiento graph_enrich.enrich_alert()

Por cada usuario en evidence: displayName, jobTitle, department, manager, grupos (top 10). Por cada dispositivo: hostname, OS, isCompliant, isManaged. Cache TTL 5 minutos para no saturar Graph.

4

Persistencia en SQLite

INSERT en security_alerts y/o security_incidents. evidence_json y enrichment_json almacenados como JSON. first_seen_at permite detectar si ya existía la alerta.

5

Evaluación de filtros de severidad

Recorre severity_filters ordenados por sort_order. Compara min_severity, service_source, category y title_contains. La primera coincidencia gana — determina si notificar y cómo.

6

Notificación SISAlerts (notify_sisalerts: true)

sisalerts_client.ingest_security_alert() envía la alerta enriquecida con rule_id y priority del filtro. Canal sisalerts en notification_log previene reenvíos.

7

Creación de tickets Jira (create_jira: true)

Alerta: ticket tipo Alertas 365 en proyecto 1 (y opcionalmente proyecto 2).
Incidente: ticket madre tipo Incidente 365 en proyecto 1 (y opcionalmente proyecto 2). Alertas del incidente se enlazan con Relates a su respectivo ticket madre — sin links cruzados entre proyectos.
Contenido del ticket: wiki markup completo con severidad, evidencias, MITRE ATT&CK, contexto enriquecido de usuarios y dispositivos.

# notifier.py — flujo simplificado de process_new_alert()
def process_new_alert(alert, severity_filter, cfg):
    # 1. Notificar SISAlerts si procede
    if severity_filter['notify_sisalerts'] and not already_notified(alert_id, 'sisalerts'):
        sisalerts_client.ingest_security_alert(alert, severity_filter)
        log_notification(alert_id, 'sisalerts')

    # 2. Crear ticket Jira proyecto 1
    if severity_filter['create_jira'] and not already_notified(alert_id, 'jira_create'):
        key1 = jira_client.create_ticket(alert, severity_filter, project='key_1')
        log_notification(alert_id, 'jira_create', detail=key1)

    # 3. Crear ticket Jira proyecto 2 (si configurado)
    if severity_filter.get('jira_project_key_2') and not already_notified(alert_id, 'jira_create_2'):
        key2 = jira_client.create_ticket(alert, severity_filter, project='key_2')
        log_notification(alert_id, 'jira_create_2', detail=key2)

Filtros de severidad y doble proyecto Jira

Los filtros de severidad son la pieza central de configuración del flujo Security API. Determinan qué alertas notificar, con qué prioridad y a qué proyectos Jira.

4.1 Campos del filtro de severidad

CampoTipoDescripción
namestringNombre descriptivo del filtro (ej: “Defender High”)
min_severityenumSeveridad mínima para que aplique: high / medium / low / informational
service_sourcestring (opt.)Filtro por origen del servicio (ej: microsoftDefenderForEndpoint). Vacío = cualquiera.
categorystring (opt.)Subcadena de la categoría de la alerta (case-insensitive contains). Vacío = cualquiera.
title_containsstring (opt.)Subcadena del título de la alerta (case-insensitive). Vacío = cualquiera.
notify_sisalertsboolSi se debe enviar a SISAlerts cuando coincide este filtro
sisalerts_rule_idstringRule ID en SISAlerts (determina canales Teams/Telegram)
sisalerts_priorityenumPrioridad para SISAlerts: critical / high / normal / low
create_jiraboolSi se debe crear ticket Jira
jira_project_keystringProyecto Jira principal (ej: MESA)
jira_issue_typestringTipo de issue en proyecto 1 (ej: Alertas 365)
jira_labelsstringLabels separadas por coma para proyecto 1
jira_project_key_2string (opt.)Segundo proyecto Jira. Vacío = sin segundo ticket
jira_issue_type_2string (opt.)Tipo de issue en proyecto 2
jira_labels_2string (opt.)Labels para proyecto 2
enabledboolToggle on/off sin eliminar el filtro
sort_orderintOrden de evaluación. Reordenable con drag&drop en la UI.
Regla first-match-wins: los filtros se evalúan en orden ascendente de sort_order. En cuanto una alerta satisface todos los criterios de un filtro (min_severity, service_source si está definido, category si está definido, title_contains si está definido), ese filtro es el que aplica y el resto no se evalúa. Los filtros deshabilitados se saltan.

4.2 Doble proyecto Jira (Dual Project)

🔖

Alerta con dual project activo

Se crean dos tickets independientes: uno en jira_project_key (ej: MESA-1234) y otro en jira_project_key_2 (ej: SOS-567). Cada canal de idempotencia es independiente: jira_create y jira_create_2.

📄

Incidente con dual project activo

Ticket madre en proyecto 1 y ticket madre en proyecto 2. Las alertas hijas se enlazan (Relates) al ticket madre de su mismo proyecto. No hay links cruzados entre proyectos. Canales: jira_link:{incident_key_1} y jira_link_2:{incident_key_2}.

4.3 Configuración global Jira

Clave configDescripción
default_jira_projectProyecto 1 por defecto (si el filtro de severidad no especifica proyecto)
default_jira_issue_typeTipo de issue para alertas en proyecto 1 (ej: Alertas 365)
default_jira_incident_typeTipo de issue para incidentes en proyecto 1 (ej: Incidente 365). Campo separado del tipo de alerta.
default_jira_labelsLabels por defecto para proyecto 1
default_jira_project_2Proyecto 2 por defecto
default_jira_issue_type_2Tipo de issue para alertas en proyecto 2
default_jira_incident_type_2Tipo de issue para incidentes en proyecto 2

4.4 Contenido del ticket Jira (alerta)

El contenido del ticket se genera en Jira wiki markup con secciones completas: severidad + estado + servicio, categoría, actor, enlace a M365 portal, timeline (created / first_event / last_event), entidades afectadas (usuarios, dispositivos, mailboxes de la evidence), descripción, recommended_actions, técnicas MITRE ATT&CK, contexto enriquecido de usuarios (con manager y grupos), contexto de dispositivos y evidencia técnica (procesos, ficheros, conexiones de red, claves de registro, URLs).

Informe mensual de postura de seguridad M365

El scheduler dispara automáticamente un informe HTML de postura M365 los días 1 y 15 de cada mes. También se puede generar bajo demanda desde la UI de administración.

📋

Sign-ins bloqueados

Consulta auditLogs/signIns filtrando códigos de error 50053 (cuenta bloqueada), 50126 (credenciales inválidas), 50055 (contraseña caducada) y 90095 (consentimiento de admin requerido).

🔒

Risk Detections

Obtiene identityProtection/riskDetections del período y los agrupa por tipo de detección para mostrar la distribución de amenazas de identidad.

🛡️

Alertas de seguridad

Agrega las alertas del período por categoría, servicio y severidad. Usa security/alerts_v2 con chunks diarios y sleep(3) entre días para evitar throttling.

👤

Usuarios en riesgo

Lista identityProtection/riskyUsers activos. Auto-dismiss de usuarios en riesgo stale (sin actividad reciente) si auto_dismiss: true en config.

🌍

Mapa chorópleta de sign-ins

Distribución geográfica de sign-ins bloqueados renderizada con Chart.js + topojson. Descargados al primer arranque del servicio en assets/.

📄

Adjunto a ticket Jira

El informe HTML se adjunta al ticket Jira creado en default_jira_project. Todos los tickets de incidentes del período se enlazan al ticket del informe con Relates.

5.1 Ventana temporal y ejecución

ModoVentana temporalDescripción
Programado (días 1 y 15) since_dt = última ejecución exitosa (fallback: 60 días) El scheduler verifica cada ciclo si es día 1 o 15 y si ya se ejecutó hoy. Registra en protection_report_runs.
Bajo demanda (“Generar informe ahora”) Configurable: days_back desde la UI Botón en admin_settings.html. Estado en tiempo real via polling cada 5s.

5.2 Tabla de auditoría protection_report_runs

CampoDescripción
idPK auto-increment
run_atTimestamp de ejecución
on_demand0 = programado, 1 = manual
since_dtFecha de inicio del período analizado
periodDescripción del período (ej: “2026-04-15 → 2026-05-01”)
jira_keyClave del ticket Jira creado (ej: MESA-890)
statusok / error / running
errorMensaje de error si status=error

5.3 Endpoints Graph usados por el informe

EndpointPermiso requeridoUso
auditLogs/signInsAuditLog.Read.AllSign-ins bloqueados, chunks diarios
identityProtection/riskDetectionsIdentityRiskEvent.Read.AllRisk events por tipo
security/alerts_v2SecurityAlert.Read.AllAlertas del período, chunks diarios + sleep(3)
identityProtection/riskyUsersIdentityRiskyUser.ReadWrite.AllUsuarios en riesgo + auto-dismiss stale

Esquema SQLite — 6 tablas

SQLite en modo WAL. Auto-migraciones al arrancar (columnas nuevas se añaden sin recrear tablas). Bootstrap admin configurado en app-config.json.

TablaFlujoColumnas clave
security_alerts Security API graph_alert_id, title, severity, status, category, service_source, description, recommended_actions, mitre_techniques, actor_display_name, first_activity_datetime, last_activity_datetime, evidence_json, enrichment_json, jira_ticket_key, jira_ticket_key_2, sisalerts_id, graph_incident_id, created_at_graph, first_seen_at
security_incidents Security API graph_incident_id, display_name, severity, status, classification, incident_web_url, jira_ticket_key, jira_ticket_key_2, created_at_graph, first_seen_at
severity_filters Security API name, min_severity, service_source, category, title_contains, notify_sisalerts, sisalerts_rule_id, sisalerts_priority, create_jira, jira_project_key, jira_issue_type, jira_labels, jira_project_key_2, jira_issue_type_2, jira_labels_2, enabled, sort_order
notification_log Security API entity_type, entity_graph_id, channel, detail, status, created_at
protection_report_runs Protection Report run_at, on_demand, since_dt, period, jira_key, status, error
users SSO upn, is_admin, last_seen
Canales de idempotencia en notification_log: sisalertsjira_createjira_create_2jira_link:{incident_jira_key}jira_link_2:{incident_jira_key_2}. Cada canal se registra con status ok o error. Solo se intenta una notificación si no existe ya un registro ok para ese canal + entidad.

Organización del repositorio

Backend Python modular con servicios desacoplados. Frontend Jinja2 + Vanilla JS, sin frameworks ni build step. Entry point único que arranca Flask + scheduler.

security-monitor/
  config/
    app-config.json              — Config local (gitignored)
    app-config.example.json      — Plantilla documentada con todos los campos
  db/
    database.py                  — SQLite WAL, auto-migraciones, bootstrap admin
    schema.sql                   — Tablas base (9 tablas)
  services/
    graph_security.py            — fetch_alerts(), fetch_incidents(), fetch_alert_by_id()
    graph_auth.py                — MSAL singleton — cert store Windows o fichero PFX/PEM
    graph_enrich.py              — enrich_alert() — usuario + dispositivo, cache TTL 5min
    graph_protection.py          — Fetchers para informe de protección (signIns, risks, alerts, risky_users)
    jira_client.py               — create_ticket(), add_comment(), link_issues(), add_attachment()
    notifier.py                  — process_new_alert(), process_new_incident(), update_incident_ticket()
    protection_report.py         — run_protection_report(), render_html(), load_assets()
    sisalerts_client.py          — ingest_security_alert() — POST a SISAlerts /api/ingest
  assets/                        — Chart.js + topojson (gitignored, descargados al primer arranque)
  webapp/
    app.py                       — Flask app, blueprints, init DB
    auth.py                      — SSO App Proxy (X-MS-CLIENT-PRINCIPAL-NAME)
    routes/
      admin.py                   — CRUD severity_filters, settings, test-jira-pipeline, ingest-alert
      api.py                     — /api/test-jira, /api/poll-security-now, /api/status
      dashboard.py               — Stats Security API (alertas, incidentes)
    templates/
      base.html                  — Layout con navbar, theme toggle
      dashboard.html             — Dashboard: incidentes + alertas recientes, poll manual
      admin_severity.html        — CRUD filtros severidad + drag&drop + badge segundo proyecto
      admin_settings.html        — Config + Jira defaults + informe protección on-demand
  scheduler.py                 — Loop polling Security API + protection report scheduler
  run_webapp.py                — Entry point: Flask + scheduler en background thread
  security-monitor-service.xml — Descriptor WinSW
  deploy.ps1                   — git pull + pip install + restart servicio

Panel de administración y herramientas

UI completa para gestionar filtros de severidad, probar la integración Jira, importar alertas manualmente y generar el informe de protección bajo demanda.

Sección AdminFuncionalidades
Filtros de severidad CRUD completo + drag&drop reorder + toggle on/off. Formulario inline para crear. Modal para editar. Badge visual del segundo proyecto en la tabla.
Ajustes Jira Configuración de default_jira_project, default_jira_issue_type (alertas), default_jira_incident_type (incidentes) para proyecto 1 y 2. Botón “Test pipeline” que crea tickets [TEST] reales en todos los proyectos configurados (incidente + alerta enlazados).
Importar alerta manualmente Campo para introducir un Graph alert ID. Re-importa la alerta forzando re-notificación (limpia el notification_log de esa entidad).
Test SISAlerts Envía una alerta sintética con rule_id configurable. Verifica conectividad sin necesitar alerta real.
Informe de protección on-demand Botón “Generar informe ahora” con campo days_back. Estado en tiempo real vía polling cada 5 segundos (runningok / error).
Dashboard Security Stats últimas 24h / 7 días. Tabla de alertas recientes con badge de severidad y links directos a Jira y al portal M365.
high medium low informational

Badges de severidad y estado usados en el dashboard.

Referencia de app-config.json

Todos los parámetros de configuración del sistema, organizados por categoría.

Security API

ClaveTipoDescripción
security_polling_enabledboolActivar/desactivar el polling de Graph Security API
security_polling_interval_secondsintIntervalo de polling (defecto: 120)
min_severity_globalstringSeveridad mínima global para preprocesar alertas: high / medium / low / informational
enrich_alertsboolActivar enriquecimiento con Graph User y Device
security_retention_daysintDías de retención de alertas e incidentes en BD

Jira

ClaveDescripción
jira.base_urlURL base de Jira Cloud (ej: https://idealista.atlassian.net)
jira.user_emailEmail del usuario Jira para autenticación básica
jira.api_tokenAPI token de Jira (Atlassian API token)
default_jira_projectProyecto 1 por defecto (ej: MESA)
default_jira_issue_typeTipo issue para alertas en proyecto 1 (ej: Alertas 365)
default_jira_incident_typeTipo issue para incidentes en proyecto 1 (ej: Incidente 365)
default_jira_labelsLabels por defecto proyecto 1 (lista)
default_jira_project_2Proyecto 2 por defecto (ej: SOS)
default_jira_issue_type_2Tipo issue para alertas en proyecto 2
default_jira_incident_type_2Tipo issue para incidentes en proyecto 2

Graph API

ClaveDescripción
graph.tenant_idTenant ID de Entra ID (d78b7929-c2a3-4897-ae9a-7d8f8dc1a1cf)
graph.client_idClient ID del App Registration (250c7cb4-...)
graph.auth_modeModo de auth: certificate
graph.certificate_thumbprintThumbprint del certificado en el Windows Cert Store

Informe de Protección

ClaveDescripción
protection_report.enabledActivar generación automática los días 1 y 15
protection_report.jira_project_keyProyecto Jira donde adjuntar el informe (legacy fallback; usa default_jira_project si no se especifica)
protection_report.auto_dismissAuto-dismiss de risky users stale al generar el informe

General

ClaveDescripción
portPuerto de la webapp (8083)
bootstrap_adminUPN del admin inicial (se crea en primer arranque)
sisalerts.urlURL base de SISAlerts (ej: http://sisalerts-server:8082)
sisalerts.api_keyAPI key de SISAlerts para /api/ingest
sso.dev_bypass_upnUPN para bypass SSO en desarrollo local
// app-config.example.json — estructura completa
{
  "port": 8083,
  "bootstrap_admin": "[email protected]",
  "security_polling_enabled": false,
  "security_polling_interval_seconds": 120,
  "min_severity_global": "medium",
  "enrich_alerts": true,
  "security_retention_days": 90,
  "graph": {
    "tenant_id": "d78b7929-c2a3-4897-ae9a-7d8f8dc1a1cf",
    "client_id": "250c7cb4-...",
    "auth_mode": "certificate",
    "certificate_thumbprint": "..."
  },
  "sisalerts": { "url": "http://sisalerts-server:8082", "api_key": "..." },
  "default_jira_project": "MESA",
  "default_jira_issue_type": "Alertas 365",
  "default_jira_incident_type": "Incidente 365",
  "default_jira_project_2": "SOS",
  "default_jira_issue_type_2": "Alertas 365",
  "default_jira_incident_type_2": "Incidente 365",
  "jira": {
    "base_url": "https://idealista.atlassian.net",
    "user_email": "...",
    "api_token": "..."
  },
  "protection_report": {
    "enabled": true,
    "auto_dismiss": false
  },
  "sso": { "dev_bypass_upn": "" }
}

Permisos Graph API (250c7cb4)

App Registration en Entra ID con autenticación por certificado (thumbprint en config). Permisos de tipo Application (sin usuario interactivo).

PermisoTipoUso
SecurityAlert.Read.AllApplicationLeer alertas Defender vía /security/alerts_v2
SecurityIncident.Read.AllApplicationLeer incidentes vía /security/incidents
User.Read.AllApplicationEnriquecimiento: perfil, manager de usuarios en la evidencia
Device.Read.AllApplicationEnriquecimiento: hostname, OS, compliance de dispositivos
GroupMember.Read.AllApplicationEnriquecimiento: grupos del usuario (top 10)
AuditLog.Read.AllApplicationInforme de protección: sign-ins bloqueados
IdentityRiskEvent.Read.AllApplicationInforme de protección: risk detections
IdentityRiskyUser.ReadWrite.AllApplicationInforme de protección: listar y hacer dismiss de risky users

Producción en Windows Server

WinSW como servicio Windows. Entra ID App Proxy para acceso externo con SSO. Deploy automático vía deploy.ps1.

💻 Sistema

  • Windows Server 2022
  • Python 3 con venv
  • SQLite WAL (security-monitor.db)
  • Certificado en Windows Cert Store
  • Puerto 8083 (local)

🌐 Acceso

  • Entra ID App Proxy (SSO)
  • Cabecera: X-MS-CLIENT-PRINCIPAL-NAME
  • Dev bypass: sso.dev_bypass_upn en config
  • Bootstrap admin en primer arranque

⚙️ Servicio Windows

  • security-monitor-service.xml (WinSW)
  • Log rotativo en logs/
  • Hot-reload de config sin reinicio

🚀 Deploy

  • deploy.ps1: git pull + pip install
  • Restart del servicio WinSW
  • Auto-migración DB en arranque
  • Assets Chart.js descargados si faltan

Decisiones de diseño y problemas resueltos

Los principales retos de implementación del sistema, con la solución adoptada en cada caso.

🔗

Idempotencia de notificaciones ante reinicios del servicio

Si el servicio se reinicia a mitad de un ciclo de polling, puede intentar re-notificar alertas ya enviadas. Solución: tabla notification_log con canales granulares (sisalerts, jira_create, jira_create_2, jira_link:{key}, jira_link_2:{key}). Antes de cualquier notificación se consulta si ya existe un registro ok para esa entidad+canal. Las operaciones son idempotentes por diseño.

📄

Links Jira entre alertas e incidentes sin cruzar proyectos

Con dual project activo, una alerta tiene un ticket en MESA y otro en SOS, y su incidente padre también tiene tickets en MESA y SOS. Los links Relates deben unir MESA-alerta con MESA-incidente y SOS-alerta con SOS-incidente, nunca cruzados. Solución: canales de idempotencia separados jira_link:{incident_key_1} vs jira_link_2:{incident_key_2}. El notifier resuelve qué clave de incidente usar según el proyecto del ticket de alerta.

Throttling de Graph API en el informe de protección

El informe analiza ventanas de 15-60 días con queries diarias para sign-ins y alertas. Hacer todas las peticiones seguidas agota rápidamente el quota de Graph. Solución: chunks diarios con sleep(3) entre peticiones en graph_protection.py. El cache TTL de 5 minutos en graph_enrich.py reduce las peticiones de enriquecimiento repetidas para los mismos usuarios/dispositivos.

📋

Tipo de issue diferente para alertas vs incidentes en Jira

Alertas e incidentes son entidades diferentes en Defender y merecen tipos de issue distintos en Jira (Alertas 365 vs Incidente 365). Sin embargo, los filtros de severidad solo tenían un campo de tipo issue. Solución: se añadió default_jira_incident_type (y _2) en config global, separado de default_jira_issue_type. El notifier selecciona el tipo correcto según si la entidad es alerta o incidente.

📊

Estado del informe de protección en tiempo real

La generación del informe puede tardar varios minutos (queries de 60 días + render + subida a Jira). La UI necesita mostrar progreso sin websockets. Solución: la generación se lanza en un thread y escribe el estado en protection_report_runs (runningok/error). La UI hace polling GET cada 5 segundos para actualizar el badge de estado sin recargar la página.

🔍

Enriquecimiento con cache sin invalidar prematuramente

El enriquecimiento de usuario requiere hasta 3 llamadas Graph por usuario (profile, manager, groups). En un ciclo con muchas alertas del mismo usuario, esto multiplica las peticiones. Solución: cache en memoria con TTL de 5 minutos en graph_enrich.py, keyed por UPN/device_id. En el ciclo de 120s, si el mismo usuario aparece en varias alertas consecutivas se sirve del cache. El TTL corto asegura que cambios organizativos (manager, grupos) se reflejan en el día.

Dependencias y tecnologías

CapaTecnologíaUso
Backend runtime Python 3 Webapp Flask + scheduler en background thread. Un único proceso.
Web framework Flask + Waitress HTTP server WSGI en producción. Blueprints: admin, api, dashboard.
Base de datos SQLite (WAL mode) 9 tablas. WAL permite lecturas concurrentes durante escrituras del scheduler.
Microsoft Graph requests + MSAL cert Security API, Mail, AuditLogs, IdentityProtection. Auth con certificado del Windows Cert Store.
Integración Jira requests (Jira REST v3) Crear tickets, añadir comentarios, enlazar issues, adjuntar ficheros. Dual project.
Frontend Jinja2 + Vanilla JS Templates server-side. JS sin frameworks ni build step. Drag&drop HTML5 API.
Charts (informe) Chart.js + topojson Choropleth map de sign-ins + bar charts. Descargados al primer arranque.
Auth Entra ID App Proxy SSO vía cabecera X-MS-CLIENT-PRINCIPAL-NAME. Bypass configurable para dev.
Servicio Windows WinSW Descriptor XML para instalar como servicio Windows en producción.
Destino alertas SISAlerts /api/ingest ingest_security_alert() para Graph Security flow.