Back
Ida7b167e2-9582-440b-8d47-53df2f73c4f4
RulenameDelinea - Sensitive secret accessed by new user (first access in 14 days)
DescriptionDetects when a user accesses a high-value secret for the first time, with no access to that

same secret by that user in the prior 14 days. First-time access to a sensitive secret can

indicate lateral movement or credential targeting.

Delinea reports no in-band sensitivity flag, so a secret is treated as high-value when its

name or its folder matches ‘sensitivePattern’ (common privileged naming conventions), or

when it appears in ‘customSecretNames’ / ‘customFolders’. Both knobs are meant to be edited

at deployment time - the pattern for substring matching, the lists for exact tenant-specific

names. The folder is read from the notes document, since the platform leaves the top-level

target.containerName empty on secret events.
SeverityHigh
TacticsInitialAccess
LateralMovement
CredentialAccess
TechniquesT1078
T1555
Required data connectorsDelineaPlatformConnector
KindScheduled
Query frequency1h
Query period14d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Delinea%20Platform/Analytic%20Rules/DelineaSensitiveSecretNewUser.yaml
Version1.3.0
Arm templatea7b167e2-9582-440b-8d47-53df2f73c4f4.json
Deploy To Azure
let detectionWindow = 1h;   // must equal queryFrequency
let baseline = 14d;         // must stay <= queryPeriod
// Delinea exposes no in-band sensitivity flag - tags and analyticData are null on every
// secret event - so value is inferred from the secret name and its folder. Secret Server
// permissions are inherited from folder placement, which makes containerName the strongest
// tiering signal, and it exists only inside notes: top-level target.containerName is null.
//
// TUNING 1 of 2: common high-value naming conventions, matched as a case insensitive
// substring against both the secret name and its folder. Trim groups that are noisy in
// your tenant; add cloud and CI terms (azure, aws, gcp, jenkins, ansible, terraform) only
// if those secrets are genuinely privileged, as they match broadly.
let 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";
// TUNING 2 of 2: tenant specific names, filled in at deployment time. Exact match, case
// insensitive - use sensitivePattern above for substring matching. Leave empty if unused.
let customSecretNames = dynamic([]);
//   e.g. dynamic(["Break Glass Account", "ESXi root", "SAP_PROD_ADMIN"]);
let customFolders = dynamic([]);
//   e.g. dynamic(["Tier 0", "Domain Admins", "Production Databases"]);
let access =
    DelineaAuditEvents_CL
    | where TimeGenerated > ago(baseline)
    | extend IngestedAt = ingestion_time()
    | extend d = parse_json(RawData)
    | where tostring(d.eventType.name) == "Delinea.Vault.Secret.Viewed"
    // notes is a JSON document carried as a string - same idiom as
    // Workbooks/DelineaSecretServerVaultActivity.json
    | extend n = parse_json(tostring(d.notes))
    | extend Actor = tostring(coalesce(d.actor.name, d.actor.email, d.actor.displayName)),
             ActorEmail = tostring(d.actor.email),
             SecretKey = tostring(coalesce(d.target.id, d.target.platformId, n.itemId)),
             // Trimmed because these become entity identifiers - Delinea returns names
             // such as '10.60.25.30 MYSQL ' with trailing whitespace.
             SecretName = trim(@'\s', coalesce(tostring(d.target.displayName), tostring(d.target.name),
                                   tostring(n.itemNameForDisplay), tostring(n.itemName), SecretId)),
             Folder = trim(@'\s', tostring(n.containerName)),
             Details = tostring(n.eventDetails),
             RawSrcIP = tostring(coalesce(d.source.host.network.ipAddress, n.ipAddress))
    // eventDetails is free text: " Account Name: qa-e2e-target  Target Server: 10.60.25.51 "
    | extend AccountName = extract(@'Account Name:\s*(.*?)\s*(?:Target Server:|$)', 1, Details),
             TargetServer = extract(@'Target Server:\s*(.*?)\s*$', 1, Details)
    | extend SrcIP = iff(RawSrcIP in ("", "0.0.0.0"), "", RawSrcIP)
    | where isnotempty(Actor) and isnotempty(SecretKey)
    // To rely on the custom lists alone, delete the two 'matches regex' clauses - do not
    // blank sensitivePattern, an empty regex matches every row.
    | where SecretName matches regex sensitivePattern
         or Folder matches regex sensitivePattern
         or SecretName in~ (customSecretNames)
         or Folder in~ (customFolders);
