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

# View Package Firewall logs

> View, filter, and query the events the Package Firewall records for blocked and warned package installations.

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

Package Firewall records the actions the firewall takes on package installation requests, which is helpful for debugging and compliance. The logs include details such as the package, version, time of the event, and the reason the firewall flagged the package.

To view Package Firewall logs:

1. Select **Package Firewall** from the left sidebar.

<Draft>
  <img src="https://mintcdn.com/endorlabs-b4795f4f/Cq2jQsQZUU2aI4dH/images/package-firewall/pkg-firewall.webp?fit=max&auto=format&n=Cq2jQsQZUU2aI4dH&q=85&s=687f6a44f42fe84fa3c046b7a28bd07e" alt="Package Firewall" width="2540" height="1394" data-path="images/package-firewall/pkg-firewall.webp" />
</Draft>

2. Select an event to view the following details:

   * **Info**: Package name, package version, API key, remote address, request URL, action taken, the reason the event was flagged, and when the event occurred.

   For malware events, you can also view the following details:

   * **Risk Details**: Explanation of why the package was flagged and remediation guidance.
   * **Metadata**: Ecosystem, package release date, advisory published date, CWE ID, and OSV ID when available.
   * **Malware Info**: Malware detection record in raw JSON format.

   For minimum package age events, you can also view the package age in hours.

   For restricted license events, you can also view the detected package license.

   For vulnerability events, you can also view the severity of the detected vulnerability.

<Draft>
  <img src="https://mintcdn.com/endorlabs-b4795f4f/Cq2jQsQZUU2aI4dH/images/package-firewall/pkg-firewall-log.webp?fit=max&auto=format&n=Cq2jQsQZUU2aI4dH&q=85&s=d82dee0ba9055f1b1e79016a71e7c0ef" alt="Package Firewall log" width="2758" height="1278" data-path="images/package-firewall/pkg-firewall-log.webp" />
</Draft>

## Filter Package Firewall logs

Use filters to narrow Package Firewall logs by ecosystem, action, rule reason, or time.

1. Select **Package Firewall** from the left sidebar.
2. Toggle the filter panel to show the filters.
3. Set any of the following filters to narrow the log list.

   * **Ecosystem** - Filter logs by their package ecosystem.
   * **Action**: Filter logs by the action taken on the package installation, either **Warning** or **Blocked**.
   * **Reason**: Filter logs by why the package was flagged, which can be **Malware detected**, **Minimum package age not met**, or **Restricted license**.
   * **All Time** - Filter logs by when the event was recorded. You can select **All Time**, **Last Day**, **Last Week**, **Last Month**, **Last 60 Days**, **Last 90 Days**, or a custom range.

   You can use the same filters to query logs through `endorctl`. See [Query Package Firewall logs using endorctl](#query-package-firewall-logs-using-endorctl).

## Query Package Firewall logs using endorctl

The Package Firewall logs record every action the firewall takes on package installation requests. You can view them by querying the `endorctl` API.

* To list all Package Firewall logs in your namespace, run the following command. Replace `<namespace>` with your namespace.

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

* To list logs only for a specific ecosystem, add a filter.

  ```bash theme={null}
  endorctl api list -r PackageFirewallLog -n <namespace> --filter 'spec.ecosystem==<ecosystem_variable>'
  ```

  Replace:

  * `<ecosystem_variable>` with `ECOSYSTEM_NPM` for npm, `ECOSYSTEM_PYPI` for PyPI, `ECOSYSTEM_GO` for Go, and `ECOSYSTEM_MAVEN` for Maven.
  * `<namespace>` with your namespace.

* To list logs for a specific package in an ecosystem, use a filter with `spec.ecosystem`, `spec.package_name`, and `spec.package_version`.

  ```bash theme={null}
  endorctl api list -r PackageFirewallLog -n <namespace> --filter 'spec.ecosystem==<ecosystem_variable> and spec.package_name=="<package_name>" and spec.package_version=="<package_version>"'
  ```

  Replace:

  * `<namespace>` with your namespace.
  * `<ecosystem_variable>` with `ECOSYSTEM_NPM` for npm, `ECOSYSTEM_PYPI` for PyPI, `ECOSYSTEM_GO` for Go, and `ECOSYSTEM_MAVEN` for Maven.
  * `<package_name>` with the package name you want to query.
  * `<package_version>` with the package version you want to query.

You can use a combination of filters to narrow your query.

<YamlTable>
  {`

    - Filter: \`spec.ecosystem\`
    Supported values: \`ECOSYSTEM_NPM\`, \`ECOSYSTEM_PYPI\`, \`ECOSYSTEM_GO\`, \`ECOSYSTEM_MAVEN\`
    Description: Ecosystem of the package.
    - Filter: \`spec.package_name\`
    Supported values: String
    Description: Name of the package in the ecosystem's normal form.
    - Filter: \`spec.package_version\`
    Supported values: String
    Description: Version of the package resolved for the request, when available.
    - Filter: \`spec.action\`
    Supported values: \`PACKAGE_FIREWALL_ACTION_WARN\`, \`PACKAGE_FIREWALL_ACTION_BLOCK\`
    Description: Action the firewall took on the request.
    - Filter: \`spec.reason\`
    Supported values: \`PACKAGE_FIREWALL_REASON_MALWARE_DETECTED\`, \`PACKAGE_FIREWALL_REASON_CVSS_SEVERITY_DETECTED\`, \`PACKAGE_FIREWALL_REASON_MIN_AGE_NOT_MET\`, \`PACKAGE_FIREWALL_REASON_RESTRICTED_LICENSE\`
    Description: Reason the package was flagged.
    - Filter: \`spec.package_age_hours\`
    Supported values: Integer
    Description: Number of hours since the package was published. Use when the reason is set to \`PACKAGE_FIREWALL_REASON_MIN_AGE_NOT_MET\`.
    - Filter: \`spec.package_license\`
    Supported values: String
    Description: License detected on the package. Use when the reason is set to \`PACKAGE_FIREWALL_REASON_RESTRICTED_LICENSE\`.
    - Filter: \`spec.cvss_severity_level\`
    Supported values: \`CVSS_SEVERITY_LEVEL_HIGH\`, \`CVSS_SEVERITY_LEVEL_CRITICAL\`
    Description: Severity of the detected vulnerability. Use when the reason is set to \`PACKAGE_FIREWALL_REASON_CVSS_SEVERITY_DETECTED\`.

    `}
</YamlTable>

<Note>
  The API key created with `SYSTEM_ROLE_PACKAGE_FIREWALL` routes traffic through the Package Firewall. It does not grant access to the Package Firewall Log API.

  To query logs, create an API key with at least the **Read-only** role. For more information about roles and permissions, see [Authorization roles](/platform-administration/rbac/authorization-roles).
</Note>
