DevSecOps XL Labs — a domain-agnostic CLI framework to drive hands-on learning labs across multiple repositories
Project description
dsoxlab — DevSecOps XL Labs CLI
Read this in another language: Français
dsoxlab is a domain-agnostic CLI framework that drives hands-on
learning labs spread across multiple repositories. Each repository
declares its own catalog through a root meta.yml file and one lab.yaml
per lab.
The framework serves Linux, Ansible, Kubernetes or Terraform labs equally
well — anything that honors the declarative contract. It provisions the
environment, runs infrastructure-level validation (pytest +
pytest-testinfra), scores progress, and stores history locally. Nothing
about a specific domain lives in the engine.
Companion to the tutorials on blog.stephane-robert.info.
Why dsoxlab
- One engine, many catalogs. A single CLI drives every training
repository. Add a new domain by writing a
meta.yml, not by patching the tool. - Validation proves, it does not trust. Labs are graded on the actual
state of the system (
pytest-testinfra) and, when it matters, on persistence after reboot — the trap that fails RHCSA/LFCS candidates. - Multiple runtimes. Run a lab in a plain shell, an Incus container, or a full KVM/libvirt virtual machine, chosen per lab.
- Progress that sticks. Scores, hint costs and history are persisted in a local SQLite database following the XDG spec.
- Bilingual UX. Every user-facing string ships in English and French
(
DSOXLAB_LANG=en|fr).
Installation
Requires Python 3.11+ and uv.
# From the Git repository (development / editable mode)
git clone https://github.com/stephrobert/dsoxlab.git
cd dsoxlab
uv tool install --editable .
# Verify
dsoxlab --version
dsoxlab doctor
dsoxlab doctor --fix diagnoses (and repairs where possible) the local
toolchain expected by the runtimes: SSH, Terraform, libvirt/Incus, and the
embedded pytest stack.
Quickstart
Labs live in their own repositories, published separately from the engine.
Clone one first, then run dsoxlab from inside it:
# 1. Clone a lab catalog (e.g. linux-dsoxlab-training)
git clone https://github.com/stephrobert/linux-dsoxlab-training.git
cd linux-dsoxlab-training
# 2. The active context is detected automatically from the repo's meta.yml
dsoxlab list-labs
dsoxlab show linux.depanner.service-crash-loop
dsoxlab guide linux.depanner.service-crash-loop # read the course in your browser
dsoxlab run linux.depanner.service-crash-loop
dsoxlab check linux.depanner.service-crash-loop
Reading the course
The course itself is not bundled in the lab repository: each lab declares a
doc_url pointing to the trainer's site. dsoxlab guide opens that page in a
real browser tab, so it renders exactly as published, with its images, code
blocks and navigation.
dsoxlab guide # the active lab
dsoxlab guide <id> # a specific lab
dsoxlab guide <id> --print # print the URL instead (useful over SSH)
The URL carries campaign parameters (utm_source=dsoxlab, utm_medium=lab,
utm_campaign=<lab_id>), so a trainer can see which labs actually drive readers
to which guides. A link opened from a local interface carries no usable referrer,
so without this marking those reads would be indistinguishable from direct traffic.
Switch language on the fly:
DSOXLAB_LANG=fr dsoxlab fullhelp
DSOXLAB_LANG=en dsoxlab fullhelp
The declarative contract
A lab-hosting repository describes its catalog with two levels of files.
1. meta.yml at the repository root
Repository metadata, infrastructure topology (KVM/Incus), section ordering.
repo:
id: linux-training
category: linux
title: "Linux Training — RHCSA + LFCS 2026"
blog_url: "https://blog.stephane-robert.info/docs/admin-serveurs/linux/"
infra:
network: lab-linux
hosts:
- { name: alma-rhcsa-1.lab, ip: 10.10.30.11, distro: alma10 }
- { name: alma-rhcsa-2.lab, ip: 10.10.30.12, distro: alma10 }
- { name: ubuntu-lfcs-1.lab, ip: 10.10.30.21, distro: ubuntu24 }
sections:
- id: depanner
title: "Troubleshooting"
labs:
- depanner/services-processus/service-crash-loop
- depanner/stockage-fs/disque-plein-mais-pas-de-fichiers
2. lab.yaml per lab (under labs/<category>/<section>/<lab>/)
Lab-specific metadata (skills, runtime, distros, validation).
id: depanner-service-crash-loop
title: "Identify and fix a crash-looping systemd service"
section: linux
level: l2
track: [depanner, rhcsa]
skills: [systemd, journalctl, debug]
difficulty: intermediate
estimated_time: 30m
runtime:
type: kvm
host: alma-rhcsa-1.lab
distros: [rhel10, ubuntu24.04]
doc_url: https://blog.stephane-robert.info/docs/admin-serveurs/linux/depanner/services-processus/service-crash-loop/
validation:
functional: true
security: false
persistence_after_reboot: true
An optional lab.fr.yaml may override title and description for French
only.
dsoxlab validate-structure checks that the whole contract holds: the root
meta.yml is well-formed, every referenced lab exists with a valid
lab.yaml, each runtime.host maps to a declared host, and all referenced
scripts and test files are present.
Command reference
| Command | Purpose |
|---|---|
dsoxlab use [section[/level]] |
Set the active context; --reset clears it, --provider selects the infra provider |
dsoxlab list-labs |
List labs of the current repo (filter by --section/--level/--type/--bloc) |
dsoxlab show <id> |
Show a lab's details |
dsoxlab course [section] |
Display a course section, or the table of contents |
dsoxlab guide [id] |
Open the lab's online guide in a browser (--print shows the URL) |
dsoxlab run <id> |
Prepare and start the lab environment |
dsoxlab challenge <id> |
Show the challenge mission for a lab |
dsoxlab hint <id> |
Reveal a hint (deducted from the score) |
dsoxlab check <id> |
Run the pytest validation and record the score |
dsoxlab submit <id> |
Final submission: run tests, record score, close the session |
dsoxlab scores |
Show run history (local SQLite) |
dsoxlab progress |
Progression by bloc (labs done, average score, challenges) |
dsoxlab next |
Recommend the next lab or challenge to tackle |
dsoxlab reset <id> |
Reset the lab to its initial state |
dsoxlab clean <id> |
Run the lab's cleanup.sh |
dsoxlab provision |
Provision the lab infrastructure (terraform apply) |
dsoxlab destroy |
Destroy the lab infrastructure (terraform destroy) |
dsoxlab status |
Check SSH connectivity to all hosts in meta.yml |
dsoxlab ssh <host> |
Open an interactive SSH session on a lab host |
dsoxlab validate-structure |
Validate the contract (meta.yml + every lab.yaml) |
dsoxlab doctor [--fix] |
Diagnose (and repair) the local environment |
dsoxlab install |
Install the shell wrapper and auto-completion |
dsoxlab instructor bootstrap |
Instructor tooling (e.g. generate the lab SSH key) |
dsoxlab fullhelp |
Full multilingual guide (EN/FR) |
Run dsoxlab <command> --help for the options of any command.
Runtimes
| Runtime | Backend | Typical use |
|---|---|---|
shell |
Local shell | Quick, single-host exercises with no VM overhead |
incus |
Incus containers | Isolated, fast-booting Linux environments |
kvm |
Terraform + libvirt | Full VMs with reboot/persistence testing |
Each runtime is opt-in and self-describing (is_available()), so the engine
never hard-depends on a backend the user has not installed. Provisioning
templates (Terraform HCL, cloud-init) live under dsoxlab.templates and
support Incus, KVM/libvirt and Outscale.
Architecture
src/dsoxlab/
├── cli.py ← Typer entry point (+ i18n command group)
├── config.py ← LAB_HOME, active context, .dsoxlab-context.json
├── i18n/ ← get_lang(), _(), en.py + fr.py
├── models/ ← typed schemas of the declarative contract
├── discovery/ ← scan meta.yml + every lab.yaml of the current repo
├── services/ ← business orchestration (get_lab, run_lab, check_lab…)
├── sessions/ ← SQLite persistence (results + hint_requests)
├── runtimes/ ← BaseRuntime, ShellRuntime, IncusRuntime, KvmRuntime
├── infra/ ← Terraform, Ansible, inventory, snapshots
├── validators/ ← contract validation (meta.yml + lab.yaml)
├── reporting/ ← Rich terminal output
├── utils/ ← centralized subprocess wrapper
└── templates/ ← provisioning templates (HCL, cloud-init)
The engine stays independent of any single repository layout: discovery/
works on whatever tree the meta.yml declares.
Persistence
- Local sessions:
.dsoxlab-context.jsonin the current repo (gitignored by each lab repository). - Scores and hints:
~/.local/share/dsoxlab/progress.db(XDG). The global lab id is<category>.<section>.<lab>, so the schema stays universal. - User config:
~/.config/dsoxlab/config.yaml(optional).
Override the locations with the standard XDG_DATA_HOME / XDG_CONFIG_HOME
environment variables.
Development
uv sync # install dev dependencies
uv run pre-commit install --install-hooks # enable the git hooks
uv run ruff check src/dsoxlab # lint + security
uv run mypy src/dsoxlab # type-check (strict)
uv run pytest # tests
See CONTRIBUTING.md for the workflow, the commit
conventions, and the non-negotiable rules (the engine must stay
domain-agnostic, every user-facing string goes through _() in both
languages).
Security
Security posture is enforced, not aspirational — every workflow is scanned by its own tooling on each push and pull request:
- Hardened GitHub Actions. Every action is pinned to a full commit SHA, the
default token has no permissions (jobs opt into least privilege), and
checkoutnever persists credentials. - zizmor statically analyzes the
workflows on every PR (
ci.yml). - Plumber validates the CI/CD against a trust
policy (
.plumber.yaml) at a 100% compliance threshold, and publishes the score badge (plumber.yml). - OpenSSF Scorecard tracks the
supply-chain posture (
scorecard.yml). - PyPI Trusted Publishing (OIDC). Releases carry no long-lived token and
ship PEP 740 attestations (
release.yml). - Pre-commit secret scanning. TruffleHog and private-key detection run locally before every commit (see CONTRIBUTING.md).
To report a vulnerability, follow SECURITY.md.
License & attribution
Licensed under the Apache License 2.0 — see LICENSE and NOTICE.
You may use, share and adapt this project, including commercially, provided you give appropriate credit to Stephane Robert and link back to https://blog.stephane-robert.info, and indicate whether changes were made. Apache-2.0 keeps those same two obligations — attribution and stating your changes — and adds an express patent grant.
Up to and including 0.1.12, dsoxlab was distributed under Creative Commons
Attribution 4.0 (CC BY 4.0). That grant is irrevocable, so those releases remain
available under CC BY 4.0. From 0.1.13 onwards the project is Apache-2.0:
Creative Commons licences are not designed for software, and this one left the
patent question open while marking the package as Other/NOASSERTION on PyPI.
© 2026 Stephane Robert.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dsoxlab-0.1.20.tar.gz.
File metadata
- Download URL: dsoxlab-0.1.20.tar.gz
- Upload date:
- Size: 627.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b88888c5b656d6449a11358b6808fed256d4b8cc63d21c38cf5022e2761708bf
|
|
| MD5 |
77b02410fa785e89951123e72bde4340
|
|
| BLAKE2b-256 |
4f48954ab729ca69c1b259e92dc612a9f538f957ddc9e8d0be9fc188ef8cb280
|
Provenance
The following attestation bundles were made for dsoxlab-0.1.20.tar.gz:
Publisher:
release.yml on stephrobert/dsoxlab
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dsoxlab-0.1.20.tar.gz -
Subject digest:
b88888c5b656d6449a11358b6808fed256d4b8cc63d21c38cf5022e2761708bf - Sigstore transparency entry: 2205773755
- Sigstore integration time:
-
Permalink:
stephrobert/dsoxlab@3c5afcedc4d91eb735b88468738c2110c8c9b814 -
Branch / Tag:
refs/tags/v0.1.20 - Owner: https://github.com/stephrobert
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3c5afcedc4d91eb735b88468738c2110c8c9b814 -
Trigger Event:
push
-
Statement type:
File details
Details for the file dsoxlab-0.1.20-py3-none-any.whl.
File metadata
- Download URL: dsoxlab-0.1.20-py3-none-any.whl
- Upload date:
- Size: 148.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ab538a9bf51533112036569a649b5931784a9ea771587fe483df523467ade74
|
|
| MD5 |
2e3799a65041805a7874a26c82b219f8
|
|
| BLAKE2b-256 |
6b0dcf2546adfc913e35b0f424003d34b46c267a9dbb9a60dd85bed7b21ff4eb
|
Provenance
The following attestation bundles were made for dsoxlab-0.1.20-py3-none-any.whl:
Publisher:
release.yml on stephrobert/dsoxlab
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dsoxlab-0.1.20-py3-none-any.whl -
Subject digest:
1ab538a9bf51533112036569a649b5931784a9ea771587fe483df523467ade74 - Sigstore transparency entry: 2205773838
- Sigstore integration time:
-
Permalink:
stephrobert/dsoxlab@3c5afcedc4d91eb735b88468738c2110c8c9b814 -
Branch / Tag:
refs/tags/v0.1.20 - Owner: https://github.com/stephrobert
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3c5afcedc4d91eb735b88468738c2110c8c9b814 -
Trigger Event:
push
-
Statement type: