Back
Idf6c76cc9-218c-5b76-9b82-8607f09ea1b4
RulenameGreyNoise TI Map IP Entity to SigninLogs
DescriptionThis query maps any GreyNoise IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.
SeverityMedium
TacticsCommandAndControl
TechniquesT1071
Required data connectorsAzureActiveDirectory
GreyNoise2SentinelAPI
MicrosoftDefenderThreatIntelligence
ThreatIntelligence
ThreatIntelligenceTaxii
KindScheduled
Query frequency1h
Query period14d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/GreyNoiseThreatIntelligence/Analytic%20Rules/GreyNoise_IPEntity_SigninLogs.yaml
Version1.0.2
Arm templatef6c76cc9-218c-5b76-9b82-8607f09ea1b4.json
Deploy To Azure
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
// Fetch GreyNoise IPv4 indicators from the ThreatIntelIndicators (STIX 2.1) table.
// GreyNoise only publishes ipv4-addr patterns, so a single ObservableKey filter is sufficient.
// Materialized because the union below evaluates this branch twice.
let GreyNoise_IPs = materialize (
  ThreatIntelIndicators
  | where TimeGenerated >= ago(ioc_lookBack)
  | where SourceSystem == 'GreyNoise'
  | where ObservableKey == 'ipv4-addr:value'
  // Take the latest row per indicator first, then evaluate its current state, so an
  // indicator that has since been deactivated cannot be resurrected by an older row.
  | summarize arg_max(TimeGenerated, *) by Id
  | where IsActive == true and IsDeleted == false and ValidUntil > now()
  | extend TI_ipEntity = ObservableValue
  | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
  // Explicit projection keeps TimeGenerated and Type off the left side of the join, avoiding column collisions.
  | project IndicatorId = Id, TI_ipEntity, ThreatType = tostring(Data.indicator_types[0]), IoCDescription = tostring(Data.description), Tags, Confidence, ValidFrom, ValidUntil
);
let aadFunc = (tableName:string){
GreyNoise_IPs
// using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
| join kind=innerunique (
    table(tableName) | where TimeGenerated >= ago(dt_lookBack)
    | extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)
    | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails), StatusReason = tostring(Status.failureReason)
    | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
    // renaming time column so it is clear the log this came from
    | extend SigninLogs_TimeGenerated = TimeGenerated
)
on $left.TI_ipEntity == $right.IPAddress
| where SigninLogs_TimeGenerated < ValidUntil
| summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by IndicatorId, IPAddress
| project SigninLogs_TimeGenerated, IoCDescription, IndicatorId, ThreatType, ValidFrom, ValidUntil, Confidence, Tags,
TI_ipEntity, IPAddress, UserPrincipalName, AppDisplayName, StatusCode, StatusDetails, StatusReason, Type
| extend timestamp = SigninLogs_TimeGenerated, Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt
name: GreyNoise TI Map IP Entity to SigninLogs
triggerOperator: gt
kind: Scheduled
queryFrequency: 1h
description: |
  'This query maps any GreyNoise IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.'
id: f6c76cc9-218c-5b76-9b82-8607f09ea1b4
triggerThreshold: 0
queryPeriod: 14d
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  // Fetch GreyNoise IPv4 indicators from the ThreatIntelIndicators (STIX 2.1) table.
  // GreyNoise only publishes ipv4-addr patterns, so a single ObservableKey filter is sufficient.
  // Materialized because the union below evaluates this branch twice.
  let GreyNoise_IPs = materialize (
    ThreatIntelIndicators
    | where TimeGenerated >= ago(ioc_lookBack)
    | where SourceSystem == 'GreyNoise'
    | where ObservableKey == 'ipv4-addr:value'
    // Take the latest row per indicator first, then evaluate its current state, so an
    // indicator that has since been deactivated cannot be resurrected by an older row.
    | summarize arg_max(TimeGenerated, *) by Id
    | where IsActive == true and IsDeleted == false and ValidUntil > now()
    | extend TI_ipEntity = ObservableValue
    | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    // Explicit projection keeps TimeGenerated and Type off the left side of the join, avoiding column collisions.
    | project IndicatorId = Id, TI_ipEntity, ThreatType = tostring(Data.indicator_types[0]), IoCDescription = tostring(Data.description), Tags, Confidence, ValidFrom, ValidUntil
  );
  let aadFunc = (tableName:string){
  GreyNoise_IPs
  // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (
      table(tableName) | where TimeGenerated >= ago(dt_lookBack)
      | extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)
      | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails), StatusReason = tostring(Status.failureReason)
      | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
      // renaming time column so it is clear the log this came from
      | extend SigninLogs_TimeGenerated = TimeGenerated
  )
  on $left.TI_ipEntity == $right.IPAddress
  | where SigninLogs_TimeGenerated < ValidUntil
  | summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by IndicatorId, IPAddress
  | project SigninLogs_TimeGenerated, IoCDescription, IndicatorId, ThreatType, ValidFrom, ValidUntil, Confidence, Tags,
  TI_ipEntity, IPAddress, UserPrincipalName, AppDisplayName, StatusCode, StatusDetails, StatusReason, Type
  | extend timestamp = SigninLogs_TimeGenerated, Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
