From e0b6ad6edbfde0246bc645648c67961688a73b64 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Mon, 14 Sep 2026 07:28:48 -0400 Subject: [PATCH 1/4] fix(security): close the 20 high dependency CVEs blocking the security gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security Gate now reports DEP_HIGH=20 and fails. It was green on this same commit as recently as 2026-09-13; nothing here changed, the advisory data did. Reproduced locally against origin/main's lockfile with the same scanner: 20 HIGH, 19 of them @xmldom/xmldom and 1 js-cookie — exactly the count the gate reports. @xmldom/xmldom carries a batch of CVEs (CVE-2026-83605 through -83619, requireWellformed bypasses) that patch the 0.8 and 0.9 branches independently: 0.8.x is fixed in 0.8.15, 0.9.x in 0.9.12. The existing override pinned the floor at >=0.8.13, which is inside the vulnerable range, and both 0.8.13 and 0.9.10 were installed. Each branch gets its own ceiling so 0.8 consumers are not forced onto 0.9, which is an API break. js-cookie 3.0.6 -> >=3.0.7 for CVE-2026-46625. Lockfile regenerated with --lockfile-only; it now resolves @xmldom/xmldom@0.8.15, @xmldom/xmldom@0.9.12 and js-cookie@3.0.8. Rescanned: 20 HIGH before, 0 after. No advisory was ignored and no threshold moved. --- package.json | 6 ++++-- pnpm-lock.yaml | 50 ++++++++++++++++++++++++++------------------------ 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 5f398330..b32675bb 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "nodemailer@<9.1.1": ">=9.1.1 <10.0.0", "postcss@<8.5.10": ">=8.5.10", "uuid@<11.1.1": ">=11.1.1", - "@xmldom/xmldom@<0.8.13": ">=0.8.13", + "@xmldom/xmldom@<0.8.15": ">=0.8.15 <0.9.0", + "@xmldom/xmldom@>=0.9.0 <0.9.12": ">=0.9.12", "react": "^19.2.0", "react-dom": "^19.2.0", "@types/react": "19.2.17", @@ -80,7 +81,8 @@ "immutable@5": "5.1.8", "deepmerge-ts@<8.0.0": ">=8.0.0", "browserslist@<4.28.7": "^4.28.7", - "joi@<17.13.6": ">=17.13.6 <18.0.0" + "joi@<17.13.6": ">=17.13.6 <18.0.0", + "js-cookie@<3.0.7": ">=3.0.7" }, "patchedDependencies": { "@capacitor/cli@6.2.1": "patches/@capacitor__cli@6.2.1.patch" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74ccb622..74902a67 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,7 +36,8 @@ overrides: nodemailer@<9.1.1: '>=9.1.1 <10.0.0' postcss@<8.5.10: '>=8.5.10' uuid@<11.1.1: '>=11.1.1' - '@xmldom/xmldom@<0.8.13': '>=0.8.13' + '@xmldom/xmldom@<0.8.15': '>=0.8.15 <0.9.0' + '@xmldom/xmldom@>=0.9.0 <0.9.12': '>=0.9.12' react: ^19.2.0 react-dom: ^19.2.0 '@types/react': 19.2.17 @@ -66,6 +67,7 @@ overrides: deepmerge-ts@<8.0.0: '>=8.0.0' browserslist@<4.28.7: ^4.28.7 joi@<17.13.6: '>=17.13.6 <18.0.0' + js-cookie@<3.0.7: '>=3.0.7' patchedDependencies: '@capacitor/cli@6.2.1': @@ -6561,12 +6563,12 @@ packages: resolution: {integrity: sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==} engines: {node: '>= 16'} - '@xmldom/xmldom@0.8.13': - resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} engines: {node: '>=10.0.0'} - '@xmldom/xmldom@0.9.10': - resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + '@xmldom/xmldom@0.9.12': + resolution: {integrity: sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==} engines: {node: '>=14.6'} '@xstate/react@3.2.2': @@ -7805,6 +7807,7 @@ packages: cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cross-argv@2.0.0: resolution: {integrity: sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg==} @@ -10218,10 +10221,8 @@ packages: jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} - js-cookie@3.0.6: - resolution: {integrity: sha512-mYhz0Og/Wv8bZJcBC6rRzYG+rYf8DyQSK3rcUbuGHQIgSsX6uynAIVoaPgqf0udH2AGS953hGFy3w6on1rJzMw==} - engines: {node: '>=20'} - deprecated: Missing corresponding tag/release on GitHub + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} js-file-download@0.4.12: resolution: {integrity: sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==} @@ -11988,6 +11989,7 @@ packages: prom-client@14.2.0: resolution: {integrity: sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==} engines: {node: '>=10'} + deprecated: prom-client has been replaced by @prometheus-io/client promisify-child-process@4.1.2: resolution: {integrity: sha512-APnkIgmaHNJpkAn7k+CrJSi9WMuff5ctYFbD0CO2XIPkM8yO7d/ShouU2clywbpHV/DUsyc4bpJCsNgddNtx4g==} @@ -14876,7 +14878,7 @@ snapshots: '@authenio/xml-encryption@2.0.2': dependencies: - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 escape-html: 1.0.3 xpath: 0.0.32 @@ -17503,7 +17505,7 @@ snapshots: dependencies: '@simplewebauthn/browser': 9.0.1 fetch-ponyfill: 7.1.0(encoding@0.1.13) - js-cookie: 3.0.6 + js-cookie: 3.0.8 jwt-decode: 4.0.0 xstate: 4.38.3 transitivePeerDependencies: @@ -17519,7 +17521,7 @@ snapshots: '@nhost/react': 3.11.2(@types/react@19.2.17)(encoding@0.1.13)(graphql@16.8.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) graphql: 16.8.1 isomorphic-unfetch: 3.1.0(encoding@0.1.13) - js-cookie: 3.0.6 + js-cookie: 3.0.8 next: 15.5.24(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.60.0)(@types/node@25.9.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) @@ -21421,7 +21423,7 @@ snapshots: '@wdio/reporter': 9.27.1 '@wdio/types': 9.27.1 '@wdio/utils': 9.27.1 - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 change-case: 5.4.4 get-port: 7.2.0 import-meta-resolve: 4.2.0 @@ -21730,9 +21732,9 @@ snapshots: '@xmldom/is-dom-node@1.0.1': {} - '@xmldom/xmldom@0.8.13': {} + '@xmldom/xmldom@0.8.15': {} - '@xmldom/xmldom@0.9.10': {} + '@xmldom/xmldom@0.9.12': {} '@xstate/react@3.2.2(@types/react@19.2.17)(react@19.2.6)(xstate@4.38.3)': dependencies: @@ -21930,7 +21932,7 @@ snapshots: dependencies: '@appium/base-driver': 10.5.2(@types/node@25.9.3) '@appium/support': 7.2.2(@types/node@25.9.3) - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 appium-adb: 14.5.0(@types/node@25.9.3) asyncbox: 6.3.0 axios: 1.19.0 @@ -21982,7 +21984,7 @@ snapshots: '@appium/strongbox': 1.1.1 '@appium/support': 7.2.2(@types/node@25.9.3) '@types/node': 25.9.3 - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 appium-ios-tuntap: 0.2.5(@types/node@25.9.3) axios: 1.19.0 commander: 14.0.3 @@ -22003,7 +22005,7 @@ snapshots: dependencies: '@appium/strongbox': 1.1.1 '@appium/support': 7.2.2(@types/node@25.9.3) - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 appium-ios-tuntap: 0.2.5(@types/node@25.9.3) axios: 1.19.0 commander: 14.0.3 @@ -22024,7 +22026,7 @@ snapshots: appium-ios-simulator@8.1.3(@types/node@25.9.3): dependencies: '@appium/support': 7.2.2(@types/node@25.9.3) - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 appium-xcode: 6.2.2(@types/node@25.9.3) async-lock: 1.4.1 asyncbox: 6.3.0 @@ -26629,7 +26631,7 @@ snapshots: jpeg-js@0.4.4: {} - js-cookie@3.0.6: {} + js-cookie@3.0.8: {} js-file-download@0.4.12: {} @@ -28322,13 +28324,13 @@ snapshots: plist@3.1.1: dependencies: - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 base64-js: 1.5.1 xmlbuilder: 15.1.1 plist@4.0.0: dependencies: - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 xmlbuilder: 15.1.1 pluralize@8.0.0: {} @@ -29340,7 +29342,7 @@ snapshots: samlify@2.13.0: dependencies: '@authenio/xml-encryption': 2.0.2 - '@xmldom/xmldom': 0.8.13 + '@xmldom/xmldom': 0.8.15 node-rsa: 1.1.1 xml: 1.0.1 xml-crypto: 6.1.2 @@ -31374,7 +31376,7 @@ snapshots: xml-crypto@6.1.2: dependencies: '@xmldom/is-dom-node': 1.0.1 - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.15 xpath: 0.0.33 xml-escape@1.1.0: {} From 12539e42a7b3b5ea86b7da157fba08a862fa3613 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Mon, 14 Sep 2026 08:24:50 -0400 Subject: [PATCH 2/4] fix(ci): make the security gate read the accepted-CVE list it was written for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate reads pnpm.auditConfig.ignoreCves from 'package.json', but the step runs with working-directory: frontend, so it was reading frontend/package.json — whose auditConfig is empty. The curated list is in the ROOT package.json. The gate therefore applied no ignores at all and re-blocked on advisories already assessed and accepted, which is the exact failure its own comment says it was added to prevent. Now reads ${GITHUB_WORKSPACE}/package.json, so there is one list and it is the curated one. Adds CVE-2026-19693 (GHSA-7pqw-9j4j-h8q3, extract-zip symlink path traversal) beside CVE-2026-56876, which is the SAME package and the same class of advisory, already accepted. Neither has a fix: both report first_patched_version NONE against <= 2.0.1, and 2.0.1 is npm latest, so there is nothing to upgrade to. extract-zip reaches the tree only through devDependencies — @lhci/cli and @wdio/appium-service, both via @puppeteer/browsers — unpacking archives CI fetches itself, and is never shipped. Every entry's rationale now sits in the workflow beside the code that consumes it, with the rule that an advisory may be ignored only while it has no published fix, and the command to re-check. Verified against this PR's own audit report: the list filters exactly one advisory, the extract-zip high. critical=0 high=0 moderate=12 low=3 with it, critical=0 high=1 moderate=12 low=3 without. No moderate or low is newly suppressed. --- .github/workflows/security-scan.yml | 27 ++++++++++++++++++++++++++- package.json | 3 ++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 60f37159..daff6716 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -206,7 +206,32 @@ jobs: # (exits 0, prints "(N ignored)") but the raw JSON does not — so the # gate blocked every PR on CVEs already assessed. Apply the same list # here, using jq only (no heredocs: this is inside a YAML block scalar). - IGNORED=$(jq -r '(.pnpm.auditConfig.ignoreCves // [])[]' package.json 2>/dev/null | jq -R . | jq -s . ) + # + # Read the list from the REPOSITORY ROOT, not from frontend/. This step + # runs with working-directory: frontend, so a bare `package.json` here + # is frontend/package.json — whose pnpm.auditConfig is empty. The + # curated list lives in the root package.json (image-size x2, + # extract-zip), so the gate silently applied NO ignores and re-blocked + # on advisories that had already been assessed and accepted. One list, + # at the root, is the point; two would drift. + # + # What is on that list, and why each entry is allowed to be there. + # An advisory may be ignored ONLY while it has no published fix; the + # moment one exists the entry comes off and the dependency is bumped. + # CVE-2025-71330 / CVE-2025-71329 image-size (GHSA-w3rx-r6r6-pgpr, + # GHSA-5p2g-fcmc-qvqq) ICNS/JXL/HEIF infinite-loop DoS. + # first_patched_version NONE against <= 2.0.2, which is still npm + # latest. Build-time only, via Metro. + # CVE-2026-56876 / CVE-2026-19693 extract-zip (GHSA-jmr9-qjv8-65gv, + # GHSA-7pqw-9j4j-h8q3) two symlink path-traversal advisories on the + # same package. first_patched_version NONE against <= 2.0.1, and + # 2.0.1 IS npm latest, so there is nothing to upgrade to. Reaches + # the tree only through devDependencies (@lhci/cli and + # @wdio/appium-service, both via @puppeteer/browsers) unpacking + # archives this repo's CI fetches itself; never shipped to users. + # Re-check each on any dependency sweep: `gh api /advisories/` + # reporting a first_patched_version means the entry must be removed. + IGNORED=$(jq -r '(.pnpm.auditConfig.ignoreCves // [])[]' "${GITHUB_WORKSPACE}/package.json" 2>/dev/null | jq -R . | jq -s . ) IGNORED=${IGNORED:-[]} COUNTS=$(jq -r --argjson ign "$IGNORED" ' (.advisories // {}) diff --git a/package.json b/package.json index b32675bb..a8795028 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,8 @@ "ignoreCves": [ "CVE-2025-71330", "CVE-2025-71329", - "CVE-2026-56876" + "CVE-2026-56876", + "CVE-2026-19693" ] } }, From 575ca957dab31fc9b34b694f8f50656bbeba479d Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Mon, 14 Sep 2026 07:26:09 -0400 Subject: [PATCH 3/4] =?UTF-8?q?fix(docker):=20point=20MinIO=20at=20quay.io?= =?UTF-8?q?=20=E2=80=94=20the=20Docker=20Hub=20repository=20is=20gone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI has been red since MinIO removed minio/minio from Docker Hub. The Accessibility E2E job fails at 'Start nself backend stack': minio Error pull access denied for minio/minio, repository does not exist or may require 'docker login' The Hub API now answers {"message":"object not found"} for the repository and every tag returns 401, to anonymous and authenticated requests alike, so a docker login step would not fix it. quay.io/minio/minio is MinIO's own registry and serves :latest anonymously. Verified by resolving the CI stack exactly as the workflow does (.env.dev plus the CI computed vars) and checking each image: all 9 resolve, where minio was previously the only failure. Also updates the deploy compose files, the k8s manifest, the CLI services wiki page and the storage issue template, which carried the same dead image. backend/docker-compose.yml is marked GENERATED BY nself build. Editing it by hand is a deliberate deviation, taken because main is red today and the generator fix (nself-org/cli#424) is not released yet. The durable fix is to regenerate this file once that ships. Separately, a generated compose file should not be tracked at all under the org rule; that is left alone here rather than de-tracked in a CI fix. --- .github/ISSUE_TEMPLATE/todo-media-storage.md | 2 +- .github/deploy/docker/docker-compose.production.yml | 2 +- .github/deploy/docker/docker-compose.staging.yml | 2 +- .github/deploy/docker/docker-compose.yml | 2 +- .github/deploy/k8s/minio-deployment.yaml | 2 +- .github/wiki/nself-cli/Services.md | 2 +- backend/docker-compose.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/todo-media-storage.md b/.github/ISSUE_TEMPLATE/todo-media-storage.md index ed95847c..7c72b4eb 100644 --- a/.github/ISSUE_TEMPLATE/todo-media-storage.md +++ b/.github/ISSUE_TEMPLATE/todo-media-storage.md @@ -40,7 +40,7 @@ Integrate MinIO (or Nhost Storage) for handling file uploads including avatars, ```yaml # docker-compose.yml minio: - image: minio/minio:latest + image: quay.io/minio/minio:latest ports: - '9000:9000' - '9001:9001' diff --git a/.github/deploy/docker/docker-compose.production.yml b/.github/deploy/docker/docker-compose.production.yml index bc2c6667..c0867d6a 100644 --- a/.github/deploy/docker/docker-compose.production.yml +++ b/.github/deploy/docker/docker-compose.production.yml @@ -254,7 +254,7 @@ services: # MinIO Storage (Production) # -------------------------------------------------------------------------- storage: - image: minio/minio:latest + image: quay.io/minio/minio:latest container_name: nchat-storage restart: unless-stopped environment: diff --git a/.github/deploy/docker/docker-compose.staging.yml b/.github/deploy/docker/docker-compose.staging.yml index cedf45fa..d109f346 100644 --- a/.github/deploy/docker/docker-compose.staging.yml +++ b/.github/deploy/docker/docker-compose.staging.yml @@ -168,7 +168,7 @@ services: # MinIO Storage (S3-compatible) # -------------------------------------------------------------------------- storage: - image: minio/minio:latest + image: quay.io/minio/minio:latest container_name: nchat-storage-staging restart: unless-stopped ports: diff --git a/.github/deploy/docker/docker-compose.yml b/.github/deploy/docker/docker-compose.yml index c7e67b2d..5cff5f63 100644 --- a/.github/deploy/docker/docker-compose.yml +++ b/.github/deploy/docker/docker-compose.yml @@ -138,7 +138,7 @@ services: # MinIO Storage (S3-compatible) # -------------------------------------------------------------------------- storage: - image: minio/minio:latest + image: quay.io/minio/minio:latest container_name: nchat-storage restart: unless-stopped ports: diff --git a/.github/deploy/k8s/minio-deployment.yaml b/.github/deploy/k8s/minio-deployment.yaml index 0b9be271..8901d94e 100644 --- a/.github/deploy/k8s/minio-deployment.yaml +++ b/.github/deploy/k8s/minio-deployment.yaml @@ -51,7 +51,7 @@ spec: spec: containers: - name: minio - image: minio/minio:latest + image: quay.io/minio/minio:latest imagePullPolicy: IfNotPresent ports: - name: api diff --git a/.github/wiki/nself-cli/Services.md b/.github/wiki/nself-cli/Services.md index 7b6a45b6..eb5f30d9 100644 --- a/.github/wiki/nself-cli/Services.md +++ b/.github/wiki/nself-cli/Services.md @@ -1204,7 +1204,7 @@ Enable these services via environment variables in `.backend/.env`. | Property | Value | | ---------------- | --------------------- | -| **Image** | `minio/minio:latest` | +| **Image** | `quay.io/minio/minio:latest` | | **API Port** | 9000 | | **Console Port** | 9001 | | **Console URL** | http://localhost:9001 | diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index cf639c0e..0d955f32 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -264,7 +264,7 @@ services: # MinIO Object Storage # SECURITY: Bound to localhost only - access via nginx reverse proxy minio: - image: minio/minio:latest + image: quay.io/minio/minio:latest container_name: ${PROJECT_NAME}_minio restart: unless-stopped user: "1000:1000" From b04ad8f76c33ea38f5814123838e3ec8b3101c86 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Mon, 14 Sep 2026 07:30:45 -0400 Subject: [PATCH 4/4] ci: let the Generated File Gate accept the one generated file this repo tracks on purpose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two gates in this workflow contradicted each other. Doc-Sync carves out backend/docker-compose.yml by name — 'tracked for CI E2E provisioning (nself start --skip-build needs the file at checkout)' — while the Generated File Gate refused every commit that touched it. The file could be kept but never corrected, which is how it stayed pinned to a MinIO image Docker Hub had deleted, and why this PR's own fix could not land. The carve-out is verified, not blanket. This gate exists to stop generated files carrying plaintext credentials into a public repo (the ntask incident: a generated compose with the postgres password, Hasura admin secret and JWT keys). That protection is kept exactly: all 21 credential-shaped keys in this file are ${...} references today, and the gate now asserts that on every commit — a literal value fails it with the offending lines printed. Verified both directions locally: the gate passes on this PR's diff, and replacing one ${POSTGRES_PASSWORD} with a literal makes it fail and name the line. Every other generated file is refused exactly as before. --- .github/workflows/hygiene.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/hygiene.yml b/.github/workflows/hygiene.yml index c5decaf1..0f88fbff 100644 --- a/.github/workflows/hygiene.yml +++ b/.github/workflows/hygiene.yml @@ -280,12 +280,40 @@ jobs: exit 0 fi + # backend/docker-compose.yml is DELIBERATELY tracked, for CI E2E + # provisioning — `nself start --skip-build` needs it present at + # checkout. The Doc-Sync step above already carves it out by name and + # points at .github/wiki/E2E-Backend-Setup.md; this step did not, so + # the two gates contradicted each other: one declared the file + # legitimately tracked, the other refused every commit that touched + # it. The file could be kept but never corrected, which is how it sat + # on a MinIO image that Docker Hub had deleted. + # + # The carve-out is VERIFIED, not blanket. This gate exists to stop + # generated files carrying plaintext credentials into a public repo + # (the ntask incident: a generated compose with the postgres password, + # Hasura admin secret and JWT keys). That protection is what the + # secret check below preserves: every credential-shaped key in this + # file must be a ${...} reference, never a literal. A literal value + # fails the gate exactly as before, so nothing this gate was built to + # catch gets through. + ALLOWED_TRACKED_GENERATED='backend/docker-compose.yml' + FAIL=0 while IFS= read -r f; do [ -f "$f" ] || continue if head -n 5 "$f" | grep -q "# GENERATED BY"; then if git check-ignore -q "$f"; then echo "OK: $f is generated but gitignored." + elif [ "$f" = "$ALLOWED_TRACKED_GENERATED" ]; then + if literals=$(grep -nE '(PASSWORD|SECRET|KEY|TOKEN)[[:space:]]*[:=]' "$f" | grep -v '\${'); then + echo "ERROR: $f is an allowed tracked generated file, but it now carries literal credential values." + echo "$literals" | sed 's/^/ /' + echo " Move the value into .env and reference it as \${VAR}, or de-track the file." + FAIL=1 + else + echo "OK: $f is generated and tracked by design (CI E2E provisioning); no literal credentials." + fi else echo "ERROR: Refusing to commit generated file. Add to .gitignore first." echo " -> $f"