{ "$schema": "https://schema.management.azure.com/schemas/2019-08-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "Workspace": { "type": "string", "metadata": { "description": "The Microsoft Sentinel workspace into which the function will be deployed. Has to be in the selected Resource Group." } }, "WorkspaceRegion": { "type": "string", "defaultValue": "[resourceGroup().location]", "metadata": { "description": "The region of the selected workspace. The default value will use the Region selection above." } } }, "resources": [ { "type": "Microsoft.OperationalInsights/workspaces/savedSearches", "apiVersion": "2020-08-01", "name": "[concat(parameters('Workspace'), '/ASimDataTester')]", "location": "[parameters('WorkspaceRegion')]", "properties": { "etag": "*", "displayName": "ASIM Data tester", "category": "ASIM", "FunctionAlias": "ASimDataTester", "query": "let MACaddr_regex = @'^[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}$';\nlet FQDN_regex = @'^([_a-zA-Z0-9][_a-zA-Z0-9-]{0,62}\\.)+[a-zA-Z]{2,63}$'; // -- based on https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation without lookahead.\nlet DNS_domain_regex = @'^([_a-zA-Z0-9][_a-zA-Z0-9-]{0,62}\\.)*[a-zA-Z]{2,63}$'; // -- Allow underscores in domain names, used by Microsoft DNS server\nlet domain_regex = @'^([_a-zA-Z0-9][_a-zA-Z0-9-]{0,62}\\.)*[a-zA-Z]{2,63}$';\nlet Hostname_regex = @'^[a-zA-Z0-9-]{1,61}$';\nlet MD5_regex = @'[a-zA-Z0-9]{32}';\nlet SHA1_regex = @'[a-zA-Z0-9]{40}';\nlet SHA256_regex = @'[a-zA-Z0-9]{64}';\nlet SHA512_regex = @'[a-zA-Z0-9]{128}';\nlet IPprotocol = materialize (externaldata (code: string, value: string)\n[@\"https://www.iana.org/assignments/protocol-numbers/protocol-numbers-1.csv\"] with (format=\"csv\", IgnoreFirstRecord=true) | project value);\nlet DnsQueryTypeName = materialize (externaldata (value: string)\n[@\"https://www.iana.org/assignments/dns-parameters/dns-parameters-4.csv\"] with (format=\"csv\", IgnoreFirstRecord=true));\nlet DnsResponseCodeName = materialize (externaldata (code: string, value: string)\n[@\"https://www.iana.org/assignments/dns-parameters/dns-parameters-6.csv\"] with (format=\"csv\", IgnoreFirstRecord=true) | project toupper(value));\n//let DnsQueryClassName = materialize (externaldata (dec: string, dex: string, value: string)\n// [@\"https://www.iana.org/assignments/dns-parameters/dns-parameters-2.csv\"] with (format=\"csv\", IgnoreFirstRecord=true) | project value);\nlet DnsQueryClassName = datatable(Class:int, ClassName: string)[\n 0, 'Reserved',\n 1, 'IN',\n 2, 'Unassigned',\n 3, 'CH',\n 4, 'HS',\n 254, 'None',\n 255, 'Any'] | project ClassName;\nlet ASimFields = materialize(externaldata (ColumnName: string, ColumnType: string, Class: string, Schema: string, LogicalType:string, ListOfValues: string, AliasedField: string, DynamicType: string, ArrayValuesType: string)\n [@\"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/ASIM/dev/ASimTester/ASimTester.csv\"] with (format=\"csv\", IgnoreFirstRecord=true)\n | project-rename dict_schema = Schema);\nlet ASimFieldsWithAliases = materialize(ASimFields\n | project-rename SchemaColumn = ColumnName, SchemaType = ColumnType\n | lookup (ASimFields | project ParentClass = Class, ParentColumn = ColumnName, dict_schema=dict_schema) on $left.AliasedField == $right.ParentColumn, $left.dict_schema == $right.dict_schema);\nlet ParserSchema = (T | getschema | project ColumnName, type = ColumnType);\nlet total_records_materialize = materialize (T | count);\nlet total_records = toscalar (total_records_materialize);\nlet percent_records = (records:int) { round(todouble(records) / todouble(total_records) * 100, 2) };\nT\n| extend f=pack_all()\n| extend EventSchema = coalesce(selected_schema, f['EventSchema'], 'Common')\n| mv-expand f\n| project f, EventSchema\n| extend ColumnName = tostring(bag_keys(f)[0])\n| extend value = f[ColumnName]\n| lookup ParserSchema on ColumnName\n| extend dynamictype = iff(type == 'dynamic', gettype(value), '')\n| mv-apply arrayValue = value on (\n summarize array_types = make_set(gettype(arrayValue), 3) | extend numOfArrayTypes = array_length(array_types) | project numOfArrayTypes\n)\n| extend arrayValueType = iff(dynamictype == 'array', case(\n numOfArrayTypes == 1, tostring(gettype(value[0])),\n numOfArrayTypes >= 2, \"FAIL\",\n \"\"\n ), \"\")\n| summarize cnt_records = count() by ColumnName, tostring(value), type, EventSchema, dynamictype, arrayValueType\n| lookup ASimFieldsWithAliases on $left.ColumnName == $right.SchemaColumn, $left.EventSchema == $right.dict_schema\n| extend Result = case(\n SchemaType != \"\" and type != \"null\" and SchemaType != type, strcat (\"(0) Error: type mismatch for column [\", ColumnName, \"]. It is currently [\", type, \"] and should be [\", SchemaType, \"] (Schema:\", EventSchema, \")\"),\n Class == \"Mandatory\" and value == \"\", strcat (\"(1) Warning: Empty value in \", cnt_records, \" records (\",percent_records(cnt_records),\"%) in mandatory field [\", ColumnName, \"] (Schema:\", EventSchema, \")\"),\n Class == \"Recommended\" and value == \"\", strcat (\"(2) Info: Empty value in \", cnt_records, \" records (\",percent_records(cnt_records),\"%) in recommended field [\", ColumnName, \"] (Schema:\", EventSchema, \")\"),\n Class == \"Optional\" and value == \"\", strcat (\"(2) Info: Empty value in \", cnt_records, \" records (\",percent_records(cnt_records),\"%) in optional field [\", ColumnName, \"] (Schema:\", EventSchema, \")\"),\n isnotempty(DynamicType) and DynamicType != dynamictype, \"Invalid Dynamic Type\",\n isnotempty(DynamicType) and DynamicType == 'array' and (arrayValueType == \"FAIL\" or arrayValueType != ArrayValuesType), \"Invalid Dynamic Array Values\",\n LogicalType == \"Enumerated\" and ListOfValues != \"\" and ListOfValues !has value, \"Invalid Value\",\n (LogicalType == \"MAC Address\") and (value != \"\") and not (value matches regex MACaddr_regex), \"Invalid Value\",\n (LogicalType == \"IP Address\") and (value != \"\") and not(ipv4_is_match(value, \"0.0.0.0\",0)) and not(ipv6_is_match(value, \"::1\",0)) and not(ipv6_is_match(value, \"0.0.0.0\",0)), \"Invalid Value\",\n (LogicalType == \"FQDN\") and (value != \"\") and not (value matches regex FQDN_regex), \"Invalid Value\",\n (LogicalType == \"RecommendedFQDN\") and (value != \"\") and not (value matches regex FQDN_regex), \"Abnormal Value\",\n (LogicalType == \"Domain\") and (value != \"\") and not (value matches regex domain_regex), \"Invalid Value\",\n (LogicalType == \"DnsDomain\") and (value != \"\") and not (value matches regex DNS_domain_regex), \"Invalid Value\",\n (LogicalType == \"RecommendedDnsDomain\") and (value != \"\") and not (value matches regex DNS_domain_regex), \"Abnormal Value\",\n (LogicalType == \"GUID\") and (value != \"\") and isnull(toguid(value)), \"Invalid Value\",\n (LogicalType == \"Hostname\") and (value != \"\") and not (value matches regex Hostname_regex) and not(ipv4_is_match(value, \"0.0.0.0\",0)) and not(ipv6_is_match(value, \"::1\",0)), \"Invalid Value\",\n (LogicalType == \"RiskLevel\") and (value != \"\") and (toint(value) < 0 or toint(value) > 100), \"Invalid Value\",\n (LogicalType == \"DnsResponseCodeName\") and (value != \"\") and value !in (DnsResponseCodeName) and value != \"NA\" , \"Invalid Value\",\n (LogicalType == \"DnsQueryTypeName\") and (value != \"\") and value !in (DnsQueryTypeName) and value != \"ANY\" and not(value matches regex @'^TYPE\\d+$'), \"Invalid Value\",\n (LogicalType == \"DnsQueryClassName\") and (value != \"\") and value !in (DnsQueryClassName) and not(value matches regex @'^CLASS\\d+$'), \"Invalid Value\",\n (LogicalType == \"NetworkProtocol\") and (value != \"\") and value !in (IPprotocol), \"Invalid Value\",\n (LogicalType == \"MD5\") and (value != \"\") and not (value matches regex MD5_regex), \"Invalid Value\",\n (LogicalType == \"SHA1\") and (value != \"\") and not (value matches regex SHA1_regex), \"Invalid Value\",\n (LogicalType == \"SHA256\") and (value != \"\") and not (value matches regex SHA256_regex), \"Invalid Value\",\n (LogicalType == \"SHA512\") and (value != \"\") and not (value matches regex SHA512_regex), \"Invalid Value\",\n 'None'\n )\n| where Result != \"None\"\n| summarize values = make_set(value, 10), cnt_values=count(), cnt_records=sum(cnt_records) by Result, ColumnName, LogicalType, EventSchema, DynamicType, ArrayValuesType\n| extend Result = iif (Result == \"Invalid Value\", strcat (\"(0) Error: \", cnt_values, \" invalid value(s) (up to 10 listed) in \", cnt_records, \" records (\",percent_records(cnt_records),\"%) for field [\", ColumnName, \"] of type [\", LogicalType, \"]: \", tostring(values), \" (Schema:\", EventSchema, \")\"), Result)\n| extend Result = iif (Result == \"Abnormal Value\", strcat (\"(1) Warning: \", cnt_values, \" abnormal value(s) (up to 10 listed) in \", cnt_records, \" records (\",percent_records(cnt_records),\"%) for field [\", ColumnName, \"] of type [\", LogicalType, \"]: \", tostring(values), \" (Schema:\", EventSchema, \")\"), Result)\n| extend Result = iif (Result == \"Invalid Dynamic Type\", strcat (\"(0) Error: \", cnt_values, \" invalid dynamic type (up to 10 listed) in \", cnt_records, \" records (\",percent_records(cnt_records),\"%) for field [\", ColumnName, \"] of type [\", DynamicType, \"]: \", tostring(values), \" (Schema:\", EventSchema, \")\"), Result)\n| extend Result = iif (Result == \"Invalid Dynamic Array Values\", strcat (\"(0) Error: \", cnt_values, \" invalid dynamic array(s). All inner values must be of type [\", ArrayValuesType, \"] (up to 10 listed) in \", cnt_records, \" records (\",percent_records(cnt_records),\"%) for field [\", ColumnName, \"] of type [\", DynamicType, \"]: \", tostring(values), \" (Schema:\", EventSchema, \")\"), Result)\n| project Result\n| sort by Result asc", "version": 1, "functionParameters": "T:(*),selected_schema:string=''" } } ] }