// Batch by ingestion time so a late arriving event is still evaluated exactly once.
let priorAccess = access | where IngestedAt <= ago(detectionWindow) | distinct Actor, SecretKey;
access
| where IngestedAt > ago(detectionWindow)
| join kind=leftanti priorAccess on Actor, SecretKey
| summarize FirstAccess = min(TimeGenerated), LastAccess = max(TimeGenerated), Accesses = count(),
            SecretName = take_any(SecretName), Folder = take_any(Folder),
            ActorEmail = take_anyif(ActorEmail, isnotempty(ActorEmail)),
            AccountNames = make_set_if(AccountName, isnotempty(AccountName), 10),
            TargetServers = make_set_if(TargetServer, isnotempty(TargetServer), 10),
            TargetServer = take_anyif(TargetServer, isnotempty(TargetServer)),
            SourceIPs = make_set_if(SrcIP, isnotempty(SrcIP), 20),
            SrcIP = take_anyif(SrcIP, isnotempty(SrcIP))
        by Actor, SecretKey
// 'Target Server' is reported as either an address or a hostname, so each row feeds
// whichever entity fits and blanks the other. Sentinel skips empty entity values.
| extend TargetIsIp = isnotnull(parse_ipv4(TargetServer)) or isnotnull(parse_ipv6(TargetServer))
| extend TargetIP = iff(TargetIsIp, TargetServer, ""),
         TargetHost = iff(TargetIsIp, "", TargetServer)
| project FirstAccess, LastAccess, Actor, ActorEmail, SecretKey, SecretName, Folder,
          AccountNames, TargetServers, TargetHost, TargetIP, Accesses, SourceIPs, SrcIP
| sort by FirstAccess desc
incidentConfiguration:
  groupingConfiguration:
    lookbackDuration: P1D
    reopenClosedIncident: false
    groupByEntities:
    - Account
    matchingMethod: Selected
    enabled: true
  createIncident: true
triggerOperator: gt
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Delinea%20Platform/Analytic%20Rules/DelineaSensitiveSecretNewUser.yaml
triggerThreshold: 0
id: a7b167e2-9582-440b-8d47-53df2f73c4f4
kind: Scheduled
relevantTechniques:
- T1078
- T1555
version: 1.3.0
tactics:
- InitialAccess
- LateralMovement
- CredentialAccess
queryPeriod: 14d
queryFrequency: 1h
severity: High
name: Delinea - Sensitive secret accessed by new user (first access in 14 days)
customDetails:
  SecretKey: SecretKey
  Accesses: Accesses
  AccountNames: AccountNames
  Folder: Folder
  SecretName: SecretName
  TargetServers: TargetServers
requiredDataConnectors:
- connectorId: DelineaPlatformConnector
  dataTypes:
  - DelineaAuditEvents_CL
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: Actor
  - identifier: FullName
    columnName: ActorEmail
- entityType: File
  fieldMappings:
  - identifier: Name
    columnName: SecretName
  - identifier: Directory
    columnName: Folder
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIP
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: TargetHost
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: TargetIP
description: |
  Detects when a user accesses a high-value secret for the first time, with no access to that
  same secret by that user in the prior 14 days. First-time access to a sensitive secret can
  indicate lateral movement or credential targeting.
  Delinea reports no in-band sensitivity flag, so a secret is treated as high-value when its
  name or its folder matches 'sensitivePattern' (common privileged naming conventions), or
  when it appears in 'customSecretNames' / 'customFolders'. Both knobs are meant to be edited
  at deployment time - the pattern for substring matching, the lists for exact tenant-specific
  names. The folder is read from the notes document, since the platform leaves the top-level
  target.containerName empty on secret events.
eventGroupingSettings:
  aggregationKind: AlertPerResult
