Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@
},
"stepCollectorConfigs": {
"air_marshal_events": {
"shouldJoinNestedData": false,
"shouldJoinNestedData": true,
"joinedDataStepName": "expand",
"request": {
"apiEndpoint": "[[concat(parameters('apiBaseUrl'),'/networks/$networkId$/wireless/airMarshal')]",
"httpMethod": "GET",
Expand All @@ -276,6 +277,11 @@
}
}
},
"shouldJoinNestedData": true,
"joinedDataStepName": "network",
"extra": {
"nestedTransformName": "/ASI/Microsoft/MvExpandTransformer"
},
"connectorDefinitionName": "CiscoMerakiConnector",
"dataType": "CiscoMerakiAirMarshalEvents",
"dcrConfig": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,40 +173,12 @@
"Custom-CiscoMerakiAirMarshalEvents_CL": {
"columns": [
{
"name": "ssid",
"type": "string"
},
{
"name": "bssids",
"name": "network",
"type": "dynamic"
},
{
"name": "channels",
"name": "expand",
"type": "dynamic"
},
{
"name": "firstSeen",
"type": "datetime"
},
{
"name": "lastSeen",
"type": "datetime"
},
{
"name": "wiredMacs",
"type": "dynamic"
},
{
"name": "wiredVlans",
"type": "dynamic"
},
{
"name": "wiredLastSeen",
"type": "datetime"
},
{
"name": "networkId",
"type": "string"
}
]
},
Expand Down Expand Up @@ -638,7 +610,7 @@
"clv2ws1"
],
"outputStream": "Custom-CiscoMerakiAirMarshalEvents_CL",
"transformKql": "source | extend TimeGenerated = iff(isnull(lastSeen), now(), todatetime(lastSeen)) , Ssid = ['ssid'] , Bssids = ['bssids'] , Channels = ['channels'] , FirstSeen = ['firstSeen'] , LastSeen = ['lastSeen'] , WiredMacs = ['wiredMacs'] , WiredVlans = ['wiredVlans'] , WiredLastSeen = ['wiredLastSeen'] , NetworkId = ['networkId'] | project TimeGenerated , Ssid , Bssids , Channels , FirstSeen , LastSeen , WiredMacs , WiredVlans , WiredLastSeen , NetworkId"
"transformKql": "source | extend Ssid = tostring(expand.ssid) , Bssids = expand.bssids , Channels = expand.channels , FirstSeen = unixtime_seconds_todatetime(tolong(expand.firstSeen)) , LastSeen = unixtime_seconds_todatetime(tolong(expand.lastSeen)) , WiredMacs = expand.wiredMacs , WiredVlans = expand.wiredVlans , WiredLastSeen = unixtime_seconds_todatetime(tolong(expand.wiredLastSeen)) , NetworkId = tostring(network.id) , Types = expand.types | extend TimeGenerated = iff(isnull(LastSeen), now(), LastSeen) | project TimeGenerated , Ssid , Bssids , Channels , FirstSeen , LastSeen , WiredMacs , WiredVlans , WiredLastSeen , NetworkId , Types"
},
{
"streams": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Cisco Meraki Air Marshal ingestion

The Air Marshal collector lists an organization's wireless networks and calls
`/networks/{networkId}/wireless/airMarshal` for each network. Cisco's scan results
do not include the network ID.

The collector retains the parent network record as `network` and the child scan
results as `expand`. The CCF nested `MvExpandTransformer` emits one row per scan
result with its parent network attached. The Air Marshal DCR input stream therefore
contains two dynamic columns, `network` and `expand`.

The DCR maps `network.id` to `NetworkId`, preserves `expand.types` as the dynamic
`Types` array, and converts `firstSeen`, `lastSeen`, and `wiredLastSeen` from Unix
epoch seconds to UTC datetimes. `TimeGenerated` uses the converted `LastSeen`,
falling back to ingestion time when it is absent. Other event timestamps remain
null when absent.

All classifications are retained, including empty arrays; ingestion does not
filter out unclassified SSIDs. For example, query classified results with:

```kusto
CiscoMerakiAirMarshalEvents
| where Types has_any ("rogue", "spoof")
| project TimeGenerated, NetworkId, Ssid, Types, FirstSeen, LastSeen, WiredLastSeen
```

## Updating existing installations

Deploy the updated Air Marshal table schema, DCR, and polling configuration
together. The joined input shape is not compatible with the previous flat DCR
input stream. Updating only the saved-search alias or table schema does not fix
collection. Existing ingested rows with missing values are not repaired by this
change.

After updating, verify new scan results from multiple networks, including the
same SSID in different networks. Each result must retain its own `NetworkId`,
classification array, and converted timestamps. Also verify networks with no
scan results do not produce event rows.

References:

- [Cisco Air Marshal API](https://developer.cisco.com/meraki/api-v1/get-network-wireless-air-marshal/)
- [CCF nested API polling](https://learn.microsoft.com/azure/sentinel/isv/custom-connector-nested-api-polling)
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@
"name": "NetworkId",
"type": "string",
"description": "Network ID where the Air Marshal event was detected."
},
{
"name": "Types",
"type": "dynamic",
"description": "Array of SSID threat classifications, such as rogue or spoof. Empty when no classification applies."
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
],
"Metadata": "SolutionMetadata.json",
"BasePath": "C:\\Github\\Azure-Sentinel\\Solutions\\",
"Version": "3.2.1",
"Version": "3.2.2",
"TemplateSpec": true,
"Is1PConnector": false
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@
"email": "support@microsoft.com",
"_email": "[variables('email')]",
"_solutionName": "Cisco Meraki Events via REST API",
"_solutionVersion": "3.2.1",
"_solutionVersion": "3.2.2",
"solutionId": "azuresentinel.azure-sentinel-solution-ciscomerakinativepoller",
"_solutionId": "[variables('solutionId')]",
"workspaceResourceId": "[resourceId('microsoft.OperationalInsights/Workspaces', parameters('workspace'))]",
"dataConnectorCCPVersion": "3.2.1",
"dataConnectorCCPVersion": "3.2.2",
"_dataConnectorContentIdConnectorDefinition1": "CiscoMerakiConnector",
"dataConnectorTemplateNameConnectorDefinition1": "[concat(parameters('workspace'),'-dc-',uniquestring(variables('_dataConnectorContentIdConnectorDefinition1')))]",
"_dataConnectorContentIdConnections1": "CiscoMerakiConnectorConnections",
Expand Down Expand Up @@ -638,40 +638,12 @@
"Custom-CiscoMerakiAirMarshalEvents_CL": {
"columns": [
{
"name": "ssid",
"type": "string"
},
{
"name": "bssids",
"type": "dynamic"
},
{
"name": "channels",
"type": "dynamic"
},
{
"name": "firstSeen",
"type": "datetime"
},
{
"name": "lastSeen",
"type": "datetime"
},
{
"name": "wiredMacs",
"name": "network",
"type": "dynamic"
},
{
"name": "wiredVlans",
"name": "expand",
"type": "dynamic"
},
{
"name": "wiredLastSeen",
"type": "datetime"
},
{
"name": "networkId",
"type": "string"
}
]
},
Expand Down Expand Up @@ -1103,7 +1075,7 @@
"clv2ws1"
],
"outputStream": "Custom-CiscoMerakiAirMarshalEvents_CL",
"transformKql": "source | extend TimeGenerated = iff(isnull(lastSeen), now(), todatetime(lastSeen)) , Ssid = ['ssid'] , Bssids = ['bssids'] , Channels = ['channels'] , FirstSeen = ['firstSeen'] , LastSeen = ['lastSeen'] , WiredMacs = ['wiredMacs'] , WiredVlans = ['wiredVlans'] , WiredLastSeen = ['wiredLastSeen'] , NetworkId = ['networkId'] | project TimeGenerated , Ssid , Bssids , Channels , FirstSeen , LastSeen , WiredMacs , WiredVlans , WiredLastSeen , NetworkId"
"transformKql": "source | extend Ssid = tostring(expand.ssid) , Bssids = expand.bssids , Channels = expand.channels , FirstSeen = unixtime_seconds_todatetime(tolong(expand.firstSeen)) , LastSeen = unixtime_seconds_todatetime(tolong(expand.lastSeen)) , WiredMacs = expand.wiredMacs , WiredVlans = expand.wiredVlans , WiredLastSeen = unixtime_seconds_todatetime(tolong(expand.wiredLastSeen)) , NetworkId = tostring(network.id) , Types = expand.types | extend TimeGenerated = iff(isnull(LastSeen), now(), LastSeen) | project TimeGenerated , Ssid , Bssids , Channels , FirstSeen , LastSeen , WiredMacs , WiredVlans , WiredLastSeen , NetworkId , Types"
},
{
"streams": [
Expand Down Expand Up @@ -1308,6 +1280,11 @@
"name": "NetworkId",
"type": "string",
"description": "Network ID where the Air Marshal event was detected."
},
{
"name": "Types",
"type": "dynamic",
"description": "Array of SSID threat classifications, such as rogue or spoof. Empty when no classification applies."
}
]
}
Expand Down Expand Up @@ -2313,7 +2290,8 @@
},
"stepCollectorConfigs": {
"air_marshal_events": {
"shouldJoinNestedData": false,
"shouldJoinNestedData": true,
"joinedDataStepName": "expand",
"request": {
"apiEndpoint": "[[concat(parameters('apiBaseUrl'),'/networks/$networkId$/wireless/airMarshal')]",
"httpMethod": "GET",
Expand All @@ -2336,6 +2314,11 @@
}
}
},
"shouldJoinNestedData": true,
"joinedDataStepName": "network",
"extra": {
"nestedTransformName": "/ASI/Microsoft/MvExpandTransformer"
},
"connectorDefinitionName": "CiscoMerakiConnector",
"dataType": "CiscoMerakiAirMarshalEvents",
"dcrConfig": {
Expand Down Expand Up @@ -2617,7 +2600,7 @@
"[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]"
],
"properties": {
"description": "parser_CiscoMerakiOrganizationsAliasFunction Data Parser with template version 3.2.1",
"description": "parser_CiscoMerakiOrganizationsAliasFunction Data Parser with template version 3.2.2",
"mainTemplate": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "[variables('parserObject1').parserVersion1]",
Expand Down Expand Up @@ -2749,7 +2732,7 @@
"[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]"
],
"properties": {
"description": "parser_CiscoMerakiOrganizationNetworksAliasFunction Data Parser with template version 3.2.1",
"description": "parser_CiscoMerakiOrganizationNetworksAliasFunction Data Parser with template version 3.2.2",
"mainTemplate": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "[variables('parserObject2').parserVersion2]",
Expand Down Expand Up @@ -2881,7 +2864,7 @@
"[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]"
],
"properties": {
"description": "parser_CiscoMerakiNetworkClientsAliasFunction Data Parser with template version 3.2.1",
"description": "parser_CiscoMerakiNetworkClientsAliasFunction Data Parser with template version 3.2.2",
"mainTemplate": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "[variables('parserObject3').parserVersion3]",
Expand Down Expand Up @@ -3013,7 +2996,7 @@
"[extensionResourceId(resourceId('Microsoft.OperationalInsights/workspaces', parameters('workspace')), 'Microsoft.SecurityInsights/contentPackages', variables('_solutionId'))]"
],
"properties": {
"description": "parser_CiscoMerakiAirMarshalEventsAliasFunction Data Parser with template version 3.2.1",
"description": "parser_CiscoMerakiAirMarshalEventsAliasFunction Data Parser with template version 3.2.2",
"mainTemplate": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "[variables('parserObject4').parserVersion4]",
Expand Down Expand Up @@ -3141,7 +3124,7 @@
"apiVersion": "2023-04-01-preview",
"location": "[parameters('workspace-location')]",
"properties": {
"version": "3.2.1",
"version": "3.2.2",
"kind": "Solution",
"contentSchemaVersion": "3.0.0",
"displayName": "Cisco Meraki Events via REST API",
Expand Down
2 changes: 2 additions & 0 deletions Solutions/Cisco Meraki Events via REST API/ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
| **Version** | **Date Modified (DD-MM-YYYY)** | **Change History** |
|-------------|--------------------------------|--------------------------------------------------------------------|
| 3.2.2 | 29-10-2026 | Fixed Data Connector Air Marshal ingestion to retain the parent network ID for each scan result, preserve threat classifications in Types, and convert first-seen, last-seen, and wired-last-seen epoch seconds to UTC datetimes. |
| 3.2.2 | 29-10-2026 | Fixed **Data Connector** Air Marshal ingestion to retain the parent network ID for each scan result, preserve threat classifications in `Types`, and convert first-seen, last-seen, and wired-last-seen epoch seconds to UTC datetimes. |
| 3.2.1 | 15-09-2026 | Fixed Cisco Meraki connector regressions by correcting the Organizations/Organization Networks health check window, routing security events except File Scanned to ASIM Network Session logs, separating File Scanned events into a dedicated custom table, restoring query-string support for API request URLs, preserving additional configuration-change context, and restoring ASIM Network Session `DvcAction` mapping from the `blocked` field. Fixed DCR creation errors by replacing unsupported `coalesce` expressions and matching File Scanned `EventCount` to the destination string schema. Corrected Network Clients and Air Marshal nested polling to extract the network ID from each parent record without expanding its properties. |
| 3.2.0 | 28-08-2026 | Promoted the updated Cisco Meraki CCF REST API data connector introduced in version 3.1.0 to General Availability (GA). |
| 3.1.1 | 24-08-2026 | Standardized all CCF Connector data types to use checkpointed `t0`/`t1` time windowing (UnixTimestamp), aligning the newer Organizations, Organization Networks, Network Clients, and Air Marshal Events data types with the existing ASIM log endpoints to prevent duplicate ingestion. <br/> Updated the polling interval and `t0`/`t1` time window configuration of the **CCF Connector** data types to fix duplicate ingestion.|
Expand Down
Loading