Skip to content

Commit 651aeb3

Browse files
authored
Merge pull request Azure#12939 from markolauren/ml-tablecreator-v2.5
v2.5 update
2 parents 2a0749f + 415098c commit 651aeb3

File tree

3 files changed

+133
-50
lines changed

3 files changed

+133
-50
lines changed

Tools/TableCreator/README.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
1-
# 💡 tableCreator.ps1 (v2.4)
1+
# 💡 tableCreator.ps1 (v2.5)
22

33
**Author:** Marko Lauren
44

5+
![screenshot](https://github.com/user-attachments/assets/6732b1fb-b83a-4dcf-911b-6143e1098ec5)
6+
57
## Purpose
68

7-
`tableCreator.ps1` is a PowerShell script designed to streamline the process of duplicating the schema of an existing Microsoft Sentinel table and creating a new table with the same schema. The script supports Analytics, Data Lake, Auxiliary and Basic table types. This tool is ideal for scenarios such as streaming the logs to table with different/cheaper plan or splitting log to multiple tables.
9+
`tableCreator.ps1` is a PowerShell script designed to streamline the process of duplicating the schema of an existing Microsoft Sentinel table and creating a new table with the same schema. Alternatively, you can bring your own schema via a JSON file (BYOS - bring-your-own-schema). The script supports Analytics, Data Lake, Auxiliary and Basic table types. This tool is ideal for scenarios such as streaming the logs to table with different/cheaper plan or splitting log to multiple tables.
810

911
## Key Features
1012

1113
- **Data Lake Table Creation:** Easily create new tables with the same schema as existing tables.
1214
- **Schema Duplication:** Automatically capture and reuse the schema from any existing Sentinel table.
15+
- **Bring-Your-Own-Schema (BYOS):** Create tables using a custom JSON schema file instead of copying from existing tables.
1316
- **Flexible Table Types:** Supports Analytics, Data Lake, Auxiliary and Basic types.
1417
- **Retention Settings:** Define both interactive and total retention periods for new tables.
1518
- **Dynamic Column Handling:** Optionally convert dynamic columns to string for compatibility with Data Lake and Auxiliary tables.
1619
- **Interactive & Command-Line Modes:** Use prompts for missing parameters or provide all options via command line.
1720
- **Resource Targeting:** Specify your Sentinel workspace via parameter or prompt.
18-
- **Tenant Selection:** Use `-tenantId` for authentication outside Azure Cloud Shell.
21+
- **Tenant Selection:** Use `-TenantId` for authentication outside Azure Cloud Shell.
1922

2023
## Usage
2124

2225
### 1. Define Your Sentinel Resource ID
2326

27+
To obtain full resource ID, go to log analytics workspace and either choose "JSON view" in overview or go to "Properties".<br>
2428
You can provide the resource ID in two ways:
2529

2630
- **Command-Line:**
@@ -51,7 +55,7 @@ You will be prompted for the source table name, new table name, table type, and
5155

5256
Specify all required parameters:
5357
```
54-
.\tableCreator.ps1 -FullResourceId <RESOURCE_ID> -tableName <SourceTable> -newTableName <NewTable> -type <datalake|dl|analytics|basic|aux|auxiliary> -retention <Days> -totalRetention <Days> [-ConvertToString] [-tenantId <TenantId>]
58+
.\tableCreator.ps1 -FullResourceId <RESOURCE_ID> -tableName <SourceTable> -newTableName <NewTable> -type <datalake|dl|analytics|basic|aux|auxiliary> -retention <Days> -totalRetention <Days> [-ConvertToString] [-TenantId <TenantId>]
5559
```
5660

5761
**Examples:**
@@ -61,25 +65,43 @@ Specify all required parameters:
6165
.\tableCreator.ps1 -tableName MyTable -newTableName MyAuxTable_CL -type aux -totalRetention 365 -ConvertToString
6266
```
6367

68+
#### Bring Your Own Schema (BYOS) Mode
69+
70+
Create tables using a custom JSON schema file:
71+
```
72+
.\tableCreator.ps1 -SchemaFile <path-to-schema.json> -newTableName <NewTable> -type <datalake|dl|analytics|basic|aux|auxiliary> -retention <Days> -totalRetention <Days>
73+
```
74+
75+
**Example:**
76+
```
77+
.\tableCreator.ps1 -SchemaFile mySchema.json -newTableName MyCustomTable_CL -type datalake -totalRetention 365
78+
```
79+
80+
The schema file should be a JSON array containing objects with `name` and `type` properties:
81+
```json
82+
[
83+
{"name": "TimeGenerated", "type": "datetime"},
84+
{"name": "Action", "type": "string"},
85+
{"name": "Status", "type": "int"}
86+
]
87+
```
88+
6489
### Parameters
6590

66-
- `-FullResourceId` : (Optional) Full Azure Resource ID of the Sentinel workspace.
67-
- `-tableName` : Name of the existing table to copy schema from.
91+
- `-FullResourceId` : Full Azure Resource ID of the Sentinel workspace.
92+
- `-tableName` : Name of the existing table to copy schema from (not required when using `-SchemaFile`).
6893
- `-newTableName` : Name for the new table.
6994
- `-type` : Table type (`analytics`, `datalake`/`dl`, `auxiliary`/`aux`, `basic`).
7095
- `-retention` : Interactive/analytics retention in days.
7196
- `-totalRetention` : Total retention in days.
7297
- `-ConvertToString` : (Optional) Convert dynamic columns to string (recommended for Data Lake and Auxiliary tables).
73-
- `-tenantId` : (Optional) Azure tenant ID for authentication.
98+
- `-SchemaFile` : (Optional) Path to JSON schema file for Bring Your Own Schema (BYOS) functionality.
99+
- `-TenantId` : (Optional) Azure tenant ID for authentication.
74100

75101
## Notes
76102

77103
- The script uses KQL `getschema` to retrieve table schemas. Columns of type `guid` are reported as `string` due to unknown reason. If the table you're creating a copy has guid type column(s) it causes a mismatch with column types when creating DCR. Workaround is to modify DCR with transformKql:
78104
"transformKql": "source | extend SomeGuid = tostring(SomeGuid), AnotherGuid = tostring(AnotherGuid)"
79105
Another workaround is to debug the script and interpret those columns on the fly. This is already done for SecurityEvent and SigninLogs table.
80106

81-
## Screenshot
82-
83-
![screenshot](https://github.com/user-attachments/assets/6732b1fb-b83a-4dcf-911b-6143e1098ec5)
84-
85107
---

Tools/TableCreator/mySchema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{ "name": "TimeGenerated", "type": "datetime" },
3+
{ "name": "Action", "type": "string" },
4+
{ "name": "UserId", "type": "guid" },
5+
{ "name": "Status", "type": "int" },
6+
{ "name": "IsActive", "type": "boolean" }
7+
]

Tools/TableCreator/tableCreator.ps1

Lines changed: 93 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
<#
22
.SYNOPSIS
3-
Creates a new Sentinel table with the same schema as an existing table.
3+
Creates a new Sentinel table with the same schema as an existing table, or BYOS (bring-your-own-schema).
44
55
.DESCRIPTION
66
This script queries the schema of an existing Sentinel table and creates a new table with the same schema.
7+
Alternatively, you can provide a JSON schema file (bring-your-own-schema).
78
It supports Analytics, Auxiliary/Data Lake, and Basic table types, and allows for retention settings and conversion of dynamic columns to string for Auxiliary/Data Lake tables.
89
The script prompts for any missing parameters and can be run interactively or with command-line arguments.
910
1011
.PARAMETER FullResourceId
1112
The full resource ID of the Sentinel/Log Analytics Workspace. If not provided, you will be prompted.
1213
Resource ID can be found in Log Analytics Workspace > JSON View > Copy button.
13-
To hardcode the Resource ID for your environment, edit the $resourceId variable in the script (line 70).
14+
To hardcode the Resource ID for your environment, edit the $resourceId variable in the script (line 78).
1415
1516
.PARAMETER tableName
1617
The name of the existing table to copy the schema from (e.g., SecurityEvent).
@@ -32,27 +33,34 @@
3233
For Auxiliary/Data Lake tables, converts dynamic columns to string.
3334
PRO TIP: If the copied table has dynamic columns, you may create it initially as Analytics, and then change to Data Lake later. This will preserve the dynamic types.
3435
35-
.PARAMETER tenantId
36+
.PARAMETER TenantId
3637
Azure tenant ID. Required only if not running in Azure Cloud Shell.
3738
Requires the Az PowerShell module installed.
3839
40+
.PARAMETER SchemaFile
41+
Path to a JSON schema file (bring-your-own-schema). If provided, the schema will be read from this file instead of querying an existing table.
42+
3943
.EXAMPLE
4044
.\tableCreator.ps1 -tableName MyTable -newTableName MyNewTable_CL -type analytics -retention 180 -totalRetention 365
4145
4246
.EXAMPLE
43-
.\tableCreator.ps1 -ConvertToString
47+
.\tableCreator.ps1 -TenantId YOUR_TENANT_ID -FullResourceId /subscriptions/YOUR_SUBSCRIPTION_ID/resourcegroups/YOUR_RESOURCE_GROUP/providers/Microsoft.OperationalInsights/workspaces/YOUR_WORKSPACE_NAME
48+
49+
.EXAMPLE
50+
.\tableCreator.ps1 -SchemaFile mySchema.json -newTableName MyNewTable_CL -type datalake
4451
4552
#>
4653

4754
# Define parameters for the script
4855
param (
49-
[string]$tenantId,
56+
[string]$TenantId,
5057
[string]$tableName,
5158
[string]$newTableName,
5259
[string]$type,
5360
[int]$retention,
5461
[int]$totalRetention,
5562
[switch]$ConvertToString,
63+
[string]$SchemaFile, # New: path to a JSON schema file (bring-your-own-schema)
5664
[ValidateScript({
5765
if ($_ -match '^/subscriptions/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/resourcegroups/[a-zA-Z0-9-_]+/providers/microsoft.operationalinsights/workspaces/[a-zA-Z0-9-_]+$') {
5866
$true
@@ -75,14 +83,52 @@ if($FullResourceId){
7583
}
7684
##################################################################################################################
7785

86+
# Immediately read/validate SchemaFile (fail early)
87+
if ($SchemaFile) {
88+
if (-not (Test-Path -Path $SchemaFile)) {
89+
Write-Host "[Error] Schema file '$SchemaFile' not found." -ForegroundColor Red
90+
exit 1
91+
}
92+
93+
Write-Host "[Using schema from file: $SchemaFile]" -ForegroundColor Green
94+
95+
try {
96+
$raw = Get-Content -Raw -Path $SchemaFile
97+
$schemaArray = $raw | ConvertFrom-Json
98+
} catch {
99+
Write-Host "[Error] Failed to read/parse JSON schema file: $($_.Exception.Message)" -ForegroundColor Red
100+
exit 1
101+
}
102+
103+
if (-not ($schemaArray -is [System.Array])) {
104+
Write-Host "[Error] Schema file must contain a JSON array of objects with 'name' and 'type' properties." -ForegroundColor Red
105+
exit 1
106+
}
107+
108+
# Normalize to the same structure the script expects from getschema
109+
$queryResult = $schemaArray | ForEach-Object {
110+
[pscustomobject]@{
111+
ColumnName = $_.name
112+
ColumnType = $_.type
113+
}
114+
}
115+
}
116+
# End SchemaFile handling
117+
78118
# Connect Azure Account, no need to run in Cloud Shell, but you do need the Az module installed.
79-
if ($tenantId) {
80-
Connect-AzAccount -TenantId $tenantId
119+
if ($TenantId) {
120+
try {
121+
Connect-AzAccount -TenantId $TenantId -ErrorAction Stop
122+
}
123+
catch {
124+
Write-Host "[Error] Failed to connect to Azure: $($_.Exception.Message)" -ForegroundColor Red
125+
exit 1
126+
}
81127
}
82128

83129
# Display the banner
84130
Write-Host " +=======================+" -ForegroundColor Green
85-
Write-Host " | tableCreator.ps1 v2.4 |" -ForegroundColor Green
131+
Write-Host " | tableCreator.ps1 v2.5 |" -ForegroundColor Green
86132
Write-Host " +=======================+" -ForegroundColor Green
87133
Write-Host ""
88134

@@ -117,12 +163,15 @@ if ($resourceId -eq "/subscriptions/YOUR_SUBSCRIPTION_ID/resourceGroups/YOUR_RES
117163
}
118164

119165
# Prompt for input if necessary
120-
if (-not $tableName) {
121-
$tableName = PromptForInput "Enter Table Name to get Schema from"
122-
}
166+
# If SchemaFile is provided we don't need the source $tableName.
167+
if (-not $SchemaFile) {
168+
if (-not $tableName) {
169+
$tableName = PromptForInput "Enter table name to get schema from"
170+
}
171+
}
123172

124173
if (-not $newTableName) {
125-
$newTableName = PromptForInput "Enter new Table Name to be created with the same Schema (remember _CL -suffix)"
174+
$newTableName = PromptForInput "Enter new table name to be created with the schema (remember _CL -suffix)"
126175
}
127176

128177
# Prompt for table type, defaulting to 'analytics' if not provided
@@ -165,42 +214,47 @@ if (-not $totalRetention) {
165214
}
166215

167216
# Set query to get the schema of the specified table
168-
$query = "$tableName | getschema | project ColumnName, ColumnType"
217+
# If $queryResult is already populated (from SchemaFile), skip querying the workspace.
218+
if ($queryResult) {
219+
# Schema already loaded from file; nothing to do here.
220+
} else {
221+
$query = "$tableName | getschema | project ColumnName, ColumnType"
169222

170-
# Query the workspace to get the schema
171-
Write-Host "[Querying $tableName table schema...]"
223+
# Query the workspace to get the schema
224+
Write-Host "[Querying $tableName table schema...]"
172225

173-
# Construct the request body
174-
$body = @{
175-
query = $query
176-
} | ConvertTo-Json -Depth 2
226+
# Construct the request body
227+
$body = @{
228+
query = $query
229+
} | ConvertTo-Json -Depth 2
177230

178-
$response = Invoke-AzRestMethod -Path "$resourceId/query?api-version=2017-10-01" -Method POST -Payload $body
231+
$response = Invoke-AzRestMethod -Path "$resourceId/query?api-version=2017-10-01" -Method POST -Payload $body
179232

180-
# Convert Content from JSON string to PowerShell object
181-
$data = $response.Content | ConvertFrom-Json
233+
# Convert Content from JSON string to PowerShell object
234+
$data = $response.Content | ConvertFrom-Json
182235

183-
# Check if the response is successful
184-
if ($response.StatusCode -eq 200 -or $response.StatusCode -eq 202) {
185-
Write-Host "[Table schema successfully captured]"
186-
}
187-
else {
188-
# Output error details if the creation failed
189-
Write-Host "[Error] Failed to query the table '$TableName'. Status code: $($response.StatusCode)" -ForegroundColor Red
236+
# Check if the response is successful
237+
if ($response.StatusCode -eq 200 -or $response.StatusCode -eq 202) {
238+
Write-Host "[Table schema successfully captured]"
239+
}
240+
else {
241+
# Output error details if the creation failed
242+
Write-Host "[Error] Failed to query the table '$TableName'. Status code: $($response.StatusCode)" -ForegroundColor Red
190243

191-
exit
192-
}
244+
exit
245+
}
193246

194-
# do the mapping to queryResult
195-
$columns = $data.tables[0].columns
196-
$rows = $data.tables[0].rows
247+
# do the mapping to queryResult
248+
$columns = $data.tables[0].columns
249+
$rows = $data.tables[0].rows
197250

198-
$queryResult = $rows | ForEach-Object {
199-
$object = @{}
200-
for ($i = 0; $i -lt $columns.Count; $i++) {
201-
$object[$columns[$i].name] = $_[$i]
251+
$queryResult = $rows | ForEach-Object {
252+
$object = @{}
253+
for ($i = 0; $i -lt $columns.Count; $i++) {
254+
$object[$columns[$i].name] = $_[$i]
255+
}
256+
[pscustomobject]$object
202257
}
203-
[pscustomobject]$object
204258
}
205259

206260
## Prepare an array to hold names of columns converted to string

0 commit comments

Comments
 (0)