name: willow-audit
$schema: https://schemas.humio.com/parser/v0.3.0

# CPS-compliant parser for Willow audit logs ingested into CrowdStrike Falcon
# LogScale / Next-Gen SIEM via the Splunk-HEC-compatible collector endpoint
# (POST <base>/services/collector).
#
# The Willow gateway posts a HEC envelope shaped like:
#   {
#     "time": <epoch seconds, may be fractional>,
#     "sourcetype": "audit_log",
#     "source": "audit-logs",
#     "fields": { "action": "<action>", "org_id": "<org>" },
#     "event": { ...AuditLog... }
#   }
#
# This parser normalizes to the CrowdStrike Parsing Standard (CPS / ECS 8.11)
# and keeps Willow-specific fields under the `willow.*` namespace, per CPS rules
# for non-ECS fields.

tests:
  - '{"time":1690000000.5,"sourcetype":"audit_log","source":"audit-logs","fields":{"action":"tool_call","org_id":"org_123"},"event":{"id":"al_1","created_at":"2026-07-26T06:00:00.000Z","updated_at":"2026-07-26T06:00:00.000Z","org_id":"org_123","action":"tool_call","transport":"http","mcp_client":"cursor","user":{"id":"u_1","name":"Ada Lovelace","email":"ada@example.com"},"data":{"mcp":"github","tool":"create_issue","success":true},"tool_call_mcp":"github","tool_call_tool":"create_issue","tool_call_success":true,"performance_total_time":142,"token_count_request":120,"token_count_response":340,"arguments_length":88,"response_length":512}}'
  - '{"time":1690000100,"sourcetype":"audit_log","source":"audit-logs","fields":{"action":"guard_check","org_id":"org_123"},"event":{"id":"al_2","created_at":"2026-07-26T06:01:00.000Z","updated_at":"2026-07-26T06:01:00.000Z","org_id":"org_123","action":"guard_check","user":{"id":"u_1","name":"Ada Lovelace","email":"ada@example.com"},"data":{"scope":"runtime","entity_type":"integration","entity_name":"github","passed":false},"guard_check_scope":"runtime"}}'
  - '{"time":1690000200,"sourcetype":"audit_log","source":"audit-logs","fields":{"action":"connector_auth","org_id":"org_123"},"event":{"id":"al_3","created_at":"2026-07-26T06:02:00.000Z","updated_at":"2026-07-26T06:02:00.000Z","org_id":"org_123","action":"connector_auth","user":{"id":"u_2","name":"Alan Turing","email":"alan@example.com"},"data":{"integration_slug":"github","auth_type":"oauth","step":"code_exchange","success":false,"error_code":"invalid_grant"}}}'
  - '{"time":1690000300,"sourcetype":"audit_log","source":"audit-logs","fields":{"action":"init","org_id":"org_123"},"event":{"id":"al_4","created_at":"2026-07-26T06:03:00.000Z","updated_at":"2026-07-26T06:03:00.000Z","org_id":"org_123","action":"init","transport":"stdio","data":{"tools":["a","b","c"]},"init_tools":["a","b","c"]}}'

