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

# GitLab App MR scans

> Learn how to enable MR scans using the GitLab App.

export const CodeFile = ({src, lang, expandable = true, maxLines = 15}) => {
  const [content, setContent] = useState(null);
  const [wrapperLang, setWrapperLang] = useState('text');
  const [error, setError] = useState('');
  const [copied, setCopied] = useState(false);
  const [isDark, setIsDark] = useState(false);
  const [expanded, setExpanded] = useState(false);
  useEffect(() => {
    const root = document.documentElement;
    setIsDark(root.classList.contains('dark'));
    const observer = new MutationObserver(() => {
      setIsDark(root.classList.contains('dark'));
    });
    observer.observe(root, {
      attributes: true,
      attributeFilter: ['class']
    });
    return () => {
      observer.disconnect();
    };
  }, []);
  useEffect(() => {
    if (!src) {
      setError('No src provided.');
      return undefined;
    }
    const ac = new AbortController();
    fetch(src, {
      signal: ac.signal
    }).then(r => {
      if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
      return r.json();
    }).then(data => {
      setContent(data.content || '');
      if (data.lang) setWrapperLang(data.lang);
    }).catch(err => {
      if (err.name !== 'AbortError') {
        setError('Unable to load ' + src + ': ' + err.message);
      }
    });
    return () => {
      ac.abort();
    };
  }, [src]);
  const language = lang || wrapperLang || 'text';
  const handleCopy = () => {
    if (!content) return;
    navigator.clipboard.writeText(content).then(() => {
      setCopied(true);
      setTimeout(() => {
        setCopied(false);
      }, 2000);
    }).catch(() => {});
  };
  const toggleExpand = () => {
    setExpanded(v => !v);
  };
  const statusBoxStyle = variant => {
    const base = {
      padding: '12px 16px',
      borderRadius: '6px',
      fontSize: '14px'
    };
    if (variant === 'error') {
      return {
        ...base,
        background: isDark ? '#451a1a' : '#fef2f2',
        color: isDark ? '#fca5a5' : '#dc2626',
        border: '1px solid ' + (isDark ? '#7f1d1d' : '#fecaca')
      };
    }
    return {
      ...base,
      background: isDark ? '#1f2937' : '#f5f5f5',
      color: isDark ? '#9ca3af' : '#6b7280'
    };
  };
  const shellStyle = {
    position: 'relative',
    borderRadius: '8px',
    border: '1px solid ' + (isDark ? 'rgba(255,255,255,0.1)' : '#d1d5db'),
    overflow: 'hidden'
  };
  const copyBtnStyle = {
    position: 'absolute',
    top: '8px',
    right: '8px',
    padding: '4px 8px',
    fontSize: '12px',
    lineHeight: '1',
    border: '1px solid rgba(255,255,255,0.3)',
    borderRadius: '4px',
    background: 'rgba(255,255,255,0.15)',
    color: '#fff',
    cursor: 'pointer',
    zIndex: 2,
    transition: 'background 0.15s, color 0.15s'
  };
  const renderErrorBox = () => <div style={statusBoxStyle('error')}>{error}</div>;
  const renderLoadingBox = () => <div style={statusBoxStyle('loading')}>Loading…</div>;
  const countLogicalLines = text => {
    const normalized = text.replace(/\n+$/, '');
    if (normalized === '') {
      return 0;
    }
    return normalized.split('\n').length;
  };
  const lineCount = content ? countLogicalLines(content) : 0;
  const showTruncate = expandable && lineCount >= maxLines && !expanded;
  const showExpandToggle = expandable && lineCount >= maxLines;
  const LINE_HEIGHT_PX = 25;
  const previewMaxHeightPx = maxLines * LINE_HEIGHT_PX;
  const preStyle = {
    margin: 0,
    padding: '16px',
    overflow: 'auto',
    maxHeight: showTruncate ? previewMaxHeightPx + 'px' : 'none',
    overflowY: showTruncate ? 'hidden' : 'auto'
  };
  const toggleLinkStyle = {
    display: 'block',
    width: '100%',
    boxSizing: 'border-box',
    background: 'none',
    border: 'none',
    padding: '8px 16px 12px 16px',
    cursor: 'pointer',
    color: '#26D07C',
    fontSize: '13px',
    fontFamily: 'inherit',
    textAlign: 'left'
  };
  const renderLoaded = () => <div style={shellStyle}>
      <button type="button" onClick={handleCopy} aria-label="Copy code" style={copyBtnStyle} onMouseEnter={e => {
    e.currentTarget.style.background = 'rgba(255,255,255,0.25)';
  }} onMouseLeave={e => {
    e.currentTarget.style.background = 'rgba(255,255,255,0.15)';
  }}>
        {copied ? 'Copied!' : 'Copy'}
      </button>
      <pre style={preStyle}>
        <code className={'language-' + language} style={{
    fontSize: '14px'
  }}>
          {content}
        </code>
      </pre>
      {showExpandToggle ? <button type="button" style={toggleLinkStyle} onClick={toggleExpand} aria-expanded={expanded ? 'true' : 'false'} aria-label={expanded ? 'Show less code' : 'See all ' + lineCount + ' lines of code'}>
          {expanded ? '... Show less' : '... See all ' + lineCount + ' lines'}
        </button> : null}
    </div>;
  if (error) {
    return renderErrorBox();
  }
  if (content === null) {
    return renderLoadingBox();
  }
  return renderLoaded();
};

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

