renovate 44.78.0 → 44.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/config/global.d.ts +1 -1
  2. package/dist/config/global.js +2 -0
  3. package/dist/config/global.js.map +1 -1
  4. package/dist/config/options/index.js +35 -0
  5. package/dist/config/options/index.js.map +1 -1
  6. package/dist/config/presets/http/index.js +3 -1
  7. package/dist/config/presets/http/index.js.map +1 -1
  8. package/dist/config/presets/index.js +7 -1
  9. package/dist/config/presets/index.js.map +1 -1
  10. package/dist/config/presets/npm/index.js +4 -2
  11. package/dist/config/presets/npm/index.js.map +1 -1
  12. package/dist/config/types.d.ts +4 -1
  13. package/dist/config/types.js.map +1 -1
  14. package/dist/config/validation.js +18 -0
  15. package/dist/config/validation.js.map +1 -1
  16. package/dist/constants/error-messages.d.ts +2 -1
  17. package/dist/constants/error-messages.js +2 -1
  18. package/dist/constants/error-messages.js.map +1 -1
  19. package/dist/global-config-option-defaults.generated.js +2 -0
  20. package/dist/global-config-option-defaults.generated.js.map +1 -1
  21. package/dist/modules/datasource/docker/common.js +3 -2
  22. package/dist/modules/datasource/docker/common.js.map +1 -1
  23. package/dist/modules/datasource/index.js +1 -1
  24. package/dist/modules/datasource/index.js.map +1 -1
  25. package/dist/modules/datasource/maven/util.js +3 -2
  26. package/dist/modules/datasource/maven/util.js.map +1 -1
  27. package/dist/modules/datasource/npm/get.js +1 -1
  28. package/dist/modules/datasource/npm/get.js.map +1 -1
  29. package/dist/modules/datasource/pod/index.js +2 -1
  30. package/dist/modules/datasource/pod/index.js.map +1 -1
  31. package/dist/modules/datasource/repology/index.js +3 -2
  32. package/dist/modules/datasource/repology/index.js.map +1 -1
  33. package/dist/types/host-rules.d.ts +35 -2
  34. package/dist/types/index.d.ts +2 -2
  35. package/dist/util/host-rules.d.ts +8 -2
  36. package/dist/util/host-rules.js +54 -6
  37. package/dist/util/host-rules.js.map +1 -1
  38. package/dist/util/http/host-guard.js +279 -0
  39. package/dist/util/http/host-guard.js.map +1 -0
  40. package/dist/util/http/host-rules.js.map +1 -1
  41. package/dist/util/http/http.d.ts +8 -0
  42. package/dist/util/http/http.js +37 -21
  43. package/dist/util/http/http.js.map +1 -1
  44. package/dist/util/http/types.d.ts +6 -0
  45. package/dist/util/http/types.js.map +1 -1
  46. package/dist/util/http/util.js +10 -1
  47. package/dist/util/http/util.js.map +1 -1
  48. package/dist/workers/repository/init/inherited.js +28 -4
  49. package/dist/workers/repository/init/inherited.js.map +1 -1
  50. package/dist/workers/repository/init/merge.js +28 -12
  51. package/dist/workers/repository/init/merge.js.map +1 -1
  52. package/package.json +1 -1
  53. package/renovate-schema.json +17 -2
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../lib/modules/datasource/repology/index.ts"],"sourcesContent":["import { isNonEmptyArray } from '@sindresorhus/is';\nimport { HOST_DISABLED } from '../../../constants/error-messages.ts';\nimport { logger } from '../../../logger/index.ts';\nimport { ExternalHostError } from '../../../types/errors/external-host-error.ts';\nimport { withCache } from '../../../util/cache/package/with-cache.ts';\nimport { getQueryString, joinUrlParts } from '../../../util/url.ts';\nimport { Datasource } from '../datasource.ts';\nimport type { GetReleasesConfig, ReleaseResult } from '../types.ts';\nimport { type RepologyPackage, RepologyPackages } from './schema.ts';\nimport type { RepologyPackageType } from './types.ts';\n\nconst packageTypes: RepologyPackageType[] = ['binname', 'srcname'];\n\nfunction findPackageInResponse(\n response: RepologyPackage[],\n repoName: string,\n pkgName: string,\n types: RepologyPackageType[],\n): RepologyPackage[] | null {\n const repoPackages = response.filter((pkg) => pkg.repo === repoName);\n\n if (repoPackages.length === 0) {\n // no packages associated with repoName\n return null;\n }\n\n if (repoPackages.length === 1) {\n // repo contains exactly one package, so we can return them safely\n return repoPackages;\n }\n\n // In some cases Repology bundles multiple packages into a single project, which might result in ambiguous results.\n // We need to do additional filtering by matching allowed package types passed as params with package description.\n // Remaining packages are the one we are looking for\n const packagesWithType = repoPackages.filter((pkg) => {\n for (const pkgType of types) {\n if (pkg[pkgType] && pkg[pkgType] === pkgName) {\n return true;\n }\n }\n return false;\n });\n\n return packagesWithType.length > 0 ? packagesWithType : null;\n}\n\nexport class RepologyDatasource extends Datasource {\n static readonly id = 'repology';\n\n override readonly defaultRegistryUrls = ['https://repology.org/'];\n\n override readonly registryStrategy = 'hunt';\n\n constructor() {\n super(RepologyDatasource.id);\n }\n\n private async queryPackages(url: string): Promise<RepologyPackage[]> {\n try {\n const res = await this.http.getJson(url, RepologyPackages);\n return res.body;\n } catch (err) {\n if (err.statusCode === 404) {\n // Return an array here because the api does not return proper http codes\n // and instead of an 404 error an empty array with code 200 is returned\n // When querying the resolver 404 is thrown if package could not be resolved\n // and 403 if the repo is not supported\n // 403 is handled later because in this case we are trying the API\n return [];\n }\n\n throw err;\n }\n }\n\n private async queryPackagesViaResolver(\n registryUrl: string,\n repoName: string,\n packageName: string,\n packageType: RepologyPackageType,\n ): Promise<RepologyPackage[]> {\n const query = getQueryString({\n repo: repoName,\n name_type: packageType,\n target_page: 'api_v1_project',\n noautoresolve: 'on',\n name: packageName,\n });\n\n // Retrieve list of packages by looking up Repology project\n const packages = await this.queryPackages(\n joinUrlParts(registryUrl, `tools/project-by?${query}`),\n );\n\n return packages;\n }\n\n private async queryPackagesViaAPI(\n registryUrl: string,\n packageName: string,\n ): Promise<RepologyPackage[]> {\n // Directly query the package via the API. This will only work if `packageName` has the\n // same name as the repology project\n const packages = await this.queryPackages(\n joinUrlParts(registryUrl, `api/v1/project`, packageName),\n );\n\n return packages;\n }\n\n private async _queryPackage(\n registryUrl: string,\n repoName: string,\n pkgName: string,\n ): Promise<RepologyPackage[] | undefined> {\n let response: RepologyPackage[];\n // Try getting the packages from tools/project-by first for type binname and\n // afterwards for srcname. This needs to be done first, because some packages\n // resolve to repology projects which have a different name than the package\n // e.g. `pulseaudio-utils` resolves to project `pulseaudio`, BUT there is also\n // a project called `pulseaudio-utils` but it does not contain the package we\n // are looking for.\n try {\n for (const pkgType of packageTypes) {\n response = await this.queryPackagesViaResolver(\n registryUrl,\n repoName,\n pkgName,\n pkgType,\n );\n\n if (response) {\n const pkg = findPackageInResponse(response, repoName, pkgName, [\n pkgType,\n ]);\n if (isNonEmptyArray(pkg)) {\n // exit immediately if package found\n return pkg;\n }\n }\n }\n } catch (err) {\n if (err.statusCode === 403) {\n logger.debug(\n { repoName, pkgName },\n 'Repology does not support tools/project-by lookups for repository. Will try direct API access now',\n );\n\n // If the repository is not supported in tools/project-by we try directly accessing the\n // API. This will support all repositories but requires that the project name is equal to the\n // package name. This won't be always the case but for a good portion we might be able to resolve\n // the package this way.\n response = await this.queryPackagesViaAPI(registryUrl, pkgName);\n const pkg = findPackageInResponse(\n response,\n repoName,\n pkgName,\n packageTypes,\n );\n if (isNonEmptyArray(pkg)) {\n // exit immediately if package found\n return pkg;\n }\n } else if (err.statusCode === 300) {\n logger.warn(\n { repoName, pkgName },\n 'Ambiguous redirection from package name to project name in Repology. Skipping this package',\n );\n return undefined;\n }\n\n throw err;\n }\n\n logger.debug(\n { repoName, pkgName },\n 'Repository or package not found on Repology',\n );\n\n return undefined;\n }\n\n queryPackage(\n registryUrl: string,\n repoName: string,\n pkgName: string,\n ): Promise<RepologyPackage[] | undefined> {\n return withCache(\n {\n ttlMinutes: 60,\n namespace: `datasource-${RepologyDatasource.id}`,\n key: joinUrlParts(registryUrl, repoName, pkgName),\n },\n () => this._queryPackage(registryUrl, repoName, pkgName),\n );\n }\n\n async getReleases({\n packageName,\n registryUrl,\n }: GetReleasesConfig): Promise<ReleaseResult | null> {\n /* v8 ignore next -- should never happen */\n if (!registryUrl) {\n return null;\n }\n // Ensure lookup name contains both repository and package\n const [repoName, pkgName] = packageName.split('/', 2);\n if (!repoName || !pkgName) {\n throw new ExternalHostError(\n new Error(\n 'Repology lookup name must contain repository and package separated by slash (<repo>/<pkg>)',\n ),\n );\n }\n\n logger.trace(`repology.getReleases(${repoName}, ${pkgName})`);\n try {\n // Try to retrieve (cached) package information from Repology\n const pkg = await this.queryPackage(registryUrl, repoName, pkgName);\n if (!pkg) {\n return null;\n }\n\n // Always prefer origversion if available, otherwise default to version\n // This is required as source packages usually have no origversion\n const releases = pkg.map((item) => ({\n version: item.origversion ?? item.version,\n }));\n return { releases };\n } catch (err) {\n if (err.message === HOST_DISABLED) {\n logger.trace({ packageName, err }, 'Host disabled');\n } else {\n logger.once.warn(\n { packageName, err },\n 'Repology lookup failed with unexpected error',\n );\n }\n\n this.handleGenericErrors(err);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../lib/modules/datasource/repology/index.ts"],"sourcesContent":["import { isNonEmptyArray } from '@sindresorhus/is';\nimport {\n HOST_BLOCKED,\n HOST_DISABLED,\n} from '../../../constants/error-messages.ts';\nimport { logger } from '../../../logger/index.ts';\nimport { ExternalHostError } from '../../../types/errors/external-host-error.ts';\nimport { withCache } from '../../../util/cache/package/with-cache.ts';\nimport { refusedHostMessage } from '../../../util/http/util.ts';\nimport { getQueryString, joinUrlParts } from '../../../util/url.ts';\nimport { Datasource } from '../datasource.ts';\nimport type { GetReleasesConfig, ReleaseResult } from '../types.ts';\nimport { type RepologyPackage, RepologyPackages } from './schema.ts';\nimport type { RepologyPackageType } from './types.ts';\n\nconst packageTypes: RepologyPackageType[] = ['binname', 'srcname'];\n\nfunction findPackageInResponse(\n response: RepologyPackage[],\n repoName: string,\n pkgName: string,\n types: RepologyPackageType[],\n): RepologyPackage[] | null {\n const repoPackages = response.filter((pkg) => pkg.repo === repoName);\n\n if (repoPackages.length === 0) {\n // no packages associated with repoName\n return null;\n }\n\n if (repoPackages.length === 1) {\n // repo contains exactly one package, so we can return them safely\n return repoPackages;\n }\n\n // In some cases Repology bundles multiple packages into a single project, which might result in ambiguous results.\n // We need to do additional filtering by matching allowed package types passed as params with package description.\n // Remaining packages are the one we are looking for\n const packagesWithType = repoPackages.filter((pkg) => {\n for (const pkgType of types) {\n if (pkg[pkgType] && pkg[pkgType] === pkgName) {\n return true;\n }\n }\n return false;\n });\n\n return packagesWithType.length > 0 ? packagesWithType : null;\n}\n\nexport class RepologyDatasource extends Datasource {\n static readonly id = 'repology';\n\n override readonly defaultRegistryUrls = ['https://repology.org/'];\n\n override readonly registryStrategy = 'hunt';\n\n constructor() {\n super(RepologyDatasource.id);\n }\n\n private async queryPackages(url: string): Promise<RepologyPackage[]> {\n try {\n const res = await this.http.getJson(url, RepologyPackages);\n return res.body;\n } catch (err) {\n if (err.statusCode === 404) {\n // Return an array here because the api does not return proper http codes\n // and instead of an 404 error an empty array with code 200 is returned\n // When querying the resolver 404 is thrown if package could not be resolved\n // and 403 if the repo is not supported\n // 403 is handled later because in this case we are trying the API\n return [];\n }\n\n throw err;\n }\n }\n\n private async queryPackagesViaResolver(\n registryUrl: string,\n repoName: string,\n packageName: string,\n packageType: RepologyPackageType,\n ): Promise<RepologyPackage[]> {\n const query = getQueryString({\n repo: repoName,\n name_type: packageType,\n target_page: 'api_v1_project',\n noautoresolve: 'on',\n name: packageName,\n });\n\n // Retrieve list of packages by looking up Repology project\n const packages = await this.queryPackages(\n joinUrlParts(registryUrl, `tools/project-by?${query}`),\n );\n\n return packages;\n }\n\n private async queryPackagesViaAPI(\n registryUrl: string,\n packageName: string,\n ): Promise<RepologyPackage[]> {\n // Directly query the package via the API. This will only work if `packageName` has the\n // same name as the repology project\n const packages = await this.queryPackages(\n joinUrlParts(registryUrl, `api/v1/project`, packageName),\n );\n\n return packages;\n }\n\n private async _queryPackage(\n registryUrl: string,\n repoName: string,\n pkgName: string,\n ): Promise<RepologyPackage[] | undefined> {\n let response: RepologyPackage[];\n // Try getting the packages from tools/project-by first for type binname and\n // afterwards for srcname. This needs to be done first, because some packages\n // resolve to repology projects which have a different name than the package\n // e.g. `pulseaudio-utils` resolves to project `pulseaudio`, BUT there is also\n // a project called `pulseaudio-utils` but it does not contain the package we\n // are looking for.\n try {\n for (const pkgType of packageTypes) {\n response = await this.queryPackagesViaResolver(\n registryUrl,\n repoName,\n pkgName,\n pkgType,\n );\n\n if (response) {\n const pkg = findPackageInResponse(response, repoName, pkgName, [\n pkgType,\n ]);\n if (isNonEmptyArray(pkg)) {\n // exit immediately if package found\n return pkg;\n }\n }\n }\n } catch (err) {\n if (err.statusCode === 403) {\n logger.debug(\n { repoName, pkgName },\n 'Repology does not support tools/project-by lookups for repository. Will try direct API access now',\n );\n\n // If the repository is not supported in tools/project-by we try directly accessing the\n // API. This will support all repositories but requires that the project name is equal to the\n // package name. This won't be always the case but for a good portion we might be able to resolve\n // the package this way.\n response = await this.queryPackagesViaAPI(registryUrl, pkgName);\n const pkg = findPackageInResponse(\n response,\n repoName,\n pkgName,\n packageTypes,\n );\n if (isNonEmptyArray(pkg)) {\n // exit immediately if package found\n return pkg;\n }\n } else if (err.statusCode === 300) {\n logger.warn(\n { repoName, pkgName },\n 'Ambiguous redirection from package name to project name in Repology. Skipping this package',\n );\n return undefined;\n }\n\n throw err;\n }\n\n logger.debug(\n { repoName, pkgName },\n 'Repository or package not found on Repology',\n );\n\n return undefined;\n }\n\n queryPackage(\n registryUrl: string,\n repoName: string,\n pkgName: string,\n ): Promise<RepologyPackage[] | undefined> {\n return withCache(\n {\n ttlMinutes: 60,\n namespace: `datasource-${RepologyDatasource.id}`,\n key: joinUrlParts(registryUrl, repoName, pkgName),\n },\n () => this._queryPackage(registryUrl, repoName, pkgName),\n );\n }\n\n async getReleases({\n packageName,\n registryUrl,\n }: GetReleasesConfig): Promise<ReleaseResult | null> {\n /* v8 ignore next -- should never happen */\n if (!registryUrl) {\n return null;\n }\n // Ensure lookup name contains both repository and package\n const [repoName, pkgName] = packageName.split('/', 2);\n if (!repoName || !pkgName) {\n throw new ExternalHostError(\n new Error(\n 'Repology lookup name must contain repository and package separated by slash (<repo>/<pkg>)',\n ),\n );\n }\n\n logger.trace(`repology.getReleases(${repoName}, ${pkgName})`);\n try {\n // Try to retrieve (cached) package information from Repology\n const pkg = await this.queryPackage(registryUrl, repoName, pkgName);\n if (!pkg) {\n return null;\n }\n\n // Always prefer origversion if available, otherwise default to version\n // This is required as source packages usually have no origversion\n const releases = pkg.map((item) => ({\n version: item.origversion ?? item.version,\n }));\n return { releases };\n } catch (err) {\n if ([HOST_BLOCKED, HOST_DISABLED].includes(err.message)) {\n logger.trace({ packageName, err }, refusedHostMessage(err));\n } else {\n logger.once.warn(\n { packageName, err },\n 'Repology lookup failed with unexpected error',\n );\n }\n\n this.handleGenericErrors(err);\n }\n }\n}\n"],"mappings":"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"}
@@ -8,6 +8,17 @@ interface HostRule {
8
8
  timeout?: number;
9
9
  abortOnError?: boolean;
10
10
  abortIgnoreStatusCodes?: number[];
11
+ /**
12
+ * Whether requests to this host may reach internal addresses.
13
+ *
14
+ * As they have been marked as trusted by the administrator:
15
+ *
16
+ * - when `internalHostAccess=block`, these are not blocked
17
+ * - when `internalHostAccess=warn`, no warning occurs
18
+ *
19
+ * This is only allowed in global self-hosted configuration, and if found in repository or preset config, it is stripped.
20
+ */
21
+ allowInternal?: boolean;
11
22
  enabled?: boolean;
12
23
  enableHttp2?: boolean;
13
24
  concurrentRequestLimit?: number;
@@ -25,7 +36,29 @@ interface HostRule {
25
36
  resolvedHost?: string;
26
37
  readOnly?: boolean;
27
38
  }
28
- type CombinedHostRule = Omit<HostRule, 'encrypted' | 'hostType' | 'matchHost' | 'resolvedHost' | 'readOnly'>;
39
+ /**
40
+ * How the trusted host rules treat internal-host access for a request, computed by `find()`. Never settable through configuration: only rules from the administrator's own config, and from organization-inherited config the administrator has opted into trusting through `inheritConfigTrusted`, contribute to it.
41
+ *
42
+ * The administrator's own rules are resolved first, and an `allowInternal: false` of theirs vetoes every field below: inherited config can never re-grant a host the administrator refused, in any grant shape.
43
+ */
44
+ interface InternalHostGrant {
45
+ /**
46
+ * The `allowInternal` value of the matching trusted rules, with the most specific rule winning, and the administrator's own rules winning over inherited config's. `undefined` when none of them set it.
47
+ */
48
+ explicit?: boolean;
49
+ /**
50
+ * As `explicit`, but counting only rules deliberately scoped by a `hostType` or by a URL-prefix `matchHost`. Surfaces where a grant must be deliberate - such as HTTP presets, whose responses become config - check this value instead of `explicit`.
51
+ */
52
+ scoped?: boolean;
53
+ /**
54
+ * Whether any trusted rule naming a host (`matchHost`) matched the request: hosts named in configuration the administrator trusts are implicitly permitted. Always `false` under an administrator's veto.
55
+ */
56
+ implicit: boolean;
57
+ }
58
+ type CombinedHostRule = Omit<HostRule, 'encrypted' | 'hostType' | 'matchHost' | 'resolvedHost' | 'readOnly' | 'allowInternal'> & {
59
+ /** computed by `find()` - see {@link InternalHostGrant} */
60
+ internalHostGrant?: InternalHostGrant;
61
+ };
29
62
  //#endregion
30
- export { CombinedHostRule, HostRule };
63
+ export { CombinedHostRule, HostRule, InternalHostGrant };
31
64
  //# sourceMappingURL=host-rules.d.ts.map
@@ -5,7 +5,7 @@ import { ProcessResult, ProcessStatus, RepositoryResult } from "../workers/repos
5
5
  import { ModuleApi, RenovatePackageJson } from "./base.js";
6
6
  import { BranchStatus } from "./branch-status.js";
7
7
  import { CommitMessageJSON } from "./commit-message-json.js";
8
- import { CombinedHostRule, HostRule } from "./host-rules.js";
8
+ import { CombinedHostRule, HostRule, InternalHostGrant } from "./host-rules.js";
9
9
  import { PrState } from "./pr-state.js";
10
10
  import { SkipReason, StageName } from "./skip-reason.js";
11
11
  import { RangeStrategy } from "./versioning.js";
@@ -33,5 +33,5 @@ type MaybePromise<T> = T | Promise<T>;
33
33
  */
34
34
  type Extends<T, U extends T> = U;
35
35
  //#endregion
36
- export { AutoMergeType, type BranchCache, type BranchResult, type BranchStatus, type BranchUpgradeCache, type BunyanRecord, type CombinedHostRule, type CommitMessageJSON, Extends, type HostRule, MaybePromise, type MergeStrategy, type ModuleApi, Nullish, type PrBlockedBy, type PrState, type ProcessResult, type ProcessStatus, type RangeStrategy, type RenovatePackageJson, type RenovateSplit, type RepositoryResult, type SkipReason, type StageName, type UpdateType, type ValidationMessage };
36
+ export { AutoMergeType, type BranchCache, type BranchResult, type BranchStatus, type BranchUpgradeCache, type BunyanRecord, type CombinedHostRule, type CommitMessageJSON, Extends, type HostRule, type InternalHostGrant, MaybePromise, type MergeStrategy, type ModuleApi, Nullish, type PrBlockedBy, type PrState, type ProcessResult, type ProcessStatus, type RangeStrategy, type RenovatePackageJson, type RenovateSplit, type RepositoryResult, type SkipReason, type StageName, type UpdateType, type ValidationMessage };
37
37
  //# sourceMappingURL=index.d.ts.map
@@ -32,11 +32,17 @@ interface AddHostRuleOptions {
32
32
  /** the effective allowlist. Defaults to `GlobalConfig`; pass it explicitly when `GlobalConfig` does not yet reflect the repository the rule is registered for */
33
33
  allowedHeaders?: string[];
34
34
  /**
35
- * Whether this rule comes from the self-hosted administrator's own global config, rather than from repository or preset config.
35
+ * Whether this rule comes from the self-hosted administrator's own configuration, rather than from repository or preset config.
36
36
  *
37
- * Only affects how `headers` are combined (see {@link find}), and is deliberately opt-in: a caller that says nothing registers into the untrusted tier, so a new call site cannot grant itself the administrator's precedence by omission.
37
+ * Registers the rule into the `admin` tier - see {@link HostRuleTrustTier}.
38
38
  */
39
39
  trusted?: boolean;
40
+ /**
41
+ * Whether this rule comes from organization-inherited config which the administrator has opted into trusting, through `inheritConfigTrusted`.
42
+ *
43
+ * Registers the rule into the `inherit` tier - see {@link HostRuleTrustTier}. Ignored when `trusted` is also set.
44
+ */
45
+ inherited?: boolean;
40
46
  }
41
47
  declare function add(params: HostRule, options?: AddHostRuleOptions): void;
42
48
  interface HostRuleSearch {
@@ -5,7 +5,7 @@ import { addSecretForSanitizing, clearRepoSanitizedSecretsList } from "./sanitiz
5
5
  import { logger } from "../logger/index.js";
6
6
  import { clone } from "./clone.js";
7
7
  import { isHttpUrl, massageHostUrl, parseUrl } from "./url.js";
8
- import { isFalsy, isString, isTruthy, isUndefined } from "@sindresorhus/is";
8
+ import { isFalsy, isNonEmptyString, isString, isTruthy, isUndefined } from "@sindresorhus/is";
9
9
  //#region lib/util/host-rules.ts
10
10
  let hostRules = [];
11
11
  /**
@@ -73,10 +73,26 @@ function filterAllowedHeaders(rules, allowedHeaders, warnOnDenied = true) {
73
73
  if (denied.length && warnOnDenied) logger.warn({ denied }, "Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`");
74
74
  return result;
75
75
  }
76
+ /**
77
+ * The trust tier a caller asked for.
78
+ *
79
+ * Deliberately opt-in: a caller that says nothing registers into the untrusted tier, so a new call site cannot grant itself the administrator's precedence by omission.
80
+ */
81
+ function trustTierFor(options) {
82
+ if (options?.trusted) return "admin";
83
+ if (options?.inherited) return "inherit";
84
+ return "untrusted";
85
+ }
76
86
  function add(params, options) {
77
- let rule = migrateRule(params);
87
+ let rule = {
88
+ ...migrateRule(params),
89
+ trustTier: trustTierFor(options)
90
+ };
78
91
  delete rule.trusted;
79
- if (options?.trusted) rule.trusted = true;
92
+ if (!isUndefined(rule.allowInternal) && rule.trustTier === "untrusted") {
93
+ logger.debug(`Ignoring hostRules allowInternal for ${rule.matchHost ?? rule.hostType} from untrusted config`);
94
+ delete rule.allowInternal;
95
+ }
80
96
  if (rule.headers) [rule] = filterAllowedHeaders([rule], options?.allowedHeaders);
81
97
  if (rule.matchHost) {
82
98
  rule.matchHost = massageHostUrl(rule.matchHost);
@@ -128,6 +144,18 @@ function fromLowerRankAndShorterMatchHost(a, b) {
128
144
  function headersOfLastRuleToSetThem(rules) {
129
145
  return rules.map((rule) => rule.headers).filter(isTruthy).pop();
130
146
  }
147
+ /**
148
+ * The last defined value, following the same "most specific rule wins" ordering as {@link headersOfLastRuleToSetThem}.
149
+ */
150
+ function lastDefined(values) {
151
+ return values.filter((value) => !isUndefined(value)).pop();
152
+ }
153
+ /**
154
+ * The rules whose grant is deliberately scoped, by a `hostType` or by a URL-prefix `matchHost` - see {@link InternalHostGrant}.
155
+ */
156
+ function scopedRules(rules) {
157
+ return rules.filter((rule) => isNonEmptyString(rule.hostType) || isHttpUrl(rule.matchHost));
158
+ }
131
159
  function find(search) {
132
160
  if ([search.hostType, search.url].every(isFalsy)) {
133
161
  logger.warn({ search }, "Invalid hostRules search");
@@ -159,17 +187,37 @@ function find(search) {
159
187
  if (hostTypeMatch && readOnlyMatch && hostMatch) matchedRules.push(clone(rule));
160
188
  }
161
189
  const res = Object.assign({}, ...matchedRules);
162
- const untrustedHeaders = headersOfLastRuleToSetThem(matchedRules.filter((rule) => !rule.trusted));
163
- const trustedHeaders = headersOfLastRuleToSetThem(matchedRules.filter((rule) => rule.trusted));
164
- if (untrustedHeaders ?? trustedHeaders) res.headers = {
190
+ const trustedRules = matchedRules.filter((rule) => rule.trustTier === "admin");
191
+ const inheritedRules = matchedRules.filter((rule) => rule.trustTier === "inherit");
192
+ const untrustedRules = matchedRules.filter((rule) => rule.trustTier === "untrusted");
193
+ const untrustedHeaders = headersOfLastRuleToSetThem(untrustedRules);
194
+ const inheritedHeaders = headersOfLastRuleToSetThem(inheritedRules);
195
+ const trustedHeaders = headersOfLastRuleToSetThem(trustedRules);
196
+ if (untrustedHeaders ?? inheritedHeaders ?? trustedHeaders) res.headers = {
165
197
  ...untrustedHeaders,
198
+ ...inheritedHeaders,
166
199
  ...trustedHeaders
167
200
  };
201
+ const enabled = lastDefined(trustedRules.map((rule) => rule.enabled)) ?? lastDefined(inheritedRules.map((rule) => rule.enabled)) ?? lastDefined(untrustedRules.map((rule) => rule.enabled));
202
+ if (!isUndefined(enabled)) res.enabled = enabled;
203
+ const trustedExplicit = lastDefined(trustedRules.map((rule) => rule.allowInternal));
204
+ const trustedScoped = lastDefined(scopedRules(trustedRules).map((rule) => rule.allowInternal));
205
+ const adminVeto = trustedExplicit === false || trustedScoped === false;
206
+ const inheritedExplicit = adminVeto ? false : lastDefined(inheritedRules.map((rule) => rule.allowInternal));
207
+ const inheritedScoped = adminVeto ? false : lastDefined(scopedRules(inheritedRules).map((rule) => rule.allowInternal));
208
+ const internalHostGrant = {
209
+ explicit: trustedExplicit ?? inheritedExplicit,
210
+ scoped: trustedScoped ?? inheritedScoped,
211
+ implicit: !adminVeto && [...trustedRules, ...inheritedRules].some((rule) => isNonEmptyString(rule.matchHost))
212
+ };
213
+ if (!isUndefined(internalHostGrant.explicit) || !isUndefined(internalHostGrant.scoped) || internalHostGrant.implicit) res.internalHostGrant = internalHostGrant;
168
214
  delete res.hostType;
169
215
  delete res.resolvedHost;
170
216
  delete res.matchHost;
171
217
  delete res.readOnly;
218
+ delete res.trustTier;
172
219
  delete res.trusted;
220
+ delete res.allowInternal;
173
221
  return res;
174
222
  }
175
223
  function hosts({ hostType }) {
@@ -1 +1 @@
1
- {"version":3,"file":"host-rules.js","names":[],"sources":["../../lib/util/host-rules.ts"],"sourcesContent":["import { isFalsy, isString, isTruthy, isUndefined } from '@sindresorhus/is';\nimport { GlobalConfig } from '../config/global.ts';\nimport { logger } from '../logger/index.ts';\nimport type { CombinedHostRule, HostRule } from '../types/index.ts';\nimport { clone } from './clone.ts';\nimport * as sanitize from './sanitize.ts';\nimport { toBase64 } from './string.ts';\nimport { matchRegexOrGlobList } from './string-match.ts';\nimport { isHttpUrl, massageHostUrl, parseUrl } from './url.ts';\n\n/**\n * A host rule as registered through {@link add}.\n *\n * `trusted` is deliberately not a field of `HostRule`: it is set from {@link AddHostRuleOptions} at registration time and must never be settable through configuration.\n */\ninterface RegisteredHostRule extends HostRule {\n /** whether the rule came from the self-hosted administrator's own global config, rather than from repository or preset config */\n trusted?: boolean;\n}\n\nlet hostRules: RegisteredHostRule[] = [];\n\n/**\n * Fields within `HostRule`s that must have their value registered for sanitising through `sanitize.addSecretForSanitizing()`.\n *\n * Kept in sync with `redactedFields` through tests.\n */\nexport const confidentialFields: (keyof HostRule)[] = [\n 'password',\n 'token',\n 'httpsPrivateKey',\n /* not actually sensitive, but redacted nonetheless */\n 'httpsCertificate',\n /* not actually sensitive, but redacted nonetheless */\n 'httpsCertificateAuthority',\n];\n\nexport interface LegacyHostRule {\n hostName?: string;\n domainName?: string;\n baseUrl?: string;\n host?: string;\n endpoint?: string;\n}\n\nexport function migrateRule(rule: LegacyHostRule & HostRule): HostRule {\n const cloned: LegacyHostRule & HostRule = clone(rule);\n delete cloned.hostName;\n delete cloned.domainName;\n delete cloned.baseUrl;\n const result: HostRule = cloned;\n\n const { matchHost } = result;\n const { hostName, domainName, baseUrl } = rule;\n const hostValues = [matchHost, hostName, domainName, baseUrl].filter(\n isTruthy,\n );\n if (hostValues.length === 1) {\n const [matchHost] = hostValues;\n result.matchHost = matchHost;\n } else if (hostValues.length > 1) {\n throw new Error(\n `hostRules cannot contain more than one host-matching field - use \"matchHost\" only.`,\n );\n }\n\n return result;\n}\n\n/**\n * Enforce the `allowedHeaders` allowlist on a set of host rules.\n *\n * Loudly remove anything that's not permitted, logging a WARN.\n *\n * `add()` applies this to every rule it registers, so callers only need it themselves to pre-filter - e.g. to avoid repeating the WARN when the same rules are registered again and again.\n *\n * @param [allowedHeaders] the effective allowlist. Defaults to `GlobalConfig`, but must be passed explicitly when filtering before `GlobalConfig` reflects the repository being processed, i.e. for a `repositories[]` entry's own `allowedHeaders` override\n * @param [warnOnDenied=true] whether to log the WARN. Pass `false` where the very same rules are filtered again, so that it is logged once rather than repeated\n *\n * Headers that survive this allowlist are still subject to how {@link find} combines them: an admin's headers for a host are applied over those of any repository or preset rule matching the same request, so a repository can neither drop nor substitute them.\n */\nexport function filterAllowedHeaders(\n rules: HostRule[],\n allowedHeaders?: string[],\n warnOnDenied = true,\n): HostRule[] {\n // `??`, rather than a parameter default: a default only applies to `undefined`, and a `null` can reach us from user config\n const allowlist = allowedHeaders ?? GlobalConfig.get('allowedHeaders');\n const denied: string[] = [];\n\n const result = rules.map((rule) => {\n if (!rule.headers) {\n return rule;\n }\n\n const allowed: Record<string, string> = {};\n const ruleDenied: string[] = [];\n for (const [name, value] of Object.entries(rule.headers)) {\n if (matchRegexOrGlobList(name, allowlist)) {\n allowed[name] = value;\n } else {\n ruleDenied.push(name);\n }\n }\n\n if (!ruleDenied.length) {\n return rule;\n }\n denied.push(...ruleDenied);\n\n const filtered: HostRule = { ...rule, headers: allowed };\n if (!Object.keys(allowed).length) {\n // drop the key rather than leave an empty object behind: `find()` treats any rule that sets `headers` as replacing the headers of the broader rules of its own tier, so a rule left with none would suppress them\n delete filtered.headers;\n }\n return filtered;\n });\n\n if (denied.length && warnOnDenied) {\n logger.warn(\n { denied },\n \"Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`\",\n );\n }\n return result;\n}\n\nexport interface AddHostRuleOptions {\n /** the effective allowlist. Defaults to `GlobalConfig`; pass it explicitly when `GlobalConfig` does not yet reflect the repository the rule is registered for */\n allowedHeaders?: string[];\n\n /**\n * Whether this rule comes from the self-hosted administrator's own global config, rather than from repository or preset config.\n *\n * Only affects how `headers` are combined (see {@link find}), and is deliberately opt-in: a caller that says nothing registers into the untrusted tier, so a new call site cannot grant itself the administrator's precedence by omission.\n */\n trusted?: boolean;\n}\n\nexport function add(params: HostRule, options?: AddHostRuleOptions): void {\n let rule: RegisteredHostRule = migrateRule(params);\n\n // set only from `options`, and dropped first so that it cannot be carried over from `params`: `HostRule` has no `trusted` field, but configuration is parsed from JSON, so a repository could otherwise smuggle one in and have its headers treated as the administrator's\n delete rule.trusted;\n if (options?.trusted) {\n rule.trusted = true;\n }\n\n if (rule.headers) {\n // enforced here, at the single registration chokepoint, so that no current or future caller can register a rule whose headers bypass `allowedHeaders`; `applyHostRule` filters by header name again at request time as defence in depth\n [rule] = filterAllowedHeaders([rule], options?.allowedHeaders);\n }\n\n if (rule.matchHost) {\n rule.matchHost = massageHostUrl(rule.matchHost);\n const parsedUrl = parseUrl(rule.matchHost);\n rule.resolvedHost = parsedUrl?.hostname ?? rule.matchHost;\n confidentialFields.forEach((field) => {\n if (rule[field]) {\n logger.debug(\n // TODO: types (#22198)\n `Adding ${field} authentication for ${rule.matchHost!} (hostType=${\n rule.hostType\n }) to hostRules`,\n );\n }\n });\n }\n confidentialFields.forEach((field) => {\n const secret = rule[field];\n if (isString(secret) && secret.length > 3) {\n sanitize.addSecretForSanitizing(secret);\n }\n });\n if (rule.username && rule.password) {\n sanitize.addSecretForSanitizing(\n toBase64(`${rule.username}:${rule.password}`),\n );\n }\n hostRules.push(rule);\n}\n\nexport interface HostRuleSearch {\n hostType?: string;\n url?: string;\n readOnly?: boolean;\n}\n\nexport function matchesHost(url: string, matchHost: string): boolean {\n const parsedUrl = parseUrl(url);\n if (!parsedUrl) {\n return false;\n }\n\n const parsedMatchHost = parseUrl(matchHost);\n if (isHttpUrl(parsedUrl) && isHttpUrl(parsedMatchHost)) {\n return parsedUrl.href.startsWith(parsedMatchHost!.href);\n }\n\n const { hostname } = parsedUrl;\n if (!hostname) {\n return false;\n }\n\n if (hostname === matchHost) {\n return true;\n }\n\n const topLevelSuffix = matchHost.startsWith('.')\n ? matchHost\n : `.${matchHost}`;\n return hostname.endsWith(topLevelSuffix);\n}\n\nfunction fromShorterToLongerMatchHost(a: HostRule, b: HostRule): number {\n if (!a.matchHost || !b.matchHost) {\n return 0;\n }\n return a.matchHost.length - b.matchHost.length;\n}\n\nfunction hostRuleRank({ hostType, matchHost, readOnly }: HostRule): number {\n if ((hostType || readOnly) && matchHost) {\n return 3;\n }\n\n if (matchHost) {\n return 2;\n }\n\n if (hostType) {\n return 1;\n }\n\n return 0;\n}\n\nfunction fromLowerToHigherRank(a: HostRule, b: HostRule): number {\n return hostRuleRank(a) - hostRuleRank(b);\n}\n\n// A single comparator, rather than two consecutive sorts: `fromShorterToLongerMatchHost` returns 0 for rules without a `matchHost`, which makes it non-transitive on its own, so a standalone length sort could leave two `matchHost` rules unordered when a host-less rule sits between them\nfunction fromLowerRankAndShorterMatchHost(a: HostRule, b: HostRule): number {\n return fromLowerToHigherRank(a, b) || fromShorterToLongerMatchHost(a, b);\n}\n\n/**\n * The `headers` that apply from a set of matching rules of the same trust tier.\n *\n * The last rule to set any wins outright, as `find()`'s callers receive them sorted from least to most specific. That is the behaviour every matching rule had before `headers` were combined across tiers, and it is what lets a broad rule's headers be masked by a narrower rule from the same source.\n */\nfunction headersOfLastRuleToSetThem(\n rules: RegisteredHostRule[],\n): Record<string, string> | undefined {\n return rules\n .map((rule) => rule.headers)\n .filter(isTruthy)\n .pop();\n}\n\nexport function find(search: HostRuleSearch): CombinedHostRule {\n if ([search.hostType, search.url].every(isFalsy)) {\n logger.warn({ search }, 'Invalid hostRules search');\n return {};\n }\n\n // Sort primarily by rank, and secondarily by matchHost length\n const sortedRules = hostRules.sort(fromLowerRankAndShorterMatchHost);\n\n const matchedRules: RegisteredHostRule[] = [];\n for (const rule of sortedRules) {\n let hostTypeMatch = true;\n let hostMatch = true;\n let readOnlyMatch = true;\n\n if (rule.hostType) {\n hostTypeMatch = false;\n // v8 ignore else -- TODO: add test #40625\n if (search.hostType === rule.hostType) {\n hostTypeMatch = true;\n }\n }\n\n if (rule.matchHost && rule.resolvedHost) {\n hostMatch = false;\n if (search.url) {\n hostMatch = matchesHost(search.url, rule.matchHost);\n }\n }\n\n if (!isUndefined(rule.readOnly)) {\n readOnlyMatch = false;\n // v8 ignore else -- TODO: add test #40625\n if (search.readOnly === rule.readOnly) {\n readOnlyMatch = true;\n hostTypeMatch = true; // When we match `readOnly`, we don't care about `hostType`\n }\n }\n\n if (hostTypeMatch && readOnlyMatch && hostMatch) {\n matchedRules.push(clone(rule));\n }\n }\n\n const res: RegisteredHostRule = Object.assign({}, ...matchedRules);\n\n // `headers` are resolved per trust tier and then combined key by key, so that repository or preset config can no longer discard - or substitute - the headers a self-hosted admin configured for the same host\n // Within a tier nothing changes: the most specific rule's `headers` still replace those of the broader rules it is combined with, so an admin masking their own broad rule with a narrower one keeps working, as does a repository doing the same among its own rules\n // This is deliberately scoped to `headers` alone, as combining any more of the fields than we already do has caused authentication regressions before: an inherited `token` and a specific rule's `username`/`password` are not meant to be used together\n const untrustedHeaders = headersOfLastRuleToSetThem(\n matchedRules.filter((rule) => !rule.trusted),\n );\n const trustedHeaders = headersOfLastRuleToSetThem(\n matchedRules.filter((rule) => rule.trusted),\n );\n if (untrustedHeaders ?? trustedHeaders) {\n // the admin's own headers are applied last, so a repository cannot override one they set for this host either\n res.headers = { ...untrustedHeaders, ...trustedHeaders };\n }\n\n delete res.hostType;\n delete res.resolvedHost;\n delete res.matchHost;\n delete res.readOnly;\n delete res.trusted;\n return res;\n}\n\nexport function hosts({ hostType }: { hostType: string }): string[] {\n return hostRules\n .filter((rule) => rule.hostType === hostType)\n .map((rule) => rule.resolvedHost)\n .filter(isTruthy);\n}\n\nexport function hostType({ url }: { url: string }): string | null {\n return (\n hostRules\n .filter((rule) => rule.matchHost && matchesHost(url, rule.matchHost))\n .sort(fromShorterToLongerMatchHost)\n .map((rule) => rule.hostType)\n .filter(isTruthy)\n .pop() ?? null\n );\n}\n\nexport function findAll({ hostType }: { hostType: string }): HostRule[] {\n return hostRules.filter((rule) => rule.hostType === hostType);\n}\n\n/**\n * @returns a deep copy of all known host rules without any filtering\n */\nexport function getAll(): HostRule[] {\n return clone(hostRules);\n}\n\nexport function clear(): void {\n logger.debug('Clearing hostRules');\n hostRules = [];\n sanitize.clearRepoSanitizedSecretsList();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"host-rules.js","names":[],"sources":["../../lib/util/host-rules.ts"],"sourcesContent":["import {\n isFalsy,\n isNonEmptyString,\n isString,\n isTruthy,\n isUndefined,\n} from '@sindresorhus/is';\nimport { GlobalConfig } from '../config/global.ts';\nimport { logger } from '../logger/index.ts';\nimport type {\n CombinedHostRule,\n HostRule,\n InternalHostGrant,\n} from '../types/index.ts';\nimport { clone } from './clone.ts';\nimport * as sanitize from './sanitize.ts';\nimport { toBase64 } from './string.ts';\nimport { matchRegexOrGlobList } from './string-match.ts';\nimport { isHttpUrl, massageHostUrl, parseUrl } from './url.ts';\n\n/**\n * How much a host rule is trusted, according to the configuration it came from, from most to least trusted:\n *\n * - `admin`: the self-hosted administrator's own configuration - their global config, or a `repositories[]` entry\n * - `inherit`: organization-inherited config, and only where the administrator has opted into trusting it through `inheritConfigTrusted`\n * - `untrusted`: repository configuration, and the presets it extends\n */\ntype HostRuleTrustTier = 'admin' | 'inherit' | 'untrusted';\n\n/**\n * A host rule as registered through {@link add}.\n *\n * `trustTier` is deliberately not a field of `HostRule`: it is set from {@link AddHostRuleOptions} at registration time and must never be settable through configuration.\n */\ninterface RegisteredHostRule extends HostRule {\n /** which configuration the rule came from - see {@link HostRuleTrustTier}. Always set by `add()`, but optional so that `find()` can delete it from its result */\n trustTier?: HostRuleTrustTier;\n\n /** never set by us, and declared only so that a `trusted` smuggled in through configuration can be deleted before the rule is registered. It was the boolean `trustTier` replaced, so a rule carrying one may well be an attempt at the administrator's precedence */\n trusted?: never;\n}\n\nlet hostRules: RegisteredHostRule[] = [];\n\n/**\n * Fields within `HostRule`s that must have their value registered for sanitising through `sanitize.addSecretForSanitizing()`.\n *\n * Kept in sync with `redactedFields` through tests.\n */\nexport const confidentialFields: (keyof HostRule)[] = [\n 'password',\n 'token',\n 'httpsPrivateKey',\n /* not actually sensitive, but redacted nonetheless */\n 'httpsCertificate',\n /* not actually sensitive, but redacted nonetheless */\n 'httpsCertificateAuthority',\n];\n\nexport interface LegacyHostRule {\n hostName?: string;\n domainName?: string;\n baseUrl?: string;\n host?: string;\n endpoint?: string;\n}\n\nexport function migrateRule(rule: LegacyHostRule & HostRule): HostRule {\n const cloned: LegacyHostRule & HostRule = clone(rule);\n delete cloned.hostName;\n delete cloned.domainName;\n delete cloned.baseUrl;\n const result: HostRule = cloned;\n\n const { matchHost } = result;\n const { hostName, domainName, baseUrl } = rule;\n const hostValues = [matchHost, hostName, domainName, baseUrl].filter(\n isTruthy,\n );\n if (hostValues.length === 1) {\n const [matchHost] = hostValues;\n result.matchHost = matchHost;\n } else if (hostValues.length > 1) {\n throw new Error(\n `hostRules cannot contain more than one host-matching field - use \"matchHost\" only.`,\n );\n }\n\n return result;\n}\n\n/**\n * Enforce the `allowedHeaders` allowlist on a set of host rules.\n *\n * Loudly remove anything that's not permitted, logging a WARN.\n *\n * `add()` applies this to every rule it registers, so callers only need it themselves to pre-filter - e.g. to avoid repeating the WARN when the same rules are registered again and again.\n *\n * @param [allowedHeaders] the effective allowlist. Defaults to `GlobalConfig`, but must be passed explicitly when filtering before `GlobalConfig` reflects the repository being processed, i.e. for a `repositories[]` entry's own `allowedHeaders` override\n * @param [warnOnDenied=true] whether to log the WARN. Pass `false` where the very same rules are filtered again, so that it is logged once rather than repeated\n *\n * Headers that survive this allowlist are still subject to how {@link find} combines them: an admin's headers for a host are applied over those of any repository or preset rule matching the same request, so a repository can neither drop nor substitute them.\n */\nexport function filterAllowedHeaders(\n rules: HostRule[],\n allowedHeaders?: string[],\n warnOnDenied = true,\n): HostRule[] {\n // `??`, rather than a parameter default: a default only applies to `undefined`, and a `null` can reach us from user config\n const allowlist = allowedHeaders ?? GlobalConfig.get('allowedHeaders');\n const denied: string[] = [];\n\n const result = rules.map((rule) => {\n if (!rule.headers) {\n return rule;\n }\n\n const allowed: Record<string, string> = {};\n const ruleDenied: string[] = [];\n for (const [name, value] of Object.entries(rule.headers)) {\n if (matchRegexOrGlobList(name, allowlist)) {\n allowed[name] = value;\n } else {\n ruleDenied.push(name);\n }\n }\n\n if (!ruleDenied.length) {\n return rule;\n }\n denied.push(...ruleDenied);\n\n const filtered: HostRule = { ...rule, headers: allowed };\n if (!Object.keys(allowed).length) {\n // drop the key rather than leave an empty object behind: `find()` treats any rule that sets `headers` as replacing the headers of the broader rules of its own tier, so a rule left with none would suppress them\n delete filtered.headers;\n }\n return filtered;\n });\n\n if (denied.length && warnOnDenied) {\n logger.warn(\n { denied },\n \"Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`\",\n );\n }\n return result;\n}\n\nexport interface AddHostRuleOptions {\n /** the effective allowlist. Defaults to `GlobalConfig`; pass it explicitly when `GlobalConfig` does not yet reflect the repository the rule is registered for */\n allowedHeaders?: string[];\n\n /**\n * Whether this rule comes from the self-hosted administrator's own configuration, rather than from repository or preset config.\n *\n * Registers the rule into the `admin` tier - see {@link HostRuleTrustTier}.\n */\n trusted?: boolean;\n\n /**\n * Whether this rule comes from organization-inherited config which the administrator has opted into trusting, through `inheritConfigTrusted`.\n *\n * Registers the rule into the `inherit` tier - see {@link HostRuleTrustTier}. Ignored when `trusted` is also set.\n */\n inherited?: boolean;\n}\n\n/**\n * The trust tier a caller asked for.\n *\n * Deliberately opt-in: a caller that says nothing registers into the untrusted tier, so a new call site cannot grant itself the administrator's precedence by omission.\n */\nfunction trustTierFor(options?: AddHostRuleOptions): HostRuleTrustTier {\n if (options?.trusted) {\n return 'admin';\n }\n if (options?.inherited) {\n return 'inherit';\n }\n return 'untrusted';\n}\n\nexport function add(params: HostRule, options?: AddHostRuleOptions): void {\n let rule: RegisteredHostRule = {\n ...migrateRule(params),\n // set only from `options`, and assigned unconditionally so that it cannot be carried over from `params`: `HostRule` has no `trustTier` field, but configuration is parsed from JSON, so a repository could otherwise smuggle one in and have its rules treated as the administrator's\n trustTier: trustTierFor(options),\n };\n // as above, for the boolean this tier replaced\n delete rule.trusted;\n\n // like the trust tier, `allowInternal` may only come from configuration the administrator trusts: a repository or preset rule must not be able to grant itself access to internal hosts\n if (!isUndefined(rule.allowInternal) && rule.trustTier === 'untrusted') {\n logger.debug(\n `Ignoring hostRules allowInternal for ${rule.matchHost ?? rule.hostType} from untrusted config`,\n );\n delete rule.allowInternal;\n }\n\n if (rule.headers) {\n // enforced here, at the single registration chokepoint, so that no current or future caller can register a rule whose headers bypass `allowedHeaders`; `applyHostRule` filters by header name again at request time as defence in depth\n [rule] = filterAllowedHeaders([rule], options?.allowedHeaders);\n }\n\n if (rule.matchHost) {\n rule.matchHost = massageHostUrl(rule.matchHost);\n const parsedUrl = parseUrl(rule.matchHost);\n rule.resolvedHost = parsedUrl?.hostname ?? rule.matchHost;\n confidentialFields.forEach((field) => {\n if (rule[field]) {\n logger.debug(\n // TODO: types (#22198)\n `Adding ${field} authentication for ${rule.matchHost!} (hostType=${\n rule.hostType\n }) to hostRules`,\n );\n }\n });\n }\n confidentialFields.forEach((field) => {\n const secret = rule[field];\n if (isString(secret) && secret.length > 3) {\n sanitize.addSecretForSanitizing(secret);\n }\n });\n if (rule.username && rule.password) {\n sanitize.addSecretForSanitizing(\n toBase64(`${rule.username}:${rule.password}`),\n );\n }\n hostRules.push(rule);\n}\n\nexport interface HostRuleSearch {\n hostType?: string;\n url?: string;\n readOnly?: boolean;\n}\n\nexport function matchesHost(url: string, matchHost: string): boolean {\n const parsedUrl = parseUrl(url);\n if (!parsedUrl) {\n return false;\n }\n\n const parsedMatchHost = parseUrl(matchHost);\n if (isHttpUrl(parsedUrl) && isHttpUrl(parsedMatchHost)) {\n return parsedUrl.href.startsWith(parsedMatchHost!.href);\n }\n\n const { hostname } = parsedUrl;\n if (!hostname) {\n return false;\n }\n\n if (hostname === matchHost) {\n return true;\n }\n\n const topLevelSuffix = matchHost.startsWith('.')\n ? matchHost\n : `.${matchHost}`;\n return hostname.endsWith(topLevelSuffix);\n}\n\nfunction fromShorterToLongerMatchHost(a: HostRule, b: HostRule): number {\n if (!a.matchHost || !b.matchHost) {\n return 0;\n }\n return a.matchHost.length - b.matchHost.length;\n}\n\nfunction hostRuleRank({ hostType, matchHost, readOnly }: HostRule): number {\n if ((hostType || readOnly) && matchHost) {\n return 3;\n }\n\n if (matchHost) {\n return 2;\n }\n\n if (hostType) {\n return 1;\n }\n\n return 0;\n}\n\nfunction fromLowerToHigherRank(a: HostRule, b: HostRule): number {\n return hostRuleRank(a) - hostRuleRank(b);\n}\n\n// A single comparator, rather than two consecutive sorts: `fromShorterToLongerMatchHost` returns 0 for rules without a `matchHost`, which makes it non-transitive on its own, so a standalone length sort could leave two `matchHost` rules unordered when a host-less rule sits between them\nfunction fromLowerRankAndShorterMatchHost(a: HostRule, b: HostRule): number {\n return fromLowerToHigherRank(a, b) || fromShorterToLongerMatchHost(a, b);\n}\n\n/**\n * The `headers` that apply from a set of matching rules of the same trust tier.\n *\n * The last rule to set any wins outright, as `find()`'s callers receive them sorted from least to most specific. That is the behaviour every matching rule had before `headers` were combined across tiers, and it is what lets a broad rule's headers be masked by a narrower rule from the same source.\n */\nfunction headersOfLastRuleToSetThem(\n rules: RegisteredHostRule[],\n): Record<string, string> | undefined {\n return rules\n .map((rule) => rule.headers)\n .filter(isTruthy)\n .pop();\n}\n\n/**\n * The last defined value, following the same \"most specific rule wins\" ordering as {@link headersOfLastRuleToSetThem}.\n */\nfunction lastDefined<T>(values: (T | undefined)[]): T | undefined {\n return values.filter((value) => !isUndefined(value)).pop();\n}\n\n/**\n * The rules whose grant is deliberately scoped, by a `hostType` or by a URL-prefix `matchHost` - see {@link InternalHostGrant}.\n */\nfunction scopedRules(rules: RegisteredHostRule[]): RegisteredHostRule[] {\n return rules.filter(\n (rule) => isNonEmptyString(rule.hostType) || isHttpUrl(rule.matchHost),\n );\n}\n\nexport function find(search: HostRuleSearch): CombinedHostRule {\n if ([search.hostType, search.url].every(isFalsy)) {\n logger.warn({ search }, 'Invalid hostRules search');\n return {};\n }\n\n // Sort primarily by rank, and secondarily by matchHost length\n const sortedRules = hostRules.sort(fromLowerRankAndShorterMatchHost);\n\n const matchedRules: RegisteredHostRule[] = [];\n for (const rule of sortedRules) {\n let hostTypeMatch = true;\n let hostMatch = true;\n let readOnlyMatch = true;\n\n if (rule.hostType) {\n hostTypeMatch = false;\n // v8 ignore else -- TODO: add test #40625\n if (search.hostType === rule.hostType) {\n hostTypeMatch = true;\n }\n }\n\n if (rule.matchHost && rule.resolvedHost) {\n hostMatch = false;\n if (search.url) {\n hostMatch = matchesHost(search.url, rule.matchHost);\n }\n }\n\n if (!isUndefined(rule.readOnly)) {\n readOnlyMatch = false;\n // v8 ignore else -- TODO: add test #40625\n if (search.readOnly === rule.readOnly) {\n readOnlyMatch = true;\n hostTypeMatch = true; // When we match `readOnly`, we don't care about `hostType`\n }\n }\n\n if (hostTypeMatch && readOnlyMatch && hostMatch) {\n matchedRules.push(clone(rule));\n }\n }\n\n const res: RegisteredHostRule & Pick<CombinedHostRule, 'internalHostGrant'> =\n Object.assign({}, ...matchedRules);\n\n // the sensitive fields below are resolved a trust tier at a time, so that the outcome cannot depend on how specific a rule from a less trusted tier makes itself\n const trustedRules = matchedRules.filter(\n (rule) => rule.trustTier === 'admin',\n );\n const inheritedRules = matchedRules.filter(\n (rule) => rule.trustTier === 'inherit',\n );\n const untrustedRules = matchedRules.filter(\n (rule) => rule.trustTier === 'untrusted',\n );\n\n // `headers` are resolved per trust tier and then combined key by key, so that repository or preset config can no longer discard - or substitute - the headers a self-hosted admin configured for the same host\n // Within a tier nothing changes: the most specific rule's `headers` still replace those of the broader rules it is combined with, so an admin masking their own broad rule with a narrower one keeps working, as does a repository doing the same among its own rules\n // This is deliberately scoped to `headers` alone, as combining any more of the fields than we already do has caused authentication regressions before: an inherited `token` and a specific rule's `username`/`password` are not meant to be used together\n const untrustedHeaders = headersOfLastRuleToSetThem(untrustedRules);\n const inheritedHeaders = headersOfLastRuleToSetThem(inheritedRules);\n const trustedHeaders = headersOfLastRuleToSetThem(trustedRules);\n if (untrustedHeaders ?? inheritedHeaders ?? trustedHeaders) {\n // the admin's own headers are applied last, so neither a repository nor inherited config can override one they set for this host\n res.headers = {\n ...untrustedHeaders,\n ...inheritedHeaders,\n ...trustedHeaders,\n };\n }\n\n // `enabled` is resolved per trust tier like `headers`: a repository or preset rule must not be able to re-enable a host the administrator's own rules disabled (or vice versa) by out-specifying them with a longer `matchHost`\n const enabled =\n lastDefined(trustedRules.map((rule) => rule.enabled)) ??\n lastDefined(inheritedRules.map((rule) => rule.enabled)) ??\n lastDefined(untrustedRules.map((rule) => rule.enabled));\n if (!isUndefined(enabled)) {\n res.enabled = enabled;\n }\n\n // computed here and never from configuration: `add()` strips `allowInternal` from untrusted registrations, so only rules from configuration the administrator trusts can contribute\n // the admin's own rules are resolved first, and an `allowInternal: false` of theirs is an absolute veto: inherited config cannot talk an organization's way back in through any grant shape, whether scoped, unscoped, or merely implicit in naming the host\n // each shape must therefore be resolved from the admin tier's verdict rather than falling through to the inherit tier on its own - a bare admin rule sets no scoped verdict, and an inherited scoped grant would otherwise fill the gap for exactly the requests which consult it, such as fetching config over HTTP\n const trustedExplicit = lastDefined(\n trustedRules.map((rule) => rule.allowInternal),\n );\n const trustedScoped = lastDefined(\n scopedRules(trustedRules).map((rule) => rule.allowInternal),\n );\n // derived from the resolved values, so an admin masking a broad `allowInternal: false` of their own with a narrower `true` is not a veto\n const adminVeto = trustedExplicit === false || trustedScoped === false;\n // a vetoed request has no inherit tier left to fall through to, and is clamped to `false` rather than to undefined so that a consumer's `explicit ?? implicit` still short-circuits\n const inheritedExplicit = adminVeto\n ? false\n : lastDefined(inheritedRules.map((rule) => rule.allowInternal));\n const inheritedScoped = adminVeto\n ? false\n : lastDefined(\n scopedRules(inheritedRules).map((rule) => rule.allowInternal),\n );\n const internalHostGrant: InternalHostGrant = {\n explicit: trustedExplicit ?? inheritedExplicit,\n scoped: trustedScoped ?? inheritedScoped,\n implicit:\n !adminVeto &&\n [...trustedRules, ...inheritedRules].some((rule) =>\n isNonEmptyString(rule.matchHost),\n ),\n };\n if (\n !isUndefined(internalHostGrant.explicit) ||\n !isUndefined(internalHostGrant.scoped) ||\n internalHostGrant.implicit\n ) {\n res.internalHostGrant = internalHostGrant;\n }\n\n delete res.hostType;\n delete res.resolvedHost;\n delete res.matchHost;\n delete res.readOnly;\n delete res.trustTier;\n delete res.trusted;\n delete res.allowInternal;\n return res;\n}\n\nexport function hosts({ hostType }: { hostType: string }): string[] {\n return hostRules\n .filter((rule) => rule.hostType === hostType)\n .map((rule) => rule.resolvedHost)\n .filter(isTruthy);\n}\n\nexport function hostType({ url }: { url: string }): string | null {\n return (\n hostRules\n .filter((rule) => rule.matchHost && matchesHost(url, rule.matchHost))\n .sort(fromShorterToLongerMatchHost)\n .map((rule) => rule.hostType)\n .filter(isTruthy)\n .pop() ?? null\n );\n}\n\nexport function findAll({ hostType }: { hostType: string }): HostRule[] {\n return hostRules.filter((rule) => rule.hostType === hostType);\n}\n\n/**\n * @returns a deep copy of all known host rules without any filtering\n */\nexport function getAll(): HostRule[] {\n return clone(hostRules);\n}\n\nexport function clear(): void {\n logger.debug('Clearing hostRules');\n hostRules = [];\n sanitize.clearRepoSanitizedSecretsList();\n}\n"],"mappings":"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"}
@@ -0,0 +1,279 @@
1
+ import { HOST_BLOCKED } from "../../constants/error-messages.js";
2
+ import { coerceObject } from "../object.js";
3
+ import { regEx } from "../regex.js";
4
+ import { GlobalConfig } from "../../config/global.js";
5
+ import { logger } from "../../logger/index.js";
6
+ import { parseUrl } from "../url.js";
7
+ import { find } from "../host-rules.js";
8
+ import { hasProxy } from "../../proxy.js";
9
+ import { isString } from "@sindresorhus/is";
10
+ import dns from "node:dns";
11
+ import dnsPromises from "node:dns/promises";
12
+ import net from "node:net";
13
+ //#region lib/util/http/host-guard.ts
14
+ /**
15
+ * Instance-metadata hostnames, blocked regardless of what they resolve to.
16
+ *
17
+ * Only names which serve nothing but metadata belong here: a name in this set can never be permitted, whatever the configuration says. Providers whose metadata service is only reachable by IP (AWS, Azure, ...) are covered by {@link metadataBlockList} instead.
18
+ */
19
+ const metadataHostnames = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.packet.net"]);
20
+ const metadataBlockList = new net.BlockList();
21
+ metadataBlockList.addAddress("169.254.169.254", "ipv4");
22
+ metadataBlockList.addAddress("fd00:ec2::254", "ipv6");
23
+ metadataBlockList.addAddress("169.254.170.2", "ipv4");
24
+ metadataBlockList.addAddress("169.254.170.23", "ipv4");
25
+ metadataBlockList.addAddress("100.100.100.200", "ipv4");
26
+ const internalBlockList = new net.BlockList();
27
+ internalBlockList.addSubnet("0.0.0.0", 8, "ipv4");
28
+ internalBlockList.addSubnet("10.0.0.0", 8, "ipv4");
29
+ internalBlockList.addSubnet("172.16.0.0", 12, "ipv4");
30
+ internalBlockList.addSubnet("192.168.0.0", 16, "ipv4");
31
+ internalBlockList.addSubnet("100.64.0.0", 10, "ipv4");
32
+ internalBlockList.addSubnet("127.0.0.0", 8, "ipv4");
33
+ internalBlockList.addSubnet("169.254.0.0", 16, "ipv4");
34
+ internalBlockList.addSubnet("224.0.0.0", 4, "ipv4");
35
+ internalBlockList.addAddress("::", "ipv6");
36
+ internalBlockList.addAddress("::1", "ipv6");
37
+ internalBlockList.addSubnet("fc00::", 7, "ipv6");
38
+ internalBlockList.addSubnet("fe80::", 10, "ipv6");
39
+ /**
40
+ * Classifies an IP address, returning the category it is blocked under, or `null` for a public address.
41
+ *
42
+ * A string that is not a valid IP address is classified as `internal` - we should fail closed instead of possibly connecting to something we shouldn't be.
43
+ */
44
+ function classifyIpAddress(address) {
45
+ const version = net.isIP(address);
46
+ if (version === 0) return "internal";
47
+ const family = version === 4 ? "ipv4" : "ipv6";
48
+ if (metadataBlockList.check(address, family)) return "metadata";
49
+ if (internalBlockList.check(address, family)) return "internal";
50
+ return null;
51
+ }
52
+ /**
53
+ * Classifies a hostname against the metadata-endpoint hostname list.
54
+ *
55
+ * Only metadata hostnames can be classified without DNS resolution - any other hostname needs its resolved addresses checked via {@link classifyIpAddress}.
56
+ */
57
+ function classifyHostname(hostname) {
58
+ const normalized = hostname.toLowerCase().replace(regEx(/\.$/), "");
59
+ if (metadataHostnames.has(normalized)) return "metadata";
60
+ return null;
61
+ }
62
+ /** Strips the brackets `URL` keeps around IPv6 hosts, leaving other hosts as they are. */
63
+ function unbracketHostname(hostname) {
64
+ if (hostname.startsWith("[") && hostname.endsWith("]")) return hostname.slice(1, -1);
65
+ return hostname;
66
+ }
67
+ /**
68
+ * Classifies a URL from its host alone: IP-literal hosts are classified by address, and metadata hostnames by name.
69
+ *
70
+ * Returns `null` when nothing can be decided at the URL level - i.e. the host is a regular hostname, whose resolved addresses must be checked via {@link classifyIpAddress} at DNS lookup time.
71
+ */
72
+ function classifyUrl(url) {
73
+ const hostname = unbracketHostname(url.hostname);
74
+ if (net.isIP(hostname) !== 0) return classifyIpAddress(hostname);
75
+ return classifyHostname(hostname);
76
+ }
77
+ /**
78
+ * Returns the resolved address which decides the outcome for a hostname, or `null` when every one of them is a public address.
79
+ *
80
+ * Each resolved record is a potential connection target, so each must pass. A metadata address decides the outcome over an internal one, as it is blocked in every mode.
81
+ */
82
+ function findBlockedAddress(addresses) {
83
+ let internal = null;
84
+ for (const address of addresses) {
85
+ const category = classifyIpAddress(address);
86
+ if (category === "metadata") return {
87
+ address,
88
+ category
89
+ };
90
+ if (category === "internal") internal ??= {
91
+ address,
92
+ category
93
+ };
94
+ }
95
+ return internal;
96
+ }
97
+ /**
98
+ * Applies `decision` to the addresses a hostname resolved to, logging the outcome, and returning `true` when the request must not be made.
99
+ */
100
+ function isResolutionBlocked(hostname, addresses, decision, hostType, responseBecomesConfig) {
101
+ const blocked = findBlockedAddress(addresses);
102
+ if (!blocked) return false;
103
+ if (blocked.category === "internal") {
104
+ if (decision === "granted") return false;
105
+ if (decision === "warn-dns") {
106
+ warnInternalHost(hostname, hostType, responseBecomesConfig);
107
+ return false;
108
+ }
109
+ }
110
+ logger.warn({
111
+ hostname,
112
+ address: blocked.address
113
+ }, "Blocked HTTP request: hostname resolves to a blocked address");
114
+ return true;
115
+ }
116
+ /**
117
+ * Validates every address a hostname resolves to, on every connection, which also catches a host whose answer changes between checks (DNS rebinding).
118
+ *
119
+ * The guard is built per request, as the policy it enforces is read from the configuration when the request is made.
120
+ */
121
+ function makeGuardedLookup(decision, hostType, responseBecomesConfig) {
122
+ return function guardedLookup(hostname, options, callback) {
123
+ dns.lookup(hostname, options, (err, address, family) => {
124
+ if (err) {
125
+ callback(err, address, family);
126
+ return;
127
+ }
128
+ if (isResolutionBlocked(hostname, isString(address) ? [address] : address.map((entry) => entry.address), decision, hostType, responseBecomesConfig)) {
129
+ callback(new Error(HOST_BLOCKED), address, family);
130
+ return;
131
+ }
132
+ callback(err, address, family);
133
+ });
134
+ };
135
+ }
136
+ function isPlatformEndpoint(url) {
137
+ const endpoint = GlobalConfig.get("endpoint");
138
+ if (!endpoint) return false;
139
+ return parseUrl(endpoint)?.origin === url.origin;
140
+ }
141
+ /**
142
+ * Whether the administrator has permitted internal access for this request, leaving aside the `internalHostAccess` mode: a permitted request neither blocks nor warns, whatever the mode is.
143
+ */
144
+ function isInternalGranted(url, grant, responseBecomesConfig) {
145
+ if (isPlatformEndpoint(url)) return true;
146
+ if (responseBecomesConfig) return grant?.scoped === true;
147
+ return grant?.explicit ?? grant?.implicit ?? false;
148
+ }
149
+ /**
150
+ * Warns about a request which `internalHostAccess=block` would refuse, once per host, naming the grant which would permit it.
151
+ */
152
+ function warnInternalHost(hostname, hostType, responseBecomesConfig) {
153
+ if (responseBecomesConfig) {
154
+ logger.once.warn({
155
+ hostname,
156
+ hostType
157
+ }, "HTTP request to an internal host whose response becomes configuration, which `internalHostAccess=block` would refuse - permit it with a `hostRules` entry setting `allowInternal: true`, scoped with a `hostType` or a URL-prefix `matchHost`, or set `internalHostAccess=block` to enforce this now. The default will become `block` in a future major release.");
158
+ return;
159
+ }
160
+ logger.once.warn({
161
+ hostname,
162
+ hostType
163
+ }, "HTTP request to an internal host, which `internalHostAccess=block` would refuse - permit it with a `hostRules` entry naming the host, or one setting `allowInternal: true`, or set `internalHostAccess=block` to enforce this now. The default will become `block` in a future major release.");
164
+ }
165
+ /**
166
+ * Enforce the internal-host policy on a request URL, throwing `HOST_BLOCKED` when the request must not be made.
167
+ *
168
+ * Metadata endpoints are blocked no matter what the configuration says: Renovate's HTTP layer never has a legitimate reason to request them.
169
+ */
170
+ function checkUrl(url, hostType, grant, responseBecomesConfig) {
171
+ const category = classifyUrl(url);
172
+ if (category === "metadata") {
173
+ logger.warn({
174
+ url: url.href,
175
+ hostType
176
+ }, "Blocked HTTP request to a cloud instance-metadata endpoint");
177
+ throw new Error(HOST_BLOCKED);
178
+ }
179
+ const mode = GlobalConfig.get("internalHostAccess");
180
+ if (mode === "allow" || isInternalGranted(url, grant, responseBecomesConfig)) {
181
+ if (category === "internal") logger.once.info(`Internal host ${url.hostname} permitted by configuration`);
182
+ return "granted";
183
+ }
184
+ if (mode === "warn") {
185
+ if (category === "internal") warnInternalHost(url.hostname, hostType, responseBecomesConfig);
186
+ return "warn-dns";
187
+ }
188
+ if (category === "internal") {
189
+ logger.warn({
190
+ url: url.href,
191
+ hostType
192
+ }, "Blocked HTTP request to an internal host - a self-hosted administrator can permit it via `hostRules`, or with `internalHostAccess=allow`");
193
+ throw new Error(HOST_BLOCKED);
194
+ }
195
+ return "check-dns";
196
+ }
197
+ /**
198
+ * Validates what a request's hostname resolves to, before the request is made, for deployments where `dnsLookup` cannot do it.
199
+ *
200
+ * A proxy agent connects to the proxy and hands it the target hostname, so the target is resolved by the proxy and Renovate's `dnsLookup` guard never runs. Resolving here instead is best-effort only: the proxy re-resolves the hostname itself, so a DNS-rebinding or split-horizon answer can still differ from what we saw. Pair it with egress controls on the proxy.
201
+ */
202
+ async function preflightHostname(url, decision, hostType, responseBecomesConfig) {
203
+ const hostname = unbracketHostname(url.hostname);
204
+ if (net.isIP(hostname) !== 0) return;
205
+ let addresses;
206
+ try {
207
+ addresses = await dnsPromises.lookup(hostname, { all: true });
208
+ } catch {
209
+ logger.once.debug(`Host guard could not resolve ${hostname} - relying on the proxy for this request`);
210
+ return;
211
+ }
212
+ if (isResolutionBlocked(hostname, addresses.map((entry) => entry.address), decision, hostType, responseBecomesConfig)) throw new Error(HOST_BLOCKED);
213
+ }
214
+ function makeBeforeRequest(hostType, responseBecomesConfig) {
215
+ return async function hostGuardBeforeRequest(options) {
216
+ const url = options.url instanceof URL ? options.url : null;
217
+ if (!url) throw new Error(HOST_BLOCKED);
218
+ const grant = find({
219
+ hostType,
220
+ url: url.toString()
221
+ }).internalHostGrant;
222
+ await preflightHostname(url, checkUrl(url, hostType, grant, responseBecomesConfig), hostType, responseBecomesConfig);
223
+ };
224
+ }
225
+ /**
226
+ * Removes the credentials belonging to the host a request is being redirected away from, so that a redirect cannot hand them to another origin.
227
+ *
228
+ * got strips the credentials it knows about itself - `authorization`, `cookie` and any URL userinfo - before this hook runs, and treats a scheme downgrade as a different origin, so an `https:` to `http:` redirect is covered. What it does not know about survives: the `Private-token` header GitLab personal access tokens are sent in (see `applyAuthorization`), and any credential an administrator configured through a `hostRules` `headers` entry.
229
+ *
230
+ * Those headers are dropped rather than replaced with the target host's own: `hostRules` headers are resolved once, when the request is built, so there is nothing to re-apply here. Sending none is the safe outcome.
231
+ */
232
+ function stripCrossOriginCredentials(options, fromUrl, toUrl, hostType) {
233
+ if (fromUrl?.origin === toUrl.origin) return;
234
+ if (fromUrl) {
235
+ const configured = find({
236
+ hostType,
237
+ url: fromUrl.toString()
238
+ }).headers;
239
+ for (const name of Object.keys(coerceObject(configured))) delete options.headers[name.toLowerCase()];
240
+ }
241
+ delete options.headers.authorization;
242
+ delete options.headers.cookie;
243
+ delete options.headers["private-token"];
244
+ }
245
+ function makeBeforeRedirect(hostType, responseBecomesConfig) {
246
+ return function hostGuardBeforeRedirect(options, response) {
247
+ const url = options.url instanceof URL ? options.url : null;
248
+ if (!url) throw new Error(HOST_BLOCKED);
249
+ stripCrossOriginCredentials(options, isString(response.url) ? parseUrl(response.url) : null, url, hostType);
250
+ const grant = find({
251
+ hostType,
252
+ url: url.toString()
253
+ }).internalHostGrant;
254
+ options.dnsLookup = makeGuardedLookup(checkUrl(url, hostType, grant, responseBecomesConfig), hostType, responseBecomesConfig);
255
+ };
256
+ }
257
+ /**
258
+ * Enforce the internal-host policy for a request to `url`, throwing `HOST_BLOCKED` when the request must not be made at all.
259
+ *
260
+ * The returned got options carry the policy through the rest of the request's lifetime: `dnsLookup` validates what hostnames resolve to (on every connection, which also covers DNS rebinding), and `beforeRedirect` re-runs this check for every redirect target.
261
+ *
262
+ * When a proxy is configured, the proxy agent resolves the target hostname itself and `dnsLookup` is never called, so a `beforeRequest` hook resolves and validates the hostname up front instead - see {@link preflightHostname} for what that can and cannot catch.
263
+ *
264
+ * `responseBecomesConfig` marks a request whose response is interpreted as Renovate configuration, which an internal host may only serve under a deliberately-scoped `allowInternal` grant.
265
+ *
266
+ * Under the `internalHostAccess=warn` default, an internal host which no grant permits is logged rather than blocked, so that administrators can add the grants they need before the default becomes `block`.
267
+ */
268
+ function applyHostGuard(url, hostType, grant, responseBecomesConfig) {
269
+ const guard = {
270
+ dnsLookup: makeGuardedLookup(checkUrl(url, hostType, grant, responseBecomesConfig), hostType, responseBecomesConfig),
271
+ beforeRedirect: makeBeforeRedirect(hostType, responseBecomesConfig)
272
+ };
273
+ if (hasProxy()) guard.beforeRequest = makeBeforeRequest(hostType, responseBecomesConfig);
274
+ return guard;
275
+ }
276
+ //#endregion
277
+ export { applyHostGuard, checkUrl, classifyHostname, classifyIpAddress, classifyUrl };
278
+
279
+ //# sourceMappingURL=host-guard.js.map