Skip to content

Filter Runner-Guard RGS-012 false positives for local Copilot allow-tools - #54341

Merged
pelikhan merged 8 commits into
mainfrom
copilot/rgs-012-fix-secret-exfiltration-another-one
Aug 21, 2026
Merged

Filter Runner-Guard RGS-012 false positives for local Copilot allow-tools#54341
pelikhan merged 8 commits into
mainfrom
copilot/rgs-012-fix-secret-exfiltration-another-one

Conversation

Copilot AI commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Runner-Guard flagged generated Copilot allow-tool declarations for local curl targets as possible secret exfiltration. These lines describe permitted tools for loopback/local dev-server access; they are not executable outbound requests.

  • Runner-Guard filtering

    • Suppresses RGS-012 findings only when they point to Copilot allow-tool declarations for local curl hosts:
      • localhost
      • 127.0.0.1
      • ::1
      • host.docker.internal
    • Keeps findings for non-local allow-tool targets.
  • Safety boundaries

    • Preserves findings on actual executable curl commands, including local targets carrying payloads.
    • Preserves unrelated Runner-Guard rules.
# Suppressed: generated allow-tool declaration, not executable curl
# --allow-tool shell(curl http://localhost:*)

# Preserved: executable curl with payload
run: curl -fsSL http://localhost:4321/collect -d "secret=$SECRET_TOKEN"

Run: https://github.com/github/gh-aw/actions/runs/32420067325> Generated by 👨‍🍳 PR Sous Chef · gpt54 · 7.36 AIC · ⌖ 8.07 AIC · ⊞ 9.3K ·

Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 14.8 AIC · ⌖ 8.04 AIC · ⊞ 9.3K ·
Comment /souschef to run again


PR Sous Chef branch refresh requested from run https://github.com/github/gh-aw/actions/runs/32422171255> Generated by 👨‍🍳 PR Sous Chef · gpt54 · 12.8 AIC · ⌖ 10.5 AIC · ⊞ 9.3K ·

Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix secret exfiltration via outbound HTTP request in visual-regression-checker.lock.yml Filter Runner-Guard RGS-012 false positives for local Copilot allow-tools Aug 20, 2026
Copilot AI requested a review from pelikhan August 20, 2026 20:39
@pelikhan
pelikhan marked this pull request as ready for review August 20, 2026 21:08
Copilot AI balanced review requested due to automatic review settings August 20, 2026 21:08
@github-actions

github-actions Bot commented Aug 20, 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 20, 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 20, 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 20, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Generated by Ponytail Reviewer for #54341

@github-actions

github-actions Bot commented Aug 20, 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-20T00:00:00Z
review_event: COMMENT
top_themes:
  - no actionable changed-line issues found
  - local allow-tool suppression remains narrowly scoped in tests
files_reviewed:
  - pkg/cli/runner_guard.go
  - pkg/cli/runner_guard_copilot_allow_tool.go
  - pkg/cli/runner_guard_copilot_allow_tool_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 · gpt54 · 4.54 AIC · ⌖ 6.82 AIC · ⊞ 7K ·
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.

Overall, this change looks narrowly scoped and the new tests cover the main false-positive cases I would expect.

Review notes

I did not find a changed-line correctness or security regression worth blocking on. The new suppression only applies to RGS-012, still preserves executable curl invocations, and explicitly refuses mixed local/non-local allow-tool contexts.

The requested grumpy-coder sub-agent was unavailable in this environment, so this review is based on my own pass over the diff.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 4.54 AIC · ⌖ 6.82 AIC · ⊞ 7K
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.

The implementation is correct and well-tested. The filter conservatively suppresses RGS-012 only when: (1) the finding falls inside a step named exactly Execute GitHub Copilot CLI, (2) the finding line precedes run:, and (3) all allow-tool hosts in the step are loopback addresses. Any step mixing local and non-local curl targets is preserved as a live finding. Tests cover all the key cases including the evil.example.com mixed scenario. LGTM.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 27.9 AIC · ⌖ 8.79 AIC · ⊞ 5.7K

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

Ponytail pass (over-engineering only).

net: -15 lines possible.

Generated by ✂️ Ponytail Reviewer for #54341 · auto · 26.6 AIC · ⌖ 8.19 AIC · ⊞ 7.3K
Comment /ponytail to run again

Comment on lines +186 to +196
func allDigits(value string) bool {
if value == "" {
return false
}
for _, r := range value {
if r < '0' || r > '9' {
return false
}
}
return true
}

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.

L186-196: stdlib: hand-rolled digit-only check. _, err := strconv.Atoi(port); err == nil, 1 line.

Comment on lines +126 to +157
func curlAllowToolCommentHost(line string) (string, bool) {
trimmed := strings.TrimSpace(line)
trimmed = strings.TrimPrefix(trimmed, "#")
trimmed = strings.TrimSpace(trimmed)
const prefix = "--allow-tool shell(curl "
if !strings.HasPrefix(trimmed, prefix) || !strings.HasSuffix(trimmed, ")") {
return "", false
}

return curlTargetHost(strings.TrimSuffix(strings.TrimPrefix(trimmed, prefix), ")"))
}

func curlAllowToolHosts(line string) []string {
const prefix = "shell(curl "
var hosts []string
remaining := line
for {
index := strings.Index(remaining, prefix)
if index < 0 {
return hosts
}
remaining = remaining[index+len(prefix):]
end := strings.Index(remaining, ")")
if end < 0 {
return hosts
}
if host, ok := curlTargetHost(remaining[:end]); ok {
hosts = append(hosts, host)
}
remaining = remaining[end+1:]
}
}

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.

L126-157: yagni: two near-identical curl-host extractors (comment-single vs line-multi). One regex/loop over shell(curl ...) occurrences handles both; comment form is just the multi-host loop with 1 match expected.

@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 /diagnosing-bugs and /tdd — requesting changes on one security-relevant correctness issue.

📋 Key Themes & Highlights

Key Themes

  • Fast-path bypass (security): findingInCopilotLocalCurlAllowTool has a fast path that suppresses findings for any line matching the local curl allow-tool pattern without verifying the line belongs to a Copilot execution step. The safer slow path below does check step membership — the fast path should either be removed or guarded identically.
  • Edge case in curlTargetHost: mixed-wildcard port strings (e.g. 3*00) are not stripped, leaving the host unmatched and a finding unexpectedly un-suppressed. Low probability but worth a comment or a guard.
  • Missing unit test for isStepBoundaryLine: the tests rely on coincidental fixture structure; an explicit test would pin the contract.

Positive Highlights

  • ✅ Excellent test coverage — both positive (suppressed) and negative (preserved) cases are exercised.
  • hasNonLocalCurlAllowTool guard prevents suppression when a step mixes local and remote targets — exactly the right safety boundary.
  • ✅ Clean, single-responsibility helper functions throughout.
  • ✅ File-lines cache (fileLinesByPath) avoids repeated disk reads for multi-finding workflows.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 38.3 AIC · ⌖ 10.3 AIC · ⊞ 7.8K
Comment /matt to run again

return true
}

stepStart := -1

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 fast path in findingInCopilotLocalCurlAllowTool suppresses findings on a matching line without verifying the line is inside a Copilot execution step — inconsistent with the stricter step-scoped check below it.

💡 Details and suggested fix

The fast path fires for any file that has # --allow-tool shell(curl (localhost/redacted) even in non-Copilot steps. The slow path guards against this with isStepNameLine(lines[stepStart], copilotExecutionStepNameMarker)`, but the fast path bypasses that guard:

// line 47 — no step-membership check
if isLocalCurlAllowToolComment(lines[lineIndex]) || isLocalCurlAllowToolArgumentLine(lines[lineIndex]) {
    return true
}

