{
  "$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/a7b167e2-9582-440b-8d47-53df2f73c4f4')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/a7b167e2-9582-440b-8d47-53df2f73c4f4')]",
      "properties": {
        "alertRuleTemplateName": "a7b167e2-9582-440b-8d47-53df2f73c4f4",
        "customDetails": {
          "Accesses": "Accesses",
          "AccountNames": "AccountNames",
          "Folder": "Folder",
          "SecretKey": "SecretKey",
          "SecretName": "SecretName",
          "TargetServers": "TargetServers"
        },
        "description": "Detects when a user accesses a high-value secret for the first time, with no access to that\nsame secret by that user in the prior 14 days. First-time access to a sensitive secret can\nindicate lateral movement or credential targeting.\nDelinea reports no in-band sensitivity flag, so a secret is treated as high-value when its\nname or its folder matches 'sensitivePattern' (common privileged naming conventions), or\nwhen it appears in 'customSecretNames' / 'customFolders'. Both knobs are meant to be edited\nat deployment time - the pattern for substring matching, the lists for exact tenant-specific\nnames. The folder is read from the notes document, since the platform leaves the top-level\ntarget.containerName empty on secret events.\n",
        "displayName": "Delinea - Sensitive secret accessed by new user (first access in 14 days)",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "Account",
            "fieldMappings": [
              {
                "columnName": "Actor",
                "identifier": "Name"
              },
              {
                "columnName": "ActorEmail",
                "identifier": "FullName"
              }
            ]
          },
          {
            "entityType": "File",
            "fieldMappings": [
              {
                "columnName": "SecretName",
                "identifier": "Name"
              },
              {
                "columnName": "Folder",
                "identifier": "Directory"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "SrcIP",
                "identifier": "Address"
              }
            ]
          },
          {
            "entityType": "Host",
            "fieldMappings": [
              {
                "columnName": "TargetHost",
                "identifier": "HostName"
              }
            ]
          },
          {
            "entityType": "IP",
            "fieldMappings": [
              {
                "columnName": "TargetIP",
                "identifier": "Address"
              }
            ]
          }
        ],
        "eventGroupingSettings": {
          "aggregationKind": "AlertPerResult"
        },
        "incidentConfiguration": {
          "createIncident": true,
          "groupingConfiguration": {
            "enabled": true,
            "groupByEntities": [
              "Account"
            ],
            "lookbackDuration": "P1D",
            "matchingMethod": "Selected",
            "reopenClosedIncident": false
          }
        },
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Delinea%20Platform/Analytic%20Rules/DelineaSensitiveSecretNewUser.yaml",
        "query": "let detectionWindow = 1h;   // must equal queryFrequency\nlet baseline = 14d;         // must stay <= queryPeriod\n// Delinea exposes no in-band sensitivity flag - tags and analyticData are null on every\n// secret event - so value is inferred from the secret name and its folder. Secret Server\n// permissions are inherited from folder placement, which makes containerName the strongest\n// tiering signal, and it exists only inside notes: top-level target.containerName is null.\n//\n// TUNING 1 of 2: common high-value naming conventions, matched as a case insensitive\n// substring against both the secret name and its folder. Trim groups that are noisy in\n// your tenant; add cloud and CI terms (azure, aws, gcp, jenkins, ansible, terraform) only\n// if those secrets are genuinely privileged, as they match broadly.\nlet sensitivePattern = @\"(?i)admin|administrator|\\broot\\b|sudo|superuser|breakglass|break.?glass|emergency|privileged|\\bpriv\\b|\\bda\\b|\\bea\\b|domain.?admin|enterprise.?admin|\\bsa[-_]|\\bsvc[-_]|service.?account|\\bprod\\b|production|\\bprd\\b|backup|restore|disaster|\\bdr\\b|domain.?controller|\\bdc\\d|firewall|router|esxi|vcenter|hypervisor|\\bsql\\b|mysql|mssql|postgres|oracle|mongo|\\bdba\\b|\\bkey\\b|api.?key|keystore|vault|token|credential|password|passwd|\\bpwd\\b|cert\\b|certificate|\\bpki\\b|ssh|rdp|winrm\";\n// TUNING 2 of 2: tenant specific names, filled in at deployment time. Exact match, case\n// insensitive - use sensitivePattern above for substring matching. Leave empty if unused.\nlet customSecretNames = dynamic([]);\n//   e.g. dynamic([\"Break Glass Account\", \"ESXi root\", \"SAP_PROD_ADMIN\"]);\nlet customFolders = dynamic([]);\n//   e.g. dynamic([\"Tier 0\", \"Domain Admins\", \"Production Databases\"]);\nlet access =\n    DelineaAuditEvents_CL\n    | where TimeGenerated > ago(baseline)\n    | extend IngestedAt = ingestion_time()\n    | extend d = parse_json(RawData)\n    | where tostring(d.eventType.name) == \"Delinea.Vault.Secret.Viewed\"\n    // notes is a JSON document carried as a string - same idiom as\n    // Workbooks/DelineaSecretServerVaultActivity.json\n    | extend n = parse_json(tostring(d.notes))\n    | extend Actor = tostring(coalesce(d.actor.name, d.actor.email, d.actor.displayName)),\n             ActorEmail = tostring(d.actor.email),\n             SecretKey = tostring(coalesce(d.target.id, d.target.platformId, n.itemId)),\n             // Trimmed because these become entity identifiers - Delinea returns names\n             // such as '10.60.25.30 MYSQL ' with trailing whitespace.\n             SecretName = trim(@'\\s', coalesce(tostring(d.target.displayName), tostring(d.target.name),\n                                   tostring(n.itemNameForDisplay), tostring(n.itemName), SecretId)),\n             Folder = trim(@'\\s', tostring(n.containerName)),\n             Details = tostring(n.eventDetails),\n             RawSrcIP = tostring(coalesce(d.source.host.network.ipAddress, n.ipAddress))\n    // eventDetails is free text: \" Account Name: qa-e2e-target  Target Server: 10.60.25.51 \"\n    | extend AccountName = extract(@'Account Name:\\s*(.*?)\\s*(?:Target Server:|$)', 1, Details),\n             TargetServer = extract(@'Target Server:\\s*(.*?)\\s*$', 1, Details)\n    | extend SrcIP = iff(RawSrcIP in (\"\", \"0.0.0.0\"), \"\", RawSrcIP)\n    | where isnotempty(Actor) and isnotempty(SecretKey)\n    // To rely on the custom lists alone, delete the two 'matches regex' clauses - do not\n    // blank sensitivePattern, an empty regex matches every row.\n    | where SecretName matches regex sensitivePattern\n         or Folder matches regex sensitivePattern\n         or SecretName in~ (customSecretNames)\n         or Folder in~ (customFolders);\n// Batch by ingestion time so a late arriving event is still evaluated exactly once.\nlet priorAccess = access | where IngestedAt <= ago(detectionWindow) | distinct Actor, SecretKey;\naccess\n| where IngestedAt > ago(detectionWindow)\n| join kind=leftanti priorAccess on Actor, SecretKey\n| summarize FirstAccess = min(TimeGenerated), LastAccess = max(TimeGenerated), Accesses = count(),\n            SecretName = take_any(SecretName), Folder = take_any(Folder),\n            ActorEmail = take_anyif(ActorEmail, isnotempty(ActorEmail)),\n            AccountNames = make_set_if(AccountName, isnotempty(AccountName), 10),\n            TargetServers = make_set_if(TargetServer, isnotempty(TargetServer), 10),\n            TargetServer = take_anyif(TargetServer, isnotempty(TargetServer)),\n            SourceIPs = make_set_if(SrcIP, isnotempty(SrcIP), 20),\n            SrcIP = take_anyif(SrcIP, isnotempty(SrcIP))\n        by Actor, SecretKey\n// 'Target Server' is reported as either an address or a hostname, so each row feeds\n// whichever entity fits and blanks the other. Sentinel skips empty entity values.\n| extend TargetIsIp = isnotnull(parse_ipv4(TargetServer)) or isnotnull(parse_ipv6(TargetServer))\n| extend TargetIP = iff(TargetIsIp, TargetServer, \"\"),\n         TargetHost = iff(TargetIsIp, \"\", TargetServer)\n| project FirstAccess, LastAccess, Actor, ActorEmail, SecretKey, SecretName, Folder,\n          AccountNames, TargetServers, TargetHost, TargetIP, Accesses, SourceIPs, SrcIP\n| sort by FirstAccess desc\n",
        "queryFrequency": "PT1H",
        "queryPeriod": "P14D",
        "severity": "High",
        "status": "Available",
        "subTechniques": [],
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "CredentialAccess",
          "InitialAccess",
          "LateralMovement"
        ],
        "techniques": [
          "T1078",
          "T1555"
        ],
        "templateVersion": "1.3.0",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}
