Skip to content

Add rule to block toJSON(secrets) expressions#44922

Merged
pelikhan merged 7 commits into
mainfrom
copilot/update-go-compiler-rules
Jul 11, 2026
Merged

Add rule to block toJSON(secrets) expressions#44922
pelikhan merged 7 commits into
mainfrom
copilot/update-go-compiler-rules

Conversation

Copilot AI commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Expressions like ${{ toJSON(secrets) }} serialize the entire secrets context, exposing all secrets to the agent. This adds a dedicated compiler rule to catch this pattern.

Behaviour

  • Strict mode (default): compilation error
  • Non-strict (strict: false): warning, compilation proceeds

Changes

  • expression_secrets_serialization_validation.go — new rule:

    • Case-insensitive regex matching toJSON(secrets) but not toJSON(secrets.MY_SECRET) (specific refs are fine)
    • validateSecretsSerializationExpressions — errors or warns based on effective strict mode
    • neutralizeSecretsSerializationExpressions — replaces matches with ${{ false }} in non-strict mode before the allowlist check runs, preventing a confusing secondary error
  • compiler_validators.go — wires the new check into validateExpressions before the existing allowlist-based validateExpressionSafety

  • expression_secrets_serialization_validation_test.go — 23 cases covering strict/non-strict, markdown body vs frontmatter YAML, case variants, and safe patterns

Safe patterns (not flagged)

${{ secrets.MY_SECRET }}
${{ toJSON(secrets.MY_SECRET) }}
${{ toJSON(steps) }}

Copilot AI and others added 3 commits July 11, 2026 08:57
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Add toJSON(secrets) secrets serialization validation rule Add rule to block toJSON(secrets) expressions Jul 11, 2026
Copilot AI requested a review from pelikhan July 11, 2026 09:15
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

Hey @copilot-swe-agent 👋 — thanks for working on this security rule to catch toJSON(secrets) serialization! The implementation looks thoughtful, with good test coverage (23 cases) and a clear description.

However, this PR was submitted directly to the repository, which does not match the contribution process for non-core team members.

  • Process issue — This project requires non-core contributors to open a detailed issue with an agentic plan first, rather than submitting a PR directly. A core team member then picks up the plan and implements it using agents. Please close this PR and open an issue describing the toJSON(secrets) security rule you would like to add, following the agentic plan format described in CONTRIBUTING.md.

If you would like to reformulate this as an agentic plan issue, here is a ready-to-use prompt:

Create a GitHub issue in the github/gh-aw repository proposing a new compiler validation rule.

Title: Add compiler rule to block toJSON(secrets) expressions

Body should describe:
1. The security problem: expressions like `${{ toJSON(secrets) }}` serialize the entire secrets context, exposing all secrets to the agent.
2. Proposed behaviour:
   - Strict mode (default): compilation error
   - Non-strict (strict: false): warning, compilation proceeds, expression replaced with `${{ false }}`
3. Implementation sketch:
   - New file `pkg/workflow/expression_secrets_serialization_validation.go`
   - Wire into `validateExpressions` in `compiler_validators.go`
   - Test cases covering strict/non-strict, markdown body vs frontmatter YAML, case variants, and safe patterns
4. Safe patterns that should NOT be flagged:
   - `${{ secrets.MY_SECRET }}`
   - `${{ toJSON(secrets.MY_SECRET) }}`
   - `${{ toJSON(steps) }}`

Generated by ✅ Contribution Check · 107.3 AIC · ⌖ 11.5 AIC · ⊞ 6.2K ·

@pelikhan pelikhan marked this pull request as ready for review July 11, 2026 09:27
Copilot AI review requested due to automatic review settings July 11, 2026 09:27
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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

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

Adds validation preventing workflows from serializing the complete secrets context.

Changes:

  • Detects and reports toJSON(secrets) based on strict mode.
  • Integrates detection before expression allowlist validation.
  • Adds validation tests and modifies the auto-upgrade schedule.
Show a summary per file
File Description
pkg/workflow/expression_secrets_serialization_validation.go Implements detection and neutralization.
pkg/workflow/expression_secrets_serialization_validation_test.go Tests strict, non-strict, and safe patterns.
pkg/workflow/compiler_validators.go Integrates the new validator.
.github/workflows/agentic-auto-upgrade.yml Changes the weekly schedule.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Medium

Comment on lines +80 to +83
return ExpressionPatternDotAll.ReplaceAllStringFunc(content, func(match string) string {
groups := ExpressionPatternDotAll.FindStringSubmatch(match)
if len(groups) >= 2 && secretsSerializationPattern.MatchString(groups[1]) {
return "${{ false }}"
// toJSON(secrets.SPECIFIC_KEY) because a dot after "secrets" would be consumed
// by \s*\) only if there is no further content — the closing \) requires that
// nothing follows "secrets" except optional whitespace.
var secretsSerializationPattern = regexp.MustCompile(`(?i)\btoJSON\s*\(\s*secrets\s*\)`)
Comment on lines +209 to +211
} else {
assert.NoError(t, err, "expected no error")
}
on:
schedule:
- cron: "21 3 * * 5" # Weekly (auto-upgrade)
- cron: "11 4 * * 6" # Weekly (auto-upgrade)
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 88/100 — Excellent

Analyzed 3 test(s): 3 design, 0 implementation, 0 violation(s).

📊 Metrics (3 tests)
Metric Value
Analyzed 3 (Go: 3, JS: 0)
✅ Design 3 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 2/3 (67%)
Duplicate clusters 0
Inflation No (231 test lines / 132 prod lines ≈ 1.75:1)
🚨 Violations 0
Test File Classification Issues
TestFindSecretsSerializationExpressions ..._test.go:14 design_test / high_value None
TestValidateSecretsSerializationExpressions ..._test.go:87 design_test / high_value wantWarning field declared but never asserted
TestValidateSecretsSerializationViaValidateExpressions ..._test.go:218 design_test / high_value None
⚠️ Flagged Tests (1)

TestValidateSecretsSerializationExpressions (expression_secrets_serialization_validation_test.go:87) — The struct field wantWarning bool is declared and set to true for non-strict mode cases, but the test body never reads or asserts on it. Non-strict cases only confirm err == nil, not that a warning was actually emitted to stderr. This leaves the warning-emission code path unverified. Fix: capture os.Stderr (or redirect via a pipe) and assert the output contains the expected warning text when wantWarning: true.

Verdict

Passed. 0% implementation tests (threshold: 30%). No guideline violations. Build tag //go:build !integration is present on line 1. Minor gap: wantWarning assertions are incomplete but not blocking.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 35.5 AIC · ⌖ 10.3 AIC · ⊞ 6.8K ·
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.

✅ Test Quality Sentinel: 88/100. 0% implementation tests (threshold: 30%). All 3 tests are design-level with table-driven coverage. Minor gap: wantWarning assertions incomplete but not blocking.

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

Review: Add rule to block toJSON(secrets) expressions

The new rule is well-structured and consistent with existing patterns.

One issue: The wantWarning field in TestValidateSecretsSerializationExpressions is declared and set for three test cases but is never asserted — the test body only checks NoError/Error, never verifying that IncrementWarningCount() was actually called. A regression dropping that call in non-strict mode would pass silently.

Everything else looks good
  • Regex correctly excludes toJSON(secrets.KEY) via the closing ) constraint
  • neutralizeSecretsSerializationExpressions cleanly prevents double-errors on the non-strict path
  • Frontmatter YAML is scanned but the allowlist only scans MarkdownContent, so no double-error risk
  • Coverage spans strict/non-strict, markdown/frontmatter, case variants, and safe patterns
  • Integration wiring in validateExpressions is correct and ordered before the allowlist check

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 34.4 AIC · ⌖ 5.46 AIC · ⊞ 4.8K

strictMode bool
wantError bool
errorContains string
wantWarning bool

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 wantWarning field is declared and set in several test cases but is never checked in the assertion block — the test runner never verifies that IncrementWarningCount was actually called.

Consider adding an assertion like:

if tt.wantWarning {
    assert.Equal(t, 1, compiler.GetWarningCount(), "expected exactly one warning")
}

(adjust the accessor name to whatever the Compiler exposes.)

Without this, a regression that silently skips c.IncrementWarningCount() in non-strict mode would pass all tests.

@copilot please address this.

@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 — commenting with test-coverage gaps to address.

📋 Key Themes & Highlights

Key Themes

  • Untested warning assertions: wantWarning fields in 3 table-driven cases are never checked, so warning regressions pass silently
  • Missing non-strict integration test: TestValidateSecretsSerializationViaValidateExpressions only exercises the strict error path; a matching non-strict path would catch neutralisation bugs
  • Minor: regex doc comment could be clearer about the exact mechanism that excludes secrets.KEY patterns

Positive Highlights

  • ✅ Excellent use of the existing strict/non-strict pattern — consistent with the rest of the codebase
  • neutralizeSecretsSerializationExpressions is a clean solution for preventing confusing secondary allowlist errors
  • ✅ Case-insensitive regex is correctly scoped: toJSON(secrets) flagged, toJSON(secrets.KEY) and toJSON(steps) safe
  • ✅ 23 test cases provide solid baseline coverage
  • ✅ Check is wired before allowlist validation with a clear comment explaining the ordering

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 47.9 AIC · ⌖ 5.56 AIC · ⊞ 6.6K
Comment /matt to run again

}
} else {
assert.NoError(t, err, "expected no 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.

[/tdd] wantWarning is declared in every non-strict test case but never asserted — the tests pass vacuously even if no warning is emitted.

💡 Suggested assertion

After calling validateSecretsSerializationExpressions, check the compiler warning count:

if tt.wantWarning {
    assert.Equal(t, 1, compiler.WarningCount(), "expected exactly one warning")
}

Without this, regressions in the warning path (e.g. accidentally removing IncrementWarningCount) go undetected.

@copilot please address this.

// secretsSerializationPattern matches function calls that pass the entire secrets
// context as an argument, e.g. toJSON(secrets).
//
// GitHub Actions function names are case-insensitive, so (?i) is used. The

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.

[/diagnosing-bugs] The regex comment says "nothing follows 'secrets' except optional whitespace" but that description is slightly misleading: the actual guard is \s*\) which only checks the closing paren. toJSON(secrets .DOTTED) (with a space before the dot) would not be flagged, which is the intended behaviour — but the comment doesn't make the space-before-dot edge case explicit. This could cause confusion for future maintainers.

💡 Suggested comment clarification
// The pattern matches toJSON(secrets) but NOT toJSON(secrets.KEY) because
// the closing \) requires that "secrets" is immediately followed by optional
// whitespace then ')'. Any dot or alphanumeric character after "secrets"
// causes the \s*\) to fail, so specific property accesses are safe.
var secretsSerializationPattern = ...

@copilot please address this.

RawFrontmatter: map[string]any{},
}

