Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/openhound_sccm/collectors/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ def collect_registry(target: str, ctx: "SourceContext") -> Iterable[tuple[str, d
# server name only when AD resolution failed (ad_object
# is None or lacks a name).
logger.info("Found %s, this target is a site server", SCCM_REG_KEYS["component_servers"])
target_entry = ctx.target_hosts_by_hostname[target]
target_entry = ctx.target_hosts_by_hostname[target.lower()]
row = {
**(target_entry.ad_object or {}),
"source": "RemoteRegistry-ComponentServers",
Expand Down Expand Up @@ -529,7 +529,7 @@ def collect_registry(target: str, ctx: "SourceContext") -> Iterable[tuple[str, d
"database lives elsewhere. Emitting the SQL Server role as assumed "
"pending corroboration in preprocess.", target,
)
target_entry = ctx.target_hosts_by_hostname[target]
target_entry = ctx.target_hosts_by_hostname[target.lower()]
row = {
**(target_entry.ad_object or {}),
"source": "RemoteRegistry-MultisiteComponentServers",
Expand Down Expand Up @@ -600,7 +600,7 @@ def get_current_user(probe: _RegistryProbe, ctx: SourceContext) -> Iterable[tupl
current_user_ad_object = ctx.resolve_principal(current_user_sid)
if current_user_ad_object:
logger.info("Found current user: %s (%s)", current_user_ad_object.get("sam_account_name"), current_user_sid)
target_entry = ctx.target_hosts_by_hostname.get(probe.hostname)
target_entry = ctx.target_hosts_by_hostname.get(probe.hostname.lower())
host_sid = target_entry.ad_object.get("object_sid") if (target_entry and target_entry.ad_object) else None
if host_sid is None:
# No resolved host AD object — HasSession can't be built for this row downstream; keep the row but log.
Expand Down Expand Up @@ -663,7 +663,7 @@ def get_ntlm_settings(probe: _RegistryProbe, ctx: SourceContext) -> Iterable[tup
disable_loopback_check = disable_loopback_reg == 1
logger.verbose(f"DisableLoopbackCheck is {'enabled' if disable_loopback_check else 'disabled'}")

target_entry = ctx.target_hosts_by_hostname[probe.hostname]
target_entry = ctx.target_hosts_by_hostname[probe.hostname.lower()]
row = {
**(target_entry.ad_object or {}),
"source": "RemoteRegistry-NTLMSettings",
Expand Down Expand Up @@ -869,7 +869,7 @@ def get_mssql_settings(probe: _RegistryProbe, ctx: SourceContext) -> Iterable[tu
probe, instance_names
)

target_entry = ctx.target_hosts_by_hostname[probe.hostname]
target_entry = ctx.target_hosts_by_hostname[probe.hostname.lower()]

yield "remoteregistry_mssql_servers", {
"source": "RemoteRegistry-MSSQL",
Expand Down
35 changes: 35 additions & 0 deletions tests/registry_collect_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,41 @@ def test_none_result_does_not_crash(monkeypatch):
assert [row for table, row in rows if table == "remoteregistry_users"] == []


# --- target_hosts_by_hostname case sensitivity ---------------------------------

def test_mixed_case_target_does_not_kill_the_registry_phase(monkeypatch):
"""Regression: target_hosts_by_hostname is keyed by lowercased hostname
(context.py), but collect_registry is called with whatever case the target
was actually discovered in -- e.g. LDAP name-pattern matches return the AD
computer's own casing ('MECM.sccm.lab'), not lowercase. Every lookup against
that dict must lower() its key first, or get_ntlm_settings raises KeyError
before ever reaching the low-privilege-readable site-code / component-server
keys later in the same generator -- silently losing the whole RemoteRegistry
phase for that host, not just the NTLM-settings row."""
mixed_case_target = "PS1-PSS.MAYYHEM.COM"
ctx = FakeCtx()
ctx.target_hosts_by_hostname = {TARGET: _Entry(ad_object={"name": "PS1-PSS"})}

def _fake_probe(*a, **k):
probe = FakeProbe(_site_server(**{COMPONENTS: ["PS1-CCM.MAYYHEM.COM"]}), [])
probe.hostname = mixed_case_target
return probe

monkeypatch.setattr(registry, "_RegistryProbe", _fake_probe)

rows = list(registry.collect_registry(mixed_case_target, ctx))

# Reached past get_ntlm_settings (no KeyError) and all the way to the
# component-server site-server row -- the exact data this bug was blocking.
site_server_rows = [
row for table, row in rows
if table == "remoteregistry_computers"
and row["source"] == "RemoteRegistry-ComponentServers"
and row["sccm_site_system_roles"] == ["SMS Site Server@PS1"]
]
assert len(site_server_rows) == 1


# --- Multisite Component Servers ----------------------------------------------

def test_multisite_empty_key_marks_local_site_database(monkeypatch):
Expand Down