Lab 01 — Migrate a Legacy Script into a Spec-Driven uv Project¶
Hands-on lab. Environment:
plaintext-labs/python-for-security/01-modern-toolchain(a container withuv,ruff,pyright,git+ the legacyalert_parse.pyand a bundled Suricataeve.json). Objective: migrate a working legacy script into a modern, CI-gateduvproject without breaking it — landingsiftv0, the alert-triage tool you grow across all nine modules. Target: ~2–3 hrs. Intermediate-plus: the steps state objectives; you derive theuv/ruff/CI commands (with the copilot).
✈ Flight card — the 6 things to hold¶
Glance here when you lose the thread.
| # | Fact | Why it matters |
|---|---|---|
| 1 | The toolchain is a security control, not a preference. | A hash-locked lockfile defeats the torchtriton dependency-confusion class. |
| 2 | Hash-locked ≠ version-pinned. | torch==2.0.1 fixes the version, not the bytes; the hash refuses tampered bytes. |
| 3 | Strangler-fig: wrap first, never big-bang. | Wrap the working script, get CI green around it, refactor behind the gate. |
| 4 | Spec → AI implements → review against the spec. | A far sharper lens than reading generated code cold — and how you own it. |
| 5 | CI is where the bar is enforced. | Lint, types, and lockfile checks fail the build — not your goodwill. |
| 6 | This lab lands sift v0. |
Every later module adds a stage; the migration is the seed of the whole track. |
↳ Go deeper — pull only when a step doesn't click: the module's core idea and the
torchtritonprimary source.
Warm-up — answer before you build (2 min)¶
- Why does pinning
torch==2.0.1not fully protect you, and what does a hash-locked file add? - What breaks if you delete
alert_parse.pyand have the copilot regenerate it clean, versus wrapping it and migrating behind a passing CI gate?
Setup¶
git clone https://github.com/plaintext-security/plaintext-labs
cd plaintext-labs/python-for-security/01-modern-toolchain
make up # build the toolchain container
make shell # drop into the project with the legacy alert_parse.py + bundled eve.json
make demo # runs the migrated project's CI gate (lint + types + locked install + the legacy script)
make down
Authorization note. Everything runs locally in the lab container against bundled sample data — only test systems you own or have written permission to test.
Build it — objective, then a signal (intermediate-plus: you drive the commands)¶
Step 1 — Reproduce the legacy baseline (your ground truth)¶
Concept (30 sec): Flight-card #3. The migration is correct only if the legacy output never changes. Capture it before you touch anything.
Do: run alert_parse.py against the bundled eve.json; save its output (it counts alert events by
alert.signature).
▸ On track if: you have a saved baseline output you can diff against at every later step.
Step 2 — Write the spec first¶
Concept (30 sec): Flight-card #4. The spec is the contract you review the copilot's work against.
Do: write a short change spec for the migration — wrap the script unchanged in a uv project; add
ruff, pyright, and a hash-locked lockfile; gate all three in CI; behaviour identical.
▸ On track if: the spec names the typed contract and the acceptance checks — not just "modernize it."
Step 3 — Stand up the uv project around the script (strangler-fig)¶
Concept (30 sec): Flight-card #3. Wrap, don't rewrite. No refactor yet.
Do: uv init the project, add the script unchanged as an entry point, and get uv run to reproduce
Step 1's output.
▸ On track if:
uv runreproduces the baseline byte-for-byte — and you haven't edited the script's logic.
Step 4 — Add the gates (and review the copilot's defaults)¶
Concept (30 sec): Flight-card #1 + #2. Let the copilot generate the config, then hold it to the bar.
Do: configure ruff and pyright in pyproject.toml; produce a hash-locked uv.lock.
▸ On track if: the lockfile is pinned and hashed, and the type gate is actually on — catch the copilot if it emitted
pip/requirements.txt, skipped the hashes, or left types off.
Step 5 — Wire CI, then prove it bites¶
Do: add a workflow that fails on any of ruff findings, pyright errors, or a non-reproducible
install. Plant a lint error, watch CI fail, fix it.
▸ On track if: you have demonstrated both a red build (planted error) and a green one (fixed).
Step 6 — Migrate one slice behind the green gate¶
Do: refactor one piece (e.g. type the parse function, or split I/O from logic); keep the prior commit as rollback.
▸ On track if: Step 1's output still matches byte-for-byte and CI stays green after the refactor.
Prove the control (your finish line)¶
Commit sift v0 and confirm the whole gate holds:
- [ ]
uv runreproduces the legacy output byte-for-byte (migration broke nothing). - [ ]
ruff+pyrightclean; install reproducible from a hash-lockeduv.lock. - [ ] CI fails on a planted lint/type error and passes when fixed (you showed both).
- [ ] The migration spec exists and you can point to the implementation that satisfies it.
- [ ]
ADR-001-toolchain.mdrecords the decision with an honest Consequences section.
Recall check — close the doc, answer from memory (3 min)¶
- Hash-locked vs version-pinned — which one stops the
torchtritonclass, and why? - State the strangler-fig sequence in three moves.
- What does reviewing an implementation against a spec catch that reading the code cold does not?
Deliverables¶
The migrated sift v0 repo: the uv project wrapping the legacy script, pyproject.toml
(ruff/pyright), the hash-locked uv.lock, the CI workflow, the migration spec, and
ADR-001-toolchain.md. Do not commit real API keys or .env — later modules add secrets handling.
Automate & own it¶
Required. Commit sift v0 with the CI gate, the spec, and the ADR. In the commit/PR, note what the
copilot generated, what you corrected, and the one thing it defaulted to that you had to fix (the old
toolchain, the missing lockfile hashes, or a dropped behaviour). Reviewing the AI's setup is where this
class of risk lives.
Definition of done (modern-toolchain ✅)¶
- [ ]
siftv0 is committed with a green CI gate over lint + types + a hash-locked install. - [ ] The byte-for-byte migration, the spec, and
ADR-001-toolchain.mdare all in the repo. - [ ] You can explain all six flight-card facts cold.
Connects forward¶
This skeleton is the home for every later module: 02 adds pydantic validation inside it, 04
makes it async, 09 hardens the very supply-chain gate you started here. The strangler-fig muscle
returns in Track 06 (brownfield AD tiering) and Track 10 (click-ops → IaC), where a big-bang cutover means
a real outage.
Marketable proof¶
"I migrate legacy Python into a modern
uv/ruff/pyrightproject with a hash-locked supply-chain gate in CI, using a spec-driven workflow to direct and review AI-generated code — without breaking the running script."
Stretch (optional)¶
- Add
pip-audit(oruv's audit) to CI and make it fail on a known-vulnerable pinned dependency — a preview of Module 09. - Reproduce the
torchtritonclass in miniature: a tiny private index, an unpinned install resolving the public shadowing package, then the hash-locked install refusing it.
Comments
Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).