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

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://docs.endorlabs.com/feedback

```json
{
  "path": "/setup-deployment/ci-cd/scan-with-github-actions/index",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>

# 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' : '#fff',
      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 : 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 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>;
};

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.

```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@v1 # Replace v1 with the commit SHA of the latest version of the GitHub Action for enhanced security
      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@v1 # Replace v1 with the commit SHA of the latest version of the GitHub Action for enhanced security
      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'
```

## 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@v1 # Replace v1 with the commit SHA of the latest version of the GitHub Action for enhanced security
  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@v1 # Replace v1 with the commit SHA of the latest version of the GitHub Action for enhanced security
      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@v1 # Replace v1 with the commit SHA of the latest version of the GitHub Action for enhanced security
        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@v1 # Replace v1 with the commit SHA of the latest version of the GitHub Action for enhanced security
        with:
          namespace: 'example'
          gcp_service_account: '<Insert_Your_Service_Account>@<Insert_Your_Project>.iam.gserviceaccount.com'
          enable_github_action_token: false
```

## 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>
  To enable AI SAST analysis, set the `additional_args` parameter to `--ai-sast-analysis=agent-fallback`.
</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>
