Logs
Field-level reference for firewall log entries and the query and streaming API.
A firewall log entry records one of two things: a rule decision on a package request, or a failed call to an upstream registry. For reading them in practice, see View logs; for how they relate to the other records, see Observability.
Entry shape
{
"timestamp": "2026-07-03T09:14:02Z",
"user": "user-7d2f...",
"request": "8f4c2e...",
"interaction": "build-4711",
"userAgent": "npm/10.8.1 node/v22.4.0",
"remoteAddress": "203.0.113.7",
"firewall": "3e7f5cb806f26e3beae2",
"firewallVersion": 12,
"upstream": "npmjs",
"rule": "block-critical-vulns",
"executionPhase": "download",
"blocked": true,
"ecosystem": "npm",
"packageName": "lodash",
"version": "4.17.20",
"published": "2020-08-13T16:53:52Z",
"message": "Block critical vulnerabilities",
"output": { "vulnerabilities": ["CVE-2020-8203"] }
}Fields
| Field | Type | Description |
|---|---|---|
timestamp | string | When the decision was made |
user | string | Who made the request, as an opaque user ID (the token's OIDC subject), not an email or display name. PATs resolve to the user; SATs to the token's identity. The dashboard resolves IDs to names where it can |
request | string | Request ID. Also shown in blocked-install error messages, which is how a developer's error finds this entry |
interaction | string | Interaction ID from the token suffix, if any |
userAgent | string | Client user agent |
remoteAddress | string | Client address |
firewall | string | Firewall ID the request hit |
firewallVersion | number | Config version the request was evaluated under |
sourceFirewall | string | Firewall the matched rule came from, when it was inherited. Absent for the firewall's own rules |
sourceFirewallVersion | number | Config version of sourceFirewall |
upstream | string | Upstream that served or would have served the package |
rule | string | Rule that matched |
exception | string | Exception ID, when an exception allowed the request. Message is EXCEPTION |
exceptionSourceFirewall | string | Firewall the applied exception came from, when it was inherited. Absent for the firewall's own exceptions |
exceptionSourceFirewallVersion | number | Config version of exceptionSourceFirewall |
executionPhase | string | versions, download, or upload. Reflects the request, not the matched rule's phase: a versions rule that matches during an artifact fetch is recorded as download, so versions entries always come from metadata requests |
blocked | boolean | Whether the effect blocked the request |
ecosystem | string | Package ecosystem |
packageName | string | Package name |
version | string | Package version |
published | string | The version's publish date, when known |
message | string | Rule description, or rule-specific message such as Malware detected in npm/example@1.0.0: ... |
output | object | Rule-specific detail: vulnerability IDs and scores; malware finding summary, scanner, confidence, and signals; redacted secrets findings |
upstreamError | object | Present only on upstream error entries, which record a failed upstream call instead of a rule decision |
One request can produce several entries: each matching log-effect rule writes one, and a block ends the sequence.
Upstream error entries
A firewall fetches packages from its upstream registries, often more than one. When a call to one of them fails, the firewall answers with what the other upstreams returned, so the request often succeeds and the client sees nothing unusual. The failed call gets its own log entry, which is what makes a failing upstream visible.
An error entry describes a failed upstream call, not a decision about a package. It carries no rule and no exception, blocked is always false, and packageName and version are empty when the call failed before the firewall knew them.
{
"timestamp": "2026-08-20T11:02:41Z",
"user": "user-7d2f...",
"request": "3b91af...",
"userAgent": "npm/10.8.1 node/v22.4.0",
"firewall": "3e7f5cb806f26e3beae2",
"firewallVersion": 12,
"upstream": "npmjs",
"executionPhase": "versions",
"blocked": false,
"ecosystem": "npm",
"packageName": "lodash",
"version": "",
"message": "upstream request failed: rate limited by upstream (status 429) [https://registry.npmjs.org]",
"upstreamError": {
"endpoint": "https://registry.npmjs.org",
"url": "https://registry.npmjs.org/lodash",
"status": 429,
"reason": "rate limited by upstream"
}
}| Field | Type | Description |
|---|---|---|
upstream | string | The upstream that failed, by its configured ID |
upstreamError.endpoint | string | That upstream's endpoint |
upstreamError.url | string | The exact URL that failed |
upstreamError.status | number | The status the upstream returned. Absent when there was no response at all, such as a timeout or a hostname that did not resolve |
upstreamError.reason | string | Short description of the failure, such as rate limited by upstream or dns lookup failed |
upstreamError.count | number | How many identical failures this one entry stands for. Absent when it stands for a single failure |
What is recorded
- Rate limiting (
429), server errors (5xx), denied access (403), and any other unexpected status. 401from an upstream that has credentials stored. That is how a wrong or expired credential shows up; see Manage upstream credentials.- No response at all: a hostname that did not resolve, a request that timed out, a refused connection, or an address the firewall is not allowed to reach.
These are not recorded:
404and410. The upstream is saying it has no such package or version, which is a normal answer: the firewall asks the next upstream and carries on.401from an upstream that has no credentials stored. Some registries answer this way instead of404for a package the caller cannot see, so it means the same thing here.
If the same upstream fails the same way several times inside one request, the failures are collapsed into one entry with a count. Nothing is collapsed across requests: an upstream that keeps failing writes one entry per affected request, not one entry for the whole outage.
Entries are written for metadata and download requests in every ecosystem, and for NuGet search and autocomplete requests. A few fallback paths inside the firewall handle a failed call without recording it, so an entry is good evidence that an upstream is failing, while an empty result only means nothing was recorded.
Upstream error entries are never delivered to webhooks.
Query API
GET /v1/<namespace-id>/log| Parameter | Description |
|---|---|
firewall | Filter to one firewall |
user | Filter to a specific user, by raw user ID, not display name |
allUsers | true for entries from everyone. Mutually exclusive with user |
search | Free-text search |
phase | Only one execution phase: versions, download, or upload |
kind | rule for rule decisions, error for upstream error entries. Both kinds when omitted |
status | blocked, allowed, or error. Every outcome when omitted. The three are exclusive, and an upstream failure outranks a block: an entry that blocked and also recorded a failed upstream matches error, not blocked |
limit | Entries per page. Default 100, capped at 1000 |
before | Opaque cursor from a previous response's nextCursor, to fetch the next older page |
Without user or allUsers, the server returns the caller's own entries; this default is server-side, not a CLI convenience. Reading logs requires only namespace access.
{
"entries": [],
"nextCursor": "MTc1MTUzMjA0MjAwMDAwMDAwMDoxNDIz"
}Entries are newest first. nextCursor is an opaque token: pass it back as before for the next page, and treat an empty value as the end of the results. Do not parse it. An invalid limit, before, phase, kind, or status value is answered with 400.
Streaming
GET /v1/<namespace-id>/log/streamServer-sent events with the same parameters: 10 entries of backfill, then new entries as they happen, with a heartbeat every 20 seconds. This is what bsfw logs --follow and the dashboard's live view consume.
Related
- How-to: View logs, Investigate a blocked install
- Concept: Observability
- Reference: CLI, API