> ## Documentation Index
> Fetch the complete documentation index at: https://bloodhound.specterops.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Entity Panel Content

> Define and validate custom Entity Panel content for nodes and relationships in BloodHound.

export const Checklist = ({checklistKey, title, children}) => {
  const [items, setItems] = useState([]);
  useEffect(() => {
    const saved = localStorage.getItem(`checklist-${checklistKey}`);
    if (saved) {
      try {
        const parsed = JSON.parse(saved);
        setItems(parsed);
      } catch {
        const listItems = document.querySelectorAll(`[data-checklist="${checklistKey}"] li`);
        setItems(new Array(listItems.length).fill(false));
      }
    } else {
      const listItems = document.querySelectorAll(`[data-checklist="${checklistKey}"] li`);
      setItems(new Array(listItems.length).fill(false));
    }
  }, [checklistKey]);
  useEffect(() => {
    if (items.length > 0) {
      localStorage.setItem(`checklist-${checklistKey}`, JSON.stringify(items));
    }
  }, [items, checklistKey]);
  const handleToggle = index => {
    setItems(prev => {
      const newItems = [...prev];
      newItems[index] = !newItems[index];
      return newItems;
    });
  };
  return <div className="checklist-container" data-checklist={checklistKey}>
      {title && <h3 className="checklist-title">{title}</h3>}
      <div className="checklist-content">
        {React.Children.map(children, child => {
    if (React.isValidElement(child) && child.type === 'ul') {
      return <ul className="checklist-list">
                {React.Children.map(child.props.children, (li, liIndex) => {
        if (React.isValidElement(li) && li.type === 'li') {
          return <li key={liIndex}>
                        <label>
                          <input type="checkbox" checked={items[liIndex] || false} onChange={() => handleToggle(liIndex)} />
                          <span className="task-text">{li.props.children}</span>
                        </label>
                      </li>;
        }
        return li;
      })}
              </ul>;
    }
    return child;
  })}
      </div>
      
      <style jsx>{`
        .checklist-container {
          display: block;
          margin: 1rem 0;
          padding: 1rem;
          border-radius: 0.5rem;
          background: var(--background-secondary, #f4f4f4);
        }

        .dark .checklist-container {
          background: var(--background-secondary, #1a1a1a);
        }

        .checklist-title {
          margin-top: 0;
          margin-bottom: 1rem;
          color: var(--text-primary, #1d1b20);
        }

        .dark .checklist-title {
          color: var(--text-primary, #ffffff);
        }

        .checklist-content {
          position: relative;
        }

        .checklist-list {
          list-style: none !important;
          padding: 0 !important;
          margin: 0 !important;
        }

        .checklist-list li {
          margin: 0.5rem 0 !important;
          padding: 0 !important;
          list-style: none !important;
        }

        .checklist-list li::before {
          content: none !important;
        }

        label {
          display: grid;
          grid-template-columns: 1.5rem 1fr;
          gap: 0.5rem;
          align-items: start;
          padding: 0.5rem;
          border-radius: 0.25rem;
          cursor: pointer;
          color: var(--text-secondary, #55595c);
          transition: all 0.2s ease;
        }

        .dark label {
          color: var(--text-secondary, #a0a0a0);
        }

        label:hover {
          background: var(--background-tertiary, #e3e7ea);
        }

        .dark label:hover {
          background: var(--background-tertiary, #2a2a2a);
        }

        input[type='checkbox'] {
          appearance: none;
          -webkit-appearance: none;
          width: 1.25rem;
          height: 1.25rem;
          border: 2px solid var(--border-primary, #cacfd3);
          border-radius: 0.25rem;
          margin: 0;
          cursor: pointer;
          transition: all 0.2s ease;
          flex-shrink: 0;
          position: relative;
        }

        .dark input[type='checkbox'] {
          border-color: var(--border-primary, #404040);
        }

        input[type='checkbox']:focus-visible {
          outline: 2px solid var(--accent-primary, #00aa66);
          outline-offset: 2px;
        }

        input[type='checkbox']:checked {
          background-color: var(--accent-primary, #00aa66);
          border-color: var(--accent-primary, #00aa66);
        }

        input[type='checkbox']:checked::after {
          content: '';
          position: absolute;
          display: block;
          width: 0.5rem;
          height: 0.875rem;
          border: solid white;
          border-width: 0 2px 2px 0;
          transform: translate(0.25rem, -0.125rem) rotate(45deg);
        }

        label:has(> input[type='checkbox']:checked) {
          color: var(--accent-primary, #00aa66);
          text-decoration: line-through;
          text-decoration-color: var(--accent-primary, #00aa66);
        }

        .task-text {
          line-height: 1.5;
        }
      `}
    </style>
    </div>;
};

<img noZoom src="https://mintcdn.com/specterops/tTIczgde9H07oLXf/assets/enterprise-AND-community-edition-pill-tag.svg?fit=max&auto=format&n=tTIczgde9H07oLXf&q=85&s=ad49a576589f4d2a8081df77d07fdf56" alt="Applies to BloodHound Enterprise and Community Edition" width="482" height="45" data-path="assets/enterprise-AND-community-edition-pill-tag.svg" />

When you select a node or relationship in **Explore**, BloodHound displays its properties in the Entity Panel.

OpenGraph extensions can add custom sections to the panel for extension-specific context. Define custom content in the `info` object of an [extension definition schema](/opengraph/developer/graph-definition) using static Markdown or dynamic Go templates.

This page covers custom section structure, Markdown and template validation, and dynamic content options.

## Custom Entity Panel content

Use the [`info`](/opengraph/developer/graph-definition#param-info) object to define custom Entity Panel sections for node kinds and relationship kinds. When a user selects a node or relationship in Explore, BloodHound renders an Entity Panel with the specified accordion sections as defined by the `info` entries.

Each `info` object is a map of section identifiers to rendered sections. Use stable identifiers for the keys so future schema updates can modify the same section without changing its identity.

```json theme={null}
"info": {
  "overview": {
    "title": "Overview",
    "position": 1,
    "markdown": {
      "content": "This content appears in the Entity Panel."
    }
  }
}
```

<Tip>
  Draft your content in a Markdown editor, then convert it to a single-line JSON string before adding it to the `content` field. Encode newlines as escaped `\n` sequences. This workflow applies to both static and dynamic content.
</Tip>

<ResponseField name="info key" type="string">
  Stable section identifier. Must match `^[a-z0-9_-]{1,128}$`, which allows lowercase letters, numbers, hyphens, and underscores.
</ResponseField>

<ResponseField name="title" type="string">
  Section title displayed in the Entity Panel accordion header.
</ResponseField>

<ResponseField name="position" type="integer">
  Positive integer (`1` or higher) that controls the order of extension-defined sections. Lower values render first. Use `1` for the first extension-defined section.
</ResponseField>

<ResponseField name="markdown.content" type="string">
  Single-line JSON string containing the Markdown rendered in the section body. Encode line breaks as escaped `\n` sequences. Required when `markdown` is present. You can use static Markdown or a [Go template](/opengraph/developer/entity-panel-content#dynamic-entity-panel-content) that renders values for the selected entity.
</ResponseField>

Every Entity Panel starts with **Object Information** at position `0`. This section lists all properties for the selected node or relationship.

BloodHound renders additional `info` entries for the selected node's primary kind or the selected relationship's kind after **Object Information**. BloodHound orders those extension-defined sections by `position`, then `title`.

If no `info` entries are defined for the selected kind, BloodHound still renders **Object Information**, but does not render additional extension-defined Entity Panel sections.

### Markdown validation

When you upload an extension definition schema, BloodHound validates each Markdown field to ensure it does not contain unsafe or disallowed HTML.

<Note>
  Validation only checks the content; BloodHound stores your original Markdown without modifying it.
</Note>

BloodHound validates Markdown in the following fields:

| Field(s)                          | Where                                                                                                                                                   |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `info.<section>.markdown.content` | `node_kinds` and `relationship_kinds` arrays (see [Custom Entity Panel content](/opengraph/developer/entity-panel-content#custom-entity-panel-content)) |

#### Supported Markdown

BloodHound supports [CommonMark](https://commonmark.org/) plus the following [GitHub Flavored Markdown](https://github.github.com/gfm/) extensions:

* Tables
* Strikethrough
* Task lists
* Autolinks
* Fenced code blocks (with an alphanumeric language hint)

You can also include inline HTML that is on BloodHound's allowlist of safe elements and attributes, such as links and basic text formatting.

#### Rejected content

BloodHound rejects the upload when a Markdown field contains unsafe or disallowed HTML, including:

* `<script>` tags and other executable content
* Inline event-handler attributes, such as `onclick`
* `javascript:` links
* `<iframe>`, `<style>`, and similar elements

<Warning>
  If any Markdown field fails validation, the entire extension definition schema upload fails.
</Warning>

## Dynamic Entity Panel content

Most Entity Panel content can be ordinary Markdown. When a panel should adapt to the selected node or relationship, use a template expression to insert values or describe relationship endpoints.

This guide is organized by authoring goal:

* Read a value: **Basic templates**
* Transform or reuse a value: **Pipelines and variables**
* Include or omit content: **Control flow**

The panel's section name, title, and position are static. Templates change the Markdown rendered inside a section; they do not create or remove accordion sections.

<Tip>
  **Best practice**

  Guard dynamic content before rendering it. See Guarding Dynamic Content.
</Tip>

### Template expressions

The simplest template expression evaluates a value and inserts the result into the Markdown. Expressions use Go `text/template` syntax and are enclosed in `{{` and `}}`:

```markdown theme={null}
# {{ .Properties.name }}
```

For a node whose `name` property is `Alice`, the rendered content is:

```markdown theme={null}
# Alice
```

This small expression is the foundation for the rest of the guide. Static Markdown remains static (defined in advance). Use templates when content needs to be evaluated at render time (for example, to read entity data, compute or reuse a value, or conditionally render part of the Markdown).

### Basic templates

Template expressions evaluate values available to the template and insert their results into Markdown. Those values may come from the current graph context, such as `.Properties`, `.Source`, or `.Target`, or from template variables declared with `:=`. The surrounding Markdown remains literal, while the template determines which values are evaluated and where their results appear.

* Graph context values come from the selected node or relationship and are accessed from `.`.
* Template variables are local names created with `:=`, such as `$name` or `$value`. They store a value so the template can reuse it.

Graph context describes the data the panel starts with. Template variables describe values that the template creates and reuses while rendering.

#### Graph context

The graph context is the entity data made available to the panel. Its shape depends on whether the panel is rendered for a node or a relationship.

##### Node context

For a node panel, the root context represents the selected node:

| Expression               | Description                                                          |
| ------------------------ | -------------------------------------------------------------------- |
| `.NodeID`                | Graph-assigned ID of the selected node                               |
| `.Kinds`                 | Kinds assigned to the node (each kind exposes `.Name` and `.KindID`) |
| `.Properties`            | Map of node properties                                               |
| `.Properties.<property>` | A property with an identifier-safe key (such as `.Properties.name`)  |

Template example:

```markdown theme={null}
# {{ .Properties.name | default "Unknown" }}

Kinds: {{ join ", " .Kinds }}
```

Rendered example:

```markdown theme={null}
# Alice

Kinds: User, Entity
```

##### Relationship context

For a relationship panel, the root context represents the relationship and includes source and target node contexts:

| Expression                      | Description                                          |
| ------------------------------- | ---------------------------------------------------- |
| `.RelationshipID`               | Graph-assigned ID of the selected relationship       |
| `.Kind.Name`                    | Name of the relationship kind                        |
| `.Properties`                   | Map of relationship properties                       |
| `.Properties.<property>`        | A specific property (such as `.Properties.lastseen`) |
| `.Source`                       | Node context for the relationship source             |
| `.Target`                       | Node context for the relationship target             |
| `.Source.Properties.<property>` | Property on the source node                          |
| `.Target.Properties.<property>` | Property on the target node                          |

Example:

```markdown theme={null}
## {{ .Source.Properties.name | default "Unknown source" }} → {{ .Target.Properties.name | default "Unknown target" }}

This is a **{{ .Kind.Name }}** relationship.
```

Rendered example:

```markdown theme={null}
## Alice → Domain Admins

This is a **MemberOf** relationship.
```

The context is intentionally limited. Templates cannot fetch additional data, traverse the graph, or access the complete internal node or relationship object.

##### Direct Property References and `get`

Use a direct property reference when the key is a normal identifier, such as `.Properties.displayName`.

Use `get` when you need a string-based map lookup—for example, when a key contains punctuation or spaces, or when the key is stored in another template variable:

```markdown theme={null}
{{ get .Properties "display-name" | default "Unknown" }}
```

`get` only looks up a key in the `.Properties` map; it does not retrieve additional graph data or make absent properties available.

#### Function syntax

Go template functions use space-separated call syntax: the function name comes first, followed by its arguments. Parentheses can group an argument and nest a function call, but they are not written as conventional function-call syntax:

```markdown theme={null}
{{ get .Properties "display-name" }}
{{ default "Unknown" .Properties.name }}
{{ upper (default "Unknown" .Properties.name) }}
```

Use parentheses when a function result must be passed as an argument to another function or when a condition combines multiple comparisons:

```markdown theme={null}
{{ if and (ne .Properties.name "") (hasPrefix .Properties.name "svc-") }}
This service account uses the expected naming convention.
{{ end }}
```

Use pipelines for straightforward left-to-right transformations. A function receives graph context expressions, template variables, literal values, or parenthesized expressions and returns a result. Assign the result with `:=` when it needs to be reused.

#### Pipelines

Go template pipelines use `|` to pass the result of one command to the next command as its final argument:

```markdown theme={null}
{{ .Properties.name | upper | default "Unknown" }}
```

Functions with arguments can be used in pipelines. In Go templates, the result of the preceding command is appended as the final argument to the next command, so place fixed arguments before the piped value:

```markdown theme={null}
{{ .Properties.name | replace " " "-" | default "Unknown" }}
```

Here, `replace` receives its fixed arguments (`" "` and `"-"`) before the piped property value. If the piped value belongs in another position, use a direct function call and pipe its result instead. For example, `get` expects the map before the key:

```markdown theme={null}
{{ get .Properties "display-name" | default "Unknown" }}
```

#### Whitespace trimming

Hyphens next to the template brackets are optional. They trim whitespace around an action:

* `{{-` trims whitespace immediately before the action.
* `}}` trims whitespace immediately after the action.
* `{{- ... -}}` trims whitespace on both sides.

Whitespace includes spaces, tabs, and newlines. Hyphens do not represent subtraction and do not change the value being evaluated.

Trimming is common around control-flow actions because a block that renders no content can otherwise leave an unwanted blank line.

Without trimming:

```markdown theme={null}
This account requires delegation protection.
{{ if .Properties.isTierZero }}
Because it is Tier Zero, review its delegation exposure carefully.
{{ end }}
Continue with the next steps.
```

When `isTierZero` is false, the rendered content contains an empty line:

```markdown theme={null}
This account requires delegation protection.

Continue with the next steps.
```

With trimming around the control-flow actions:

```markdown theme={null}
This account requires delegation protection.
{{- if .Properties.isTierZero }}
Because it is Tier Zero, review its delegation exposure carefully.
{{- end }}
Continue with the next steps.
```

When `isTierZero` is false, the rendered content is:

```markdown theme={null}
This account requires delegation protection.
Continue with the next steps.
```

Use trimming deliberately. It can remove meaningful spacing that may be intended.

For example:

```json theme={null}
Access: {{ .Properties.level }}
```

Renders as `Access: admin`, while:

```json theme={null}
Access:{{- .Properties.level }}
```

Renders as `Access:admin`.

#### Template variables

Template variables are local names that start with `$` and are assigned with `:=`.

A variable declared in the current template body remains available through the end of that template execution. A variable declared inside an `if`, `range`, or `with` block is available only within that block. Each `markdown.content` value is rendered independently, so variables do not carry between separate Entity Panel objects.

##### Reusable variables

Create a local template value by writing a `$`-prefixed identifier first, followed by `:=` and the expression that supplies its value: `$identifier := expression`. Reuse that value later by referencing the same identifier, such as `$edgeType`. In the example below, each value is assigned once and reused in multiple rendered sentences or list items.

```markdown theme={null}
{{- $edgeType := .Kind.Name | default "unknown" -}}
{{- $sourceName := .Source.Properties.name | default "unknown source" -}}
{{- $targetName := .Target.Properties.name | default "unknown target" -}}

This is a {{ $edgeType }} relationship.

Here are some additional details about {{ $edgeType }}:
* Source: {{ $sourceName }}
* Target: {{ $targetName }}
```

Rendered example:

```markdown theme={null}
This is a MemberOf relationship.

Here are some additional details about MemberOf:
* Source: Alice
* Target: Domain Admins
```

### Control flow

The `if`, `else`, `end`, `range`, and `with` elements are Go template actions (not helper functions) that control if and how parts of the Markdown content are displayed.

#### Required versus optional syntax

The following structural elements are required or optional as follows:

* `if` starts a conditional block.
* `end` closes an `if`, `range`, or `with` block.
* `else` is optional and provides the alternate branch of an `if` block.
* `else if` is optional and adds another conditional branch before the final `else` or `end`.
* `range` starts an iteration block.
* `with` starts a block that changes the current context when a value is present.
* Hyphens next to the brackets are optional whitespace controls. They are not required for any action.

An opening `if` action requires one or more arguments that evaluate to boolean values. Use conditional checks to produce those values. The following example shows an `if` action without an `else` or `else if` branch:

```markdown theme={null}
{{- if and (ne .Properties.name "") (hasPrefix .Properties.name "svc-") -}}
This service account uses the expected naming convention.
{{- end -}}
```

An `else` action does not take arguments; it renders the alternate branch when the `if` condition is false. See [Conditional content](/opengraph/developer/entity-panel-content/#conditional-content) below for an example.

An `else if` action accepts its own arguments (like `if`) and evaluates them only when the preceding condition is false. Use `else if` to add another conditional branch before an optional `else` action.

#### Conditional content

A conditional can wrap a sentence, paragraph, list, code block, or complete Markdown subsection. This is useful when the same guidance needs different explanatory or action-oriented copy depending on the selected entity's state.

For example, security guidance for an entity could present different Kerberos-delegation options for service accounts and non-service accounts. Protected Users might be preferred for non-service accounts, while service accounts may not be compatible with Protected Users; in that case, mark the account as sensitive instead.

The property name below is illustrative; use the property exposed by the extension's entity data.

The following example shows an `if`/`else` pair:

```markdown theme={null}
## Kerberos Delegation Context

{{- if (default false .Properties.isServiceAccount) -}}
Because this is a service account, do not add it to the Protected Users group. Mark it as sensitive and cannot be delegated instead, then test the dependent service.
{{ else }}
For a non-service account, adding it to the Protected Users group is the preferred approach. If that is not practical, mark the account as sensitive and cannot be delegated.
{{ end }}
```

For a service account, the rendered content is:

```markdown theme={null}
## Kerberos Delegation Context

Because this is a service account, do not add it to the Protected Users group. Mark it as sensitive and cannot be delegated instead, then test the dependent service.
```

For a non-service account, the rendered content is:

```markdown theme={null}
## Kerberos Delegation Context

For a non-service account, adding it to the Protected Users group is the preferred approach. If that is not practical, mark the account as sensitive and cannot be delegated.
```

This is a true either/or rendering decision: exactly one branch is emitted. The conditional can remove the section's content, but it cannot remove the accordion itself. The section remains because its structure is statically defined in the extension definition schema.

Use `else` when the reader needs alternate wording. Use `with` when a block should appear only when a value is present; inside the block, `.` becomes that value. Use `range` to render a collection, such as a list of tags.

### Supported functions

Functions are separate from Go template actions. The Entity Panel function map is a curated Sprig-based allowlist.

| Goal                                 | Supported functions                                                                                                                  |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| Conditional checks and null handling | `default`, `coalesce`, `empty`, `ternary`, `contains`, `hasPrefix`, `hasSuffix`, `eq`, `ne`, `lt`, `gt`, `and`, `or`, `not`          |
| Text formatting                      | `printf`, `trim`, `trimPrefix`, `trimSuffix`, `upper`, `lower`, `title`, `replace`, `trunc`, `substr`, `repeat`, `indent`, `nindent` |
| Lists and iteration helpers          | `list`, `join`, `first`, `last`, `rest`, `initial`, `reverse`, `uniq`, `compact`, `has`, `sortAlpha`, `len`                          |
| Dictionary lookup and manipulation   | `dict`, `get`, `hasKey`, `keys`, `values`, `pick`, `omit`                                                                            |
| Numbers and conversion               | `add`, `sub`, `mul`, `div`, `mod`, `max`, `min`, `ceil`, `floor`, `round`, `int`, `int64`, `float64`, `atoi`                         |
| JSON and encoding                    | `toJson`, `toPrettyJson`, `fromJson`, `b64enc`, `b64dec`, `urlquery`                                                                 |
| Quoting                              | `quote`, `squote`                                                                                                                    |

Template actions:

| Category                    | Actions             |
| --------------------------- | ------------------- |
| Conditional control flow    | `if`, `else`, `end` |
| Context and optional blocks | `with`, `end`       |
| List iteration              | `range`, `end`      |

The function map excludes functions that are unsafe, expensive, or inappropriate for Entity Panel authoring, including regular-expression, cryptographic, certificate-generation, random-number, URL-parsing, and URL-joining helpers. The supported-function reference is the source of truth for the target release.

### Errors and testing

If a template cannot be parsed or executed, the response retains the original unexecuted template string and includes a template error describing the failure.

Schema validation should catch many invalid syntax errors, but not all errors are known prior to runtime resolution.

Common causes include:

* Misspelled context fields or properties
* Unsupported functions
* Invalid template syntax
* Missing values without fallback logic
* Values passed to a helper with an unexpected type

Test templates incrementally:

1. Start with static Markdown.
2. Add one value expression.
3. Add a fallback with `default`.
4. Add a pipeline or reusable variable.
5. Add control flow and test every branch.
6. Inspect rendered Markdown, including whitespace.

### Quick checklist

Before publishing a dynamic Entity Panel:

<Checklist checklistKey="templating">
  * Use the correct node or relationship context.
  * Use direct property references for ordinary keys and `get` for string-based map lookup.
  * Keep variables, pipelines, and control-flow actions conceptually separate.
  * Remember that hyphens are optional whitespace controls, not required syntax.
  * Test rendered output, not only the template source.
  * Use only documented helper functions.
  * Confirm that each accordion remains useful when conditional content is absent.
</Checklist>