err := compiler.validateExpressions(workflowData, "/tmp/test.md")

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.

[/tdd] TestValidateSecretsSerializationViaValidateExpressions only covers the strict-mode error path via validateExpressions. A non-strict integration test is missing: it should verify that in non-strict mode the call returns nil and the compiler warning count increments (and that no secondary allowlist error is surfaced).

💡 Suggested additional test
func TestValidateSecretsSerializationNonStrictViaValidateExpressions(t *testing.T) {
    compiler := NewCompiler()
    compiler.strictMode = false

    workflowData := &WorkflowData{
        Name:            "Test",
        MarkdownContent: "Expose everything: ${{ toJSON(secrets) }}",
        RawFrontmatter:  map[string]any{"strict": false},
    }

    err := compiler.validateExpressions(workflowData, "/tmp/test.md")
    assert.NoError(t, err, "non-strict should not return an error")
    assert.Equal(t, 1, compiler.WarningCount(), "expected one warning")
}

@copilot please address this.

@github-actions

Copy link
Copy Markdown
Contributor

Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (379 new lines in pkg/) but does not have a linked Architecture Decision Record (ADR).

Draft ADR committed: docs/adr/44922-block-tojson-secrets-serialization-expressions.md — review and complete it before merging.

This PR cannot merge until an ADR is linked in the PR body.

What to do next
  1. Review the draft ADR committed to your branch (docs/adr/44922-block-tojson-secrets-serialization-expressions.md) — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and verify the alternatives accurately reflect what you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:
ADR: [ADR-44922: Block toJSON(secrets) Serialization](docs/adr/44922-block-tojson-secrets-serialization-expressions.md)

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 44922-block-tojson-secrets-serialization-expressions.md for this PR).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 57.5 AIC · ⌖ 12.9 AIC · ⊞ 8.5K ·
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.

REQUEST_CHANGES — 3 medium issues, 1 blocking testability gap

The security intent is sound and the core regex logic is correct. However there are issues that must be addressed before merge.

🔍 Findings summary

1. wantWarning is never asserted (blocking)
Tests define wantWarning: true for all non-strict cases but the assertion is never made. The entire warning code path — including the content and presence of the warning — is unverified. This is a direct consequence of writing to os.Stderr directly; the fix requires making the output writer injectable.

