# Book a demo (/firewall/book-a-demo) See how the Bytesafe Dependency Firewall controls which open source packages reach your developers and CI. Book a 30-minute call with our team. We'll focus the demo on your package ecosystems, current setup, and the policies you want to enforce. ## What we'll cover [#what-well-cover] * Where the firewall fits between your package managers and public registries. * How rules can delay new versions and block known vulnerabilities or malware. * How the firewall could work with your development and CI/CD setup. The scheduler opens in a new tab. Prefer email? Reach us at [support@bytesafe.dev](mailto:support@bytesafe.dev). Want more context first? Read [What is a dependency firewall?](/firewall/getting-started/what-is-dependency-firewall) or review the [supported ecosystems](/firewall/ecosystems). # Early access (/firewall/early-access) The Bytesafe Dependency Firewall is in early access. You can sign up directly and start a trial. We use feedback to guide updates during early access and beyond. Tell us if something could work better for your use case. See the [updated pricing](https://bytesafe.dev/pricing) for plan details. ## How early access works [#how-early-access-works] * Early access is open to anyone. Sign up and use the product with your own projects and workflows. * We use feedback to prioritize improvements to the product and documentation. [Release notes](/firewall/release-notes) cover capability-level changes, not every field or wording change. * Feedback is always welcome. Tell us about rough edges, confusing error messages, missing capabilities, unclear documentation, or anything that could work better for your use case. ## Feedback [#feedback] Email [support@bytesafe.dev](mailto:support@bytesafe.dev). Bug reports and product feedback go to the same address. For bug reports, describe what you expected, what happened instead, and the steps that produced the problem. When available, include the namespace ID, firewall ID, error text, and screenshots. Do not include access tokens. ## Known issues [#known-issues] Issues found during early access are listed here until fixed. None are open right now. Current availability: * Some ecosystems are in beta. The [ecosystems overview](/firewall/ecosystems) marks which, and each ecosystem page describes its current behavior. * [Container images](/firewall/ecosystems/containers) are coming in Q4 2026. ## Frequently asked questions [#frequently-asked-questions] You can sign up directly and use the product for a 14-day trial. This is the next generation of Bytesafe's firewall; see [what's new compared to the previous version](https://bytesafe.dev/firewall-next-gen). It handles real traffic, but this version is not generally available yet. Feedback during early access helps us prioritize improvements, and remains welcome after general availability. Follow [Migrate from previous-generation Bytesafe](/firewall/how-to/migrate-from-previous-generation). It maps every legacy policy and plugin to its replacement, covers moving private packages to a repository manager, and ends with testing before you switch. Questions along the way go to [support@bytesafe.dev](mailto:support@bytesafe.dev). Email [support@bytesafe.dev](mailto:support@bytesafe.dev) with the details above under [Feedback](#feedback). Product questions not specific to early access are answered in the [Support FAQ](/firewall/support). # Bytesafe Dependency Firewall (/firewall) The Bytesafe Dependency Firewall sits between your package managers and public registries. Every install and publish goes through a firewall you configure with rules: delay new versions, block known vulnerabilities and malware, and keep an audit trail of every decision. Previous-generation docs: [docs-v1.bytesafe.dev](https://docs-v1.bytesafe.dev/). Migrating? See the [migration guide](/firewall/how-to/migrate-from-previous-generation). ## See how it works [#see-how-it-works] Read [What is a dependency firewall?](/firewall/getting-started/what-is-dependency-firewall) for a short introduction and video. ## Start here [#start-here] ## Common tasks [#common-tasks] ## Look things up [#look-things-up] # Free for open source (/firewall/open-source) The free Community Edition of the previous firewall generation has been discontinued. For teams and companies, see [Bytesafe Cloud pricing](https://bytesafe.dev/pricing) or [Self-hosted](/firewall/self-hosted). Open source projects have supply chains too. If you maintain an active, public open source project, you can apply to run the [Dependency Firewall](/firewall) in your CI and release workflows, sponsored by Bytesafe. ## What sponsorship includes [#what-sponsorship-includes] Sponsorship normally includes one Cloud firewall endpoint for the approved project. Multi-ecosystem projects can request additional endpoints. It runs for 12 months and is reviewed annually. ## What we ask in return [#what-we-ask-in-return] * Publish your source under an open source license. * Use the sponsored access only for the approved project. * List Bytesafe as a sponsor in the README and on the project website, with a link to [bytesafe.dev](https://bytesafe.dev). Applications are reviewed case by case on project activity, adoption, fit, and program capacity. Email [support@bytesafe.dev](mailto:support@bytesafe.dev?subject=Open%20source%20sponsorship) with the repository URL, package ecosystems, CI provider, and estimated build activity. ## Terms [#terms] Sponsored access has no fixed request or bandwidth quota. It covers the dependency traffic generated by the approved project's CI and release workflows. It is provided under our [Terms of Service](https://bytesafe.dev/legal/terms#6-acceptable-use), together with the conditions on this page: keep it to the approved project, and do not resell, share, or repurpose it. If usage grows beyond that or affects service capacity, we contact the project first where practical, and may agree a change, recommend a paid plan, or pause access. We can suspend access immediately to protect the service. Your data is handled the same as any other account, under our [Privacy Policy](https://bytesafe.dev/legal/privacy) and [Data Processing Agreement](https://bytesafe.dev/legal/gdpr-dpa). *** Got questions? Email [support@bytesafe.dev](mailto:support@bytesafe.dev). # Self-hosted (/firewall/self-hosted) The Bytesafe Dependency Firewall is available self-hosted: run it in your own datacenter or cloud account, so package traffic and upstream credentials never leave your perimeter. It is the same product as the SaaS offering, same rules, ecosystems, CLI, and API, just operated by you instead of Bytesafe. For what changes (and what does not) between SaaS and self-hosted, see [Deployment models](/firewall/concepts/deployment-models). To get started, contact [support@bytesafe.dev](mailto:support@bytesafe.dev). # Access and permissions (/firewall/concepts/access-and-permissions) Everything in the Dependency Firewall happens inside a namespace, and every actor in it, a person or a system, acts through a set of permissions. This page explains how that set is computed. How a person proves who they are before any of it applies is covered in [Authentication](/firewall/platform/authentication). For the day-to-day tasks, see [Manage access](/firewall/how-to/manage-access) and [Manage tokens](/firewall/how-to/manage-tokens). ## Namespaces, teams, grants, and tokens [#namespaces-teams-grants-and-tokens] * **Namespace.** The isolation boundary. Users, teams, tokens, and firewalls all live in one namespace; the namespace is part of every URL. * **Teams.** Users belong to teams; teams carry namespace-level permissions. Two protected teams always exist: **everybody** (all users; namespace access, PAT creation, firewall creation and read) and **admin** (adds team management, SAT creation, and full firewall write). * **Per-firewall grants.** Each firewall's configuration maps teams to permissions on that firewall. New firewalls grant read (`fw:access`, `fw:clone`, `fw:read`) to everybody and everything to the creating team. * **Tokens.** Requests authenticate with a token, and the token can carry its own restrictions. Permissions are verbs, not roles: `fw:update:rule`, `ns:create:pat`, `team:update:membership`. The full catalog is in the [permissions reference](/firewall/references/permissions). ## How a user's effective permissions are computed [#how-a-users-effective-permissions-are-computed] For a user acting on a firewall, three layers intersect: Additive within a layer, restrictive across layers: more teams never reduce access, but every layer must allow an action for it to be permitted. A firewall grant means nothing to a user whose teams lack the namespace-level permission, and a token scoped to `fw:access` cannot update rules regardless of how privileged its owner is. ## Person tokens and system tokens [#person-tokens-and-system-tokens] The two token types resolve permissions differently: * **Personal Access Tokens (PAT)** act as the user. Permissions are computed from team memberships as above, then narrowed by any restrictions on the token. A PAT is never more powerful than its owner, and revoking team membership immediately affects existing tokens. * **Service Access Tokens (SAT)** are self-contained. They belong to the namespace, not a user, and carry their permissions explicitly. A fixed catalog caps what a SAT can ever hold: access, read, clone, and configuration/rule/exception updates, but not team management, credential updates, or namespace and firewall deletion. Those actions require a person. Both types can be restricted to specific firewalls, and both expire. Trusted Builder tokens are SATs minted through an OpenID Connect exchange with a 15-minute lifetime; the same rules apply, only the issuance differs. See [Access Tokens](/firewall/getting-started/access-tokens). ## Consequences [#consequences] * **To give one person more access, use a team.** There are no per-user grants; a team with one member is the intended pattern. * **Read access is namespace-wide by default.** Everybody can see every firewall's configuration unless a firewall's grants say otherwise; write access is what teams gate. * **Scoped tokens are the cheap safety win.** A CI token with `fw:access` restricted to one firewall leaks far less than it could; see [Manage tokens](/firewall/how-to/manage-tokens). ## Related [#related] * How-to: [Manage access](/firewall/how-to/manage-access), [Manage tokens](/firewall/how-to/manage-tokens) * Reference: [Permissions](/firewall/references/permissions), [Tokens](/firewall/references/tokens) * Getting started: [Access Tokens](/firewall/getting-started/access-tokens) * Platform: [Authentication](/firewall/platform/authentication) # Deployment models (/firewall/concepts/deployment-models) The Dependency Firewall is the same product in every deployment model: the same rules, ecosystems, CLI, and API. What changes is where the proxy runs and who operates it. ## SaaS [#saas] The default. Bytesafe operates the service; you create a namespace, point package managers at your endpoint, and everything in these docs applies as written. Package traffic flows through Bytesafe-operated infrastructure, with upstream credentials encrypted at rest. Choose SaaS unless a policy says you cannot: it is the shortest path to a working firewall and carries no operational load. ## Your own infrastructure (self-hosted) [#your-own-infrastructure-self-hosted] For organizations whose packages, credentials, or network topology must not leave their environment, the firewall runs on your own infrastructure, self-hosted in your datacenter or your cloud account. You operate it; package traffic and upstream credentials never leave your perimeter. Internal registries that are unreachable from the internet can then sit behind the firewall as upstreams, which SaaS cannot reach. The [self-hosted page](/firewall/self-hosted) summarizes availability. Contact [support@bytesafe.dev](mailto:support@bytesafe.dev) to discuss deployment requirements and setup. ## What does not change [#what-does-not-change] * **Client configuration.** Package managers speak the same protocols against the same URL shapes; only the hostname differs. Ecosystem pages apply unchanged. * **Policy and API.** Rules, exceptions, tokens, the management API, and `bsfw` work identically, which also means [config as code](/firewall/how-to/manage-config-as-code) can move a firewall between deployment models: export from one, import into the other, set credentials. ## What to check for self-hosted [#what-to-check-for-self-hosted] Two capabilities depend on services around the core proxy, so confirm them for your environment when planning a self-hosted deployment: vulnerability data for the `vulnerabilities` selector, and malware scanning for the `malware` selector. ## Related [#related] * [Self-hosted](/firewall/self-hosted) * Concept: [Prevention model](/firewall/concepts/prevention-model) * How-to: [Manage config as code](/firewall/how-to/manage-config-as-code) # Exception model (/firewall/concepts/exception-model) Every blocking policy eventually collides with a legitimate need: a security patch younger than your delay window, a vulnerable version with no upgrade path that your code cannot trigger. The failure mode of most policy systems is what happens next: the rule gets widened, or the policy gets bypassed, and either way the control quietly erodes. Exceptions are the firewall's designed answer. For the step-by-step, see [Manage exceptions](/firewall/how-to/manage-exceptions); this page is why they work the way they do. ## An exception belongs to one rule [#an-exception-belongs-to-one-rule] An exception is attached to a single rule and consulted only after that rule's selector has matched. If the exception's own selector also matches, the rule is skipped for that request; every other rule still runs. This is the property that makes exceptions safe to grant. Waiving the vulnerability rule for one package does not exempt it from the malware rule, the delay rule, or anything added later. There is no "allowlist this package everywhere" primitive, deliberately. ## Scope is a selector [#scope-is-a-selector] The exception's selector has the same shape as a rule selector, so scope is expressed in the same vocabulary: package name, version, upstream, wildcards. The narrower the selector, the smaller the hole. `lodash` at `4.17.20` is a decision about one artifact; `lodash` at any version is a standing exemption that outlives the situation that justified it. ## Expiry makes waivers temporary [#expiry-makes-waivers-temporary] An exception can carry an expiry timestamp. Once it passes, the exception stops matching and the rule's block returns, with no cleanup task, no review meeting, no one having to remember. If the need persists, someone has to actively renew the exception, which is exactly the review point you want. The alternative, permanent waivers that someone intends to revisit, is how policy systems accumulate silent holes. Prefer short expiries and cheap renewals. ## Reasons make the log auditable [#reasons-make-the-log-auditable] Every exception records a reason from a fixed vocabulary (`not_exploitable`, `no_upgrade_path`, `evaluating_component`, and so on; the full list is in the [reference](/firewall/references/exceptions#reasons)) plus free-text notes. When an exception allows a request through with logging enabled, the log entry carries the exception ID. Together these answer the auditor's question, why was this blocked package allowed, from the log alone: which rule was waived, on what grounds, by which exception, until when. ## From block to exception [#from-block-to-exception] A block is not a dead end but a request for a decision: The developer never needs registry bypass rights, and the policy owner never needs to weaken a rule to unblock one team. ## Related [#related] * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions) * Reference: [Exceptions](/firewall/references/exceptions) * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) # Execution phases (/firewall/concepts/execution-phases) A package manager asks a registry for three things: which versions of a package exist, the file for one of those versions, and to accept a file it is publishing. A firewall answers all three, so all three are points where your rules can act. Every rule runs in exactly one of them. The three execution phases are `versions`, `download`, and `upload`, which is the lowercase spelling the config, API, and CLI use, and what this page uses. The dashboard's **Applies when (Execution Phase)** picker spells them out instead, as "Resolving package versions", "Downloading a package", and "Uploading a package". Each phase gives something up: a `versions` rule can still steer an install but only sees the registry's metadata, a `download` rule sees the actual package but can only allow or fail it. That tradeoff decides what happens to the developer's install when a rule matches, which is the choice worth settling before you write the rest of the rule. **A policy is not a rule.** A policy is what your organization requires: no dependency younger than a week, no copyleft license in a shipped product, nothing carrying a known critical vulnerability. A rule is how one phase enforces part of that. One policy is usually several rules, sometimes across different phases, plus the [exceptions](/firewall/concepts/exception-model) you decide to grant. This page is about the phase a rule runs in; who sets the policy behind it is covered in [Prevention model](/firewall/concepts/prevention-model#who-owns-policy). ## What the package manager asks for [#what-the-package-manager-asks-for] | What the package manager needs | The request it makes | Phase | What a rule can do | | -------------------------------- | -------------------------- | ---------- | -------------------------------- | | To find out which versions exist | The package's version list | `versions` | Leave versions out of the answer | | To get the file for one version | The package file | `download` | Serve it, or fail the request | | To publish a file | The upload | `upload` | Accept the publish, or reject it | The firewall answers each of these in the ecosystem's own protocol, so the package manager talks to what looks like a registry and gets answers shaped by your rules (see [Client operations](/firewall/references/client-operations)). Which of the three you get depends on what the project already carries, not on the command someone typed. A project that declares ranges has versions to work out; a project with a lockfile, or with exact versions written down, has none, so installing it is downloads and nothing else. The phase follows the request, not the command. ## The three execution phases [#the-three-execution-phases] ### Versions phase [#versions-phase] **This phase is version resolution.** A project depending on `lodash ^4.17.0` has not named a version: the caret means any 4.x release from 4.17.0 upward, so the package manager has to find out what exists before it can choose. That question is what this phase answers, for the whole dependency tree, and a build installing from a lockfile never asks it. The rules still apply to those installs, enforced at download time instead; the callout below explains the difference. A blocking rule here removes the matching versions from the metadata response before the package manager sees it, along with any tags pointing at them. The package manager then picks the newest version that is left and still satisfies the dependency range, so resolution lands on something that complies with your rules. A maturity rule like the one above, blocking versions younger than some age, can be declared in either phase, and the phase changes what happens when a dependency range would otherwise resolve to a young version. Declared in `download`, it fails the install outright. In the example above, an install allowed to pick `4.17.23` would be rejected two days after that release went out, including for transitive dependencies nobody chose directly, and keep failing until the version ages past the window or someone grants an exception. Declared in `versions`, the same install steers around it instead, the way the illustration shows: the package manager never sees `4.17.23` in the version list it gets back, so it resolves straight to `4.17.21` without ever asking for the younger one. Either phase still stops a request that names the young version directly instead of resolving a range, such as a lockfile pinned to `4.17.23`. There is no older version to fall back to there, so it fails the same way a `download` block would, enforced at download time (see the callout below). It is also how you control which copy of a package wins when [upstreams](/firewall/concepts/upstreams-and-caching) overlap, say a patched internal build alongside the public release. The package manager picks from the combined list, and a `versions` rule decides what goes into it. Two limits are worth knowing: * **There has to be another version to pick.** An exact pin of a removed version, or a tag pointing at one, has nothing to fall back to and fails. * **It only sees what the registry knows.** A `versions` rule can use the package name, the upstream, registry status such as deprecation, and the publish date (`maxAgeHours`). Anything that needs the package file itself is out of reach. No, but they fail differently. An exact pin (`react@18.2.0`) is checked against the filtered list, so the package manager reports that version as not found. A request that skips the list entirely and goes straight for the package file, such as a tarball URL taken from a lockfile, is checked against the `versions` rules at download time instead. That one fails with the same explicit error a `download` block gives: the package, the version, and a request ID, recorded in the firewall log as a `download` entry. Either way, a version a `versions` rule removed cannot get in. What differs is the error the developer sees. "This version does not exist" when the package manager used the version list, an explicit block when it went straight for the file. Rules that should always fail loudly still belong in `download`, alone or in addition. ### Download phase [#download-phase] **This phase is downloading a package.** Every package that reaches a build is fetched one file at a time, whether its version was resolved a moment earlier or written into a lockfile months ago. The firewall has that file in hand when it decides. A blocking rule here fails the request. The package manager has already committed to a version, so there is nothing for it to fall back to. `download` rules run once per package fetched, with the package file available to read, which is what makes content checks possible. What this phase gives you is coverage that resolution cannot provide. Every package file entering a build passes through it, including the dependencies several levels down that no developer picked. Nothing gets in by being requested a different way, which is what makes a `download` rule the one you can state as a guarantee, to an auditor or to a customer. Use it when a different version of the same package would not make the problem acceptable: a malware verdict, a [vulnerability threshold](/firewall/how-to/block-vulnerable-packages), a package that must never be fetched from outside your organization. A block answers the request with the firewall's error instead of the package file. How much of that error a developer sees depends on the package manager, so each [ecosystem page](/firewall/ecosystems) shows what its clients print. The reason lives in the firewall log either way: the block carries a request ID, and the entry it points at names the rule that fired, the package, and the version. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ### Upload phase [#upload-phase] **This phase is publishing.** When a developer or a pipeline pushes a package to your publish target, it goes through the firewall on the way. It is the only phase that looks at packages leaving your organization. `upload` rules evaluate publishes before they reach the [publish target](/firewall/how-to/configure-upstreams#choose-a-publish-target). A blocked publish is never forwarded, and the rejection names the package, the version, and a request ID. Because this is where you can still stop something before it leaves, [secrets scanning](/firewall/how-to/scan-for-secrets) runs here. Malware and [install-time code execution](/firewall/how-to/block-install-scripts) checks also run here for ecosystems with publish support, so what you publish is held to the standard you apply to what you consume. ### Comparing versions and download [#comparing-versions-and-download] The two phases that act on packages coming in are the pair you choose between when you write a rule. | | `versions` | `download` | | ------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------- | | Runs when | The package manager asks which versions exist | The package manager fetches one package file | | Can use | Name, version, upstream, publish date, registry status such as deprecation | All of that, and the package contents | | On a match | The version is left out of the answer | The request fails | | The developer sees | A successful install, possibly on an older version | A failed install, with a block from the firewall behind it | ### Which checks run in which phase [#which-checks-run-in-which-phase] The phase also decides which [selector functions](/firewall/references/selector-functions) a rule can use, and that often decides the phase for you. Check the **[selector function matrix](/firewall/references/selector-functions#matrix)** before writing a rule. The dashboard only offers compatible combinations, and the API rejects a rule when its selector function cannot evaluate in the selected phase. Two consequences come up often. Vulnerability and license checks are offered in `download`, so rules built on them fail the install rather than steering it. Package age, deprecation, unlisted status, publishing trust, and Composer plugin metadata can be checked in `versions` and steer resolution instead. ### What each phase records [#what-each-phase-records] * A `versions` rule with `log: true` records the versions it removed, so filtering stays visible in the [firewall log](/firewall/how-to/view-logs) even though the install succeeded. * [Webhook events](/firewall/references/webhooks) come from `download` and `upload` decisions. Metadata filtering never produces events, however many versions it removes. A `versions` rule that blocks an artifact request is a `download` decision, so that block does produce one. ## Choosing a phase for a rule [#choosing-a-phase-for-a-rule] Start from what you want to enforce rather than from the phase. Two questions settle it. 1. **Would another version of this package be acceptable?** If yes, `versions` enforces it and lets the install continue on a version that complies. If no version of it may enter, `download` is the phase that says so. 2. **Does the check need to read the package file?** If it does, the phase is decided for you: `download` for what comes in, `upload` for what you publish. The [selector function matrix](/firewall/references/selector-functions#matrix) is the list. Many requirements want both phases, and the two rule sets are separate gates, so a version that survives filtering is still evaluated on download. A common baseline is a `maxAgeHours` window in `versions` to delay fresh releases without breaking installs, plus `malware` and `vulnerabilities` rules in `download` as hard stops. Pairing the same selector across both phases works too, when you want resolution steered away from something and a hard stop if a lockfile asks for it anyway. | What you want to enforce | Phase | What the developer sees | | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------- | | An acceptable version exists, so send installs to it instead (age windows, [deprecated packages](/firewall/how-to/block-deprecated-packages)) | `versions` | The install succeeds on an older version. Nothing fails | | Installs must only ever choose from versions you control (overlapping upstreams, internal builds) | `versions` | The package manager only ever sees the versions you allow | | No matching version may enter, whatever that breaks ([malware](/firewall/how-to/block-malware), [vulnerabilities](/firewall/how-to/block-vulnerable-packages)) | `download` | The install fails, including when the package is a dependency of a dependency | | Control what gets published ([secrets](/firewall/how-to/scan-for-secrets)) | `upload` | The publish is rejected before it reaches the upstream | ## Starting points in the dashboard [#starting-points-in-the-dashboard] You do not have to pick a phase cold. **Add Rule** opens with a short list of common goals, each one a policy people usually want, and choosing one starts the rule in the phase that policy needs. | The goal you pick | Where it starts the rule | | ---------------------------------------- | --------------------------------------------------------- | | **Let new releases mature** | `versions`, with a 7-day window already filled in | | **Hide packages or versions** | `versions` | | **Block packages from being downloaded** | `download` | | **Check packages you publish** | `upload`, offered for ecosystems that have a publish path | | **Start from a blank rule** | Nothing preselected | What you get is a starting point, not a finished rule. The next screen is the normal rule form, where you add the [selector](/firewall/references/rules-and-selectors) that says which packages to match and the [selector function](/firewall/references/selector-functions) that says what to check, and where every seeded value stays editable, phase included. The [how-to guides](/firewall/how-to) each work one policy through end to end and state the phase they use and why. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation), [Exception model](/firewall/concepts/exception-model) * How-to: [Delay new versions](/firewall/how-to/delay-new-versions) (`versions`), [Block malware](/firewall/how-to/block-malware) (`download`), [Scan for secrets on publish](/firewall/how-to/scan-for-secrets) (`upload`), [Investigate a blocked install](/firewall/how-to/investigate-blocked-install) * Reference: [Client operations](/firewall/references/client-operations), [Rules and selectors](/firewall/references/rules-and-selectors), [Selector functions](/firewall/references/selector-functions#matrix) # All concepts (/firewall/concepts) Background reading for how the firewall works. No steps here, see [how-to guides](/firewall/how-to) for tasks. ## Rules and enforcement [#rules-and-enforcement] ## Access [#access] ## Operations [#operations] # Observability (/firewall/concepts/observability) A firewall produces four kinds of records. They overlap, so it is easy to look in the wrong one: | Record | Answers | Granularity | Where | | ------------ | ------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Firewall log | What happened to this package request, and which rule decided | One entry per logged rule decision, and one each time a call to an upstream fails | [View logs](/firewall/how-to/view-logs), [CLI](/firewall/references/cli#bsfw-logs), [reference](/firewall/references/logs) | | Audit log | Who changed configuration, policy, tokens, teams | One event per control-plane action | [Review the audit log](/firewall/how-to/review-audit-log), [CLI](/firewall/references/cli#bsfw-audit), [reference](/firewall/references/audit-log) | | Metrics | How much traffic, how much blocked, over time | Time-bucketed counters | Dashboard, [reference](/firewall/references/metrics) | | Observations | Which packages and versions are actually in use | One row per package version seen | Dashboard, [CLI](/firewall/references/cli#bsfw-observations), [reference](/firewall/references/observations) | ## Firewall log [#firewall-log] A rule match writes an entry when the rule has logging enabled. Each one records: * who requested which package version, through which firewall and upstream, * which rule and execution phase decided, and whether it blocked, * rule-specific output, such as vulnerability IDs or a malware finding with its scanner and confidence. Exceptions that allowed a request through are recorded the same way. The log also records upstream failures: an upstream that rate limits the firewall, rejects its credentials, or cannot be reached. The client is never told about it, so the entry is what makes it visible. No rule fired and nothing was blocked. See [Check for failing upstreams](/firewall/how-to/view-logs#check-for-failing-upstreams). Nothing else is written. A request that matches no rule, or matches a rule with logging turned off, leaves no entry: this is a decision trail, not a request trail. See [What is logged when](/firewall/how-to/view-logs#what-is-logged-when). That is why "what did we download" is an observations question rather than a log question. Observations are built from traffic itself, so a successful install appears there whether or not a rule had anything to say about it. See [See what was actually downloaded](/firewall/how-to/view-logs#see-what-was-actually-downloaded). This is the log you use day to day: a developer's blocked install, a dry-run rule under evaluation, an exception being exercised. It can be queried or streamed live, and it defaults to your own entries, so looking at someone else's request takes an explicit flag. See [View logs](/firewall/how-to/view-logs). ## Audit log [#audit-log] Where the firewall log records what the firewall did to traffic, the audit log records what people did to the firewall: config imports, rule and exception changes, credential updates, token creation, team changes, permission denials, and OIDC exchanges. Events carry the actor, the outcome, and action detail. Failures are recorded as deliberately as successes, so an attempt that was denied leaves the same kind of trail as a change that went through. ## Metrics [#metrics] Counters per time bucket, for the namespace or for one firewall: how much traffic came through, how much was blocked, how much an exception let through, and how many rules and exceptions are configured. The [metrics reference](/firewall/references/metrics) lists every counter. Metrics tell you that blocking spiked on Tuesday. The firewall log tells you which rule and which packages. Versions hidden from a metadata response count as filtered versions. A blocked artifact request counts as a blocked download instead, whichever phase the rule that stopped it runs in. ## Observations [#observations] An observation is a package version the firewall has actually served: first seen, last seen, request count, internal or external, enriched with a vulnerability severity summary. Where the other three records are about events, observations are about state: what your organization currently depends on, as evidenced by real traffic rather than manifest files. It is the closest thing to an SBOM the firewall derives on its own. ## Webhooks [#webhooks] The four records are pull surfaces: you query them. [Webhooks](/firewall/references/webhooks) push instead, delivering to HTTPS endpoints you run, signed and batched, as events happen: * firewall log entries, download and upload decisions only, never upstream failures, * audit events, * configuration changes, * successful publishes. The records stay the source of truth: a delivery that never arrives is a gap in your system, not in the log. See [Receive webhook events](/firewall/how-to/receive-webhook-events). ## Request and interaction IDs [#request-and-interaction-ids] Request IDs and interaction IDs stitch the records together. A blocked install error carries a request ID that finds the firewall log entry. An [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) appended to a CI token groups that pipeline's package requests and its audit events. That is how you get from "the build failed" to the rule, the package, and the change behind it without guessing. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ## Related [#related] * How-to: [View logs](/firewall/how-to/view-logs), [Review the audit log](/firewall/how-to/review-audit-log) * Reference: [Logs](/firewall/references/logs), [Audit log](/firewall/references/audit-log), [Metrics](/firewall/references/metrics), [Observations](/firewall/references/observations) # Prevention model (/firewall/concepts/prevention-model) A dependency firewall is a policy enforcement point between your package managers and public registries. Clients install through it; the firewall applies your rules to every version list, download, and publish, and either serves the request or blocks it. This page is the mental model; for the request mechanics see [Rule evaluation](/firewall/concepts/rule-evaluation). ## Prevention, not detection [#prevention-not-detection] Most dependency security tooling is detection: it scans what you already depend on and files findings after the fact. A firewall is prevention: the package that violates policy never enters a build to begin with. The difference is when the control acts: Both matter. A scanner covers what is already inside; the firewall decides what gets in from now on. The firewall's rules also consume the same kind of intelligence scanners use (vulnerability data, malware verdicts), but apply it at request time. ## Before an advisory exists [#before-an-advisory-exists] Several firewall controls act without waiting for a vulnerability advisory. They use package age, artifact findings, install behavior, package origin, and publishing evidence rather than requiring a CVE: * **Package maturity delay.** [Delay new versions](/firewall/how-to/delay-new-versions) filters newly published releases for a configurable period, giving problems time to surface before builds can select them. * **Malware scanning.** [Block malware](/firewall/how-to/block-malware) evaluates malicious-package intelligence and package-content analysis on downloaded and published artifacts. * **Install-time code execution.** [Block install-time code execution](/firewall/how-to/block-install-scripts) matches npm lifecycle scripts, PyPI source-build hooks, and Composer plugins. * **Dependency confusion protection.** [Prevent dependency confusion](/firewall/how-to/prevent-dependency-confusion) keeps public packages from replacing package names already supplied by an internal upstream. * **Publishing-trust checks.** [Block trust downgrades](/firewall/how-to/block-trust-downgrades) matches npm releases published with weaker trust evidence than earlier releases of the same package. ## Compared to a repository manager [#compared-to-a-repository-manager] Repository managers (private registries, artifact proxies) also sit between clients and public registries, primarily to host internal packages and cache external ones. The firewall shares that proxy position and adds policy as the first-class object: * **Rules, not just allowlists.** Policy is expressed as rules over [execution phases and selectors](/firewall/concepts/rule-evaluation): age windows, vulnerability thresholds, malware scans, name patterns, upstream origin. * **Exceptions with expiry and reasons.** Waivers are scoped, justified, and time-bounded, not permanent edits to a list. * **A decision log.** A matching rule with logging enabled writes an entry answering who requested what, which rule fired, and why. See [View logs](/firewall/how-to/view-logs). The firewall also covers the repository manager's core moves where policy needs them: it proxies and caches upstreams, serves internal packages from [internal upstreams](/firewall/concepts/upstreams-and-caching), and receives publishes. Existing repository managers can sit behind it as upstreams. ## One firewall per ecosystem [#one-firewall-per-ecosystem] A firewall speaks its ecosystem's native protocol: npm registry, PyPI simple index, Maven repository layout, NuGet v3, GOPROXY, Cargo sparse index, Composer repository, Conda channel. Clients need a URL change and a token, nothing installed. One namespace typically runs several firewalls for different ecosystems, each with its own rules and upstreams. See [Ecosystems](/firewall/ecosystems). ## Who owns policy [#who-owns-policy] The teams that feel a bad dependency first (platform, security) set policy centrally; developers keep using their normal tools. When a rule blocks something a developer needs, the resolution path is an [exception](/firewall/concepts/exception-model), reviewed and time-bounded, instead of an ad-hoc registry bypass. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation), [Upstreams and caching](/firewall/concepts/upstreams-and-caching) * Getting started: [Quickstart](/firewall/getting-started/quickstart) # Rule evaluation (/firewall/concepts/rule-evaluation) Every request that flows through a firewall is evaluated against its rules. Understanding that path makes rule behavior predictable: which requests a rule sees, when it matches, and what happens on a match. ## How one request is evaluated [#how-one-request-is-evaluated] Rules are evaluated cheapest first, not in configuration order. Rules of equal cost keep their configuration order. A matching rule with a log effect writes a log entry and evaluation continues. A matching rule with a block effect ends evaluation: the request is blocked. Disabled rules are skipped entirely. Cost ordering keeps the added latency close to the cheapest policy that can decide the request: an expensive check is paid for only when every cheaper rule has already let the request through. It also decides attribution. Because a block ends evaluation, the rule recorded in the log is the cheapest rule that matched, not the first one configured. The exact order is an implementation detail and can change, so do not build policy that depends on one rule being reached before another. Which rules apply depends on the request. A metadata request runs the `versions` rules, a publish the `upload` rules. An artifact request runs both the `versions` and the `download` rules. When a firewall [inherits configuration](/firewall/how-to/share-configuration-across-firewalls), the inherited rules come first in configuration order, from the top of the parent chain down, followed by the firewall's own. That is the tie-break between rules of equal cost, so a rule of the firewall's own can still be evaluated ahead of an inherited one. It does not weaken the baseline: an inherited block still blocks the request, it may just not be the rule that logged it. ## Execution phases [#execution-phases] A rule runs in exactly one phase. The phase decides which requests the rule sees and what blocking means there. This section is the summary; [Execution phases](/firewall/concepts/execution-phases) walks through a real install and when to pick which phase. The dashboard rule form labels the phases **Versions**, **Download**, and **Upload**. The config, API, and CLI use `versions`, `download`, and `upload`. | Phase | When | Blocking means | | ---------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `versions` | Client asks for a package's version list, and again when it fetches an artifact | The version is filtered out of the metadata, along with tags pointing at it. Range requests and versionless installs resolve to an allowed version; exact pins and named tags fail as not found. A direct artifact request for the version is blocked with an error and a request ID | | `download` | Client fetches a package artifact | The download returns an error identifying the block and a request ID | | `upload` | Client publishes a package | The publish is rejected before reaching the publish target | The phase also determines what information is available. Version filtering happens on metadata, so a rule there can use the publish date (`maxAgeHours`) but nothing that requires the artifact. Malware scanning and install-script inspection need the artifact, so they run on `download` and `upload`; secrets scanning reads the contents of what is being published, so it runs on `upload` only. The two blocking styles are complementary. A versions rule steers every resolution in the dependency tree toward allowed versions and fails softly wherever the client has a choice; a download rule is the hard stop with an explicit error naming the block and a request ID. A versions rule does not leave a side door either: artifact requests are evaluated against it as well, so a lockfile URL for a filtered version is blocked, with the same explicit error a download rule returns. The phases differ in what they can inspect and how the failure surfaces, not in which requests they can stop. ## Selectors [#selectors] A selector decides which packages a rule applies to. It is a set of conditions that must **all** match: * **Where from:** `upstreamId`, `upstreamType` (internal or external upstream) * **What:** `packageName` and `version`, matched as text with wildcards, and `versionRange`, matched with the ecosystem's version semantics * **How new:** `maxAgeHours` matches versions published more recently than the given number of hours * **Computed:** `function`, a named check such as `vulnerabilities` or `malware`, optionally with options Empty fields match everything, so an empty selector matches every package in the phase. `packageName` and `version` support a single wildcard at the start or end: `@acme/*` (prefix) or `*-beta` (suffix), or `*` alone for everything. `versionRange` is the semantic alternative to a version wildcard: it understands that `1.10.0` is newer than `1.9.0`, and each ecosystem writes ranges its own way. Both fields exist and both still apply when set together. Notation per ecosystem: [Version ranges](/firewall/references/rules-and-selectors#version-ranges). Selector functions are where external knowledge enters: vulnerability data (`vulnerabilities`), malware verdicts (`malware`), artifact contents or repository metadata (`install-scripts`, `secrets`), registry state (`deprecated`, `unlisted`), license data (`license`), and publish-trust comparisons (`trust-downgrade`). Plain selector fields answer "which package is this"; functions answer "what do we know about it". ## Effects [#effects] An effect has two independent switches: * `block: true` stops the request. * `log: true` writes a log entry on every match. `log` without `block` is a dry run, and the standard rollout pattern: deploy the rule log-only, watch what it would block, then enable blocking. `block` without `log` works but leaves you blind; there is rarely a reason to turn logging off. A dry run only sees the requests that reach it. A blocking rule that is cheaper to evaluate ends the request first, so a log-only `malware` or `license` rule under-reports on requests some cheaper rule already blocks. ## Exceptions [#exceptions] An exception is attached to one rule and consulted only after that rule's selector matched. It has its own selector, and if that matches too, the rule is skipped for this request. Two properties keep exceptions honest: * **Expiry.** An expired exception stops matching, and the block returns. Waivers are temporary by construction, not by promise. * **Reason and notes.** Every exception records why it exists, which is what makes the log trail meaningful in a review. Because an exception belongs to a single rule, allowing a package through the vulnerability rule does not also exempt it from the malware rule. ## Consequences [#consequences] * **Decisions are per request, never stored.** When the data behind a selector function changes, a CVSS score raised or lowered, an advisory withdrawn, a package deprecated, the next request reflects it. See [Freshness and score changes](/firewall/references/advisory-sources#freshness-and-score-changes). * **A block silences the rules after it.** Evaluation stops at the first blocking match, and the order is cost-based, so a cheap blocking rule can keep a log-only `vulnerabilities`, `license`, or `malware` rule from logging that request. * **A package can pass `versions` and still be blocked at `download`.** The two rule sets are separate gates, not one decision, even though an artifact request runs both. * **Selector conditions AND together.** Two conditions in one rule mean both must hold. Two separate rules mean either can act. Use separate rules when you mean OR. ## Related [#related] * Reference: [Rules and selectors](/firewall/references/rules-and-selectors) * 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) # Upstreams and caching (/firewall/concepts/upstreams-and-caching) A firewall is not a package store. It hosts no packages of its own: it resolves them from upstreams, the registries listed in its configuration, and can cache copies of what it has served. Understanding how upstream responses are combined explains where any given version comes from, and why marking an upstream internal changes resolution. ## How resolution works [#how-resolution-works] For a package request, the firewall queries its upstreams for metadata and combines the results: Three properties follow: * **An unreachable or failing upstream is skipped.** The firewall answers with what the other upstreams returned. Upstreams are not copies of each other, so if the skipped one was the only upstream holding the package, the request fails anyway, with an error that names the package and never the upstream ([failure behavior](/firewall/references/upstreams#failure-behavior) has the exact statuses). The firewall log is where the failure shows up; see [Check for failing upstreams](/firewall/how-to/view-logs#check-for-failing-upstreams). * **Each version is served once.** Duplicate version numbers across upstreams are deduplicated in the merged list. * **Rules see the upstream.** Every candidate version carries which upstream it came from, which is what the `upstreamId` and `upstreamType` selector fields match against. ## Internal upstreams [#internal-upstreams] Marking an upstream **internal** declares that it is the authority for the packages it hosts. The resolution consequence is the internal-wins step above: if any internal upstream knows a package name, external copies of that name are not served at all. This is the structural defense against [dependency confusion](/firewall/how-to/prevent-dependency-confusion): an attacker publishing your internal package name on a public registry cannot outbid your registry with a higher version number, because the public copy never enters the merge. Internal is about ownership and trust, not network location. A SaaS-hosted private registry is still internal in this sense. ## Caching [#caching] Each upstream has a cache flag. When enabled, the firewall caches upstream responses, metadata for a short interval and fetched artifacts, so repeated installs do not re-fetch from the upstream every time. Three details worth knowing: * **Caching applies to internal upstreams**, the registries you own. See [Configure upstreams](/firewall/how-to/configure-upstreams). * **Maven Central is always cached** (`repo1.maven.org`, `repo.maven.apache.org`), regardless of the flag. * **Cache entries are tied to the firewall's config version**, so a change to rules or upstreams is never answered from entries built under the previous version. The change itself propagates across the cluster within a few seconds. A cache entry is a copy of what has passed through, not a package store: entries expire, nothing in them is addressable as a registry, and a cached artifact is still evaluated against the rules on every request. Caching is about latency and upstream load. If a cached upstream is unreachable, the firewall can fall back to a stale cached response rather than failing the request, which softens a short outage. Do not plan around it as an availability guarantee: it only covers what has already been fetched and cached. ## Publishing [#publishing] Publishes are the reverse direction: the firewall receives the upload, runs **upload** phase rules on it, and forwards it to the upstream whose ID is set as the firewall's `publishTarget`, authenticating with that upstream's credentials. Publishing does not write to any other upstream. ## Related [#related] * How-to: [Configure upstreams](/firewall/how-to/configure-upstreams), [Prevent dependency confusion](/firewall/how-to/prevent-dependency-confusion) * Reference: [Upstreams](/firewall/references/upstreams), [Firewall config](/firewall/references/firewall-config) * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) # Cargo (/firewall/ecosystems/cargo) A Cargo firewall is a crate registry that your Rust projects install from instead of [crates.io](https://crates.io). The standard `cargo` command builds through it, with no plugin or wrapper involved: it asks the firewall which versions of a crate exist and downloads the crate file from it, and your rules decide what the firewall is willing to answer. The operations it covers are listed in [Client operations](/firewall/references/client-operations). The firewall fetches from crates.io on your behalf, so nothing you depend on has to move. Setup is one config file in the project, and `Cargo.toml` does not change: the dependencies you already have start going through the firewall as they are. The examples use `` and `` placeholders. Replace them with IDs, not with namespace or firewall names. The firewall's **Setup** tab shows these snippets with both values already filled in. ## Index endpoint [#index-endpoint] ```text sparse+https://eu-sov-1.bytesafecloud.eu/v1//cargo// ``` Two details in that URL are easy to lose and both stop it working: * **`sparse+`** tells cargo to read the index over plain HTTP requests, which is what the firewall serves. Without it, cargo tries to clone a git index instead. * **The trailing slash.** Cargo builds every request by appending to this URL, so dropping the slash replaces the last part of the path instead of extending it. The endpoint needs **Rust 1.74 or newer**, the release where cargo learned to authenticate to a registry for reads as well as publishes. Older cargo does not send your token at all, so every request comes back `401` with nothing in the message to explain why, and no configuration changes that. Cargo authenticates with an [access token](/firewall/getting-started/access-tokens), which it sends exactly as you stored it, with no `Bearer` prefix. That is the Cargo registry convention rather than a Bytesafe one. A Cargo firewall created without upstreams gets `crates-io` (`https://index.crates.io`) as its default upstream; point it elsewhere on the firewall's **Upstreams** tab. One thing to get right when you do: a cargo upstream's endpoint is the registry's index, not its web API. The firewall reads the registry's `config.json` from the index and takes the download host from that file, which is how a crates.io download ends up at `static.crates.io`. ## Configure the project [#configure-the-project] Put this in `.cargo/config.toml` in the project, and commit it: ```toml [registries.firewall] index = "sparse+https://eu-sov-1.bytesafecloud.eu/v1//cargo//" [source.crates-io] replace-with = "firewall" [registry] global-credential-providers = ["cargo:token"] ``` `firewall` is a name you pick for the registry entry, not a cargo keyword. It is the name you pass to `--registry` later, and the name in the credentials file below. All three blocks are needed, and each does a different job: * **`[registries.firewall]`** introduces the firewall to cargo, and says where its index is. * **`[source.crates-io] replace-with`** is the line that sends your existing dependencies there. It tells cargo that anything it would have fetched from crates.io comes from the firewall instead, which is why `Cargo.toml` needs no edit. `Cargo.lock` still records crates.io as the source, so the same lockfile works for someone building without the firewall. * **`[registry] global-credential-providers`** lets cargo use the token you store in the next step. Cargo will not authenticate to a registry without it, and fails with `authenticated registries require a credential-provider to be configured`. There is a tempting shortcut that quietly does the wrong thing: pointing `[registries.crates-io] index` at the firewall. Cargo ignores that and fetches the real crates.io index, so the build succeeds having skipped every rule. Replacing the source is the only setting that actually reroutes crates.io. Then add the token. Cargo keeps credentials in a separate file, so `config.toml` stays committable: ```toml # ~/.cargo/credentials.toml [registries.firewall] token = "" ``` Store the token on its own, with nothing in front of it: cargo sends the value exactly as written. Keep `credentials.toml` out of version control. ## Verify [#verify] ```shell cargo fetch ``` That downloads every dependency in the lockfile, which is enough to prove both halves work: cargo found the versions through the firewall and got the files from it. If a rule matched along the way, it has a firewall log entry: ```shell bsfw logs --firewall ``` ## CI [#ci] Use a Service Access Token (SAT) instead of a PAT. A SAT identifies the CI system rather than a person. Create one as described in [Access Tokens](/firewall/getting-started/access-tokens#service-access-tokens-sat) and store it as a CI secret. Rather than writing `credentials.toml` in the pipeline, pass the token in the environment variable cargo reads for a named registry: ```shell export CARGO_REGISTRIES_FIREWALL_TOKEN="$BYTESAFE_TOKEN" cargo build --locked ``` The variable name comes from the registry name in `config.toml`, uppercased: a registry named `firewall` reads `CARGO_REGISTRIES_FIREWALL_TOKEN`. It is the `cargo:token` provider that reads it, which is the second reason that line belongs in `config.toml`. To correlate firewall log entries from one pipeline run, append `::` and the run ID to the token. The firewall validates the token before the suffix and records the suffix as the [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids). Use the same value for every request in the run. One trap in containers: if the image sets `CARGO_HOME`, and the official `rust` images do, then a config written to `~/.cargo/config.toml` is ignored, with no error. Cargo just goes to the real crates.io. Write the config under `$CARGO_HOME`, or keep it in the project at `.cargo/config.toml`. ## What the firewall serves [#what-the-firewall-serves] | Path | Purpose | | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `/config.json` | Registry settings, which the firewall writes itself: it points downloads back at the firewall, and tells cargo the registry needs a token | | `/{prefix}/{crate}` | The crate's index file, one line per version. Versions rules remove blocked versions before the firewall returns it | | `/api/v1/crates/{crate}/{version}/download` | Crate download. Download-phase rules run here | | `/api/v1/crates/new` | `cargo publish`. Upload-phase rules run here | | `/api/v1/crates/{crate}/{version}/yank` and `/unyank` | `cargo yank` and `cargo yank --undo`. Upload-phase rules run here | `{prefix}` is a directory layout cargo works out from the crate name itself, which is why `cfg-if` is requested as `cf/g-/cfg-if`. Each surviving version's line is passed through unchanged, so anything in it the firewall does not use is preserved rather than dropped. Owner management and search are not part of a firewall endpoint. `cargo owner` and `cargo search` answer `501 Not Implemented`, with a message naming what is supported instead. Neither one moves a package, so there is no rule that could apply to them; see [Client operations](/firewall/references/client-operations#what-the-firewall-does-not-implement). ## How rules apply [#how-rules-apply] Each rule runs on one [execution phase](/firewall/concepts/rule-evaluation#execution-phases), and the phase decides how a block reaches the developer: * **Versions** rules take the version out of the index before cargo's resolver reads it, so it is never picked in the first place. * **Download** rules let the version be picked and then refuse the crate file. * **Upload** rules apply to publishing: `cargo publish`, and yanking or unyanking a version. Selectors match the crate name and the version as the registry published them. Crate names match case-insensitively, so a rule naming `inflector` also matches the crate's canonical `Inflector` spelling. Hyphens and underscores are **not** folded: `cfg-if` and `cfg_if` are different crates, and a selector must be written the way the crate is actually named. `versionRange` selectors use semver, the same notation `Cargo.toml` uses, including comma-separated AND terms such as `">=1.2, <1.5"`. ### Yanked crates [#yanked-crates] A crate version is yanked when its publisher withdraws it from new resolution without deleting it. The `deprecated` [selector function](/firewall/references/selector-functions#deprecated) matches yanked versions, which lets you block them for existing lockfiles too, not just for new resolution. ### Delay rules and publication time [#delay-rules-and-publication-time] A [delay rule](/firewall/how-to/delay-new-versions) holds a version back until it has been public for a while, which means the firewall has to know when it was published. Cargo indexes can carry that date per version, and crates.io does, including for crates released long before the field existed. Worth checking before you rely on one against a **private** registry: if its index leaves the date out, the firewall has no age to compare against, and the rule lets the version through rather than holding it back. ## Selector function support [#selector-function-support] Three selector functions are ecosystem-specific and not available for Cargo. See the [selector function matrix](/firewall/references/selector-functions#matrix): * `install-scripts` is not available. It is implemented for npm, PyPI, and Composer. * `unlisted` is implemented for NuGet only, and `trust-downgrade` for npm only. Everything else applies: `vulnerabilities`, `vulnerability-fix`, `deprecated`, `malware`, `secrets`, and `license`. ## Publishing [#publishing] A Cargo firewall accepts `cargo publish`, `cargo yank`, and `cargo yank --undo`. First set a [publish target](/firewall/how-to/configure-upstreams#choose-a-publish-target): the upstream that should receive them. Without one the firewall is install-only and answers `firewall has no publish target configured`. Replacing the source only reroutes dependency fetches, so for anything else cargo has to be told which registry you mean: ```shell cargo publish --registry firewall cargo yank --registry firewall --version 1.0.0 ``` Upload rules run before anything is sent onward. This is where [secrets scanning](/firewall/how-to/scan-for-secrets) applies, so a crate carrying a leaked credential is stopped before it leaves your organization. Yanks and unyanks are checked as well, since both change what other people's builds resolve to. The firewall stores nothing of its own. A publish that passes the rules is forwarded to the publish target, and that registry's reply is passed straight back, so any warnings it produces still reach the person publishing. Where the publish goes is read from the target registry's own settings, so a registry that offers no publishing endpoint cannot accept one: the firewall then answers `the upstream registry advertises no web API and cannot accept publishes or yanks`. ## What a blocked install looks like [#what-a-blocked-install-looks-like] A **versions** rule removes the version from the index file, so cargo's resolver never sees it. Cargo reports that no version matches your requirement, and lists the ones it did find. Nothing in that message mentions the firewall, which is worth knowing before you go looking for a broken dependency. A **download** rule lets cargo pick the version and then refuses the file. The firewall answers `404` and cargo prints its message: ```text serde@1.0.200 is blocked. Check firewall log entries for request for details ``` An **upload** rule blocks a publish with `403` and `@ upload blocked`, and a yank or unyank with `yank of @ blocked`. Each of these names a request ID; take it to the logs to see which rule fired. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ## Troubleshooting [#troubleshooting] The `[registry] global-credential-providers` line is missing from the config cargo actually read. Every cargo version needs it, and without it nothing else on this page works. Either cargo is older than 1.74, in which case it never sends a token at all and there is no configuration that changes that, or the token in `credentials.toml` is unset, expired, or revoked. Check `cargo --version` first. One 401 is normal and not an error: cargo probes `config.json` unauthenticated, sees the firewall require authentication, and retries with the token. Only a 401 that ends the command is a failure. If a crate that should match a block rule resolves with no matching log entry, cargo did not read the config you edited. Check `CARGO_HOME`: when it is set, `~/.cargo/config.toml` is ignored. Then check that the config uses `[source.crates-io] replace-with` rather than a `[registries.crates-io] index` override, which cargo ignores. The firewall log is the authoritative check: a fetch that produced no entries for the crate did not go through the firewall. Source replacement routes dependency resolution, not publishing. Pass `--registry firewall` on `publish`, `yank`, and any other command that contacts the registry directly. Most likely a versions rule filtered every version that satisfies your requirement. Check the firewall logs before assuming an upstream problem. Also check the spelling: hyphens and underscores are distinct in crate names, and the firewall serves the name the registry uses. ## Related [#related] * How-to: [Block deprecated packages](/firewall/how-to/block-deprecated-packages), [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages), [Delay new versions](/firewall/how-to/delay-new-versions), [Scan for secrets](/firewall/how-to/scan-for-secrets) * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Rules and selectors](/firewall/references/rules-and-selectors), [Selector functions](/firewall/references/selector-functions), [Client operations](/firewall/references/client-operations) # Composer (/firewall/ecosystems/composer) A Composer firewall serves a Composer repository, so a PHP project installs through it the same way it installs from Packagist. Composer 2 resolves versions and downloads packages through the firewall. Versions rules filter the metadata Composer resolves against, and download rules evaluate the package file. The operations the endpoint covers are listed in [Client operations](/firewall/references/client-operations). [Packagist](https://packagist.org) is the public registry Composer uses by default, and the default upstream for a Composer firewall. It is an index rather than a package store: it holds package metadata, while each download can come from a separate artifact host. Most Packagist dist URLs use `api.github.com`. That split is the reason this page has an [artifact host token](#the-artifact-host-token) step the other ecosystem pages do not. The examples use `` and `` placeholders. Replace them with IDs, not with namespace or firewall names. The firewall's **Setup** tab shows these snippets with both values already filled in. ## Repository endpoint [#repository-endpoint] ```text https://eu-sov-1.bytesafecloud.eu/v1//composer/ ``` The namespace ID and firewall ID are part of the URL. Composer appends `packages.json` itself, so configure the base URL above, without a suffix. The endpoint speaks the Composer 2 repository protocol. Composer 1 is not supported, and Packagist ended its own Composer 1 support on 1 September 2025. Authentication is HTTP basic auth with the [access token](/firewall/getting-started/access-tokens) as the password; the username can be anything. A Composer firewall created without upstreams gets `packagist` (`https://repo.packagist.org`) as its default upstream; point it elsewhere on the firewall's Upstreams tab. ## Configure the project [#configure-the-project] Add the firewall as a repository, then disable packagist.org: ```shell composer config repositories.firewall composer https://eu-sov-1.bytesafecloud.eu/v1//composer/ composer config repo.packagist false ``` `firewall` in the first command is a name you pick for the repository entry, not a Composer keyword. It becomes the entry's `name` in `composer.json`. Re-running the command with the same name updates that entry. Using a different name adds another entry. Disabling packagist.org is not optional. Composer queries every configured repository, so leaving it enabled means a package the firewall filtered can still be resolved and installed straight from Packagist, with no rule involved. The commands write both settings into `composer.json`, so committing that file gives every clone and every CI run the same configuration: ```json { "repositories": [ { "packagist": false }, { "name": "firewall", "type": "composer", "url": "https://eu-sov-1.bytesafecloud.eu/v1//composer/" } ] } ``` Then add the token. Composer keeps credentials out of `composer.json`, in `auth.json`, keyed by host: ```json { "http-basic": { "eu-sov-1.bytesafecloud.eu": { "username": "composer", "password": "" } } } ``` The firewall reads the token from the password field and does not check the username. Keep `auth.json` out of version control; Composer's own documentation says to add it to `.gitignore`. `composer config http-basic.eu-sov-1.bytesafecloud.eu composer ` writes the same file, and `--global` writes it in your Composer home directory instead of the project. ## The artifact host token [#the-artifact-host-token] Packagist stores metadata, not package files. Most Packagist dist URLs point to `api.github.com`. GitHub allows 60 unauthenticated requests an hour per egress IP and 5,000 authenticated requests. Firewall requests that use the same egress IP share that allowance, so GitHub-hosted downloads can fail after the anonymous limit is exhausted. Set a GitHub token on the firewall's upstream. It is read against public repositories, so it needs no scopes. In the dashboard: open the firewall's **Upstreams**, edit the packagist upstream, and fill in **GitHub token** under **Artifact host token**. Through the API: ```shell curl -X PUT \ -H "Authorization: Bearer $BYTESAFE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"token": ""}' \ "https://eu-sov-1.bytesafecloud.eu/v1//config//upstreams//artifact-credentials/api.github.com" ``` This credential is separate from the upstream's own credentials by design: the token you set for the upstream is only ever sent to the upstream's own host, so a Packagist credential never travels to GitHub. Both are encrypted at rest and redacted in API responses and exports. See [Artifact host credentials](/firewall/references/upstreams#artifact-host-credentials). Creating the firewall and resolving metadata do not require this token. A missing or invalid token only becomes visible when the artifact host refuses a download. See [Troubleshooting](#troubleshooting) for the resulting error. ## Verify [#verify] ```shell composer require monolog/monolog --prefer-dist ``` The install writes `composer.lock` and unpacks the package into `vendor/`. If a rule matched during the install, inspect its firewall log entry: ```shell bsfw logs --firewall ``` ## CI [#ci] Use a Service Access Token (SAT) instead of a PAT. A SAT identifies the CI system rather than a person. Create one as described in [Access Tokens](/firewall/getting-started/access-tokens#service-access-tokens-sat) and store it as a CI secret. Rather than writing `auth.json` in the pipeline, pass the credentials in `COMPOSER_AUTH`, which holds the same JSON as the file: ```shell export COMPOSER_AUTH='{"http-basic":{"eu-sov-1.bytesafecloud.eu":{"username":"composer","password":"'"$BYTESAFE_TOKEN"'"}}}' composer install --no-interaction --prefer-dist ``` To correlate firewall log entries from one pipeline run, append `::` and the run ID to the token. The firewall validates the token before the suffix and records the suffix as the [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids). Use the same value for every request in the run. ## What the firewall serves [#what-the-firewall-serves] | Path | Purpose | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `/packages.json` | Repository manifest. Points Composer at the metadata and advisory endpoints below | | `/p2/{vendor}/{package}.json` | Package metadata for one package. Versions rules remove blocked versions before the firewall returns it | | `/p2/{vendor}/{package}~dev.json` | Branch metadata for the same package. Composer requests it for every package it resolves | | `/dist/{vendor}/{package}/{version}/{reference}.zip` | Package download. Download-phase rules run here | | `/api/security-advisories/` | Answers `composer audit` | The firewall changes Packagist metadata in three ways: * It rewrites each download URL to the firewall, so the package file passes through your rules instead of being fetched directly from the artifact host. * It removes the `source` entry, so `--prefer-source` or a failed download cannot fall back to a direct clone that bypasses the firewall. * It drops versions with no downloadable file. With the `source` entry removed, those versions cannot be installed. This includes metapackages such as `roave/security-advisories`, which contain version constraints but no package file. ## How rules apply [#how-rules-apply] Rules on the **versions** [execution phase](/firewall/concepts/rule-evaluation#execution-phases) remove versions from the metadata document before Composer's resolver reads it; a filtered version cannot be picked. Rules on the **download** phase block the file fetch itself. Selectors match the Packagist package name (`vendor/package`, lowercased, as Packagist stores it) and the version as the package published it. Version constraints follow Composer's own rules, not npm's. `~1.2` means `>=1.2 <2.0` in Composer, where the same string means `>=1.2 <1.3` in npm. Write `versionRange` selectors the way you would write them in `composer.json`. Age-based selectors ([delay new versions](/firewall/how-to/delay-new-versions)) use the time the version became installable from the index, which is what Packagist records as `published-time`. For packages whose git tags were backdated, that is later than the tag date. ### Install-time code execution [#install-time-code-execution] The `install-scripts` [selector function](/firewall/references/selector-functions#install-scripts) matches releases whose Composer package type is `composer-plugin` or the legacy `composer-installer`. It reads the package type and plugin classes from repository metadata, not from the package file. For Composer, the function supports **versions** and **download** rules. A versions rule removes the plugin release before Composer resolves it. A download rule blocks the package file when a client requests an already-resolved version. Matching log entries begin with `Composer plugin detected` and record the package type and any plugin classes declared in `extra.class`. ### Abandoned packages [#abandoned-packages] Packagist lets a maintainer mark a package abandoned, optionally naming a replacement. The `deprecated` [selector function](/firewall/references/selector-functions#deprecated) matches those, and the log entry names the replacement when the maintainer gave one. ### composer audit [#composer-audit] `composer audit` asks the repository it installed from, which is the firewall, and the firewall answers from its own [advisory dataset](/firewall/references/advisory-sources) rather than forwarding the question to Packagist. So `composer audit` and the `vulnerabilities` [selector function](/firewall/references/selector-functions#vulnerabilities) read the same data, and audit keeps working against a firewall whose upstream is a private mirror with no advisory endpoint. Composer 2.10 also refuses to install a package the repository reports an advisory for, unless `--no-security-blocking` is passed. That is Composer's own behavior, separate from your rules, and it applies whether or not the firewall has a `vulnerabilities` rule. ## Publishing [#publishing] The Composer repository protocol has no upload operation: packages are published by tagging a release in version control and having Packagist read it. So there is nothing for a firewall endpoint to accept, and the `upload` phase does not apply. ## License data [#license-data] Licenses come from the package's own Packagist metadata. There is no public license data source for Composer, so a package that declares no license resolves as unknown; see [Enforce a license policy](/firewall/how-to/enforce-license-policy). ## What a blocked install looks like [#what-a-blocked-install-looks-like] A **versions** rule removes the version from the metadata document, so Composer's resolver never sees it. The client reports that your constraint cannot be satisfied, naming the versions it did find. A **download** rule blocks the file fetch. The firewall returns a 404 with: ```text monolog/monolog@2.9.1 is blocked. Check firewall log entries for request for details ``` Take the request ID to the logs to see which rule fired. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ## Troubleshooting [#troubleshooting] The token in `auth.json` (or `COMPOSER_AUTH`) is unset, expired, or revoked. Check that the `http-basic` key matches the firewall host exactly, without a scheme or path. The artifact host refused the download. The error names the host: ```text dist host api.github.com refused the request and no credentials are configured for it ``` For `api.github.com`, this usually means the anonymous rate limit was exhausted. Set the [artifact host token](#the-artifact-host-token). If credentials were sent, the message instead tells you to check them; the token may have expired or been revoked. If a package that should match a block rule installs with no matching log entry, packagist.org is probably still enabled. Check `composer.json` for the `{"packagist": false}` entry. A global setting on one developer machine does not apply to other clones or CI. Run `composer clear-cache` before testing to rule out a warm client cache. Most likely a versions rule filtered every version that satisfies your constraint. Check the firewall logs before assuming an upstream problem. The other case is a package with no downloadable file for any version, which the firewall does not serve; see [What the firewall serves](#what-the-firewall-serves). Composer is resolving the package from another repository entry. Check `composer.json` for a `vcs` or `path` repository, or for a branch alias resolved from a repository you added. The firewall removes the `source` entry from everything it serves, so an install through it cannot reach a VCS host on its own. ## Related [#related] * How-to: [Block deprecated packages](/firewall/how-to/block-deprecated-packages), [Block install-time code execution](/firewall/how-to/block-install-scripts), [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages), [Manage upstream credentials](/firewall/how-to/manage-upstream-credentials) * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Rules and selectors](/firewall/references/rules-and-selectors), [Selector functions](/firewall/references/selector-functions), [Upstreams](/firewall/references/upstreams) # Conda (/firewall/ecosystems/conda) A Conda firewall serves a Conda channel. conda, mamba, and micromamba install through it; rules filter the package metadata and block artifact downloads. The operations it covers are listed in [Client operations](/firewall/references/client-operations). The examples use `` and `` placeholders. Replace them with IDs, not with namespace or firewall names. The firewall's **Setup** tab shows these snippets with both values already filled in. ## Channel endpoint [#channel-endpoint] ```text https://eu-sov-1.bytesafecloud.eu/v1//conda/ ``` The namespace ID and firewall ID are part of the URL. Use HTTP basic authentication. Set the username to the literal value `__token__` and the password to your Bytesafe [access token](/firewall/getting-started/access-tokens). A Conda firewall created without upstreams gets `conda-forge` (`https://conda.anaconda.org/conda-forge`) as its default upstream; point it elsewhere on the firewall's Upstreams tab. ## Configure the client [#configure-the-client] Use the firewall as the only channel, with credentials embedded in the URL: ```shell export BYTESAFE_TOKEN= export CHANNEL_URL="https://__token__:${BYTESAFE_TOKEN}@eu-sov-1.bytesafecloud.eu/v1//conda/" conda install --override-channels -c "$CHANNEL_URL" numpy ``` ```shell export BYTESAFE_TOKEN= export CHANNEL_URL="https://__token__:${BYTESAFE_TOKEN}@eu-sov-1.bytesafecloud.eu/v1//conda/" micromamba install --override-channels -c "$CHANNEL_URL" numpy ``` Always pass `--override-channels` (or clear the `channels` list in `.condarc`): otherwise `defaults` or a cached conda-forge entry can resolve packages around the firewall. To persist the channel instead of passing `-c` on every command, set it in `.condarc`: ```yaml channels: - https://__token__:@eu-sov-1.bytesafecloud.eu/v1//conda/ ``` This writes the token in plain text into `.condarc`, because conda channels have no separate credential store. Keep that file out of version control and out of shared images. If you need the channel in a repository or a CI image, pass `-c "$CHANNEL_URL"` from an environment variable instead. ## What the firewall serves [#what-the-firewall-serves] | Path | Purpose | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `/channeldata.json` | Channel-level package summary, passed through from the upstream | | `/{subdir}/repodata.json` | Package index for one platform, such as `linux-64`, `osx-arm64`, or `noarch`. Versions rules remove blocked package records before the firewall returns it | | `/{subdir}/{name}-{version}-{build}.conda` | Package artifact download; also `.tar.bz2`. Download-phase rules run here | Conda clients may request `current_repodata.json`, `repodata_from_packages.json`, or compressed `.zst` and `.bz2` variants. The firewall applies the same filtering and returns the requested format. ## How rules apply [#how-rules-apply] Rules on the **versions** [execution phase](/firewall/concepts/rule-evaluation#execution-phases) filter package records out of `repodata.json` before the solver sees them; a filtered version cannot be picked. Rules on the **download** phase block the artifact fetch itself. Selectors match the Conda package's `name` and `version` fields from the repodata record. Different builds of one version are distinct artifacts, but version selectors intentionally match the Conda version, not the build string. Age-based selectors ([delay new versions](/firewall/how-to/delay-new-versions)) use the repodata record's `timestamp`. ## Selector function support [#selector-function-support] Two selector functions have ecosystem-specific behavior. See the [selector function matrix](/firewall/references/selector-functions#matrix): * `vulnerabilities` is not supported for Conda firewalls; the dashboard does not offer it for Conda firewalls, and the API rejects a JSON-configured rule or exception. * `license` has no data source for Conda packages, so versions resolve as unknown license; only the function's `unknown` option has an effect. ## Verify [#verify] The install command under [Configure the client](#configure-the-client) is the client verification. `--override-channels` excludes other configured channels. If `numpy` may already be installed or cached, use a fresh environment and clear the package cache before testing. If a rule matched during the install, inspect its firewall log entry: ```shell bsfw logs --firewall ``` ## CI [#ci] Use a Service Access Token (SAT) instead of a PAT. A SAT identifies the CI system rather than a person. Create one as described in [Access Tokens](/firewall/getting-started/access-tokens#service-access-tokens-sat), store it as a CI secret, and build the channel URL from it. To correlate firewall log entries from one pipeline run, append `::` and the run ID to the token. The firewall validates the token before the suffix and records the suffix as the [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids). ```shell export CHANNEL_URL="https://__token__:${BYTESAFE_TOKEN}::${CI_PIPELINE_ID}@eu-sov-1.bytesafecloud.eu/v1//conda/" ``` Use the same value for every package request in the run. See [Correlating requests with interaction IDs](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) for the token format and CLI options. ## Publishing [#publishing] There is no publish support for Conda: the firewall serves channels read-only, and the `upload` phase does not apply. ## What a blocked install looks like [#what-a-blocked-install-looks-like] A **versions** rule removes the package record from `repodata.json`, so the solver fails to find the package or version; the client reports it as unsolvable or not found from the configured channels. A **download** rule blocks the artifact fetch. The firewall returns a 404 with: ```text numpy@2.3.1 is blocked. Check firewall log entries for request for details ``` Take the request ID to the logs to see which rule fired. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ## Troubleshooting [#troubleshooting] The token in the channel URL is unset, expired, or revoked. The token rides in the URL's password slot; check the URL the client actually uses and confirm the token exists in the dashboard. Most likely a versions rule filtered it out of `repodata.json`. Check the firewall logs before assuming an upstream problem. If a package that should match a block rule installs with no matching log entry, another channel or the local package cache may have answered. Check `.condarc` for `defaults` or other `channels` entries, use `--override-channels`, and clear the client's package cache (`conda clean --all`) before testing again. ## Related [#related] * How-to: [Delay new versions](/firewall/how-to/delay-new-versions), [Configure upstreams](/firewall/how-to/configure-upstreams) * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Rules and selectors](/firewall/references/rules-and-selectors), [Selector functions](/firewall/references/selector-functions) # Containers (OCI) (/firewall/ecosystems/containers) We are currently working on container image (OCI) firewalling for Q4 2026. This page summarizes the planned scope and will become the setup guide at launch. ## Expected capabilities [#expected-capabilities] Like the package firewalls, the container firewall will use a registry proxy in front of image sources. It will evaluate each pull against your rules before the image reaches a build agent or node. * **Registry proxy** in front of Docker Hub, GitHub Container Registry, AWS ECR, Google Artifact Registry, and private OCI registries. * **Vulnerability blocking** for images with known CVEs in base layers or bundled packages, with CVSS and EPSS thresholds like the [package equivalent](/firewall/how-to/block-vulnerable-packages). * **Malware and secrets scanning** of image contents. * **Rules** by image name, tag pattern, registry source, and age, with block or log effects and time-limited exceptions, following the same [rule model](/firewall/concepts/rule-evaluation). * **Logging and audit** of every pull, block, and exception. Details may change before launch. The design carries over concepts from the package firewalls: [rules and evaluation](/firewall/concepts/rule-evaluation), [upstreams](/firewall/concepts/upstreams-and-caching), and [exceptions](/firewall/concepts/exception-model). If container firewalling matters for your adoption, tell us at [support@bytesafe.dev](mailto:support@bytesafe.dev); we can notify you when it is available. # Go (/firewall/ecosystems/go) A Go firewall implements the GOPROXY protocol. The standard Go toolchain downloads modules through it; no extra client is involved. The operations it covers are listed in [Client operations](/firewall/references/client-operations). The examples use `` and `` placeholders. Replace them with IDs, not with namespace or firewall names. The firewall's **Setup** tab shows these snippets with both values already filled in. ## Proxy endpoint [#proxy-endpoint] ```text https://eu-sov-1.bytesafecloud.eu/v1//go/ ``` The namespace ID and firewall ID are part of the URL. Authentication is HTTP basic auth with the [access token](/firewall/getting-started/access-tokens) as the password; the username can be anything. ## Configure the toolchain [#configure-the-toolchain] Set `GOPROXY` with the credentials embedded: ```shell export BYTESAFE_TOKEN= export GOPROXY="https://go:${BYTESAFE_TOKEN}@eu-sov-1.bytesafecloud.eu/v1//go/" ``` Do not append `,direct` or another proxy as a fallback. Go falls back to the next `GOPROXY` entry on errors, which would turn every firewall block into a silent bypass. `go env -w GOPROXY=...` persists the setting per machine, but it writes the token to the Go env file in plain text. Prefer the environment variable so the token stays in your shell or CI secret store. ## The checksum database [#the-checksum-database] The Go toolchain verifies downloaded modules against the public checksum database (`sum.golang.org`). Modules served through the firewall are byte-identical to upstream, so public modules verify as usual. Modules from an internal upstream are not in the public database and fail verification; exempt them: ```shell export GONOSUMDB="go.acme.internal/*" ``` Use `GONOSUMDB`, not `GOPRIVATE`: `GOPRIVATE` also excludes the modules from `GOPROXY`, which would fetch them directly from version control and bypass the firewall. ## Verify [#verify] ```shell go mod download golang.org/x/text ``` With no fallback in `GOPROXY`, a successful download confirms the firewall served the module. Do not expect a log entry from this. A request is logged only when it matches a rule with `log: true` or a logging exception, so a download that breaks no rule leaves no trace. An empty log is not evidence that the toolchain bypassed the firewall. To see the requests while verifying setup, add a log-only rule that matches all modules. See [What is logged when](/firewall/how-to/view-logs#what-is-logged-when). For a record of what was actually served, see [See what was actually downloaded](/firewall/how-to/view-logs#see-what-was-actually-downloaded). ## CI [#ci] Use a Service Access Token (SAT) instead of a PAT. Set it as a secret in your CI system and build `GOPROXY` from it before build steps. To correlate all requests from one pipeline run, append an [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) to the token: ```shell export GOPROXY="https://go:${BYTESAFE_TOKEN}::${CI_PIPELINE_ID}@eu-sov-1.bytesafecloud.eu/v1//go/" ``` The ID shows up on each log entry generated by a matching logging rule or exception from that run. ## Publishing [#publishing] There is nothing to publish to a Go firewall. Go modules are distributed from version control, not uploaded to registries, so the firewall's `upload` phase does not apply to the Go ecosystem. ## What a blocked download looks like [#what-a-blocked-download-looks-like] Rules on the **versions** phase filter versions out of the proxy's version list. The effect depends on how the module is requested: * `go get module@latest` and queries like `@v1` resolve to the newest version that passed the rules. * A `go.mod` requirement on an exact filtered version fails as not found, since modules are pinned by exact version in `go.mod`. Rules on the **download** phase block the module fetch. The firewall returns a 404 with: ```text golang.org/x/text@v0.3.7 is blocked. Check firewall log entries for request for details ``` Take the request ID to the logs to see which rule fired. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ## Troubleshooting [#troubleshooting] The token in `GOPROXY` is unset, expired, or revoked. Confirm `GOPROXY` is set in the environment that runs the build, and that the token is still listed and unexpired in the dashboard. `GOPROXY` carries the token inside the URL, so `go env GOPROXY` prints a live credential. Keep that output out of tickets, chat, and CI logs. Most likely a versions rule filtered it. Because `go.mod` pins exact versions, a filtered version fails the build instead of resolving to an older one. Check the firewall logs before assuming an upstream problem. If a rule expected to log the download but no matching entry appears, check `go env GOPROXY` for extra entries (`,direct`, `,https://proxy.golang.org`) and `go env GOPRIVATE GONOPROXY`, all of which route modules around the firewall. Also check for a `GOFLAGS=-mod=vendor` setup; vendored builds do not contact the proxy at all. Internal modules are not in the public checksum database. Add them to `GONOSUMDB` (see above). If the error concerns a public module, investigate it rather than disabling `GOSUMDB`: modules served through the firewall are byte-identical to upstream, so a mismatch is not explained by the firewall being in the path. # All ecosystems (/firewall/ecosystems) Each firewall endpoint proxies one package ecosystem. Configure your package manager to use the firewall endpoint instead of the public registry, authenticate with an [access token](/firewall/getting-started/access-tokens), and every install goes through your rules. The [dashboard](https://bsfw.bytesafecloud.eu/) is the browser interface for creating and managing firewalls. Package managers use a different, ecosystem-specific URL under `https://eu-sov-1.bytesafecloud.eu/v1/...`; each firewall's **Setup** tab shows the complete value. No firewall yet? Start with the [quickstart](/firewall/getting-started/quickstart), which creates a firewall, installs a package through it, and adds a first rule. New to the product? [What is a dependency firewall?](/firewall/getting-started/what-is-dependency-firewall) explains the model first. ## Supported ecosystems and clients [#supported-ecosystems-and-clients] | Ecosystem | Clients | Firewall endpoint | | ------------------------------------------------ | -------------------------------- | --------------------------------------------------- | | [npm](/firewall/ecosystems/npm) | npm, yarn, pnpm, Bun | `/v1//npm//` | | [PyPI](/firewall/ecosystems/pypi) | pip, uv, Poetry | `/v1//pypi//simple/` | | [Maven](/firewall/ecosystems/maven) | Maven, Gradle | `/v1//maven//` | | [NuGet](/firewall/ecosystems/nuget) | dotnet, nuget.exe, Visual Studio | `/v1//nuget//index.json` | | [Go](/firewall/ecosystems/go) | go toolchain via GOPROXY | `/v1//go/` | | [Cargo](/firewall/ecosystems/cargo) (beta) | cargo, Rust 1.74 or newer | `/v1//cargo//` | | [Composer](/firewall/ecosystems/composer) (beta) | Composer 2 | `/v1//composer/` | | [Conda](/firewall/ecosystems/conda) (beta) | conda, mamba, micromamba | `/v1//conda/` | Ecosystems marked beta are fully usable and enforce your rules the same way the others do. The label says their setup and behavior are still moving. Each ecosystem page states what is available there today, and this table is the only place that tracks which ecosystems carry the label. The endpoints in the table are paths. The full URL is `https://eu-sov-1.bytesafecloud.eu` followed by the path, with your own namespace and firewall IDs filled in. Each firewall's **Setup** tab shows that URL ready to copy, so you never have to assemble it by hand. Each endpoint implements the operations that move packages: resolving versions, downloading artifacts, and publishing. Which of those each ecosystem supports, and why account and discovery commands are not part of it, is in [Client operations](/firewall/references/client-operations). Container image (OCI) firewalling is coming in Q4 2026; see [Containers](/firewall/ecosystems/containers). Using Artifactory, Nexus, or another repository manager? The firewall can sit behind it, in front of it, or on both sides; see [Integrate with repository managers](/firewall/how-to/integrate-repository-managers). ## The pattern [#the-pattern] Regardless of ecosystem, setup is the same three steps: 1. **Replace the registry URL.** Point the client at your firewall endpoint instead of the public registry. 2. **Authenticate.** Supply an access token in the client's credential config. 3. **Prevent bypass.** Commit the config to the repository or enforce it in CI. A machine-local config only protects that machine, and an install that falls back to the public registry never reaches your rules. ## Rule support differences [#rule-support-differences] Rules and exceptions work the same in every ecosystem. Some selector functions are ecosystem-specific: [`unlisted`](/firewall/references/selector-functions#unlisted) exists only for NuGet, [`trust-downgrade`](/firewall/references/selector-functions#trust-downgrade) only for npm, [`install-scripts`](/firewall/references/selector-functions#install-scripts) for npm, PyPI, and Composer, and [`vulnerabilities`](/firewall/references/selector-functions#vulnerabilities) is not available for Conda. The full matrix is in the [selector functions reference](/firewall/references/selector-functions). # Maven (/firewall/ecosystems/maven) A Maven firewall implements the Maven repository layout. Maven and Gradle both resolve and publish through it. The operations it covers are listed in [Client operations](/firewall/references/client-operations). The examples use `` and `` placeholders. Replace them with IDs, not with namespace or firewall names. XML examples use `NAMESPACE_ID` and `FIREWALL_ID` so the files stay valid XML before you replace them. The firewall's **Setup** tab shows a ready-to-copy Maven snippet with both values already filled in. This page also covers Gradle, whose configuration is not generated in the dashboard. ## Repository endpoint [#repository-endpoint] ```text https://eu-sov-1.bytesafecloud.eu/v1//maven// ``` The namespace ID and firewall ID are part of the URL. Authentication is a bearer token header for resolution; publishing also accepts HTTP basic auth with the token as the password. ## Configure the project [#configure-the-project] Mirror everything through the firewall in `~/.m2/settings.xml` (or a checked-in settings file passed with `-s`). Maven sends credentials from the `` whose ID matches the mirror: ```xml title="settings.xml" firewall Bytesafe Dependency Firewall https://eu-sov-1.bytesafecloud.eu/v1/NAMESPACE_ID/maven/FIREWALL_ID/ * firewall Authorization Bearer ${env.BYTESAFE_TOKEN} ``` `mirrorOf: *` routes every repository, including plugin repositories, through the firewall. Replace the repository block in `build.gradle` (or `settings.gradle` under `dependencyResolutionManagement`): ```groovy title="build.gradle" repositories { maven { url = uri('https://eu-sov-1.bytesafecloud.eu/v1//maven//') credentials(HttpHeaderCredentials) { name = 'Authorization' value = "Bearer ${System.getenv('BYTESAFE_TOKEN')}" } authentication { header(HttpHeaderAuthentication) } } } ``` Remove `mavenCentral()` and other repository entries. Gradle tries repositories in order, and any direct entry is a firewall bypass. Each developer exports a [personal access token](/firewall/getting-started/access-tokens): ```shell export BYTESAFE_TOKEN= ``` ## Verify [#verify] ```shell mvn dependency:resolve ``` ```shell gradle dependencies ``` A successful resolution confirms the firewall is serving the artifacts, and every request through that repository is evaluated against your rules. Do not expect a log entry from this. A request is logged only when it matches a rule with `log: true` or a logging exception, so a build that breaks no rule leaves no trace. An empty log is not evidence that the build bypassed the firewall. To see the requests while verifying setup, add a log-only rule that matches all packages. See [What is logged when](/firewall/how-to/view-logs#what-is-logged-when). For a record of what was actually served, see [See what was actually downloaded](/firewall/how-to/view-logs#see-what-was-actually-downloaded). ## CI [#ci] Use a Service Access Token (SAT) instead of a PAT. Set it as a secret in your CI system and export it as `BYTESAFE_TOKEN` before build steps. To correlate all requests from one pipeline run, append an [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) to the token: ```shell export BYTESAFE_TOKEN="${BYTESAFE_TOKEN}::${CI_PIPELINE_ID}" ``` The ID shows up on every log entry from that run. ## Publishing [#publishing] A firewall can receive `mvn deploy` and `gradle publish`. First set a [publish target](/firewall/how-to/configure-upstreams#choose-a-publish-target): the upstream that should receive published artifacts. Without one the firewall is install-only and rejects publishes. Then publish with the firewall as the repository. Upload rules run against the publish before it is forwarded. ```xml title="pom.xml" firewall https://eu-sov-1.bytesafecloud.eu/v1/NAMESPACE_ID/maven/FIREWALL_ID/ ``` With basic-auth credentials on the matching server. The token goes in the password field; the username is not checked, so `deploy` here is only a label: ```xml title="settings.xml" firewall deploy ${env.BYTESAFE_TOKEN} ``` ```groovy title="build.gradle" publishing { repositories { maven { url = uri('https://eu-sov-1.bytesafecloud.eu/v1//maven//') credentials { username = 'deploy' password = System.getenv('BYTESAFE_TOKEN') } } } } ``` A blocked publish fails with: ```text com.acme:app@1.0.0 upload blocked. Check firewall log entries for request for details ``` ## What a blocked install looks like [#what-a-blocked-install-looks-like] Rules on the **versions** phase filter versions out of `maven-metadata.xml`. Maven and Gradle dependencies are usually exact versions, so the practical effects are: * Version ranges and `LATEST`/`RELEASE` style resolution pick the newest version that passed the rules. * An exact dependency on a filtered version fails as not found (`Could not find artifact` in Maven, `Could not resolve` in Gradle). Rules on the **download** phase block the artifact fetch. The firewall returns a 404 with: ```text com.acme:app@1.0.0 is blocked. Check firewall log entries for request for details ``` Take the request ID to the logs to see which rule fired. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ## Maven Central caching [#maven-central-caching] Responses from Maven Central (`repo1.maven.org`, `repo.maven.apache.org`) are always cached by the firewall, regardless of the upstream's cache setting. Maven Central rate-limits heavy traffic with `429 Too Many Requests`; the always-on cache keeps repeat resolutions from counting against that limit. Cache misses are fetched with automatic failover, so a throttling or failing Central does not break the resolution. This is automatic; there is nothing to configure. Other upstreams are cached when the upstream has caching enabled, which applies to internal upstreams. See [Upstreams and caching](/firewall/concepts/upstreams-and-caching). ## Troubleshooting [#troubleshooting] `BYTESAFE_TOKEN` is unset, expired, or revoked, or the `` ID does not match the mirror ID, in which case Maven never attaches the header at all. Confirm the variable is set in the shell that runs the build, that both IDs are identical, and that the token is still listed and unexpired in the dashboard. A 429 in your build output means the build is talking to Maven Central directly, not through the firewall; Central throttles heavy or unauthenticated traffic. Fix the bypass (see below) and the problem disappears: the firewall always caches Maven Central responses, so repeat resolutions are served from cache instead of hitting Central, and cache misses are fetched with automatic failover when Central throttles. Most likely a versions rule filtered it. Because Maven dependencies are exact versions, a filtered version fails the build instead of resolving to an older one. Check the firewall logs before assuming an upstream problem. No log entry does not by itself mean the build bypassed the firewall. Approved packages that match no logging rule pass through without an entry. First test with a log-only rule matching the package or all packages. If that rule still does not produce an entry, check for `` in POMs (a `mirrorOf: *` mirror catches these, a narrower one may not) and extra `repositories` blocks in Gradle builds, including `settings.gradle` plugin management. Plugin repositories go through the firewall too. With Maven, `mirrorOf: *` covers them. With Gradle, add the firewall repository to `pluginManagement.repositories` in `settings.gradle` as well. # npm (/firewall/ecosystems/npm) An npm firewall implements the npm registry protocol. Anything that speaks it can install through the firewall: npm, yarn, pnpm, and Bun. It covers the operations that move packages, which are listed in [Client operations](/firewall/references/client-operations). The examples use `` and `` placeholders. Replace them with IDs, not with namespace or firewall names. The firewall's **Setup** tab shows a ready-to-copy npm snippet with both values already filled in. This page also covers yarn, pnpm, and Bun, whose configuration is not generated in the dashboard. ## Registry endpoint [#registry-endpoint] ```text https://eu-sov-1.bytesafecloud.eu/v1//npm// ``` The namespace ID and firewall ID are part of the URL, so one machine can use different firewalls per project. ## Configure the project [#configure-the-project] Commit a `.npmrc` at the project root: ```ini title=".npmrc" registry=https://eu-sov-1.bytesafecloud.eu/v1//npm// //eu-sov-1.bytesafecloud.eu/v1//npm//:_authToken=${BYTESAFE_TOKEN} ``` npm 6 and earlier need `always-auth=true` as well, see [always-auth](#always-auth). Yarn 1 (Classic) reads the same `.npmrc` as npm, and needs one extra line: ```ini title=".npmrc" registry=https://eu-sov-1.bytesafecloud.eu/v1//npm// //eu-sov-1.bytesafecloud.eu/v1//npm//:_authToken=${BYTESAFE_TOKEN} always-auth=true ``` Without `always-auth`, Yarn Classic does not send credentials on reads, and every install fails with a 401. See [always-auth](#always-auth). Yarn Berry (2+) uses its own config and needs no equivalent: ```yaml title=".yarnrc.yml" npmRegistryServer: "https://eu-sov-1.bytesafecloud.eu/v1//npm//" npmAuthToken: "${BYTESAFE_TOKEN}" ``` pnpm reads the same `.npmrc` as npm: ```ini title=".npmrc" registry=https://eu-sov-1.bytesafecloud.eu/v1//npm// //eu-sov-1.bytesafecloud.eu/v1//npm//:_authToken=${BYTESAFE_TOKEN} ``` Bun reads `.npmrc`, or its own config: ```toml title="bunfig.toml" [install] registry = { url = "https://eu-sov-1.bytesafecloud.eu/v1//npm//", token = "$BYTESAFE_TOKEN" } ``` Each developer exports a [personal access token](/firewall/getting-started/access-tokens): ```shell export BYTESAFE_TOKEN= ``` Committing the registry config, with the token read from an environment variable, gives every clone of the repository the firewall by default and keeps credentials out of git. ### always-auth [#always-auth] The firewall requires an access token on every request, metadata reads included, and never sends an authentication challenge. A client that waits to be challenged before presenting credentials gets a 401 instead. Modern npm does not wait. It matches the registry-scoped `:_authToken` line against the request URL and sends the token every time, so the `.npmrc` above needs nothing further. `always-auth` was npm 6's way of forcing the same behavior; npm 7 and later dropped the setting, and current versions warn that it is an unknown config. Add `always-auth=true` only for a client that still needs it: * **Yarn Classic (1.x).** Required. Without it, reads are unauthenticated and fail. * **npm 6 and earlier.** Required, for the same reason. A project shared by Yarn Classic and a current npm keeps the line and lives with npm's unknown-config warning. npm ignores the setting, Yarn Classic depends on it, and the warning has no effect on the install. ## Verify [#verify] ```shell npm install lodash ``` ```shell yarn add lodash ``` ```shell pnpm add lodash ``` ```shell bun add lodash ``` A successful install confirms the firewall is serving the package, and every request through that registry is evaluated against your rules. Do not expect a log entry from this. A request is logged only when it matches a rule with `log: true` or a logging exception, so an install that breaks no rule leaves no trace. An empty log is not evidence that the client bypassed the firewall. To see the requests while verifying setup, add a log-only rule that matches all packages. See [What is logged when](/firewall/how-to/view-logs#what-is-logged-when). For a record of what was actually served, see [See what was actually downloaded](/firewall/how-to/view-logs#see-what-was-actually-downloaded). ## CI [#ci] Use a Service Access Token (SAT) instead of a PAT. Set it as a secret in your CI system and export it as `BYTESAFE_TOKEN` before install steps. The committed registry config does the rest. To correlate all requests from one pipeline run, append an [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) to the token: ```shell export BYTESAFE_TOKEN="${BYTESAFE_TOKEN}::${CI_PIPELINE_ID}" ``` The ID shows up on every log entry from that run. ## Publishing [#publishing] A firewall can also receive `npm publish`. First set a [publish target](/firewall/how-to/configure-upstreams#choose-a-publish-target): the upstream that should receive published packages. Then publish as usual with the firewall as the registry. Upload rules run against the publish before it is forwarded. Without a publish target the firewall is install-only and rejects publishes. That is a deliberate setting, not a limitation: leave it unset on firewalls that should only pull packages in. ## Dist-tags, deprecate, and unpublish [#dist-tags-deprecate-and-unpublish] `npm dist-tag ls`, `add`, and `rm` work through the firewall. `ls` reads the same rule-filtered view a client resolves against, so a tag pointing at a filtered version does not show. `add` and `rm` need a publish target and go through upload rules first, the same gate publish uses, so a rule can stop `latest` from being moved onto a version the firewall blocks. `npm deprecate` and `npm unpublish` also need a publish target and are gated the same way, per version. Unpublish works version by version: removing a package's last version, the point where npm switches to a whole-package delete, is refused, so a package can never be fully removed through the firewall. ## What a blocked install looks like [#what-a-blocked-install-looks-like] Rules on the **versions** phase filter versions out of the metadata npm sees. Dist-tags pointing at a filtered version are removed. The effect depends on how the dependency is declared: * A range like `^4.0.0` resolves to the newest version that passed the rules. The install succeeds, possibly with an older version than the public latest. * An exact pin to a filtered version fails with npm's `No matching version found` error (`ETARGET`). * A named tag spec like `some-package@canary` fails the same way when the tag points at a filtered version, because the tag is removed from the metadata. The default tag is the exception: `npm install some-package` (implicitly `@latest`) falls back and resolves to the newest version that passed the rules, like a range. Rules on the **download** phase block the tarball fetch instead. The blocked version is often a transitive dependency that a range resolved to, not the package on the command line. npm fails with a 404 from the firewall and a message like: ```text lodash@4.17.20 is blocked. Check firewall log entries for request for details ``` Take the request ID to the logs to see which rule fired. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ## Troubleshooting [#troubleshooting] `BYTESAFE_TOKEN` is unset, expired, or revoked. Confirm the variable is set in the shell that runs the install, and that the token is still listed and unexpired in the dashboard. Most likely a versions rule filtered it. Check the firewall logs before assuming an upstream problem. Expected. The firewall implements the operations that move packages: resolve, download, publish, and audit. Identity and access are managed in the web interface rather than through a package manager, so `whoami`, `login`, `logout`, and token commands are not part of the endpoint. `npm search` is not either, because searching is not a step in resolving or fetching a package. To check which identity a token carries, use [`bsfw whoami`](/firewall/references/cli). For the full picture, see [Client operations](/firewall/references/client-operations). No log entry does not by itself mean the client bypassed the firewall. Approved packages that match no logging rule pass through without an entry. First test with a log-only rule matching the package or all packages. If that rule still does not produce an entry, check for overriding config in `~/.npmrc` or a `NPM_CONFIG_REGISTRY` environment variable, and confirm lockfile `resolved` URLs point at the firewall, not `registry.npmjs.org`. If you route only certain scopes through the firewall (`@acme:registry=...`), everything outside the scope still goes to the public registry unchecked. Route the whole registry through the firewall unless you have a reason not to. # NuGet (/firewall/ecosystems/nuget) A NuGet firewall implements the NuGet v3 API. Clients discover everything through the service index, so dotnet, nuget.exe, and Visual Studio all work against one URL. The operations it covers are listed in [Client operations](/firewall/references/client-operations). The examples use `` and `` placeholders. Replace them with IDs, not with namespace or firewall names. XML examples use `NAMESPACE_ID` and `FIREWALL_ID` so the files stay valid XML before you replace them. The firewall's **Setup** tab shows ready-to-copy `NuGet.Config` and `dotnet` snippets with both values already filled in. This page also covers `nuget.exe`. ## Source endpoint [#source-endpoint] ```text https://eu-sov-1.bytesafecloud.eu/v1//nuget//index.json ``` The namespace ID and firewall ID are part of the URL. Authentication is HTTP basic auth with the [access token](/firewall/getting-started/access-tokens) as the password (any username), or the token as the API key when pushing. ## Configure the project [#configure-the-project] Commit a `NuGet.Config` at the repository root. `` removes nuget.org from the source list, which is what prevents installs from bypassing the firewall: ```xml title="NuGet.Config" ``` `%BYTESAFE_TOKEN%` reads the token from the environment, keeping credentials out of git. Each developer exports a [personal access token](/firewall/getting-started/access-tokens): ```shell export BYTESAFE_TOKEN= ``` Visual Studio uses the same `NuGet.Config`; the Browse tab in **Manage NuGet Packages** works because the firewall implements the search service. That tab lists more than your rules allow you to install, see [Search and Browse results](#search-and-browse-results). ## Verify [#verify] ```shell dotnet add package Newtonsoft.Json ``` ```shell nuget.exe install Newtonsoft.Json ``` A successful install confirms the firewall is serving the package, and every request through that source is evaluated against your rules. Do not expect a log entry from this. A request is logged only when it matches a rule with `log: true` or a logging exception, so an install that breaks no rule leaves no trace. An empty log is not evidence that the client bypassed the firewall. To see the requests while verifying setup, add a log-only rule that matches all packages. See [What is logged when](/firewall/how-to/view-logs#what-is-logged-when). For a record of what was actually served, see [See what was actually downloaded](/firewall/how-to/view-logs#see-what-was-actually-downloaded). ## CI [#ci] Use a Service Access Token (SAT) instead of a PAT. Set it as a secret in your CI system and export it as `BYTESAFE_TOKEN` before restore steps. The committed `NuGet.Config` does the rest. To correlate all requests from one pipeline run, append an [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) to the token: ```shell export BYTESAFE_TOKEN="${BYTESAFE_TOKEN}::${CI_PIPELINE_ID}" ``` The ID shows up on every log entry from that run. ## Vulnerability audit [#vulnerability-audit] `dotnet restore` audit warnings (`NU1901` to `NU1904`) work through the firewall. The vulnerability info resource is served from the firewall's advisory data, so restore-time audits keep working without a direct connection to nuget.org. ## Publishing [#publishing] A firewall can receive `push`. First set a [publish target](/firewall/how-to/configure-upstreams#choose-a-publish-target): the upstream that should receive published packages. Without one the firewall is install-only and rejects pushes. Then push with the token as the API key: ```shell dotnet nuget push Acme.Utils.1.0.0.nupkg \ --source https://eu-sov-1.bytesafecloud.eu/v1//nuget//index.json \ --api-key $BYTESAFE_TOKEN ``` ```shell nuget.exe push Acme.Utils.1.0.0.nupkg \ -Source https://eu-sov-1.bytesafecloud.eu/v1//nuget//index.json \ -ApiKey $BYTESAFE_TOKEN ``` Upload rules run against the push before it is forwarded. Unlisting a version (`dotnet nuget delete`) goes through upload rules the same way. A blocked push fails with: ```text Acme.Utils@1.0.0 upload blocked. Check firewall log entries for request for details ``` ## What a blocked install looks like [#what-a-blocked-install-looks-like] Rules on the **versions** phase filter versions out of the flat container index and registration pages. The effect depends on how the dependency is declared: * A version range or floating version (`1.*`) resolves to the newest version that passed the rules. * An exact `PackageReference` to a filtered version fails as not found (NuGet error `NU1102`, unable to find package with the requested version). Rules on the **download** phase block the package fetch. The firewall returns a 404 with: ```text Newtonsoft.Json@13.0.1 is blocked. Check firewall log entries for request for details ``` Take the request ID to the logs to see which rule fired. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ## Search and Browse results [#search-and-browse-results] Search shows what the upstream offers, including versions your rules block. `dotnet package search`, `nuget.exe search`, and the Visual Studio **Browse** tab are all answered this way. Being listed is not being installable. Policy is enforced where the package is actually resolved and downloaded, so a blocked version you can see in search fails at restore exactly as it would if you had typed the version in by hand. Search answers what exists; the install path decides what you get. This is deliberate. Filtering the results would hide packages from a developer browsing for something to use, which is the workflow those tools exist for. Autocomplete version lists are filtered. ## Deprecated and unlisted versions [#deprecated-and-unlisted-versions] Two selector functions are useful with NuGet specifically: * `deprecated` matches versions their publisher has marked deprecated. * `unlisted` matches versions hidden from search (`listed: false`). Unlisted versions remain installable for existing consumers on nuget.org; a rule with this selector blocks them. See the [selector functions reference](/firewall/references/selector-functions). ## Troubleshooting [#troubleshooting] `BYTESAFE_TOKEN` is unset, expired, or revoked. Confirm the variable is set for the process that runs the restore, not only in your own shell profile, and that the token is still listed and unexpired in the dashboard. `%BYTESAFE_TOKEN%` in the config file is only substituted if the variable exists in that environment, and a missing variable looks the same as a wrong token. Most likely a versions rule filtered it. Check the firewall logs before assuming an upstream problem. No log entry does not by itself mean the client bypassed the firewall. Approved packages that match no logging rule pass through without an entry. First test with a log-only rule matching the package or all packages. If that rule still does not produce an entry, another source may be active. NuGet merges config files from the machine, user, and project levels; run `dotnet nuget list source` and confirm only the firewall source is enabled. Keep the `` element in the committed config. The credential key inside `` must match the source key exactly (`firewall` in the examples). Keep `ValidAuthenticationTypes` set to `basic` so the client does not stall trying other schemes. # PyPI (/firewall/ecosystems/pypi) A PyPI firewall implements the Simple Repository API, the interface pip and its peers use to find and download packages. Anything that speaks it can install through the firewall: pip, uv, and Poetry. The operations it covers are listed in [Client operations](/firewall/references/client-operations). The examples use `` and `` placeholders. Replace them with IDs, not with namespace or firewall names. The firewall's **Setup** tab shows a ready-to-copy pip snippet with both values already filled in. This page also covers uv and Poetry, whose configuration is not generated in the dashboard. ## Index endpoint [#index-endpoint] ```text https://eu-sov-1.bytesafecloud.eu/v1//pypi//simple/ ``` The namespace ID and firewall ID are part of the URL. Use HTTP basic authentication. Set the username to the literal value `__token__` and the password to your Bytesafe [access token](/firewall/getting-started/access-tokens). ## Configure the project [#configure-the-project] Commit a `pip.conf` (`pip.ini` on Windows) or set the index per project: ```ini title="pip.conf" [global] index-url = https://__token__:${BYTESAFE_TOKEN}@eu-sov-1.bytesafecloud.eu/v1//pypi//simple/ ``` Or without a config file: ```shell pip install --index-url "https://__token__:${BYTESAFE_TOKEN}@eu-sov-1.bytesafecloud.eu/v1//pypi//simple/" requests ``` Use `index-url`, not `extra-index-url`. An extra index keeps pypi.org as a parallel source, and pip is free to install from it, bypassing the firewall entirely. uv reads the index from `pyproject.toml`: ```toml title="pyproject.toml" [[tool.uv.index]] name = "firewall" url = "https://eu-sov-1.bytesafecloud.eu/v1//pypi//simple/" default = true ``` With credentials in the environment: ```shell export UV_INDEX_FIREWALL_USERNAME=__token__ export UV_INDEX_FIREWALL_PASSWORD=$BYTESAFE_TOKEN ``` The pip-compatible interface also accepts the flag directly: ```shell uv pip install --index-url "https://__token__:${BYTESAFE_TOKEN}@eu-sov-1.bytesafecloud.eu/v1//pypi//simple/" requests ``` Point Poetry's source at the firewall and store the credential outside the repository: ```toml title="pyproject.toml" [[tool.poetry.source]] name = "firewall" url = "https://eu-sov-1.bytesafecloud.eu/v1//pypi//simple/" priority = "primary" ``` ```shell poetry config http-basic.firewall __token__ $BYTESAFE_TOKEN ``` Each developer exports a [personal access token](/firewall/getting-started/access-tokens): ```shell export BYTESAFE_TOKEN= ``` ## Verify [#verify] ```shell pip install requests ``` ```shell uv add requests ``` ```shell poetry add requests ``` A successful install confirms the firewall is serving the package, and every request through that index is evaluated against your rules. Do not expect a log entry from this. A request is logged only when it matches a rule with `log: true` or a logging exception, so an install that breaks no rule leaves no trace. An empty log is not evidence that the client bypassed the firewall. To see the requests while verifying setup, add a log-only rule that matches all packages. See [What is logged when](/firewall/how-to/view-logs#what-is-logged-when). For a record of what was actually served, see [See what was actually downloaded](/firewall/how-to/view-logs#see-what-was-actually-downloaded). ## CI [#ci] Use a Service Access Token (SAT) instead of a PAT. Set it as a secret in your CI system and export it as `BYTESAFE_TOKEN` before install steps. To correlate all requests from one pipeline run, append an [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) to the token: ```shell export BYTESAFE_TOKEN="${BYTESAFE_TOKEN}::${CI_PIPELINE_ID}" ``` The ID shows up on each log entry generated by a matching logging rule or exception from that run. ## Publishing [#publishing] A firewall can also receive uploads. First set a [publish target](/firewall/how-to/configure-upstreams#choose-a-publish-target): the upstream that should receive published packages. Without one the firewall is install-only and rejects uploads. Then upload with twine against the firewall's legacy endpoint: ```shell twine upload \ --repository-url https://eu-sov-1.bytesafecloud.eu/v1//pypi//legacy/ \ -u __token__ -p $BYTESAFE_TOKEN \ dist/* ``` Upload rules run against the publish before it is forwarded. A blocked upload fails with: ```text requests@2.32.0 upload blocked. See firewall log for request ``` ## What a blocked install looks like [#what-a-blocked-install-looks-like] Rules on the **versions** phase filter releases out of the simple index. The effect depends on how the dependency is declared: * A range like `requests>=2.31` resolves to the newest version that passed the rules. The install succeeds, possibly with an older version than the public latest. * An exact pin to a filtered version fails with pip's `Could not find a version that satisfies the requirement` error, because that release no longer exists as far as the client can see. Rules on the **download** phase block the file fetch. The firewall returns a 404 with: ```text requests@2.32.0 is blocked. Check firewall log entries for request for details ``` Take the request ID to the logs to see which rule fired. See [Investigate a blocked install](/firewall/how-to/investigate-blocked-install). ## Yanked releases [#yanked-releases] Releases yanked by their publisher (PEP 592) match the `deprecated` [selector function](/firewall/references/selector-functions#deprecated), so one rule keeps yanked releases out of your builds. ## Install scripts in sdists [#install-scripts-in-sdists] The `install-scripts` [selector function](/firewall/references/selector-functions#install-scripts) matches source distributions that run package code at build time: a root `setup.py`, or an in-tree PEP 517 build backend (`backend-path` in `pyproject.toml`). Wheels install without executing package code, so they never match. This asymmetry is visible during rollout: for a release shipping both formats, pip normally picks the wheel and installs fine, while a client forced to build from source (`--no-binary`, or a platform with no matching wheel) hits the sdist and the rule. See [Block install-time code execution](/firewall/how-to/block-install-scripts). ## Troubleshooting [#troubleshooting] `BYTESAFE_TOKEN` is unset, expired, or revoked, or the URL lost its credential part. Confirm the variable is set in the shell that runs the install, that the URL still carries `__token__:@` before the host, and that the token is still listed and unexpired in the dashboard. The token travels in the password field; the username is not checked, so `__token__` is convention, not a requirement. Most likely a versions rule filtered it. Check the firewall logs before assuming an upstream problem. If a rule expected to log an install but no matching entry appears, check for `extra-index-url` entries and overriding config with `pip config list`, and check the `PIP_INDEX_URL` environment variable. Any extra index pointing at pypi.org is an open bypass. Credentials embedded in the index URL are parsed as a URL, so characters like `@` or `/` in a password would need percent-encoding. Bytesafe tokens (`pat-`/`sat-` plus a UUID) contain none of these, but an appended interaction ID should stick to URL-safe characters. # Access Tokens (/firewall/getting-started/access-tokens) Every request to a firewall is authenticated. Package managers, the CLI, and CI systems all present an access token. There are three kinds. | Token | Prefix | Bound to | Typical use | Lifetime | | --------------------------- | ------ | ---------------------- | --------------------------------- | --------------------------- | | Personal Access Token (PAT) | `pat-` | A user | Developer machines | Long-lived, revocable | | Service Access Token (SAT) | `sat-` | The namespace | CI, build servers, shared systems | Long-lived, revocable | | Trusted Builder token | `sat-` | A CI workflow identity | GitHub Actions via OpenID Connect | Short-lived, issued per run | Exact lifetimes, permissions, and wire format are in the [tokens reference](/firewall/references/tokens). ## Personal Access Tokens (PAT) [#personal-access-tokens-pat] Use a PAT on your own machine. Installs are attributed to you in the firewall logs, which is what makes log entries useful when someone asks why a package was fetched. Access Tokens is not in the main sidebar. Open the user menu in the lower-left corner of the dashboard (your name and avatar), then select **Access Tokens**. On that page: 1. Select **Create Token**, fill in the name, validity, and permissions (see [Permissions and validity](#permissions-and-validity) below), and create it. 2. Copy the value straight away: it is shown once and cannot be retrieved afterwards. If you lose it, create a new token and revoke the old one. Use it in your package manager config (see [Ecosystems](/firewall/ecosystems)) or export it for the CLI: ```shell export BYTESAFE_TOKEN= ``` ## Service Access Tokens (SAT) [#service-access-tokens-sat] Use a Service Access Token where no person is involved: CI pipelines, container builds, shared build servers. Service Access Tokens are created the same way from the **Access Tokens** page, given a name that identifies the system using it. Do not reuse one Service Access Token everywhere. One token per system keeps the logs attributable, and it means revoking a leaked token affects that one system instead of every pipeline you run. ## Permissions and validity [#permissions-and-validity] The **Create Token** dialog asks for a name, how long the token is valid, permissions, and optional firewall restrictions. | Token | Who can create it | Permissions | | --------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------- | | PAT | A user with `ns:create:pat` | Narrows the user's own permissions. It cannot grant more than the user already has | | SAT | A user with `ns:create:service-token` | Grants the selected Service Access Token permissions. It cannot grant more than the creator already has | | Trusted Builder token | Registered Trusted Builder | Uses the permissions on the Trusted Builder registration | For PATs and SATs, leaving **Restrict to firewalls** empty allows every firewall the token has permission to use. Selecting firewalls limits the token to those firewall IDs. SAT permissions are limited to unattended work. A SAT can install, publish, read firewall config, create or clone firewalls, and update config, rules, or exceptions when granted. It cannot manage upstream credentials, teams, tokens, namespace deletion, or firewall deletion. Dashboard token presets are 1 day, 7 days, 30 days, 90 days, and 1 year. The API accepts `validForHours`; the default is 12 hours and the maximum is 2 years. Trusted Builder tokens are valid for 15 minutes. Every token has an expiry, so pick a validity that matches how long the system will use it, and plan for the replacement. Trusted Builders avoid that work entirely, since each run gets its own token. See [Permissions](/firewall/references/permissions) for the permission catalog and [Tokens](/firewall/references/tokens) for the wire format. ## Trusted Builders (OIDC) [#trusted-builders-oidc] For CI systems that can prove their identity with OpenID Connect (OIDC), there is a better option than a stored secret: register the workflow as a Trusted Builder. At run time, the workflow exchanges its OIDC identity token for a short-lived Service Access Token. Nothing long-lived is stored in the CI system. Trusted Builders currently support GitHub Actions. Setup is covered in the [GitHub Actions guide](/firewall/getting-started/github-actions). ## Correlating requests with interaction IDs [#correlating-requests-with-interaction-ids] A single token is often shared across many contexts: a build server runs every pipeline, a CI workflow runs every branch. Interaction IDs add traceability on top without minting a token per build, application, or team. Any token can carry an opaque interaction ID by appending it after a double colon: ```text ::build-4711 ``` The firewall strips the suffix before validating the token and records it in the log entry. The value is opaque, so use whatever dimension you want to correlate on later: a CI run (`build-4711`), an application or project (`checkout-service`), an environment (`staging`), or a combination. The CLI does this with the `--interaction` flag or the `BYTESAFE_INTERACTION` environment variable. ## Next steps [#next-steps] * Put the token to use: [configure npm](/firewall/ecosystems/npm). * See what your token is doing: [view logs](/firewall/how-to/view-logs). # Config as code (/firewall/getting-started/config-as-code) A firewall's entire policy, upstreams, rules, and exceptions, is one JSON document. That makes it a natural fit for git: policy changes become pull requests with review and history, and applying them is one command. It is the GitOps approach applied to dependency policy. This tutorial walks the full loop once: export, commit, change, import, verify, roll back. You need the [CLI installed and authenticated](/firewall/references/cli#initial-setup) and a firewall to work with. Replace `` with its ID, not its name. ## Step 1: export the configuration [#step-1-export-the-configuration] ```shell bsfw firewall export > firewall-config.json git add firewall-config.json git commit -m "Snapshot firewall config" ``` Open the file. You will recognize the structure from the [firewall config reference](/firewall/references/firewall-config): `upstreams`, `rules`, `exceptions`, `permissions`. Upstream credentials are placeholders, not secrets; the export is safe to commit. ## Step 2: change policy in the file [#step-2-change-policy-in-the-file] Add a rule to the `rules` array, for example a 14-day delay rule: ```json { "id": "delay-14d", "executionPhase": "versions", "selector": { "maxAgeHours": 336 }, "ruleEffect": { "block": true, "log": true }, "description": "Block versions younger than 14 days" } ``` Commit it. In a team setup this is where the pull request happens: the diff shows exactly which policy changes, and review happens like any code review. ## Step 3: import it back [#step-3-import-it-back] ```shell bsfw firewall import --file firewall-config.json ``` Import replaces the whole configuration with the file's content. The firewall's `version` increments, and the change lands in the [audit log](/firewall/how-to/review-audit-log) as `firewall.config.upsert`. Because it is a full replace, anything changed in the dashboard since your export is overwritten. Once a firewall is managed from git, treat the file as the source of truth: export first if someone may have edited it elsewhere. ## Step 4: verify [#step-4-verify] ```shell bsfw firewall rules ``` The new rule is in the list. Requests now evaluate against it; a package version younger than 14 days is filtered. ## Step 5: roll back [#step-5-roll-back] Every import creates a new config version, and the server keeps history. To roll back, re-import the previous file from git: ```shell git checkout HEAD~1 -- firewall-config.json bsfw firewall import --file firewall-config.json ``` The API can also roll back server-side to a recorded version (`PUT /v1//config//version/{version}`, history via `GET .../history`), which is useful when the git copy is not at hand. See the [API reference](/firewall/references/api#configuration). Either way, a rollback is itself a new version; history moves forward. ## What does not travel through git [#what-does-not-travel-through-git] Credentials. Exports carry placeholders instead of secret values, and imports reject plaintext secrets, so a config file cannot leak an upstream credential. The flip side is that credentials do not travel with the config: after importing a firewall into a new environment, set them once with the CLI. ```shell bsfw firewall credentials --firewall --upstream ``` See [Manage upstream credentials](/firewall/how-to/manage-upstream-credentials). ## Next steps [#next-steps] * Make this a pipeline: [Manage config as code](/firewall/how-to/manage-config-as-code) covers repo layout, CI import, and drift detection. * Run the import from CI without stored secrets: [GitHub Actions](/firewall/getting-started/github-actions). * Reference: [Firewall config](/firewall/references/firewall-config), [CLI](/firewall/references/cli). # GitHub Actions (/firewall/getting-started/github-actions) A workflow that installs packages through the firewall needs a token. Storing a long-lived Service Access Token as a repository secret works, but it is a stored credential: it can leak, and someone has to rotate it. GitHub Actions can do better, because every workflow run can prove its identity with an OpenID Connect (OIDC) ID token signed by GitHub. This tutorial registers your repository as a **Trusted Builder** and sets up a workflow that exchanges its OIDC identity for a firewall token at run time. The exchanged token is a Service Access Token valid for 15 minutes; there is nothing to store and nothing to rotate. The examples use `` and `` placeholders. Use IDs, not namespace or firewall names. ## Before you start [#before-you-start] * You can create Service Access Tokens (the `ns:create:service-token` permission, held by admins by default). A Trusted Builder creates a Service Access Token on every exchange, so registering one requires this permission. * Your repository already has the firewall configured as its registry (see your [ecosystem page](/firewall/ecosystems)), with the token read from `BYTESAFE_TOKEN`. ## Step 1: register the Trusted Builder [#step-1-register-the-trusted-builder] In the dashboard, open the user menu in the lower-left corner and select **Access Tokens** (not in the main sidebar), switch to the **Trusted Builders** tab and select **Add Builder**: * **Name**: something that identifies the workflow, for example `acme/webshop CI`. * **Repository owner** and **Repository**: your GitHub organization and repository, for example `acme` and `acme/webshop`. The exchange verifies these against the token's claims; leave a field empty to not constrain it. * **Repository ID**: GitHub's numeric repository ID. Optional but recommended: names can be reassigned after a repository is deleted or renamed, the ID cannot. * **Permissions**: what the exchanged token may do. For installs, `fw:access` is enough. * **Restrict to firewalls**: limit the token to the firewalls this workflow needs. Empty allows all firewalls. Save it and copy the **audience** value. It has the form: ```text bytesafe:trusted-builder:6f1e9c2a-... ``` A Trusted Builder cannot grant more than you hold yourself; permissions are capped at creation just like manual Service Access Tokens. ## Step 2: add the exchange to the workflow [#step-2-add-the-exchange-to-the-workflow] The job needs `id-token: write` permission, which is what lets it request an OIDC token from GitHub. Then `bsfw oidc exchange` fetches the ID token for your audience and swaps it for a firewall token: ```yaml title=".github/workflows/build.yml" jobs: build: runs-on: ubuntu-latest permissions: id-token: write contents: read env: BYTESAFE_ENDPOINT: https://eu-sov-1.bytesafecloud.eu BYTESAFE_NAMESPACE: BYTESAFE_TRUSTED_BUILDER_AUDIENCE: bytesafe:trusted-builder:6f1e9c2a-... steps: - uses: actions/checkout@v4 - name: Install bsfw env: BSFW_INSTALL_DIR: ${{ runner.temp }}/bsfw run: | curl -fsSL https://raw.githubusercontent.com/bytesafedev/bsfw/main/install.sh | sh echo "$BSFW_INSTALL_DIR" >> "$GITHUB_PATH" - name: Get firewall token run: | TOKEN=$(bsfw oidc exchange) echo "::add-mask::$TOKEN" echo "BYTESAFE_TOKEN=$TOKEN" >> "$GITHUB_ENV" - name: Install dependencies run: npm ci ``` The install step downloads the latest release, checks it against the release checksums, and puts it on the job's `PATH`. Set `BSFW_VERSION` on that step to pin a release instead. Other install methods: [CLI installation](/firewall/references/cli#install). No `BYTESAFE_TOKEN` is needed before the exchange step, because the exchange endpoint authenticates the job by its GitHub identity rather than by a token. The `::add-mask::` line tells GitHub to redact the returned token from the workflow log, and the last line puts it in `BYTESAFE_TOKEN`, which is where your committed registry config already looks for it. The exchanged token is valid for 15 minutes from the exchange, not from the start of the job. In a long job, run the exchange again in the step that needs it rather than once at the top. To correlate all requests from one run in the logs, change the first line of the exchange step to `TOKEN="$(bsfw oidc exchange)::${GITHUB_RUN_ID}"`. Keep the mask and environment-file lines unchanged. ## Without the CLI [#without-the-cli] The exchange is two HTTP calls, so a workflow can do it with `curl`: ```yaml - name: Get firewall token run: | ID_TOKEN=$(curl -sSf -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$BYTESAFE_TRUSTED_BUILDER_AUDIENCE" | jq -r .value) ACCESS_TOKEN=$(curl -sSf -X POST \ "$BYTESAFE_ENDPOINT/v1/$BYTESAFE_NAMESPACE/oidc/exchange" \ -H "Content-Type: application/json" \ -d "{\"idToken\": \"$ID_TOKEN\"}" | jq -r .accessToken) echo "BYTESAFE_TOKEN=$ACCESS_TOKEN" >> "$GITHUB_ENV" ``` `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` are injected by GitHub into any job with `id-token: write`. ## Step 3: verify [#step-3-verify] Run the workflow, then check the firewall logs: ```shell bsfw logs --firewall --all-users ``` Requests from the run are attributed to a token named `github:acme/webshop:`. If you appended the run ID as an interaction ID, it appears on the entries too. ## When the exchange fails [#when-the-exchange-fails] The exchange returns 401 without detail, so start with the two values the workflow controls: the job needs `id-token: write`, and `BYTESAFE_TRUSTED_BUILDER_AUDIENCE` has to match the audience shown on the Trusted Builder registration exactly. If the token identifies a configured Trusted Builder, the [audit log](/firewall/how-to/review-audit-log) records the `oidc.exchange` result and reasons such as repository mismatch or verification failure. Malformed tokens and unknown audiences cannot be assigned to a tenant, so they are not shown in its audit log. An exchange that fails without leaving an audit entry points at the audience or the ID token itself. ## Next steps [#next-steps] * Restrict what the workflow can do: the Trusted Builder's permissions and firewall list are the place, see [Manage tokens](/firewall/how-to/manage-tokens). * Correlate runs in logs: [interaction IDs](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids). * Reference: [Tokens](/firewall/references/tokens), [CLI](/firewall/references/cli#bsfw-oidc-exchange). # Getting started (/firewall/getting-started) Start here if you are new to the firewall. These pages explain what the product does, then walk through setup paths for local use, CI, and config as code. # Quickstart (/firewall/getting-started/quickstart) This tutorial takes you from nothing to a working npm firewall with one active rule and one exception, touching each core part along the way: firewalls, upstreams, tokens, rules, exceptions, and logs. Every ecosystem follows the same pattern; npm just keeps the steps concrete. Before you start, install npm. [Sign up and start a 14-day trial](https://bsfw.bytesafecloud.eu/signup). If you already have an account, [open the dashboard](https://bsfw.bytesafecloud.eu/). ### Create a firewall [#create-a-firewall] In the [dashboard](https://bsfw.bytesafecloud.eu/), select your namespace, choose **npm** as the ecosystem, and name the firewall. The name is only a label. The firewall also gets a generated **ID**, shown in the firewall list and on the **Setup** tab. That ID, not the name, is what goes in the endpoint path and in every `--firewall` flag. The namespace works the same way: the path and `BYTESAFE_NAMESPACE` use the namespace ID, not its display name. The new firewall comes with a default upstream pointing at `registry.npmjs.org`. That is all it needs to start proxying packages. ### Create an access token [#create-an-access-token] Package managers authenticate to the firewall with an access token. Access Tokens is not in the main sidebar: open the user menu in the lower-left corner of the dashboard (your name and avatar) and select **Access Tokens**, then create a personal access token (PAT). Copy the token value: it is shown once. For CI systems, use a Service Access Token (SAT) instead. See [Access Tokens](/firewall/getting-started/access-tokens) for when to use which. ### Configure npm [#configure-npm] Point npm at your firewall in the project's `.npmrc`. npm uses the firewall endpoint, not the dashboard URL. The firewall's **Setup** tab provides a ready-to-use snippet. In the example below, replace `` and `` with the IDs from that tab: ```ini title=".npmrc" registry=https://eu-sov-1.bytesafecloud.eu/v1//npm// //eu-sov-1.bytesafecloud.eu/v1//npm//:_authToken=${BYTESAFE_TOKEN} ``` Export the token: ```shell export BYTESAFE_TOKEN= ``` Keeping the token in an environment variable keeps it out of version control. ### Install a package [#install-a-package] ```shell npm install lodash ``` The install works as before. The difference: the request went through your firewall, was checked against your rules (none yet), fetched from the upstream, and logged. ### Add a delay rule [#add-a-delay-rule] A delay rule keeps versions younger than a set age out of your builds. The point is to let a release sit in public for a while, so that a malicious version has a chance to be reported and removed before it reaches you. In the dashboard, open your firewall, go to **Rules**, and select **Add Rule**. On the intent step, choose **Let new releases mature** and select **Continue**. The choice seeds the phase and a 7-day max-age value; both remain editable in the rule form. * Execution phase: **Versions** (called `versions` in config, API, and CLI) * Selector, max age: **7 days** * Effect: **block** and **log** The same rule as JSON, if you manage config as code: ```json { "executionPhase": "versions", "selector": { "maxAgeHours": 168 }, "ruleEffect": { "block": true, "log": true }, "description": "Block versions younger than 7 days" } ``` `maxAgeHours` is a whole number of hours, so 168 is 7 days. You do not have to use the dashboard for this. Firewalls, rules, and exceptions can also be created from the terminal; see the [CLI reference](/firewall/references/cli#bsfw-firewall-import). ### See the rule work [#see-the-rule-work] Pick any package whose newest version was published in the last few days, and ask for that exact version. Substitute the real name and version for the placeholders below: ```shell npm install some-package@9.9.9 ``` Because that version is younger than 7 days, the firewall filters it out of the version list npm sees, and the exact pin fails with `No matching version found`. Requests that are not pinned keep working: * A version range, like the `^1.2.3` entries in `package.json`, resolves to the newest version older than 7 days and installs fine. * `npm install some-package` without a version does the same: the firewall removes the `latest` tag when it points at a filtered version, and npm falls back to the newest allowed version. * A named tag like `@canary` fails as not found when it points at a filtered version. ### Add an exception [#add-an-exception] Suppose you need a fresh release right now, for example a security patch. Instead of weakening the rule, add an exception scoped to that one package version. In the dashboard, open **Exceptions** on the firewall and add one: * Rule: your delay rule * Package name: `some-package` * Version: the exact version you need * Reason: pick one, for example **evaluating component** * Expires: set a date, for example two weeks out Run the install again. The version resolves, and the exception is recorded in the logs. Expiring exceptions are the point: the waiver disappears on its own instead of becoming permanent policy nobody remembers. ### Read the logs [#read-the-logs] Every rule match is logged. In the dashboard, open **Logs** on the firewall to see requests, matched rules, and block decisions. From the terminal, with the [bsfw CLI](/firewall/references/cli): ```shell export BYTESAFE_ENDPOINT=https://eu-sov-1.bytesafecloud.eu export BYTESAFE_NAMESPACE="" bsfw logs --firewall --follow ``` `--follow` streams new entries live. Trigger an install in another terminal and watch the entries arrive. ## Where to go next [#where-to-go-next] * [Set up npm properly](/firewall/ecosystems/npm) for your team and CI, including yarn, pnpm, and Bun. * [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages) with CVSS and EPSS thresholds. * [Rule evaluation](/firewall/concepts/rule-evaluation) explains how phases, selectors, and exceptions fit together. * [Migrate from previous-generation Bytesafe](/firewall/how-to/migrate-from-previous-generation) if you use the previous product. # What is a dependency firewall? (/firewall/getting-started/what-is-dependency-firewall) A dependency firewall is a policy checkpoint between your package managers and public registries. Instead of installing packages directly from npmjs.org or PyPI, your tools install through a firewall endpoint. The firewall fetches packages from its configured upstreams, evaluates your rules, and either serves the package or blocks it. Think of it as a network firewall for dependencies: traffic still flows through, but rules decide what is allowed, and exceptions document the cases that should pass anyway. The decision happens at install time, before code reaches a developer machine or CI runner. That is the difference from scanners that report on what you already installed. ## How it works [#how-it-works] Each firewall endpoint proxies one package ecosystem. A typical setup has one firewall per ecosystem, or several per ecosystem when teams need different policies. A firewall consists of: * **Upstreams.** The registries the firewall fetches from, such as `registry.npmjs.org` or a private registry. Upstreams can be marked internal to protect your own package names, and cached so repeat requests are served from the firewall's own copy. * **Rules.** Policy statements evaluated against every request. A rule combines an execution phase (version resolution, download, or upload), a selector that decides which packages it applies to, and an effect (block, log, or both). * **Exceptions.** Scoped waivers attached to a rule. When a rule blocks a package you need anyway, an exception allows that specific package through, with a documented reason and optional expiration. * **Logs.** Every rule match is recorded: who requested what, which rule fired, and whether the request was blocked. When a rule blocks, the firewall does not serve the package, and the package manager reports a failed install. What the client prints depends on the ecosystem and on which execution phase blocked the request. [Investigate a blocked install](/firewall/how-to/investigate-blocked-install) shows how to trace an error back to the rule that caused it. ## What you can enforce [#what-you-can-enforce] Rules can target package metadata, security signals, publish timing, and request context. Common policies include: * Delay newly published versions, so the ecosystem has time to catch malicious releases before your builds pick them up. See [Delay new versions](/firewall/how-to/delay-new-versions). * Block packages with known vulnerabilities above CVSS or EPSS thresholds. See [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages). * Block packages flagged as malware. * Block deprecated, yanked, or unlisted releases. * Block packages by license, with named license sets as deny or allow lists. See [Enforce license policy](/firewall/how-to/enforce-license-policy). * Block publishes of packages containing leaked credentials. See [Scan for secrets on publish](/firewall/how-to/scan-for-secrets). * Protect internal package names against dependency confusion by resolving them only from internal upstreams. ## Supported ecosystems [#supported-ecosystems] | Ecosystem | Clients | | --------- | -------------------------------- | | npm | npm, yarn, pnpm, Bun | | PyPI | pip, uv, Poetry | | Maven | Maven, Gradle | | NuGet | dotnet, NuGet.exe, Visual Studio | | Go | go modules via GOPROXY | | Cargo | cargo, Rust 1.74 or newer | | Composer | Composer 2 | | Conda | conda, mamba, micromamba | See [Ecosystems](/firewall/ecosystems) for setup instructions. ## Where it fits [#where-it-fits] * **Compared to SCA scanners.** A scanner tells you after the fact that a vulnerable package is in your tree. The firewall stops it from entering. The two answer different questions, and many teams run both: the firewall as the gate, a scanner for inventory and depth. * **Compared to repository managers.** Repository managers such as Artifactory and Nexus mirror and host packages. The firewall does not host packages of its own; it applies policy to requests as they pass through. It can sit in front of or beside a repository manager. See [Integrate with repository managers](/firewall/how-to/integrate-repository-managers). ## Next steps [#next-steps] Follow the [quickstart](/firewall/getting-started/quickstart) to set up your first firewall, or read [rule evaluation](/firewall/concepts/rule-evaluation) for the mental model behind rules, selectors, and exceptions. # Authentication (/firewall/platform/authentication) You sign in to Bytesafe once, with one user, and that user carries across the dashboard and every namespace your organization has. ## Open the web interface [#open-the-web-interface] Open the [Dependency Firewall dashboard](https://bsfw.bytesafecloud.eu/) to sign in. If you do not have an account, [start a 14-day trial](https://bsfw.bytesafecloud.eu/signup). ## Sign-in methods [#sign-in-methods] Every method is passwordless. Your identity comes from a code sent to your mailbox, an account you already hold at a provider, or a key held by your device. The screen asks for your email address first and lists the other methods under **Or continue with**. * **One-time password (OTP).** Type your email address and Bytesafe emails you a 6-digit code. Enter the code to finish signing in. A code stays valid for five minutes, and the screen sends a new one on request. * **Google, Microsoft, or GitHub.** Pick the provider, sign in there, and you come back to Bytesafe as the account you used. Only the providers your Bytesafe instance has configured appear on the screen. * **Passkey.** Confirm with the fingerprint, face, or PIN that unlocks the device or password manager holding the passkey. Browsers that support it offer a registered passkey straight from the email field. The first sign-in from an email address that has no Bytesafe user creates that user, so the **Sign up** link leads through the same screen and the same methods. ## Manage your sign-in methods [#manage-your-sign-in-methods] One user can have several methods at once, and any of them signs you in as the same person. Someone who started with a one-time password can register a passkey and use that from then on, or link a provider account and keep both. The account **Security** page holds three lists: the provider accounts linked to your user, the passkeys registered for it, and the sessions currently open. It is also where you end a session on a device you no longer have. Adding or removing a method leaves your user, your team memberships, and everything you own untouched. ## Single sign-on (SSO) [#single-sign-on-sso] An organization that runs its own identity provider (IdP) can connect it to Bytesafe over OIDC. People then sign in with the company account they already use, and access follows the joiner and leaver process the organization already runs: closing the account at the IdP closes the way in to Bytesafe. Connecting an IdP is arranged with us rather than configured in the dashboard, and availability depends on your subscription. Ask us through the chat in the lower right corner of this page, or email [support@bytesafe.dev](mailto:support@bytesafe.dev). ## Namespace access and permissions [#namespace-access-and-permissions] Signing in establishes who you are, and your organization and your teams decide the rest. Two people who sign in the same way can land on different namespaces, with different things editable in each. * **Your organization** owns the namespaces you can open. It is also the list the dashboard draws from when someone is added to a team. * **Your teams** inside a namespace carry the permissions. Membership of the **everybody** team makes you a member of the namespace, and further teams add what you can change. See [Manage access](/firewall/how-to/manage-access) and [Access and permissions](/firewall/concepts/access-and-permissions). When a namespace or a setting you expected is missing, the answer is in your teams and your organization rather than in the method you signed in with. ## Related [#related] * Concept: [Access and permissions](/firewall/concepts/access-and-permissions) * How-to: [Manage access](/firewall/how-to/manage-access) # Security & platform (/firewall/platform) This section describes the Bytesafe-operated SaaS around the firewall itself: who can get in and how. If you run the firewall [self-hosted](/firewall/self-hosted), you operate the infrastructure, so how people reach it is yours to define. See [Deployment models](/firewall/concepts/deployment-models) for what changes between the two, and email [support@bytesafe.dev](mailto:support@bytesafe.dev) to discuss requirements. ## In this section [#in-this-section] * [Authentication](/firewall/platform/authentication): sign-in methods, account security, and single sign-on with your own identity provider. To report a security issue, email [support@bytesafe.dev](mailto:support@bytesafe.dev). # Block deprecated packages (/firewall/how-to/block-deprecated-packages) When a publisher deprecates a version, they are telling you not to use it: it is broken, superseded, or unsafe. Package managers surface this as a warning at best, and automation ignores warnings. Two selector functions turn the publisher's signal into policy: `deprecated` and, for NuGet, `unlisted`. What counts as deprecated differs per ecosystem, and the selector maps to each registry's native signal: | Ecosystem | Signal matched by `deprecated` | | --------- | ------------------------------------------------------------------------- | | npm | Version has a deprecation message | | PyPI | Release is yanked (PEP 592) | | NuGet | Version has a deprecation entry | | Composer | Package is marked abandoned on Packagist, optionally naming a replacement | | Cargo | Crate version is yanked | Maven, Go, and Conda have no registry-level deprecation signal the firewall can read, so the selector is not available for those ecosystems. Composer marks the whole package rather than one version, so a rule using it blocks every version of an abandoned package. The full matrix is in the [selector functions reference](/firewall/references/selector-functions#matrix). ## Add the rule [#add-the-rule] In the dashboard, open the firewall, go to **Rules**, and select **Add Rule**. Choose **Hide packages or versions** on the intent step, then select **Continue**. * Execution phase: **Versions** (called `versions` in config, API, and CLI) * Selector function: **Deprecated** (no options) * Effect: **block** and **log** As JSON: ```json { "id": "block-deprecated", "executionPhase": "versions", "selector": { "function": { "name": "deprecated" } }, "ruleEffect": { "block": true, "log": true }, "description": "Filter versions deprecated by their publisher" } ``` To apply the JSON without the dashboard: export the config with `bsfw firewall export`, add the rule to the `rules` array, and import it back. See [firewall import](/firewall/references/cli#bsfw-firewall-import). On the **versions** phase the rule filters deprecated versions out of the metadata, so version ranges quietly resolve to a non-deprecated version. Add the same selector on **download** if you also want a hard stop for exact pins and lockfile URLs. ## NuGet: also block unlisted versions [#nuget-also-block-unlisted-versions] Unlisting is NuGet's soft removal: the version disappears from search but stays installable for anyone who pins it. Publishers unlist versions they no longer want consumed, including botched releases. A second rule closes that path: ```json { "id": "block-unlisted", "executionPhase": "versions", "selector": { "function": { "name": "unlisted" } }, "ruleEffect": { "block": true, "log": true }, "description": "Filter versions unlisted from search" } ``` ## Roll out [#roll-out] Deprecated versions are common in older dependency trees, so unlike a [malware rule](/firewall/how-to/block-malware), this one benefits from a dry run: deploy with `{ "block": false, "log": true }` first, watch the log for what would be blocked, and fix or except the legitimate stragglers before enabling block. The pattern is described in [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages#roll-out-with-log-first). ## What developers see [#what-developers-see] Ranges resolve past deprecated versions without anyone noticing; that is the point. An exact pin to a deprecated version fails as not found on the versions phase, or with the blocked-download error if you added the download rule. See your [ecosystem page](/firewall/ecosystems) for the exact client behavior. ## When a deprecated version is still needed [#when-a-deprecated-version-is-still-needed] A pinned build that cannot move yet: add an [exception](/firewall/how-to/manage-exceptions) scoped to that package and version with reason `no_upgrade_path` and an expiry, rather than weakening the rule. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Selector functions](/firewall/references/selector-functions#deprecated) * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions), [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages) # Block install-time code execution (/firewall/how-to/block-install-scripts) The `install-scripts` selector function matches releases that can execute package-author code during installation or build. It supports npm, PyPI, and Composer and has no options. For npm and PyPI, it inspects the package artifact in the **download** and **upload** phases. For Composer, it reads plugin metadata and supports **versions** and **download**. Many legitimate packages match these conditions, so review log-only results before you enable blocking. ## What matches [#what-matches] | Ecosystem | Matches | Does not match | | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | npm | Any of `preinstall`, `install`, `postinstall` declared in the package's `package.json` `scripts` | Other lifecycle scripts (`prepare`, `postpublish`, ...), scripts in dependencies of the package | | PyPI | A source distribution (`.tar.gz` or `.zip`) with a `setup.py` at the root, or a root `pyproject.toml` whose `[build-system]` declares `backend-path` (an in-tree PEP 517 build backend) | Wheels (`.whl`), which install without running package code; sdists using an external build backend such as `setuptools.build_meta` without `backend-path` | | Composer | A release with package type `composer-plugin` or the legacy `composer-installer` | Other package types, including the default `library` type | For PyPI, only files at the sdist root (or its single top-level directory) count; a `setup.py` under `tests/` does not match. ## Add the rule [#add-the-rule] The rule is not part of the default rule set; create it yourself. In the dashboard, open the firewall, go to **Rules**, and select **Add Rule**. Choose **Block packages from being downloaded** on the intent step, then select **Continue**. * Execution phase: **Download** for any supported ecosystem, or **Versions** for Composer when you want matching releases removed before resolution * Selector function: **Install-time code execution** (no options) * Effect: start with **log** only For npm or PyPI publishing, add the same rule on the **Upload** phase. Composer has no upload operation. As JSON: ```json { "id": "log-install-time-code-download", "executionPhase": "download", "selector": { "function": { "name": "install-scripts" } }, "ruleEffect": { "block": false, "log": true }, "description": "Log releases that can execute code at install time" } ``` To apply the JSON without the dashboard: export the config with `bsfw firewall export`, add the rule to the `rules` array, and import it back. See [firewall import](/firewall/references/cli#bsfw-firewall-import). Unlike [malware blocking](/firewall/how-to/block-malware), do not block on day one. Native modules, postinstall downloads, build hooks, and Composer plugins can all be legitimate. Run log-only, review the matches in [logs](/firewall/how-to/view-logs), add [exceptions](/firewall/how-to/manage-exceptions) for the packages you accept, then switch the effect to **block**. ## What a match looks like [#what-a-match-looks-like] The log entry's message names what was found. For npm: ```json { "message": "Install scripts detected: preinstall, postinstall", "scripts": ["preinstall", "postinstall"] } ``` For PyPI, the rule output also records the finding locations: ```json { "message": "PyPI source-build hooks detected: setup.py", "ecosystem": "pypi", "artifactType": "sdist", "findings": [ { "kind": "setup.py", "path": "demo-1.0.0/setup.py" } ] } ``` A `pyproject.toml backend-path` finding additionally carries the declared `backend` and `backendPath` values. For Composer, the rule output records the package type and any plugin classes declared in `extra.class`: ```json { "message": "Composer plugin detected: Composer\\Installers\\Plugin", "ecosystem": "composer", "packageType": "composer-plugin", "pluginClasses": ["Composer\\Installers\\Plugin"] } ``` ## Inspection source [#inspection-source] npm reads `package.json` from the package artifact. PyPI reads a root `setup.py` or `pyproject.toml` from an sdist. Composer reads `type` and `extra.class` from repository metadata, so it can match before the package file is downloaded. ## Verify [#verify] With the log-only rule in place, follow the install verification on the relevant [ecosystem page](/firewall/ecosystems), then inspect the matching entry in [Logs](/firewall/how-to/view-logs). npm entries begin with `Install scripts detected`, PyPI entries with `PyPI source-build hooks detected`, and Composer entries with `Composer plugin detected`. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Selector functions](/firewall/references/selector-functions#install-scripts) * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions), [Block malware](/firewall/how-to/block-malware), [Investigate a blocked install](/firewall/how-to/investigate-blocked-install) # Block malware (/firewall/how-to/block-malware) The `malware` selector function evaluates findings from malicious-package intelligence and package-content analysis. It runs where the artifact is available: the **download** phase for installs and the **upload** phase for publishes. A version-metadata rule cannot do this. Two options shape a rule: how weak a finding may be and still match ([confidence](#choose-the-confidence-level)), and how much the rule waits for before it decides ([effort](#choose-the-effort)). ## Add the rules [#add-the-rules] In the dashboard, open the firewall, go to **Rules**, and select **Add Rule**. Choose **Block packages from being downloaded** on the intent step, then select **Continue**. * Execution phase: **Download** * Selector function: **Malware** * Minimum confidence: **Known malware** * Effort: **Known findings only** * Effect: **block** and **log** If the firewall receives publishes, add the same rule on the **Upload** phase, so infected artifacts cannot enter your own registry through you. That rule is also the best candidate for a higher effort, see [Choose the effort](#choose-the-effort). As JSON: ```json [ { "id": "block-malware-download", "executionPhase": "download", "selector": { "function": { "name": "malware", "options": { "minimumConfidence": "known_malware", "effort": "known" } } }, "ruleEffect": { "block": true, "log": true }, "description": "Block malicious artifacts on download" }, { "id": "block-malware-upload", "executionPhase": "upload", "selector": { "function": { "name": "malware", "options": { "minimumConfidence": "known_malware", "effort": "known" } } }, "ruleEffect": { "block": true, "log": true }, "description": "Block malicious artifacts on publish" } ] ``` To apply the JSON without the dashboard: export the config with `bsfw firewall export`, add the rules to the `rules` array, and import it back. See [firewall import](/firewall/references/cli#bsfw-firewall-import). ## Choose the confidence level [#choose-the-confidence-level] `minimumConfidence` is the weakest finding that matches: | Config value | Dashboard label | Matches | | ---------------- | ---------------------- | ------------------------------------------------------------ | | `known_malware` | Known malware | Definitive findings only. Default when the option is omitted | | `suspicious` | Suspicious or stronger | Suspicious and known-malware findings | | `weak_indicator` | Any indicator | Every finding, including weak indicators | Start with **Known malware** and a blocking effect. If you lower the threshold, use a log-only rule first to review the additional matches before enabling block. ## Choose the effort [#choose-the-effort] `effort` is how much the rule waits for before it decides: | Config value | Dashboard label | The rule decides on | Effect on the request | | ------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | | `known` | Known findings only | Findings that already exist for the artifact: malicious-package intelligence, plus content analysis that has already been done. Default when the option is omitted | Nothing to wait for | | `deep` | Wait for deep analysis | The same, and it has the artifact's contents analyzed as part of the request, waiting for that result | Slower. The first request for an artifact nobody has analyzed yet waits for the whole analysis | Effort and confidence are independent. Effort decides how much is known when the rule runs, confidence decides how weak a finding may be and still match. `known` is the default, so a rule created without a thought about effort does not hold up installs. `deep` is the deliberate choice, and worth sizing before you make it: that first analysis can take minutes on a large package. Requests that follow are faster, because the result exists by then. Publishes are usually the better place to spend that time than installs. A publish is one request by one person who is already waiting for a pipeline, while an install is every developer and every CI job. Set the upload rule to **Wait for deep analysis** and leave the download rule on **Known findings only**, and only take the download rule to `deep` for a firewall where a slow install is acceptable. ```json { "id": "block-malware-upload", "executionPhase": "upload", "selector": { "function": { "name": "malware", "options": { "minimumConfidence": "known_malware", "effort": "deep" } } }, "ruleEffect": { "block": true, "log": true }, "description": "Block malicious artifacts on publish, after full analysis" } ``` ## How a verdict is reached [#how-a-verdict-is-reached] A rule does not compute a verdict itself. It matches against **findings** already associated with the artifact, and a finding is one assessment of one package version. Findings come from two kinds of input: * **Malicious-package intelligence.** Assessments published about specific package versions, from threat-intelligence datasets that track malicious packages across the public ecosystems. * **Package-content analysis.** Inspection of the artifact itself, which is why the rule only runs where the bytes exist: the download phase for installs, the upload phase for publishes. Each finding carries three things the rule and the log entry expose: * `scanner` names the detection source, so you can tell which input produced the finding. * `confidence` is how strong the evidence is, not how dangerous the package would be. A definitive signature match and a weak heuristic hit can describe the same behavior with very different certainty, and `minimumConfidence` is where you decide how much certainty you require before blocking. * `signals` is the evidence behind the finding: a matched signature, a behavior identified in the package contents, or a reference to a published advisory. The two options interact here. **Effort** decides how much is known at the moment the rule runs: `known` matches against findings that already exist, while `deep` has the artifact analyzed as part of the request and waits for the result. **Confidence** then decides which of those findings are strong enough to act on. A verdict is a judgment about evidence, so it can be wrong in both directions. If you think one is, see [If you believe it is a false positive](#if-you-believe-it-is-a-false-positive). ## What a match looks like [#what-a-match-looks-like] The install fails as a blocked download (see your [ecosystem page](/firewall/ecosystems) for the exact client error). The log entry's message identifies the package and finding summary: ```text Malware detected in /@: ``` The rule output carries: | Field | Meaning | | ------------ | ------------------------------------------------------------------- | | `malware` | Finding summary | | `scanner` | Detection source | | `confidence` | `known_malware`, `suspicious`, or `weak_indicator` | | `signals` | Evidence for the finding, such as a signature or advisory reference | ## If you believe it is a false positive [#if-you-believe-it-is-a-false-positive] Do not disable the rule. Add an [exception](/firewall/how-to/manage-exceptions) scoped to the exact package and version, with a short expiry and a reason, and report the package to [support@bytesafe.dev](mailto:support@bytesafe.dev) so the verdict can be reviewed. An exception on the malware rule does not exempt the package from your other rules, and it stops applying when it expires, so the block returns by itself if the verdict turns out to be correct. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Selector functions](/firewall/references/selector-functions#malware) * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions), [Investigate a blocked install](/firewall/how-to/investigate-blocked-install) # Block packages by name, version, or source (/firewall/how-to/block-packages-by-name-version-or-source) Use plain selectors when the policy is about a package's identity or source, rather than a computed property such as vulnerabilities or malware. A rule can match one package, a group of names, one version, a version range, one upstream, or internal or external packages. ## Add a rule [#add-a-rule] In the dashboard, open the firewall, go to **Rules**, and select **Add Rule**. Choose **Start from a blank rule** on the intent step, then select **Continue**. Choose the execution phase, set one or more selector fields, then enable **block** and **log**. This rule blocks one package in the versions phase: ```json { "id": "block-example-package", "executionPhase": "versions", "selector": { "packageName": "example-package" }, "ruleEffect": { "block": true, "log": true }, "description": "Block example-package" } ``` Use **Versions** (`versions` in config, API, and CLI) when versions should disappear during dependency resolution. Use **Download** when the client should see an explicit block while fetching an artifact. Use **Upload** for rules that apply when a package is published through the firewall. See [Execution phases](/firewall/concepts/execution-phases) for the request flow. ## Choose selector fields [#choose-selector-fields] | Field | Use it to match | Example | | ------------- | -------------------------------------------------------------- | ---------------------- | | Package | One package or a prefix or suffix wildcard | `@acme/*` | | Version | One version string or a prefix or suffix wildcard | `1.2.3`, `1.*` | | Version range | Versions in the ecosystem's range notation | `>=1.0.0 <2.0.0` | | Upstream | Packages served from one configured upstream | `` | | Upstream type | Packages resolved from an upstream marked internal or external | `internal`, `external` | Package and version wildcards accept one `*`, at the start or end. Version ranges use each ecosystem's own notation and are not available for Conda. The [rules and selectors reference](/firewall/references/rules-and-selectors#selector) has the exact matching and range syntax. ## Combine conditions [#combine-conditions] All set selector fields must match. This rule blocks versions below 2.0.0 only when the named package comes from an external upstream: ```json { "id": "block-old-external-example", "executionPhase": "versions", "selector": { "packageName": "example-package", "versionRange": "<2.0.0", "upstreamType": "external" }, "ruleEffect": { "block": true, "log": true }, "description": "Block external example-package versions below 2.0.0" } ``` Leaving a field empty makes it unrestricted. A rule with no selector fields matches every package in its execution phase. ## Test with log first [#test-with-log-first] For a broad name wildcard or version range, start with **log** on and **block** off. Matching requests continue, while the logs show what the rule would affect. After reviewing the matches, enable **block** without changing the selector. ## Verify [#verify] Request a package that matches the selector, then inspect the logs: ```shell bsfw logs --firewall ``` Confirm the entry contains the expected rule ID, package, version, and upstream. For a client-side request, use the install or publish example on the relevant [ecosystem page](/firewall/ecosystems). ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation), [Execution phases](/firewall/concepts/execution-phases) * Reference: [Rules and selectors](/firewall/references/rules-and-selectors) * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions), [View logs](/firewall/how-to/view-logs) # Block trust downgrades (/firewall/how-to/block-trust-downgrades) A package can establish a trusted publishing process and then release a later version through a weaker path. That change can be legitimate, but it can also indicate that a maintainer account or publishing token was used instead of the established release workflow. The `trust-downgrade` selector lets you review or block that regression. The selector currently supports npm. The rule name is ecosystem-neutral so the same policy can cover other ecosystems if their registries expose comparable trust evidence in the future. ## How matching works [#how-matching-works] For the selected release, the firewall compares its publishing trust evidence with releases of the same package published earlier. Trust is ordered from strongest to weakest: 1. Trusted publisher 2. Provenance attestation 3. No trust evidence The selector matches when any earlier release has stronger evidence. For example: | Earlier release | Selected release | Match | | ---------------------- | ---------------------- | ----- | | Trusted publisher | Provenance attestation | yes | | Provenance attestation | No trust evidence | yes | | Provenance attestation | Provenance attestation | no | | No trust evidence | Provenance attestation | no | Comparison uses publication date, not version number. Prereleases do not establish the trust baseline for a stable release. A match identifies a change in publishing evidence; it does not by itself prove that the release is malicious. ## Add the rule [#add-the-rule] On an npm firewall, open **Rules** and select **Add Rule**. Choose **Hide packages or versions** on the intent step, then select **Continue**. * Execution phase: **Versions** * Selector function: **Trust downgrade** * Effect: **block** and **log** As JSON: ```json { "id": "block-trust-downgrades", "executionPhase": "versions", "selector": { "function": { "name": "trust-downgrade" } }, "ruleEffect": { "block": true, "log": true }, "description": "Block releases with weaker publishing trust" } ``` A versions-phase match removes the release during resolution. The same rule is also checked when a client fetches that release directly, so an exact lockfile URL cannot bypass it. ## Test with log first [#test-with-log-first] If you need to assess existing packages before blocking, start with **log** on and **block** off. Review matches for planned changes to publishing workflows. Enable **block** after you have accounted for legitimate changes. ## Verify [#verify] Request metadata or an artifact for a package with a downgraded release, then inspect the logs: ```shell bsfw logs --firewall ``` Confirm the entry contains the rule ID, package, and version. If a publishing change is expected, add an [exception](/firewall/how-to/manage-exceptions) scoped to that package and version rather than disabling the rule. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Selector functions](/firewall/references/selector-functions#trust-downgrade), [Rules and selectors](/firewall/references/rules-and-selectors) * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions), [View logs](/firewall/how-to/view-logs) # Block vulnerable packages (/firewall/how-to/block-vulnerable-packages) The `vulnerabilities` selector function checks each requested package version against known vulnerability data and matches when a vulnerability exceeds your thresholds. With a block effect, the download is stopped before the package reaches the developer or build. ## Add the rule [#add-the-rule] In the dashboard, open the firewall, go to **Rules**, and select **Add Rule**. Choose **Block packages from being downloaded** on the intent step, then select **Continue**. * Execution phase: **Download** * Selector function: **vulnerabilities** * Max CVSS score: for example **9.0** * Max EPSS score: for example **0.5** * Effect: **block** and **log** As JSON: ```json { "executionPhase": "download", "selector": { "function": { "name": "vulnerabilities", "options": { "maxCvssScore": 9.0, "maxEpssScore": 0.5 } } }, "ruleEffect": { "block": true, "log": true }, "description": "Block critical vulns with high exploitation probability" } ``` To apply the JSON without the dashboard, use the export, edit, import round trip described in [firewall import](/firewall/references/cli#bsfw-firewall-import). ## How the thresholds work [#how-the-thresholds-work] The rule matches when a vulnerability exceeds **both** thresholds: * **CVSS** measures severity, 0 to 10. * **EPSS** estimates the probability the vulnerability is exploited in the wild, 0 to 1. The fields are named "max" because they state the highest score you are willing to accept. A vulnerability has to come in above both numbers to match, and the comparison is strict: with the thresholds above, a vulnerability scoring exactly 9.0 does not match. Requiring both keeps the false positive rate down. A CVSS 9.8 finding with an EPSS of 0.01 is severe on paper but unlikely to be exploited; a CVSS-only policy would block it and generate exception churn. With the example above, only vulnerabilities that are critical and likely to be exploited are blocked. Where the advisory data and scores come from is in [Advisory sources](/firewall/references/advisory-sources), including how a score is chosen when vendors disagree and what happens when a vulnerability carries no score at all. ## When a score changes [#when-a-score-changes] Scores move after publication. A CVE can start at 7.5 and be raised to 10 once analysis completes, a critical can be downgraded, and an advisory can be withdrawn. The firewall does not store verdicts; it evaluates each download request against its current advisory data, so a decision is never sticky: * A score lowered below your thresholds means the package passes on the next request. There is nothing to reset, and an exception created during the block is simply no longer consulted. * A score raised above your thresholds means the next request is blocked, even if the same version installed fine yesterday. When a changed score becomes visible to a firewall, and which caches can serve a version without a new decision, is covered under [Freshness and score changes](/firewall/references/advisory-sources#freshness-and-score-changes). ## Roll out with log first [#roll-out-with-log-first] On an existing codebase, a new blocking rule can break builds the same day. Start with log only: ```json "ruleEffect": { "block": false, "log": true } ``` Watch the logs for a week, see what would have been blocked, handle the real cases, then switch on `block`. Nothing is enforced in the meantime: a log-only rule records the match and serves the package. ## Scope the rule if needed [#scope-the-rule-if-needed] The selector function combines with the other selector fields. To apply the rule only to external packages, leaving your internal packages to their own process: ```json "selector": { "upstreamType": "external", "function": { "name": "vulnerabilities", "options": { "maxCvssScore": 9.0, "maxEpssScore": 0.5 } } } ``` ## When a needed package is blocked [#when-a-needed-package-is-blocked] The blocked install error contains a request ID; the matching log entry shows the vulnerability details behind the decision. If you decide to accept the risk, create an [exception](/firewall/how-to/manage-exceptions) with a documented reason such as `not_exploitable` and an expiry, rather than loosening the thresholds for everyone. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Rules and selectors](/firewall/references/rules-and-selectors) * How-to: [Investigate a blocked install](/firewall/how-to/investigate-blocked-install) # Configure upstreams (/firewall/how-to/configure-upstreams) Upstreams are the registries a firewall fetches packages from. A new firewall typically starts with the ecosystem's public registry; this guide covers adding more, wiring in private registries, and choosing where publishes go. ## Add an upstream [#add-an-upstream] In the dashboard, open the firewall, go to **Upstreams**, and add an upstream: * **Endpoint**: the registry URL, for example `https://registry.npmjs.org` * **Internal registry**: on if this is your private registry (see below) * **Cache upstream**: on to let the firewall cache responses from this upstream. Available only while **Internal registry** is on; the switch is disabled otherwise * **Credentials**: username/password or a token, if the registry requires authentication As JSON, an upstream inside the firewall config: ```json { "id": "npmjs", "endpoint": "https://registry.npmjs.org", "credentials": {}, "internal": false, "cached": false } ``` The `cached` flag applies to internal upstreams: set `cached: true` on the registries you own, so repeat requests are served from the firewall's copy instead of the registry. An external upstream with `cached: true` is rejected with `400`, in the dashboard and on import alike. The flag is not the whole caching story; [Upstreams and caching](/firewall/concepts/upstreams-and-caching) describes what the firewall keeps in either case. To apply JSON without the dashboard: export the config with `bsfw firewall export`, edit the `upstreams` array, and import it back. See [firewall import](/firewall/references/cli#bsfw-firewall-import). Imports must not contain plaintext credentials. For an existing upstream, keep its exported placeholders to preserve the stored values. A new upstream has no stored value to preserve, so set its credentials separately. ## How multiple upstreams behave [#how-multiple-upstreams-behave] The firewall queries all upstreams for package metadata and merges the results, with one exception: when an upstream marked **internal** knows a package, only internal upstreams' versions are served for it. External copies of that package name disappear. That is the [dependency confusion](/firewall/how-to/prevent-dependency-confusion) defense. Rules can also target specific upstreams with the `upstreamId` and `upstreamType` selector fields; see [Rules and selectors](/firewall/references/rules-and-selectors). ## Private registries and credentials [#private-registries-and-credentials] For a registry that requires authentication, set the upstream's credentials. They are encrypted at rest, and API responses never return them; edits in the dashboard show a placeholder that means "keep the stored value". Mark your private registry **internal** if it hosts your own packages. Internal here is a statement about trust and package ownership, not network location. ## Choose a publish target [#choose-a-publish-target] If developers publish through the firewall, set `publishTarget` to the ID of the upstream that should receive published packages: ```json { "id": "3e7f5cb806f26e3beae2", "ecosystem": "npm", "upstreams": [ { "id": "npmjs", "endpoint": "https://registry.npmjs.org", "internal": false, "cached": false }, { "id": "e2de947a", "endpoint": "https://npm.acme.internal", "internal": true, "cached": true } ], "publishTarget": "e2de947a" } ``` Publishes pass the firewall's **upload** rules first, then are forwarded to the publish target with that upstream's credentials. ## Verify [#verify] Install a package through a rule with logging enabled, then open **Logs**. The table is a summary and does not have an upstream column. Select the row and confirm its detail shows the expected `upstream` value. ```shell bsfw logs --firewall ``` For client-side install commands, see your [ecosystem page](/firewall/ecosystems). ## Troubleshoot [#troubleshoot] If the internal upstream claims a package name, external versions of that name are no longer served. If the internal registry has the name but not the versions you need, that is working as designed; publish the versions internally or unmark the upstream as internal. Caching applies to internal upstreams. Turn on **Internal registry** to enable the switch; turning it back off clears the cache setting. On import, an external upstream with `cached: true` is rejected with `upstream : caching is only allowed on internal upstreams`. When an upstream rejects the firewall's credentials, the firewall answers with what the other upstreams returned. If that upstream was the only one holding the package, the request fails anyway, with an error that names the package and never the credential. The firewall log is where the rejection shows up. Set **Status** to **Error** on the Logs page, or run `bsfw logs --all-users --kind error --since 24h`, and look for an entry naming that upstream. A rejected credential is recorded with status `401`. See [Check for failing upstreams](/firewall/how-to/view-logs#check-for-failing-upstreams). `bsfw firewall import` refuses plaintext credentials in the config file. Keep exported placeholder values as-is in config files under version control. They preserve credentials already stored for the same firewall and upstream; for a new firewall or upstream, set credentials separately after import. ## Related [#related] * Concept: [Upstreams and caching](/firewall/concepts/upstreams-and-caching) * Reference: [Upstreams](/firewall/references/upstreams), [Firewall config](/firewall/references/firewall-config) * How-to: [Prevent dependency confusion](/firewall/how-to/prevent-dependency-confusion) # Delay new versions (/firewall/how-to/delay-new-versions) A compromised release is usually reported and pulled some time after it is published, but automated dependency updates and loose version ranges can pull it into a build minutes after it goes live. A delay rule closes that window: versions younger than a set age are filtered out, so your builds only see releases that have been public long enough for problems to surface. It is a good first rule. It needs no vulnerability data, no tuning, and it applies to every package equally. ## Add the rule [#add-the-rule] In the dashboard, open the firewall, go to **Rules**, and select **Add Rule**. Choose **Let new releases mature** on the intent step, then select **Continue**. It prefills a 7-day max-age value; the rule form keeps it editable. * Execution phase: **Versions** (called `versions` in config, API, and CLI) * Selector, max age: **7 days** (presets range from 1 to 90 days; a custom value is also possible) * Effect: **block** and **log** As JSON: ```json { "id": "delay-new-versions", "executionPhase": "versions", "selector": { "maxAgeHours": 168 }, "ruleEffect": { "block": true, "log": true }, "description": "Block versions younger than 7 days" } ``` `maxAgeHours` is a whole number of hours. 168 is 7 days. The selector matches versions whose publish date is more recent than the max age, and the block effect filters them. To apply the JSON without the dashboard: export the config with `bsfw firewall export`, add the rule to the `rules` array, and import it back. See [firewall import](/firewall/references/cli#bsfw-firewall-import). ## Why the versions phase [#why-the-versions-phase] A delay rule belongs in the **versions** phase (**Versions** in the dashboard): clients resolve against the filtered version list and quietly land on an older version. The same max-age selector in the **download** phase leaves the metadata untouched, so the client resolves to a version inside the window and then fails to fetch it. An install of an actively maintained package fails outright that way, because some dependency deep in the tree is usually younger than the window. Use the versions phase to delay, the download phase for hard gates. [Execution phases](/firewall/concepts/execution-phases) covers the difference in depth. ## Choose a window [#choose-a-window] * **7 days** is a reasonable default. Long enough that a bad release has time to be reported, short enough that ordinary updates are not held up much. * **14 to 30 days** suits stability-focused teams that update on a monthly cadence anyway. * **1 day** still blocks the worst case, an install seconds after a hostile publish, at almost no cost. ## What developers see [#what-developers-see] Version ranges, which is what dependency declarations normally are, keep working and resolve to the newest version older than the window. Nothing changes for lockfile versions older than the window, which is the normal case. What a request inside the window gets depends on its shape: * An exact pin of a filtered version fails as not found, and so does a named tag or label that points at one. * An install without a stated version generally resolves to the newest remaining version. * A client that skips resolution and fetches the artifact directly, such as a lockfile install of a version inside the window, gets the blocked-download error with a request ID. The exact behavior and error messages differ per package manager; each [ecosystem page](/firewall/ecosystems) documents them under "What a blocked install looks like". ## Verify [#verify] Install a package version published within your window, then confirm the rule fired: ```shell bsfw logs --firewall ``` The log entry shows the rule match and the filtered version. For the client-side check with your package manager, see your [ecosystem page](/firewall/ecosystems). ## Let vulnerability fixes through [#let-vulnerability-fixes-through] The **Vulnerability Fixes** exception condition lets versions published as a fix for a known vulnerability skip this delay. It is available for npm, Go, Maven, PyPI, NuGet, Composer, and Cargo delay rules. Create an exception for the delay rule, select **Vulnerability Fixes** under **Condition**, and keep **Log when exception matches** on. As JSON: ```json { "id": "allow-vulnerability-fixes", "rule": "delay-new-versions", "selector": { "function": { "name": "vulnerability-fix" } }, "log": true, "reason": "other", "notes": "Allow versions published as vulnerability fixes through the delay rule." } ``` The condition uses advisory data and matches the version identified as a published fix. It does not waive the package from other rules. See [`vulnerability-fix`](/firewall/references/selector-functions#vulnerability-fix) for its exact behavior. ## When you need another fresh version [#when-you-need-another-fresh-version] For a release you have verified yourself that is not identified as a vulnerability fix, do not widen the rule. Add an [exception](/firewall/how-to/manage-exceptions) scoped to that package and version, with an expiry. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Rules and selectors](/firewall/references/rules-and-selectors) * Ecosystem behavior: [Ecosystems](/firewall/ecosystems) # Enforce firewall use (/firewall/how-to/enforce-firewall-use) A firewall only tells you what your builds pulled if your builds cannot pull from anywhere else. A developer with a stale machine-level config, a CI job with a hardcoded registry URL, or a lockfile written before the migration all resolve straight from the public registry, and the firewall never sees it. There is no single setting that closes this. Organizations reduce the risk in different ways, depending on how much control they have over developer machines, build agents, and the network. This page covers two that work well together and cover most setups. Treat them as a starting point rather than a complete list. Some organizations also block the main public registry hosts at the network level, so a misconfigured client fails outright instead of quietly resolving around the firewall. That works for a handful of hosts, but scales poorly once mirrors, vendor feeds, and private hosting enter the picture. ### Commit the client configuration to the repository [#commit-the-client-configuration-to-the-repository] Client configuration committed to the repository travels with every clone and every CI checkout. Machine-local configuration does not, and it is the usual reason one developer's build behaves differently from everyone else's. For each ecosystem you use, commit the registry or index configuration at the repository root. The [ecosystem pages](/firewall/ecosystems) show the file and its contents per client. Commit the configuration, never the token. Every client can read its credential from an environment variable, so the committed file names the variable and the [access token](/firewall/getting-started/access-tokens) stays in each developer's shell and in your CI secret store. Two things decide whether that configuration actually covers you. **The firewall has to be the only source** Package managers can be configured with several package sources at once, and will fall back between them or query them in parallel. If the public registry is still configured next to the firewall, some installs will be answered by it, and those installs never reach your rules. Nothing appears broken, which is what makes this easy to miss. Every ecosystem has a way of saying *this is the only source*: replacing the index rather than adding a second one, clearing inherited sources before adding yours, or defining a mirror that covers every repository. That is the setting you want, and the [ecosystem pages](/firewall/ecosystems) name it for each client. **Partial routing leaves the rest unchecked** Some clients can send only part of your dependencies to the firewall, typically the packages under one namespace or prefix, and leave everything else going directly to the public registry. That is occasionally deliberate, but it means only that slice is checked. The packages most likely to carry a supply-chain problem are the ones nobody chose by name, and those are exactly the ones outside the slice. Route the whole ecosystem through the firewall unless you have a specific reason not to. If you do scope it, write down what is excluded, because the exclusion will not be visible in the firewall's own logs. ### Confirm the firewall is in the path [#confirm-the-firewall-is-in-the-path] An empty firewall log is not evidence that everything is going through the firewall. It is equally consistent with nothing going through it, because a request that matches no rule is not logged. **Log every request while you check.** Add a rule matching all packages with `{ "block": false, "log": true }`, run a build, and confirm entries appear: ```json { "id": "log-all-traffic", "executionPhase": "download", "selector": {}, "ruleEffect": { "block": false, "log": true }, "description": "Record every artifact request" } ``` **Depend on a package only the firewall can serve.** Some organizations publish a small marker package to an [internal upstream](/firewall/how-to/prevent-dependency-confusion) and depend on it from their projects. Because the name exists nowhere else, a build that resolves it must have gone through the firewall, and a build that cannot find it is pointed somewhere else. That turns a question about configuration into a build that either passes or fails, which CI can answer on every run. **Read the records you already have** * [Observations](/firewall/references/observations) show which package versions the firewall actually served over a period. A dependency your team clearly uses that never appears is worth chasing. * Lockfiles record where each artifact came from. Entries pointing at a public registry host mean that install resolved around the firewall, and they keep doing so until the lockfile is regenerated. * [Metrics](/firewall/references/metrics) show request volume per firewall. A number that does not move while a team is building is the clearest signal of all. Once the log-everything rule has served its purpose, decide whether to keep it. It records every request, which is the point, and it costs a log entry per request. ## Internal packages are unaffected [#internal-packages-are-unaffected] Making the firewall the only source does not cut off your own registry: point the firewall at it as an [internal upstream](/firewall/how-to/prevent-dependency-confusion), and internal names resolve from it in preference to any public package of the same name. Clients keep one source, and you keep the guarantee. ## Related [#related] * Concept: [Prevention model](/firewall/concepts/prevention-model), [Observability](/firewall/concepts/observability) * How-to: [Prevent dependency confusion](/firewall/how-to/prevent-dependency-confusion), [Integrate with repository managers](/firewall/how-to/integrate-repository-managers), [View logs](/firewall/how-to/view-logs) * Reference: [Client operations](/firewall/references/client-operations), [Observations](/firewall/references/observations) # Enforce license policy (/firewall/how-to/enforce-license-policy) A license policy has two parts: a **license set**, a named list of SPDX license IDs owned by the namespace, and a firewall **rule** with the `license` selector function that matches downloads against that set. Sets are shared across firewalls, so one "Copyleft" list can back rules in every ecosystem. The `license` function runs in the **download** execution phase: the artifact download is blocked, versions keep appearing in metadata. ## Create or edit a license set [#create-or-edit-a-license-set] In the dashboard, open **Licenses** in the sidebar. Every namespace starts with a seeded **Copyleft** set (GPL, AGPL, LGPL variants, SSPL-1.0, EUPL-1.2, CC-BY-SA-4.0) that you can use as-is, edit, or copy from. Sets list SPDX license IDs (`GPL-3.0-only`, `MIT`, ...); the editor autocompletes from the SPDX catalog and accepts free text. Matching is case-insensitive. Changing sets requires the `ns:update:licenses` [permission](/firewall/references/permissions); via the API it is the `/license-sets` [endpoints](/firewall/references/api#license-sets). ## Add the rule [#add-the-rule] Open **Rules**, select **Add Rule**, choose **Block packages from being downloaded** on the intent step, and select **Continue**. In the rule form, choose the **License** selector function. The **Match** select offers the three modes, with a **License Set** picker for the first two: * **Block licenses in set**: deny list. Matches when any of the version's licenses is in the set. * **Block licenses NOT in set**: allow list. Matches when licenses were resolved but none is in the set. * **Block unknown licenses**: matches when no license information could be resolved at all. As JSON, a deny rule against the Copyleft set (reference the set by its ID; the seeded set's ID is generated per namespace, visible on the Licenses page): ```json { "id": "block-copyleft", "executionPhase": "download", "selector": { "function": { "name": "license", "options": { "licenseSet": "", "mode": "deny" } } }, "ruleEffect": { "block": true, "log": true }, "description": "Block copyleft-licensed packages" } ``` An allow-list rule uses `"mode": "allow"` with a set of approved licenses. The unknown rule uses `{ "unknown": true }` and no `licenseSet`; a selector must have exactly one of the two. A strict policy is a pair of rules: an allow rule for the approved set, plus an unknown rule, so a package passes only with a positively approved license. Roll out like any other policy: `log` without `block` first, watch the log, then enable blocking. ## Where license data comes from [#where-license-data-comes-from] The firewall resolves a version's licenses from a public license data source first, falling back to the license declared in the package's own registry metadata (npm, NuGet, and Composer; PyPI, Maven, Go, and Cargo have no metadata fallback). Results are cached. Composer has no public license data source at all, so a Composer version resolves only from what Packagist metadata declares, and a package that declares nothing resolves as unknown. Conda has no license data source: Conda versions resolve as unknown license, so only the **Block unknown licenses** mode has an effect there. See [Conda selector function support](/firewall/ecosystems/conda#selector-function-support). License expressions are flattened: every license ID mentioned in an expression counts, without evaluating `OR`/`AND`. A package declaring `MIT OR GPL-3.0-only` matches a deny set containing `GPL-3.0-only`, even though an MIT choice exists. Use an [exception](/firewall/how-to/manage-exceptions) for dual-licensed packages you accept. ## Verify [#verify] Add the rule log-only, install a package with a matching license through the firewall, and check the log entry: its output carries the resolved `licenses`, the `licenseSet`, and the `source` the licenses came from. Then enable `block` and confirm the install fails. Client-side verification steps per ecosystem are on the [ecosystem pages](/firewall/ecosystems). ## Troubleshoot [#troubleshoot] Expected: expressions are not evaluated, any mentioned license counts. If the package's license choice is acceptable, add an exception on the rule for that package. The unknown rule matched: the license data source has no data for the version and the registry metadata declares no license (or the ecosystem has no metadata fallback). Verify the package's licensing yourself, then add an exception for it. A set referenced by any firewall rule cannot be deleted; the API answers 409 and names the firewall. Remove or repoint the rules first. The rule references a license set ID that does not exist in the namespace. Pick the set from the rule form, or copy the ID from the Licenses page; the set must be created before the rule. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions), [Investigate a blocked install](/firewall/how-to/investigate-blocked-install) * Reference: [Selector functions](/firewall/references/selector-functions#license), [API: license sets](/firewall/references/api#license-sets), [Permissions](/firewall/references/permissions) # All how-to guides (/firewall/how-to) Task-oriented guides for working with an existing firewall. Each one states a goal, walks the steps, and tells you how to verify the result. ## Rules [#rules] ## Upstreams and config [#upstreams-and-config] ## Observability [#observability] ## Access [#access] ## Migration [#migration] # Integrate with repository managers (/firewall/how-to/integrate-repository-managers) The Dependency Firewall speaks the standard registry protocols on both sides. To a repository manager it looks like a registry; to a public registry it looks like an ordinary client, no different from npm, mvn, pip, dotnet, go, or conda. That makes it compatible with any repository manager that does not lock you into its own ecosystem: if the manager lets you configure your own upstream registry URLs, it can fetch through the firewall, and any manager can serve as a package source behind the firewall. This page covers the three integration methods and how the major repository managers fit each one. ## Integration methods [#integration-methods] ### Manager as a firewall upstream [#manager-as-a-firewall-upstream] The firewall fronts developers and CI: they point at the firewall, and you add the **repository manager as a firewall upstream**, typically via its virtual, group, or feed URL. It sits alongside the public registry upstreams the firewall already resolves for that ecosystem, so internal packages and the manager's cache stay available behind firewall policy. Firewall rules are enforced either way; the two methods differ in who fronts the clients and therefore where identity and caching live. Choose this when you want firewall policy, logs, and per-developer identity on every install, while keeping the repository manager as a package source. Client setup is the standard [ecosystem setup](/firewall/ecosystems); the repository manager side is one more upstream entry, resolved together with the firewall's public registry upstreams on each request ([upstream resolution](/firewall/concepts/upstreams-and-caching)): ```json { "id": "e2de947a", "endpoint": "https://acme.jfrog.io/artifactory/api/npm/npm-virtual", "credentials": { "token": "" }, "internal": true, "cached": false } ``` Mark the upstream **internal** if the manager hosts your own packages; that activates the [dependency confusion](/firewall/how-to/prevent-dependency-confusion) defense for those names. The firewall authenticates upstream fetches with whichever credential fields the upstream has: a `token` is sent as `Authorization: Bearer`, and `username`/`password` are sent as basic auth when no token is set (Azure Artifacts endpoints additionally receive a token as basic auth, which is what Azure expects). That covers both auth styles repository managers use for reads: bearer-token registries get the token, and managers that authenticate reads with basic auth, such as Nexus user tokens or GitHub Packages PATs, get their credential as username and password. ### Firewall as the manager's upstream [#firewall-as-the-managers-upstream] The repository manager fronts developers and CI: they keep their existing repository manager URL, and the **firewall is the manager's upstream**. The manager's remote (proxy) repository points at the firewall instead of the public registry, so everything the manager pulls from outside passes firewall policy first. Choose this when the repository manager is entrenched and you cannot change client configuration. Two tradeoffs to accept: * **Identity is aggregated.** Every request reaches the firewall as the repository manager's service account, so firewall logs show one user for all traffic. Per-developer attribution stays in the repository manager's own logs. * **The manager's cache answers before the firewall.** Once the manager has cached a package, repeat installs are served from that cache without a firewall check, so new or tightened rules do not retroactively apply to already-cached packages. Keep proxy cache retention short for metadata, or accept that policy applies at first fetch. On the firewall side the repository manager is just another client. Point the remote repository at the [ecosystem endpoint](/firewall/ecosystems), for example `https://eu-sov-1.bytesafecloud.eu/v1//npm//`, and authenticate with a [Service Access Token](/firewall/getting-started/access-tokens). The firewall accepts the token as basic auth (any username, token as the password) or as a bearer header, which covers how repository managers send remote-repository credentials. ### Firewall on both sides [#firewall-on-both-sides] An advanced setup with two separate firewalls: * a **client-facing firewall** between developers and the repository manager, enforcing install policy, identity, and logging, * an **egress firewall** between the repository manager and public registries, controlling what may enter from outside. Rules of the road: * Use **two firewall configurations**, one per role. The client-facing firewall enforces install policy (delays, vulnerability thresholds, exceptions); the egress firewall enforces outbound policy (which registries, which packages may enter the building at all). * **Avoid loops.** The egress firewall's upstream is the public registry, never the repository manager. The client-facing firewall's upstream is the repository manager. Every hop points strictly toward the public registry. * The repository manager typically needs separate repositories for internal packages (hosted) and external packages (remote via the egress firewall), aggregated by a virtual or group repository. This method is the first two running at once, so it works wherever both do. ## Repository managers [#repository-managers] How each manager fits the integration methods, per ecosystem, with the vendor's own documentation linked. Where a method does not work, the constraint is the named product's, stated in the notes. Two HTTP authentication styles come up in every section: * **Basic auth**: the client sends a username and password with each request. * **Bearer auth**: the client sends a token in the `Authorization: Bearer` header. The firewall speaks both on both sides. It accepts either style when it receives requests (firewall as upstream). When it fetches from an upstream (manager as upstream), the upstream's credential fields decide the style: a `token` is sent as a bearer header, a username and password as basic auth. So the manager-as-upstream question is only which credential the vendor gives you, not whether the auth styles line up. The "yes" cells below follow the vendor's documented auth combined with this fetch behavior; not every combination has been validated against a live vendor instance. ### JFrog Artifactory [#jfrog-artifactory] Artifactory is JFrog's repository manager, self-hosted or cloud. A **remote repository** proxies an external registry and caches what it fetches; a **virtual repository** bundles several repositories behind one client URL. Remote repositories accept any upstream URL, and [access tokens](https://docs.jfrog.com/administration/docs/access-tokens) work both as bearer tokens and as basic-auth passwords, which makes Artifactory the strongest fit for every method. * **Firewall as upstream**: the repository manager fronts clients and fetches external packages through the firewall. * **Manager as upstream**: the firewall fronts clients and fetches from the repository manager. Details in [Integration methods](#integration-methods). **Firewall as upstream:** create a remote repository per ecosystem with the firewall endpoint as the URL and a Service Access Token as the password (any username works). **Manager as upstream:** add the virtual repository URL as a firewall upstream with an access token. To keep the token read-only, issue it for a user or group that only has read permission; tokens created with expiry 0 never expire. | Ecosystem | Firewall as upstream | Manager as upstream | Artifactory docs | | --------- | -------------------- | ------------------- | -------------------------------------------------------------------------------- | | npm | yes | yes | [npm repositories](https://docs.jfrog.com/artifactory/docs/npm-repositories) | | Maven | yes | yes | [Maven repositories](https://docs.jfrog.com/artifactory/docs/maven-repositories) | | NuGet | yes | yes | [NuGet repositories](https://docs.jfrog.com/artifactory/docs/nuget-repositories) | | PyPI | yes | yes | [PyPI repositories](https://docs.jfrog.com/artifactory/docs/pypi-repositories) | | Go | yes | yes | [Go registries](https://docs.jfrog.com/artifactory/docs/go-modules) | | Conda | yes | yes | [Conda repositories](https://docs.jfrog.com/artifactory/docs/conda-repositories) | See also: [remote repositories](https://docs.jfrog.com/artifactory/docs/remote-repositories), [virtual repositories](https://docs.jfrog.com/artifactory/docs/virtual-repositories), [access tokens](https://docs.jfrog.com/administration/docs/access-tokens). ### Sonatype Nexus Repository [#sonatype-nexus-repository] Nexus Repository is Sonatype's self-hosted repository manager. A **proxy repository** fetches from an external registry and caches the result; a **group repository** bundles several repositories behind one client URL. * **Firewall as upstream**: the repository manager fronts clients and fetches external packages through the firewall. * **Manager as upstream**: the firewall fronts clients and fetches from the repository manager. Details in [Integration methods](#integration-methods). **Firewall as upstream:** works for every ecosystem. Proxy repositories accept any upstream URL and authenticate outbound with a username and password: set any username and the firewall Service Access Token as the password. **Manager as upstream:** works for every ecosystem. Nexus authenticates reads with basic auth: create a [user token](https://help.sonatype.com/en/user-tokens.html) for a read-only account and store its name and passcode as the upstream's username and password. For npm you can alternatively enable the [npm Bearer Token Realm](https://help.sonatype.com/en/realms.html) and store an npm login token as the upstream's `token`; for Maven, PyPI, NuGet, Go, and Conda, Nexus has no bearer realms, so the user-token pair is the way. One Conda version note: proxy repositories exist in all supported Nexus versions, but hosted and group Conda repositories need Nexus 3.92 or later. | Ecosystem | Firewall as upstream | Manager as upstream | Nexus docs | | --------- | -------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------- | | npm | yes | yes, user token as username/password, or npm Bearer Token Realm | [npm registry](https://help.sonatype.com/en/npm-registry.html) | | Maven | yes | yes, user token as username/password | [Maven repositories](https://help.sonatype.com/en/maven-repositories.html) | | NuGet | yes | yes, user token as username/password | [NuGet repositories](https://help.sonatype.com/en/nuget-repositories.html) | | PyPI | yes | yes, user token as username/password | [PyPI repositories](https://help.sonatype.com/en/pypi-repositories.html) | | Go | yes | yes, user token as username/password | [Go repositories](https://help.sonatype.com/en/go-repositories.html) | | Conda | yes | yes, user token as username/password | [Conda repositories](https://help.sonatype.com/en/conda-repositories.html) | See also: [repository types](https://help.sonatype.com/en/repository-types.html), [proxy outbound HTTP settings](https://help.sonatype.com/en/http-request-and-proxy-settings.html), [realms](https://help.sonatype.com/en/realms.html), [user tokens](https://help.sonatype.com/en/user-tokens.html). ### GitLab package registry [#gitlab-package-registry] The package registry is built into GitLab: projects and groups host their own packages. It is a hosting service first; it has no general proxy repository. * **Firewall as upstream**: the repository manager fronts clients and fetches external packages through the firewall. * **Manager as upstream**: the firewall fronts clients and fetches from the repository manager. Details in [Integration methods](#integration-methods). **Firewall as upstream:** no for the standard package registry, with one Maven exception. The only outbound mechanism is [request forwarding](https://docs.gitlab.com/user/packages/package_registry/supported_functionality/#forwarding-requests): when a package is not in your GitLab registry, GitLab asks the package manager's official public registry (npmjs.com for npm, pypi.org for PyPI, Maven Central for Maven). That target is hardcoded per format; you cannot point it at another URL, so it cannot go through the firewall. The exception is the Maven **virtual registry** (beta), which does accept custom upstream URLs with a username and password: point it at the firewall's Maven endpoint with a Service Access Token as the password. **Manager as upstream:** works for npm, Maven, NuGet, and PyPI. GitLab's npm endpoint accepts tokens the way npm sends them, as a bearer header, so store the token as the upstream's `token`. The other formats authenticate reads with basic auth: store a deploy token's username and the token value as the upstream's username and password. | Ecosystem | Firewall as upstream | Manager as upstream | GitLab docs | | --------- | ----------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | npm | no, forwarding targets are fixed | yes, token as `token` | [npm registry](https://docs.gitlab.com/user/packages/npm_registry/) | | Maven | yes, via virtual registry (beta) | yes, deploy token as username/password | [Maven virtual registry](https://docs.gitlab.com/user/packages/virtual_registry/maven/), [Maven repository](https://docs.gitlab.com/user/packages/maven_repository/) | | NuGet | no, no forwarding | yes, deploy token as username/password | [NuGet repository](https://docs.gitlab.com/user/packages/nuget_repository/) | | PyPI | no, forwarding targets are fixed | yes, deploy token as username/password | [PyPI repository](https://docs.gitlab.com/user/packages/pypi_repository/) | | Go | no | no | [supported functionality](https://docs.gitlab.com/user/packages/package_registry/supported_functionality/) | | Conda | not offered by the package registry | not offered | [supported functionality](https://docs.gitlab.com/user/packages/package_registry/supported_functionality/) | Tokens for the manager-as-upstream method: a group deploy token or group access token with `read_package_registry`. ### GitHub Packages [#github-packages] GitHub Packages is GitHub's package hosting service, tied to repositories and organizations (npm, Maven/Gradle, NuGet, RubyGems, containers; no PyPI, Go modules, or Conda). The credential for every format is a classic personal access token (PAT) with `read:packages`, best created on a machine user; GitHub states ["GitHub Packages only supports authentication using a personal access token (classic)"](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages#authenticating-to-github-packages). The per-format question is only how the PAT is sent. * **Firewall as upstream**: the repository manager fronts clients and fetches external packages through the firewall. * **Manager as upstream**: the firewall fronts clients and fetches from the repository manager. Details in [Integration methods](#integration-methods). **Firewall as upstream:** does not apply. GitHub Packages does not proxy custom upstreams. **Manager as upstream:** works for npm, Maven, and NuGet. For npm, bearer auth is what [GitHub documents for npm installs](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#installing-a-package), so store the PAT as the upstream's `token`. For Maven and NuGet, GitHub documents the PAT as a basic-auth password ([settings.xml](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry#authenticating-with-a-personal-access-token), [Gradle credentials block](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#authenticating-with-a-personal-access-token), [nuget.config](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry#authenticating-with-a-personal-access-token)): store the GitHub username and the PAT as the upstream's username and password. | Ecosystem | Firewall as upstream | Manager as upstream | GitHub docs | | --------- | ------------------------------ | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | npm | no, hosting only | yes, PAT as `token` | [npm registry](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-npm-registry) | | Maven | no, hosting only | yes, PAT as basic-auth password | [Maven registry](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry) | | NuGet | no, hosting only | yes, PAT as basic-auth password | [NuGet registry](https://docs.github.com/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry) | | PyPI | not offered by GitHub Packages | not offered | [introduction](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages) | | Go | not offered by GitHub Packages | not offered | [introduction](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages) | | Conda | not offered by GitHub Packages | not offered | [introduction](https://docs.github.com/en/packages/learn-github-packages/introduction-to-github-packages) | See also: [permissions](https://docs.github.com/en/packages/learn-github-packages/about-permissions-for-github-packages). ### Azure Artifacts [#azure-artifacts] Azure Artifacts is the package service in Azure DevOps. A **feed** hosts your packages and can pull missing packages from **upstream sources**. * **Firewall as upstream**: the repository manager fronts clients and fetches external packages through the firewall. * **Manager as upstream**: the firewall fronts clients and fetches from the repository manager. Details in [Integration methods](#integration-methods). **Firewall as upstream:** npm only. Custom upstream URLs are limited to npm: the [upstream sources documentation](https://learn.microsoft.com/en-us/azure/devops/artifacts/concepts/upstream-sources?view=azure-devops#save-packages-from-upstream-sources) states "Custom upstream sources are only supported for npm packages." Feeds for other protocols can only use the built-in public registries and other Azure feeds as upstreams, so they cannot point at the firewall. **Manager as upstream:** works across Azure's protocols. The firewall detects `pkgs.dev.azure.com` endpoints and sends the credential as basic auth automatically, which is what Azure feeds expect. Use a [PAT](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops) with **Packaging: Read**; the maximum lifetime is one year, [organization policy](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/manage-pats-with-policies-for-administrators?view=azure-devops) may enforce shorter, so plan rotation. | Ecosystem | Firewall as upstream | Manager as upstream | Azure docs | | --------- | --------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | npm | yes, custom upstream | yes | [npmrc setup](https://learn.microsoft.com/en-us/azure/devops/artifacts/npm/npmrc?view=azure-devops) | | Maven | no, custom upstreams are npm-only | yes | [upstream sources](https://learn.microsoft.com/en-us/azure/devops/artifacts/concepts/upstream-sources?view=azure-devops) | | NuGet | no, custom upstreams are npm-only | yes | [NuGet with Visual Studio](https://learn.microsoft.com/en-us/azure/devops/artifacts/nuget/install-nuget-packages-with-visual-studio?view=azure-devops) | | PyPI | no, custom upstreams are npm-only | yes | [upstream sources](https://learn.microsoft.com/en-us/azure/devops/artifacts/concepts/upstream-sources?view=azure-devops) | | Go | not offered by Azure Artifacts | not offered | [Azure Artifacts overview](https://learn.microsoft.com/en-us/azure/devops/artifacts/?view=azure-devops) | | Conda | not offered by Azure Artifacts | not offered | [Azure Artifacts overview](https://learn.microsoft.com/en-us/azure/devops/artifacts/?view=azure-devops) | See also: [set up upstream sources](https://learn.microsoft.com/en-us/azure/devops/artifacts/how-to/set-up-upstream-sources?view=azure-devops), [feed permissions](https://learn.microsoft.com/en-us/azure/devops/artifacts/feeds/feed-permissions?view=azure-devops), [personal access tokens](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops). ### AWS CodeArtifact [#aws-codeartifact] CodeArtifact is AWS's managed package repository. Repositories can chain to other CodeArtifact repositories as upstreams, and public packages enter through an **external connection**. * **Firewall as upstream**: the repository manager fronts clients and fetches external packages through the firewall. * **Manager as upstream**: the firewall fronts clients and fetches from the repository manager. Details in [Integration methods](#integration-methods). **Firewall as upstream:** does not apply; an AWS design constraint, not a protocol incompatibility. External connections are a [fixed list of public registries](https://docs.aws.amazon.com/codeartifact/latest/ug/external-connection.html#supported-public-repositories) (npmjs, Maven Central, NuGet Gallery, PyPI, RubyGems, crates.io, and a few Maven variants; one per repository), and [upstream repositories must be other CodeArtifact repositories](https://docs.aws.amazon.com/codeartifact/latest/ug/repos-upstream.html). There is no field for a custom registry URL on either mechanism. **Manager as upstream:** works across CodeArtifact's formats. For npm, [npm auth](https://docs.aws.amazon.com/codeartifact/latest/ug/npm-auth.html) uses `_authToken`, the same bearer header the firewall sends for a `token`. For Maven, NuGet, PyPI, and Go, AWS documents the authorization token as a basic-auth password with username `aws` ([settings.xml with username `aws`](https://docs.aws.amazon.com/codeartifact/latest/ug/maven-mvn.html), [pip URL-embedded credentials](https://docs.aws.amazon.com/codeartifact/latest/ug/python-configure-pip.html)): store `aws` as the upstream's username and the token as the password. In every case [CodeArtifact authorization tokens](https://docs.aws.amazon.com/codeartifact/latest/ug/tokens-authentication.html) expire after at most 12 hours, so stored credentials need scheduled rotation: push a fresh token with [`bsfw firewall credentials`](/firewall/how-to/manage-upstream-credentials) on a schedule, or keep tokens out of the firewall with the [credential envelope](/firewall/references/tokens#credential-envelope). | Ecosystem | Firewall as upstream | Manager as upstream | AWS docs | | --------- | ----------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | npm | no, external connections are a fixed list | yes, token as `token`, with rotation | [npm auth](https://docs.aws.amazon.com/codeartifact/latest/ug/npm-auth.html) | | Maven | no, external connections are a fixed list | yes, `aws` + token as username/password, with rotation | [Maven](https://docs.aws.amazon.com/codeartifact/latest/ug/maven-mvn.html) | | NuGet | no, external connections are a fixed list | yes, `aws` + token as username/password, with rotation | [NuGet](https://docs.aws.amazon.com/codeartifact/latest/ug/nuget-cli.html) | | PyPI | no, external connections are a fixed list | yes, `aws` + token as username/password, with rotation | [pip](https://docs.aws.amazon.com/codeartifact/latest/ug/python-configure-pip.html) | | Go | no, external connections are a fixed list | yes, `aws` + token as username/password, with rotation | [upstream repositories](https://docs.aws.amazon.com/codeartifact/latest/ug/repos-upstream.html) | | Conda | not offered by CodeArtifact | not offered | [CodeArtifact concepts](https://docs.aws.amazon.com/codeartifact/latest/ug/codeartifact-concepts.html) | See also: [external connections](https://docs.aws.amazon.com/codeartifact/latest/ug/external-connection.html), [tokens and authentication](https://docs.aws.amazon.com/codeartifact/latest/ug/tokens-authentication.html). ### Other repository managers [#other-repository-managers] The same two questions decide the fit for any other repository manager: 1. **Firewall as upstream:** can it proxy a custom upstream registry URL with credentials? If yes, point that at the firewall endpoint with a Service Access Token; the firewall accepts basic auth and bearer tokens. 2. **Manager as upstream:** does it serve reads to a client presenting a bearer token or basic-auth credentials (or allow anonymous reads)? If yes, add it as a firewall upstream with the matching credential: a token for bearer, a username and password for basic auth. From the manager's perspective the firewall is a standard registry; from the firewall's perspective the manager is a standard upstream. There is nothing vendor-specific on the Bytesafe side. ## Credentials [#credentials] ### Repository manager to firewall (firewall as upstream) [#repository-manager-to-firewall-firewall-as-upstream] The repository manager is the client here and the firewall is its upstream, so the credential lives in the manager's remote-repository settings. Create a [Service Access Token](/firewall/how-to/manage-tokens) for the repository manager; it is a service, not a person. In the manager's remote-repository credential fields, set any username (the token name is a good convention) and the token as the password. The firewall reads the token from the basic-auth password, from the username field if the password is empty, or from a `Bearer` header; whichever the manager sends works. ### Firewall to repository manager (manager as upstream) [#firewall-to-repository-manager-manager-as-upstream] The firewall is the client here and the repository manager is the firewall's upstream, so the credential lives in the firewall's upstream config. Create a read-only credential on the manager's side and store it in the upstream's credential fields (dashboard, or [`bsfw firewall credentials`](/firewall/how-to/manage-upstream-credentials)). Which field decides the auth style the firewall sends: `token` goes out as a bearer header, username and password as basic auth. | Vendor | Credential to create | Store as | Minimum scope | Expiry | | ---------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------- | -------------------------------------------------- | | Artifactory | Access token for a service user | `token`, or username/password | Read permission target on the repository or virtual repository | Configurable, non-expiring supported | | Nexus | User token (all formats); npm alternatively an npm login token via the npm Bearer Token Realm | Username/password (user token name and passcode); npm login token as `token` | Repository-view read privilege | Valid until reset | | GitLab | Group deploy token or group access token | npm: `token`. Maven, NuGet, PyPI: username/password | `read_package_registry` | Deploy tokens: optional. Access tokens: max 1 year | | GitHub Packages | Classic PAT on a machine user | npm: `token`. Maven, NuGet: username (account) and PAT as password | `read:packages` | Optional, org policy may force one | | Azure Artifacts | PAT | `token` (sent as basic auth automatically on `pkgs.dev.azure.com`) | Packaging: Read | Max 1 year; rotation needed | | AWS CodeArtifact | `aws codeartifact get-authorization-token` | npm: `token`. Maven, NuGet, PyPI, Go: username `aws` and token as password | `codeartifact:ReadFromRepository` plus token issuance permissions | 15 minutes to 12 hours; rotation required | Wrong or expired upstream credentials do not show up as authentication errors on the client. The firewall stops using that upstream and serves what the other upstreams return, so a package held only by the repository manager fails to install, with an error naming the package rather than the credential. The rejection is recorded in the firewall log as an upstream error, which is the fastest way to confirm it. See [Check for failing upstreams](/firewall/how-to/view-logs#check-for-failing-upstreams) and [Manage upstream credentials](/firewall/how-to/manage-upstream-credentials). ## Prevent bypass [#prevent-bypass] Whichever method you run, the policy is only as good as the weakest path to a public registry: * **Firewall as upstream:** remove or disable remote repositories that still point directly at public registries, and remove them from virtual/group repository member lists. A virtual repository that aggregates both the firewall-backed remote and a direct public remote silently bypasses policy for whatever the direct remote answers first. * **Manager as upstream:** enforce the firewall endpoint in committed client config and CI images, the same as plain firewall setups; see [the ecosystem pattern](/firewall/ecosystems#the-pattern). * **Firewall on both sides:** the repository manager host should only be able to reach public registries through the egress firewall; enforce with network egress rules, not just configuration. ## Verify [#verify] Install a package through the full chain, then confirm the firewall saw it: ```shell bsfw logs --firewall --all-users ``` With the firewall as the manager's upstream, the log entry's user is the repository manager's Service Access Token. With the manager as a firewall upstream, check that the entry's `upstream` field contains the repository manager upstream ID. Then verify a block: add a rule that blocks a test package and confirm the install fails through the whole chain, not just against the firewall directly. ## Troubleshoot [#troubleshoot] The firewall skips upstreams that reject its credentials, so auth failures look like missing packages. Check that the credential sits in the right upstream field for the format (see the per-vendor tables above: `token` is sent as a bearer header, username/password as basic auth, and the token wins when both are set) and that it has read scope on the exact repository or feed the endpoint URL names. Test the URL from outside the firewall with the same style: `curl -H "Authorization: Bearer $TOKEN"` for a token, `curl -u "$USER:$PASSWORD"` for basic auth. When the repository manager fronts clients (firewall as upstream), suspect the manager's cache (the package was fetched before the rule existed) or a virtual/group repository that still contains a direct public remote. Clear the manager's cache for that package and audit the repository member list. Some path points back at itself: a firewall upstream naming a manager repository whose remote is that same firewall, or an egress firewall with the manager as upstream. Draw the chain; every hop must point strictly toward the public registry. Expected when the firewall is the manager's upstream: the repository manager is the client, so its Service Access Token is the user on every entry. If you need per-developer identity in firewall logs, put the firewall in front of developers instead (manager as upstream, or firewall on both sides). ## Related [#related] * Concept: [Upstreams and caching](/firewall/concepts/upstreams-and-caching), [Deployment models](/firewall/concepts/deployment-models) * How-to: [Configure upstreams](/firewall/how-to/configure-upstreams), [Manage upstream credentials](/firewall/how-to/manage-upstream-credentials), [Prevent dependency confusion](/firewall/how-to/prevent-dependency-confusion), [Migrate from previous-generation Bytesafe](/firewall/how-to/migrate-from-previous-generation) * Reference: [Upstreams](/firewall/references/upstreams), [Tokens](/firewall/references/tokens) # Investigate a blocked install (/firewall/how-to/investigate-blocked-install) A developer or a CI job reports a failed install. This guide takes you from the error message to the responsible rule to a resolution. ## 1. Recognize the two failure shapes [#1-recognize-the-two-failure-shapes] **Download blocked.** The client got an explicit block: an error response from the firewall naming the package and a request ID. The rule behind it can sit on either the download or the versions phase, since an artifact request is evaluated against both. With npm, for example: ```text lodash@4.17.20 is blocked. Check firewall log entries for request for details ``` The request ID in the message identifies the matching log entry. The named package is the one that was blocked, which is often a transitive dependency rather than the package that was installed. Each [ecosystem page](/firewall/ecosystems) shows the exact error per package manager. **Version filtered.** The client reports a version as not found even though it exists on the public registry. A rule on the versions phase filtered it out of the metadata the client resolved against, most commonly a [delay rule](/firewall/how-to/delay-new-versions). There is no request ID in the client error; search the logs by package name. ## 2. Find the log entry [#2-find-the-log-entry] In the dashboard, open **Logs** on the firewall and search for the package. Select a likely row and compare its `request` field with the ID from the client error. From the terminal, search the same logs by package name and ask for complete entries: ```shell bsfw logs --all-users --firewall --search -o json ``` `--all-users` matters here: someone else hit the block, and without it you only see your own requests. If the report came from CI, narrow further by that pipeline's [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) or the Service Access Token user. ## 3. Read the entry [#3-read-the-entry] The log entry tells you: * **Rule.** Which rule matched, and its description. * **Rule output.** Selector-function details, for example the CVEs, CVSS, and EPSS values behind a vulnerabilities match. * **Blocked.** Whether the request was actually blocked or only logged. * **User, timestamp, upstream.** Who hit it, when, and where the package would have come from. To see the findings behind the decision without replaying the install, ask about the exact version the client wanted. The version has to be exact, no ranges: ```shell bsfw package -o json ``` This is the same vulnerability and malware data the rules evaluate, so it also answers whether a newer version is clean. An absent malware result means unknown, not proof that the package is clean. ## 4. Resolve [#4-resolve] Three options, in order of preference: 1. **Change the dependency.** If the block is legitimate (real vulnerability, malware, too-new release), the right fix is usually a different version. Often the version range already permits an older, allowed release. 2. **Add an exception.** The package is needed as-is and the risk is understood. Create an [exception](/firewall/how-to/manage-exceptions) scoped to the exact package and version, with reason and expiry. This unblocks one case and leaves the rule intact. 3. **Adjust the rule.** Only when the log shows a pattern of false positives, not for one blocked install. Consider switching the rule to log-only while retuning. ## If there is no log entry [#if-there-is-no-log-entry] A missing entry does not mean nothing happened. Work through these: * **The rule blocks without logging.** `block` and `log` are independent effects, so a rule with `{ "block": true, "log": false }` fails the install and records nothing. Check the firewall's rules for blocking rules with logging off; this is the one case where the client fails and the log stays empty by design. * **An upstream failed, and no rule was involved.** A version can go missing because the upstream that has it did not answer, not because a rule filtered it. Those failures get their own entries: look for them with `bsfw logs --all-users --kind error`, or set **Status** to **Error** in the dashboard. See [Check for failing upstreams](/firewall/how-to/view-logs#check-for-failing-upstreams). * **The request never reached the firewall.** Check the client's registry config and the lockfile's `resolved` URLs. * **Authentication failed.** A 401 happens before rule evaluation and creates no rule log entry. Check the token. ## Related [#related] * How-to: [View logs](/firewall/how-to/view-logs), [Manage exceptions](/firewall/how-to/manage-exceptions) * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) # Manage access (/firewall/how-to/manage-access) Access in a namespace is organized around teams. Users belong to teams, teams carry permissions, and firewalls grant permissions to teams. There are no per-user grants; to give one person special access, give them a team. For the underlying model, see [Access and permissions](/firewall/concepts/access-and-permissions). This page is the task view. ## The two built-in teams [#the-two-built-in-teams] Every namespace has two protected teams that cannot be edited or deleted: * **everybody** contains all users. It grants namespace access, PAT creation, firewall creation, and read access to firewalls. * **admin** members can additionally manage teams, create Service Access Tokens, and get full write access to firewalls. Membership in **everybody** is what makes someone a member of the namespace at all: a user has to be in it before they can be added to any other team. Its permissions are therefore the floor for everyone. Teams add permissions on top; they cannot take away what **everybody** already grants. Day one, every user can read every firewall and admins can change everything. Teams beyond that exist to grant write access more precisely. ## Create a team and add members [#create-a-team-and-add-members] 1. In the dashboard, open **Teams** and choose **Create Team**. Give it a name and an optional description. 2. Open the team and use **Add Member** to add users. The picker lists everyone in your organization by name, minus current members. Users can be members of multiple teams. 3. Set the team's permissions. These are namespace-level: they cap what members can do anywhere in the namespace. A sensible starting shape is one team per area of responsibility, for example a `platform` team that manages firewall configuration and a `security` team that manages rules and exceptions. ## Grant a team access to a firewall [#grant-a-team-access-to-a-firewall] Firewall write access is granted per firewall: 1. Open the firewall and go to its access settings. 2. Choose **Add Team** and select the team. 3. Pick the permissions the team should have on this firewall, for example rule and exception updates but not configuration or deletion. A new firewall starts with read access (`fw:access`, `fw:clone`, `fw:read`) for **everybody** and the full permission set for the owner team named at creation; without one, the full set also goes to **everybody**. Defaults and the guard against locking a firewall out of management: [Permissions](/firewall/references/permissions#defaults-for-a-new-firewall). ## How the two levels combine [#how-the-two-levels-combine] A user's effective permissions on a firewall are the firewall's grants to their teams, limited by their namespace-level permissions. Both levels must allow an action: * A firewall grant without the namespace permission does nothing. * A namespace permission without a firewall grant gives no access to that firewall. Tokens can narrow this further; a Personal Access Token never grants more than the user's teams do. See [Manage tokens](/firewall/how-to/manage-tokens). ## Verify [#verify] Have a member of the team open the firewall in the dashboard and attempt the granted action, for example editing a rule. Then confirm an action they should not have is rejected. Permission denials are recorded in the audit log with the missing permission. ## Related [#related] * Concept: [Access and permissions](/firewall/concepts/access-and-permissions) * Reference: [Permissions](/firewall/references/permissions) * How-to: [Manage tokens](/firewall/how-to/manage-tokens) * Platform: [Authentication](/firewall/platform/authentication), how the users in the picker sign in # Manage config as code (/firewall/how-to/manage-config-as-code) Once policy lives in git (walked through once in the [config as code tutorial](/firewall/getting-started/config-as-code)), the remaining questions are operational: how to organize the files, who applies them, and how to notice when reality and git disagree. This page is the working setup. ## Repository layout [#repository-layout] One JSON file per firewall, exported with the CLI: ```text firewall-config/ npm-firewall.json pypi-firewall.json maven-firewall.json README.md ``` Keep the files exactly as `bsfw firewall export` writes them. Hand-formatting invites noisy diffs; the export format is the canonical one. ## The change flow [#the-change-flow] 1. Branch, edit the JSON (add a rule, change an exception, adjust an upstream). 2. Open a pull request. The diff is the policy change; review it like code. 3. On merge, apply: ```shell bsfw firewall import --file firewall-config/npm-firewall.json ``` Import is a full upsert: the file replaces the firewall's configuration and the config `version` increments. A rule or exception deleted from the file is deleted on the firewall, and any change made in the dashboard since the file was exported is overwritten. Whoever (or whatever) runs the import needs the matching permissions, `fw:update:rule` and `fw:update:exception` for policy edits, `fw:update:config` for upstream changes, `fw:create` if the file's firewall ID is new. ## Applying from CI [#applying-from-ci] Run the import step on merge to the main branch. Two ways to authenticate: * **Trusted Builder (recommended on GitHub Actions).** No stored secret; grant the builder the update permissions and restrict it to the config repo. See [GitHub Actions](/firewall/getting-started/github-actions). * **Service Access Token.** Store it as a CI secret, scope it to the firewalls the repo manages, and give it only the update permissions it needs. Append an [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) to the token (`::${CI_PIPELINE_ID}`) so each applied change is traceable to a pipeline run in the audit log. ## Credentials stay out of the repo [#credentials-stay-out-of-the-repo] Exports contain placeholders for upstream credentials and imports reject plaintext values, so the repo never carries secrets. On an existing firewall and upstream, imports keep the stored values untouched. A placeholder imported under a new firewall ID or for a new upstream resolves to empty. Set credentials once per environment with [`bsfw firewall credentials`](/firewall/references/cli#bsfw-firewall-credentials). See [Manage upstream credentials](/firewall/how-to/manage-upstream-credentials). ## Prevent and detect drift [#prevent-and-detect-drift] Anyone with the right permissions on a firewall can still change it directly in the dashboard, and then git no longer matches reality. Three countermeasures, in order of how much they save you: * **Grant write access only to the pipeline.** Firewall permissions are granted per firewall to teams, and a user gets only what the firewall's map grants their teams. Namespace-level admin does not override it. Leave `fw:update:config`, `fw:update:rule`, and `fw:update:exception` out of the grants for everyday teams, and the dashboard becomes read-only for them: the import is then the only way policy changes. Service Access Tokens are not subject to the map, so the CI token keeps working. Two limits: the [lockout guard](/firewall/references/permissions#lockout-guard) requires at least one team to keep `fw:update:config`, so keep that team small, and anyone who can manage team membership can add themselves to it, which the audit log records. * **Compare on a schedule or before each apply:** ```shell bsfw firewall export | diff - firewall-config/npm-firewall.json ``` A non-empty diff means someone changed the firewall outside git. Decide per case: export and commit the change, or re-import the file to overrule it. * **Watch the audit log** for `firewall.config.upsert`, `firewall.rule.upsert`, and `firewall.exception.upsert` events whose actor is not the CI identity. See [Review the audit log](/firewall/how-to/review-audit-log). Note that the export includes the server-maintained `version` and `updatedAt` fields, which advance on every change; a diff that touches only those is not policy drift. ## Roll back [#roll-back] Git is the primary rollback path: check out the previous file and import it. The server additionally keeps a version history per firewall, 20 versions by default and up to 200 with `--limit`, useful when a bad change came from outside git: ```shell bsfw firewall history bsfw firewall export --version 7 > check.json # read a past version, no change ``` There is no CLI command for rollback itself. Either call `PUT /v1//config//version/{version}` directly, or export the old version and import it, which reaches the same result through the CLI. Both paths create a new version; nothing is rewritten. See the [API reference](/firewall/references/api#configuration) and [CLI reference](/firewall/references/cli#bsfw-firewall-history). ## Verify [#verify] After an apply, confirm the live config matches the file: ```shell bsfw firewall export | diff - firewall-config/npm-firewall.json && echo "in sync" ``` Expect a diff on `version`/`updatedAt` only (see above); anything else means the apply did not do what the file says. ## Related [#related] * Tutorial: [Config as code](/firewall/getting-started/config-as-code) * Reference: [Firewall config](/firewall/references/firewall-config), [CLI](/firewall/references/cli), [API](/firewall/references/api) * How-to: [Manage upstream credentials](/firewall/how-to/manage-upstream-credentials), [Review the audit log](/firewall/how-to/review-audit-log) # Manage exceptions (/firewall/how-to/manage-exceptions) Rules are general; reality has edge cases. An exception lets a specific package through a specific rule without changing the rule itself. Every exception carries a reason and, ideally, an expiry date, so waivers stay visible and temporary instead of accumulating as silent policy holes. ## Create an exception from a log entry [#create-an-exception-from-a-log-entry] The fastest path starts from the block itself: 1. Open **Logs** on the firewall and find the blocked request. 2. Create an exception from the log entry. The rule, package name, and version are prefilled. 3. Pick a **Reason** and set **Expiration**. 4. Save, then retry the install. ## Create an exception manually [#create-an-exception-manually] Open **Exceptions** on the firewall and add one: * **Rule.** The rule to except from. An exception belongs to exactly one rule. * **Selector.** Which packages the exception covers: package name and version, matched the same way a rule selector matches them. * **Reason.** One of the dashboard reasons below. Exported JSON stores its corresponding snake-case config value. * **Expires.** Optional, recommended. * **Log.** Whether matching requests still produce a log entry. Keep this on; allowed-by-exception is exactly what you want visible in an audit. As JSON: ```json { "rule": "block-critical-vulns", "selector": { "packageName": "lodash", "version": "4.17.20" }, "log": true, "expires": "2026-08-01T00:00:00Z", "reason": "not_exploitable", "notes": "Vulnerable function not reachable from our code. Tracked in JIRA-1234." } ``` To apply the JSON without the dashboard: export the config with `bsfw firewall export`, add the exception to the `exceptions` array, and import it back. See [firewall import](/firewall/references/cli#bsfw-firewall-import). ## Scope narrowly [#scope-narrowly] An exception selector with only a package name covers every version of that package, including versions published after you wrote it, against that rule. Prefer pinning the exact version. Wildcards work (`lodash@4.17.*`) but widen the hole. The narrower the exception, the less it erodes the rule. An exception applies to one rule. It does not exempt the package from your other rules, so a package excepted from a license rule is still checked for malware. If the rule comes through [inheritance](/firewall/how-to/share-configuration-across-firewalls), adding an exception needs `fw:update:exception` on both this firewall and the source firewall that owns the rule. The source may be any ancestor in the chain. Without both permissions, the dashboard greys out **Add exception** and its tooltip identifies the missing permission. ## Let vulnerability fixes skip a delay [#let-vulnerability-fixes-skip-a-delay] For a versions- or download-phase rule on an npm, Go, Maven, PyPI, or NuGet firewall, the dashboard offers **Vulnerability Fixes** under **Condition**. It matches versions identified in current advisory data as published fixes. On a [delay rule](/firewall/how-to/delay-new-versions), this lets a security-fix release through the age window without allowing unrelated new releases. Keep exception logging on. This makes each automatic bypass visible. The `vulnerability-fix` [selector function reference](/firewall/references/selector-functions#vulnerability-fix) documents the matching behavior and JSON shape. ## Reasons [#reasons] | Reason | Meaning | | ------------------------ | ----------------------------------------------------------- | | `acknowledged_violation` | We know, we accept it for now | | `evaluating_component` | Under assessment, temporary access | | `mitigated_externally` | Risk handled outside the package, for example WAF or config | | `no_upgrade_path` | No fixed version available yet | | `not_exploitable` | Vulnerability cannot be triggered in our usage | | `not_reachable` | Vulnerable code path not reachable | | `other` | Explain in notes | ## Expiry [#expiry] An expired exception stops matching. The next install of that package is blocked again, and the returning block is what tells you the date passed, so pick a date you are prepared to act on. That is the point of expiry: `no_upgrade_path` gets re-checked when it expires instead of surviving three years past the fix. ## Review existing exceptions [#review-existing-exceptions] List them per firewall in the dashboard, or with the CLI: ```shell bsfw firewall exceptions -o json ``` Ask for JSON here rather than the default table: the table is a compact view, and reason, expiry, and notes are exactly the fields a review turns on. The dashboard shows the same three with names resolved instead of IDs. Worth a periodic review: exceptions without expiry and exceptions with reason `other` and empty notes are the ones that turn into permanent policy by accident. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation), including when exceptions are checked * How-to: [Share configuration across firewalls](/firewall/how-to/share-configuration-across-firewalls), for exceptions on inherited rules * Reference: [Rules and selectors](/firewall/references/rules-and-selectors), [Permissions](/firewall/references/permissions) # Manage tokens (/firewall/how-to/manage-tokens) Every request to a firewall carries an access token. This page covers creating them with the right scope, and revoking them. For which token type to use where, see [Access Tokens](/firewall/getting-started/access-tokens); for how token scope interacts with team permissions, see [Access and permissions](/firewall/concepts/access-and-permissions). ## Create a token [#create-a-token] 1. In the dashboard, open the user menu in the lower-left corner and select **Access Tokens** (it is not in the main sidebar), then choose **Create Token**. 2. Name it after what will use it: your machine for a PAT, the system for a SAT (`ci-pipeline`, `build-server-eu`). 3. Pick a validity. Dashboard presets range from 1 day to 1 year, defaulting to 7 days. The API accepts any duration up to 2 years (`validForHours`). 4. Optionally narrow the scope (next section). 5. Copy the value. It is shown once. Creating a PAT requires the `ns:create:pat` permission (every user has it through the **everybody** team); creating a SAT requires `ns:create:service-token` (admins by default). ## Scope a token [#scope-a-token] Two optional restrictions make a leaked token less useful: * **Permissions.** A token can carry a subset of permissions. A PAT with only `fw:access` works for installs but cannot read or change configuration, even if you can. A token can never do more than its creator: PATs are capped by your team permissions, and SATs by what the creating session holds. * **Restrict to firewalls.** A token listing specific firewalls is rejected on every other firewall. A CI token for one project is the canonical case: a SAT with `fw:access` only, restricted to that project's firewall. Service Access Tokens are further capped by a fixed catalog: they can hold access, read, and configuration/rule/exception update permissions, but never team management, credential updates, or namespace and firewall deletion. See [Permissions](/firewall/references/permissions). ## Revoke a token [#revoke-a-token] Delete the token from the **Access Tokens** page. Validation fails from that moment; there is no grace period. Expired tokens reject the same way, so an expiry is the automatic version of this. Any system still using a deleted token starts failing on its next request, which is the point during an incident and a hazard during housekeeping. Rotate by creating the replacement first, updating the consumer, then deleting the old token. ## Prefer Trusted Builders over stored CI secrets [#prefer-trusted-builders-over-stored-ci-secrets] A SAT stored in CI is a long-lived secret. For CI systems that can prove their identity with OpenID Connect (currently GitHub Actions), register a Trusted Builder instead: each run exchanges its OIDC token for a Service Access Token valid for 15 minutes. There is nothing to rotate, and a token that leaks from a build log is worthless within the quarter hour. Setup is covered in the [GitHub Actions guide](/firewall/getting-started/github-actions). ## Keep logs attributable [#keep-logs-attributable] * One token per system. A shared `ci` token makes the firewall log useless for tracing which pipeline pulled a package. * Append an interaction ID (`::build-4711`) to correlate all requests from one run. See [Access Tokens](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids). ## Verify [#verify] Use the new token once, then check it appears in the logs attributed as expected: ```shell bsfw logs --firewall ``` PAT requests are attributed to the owning user; SAT requests to the token's name. ## Related [#related] * Concept: [Access and permissions](/firewall/concepts/access-and-permissions) * Reference: [Tokens](/firewall/references/tokens), [Permissions](/firewall/references/permissions) * Getting started: [Access Tokens](/firewall/getting-started/access-tokens) # Manage upstream credentials (/firewall/how-to/manage-upstream-credentials) Upstream credentials are what the firewall presents to a private registry when fetching packages or forwarding publishes. They are deliberately separated from the rest of the configuration: encrypted at rest, never returned by the API, excluded from exports, and changed through their own permission (`fw:update:credentials`) and audit event. Which fields you set decides the auth style the firewall sends: a `token` goes out as an `Authorization: Bearer` header, a username and password as HTTP basic auth. If both are set, the token wins. Details per path in the [upstreams reference](/firewall/references/upstreams#credentials). ## Set or rotate credentials [#set-or-rotate-credentials] With the CLI, set `NEW_UPSTREAM_TOKEN` from your secret manager or a secure prompt before running the command: ```shell bsfw firewall credentials --firewall --upstream --token "$NEW_UPSTREAM_TOKEN" ``` Use the upstream's `id` from the firewall config, not its endpoint. Fields you do not pass stay unchanged, so rotating a token does not require re-entering a username and password. Alternatives: `--user`/`--password` flags, `--stdin` with a JSON body (`{"token": "..."}`), or an interactive prompt when run without flags in a terminal. In the dashboard: open the firewall's **Upstreams**, edit the upstream, and replace the credential field. Existing values show as a placeholder meaning "keep the stored value"; leave it untouched to keep, overwrite to replace, clear it to remove. Rotation involves the registry as well: create the new credential there first, set it on the firewall, then revoke the old one at the registry. In that order, no request is left holding a credential that no longer works. ## Credentials for an artifact host [#credentials-for-an-artifact-host] Some registries serve package files from a different host than their metadata. Credentials set on the upstream are only sent to the upstream's own host, so each artifact host that needs authentication requires its own entry. Composer is the current case: most Packagist dist URLs use `api.github.com`, while other packages can name another host. See [Artifact host credentials](/firewall/references/upstreams#artifact-host-credentials). For `api.github.com`, set the GitHub token on the upstream's edit form, or through the API: ```shell curl -X PUT \ -H "Authorization: Bearer $BYTESAFE_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"token\": \"$NEW_ARTIFACT_TOKEN\"}" \ "https://eu-sov-1.bytesafecloud.eu/v1//config//upstreams//artifact-credentials/api.github.com" ``` It needs the same `fw:update:credentials` permission, is encrypted at rest the same way, and merges per field, so rotating one field leaves the others alone. ## Verify [#verify] Install a package that only the private upstream serves, then check that the log entry contains its upstream ID: ```shell bsfw logs --firewall ``` A wrong credential does not show up as an authentication error on the client. That follows from how [resolution works](/firewall/concepts/upstreams-and-caching): the firewall asks each of its upstreams in turn, and one upstream rejecting it must not break a request that another upstream can answer, so it skips that upstream and serves what the others return. If the private registry is the only upstream holding the package, nothing is left to serve and the install fails, with an error that names the package rather than the credential. See [Failure behavior](/firewall/references/upstreams#failure-behavior) for how that failure reaches each client. If private packages disappear after a rotation, check the credential first. The firewall records the rejection in its log as an upstream error, naming the upstream and the status it returned. That is the quickest way to confirm it: run `bsfw logs --all-users --kind error --since 24h`, or set **Status** to **Error** on the dashboard's Logs page. See [Check for failing upstreams](/firewall/how-to/view-logs#check-for-failing-upstreams). Every change is recorded in the [audit log](/firewall/how-to/review-audit-log) as `firewall.credentials.upsert`, with the actor and which fields changed, never the values. ## Just-in-time credentials [#just-in-time-credentials] Some organizations do not want upstream secrets stored server-side at all, even encrypted. For that case, a client can carry the upstream credentials inside its own token per request, using the credential envelope (`env-` token format): the firewall uses the bundled credentials for the named upstreams instead of stored ones, and discards them with the request. The envelope format is documented in the [tokens reference](/firewall/references/tokens#credential-envelope). Building the envelope is currently an integration task (base64-encoded JSON around an existing token); there is no dashboard flow for it. ## Who can do this [#who-can-do-this] `fw:update:credentials` is granted to the admin team and the firewall's owning team by default, and can be granted per firewall to other teams. Service Access Tokens can never hold it; credential changes always trace to a person. See [Permissions](/firewall/references/permissions). ## Related [#related] * How-to: [Configure upstreams](/firewall/how-to/configure-upstreams), [Review the audit log](/firewall/how-to/review-audit-log) * Reference: [Upstreams](/firewall/references/upstreams), [Tokens](/firewall/references/tokens), [CLI](/firewall/references/cli#bsfw-firewall-credentials) * Concept: [Upstreams and caching](/firewall/concepts/upstreams-and-caching) # Migrate from previous-generation Bytesafe (/firewall/how-to/migrate-from-previous-generation) This guide is for teams moving from **Bytesafe**, the previous generation with hosted registries, to the **Dependency Firewall**, the new generation. It walks the migration end to end: review what you run today, create firewalls, move private packages to a repository manager, recreate policies and plugins as rules, and test before switching. Both generations can run at the same time. Nothing here touches your production setup until the switch at the end. The steps use the dashboard. Everything can also be managed as JSON through the CLI and API; see [Config as code](/firewall/getting-started/config-as-code) if you prefer that route. The previous generation of Bytesafe Dependency Firewall reaches end of life on **December 31, 2026**. Both generations can run at the same time while you prepare and test your migration, so start well ahead of that date. Questions or help planning the migration: [support@bytesafe.dev](mailto:support@bytesafe.dev). ## What changes [#what-changes] Bytesafe combined a firewall with a hosted package registry: packages were stored in your registry, and policies and plugins acted on the stored contents. The Dependency Firewall is a standalone security control. Every request is checked against your rules as it happens, and allowed or blocked on the spot. That changes where things live: * **Policy** moves from policies and plugins to [rules](/firewall/concepts/rule-evaluation). Rules can do more, and the mapping table below covers every policy and plugin. * **Private packages** move to a repository manager such as Artifactory, Nexus, GitLab, GitHub Packages, Azure Artifacts, or AWS CodeArtifact. If you already run one, you keep it: the firewall [integrates with the major repository managers](/firewall/how-to/integrate-repository-managers), so the manager keeps storing your packages while the firewall enforces policy. ## Why migrate [#why-migrate] The migration is work, so it is worth knowing what you get for it. The differences you notice day to day: * **Every block is explained.** A blocked install names the package, the version, and a request ID that [finds the log entry](/firewall/how-to/investigate-blocked-install) with the matched rule and requester. * **One rule model instead of a fixed set of policies.** A rule combines an [execution phase](/firewall/concepts/execution-phases), a [selector](/firewall/references/rules-and-selectors) (name, version range, upstream, or a computed check such as vulnerability, license, or malware data) and an effect. Conditions combine, and any rule can be scoped to one package, one prefix, or one upstream. * **Policy is reviewable.** Exceptions carry a reason and an optional expiry, changes land in an [audit log](/firewall/how-to/review-audit-log), and the whole configuration is one JSON document that fits in git. * **New controls.** Alongside npm, PyPI, Maven, NuGet, Go, Cargo, Composer, and Conda, there are checks the previous generation had no equivalent for: malware scanning, secrets detection on publish, and trust downgrade detection. The full feature comparison: [what's new in the next generation](https://bytesafe.dev/firewall-next-gen). ## The migration [#the-migration] ### Review your current Bytesafe setup [#review-your-current-bytesafe-setup] The migration goes smoothly when you know what you are replacing, so start with a short inventory. For each Bytesafe registry, note: * the ecosystem and what the registry is for, * which packages are private or internal, * which policies and plugins are enabled, and what each one is meant to achieve, * upstreams and who owns their credentials, * users, teams, and CI tokens, * everywhere clients point at Bytesafe: search for your `.bytesafe.dev` URL in client config files committed to repositories, CI configuration, and build images. The policies and plugins list becomes your worksheet for the mapping step. Capture the intent behind each one, not just its settings: some are replaced by a different mechanism with the same outcome. ### Create your firewalls [#create-your-firewalls] A firewall replaces a Bytesafe registry as the thing clients talk to. It holds configuration only, so creating one takes a minute. Create at least one firewall per ecosystem. Create more when different teams or environments need different rules, for example production and experimentation. In the dashboard, create a firewall, name it, and pick the ecosystem. The ecosystem's public registry is added as an upstream automatically, so the firewall proxies public packages right away. For additional firewalls, two fields in the create dialog save repetition: * **Inherit configuration from** keeps the new firewall in sync with a parent: rules, exceptions, and upstreams from the parent apply automatically, also when the parent changes later. See [Share configuration across firewalls](/firewall/how-to/share-configuration-across-firewalls). * **Template** copies another firewall's upstreams, rules, and exceptions once, as a starting point you then change freely. ### Connect your repository manager [#connect-your-repository-manager] The firewall is not a package store, so private packages are served by your repository manager through the firewall. Skip this step if you have no private packages and fetch directly from public sources. The standard setup keeps the firewall in front: developers and CI talk to the firewall, and the repository manager is one of its upstreams. On the firewall's **Upstreams** tab, add the manager's repository URL and credentials, then: * Enable **Internal registry** on the upstream if it hosts your internal packages. This replaces the internal flag in Bytesafe: when an internal upstream knows a package name, the firewall serves that name only from internal upstreams, which [prevents dependency confusion](/firewall/how-to/prevent-dependency-confusion). * Set the upstream as the publish target if developers or CI publish through the firewall. Details, including per-vendor instructions and other layouts such as keeping the repository manager in front, are in [Integrate with repository managers](/firewall/how-to/integrate-repository-managers). ### Move private packages out of Bytesafe [#move-private-packages-out-of-bytesafe] Your repository manager becomes the home of your private packages, so packages stored only in Bytesafe need to move there. Bytesafe pushes stored packages to a configured upstream directly from the UI, which makes this the easy part. First add the repository manager's hosted repository as an upstream of the Bytesafe registry, with publish credentials. Then push at whichever level fits: * **A whole registry:** on the registry's **Packages** tab, select **Push all**. Pick the target upstream, and enable **Push internal packages only** to skip public packages, which the firewall can fetch from the public registry anyway. * **One package:** open the package and select **Push all versions**. * **One version:** open the version and select **Push**. Versions that already exist at the target are skipped, so a push is safe to run again. Start with one test package, check that it installs from the repository manager, then push the rest and compare package counts. Keep the Bytesafe registry as it is for now; it stays your fallback until after the switch. ### Recreate policies and plugins as rules [#recreate-policies-and-plugins-as-rules] This is the heart of the migration. A [rule](/firewall/references/rules-and-selectors) has three parts: which packages it matches, [where it applies](/firewall/concepts/execution-phases) (version resolution, download, or publish), and what happens on a match (block, log, or both). When a rule blocks something you need anyway, an [exception](/firewall/how-to/manage-exceptions) waives it for that package, with a reason and an optional expiry. Create rules on the firewall's **Rules** tab. Each linked guide shows the exact settings. Every Bytesafe policy has a replacement: | Bytesafe policy | Replacement | Notes | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Allow-only](https://docs-v1.bytesafe.dev/policies/allow-only/) | One rule that blocks everything, plus an [exception](/firewall/how-to/manage-exceptions) per approved package | Approvals become documented exceptions instead of list entries | | [Block](https://docs-v1.bytesafe.dev/policies/block/) | A rule matching the package name, with a version or a version range, and the block effect | Names and exact versions match as written or with [one `*` wildcard](/firewall/references/rules-and-selectors#wildcards). A SemVer range goes in the rule's **Version Range** field and needs no rewriting on npm; other ecosystems use [their own notation](/firewall/references/rules-and-selectors#version-ranges) | | [Block downstream](https://docs-v1.bytesafe.dev/policies/block-downstream/) | An install-only firewall | A firewall only accepts publishes when a [publish target](/firewall/how-to/configure-upstreams#choose-a-publish-target) is set. Leave it unset and every publish is rejected, while installs work as normal | | [Block install scripts](https://docs-v1.bytesafe.dev/policies/block-install-scripts/) | [Block install-time code execution](/firewall/how-to/block-install-scripts) rule | Blocks in real time instead of holding packages. Covers npm, PyPI, and Composer | | [Quarantine new](https://docs-v1.bytesafe.dev/policies/quarantine-new/) | Blocking rules plus exceptions | Nothing is held for review. A blocked version stays out until you approve it with an exception, and builds keep using allowed versions meanwhile | | [Delay upstream](https://docs-v1.bytesafe.dev/policies/delay-upstream/) | [Delay new versions](/firewall/how-to/delay-new-versions) rule | Same protection. The window is a max age in hours, and it can be scoped to specific packages or upstreams like any other rule | | [Freeze](https://docs-v1.bytesafe.dev/policies/freeze/) | Block publishing; pin installs with an allow-only setup if needed | The firewall hosts no packages, so there is nothing to freeze. Retention lives in the [repository manager](/firewall/how-to/integrate-repository-managers) | | [Immutable versions](https://docs-v1.bytesafe.dev/policies/immutable-versions/) | A repository manager setting | Overwrite protection belongs to the system that stores packages | Plugins map the same way. Most become rules with a [selector function](/firewall/references/selector-functions): they match what is known about a package rather than its name: | Bytesafe plugin | Replacement | Notes | | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | [Vulnerability scanner](https://docs-v1.bytesafe.dev/plugins/vulnerability-scanner/) | [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages) rule | Blocks the download itself, with CVSS and EPSS thresholds. Each request uses current [advisory data](/firewall/references/advisory-sources) | | [License Compliance](https://docs-v1.bytesafe.dev/plugins/license-compliance/) | [Enforce license policy](/firewall/how-to/enforce-license-policy) rule | Blocks or logs by license list instead of opening issues | | [Deprecated packages](https://docs-v1.bytesafe.dev/plugins/deprecated-packages/) | [Block deprecated packages](/firewall/how-to/block-deprecated-packages) rule | Covers npm deprecations, PyPI yanked releases, and NuGet deprecations | | [Forward](https://docs-v1.bytesafe.dev/plugins/forward/) | The firewall's publish target | A publish that passes the rules is forwarded to the target upstream | | [Version auto increment](https://docs-v1.bytesafe.dev/plugins/version-auto-increment/) | Your CI or release workflow | The firewall never changes a package | The mapping above only covers what Bytesafe could do. While you are here, also look at the controls that are new: [malware blocking](/firewall/how-to/block-malware), [secrets scanning on publish](/firewall/how-to/scan-for-secrets), and [trust downgrade detection](/firewall/references/selector-functions#trust-downgrade) for npm releases published with weaker provenance than earlier versions, the pattern of an account takeover. And everything is visible: every rule decision lands in the [firewall log](/firewall/how-to/view-logs) with the package, rule, and requester, every configuration change in the [audit log](/firewall/how-to/review-audit-log), and [webhooks](/firewall/how-to/receive-webhook-events) push both to your own systems. ### Move developers and CI [#move-developers-and-ci] Clients authenticate to the firewall with access tokens, and each install is logged under the identity behind the token. Create [Personal Access Tokens](/firewall/getting-started/access-tokens) for developers and Service Access Tokens for CI systems, one per system so logs stay attributable. GitHub Actions can skip long-lived tokens with [Trusted Builders](/firewall/getting-started/github-actions). Then point each client at the firewall endpoint, shown on the firewall's **Setup** tab, in the form `https://eu-sov-1.bytesafecloud.eu/v1//npm//` for npm. The [ecosystem guides](/firewall/ecosystems) walk the exact configuration per package manager. Sweep your repositories, CI configuration, and build images for old Bytesafe registry URLs so nothing keeps installing around the firewall. ### Test before switching [#test-before-switching] Run a pilot team or a copy of a real CI pipeline against the new firewall while production still runs on Bytesafe. When you are unsure what a rule will hit, create it with only **Log** enabled and **Block** off. It records every match in the [firewall log](/firewall/how-to/view-logs) without failing a single build. Review the matches, add exceptions where needed, then enable **Block**. Check that: * a public package installs through the firewall, and a private one installs from the repository manager, * each migrated rule fires once and the log entry names the rule, package, and requester, * a blocked install shows an error with a request ID, * an exception lets through exactly its package and nothing else. ### Switch over [#switch-over] Push any packages added to Bytesafe since your first migration run, then switch developer and CI configuration to the firewall endpoints. Watch the firewall log and your builds for the first days: missing packages and authentication errors show up immediately and are usually a credential or URL detail. Keep the Bytesafe registry untouched for an agreed period as a fallback. Once the new setup has proven itself, retire the old endpoints and tokens. Do not leave the fallback in place indefinitely: the previous generation reaches end of life on December 31, 2026. ## Frequently asked questions [#frequently-asked-questions] Work through the review queue before migrating: release what should be released, discard the rest. The Dependency Firewall holds no packages for review, so there is no queue to carry over. The same workflow becomes a blocking rule plus an exception added after review, and builds keep using approved versions while a new one waits. Only if you have private packages. Teams that only consume public registries point their clients at the firewall and are done. Rule selectors have a **Version Range** field that takes them directly. On npm firewalls the syntax is the same SemVer grammar the Block policy used, so a definition such as `0.0.1 - 0.0.3 || >2.0.1` carries over unchanged. To recreate one Block policy: 1. Open the firewall, go to **Rules**, and select **Add Rule**. 2. Choose **Start from a blank rule** on the intent step, then select **Continue**. 3. Set **Package** to the package name, exactly as the registry spells it. 4. Put the range in **Version Range** and leave **Version** empty. Setting both narrows the rule to versions that satisfy each one. 5. Choose the [execution phase](/firewall/concepts/execution-phases): **Versions** filters the matching versions out of what clients can resolve, **Download** stops the fetch outright. Then set the effect to block and log. Other ecosystems keep their own notation: Maven uses `[1.0,2.0)`, PyPI uses PEP 440 specifiers like `>=1.0,<2.0`, and NuGet takes SemVer ranges rather than its own bracket intervals. conda has no range support, so the field is not shown there. [Version ranges](/firewall/references/rules-and-selectors#version-ranges) lists the notation per ecosystem. The range is checked when you save, so a range the ecosystem cannot parse is rejected then and there. Two cases are still better expressed without a range. A whole major version reads more clearly as the version wildcard `1.*`, and a policy guarding against releases that are too new belongs in a [delay rule](/firewall/how-to/delay-new-versions), which matches on publish date and keeps working as new versions appear. They were registry features, and the firewall hosts no packages. After migration your repository manager owns stored packages and their lifecycle. Publishing through the firewall forwards to the publish target once the rules pass; releasing a package under a new version, what promote did, belongs in your CI release workflow. ## Get help [#get-help] We are here to help. If you have questions or want advice on any part of your migration, contact [support@bytesafe.dev](mailto:support@bytesafe.dev). ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation), [Execution phases](/firewall/concepts/execution-phases) * How-to: [Integrate with repository managers](/firewall/how-to/integrate-repository-managers), [Configure upstreams](/firewall/how-to/configure-upstreams), [Manage exceptions](/firewall/how-to/manage-exceptions) * Reference: [Rules and selectors](/firewall/references/rules-and-selectors), [Selector functions](/firewall/references/selector-functions), [Firewall config](/firewall/references/firewall-config) * Getting started: [Quickstart](/firewall/getting-started/quickstart), [Config as code](/firewall/getting-started/config-as-code) # Prevent dependency confusion (/firewall/how-to/prevent-dependency-confusion) Dependency confusion is an attack on private package names: an attacker publishes a package on the public registry under the same name as one of your internal packages, often with a high version number, and waits for a build to resolve the public copy instead of yours. The firewall closes this with its internal upstream model, plus an optional rule as a second layer. ## How the firewall resolves internal names [#how-the-firewall-resolves-internal-names] When any upstream marked **internal** knows a package, the firewall serves versions only from internal upstreams. Public copies of that name are not merged in, no matter their version numbers. ## Step 1: mark your private registry as internal [#step-1-mark-your-private-registry-as-internal] In the dashboard, open the firewall, go to **Upstreams**, edit your private registry, and enable **Internal registry**. As JSON: ```json { "id": "e2de947a", "endpoint": "https://npm.acme.internal", "internal": true, "cached": false } ``` See [Configure upstreams](/firewall/how-to/configure-upstreams) for the full upstream setup. ## Step 2 (optional): reserve your package names [#step-2-optional-reserve-your-package-names] Step 1 protects the names your internal registry serves today. Names it does not serve are still resolved from the public registry: a package you deleted internally, or a prefix you have reserved but not published under yet. A rule closes that gap. It matches the naming convention your internal packages use and blocks it from external upstreams: In the dashboard, open **Rules**, select **Add Rule**, choose **Hide packages or versions** on the intent step, and select **Continue**. * Execution phase: **Versions** (called `versions` in config, API, and CLI) * Selector: package name **`@acme/*`** (your internal prefix), upstream type: **External** * Effect: **block** and **log** As JSON: ```json { "id": "acme-scope-internal-only", "executionPhase": "versions", "selector": { "packageName": "@acme/*", "upstreamType": "external" }, "ruleEffect": { "block": true, "log": true }, "description": "Never serve @acme/* from external registries" } ``` A name under your prefix can now only come from an internal upstream, whether or not your internal registry currently lists it. Use whatever marks your internal packages: an npm scope, a Maven group prefix (`com.acme.*`), a Go module path prefix. ## Verify [#verify] Request an internal package and confirm the log entry contains your internal upstream ID, not the public upstream ID: ```shell bsfw logs --firewall ``` If you added the step 2 rule, also try a name under your prefix that only exists publicly; the request should be blocked and logged with the rule ID. ## Related [#related] * Concept: [Upstreams and caching](/firewall/concepts/upstreams-and-caching) * Reference: [Rules and selectors](/firewall/references/rules-and-selectors), [Upstreams](/firewall/references/upstreams) * How-to: [Configure upstreams](/firewall/how-to/configure-upstreams) # Receive webhook events (/firewall/how-to/receive-webhook-events) Webhooks push namespace events out of Bytesafe as they happen: the firewall POSTs signed JSON batches to an HTTPS endpoint you run, so blocked installs, configuration changes, and publishes land in your SIEM, chat, or ticketing system without polling. Webhooks belong to the namespace. Creating, editing, testing, and deleting them needs the `ns:update:webhooks` [permission](/firewall/references/permissions) (part of the admin team's set); reading them and their deliveries only needs `ns:access`. ## Create a webhook [#create-a-webhook] In the dashboard, open **Webhooks** and click **New Webhook**: * **Name** and optional **Description** * **Endpoint URL**: a publicly reachable HTTPS endpoint you control * **Subscribed events**: at least one event type (next section) * **Enabled**: on On create, the dialog shows the webhook's **Signing secret** (`whsec_...`). Copy it into your receiver's configuration; managers can view or rotate it later on the webhook's **Overview** tab. The API equivalent is `POST /v1//webhooks`; the `201` response carries the plaintext `secret` once alongside the webhook. See the [API reference](/firewall/references/api#webhooks). ## Choose event types [#choose-event-types] Firewall traffic events, one per log entry, plus publish outcomes: * `firewall.blocked`: a 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 * `package.published`, `package.unlisted`: a publish passed the upload rules and was forwarded upstream (unlist is NuGet only) Audit events mirror the [audit action catalog](/firewall/references/audit-log#action-catalog) as `audit.`, for example `audit.team.create` or `audit.firewall.rule.upsert`. Configuration events (`config.firewall.*`, `config.webhook.*`) are the automation feed: they fire on successful changes only and carry the complete resource after the change, so a consumer can mirror or react to the new state without a follow-up API call. The matching `audit.*` event is the security-log counterpart. A trailing `.*` subscribes to a prefix: `firewall.*`, `audit.*`, `config.*`, `package.*`, or a narrower slice like `audit.team.*`. A bare `*` is rejected; combine the group wildcards for everything. Traffic events (`firewall.*` and `package.*`) can additionally be scoped to specific firewalls in the picker; they are delivered for all firewalls unless you select specific ones. Audit and configuration events are never restricted by the selection. Two things the picker cannot change: * System events (`webhook.rate_limited`, `webhook.test`) are always delivered, so consumers can detect throttling and dropped events. * Firewall events from the `versions` [execution phase](/firewall/concepts/rule-evaluation#execution-phases) are never delivered; one install can evaluate thousands of versions. Webhooks carry `download` and `upload` decisions only, the same decisions worth alerting on. The picker also lists **Vulnerabilities** event types as coming soon; they are disabled and not emitted yet. ## Verify the endpoint [#verify-the-endpoint] On the webhook's **Overview** tab, use **Send a test event**: pick the generic test event or a sample firewall event and click **Send test**. The result shows the HTTP status, duration, and your endpoint's response body, and the attempt is recorded as a delivery marked `test`. Subscriptions do not apply to tests. The API equivalent is `POST /v1//webhooks/{webhook}/test`. Your receiver should verify every delivery's signature before trusting it. Deliveries follow the [Standard Webhooks](https://www.standardwebhooks.com/) convention, so its libraries work as-is: verify the `webhook-signature` header (HMAC-SHA256 over `webhook-id`, `webhook-timestamp`, and the raw body) with your signing secret. Header formats and a worked verification example: [Webhooks reference](/firewall/references/webhooks#signing). The payload shapes are described by a published [JSON Schema](/firewall/references/webhooks#json-schema), linked from the picker, for generating receiver types. ## Handle deliveries reliably [#handle-deliveries-reliably] * Respond with a `2xx` quickly; process the payload asynchronously if the work is slow. Attempts time out after 10 seconds, and redirects count as failures. * Expect batches: one delivery carries up to 100 events in a single `events` array. * Deliveries are at-least-once. The `webhook-id` header is stable across retries; deduplicate on it. * Watch for `webhook.rate_limited` events with reason `buffer_overflow`: they mean events were dropped and there is a gap to reconcile against the [firewall log](/firewall/references/logs) or [audit log](/firewall/references/audit-log), which remain the source of truth. ## Rotate the secret [#rotate-the-secret] On the **Overview** tab, the **Signing secret** card has **Reveal** and **Rotate**. Rotating generates a new secret immediately; the previous secret keeps verifying deliveries for 24 hours, and during that window every delivery carries two signatures (space-separated in `webhook-signature`), so a receiver that matches either signature never breaks mid-rotation. ## When deliveries fail [#when-deliveries-fail] A failed delivery (non-`2xx`, timeout, or connection error) is retried on a backoff schedule of 30 seconds, 2 minutes, 10 minutes, 30 minutes, and 1 hour; a `Retry-After` header on `429` or `503` responses is honored up to 1 hour. When all attempts fail, that batch is recorded as `failed` and the webhook is quarantined for 24 hours: deliveries pause, new events keep buffering, and delivery resumes automatically when the quarantine expires. The webhook's page shows a banner with the reason and a **Release now** button to resume earlier once the endpoint is fixed. Quarantine pauses deliveries to your endpoint; it never holds packages. ## Monitor deliveries [#monitor-deliveries] The **Deliveries** tab keeps the recent deliveries for debugging; click a row for the exact signed payload and your endpoint's response. The **Metrics** tab charts delivered and failed deliveries, events sent and dropped, and delivery latency over selectable periods. Field-level detail: [Webhooks reference](/firewall/references/webhooks#delivery-records). ## Related [#related] * Concept: [Observability](/firewall/concepts/observability) * Reference: [Webhooks](/firewall/references/webhooks), [API](/firewall/references/api#webhooks), [Audit log](/firewall/references/audit-log), [Permissions](/firewall/references/permissions) * How-to: [Review the audit log](/firewall/how-to/review-audit-log), [View logs](/firewall/how-to/view-logs) # Review the audit log (/firewall/how-to/review-audit-log) The audit log records control-plane actions: configuration and policy changes, token and team management, permission denials, and OIDC exchanges. It answers "who changed the firewall", where the [firewall log](/firewall/how-to/view-logs) answers "what happened to that package request". Every namespace has one; events cannot be edited or deleted from the API. ## Read it [#read-it] In the dashboard, open **Audit**. The table shows Timestamp, Action, Resource, Actor, IP, and Status. Select an event for its API `outcome`, request metadata, and action-specific detail. The detail shows both the actor's name and raw user ID, and links the resource to its page where one exists. From the terminal, the same events with the same filters. The first command is one firewall's change history, the second is everything that was rejected since a date: ```shell bsfw audit --resource-type firewall --resource-id bsfw audit --outcome failure --from 2026-08-01 -o json ``` Name the firewall by its ID, not its display name. The table is a compact view; `-o json` returns complete events, including the action-specific detail. `--from` and `--to` take a date or an RFC 3339 timestamp, see [dates and durations](/firewall/references/cli#dates-and-durations). Via the API: ```shell curl -H "Authorization: Bearer $BYTESAFE_TOKEN" \ "https://eu-sov-1.bytesafecloud.eu/v1//audit?action=firewall.config.upsert&from=2026-07-01T00:00:00Z" ``` Filters: `action`, `resourceType`, `resourceId`, `actorUserId`, `outcome` (`success`/`failure`), `from`/`to` (RFC 3339), `limit` (up to 1000), `offset`. `actorUserId` takes the raw user ID, not a name; copy it from an event's detail view. The full event shape and action catalog are in the [audit log reference](/firewall/references/audit-log). ## Reviews worth running [#reviews-worth-running] **Who changed policy?** Filter `action=firewall.rule.upsert` (and `firewall.exception.upsert`, `firewall.config.upsert`). Each event names the actor and the firewall. If you run [config as code](/firewall/how-to/manage-config-as-code), anything here not coming from the CI identity is drift. **Denied attempts.** `action=authz.denied` lists actions users tried without permission; the event data names the missing permission. Repeated denials are either someone probing or someone who needs a grant, and both are worth knowing. **Failed logins and bad tokens.** `action=auth.failed` records authentication failures with a hashed token identifier when the requested namespace can be assigned to one tenant. This makes retries correlatable without exposing the token. An attempt that cannot be assigned safely is not shown in a tenant's namespace audit log. **Token lifecycle.** `token.personal.create`, `token.service.create`, `token.delete`. Service token creation is admin-gated, so this list should be short and recognizable. **CI identity exchanges.** `oidc.exchange` events record successful exchanges and failures that identify a configured Trusted Builder, including repository mismatch and verification failure. Malformed tokens and unknown audiences cannot be assigned to a tenant and are not shown in its audit log. For a known builder, this is the first place to look when a [GitHub Actions](/firewall/getting-started/github-actions) workflow cannot get a token. **Exception governance.** `firewall.exception.upsert` paired with the exception's reason and expiry in the event data shows who is granting waivers and how often; expired-and-renewed patterns show up here. ## Correlate with the firewall log [#correlate-with-the-firewall-log] Audit events carry the same `requestId` and `interactionId` fields as firewall log entries. An interaction ID appended to a CI token ties the pipeline's package requests (firewall log) and its config changes (audit log) into one story. See [Access Tokens](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids). ## Verify [#verify] Make a harmless change (rename a rule description, create and delete a token) and confirm it appears in `bsfw audit` with your user as the actor. If you review on a schedule, use JSON output or the API and alert on `authz.denied` and `auth.failed` counts. ## Related [#related] * Reference: [Audit log](/firewall/references/audit-log), [CLI](/firewall/references/cli#bsfw-audit) * Concept: [Observability](/firewall/concepts/observability) * How-to: [View logs](/firewall/how-to/view-logs), [Manage config as code](/firewall/how-to/manage-config-as-code) # Scan for secrets on publish (/firewall/how-to/scan-for-secrets) The `secrets` selector function scans the contents of a package being published for leaked credentials: cloud provider keys, API tokens, private keys, and the firewall's own [access tokens](/firewall/references/tokens). It runs on the **upload** phase only, so a matching block rule rejects the publish before it reaches the [publish target](/firewall/how-to/configure-upstreams#choose-a-publish-target). It applies to the ecosystems that accept publishes through the firewall: npm, PyPI, Maven, NuGet, and Cargo. Detection covers several hundred credential formats, plus Bytesafe's own token formats, so a package that would leak the very token used to publish it is caught too. Matched secret values are redacted before anything is logged; the raw secret never appears in a log entry. ## Add the rule [#add-the-rule] In the dashboard, open the firewall, go to **Rules**, and select **Add Rule**. Choose **Check packages you publish** on the intent step, then select **Continue**. * Execution phase: **Upload** * Selector function: **Secrets** * Effect: **block** and **log** As JSON: ```json { "id": "block-secrets-on-publish", "executionPhase": "upload", "selector": { "function": { "name": "secrets", "options": { "ignoredFiles": [], "excludeTestFiles": false } } }, "ruleEffect": { "block": true, "log": true }, "description": "Block publishes containing leaked credentials" } ``` `options` can be omitted entirely; both fields default to off. To apply the JSON without the dashboard: export the config with `bsfw firewall export`, add the rule to the `rules` array, and import it back. See [firewall import](/firewall/references/cli#bsfw-firewall-import). If your teams publish frequently and you want to gauge the false positive rate first, start with `{ "block": false, "log": true }` and watch the log for a few days before enabling block. ## What a match looks like [#what-a-match-looks-like] The rule matches when at least one potential secret remains after the options below are applied. The publish is rejected with an error naming the block (see your [ecosystem page](/firewall/ecosystems) for what the client shows), and the log entry's message reads: ```text 2 potential secrets detected ``` The log entry's rule output lists each finding with the matched rule, the file path within the package, the line, and a fingerprint. The secret value itself is always `REDACTED`: ```json { "message": "2 potential secrets detected", "findingsTotal": 2, "truncated": false, "findings": [ { "rule": "aws-access-token", "file": "package/config/prod.env", "line": 12, "secret": "REDACTED", "fingerprint": "package/config/prod.env:aws-access-token:12" }, { "rule": "bytesafe-personal-access-token", "file": "package/scripts/publish.sh", "line": 3, "secret": "REDACTED", "fingerprint": "package/scripts/publish.sh:bytesafe-personal-access-token:3" } ] } ``` The `findings` list is capped at 20 entries; `findingsTotal` and `truncated` carry the full count. Paths inside nested archives are separated by `!`. ## Handle false positives [#handle-false-positives] Secret detection is pattern-based, so example keys in documentation or test fixtures can match. Fix the finding at the source when you can: a credential that only looks fake is indistinguishable from a real one to everyone downstream. When the content is intentional, the rule has two options to ignore it: | Option | Dashboard label | Meaning | | ------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ignoredFiles` | Ignored Files | List of file paths whose findings are ignored. Use the path exactly as shown in the log entry's `file` field. Each pattern is an exact path or has a single leading or trailing `*` [wildcard](/firewall/references/rules-and-selectors#wildcards); for example `package/docs/*` or `*.pem`. Paths are always matched case-sensitively | | `excludeTestFiles` | Ignore findings in test files | Ignore findings in paths that look like test content: a path segment named `test`, `tests`, `__tests__`, `spec`, `specs`, `testdata`, or `fixtures`, or a file name ending in `_test`, `.test`, or `.spec` before its extension | Prefer these options over an [exception](/firewall/how-to/manage-exceptions): an exception waives the whole rule for the matching package, options only silence the named findings. Use an exception when a specific package must go out now and you will clean it up after. ## Limits [#limits] * Artifacts larger than 256 MB are not scanned. * A scan is bounded to 30 seconds. * Archives nested inside the package are scanned three levels deep, and base64-encoded content is decoded up to three levels, so encoded credentials are still found. * JavaScript and CSS source maps (`*.js.map`, `*.css.map`, and similar) are skipped: their embedded `sourcesContent` duplicates the shipped sources, where a real secret would also be found. ## Verify [#verify] Publish a throwaway package containing a documented example credential, such as AWS's example access key ID `AKIAIOSFODNN7EXAMPLE`, through the firewall. The publish should be rejected, and the log entry should show the finding with the file and line. Never use a real credential to test. ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation) * Reference: [Selector functions](/firewall/references/selector-functions#secrets), [Logs](/firewall/references/logs) * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions), [Block malware](/firewall/how-to/block-malware) # Share configuration across firewalls (/firewall/how-to/share-configuration-across-firewalls) A firewall can inherit configuration from another firewall in the same namespace by naming it as its parent. The parent's rules, exceptions, and upstreams then apply to the child's traffic, live: change the parent and every inheriting firewall picks it up within a few seconds, no copying or re-import. This is how one baseline policy serves a whole organization. Keep the organization's non-negotiables (malware blocking, vulnerability thresholds, approved upstreams) in one baseline firewall per ecosystem, and let each team's firewall inherit it and add its own rules on top. ## Set a parent [#set-a-parent] In the dashboard, the firewall form (create or edit) has an **Inherit configuration from** selector. It offers firewalls of the same ecosystem and hides any choice that would create a loop. The parent can be any firewall; nothing special marks it as a baseline, and it can keep serving its own traffic. In config as code, the parent is the `parentId` field: ```json { "id": "3e7f5cb806f26e3beae2", "ecosystem": "npm", "parentId": "43b5799c6cb335570cad", "rules": [], "exceptions": [], "upstreams": [] } ``` Setting or changing the parent is a configuration change (`fw:update:config`). The server rejects a parent that does not exist, has a different ecosystem, or would close a cycle, including inheriting from itself. ## What the child serves [#what-the-child-serves] At request time the firewall resolves the parent chain into one effective configuration: | Part | Merge behavior | | -------------- | ------------------------------------------------------------------------------------------------ | | Rules | Union, parents first, the firewall's own last. That is the merge order, not the evaluation order | | Exceptions | Union, same order | | Upstreams | Union; a child upstream with the same `id` replaces the parent's | | Publish target | The child's own if set, otherwise the nearest ancestor's | | Permissions | Not inherited; access is granted per firewall | Three consequences: * **Inheritance is additive.** A child can add rules. It cannot delete, edit, or override an inherited one, and an inherited rule that blocks still blocks. * **Parents can inherit too.** Chains work (baseline → division → team) up to a depth of 32. * **Editing and export stay local.** The firewall's own configuration contains only its own entries plus `parentId`; `bsfw firewall export` does not embed inherited rules. The merge happens at serving time only. Inherited rules and the child's own end up in one set, evaluated cheapest selector first rather than parents before children; see [rule evaluation](/firewall/concepts/rule-evaluation). ## What a child can and cannot change [#what-a-child-can-and-cannot-change] | Inherited part | Can the child change it? | | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Rules | **No.** It cannot delete, edit, or disable an inherited rule. It can only add an exception to one, described below | | Exceptions | **No.** A parent's exception reaches the child as it stands | | Upstreams | **Yes.** An upstream with the same `id` as an inherited one replaces it, for example to point the child at a different mirror | | Publish target | **Yes.** Setting the child's own `publishTarget` sends its publishes elsewhere | The child cannot remove, edit, or disable a baseline rule. It can add a narrow exception, but only with permission from the firewall that owns the rule. ## Exceptions on an inherited rule [#exceptions-on-an-inherited-rule] An **exception** tells one rule not to block requests that match the exception's selector. Other rules still apply. An exception stored on a child applies on that child and on descendants that inherit from it. It does not apply upward to its parent or sideways to sibling firewalls. [Manage exceptions](/firewall/how-to/manage-exceptions) covers how to add one. An example. The org baseline blocks packages with a critical vulnerability. One team depends on a package that has one, and no fixed version exists yet. On the team's firewall, they open **Rules**, find the baseline rule under **Inherited rules**, and select **Add exception**. They choose the package and version, then add a reason and expiry date. The exception is stored on the team's firewall. It stops that inherited rule from blocking the selected version there and on any descendants, but it does not edit or disable the baseline rule. Other rules can still block the package. The parent and sibling firewalls are unchanged. When the exception expires, it stops matching. Inheritance guarantees the rule is present throughout the descendant chain. It does not guarantee that no descendant has an exception to it. That is why exceptions need permission from both sides, show up in the [audit log](/firewall/references/audit-log#exceptions-on-inherited-rules), and can be given an expiry date. ### Both firewalls have to allow it [#both-firewalls-have-to-allow-it] Adding or changing an exception on an inherited rule needs `fw:update:exception` on **two** firewalls: 1. **The child**, because that is where the exception is stored. 2. **The source firewall that owns the rule**, which may be any ancestor in the chain. If you have permission on only one, the save is refused. This requires approval from both the firewall storing the exception and the firewall responsible for the rule. If the source permission is missing, ask that firewall's owners. Removing an exception needs permission on the child only because deletion strengthens enforcement. Other matching exceptions, if any, can still apply. Two details worth knowing: * The same two-firewall check runs when you import a whole config or roll one back, not only when you save a single exception. Exceptions that were already stored and are unchanged are left alone, so an unrelated edit never trips over an old one. * An exception names its rule by ID. If that ID matches no rule, or matches two different rules in the chain, the save is refused rather than guessed at. ## See what a firewall inherits [#see-what-a-firewall-inherits] Open the firewall in the dashboard. The line under its name lists readable ancestors, closest parent first, each one a link: `inherits team-npm → org-baseline`. Read access is granted per firewall. If the immediate parent is unreadable, the line shows its ID without a link. If the visible chain continues past an unreadable ancestor, it ends with `→…`. Rules from hidden firewalls still apply to your traffic; you just cannot open those firewalls from here. The **Rules**, **Exceptions**, and **Upstreams** tabs show the firewall's own count first and an inherited count in parentheses. When every ancestor is readable, **Rules** showing `2 (+7)` means two rules of its own and seven from ancestors. If part of the chain is unreadable, Rules and Exceptions count only visible ancestors even though their inherited sections can still show entries from the whole chain. Upstreams counts inherited entries that remain in effect. Inherited entries never mix into the firewall's own list. They get their own section below it: | Tab | Section | What is in it | | ---------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Rules | Inherited rules | Rules from parent firewalls. You can add an exception here, if you have permission | | Exceptions | Inherited exceptions | Exceptions the parents set on their own rules | | Upstreams | Inherited upstreams | Upstreams from parent firewalls. A struck-through row is replaced by a nearer firewall in the chain with the same `id` | Every inherited row carries a **Source** badge naming the firewall it comes from. Nothing inherited can be edited or deleted here. The badge links to the firewall where it can be, and a change made there reaches every firewall that inherits it. If **Add exception** is greyed out on an inherited rule, hover it. The tooltip names the reason; see the troubleshooting entry below for what to do about each. ## Inheritance or cloning [#inheritance-or-cloning] These are different features. **Clone** (`fw:clone`, "use as template" when creating a firewall) copies a configuration once; the copy diverges freely afterwards. **Inheritance** (`parentId`) is a live link; the child follows every parent change. Use clone for a starting point, inheritance for an enforced baseline. ## Verify [#verify] * The firewall list shows the parent in **Inherits**. * Open the child. If its ancestors are readable, its **Rules** tab badge reads `0 (+1)` when the child has no rules of its own and inherits one. **Exceptions** follows the same visible-ancestor rule. **Upstreams** leaves out every overridden inherited entry because it is no longer in effect. * On the child's **Rules** tab, the **Inherited rules** section lists the parent's rules, each with a Source badge naming the parent. * Trigger an inherited rule through the child firewall (install a package the baseline blocks). Select its [log entry](/firewall/references/logs) to open the complete detail. It records the child firewall ID in `firewall` and the baseline firewall ID in `sourceFirewall`, which is how you tell baseline policy from team policy. ## Troubleshoot [#troubleshoot] A firewall that others inherit from cannot be deleted; the API answers 409 and names the inheriting firewalls. Clear or repoint `parentId` on each of them first. The selector only offers firewalls of the same ecosystem, and excludes the firewall itself and its descendants, because those would create a loop. The API enforces the same on save: self-parent, ecosystem mismatch, and circular inheritance are rejected. Do not change the baseline for one team. Add an exception on that team's firewall referencing the inherited rule's ID. It applies there and on descendants that inherit from it, but not on the parent or sibling firewalls. See [Manage exceptions](/firewall/how-to/manage-exceptions). Hover the button. The tooltip names one of three reasons: * You do not have `fw:update:exception` on this firewall. Ask an admin for it. * You do not have it on the firewall that owns the rule. Adding an exception to that rule needs permission there too, so ask that firewall's owners. * The rule's ID appears more than once in the chain, so the firewall cannot tell which one you mean. On one source firewall, create a replacement rule with a unique ID, then delete the duplicate rule. Parent and child must share an ecosystem, so a firewall's ecosystem cannot change while others inherit from it. Create a new firewall for the other ecosystem. Do not detach children merely to change an exported config: imports do not translate ecosystem-specific upstreams or rules. See the [firewall-config warning](/firewall/references/firewall-config#fields). ## Related [#related] * Concept: [Rule evaluation](/firewall/concepts/rule-evaluation), [Upstreams and caching](/firewall/concepts/upstreams-and-caching) * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions), [Manage config as code](/firewall/how-to/manage-config-as-code) * Reference: [Firewall config](/firewall/references/firewall-config), [Logs](/firewall/references/logs), [Permissions](/firewall/references/permissions) # View logs (/firewall/how-to/view-logs) A rule match with logging enabled produces a log entry: who requested which package, through which firewall, which rule fired, and whether the request was blocked. Logs are the place to answer "why did this install fail" and "what would this rule have blocked". They are not a record of every request; see [What is logged when](#what-is-logged-when) below. The log holds one other kind of entry: an upstream registry the firewall could not get an answer from. Nothing was blocked in those entries, and the request often succeeded through another upstream. See [Check for failing upstreams](#check-for-failing-upstreams). ## In the dashboard [#in-the-dashboard] Open **Logs** on a firewall. Each entry shows the timestamp, the block decision, the [execution phase](/firewall/concepts/rule-evaluation#execution-phases) (versions, download, or upload), the package and version, the matched rule, and the user. Users appear by name where the dashboard can resolve them; service tokens and removed users show the raw ID. From a log entry you can create an exception directly, prefilled with the rule and package. **Status** filters the table by outcome: * **Blocked**: a rule blocked the request. * **Allowed**: a rule or an exception matched and the request went through. * **Error**: the firewall could not get an answer from an upstream. The three are exclusive, and an upstream failure outranks a block: an entry that both blocked and recorded a failed upstream is filtered as **Error**. An error row carries an amber **Error** badge instead of Blocked or Allowed, and its rule column says which upstream failed and what came back. The rule column shows the rule's [description](/firewall/references/rules-and-selectors) when it has one, and its ID when it does not. A description is worth writing for that reason alone: it is what the log reads back to whoever is investigating. Filtering by a single firewall or a single user drops that column from the table, since every row would repeat the same value. Select a row to see the complete entry. The detail includes fields not shown as table columns, such as request, interaction, upstream, and, when applicable, the source firewall or exception. For an error row it also lists the endpoint, the exact URL that failed, and the status the upstream returned, or a network error when there was no response. The newest entries load first. **Load more** at the bottom of the table fetches the next batch of older entries, so you can page back as far as the retained history goes. New entries arriving while you page do not shift or duplicate the ones already on screen. ## From the terminal [#from-the-terminal] The [bsfw CLI](/firewall/references/cli#bsfw-logs) reads the same log the dashboard shows. Its table is a compact view built for scanning, so it leaves out some fields; add `-o json` or `-o ndjson` to get complete entries, including the request, exception, source firewall, and upstream IDs. One thing to know before the first run: without `--all-users`, you only see your own requests. A CI token has no user behind it, so it sees nothing at all unless you pass `--all-users`. ```shell export BYTESAFE_ENDPOINT=https://eu-sov-1.bytesafecloud.eu export BYTESAFE_NAMESPACE="" export BYTESAFE_TOKEN= bsfw logs --firewall ``` ### Filter entries [#filter-entries] ```shell bsfw logs --all-users --blocked --since 24h bsfw logs --firewall --phase download --search lodash ``` `--blocked` narrows the list to requests the firewall stopped, which is where a reported install failure is usually found. `--search` matches on user, ecosystem, package, version, and message, so the package name from the error message is a good search term. `--since` takes a duration counted back from now (`30m`, `24h`, `168h`) or a point in time (`2026-08-01`, or an RFC 3339 timestamp). There is no day unit, so a week is `168h`. See [dates and durations](/firewall/references/cli#dates-and-durations). ### Stream live [#stream-live] ```shell bsfw logs --firewall --follow ``` `--follow` keeps the connection open and prints entries as they happen. Useful while testing a new rule: run the install in one terminal and watch the decision in the other. A stream starts from the newest entries and goes forward, so `--since` and `--limit` have no effect on it; the other filters do. ### Filter by user [#filter-by-user] ```shell bsfw logs --user user-7d2f... bsfw logs --all-users ``` `--user` and `--all-users` are mutually exclusive, and `--user` takes the raw user ID, not a name or email. Omit both to see your own entries. ### Read complete entries [#read-complete-entries] ```shell bsfw logs --firewall --search lodash -o json bsfw logs --all-users --follow -o ndjson ``` `-o json` returns the whole response as one document, which suits a saved report. `-o ndjson` writes one complete entry per line, which is what to use with `--follow` or when piping into `jq` or a log collector. ## Check for failing upstreams [#check-for-failing-upstreams] A firewall fetches packages from its upstream registries, often more than one. When an upstream fails, the firewall answers with what the other upstreams returned, so builds usually keep working. If no upstream could answer, the request fails, and the error names the package with nothing in it about the registry behind it. Either way the failure itself only shows up here, which makes this the place to find a rate limit, an expired credential, or a registry the firewall cannot reach. ```shell bsfw logs --all-users --kind error --since 24h ``` In the dashboard, open **Logs** and set **Status** to **Error**. The CLI flag is `--kind`, not `--status`; it filters decisions from upstream failures, with no way to ask for blocked entries only. Each entry names the upstream, the URL that failed, and what came back. The common cases: * **Rate limited (`429`)**: the upstream is throttling the firewall. The other upstreams still answer, but any version held only by this one can go missing until it lets the firewall back in. * **Rejected credentials (`401`)**: the stored credential for that upstream is wrong or expired. See [Manage upstream credentials](/firewall/how-to/manage-upstream-credentials). * **Server error (`5xx`) or no response**: the upstream is down, unreachable from the firewall, or its hostname does not resolve. Only real faults land here. An upstream answering that it has no such package is a normal part of resolution, so this view stays quiet until something is actually wrong. The [logs reference](/firewall/references/logs#upstream-error-entries) lists exactly what is recorded. ## Correlate CI runs [#correlate-ci-runs] Log entries record the interaction ID when the token carries one (`::`, see [Access Tokens](/firewall/getting-started/access-tokens)). Set it to the pipeline run ID in CI and every package request from that run shares the ID. ## What is logged when [#what-is-logged-when] * A rule with `log: true` writes an entry on every match, blocked or not. A log-only rule (`block: false, log: true`) is the standard way to trial a policy before enforcing it. * An exception with `log: true` writes an entry when it lets a request through. The complete entry identifies the exception. * A rule with `log: false` writes nothing, even when it blocks the request. If an install fails with no entry to show for it, this is the first thing to check. * A failed upstream call writes its own entry, even when no rule matched and even when the request succeeded. Nothing was blocked in those entries. See [Check for failing upstreams](#check-for-failing-upstreams). * Other than those upstream failures, a request that matches no rule is not logged at all. An empty log means no rule had anything to say, not that no packages were installed. To see traffic that no policy rule covers, add a rule matching all packages with `{ "block": false, "log": true }`. That is also the quickest way to confirm a client really is going through the firewall. ## See what was actually downloaded [#see-what-was-actually-downloaded] The firewall log answers "which rule decided this", not "what did we download". A successful install that broke no rule leaves nothing behind, by design: the log exists so a developer can find out why something was blocked. Two ways to get the download record instead: * **[Observations](/firewall/references/observations)** are the ready-made answer. Each row is a package version the firewall has served, with when and how often, built from real traffic rather than from what manifests declare. Nothing to configure. ```shell bsfw observations --period 30d --firewall ``` * **A match-all log-only rule** turns the firewall log itself into a full request trail, at the cost of an entry per request. Use it when you want the log's fields, such as the identity and interaction ID on each request, rather than an inventory. Neither is the [audit log](/firewall/references/audit-log), which records control-plane actions: who changed configuration, policy, tokens, and teams. ## Related [#related] * How-to: [Investigate a blocked install](/firewall/how-to/investigate-blocked-install) * Reference: [CLI](/firewall/references/cli) # Release notes (/firewall/release-notes) ## September 2026 [#september-2026] ### Rust dependencies through a Cargo firewall [#rust-dependencies-through-a-cargo-firewall] Rust projects can now resolve, download, and publish crates through a firewall. Replacing the crates-io source routes the dependencies a project already has, with no `Cargo.toml` changes. Route Rust dependencies through a [Cargo firewall](/firewall/ecosystems/cargo) (beta), with controls for vulnerabilities, yanked crates, licenses, release maturity, and malware. ## August 2026 [#august-2026] ### Self-service trials, Composer, the CLI, and clearer firewall logs [#self-service-trials-composer-the-cli-and-clearer-firewall-logs] You can now sign up directly and start a 14-day trial. PHP projects can install dependencies through a Composer firewall. The Bytesafe Dependency Firewall CLI has been released for macOS, Linux, and Windows. Firewall logs record upstream failures and can be filtered by outcome. Firewalls with a parent show their inherited configuration. View rules, exceptions, and upstreams inherited from a parent firewall in the [dashboard and API](/firewall/how-to/share-configuration-across-firewalls#see-what-a-firewall-inherits) . Find relevant [firewall log](/firewall/how-to/view-logs) entries with improved filtering. Adding rules is simpler: choose what you want to prevent, or start from scratch. See [Delay new versions](/firewall/how-to/delay-new-versions) . Start a [14-day trial](/firewall/early-access) directly from the signup page, without requesting access first. Route PHP dependencies through a [Composer firewall](/firewall/ecosystems/composer) (beta), with controls for vulnerabilities, abandoned packages, licenses, release maturity, and install-time code execution. The [Bytesafe Dependency Firewall CLI](/firewall/references/cli#install) , `bsfw` , is released for macOS, Linux, and Windows. Install it with Homebrew, the install script, or a direct download. Record [upstream failures](/firewall/references/logs#upstream-error-entries) in the firewall log. Each entry identifies the registry and its response. Failures are logged even when another upstream serves the request. ## July 2026 [#july-2026] ### The next generation: a firewall in front of your registries [#the-next-generation-a-firewall-in-front-of-your-registries] The Bytesafe Dependency Firewall is a checkpoint in front of the registries your builds already pull from. Every install and publish passes your rules on the way through: block a package, filter it out of the version list, or delay it until the version has aged. Rules act on what is known about a package, not just its name: vulnerabilities, malware, licenses, install scripts, and how long the version has been public. An exception waives one with a reason and an expiry. [Rules](/firewall/concepts/rule-evaluation) replace policies and plugins: one model combining an [execution phase](/firewall/concepts/execution-phases) , a [selector](/firewall/references/rules-and-selectors) (name, version range, upstream, or a computed check), and an effect. Any rule can be scoped to one package, one prefix, or one upstream. [Exceptions](/firewall/how-to/manage-exceptions) carry a reason and an optional expiry, so a waiver is documented and ends by itself. [Repository managers](/firewall/how-to/integrate-repository-managers) : run the firewall in front of, behind, or on both sides of Artifactory or Nexus. [Go](/firewall/ecosystems/go) and [Conda](/firewall/ecosystems/conda) (beta) join npm, PyPI, Maven, and NuGet. [Container images](/firewall/ecosystems/containers) are coming in Q4 2026. [Malware blocking](/firewall/how-to/block-malware) on download. [Secrets scanning on publish](/firewall/how-to/scan-for-secrets) , so credentials do not leave in a package. [Trust downgrade detection](/firewall/references/selector-functions#trust-downgrade) for npm releases published with weaker provenance than earlier versions. [Firewall log](/firewall/how-to/view-logs) and [audit log](/firewall/how-to/review-audit-log) : every decision with package, rule, and requester; every configuration change with who made it. [Webhooks](/firewall/how-to/receive-webhook-events) : signed events for firewall decisions, audit events, configuration changes, and publishes. [Trusted Builders](/firewall/getting-started/github-actions) : GitHub Actions authenticate with their OIDC identity, no stored secrets in CI. [Config as code](/firewall/getting-started/config-as-code) : firewall configuration in git, with rollback. This documentation site. Start at the [Quickstart](/firewall/getting-started/quickstart) or [What is a dependency firewall?](/firewall/getting-started/what-is-dependency-firewall) Running previous-generation Bytesafe? The [migration guide](/firewall/how-to/migrate-from-previous-generation) maps every policy and plugin to its replacement and covers moving private packages to a repository manager. # Advisory sources (/firewall/references/advisory-sources) The `vulnerabilities` [selector function](/firewall/references/selector-functions#vulnerabilities) matches package versions against the Bytesafe advisory dataset. The firewall uses advisory data for the [supported package ecosystems](/firewall/ecosystems), except Conda; see [Ecosystem coverage](#ecosystem-coverage). Operating system feeds are not listed here because container image firewalling is planned for Q4 2026 and is not implemented yet. ## Advisory sources [#advisory-sources] | Source | URL | Used for | | ------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | | National Vulnerability Database (NVD) | [nvd.nist.gov](https://nvd.nist.gov/) | CVE metadata, CVSS scores, publication dates, and modified dates. | | GitHub Advisory Database (GHSA) | [github.com/advisories](https://github.com/advisories) | Package advisories from GitHub's advisory database. | | GitLab Advisory Database | [gitlab.com/gitlab-org/advisories-community](https://gitlab.com/gitlab-org/advisories-community) | Package advisories from GitLab's advisory database. | | Go Vulnerability Database | [pkg.go.dev/vuln](https://pkg.go.dev/vuln) | Go module advisories. | The [European Vulnerability Database (EUVD)](https://euvd.enisa.europa.eu/) is planned as an additional source. It is not used by the firewall today. EPSS is attached separately from advisory data: | Source | URL | Used for | | ---------- | --------------------------------------------- | --------------------------------------------------------------------------------------------- | | FIRST EPSS | [first.org/epss](https://www.first.org/epss/) | Exploit probability and percentile by CVE. Rules compare against probability, not percentile. | ## Dataset format [#dataset-format] The firewall downloads a prebuilt dataset that Bytesafe builds from the sources above, caches it on disk, and loads it when the process starts. Self-hosted deployments need outbound HTTPS access to fetch it; see [Deployment models](/firewall/concepts/deployment-models). The dataset has two record types: * **Advisories**: one affected package from one source, including ecosystem, package name, vulnerable ranges, patched versions, timestamps, source, and withdrawn status. * **Vulnerabilities**: one vulnerability ID, including title, description, CVSS data by vendor, severity by vendor, CWE IDs, references, and EPSS. Withdrawn advisories are ignored during matching. ## Ecosystem coverage [#ecosystem-coverage] Advisory matching follows the [supported package ecosystems](/firewall/ecosystems), except Conda: the dataset has no Conda advisories, and the `vulnerabilities` [selector function](/firewall/references/selector-functions#vulnerabilities) is not supported for Conda firewalls. Version matching uses each ecosystem's native version scheme. Package names are normalized before matching according to each ecosystem's rules. ## CVSS score [#cvss-score] A rule like `maxCvssScore: 7.0` compares against one score per vulnerability: If the effective vendor provides a numeric CVSS score, the firewall uses it. If not, the firewall maps the vendor severity label to a score: | Severity | Score used | | -------- | ---------- | | CRITICAL | 10 | | HIGH | 8.9 | | MEDIUM | 6.9 | | LOW | 3.9 | If the vulnerability has no score and no severity, it counts as 10. ### Vendor priority [#vendor-priority] When multiple vendors report data for the same vulnerability, the firewall chooses the effective vendor in this order: 1. NVD 2. GHSA 3. Go Vulnerability Database ## EPSS score [#epss-score] Each vulnerability can carry an EPSS score from FIRST. EPSS has two values: * `probability`: likelihood of exploitation, 0 to 1. * `percentile`: ranking compared with other CVEs, 0 to 1. The selector's `maxEpssScore` compares against `probability`. Both thresholds must be exceeded for a match: CVSS score must be greater than `maxCvssScore`, and EPSS probability must be greater than `maxEpssScore`. Details and examples are in [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages). ## Freshness and score changes [#freshness-and-score-changes] Advisory data changes after publication. A CVE often appears with a provisional CVSS score that is revised once analysis completes: 7.5 can become 10, a critical can be downgraded, and an advisory can be withdrawn entirely. EPSS probabilities are recalculated as exploitation data comes in. The firewall stores no verdicts. Every download request is evaluated against the advisory dataset the firewall currently has loaded, so score changes take effect in both directions: * **Score drops below your threshold**: the `vulnerabilities` rule stops matching, and the next request for the package passes. Nothing needs to be reset. An [exception](/firewall/how-to/manage-exceptions) created while the package was blocked stops being consulted, because exceptions are only checked after the rule matches. * **Score rises above your threshold**: the rule starts matching, and the next request is blocked. This applies even if the same version was downloaded through the firewall before; the firewall's artifact cache does not bypass rule evaluation. * **Advisory withdrawn**: it is excluded from matching entirely. Two things bound how quickly a change shows up: * The dataset is versioned and timestamped. A running firewall loads it once at process start; if the cached dataset exists, the firewall reuses it, otherwise it downloads a fresh copy. A score change reaches a firewall when it loads a dataset that contains it. * Only requests that reach the firewall are evaluated. A version already sitting in a client's local package manager cache, or in a [repository manager](/firewall/how-to/integrate-repository-managers) cache in front of the firewall, is served from there without a new firewall decision. ## Other package signals [#other-package-signals] * **Malware detection** combines package-content findings with dedicated malicious-package intelligence. It does not use the vulnerability dataset described on this page. See [Block malware](/firewall/how-to/block-malware). * **Deprecated and unlisted** status comes from upstream registry metadata. See [Block deprecated packages](/firewall/how-to/block-deprecated-packages). ## Related [#related] * How-to: [Block vulnerable packages](/firewall/how-to/block-vulnerable-packages) * Reference: [Selector functions](/firewall/references/selector-functions), [Rules and selectors](/firewall/references/rules-and-selectors) # API (/firewall/references/api) The management API is what the dashboard and the [CLI](/firewall/references/cli) use. Everything lives under the namespace ID prefix: ```text https://eu-sov-1.bytesafecloud.eu/v1//... ``` Package traffic (the ecosystem protocol endpoints under `/npm/`, `/pypi/`, `/maven/`, `/nuget/`, `/go/`, `/cargo/`, `/composer/`, `/conda/`) shares the same prefix and authentication; those endpoints are documented per [ecosystem](/firewall/ecosystems). This page covers the management surface. ## Authentication [#authentication] Every endpoint except the OIDC exchange requires a token in the `Authorization` header, `Bearer` or basic auth; formats, interaction ID suffix, and the credential envelope are in the [tokens reference](/firewall/references/tokens#wire-format). Endpoints enforce the [permissions](/firewall/references/permissions) noted below; all of them additionally require `ns:access`. Responses are JSON. Errors carry a `requestId` for correlation with logs. ## Configuration [#configuration] | Method and path | Description | Permission | | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | `GET /config` | List firewalls | `fw:read` per firewall | | `GET /config/` | Full config, credentials redacted | `fw:read` | | `PUT /config/` | Upsert full config; creates when the ID is new. The ID must be a [valid firewall ID](/firewall/references/firewall-config#firewall-id). Plaintext credentials rejected. On create, accepts an `owner` team in the body and defaults the permissions map when omitted | `fw:update:config` (+`fw:create` for new) | | `DELETE /config/` | Delete the firewall. 409 when other firewalls inherit from it | `fw:delete` | | `GET /config//history` | Config versions, newest first. 20 by default, `?limit=` raises it to up to 200 | `fw:read` | | `GET /config//version/{version}` | One stored version, read-only, credentials redacted. 404 for a version not in history | `fw:read` | | `PUT /config//version/{version}` | Roll back to a recorded version; creates a new version. 404 for a version not in history | `fw:update:config` | | `PUT /config//upstreams/{id}/credentials` | Set upstream credentials, plaintext body, per-field merge | `fw:update:credentials` | | `PUT /config//upstreams/{id}/artifact-credentials/{host}` | Set credentials for one [artifact host](/firewall/references/upstreams#artifact-host-credentials) that is not the upstream's own, plaintext body, per-field merge. `{host}` is a hostname with an optional port | `fw:update:credentials` | | `GET /config//upstreams/inherited` | Only inherited upstreams, including entries overridden by a nearer firewall in the chain. Credentials are redacted | `fw:read` | Config upserts and rollbacks reject an unknown ecosystem, invalid upstream settings, unparseable version ranges, unknown `upstreamType` values, the legacy selector key `internal`, and selector functions that do not support the firewall's ecosystem. A `parentId` must also name an existing same-ecosystem firewall without creating a cycle ([inheritance](/firewall/how-to/share-configuration-across-firewalls)), and a changed `permissions` map must not lock the firewall out of management ([lockout guard](/firewall/references/permissions#lockout-guard)). These validation failures return 400. A config upsert or rollback that adds or changes an exception on an inherited rule also requires `fw:update:exception` on the source firewall that owns that rule, which may be any ancestor. Exceptions already stored on the firewall and left unchanged do not need that permission again, so an unrelated edit is never blocked by an old exception. ## Rules and exceptions [#rules-and-exceptions] Granular alternatives to a full config upsert: | Method and path | Description | Permission | | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | --------------------- | | `GET /config//rules` | This firewall's **own** rules. Inherited rules are not included | `fw:read` | | `GET /config//rules/inherited` | Only the rules this firewall **inherits**, each naming the firewall it comes from. Its own rules are not included | `fw:read` | | `PUT /config//rules/{id}` | Upsert one rule | `fw:update:rule` | | `DELETE /config//rules/{id}` | Delete one rule | `fw:update:rule` | | `GET /config//exceptions` | This firewall's **own** exceptions. Inherited ones are not included | `fw:read` | | `GET /config//exceptions/inherited` | Only the exceptions this firewall **inherits**, each naming the firewall it comes from | `fw:read` | | `PUT /config//exceptions/{id}` | Upsert one exception. If the rule is inherited, also requires `fw:update:exception` on its source firewall | `fw:update:exception` | | `DELETE /config//exceptions/{id}` | Delete one exception | `fw:update:exception` | `/rules` returns rules defined on this firewall; `/rules/inherited` returns ancestor-owned rules. Both contribute to enforcement. The same rule ID can appear in both lists, which makes an exception for that ID ambiguous. Exceptions are split by ownership in the same way. Entries from the inherited endpoints carry source fields: | Field | What it holds | | ----------------------- | --------------------------------------------------------- | | `sourceFirewall` | ID of the firewall the entry is defined on | | `sourceFirewallName` | That firewall's display name | | `sourceFirewallVersion` | Version of that firewall's config that supplied the entry | Inherited upstreams use two more fields when a nearer firewall declares the same `id`: | Field | What it holds | | -------------- | ----------------------------------------------------- | | `overridden` | `true` when this upstream is not in effect | | `overriddenBy` | ID of the nearer firewall whose upstream is in effect | Rules from both `/rules` and `/rules/inherited` carry two more, which answer "may this caller add an exception to this rule" so a UI can disable the action before anyone fills in a form: | Field | What it holds | | ----------------------- | ----------------------------------------------------------------------- | | `canAddException` | Whether the token making this request may add an exception to this rule | | `exceptionDeniedReason` | Why not, present only when `canAddException` is false | `exceptionDeniedReason` is one of: * `missing_child_permission` — the caller lacks `fw:update:exception` on this firewall. * `missing_owner_permission` — the caller has it here but not on the source firewall that owns the rule, which is also required. See [permissions](/firewall/references/permissions). * `ambiguous_rule_id` — this rule's ID appears more than once across the firewall and its parents, so an exception naming it would be ambiguous. Reading inherited entries needs `fw:read` on this firewall only. It neither requires nor grants permission on source firewalls. Rule and exception upserts reject an unparseable version range, an unknown `upstreamType` value, the legacy selector key `internal`, or a selector function that does not support the firewall's ecosystem. An exception upsert additionally rejects (400) a `rule` ID that matches no rule, or more than one rule, across this firewall and its parents. See [Share configuration across firewalls](/firewall/how-to/share-configuration-across-firewalls) for the inheritance model behind all of this. ## License sets [#license-sets] Named lists of SPDX license IDs, referenced by `license` [selector rules](/firewall/references/selector-functions#license). See [Enforce license policy](/firewall/how-to/enforce-license-policy). | Method and path | Description | Permission | | ----------------------------------- | ------------------------------------------------- | ----------------------- | | `GET /license-sets` | List license sets | none beyond `ns:access` | | `POST /license-sets` | Create one (`name`, `description`, `licenses`) | `ns:update:licenses` | | `PUT /license-sets/{licenseSet}` | Update | `ns:update:licenses` | | `DELETE /license-sets/{licenseSet}` | Delete. 409 while any firewall rule references it | `ns:update:licenses` | ## Logs, metrics, observations, audit [#logs-metrics-observations-audit] | Method and path | Description | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /log` | Query firewall log entries, newest first. Params: `firewall`, `user`, `allUsers`, `search`, `phase` (`versions`, `download`, `upload`), `kind` (`rule`, `error`), `limit` (default 100, max 1000), `before` (opaque cursor). Returns `{entries, nextCursor}`. Defaults to the caller's own entries; see [Logs](/firewall/references/logs) | | `GET /log/stream` | Server-sent events: last 10 entries, then new ones live. Same params | | `GET /metrics` | Namespace metrics. Params: `periodMinutes` (default 1440, max 43200), `bucketMinutes` (rollup size, max 1440). See [Metrics](/firewall/references/metrics) | | `GET /metrics/firewall/` | Same shape for one firewall | | `GET /observations` | Package usage inventory. Params: `firewall`, `period` (`today`, `7d`, `30d`, `90d`), `pageSize`, `pageToken`. See [Observations](/firewall/references/observations) | | `GET /audit` | Audit events. Params: `action`, `resourceType`, `resourceId`, `actorUserId`, `outcome`, `from`, `to`, `limit` (max 1000), `offset`. See [Audit log](/firewall/references/audit-log) | ## Webhooks [#webhooks] Push firewall, audit, configuration, and publish events to your own systems; wire format and delivery behavior in the [webhooks reference](/firewall/references/webhooks), setup in [Receive webhook events](/firewall/how-to/receive-webhook-events). | Method and path | Description | Permission | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | `GET /webhooks` | List webhooks, secrets redacted | none beyond `ns:access` | | `POST /webhooks` | Create (`name`, `description`, `url`, `enabled`, `eventTypes`, optional `firewalls` scope). The `201` response carries the plaintext signing `secret` | `ns:update:webhooks` | | `GET /webhooks/{webhook}` | Detail, secret redacted | none beyond `ns:access` | | `PUT /webhooks/{webhook}` | Update configuration; the secret is untouched | `ns:update:webhooks` | | `DELETE /webhooks/{webhook}` | Delete the webhook and its buffered events | `ns:update:webhooks` | | `GET /webhooks/{webhook}/secret` | The decrypted signing secret, plus the previous secret during its 24-hour rotation window | `ns:update:webhooks` | | `POST /webhooks/{webhook}/rotate-secret` | Generate a new secret immediately; the old one keeps verifying for 24 hours | `ns:update:webhooks` | | `POST /webhooks/{webhook}/test` | Send one signed test event synchronously, optional body `{"type": ...}`; returns status, response body, and duration | `ns:update:webhooks` | | `POST /webhooks/{webhook}/unquarantine` | Release a quarantined webhook; delivery resumes immediately | `ns:update:webhooks` | | `GET /webhooks/{webhook}/deliveries` | Delivery records. Params: `limit` (default 50, max 128), `offset` | none beyond `ns:access` | | `GET /webhooks/{webhook}/deliveries/{delivery}` | One delivery with full payload and endpoint response | none beyond `ns:access` | | `GET /webhooks/{webhook}/metrics` | Delivery metrics. Param `periodMinutes` (max 10080) | none beyond `ns:access` | ## Tokens and identity [#tokens-and-identity] | Method and path | Description | Permission | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `GET /whoami` | The calling identity and its permissions | any | | `GET /tokens/personal` | List your own Personal Access Tokens | any | | `POST /tokens/personal` | Create a PAT (`name`, `validForHours`, optional `permissions`, `firewalls`) | `ns:create:pat` | | `GET /tokens/service` | List Service Access Tokens | `ns:create:service-token` | | `POST /tokens/service` | Create a SAT (same body shape) | `ns:create:service-token` | | `DELETE /tokens/{token}` | Revoke a token by deleting it; it stops validating immediately. Own PAT: always allowed. SAT: `ns:create:service-token`. Another user's PAT: `team:update` | see description | ## Teams [#teams] | Method and path | Description | Permission | | --------------------------------------- | ------------------------------------- | ------------------------ | | `GET /teams` | List teams | `team:read` | | `POST /teams` | Create a team | `team:update` | | `PUT /teams/{team}` | Update name, description, permissions | `team:update` | | `DELETE /teams/{team}` | Delete a team | `team:delete` | | `GET /teams/{team}/members` | List members | `team:read` | | `POST /teams/{team}/members` | Add a member | `team:update:membership` | | `DELETE /teams/{team}/members/{userId}` | Remove a member | `team:update:membership` | ## Trusted Builders and OIDC [#trusted-builders-and-oidc] | Method and path | Description | Permission | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------- | | `GET /trusted-builders` | List Trusted Builders | `ns:create:service-token` | | `POST /trusted-builders` | Register one (`name`, `repositoryOwner`, `repository`, `repositoryId`, `permissions`, `firewalls`, `enabled`) | `ns:create:service-token` | | `PUT /trusted-builders/{builder}` | Update | `ns:create:service-token` | | `DELETE /trusted-builders/{builder}` | Delete | `ns:create:service-token` | | `POST /oidc/exchange` | Exchange a GitHub OIDC ID token (`{"idToken": ...}`) for a 15-minute SAT. **Unauthenticated** | none | ## Advisories [#advisories] | Method and path | Description | | ------------------------ | ------------------------------------------ | | `GET /advisories/{purl}` | Vulnerability advisories for a package URL | ## Related [#related] * Reference: [CLI](/firewall/references/cli), [Firewall config](/firewall/references/firewall-config), [Tokens](/firewall/references/tokens), [Permissions](/firewall/references/permissions) * Getting started: [GitHub Actions](/firewall/getting-started/github-actions), [Config as code](/firewall/getting-started/config-as-code) # Audit log (/firewall/references/audit-log) The audit log records control-plane actions in a namespace: who did what to firewalls, tokens, teams, webhooks, and Trusted Builders, and what was denied. For review workflows, see [Review the audit log](/firewall/how-to/review-audit-log). ## Event shape [#event-shape] ```json { "id": "01J...", "createdAt": "2026-07-03T10:22:41Z", "namespace": "ns-01KYA296JWEE36NDNRTJ4KM82Q", "action": "firewall.rule.upsert", "resourceType": "firewall", "resourceId": "3e7f5cb806f26e3beae2", "actorUserId": "user-7d2f...", "actorTokenIdentifier": "5f8a...", "requestId": "8f4c2e...", "interactionId": "build-4711", "remoteAddr": "203.0.113.7", "userAgent": "bsfw/1.4.0", "requestMethod": "PUT", "requestPath": "/v1/ns-01KYA296JWEE36NDNRTJ4KM82Q/config/3e7f5cb806f26e3beae2/rules/delay-14d", "outcome": "success", "statusCode": 200, "eventData": { "rule": "delay-14d" } } ``` ## Fields [#fields] | Field | Type | Description | | --------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Event ID | | `createdAt` | string | When the action happened | | `namespace` | string | Namespace ID the action targeted | | `action` | string | What was done, see catalog below | | `resourceType` | string | `firewall`, `token`, `namespace`, `session`, `team`, `trusted_builder`, `license_set`, `webhook` | | `resourceId` | string | The specific resource. For firewalls this is the firewall ID, never the display name; firewalls created in the dashboard get a generated ID that differs from their name, so search by ID | | `actorUserId` | string | Acting user's ID, when the session belongs to one. An opaque identifier (the token's OIDC subject), not an email or display name; the dashboard resolves it to a name where it can | | `actorTokenIdentifier` | string | Hash-derived identifier of the acting token. Never contains token material, but is stable, so repeated use of one token is correlatable | | `requestId` | string | Request ID, shared with firewall log entries from the same request | | `interactionId` | string | Interaction ID from the token suffix, if any | | `remoteAddr`, `userAgent`, `requestMethod`, `requestPath` | string | Request metadata. Path excludes the query string | | `outcome` | string | `success` or `failure` | | `statusCode` | number | HTTP status returned | | `errorMessage` | string | Short failure reason, on failures | | `eventData` | object | Action-specific detail, for example the missing permission on a denial or the claim mismatch on an OIDC failure | ## Action catalog [#action-catalog] | Action | Emitted when | | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `auth.failed` | A request presented a missing or invalid token and its requested namespace could be assigned safely to one tenant | | `auth.namespace_denied`, `authz.namespace_denied` | A valid token was used outside its namespace or without namespace access | | `authz.denied` | An action was attempted without the required permission; `eventData.permission` names it | | `firewall.access` | Firewall traffic access was denied per-resource | | `firewall.config.upsert`, `firewall.config.delete`, `firewall.config.rollback` | Configuration imported/changed, firewall deleted, config rolled back | | `firewall.rule.upsert`, `firewall.rule.delete` | A rule changed or was removed | | `firewall.exception.upsert`, `firewall.exception.delete` | An exception changed or was removed. See [exceptions on inherited rules](#exceptions-on-inherited-rules) when the rule came from a parent firewall | | `firewall.credentials.upsert` | Upstream credentials were set or rotated | | `license_set.create`, `license_set.update`, `license_set.delete` | [License set](/firewall/references/api#license-sets) management | | `namespace.provision`, `namespace.create`, `namespace.update`, `namespace.delete`, `namespace.clone` | Namespace lifecycle; `provision` is the auto-provisioning on an admin's first login | | `token.personal.create`, `token.service.create`, `token.delete` | Token lifecycle | | `team.create`, `team.update`, `team.delete`, `team.member.add`, `team.member.remove` | Team management | | `trusted_builder.create`, `trusted_builder.update`, `trusted_builder.delete` | Trusted Builder management | | `webhook.create`, `webhook.update`, `webhook.delete`, `webhook.test`, `webhook.secret.rotate`, `webhook.unquarantine` | [Webhook](/firewall/references/webhooks) management. Subscribable as `audit.webhook.*` webhook events | | `oidc.exchange` | A Trusted Builder exchange succeeded, or a failed exchange identified a configured builder. Malformed tokens and unknown audiences are not visible in a tenant audit log | ## Exceptions on inherited rules [#exceptions-on-inherited-rules] Firewalls can [inherit](/firewall/how-to/share-configuration-across-firewalls) rules from an ancestor. A team can add an exception on its own firewall for one of those inherited rules. Successful events stay attached to the team's firewall because that is where the configuration changed. They also record which firewall owns the rule. In the dashboard, open an event and look under **Event data**. In the API response, the same object is named `eventData`. These fields identify the rule owner: * `rule_source_firewall_id` is the ID of the firewall that owns the rule referenced by one saved exception. * `exception_rule_source_firewall_ids` is the list of rule-owner firewall IDs involved in a config import or rollback. It can include the changed firewall when an exception references one of its local rules. Audit filters do not search Event data. To find successful exceptions against a baseline, filter by the relevant action, open the results, and compare these fields with the baseline firewall ID. Filtering by the baseline's `resourceId` finds source-permission denials filed against that baseline, not successful changes filed against child firewalls. | What happened | What you will find | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A team saved an exception on an inherited rule | A `firewall.exception.upsert` event on the team's firewall. Under Event data, `rule_source_firewall_id` identifies the firewall that owns the rule | | A team imported a config or rolled one back, changing exceptions | A `firewall.config.upsert` or `firewall.config.rollback` event on the team's firewall. Under Event data, `exception_rule_source_firewall_ids` lists the distinct rule owners involved | | Someone lacked permission on the rule's source firewall | An `authz.denied` event filed against the source firewall. Under Event data, `target_firewall` identifies the firewall they were changing and `rule_id` identifies the rule | Adding an exception to an inherited rule requires permission on both the firewall being changed and the source firewall that owns the rule. The denial event shows which of those checks failed. ## Query API [#query-api] ```text GET /v1//audit ``` | Parameter | Description | | -------------- | ------------------------------------------------------------------------------------------ | | `action` | Exact action string from the catalog | | `resourceType` | Resource type | | `resourceId` | Specific resource | | `actorUserId` | Acting user's raw ID, not the display name. Copy it from the event detail in the dashboard | | `outcome` | `success` or `failure` | | `from`, `to` | RFC 3339 time bounds | | `limit` | Page size, default 100, maximum 1000 | | `offset` | Page offset | The response wraps events with `total`, `limit`, and `offset` for paging. The CLI exposes the same filters with `bsfw audit`. See the [CLI reference](/firewall/references/cli#bsfw-audit). ## Related [#related] * How-to: [Review the audit log](/firewall/how-to/review-audit-log) * Concept: [Observability](/firewall/concepts/observability), [Access and permissions](/firewall/concepts/access-and-permissions) * Reference: [CLI](/firewall/references/cli#bsfw-audit), [API](/firewall/references/api), [Logs](/firewall/references/logs) # CLI (/firewall/references/cli) `bsfw` is the Bytesafe Dependency Firewall CLI. It is distributed from [bytesafedev/bsfw](https://github.com/bytesafedev/bsfw) and uses the same management API as the dashboard, so anything it reads or changes is the same data the dashboard shows. | To do this | Use | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | See which firewalls exist and how one is set up | [`bsfw firewall list`](#bsfw-firewall-list), [`bsfw firewall show`](#bsfw-firewall-show) | | Find out why an install was blocked | [`bsfw logs`](#bsfw-logs) | | See whether an upstream is failing | [`bsfw logs --kind error`](#bsfw-logs) | | Check what the firewall knows about one package version | [`bsfw package`](#bsfw-package) | | Keep policy in git and apply it | [`bsfw firewall export`](#bsfw-firewall-export), [`bsfw firewall import`](#bsfw-firewall-import) | | See or export a past config version | [`bsfw firewall history`](#bsfw-firewall-history), [`bsfw firewall export --version`](#bsfw-firewall-export) | | See who changed a firewall, a token, or a team | [`bsfw audit`](#bsfw-audit) | | List the packages your builds actually pulled | [`bsfw observations`](#bsfw-observations) | | Authenticate a CI job without storing a token | [`bsfw oidc exchange`](#bsfw-oidc-exchange) | ## Install [#install] ### Homebrew [#homebrew] Homebrew supports macOS and Linux, including Linux running under Windows Subsystem for Linux (WSL) 2. The `bsfw` cask installs the binary, shell completions, and the `bsfw` man page. ```shell brew install bytesafedev/tap/bsfw ``` ### Install script [#install-script] The install script supports macOS and Linux on amd64 and arm64. It downloads the latest release archive, verifies it against the release's `checksums.txt`, and installs `bsfw` to `/usr/local/bin` when writable or `~/.local/bin` otherwise. ```shell curl -fsSL https://raw.githubusercontent.com/bytesafedev/bsfw/main/install.sh | sh ``` | Variable | Description | | ------------------ | ----------------------------------------------------------------------- | | `BSFW_VERSION` | Install a specific release, for example `v0.2.0`, instead of latest | | `BSFW_INSTALL_DIR` | Install the binary in this directory | | `BSFW_BASE_URL` | Download release files from an internal mirror. Requires `BSFW_VERSION` | ### Native Windows and manual installation [#native-windows-and-manual-installation] On native Windows, download the Windows archive from [GitHub Releases](https://github.com/bytesafedev/bsfw/releases), verify it against `checksums.txt`, and put `bsfw` on `PATH`. WSL 2 users can use the Homebrew command above. Releases provide macOS, Linux, and Windows archives for amd64 and arm64. Each archive also contains shell completions and man pages. Verify any installation: ```shell bsfw version ``` ## Initial setup [#initial-setup] Create a [Personal Access Token (PAT)](/firewall/getting-started/access-tokens#personal-access-tokens-pat) in the dashboard, then store the endpoint and namespace on the active profile and log in: ```shell bsfw profile set endpoint https://eu-sov-1.bytesafecloud.eu bsfw profile set namespace bsfw auth login bsfw whoami ``` `auth login` prompts for the token, verifies it against the configured endpoint and namespace, then stores it for the active profile. Tokens are not written to the profile config. The CLI uses the operating system keyring and falls back to `~/.config/bytesafe/credentials.json` with file mode `0600` when no keyring is available. In CI, set `BYTESAFE_TOKEN` instead of storing a token. For GitHub Actions without a long-lived token, use [`bsfw oidc exchange`](#bsfw-oidc-exchange). ## Configuration resolution [#configuration-resolution] The CLI needs an endpoint, a namespace, an access token, and for many commands a firewall. Each of those can be set in more than one place: a flag on the command, an environment variable, the active profile, or a built-in default. Resolution is how the CLI picks one when several are set. It takes the first that has a value: 1. Command flag 2. Environment variable 3. Active profile 4. Built-in default, where one exists Nothing warns you when two sources disagree, and the winner is not the one set most recently: an exported `BYTESAFE_NAMESPACE` overrides the namespace on your profile every time. Keep one source per setting where you can: profiles for local work, environment variables in CI, flags for one-off overrides. The access token follows the same order with one difference. Below the environment variable it comes from the token stored by `bsfw auth login`, not from the profile, because tokens are never written to the profile config. `bsfw auth status` prints the resolved values and the source each came from, which is the fastest way to settle a "but I set that" question. Add `--no-verify` to inspect resolution without contacting the service. ### Profiles [#profiles] A profile stores an endpoint, namespace, and optional default firewall. Profiles live in `~/.config/bytesafe/config.toml`; `XDG_CONFIG_HOME` changes the base directory and `BYTESAFE_CONFIG` replaces the full path. | Command | Description | | -------------------------------- | ------------------------------------------------------------------- | | `bsfw profile list` | List profiles; `*` marks the active profile | | `bsfw profile show` | Show the active profile's resolved settings | | `bsfw profile use ` | Make a profile active | | `bsfw profile set ` | Set `endpoint`, `namespace`, or `firewall` | | `bsfw profile remove ` | Delete a profile; add `--with-token` to remove its stored token too | Use `--profile ` or `BYTESAFE_PROFILE` for one command without changing the active profile. ```shell bsfw profile set --profile production endpoint https://eu-sov-1.bytesafecloud.eu bsfw profile set --profile production namespace bsfw profile set --profile production firewall bsfw logs --profile production --all-users ``` ## Environment variables [#environment-variables] | Variable | Description | | ----------------------------------- | --------------------------------------------------------------------------- | | `BYTESAFE_ENDPOINT` | Management API base URL | | `BYTESAFE_NAMESPACE` | Namespace ID | | `BYTESAFE_TOKEN` | Access token | | `BYTESAFE_FIREWALL` | Default firewall ID | | `BYTESAFE_PROFILE` | Profile to use | | `BYTESAFE_CONFIG` | Full path to the profile config file | | `BYTESAFE_CREDENTIAL_STORE` | Stored-token backend: `auto`, `keyring`, or `file` | | `BYTESAFE_INTERACTION` | Interaction ID appended to the token for firewall and audit-log correlation | | `BYTESAFE_TRUSTED_BUILDER_AUDIENCE` | Default audience for `bsfw oidc exchange` | | `NO_COLOR` | Disable colored output when set | Namespace and firewall values are IDs, not display names. Use `bsfw namespaces` and `bsfw firewall list` to find them. ## Global flags [#global-flags] | Flag | Description | | ---------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `--endpoint ` | Management API base URL | | `--namespace ` | Namespace ID | | `--token ` | Access token. A command-specific `--token` on `firewall credentials` means the upstream token instead | | `--firewall ` | Default firewall ID for the command | | `--profile ` | Profile to use | | `--credential-store auto\|keyring\|file` | Stored-token backend | | `--interaction ` | Interaction ID for log correlation | | `-o, --output table\|json` | Output format. `logs` also supports `ndjson` and `logfmt` | | `--timeout ` | Request timeout; default 30 | | `--no-color` | Disable colored output | ## Dates and durations [#dates-and-durations] Every flag that takes a point in time (`logs --since`, `audit --from`, `audit --to`) accepts the same four forms. | Form | Example | Means | | ---------------------- | ----------------------------------------------- | -------------------------------------------------------------------- | | Date | `2026-08-01` | Midnight local time on that date | | Date and time | `"2026-08-01 14:30"` or `"2026-08-01 14:30:00"` | That local time. Quote it, or the shell splits it into two arguments | | RFC 3339 timestamp | `2026-08-01T14:30:00Z` | Exactly that instant, in the time zone the value carries | | Duration back from now | `30m`, `24h` | `--since` only | The date order is fixed: `YYYY-MM-DD`, the same order the CLI prints. Other orders such as `08/01/2026` are rejected with a usage error. A value without a time zone is read in the machine's local time zone, so the same command run in two time zones covers two different windows. Pass an RFC 3339 timestamp when the window has to be identical everywhere, which is the usual choice in CI. Durations use hours, minutes, and seconds (`h`, `m`, `s`). There is no day unit: for a week use `168h` or a date, not `7d`. The fixed windows `7d`, `30d`, and `90d` belong to `observations --period` only. Timestamps in table output are printed in local time as `YYYY-MM-DD HH:MM:SS`. JSON output keeps the server's RFC 3339 UTC timestamps unchanged. ## Commands [#commands] ### bsfw auth [#bsfw-auth] | Command | Description | | ------------------ | ---------------------------------------------------------------------------------- | | `bsfw auth login` | Prompt for, verify, and store an access token. `--token-stdin` reads it from stdin | | `bsfw auth status` | Show resolved configuration and verify the token. `--no-verify` skips the request | | `bsfw auth logout` | Remove the stored token for the selected profile | ### bsfw whoami [#bsfw-whoami] Show the organization, namespace, user, and exact permissions granted by the token. Use this first when a command returns 403. ```shell bsfw whoami bsfw whoami -o json ``` ### bsfw namespaces [#bsfw-namespaces] List namespaces available to the current credentials. Personal and Service Access Tokens are pinned to one namespace, so they normally return one entry. ```shell bsfw namespaces ``` ### bsfw firewall list [#bsfw-firewall-list] Every firewall in the namespace, with the IDs the other commands take. Firewalls created in the dashboard get a generated ID that differs from the display name, and commands, profiles, and API paths all want the ID. ```shell bsfw firewall list ``` The five commands below take the firewall ID as an argument. Leave it out to use the firewall set on the profile or in `BYTESAFE_FIREWALL`; without either, the command exits with a usage error. ### bsfw firewall show [#bsfw-firewall-show] Summarize one firewall: ecosystem, which firewall it inherits from, its upstreams, and how many rules and exceptions it has. For the raw configuration, use `export`. ```shell bsfw firewall show ``` ### bsfw firewall rules [#bsfw-firewall-rules] The firewall's rules, one per row. `--verbose` adds the selector columns, and `-o json` returns the complete rule objects. Inherited rules are not included; they stay on the parent firewall. ```shell bsfw firewall rules --verbose ``` ### bsfw firewall exceptions [#bsfw-firewall-exceptions] The firewall's exceptions, one per row. `--verbose` adds columns; `-o json` returns the complete objects, including reason, expiry, and notes, which is what a review of open exceptions needs. ```shell bsfw firewall exceptions -o json ``` The CLI deliberately changes firewall policy as one configuration document. Export, edit, and import it. Granular rule and exception editing lives in the dashboard, and the CLI has no delete command for a firewall. ### bsfw firewall export [#bsfw-firewall-export] Print the firewall's complete configuration as JSON on stdout: upstreams, rules, exceptions, and permissions. Redirect it to a file to keep it in git. Stored upstream credentials come out as placeholders, never as values, so an export is safe to commit. Inherited rules stay on the parent firewall and are not part of the document. Without `--version` this exports the current configuration. `--version` exports a stored past version instead, without changing anything; list the available versions with `bsfw firewall history`. ```shell bsfw firewall export > firewall-config.json bsfw firewall export --version 7 > firewall-config-v7.json ``` ### bsfw firewall history [#bsfw-firewall-history] Every save keeps the previous version. List them, newest first, to find a version number before exporting it with `bsfw firewall export --version`. `--limit` caps how many versions come back, 20 by default and 200 at most. ```shell bsfw firewall history bsfw firewall history --limit 50 -o json ``` ### bsfw firewall import [#bsfw-firewall-import] Read a complete firewall configuration from a file or stdin and apply it. The `id` in the JSON says which firewall it belongs to; there is no `--firewall` flag here. Import creates that firewall when it does not exist and replaces its entire configuration when it does, so a rule or exception missing from the file is removed from the firewall. ```shell bsfw firewall import --file firewall-config.json bsfw firewall import < firewall-config.json ``` Imports reject plaintext credentials. Exported placeholders preserve stored values for the same firewall and upstream. A new firewall or upstream has no stored value to preserve; set it with [`firewall credentials`](#bsfw-firewall-credentials). ### bsfw firewall credentials [#bsfw-firewall-credentials] Set the credentials a firewall uses against one private upstream. This is the command that fills in what an export leaves as a placeholder, and the one to run on a schedule when an upstream's token expires. Only the fields you pass change, so rotating a token does not mean re-entering a username and password. With no credential flags and no `--stdin`, the CLI prompts for the values without echoing them, which keeps secrets out of shell history. ```shell bsfw firewall credentials --firewall --upstream printf '%s' '{"token":""}' | \ bsfw firewall credentials --firewall --upstream --stdin ``` | Flag | Description | | -------------------- | ----------------------------------------------------------------------------------------------------------- | | `--firewall ` | Which firewall owns the upstream | | `--file ` | Take the firewall ID from a config JSON file instead of `--firewall` | | `--upstream ` | Required. The upstream's `id` from the firewall config, not its URL | | `--user ` | Username. Sent with the password as basic auth | | `--password ` | Password | | `--token ` | Token, sent as a bearer header. When both a token and a username and password are stored, the token is used | | `--stdin` | Read `user`, `password`, and `token` as one JSON object from stdin, for scripted rotation | ### bsfw logs [#bsfw-logs] One entry per package request the firewall evaluated, and what it decided. This is where a failed install is explained. Results are newest first. Without `--user` or `--all-users`, the server returns only the calling user's own entries, which is why a first run often looks empty. A Service Access Token belongs to no user, so a CI token sees nothing unless `--all-users` is given. ```shell bsfw logs --firewall bsfw logs --all-users --blocked --since 24h bsfw logs --all-users --kind error --since 24h bsfw logs --all-users --follow -o ndjson ``` | Flag | Description | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--firewall ` | Only this firewall. Falls back to the profile default when omitted | | `--user ` | Only this user's requests, by raw user ID, not a name or email. Mutually exclusive with `--all-users` | | `--all-users` | Everyone's requests, not just your own | | `--blocked` | Only requests the firewall stopped. Start here when someone reports a failed install | | `--phase versions\|download\|upload` | Only one [execution phase](/firewall/concepts/execution-phases): `versions` is the metadata request that lists versions, `download` is fetching one artifact, `upload` is a publish | | `--kind rule\|error` | `rule` for firewall decisions, `error` for [upstream failures](/firewall/references/logs#upstream-error-entries) such as rate limiting or rejected credentials. Both kinds when omitted | | `--search ` | Free-text match across user, ecosystem, package name, version, and message. Use the package name reported in the install error | | `--since ` | Entries newer than a duration or a point in time. See [Dates and durations](#dates-and-durations) | | `--limit ` | Entries to return; default 100. Asking for more than one server page pages through automatically | | `-f, --follow` | Keep the connection open and print entries as they arrive, reconnecting if the stream drops | `--follow` starts from the server's short backfill and then prints new entries, so `--since` and `--limit` do not apply to it. Filters and output format do. An entry for a failed upstream call shows `ERROR` in the status column. Its rule column names the upstream and what came back, such as `npmjs 429`, or `npmjs network` when there was no response at all. Nothing was blocked in those rows. See [Check for failing upstreams](/firewall/how-to/view-logs#check-for-failing-upstreams). The default table is a compact view. Use `-o json` for the complete response, `-o ndjson` for one complete entry per line (the form to pipe into `jq` while following), or `-o logfmt` for the legacy single-line form. ### bsfw package [#bsfw-package] Known vulnerability and malware findings for one exact package version: the same data the firewall's rules evaluate. Use it to explain a block, or to check a dependency before adding it. ```text bsfw package ``` Ecosystem is one of `npm`, `pypi`, `maven`, `nuget`, `go`, `cargo`, `composer`, or `conda`. The name is written the way that ecosystem writes it, and the version must be exact: no ranges, no dist-tags. ```shell bsfw package npm lodash 4.17.21 bsfw package npm @scope/name 1.2.3 bsfw package maven org.apache.commons:commons-lang3 3.14.0 -o json ``` An absent malware result is reported as unknown, not clean. ### bsfw observations [#bsfw-observations] The inventory view: which package versions your builds actually pulled through the firewall in a period, how often each was requested, and how many known vulnerabilities each carries today. Derived from traffic, not from manifests. ```shell bsfw observations --period 30d --firewall bsfw observations --ecosystem npm -o json ``` | Flag | Description | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--period today\|7d\|30d\|90d` | How far back to look; default `today`. These four fixed windows are the only accepted values | | `--firewall ` | Only this firewall. Falls back to the profile default when omitted | | `--ecosystem ` | Keep only rows from one ecosystem. Applied by the CLI after the server returns a page, so other ecosystems still fill that page and a filtered page can come back empty while more pages remain | | `--page-size ` | Rows per server page; the server's own default applies when unset | | `--page-token ` | Continue from the previous page. The CLI prints the token to pass when more rows are available | The vulnerability column is compact: `C2 H1` means two critical and one high, and `-` means none known. Use `-o json` for the counts as fields. Field-level detail: [Observations reference](/firewall/references/observations). ### bsfw audit [#bsfw-audit] The audit log answers who changed the setup: policy edits, upstream credentials, tokens, teams, and denied attempts. It does not contain package traffic, which is what [`bsfw logs`](#bsfw-logs) shows. Every event records what it acted on as a resource type plus the ID of that one resource, so most reviews start by naming one or both. These are the values `--resource-type` takes: | Resource type | Covers | | ----------------- | ----------------------------------------------------------------------------------- | | `firewall` | Config imports, rule and exception changes, upstream credentials, firewall deletion | | `token` | Personal and Service Access Token creation and deletion | | `team` | Team creation, updates, and membership changes | | `namespace` | Namespace creation, rename, settings, cloning, deletion | | `trusted_builder` | Trusted Builder registration and OIDC exchanges | | `license_set` | License set changes | | `webhook` | Webhook endpoint changes, test deliveries, and secret rotation | | `session` | Sign-ins, including failed ones | `--resource-id` takes the exact ID the event carries. For a firewall that is the firewall ID from `bsfw firewall list`, never its display name. For the other types, copy the ID from an event with `-o json` or from the event detail in the dashboard, then reuse it as a filter. ```shell bsfw audit --resource-type firewall --resource-id bsfw audit --outcome failure --from 2026-08-01 bsfw audit --action firewall.config.upsert -o json ``` | Flag | Description | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--resource-type ` | What kind of thing was acted on, from the table above | | `--resource-id ` | Which one, by ID. Combine with `--resource-type` to scope a review to a single firewall or token | | `--action ` | One exact action string, for example `firewall.rule.upsert` or `token.delete`. Prefixes and wildcards do not match; the full list is the [action catalog](/firewall/references/audit-log#action-catalog) | | `--actor ` | Who did it, by user ID. `bsfw whoami` prints your own ID; other users' IDs appear in `-o json` output and in the dashboard event detail. It is an opaque ID, not an email address | | `--outcome success\|failure` | Whether the action went through. `failure` collects permission denials, rejected imports, and failed sign-ins, which is the useful review for spotting misconfigured automation | | `--from`, `--to` | Time bounds, inclusive. See [Dates and durations](#dates-and-durations) | | `--limit ` | Events per request, 1 to 1000; default 100 | | `--offset ` | Skip this many events. When more events match than were returned, the table footer prints the `--offset` value for the next page | The default table shows time, action, outcome, resource, actor, status, and IP. Use `-o json` for the complete events, including the request path and the action-specific `eventData`. Field-level detail: [Audit log reference](/firewall/references/audit-log). ### bsfw oidc exchange [#bsfw-oidc-exchange] Exchange a GitHub Actions OIDC ID token for a short-lived Service Access Token. With `permissions: id-token: write`, the CLI fetches the GitHub token automatically. The exchange endpoint does not require an existing access token. ```shell bsfw oidc exchange --audience bytesafe:trusted-builder: ``` | Flag | Description | | ---------------------- | ------------------------------------------------------------------------- | | `--audience ` | Trusted Builder audience; defaults to `BYTESAFE_TRUSTED_BUILDER_AUDIENCE` | | `--id-token ` | Supply the OIDC token instead of fetching it from GitHub Actions | | `--format token\|json` | Print only the token or the full response; default `token` | End-to-end setup: [GitHub Actions](/firewall/getting-started/github-actions). ### bsfw completion [#bsfw-completion] Generate completion scripts for bash, zsh, fish, or PowerShell. Homebrew installs completions automatically; release archives include generated completion files. ```shell bsfw completion zsh > "${fpath[1]}/_bsfw" bsfw completion bash > /etc/bash_completion.d/bsfw ``` ### bsfw version [#bsfw-version] Print the installed release, commit, build time, Go version, and platform. Release archives also include generated man pages. ```shell bsfw version -o json ``` ## Exit codes [#exit-codes] Scripts can branch on these instead of parsing messages. | Code | Meaning | Usually means | | ---- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `0` | Success | | | `1` | Runtime error | The server rejected the request, for example an import that failed validation | | `2` | Usage error | A misspelled flag or value, a missing argument, or no endpoint, namespace, or firewall configured | | `3` | Authentication or authorization failure (401 or 403) | Missing, expired, or wrong-namespace token, or a token without the permission the command needs. Run `bsfw whoami` | | `4` | Not found (404) | The firewall, namespace, or package version does not exist under that ID. Check with `bsfw firewall list` | | `5` | Network failure or timeout | The endpoint is unreachable, or the request took longer than `--timeout` | ## Related [#related] * How-to: [View logs](/firewall/how-to/view-logs), [Review the audit log](/firewall/how-to/review-audit-log), [Manage upstream credentials](/firewall/how-to/manage-upstream-credentials) * Getting started: [Access Tokens](/firewall/getting-started/access-tokens), [GitHub Actions](/firewall/getting-started/github-actions), [Config as code](/firewall/getting-started/config-as-code) * Reference: [API](/firewall/references/api), [Observations](/firewall/references/observations), [Audit log](/firewall/references/audit-log) # Client operations (/firewall/references/client-operations) A firewall endpoint speaks the package ecosystem's own protocol, so your existing clients work without a plugin or a wrapper. It implements the part of that protocol that moving packages requires. This page is what that covers. ## What the firewall is for [#what-the-firewall-is-for] The firewall sits between your clients and your upstream registries and repository managers. Everything it implements serves the three things a package manager asks a registry for: * **Which versions of this package exist.** The version list a client resolves against. * **The file for one version.** The artifact download. * **Take this file I am publishing.** The upload, sent to the upstream you nominate as the publish target. Each one is a firewall [execution phase](/firewall/concepts/execution-phases), so your rules apply to all three. Which client commands work follows from that, and so does which ones do not. ## Operations by ecosystem [#operations-by-ecosystem] | Operation | npm | PyPI | Maven | NuGet | Go | Cargo | Composer | Conda | | ------------------- | -------------------- | --------------- | ------------- | -------------------------------- | ------------ | ----------- | ---------- | ------------------------ | | Clients | npm, yarn, pnpm, Bun | pip, uv, Poetry | Maven, Gradle | dotnet, nuget.exe, Visual Studio | go toolchain | cargo 1.74+ | Composer 2 | conda, mamba, micromamba | | Resolve versions | yes | yes | yes | yes | yes | yes | yes | yes | | Download artifact | yes | yes | yes | yes | yes | yes | yes | yes | | Publish | yes | yes | yes | yes | n/a | yes | n/a | no | | Delete | yes | n/a | n/a | yes | n/a | yes | n/a | n/a | | Vulnerability audit | yes | n/a | n/a | yes | n/a | n/a | yes | n/a | | Search service | no | n/a | n/a | yes | n/a | no | no | n/a | The **Clients** row is what each ecosystem's own page sets up. Clients that speak the same protocol behave the same through a firewall, so the row is the ecosystem's protocol restated as tool names. Cargo carries a version floor because registry-wide authentication did not exist before Rust 1.74. Every ecosystem resolves versions and downloads artifacts. The remaining rows differ because the protocols themselves differ, so the table separates two cases: * **n/a**: the ecosystem's repository protocol defines no such operation. For example, the Go module proxy and Composer repository protocols have no publish operation. Composer packages are published by tagging a release in version control, not by uploading to the repository used for installs. * **no**: the protocol defines the operation, but the firewall endpoint does not implement it. The firewall serves Conda channels read-only, so it registers no upload route. Packagist advertises search and package-listing endpoints, but a Composer firewall advertises neither, and the Cargo web API defines search and owner management, which a Cargo firewall answers 501. Two **Delete** cells mean something narrower than removal. NuGet's delete API unlists a version, and Cargo's equivalent is `cargo yank`, which withdraws a version from new resolution without removing it; `cargo yank --undo` reverses it. Both are policed as uploads, because both change what a consumer resolves. The supported vulnerability audits use different data paths. `npm audit` is forwarded to npmjs.org, so it reports what npmjs.org knows and needs network access to that host. The firewall answers `composer audit` from its own [advisory dataset](/firewall/references/advisory-sources), so it reports the same data as the `vulnerabilities` selector function and works without a public upstream. An ecosystem can still publish the same information through a service that sits outside its repository protocol, as pkg.go.dev, search.maven.org, and PyPI's JSON API do. A package manager does not reach those through a firewall endpoint, and routing your clients through a firewall leaves them as they were. ## What the firewall does not implement [#what-the-firewall-does-not-implement] A registry does more than serve packages. It also holds user accounts, issues tokens, and answers search queries. None of that fetches or pushes a package, so there is nothing in it for a firewall to decide: no version to resolve, no artifact to check, no rule that could apply. Account, token, and discovery requests are outside what a firewall endpoint implements, and they return 404. Discovery sits outside it because you choose a package on the upstream's own site, before it enters your project. Policy belongs where the package is resolved and downloaded, which is what the table above covers. NuGet is the exception, and only because its clients browse the feed itself. Accounts and tokens sit outside it because identity is not registry-side here. A client presents an access token created in Bytesafe, described below. The operations table above is the dependable statement of what works. If a command maps to a row in it, the firewall supports it. ### How authentication works [#how-authentication-works] Every request to a firewall endpoint carries an [access token](/firewall/getting-started/access-tokens), including metadata reads. There is no login step and no session. 1. Create a token in the dashboard, or with [`bsfw`](/firewall/references/cli). Personal tokens identify a person, Service Access Tokens identify a CI system. 2. Put it in your client's own credential configuration, which is the `.npmrc`, `NuGet.Config`, `settings.xml`, or index URL shown on each [ecosystem page](/firewall/ecosystems). The client then sends it on every request. That is why an endpoint has nothing to log into and nothing to answer about who a client is: the token is the identity, and it is issued outside the registry protocol. To check which identity a token carries, run `bsfw whoami`. ## Related [#related] * Ecosystems: [All ecosystems](/firewall/ecosystems) for per-client setup * Concept: [Execution phases](/firewall/concepts/execution-phases) for where policy runs on each operation * Reference: [Selector functions](/firewall/references/selector-functions), [CLI](/firewall/references/cli) # Exceptions (/firewall/references/exceptions) An exception waives one rule for requests matching its selector. It is consulted only after the rule's own selector has matched, and only while unexpired. For the mental model, see [Exception model](/firewall/concepts/exception-model); for workflows, see [Manage exceptions](/firewall/how-to/manage-exceptions). ## Shape [#shape] ```json { "id": "allow-lodash-41720", "rule": "block-critical-vulns", "selector": { "packageName": "lodash", "version": "4.17.20" }, "log": true, "expires": "2026-08-01T00:00:00Z", "reason": "not_exploitable", "notes": "Not reachable from our code. JIRA-1234." } ``` ## Fields [#fields] | Field | Type | Description | | ---------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique within the firewall. Recorded on log entries for requests the exception allowed | | `rule` | string | ID of the rule this exception waives. Exactly one rule per exception | | `selector` | object | Same shape as a [rule selector](/firewall/references/rules-and-selectors#selector), including wildcards and functions. Scope narrowly | | `log` | boolean | Write a log entry (message `EXCEPTION`, carrying the exception ID) when the exception allows a request | | `expires` | string | RFC 3339 timestamp. From this moment the exception stops matching and the rule applies again. Optional; omitted means no expiry | | `reason` | string | One of the reason codes below | | `notes` | string | Free text; the place for ticket links and justification detail | ## Reasons [#reasons] | Config value | Dashboard label | Meaning | | ------------------------ | ---------------------- | ------------------------------------------------------------------------- | | `acknowledged_violation` | Acknowledged Violation | The violation is known and accepted for now | | `evaluating_component` | Evaluating Component | The package is under assessment, temporary access | | `mitigated_externally` | Mitigated Externally | The risk is handled outside the package, for example WAF or configuration | | `no_upgrade_path` | No Upgrade Path | No fixed version is available yet | | `not_exploitable` | Not Exploitable | The vulnerability cannot be triggered in this usage | | `not_reachable` | Not Reachable | The vulnerable code path is not reachable | | `other` | Other | Anything else, explained in `notes` | ## Evaluation semantics [#evaluation-semantics] * The exception is checked only after its rule's selector matched the request. * Expired exceptions are skipped entirely, as if deleted. * If several exceptions on a rule exist, the first match wins; further exceptions and the rule's effect are not evaluated for that request. * An exception affects only its own rule. All other rules still see the request. ## Related [#related] * Concept: [Exception model](/firewall/concepts/exception-model), [Rule evaluation](/firewall/concepts/rule-evaluation) * How-to: [Manage exceptions](/firewall/how-to/manage-exceptions) * Reference: [Rules and selectors](/firewall/references/rules-and-selectors) # Firewall config (/firewall/references/firewall-config) The firewall config is the complete description of one firewall: identity, upstreams, policy, and access grants. It is what `bsfw firewall export` writes and `bsfw firewall import` applies, and what the dashboard edits piece by piece. ## Shape [#shape] ```json { "id": "3e7f5cb806f26e3beae2", "version": 12, "ecosystem": "npm", "name": "npm production", "description": "Firewall for all production npm installs", "upstreams": [ { "id": "npmjs", "endpoint": "https://registry.npmjs.org", "credentials": {}, "internal": false, "cached": false } ], "publishTarget": "npmjs", "rules": [ { "id": "delay-new-versions", "executionPhase": "versions", "selector": { "maxAgeHours": 168 }, "ruleEffect": { "block": true, "log": true }, "description": "Block versions younger than 7 days" } ], "exceptions": [], "permissions": { "everybody": ["fw:access", "fw:clone", "fw:read"], "platform": ["fw:access", "fw:read", "fw:update:rule", "fw:update:exception"] }, "updatedAt": "2026-07-02T09:30:00Z" } ``` ## Fields [#fields] | Field | Type | Description | | --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique within the namespace. Part of every client-facing URL, so treat it as permanent. See [Firewall ID](#firewall-id) | | `version` | number | Incremented on every config change. Log entries record the version they were evaluated under, and caches key on it | | `ecosystem` | string | `npm`, `pypi`, `maven`, `nuget`, `go`, `cargo`, `composer`, or `conda`. Choose it at creation and treat it as permanent | | `upstreams` | array | Registries the firewall resolves from. See [Upstreams](/firewall/references/upstreams) | | `publishTarget` | string | ID of the upstream that receives publishes. Optional; without it the firewall is install-only | | `rules` | array | Array order is not evaluation order: rules are evaluated cheapest first. See [Evaluation order](/firewall/references/rules-and-selectors#evaluation-order) | | `exceptions` | array | Waivers bound to rules by ID. See [Exceptions](/firewall/references/exceptions) | | `name` | string | Display name. Optional | | `description` | string | Free text. Optional | | `parentId` | string | ID of a firewall to inherit configuration from. Optional. Must name an existing firewall of the same ecosystem, and must not create a cycle. See [Share configuration across firewalls](/firewall/how-to/share-configuration-across-firewalls) | | `permissions` | object | Map of team ID to permission list for this firewall. See [Permissions](/firewall/references/permissions) | | `updatedAt` | string | RFC 3339 timestamp of the last change. Set by the server | With `parentId` set, the stored config keeps holding only the firewall's own entries; inherited rules, exceptions, and upstreams are resolved from the parent chain at serving time. Exports and the granular rule/exception endpoints operate on the firewall's own entries only. The dashboard does not let you change a firewall's ecosystem. A full config import may accept the change when no other firewall inherits from it, but it does not translate the config. The API rejects selector functions or version ranges that are incompatible with the new ecosystem, but it does not check whether upstream endpoints speak that ecosystem's protocol. Create a new firewall with the required ecosystem instead. ## Firewall ID [#firewall-id] The ID is a path segment in every client-facing URL, so it is constrained to characters that need no escaping: * Letters, digits, and dashes only * Must start and end with a letter or digit, so no leading dash (which reads as a flag on a command line) and no trailing dash * At most 128 characters The server rejects anything else with 400. The rule applies to every config write, not just creation, so an existing firewall with a non-conforming ID cannot carry it forward through an import. The dashboard generates conforming IDs on its own. ## Import semantics [#import-semantics] `bsfw firewall import` PUTs the whole object: * Importing a config with a new `id` creates the firewall (requires `fw:create`). * Importing an existing `id` replaces its configuration. * Plaintext values in `credentials` are rejected. On an existing firewall, an exported placeholder keeps the stored value for the same upstream. When a new firewall ID or upstream has no stored value, the placeholder resolves to empty; set its credentials separately through the dashboard or [`bsfw firewall credentials`](/firewall/references/cli#bsfw-firewall-credentials). * The ecosystem must be one of the supported values. Rules and exceptions whose selector function or version range does not support that ecosystem are rejected. * `upstreamType` is empty, `internal`, or `external`. The old selector key `internal` is rejected; use `upstreamType` instead. * The `permissions` map is optional on create. Without one, the server applies the [defaults for a new firewall](/firewall/references/permissions#defaults-for-a-new-firewall). The create request body also accepts an `owner` field (a team ID, not stored on the config): the named team receives the full default permission set instead of the `everybody` team. * A `permissions` map that would leave no team with `fw:update:config`, or would drop the caller's own ability to update the configuration, is rejected; see [Permissions](/firewall/references/permissions#lockout-guard). See [CLI](/firewall/references/cli#bsfw-firewall-import). ## Related [#related] * Reference: [Upstreams](/firewall/references/upstreams), [Rules and selectors](/firewall/references/rules-and-selectors), [Exceptions](/firewall/references/exceptions), [Permissions](/firewall/references/permissions), [CLI](/firewall/references/cli) * How-to: [Configure upstreams](/firewall/how-to/configure-upstreams) # All references (/firewall/references) Exhaustive lookup tables and field-level detail. See [concepts](/firewall/concepts) for the mental models, [how-to guides](/firewall/how-to) for tasks. ## Rules and policy [#rules-and-policy] ## Upstreams [#upstreams] ## Clients [#clients] ## Access [#access] ## Automation [#automation] ## Logging and observability [#logging-and-observability] # Logs (/firewall/references/logs) 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](/firewall/how-to/view-logs); for how they relate to the other records, see [Observability](/firewall/concepts/observability). ## Entry shape [#entry-shape] ```json { "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 [#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](/firewall/how-to/share-configuration-across-firewalls). 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](/firewall/how-to/scan-for-secrets#what-a-match-looks-like) | | `upstreamError` | object | Present only on [upstream error entries](#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 [#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. ```json { "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 [#what-is-recorded] * Rate limiting (`429`), server errors (`5xx`), denied access (`403`), and any other unexpected status. * `401` from an upstream that has credentials stored. That is how a wrong or expired credential shows up; see [Manage upstream credentials](/firewall/how-to/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: * `404` and `410`. 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. * `401` from an upstream that has no credentials stored. Some registries answer this way instead of `404` for 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](/firewall/references/webhooks). ## Query API [#query-api] ```text GET /v1//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](#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. ```json { "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 [#streaming] ```text GET /v1//log/stream ``` Server-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 [#related] * How-to: [View logs](/firewall/how-to/view-logs), [Investigate a blocked install](/firewall/how-to/investigate-blocked-install) * Concept: [Observability](/firewall/concepts/observability) * Reference: [CLI](/firewall/references/cli#bsfw-logs), [API](/firewall/references/api) # Metrics (/firewall/references/metrics) Metrics are time-bucketed counters of what the firewall processed and enforced. The dashboard charts them on the namespace overview and on each firewall's page; the API endpoints below return the raw numbers for your own monitoring. The [CLI](/firewall/references/cli) has no metrics command. For which record answers which question, see [Observability](/firewall/concepts/observability). ## Endpoints [#endpoints] ```text GET /v1//metrics GET /v1//metrics/firewall/ ``` | Parameter | Description | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `periodMinutes` | Window to return, ending now. Default 1440 (24 hours), maximum 43200 (30 days) | | `bucketMinutes` | Size of each returned bucket, maximum 1440 (24 hours). Metrics are recorded per minute and rolled up server-side. Defaults by period: 1 up to 1 hour, 5 up to 6 hours, 10 up to 24 hours, 60 up to 7 days, 180 beyond | Reading metrics requires only namespace access. ## Response shape [#response-shape] ```json { "periodMinutes": 1440, "bucketMinutes": 10, "buckets": [ { "timestamp": "2026-07-03T09:00:00Z", "requests": 412, "downloads": 268, "published": 2, "processedVersions": 3801, "blockedFilteredVersions": 121, "blockedDownloads": 4, "blockedUploads": 0, "allowedByException": 1, "rules": 6, "exceptions": 3 } ], "totals": { "requests": 9822, "downloads": 6120, "published": 14, "processedVersions": 88213, "blockedFilteredVersions": 2901, "blockedDownloads": 37, "blockedUploads": 1, "allowedByException": 12 }, "previousTotals": { "requests": 8514, "downloads": 5390, "published": 9, "processedVersions": 79102, "blockedFilteredVersions": 2544, "blockedDownloads": 29, "blockedUploads": 0, "allowedByException": 10 }, "inventoryCurrent": { "rules": 6, "exceptions": 3 } } ``` `totals` sums the buckets over the period. `previousTotals` sums the window of equal length immediately before it, for period-over-period comparison; those buckets are not returned. `inventoryCurrent` is the rule and exception count from the most recent bucket. Within a bucket, counters sum the underlying minutes; the `rules` and `exceptions` gauges take the highest value observed. ## Counters [#counters] | Counter | Counts | | ------------------------- | --------------------------------------------------------------------------------------------------- | | `requests` | Package requests handled | | `downloads` | Artifacts served | | `published` | Publishes forwarded to the publish target | | `processedVersions` | Versions the firewall read from upstream metadata responses | | `blockedFilteredVersions` | Versions filtered out of a metadata response by rules | | `blockedDownloads` | Downloads blocked by rules, on either the versions or the download phase | | `blockedUploads` | Publishes blocked by rules | | `allowedByException` | Package accesses a rule would have blocked but an exception allowed through, counted in every phase | | `rules` | Rules configured (gauge per bucket) | | `exceptions` | Exceptions configured (gauge per bucket) | The enforcement counters describe different outcomes: * `blockedFilteredVersions` counts versions the firewall removed from package metadata before the client picked one. The client usually does not notice: a version range resolves to an older version that passed the rules. This counter runs high compared to the other two: it counts filtered versions in metadata responses, not failed installs. See [rule evaluation](/firewall/concepts/rule-evaluation) for how filtering plays out per request shape. * `blockedDownloads` and `blockedUploads` are hard stops: a client asked for a specific artifact, or tried to publish one, and a rule refused. Artifact requests run the versions-phase rules too, so a client that skips resolution and fetches a filtered version directly is counted here, not under `blockedFilteredVersions`. These are the failures users experience, and the [firewall log](/firewall/references/logs) shows which rule and which package. * `allowedByException` counts requests a rule would have blocked but an exception let through. If it rises steadily, those exceptions carry real traffic every day and are worth a [review](/firewall/how-to/manage-exceptions#review-existing-exceptions). ## Related [#related] * Concept: [Observability](/firewall/concepts/observability) * Reference: [Logs](/firewall/references/logs), [API](/firewall/references/api) * Per-webhook delivery metrics are separate: [Webhooks](/firewall/references/webhooks#metrics) # Observations (/firewall/references/observations) An observation is one package version the firewall has served, with when and how often. Together they are a usage inventory derived from real traffic: not what manifests declare, but what builds actually fetched. For how observations relate to logs and metrics, see [Observability](/firewall/concepts/observability). ## Endpoint [#endpoint] ```text GET /v1//observations ``` | Parameter | Description | | ----------- | ----------------------------------------------------------- | | `firewall` | Limit to one firewall | | `period` | `today` (default), `7d`, `30d`, or `90d`, by last-seen time | | `pageSize` | Page size | | `pageToken` | Cursor from the previous response's `nextPageToken` | Reading observations requires only namespace access. ## CLI [#cli] The same inventory from the terminal, as a table or as JSON. Without `--period` it covers today only, so widen the window for a review. ```shell bsfw observations --period 30d --firewall 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.` | The audit event with that action was recorded, e.g. `audit.firewall.rule.upsert`; one type per [audit action](/firewall/references/audit-log#action-catalog) | | `config.firewall.created`, `config.firewall.updated`, `config.firewall.deleted` | A firewall was created, changed (config, rule, exception, credentials, or rollback; one event per change), or deleted. Carries the complete config after the change | | `config.webhook.created`, `config.webhook.updated`, `config.webhook.deleted` | A webhook was created, changed (including secret rotation and quarantine release), or deleted. Signing secrets are never included | | `package.published` | A package version passed the upload rules and was forwarded to the publish-target upstream (npm, Maven, PyPI, NuGet) | | `package.unlisted` | A NuGet package version was unlisted (DELETE) on the publish-target upstream | | `vulnerability.*` | Reserved, not emitted yet (shown as coming soon and disabled in the dashboard picker) | | `webhook.rate_limited` | System event: the webhook's buffer overflowed (`reason: "buffer_overflow"`, with `droppedEvents`) or deliveries are being paced against a backlog (`reason: "rate_limited"`) | | `webhook.test` | System event: a test send | A subscription lists the event types to receive, either as exact types (`firewall.blocked`) or as a prefix with a trailing `.*` (`firewall.*`, `audit.*`, `audit.team.*`, `config.*`, `package.*`): * A bare `*` is rejected; name the families you want. * Exact `firewall.*`, `config.*`, and `package.*` names must be one of the types in the table above. Exact `audit.` and `vulnerability.` names are not checked against a list, so a subscription to a specific action keeps working when new actions are added. * `webhook.*` types cannot be subscribed to: the system events are always delivered and cannot be turned off. * Firewall events come from `download` and `upload` decisions only. Version filtering on a metadata request never produces events, however many versions it removes. A rule on the `versions` [execution phase](/firewall/concepts/rule-evaluation#execution-phases) that blocks an artifact fetch counts as a `download` decision and is delivered. * Only decisions are delivered. [Upstream error entries](/firewall/references/logs#upstream-error-entries) record a failed call to an upstream registry rather than a rule that fired, so they never produce events. Read them in the firewall log instead. `audit.*` and `config.*` deliberately overlap: the audit event is the security log (successes and failures, thin metadata), the config event is the automation feed (successes only, carrying the full resource state after the change). ## Firewall scope [#firewall-scope] A webhook can optionally restrict the data-plane events, `firewall.*` and `package.*`, the only types that name a firewall, to a selection of the namespace's firewalls. An empty selection means every firewall. `audit.*`, `config.*`, and `webhook.*` events are never filtered by the selection. Selecting an unknown firewall is rejected at create/update; a firewall deleted after being selected simply stops matching and does not block later edits. ## Envelope [#envelope] Every delivery is one JSON object with a batch of events, oldest first, up to 100 events or 512 KB per delivery: ```json { "version": 1, "events": [ { "id": "evt-01JZX2M8Q0V5T2C7N9R4W6YB3D", "type": "firewall.blocked", "time": "2026-07-13T09:30:00Z", "namespace": "ns-01KYA296JWEE36NDNRTJ4KM82Q", "data": { "firewall": "3e7f5cb806f26e3beae2", "executionPhase": "download", "blocked": true, "ecosystem": "npm", "packageName": "left-pad", "version": "1.3.0", "rule": "log-install-scripts-download", "message": "Install scripts detected: postinstall" } } ] } ``` Event `id`s are stable: a retried delivery carries the same events with the same ids. Every payload shape is described by the published [JSON Schema](#json-schema). ## Event data [#event-data] `data` holds the per-type payload, described below per event family. Fields marked "when set" are omitted when empty; every other field is always present (`blocked` is sent even when `false`). ### `firewall.*` [#firewall] One event per download or upload decision, carrying the fields of the [firewall log entry](/firewall/references/logs#fields), which documents each in detail: | Field | Meaning | | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `firewall`, `firewallVersion` | Firewall that made the decision, and the config version it evaluated under | | `executionPhase` | `download` or `upload`. The phase is a field, not a separate event type | | `blocked` | Whether the request was blocked | | `ecosystem`, `packageName`, `version` | The package the decision was about | | `upstream` | Upstream that served or would have served the package | | `rule` | Rule that matched | | `user` | Who made the request, as an opaque user ID | | `request` | Request ID, also shown in the client's blocked-install error message | | `message` | Rule description, or rule-specific message such as `Malware detected in npm/example@1.0.0: ...` | | `sourceFirewall`, `sourceFirewallVersion` | When set: the firewall the matched rule was [inherited](/firewall/how-to/share-configuration-across-firewalls) from, and its config version. Absent when the rule is the firewall's own | | `exception`, `exceptionSourceFirewall`, `exceptionSourceFirewallVersion` | When set, on `firewall.exception-allowed`: the exception that let the request through, and where it was inherited from | | `published` | When set: the version's publish date upstream | | `output` | When set: rule-specific detail, such as vulnerability IDs and scores or a malware finding's summary, scanner, confidence, and signals | | `interaction` | When set: [interaction ID](/firewall/getting-started/access-tokens#correlating-requests-with-interaction-ids) from the token suffix | | `userAgent`, `remoteAddress` | When set: client metadata | ### `audit.*` [#audit] The fields of the [audit event](/firewall/references/audit-log#fields), which documents each in detail: | Field | Meaning | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `action` | What was done, one of the [audit actions](/firewall/references/audit-log#action-catalog) | | `outcome`, `statusCode` | `success` or `failure`, and the HTTP status returned | | `resourceType`, `resourceId` | When set: the resource the action targeted | | `actorUserId`, `actorTokenIdentifier` | When set: acting user's ID, and a hash-derived identifier of the acting token (never token material) | | `requestId`, `interactionId` | When set: request ID, shared with firewall log entries from the same request, and interaction ID from the token suffix | | `remoteAddr`, `userAgent`, `requestMethod`, `requestPath` | When set: request metadata | | `errorMessage` | When set: short failure reason | | `eventData` | When set: action-specific detail, for example the missing permission on a denial | ### `config.firewall.*` [#configfirewall] | Field | Meaning | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `firewall` | The firewall that changed | | `version` | Config version after the change | | `ecosystem` | The firewall's ecosystem | | `actor` | Who made the change, see [the actor object](#the-actor-object) | | `config` | The complete [firewall configuration](/firewall/references/firewall-config) after the change. `config.firewall.deleted` carries the last known config | Upstream credentials inside `config` are always the redaction placeholder, never credential material. That covers [artifact host credentials](/firewall/references/upstreams#artifact-host-credentials) too, which appear as an `artifactCredentials` array on the upstream. ### `config.webhook.*` [#configwebhook] | Field | Meaning | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `webhook` | The webhook that changed | | `actor` | Who made the change, see [the actor object](#the-actor-object) | | `config` | The webhook's configuration: id, name, url, enabled state, event types, firewall scope, secret-rotation and quarantine timestamps. Signing secrets have no field in this shape at all | ### `package.*` [#package] | Field | Meaning | | ------------------------------------- | ----------------------------------------------------------------------------------- | | `firewall` | Firewall the publish went through | | `ecosystem`, `packageName`, `version` | The published package | | `actor` | Who published, see [the actor object](#the-actor-object) | | `upstream` | When set: the publish-target upstream the upload was forwarded to | | `file` | When set: the upload request path, for ecosystems that publish one file per request | Maven and PyPI upload one file per request, so one release can emit several events; deduplicate on `ecosystem`, `packageName`, and `version`. ### The actor object [#the-actor-object] `config.*` and `package.*` events carry an `actor` object identifying who caused the change. Every field is omitted when empty: | Field | Meaning | | ---------------------------- | ----------------------------------------------------------------- | | `userId` | Acting user's ID | | `tokenIdentifier` | Hash-derived identifier of the acting token, never token material | | `requestId` | Request ID of the request that caused the change | | `remoteAddress`, `userAgent` | Client metadata | ### System events [#system-events] `webhook.rate_limited` reports delivery pressure on the webhook itself: | Field | Meaning | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `webhook` | The affected webhook | | `reason` | `buffer_overflow`: events were dropped and there is a gap in the stream. `rate_limited`: deliveries are paced against a backlog of more than 1000 buffered events, nothing lost yet | | `droppedEvents` | Events discarded at the buffer cap; they will never arrive | | `bufferedEvents` | Events waiting for delivery | `webhook.test` carries the `webhook` id and a fixed `message` ("Test event"). ## JSON Schema [#json-schema] `GET /schemas/webhook-events-v1.json` on the dashboard origin returns a JSON Schema describing the envelope and every event payload, generated from the same types that render the events. It is a public contract document, served without authentication; use it to generate types or validate deliveries in the receiver. ## Signing [#signing] Deliveries are signed per the [Standard Webhooks](https://www.standardwebhooks.com/) convention. Three headers on every POST: | Header | Value | | ------------------- | ----------------------------------------------------------------------------------------------------- | | `webhook-id` | The delivery ID (`whdel-...`), stable across retries of the same batch; use it as the idempotency key | | `webhook-timestamp` | Unix time in seconds at the attempt | | `webhook-signature` | One or more space-separated `v1,` values | Each value in `webhook-signature` is an HMAC over the delivery: proof that it was produced by someone holding the webhook's signing secret and that the body was not modified in transit. The delivery id and timestamp are signed together with the body, so a captured signature cannot be reused for a different delivery, and the timestamp gives the receiver a way to reject stale deliveries. The signature is computed as: ```text base64( HMAC-SHA256( key, "{webhook-id}.{webhook-timestamp}.{body}" ) ) ``` `key` is the binary HMAC key inside the signing secret: the secret is `whsec_` followed by the base64-encoded key. To verify a delivery: 1. Base64-decode the signing secret after the `whsec_` prefix; the result is the HMAC key. 2. Join the `webhook-id` header, the `webhook-timestamp` header, and the request body with dots. Use the raw body bytes as received, before any JSON parsing. 3. Compute HMAC-SHA256 over that string with the key, and base64-encode the result. 4. Compare it in constant time against each space-separated `v1,` value; the delivery is authentic if any signature matches. Standard Webhooks libraries implement exactly this, so a library call with the secret and the headers is usually all a receiver needs. The secret is returned on create and managers can reveal or rotate it later; see [Receive webhook events](/firewall/how-to/receive-webhook-events). For 24 hours after a secret rotation, deliveries carry two signatures, new and previous secret, so receivers on either secret keep verifying. Requests are sent with `content-type: application/json` and user agent `bytesafe-d3-webhooks/1`. ## Delivery [#delivery] | Behavior | Value | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | Success | Any `2xx` response. Redirects are not followed and count as failures | | Attempt timeout | 10 seconds | | Retry schedule | 30s, 2m, 10m, 30m, 1h after the immediate attempt (6 attempts total) | | `Retry-After` | Honored on `429` and `503`, capped at 1 hour | | Ordering | Per webhook, oldest first, one delivery in flight at a time | | Pacing | At most 5 deliveries per second per webhook | | Batch size | Up to 100 events or 512 KB per delivery | | Buffer | 5000 events per webhook; on overflow the oldest are dropped and a `webhook.rate_limited` event with `reason: "buffer_overflow"` reports the gap | | Guarantee | At-least-once from buffering to retry exhaustion; deduplicate on `webhook-id` | ## Quarantined webhooks [#quarantined-webhooks] When a batch exhausts all retry attempts, it is recorded as `failed` and dropped; that batch is not delivered again. The webhook itself is quarantined for 24 hours. During quarantine, no deliveries are attempted, but new events keep buffering up to the buffer cap. When the quarantine expires, delivery resumes automatically with the buffered backlog. To resume earlier, release the quarantine manually: the dashboard shows the quarantine reason and a **Release now** action on the webhook, and the API equivalent is `POST .../webhooks/{webhook}/unquarantine`. Quarantine is a state of the webhook's deliveries, not of any package: the firewall never holds a package back for review. ## Delivery records [#delivery-records] The last 128 deliveries per webhook are kept as a ring buffer for debugging, each with the signed payload and the endpoint's response: | Field | Description | | ----------------------------- | --------------------------------------------------------------------------------- | | `id` | Delivery ID, equals the `webhook-id` header | | `test` | `true` for test sends | | `status` | `retrying`, `success`, or `failed` (the dashboard shows `success` as "delivered") | | `attempts` | Attempts made so far | | `eventCount`, `eventTypes` | Batch contents summary | | `payload`, `payloadTruncated` | The delivered body, stored up to 64 KB | | `responseStatus` | HTTP status of the last attempt; `0` means no response (network error, timeout) | | `responseBody` | Endpoint response, stored up to 4 KB | | `error` | Last transport error, when any | | `durationMillis` | Last attempt duration | | `createdAt`, `completedAt` | First attempt and final outcome times | ## Metrics [#metrics] Delivery metrics answer whether a receiver is keeping up: how many deliveries succeed and fail, how many events get through, and whether any are lost. Rising `deliveriesFailed` or any `eventsDropped` is the signal to check the endpoint and the [delivery records](#delivery-records). `GET .../webhooks/{webhook}/metrics?periodMinutes=...` returns them per webhook; the dashboard's **Metrics** tab charts the same data. `periodMinutes` defaults to 1440 (24 hours) and caps at 10080 (7 days). Buckets are per minute; unlike [firewall metrics](/firewall/references/metrics), longer periods have no coarser rollup. The response carries a `buckets` array and a `totals` object summing the same counters over the whole period: | Field | Meaning | | ------------------- | -------------------------------------------------------------------------------- | | `timestamp` | Bucket start. Buckets only | | `deliveriesSuccess` | Delivery attempts answered with `2xx` | | `deliveriesFailed` | Failed delivery attempts, including retries of the same batch | | `eventsSent` | Events contained in successful deliveries | | `eventsDropped` | Events lost: dropped at the buffer cap, or in a batch that exhausted its retries | | `avgDeliveryMillis` | Average attempt duration in the bucket. Buckets only, not in `totals` | ## Endpoint requirements [#endpoint-requirements] The endpoint URL must be `http` or `https` with a hostname that is not on the firewall's rejected-domain list, and it must resolve to a public address: requests to private, loopback, and link-local addresses are refused. The delivery client never follows redirects. ## Related [#related] * How-to: [Receive webhook events](/firewall/how-to/receive-webhook-events) * Reference: [API](/firewall/references/api#webhooks), [Logs](/firewall/references/logs), [Audit log](/firewall/references/audit-log), [Permissions](/firewall/references/permissions) * Concept: [Observability](/firewall/concepts/observability) # Support (/firewall/support) ## Contact [#contact] Use the form below or email [support@bytesafe.dev](mailto:support@bytesafe.dev). To attach screenshots or other files, use the chat in the lower-right corner or email us. For legal, privacy, and terms, see [bytesafe.dev](https://bytesafe.dev). ## Frequently asked questions [#frequently-asked-questions] npm, PyPI, Maven, NuGet, Go, Cargo, Composer, and Conda. Each has a setup page under [Ecosystems](/firewall/ecosystems), which also marks which are in beta. The message contains a request ID. Follow [Investigate a blocked install](/firewall/how-to/investigate-blocked-install) to trace it to the rule that fired, and [Manage exceptions](/firewall/how-to/manage-exceptions) if the package should be allowed. Usually a rule on the versions phase filtered it, most often a [delay rule](/firewall/how-to/delay-new-versions) and a recently published version. Check the [firewall logs](/firewall/how-to/view-logs) before assuming an outage. Add an [exception](/firewall/how-to/manage-exceptions): scoped to the package and version, with a reason and an expiry. The rule keeps applying to everything else. The firewall proxies upstream responses and adds one network hop. It can also cache responses from internal upstreams, so repeated installs are served without a round trip to that registry. See [Upstreams and caching](/firewall/concepts/upstreams-and-caching). Rule evaluation is ordered by cost rather than by configuration: a request pays for the expensive checks only when the cheaper rules have allowed it, and a block skips everything after it. See [Evaluation order](/firewall/references/rules-and-selectors#evaluation-order). Yes, see [Self-hosted](/firewall/self-hosted). Container image firewalling is coming in Q4 2026. Today the firewall covers the package ecosystems listed above. Contact us if this matters for your adoption.