Skip to content

Replace template-injection grep with parsed workflow check - #55863

Merged
pelikhan merged 2 commits into
mainfrom
copilot/deep-report-replace-grep-with-ast-check
Aug 25, 2026
Merged

Replace template-injection grep with parsed workflow check#55863
pelikhan merged 2 commits into
mainfrom
copilot/deep-report-replace-grep-with-ast-check

Conversation

Copilot AI commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Daily Secrets Analysis reported thousands of github.event.* matches using a line-oriented grep that could not distinguish env: assignments from executable shell interpolation. This replaces it with a deterministic parsed-workflow check targeting only executable run: blocks.

  • Template-injection guard

    • Parse each compiled .lock.yml.
    • Inspect only run: script values for direct github.event.* expressions.
    • Ignore expressions in env:, shell comments, and heredoc content.
  • Daily report integration

    • Replace the heuristic grep with the focused Go guard.
    • Report direct shell interpolation as a template-injection risk.
  • Regression coverage

    • Cover safe environment-variable assignment and unsafe direct interpolation.
env:
  ISSUE_TITLE: ${{ github.event.issue.title }}
run: echo "$ISSUE_TITLE" # ignored

run: echo "${{ github.event.issue.title }}" # reported

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Replace heuristic github.event.* template-injection grep with AST-based check Replace template-injection grep with parsed workflow check Aug 25, 2026
Copilot AI requested a review from pelikhan August 25, 2026 20:34
@pelikhan
pelikhan marked this pull request as ready for review August 25, 2026 22:31
Copilot AI balanced review requested due to automatic review settings August 25, 2026 22:31
@pelikhan
pelikhan merged commit 818e3a3 into main Aug 25, 2026
48 of 60 checks passed
@pelikhan
pelikhan deleted the copilot/deep-report-replace-grep-with-ast-check branch August 25, 2026 22:32
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Generated by Ponytail Reviewer for #55863

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-25T22:36:00Z
review_event: REQUEST_CHANGES
top_themes:
  - narrowed template-injection coverage misses non-github.event unsafe contexts
  - workflow report no longer matches broader compiler validation scope
files_reviewed:
  - .github/workflows/daily-secrets-analysis.lock.yml
  - .github/workflows/daily-secrets-analysis.md
  - pkg/workflow/lock_file_template_injection_test.go
comment_count: 0

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · pi · gpt54 · 6.75 AIC · ⌖ 6.88 AIC · ⊞ 4.6K ·
Comment /review to run again

@github-actions github-actions 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.

Requesting changes

This narrows the false positives from the old grep, but it also weakens the guard from "no GitHub expressions in run scripts" to "only no github.event expressions in run scripts". That leaves a real regression hole for other untrusted contexts that were previously covered by the broader compiler validation.

Blocking themes
  • The new test only flags github.event.*, so direct ${{ inputs.* }} and ${{ steps.*.outputs.* }} interpolation in run: blocks can now slip through this daily check even though they are still template-injection sources.
  • The workflow report now claims the check is a template-injection guard, but it is no longer aligned with the broader unsafe-context validation already implemented in pkg/workflow.

🔎 Code quality review by PR Code Quality Reviewer · pi · gpt54 · 6.75 AIC · ⌖ 6.88 AIC · ⊞ 4.6K
Comment /review to run again

@github-actions github-actions 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.

Skills-Based Review 🧠

Applied /tdd and /diagnosing-bugs — requesting changes on two correctness gaps and one silent-failure risk.

📋 Key Themes & Highlights

Key Themes

  • Unquoted heredoc false negative (/tdd): removeHeredocContent strips unquoted heredocs, but GitHub substitutes ${{ ... }} before the shell runs — so unquoted heredocs are still an injection vector and the test suite doesn't cover this case.
  • Narrow injection scope (/tdd): The guard only matches github.event.*; other well-known injectable contexts (github.head_ref, github.base_ref) are silently ignored with no test or documented rationale.
  • Silent guard bypass (/diagnosing-bugs): The workflow script soft-skips when go is not on PATH, exiting 0 and printing an info message. If the container lacks Go, the entire template-injection check is a no-op.

Positive Highlights

  • ✅ Replacing the line-oriented grep with an AST-aware parsed check is exactly the right fix — eliminating the env: false positives was the core problem.
  • ✅ The test structure is clean and readable (Arrange/Act/Assert per case).
  • ✅ Reusing the existing extractRunBlocks / stripShellLineComments / removeHeredocContent helpers rather than duplicating parsing logic is good deep-module design.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet46 · 48.4 AIC · ⌖ 11.4 AIC · ⊞ 7.6K
Comment /matt to run again

Comments that could not be inline-anchored

pkg/workflow/lock_file_template_injection_test.go:100

