Skip to content

Bug fixes - #40

Merged
llambeau merged 7 commits into
masterfrom
fix/medium-severity-findings
Jul 17, 2026
Merged

Bug fixes#40
llambeau merged 7 commits into
masterfrom
fix/medium-severity-findings

Conversation

@llambeau

Copy link
Copy Markdown
Contributor

No description provided.

llambeau and others added 7 commits July 17, 2026 09:58
The `secrets` and `secrets validate` commands each carried a verbatim
~40-line copy of the same discovery routine: build the provider set,
scan monorepo-level config (env/vars/tasks/defaults/flavors), scan every
component's config (tasks/resources), then aggregate by unique
provider+path+key. Any change to what gets scanned had to be made twice
and would silently drift.

Extract it into collectAllSecrets(monorepo, secretProviders) alongside
discoverSecrets/aggregateSecrets and call it from both commands. Each
command keeps its own downstream mapping (SecretInfo table vs. per-secret
validation); only the shared discovery+aggregation moves.

Addresses REPORT.md finding #9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `images` and `images delete` commands each filtered an image's
RepoTags with the same expression — `delete.ts` even carried a
"De-duplicate this (also in images/index.ts)" TODO. The match was also
fragile: `tag.indexOf(name) === 0` is an unanchored prefix test, so a
project named `foo` would also capture a different project's
`foobar/...` tags.

Extract projectImageTags(repoTags, projectName) into src/docker/images/
and call it from both commands. It anchors on the `<project>/` boundary
(startsWith(name + '/')) instead of a bare prefix, fixing the
cross-project match while removing the duplication.

Addresses REPORT.md finding #17.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`validateUserConfig` and `validateEmbfile` guarded the read with
`if (await stat(path))`. stat() resolves to a (truthy) Stats object or
rejects — it is never falsy — so the `else { throw 'Could not find
file' }` branch was dead code, and a missing file surfaced a raw
`ENOENT: no such file or directory, stat '...'` instead.

Wrap stat() in try/catch so the intended friendly message actually
fires when the file is absent.

Addresses REPORT.md finding #23.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The constructor runs top-level tasks through toIdentifedHash, which
stamps each with synthesized `id`/`name` keys for runtime resolution.
toJSON() returned those tasks verbatim, so the keys leaked into the
serialized config. Since `emb config print` emits `toJSON()` output and
TaskConfig has `additionalProperties: false`, the printed config failed
its own re-validation with `unknown property 'id'`.

Strip `id`/`name`/`component` from tasks at the serialization boundary,
leaving `this.tasks` untouched for runtime consumers. `with()` still
works — it re-runs toIdentifedHash on the cleaned tasks.

Addresses REPORT.md finding #26.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
validateUserConfig and validateEmbfile carried copy-pasted load/parse
logic (stat check, readFile, redundant double .toString(), yaml.parse),
and the duplication had already let behavior drift: validateEmbfile
guarded against an empty document (`if (!component) return {}`) while
validateUserConfig did not, so an empty or comments-only .emb.yml failed
every command with the opaque Ajv message "/: must be object".

Extract a shared loadYamlDocument() helper (missing-file friendly error
+ read + parse) consumed by both validators, and have validateUserConfig
report an empty document as "Configuration file is empty: <file>".

Addresses REPORT.md finding #28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
withFlavor() builds a fresh Monorepo, whose new TaskManagerFactory
defaults to the standard renderer. BaseCommand.init() calls
setTaskRenderer('verbose') on the base repo when --verbose/EMB_VERBOSE
is set, but FlavouredCommand.init then swaps in the withFlavor() result,
so `emb up --verbose --flavor production` silently rendered with the
default renderer instead of the verbose one.

Add TaskManagerFactory.getRenderer() and have withFlavor() copy the
base repo's renderer onto the flavored repo before returning.

Addresses REPORT.md finding #31.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mkdirp() normalized paths (normalize(join('/', path))) to keep writes
inside the store, but join() — used by writeFile/readFile/stat and the
read/write streams to build the actual target — passed the raw path to
node:path.join, which resolves '..'. So a '..' path escaped the
per-flavor sandbox: store.writeFile('../shared.txt', ...) landed in
<store>/ instead of <store>/<flavor>/, and deeper '../../' paths ENOENT
because mkdirp created the parent in the sanitized location while the
write targeted the unsanitized one.

Move the confinement into join() (the single choke point) and have
mkdirp() rely on it, so directory creation and the file operation always
agree and nothing can leave the flavor directory.

Addresses REPORT.md finding #32.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@llambeau
llambeau merged commit 51759e0 into master Jul 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant