Introducción a los gráficos personalizados en Microsoft Sentinel (versión preliminar)

Los gráficos personalizados de Microsoft Sentinel permiten a los investigadores y analistas de seguridad crear representaciones de grafos personalizadas de sus datos de seguridad. Mediante la creación de gráficos personalizados, puede modelar patrones de ataque específicos, investigar amenazas y ejecutar algoritmos de grafos avanzados para descubrir relaciones ocultas dentro del entorno digital. Esta guía le guiará por los pasos para crear y administrar gráficos personalizados mediante cuadernos de Jupyter Notebook en la extensión Microsoft Sentinel Visual Studio Code.

Este artículo se centra en la creación manual de gráficos personalizados mediante código. Para obtener una experiencia basada en la inteligencia artificial, consulte Creación de gráficos personalizados asistidos por IA en Microsoft Sentinel.

Requisitos previos

Los siguientes son necesarios para crear gráficos personalizados en Microsoft Sentinel:

Habilite el conector de Microsoft Entra ID para ingerir las tablas de recursos de Microsoft Entra que se usan en el código de ejemplo de este artículo. Para obtener más información, consulte Ingesta de datos de activos en el lago de datos de Microsoft Sentinel.

Permissions

Para interactuar con gráficos personalizados, necesita los siguientes permisos XDR en Sentinel lago de datos. En la tabla siguiente se enumeran los requisitos de permisos para las operaciones comunes de grafos:

Operación de grafo Permisos necesarios
Modelar y compilar un gráfico de cuaderno Use un rol RBAC unificado de Microsoft Defender XDR personalizado con permisos de datos (administrar) a través de la recopilación de datos Microsoft Sentinel.
Conservar un grafo en el inquilino Use uno de los siguientes roles de Microsoft Entra ID:
Operador de seguridad
Administrador de seguridad
Administrador global
Consulta de un gráfico persistente Use un rol RBAC personalizado Microsoft Defender XDR unificado con permisos básicos de datos de seguridad (lectura) sobre la recopilación de datos Microsoft Sentinel.

Importante

Debe tener permisos para leer los datos usados en el gráfico. Si no tiene acceso a un conjunto de datos específico, esos datos no se incluirán en el gráfico. Para crear un gráfico, no debe estar restringido por un ámbito de Sentinel. Un usuario con ámbito no puede crear un gráfico personalizado.

Microsoft Entra ID roles proporcionan un acceso amplio a todas las áreas de trabajo del lago de datos. Para obtener más información, vea Roles y permisos en Microsoft Sentinel.

Instalar Visual Studio Code y la extensión Microsoft Sentinel

Cree gráficos personalizados mediante cuadernos de Jupyter Notebook en la extensión Microsoft Sentinel Visual Studio Code. Para obtener más información, vea Instalar Visual Studio Code y la extensión Microsoft Sentinel

Creación de un gráfico personalizado

Para crear y trabajar con gráficos personalizados, siga estos pasos:

  1. Modelar un gráfico personalizado
  2. Conservar el gráfico personalizado mediante la programación de un trabajo de grafo
  3. Visualización y administración de gráficos personalizados

Modelar un gráfico personalizado

Cree un gráfico personalizado mediante un cuaderno de Jupyter Notebook en la extensión Microsoft Sentinel Visual Studio Code.

Los pasos siguientes le guiarán a través de la creación del primer gráfico personalizado mediante un cuaderno de ejemplo.

