feat: extract Windows identity from LDAP + Kerberos traffic (#809) - #810
Open
NotYuSheng wants to merge 7 commits into
Open
NotYuSheng wants to merge 7 commits into
NotYuSheng wants to merge 7 commits into
Conversation
Closes the gap found via the #808 CTF-demo walkthrough: TracePcap correctly identified the STRRAT C2 IP and malware family, unprompted, on a real malware-traffic-analysis.net incident capture — but missed the victim's Windows username (ccollier / "Clark Collier"), which the exercise's own official answer key found via a manual LDAP filter. TracePcap had zero LDAP/Kerberos parsing. Two corroborating signals, verified directly against the real capture: - Kerberos AS-REQ (kerberos.msg_type==10, kerberos.CNameString) — sent by the client itself, so this is MEASURED-grade: unambiguous proof of who is logged in at that IP. AS-REP/TGS-REP are deliberately excluded: their source IP is the KDC, not the client, so including them would mislabel the domain controller's own IP with every account it has issued a ticket for. - LDAP searchRequest DN (ldap.baseObject) — REPORTED-grade and weaker (ambiguous: could be a self-lookup or a query about someone else). Filtering on which attributes were requested was tried and abandoned: a certificate-template DN in the real capture legitimately requests displayName/cn too, producing a false positive. The robust discriminator is the DN's own shape (CN=<name>,CN=Users,DC=...) — AD person objects live under CN=Users, infrastructure objects do not. Architecture follows two existing patterns rather than inventing a new one: - Extraction: the conflict-preserving hostname-claims pattern (HostnameResolverService/HostnameClaimWriter) — a username is testimony, not a measurement to pick a winner for at write time. - Adjudication: the generic Adjudicator/HumanOverrideEntity framework (not the older static-priority HostnameAdjudicator) — a new "windows-identity" question gets human override, staleness, and audit trail for free, with zero new table or REST endpoint for override support. Deliberately simpler than HostIdentityService: source-priority (Kerberos > LDAP-DN), not weighted voting, since there's no equivalent of device-type classification signals to combine. New WindowsIdentityClaimLookup SPI port (analysis.spi) is required by the frozen ArchUnit layer rule (modules_use_analysis_spi_not_its_internals) — the insights-module adjudicator cannot import analysis' repository/entity directly. Verified LayerDependencyTest passes with the frozen store unchanged. Frontend needed zero new components: AdjudicationPanel is already question-agnostic by design; this is one new instance in HostIdentitySection.tsx. Verified end-to-end against the real exercise capture through a rebuilt backend: victim 172.16.1.66 -> primaryLabel "ccollier", basis MACHINE, confidence 90, not contested, with the LDAP "Clark Collier" claim carried as a corroborating candidate — and confirmed no false positive on the domain controller and no $-suffixed machine-account principal ever surfaces as a primary label. Full backend suite (494/494 effective, 1 skipped unrelated) and frontend suite (642/642) green; new code lint-clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmsoLQT1zESF94noajQ4t8
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
) New docs/features/windows-identity.rst, matching the existing feature-doc conventions (see label-staleness.rst) rather than folding the explanation into the #808 demo walkthrough — the capability documents like any other TracePcap feature, independent of the specific incident that motivated it. Covers the mechanism (Kerberos AS-REQ vs. LDAP DN, source priority, the machine-account and cleartext-credential exclusions), a worked example using the real verified capture from #809's implementation, where it surfaces in the UI, and what it deliberately does not do. Added to the Features toctree in index.rst, next to mac-lookup (the other host-identification feature). Built cleanly with sphinx-build -W: the only warnings/errors in the full build are pre-existing, in network-monitor.rst and streaming-upload.rst, neither touched here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmsoLQT1zESF94noajQ4t8
…uidance Two small UI fixes found while verifying #809's Windows Identity panel, folded into this PR at the user's request: - HostIdentitySection's "Evidence weighed" axis facts were joined into one run-on line (facts.join(' · ')) instead of appearing one per line. Now a proper unstyled list, one <li> per fact. Also switched the row's align-items from center to start, since the label/chevron no longer sit next to a single-line value. - AdjudicationPanel's "Add evidence" weight slider (1-100) had no guidance on what value to pick. Added a short calibration hint anchored to the scores already visible in the same panel's "Why" section, since every question type using this generic component shows per-candidate scores there. Both are pre-existing code, unmodified by #809 itself, unrelated to the Windows Identity feature — found only because manually verifying that feature in the browser surfaced them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XmsoLQT1zESF94noajQ4t8
- WindowsIdentitiesController: gate the lazy-backfill on there being an actual unadjudicated claim or human override, not just on the rows table being empty. Fixes permanent re-adjudication on every GET for files with no Kerberos/LDAP signal, where an empty result is a legitimate terminal state. - HostIdentitySection.tsx: reset windowsIdentity at the top of load(). It's an independent fetch not gated by the `loading` flag, so a stale value from the previous host could render under the new host's panel while the (slower) windows-identity fetch was still in flight. - WindowsIdentityResolverService: drop the unused ldap.AttributeDescription field extraction (still used in the -Y filter, just never read from the row), and widen the CN capture group to consume RFC 4514 backslash-escaped commas instead of truncating at them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rework the Windows identity feature per review: instead of a separate adjudicated "windows-identity" question with its own table, endpoint, and UI panel, a Windows domain sign-in (Kerberos AS-REQ / LDAP) is now what it actually is — evidence that the host is a domain-joined Windows workstation, and a person-level attribute of that host. - New WindowsDomainAuthSignal (device classifier): votes toward LAPTOP_DESKTOP, with reasons naming the principal and protocol, so the sign-in shows up under the host's "Evidence weighed" breakdown and the differing Kerberos (+40, authenticated) vs LDAP (+15, lookup) weights are self-explanatory. - New signed-in-user host attribute (logged_in_user / _source on host_classifications), set in AnalysisService from the claims — mirrors the hostname attribute and surfaces as a "Signed-in user" row with a Kerberos/LDAP source badge, next to Hostname. - Set the attribute in AnalysisService (analysis module, which owns the entity) rather than in the classifier, so no new hostclassification -> analysis.entity dependency is introduced; the ArchUnit store is refreshed only for the classify() signature change (29 -> 29, no growth). - Remove the bespoke stack: WindowsIdentityService adjudicator, its entity/ repo/dto/controller, the claims persistence layer (entity/repo/writer/SPI/ adapter), migrations V42/V43, and the frontend windows-identity panel/ endpoint/type. The resolver is kept and feeds the signal in-memory. - Regenerate OpenAPI baseline + schema.d.ts (endpoint removed, loggedInUser added); rewrite the feature doc to describe a signal + attribute. Verified end-to-end against the real STRRAT exercise pcap: 172.16.1.66 classifies LAPTOP_DESKTOP with "ccollier" (Kerberos) as signed-in user and both signals cited in evidence; the domain controller 172.16.1.4 correctly has no signed-in user; no machine-account principal is ever surfaced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Let analysts show the Windows signed-in user (ccollier) as a line under each node in the network diagram, alongside hostname/IP/device type. Added to the node-label field set (default off, appended to existing configs via the normalizer) with a live-preview example and value resolution from node.data.loggedInUser, which the classification data already carries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RFC 4514 regression: personNameFromDn now unescapes the CN value, so a "Last, First" display name (CN=Collier\, Clark,...) is stored/shown as "Collier, Clark", not with the wire-format backslash. Handles both \c and \XX hex escapes; test updated to assert the unescaped value. - LDAP recall gap: the tshark -Y filter no longer also requires ldap.AttributeDescription. A searchRequest for all attributes carries no attribute list, and requiring it silently dropped those person lookups — the DN-shape regex is the real person-vs-infrastructure discriminator. Verified against the exercise pcap: still resolves only "Clark Collier", no false positives on the certificate-template/config DNs. - WindowsDomainAuthSignal Javadoc corrected to match the additive behavior (both Kerberos and LDAP votes stack; LDAP weighs less, it isn't suppressed). - Accessibility: the "Evidence weighed" per-line facts are now spans in a flex column, not a <ul>/<li> nested inside a role="button" element (invalid content model that confuses accessible-name computation). - De-duplicated the source-chip badges: new shared SourceBadge parameterised by a source-info map; HostnameSourceBadge and LoggedInUserSourceBadge now delegate to it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Closes #809. Motivated by #808 — a CTF-demo walkthrough of a real malware-traffic-analysis.net incident found TracePcap correctly identifies malware C2 (Suricata) and victim IP/MAC/hostname unprompted, but had zero LDAP/Kerberos parsing and missed the victim's real identity (
ccollier/ "Clark Collier"), which the exercise's own official answer key found via a manual LDAP filter.Two corroborating signals, verified directly against the real capture
kerberos.msg_type==10,kerberos.CNameString) — sent by the client itself, so this is MEASURED-grade: unambiguous proof of who is logged in at that IP. AS-REP/TGS-REP are deliberately excluded: their source IP is the KDC, not the client, so including them would mislabel the domain controller's own IP with every account it has ever issued a ticket for.ldap.baseObject) — REPORTED-grade and weaker (ambiguous: could be a self-lookup, or a query about someone else). Filtering on which attributes were requested (givenName,sn,displayName, ...) was tried and abandoned: a certificate-template DN in the real capture legitimately requestsdisplayName/cntoo, producing a false positive. The robust discriminator is the DN's own shape (CN=<name>,CN=Users,DC=...) — AD person objects live underCN=Users; infrastructure objects do not.Machine-account principals (
DESKTOP-SKBR25F$) are excluded. No cleartext LDAP bind credential (ldap.simple) is ever requested or parsed — stated as a deliberate design constraint in the resolver's own class doc.Architecture — two existing patterns, not a new one
HostnameResolverService/HostnameClaimWriter) — a username is testimony, not a measurement to pick a winner for at write time.Adjudicator/HumanOverrideEntityframework (not the older static-priorityHostnameAdjudicator) — a new"windows-identity"question gets human override, staleness, and audit trail for free, with zero new table or REST endpoint for override support. Deliberately simpler thanHostIdentityService: source-priority (Kerberos > LDAP-DN), not weighted voting, since there's no equivalent of device-type classification signals to combine.A new
WindowsIdentityClaimLookupSPI port (analysis.spi) is required by the frozen ArchUnit layer rule (modules_use_analysis_spi_not_its_internals) — theinsights-module adjudicator can't importanalysis' repository/entity directly.LayerDependencyTestpasses with the frozen store unchanged.Frontend needed zero new components:
AdjudicationPanelis already question-agnostic by design — this is one new instance inHostIdentitySection.tsx.Verified end-to-end against the real exercise capture
Rebuilt the backend, uploaded the actual "You dirty rat!" exercise pcap, confirmed via
GET /files/{fileId}/windows-identities:[ { "ip": "172.16.1.66", "primaryLabel": "ccollier", "basis": "MACHINE", "confidence": 90, "contested": false, "candidates": [ { "label": "ccollier", "score": 90, "source": "kerberos_as_req" }, { "label": "Clark Collier", "score": 55, "source": "ldap_dn" } ] } ]Also confirmed: exactly one row total (no false positive on the domain controller
172.16.1.4), and no$-suffixed machine-account principal ever surfaces as a primary label anywhere.Test plan
mvn test -Dtest=WindowsIdentityResolverServiceTest,WindowsIdentityServiceTest— 11/11 pass (DN-regex discriminator including the real certificate-template false-positive case, machine-account exclusion, Kerberos-over-LDAP priority, contested-on-multiple-distinct-usernames, human-override-preserves-machine-candidates, no-claims-no-row)mvn test -Dtest=LayerDependencyTest— passes, frozen ArchUnit store unchangedmvn test(full backend suite) — 494/494 effective pass (1 skipped, unrelated@Tag("system")test)npm run typecheck/npm run test/npm run lint(frontend) — typecheck clean, 642/642 tests pass, new code lint-cleanV42/V43applied cleanly on a real Postgres via Flyway (Successfully applied 2 migrations ... now at version v43)Adjudicate: 2 adjudicator(s) (2 DETERMINISTIC) — questions: host-identity, windows-identityccolliersurfaces correctly atGET /files/{fileId}/windows-identitieswith both signals citedopenapi/baseline.json,frontend/src/services/api/generated/schema.d.ts) against the live rebuilt backend🤖 Generated with Claude Code