Skip to content

Releases: Nobrac/NTLM-Analyzer

v2.4.0

Choose a tag to compare

@Nobrac Nobrac released this 25 Sep 20:44

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 the setspn command 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, so cifs/ or http/ on a computer account is not reported.
  • 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 read servicePrincipalName, 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

  1. Collector: replace ntlm-collector.py, restart, hard-refresh.
  2. 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

Choose a tag to compare

@Nobrac Nobrac released this 25 Sep 20:02

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

  1. Collector: replace ntlm-collector.py, restart, hard-refresh.
    Own scripts that post to /ingest or /status must send
    Content-Type: application/json - the agent always does.
  2. Agents: nothing to do if they run 2.3.1. Older ones: see 2.3.1.

v2.3.1

Choose a tag to compare

@Nobrac Nobrac released this 25 Sep 19:35

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 under C:\ProgramData. Whoever created NtlmAgent before 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
    failing icacls was 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 NAME keep the key out of shell history and event 4688.
  • HTTPS required (low). install/configure refuse an http:// collector
    unless --allow-http (MSI: ALLOWHTTP=1) is given. Existing configurations
    keep working and warn at every service start.
  • Build supply chain (low). Cargo.lock is 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.
  • configure without a key no longer wipes the stored one, so changing the URL
    or upgrading silently keeps it (--clear-api-key removes 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
    that http:// 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

  1. Collector: replace ntlm-collector.py, restart, hard-refresh.
  2. 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, then ntlm-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.
  3. Collector on plain HTTP? Use https:// - or add ALLOWHTTP=1 (MSI) /
    --allow-http (EXE) to keep HTTP knowingly.

v2.3.0

Choose a tag to compare

@Nobrac Nobrac released this 24 Sep 22:19

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.
  • 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 now open, in_progress and done; audit states reported by the agent
    are on/off instead of an/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/account and /report, all behind the
    dashboard login.
  • Agent: 12 unit tests for the audit-policy parser and the new event mappings.

Upgrading

  1. Collector: replace ntlm-collector.py, restart, hard-refresh
    (Ctrl+F5). Database changes happen on start, nothing to do by hand.
  2. 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.
  3. 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

Choose a tag to compare

@Nobrac Nobrac released this 23 Sep 11:49

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

  1. 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.
  2. 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.
  3. Agent — everywhere else at your own pace. The MSI upgrades 2.1.x in
    place and keeps the configuration.
  4. 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

Choose a tag to compare

@Nobrac Nobrac released this 23 Aug 00:58

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 disabled position:sticky on 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 with overflow-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

Choose a tag to compare

@Nobrac Nobrac released this 17 Aug 22:21

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.exe now 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 replaceState was 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

Choose a tag to compare

@github-actions github-actions released this 17 Aug 12:02

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-scheme was 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

Choose a tag to compare

@github-actions github-actions released this 16 Aug 22:59

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

Choose a tag to compare

@github-actions github-actions released this 16 Aug 13:18

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) to 600. 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.0 without 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's ssl-cert group), 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.