Configuración del cuaderno y conexión al lago de datos

  1. En Visual Studio Code con la extensión Microsoft Sentinel instalada, seleccione el icono Microsoft Sentinel en el menú de la izquierda.

  2. Seleccione Iniciar sesión para ver los gráficos.

  3. Aparece un cuadro de diálogo con el texto La extensión "Microsoft Sentinel" quiere iniciar sesión con Microsoft. Seleccione Permitir para iniciar sesión.

  4. Inicie sesión con sus credenciales.

  5. Después de iniciar sesión, seleccione +Crear nuevo cuaderno.

  6. Asigne al archivo del cuaderno el nombre y guárdelo en una ubicación adecuada del área de trabajo.

    Captura de pantalla de la página de inicio de sesión de los gráficos de Visual Studio Code.

  7. Seleccione Seleccionar kernel en la parte superior derecha de la ventana del cuaderno para seleccionar un grupo de proceso de Spark.

  8. Seleccione Microsoft Sentinel y, a continuación, seleccione cualquiera de los grupos de Spark disponibles.

    Captura de pantalla de la página seleccionar kernel en Visual Studio Code.

    Sugerencia

    Puede usar solicitudes de inteligencia artificial para ayudarle a crear un cuaderno de grafos personalizado. Para obtener más información, consulte Creación de gráficos personalizados asistidos por IA en Microsoft Sentinel.

  9. Ejecute una celda a seleccionando el icono ejecutar triángulo de celda a la izquierda de la celda. La primera vez que ejecute una celda, es posible que se le pida que seleccione un kernel si aún no ha seleccionado una.

    La primera vez que ejecute una celda, tarda unos cinco minutos en iniciar la sesión de Spark.

    Captura de pantalla que muestra la ejecución de la primera celda de Visual Studio Code.

Creación de un gráfico

