Skip to content

security(actions): detect transport-only verdict poll bounds - #1088

Draft
seonghobae wants to merge 96 commits into
developfrom
sentinel/detect-transport-only-poll-bound-1087
Draft

security(actions): detect transport-only verdict poll bounds#1088
seonghobae wants to merge 96 commits into
developfrom
sentinel/detect-transport-only-poll-bound-1087

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Goal

Close AppGuardrail issue #1087 by converting the verified ContextualWisdomLab/.github required-review runner-capacity defect into executable AppGuardrail detector evidence while preserving explicit false-positive/false-negative boundaries.

Current exact candidate: d9744331b83e9a47d4b0a84f1e81e42e382aa4c2 on develop@e71d37e7c58118e6764c96ab7c4492fe33eed6f8.

Status: not merge-ready. The current detector-precision RED obligations are repaired, but required security/code-scanning execution, independent approval, and review reconciliation are not complete.

Verified causal incident

Protected predecessor ContextualWisdomLab/.github@5c561a65cca3b925d533e4b40c5c3ac00f16524e had a verdict poll whose transport-failure budget did not bound healthy API/no-verdict iterations. The canonical owner wall-clock repair is protected as ContextualWisdomLab/.github@e29302c05eade7da7b0bdbb453e53980bc9d577b. .github owns workflow behavior; AppGuardrail owns reusable detection evidence.

Executable detector family

The #1087 family retains four packaged HIGH/CWE-400 identities:

  • github-actions-transport-only-poll-bound;
  • github-actions-transport-failure-budget-poll-bound;
  • github-actions-poll-bound-state-reset;
  • github-actions-poll-bound-unreachable-exit.

Evidence remains bounded to conventional GitHub Actions jobs and literal shell blocks. The rules correlate executable gh api, sleep/back-edge reachability, retry state, total bounds, and owning-job timeout rather than treating isolated tokens as proof.

Repaired current-head obligations

RED commit d50f49ccea1cbf2aecc6da268850fddfc80db3b6 pinned two blocker-class false-positive boundaries:

  1. causally initialized forward -gt deadline/attempt guards are finite like -ge, while reversed/non-expiring comparisons remain findings;
  2. owning-job timeout-minutes: ${{ 20 }} is a statically positive hard bound, while zero, negative, dynamic, empty, and sibling-job values are not proof.

Production commit 99bdf459cf7939896740c61c2c3fe9c222377312 repairs both contracts. Documentation commit e63c34279491c9c8c90764215d6ba39c8ad8d9e3 records the exact detector and Gap evidence.

Exact-head evidence

Exact head d9744331b83e9a47d4b0a84f1e81e42e382aa4c2 changes only ownership of the duplicate Gap document relative to e63c34279491c9c8c90764215d6ba39c8ad8d9e3; detector and test trees are identical.

  • On e63c342..., local production _scan_file polling corpus passed 126/126 and the full suite passed 1,126 with one unrelated pre-existing src/....py Path/String contract failure that also reproduces on b34670b....
  • Because documentation deletion created a new exact head, all nine repository workflows on d9744331... are fresh queued: Tests 34082936289, Security Process 34082936385, Security Scan 34082936283, SAST Semgrep 34082936280, CodeQL PR 34082936357, Pinned HTTPS 34082936293, OpenSSF Evidence 34082936367, Retention Audit 34082936298, and Scan path context 34082936366.
  • No predecessor success transfers. No qualifying independent approval is present, and review threads remain open for current-head reconciliation.

Single-writer repair

docs/product-technical-gap-baseline.md is owned by canonical writer PR #999. Current #999 head e3cb3a2d57da54f31739ce92fff307c5d8d2b3d0 contains the complete, more current organization baseline, including #1088 exact head d9744331b83e9a47d4b0a84f1e81e42e382aa4c2 and its detector predecessor e63c34279491c9c8c90764215d6ba39c8ad8d9e3, RED d50f49c..., production 99bdf459..., current Checks, G-06, and the wider PR/security corpus. Normal descendant d9744331b83e9a47d4b0a84f1e81e42e382aa4c2 removed the duplicate, narrower baseline copy from this detector branch after that carryover check; no valid baseline requirement or evidence was retired.

Product/architecture boundary

Composite actions, generated/non-shell control flow, cross-file state, materially different loop frameworks, and general shell ownership remain the Proposed structural GitHub Actions + shell analyzer Gap. Current rule IDs and regression corpus are migration oracles, not a claim of universal shell parsing.

Acceptance

Merge only through ordinary protection on an unchanged exact head after all required Tests/security/SAST/coverage/code-scanning lanes are terminal-success, live findings are reconciled, and a qualifying independent non-author approval exists. Do not use local probes, author replies, predecessor checks, self-approval, status coercion, force-push, or protection bypass as acceptance.

Refs #1087.

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

GitHub Actions 폴링 탐지기가 조건부 종료, 전체 시도 제한, 상태 재설정, 도달 불가능한 종료와 셸 명령 경계를 구분하도록 확장되었다. 새 규칙, 회귀 테스트, 보안 코퍼스와 추적성 문서가 추가되었다.

