> ## 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": "/scan/secrets/secret-rules/index",
  "feedback": "Description of the issue"
}
```

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

</AgentInstructions>

# Manage secret rules

> Use secret rules to scan and detect secrets

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 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>;
    return processText(text);
  };
  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}>{processBadges(row[col])}</td>)}
          </tr>)}
      </tbody>
    </table>;
};

You can use the following rules to scan your codebase and detect secrets:

* **System rules**: Endor Labs provides out-of-the-box rules for secret patterns for many public services like GitHub, GitLab, AWS, Bitbucket, Dropbox, and more.

* **Custom rules**: If you are using a service that is not included in the out-of-the-box list of secret patterns provided by Endor Labs, you can build your own custom rule to scan and detect the secrets for any service.

The following table lists the most important fields of the rule definition.

<YamlTable>
  {`


    - Field_name: \`meta.name\`
    Description: The name of the rule.
    - Field_name: \`spec.rule_id\`
    Description: The rule identifier must be unique across all rules, both the system and the ones created in your namespace.
    - Field_name: \`spec.regex\`
    Description: The secret detection rule contains the pattern that the scanner will try to match.
    - Field_name: \`spec.keywords\`
    Description: The keywords are used for an initial check of a pattern before the full regex expression gets evaluated.
    - Field_name: \`spec.validation\`
    Description: The details about how to validate a secret.
    - Field_name: \`spec.entropy\`
    Description: The minimum Shannon entropy a regex group must have to be considered.
    - Field_name: \`spec.disabled\`
    Description: Set to \`false\` for system rules.


    `}
</YamlTable>

## Create a secret rule

1. Select **Policies & Rules** from the left sidebar.

2. Select **Secret Rules**.

3. Click **Create Secret Rules**.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/266r_aE9BWR2B51C/images/scan/secrets/add-secret-rule.webp?fit=max&auto=format&n=266r_aE9BWR2B51C&q=85&s=b79dc942cbf4c386378bac90688ef6f2" alt="Create secret rules" width="1874" height="1876" data-path="images/scan/secrets/add-secret-rule.webp" />

4. Enter the unique **Rule Identifier** and **Rule Name**.

5. Enter the **Description** of the secret rule.

6. Enter the regex for the secret rule in **Detection Rule**.

7. Enter keywords for pre-regex check filtering as comma separated values in **Keywords**.

8. Optionally, enter the minimum Shannon entropy a regex group must have to be considered in **Entropy**.

9. Optionally, add validation details to validate the secret:

   * **Validation URL**: Enter the URL for validation.
   * **Validation Method**: Choose between GET and POST methods.
   * **Success Response Codes**: Enter valid response codes (For example, `200` for HTTP Status OK)
   * **Failure Response Codes**: Enter invalid response codes (For example, `401` for HTTP Status Unauthorized)
   * **Authorization Details**: You can choose between Authorization Header, Bearer Token, and Basic Authentication.

10. Select **Propagate this rule to all child namespaces** to apply the secret rule to all child namespaces.

11. Click **Add Rule**.

### Create secret rules from the command line

For example, consider a token "demo\_value123" can be described using a regular expression. Here is an example of the rule specification:

```bash theme={null}
"meta": {
    "name": "Demo Token"
},
"spec": {
    "disabled": false,
    "keywords": [
        "demo_"
    ],
    "regex": "demo_[0-9a-zA-Z]{20}",
    "rule_id": "demo-rule"
}
```

Use the following command from the CLI to create this custom rule.

```bash expandable theme={null}
$ endorctl api create -r SecretRule -n demo  \
> --data '{
> "meta": {
>     "name": "Demo Token"
> },
> "spec": {
>     "disabled": false,
>     "keywords": [
>         "demo_"
>     ],
>     "regex": "demo_[0-9a-zA-Z]{20}",
>     "rule_id": "demo-rule"
> }
> }'
INFO: Initiating host-check ...
INFO: Host-check complete
{
  "meta": {
    "create_time": "2023-09-27T17:08:18.436936Z",
    "kind": "SecretRule",
    "name": "Demo Token",
    "update_time": "2023-09-27T17:08:18.436936Z",
    "upsert_time": "2023-09-27T17:08:18.436936Z",
    "version": "v1"
  },
  "spec": {
    "disabled": false,
    "keywords": [
      "demo_"
    ],
    "regex": "demo_[0-9a-zA-Z]{20}",
    "rule_id": "demo-rule"
  },
  "tenant_meta": {
    "namespace": "demo"
  },
  "uuid": "65146182aaeeffbaf5b6b553"
}
```

After the rule is created, the system uses this rule to detect this category of secrets.