2. Log reports c.strictMode but enforcement uses effectiveStrictMode (medium)
When frontmatter overrides the compiler default, the log says strictMode=true while the code behaves non-strictly. Actively misleading during debugging.

3. Expression reported in errors is inconsistently normalized (medium)
findSecretsSerializationExpressions trims outer whitespace of the captured group but not inner — the reported expression in error messages is neither the literal source text nor a canonical form. Use match[0] to report the original, or normalize fully.

🔎 Code quality review by PR Code Quality Reviewer · 49.4 AIC · ⌖ 4.77 AIC · ⊞ 5.4K
Comment /review to run again

Comments that could not be inline-anchored

pkg/workflow/expression_secrets_serialization_validation_test.go:231

wantWarning is defined in the test struct but never asserted — the entire non-strict warning path is untested.

<details>
<summary>💡 Details</summary>

Multiple test cases set wantWarning: true (e.g. "toJSON(secrets) in markdown body — non-strict emits warning") but the test loop body never reads that field. Any regression — missing warning, wrong message, spurious warning — passes silently.

Root cause: fmt.Fprintln(os.Stderr, ...) is not capturable in unit tests without redirecting…

pkg/workflow/expression_secrets_serialization_validation.go:148

Log at line 148 reports c.strictMode but enforcement uses effectiveStrictMode, making debug logs misleading.

<details>
<summary>💡 Details</summary>

// Current (line 148):
expressionSecretsSerializationLog.Printf(&quot;Validating secrets serialization expressions (strictMode=%t)&quot;, c.strictMode)

c.strictMode is the compiler-level default. The actual enforcement path calls c.effectiveStrictMode(workflowData.RawFrontmatter), which can return false even when `c.strictMode == t…

pkg/workflow/expression_secrets_serialization_validation.go:111

Error/warning message reports a normalized version of the expression, not the literal text from source — users may not recognize what triggered the error.

<details>
<summary>💡 Details</summary>

full := &quot;${{ &quot; + strings.TrimSpace(exprContent) + &quot; }}&quot;

If a workflow contains ${{ toJSON( secrets ) }} (extra spaces), the error message reports ${{ toJSON( secrets ) }} — the outer whitespace is trimmed but the inner spacing is preserved, producing an inconsistent and potent…

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Implemented pr-finisher pass in commit 808779e: addressed the secrets-serialization validator/test feedback, reverted the unrelated generated workflow schedule change, and ran local validation (make fmt, make lint, make test-unit, make test, make recompile, make agent-report-progress).

Copilot AI and others added 2 commits July 11, 2026 11:59
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot Please run the pr-finisher skill, address unresolved review feedback, and rerun checks once the branch is up to date.

Generated by 👨‍🍳 PR Sous Chef · 15.8 AIC · ⌖ 6.83 AIC · ⊞ 4.2K ·
Comment /souschef to run again

Copilot AI commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Please run the pr-finisher skill, address unresolved review feedback, and rerun checks once the branch is up to date.

...

Completed a pr-finisher pass on 345b8b8. I verified unresolved feedback is already addressed in current commits, and re-ran local checks (make fmt, make lint, make test-unit, make test, make recompile) successfully. CI on this head is still unverified from the agent side and needs a maintainer-triggered re-run.

Copilot AI requested a review from gh-aw-bot July 11, 2026 12:28
@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category feature
Risk 🟡 Medium
Score 72/100 (impact:40, urgency:20, quality:12)
Action 🚀 fast_track

Rationale: Adds compiler rule blocking toJSON(secrets) — a security-sensitive pattern that exposes all secrets. CI passing ✅, approved by automation. Highest-priority PR in this batch due to security impact. Copilot reviewer + automation reviewed; 5 files changed.

Generated by 🔧 PR Triage Agent · 242.5 AIC · ⌖ 9.07 AIC · ⊞ 5.6K ·

@pelikhan pelikhan merged commit c1b56d6 into main Jul 11, 2026
1 check passed
@pelikhan pelikhan deleted the copilot/update-go-compiler-rules branch July 11, 2026 12:44
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.

Generated by 👨‍🍳 PR Sous Chef · 13 AIC · ⌖ 6.74 AIC · ⊞ 4.2K ·
Comment /souschef to run again

@github-actions github-actions Bot mentioned this pull request Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants