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

# Automated Pull Requests

> Automatically generate pull requests with dependency upgrades and security fixes.

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 renderSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" fill="currentColor" />
    </svg>;
  const renderFailureIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path fillRule="evenodd" clipRule="evenodd" d="M11.9902 1.98633C12.5208 1.9864 13.0426 2.12743 13.501 2.39453C13.9581 2.66107 14.3372 3.04388 14.5986 3.50391L22.5967 17.5L22.6895 17.6738C22.8921 18.0851 22.9979 18.5387 22.9981 18.999C22.9981 19.5253 22.8605 20.0431 22.5977 20.499C22.3348 20.9549 21.9555 21.3332 21.5 21.5967C21.0445 21.8601 20.5272 21.9994 20.001 22H4.00001C3.47453 22.0031 2.95699 21.868 2.50001 21.6084C2.04032 21.3471 1.65712 20.9684 1.39063 20.5117C1.12431 20.055 0.983643 19.5355 0.982429 19.0068C0.981281 18.4793 1.11967 17.9611 1.38282 17.5039L9.38184 3.50391C9.64344 3.04359 10.023 2.66111 10.4805 2.39453C10.9388 2.12755 11.4598 1.98636 11.9902 1.98633ZM12 16.9004C11.3925 16.9004 10.9004 17.3925 10.9004 18C10.9004 18.6075 11.3925 19.0996 12 19.0996H12.0098C12.6173 19.0996 13.1104 18.6075 13.1104 18C13.1104 17.3925 12.6173 16.9004 12.0098 16.9004H12ZM12 5.90039C11.3925 5.9004 10.9004 6.39249 10.9004 7V13C10.9004 13.6075 11.3925 14.0996 12 14.0996C12.6075 14.0996 13.0996 13.6075 13.0996 13V7C13.0996 6.39249 12.6075 5.90039 12 5.90039Z" fill="currentColor" />
    </svg>;
  const renderPartialSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM12 5C13.8565 5 15.6374 5.73705 16.9502 7.0498C18.263 8.36256 19 10.1435 19 12C19 13.8565 18.263 15.6374 16.9502 16.9502C15.6374 18.263 13.8565 19 12 19C11.4477 19 11 18.5523 11 18V6C11 5.73478 11.1054 5.4805 11.293 5.29297C11.4805 5.10543 11.7348 5 12 5Z" fill="currentColor" />
    </svg>;
  const renderPendingIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
      <path d="M10.1 2.182a10 10 0 0 1 3.8 0" />
      <path d="M13.9 21.818a10 10 0 0 1-3.8 0" />
      <path d="M17.609 3.721a10 10 0 0 1 2.69 2.7" />
      <path d="M2.182 13.9a10 10 0 0 1 0-3.8" />
      <path d="M20.279 17.609a10 10 0 0 1-2.7 2.69" />
      <path d="M21.818 10.1a10 10 0 0 1 0 3.8" />
      <path d="M3.721 6.391a10 10 0 0 1 2.7-2.69" />
      <path d="M6.391 20.279a10 10 0 0 1-2.69-2.7" />
    </svg>;
  const renderRunningIcon = () => <svg className="yt-status-running-svg" viewBox="22 22 44 44" width="100%" height="100%" aria-hidden="true">
      <circle className="yt-status-running-circle" cx="44" cy="44" r="20.2" fill="none" stroke="currentColor" strokeWidth="3.6" />
    </svg>;
  const renderSkippedIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <circle cx="12" cy="12" r="10" />
      <path d="M8 12h8" />
    </svg>;
  const STATUS_ICON_DEFS = [{
    re: /^-status-success-$/i,
    colorClass: 'yt-status-color-success',
    label: 'Success',
    render: renderSuccessIcon
  }, {
    re: /^-status-failure-$/i,
    colorClass: 'yt-status-color-failure',
    label: 'Failure',
    render: renderFailureIcon
  }, {
    re: /^-status-partial-success-$/i,
    colorClass: 'yt-status-color-partial',
    label: 'Partial success',
    render: renderPartialSuccessIcon
  }, {
    re: /^-status-pending-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Pending',
    render: renderPendingIcon
  }, {
    re: /^-status-running-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Running',
    render: renderRunningIcon
  }, {
    re: /^-status-skipped-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Skipped',
    render: renderSkippedIcon
  }];
  const renderStatusBadge = def => <span className={['yt-status-icon', def.colorClass].join(' ')} role="img" aria-label={def.label} title={def.label}>
      {def.render()}
    </span>;
  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>;
    const statusBadge = STATUS_ICON_DEFS.find(def => def.re.test(text));
    if (statusBadge) return renderStatusBadge(statusBadge);
    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);
  const scrollWrapRef = useRef(null);
  const [isScrollable, setIsScrollable] = 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]);
  useEffect(() => {
    const wrap = scrollWrapRef.current;
    if (!wrap) return undefined;
    const updateScrollable = () => {
      setIsScrollable(wrap.scrollWidth > wrap.clientWidth);
    };
    updateScrollable();
    const observer = new ResizeObserver(updateScrollable);
    observer.observe(wrap);
    return () => observer.disconnect();
  }, [mounted, data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <div ref={scrollWrapRef} style={{
    overflowX: 'auto',
    margin: '1.75rem 0'
  }} role={isScrollable ? 'region' : undefined} aria-label={isScrollable ? 'Scrollable table' : undefined} tabIndex={isScrollable ? 0 : undefined}>
      <table style={{
    display: 'table',
    width: '100%',
    minWidth: '100%',
    margin: 0
  }}>
        <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>
    </div>;
};

You can set up Remediation PRs in your GitHub environment if you use the [Endor Labs GitHub App (Pro)](/setup-deployment/scm-integrations/github-app) or the [Endor Labs GitHub Enterprise Server App](/setup-deployment/scm-integrations/github-app/github-enterprise-app). When Remediation PRs are set up, Endor Labs creates a PR to update the manifest files with dependency version upgrades, based on a remediation policy, to address vulnerability findings.

<Warning>
  You cannot have both the GitHub App and the GitHub App (Pro) simultaneously in your environment. When you migrate from one app to the other, select the same set of repositories as before to preserve the currently scanned projects and vulnerability findings after the migration.
</Warning>

Your tenant must have the upgrades and remediation feature for Remediation PRs to function.

## Understanding Remediation PRs

If Endor Labs identifies any fixes that address vulnerability findings according to the remediation policy in the next scan, it creates a pull request in GitHub with the details of the patch. You can merge the PR after review to fix the vulnerability findings.

Endor Labs updates the PR if there is a recommendation change in [upgrade impact analysis](/risk-remediation/upgrade-impact-analysis). If there are any changes in the vulnerability findings, Endor Labs updates the PR description. If there is new patch version available, Endor Labs closes the existing PR with comments and opens a new PR. If you resolve the notification in Endor Labs, the PR is closed with a comment.

Endor Labs does not further update the PR in the following scenarios, if you:

* Add a commit to the PR
* Close the PR
* Delete the PR branch
* Dismiss the notification in Endor Labs

## Set up Remediation PRs

Complete the following tasks to set up automated PR.

1. Install and enable SCA scanner using either [GitHub App (Pro)](/setup-deployment/scm-integrations/github-app) or [GitHub Enterprise Server App](/setup-deployment/scm-integrations/github-app/github-enterprise-app).
2. [Create a GitHub PR for remediations notification integration.](#create-a-github-pr-for-remediations-notification-integration)
3. [Create a remediation policy with the notification integration that you created in the previous step.](/platform-administration/policies/remediation-policies)

   The following image shows an example of a remediation policy that targets projects with the tag `java` and automatically raises a PR when remediation is found for reachable dependencies that resolve critical and high issues with low upgrade risk.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/Uc3T4mPoaFbRUPbf/images/risk-remediation/automated-pull-requests/auto-pr-remediation-policy.webp?fit=max&auto=format&n=Uc3T4mPoaFbRUPbf&q=85&s=aa65970cec540e09535aa813d667ed6c" alt="Remediation Policy 1" style={{width: '70%'}} width="1546" height="1542" data-path="images/risk-remediation/automated-pull-requests/auto-pr-remediation-policy.webp" />

## Remediation PRs support

Remediation PRs are supported for Java (with Gradle or Maven), Go (version 1.18 and higher), Python, .NET, and JavaScript.

#### Remediation PRs support matrix

<YamlTable>
  {`


    - Ecosystem: Python
    Package_manager: pip
    Build_files: \`pyproject.toml\`, \`requirements.txt\`
    Automated_PR_remediation: -yes-
    - Ecosystem:
    Package_manager: pip
    Build_files: \`setup.py\`, \`setup.cfg\`
    Automated_PR_remediation: -no-
    - Ecosystem:
    Package_manager: Poetry
    Build_files: \`pyproject.toml\`, \`poetry.lock\`
    Automated_PR_remediation: -no-
    - Ecosystem:
    Package_manager: PDM
    Build_files: \`pyproject.toml\`, \`pdm.lock\`
    Automated_PR_remediation: -no-
    - Ecosystem: Java
    Package_manager: Maven / Gradle
    Build_files: \`pom.xml\`, \`build.gradle\`, \`build.gradle.kts\`
    Automated_PR_remediation: -yes-
    - Ecosystem: Go
    Package_manager: Go modules
    Build_files: \`go.mod\`, \`go.sum\`
    Automated_PR_remediation: -yes-
    - Ecosystem: JavaScript
    Package_manager: npm / yarn / pnpm
    Build_files: \`package.json\`, \`package-lock.json\`, \`yarn.lock\`, \`pnpm-lock.yaml\`
    Automated_PR_remediation: -yes-
    - Ecosystem: .NET
    Package_manager: NuGet
    Build_files: \`*.csproj\`, \`packages.lock.json\`
    Automated_PR_remediation: -yes-


    `}
</YamlTable>

#### Limitations of Remediation PRs

Currently, Remediation PRs have the following limitations:

* Maven projects that use `dependencyManagement` tags and rely solely on dependency information in the parent pom file are not supported.
* Gradle projects with convention files (Groovy files with `.gradle` extension with any name) are not supported.
* Gradle projects with resource catalogues (version defined in `.toml` files) are not supported.
* * Gradle projects that use Spring Framework plugins, such as the Spring Boot Gradle plugin, to manage dependency versions are not supported. These plugins handle versioning internally, so dependency versions are not explicitly declared in the Gradle manifest file.
* Go projects that use the `replace` directive in `go.mod` are not supported. `replace` directives are commonly used for local development, debugging, or patching dependencies.
* JavaScript projects using npm and Yarn workspaces are not supported.
* .NET projects using `Directory.Packages.props` (central dependency management) or `packages.config`, are not supported.
* Dependency names are case sensitive.
* Updates to .NET dependencies with wildcard characters in their names are not supported.

## Create a GitHub PR for remediations notification integration

Remediation notification integration allows Endor Labs to get a notification from GitHub regarding pull requests. The notification alerts the GitHub App to perform Remediation PRs.

1. Select **User menu** > **Integrations** from the left sidebar.

2. Under **Notifications**, click **Add** for **GitHub PR for Remediations**.

3. Click **Add Notification Integration**.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/Uc3T4mPoaFbRUPbf/images/risk-remediation/automated-pull-requests/automate-pr-create-notification.webp?fit=max&auto=format&n=Uc3T4mPoaFbRUPbf&q=85&s=68d96761917f4bdba4187c6cc1334465" alt="Add GitHub PR for Remediation" style={{width: '70%'}} width="1280" height="564" data-path="images/risk-remediation/automated-pull-requests/automate-pr-create-notification.webp" />

4. Enter a name and description for this integration.

5. Select **Enable GitHub PR Notification Integration for Remediations**.

6. Optionally, select **Propagate this notification target to all child namespaces** so that the notification integration applies to all child namespaces.

7. Click **Add Notification Integration**.

## View remediation PRs in GitHub

Endor Labs automatically generates pull requests in GitHub repositories for dependency upgrades and security fixes. Each PR contains version changes, vulnerability details, and compatibility analysis.

To view the remediation PRs:

1. Navigate to your GitHub repository.

2. Click **Pull Requests** to view all the remediation PRs in the repository.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/2CFeZIRm7eKUPEq0/images/risk-remediation/automated-pull-requests/remediation-pr.webp?fit=max&auto=format&n=2CFeZIRm7eKUPEq0&q=85&s=67f26b7e4f63bfb596ffa48173cae23f" alt="GitHub Remediation PRs" style={{width: '70%'}} width="2615" height="1904" data-path="images/risk-remediation/automated-pull-requests/remediation-pr.webp" />

3. Click on a PR to view it's details.
   * Select **Conversation** to view the version changes, security impact, fixed vulnerabilities, and potential risks, providing context to assess the upgrade.

     <img src="https://mintcdn.com/endorlabs-b4795f4f/2CFeZIRm7eKUPEq0/images/risk-remediation/automated-pull-requests/remediation-pr-description.webp?fit=max&auto=format&n=2CFeZIRm7eKUPEq0&q=85&s=edcad4e52fd1e844a8c3a6ecd61e403e" alt="PR description" style={{width: '70%'}} width="1928" height="1306" data-path="images/risk-remediation/automated-pull-requests/remediation-pr-description.webp" />

   * Select **Files changed** to view the changes made to the manifest files.

     <img src="https://mintcdn.com/endorlabs-b4795f4f/2CFeZIRm7eKUPEq0/images/risk-remediation/automated-pull-requests/remediation-pr-files-changed.webp?fit=max&auto=format&n=2CFeZIRm7eKUPEq0&q=85&s=793a8276ee75c9f6586460f8262919bb" alt="files changed in the PR" style={{width: '70%'}} width="2380" height="1010" data-path="images/risk-remediation/automated-pull-requests/remediation-pr-files-changed.webp" />
