From c22a6908ba4c91185d9b7949ef1933476fb47837 Mon Sep 17 00:00:00 2001 From: Miccy Date: Sun, 23 Aug 2026 03:15:08 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=94=92=20feat:=20test=20what=20the=20?= =?UTF-8?q?presets=20decide,=20not=20just=20how=20they=20are=20written?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The validator checks shape. Every fault this preset has shipped passed it: - `^jest` unanchored matched every jest-* package on npm, a prefix nobody owns, and those automerged with the trust meant for Jest - per-tool rules sat after the anchored trust list and re-granted automerge through looser patterns - the GitHub Actions rule had no matchUpdateTypes, so it overrode "major: automerge false" and merged bare digest moves - matchManagers named pnpm and yarn, which are not managers, so a rule that read as covering three ecosystems covered one tooling/test_policy.py freezes those as 22 cases over the automerge decision, fails if lockFileMaintenance gets automerge back, and rejects manager names Renovate does not have. Each guard was verified by reintroducing the fault it covers and watching it fail. It reimplements Renovate's matching rather than calling Renovate, so it can drift. That limitation is written at the top of the file and in SECURITY.md rather than left for someone to discover. SECURITY.md was missing on a repo about supply-chain security. It says where to report, what this preset does not do -- it cannot help a repo that already installed a hostile version -- and the limitations a reader would otherwise find the hard way. MIT badge is green; yellow read as a warning. --- README-cs.md | 17 +++++ README.md | 19 ++++- SECURITY.md | 45 +++++++++++ tooling/test_policy.py | 165 +++++++++++++++++++++++++++++++++++++++++ tooling/validate.sh | 4 + 5 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 SECURITY.md create mode 100755 tooling/test_policy.py diff --git a/README-cs.md b/README-cs.md index ece7d55..2499ae9 100644 --- a/README-cs.md +++ b/README-cs.md @@ -157,6 +157,23 @@ Pozor na jedno omezení: **`renovate-config-validator` nevaliduje názvy managerů.** `matchManagers: ["npm", "pnpm", "yarn"]` projde čistě a nematchuje nic. Gate pokrývá schéma, ne význam. +### Co gate kontroluje + +`validate.sh` pouští dvě věci a ta druhá je důležitější: + +- `renovate-config-validator --strict` — presety jsou správně zapsané +- `tooling/test_policy.py` — presety **rozhodují** to, co mají + +Validátor kontroluje tvar, ne význam. Každá chyba, kterou tenhle preset vydal, +jím prošla: neukotvený vzor dávající důvěru namespace, který nikdo nevlastní, +pravidlo zařazené tak, že rušilo to nad sebou, dva neexistující názvy managerů. +Policy test je má zmrazené jako případy a spadne, kdyby obnova lockfilu zase +dostala automerge. + +Renovate matching reimplementuje, místo aby volal Renovate, takže se od +skutečného enginu může rozejít. Selhání je důvod se podívat; průchod je slabší +důkaz než dry run. + ## Pod vlastním účtem Repozitář si zkopíruj, ale **neforkuj ho pro každou organizaci zvlášť.** diff --git a/README.md b/README.md index 3132e0f..b3c1c40 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) [![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com/) ![Ecosystems](https://img.shields.io/badge/ecosystems-10+-blue.svg) @@ -246,6 +246,23 @@ ln -s ../../tooling/validate.sh .git/hooks/pre-push A local hook is a convenience, not a boundary — anyone can skip it. CI stays the real gate. +### What the gate checks + +`validate.sh` runs two things, and the second matters more: + +- `renovate-config-validator --strict` — the presets are well formed +- `tooling/test_policy.py` — the presets *decide* what they should + +The validator checks shape, not meaning. Every fault this preset has shipped +passed it: an unanchored pattern granting trust to a namespace nobody owns, a +rule ordered so it undid the one above it, two manager names that do not exist. +The policy test freezes those as cases, and fails if lockfile maintenance ever +gets automerge back. + +It reimplements Renovate's matching rather than calling Renovate, so it can +drift from the real engine. A failure is a reason to look; a pass is weaker +evidence than a dry run. + ## Testing locally You can test this config locally before deploying: diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c3f4faa --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,45 @@ +# Security policy + +## Reporting + +Report a problem with this preset through +[private vulnerability reporting](https://github.com/ownctrl/supply-chain/security/advisories/new), +not a public issue. + +Worth reporting: + +- a rule that lets something merge unattended when it should not +- a matcher that is broader than it reads — an unanchored pattern over a + package prefix nobody owns is the fault this preset has shipped most often +- a package on the watch list that should not be, or one missing that should +- remediation text pointing somewhere a reader cannot follow + +## What this preset is and is not + +It decides **which dependency updates may land without a human looking at +them.** That is all. It does not scan code, detect compromise, or verify that a +published package matches its source. + +It cannot protect a repository that has already installed a hostile version. It +gates *updates*, so a compromised package already in your lockfile is outside +its reach — check the lockfile directly and rotate anything the package could +have read. + +## Known limitations + +- `renovate-config-validator` does not validate manager names. A typo'd manager + silently disables the rule containing it. `tooling/test_policy.py` checks the + names this preset uses against a hardcoded list, which needs updating when + Renovate adds a manager. +- `tooling/test_policy.py` reimplements Renovate's matching rather than calling + Renovate. It can drift from the real engine. A failure is a reason to look; a + pass is weaker evidence than a Renovate dry run. +- `minimumReleaseAge` does not apply to `pin`, `lockFileMaintenance`, + `lockfileUpdate`, `rollback`, `bump` or `replacement`. This preset disables + automerge for lockfile maintenance because of it. +- Renovate has a hosted app on GitHub.com only. On every other forge you run it + yourself, and its token can write to all of your repositories. + +## Supported versions + +The latest tag. Older tags do not get fixes — move the pin. diff --git a/tooling/test_policy.py b/tooling/test_policy.py new file mode 100755 index 0000000..6ac976f --- /dev/null +++ b/tooling/test_policy.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Assert what this preset lets merge unattended. + +`renovate-config-validator` checks that default.json is well formed. It does +not check what the rules *decide* -- it will happily accept a rule that grants +automerge to every package on npm, or one whose matcher never fires. Every +fault this preset has shipped was of that kind: + + * `^jest` unanchored matched every `jest-*` package on npm, a prefix nobody + owns, and those automerged with the trust meant for Jest. + * Per-tool rules sat after the anchored trust list and re-granted automerge + through looser patterns, undoing the anchoring above them. + * The GitHub Actions rule had no matchUpdateTypes, so it overrode + "major: automerge false" and merged bare digest moves unattended. + * `matchManagers: ["npm", "pnpm", "yarn"]` named two managers that do not + exist, so a rule that looked like it covered three ecosystems covered one. + +The validator passed all four. The cases below are those bugs, frozen. + +LIMITATION, and it is not a small one: this reimplements Renovate's matching +rather than calling Renovate. It can drift from the real engine. Treat a +failure here as a reason to look, and a pass as weaker evidence than a Renovate +dry run. It exists because the alternative is checking nothing. +""" + +import fnmatch +import json +import re +import sys +from pathlib import Path + +CONFIG = Path(__file__).resolve().parent.parent / "default.json" + + +def _matches_name(pattern: str, name: str) -> bool: + """Renovate matchPackageNames: exact, glob, or /regex/.""" + if pattern == "*": + return True + if len(pattern) > 1 and pattern.startswith("/") and pattern.endswith("/"): + return re.search(pattern[1:-1], name) is not None + if any(c in pattern for c in "*?["): + return fnmatch.fnmatch(name, pattern) + return pattern == name + + +def _rule_applies(rule, *, name, dep_type, manager, update_type, datasource): + for key, value in ( + ("matchManagers", manager), + ("matchDepTypes", dep_type), + ("matchUpdateTypes", update_type), + ("matchDatasources", datasource), + ): + if key in rule and value not in rule[key]: + return False + if "matchPackageNames" in rule: + if not any(_matches_name(p, name) for p in rule["matchPackageNames"]): + return False + return True + + +def resolve(rules, key, **ctx): + """Last matching rule wins, which is how Renovate layers packageRules.""" + value, source = None, "unset" + for rule in rules: + if _rule_applies(rule, **ctx) and key in rule: + value, source = rule[key], rule.get("description", "") + return value, source + + +# name, depType, manager, updateType, datasource, automerge expected +CASES = [ + # Trusted dev tooling merges unattended. That is the whole allowance. + ("jest", "devDependencies", "npm", "patch", "npm", True), + ("vitest", "devDependencies", "npm", "patch", "npm", True), + ("oxlint", "devDependencies", "npm", "patch", "npm", True), + ("typescript", "devDependencies", "npm", "patch", "npm", True), + ("@biomejs/biome", "devDependencies", "npm", "patch", "npm", True), + ("@types/node", "devDependencies", "npm", "patch", "npm", True), + # Neighbours in unowned npm prefixes must NOT inherit that trust. + ("jest-environment-foo", "devDependencies", "npm", "patch", "npm", False), + ("vitest-fetch-mock", "devDependencies", "npm", "patch", "npm", False), + ("oxlint-plugin-evil", "devDependencies", "npm", "patch", "npm", False), + ("typescript-eslint-hijack", "devDependencies", "npm", "patch", "npm", False), + # Production dependencies never merge unattended, whatever they are called. + ("typescript", "dependencies", "npm", "patch", "npm", False), + ("vitest", "dependencies", "npm", "patch", "npm", False), + ("@biomejs/biome", "dependencies", "npm", "patch", "npm", False), + ("express", "dependencies", "npm", "patch", "npm", False), + ("serde", "dependencies", "cargo", "patch", "crate", False), + ("django", "dependencies", "poetry", "patch", "pypi", False), + # Watch-listed packages stay gated even as devDependencies. + ("posthog-node", "dependencies", "npm", "patch", "npm", False), + ("tinycolor2", "devDependencies", "npm", "patch", "npm", False), + # CI actions: minor/patch only. A bare digest move on an unchanged tag is + # the tj-actions/changed-files vector and must wait for a human. + ("actions/checkout", None, "github-actions", "patch", "github-tags", True), + ("actions/checkout", None, "github-actions", "minor", "github-tags", True), + ("actions/checkout", None, "github-actions", "digest", "github-tags", False), + ("actions/checkout", None, "github-actions", "major", "github-tags", False), +] + + +def main() -> int: + config = json.loads(CONFIG.read_text()) + rules = config["packageRules"] + failures = [] + + for name, dep_type, manager, update_type, datasource, expected in CASES: + got, source = resolve( + rules, + "automerge", + name=name, + dep_type=dep_type, + manager=manager, + update_type=update_type, + datasource=datasource, + ) + if bool(got) is not expected: + failures.append( + f" {name} ({dep_type or manager}, {update_type}): " + f"expected automerge={expected}, got {bool(got)} " + f"from rule {source!r}" + ) + + # Lockfile refreshes are not covered by minimumReleaseAge, so automerging + # one would pull every transitive dependency in unreviewed and ungated. + if config["lockFileMaintenance"].get("automerge"): + failures.append( + " lockFileMaintenance.automerge is true: minimumReleaseAge does " + "not apply to lockfile refreshes, so this merges every transitive " + "update with no age gate and no review" + ) + + # Renovate has no pnpm or yarn manager; the npm manager reads all three + # lockfiles. A rule naming them looks broader than it is, and the schema + # validator does not catch it. + known = { + "npm", "bun", "bun-version", "deno", "cargo", "gomod", "docker", + "github-actions", "nix", "terraform", "ansible", "nuget", "composer", + "bundler", "pub", "swift", "cocoapods", "kubernetes", "helmv3", + "helm-values", "helm-requirements", "helmfile", "flux", "argocd", + "gradle", "gradle-wrapper", "maven", "maven-wrapper", "sbt", + "pip_requirements", "pip-compile", "poetry", "pep621", "pipenv", + "setup-cfg", + } + for rule in rules: + for manager in rule.get("matchManagers", []): + if manager not in known: + failures.append( + f" unknown manager {manager!r} in rule " + f"{rule.get('description', '')!r} -- it matches nothing. " + f"If Renovate added it, add it to `known` here." + ) + + if failures: + print(f"policy: {len(failures)} failure(s)\n") + print("\n".join(failures)) + return 1 + + print(f"policy: {len(CASES)} cases, lockfile gate and manager names all OK") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tooling/validate.sh b/tooling/validate.sh index a569c38..04209f0 100755 --- a/tooling/validate.sh +++ b/tooling/validate.sh @@ -31,3 +31,7 @@ fi echo "validating ${#presets[@]} presets against renovate@${RENOVATE_VERSION}" npx --yes --package "renovate@${RENOVATE_VERSION}" -- \ renovate-config-validator --strict "${presets[@]}" + +# The validator checks that the rules are well formed, not what they decide. +# Every fault this preset has shipped passed it. +python3 tooling/test_policy.py From 6edf605d4af0545ded965de481a50678cc972dee Mon Sep 17 00:00:00 2001 From: Miccy Date: Sun, 23 Aug 2026 03:24:01 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=94=92=20feat:=20run=20biome=20in=20t?= =?UTF-8?q?he=20gate,=20and=20say=20what=20a=20copy=20costs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Biome was configured and run by nothing. Now part of validate.sh, pinned to the version biome.json declares -- biome rejects a schema from a different release, so the two move together or not at all. The custom manager in renovate.json was widened to keep both pinned tools updated, not just renovate. Formatting applied. Verified it changed no meaning: every preset parses to the same object as before. The adoption section contradicted itself -- "copy this repo" followed by "do not fork it". Those were two different situations written as one. Split into three, with the cost of each stated: - extend: fixes reach you - extend and override: same, for a second brand, instead of forking - take a copy: you own it, and it stops receiving fixes That last cost is not hypothetical. Four faults were found here that no amount of reading the documentation would have surfaced, and a copy taken before each one kept it. Anyone choosing that path should choose it knowing. The template flag is back on. Turning it off during the transfer cleanup was my error: I read it as contradicting "inherit, do not fork", but that advice is about a second brand inside one org, not about someone else adopting this as a starting point. Those are different, and the README now says so. --- README-cs.md | 34 ++++++++++++++++++++++++++++++---- README.md | 36 ++++++++++++++++++++++++++++++------ aggressive.json | 12 +++--------- default.json | 19 +++++++++++++++++-- lockdown.json | 25 ++++++------------------- no-automerge.json | 8 ++------ renovate.json | 14 +++++--------- tooling/validate.sh | 7 +++++++ 8 files changed, 100 insertions(+), 55 deletions(-) diff --git a/README-cs.md b/README-cs.md index 2499ae9..c95a859 100644 --- a/README-cs.md +++ b/README-cs.md @@ -174,10 +174,17 @@ Renovate matching reimplementuje, místo aby volal Renovate, takže se od skutečného enginu může rozejít. Selhání je důvod se podívat; průchod je slabší důkaz než dry run. -## Pod vlastním účtem +## Tři způsoby, jak si to vzít, a co který stojí -Repozitář si zkopíruj, ale **neforkuj ho pro každou organizaci zvlášť.** -Dědění je levnější: +**Jen dědit.** Opravy k tobě dorazí ve chvíli, kdy vzniknou. Nic neudržuješ. + +```json +{ "extends": ["github>ownctrl/supply-chain"] } +``` + +**Dědit a přebít.** Totéž plus vlastní doplňky. Tohle použij pro druhou značku +nebo tým místo forku — jedna bezpečnostní oprava by se neměla dělat tolikrát, +kolik máš kopií. ```json { @@ -186,7 +193,26 @@ Dědění je levnější: } ``` -Fork znamená opravit každou bezpečnostní vadu tolikrát, kolik máš kopií. +**Vzít si kopii.** Tenhle repozitář je GitHub template. Sáhni po něm, když +politiku potřebuješ vlastnit — odstřižené prostředí, požadavek na compliance, +nesouhlas s nějakým rozhodnutím tady. + +Ale ber to s cenovkou: **kopie přestane dostávat opravy.** V tomhle presetu +se našly čtyři vady, na které by nikdo nepřišel čtením dokumentace, a kopie +vzatá před kteroukoli z nich si ji nechala. U bezpečnostní politiky je tohle +ten drahý směr. + +Když chceš vlastní adresu **i** opravy, ať tvoje kopie dědí odsud: + +```json +{ + "extends": ["github>ownctrl/supply-chain"], + "packageRules": [ /* jen tvoje odchylky */ ] +} +``` + +Workflow `Setup Owner` po ručním spuštění přepíše reference a LICENSE na +nového vlastníka. ## Verzování diff --git a/README.md b/README.md index b3c1c40..b313b50 100644 --- a/README.md +++ b/README.md @@ -160,11 +160,17 @@ Pin a release if you do not want your policy to change under you: { "extends": ["github>ownctrl/supply-chain#v1.0.0"] } ``` -### Using it under your own account +### Three ways to adopt this, and what each costs -Copy this repo, then reference your own copy. Do **not** fork it per -organisation — inherit instead, so one security fix does not have to be applied -once per copy: +**Just extend it.** Fixes reach you the moment they land. Nothing to maintain. + +```json +{ "extends": ["github>ownctrl/supply-chain"] } +``` + +**Extend it and override.** Same, plus your own additions. Use this for a +second brand or team rather than forking — one security fix should not have to +be applied once per copy. ```json { @@ -173,8 +179,26 @@ once per copy: } ``` -The `Setup Owner` workflow rewrites the examples and LICENSE to the new owner -when you dispatch it manually. +**Take a copy.** This repo is a GitHub template. Use it when you need to own +the policy outright — an air-gapped mirror, a compliance requirement, a +disagreement with a decision here. + +Be clear about the price: **a copy stops receiving fixes.** Four faults were +found in this preset that no amount of reading the documentation would have +surfaced, and a copy taken before each one kept it. For a security policy that +is the expensive direction. + +If you want your own address *and* the fixes, have your copy extend upstream: + +```json +{ + "extends": ["github>ownctrl/supply-chain"], + "packageRules": [ /* your deviations, and only those */ ] +} +``` + +The `Setup Owner` workflow rewrites the preset references and LICENSE to the +new owner when you dispatch it manually. ### JavaScript runtimes and package managers diff --git a/aggressive.json b/aggressive.json index 996643b..1b16612 100644 --- a/aggressive.json +++ b/aggressive.json @@ -1,20 +1,14 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "description": "Low-friction: updates any time, no release-age floor, higher PR limit. Not for use during an active supply-chain incident.", - "extends": [ - "local>ownctrl/supply-chain" - ], - "schedule": [ - "at any time" - ], + "extends": ["local>ownctrl/supply-chain"], + "schedule": ["at any time"], "prConcurrentLimit": 10, "minimumReleaseAge": null, "packageRules": [ { "description": "Top-level minimumReleaseAge cannot lift the inherited npm rule -- a packageRule outranks it, so drop the floor here too", - "matchDatasources": [ - "npm" - ], + "matchDatasources": ["npm"], "minimumReleaseAge": null } ] diff --git a/default.json b/default.json index 735a572..ee07335 100644 --- a/default.json +++ b/default.json @@ -140,7 +140,14 @@ }, { "description": "Python: group together", - "matchManagers": ["pip_requirements", "pip-compile", "poetry", "pep621", "pipenv", "setup-cfg"], + "matchManagers": [ + "pip_requirements", + "pip-compile", + "poetry", + "pep621", + "pipenv", + "setup-cfg" + ], "groupName": "Python", "separateMinorPatch": false }, @@ -182,7 +189,15 @@ }, { "description": "Kubernetes: group together", - "matchManagers": ["kubernetes", "helmv3", "helm-values", "helm-requirements", "helmfile", "flux", "argocd"], + "matchManagers": [ + "kubernetes", + "helmv3", + "helm-values", + "helm-requirements", + "helmfile", + "flux", + "argocd" + ], "groupName": "Kubernetes", "separateMinorPatch": false }, diff --git a/lockdown.json b/lockdown.json index d7c235d..d30d96f 100644 --- a/lockdown.json +++ b/lockdown.json @@ -1,37 +1,26 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "description": "🔒 Lockdown: no automerge anywhere, 14-day floor, every update waits for dashboard approval. For use during an active supply-chain incident.", - "extends": [ - "local>ownctrl/supply-chain" - ], - "schedule": [ - "before 06:00 on monday" - ], + "extends": ["local>ownctrl/supply-chain"], + "schedule": ["before 06:00 on monday"], "minimumReleaseAge": "14 days", "prConcurrentLimit": 2, "dependencyDashboardApproval": true, "packageRules": [ { "description": "LOCKDOWN: npm floor to 14 days -- must be a packageRule, the top-level value is outranked by the inherited npm rule", - "matchDatasources": [ - "npm" - ], + "matchDatasources": ["npm"], "minimumReleaseAge": "14 days" }, { "description": "LOCKDOWN: Disable ALL automerge", "automerge": false, "dependencyDashboardApproval": true, - "matchPackageNames": [ - "*" - ] + "matchPackageNames": ["*"] }, { "description": "LOCKDOWN: Production deps require manual approval", - "matchDepTypes": [ - "dependencies", - "peerDependencies" - ], + "matchDepTypes": ["dependencies", "peerDependencies"], "prPriority": -50, "prBodyNotes": [ "🔒 **LOCKDOWN MODE ACTIVE**", @@ -47,9 +36,7 @@ }, { "description": "LOCKDOWN: Block all major updates", - "matchUpdateTypes": [ - "major" - ], + "matchUpdateTypes": ["major"], "enabled": false } ] diff --git a/no-automerge.json b/no-automerge.json index 4e1a055..365d7de 100644 --- a/no-automerge.json +++ b/no-automerge.json @@ -1,15 +1,11 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "description": "Disable automerge entirely, keeping every other policy from the base preset.", - "extends": [ - "local>ownctrl/supply-chain" - ], + "extends": ["local>ownctrl/supply-chain"], "packageRules": [ { "automerge": false, - "matchPackageNames": [ - "*" - ] + "matchPackageNames": ["*"] } ] } diff --git a/renovate.json b/renovate.json index 0a27d99..34842e5 100644 --- a/renovate.json +++ b/renovate.json @@ -1,18 +1,14 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "description": "This repo eats its own dog food: it consumes the preset it publishes. Without this, the action digests and the pinned renovate version in .github/workflows/ would rot, and a stale pin is worse than a floating tag.", - "extends": [ - "local>ownctrl/supply-chain" - ], + "description": "This repo eats its own dog food: it consumes the preset it publishes. Without this, the action digests in .github/workflows/ and the tool versions pinned in tooling/validate.sh would rot, and a stale pin is worse than a floating tag.", + "extends": ["local>ownctrl/supply-chain"], "customManagers": [ { - "description": "Keep the validator pin in tooling/validate.sh updated", + "description": "Keep the pinned tool versions in tooling/validate.sh updated", "customType": "regex", - "managerFilePatterns": [ - "/^tooling/validate\\.sh$/" - ], + "managerFilePatterns": ["/^tooling/validate\\.sh$/"], "matchStrings": [ - "# renovate: datasource=(?\\S+) depName=(?\\S+)\\s+RENOVATE_VERSION=\"\\$\\{RENOVATE_VERSION:-(?[^}\"]+)\\}\"" + "# renovate: datasource=(?\\S+) depName=(?\\S+)\\s+[A-Z_]+_VERSION=\"\\$\\{[A-Z_]+:-(?[^}\"]+)\\}\"" ] } ] diff --git a/tooling/validate.sh b/tooling/validate.sh index 04209f0..bca45a1 100755 --- a/tooling/validate.sh +++ b/tooling/validate.sh @@ -13,6 +13,8 @@ set -euo pipefail # renovate: datasource=npm depName=renovate RENOVATE_VERSION="${RENOVATE_VERSION:-44.39.1}" +# renovate: datasource=npm depName=@biomejs/biome +BIOME_VERSION="${BIOME_VERSION:-2.5.9}" cd "$(dirname "$0")/.." @@ -35,3 +37,8 @@ npx --yes --package "renovate@${RENOVATE_VERSION}" -- \ # The validator checks that the rules are well formed, not what they decide. # Every fault this preset has shipped passed it. python3 tooling/test_policy.py + +# Formatting. Pinned to the version biome.json declares -- biome rejects a +# schema from a different release, so the two move together or not at all. +echo "checking formatting with @biomejs/biome@${BIOME_VERSION}" +npx --yes --package "@biomejs/biome@${BIOME_VERSION}" -- biome check . From 8984e9e4dcb22b243ec271adb558e87f47548d91 Mon Sep 17 00:00:00 2001 From: Miccy Date: Sun, 23 Aug 2026 03:28:35 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20docs:=20state=20the=20limits?= =?UTF-8?q?=20in=20the=20README,=20not=20only=20in=20the=20licence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MIT already disclaims warranty and liability, but nobody reads LICENSE. A preset that decides what merges unattended should say where it stops, in the file people actually open. Five limits, all of them true and none obvious: it gates updates rather than what is already installed, it is not a scanner, a release-age delay is not a guarantee, the watch list describes one attack in one month, and the policy test reimplements Renovate's matching rather than calling it. Documents the Setup Owner workflow for the copy path, including that it runs with contents: write and pushes to the default branch. Anyone extending the preset instead of copying it will never run it, and should know that without having to read the YAML. --- README-cs.md | 33 +++++++++++++++++++++++++++++++++ README.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/README-cs.md b/README-cs.md index c95a859..ef5f37f 100644 --- a/README-cs.md +++ b/README-cs.md @@ -214,6 +214,39 @@ Když chceš vlastní adresu **i** opravy, ať tvoje kopie dědí odsud: Workflow `Setup Owner` po ručním spuštění přepíše reference a LICENSE na nového vlastníka. +## Co to nedělá + +Preset rozhoduje, které aktualizace závislostí smějí přistát bez lidského +pohledu. To je celý jeho rozsah a stojí za to říct natvrdo, kde končí: + +- **Hlídá aktualizace, ne to, co už máš.** Hostilní verze, která ti už leží + v lockfilu, je mimo jeho dosah. Zkontroluj lockfile přímo a rotuj všechno, na + co ten balíček mohl dosáhnout. +- **Není to skener.** Neprohlíží obsah balíčků, neověřuje, že vydaný artefakt + odpovídá zdrojáku, ani nedetekuje kompromitaci. Kombinuj ho s něčím, co to + umí. +- **Odstup není záruka.** `minimumReleaseAge` kupuje čas, aby si někdo jiný + všiml špatného vydání. Nemusí si všimnout nikdo. Shai-Hulud zůstal + u některých balíčků nepovšimnutý déle než sedm dní. +- **Watch list je snímek.** 428 balíčků známých z jednoho útoku v listopadu + 2025. O tom příštím neříká nic. +- **Policy test reimplementuje Renovate matching**, místo aby volal Renovate, + takže se od skutečného enginu může rozejít. + +Poskytováno tak, jak je, bez záruky, pod [licencí MIT](./LICENSE). Rozhodnutí, +co smí tvůj projekt sloučit bez dozoru, je tvoje; tenhle preset je výchozí bod +s odůvodněním, ne náhrada za to rozhodnutí. + +## Workflow `Setup Owner` + +`.github/workflows/setup-owner.yml` je tu pro cestu s kopií. Po ručním +spuštění přepíše reference presetu a copyright v LICENSE na nového vlastníka +a pak sám sebe smaže. + +Běží s `contents: write` a pushuje na výchozí větev, což k té práci potřebuje. +Je jen `workflow_dispatch` — nic ho nespouští automaticky. Když preset dědíš +místo kopírování, nikdy ho nepustíš. + ## Verzování `main` je to, co dostane každý, kdo si nepinnul `github>ownctrl/supply-chain` — diff --git a/README.md b/README.md index b313b50..758c607 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,40 @@ The full list lives in [`default.json`](./default.json). Affected version ranges --- +## What this does not do + +This preset decides which dependency updates may merge unattended. That is its +whole scope, and it is worth being blunt about the edges: + +- **It gates updates, not what you already have.** A hostile version already in + your lockfile is outside its reach. Check the lockfile directly and rotate + anything the package could have read. +- **It is not a scanner.** It does not inspect package contents, verify that a + published artifact matches its source, or detect compromise. Pair it with + something that does. +- **A delay is not a guarantee.** `minimumReleaseAge` buys time for someone + else to notice a bad release. Nobody may notice. Shai-Hulud went unnoticed + for longer than seven days in some packages. +- **The watch list is a snapshot.** 428 packages known compromised in one + attack, in November 2025. It says nothing about the next one. +- **The policy test reimplements Renovate's matching** rather than calling + Renovate, so it can drift from the real engine. + +Provided as is, without warranty, under the [MIT licence](./LICENSE). Deciding +what your project may merge unattended is your decision; this preset is a +starting point with reasoning attached, not a substitute for making it. + +## The `Setup Owner` workflow + +`.github/workflows/setup-owner.yml` exists for the copy path. Dispatch it +manually after taking a copy and it rewrites the preset references and the +LICENSE copyright to whoever now owns the repo, then deletes itself. + +It runs with `contents: write` and pushes to the default branch, which is what +it needs to do that job. It is `workflow_dispatch` only — nothing triggers it +automatically. If you are extending this preset rather than copying it, you +will never run it. + ## Versioning `main` is what an unpinned `github>ownctrl/supply-chain` resolves to, so every