If you can validate the secret using an HTTP request, then you can also add [validation](#validator) to this rule. See the following example for creating a validation rule for a demo\_test123 token.

```bash theme={null}
curl -H "Authorization: Bearer "demo_test123" https://api.testserver.com/user
```

Then the validation specification can be:

```bash expandable theme={null}
"validation": {
    "name": "Demo secrets validator",
    "http_request": {
        "header": [
            {
                "key": "Bearer",
                "value": "{{.AuthzValue}}",
                "authz": true
            }
        ],
        "method": "GET",
        "uri": "https://api.testserver.com/user"
    },
    "http_response": {
        "failed_auth_codes": [
            401
        ],
        "successful_auth_codes": [
            200
        ]
    }
}
```

#### Validator

You can use a validator to check if a discovered secret is valid or not. The Endor Labs system rules for secrets include the necessary validator. When you validate a secret, the finding for that secret is categorized as critical, ensuring it receives higher priority compared to others.

When defining a custom rule, you can add your own validator from the command line or from the user interface. The system uses this information to send an HTTP request such as a GET or POST to the address specified by the public service for the detected secret.

For example, when a GitHub Personal Access Token named "ghp\_endor123" is detected, the system sends the following HTTP request to GitHub's address:

```bash theme={null}
curl -H "Authorization: Token "ghp_endor123" https://api.github.com/user
```

The authentication codes defined by the service are used to mark the secrets as valid or invalid.

The validation portion of the secret rule contains the following fields:

<YamlTable>
  {`


    - Field: name
    Description: The name of the validator
    - Field: http_request.uri
    Description: The address where the HTTP request should be sent
    - Field: http_request.method
    Description: The HTTP method to be used (GET or POST)
    - Field: http_request.header.(key, val)
    Description: A set of key/value pairs that are added to the HTTP header. See [HTTP Request Header](#http-request-header)
    - Field: http_response.successful_auth_codes
    Description: The set of HTTP response codes that should be used to tag a secret as valid. For example, \`http.StatusOK (200)\`
    - Field: http_response.failed_auth_codes
    Description: The set of HTTP response codes that should be used to tag a secret as invalid. For example, \`http.StatusUnauthorized (401)\`


    `}
</YamlTable>

#### HTTP request header

HTTP request header is a set of key-value pairs that should be added to the header.

```bash theme={null}
{
    "key": "Content-Type",
    "value": "application/json"
}
```

There are cases where one needs to use a value on runtime and substitute a pattern. For example, the secret itself that needs to be substituted is one such case. This is achieved by declaring a value using the `{{.Value}}` pattern.

For the HTTP header section that includes the secret, the block looks like the following snippet.

```bash theme={null}
{
    "key": "Token",
    "value": "{{.AuthzValue}}",
    "authz": true,
}
```

In this case, the scanner replaces the candidate secret that was detected and adds it to the HTTP request header in place of `{{.AuthzValue}}`.

The following table describes a special case where the key-value pair is marked with the `authz` flag and is used to craft the "Authorization" part of the header, where three options are supported.

<YamlTable>
  {`


    - Key: Basic
    Header: 'Authorization: \`Basic "hash{{.AuthzValue}}"’\`'
    - Key: Bearer
    Header: 'Authorization: \`Bearer {{.AuthzValue}}\`'
    - Key: Token
    Header: 'Authorization: \`Token {{.AuthzValue}}\`'


    `}
</YamlTable>

## Manage secret rules

1. Select **Policies & Rules** from the left sidebar.
2. Select **Secret Rules**.

   The list of all secret rules appears.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/266r_aE9BWR2B51C/images/scan/secrets/secret-rules.webp?fit=max&auto=format&n=266r_aE9BWR2B51C&q=85&s=6968280d000494359b8b4b2aaf6751b4" alt="Secret rules" width="1896" height="1746" data-path="images/scan/secrets/secret-rules.webp" />
3. Select the rule for which you want to view the details.

   The rule details appear in the right sidebar.

   <img src="https://mintcdn.com/endorlabs-b4795f4f/W0xEjuxQ6UT-8MM3/images/scan/secrets/secret-rule-details.webp?fit=max&auto=format&n=W0xEjuxQ6UT-8MM3&q=85&s=84d6410b61a9d146992809da1abd41d4" alt="Secret rule details" style={{width: '50%'}} width="758" height="1276" data-path="images/scan/secrets/secret-rule-details.webp" />

### Clone a secret rule

Click the three vertical dots on the right side of the rule and select **Clone Rule**.

The cloned rule appears in the list of secret rules and you can edit it.

### Edit a secret rule

Click the three vertical dots on the right side of the rule and select **Edit Rule**.

You can only edit the custom rules that you created or the system rules that you cloned.

### Fetch secret rules with endorctl

To fetch the Endor Labs secret scanning rules from the command line type the following commands:

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

For example, to see the rule for the GitHub Personal Access Token, you could search by the name `GitHub Personal Access Token` or by the rule-id `github-pat`:

```bash theme={null}
endorctl api get -r SecretRule -n <your-namespace> --name "GitHub Personal Access Token"
endorctl api list -r SecretRule -n <your-namespace> --filter=spec.rule_id==github-pat
```
