> ## 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 it takes on package installation requests, providing an audit trail for debugging and compliance. The logs include details such as the package name, version, event time, the reason the package was flagged, and the user who initiated the request.

To view Package Firewall logs:

1. Select **Package Firewall** from the left sidebar.
2. Select an event to view the following details:

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

   User Attribution shows who made the request, as `<user>@<machine>`. The MDM deployment scripts add this label to each machine's package manager credentials. See [User attribution](/package-firewall/mdm-deployment#user-attribution) to learn more.

   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.

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

* To list logs attributed to a specific developer and machine, use a filter with `spec.user`.

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

  Replace:

  * `<namespace>` with your namespace.
  * `<attributed-user>` with the attribution label you want to query, in the form `<user>@<machine>`.

* To count events for each attributed user, group logs by `spec.user`. Replace `<namespace>` with your namespace.

  ```bash theme={null}
  endorctl api list -r PackageFirewallLog -n <namespace> --group-aggregation-paths=spec.user
  ```

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.user\`
    Supported values: String
    Description: User the request is attributed to, in the form \`<user>@<machine>\`, and it is recorded only when the request carries an attribution label.
    - Filter: \`spec.api_key_name\`
    Supported values: String
    Description: Name of the API key used for the request.
    - 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>
