Detection rules › Kusto

BTP - Cloud Integration artifact deployment

Status
available
Severity
high
Time window
15m
Source
github.com/Azure/Azure-Sentinel

Identifies deployment and undeployment of integration artifacts in SAP Cloud Integration. Integration flows are executable code that can process, transform, and route data between systems. Unauthorized artifact deployment could indicate: - Attacker deploying malicious integration flows for data exfiltration - Deployment of rogue code for persistent access - Undeployment of critical integrations causing denial of service

MITRE ATT&CK coverage

Rule body kusto

id: a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
kind: Scheduled
name: BTP - Cloud Integration artifact deployment
description: |
  Identifies deployment and undeployment of integration artifacts in SAP Cloud Integration.
  Integration flows are executable code that can process, transform, and route data between
  systems.
  
  Unauthorized artifact deployment could indicate:
  - Attacker deploying malicious integration flows for data exfiltration
  - Deployment of rogue code for persistent access
  - Undeployment of critical integrations causing denial of service
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SAPBTPAuditEvents
    dataTypes:
      - SAPBTPAuditLog_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - Persistence
relevantTechniques:
  - T1059
  - T1546
query: |
  SAPBTPAuditLog_CL
  | where Category == "audit.configuration"
  | extend objectType = tostring(Message.object.type)
  | where objectType in ("Deployment", "Undeployment")
  | extend attributes = todynamic(Message.attributes)
  | mv-apply attr = attributes on (
      summarize
          SymbolicName = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "symbolicName"),
          ArtifactId = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "id"),
          ArtifactVersion = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "version"),
          DeployedBy = take_anyif(tostring(attr.["new"]), tostring(attr.name) == "deployedBy"),
          UndeployedBy = take_anyif(tostring(attr.["new"]), tostring(attr.name) == "undeployedBy"),
          Creator = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "creator"),
          TenantName = take_anyif(tostring(attr.["new"]), tostring(attr.name) == "tenantName"),
          RuntimeLocationId = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "runtimeLocationId")
  )
  | extend Actor = case(
      isnotempty(DeployedBy), DeployedBy,
      isnotempty(UndeployedBy), UndeployedBy,
      isnotempty(Creator), Creator,
      ""
  )
  | extend ActionCategory = iff(objectType == "Deployment", "Deploy", "Undeploy"),
           normalizedAction = iff(objectType == "Deployment", "deployed", "undeployed")
  | extend MessageText = strcat("Integration artifact '", SymbolicName, "' (version ", ArtifactVersion, ") was ", normalizedAction, " in tenant ", TenantName)
  | extend AccountName = iff(Actor has "@", tostring(split(Actor, "@")[0]), ""),
           UPNSuffix   = iff(Actor has "@", tostring(split(Actor, "@")[1]), "")
  | project
      UpdatedOn,
      Actor,
      AccountName,
      UPNSuffix,
      MessageText,
      ArtifactName = SymbolicName,
      ArtifactId,
      ArtifactVersion,
      ActionCategory,
      ObjectType = objectType,
      TenantName,
      RuntimeLocationId,
      Tenant,
      CloudApp = "SAP Cloud Integration"
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: CloudApp
alertDetailsOverride:
  alertDisplayNameFormat: 'SAP Cloud Integration: {{MessageText}}'
  alertDescriptionFormat: |
    {{MessageText}} by {{Actor}}.
    
    This could indicate:
    - Legitimate integration artifact deployment or maintenance
    - Unauthorized deployment of malicious integration code
    - Attacker undeploying security-relevant integrations
customDetails:
  ArtifactName: ArtifactName
  ArtifactId: ArtifactId
  ArtifactVersion: ArtifactVersion
  ActionCategory: ActionCategory
  TenantName: TenantName
  RuntimeLocationId: RuntimeLocationId
version: 1.1.0

Stages and Predicates

Stage 1: source

SAPBTPAuditLog_CL

Stage 2: where

| where Category == "audit.configuration"

Stage 3: extend

| extend objectType = tostring(Message.object.type)

Stage 4: where

| where objectType in ("Deployment", "Undeployment")

Stage 5: extend

| extend attributes = todynamic(Message.attributes)

Stage 6: kusto:mv-apply

| mv-apply attr = attributes on (
    summarize
        SymbolicName = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "symbolicName"),
        ArtifactId = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "id"),
        ArtifactVersion = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "version"),
        DeployedBy = take_anyif(tostring(attr.["new"]), tostring(attr.name) == "deployedBy"),
        UndeployedBy = take_anyif(tostring(attr.["new"]), tostring(attr.name) == "undeployedBy"),
        Creator = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "creator"),
        TenantName = take_anyif(tostring(attr.["new"]), tostring(attr.name) == "tenantName"),
        RuntimeLocationId = take_anyif(tostring(coalesce(attr.["new"], attr.["old"])), tostring(attr.name) == "runtimeLocationId")
)

Stage 7: extend (4 consecutive steps)

| extend Actor = case(
    isnotempty(DeployedBy), DeployedBy,
    isnotempty(UndeployedBy), UndeployedBy,
    isnotempty(Creator), Creator,
    ""
)
| extend ActionCategory = iff(objectType == "Deployment", "Deploy", "Undeploy"),
         normalizedAction = iff(objectType == "Deployment", "deployed", "undeployed")
| extend MessageText = strcat("Integration artifact '", SymbolicName, "' (version ", ArtifactVersion, ") was ", normalizedAction, " in tenant ", TenantName)
| extend AccountName = iff(Actor has "@", tostring(split(Actor, "@")[0]), ""),
         UPNSuffix   = iff(Actor has "@", tostring(split(Actor, "@")[1]), "")
Actor =
ifisnotempty(DeployedBy)DeployedBy
elifisnotempty(UndeployedBy)UndeployedBy
elifisnotempty(Creator)Creator
else""

Stage 8: project

| project
    UpdatedOn,
    Actor,
    AccountName,
    UPNSuffix,
    MessageText,
    ArtifactName = SymbolicName,
    ArtifactId,
    ArtifactVersion,
    ActionCategory,
    ObjectType = objectType,
    TenantName,
    RuntimeLocationId,
    Tenant,
    CloudApp = "SAP Cloud Integration"

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Categoryeq
  • audit.configuration transforms: cased
objectTypein
  • Deployment transforms: cased
  • Undeployment transforms: cased

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
AccountNameproject
ActionCategoryproject
Actorproject
ArtifactIdproject
ArtifactNameproject
ArtifactVersionproject
CloudAppproject
MessageTextproject
ObjectTypeproject
RuntimeLocationIdproject
Tenantproject
TenantNameproject
UPNSuffixproject
UpdatedOnproject