From 76383ac731a76c011a440c87097adc7be2e258bc Mon Sep 17 00:00:00 2001 From: Devendra Variya Date: Sun, 2 Aug 2026 20:22:21 +0530 Subject: [PATCH 1/2] [audit] Flag printf with escaped newlines in prefer-write-over-heredoc The printf branch only matched a literal embedded newline, but printf commands write multi-line files with \n escapes (printf "a\nb\n" > file), which never contain a real newline character, so they slipped through. Add a branch that matches the \n escape when it has content after it, so a trailing-only \n (printf "%s\n" ...) stays unflagged as the single line it is. Adds tests for both. --- CHANGELOG.md | 1 + __tests__/audit/detectors.test.ts | 6 ++++++ src/audit/detectors/prefer-write-over-heredoc.ts | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 457d062a..2a46dfea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.0.16-beta.0 — 2026-07-31 ### Fixes +- Flag `printf "a\nb\n" > file` in the `prefer-write-over-heredoc` detector. The `printf` branch only matched literal embedded newlines, so the usual `\n`-escape form (which `printf` interprets into real newlines) slipped through. A `\n` only at the end (`printf "%s\n" ...`) stays unflagged, since that is a single line. (#PENDING) - Harden the release workflow against shell injection from ref names and generated outputs, align every Bun cache key with the tracked `bun.lock`, and discard the temporary publish-version edit before switching to `main` for the development-version bump. (#634) - Ship the binaries the release already builds, and stop a branch dispatch from rewriting main's version. The daemon split added every packaging input — platform manifests, pinned optional dependencies, a 4-way cross-compile matrix — but never touched `publish.yml`, so each release built four binaries as Actions artifacts and discarded them with the runner; CI stayed green because nothing checks that what gets built also gets shipped. `publish.yml` is now four jobs — preflight (version/dist-tag resolution, an npm credential check that fails in seconds rather than after a 20-minute matrix, and daemon detection), a call into `build-daemon.yml` as a reusable workflow, an asset job that assembles `SHA256SUMS` and attaches it plus the four binaries to the GitHub Release, and the npm publish — in that order, because the installed CLI downloads its daemon from that release tag and publishing the package first ships a version whose binary does not exist yet. A failed cross-compile now blocks the publish explicitly: a failed dependency leaves its dependents `skipped`, which the old-style guard would have read as "nothing to do". The version bump checks main out and pushes to it, so it runs only for a release or a dispatch from main, and `latest` is refused from a non-main dispatch (`auto` resolves to `next` there) so a branch build cannot move a dist-tag that a later release from main would move backwards. Adds a `dry_run` input that builds, checksums and validates the publish while writing nothing, and fixes the bun cache key, which hashed a `bun.lockb` this repo does not track. All of it is gated on the ref carrying a Rust workspace, so on main this changes nothing until the daemon lands. (#634) diff --git a/__tests__/audit/detectors.test.ts b/__tests__/audit/detectors.test.ts index 0df1aef5..649e26d6 100644 --- a/__tests__/audit/detectors.test.ts +++ b/__tests__/audit/detectors.test.ts @@ -94,6 +94,12 @@ describe("prefer-write-over-heredoc", () => { it("matches `echo \"multi\\nline\" > file`", () => { expect(preferWriteOverHeredoc.detect(bash('echo "a\nb" > out'), {})).not.toBeNull(); }); + it("matches `printf \"a\\nb\\n\" > file` (escaped newlines)", () => { + expect(preferWriteOverHeredoc.detect(bash('printf "line1\\nline2\\n" > out.txt'), {})).not.toBeNull(); + }); + it("does not match `printf \"%s\\n\" ... > file` (single line)", () => { + expect(preferWriteOverHeredoc.detect(bash('printf "%s\\n" "$var" > out.txt'), {})).toBeNull(); + }); }); describe("sleep-polling-loop", () => { diff --git a/src/audit/detectors/prefer-write-over-heredoc.ts b/src/audit/detectors/prefer-write-over-heredoc.ts index 322813f8..4277a45b 100644 --- a/src/audit/detectors/prefer-write-over-heredoc.ts +++ b/src/audit/detectors/prefer-write-over-heredoc.ts @@ -31,6 +31,14 @@ export const preferWriteOverHeredoc: Detector = { const summary = cmd.replace(/\s+/g, " ").trim().slice(0, 160); return { example: summary }; } + // `printf "a\nb\n" > file`. printf always interprets \n escapes into real + // newlines, so a format string whose \n has more content after it is + // multi-line content headed for a file. A \n only at the very end + // (`printf "%s\n" ...`) is a single line, so it is left alone. + if (/(?:^|\s|;|&&|\|\|)printf\s+["'][^"']*\\n[^"'][^"']*["']\s*>\s*\S/.test(cmd)) { + const summary = cmd.replace(/\s+/g, " ").trim().slice(0, 160); + return { example: summary }; + } return null; }, }; From d9c0cf6e430200af977c28a9c2b4a6ccac24235d Mon Sep 17 00:00:00 2001 From: Devendra Variya Date: Sun, 2 Aug 2026 20:23:04 +0530 Subject: [PATCH 2/2] [audit] Reference PR number in changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a46dfea..f3515c13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 0.0.16-beta.0 — 2026-07-31 ### Fixes -- Flag `printf "a\nb\n" > file` in the `prefer-write-over-heredoc` detector. The `printf` branch only matched literal embedded newlines, so the usual `\n`-escape form (which `printf` interprets into real newlines) slipped through. A `\n` only at the end (`printf "%s\n" ...`) stays unflagged, since that is a single line. (#PENDING) +- Flag `printf "a\nb\n" > file` in the `prefer-write-over-heredoc` detector. The `printf` branch only matched literal embedded newlines, so the usual `\n`-escape form (which `printf` interprets into real newlines) slipped through. A `\n` only at the end (`printf "%s\n" ...`) stays unflagged, since that is a single line. (#638) - Harden the release workflow against shell injection from ref names and generated outputs, align every Bun cache key with the tracked `bun.lock`, and discard the temporary publish-version edit before switching to `main` for the development-version bump. (#634) - Ship the binaries the release already builds, and stop a branch dispatch from rewriting main's version. The daemon split added every packaging input — platform manifests, pinned optional dependencies, a 4-way cross-compile matrix — but never touched `publish.yml`, so each release built four binaries as Actions artifacts and discarded them with the runner; CI stayed green because nothing checks that what gets built also gets shipped. `publish.yml` is now four jobs — preflight (version/dist-tag resolution, an npm credential check that fails in seconds rather than after a 20-minute matrix, and daemon detection), a call into `build-daemon.yml` as a reusable workflow, an asset job that assembles `SHA256SUMS` and attaches it plus the four binaries to the GitHub Release, and the npm publish — in that order, because the installed CLI downloads its daemon from that release tag and publishing the package first ships a version whose binary does not exist yet. A failed cross-compile now blocks the publish explicitly: a failed dependency leaves its dependents `skipped`, which the old-style guard would have read as "nothing to do". The version bump checks main out and pushes to it, so it runs only for a release or a dispatch from main, and `latest` is refused from a non-main dispatch (`auto` resolves to `next` there) so a branch build cannot move a dist-tag that a later release from main would move backwards. Adds a `dry_run` input that builds, checksums and validates the publish while writing nothing, and fixes the bun cache key, which hashed a `bun.lockb` this repo does not track. All of it is gated on the ref carrying a Rust workspace, so on main this changes nothing until the daemon lands. (#634)