status: Available
query: |
  let detectionWindow = 1h;   // must equal queryFrequency
  let baseline = 14d;         // must stay <= queryPeriod
  // Delinea exposes no in-band sensitivity flag - tags and analyticData are null on every
  // secret event - so value is inferred from the secret name and its folder. Secret Server
  // permissions are inherited from folder placement, which makes containerName the strongest
  // tiering signal, and it exists only inside notes: top-level target.containerName is null.
  //
  // TUNING 1 of 2: common high-value naming conventions, matched as a case insensitive
  // substring against both the secret name and its folder. Trim groups that are noisy in
  // your tenant; add cloud and CI terms (azure, aws, gcp, jenkins, ansible, terraform) only
  // if those secrets are genuinely privileged, as they match broadly.
  let 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";
  // TUNING 2 of 2: tenant specific names, filled in at deployment time. Exact match, case
  // insensitive - use sensitivePattern above for substring matching. Leave empty if unused.
  let customSecretNames = dynamic([]);
  //   e.g. dynamic(["Break Glass Account", "ESXi root", "SAP_PROD_ADMIN"]);
  let customFolders = dynamic([]);
  //   e.g. dynamic(["Tier 0", "Domain Admins", "Production Databases"]);
  let access =
      DelineaAuditEvents_CL
      | where TimeGenerated > ago(baseline)
      | extend IngestedAt = ingestion_time()
      | extend d = parse_json(RawData)
      | where tostring(d.eventType.name) == "Delinea.Vault.Secret.Viewed"
      // notes is a JSON document carried as a string - same idiom as
      // Workbooks/DelineaSecretServerVaultActivity.json
      | extend n = parse_json(tostring(d.notes))
      | extend Actor = tostring(coalesce(d.actor.name, d.actor.email, d.actor.displayName)),
               ActorEmail = tostring(d.actor.email),
               SecretKey = tostring(coalesce(d.target.id, d.target.platformId, n.itemId)),
               // Trimmed because these become entity identifiers - Delinea returns names
               // such as '10.60.25.30 MYSQL ' with trailing whitespace.
               SecretName = trim(@'\s', coalesce(tostring(d.target.displayName), tostring(d.target.name),
                                     tostring(n.itemNameForDisplay), tostring(n.itemName), SecretId)),
               Folder = trim(@'\s', tostring(n.containerName)),
               Details = tostring(n.eventDetails),
               RawSrcIP = tostring(coalesce(d.source.host.network.ipAddress, n.ipAddress))
      // eventDetails is free text: " Account Name: qa-e2e-target  Target Server: 10.60.25.51 "
      | extend AccountName = extract(@'Account Name:\s*(.*?)\s*(?:Target Server:|$)', 1, Details),
               TargetServer = extract(@'Target Server:\s*(.*?)\s*$', 1, Details)
      | extend SrcIP = iff(RawSrcIP in ("", "0.0.0.0"), "", RawSrcIP)
      | where isnotempty(Actor) and isnotempty(SecretKey)
      // To rely on the custom lists alone, delete the two 'matches regex' clauses - do not
      // blank sensitivePattern, an empty regex matches every row.
      | where SecretName matches regex sensitivePattern
           or Folder matches regex sensitivePattern
           or SecretName in~ (customSecretNames)
           or Folder in~ (customFolders);
  // Batch by ingestion time so a late arriving event is still evaluated exactly once.
  let priorAccess = access | where IngestedAt <= ago(detectionWindow) | distinct Actor, SecretKey;
  access
  | where IngestedAt > ago(detectionWindow)
  | join kind=leftanti priorAccess on Actor, SecretKey
  | summarize FirstAccess = min(TimeGenerated), LastAccess = max(TimeGenerated), Accesses = count(),
              SecretName = take_any(SecretName), Folder = take_any(Folder),
              ActorEmail = take_anyif(ActorEmail, isnotempty(ActorEmail)),
              AccountNames = make_set_if(AccountName, isnotempty(AccountName), 10),
              TargetServers = make_set_if(TargetServer, isnotempty(TargetServer), 10),
              TargetServer = take_anyif(TargetServer, isnotempty(TargetServer)),
              SourceIPs = make_set_if(SrcIP, isnotempty(SrcIP), 20),
              SrcIP = take_anyif(SrcIP, isnotempty(SrcIP))
          by Actor, SecretKey
  // 'Target Server' is reported as either an address or a hostname, so each row feeds
  // whichever entity fits and blanks the other. Sentinel skips empty entity values.
  | extend TargetIsIp = isnotnull(parse_ipv4(TargetServer)) or isnotnull(parse_ipv6(TargetServer))
  | extend TargetIP = iff(TargetIsIp, TargetServer, ""),
           TargetHost = iff(TargetIsIp, "", TargetServer)
  | project FirstAccess, LastAccess, Actor, ActorEmail, SecretKey, SecretName, Folder,
            AccountNames, TargetServers, TargetHost, TargetIP, Accesses, SourceIPs, SrcIP
  | sort by FirstAccess desc
{
  "$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"
    }
  ]
}