Back
Id5e8f2a1b-7c3d-4b9e-a6f0-1d2e3c4b5a6f
RulenameSAP BTP - Unaudited custom app with login-only activity
DescriptionIdentifies SAP BTP custom applications (CloudFoundry, SAP CAP, etc.) that only produce

XSUAA authentication events (TokenIssuedEvent, ClientAuthenticationSuccess) but have not

generated any business audit log activity in the past 7 days. This pattern indicates that

the application has not implemented audit logging (e.g., missing @AuditLog annotations in

CAP or missing audit log service bindings), creating a security blind spot where user

actions within the application are invisible to monitoring. The 7-day lookback avoids

false positives for properly instrumented apps whose users simply have not performed

auditable actions in the current session. Attackers could exploit such unaudited

applications to perform malicious operations without detection.
SeverityMedium
TacticsDefenseEvasion
TechniquesT1562
T1562.008
Required data connectorsSAPBTPAuditEvents
KindScheduled
Query frequency1h
Query period7d
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/SAP%20BTP/Analytic%20Rules/BTP%20-%20Unaudited%20custom%20app%20with%20login-only%20activity.yaml
Version1.0.4
Arm template5e8f2a1b-7c3d-4b9e-a6f0-1d2e3c4b5a6f.json
Deploy To Azure
// Lookback period for audit events - a longer window avoids false positives for
// properly instrumented apps where users logged in but haven't performed actions yet
let audit_lookback = ago(7d);
// Known BTP platform service patterns (excluded from detection)
let platform_service_patterns = dynamic([
    "app-studio", "auditlog", "cis-local", "service-manager",
    "destination-xsappname", "connectivity-proxy", "feature-flags"
]);
// Step 1: From XSUAA TokenIssuedEvent, identify custom apps with interactive user logins
// XSUAA is a shared subaccount service - the client_id in each token identifies the actual app
let app_logins = SAPBTPAuditLog_CL
    // Scope logins to the current 1h run cycle (matches queryFrequency)
    | where TimeGenerated > ago(1h)
    | where Category == "audit.security-events"
    | extend data_s = tostring(Message.data)
    | where data_s has "TokenIssuedEvent"
    | extend ParsedData = parse_json(data_s)
    // origin field = client_id of the app the token was issued for
    | extend ClientId = tostring(ParsedData.origin)
    | where ClientId startswith "sb-"
    // Parse nested event message for human user and grant type
    | extend EventMessage = tostring(ParsedData.message)
    | extend GrantType = extract(@'"grant_type"\s*:\s*"([^"]+)"', 1, EventMessage)
    // Only interactive browser-based logins (not service-to-service tokens)
    | where GrantType == "authorization_code"
    // Derive app identifiers: sb-<XsAppName>!t<number>
    | extend XsAppName = extract(@"^sb-(.+?)!\w+$", 1, ClientId)
    | where isnotempty(XsAppName)
    // Exclude known platform services
    | where not(XsAppName has_any (platform_service_patterns))
    // Extract human-readable app name (first segment before subaccount qualifier)
    | extend AppName = extract(@"^([A-Za-z][A-Za-z0-9_]*)", 1, XsAppName)
    | extend HumanUser = extract(@'"user_name"\s*:\s*"([^"]+)"', 1, EventMessage)
    | extend IPAddress = tostring(Message.ip);
// Step 2a: Identify apps that have business audit events where the service binding
// identity appears in UserName (e.g. platform-generated audit events)
let audited_apps = SAPBTPAuditLog_CL
    | where TimeGenerated > audit_lookback
    | where Category !in ("audit.security-events")
    | where UserName startswith "sb-"
    | extend AuditedAppName = extract(@"^sb-(.+?)!\w+", 1, UserName)
    | where isnotempty(AuditedAppName)
    | distinct AuditedAppName, Tenant;
