For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the three coerce-and-relay possible edges (CoerceAndRelayToAdminService, CoerceAndRelayToMSSQL, CoerceAndRelayToSMB) and the synthetic Authenticated Users group node in preproc, emitting them through the unchanged Convert2-Read-DB pipeline — porting CMBP's Process-CoerceAndRelayToAdminService / …ToMSSQL / …ToSMB and the synthetic Authenticated Users Upsert-Node.
Architecture: Pure transforms.py + graph.py + models/graph_edge.py + kinds/edges.py additions flowing through the existing pipeline — no new collection, no new node model (Authenticated Users is a Group emitted by the existing GroupNode). Each relay is a set-based join over the node tables Stages 1–5 already produced: AdminService joins SMS-Provider × Site-Server computers per site; MSSQL drives off node_mssql_login (which already encodes the sysadmin-computer↔server pairing); SMB joins SMB-signing-disabled site systems × Site-Server computers. The synthetic Authenticated Users node is built lazily — only for the domains a relay edge actually starts from — and inserted into node_group before _node_backfill/_graph_edges_split so it joins the AD id set and merges with SharpHound's well-known-SID node.
Tech Stack: Python 3.13+, dlt, duckdb, openhound (v0.2.x), pydantic, pytest, uv.
Tracking: gtk ope-d820 (links Ope-o008 "Verify CoerceAndRelayToSMB Lifecycle", Ope-zaja "Relay to Management Point"). Baseline: post-Stage-5 working tree — transforms() ends … _edge_mssql_db_assign_all → _graph_edges_dedup → _node_backfill → _graph_edges_split; graph_edges is (start_id, end_id, kind, collection_source VARCHAR[]); main.py uses SCCM_NODE_SPECS/AD_NODE_SPECS/SCCM_EDGE_SPECS/AD_EDGE_SPECS. Spec: ../specs/2026-06-16-sccm-preproc-convert-design.md §6 Stage 6 + inventory §3. CMBP reference: Process-CoerceAndRelayToAdminService ps1:6572-6624; …ToMSSQL ps1:6626-6726; …ToSMB ps1:6728-6781; call site (per non-secondary site) ps1:1948-1962; synthetic Authenticated Users Upsert-Node ps1:6609/:6708/:6766; Upsert-Edge array-merge ps1:2155-2158; traversable allow-list ps1:2216-2249.
- Only modify code under
sccm/sccm/. Never edit OpenHound core (openhound/...). (CLAUDE.md) - Do NOT
git addorgit commit. Each task ends at a green-test checkpoint only — run the tests, confirm pass, then stop. The user commits after testing. (CLAUDE.md, [[sdd-no-commit-harness]]) - Validate in the isolated uv env (already synced from Stage 0–5) — never touch the repo
.venv:UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest <abs test path> -v. (AGENTS.md) - Log every
if/elseandtry/exceptbranch at an appropriate level, or leave a comment. (CLAUDE.md) - Edge/node property keys are CMBP-cased on output, snake_case in DuckDB. New
SCCMRelayEdgePropertiesfields use ConfigManBearPig.ps1-verbatim casing:coercionVictimAndRelayTargetPairs,coercionVictimHostnames. DuckDB columns + model input fields stay snake_case (coercion_victim_and_relay_target_pairs,coercion_victim_hostnames,smb_signing_source). (See [[sccm-property-casing-cmbp]].) - Tests live in
sccm/sccm/tests/as<name>_test.py. Run with the isolated-env pytest targetingtests/. - No re-collect for Stage 6 — preproc/convert-only; reads the existing lab raw tree.
- Reuse the existing helpers in
transforms.py:_safe(con, label, sql)(skips+logs missing tables; takes no SQL params — inline literals),_ensure_columns,_arr(col),_root_code.domain_environment_id(sid, fallback_domain_sid=None)lives ingraph.pyand is used by the models. - Standard test loop (collect once, reuse the raw JSONL):
openhound preprocess sccm <raw> <raw>/lookup.duckdb openhound convert sccm <raw>/sccm <graph> --lookup-file <raw>/lookup.duckdb # then inspect <graph>/*.json for the three CoerceAndRelay* edges + the AUTHENTICATED USERS group node
--disable-possible-edges= SURGICAL. Default (flag off) keeps CMBP's "null NTLM/EPA ⇒ assume vulnerable ⇒ emit". With the flag, every assumption tightens to explicitly confirmed: NTLM literallyOff(drop NULL), EPA literallyOff(drop NULL/assumed), SMB-signing literallyfalse(already explicit). Confirmed relay paths survive; speculative ones drop. (CMBP itself only gated the MSSQL-EPA-unknown branch — this is the faithful-but-fixed reading of the spec's Stage 6 validation, which says all three vanish under the flag.)- Authenticated Users node id =
UPPER(<FQDN>)-S-1-5-11(SharpHound well-known-SID form, e.g.MAYYHEM.COM-S-1-5-11) so it merges with SharpHound-collected AD data — fixes CMBP's as-collected-case id (ps1:6606). FQDN derived from the victim computer'sdnshostname(first/host label stripped).name = AUTHENTICATED USERS@<FQDN-UPPER>, kindsGroup+Base,environmentid= the victim's AD-domain SID (thefallback_domain_sid, sinceS-1-5-11has no domain SID). Built lazily — only for domains an actual relay edge starts from. - SMB relay CMBP bug FIXED. CMBP's traversable allow-list (ps1:2221) names
CoerceAndRelayNTLMtoSMB, but the function (ps1:6775) emitsCoerceAndRelayToSMB— they never match, so CMBP renders the SMB relay non-traversable. The port emitsCoerceAndRelayToSMBAND makes it traversable, by replacing the deadCoerceAndRelayNTLMtoSMBstring inTRAVERSABLE_EDGE_KINDS. (CoerceAndRelayToAdminService/CoerceAndRelayToMSSQLare already correct in the set.) - Coercion-context props = port, relay-only. Add typed
coercion_victim_and_relay_target_pairs VARCHAR[]+coercion_victim_hostnames VARCHAR[]columns tograph_edges; array-union them in_graph_edges_dedup(CMBP'sUpsert-Edgemerges arrays, ps1:2155-2158); carry them through_graph_edges_split. A relay-onlySCCMRelayEdgeProperties(SCCMEdgeProperties)subclass surfacescoercionVictimAndRelayTargetPairs/coercionVictimHostnameson the three relay kinds only, so the ~33 other edges keep their current clean output. - SMB relay
collectionSource= add provenance column.node_computercarries no per-sourcecollection_source. Addsmb_signing_source VARCHAR[], set by thesmb_computersSELECT (['SMB-Negotiate']) andremoteregistry_computersSELECT (['RemoteRegistry-SMBSigningCheck']), array-unioned in the final GROUP BY; the SMB relay filters it. Cross-cutting Stage 1 change ⇒ update the Stage 1node_computercoverage + ARCHITECTURE.md. (AdminService stays the static['Post-processing']CMBP passes at :1955; MSSQL filtersnode_mssql_server.collection_sourceto the EPA sources, which already exist.)
| # | Kind | Start → End | CMBP | NTLM/EPA/SMB gate (default ⇒ flag) | collection_source |
Extra prop |
|---|---|---|---|---|---|---|
| 1 | CoerceAndRelayToAdminService |
AuthUsers(site-server domain) → SCCM_Site | :6615 | provider NTLM null/Off ⇒ explicit Off |
['Post-processing'] |
coercionVictimAndRelayTargetPairs |
| 2 | CoerceAndRelayToMSSQL |
AuthUsers(sysadmin-computer domain) → MSSQL_Login | :6717 | host NTLM null/Off and server EPA null/Off ⇒ both explicit Off |
server EPA sources | coercionVictimAndRelayTargetPairs |
| 3 | CoerceAndRelayToSMB |
AuthUsers(site-server domain) → Computer (vuln. site system) | :6775 | target smb_signing_required=false (explicit) and NTLM null/Off ⇒ explicit Off |
target smb_signing_source |
coercionVictimHostnames |
All three relay edges route to the AD payload (
graph_edges_ad): each starts at the Authenticated Users node, which is an AD node (node_group). No change to_graph_edges_split's routing logic is needed beyond carrying the two new columns through it.
| File | Responsibility |
|---|---|
src/openhound_sccm/kinds/edges.py |
modify — add 3 relay edge-kind constants; fix TRAVERSABLE_EDGE_KINDS (CoerceAndRelayNTLMtoSMB → CoerceAndRelayToSMB). |
src/openhound_sccm/graph.py |
modify — add SCCMRelayEdgeProperties(SCCMEdgeProperties) with the two CMBP-cased coercion list fields. |
src/openhound_sccm/models/graph_edge.py |
modify — read the two coercion columns; emit SCCMRelayEdgeProperties for relay kinds, base SCCMEdgeProperties otherwise. |
src/openhound_sccm/transforms.py |
modify — add the _authed_users_id SQL helper; add coercion_* columns in _graph_edges_init; carry them in _graph_edges_dedup/_graph_edges_split; add smb_signing_source to _node_computer; add _edge_coerce_relay_adminservice, _edge_coerce_relay_mssql, _edge_coerce_relay_smb, _node_authenticated_users; wire all four into transforms(). |
README.md |
modify — Edge Reference (3 relay edges, possible-edge + surgical --disable-possible-edges note), Node Reference (synthetic Authenticated Users group), mayyhem.com examples. |
ARCHITECTURE.md |
modify — §11c graph_edges columns (+2 coercion cols, relay-only props); node_computer smb_signing_source provenance; Stage 6 changelog entry. |
docs/superpowers/plans/2026-06-30-sccm-preproc-convert-stage6-validation.md |
create — code-tour validation harness (final task). |
tests/kinds_edges_stage6_test.py · graph_edge_relay_props_test.py · graph_edges_coercion_cols_test.py · node_computer_smb_signing_source_test.py · edge_coerce_relay_adminservice_test.py · edge_coerce_relay_mssql_test.py · edge_coerce_relay_smb_test.py · node_authenticated_users_test.py |
create — per-task test suites. |
Convert/lookup (no change): SCCMLookup.table_rows is generic; convert_pipeline.emit_graph_from_duckdb iterates (table, model) specs generically. The three relay edges flow through the single graph_edges table → _graph_edges_split → graph_edges_ad (read by AD_EDGE_SPECS, untagged). The Authenticated Users node lands in node_group (already in AD_NODE_SPECS). So Stage 6 adds no spec entries and no main.py/convert_pipeline.py change.
Files:
- Modify:
src/openhound_sccm/kinds/edges.py - Test:
tests/kinds_edges_stage6_test.py
Interfaces:
-
Produces:
edges.COERCE_AND_RELAY_TO_ADMIN_SERVICE == "CoerceAndRelayToAdminService",COERCE_AND_RELAY_TO_MSSQL == "CoerceAndRelayToMSSQL",COERCE_AND_RELAY_TO_SMB == "CoerceAndRelayToSMB"(consumed by Phase E/F/G builders andgraph_edge.py).TRAVERSABLE_EDGE_KINDScontains all three relay strings and no longer contains"CoerceAndRelayNTLMtoSMB". -
Step 1: Write the failing test — create
tests/kinds_edges_stage6_test.py:
from openhound_sccm.kinds import edges as ek
def test_relay_edge_kind_values():
assert ek.COERCE_AND_RELAY_TO_ADMIN_SERVICE == "CoerceAndRelayToAdminService"
assert ek.COERCE_AND_RELAY_TO_MSSQL == "CoerceAndRelayToMSSQL"
assert ek.COERCE_AND_RELAY_TO_SMB == "CoerceAndRelayToSMB"
def test_all_three_relays_traversable():
# All three relay edges are real attack paths; CMBP intended them traversable
# (the SMB one was a name-mismatch bug, ps1:2221 vs :6775 — fixed here).
assert ek.COERCE_AND_RELAY_TO_ADMIN_SERVICE in ek.TRAVERSABLE_EDGE_KINDS
assert ek.COERCE_AND_RELAY_TO_MSSQL in ek.TRAVERSABLE_EDGE_KINDS
assert ek.COERCE_AND_RELAY_TO_SMB in ek.TRAVERSABLE_EDGE_KINDS
def test_dead_smb_allowlist_name_removed():
# The never-matching CMBP allow-list string must be gone, else the SMB relay
# silently reverts to non-traversable.
assert "CoerceAndRelayNTLMtoSMB" not in ek.TRAVERSABLE_EDGE_KINDS- Step 2: Run the test to verify it fails
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/kinds_edges_stage6_test.py -v
Expected: FAIL — AttributeError: module ... has no attribute 'COERCE_AND_RELAY_TO_ADMIN_SERVICE' (and the traversable assertions fail because CoerceAndRelayNTLMtoSMB is still present).
- Step 3: Add the constants. In
src/openhound_sccm/kinds/edges.py, after the Stage 5 block (the lineMSSQL_GET_TGS = "MSSQL_GetTGS") add:
# Stage 6 edge kinds (coerce-and-relay possible edges).
COERCE_AND_RELAY_TO_ADMIN_SERVICE = "CoerceAndRelayToAdminService"
COERCE_AND_RELAY_TO_MSSQL = "CoerceAndRelayToMSSQL"
COERCE_AND_RELAY_TO_SMB = "CoerceAndRelayToSMB"- Step 4: Fix the traversable set. In
src/openhound_sccm/kinds/edges.py, inside theTRAVERSABLE_EDGE_KINDSfrozenset, change the line
"CoerceAndRelayToAdminService", "CoerceAndRelayToMSSQL", "CoerceAndRelayNTLMtoSMB",to
# CMBP's allow-list (ps1:2221) named "CoerceAndRelayNTLMtoSMB", but the function
# (ps1:6775) emits "CoerceAndRelayToSMB" — the mismatch left the SMB relay
# non-traversable. The port emits CoerceAndRelayToSMB and marks it traversable.
"CoerceAndRelayToAdminService", "CoerceAndRelayToMSSQL", "CoerceAndRelayToSMB",- Step 5: Run the test to verify it passes
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/kinds_edges_stage6_test.py -v
Expected: 3 passed.
- Step 6: Green checkpoint (NO commit). Stop. Report PASS + the three new constants.
Files:
- Modify:
src/openhound_sccm/graph.py - Modify:
src/openhound_sccm/transforms.py(_graph_edges_initonly) - Modify:
src/openhound_sccm/models/graph_edge.py - Test:
tests/graph_edge_relay_props_test.py
Interfaces:
-
Consumes:
edges.COERCE_AND_RELAY_TO_*(Task A1);SCCMEdgeProperties(existing,graph.py). -
Produces:
graph.SCCMRelayEdgeProperties(fieldscoercionVictimAndRelayTargetPairs: list[str],coercionVictimHostnames: list[str], plus the inheritedcollectionSource/traversable);graph_edgestable gainscoercion_victim_and_relay_target_pairs VARCHAR[]+coercion_victim_hostnames VARCHAR[];GraphEdgereadscoercion_victim_and_relay_target_pairs/coercion_victim_hostnames(consumed by Phase E/F/G via the materialized table). -
Step 1: Write the failing test — create
tests/graph_edge_relay_props_test.py:
from openhound_sccm.graph import SCCMEdgeProperties, SCCMRelayEdgeProperties
from openhound_sccm.kinds import edges as ek
from openhound_sccm.models.graph_edge import GraphEdge
def test_relay_kind_emits_relay_properties_with_coercion_lists():
row = GraphEdge(
start_id="MAYYHEM.COM-S-1-5-11",
end_id="PS1",
kind=ek.COERCE_AND_RELAY_TO_ADMIN_SERVICE,
collection_source=["Post-processing"],
coercion_victim_and_relay_target_pairs=["Coerce SS01.mayyhem.com, relay to PROV01.mayyhem.com"],
coercion_victim_hostnames=None,
)
edge = next(iter(row.edges))
assert isinstance(edge.properties, SCCMRelayEdgeProperties)
assert edge.properties.traversable is True
assert edge.properties.collectionSource == ["Post-processing"]
assert edge.properties.coercionVictimAndRelayTargetPairs == [
"Coerce SS01.mayyhem.com, relay to PROV01.mayyhem.com"
]
assert edge.properties.coercionVictimHostnames == []
def test_smb_relay_carries_victim_hostnames():
row = GraphEdge(
start_id="MAYYHEM.COM-S-1-5-11",
end_id="S-1-5-21-1-2-3-1104",
kind=ek.COERCE_AND_RELAY_TO_SMB,
collection_source=["SMB-Negotiate"],
coercion_victim_and_relay_target_pairs=None,
coercion_victim_hostnames=["SS01.mayyhem.com"],
)
edge = next(iter(row.edges))
assert isinstance(edge.properties, SCCMRelayEdgeProperties)
assert edge.properties.coercionVictimHostnames == ["SS01.mayyhem.com"]
def test_non_relay_kind_uses_base_properties_no_coercion_fields():
row = GraphEdge(
start_id="A", end_id="B", kind=ek.SCCM_HAS_MEMBER,
collection_source=["AdminService-SMS_FullCollectionMembership"],
)
edge = next(iter(row.edges))
assert isinstance(edge.properties, SCCMEdgeProperties)
assert not isinstance(edge.properties, SCCMRelayEdgeProperties)
assert not hasattr(edge.properties, "coercionVictimHostnames")- Step 2: Run the test to verify it fails
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/graph_edge_relay_props_test.py -v
Expected: FAIL — ImportError: cannot import name 'SCCMRelayEdgeProperties'.
- Step 3: Add the relay properties dataclass. In
src/openhound_sccm/graph.py, immediately after theSCCMEdgePropertiesdataclass (the one whose only field iscollectionSource), add:
@dataclass
class SCCMRelayEdgeProperties(SCCMEdgeProperties):
# Coerce-and-relay context (CMBP Process-CoerceAndRelayTo*). ONLY the three
# CoerceAndRelay* edge kinds carry these — every other edge keeps the lean base
# SCCMEdgeProperties so its BloodHound panel stays uncluttered. Field names mirror
# ConfigManBearPig.ps1 exactly (ps1:6617/6719/6777).
coercionVictimAndRelayTargetPairs: list[str] = field(default_factory=list, kw_only=True)
coercionVictimHostnames: list[str] = field(default_factory=list, kw_only=True)- Step 4: Add the two
graph_edgescolumns. Insrc/openhound_sccm/transforms.py, replace the body of_graph_edges_initso theCREATEcarries the coercion columns:
def _graph_edges_init(con: duckdb.DuckDBPyConnection, schema: str) -> None:
"""Create the empty graph_edges table that every edge builder INSERTs into.
Always runs (even with no site/edge data) so convert can read the table.
The coercion_* columns are populated only by the Stage 6 relay builders; every
other builder INSERTs BY NAME and leaves them NULL (dedup coalesces NULL -> [])."""
con.execute(
f"CREATE OR REPLACE TABLE {schema}.graph_edges "
f"(start_id VARCHAR, end_id VARCHAR, kind VARCHAR, collection_source VARCHAR[], "
f"coercion_victim_and_relay_target_pairs VARCHAR[], coercion_victim_hostnames VARCHAR[])"
)- Step 5: Wire the columns through
GraphEdge. Replacesrc/openhound_sccm/models/graph_edge.pywith:
# src/openhound_sccm/models/graph_edge.py
"""GraphEdge: converts any graph_edges row into an OpenGraph edge of the matching kind.
Each row in the graph_edges preproc table represents one directed relationship
between two graph nodes. This model reads those rows and emits an Edge whose
`traversable` property is set from the CMBP allow-list. The three coerce-and-relay
kinds additionally carry coercion-context lists (SCCMRelayEdgeProperties); every
other kind uses the lean base SCCMEdgeProperties. It never produces a node
(as_node returns None) because graph_edges rows are pure edge data.
"""
import logging
from typing import Iterator
from openhound.core.asset import BaseAsset
from openhound.core.models.entries_dataclass import Edge, EdgePath
from pydantic import ConfigDict
from ..graph import SCCMEdgeProperties, SCCMRelayEdgeProperties
from ..kinds.edges import (
COERCE_AND_RELAY_TO_ADMIN_SERVICE,
COERCE_AND_RELAY_TO_MSSQL,
COERCE_AND_RELAY_TO_SMB,
TRAVERSABLE_EDGE_KINDS,
)
logger = logging.getLogger(__name__)
# The only edge kinds that carry coerce-and-relay context lists.
_RELAY_KINDS = frozenset({
COERCE_AND_RELAY_TO_ADMIN_SERVICE,
COERCE_AND_RELAY_TO_MSSQL,
COERCE_AND_RELAY_TO_SMB,
})
class GraphEdge(BaseAsset):
"""One graph_edges row -> one OpenGraph edge of any kind. Endpoints matched by id;
`traversable` is set from the CMBP allow-list. Never produces a node."""
model_config = ConfigDict(populate_by_name=True, extra="ignore")
start_id: str | None = None
end_id: str | None = None
kind: str | None = None
collection_source: list[str] | None = None
coercion_victim_and_relay_target_pairs: list[str] | None = None
coercion_victim_hostnames: list[str] | None = None
@property
def as_node(self) -> None:
"""Graph edges never produce a node."""
return None
@property
def edges(self) -> Iterator[Edge]:
"""Yield one Edge for this row.
If start_id, end_id, or kind is missing, the row is dropped with a
warning rather than emitting a malformed edge.
"""
if not self.start_id or not self.end_id or not self.kind:
logger.warning(
"GraphEdge: dropping incomplete row (start=%r end=%r kind=%r)",
self.start_id, self.end_id, self.kind,
)
return
traversable = self.kind in TRAVERSABLE_EDGE_KINDS
if self.kind in _RELAY_KINDS:
# Relay edges carry the operator-facing coercion context (CMBP).
properties = SCCMRelayEdgeProperties(
traversable=traversable,
collectionSource=self.collection_source or [],
coercionVictimAndRelayTargetPairs=self.coercion_victim_and_relay_target_pairs or [],
coercionVictimHostnames=self.coercion_victim_hostnames or [],
)
else:
# Every other edge keeps the lean base properties.
properties = SCCMEdgeProperties(
traversable=traversable,
collectionSource=self.collection_source or [],
)
yield Edge(
kind=self.kind,
start=EdgePath(match_by="id", value=self.start_id),
end=EdgePath(match_by="id", value=self.end_id),
properties=properties,
)- Step 6: Run the test to verify it passes
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/graph_edge_relay_props_test.py -v
Expected: 3 passed.
- Step 7: Run the existing edge-model + convert tests to confirm no regression
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests -k "graph_edge or convert or edge_" -v
Expected: all pass (the non-relay path is unchanged — base SCCMEdgeProperties, same output).
- Step 8: Green checkpoint (NO commit). Stop. Report PASS.
Files:
- Modify:
src/openhound_sccm/transforms.py(_graph_edges_dedup,_graph_edges_split) - Test:
tests/graph_edges_coercion_cols_test.py
Interfaces:
-
Consumes: the
graph_edgesschema from Task B1. -
Produces:
_graph_edges_deduparray-unions both coercion columns per(start_id, end_id, kind);graph_edges_adandgraph_edges_sccmboth exposecoercion_victim_and_relay_target_pairs+coercion_victim_hostnames(consumed byGraphEdgeviaAD_EDGE_SPECS/SCCM_EDGE_SPECS). -
Step 1: Write the failing test — create
tests/graph_edges_coercion_cols_test.py:
import duckdb
from openhound_sccm.transforms import _graph_edges_init, _graph_edges_dedup, _graph_edges_split
def _seed(con):
con.execute("CREATE SCHEMA IF NOT EXISTS sccm")
_graph_edges_init(con, "sccm")
# Two duplicate relay rows for the same (start,kind,end) with different pairs:
# dedup must array-union them (mirrors CMBP Upsert-Edge merge).
con.execute(
"INSERT INTO sccm.graph_edges VALUES "
"('MAYYHEM.COM-S-1-5-11','PS1','CoerceAndRelayToAdminService',"
" ['Post-processing'], ['Coerce A, relay to X'], NULL),"
"('MAYYHEM.COM-S-1-5-11','PS1','CoerceAndRelayToAdminService',"
" ['Post-processing'], ['Coerce B, relay to X'], NULL),"
# A non-relay edge with NULL coercion columns must survive as empty lists.
"('PS1','C1','SCCM_Contains', ['SCCM_Invoke-PostProcessing'], NULL, NULL)"
)
def test_dedup_unions_coercion_pairs_and_handles_nulls():
con = duckdb.connect()
_seed(con)
_graph_edges_dedup(con, "sccm")
rows = con.execute(
"SELECT kind, sort(coercion_victim_and_relay_target_pairs), coercion_victim_hostnames "
"FROM sccm.graph_edges WHERE kind = 'CoerceAndRelayToAdminService'"
).fetchall()
assert len(rows) == 1
assert rows[0][1] == ["Coerce A, relay to X", "Coerce B, relay to X"]
assert rows[0][2] == [] # NULL -> []
nonrelay = con.execute(
"SELECT coercion_victim_and_relay_target_pairs, coercion_victim_hostnames "
"FROM sccm.graph_edges WHERE kind = 'SCCM_Contains'"
).fetchone()
assert nonrelay == ([], []) # NULL coalesced to [] for non-relay rows too
def test_split_carries_coercion_columns_to_ad_payload():
con = duckdb.connect()
_seed(con)
_graph_edges_dedup(con, "sccm")
# Minimal node tables so the AuthUsers start id lands in the AD id set.
con.execute("CREATE TABLE sccm.node_computer AS SELECT NULL::VARCHAR AS sid WHERE false")
con.execute("CREATE TABLE sccm.node_user AS SELECT NULL::VARCHAR AS sid WHERE false")
con.execute(
"CREATE TABLE sccm.node_group AS SELECT 'MAYYHEM.COM-S-1-5-11'::VARCHAR AS sid"
)
con.execute("CREATE TABLE sccm.node_backfill AS SELECT NULL::VARCHAR AS id WHERE false")
_graph_edges_split(con, "sccm")
ad_cols = [d[0] for d in con.execute("DESCRIBE sccm.graph_edges_ad").fetchall()]
assert "coercion_victim_and_relay_target_pairs" in ad_cols
assert "coercion_victim_hostnames" in ad_cols
# The AdminService relay (AuthUsers start) is AD-routed.
ad_kinds = [r[0] for r in con.execute("SELECT kind FROM sccm.graph_edges_ad").fetchall()]
assert "CoerceAndRelayToAdminService" in ad_kinds- Step 2: Run the test to verify it fails
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/graph_edges_coercion_cols_test.py -v
Expected: FAIL — dedup currently drops the coercion columns (Binder Error: ... coercion_victim_and_relay_target_pairs not in the rebuilt table) / split's SELECT doesn't include them.
- Step 3: Update
_graph_edges_dedup. Insrc/openhound_sccm/transforms.py, replace thecon.execute(...)inside_graph_edges_dedupwith:
con.execute(
f"CREATE OR REPLACE TABLE {schema}.graph_edges AS "
f"SELECT start_id, end_id, kind, "
f" coalesce(list_distinct(flatten(list(collection_source))), CAST([] AS VARCHAR[])) AS collection_source, "
# Stage 6: array-union the coercion lists (CMBP Upsert-Edge merges arrays, ps1:2155-2158).
# FILTER drops the NULLs that every non-relay builder leaves in these columns.
f" coalesce(list_distinct(flatten(list(coercion_victim_and_relay_target_pairs) "
f" FILTER (WHERE coercion_victim_and_relay_target_pairs IS NOT NULL))), CAST([] AS VARCHAR[])) "
f" AS coercion_victim_and_relay_target_pairs, "
f" coalesce(list_distinct(flatten(list(coercion_victim_hostnames) "
f" FILTER (WHERE coercion_victim_hostnames IS NOT NULL))), CAST([] AS VARCHAR[])) "
f" AS coercion_victim_hostnames "
f"FROM {schema}.graph_edges "
f"GROUP BY start_id, end_id, kind"
)- Step 4: Update
_graph_edges_split. Insrc/openhound_sccm/transforms.py, in_graph_edges_split, extend BOTHSELECTlists (thegraph_edges_adone and thegraph_edges_sccmone) to carry the coercion columns. Change each
f"SELECT e.start_id, e.end_id, e.kind, e.collection_source "to
f"SELECT e.start_id, e.end_id, e.kind, e.collection_source, "
f" e.coercion_victim_and_relay_target_pairs, e.coercion_victim_hostnames "- Step 5: Run the test to verify it passes
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/graph_edges_coercion_cols_test.py -v
Expected: 2 passed.
- Step 6: Run the existing dedup/split tests to confirm no regression
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests -k "dedup or split or graph_edges" -v
Expected: all pass.
- Step 7: Green checkpoint (NO commit). Stop. Report PASS.
Files:
- Modify:
src/openhound_sccm/transforms.py(_node_computer) - Test:
tests/node_computer_smb_signing_source_test.py
Interfaces:
-
Produces:
node_computer.smb_signing_source VARCHAR[]—['SMB-Negotiate']for rows seen by the SMB negotiate probe,['RemoteRegistry-SMBSigningCheck']for rows seen by the remote-registry signing check, array-unioned per SID;[]for computers neither probe touched (consumed by the SMB relay in Phase G). -
Step 1: Write the failing test — create
tests/node_computer_smb_signing_source_test.py:
import duckdb
from openhound_sccm.transforms import _node_computer
def _seed(con):
con.execute("CREATE SCHEMA IF NOT EXISTS sccm")
# Same SID seen by BOTH the SMB negotiate probe and the remote-registry check.
con.execute(
"CREATE TABLE sccm.smb_computers AS SELECT "
"'S-1-5-21-1-2-3-1104' AS object_sid, 'SS01' AS name, "
"'SS01.mayyhem.com' AS dns_host_name, 'CN=SS01' AS distinguished_name, "
"false AS smb_signing_required"
)
con.execute(
"CREATE TABLE sccm.remoteregistry_computers AS SELECT "
"'S-1-5-21-1-2-3-1104' AS object_sid, 'SS01' AS name, "
"'SS01.mayyhem.com' AS dns_host_name, 'CN=SS01' AS distinguished_name, "
"false AS smb_signing_required, 'Off' AS restrict_receiving_ntlm_traffic"
)
# A computer only LDAP knew about -> no smb_signing_source.
con.execute(
"CREATE TABLE sccm.adminservice_r_system AS SELECT "
"'S-1-5-21-1-2-3-2222' AS sid, 'WS01' AS name, NULL AS resource_id, "
"NULL AS source_site_code, NULL AS system_roles, NULL AS sms_unique_identifier, "
"false AS obsolete"
)
def test_smb_signing_source_unions_both_probes():
con = duckdb.connect()
_seed(con)
_node_computer(con, "sccm")
src = con.execute(
"SELECT sort(smb_signing_source) FROM sccm.node_computer "
"WHERE sid = 'S-1-5-21-1-2-3-1104'"
).fetchone()[0]
assert src == ["RemoteRegistry-SMBSigningCheck", "SMB-Negotiate"]
def test_smb_signing_source_empty_when_unprobed():
con = duckdb.connect()
_seed(con)
_node_computer(con, "sccm")
src = con.execute(
"SELECT smb_signing_source FROM sccm.node_computer "
"WHERE sid = 'S-1-5-21-1-2-3-2222'"
).fetchone()[0]
assert src == []- Step 2: Run the test to verify it fails
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/node_computer_smb_signing_source_test.py -v
Expected: FAIL — Binder Error: Referenced column "smb_signing_source" not found.
- Step 3: Add the staging column. In
_node_computer, in theCREATE OR REPLACE TABLE {schema}.node_computer (...)staging DDL, add the column right after thesmb_signing_required BOOLEAN,line:
"smb_signing_required BOOLEAN, "
"smb_signing_source VARCHAR[], " # which probe(s) reported signing: SMB-Negotiate / RemoteRegistry-SMBSigningCheck- Step 4: Tag the two probe sources. In the
smb_computersINSERT SELECT, add the tag line right afterf"smb_signing_required, ":
f"smb_signing_required, "
f"['SMB-Negotiate'] AS smb_signing_source, "In the remoteregistry_computers INSERT SELECT, add right after its f"smb_signing_required, ":
f"smb_signing_required, "
f"['RemoteRegistry-SMBSigningCheck'] AS smb_signing_source, "(All other source SELECTs leave smb_signing_source unnamed; INSERT ... BY NAME defaults it to NULL.)
- Step 5: Aggregate in the final GROUP BY. In
_node_computer's finalCREATE OR REPLACE TABLE {schema}.node_computer AS SELECT ...(the collapse), add this aggregate right after thebool_or(smb_signing_required) AS smb_signing_required,line:
f" bool_or(smb_signing_required) AS smb_signing_required, "
# Array-union the probe tags; FILTER drops the NULLs other sources leave.
f" coalesce(list_distinct(flatten(list(smb_signing_source) "
f" FILTER (WHERE smb_signing_source IS NOT NULL))), CAST([] AS VARCHAR[])) AS smb_signing_source, "- Step 6: Run the test to verify it passes
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/node_computer_smb_signing_source_test.py -v
Expected: 2 passed.
- Step 7: Run the existing node_computer + Stage 1 tests to confirm no regression
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests -k "node_computer or computer or stage1 or node1" -v
Expected: all pass (the new column is additive; ComputerNode ignores it via extra="ignore", so the node output is unchanged).
- Step 8: Green checkpoint (NO commit). Stop. Report PASS. Note in the task report that
ARCHITECTURE.mdwill document this cross-cutting change in Task I1.
Files:
- Modify:
src/openhound_sccm/transforms.py(add_authed_users_idhelper +_edge_coerce_relay_adminservice) - Test:
tests/edge_coerce_relay_adminservice_test.py
Interfaces:
-
Consumes:
edges.COERCE_AND_RELAY_TO_ADMIN_SERVICE;node_computer(sid,dnshostname,site_system_roles,restrict_receiving_ntlm_traffic);site_hierarchy(site_code,site_type). -
Produces:
_authed_users_id(col)-> SQL fragmentupper(regexp_replace(col, '^[^.]+\.', '')) || '-S-1-5-11';_edge_coerce_relay_adminservice(con, schema, disable_possible)INSERTsCoerceAndRelayToAdminServicerows (start = Authenticated Users id of the site server's domain, end = non-secondary site code) withcoercion_victim_and_relay_target_pairs. -
Step 1: Write the failing test — create
tests/edge_coerce_relay_adminservice_test.py:
import duckdb
from openhound_sccm.transforms import _graph_edges_init, _edge_coerce_relay_adminservice
def _seed(con, provider_ntlm):
con.execute("CREATE SCHEMA IF NOT EXISTS sccm")
con.execute(
"CREATE TABLE sccm.site_hierarchy AS SELECT 'PS1' AS site_code, 2 AS site_type "
"UNION ALL SELECT 'SEC' AS site_code, 1 AS site_type"
)
con.execute(
"CREATE TABLE sccm.node_computer AS SELECT * FROM (VALUES "
# provider: SMS Provider@PS1 with given NTLM value
"('S-1-5-21-1-2-3-1001','PROV01.mayyhem.com',['SMS Provider@PS1'], ?), "
# site server: SMS Site Server@PS1, NTLM unknown
"('S-1-5-21-1-2-3-1002','SS01.mayyhem.com',['SMS Site Server@PS1'], NULL), "
# a secondary-site system, must be ignored
"('S-1-5-21-1-2-3-1003','SEC01.mayyhem.com',['SMS Site Server@SEC'], NULL)"
") AS t(sid, dnshostname, site_system_roles, restrict_receiving_ntlm_traffic)",
[provider_ntlm],
)
_graph_edges_init(con, "sccm")
def test_adminservice_relay_default_emits_with_null_ntlm():
con = duckdb.connect()
_seed(con, None) # provider NTLM unknown -> assume vulnerable (default)
_edge_coerce_relay_adminservice(con, "sccm", disable_possible=False)
rows = con.execute(
"SELECT start_id, end_id, kind, collection_source, "
"coercion_victim_and_relay_target_pairs FROM sccm.graph_edges"
).fetchall()
assert len(rows) == 1
start, end, kind, csrc, pairs = rows[0]
assert start == "MAYYHEM.COM-S-1-5-11"
assert end == "PS1" # non-secondary site code (raw case)
assert kind == "CoerceAndRelayToAdminService"
assert csrc == ["Post-processing"]
assert pairs == ["Coerce SS01.mayyhem.com, relay to PROV01.mayyhem.com"]
def test_adminservice_relay_flag_drops_null_ntlm():
con = duckdb.connect()
_seed(con, None)
_edge_coerce_relay_adminservice(con, "sccm", disable_possible=True)
assert con.execute("SELECT count(*) FROM sccm.graph_edges").fetchone()[0] == 0
def test_adminservice_relay_flag_keeps_confirmed_off():
con = duckdb.connect()
_seed(con, "Off") # explicitly confirmed not-restricting NTLM
_edge_coerce_relay_adminservice(con, "sccm", disable_possible=True)
assert con.execute("SELECT count(*) FROM sccm.graph_edges").fetchone()[0] == 1- Step 2: Run the test to verify it fails
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/edge_coerce_relay_adminservice_test.py -v
Expected: FAIL — ImportError: cannot import name '_edge_coerce_relay_adminservice'.
- Step 3: Add the
_authed_users_idhelper. Insrc/openhound_sccm/transforms.py, next to the existing_arrhelper, add:
def _authed_users_id(dnshostname_col: str) -> str:
"""SQL fragment: the SharpHound-form Authenticated Users node id for the domain of a
computer, derived from its dnshostname column. FQDN = dnshostname with the first
(host) label stripped, uppercased (e.g. 'PROV01.mayyhem.com' -> 'MAYYHEM.COM-S-1-5-11').
Always pair with a `<col> LIKE '%.%'` guard so a bare hostname can't yield a bad id."""
return f"upper(regexp_replace({dnshostname_col}, '^[^.]+\\.', '')) || '-S-1-5-11'"- Step 4: Add the builder. In
src/openhound_sccm/transforms.py, add (place it after_edge_mssql_db_assign_all, with the other edge builders):
def _edge_coerce_relay_adminservice(
con: duckdb.DuckDBPyConnection, schema: str, disable_possible: bool
) -> None:
"""CoerceAndRelayToAdminService: Authenticated Users -> SCCM_Site (CMBP ps1:6572-6624).
For each non-secondary site, every SMS Provider relay target is paired with every Site
Server coercion victim (provider != site server). The relay coerces the site server and
relays its NTLM to the provider's AdminService; the edge end is the site code (the
SCCM_Site node id). Start = the Authenticated Users node of the SITE SERVER's domain
(CMBP keys it off the coerced victim, ps1:6606).
NTLM gate is on the PROVIDER (the relay target must accept NTLM): default treats
null-or-'Off' as vulnerable; with --disable-possible-edges only explicit 'Off' qualifies
(Stage 6 decision #1). collectionSource is the static ['Post-processing'] CMBP passes at
ps1:1955. _safe() skips+logs if site_hierarchy / node_computer is missing."""
from .kinds.edges import COERCE_AND_RELAY_TO_ADMIN_SERVICE
# Default: null/Off => vulnerable. Flag: only explicit 'Off'.
ntlm_ok = ("upper(coalesce(c.restrict_receiving_ntlm_traffic, 'OFF')) = 'OFF'"
if not disable_possible
else "upper(c.restrict_receiving_ntlm_traffic) = 'OFF'")
_safe(
con, "edge_coerce_relay_adminservice",
f"INSERT INTO {schema}.graph_edges BY NAME "
f"WITH nonsec AS ("
f" SELECT site_code, upper(site_code) AS u FROM {schema}.site_hierarchy "
f" WHERE coalesce(site_type, 0) != 1 AND site_code IS NOT NULL"
f"), "
f"providers AS ("
f" SELECT c.sid, c.dnshostname, upper(regexp_extract(role, '@(.+)$', 1)) AS site "
f" FROM {schema}.node_computer c, UNNEST(c.site_system_roles) AS t(role) "
f" WHERE role LIKE 'SMS Provider@%' AND c.sid IS NOT NULL AND ({ntlm_ok})"
f"), "
f"servers AS ("
f" SELECT c.sid, c.dnshostname, upper(regexp_extract(role, '@(.+)$', 1)) AS site "
f" FROM {schema}.node_computer c, UNNEST(c.site_system_roles) AS t(role) "
f" WHERE role LIKE 'SMS Site Server@%' AND c.sid IS NOT NULL AND c.dnshostname LIKE '%.%'"
f") "
f"SELECT DISTINCT {_authed_users_id('srv.dnshostname')} AS start_id, "
f" n.site_code AS end_id, "
f" '{COERCE_AND_RELAY_TO_ADMIN_SERVICE}' AS kind, "
f" ['Post-processing'] AS collection_source, "
f" ['Coerce ' || coalesce(srv.dnshostname, srv.sid) || ', relay to ' "
f" || coalesce(prov.dnshostname, prov.sid)] AS coercion_victim_and_relay_target_pairs, "
f" CAST(NULL AS VARCHAR[]) AS coercion_victim_hostnames "
f"FROM providers prov "
f"JOIN servers srv ON srv.site = prov.site AND srv.sid != prov.sid "
f"JOIN nonsec n ON n.u = srv.site"
)- Step 5: Run the test to verify it passes
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/edge_coerce_relay_adminservice_test.py -v
Expected: 3 passed.
- Step 6: Green checkpoint (NO commit). Stop. Report PASS. (Wiring into
transforms()happens in Task H1.)
Files:
- Modify:
src/openhound_sccm/transforms.py(add_edge_coerce_relay_mssql) - Test:
tests/edge_coerce_relay_mssql_test.py
Interfaces:
-
Consumes:
edges.COERCE_AND_RELAY_TO_MSSQL;node_mssql_login(login_id,server_id,host_sid,sysadmin_computer_sid);node_mssql_server(server_id,dns_host_name,name,port,extended_protection,collection_source);node_computer(sid,dnshostname,restrict_receiving_ntlm_traffic); the_authed_users_idhelper (Task E1). -
Produces:
_edge_coerce_relay_mssql(con, schema, disable_possible)INSERTsCoerceAndRelayToMSSQLrows (start = Authenticated Users of the sysadmin computer's domain, end =login_id) withcoercion_victim_and_relay_target_pairsand an EPA-sourcecollection_source. -
Step 1: Write the failing test — create
tests/edge_coerce_relay_mssql_test.py:
import duckdb
from openhound_sccm.transforms import _graph_edges_init, _edge_coerce_relay_mssql
def _seed(con, host_ntlm, epa):
con.execute("CREATE SCHEMA IF NOT EXISTS sccm")
# SQL host computer (the site DB server) + the sysadmin victim computer.
con.execute(
"CREATE TABLE sccm.node_computer AS SELECT * FROM (VALUES "
"('S-1-5-21-1-2-3-5001','SQL01.mayyhem.com', ?), " # host (site DB)
"('S-1-5-21-1-2-3-1002','SS01.mayyhem.com', NULL)" # sysadmin victim
") AS t(sid, dnshostname, restrict_receiving_ntlm_traffic)",
[host_ntlm],
)
con.execute(
"CREATE TABLE sccm.node_mssql_server AS SELECT "
"'S-1-5-21-1-2-3-5001:1433' AS server_id, 'SQL01.mayyhem.com' AS dns_host_name, "
"'SQL01.mayyhem.com' AS name, '1433' AS port, ? AS extended_protection, "
"['MSSQL-ScanForEPA','SCCM_Add-MSSQLServerNodesAndEdges'] AS collection_source",
[epa],
)
con.execute(
"CREATE TABLE sccm.node_mssql_login AS SELECT "
"'MAYYHEM\\SS01$@S-1-5-21-1-2-3-5001:1433' AS login_id, "
"'S-1-5-21-1-2-3-5001:1433' AS server_id, 'S-1-5-21-1-2-3-5001' AS host_sid, "
"'S-1-5-21-1-2-3-1002' AS sysadmin_computer_sid"
)
_graph_edges_init(con, "sccm")
def test_mssql_relay_default_emits_when_epa_null():
con = duckdb.connect()
_seed(con, host_ntlm=None, epa=None) # both unknown -> assume vulnerable
_edge_coerce_relay_mssql(con, "sccm", disable_possible=False)
rows = con.execute(
"SELECT start_id, end_id, kind, collection_source, "
"coercion_victim_and_relay_target_pairs FROM sccm.graph_edges"
).fetchall()
assert len(rows) == 1
start, end, kind, csrc, pairs = rows[0]
assert start == "MAYYHEM.COM-S-1-5-11"
assert end == "MAYYHEM\\SS01$@S-1-5-21-1-2-3-5001:1433"
assert kind == "CoerceAndRelayToMSSQL"
assert csrc == ["MSSQL-ScanForEPA"] # EPA sources only
assert pairs == ["Coerce SS01.mayyhem.com, relay to SQL01.mayyhem.com:1433"]
def test_mssql_relay_skips_when_epa_enabled():
con = duckdb.connect()
_seed(con, host_ntlm="Off", epa="Required") # EPA on -> never a relay target
_edge_coerce_relay_mssql(con, "sccm", disable_possible=False)
assert con.execute("SELECT count(*) FROM sccm.graph_edges").fetchone()[0] == 0
def test_mssql_relay_flag_drops_assumed_epa():
con = duckdb.connect()
_seed(con, host_ntlm="Off", epa=None) # EPA unknown -> dropped under the flag
_edge_coerce_relay_mssql(con, "sccm", disable_possible=True)
assert con.execute("SELECT count(*) FROM sccm.graph_edges").fetchone()[0] == 0- Step 2: Run the test to verify it fails
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/edge_coerce_relay_mssql_test.py -v
Expected: FAIL — ImportError: cannot import name '_edge_coerce_relay_mssql'.
- Step 3: Add the builder. In
src/openhound_sccm/transforms.py, add after_edge_coerce_relay_adminservice:
def _edge_coerce_relay_mssql(
con: duckdb.DuckDBPyConnection, schema: str, disable_possible: bool
) -> None:
"""CoerceAndRelayToMSSQL: Authenticated Users -> MSSQL_Login (CMBP ps1:6626-6726).
Driven off node_mssql_login, which already encodes the (sysadmin computer, server)
pairing CMBP reconstructs by hand (and already excludes the SQL host as its own
sysadmin, so 'can't relay to self' is satisfied). For each login: coerce the sysadmin
computer and relay NTLM to the site DB server, authenticating as that login.
Two gates (Stage 6 decision #1): the SQL HOST computer's NTLM and the SERVER's Extended
Protection. Default treats null as vulnerable (null NTLM => assume Off; null EPA =>
assume Off). A known EPA other than 'Off' always disqualifies the server. With
--disable-possible-edges both must be EXPLICITLY 'Off'. collectionSource = the server's
EPA-determination sources only (CMBP ps1:6715). _safe() skips+logs missing tables."""
from .kinds.edges import COERCE_AND_RELAY_TO_MSSQL
if not disable_possible:
epa_ok = "(s.extended_protection IS NULL OR upper(s.extended_protection) = 'OFF')"
ntlm_ok = "upper(coalesce(h.restrict_receiving_ntlm_traffic, 'OFF')) = 'OFF'"
else:
epa_ok = "upper(s.extended_protection) = 'OFF'"
ntlm_ok = "upper(h.restrict_receiving_ntlm_traffic) = 'OFF'"
_safe(
con, "edge_coerce_relay_mssql",
f"INSERT INTO {schema}.graph_edges BY NAME "
f"SELECT DISTINCT {_authed_users_id('v.dnshostname')} AS start_id, "
f" l.login_id AS end_id, "
f" '{COERCE_AND_RELAY_TO_MSSQL}' AS kind, "
f" coalesce(list_filter(s.collection_source, "
f" x -> x IN ('MSSQL-ScanForEPA', 'RemoteRegistry-MSSQL')), CAST([] AS VARCHAR[])) "
f" AS collection_source, "
f" ['Coerce ' || coalesce(v.dnshostname, v.sid) || ', relay to ' "
f" || coalesce(s.dns_host_name, s.name) || ':' || coalesce(s.port, '1433')] "
f" AS coercion_victim_and_relay_target_pairs, "
f" CAST(NULL AS VARCHAR[]) AS coercion_victim_hostnames "
f"FROM {schema}.node_mssql_login l "
f"JOIN {schema}.node_mssql_server s ON s.server_id = l.server_id "
f"JOIN {schema}.node_computer h ON h.sid = l.host_sid "
f"JOIN {schema}.node_computer v "
f" ON v.sid = l.sysadmin_computer_sid AND v.dnshostname LIKE '%.%' "
f"WHERE ({epa_ok}) AND ({ntlm_ok})"
)- Step 4: Run the test to verify it passes
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/edge_coerce_relay_mssql_test.py -v
Expected: 3 passed.
- Step 5: Green checkpoint (NO commit). Stop. Report PASS.
Files:
- Modify:
src/openhound_sccm/transforms.py(add_edge_coerce_relay_smb) - Test:
tests/edge_coerce_relay_smb_test.py
Interfaces:
-
Consumes:
edges.COERCE_AND_RELAY_TO_SMB;node_computer(sid,dnshostname,site_system_roles,smb_signing_required,restrict_receiving_ntlm_traffic,smb_signing_sourcefrom Task D1);site_hierarchy;_authed_users_id. -
Produces:
_edge_coerce_relay_smb(con, schema, disable_possible)INSERTsCoerceAndRelayToSMBrows (start = Authenticated Users of the site server's domain, end = the SMB-signing-disabled site-system Computer SID) withcoercion_victim_hostnamesand an SMB-sourcecollection_source. -
Step 1: Write the failing test — create
tests/edge_coerce_relay_smb_test.py:
import duckdb
from openhound_sccm.transforms import _graph_edges_init, _edge_coerce_relay_smb
def _seed(con, target_ntlm):
con.execute("CREATE SCHEMA IF NOT EXISTS sccm")
con.execute(
"CREATE TABLE sccm.site_hierarchy AS SELECT 'PS1' AS site_code, 2 AS site_type"
)
con.execute(
"CREATE TABLE sccm.node_computer AS SELECT * FROM (VALUES "
# vulnerable target: a DP@PS1 with signing not required + given NTLM
"('S-1-5-21-1-2-3-7001','DP01.mayyhem.com',['SMS Distribution Point@PS1'],"
" false, ['SMB-Negotiate'], ?), "
# coerced site server
"('S-1-5-21-1-2-3-1002','SS01.mayyhem.com',['SMS Site Server@PS1'],"
" true, [], NULL)"
") AS t(sid, dnshostname, site_system_roles, smb_signing_required, "
"smb_signing_source, restrict_receiving_ntlm_traffic)",
[target_ntlm],
)
_graph_edges_init(con, "sccm")
def test_smb_relay_default_emits():
con = duckdb.connect()
_seed(con, target_ntlm=None) # NTLM unknown -> assume vulnerable
_edge_coerce_relay_smb(con, "sccm", disable_possible=False)
rows = con.execute(
"SELECT start_id, end_id, kind, collection_source, coercion_victim_hostnames "
"FROM sccm.graph_edges"
).fetchall()
assert len(rows) == 1
start, end, kind, csrc, victims = rows[0]
assert start == "MAYYHEM.COM-S-1-5-11"
assert end == "S-1-5-21-1-2-3-7001" # the vulnerable site system
assert kind == "CoerceAndRelayToSMB"
assert csrc == ["SMB-Negotiate"]
assert victims == ["SS01.mayyhem.com"] # the coerced site server
def test_smb_relay_flag_drops_null_ntlm():
con = duckdb.connect()
_seed(con, target_ntlm=None)
_edge_coerce_relay_smb(con, "sccm", disable_possible=True)
assert con.execute("SELECT count(*) FROM sccm.graph_edges").fetchone()[0] == 0
def test_smb_relay_flag_keeps_confirmed():
con = duckdb.connect()
_seed(con, target_ntlm="Off") # confirmed NTLM not restricted + signing off
_edge_coerce_relay_smb(con, "sccm", disable_possible=True)
assert con.execute("SELECT count(*) FROM sccm.graph_edges").fetchone()[0] == 1- Step 2: Run the test to verify it fails
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/edge_coerce_relay_smb_test.py -v
Expected: FAIL — ImportError: cannot import name '_edge_coerce_relay_smb'.
- Step 3: Add the builder. In
src/openhound_sccm/transforms.py, add after_edge_coerce_relay_mssql:
def _edge_coerce_relay_smb(
con: duckdb.DuckDBPyConnection, schema: str, disable_possible: bool
) -> None:
"""CoerceAndRelayToSMB: Authenticated Users -> Computer (CMBP ps1:6728-6781).
The edge END is a site system whose SMB signing is NOT required (the relay target); the
coerced victim is a Site Server in the same non-secondary site (system != server). Start
= the Authenticated Users node of the SITE SERVER's domain (CMBP ps1:6763).
Gates (Stage 6 decision #1): the TARGET's smb_signing_required is false (always explicit
in CMBP) AND its NTLM is null-or-'Off' (default) / explicitly 'Off' (flag).
collectionSource = the target's smb_signing_source filtered to the SMB-signing probes
(CMBP ps1:6773). coercionVictimHostnames = the coerced site server's dnshostname.
_safe() skips+logs missing tables."""
from .kinds.edges import COERCE_AND_RELAY_TO_SMB
ntlm_ok = ("upper(coalesce(c.restrict_receiving_ntlm_traffic, 'OFF')) = 'OFF'"
if not disable_possible
else "upper(c.restrict_receiving_ntlm_traffic) = 'OFF'")
_safe(
con, "edge_coerce_relay_smb",
f"INSERT INTO {schema}.graph_edges BY NAME "
f"WITH nonsec AS ("
f" SELECT upper(site_code) AS u FROM {schema}.site_hierarchy "
f" WHERE coalesce(site_type, 0) != 1 AND site_code IS NOT NULL"
f"), "
f"targets AS ("
f" SELECT DISTINCT c.sid, c.smb_signing_source, "
f" upper(regexp_extract(role, '@(.+)$', 1)) AS site "
f" FROM {schema}.node_computer c, UNNEST(c.site_system_roles) AS t(role) "
f" WHERE role LIKE '%@%' AND c.sid IS NOT NULL "
f" AND c.smb_signing_required = false AND ({ntlm_ok})"
f"), "
f"servers AS ("
f" SELECT DISTINCT c.sid, c.dnshostname, upper(regexp_extract(role, '@(.+)$', 1)) AS site "
f" FROM {schema}.node_computer c, UNNEST(c.site_system_roles) AS t(role) "
f" WHERE role LIKE 'SMS Site Server@%' AND c.sid IS NOT NULL AND c.dnshostname LIKE '%.%'"
f") "
f"SELECT DISTINCT {_authed_users_id('srv.dnshostname')} AS start_id, "
f" tgt.sid AS end_id, "
f" '{COERCE_AND_RELAY_TO_SMB}' AS kind, "
f" coalesce(list_filter(tgt.smb_signing_source, "
f" x -> x IN ('SMB-Negotiate', 'RemoteRegistry-SMBSigningCheck')), CAST([] AS VARCHAR[])) "
f" AS collection_source, "
f" CAST(NULL AS VARCHAR[]) AS coercion_victim_and_relay_target_pairs, "
f" [srv.dnshostname] AS coercion_victim_hostnames "
f"FROM targets tgt "
f"JOIN servers srv ON srv.site = tgt.site AND srv.sid != tgt.sid "
f"JOIN nonsec n ON n.u = tgt.site"
)- Step 4: Run the test to verify it passes
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/edge_coerce_relay_smb_test.py -v
Expected: 3 passed.
- Step 5: Green checkpoint (NO commit). Stop. Report PASS.
Files:
- Modify:
src/openhound_sccm/transforms.py(add_node_authenticated_users; edittransforms()) - Test:
tests/node_authenticated_users_test.py
Interfaces:
-
Consumes:
graph_edgesrelay rows (Phase E/F/G);node_computer(sid,dnshostname);node_group(the final coalesce schema:sid,name,sccm_infra,sccm_resource_ids,fallback_domain_sid);_read_disable_possible(existing, called once intransforms()). -
Produces:
_node_authenticated_users(con, schema)INSERTs onenode_grouprow per relay-start domain (sid = UPPER(FQDN)-S-1-5-11,name = AUTHENTICATED USERS@<FQDN-UPPER>,sccm_infra=false,sccm_resource_ids=[],fallback_domain_sid = <domain SID>);transforms()calls the three relay builders +_node_authenticated_usersafter_edge_mssql_db_assign_alland before_graph_edges_dedup. -
Step 1: Write the failing test — create
tests/node_authenticated_users_test.py:
import duckdb
from openhound_sccm.transforms import (
_graph_edges_init, _edge_coerce_relay_adminservice, _node_authenticated_users,
)
from openhound_sccm.models.group import GroupNode
def _seed(con):
con.execute("CREATE SCHEMA IF NOT EXISTS sccm")
con.execute("CREATE TABLE sccm.site_hierarchy AS SELECT 'PS1' AS site_code, 2 AS site_type")
con.execute(
"CREATE TABLE sccm.node_computer AS SELECT * FROM (VALUES "
"('S-1-5-21-1-2-3-1001','PROV01.mayyhem.com',['SMS Provider@PS1'], NULL), "
"('S-1-5-21-1-2-3-1002','SS01.mayyhem.com',['SMS Site Server@PS1'], NULL)"
") AS t(sid, dnshostname, site_system_roles, restrict_receiving_ntlm_traffic)"
)
# node_group with the final coalesce columns (empty to start).
con.execute(
"CREATE TABLE sccm.node_group (sid VARCHAR, name VARCHAR, sccm_infra BOOLEAN, "
"sccm_resource_ids VARCHAR[], fallback_domain_sid VARCHAR)"
)
_graph_edges_init(con, "sccm")
_edge_coerce_relay_adminservice(con, "sccm", disable_possible=False)
def test_authenticated_users_node_built_for_relay_domain():
con = duckdb.connect()
_seed(con)
_node_authenticated_users(con, "sccm")
row = con.execute(
"SELECT sid, name, sccm_infra, sccm_resource_ids, fallback_domain_sid "
"FROM sccm.node_group WHERE sid = 'MAYYHEM.COM-S-1-5-11'"
).fetchone()
assert row is not None
sid, name, infra, rids, fallback = row
assert name == "AUTHENTICATED USERS@MAYYHEM.COM"
assert infra is False
assert rids == []
assert fallback == "S-1-5-21-1-2-3" # domain SID stripped from a domain computer SID
def test_group_model_emits_authusers_with_domain_environmentid():
con = duckdb.connect()
_seed(con)
_node_authenticated_users(con, "sccm")
r = con.execute(
"SELECT sid, name, sccm_infra, sccm_resource_ids, fallback_domain_sid "
"FROM sccm.node_group WHERE sid = 'MAYYHEM.COM-S-1-5-11'"
).fetchone()
node = GroupNode(
sid=r[0], name=r[1], sccm_infra=r[2], sccm_resource_ids=r[3], fallback_domain_sid=r[4]
).as_node
assert node is not None
assert node.id == "MAYYHEM.COM-S-1-5-11"
assert node.properties.environmentid == "S-1-5-21-1-2-3"
assert "Group" in node.kinds and "Base" in node.kinds- Step 2: Run the test to verify it fails
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/node_authenticated_users_test.py -v
Expected: FAIL — ImportError: cannot import name '_node_authenticated_users'.
- Step 3: Add the node builder. In
src/openhound_sccm/transforms.py, add after_edge_coerce_relay_smb:
def _node_authenticated_users(con: duckdb.DuckDBPyConnection, schema: str) -> None:
"""Synthesise the Authenticated Users group node for every domain a Stage 6 relay edge
starts from (CMBP Upsert-Node ps1:6609/6708/6766).
id = UPPER(<FQDN>)-S-1-5-11 (SharpHound well-known-SID form, so it merges with
SharpHound-collected AD data); name = 'AUTHENTICATED USERS@<FQDN-UPPER>'. environmentid
is resolved by GroupNode from fallback_domain_sid — here the AD-domain SID of any
domain-joined computer in that FQDN (S-1-5-11 has no domain part of its own).
Lazy: only domains that actually produced a relay edge get a node — matching CMBP, where
the node is upserted inside the relay loop. Inserted into node_group BEFORE
_node_backfill / _graph_edges_split so the AD id set and GroupNode pick it up. The
DISTINCT join to _domain_to_sid guarantees the edge's start id has a resolvable domain
SID; the same domain computer that seeded the relay's start id seeds this map, so every
relay-start id resolves."""
from .kinds.edges import (
COERCE_AND_RELAY_TO_ADMIN_SERVICE,
COERCE_AND_RELAY_TO_MSSQL,
COERCE_AND_RELAY_TO_SMB,
)
# UPPER(FQDN) -> AD-domain SID, from every domain-joined computer.
con.execute(
f"CREATE OR REPLACE TEMP TABLE _domain_to_sid AS "
f"SELECT DISTINCT upper(regexp_replace(dnshostname, '^[^.]+\\.', '')) AS fqdn_upper, "
f" regexp_extract(upper(sid), '^(S-1-5-21(?:-\\d+){{3}})-\\d+$', 1) AS domain_sid "
f"FROM {schema}.node_computer "
f"WHERE dnshostname LIKE '%.%' AND sid IS NOT NULL "
f" AND regexp_extract(upper(sid), '^(S-1-5-21(?:-\\d+){{3}})-\\d+$', 1) != ''"
)
relay_kinds = (f"('{COERCE_AND_RELAY_TO_ADMIN_SERVICE}', "
f"'{COERCE_AND_RELAY_TO_MSSQL}', '{COERCE_AND_RELAY_TO_SMB}')")
_safe(
con, "node_group<-authenticated_users",
f"INSERT INTO {schema}.node_group BY NAME "
f"SELECT DISTINCT ge.start_id AS sid, "
f" 'AUTHENTICATED USERS@' || replace(ge.start_id, '-S-1-5-11', '') AS name, "
f" false AS sccm_infra, "
f" CAST([] AS VARCHAR[]) AS sccm_resource_ids, "
f" d.domain_sid AS fallback_domain_sid "
f"FROM {schema}.graph_edges ge "
f"JOIN _domain_to_sid d ON d.fqdn_upper = replace(ge.start_id, '-S-1-5-11', '') "
f"WHERE ge.kind IN {relay_kinds} AND ge.start_id LIKE '%-S-1-5-11'"
)
n = con.execute(
f"SELECT count(*) FROM {schema}.node_group WHERE sid LIKE '%-S-1-5-11'"
).fetchone()[0]
logger.info("node_authenticated_users: %d AUTHENTICATED USERS node(s) in schema %r", n, schema)- Step 4: Wire all four builders into
transforms(). Insrc/openhound_sccm/transforms.py, intransforms(), immediately after the_edge_mssql_db_assign_all(con, schema)line and before_graph_edges_dedup(con, schema), insert:
# Stage 6: coerce-and-relay possible edges + the synthetic Authenticated Users node.
# disable_possible was read above (for _node_client_device_possible). The relay builders
# gate the "assume vulnerable on null" cases on it (surgical, Stage 6 decision #1).
# _node_authenticated_users runs AFTER the relay builders (it reads their start ids) and
# BEFORE dedup/backfill/split (which read node_group for the AD id set).
_edge_coerce_relay_adminservice(con, schema, disable_possible)
_edge_coerce_relay_mssql(con, schema, disable_possible)
_edge_coerce_relay_smb(con, schema, disable_possible)
_node_authenticated_users(con, schema)- Step 5: Run the test to verify it passes
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests/node_authenticated_users_test.py -v
Expected: 2 passed.
- Step 6: Run the FULL suite to confirm the wired pipeline is green end-to-end
Run: UV_PROJECT_ENVIRONMENT=C:/Users/domainadmin/AppData/Local/Temp/openhound-venv uv run --project C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm pytest C:/Users/domainadmin/Desktop/OpenHound/sccm/sccm/tests -q
Expected: all pass (Stage-5 baseline count + the new Stage-6 tests; 0 failed). If a prior whole-pipeline integration test asserts an exact edge/node count, update it to include the relay edges + AuthUsers node and note the change in the task report.
- Step 7: Green checkpoint (NO commit). Stop. Report PASS + the full-suite pass/fail/skip counts.
Files:
- Modify:
README.md - Modify:
ARCHITECTURE.md - (No test — verified by the code-truth review in Step 4.)
Interfaces: none (docs only).
- Step 1: README — Edge Reference. Add the three relay edges to the SCCM README Edge Reference table, matching the existing column layout. Use these code-true facts:
| Edge | Start → End | Traversable | collectionSource |
Possible? |
|---|---|---|---|---|
CoerceAndRelayToAdminService |
Authenticated Users → SCCM_Site | Yes | Post-processing |
Yes — gated by --disable-possible-edges (NTLM) |
CoerceAndRelayToMSSQL |
Authenticated Users → MSSQL_Login | Yes | MSSQL-ScanForEPA / RemoteRegistry-MSSQL |
Yes — gated (NTLM + Extended Protection) |
CoerceAndRelayToSMB |
Authenticated Users → Computer | Yes | SMB-Negotiate / RemoteRegistry-SMBSigningCheck |
Yes — gated (NTLM + SMB signing) |
Note the relay-only properties coercionVictimAndRelayTargetPairs (AdminService + MSSQL) and coercionVictimHostnames (SMB) and what they show the operator.
- Step 2: README — Node Reference +
--disable-possible-edgessemantics. Add a note under theGroupnode entry that a synthetic Authenticated Users group (UPPER(<FQDN>)-S-1-5-11, e.g.MAYYHEM.COM-S-1-5-11) is emitted as the start of every relay edge so it merges with SharpHound's well-known-SID node. In the--disable-possible-edgesdocumentation, add the surgical Stage 6 behavior:
With `--disable-possible-edges`, the three coerce-and-relay edges drop their
*assumed-vulnerable* cases and keep only *confirmed* ones: a relay survives only
when NTLM restriction is explicitly `Off` (not merely uncollected), the MSSQL
relay's Extended Protection is explicitly `Off`, and SMB signing is explicitly
not required. Without the flag (default), an uncollected NTLM/EPA setting is
assumed vulnerable, matching ConfigManBearPig.Add a copy-pasteable mayyhem.com example showing both runs:
# default — speculative relays included
openhound preprocess sccm <raw> <raw>/lookup.duckdb
openhound convert sccm <raw>/sccm <graph> --lookup-file <raw>/lookup.duckdb
# high-confidence — only confirmed relay paths
openhound preprocess sccm <raw> <raw>/lookup.duckdb --disable-possible-edges
openhound convert sccm <raw>/sccm <graph-confirmed> --lookup-file <raw>/lookup.duckdb-
Step 3: ARCHITECTURE.md. In the preproc/convert divergence section: (a) update the §11c
graph_edgescolumn list to includecoercion_victim_and_relay_target_pairs VARCHAR[]+coercion_victim_hostnames VARCHAR[]and noteGraphEdgeemits the relay-onlySCCMRelayEdgePropertiessubclass for the threeCoerceAndRelay*kinds; (b) notenode_computer.smb_signing_source VARCHAR[](SMB-signing probe provenance, consumed by the SMB relay); (c) add a Stage 6 changelog entry summarizing the surgical--disable-possible-edgessemantics, the lazy Authenticated Users node, and theCoerceAndRelayToSMBtraversable bug fix. Fix anyfile:linereferences this change invalidates. -
Step 4: Code-truth review. Re-read the emitted edge kinds (
kinds/edges.py),SCCMRelayEdgeProperties(graph.py), and the three builders. Confirm every README/ARCHITECTURE claim matches the code exactly (no edge/property the code doesn't emit; correct traversable flags; correctcollectionSourcetags). Per [[readme-code-truth-scope]], the README is code-truth for nodes/edges. Fix any mismatch. -
Step 5: Green checkpoint (NO commit). Stop. Report the doc sections changed + the code-truth check results.
Files:
- Create:
docs/superpowers/plans/2026-06-30-sccm-preproc-convert-stage6-validation.md
Interfaces: none (doc only).
-
Step 1: Write the harness doc as a code tour (spec §6 style; mirror
2026-06-29-sccm-preproc-convert-stage5-validation.md). It must contain:- A small in-process driver script: open the lab
lookup.duckdb, calltransforms(con), then querygraph_edges_adfor the threeCoerceAndRelay*kinds andnode_groupfor%-S-1-5-11. - Ordered breakpoints with exact
file:line(resolve against the post-Stage-6transforms.py):_edge_coerce_relay_adminservice(inspect the providers/servers CTE results),_edge_coerce_relay_mssql(inspect the EPA/NTLM gate),_edge_coerce_relay_smb(inspectsmb_signing_sourcefiltering),_node_authenticated_users(inspect_domain_to_sid+ the inserted rows),_graph_edges_dedup(the coercion array-union),_graph_edges_split(relay rows land ingraph_edges_ad). For each: what to inspect, expected debugger state, and which plan decision it verifies. - A black-box CLI smoke check against the lab raw tree (the Global-Constraints test loop), with the expected: three relay edges present with
traversable=true; each carries non-empty coercion context + a realcollectionSource; theAUTHENTICATED USERS@<FQDN-UPPER>group node present withenvironmentid= the lab domain SID; a second run with--disable-possible-edgesdrops the assumed-vulnerable relays and keeps only confirmed ones.
- A small in-process driver script: open the lab
-
Step 2: Run the live black-box smoke check against the existing lab raw tree (per the Global-Constraints loop). Record actual counts/ids observed (both default and
--disable-possible-edges). If the lab has no relay-eligible topology (no SMS Provider/Site Server NTLM-unrestricted, no EPA-off SQL, no signing-disabled site system), note that and validate the negative case (zero relays) plus the synthetic unit-test coverage. -
Step 3: Green checkpoint (NO commit). Stop. Report the harness path + the live-run observations.
1. Spec coverage (spec §6 Stage 6 + §3 inventory):
Process-CoerceAndRelayToAdminService→ Task E1. ✓Process-CoerceAndRelayToMSSQL→ Task F1. ✓Process-CoerceAndRelayToSMB→ Task G1. ✓- Synthetic Authenticated Users node (:6609/6708/6766) → Task H1. ✓
- EPA/SMB-signing/NTLM gating + possible-edge default +
--disable-possible-edges→ decision #1, implemented in E1/F1/G1; surgical semantics. ✓ - "EPA labeled per the uncertainty convention" → the MSSQL relay reads
extended_protectionas-collected (Off/Allowed/Required/Unknown) and onlyOff(or null-assumed) qualifies; the literal value is preserved on theMSSQL_Servernode (Stage 5), satisfying [[feedback_epa_uncertainty_label]]. ✓ - Validate block (relays appear with possible-edges on; vanish/tighten with the flag; EPA labels read correctly) → Task J1. ✓
2. Placeholder scan: no "TBD"/"add error handling"/"similar to Task N"/"write tests for the above" — every code + test step carries real content. ✓
3. Type/name consistency: COERCE_AND_RELAY_TO_* constants (A1) used identically in graph_edge.py (B1) and the builders (E1/F1/G1/H1); SCCMRelayEdgeProperties field names (coercionVictimAndRelayTargetPairs, coercionVictimHostnames) match between graph.py (B1) and graph_edge.py (B1); DuckDB columns (coercion_victim_and_relay_target_pairs, coercion_victim_hostnames, smb_signing_source) match across _graph_edges_init (B1), _graph_edges_dedup/_graph_edges_split (C1), _node_computer (D1), and the builders; _authed_users_id defined in E1, reused in F1/G1; node_mssql_login columns (login_id/server_id/host_sid/sysadmin_computer_sid) match Stage 5's _node_mssql_login. ✓
Risks to watch during execution:
- A whole-pipeline integration test asserting an exact edge/node count will need updating once relays + AuthUsers are wired (Task H1 Step 6).
regexp_replace(dnshostname, '^[^.]+\.', '')requires a dotted FQDN; theLIKE '%.%'guard is present in every builder + the_domain_to_sidmap, so a bare-hostname infra computer is skipped (drop+log via the empty result) rather than producing a malformed id. CMBP's$script:Domainglobal fallback is intentionally NOT ported (no global primary domain exists in the set-based world); note this divergence in ARCHITECTURE.md (Task I1).
Plan complete and saved to docs/superpowers/plans/2026-06-30-sccm-preproc-convert-stage6.md. Two execution options:
1. Subagent-Driven (recommended) — a fresh subagent per task (A1 → J1), two-stage review (spec-faithfulness + quality) between tasks, against the no-commit harness ([[sdd-no-commit-harness]]).
2. Inline Execution — execute the tasks in this session with checkpoints for review.