In our test export — two and a half days of SM20 from one SAP system — the file ran 36,362 records: roughly 1.8 million tokens at an approximate 50 tokens per row, more than most models will accept in one message. And it was almost all operational noise: a single background job re-authenticating accounted for 73% of the file on its own, 95% was noise of one kind or another, and only 0.57% of records — 209 rows — carried anything a security team would call a finding.
The fix isn't a bigger context window — it's reducing before you analyse: aggregate the noise into a volume picture, keep only the security-relevant event codes in full detail, then hand both to an LLM with a prompt that knows the difference between an absent record and a proven one. This prompt turns that reduced export into a ranked, sourced finding register: severity, confidence, and the exact SAP transaction that would confirm each one.
It's free. No form, no login. Reduce your export first (see below), then copy the prompt and run it against your own data.
How this works
- Export the audit log. Run SM20 (or RSAU_READ_LOG on S/4HANA) for your date range, all clients, all users, all audit classes, and save it as CSV or tab-delimited text.
- Reduce the export. Run the free sm20_reduce.py script (or the Excel filter) to split a raw export into a volume summary and a security-signal detail file before pasting anything into an LLM.
- Run the triage prompt. Copy the prompt below into your company-approved LLM — a commercial assistant or a model running on your own infrastructure — and attach the summary and signal files (or the raw export, if you have not reduced it yet).
- Read the finding register. Work top-down by severity: Critical and High findings first, each with its evidence count, confidence level, and the exact SAP transaction or table that would confirm it.
Reduce the export first
An SM20 export can run to tens of thousands of records fast. In our test export, two and a half days from a single system produced 36,362 records — roughly 1.8 million tokens at an approximate 50 tokens per row. Too large for most models, and unnecessary: 95% of that file was operational noise (a single background job re-authenticating was 73% on its own) and only 0.57% of records carried security signal. You would be paying to load 36,362 rows in order to read 209.
Option A — filter on the SM20 selection screen (no scripting)
| Setting | Value | Why |
|---|---|---|
| Audit classes | Untick RFC Function Call and Report Start | Highest-volume, lowest-value classes |
| Severity | Critical + Severe only | Drops the batch-logon flood |
| User | Exclude DDIC, SAPSYS, and your batch user | One batch user alone was 73% of our test file |
| Date range | 24–72 hours at a time | Not 30 days |
Dropping the machine-logon flood removes most of the file.
Filtering out the batch logons also removes the evidence for the single biggest finding in our test — "one job is consuming 73% of your audit log capacity." If you want that finding, use Option B.
Option B — two files (recommended)
Produce two files and attach both:
sm20_summary.csv— aggregate of the FULL export:Event | User | Client | Peer | ABAP Source | Count. One row per unique combination, so tens of thousands of records collapse to a few hundred rows. Preserves the volume picture. No timestamps.sm20_signal.csv— full-detail rows, filtered to security-relevant events only — typically a small fraction of the raw export.
AU2, AU4, AU5, AU6, AU7, AU8, AU9, AUA, AUB, AUD, AUE, AUF, AUI, AUJ,
AUL, AUM, AUN, AUY, BU1, BUJ, CUZ
+ AU3 rows where the transaction is on the privileged list
+ DU9 rows where the auth check FAILED
+ AU1 rows where logon type is A or H (interactive) — needed to spot dual-use accounts
Use the free reducer script below, or the Excel formula, to produce both files.
Excel filter for sm20_signal.csv
=OR(
ISNUMBER(MATCH([@Event],{"AU2","AU4","AU5","AU6","AU7","AU8","AU9","AUA","AUB","AUD","AUE","AUF","AUI","AUJ","AUL","AUM","AUN","AUY","BU1","BUJ","CUZ"},0)),
AND([@Event]="DU9", ISERROR(SEARCH("passed",[@[Audit Log Message]]))),
AND([@Event]="AU1", OR([@[Variable Data]]="A",[@[Variable Data]]="H")),
AND([@Event]="AU3", ISNUMBER(MATCH([@[Variable Data]],{"SU01","SU10","PFCG","SE38","SE37","SE80","SE16","SE16N","SM30","SM31","SM34","SM59","SM49","SM69","SCC4","SCC5","RZ10","RZ11","STMS","SM01","SM12","SM19","SM20","SA38","SE93","SE11","ST05"},0)))
)
Filter to TRUE, copy the visible rows out.
Free download — sm20_reduce.py
# sm20_reduce.py - collapse a raw SM20 export into two LLM-ready files.
# Usage: python sm20_reduce.py sm20.txt
# Produces: sm20_summary.csv (volume picture) + sm20_signal.csv (security detail)
# Requires: pandas
import sys, pandas as pd
SIGNAL = {"AU2","AU4","AU5","AU6","AU7","AU8","AU9","AUA","AUB","AUD","AUE",
"AUF","AUI","AUJ","AUL","AUM","AUN","AUY","BU1","BUJ","CUZ"}
PRIV = {"SU01","SU10","PFCG","SE38","SE37","SE80","SE16","SE16N","SM30",
"SM31","SM34","SM59","SM49","SM69","SCC4","SCC5","RZ10","RZ11",
"STMS","SM01","SM12","SM19","SM20","SA38","SE93","SE11","ST05"}
raw = open(sys.argv[1], encoding="utf-8", errors="replace").read().replace("\r\n", "\n").split("\n")
h = next(i for i, l in enumerate(raw) if l.lstrip().startswith("SAP System"))
cols = [c.strip() or f"c{i}" for i, c in enumerate(raw[h].split("\t"))]
rows = [(l.split("\t") + [""] * len(cols))[:len(cols)]
for l in raw[h+1:] if l.strip() and not l.lstrip().startswith("SAP System")]
df = pd.DataFrame(rows, columns=cols).apply(lambda s: s.str.strip())
# File 1 - volume summary. Preserves the noise picture in a few hundred rows.
(df.groupby(["Event", "User", "Cl.", "Peer", "ABAP Source"])
.size().reset_index(name="Count")
.sort_values("Count", ascending=False)
.to_csv("sm20_summary.csv", index=False))
# File 2 - signal only. The rows a human would actually read.
keep = (df["Event"].isin(SIGNAL)
| (df["Event"].eq("DU9") & ~df["Audit Log Message"].str.contains("passed", na=False))
| (df["Event"].eq("AU1") & df["Variable Data"].isin({"A", "H"}))
| (df["Event"].eq("AU3") & df["Variable Data"].isin(PRIV)))
df[keep].to_csv("sm20_signal.csv", index=False)
print(f"{len(df):,} records -> summary "
f"{df.groupby(['Event','User','Cl.','Peer','ABAP Source']).ngroups:,} rows | "
f"signal {keep.sum():,} rows ({keep.mean():.2%})")
What you'll need
| Column | Source |
|---|---|
SAP System | SID |
AS Instance | Application server instance |
Date / Time | Event timestamp |
Cl. | Client (MANDT) |
Event | Audit message ID (AU1, AU2, AU3, AUD, BU1, CUZ, DU9, BUJ…) |
User | SAP user ID |
Terminal | Workstation name |
Peer | Source IP address |
TCode | Transaction code |
ABAP Source | Calling program |
Audit Log Message | Resolved message text |
Variable Data / Variable 2 / Variable 3 | Message parameters (tcode, reason code, logon type) |
Export from SM20 (or RSAU_READ_LOG on S/4HANA) for your date range, all clients, all users, all audit classes. Save as a CSV with the columns above.
The prompt
Copy this verbatim into your LLM of choice, then attach the CSV described above.
You are an SAP security auditor. You are given a raw SM20 / RSAU Security Audit
Log export. Separate operational noise from security signal and produce a ranked
finding register that a human can act on in under ten minutes.
Source columns:
SAP System, AS Instance, Date, Time, Cl., Event, User, Terminal, Peer, TCode,
ABAP Source, Audit Log Message, Variable Data, Variable 2, Variable 3
===========================================================================
STEP 0 - CHECK WHAT YOU WERE GIVEN
===========================================================================
You may receive ONE or TWO files:
sm20_summary.csv : aggregated counts (Event, User, Client, Peer, ABAP Source,
Count). Use for VOLUME findings ONLY - noise ratio, dominant
job, retention projection, account inventory. It has NO
timestamps. Never draw a timing or sequence conclusion from it.
sm20_signal.csv : full-detail rows, filtered to security-relevant events.
Use for every finding needing a timestamp, IP, sequence, or
attribution.
If you receive ONE file, identify it by whether it has a Count column (summary)
or a Time column (detail), and state which you are working with.
If you receive a RAW, UNFILTERED export (tens of thousands of rows, mostly AU1):
Do NOT read it row by row.
1. Aggregate first: count by Event; by User; by Event + User + Peer + ABAP Source.
2. Derive volume findings from the aggregate.
3. Then extract ONLY security-signal events into detail and analyse those.
4. State at the top how many records you AGGREGATED vs how many you INSPECTED
INDIVIDUALLY.
If the signal set is EMPTY, do not manufacture findings from the noise. Report
that no security-relevant events were recorded, then note that this can mean
either a clean system OR that the relevant audit classes were never switched on -
and that you cannot distinguish the two. Name SM19 / RSAU_CONFIG as the check.
===========================================================================
THE NULL-EVIDENCE RULE - READ THIS BEFORE YOU WRITE ANY FINDING
===========================================================================
The absence of a record is NOT evidence that something was bypassed, hidden, or
suppressed. SM20 only records what the audit configuration was told to record.
Specifically, you must NEVER write any of the following:
- "no logon event exists for this user, therefore the connection bypassed the
audit trail"
- "this action was not logged, therefore it was concealed"
- "the attacker evaded auditing"
...unless an AUE / AUF / AUI / AUJ event proves the audit configuration was
actually changed during the window.
Successful RFC logons, reused sessions, trusted-RFC calls, and internal system
logons may legitimately produce no AU1/AU6 record depending on which audit
classes are active. An absence means ONE of: (a) the event class is not enabled,
(b) the event type does not generate a record, or (c) it did not happen. You
cannot tell which from SM20 alone. If an absence is material, report it as an
OPEN QUESTION with confidence "Requires Correlation" and name SM19 / RSAU_CONFIG
as the check. Do not escalate severity on the strength of a null.
===========================================================================
STEP 1 - RECONCILE THE EXPORT
===========================================================================
Report: total records, distinct event IDs, distinct users, distinct clients,
distinct source IPs (Peer), first timestamp, last timestamp, elapsed hours.
If the export header declares Critical / Severe / Other counts, confirm they sum
to the parsed record count. Flag any mismatch - a short export is an incomplete
audit and every downstream percentage is wrong.
===========================================================================
STEP 2 - CLASSIFY EVERY RECORD. NOTHING MAY BE LEFT UNCLASSIFIED.
===========================================================================
First decode the LOGON TYPE on every AU1 and AU2 record (it is in Variable Data):
A = Dialog (SAPGUI, interactive) -> HUMAN
H = HTTP / Fiori / OData -> HUMAN
B = Batch / background job step -> MACHINE
F = Background processing -> MACHINE
E = Internal / system (e.g. SAPMSSYC) -> MACHINE
G = RFC -> MACHINE
S = SAP internal service -> MACHINE
V = Web service / external call -> MACHINE
Any other value: report the raw code, label it UNMAPPED, count it separately,
and treat it as MACHINE for bucketing. Do not silently discard it.
Now bucket:
NOISE (high volume, low security value - count and set aside):
AU1 Logon successful, any MACHINE logon type (B, F, E, G, S, V)
CUI OData / Gateway application started
BU4 Dynamic ABAP code generated
DU9 Generic table access WHERE the message says "auth. check: passed"
AUC User logoff
CONTEXT (matters in aggregate):
AU3 Transaction started
AU1 Logon successful, HUMAN logon type (A, H)
FU9 Virus scan profile not active
EU4 Logical file name validation
SIGNAL (low volume, inspect every record individually):
AU2, AU4, AU5, AU6, AU7, AU8, AU9, AUA, AUB, AUD, AUE, AUF, AUI, AUJ,
AUL, AUM, AUN, AUY, BU1, BUJ, CUZ
DU9 where the auth check FAILED
Any event ID not named anywhere above - report it raw, never drop it.
Output a table: bucket, event ID, count, % of total. Print the SIGNAL subtotal as
a percentage - this is the signal-to-noise ratio.
MANDATORY: the three bucket subtotals plus any UNMAPPED count MUST sum to exactly
the STEP 1 record count. Print that arithmetic check. If anything is left over,
you have mis-bucketed - go back and fix it. Do not publish an "unclassified"
residual.
===========================================================================
STEP 3 - FIND THE VOLUME DRIVER
===========================================================================
Identify the single User + Event + ABAP Source combination producing the most
records. Report count, % of log, and rate per hour. State whether the rate is
CONSTANT across the window (a scheduled job) or BURSTY (human or adversary).
If one combination exceeds 40% of the log, raise it as a finding: audit log
capacity is being consumed by an operational defect, not by security events, so
the real retention window is far shorter than the audit policy assumes. Project
the annual record volume from the observed rate.
===========================================================================
STEP 4 - THE ABSENCE-OF-DENIAL TEST. THIS IS THE MOST IMPORTANT CHECK.
===========================================================================
Count:
- AU4 with reason code 1 ("no authorization")
- AU5 authorization check failures
- DU9 records where the auth check FAILED
Compare against total AU3 transaction starts + total DU9 table reads.
If denials are ZERO or near-zero across meaningful volume, report as HIGH with
this reasoning: in a least-privilege landscape you expect a steady background
rate of denials, because users routinely attempt actions outside their roles.
Zero denials over significant volume is the audit-log signature of over-privileged
users - SAP_ALL, SAP_NEW, or equivalent wide profiles.
State it as an INFERENCE, not proof. Name PFCG role review and an SAP_ALL
assignment check (AGR_USERS / UST04) as the confirming step.
Note: this is the one place where an absence IS reportable - because you are
comparing it against a large population of events that DID fire from the same
audit classes. That is a rate finding, not a null finding. Say so.
===========================================================================
STEP 5 - REASON CODES
===========================================================================
AU2 Logon failed:
reason 1 = invalid credentials (unknown user or wrong password)
reason 8 = logon attempted against an account that is ALREADY LOCKED
AU4 Transaction start failed:
reason 1 = no authorization -> a SECURITY denial
reason 2 = transaction does not exist, or is locked in SM01
-> usually a TYPO, not a security event
Any other code: print the raw value and state that it is unmapped. Do not guess.
For every AU4 reason 2, print the attempted transaction code. If the codes are
near-misses of real SAP transactions (NIGO/MIGO, FNL3N/FBL3N, MRL/MRRL), or are
scattered across unrelated functional modules, classify them as TYPING ERRORS and
rate LOW. Only if the attempted codes are systematic, sequential, or alphabetical
should you classify them as ENUMERATION and raise severity.
===========================================================================
STEP 6 - STANDING CHECKS. Report each as PRESENT or ABSENT with evidence.
===========================================================================
a) BUJ non-encrypted communication -> SNC is not enforced. List the distinct
TERMINALS. Note: BUJ records often carry a BLANK Peer field. Do NOT attribute
an IP to a BUJ record unless the Peer field on that record is populated, or
the same Terminal appears on an AU1 record that does carry a Peer. Otherwise
report the IP as unknown. Do not borrow IP counts from elsewhere in the log.
b) Any Peer IP outside RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
-> users are reaching SAP from public networks. Combined with (a), credentials
cross the public internet in cleartext. Rate HIGH.
c) FU9 virus scan profile not active -> list each inactive profile with counts.
d) AUE / AUF / AUI / AUJ audit configuration changes -> if PRESENT, the audit
trail's own integrity is in question for this window. Rate CRITICAL. If ABSENT,
state that audit configuration was stable and the log is defensible for the
period. (This is the ONLY absence you may report as reassuring.)
e) STANDARD ACCOUNT MISUSE. Check whether DDIC, SAP*, SAPCPIC, EARLYWATCH, or
TMSADM appear with ANY logon type.
- DDIC or SAP* as a BATCH JOB STEP USER (AU1 type B, ABAP source RSBTCRTE) is
a FINDING, rate HIGH. It is NOT "expected" and NOT "normal" - jobs must run
under a dedicated technical user. DDIC as a job step user breaks attribution
(every job action is logged as DDIC) and blocks password rotation (rotating
DDIC breaks every job). Do not excuse it because the logon type is not dialog.
- DDIC or SAP* with a DIALOG logon (type A) is rate CRITICAL.
f) DUAL-USE ACCOUNTS. For every user, list the set of logon types observed.
Any user showing BOTH a MACHINE type (B/E/F/G/S/V) or RFC activity (CUZ) AND a
HUMAN type (A/H) is DUAL-USE. Rate HIGH. Reasoning: a service or RFC identity
that is also used interactively destroys attribution - you can no longer tell
whether an action came from the interface or from a person using its
credentials. Recommend setting the account's user type to SYSTEM or SERVICE
(USR02-USTYP = B or S) so dialog logon becomes impossible, and rotating the
password. THIS CHECK IS MANDATORY - run it even if the account looks benign.
g) AUD "user master record changed" with NO corresponding AU9/AUA lock or unlock
event -> SM20 proves a change occurred but not WHAT changed. A password reset,
a lock removal, and a role grant are indistinguishable. Name CDHDR / CDPOS
change documents as the required correlation source. Confidence: Requires
Correlation. If a user was changed and then STILL failed to log on afterwards,
say so explicitly - the change may not have been the unlock it appears to be.
h) RECONNAISSANCE PATTERN. Check whether any single user + single IP read
IDENTITY tables (USR02, USR21, USER_ADDR, ADRP, ADR6) AND AUTHORIZATION tables
(AGR_1251, AGR_USERS, AGR_AGRS, AGR_DEFINE, UST04) AND SYSTEM tables (RFCDES,
PAHI, E070, SNAP, DD03L, RSAU_BUF_DATA) within a short window - via CUZ (RFC)
or DU9 (internal).
If yes, report it - AND state plainly that SM20 ALONE CANNOT DISTINGUISH AN
AUTHORISED SECURITY SCANNER FROM AN ADVERSARY. The records produced by a
sanctioned vulnerability scanner, a GRC tool, an SAP license audit, and a
post-exploitation enumeration are identical. Give the exact user, IP, table
list, timestamp window and read rate so a human can resolve it in one minute
by confirming whether that account and IP belong to a sanctioned tool.
Do NOT assume benign. Do NOT assume malicious. Confidence: Requires
Correlation. Severity: High (because it is unresolved, not because it is
proven hostile).
===========================================================================
STEP 7 - OUTPUT THE FINDING REGISTER
===========================================================================
One row per finding. Columns in this exact order:
- Finding ID
- Severity (Critical, High, Medium, Low)
- Confidence (Confirmed, Inferred, Requires Correlation)
- Title
- Evidence Count (number of underlying SM20 records)
- Event IDs
- Users Affected
- Source IPs (write "not recorded" if the Peer field is blank - never guess)
- First Seen
- Last Seen
- Reasoning (2-3 plain sentences: why this is a finding)
- Confirming Check (the exact SAP transaction or table that would prove it)
Confidence definitions:
- Confirmed : the SM20 records alone prove the finding.
- Inferred : it follows logically from the pattern or from a RATE of
expected events being absent, but needs a second source.
- Requires Correlation : SM20 shows something happened but not enough to attribute
or characterise it.
Sort by Severity (Critical first), then Evidence Count descending.
Then print a plain-language executive summary of no more than eight sentences.
===========================================================================
RULES
===========================================================================
- Never invent an event ID, reason code, table name, or logon type not in the data.
- If a count is zero, say zero.
- Never escalate severity on the strength of an absent record (see NULL-EVIDENCE RULE).
- Never call something "expected" or "normal" if it appears in the STEP 6 checks.
- Do not pad the register with noise-bucket events to look thorough.
- Every finding must cite a record count and a timestamp range.
- If a finding could have an innocent explanation, state the innocent explanation
and say what fact would rule it out.
Why the null-evidence rule matters
This rule exists because a missing record is the single easiest way for an LLM to go wrong with an audit log. Given a raw export, a prompt without this rule can look at a missing logon record and conclude outright that "the RFC connection bypassed the audit trail" — when the record is absent simply because that audit class was never switched on, not because anything was bypassed. This prompt only lets an absence raise severity in the one place it's actually earned — a near-zero denial rate against significant transaction volume — and everywhere else treats a null as an open question for SM19 / RSAU_CONFIG to resolve, not a verdict.
Beyond a one-time snapshot
These prompts are a one-time read of an exported log. Syntasec does it live and continuously across your entire SAP landscape — correlating SM20 with change documents, role assignments, and system parameters, so the questions these prompts have to leave open ("what actually changed?", "is this scanner sanctioned?") are already answered.
Want the SM20 reducer script and the privileged-transaction watchlist as a ready-to-run file?
Optional. The prompt above is fully readable and copyable whether or not you use this.
Frequently asked questions
How do I analyse SM20 logs with AI without hitting the context limit?
Reduce before you paste. In our test export, two and a half days of SM20 from one SAP system produced 36,362 records — roughly 1.8 million tokens at an approximate 50 tokens per row, more than most models will accept in one message and far more than you need to read: only 0.57% of those records carried security signal. Aggregate the volume noise into a summary file and keep only the security-relevant event codes in full detail (see "Reduce the export first" below), or run the free sm20_reduce.py script to do both steps automatically.
Why is most of my SM20 audit log just logon events?
Because SM20 logs every successful logon by every account — including every batch job step, every RFC call, and every background service tick — not just the events a security team cares about. In our test export, a single background job re-authenticating accounted for 73% of the file on its own. That volume is a capacity and retention problem as much as it's noise: it's worth flagging as a finding in its own right, since it eats into how far back your audit log actually reaches.
What does it mean if SM20 shows zero authorization failures?
It usually means the opposite of what it sounds like. In a genuinely least-privilege landscape, some background rate of denied actions is normal — users routinely attempt things outside their roles and get blocked. Zero denials over meaningful volume is the audit-log signature of over-permissioned users, most often a wide profile like SAP_ALL or SAP_NEW sitting somewhere in the role assignments. It's an inference, not proof — confirm it with a PFCG role review or an SAP_ALL assignment check.
Can SM20 tell the difference between a security scanner and an attacker?
No — and any analysis that claims otherwise is guessing. A sanctioned vulnerability scanner, a GRC tool, a license measurement run, and a post-exploitation enumeration script can all produce the exact same read sequence across identity, authorization, and system tables. SM20 has no field that separates them. The honest output is to flag the pattern, name the exact user, IP, and table list involved, and let a human confirm in about a minute whether that account and IP belong to an approved tool.
What is the difference between AU2 reason code 1 and reason code 8?
In practice, reason 1 indicates a failed logon attempt where the credentials were simply wrong or the user doesn’t exist — the closest thing SM20 has to a "someone is guessing passwords" signal. Reason 8 indicates the account was already locked before the attempt — usually a person retrying an account they already know is locked, which is a support ticket, not an attack. Mixing the two together is how a locked-out employee ends up misread as a brute-force attempt.
Is it safe to paste SAP audit logs into an AI model?
Not into a public model, no. SM20 records contain user IDs, source IP addresses, workstation/terminal names, and a transaction-by-transaction history of what each person did — that's personal data under GDPR and equivalent regimes, not just technical log data. Anonymise the user and IP fields before pasting an export into a public AI tool, or run the analysis against a model that stays inside your own infrastructure. Syntasec runs entirely on the customer's own infrastructure and is LLM-agnostic, so the data behind this kind of analysis never has to leave your network in the first place.