Skip to content
Merged
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
15 changes: 9 additions & 6 deletions docs/OPENGRAPH.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand Down
30 changes: 16 additions & 14 deletions modules/BloodhoundImporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
29 changes: 20 additions & 9 deletions tests/test_importer_opengraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand All @@ -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():
Expand Down