diff --git a/.github/workflows/sdk-cli-ci.yml b/.github/workflows/sdk-cli-ci.yml index ea7dad28a..e66c7c609 100644 --- a/.github/workflows/sdk-cli-ci.yml +++ b/.github/workflows/sdk-cli-ci.yml @@ -22,15 +22,28 @@ on: # scanner filtered to `src/sdk/cli/**` cannot see a change to `src/packages/**` # or `src/apps/**`, which is most of the code in this repository. # - # The `push` trigger below keeps its filter on purpose: pushes are not gated by + # The `push` trigger below keeps a filter on purpose: pushes are not gated by # required checks, so no deadlock is possible there and the CI minutes are worth - # saving. + # saving on docs-only changes. The filter now spans every tree CodeQL scans + # (GT-713) — a push that changes no code still skips the run. pull_request: branches: [main, develop] push: branches: [main, develop] paths: - 'src/sdk/cli/**' + # GT-713: the `CodeQL SAST` job below is the ONLY producer of the CodeQL + # analysis for `refs/heads/main` (category `/language:javascript-typescript`, + # the one the Security tab's alerts are keyed to). A pull-request run is + # diff-informed and does not update the branch's alerts, and the default + # "Code Quality" setup is a different suite. With the filter limited to the + # CLI tree, promotion 19d736da (2026-09-19, changes under src/packages and + # src/apps only) landed on main with no analysis at all: the tab kept + # reporting alerts on code that no longer existed until a manual + # `gh workflow run sdk-cli-ci.yml --ref main`. Every path CodeQL scans + # therefore has to be a path that triggers this workflow on push. + - 'src/packages/**' + - 'src/apps/**' - '.harness/**' # package.json / package-lock.json: this pipeline installs with `npm ci`, # so a lock desync breaks every job here -- and until now neither file diff --git a/README.es.md b/README.es.md index 78f296c73..755e6c10b 100644 --- a/README.es.md +++ b/README.es.md @@ -25,7 +25,7 @@ Y no es solo un linter: **las reglas van atadas a la fase SDLC del producto.** E Es para equipos que quieren sus decisiones de arquitectura aplicadas en CI y no revisadas a mano, para plataformas que bloquean artefactos no conformes antes de producción, y para agentes de IA que necesitan validar su propia salida contra las mismas reglas. -[Pruébalo](#pruébalo-en-dos-minutos) · [Cuatro términos](#cuatro-términos-que-necesitas) · [En CI](#en-ci) · [Qué hay dentro](#qué-hay-dentro) · [Qué no es](#qué-no-es) · [Documentación](#documentación) · [Atlas interactivo](https://beyondnetcode.github.io/evolith_arch32/) +[Pruébalo](#pruébalo-en-dos-minutos) · [Cuatro términos](#cuatro-términos-que-necesitas) · [En CI](#en-ci) · [Qué hay dentro](#qué-hay-dentro) · [Cómo se compara](#cómo-se-compara) · [Qué no es](#qué-no-es) · [Documentación](#documentación) · [Atlas interactivo](https://beyondnetcode.github.io/evolith_arch32/) --- @@ -95,6 +95,26 @@ Cuántas reglas, packs y ADRs carga tu instalación lo imprime `evolith rulesets --- +## Cómo se compara + +Las herramientas a las que uno acude primero comprueban cosas distintas, y las diferencias están en las filas, no en los adjetivos. Verificado contra la documentación de cada herramienta el 2026-09-19; las correcciones son bienvenidas como PR. + +| | ArchUnit | dependency-cruiser | Conftest | Evolith | +|---|---|---|---|---| +| **Qué lee** | Bytecode de la JVM: clases, paquetes, capas | El grafo de imports de módulos JS/TS | Ficheros de configuración estructurados (YAML, JSON, HCL, Dockerfile…) | El repositorio alrededor del código: layout, workflows, manifiestos, ADRs — **no** el AST | +| **Lenguaje de reglas** | DSL fluido en Java, ejecutado como tests unitarios | Configuración JSON/JS (`forbidden` / `allowed`) | Rego | Rego, compilado a Wasm, en packs JSON | +| **Dónde viven las reglas** | En el código, por repositorio | En el repositorio (`.dependency-cruiser.js`) | Un directorio de políticas; compartible con `conftest pull` (git, OCI) | Una biblioteca fuera de los repositorios, adoptada por repositorio con `--select` | +| **Una regla que no se evaluó** | Falla la regla cuyo `should` recibió un conjunto vacío (`failOnEmptyShould`, activo por defecto) | `severity: ignore` la omite en silencio; no existe otro resultado | Sin resultado: un `deny` indefinido es un aprobado | `skipped` es un veredicto de primera clase, contado junto a `passed` y `failed`; un `skipped` **bloqueante** hace fallar la ejecución | +| **Código de salida** | Un test unitario que falla | El número de violaciones `error` | `1` si falla (`0`/`1`/`2` con `--fail-on-warn`) | `0` pasa · `1` falló la herramienta · `2` el gate bloqueó · `3` invocación inválida | +| **Superficies** | Tests Java | CLI (+ grafos de dependencias) | CLI | CLI · GitHub Action · servidor MCP · API REST | +| **Reglas derivadas de ADRs** | — | — | — | Sí: `evolith adr create`, y muchos packs se derivan de decisiones | +| **Alcance de lenguaje** | JVM | JavaScript / TypeScript | Cualquier fichero estructurado | Cualquier repositorio para reglas estructurales, de CI/CD y de ADR; Node/TypeScript para las de dependencias y linters | +| **Licencia** | Apache-2.0 | MIT | Apache-2.0 | MIT | + +Son complementos, no sustitutos: ArchUnit y dependency-cruiser ven *dentro* del código, Conftest ve un fichero cada vez, Evolith ve el repositorio como unidad gobernada y cuenta lo que no pudo decidir. Ejecutar Evolith junto a una de ellas es la configuración prevista. + +--- + ## Qué no es - **No sustituye a ArchUnit, Conftest ni dependency-cruiser; los complementa.** Las reglas viven fuera del código, como datos que gobiernan muchos repositorios y que un agente puede leer. Evolith *es* OPA por debajo y añade la biblioteca de reglas, la derivación de ADR a regla y la contabilidad de cobertura. diff --git a/README.md b/README.md index d46710c74..7f61b648a 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ And it is not only a linter: **the rules are tied to the product's SDLC phase.** It is for engineering teams that want their architecture decisions enforced in CI rather than reviewed by hand, for platform teams blocking non-conformant artifacts before production, and for AI agents that need to validate their own output against the same rules. -[Try it](#try-it-in-two-minutes) · [Four terms](#four-terms-you-need) · [In CI](#in-ci) · [What is inside](#what-is-inside) · [What it is not](#what-it-is-not) · [Documentation](#documentation) · [Interactive atlas](https://beyondnetcode.github.io/evolith_arch32/) +[Try it](#try-it-in-two-minutes) · [Four terms](#four-terms-you-need) · [In CI](#in-ci) · [What is inside](#what-is-inside) · [How it compares](#how-it-compares) · [What it is not](#what-it-is-not) · [Documentation](#documentation) · [Interactive atlas](https://beyondnetcode.github.io/evolith_arch32/) --- @@ -95,6 +95,26 @@ How many rules, packs and ADRs your installation loads is printed by `evolith ru --- +## How it compares + +The tools people reach for first check different things, and the differences are in the rows, not in the adjectives. Verified against each tool's own documentation on 2026-09-19; corrections welcome as a PR. + +| | ArchUnit | dependency-cruiser | Conftest | Evolith | +|---|---|---|---|---| +| **What it reads** | JVM bytecode: classes, packages, layers | The JS/TS module import graph | Structured config files (YAML, JSON, HCL, Dockerfile…) | The repository around the code: layout, workflows, manifests, ADRs — **not** the AST | +| **Rule language** | Java fluent DSL, run as unit tests | JSON/JS config (`forbidden` / `allowed`) | Rego | Rego, compiled to Wasm, in JSON packs | +| **Where rules live** | In the code base, per repository | In the repository (`.dependency-cruiser.js`) | A policy directory; shareable with `conftest pull` (git, OCI) | A library outside the repositories, adopted per repository with `--select` | +| **A rule that did not evaluate** | Fails a rule whose `should` got an empty set (`failOnEmptyShould`, on by default) | `severity: ignore` skips it silently; no other outcome exists | No outcome: an undefined `deny` is a pass | `skipped` is a first-class verdict, counted next to `passed` and `failed`; a **blocking** `skipped` fails the run | +| **Exit code** | A failing unit test | The number of `error` violations | `1` on failure (`0`/`1`/`2` with `--fail-on-warn`) | `0` pass · `1` the tool failed · `2` the gate blocked · `3` invalid invocation | +| **Surfaces** | Java tests | CLI (+ dependency graphs) | CLI | CLI · GitHub Action · MCP server · REST API | +| **Rules derived from ADRs** | — | — | — | Yes: `evolith adr create`, and many packs are derived from decisions | +| **Language scope** | JVM | JavaScript / TypeScript | Any structured file | Any repository for structural, CI/CD and ADR rules; Node/TypeScript for the dependency and linter rules | +| **License** | Apache-2.0 | MIT | Apache-2.0 | MIT | + +They are complements, not substitutes: ArchUnit and dependency-cruiser see *inside* the code, Conftest sees one file at a time, Evolith sees the repository as a governed unit and counts what it could not decide. Running Evolith next to one of them is the intended setup. + +--- + ## What it is not - **Not a replacement for ArchUnit, Conftest or dependency-cruiser; it complements them.** Rules live outside the codebase, as data that governs many repositories and that an agent can read. Evolith *is* OPA underneath, and adds the rule library, the ADR-to-rule derivation and the coverage accounting. diff --git a/reference/core/control-center/evidence/gap-closure-evidence.json b/reference/core/control-center/evidence/gap-closure-evidence.json index 278ab5828..0eb38f274 100644 --- a/reference/core/control-center/evidence/gap-closure-evidence.json +++ b/reference/core/control-center/evidence/gap-closure-evidence.json @@ -10671,6 +10671,51 @@ "node .harness/scripts/ci/09-reconcile-maturity.mjs --check # still green on the same tree; the warning band is empty today", "node .harness/scripts/ci/40-validate-path-literals.mjs # the workflow's run: body resolves" ] + }, + { + "id": "GT-712", + "closedAt": "2026-09-19", + "closureCommit": "cb2c8a1e", + "dependencyDisposition": "none", + "evidence": [ + "src/packages/core-domain/src/domain/interfaces.ts", + "src/packages/infra-providers/src/architecture/nx-workspace.strategy.ts", + "src/packages/mcp-server/src/tools/scaffold.tool.ts", + "src/packages/mcp-server/src/tools/scaffold.tool.spec.ts", + "src/apps/core-api/src/application/services/workspace-reference-resolver.service.ts", + "src/apps/core-api/src/presentation/controllers/evaluation.controller.ts", + "src/apps/core-api/src/presentation/controllers/evaluation.controller.spec.ts", + "src/apps/core-api/src/presentation/controllers/architecture.controller.ts", + "src/packages/core-domain/src/application/use-cases/initialize-project.use-case.ts", + "src/packages/core-domain/src/application/services/project-scaffolder.service.ts", + "src/packages/mcp-server/src/tools/config.tools.ts", + "src/packages/mcp-server/src/tools/config.tools.spec.ts" + ], + "validationCommands": [ + "THE COUNT IS CODEQL'S, NOT OURS: the /language:javascript-typescript analysis of main at c5547114 (sdk-cli-ci run 35445364556, dispatched by hand -- see GT-713) reports 37 results and 0 open alerts; the Scorecard run dispatched the same afternoon (35445777316) closed the 7 Token-Permissions alerts; Dependabot and secret-scanning were already at 0. The 58 dismissals each carry a dismissed_comment naming why (false positive / used in tests / won't fix).", + "THREE COMMITS, ONE CLOSURE: cb2c8a1e (#725) fixed the 37; 8920b140 (#737) rewrote three guards into the shapes CodeQL models (path.resolve + one startsWith; includes('..') + isAbsolute on the same variable; a literal === '__proto__' at the write) after the main run kept 10 open on containment it did not credit; b200c5cb (#748) replaced the .NET scaffolder's second read of input.name with path.basename(projectDir). Promoted in #726, #739, #752.", + "MEASURED, NOT ASSUMED: the former /\\/+$/ on 200 000 slashes took 15 326 ms in node before the change; the char-based trim is O(n). parseRepoUrl was checked against https, .git, git@…: and ssh:// forms (accepted) and evil.example/github.com/…, github.com.evil (rejected).", + "npx jest --config src/packages/mcp-server/jest.config.js --runInBand src/packages/mcp-server/src/tools/scaffold.tool.spec.ts src/packages/mcp-server/src/tools/config.tools.spec.ts # 12 tests: every execFile call is npx/npm with the exact argv; 'api; rm -rf /' and '../../escape' refused before any nx g; __proto__/constructor/prototype refused with the file untouched", + "npx jest --config src/apps/core-api/jest.config.js --runInBand src/apps/core-api/src/presentation/controllers/evaluation.controller.spec.ts src/apps/core-api/src/presentation/controllers/architecture.controller.spec.ts # /etc, ../outside, /workspaces/../etc, /workspacesX/sat, /somewhere/else are 400 before the use case; the manifest's paths are pinned; no resolver fails closed", + "npx jest --config src/packages/core-domain/jest.config.js --runInBand src/packages/core-domain/src/application/use-cases/initialize-project.use-case.spec.ts # ../escape, a/b, .., .hidden, -flag, NUL and '' refused as project names with nothing written; Billing.Api -> Billing.Api.csproj", + "npx tsc -b tsconfig.json # clean on the 11 projects", + "node .harness/scripts/ci/40-validate-path-literals.mjs # the seven workflows still resolve every path they name" + ] + }, + { + "id": "GT-713", + "closedAt": "2026-09-19", + "closureCommit": "72aceb70", + "dependencyDisposition": "none", + "evidence": [ + ".github/workflows/sdk-cli-ci.yml" + ], + "validationCommands": [ + "THE GAP WAS OBSERVED TWICE THE SAME AFTERNOON: promotions 19d736da (12:25) and c5547114 (13:15) reached main with no push run of sdk-cli-ci (their changes were under src/packages and src/apps only), no /language:javascript-typescript analysis for refs/heads/main was recorded for either SHA, and the tab kept reporting alerts on code that was gone; the pull_request run on the same head (PR #740) uploaded to refs/pull/740/merge with results=0 because PR analyses are diff-informed. Both times `gh workflow run sdk-cli-ci.yml --ref main` produced the analysis (runs 35443436054 and 35445364556) and the alerts closed.", + "WHAT IS NOT CLAIMED: that the widened filter fired on a real promotion. The first push to main after this lands that changes src/packages/** or src/apps/** is what confirms it -- a push-event run of 'Evolith SDK CLI - CI Pipeline' on that SHA and a refs/heads/main CodeQL analysis with that commit_sha, without a dispatch. The promotion carrying this change touches the workflow file itself, which was already in the filter, so it proves nothing about the new paths.", + "node -e \"const y=require('yaml');const d=y.parse(require('fs').readFileSync('.github/workflows/sdk-cli-ci.yml','utf8'));const p=d.on.push.paths;if(!p.includes('src/packages/**')||!p.includes('src/apps/**')||d.on.pull_request.paths)process.exit(1);console.log(p.join(' '))\" # push filter spans every tree CodeQL scans; the PR trigger stays unfiltered", + "node .harness/scripts/ci/40-validate-path-literals.mjs # the workflow's paths resolve" + ] } ] } diff --git a/reference/core/control-center/gaps/gap-reference-catalog.es.md b/reference/core/control-center/gaps/gap-reference-catalog.es.md index 5c857753c..a98a80442 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.es.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.es.md @@ -10154,3 +10154,77 @@ Los dos se arreglaron de forma estructural y no como correcciones: el rethrow no - [x] **FALSABILIDAD:** el camino rojo se observó, no se supuso. **CUMPLIDO** — `--freshness --now=2026-10-13` imprime cuatro líneas `turns stale on 2026-10-20 (6 day(s) left)` y sale 1; `--now=2026-10-20` imprime cuatro líneas `STALE since 2026-10-20` y sale 1; hoy sale 0. - [x] Lo que aún no puede observarse queda escrito como tal, no reclamado: la ejecución programada abriendo la issue es observable por primera vez el 2026-10-13 (primer día dentro de la banda para la evidencia observada el 2026-09-19) y cerrándola tras la re-observación. **CUMPLIDO como declaración de lo que NO se reclama** — la fecha consta aquí y en el registro de cierre; los pasos de issue son los del canary publicado (GT-671), con el mismo estado de aún-no-disparado. - **Estado:** `COMPLETADO` + +#### GT-712 + +**Título:** De las 95 alertas de la pestaña Security, 37 eran reales: una línea de shell construida con entrada del tool MCP, rutas de fichero tomadas tal cual de tres cuerpos HTTP, un escritor de configuración que llegaba a `Object.prototype`, ocho regex polinómicos y siete workflows con un token por defecto con escritura + +- **Propósito:** Arreglar en el origen lo que un agente manejando el tool de scaffold del MCP o un cliente REST autenticado podía explotar de verdad, y dejar la pestaña con cero alertas abiertas y un motivo escrito en cada descartada, para que el próximo triaje empiece por el código y no por `per_page=100`. +- **Evidencia, medida el 2026-09-19 contra el SARIF del último análisis de CodeQL sobre `main` (`GET …/code-scanning/analyses/{id}` con `Accept: application/sarif+json`; sus `codeFlows` nombran el origen de cada taint):** + + | hecho | valor | + |---|---| + | abiertas en la pestaña | 95 (75 CodeQL, 20 Scorecard); Dependabot 0, secret-scanning 0 | + | CWE-78 (11) | `NxWorkspaceStrategy` construía `npx nx g @nx/${fw}:host --name=${name} --remotes=${remotes} --directory=apps/${name}` y lo ejecutaba con `execAsync`; `name`, `remotes` y `domains` llegaban sin validar del tool MCP `evolith-scaffold` (`apiName`, `hostName`, `remotes`, `domains`); `git-log-reader` interpolaba `--since` | + | CWE-22 (9 sinks en `node-filesystem.provider.ts`) | orígenes `evaluation.controller.ts:168-169` (`body.satellitePath`, `body.corePath`, rama legacy, directos al pipeline que lista y lee directorios bajo ambos), `architecture.controller.ts:125` (`body.manifest.satellitePath/corePath` pisan el `workspaceRef` resuelto dentro del caso de uso), `projects.controller.ts:26` (`body.name` → `${cwd}/${name}`) | + | CWE-1321 (1) | `ConfigToolService.setConfig` recorría `key.split('.')` por el documento sin guardar `__proto__`/`constructor`/`prototype` | + | ReDoS (8) | `/\/+$/` ×5, `/=+$/`, `/^[_\-./]+|[_\-./]+$/`, y un `github\.com[/:]…` sin anclar — el primero medido en **15 326 ms** con 200 000 barras | + | Scorecard Token-Permissions (7) | `ci-cd.yml`, `docker-images.yml` (`packages: write` de nivel superior), `docs-release.yml`, `enforce-root-cleanliness.yml`, `reliability.yml`, `sdk-cli-ci.yml`, `sdk-cli-release.yml` sin un `permissions:` de solo lectura arriba | + | no reales (58) | `insufficient-password-hash` ×9 (SHA-256 como normalización de longitud antes de `timingSafeEqual`), `user-controlled-bypass` ×3 (cadena de autenticación, tiempo constante), `clear-text-logging` (el flujo tainta `options` por llevar `apiKey`; lo que se registra es `correlationId`), `http-to-file-access`/`file-system-race`/`insecure-temporary-file` (JSON del registro, caché por mtime, rutas elegidas por el usuario, tests), `path-injection` ×7 en un spec de integración, `unused-local-variable` ×13, `unneeded-defensive-code` ×2, `.wasm` ×8 (compilados del Rego contiguo, paridad en `opa-parity.yml`), `npm install -g` ×2 de nuestra propia versión exacta, Fuzzing/CII/Code-Review | + +- **Lo que lo cierra:** + - `cb2c8a1e` ([#725](https://github.com/beyondnetcode/evolith_arch32/pull/725)): `ICommandExecutor` gana `executeFile`/`executeFileOrThrow` (argv, sin shell), implementados por el `CommandExecutor` del CLI y el `NodeCommandExecutor` del MCP; `NxWorkspaceStrategy` construye argv y rechaza cualquier nombre fuera de `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` antes de lanzar nada (`--directory=apps/../..` y `--name=--flag` incluidos); `git-log-reader` usa `execFile`. `WorkspaceReferenceResolverService.resolveLegacyPath`/`resolveCorePathOverride` contienen las rutas legacy, `EvaluationController` falla cerrado sin resolver, `ArchitectureController` fija las rutas del manifest a las resueltas, `InitProjectDto.name` y `InitializeProjectUseCase` aceptan un solo segmento de directorio. `ConfigToolService` rechaza los tres segmentos de prototipo y solo desciende por objetos planos propios. Los ocho regex pasan a recortes carácter a carácter; `parseRepoUrl` queda anclado al host. Los siete workflows declaran `contents: read` arriba y las escrituras `packages`/`contents`/`pull-requests` en los jobs que las necesitan. 58 alertas descartadas, cada una con su motivo. + - `8920b140` ([#737](https://github.com/beyondnetcode/evolith_arch32/pull/737)): la corrida de CodeQL sobre `main` cerró 20 de 37 y mantuvo 10 — la contención era real pero estaba escrita en formas que el motor no modela. Reescritas a sus formas canónicas: `path.resolve` + UN `startsWith(root + sep)` (la compuesta `resolved !== root && …` no se acreditaba), `includes('..')` + `path.isAbsolute` sobre la misma variable (un regex solo no es barrera), una comparación literal `=== '__proto__'` en la escritura (un `Set` en otra función no lo es). + - `b200c5cb` ([#748](https://github.com/beyondnetcode/evolith_arch32/pull/748)): la última — el scaffolder .NET releía `input.name` como propiedad para `${projectDir}/${input.name}.csproj`; ahora toma `path.basename(projectDir)`, la misma cadena, ya saneada para el motor. +- **Casos de uso:** + - Un agente llama a `evolith-scaffold` con `apiName: "api; rm -rf /"`; antes, el shell lo ejecutaba; ahora el tool responde `Invalid API app name` y no se lanza nada. + - Un Tracker con clave válida envía `POST /evaluate {satellitePath: "/etc"}`; antes, el Core listaba y leía `/etc`; ahora responde 400 `satellitePath resolves outside the workspace root; send an opaque workspaceRef instead`. +- **Impacto:** Ejecución remota de comandos alcanzable desde la superficie MCP y lectura de directorios arbitrarios alcanzable desde la superficie REST, ambas detrás de credenciales que el despliegue entrega a sus propios clientes. +- **Resultado esperado:** Cero alertas abiertas en `main` con cada descarte llevando su motivo, y las tres superficies (CLI, MCP, REST) sin cambio para entrada válida. +- **Ficheros afectados:** `src/packages/core-domain/src/domain/interfaces.ts`, `src/packages/infra-providers/src/architecture/nx-workspace.strategy.ts`, `src/packages/mcp-server/src/tools/scaffold.tool.ts`, `src/sdk/cli/src/infrastructure/cli/command-executor.ts`, `src/packages/core-domain/src/domain/metrics/git-log-reader.ts`, `src/apps/core-api/src/application/services/workspace-reference-resolver.service.ts`, `src/apps/core-api/src/presentation/controllers/evaluation.controller.ts`, `src/apps/core-api/src/presentation/controllers/architecture.controller.ts`, `src/apps/core-api/src/presentation/dtos/projects.dto.ts`, `src/apps/core-api/src/presentation/dtos/satellite-manifest.dto.ts`, `src/packages/core-domain/src/application/use-cases/initialize-project.use-case.ts`, `src/packages/core-domain/src/application/services/project-scaffolder.service.ts`, `src/packages/mcp-server/src/tools/config.tools.ts`, ocho sitios de regex, siete workflows bajo `.github/workflows/` +- **Componente:** `Core` · **Criticidad:** P0 · **Complejidad:** M +- **Principal:** `M` · **Interés:** `HIGH` · **Base:** `estimate` +- **Procedencia:** Registrado el 2026-09-19 a partir de la petición del owner de revisar la pestaña Security, arreglar lo importante y descartar el resto; el triaje anterior del 2026-09-15 (GT-709/GT-710) había cerrado las mitades de dependencias y pines y dejado los hallazgos de CodeQL en `src` y Token-Permissions como el siguiente valor. +- **Criterios de aceptación:** + - [x] No queda ninguna línea de shell construida con entrada del llamador: la estrategia y el lector de git lanzan con argv. **CUMPLIDO** — el spec del MCP afirma que cada llamada `execFile` registrada es `npx`/`npm` con el argv exacto, y que `api; rm -rf /` y `../../escape` se rechazan antes de cualquier `nx g`. + - [x] Ninguna ruta de un cuerpo HTTP llega al sistema de ficheros fuera de `WORKSPACE_ROOT`/`CORE_PATH`. **CUMPLIDO** — `/etc`, `../outside`, `/workspaces/../etc`, `/workspacesX/sat` y `/somewhere/else` son 400 antes de que corra el caso de uso; las rutas del manifest se sobrescriben; `../escape`, `a/b`, `..`, `.hidden`, `-flag`, un byte NUL y `""` se rechazan como nombre de proyecto sin escribir nada. + - [x] Una clave con ruta de puntos no puede llegar a `Object.prototype`. **CUMPLIDO** — `__proto__.polluted`, `product.constructor.prototype.polluted` y `product..phase` se rechazan y el fichero queda intacto. + - [x] Los regex son lineales. **CUMPLIDO** — el viejo `/\/+$/` medido en 15 326 ms con 200 000 barras; los recortes carácter a carácter son O(n); `parseRepoUrl` sigue aceptando las formas https, `.git`, `git@…:` y `ssh://` y rechaza `evil.example/github.com/…` y `github.com.evil`. + - [x] Cada workflow arranca en solo lectura y cada escritura está declarada en su job. **CUMPLIDO** — parseado con `yaml`: siete `contents: read` de nivel superior; `packages: write` en `docker-images/build` y `ci-cd/docker-services`, `contents: write` en `docs-release/update-version-log`, `docs-release/create-release` y `sdk-cli-release/upload-assets`, `pull-requests: read` en `ci-cd/governance-guards` para el `gh pr list/diff` del guard 50. + - [x] **FALSABILIDAD:** el recuento es el de CodeQL sobre `main`, no una afirmación local. **CUMPLIDO** — análisis de `c5547114` (`sdk-cli-ci` lanzado a mano, ver GT-713): 37 resultados, 0 abiertas; Scorecard lanzado la misma tarde: 0 abiertas; Dependabot 0; secret-scanning 0. + - [x] Nada válido cambió de comportamiento. **CUMPLIDO** — `tsc -b` limpio en los 11 proyectos; las suites tocadas: core-domain 75+, mcp-server 12 (+2), core-api 71 (+4), agent-runtime 85, infra-providers 21, CLI 133; ESLint sobre los ficheros tocados muestra los mismos 8 errores preexistentes `max-lines`/`max-params`/`complexity` que `develop` y ninguno nuevo. +- **Estado:** `COMPLETADO` + +#### GT-713 + +**Título:** El análisis al que están ligadas las alertas de la pestaña Security solo lo produce la corrida `push` de `sdk-cli-ci.yml`, y su filtro de rutas saltaba casi todo el código + +- **Propósito:** Que una promoción de código a `main` re-analice `main`, para que la pestaña describa el commit que está ahí y no el anterior. +- **Evidencia, medida el 2026-09-19 contra la API de code-scanning y el historial de Actions:** + + | hecho | valor | + |---|---| + | a qué análisis siguen las alertas | categoría `/language:javascript-typescript`, subida por el job `CodeQL SAST` de `sdk-cli-ci.yml` vía `github/codeql-action/analyze` | + | qué hace una corrida de pull request | diff-informed: `refs/pull/737/merge` analizado con `results=0` mientras `main` aún tenía 55 — los resultados fuera del diff se recortan y las alertas de la rama nunca se mueven | + | qué es "Code Quality: Push on main" | la suite de la configuración por defecto, otra categoría; corrió verde en `19d736da` mientras la pestaña seguía en 10 alertas | + | el filtro de `push` | `src/sdk/cli/**`, `.harness/**`, `package.json`, `package-lock.json`, el propio workflow — a propósito, para ahorrar minutos en pushes, que los checks requeridos no gatean | + | lo que se coló | la promoción `19d736da` (12:25, cambios solo bajo `src/packages`, `src/apps`, `.github`) y `c5547114` (13:15, solo `src/packages/core-domain`): sin corrida push, sin análisis, pestaña desactualizada | + | qué cerró las alertas cada vez | `gh workflow run sdk-cli-ci.yml --ref main`, a mano, 25 y 10 minutos después del merge | + | la última vez que funcionó solo | `50121746` (11:20) — porque #725 había tocado `src/sdk/cli/**` | + +- **Lo que lo cierra, en `72aceb70`:** el filtro de `push` añade `src/packages/**` y `src/apps/**`, los árboles que CodeQL escanea. El filtro se mantiene: un push solo de documentación sigue saltándose los 13 jobs, y el trigger de PR no cambia (ya sin filtro desde el bloqueo de #218). El comentario del trigger deja escrito por qué existe el filtro Y por qué tiene que cubrir cada árbol escaneado, para que el siguiente lector no lo vuelva a estrechar. +- **Casos de uso:** + - Una promoción que arregla un hallazgo de CodeQL en `src/packages` llega a `main`; la pestaña cierra la alerta en la siguiente corrida push sin que nadie la lance. + - Una regresión introducida bajo `src/apps` llega a `main`; la pestaña la reporta en ese commit, no en el siguiente cambio del CLI. +- **Impacto:** La pestaña Security de `main` describía el commit anterior tras la mayoría de las promociones de código; el 2026-09-19 mostró 10 alertas durante 35 minutos sobre código ya arreglado y promovido. +- **Resultado esperado:** Cada push a `main`/`develop` que cambie código escaneado produce un análisis `refs/heads/`; el lanzamiento manual deja de ser parte de una promoción. +- **Ficheros afectados:** `.github/workflows/sdk-cli-ci.yml` +- **Componente:** `Infra` · **Criticidad:** P2 · **Complejidad:** XS +- **Principal:** `XS` · **Interés:** `MED` · **Base:** `estimate` +- **Procedencia:** Registrado el 2026-09-19 al cerrar GT-712: tras la promoción #739 la pestaña seguía mostrando las 10 alertas que #737 había arreglado; la corrida del CLI CI sobre la cabeza de `main` era un evento `pull_request` (PR #740, cabeza `main`), su análisis había caído en `refs/pull/740/merge` con `results=0`, y no existía corrida `push` para el commit. +- **Criterios de aceptación:** + - [x] El filtro de push cubre cada árbol que CodeQL escanea. **CUMPLIDO** — `on.push.paths` parseado con `yaml`: `src/sdk/cli/**`, `src/packages/**`, `src/apps/**`, `.harness/**`, `package.json`, `package-lock.json`, `.github/workflows/sdk-cli-ci.yml`. + - [x] Un push solo de documentación sigue saltándose la corrida. **CUMPLIDO** — `reference/**`, `README*` y los `.github/workflows/*.yml` distintos de este no están en el filtro. + - [x] El trigger de pull request no se toca. **CUMPLIDO** — `on.pull_request` no tiene `paths`, como exigió #218. + - [x] **FALSABILIDAD:** lo que aún no puede observarse queda escrito como tal, no reclamado. **CUMPLIDO como declaración de lo que NO se reclama** — el primer push a `main` tras esto que cambie `src/packages/**` o `src/apps/**` debe mostrar una corrida con evento `push` de `Evolith SDK CLI - CI Pipeline` sobre ese SHA y un análisis de CodeQL `refs/heads/main` con ese `commit_sha`, sin lanzamiento manual. El propio fichero del workflow está en el filtro, así que la promoción que lleve este cambio correrá, pero eso prueba la ruta del fichero, no las nuevas; la fila se cierra por el cambio del filtro y la primera promoción solo de `src/packages` es lo que lo confirma. Consta aquí y en el registro de cierre. +- **Estado:** `COMPLETADO` diff --git a/reference/core/control-center/gaps/gap-reference-catalog.md b/reference/core/control-center/gaps/gap-reference-catalog.md index 6d8fd1568..d0c66410d 100644 --- a/reference/core/control-center/gaps/gap-reference-catalog.md +++ b/reference/core/control-center/gaps/gap-reference-catalog.md @@ -10247,3 +10247,77 @@ Both were fixed structurally rather than corrected: the rethrow now names BOTH f - [x] **FALSIFIABILITY:** the red path was observed, not assumed. **MET** — `--freshness --now=2026-10-13` prints four `turns stale on 2026-10-20 (6 day(s) left)` lines and exits 1; `--now=2026-10-20` prints four `STALE since 2026-10-20` lines and exits 1; today exits 0. - [x] What cannot be observed yet is written down as such, not claimed: the scheduled run opening the issue is first observable on 2026-10-13 (the first day inside the band for the evidence observed 2026-09-19) and closing it after the re-observation. **MET as a statement of what is NOT claimed** — the date is recorded here and in the closure record; the issue steps are the published canary's (GT-671), which have the same not-yet-fired status. - **Status:** `DONE` + +#### GT-712 + +**Title:** Of the 95 alerts on the Security tab, 37 were real: a shell line built from MCP tool input, filesystem paths taken verbatim from three HTTP bodies, a config writer that reached `Object.prototype`, eight polynomial regexes and seven workflows with a write-capable default token + +- **Purpose:** Fix at the source what an agent driving the MCP scaffold tool or an authenticated REST client could actually exploit, and leave the tab with zero open alerts and a written reason on every dismissed one, so the next triage starts from the code and not from `per_page=100`. +- **Evidence, measured 2026-09-19 against the SARIF of the last CodeQL analysis on `main` (`GET …/code-scanning/analyses/{id}` with `Accept: application/sarif+json`, its `codeFlows` name the source of every taint):** + + | fact | value | + |---|---| + | open on the tab | 95 (75 CodeQL, 20 Scorecard); Dependabot 0, secret-scanning 0 | + | CWE-78 (11) | `NxWorkspaceStrategy` built `npx nx g @nx/${fw}:host --name=${name} --remotes=${remotes} --directory=apps/${name}` and ran it through `execAsync`; `name`, `remotes` and `domains` came unvalidated from the MCP tool `evolith-scaffold` (`apiName`, `hostName`, `remotes`, `domains`); `git-log-reader` interpolated `--since` | + | CWE-22 (9 sinks in `node-filesystem.provider.ts`) | sources `evaluation.controller.ts:168-169` (`body.satellitePath`, `body.corePath`, legacy branch, straight to the pipeline that lists and reads directories under both), `architecture.controller.ts:125` (`body.manifest.satellitePath/corePath` override the resolved `workspaceRef` inside the use case), `projects.controller.ts:26` (`body.name` → `${cwd}/${name}`) | + | CWE-1321 (1) | `ConfigToolService.setConfig` walked `key.split('.')` into the document with no guard on `__proto__`/`constructor`/`prototype` | + | ReDoS (8) | `/\/+$/` ×5, `/=+$/`, `/^[_\-./]+|[_\-./]+$/`, and an unanchored `github\.com[/:]…` — the first measured at **15 326 ms** on 200 000 slashes | + | Scorecard Token-Permissions (7) | `ci-cd.yml`, `docker-images.yml` (top-level `packages: write`), `docs-release.yml`, `enforce-root-cleanliness.yml`, `reliability.yml`, `sdk-cli-ci.yml`, `sdk-cli-release.yml` without a read-only top-level `permissions:` | + | not real (58) | `insufficient-password-hash` ×9 (SHA-256 as length normalisation before `timingSafeEqual`), `user-controlled-bypass` ×3 (auth chain, constant-time), `clear-text-logging` (the flow taints `options` for carrying `apiKey`; what is logged is `correlationId`), `http-to-file-access`/`file-system-race`/`insecure-temporary-file` (registry JSON, mtime cache, user-chosen paths, tests), `path-injection` ×7 in an integration spec, `unused-local-variable` ×13, `unneeded-defensive-code` ×2, `.wasm` ×8 (compiled from the adjacent Rego, parity in `opa-parity.yml`), `npm install -g` ×2 of our own exact version, Fuzzing/CII/Code-Review | + +- **What closes it:** + - `cb2c8a1e` ([#725](https://github.com/beyondnetcode/evolith_arch32/pull/725)): `ICommandExecutor` gains `executeFile`/`executeFileOrThrow` (argv, no shell), implemented by the CLI `CommandExecutor` and the MCP `NodeCommandExecutor`; `NxWorkspaceStrategy` builds argv and rejects any name outside `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$` before spawning (`--directory=apps/../..` and `--name=--flag` included); `git-log-reader` uses `execFile`. `WorkspaceReferenceResolverService.resolveLegacyPath`/`resolveCorePathOverride` contain the legacy paths, `EvaluationController` fails closed without a resolver, `ArchitectureController` pins the manifest's paths to the resolved ones, `InitProjectDto.name` and `InitializeProjectUseCase` accept one directory segment. `ConfigToolService` refuses the three prototype segments and only descends into own plain objects. The eight regexes become char-based trims; `parseRepoUrl` is anchored to the host. The seven workflows declare `contents: read` at the top and `packages`/`contents`/`pull-requests` writes on the jobs that need them. 58 alerts dismissed with a reason each. + - `8920b140` ([#737](https://github.com/beyondnetcode/evolith_arch32/pull/737)): the CodeQL run on `main` closed 20 of 37 and kept 10 — the containment was real but written in shapes the engine does not model. Rewritten to its canonical ones: `path.resolve` + ONE `startsWith(root + sep)` (the compound `resolved !== root && …` was not credited), `includes('..')` + `path.isAbsolute` on the same variable (a regex alone is not a barrier), a literal `=== '__proto__'` comparison at the write (a `Set` in another function is not). + - `b200c5cb` ([#748](https://github.com/beyondnetcode/evolith_arch32/pull/748)): the last one — the .NET scaffolder re-read `input.name` as a property for `${projectDir}/${input.name}.csproj`; it now takes `path.basename(projectDir)`, the same string, already sanitised for the engine. +- **Use cases:** + - An agent calls `evolith-scaffold` with `apiName: "api; rm -rf /"`; before, the shell ran it; now the tool answers `Invalid API app name` and nothing is spawned. + - A Tracker with a valid key sends `POST /evaluate {satellitePath: "/etc"}`; before, the Core listed and read `/etc`; now it answers 400 `satellitePath resolves outside the workspace root; send an opaque workspaceRef instead`. +- **Impact:** Remote command execution reachable from the MCP surface and arbitrary directory reads reachable from the REST surface, both behind credentials the deployment hands to its own clients. +- **Expected outcome:** Zero open alerts on `main` with every dismissal carrying its reason, and the three surfaces (CLI, MCP, REST) unchanged for valid input. +- **Files affected:** `src/packages/core-domain/src/domain/interfaces.ts`, `src/packages/infra-providers/src/architecture/nx-workspace.strategy.ts`, `src/packages/mcp-server/src/tools/scaffold.tool.ts`, `src/sdk/cli/src/infrastructure/cli/command-executor.ts`, `src/packages/core-domain/src/domain/metrics/git-log-reader.ts`, `src/apps/core-api/src/application/services/workspace-reference-resolver.service.ts`, `src/apps/core-api/src/presentation/controllers/evaluation.controller.ts`, `src/apps/core-api/src/presentation/controllers/architecture.controller.ts`, `src/apps/core-api/src/presentation/dtos/projects.dto.ts`, `src/apps/core-api/src/presentation/dtos/satellite-manifest.dto.ts`, `src/packages/core-domain/src/application/use-cases/initialize-project.use-case.ts`, `src/packages/core-domain/src/application/services/project-scaffolder.service.ts`, `src/packages/mcp-server/src/tools/config.tools.ts`, eight regex sites, seven workflows under `.github/workflows/` +- **Component:** `Core` · **Criticality:** P0 · **Complexity:** M +- **Principal:** `M` · **Interest:** `HIGH` · **Basis:** `estimate` +- **Provenance:** Registered 2026-09-19 from the owner's request to review the Security tab, fix what mattered and dismiss the rest; the previous triage of 2026-09-15 (GT-709/GT-710) had closed the dependency and pinning halves and left the CodeQL findings in `src` and Token-Permissions as the next value. +- **Acceptance criteria:** + - [x] Every shell line built from caller input is gone: the strategy and the git reader spawn with argv. **MET** — the MCP spec asserts every recorded `execFile` call is `npx`/`npm` with the exact argv, and that `api; rm -rf /` and `../../escape` are refused before any `nx g` call. + - [x] No HTTP body path reaches the filesystem outside `WORKSPACE_ROOT`/`CORE_PATH`. **MET** — `/etc`, `../outside`, `/workspaces/../etc`, `/workspacesX/sat` and `/somewhere/else` are 400 before the use case runs; the manifest's paths are overwritten; `../escape`, `a/b`, `..`, `.hidden`, `-flag`, a NUL byte and `""` are refused as project names with nothing written. + - [x] A dot-path key cannot reach `Object.prototype`. **MET** — `__proto__.polluted`, `product.constructor.prototype.polluted` and `product..phase` are refused and the file is untouched. + - [x] The regexes are linear. **MET** — the old `/\/+$/` measured at 15 326 ms on 200 000 slashes; the char-based trims are O(n); `parseRepoUrl` still accepts the https, `.git`, `git@…:` and `ssh://` forms and rejects `evil.example/github.com/…` and `github.com.evil`. + - [x] Every workflow starts read-only and each write is declared on its job. **MET** — parsed with `yaml`: seven top-level `contents: read`; `packages: write` on `docker-images/build` and `ci-cd/docker-services`, `contents: write` on `docs-release/update-version-log`, `docs-release/create-release` and `sdk-cli-release/upload-assets`, `pull-requests: read` on `ci-cd/governance-guards` for guard 50's `gh pr list/diff`. + - [x] **FALSIFIABILITY:** the count is CodeQL's on `main`, not a local claim. **MET** — analysis of `c5547114` (`sdk-cli-ci` dispatched by hand, see GT-713): 37 results, 0 open; Scorecard dispatched the same afternoon: 0 open; Dependabot 0; secret-scanning 0. + - [x] Nothing valid changed behaviour. **MET** — `tsc -b` clean on the 11 projects; the touched suites: core-domain 75+, mcp-server 12 (+2), core-api 71 (+4), agent-runtime 85, infra-providers 21, CLI 133; ESLint on the touched files shows the same 8 pre-existing `max-lines`/`max-params`/`complexity` errors as `develop` and none new. +- **Status:** `DONE` + +#### GT-713 + +**Title:** The analysis that the Security tab's alerts are keyed to is produced only by the `push` run of `sdk-cli-ci.yml`, and its path filter skipped most of the code + +- **Purpose:** Make a code promotion to `main` re-analyse `main`, so the tab describes the commit that is there and not the previous one. +- **Evidence, measured 2026-09-19 against the code-scanning API and the Actions history:** + + | fact | value | + |---|---| + | which analysis the alerts follow | category `/language:javascript-typescript`, uploaded by the `CodeQL SAST` job of `sdk-cli-ci.yml` via `github/codeql-action/analyze` | + | what a pull-request run does | diff-informed: `refs/pull/737/merge` analysed with `results=0` while `main` still held 55 — results outside the diff are pruned and the branch's alerts never move | + | what "Code Quality: Push on main" is | the default-setup suite, a different category; it ran green on `19d736da` while the tab stayed at 10 alerts | + | the `push` filter | `src/sdk/cli/**`, `.harness/**`, `package.json`, `package-lock.json`, the workflow file — by design, to save minutes on pushes, which required checks do not gate | + | what fell through | promotion `19d736da` (12:25, changes only under `src/packages`, `src/apps`, `.github`) and `c5547114` (13:15, `src/packages/core-domain` only): no push run, no analysis, tab stale | + | what closed the alerts each time | `gh workflow run sdk-cli-ci.yml --ref main`, by hand, 25 and 10 minutes after the merge | + | last time it did work by itself | `50121746` (11:20) — because #725 had touched `src/sdk/cli/**` | + +- **What closes it, in `72aceb70`:** the `push` filter adds `src/packages/**` and `src/apps/**`, the trees CodeQL scans. The filter stays: a docs-only push still skips the 13 jobs, and the PR trigger is unchanged (already unfiltered since #218's deadlock). The comment on the trigger records why the filter exists AND why it must cover every scanned tree, so the next reader does not narrow it back. +- **Use cases:** + - A promotion that fixes a CodeQL finding in `src/packages` reaches `main`; the tab closes the alert on the next push run without anyone dispatching it. + - A regression introduced under `src/apps` reaches `main`; the tab reports it on that commit, not on the next CLI change. +- **Impact:** The Security tab on `main` described the previous commit after most code promotions; on 2026-09-19 it showed 10 alerts for 35 minutes on code that had been fixed and promoted. +- **Expected outcome:** Every push to `main`/`develop` that changes scanned code produces a `refs/heads/` analysis; manual dispatch is no longer part of a promotion. +- **Files affected:** `.github/workflows/sdk-cli-ci.yml` +- **Component:** `Infra` · **Criticality:** P2 · **Complexity:** XS +- **Principal:** `XS` · **Interest:** `MED` · **Basis:** `estimate` +- **Provenance:** Registered 2026-09-19 while closing GT-712: after promotion #739 the tab still showed the 10 alerts #737 had fixed; the CLI CI run on `main`'s head was a `pull_request` event (PR #740, head `main`), its analysis had landed on `refs/pull/740/merge` with `results=0`, and no `push` run existed for the commit. +- **Acceptance criteria:** + - [x] The push filter covers every tree CodeQL scans. **MET** — `on.push.paths` parsed with `yaml`: `src/sdk/cli/**`, `src/packages/**`, `src/apps/**`, `.harness/**`, `package.json`, `package-lock.json`, `.github/workflows/sdk-cli-ci.yml`. + - [x] A docs-only push still skips the run. **MET** — `reference/**`, `README*` and `.github/workflows/*.yml` other than this one are not in the filter. + - [x] The pull-request trigger is untouched. **MET** — `on.pull_request` has no `paths`, as #218 required. + - [x] **FALSIFIABILITY:** what cannot be observed yet is written down as such, not claimed. **MET as a statement of what is NOT claimed** — the first push to `main` after this lands that changes `src/packages/**` or `src/apps/**` must show a `push`-event run of `Evolith SDK CLI - CI Pipeline` on that SHA and a `refs/heads/main` CodeQL analysis with that `commit_sha`, with no manual dispatch. This workflow file itself is in the filter, so the promotion carrying this change will run, but that proves the file path, not the new ones; the row closes on the filter change and the first `src/packages`-only promotion is what confirms it. Recorded here and in the closure record. +- **Status:** `DONE` diff --git a/reference/core/control-center/gaps/gap-tracking.es.md b/reference/core/control-center/gaps/gap-tracking.es.md index 6f0c726c1..49a14973a 100644 --- a/reference/core/control-center/gaps/gap-tracking.es.md +++ b/reference/core/control-center/gaps/gap-tracking.es.md @@ -4,6 +4,7 @@ **Estado:** Seguimiento Activo **Responsable:** Evolith Architecture Board +**Última Actualización:** 2026-09-19 (**Dos gaps registrados y cerrados en una tarde sobre la pestaña Security: las 37 alertas que eran reales, y la razón por la que la pestaña las seguía mostrando después de arregladas.** `GT-712` → COMPLETADO, `GT-713` → COMPLETADO. La pestaña tenía 95 alertas abiertas (75 CodeQL, 20 Scorecard). Triadas desde el SARIF del último análisis sobre `main`, no desde la pestaña: 58 eran falsos positivos o diseño (SHA-256 como normalización de longitud antes de `timingSafeEqual`, tests, `.wasm` compilados con workflow de paridad) y quedan descartadas con motivo escrito; 37 eran reales — el tool MCP `evolith-scaffold` interpolaba cuatro nombres del llamador en una línea de shell, `POST /evaluate` leía `body.satellitePath`/`corePath` directo al sistema de ficheros (el DTO decía "path traversal validation required" y nadie lo hacía), un escritor de configuración por ruta de puntos llegaba a `Object.prototype`, ocho regex con la forma `/\/+$/` tardaban 15 s en 200k barras, y siete workflows corrían con un token por defecto con permiso de escritura. Cerrados en [#725](https://github.com/beyondnetcode/evolith_arch32/pull/725), [#737](https://github.com/beyondnetcode/evolith_arch32/pull/737) y [#748](https://github.com/beyondnetcode/evolith_arch32/pull/748), promovidos en #726/#739/#752: CodeQL 0, Scorecard 0, Dependabot 0, secret-scanning 0 en `c5547114`. **Lo que merece guardarse:** dos de los tres seguimientos no fueron defectos sino la gramática del escáner — CodeQL solo acredita un sanitizador escrito en su forma canónica (`path.resolve` + un solo `startsWith`, `includes('..')` + `isAbsolute` sobre la misma variable, un `=== '__proto__'` literal en la escritura) — y el tercero fue `GT-713`: el análisis que gobierna la pestaña solo lo sube la corrida `push` de este workflow, cuyo filtro de rutas saltaba casi todo el código, así que una promoción que solo tocaba `src/packages` dejó `main` sin analizar y la pestaña desactualizada hasta un lanzamiento manual.) **Última Actualización:** 2026-09-19 (**Un gap registrado y cerrado a partir del rojo que causó: la ventana de la evidencia de madurez se agotó sobre un PR que solo tocaba el README, por segunda vez.** `GT-711` → COMPLETADO. Los cuatro checks de runtime de `maturity-evidence.json` se observaron el 2026-08-18 y caducaron el 2026-09-18; el primer PR en enterarse fue [#724](https://github.com/beyondnetcode/evolith_arch32/pull/724), un cambio de posicionamiento en los dos README, cuando `Validate documentation` (requerido) se puso en rojo en `09-reconcile-maturity.mjs`. La misma forma que el 2026-08-18, cuando tropezó una promoción. La evidencia se re-observó contra runs reales (develop `4d655745`, main `99b53259`) en `2ee3f9a0`, y la ventana ahora **se anuncia en vez de descubrirse**: cada ejecución del reconciliador nombra los checks que caducan en menos de siete días, con la fecha; `--freshness` lo convierte en un código de salida; y un workflow diario abre UNA issue `maturity-evidence` una semana antes. **Lo que vale la pena conservar:** la ventana nunca fue el defecto — una fecha conocida con treinta días de antelación se aprendía el día en que empezaba a bloquear merges, por quien abriera un PR esa mañana.) **Última Actualización:** 2026-09-05 (**Dos filas nuevas de un mismo hilo: una CVE ALTA que llevaba tres días viva en `main` y la razón por la que no detuvo nada.** `GT-709` → COMPLETADO: `Security Audit` estaba rojo desde `b84523b4` no por una dependencia sin arreglo, sino porque `overrides.fast-uri` estaba fijado en `3.1.5`, **exactamente la última versión vulnerable de la rama 3.x**, con el parche en `3.1.6` y dentro del rango que `ajv` declara. Medido con el guard real, `63-validate-npm-audit-gate.mjs`: de **11 filas bloqueantes / 7 altas a 0 y 0**, sin declarar ninguna excepción. `GT-710` → PENDIENTE: ese gate **no es un check requerido**, y en el intervalo en que estuvo rojo se mergearon **ocho** PR a `main`, cuatro de ellos de dependencias npm. **Lo que merece llevarse:** el mecanismo que se usa para cerrar un advisory —el `overrides`— es el mismo que después impide cerrarlo, y el único check que lo ve no bloquea nada.) **Última Actualización:** 2026-08-18 (**Un gap cerrado por el disparador que él mismo había escrito — que se activó dos días después de escribirlo, y nombraba la release equivocada.** `GT-691` → COMPLETADO. La CVE ALTA de `js-yaml` que bloqueaba toda promoción a `main` salió del árbol por una actualización, no por un descarte: `@nestjs/swagger@11.4.7` —un PARCHE sobre la línea `11.4.x` que la fila había dado por agotada, no la 12 estable que decía esperar— declara `"js-yaml": "5.3.0"`, y `package-lock.json` resuelve ahora `node_modules/@nestjs/swagger/node_modules/js-yaml` hacia ella. `npm audit` pasa de 1 alta a 0 altas / 0 críticas; el criterio de falsabilidad de la fila se volvió a medir contra el árbol nuevo en vez de heredarlo, y ninguno de los dos falsadores se disparó. **Lo que merece llevarse es el elemento a vigilar, no la CVE:** la fila identificó bien que había que vigilar `@nestjs/swagger` y no `js-yaml`, y luego ató esa vigilancia a un major que no había salido.) @@ -22,6 +23,8 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | ID | Gap | En simple | Qué resuelve | Componente | Fase | Criticidad | Complejidad | Estado | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| +| [`GT-713`](./gap-reference-catalog.es.md#gt-713) | **El análisis al que están ligadas las alertas de la pestaña Security solo lo produce la corrida `push` de `sdk-cli-ci.yml`, y su filtro de rutas saltaba casi todo el código.** El job `CodeQL SAST` es el único que sube el análisis `/language:javascript-typescript` para `refs/heads/main`; las corridas de pull request son diff-informed (se recortan al diff y nunca mueven las alertas de la rama) y la configuración por defecto "Code Quality" es otra suite. El trigger `push` estaba filtrado a `src/sdk/cli/**`, `.harness/**` y los lockfiles. Medido el 2026-09-19: la promoción `19d736da` (cambios solo bajo `src/packages` y `src/apps`) llegó a `main` sin análisis alguno, así que la pestaña siguió mostrando 10 alertas sobre código que ya no existía; `c5547114` hizo lo mismo 40 minutos después. Las dos necesitaron `gh workflow run sdk-cli-ci.yml --ref main` a mano. **CERRADO 2026-09-19** en `72aceb70`: el filtro cubre ahora `src/packages/**` y `src/apps/**` — todo lo que CodeQL escanea — y un push solo de documentación sigue sin gastar la corrida. | El escáner que decide qué muestra la pestaña Security no volvía a correr cuando cambiaba la mayor parte del código, así que la pestaña describía el commit anterior. | Una promoción de código a `main` re-analiza `main`; la pestaña está al día sin que nadie tenga que acordarse de lanzarla. | `Infra` | Cross | P2 | XS | `COMPLETADO` | +| [`GT-712`](./gap-reference-catalog.es.md#gt-712) | **De las 95 alertas de la pestaña Security, 37 eran reales: una línea de shell construida con entrada del tool MCP, rutas de fichero tomadas tal cual de tres cuerpos HTTP, un escritor de configuración que llegaba a `Object.prototype`, ocho regex polinómicos y siete workflows con un token por defecto con escritura.** Triadas desde el SARIF del último análisis de CodeQL sobre `main` (sus `codeFlows` nombran el controlador o tool por el que entra cada taint), no desde la pestaña. CWE-78: `NxWorkspaceStrategy` interpolaba `apiName`/`hostName`/`remotes`/`domains` — argumentos sin validar del tool MCP `evolith-scaffold` — en `npx nx g … --name=… --directory=apps/…` a través de un shell; `git-log-reader` hacía lo mismo con `--since`. CWE-22: `POST /evaluate` (rama legacy) pasaba `body.satellitePath` y `body.corePath` al pipeline tal cual — la propia descripción del DTO decía "Path traversal validation required before use" — así que un llamador autenticado podía evaluar cualquier directorio del host; `POST /architecture/validate-satellite` dejaba que el manifest pisara el workspace resuelto; `POST /projects/initialize` construía `${cwd}/${name}` con un `name` sin validar. CWE-1321: `evolith-config-set` recorría una ruta de puntos sin guardar `__proto__`/`constructor`/`prototype`. Ocho regex `/\/+$/`, `/=+$/` y de recorte de separadores eran polinómicos — medidos en 15 s con 200k barras. Siete workflows no tenían `permissions:` de nivel superior (Scorecard Token-Permissions). **CERRADO 2026-09-19** en `cb2c8a1e` (#725), `8920b140` (#737) y `b200c5cb` (#748): la estrategia ejecuta sin shell vía `executeFileOrThrow` — nuevo en el puerto `ICommandExecutor`, implementado por el ejecutor del CLI y el del MCP — y cada nombre debe cumplir `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$`; `WorkspaceReferenceResolverService.resolveLegacyPath`/`resolveCorePathOverride` contienen las rutas legacy bajo `WORKSPACE_ROOT`/`CORE_PATH` y una instancia sin resolver falla cerrada; las rutas del manifest se fijan a las resueltas; `name` es un solo segmento de directorio en el DTO y en el caso de uso; el escritor de configuración rechaza los tres segmentos de prototipo en la escritura; los regex son recortes carácter a carácter (la forma que ya usaba `codeowners.ts`); los siete workflows declaran `contents: read` arriba y cada escritura en su job. Las 58 alertas restantes quedan descartadas cada una con su motivo escrito (SHA-256 como normalización de longitud antes de `timingSafeEqual`, cadenas de autenticación con comparación en tiempo constante, fixtures de test, `.wasm` compilados del Rego contiguo con `opa-parity.yml`, `npm install -g` de nuestra propia versión exacta). Recuento final en `c5547114`: CodeQL 0, Scorecard 0, Dependabot 0, secret-scanning 0. | Un agente manejando el tool de scaffold del MCP, o un cliente de la API REST del Core con una clave válida, podía ejecutar comandos de shell o leer directorios del host del Core. | Las 37 alertas reales se arreglan en el origen y las verifica CodeQL sobre `main`; las 58 que no lo eran llevan el motivo en la pestaña para que nadie las vuelva a triar. | `Core` | Cross | P0 | M | `COMPLETADO` | | [`GT-711`](./gap-reference-catalog.es.md#gt-711) | **La ventana de la evidencia de madurez se cierra en una fecha conocida con treinta días de antelación, y el primero en enterarse era quien abriera un PR esa mañana.** `validateRuntimeEvidence` en `09-reconcile-maturity.mjs` rechaza cualquiera de los cuatro checks de runtime de `maturity-evidence.json` cuando `observedAt` supera los 30 días, y `Validate documentation` — contexto requerido en `main` y `develop` — lo ejecuta con `--check`. La ventana es correcta: la evidencia que caduca se re-toma, no se estira. Lo incorrecto era que nada miraba el calendario antes del día en que importaba. Medido dos veces: el 2026-08-18 el check `documentation` cruzó los 31 días y puso en rojo una promoción (consta en el propio fichero de evidencia), y el 2026-09-19 los cuatro — observados el 2026-08-18, caducados desde el 2026-09-18 — pusieron en rojo [#724](https://github.com/beyondnetcode/evolith_arch32/pull/724), un cambio de posicionamiento del README, después de que el run de develop del 2026-09-15 hubiera pasado a 28 días. **CERRADO el 2026-09-19** en `30065070`: `assessEvidenceFreshness` nombra, por check, el primer día en que `validateRuntimeEvidence` lo rechazará (misma constante, misma aritmética); cada ejecución del reconciliador avisa con esa fecha cuando un check entra en `EVIDENCE_WARN_DAYS = 7`; `--freshness [--now=YYYY-MM-DD]` sale 1 dentro de la banda o pasada la ventana; y `maturity-evidence-freshness.yml` lo ejecuta a diario a las 06:45 UTC contra `develop`, abriendo una issue `maturity-evidence` que se cierra sola al re-observar. La re-observación en sí entró en `2ee3f9a0` (PR #724). **No se cambió a propósito:** la ventana de 30 días, y la regla de que una re-observación es una observación nueva, nunca un cambio de fecha. | Un reloj de 30 días sobre nuestra propia evidencia se agotó dos veces sobre gente que no la había tocado, el día en que empezaba a bloquear merges. | Enterarse de la caducidad una semana antes, en una issue, en lugar de ese día, en un check requerido en rojo sobre un PR ajeno. | `Infra` | Cross | P2 | S | `COMPLETADO` | | [`GT-710`](./gap-reference-catalog.es.md#gt-710) | **El gate que mide las CVE no es un check requerido, así que ocho merges pasaron por encima de él estando rojo.** Los nueve contextos requeridos de `main` y `develop` son `CodeQL SAST`, `Secret Detection (gitleaks)`, `Services build (GHCR)`, `Test`, `Test core`, `Test core-api`, `Test core-domain`, `Test mcp-server` y `Validate documentation`. **`Security Audit` no está entre ellos**, y tampoco lo están `Trivy` ni `build-and-test`. Medido el 2026-09-05: entre `b84523b4` (02-sep), el commit donde `Security Audit` se puso rojo, y su arreglo en [`GT-709`](./gap-reference-catalog.es.md#gt-709), se mergearon a `main` **ocho pull requests con el gate en rojo** — y cuatro de ellos ([#664](https://github.com/beyondnetcode/evolith_arch32/pull/664), [#665](https://github.com/beyondnetcode/evolith_arch32/pull/665), [#666](https://github.com/beyondnetcode/evolith_arch32/pull/666), [#667](https://github.com/beyondnetcode/evolith_arch32/pull/667)) eran cambios de dependencias npm, exactamente la clase de cambio que ese gate existe para juzgar. GitHub los presenta como `UNSTABLE` y no como `BLOCKED`, así que el flujo normal de revisión los mergea sin fricción. **El propio workflow nombra este modo de fallo por escrito:** el comentario de `sdk-cli-ci.yml` dice que un check siempre rojo enseña a los revisores a descontar el rojo, y luego deja el check fuera de los requeridos, que es la manera más directa de garantizar que eso ocurra. **Aplicado en parte el 2026-09-05:** `Security Audit` ya es requerido en `main` y `develop` (10 contextos, verificado), y el dueño decidió que `Trivy` y `build-and-test` lo sean también. Sigue abierta porque **el falsador de esta fila aún no se ha disparado** —no hay ninguna alta viva que observar quedando `BLOCKED` en vez de `UNSTABLE`— y porque `build-and-test` **no puede requerirse tal como está**: vive en `sdk-cli-release.yml`, cuyo `pull_request` filtra por rutas, y un check requerido detrás de un filtro nunca reporta y deja el PR inmergeable con todo en verde. Se registra aparte de [`GT-709`](./gap-reference-catalog.es.md#gt-709) a propósito: aquella era una CVE con arreglo de dos líneas, esta es la razón por la que la CVE pudo vivir tres días sin detener nada. | Tenemos un chequeo de seguridad que mide bien y no impide nada; ocho cambios entraron con él en rojo. | Que un advisory ALTA sin declarar bloquee el merge en lugar de limitarse a informarlo. | `Infra` | Cross | P1 | S | `PENDIENTE` | | [`GT-709`](./gap-reference-catalog.es.md#gt-709) | **Un `overrides` puesto para cerrar un advisory se convierte en el techo que impide cerrarlo la vez siguiente.** `Security Audit` llevaba rojo en `main` desde `b84523b4` (2026-09-02) por `GHSA-jqff-g426-hqxp`, una CVE ALTA en `fast-uri` — y la causa no era una dependencia sin arreglo publicado, sino **dos pins propios que se quedaron por debajo de la versión parcheada**, que el gate reporta con la misma forma que una advisory ajena. `overrides.fast-uri` estaba fijado en `3.1.5`, **exactamente la última versión vulnerable de la rama 3.x**; el parche es `3.1.6`, dentro del `^3.0.1` que declara `ajv@8.20.0`, así que el arreglo cabía en el pin que ya existía y nadie lo miró porque parecía configuración resuelta. **Medido con `63-validate-npm-audit-gate.mjs`, el mismo guard que corre CI, y no inferido de changelogs:** `origin/main` daba **11 filas bloqueantes / 7 altas**; con `fast-uri` a `3.1.7` caen 9 de las 11 — las cuatro advisories suyas más las cinco filas de la cadena `ajv`/`commitlint` que llegaban *via* `fast-uri`. Las dos restantes eran `browserslist` `4.28.4`, transitivo solo-dev (`ts-jest`→`@babel/core`, `@nestjs/cli`→`webpack`) con arreglo en `4.28.7`: mismo patrón, mismo tipo de override, pineado a `4.28.9`. **Resultado: 0 filas bloqueantes, 0 altas.** La moderada de `qs` sobrevive a propósito — el gate no bloquea por debajo de HIGH. **CERRADA el 2026-09-05** por [#689](https://github.com/beyondnetcode/evolith_arch32/pull/689), merge `eb458372` en `main`. **Lo que merece llevarse no es la CVE sino el modo de fallo:** el mecanismo que se usa para cerrar un advisory es el mismo que después lo mantiene abierto, y no hay nada que vigile los pins. Mismo patrón que [`GT-691`](./gap-reference-catalog.es.md#gt-691), donde la vigilancia quedó atada a un major que no había salido; allí el elemento mal vigilado fue `@nestjs/swagger`, aquí es el propio bloque `overrides`. El defecto de que este gate no bloquee ningún merge queda registrado aparte, en [`GT-710`](./gap-reference-catalog.es.md#gt-710). | El chequeo de vulnerabilidades llevaba tres días en rojo por dos versiones que nosotros mismos habíamos fijado una por debajo del arreglo. | Que el bloque de `overrides` deje de ser el sitio donde una CVE se queda a vivir, y que el chequeo de seguridad vuelva a significar algo. | `Infra` | Cross | P2 | S | `COMPLETADO` | @@ -733,7 +736,7 @@ Este tablero es la única fuente de verdad para deuda técnica, gaps, oportunida | [`GT-706`](./gap-reference-catalog.es.md#gt-706) | **Nada asegura que los `exports` que un paquete declara resuelvan dentro de su propio tarball, así que un productor publica una subruta fantasma y solo la descubre un consumidor — una publicación demasiado tarde.** `contracts@1.1.0` declaró una subruta de export que no incluía; el fallo salió en el smoke de sala limpia de `infra-providers@1.2.1`, **después de que `core-domain@1.3.1` ya estuviera irreversiblemente en el registry**, dejando la release a medio entregar y sin despublicar posible pasadas 72 horas. La comprobación que existe es real y tiene la forma equivocada: `npm-release.yml:213` calcula «prometidos» como `[pkg.main, ...bin]`, y **`exports` no está en esa lista**. FALSABILIDAD DEMOSTRADA, OBSERVADA EN VERDE: un paquete de dos ficheros que declara `"./ingest"` con solo `dist/index.js` en disco pasa esa aserción corrida literal — `exit=0`, mientras `require pkg/ingest` responde `MODULE_NOT_FOUND`. El smoke de sala limpia tampoco lo cubre, y no es defecto suyo: resuelve lo que un paquete IMPORTA, así que el fantasma del productor es invisible hasta el turno de un consumidor, que es después del paso irreversible. Exposición: 3 de 8 paquetes publicables declaran **23 subrutas de export**, ninguna asegurada, y dos declaran además un `./*` sin cota. **ARREGLADO 2026-08-16 — `.harness/scripts/ci/67-validate-declared-exports.mjs`, corriendo en tiempo de PR sobre todos los workspaces publicables Y por paquete dentro del bucle de release, justo antes de `npm publish`.** Recoge cada hoja de texto del árbol de condiciones, así que `types` cuenta tanto como `default`, e incluye `main`/`bin`, siendo un superconjunto de la aserción que sustituye. **La propia afirmación de esta fila sobre el registry la refutó el guard en su primera corrida:** «22 de 22 resuelven, 0 fantasmas» excluía las claves con comodín por su propio filtro, y una está MUERTA — `core-domain` declara `./infrastructure/adapters/*` **sin ningún directorio `adapters`**, 0 coincidencias en un packlist de 796 ficheros, `MODULE_NOT_FOUND` en el 1.3.1 publicado, y **ningún commit de este repositorio llevó jamás ese path**. Borrada, no ampliada: nunca hubo nada detrás. Falsabilidad observada por los dos lados — rojo con la fixture `./ingest`, con `core-domain` de verdad, y con un fichero presente en disco pero excluido por `files`; verde con la misma fixture en cuanto se incluye y con el árbol entero, **68 destinos declarados en 9 paquetes**. | Un paquete puede prometer una ruta de import que nunca incluyó, y quien se entera es el siguiente paquete en publicarse. | La release se niega a publicar un manifiesto que miente, antes de que nada sea irreversible. | `Infra` | Cross | P1 | S | `COMPLETADO` | -**Progreso:** 679 / 709 completados · 3 en progreso · 1 pendiente · 26 diferidos +**Progreso:** 681 / 711 completados · 3 en progreso · 1 pendiente · 26 diferidos **Oleada 2026-06-23 (auditoría profunda de Winston III):** Añadidos 14 gaps nuevos `GT-212`…`GT-225` del Winston Audit Playbook que cubren: higiene de estado ADR (GT-212), metadata + presupuestos operativos + corpus de guías por topología (GT-213, GT-217, GT-219), observabilidad + OpenAPI en controladores REST (GT-214, GT-215), paridad de input-schemas OPA + densidad de tests por topología (GT-216, GT-222), plantillas de rollback + on-call de Fase 05 (GT-218), cobertura de ramas CLI + paridad de envelope --format + limpieza de skip-list (GT-220, GT-224, GT-225), audit logging HTTP de MCP (GT-221), y tests e2e de paridad cross-surface (GT-223). diff --git a/reference/core/control-center/gaps/gap-tracking.md b/reference/core/control-center/gaps/gap-tracking.md index 027fc0e93..47d5a0c8b 100644 --- a/reference/core/control-center/gaps/gap-tracking.md +++ b/reference/core/control-center/gaps/gap-tracking.md @@ -4,6 +4,7 @@ **Status:** Active Tracking **Owner:** Evolith Architecture Board +**Last Updated:** 2026-09-19 (**Two gaps registered and closed off one afternoon on the Security tab: the 37 alerts that were real, and the reason the tab kept showing them after they were fixed.** `GT-712` → DONE, `GT-713` → DONE. The tab held 95 open alerts (75 CodeQL, 20 Scorecard). Triaged from the SARIF of the last analysis on `main`, not from the tab: 58 were false positives or design (SHA-256 as length normalisation before `timingSafeEqual`, tests, compiled `.wasm` with a parity workflow) and are dismissed with a written reason; 37 were real — the MCP tool `evolith-scaffold` interpolated four caller-supplied names into a shell line, `POST /evaluate` read `body.satellitePath`/`corePath` straight into the filesystem (the DTO said "path traversal validation required" and nothing did it), a dot-path config writer reached `Object.prototype`, eight `/\/+$/`-shaped regexes took 15 s on 200k slashes, and seven workflows ran with a write-capable default token. Closed in [#725](https://github.com/beyondnetcode/evolith_arch32/pull/725), [#737](https://github.com/beyondnetcode/evolith_arch32/pull/737) and [#748](https://github.com/beyondnetcode/evolith_arch32/pull/748), promoted in #726/#739/#752: CodeQL 0, Scorecard 0, Dependabot 0, secret-scanning 0 on `c5547114`. **What is worth keeping:** two of the three follow-ups were not defects but the scanner's grammar — CodeQL only credits a sanitizer written in its canonical shape (`path.resolve` + one `startsWith`, `includes('..')` + `isAbsolute` on the same variable, a literal `=== '__proto__'` at the write) — and the third was `GT-713`: the analysis that governs the tab is uploaded only by this workflow's `push` run, whose path filter skipped most of the code, so a promotion touching only `src/packages` left `main` unanalysed and the tab stale until a manual dispatch.) **Last Updated:** 2026-09-19 (**One gap registered and closed off the red it caused: the maturity-evidence window ran out on a README-only PR, for the second time.** `GT-711` → DONE. The four runtime checks in `maturity-evidence.json` were observed 2026-08-18 and turned stale on 2026-09-18; the first PR to learn it was [#724](https://github.com/beyondnetcode/evolith_arch32/pull/724), a positioning change to the two READMEs, when `Validate documentation` (required) went red in `09-reconcile-maturity.mjs`. Same shape as 2026-08-18, when a promotion tripped it. The evidence was re-observed against real runs (develop `4d655745`, main `99b53259`) in `2ee3f9a0`, and the window is now **announced instead of discovered**: every reconciler run names each check that turns stale within seven days, with the date; `--freshness` turns that into an exit code; and a daily workflow opens one `maturity-evidence` issue a week before. **What is worth keeping:** the window was never the defect — a date known thirty days ahead was being learned on the day it started blocking merges, by whoever happened to open a PR.) **Last Updated:** 2026-09-05 (**Two new rows off one thread: a HIGH CVE that had been live on `main` for three days, and the reason it stopped nothing.** `GT-709` → DONE: `Security Audit` had been red since `b84523b4` not because of a dependency without a fix, but because `overrides.fast-uri` was pinned at `3.1.5`, **exactly the last vulnerable release of the 3.x line**, with the patch in `3.1.6` and inside the range `ajv` declares. Measured with the real guard, `63-validate-npm-audit-gate.mjs`: from **11 blocking rows / 7 high to 0 and 0**, with no exception declared. `GT-710` → PENDING: that gate **is not a required check**, and in the window it was red **eight** PRs were merged into `main`, four of them npm dependency changes. **What is worth keeping:** the mechanism used to close an advisory — the `overrides` entry — is the same one that later prevents closing it, and the only check that sees it blocks nothing.) **Last Updated:** 2026-08-18 (**One gap closed by the trigger it had written down for itself — which fired two days after it was written, and named the wrong release.** `GT-691` → DONE. The HIGH `js-yaml` CVE that blocked every promotion to `main` left the tree by an upgrade, not a dismissal: `@nestjs/swagger@11.4.7` — a PATCH on the `11.4.x` line the row had declared exhausted, not the stable 12 it said to wait for — declares `"js-yaml": "5.3.0"`, and `package-lock.json` now resolves `node_modules/@nestjs/swagger/node_modules/js-yaml` to it. `npm audit` goes from 1 high to 0 high / 0 critical; the row's falsifiability criterion was re-measured against the new tree rather than inherited, and neither falsifier fired. **What is worth carrying forward is the watch item, not the CVE:** the row correctly identified that the thing to watch was `@nestjs/swagger` rather than `js-yaml`, then tied that watch to a major release that had not shipped.) @@ -22,6 +23,8 @@ This board is the single source of truth for technical debt, gaps, opportunities | ID | Gap | In plain terms | What it fixes | Component | Phase | Criticality | Complexity | Status | |---|---|---|---|:---:|:---:|:---:|:---:|:---:| +| [`GT-713`](./gap-reference-catalog.md#gt-713) | **The analysis that the Security tab's alerts are keyed to is produced only by the `push` run of `sdk-cli-ci.yml`, and its path filter skipped most of the code.** The `CodeQL SAST` job is the sole uploader of the `/language:javascript-typescript` analysis for `refs/heads/main`; pull-request runs are diff-informed (they prune to the diff and never move the branch's alerts) and the default "Code Quality" setup is a different suite. The `push` trigger was filtered to `src/sdk/cli/**`, `.harness/**` and the lockfiles. Measured 2026-09-19: promotion `19d736da` (changes under `src/packages` and `src/apps` only) reached `main` with no analysis at all, so the tab kept reporting 10 alerts on code that no longer existed; `c5547114` did the same 40 minutes later. Both needed `gh workflow run sdk-cli-ci.yml --ref main` by hand. **CLOSED 2026-09-19** in `72aceb70`: the filter now spans `src/packages/**` and `src/apps/**` — every tree CodeQL scans — while a docs-only push still skips the run. | The scanner that decides what the Security tab shows was not re-run when most of the code changed, so the tab described the previous commit. | A code promotion to `main` re-analyses `main`; the tab is current without anyone remembering to dispatch it. | `Infra` | Cross | P2 | XS | `DONE` | +| [`GT-712`](./gap-reference-catalog.md#gt-712) | **Of the 95 alerts on the Security tab, 37 were real: a shell line built from MCP tool input, filesystem paths taken verbatim from three HTTP bodies, a config writer that reached `Object.prototype`, eight polynomial regexes and seven workflows with a write-capable default token.** Triaged from the SARIF of the last CodeQL analysis on `main` (its `codeFlows` name the controller or tool each taint enters from), not from the tab. CWE-78: `NxWorkspaceStrategy` interpolated `apiName`/`hostName`/`remotes`/`domains` — unvalidated arguments of the MCP tool `evolith-scaffold` — into `npx nx g … --name=… --directory=apps/…` through a shell; `git-log-reader` did the same with `--since`. CWE-22: `POST /evaluate` (legacy branch) passed `body.satellitePath` and `body.corePath` to the pipeline as-is — the DTO's own description said "Path traversal validation required before use" — so an authenticated caller could evaluate any directory on the host; `POST /architecture/validate-satellite` let the manifest override the resolved workspace; `POST /projects/initialize` built `${cwd}/${name}` from an unvalidated `name`. CWE-1321: `evolith-config-set` walked a dot-path with no guard on `__proto__`/`constructor`/`prototype`. Eight `/\/+$/`, `/=+$/` and separator-trim regexes were polynomial — measured at 15 s on 200k slashes. Seven workflows had no top-level `permissions:` (Scorecard Token-Permissions). **CLOSED 2026-09-19** in `cb2c8a1e` (#725), `8920b140` (#737) and `b200c5cb` (#748): the strategy runs shell-free through `executeFileOrThrow` — new on the `ICommandExecutor` port, implemented by the CLI executor and the MCP one — and every name must match `^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$`; `WorkspaceReferenceResolverService.resolveLegacyPath`/`resolveCorePathOverride` contain the legacy paths under `WORKSPACE_ROOT`/`CORE_PATH` and an instance without a resolver fails closed; the manifest's paths are pinned to the resolved ones; `name` is one directory segment in the DTO and in the use case; the config writer refuses the three prototype segments at the write; the regexes are char-based trims (the shape `codeowners.ts` already used); the seven workflows declare `contents: read` at the top and each write on its job. The 58 remaining alerts are dismissed with a written reason each (SHA-256 as length normalisation before `timingSafeEqual`, auth chains with constant-time comparison, test fixtures, `.wasm` compiled from adjacent Rego with `opa-parity.yml`, `npm install -g` of our own exact version). Final count on `c5547114`: CodeQL 0, Scorecard 0, Dependabot 0, secret-scanning 0. | An agent driving the MCP scaffold tool, or a client of the Core's REST API with a valid key, could run shell commands or read directories on the Core host. | The 37 real alerts are fixed at the source and verified by CodeQL on `main`; the 58 that were not real carry the reason in the tab so nobody re-triages them. | `Core` | Cross | P0 | M | `DONE` | | [`GT-711`](./gap-reference-catalog.md#gt-711) | **The maturity-evidence window closes on a date known thirty days ahead, and the first to learn it was whoever opened a PR that morning.** `validateRuntimeEvidence` in `09-reconcile-maturity.mjs` rejects any of the four runtime checks in `maturity-evidence.json` once `observedAt` is more than 30 days old, and `Validate documentation` — a required context on `main` and `develop` — runs it with `--check`. The window is right: evidence that ages out is re-taken, not extended. What was wrong is that nothing looked at the calendar before the day it mattered. Measured twice: on 2026-08-18 the `documentation` check crossed 31 days and turned a promotion red (recorded in the evidence file itself), and on 2026-09-19 all four — observed 2026-08-18, stale since 2026-09-18 — turned [#724](https://github.com/beyondnetcode/evolith_arch32/pull/724) red, a README positioning change, after the develop run of 2026-09-15 had passed at 28 days. **CLOSED 2026-09-19** in `30065070`: `assessEvidenceFreshness` names, per check, the first day `validateRuntimeEvidence` will reject it (same constant, same arithmetic); every reconciler run warns with that date once a check is within `EVIDENCE_WARN_DAYS = 7`; `--freshness [--now=YYYY-MM-DD]` exits 1 inside the band or past it; and `maturity-evidence-freshness.yml` runs it daily at 06:45 UTC against `develop`, opening one `maturity-evidence` issue that closes itself once re-observed. The re-observation itself landed in `2ee3f9a0` (PR #724). **Not changed on purpose:** the 30-day window, and the rule that a re-observation is a new observation, never a date bump. | A 30-day clock on our own evidence ran out twice on people who had touched none of it, on the day it started blocking merges. | Learn the expiry a week ahead, in an issue, instead of on the day, in a red required check on an unrelated PR. | `Infra` | Cross | P2 | S | `DONE` | | [`GT-710`](./gap-reference-catalog.md#gt-710) | **The gate that measures CVEs is not a required check, so eight merges went past it while it was red.** The nine required contexts on `main` and `develop` are `CodeQL SAST`, `Secret Detection (gitleaks)`, `Services build (GHCR)`, `Test`, `Test core`, `Test core-api`, `Test core-domain`, `Test mcp-server` and `Validate documentation`. **`Security Audit` is not among them**, and neither are `Trivy` or `build-and-test`. Measured 2026-09-05: between `b84523b4` (Sep 2), the commit where `Security Audit` turned red, and its fix in [`GT-709`](./gap-reference-catalog.md#gt-709), **eight pull requests were merged into `main` with the gate red** — four of them ([#664](https://github.com/beyondnetcode/evolith_arch32/pull/664), [#665](https://github.com/beyondnetcode/evolith_arch32/pull/665), [#666](https://github.com/beyondnetcode/evolith_arch32/pull/666), [#667](https://github.com/beyondnetcode/evolith_arch32/pull/667)) npm dependency changes, precisely the class of change that gate exists to judge. GitHub renders them `UNSTABLE` rather than `BLOCKED`, so ordinary review merges them without friction. **The workflow names this failure mode in its own words:** the comment in `sdk-cli-ci.yml` says a permanently red check trains reviewers to discount red, and then leaves the check out of the required set, which is the most direct way to guarantee exactly that. **Partly applied 2026-09-05:** `Security Audit` is now required on `main` and `develop` (10 contexts, verified), and the owner decided `Trivy` and `build-and-test` should be too. It stays open because **this row's falsifier has not fired yet** — there is no live high advisory to observe coming out `BLOCKED` rather than `UNSTABLE` — and because `build-and-test` **cannot be required as it stands**: it lives in `sdk-cli-release.yml`, whose `pull_request` trigger is path-filtered, and a required check behind a filter never reports, leaving the PR unmergeable with everything green. Registered separately from [`GT-709`](./gap-reference-catalog.md#gt-709) on purpose: that one was a CVE with a two-line fix, this is why the CVE could live for three days without stopping anything. | We have a security check that measures correctly and prevents nothing; eight changes landed while it was red. | Make an undeclared HIGH advisory block the merge instead of merely reporting it. | `Infra` | Cross | P1 | S | `PENDING` | | [`GT-709`](./gap-reference-catalog.md#gt-709) | **An `overrides` pin added to close an advisory becomes the ceiling that prevents closing it the next time.** `Security Audit` had been red on `main` since `b84523b4` (2026-09-02) over `GHSA-jqff-g426-hqxp`, a HIGH CVE in `fast-uri` — and the cause was not a dependency without an upstream fix, but **two pins of our own left below the patched version**, which the gate reports in the same shape as somebody else's advisory. `overrides.fast-uri` was pinned at `3.1.5`, **exactly the last vulnerable release of the 3.x line**; the patch is `3.1.6`, inside the `^3.0.1` that `ajv@8.20.0` declares, so the fix fitted in the pin that was already there and nobody looked because it read as settled configuration. **Measured with `63-validate-npm-audit-gate.mjs`, the same guard CI runs, not inferred from changelogs:** `origin/main` reported **11 blocking rows / 7 high**; with `fast-uri` at `3.1.7`, 9 of the 11 go — its own four advisories plus the five `ajv`/`commitlint` chain rows that arrived *via* `fast-uri`. The remaining two were `browserslist` `4.28.4`, a dev-only transitive (`ts-jest`→`@babel/core`, `@nestjs/cli`→`webpack`) fixed in `4.28.7`: same pattern, same kind of override, pinned to `4.28.9`. **Result: 0 blocking rows, 0 high.** The moderate `qs` advisory survives on purpose — the gate does not block below HIGH. **CLOSED 2026-09-05** by [#689](https://github.com/beyondnetcode/evolith_arch32/pull/689), merge `eb458372` on `main`. **What is worth keeping is not the CVE but the failure mode:** the mechanism used to close an advisory is the same one that later holds it open, and nothing watches the pins. Same pattern as [`GT-691`](./gap-reference-catalog.md#gt-691), where the watch was tied to a major that had not shipped; there the mis-watched item was `@nestjs/swagger`, here it is the `overrides` block itself. That this gate blocks no merge at all is registered separately, as [`GT-710`](./gap-reference-catalog.md#gt-710). | The vulnerability check sat red for three days because of two versions we had ourselves pinned one release below the fix. | Stop the `overrides` block being the place a CVE settles in, and make the security check mean something again. | `Infra` | Cross | P2 | S | `DONE` | @@ -733,7 +736,7 @@ This board is the single source of truth for technical debt, gaps, opportunities | [`GT-706`](./gap-reference-catalog.md#gt-706) | **Nothing asserts that a package's own declared `exports` resolve inside its own tarball, so a producer publishes a phantom subpath and only a consumer discovers it — one publish too late.** `contracts@1.1.0` declared an export subpath it did not ship; the failure surfaced at `infra-providers@1.2.1`'s clean-room smoke, **after `core-domain@1.3.1` was already irreversibly on the registry**, leaving the release half-shipped with no unpublish available after 72 hours. The check that exists is real and the wrong shape: `npm-release.yml:213` computes "promised" as `[pkg.main, ...bin]`, and **`exports` is not in that list**. PROVEN FALSIFIABLE, OBSERVED GREEN: a two-file package declaring `"./ingest"` with only `dist/index.js` on disk passes that assertion run verbatim — `exit=0`, while `require pkg/ingest` answers `MODULE_NOT_FOUND`. The clean-room smoke does not cover it either, and that is not its defect: it resolves what a package IMPORTS, so a producer's phantom is invisible until a consumer's turn, which is after the irreversible step. Exposure: 3 of 8 publishable packages declare **23 export subpaths**, none asserted, two of them also declaring an unbounded `./*`. **FIXED 2026-08-16 — `.harness/scripts/ci/67-validate-declared-exports.mjs`, run at PR time over every publishable workspace AND per package inside the release loop, immediately before `npm publish`.** It collects every string leaf of the condition tree, so `types` counts as much as `default`, and folds in `main`/`bin`, making it a superset of the assertion it replaces. **The row's own claim about the registry was refuted by the guard on its first run:** "22 of 22 resolve, 0 phantom" excluded wildcard keys by its own filter, and one is DEAD — `core-domain` declares `./infrastructure/adapters/*` with **no `adapters` directory at all**, 0 matches in a 796-file packlist, `MODULE_NOT_FOUND` on the published 1.3.1, and **no commit in this repository ever carried that path**. Deleted, not widened: there was never anything behind it. Falsifiability observed on both sides — red on the `./ingest` fixture, on `core-domain` for real, and on a file present on disk but excluded by `files`; green on the same fixture once it ships and on the whole tree, **68 declared targets across 9 packages**. | A package can promise an import path it never shipped, and the next package to publish is the one that finds out. | The release refuses to publish a manifest that lies, before anything becomes irreversible. | `Infra` | Cross | P1 | S | `DONE` | -**Progress:** 679 / 709 done · 3 in progress · 1 pending · 26 deferred +**Progress:** 681 / 711 done · 3 in progress · 1 pending · 26 deferred **Wave 2026-06-23 (Winston deep audit III):** Added 14 new gaps `GT-212`…`GT-225` from the Winston Audit Playbook covering: ADR status hygiene (GT-212), topology manifest metadata + operational budgets + guidance corpus (GT-213, GT-217, GT-219), REST controller observability + OpenAPI (GT-214, GT-215), OPA input-schema parity + per-topology test density (GT-216, GT-222), SDLC Phase 05 rollback + on-call templates (GT-218), CLI branch coverage + envelope format coverage + skip-list cleanup (GT-220, GT-224, GT-225), MCP HTTP audit logging (GT-221), and cross-surface parity e2e tests (GT-223). diff --git a/reference/core/control-center/maturity-reports/executive-summary.es.md b/reference/core/control-center/maturity-reports/executive-summary.es.md index 1aeef5f6d..319be3a39 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.es.md +++ b/reference/core/control-center/maturity-reports/executive-summary.es.md @@ -42,14 +42,14 @@ La forma correcta de usar este resumen es simple: si necesitas contexto, abre so | Indicador | Valor | |---|---:| | Fecha canónica del tablero | 2026-09-19 | -| Gaps totales | 709 | -| Gaps cerrados | 679 | +| Gaps totales | 711 | +| Gaps cerrados | 681 | | Gaps pendientes | 30 | | P0 abiertos | 1 | | P1 abiertos | 9 | | P2 abiertos | 16 | | Cierre total | 95.8% | -| Registros de evidencia de cierre | 661 | +| Registros de evidencia de cierre | 663 | | Readiness registrado | 4 PASS | | Área | Pendientes | P0 | P1 | Primeros IDs | diff --git a/reference/core/control-center/maturity-reports/executive-summary.md b/reference/core/control-center/maturity-reports/executive-summary.md index a21538a88..38cabea74 100644 --- a/reference/core/control-center/maturity-reports/executive-summary.md +++ b/reference/core/control-center/maturity-reports/executive-summary.md @@ -42,14 +42,14 @@ Use this summary with a simple rule: if you need context, open only the linked I | Indicator | Value | |---|---:| | Canonical board date | 2026-09-19 | -| Total gaps | 709 | -| Closed gaps | 679 | +| Total gaps | 711 | +| Closed gaps | 681 | | Open gaps | 30 | | Open P0 | 1 | | Open P1 | 9 | | Open P2 | 16 | | Total closure | 95.8% | -| Closure evidence records | 661 | +| Closure evidence records | 663 | | Recorded readiness | 4 PASS | | Area | Open | P0 | P1 | First IDs | diff --git a/reference/core/control-center/maturity-reports/maturity-reconciliation.json b/reference/core/control-center/maturity-reports/maturity-reconciliation.json index e900a999a..ed3d46326 100644 --- a/reference/core/control-center/maturity-reports/maturity-reconciliation.json +++ b/reference/core/control-center/maturity-reports/maturity-reconciliation.json @@ -3,14 +3,14 @@ "scope": "evolith-core", "asOf": "2026-09-19", "gaps": { - "total": 709, - "done": 679, + "total": 711, + "done": 681, "pending": 1, "inProgress": 3, "deferred": 26 }, "evidence": { - "closureRecords": 661, + "closureRecords": 663, "cliPackage": "@beyondnet/evolith-cli@1.3.2", "adrCount": 144, "rulesetCount": 184,