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

# Pull Request scans

> Scan pull requests created in your repository.

export const Diagram = ({children}) => {
  const [svg, setSvg] = useState('');
  const [error, setError] = useState(null);
  const [mounted, setMounted] = useState(false);
  const [id] = useState(() => `diagram-${Math.random().toString(36).slice(2)}`);
  const DEFAULTS = {
    theme: 'base',
    fontSize: '14px',
    fontFamily: 'inherit',
    primaryColor: '#26D07C',
    primaryTextColor: '#000000',
    primaryBorderColor: '#059669',
    secondaryColor: '#e5e7eb',
    secondaryTextColor: '#000000',
    secondaryBorderColor: '#9ca3af',
    tertiaryColor: '#e5e7eb',
    tertiaryTextColor: '#000000',
    tertiaryBorderColor: '#9ca3af',
    lineColor: '#6b7280',
    background: '#ffffff',
    edgeLabelBackground: '#f9fafb',
    clusterBkg: '#f0fdf4',
    clusterBorder: '#059669',
    nodeTextColor: '#000000',
    endorColor: '#26D07C',
    endorBorder: '#059669',
    managedColor: '#A7F3D0',
    managedBorder: '#059669',
    externalColor: '#e5e7eb',
    externalBorder: '#9ca3af'
  };
  const VAR_LINE_RE = /^(\w+):\s*(.+)$/;
  const TOKEN_RE = /\{\{(\w+)\}\}/g;
  const parseVarsBlock = raw => {
    const vars = {};
    const lines = raw.trim().split('\n');
    const diagramLines = [];
    let inVars = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '%%vars') {
        inVars = true;
        continue;
      }
      if (inVars && trimmed === '%%') {
        inVars = false;
        continue;
      }
      if (inVars) {
        const m = VAR_LINE_RE.exec(trimmed);
        if (m) vars[m[1]] = m[2];
      } else {
        diagramLines.push(line);
      }
    }
    return {
      vars,
      diagramLines
    };
  };
  const buildInitConfig = merged => ({
    theme: merged.theme,
    themeVariables: {
      fontSize: merged.fontSize,
      fontFamily: merged.fontFamily,
      primaryColor: merged.primaryColor,
      primaryTextColor: merged.primaryTextColor,
      primaryBorderColor: merged.primaryBorderColor,
      secondaryColor: merged.secondaryColor,
      secondaryTextColor: merged.secondaryTextColor,
      secondaryBorderColor: merged.secondaryBorderColor,
      tertiaryColor: merged.tertiaryColor,
      tertiaryTextColor: merged.tertiaryTextColor,
      tertiaryBorderColor: merged.tertiaryBorderColor,
      lineColor: merged.lineColor,
      background: merged.background,
      edgeLabelBackground: merged.edgeLabelBackground,
      clusterBkg: merged.clusterBkg,
      clusterBorder: merged.clusterBorder,
      nodeTextColor: merged.nodeTextColor
    }
  });
  const renderWithMermaid = (mermaid, fullDiagram) => {
    mermaid.initialize({
      startOnLoad: false,
      zoom: {
        enabled: false
      }
    });
    mermaid.render(id, fullDiagram).then(({svg: rendered}) => {
      setSvg(rendered);
      setError(null);
    }).catch(err => setError(err.message));
  };
  useEffect(() => {
    setMounted(true);
  }, []);
  useEffect(() => {
    if (!mounted || !children) return;
    const raw = typeof children === 'string' ? children : String(children);
    const {vars, diagramLines} = parseVarsBlock(raw);
    const merged = {
      ...DEFAULTS,
      ...vars
    };
    const diagram = diagramLines.join('\n').trim().replaceAll(TOKEN_RE, (_, key) => merged[key] || '');
    const fullDiagram = `%%{init: ${JSON.stringify(buildInitConfig(merged))}}%%\n${diagram}`;
    const existing = document.getElementById(id);
    if (existing) existing.remove();
    if (globalThis.mermaid) {
      renderWithMermaid(globalThis.mermaid, fullDiagram);
    } else {
      const script = document.createElement('script');
      script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
      script.onload = () => renderWithMermaid(globalThis.mermaid, fullDiagram);
      script.onerror = () => setError('Failed to load Mermaid');
      document.head.appendChild(script);
    }
  }, [mounted, children, id]);
  if (!mounted) return null;
  if (error) {
    return <pre style={{
      color: '#dc2626',
      background: '#fef2f2',
      padding: '12px',
      borderRadius: '6px',
      fontSize: '13px',
      overflowX: 'auto'
    }}>
        Diagram error: {error}
      </pre>;
  }
  if (!svg) {
    return <div style={{
      height: '200px',
      background: '#f3f4f6',
      borderRadius: '8px',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      color: '#9ca3af',
      fontSize: '14px'
    }}>
        Loading diagram...
      </div>;
  }
  return <div dangerouslySetInnerHTML={{
    __html: svg
  }} style={{
    overflowX: 'auto',
    padding: '8px 0'
  }} />;
};

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

