diff --git a/docs/assets/opengraph/opengraph-edge.json b/docs/assets/opengraph/opengraph-edge.json
deleted file mode 100644
index 0253fa1c..00000000
--- a/docs/assets/opengraph/opengraph-edge.json
+++ /dev/null
@@ -1,195 +0,0 @@
-{
- "title": "Generic Ingest Edge",
- "description": "Defines an edge between two nodes in a generic graph ingestion system. Each edge specifies a start and end node using one of three matching strategies: by unique identifier (match_by: id), by name (match_by: name, deprecated), or by one or more property matchers (match_by: property). A kind is required to indicate the relationship type. Optional properties may include custom attributes. You may optionally constrain the start or end node to a specific kind using the kind field inside each reference.",
- "type": "object",
- "$defs": {
- "property_map": {
- "type": ["object", "null"],
- "description": "A key-value map of edge attributes. Values must not be objects. If a value is an array, it must contain only primitive types (e.g., strings, numbers, booleans) and must be homogeneous (all items must be of the same type).",
- "additionalProperties": {
- "anyOf": [
- { "type": "string" },
- { "type": "number" },
- { "type": "boolean" },
- {
- "type": "array",
- "anyOf": [
- { "items": { "type": "string" } },
- { "items": { "type": "number" } },
- { "items": { "type": "boolean" } }
- ]
- }
- ]
- }
- },
- "endpoint": {
- "type": "object",
- "properties": {
- "match_by": {
- "type": "string",
- "enum": ["id", "name", "property"],
- "default": "id",
- "description": "Whether to match the start node by its unique object ID or by a series of property matches. Note that the name value here is deprecated and will be removed in future versions. Users are advised to use the multi-property match strategy moving forward."
- },
- "property_matchers": {
- "type": "array",
- "minItems": 1,
- "items": {
- "type": "object",
- "properties": {
- "key": {
- "type": "string"
- },
- "operator": {
- "type": "string",
- "enum": ["equals"]
- },
- "value": {
- "type": ["string", "number", "boolean"]
- }
- },
- "required": ["key", "operator", "value"]
- }
- },
- "value": {
- "type": "string",
- "description": "The value used for matching — either an object ID or a name, depending on match_by."
- },
- "kind": {
- "type": "string",
- "description": "Optional kind filter; the referenced node must have this kind."
- }
- },
- "if": {
- "allOf": [
- {
- "properties": {
- "match_by": {
- "type": "string",
- "const": "property"
- }
- }
- },
- {
- "not": {
- "properties": {
- "match_by": {
- "type": "null"
- }
- }
- }
- }
- ]
- },
- "then": {
- "required": ["property_matchers"],
- "not": {
- "required": ["value"]
- }
- },
- "else": {
- "required": ["value"],
- "not": {
- "required": ["property_matchers"]
- }
- }
- }
- },
- "properties": {
- "start": {
- "$ref": "#/$defs/endpoint"
- },
- "end": {
- "$ref": "#/$defs/endpoint"
- },
- "kind": {
- "type": "string",
- "description": "Edge kind name must contain only alphanumeric characters and underscores.",
- "pattern": "^[A-Za-z0-9_]+$"
- },
- "properties": {
- "$ref": "#/$defs/property_map"
- }
- },
- "required": ["start", "end", "kind"],
- "examples": [
- {
- "start": {
- "match_by": "id",
- "value": "user-1234"
- },
- "end": {
- "match_by": "id",
- "value": "server-5678"
- },
- "kind": "has_session",
- "properties": {
- "timestamp": "2025-04-16T12:00:00Z",
- "duration_minutes": 45
- }
- },
- {
- "start": {
- "match_by": "property",
- "property_matchers": [
- {
- "key": "prop_1",
- "operator": "equals",
- "value": "value"
- }
- ]
- },
- "end": {
- "match_by": "id",
- "value": "server-5678"
- },
- "kind": "has_session",
- "properties": {
- "timestamp": "2025-04-16T12:00:00Z",
- "duration_minutes": 45
- }
- },
- {
- "start": {
- "match_by": "name",
- "value": "alice",
- "kind": "User"
- },
- "end": {
- "match_by": "name",
- "value": "file-server-1",
- "kind": "Server"
- },
- "kind": "accessed_resource",
- "properties": {
- "via": "SMB",
- "sensitive": true
- }
- },
- {
- "start": {
- "value": "admin-1"
- },
- "end": {
- "value": "domain-controller-9"
- },
- "kind": "admin_to",
- "properties": {
- "reason": "elevated_permissions",
- "confirmed": false
- }
- },
- {
- "start": {
- "match_by": "name",
- "value": "Printer-007"
- },
- "end": {
- "match_by": "id",
- "value": "network-42"
- },
- "kind": "connected_to",
- "properties": null
- }
- ]
-}
diff --git a/docs/assets/opengraph/opengraph-node.json b/docs/assets/opengraph/opengraph-node.json
deleted file mode 100644
index 5b2a6381..00000000
--- a/docs/assets/opengraph/opengraph-node.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "title": "Generic Ingest Node",
- "description": "A node used in a generic graph ingestion system. Each node must have a unique identifier (`id`) and at least one kind describing its role or type. Nodes may also include a `properties` object containing custom attributes.",
- "type": "object",
- "$defs": {
- "property_map": {
- "type": ["object", "null"],
- "description": "A key-value map of entity attributes. Values must not be objects. If a value is an array, it must contain only primitive types (e.g., strings, numbers, booleans) and must be homogeneous (all items must be of the same type).",
- "additionalProperties": {
- "anyOf": [
- { "type": "string" },
- { "type": "number" },
- { "type": "boolean" },
- {
- "type": "array",
- "anyOf": [
- { "items": { "type": "string" } },
- { "items": { "type": "number" } },
- { "items": { "type": "boolean" } }
- ]
- }
- ]
- },
-
- "not": {
- "required": ["objectid"]
- }
- }
- },
- "properties": {
- "id": {
- "type": "string"
- },
- "properties": {
- "$ref": "#/$defs/property_map"
- },
- "kinds": {
- "type": ["array"],
- "items": { "type": "string" },
- "minItems": 0,
- "maxItems": 3,
- "description": "An array of kind labels for the node. The first element is treated as the node's primary kind and is used to determine which icon to display in the graph UI. This primary kind is only used for visual representation and has no semantic significance for data processing."
- }
- },
- "required": ["id", "kinds"],
- "examples": [
- {
- "id": "user-1234",
- "kinds": ["Person"]
- },
- {
- "id": "device-5678",
- "properties": {
- "manufacturer": "Brandon Corp",
- "model": "4000x",
- "is_active": true,
- "rating": 43.5
- },
- "kinds": ["Device", "Asset"]
- }
- ]
-}
diff --git a/docs/assets/opengraph/payload-schema.json b/docs/assets/opengraph/payload-schema.json
new file mode 100644
index 00000000..72a7d710
--- /dev/null
+++ b/docs/assets/opengraph/payload-schema.json
@@ -0,0 +1,3 @@
+{
+ "$ref": "https://raw.githubusercontent.com/SpecterOps/chow/refs/heads/main/pkg/validator/jsonschema/opengraph.json"
+}
diff --git a/docs/opengraph/developer/schema.mdx b/docs/opengraph/developer/schema.mdx
index af49bff7..875e2f30 100644
--- a/docs/opengraph/developer/schema.mdx
+++ b/docs/opengraph/developer/schema.mdx
@@ -4,6 +4,8 @@ sidebarTitle: Schema
description: "Description of the OpenGraph JSON Schema"
---
+import { GitHubJsonCodeblock } from "/snippets/opengraph/github-json-codeblock.jsx";
+
You can find the latest node and edge schemas in the BloodHound [source code](https://github.com/SpecterOps/BloodHound/tree/main/cmd/api/src/services/upload/jsonschema) on GitHub.
-
## Ingesting OpenGraph Data
### File Requirements
@@ -54,6 +54,27 @@ When ingest completes, you can [search](/analyze-data/explore/search) OpenGraph
**Entity Panels**: Clicking on an OpenGraph node or edge will currently display the full list of the entity's properties and their values. Future enhancements will enable OpenGraph extension authors to define structured and organized entity panel representations.
+### Data Payload Validation
+
+Before structuring your payload, you can optionally set up JSON schema validation in your editor to catch errors before ingestion. Add the following to the top level of your JSON document:
+
+```json
+{
+ "$schema": "https://bloodhound.specterops.io/assets/opengraph/payload-schema.json"
+}
+```
+
+Most editors will ask you to allow-list external domains for JSON schemas. You'll need to add the following URIs to your editor's configuration:
+
+```
+https://bloodhound.specterops.io/assets/opengraph/payload-schema.json
+https://raw.githubusercontent.com/SpecterOps/chow/refs/heads/main/pkg/validator/jsonschema/
+```
+
+
+ Source: {fileName} +
+ )} + {renderCode('json', fileName, codeText)} + > + ); +}; + +export default GitHubJsonCodeblock;