ACKLOG ACKLOG Docs

ACKLOG Query Language Reference and Examples

How to write queries in the ACKLOG SIEM search box: syntax, operators, every pipeline statement and function, and 117 example queries, including a cookbook of security detection scenarios.

Last updated · Also available as Markdown

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 - count
Selects events whose UserName starts with "adm" and that have a SourceIP, keeps only two source addresses, counts events per user, keeps users with more than 10 events and lists the largest counts first.

Key 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+I formats 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.

*
All events in the selected time range.
EventID=4625
Windows failed logon events.
EventID=4625  | agg count by TargetUserName  | sort - count
Failed logons per account, most failures first.
EventID=4625 | latest 20
The 20 most recent failed logons.
SourceIP=10.0.0.0/8 DestinationPort=[22, 3389]
SSH or RDP connections from the 10.x.x.x network.
TargetUserName=*admin* | table EventTime, NodeName, EventID, TargetUserName
Events for accounts that contain "admin", shown as a table.
NodeName=*  | agg count by NodeName  | timespan 1h
Event volume per log source per hour.
SourceIP=*  | agg count by SourceIP  | iplocation SourceIP  | size 20
Top 20 source IPs with country and city.
Page=*| where Page contains ["../", "etc/passwd"]| table EventTime, SourceIP, Page
Web requests with path traversal patterns.
SourceIP=* sentbyte=*  | agg sum(sentbyte) as Sent by SourceIP  | sort - Sent  | size 10  | fieldformat Sent=bytes(2)
Top 10 senders by volume, shown as KB/MB/GB.

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.

You ask

“Show me source IPs with more than 20 failed logons in the last 24 hours”

Ask AI writes
EventID=4625 IpAddress=*  | agg count as Failures by IpAddress  | where Failures > 20  | sort - Failures
The time range "last 24 hours" is applied to the time picker when you click Use query.
You ask

“List users added to administrator groups in the last 7 days”

Ask AI writes
EventID=[4728, 4732, 4756]| table EventTime, NodeName, SubjectUserName, MemberName, TargetUserName
You ask

“Which internal hosts sent the most data? Show the totals in MB or GB”

Ask AI writes
SourceIP=10.0.0.0/8 sentbyte=*  | agg sum(sentbyte) as Sent by SourceIP  | sort - Sent  | size 10  | fieldformat Sent=bytes(2)
You ask

“How do I count events per hour for each log source?”

Ask AI writes
NodeName=*  | agg count by NodeName  | timespan 1h
The assistant also answers how-to questions about the query language.

How 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

*
All events.
login.aspx
Events that contain "login.aspx" in any field.
"failed password"
Events that contain the exact phrase "failed password".

Field conditions

OperatorMeaningExample
= == :equals (all three are the same)EventID=4625 EventID:4625
!=not equalEventID!=4624
> >= < <=numeric comparisonSourcePort>1024
=*the field exists (has any value)SourceIP=*
Page=*
Events that have a Page field.
SourcePort>1024
Source port greater than 1024.
MemberName!="-"
MemberName is not "-".

Wildcards

* matches any number of characters at the start, the end or both ends of a value.

Page=log*
Page starts with "log".
Page=*ogin.aspx
Page ends with "ogin.aspx".
UserName=*admin*
UserName contains "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=admin
Both conditions (implicit AND).
Page=* AND (UserName=admin OR SourceIP=172.16.0.1)
Page exists and either the user is admin or the source is 172.16.0.1.
EventID=4625 AND NOT TargetUserName=*$
Failed logons, excluding machine accounts (names ending with $).
not EventID=4624
Every event except successful logons.

Quotes: phrases and exact values

FormUse it forExample
no quotessingle words, numbers, IPs, wildcardsUserName=admin
double quotes "..."phrases and values with spaces or special charactersMessage="access denied"
single quotes '...'an exact match of the whole field valueTargetUserName='Administrator'

Lists of values

A bracketed list matches any of the values (OR).

EventID=[4624, 4625, 4644]
EventID is 4624, 4625 or 4644.
DestinationPort=[22, 3389, 5985]
SSH, RDP or WinRM ports.

IP subnets and ranges

SourceIP=10.0.0.0/16
CIDR subnet.
SourceIP=[10.10.1.1 TO 10.12.5.254]
An explicit IP range (inclusive).

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]
Literal values and the content of the CriticalEventIDs list.

Comments and multi-line queries