script: |
  // #region PREPARSE
  /************************************************************
  ****** Parse the HEC envelope and the nested audit-log event
  ****** Establish the event timestamp
  ************************************************************/

  // Flatten the HEC envelope. When `event` is a JSON object this produces
  // dotted fields such as event.action, event.org_id, event.user.email.
  | parseJson()

  // Defensive: if the collector delivered `event` as a JSON *string* instead
  // of an object, parse it too. No-op for the normal object case.
  | case {
      event=/^\s*\{/ | parseJson(field=event, prefix="event.") ;
      * ;
    }

  // Event time. HEC `time` is epoch seconds (fractional); @timestamp is millis.
  // Fall back to the audit log's ISO created_at when `time` is absent.
  | case {
      time=*  | @timestamp := time * 1000 ;
      *       | parseTimestamp("yyyy-MM-dd'T'HH:mm:ss.SSSX", field="event.created_at", as="@timestamp") ;
    }

  // #endregion

  // #region METADATA
  /************************************************************
  ****** Static Metadata Definitions
  ************************************************************/

  | ecs.version    := "8.11.0"
  | Cps.version    := "1.0.0"
  | Parser.version := "1.0.0"
  | Vendor         := "Willow"
  | event.module   := "willow"
  | event.dataset  := "willow.audit"
  | event.kind     := "event"

  | observer.vendor  := "Willow"
  | observer.product := "Willow"
  | observer.type    := "application"
  | service.name     := "willow"

  // #endregion

  // #region NORMALIZATION
  /************************************************************
  ****** Map audit-log fields onto the ECS / CPS data model
  ************************************************************/

  // Core event fields. `event.action` and `event.id` already exist from the
  // flattened JSON (they map 1:1 onto the ECS fields of the same name).

  // Identity
  | organization.id := event.org_id
  | user.id         := event.user.id
  | user.name       := event.user.name
  | user.email      := event.user.email

  // Categorize by action. ECS event.category / event.type are arrays.
  | case {
      event.action="tool_call"      | event.category[0] := "api"                 | event.type[0] := "info" ;
      event.action="init"           | event.category[0] := "session"             | event.type[0] := "start" ;
      event.action="connect"        | event.category[0] := "authentication"      | event.type[0] := "start" ;
      event.action="connector_auth" | event.category[0] := "authentication"      | event.type[0] := "access" ;
      event.action="add_skill"      | event.category[0] := "configuration"       | event.type[0] := "installation" ;
      event.action="add_command"    | event.category[0] := "configuration"       | event.type[0] := "creation" ;
      event.action="webhook"        | event.category[0] := "web"                 | event.type[0] := "info" ;
      event.action="guard_check"    | event.category[0] := "intrusion_detection" | event.type[0] := "info" ;
      *                             | event.category[0] := "process"             | event.type[0] := "info" ;
    }

  // Unify the per-action success/pass signal into a single temp field.
  | case {
      event.action="tool_call"      | _success := event.data.success ;
      event.action="connect"        | _success := event.data.success ;
      event.action="connector_auth" | _success := event.data.success ;
      event.action="webhook"        | _success := event.data.success ;
      event.action="guard_check"    | _success := event.data.passed ;
      * ;
    }

  | case {
      _success=true  | event.outcome := "success" ;
      _success=false | event.outcome := "failure" ;
      *              | event.outcome := "unknown" ;
    }

  // #endregion

  // #region POST-NORMALIZATION
  /************************************************************
  ****** Willow-specific fields (willow.* namespace)
  ****** Human-readable message, cleanup
  ************************************************************/

  // Gateway / client context
  | willow.mcp_client   := event.mcp_client
  | willow.toolkit_slug := event.toolkit_slug
  | willow.transport    := event.transport

  // Tool call
  | willow.tool.mcp     := event.data.mcp
  | willow.tool.name    := event.data.tool
  | willow.tool.success := event.tool_call_success
  | willow.performance.total_ms   := event.performance_total_time
  | willow.token_count.request    := event.token_count_request
  | willow.token_count.response   := event.token_count_response
  | willow.arguments_length       := event.arguments_length
  | willow.response_length        := event.response_length

  // Connector auth
  | willow.connector.integration_slug := event.data.integration_slug
  | willow.connector.auth_type        := event.data.auth_type
  | willow.connector.step             := event.data.step
  | willow.connector.error_code       := event.data.error_code

  // Guard check
  | willow.guard_check.scope       := event.guard_check_scope
  | willow.guard_check.entity_type := event.data.entity_type
  | willow.guard_check.entity_name := event.data.entity_name
  | willow.guard_check.passed      := event.data.passed

  // Add skill
  | willow.skill.name       := event.add_skill_name
  | willow.skill.agent_type := event.add_skill_agent_type

  // Human-readable summary
  | format("%s %s", field=[event.action, event.outcome], as=message)

  // Remove HEC envelope noise and temp fields. Original audit-log fields remain
  // available under the event.* prefix for ad-hoc searching.
  | drop([_success, sourcetype, source, "fields.action", "fields.org_id"])

  // #endregion

tagFields:
  - Cps.version
  - Vendor
  - ecs.version
  - event.dataset
  - event.kind
  - event.module
  - event.outcome
  - observer.type