// Step 2b: Identify CF app instances that have an audit log service binding.
// When a CAP/custom app binds the audit log service, BTP provisions a
// 'customer-auditlog' service key whose XSUAA token events share the same
// AlsServiceId (CF app GUID) as the app's own login tokens. This gives a
// reliable per-app correlation that works even when the audit log service
// instance is deployed in a different CF space than the app itself.
let audited_app_instances = SAPBTPAuditLog_CL
    | where TimeGenerated > audit_lookback
    | where Category == "audit.security-events"
    | where UserName contains "customer-auditlog"
    | where isnotempty(AlsServiceId)
    | distinct AlsServiceId, Tenant;
// Step 3: Find custom apps with logins but no business audit trail
app_logins
| join kind=leftanti (audited_apps | project XsAppName = AuditedAppName, Tenant) on XsAppName, Tenant
| join kind=leftanti (audited_app_instances) on AlsServiceId, Tenant
| summarize
    LoginCount = count(),
    FirstLogin = min(TimeGenerated),
    LastLogin = max(TimeGenerated),
    Users = make_set(HumanUser, 100),
    IPs = make_set(IPAddress, 100),
    AppName = take_any(AppName),
    SubaccountName = take_any(SubaccountName),
    OrgId = take_any(OrgId),
    SpaceId = take_any(SpaceId)
    by XsAppName, ClientId, Tenant
| extend Users = set_difference(Users, dynamic([""]))
| project
    FirstLogin,
    LastLogin,
    AppName,
    XsAppName,
    ClientId,
    SubaccountName,
    Tenant,
    OrgId,
    SpaceId,
    LoginCount,
    Users,
    IPs,
    CloudApp = "SAP BTP"
version: 1.0.4
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/SAP%20BTP/Analytic%20Rules/BTP%20-%20Unaudited%20custom%20app%20with%20login-only%20activity.yaml
triggerThreshold: 0
requiredDataConnectors:
- dataTypes:
  - SAPBTPAuditLog_CL
  connectorId: SAPBTPAuditEvents
tactics:
- DefenseEvasion
customDetails:
  AppName: AppName
  LoginCount: LoginCount
  IPs: IPs
  SubaccountName: SubaccountName
  OrgId: OrgId
  SpaceId: SpaceId
  ClientId: ClientId
  XsAppName: XsAppName
  Users: Users
  Tenant: Tenant
relevantTechniques:
- T1562
- T1562.008
kind: Scheduled
alertDetailsOverride:
  alertDescriptionFormat: |
    The custom app '{{AppName}}' ({{XsAppName}}) in subaccount '{{SubaccountName}}' produced only
    XSUAA authentication events but no business audit trail within the past 7 days.

    This indicates the app has not implemented audit logging, creating a security blind spot
    where user actions within the application are invisible to Microsoft Sentinel.

    Recommended actions:
    1. Review the application's audit log implementation
    2. Add the audit log service binding in SAP BTP cockpit
    3. For CAP apps, implement @AuditLog annotations on sensitive entities and services
    4. Investigate user activity through alternative logs (application logs, HTTP access logs)
  alertDisplayNameFormat: "SAP BTP: Unaudited custom app '{{AppName}}' - login-only activity detected"
entityMappings:
- fieldMappings:
  - identifier: Name
    columnName: CloudApp
  entityType: CloudApplication
description: |
  Identifies SAP BTP custom applications (CloudFoundry, SAP CAP, etc.) that only produce
  XSUAA authentication events (TokenIssuedEvent, ClientAuthenticationSuccess) but have not
  generated any business audit log activity in the past 7 days. This pattern indicates that
  the application has not implemented audit logging (e.g., missing @AuditLog annotations in
  CAP or missing audit log service bindings), creating a security blind spot where user
  actions within the application are invisible to monitoring. The 7-day lookback avoids
  false positives for properly instrumented apps whose users simply have not performed
  auditable actions in the current session. Attackers could exploit such unaudited
  applications to perform malicious operations without detection.