Scan pull requests as soon as they are raised in your repository. PR scans detect vulnerabilities in your branch when they are introduced, making it easier to identify and fix them early.

PR scans help you to:

* Detect new vulnerabilities as developers open or update PRs, instead of after merge.
* Gate merges based on security and compliance.
* Enforce policies that can block risky PRs or fail CI builds.
* Give developers fast feedback on open source, SAST, and secrets findings tied to their changes through PR comments and PR Runs in the Endor Labs UI.

Endor Labs supports scanning pull requests and merge requests to evaluate the impact of proposed changes before they are merged. You can run PR scans in the following ways:

* [Scan PRs using endorctl](#scan-prs-using-endorctl) to run pull request scans from the CLI. This approach supports scanning the pull request branch and scanning changes relative to a baseline branch.
* [Configure PR scans in SCM apps](#configure-pr-scans-in-scm-apps) to trigger scans when a pull request or merge request is opened or updated.
* [Configure PR scans in CI pipelines](#run-pr-scans-from-ci) to invoke endorctl as part of a CI pipeline in the pull request or merge request context.

## Pull request scan workflow

The following workflow describes a robust approach for scanning pull requests and merge requests against a stable baseline branch.

1. **Establish and maintain a baseline branch**

   Scan your baseline branch, such as main, regularly with monitoring scans or CI scans. See [Set a default branch](/scan/sca/scanning-strategies#set-a-default-branch) for how the default branch is chosen and used. See [Scanning strategies](/scan/sca/scanning-strategies) and [Branches and workflows](/best-practices/operational-best-practices) for more information on branch strategy, default branch setup, and recommended scan flags.

2. **Trigger PR scans on feature branches**

   Configure PR scans for PRs targeting the baseline branch, for example, `main`. For large monorepos, enable incremental PR scans to focus only on changed dependencies and code.

3. **Use policies to enforce standards**

   Use finding and action policies to decide when to warn, break builds, or block merges for PR scans.

4. **Integrate first-party scans**

   Optionally, for app-triggered scans, enable SAST and secrets in the SCM integration when you install or manage the app. For CI-triggered scans, include the appropriate endorctl flags and steps in your pipeline so each run covers dependencies, first-party code, and secrets as needed. To analyze first-party code changes with AI SAST on pull requests, see [AI SAST PR scans](/scan/ai-sast/ai-sast-pr-scans).

The following diagram shows how a pull request scan flows from the baseline to the merge decision.

<Diagram>
  {`
    flowchart TD
    A(["Scan the baseline branch"]) --> B["Developer opens or updates a PR"]
    B --> C["Incremental PR quick scan runs"]
    C --> D{"Build file changes<br/>for a language?"}
    D -->|No| E["Skip dependency resolution<br/>for that language"]
    D -->|Yes| F["Resolve dependencies<br/>for affected packages"]
    E --> G["Compare results against the baseline"]
    F --> G
    G --> H["Report only new findings"]
    H --> I["Post PR comments and<br/>apply action policies"]
    I --> J{"Policy violation?"}
    J -->|Yes| K["Break the build or block the merge"]
    J -->|No| L["Checks pass"]
    `}
</Diagram>

## Scan PRs using endorctl

You can scan pull requests or merge requests using endorctl for GitHub, GitLab, and Bitbucket. The `--pr` flag runs the scan for the current commit and records the results as PR Runs that do not affect main branch monitoring scans and reports. Endor Labs stores PR and MR scan findings in PR Runs for three weeks, after which they are removed to accommodate new PR scans.

Before you run PR scans, scan your baseline branch at least once so incremental PR scans have a baseline to compare against.

A PR scan command combines the following flags.

<YamlTable>
  {`
    - Flag: \`--namespace\`
    Required: Yes
    Description: The Endor Labs namespace that stores the scan results.
    - Flag: \`--pr\`
    Required: Yes
    Description: Records the results as a PR Run instead of a monitoring scan.
    - Flag: \`--pr-incremental\`
    Required: No
    Description: Scans only the packages and dependencies that changed relative to the baseline. Recommended for fast PR feedback. See [Perform incremental PR scan](#perform-incremental-pr-scan).
    - Flag: \`--quick-scan\`
    Required: No
    Description: Skips call graph generation, so findings are not annotated with reachability. With \`--pr-incremental\`, it also skips dependency resolution for unaffected languages and packages.
    - Flag: \`--scm-pr-id\`
    Required: No
    Description: The pull request or merge request number. Required when you set \`--enable-pr-comments\`.
    - Flag: \`--enable-pr-comments\`
    Required: No
    Description: Posts new findings as review comments and infers the baseline from the merge target of the PR. Do not set together with \`--pr-baseline\`.
    - Flag: \`--scm-token\`
    Required: No
    Description: The token endorctl uses to authenticate with your SCM to read PR metadata and post comments. You can also set it through \`ENDOR_SCAN_SCM_TOKEN\`.
    `}
</YamlTable>

The following sections explain how these flags work together. To go straight to the ready-to-run command, see [Set up PR scans step by step](#set-up-pr-scans-step-by-step).

## Perform incremental PR scan

An incremental PR scan scans only the parts of the codebase and dependencies that have changed since the last full baseline scan.

* Endor Labs identifies packages and dependencies in the PR and scans only those that changed relative to the baseline.
* If no dependencies changed, the scan is skipped, and Endor Labs reports `No changes found`.
* Incremental PR scans only report findings that do not exist in the baseline and are associated with changed dependencies in the PR.
* You can enable incremental PR scans using the `--pr-incremental` flag or the equivalent CI settings. This flag is also available for [SAST incremental scans](/scan/sast#sast-incremental-scans) and [Incremental secret scans](/scan/secrets#incremental-secret-scans).

You need to set a baseline for incremental PR scans so only findings new relative to that branch are reported. For GitHub App or GitLab App scans, or when PR comments are enabled, the baseline is detected automatically. Otherwise, pass `--pr-baseline` when you run the scan. See [Set a default branch](/scan/sca/scanning-strategies#set-a-default-branch) for how the default branch is chosen and used.

<Note>
  **Baseline mismatch in PR scans**

  If a finding is fixed in the baseline by upgrading or downgrading a dependency and a PR still modifies that package, the finding can be reported as new. To mitigate this, rebase the PR with the latest baseline content and re-run the PR check.
</Note>

### Publish findings as PR comments

Endor Labs can post new findings as review comments on the pull request or merge request. Comments are posted according to your action policies. To publish comments:

* Set `--enable-pr-comments` and `--scm-pr-id` so the scan posts comments to the right PR and infers the baseline from its merge target.
* Authenticate with `--scm-token` or the `ENDOR_SCAN_SCM_TOKEN` environment variable.
* Configure an action policy with Branch Type **Pull Request** so violations generate comments.

See [Pull Request comments](/scan/pr-scans/pr-comments) for app-based setup, the required action policy configuration, and comment templates.

### Skip unaffected languages during incremental PR scans

When you combine `--pr-incremental` with `--quick-scan`, Endor Labs inspects the changed files in the pull request before resolving dependencies for each language. If the pull request contains no build file changes for a language, Endor Labs skips dependency resolution for that language entirely. This behavior is enabled by default for all supported languages and ecosystems in endorctl v1.7.1002 and later.

The scan log records each skipped language.

```text theme={null}
Skip Java dependency resolution: No relevant changes detected
```

### Skip unaffected packages during incremental PR scans

When a pull request does change build files, Endor Labs can prune further and resolve dependencies only for the packages those files affect. This reduces incremental PR scan times in large repositories and monorepos.

Endor Labs classifies each changed build file by its scope of impact:

* A single package, such as `gradle.lockfile`.
* A package and every package under it, such as a parent `pom.xml`, `settings.gradle`, or `gradle.properties`.
* The entire repository, such as `gradle-wrapper.properties`, files under `buildSrc` or `.mvn`, and version catalogs like `gradle/libs.versions.toml`.

Packages that no changed file affects are not resolved again, and their results carry forward from the baseline scan.

Package-level skipping is enabled by default for Java and Kotlin projects that build with Maven or Gradle, in endorctl v1.7.1046 and later. Support for more ecosystems is rolling out in upcoming releases.

To turn off package-level skipping, set `ENDOR_SCAN_INCREMENTAL_DEP_RES=false` before you run the scan.

### Troubleshoot incremental PR scan performance

If an incremental PR scan takes longer than expected or does not skip dependency resolution, do the following checks:

1. Verify that endorctl is v1.7.1002 or later. Package-level skipping for Java and Kotlin requires v1.7.1046 or later.
2. Confirm that the scan sets both `--pr-incremental` and `--quick-scan`.
3. Confirm that the scan is a pull request scan. Scans tagged `merge-to-main` resolve all dependencies.
4. Check the scan log for `Detected N changed files`. An unexpectedly large count means the pull request diff could not be computed correctly.
5. Look for skip confirmations in the scan log, such as `Skip dependency resolution for 3/12 maven modules: No relevant changes detected`.

## Configure PR scans in SCM apps

The Endor Labs SCM integrations let you scan pull requests or merge requests when they are opened or updated. In the integration settings, enable PR or MR scans to run them automatically and, optionally, enable pull request comments to post findings as review comments. Action policies apply to PR scans the same way as to other scan types.

The following describe platform-specific setup and configuration options:

* [Endor Labs GitHub App PR scans](/setup-deployment/scm-integrations/github-app/github-app#configure-pr-scans-during-github-app-pro-installation)
* [GitLab App MR scans](/setup-deployment/scm-integrations/gitlab-app/gitlab-mr-scan)
* [Bitbucket Cloud App](/setup-deployment/scm-integrations/bitbucket-cloud/bitbucket-cloud-pr-scans)
* [Bitbucket Data Center App](/setup-deployment/scm-integrations/bitbucket-datacenter-app/bitbucket-datacenter-pr-scans)

## Run PR scans from CI

PR scans can be run from a continuous integration pipeline by invoking `endorctl scan` with pull request flags in jobs triggered by pull request or merge request events. This approach provides control over when scans run, supports posting findings as PR or MR comments, and allows policies to be enforced, such as failing builds or blocking merges. The scan profile assigned to the project determines the toolchains and environment used for the scan.

* See [CI scans](/setup-deployment/ci-cd) for platform-specific pipeline configuration.
* See [Pull request flags](/developers-api/cli/commands/scan#pull-request-ci-flags) for available flags.

## Block pull requests on findings

A PR scan reports findings, but it does not block a merge on its own.

To gate a pull request, ensure that you have the following configurations:

* An action policy that breaks the build when the findings you care about appear.
* A required status check in your source control manager that enforces the result before a merge.

The action policy decides which findings block a pull request, and the required status check is what stops the merge.

### Create an action policy that breaks the build

In Endor Labs, create an action policy and choose the **Break the Build** enforcement action. Select the policy template for the finding types you want to gate, such as SCA, secrets, or SAST. The policy applies to PR scans the same way it applies to other scans.

See [Create an action policy from template](/platform-administration/policies/action-policies#create-an-action-policy-from-template) for the full procedure.

### Configure the required status check in GitHub to block merges

GitHub blocks a merge only when the Endor Labs check is a required status check on the target branch. The check you require depends on how Endor Labs scans your pull requests. The check appears as a selectable option only after it has reported on a pull request within the last seven days. Open or update a pull request so the Endor Labs check runs once, then add it to your branch protection rule.

To add the check to a branch protection rule:

1. In your GitHub repository, select **Settings** > **Branches**.
2. Next to the rule for your default branch, click **Edit**.
3. Select **Require status checks to pass before merging**.
4. Enter the check name (**Endor Labs Automated Scan** for the GitHub App, or your workflow job name for GitHub Actions), and select it from the results.
5. Click **Save changes**.

To create a rule instead, click **Add branch protection rule**, enter a **Branch name pattern**, and follow the same steps.

GitHub also supports requiring status checks through repository and organization rulesets, which GitHub now recommends. Use an organization ruleset to enforce the check across many repositories at once. For more information, refer to [About rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets).

After you require the check, GitHub keeps the merge blocked until the check passes. A check that concludes as neutral or skipped does not block the merge, even when it is required. Endor Labs reports a neutral result when there is nothing to scan, such as a pull request with no relevant changes.

For more information, see [Managing a branch protection rule](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule) and [About rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets), and [Troubleshooting required status checks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks) in the GitHub documentation.

## Ignore files in PR scans

Ignore files let you dismiss findings by committing a file in your repository as part of a pull or merge request. During a PR scan, Endor Labs applies the ignore file from the repository version being scanned. Findings that match entries in the ignore file are excluded from PR Runs, do not appear in [Pull Request comments](#pull-request-comments), and do not trigger action policies.

<Note>
  **Tenant setting required for ignore files**

  You must [allow ignore files to dismiss findings](/platform-administration/configure-system-settings#allow-ignore-files-to-dismiss-findings) in **Settings** > **SYSTEM SETTINGS** > **Developer Workflows** for scans to process ignore files.
</Note>

To add or update entries, use [`endorctl ignore`](/developers-api/cli/commands/ignore) and validate the file with [`endorctl validate ignore`](/developers-api/cli/commands/validate/ignore). See [Dismiss findings using an ignore file](/inventory-insights/findings#dismiss-using-an-ignore-file) for more details on ignore file format and structure.

## Scan profiles for PR scans

A scan profile defines the configuration applied to PR scans for a project, including languages, toolchains, path filters, and parameters such as `enable_automated_pr_scans` and `enable_pr_comments`. For CI-initiated PR scans, the scan profile determines the toolchains and environment configuration used to execute the scan.

<Note>
  App-triggered PR scans run only when both of the following are true.

  * Pull Request scans or Merge Request scans are enabled during SCM app installation so the app receives PR or MR events.
  * **Pull request scans** or `enable_automated_pr_scans` is enabled in the scan profile assigned to the project.
</Note>

To scope app-triggered PR scans to selected projects, enable **Pull request scans** only in the scan profiles assigned to those projects. In GitLab, MR scans can alternatively be scoped by configuring merge request webhooks for selected projects. See [Configure scan profile through the UI](/scan/scan-profiles/configure-scanprofile-ui#configure-general-scan-profile-settings) for more information on scan profile settings.

## Pull Request comments

PR comments are automated comments posted on pull or merge requests when Endor Labs detects policy violations during a PR scan. Enable them in your SCM integration or using `--enable-pr-comments` in CI, then configure an action policy with Branch Type **Pull Request**. See [Pull Request comments](/scan/pr-scans/pr-comments) and [Action policies](/platform-administration/policies/action-policies) for setup and configuration.

## Set up PR scans step by step

Add the following flags to your scan command in order, and verify each step in your CI pipeline before adding the next.

<Steps>
  <Step title="Start with a PR scan">
    Run a PR scan on the current commit. Endor Labs records the results as a PR Run instead of a monitoring scan.

    ```bash theme={null}
    endorctl scan --namespace <your-namespace> --pr
    ```
  </Step>

  <Step title="Scan only what changed">
    Add `--pr-incremental` and point `--pr-baseline` at the branch the PR merges into. The scan reports only findings that are new relative to the baseline.

    ```bash theme={null}
    endorctl scan --namespace <your-namespace> --pr --pr-incremental --pr-baseline=main
    ```
  </Step>

  <Step title="Speed up the scan">
    Add `--quick-scan` to skip call graph generation and skip dependency resolution for languages and packages the PR does not affect.

    ```bash theme={null}
    endorctl scan --namespace <your-namespace> --pr --pr-incremental --pr-baseline=main --quick-scan
    ```
  </Step>

  <Step title="Post findings as PR comments">
    Add `--enable-pr-comments` with `--scm-pr-id` and an SCM token. Remove `--pr-baseline` because the scan now infers the baseline from the merge target of the PR. This is the recommended command.

    ```bash theme={null}
    export ENDOR_SCAN_SCM_TOKEN=<your-scm-token>
    endorctl scan --namespace <your-namespace> --pr --pr-incremental --quick-scan \
      --scm-pr-id <pr-or-mr-id> --enable-pr-comments
    ```
  </Step>

  <Step title="Block merges on findings">
    The scan reports findings and posts comments, but it does not block merges on its own. Pair the command with an action policy that breaks the build and a required status check on your target branch. See [Block pull requests on findings](#block-pull-requests-on-findings).
  </Step>

  <Step title="Optional: Point to GitHub Enterprise Server">
    If your organization runs GitHub Enterprise Server, add `--github-api-url` with the API URL of your server.

    ```bash theme={null}
    endorctl scan --namespace <your-namespace> --pr --pr-incremental --quick-scan \
      --scm-pr-id <pr-or-mr-id> --enable-pr-comments --github-api-url=<your-github-api-url>
    ```
  </Step>
</Steps>

### Adjust the command for your setup

* **Run without PR comments**: Stop after step 3. Keep `--pr-baseline` and do not set `--enable-pr-comments` or `--scm-pr-id`.
* **Include reachability analysis**: Drop `--quick-scan` to resolve dependencies fully and generate call graphs. The scan takes longer, but findings include reachability information.

## View PR scan findings

PR scan findings are stored as **PR Runs** and kept for three weeks to accommodate new PR activity.

To view PR scan results in the Endor Labs:

1. Select **Projects** from the left sidebar.
2. Search for and select your project from the list.
3. Select **PR RUNS** to review the past scans.

   **PR Runs** captures the commit ID, **Commit SHA**, the referenced branch, its findings, and the tags added to the scan as configured in the policies.

   Select the specific PR scan to view its findings in detail. You can view the scan metadata, severity summary, and open any scan for findings, issues, and logs.

<img src="https://mintcdn.com/endorlabs-b4795f4f/Uc3T4mPoaFbRUPbf/images/scan/pr-scans/pr-runs.webp?fit=max&auto=format&n=Uc3T4mPoaFbRUPbf&q=85&s=9acc5ab7d60669791b11aa30fa84e8d5" alt="PR scan results in PR Runs" style={{width: '80%'}} width="2226" height="1368" data-path="images/scan/pr-scans/pr-runs.webp" />

## FAQs

<AccordionGroup>
  <Accordion title="How long are PR scan results retained?">
    Endor Labs retains PR scan findings as PR Runs for three weeks.
  </Accordion>

  <Accordion title="Why does a finding appear as new when it was fixed in the baseline?">
    Endor Labs reports a finding as new if the baseline includes a fix for the issue and the pull request modifies the affected package. To resolve the finding, rebase the pull request on the latest baseline and re-run the PR scan.
  </Accordion>

  <Accordion title="Why do two PR scans on the same pull request produce different results?">
    Results can differ when the PR or baseline branch changes, the scan profile is updated, or the vulnerability database is updated between runs. PR Runs store the findings produced at the time of execution. Subsequent PR scans evaluate the pull request using the current scan profile and the latest analysis data.
  </Accordion>

  <Accordion title="Do PR scans block merges automatically?">
    No, PR scans do not block merges by default. Blocking requires an action policy that breaks the build and a required status check that enforces the result. See [Block pull requests on findings](#block-pull-requests-on-findings) for the steps.
  </Accordion>

  <Accordion title="Why does an incremental PR scan fall back to a full scan?">
    Endor Labs falls back to a full PR scan when no valid baseline exists for the project or when it cannot determine what changed relative to the baseline. Ensure the baseline branch has been scanned at least once before relying on incremental scans. See [Troubleshoot incremental PR scan performance](#troubleshoot-incremental-pr-scan-performance) for more checks.
  </Accordion>
</AccordionGroup>
