> ## 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.

# Pre-computed Reachability analysis

> Pragmatically assess vulnerability reachability in transitive dependencies without builds. Filter out irrelevant vulnerabilities and focus your team's time.

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>;
};

## Introduction

Modern applications rely on open-source dependencies, with most vulnerabilities (approximately **95%** according to [Endor Labs' State of Dependency Management report](https://www.endorlabs.com/state-of-dependency-management)) existing in transitive (indirect) dependencies rather than direct ones. This means most security risks come from packages you didn't explicitly add to your project, making it challenging to assess which vulnerabilities actually pose a threat to your application.

**Pre-computed reachability** is a pragmatic analysis technique that enables you to assess whether vulnerabilities in transitive dependencies could be reachable from your direct dependencies, all without requiring code compilation or builds. It serves two primary purposes: as a fallback mechanism for full scans when builds fail, and as an optional enhancement for quick scans when you want reachability analysis without build requirements. This analysis technique filters out vulnerabilities that affect functions never called within your dependency chain, helping you focus your team's time and attention on the security issues that truly matter.

The benefits of pre-computed reachability include:

* **Pragmatic and easy**: Results are pre-computed and cached, making it a practical approach to reachability analysis. It works solely from your manifest files (for example, `package.json`, `go.mod`, `pom.xml`, `build.gradle`, and others), requiring no access to your application's built artifacts.
* **Excellent noise reduction**: By analyzing how your direct dependencies interact with transitive dependencies, pre-computed reachability can filter out a significant portion of irrelevant vulnerabilities, enabling you to focus your team's time on the security issues that truly matter.
* **Build-independent**: Works without requiring builds or compilation, making it a pragmatic option when you want reachability analysis without build setup.
* **Reliable fallback for full scans**: When builds fail or call graph generation isn't possible, pre-computed reachability ensures you still get actionable security insights rather than missing out on reachability analysis entirely.

## Getting Started

**Pre-computed reachability is automatically used as the default fallback for full scans** when builds fail or call graph generation isn't possible, ensuring you always get reachability insights. **For quick scans, pre-computed reachability is optional** and can be enabled using the `ENDOR_SCAN_ENABLE_PRECOMPUTED_CALLGRAPHS` flag.

To enable pre-computed reachability analysis, use the `ENDOR_SCAN_ENABLE_PRECOMPUTED_CALLGRAPHS` flag:

```bash theme={null}
export ENDOR_SCAN_ENABLE_PRECOMPUTED_CALLGRAPHS=true
```

The system uses pre-computed reachability analysis in the following scenarios:

* **Full Scan**: When you run `endorctl scan`, the system attempts full call graph generation first for maximum precision. If the build fails or call graph generation isn't possible, it automatically falls back to pre-computed reachability by default, ensuring you still get valuable reachability insights.

* **Quick Scan**: When you run `endorctl scan --quick-scan`, you can optionally enable pre-computed reachability analysis by setting the `ENDOR_SCAN_ENABLE_PRECOMPUTED_CALLGRAPHS` flag. This provides reachability insights without requiring builds.

Endor Labs supports pre-computed reachability for the following languages: `java`, `javascript`, `typescript`, `kotlin`, `python`, `scala`, and `C#`.

### Scan modes and pre-computed reachability

Endor Labs supports multiple scan modes that can utilize pre-computed reachability analysis. For full scans, pre-computed reachability is automatically used as the default fallback when needed. For quick scans, it is optional and requires the flag to be enabled.

The following table summarizes how pre-computed reachability is used across different scan modes:

<YamlTable>
  {`


    - Scan_type: Quick scan
    Reachability_analysis_type: None
    Reachability_Coverage: None
    Call_graph_generated: -no-

    - Scan_type: Quick scan with pre-computed flag
    Reachability_analysis_type: Pre-computed
    Reachability_Coverage: Transitive dependencies
    Call_graph_generated: -no-

    - Scan_type: Full scan
    Reachability_analysis_type: Full dependency-level and function-level. Automatically falls back to pre-computed reachability if build fails
    Reachability_Coverage: Direct and transitive dependencies
    Call_graph_generated: -yes-

    - Scan_type: Full scan with pre-computed flag
    Reachability_analysis_type: Dependency-level and function-level. If the code build fails, it falls back to pre-computed reachability
    Reachability_Coverage: Direct and transitive dependencies
    Call_graph_generated: -yes-


    `}
</YamlTable>

## How Pre-computed Reachability Works

Pre-computed reachability analysis evaluates transitive dependencies using a simple but effective approach: it assumes that everything in your direct dependencies is reachable, then uses that assumption to assess whether vulnerabilities in transitive dependencies could be called.

### From Manifest to Dependency Graph

The analysis begins with your manifest files (`package.json`, `go.mod`, `Gemfile`, `pom.xml`, `build.gradle`, and others). Endor Labs resolves your dependencies to build a complete list of direct and transitive dependencies—along with the dependency graph that connects them. **Dependency resolution is required to identify all transitive dependencies, but no build or compilation is necessary.**

### Computing Vulnerability Reachability

For every detected CVE in a transitive dependency, the analysis works as follows:

1. **Assume direct dependencies are fully reachable**: Pre-computed reachability assumes that all functions and code in your direct dependencies are reachable. This includes both open-source dependencies and private, non-open source first-party libraries. This conservative assumption ensures comprehensive coverage without needing to analyze your application's source code.

2. **Assess transitive dependency reachability**: Using pre-computed call graph information from open-source dependencies, the system analyzes how your direct dependencies interact with their transitive dependencies. If a direct dependency can call a vulnerable function in a transitive dependency, that vulnerability is marked as reachable.

3. **Filter unreachable vulnerabilities**: If a vulnerable function in a transitive dependency cannot be reached through any of your direct dependencies (based on the pre-computed call graph information), the CVE is marked as unreachable and can be deprioritized.

The key insight is that pre-computed reachability leverages the fact that Endor Labs has already analyzed how open-source packages interact with their dependencies. By assuming your direct dependencies are fully reachable and using this pre-computed information, the analysis can determine which transitive dependency vulnerabilities could be reachable without needing to build your application or analyze your source code.

### Reachable vs Unreachable CVEs

Based on the analysis:

* **Unreachable CVEs**: If a vulnerable function in a transitive dependency cannot be reached through any of your direct dependencies (based on pre-computed call graph information), the CVE is marked as unreachable. These can be deprioritized, as the vulnerable code cannot be invoked through your dependency chain.

* **Reachable CVEs**: If a vulnerable function in a transitive dependency can be reached through one or more of your direct dependencies, the vulnerability is flagged as *reachable*. For reachable CVEs, Endor Labs shows the list of function calls in your dependencies that may trigger the vulnerability.

Pre-computed reachability assumes all direct dependencies are reachable, so if a direct dependency can call a vulnerable function in a transitive dependency, that vulnerability is marked as reachable. Pre-computed reachability focuses on analyzing dependency relationships rather than your application's source code usage. For the most precise assessment when builds are successful, [function-level reachability analysis](/scan/sca/reachability-analysis#function-level-reachability) analyzes your application's source code directly through full call graph generation to determine actual usage.

<Note>
  Pre-computed reachability analyzes how your direct dependencies interact with transitive dependencies, but does not analyze direct calls to transitive dependencies from your application code. For complete coverage including direct usage of transitive dependencies, [function-level reachability analysis](/scan/sca/reachability-analysis#function-level-reachability) provides full analysis when builds are successful.
</Note>

## When Pre-computed Reachability is Used

Pre-computed reachability serves two distinct purposes:

### As a Fallback for Full Scans

For full scans, pre-computed reachability automatically serves as a fallback when:

* **Builds fail**: If your project build fails or encounters errors, pre-computed reachability ensures you still get reachability analysis rather than missing out entirely.
* **Call graph generation isn't possible**: In cases where full call graph generation isn't available, pre-computed reachability provides valuable reachability insights.

### As an Option for Quick Scans

For quick scans, you can proactively enable pre-computed reachability when you want reachability analysis without build requirements. This pragmatic approach provides vulnerability assessment based on manifest files alone, making it ideal for:

* **Build-free analysis**: When you want reachability insights without setting up build environments or waiting for compilation.
* **CI/CD pipelines**: In environments where you want reachability analysis without build dependencies.
* **Large-scale scanning**: When scanning multiple repositories or projects, where build setup isn't practical.

The philosophy behind pre-computed reachability is simple: don't let perfect get in the way of better. When full call graph analysis isn't possible or when you want a pragmatic approach without build requirements, pre-computed reachability ensures you still get actionable security insights rather than no analysis at all.

## Comparison with Other Reachability Analysis Types

Pre-computed reachability complements Endor Labs' other reachability analysis capabilities:

* **[Function-level reachability](/scan/sca/reachability-analysis#function-level-reachability)**: The most precise analysis that examines your application's source code directly through full call graph generation. This is the primary and preferred method when builds succeed, providing the highest accuracy for production applications.
* **[Pre-computed reachability](/scan/sca/reachability-analysis/pre-computed-reachability)**: A pragmatic, manifest-based analysis that works without builds or source code access. Serves as an automatic fallback for full scans when builds fail, and can be enabled for quick scans when you want reachability analysis without build requirements. Focuses on transitive dependencies.

Endor Labs automatically uses the best available analysis method for your situation. When full call graph generation is possible, it's used for maximum precision. When it's not, pre-computed reachability steps in as a reliable fallback, ensuring you always get actionable security insights.

## Limitations

### Direct Dependency Vulnerabilities

Pre-computed reachability analysis is optimized for vulnerabilities in transitive/indirect dependencies, where it can leverage the analysis of how your direct dependencies interact with their dependencies. When full call graph generation is possible, [function-level reachability analysis](/scan/sca/reachability-analysis#function-level-reachability) provides more precise results by analyzing your application's source code directly.