triggerOperator: gt
status: Available
queryFrequency: 1h
id: 5e8f2a1b-7c3d-4b9e-a6f0-1d2e3c4b5a6f
name: SAP BTP - Unaudited custom app with login-only activity
severity: Medium
query: |
  // Lookback period for audit events - a longer window avoids false positives for
  // properly instrumented apps where users logged in but haven't performed actions yet
  let audit_lookback = ago(7d);
  // Known BTP platform service patterns (excluded from detection)
  let platform_service_patterns = dynamic([
      "app-studio", "auditlog", "cis-local", "service-manager",
      "destination-xsappname", "connectivity-proxy", "feature-flags"
  ]);
  // Step 1: From XSUAA TokenIssuedEvent, identify custom apps with interactive user logins
  // XSUAA is a shared subaccount service - the client_id in each token identifies the actual app
  let app_logins = SAPBTPAuditLog_CL
      // Scope logins to the current 1h run cycle (matches queryFrequency)
      | where TimeGenerated > ago(1h)
      | where Category == "audit.security-events"
      | extend data_s = tostring(Message.data)
      | where data_s has "TokenIssuedEvent"
      | extend ParsedData = parse_json(data_s)
      // origin field = client_id of the app the token was issued for
      | extend ClientId = tostring(ParsedData.origin)
      | where ClientId startswith "sb-"
      // Parse nested event message for human user and grant type
      | extend EventMessage = tostring(ParsedData.message)
      | extend GrantType = extract(@'"grant_type"\s*:\s*"([^"]+)"', 1, EventMessage)
      // Only interactive browser-based logins (not service-to-service tokens)
      | where GrantType == "authorization_code"
      // Derive app identifiers: sb-<XsAppName>!t<number>
      | extend XsAppName = extract(@"^sb-(.+?)!\w+$", 1, ClientId)
      | where isnotempty(XsAppName)
      // Exclude known platform services
      | where not(XsAppName has_any (platform_service_patterns))
      // Extract human-readable app name (first segment before subaccount qualifier)
      | extend AppName = extract(@"^([A-Za-z][A-Za-z0-9_]*)", 1, XsAppName)
      | extend HumanUser = extract(@'"user_name"\s*:\s*"([^"]+)"', 1, EventMessage)
      | extend IPAddress = tostring(Message.ip);
  // Step 2a: Identify apps that have business audit events where the service binding
  // identity appears in UserName (e.g. platform-generated audit events)
  let audited_apps = SAPBTPAuditLog_CL
      | where TimeGenerated > audit_lookback
      | where Category !in ("audit.security-events")
      | where UserName startswith "sb-"
      | extend AuditedAppName = extract(@"^sb-(.+?)!\w+", 1, UserName)
      | where isnotempty(AuditedAppName)
      | distinct AuditedAppName, Tenant;
  // Step 2b: Identify CF app instances that have an audit log service binding.
  // When a CAP/custom app binds the audit log service, BTP provisions a
  // 'customer-auditlog' service key whose XSUAA token events share the same
  // AlsServiceId (CF app GUID) as the app's own login tokens. This gives a
  // reliable per-app correlation that works even when the audit log service
  // instance is deployed in a different CF space than the app itself.
  let audited_app_instances = SAPBTPAuditLog_CL
      | where TimeGenerated > audit_lookback
      | where Category == "audit.security-events"
      | where UserName contains "customer-auditlog"
      | where isnotempty(AlsServiceId)
      | distinct AlsServiceId, Tenant;
  // Step 3: Find custom apps with logins but no business audit trail
  app_logins
  | join kind=leftanti (audited_apps | project XsAppName = AuditedAppName, Tenant) on XsAppName, Tenant
  | join kind=leftanti (audited_app_instances) on AlsServiceId, Tenant
  | summarize
      LoginCount = count(),
      FirstLogin = min(TimeGenerated),
      LastLogin = max(TimeGenerated),
      Users = make_set(HumanUser, 100),
      IPs = make_set(IPAddress, 100),
      AppName = take_any(AppName),
      SubaccountName = take_any(SubaccountName),
      OrgId = take_any(OrgId),
      SpaceId = take_any(SpaceId)
      by XsAppName, ClientId, Tenant
  | extend Users = set_difference(Users, dynamic([""]))
  | project
      FirstLogin,
      LastLogin,
      AppName,
      XsAppName,
      ClientId,
      SubaccountName,
      Tenant,
      OrgId,
      SpaceId,
      LoginCount,
      Users,
      IPs,
      CloudApp = "SAP BTP"
