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

# Secrets detection

> Detect leaked credentials and sensitive data in your codebase.

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

Secrets are access credentials such as passwords, API keys, and personal access tokens that grant access to services and resources. When a secret is committed to a repository, anyone with access to the code, now or later, can use it to reach the services it unlocks.

Leaked secrets lead to data breaches, unauthorized access, financial loss, and compliance violations. Endor Labs scans your source code and Git history for leaked secrets so your teams can find and revoke exposed credentials before they are abused.

## How secret detection works

Endor Labs detects secrets in stages so that scans stay fast and findings stay actionable:

* **Keyword pre-filter**: A fast string check narrows the files that a rule evaluates.
* **Pattern match**: A regular expression identifies candidate secrets.
* **Entropy check**: An optional Shannon entropy threshold filters out low-randomness matches, such as common words.
* **Validation**: For supported credential types, Endor Labs sends a request to the issuing service to determine whether the secret is still active.

Active secrets are raised as critical findings so that you can prioritize them.

## Capabilities

<CardGroup cols={3}>
  <Card title="Manage secret rules" icon="key" href="/scan/secrets/secret-rules">
    Use built-in system rules or create custom rules to detect secrets for any service.
  </Card>

  <Card title="Scan for secrets" icon="magnifying-glass" href="/scan/secrets/scan-secrets">
    Scan source code, Git history, changed files, or staged commits with endorctl.
  </Card>

  <Card title="View secret findings" icon="list-check" href="/scan/secrets/view-secret-findings">
    Review, prioritize, and remediate the secrets that Endor Labs detects.
  </Card>
</CardGroup>

## Supported secrets

Endor Labs detects secrets using a large set of built-in system rules that cover common providers and credential formats, including:

* Cloud provider credentials, such as AWS, Google Cloud, and Azure.
* Source control and CI tokens, such as GitHub, GitLab, and Bitbucket.
* API keys for services, such as Slack, Twilio, Datadog, Stripe, and OpenAI.
* OAuth tokens and personal access tokens.
* Private keys and certificates.

Endor Labs maintains the system rules and updates them over time. To see the exact rules available in your environment, list them with endorctl.

```bash theme={null}
endorctl api list -r SecretRule -n <your-namespace> --list-all
```

To detect a credential format that no system rule covers, create a custom rule. See [Manage secret rules](/scan/secrets/secret-rules).

## Scan modes

The scan mode determines where rules come from and whether the scan needs to reach Endor Labs.

<YamlTable>
  {`
    - Mode: Server scan
    Flag: none (default)
    Rule_source: Your namespace rules
    API_key: Yes
    Offline: No
    - Mode: Local scan
    Flag: \`--local\`
    Rule_source: Built-in rules, or \`--secret-rules-file\`
    API_key: No
    Offline: Yes
    - Mode: Pre-commit
    Flag: \`--pre-commit-checks\`
    Rule_source: Built-in rules, or \`--secret-rules-file\`
    API_key: No
    Offline: Yes
    - Mode: Git history
    Flag: \`--git-logs\`
    Rule_source: Your namespace rules
    API_key: Yes
    Offline: No
    - Mode: Changed files
    Flag: \`--diff-scope\`
    Rule_source: Your namespace rules
    API_key: Yes
    Offline: No
    `}
</YamlTable>

For the commands behind each mode, see [Scan for secrets](/scan/secrets/scan-secrets).

## Incremental secret scans

Endor Labs scans incrementally so that repeated scans stay fast as your Git history and pull requests grow. Incremental behavior applies in two places.

* **Git history**: After the first [`--git-logs`](/scan/secrets/scan-secrets#scan-complete-history) scan examines the full reachable history, later scans examine only the commits added since the last scan. Endor Labs falls back to a full rescan the first time a repository's history is scanned, and whenever a secret rule in the namespace changes. Run a scan with `--force-rescan` to re-examine the entire history on demand.
* **Pull requests**: Pass `--pr-incremental`, or enable pull request scans in a [monitoring scan](/setup-deployment/scm-integrations), to scan only the files that changed relative to the baseline branch instead of the whole repository. See [Perform incremental PR scan](/scan/pr-scans#perform-incremental-pr-scan).

To limit a single scan to changed files without comparing against a baseline, use [`--diff-scope`](/scan/secrets/scan-secrets#scan-changed-files-only).

## Secrets deduplication

Duplicate secrets increase the attack surface and the risk of unauthorized access. Managing multiple duplicate secrets can be complex and error-prone. Endor Labs categorizes instances of identical secrets found within your application components and repositories, helping an organization achieve:

* **Efficient prioritization**: Simplifies the prioritization of widely dispersed secrets, because more occurrences signify increased exposure and risk.
* **Comprehensive visibility**: Ensures you have a complete view of all instances associated with a specific secret, which helps when the secret is discovered or changes.
* **Optimized issue handling**: Generates a single finding for multiple instances of a secret, simplifying the work of managing and addressing related findings together.
