SpecterOps OpenGraph (Experimental) API
The OpenGraph (Experimental) API from SpecterOps — 5 operation(s) for opengraph (experimental).
The OpenGraph (Experimental) API from SpecterOps — 5 operation(s) for opengraph (experimental).
openapi: 3.0.3
info:
title: BloodHound AD Base Entities OpenGraph (Experimental) API
contact:
name: BloodHound Enterprise Support
url: https://bloodhound.specterops.io/
email: support@specterops.io
license:
name: Apache-2.0
url: https://www.apache.org/licenses/LICENSE-2.0
version: v2
description: "This is the API that drives BloodHound Enterprise and Community Edition.\nEndpoint availability is denoted using the `Community` and `Enterprise` tags.\n\nContact information listed is for BloodHound Enterprise customers. To get help with\nBloodHound Community Edition, please join our\n[Slack community](https://ghst.ly/BHSlack/).\n\n## Authentication\n\nThe BloodHound API supports two kinds of authentication: JWT bearer tokens and Signed Requests.\nFor quick tests or one-time calls, the JWT used by your browser may be the simplest route. For\nmore secure and long lived API integrations, the recommended option is signed requests.\n\n### JWT Bearer Token\n\nThe API will accept calls using the following header structure in the HTTP request:\n```\nAuthorization: Bearer $JWT_TOKEN\n```\nIf you open the Network tab within your browser, you will see calls against the API made utilizing\nthis structure. JWT bearer tokens are supported by the BloodHound API, however it is recommended\nthey only be used for temporary access. JWT tokens expire after a set amount of time and require\nre-authentication using secret credentials.\n\n### Signed Requests\n\nSigned requests are the recommended form of authentication for the BloodHound API. Not only are\nsigned requests better for long lived integrations, they also provide more security for the\nrequests being sent. They provide authentication of the client, as well as verification of request\nintegrity when received by the server.\n\nSigned requests consist of three main parts: The client token ID, the request timestamp, and a\nbase64 encoded HMAC signature. These three pieces of information are sent with the request using\nthe following header structure:\n\n```\nAuthorization: bhesignature $TOKEN_ID\nRequestDate: $RFC3339_DATETIME\nSignature: $BASE64ENCODED_HMAC_SIGNATURE\n```\n\nTo use signed requests, you will need to generate an API token. Each API token generated in the\nBloodHound API comes with two parts: The Token ID, which is used in the `Authorization` header,\nand the Token Key, which is used as part of the HMAC hashing process. The token ID should be\nconsidered as public (like a username) and the token key should be considered secret (like a\npassword). Once an API token is generated, you can use the key to sign requests.\n\nFor more documentation about how to work with authentication in the API, including examples\nof how to generate an API token in the BloodHound UI, please refer to this support doc:\n[Working with the BloodHound API](https://bloodhound.specterops.io/integrations/bloodhound-api/working-with-api).\n\n#### Signed Request Pseudo-code Example\n\nFirst, a digest is initiated with HMAC-SHA-256 using the token key as the digest key:\n```python\ndigester = hmac.new(sha256, api_token_key)\n```\n\nOperationKey is the first HMAC digest link in the signature chain. This prevents replay attacks that\nseek to modify the request method or URI. It is composed of concatenating the request method and\nthe request URI with no delimiter and computing the HMAC digest using the token key as the digest\nsecret:\n```python\n# Example: GET /api/v2/test/resource HTTP/1.1\n# Signature Component: GET/api/v2/test/resource\ndigester.write(request_method + request_uri)\n\n# Update the digester for further chaining\ndigester = hmac.New(sha256, digester.hash())\n```\n\nDateKey is the next HMAC digest link in the signature chain. This encodes the RFC3339\nformatted datetime value as part of the signature to the hour to prevent replay\nattacks that are older than max two hours. This value is added to the signature chain\nby cutting off all values from the RFC3339 formatted datetime from the hours value\nforward:\n```python\n# Example: 2020-12-01T23:59:60Z\n# Signature Component: 2020-12-01T23\nrequest_datetime = date.now()\ndigester.write(request_datetime[:13])\n\n# Update the digester for further chaining\ndigester = hmac.New(sha256, digester.hash())\n```\n\nBody signing is the last HMAC digest link in the signature chain. This encodes the\nrequest body as part of the signature to prevent replay attacks that seek to modify\nthe payload of a signed request. In the case where there is no body content the\nHMAC digest is computed anyway, simply with no values written to the digester:\n```python\nif request.body is not empty:\n digester.write(request.body)\n```\n\nFinally, base64 encode the final hash and write the three required headers before\nsending the request:\n```python\nencoded_hash = base64_encode(digester.hash())\nrequest.header.write('Authorization', 'bhesignature ' + token_id)\nrequest.header.write('RequestDate', request_datetime)\nrequest.header.write('Signature', encoded_hash)\n```\n"
servers:
- url: /
description: This is the base path for all endpoints, relative to the domain where the API is being hosted.
security:
- JWTBearerToken: []
- SignedRequest: []
RequestDate: []
HMACSignature: []
tags:
- name: OpenGraph (Experimental)
paths:
/api/v2/relationships/{relationship_id}:
parameters:
- name: relationship_id
in: path
required: true
description: The relationship id assigned by the graph
schema:
type: integer
format: int64
- name: include-info
in: query
required: false
description: 'When false or omitted, excludes entity panel information.
When true, returns relationship details with entity panel information included.
'
schema:
type: boolean
get:
operationId: GetRelationshipByID
summary: Get Relationship by Graph Relationship ID
description: '**Experimental** - Returns the details of a graph relationship identified by its graph-assigned integer ID'
tags:
- OpenGraph (Experimental)
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
anyOf:
- $ref: '#/components/schemas/model.relationship-details'
- $ref: '#/components/schemas/model.relationship-details-with-info'
'400':
$ref: '#/components/responses/bad-request'
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'404':
$ref: '#/components/responses/not-found'
'429':
$ref: '#/components/responses/too-many-requests'
'500':
$ref: '#/components/responses/internal-server-error'
/api/v2/nodes/{node_id}:
parameters:
- name: node_id
in: path
required: true
description: The node id assigned by the graph
schema:
type: integer
format: int64
get:
operationId: GetNodeByID
summary: Get Node by Graph Node ID
description: '**Experimental** - Returns the details of a graph node identified by its graph-assigned integer ID'
tags:
- OpenGraph (Experimental)
parameters:
- name: include-info
in: query
required: false
description: When true, populates `data.info` with kindinfo objects for the node's registered kinds.
schema:
type: boolean
default: false
responses:
'200':
description: 'OK
When `include-info=true`, the `info` field is populated with kindinfo objects.
'
content:
application/json:
schema:
type: object
properties:
data:
allOf:
- $ref: '#/components/schemas/model.node-details'
- type: object
properties:
info:
type: array
description: Kindinfo objects populated when `include-info=true`.
items:
type: object
required:
- name
- title
- position
- node_kind_id
- markdown
properties:
name:
type: string
description: Kindinfo key.
title:
type: string
description: Human-readable kindinfo title.
position:
type: integer
format: int32
description: Display position for the kindinfo object.
node_kind_id:
type: integer
format: int32
description: Schema node kind ID associated with the kindinfo object.
markdown:
type: object
required:
- content
properties:
content:
type: string
description: Markdown content for the kindinfo object.
examples:
Info omitted:
summary: Node details when include-info is not present
value:
data:
node_id: 1234567890
kinds:
- node_kind_id: 1
name: User
- node_kind_id: 2
name: Entity
properties:
objectid: S-1-5-21-3130019616-2776909439-2417379446-1108
name: ADMIN@CORP.EXAMPLE.COM
displayName: Admin
lastSeen: '2026-07-14T18:24:32Z'
Info included:
summary: Node details when include-info is true
value:
data:
node_id: 1234567890
kinds:
- node_kind_id: 1
name: User
- node_kind_id: 2
name: Entity
properties:
objectid: S-1-5-21-3130019616-2776909439-2417379446-1108
name: ADMIN@CORP.EXAMPLE.COM
displayName: Admin
lastSeen: '2026-07-14T18:24:32Z'
info:
- name: overview
title: Overview
position: 0
node_kind_id: 1
markdown:
content: This user has privileged access in the environment.
'400':
$ref: '#/components/responses/bad-request'
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'404':
$ref: '#/components/responses/not-found'
'429':
$ref: '#/components/responses/too-many-requests'
'500':
$ref: '#/components/responses/internal-server-error'
/api/v2/extensions:
put:
description: '**Experimental** - Upserts the OpenGraph extension'
tags:
- OpenGraph (Experimental)
operationId: UpsertOpenGraphExtension
summary: Upserts the OpenGraph Extension
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/model.graph-extension.payload'
examples:
Generic:
summary: Generic OpenGraph Extension Example
description: An example generic OpenGraph Extension payload.
value:
schema:
name: OpenGraph_Extension
display_name: OpenGraph Extension
version: v1.0.0
namespace: OGE
node_kinds:
- name: OGE_Node_Kind_1
display_name: Node Kind 1
description: A generic node kind
is_display_kind: true
icon: computer
color: '#FF0000'
info:
overview:
title: Overview
position: 1
markdown:
content: This is an overview panel for Node Kind 1
details:
title: Additional Details
position: 2
markdown:
content: This panel provides additional information about the node
- name: OGE_Node_Kind_2
display_name: Node Kind 2
description: A secondary generic node kind
is_display_kind: false
- name: OGE_Environment_Kind
display_name: Environment
description: A generic environment kind
icon: sitemap
color: '#00FF00'
is_display_kind: true
relationship_kinds:
- name: OGE_ConnectedTo
description: A generic relationship kind
is_traversable: true
info:
abuse:
title: Abuse Info
position: 1
markdown:
content: Information about how this relationship can be abused
environments:
- environment_kind: OGE_Environment_Kind
source_kind: OGExtension
principal_kinds:
- OGE_Node_Kind_1
- OGE_Node_Kind_2
relationship_findings:
- name: OGE_Finding_1
display_name: Finding 1
relationship_kind: OGE_ConnectedTo
environment_kind: OGE_Environment_Kind
remediation:
short_description: a remediation
long_description: a remediation to fix Finding 1
short_remediation: Do X
long_remediation: Do X to fix Finding 1
Active Directory:
summary: Active Directory Example
description: An example Active Directory OpenGraph Extension. As a note, the Active Directory objects in the DB do not currently conform to the expected validation rules. This will change in the future. The example provided is how an Active Directory extension would look in the OpenGraph framework.
value:
schema:
name: Active_Directory
display_name: Active Directory
version: v1.0.0
namespace: AD
node_kinds:
- name: AD_Computer
display_name: Computer
description: An Active Directory Computer object
is_display_kind: true
icon: computer
color: '#FF0000'
info:
overview:
title: Computer Overview
position: 1
markdown:
content: Active Directory computer objects represent physical or virtual machines joined to the domain
- name: AD_User
display_name: User
description: An Active Directory User object
is_display_kind: true
icon: user
color: '#0000FF'
info:
overview:
title: User Overview
position: 1
markdown:
content: Active Directory user accounts represent individuals who can authenticate to the domain
security:
title: Security Considerations
position: 2
markdown:
content: User accounts should follow the principle of least privilege
- name: AD_LocalGroup
display_name: Local Group
description: An Active Directory Local Group object
is_display_kind: false
- name: AD_Domain
display_name: Domain
description: An Active Directory Domain environment object
icon: sitemap
color: '#00FF00'
is_display_kind: true
relationship_kinds:
- name: AD_MemberOf
description: An Active Directory relationship kind
is_traversable: true
info:
description:
title: MemberOf Relationship
position: 1
markdown:
content: The MemberOf relationship indicates group membership in Active Directory
abuse:
title: Abuse Information
position: 2
markdown:
content: Group membership can be leveraged to inherit permissions and access rights
environments:
- environment_kind: AD_Domain
source_kind: ActiveDirectory
principal_kinds:
- AD_User
- AD_Computer
relationship_findings:
- name: AD_Finding_1
display_name: Finding 1 (Active Directory)
relationship_kind: AD_MemberOf
environment_kind: AD_Domain
remediation:
short_description: a remediation
long_description: a remediation to fix Finding 1
short_remediation: Do X
long_remediation: Do X to fix Finding 1
responses:
'200':
description: OK
'201':
description: CREATED
'400':
$ref: '#/components/responses/bad-request'
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'404':
$ref: '#/components/responses/not-found'
'429':
$ref: '#/components/responses/too-many-requests'
'500':
$ref: '#/components/responses/internal-server-error'
get:
description: '**Experimental** - Lists pertinent OpenGraph extension information such as id, name, version, and if it is a built-in extension'
tags:
- OpenGraph (Experimental)
operationId: ListExtensions
summary: List OpenGraph Extensions Information
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/api.response.graph-extensions-info'
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'500':
$ref: '#/components/responses/internal-server-error'
/api/v2/extensions/{extension_id}:
delete:
description: '**Experimental** - Deletes an OpenGraph Extension by Extension ID'
tags:
- OpenGraph (Experimental)
operationId: DeleteExtension
summary: Delete OpenGraph Extension
parameters:
- name: extension_id
in: path
required: true
description: Extension ID for deletion
schema:
type: integer
format: int32
responses:
'204':
$ref: '#/components/responses/no-content'
'400':
$ref: '#/components/responses/bad-request'
'401':
$ref: '#/components/responses/unauthorized'
'403':
$ref: '#/components/responses/forbidden'
'404':
$ref: '#/components/responses/not-found'
'500':
$ref: '#/components/responses/internal-server-error'
/api/v2/extensions-edges:
parameters:
- $ref: '#/components/parameters/header.prefer'
get:
operationId: ListEdgeKinds
summary: List Edge Kinds
description: '**Experimental** - List all Edge Kinds across OpenGraph Schemas'
tags:
- OpenGraph (Experimental)
parameters:
- name: schemas
description: Schema names to filter response by
in: query
required: false
schema:
type: array
items:
type: string
- name: is_traversable
description: Filter response by whether or not an edge is traversable
in: query
required: false
schema:
$ref: '#/components/schemas/api.params.predicate.filter.boolean'
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/model.graph-schema-edge-kinds'
'400':
$ref: '#/components/responses/bad-request'
'401':
$ref: '#/components/responses/unauthorized'
'429':
$ref: '#/components/responses/too-many-requests'
'500':
$ref: '#/components/responses/internal-server-error'
components:
responses:
not-found:
description: '**Not Found**
This error typically comes from operations where a valid ID was passed to the request
to look up an entity but the entity could not be found.
'
content:
application/json:
schema:
$ref: '#/components/schemas/api.error-wrapper'
example:
http_status: 404
timestamp: '2024-02-19T19:27:43.866Z'
request_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
errors:
- context: clients
message: The requested client could not be found.
no-content:
description: '**No Content**
This response will contain no response body.
'
content:
text/plain:
schema:
type: string
example: '[this request has no response data]'
bad-request:
description: '**Bad Request**
This could be due to one of the following reasons:
- JSON payload is missing or malformed
- Path or query parameters are missing or invalid/malformed
- The data sent is not valid (ex- sending a `string` in an `integer` field)
'
content:
application/json:
schema:
$ref: '#/components/schemas/api.error-wrapper'
example:
http_status: 400
timestamp: '2024-02-19T19:27:43.866Z'
request_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
errors:
- context: clients
message: The JSON payload could not be unmarshalled.
forbidden:
description: '**Forbidden**
This is most commonly caused by an authenticated client trying to
access a resource that it does not have permission for.
'
content:
application/json:
schema:
$ref: '#/components/schemas/api.error-wrapper'
example:
http_status: 403
timestamp: '2024-02-19T19:27:43.866Z'
request_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
errors:
- context: clients
message: You do not have permission to access this resource
too-many-requests:
description: '**Too Many Requests**
The client has sent too many requests within a certain time window
and tripped the rate limiting middleware.
'
content:
application/json:
schema:
$ref: '#/components/schemas/api.error-wrapper'
example:
http_status: 429
timestamp: '2024-02-19T19:27:43.866Z'
request_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
errors:
- context: middleware
message: Too many requests. Please try again later.
internal-server-error:
description: '**Internal Server Error**
This is usually the result of either an unexpected database or application error.
The client may try modifying or resending the request, but the error is likely not related to the client
doing something wrong.
'
content:
application/json:
schema:
$ref: '#/components/schemas/api.error-wrapper'
example:
http_status: 500
timestamp: '2024-02-19T19:27:43.866Z'
request_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
errors:
- context: clients
message: The request could not be handled due to an unexpected database error.
unauthorized:
description: '**Unauthorized**
This endpoint failed an authentication requirement. Either the client tried to access
a protected endpoint without being authenticated, or an auth validation failed (ex- invalid
credentials or expired token).
'
content:
application/json:
schema:
$ref: '#/components/schemas/api.error-wrapper'
example:
http_status: 401
timestamp: '2024-02-19T19:27:43.866Z'
request_id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
errors:
- context: login
message: Unauthorized
schemas:
api.params.predicate.filter.boolean:
type: string
pattern: ^((eq|neq):)?(t|T|TRUE|true|True|f|F|FALSE|false|False)$
description: 'Filter results by column boolean value. Valid filter predicates are `eq`, `neq`.
'
api.response.graph-extensions-info:
type: object
properties:
data:
type: object
required:
- extensions
properties:
extensions:
type: array
items:
type: object
required:
- id
- name
- version
- is_builtin
- namespace
properties:
id:
type: integer
format: int32
name:
type: string
version:
type: string
is_builtin:
type: boolean
namespace:
type: string
required:
- data
example:
data:
extensions:
- id: 1
name: Active Directory
version: v1.0.0
is_builtin: true
namespace: AD
model.relationship-details-with-info:
allOf:
- $ref: '#/components/schemas/model.relationship-details'
- type: object
properties:
info:
type: array
items:
allOf:
- $ref: '#/components/schemas/model.kind-info-response'
- type: object
required:
- relationship_kind_id
properties:
relationship_kind_id:
type: integer
model.relationship-details:
type: object
required:
- relationship_id
- source_node_id
- target_node_id
- kind
- properties
properties:
relationship_id:
type: integer
format: int64
readOnly: true
example: 1234567890
source_node_id:
type: integer
format: int64
readOnly: true
example: 1234567890
target_node_id:
type: integer
format: int64
readOnly: true
example: 1234567890
kind:
type: object
required:
- relationship_kind_id
- name
properties:
relationship_kind_id:
type: integer
format: int32
nullable: true
name:
type: string
properties:
type: object
additionalProperties: true
required:
- is_traversable
- lastSeen
properties:
is_traversable:
type: boolean
lastSeen:
type: string
format: date-time
model.graph-extension.kind-info-definition:
type: object
description: 'A map of named info panels used in extension definitions.
Each property key is a stable panel identifier, and each property value
must be a valid KindInfo object with title, position, and content.
'
propertyNames:
pattern: ^[a-z0-9_-]{1,128}$
description: Keys may only include lowercase alphanumeric characters, hyphens, and underscores
additionalProperties:
type: object
required:
- title
- position
properties:
title:
type: string
example: Entity Panel Section
position:
type: integer
minimum: 1
markdown:
type: object
required:
- content
properties:
content:
type: string
example: This is markdown content for the info panel
model.kind-info-content:
type: object
oneOf:
- $ref: '#/components/schemas/model.kind-info-type-markdown'
api.error-detail:
type: object
properties:
context:
type: string
description: The context in which the error took place
message:
type: string
description: A human-readable description of the error
model.kind-info-type-markdown:
type: object
required:
- markdown
properties:
markdown:
type: object
required:
- content
properties:
content:
type: string
model.kind-info-response:
allOf:
- type: object
required:
- name
properties:
name:
type: string
example: panel_section_1
- $ref: '#/components/schemas/model.kind-info-base'
- $ref: '#/components/schemas/model.kind-info-content'
model.graph-extension.payload:
type: object
properties:
schema:
type: object
properties:
name:
type: string
example: OpenGraph_Extension
display_name:
type: string
example: OpenGraph Extension
version:
type: string
example: v1.0.0
namespace:
type: string
example: OGE
node_kinds:
type: array
items:
type: object
properties:
name:
type: string
example: OGE_Computer
display_name:
type: string
example: OGE Computer
description:
type: string
example: An OpenGraph Extension Computer object
is_display_kind:
type: boolean
icon:
type: string
example: Desktop
color:
type: string
example: '#FF0000'
# --- truncated at 32 KB (37 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/specterops/refs/heads/main/openapi/specterops-opengraph-experimental-api-openapi.yml