An attacker (or an accidental workflow) can silence an RGS-012 finding simply by placing `# --allow-tool shell(curl (localhost/redacted) in any step.

Simplest fix: remove the fast path entirely. The slow path already handles these lines correctly since lineIndex < runIndex places them before run:.

@copilot please address this.

filtered := filterCopilotLocalAllowToolFindings(findings, gitRoot)

require.Len(t, filtered, 3)
assert.Equal(t, suspiciousCurlLine, filtered[0].Line)

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] The test uses lineContaining(t, lines, "Execute GitHub Copilot CLI") to find the step name line, but that line is the name: field — not a step boundary marker. The isStepBoundaryLine function may use different criteria. If the test passes because of how step-finding works, it's worth an explicit unit test for isStepBoundaryLine to document what constitutes a "step boundary".

💡 Why this matters

If isStepBoundaryLine looks for - name: or a specific YAML indentation pattern, the test's use of a plain name: line might coincidentally work today but mask a regression if the boundary logic changes. A focused test like:

func TestIsStepBoundaryLine(t *testing.T) {
    assert.True(t, isStepBoundaryLine("      - name: Execute GitHub Copilot CLI"))
    assert.False(t, isStepBoundaryLine("        name: Execute GitHub Copilot CLI"))
}

...would pin the contract and catch future regressions.

@copilot please address this.

func curlTargetHost(target string) (string, bool) {
target = strings.TrimSpace(target)
if fields := strings.Fields(target); len(fields) > 0 {
target = fields[0]

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] curlTargetHost strips port wildcards (*) and digits but not other wildcard patterns (e.g. (localhost/redacted) The path is already stripped by the /check, but a URL like(localhost/redacted) (unusual but not impossible from a crafted allow-tool line) would leave the port stripping silently skipping: allDigits("3*00") is false and port == "*" is false, so the port stays in the host string, causing isLocalCurlAllowToolHost to return false and the finding to be preserved unexpectedly.

💡 Suggested fix

The risk is low in practice but worth a comment or an additional strings.HasPrefix(port, "*") guard:

if port == "*" || allDigits(port) || strings.ContainsAny(port, "*?") {
    target = target[:colon]
}

Or simply document that wildcard ports other than * are out of scope.

@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

Filters Runner-Guard RGS-012 false positives for generated Copilot local-curl allow-tool declarations, addressing #54157.

Changes:

  • Adds filtering for approved local hosts.
  • Preserves unrelated findings and executable curl findings.
  • Adds focused unit tests.
Show a summary per file
File Description
pkg/cli/runner_guard.go Integrates the new filter.
pkg/cli/runner_guard_copilot_allow_tool.go Implements detection and host parsing.
pkg/cli/runner_guard_copilot_allow_tool_test.go Tests suppression boundaries.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment on lines +43 to +44
if isLocalCurlAllowToolComment(lines[lineIndex]) || isLocalCurlAllowToolArgumentLine(lines[lineIndex]) {
return true
}
}

return hasToolCommentHeader && hasLocalCurlAllowTool && !hasNonLocalCurlAllowTool
Comment on lines +172 to +178
target = strings.Trim(target, "[]")
if colon := strings.LastIndex(target, ":"); colon >= 0 {
port := target[colon+1:]
if port == "*" || allDigits(port) {
target = target[:colon]
}
}
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/54341-filter-runner-guard-rgs012-false-positives-copilot-allow-tools.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 — it was generated from the PR diff
  2. Complete the missing sections — add context the AI couldn't infer, refine the decision rationale, and list real alternatives 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-54341: Filter Runner-Guard RGS-012 False Positives for Copilot Local Allow-Tool Declarations

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

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

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., 0042-use-postgresql.md for PR #42).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 · 74.8 AIC · ⌖ 37.5 AIC · ⊞ 9.1K ·
Comment /review to run again

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot PR sous-chef triage:

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 7.36 AIC · ⌖ 8.07 AIC · ⊞ 9.3K ·
Comment /souschef to run again

Copilot AI commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot This PR looks close, but a maintainer-facing finish pass is still needed before merge.

Please:...

Finish pass complete at d9ba5e2. No new code changes were needed — nothing to commit or push.

ADR review
docs/adr/54341-filter-runner-guard-rgs012-false-positives-copilot-allow-tools.md was reviewed line by line against the implementation and is accurate: Context, Decision (including the curl-outside-allow-tool refusal and IPv6 loopback parsing), Alternatives, and Consequences all match pkg/cli/runner_guard_copilot_allow_tool.go. The "third targeted filter" claim was verified against runner_guard.go (order: filterRunnerGuardFindingsfilterRunnerGuardIgnoredFindingsfilterCopilotLocalAllowToolFindingsfilterGvisorInstallFindings). Status stays Draft — flipping to Accepted is a maintainer decision, and the ADR itself says so.

Validation status

  • ✅ Reviews — all three RGS-012 review threads addressed in 3c94815, with unit tests.
  • ✅ Checks (local) — make agent-report-progress green on this HEAD (fmt, lint, impacted unit tests, 286 workflows compiled, lock files in sync); runner-guard tests re-run clean this pass.
  • ✅ Mergeable — branch is 0 commits behind origin/main after a fresh fetch; no conflicts, no unpushed changes. No GitHub-offered branch-refresh path was needed.
  • ✅ Design Decision Gate 🏗️ — run 32417883127 concluded success on all 8 jobs; its only output was the draft ADR, now on the branch.
  • ❓ Checks (CI) — stale on d9ba5e2 and blocked on approval.

Action-required runs on d9ba5e2 (maintainer approval needed — the agent cannot approve these):

Workflow Run
CGO https://github.com/github/gh-aw/actions/runs/32421562879
CWI https://github.com/github/gh-aw/actions/runs/32421562826
Doc Build - Deploy https://github.com/github/gh-aw/actions/runs/32421562810

Remaining human actions

  1. Approve the three action_required runs above (or close/reopen the PR) so CI executes against d9ba5e2.
  2. Decide whether ADR-54341 moves from Draft to Accepted.
  3. Human review + merge. Not merging.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR Triage

Category: bug · Risk: medium · Priority: low

Score: 42/100 (impact 22/50 + urgency 12/30 + quality 8/20)

Recommended action: defer

CI has 1 failure. Has ADR and tests but needs CI fix before merge.

Automated triage — run 32432526976

Generated by 🔧 PR Triage Agent · auto · 63.7 AIC · ⌖ 4.49 AIC · ⊞ 8.3K ·

@github-actions

Copy link
Copy Markdown
Contributor

PR Triage

  • Category: bug
  • Risk: medium
  • Priority score: 77/100 (impact 40 + urgency 25 + quality 12)
  • Recommended action: fast_track
  • CI: 27 passing / 1 failing / 32 total

Generated by 🔧 PR Triage Agent · auto · 79.3 AIC · ⌖ 6.15 AIC · ⊞ 8.3K ·

@pelikhan

Copy link
Copy Markdown
Collaborator

@pelikhan

Copy link
Copy Markdown
Collaborator

/q determine who the pr-sous-chef agentic workflow is not adding comments to this PR

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🍳 skillet has started processing this pull request comment

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🎩 Mission equipment ready! Q has optimized your workflow. Use wisely, 007! 🔫

🎩 Equipped by Q

@github-actions

Copy link
Copy Markdown
Contributor

Q Investigation: Why pr-sous-chef hasn't commented on PR #54341

Finding — expected behavior, not a bug.

The pr-sous-chef candidate-filter step (.github/workflows/pr-sous-chef.md, skip rule #1) excludes any PR that has a check run in QUEUED/IN_PROGRESS/PENDING state that started less than 1 hour ago.

Looking at PR #54341's current check runs (head SHA d9ba5e2):

  • copilot check: status: in_progress, started_at: 2026-08-21T10:40:43Z — this is the Copilot coding agent working on the PR, started only ~2 minutes before your /q request.
  • This is well under the 1‐hour cutoff, so pr-sous-chef's prefilter marks the PR as checks_pending=true and drops it from the eligible-candidates list (filtered_checks_pending counter) before any nudge/comment logic runs.
  • This gate is intentional: comments in the workflow source explain that pending checks are ignored only after 1 hour specifically so long-running agentic checks (Q, coding agents) don't get nudged/interrupted mid-flight, while still eventually allowing a nudge if the agent stalls for over an hour.

Separately, the /souschef slash command (which bypasses the schedule prefilter and always posts an acknowledgement comment per the "Slash-command acknowledgement requirement") was not used here — only /q was invoked on this PR, and the last 15 scheduled pr-sous-chef runs (every ~15–20 min) all completed successfully but simply skipped PR #54341 for the reason above.

No code change is warranted. The behavior matches the documented design (skip rule #1, with its 1‐hour agentic-check exception). If a comment is desired sooner, run /souschef explicitly on this PR — it forces the acknowledgement comment regardless of pending checks.

🎩 Equipped by Q · auto · 34.6 AIC · ⌖ 4.83 AIC · ⊞ 12.1K ·
Comment /q to run again

Copilot AI and others added 2 commits August 21, 2026 10:48
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix https://github.com/github/gh-aw/actions/runs/32421562879/job/96618485546#step:10:1

Fixed in 1c5c0e5 (plus a wording tweak in 98b8387).

That step runs gh-aw-linters -errormessage scoped to the PR's changed Go files. Because this PR touches pkg/cli/runner_guard.go (the four-line filter hook), the whole file came into scope and 15 pre-existing error-message violations surfaced — 12 × "negative language without constructive guidance" and 3 × "generic 'failed to ...: %w' wrapping".

I rewrote the affected messages to include expected/example guidance and dropped the generic failed to ...: %w wrapping, e.g.:

  • invalid git root %q: %wgit root %q is not a valid absolute path; runner-guard requires an absolute repository root. Example: run gh aw from inside a git checkout: %w
  • failed to parse runner-guard JSON output: %wrunner-guard JSON output could not be parsed; expected a JSON object. Example: {"findings":[]}: %w
  • runner-guard failed with exit code %d → adds expected 0 (clean) or 1 (findings reported). Example: rerun with gh aw --verbose to see the scanner output

Substrings asserted by existing tests (must stay local, invalid control characters) were preserved, so runner_guard_test.go and compile_batch_operations_test.go still pass.

Verified locally by reproducing the exact CI invocation: gh-aw-linters -errormessage -errormessage.changed-files="pkg/cli/runner_guard.go,pkg/cli/runner_guard_copilot_allow_tool.go,pkg/cli/runner_guard_copilot_allow_tool_test.go" ./pkg/cli/... now exits 0. make agent-report-progress is green.

@pelikhan
pelikhan merged commit 9bd8e7e into main Aug 21, 2026
@pelikhan
pelikhan deleted the copilot/rgs-012-fix-secret-exfiltration-another-one branch August 21, 2026 11:23
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.87.4

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.

[static-analysis] RGS-012: Secret Exfiltration via Outbound HTTP Request in visual-regression-checker.lock.yml

4 participants