Skip to content

Add offline ldap2json import (#11)#21

Open
p0dalirius wants to merge 1 commit into
masterfrom
enhancement-ldap2json-import
Open

Add offline ldap2json import (#11)#21
p0dalirius wants to merge 1 commit into
masterfrom
enhancement-ldap2json-import

Conversation

@p0dalirius

Copy link
Copy Markdown
Owner

Linked Issue

Closes #11

Motivation

Issue #11 asks for the ability to load an ldap2json export and run queries against it without a live LDAP server (useful for post-engagement triage, sharing datasets, or writing queries without access to the DC). This PR wires the JSON loader and a filter-aware in-memory searcher into the existing console, so the existing `query`, `presetquery`, `diff`, and `export` commands work against an offline dataset.

What Changed

  • `flatten_ldap2json(tree)`: walks the hierarchical ldap2json format (keys are RDN components because they contain `=`; attributes do not) and reconstructs `{dn: {attr: value, ...}}`. The ldap2json writer reverses DN components on write, so we rejoin them in reverse order to rebuild the original DN.
  • `load_ldap2json_file(path)`: parses the JSON, flattens it, and derives the set of naming contexts (any DN with no ancestor DN also present in the dataset).
  • `parse_ldap_filter(filter_str)`: recursive-descent parser for the common subset of RFC 4515: `(attr=value)`, `(attr=)`, `(attr=prefix)`, `(attr=*suffix)`, `(attr=infix)`, `(&...)`, `(|...)`, `(!...)`. Returns a matcher closure `attrs -> bool` with case-insensitive attribute names and string comparisons (matches AD's default). Extensible-match (`:rule:`) and approximate match (`~=`) are out of scope; the parser strips extensible-match modifiers on the attribute name so filters that include them don't raise.
  • `OfflineLDAPSearcher`: exposes the same `query(base_dn, query, attributes, search_scope=...)` and `query_all_naming_contexts(...)` signatures used by the REPL. Supports `BASE`, `LEVEL`, and `SUBTREE` scope. Honors the `attributes` projection (`["*"]` returns the full record, otherwise only the listed attributes).
  • New CLI flag `--ldap2json FILE`: when set, the main flow skips `init_ldap_session` entirely, builds an `OfflineLDAPSearcher` from the file, and uses the first discovered naming context as `search_base`. Hash parsing and the `--kdcHost` check are now gated behind "no offline file was provided" so offline mode does not require credential arguments.

Design Notes

The offline searcher is intentionally independent of ldap3's server/connection objects — it only needs the flattened dict and the list of NCs. The parser is small (~60 lines) and correct for the filter shapes that appear in `PresetQueries` and typical user queries; more exotic filter grammar (octet escape sequences, extensible matching rules) is documented as unsupported rather than half-implemented.

Acceptance Criteria Check

  • New `--ldap2json FILE` argument accepted — registered alongside `--xlsx` in `parseArgs`.
  • Offline mode bypasses authentication — main flow branches on `options.ldap2json_file` and does not call `init_ldap_session`.
  • Queries against the offline dataset work — `OfflineLDAPSearcher.query` returns results keyed by DN, same shape as `LDAPSearcher.query`.
  • Compatible with the existing `-q` single-query path and the interactive REPL — both call `ls.query(...)` which dispatches polymorphically.

How Verified

Runtime: with a representative ldap2json sample (users in `CN=Users`, a computer in `CN=Computers`, a `krbtgt` entry with an SPN):

  • `load_ldap2json_file` reconstructed the six expected DNs and identified the two top-level container DNs as naming contexts.
  • `(sAMAccountName=alice)` returned the single alice entry.
  • The full kerberoastable filter `(&(objectClass=user)(servicePrincipalName=*)(!(objectClass=computer))(!(cn=krbtgt)))` correctly returned only alice.
  • `(sAMAccountName=*)` returned all four account-bearing DNs.
  • Wildcard forms `al*`, `*ob`, `li` each narrowed to the expected match.
  • `BASE` scope on `CN=alice,...` returned exactly that DN; `LEVEL` scope on `CN=Users,...` returned only its direct children.
  • Attribute projection `attributes=['sAMAccountName']` returned only that key per record.
  • Malformed filter `((broken)` surfaced `Invalid Filter. (Expected '=' at position 8 ...)` rather than crashing.

Test Coverage

None — the repository has no test suite. The behaviors above were exercised via a one-off smoke script against a synthetic ldap2json sample.

Scope of Change

  • Files changed: `ldapconsole.py`
  • Submodule pointer updated: no
  • Public API changes: additive — new module-level helpers (`flatten_ldap2json`, `load_ldap2json_file`, `parse_ldap_filter`), new `OfflineLDAPSearcher` class, new `--ldap2json` CLI flag. Existing CLI surface and `LDAPSearcher` untouched.
  • Behavioral changes outside the stated enhancement: none

Risk and Rollout

Behavior on the live-LDAP path is unchanged — the offline branch is only taken when `--ldap2json` is supplied. Safe to merge without staged rollout.

Notes

The supported filter grammar is documented inline on `parse_ldap_filter`. Extensible matching rules and `~=` are not implemented; filters that rely on them will either raise or silently ignore the modifier (extensible prefix on the attribute name is stripped).

Add --ldap2json FILE flag that loads an ldap2json JSON export,
flattens the hierarchical tree back into {dn: {attr: value, ...}}, and
serves queries from memory via a new OfflineLDAPSearcher. Includes a
small recursive-descent parser for the common subset of RFC 4515
filters (equality, presence, substring wildcards, AND, OR, NOT) so the
existing query/presetquery/export commands work offline.
@p0dalirius p0dalirius self-assigned this Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[enhancement] Add an option to read and import an LDAP2JSON .json file offline

1 participant