diff --git a/docs/OPENGRAPH.md b/docs/OPENGRAPH.md index 1b65044..926395c 100644 --- a/docs/OPENGRAPH.md +++ b/docs/OPENGRAPH.md @@ -29,8 +29,9 @@ data. Mixed imports such as `SharpHound.zip MSSQLHound.zip` are treated as a BloodHound data import because they can establish or refresh the AD baseline. `metadata` may be omitted. If present, it must be a JSON object. When -`metadata.source_kind` is present, it must be a non-empty string matching the -same safe identifier rule as labels. +`metadata.source_kind` is present, it must be a string. Empty or whitespace-only +values are treated as omitted; non-empty values must match the same safe +identifier rule as labels. If BloodHound CE analysis takes longer than the default 600 seconds, set `ADPF_BH_INGESTION_TIMEOUT_SECONDS` to a positive integer number of seconds. @@ -140,8 +141,9 @@ Property endpoint example: ## Stubs Endpoint stubs use `metadata.source_kind` as their label when it is present. If -`source_kind` is missing, the importer uses `OpenGraph_Stub`. Invalid or empty -`source_kind` values are rejected before any OpenGraph writes are attempted. +`source_kind` is missing, empty, or whitespace-only, the importer uses +`OpenGraph_Stub`. Invalid non-empty `source_kind` values are rejected before any +OpenGraph writes are attempted. Stubs are created only for `match_by: "id"` endpoints that do not supply an endpoint `kind`. A typed endpoint that does not resolve is left unmatched rather @@ -300,8 +302,9 @@ semantics. The upstream docs are: Intentional ADPathfinder differences: -- `metadata.source_kind` may be omitted. If present, it must be a non-empty safe - identifier because ADPathfinder uses it as the fallback endpoint-stub label. +- `metadata.source_kind` may be omitted. Empty or whitespace-only values are + treated as omitted. Non-empty values must be safe identifiers because + ADPathfinder uses them as fallback endpoint-stub labels. - ADPathfinder does not append `source_kind` to every imported node. - Missing or empty `node.kinds` falls back to `Base`; generic Base-only nodes are merged as `OpenGraph_Stub`. diff --git a/modules/BloodhoundImporter.py b/modules/BloodhoundImporter.py index 9420f48..e3eabce 100644 --- a/modules/BloodhoundImporter.py +++ b/modules/BloodhoundImporter.py @@ -526,17 +526,7 @@ def _validate_opengraph_payload( metadata = data.get('metadata', {}) if not isinstance(metadata, dict): raise ValueError(f"OpenGraph {source_name}: metadata must be an object") - source_kind = metadata.get('source_kind', '') - if 'source_kind' in metadata and not isinstance(source_kind, str): - raise ValueError( - f"OpenGraph {source_name}: metadata.source_kind must be a string" - ) - if 'source_kind' in metadata: - source_kind = self._safe_opengraph_identifier( - source_kind, - 'source_kind', - f"OpenGraph {source_name}: metadata.source_kind", - ) + source_kind = self._validated_source_kind(metadata, source_name) if merge_mode == 'opengraph' and source_kind in reserved_labels(): raise ValueError( @@ -604,6 +594,20 @@ def _validate_opengraph_payload( "properties must be an object" ) + def _validated_source_kind(self, metadata: dict, source_name: str) -> str: + source_kind = metadata.get('source_kind', '') + if 'source_kind' in metadata and not isinstance(source_kind, str): + raise ValueError( + f"OpenGraph {source_name}: metadata.source_kind must be a string" + ) + if not source_kind or not source_kind.strip(): + return '' + return self._safe_opengraph_identifier( + source_kind, + 'source_kind', + f"OpenGraph {source_name}: metadata.source_kind", + ) + def _validate_opengraph_endpoint( self, source_name: str, @@ -688,9 +692,7 @@ def _stub_label_for_source_kind(self, data: dict) -> str: source_kind = metadata.get('source_kind', '') if 'source_kind' in metadata and not isinstance(source_kind, str): raise ValueError("OpenGraph metadata.source_kind must be a string") - if 'source_kind' in metadata and not source_kind: - raise ValueError("OpenGraph metadata.source_kind must be a non-empty string") - if source_kind: + if source_kind and source_kind.strip(): label = safe_cypher_identifier(source_kind, 'source_kind') if label in reserved_labels(): raise ValueError( diff --git a/tests/test_importer_opengraph.py b/tests/test_importer_opengraph.py index 1a09a08..1a5d4da 100644 --- a/tests/test_importer_opengraph.py +++ b/tests/test_importer_opengraph.py @@ -1154,11 +1154,12 @@ def test_opengraph_rejects_present_non_string_source_kind_before_query(source_ki assert conn.queries == [] -def test_opengraph_rejects_empty_source_kind_before_query(): - conn = RecordingConnection() +@pytest.mark.parametrize("source_kind", ["", " "]) +def test_opengraph_treats_blank_source_kind_as_omitted(source_kind): + conn = RecordingConnection(responses=[[{"c": 0}]]) importer = BloodhoundImporter(conn) data = { - "metadata": {"source_kind": ""}, + "metadata": {"source_kind": source_kind}, "graph": { "nodes": [ { @@ -1167,16 +1168,26 @@ def test_opengraph_rejects_empty_source_kind_before_query(): "properties": {}, } ], - "edges": [], + "edges": [ + { + "kind": "Jenkins_AdminTo", + "start": {"value": "jenkins-1"}, + "end": {"value": "external-1"}, + "properties": {}, + } + ], }, } - with pytest.raises(ValueError, match="metadata.source_kind"): - importer._import_opengraph_files_directly( - [("jenkins.json", data, "opengraph")] - ) + stub_labels = importer._import_opengraph_files_directly( + [("jenkins.json", data, "opengraph")] + ) - assert conn.queries == [] + assert stub_labels == {"OpenGraph_Stub"} + assert any( + "MERGE (n:`OpenGraph_Stub` {objectid: $objectid})" in q + for q, _ in conn.queries + ) def test_upload_validates_ad_companion_properties_before_filter():