diff --git a/build.py b/build.py index c3530c5..e155025 100644 --- a/build.py +++ b/build.py @@ -80,6 +80,30 @@ def _rmtree_resilient(path: Path): ) +# Never swept into a built room, wherever they appear in a source tree. +# +# This is a SECURITY boundary, not tidiness. gen// is the docker build +# context, soleprint/Dockerfile is `COPY . .`, and there is no .dockerignore — +# so anything that reaches gen/ reaches an image layer, and registry.mcrn.ar is +# public-read. That is how station/tools/tester/.env, gitignored since the last +# incident, still ended up baked into soleprint_localtest-soleprint:latest with +# its API key intact. .gitignore does not bind shutil. +# +# Applied to bulk directory copies only. A caller naming a single file is making +# an explicit request (cfg//.env.example is the one that matters) and is +# left alone. +ALWAYS_IGNORE = {".git", "__pycache__", "node_modules", ".venv", "venv", ".env"} +ALWAYS_IGNORE_SUFFIXES = (".pyc", ".pyo") + + +def is_ignored(name: str) -> bool: + return name in ALWAYS_IGNORE or name.endswith(ALWAYS_IGNORE_SUFFIXES) + + +def _copytree_ignore(directory, files): + return {f for f in files if is_ignored(f)} + + def copy_path(source: Path, target: Path, quiet: bool = False): """Copy file or directory, resolving symlinks.""" if target.is_symlink(): @@ -91,7 +115,7 @@ def copy_path(source: Path, target: Path, quiet: bool = False): target.unlink() if source.is_dir(): - shutil.copytree(source, target, symlinks=False) + shutil.copytree(source, target, symlinks=False, ignore=_copytree_ignore) if not quiet: log.info(f" {target.name}/") else: @@ -111,6 +135,8 @@ def merge_into(source: Path, target: Path): for item in source.rglob("*"): if item.is_file(): rel = item.relative_to(source) + if any(is_ignored(part) for part in rel.parts): + continue dest = target / rel dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(item, dest) @@ -687,6 +713,7 @@ def build_soleprint(output_dir: Path, room: str): "index.html", "requirements.txt", "Dockerfile", + ".dockerignore", ]: if (soleprint / name).exists(): copy_path(soleprint / name, output_dir / name) diff --git a/soleprint/.dockerignore b/soleprint/.dockerignore new file mode 100644 index 0000000..47f779b --- /dev/null +++ b/soleprint/.dockerignore @@ -0,0 +1,24 @@ +# The build context is gen//, and the Dockerfile is `COPY . .` — so this +# file is the last thing standing between a stray secret and a public image +# layer. build.py already filters these out of the copy into gen/; this repeats +# the rule at the docker boundary so a hand-built context, or a future copy path +# that forgets, still cannot bake one in. +# +# Copied into gen// by build.py's named-file list alongside the Dockerfile. + +.env +.env.* +**/.env +**/.env.* +!.env.example +!**/.env.example + +__pycache__/ +**/__pycache__/ +*.pyc +*.pyo + +.git/ +.venv/ +venv/ +node_modules/