fix(modules-1,15,17): onboarding step, make M15 gate actually catch the plant, M17 .env override

- M1: add a no-git "Get the course materials" step (download+unzip; clone noted
  as Module 8) so Part A's paths resolve without assuming git. URL flagged
  Verify-before-publish (swap to public host before publishing).
- M15: security gate was failing OPEN on python3-only systems (bare `python`)
  and missing the UNTRACKED config.py, so the planted secret passed green. Now
  guards python3, fails CLOSED on any non-clean exit, and stages files so the
  planted SYNC_API_KEY + typosquat dep are actually caught.
- M15: correct the false "Bandit flags the API key" claim (B105-107 need
  password-named ids); add an honest MD5 (B324) flaw so the SAST demo fires.
  Planted secret/deps preserved.
- M17: require the .env loader to use setdefault so Part D's override demo works;
  explain precedence. Hardcoded "before" anti-pattern left intact.

Closes #6
Closes #17
Closes #18
Closes #19
Closes #29

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
2026-06-22 15:48:27 -04:00
parent 06b9f8f308
commit 3bab54d135
5 changed files with 112 additions and 20 deletions
@@ -332,7 +332,9 @@ config per environment.
> *"Refactor `sync.py` so it reads `TASKS_API_KEY` and `APP_ENV` from environment variables
> instead of hardcoding them. Pick the backend URL from `APP_ENV` (dev/staging/prod). Fail loudly
> with a clear message if `TASKS_API_KEY` is missing. Don't add any third-party dependency — load
> the `.env` file with a few lines of plain Python."*
> the `.env` file with a few lines of plain Python, and make sure the loader does **not**
> overwrite a variable that's already set in the environment, so a value passed on the command
> line still wins."*
You're looking for a result shaped like this (read the diff before you accept it):
@@ -372,6 +374,15 @@ config per environment.
grep -n "sk-live" sync.py # should print nothing
```
**Why `setdefault` and not plain assignment?** The loader uses `os.environ.setdefault(key, value)`,
which sets a variable *only if it isn't already set*. That precedence is load-bearing: a value the
environment already supplies — like an `APP_ENV` you pass on the command line — wins over the
`.env` file. A loader that writes `os.environ[key] = value` instead **clobbers** anything already
there, so the file silently overrides your command line and Part D's override demo does nothing.
This matches the real-world dotenv default (`override=False`): the file fills in gaps, it doesn't
stomp on what's already in the environment. If the AI hands you plain assignment, that's the
correction to make.
### Part D — Run it from the environment
5. Run it reading from your `.env`:
@@ -395,7 +406,9 @@ config per environment.
```
Watch the backend URL change with `APP_ENV` while the source never does. That's config in the
environment.
environment. **If the URL *doesn't* change, your loader is clobbering variables that were already
set** — it's using `os.environ[key] = value` where it needs `os.environ.setdefault(...)` (see
Part C). Fix the loader so the command line wins, and the override takes effect.
### Part E — Commit, and verify the secret didn't tag along