Repository navigation
Say where the telemetry stops, instead of showing an empty table - #40
Merged
Merged
Conversation
Techniques compare through the ATT&CK hierarchy rather than as strings, the console gets a design system and charts, and four separate silent breaks behind one blank inventory tab are fixed and pinned: a snapshot table that was also deduplicated, an encrypted column too narrow for its ciphertext, a collector chain where the first failure disabled the four after it, and a read that never decrypted.
d3vhex
added a commit
that referenced
this pull request
Sep 8, 2026
…alth Say where the telemetry stops, instead of showing an empty table
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three ways to render an empty table
The Network and Hardware tabs were blank. The agent's log said
every cycle, and the server held none of them. Both were telling the truth,
and there turned out to be three unrelated breaks behind one symptom.
A table cannot be both a snapshot and deduplicated.
network_connections,hardware_inventory,critical_filesanddocker_containerswere in bothsets. A snapshot is emptied before each batch; a duplicate row is skipped when
its fingerprint has been seen. Together: batch one writes 50 rows, batch two
deletes them and skips every replacement, and every batch after that does the
same. Full once, permanently empty afterwards. The two lists are now disjoint
and
_assert_ingest_modes_are_exclusiverefuses the overlap at import, so thenext one is a server that will not start rather than a tab nobody can explain.
An encrypted column has to be wide enough for the ciphertext.
local_addrwasVARCHAR(64)and is one of the fieldsenc_dbencrypts. AFernet token is over a hundred characters whatever the plaintext, so every
batch failed on its first row with
1406 Data too long— after the snapshotDELETE had already run. Four columns were too narrow;
tests/test_encrypted_column_widths.pycompares the two declarations that haveto agree, so the next one fails in CI.
One failing collector took the four after it.
edr_enforcer.maincalledfive collectors as five bare statements, so the first to raise ended the cycle
— and the caller retries on a timer, so it ended every cycle after that too.
Network is second and hardware is fourth, which is why they went dark together.
They are isolated now and each names itself.
periodic_wrappedalso reportedfailures behind
if debug, which nothing sets: a collector that raised on itsfirst line failed silently for the life of the agent.
Telemetry health
The deeper reason none of that was visible: the agent marks rows sent as soon
as
sendallreturns. The ingest protocol has no reply, so a server thataccepts a batch and stores none of it is indistinguishable from one that
stored all of it. The agent reported success in all three cases and was not
wrong to.
/telemetry-health/<agent>puts the two halves next to each other — what theagent holds and believes it shipped, against what this server holds — and
names the break:
lost in transit,not collected,send failing,queued,flowing. All five used to be the same blank list, and each needs a differentperson to do a different thing. The page is reached from the host, which is
where the empty tab that prompts the question is.
The proper fix is an acknowledged ingest, so the agent stops deleting rows it
only assumes arrived. That is a wire-protocol change and belongs with the TLS
work, where the ingest protocol is opened anyway.
ATT&CK as a hierarchy
The coverage endpoint compared techniques as opaque strings, so a rule tagged
T1003.001and an event carryingT1003never matched: one landed in"covered but never seen" and the other in "seen with nothing covering it".
Those two lists are the only ones an operator acts on — one sends somebody to
test a detection that already works, the other to write a rule that exists.
The fix is not "compare parents". Detecting
T1003.001(LSASS memory) saysnothing about
T1003.003(the AD database): different action, differenttelemetry, and the rule for one will never fire on the other. So coverage
travels down from a parent and only partially up from a child, and
covered_only_by_a_siblingis now its own state — the one most easilymistaken for coverage, because the rolled-up cell is green while that specific
action would go unnoticed.
/attack-chain/<agent>lays a host's tactics out in kill-chain order with thestages that did not happen drawn as gaps. The gaps are the point: you cannot
see from a list of the four tactics that fired that nothing was ever
exfiltrated.
A design system, and charts
1291 inline style objects across 24 pages, and
StatCardwritten threeseparate times. No page was written carelessly — there was no shared
vocabulary to be careless about, so spacing, radius and colour were decided
again on every page and diverged.
components/uiis that vocabulary: page header, card, stat, table, badge, andLoadingState/EmptyState/ErrorStateas three separate things, becausecollapsing them is how an empty alert list and an alert list that failed to
load came to look identical.
The toy affordances went with it.
.card:hoverlifted two pixels and glowedover a 12px backdrop blur — on a page that is mostly cards, the whole console
moved when the pointer crossed it. Radius is one small scale now; the old set
had 12px cards beside 4px inputs.
Recharts sits behind wrappers that read the tokens, because it styles itself
with props and left alone draws a white grid on a black page. Series colours
are deliberately not the severity colours: a bar being red should not imply
danger unless danger is what the bar measures. The dashboard gains fleet
health, connection status, version spread and a two-axis threat trend — a
spike in detections against a flat distinct-technique line is one noisy rule,
not an incident, and only both series together show that.
Also
channel_connected: whether a command would reachthe host now, which is not the same question as whether it is reporting.
verify_auth_wiringcould not resolve websocket handlers, so the boot-timeauth audit covered none of them — and those three are the agent channel, a
root shell, and a live view of a screen.
bash -ntests were failing on a WSL VM that would not start, whichread as "the build script is broken shell". A syntax error names itself on
stderr; an unrunnable shell leaves it empty.