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

# Scanning with GitHub Actions

> Learn how to implement Endor Labs in GitHub action workflows.

export const GithubActionSha = ({action = 'scan', owner = 'endorlabs', repo = 'github-action'}) => {
  const CACHE_TTL = 60 * 60 * 1000;
  const ACTION_LABELS = {
    scan: 'Scan',
    sign: 'Sign',
    verify: 'Verify'
  };
  const actionPath = action === 'scan' ? '' : '/' + action;
  const actionLabel = ACTION_LABELS[action] || action;
  const isFull = action === 'scan';
  const [isDark, setIsDark] = useState(false);
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [copyStates, setCopyStates] = useState({});
  useEffect(() => {
    const check = () => {
      const r = document.documentElement;
      setIsDark(r.dataset.theme === 'dark' || r.classList.contains('dark'));
    };
    check();
    const obs = new MutationObserver(check);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => obs.disconnect();
  }, []);
  useEffect(() => {
    let cancelled = false;
    const cacheKey = 'gha-sha-' + owner + '/' + repo;
    const regKey = owner + '/' + repo;
    if (!globalThis.__ghaRegistry) {
      globalThis.__ghaRegistry = {};
    }
    const registry = globalThis.__ghaRegistry;
    const getCached = () => {
      try {
        const raw = sessionStorage.getItem(cacheKey);
        if (!raw) return null;
        const cached = JSON.parse(raw);
        if (Date.now() - cached.ts > CACHE_TTL) {
          sessionStorage.removeItem(cacheKey);
          return null;
        }
        return cached.data;
      } catch (e) {
        console.debug('gha: sessionStorage read failed', e);
        return null;
      }
    };
    const setCache = d => {
      try {
        sessionStorage.setItem(cacheKey, JSON.stringify({
          ts: Date.now(),
          data: d
        }));
      } catch (e) {
        console.debug('gha: sessionStorage write failed', e);
      }
    };
    const fetchRelease = async () => {
      const cached = getCached();
      if (cached) return cached;
      if (registry[regKey]) return registry[regKey];
      const promise = (async () => {
        const releaseResp = await fetch('https://api.github.com/repos/' + owner + '/' + repo + '/releases/latest');
        if (!releaseResp.ok) throw new Error('HTTP ' + releaseResp.status);
        const release = await releaseResp.json();
        const tagName = release.tag_name;
        const refResp = await fetch('https://api.github.com/repos/' + owner + '/' + repo + '/git/ref/tags/' + encodeURIComponent(tagName));
        if (!refResp.ok) throw new Error('HTTP ' + refResp.status);
        const ref = await refResp.json();
        let sha = ref.object.sha;
        if (ref.object.type === 'tag') {
          const tagResp = await fetch('https://api.github.com/repos/' + owner + '/' + repo + '/git/tags/' + sha);
          if (tagResp.ok) {
            const tag = await tagResp.json();
            sha = tag.object.sha;
          }
        }
        const result = {
          version: tagName,
          sha: sha
        };
        setCache(result);
        return result;
      })();
      registry[regKey] = promise;
      promise.finally(() => {
        delete registry[regKey];
      });
      return promise;
    };
    fetchRelease().then(d => {
      if (!cancelled) setData(d);
    }).catch(() => {
      if (!cancelled) setError(true);
    });
    return () => {
      cancelled = true;
    };
  }, [owner, repo]);
  const handleCopy = (text, key) => {
    if (!text) return;
    const flashSuccess = () => {
      setCopyStates(prev => ({
        ...prev,
        [key]: true
      }));
      setTimeout(() => setCopyStates(prev => ({
        ...prev,
        [key]: false
      })), 2000);
    };
    const fallbackCopy = () => {
      try {
        const ta = document.createElement('textarea');
        ta.value = text;
        ta.setAttribute('readonly', '');
        ta.style.cssText = 'position:fixed;left:-9999px;opacity:0';
        document.body.appendChild(ta);
        ta.select();
        const ok = document.execCommand('copy');
        ta.remove();
        if (ok) flashSuccess();
      } catch (e) {
        console.debug('gha: fallback copy failed', e);
      }
    };
    if (navigator.clipboard?.writeText) {
      navigator.clipboard.writeText(text).then(flashSuccess).catch(fallbackCopy);
    } else {
      fallbackCopy();
    }
  };
  const bgColor = isDark ? '#0d1117' : '#ffffff';
  const bgLight = isDark ? '#161b22' : '#f6f8fa';
  const textColor = isDark ? '#e6edf3' : '#1f2937';
  const cardBorder = '2px solid rgba(38,208,124,' + (isDark ? '0.4' : '0.5') + ')';
  const cardShadow = '0 2px 8px rgba(38,208,124,' + (isDark ? '0.05' : '0.1') + ')';
  const S = {
    copyBtn: copied => ({
      marginLeft: 'auto',
      padding: '0.25rem 0.5rem',
      background: copied ? '#28a745' : 'linear-gradient(to bottom, rgba(50,225,140,0.92), rgba(30,190,110,0.92))',
      color: isDark ? '#000' : '#0A2E1D',
      border: '1px solid rgba(38,208,124,0.5)',
      borderRadius: '6px',
      fontSize: '0.75rem',
      cursor: 'pointer',
      fontWeight: 500,
      whiteSpace: 'nowrap',
      flexShrink: 0
    }),
    infoRow: {
      display: 'flex',
      alignItems: 'center',
      gap: '0.75rem',
      padding: '0.5rem 0.75rem',
      background: bgLight,
      border: '1px solid rgba(38,208,124,0.2)',
      borderRadius: '8px',
      marginBottom: '0.5rem'
    },
    label: {
      fontWeight: 600,
      fontSize: '0.8rem',
      color: textColor,
      minWidth: '5.5rem',
      flexShrink: 0
    },
    value: {
      fontSize: '0.85rem',
      color: textColor,
      flex: 1,
      minWidth: 0,
      overflow: 'hidden',
      textOverflow: 'ellipsis'
    },
    mono: {
      fontFamily: "'Monaco','Menlo','Ubuntu Mono',monospace",
      fontSize: '0.8rem'
    },
    usageBlock: {
      background: bgLight,
      border: '1px solid rgba(38,208,124,0.3)',
      borderRadius: '8px',
      padding: '0.75rem'
    },
    previewHeader: {
      display: 'flex',
      alignItems: 'center',
      color: textColor,
      fontWeight: 600,
      margin: '0 0 0.5rem 0',
      fontSize: '0.85rem'
    },
    codeBlock: {
      background: bgColor,
      color: textColor,
      padding: '0.6rem 0.75rem',
      borderRadius: '6px',
      fontFamily: "'Monaco','Menlo','Ubuntu Mono',monospace",
      fontSize: '0.8rem',
      lineHeight: 1.4,
      overflowX: 'auto',
      margin: 0,
      whiteSpace: 'pre-wrap',
      wordBreak: 'break-all',
      border: '1px solid rgba(38,208,124,0.1)'
    },
    errorBox: {
      marginTop: '0.75rem',
      padding: '0.6rem 0.75rem',
      background: isDark ? '#3b1111' : '#fef2f2',
      border: '1px solid ' + (isDark ? '#7f1d1d' : '#fecaca'),
      borderRadius: '8px',
      color: isDark ? '#fecaca' : '#7f1d1d',
      fontSize: '0.8rem',
      lineHeight: 1.5
    },
    errorLink: {
      color: isDark ? '#fecaca' : '#7f1d1d',
      textDecoration: 'underline'
    },
    loading: {
      color: textColor,
      opacity: 0.5,
      fontStyle: 'italic'
    }
  };
  const refText = data ? owner + '/' + repo + actionPath + '@' + data.sha + ' # ' + data.version : null;
  const releasesUrl = 'https://github.com/' + owner + '/' + repo + '/releases/latest';
  const renderUsageBlock = label => <div style={S.usageBlock}>
      <div style={S.previewHeader}>
        <span>{label}</span>
        <button type="button" onClick={() => handleCopy(refText, 'ref')} disabled={!data} style={{
    ...S.copyBtn(copyStates.ref),
    ...data ? {} : {
      opacity: 0.5,
      cursor: 'not-allowed'
    }
  }}>
          {copyStates.ref ? '\u2713 Copied!' : '\uD83D\uDCCB Copy'}
        </button>
      </div>
      <pre style={S.codeBlock}>
        {data ? refText : <span style={S.loading}>Loading...</span>}
      </pre>
    </div>;
  const renderError = compact => <div style={{
    ...S.errorBox,
    ...compact ? {
      marginTop: '0.5rem',
      fontSize: '0.75rem'
    } : {}
  }}>
      <span>{'\u26A0\uFE0F'} Could not fetch the latest commit SHA automatically. </span>
      {compact ? <a href={releasesUrl} target="_blank" rel="noopener" style={S.errorLink}>View releases</a> : <div style={{
    marginTop: '0.4rem'
  }}>
          To find the SHA manually: go to{' '}
          <a href={releasesUrl} target="_blank" rel="noopener" style={S.errorLink}>Releases</a>,
          click the release tag's commit, and copy the full 40-character SHA from the URL.
        </div>}
    </div>;
  if (isFull) {
    return <div className="not-prose" style={{
      margin: '1.5rem 0',
      fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
    }}>
        <div style={{
      background: bgColor,
      border: cardBorder,
      borderRadius: '16px',
      padding: '1.25rem',
      color: textColor,
      boxShadow: cardShadow
    }}>
          <div style={{
      marginBottom: '1rem',
      display: 'flex',
      alignItems: 'center',
      gap: '0.75rem'
    }}>
            <span style={{
      color: '#26D07C',
      fontSize: '1.1rem',
      flexShrink: 0
    }}>{'\uD83D\uDEE1\uFE0F'}</span>
            <div>
              <div style={{
      color: textColor,
      marginBottom: '0.25rem',
      fontWeight: 600,
      fontSize: '1.1rem'
    }}>Latest GitHub Action Release</div>
              <p style={{
      color: textColor,
      opacity: 0.8,
      margin: 0,
      fontSize: '0.85rem',
      lineHeight: 1.4
    }}>Pin to an immutable commit SHA for enhanced security.</p>
            </div>
          </div>

          {!error && <div>
              <div style={S.infoRow}>
                <span style={S.label}>Version</span>
                <span style={S.value}>
                  {data ? data.version : <span style={S.loading}>Loading...</span>}
                </span>
              </div>
              <div style={S.infoRow}>
                <span style={S.label}>Commit SHA</span>
                <span style={{
      ...S.value,
      ...S.mono
    }}>
                  {data ? data.sha : <span style={S.loading}>Loading...</span>}
                </span>
                <button type="button" onClick={() => handleCopy(data?.sha, 'sha')} disabled={!data} style={{
      ...S.copyBtn(copyStates.sha),
      ...data ? {} : {
        opacity: 0.5,
        cursor: 'not-allowed'
      }
    }}>
                  {copyStates.sha ? '\u2713 Copied!' : '\uD83D\uDCCB Copy SHA'}
                </button>
              </div>
              <div style={{
      marginTop: '0.75rem'
    }}>
                {renderUsageBlock('Use in your workflow')}
              </div>
            </div>}

          {error && renderError(false)}
        </div>
      </div>;
  }
  return <div className="not-prose" style={{
    margin: '1rem 0',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
  }}>
      {!error && renderUsageBlock('Latest ' + actionLabel + ' action \u2014 pin to commit SHA')}
      {error && renderError(true)}
    </div>;
};

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

GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline.
You can use GitHub Actions to include Endor Labs into your CI pipeline seamlessly.

Using this pipeline, developers can view and detect:

* Policy violations in the source code
* Secrets inadvertently included in the source code

The Endor Labs verifications are conducted as automated checks and help you discover violations before pushing code to the repository. Information about the violations can be included as comments on the corresponding pull request (PR). This enables developers to easily identify issues and take remedial measures early in the development life cycle.

* For policy violations, the workflow is designed to either emit a warning or return an error based on your action policy configurations.
* For secrets discovered in the commits, developers can view the PR comments and take necessary remedial measures.

## Install Software Prerequisites

To ensure the successful execution of the Endor Labs GitHub action, the following prerequisites must be met:

* The GitHub action must be able to authenticate with the Endor Labs API.
* You must have the value of the Endor Labs namespace handy for authentication.
* You must have access to the Endor Labs API.
* If you use keyless authentication, you must set an authorization policy in Endor Labs. See [Authorization policies](/platform-administration/rbac/authorization-policies) for details.

## Secure GitHub Actions with immutable commit SHA

Endor Labs recommends pinning the commit SHA of the GitHub Actions to enhance security. By pinning GitHub Actions to a commit SHA, you make the code immutable even if the tag version is changed or the code is updated.