[/tdd] The heredoc test only covers a quoted delimiter (&lt;&lt; &#39;EOF&#39;). removeHeredocContent also strips unquoted heredocs, but GitHub Actions substitutes ${{ ... }} expressions before the shell runs — so content inside an unquoted heredoc is still attacker-controlled at expression-evaluation time. The current test suite won't catch a false negative here.

<details>
<summary>💡 Add an unquoted heredoc test and document the intended behaviour</summary>

{
    name: &quot;unquoted h</details>

<details><summary>pkg/workflow/lock_file_template_injection_test.go:33</summary>

**[/tdd]** The test checks `github.event.*` expressions but the guard could also miss **`github.event_path`** or **`github.head_ref`** which are equally injectable. The current check `strings.Contains(expression, &quot;github.event.&quot;)` is a substring match, not a structural one`github.event_path` does NOT match `github.event.` (note the dot), so it would silently pass.

&lt;details&gt;
&lt;summary&gt;💡 Broaden the match or add a test for the known cases&lt;/summary&gt;

Add tests for the other commonly exploitabl</details>

<details><summary>.github/workflows/daily-secrets-analysis.md:10</summary>

**[/diagnosing-bugs]** The `go test` fallback in the workflow script uses `command -v go` to gate execution, and silently skips with an info message when Go is unavailable. This means the template-injection check **never actually runs** inside the compiled workflow&#39;s `run:` shellthe workflow executes in a sandboxed container where `go` might not be on `PATH`. If the guard is bypassed at runtime, it provides a false sense of security.

&lt;details&gt;
&lt;summary&gt;💡 Verify the Go toolchain is availabl</details>

@github-actions github-actions 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.

The new guard is useful, but the test can be trimmed by leaning on the existing validator instead of a parallel parser. net: -16 lines possible.

Generated by ✂️ Ponytail Reviewer for #55863 · codex · mai10 · 7.42 AIC · ⌖ 1.94 AIC · ⊞ 16.7K
Comment /ponytail to run again

// findGitHubEventRunExpressions parses a compiled workflow and returns github.event
// expressions that reach an executable run: block. Expressions assigned to env: are
// deliberately excluded because the shell receives their values as data.
func findGitHubEventRunExpressions(lockContent []byte) ([]string, error) {

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.

L19-35: shrink: bespoke YAML walk and string scan for github.event in run blocks. Reuse the existing validator path in pkg/workflow/template_injection_validation.go and assert on its error.

@github-actions github-actions 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.

Good improvement — replacing the fragile grep with parsed YAML inspection eliminates false positives from env: assignments and is a clear correctness win.

Two non-blocking issues worth addressing:

  1. Coverage gap (line 29): strings.Contains(expression, "github.event.") (trailing dot) won't catch ${{ toJSON(github.event) }} or bare ${{ github.event }}. Drop the trailing dot.
  2. Test isolation (line 50): require.NoError on YAML parse failures calls t.FailNow() and masks results for all remaining lock files. Prefer assert.NoError + continue.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · copilot · sonnet46 · 38.2 AIC · ⌖ 9.28 AIC · ⊞ 6.2K

for _, runContent := range extractRunBlocks(workflow) {
executableContent := stripShellLineComments(removeHeredocContent(runContent))
for _, expression := range InlineExpressionPattern.FindAllString(executableContent, -1) {
if strings.Contains(expression, "github.event.") {

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.

The check strings.Contains(expression, "github.event.") (note the trailing dot) misses expressions like ${{ toJSON(github.event) }} or ${{ github.event }} that access the full event object without a property suffix. These can still carry attacker-controlled data and represent real injection risks.

Consider broadening the match to strings.Contains(expression, "github.event") (no trailing dot) and add a test case:

{
    name: "toJSON(github.event) in run is reported",
    yaml: `
jobs:
  test:
    steps:
      - run: echo "${{ toJSON(github.event) }}"
`,
    expected: []string{"${{ toJSON(github.event) }}"},
},

@copilot please address this.

require.NoError(t, err, "should read %s", lockFile)

violations, err := findGitHubEventRunExpressions(lockContent)
require.NoError(t, err, "should parse %s as YAML", lockFile)

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.

The loop uses require.NoError for YAML parse errors, which calls t.FailNow() and stops the entire test on the first unparseable file. This means a corrupted lock file silently hides failures in all subsequent files.

Consider using assert.NoError + continue so all files are checked:

violations, err := findGitHubEventRunExpressions(lockContent)
if !assert.NoError(t, err, "should parse %s as YAML", lockFile) {
    continue
}
assert.Empty(t, violations, ...)

@copilot please address this.

Copilot AI 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.

Pull request overview

Replaces a noisy grep-based template-injection check with parsed workflow inspection.

Changes:

  • Adds a Go guard for github.event.* expressions in run: blocks.
  • Integrates the guard into the daily secrets report.
  • Regenerates workflow metadata.
Show a summary per file
File Description
pkg/workflow/lock_file_template_injection_test.go Adds detection and regression tests.
.github/workflows/daily-secrets-analysis.md Runs the parsed-workflow guard.
.github/workflows/daily-secrets-analysis.lock.yml Updates generated metadata.

Review details

Suppressed comments (1)

pkg/workflow/lock_file_template_injection_test.go:29

  • This textual scan misses valid unsafe expressions. InlineExpressionPattern fails on ${{ format('{0}', github.event.issue.title) }} because the string literal contains }, and the subsequent substring check misses equivalent index syntax such as ${{ github['event']['issue']['title'] }}. Use expression-aware extraction/reference detection and add both forms as regressions; otherwise the new security gate can pass direct event-data interpolation.
		for _, expression := range InlineExpressionPattern.FindAllString(executableContent, -1) {
			if strings.Contains(expression, "github.event.") {
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Balanced


var violations []string
for _, runContent := range extractRunBlocks(workflow) {
executableContent := stripShellLineComments(removeHeredocContent(runContent))
}

var violations []string
for _, runContent := range extractRunBlocks(workflow) {
@github-actions

Copy link
Copy Markdown
Contributor

Test comment

🧪 Test quality analysis by Test Quality Sentinel · copilot · haiku45 · 41.6 AIC · ⌖ 6.01 AIC · ⊞ 8.3K ·
Comment /review to run again

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR adds 111 lines to pkg/workflow/ (a business-logic directory), crossing the 100-line threshold that requires an Architecture Decision Record before merge.

No ADR was found in the PR body, in docs/adr/, or in the linked issue #55851.


What to do

Add an ADR at docs/adr/55863-replace-template-injection-grep-with-go-ast-check.md and link it in this PR's description. A draft has been generated below based on the PR diff — review it, fill any gaps, and commit it to the branch.

📄 Draft ADR (click to expand)
# ADR-55863: Replace Template-Injection Grep with Go AST-Based Check

**Date**: 2026-08-25
**Status**: Draft
**Deciders**: Unknown

---

### Context

The Daily Secrets Analysis report used a line-oriented grep (`grep -rn "github.event."`) to flag potential template-injection risks in compiled `.lock.yml` workflow files. Because grep cannot distinguish YAML structural contexts, it matched `github.event.*` references inside `env:` assignments—which are safe, as the shell receives those values as data—as well as inside shell comments and heredoc content. On the 2026-08-25 run this produced 4,946 flagged lines, explicitly noted as a "high false-positive rate," making the signal unreliable and requiring manual review of every run. The existing `pkg/workflow` package already parses `${{ }}` expressions during lock-file compilation, providing a reusable YAML/expression-parsing foundation for a precise check.

### Decision

We will replace the heuristic grep with a Go test (`TestCompiledLockFiles_NoGitHubEventExpressionsInRunScripts`) in `pkg/workflow/` that YAML-parses each compiled `.lock.yml` file, walks only the `run:` script values, strips shell comments and heredoc content, and reports any `github.event.*` inline expressions it finds there. The daily report's bash section delegates to this Go test when the Go toolchain is available, and prints an informational skip message otherwise. This produces a deterministic, zero-false-positive signal that can also gate CI.

### Alternatives Considered

#### Alternative 1: Enhanced Multi-Stage Grep

Extend the existing grep with additional pipe stages to exclude `env:` lines (e.g., `grep ... | grep -v "env:"` or negative-lookahead patterns). This avoids introducing a Go dependency in the daily report script. Rejected because text-based approaches cannot reliably distinguish structural YAML contexts—`env:` may appear in a block nested under `run:`, and line-by-line filtering cannot resolve that ambiguity without parsing the YAML structure.

#### Alternative 2: Accept False Positives and Review Manually

Keep the existing grep and rely on human reviewers to eyeball each hit per report cycle. No code change required. Rejected because 4,946 hits per run makes manual review infeasible; the signal is so noisy it is effectively suppressed in practice, leaving real template-injection risks undetected.

### Consequences

#### Positive
- Eliminates false positives: `env:` assignments, shell comments, and heredoc content are no longer flagged, reducing noise from ~4,946 hits to only genuine `run:` interpolations.
- The check is enforceable in CI as a standard Go test, not just during the daily report.
- Reuses the existing YAML and expression-parsing infrastructure in `pkg/workflow/`, minimising new code surface.
- Unit tests cover safe and unsafe patterns explicitly, making the detection logic auditable and easy to extend.

#### Negative
- Requires the Go toolchain at daily-report execution time; when Go is unavailable the shell script emits an informational skip and defers enforcement to CI, creating a gap in local/ad-hoc runs.
- Adds a new test file to `pkg/workflow/`, increasing the package's test surface and the scope of changes that could break the guard.

#### Neutral
- The daily-report bash section is simplified by delegating detection logic to the Go test rather than maintaining inline grep pipelines.
- The guard runs only over compiled `.lock.yml` files, not over raw `.md` skill sources, which is consistent with the threat model (compiled files are what GitHub Actions actually executes).

---

*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*

Note: The draft ADR could not be pushed directly to the PR branch (workflow checked out the merge commit rather than the PR head ref). Please copy the draft above into docs/adr/55863-replace-template-injection-grep-with-go-ast-check.md and commit it to your branch, then link to it in this PR's description.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · claude · sonnet46 · 71.3 AIC · ⌖ 16.3 AIC · ⊞ 8.4K ·
Comment /review to run again

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.87.8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[deep-report] Replace heuristic github.event.* template-injection grep with AST-based check

3 participants