feat(host-rules): ignore URL credentials when matching matchHost
`matchesHost()` compared the raw href of the searched URL against
`matchHost`, so a URL carrying `user:pass@` userinfo (often a literal
placeholder such as `${USER}:${PASS}@`) never matched a `matchHost`
without credentials, including path-scoped ones. Callers worked around
this by stripping `username`/`password` off the URL by hand before
calling `hostRules.find()`.
- Add `stripUrlCredentials()` to `lib/util/url.ts`, returning the URL's
`href` without userinfo (or `null` if unparseable), covered by
`lib/util/url.spec.ts`.
- `matchesHost()` in `lib/util/host-rules.ts` now strips credentials
from the searched URL before comparing it against an HTTP(S)
`matchHost`, so `hostRules.find()` ignores userinfo on every caller's
URL. Covered by new cases in `lib/util/host-rules.spec.ts` (a
`matchHost` rule matches a URL carrying `user:pass@`, including a
path-scoped `matchHost`).
- Remove the hand-rolled strip in
`lib/modules/datasource/pypi/host-rules.ts`'s
`findPypiIndexCredentials()`, now redundant since `find()` does it.
`lib/modules/datasource/pypi/host-rules.spec.ts` already covers this
behaviour and needed no changes.
This is strictly additive: more URLs match `matchHost` than before,
and nothing that matched before stops matching.
As part of looking to allow Inherit Config to manage Host Rules' grants,
we need to perform a prefactor to create an additional trust tier
between the global self-hosted administrator's config, and presets/repo
config.
The `find()` method already resolves sensitive fields - `headers`,
`enabled`, and the computed internal-host grant - a "trust tier" at a
time, so that the outcome cannot depend on how specific a rule from a
less trusted tier makes itself. The new `inherit` tier slots in between
the two we had:
- `headers` are applied untrusted first, then inherited, then the
administrator's, so theirs still win outright
- `enabled` is taken from the administrator's rules, falling back to
inherited config's, then a repository's
- an administrator's `allowInternal: false` acts as a veto: it is
resolved before inherited config's rules, and clears the scoped
and implicit grants too, so inherited config cannot re-grant the
host in any shape by out-scoping it. Meanwhile any inherited rule
naming a host in its `matchHost` - credentialed or not - now
implicitly permits it, like the admin's own rules do
We introduce a separate field than `trusted` to avoid Inherit Config
gaining the same precedence as the admin themselves.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
As part of follow-up changes, we're looking to tighten the way that
Renovate handles outgoing HTTP traffic.
Before we do this, we can introduce a new config option,
`hostRules[].allowInternal`, that allows a self-hosted administrator to
mark an internal host as "trusted" for access.
This wires in the new global-only config option, `allowInternal`, which
implies a new internal field `internalHostGrant`.
We'll add full documentation in a future commit, when the full
enforcement is in place, as there are some wider changes to enforce it
fully.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
As part of follow-up changes, we're looking to tighten the way that
Renovate handles outgoing HTTP traffic, and a self-hosted administrator
disabling a host needs to be something a repository cannot undo.
In a similar change to
4732da7d2b, we can resolve `enabled` per
trust tier, so that a repository or preset rule can no longer re-enable
- or disable - a host the administrator's own rules set, which was
previously possible with a longer `matchHost`.
Within a tier nothing changes: the most specific rule still wins, so a
repository disabling a host of its own keeps working, as does an
administrator masking their own broad rule with a narrower one.
This is a behaviour change for repository config that previously relied
on overriding an admin rule's `enabled`.
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
As part of changes we've recently made to the merging of
`hostRules[].headers`, it highlighted that a repository - or a preset
it extends - could lead to a header an admin has configured for one host
to be sent to another.
This happened due to the way we additively merged i.e.
`Object.assign({}, ...matchedHeaders)`.
This doesn't require the repository to know a credential is being
set (for a specific host), or to act maliciously - any header of their
own for that host is enough to bring the administrator's masked one
back, with nothing logged or failing to show it happened.
To fix this, we can:
- apply admin headers last, as they are "trusted"
- stop repositories from being able to unset an admin-set header
- retain the existing logic for the "most specific rule" being used
- make sure that `trusted` isn't a field that could be injected by a
repository, and make sure we explicitly only set it where we know
we're using self-hosted administrator config
- make `filterAllowedHeaders` drop `headers` if every header name was
denied, so `{}` isn't returned and used to suppress headers when
merged
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
We previously enforced `allowedHeaders` by making sure that each
`hostRules.add()` call had `filterAllowedHeaders`.
This isn't super future proofed, and although we could add a linting
rule, we should instead enforce this by making sure that
`hostRules.add()` applies the filtering.
This also catches cases like host rules from `.npmrc` which wasn't
currently being filtered - albeit there was no means to bypass
`allowedHeaders`.
Moving `filterAllowedHeaders` into `lib/util` removes a dependency from
the global bootstrap and `config-validator` importing from
`lib/workers/repository/init/`.
While doing so, we can also make sure that:
- we log only one WARN for global `hostRules` being invalid, rather than
once per `repositories[]`
- `renovate-config-validator` validates a global config file's
`hostRules` against that file's own `allowedHeaders`, like a real run
would after parsing it
Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io>
Previously, if there was a `hostRule[].headers` value for a given host
set in both global self-hosted configuration and repo config, the repo
config would overwrite /all/ headers set by the admin.
To resolve this, we can now correctly merge the keys, with the repo
config taking priority.
This still only takes into account the `allowedHeaders`, so a repository
cannot bypass admin settings.
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
* feat(lint): add renovate/prefer-is-helpers and renovate/prefer-is-object rules
Flag ad-hoc type checks that have @sindresorhus/is equivalents:
- .filter(Boolean) -> .filter(isTruthy)
- typeof x === 'string' -> isString(x)
- x === null || x === undefined -> isNullOrUndefined(x) (and negation)
The typeof x === 'object' shape lives in a separate suggestion-only rule
(renovate/prefer-is-object, kept at warn) because isObject()/isPlainObject()
are not drop-in equivalents: null, arrays and functions behave differently.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: fix renovate/prefer-is-helpers violations
Replace ad-hoc type checks with @sindresorhus/is helpers across lib/:
- 13x .filter(Boolean) -> .filter(isTruthy)
- 36x typeof x ==/!== 'string' -> isString(x) / !isString(x)
- 6x x === null || x === undefined -> isNullOrUndefined(x) (and negation)
Promote renovate/prefer-is-helpers to error now that the codebase is clean.
renovate/prefer-is-object stays at warn: its 15 remaining sites guard
parsed YAML/JSON or unknown values where isObject()/isPlainObject() are
not provably equivalent (null/array/function semantics).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: enforce renovate/prefer-is-object at error level
Resolve the 15 remaining warnings and raise the rule from warn to error:
- Migrate 10 sites to isObject() where the guarded value provably cannot
be a function (parsed YAML/JSON, typed exec command arguments, or
test-controlled fixtures): helm-values, woodpecker, bun, pre-commit
managers, exec utils, file package cache, and the github http spec.
In the bun manager this also fixes a latent crash where a null
`workspaces` value would throw on the `in` operator.
- Keep 5 sites as reasoned oxlint-disable comments where excluding
functions is load-bearing: the byte-exact fingerprint canonicalizer
(2), the circularity walker, the zod result guard, and the generic
hasKey helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* chore(util): note that `httpsCertificate` and `httpsCertificateAuthority` aren't secret
Although we're currently treating them as such, they're not technically
unsafe to appear in the logs, as a Certificate (and the authority) are
public.
* fix(util/host-rules): sanitise TLS credential fields in hostRules
We previously noted that the `httpsPrivateKey`, `httpsCertificate`, and
`httpsCertificateAuthority` fields would be marked as confidential, but
we weren't correctly sanitising them.
We can make sure that these fields are correctly flagged as secret
("confidential") and that they need to be sanitised when referenced.
Although the `httpsCertificate` and `httpsCertificateAuthority` aren't
secret, we can keep them sanitised for consistency with how we've
currently documented them as being sanitised.
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
* test(util/host-rules): ensure that all secret fields are marked as secret
As a way to make sure that new fields being added `HostRule`s are
considered as to whether the field should be marked as secret
("confidential") or not, we can add a test (and a compile-time
exhaustive check) to validate that `redactedFields` and
`confidentialFields` are kept in sync.
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
---------
Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>