// failed logons per accountEventID=4625| agg count by TargetUserName| sort - count
A line that starts with // is ignored. Each pipe may start a new line.

The 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 example count, dc(Page) or a name given with as).
StatementSearch ContextAggregation ContextPurpose
whereyesyesfilter events / result rows
evalyesyesadd a calculated field / column
aggyes (once)—aggregate; opens the Aggregation Context
timespan—yes (once)group by time buckets
sortyesyesorder results
sizeyes (once)yes (once)limit events / rows
latest / earliestyes (once)—newest / oldest n events
tableyes (once)—one row per event with chosen fields
fieldsyes (once)yes (once)return only chosen fields / columns
renameyesyes (once, last)rename fields / columns
drop—yes (once)remove columns
iplocationyesyesadd country and city of an IP
lookupyesyesenrich from a CSV file
transactionyes (once)—group related events
fieldformat—yesdisplay format of columns

Ordering rules

  • agg can be used once per query. table cannot be combined with agg.
  • latest and earliest cannot be combined with each other, with size or with sort.
  • In the Search Context table and fields cannot be combined.
  • In the Aggregation Context fields must come before sort, and rename must be the last statement.
  • drop and fieldformat refer to column names as they are before rename.
  • Aggregation results are sorted in descending order when no sort is 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]
Keep events from two source addresses.
EventID=4625 | where TargetUserName startswith [adm, svc]
Accounts whose name starts with "adm" or "svc".
TargetUserName=*| where TargetUserName endswith ["$"]  | agg count by TargetUserName
Machine accounts only.
EventID=4625| where exists [IpAddress]  | agg count by IpAddress
Only events that have an IpAddress field.
UserName=* SourceIP=*  | agg count by SourceIP  | where count > 10
After AGG: keep source IPs with more than 10 events.
SourceIP=*  | agg count by SourceIP  | where SourceIP notin [10.0.0.1, 10.0.0.2]
After AGG: exclude two rows.

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 SourceIP
Very long DNS names per source (a DNS tunneling indicator).
UserName=* SourceIP=*| eval SourceCnt=concat(SourceIPCountryCode, "-", SourceIPCountryName)  | agg count by SourceCnt
Combine two fields into one and count.
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)  | sort - FailRate
After AGG: ratio of failed to successful logons per account.

AGG — 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 - count
Failed authentications per source and user.
NodeName=IIS* SourceIPCountryCode=* | agg count by SourceIPCountryCode
IIS requests per country.
Page=* SourceIP=*  | agg dc(Page) as DistinctCount, values(Page) by SourceIP  | where DistinctCount > 200
Sources that requested more than 200 distinct pages (crawling / scanning).
SentBytes=* SourceIP=*  | agg count as NumberofLogs, sum(SentBytes) as TotalSentBytes by SourceIP  | sort - TotalSentBytes
Several functions with names.
Url=* SourceIP=*  | agg count as TotalRequests, count where HttpStatus startswith [4, 5] as ErrorResponses by SourceIP  | sort - ErrorResponses
Conditional count: all requests and error responses side by side.
UserName=*  | agg dc(DestinationIP) where DestinationPort in [3389, 22] as RemoteAccessTargets by UserName  | where RemoteAccessTargets > 10
Users who reached more than 10 hosts over RDP or SSH.
NodeName=trappot*  | agg min(EventTime) where EventType="startup" as BootTime, min(EventTime) where EventType != "startup" as FirstAttackTime by NodeName
Two conditional minimums per honeypot.
Taxonomy=Authentication.Fail SourceIP=*  | agg count as FailedLogons, count where notexists [MFAToken, SmartcardId] as FailedLogonsWithoutMFA by SourceIP  | sort - FailedLogonsWithoutMFA
Conditional count with an existence test.

TIMESPAN — 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 1h
Distinct pages per source per hour.
EventID=4625  | agg count by TargetUserName  | timespan 15m
Failed logons per account in 15-minute buckets.

SORT — 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 100
Events sorted by two fields.
SourceIP=* SentByte=*  | agg sum(SentByte) by SourceIP  | sort - sum(SentByte)
Sort by an unnamed aggregation column.

SIZE — limit events or rows

size <n>
UserName=*admin* or User=*admin* | size 10
At most 10 events.
(UserName=*admin* or User=*admin*) Hostname=*  | agg count by Hostname  | size 5
Top 5 rows of the aggregation.

LATEST 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 20
The 20 most recent failed logons.
SourceIP=172.70.10.11 NodeName=IIS_Server | earliest 5
The first 5 requests of a source in the time range.
EventID=4625 | latest
Only the most recent event.

TABLE — 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, EventTime
Failed logons as a table with a renamed column.

FIELDS — 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, DestinationPort
Return three fields only.
DestinationPort=445 or SourcePort=445  | agg sum(sentbyte) as sumsentbyte, sum(rcvdbyte) as sumrcvdbyte by UserName  | eval TotalBytes=add(sumsentbyte, sumrcvdbyte)  | fields UserName, TotalBytes
Keep only the final columns after a calculation.

RENAME — 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 1000
Two renames in the Search Context.
TargetUserName=* EventID=4625  | agg count by TargetUserName  | rename TargetUserName as USER_NAME, count as FAILED_AUTH_COUNT
Rename result columns.

DROP — 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, sumrcvdbyte
Keep only the total.

IPLOCATION — country and city of an IP

iplocation <ipField> [, ...]

Adds <field>Country and <field>City. For example IpAddress gives IpAddressCountry and IpAddressCity.

srcIp=* | iplocation srcIp
Location of each event.
srcIp=*  | agg count by srcIp  | iplocation srcIp
Location of each aggregated source (faster: one lookup per row).

LOOKUP — 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 EventDesc
Add a description to each event ID.
EventID=[4728, 4732] MemberName!="-"| lookup EventIDs.csv EventID as EventID output Explanation as EventDesc| eval UserName=regex(MemberName, "(?i)cn=([^,]+)")| table EventTime, EventID, EventDesc
Enrich, extract the user name from a DN, show as a table.

TRANSACTION — 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, ObjectName
Object access and deletion events of the same handle.
EventID=[4663, 4660]| transaction SubjectLogonId, HandleId group_must_include=(EventID=[4663, 4660])| table EventTime, NodeName
Only handles that were both accessed and deleted.

FIELDFORMAT — 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)
1234567 is shown as 1.234.567.
SourceIP=* SentByte=*  | agg sum(SentByte) by SourceIP  | sort - sum(SentByte)  | fieldformat sum(SentByte)=bytes(2)
Byte counts as KB, MB, GB.
user=* SentByte=*  | agg sum(SentByte) as Traffic by user  | fieldformat Traffic=dots(2)  | fieldformat Traffic=suffix(" bytes")
Number format plus a unit.
NodeName=*  | agg min(EventTime) as FirstSeen by NodeName  | fieldformat FirstSeen=strftime("yyyy.MM.dd HH:mm:ss")
Epoch time as a readable date.
user=* SentByte=*  | agg sum(SentByte) as SentByte by user  | sort - SentByte  | fieldformat SentByte=dots(0)  | rename SentByte as SENT_BYTES
FIELDFORMAT uses the name before RENAME.

Aggregation functions

FunctionReturnsNotes
countnumber of events in the group
count(f)number of events where f has a value
dc(f)distinct count: number of different values of fscanning, spraying and fan-out detection
values(f)list of the distinct values of fcan time out on large data sets
sum(f)sumnumeric fields
avg(f)averagenumeric fields
min(f) max(f)smallest / largest valuenumeric and time fields
minstr(f) maxstr(f)alphabetically first / last valuetext fields
var(f)variancenumeric fields
stdev(f)standard deviationnumeric fields
NodeID="*_*" | agg dc(SourceIP), count where exists [Password] by ModuleName
Unnamed columns are named dc(SourceIP) and count.

EVAL functions

FunctionDescription
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 3
upper, len, substr and concat.
SourceIP=*| eval Net=regex(SourceIP, "^([0-9]+[.][0-9]+)[.]")  | agg count by Net
Extract the first two octets of an IP with a regular expression.
UserName=*| eval U=lower(UserName)| eval Domain=split(UserName, "\\", 0)| table EventTime, U, Domain
Lower case, and the domain part of DOMAIN\user.

Date functions

NodeName=*| eval Hour=strftime(EventTime, HH)  | agg count by Hour  | sort Hour
Event count per hour of the day.
NodeName=*| eval Day=strftime(EventTime, yyyy-MM-dd)  | agg count by Day
Event count per day.

Arithmetic

