diff --git a/.github/workflows/setup-owner.yml b/.github/workflows/setup-owner.yml index da0043a..440277c 100644 --- a/.github/workflows/setup-owner.yml +++ b/.github/workflows/setup-owner.yml @@ -28,9 +28,9 @@ jobs: env: OWNER: ${{ steps.owner.outputs.owner }} run: | - # Point the examples at the new owner's copy of this repo. - sed -i "s|ownctrl/supply-chain|${OWNER}/supply-chain|g" examples/*.json - echo "Updated examples to use owner: ${OWNER}" + # Point the presets at the new owner's copy of this repo. + sed -i "s|ownctrl/supply-chain|${OWNER}/supply-chain|g" ./*.json + echo "Updated presets to use owner: ${OWNER}" - name: Update License env: @@ -51,6 +51,6 @@ jobs: env: OWNER: ${{ steps.owner.outputs.owner }} run: | - git add examples/ LICENSE .github/workflows/setup-owner.yml + git add ./*.json LICENSE .github/workflows/setup-owner.yml git commit -m "chore: setup repository owner ($OWNER)" git push diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d467de4..18649af 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -33,10 +33,11 @@ jobs: node-version: 22 # Pinned so CI does not silently execute whatever renovate publishes next. - - name: Validate default preset and examples + - name: Validate all presets env: # renovate: datasource=npm depName=renovate RENOVATE_VERSION: 44.39.1 run: | npx --yes --package "renovate@${RENOVATE_VERSION}" -- \ - renovate-config-validator --strict default.json renovate.json examples/*.json + renovate-config-validator --strict default.json renovate.json \ + lockdown.json aggressive.json no-automerge.json diff --git a/README.md b/README.md index 4dd676a..f88a11a 100644 --- a/README.md +++ b/README.md @@ -208,72 +208,25 @@ LOG_LEVEL=debug renovate --platform=local --dry-run=true npx renovate --platform=local --dry-run=true ``` -## Common overrides +## Presets -See practical examples in [`examples/`](./examples/) directory. +Each of these is a ready preset, not a snippet to copy. Reference it directly: -### 🔒 Lockdown Mode (Active Threats) +| Preset | Reference | What it changes | +| --- | --- | --- | +| base | `local>ownctrl/supply-chain` | the policy described above | +| lockdown | `local>ownctrl/supply-chain:lockdown` | nothing automerges, 14-day npm floor, every update waits for dashboard approval | +| no-automerge | `local>ownctrl/supply-chain:no-automerge` | automerge off, everything else unchanged | +| aggressive | `local>ownctrl/supply-chain:aggressive` | any time, no release-age floor, higher PR limit | -For maximum security during active supply chain attacks ([example](./examples/renovate-lockdown.json)): +Reach for **lockdown** during an active supply-chain incident and **aggressive** +only when you are certain there is not one. ```json -{ - "extends": ["local>ownctrl/supply-chain"], - "prConcurrentLimit": 2, - "dependencyDashboardApproval": true, - "packageRules": [ - { "matchDatasources": ["npm"], "minimumReleaseAge": "14 days" }, - { "matchPackageNames": ["*"], "automerge": false } - ] -} -``` - -### 🛡️ Security-Hardened (Recommended) - -Balanced security without too much friction ([example](./examples/renovate-security-hardened.json)): - -```json -{ - "extends": ["local>ownctrl/supply-chain"], - "packageRules": [ - { "matchDepTypes": ["dependencies"], "automerge": false } - ] -} +{ "extends": ["local>ownctrl/supply-chain:lockdown"] } ``` -### More aggressive updates - -For non-critical projects where you want faster updates ([example](./examples/renovate-aggressive.json)): - -```json -{ - "extends": ["local>ownctrl/supply-chain"], - "schedule": ["at any time"], - "prConcurrentLimit": 10, - "minimumReleaseAge": null, - "packageRules": [ - { "matchDatasources": ["npm"], "minimumReleaseAge": null } - ] -} -``` - -⚠️ **Warning**: Not recommended during active supply chain threats! - -### Disable automerge completely - -For critical projects requiring manual review ([example](./examples/renovate-no-automerge.json)): - -```json -{ - "extends": ["local>ownctrl/supply-chain"], - "packageRules": [ - { - "matchPackageNames": ["*"], - "automerge": false - } - ] -} -``` +Sub-presets extend the base themselves, so you do not list both. ### Custom timezone diff --git a/examples/renovate-aggressive.json b/aggressive.json similarity index 54% rename from examples/renovate-aggressive.json rename to aggressive.json index dc1c3b8..996643b 100644 --- a/examples/renovate-aggressive.json +++ b/aggressive.json @@ -1,14 +1,20 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "description": "Example: More aggressive updates", - "extends": ["local>ownctrl/supply-chain"], - "schedule": ["at any time"], + "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" + ], "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 99832df..37661ba 100644 --- a/default.json +++ b/default.json @@ -50,19 +50,7 @@ "pinDigests": true }, { - "description": "Group all non-major updates together", - "matchManagers": [ - "npm", - "bun", - "bun-version", - "deno", - "cargo", - "docker", - "github-actions", - "nix", - "terraform", - "ansible" - ], + "description": "Group all non-major updates together, in every ecosystem. Per-language rules below split specific ones back out.", "matchUpdateTypes": ["minor", "patch", "pin", "digest"], "groupName": "all non-major updates" }, @@ -144,6 +132,60 @@ "groupName": "Deno", "separateMinorPatch": false }, + { + "description": "Go: group together", + "matchManagers": ["gomod"], + "groupName": "Go", + "separateMinorPatch": false + }, + { + "description": "Python: group together", + "matchManagers": ["pip_requirements", "pip-compile", "poetry", "pep621", "pipenv", "setup-cfg"], + "groupName": "Python", + "separateMinorPatch": false + }, + { + "description": "JVM: group together", + "matchManagers": ["gradle", "gradle-wrapper", "maven", "maven-wrapper", "sbt"], + "groupName": "JVM", + "separateMinorPatch": false + }, + { + "description": ".NET: group together", + "matchManagers": ["nuget"], + "groupName": ".NET", + "separateMinorPatch": false + }, + { + "description": "PHP: group together", + "matchManagers": ["composer"], + "groupName": "PHP", + "separateMinorPatch": false + }, + { + "description": "Ruby: group together", + "matchManagers": ["bundler"], + "groupName": "Ruby", + "separateMinorPatch": false + }, + { + "description": "Dart: group together", + "matchManagers": ["pub"], + "groupName": "Dart", + "separateMinorPatch": false + }, + { + "description": "Swift: group together", + "matchManagers": ["swift", "cocoapods"], + "groupName": "Swift", + "separateMinorPatch": false + }, + { + "description": "Kubernetes: group together", + "matchManagers": ["kubernetes", "helmv3", "helm-values", "helm-requirements", "helmfile", "flux", "argocd"], + "groupName": "Kubernetes", + "separateMinorPatch": false + }, { "description": "Rust: Group cargo crates together", "matchManagers": ["cargo"], diff --git a/docs/clanek-supply-chain-cs.md b/docs/clanek-supply-chain-cs.md new file mode 100644 index 0000000..f525b1c --- /dev/null +++ b/docs/clanek-supply-chain-cs.md @@ -0,0 +1,224 @@ +--- +title: "Váš robot na aktualizace závislostí je taky cesta dovnitř" +perex: "Automatické aktualizace závislostí jsou dnes standard. Málokdo si přitom přečte, co přesně jeho konfigurace dovoluje sloučit bez lidského oka. Když jsme si to přečetli my, našli jsme čtyři díry — a tři z nich by nenašel nikdo, kdo jen následuje dokumentaci." +tags: [supply-chain, renovate, bezpečnost, open-source] +lang: cs +draft: true +--- + +## Útok, který nepotřebuje vaši chybu + +V listopadu 2025 se npm ekosystémem prohnal Shai-Hulud 2.0. Nešlo o díru +v něčím kódu. Šlo o to, že se útočník dostal k publikačním právům legitimních +balíčků a vydal novou verzi — a všechno, co ten balíček používalo v rozsahu +semver, si ji samo stáhlo. + +To je na tom to podstatné a je dobré si to říct nahlas: **vy jste neudělali +nic špatně.** Váš kód se nezměnil. Změnilo se něco, čemu jste se rozhodli +věřit, a ta důvěra byla vyjádřená stříškou ve `package.json`. + +Obrana proti tomuhle není lepší kód. Je to politika. Konkrétně odpověď na +jedinou otázku: + +> Které aktualizace závislostí smějí přistát, aniž se na ně někdo podívá? + +Většina týmů na tuhle otázku nikdy vědomě neodpověděla. Odpověděl za ně +výchozí config jejich bota. + +## Co je supply-chain + +[`ownctrl/supply-chain`](https://github.com/ownctrl/supply-chain) je sdílený +preset pro [Renovate](https://renovatebot.com), který tu odpověď zapisuje +explicitně. Vezmete si ho jedním řádkem: + +```json +{ "extends": ["local>ownctrl/supply-chain"] } +``` + +A to je celé nastavení. Žádný per-repo tuning. + +Odpověď, kterou ten preset dává, je záměrně úzká: + +**Sloučí se samo:** důvěryhodné vývojářské nástroje (Biome, Oxlint, +TypeScript, Vitest, Jest, ESLint, Prettier) a minor/patch aktualizace CI akcí. + +**Počká na vás:** produkční závislosti, obnova lockfilu, holé změny digestu +a všechny major verze. + +Ta druhá skupina není seznam nepohodlných výjimek. To jsou přesně cesty, +kterými se supply-chain útok šíří. + +## Čtyři věci, které jsme našli ve vlastním configu + +Preset jsme napsali jako reakci na Shai-Hulud. O devět měsíců později jsme si +ho pořádně přečetli. Tady je, co v něm bylo — a proč to jsou obecné pasti, ne +naše specifické hlouposti. + +### 1. Sedmidenní odstup nikdy neplatil + +V configu stálo `"minimumReleaseAge": "7 days"`. V README stálo, že čekáme +sedm dní, než balíček navrhneme. Obojí byla pravda o tom, co jsme napsali, +a lež o tom, co se dělo. + +Preset totiž dědil `security:minimumReleaseAgeNpm`, který nastavuje **tři dny +přes `packageRule`**. A `packageRule` přebíjí hodnotu z nejvyšší úrovně. Takže +npm — ekosystém, kvůli kterému ten preset vůbec vznikl — jel na třech dnech. + +To je past, do které spadne každý, kdo kombinuje vlastní hodnoty s děděnými +presety. **Nastavení na nejvyšší úrovni je slabší než pravidlo, ne silnější.** + +### 2. `^jest` je celý cizí namespace + +Měli jsme seznam „důvěryhodných" nástrojů, které se smějí slučovat samy. +Vypadal rozumně: `^jest`, `^vitest`, `^oxlint`. + +Jenže to jsou regulární výrazy bez ukotvení konce. `^jest` neodpovídá balíčku +`jest`. Odpovídá **každému balíčku, jehož název začíná na `jest`** — a prefix +`jest-` na npm nikdo nevlastní. Může do něj publikovat kdokoli. + +Kdokoli by tedy mohl vydat `jest-cokoli`, vy byste si to jednou přidali jako +vývojářskou závislost, a od té chvíle by se jeho aktualizace slučovaly bez +review. S důvěrou, kterou jste chtěli dát projektu Jest. + +**Ukotvujte celý název**, pokud není ve scope, který vlastníte. `^jest$` ano. +`^@testing-library/` taky ano, protože npm scopes vlastníka mají. `^jest` ne. + +### 3. Obnova lockfilu obchází všechny brzdy + +Renovate umí týdně přegenerovat lockfile, aby stáhl nejnovější verze v rámci +existujících rozsahů. Měli jsme to zapnuté a slučovalo se to samo. + +Ta funkce ale **nepodléhá `minimumReleaseAge`**. Renovate to říká ve vlastní +dokumentaci: kontrola stáří se nevztahuje na `pin`, `lockFileMaintenance`, +`lockfileUpdate`, `rollback`, `bump` ani `replacement`. + +Takže každé pondělí ráno se každá tranzitivní závislost posunula na nejnovější +vyhovující verzi, bez jakéhokoli odstupu a bez review. Tranzitivní šíření +uvnitř semver rozsahů je přesně to, jak Shai-Hulud cestoval. + +Vypnuli jsme to. Stojí to jeden klik týdně. + +### 4. Validátor nechytá to, co byste čekali + +Přidali jsme do CI `renovate-config-validator --strict`. Hned se to vyplatilo: +zachytil chybu v opravě, kterou jsme psali o commit dřív, a která by způsobila, +že Renovate celé pravidlo zahodí — díra by zůstala otevřená, ale vypadalo by to +opravené. + +Jenže má hranice, které stojí za to znát. Config obsahoval +`matchManagers: ["npm", "pnpm", "yarn"]`. **`pnpm` ani `yarn` nejsou managery** +— všechny tři lockfily obsluhuje manager `npm`. Ty dva zápisy neodpovídaly +ničemu. + +Validátor to pustil bez jediného varování. Ověřovali jsme to schválně. + +Horší bylo, co z toho plynulo: manager `bun` je samostatný a v tom pravidle +chyběl. Repozitáře, které jedou výhradně na bunu, nedostávaly pinování verzí, +které jsme v README inzerovali jako základní vlastnost. + +**Váš CI gate kontroluje schéma, ne význam.** Překlep v názvu manageru tiše +vypne celé pravidlo. + +## Co to usnadňuje + +Praktický zisk není „bezpečnost" jako abstraktní pocit. Je konkrétní: + +- **Nové repo je hotové jedním řádkem.** Žádné kopírování konfigurace, žádné + rozhodování, žádný per-projekt tuning. +- **Jedna oprava platí všude.** Když se objeví další Shai-Hulud, upravíte + jeden soubor, ne dvacet repozitářů. +- **Politika je čitelná.** Můžete ji někomu ukázat. Můžete se o ní hádat. + To o výchozím nastavení bota nejde. +- **Nezáleží na jazyce.** JavaScript, Rust, Go, Python, JVM, .NET, PHP, Ruby, + Nix, Terraform, Docker, Kubernetes. Hrozba se o váš jazyk nezajímá, tak proč + by se měla politika. + +## Jak si ho vzít a upravit + +### Základ + +Do nového repozitáře stačí `renovate.json`: + +```json +{ "extends": ["local>ownctrl/supply-chain"] } +``` + +Prefix `local>` je tu záměrně. Řekne Renovate, ať preset hledá na tom forge, +kde zrovna běží — takže tentýž řádek funguje na GitHubu, GitLabu, Codebergu +i na vlastním Forgejo, pokud je preset zrcadlený pod stejnou cestou. + +### Když zrovna hoří + +Při aktivním incidentu si přepněte do lockdownu. Nic se neslučuje samo, +čtrnáctidenní odstup, všechno čeká na schválení: + +```json +{ "extends": ["local>ownctrl/supply-chain:lockdown"] } +``` + +### Když nehoří a spěcháte + +Pro nekritické projekty: + +```json +{ "extends": ["local>ownctrl/supply-chain:aggressive"] } +``` + +Žádný odstup, aktualizace kdykoli. Nepoužívejte to, když si nejste jistí, že +zrovna neprobíhá útok. + +### Přepsání jednotlivostí + +Cokoli z presetu jde přebít ve vlastním souboru. Ale pozor na past číslo 1 — +pokud přepisujete něco, co je v presetu nastavené jako `packageRule`, musíte +to taky napsat jako `packageRule`: + +```json +{ + "extends": ["local>ownctrl/supply-chain"], + "packageRules": [ + { "matchDatasources": ["npm"], "minimumReleaseAge": "14 days" } + ] +} +``` + +Nastavení `"minimumReleaseAge": "14 days"` na nejvyšší úrovni by tady +nefungovalo. Přesně tak jsme si tu chybu vyrobili my. + +### Pro vlastní organizaci + +Repozitář si zkopírujte, ale **neforkujte ho pro každou značku zvlášť.** +Dědění je levnější: + +```json +{ + "extends": ["local>ownctrl/supply-chain"], + "labels": ["dependencies", "vaše-značka"] +} +``` + +Fork znamená opravit každou bezpečnostní vadu tolikrát, kolik máte kopií. +Ty čtyři výše bychom opravovali čtyřikrát. + +Pokud nechcete, aby se vám politika měnila pod rukama, připněte si vydání: + +```json +{ "extends": ["local>ownctrl/supply-chain#v1.0"] } +``` + +## Co si z toho odnést, i když náš preset nepoužijete + +1. **Přečtěte si, co váš bot smí sloučit sám.** Ne co si myslíte, že smí. +2. **Ukotvujte vzory názvů**, pokud nejste vlastníkem prefixu nebo scope. +3. **Zjistěte, které typy aktualizací obcházejí vaše brzdy.** U Renovate to + jsou `lockFileMaintenance`, `pin`, `bump`, `rollback` a `replacement`. +4. **Nezaměňujte validaci schématu za validaci významu.** Zelené CI neznamená, + že vaše pravidla něčemu odpovídají. + +Nic z toho není v dokumentaci označené jako past. Všechno to jsou důsledky +dvou pravidel, která dávají samostatně smysl a dohromady vás překvapí. + +--- + +*Preset je pod MIT licencí. Chyby, nápady a nesouhlas vítáme — +[ownctrl/supply-chain](https://github.com/ownctrl/supply-chain).* diff --git a/examples/renovate-no-automerge.json b/examples/renovate-no-automerge.json deleted file mode 100644 index 4f9541d..0000000 --- a/examples/renovate-no-automerge.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "description": "Example: Disable automerge for critical projects", - "extends": ["local>ownctrl/supply-chain"], - "packageRules": [ - { - "automerge": false, - "matchPackageNames": ["*"] - } - ] -} diff --git a/examples/renovate-security-hardened.json b/examples/renovate-security-hardened.json deleted file mode 100644 index 9b4e039..0000000 --- a/examples/renovate-security-hardened.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "description": "🛡️ Security-hardened: Extended stability period, no automerge for production deps, Shai-Hulud package warnings.", - "extends": ["local>ownctrl/supply-chain"], - "packageRules": [ - { - "description": "Production dependencies: Never automerge", - "matchDepTypes": ["dependencies", "peerDependencies", "optionalDependencies"], - "automerge": false - }, - { - "description": "Dev dependencies: Automerge only trusted packages", - "matchDepTypes": ["devDependencies"], - "automerge": false - }, - { - "description": "Trusted dev tools: Allow automerge", - "matchDepTypes": ["devDependencies"], - "automerge": true, - "matchPackageNames": [ - "/^@biomejs//", - "/^@types//", - "/^typescript$/", - "/^vitest$/", - "/^@vitest//", - "/^prettier$/", - "/^eslint$/" - ] - } - ] -} diff --git a/examples/renovate.json b/examples/renovate.json deleted file mode 100644 index eca41f2..0000000 --- a/examples/renovate.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "description": "Example: Basic usage of the shared preset", - "extends": ["local>ownctrl/supply-chain"] -} diff --git a/examples/renovate-lockdown.json b/lockdown.json similarity index 69% rename from examples/renovate-lockdown.json rename to lockdown.json index 666b3f6..d7c235d 100644 --- a/examples/renovate-lockdown.json +++ b/lockdown.json @@ -1,26 +1,37 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "description": "🔒 LOCKDOWN MODE: Maximum security during active supply chain attacks. No automerge, extended stability period, manual approval required.", - "extends": ["local>ownctrl/supply-chain"], - "schedule": ["before 06:00 on monday"], + "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" + ], "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**", @@ -36,7 +47,9 @@ }, { "description": "LOCKDOWN: Block all major updates", - "matchUpdateTypes": ["major"], + "matchUpdateTypes": [ + "major" + ], "enabled": false } ] diff --git a/no-automerge.json b/no-automerge.json new file mode 100644 index 0000000..4e1a055 --- /dev/null +++ b/no-automerge.json @@ -0,0 +1,15 @@ +{ + "$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" + ], + "packageRules": [ + { + "automerge": false, + "matchPackageNames": [ + "*" + ] + } + ] +}