You can configure MR scans while creating a new GitLab App installation or for the existing GitLab App installations. Endor Labs uses group webhooks to scan your merge requests. For more information, refer to [GitLab webhooks](https://docs.gitlab.com/user/project/integrations/webhooks/#group-webhooks).

<Note>
  **Permissions for the GitLab App MR scans**

  GitLab App installation requires a personal access token of the project developer role with the `api` scope. You need to configure webhooks to complete the MR scans configuration. Webhooks configuration through cURL requires the personal access token of the group owner with the `api` scope or the group owner role needs to configure the webhook on the GitLab UI.
</Note>

You can also choose to receive MR comments on your merge requests. After you configure MR comments, Endor Labs posts a comment on the merge request if any issues are detected during the MR scan. See [GitLab MR comments](#gitlab-mr-comments) for more information.

## Configure MR scans during a GitLab App installation

To configure MR scans during a GitLab App installation:

1. Select **Merge Request Settings**.

2. Toggle **Enable Merge Request scans** to enable merge request scans.

3. You can either configure the webhook on the GitLab UI or use the cURL command to set up the webhook.

   <AccordionGroup>
     <Accordion title="Configure the webhook on the GitLab UI">
       Ensure that you have the group owner role to configure the webhook on GitLab.

       1. Sign in to GitLab and select the group for which you want to configure the webhook.

       2. Select **Settings** > **Webhooks** from the left sidebar.

       3. Click **Add Webhook**.

       4. Configure the webhook in GitLab.

          * Name: Name for the webhook.
          * Description: Description for the webhook.
          * URL: Enter `https://api.endorlabs.com/webhooks/gitlab` as the URL to access the Endor Labs webhook API.
          * Secret Token: The secret token from Endor Labs.

          You can copy the values from the Endor Labs user interface.

              <img src="https://mintcdn.com/endorlabs-b4795f4f/-ooudT-Yh-sRP9TN/images/setup-deployment/scm-integrations/gitlab/gitlab-mr-scan-setup-gitlabui-webhook.webp?fit=max&auto=format&n=-ooudT-Yh-sRP9TN&q=85&s=03c8c9be776e7250ed0cc8352699b762" alt="GitLab App MR scan setup webhook" style={{width: '60%'}} width="699" height="922" data-path="images/setup-deployment/scm-integrations/gitlab/gitlab-mr-scan-setup-gitlabui-webhook.webp" />

       5. Click **Add Custom Header** and enter the following values:
          * Key: `X-Endor-Installation-ID`
          * Value: The Custom Header Value from the Endor Labs user interface. It is the installation ID of the Endor Labs GitLab installation.

       6. Select **Merge request events** under the **Trigger** section.

       7. Ensure that **Enable SSL verification** is selected under the **SSL verification** section.

       8. Click **Add Webhook** to save the changes.

          You can create a webhook without SSL verification, but it is not recommended. Without SSL verification, the webhook is vulnerable to man-in-the-middle attacks.
     </Accordion>

     <Accordion title="Configure the webhook using the cURL command">
       Ensure that you have the personal access token of the group owner with the `api` scope to configure the cURL command.

       The following table lists the configuration options required to create the GitLab group webhook.

       <YamlTable>
         {`

               - Parameter: \`<gitlab_base_url>\`
               Description: GitLab API root (for GitLab.com use <code>https://gitlab.com</code>). No trailing slash.
               - Parameter: \`<group_path>\`
               Description: Group path for the GitLab group hooks API.
               - Parameter: \`<group_owner_api_pat_here>\`
               Description: Group owner personal access token with \`api\` scope.
               - Parameter: \`url\`
               Description: Endor Labs webhook URL. Default is \`https://api.endorlabs.com/webhooks/gitlab\`. Use your tenant URL if your administrator provides a different host.
               - Parameter: \`<webhook_secret_token>\`
               Description: Webhook secret GitLab sends with requests. Must match the secret configured for your GitLab App installation in Endor Labs.
               - Parameter: \`<installation_uuid>\`
               Description: UUID of your GitLab App installation in Endor Labs.

               `}
       </YamlTable>

       Run the command on your system to register the webhook with GitLab.

       ```bash expandable theme={null}
       curl --location '<gitlab_base_url>/api/v4/groups/<group_path>/hooks' \
       --header 'content-type: application/json' \
       --header 'PRIVATE-TOKEN: <group_owner_api_pat_here>' \
       --data '{
         "name": "Endor Labs MR Security Scanner",
         "description": "Group webhook for GitLab MR scanning with Endor Labs.",
         "url": "https://api.endorlabs.com/webhooks/gitlab",
         "token": "<webhook_secret_token>",
         "enable_ssl_verification": true,
         "push_events": false,
         "merge_requests_events": true,
         "custom_headers": [
           {
             "key": "X-Endor-Installation-ID",
             "value": "<installation_uuid>"
           }
         ]
       }'
       ```
     </Accordion>
   </AccordionGroup>

   <Warning>
     **Save the webhook configuration**

     Ensure that you complete the webhook configuration in GitLab and save the configuration in Endor Labs. Otherwise, the MR scans will not be enabled.
   </Warning>

4. Optionally, select Merge Request Comments to enable MR comments.

   When you enable MR comments, Endor Labs will post a comment on the merge request if any issues are detected during the MR scan. You need to set up MR comments in Endor Labs to receive the comments. See [GitLab MR comments](#gitlab-mr-comments) for more information.

   <Note>
     **Enable MR scans for selected projects**

     If you select the options to configure MR scans in your GitLab App installation, merge requests for all the projects in the groups and subgroups are scanned. Instead, you can choose to configure MR scans and MR comments for selected projects. Choose not to select **Merge Request Scans** but continue to set up the webhook for MR scans. Set up a scan profile to configure MR scans. Ensure that you select **Pull Request Scans** and optionally **Pull Request Comments** under **Developer Workflow** when you create the scan profile.

     See [Scan profiles](/scan/scan-profiles) for more information.
   </Note>

5. Toggle **Enable Full scan with reachability** to include reachability analysis and call graph generation in merge request scans.

6. Select **Yes, I understand MR scans work only if the Webhook is set up correctly** and click **Save MR Settings**.

## Configure MR scans for existing GitLab installations

You can configure MR scans for existing GitLab installations or after the creation of a new GitLab installation.

<Note>
  **Scope of the Personal Access Token**

  Replace the personal access token with the personal access token of a project developer (minimum) role with the `api` scope for MR scans.
</Note>

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

2. Click **Manage** in **GitLab** under **Source Control Managers**.

3. Click the vertical three dots next to the GitLab installation that you want to update.

4. Select **Edit Integration**.

5. Select **Merge Request Settings**.

6. Toggle **Enable Merge Request scans** to scan merge requests.

7. You can either configure the webhook on the GitLab UI or use the cURL command to set up the webhook.

   <AccordionGroup>
     <Accordion title="Configure the webhook on the GitLab UI">
       Ensure that you have the group owner role to configure the webhook on GitLab.

       1. Sign in to GitLab and select the group for which you want to configure the webhook.

       2. Select **Settings** > **Webhooks** from the left sidebar.

       3. Click **Add Webhook**.

       4. Configure the webhook in GitLab.

          * Name: Name for the webhook.
          * Description: Description for the webhook.
          * URL: Enter `https://api.endorlabs.com/webhooks/gitlab` as the URL to access the Endor Labs webhook API.
          * Secret Token: The secret token from Endor Labs.

          You can copy the values from the Endor Labs user interface.

              <img src="https://mintcdn.com/endorlabs-b4795f4f/-ooudT-Yh-sRP9TN/images/setup-deployment/scm-integrations/gitlab/gitlab-mr-scan-setup-gitlabui-webhook.webp?fit=max&auto=format&n=-ooudT-Yh-sRP9TN&q=85&s=03c8c9be776e7250ed0cc8352699b762" alt="GitLab App MR scan setup webhook" style={{width: '60%'}} width="699" height="922" data-path="images/setup-deployment/scm-integrations/gitlab/gitlab-mr-scan-setup-gitlabui-webhook.webp" />

       5. Click **Add Custom Header** and enter the following values:
          * Key: `X-Endor-Installation-ID`
          * Value: The Custom Header Value from the Endor Labs user interface. It is the installation ID of the Endor Labs GitLab installation.

       6. Select **Merge request events** under the **Trigger** section.

       7. Ensure that **Enable SSL verification** is selected under the **SSL verification** section.

       8. Click **Add Webhook** to save the changes.

          You can create a webhook without SSL verification, but it is not recommended. Without SSL verification, the webhook is vulnerable to man-in-the-middle attacks.
     </Accordion>

     <Accordion title="Configure the webhook using the cURL command">
       Ensure that you have the personal access token of the group owner with the `api` scope to configure the cURL command.

       The following table lists the configuration options required to create the GitLab group webhook.

       <YamlTable>
         {`

               - Parameter: \`<gitlab_base_url>\`
               Description: GitLab API root (for GitLab.com use <code>https://gitlab.com</code>). No trailing slash.
               - Parameter: \`<group_path>\`
               Description: Group path for the GitLab group hooks API.
               - Parameter: \`<group_owner_api_pat_here>\`
               Description: Group owner personal access token with \`api\` scope.
               - Parameter: \`url\`
               Description: Endor Labs webhook URL. Default is \`https://api.endorlabs.com/webhooks/gitlab\`. Use your tenant URL if your administrator provides a different host.
               - Parameter: \`<webhook_secret_token>\`
               Description: Webhook secret GitLab sends with requests. Must match the secret configured for your GitLab App installation in Endor Labs.
               - Parameter: \`<installation_uuid>\`
               Description: UUID of your GitLab App installation in Endor Labs.

               `}
       </YamlTable>

       Run the command on your system to register the webhook with GitLab.

       ```bash expandable theme={null}
       curl --location '<gitlab_base_url>/api/v4/groups/<group_path>/hooks' \
       --header 'content-type: application/json' \
       --header 'PRIVATE-TOKEN: <group_owner_api_pat_here>' \
       --data '{
         "name": "Endor Labs MR Security Scanner",
         "description": "Group webhook for GitLab MR scanning with Endor Labs.",
         "url": "https://api.endorlabs.com/webhooks/gitlab",
         "token": "<webhook_secret_token>",
         "enable_ssl_verification": true,
         "push_events": false,
         "merge_requests_events": true,
         "custom_headers": [
           {
             "key": "X-Endor-Installation-ID",
             "value": "<installation_uuid>"
           }
         ]
       }'
       ```
     </Accordion>
   </AccordionGroup>

8. Optionally, toggle **Post comments on Merge Requests** to enable MR comments.

   Ensure that you complete the MR comments configuration in Endor Labs to receive the comments. See [GitLab MR comments](#gitlab-mr-comments) for more information.

9. Toggle **Enable Full scan with reachability** to include reachability analysis and call graph generation in merge request scans

10. Select **Set up the webhook now** under **Webhook Settings**.

11. Select **Yes, I understand MR scans work only if the Webhook is set up correctly** and click **Save MR Settings**.

## Configure webhook for GitLab App MR scans

GitLab MR scans require a webhook to be configured on GitLab. You can configure the webhook on the GitLab UI or use the cURL command to configure the webhook. For more information, refer to [GitLab webhooks](https://docs.gitlab.com/user/project/integrations/webhooks/#group-webhooks).

### Configure the webhook on the GitLab UI

Ensure that you have the group owner role to configure the webhook on GitLab.

1. Sign in to GitLab and select the group for which you want to configure the webhook.

2. Select **Settings** > **Webhooks** from the left sidebar.

3. Click **Add Webhook**.

4. Configure the webhook in GitLab.

   * Name: Name for the webhook.
   * Description: Description for the webhook.
   * URL: Enter `https://api.endorlabs.com/webhooks/gitlab` as the URL to access the Endor Labs webhook API.
   * Secret Token: The secret token from Endor Labs.

   You can copy the values from the Endor Labs user interface.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/-ooudT-Yh-sRP9TN/images/setup-deployment/scm-integrations/gitlab/gitlab-mr-scan-setup-gitlabui-webhook.webp?fit=max&auto=format&n=-ooudT-Yh-sRP9TN&q=85&s=03c8c9be776e7250ed0cc8352699b762" alt="GitLab App MR scan setup webhook" style={{width: '60%'}} width="699" height="922" data-path="images/setup-deployment/scm-integrations/gitlab/gitlab-mr-scan-setup-gitlabui-webhook.webp" />

5. Click **Add Custom Header** and enter the following values:
   * Key: `X-Endor-Installation-ID`
   * Value: The Custom Header Value from the Endor Labs user interface. It is the installation ID of the Endor Labs GitLab installation.

6. Select **Merge request events** under the **Trigger** section.

7. Ensure that **Enable SSL verification** is selected under the **SSL verification** section.

8. Click **Add Webhook** to save the changes.

   You can create a webhook without SSL verification, but it is not recommended. Without SSL verification, the webhook is vulnerable to man-in-the-middle attacks.

### Configure the webhook using the cURL command

Ensure that you have the personal access token of the group owner with the `api` scope to configure the cURL command.

The following table lists the configuration options required to create the GitLab group webhook.

<YamlTable>
  {`

    - Parameter: \`<gitlab_base_url>\`
    Description: GitLab API root (for GitLab.com use <code>https://gitlab.com</code>). No trailing slash.
    - Parameter: \`<group_path>\`
    Description: Group path for the GitLab group hooks API.
    - Parameter: \`<group_owner_api_pat_here>\`
    Description: Group owner personal access token with \`api\` scope.
    - Parameter: \`url\`
    Description: Endor Labs webhook URL. Default is \`https://api.endorlabs.com/webhooks/gitlab\`. Use your tenant URL if your administrator provides a different host.
    - Parameter: \`<webhook_secret_token>\`
    Description: Webhook secret GitLab sends with requests. Must match the secret configured for your GitLab App installation in Endor Labs.
    - Parameter: \`<installation_uuid>\`
    Description: UUID of your GitLab App installation in Endor Labs.

    `}
</YamlTable>

Run the command on your system to register the webhook with GitLab.

```bash expandable theme={null}
curl --location '<gitlab_base_url>/api/v4/groups/<group_path>/hooks' \
--header 'content-type: application/json' \
--header 'PRIVATE-TOKEN: <group_owner_api_pat_here>' \
--data '{
  "name": "Endor Labs MR Security Scanner",
  "description": "Group webhook for GitLab MR scanning with Endor Labs.",
  "url": "https://api.endorlabs.com/webhooks/gitlab",
  "token": "<webhook_secret_token>",
  "enable_ssl_verification": true,
  "push_events": false,
  "merge_requests_events": true,
  "custom_headers": [
    {
      "key": "X-Endor-Installation-ID",
      "value": "<installation_uuid>"
    }
  ]
}'
```

## GitLab MR comments

MR comments are automated comments added to merge requests when Endor Labs detects policy violations or security issues during scans. When an MR is raised or updated, Endor Labs runs scans on the proposed changes and adds a comment if any violations are detected based on the configured action policies.

After you enable MR comments, you need to set up an action policy to allow comments to be posted on merge requests.

### Configure action policy for MR comments

The action policy that you create triggers the posting of comments on your merge request after a scan is complete. See [Action policy](/platform-administration/policies/action-policies) for more information. You can create multiple action policies based on your requirements, which the MR scan can trigger. If you create action policy with the `Secret` template, you get an inline comment with the line number where the secret is detected.

Ensure that you configure the following important settings in the action policy:

1. Choose an appropriate action policy template or create a custom action policy.

   You can choose an action policy template like [Containers](/platform-administration/policies/action-policies/templates#containers) or create a custom action policy.
2. Under **Action**, select **Enforce Policy**, then choose:
   * **Warn** to post a comment without breaking the build.
   * **Break the Build** to fail the build and block the pull request.
3. Define the scope of the policy using tags. Only projects that match the specified tags will receive MR comments.
4. Select **Propagate this policy to all child namespaces** if you want to apply the policy to all child namespaces.

   <Note>
     **Action policy propagation in child namespaces**

     If you select **Propagate this policy to all child namespaces**, and update the policy in the child namespace, the policy in the child namespace takes precedence over the policy in the parent namespace. If you select the propagate option for the child namespace, its child namespaces will also inherit the policy. Since [namespace hierarchy follows the group and subgroup hierarchy of GitLab](../#managed-namespaces-for-gitlab), you can effectively use this option to control the policy for different levels of your organization.
   </Note>

### MR comments template

Endor Labs provides a default template for MR comments that you can use out-of-the-box. You can also create custom templates using [Go Templates](https://pkg.go.dev/text/template).

The following section shows the default template for MR comments.

<CodeFile src="/templates/gitlab/gitlab_mr_comment_template.json" lang="go" />

You can create your custom template by editing the default template and saving the changes.

The following specification shows the additional functions that you can use in your custom template. You can access these functions by using their corresponding keys.

<CodeFile src="/templates/gitlab/gitlab_mr_comment_func_map.json" lang="go" />

To edit the default template:

1. Select **User menu** > **Integrations** from the left sidebar.
2. Click **Edit Template** next to **GitLab** under **Template for PR Comments**.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/-ooudT-Yh-sRP9TN/images/setup-deployment/scm-integrations/gitlab/gitlab-app-mr-comment-template.webp?fit=max&auto=format&n=-ooudT-Yh-sRP9TN&q=85&s=7d348cd636817fe20e9a2593340bd8d2" alt="" width="2422" height="1740" data-path="images/setup-deployment/scm-integrations/gitlab/gitlab-app-mr-comment-template.webp" />
3. Update the template with the required changes.
4. Select **Propagate this template to all child namespaces** if you want to apply the template to all child namespaces.

   <Note>
     **Template propagation in child namespaces**

     If you select **Propagate this template to all child namespaces**, and update the template in the child namespace, the template in the child namespace takes precedence over the template in the parent namespace. If you select the propagate option for the child namespace, its child namespaces will also inherit the template. Since [namespace hierarchy follows the group and subgroup hierarchy of GitLab](../#managed-namespaces-for-gitlab), you can effectively use this option to control the template for different levels of your organization.
   </Note>
5. Click **Save Template** to save the changes.

<Note>
  **Restore the default template**

  You can restore the default template by clicking **Restore to Default** in the template editor to go back to the initial template.
</Note>

### MR scan comments in GitLab

After you enable MR comments, Endor Labs posts a comment on the merge request if any issues are detected during the MR scan based on the action policies.

The following example shows a comment on the merge request as a result of the action policy for identifying leaked secrets.

<img src="https://mintcdn.com/endorlabs-b4795f4f/-ooudT-Yh-sRP9TN/images/setup-deployment/scm-integrations/gitlab/gitlab-mr-comment-secret.webp?fit=max&auto=format&n=-ooudT-Yh-sRP9TN&q=85&s=80b919a62d70f128c59ac3b524365e43" alt="GitLab MR comment secret" style={{width: '50%'}} width="941" height="368" data-path="images/setup-deployment/scm-integrations/gitlab/gitlab-mr-comment-secret.webp" />

You can expand and view the details of the finding.

<img src="https://mintcdn.com/endorlabs-b4795f4f/-ooudT-Yh-sRP9TN/images/setup-deployment/scm-integrations/gitlab/gitlab-mr-comment-secret-details.webp?fit=max&auto=format&n=-ooudT-Yh-sRP9TN&q=85&s=fc9d4c28bc3dbd9a5f42c2e5bdbbccfa" alt="GitLab MR comment secret details" style={{width: '50%'}} width="941" height="684" data-path="images/setup-deployment/scm-integrations/gitlab/gitlab-mr-comment-secret-details.webp" />

Click **Link to Finding** to view the details of the finding in Endor Labs.

For secrets, Endor Labs also generates a comment with the line number where the secret is detected.

<img src="https://mintcdn.com/endorlabs-b4795f4f/-ooudT-Yh-sRP9TN/images/setup-deployment/scm-integrations/gitlab/gitlab-mr-comment-secret-line.webp?fit=max&auto=format&n=-ooudT-Yh-sRP9TN&q=85&s=228387115802cf6053c9cc41e653f59a" alt="GitLab MR comment secret line" style={{width: '50%'}} width="948" height="652" data-path="images/setup-deployment/scm-integrations/gitlab/gitlab-mr-comment-secret-line.webp" />

## View MR scan findings

When you create a new merge request, the Endor Labs GitLab App scans the merge request. Endor Labs generates findings based on the finding policy.

1. Select **Projects** from the left sidebar.

2. Select the project for which you want to view the MR scan findings.

3. Select **PR runs** to view the MR scan findings.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/WValpCeFuCmyj4QD/images/setup-deployment/scm-integrations/mr-scan-findings.webp?fit=max&auto=format&n=WValpCeFuCmyj4QD&q=85&s=fb5fa544f915601dbc5ad733731f02df" alt="View MR scan findings" width="2988" height="1780" data-path="images/setup-deployment/scm-integrations/mr-scan-findings.webp" />

4. Select the MR for which you want to view the findings.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/WValpCeFuCmyj4QD/images/setup-deployment/scm-integrations/mr-scan-pane.webp?fit=max&auto=format&n=WValpCeFuCmyj4QD&q=85&s=7634c068eeddada923d009fa031c3e2e" alt="View MR scan pane" style={{width: '50%'}} width="882" height="1742" data-path="images/setup-deployment/scm-integrations/mr-scan-pane.webp" />

5. Click **View Findings** to view the findings on the MR.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/WValpCeFuCmyj4QD/images/setup-deployment/scm-integrations/mr-scan-findings-detail.webp?fit=max&auto=format&n=WValpCeFuCmyj4QD&q=85&s=6c63bdbe49dbbecb343c8884b4e4218f" alt="View MR scan findings in detail" width="2988" height="1780" data-path="images/setup-deployment/scm-integrations/mr-scan-findings-detail.webp" />

See [View Findings](/inventory-insights/findings) for more information on Findings in Endor Labs.

## Update the webhook secret

You might want to update the webhook secret for rotation or because you have lost the secret.

### Update the webhook secret on the Endor Labs GitLab App

1. Select **User menu** > **Integrations** from the left sidebar.
2. Click **Manage** in **GitLab** under **Source Control Managers**.
3. Click the vertical three dots next to the GitLab installation that you want to update.
4. Select **Edit Integration**.
5. Select **Merge Request Settings**.
6. Enter the new **Secret Token** and click **Save MR Settings** to save the changes.

   The secret token can be any random string.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/mG98Yzqym4nC9Ehd/images/setup-deployment/scm-integrations/gitlab/gitlab-mr-scan-update-webhook-secret.webp?fit=max&auto=format&n=mG98Yzqym4nC9Ehd&q=85&s=b6af83aa82d7bc9a5486a95322e8f213" alt="GitLab App MR scan update webhook secret" style={{width: '50%'}} width="648" height="1400" data-path="images/setup-deployment/scm-integrations/gitlab/gitlab-mr-scan-update-webhook-secret.webp" />

### Update the webhook secret in GitLab

To update the webhook secret in GitLab UI, you need to log in with the group owner role.

1. Sign in to GitLab and select the group for which you want to update the webhook secret.
2. Select **Settings** > **Webhooks** from the left sidebar.
3. Click **Edit** next to the webhook that you want to update.
4. Enter the new **Secret token**, and click **Save changes** to save the changes.

   Ensure that you use the same secret token that you used in Endor Labs.
