This document helps to create clean and readable KQL code for parsing and detection rules.
All views are my own based on writing lots of code in PowerShell and other languages.
This is a living document that helps to create a common baseline.
- Place a spaces before and after the '=' character for readability.
- allign the code using instead of spaces. Keep the '=' character and default values alligned.
let parser=(
starttime:datetime=datetime(null)
, endtime:datetime=datetime(null)
, eventresult:string='*'
, disabled:bool=false)
{
let parser = (
starttime:datetime = datetime(null)
, endtime:datetime = datetime(null)
, srcipaddr_has_any_prefix:dynamic = dynamic([])
, dstipaddr_has_any_prefix:dynamic = dynamic([])
, ipaddr_has_any_prefix:dynamic = dynamic([])
, dstportnumber:int = int(null)
, hostname_has_any:dynamic = dynamic([])
, dvcaction:dynamic = dynamic([])
, eventresult:string = '*'
, disabled:bool = false
)
{
- Don't go crazy on indention of code. This doesn't help when reading the code in the kusto editor window.
let AuthProductName=(starttime:datetime=datetime(null)
, endtime:datetime=datetime(null)
, targetusername_has:string="*"
, disabled:bool=false){
let AuthProductName = (
starttime:datetime = datetime(null)
, endtime:datetime = datetime(null)
, targetusername_has:string = "*"
, disabled:bool = false
)
{
CommonSecurityLog | where not(disabled) | where TimeGenerated <= ago(5m)
CommonSecurityLog
| where not(disabled)
| where TimeGenerated <= ago(5m)
| parse-kv DeviceCustomString3 as (Host:string, Referer:string, ['User-Agent']:string, Accept:string, ['Content-Type']:string, ['X-Forwarded-For']:string) with (pair_delimiter=@'\r\n', kv_delimiter=': ')
| parse-kv DeviceCustomString3 as (
Host:string
, Referer:string
, ['User-Agent']:string
, Accept:string
, ['Content-Type']:string
, ['X-Forwarded-For']:string
)
with (
pair_delimiter=@'\r\n'
, kv_delimiter=': '
)
To give clarity on where something is happening in de code split the lines between the operator and the data.
- note: this is not the case for the
| where
operator OR when executed on a column.
| project-rename Src = SourceIP
, Dst = DestinationIP
, HttpRequestMethod = RequestMethod
, HttpReferrer = Referer
, HttpContentFormat = Accept
, HttpContentType = ['Content-Type']
, UserAgent = ['User-Agent']
, HttpRequestXff = ['X-Forwarded-For']
| project-rename
Src = SourceIP
, Dst = DestinationIP
, HttpRequestMethod = RequestMethod
, HttpReferrer = Referer
, HttpContentFormat = Accept
, HttpContentType = ['Content-Type']
, UserAgent = ['User-Agent']
, HttpRequestXff = ['X-Forwarded-For']
Extra spaces result in future edits where the only change is a space being added or removed
| project-rename..
TargetDvcHostname = Computer
, EventOriginalUid=EventOriginId..........
, EventOriginalType=EventID..
| extend EventCount=int(1)
, EventSchemaVersion='0.1.0'
, ActorUserIdType='SID'
, TargetUserIdType='SID'
, EventVendor='Microsoft'..
, EventStartTime =TimeGenerated...
, EventEndTime=TimeGenerated...
| project-rename
TargetDvcHostname = Computer
, EventOriginalUid = EventOriginId
, EventOriginalType = EventID
| extend
EventCount = int(1)
, EventSchemaVersion = var_schemaVersion
, ActorUserIdType = 'SID'
, TargetUserIdType = 'SID'
, EventVendor = var_eventVendor
, EventStartTime = TimeGenerated
, EventEndTime = TimeGenerated
White-space is (mostly) irrelevant to KQL, but its proper use is key to writing easily readable code.
Use a single space after commas and semicolons, and around pairs of curly braces.
Every braceable statement should also have the opening brace on the end of a line, and the closing brace at the beginning of a line.
let SeverityLookup = datatable(var_LogSeverity:string, EventSeverity:string)
[ "low", "Low"
,"medium", "Medium"
,"high", "High"
,"", "Informational"];
let ActionLookup = datatable(DvcOriginalAction:string, DvcAction:string)
[ "drop", "Drop"
, "forward", "Allow"
, "source-reset", "Reset Source"
, "dest-reset", "Reset Destination"
, "source-dest-reset", "Reset"
, "proxy", "Allow"
, "challenge", "Reset"
, "quarantine", "Reset"
, "drop-and-quarantine", "Drop"
, "allow", "Allow"];
let SeverityLookup = datatable (var_LogSeverity:string, EventSeverity:string) [
"low", "Low"
, "medium", "Medium"
, "high", "High"
, "", "Informational"
];
let ActionLookup = datatable (DvcOriginalAction:string, DvcAction:string) [
"drop", "Drop"
, "forward", "Allow"
, "source-reset", "Reset Source"
, "dest-reset", "Reset Destination"
, "source-dest-reset", "Reset"
, "proxy", "Allow"
, "challenge", "Reset"
, "quarantine", "Reset"
, "drop-and-quarantine", "Drop"
, "allow", "Allow"
];
- Large comment blocks don't improve the readability of the code and also brings allignment issues.
| lookup SeverityLookup on LogSeverity
// ************************
// <Constants>
// ************************
| extend
EventSchemaVersion = '0.1.1'
, EventSchema = "Authentication"
, EventCount = 1
, TargetUsernameType = "Simple"
// ************************
// <Aliases>
// ************************
| extend
EventOriginalUid = tostring(ExternalID)
, TargetDomain = DstDomain
, TargetDomainType = DstDomainType
, User = TargetUsername
, UserType = TargetUsernameType
, Dst = TargetIpAddr
| extend
AdditionalFields = pack(
| lookup SeverityLookup on LogSeverity
// extending with constant values
| extend
EventSchemaVersion = '0.1.1'
, EventSchema = 'Authentication'
, EventCount = 1
, TargetUsernameType = "Simple"
// adding aliases
| extend
EventOriginalUid = tostring(ExternalID)
, TargetDomain = DstDomain
, TargetDomainType = DstDomainType
, User = TargetUsername
, UserType = TargetUsernameType
, Dst = TargetIpAddr
| extend
AdditionalFields = pack(
Use line breaks between funtions and let variable declarations.
Try to keep code that is related close to create a logical flow when reading the code.
It does help to add a linebreak between let statements that are not related.
let LogonEvents=dynamic([4624,4625]);
let LogoffEvents=dynamic([4634,4647]);
let LogonTypes=datatable(LogonType:int, EventSubType:string)[
2, 'Interactive',
3, 'Network',
4, 'Batch',
5, 'Service',
7, 'Unlock'];
let LogonStatus=datatable
(EventStatus:string,EventOriginalResultDetails:string, EventResultDetails:string)[
'0x80090325', 'SEC_E_UNTRUSTED_ROOT','Other',
'0xc0000064', 'STATUS_NO_SUCH_USER','No such user or password',
'0xc000006f', 'STATUS_INVALID_LOGON_HOURS','Logon violates policy',
'0xc0000070', 'STATUS_INVALID_WORKSTATION','Logon violates policy',
let LogonEvents = dynamic([4624,4625]);
let LogoffEvents = dynamic([4634,4647]);
let LogonTypes=datatable(LogonType:int, EventSubType:string) [
2, 'Interactive'
, 3, 'Network'
, 4, 'Batch'
, 5, 'Service'
, 7, 'Unlock'
];
let LogonStatus=datatable(EventStatus:string,EventOriginalResultDetails:string, EventResultDetails:string) [
, '0x80090325', 'SEC_E_UNTRUSTED_ROOT','Other'
, '0xc0000064', 'STATUS_NO_SUCH_USER','No such user or password'
, '0xc000006f', 'STATUS_INVALID_LOGON_HOURS','Logon violates policy'
, '0xc0000070', 'STATUS_INVALID_WORKSTATION','Logon violates policy'
];
let 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.
let 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
let domain_regex = @'^([_a-zA-Z0-9][_a-zA-Z0-9-]{0,62}\.)*[a-zA-Z]{2,63}$';
let Hostname_regex = @'^[a-zA-Z0-9-]{1,61}$';
let MD5_regex = @'[a-zA-Z0-0]{32}';
let SHA1_regex = @'[a-zA-Z0-0]{40}';
let SHA256_regex = @'[a-zA-Z0-0]{64}';
let SHA512_regex = @'[a-zA-Z0-0]{128}';
let IPprotocol = materialize (externaldata (code: string, value: string)
[@"https://www.iana.org/assignments/protocol-numbers/protocol-numbers-1.csv"] with (format="csv", IgnoreFirstRecord=true) | project value);
let DnsQueryTypeName = materialize (externaldata (value: string)
[@"https://www.iana.org/assignments/dns-parameters/dns-parameters-4.csv"] with (format="csv", IgnoreFirstRecord=true));
let DnsResponseCodeName = materialize (externaldata (code: string, value: string)
[@"https://www.iana.org/assignments/dns-parameters/dns-parameters-6.csv"] with (format="csv", IgnoreFirstRecord=true) | project toupper(value));
//let DnsQueryClassName = materialize (externaldata (dec: string, dex: string, value: string)
//[@"https://www.iana.org/assignments/dns-parameters/dns-parameters-2.csv"] with (format="csv", IgnoreFirstRecord=true) | project value);
//let schemas_in_data = materialize (T | summarize schemas = make_set(EventSchema));
let ASimFields = materialize(externaldata (ColumnName: string, ColumnType: string, Class: string, Schema: string, LogicalType:string, ListOfValues: string, AliasedField: string)
[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/ASIM/dev/ASimTester/ASimTester.csv"] with (format="csv", IgnoreFirstRecord=true) | project-rename dict_schema = Schema);
let MACaddr_regex = @'^[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}$';
let FQDN_regex = @'^([_a-zA-Z0-9][_a-zA-Z0-9-]{0,62}\.)+[a-zA-Z]{2,63}$';
let DNS_domain_regex = @'^([_a-zA-Z0-9][_a-zA-Z0-9-]{0,62}\.)*[a-zA-Z]{2,63}$';
let domain_regex = @'^([_a-zA-Z0-9][_a-zA-Z0-9-]{0,62}\.)*[a-zA-Z]{2,63}$';
let Hostname_regex = @'^[a-zA-Z0-9-]{1,61}$';
let MD5_regex = @'[a-zA-Z0-0]{32}';
let SHA1_regex = @'[a-zA-Z0-0]{40}';
let SHA256_regex = @'[a-zA-Z0-0]{64}';
let SHA512_regex = @'[a-zA-Z0-0]{128}';
// let schemas_in_data = materialize (T | summarize schemas = make_set(EventSchema));
let IPprotocol = materialize (externaldata (code: string, value: string) [
@"https://www.iana.org/assignments/protocol-numbers/protocol-numbers-1.csv"
]
with (
format = "csv"
, IgnoreFirstRecord = true
)
| project
value
);
// let DnsQueryClassName = materialize (externaldata (dec: string, dex: string, value: string)
let DnsQueryTypeName = materialize (
externaldata (value: string) [
@"https://www.iana.org/assignments/dns-parameters/dns-parameters-4.csv"
]
with (
format = "csv"
, IgnoreFirstRecord = true
)
);