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

# Bazel Aspects

> <Badge color="green">Beta</Badge> <br /> Learn how to implement Endor Labs in monorepos using Bazel aspects

export const YamlTable = ({children, data: propData, content}) => {
  const KV_RE = /^([A-Za-z][A-Za-z0-9_()/#\s-]+?):\s*(.+)$/;
  const INLINE_MD_RE = /(\[([^\]]+)\]\(([^)]+)\))|(`([^`]+)`)|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)/g;
  const YES_RE = /^-yes-$/i;
  const NO_RE = /^-no-$/i;
  const LIMITED_RE = /^-(limited|partial)-$/i;
  const NA_RE = /^-(na|none)-$/i;
  const NA2_RE = /^-na2-$/i;
  const SIMPLE_TAG_RE = /(<br\s*\/?>)|(<p\s*\/?>)|(-note-)|(-warning-)/gi;
  const tryParseKV = trimmed => {
    const m = KV_RE.exec(trimmed);
    return m ? {
      key: m[1],
      value: m[2].trim()
    } : null;
  };
  const registerKey = (key, seenKeys, orderedKeys) => {
    if (!seenKeys.has(key)) {
      orderedKeys.push(key);
      seenKeys.add(key);
    }
  };
  const flushEntry = (currentEntry, entries) => {
    if (Object.keys(currentEntry).length > 0) entries.push(currentEntry);
  };
  const parseDashPrefixed = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('- ')) {
        if (inEntry) entries.push(currentEntry);
        currentEntry = {};
        inEntry = true;
        const kv = tryParseKV(trimmed.substring(2).trim());
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      } else if (inEntry && trimmed !== '') {
        const kv = tryParseKV(trimmed);
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      }
    }
    flushEntry(currentEntry, entries);
  };
  const parseBlankSeparated = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '') {
        if (inEntry) {
          flushEntry(currentEntry, entries);
          currentEntry = {};
          inEntry = false;
        }
        continue;
      }
      const kv = tryParseKV(trimmed);
      if (!kv) continue;
      const isNewEntry = !line.startsWith(' ') && !line.startsWith('\t');
      if (isNewEntry && inEntry && Object.keys(currentEntry).length > 0) {
        entries.push(currentEntry);
        currentEntry = {};
      }
      registerKey(kv.key, seenKeys, orderedKeys);
      currentEntry[kv.key] = kv.value;
      inEntry = true;
    }
    flushEntry(currentEntry, entries);
  };
  const normalizeEntries = (entries, orderedKeys) => entries.map(entry => {
    const filled = {};
    for (const key of orderedKeys) filled[key] = entry[key] || '';
    return filled;
  });
  const parseYamlTableContent = contentStr => {
    if (!contentStr) return [];
    const entries = [];
    const orderedKeys = [];
    const seenKeys = new Set();
    const lines = contentStr.split('\n');
    if (lines.some(line => line.trim().startsWith('- '))) {
      parseDashPrefixed(lines, entries, orderedKeys, seenKeys);
    } else {
      parseBlankSeparated(lines, entries, orderedKeys, seenKeys);
    }
    return normalizeEntries(entries, orderedKeys);
  };
  const processText = text => {
    if (!text) return text;
    const parts = [];
    let keyIndex = 0;
    let lastIndex = 0;
    let match;
    while ((match = INLINE_MD_RE.exec(text)) !== null) {
      if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
      if (match[1]) {
        parts.push(<a key={keyIndex++} href={match[3]}>{match[2]}</a>);
      } else if (match[4]) {
        parts.push(<code key={keyIndex++}>{match[5]}</code>);
      } else if (match[6]) {
        parts.push(<strong key={keyIndex++}>{match[7]}</strong>);
      } else if (match[8]) {
        parts.push(<em key={keyIndex++}>{match[9]}</em>);
      }
      lastIndex = match.index + match[0].length;
    }
    if (lastIndex < text.length) parts.push(text.slice(lastIndex));
    if (parts.length === 0) return text;
    const keyRef = {
      current: keyIndex
    };
    return expandHtmlTags(parts, keyRef);
  };
  const processBadges = text => {
    if (!text || typeof text !== 'string') return text;
    if (YES_RE.test(text)) return <span className="yt-badge-yes" role="img" aria-label="Supported" title="Supported">✓</span>;
    if (NO_RE.test(text)) return <span className="yt-badge-no" role="img" aria-label="Not supported" title="Not supported">✗</span>;
    if (LIMITED_RE.test(text)) return <span className="yt-badge-limited" role="img" aria-label="Partially supported" title="Partially supported">◐</span>;
    if (NA_RE.test(text) || NA2_RE.test(text)) return <span className="yt-sr-only" title="Not applicable">Not applicable</span>;
    return processText(text);
  };
  const cellClassName = text => {
    if (!text || typeof text !== 'string') return undefined;
    if (NA_RE.test(text)) return 'yt-cell-na';
    if (NA2_RE.test(text)) return 'yt-cell-na2';
    return undefined;
  };
  const expandSimpleTags = (str, keyRef) => {
    const result = [];
    let last = 0;
    SIMPLE_TAG_RE.lastIndex = 0;
    let m;
    while ((m = SIMPLE_TAG_RE.exec(str)) !== null) {
      if (m.index > last) result.push(str.slice(last, m.index));
      if (m[1]) {
        result.push(<br key={keyRef.current++} />);
      } else if (m[2]) {
        result.push(<br key={keyRef.current++} />, <br key={keyRef.current++} />);
      } else if (m[3]) {
        result.push(<span key={keyRef.current++} className="yt-badge-note" style={{
          fontWeight: 600
        }}>Note: </span>);
      } else if (m[4]) {
        result.push(<span key={keyRef.current++} className="yt-badge-warning" style={{
          fontWeight: 600
        }}>Warning: </span>);
      }
      last = m.index + m[0].length;
    }
    if (last < str.length) result.push(str.slice(last));
    return result;
  };
  const expandHtmlTags = (chunks, keyRef) => {
    const out = [];
    for (const chunk of chunks) {
      if (typeof chunk === 'string') {
        out.push(...expandSimpleTags(chunk, keyRef));
      } else {
        out.push(chunk);
      }
    }
    return out;
  };
  const extractText = node => {
    if (node === null || node === undefined) return '';
    if (typeof node === 'string') return node;
    if (typeof node === 'number') return String(node);
    if (typeof node === 'boolean') return '';
    if (Array.isArray(node)) return node.map(extractText).join('');
    if (node && typeof node === 'object' && node.type) {
      const props = node.props || ({});
      if (typeof props.children === 'string') return props.children;
      if (props.children) return extractText(props.children);
      return '';
    }
    return String(node || '');
  };
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  const data = useMemo(() => {
    if (propData) return propData;
    if (content && typeof content === 'string') return parseYamlTableContent(content);
    if (!children) return [];
    if (typeof children === 'string') return parseYamlTableContent(children);
    const childrenArray = Array.isArray(children) ? children : [children];
    return parseYamlTableContent(childrenArray.map(extractText).join('').trim());
  }, [children, propData, content]);
  const columns = useMemo(() => {
    if (!data || data.length === 0) return [];
    const firstRow = data[0];
    if (!firstRow || typeof firstRow !== 'object') return [];
    return Object.keys(firstRow);
  }, [data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <table>
      <thead>
        <tr>
          {columns.map(col => <th key={col}>{col.replaceAll('_', ' ')}</th>)}
        </tr>
      </thead>
      <tbody>
        {data.map(row => <tr key={rowKey(row)}>
            {columns.map(col => <td key={col} className={cellClassName(row[col])}>{processBadges(row[col])}</td>)}
          </tr>)}
      </tbody>
    </table>;
};

In Bazel, a rule defines how a target is built. An aspect is a reusable extension that Bazel can apply to that rule and its dependencies during analysis. Refer to the [Bazel documentation](https://bazel.build/extending/aspects) for more information.

Endor Labs uses aspects to perform software composition analysis on your software packages and extract dependency information in a structured and repeatable manner.

Endor Labs provides built-in Bazel aspects that automatically enhance dependency resolution when scanning Bazel workspaces. You can run scans with aspects enabled so that Endor Labs can automatically discover and use the appropriate aspect rules for your project. If you have custom rules to build your software, you can create your own [custom Bazel aspects](#custom-bazel-aspects) and integrate them with Endor Labs.

You can also use Endor Labs with [Bzlmod](https://bazel.build/external/migration) when you use Bazel aspects. Currently, Go, Java, JavaScript, TypeScript, Python, Scala, Rust, and Swift rulesets support Bzlmod. Software composition analysis with Bazel includes reachability analysis for Go, Java, Scala, and Python.

## Bazel aspect command reference

The following table lists the Bazel aspect command reference.

<YamlTable>
  {`


    - Flag: \`--use-bazel-aspects\`
    Description: Enable the Bazel aspect framework. You need to use this flag along with \`--use-bazel\`.
    - Flag: \`--bazel-aspect-package\`
    Description: By default, endorctl reads the contents of the \`@//.endorctl/aspects\` package for the available aspects. To override the base aspect package, use the \`--bazel-aspect-package\` flag. For example, \`--bazel-aspect-package=@//endor_aspects\`.
    - Flag: \`--bazel-vendor-manifest-path\`
    Description: Path to the \`go.mod\` file for Go projects using vendored dependencies. When provided, endorctl reads the \`go.mod\` file and passes the dependency information to the Go aspect through the \`json_go_mod\` parameter, enabling correct resolution of vendored packages. See [Scan Go vendored projects with Bazel aspects](#scan-go-vendored-projects-with-bazel-aspects) for more information.

    `}
</YamlTable>

## Supported open-source rulesets

Endor Labs supports Bazel aspects for the following open-source rulesets:

<YamlTable>
  {`


    - Ruleset: [rules_go](https://github.com/bazelbuild/rules_go)
    Minimum_Version: 0.42.0
    Supported_Languages: Go (Supports Bzlmod)
    - Ruleset: [rules_java](https://github.com/bazelbuild/rules_java)
    Minimum_Version: 5.0.0
    Supported_Languages: Java (Supports Bzlmod)
    - Ruleset: [rules_python](https://github.com/bazelbuild/rules_python)
    Minimum_Version: 0.9.0
    Supported_Languages: Python (WORKSPACE from 0.9.0 and Bzlmod from 0.30.0)
    - Ruleset: [rules_scala](https://github.com/bazelbuild/rules_scala)
    Minimum_Version: 5.0.0
    Supported_Languages: Scala (Supports Bzlmod)
    - Ruleset: [rules_swift](https://github.com/bazelbuild/rules_swift)
    Minimum_Version: 2.0.0
    Supported_Languages: Swift (Supports Bzlmod)
    - Ruleset: [rules_rust](https://github.com/bazelbuild/rules_rust)
    Minimum_Version: 0.40.0
    Supported_Languages: Rust (Supports Bzlmod)
    - Ruleset: [rules_js](https://github.com/aspect-build/rules_js)
    Minimum_Version: 2.0.0
    Supported_Languages: JavaScript (Supports Bzlmod)
    - Ruleset: [rules_ts](https://github.com/aspect-build/rules_ts)
    Minimum_Version: 1.0.0
    Supported_Languages: TypeScript (Supports both WORKSPACE model and Bzlmod)

    `}