<GithubActionSha />

To manually find the Endor Labs GitHub action's latest commit SHA:

1. Go to [Endor Labs GitHub Actions](https://github.com/endorlabs/github-action) and select the latest release version in **Releases**.
2. Click the commit of the release.
   <img src="https://mintcdn.com/endorlabs-b4795f4f/266r_aE9BWR2B51C/images/setup-deployment/ci-cd/github-action-pin-commit-sha.webp?fit=max&auto=format&n=266r_aE9BWR2B51C&q=85&s=df3236e1f4de50139982cae07e862b29" alt="Select commit SHA of latest release" width="2518" height="398" data-path="images/setup-deployment/ci-cd/github-action-pin-commit-sha.webp" />
3. Copy the commit SHA from the URL.

   The commit SHA is the 40-character alphanumeric string in the URL after `/commit/`.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/266r_aE9BWR2B51C/images/setup-deployment/ci-cd/github-action-copy-commit-sha.webp?fit=max&auto=format&n=266r_aE9BWR2B51C&q=85&s=cc1d3b48266370066f11aca515c2e65f" alt="Copy commit SHA" width="1806" height="90" data-path="images/setup-deployment/ci-cd/github-action-copy-commit-sha.webp" />

## Example GitHub Action Workflow

Endor Labs scanning workflow using GitHub Actions that accomplishes the following tasks in your CI environment:

* Tests PRs to the default branch and monitors the most recent push to the default branch.
* Builds a Java project and sets up the Java build tools. If your project is not on Java, then configure this workflow with your project-specific steps and build tools.
* Authenticates to Endor Labs with GitHub Actions keyless authentication.
* Scan with Endor Labs.
* Comments on PRs if any policy violations occur.
* Generates findings and uploads results to GitHub in SARIF format.

The following example workflow shows how to scan with Endor Labs for a Java application using the recommended keyless authentication for GitHub Actions.

<Note>
  The examples pin the Endor Labs GitHub Action to release `v1.1.12`. To use a newer release, copy the **Use in your workflow** reference from **Latest GitHub Action Release** in [Secure GitHub Actions with immutable commit SHA](/setup-deployment/ci-cd/scan-with-github-actions#secure-github-actions-with-immutable-commit-sha).
</Note>

```yaml expandable theme={null}
name: Endor Labs Dependency and Secrets Scan
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs:
  scan:
    permissions:
      security-events: write # Used to upload Sarif artifact to GitHub
      contents: read # Used to check out a private repository
      actions: read # Required for private repositories to upload Sarif files. GitHub Advanced Security licenses are required.
      id-token: write # Used for keyless authentication with Endor Labs
      pull-requests: write # Required to automatically comment on PRs for new policy violations
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v3
    - name: Setup Java
      uses: actions/setup-java@v3
      with:
        distribution: 'microsoft'
        java-version: '17'
    - name: Build Package
      run: mvn clean install
    - name: Endor Labs Scan Pull Request
      if: github.event_name == 'pull_request'
      uses: endorlabs/github-action@b8992820cc4d9c9e7ded5022adf6cabe2dc11946 # v1.1.12
      with:
        namespace: 'example' # Replace with your Endor Labs tenant namespace
        scan_dependencies: true
        scan_secrets: true
        pr: true
        enable_pr_comments: true # Required to automatically comment on PRs for new policy violations
        github_token: ${{ secrets.GITHUB_TOKEN }} # Required for PR comments on new policy violations

  scan-main:
    permissions:
      id-token: write
      repository-projects: read
      pull-requests: read
      contents: read
    name: endorctl-scan
    runs-on: ubuntu-latest
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v3
    - name: Setup Java
      uses: actions/setup-java@v3
      with:
        distribution: 'microsoft'
        java-version: '17'
    - name: Build Package
      run: mvn clean install
    - name: 'Endor Labs Scan Push to main'
      if: ${{ github.event_name == 'push' }}
      uses: endorlabs/github-action@b8992820cc4d9c9e7ded5022adf6cabe2dc11946 # v1.1.12
      with:
        namespace: 'example' # Replace with your Endor Labs tenant namespace
        scan_dependencies: true
        scan_secrets: true
        pr: false
        scan_summary_output_type: 'table'
        sarif_file: 'findings.sarif'
    - name: Upload findings to github
      uses: github/codeql-action/upload-sarif@v3
      with:
        sarif_file: 'findings.sarif'
```

<Note>
  A policy violation makes the scan exit with a non-zero code, which fails the job. Failing the job does not block a merge on its own. To block pull requests, mark the scan job as a required status check in GitHub. See [Block pull requests on findings](/scan/pr-scans/block-pull-requests).
</Note>

## Set up branch tracking in GitHub Actions

In Git, a detached HEAD state occurs when the repository checks out a specific commit instead of a branch reference. In this state, Git points the HEAD directly to a commit hash, without associating it with a named branch. As a result, actions performed, such as creating new commits or running automated scans, do not carry branch identity unless explicitly specified.

Proper branch context enables Endor Labs to:

* Associate scans with the correct branch
* Identify scans on the monitored default branch
* Track findings and display metrics accurately across branches

Without proper branch configuration, Endor Labs may create multiple branch entries for the same logical branch, leading to fragmented reporting and inaccurate metrics.

<img src="https://mintcdn.com/endorlabs-b4795f4f/266r_aE9BWR2B51C/images/setup-deployment/ci-cd/branch-fragmentation.webp?fit=max&auto=format&n=266r_aE9BWR2B51C&q=85&s=da0b5af516eb674f7a243de8d5ce3a73" alt="Project with multiple branch entries" style={{width: '80%'}} width="1268" height="382" data-path="images/setup-deployment/ci-cd/branch-fragmentation.webp" />

GitHub Actions typically maintains proper branch context automatically. However, to ensure scans are properly tracked, when using the Endor Labs GitHub Action, set `pr: false` to create monitored versions that appear on dashboards. Use `if: github.event_name == 'push'` to trigger only on pushes to the default branch:

```yaml theme={null}
- name: Endor Labs Scan Push to main
  if: github.event_name == 'push'
  uses: endorlabs/github-action@b8992820cc4d9c9e7ded5022adf6cabe2dc11946 # v1.1.12
  with:
    namespace: 'example'
    scan_dependencies: true
    pr: false
```

## Scan containers with OS reachability

You can use the Endor Labs GitHub Action to scan container images with [OS reachability](/scan/containers/container-reachability) enabled. Container reachability identifies which packages inside a container image are actually used at runtime, helping you prioritize the most critical vulnerabilities for remediation. Set `scan_container: true` and `os_reachability: true` to enable container scanning with reachability analysis:

```yaml theme={null}
    - name: Scan container with OS reachability
      uses: endorlabs/github-action@b8992820cc4d9c9e7ded5022adf6cabe2dc11946 # v1.1.12
      with:
        namespace: 'example'
        enable_github_action_token: 'true'
        scan_container: true
        scan_dependencies: false
        image: "nginx:latest"
        os_reachability: true
        pr: false
        project_name: 'my-project'
```

## Authenticate with Endor Labs

Endor Labs recommends using keyless authentication in CI environments. Keyless authentication is more secure and reduces the cost of secret rotation. To set up keyless authentication see [Keyless Authentication](/setup-deployment/ci-cd/keyless-authentication).

If you choose not to use keyless authentication, you can configure an API key and secret in GitHub for authentication as outlined in [Managing API keys](/platform-administration/api-keys).

### Authentication Without Keyless Authentication for GitHub

If you are not using keyless authentication for GitHub Actions, you must not provide `id-token: write` permissions to your GitHub token unless specifically required by a step in this job. You must also set `enable_github_action_token: false` in your Endor Labs GitHub Action configuration.

The following example configuration uses the Endor Labs API key for authentication:

```yaml theme={null}
      - name: Scan with Endor Labs
        uses: endorlabs/github-action@b8992820cc4d9c9e7ded5022adf6cabe2dc11946 # v1.1.12
        with:
          namespace: 'example'
          api_key: ${{ secrets.ENDOR_API_CREDENTIALS_KEY }}
          api_secret: ${{ secrets.ENDOR_API_CREDENTIALS_SECRET }}
          enable_github_action_token: false
```

The following example configuration uses a GCP service account for keyless authentication to Endor Labs:

```yaml theme={null}
      - name: Scan with Endor Labs
        uses: endorlabs/github-action@b8992820cc4d9c9e7ded5022adf6cabe2dc11946 # v1.1.12
        with:
          namespace: 'example'
          gcp_service_account: '<Insert_Your_Service_Account>@<Insert_Your_Project>.iam.gserviceaccount.com'
          enable_github_action_token: false
```

## Configure for the EU cluster

If you're an EU tenant, set the `ENDOR_API` environment variable to `https://api.eu.endorlabs.com` in your workflow and use an API key generated from your EU tenant at `https://app.eu.endorlabs.com`.

Add an `env` block at the job level to apply the EU endpoint to all steps in the job:

```yaml theme={null}
jobs:
  scan:
    runs-on: ubuntu-latest
    env:
      ENDOR_API: https://api.eu.endorlabs.com
    steps:
    - name: Checkout Repository
      uses: actions/checkout@v3
    - name: Scan with Endor Labs
      uses: endorlabs/github-action@b8992820cc4d9c9e7ded5022adf6cabe2dc11946 # v1.1.12
      with:
        namespace: '<your-eu-namespace>'
        api_key: ${{ secrets.ENDOR_API_CREDENTIALS_KEY }}
        api_secret: ${{ secrets.ENDOR_API_CREDENTIALS_SECRET }}
        enable_github_action_token: false
        scan_dependencies: true
        scan_secrets: true
        pr: false
```

<Note>
  Store your EU API credentials as secrets in your GitHub repository (for example, `ENDOR_API_CREDENTIALS_KEY` and `ENDOR_API_CREDENTIALS_SECRET`) and generate them from your EU tenant at `https://app.eu.endorlabs.com`.
</Note>

## Endor Labs GitHub Action Configuration Parameters

The following input configuration parameters are supported for the Endor Labs GitHub Action:

### Common parameters

Endor Labs GitHub Actions supports the following input global parameters:

<YamlTable>
  {`


    - Flags: \`api_key\`
    Description: Set the API key used to authenticate with Endor Labs.

    - Flags: \`api_secret\`
    Description: Set the secret corresponding to the API key used to authenticate with Endor Labs.

    - Flags: \`enable_github_action_token\`
    Description: Set to \`false\` if you prefer to use another form of authentication over GitHub action OIDC tokens. (Default: \`true\`)

    - Flags: \`endorctl_checksum\`
    Description: Set to the checksum associated with a pinned version of endorctl.

    - Flags: \`endorctl_version\`
    Description: Set to a version of endorctl to pin this specific version for use. Defaults to the latest version.

    - Flags: \`gcp_service_account\`
    Description: Set the target service account for GCP based authentication. GCP authentication is only enabled if this flag is set. Cannot be used with \`api_key\`.

    - Flags: \`log_level\`
    Description: Set the log level. (Default: \`info\`)

    - Flags: \`log_verbose\`
    Description: Set to \`true\` to enable verbose logging. (Default: \`false\`)

    - Flags: \`namespace\`
    Description: Set to the namespace of the project that you are working with. (Required)


    `}
</YamlTable>

### Scanning parameters

The following input parameters are also supported for the Endor Labs GitHub Action when used for scanning:

<YamlTable>
  {`


    - Flags: \`additional_args\`
    Description: Use \`additional_args\` with \`endorctl scan\` for advanced scenarios. However, no example use case currently exists as standard options suffice for typical needs.

    - Flags: \`use-bazel\`
    Description: Enable the usage of Bazel for the scan. (Default: \`false\`)

    - Flags: \`bazel_exclude_targets\`
    Description: Specify a list of Bazel targets to exclude from the scan.

    - Flags: \`bazel_include_targets\`
    Description: Specify a list of Bazel targets to scan. If \`bazel_targets_include\` is not set, the \`bazel_targets_query\` value is used to determine which Bazel targets to scan.

    - Flags: \`bazel_targets_query\`
    Description: Specify a Bazel query to determine which Bazel targets to scan. Ignored if \`bazel_targets_include\` is set.

    - Flags: \`pr\`
    Description: Set to \`false\` to track this scan as a monitored version within Endor Labs, as opposed to a point-in-time policy and finding test for a PR. (Default: \`true\`)

    - Flags: \`enable_pr_comments\`
    Description: Set to \`true\` to publish new findings as review comments. Requires \`pr\` and \`github_token\`. Additionally, the \`pull-requests: write\` permissions must be set in the workflow. (Default: \`false\`)

    - Flags: \`pr_baseline\`
    Description: Set to the Git reference that you are merging to, such as the default branch. Enables endorctl to compare findings, so developers are only alerted to issues in the current changeset. Example: \`pr_baseline: "main"\`. Note: Not needed if \`enable_pr_comments\` is set to \`true\`.

    - Flags: \`github_token\`
    Description: Set the token used to authenticate with GitHub. Mandatory if \`enable_pr_comments\` is set to \`true\`

    - Flags: \`run_stats\`
    Description: Set to \`false\` to disable reporting of CPU/RAM/time scan statistics via \`time -v\` (sometimes required on Windows runners). (Default: \`true\`)

    - Flags: \`phantom_dependencies\`
    Description: Set to \`true\` to enable phantom dependency analysis. (Default: \`false\`)

    - Flags: \`project_name\`
    Description: Specify a project name for a container image scan or a package scan.

    - Flags: \`image\`
    Description: Specify a container image to scan.

    - Flags: \`tags\`
    Description: Specify a list of user-defined tags to add to this scan. You can use tags to search and filter scans later.

    - Flags: \`scan_dependencies\`
    Description: Scan Git commits and generate findings for all dependencies. (Default: \`true\`)

    - Flags: \`scan_git_logs\`
    Description: Perform a more complete and detailed scan of secrets in the repository history. Must be used together with \`scan_secrets\`. (Default: \`false\`)

    - Flags: \`scan_github_actions\`
    Description: Scan source code repository for GitHub Actions used in workflow files to analyze vulnerabilities and malware. (Default: \`false\`)

    - Flags: \`scan_path\`
    Description: Set the path of the directory to scan. (Default: \`.\`)

    - Flags: \`scan_secrets\`
    Description: Scan the source code repository and generate findings for secrets. See also \`scan_git_logs\`. (Default: \`false\`)

    - Flags: \`scan_tools\`
    Description: Scan source code repository for CI/CD tools. (Default: \`false\`)

    - Flags: \`scan_package\`
    Description: Scan a specified artifact or a package. Set the path to an artifact with \`scan_path\`. (Default: \`false\`)

    - Flags: \`scan_container\`
    Description: Scan a specified container image. The image must be set with \`image\` and a project can be defined with \`project_name\`. (Default: \`false\`)

    - Flags: \`os_reachability\`
    Description: Enable OS reachability analysis for container image scans. When set to \`true\`, Endor Labs profiles the container at runtime to determine which OS packages are actually used, helping prioritize vulnerability remediation. Must be used with \`scan_container\`. (Default: \`false\`)

    - Flags: \`scan_sast\`
    Description: Scan the source code repository and generate findings for SAST. (Default: \`false\`)

    - Flags: \`disable_code_snippet_storage\`
    Description: Disable code snippet storage for SAST scans. (Default: \`false\`)

    - Flags: \`scan_summary_output_type\`
    Description: Set the desired output format to \`table\`, \`json\`, \`yaml\`, or \`summary\`. (Default: \`json\`)

    - Flags: \`sarif_file\`
    Description: Set to a path on your GitHub runner to store the analysis results in SARIF format.

    - Flags: \`export_scan_result_artifact\`
    Description: Set to \`false\` to disable the json scan result artifact export. (Default: \`true\`)

    - Flags: \`output_file\`
    Description: Set a file to save the scan results. Use this instead of \`export_scan_result_artifact\` to save any scan results data to a file in the workspace for processing by others steps in the same job, instead of the workflow run log.


    `}
</YamlTable>

<Note>
  **AI SAST scan options**

  * To enable an AI SAST triage agent scan, set the `additional_args` parameter to `--ai-sast-analysis=agent-fallback`.
  * To enable an AI SAST detection agent scan, set the `additional_args` parameter to `--ai-sast`.
</Note>

### Environmental variables

You can use the following environmental variable for the Endor Labs GitHub Action:

<YamlTable>
  {`


    - Flags: \`ENDOR_JS_ENABLE_TSSERVER\`
    Description: Set to \`false\` to skip installing \`tsserver\` when function reachability analysis is not required for JavaScript/TypeScript projects.


    `}
</YamlTable>

### Artifact scanning parameters

Use the following parameters and the latest sign action to build artifact signing through Endor Labs GitHub Actions.

<GithubActionSha action="sign" />

The optional parameters are required only if `enable_github_action_token` in [common parameters](#common-parameters) is `false`. If `true` (default value), GitHub automatically populates the optional and other parameters as token claims and sends them to Endor Labs.

<YamlTable>
  {`


    - Flags: \`artifact_name\`
    Required: Mandatory
    Description: Set to the name of the artifact to be signed.

    - Flags: \`source_repository_ref\`
    Required: Optional
    Description: Set to the repository reference on which the build run was based. For example, \`ref/tags/v1.0.1\`.

    - Flags: \`certificate_oidc_issuer\`
    Required: Optional
    Description: Set to the OIDC issuer of the token expected in the certificate. For example, \`https://token.actions.githubusercontent.com\`.


    `}
</YamlTable>

### Artifact verifying parameters

Use the following verification parameters and the latest verify action to build artifact verification through Endor Labs GitHub Actions.

<GithubActionSha action="verify" />

<YamlTable>
  {`


    - Flags: \`artifact_name\`
    Required: Mandatory
    Description: Set to the name of the artifact to be verified.

    - Flags: \`certificate_oidc_issuer\`
    Required: Mandatory
    Description: Set to the OIDC issuer of the token expected in the certificate. For example, \`https://token.actions.githubusercontent.com\`.


    `}
</YamlTable>