queryPeriod: 7d
eventGroupingSettings:
  aggregationKind: AlertPerResult
{
  "$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/5e8f2a1b-7c3d-4b9e-a6f0-1d2e3c4b5a6f')]",
      "kind": "Scheduled",
      "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/5e8f2a1b-7c3d-4b9e-a6f0-1d2e3c4b5a6f')]",
      "properties": {
        "alertDetailsOverride": {
          "alertDescriptionFormat": "The custom app '{{AppName}}' ({{XsAppName}}) in subaccount '{{SubaccountName}}' produced only\nXSUAA authentication events but no business audit trail within the past 7 days.\n\nThis indicates the app has not implemented audit logging, creating a security blind spot\nwhere user actions within the application are invisible to Microsoft Sentinel.\n\nRecommended actions:\n1. Review the application's audit log implementation\n2. Add the audit log service binding in SAP BTP cockpit\n3. For CAP apps, implement @AuditLog annotations on sensitive entities and services\n4. Investigate user activity through alternative logs (application logs, HTTP access logs)\n",
          "alertDisplayNameFormat": "SAP BTP: Unaudited custom app '{{AppName}}' - login-only activity detected"
        },
        "alertRuleTemplateName": "5e8f2a1b-7c3d-4b9e-a6f0-1d2e3c4b5a6f",
        "customDetails": {
          "AppName": "AppName",
          "ClientId": "ClientId",
          "IPs": "IPs",
          "LoginCount": "LoginCount",
          "OrgId": "OrgId",
          "SpaceId": "SpaceId",
          "SubaccountName": "SubaccountName",
          "Tenant": "Tenant",
          "Users": "Users",
          "XsAppName": "XsAppName"
        },
        "description": "Identifies SAP BTP custom applications (CloudFoundry, SAP CAP, etc.) that only produce\nXSUAA authentication events (TokenIssuedEvent, ClientAuthenticationSuccess) but have not\ngenerated any business audit log activity in the past 7 days. This pattern indicates that\nthe application has not implemented audit logging (e.g., missing @AuditLog annotations in\nCAP or missing audit log service bindings), creating a security blind spot where user\nactions within the application are invisible to monitoring. The 7-day lookback avoids\nfalse positives for properly instrumented apps whose users simply have not performed\nauditable actions in the current session. Attackers could exploit such unaudited\napplications to perform malicious operations without detection.\n",
        "displayName": "SAP BTP - Unaudited custom app with login-only activity",
        "enabled": true,
        "entityMappings": [
          {
            "entityType": "CloudApplication",
            "fieldMappings": [
              {
                "columnName": "CloudApp",
                "identifier": "Name"
              }
            ]
          }
        ],
        "eventGroupingSettings": {
          "aggregationKind": "AlertPerResult"
        },
        "OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/SAP%20BTP/Analytic%20Rules/BTP%20-%20Unaudited%20custom%20app%20with%20login-only%20activity.yaml",
        "query": "// Lookback period for audit events - a longer window avoids false positives for\n// properly instrumented apps where users logged in but haven't performed actions yet\nlet audit_lookback = ago(7d);\n// Known BTP platform service patterns (excluded from detection)\nlet platform_service_patterns = dynamic([\n    \"app-studio\", \"auditlog\", \"cis-local\", \"service-manager\",\n    \"destination-xsappname\", \"connectivity-proxy\", \"feature-flags\"\n]);\n// Step 1: From XSUAA TokenIssuedEvent, identify custom apps with interactive user logins\n// XSUAA is a shared subaccount service - the client_id in each token identifies the actual app\nlet app_logins = SAPBTPAuditLog_CL\n    // Scope logins to the current 1h run cycle (matches queryFrequency)\n    | where TimeGenerated > ago(1h)\n    | where Category == \"audit.security-events\"\n    | extend data_s = tostring(Message.data)\n    | where data_s has \"TokenIssuedEvent\"\n    | extend ParsedData = parse_json(data_s)\n    // origin field = client_id of the app the token was issued for\n    | extend ClientId = tostring(ParsedData.origin)\n    | where ClientId startswith \"sb-\"\n    // Parse nested event message for human user and grant type\n    | extend EventMessage = tostring(ParsedData.message)\n    | extend GrantType = extract(@'\"grant_type\"\\s*:\\s*\"([^\"]+)\"', 1, EventMessage)\n    // Only interactive browser-based logins (not service-to-service tokens)\n    | where GrantType == \"authorization_code\"\n    // Derive app identifiers: sb-<XsAppName>!t<number>\n    | extend XsAppName = extract(@\"^sb-(.+?)!\\w+$\", 1, ClientId)\n    | where isnotempty(XsAppName)\n    // Exclude known platform services\n    | where not(XsAppName has_any (platform_service_patterns))\n    // Extract human-readable app name (first segment before subaccount qualifier)\n    | extend AppName = extract(@\"^([A-Za-z][A-Za-z0-9_]*)\", 1, XsAppName)\n    | extend HumanUser = extract(@'\"user_name\"\\s*:\\s*\"([^\"]+)\"', 1, EventMessage)\n    | extend IPAddress = tostring(Message.ip);\n// Step 2a: Identify apps that have business audit events where the service binding\n// identity appears in UserName (e.g. platform-generated audit events)\nlet audited_apps = SAPBTPAuditLog_CL\n    | where TimeGenerated > audit_lookback\n    | where Category !in (\"audit.security-events\")\n    | where UserName startswith \"sb-\"\n    | extend AuditedAppName = extract(@\"^sb-(.+?)!\\w+\", 1, UserName)\n    | where isnotempty(AuditedAppName)\n    | distinct AuditedAppName, Tenant;\n// Step 2b: Identify CF app instances that have an audit log service binding.\n// When a CAP/custom app binds the audit log service, BTP provisions a\n// 'customer-auditlog' service key whose XSUAA token events share the same\n// AlsServiceId (CF app GUID) as the app's own login tokens. This gives a\n// reliable per-app correlation that works even when the audit log service\n// instance is deployed in a different CF space than the app itself.\nlet audited_app_instances = SAPBTPAuditLog_CL\n    | where TimeGenerated > audit_lookback\n    | where Category == \"audit.security-events\"\n    | where UserName contains \"customer-auditlog\"\n    | where isnotempty(AlsServiceId)\n    | distinct AlsServiceId, Tenant;\n// Step 3: Find custom apps with logins but no business audit trail\napp_logins\n| join kind=leftanti (audited_apps | project XsAppName = AuditedAppName, Tenant) on XsAppName, Tenant\n| join kind=leftanti (audited_app_instances) on AlsServiceId, Tenant\n| summarize\n    LoginCount = count(),\n    FirstLogin = min(TimeGenerated),\n    LastLogin = max(TimeGenerated),\n    Users = make_set(HumanUser, 100),\n    IPs = make_set(IPAddress, 100),\n    AppName = take_any(AppName),\n    SubaccountName = take_any(SubaccountName),\n    OrgId = take_any(OrgId),\n    SpaceId = take_any(SpaceId)\n    by XsAppName, ClientId, Tenant\n| extend Users = set_difference(Users, dynamic([\"\"]))\n| project\n    FirstLogin,\n    LastLogin,\n    AppName,\n    XsAppName,\n    ClientId,\n    SubaccountName,\n    Tenant,\n    OrgId,\n    SpaceId,\n    LoginCount,\n    Users,\n    IPs,\n    CloudApp = \"SAP BTP\"\n",
        "queryFrequency": "PT1H",
        "queryPeriod": "P7D",
        "severity": "Medium",
        "status": "Available",
        "subTechniques": [
          "T1562.008"
        ],
        "suppressionDuration": "PT1H",
        "suppressionEnabled": false,
        "tactics": [
          "DefenseEvasion"
        ],
        "techniques": [
          "T1562"
        ],
        "templateVersion": "1.0.4",
        "triggerOperator": "GreaterThan",
        "triggerThreshold": 0
      },
      "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
    }
  ]
}