En el ejemplo siguiente se crea un gráfico para recorrer Microsoft Entra pertenencias a grupos y comprender las relaciones de grupo anidadas. El código de ejemplo le ayuda a empezar a usar un caso de uso sencillo para aprender la funcionalidad de gráfico personalizado y aprovechar la eficacia del recorrido de grafos para las investigaciones. Puede crear un gráfico a partir de cualquier tabla disponible en el lago de datos de Sentinel.

  1. Conéctese al área de trabajo y lea Entra tablas de recursos para empezar a compilar el gráfico.

    from pyspark.sql import functions as F
    from sentinel_lake.providers import MicrosoftSentinelProvider
    
    lake_provider = MicrosoftSentinelProvider(spark=spark)
    
    # Use the "System tables" workspace which contains the Entra* Assets tables
    # If you are data is in a different workspace, update this variable accordingly and ensure the tables are present
    LOG_ANALYTICS_WORKSPACE = "System tables"
    
    # Dynamically get the latest snapshot time from EntraUsers
    snapshot_time = (
        lake_provider.read_table("EntraUsers", LOG_ANALYTICS_WORKSPACE)
        .df.agg(F.max("_SnapshotTime").alias("max_snapshot"))
        .collect()[0]["max_snapshot"]
        .strftime("%Y-%m-%dT%H:%M:%SZ")
    )
    print(f"Using snapshot_time: {snapshot_time}")
    
    snapshot_filter = (F.col("_SnapshotTime") == F.lit(snapshot_time).cast("timestamp"))
    
    # Load EntraMembers - edges: group contains user/group/servicePrincipal
    df_members = (
        lake_provider.read_table("EntraMembers", LOG_ANALYTICS_WORKSPACE)
        .filter(
            snapshot_filter
            & (F.col("sourceType") == "group")
            & (F.col("targetType").isin("user", "group", "servicePrincipal"))
        )
    )
    
    # Load EntraGroups - nodes
    df_groups = (
        lake_provider.read_table("EntraGroups", LOG_ANALYTICS_WORKSPACE)
        .filter(snapshot_filter)
        .select("id", "displayName", "mailEnabled")
    )
    
    # Load EntraUsers - nodes
    df_users = (
        lake_provider.read_table("EntraUsers", LOG_ANALYTICS_WORKSPACE)
        .filter(snapshot_filter)
        .select("id", "accountEnabled", "displayName", "department",
                "lastPasswordChangeDateTime", "userPrincipalName", "usageLocation")
    )
    
    # Load EntraServicePrincipals - nodes
    df_service_principals = (
        lake_provider.read_table("EntraServicePrincipals", LOG_ANALYTICS_WORKSPACE)
        .filter(snapshot_filter)
        .select("accountEnabled", "id", "displayName", "servicePrincipalType",
                "tenantId", "organizationId")
    )
    
    # Fix for Spark 3.x Parquet datetime rebase issue. Required when reading Parquet files
    # written by Spark 2.x which used the Julian calendar, whereas Spark 3.x uses Proleptic
    # Gregorian. Without these settings, timestamp columns (e.g. lastPasswordChangeDateTime)
    # may throw errors or return incorrect values. Safe to remove if all data was written by
    # Spark 3.x (typical for current Fabric/Sentinel environments).
    spark.conf.set("spark.sql.parquet.datetimeRebaseModeInRead", "CORRECTED")
    spark.conf.set("spark.sql.parquet.datetimeRebaseModeInWrite", "CORRECTED")
    spark.conf.set("spark.sql.parquet.int96RebaseModeInRead", "CORRECTED")
    spark.conf.set("spark.sql.parquet.int96RebaseModeInWrite", "CORRECTED")
    
  2. Preparación de los dataframes perimetrales y de nodo necesarios para compilar el gráfico

    # ============================================================
    # NODE PREPARATION
    # ============================================================
    
    # EntraUser nodes - keyed by user id
    user_nodes = (
        df_users.df
        .select(
            F.col("id"),
            F.col("displayName"),
            F.col("accountEnabled"),
            F.col("department"),
            F.col("lastPasswordChangeDateTime"),
            F.col("userPrincipalName"),
            F.col("usageLocation")
        )
    )
    
    # EntraGroup nodes - keyed by group id
    group_nodes = (
        df_groups.df
        .select(
            F.col("id"),
            F.col("displayName"),
            F.col("mailEnabled")
        )
    )
    
    # EntraServicePrincipal nodes - keyed by SP id
    sp_nodes = (
        df_service_principals.df
        .select(
            F.col("id"),
            F.col("displayName"),
            F.col("accountEnabled"),
            F.col("servicePrincipalType"),
            F.col("tenantId"),
            F.col("organizationId")
        )
    )
    
    # ============================================================
    # EDGE PREPARATION
    # ============================================================
    
    # Edge: EntraGroup --Contains--> EntraUser
    edge_group_contains_user = (
        df_members.df
        .filter(F.col("targetType") == "user")
        .select(
            F.col("sourceId").alias("SourceGroupId"),
            F.col("targetId").alias("TargetUserId")
        )
        .distinct()
        .withColumn("EdgeKey", F.concat_ws("_", F.col("SourceGroupId"), F.col("TargetUserId")))
    )
    
    # Edge: EntraGroup --Contains--> EntraGroup (nested groups)
    edge_group_contains_group = (
        df_members.df
        .filter(F.col("targetType") == "group")
        .select(
            F.col("sourceId").alias("SourceGroupId"),
            F.col("targetId").alias("TargetGroupId")
        )
        .distinct()
        .withColumn("EdgeKey", F.concat_ws("_", F.col("SourceGroupId"), F.col("TargetGroupId")))
    )
    
    # Edge: EntraGroup --Contains--> EntraServicePrincipal
    edge_group_contains_sp = (
        df_members.df
        .filter(F.col("targetType") == "servicePrincipal")
        .select(
            F.col("sourceId").alias("SourceGroupId"),
            F.col("targetId").alias("TargetSPId")
        )
        .distinct()
        .withColumn("EdgeKey", F.concat_ws("_", F.col("SourceGroupId"), F.col("TargetSPId")))
    )
    
  3. Definir el esquema del grafo y enlazar a los dataframes creados en el paso anterior

    from sentinel_graph import GraphSpecBuilder, Graph
    
    # Define the graph schema 
    
    entra_group_graph_spec = (
        GraphSpecBuilder.start()
    
        # === NODES ===
    
        .add_node("EntraUser")
        .from_dataframe(user_nodes)  # Native Spark DataFrame (from .df + .select + .distinct)
        .with_columns(
            "id", "displayName", "accountEnabled",
            "department", "lastPasswordChangeDateTime", "userPrincipalName", "usageLocation",
            key="id", display="displayName"
        )
    
        .add_node("EntraGroup")
        .from_dataframe(group_nodes)  # Native Spark DataFrame
        .with_columns(
            "id", "displayName", "mailEnabled",
            key="id", display="displayName"
        )
    
        .add_node("EntraServicePrincipal")
        .from_dataframe(sp_nodes)  # Native Spark DataFrame
        .with_columns(
            "id", "displayName", "accountEnabled",
            "servicePrincipalType", "tenantId", "organizationId",
            key="id", display="displayName"
        )
    
        # === EDGES ===
    
        # EntraGroup --ContainsUser--> EntraUser
        .add_edge("ContainsUser")
        .from_dataframe(edge_group_contains_user)  # Native Spark DataFrame
        .source(id_column="SourceGroupId", node_type="EntraGroup")
        .target(id_column="TargetUserId", node_type="EntraUser")
        .with_columns("SourceGroupId", "TargetUserId", "EdgeKey",
                      key="EdgeKey", display="EdgeKey")
    
        # EntraGroup --ContainsGroup--> EntraGroup (nested groups)
        .add_edge("ContainsGroup")
        .from_dataframe(edge_group_contains_group)  # Native Spark DataFrame
        .source(id_column="SourceGroupId", node_type="EntraGroup")
        .target(id_column="TargetGroupId", node_type="EntraGroup")
        .with_columns("SourceGroupId", "TargetGroupId", "EdgeKey",
                      key="EdgeKey", display="EdgeKey")
    
        # EntraGroup --ContainsServicePrincipal--> EntraServicePrincipal
        .add_edge("ContainsServicePrincipal")
        .from_dataframe(edge_group_contains_sp)  # Native Spark DataFrame
        .source(id_column="SourceGroupId", node_type="EntraGroup")
        .target(id_column="TargetSPId", node_type="EntraServicePrincipal")
        .with_columns("SourceGroupId", "TargetSPId", "EdgeKey",
                      key="EdgeKey", display="EdgeKey")
    
    ).done()
    
  4. Validación del esquema del grafo

    # Check the schema of the graph spec to ensure it's correct
    entra_group_graph_spec.show_schema()
    
  5. Compilación del gráfico, incluida la preparación de los datos y la publicación del gráfico

    # Build the graph from the spec - this will validate the spec and prepare it for querying
    # Alter options is to use Graph.prepare() to prepare the graph nodes and edges in the lake
    # and then use Graph.publish() to create the graph. You would typically call prepare() and publish()
    # seperately to understand the cost of Graph API calls that are triggeterd by Graph.publish()
    # see https://learn.microsoft.com/azure/sentinel/billing?tabs=simplified%2Ccommitment-tiers
    intra_group_graph = Graph.build(entra_group_graph_spec)
    

    Nota:

    Los gráficos creados durante las sesiones interactivas de cuadernos se quitan cuando se cierra la sesión del cuaderno. Para conservar el gráfico para su reutilización y uso compartido, consulte Conservación del gráfico personalizado.

Ahora ha creado un gráfico en el cuaderno.

Para mostrar una representación visual del gráfico, en una nueva celda pegue y ejecute el código siguiente:

# Query 1: Find nested group relationships nexting up to 8 levels deep
# Update the Entra Group name that you want to traverse from
query_nested_groups = """
MATCH p=(g1:EntraGroup)-[cg]->{1,8}(g2)
WHERE g1.displayName = 'tmplevel3'
RETURN *
"""
intra_group_graph.query(query_nested_groups).show()

Este código ejecuta una consulta de ejemplo del Lenguaje de consulta de Graph (GQL) para recuperar toda la pertenencia a grupos anidados hasta 8 niveles de profundidad El gráfico resultante se visualiza en la salida.

Captura de pantalla que muestra la visualización de un gráfico en Visual Studio Code.

Conservar el gráfico personalizado

Una vez creado el código de grafo en el cuaderno, puede ejecutar el cuaderno en una sesión interactiva o programar un trabajo de grafo. Los gráficos creados durante la sesión interactiva del cuaderno son temporales y solo están disponibles en el contexto de la sesión del cuaderno. Para guardar el gráfico y compartirlo con su equipo, programe un trabajo de grafo para volver a generar el gráfico con frecuencia. Una vez guardado el gráfico, es accesible desde: la experiencia del grafo en Microsoft Defender portal en Sentinel, Visual Studio Code Notebooks y graph query API.

  1. En el cuaderno de grafos, seleccione Crear trabajo programado y, a continuación, seleccione Crear un trabajo de grafo.

    Captura de pantalla que muestra el botón Crear trabajo programado en un cuaderno de grafos.

  2. En el formulario Crear trabajo de grafo , escriba el nombre del gráfico y la descripción, y compruebe que el cuaderno de grafos correcto se incluye en Ruta de acceso.

  3. Para compilar el gráfico sin configurar una programación de actualización, seleccione A petición en la sección Programación y, a continuación, seleccione Enviar para crear el gráfico.

    Nota:

    Los gráficos creados mediante la programación a petición tienen una retención predeterminada de 30 días y se eliminan al expirar.

  4. Para compilar el gráfico en el que los datos del grafo se actualizan periódicamente, seleccione Programado en la sección Programación .

    1. Seleccione repetir frecuencia para el trabajo. Puede elegir entre Por minuto, Por hora, Semanal, Diario o Mensual.

    2. Se muestran más opciones para configurar la programación, en función de la frecuencia que seleccione. Por ejemplo, día de la semana, hora del día o día del mes.

    3. Seleccione una hora de inicio para que la programación empiece a ejecutarse.

    4. Seleccione una hora de finalización para que la programación deje de ejecutarse. Si no desea establecer una hora de finalización para la programación, seleccione Establecer trabajo para que se ejecute indefinidamente. Las fechas y horas están en la zona horaria.

    5. Seleccione Enviar para guardar la configuración del trabajo y publicar el trabajo. El proceso de creación de gráficos se inicia en el inquilino. Vea el gráfico recién creado y su estado más reciente en la extensión Sentinel.

    Captura de pantalla de la página Crear trabajo de grafo.

Visualización y administración de gráficos personalizados

Después de crear un trabajo de grafo, puede ver y administrar el grafo en el inquilino desde la extensión Microsoft Sentinel en Visual Studio Code.

  1. En la lista de gráficos, seleccione el gráfico materializado para ver sus detalles.

  2. Seleccione la pestaña Detalles del trabajo para ver el estado del trabajo de grafo, incluido el tiempo de última ejecución, el siguiente tiempo de ejecución y los errores detectados durante el proceso de compilación.

  3. Seleccione Ejecutar ahora para desencadenar manualmente una compilación de grafos fuera de las horas programadas. El estado cambia a En cola y, a continuación, "En curso" mientras se compila el gráfico.

    Captura de pantalla que muestra la pestaña de detalles del trabajo de grafo en Visual Studio Code.

  4. Una vez completada la compilación del grafo, el estado se actualiza a Listo. Seleccione la pestaña Detalles del gráfico para ver información sobre el gráfico.

    Captura de pantalla de la pestaña detalles del gráfico.

  5. Ahora puede consultar y visualizar el gráfico desde la visualización del gráfico en Microsoft Sentinel en el portal de Defender. Para obtener más información, consulte Visualización de gráficos en Microsoft Sentinel gráfico (versión preliminar).