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

# Scan GitHub Actions

> Scan the GitHub Actions used in your workflow files for vulnerabilities, malware, and risky configuration.

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 scanning analyzes third-party Actions your workflows depend on. It is not the same as [Scanning with GitHub Actions](/setup-deployment/ci-cd/scan-with-github-actions), which runs Endor Labs inside a CI job on GitHub-hosted runners. Here you analyze Actions referenced in workflow files under `.github/workflows/` (including subdirectories), relative to the directory you scan.

Scanning those dependencies gives you visibility into supply chain risk in CI. You can detect known vulnerabilities, malware, and unsafe patterns in workflow YAML.

## How Endor Labs scans your workflows

Endor Labs discovers workflow files only under `.github/workflows/` relative to the directory you scan (`--path`, default the current directory). It walks that folder recursively and includes files with a `.yml` or `.yaml` extension. Workflow files outside that tree are not part of discovery.

From those files, Endor Labs resolves each `uses:` reference as a GitHub Actions dependency and models each action as a package with direct and transitive dependencies.

Endor Labs also evaluates workflow YAML with analytics and raises findings for risky configurations.

## What Endor Labs detects

The following table summarizes the main areas. For policy names and severities, see [GitHub Action policies](/platform-administration/policies/finding-policies/github-action-policies).

<YamlTable>
  {`


    - Area: Action dependencies
    What_is_checked: Vulnerabilities and malware in the GitHub Actions that your workflows reference.
    Learn_more: See [View GitHub Action findings](/inventory-insights/findings#view-github-action-findings).

    - Area: Workflow YAML
    What_is_checked: Imposter commits, script injection risks, misuse of secrets in expressions, unsafe checkouts with \`pull_request_target\`, and non-OIDC cloud authentication patterns.
    Learn_more: See [Policies for assessing configuration settings in workflow files](/platform-administration/policies/finding-policies/github-action-policies#policies-for-assessing-configuration-settings-in-workflow-files).

    - Area: GitHub organization and repository posture
    What_is_checked: Default workflow token permissions, which repositories may run GitHub Actions, runner group exposure, and workflows that create or approve PRs without human review.
    Learn_more: See [Policies for RSPM](/platform-administration/policies/finding-policies/github-action-policies#policies-for-rspm).


    `}
</YamlTable>

## Automate enforcement with action policies

[Action policies](/platform-administration/policies/action-policies) determine the automated response when a GitHub Action finding matches. Examples include failing a CI check, adding a PR comment, or sending a notification.

Use the [GitHub Actions policy template](/platform-administration/policies/action-policies/templates#github-actions) to create action policies that target workflow-related findings. The template exposes parameters such as finding name (defaults include **Unpinned direct dependency**, **Untrusted code checkout**, and **Imposter commit**) and severity.

## Enable GitHub Actions scanning

You can scan GitHub Actions through the Endor Labs GitHub App, Endor Labs GitHub Action, or with endorctl.

### Scan GitHub Actions with GitHub App

When you install the [Endor Labs GitHub App](/setup-deployment/scm-integrations/github-app-pro/github-app) or the [GitHub Enterprise Server App](/setup-deployment/scm-integrations/github-app-pro/github-enterprise-app), enable **GitHub Actions** among the scanners. That schedules repository scans that include workflow dependency and posture analysis. See also [Scan capabilities of the Endor Labs GitHub Apps](/setup-deployment/scm-integrations/github-app-pro/scan-with-githubapp).

### Scan GitHub Actions with Endor Labs GitHub Action

In your CI workflow, pass `scan_github_actions: true` to the Endor Labs GitHub Action. See [GitHub Action configuration parameters](/setup-deployment/ci-cd/scan-with-github-actions#endor-labs-github-action-configuration-parameters).

### Scan GitHub Actions with endorctl

Run the following command to scan the GitHub Actions in your repository.

```bash theme={null}
endorctl scan --ghactions
```

The flag enables GitHub Actions workflow scanning. You can combine it with other scan options as needed. The environment variable is `ENDOR_SCAN_GHACTIONS`. For the full CLI reference, see [endorctl scan](/developers-api/cli/commands/scan).

## Limitations of GitHub Actions scanning

GitHub Actions scanning has the following limitations:

* Endor Labs detects vulnerabilities and dependencies for GitHub Action packages written in JavaScript or TypeScript.
* Private GitHub Actions and private reusable workflows referenced from other repositories are not detected.
* Test dependencies are not detected for GitHub Action packages.