</YamlTable>

<Note>
  **Version support**

  Endor Labs automatically selects the appropriate aspect rule version based on the ruleset version detected in your workspace.
</Note>

## Run endorctl with Bazel aspects

Run the following command to scan the workspace using Bazel aspects.

```shell theme={null}
endorctl scan --use-bazel --use-bazel-aspects
```

### Scan Go vendored projects with Bazel aspects

If your Go project uses [Bazel with Gazelle in vendored mode](https://github.com/bazelbuild/bazel-gazelle?tab=readme-ov-file#bazel-rule), vendored dependencies are stored under a `vendor/` directory and are not resolved through standard external repository mechanisms. To correctly identify these dependencies during an aspect scan, you must provide the path to your `go.mod` manifest file using the `--bazel-vendor-manifest-path` flag.

When you specify this flag, endorctl reads the `go.mod` file, serializes its dependency information as JSON, and passes it to the Go aspect through the `json_go_mod` aspect parameter. The aspect uses this information to resolve vendored packages to their correct module names and versions.

Run the following command to scan a Go vendored project using Bazel aspects.

```shell theme={null}
endorctl scan --use-bazel --use-bazel-aspects \
  --bazel-include-targets=//your-go-target \
  --bazel-vendor-manifest-path=./go.mod
```

<Note>
  The `--bazel-vendor-manifest-path` flag is only applicable to Go targets. Without it, vendored Go dependencies may not be correctly resolved in the dependency graph.
</Note>

## Aspect directory structure

Aspect rules are located under the `.endorctl/aspects` directory in the workspace.

For example, if your workspace is located at `~/my-workspace`, the aspect rules will be located at `~/my-workspace/.endorctl/aspects`.

Place your custom aspects in the `.endorctl/aspects/custom` directory.

## How Bazel aspect scans work

When Endor Labs scans a Bazel workspace with aspects enabled, it performs the following steps:

1. **Set up Aspects:** Initializes and extracts the Bazel aspects plugin to the workspace.
2. **Query the workspace:** Runs `bazel query` to get information about the rules versions used in the workspace.
3. **Query the target:** Runs `bazel query` to query the target being scanned and get information about the external dependencies used by it.
4. **Execute the aspect rule:** Runs `bazel build` to execute the aspect rule.
5. **Read the aspect output:** Reads the aspect output to get the dependency information.

## Bazel aspect output

Bazel aspects output data in JSON format, which Endor Labs uses to populate the dependency graph.

## Bazel build configuration

When executing aspects, Endor Labs runs `bazel build` with specific flags and configuration.

### Bazel aspect configuration flags

Endor Labs creates a temporary `.bazelrc` configuration that includes:

<YamlTable>
  {`


    - Flag: \`--aspects=<aspect_reference>\`
    Purpose: Specifies the aspect to execute.
    - Flag: \`--output_groups=endor_sca_info\`
    Purpose: Requests only the \`endor_sca_info\` output group.
    - Flag: \`--aspects_parameters=external_target_json='<json>'\`
    Purpose: Passes external dependency information to the aspect.
    - Flag: \`--aspects_parameters=ref='<target_ref>'\`
    Purpose: Passes the target reference (for example, git commit SHA) to the aspect.
    - Flag: \`--build_event_json_file=<bep_file>\`
    Purpose: Specifies the Build Event Protocol (BEP) output file. endorctl always uses BEP to read build events and retrieve aspect-generated files.
    - Flag: \`--aspects_parameters=json_go_mod='<go_mod_json>'\`
    Purpose: Passes Go module dependency information. (Go targets only)

    `}
</YamlTable>

### Bazel aspect remote execution and caching

When using remote executors or remote caching, aspect-generated files may be stored remotely, making them inaccessible to endorctl for processing.

To ensure all Bazel aspect outputs are available locally, endorctl automatically sets the following flags:

* `--remote_download_outputs=all`: Forces all aspect outputs to be downloaded locally when using remote executors (for example, Build without Bytes). This is required because endorctl needs to read the json files generated by aspects to populate the dependency graph.
* `--remote_download_toplevel_outputs=all`: Ensures top-level outputs are also downloaded locally, which is necessary for accessing aspect-generated files.

For more information about these Bazel flags, refer to the [Bazel command-line reference](https://bazel.build/reference/command-line-reference).

## Custom Bazel aspects

You can extend Bazel with custom rules to support proprietary toolchains, internal build workflows or enterprise-specific requirements that are not covered by Bazel's built-in rules. While powerful, these custom rules can obscure dependency information from standard analysis tools.

### Dependency information in custom aspects

Endor Labs can automatically analyze dependencies for open-source rule sets. However, custom rules often define dependencies in a non-standard way, such as:

* Generated targets
* Internal dependency resolution logic

Since Bazel considers custom rules as first-class citizens, dependency information inside them is not automatically visible unless explicitly surfaced. Without an aspect, Endor Labs cannot reliably determine:

* What dependencies the rule introduces
* Whether those dependencies are internal or third-party
* How they relate to the rest of the build graph

Custom aspects solve this by explicitly exposing dependency metadata in a format Endor Labs understands.

### Prerequisites for building custom aspects

Before you can get started with developing your own aspects, ensure you have the following set up.

#### Repository Access

Your machine must have the relevant permissions to access the git repository regardless of where it is hosted, be it GitHub, GitLab, or self-hosted.

#### Bazel

Bazel should be installed in the machine you are going to build custom aspects. If you don't have it installed already, follow the [Bazel installation instructions](https://bazel.build/install).

Run the following command to check your Bazel installation.

```bash theme={null}
bazel version
```

#### endorctl CLI

You also need the endorctl CLI available in your path. See [endorctl CLI documentation](https://docs.endorlabs.com/getting-started/quickstart/quickstart-local-system/) for more information.

### Build your custom Bazel aspects

<Note>
  **Beta**

  Custom aspects support is currently in beta. The API and behavior may change in future releases as we continue to improve the framework based on feedback.
</Note>

The following sections provide information to help you build your custom Bazel aspects.

* [Determine if a custom aspect is required](#determine-if-a-custom-aspect-is-required)
* [Custom aspect directory structure](#custom-aspect-directory-structure)
* [Aspect attributes](#aspect-attributes)
* [Output file schema definition for custom aspects](#output-file-schema-definition-for-custom-aspects)
* [Bazel custom aspect example](#bazel-custom-aspect-example)

To help engineers get started, we have open-sourced an example for JavaScript rules. You can find the complete codebase in the [example repository](https://github.com/endorlabs/endor-aspects-it).

#### Determine if a custom aspect is required

You need a custom Bazel aspect if:

* Your dependency graph flows through a custom Bazel rule kind (rule class) that Endor Labs does not support out of the box, such as `my_company_js_binary`.
* The rule declares dependencies in non-standard locations, including custom attribute names, generated targets, or internal dependency resolution logic.

#### Custom aspect directory structure

Custom aspects must be available in the repository that you want to scan.

Ensure that you organize them as shown in the following directory structure for endorctl to recognize them. Use **--bazel-aspect-package** to configure the base package (defaults to `@//.endorctl/aspects`).

```text theme={null}
.endorctl/aspects/
└── custom/                           # User-defined custom aspects
   └── {ecosystem}/
       └── {rule_class}/             # Directory named after rule class
           └── {rule_class}.bzl      # Custom aspect file
```

Use the following path pattern to create your custom aspect.

```text theme={null}
{baseAspectPackage}/custom/{ecosystem}/{rule_class}/{rule_class}.bzl
```

<YamlTable>
  {`


    - Component: Ecosystem
    Example: Go, Rust, JavaScript
    - Component: Rule Class
    Example: go_binary, my_custom_rule

    `}
</YamlTable>

#### Aspect attributes

Your custom aspect must be named `endor_resolve_dependencies`. endorctl discovers it by looking for this symbol in a `.bzl` file at the path described above.

The aspect definition must declare `attr_aspects` to tell Bazel which rule attributes to traverse (for example, `deps`, `data`, `srcs`). It must also declare the following mandatory attributes. The scan fails if any are excluded.

<YamlTable>
  {`


    - Attribute: \`ref\`
    Type: \`attr.string()\`
    Required: Yes
    Description: Git reference (branch/tag) for the scan. Passed by endorctl via \`--aspects_parameters\`.
    - Attribute: \`log_level\`
    Type: \`attr.string(default = "DEBUG")\`
    Required: Yes
    Description: Logging verbosity. Used internally by the aspect for debug output.
    - Attribute: \`external_target_json\`
    Type: \`attr.string(default = "{}")\`
    Required: Yes
    Description: JSON output of external dependency query. Passed by endorctl via \`--aspects_parameters\`.

    `}
</YamlTable>

The following attribute is language-specific and optional.

<YamlTable>
  {`


    - Attribute: \`json_go_mod\`
    Type: \`attr.string(default = "{}")\`
    Required: Go only
    Description: Go module dependency information. Passed by endorctl via \`--aspects_parameters\` for Go targets.

    `}
</YamlTable>

#### Output file schema definition for custom aspects

The output files must be JSON. Serialize your provider (for example, `EndorDependencyInfo`) to JSON with `json.encode_indent()`. The following table lists the fields Endor Labs expects.

<YamlTable>
  {`


    - Field: original_label
    Type: string
    Required: Yes
    Description: Canonical Bazel label (must use @@// prefix)
    - Field: purl
    Type: string
    Required: Yes
    Description: Package URL (PURL) for the dependency, for example \`pkg:npm/package-name@version\`
    - Field: internal
    Type: boolean
    Required: Yes
    Description: true for first-party code, false for third-party
    - Field: dependencies
    Type: string[]
    Required: No
    Description: List of direct dependency labels
    - Field: vendored
    Type: boolean
    Required: No
    Description: true if vendored dependency
    - Field: hide
    Type: boolean
    Required: No
    Description: true to hide the node from the Endor Labs dependency graph

    `}
</YamlTable>

<Note>
  **depset requirement**

  The output file must be returned in a `depset` from the `endor_sca_info` output group. endorctl reads these depsets through BEP to construct the complete dependency tree.
</Note>

#### Bazel custom aspect example

The [Endor Labs aspects example repository](https://github.com/endorlabs/endor-aspects-it/blob/main/example/javascript/js_library.bzl) provides a complete custom aspect for JavaScript rules.

The example defines an `EndorDependencyInfo` provider that carries the metadata Endor Labs needs for each target: `original_label`, `purl`, `dependencies`, `internal`, `vendored`, and `hide`.

After defining the provider, it defines helper functions. `_get_dependency_list()` goes through each dependency attribute, and collects labels of targets that have an `endor_sca_info` output group. `_get_dependency_files()` collects the output files from those targets. `_get_sca_information()` resolves the package name and version from the rule context, and falls back to the target label and `ref` attribute when explicit metadata is not available.

The aspect implementation (`_impl`) extracts `deps`, `data`, `src`, and `srcs` from the rule attributes. It calls the helpers to build a list of dependency labels and collect transitive dependency files. It then constructs a PURL (for example, `pkg:npm/package-name@version`), populates the `EndorDependencyInfo` provider, and writes it to a JSON file using `json.encode_indent()`. Finally, it returns `OutputGroupInfo(endor_sca_info = depset([output_file] + dependency_files))`, combining the current target's output with all files from its transitive dependencies.

The aspect itself is defined as `endor_resolve_dependencies` with the mandatory attributes described in [Aspect attributes](#aspect-attributes).

endorctl reads the resulting depsets through the Build Event Protocol (BEP) to construct the complete dependency graph. These files must be available locally. endorctl ensures downloads when using remote execution or caching (see [Bazel aspect remote execution and caching](#bazel-aspect-remote-execution-and-caching)).
