Overview
The ACKLOG Query Language is the language you type into the search box of the ACKLOG SIEM search screen. A query first selects events, then passes them through a pipeline of statements separated by the pipe character |. Each statement transforms the result of the previous one: filter, calculate, aggregate, sort, format and present.
<search part> | <statement> | <statement> | ...
UserName=adm* SourceIP=*| where SourceIP in [192.168.10.100, 10.16.8.10] | agg count by UserName | where count > 10 | sort - countKey facts
- The time range is chosen with the time picker of the search screen. Queries do not contain a time clause.
- Keywords and statement names are case-insensitive (
agg=AGG). Write field names and values as they appear in your logs. - Field names in the examples (
EventID,TargetUserName,SourceIP,DestinationPort, ...) are typical ACKLOG fields for Windows, firewall, proxy and web logs. Your field list depends on your log sources; the search screen lists the available fields and suggests them while you type. - A query may span several lines. Lines that start with
//are comments. - The search box editor suggests statements, fields, functions and values as you type, shows inline documentation on hover and marks errors before you run the query.
Ctrl+Iformats a long query. - Prefer plain language? The Ask AI button next to the search box writes the query for you. See AI query assistant.
Quick start
Ten queries that cover most daily work. Copy one, adjust the field names and run it.
*EventID=4625EventID=4625 | agg count by TargetUserName | sort - countEventID=4625 | latest 20SourceIP=10.0.0.0/8 DestinationPort=[22, 3389]TargetUserName=*admin* | table EventTime, NodeName, EventID, TargetUserNameNodeName=* | agg count by NodeName | timespan 1hSourceIP=* | agg count by SourceIP | iplocation SourceIP | size 20Page=*| where Page contains ["../", "etc/passwd"]| table EventTime, SourceIP, PageSourceIP=* sentbyte=* | agg sum(sentbyte) as Sent by SourceIP | sort - Sent | size 10 | fieldformat Sent=bytes(2)AI query assistant: describe it, get the query
You do not have to learn the syntax before you start. Click Ask AI next to the search box and describe what you want to find, the way you would ask a colleague. The query assistant writes the ACKLOG query, explains what it does, checks it, and puts it in the search box with one click. Every query on this page could just as well start as a sentence.
“Show me source IPs with more than 20 failed logons in the last 24 hours”
EventID=4625 IpAddress=* | agg count as Failures by IpAddress | where Failures > 20 | sort - Failures“List users added to administrator groups in the last 7 days”
EventID=[4728, 4732, 4756]| table EventTime, NodeName, SubjectUserName, MemberName, TargetUserName“Which internal hosts sent the most data? Show the totals in MB or GB”
SourceIP=10.0.0.0/8 sentbyte=* | agg sum(sentbyte) as Sent by SourceIP | sort - Sent | size 10 | fieldformat Sent=bytes(2)“How do I count events per hour for each log source?”
NodeName=* | agg count by NodeName | timespan 1hHow to use it
- Click Ask AI next to the search box.
- Describe what you want to find, or pick one of the suggested questions (logons, network, Windows events, query language).
- The assistant answers with one or more queries and a short explanation of each.
- Click Use query. The query goes into the search box and the time range is taken from your request, such as "last 24 hours" or "this week". Review it and run the search.
Why you can trust the result
- Checked before you see it. Every generated query is run through ACKLOG's own search engine. If the check finds a problem, the assistant corrects the query automatically, and each answer shows whether its query passed the check.
- Knows your environment. The assistant writes queries with the field names of your own log sources, not generic ones.
- Your logs stay private. The assistant only writes queries. It never reads your log data or your search results.
- English or Turkish. Ask in either language; explanations come back in the language you write in.
Fix with AI
When a query has an error, a Fix with AI button appears under the error message. It sends the query and the error to the assistant, which returns a corrected query and explains what was wrong. A quick way to learn the rules while you work.
History
Your questions are kept in the History tab of the assistant. Search them, reuse a query, or ask the same question again.
Search part: selecting events
The search part is everything before the first |. It selects events from the index and is the fastest way to narrow the data, so put as much filtering here as possible.
Free text and all events
*login.aspx"failed password"Field conditions
| Operator | Meaning | Example |
|---|---|---|
= == : | equals (all three are the same) | EventID=4625 EventID:4625 |
!= | not equal | EventID!=4624 |
> >= < <= | numeric comparison | SourcePort>1024 |
=* | the field exists (has any value) | SourceIP=* |
Page=*SourcePort>1024MemberName!="-"Wildcards
* matches any number of characters at the start, the end or both ends of a value.
Page=log*Page=*ogin.aspxUserName=*admin*AND, OR, NOT and parentheses
Terms separated by a space are combined with AND. Use or, and and not (any case) and parentheses for grouping.
EventID=4625 TargetUserName=adminPage=* AND (UserName=admin OR SourceIP=172.16.0.1)EventID=4625 AND NOT TargetUserName=*$not EventID=4624Quotes: phrases and exact values
| Form | Use it for | Example |
|---|---|---|
| no quotes | single words, numbers, IPs, wildcards | UserName=admin |
double quotes "..." | phrases and values with spaces or special characters | Message="access denied" |
single quotes '...' | an exact match of the whole field value | TargetUserName='Administrator' |
Lists of values
A bracketed list matches any of the values (OR).
EventID=[4624, 4625, 4644]DestinationPort=[22, 3389, 5985]IP subnets and ranges
SourceIP=10.0.0.0/16SourceIP=[10.10.1.1 TO 10.12.5.254]Predefined lists: {@ListName}
Values kept in an ACKLOG list (for example critical event IDs, VIP accounts or blocked IPs) can be used inside a value list with {@ListName}. The list is expanded when the query runs, so the query stays short and always uses the current list content.
EventID=[4026, 4025, {@CriticalEventIDs}, 6012]Comments and multi-line queries
// failed logons per accountEventID=4625| agg count by TargetUserName| sort - countThe pipeline and the two contexts
Statements after the search part run in order. A query has two contexts:
- Search Context — from the start of the query up to
agg. Statements work on individual events (documents). - Aggregation Context — after
agg. Statements work on the rows of the aggregation result table. Column names here are the names produced by AGG (for examplecount,dc(Page)or a name given withas).
| Statement | Search Context | Aggregation Context | Purpose |
|---|---|---|---|
| where | yes | yes | filter events / result rows |
| eval | yes | yes | add a calculated field / column |
| agg | yes (once) | — | aggregate; opens the Aggregation Context |
| timespan | — | yes (once) | group by time buckets |
| sort | yes | yes | order results |
| size | yes (once) | yes (once) | limit events / rows |
| latest / earliest | yes (once) | — | newest / oldest n events |
| table | yes (once) | — | one row per event with chosen fields |
| fields | yes (once) | yes (once) | return only chosen fields / columns |
| rename | yes | yes (once, last) | rename fields / columns |
| drop | — | yes (once) | remove columns |
| iplocation | yes | yes | add country and city of an IP |
| lookup | yes | yes | enrich from a CSV file |
| transaction | yes (once) | — | group related events |
| fieldformat | — | yes | display format of columns |
Ordering rules
aggcan be used once per query.tablecannot be combined withagg.latestandearliestcannot be combined with each other, withsizeor withsort.- In the Search Context
tableandfieldscannot be combined. - In the Aggregation Context
fieldsmust come beforesort, andrenamemust be the last statement. dropandfieldformatrefer to column names as they are beforerename.- Aggregation results are sorted in descending order when no
sortis given.
Statement reference
WHERE — filter events or result rows
where <field> <= | == | != | > | >= | < | <=> <value>
where <field> in | notin [<value>, ...]
where <field> contains | notcontains [<value>, ...]
where <field> startswith | notstartswith [<value>, ...]
where <field> endswith | notendswith [<value>, ...]
where exists | notexists [<field>, ...]
In the Search Context WHERE filters events; after AGG it filters result rows, so it can test counts and calculated columns. Each WHERE holds one condition; chain several WHERE statements for more. Text comparisons are case-insensitive.
UserName=* SourceIP=* | where SourceIP in [10.0.0.1, 10.0.0.2]EventID=4625 | where TargetUserName startswith [adm, svc]TargetUserName=*| where TargetUserName endswith ["$"] | agg count by TargetUserNameEventID=4625| where exists [IpAddress] | agg count by IpAddressUserName=* SourceIP=* | agg count by SourceIP | where count > 10SourceIP=* | agg count by SourceIP | where SourceIP notin [10.0.0.1, 10.0.0.2]EVAL — calculated fields
eval <newField>=<expression>
Creates one new field (Search Context) or column (Aggregation Context). Chain EVAL statements to create several. See EVAL functions for every expression.
DNSDomainName=* SourceIP=*| eval DNSDomainNameLen=len(DNSDomainName)| where DNSDomainNameLen > 100 | agg count, values(DNSDomainName) by SourceIPUserName=* SourceIP=*| eval SourceCnt=concat(SourceIPCountryCode, "-", SourceIPCountryName) | agg count by SourceCntEventID=[4624, 4625] | agg count(EventID) where EventID=4624 as SuccessfulLogons, count(EventID) where EventID=4625 as FailedLogons by TargetUserName | eval FailRate=div(FailedLogons, SuccessfulLogons) | sort - FailRateAGG — aggregate (count, sum, distinct count ...)
agg <function> [where <condition>] [as <name>] [, ...] [by <field>, ...]
function: count | count(f) | sum(f) | avg(f) | min(f) | max(f) | minstr(f) | maxstr(f) | dc(f) | values(f) | var(f) | stdev(f)
Groups events by the BY fields and calculates one or more functions per group. AGG opens the Aggregation Context. Without AS, a column is named after the function as written: count, dc(Page), sum(SentByte). Each function may have its own WHERE; when more than one function uses WHERE, give each a name with AS. See aggregation functions.
Taxonomy=Authentication.Fail SourceIP=* | agg count by SourceIP, UserName | sort - countNodeName=IIS* SourceIPCountryCode=* | agg count by SourceIPCountryCodePage=* SourceIP=* | agg dc(Page) as DistinctCount, values(Page) by SourceIP | where DistinctCount > 200SentBytes=* SourceIP=* | agg count as NumberofLogs, sum(SentBytes) as TotalSentBytes by SourceIP | sort - TotalSentBytesUrl=* SourceIP=* | agg count as TotalRequests, count where HttpStatus startswith [4, 5] as ErrorResponses by SourceIP | sort - ErrorResponsesUserName=* | agg dc(DestinationIP) where DestinationPort in [3389, 22] as RemoteAccessTargets by UserName | where RemoteAccessTargets > 10NodeName=trappot* | agg min(EventTime) where EventType="startup" as BootTime, min(EventTime) where EventType != "startup" as FirstAttackTime by NodeNameTaxonomy=Authentication.Fail SourceIP=* | agg count as FailedLogons, count where notexists [MFAToken, SmartcardId] as FailedLogonsWithoutMFA by SourceIP | sort - FailedLogonsWithoutMFATIMESPAN — time buckets
timespan <n><ms|s|m|h|d> [as <name>]
Splits the aggregation into time buckets and adds a time column (named timespan unless AS is given). Very short intervals over long time ranges produce many rows and slow responses.
Method=* Page=* SourceIP=* | agg dc(Page) as DistinctCount by SourceIP | timespan 1hEventID=4625 | agg count by TargetUserName | timespan 15mSORT — order results
sort [+|-] <field> [, ...]
+ (default) is ascending, - is descending. After AGG, sort by any column, including unnamed ones such as sum(SentByte).
SourceIP=* DestinationIP=* SentBytes>100| sort SourceIP, DestinationIP| table SourceIP, DestinationIP, SentBytes| size 100SourceIP=* SentByte=* | agg sum(SentByte) by SourceIP | sort - sum(SentByte)SIZE — limit events or rows
size <n>
UserName=*admin* or User=*admin* | size 10(UserName=*admin* or User=*admin*) Hostname=* | agg count by Hostname | size 5LATEST and EARLIEST — newest or oldest events
latest [n]
earliest [n]
Return the n newest (LATEST) or oldest (EARLIEST) events by EventTime. n defaults to 1. Cannot be combined with SIZE or SORT.
EventID=4625 | latest 20SourceIP=172.70.10.11 NodeName=IIS_Server | earliest 5EventID=4625 | latestTABLE — one row per event
table <field> [as <name>] [, ...]
Shows events in the TABLE tab with the chosen fields as columns, in the given order. Cannot be combined with AGG or FIELDS.
TargetUserName=* IpAddress=* EventID=4625| table TargetUserName as USER_NAME, IpAddress, EventTimeFIELDS — keep only some fields or columns
fields <field> [, ...]
In the Search Context, returns only the listed fields, which makes large result sets faster to load. After AGG, keeps only the listed columns (names before RENAME; unknown names are ignored) and must come before SORT. Usable once in each context.
SourceIP=* DestinationIP=* DestinationPort=*| fields SourceIP, DestinationIP, DestinationPortDestinationPort=445 or SourcePort=445 | agg sum(sentbyte) as sumsentbyte, sum(rcvdbyte) as sumrcvdbyte by UserName | eval TotalBytes=add(sumsentbyte, sumrcvdbyte) | fields UserName, TotalBytesRENAME — rename fields or columns
Search Context: rename <field> as <name>
Aggregation Context: rename <column> as <name> [, <column> as <name> ...]
In the Search Context RENAME copies one field under a new name (the original is kept); use one RENAME per field. After AGG it renames columns, may list several, can be used once and must be the last statement.
srcip=* dstip=*| rename srcip as Source_IP| rename dstip as Destination_IP| table Source_IP, Destination_IP| size 1000TargetUserName=* EventID=4625 | agg count by TargetUserName | rename TargetUserName as USER_NAME, count as FAILED_AUTH_COUNTDROP — remove columns
drop <column> [, <column> ...]
Aggregation Context only, once. Typical use: remove helper columns after an EVAL. At least one column must remain.
DestinationPort=445 or SourcePort=445 | agg sum(sentbyte) as sumsentbyte, sum(rcvdbyte) as sumrcvdbyte by UserName, SourceIP | timespan 1h | eval TotalBytes=add(sumsentbyte, sumrcvdbyte) | sort - TotalBytes | drop sumsentbyte, sumrcvdbyteIPLOCATION — country and city of an IP
iplocation <ipField> [, ...]
Adds <field>Country and <field>City. For example IpAddress gives IpAddressCountry and IpAddressCity.
srcIp=* | iplocation srcIpsrcIp=* | agg count by srcIp | iplocation srcIpLOOKUP — enrich from a CSV file
lookup <file.csv> <lookupColumn> as <eventField> output <lookupColumn> as <newField>
Matches a field against a column of an uploaded CSV file and copies another column into the result. The file name must include its extension.
EventID=* | agg count by EventID | lookup EventIDs.csv EventID as EventID output Explanation as EventDescEventID=[4728, 4732] MemberName!="-"| lookup EventIDs.csv EventID as EventID output Explanation as EventDesc| eval UserName=regex(MemberName, "(?i)cn=([^,]+)")| table EventTime, EventID, EventDescTRANSACTION — group related events
transaction <field> [, ...] [group_must_include=(<field>=[<value>, ...] [, ...])]
Groups events that share the same values of the listed fields into one transaction. group_must_include keeps only groups that contain the listed values. Most useful with TABLE.
EventID=[4663, 4660] ObjectName!=$Recycle.Bin| transaction SubjectLogonId, HandleId| table EventTime, NodeName, ObjectNameEventID=[4663, 4660]| transaction SubjectLogonId, HandleId group_must_include=(EventID=[4663, 4660])| table EventTime, NodeNameFIELDFORMAT — display format of columns
fieldformat <column> = <formatFunction>([<argument>, ...]) [, <column> = <formatFunction>(...) ...]
Aggregation Context only. Changes how a column is displayed; the underlying numbers stay the same, so sorting and the GRAPH / MAP tabs still use real values. Column names are the names before RENAME. Several FIELDFORMAT statements can be chained, for example a number format followed by a suffix. A formatted column holds text, so do not use it in a later EVAL or WHERE. See format functions.
user=* SentByte=* | agg sum(SentByte) as SentByte by user | sort - SentByte | fieldformat SentByte=dots(0)SourceIP=* SentByte=* | agg sum(SentByte) by SourceIP | sort - sum(SentByte) | fieldformat sum(SentByte)=bytes(2)user=* SentByte=* | agg sum(SentByte) as Traffic by user | fieldformat Traffic=dots(2) | fieldformat Traffic=suffix(" bytes")NodeName=* | agg min(EventTime) as FirstSeen by NodeName | fieldformat FirstSeen=strftime("yyyy.MM.dd HH:mm:ss")user=* SentByte=* | agg sum(SentByte) as SentByte by user | sort - SentByte | fieldformat SentByte=dots(0) | rename SentByte as SENT_BYTESAggregation functions
| Function | Returns | Notes |
|---|---|---|
count | number of events in the group | |
count(f) | number of events where f has a value | |
dc(f) | distinct count: number of different values of f | scanning, spraying and fan-out detection |
values(f) | list of the distinct values of f | can time out on large data sets |
sum(f) | sum | numeric fields |
avg(f) | average | numeric fields |
min(f) max(f) | smallest / largest value | numeric and time fields |
minstr(f) maxstr(f) | alphabetically first / last value | text fields |
var(f) | variance | numeric fields |
stdev(f) | standard deviation | numeric fields |
NodeID="*_*" | agg dc(SourceIP), count where exists [Password] by ModuleNameEVAL functions
| Function | Description |
|---|---|
len(f) | character length |
substr(f, start, end) | part of a text; negative indexes count from the end |
lower(f) upper(f) | change case |
int(f) str(f) | convert to integer / text |
concat(f|"text", ...) | join fields and constants |
split(f, "delimiter", index) | split and return the part at index (0-based) |
regex(f, "pattern") | return the first capture group; (?i) makes it case-insensitive |
strftime(dateField, format) | format a date; write the format without quotes and without spaces, e.g. yyyy-MM-dd |
add(a, b, ...[, decimals]) | sum of two or more operands |
sub(a, b[, decimals]) | a − b |
mul(a, b, ...[, decimals]) | product |
div(a, b[, decimals]) | a ÷ b; division by zero returns 99999999 |
calc(expression[, decimals]) | arithmetic with + − * /, unary minus and parentheses |
coalesce(f|"text", f|"text", ...) | first argument that has a value |
case(cond => result, ... [, default => result]) | result of the first matching condition |
Text functions
NodeName=*| eval N=upper(NodeName)| eval L=len(NodeName)| eval P=substr(NodeName, 0, 3)| eval C=concat(NodeName, "-", L)| table N, L, P, C| size 3SourceIP=*| eval Net=regex(SourceIP, "^([0-9]+[.][0-9]+)[.]") | agg count by NetUserName=*| eval U=lower(UserName)| eval Domain=split(UserName, "\\", 0)| table EventTime, U, DomainDate functions
NodeName=*| eval Hour=strftime(EventTime, HH) | agg count by Hour | sort HourNodeName=*| eval Day=strftime(EventTime, yyyy-MM-dd) | agg count by DayArithmetic
SourcePort=* DestinationPort=*| eval S=sub(SourcePort, DestinationPort)| eval M=mul(SourcePort, 2)| eval A=add(SourcePort, DestinationPort)| table S, M, A| size 3DestinationPort=445 or SourcePort=445 | agg sum(sentbyte) as sumsentbyte, sum(rcvdbyte) as sumrcvdbyte by SourceIP | eval TotalKB=calc((sumsentbyte + sumrcvdbyte) / 1024, 0) | sort - TotalKBCOALESCE
UserName=*| eval Actor=coalesce(UserName, SamAccountName, "UNKNOWN") | agg count by ActorNodeName=* | agg min(EventTime) where EventType="startup" as BootTime, min(EventTime) where EventType != "startup" as FirstEventTime by NodeName | eval RepresentativeTime=coalesce(BootTime, FirstEventTime)CASE
Conditions use these operators: eq, ne, lt, le, gt, ge, in, notin, contains, notcontains, startswith, notstartswith, endswith, notendswith. A result is a constant or field(<name>). default must be last.
EventID=[4624, 4625]| eval Outcome=case(EventID eq 4624 => success, EventID eq 4625 => failure, default => other) | agg count by OutcomeHttpStatus=*| eval Bucket=case(HttpStatus startswith [4, 5] => error, Action in [deny, drop, reset] => blocked, default => ok)SourcePort=*| eval Direction=case(SourcePort gt field(DestinationPort) => "src-higher", SourcePort eq field(DestinationPort) => equal, default => "dst-higher")SourceIP=* | agg count by SourceIP | eval Level=case(count gt 1000 => high, count gt 100 => medium, default => low)FIELDFORMAT functions
| Function | Output example | Description |
|---|---|---|
num([decimals][, "group"][, "decimal"]) | 1,234,567.89 | general number format with custom separators |
commas([decimals]) | 1,234,567.89 | comma groups, dot decimals |
dots([decimals]) | 1.234.567,89 | dot groups, comma decimals |
spaces([decimals]) | 1 234 567,89 | space groups, comma decimals |
percent([decimals]) | 12% | ratio × 100 with %; pairs with div() |
bytes([decimals][, 1024|1000]) | 1.18 MB | scales to B, KB, MB, GB, TB, PB, EB |
duration([s|ms]) | 01:02:03 | seconds (or ms) as HH:MM:SS, D+HH:MM:SS beyond a day |
strftime("format"[, s|ms]) | 2026.01.31 14:05:00 | epoch value as a date |
printf("format") | 1234.57 ms | C-style format with exactly one placeholder (%d %x %f %e %g %s) |
prefix("text") suffix("text") | $ 1.234 / 1.234 MB | add text before / after an already formatted value |
EventID=[4624, 4625] | agg count(EventID) where EventID=4624 as SuccessfulLogons, count(EventID) where EventID=4625 as FailedLogons by TargetUserName | eval FailRate=div(FailedLogons, SuccessfulLogons) | fieldformat FailRate=percent(2) | sort - FailRateuser=* SentByte=* | agg sum(SentByte) as SentByte, count as Requests by user | fieldformat SentByte=dots(2), Requests=dots(0)SourceIP=* | agg avg(ResponseTime) as AvgResponse by SourceIP | fieldformat AvgResponse=printf("%.2f ms")Scenario cookbook: security queries
Ready-to-use queries for common detection and threat hunting tasks. Event IDs are Windows Security log IDs; adjust field names to your log sources and thresholds to your environment.
Authentication
Brute force: many failed logons from one source
Goal: Find source addresses with many failed logons.
EventID=4625 IpAddress=* | agg count as Failures, dc(TargetUserName) as Users by IpAddress | where Failures > 20 | sort - FailuresAsk AI Or just ask: “Which source IPs had more than 20 failed logons, and how many accounts did each try?”
How it works: 4625 is a failed logon. Counting per IpAddress and keeping rows above a threshold shows the noisiest sources; Users shows how many accounts each tried.
Password spraying: one source, many accounts
Goal: Find sources that try a few passwords against many different accounts.
EventID=4625 IpAddress=* | agg dc(TargetUserName) as DistinctUsers, count as Attempts by IpAddress | where DistinctUsers > 10 | sort - DistinctUsersAsk AI Or just ask: “Find sources that tried to log on to more than 10 different accounts”
How it works: Spraying shows up as a high distinct count of accounts (dc) rather than a high count on one account.
Successful logon after repeated failures
Goal: Find account and source pairs that failed many times and then succeeded.
EventID=[4624, 4625] IpAddress=* | agg count where EventID=4625 as Fail, count where EventID=4624 as Success by IpAddress, TargetUserName | where Fail > 10 | where Success > 0Ask AI Or just ask: “Show accounts that logged on successfully after more than 10 failed attempts from the same IP”
How it works: Two conditional counts in one AGG give failures and successes side by side; the two WHERE statements keep pairs that match both conditions.
Failed logon rate per account
Goal: Rank accounts by the ratio of failed to successful logons.
EventID=[4624, 4625] | agg count(EventID) where EventID=4624 as SuccessfulLogons, count(EventID) where EventID=4625 as FailedLogons by TargetUserName | eval FailRate=div(FailedLogons, SuccessfulLogons) | fieldformat FailRate=percent(2) | sort - FailRateAsk AI Or just ask: “Rank users by their failed logon rate as a percentage”
How it works: DIV calculates the ratio, PERCENT shows it as a percentage while SORT still uses the real number.
Failed logons over time
Goal: See when failed logons peak.
EventID=4625 | agg count by TargetUserName | timespan 15mAsk AI Or just ask: “Show failed logons per user in 15 minute intervals”
How it works: TIMESPAN buckets the counts into 15-minute intervals, which the GRAPH tab can plot.
RDP logons per account
Goal: List who logged on over Remote Desktop and from how many addresses.
EventID=4624 LogonType=10 | agg count, dc(IpAddress) as Sources by TargetUserName | sort - countAsk AI Or just ask: “Who logged on over RDP, and from how many different addresses?”
How it works: Logon type 10 (RemoteInteractive) is an RDP logon.
RDP logons with country and city
Goal: Spot RDP logons from unexpected countries.
EventID=4624 LogonType=10 IpAddress=*| iplocation IpAddress| table EventTime, TargetUserName, IpAddress, IpAddressCountry, IpAddressCityAsk AI Or just ask: “List RDP logons with the country and city of the source IP”
How it works: IPLOCATION adds IpAddressCountry and IpAddressCity to each event.
Activity of admin and service accounts
Goal: Count failed logons for accounts whose names start with adm or svc.
EventID=4625| where TargetUserName startswith [adm, svc] | agg count by TargetUserName | sort - countAsk AI Or just ask: “Count failed logons for accounts starting with adm or svc”
How it works: STARTSWITH with a list matches any of the prefixes, case-insensitively.
Accounts and audit
New user accounts
Goal: Who created which account, and where.
EventID=4720| table EventTime, SubjectUserName as CreatedBy, TargetUserName as NewAccount, NodeNameAsk AI Or just ask: “Which user accounts were created, and by whom?”
How it works: 4720 is "a user account was created". TABLE with AS gives readable column names.
Members added to security groups
Goal: Track additions to global, local and universal security groups.
EventID=[4728, 4732, 4756]| table EventTime, NodeName, SubjectUserName, MemberName, TargetUserNameAsk AI Or just ask: “Users added to security groups this week”
How it works: 4728, 4732 and 4756 are member-added events for the three group scopes; TargetUserName is the group.
Audit log cleared
Goal: Detect clearing of the Security or System event log.
EventID=1102 or EventID=104 | table EventTime, NodeName, SubjectUserNameAsk AI Or just ask: “Has anyone cleared the security or system event log?”
How it works: 1102 (Security log) and 104 (System log) are written when a log is cleared, a common anti-forensics step.
Who deleted which file
Goal: Correlate object access and delete events of the same file handle.
EventID=[4663, 4660]| transaction SubjectLogonId, HandleId group_must_include=(EventID=[4663, 4660])| table EventTime, NodeName, ObjectNameAsk AI Or just ask: “Who deleted which files?”
How it works: 4663 holds the file name, 4660 the deletion. TRANSACTION joins them by logon and handle, group_must_include keeps only handles that have both.
Network
Lateral movement: fan-out over remote admin ports
Goal: Find sources that connect to many hosts over SSH, RDP, WinRM or SMB.
DestinationPort=[22, 3389, 5985, 445] SourceIP=* | agg dc(DestinationIP) as Targets by SourceIP | where Targets > 20 | sort - TargetsAsk AI Or just ask: “Find hosts connecting to more than 20 machines over SSH, RDP, WinRM or SMB”
How it works: A workstation normally talks to few hosts on admin ports; a high distinct count of destinations is suspicious.
Port scan
Goal: Find source and destination pairs with many distinct destination ports.
SourceIP=* DestinationPort=* | agg dc(DestinationPort) as Ports by SourceIP, DestinationIP | where Ports > 50 | sort - Ports | size 20Ask AI Or just ask: “Detect port scans: sources hitting more than 50 ports on the same host”
How it works: Vertical scans touch many ports on one host; dc(DestinationPort) per pair exposes them.
Top talkers by volume
Goal: Which internal hosts sent the most data.
SourceIP=10.0.0.0/8 sentbyte=* | agg sum(sentbyte) as Sent by SourceIP | sort - Sent | size 10 | fieldformat Sent=bytes(2)Ask AI Or just ask: “Top 10 internal hosts by data sent, in MB or GB”
How it works: CIDR limits the search to internal sources; BYTES shows the totals as KB/MB/GB.
Possible data exfiltration: upload much larger than download
Goal: Find hosts whose sent bytes are far higher than received bytes.
SourceIP=* sentbyte=* rcvdbyte=* | agg sum(sentbyte) as Up, sum(rcvdbyte) as Down by SourceIP | eval Ratio=div(Up, Down, 2) | where Ratio > 10 | sort - RatioAsk AI Or just ask: “Hosts that upload at least 10 times more than they download”
How it works: Normal browsing downloads more than it uploads. A high upload/download ratio is worth a look.
SMB traffic volume per host
Goal: Total SMB (445) traffic per source in KB.
DestinationPort=445 or SourcePort=445 | agg sum(sentbyte) as s, sum(rcvdbyte) as r by SourceIP | eval TotalKB=calc((s + r) / 1024, 0) | sort - TotalKB | drop s, r | size 10Ask AI Or just ask: “Total SMB traffic per host in KB, top 10”
How it works: CALC adds and converts in one expression; DROP removes the helper columns.
DNS tunneling: very long domain names
Goal: Find sources that resolve unusually long names.
DNSDomainName=*| eval Len=len(DNSDomainName)| where Len > 60 | agg count, dc(DNSDomainName) as Names by SourceIP | sort - NamesAsk AI Or just ask: “Find sources querying unusually long domain names”
How it works: Tunneling tools encode data in sub-domains, producing long and unique names.
Web
Web attack patterns in URLs
Goal: Requests with path traversal, SQL injection or XSS patterns.
Page=*| where Page contains ["../", "etc/passwd", "union select", "<script"]| table EventTime, SourceIP, Method, Page, HttpStatusAsk AI Or just ask: “Show web requests that look like path traversal, SQL injection or XSS”
How it works: CONTAINS with a list matches any pattern, case-insensitively.
Sources with a high HTTP error rate
Goal: Find clients whose requests mostly fail (scanners, brute forcers).
HttpStatus=* SourceIP=* | agg count as Total, count where HttpStatus startswith [4] as ClientErrors by SourceIP | eval ErrRate=div(ClientErrors, Total, 4) | where Total > 100 | sort - ErrRate | fieldformat ErrRate=percent(1)Ask AI Or just ask: “Which clients get the highest rate of 4xx errors?”
How it works: A conditional count gives 4xx responses next to the total; DIV and PERCENT give the rate.
Crawling and content discovery
Goal: Sources that requested many distinct pages per hour.
Method=* Page=* SourceIP=* | agg dc(Page) as DistinctCount by SourceIP | timespan 1hAsk AI Or just ask: “Sources requesting many distinct pages per hour”
How it works: Distinct pages per hour separates crawlers from normal users.
Log source health
Event volume per log source per hour
Goal: Spot sources whose volume drops or spikes.
NodeName=* | agg count by NodeName | timespan 1hAsk AI Or just ask: “Event count per log source per hour”
How it works: Plot it in the GRAPH tab; gaps mean a source stopped sending.
Last event time of each log source
Goal: Find silent log sources.
NodeName=* | agg max(EventTime) as LastSeen by NodeName | sort LastSeen | fieldformat LastSeen=strftime("yyyy-MM-dd HH:mm:ss")Ask AI Or just ask: “When did each log source send its last event?”
How it works: Sorting ascending puts the sources that have been silent the longest at the top.
Rules and common errors
The editor marks these problems while you type.
| Message / rule | Fix |
|---|---|
| Unknown statement | Check the spelling after |. See the statement list. |
| "x" cannot be used in the Aggregation Context | Move the statement before agg, or use the Aggregation Context alternative. |
| "table" and "agg" cannot be used together | Use either TABLE (rows per event) or AGG (grouped rows). |
| "latest" and "size"/"sort" cannot be used together | LATEST/EARLIEST already sort and limit. |
| "rename" must be the last statement in the Aggregation Context | Move RENAME to the end. |
| "fields" must be written before "sort" | After AGG, put FIELDS before SORT. |
| More than one aggregation function uses WHERE | Name each conditional function with as. |
| The column name is produced more than once | Give each function a distinct name with as. |
| Column does not exist in the aggregation results | DROP, FIELDS and FIELDFORMAT use the column names before RENAME. |
| At least one column must remain after DROP | Drop fewer columns. |
| Invalid duration | TIMESPAN takes <number><ms|s|m|h|d>, for example 1h or 15m. |
| The CSV file name must include its extension | Write EventIDs.csv, not EventIDs. |
| Unterminated quote / Unclosed bracket | Close every ", ', ( and [. |
Performance tips
- Filter as much as possible in the search part; it is evaluated by the index and is the fastest step.
- Use
field=*to keep only events that have the fields you aggregate on. - Prefer exact values and trailing wildcards (
abc*) over leading wildcards andcontains. values()and very short TIMESPAN intervals can produce very large results.- In the Search Context,
fieldsreduces the data sent to the browser.
Deprecated: DERIVE
Older queries may contain | derive Name=<expression> after AGG. It still runs but is deprecated: use | eval Name=<expression> instead.
Frequently asked questions
Can AI write ACKLOG queries for me?
Yes. Click Ask AI next to the search box and describe what you want to find in plain language, for example "failed logons per user in the last 24 hours". The query assistant writes the ACKLOG query, explains it, checks it with the search engine and puts it in the search box with one click.
Can I ask the ACKLOG query assistant in Turkish?
Yes. Write your request in English or Turkish; the explanation comes back in the language you write in. The query itself is always ACKLOG syntax.
Does the AI query assistant read my logs?
No. The assistant only writes queries. It never reads your log data or your search results; you run the query yourself.
How do I search for all events in ACKLOG?
Type * in the search box. The time range comes from the time picker next to the search box, not from the query.
How do I count events per user, IP or host in ACKLOG?
Use the AGG statement: EventID=4625 | agg count by TargetUserName. Add | sort - count to see the largest values first.
What is the difference between the search part and WHERE?
The search part (everything before the first |) selects events from the index and is the fastest filter. | where filters what the earlier steps produced; after agg it filters result rows, so it can filter on counts like where count > 10.
How do I search for an exact value or a phrase?
Use double quotes for a phrase or a value with spaces: "failed password". Use single quotes for an exact match of the whole field value: TargetUserName='Administrator'.
How do I match several values for one field?
Use a bracketed list: EventID=[4624, 4625, 4634]. It means EventID=4624 OR EventID=4625 OR EventID=4634.
How do I search an IP range or subnet?
Use CIDR notation SourceIP=10.0.0.0/16 or an explicit range SourceIP=[10.10.1.1 TO 10.12.5.254].
How do I group results by time, such as per hour?
Add TIMESPAN after AGG: NodeName=* | agg count by NodeName | timespan 1h. Units are ms, s, m, h and d.
How do I create a calculated field?
Use EVAL: | eval NameLen=len(UserName). Each EVAL creates one field; chain several EVAL statements for more.
How do I rename columns in the result table?
Use RENAME: | agg count by TargetUserName | rename TargetUserName as USER, count as FAILS. After AGG, RENAME must be the last statement.
How do I show bytes as KB/MB/GB or numbers with thousands separators?
Use FIELDFORMAT after AGG: | fieldformat Sent = bytes(2) or | fieldformat Total = dots(0). It changes only how values are displayed; sorting still uses the real numbers.
Are ACKLOG query keywords case-sensitive?
No. agg, AGG and Agg are the same, as are and, AND, or, OR, not, NOT. Field names and values should be written as they appear in your logs.
Can I add comments to a query?
Yes. A line that starts with // is a comment and is ignored when the query runs.