bsfw observations --ecosystem npm -o json
```
`--ecosystem` is applied by the CLI to the page the server returned, not by the endpoint, so a filtered page can come back empty while later pages still hold matching rows. Page through with `--page-size` and `--page-token`; the CLI prints the token to pass next when more rows are available. Flag-level detail: [CLI reference](/firewall/references/cli#bsfw-observations).
## Response shape [#response-shape]
```json
{
"observations": [
{
"firewall": "3e7f5cb806f26e3beae2",
"ecosystem": "npm",
"packageName": "lodash",
"version": "4.17.21",
"internal": false,
"firstSeen": "2026-05-12T08:03:11Z",
"lastSeen": "2026-07-03T07:41:56Z",
"count": 412,
"vulnerabilities": { "total": 0, "critical": 0, "high": 0, "moderate": 0, "low": 0, "noRisk": 0 }
}
],
"nextPageToken": ""
}
```
## Fields [#fields]
| Field | Type | Description |
| ----------------- | ------- | ------------------------------------------------------ |
| `firewall` | string | Firewall that served the package |
| `ecosystem` | string | Package ecosystem |
| `packageName` | string | Package name |
| `version` | string | Exact version observed |
| `internal` | boolean | Whether it was served from an internal upstream |
| `firstSeen` | string | First request for this version |
| `lastSeen` | string | Most recent request |
| `count` | number | Requests for this version in the period |
| `vulnerabilities` | object | Severity summary from current advisory data, see below |
## Vulnerability summary [#vulnerability-summary]
Observations are enriched with the current known vulnerabilities for each package version, bucketed by [effective CVSS score](/firewall/references/advisory-sources#cvss-score):
| Bucket | Score |
| ---------- | ------------- |
| `critical` | 9.0 and above |
| `high` | 7.0 to 8.9 |
| `moderate` | 4.0 to 6.9 |
| `low` | 0.1 to 3.9 |
| `noRisk` | below 0.1 |
How the effective score is chosen per vulnerability, including the vendor priority and the fallback from severity labels to scores, is described under [CVSS score on the advisory sources page](/firewall/references/advisory-sources#cvss-score).
The summary reflects advisory data at query time, not at observation time; a version observed in May shows the vulnerabilities known today. That is the useful direction: "what do we currently depend on that is now known-vulnerable" is answered by sorting observations by this summary.
## Related [#related]
* Concept: [Observability](/firewall/concepts/observability)
* How-to: [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages)
* Reference: [CLI](/firewall/references/cli#bsfw-observations), [Advisory sources](/firewall/references/advisory-sources), [Metrics](/firewall/references/metrics), [API](/firewall/references/api)
# Permissions (/firewall/references/permissions)
Permissions are verbs evaluated in a context: a namespace-level permission such as `fw:create` applies in the namespace, a firewall-level one such as `fw:update:rule` applies to a specific firewall through its grants. For how the levels combine, see [Access and permissions](/firewall/concepts/access-and-permissions).
## Catalog [#catalog]
### Namespace [#namespace]
| Permission | Allows |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `ns:access` | Any use of the namespace API; the baseline everything else builds on |
| `ns:update` | Rename the namespace and update its metadata |
| `ns:delete` | Delete the namespace |
| `ns:create:pat` | Create Personal Access Tokens |
| `ns:create:service-token` | Create Service Access Tokens |
| `ns:update:licenses` | Create, update, and delete [license sets](/firewall/references/api#license-sets). Reading them only needs `ns:access` |
| `ns:update:webhooks` | Create, update, delete, and test [webhooks](/firewall/references/webhooks). Reading them and their deliveries only needs `ns:access` |
Creating a namespace is not permission-gated: it requires the tenant admin or owner role on the session, so no team grant can enable it.
### Teams [#teams]
| Permission | Allows |
| ------------------------ | -------------------------------------------------- |
| `team:read` | List teams and read team details |
| `team:update` | Change a team's name, description, and permissions |
| `team:delete` | Delete a team |
| `team:update:membership` | Add and remove team members |
### Firewalls [#firewalls]
| Permission | Allows |
| ----------------------- | ------------------------------------------------------- |
| `fw:access` | Package traffic through the firewall (install, publish) |
| `fw:create` | Create a firewall in the namespace |
| `fw:clone` | Create a firewall from an existing one as template |
| `fw:read` | Read configuration, rules, and exceptions |
| `fw:update:config` | Update configuration such as upstreams |
| `fw:update:credentials` | Update upstream credentials |
| `fw:update:rule` | Change rules |
| `fw:update:exception` | Change exceptions |
| `fw:delete` | Delete the firewall |
One case needs permission on two firewalls at once. When a firewall [inherits](/firewall/how-to/share-configuration-across-firewalls) a rule, adding or changing an exception on it requires `fw:update:exception` on both the child and the source firewall that owns the rule. The source may be any ancestor in the chain. Deleting the exception needs it on the child only, and reading inherited rules and exceptions needs only `fw:read` on the child.
## Built-in team sets [#built-in-team-sets]
Every namespace has two protected teams that cannot be updated or deleted.
**everybody** (all users):
`ns:access`, `ns:create:pat`, `team:read`, `fw:create`, `fw:access`, `fw:clone`, `fw:read`
**admin** (everybody's set plus):
`ns:update`, `ns:delete`, `ns:create:service-token`, `ns:update:licenses`, `ns:update:webhooks`, `team:update`, `team:delete`, `team:update:membership`, `fw:update:config`, `fw:update:credentials`, `fw:update:rule`, `fw:update:exception`, `fw:delete`
## Assignable to custom teams [#assignable-to-custom-teams]
Custom teams can be granted:
`ns:access`, `ns:update`, `ns:delete`, `ns:create:pat`, `ns:create:service-token`, `ns:update:licenses`, `ns:update:webhooks`, `fw:access`, `fw:create`, `fw:clone`, `fw:read`, `fw:update:config`, `fw:update:credentials`, `fw:update:rule`, `fw:update:exception`, `fw:delete`
Team-management permissions (`team:*` beyond read) stay with the admin team.
## Available to Service Access Tokens [#available-to-service-access-tokens]
A SAT can carry at most:
`ns:access`, `ns:update`, `fw:access`, `fw:create`, `fw:clone`, `fw:read`, `fw:update:config`, `fw:update:rule`, `fw:update:exception`
Notably absent: namespace and firewall deletion, credential updates, team and token management. Those actions require a person.
## Defaults for a new firewall [#defaults-for-a-new-firewall]
A firewall created without an explicit `permissions` map gets the default grants:
| Team | Permissions on the new firewall |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `everybody` | `fw:access`, `fw:clone`, `fw:read` |
| Owner team | `fw:access`, `fw:clone`, `fw:read`, `fw:update:config`, `fw:update:credentials`, `fw:update:rule`, `fw:update:exception`, `fw:delete` |
The owner team is the `owner` named in the create request ([API](/firewall/references/api#configuration)); without one, the full set goes to `everybody`, which is also the dashboard's default. A create request carrying an explicit `permissions` map is stored verbatim, so exported configs import unchanged.
## Lockout guard [#lockout-guard]
The server rejects (400) saving a firewall `permissions` map that would make the firewall unmanageable:
* The resulting map must grant `fw:update:config` to at least one team.
* A user cannot remove their own team's `fw:update:config` grant; handing over management works by granting another team first, then having that team drop yours.
The guard runs on create, update, and rollback, and only when the permissions map actually changes, so tokens restricted to rule updates can keep re-applying full configs with an unchanged map.
Service Access Tokens are exempt from the self-lockout rule: their `fw:update:config` grant comes from the token, not from the team map being edited, so a map change cannot lock them out.
## Related [#related]
* Concept: [Access and permissions](/firewall/concepts/access-and-permissions)
* How-to: [Manage access](/firewall/how-to/manage-access), [Manage tokens](/firewall/how-to/manage-tokens)
* Reference: [Tokens](/firewall/references/tokens)
# Rules and selectors (/firewall/references/rules-and-selectors)
A firewall's policy consists of rules and exceptions inside its configuration. This page documents every field. For the mental model, see [Rule evaluation](/firewall/concepts/rule-evaluation).
## Rule [#rule]
```json
{
"id": "delay-new-versions",
"disabled": false,
"executionPhase": "versions",
"selector": { "maxAgeHours": 168 },
"ruleEffect": { "block": true, "log": true },
"description": "Block versions younger than 7 days"
}
```
| Field | Type | Description |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | string | Unique within the firewall. Referenced by exceptions and log entries |
| `disabled` | boolean | Skip the rule without deleting it. Default `false` |
| `executionPhase` | string | `versions`, `download`, or `upload`. A `versions` rule filters metadata responses and is also enforced when a client fetches a matching version directly. The dashboard rule form labels these phases **Versions**, **Download**, and **Upload** |
| `selector` | object | Which packages the rule applies to. See below |
| `ruleEffect` | object | What happens on match. See below |
| `description` | string | Free text. Shown in the dashboard, and in the log table's rule column in place of the rule ID |
### Evaluation order [#evaluation-order]
Rules are not evaluated in configuration order. A request is evaluated against the cheaper rules first, so it pays for an expensive check only once every cheaper rule has allowed it. Rules of equal cost keep their configuration order, inherited rules before the firewall's own.
The first matching rule with `block: true` ends evaluation. Two things follow. The block a client sees, and the rule recorded in the log entry, is not necessarily the first rule you configured. And rules after it are never evaluated, never log, and cost the request nothing: a package stopped by a `maxAgeHours` rule is never scanned for malware.
Treat the order as an implementation detail that can change. If you need a rule to be evaluated regardless of what else matches, do not rely on ordering; give it a selector that cannot overlap with the others.
## Selector [#selector]
All set fields must match (AND). Empty fields match everything.
| Field | Type | Description |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------- |
| `upstreamId` | string | Match packages served from this upstream |
| `upstreamType` | string | `"internal"`, `"external"`, or empty for any. Restricts the rule to packages resolved from upstreams of that type |
| `packageName` | string | Exact name, or one wildcard: `prefix*`, `*suffix`, or `*` |
| `version` | string | Exact version, same wildcard support. Compared as text, never parsed |
| `versionRange` | string | Version range in the ecosystem's own notation, compared by version semantics. See [Version ranges](#version-ranges) |
| `maxAgeHours` | number | Whole hours. Matches versions published more recently than this. `168` is 7 days |
| `function` | object | Named selector function with options. See [Selector functions](/firewall/references/selector-functions) |
### Wildcards [#wildcards]
`packageName` and `version` accept at most one `*`, at the start or the end:
| Pattern | Matches |
| --------- | ------------------------------- |
| `@acme/*` | Everything in the `@acme` scope |
| `*-beta` | Names ending in `-beta` |
| `*` | Everything |
| `lodash` | Exactly `lodash` |
### Package name matching [#package-name-matching]
`packageName` is matched in the naming form the ecosystem itself considers canonical, so a selector written the way the package appears on the registry works:
| Ecosystem | Matching |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| NuGet | Case-insensitive. `Newtonsoft.Json` and `newtonsoft.json` are the same package |
| PyPI | Case-insensitive, and `-`, `_`, `.` are interchangeable ([PEP 503](https://peps.python.org/pep-0503/)). `zope.interface`, `zope_interface`, and `Zope.Interface` all select `zope-interface` |
| Cargo | Case-insensitive. `-` and `_` are **not** interchangeable, so `cfg-if` and `cfg_if` are different crates |
| npm, Maven, Go, conda | Exact, case-sensitive. `React` does not select `react`, and a Maven coordinate or Go module path must be written exactly |
NuGet, PyPI, and Cargo have their own naming rules, and the firewall follows them. Everywhere else the name is matched as written, so a rule selects only the package it names.
Write the name the way the registry shows it. Most npm names are lowercase, but older ones such as `JSONStream` are not, and a rule written `jsonstream` does not match.
Wildcards are normalized the same way, so a NuGet selector `Microsoft.*` matches `microsoft.extensions.logging`. `version` is always compared exactly and is never normalized.
Exceptions use the same matching, which matters most there: an exception meant to unblock a package has to select it to take effect.
### Version ranges [#version-ranges]
`version` and `versionRange` answer different questions. `version` compares text, so `1.*` selects anything whose version string starts with `1.`. `versionRange` parses both sides and compares them as versions, so `>=1.2.0 <2.0.0` knows that `1.10.0` is newer than `1.9.0`. Use `version` to match a naming shape, `versionRange` to match a real interval.
The two are independent fields. Set both and a version has to satisfy both.
Each ecosystem keeps its own range notation:
| Ecosystem | Notation | Examples |
| --------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| npm | semver ranges | `^1.2.0`, `~1.2.3`, `>=1.2.0 <2.0.0`, `1.x`, `0.0.1 - 0.0.3 \|\| >2.0.1` |
| Go | semver ranges. A leading `v` on the package version is accepted, so `v1.5.0` matches `>=1.2.0 <2.0.0` | `>=1.2.0 <2.0.0` |
| NuGet | semver ranges, not NuGet interval notation | `>=1.0.0 <2.0.0` |
| Maven | interval notation | `[1.0,2.0)`, `[1.0,2.0]` |
| PyPI | [PEP 440](https://peps.python.org/pep-0440/) specifiers | `>=1.0,<2.0`, `~=1.4.2` |
| Cargo | semver ranges, plus comma-separated AND terms as `Cargo.toml` writes them | `^1.2.0`, `~1.2.3`, `>=1.2, <1.5` |
| Composer | Composer constraints | `^1.2`, `~1.2`, `>=1.2 <2.0`, `1.2.*`, `^1.2 \|\| ^2.0` |
| conda | not supported. The dashboard hides the field, and the API rejects a range on a conda firewall | |
Composer constraints look like semver but do not mean the same thing. `~1.2` is `>=1.2 <2.0` in Composer and `>=1.2 <1.3` in npm, so a range copied between the two ecosystems changes meaning silently. Write Composer ranges the way you would write them in `composer.json`; the firewall parses them with Composer's own rules, including branch versions and four-segment versions that strict semver rejects.
Semver ranges support `||` for alternatives, `X - Y` for an inclusive span, `x`, `X`, or `*` as a wildcard component, and the operators `=`, `!=`, `>`, `<`, `>=`, `<=`, `~`, `^`. As in npm itself, a prerelease only matches a range that mentions a prerelease: `1.2.0-beta.1` does not satisfy `^1.2.0`.
NuGet ranges go through the semver parser, deliberately, so that range syntax is the same across ecosystems that can share it. NuGet's own bracket intervals are therefore not accepted, and `[1.0.0,2.0.0)` is rejected when you save the rule. NuGet **versions** keep their own rules, so a four-part version such as `1.5.0.3` still parses and compares correctly.
Saving a rule whose range the ecosystem cannot parse fails with an error. At request time, a package version the ecosystem cannot parse does not match the range.
## Selector functions [#selector-functions]
| Function | Phases | Ecosystems | Options |
| ------------------- | --------------------------------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `vulnerabilities` | download | all except Conda | `maxCvssScore` (0 to 10), `maxEpssScore` (0 to 1) |
| `vulnerability-fix` | versions, download | all except Conda | none; intended for exceptions |
| `malware` | download, upload | all | `minimumConfidence` (`known_malware`/`suspicious`/`weak_indicator`)
`effort` (`known`/`deep`) |
| `install-scripts` | download, upload (npm, PyPI)
versions, download (Composer) | npm, PyPI, Composer | none |
| `deprecated` | versions, download | npm, PyPI, NuGet, Composer, Cargo | none |
| `unlisted` | versions, download | NuGet | none |
| `secrets` | upload | all | `ignoredFiles`, `excludeTestFiles` |
| `license` | download | all; Conda supports `unknown` only | `licenseSet`, `mode` (`deny`/`allow`), `unknown` |
| `trust-downgrade` | versions | npm | none |
Full semantics, options, and dashboard availability per function: [Selector functions](/firewall/references/selector-functions).
The API rejects rules and exceptions whose function does not support the firewall's ecosystem. It also rejects rules whose function cannot evaluate in the selected phase. Full config imports and rollbacks apply the same validation.
## Effect [#effect]
| Field | Type | Description |
| ------- | ------- | ----------------------------------------- |
| `block` | boolean | Stop the request |
| `log` | boolean | Write a firewall log entry on every match |
`{ "block": false, "log": true }` is a dry-run rule, the standard way to trial a policy.
## Exceptions [#exceptions]
An exception waives one rule for requests matching its own selector, with an expiry and a reason. Field-level reference: [Exceptions](/firewall/references/exceptions).
## Related [#related]
* Concept: [Rule evaluation](/firewall/concepts/rule-evaluation)
* Reference: [Selector functions](/firewall/references/selector-functions), [Exceptions](/firewall/references/exceptions), [Firewall config](/firewall/references/firewall-config)
* How-to: [Delay new versions](/firewall/how-to/delay-new-versions), [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages), [Manage exceptions](/firewall/how-to/manage-exceptions)
# Selector functions (/firewall/references/selector-functions)
A selector function is the computed condition in a rule's selector: instead of matching what a package is (name, version, upstream), it matches what is known about it. This page is the exhaustive reference; for how selectors fit into evaluation, see [Rule evaluation](/firewall/concepts/rule-evaluation).
A selector has at most one function, combined with the plain fields by AND:
```json
{
"function": {
"name": "vulnerabilities",
"options": { "maxCvssScore": 9.0, "maxEpssScore": 0.5 }
}
}
```
## Matrix [#matrix]
| Function | Phases | Ecosystems | Options | In dashboard |
| ------------------- | --------------------------------------------------------------------- | ---------------------------------- | ---------------------------------- | --------------- |
| `vulnerabilities` | download | all except Conda | `maxCvssScore`, `maxEpssScore` | yes |
| `vulnerability-fix` | versions, download | all except Conda | none | exceptions only |
| `malware` | download, upload | all | `minimumConfidence`, `effort` | yes |
| `install-scripts` | download, upload (npm, PyPI)
versions, download (Composer) | npm, PyPI, Composer | none | yes |
| `deprecated` | versions, download | npm, PyPI, NuGet, Composer, Cargo | none | yes |
| `unlisted` | versions, download | NuGet | none | yes |
| `secrets` | upload | all | `ignoredFiles`, `excludeTestFiles` | yes |
| `license` | download | all; Conda supports `unknown` only | `licenseSet`, `mode`, `unknown` | yes |
| `trust-downgrade` | versions | npm | none | yes |
A dashboard label is presentation text for the API/config value. For example,
**Versions** maps to `versions`, **Install-time code execution** to
`install-scripts`, and **Trust downgrade** to `trust-downgrade`.
Saving a rule or exception whose function does not support the firewall's ecosystem fails with 400. A rule is also rejected when its function cannot evaluate in the selected phase. For `install-scripts`, npm and PyPI accept `download` and `upload`, while Composer accepts `versions` and `download`. The same validation applies to full config imports and rollbacks.
Functions differ in what they cost to evaluate, and rules run cheapest first, so a rule carrying an expensive function is reached only when every cheaper rule has allowed the request. See [Evaluation order](/firewall/references/rules-and-selectors#evaluation-order) and treat the exact order as an implementation detail.
## vulnerabilities [#vulnerabilities]
Matches when the package version has a known vulnerability exceeding **both** thresholds: CVSS score strictly above `maxCvssScore` (0 to 10) and EPSS probability strictly above `maxEpssScore` (0 to 1). The dashboard defaults are `maxCvssScore: 5`, `maxEpssScore: 0`.
Not supported for Conda firewalls: the dashboard does not offer the function there, and the API rejects a JSON-configured rule or exception.
The log entry's rule output records the vulnerability IDs and the worst CVSS and EPSS values that triggered the match.
```json
{
"name": "vulnerabilities",
"options": { "maxCvssScore": 9.0, "maxEpssScore": 0.5 }
}
```
See [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages). Where the data comes from and how scores are derived when data is missing: [Advisory sources](/firewall/references/advisory-sources).
## vulnerability-fix [#vulnerability-fix]
Matches a version identified by a current advisory as a published fix for a known vulnerability. The lookup follows each ecosystem's version rules, so equivalent version forms such as a Go version with a leading `v` can match the advisory data. Withdrawn advisories do not match.
This function is for exceptions. The dashboard offers **Vulnerability Fixes** as a condition when an exception belongs to a versions- or download-phase rule on an npm, Go, Maven, PyPI, NuGet, Composer, or Cargo firewall. It is not available for Conda.
The common use is an exception on a [delay rule](/firewall/how-to/delay-new-versions): a recognized security-fix release can pass before the normal age window ends, while other new versions remain delayed. The exception affects only its selected rule; all other rules still evaluate the release.
A matching log entry's output records the vulnerability ID behind the match. The function has no options.
```json
{
"name": "vulnerability-fix"
}
```
See [Manage exceptions](/firewall/how-to/manage-exceptions).
## malware [#malware]
Matches when a malware finding meets the rule's minimum confidence. Runs on the artifact itself, so it applies to downloads and uploads only.
`minimumConfidence` accepts `known_malware` (default), `suspicious`, or `weak_indicator`. It names the weakest finding that matches, so `suspicious` also matches `known_malware`, and `weak_indicator` matches all three levels. Any other value is rejected.
`effort` accepts `known` (default) or `deep`. Under `known` the rule decides on findings that already exist for the artifact: threat-intelligence datasets, plus any content analysis that has already been done. Under `deep` the artifact's contents are analyzed as part of the request and the rule waits for that result, which is more thorough and slower, and slowest on the first request for an artifact nobody has analyzed yet. Any other value is rejected.
Both options are independent. Effort decides how much is known when the rule runs, confidence decides how weak a finding may be and still match.
Omitting `options` is valid and means `known_malware` at `known` effort.
A matching log entry's output contains the finding summary in `malware`, plus `scanner`, `confidence`, and `signals`.
```json
{
"name": "malware",
"options": { "minimumConfidence": "known_malware", "effort": "known" }
}
```
See [Block malware](/firewall/how-to/block-malware).
## install-scripts [#install-scripts]
Matches releases that can execute package-author code during installation or build.
* **npm:** any of `preinstall`, `install`, or `postinstall` in the package's `package.json` `scripts`.
* **PyPI:** a source distribution (`.tar.gz` or `.zip`) with a root `setup.py`, or a root `pyproject.toml` whose `[build-system]` declares `backend-path`. Wheels and external build backends without `backend-path` do not match.
* **Composer:** package type `composer-plugin` or the legacy `composer-installer`, read from repository metadata. Other package types do not match.
npm and PyPI inspect artifacts in the download and upload phases. Composer reads metadata, so it supports versions and download rules.
The log entry's message names the detected hooks. Rule output carries a `scripts` array for npm; a `findings` array with `kind`, `path`, and optional `backend` and `backendPath` for PyPI; or `packageType` and optional `pluginClasses` for Composer.
```json
{
"name": "install-scripts"
}
```
See [Block install-time code execution](/firewall/how-to/block-install-scripts).
## deprecated [#deprecated]
Matches versions their publisher has marked as discouraged: npm deprecation messages, PyPI yanked releases (PEP 592), NuGet deprecations, Composer abandoned packages, Cargo yanked crate versions. For an abandoned Composer package the log entry's message names the replacement package when the maintainer gave one.
```json
{
"name": "deprecated"
}
```
## unlisted [#unlisted]
NuGet only. Matches versions hidden from search results (`listed: false` in the catalog entry). On nuget.org, unlisted versions remain installable by anyone who knows the version; a rule with this selector blocks them.
```json
{
"name": "unlisted"
}
```
## secrets [#secrets]
Matches when scanning the published artifact's contents finds at least one potential leaked credential. Runs on the upload phase only, so it acts on publishes, not installs. Detection covers several hundred credential formats, including Bytesafe's own [access token](/firewall/references/tokens) formats.
Both options manage false positives:
| Option | Meaning |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ignoredFiles` | Findings whose path within the package matches one of these patterns are ignored. Exact path or a single leading/trailing `*` wildcard, as for `packageName`, but always matched case-sensitively |
| `excludeTestFiles` | `true`: ignore findings in test-looking paths (segments `test`, `tests`, `__tests__`, `spec`, `specs`, `testdata`, `fixtures`; file names ending `_test`, `.test`, `.spec` before the extension). Default `false` |
The log entry's rule output lists the findings (rule, file, line, fingerprint) with the secret value redacted, capped at 20 entries with `findingsTotal` and `truncated` carrying the full count.
```json
{
"name": "secrets",
"options": { "ignoredFiles": ["package/docs/*"], "excludeTestFiles": true }
}
```
See [Scan for secrets on publish](/firewall/how-to/scan-for-secrets).
## license [#license]
Matches a version by its resolved licenses (from a public license data source, falling back to the license declared in npm, NuGet, or Composer package metadata) against a [license set](/firewall/references/api#license-sets), or matches versions with no resolvable license at all. Runs on downloads only. Composer has no public license data source, so a Composer version resolves only from what its Packagist metadata declares; several declared licenses are read as a disjunction. Conda has no license data source, so Conda versions resolve as unknown license and only the `unknown` option has an effect there.
Exactly one of `licenseSet` and `unknown` must be set:
| Option | Meaning |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `licenseSet` | ID of the license set to match against |
| `mode` | `deny` (default): match when any resolved license is in the set. `allow`: match when licenses resolved but none is in the set |
| `unknown` | `true`: match when no license information could be resolved. No `licenseSet` allowed |
License expressions are flattened to their mentioned IDs (no `OR`/`AND` evaluation, `WITH` exceptions dropped); matching is case-insensitive. Rules referencing a nonexistent set are rejected on save, and a set referenced by a rule cannot be deleted.
The log entry's rule output records the resolved `licenses`, the `licenseSet`, and the data `source`.
```json
{
"name": "license",
"options": { "licenseSet": "", "mode": "deny" }
}
```
See [Enforce license policy](/firewall/how-to/enforce-license-policy).
## trust-downgrade [#trust-downgrade]
npm only. Matches versions that have weaker publishing trust evidence than any earlier release of the same package, the pattern of an account-takeover release. In the dashboard, it appears in the rule form's function list on an npm firewall with the versions phase selected. Optionless.
Trust is ranked from strongest to weakest:
1. Trusted publisher
2. Provenance attestation
3. No trust evidence
A release with only provenance matches if an earlier release used a trusted publisher. In a blocking rule, downgraded versions cannot be selected or downloaded, and releases with equal or stronger trust stay available.
Versions are compared by publication date, not by version number. Prereleases are not used as the trust baseline for stable releases.
The comparison needs the package's other versions. That set is available on npm metadata requests and on npm artifact requests, which is why a versions-phase rule reaches a direct fetch of a downgraded version as well.
```json
{
"name": "trust-downgrade"
}
```
## Unknown names [#unknown-names]
Config with a function name outside this list is rejected on parse (`unknown selector function`), so a typo fails the import instead of silently never matching.
## Related [#related]
* Reference: [Rules and selectors](/firewall/references/rules-and-selectors)
* Concept: [Rule evaluation](/firewall/concepts/rule-evaluation)
* How-to: [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages), [Block malware](/firewall/how-to/block-malware)
# Tokens (/firewall/references/tokens)
Reference for the token formats and rules. For which token to use where, see [Access Tokens](/firewall/getting-started/access-tokens); for workflows, see [Manage tokens](/firewall/how-to/manage-tokens).
## Types [#types]
| Type | Prefix | Bound to | Created via | Permission required |
| --------------------------- | ------ | ---------------------- | --------------- | -------------------------- |
| Personal Access Token (PAT) | `pat-` | A user | Dashboard / API | `ns:create:pat` |
| Service Access Token (SAT) | `sat-` | The namespace | Dashboard / API | `ns:create:service-token` |
| Trusted Builder token | `sat-` | A CI workflow identity | OIDC exchange | Registered Trusted Builder |
Tokens are opaque: a prefix plus a random UUID. The server stores only a hash; the value is shown once at creation and cannot be recovered.
A Trusted Builder token is a SAT minted through an OpenID Connect (OIDC) exchange instead of created directly; see [Trusted Builders](/firewall/getting-started/access-tokens#trusted-builders-oidc) for setup.
## Lifetime [#lifetime]
| Path | Default | Maximum |
| ------------------------ | ---------- | ---------------------------------------- |
| API (`validForHours`) | 12 hours | 2 years |
| Dashboard presets | 7 days | 1 year (presets: 1, 7, 30, 90, 365 days) |
| Trusted Builder exchange | 15 minutes | 15 minutes |
Expiry is checked on every request. Deleting a token has the same immediate effect.
## Permissions [#permissions]
Token permissions are selected when the token is created. They cannot be changed later; create a new token and delete the old one to change scope.
| Token | Create permission | Permission behavior |
| --------------------- | -------------------------- | -------------------------------------------------------------------------- |
| PAT | `ns:create:pat` | Acts as the user, narrowed to the selected permissions and firewall IDs |
| SAT | `ns:create:service-token` | Acts as the namespace, with only the selected permissions and firewall IDs |
| Trusted Builder token | Registered Trusted Builder | Uses the Trusted Builder registration permissions and firewall IDs |
The server rejects token creation when the requested permissions are broader than the creator's own permissions. If the creator is already using a firewall-restricted token, the new token cannot include firewalls outside that restriction.
SATs can carry at most the [Service Access Token permission catalog](/firewall/references/permissions#available-to-service-access-tokens). SATs cannot manage upstream credentials, teams, token creation, token deletion, namespace deletion, or firewall deletion.
## Scope claims [#scope-claims]
A token can carry two optional restrictions, set at creation and immutable afterwards:
| Claim | Effect |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `permissions` | The token acts with at most these permissions. For PATs this narrows the user's team permissions; for SATs it is the token's entire permission set, capped by the [SAT catalog](/firewall/references/permissions#available-to-service-access-tokens) |
| `firewalls` | The token works only on the listed firewalls |
## Wire format [#wire-format]
The token goes in the `Authorization` header, either directly or as basic auth (the password is used as the token; the username if the password is empty):
```text
Authorization: Bearer pat-3f2a...
Authorization: Basic base64(anything:pat-3f2a...)
```
NuGet clients may send `X-NuGet-ApiKey` instead; the firewall accepts it equivalently on NuGet routes.
### Interaction ID suffix [#interaction-id-suffix]
Any token may carry an opaque correlation ID after a double colon:
```text
pat-3f2a...::build-4711
```
The firewall strips the suffix before validating and records it on every log entry from that request. `::` never occurs in token values, so the split is unambiguous.
### Credential envelope [#credential-envelope]
For just-in-time upstream credentials, a token can be wrapped in an envelope: `env-` followed by base64 (RawURL) JSON bundling the real token and per-upstream credentials:
```json
{
"tkn": "sat-9c41...",
"crd": { "e2de947a": { "token": "" } }
}
```
The envelope replaces the token in the `Authorization` header. The interaction ID suffix stays outside the envelope (`env-...::build-4711`). Maximum encoded size is 64 KiB. See [bsfw firewall credentials](/firewall/references/cli#bsfw-firewall-credentials).
## Validation summary [#validation-summary]
A request is rejected when the token is unknown, expired, deleted, or used outside its namespace; when a `firewalls` claim does not list the target firewall; or when the action needs a permission the token does not carry. Failed attempts are recorded in the audit log with a non-reversible token identifier, so repeated failures are correlatable without exposing token material.
## Related [#related]
* Getting started: [Access Tokens](/firewall/getting-started/access-tokens)
* How-to: [Manage tokens](/firewall/how-to/manage-tokens)
* Concept: [Access and permissions](/firewall/concepts/access-and-permissions)
* Reference: [Permissions](/firewall/references/permissions), [CLI](/firewall/references/cli)
# Upstreams (/firewall/references/upstreams)
An upstream is one registry a firewall resolves packages from. Upstreams live in the `upstreams` array of the [firewall config](/firewall/references/firewall-config). For behavior and patterns, see [Upstreams and caching](/firewall/concepts/upstreams-and-caching) and [Configure upstreams](/firewall/how-to/configure-upstreams).
## Shape [#shape]
```json
{
"id": "e2de947a",
"endpoint": "https://npm.acme.internal",
"credentials": {
"username": "",
"password": "",
"token": ""
},
"internal": true,
"cached": true
}
```
## Fields [#fields]
| Field | Type | Description |
| --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id` | string | Required, and unique within the firewall. Referenced by `publishTarget`, the `upstreamId` selector field, and log entries. A missing or duplicate id is rejected when you save |
| `endpoint` | string | Base URL of the registry, in the ecosystem's native protocol. Must be an absolute `http://` or `https://` URL including a host; anything else is rejected when you save |
| `credentials` | object | `username`, `password`, `token`; all optional. See below |
| `internal` | boolean | Marks this registry as the authority for its packages. When any internal upstream knows a package name, only internal upstreams' versions are served |
| `cached` | boolean | Cache responses from this upstream (metadata for a short interval, plus fetched artifacts). Applies to upstreams marked `internal` |
| `artifactCredentials` | array | Credentials for artifact hosts that are not this upstream's own host. Empty for every ecosystem whose package files live on the registry itself. See [Artifact host credentials](#artifact-host-credentials) |
## Credentials [#credentials]
The auth scheme is inferred from which fields are set; there is no separate auth-type setting.
* On fetches, `token` is sent as `Authorization: Bearer`. Without a token, `username`/`password` are sent as HTTP basic auth. If both are set, the token wins. One compat case: Azure Artifacts endpoints (`pkgs.dev.azure.com`) receive a token as basic auth in addition to the bearer header.
* Cargo is the exception to the bearer rule. A Cargo registry takes the token exactly as configured, so the firewall sends `Authorization: ` with no scheme, and adds no Azure overlay. If your registry expects a scheme, store it as part of the token value.
* On publishes forwarded to the publish target, npm follows the fetch behavior above; Maven and NuGet send `token` as a bearer header (NuGet additionally as `X-NuGet-ApiKey`) and fall back to `username`/`password` as basic auth; PyPI sends a token as `__token__` basic auth (the twine convention), or `username`/`password` as basic auth; Cargo uses the same verbatim token it uses for reads.
* Encrypted at rest. API responses and exports never contain the plaintext; they carry a placeholder value that preserves an existing stored credential on import.
* Config import rejects plaintext credential values. Leave placeholders untouched in exported files. A placeholder for a new firewall or upstream resolves to empty because that target has no stored credential; set it separately after import.
### Artifact host credentials [#artifact-host-credentials]
Some registries serve metadata and package files from different hosts. Packagist is the current case: its metadata can point a download to a separate artifact host, and most Packagist dist URLs use `api.github.com`.
Credentials set on the upstream are only ever sent to the upstream's own host. A different host is only given credentials if it has its own entry:
```json
{
"id": "packagist",
"endpoint": "https://repo.packagist.org",
"credentials": { "username": "", "password": "", "token": "" },
"internal": false,
"cached": false,
"artifactCredentials": [
{
"host": "api.github.com",
"credentials": { "username": "", "password": "", "token": "" }
}
]
}
```
* `host` is a hostname, optionally with a port (`registry.internal:8443`), never a URL. It is matched case-insensitively against the download URL's host; when both a port-qualified and a bare entry exist, the port-qualified one wins.
* If no entry matches, the firewall fetches from that host anonymously. Configuration still succeeds, but the host can later refuse a download after its anonymous rate limit is exhausted.
* Set them with `PUT /config//upstreams//artifact-credentials/`, which takes a plaintext body and merges per field, exactly like the upstream's own credentials endpoint. Currently there is no `bsfw` command for it.
* Encrypted at rest, and redacted in API responses, exports, and webhook events, like every other credential.
Composer is the only ecosystem whose downloads use these today. For Composer upstreams, the dashboard provides a field for `api.github.com`; use the API for another artifact host. See [the Composer artifact host token](/firewall/ecosystems/composer#the-artifact-host-token).
## Cache behavior [#cache-behavior]
* `cached: true` requires `internal: true`. An external upstream with caching enabled is rejected with `400`: `upstream : caching is only allowed on internal upstreams`. The rule holds for the dashboard, the API, and `bsfw firewall import` alike.
* `cached: false` disables caching for the upstream, with one override: Maven Central endpoints (`https://repo1.maven.org/maven2`, `https://repo.maven.apache.org/maven2`) are always cached. Every other Maven upstream, including a repository manager, follows its own flag.
* Maven `-SNAPSHOT` artifacts are never cached, even from a cached upstream, so a rebuilt snapshot is always fetched fresh.
* Cache keys include the firewall config version, so a config change is never answered from cache entries built under the previous version. The change itself propagates across the cluster within a few seconds.
* Cache keys include the upstream's credentials, and version listings from an upstream with any credentials set are cached per namespace, for a shorter interval than listings from an upstream without credentials.
## Failure behavior [#failure-behavior]
An upstream that cannot be reached, or rejects the firewall's credentials, is skipped during resolution. The firewall answers with what the remaining upstreams returned, so a request another upstream can serve is unaffected, and any version held only by the skipped upstream drops out of the version list.
Upstreams are not copies of each other, so that only helps when another upstream has the package. When the skipped upstream was the only one holding it, or the firewall has a single upstream, nothing is left to serve and the request fails:
* npm, PyPI, NuGet, Go, Cargo, Composer, and Conda answer as if the package does not exist.
* Maven reports the failure instead, so the client sees a retryable status: an upstream `429` is relayed as `429`, anything else becomes `502`. This is deliberate, so a failing upstream is not mistaken for a coordinate that was never published.
A failure after resolution, while the artifact itself is being fetched, ends in a server error rather than a not-found. Only an upstream `404` or bytes that have aged out of the cache produce a not-found at that point.
In every case the client error names the package, never the upstream behind it. The skip is recorded in the firewall log as an [upstream error entry](/firewall/references/logs#upstream-error-entries), naming the upstream and what it returned. An upstream answering that it has no such package is a normal part of resolution and records nothing.
When the upstream is cached and the firewall already holds a copy of what was requested, it can answer from that copy instead of skipping the upstream. That covers a short outage for content already fetched, and nothing else: a DNS failure and an upstream `404` are not covered.
## Related [#related]
* Concept: [Upstreams and caching](/firewall/concepts/upstreams-and-caching)
* How-to: [Configure upstreams](/firewall/how-to/configure-upstreams), [Prevent dependency confusion](/firewall/how-to/prevent-dependency-confusion)
* Reference: [Firewall config](/firewall/references/firewall-config)
# Webhooks (/firewall/references/webhooks)
A webhook pushes a namespace's events to your own systems as they happen, instead of you polling the logs: alert the team when a rule blocks a build, stream audit events into a SIEM, diff configuration changes against the [git copy](/firewall/getting-started/config-as-code), or track successful publishes in an inventory.
A webhook subscribes an HTTPS endpoint to the event types you choose: [firewall log](/firewall/references/logs) decisions, [audit](/firewall/references/audit-log) events, configuration changes, and successful publishes. Every delivery is signed so the receiver can verify it, and failed deliveries are retried with at-least-once delivery from buffering to retry exhaustion.
This page is the exhaustive reference for what a receiver sees on the wire and how delivery behaves; for setup, see [Receive webhook events](/firewall/how-to/receive-webhook-events). Management endpoints and permissions: [API](/firewall/references/api#webhooks).
## Event types [#event-types]
| Type | Fires when |
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `firewall.blocked` | A firewall rule blocked a download or publish |
| `firewall.exception-allowed` | A rule matched but an exception let the request through |
| `firewall.allowed` | A log-only rule matched an allowed request |
| `audit.