version: 1.0.2
requiredDataConnectors:
- connectorId: ThreatIntelligence
  dataTypes:
  - ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
  dataTypes:
  - ThreatIntelIndicators
- connectorId: AzureActiveDirectory
  dataTypes:
  - SigninLogs
- connectorId: AzureActiveDirectory
  dataTypes:
  - AADNonInteractiveUserSignInLogs
- connectorId: MicrosoftDefenderThreatIntelligence
  dataTypes:
  - ThreatIntelIndicators
- connectorId: GreyNoise2SentinelAPI
  dataTypes:
  - ThreatIntelIndicators
severity: Medium
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/GreyNoiseThreatIntelligence/Analytic%20Rules/GreyNoise_IPEntity_SigninLogs.yaml
relevantTechniques:
- T1071
tactics:
- CommandAndControl
entityMappings:
- fieldMappings:
  - identifier: Name
    columnName: Name
  - identifier: UPNSuffix
    columnName: UPNSuffix
  entityType: Account
- fieldMappings:
  - identifier: Address
    columnName: IPAddress
  entityType: IP
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "workspace": {
      "type": "String"
    }
  },
  "resources": [
    {
      "apiVersion": "2024-01-01-preview",
      "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/f6c76cc9-218c-5b76-9b82-8607f09ea1b4')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/f6c76cc9-218c-5b76-9b82-8607f09ea1b4')]",
      "properties": {
        "alertRuleTemplateName": "f6c76cc9-218c-5b76-9b82-8607f09ea1b4",
        "customDetails": null,
        "description": "'This query maps any GreyNoise IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.'\n",
        "displayName": "GreyNoise TI Map IP Entity to SigninLogs",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "Name",
                "identifier": "Name"
              },
              {
                "columnName": "UPNSuffix",
                "identifier": "UPNSuffix"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "IPAddress",
                "identifier": "Address"
              }
            ]
          }
        ],
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/GreyNoiseThreatIntelligence/Analytic%20Rules/GreyNoise_IPEntity_SigninLogs.yaml",
        "query": "let dt_lookBack = 1h;\nlet ioc_lookBack = 14d;\n// Fetch GreyNoise IPv4 indicators from the ThreatIntelIndicators (STIX 2.1) table.\n// GreyNoise only publishes ipv4-addr patterns, so a single ObservableKey filter is sufficient.\n// Materialized because the union below evaluates this branch twice.\nlet GreyNoise_IPs = materialize (\n  ThreatIntelIndicators\n  | where TimeGenerated >= ago(ioc_lookBack)\n  | where SourceSystem == 'GreyNoise'\n  | where ObservableKey == 'ipv4-addr:value'\n  // Take the latest row per indicator first, then evaluate its current state, so an\n  // indicator that has since been deactivated cannot be resurrected by an older row.\n  | summarize arg_max(TimeGenerated, *) by Id\n  | where IsActive == true and IsDeleted == false and ValidUntil > now()\n  | extend TI_ipEntity = ObservableValue\n  | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith \"fe80\" and TI_ipEntity !startswith \"::\" and TI_ipEntity !startswith \"127.\"\n  // Explicit projection keeps TimeGenerated and Type off the left side of the join, avoiding column collisions.\n  | project IndicatorId = Id, TI_ipEntity, ThreatType = tostring(Data.indicator_types[0]), IoCDescription = tostring(Data.description), Tags, Confidence, ValidFrom, ValidUntil\n);\nlet aadFunc = (tableName:string){\nGreyNoise_IPs\n// using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated\n| join kind=innerunique (\n    table(tableName) | where TimeGenerated >= ago(dt_lookBack)\n    | extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)\n    | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails), StatusReason = tostring(Status.failureReason)\n    | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)\n    // renaming time column so it is clear the log this came from\n    | extend SigninLogs_TimeGenerated = TimeGenerated\n)\non $left.TI_ipEntity == $right.IPAddress\n| where SigninLogs_TimeGenerated < ValidUntil\n| summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by IndicatorId, IPAddress\n| project SigninLogs_TimeGenerated, IoCDescription, IndicatorId, ThreatType, ValidFrom, ValidUntil, Confidence, Tags,\nTI_ipEntity, IPAddress, UserPrincipalName, AppDisplayName, StatusCode, StatusDetails, StatusReason, Type\n| extend timestamp = SigninLogs_TimeGenerated, Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])\n};\nlet aadSignin = aadFunc(\"SigninLogs\");\nlet aadNonInt = aadFunc(\"AADNonInteractiveUserSignInLogs\");\nunion isfuzzy=true aadSignin, aadNonInt\n",
        "queryFrequency": "PT1H",
        "queryPeriod": "P14D",
        "severity": "Medium",
        "subTechniques": [],
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "CommandAndControl"
        ],
        "techniques": [
          "T1071"
        ],
        "templateVersion": "1.0.2",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}