Skip to content

fix(security): workflow/CI hardening batch (#1384, #1391, #1396, #1405) - #1419

Merged
don-petry merged 23 commits into
mainfrom
fix/sec-workflow-hardening-batch
Nov 2, 2026
Merged

don-petry merged 23 commits into
mainfrom
fix/sec-workflow-hardening-batch

Conversation

@don-petry

@don-petry don-petry commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Workflow/CI hardening batch for four CodeRabbit security findings. Each one was checked against the code and reproduced locally before fixing. Each fix comes with a regression test that fails on origin/main and passes on this branch. There is one commit per issue.

Issue Area Verdict
#1384 add-to-project classify-initiative.sh log lines Confirmed
#1391 scripts/lib/dev-lead-retrigger.sh activity check Confirmed
#1396 auto-rebase-reusable.yml manual recovery commands Confirmed
#1405 agent-shield-reusable.yml scanner invocation Confirmed

Fixes #1384: untrusted titles written raw to runner logs

How it was confirmed: sweep_project printed «${title}» raw at classify-initiative.sh:282, 288, 299, 303 and also at 312 (the Theme warning). The runner parser (actions/runner ActionCommand.TryParse) matches the legacy ##[ prefix with IndexOf, so it fires anywhere in a line. TryParseV2 (::cmd::) only fires at the start of a line, after leading whitespace is trimmed. A title such as x ##[error]… or ##[stop-commands]… in an UNMATCHED/MATCH line was therefore read as a command. The new sweep tests reproduced it: the forged markers showed up verbatim in the output on main.

Fix: added atp_log_safe to the shared .github/scripts/add-to-project/lib.sh:

  • CR and LF become spaces.
  • Every :: and ##[ gets a zero-width space (U+200B) between its characters, so the title stays readable but no longer matches either command syntax. The :: replacement loops, so ::: is handled too.

All five title log sites in the sweep go through it. So does add_draft_item's dry-run line, which is the only other title echo among the sibling scripts. add-issue-or-pr.sh and reconcile-backlog.sh only log GitHub-generated URLs, repo names and config-defined labels.

Regression tests (test/workflows/add-to-project/classify-initiative.bats):

  • Unit tests for atp_log_safe.
  • Two sweep_project tests with hostile titles: dry-run MATCH/UNMATCHED, and the apply-mode ::warning:: set-failure path. They check that no ##[ and no injected :: line reaches the output.

Fixes #1391: a fork PR branch name could fake dev-lead activity

How it was confirmed: at dev-lead-retrigger.sh:39-47, the pulls?state=open filter only checked .head.ref | startswith("dev-lead/issue-<n>-"). That list includes fork PRs, and fork authors pick their own branch names. A fixture fork PR on dev-lead/issue-7-… made dl_dev_lead_active return "active" on main.

Fix: a PR now counts only when all of these hold:

  • the branch prefix matches (unchanged);
  • .head.repo != null and .head.repo.full_name == .base.repo.full_name, i.e. the PR comes from this repo, not a fork or a deleted head repo;
  • .user.login is the dev-lead runtime identity.

That identity is don-petry, as documented in standards/persona-standards.md §5.1. All 154 dev-lead/ PRs in this repo were authored by that login. It can be overridden with DEV_LEAD_LOGINS (comma-separated). The logins are JSON-encoded with jq -R, not spliced into the filter as raw strings. Fail-closed behaviour on API errors is unchanged. I also ran a read-only smoke test of the new --jq filter against real gh/gojq on this repo: open PR #1383 (dev-lead/issue-1361-…) is still detected as active.

Regression tests (new test/scripts/compliance-retrigger/dev-lead-active.bats, 10 tests). The gh stub applies the real --jq filter to fixtures. Cases covered:

  • fork PR
  • fork PR authored by the agent login
  • same-repo PR by a non-agent author
  • head.repo: null
  • agent PR among fork decoys
  • exact issue-number prefix
  • DEV_LEAD_LOGINS override and injection attempt
  • fail-closed behaviour

The suite is already run by compliance-retrigger-tests.yml, which runs the test/scripts/compliance-retrigger/ directory on scripts/lib/** changes.

Fixes #1396: base branch pasted unescaped into published shell commands

How it was confirmed: auto-rebase-reusable.yml:218 (git rebase origin/$BASE_BRANCH) and :258 (git merge origin/$BASE_BRANCH) interpolate .base.ref raw into the copy-paste snippets in PR comments. git check-ref-format --branch accepts names containing ;, $(…), backticks, |, && and quotes. The tests below assert that precondition for each case.

Fix: added auto_rebase_shell_quote to .github/scripts/auto-rebase/lib/comments.sh, which the reusable already sources from its own commit.

  • Refs made only of [A-Za-z0-9_./+-] are printed unchanged, so origin/main stays readable.
  • Anything else is single-quoted, with ' written as '\''. That form is valid in bash, zsh and sh, unlike %q.

The reusable computes MANUAL_BASE_REF once and uses it in both snippets. The README function table is updated.

Regression tests (new test/workflows/auto-rebase/manual-command-quoting.bats):

  • Round-trip and no-execution checks for hostile but git-valid names, under bash eval and sh.
  • A grep guard confirming the reusable no longer pastes origin/$BASE_BRANCH into a git command, and that both snippets use the quoted ref.

Fixes #1405: the PR's .npmrc could configure the AgentShield scanner

How it was confirmed: agent-shield-reusable.yml:60 runs npx --ignore-scripts ecc-agentshield@… scan --path . with the PR checkout as cwd. npm loads project config from <localPrefix>/.npmrc, and localPrefix is cwd-based. I reproduced this locally with npm 11.11.0. In a directory whose .npmrc sets script-shell=/usr/bin/true, npx --ignore-scripts -c 'exit 3' exits 0, so the command is silently skipped. Run from a clean prefix, the same command exits 3. npm config get registry similarly showed the hostile registry from inside the directory.

Fix: the scan step now:

  • creates $RUNNER_TEMP/agentshield-npm containing a minimal package.json, which pins localPrefix;
  • cds there;
  • runs npx --prefix=<that dir> --ignore-scripts ecc-agentshield@${AS_VERSION} scan --path "<absolute checkout path>".

The version pin and --ignore-scripts are unchanged. I checked in the ecc-agentshield v1.4.0 source that scan --path resolves its argument, so an absolute target works. The change is limited to the scan step, so it does not conflict with #1378, which edits the inputs and the cross-reference steps of this file.

Regression test (test/workflows/dependency-hardening.bats, which already runs on every .github/workflows/** change): it extracts the step's real run: script with yq and executes it inside a fake checkout that has a hostile .npmrc, with a stub npx that records its cwd and args. It asserts:

  • npx runs outside the checkout;
  • --prefix equals that cwd;
  • --path is the checkout;
  • --ignore-scripts is kept;
  • when npm is available, npm config get script-shell with the recorded flags does not resolve the hostile value. A precondition first confirms the fixture is hostile when npm runs inside the checkout.

Tests run (locally, bats 1.13 with GNU bash 5)

  • bats test/workflows/add-to-project/: 88/88 pass
  • bats test/scripts/compliance-retrigger/: 22/22 pass
  • bats test/workflows/auto-rebase/: 52/52 pass
  • bats test/workflows/dependency-hardening.bats: 5/5 pass
  • bats test/scripts/compliance-audit/ (it also sources dev-lead-retrigger.sh): 298/298 pass
  • Red check: all 16 new tests run against an origin/main tree fail as expected.
  • shellcheck -x on the changed scripts: clean apart from one existing SC2016 info in lib.sh (delete_project_item's GraphQL string, not touched here; CI runs at -S warning).
  • actionlint on both changed workflows: only an existing github.job_workflow_sha false positive from my local actionlint version, on a line not touched here.
  • gitleaks detect --no-git: no leaks.

Release notes / follow-ups

Needs a human decision

🤖 Generated with Claude Code

View guided diff Turn on auto-fix

Problem

See the linked issue.

Risk

Medium — changes GitHub Actions workflow behavior, which is exercised only post-merge; verify via the affected workflow runs.

Test plan

Tests added/updated: test/scripts/compliance-retrigger/dev-lead-active.bats,test/workflows/add-to-project/classify-initiative.bats test/workflows/auto-rebase/manual-command-quoting.bats,test/workflows/dependency-hardening.bats. Verification: bash scripts/dev-lead-lint.sh (shellcheck --severity=warning) ran pre-commit; the bats suite runs in CI.

Rollback

Revert this PR. No non-revertible side effects (no tags, migrations, or external state).

Monitoring

Watch the affected workflow run(s) in the Actions tab and this PR's Lint check for regressions.

don-petry and others added 4 commits October 27, 2026 22:20
…titles (#1384)

classify-initiative.sh printed raw issue/PR/draft titles to runner stdout and
into ::warning:: lines. The runner treats the legacy `##[cmd]` marker anywhere
in a line (and `::cmd::` at a line start) as a workflow command, so a title
could forge annotations or stop command processing for the step.

Add atp_log_safe to the shared add-to-project lib.sh: CR/LF become spaces and
each `::` / `##[` gets a zero-width space between its characters, keeping the
title readable while no longer matching either command syntax. Route every
title the sweep logs through it, plus add_draft_item's dry-run line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1391)

dl_dev_lead_active treated any open PR whose head branch started with
dev-lead/issue-<n>- as proof the agent was working the issue. The open-PR list
includes fork PRs, whose branch name the contributor chooses, so a fork PR could
suppress compliance retriggers.

Now a PR only counts when its head repo is the base repo (not a fork, not a
deleted head repo) and its author is the dev-lead runtime identity
(persona-standards.md 5.1: don-petry), overridable via DEV_LEAD_LOGINS
(comma-separated, JSON-encoded by jq rather than spliced raw into the filter).
Fail-closed behaviour on API errors is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mmands (#1396)

The auto-rebase reusable pasted the PR's base branch raw into the
`git rebase origin/<base>` / `git merge origin/<base>` commands it publishes for
maintainers to copy. Git branch names may contain shell syntax.

Add auto_rebase_shell_quote to lib/comments.sh (plain refs stay unquoted for
readability; anything else is single-quoted with ' -> '\'' , valid in bash, zsh
and sh) and use it for both commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out (#1405)

The scan ran `npx ecc-agentshield` with the PR checkout as cwd. npm loads
project config from the .npmrc of its local prefix (the cwd), so the scanned
PR's own .npmrc could set script-shell or registry for the scanner, e.g. make
the scan step exit 0 without scanning.

Run npx from an empty prefix under $RUNNER_TEMP (--prefix pinned to it) and
pass the checkout as the absolute --path scan target. Version pin and
--ignore-scripts are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@don-petry
don-petry requested a review from a team as a code owner October 28, 2026 03:21
@chatgpt-codex-connector

This comment has been minimized.

@coderabbitai

This comment has been minimized.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@don-petry

Copy link
Copy Markdown
Contributor Author

Dev-Lead — review-changes (no-changes)

No changes were needed for this PR.

@don-petry
don-petry enabled auto-merge (squash) October 28, 2026 03:24
@donpetry-bot

Copy link
Copy Markdown
Contributor

Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-28T04:25:28Z.

@donpetry-bot

Copy link
Copy Markdown
Contributor

No description provided.

@don-petry
don-petry disabled auto-merge October 28, 2026 05:27
@don-petry

Copy link
Copy Markdown
Contributor Author

This is a notice that the Codex review tool encountered an issue and could not complete the review run. The actual CI checks (linting, security scans, CodeQL, etc.) all passed successfully. This is an operational notice with no actionable code findings to address.

@don-petry

Copy link
Copy Markdown
Contributor Author

Dev-Lead — fix-bot-comment (no-changes)

Agent reasoning
**Bot:** chatgpt-codex-connector[bot]  
**Issues addressed:** 0 (non-actionable notice)  
**Disposition:** Posted `disposition=informational` on the bot's issue comment
**Analysis:**
The bot comment reports that the Codex review tool encountered an error and could not complete its review run. This is **not a code finding** — it's an operational notice. All actual CI checks (linting, security scanning, CodeQL, SonarCloud, etc.) passed successfully.
**Action taken:**
- Verified the comment is authored by the correct bot and not already resolved
- Posted a disposition reply explaining this is a tool failure notice with no actionable code issues
- Marked as `disposition=informational` to clear the maintainer-comment gate
No code changes needed. The PR's actual checks all passed.

@don-petry
don-petry enabled auto-merge (squash) October 28, 2026 05:28
@don-petry

don-petry commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

No description provided.

@don-petry
don-petry disabled auto-merge October 28, 2026 08:35
@don-petry

Copy link
Copy Markdown
Contributor Author

This is a rate-limit notice indicating CodeRabbit has reached its free OSS review quota. There are no code findings to address—the bot is simply waiting for the quota to reset in 44 minutes.

@don-petry

Copy link
Copy Markdown
Contributor Author

Dev-Lead — fix-bot-comment (no-changes)

Agent reasoning
Issues addressed: 0 (rate-limit notice only — no findings)
Disposition: informational (operational notice, no code changes needed)
Files changed: none
Comment node: IC_kwDORyesfc8AAAABgQi7SQ → dispositioned via reply
```
All CI checks pass. PR is ready for merge once the rate limit resets and CodeRabbit completes its review (or approval is otherwise granted).

@donpetry-bot

Copy link
Copy Markdown
Contributor

Automated review — escalated to human

The automated review cascade escalated this PR to a human reviewer at review cycle 0/3 (risk: HIGH, reviewed commit 29d9d94ddfedf19ac0b81add4cd65f149772472b).

Why: the cascade could neither approve the PR nor auto-request fixes, so it requested human review via CODEOWNERS and set the needs-human-review label. A human should review the PR, or remove the needs-human-review label to re-engage the automated cascade.

Reviewer summary: Four security fixes, one commit each: neutralise runner-command markers in logged project titles (#1384); count only same-repo PRs by the dev-lead identity as dev-lead activity (#1391); shell-quote the base ref in auto-rebase recovery commands (#1396); run the AgentShield scanner's npx from a clean prefix outside the PR checkout (#1405). Each fix has regression tests. I found no defects. Triage cleared this as low-risk, but I am not confirming that. It changes trust boundaries: a login-based allow-list decides which PRs count as agent activity, and the org's security-scanner reusable now runs differently. Both are HIGH under the review taxonomy, so a human should sign off. CI is fully green.

This note is updated in place on re-escalation; it is not re-posted.

@don-petry

Copy link
Copy Markdown
Contributor Author

dev-lead is withholding action on this item.

It is labeled needs-human-review (flagged for human review — this label is applied by automation as well as by people, so an item can become held without anyone noticing), so dev-lead will not pick it up while that label is present. This notice is posted once so the withhold is visible rather than looking like a stalled run.

To re-enable automated pickup: remove the needs-human-review label.

@donpetry-bot

Copy link
Copy Markdown
Contributor

Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-28T12:47:42Z.

@donpetry-bot

Copy link
Copy Markdown
Contributor

Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-28T16:41:10Z.

@donpetry-bot

Copy link
Copy Markdown
Contributor

Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-30T02:37:45Z.

coderabbitai[bot]
coderabbitai Bot previously requested changes Nov 2, 2026

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix comments: Fix CodeRabbit comments on this PR

Comment thread scripts/lib/dev-lead-retrigger.sh Resolved
GitHub logins are case-insensitive, so a DEV_LEAD_LOGINS entry whose casing
differs from the API's missed the agent's active PR, and the caller could cycle
its trigger label and start duplicate work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Nov 2, 2026

Copy link
Copy Markdown

@don-petry

This comment has been minimized.

@don-petry
don-petry disabled auto-merge November 2, 2026 22:04
@don-petry
don-petry dismissed coderabbitai[bot]’s stale review November 2, 2026 22:05

Addressed: logins compared case-insensitively via ascii_downcase; CodeRabbit confirmed in the thread.

@don-petry

Copy link
Copy Markdown
Contributor Author

@donpetry-bot please review

@donpetry-bot

Copy link
Copy Markdown
Contributor

@don-petry I'm on it — starting a fresh review now. Results will appear in a few minutes.

@don-petry
don-petry enabled auto-merge (squash) November 2, 2026 23:40
@don-petry

Copy link
Copy Markdown
Contributor Author

@donpetry-bot please review

@donpetry-bot

Copy link
Copy Markdown
Contributor

@don-petry I'm on it — starting a fresh review now. Results will appear in a few minutes.

@donpetry-bot donpetry-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review — APPROVED ✓

Risk: MEDIUM
Reviewed commit: 674f483c00a7a756194c8a7e979db51a625d1728
Review mode: triage-approved (single reviewer)

Summary

Four security hardening fixes, one commit each, with regression tests: neutralise runner-command markers in logged project titles (#1384); count only same-repo PRs by the dev-lead identity as dev-lead activity (#1391); shell-quote the base ref in auto-rebase recovery commands (#1396); run AgentShield's npx from a clean prefix outside the PR checkout (#1405). The only change since the last cascade review (29d9d94) is a case-insensitive login comparison, which answers CodeRabbit's one finding. I found no defects and CI is fully green. I rate this MEDIUM, not HIGH (reasoning under Findings).

Linked issue analysis

  • #1384: all five title log sites in sweep_project and the add_draft_item dry-run line now go through atp_log_safe. It turns CR/LF into spaces and puts a ZWSP inside every ##[ and ::. The loop handles ::: / ::::. Addressed.
  • #1391: the selector now requires a same-repo head (.head.repo != null and full_name equal to the base repo) and an author in DEV_LEAD_LOGINS (default don-petry, compared case-insensitively). The logins are JSON-encoded by jq -R before they go into the filter. It still fails closed on API errors. Addressed.
  • #1396: MANUAL_BASE_REF is computed once with auto_rebase_shell_quote and used in both the git rebase and git merge snippets. Addressed.
  • #1405: npx runs from $RUNNER_TEMP/agentshield-npm with --prefix and a minimal package.json, and scans the absolute checkout path. --ignore-scripts and the version pin are kept. Addressed.

Findings

No blocking findings.

What I checked myself:

  • auto_rebase_shell_quote (bash 5.2): origin/main comes back unchanged. origin/a'b, origin/x';touch PWNED;'y, origin/$(id) and origin/&&`id` each come back intact through eval, and none of them ran a command.
  • atp_log_safe: x ##[error]…, an embedded \n::error::, :::x and :::: all come out with no remaining ##[ or ::.
  • Logins filter: the DEV_LEAD_LOGINS parser turns "don-petry, Foo ,," into ["don-petry","foo"]. A malformed value makes the jq filter fail, which falls into the existing fail-closed path (treated as active). That is the safe direction.
  • AgentShield coverage: this repo's caller uses the local reusable (ring0 dogfood), so the PR's CI ran the new step. Its output (Files scanned: 2, grade A, 0 findings) matches the latest main run. The scanner covers the same files from the new prefix and has not gone quiet.
  • Review threads and AGENTS.md: the one CodeRabbit thread (case-sensitive logins) is resolved by 674f483 and CodeRabbit confirmed the fix. Decision logic stays in bats-tested scripts with the workflows as thin glue, as AGENTS.md requires.

Why MEDIUM, not HIGH: the earlier cascade escalated because of trust-boundary changes. On the content I disagree:

  • No secrets, tokens, permissions, third-party actions or new attack surface are added. Each workflow change narrows an existing injection or config-hijack path.
  • The login allow-list only decides whether to skip cycling a label. It controls no access, and it still fails closed.
  • The scanner change has been checked against real CI output.
  • Both reusable-workflow changes still reach callers only through the stable channel and canary rings.

Author's request to double-check #1391: a same-repo dev-lead/issue-<n>-* PR opened by anyone other than the dev-lead identity no longer suppresses the retrigger. This is intended and consistent with every existing dev-lead PR.

Non-blocking notes:

  • Secret-scanning MCP tool is not available in this run. gitleaks CI passed.
  • The needs-human-review label from the earlier escalation is still on the PR. The owner has since asked for this re-review.

CI status

All required checks green: Shellcheck/bats, Lint, CodeQL (actions and python), SonarCloud (quality gate passed, 0 new issues), gitleaks, AgentShield, Agent Security Scan, dependency-audit and duplicate-decl-gate. CodeRabbit is NEUTRAL. The skipped jobs are the dev-lead and dependabot ones and package-manager audits that do not apply here.


Reviewed automatically by the PR-review agent (single-reviewer mode: opus 5.5 [opus 4.8, opus 4.7]). Reply if you need a human review.

@donpetry-bot

Copy link
Copy Markdown
Contributor

pr-review approved on PARTIAL advisory evidence: 2/5 required advisory bots reported before the gate's head-age-timeout fallback proceeded. Recorded for the miss-rate metric (#1596).

@don-petry

Copy link
Copy Markdown
Contributor Author

Maintainer disposition of coderabbitai's status comment, posted by a Claude Code session with the maintainer's approval:

A review-limit notice with no findings. CodeRabbit has since reviewed this PR, and its one inline finding (login case) was fixed in 674f483 and the thread resolved.

@don-petry
don-petry merged commit 0a2df35 into main Nov 2, 2026
71 checks passed
@don-petry
don-petry deleted the fix/sec-workflow-hardening-batch branch November 2, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-lead:hands-off Opt an item out of the dev-lead persona automation entirely

Projects

None yet

2 participants