Repository navigation
Releases: Nobrac/NTLM-Analyzer
Release list
v2.4.0
v2.4.0
The SPN check
Most "Kerberos failed, NTLM took over" cases have a boring cause: the service
name (SPN) the client asked for is not registered in Active Directory, is
registered on two accounts, or is registered for the real server name while
clients use an alias. The events already name the SPN; now the tool also asks
AD about it.
- New panel "Kerberos configuration (SPN)" in the Act block. Every SPN
clients fell back to NTLM for, or that failed Kerberos with 0x7 ("server not
found in Kerberos database"), is looked up in AD. The panel lists what is
wrong, how much NTLM it causes, and thesetspncommand that fixes it, with
a copy button and "Check again" for after the fix:- missing — no account holds the name (for instance a NAS that never
joined the domain, or a stale name in a script) - alias — the name is a DNS alias (CNAME) or a short name; the SPN is
registered for the real server, and the command registers it for the alias - duplicate — several accounts hold it, so the KDC refuses the ticket
HOST/registrations are taken into account through the forest's
sPNMappings, socifs/orhttp/on a computer account is not reported.
- missing — no account holds the name (for instance a NAS that never
- Who looks it up: a domain controller running agent 2.4.0. The
collector hands it up to 50 names in the answer to its status report; the
agent queries the global catalog through ADSI and reports what it found.
Read-only - any domain account may readservicePrincipalName, so no extra
rights are needed. The tool never changes anything in AD; the command is
for an admin to run. Each name is checked again after a day. ntlm-agent.exe spn-check <service/host>...runs the same lookup by
hand and prints the result, on any domain-joined machine.- Status report: SPN problems become a next step.
Security notes
The lookup runs a fixed PowerShell script (-EncodedCommand); the names reach
it through an environment variable, never the command line, and each is
checked against a strict service/host[:port] pattern first - no character
with a meaning in LDAP filters or PowerShell gets through. Results are only
accepted for names the collector actually handed out, and /spn needs the
API key like /ingest. "Check again" is a dashboard action with the same
cross-site protection as the work status.
Tests
- Collector: 14 more tests (67 in all) - SPN normalisation, every verdict,
handing out and accepting only asked names, re-check, the report step. - Agent: 6 more tests (34 in all) - input validation, output parsing, the
encoding of the script.
Upgrading
- Collector: replace
ntlm-collector.py, restart, hard-refresh. - Agent 2.4.0 on at least one domain controller - that is where the
lookup runs. Other machines gain nothing new; updating them is optional.
The AD lookup could only be tested against a simulated directory before
release. If a name shows up wrongly, ntlm-agent.exe spn-check <spn> on a DC
shows exactly what AD answered - please open an issue with its output.
v2.3.2
v2.3.2
Security fixes for the collector and dashboard
After the agent in 2.3.1, the collector and its dashboard went through the
same review. Update the collector; the agent is unchanged apart from its
version number, so agents on 2.3.1 need nothing.
The review included an attack test: every field of every event,
machine and account filled with script code, then every panel, detail view,
hover card, search, report and link state opened in a browser. Nothing ran -
the escaping held everywhere. Found and fixed:
- Other web pages could use an admin's browser against the collector
(medium). Without a dashboard password and API key - the defaults - a page
the admin merely visited could mark work items as done, inject fake events
and invent machines, all through the admin's own browser. Browser requests
from other sites are now refused, and the agent endpoints only accept real
JSON, which a foreign page cannot send without the browser asking first.
With a password and an API key this was already blocked. - "limit=-1" meant no limit (low). A negative or malformed number in the
query returned the whole database or dropped the connection. Numbers are
now bounded. - Scripts need a per-page key (hardening). Each dashboard, login and report
page gives its own scripts a random nonce, and the browser runs no other
script - so even a value that slipped past the escaping could not execute.
HTTPS responses also send HSTS.
Still recommended, as before: run the collector with --password and
--key (the Linux installer sets both). Without a password, anyone who can
reach the collector can read the dashboard.
Tests
- Four more collector tests, 53 in all: foreign pages cannot post, the
dashboard itself still can, query numbers stay bounded, and every page runs
only its own scripts.
Upgrading
- Collector: replace
ntlm-collector.py, restart, hard-refresh.
Own scripts that post to/ingestor/statusmust send
Content-Type: application/json- the agent always does. - Agents: nothing to do if they run 2.3.1. Older ones: see 2.3.1.
v2.3.1
v2.3.1
Security fixes for the Rust agent
Security fixes for the agent, from a review of its code, installer and build.
Update every agent - the dashboard now marks machines that still run an
older one ("security update"). Update the collector first, as always.
Security fixes
- Data folder taken over before installation (high). Any user may create
folders underC:\ProgramData. Whoever createdNtlmAgentbefore the agent
was installed stayed its owner and could give himself access back at any
time: read the API key, redirect the collector, silence the agent, or plant
links that turned the service's own file writes into writes anywhere on the
system - as SYSTEM. Now the folder is made safe before anything is written:
a folder that is not ours (someone else's, a link, a file) is moved aside, a
fresh one is created with a protected ACL from its first moment and owned by
Administrators, and the service refuses to start from a folder that is not
safe. Most relevant on terminal servers, where many users log on. - API key readable after a failed lockdown (medium). The configuration
with the key was written before the folder's ACL was restricted, and a
failingicaclswas only logged. Now nothing is written unless the lockdown
worked. - NTLMv1 could be disguised as NTLMv2 (medium). On Server 2025 and
Windows 11 24H2 the agent reads the NTLM version from the event's message
text, which also contains names chosen by the remote client. A name with a
line break and a fake "NTLM Version: NTLMv2" line was read before the real
one. Values with line breaks are now flattened before the text is read. - An attacker could make an agent go silent (medium). Values from the
network were not all capped, and batches were limited by count, not size. A
batch over the collector's 10 MB limit was refused and retried every cycle -
nothing more arrived from that machine. Every field is capped now, batches
stay under 2 MB, and a refused batch is split so only an oversized event is
skipped. - API key in the install log (medium). The installer handed the key to a
helper that logged the whole command line. The configuration is now written
by the agent itself with its command line hidden from the log, which the
build checks on every run. New--api-key *(asks without echo) and
--api-key-env NAMEkeep the key out of shell history and event 4688. - HTTPS required (low).
install/configurerefuse anhttp://collector
unless--allow-http(MSI:ALLOWHTTP=1) is given. Existing configurations
keep working and warn at every service start. - Build supply chain (low).
Cargo.lockis committed and every build uses
exactly those versions (--locked); GitHub Actions are pinned to commits;
known vulnerabilities in dependencies are checked on every push and weekly
(cargo audit); Dependabot proposes updates. No dependency had a known
vulnerability.
Also fixed
- The MSI did not start the service after installing it - it only ran after
the next reboot. It starts right away now. configurewithout a key no longer wipes the stored one, so changing the URL
or upgrading silently keeps it (--clear-api-keyremoves it on purpose).- New SECURITY.md: how to report a vulnerability privately.
Tests
- Collector: every counting rule has a test that runs against a real
collector on a throwaway database - 8001/4020 duplicates, unconfirmed and
phantom 8001s, 4624 twins and the NTLM version travelling across one logon,
failed logons counted once, spraying, anonymous logons, one logon seen three
times per account, readiness, machines without an agent, the report, login
and API key. They run on every push on Python 3.7 and 3.13, with a syntax
check of the dashboard's JavaScript. - Agent: 28 unit tests, now part of every build, including the fixes
above. The MSI build installs the package with a key and checks that the key
is not in the log, that the data folder belongs to Administrators with
access for SYSTEM and Administrators only, that the service is running, and
thathttp://is refused.
Smaller changes
- The status report follows a dark system theme on screen; printed or saved as
PDF it stays white. - Remaining German text in the collector's description and one agent message
is English now; a test keeps the collector's comments that way.
Upgrading
- Collector: replace
ntlm-collector.py, restart, hard-refresh. - Agents: install the new MSI over the old one with the collector URL
(COLLECTORURL=https://...) - the stored API key is kept. Installed with
the bare EXE?ntlm-agent.exe uninstall, thenntlm-agent.exe install --collector-url https://...with the new EXE - the key is kept there too. The dashboard shows which machines are
still on an older version. - Collector on plain HTTP? Use
https://- or addALLOWHTTP=1(MSI) /
--allow-http(EXE) to keep HTTP knowingly.
v2.3.0
v2.3.0
Huge agent and dashboard update
The biggest release so far. 2.2 made the numbers right; 2.3 makes them
complete, and turns them into things to do. NTLMv1 is now visible where it
used to be hidden, failed and anonymous logons are no longer dropped, every
account has its own view, the dashboard names the machines it cannot see into,
and a printable status report carries all of it to people who never open the
dashboard.
Highlights
- NTLMv1 on member servers. The agent collects 4624 on every machine, so a
Server 2016–2022 file server finally says which NTLM version each logon used. - Accounts using NTLM, with an account detail view: from which
machines, to which servers, with which programs. - Failed NTLM attempts (4625 + failed 4776) with the reason in plain words,
locked-out accounts and password-spraying detection. - Anonymous logons (null sessions) are kept and labelled, instead of being
dropped or miscounted as NTLMv1. - Auditing gaps are named per machine instead of showing up as silence.
- Ready to switch off and machines without an agent — the two lists
that turn findings into a plan. - Machine detail, Ctrl+K search over everything, and a reworked,
faster-reading dashboard with a light theme. - Status report to print or save as PDF, with risks and next steps.
NTLMv1 where it was invisible
The classic NTLM events (8001, 8003, 8004) never say which NTLM version a
logon used. Before Server 2025 the only place a member server records it is
its 4624, and the agent collected that on domain controllers only. An
NTLMv1 logon to a Server 2022 file server showed up as "NTLM, version
unknown".
- The agent now collects 4624 on every machine. The filter keeps it to
NTLM logons, so the volume stays small. - One logon, one count. A member server usually writes both an 8003 and a
4624 for the same logon. They are matched per logon — same machine, same
account, same client, within ten seconds — and only the 8003 counts, since
it names the service that accepted the logon. Without an 8003 (incoming
audit off) the 4624 itself is the incoming event. - The version travels. The 4624's version is handed to every trace of the
same logon that lacks one: the 8003 on the server, the 8004 on the DC and
the 8001 on the client. So NTLMv1 now shows up in the domain view and in the
program list — which program on which client still speaks NTLMv1 —
whichever of the events reaches the collector first. - Logon auditing is checked. The agent reads "Audit Logon" (via
auditpol /backup, whose numeric column is the same in every Windows
language) and the machines panel shows per machine: logons (NTLMv1 is
recognised here), no 4624 (auditing off — NTLMv1 stays invisible, GPO
path in the tooltip) or agent before 2.3. The NTLMv1 panel says how many
machines are blind that way, so an empty list is never read as "all clear".
Every account, every attempt
- Accounts using NTLM. One row per account: NTLM logons, v1/v2, from how
many machines to how many servers, failed attempts, and whether it already
uses Kerberos elsewhere. The same logon is often seen three times — by the
client (8001), the server (8003) and the DC (8004); per account and server
the side that saw the most counts, never the sum. - Account detail. A click on an account (or Ctrl+K) opens it in the side
drawer: trend, from which machines, to which servers, with which programs,
and its failed attempts with the reason in plain words. - Failed NTLM attempts. The agent now also collects 4625 (failed NTLM
logons) on every machine; the DCs' failed 4776 were already there. One
failure seen by both the server and the DC counts once.- The reason is spelled out: wrong password, no such account, locked out,
expired, disabled, outside logon hours, … - A stale password in a service or scheduled task is named as the likely
cause, where it is. - Password spraying is flagged when one machine fails with five or more
accounts. - Failures live in their own table and never count towards the NTLM share.
- The reason is spelled out: wrong password, no such account, locked out,
- Anonymous logons (null sessions) are no longer dropped. They carry no
credential, so Windows' "NTLM V1" label on them is ignored — they show up as
anonymous instead of inflating the NTLMv1 figures. - Auditing gaps are named. A machine with outgoing, incoming or (on a DC)
domain NTLM auditing off gets a red or amber badge, and the machines panel
says how many are affected. - No more cut-off lists. Panels ended at 50 rows (NTLMv1 accounts at 15).
They now receive up to 500 and fold to ten with show all; the jump bar says
"500+" when the cap is reached.
Act on it
- Ready to switch off. Per machine and direction: auditing on, watched for
30 days, no NTLM in those 30 days — then "Restrict NTLM: Deny" can be set.
Otherwise it names what would break. Incoming counts what other machines and
the DCs saw going to the machine too, so a server with its own auditing off
is still caught. - Machines without an agent. Every NTLM logon of a domain account is
validated by a DC (4776, collected since 2.2.0). Any client in there without
an agent is listed — often the forgotten server or device. - Machine detail. A click on a machine opens everything about it in one
place: outgoing and incoming NTLM with its trend, readiness, programs and
targets, who reaches it and from where, its accounts and its auditing.
Status report
A Report button in the header opens a status report for everyone who does
not open the dashboard: a page to print or save as PDF, in German or English,
over 7, 30 or 90 days.
- Page one, the overview: the NTLM share and its change against the period
before, key figures, the weekly trend towards zero with NTLMv1 per week, the
work list (done, in progress, open) and the machines ready to switch off. - Page two, what to do: risks rated act / watch / fine — NTLMv1, the
October 2026 change, failed logons and spraying, gaps in visibility, relay
exposure — and up to six next steps derived from the data, with names. - Page three, the detail: the largest programs and accounts still using
NTLM, and how everything is counted.
Rendered on the server as plain HTML; no script needed to read it.
Quick search
Ctrl+K (⌘K on a Mac), / or the search button in the header opens one
box over everything the dashboard shows: machines and accounts open their
detail; programs and targets filter the event list; panels are jumped to. Full
keyboard use, screen-reader labels, and a single button on phones.
Text search in the event list now runs in the database. Before, it filtered
only the newest few hundred loaded rows — a program with 171 logons could show
11. Typing waits for a short pause and keeps the cursor where it was, and a
late answer can no longer overwrite a newer one.
A dashboard that reads faster
- Three blocks in the order the work goes: Situation, Act, Details.
Panels fold, long tables show ten rows with "show all", and the jump bar
shows real counts and marks where you are. - Key-figure tiles with the change against the week before; the trend as
an area with the goal line at zero; work status as coloured chips. - Light theme, following the system or picked in the header.
- A "What does this show?" answer on every panel, a compact phone header,
the brand mark in the header. - Fixed: a stray brace in the stylesheet had silently dropped two rules (the
live dot and the data-basis line); the 24-hour button was labelled "Time
range"; the jump bar showed fetch limits ("Events 300") instead of counts.
Live demo
The live demo has all of the
above: machine and account details, failed attempts including a spraying burst
and a locked-out account, an anonymous printer, a machine with auditing off,
a work list in progress, and the status report in both languages.
Under the hood
- Code, comments and identifiers are English throughout. Work status values
are nowopen,in_progressanddone; audit states reported by the agent
areon/offinstead ofan/aus. Existing databases are rewritten once
on start, and the old values are still accepted from older agents and pages.
The German UI translation and the German Windows labels the agent and
collector recognise stay German — they have to. - New table
ntlm_failures; 4776 status codes are stored in one spelling
(0xC000006A). Retention cleans both. - New endpoints
/api/machine,/api/accountand/report, all behind the
dashboard login. - Agent: 12 unit tests for the audit-policy parser and the new event mappings.
Upgrading
- Collector: replace
ntlm-collector.py, restart, hard-refresh
(Ctrl+F5). Database changes happen on start, nothing to do by hand. - Agent — member servers first. They are the ones that now send the
4624s that carry the version, and the 4625s. Then everything else. The MSI
upgrades in place and keeps the configuration. - GPO: "Audit Logon" to Success and Failure on DCs and member servers
(Advanced Audit Policy → Logon/Logoff). Success gives the NTLM version,
Failure the failed attempts; the dashboard shows where either is missing.
Compatibility
- Agent 2.2.x with collector 2.3: works; those machines just send no
member-server 4624 (so no NTLMv1 there) and no 4625. - Agent 2.3 with collector 2.2.x: don't. The older collector would count
the failed logons as NTLM in use. Update the collector first.
v2.2.0
v2.2.0
Numbers you can trust
This release is about accuracy. Checked against real Windows 11 24H2 and Server
2025 machines, three domain controllers and Microsoft's own documentation, the
dashboard turned out to count some NTLM logons twice, to file some under the
wrong panel and the wrong cause, and to count some that never happened. All of
that is fixed, and the collector can now settle doubtful cases against the
domain controllers' own record.
Expect your NTLM figures to go down after upgrading. That is not NTLM
disappearing; it is double counting and phantom entries leaving the totals.
Checked against the domain controllers
Windows 11 24H2 and Server 2025 write an 8001 as soon as NTLM is considered
during negotiation — even when Kerberos does the logon in the end. On a machine
that also writes the enhanced event 4020, an 8001 without a 4020 partner is
therefore doubtful.
The agent now collects 4776 on domain controllers: the DC's record of every
NTLM validation of a domain account. The collector checks each doubtful 8001
against it:
- A DC validated that account from that machine within two minutes → it was
real NTLM after all and is counted. - No DC validated that account from any machine, and every DC demonstrably
covered that moment → shown as Phantom (checked by DC) and not counted. - Anything in between — same account from another machine, a DC that has
not delivered for that time yet, a local account no DC ever sees → stays
unconfirmed: visible, not counted.
Nothing is hidden. The event list offers these entries behind a labelled filter
("n unconfirmed entries hidden, p of them shown to be phantoms by the DCs"), and
each one explains which DCs it was checked against.
The phantom verdict needs every DC. The machines panel now shows per DC
whether 4776 is arriving; one DC without them and no entry is called a phantom.
Counting each logon once
- 8001 and 4020 for the same logon are merged. Microsoft documents 4020 as a
superset of 8001 and keeps writing both while the classic audit is enabled.
They are paired per event — same machine, same target, within ten seconds —
not per machine: an 8001 without a partner stays visible. - 4022/4023 are incoming NTLM, logged on the server being accessed. They
were filed as domain-controller events, which put member-server logons into
the DC panel and counted a domain logon twice there.
The right cause
Windows Server 2025 writes Reason ID 10 (loopback) for several different causes,
with the correct text alongside. Windows 11 writes the same IP-address case as
ID 7. The dashboard now takes the cause from the text when it recognises it —
English and German — and falls back to the number otherwise. An IP address and
a DNS alias without an SPN no longer both show up as "loopback" with the wrong
remedy.
Agent
- 4776 on domain controllers (see above). Needs "Audit Credential
Validation", which the Default Domain Controllers Policy already enables. - Authentication-policy refusals were never collected. The agent asked the
Protected Users channel for 100/101 and the policy channel for 301. Per
Microsoft's event table 101 belongs to the policy channel and 301 does not
exist. Now 100 (Protected Users) and 101 (authentication policy) come from
the right channels. Both channels are disabled by default on DCs. - 8001 records the account that actually authenticated. With explicit
credentials (net use /user:…) that is the supplied account, not whoever ran
the process. - New application icon.
Dashboard
- Gold as the single accent colour for controls, matching the new brand mark.
Red, amber and green keep their meaning and are used for data only. - Crisper panel edges.
- New favicon.
- Faster: the classification above runs once per request instead of in
every one of ~30 queries. A month of a busy domain went from 6.4 s to 1.7 s.
Project page and demo
- New project page at nobrac.github.io/NTLM-Analyzer,
the live dashboard moved to /demo/. - New brand mark across the icon, favicons, installer artwork, README and site.
- The demo shows the DC check with synthetic data.
Privacy
The screenshots carried the author's name in their image metadata. It is gone
from all images; the pixels are unchanged.
Known limits
- The phantom rule rests on field observations, not on Microsoft
documentation: Microsoft does not document when exactly 8001 and 4020 are
written. The DC check turns it into evidence per entry, which is why doubtful
entries are settled rather than assumed. - On machines without 4020 (before Windows 11 24H2 / Server 2025) phantom 8001s
cannot be told apart and are counted. - NTLMv1 against member servers older than Server 2025 is only visible where
4624 is collected, which the agent currently does on DCs only.
Upgrading
- Collector: replace
ntlm-collector.py, restart, hard-refresh (Ctrl+F5).
Existing data is corrected on first start — 4022/4023, reason IDs, pairing.
No manual migration. - Agent — domain controllers first. Until every DC runs 2.2.0 nothing is
called a phantom; everything doubtful stays unconfirmed, nothing breaks.
The machines panel shows when all DCs deliver 4776. - Agent — everywhere else at your own pace. The MSI upgrades 2.1.x in
place and keeps the configuration. - Retention: 4776 adds a row per NTLM validation domain-wide. If you run
without--retention-days, consider setting it now.
Wire protocol compatible: 2.1.x agents keep working against the new collector,
and the collector corrects what they send.
v2.1.1
v2.1.1
Usable on phones
A patch release about screens, not about data: collection, wire format and
database are unchanged since 2.1.0.
Dashboard on phones
The dashboard used to fit on a phone without being usable on one: the cards
clipped their tables, so five of seven columns were unreachable — not even by
swiping. Now, below 760 px every table row becomes a small labelled block, with
the column name taken from the table's own header (so all nine tables and both
languages get this for free). The header controls wrap instead of scrolling out
of reach, the header itself scrolls away while the section bar stays pinned,
touch targets meet the 40 px minimum, the headline section is compact, and the
timing heatmap scrolls sideways instead of shrinking to 4 px cells.
Fixed
- Clipped tables at laptop width. From 1000 px the grid switched to two
columns, leaving each panel ~480 px wide — too narrow for the wider tables,
which were silently cut off. Two columns now start at 1400 px, and between
760 and 1400 px the wide tables scroll horizontally inside their card.
This had been broken at every laptop-sized window since the beginning. - Sticky header stopped sticking — everywhere. An
overflow-x:hidden
added against 4 px of sideways scroll turned the page into a scroll container
and silently disabledposition:stickyon every screen size. The guard is
gone; the actual 4 px came from the handover bar's segment gaps and are fixed
at the source withoverflow-x:clip, which trims without breaking sticky. - The live demo was broken on phones: its notice banner was up to 240 px
tall and pinned, and the section bar floated 285 px from the top because the
demo's layout offsets fought the new phone rules. On narrow screens the
banner now shows one line and scrolls away, and the offsets reset. - MSI wizard banner: the page title no longer collides with the artwork.
Docs
The front-page README is cut from 540 to 150 lines — GPO settings, quick start,
screenshots (collapsed), done. Everything removed lives on, word for word, in
docs/OPERATIONS.md: component details, the full CLI
reference, troubleshooting, and the checklist for actually turning NTLM off.
Upgrade notes
- Collector: replace the file, restart, hard-refresh (Ctrl+F5).
- Agent/MSI: optional — version bump only, no behaviour change.
v2.1.0
v2.1.0
Installer, live demo, shareable views
Everything the collector gathers is unchanged since 2.0.0 — same event IDs, same
wire format, same database. This release is about getting the tool into people's
hands and making a finding easy to pass on.
MSI installer
ntlm-agent.msi is attached to every release from now on. Double-clicking opens
a wizard asking for the collector URL, an optional API key and the poll
interval; the same values are properties, so an unattended rollout uses the
identical package:
msiexec /i ntlm-agent.msi /qn COLLECTORURL=https://collector:8443
The package owns the payload and the service entry, so Windows Installer removes
both again on uninstall. config.json in ProgramData is deliberately not part of
the package, so an upgrade keeps existing settings.
On the API key: the wizard field is masked and the property is hidden, but a
key supplied to an installer can still end up in a verbose install log, and on an
msiexec command line it is visible in the process list. The agent README states
this plainly and recommends setting the key separately with
ntlm-agent.exe configure for unattended rollouts. Two attempts to close the
logging path failed, and rather than claim otherwise the limitation is
documented.
The MSI builds in its own workflow. If WiX ever breaks, the agent build and its
release stay green.
Live demo
[nobrac.github.io/NTLM-Analyzer](https://nobrac.github.io/NTLM-Analyzer/) serves
the real dashboard against synthetic data from a lab domain. GitHub Pages runs
no Python, so every reachable click state — 112 of them — is pre-computed at
build time and answered from a file. The charts, drill-downs, search, filters,
language toggle, event details and CSV download all work; only the time range
and machine picker are fixed, because they multiply with everything else.
Shareable views
Filter state now lives in the URL. Clicking a cause, a chart bar or a bar
segment produces a link that opens the same view for whoever you send it to, and
bookmarks work. Back and Forward move between filter steps; typing in the search
box does not flood the history.
This is the first time the page reads anything from the address bar, so
"reflected XSS is impossible" stopped being automatically true. Every parameter
is validated against what it is allowed to be rather than trusted — an unknown
range or an over-long value never reaches the query. Re-tested with five payload
classes across ten parameters: nothing executed, nothing raw in the DOM.
Smaller things
- Sortable tables everywhere, numeric and alphabetical
- Language is remembered across reloads
- Page title shows the share, so several open tabs are distinguishable
- Skip link as the first tab stop, live region announcing the result
count after a drill-down - Favicon and an application icon, embedded in the agent binary along with a
version resource —ntlm-agent.exenow reports its version in the file
properties like any other Windows program - 4 px of horizontal scroll on phone-width viewports is gone
Fixed
- Table sorting glued a value and its badge together ("172" and "24" became
17224) — only a leading number counts now - Back left the site entirely, because only
replaceStatewas used and there
was no history entry to go back to - The page title was set and then overwritten by the next render
- The icon and version resource failed silently when the resource compiler
was missing; CI now asserts they are present
Upgrade notes
- Collector: replace the file, restart, hard-refresh with Ctrl+F5.
- Agent: optional. The binary only gains an icon and version info; behaviour
is unchanged. Existing installs keep working. - No database migration, wire protocol unchanged.
v2.0.0
v2.0.0
New dashboard
The dashboard has been rebuilt. Everything the collector gathers is unchanged —
same 33 event IDs, same wire format, same database — but the page that shows it
is new, and it answers a different first question.
The headline is a share, not a count
"4,000 NTLM events" means nothing without knowing what it is 4,000 out of. The
page now opens with "Still 26 % of all logons go through NTLM", computed
against Kerberos in the same period, with the direction of travel underneath.
The API gained krb_ev so the share can be computed at all.
Below it the handover bar splits the estate three ways — insecure, outdated,
target state. The bar is geometrically exact; hovering a segment raises a card
with the name, count and share, and clicking one filters the event list to it.
Exact figures sit in a readout beneath, which always lists all three including a
zero, because a category that silently vanishes reads as a rendering fault.
Everything is a way in
The three charts are drill-downs. Click a day in the trend, a cell in the
heatmap, a cause in "Why NTLM", or a segment of the handover bar, and the event
list filters to exactly those events. The filters run server-side using the same
SQL predicates the charts are counted with — a day three weeks back is not in
the list the page happens to be holding, and a filter built from a slightly
different expression would return a different number than the bar it came from.
Verified row by row against every panel.
Clicking an event opens the raw record as Windows reported it: every field
grouped and labelled, an explanation of what that event ID means, and what a
failure code implies. Buttons jump to everything from the same account, program
or machine.
New panels
- Most-used targets — which server absorbs the most NTLM
- Accounts already using Kerberos — the healthy side, by account
- Agents by OS — a ring counting reporting machines by Windows build, amber
for anything predating Server 2019, which cannot produce the 40xx events at all - Domain and forest functional level, read from rootDSE
Layout and readability
Full width with a fixed column count — two on anything up to a 1440p desktop, a
third only past 2900 px, because auto-fitting packed four abreast and read as a
wall. Type is larger throughout, surfaces are lighter, and the contrast of the
faintest text against a panel went from 3.31:1 to 4.69:1 — clearing WCAG AA for
the first time. Table headers are their own band; secondary cells carry real
content and sit at 7.53:1 rather than 4.69:1.
Fixed along the way
Several of these had shipped unnoticed and were only found by rendering the
running page in a real browser rather than reasoning about the code:
- The heatmap was one day off. The server stores the grid Monday-first, the
page read it Sunday-first, so every row showed the following day's data and
the "peak" line named the wrong day. Three places shared the mistake. - The trend chart drew nothing. Correct percentage heights inside a
container whose columns had no definite height, so they collapsed to zero.
Invisible to any test that does not lay the page out. - The events panel reported its own cap as the result — "300 / 300" no
matter how many matched. It now reads "300 / 2,431 – newest 300 loaded". - No way to log out when a dashboard password was configured.
- A placeholder printed literally in the machines panel. Every translation
string is now checked against its call site. - Dropdown lists were rendered light on a dark page (
color-schemewas missing). - Account lists ran together without spaces.
Security
The rebuilt dashboard was re-audited rather than assumed safe. Six payload
classes were injected into twenty attacker-influenced fields via the real ingest
API, the page was rendered against the result and every detail drawer opened:
zero executable payloads in 107,717 characters of generated HTML, with a
counter-check confirming the payloads arrived and were escaped. Five security
headers including a default-src 'none' CSP, login enforced on every endpoint,
no state-changing GET, CSV export proof against all eight injection and
delimiter attacks.
Unchanged and still worth doing before an enterprise rollout: TLS between
agent and collector, restricted network access to the dashboard, and running
with --password.
Upgrade notes
- Collector only for the dashboard. Replace the file, restart, hard-refresh
with Ctrl+F5 — without it the browser serves the old page from cache. - Existing databases gain two columns automatically at startup.
- Agents keep working unchanged. The functional-level fields stay empty until
agents are updated; nothing else depends on them. - Item status ("done") set in the old dashboard survives — it lives in the
database.
v1.9.0
v1.9.0
Prototype new dashboard
The dashboard has been rebuilt from scratch. Same data, same API, same database —
a different way of presenting it. Nothing about collection or storage changed.
The page now opens with a statement, not a grid of tiles
The old dashboard began with six metric cards of equal weight. The new one begins
with the one sentence that describes the project: "Still 21 % of all logons go
through NTLM." Below it, a handover bar shows the split between NTLMv1, NTLMv2
and Kerberos as shares rather than absolute counts, so it does not swing just
because one day was busier than another. Next to it, a live countdown to the
October 2026 deadline.
Underneath, four focus cards name what to look at first — largest item, quickest
win, what breaks at the deadline, and anything happening outside office hours —
each one a link into the section it came from.
Full screen width, self-packing layout
The layout has no fixed column count and no maximum width. Panels declare a
minimum width and the grid packs as many per row as the window allows: four on a
27-inch monitor, two on a laptop, one on a phone. Wide panels (programs,
machines, domain view) span two columns where there is room.
Everything is clickable
Clicking a program, an account, a machine or a Kerberos service filters the event
list and jumps to it. Active filters appear as chips you can remove one at a time.
Clicking an event opens a detail drawer showing every field grouped by meaning —
identity, target, process, protocol, diagnostics — with an explanation of what
that event ID means and what a failure code implies. From the drawer you can
pivot to all events of the same program, account, machine or kind.
Kept from the old dashboard
All eleven sections are still there, plus two new ones: most-used targets and
a separate Kerberos accounts panel. Also unchanged: per-program sparklines,
the exception-list generator for Group Policy, the status dropdowns with "hide
done", the timing heatmap, CSV export, the machine readiness table with its
auditing and October 2026 columns, and the German/English toggle — the full
translation set carried over and grew to 365 keys per language.
Fixed: no way to log out
The rebuilt page had a logout button in the markup but nothing ever showed it or
handled the click - anyone running with a dashboard password could log in and
never end the session from the UI. The server now reveals the button exactly
when a password is configured, and clicking it invalidates the session
server-side (the endpoint always did).
Fixed: the share of NTLM was not computable
stats.total counts every stored event including Kerberos, and stats.krb
counts Kerberos services, not tickets. Neither could answer "what share of
authentication still uses NTLM". The API now also returns krb_ev, the Kerberos
ticket count, which is what the new headline is built on.
Upgrade notes
- Collector only. Replace the file and restart the service, then hard-refresh
the browser (Ctrl+F5) so the new page is loaded instead of the cached one. - Agents are unchanged; the version bump only keeps the numbers in step.
- No database migration, wire protocol unchanged. Item statuses set in the old
dashboard carry over.
v1.8.2
v1.8.2
Enterprise operations hardening
This release is the result of a blue-team style review of the collector and
installer with production deployment in mind, plus a consistency pass over the
agent. No functional behaviour changes; everything here is about running the
tool safely in a company network.
Hardened (server): file permissions and information disclosure
- Database is no longer world-readable. The collector sets a restrictive
umask before creating any file and chmods an existing database (plus
-wal/-shm) to600. The database records who authenticates to what —
reconnaissance gold that must not be readable by every local user on a
shared server. No-op on Windows, where NTFS ACLs govern. - No Python version in response headers. The
Server:header now reads
NtlmCollector/1.0without the Python build — one less free hint for
anyone scanning the network. - TLS key permission warning. On POSIX systems the collector warns at
startup when the private key file is world-readable. Group access stays
silent on purpose: it is common practice (Debian'sssl-certgroup), and
POSIX ACLs — the install script's way of granting the service user read
access — mirror their mask into the group bits, so warning on those would
false-alarm on every clean install.
Verified as already solid (no changes needed)
Session tokens expire server-side (a stolen cookie dies after the TTL
regardless of what the client claims), expired sessions are purged, logout
invalidates server-side, and login always issues a fresh token — no session
fixation. There are no state-changing GET endpoints, so the CSRF surface ends
at SameSite=Lax plus CSP form-action 'self'. The installer's secrets file
(collector.env, mode 600) is read by the collector via environment-variable
defaults, so neither the API key nor the dashboard password ever appears in
the process list or the unit file.
Hardened (agent): service password can be prompted
--service-password * now asks for the password interactively with echo
turned off, instead of taking it inline. An inline password lands in the
admin's shell history and — where command-line auditing (event 4688) is
enabled — in the Security log itself; for an NTLM auditing tool that would be
an own goal. gMSA (trailing $, no password) remains the recommended way to
run the service. Empty input aborts cleanly.
Fixed: German placeholder leaked into API data
Rows without a process or target name were filled with (unbekannt) straight
from the SQL layer — on an English dashboard that German word showed up in the
tables. The placeholder is now (unknown); the UI language toggle was never
involved, this was a data value.
Changed: agent log and error messages unified to English
Eight leftover German strings in agent log lines and CLI errors
(unbekanntes Argument, Keine neuen NTLM-Events, …) are now English, in
line with the rest of the code base. Error messages are what people paste
into search engines. The German event label constants (Kontoname,
Gerätename) stay — they are functionality for German-language Windows, not
leftovers.
Upgrade notes
- Collector: replace the file and restart the service. The database
permission tightening applies automatically on the next start. - Agent: redeploy is optional — nothing in the collection behaviour
changed. Redeploy when you want the hidden password prompt at install time
or the English log lines. - No database migration, wire protocol unchanged.