SourcePort=* DestinationPort=*| eval S=sub(SourcePort, DestinationPort)| eval M=mul(SourcePort, 2)| eval A=add(SourcePort, DestinationPort)| table S, M, A| size 3
sub, mul and add.
DestinationPort=445 or SourcePort=445  | agg sum(sentbyte) as sumsentbyte, sum(rcvdbyte) as sumrcvdbyte by SourceIP  | eval TotalKB=calc((sumsentbyte + sumrcvdbyte) / 1024, 0)  | sort - TotalKB
calc with parentheses and decimal places.

COALESCE

UserName=*| eval Actor=coalesce(UserName, SamAccountName, "UNKNOWN")  | agg count by Actor
The first non-empty value, with a constant as the last fallback.
NodeName=*  | agg min(EventTime) where EventType="startup" as BootTime, min(EventTime) where EventType != "startup" as FirstEventTime by NodeName  | eval RepresentativeTime=coalesce(BootTime, FirstEventTime)
COALESCE on aggregation columns.

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 Outcome
Label events, then count the labels.
HttpStatus=*| eval Bucket=case(HttpStatus startswith [4, 5] => error, Action in [deny, drop, reset] => blocked, default => ok)
List operators inside CASE.
SourcePort=*| eval Direction=case(SourcePort gt field(DestinationPort) => "src-higher", SourcePort eq field(DestinationPort) => equal, default => "dst-higher")
Compare two fields with field().
SourceIP=*  | agg count by SourceIP  | eval Level=case(count gt 1000 => high, count gt 100 => medium, default => low)
CASE on an aggregation column.

FIELDFORMAT functions

FunctionOutput exampleDescription
num([decimals][, "group"][, "decimal"])1,234,567.89general number format with custom separators
commas([decimals])1,234,567.89comma groups, dot decimals
dots([decimals])1.234.567,89dot groups, comma decimals
spaces([decimals])1 234 567,89space groups, comma decimals
percent([decimals])12%ratio × 100 with %; pairs with div()
bytes([decimals][, 1024|1000])1.18 MBscales to B, KB, MB, GB, TB, PB, EB
duration([s|ms])01:02:03seconds (or ms) as HH:MM:SS, D+HH:MM:SS beyond a day
strftime("format"[, s|ms])2026.01.31 14:05:00epoch value as a date
printf("format")1234.57 msC-style format with exactly one placeholder (%d %x %f %e %g %s)
prefix("text") suffix("text")$ 1.234 / 1.234 MBadd 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 - FailRate
A ratio shown as a percentage.
user=* SentByte=*  | agg sum(SentByte) as SentByte, count as Requests by user  | fieldformat SentByte=dots(2), Requests=dots(0)
Two columns in one FIELDFORMAT.
SourceIP=*  | agg avg(ResponseTime) as AvgResponse by SourceIP  | fieldformat AvgResponse=printf("%.2f ms")
printf with a unit.

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 - Failures

Ask 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 - DistinctUsers

Ask 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 > 0

Ask 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 - FailRate

Ask 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 15m

Ask 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 - count

Ask 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, IpAddressCity

Ask 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 - count

Ask 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, NodeName

Ask 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, TargetUserName

Ask 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, SubjectUserName

Ask 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, ObjectName

Ask 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 - Targets

Ask 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 20

Ask 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 - Ratio

Ask 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 10

Ask 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 - Names

Ask 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, HttpStatus

Ask 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 1h

Ask 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 1h

Ask 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 / ruleFix
Unknown statementCheck the spelling after |. See the statement list.
"x" cannot be used in the Aggregation ContextMove the statement before agg, or use the Aggregation Context alternative.
"table" and "agg" cannot be used togetherUse either TABLE (rows per event) or AGG (grouped rows).
"latest" and "size"/"sort" cannot be used togetherLATEST/EARLIEST already sort and limit.
"rename" must be the last statement in the Aggregation ContextMove RENAME to the end.
"fields" must be written before "sort"After AGG, put FIELDS before SORT.
More than one aggregation function uses WHEREName each conditional function with as.
The column name is produced more than onceGive each function a distinct name with as.
Column does not exist in the aggregation resultsDROP, FIELDS and FIELDFORMAT use the column names before RENAME.
At least one column must remain after DROPDrop fewer columns.
Invalid durationTIMESPAN takes <number><ms|s|m|h|d>, for example 1h or 15m.
The CSV file name must include its extensionWrite EventIDs.csv, not EventIDs.
Unterminated quote / Unclosed bracketClose 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 and contains.
  • values() and very short TIMESPAN intervals can produce very large results.
  • In the Search Context, fields reduces 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.