Changes

GitHub Actions 폴링 경계 탐지

Layer / File(s) Summary
폴링 경계 규칙 구현
scanner/rules/github_actions.yml, scanner/rules/github_actions_transport_budget.yml, scanner/rules/github_actions_poll_bound_state_reset.yml, scanner/rules/github_actions_poll_unreachable_exit.yml
기존 매처가 조건부 breakexit 0을 안전 경계로 인정하지 않도록 수정했다. 식별자와 선언 순서가 다른 전송 예산을 지원했다. 상태 재설정과 도달 불가능한 fail-closed 종료를 HIGH 규칙으로 탐지한다.
기본 폴링 경계 검증
tests/fixtures/security_corpus/*, tests/test_github_actions_poll_bounds.py, tests/test_github_actions_poll_bounds_large_job.py
취약·수정 픽스처를 검증했다. 유한 시도 제한, 작업 시간 제한, 동일 작업 범위, 실행 가능한 gh apisleep, 워크플로 경로 범위를 테스트했다.
식별자 비종속 전송 예산 검증
tests/test_github_actions_poll_bound_aliases.py, tests/test_github_actions_poll_bound_command_whitespace.py, tests/test_github_actions_poll_bound_late_initialization.py, tests/test_github_actions_poll_bound_mixed_safety.py
재명명된 변수, 선언 순서, 공백과 탭, 늦은 초기화, 인과적 초기화, 헬퍼 루프 분리를 검증했다. 기존 규칙과 일반 규칙의 중복 보고도 검증했다.
상태 변이와 제어 흐름 검증
tests/test_github_actions_poll_bound_state_reset.py, tests/test_github_actions_poll_bound_review_precision.py, tests/test_github_actions_poll_deadline_tightening.py, tests/test_github_actions_poll_control_flow_review.py, tests/test_github_actions_poll_bound_control_flow_regression.py, tests/test_github_actions_poll_bound_current_head_regressions.py, tests/test_github_actions_poll_bound_unreachable_exit.py
비수렴 상태 변이와 단조 증가 상태를 구분했다. 조건부 성공 종료, 직접 도달 가능한 종료, continue 뒤의 도달 불가능한 종료, 실행되지 않는 명령 문자열의 탐지 경계를 검증했다.
추적성 및 변경 기록
CHANGELOG.d/1087-actions-transport-only-poll-bound.md, docs/TRACEABILITY.md
규칙 조건, 상태 재설정 동작, 도달 불가능한 종료, 음성 경계, 회귀 근거와 소유권 정보를 기록했다. 검토 날짜를 갱신했다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 34bc9

The PR adds static HIGH findings for unsafe GitHub Actions polling patterns, but the current rules can still flag safe workflows and overlook valid bounds in some cases, potentially blocking legitimate CI configurations. Required exact-head checks are also not yet complete, so merge should wait for the detector corrections or explicit owner acceptance.

Sequence Diagram(s)

sequenceDiagram
  participant Workflow as GitHub Actions 워크플로
  participant Scanner as _scan_file
  participant PollRule as 폴링 경계 규칙
  participant Findings as Findings
  Workflow->>Scanner: 워크플로 파일 전달
  Scanner->>PollRule: gh api·sleep·상태·종료 흐름 검사
  PollRule->>Findings: 폴링 경계 또는 상태 변이 결과 반환
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 115 functions across 15 files. (2 skipped:…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 GitHub Actions의 transport-only verdict polling bound 탐지라는 PR의 핵심 변경을 명확하게 설명합니다. 추가 규칙과 회귀 테스트를 모두 포함하지는 않지만, 주요 변경과 직접 관련됩니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sentinel/detect-transport-only-poll-bound-1087

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae seonghobae added area: ci-cd CI, GitHub Actions, checks, release, or supply chain enhancement New feature or request priority: high security Security vulnerability or security-governance work status: needs-review Open pull request requiring current-head review or checks type: feature New or expanded product capability labels Sep 2, 2026 — with ChatGPT Codex Connector
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 3 new potential issues.

Devin Review

Comment on lines +10 to +11
- pattern-regex: '^ [A-Za-z0-9_.-]+[^\n]*\n(?!(?>(?:(?!^(?:[A-Za-z0-9_.-]+| [A-Za-z0-9_.-]+)[ \t]*:)[^\n]*\n))*^ timeout-minutes[ \t]*:[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)(?>(?:(?!^ -[ \t]*run|^ run)^ {4,}[^\n]*\n)){0,80}^(?: -[ \t]*run| run)[ \t]*:[ \t]*\|[+-]?[^\n]*\n(?!(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,80}^ max_poll_transport_failures\b)(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,80}(?:^ (?P<helper_counter>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<helper_limit>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )(?P=helper_counter)[ \t]*=[ \t]*\$\(\([ \t]*(?P=helper_counter)[ \t]*\+[ \t]*1[ \t]*\)\)|^ done\b)^ {12,}[^\n]*\n)){0,20}^ {12}(?! )(?P=helper_counter)[ \t]*=[ \t]*\$\(\([ \t]*(?P=helper_counter)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {12}(?! )if[^\n]*\$(?P=helper_counter)[^\n]*-ge[^\n]*\$(?P=helper_limit)\b[^\n]*;[ \t]*then|^ done\b)^ {12,}[^\n]*\n)){0,12}^ {12}(?! )if[^\n]*\$(?P=helper_counter)[^\n]*-ge[^\n]*\$(?P=helper_limit)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n(?>(?:(?!^ done\b)^ {12,}[^\n]*\n)){0,40}^ done\b[^\n]*\n(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,80})?^ (?P<retry_counter_alias>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<retry_limit_alias>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?\n(?!(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*\$\(\([^\n]*\bdate\b[^\n]*\+%s[^\n]*\+[ \t]*[1-9][0-9]*[ \t]*\)\))^ {10,}[^\n]*\n)){0,40}^ (?P<safe_deadline>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*\$\(\([^\n]*\bdate\b[^\n]*\+%s[^\n]*\+[ \t]*[1-9][0-9]*[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )if[^\n]*\bdate\b[^\n]*\+%s[^\n]*-ge[^\n]*\$(?P=safe_deadline)\b[^\n]*;[ \t]*then|^ done\b)^ {12,}[^\n]*\n)){0,20}^ {12}(?! )if[^\n]*\bdate\b[^\n]*\+%s[^\n]*-ge[^\n]*\$(?P=safe_deadline)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n)(?!(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<safe_total_counter>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<safe_total_limit>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )(?P=safe_total_counter)[ \t]*=[ \t]*\$\(\([ \t]*(?P=safe_total_counter)[ \t]*\+[ \t]*1[ \t]*\)\)|^ done\b)^ {12,}[^\n]*\n)){0,20}^ {12}(?! )(?P=safe_total_counter)[ \t]*=[ \t]*\$\(\([ \t]*(?P=safe_total_counter)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {12}(?! )if[^\n]*\$(?P=safe_total_counter)[^\n]*-ge[^\n]*\$(?P=safe_total_limit)\b[^\n]*;[ \t]*then|^ done\b)^ {12,}[^\n]*\n)){0,12}^ {12}(?! )if[^\n]*\$(?P=safe_total_counter)[^\n]*-ge[^\n]*\$(?P=safe_total_limit)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n)(?!(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<safe_total_limit_first>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<safe_total_counter_second>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )(?P=safe_total_counter_second)[ \t]*=[ \t]*\$\(\([ \t]*(?P=safe_total_counter_second)[ \t]*\+[ \t]*1[ \t]*\)\)|^ done\b)^ {12,}[^\n]*\n)){0,20}^ {12}(?! )(?P=safe_total_counter_second)[ \t]*=[ \t]*\$\(\([ \t]*(?P=safe_total_counter_second)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {12}(?! )if[^\n]*\$(?P=safe_total_counter_second)[^\n]*-ge[^\n]*\$(?P=safe_total_limit_first)\b[^\n]*;[ \t]*then|^ done\b)^ {12,}[^\n]*\n)){0,12}^ {12}(?! )if[^\n]*\$(?P=safe_total_counter_second)[^\n]*-ge[^\n]*\$(?P=safe_total_limit_first)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n)(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12,}if[ \t]+![ \t]+(?:[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*["'']?\$\([ \t]*(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b|(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b)[^\n]*;[ \t]*then|^ done\b)^ {12,}[^\n]*\n)){0,40}^ {12,}if[ \t]+![ \t]+(?:[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*["'']?\$\([ \t]*(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b|(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b)[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14,}(?P=retry_counter_alias)[ \t]*=[ \t]*\$\(\([ \t]*(?P=retry_counter_alias)[ \t]*\+[ \t]*1[ \t]*\)\)|^ {12}fi\b)^ {14,}[^\n]*\n)){0,12}^ {14,}(?P=retry_counter_alias)[ \t]*=[ \t]*\$\(\([ \t]*(?P=retry_counter_alias)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {14,}if[^\n]*\$(?P=retry_counter_alias)[^\n]*-ge[^\n]*\$(?P=retry_limit_alias)\b[^\n]*;[ \t]*then|^ {12}fi\b)^ {14,}[^\n]*\n)){0,12}^ {14,}if[^\n]*\$(?P=retry_counter_alias)[^\n]*-ge[^\n]*\$(?P=retry_limit_alias)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {16,}exit[ \t]+[1-9][0-9]*\b)^ {16,}[^\n]*\n)){0,8}^ {16,}exit[ \t]+[1-9][0-9]*\b[^\n]*\n(?>(?:(?!^ {12}fi[ \t]*(?:#[^\n]*)?$)^ {12,}[^\n]*\n)){0,40}^ {12}fi[ \t]*(?:#[^\n]*)?\n(?!(?>(?:(?!^ {12}(?! )(?:if\b|fi\b|break(?:[ \t]+[1-9][0-9]*)?\b|exit(?:[ \t]+0)?\b))^ {12,}[^\n]*\n)){0,40}^ {12}(?! )(?:break(?:[ \t]+[1-9][0-9]*)?|exit(?:[ \t]+0)?)[ \t]*(?:#[^\n]*)?$\n)(?>(?:(?!^ {12}(?! )sleep(?:[ \t]+|$)|^ done\b)^ {12,}[^\n]*\n)){0,40}^ {12}(?! )sleep(?:[ \t]+|$)[^\n]*\n(?!(?>(?:(?!^ {12}(?! )(?:if\b|fi\b|break(?:[ \t]+[1-9][0-9]*)?\b|exit(?:[ \t]+0)?\b)|^ done\b)^ {12,}[^\n]*\n)){0,40}^ {12}(?! )(?:break(?:[ \t]+[1-9][0-9]*)?|exit(?:[ \t]+0)?)[ \t]*(?:#[^\n]*)?$\n)(?>(?:(?!^ done\b)^ {12,}[^\n]*\n)){0,40}^ done\b'
- pattern-regex: '^ [A-Za-z0-9_.-]+[^\n]*\n(?!(?>(?:(?!^(?:[A-Za-z0-9_.-]+| [A-Za-z0-9_.-]+)[ \t]*:)[^\n]*\n))*^ timeout-minutes[ \t]*:[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)(?>(?:(?!^ -[ \t]*run|^ run)^ {4,}[^\n]*\n)){0,80}^(?: -[ \t]*run| run)[ \t]*:[ \t]*\|[+-]?[^\n]*\n(?!(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,80}^ max_poll_transport_failures\b)(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,80}^ (?P<retry_limit_reverse>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<retry_counter_reverse>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?!(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*\$\(\([^\n]*\bdate\b[^\n]*\+%s[^\n]*\+[ \t]*[1-9][0-9]*[ \t]*\)\))^ {10,}[^\n]*\n)){0,40}^ (?P<safe_deadline_reverse>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*\$\(\([^\n]*\bdate\b[^\n]*\+%s[^\n]*\+[ \t]*[1-9][0-9]*[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )if[^\n]*\bdate\b[^\n]*\+%s[^\n]*-ge[^\n]*\$(?P=safe_deadline_reverse)\b[^\n]*;[ \t]*then|^ done\b)^ {12,}[^\n]*\n)){0,20}^ {12}(?! )if[^\n]*\bdate\b[^\n]*\+%s[^\n]*-ge[^\n]*\$(?P=safe_deadline_reverse)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n)(?!(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<safe_total_counter_reverse>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<safe_total_limit_reverse>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )(?P=safe_total_counter_reverse)[ \t]*=[ \t]*\$\(\([ \t]*(?P=safe_total_counter_reverse)[ \t]*\+[ \t]*1[ \t]*\)\)|^ done\b)^ {12,}[^\n]*\n)){0,20}^ {12}(?! )(?P=safe_total_counter_reverse)[ \t]*=[ \t]*\$\(\([ \t]*(?P=safe_total_counter_reverse)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {12}(?! )if[^\n]*\$(?P=safe_total_counter_reverse)[^\n]*-ge[^\n]*\$(?P=safe_total_limit_reverse)\b[^\n]*;[ \t]*then|^ done\b)^ {12,}[^\n]*\n)){0,12}^ {12}(?! )if[^\n]*\$(?P=safe_total_counter_reverse)[^\n]*-ge[^\n]*\$(?P=safe_total_limit_reverse)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n)(?!(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<safe_total_limit_reverse_first>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?\n(?>(?:(?!(?:^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$))^ {10,}[^\n]*\n)){0,40}^ (?P<safe_total_counter_reverse_second>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )(?P=safe_total_counter_reverse_second)[ \t]*=[ \t]*\$\(\([ \t]*(?P=safe_total_counter_reverse_second)[ \t]*\+[ \t]*1[ \t]*\)\)|^ done\b)^ {12,}[^\n]*\n)){0,20}^ {12}(?! )(?P=safe_total_counter_reverse_second)[ \t]*=[ \t]*\$\(\([ \t]*(?P=safe_total_counter_reverse_second)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {12}(?! )if[^\n]*\$(?P=safe_total_counter_reverse_second)[^\n]*-ge[^\n]*\$(?P=safe_total_limit_reverse_first)\b[^\n]*;[ \t]*then|^ done\b)^ {12,}[^\n]*\n)){0,12}^ {12}(?! )if[^\n]*\$(?P=safe_total_counter_reverse_second)[^\n]*-ge[^\n]*\$(?P=safe_total_limit_reverse_first)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n)(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12,}if[ \t]+![ \t]+(?:[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*["'']?\$\([ \t]*(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b|(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b)[^\n]*;[ \t]*then|^ done\b)^ {12,}[^\n]*\n)){0,40}^ {12,}if[ \t]+![ \t]+(?:[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*["'']?\$\([ \t]*(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b|(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b)[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14,}(?P=retry_counter_reverse)[ \t]*=[ \t]*\$\(\([ \t]*(?P=retry_counter_reverse)[ \t]*\+[ \t]*1[ \t]*\)\)|^ {12}fi\b)^ {14,}[^\n]*\n)){0,12}^ {14,}(?P=retry_counter_reverse)[ \t]*=[ \t]*\$\(\([ \t]*(?P=retry_counter_reverse)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {14,}if[^\n]*\$(?P=retry_counter_reverse)[^\n]*-ge[^\n]*\$(?P=retry_limit_reverse)\b[^\n]*;[ \t]*then|^ {12}fi\b)^ {14,}[^\n]*\n)){0,12}^ {14,}if[^\n]*\$(?P=retry_counter_reverse)[^\n]*-ge[^\n]*\$(?P=retry_limit_reverse)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {16,}exit[ \t]+[1-9][0-9]*\b)^ {16,}[^\n]*\n)){0,8}^ {16,}exit[ \t]+[1-9][0-9]*\b[^\n]*\n(?>(?:(?!^ {12}fi[ \t]*(?:#[^\n]*)?$)^ {12,}[^\n]*\n)){0,40}^ {12}fi[ \t]*(?:#[^\n]*)?\n(?!(?>(?:(?!^ {12}(?! )(?:if\b|fi\b|break(?:[ \t]+[1-9][0-9]*)?\b|exit(?:[ \t]+0)?\b))^ {12,}[^\n]*\n)){0,40}^ {12}(?! )(?:break(?:[ \t]+[1-9][0-9]*)?|exit(?:[ \t]+0)?)[ \t]*(?:#[^\n]*)?$\n)(?>(?:(?!^ {12}(?! )sleep(?:[ \t]+|$)|^ done\b)^ {12,}[^\n]*\n)){0,40}^ {12}(?! )sleep(?:[ \t]+|$)[^\n]*\n(?!(?>(?:(?!^ {12}(?! )(?:if\b|fi\b|break(?:[ \t]+[1-9][0-9]*)?\b|exit(?:[ \t]+0)?\b)|^ done\b)^ {12,}[^\n]*\n)){0,40}^ {12}(?! )(?:break(?:[ \t]+[1-9][0-9]*)?|exit(?:[ \t]+0)?)[ \t]*(?:#[^\n]*)?$\n)(?>(?:(?!^ done\b)^ {12,}[^\n]*\n)){0,40}^ done\b'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Strict polling bounds trigger false alarms

A finite deadline or attempt guard using -gt bypasses the detector's -ge-only exclusions. The scanner then reports a HIGH unbounded-loop finding.

Prompt for agents
Update both transport-only polling detectors in scanner/rules/github_actions_transport_budget.yml and scanner/rules/github_actions.yml to recognize strict greater-than guards as finite total bounds alongside greater-than-or-equal guards. Cover both wall-clock deadlines and total-attempt counters, both declaration orders, and add production _scan_file regressions for historical and renamed transport-budget forms. Preserve the existing direction check so reversed or non-expiring comparisons do not suppress findings.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

RED를 먼저 고정했습니다. d50f49ccea1cbf2aecc6da268850fddfc80db3b6의 production _scan_file 회귀가 historical/renamed 두 transport detector 모두에 대해 date ... -gt $overall_deadline과 total-attempt counter -gt limit를 finite negative로 요구하고, attempt counter/limit 선언 순서 두 가지를 모두 검증합니다. 현재 production matcher는 이 계약을 아직 만족하지 않으므로 thread는 unresolved로 유지합니다. GREEN은 같은 causally initialized state의 forward -gt/-ge를 모두 안전 경계로 인정하되 reversed/non-expiring 비교는 suppress하지 않고, 기존 vulnerable positives를 그대로 통과하는 exact-head test evidence입니다.

@seonghobae seonghobae Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed on current exact head e63c34279491c9c8c90764215d6ba39c8ad8d9e3. RED commit d50f49ccea1cbf2aecc6da268850fddfc80db3b6 pins forward -gt finite deadline/attempt bounds for the historical and renamed detectors and both declaration orders; production commit 99bdf459cf7939896740c61c2c3fe9c222377312 recognizes the mathematically equivalent forward -ge/-gt forms while reversed/non-expiring comparisons remain positive. Exact-head local polling regression is 126/126 GREEN and GitHub Tests run 34082329418 is success. I am leaving the thread unresolved for fresh independent review and the remaining exact-head security/code-scanning checks.

Comment thread scanner/rules/github_actions.yml Outdated
# unconditionally breaks/exits before the back edge is finite.
- id: github-actions-transport-only-poll-bound
patterns:
- pattern-regex: '^ [A-Za-z0-9_.-]+[^\n]*\n(?!(?>(?:(?!^(?:[A-Za-z0-9_.-]+| [A-Za-z0-9_.-]+)[ \t]*:)[^\n]*\n))*^ timeout-minutes[ \t]*:[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)(?>(?:(?!^ timeout-minutes[ \t]*:|^ -[ \t]*run|^ run)^ {4,}[^\n]*\n)){0,80}^(?: -[ \t]*run| run)[ \t]*:[ \t]*\|[+-]?[^\n]*\n(?=(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,80}^ (?P<hist_transport_counter>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ max_poll_transport_failures)^ {10,}[^\n]*\n)){0,40}^ max_poll_transport_failures[ \t]*=[ \t]*[1-9][0-9]*[^\n]*\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )if[ \t]+![^\n]*\$\([ \t]*(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b|^ done\b)^ {12,}[^\n]*\n)){0,80}^ {12}(?! )if[ \t]+![^\n]*\$\([ \t]*(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b[^\n]*\n(?>(?:(?!^ {14}(?! )(?P=hist_transport_counter)[ \t]*=[ \t]*\$\(\([ \t]*(?P=hist_transport_counter)[ \t]*\+[ \t]*1[ \t]*\)\)|^ done\b)^ {14,}[^\n]*\n)){0,20}^ {14}(?! )(?P=hist_transport_counter)[ \t]*=[ \t]*\$\(\([ \t]*(?P=hist_transport_counter)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {14}(?! )if[^\n]*\$(?P=hist_transport_counter)[^\n]*-ge[^\n]*\$max_poll_transport_failures\b[^\n]*;[ \t]*then|^ done\b)^ {14,}[^\n]*\n)){0,12}^ {14}(?! )if[^\n]*\$(?P=hist_transport_counter)[^\n]*-ge[^\n]*\$max_poll_transport_failures\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {16}(?! )exit[ \t]+[1-9][0-9]*\b)^ {16,}[^\n]*\n)){0,8}^ {16}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n(?>(?:(?!^ {12}(?! )sleep(?:[ \t]+|$)|^ done\b)^ {12,}[^\n]*\n)){0,80}^ {12}(?! )sleep(?:[ \t]+|$)[^\n]*\n(?>(?:(?!^ done\b)^ {12,}[^\n]*\n)){0,80}^ done\b)(?>(?:(?!^ max_poll_transport_failures)^ {10,}[^\n]*\n)){0,80}^ max_poll_transport_failures[ \t]*=[ \t]*[1-9][0-9]*[^\n]*\n(?!(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*\$\(\([^\n]*\bdate\b[^\n]*\+%s[^\n]*\+[ \t]*[1-9][0-9]*[ \t]*\)\))^ {10,}[^\n]*\n)){0,80}^ (?P<hist_deadline_any>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*\$\(\([^\n]*\bdate\b[^\n]*\+%s[^\n]*\+[ \t]*[1-9][0-9]*[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )if[^\n]*\bdate\b[^\n]*\+%s[^\n]*-[gl]e[^\n]*\$(?P=hist_deadline_any)\b[^\n]*;[ \t]*then)^ {12,}[^\n]*\n)){0,20}^ {12}(?! )if[^\n]*\bdate\b[^\n]*\+%s[^\n]*-[gl]e[^\n]*\$(?P=hist_deadline_any)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n)(?!(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,80}^ (?P<hist_counter_a>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<hist_limit_a>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )(?P=hist_counter_a)[ \t]*=[ \t]*\$\(\([ \t]*(?P=hist_counter_a)[ \t]*\+[ \t]*1[ \t]*\)\))^ {12,}[^\n]*\n)){0,20}^ {12}(?! )(?P=hist_counter_a)[ \t]*=[ \t]*\$\(\([ \t]*(?P=hist_counter_a)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {12}(?! )if[^\n]*\$(?P=hist_counter_a)[^\n]*-ge[^\n]*\$(?P=hist_limit_a)\b[^\n]*;[ \t]*then)^ {12,}[^\n]*\n)){0,12}^ {12}(?! )if[^\n]*\$(?P=hist_counter_a)[^\n]*-ge[^\n]*\$(?P=hist_limit_a)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n)(?!(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,80}^ (?P<hist_limit_b>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*[1-9][0-9]*[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ [A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?$)^ {10,}[^\n]*\n)){0,40}^ (?P<hist_counter_b>[A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*0[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,40}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12}(?! )(?P=hist_counter_b)[ \t]*=[ \t]*\$\(\([ \t]*(?P=hist_counter_b)[ \t]*\+[ \t]*1[ \t]*\)\))^ {12,}[^\n]*\n)){0,20}^ {12}(?! )(?P=hist_counter_b)[ \t]*=[ \t]*\$\(\([ \t]*(?P=hist_counter_b)[ \t]*\+[ \t]*1[ \t]*\)\)[ \t]*(?:#[^\n]*)?\n(?>(?:(?!^ {12}(?! )if[^\n]*\$(?P=hist_counter_b)[^\n]*-ge[^\n]*\$(?P=hist_limit_b)\b[^\n]*;[ \t]*then)^ {12,}[^\n]*\n)){0,12}^ {12}(?! )if[^\n]*\$(?P=hist_counter_b)[^\n]*-ge[^\n]*\$(?P=hist_limit_b)\b[^\n]*;[ \t]*then[^\n]*\n(?>(?:(?!^ {14}(?! )exit[ \t]+[1-9][0-9]*\b)^ {14,}[^\n]*\n)){0,8}^ {14}(?! )exit[ \t]+[1-9][0-9]*\b[^\n]*\n)(?>(?:(?!^ while[ \t])^ {10,}[^\n]*\n)){0,80}^ while[ \t]+(?::|true)[ \t]*;[ \t]*do[^\n]*\n(?>(?:(?!^ {12,}(?:(?:if[ \t]+![ \t]+)?(?:[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*)?["'']?\$\([ \t]*(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b|(?:if[ \t]+![ \t]+)?(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b))^ {12,}[^\n]*\n)){0,80}^ {12,}(?:(?:if[ \t]+![ \t]+)?(?:[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*)?["'']?\$\([ \t]*(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b|(?:if[ \t]+![ \t]+)?(?:timeout[ \t]+\S+[ \t]+)?gh[ \t]+api\b)[^\n]*\n(?!(?>^ {12}[^\n]*\n){0,80}^ {12}(?:break(?:[ \t]+[1-9][0-9]*)?|exit(?:[ \t]+0)?)[ \t]*(?:#[^\n]*)?$\n)(?>(?:(?!^ {12}(?! )sleep(?:[ \t]+|$))^ {12,}[^\n]*\n)){0,80}^ {12}(?! )sleep(?:[ \t]+|$)[^\n]*\n(?>(?:(?!^ done\b)^ {12,}[^\n]*\n)){0,80}^ done\b'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Constant timeout expressions trigger blockers

A positive timeout written as ${{ 20 }} does not match the literal-only timeout-minutes exclusion. Bounded jobs can receive HIGH polling findings.

Prompt for agents
Extend the owning-job timeout exclusion consistently across all four polling detector files to accept statically positive GitHub Actions constant expressions, while continuing to reject zero, negative, empty, or dynamically unprovable expressions. Add _scan_file regressions for each detector family member and ensure sibling-job timeouts still cannot suppress a finding.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

RED를 먼저 고정했습니다. d50f49ccea1cbf2aecc6da268850fddfc80db3b6에서 owning-job timeout-minutes: ${{ 20 }}를 네 polling family member 각각의 bounded negative로 실행하고, ${{ 0 }}, ${{ -1 }}, ${{ inputs.timeout }}는 unproved timeout이라 generic vulnerable loop를 suppress하지 못한다는 control도 추가했습니다. 현재 literal-only matcher는 아직 이 계약을 만족하지 않으므로 unresolved 유지합니다. GREEN은 네 detector 모두 statically positive integer constant expression만 owning-job hard bound로 인정하고 zero/negative/empty/dynamic 및 sibling-job timeout은 계속 거부하는 exact-head regression evidence입니다.

@seonghobae seonghobae Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed on current exact head e63c34279491c9c8c90764215d6ba39c8ad8d9e3. RED commit d50f49ccea1cbf2aecc6da268850fddfc80db3b6 covers timeout-minutes: ${{ 20 }} across all four polling detector identities; production commit 99bdf459cf7939896740c61c2c3fe9c222377312 accepts only a statically positive integer literal or constant expression. Zero, negative, dynamic, empty, and sibling-job timeouts remain non-suppressing. Exact-head local polling regression is 126/126 GREEN and GitHub Tests run 34082329418 is success. I am not self-resolving before fresh independent review and terminal security/code-scanning evidence.

Comment thread docs/TRACEABILITY.md Outdated

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 new potential issues.

Devin Review

Comment on lines +194 to +206
review_poll_failures=0
max_poll_transport_failures=3
while :; do
if ! reviews="$(gh api repos/example/repo/pulls/1/reviews)"; then
review_poll_failures=$((review_poll_failures + 1))
if [ "$review_poll_failures" -ge "$max_poll_transport_failures" ]; then
exit 1
fi
continue
fi
review_poll_failures=0
[ -n "$reviews" ] && break
sleep 30

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Successful polling path remains unbounded

review_poll_failures exits after transport failures but resets after successful requests. Successful empty responses still sleep indefinitely, preserving this positive regression.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +316 to +329
review_poll_failures=0
max_poll_transport_failures=3
poll_deadline_epoch=$(( $(date -u +%s) + 60 ))
while :; do
if ! reviews="$(gh api repos/example/repo/pulls/1/reviews)"; then
review_poll_failures=$((review_poll_failures + 1))
if [ "$review_poll_failures" -ge "$max_poll_transport_failures" ]; then
exit 1
fi
continue
fi
review_poll_failures=0
[ -n "$reviews" ] && break
sleep 30

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Post-loop deadline remains ineffective

The causal transport budget enables detection without creating a total bound. The deadline comparison follows done, so successful empty responses remain unbounded.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

RCA (exact-head b34670b8130f1857a8e53e6baf7cd5933826da29)

Tests job 100422974612 is 19 failed / 1108 passed, not a runner/bootstrap failure. The failures are the pinned RED precision contracts plus adjacent regex oscillation (G-06):

  • test_primary_poll_accepts_strict_total_deadline_bound / ..._attempt_bound — forward -gt total guards still emit transport-budget findings (expected finite / no finding).
  • test_primary_poll_accepts_positive_constant_timeout_expression / unreachable-exit companion — owning-job timeout-minutes: ${{ 20 }} does not suppress findings.
  • test_historical_conditional_success_termination_does_not_hide_poll and helper-deadline control-flow tests — historical positives now FN (0 == 1).
  • test_generic_transport_budget_rule_is_packaged_once — packaged alias count 2 == 1.
  • bare-exit / continue-zero cases still report state-reset / unreachable-exit identities.

coverage-evidence on this head is cancelled, strix cancelled, noema-review failed. Predecessor GREEN does not transfer.

Repair path: implement the two RED production contracts without widening regex into a general shell parser; keep current rule IDs as G-06 migration oracles. Do not Close this PR. Canonical owner remains this branch; CWL successor must not claim Closes #1087.

Copy link
Copy Markdown
Contributor Author

Stacked Draft successor #1129 maps the transport-only poll-bound family but does not replace this PR or close #1087.

#1088 remains the canonical writer for YAML rules and the current RED precision contracts. Competing production poll-loop modules, tests, and changelog fragments were removed from the successor so single-writer ownership is preserved.

Treat causally initialized forward -gt like -ge for total deadline and
attempt guards, accept statically positive timeout-minutes ${{ N }}
expressions on the owning job, keep helper-loop safety loop-local, and
treat bare exit as finite termination. Zero, negative, dynamic, reversed,
and sibling-job bounds remain unproved.

Refs #1087.
Document forward -gt total bounds and statically positive timeout
expressions as production safety evidence for issue #1087.

Refs #1087.

Copy link
Copy Markdown
Contributor Author

RED production repair on exact head e63c34279491c9c8c90764215d6ba39c8ad8d9e3

99bdf45 implements the two pinned precision contracts (forward -gt total guards and statically positive owning-job timeout-minutes: ${{ 20 }}) without replacing this PR as G-06 owner.

Local re-run of the previously failing suites:

PYTHONPATH=. .venv/bin/python -m pytest \
  tests/test_github_actions_poll_bound_current_head_regressions.py \
  tests/test_github_actions_poll_bound_aliases.py \
  tests/test_github_actions_poll_bound_bare_exit.py \
  tests/test_github_actions_poll_bound_control_flow_regression.py \
  tests/test_github_actions_poll_control_flow_review.py \
  tests/test_github_actions_poll_bound_unreachable_exit.py -q
→ 72 passed

Predecessor 19-fail result on b34670b does not transfer. Stay Draft until this exact head has terminal-success Tests/security/SAST/coverage and current-head review. Do not Close. #1129 maps the family only and does not claim Closes #1087.

Copy link
Copy Markdown
Contributor Author

Exact-head Tests GREEN

On e63c34279491c9c8c90764215d6ba39c8ad8d9e3, GitHub Tests (Python 3.11 and 3.13) are terminal success. The 19-fail result on b34670b does not transfer.

Also terminal-success on this head so far: Security Process / appguardrail-scan, Trivy, exact coverage, OpenSSF, pinned HTTPS, retention audit, CodeQL check-run.

Still Draft / BLOCKED until OpenCode, Noema, Strix, remaining security/SAST admission, and current-head review finish on this unchanged SHA. The PR body still describes unimplemented RED at b34670b; live production is 99bdf45 + e63c342. #1129 does not replace this owner and does not close #1087.

seonghobae added a commit that referenced this pull request Sep 7, 2026
Summary:
- Record 05:40 UTC exact-head evidence for #998 coverage/bootstrap
  SUCCESS, #972/#1006 non-force restacks onto develop@e71d37e, and
  #1130 Draft under canonical #1107.
- Mark G-02 in progress on #972 and G-06 as a stacked successor of
  #1088 rather than further regex growth.

Rationale:
- #999 is the single writer of the product-technical gap baseline.
- Restacked heads invalidate predecessor GREEN; the snapshot must
  not claim protected capability.

Tests:
- documentation-only; no scanner behavior change

Copy link
Copy Markdown
Contributor Author

G-06 successor exists as stacked Draft PR #1133 (feat/actions-poll-structural-analyzer-1087).

#1088 remains the regex corpus owner. The successor preserves detector IDs github-actions-transport-only-poll-bound and github-actions-transport-failure-budget-poll-bound and does not Close #1087 or #1088. ADR-0009 is Proposed.

seonghobae added a commit that referenced this pull request Sep 7, 2026
Summary:
- Snapshot 06:01 UTC records Draft #1133 `529ecb0` stacked on #1088.
- ADR-0009 remains Proposed; regex detector IDs stay migration oracles.

Rationale:
- #999 is the single writer of the product-technical gap baseline.
- Analyzer-only slice is not protected scanner emission.

Tests:
- documentation-only; analyzer evidence lives on #1133
seonghobae added a commit that referenced this pull request Sep 7, 2026
Summary:
- Snapshot 06:16 UTC records #1133 `5baeab9` with
  actions-poll-analyzer-coverage and a queued exact-head job.
- Hosted coverage is queued, not claimed GREEN.

Rationale:
- Stacked PRs targeting #1088 do not inherit develop-only coverage
  workflows. The PR-number-bound job is the exact-head evidence path.

Tests:
- documentation-only; analyzer coverage evidence lives on #1133
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ci-cd CI, GitHub Actions, checks, release, or supply chain enhancement New feature or request priority: high security Security vulnerability or security-governance work status: needs-review Open pull request requiring current-head review or checks type: feature New or expanded product capability

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant