Начало работы с пользовательскими графами в Microsoft Sentinel (предварительная версия)

Настраиваемые графы в Microsoft Sentinel позволяют специалистам и аналитикам по безопасности создавать адаптированные под их потребности графовые представления данных о безопасности. Создавая пользовательские графы, вы можете моделировать конкретные шаблоны атак, исследовать угрозы и запускать расширенные алгоритмы графов, чтобы выявить скрытые связи в цифровой среде. В этом руководстве описано, как создавать пользовательские графы и управлять ими с помощью записных книжек Jupyter в расширении Microsoft Sentinel Visual Studio Code.

Эта статья посвящена созданию пользовательских графов вручную с помощью кода. Инструкции по работе с ИИ см. в статье Создание настраиваемых графов с помощью ИИ в Microsoft Sentinel.

Предварительные условия

Для создания пользовательских графов в Microsoft Sentinel требуется следующее:

Включите коннектор Microsoft Entra ID, чтобы загрузить таблицы ресурсов Microsoft Entra, используемые в примере кода в этой статье. Дополнительные сведения см. в разделе Прием данных об активах в озере данных Microsoft Sentinel.

Разрешения

Для взаимодействия с пользовательскими графами вам потребуются следующие разрешения XDR в озере данных Sentinel. В следующей таблице перечислены требования к разрешениям для распространенных операций графа.

Операция с графом Необходимые разрешения
Моделирование и создание графа записной книжки Используйте настраиваемую унифицированную роль RBAC Microsoft Defender XDR с разрешениями данные (управление) для сбора данных Microsoft Sentinel.
Сохранить граф в арендаторе Используйте одну из следующих Microsoft Entra ID ролей:
Оператор безопасности
Администратор безопасности
Глобальный администратор
Запрос сохраняемого графа Используйте настраиваемую унифицированную роль RBAC Microsoft Defender XDR с разрешениями "Основные данные безопасности (чтение)" для сбора данных Microsoft Sentinel.

Важно!

Для чтения данных, используемых в графе, необходимо иметь разрешения. Если у вас нет доступа к определенному набору данных, эти данные не будут включены в граф. Чтобы создать граф, вы не должны быть ограничены областью Sentinel. Пользователь с ограниченной областью доступа не может создать настраиваемый график.

Роли Microsoft Entra ID обеспечивают широкий доступ ко всем рабочим областям в озере данных. Дополнительные сведения см. в разделе Роли и разрешения в Microsoft Sentinel.

Установка Visual Studio Code и расширения Microsoft Sentinel

Создавайте пользовательские диаграммы с помощью записных книжек Jupyter в расширении Microsoft Sentinel для Visual Studio Code. Дополнительные сведения см. в разделе Установка Visual Studio Code и расширение Microsoft Sentinel.

Создание пользовательского графа

Чтобы создать пользовательские графы и работать с ними, выполните следующие действия.

  1. Моделирование пользовательского графа
  2. Сохранить пользовательский граф, запланировав задание для графа
  3. Просмотр пользовательских графов и управление ими

Моделирование пользовательского графа

Создайте пользовательский граф с помощью записной книжки Jupyter в расширении Microsoft Sentinel Visual Studio Code.

Ниже приведены инструкции по созданию первого пользовательского графа с помощью примера записной книжки.

Настройте блокнот и подключитесь к озеру данных

Выполните следующие действия, чтобы создать записную книжку и подключиться к озеру данных Microsoft Sentinel.

  1. В Visual Studio Code с установленным расширением Microsoft Sentinel щелкните значок Microsoft Sentinel в меню слева.

  2. Выберите Войти, чтобы просмотреть графики.

  3. Появится диалоговое окно с текстом Расширение "Microsoft Sentinel" хочет выполнить вход с помощью Microsoft. Выберите Разрешить , чтобы войти.

  4. Войдите с учетными данными.

  5. После входа выберите +Создать записную книжку.

  6. Присвойте файлу записной книжки имя и сохраните его в соответствующем расположении в рабочей области.

    Снимок экрана: страница входа для графиков в Visual Studio Code.

  7. Выберите Выбрать ядро в правом верхнем углу окна записной книжки, чтобы выбрать пул вычислений Spark.

  8. Выберите Microsoft Sentinel, а затем выберите любой из доступных пулов Spark.

    Снимок экрана: страница выбора ядра в Visual Studio Code.

    Совет

    Вы можете использовать запросы к ИИ, чтобы создать собственный блокнот графов. Дополнительные сведения см. в статье Создание пользовательских графов с помощью ИИ в Microsoft Sentinel.

  9. Запустите ячейку, нажав значок запуска ячейки в виде треугольника слева от неё. При первом запуске ячейки может появиться запрос на выбор ядра, если оно еще не выбрано.

    При первом запуске ячейки запуск сеанса Spark занимает около пяти минут.

    Снимок экрана: запуск первой ячейки в Visual Studio Code.

Создание графа

В следующем примере создаётся граф для обхода членства в группах Microsoft Entra и анализа связей между вложенными группами. Пример кода поможет вам начать работу с простым сценарием использования, чтобы изучить функции настраиваемого графа и использовать возможности обхода графа в ходе расследований. Вы можете создать диаграмму из любой таблицы, доступной в озере данных Sentinel.

  1. Подключитесь к рабочей области и считывайте таблицы ресурсов Entra, чтобы начать построение графа.

    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. Подготовьте таблицы DataFrame для узлов и рёбер, необходимые для построения графа

    # ============================================================
    # 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. Определите схему графа и свяжите её с таблицами DataFrame для узлов и рёбер, которые вы ранее подготовили в ходе этой процедуры

    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. Проверка схемы графа

    # Check the schema of the graph spec to ensure it's correct
    entra_group_graph_spec.show_schema()
    
  5. Создание графа, включая подготовку данных и публикацию графа

    # 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)
    

    Примечание.

    Графы, созданные во время интерактивных сеансов записных книжек, удаляются при закрытии сеанса записной книжки. Чтобы сохранить граф для повторного использования и общего доступа, см. Сохранение пользовательского графа

Теперь вы создали граф в записной книжке.

Следующий пример на Python запускает запрос Graph Query Language (GQL) к только что созданному вами графу для изучения вложенных групповых отношений. Используйте такие запросы после создания графа, чтобы проходить иерархии членства и обнаруживать пути косвенного доступа. Чтобы попробовать, вставьте код в новую ячейку блокнота и запустите его. Этот пример запроса охватывает вложенные группы Microsoft Entra до восьми уровней глубины и отображает полученные отношения:

# 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()

Этот код выполняет пример запроса на языке Graph Query Language (GQL), чтобы получить все членства во вложенных группах до 8 уровней вложенности. Результирующий граф визуализируется в выводе

Снимок экрана: визуализация графа в Visual Studio Code.

Сохраните пользовательский граф

После создания кода графа в записной книжке можно запустить записную книжку в интерактивном сеансе или запланировать задание графа. Графики, созданные во время интерактивного сеанса записной книжки, являются временными и доступны только в контексте сеанса записной книжки. Чтобы сохранить граф и поделиться им с командой, запланируйте задачу, чтобы граф регулярно перестраивался. После сохранения граф будет доступен в: интерфейсе Graph на портале Microsoft Defender в разделе Sentinel, записных книжках Visual Studio Code и API запросов к Graph.

  1. В записной книжке графов выберите Создать запланированное задание, а затем — Создать задание для графа.

    Снимок экрана с кнопкой создания задания по расписанию в блокноте графа.

  2. В форме Создать задание графа введите Имя графа и Описание, и убедитесь, что в поле Путь указана правильная записная книжка графа.

  3. Чтобы создать граф без настройки расписания обновления, выберите По запросу в разделе Расписание , а затем нажмите кнопку Отправить , чтобы создать граф.

    Примечание.

    Графики, созданные по расписанию по запросу, по умолчанию хранятся в течение 30 дней и удаляются по истечении срока действия.

  4. Чтобы создать граф, на котором данные графа регулярно обновляются, выберите Запланированные в разделе Расписание .

    1. Выберите частоту повтора для задания. Вы можете выбрать по минутам, по часам, еженедельно, ежедневно или ежемесячно.

    2. В зависимости от выбранной частоты отображаются дополнительные параметры для настройки расписания. Например, день недели, время суток или день месяца.

    3. Выберите время начала, чтобы расписание начало выполняться.

    4. Выберите Время окончания, чтобы расписание перестало выполняться. Если вы не хотите задавать время окончания для расписания, выберите Задать задание для неограниченного выполнения. Даты и время указаны в вашем часовом поясе.

    5. Нажмите кнопку Отправить , чтобы сохранить конфигурацию задания и опубликовать задание. Процесс построения графа начинается в вашем арендаторе. Просмотрите только что созданный граф и его последнее состояние в расширении Sentinel.

    Снимок экрана: страница создания задания графа.

Просмотр пользовательских графов и управление ими

После создания задания графа вы можете просматривать граф в клиенте и управлять им с помощью расширения Microsoft Sentinel в Visual Studio Code.

  1. В списке графов выберите материализованный граф, чтобы просмотреть сведения о нем.

  2. Перейдите на вкладку Сведения о задании , чтобы просмотреть состояние задания графа, включая время последнего выполнения, время следующего выполнения и все ошибки, возникшие в процессе сборки.

  3. Выберите Запустить сейчас , чтобы вручную активировать сборку графа вне запланированного времени. Состояние меняется на В очереди, а затем на «Выполняется», пока строится граф.

    Снимок экрана, показывающий вкладку сведений о задании графа в Visual Studio Code.

  4. По завершении сборки графа состояние обновится на Готово. Перейдите на вкладку Сведения о графе , чтобы просмотреть сведения о графе.

    Снимок экрана: вкладка сведений о графе.

  5. Теперь вы можете запрашивать и визуализировать граф из визуализации графа в Microsoft Sentinel на портале Defender. Дополнительные сведения см. в статье Визуализация графов в Graph Microsoft Sentinel (предварительная версия).

Обновление пользовательских графов

После обновления кода графа в блокноте запустите блокнот локально, чтобы проверить внесённые изменения перед обновлением задания графа.

Изменить параметры существующего настраиваемого графика

Этот рабочий процесс используется для изменения существующего задания графа.

  1. На панели Graph Microsoft Sentinel в Visual Studio Code выберите Изменить задание графа для графа, который требуется обновить, а затем перейдите к Имя графа>Сведения о задании.

  2. Обновите поля, которые нужно изменить:

    • Имя графа (необязательно). При изменении имени графа в вашем тенанте создается новый граф. Существующий граф остается неизменным.
    • Описание (необязательно). Обновление описания заменяет текущее описание графа и перестраивает граф.
    • Конфигурация кластера (необязательно). Обновление конфигурации кластера изменяет размер вычислительных ресурсов и перестраивает граф.
    • Частота задания (необязательно). Обновление частоты задания изменяет повторяющееся расписание и перестраивает граф.

Снимок экрана, на котором показан рабочий процесс задания Edit graph на панели Microsoft Sentinel Graph в Visual Studio Code.

Перезаписать существующий пользовательский граф

Используйте этот рабочий процесс, если вы хотите повторно создать граф и перезаписать существующий граф с тем же именем.

  1. Откройте обновленную записную книжку.

  2. Выберите Создать запланированное задание в интерфейсе записных книжек Sentinel, затем выберите Создать задание для графа.

  3. В форме создания графа укажите имя графа.

    • Если указать новое имя графа, в клиенте создается новый граф, а существующий граф остается доступным.
    • Если вы повторно используете существующее имя графа для клиента, существующий граф перезаписывается после подтверждения. Имена графов являются уникальными, повторяющиеся имена графов не поддерживаются.
  4. Убедитесь, что Path указывает на ваш обновлённый блокнот.

  5. Обновите описание графа.

  6. Выберите конфигурацию кластера и частоту заданий, аналогичную созданию нового графа.

  7. При необходимости обновите сведения о расписании .

  8. При необходимости обновите поле описания .

  9. Выберите Submit, чтобы заново создать граф в вашем тенанте.

Снимок экрана: форма создания графа, используемая для перезаписи существующего настраиваемого графа в Visual Studio Code.