diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..b6b1fb3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,44 @@
+---
+name: Bug report
+about: Iets werkt niet zoals verwacht
+labels: bug
+---
+
+## Wat ging er mis
+
+
+
+## Reproductie
+
+
+
+1. ...
+2. ...
+3. ...
+
+## Omgeving
+
+- **Distro + versie:**
+- **Versie van workstation-security:**
+- **Output van `bash check.sh`:**
+ ```
+
+ ```
+- **WSL?**
+
+## Output / logs
+
+
+
+
+volledige output
+
+```
+...
+```
+
+
+
+## Wat heb je al geprobeerd
+
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..cb75eb5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Documentatie
+ url: https://github.com/MWest2020/workstation-security#readme
+ about: README, threat-model en compliance-mapping staan in de repo zelf — kijk daar eerst.
diff --git a/.github/ISSUE_TEMPLATE/distro_support.md b/.github/ISSUE_TEMPLATE/distro_support.md
new file mode 100644
index 0000000..eab4263
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/distro_support.md
@@ -0,0 +1,39 @@
+---
+name: Distro support
+about: Verzoek voor support van een distro die nog niet in alma/arch/ubuntu valt
+labels: distro-support
+---
+
+## Distro
+
+- **Naam + versie:**
+- **Package-manager:**
+- **`/etc/os-release` inhoud (relevante delen):**
+ ```
+ ID=...
+ ID_LIKE=...
+ ```
+
+## Waarom deze distro
+
+
+
+## Bestaande oplossingen
+
+
+
+- ClamAV-package:
+- ClamAV-daemon-service:
+- rkhunter-package:
+
+## Bereidheid tot testen
+
+
+
+- [ ] Ik gebruik deze distro zelf en kan PRs reviewen / testen.
+- [ ] Ik heb een test-VM / container met deze distro en kan ad-hoc testen.
+- [ ] Ik wil alleen het verzoek indienen en hoop dat iemand anders het oppakt.
+
+## Aanvullende context
+
+
diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml
index 48bc62b..85a1aa8 100644
--- a/.github/workflows/shellcheck.yml
+++ b/.github/workflows/shellcheck.yml
@@ -1,4 +1,4 @@
-name: ShellCheck
+name: shellcheck
on: [push, pull_request]
@@ -11,3 +11,8 @@ jobs:
uses: ludeeus/action-shellcheck@2.0.0
with:
scandir: '.'
+ # Match local pre-commit config (.pre-commit-config.yaml line 30):
+ # severity=warning. Filters SC1091 (un-followable source notes) en
+ # SC2016 (literal-$HOME in user-facing strings — bewust) die op
+ # default-style-niveau noise zijn.
+ severity: warning
diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml
new file mode 100644
index 0000000..0488a49
--- /dev/null
+++ b/.github/workflows/smoke.yml
@@ -0,0 +1,64 @@
+# SPDX-License-Identifier: EUPL-1.2
+#
+# Smoke tests — draait bootstrap.sh --dry-run en check.sh in een container voor
+# elk van de ondersteunde distrofamilies. Geen side effects op de runner;
+# de container is ephemeral.
+#
+# Docker images: officiële Docker Hub images, rolling tag (zie design.md D4).
+# Pin alleen als CI breekt door upstream-wijziging (zie smoke-tests spec, scenario
+# "Rolling-image pinning bij upstream-breuk").
+
+name: smoke
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+jobs:
+ smoke:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - distro: alma9
+ image: almalinux:9
+ - distro: ubuntu2404
+ image: ubuntu:24.04
+ - distro: archlatest
+ image: archlinux:latest
+
+ name: smoke (${{ matrix.distro }})
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: bootstrap.sh --dry-run + check.sh in ${{ matrix.image }}
+ run: |
+ docker run --rm \
+ -v "${{ github.workspace }}:/repo:ro" \
+ -w /repo \
+ "${{ matrix.image }}" \
+ bash -c '
+ set -e
+ echo "=== bootstrap.sh --version ==="
+ bash bootstrap.sh --version
+ echo
+ echo "=== bootstrap.sh --dry-run ==="
+ bash bootstrap.sh --dry-run
+ echo
+ echo "=== check.sh (read-only audit) ==="
+ # check.sh exit-code = aantal problemen (capped op 2).
+ # In een schone container met geen ClamAV install verwachten we
+ # warnings — toegestaan tot exit 2. Boven 2 betekent: check.sh
+ # zelf is stuk, niet de baseline-toestand.
+ bash check.sh || rc=$?
+ if [[ "${rc:-0}" -gt 2 ]]; then
+ echo "check.sh exit=${rc} > 2 — script-fout, niet alleen warnings" >&2
+ exit "${rc}"
+ fi
+ echo
+ echo "=== install-pm-cooldown.sh --dry-run ==="
+ bash common/install-pm-cooldown.sh --dry-run
+ '
diff --git a/.gitignore b/.gitignore
index db32316..601a8f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,9 +15,10 @@ incident-token-revoke.env
*_rsa
*.crt
-# IDE
+# IDE / AI-tooling
.vscode/
.idea/
+.claude/
*.swp
*.swo
*~
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba26fec..4cfbe18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,28 @@
# Changelog
-## 2026-05-18 (avond) — Docs + review-fixes
+## Unreleased — v1.0.0-release-readiness (A2 + C1-C6)
+
+OpenSpec change: [`openspec/changes/v1-release-readiness/`](openspec/changes/v1-release-readiness/). Bundelt zeven kleine clusters richting v1.0.0-tag. PRs landen per cluster; tag wordt pas geplaatst als alles gemerged en CI groen is.
+
+### Toegevoegd
+- `VERSION`-file (top-level, semver `0.9.0`) als single source of truth voor versie. Helper `ws_version()` in `common/lib.sh` leest deze at-runtime; fallback `unknown` als de file ontbreekt (een script dat los gedownload werd faalt niet).
+- `--version` / `-V` flag op alle user-facing entrypoints: `bootstrap.sh`, `check.sh`, en elke `common/*.sh` met CLI (`install-pm-cooldown.sh`, `install-shell-tools.sh`, `incident-token-revoke.sh`, `scan.sh`, `rkhunter-check.sh`, `update.sh`, `uninstall.sh`). Werkt zonder sudo. Helper `ws_handle_version()` in `lib.sh` voor de scripts die lib.sh source'n; `incident-token-revoke.sh` houdt zijn self-contained inline-variant.
+- `--dry-run` flag op alle installers: `bootstrap.sh`, `alma/install.sh`, `arch/install.sh`, `ubuntu/install.sh`, `common/install-timers.sh`, `common/install-pm-cooldown.sh`. Geen side effects in dry-run; output copy-paste-baar naar een echte run. Flag propageert via `WS_DRY_RUN=1` env-var zodat dispatch-ketens 'm doorgeven (zie [`openspec/changes/v1-release-readiness/design.md`](openspec/changes/v1-release-readiness/design.md) D2). Helpers `ws_is_dry_run()` en `ws_run_or_print()` in `lib.sh`.
+- `docs/supply-chain-cooldown.md` — staat-op-zichzelf-doc voor laag 2: aanleiding (npm-incident 2026-05-11), mechanisme per package-manager met versie-vereisten, per-workstation vs per-project vs CI-scope-gap, override-flow voor urgente CVEs. README-sectie 2 verkort tot 3-4 zinnen + link.
+- `LICENSE` — volledige EUPL-1.2-tekst (van SPDX license-list-data, canoniek). Consistent met bestaande SPDX-headers per script.
+- `CONTRIBUTING.md` — project-status, "voor een goede PR", "welke PRs landen makkelijk/lastig", OpenSpec-workflow.
+- `.github/ISSUE_TEMPLATE/` — bug_report.md, distro_support.md, config.yml (`blank_issues_enabled: false`, met link naar README).
+- `.github/workflows/smoke.yml` — GitHub Actions smoke-test matrix (alma9, ubuntu2404, archlatest via officiële Docker Hub images). Draait `bootstrap.sh --version`, `bootstrap.sh --dry-run`, `check.sh`, en `install-pm-cooldown.sh --dry-run` op elke push/PR. Geen side effects op de runner.
+- OpenSpec scaffolding (`openspec/` directory) met de v1-release-readiness change (proposal, tasks, design, drie spec-deltas).
+
+### Gewijzigd
+- `README.md` — herstructureerd: badges bovenaan (smoke, license, shellcheck), doelgroep/scope gepromoveerd vóór de drie verdedigingslagen, elke laag eigen H2 met "wat", "voor wie", "snelle start", clone-URL gecorrigeerd naar `MWest2020/`, expliciete `## License`-sectie onderaan met EUPL-1.2-motivatie (digitale soevereiniteit, NLnet-context).
+- `bootstrap.sh` — source't `common/lib.sh` voor versioning/dry-run helpers, root-check overgeslagen in dry-run-modus (CI-bruikbaar zonder sudo).
+- `common/install-base.sh` — `require_root`, `freshclam_safe`, `rkhunter_init`, `enable_clamav_services` zijn nu dry-run-aware (printen wat ze zouden doen).
+- `common/install-timers.sh` — `ws_write_unit` wrapper rond elke `cat >file </`. Een change bevat:
+
+- `proposal.md` — Why / What / Capabilities / Impact.
+- `tasks.md` — concrete checkboxes per cluster.
+- `specs//spec.md` — requirements met scenarios.
+- `design.md` — alleen wanneer een ontwerpkeuze niet vanzelfsprekend is en gedocumenteerd moet kunnen worden voor een latere lezer / auditor.
+
+Validatie: `openspec validate --strict` moet groen zijn voordat de change archived wordt. De [OpenSpec CLI](https://github.com/Fission-AI/OpenSpec) installeer je via `npm i -g @fission-ai/openspec`.
+
+## License
+
+Door een PR in te dienen ga je akkoord dat je bijdrage onder dezelfde [EUPL-1.2](LICENSE) wordt vrijgegeven die de rest van het project gebruikt. Geen CLA, geen overdracht van rechten — je behoudt copyright op je bijdrage en licentieert hem onder EUPL-1.2.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6d8cea4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,190 @@
+EUROPEAN UNION PUBLIC LICENCE v. 1.2
+EUPL © the European Union 2007, 2016
+
+This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the
+terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such
+use is covered by a right of the copyright holder of the Work).
+The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following
+notice immediately following the copyright notice for the Work:
+ Licensed under the EUPL
+or has expressed by any other means his willingness to license under the EUPL.
+
+1.Definitions
+In this Licence, the following terms have the following meaning:
+— ‘The Licence’:this Licence.
+— ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available
+as Source Code and also as Executable Code as the case may be.
+— ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or
+modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work
+required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in
+the country mentioned in Article 15.
+— ‘The Work’:the Original Work or its Derivative Works.
+— ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and
+modify.
+— ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by
+a computer as a program.
+— ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence.
+— ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to
+the creation of a Derivative Work.
+— ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the
+Licence.
+— ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating,
+transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential
+functionalities at the disposal of any other natural or legal person.
+
+2.Scope of the rights granted by the Licence
+The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for
+the duration of copyright vested in the Original Work:
+— use the Work in any circumstance and for all usage,
+— reproduce the Work,
+— modify the Work, and make Derivative Works based upon the Work,
+— communicate to the public, including the right to make available or display the Work or copies thereof to the public
+and perform publicly, as the case may be, the Work,
+— distribute the Work or copies thereof,
+— lend and rent the Work or copies thereof,
+— sublicense rights in the Work or copies thereof.
+Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the
+applicable law permits so.
+In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed
+by law in order to make effective the licence of the economic rights here above listed.
+The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the
+extent necessary to make use of the rights granted on the Work under this Licence.
+
+3.Communication of the Source Code
+The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as
+Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with
+each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to
+the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to
+distribute or communicate the Work.
+
+4.Limitations on copyright
+Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the
+exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations
+thereto.
+
+5.Obligations of the Licensee
+The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those
+obligations are the following:
+
+Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to
+the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the
+Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work
+to carry prominent notices stating that the Work has been modified and the date of modification.
+
+Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this
+Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless
+the Original Work is expressly distributed only under this version of the Licence — for example by communicating
+‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the
+Work or Derivative Work that alter or restrict the terms of the Licence.
+
+Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both
+the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done
+under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed
+in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with
+his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail.
+
+Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide
+a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available
+for as long as the Licensee continues to distribute or communicate the Work.
+Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names
+of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and
+reproducing the content of the copyright notice.
+
+6.Chain of Authorship
+The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or
+licensed to him/her and that he/she has the power and authority to grant the Licence.
+Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or
+licensed to him/her and that he/she has the power and authority to grant the Licence.
+Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions
+to the Work, under the terms of this Licence.
+
+7.Disclaimer of Warranty
+The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work
+and may therefore contain defects or ‘bugs’ inherent to this type of development.
+For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind
+concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or
+errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this
+Licence.
+This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work.
+
+8.Disclaimer of Liability
+Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be
+liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the
+Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss
+of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However,
+the Licensor will be liable under statutory product liability laws as far such laws apply to the Work.
+
+9.Additional agreements
+While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services
+consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole
+responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by
+the fact You have accepted any warranty or additional liability.
+
+10.Acceptance of the Licence
+The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window
+displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of
+applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms
+and conditions.
+Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You
+by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution
+or Communication by You of the Work or copies thereof.
+
+11.Information to the public
+In case of any Distribution or Communication of the Work by means of electronic communication by You (for example,
+by offering to download the Work from a remote location) the distribution channel or media (for example, a website)
+must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence
+and the way it may be accessible, concluded, stored and reproduced by the Licensee.
+
+12.Termination of the Licence
+The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms
+of the Licence.
+Such a termination will not terminate the licences of any person who has received the Work from the Licensee under
+the Licence, provided such persons remain in full compliance with the Licence.
+
+13.Miscellaneous
+Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the
+Work.
+If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or
+enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid
+and enforceable.
+The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of
+the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence.
+New versions of the Licence will be published with a unique version number.
+All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take
+advantage of the linguistic version of their choice.
+
+14.Jurisdiction
+Without prejudice to specific agreement between parties,
+— any litigation resulting from the interpretation of this License, arising between the European Union institutions,
+bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice
+of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union,
+— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to
+the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business.
+
+15.Applicable Law
+Without prejudice to specific agreement between parties,
+— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat,
+resides or has his registered office,
+— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside
+a European Union Member State.
+
+
+ Appendix
+
+‘Compatible Licences’ according to Article 5 EUPL are:
+— GNU General Public License (GPL) v. 2, v. 3
+— GNU Affero General Public License (AGPL) v. 3
+— Open Software License (OSL) v. 2.1, v. 3.0
+— Eclipse Public License (EPL) v. 1.0
+— CeCILL v. 2.0, v. 2.1
+— Mozilla Public Licence (MPL) v. 2
+— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
+— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software
+— European Union Public Licence (EUPL) v. 1.1, v. 1.2
+— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+).
+
+The European Commission may update this Appendix to later versions of the above licences without producing
+a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the
+covered Source Code from exclusive appropriation.
+All other changes or additions to this Appendix require the production of a new EUPL version.
diff --git a/README.md b/README.md
index fd1beb5..0610c5d 100644
--- a/README.md
+++ b/README.md
@@ -1,90 +1,36 @@
# workstation-security
-Lichtgewicht baseline voor het hardenen van developer workstations. Bedoeld
-om de minimale set verdedigingen op orde te hebben die je voor een ISO 27001
-/ SOC 2 / NEN 7510 / BIO audit moet kunnen aantonen, zonder een full-blown
-EDR uit te rollen.
+[](https://github.com/MWest2020/workstation-security/actions/workflows/smoke.yml)
+[](https://github.com/MWest2020/workstation-security/actions/workflows/shellcheck.yml)
+[](LICENSE)
-Voor mappings naar specifieke control-IDs en het bedreigingsmodel: zie
-[`docs/`](docs/) — `compliance.md` voor de framework-mapping,
-`threat-model.md` voor wat we wel/niet verdedigen.
+Lichtgewicht baseline voor het hardenen van developer workstations. Bedoeld om de minimale set verdedigingen op orde te hebben die je voor een ISO 27001 / SOC 2 / NEN 7510 / BIO audit moet kunnen aantonen, zonder een full-blown EDR uit te rollen.
-## Drie verdedigingslagen
-
-1. **Antivirus + rootkit** — ClamAV (dagelijkse scan van `/home`) en rkhunter
- (rootkit check), via systemd timers. Niet omdat developer workstations
- hét doelwit van klassieke virussen zijn, maar omdat de meeste
- compliance-frameworks *iets* aan AV willen zien.
-2. **Supply-chain cooldown** — 7-daagse quarantine op nieuwe npm / pnpm / bun
- pakketversies. npm yankt malicious supply-chain versies meestal binnen
- 24-48u; de cooldown houdt ze buiten je lockfile vóór ze worden opgemerkt.
- Aanleiding o.a. het npm supply-chain incident van 2026-05-11. User-level
- config (`~/.npmrc`, `~/.bunfig.toml`) plus per-project / CI-templates voor
- waar `~`-config niet leest.
-3. **Incident response — GitHub token compromise** — losse IR-tool voor het
- CanisterSprawl-scenario: een gestolen GitHub-PAT met dead-man's switch die
- `rm -rf ~/` triggert wanneer je 'm probeert te revoken. Detecteert,
- ontwapent veilig (SIGKILL-first, evidence buiten `$HOME`), en wacht op
- handmatige revoke met verify.
-
-Plus: een `check.sh` health-script met betekenisvolle exit-code (cron/CI),
-en een `bootstrap.sh` die `/etc/os-release` leest en automatisch de juiste
-OS-installer draait.
+Voor mappings naar specifieke control-IDs en het bedreigingsmodel: zie [`docs/`](docs/) — `compliance.md` voor de framework-mapping, `threat-model.md` voor wat we wel/niet verdedigen, `supply-chain-cooldown.md` voor de standalone uitleg van laag 2.
## Doelgroep en scope
-- Developer workstations, **niet** servers — de aanname is dat de user
- grotendeels root is op z'n eigen machine en de defaults wil kunnen
- uitleggen aan een auditor.
-- Target distros: Alma / Rocky / RHEL / Fedora / CentOS (dnf), Arch /
- Manjaro / EndeavourOS (pacman), Ubuntu / Debian / Mint / Pop / Raspbian
- (apt). macOS wordt deels ondersteund — het IR-script werkt cross-platform.
-- WSL2 (Windows Subsystem for Linux) is detecteerbaar en wordt netjes
- afgehandeld — scripts skippen systemd-features waar nodig in plaats van
- hard te falen. Zie [WSL Support](#wsl-support) onderaan.
+- Developer workstations, **niet** servers — de aanname is dat de user grotendeels root is op z'n eigen machine en de defaults wil kunnen uitleggen aan een auditor.
+- Target distros: Alma / Rocky / RHEL / Fedora / CentOS (dnf), Arch / Manjaro / EndeavourOS (pacman), Ubuntu / Debian / Mint / Pop / Raspbian (apt). macOS wordt deels ondersteund — het IR-script werkt cross-platform.
+- WSL2 (Windows Subsystem for Linux) is detecteerbaar en wordt netjes afgehandeld — scripts skippen systemd-features waar nodig in plaats van hard te falen. Zie [WSL Support](#wsl-support) onderaan.
-## Gebruik
-
-Clone en installeer in één keer — daarna nooit meer naar omkijken.
+## Drie verdedigingslagen
-### Aanbevolen: auto-detect OS
+Elk standalone te begrijpen en in te zetten. Je kunt 1, 2 en 3 los gebruiken — `bootstrap.sh` rolt 1 uit (de andere zijn aparte invocations).
-```bash
-git clone https://github.com/conduction-it/workstation-security.git
-cd workstation-security
-sudo bash bootstrap.sh
-```
+### 1. Antivirus + rootkit
-`bootstrap.sh` leest `/etc/os-release` en dispatched naar de juiste installer
-(alma/arch/ubuntu). Bij onbekend OS valt het terug op `ID_LIKE` en print
-anders een heldere foutmelding.
+**Wat:** ClamAV (dagelijkse scan van `/home`) en rkhunter (rootkit check), via systemd timers. Bij vondsten een `wall`-melding aan ingelogde users. Logs geroteerd via logrotate (wekelijks, 4 weken bewaard).
-### Of: directe per-OS installer
+**Voor wie:** elke auditor en elk compliance-framework wil *iets* aan AV zien op een workstation, ook al is het bedreigingsmodel voor klassieke virussen op dev-machines beperkt. Dit dekt die eis met minimal overhead.
+**Snelle start:**
```bash
-# Alma / Rocky / CentOS / RHEL / Fedora
-sudo bash alma/install.sh
-
-# Arch / Manjaro / EndeavourOS
-sudo bash arch/install.sh
-
-# Ubuntu / Debian / Mint / Pop / Raspbian
-sudo bash ubuntu/install.sh
+sudo bash bootstrap.sh # auto-detect OS, installeert AV + timers
+sudo bash check.sh # exit-code = aantal problemen (cron/CI-bruikbaar)
```
-Na installatie draait alles automatisch via systemd timers. Geen verdere actie nodig.
-
-## Wat wordt er geïnstalleerd?
-
-| Package | Functie |
-|-------------|----------------------------------|
-| `clamav` | Antivirus scanner |
-| `clamd` | Daemon voor realtime scanning |
-| `rkhunter` | Rootkit detectie |
-
-## Na installatie
-
-Alles loopt automatisch via systemd timers:
+Na installatie:
| Timer | Wanneer | Wat |
|--------------------------|-----------------|----------------------------------|
@@ -92,118 +38,105 @@ Alles loopt automatisch via systemd timers:
| `clamav-scan.timer` | Dagelijks 02:00 | Volledige scan van `/home` |
| `rkhunter-check.timer` | Dagelijks 03:00 | Rootkit check |
-Bij vondsten ontvangen ingelogde gebruikers een `wall`-melding.
+### 2. Supply-chain cooldown (npm / pnpm / bun)
-Logs worden automatisch geroteerd via logrotate (wekelijks, 4 weken bewaard).
+**Wat:** 7-daagse quarantine op verse pakketversies. npm yankt malicious supply-chain versies doorgaans binnen 24-48u — een cooldown houdt ze buiten je lockfile vóór ze opgemerkt worden.
-## Status check
+**Voor wie:** iedereen die `npm install` / `pnpm install` / `bun install` op een dev-machine of in CI draait. Vooral relevant als je projecten met veel transitive deps onderhoudt.
+**Snelle start:**
```bash
-sudo bash check.sh
+bash common/install-pm-cooldown.sh # default 7 dagen
+bash common/install-pm-cooldown.sh --days 14 # andere window
+bash common/install-pm-cooldown.sh --check # huidige state tonen
+bash common/install-pm-cooldown.sh --dry-run # wat het zou doen, geen wijzigingen
```
-Toont services / timers / signatures / laatste scans. Exit-code is gelijk
-aan het aantal gevonden problemen (capped op 2), zodat het in cron of CI
-gebruikt kan worden als gezondheids-probe.
-
-## Handmatige scan
-
-```bash
-# Volledige scan
-sudo clamscan -r /home --infected --log=/var/log/clamav/manual-scan.log
+User-level (`~/.npmrc` + `~/.bunfig.toml`). Voor CI / per-project / override-flow zie [`docs/supply-chain-cooldown.md`](docs/supply-chain-cooldown.md).
-# rkhunter check
-sudo rkhunter --check --skip-keypress
-```
+### 3. Incident response — GitHub-token compromise
-## Incident response — GitHub-token dead-man's switch
+**Wat:** losse IR-tool voor het CanisterSprawl-scenario (gestolen GitHub-PAT met dead-man's switch die `rm -rf ~/` triggert wanneer je 'm probeert te revoken). Detecteert, ontwapent veilig (SIGKILL-first, evidence buiten `$HOME`), en wacht op handmatige revoke met verify.
-Voor het CanisterSprawl-scenario (gestolen GitHub-PAT met dead-man's switch
-die `rm -rf ~/` triggert bij token-revoke):
+**Voor wie:** als je `gh` CLI gebruikt of een GitHub PAT in je shell hebt en het scenario klinkt niet hypothetisch genoeg om te negeren. Bij twijfel: draai eerst `--dry-run` om te zien of de detectie iets vindt.
+**Snelle start:**
```bash
bash common/incident-token-revoke.sh --dry-run # alleen detectie
bash common/incident-token-revoke.sh # volledige flow
```
-Werkt user-level (geen root nodig). Vangt eerst de huidige `gh`-token op
-(hash + last-4) voor latere verify, detecteert IOC's + heuristisch, kill't
-polling-processen met **SIGKILL voordat** systemd er een SIGTERM op stuurt
-(een TERM-trap in de payload kan namelijk alsnog `rm -rf ~/` triggeren),
-archiveert artefacten naar `/tmp/incident-/` (overleeft `rm -rf ~/`),
-maakt de token op deze machine onbruikbaar, en wacht op handmatige revoke
-op github.com/settings/tokens (er is geen user-self-revoke REST endpoint).
+User-level (geen root nodig). Schone runs laten niks achter op disk; alleen bij findings wordt `/tmp/incident-/` aangemaakt. Voor optionele mail-rapportage via SMTPS zie de "Optionele mail-rapportage"-sectie verderop.
-Schone runs laten niks achter op disk; alleen bij findings wordt
-`/tmp/incident-/` aangemaakt.
+## Installatie
-### Optionele mail-rapportage
-
-Standaard blijft alles lokaal. Voor een SMTPS-mail (bv. Gmail) na afloop:
+Clone en installeer in één keer.
```bash
-mkdir -p ~/.config/workstation-security
-cp common/incident-token-revoke.env.example ~/.config/workstation-security/mail.env
-chmod 600 ~/.config/workstation-security/mail.env
-$EDITOR ~/.config/workstation-security/mail.env
+git clone https://github.com/MWest2020/workstation-security.git
+cd workstation-security
+sudo bash bootstrap.sh
```
-Het script weigert te mailen als die file niet op mode 600/400 staat
-(bevat een App Password).
+`bootstrap.sh` leest `/etc/os-release` en dispatched naar de juiste installer (alma/arch/ubuntu). Bij onbekend OS valt het terug op `ID_LIKE` en print anders een heldere foutmelding.
-## Package-manager cooldown (npm / pnpm / bun)
+Direct per-OS installer kan ook:
+
+```bash
+sudo bash alma/install.sh # Alma / Rocky / CentOS / RHEL / Fedora
+sudo bash arch/install.sh # Arch / Manjaro / EndeavourOS
+sudo bash ubuntu/install.sh # Ubuntu / Debian / Mint / Pop / Raspbian
+```
-Een 7-daagse quarantine op verse pakketversies verdedigt tegen
-supply-chain attacks (npm yankt malicious versies doorgaans binnen 24-48u —
-een 7-daagse cooldown houdt ze buiten je lockfile vóór ze opgemerkt worden).
+Voor CI / audit-evidence-flows: elk van bovenstaande accepteert `--dry-run`. Geen side effects, exit 0, output copy-paste-baar.
```bash
-bash common/install-pm-cooldown.sh # default 7 dagen
-bash common/install-pm-cooldown.sh --days 14 # andere window
-bash common/install-pm-cooldown.sh --check # alleen huidige state tonen
+sudo bash bootstrap.sh --dry-run # print welke sub-installer aangeroepen zou worden
+bash ubuntu/install.sh --dry-run # print zou-uitgevoerd-zijn pkg-manager commando's
```
-Idempotent en user-level (geen sudo). Schrijft naar:
+`--version` werkt op alle entrypoints en respecteert geen sudo:
+
+```bash
+bash bootstrap.sh --version # workstation-security
+```
-| File | Key | Eenheid | Manager |
-|-------------------|--------------------------------|----------|---------------|
-| `~/.npmrc` | `min-release-age` | dagen | npm 11.10+ |
-| `~/.npmrc` | `minimum-release-age` | minuten | pnpm 10.16+ |
-| `~/.bunfig.toml` | `[install] minimumReleaseAge` | seconden | bun 1.3+ |
+## Status check
-Bestaande inhoud (auth tokens, registries, custom keys) en file-mode blijven
-behouden. Voor een spoedige CVE-fix die binnen het venster valt: per-project
-override via een lokale `.npmrc` / `bunfig.toml` met de waarde op `0`.
+```bash
+sudo bash check.sh
+```
-### Per-project + CI
+Toont services / timers / signatures / laatste scans. Exit-code is gelijk aan het aantal gevonden problemen (capped op 2), zodat het in cron of CI gebruikt kan worden als gezondheids-probe.
-`~/.npmrc` dekt alleen je eigen workstation. CI-runners draaien als een
-andere user zonder dit home-config, dus de cooldown is daar bypassed —
-precies waar supply-chain attacks in production builds landen. Drop
-daarom een **project-lokale** config in elke Node/Bun repo die je owned:
+## Handmatige scan + update
```bash
-cp common/templates/project-npmrc.example /.npmrc
-cp common/templates/project-bunfig.toml.example /bunfig.toml
-# committen — beide files bevatten geen secrets
+sudo clamscan -r /home --infected --log=/var/log/clamav/manual-scan.log
+sudo rkhunter --check --skip-keypress
+sudo bash common/update.sh # signatures + rkhunter db
```
-Voor projecten waar je geen file mag committen (gedeeld met teams die
-deze opinie niet delen): in plaats daarvan CI env vars zetten —
-`NPM_CONFIG_MIN_RELEASE_AGE=7` en `NPM_CONFIG_MINIMUM_RELEASE_AGE=10080`.
-Zie `common/templates/README.md` voor de volledige uitleg.
+## Incident response — uitgebreid
-## Handmatige update
+Het IR-script (`common/incident-token-revoke.sh`) vangt eerst de huidige `gh`-token op (hash + last-4) voor latere verify, detecteert IOC's + heuristisch, kill't polling-processen met **SIGKILL voordat** systemd er een SIGTERM op stuurt (een TERM-trap in de payload kan namelijk alsnog `rm -rf ~/` triggeren), archiveert artefacten naar `/tmp/incident-/` (overleeft `rm -rf ~/`), maakt de token op deze machine onbruikbaar, en wacht op handmatige revoke op github.com/settings/tokens (er is geen user-self-revoke REST endpoint).
+
+### Optionele mail-rapportage
+
+Standaard blijft alles lokaal. Voor een SMTPS-mail (bv. Gmail) na afloop:
```bash
-sudo bash common/update.sh
+mkdir -p ~/.config/workstation-security
+cp common/incident-token-revoke.env.example ~/.config/workstation-security/mail.env
+chmod 600 ~/.config/workstation-security/mail.env
+$EDITOR ~/.config/workstation-security/mail.env
```
+Het script weigert te mailen als die file niet op mode 600/400 staat (bevat een App Password).
+
## WSL Support
-De installer + checks zijn WSL-aware. Een WSL2-Ubuntu draait de gewone
-`bootstrap.sh` → `ubuntu/install.sh`-flow; WSL2-Alma idem voor `alma/`.
-Wat afwijkt op WSL:
+De installer + checks zijn WSL-aware. Een WSL2-Ubuntu draait de gewone `bootstrap.sh` → `ubuntu/install.sh`-flow; WSL2-Alma idem voor `alma/`. Wat afwijkt op WSL:
| Component | Native Linux | WSL zonder systemd | WSL met systemd-opt-in |
|---|---|---|---|
@@ -218,15 +151,7 @@ Wat afwijkt op WSL:
| `incident-token-revoke.sh` Linux-side | ✓ | ✓ + Windows-warning | ✓ + Windows-warning |
| `incident-token-revoke.sh` clipboard/URL-open | wl-copy/xclip/pbcopy | clip.exe + wslview/cmd.exe | clip.exe + wslview/cmd.exe |
-**Waarom rkhunter op WSL bewust uit?** Op WSL geeft `rkhunter --check` veel
-false-positives (proc-checks, passwd-checks rond WSL's init-proces, en
-`system_configs.t` verwacht echte init-scripts). Een daily wall-melding
-met onbetrouwbare waarschuwingen leidt tot alarm-fatigue, wat op zichzelf
-al een ISO 27001-bevinding is ("medewerkers negeren security-alerts").
-WSL's container-achtige isolatie verandert sowieso het rootkit-bedreigings-
-model — de Windows-host is daar de relevante verdedigingslaag (Defender /
-EDR). `rkhunter-check.sh` detecteert WSL en exit 0 met uitleg; de timer
-fired wel maar produceert geen wall-spam.
+**Waarom rkhunter op WSL bewust uit?** Op WSL geeft `rkhunter --check` veel false-positives (proc-checks, passwd-checks rond WSL's init-proces, en `system_configs.t` verwacht echte init-scripts). Een daily wall-melding met onbetrouwbare waarschuwingen leidt tot alarm-fatigue, wat op zichzelf al een ISO 27001-bevinding is ("medewerkers negeren security-alerts"). WSL's container-achtige isolatie verandert sowieso het rootkit-bedreigings-model — de Windows-host is daar de relevante verdedigingslaag (Defender / EDR). `rkhunter-check.sh` detecteert WSL en exit 0 met uitleg; de timer fired wel maar produceert geen wall-spam.
### WSL2 + systemd inschakelen (aanbevolen)
@@ -261,11 +186,7 @@ Alles werkt behalve de timers. Voor automatische scans heb je twee opties:
### Incident response op WSL — scope-limit
-`incident-token-revoke.sh` detecteert WSL en print bij start een
-waarschuwing dat **persistence op de Windows-host** (Task Scheduler, HKCU
-Run-keys, startup folder) niet door dit script wordt gezien. Voor een
-volledige IR op WSL controleer je óók Windows-kant — het script print de
-exacte PowerShell + reg-commands die je daar moet draaien.
+`incident-token-revoke.sh` detecteert WSL en print bij start een waarschuwing dat **persistence op de Windows-host** (Task Scheduler, HKCU Run-keys, startup folder) niet door dit script wordt gezien. Voor een volledige IR op WSL controleer je óók Windows-kant — het script print de exacte PowerShell + reg-commands die je daar moet draaien.
## Verwijderen
@@ -274,3 +195,21 @@ sudo bash common/uninstall.sh
```
Dit verwijdert de systemd timers en logrotate config. ClamAV en rkhunter packages blijven staan — verwijder die handmatig als gewenst.
+
+## Verder lezen
+
+| Doc | Voor wie |
+|-----|----------|
+| [`docs/compliance.md`](docs/compliance.md) | Auditor, security officer, sales-ondersteuning — control-mapping naar ISO 27001 / SOC 2 / NEN 7510 / BIO. |
+| [`docs/threat-model.md`](docs/threat-model.md) | Implementatie-engineers, security-reviewers — wat we wel/niet verdedigen. |
+| [`docs/supply-chain-cooldown.md`](docs/supply-chain-cooldown.md) | Devs die alleen laag 2 willen begrijpen of adopteren. |
+| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Iedereen die een PR overweegt. |
+| [`openspec/`](openspec/) | Spec-driven changes — `proposal.md` + `tasks.md` + spec-deltas per change. |
+
+## License
+
+[EUPL-1.2](LICENSE) — European Union Public Licence v. 1.2.
+
+EUPL is OSI-approved, vrije software-licentie en breed gedragen in Europese publieke-sector projecten. De keuze is bewust: deze repo komt voort uit werk rond digitale soevereiniteit en NLnet-gesponsorde projecten, waar EUPL het natuurlijke pad is voor permissive sharing zonder afhankelijkheid van een US-juridisch kader. Compatible met de meeste copyleft- en permissive licenties (inclusief GPL, AGPL, MIT en Apache-2.0) voor afgeleide werken.
+
+SPDX-headers in elke source-file (`# SPDX-License-Identifier: EUPL-1.2`) maken de licentie machine-leesbaar voor SBOM-tools.
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..ac39a10
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.9.0
diff --git a/alma/install.sh b/alma/install.sh
index e696fda..1142e55 100755
--- a/alma/install.sh
+++ b/alma/install.sh
@@ -6,21 +6,38 @@
# Style-afwijking: shebang via `env bash` voor consistentie met repo.
#
# Usage:
-# sudo bash alma/install.sh # dnf install ClamAV + rkhunter, enable services, install timers
+# sudo bash alma/install.sh # dnf install ClamAV + rkhunter, enable services, install timers
+# bash alma/install.sh --dry-run # print zou-uitgevoerd-zijn commando's, geen wijzigingen
+# bash alma/install.sh --version # print versie en exit
set -euo pipefail
# shellcheck source=/dev/null
source "$(dirname "$0")/../common/install-base.sh"
+ws_handle_version "$@"
+for arg in "$@"; do
+ case "$arg" in
+ --dry-run) export WS_DRY_RUN=1 ;;
+ *)
+ echo "error: onbekend argument: $arg" >&2
+ echo " Geldige flags: --dry-run, --version/-V" >&2
+ exit 2
+ ;;
+ esac
+done
+
require_root "alma/install.sh"
clamav_ok=0
rkhunter_ok=0
echo "==> Packages installeren..."
-dnf install -y epel-release
-if dnf install -y clamav clamd clamav-update; then
+ws_run_or_print dnf install -y epel-release
+if ws_is_dry_run; then
+ ws_run_or_print dnf install -y clamav clamd clamav-update
+ clamav_ok=1
+elif dnf install -y clamav clamd clamav-update; then
clamav_ok=1
else
echo " FOUT: ClamAV installatie mislukt." >&2
@@ -28,9 +45,9 @@ else
fi
echo "==> ClamAV configureren..."
-sed -i 's/^Example/#Example/' /etc/clamd.d/scan.conf
-sed -i 's/^#LocalSocket /LocalSocket /' /etc/clamd.d/scan.conf
-sed -i 's/^Example/#Example/' /etc/freshclam.conf
+ws_run_or_print sed -i 's/^Example/#Example/' /etc/clamd.d/scan.conf
+ws_run_or_print sed -i 's/^#LocalSocket /LocalSocket /' /etc/clamd.d/scan.conf
+ws_run_or_print sed -i 's/^Example/#Example/' /etc/freshclam.conf
echo "==> Signatures downloaden..."
freshclam_safe
@@ -39,14 +56,20 @@ echo "==> SELinux boolean voor /home scans..."
# Zonder deze boolean blokkeert SELinux clamscan (antivirus_exec_t) op /home,
# resultaat: scan eindigt met 0 dirs / 0 files / status=2 INVALIDARGUMENT.
if command -v setsebool >/dev/null 2>&1; then
- setsebool -P antivirus_can_scan_system 1
+ ws_run_or_print setsebool -P antivirus_can_scan_system 1
+elif ws_is_dry_run; then
+ echo " would run: setsebool -P antivirus_can_scan_system 1 (when SELinux beschikbaar)"
fi
echo "==> Services aanzetten..."
enable_clamav_services clamd@scan clamav-freshclam
echo "==> rkhunter installeren..."
-if dnf install -y rkhunter 2>/dev/null; then
+if ws_is_dry_run; then
+ ws_run_or_print dnf install -y rkhunter
+ rkhunter_init
+ rkhunter_ok=1
+elif dnf install -y rkhunter 2>/dev/null; then
rkhunter_init
rkhunter_ok=1
else
@@ -56,6 +79,12 @@ fi
echo "==> Timers installeren..."
install_timers
+if ws_is_dry_run; then
+ echo ""
+ echo "(dry-run; no changes made)"
+ exit 0
+fi
+
print_summary "$clamav_ok" "$rkhunter_ok" "dnf"
# Exit 0 als ClamAV OK — rkhunter is optioneel.
diff --git a/arch/install.sh b/arch/install.sh
index 1d2eb9b..f23f58c 100755
--- a/arch/install.sh
+++ b/arch/install.sh
@@ -6,13 +6,27 @@
# Style-afwijking: shebang via `env bash` voor consistentie met repo.
#
# Usage:
-# sudo bash arch/install.sh # pacman -S ClamAV + rkhunter, enable services, install timers
+# sudo bash arch/install.sh # pacman -S ClamAV + rkhunter, enable services, install timers
+# bash arch/install.sh --dry-run # print zou-uitgevoerd-zijn commando's, geen wijzigingen
+# bash arch/install.sh --version # print versie en exit
set -euo pipefail
# shellcheck source=/dev/null
source "$(dirname "$0")/../common/install-base.sh"
+ws_handle_version "$@"
+for arg in "$@"; do
+ case "$arg" in
+ --dry-run) export WS_DRY_RUN=1 ;;
+ *)
+ echo "error: onbekend argument: $arg" >&2
+ echo " Geldige flags: --dry-run, --version/-V" >&2
+ exit 2
+ ;;
+ esac
+done
+
require_root "arch/install.sh"
clamav_ok=0
@@ -23,7 +37,10 @@ echo "==> Packages installeren..."
# de package-database; geïnstalleerde packages blijven hun oude versie houden
# en verse dependencies matchen niet meer. Arch-community behandelt dit
# consistent als bug. Met --noconfirm gewoon meteen alles bijwerken.
-if pacman -Syu --noconfirm clamav; then
+if ws_is_dry_run; then
+ ws_run_or_print pacman -Syu --noconfirm clamav
+ clamav_ok=1
+elif pacman -Syu --noconfirm clamav; then
clamav_ok=1
else
echo " FOUT: ClamAV installatie mislukt." >&2
@@ -32,8 +49,8 @@ fi
# Quirk: pacman maakt /var/lib/clamav niet altijd aan met juiste eigenaar.
echo "==> ClamAV state-dir voorbereiden..."
-mkdir -p /var/lib/clamav
-chown clamav:clamav /var/lib/clamav
+ws_run_or_print mkdir -p /var/lib/clamav
+ws_run_or_print chown clamav:clamav /var/lib/clamav
echo "==> Signatures downloaden..."
freshclam_safe
@@ -42,7 +59,11 @@ echo "==> Services aanzetten..."
enable_clamav_services clamav-daemon clamav-freshclam
echo "==> rkhunter installeren..."
-if pacman -S --noconfirm rkhunter 2>/dev/null; then
+if ws_is_dry_run; then
+ ws_run_or_print pacman -S --noconfirm rkhunter
+ rkhunter_init
+ rkhunter_ok=1
+elif pacman -S --noconfirm rkhunter 2>/dev/null; then
# Quirk: rkhunter in Arch gebruikt deprecated egrep en geeft non-zero exit
# bij --update; --propupd is wel betrouwbaar. set -e tijdelijk uit om
# vroegtijdig exit te voorkomen.
@@ -57,6 +78,12 @@ fi
echo "==> Timers installeren..."
install_timers
+if ws_is_dry_run; then
+ echo ""
+ echo "(dry-run; no changes made)"
+ exit 0
+fi
+
print_summary "$clamav_ok" "$rkhunter_ok" "pacman"
exit 0
diff --git a/bootstrap.sh b/bootstrap.sh
index 9f223eb..92c4863 100755
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -8,11 +8,38 @@
# Style-afwijking: shebang via `env bash` voor consistentie met repo.
#
# Usage:
-# sudo bash bootstrap.sh # detecteert OS en dispatcht naar alma/arch/ubuntu install.sh
+# sudo bash bootstrap.sh # detecteert OS en dispatcht naar alma/arch/ubuntu install.sh
+# bash bootstrap.sh --dry-run # toon welke sub-installer aangeroepen zou worden, geen wijzigingen
+# bash bootstrap.sh --version # print versie en exit
set -euo pipefail
-if [[ $EUID -ne 0 ]]; then
+SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
+readonly SCRIPT_DIR
+
+# shellcheck source=common/lib.sh
+source "${SCRIPT_DIR}/common/lib.sh"
+
+# --version vóór de root-check zodat een gebruiker zonder sudo de versie kan
+# opvragen. ws_handle_version exit 0 als de flag aanwezig is.
+ws_handle_version "$@"
+
+# --dry-run parsing — accepteer flag, propageer via WS_DRY_RUN env-var zodat
+# sub-installers 'm ook zien (zie design.md D2). Geen andere argumenten verwacht.
+for arg in "$@"; do
+ case "$arg" in
+ --dry-run) export WS_DRY_RUN=1 ;;
+ *)
+ echo "error: onbekend argument: $arg" >&2
+ echo " Geldige flags: --dry-run, --version/-V" >&2
+ exit 2
+ ;;
+ esac
+done
+
+# Root-check overslaan in dry-run: een gebruiker wil de dispatch-keuze ook
+# zonder sudo kunnen voorspellen (CI / audit-evidence).
+if ! ws_is_dry_run && [[ $EUID -ne 0 ]]; then
echo "Run als root: sudo bash bootstrap.sh" >&2
exit 1
fi
@@ -27,11 +54,9 @@ fi
# shellcheck source=/dev/null
source /etc/os-release
-dir="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
-
dispatch() {
local sub="$1"
- local installer="$dir/$sub/install.sh"
+ local installer="${SCRIPT_DIR}/$sub/install.sh"
# Toets alleen op bestaan — de installer wordt via `bash "$installer"`
# aangeroepen en hoeft niet executable te zijn. De vorige check
# ([[ ! -x && ! -f ]]) was tautologisch: een executable bestand is per
@@ -40,6 +65,11 @@ dispatch() {
echo "error: installer ontbreekt: $installer" >&2
exit 2
fi
+ if ws_is_dry_run; then
+ echo "Would dispatch to: $sub/install.sh"
+ echo "(dry-run; no changes made)"
+ return 0
+ fi
echo "==> ${PRETTY_NAME:-${ID:-onbekend}} → $sub/install.sh"
bash "$installer"
}
diff --git a/check.sh b/check.sh
index 2ffefee..b565c94 100755
--- a/check.sh
+++ b/check.sh
@@ -17,6 +17,8 @@ readonly SCRIPT_DIR
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/common/lib.sh"
+ws_handle_version "$@"
+
errors=0
echo ""
diff --git a/common/incident-token-revoke.sh b/common/incident-token-revoke.sh
index 4097d94..d128b65 100644
--- a/common/incident-token-revoke.sh
+++ b/common/incident-token-revoke.sh
@@ -39,10 +39,22 @@
set -uo pipefail
readonly SCRIPT_NAME="incident-token-revoke"
-readonly SCRIPT_VERSION="0.1.0"
readonly TOKENS_URL="https://github.com/settings/tokens"
readonly MAX_EVIDENCE_BYTES=1048576 # 1 MiB cap per evidence-file
+# Self-contained version reader. Het IR-script vermijdt bewust een lib.sh-source
+# (zie comment in dit bestand bij WSL-detectie: "Inline detectie — IR-script
+# blijft self-contained"). Zelfde principe hier — bij ontbrekend VERSION-bestand
+# fallt het terug op "unknown" en draait gewoon door.
+_ir_version_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../VERSION"
+if [[ -r "$_ir_version_file" ]]; then
+ SCRIPT_VERSION="$(cat "$_ir_version_file")"
+else
+ SCRIPT_VERSION="unknown"
+fi
+unset _ir_version_file
+readonly SCRIPT_VERSION
+
INCIDENT_TS="$(date -u +%Y%m%dT%H%M%SZ)"
readonly INCIDENT_TS
readonly INCIDENT_DIR="/tmp/incident-${INCIDENT_TS}"
@@ -118,6 +130,10 @@ while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--yes-neutralize) AUTO_YES_NEUTRALIZE=1 ;;
+ --version | -V)
+ echo "workstation-security ${SCRIPT_VERSION}"
+ exit 0
+ ;;
--mail-env)
shift
[[ $# -gt 0 ]] || {
diff --git a/common/install-base.sh b/common/install-base.sh
index 34db57f..7078942 100644
--- a/common/install-base.sh
+++ b/common/install-base.sh
@@ -28,8 +28,13 @@ readonly WS_BASE_DIR
source "${WS_BASE_DIR}/lib.sh"
# Vereist root; toont begeleidende sudo-hint met het pad van de caller.
+# Dry-run: skip de check — een gebruiker wil ook zonder sudo kunnen voorspellen
+# wat de installer zou doen (CI / audit-evidence).
require_root() {
local caller_hint="${1:-install.sh}"
+ if ws_is_dry_run; then
+ return 0
+ fi
if [[ $EUID -ne 0 ]]; then
echo "Run als root: sudo bash ${caller_hint}" >&2
exit 1
@@ -39,6 +44,11 @@ require_root() {
# freshclam draait niet als de daemon de log-lock vasthoudt — stop hem eerst.
# Best-effort: faalt zonder error als de service niet bestaat.
freshclam_safe() {
+ if ws_is_dry_run; then
+ ws_run_or_print systemctl stop clamav-freshclam
+ ws_run_or_print freshclam
+ return 0
+ fi
systemctl stop clamav-freshclam 2>/dev/null || true
freshclam
}
@@ -47,8 +57,8 @@ freshclam_safe() {
# distro-specifieke quirk een set +e/-e-wrapper nodig heeft (Arch ships een
# rkhunter die op deprecated egrep een non-zero terugkomt — zie arch/install.sh).
rkhunter_init() {
- rkhunter --update
- rkhunter --propupd
+ ws_run_or_print rkhunter --update
+ ws_run_or_print rkhunter --propupd
}
# Enable + start een lijst van clamav-gerelateerde services.
@@ -56,6 +66,13 @@ rkhunter_init() {
# door kan gaan (packages staan al; daemon-runtime is optioneel — handmatige
# scans blijven mogelijk). Aansluiting op de gate in install-timers.sh.
enable_clamav_services() {
+ if ws_is_dry_run; then
+ local svc
+ for svc in "$@"; do
+ printf ' would run: systemctl enable --now %s\n' "$svc"
+ done
+ return 0
+ fi
if ! ws_systemd_available; then
if ws_is_wsl; then
ws_warn "WSL zonder actieve systemd — ClamAV daemons niet enable'd."
diff --git a/common/install-pm-cooldown.sh b/common/install-pm-cooldown.sh
index 2292f09..15b1255 100644
--- a/common/install-pm-cooldown.sh
+++ b/common/install-pm-cooldown.sh
@@ -25,9 +25,17 @@
# bash common/install-pm-cooldown.sh # default 7 dagen
# bash common/install-pm-cooldown.sh --days 14 # custom window
# bash common/install-pm-cooldown.sh --check # alleen huidige state tonen
+# bash common/install-pm-cooldown.sh --dry-run # print zou-toegepast-zijn wijzigingen, geen schrijven
+# bash common/install-pm-cooldown.sh --version # print versie en exit
set -euo pipefail
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+readonly SCRIPT_DIR
+
+# shellcheck source=lib.sh
+source "${SCRIPT_DIR}/lib.sh"
+
readonly NPMRC="${HOME}/.npmrc"
readonly BUNFIG="${HOME}/.bunfig.toml"
@@ -36,14 +44,16 @@ mode="install"
usage() {
cat <<'EOF'
-Usage: install-pm-cooldown.sh [--days N] [--check] [-h]
+Usage: install-pm-cooldown.sh [--days N] [--check] [--dry-run] [-h]
Installeert N-daagse package-manager cooldown voor npm, pnpm en bun.
Opties:
- --days N Cooldown-venster in dagen (default 7, min 1).
- --check Toon alleen huidige cooldown-staat, wijzig niets.
- -h, --help Deze tekst.
+ --days N Cooldown-venster in dagen (default 7, min 1).
+ --check Toon alleen huidige cooldown-staat, wijzig niets.
+ --dry-run Print de config-wijzigingen die zouden plaatsvinden, raak files niet aan.
+ --version, -V Print versie en exit.
+ -h, --help Deze tekst.
Files: ~/.npmrc en ~/.bunfig.toml (bestaande inhoud blijft behouden).
EOF
@@ -61,6 +71,11 @@ parse_args() {
days="$1"
;;
--check) mode="check" ;;
+ --dry-run) export WS_DRY_RUN=1 ;;
+ --version | -V)
+ echo "workstation-security $(ws_version)"
+ exit 0
+ ;;
-h | --help)
usage
exit 0
@@ -202,6 +217,19 @@ main() {
local pnpm_minutes=$((days * 24 * 60))
local bun_seconds=$((days * 24 * 60 * 60))
+ if ws_is_dry_run; then
+ echo "Would install ${days}-day package-manager cooldown:"
+ echo " ${NPMRC}: would upsert min-release-age=${npm_days} (npm)"
+ echo " ${NPMRC}: would upsert minimum-release-age=${pnpm_minutes} (pnpm, minuten)"
+ echo " ${BUNFIG}: would upsert [install] minimumReleaseAge=${bun_seconds} (bun, seconden)"
+ echo ""
+ echo "Huidige staat ter referentie:"
+ check_only
+ echo ""
+ echo "(dry-run; no changes made)"
+ exit 0
+ fi
+
echo "Installing ${days}-day package-manager cooldown..."
upsert_kv "$NPMRC" "min-release-age" "$npm_days"
diff --git a/common/install-shell-tools.sh b/common/install-shell-tools.sh
index 1e76246..57bb780 100755
--- a/common/install-shell-tools.sh
+++ b/common/install-shell-tools.sh
@@ -65,6 +65,10 @@ while [[ $# -gt 0 ]]; do
only_tool="$2"
shift 2
;;
+ --version | -V)
+ echo "workstation-security $(ws_version)"
+ exit 0
+ ;;
-h | --help)
sed -n '2,30p' "$0" | sed 's/^# //;s/^#$//'
exit 0
diff --git a/common/install-timers.sh b/common/install-timers.sh
index 5d59932..72fe845 100644
--- a/common/install-timers.sh
+++ b/common/install-timers.sh
@@ -7,7 +7,9 @@
# met WS_TIMERS / WS_SERVICES_GENERATED in common/lib.sh (smoke-test onderaan).
#
# Usage:
-# sudo bash common/install-timers.sh # schrijft unit files naar /etc/systemd/system en enable't timers
+# sudo bash common/install-timers.sh # schrijft unit files en enable't timers
+# bash common/install-timers.sh --dry-run # print unit-file inhoud naar stdout, geen wijzigingen
+# bash common/install-timers.sh --version # print versie en exit
# Style-afwijking: shebang via `env bash` i.p.v. `/bin/bash` — repo target
# o.a. macOS; rest van repo gebruikt al `env bash`.
set -euo pipefail
@@ -19,12 +21,37 @@ readonly UNIT_DIR="/etc/systemd/system"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/lib.sh"
+ws_handle_version "$@"
+for arg in "$@"; do
+ case "$arg" in
+ --dry-run) export WS_DRY_RUN=1 ;;
+ *)
+ echo "error: onbekend argument: $arg" >&2
+ echo " Geldige flags: --dry-run, --version/-V" >&2
+ exit 2
+ ;;
+ esac
+done
+
+# In dry-run: schrijf unit-inhoud naar stdout in plaats van naar /etc/systemd/system.
+# Functie wrapt `cat >file <"$target"
+ fi
+}
+
# --- WSL / systemd gate ---
# install-timers vereist systemd. WSL1 heeft het niet; WSL2 alleen na opt-in
# via /etc/wsl.conf ([boot] systemd=true) + 'wsl --shutdown'. Zonder systemd
# kunnen we de timers niet enable'n — warn + exit cleanly zodat de
# bootstrap-flow niet faalt op iets dat handmatig op te lossen is.
-if ! ws_systemd_available; then
+if ! ws_is_dry_run && ! ws_systemd_available; then
if ws_is_wsl; then
ws_warn "WSL gedetecteerd zonder actieve systemd-runtime."
ws_info "Om timers te activeren:"
@@ -45,7 +72,7 @@ fi
# --- Dagelijkse signature/database update ---
-cat >"$UNIT_DIR/av-update.service" <"$UNIT_DIR/av-update.timer" <<'UNIT'
+ws_write_unit "$UNIT_DIR/av-update.timer" <<'UNIT'
[Unit]
Description=Dagelijkse AV signature update (04:00)
@@ -68,7 +95,7 @@ UNIT
# --- Dagelijkse ClamAV scan ---
-cat >"$UNIT_DIR/clamav-scan.service" <"$UNIT_DIR/clamav-scan.timer" <<'UNIT'
+ws_write_unit "$UNIT_DIR/clamav-scan.timer" <<'UNIT'
[Unit]
Description=Dagelijkse ClamAV scan (02:00)
@@ -92,7 +119,7 @@ UNIT
# --- Dagelijkse rkhunter check ---
-cat >"$UNIT_DIR/rkhunter-check.service" <"$UNIT_DIR/rkhunter-check.timer" <<'UNIT'
+ws_write_unit "$UNIT_DIR/rkhunter-check.timer" <<'UNIT'
[Unit]
Description=Dagelijkse rkhunter check (03:00)
@@ -113,20 +140,39 @@ Persistent=true
WantedBy=timers.target
UNIT
-mkdir -p /var/log/clamav
+ws_run_or_print mkdir -p /var/log/clamav
# --- Logrotate ---
-cp "$SCRIPT_DIR/logrotate.conf" /etc/logrotate.d/workstation-security
+if ws_is_dry_run; then
+ echo ""
+ echo " would copy: ${SCRIPT_DIR}/logrotate.conf → /etc/logrotate.d/workstation-security"
+else
+ cp "$SCRIPT_DIR/logrotate.conf" /etc/logrotate.d/workstation-security
+fi
# --- Drift-smoke-test: zijn alle WS_TIMERS heredocs ook daadwerkelijk geschreven? ---
-for unit in "${WS_TIMERS[@]}" "${WS_SERVICES_GENERATED[@]}"; do
- if [[ ! -f "$UNIT_DIR/$unit" ]]; then
- echo "error: lib.sh noemt $unit maar het is niet door dit script geschreven —" >&2
- echo " drift tussen WS_TIMERS/WS_SERVICES_GENERATED en de heredocs." >&2
- exit 2
- fi
-done
+# In dry-run schrijft niemand naar disk, dus skip de drift-check (zou false-fail'en).
+if ! ws_is_dry_run; then
+ for unit in "${WS_TIMERS[@]}" "${WS_SERVICES_GENERATED[@]}"; do
+ if [[ ! -f "$UNIT_DIR/$unit" ]]; then
+ echo "error: lib.sh noemt $unit maar het is niet door dit script geschreven —" >&2
+ echo " drift tussen WS_TIMERS/WS_SERVICES_GENERATED en de heredocs." >&2
+ exit 2
+ fi
+ done
+fi
+
+if ws_is_dry_run; then
+ echo ""
+ for timer in "${WS_TIMERS[@]}"; do
+ echo " would run: systemctl enable --now $timer"
+ done
+ echo " would run: systemctl daemon-reload"
+ echo ""
+ echo "(dry-run; no changes made)"
+ exit 0
+fi
systemctl daemon-reload
for timer in "${WS_TIMERS[@]}"; do
diff --git a/common/lib.sh b/common/lib.sh
index 402759e..f6261fe 100644
--- a/common/lib.sh
+++ b/common/lib.sh
@@ -25,6 +25,11 @@ if [[ -n "${WS_LIB_SOURCED:-}" ]]; then
fi
readonly WS_LIB_SOURCED=1
+# Pad naar deze library — gebruikt door ws_version() om VERSION te lokaliseren
+# relatief aan de repo-root, ongeacht waar de caller vandaan source't.
+WS_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+readonly WS_LIB_DIR
+
# --- status-iconen (repo-breed) ---
readonly WS_PASS="✓"
readonly WS_FAIL="✗"
@@ -62,6 +67,26 @@ ws_warn() { printf ' %s %s\n' "$WS_WARN" "$*" >&2; }
ws_skip() { printf ' %s %s\n' "$WS_SKIP" "$*"; }
ws_info() { printf ' %s\n' "$*"; }
+# --- dry-run ---
+# Eén bron van waarheid voor dry-run-modus. Installers checken via
+# ws_is_dry_run(); arg-parsing zet `WS_DRY_RUN=1` (en export't 'm) zodat de flag
+# automatisch propageert naar sub-installers (zie design.md D2). Default 0.
+ws_is_dry_run() {
+ [[ "${WS_DRY_RUN:-0}" == "1" ]]
+}
+
+# ws_run_or_print: voer een commando uit, of print het in dry-run-modus zonder
+# side effects. Werkt voor simpele exec-vorm (geen pipes/redirects); voor
+# heredocs en redirects moet de caller zelf branchen op ws_is_dry_run.
+# Output-prefix " would run: " is consistent over installers heen voor copy-paste.
+ws_run_or_print() {
+ if ws_is_dry_run; then
+ printf ' would run: %s\n' "$*"
+ return 0
+ fi
+ "$@"
+}
+
# --- runtime detectors (WSL + systemd) ---
# WSL detecteert via /proc/sys/kernel/osrelease — bevat 'microsoft' (WSL1+2)
# of 'WSL' (WSL2-kernel-versie-suffix). Beide patterns gevangen.
@@ -69,6 +94,35 @@ ws_is_wsl() {
grep -qiE 'microsoft|wsl' /proc/sys/kernel/osrelease 2>/dev/null
}
+# --- versioning ---
+# ws_version: print de inhoud van top-level VERSION-file (zonder trailing
+# newline issue dankzij command-substitution-stripping in callers). Fallback
+# 'unknown' wanneer de file ontbreekt of niet leesbaar is — zodat een script
+# dat los gedownload werd niet faalt op een ontbrekend VERSION-bestand.
+ws_version() {
+ local version_file="${WS_LIB_DIR}/../VERSION"
+ if [[ -r "$version_file" ]]; then
+ cat "$version_file"
+ else
+ echo "unknown"
+ fi
+}
+
+# ws_handle_version: scan "$@" voor --version of -V en exit 0 met een
+# version-string als die aanwezig zijn. Aanroepen vóór andere arg-parsing zodat
+# --version altijd voorrang heeft (ook gecombineerd met andere flags).
+ws_handle_version() {
+ local arg
+ for arg in "$@"; do
+ case "$arg" in
+ --version | -V)
+ echo "workstation-security $(ws_version)"
+ exit 0
+ ;;
+ esac
+ done
+}
+
# Heeft systemd als init? Twee checks:
# 1. /run/systemd/system bestaat (alleen wanneer systemd booted heeft)
# 2. PID 1 is systemd (vs. wsl-init, sysvinit, etc.)
diff --git a/common/rkhunter-check.sh b/common/rkhunter-check.sh
index be99473..2d29f4e 100644
--- a/common/rkhunter-check.sh
+++ b/common/rkhunter-check.sh
@@ -18,6 +18,8 @@ readonly SCRIPT_DIR
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
+ws_handle_version "$@"
+
LOG="/var/log/rkhunter.log"
# WSL-skip: rkhunter geeft hier veel false-positives op /proc-checks, op de
diff --git a/common/scan.sh b/common/scan.sh
index 0d6e6f6..6ce125e 100644
--- a/common/scan.sh
+++ b/common/scan.sh
@@ -18,6 +18,8 @@ readonly SCRIPT_DIR
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
+ws_handle_version "$@"
+
LOG="/var/log/clamav/daily-scan.log"
# WSL: sluit /mnt uit (Windows-drives via DrvFs). Native Linux: /mnt is
diff --git a/common/uninstall.sh b/common/uninstall.sh
index 5a8a733..2cf03ae 100644
--- a/common/uninstall.sh
+++ b/common/uninstall.sh
@@ -16,6 +16,8 @@ readonly SCRIPT_DIR
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/lib.sh"
+ws_handle_version "$@"
+
if [[ $EUID -ne 0 ]]; then
echo "Run als root: sudo bash common/uninstall.sh" >&2
exit 1
diff --git a/common/update.sh b/common/update.sh
index cc7835c..4634ef2 100644
--- a/common/update.sh
+++ b/common/update.sh
@@ -16,6 +16,8 @@ readonly SCRIPT_DIR
# shellcheck source=install-base.sh
source "${SCRIPT_DIR}/install-base.sh"
+ws_handle_version "$@"
+
echo "==> ClamAV signatures bijwerken..."
# freshclam_safe stopt eerst clamav-freshclam.service voordat het freshclam
# zelf draait — anders race't deze update-timer (04:00) tegen de active
diff --git a/docs/README.md b/docs/README.md
index e704226..cbce163 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -10,6 +10,7 @@ collega's die de tool overnemen, of jij over zes maanden.
|---|---|---|
| [`compliance.md`](compliance.md) | Auditor, security officer, sales-ondersteuning | Mapping van workstation-security componenten op specifieke control-IDs in ISO 27001:2022, SOC 2 (CC6/CC7), NEN 7510-2:2017 en BIO. Plus expliciete gap-lijst. |
| [`threat-model.md`](threat-model.md) | Implementatie-engineers, security-reviewers | Wat verdedigen we WEL, wat NIET. Voorkomt scope-creep en zet verwachtingen voor wat dit script kan oplossen. |
+| [`supply-chain-cooldown.md`](supply-chain-cooldown.md) | Devs, sec-engineers, lezers die de baseline niet adopteren maar wel deze laag willen begrijpen | Aanleiding, mechanisme per package-manager (npm / pnpm / bun), workstation-vs-project-vs-CI-scope, en override-flow voor urgente CVEs. Staat-op-zichzelf. |
## Hoe deze docs te gebruiken
diff --git a/docs/supply-chain-cooldown.md b/docs/supply-chain-cooldown.md
new file mode 100644
index 0000000..258a595
--- /dev/null
+++ b/docs/supply-chain-cooldown.md
@@ -0,0 +1,100 @@
+# Supply-chain cooldown (npm / pnpm / bun)
+
+Een 7-daagse quarantine op verse pakketversies. Standalone te lezen — je hoeft de rest van de workstation-security baseline niet te kennen om hier de waarde van in te schatten.
+
+## Waarom
+
+Op 2026-05-11 kwam er weer een nieuwe golf supply-chain-attacks via het npm-ecosysteem: kwaadaardige patch-versies van populaire pakketten, geüpload door gecompromitteerde maintainer-accounts. Het patroon is inmiddels routineus:
+
+1. Aanvaller compromitteert een maintainer-account (phishing, token-leak, social engineering).
+2. Aanvaller publiceert een nieuwe patch-versie met malicious code (typisch: post-install script dat secrets exfiltreert).
+3. Lockfiles met `^x.y.z` of `~x.y.z` constraints pakken de versie automatisch op tijdens de volgende `npm install` / `pnpm install` / `bun install`.
+4. Binnen 24-48u detecteert npm de malicious versie, yankt 'm uit het register, en publiceert een advisory.
+5. Iedereen die in stap 3 de versie installeerde is geraakt; iedereen die na stap 4 installeert niet.
+
+Een 7-daagse cooldown plaatst je categorisch in groep 2 (na stap 4). Geen pakketversie haalt je lockfile vóór 'ie zeven dagen oud is, dus malicious versies hebben tijd om gedetecteerd en geyankt te worden voordat ze jouw build raken. Het kost je actualiteit (je loopt zeven dagen achter op patches) in ruil voor een drastisch lager supply-chain-risico — een trade-off die voor de meeste workstation- en dev-workflows ruimschoots de moeite waard is.
+
+## Mechanisme per package-manager
+
+Drie tools, drie configuratiesleutels — wel allemaal met dezelfde semantiek (refuseren te installeren als de gepubliceerde versie jonger is dan de drempel):
+
+| Manager | File | Key | Eenheid | Minimum versie |
+|---------|----------------|--------------------------------|----------|-----------------|
+| npm | `~/.npmrc` | `min-release-age` | dagen | npm 11.10+ |
+| pnpm | `~/.npmrc` | `minimum-release-age` | minuten | pnpm 10.16+ |
+| bun | `~/.bunfig.toml` | `[install] minimumReleaseAge` | seconden | bun 1.3+ |
+
+`common/install-pm-cooldown.sh` schrijft alle drie tegelijk, idempotent. Bestaande inhoud (auth tokens, custom registries, andere keys) blijft staan. File-modus blijft 0600 als de file al bestond, en wordt 0600 voor nieuwe files — auth-tokens horen nooit world-readable.
+
+```bash
+bash common/install-pm-cooldown.sh # default 7 dagen
+bash common/install-pm-cooldown.sh --days 14 # andere drempel
+bash common/install-pm-cooldown.sh --check # alleen huidige state tonen
+bash common/install-pm-cooldown.sh --dry-run # toon wat het zou doen, geen wijzigingen
+```
+
+## Drie niveaus van scope: workstation, project, CI
+
+`~/.npmrc` en `~/.bunfig.toml` zijn user-level. Dat is goed voor je eigen interactieve gebruik op je dev-machine, maar levert een gat op:
+
+| Scope | Wie leest user-level config? | Effect op cooldown |
+|----------------|------------------------------|--------------------|
+| Jouw workstation, jij ingelogd | ja | actief |
+| Jouw workstation, andere user op dezelfde machine | nee | niet actief |
+| Docker build die als `node`-user draait | nee | niet actief |
+| CI-runner (GitHub Actions, GitLab CI, etc.) | nee | niet actief |
+
+De CI-runner is precies waar de aanvaller wil belanden — daar draait je productiebuild. Dus user-level alleen is niet voldoende.
+
+**Per-project oplossing.** Drop een `.npmrc` en `bunfig.toml` in elke Node/Bun repo die je owned:
+
+```bash
+cp common/templates/project-npmrc.example /.npmrc
+cp common/templates/project-bunfig.toml.example /bunfig.toml
+git add .npmrc bunfig.toml
+git commit -m "add: package-manager cooldown config"
+```
+
+Beide files bevatten geen secrets en horen in version control. Een CI-runner die je project checkt-out leest ze automatisch.
+
+**CI-only oplossing.** Voor projecten waar je geen file mag committen (gedeeld met teams die deze opinie niet delen), zet de cooldown in CI environment variables:
+
+```yaml
+# GitHub Actions:
+env:
+ NPM_CONFIG_MIN_RELEASE_AGE: 7
+ NPM_CONFIG_MINIMUM_RELEASE_AGE: 10080 # 7 * 24 * 60 minuten
+```
+
+Geen file-wijziging, geen PR-discussie, alleen een config-blok bij de jobs die `npm install` draaien. Zie `common/templates/README.md` voor de volledige tabel met varianten.
+
+## Override voor urgente CVEs binnen het venster
+
+Wat als er een echte security-patch valt binnen je 7-daagse venster? Bijvoorbeeld: `lodash` heeft een CVE, de fix is in `4.17.45` (gisteren gepubliceerd), je cooldown blokkeert 'm. Twee opties:
+
+**Per-install override** — alleen voor de duur van één install-commando. Bij npm en pnpm via een environment variable; bij bun via een lokale config-override:
+
+```bash
+# Tijdelijke override, alleen voor één install:
+NPM_CONFIG_MIN_RELEASE_AGE=0 npm install lodash@4.17.45
+```
+
+**Per-project override** — als het project deze CVE-fix permanent op `0` wil zetten tot de versie wel oud genoeg is. Zet in de project-lokale `.npmrc` de waarde op `0`, commit dat als hotfix, en revert wanneer de cooldown het pakket toch zou hebben binnengelaten. Audit-trail: de commit zelf is je bewijs dat de override bewust is gedaan.
+
+Beide overrides zijn opt-in. De default blijft dat een ongelezen `npm install` jouw cooldown respecteert.
+
+## Wat dekt dit NIET af
+
+- **Direct attacks via je IDE / editor extensions** — VSCode-extensies, JetBrains-plugins en Sublime-packages hebben hun eigen update-mechanismen. Daar werkt deze cooldown niet voor.
+- **Browser extensies, OS-packages, Docker base-images** — andere ecosystems, andere mitigations. Pin Docker images op SHA als je daar zorgen over hebt.
+- **Compromised npm-registry zelf** — de cooldown vertrouwt op een functionerend yank-mechanisme. Een aanvaller met registry-control kan in theorie geyankte versies "ondoenken".
+- **Pre-existing malicious versies > N dagen oud** — wat al meer dan 7 dagen circuleert valt buiten de quarantine. De cooldown is een tijds-filter, geen integriteits-check.
+
+Voor het bedreigingsmodel waar deze tool wel/niet voor verdedigt: zie [`threat-model.md`](threat-model.md).
+
+## Zie ook
+
+- `common/install-pm-cooldown.sh` — installer-script.
+- `common/templates/project-npmrc.example`, `common/templates/project-bunfig.toml.example` — per-project templates.
+- `common/templates/README.md` — overzicht van wanneer welke template, plus CI env-var-tabel.
+- Aanleiding: het npm supply-chain incident van 2026-05-11 (zie ook eerdere incidenten in 2024 en 2025).
diff --git a/openspec/changes/v1-release-readiness/.openspec.yaml b/openspec/changes/v1-release-readiness/.openspec.yaml
new file mode 100644
index 0000000..231e3ab
--- /dev/null
+++ b/openspec/changes/v1-release-readiness/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-05-18
diff --git a/openspec/changes/v1-release-readiness/README.md b/openspec/changes/v1-release-readiness/README.md
new file mode 100644
index 0000000..b7c6348
--- /dev/null
+++ b/openspec/changes/v1-release-readiness/README.md
@@ -0,0 +1,3 @@
+# v1-release-readiness
+
+Bundle A2 + C1-C6: cooldown doc split, versioning, CI matrix, repo hygiene, README restructure, dry-run, v1.0.0 release
diff --git a/openspec/changes/v1-release-readiness/design.md b/openspec/changes/v1-release-readiness/design.md
new file mode 100644
index 0000000..eb5cc0f
--- /dev/null
+++ b/openspec/changes/v1-release-readiness/design.md
@@ -0,0 +1,96 @@
+## Context
+
+Deze change bundelt zeven kleine clusters (A2 + C1-C6) richting een v1.0.0-tag. De meeste keuzes zijn vanzelfsprekend (toevoegen van een LICENSE-file, README-restructuring, issue-templates). Een paar zijn dat niet, en die leggen we hier vast — een auditor of latere lezer moet kunnen reconstrueren *waarom* deze variant en niet de net-zo-redelijke andere.
+
+Leidend principe: kies de saaiste, audit-best-verdedigbare, KISS-conforme variant die breed gedragen wordt door de community. Wanneer twee opties allebei redelijk zijn, kies degene die het minst verbergt voor een lezer die de repo voor het eerst opent.
+
+## Goals / Non-Goals
+
+**Goals:**
+- v1.0.0 kunnen taggen zonder dat een externe lezer hoeft te raden naar license, status, of "wat is dit eigenlijk".
+- CI als objectief vertrouwen-signaal (badge groen of niet).
+- Installers veilig kunnen draaien in CI / audit-evidence-flows zonder root of system state te raken.
+
+**Non-Goals:**
+- Geen scope-uitbreiding naar centrale EDR / log-aggregatie / fleet management — blijft expliciet buiten v1.0.0 conform `docs/threat-model.md`.
+- Geen build-systeem of release-automatie naast wat GitHub Actions van zichzelf biedt; geen semantic-release-bot, geen changelog-generator.
+- Geen wijziging aan de drie verdedigingslagen zelf (AV, cooldown, IR) — alleen aan de presentatie en runnability.
+
+## Decisions
+
+### D1. VERSION-file lezen at-runtime, niet embedden
+
+**Keuze:** `common/lib.sh` leest de top-level `VERSION`-file via `BASH_SOURCE` op het moment dat `ws_version()` aangeroepen wordt. Geen build-step, geen sed-substitutie bij release.
+
+**Alternatieven:**
+- *Build-time substitutie* (bv. `sed -i "s/__VERSION__/$(cat VERSION)/" *.sh` in een release-script): vereist een build-stap waar er nu geen is.
+- *Hard-coded constant in `lib.sh`*: extra plek om bij elke release te updaten; vergeet-risico.
+
+**Waarom deze:** boring, KISS, geen build-pijplijn nodig. Past bij hoe de repo nu werkt (`git clone && bash bootstrap.sh`). Auditable: één file, één regel, `cat VERSION` op de host bewijst de versie. De fallback `unknown` zorgt dat een gebruiker die alleen een individueel script downloadt (zonder repo-context) geen crash krijgt.
+
+### D2. `--dry-run` propageert via env-var **én** flag
+
+**Keuze:** `bootstrap.sh` zet `WS_DRY_RUN=1` in environment *en* respecteert `--dry-run` als argument. Sub-installers lezen beide.
+
+**Alternatieven:**
+- *Alleen flag forwarding* (`bash "$installer" --dry-run`): werkt, maar als een sub-installer een toekomstige derde wrapper aanroept moet die de flag opnieuw forwarden. Brittle bij groei.
+- *Alleen env-var*: minder ontdekbaar — een gebruiker die rechtstreeks `bash alma/install.sh` draait verwacht `--dry-run` als flag, niet een env-var.
+
+**Waarom deze:** beide-werkt is een breed gedragen patroon (`NO_COLOR`, `CI`, `DEBIAN_FRONTEND`, `MAKEFLAGS`). De flag is voor mensen, de env-var is voor scripts en dispatch-ketens. Auditor ziet in elk script één parse-block dat beide checkt — geen verstopte magie.
+
+### D3. Dry-run output gaat naar stdout, niet naar log of file
+
+**Keuze:** Wat een installer "zou doen" wordt naar stdout geprint in een vorm die direct copy-paste-baar is naar een echte run (commando per regel, geen banner-decoraties die je moet weglaten).
+
+**Alternatieven:**
+- *JSON/yaml structured output*: handig voor tools, maar overkill voor wat een mens of een audit-evidence-screenshot moet kunnen lezen.
+- *Schrijven naar `/tmp/dry-run-.log`*: extra cleanup; minder transparant in CI-logs.
+
+**Waarom deze:** een CI-runner laat de stdout-stream zien, een auditor copy-paste't 'm in de evidence-bundle. Plain text wins.
+
+### D4. CI: officiële Docker Hub images, rolling tag tot het breekt
+
+**Keuze:** `almalinux:9`, `ubuntu:24.04`, `archlinux:latest`. Pinnen pas wanneer een upstream-wijziging CI breekt zonder dat er aan de repo iets veranderd is.
+
+**Alternatieven:**
+- *Vooraf pinnen op SHA*: maximaal reproduceerbaar, maar elke upstream-patch betekent een chore-PR; in praktijk roest de pin.
+- *Alleen vendor-images vanuit een eigen registry*: complexer dan deze repo nodig heeft.
+
+**Waarom deze:** officiële images zijn de community-standaard en worden door de vendor zelf onderhouden. Rolling tags zijn boring (`actions/checkout@v4` doet hetzelfde). Wanneer Arch breekt is dat een concreet signaal — pin pas dan, met een comment die aangeeft waarom en wanneer.
+
+### D5. EUPL-1.2 als license, expliciet vastgelegd in `LICENSE`
+
+**Keuze:** Volledige EUPL-1.2-tekst in `LICENSE` (al impliciet aanwezig via SPDX-headers per script).
+
+**Alternatieven:**
+- *MIT / Apache-2.0*: dominanter in dev-tooling, makkelijker te scannen door SBOM-tools.
+- *Geen LICENSE-file, alleen SPDX-headers*: technisch geldig, maar GitHub toont dan "No license" wat externe gebruikers afschrikt.
+
+**Waarom deze:** consistent met de bestaande SPDX-headers (`# SPDX-License-Identifier: EUPL-1.2`). EUPL is OSI-approved, breed gedragen in Europese publieke-sector projecten, en past bij de NLnet/digitale-soevereiniteit-context van de auteur. Een `LICENSE`-file plus de motivatie in een `## License`-sectie onderaan de README is wat externe lezers verwachten.
+
+### D6. `--version` op user-facing CLIs, niet op interne library-scripts
+
+**Keuze:** `bootstrap.sh`, `check.sh`, en elke `common/*.sh` die als CLI bedoeld is (`install-pm-cooldown.sh`, `install-shell-tools.sh`, `incident-token-revoke.sh`, `scan.sh`, `rkhunter-check.sh`, `update.sh`, `uninstall.sh`). Niet op `common/lib.sh` (geen entrypoint) of `common/check-shell-headers.sh` (developer-tool, geen end-user CLI).
+
+**Alternatieven:**
+- *Overal*: scope-creep; `lib.sh` is geen entrypoint.
+- *Alleen op `bootstrap.sh`*: een gebruiker die direct een sub-script draait krijgt geen versie-handvat.
+
+**Waarom deze:** GNU-CLI-conventie volgt deze lijn (elke `bin/`-achtige tool krijgt `--version`; libraries niet).
+
+### D7. Single bundled change, geen splits
+
+**Keuze:** Alle zeven clusters in één OpenSpec change (`v1-release-readiness`).
+
+**Alternatieven:**
+- *Zeven aparte changes*: technisch netter qua granulariteit, maar elke change zou triviaal klein zijn en C6 hangt sowieso van A+B+C af.
+
+**Waarom deze:** consistent met de oorspronkelijke delta-beschrijving ("kleinere clusters samen"). Eén release, één coherent verhaal in de CHANGELOG. PRs kunnen alsnog per cluster gemerged worden — de spec hoeft daarvoor niet gesplitst.
+
+## Risks / Trade-offs
+
+- **Rolling-image breuk** — `archlinux:latest` kan CI breken zonder repo-wijziging (zie D4). Mitigatie: pin-policy in spec, follow-up issue om de pin op te ruimen.
+- **`VERSION` raakt out-of-sync met git tag** — een release-PR kan `VERSION` updaten zonder dat de tag volgt (of andersom). Mitigatie: C6 stap 6.4-6.5 expliciet sequentieel; eventueel later een lightweight pre-commit-check toevoegen, maar buiten v1.0.0-scope.
+- **`WS_DRY_RUN` leaks** — als een gebruiker `export WS_DRY_RUN=1` in zijn shell zet en het vergeet, draait elke installer dry-run zonder dat hij het ziet. Mitigatie: elke installer print bij dry-run een duidelijke banner (`(dry-run; no changes made)`) — onmogelijk te missen.
+- **EUPL-1.2 SBOM-coverage** — sommige SBOM-tools herkennen EUPL niet zo soepel als MIT/Apache. Geaccepteerd risico; SPDX-identifier maakt het machine-leesbaar genoeg.
+- **`--version` early-exit conflicteert nooit met andere flags** — geforceerd in de spec (D6, scenario "vóór andere flags"). Geen risico op verrassend gedrag.
diff --git a/openspec/changes/v1-release-readiness/proposal.md b/openspec/changes/v1-release-readiness/proposal.md
new file mode 100644
index 0000000..6ba5ddb
--- /dev/null
+++ b/openspec/changes/v1-release-readiness/proposal.md
@@ -0,0 +1,37 @@
+## Why
+
+De repo is in praktische zin compleet (drie verdedigingslagen werken, WSL-aware, audit-docs aanwezig), maar mist de signalen die een externe lezer in 60 seconden vertrouwen geven: een herkenbare licentie, CI-badges, een releasebare versie, en een duidelijke "wat is dit" bovenaan de README. Daarnaast is de supply-chain cooldown een uniek verkooppunt dat verstopt zit in sectie 2 van een drielagig verhaal, en hebben de installers geen dry-run-modus waardoor CI/audit-evidence-flows ze niet kunnen draaien.
+
+Deze change bundelt zeven kleinere clusters (A2 + C1-C6) tot één coherent v1.0.0-leveringspakket, omdat elk afzonderlijk te dun is voor een eigen change maar samen wel het v1.0.0-tag rechtvaardigen.
+
+## What Changes
+
+- **A2** — `docs/supply-chain-cooldown.md` toegevoegd; README-sectie 2 verkort tot 3-4 zinnen + link.
+- **C1** — `VERSION`-file als single source of truth; `ws_version()` in `common/lib.sh`; `--version`/`-V` flag op alle user-facing entrypoints (`bootstrap.sh`, `check.sh`, alle `common/*.sh`-CLIs).
+- **C2** — `.github/workflows/smoke.yml` met matrix (alma9, ubuntu2404, archlatest); CI-badges in README.
+- **C3+C4** — `LICENSE` (volledige EUPL-1.2), `CONTRIBUTING.md`, GitHub issue templates (bug / distro-support / config); README herstructureerd (intro → scope → drie lagen met eigen H2 → installatie → check → IR → WSL → docs); clone-URL gecorrigeerd naar `MWest2020/`.
+- **C5** — `--dry-run` flag op `bootstrap.sh`, `alma/install.sh`, `arch/install.sh`, `ubuntu/install.sh`, `common/install-timers.sh`, `common/install-pm-cooldown.sh`. Flag propageert via `WS_DRY_RUN=1` env-var. Geen side effects in dry-run; exit 0 bij happy path.
+- **C6** — `VERSION` → `1.0.0`, CHANGELOG-entry, git tag `v1.0.0`, GitHub Release, blogpost-draft. Pas uitvoeren als A+B+C-PR's gemerged en CI groen.
+
+Geen **BREAKING** wijzigingen: bestaande aanroepen blijven werken; `--version` / `--dry-run` zijn opt-in flags, `VERSION`-file ontbrekend levert "unknown" (geen exit 2).
+
+## Capabilities
+
+### New Capabilities
+
+- `versioning`: alle entrypoints rapporteren hun versie uit één bron van waarheid (`VERSION`-file), bereikbaar via `--version`/`-V` flag en een `ws_version()` helper.
+- `installer-dry-run`: installers ondersteunen `--dry-run` om in CI en audit-evidence-flows zonder side effects gedraaid te worden; flag propageert automatisch van `bootstrap.sh` naar sub-installers via `WS_DRY_RUN`.
+- `smoke-tests`: een CI-matrix draait op elke push/PR een `bootstrap.sh --dry-run` + `check.sh` smoke-test op de drie ondersteunde distrofamilies (alma, ubuntu, arch) zodat regressies in OS-detectie of installer-structuur direct zichtbaar zijn.
+
+### Modified Capabilities
+
+Geen — er bestaan nog geen specs in `openspec/specs/`; dit is de eerste change die capabilities introduceert.
+
+## Impact
+
+- **Code** — `common/lib.sh` (helper toegevoegd), zes script-entrypoints (flag-parsing toegevoegd), één nieuwe top-level file (`VERSION`).
+- **Docs** — README aanzienlijk herschreven; nieuwe `docs/supply-chain-cooldown.md`; `docs/README.md` index uitgebreid; `CONTRIBUTING.md` en `LICENSE` toegevoegd.
+- **CI** — eerste GitHub Actions workflows in deze repo; vereist `.github/workflows/` directory.
+- **Dependencies** — geen runtime-deps; CI gebruikt publieke Docker-images (`almalinux:9`, `ubuntu:24.04`, `archlinux:latest`).
+- **Externe surface** — clone-URL in README wijzigt van `conduction-it/` naar `MWest2020/`; lezers van eerdere README zien een 404 op de oude URL maar de repo zelf is openbaar onder de nieuwe org.
+- **Release** — eerste getagde versie van de repo (`v1.0.0`); na C6 hangt de blogpost en eventuele cross-posts hieraan.
diff --git a/openspec/changes/v1-release-readiness/specs/installer-dry-run/spec.md b/openspec/changes/v1-release-readiness/specs/installer-dry-run/spec.md
new file mode 100644
index 0000000..3e79b4a
--- /dev/null
+++ b/openspec/changes/v1-release-readiness/specs/installer-dry-run/spec.md
@@ -0,0 +1,31 @@
+## ADDED Requirements
+
+### Requirement: `--dry-run` op installer-entrypoints
+De installer-entrypoints (`bootstrap.sh`, `alma/install.sh`, `arch/install.sh`, `ubuntu/install.sh`, `common/install-timers.sh`, `common/install-pm-cooldown.sh`) SHALL `--dry-run` accepteren. Met die flag MUST ze geen side effects uitvoeren (geen pakket-installaties, geen schrijven naar `/etc/systemd/system/` of `~/.npmrc` of `~/.bunfig.toml`) en SHALL ze printen wat ze gedaan zouden hebben.
+
+#### Scenario: bootstrap.sh --dry-run
+- **WHEN** een gebruiker `sudo bash bootstrap.sh --dry-run` draait
+- **THEN** het script detecteert het OS via `/etc/os-release`, print `Would dispatch to: /install.sh` plus de regel `(dry-run; no changes made)`, en exit 0 — zonder de sub-installer aan te roepen
+
+#### Scenario: per-OS installer --dry-run
+- **WHEN** een gebruiker `sudo bash alma/install.sh --dry-run` (of arch/ubuntu equivalent) draait
+- **THEN** het script print de pakket-manager-commando's die het zou draaien (bv. `dnf install -y clamav rkhunter ...`) plus de install-timers-aanroep die zou volgen, en exit 0 — zonder pakketten te installeren of timers te schrijven
+
+#### Scenario: install-timers.sh --dry-run
+- **WHEN** `bash common/install-timers.sh --dry-run` draait
+- **THEN** het script print de inhoud van elk unit-file (service + timer) naar stdout in plaats van te schrijven naar `/etc/systemd/system/`, voert geen `systemctl daemon-reload` of `systemctl enable` uit, en exit 0
+
+#### Scenario: install-pm-cooldown.sh --dry-run
+- **WHEN** een gebruiker `bash common/install-pm-cooldown.sh --dry-run` draait
+- **THEN** het script print de config-wijzigingen die het zou toepassen op `~/.npmrc` en `~/.bunfig.toml`, raakt die files niet aan, en exit 0. Logica mag hergebruikt worden uit de bestaande `--check` mode
+
+### Requirement: Dry-run propagatie via env-var
+`bootstrap.sh` SHALL de dry-run-modus doorgeven aan zijn sub-installers via de env-var `WS_DRY_RUN=1`. Sub-installers MUST zowel de eigen `--dry-run` flag als `WS_DRY_RUN=1` uit de environment respecteren.
+
+#### Scenario: bootstrap propageert WS_DRY_RUN
+- **WHEN** `sudo bash bootstrap.sh --dry-run` draait op een Ubuntu-systeem
+- **THEN** als de dispatch tóch zou plaatsvinden (in de toekomstige variant waarin bootstrap delegeert in plaats van zelf te short-circuiten), `ubuntu/install.sh` ontvangt `WS_DRY_RUN=1` in zijn environment en gedraagt zich identiek aan `bash ubuntu/install.sh --dry-run`
+
+#### Scenario: Sub-installer leest WS_DRY_RUN
+- **WHEN** een gebruiker `WS_DRY_RUN=1 bash alma/install.sh` draait (zonder `--dry-run` flag)
+- **THEN** de installer voert geen side effects uit en gedraagt zich identiek aan `bash alma/install.sh --dry-run`
diff --git a/openspec/changes/v1-release-readiness/specs/smoke-tests/spec.md b/openspec/changes/v1-release-readiness/specs/smoke-tests/spec.md
new file mode 100644
index 0000000..c53c92b
--- /dev/null
+++ b/openspec/changes/v1-release-readiness/specs/smoke-tests/spec.md
@@ -0,0 +1,30 @@
+## ADDED Requirements
+
+### Requirement: GitHub Actions smoke-test matrix
+De repo SHALL een GitHub Actions workflow bevatten (`.github/workflows/smoke.yml`) die op elke `push` en `pull_request` smoke-tests MUST draaien in een matrix over de drie ondersteunde distrofamilies (alma9, ubuntu2404, archlatest) via Docker.
+
+#### Scenario: Workflow triggers
+- **WHEN** een commit wordt gepusht naar de repo of een pull request wordt geopend
+- **THEN** de `smoke` workflow start automatisch voor elk van de drie matrix-distros
+
+#### Scenario: Smoke-stappen per distro
+- **WHEN** de workflow draait voor een distro
+- **THEN** het start een Docker-container van de bijbehorende publieke image (`almalinux:9`, `ubuntu:24.04`, `archlinux:latest`), mount de checkout op `/repo`, en draait sequentieel: `bash bootstrap.sh --dry-run` (moet exit 0) en daarna `bash check.sh` (mag exit 0, 1, of 2)
+
+#### Scenario: Geen side effects op CI-runner
+- **WHEN** de workflow draait
+- **THEN** alle baseline-scripts draaien in `--dry-run` (resp. read-only voor `check.sh`); er worden geen pakketten geïnstalleerd op de container die naar buiten lekken (de container is ephemeral) en geen workflow-stap schrijft naar de host runner
+
+### Requirement: CI-status zichtbaar in README
+De README SHALL CI-badges bovenaan tonen zodat externe lezers de status van de smoke-tests én andere relevante checks (shellcheck) in één oogopslag MUST kunnen zien.
+
+#### Scenario: README-badges
+- **WHEN** een lezer de README bekijkt op GitHub
+- **THEN** ziet hij bovenaan minimaal een shellcheck-badge en een smoke-badge die de huidige status van die workflows reflecteren
+
+### Requirement: Rolling-image pinning bij upstream-breuk
+Wanneer een matrix-image rolling is (`archlinux:latest`) en CI breekt door een upstream-wijziging die niets met deze repo te maken heeft, MAY de workflow de image pinnen op een specifieke tag. Zo'n pin MUST gedocumenteerd zijn in een comment naast de pin, en er SHALL een follow-up issue aangemaakt worden om de pin op te ruimen wanneer upstream weer stabiel is.
+
+#### Scenario: Arch image breekt CI
+- **WHEN** `archlinux:latest` een upstream-wijziging krijgt waardoor `bash bootstrap.sh --dry-run` faalt zonder repo-wijziging
+- **THEN** de matrix-entry mag gepind worden (bv. `archlinux:base-20260501`), met een comment `# pinned 2026-XX-XX wegens ` op die regel, en een follow-up issue om de pin op te ruimen wanneer upstream stabiel is
diff --git a/openspec/changes/v1-release-readiness/specs/versioning/spec.md b/openspec/changes/v1-release-readiness/specs/versioning/spec.md
new file mode 100644
index 0000000..c25f130
--- /dev/null
+++ b/openspec/changes/v1-release-readiness/specs/versioning/spec.md
@@ -0,0 +1,27 @@
+## ADDED Requirements
+
+### Requirement: Single source of truth voor versie
+De repo SHALL een top-level `VERSION`-file bevatten met één regel die de semver-versie van het project bevat. Alle entrypoints die hun versie tonen MUST die lezen uit deze file via de helper `ws_version()`.
+
+#### Scenario: VERSION-file leesbaar
+- **WHEN** een entrypoint `ws_version()` aanroept en `VERSION` bestaat en is leesbaar
+- **THEN** de helper print de inhoud van `VERSION` (zonder trailing newline-handling-issue) naar stdout en exit 0
+
+#### Scenario: VERSION-file ontbreekt of niet leesbaar
+- **WHEN** een entrypoint `ws_version()` aanroept en `VERSION` ontbreekt of is niet leesbaar
+- **THEN** de helper print `unknown` naar stdout en exit 0; het aanroepende script gaat door zonder fatale fout
+
+### Requirement: `--version` / `-V` flag op user-facing entrypoints
+Elk user-facing entrypoint (`bootstrap.sh`, `check.sh`, en elke `common/*.sh` met user-facing CLI: `install-pm-cooldown.sh`, `install-shell-tools.sh`, `incident-token-revoke.sh`, `scan.sh`, `rkhunter-check.sh`, `update.sh`, `uninstall.sh`) SHALL `--version` en `-V` accepteren als argument en MUST een version-string printen wanneer een van die flags aanwezig is.
+
+#### Scenario: --version op bootstrap.sh
+- **WHEN** een gebruiker `bash bootstrap.sh --version` draait
+- **THEN** het script print `workstation-security ` (waar `` de inhoud van `VERSION` is, of `unknown`) en exit 0 zonder OS-detectie of dispatch uit te voeren
+
+#### Scenario: -V als alias
+- **WHEN** een gebruiker `bash check.sh -V` draait
+- **THEN** het gedrag is identiek aan `--version`: print version-string en exit 0
+
+#### Scenario: --version vóór andere flags
+- **WHEN** een gebruiker `--version` combineert met andere flags (bv. `bash install-pm-cooldown.sh --version --days 14`)
+- **THEN** `--version` heeft voorrang; het script print de version-string en exit 0 zonder de andere flags te verwerken
diff --git a/openspec/changes/v1-release-readiness/tasks.md b/openspec/changes/v1-release-readiness/tasks.md
new file mode 100644
index 0000000..015dd75
--- /dev/null
+++ b/openspec/changes/v1-release-readiness/tasks.md
@@ -0,0 +1,68 @@
+## 1. A2 — Cooldown-doc-split
+
+- [x] 1.1 Schrijf `docs/supply-chain-cooldown.md` (~600-800 woorden): aanleiding (npm-incident 2026-05-11), mechanisme per package-manager met versie-vereisten, per-workstation vs per-project vs CI-gap, override-flow voor urgente CVEs, links naar `common/install-pm-cooldown.sh` + templates
+- [x] 1.2 Verkort README-sectie 2 (drie verdedigingslagen → cooldown) tot 3-4 zinnen met link naar de nieuwe doc
+- [x] 1.3 Voeg `docs/supply-chain-cooldown.md` toe aan de index-tabel in `docs/README.md`
+- [ ] 1.4 Doc-review: lees de nieuwe doc cold; zou een lezer zonder context het mechanisme en de overrides begrijpen?
+
+## 2. C1 — Versioning
+
+- [x] 2.1 Voeg top-level `VERSION`-file toe met `0.9.0`
+- [x] 2.2 Implementeer `ws_version()` helper in `common/lib.sh` (leest `VERSION` relatief aan `BASH_SOURCE`, fallback `unknown`)
+- [x] 2.3 Voeg `--version` / `-V` flag toe aan `bootstrap.sh` met early exit voor OS-detectie
+- [x] 2.4 Voeg `--version` / `-V` flag toe aan `check.sh`
+- [x] 2.5 Voeg `--version` / `-V` flag toe aan elke `common/*.sh` met user-facing CLI (`install-pm-cooldown.sh`, `install-shell-tools.sh`, `incident-token-revoke.sh`, `scan.sh`, `rkhunter-check.sh`, `update.sh`, `uninstall.sh`)
+- [ ] 2.6 Schrijf bats-test: `bash bootstrap.sh --version` print de version-string en exit 0 (handmatige smoke gedaan; bats-suite nog niet opgezet — apart van deze change)
+- [x] 2.7 Edge-case test: tijdelijke rename van `VERSION` → `ws_version()` returnt `unknown`, script werkt door (handmatig geverifieerd)
+
+## 3. C2 — CI-matrix
+
+- [x] 3.1 Schrijf `.github/workflows/smoke.yml` met matrix (`alma9`, `ubuntu2404`, `archlatest`) en stappen: checkout, `bash bootstrap.sh --dry-run` (depends op C5), `bash check.sh` (exit ≤ 2)
+- [x] 3.2 Map matrix-strings → Docker-image-namen (`almalinux:9`, `ubuntu:24.04`, `archlinux:latest`) in workflow-script
+- [x] 3.3 Voeg CI-badges (smoke + license + shellcheck) bovenaan README toe; molecule-badge nog niet — B2 bestaat niet
+- [ ] 3.4 Eerste workflow-run draaien op een PR en groen krijgen; bij Arch-breuk image pinnen + comment met reden (handmatige stap zodra er gepusht wordt)
+- [ ] 3.5 (Conditional) Schrijf `.github/workflows/ansible-molecule.yml` met `paths: ansible/**` trigger — alleen als B2 (ansible-bootstrap) al bestaat
+
+## 4. C3 + C4 — Repo-hygiene + README-restructuring
+
+- [x] 4.1 Voeg `LICENSE` toe met de volledige EUPL-1.2-tekst (van SPDX license-list-data — canonieke bron)
+- [x] 4.2 Schrijf `CONTRIBUTING.md` (project-status, "voor een goede PR", "welke PRs landen makkelijk/lastig", OpenSpec-workflow)
+- [x] 4.3 Voeg `.github/ISSUE_TEMPLATE/bug_report.md` toe (distro, versie via `bootstrap.sh --version`, output van `check.sh`, reproductie)
+- [x] 4.4 Voeg `.github/ISSUE_TEMPLATE/distro_support.md` toe (distro, package-manager, `/etc/os-release` ID/ID_LIKE, bereidheid tot testing)
+- [x] 4.5 Voeg `.github/ISSUE_TEMPLATE/config.yml` toe met `blank_issues_enabled: false`
+- [x] 4.6 Update README clone-URL `conduction-it/` → `MWest2020/`
+- [x] 4.7 Herstructureer README: intro → doelgroep/scope (gepromoveerd) → drie verdedigingslagen (elk eigen H2 met "wat", "voor wie", "snelle start") → installatie → check → IR → WSL → docs-links
+- [x] 4.8 Voeg `## License`-sectie onderaan README toe met EUPL-1.2-motivatie (digitale soevereiniteit, NLnet-context)
+- [ ] 4.9 Doc-review: nieuwe lezer kan in 60s zien of dit voor hem relevant is en welke license/contributing-policy geldt
+
+## 5. C5 — `--dry-run` op installers
+
+- [x] 5.1 Voeg `--dry-run` flag toe aan `bootstrap.sh`; print `Would dispatch to: /install.sh` + `(dry-run; no changes made)`, exit 0
+- [x] 5.2 Voeg `--dry-run` flag toe aan `alma/install.sh`; print `dnf install` commando('s) + verwijzing naar `install-timers` aanroep, exit 0
+- [x] 5.3 Idem voor `arch/install.sh` (`pacman -Syu`)
+- [x] 5.4 Idem voor `ubuntu/install.sh` (`apt install`)
+- [x] 5.5 Voeg `--dry-run` flag toe aan `common/install-timers.sh`; print unit-file-inhoud naar stdout in plaats van `/etc/systemd/system/`
+- [x] 5.6 Voeg `--dry-run` flag toe aan `common/install-pm-cooldown.sh`; print config-wijzigingen zonder `~/.npmrc` / `~/.bunfig.toml` aan te raken (hergebruik logica uit `--check`)
+- [x] 5.7 Implementeer propagatie: `bootstrap.sh` zet `WS_DRY_RUN=1` in environment bij dispatch; sub-installers respecteren zowel `--dry-run` als `WS_DRY_RUN=1` (helpers `ws_is_dry_run` en `ws_run_or_print` in `lib.sh`)
+- [ ] 5.8 CI-smoke (3.1) draait `bootstrap.sh --dry-run` op alle drie distros en bevestigt: geen pakketten geïnstalleerd, exit 0 (workflow geschreven, eerste run komt zodra gepusht)
+
+## 6. C6 — v1.0.0 release
+
+- [ ] 6.1 Voorwaarde: alle A+B+C-PR's gemerged, CI groen, geen openstaande blokkers
+- [ ] 6.2 Update `VERSION` → `1.0.0`
+- [ ] 6.3 CHANGELOG-entry onder `## v1.0.0 (YYYY-MM-DD)` met geconsolideerde samenvatting van A+B+C
+- [ ] 6.4 Merge PR `chore: release v1.0.0`
+- [ ] 6.5 `git tag -a v1.0.0 -m "v1.0.0 — initial public release"`
+- [ ] 6.6 `git push origin v1.0.0`
+- [ ] 6.7 GitHub Release aanmaken via `gh release create v1.0.0` met title + CHANGELOG-fragment als body
+- [ ] 6.8 Blogpost-draft (1500-2500 woorden): incident → drie lagen → keuzes → out-of-scope → repo-link → license; link naar `tree/v1.0.0` (niet `main`)
+- [ ] 6.9 Cross-post overwegen (dev.to / LinkedIn / HN / r/devops afhankelijk van timing)
+- [ ] 6.10 Externe lezer (collega buiten Conduction) vragen in 5 min te zeggen waar het project over gaat en of 'ie het zou oppakken; feedback verwerken in eventuele v1.0.1
+
+## 7. Definition of Done v1.0.0
+
+- [ ] 7.1 Alle bullet-points uit `proposal.md` → "Success criteria" (indien aanwezig in upstream proposal) afgevinkt
+- [ ] 7.2 GitHub Release zichtbaar op `github.com/MWest2020/workstation-security/releases`
+- [ ] 7.3 Blogpost gepubliceerd en gelinkt vanuit de Release-notes
+- [ ] 7.4 README CI-badges groen
+- [ ] 7.5 Tag `v1.0.0` aanwezig in repo en pointer correct (`git rev-parse v1.0.0`)
diff --git a/openspec/config.yaml b/openspec/config.yaml
new file mode 100644
index 0000000..392946c
--- /dev/null
+++ b/openspec/config.yaml
@@ -0,0 +1,20 @@
+schema: spec-driven
+
+# Project context (optional)
+# This is shown to AI when creating artifacts.
+# Add your tech stack, conventions, style guides, domain knowledge, etc.
+# Example:
+# context: |
+# Tech stack: TypeScript, React, Node.js
+# We use conventional commits
+# Domain: e-commerce platform
+
+# Per-artifact rules (optional)
+# Add custom rules for specific artifacts.
+# Example:
+# rules:
+# proposal:
+# - Keep proposals under 500 words
+# - Always include a "Non-goals" section
+# tasks:
+# - Break tasks into chunks of max 2 hours
diff --git a/ubuntu/install.sh b/ubuntu/install.sh
index 612cc14..5ecfec2 100644
--- a/ubuntu/install.sh
+++ b/ubuntu/install.sh
@@ -6,21 +6,38 @@
# Style-afwijking: shebang via `env bash` voor consistentie met repo.
#
# Usage:
-# sudo bash ubuntu/install.sh # apt install ClamAV + rkhunter, enable services, install timers
+# sudo bash ubuntu/install.sh # apt install ClamAV + rkhunter, enable services, install timers
+# bash ubuntu/install.sh --dry-run # print zou-uitgevoerd-zijn commando's, geen wijzigingen
+# bash ubuntu/install.sh --version # print versie en exit
set -euo pipefail
# shellcheck source=/dev/null
source "$(dirname "$0")/../common/install-base.sh"
+ws_handle_version "$@"
+for arg in "$@"; do
+ case "$arg" in
+ --dry-run) export WS_DRY_RUN=1 ;;
+ *)
+ echo "error: onbekend argument: $arg" >&2
+ echo " Geldige flags: --dry-run, --version/-V" >&2
+ exit 2
+ ;;
+ esac
+done
+
require_root "ubuntu/install.sh"
clamav_ok=0
rkhunter_ok=0
echo "==> Packages installeren..."
-apt-get update
-if apt-get install -y clamav clamav-daemon; then
+ws_run_or_print apt-get update
+if ws_is_dry_run; then
+ ws_run_or_print apt-get install -y clamav clamav-daemon
+ clamav_ok=1
+elif apt-get install -y clamav clamav-daemon; then
clamav_ok=1
else
echo " FOUT: ClamAV installatie mislukt." >&2
@@ -34,7 +51,11 @@ echo "==> Services aanzetten..."
enable_clamav_services clamav-daemon clamav-freshclam
echo "==> rkhunter installeren..."
-if apt-get install -y rkhunter 2>/dev/null; then
+if ws_is_dry_run; then
+ ws_run_or_print apt-get install -y rkhunter
+ rkhunter_init
+ rkhunter_ok=1
+elif apt-get install -y rkhunter 2>/dev/null; then
rkhunter_init
rkhunter_ok=1
else
@@ -44,6 +65,12 @@ fi
echo "==> Timers installeren..."
install_timers
+if ws_is_dry_run; then
+ echo ""
+ echo "(dry-run; no changes made)"
+ exit 0
+fi
+
print_summary "$clamav_ok" "$rkhunter_ok" "apt"
exit 0