Skip to content
Open
Show file tree
Hide file tree
Changes from 81 commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
a0130f4
test(actions): pin vulnerable transport-only verdict poll
seonghobae Sep 2, 2026
43e63be
test(actions): pin fixed verdict poll deadline
seonghobae Sep 2, 2026
fc9190c
test(actions): define transport-only poll detector contract
seonghobae Sep 2, 2026
023782b
feat(actions): detect transport-only verdict polling bounds
seonghobae Sep 2, 2026
7d60c92
docs(security): trace verdict-poll capacity defect
seonghobae Sep 2, 2026
0c79c61
docs(changelog): record bounded poll detector
seonghobae Sep 2, 2026
32e23a8
test(actions): keep polling bounds scoped to owning job
seonghobae Sep 2, 2026
f9d4195
fix(actions): scope verdict poll bounds to owning job
seonghobae Sep 2, 2026
74f16eb
docs(actions): bind poll safety evidence to owning job
seonghobae Sep 2, 2026
148458b
test(actions): pin polling scope review boundaries
seonghobae Sep 2, 2026
87db6be
fix(actions): require one executable polling loop
seonghobae Sep 2, 2026
1ca346e
docs(actions): trace executable polling-loop boundary
seonghobae Sep 2, 2026
e0d48a5
test(actions): cover reordered same-job timeout
seonghobae Sep 2, 2026
4feb533
fix(actions): honor owning job timeout regardless of key order
seonghobae Sep 2, 2026
e56aae2
test(actions): cover late same-job timeout
seonghobae Sep 2, 2026
119e949
test(actions): cover late owning-job timeouts
seonghobae Sep 2, 2026
edf2b52
fix(actions): scan full job for timeout
seonghobae Sep 2, 2026
57e683a
test(actions): define renamed transport-budget contract
seonghobae Sep 2, 2026
36f184d
feat(actions): detect renamed transport-only retry budgets
seonghobae Sep 2, 2026
3b4ea19
docs(actions): trace renamed retry-budget detection
seonghobae Sep 2, 2026
df51306
docs(changelog): record alias-safe poll detection
seonghobae Sep 2, 2026
e2fb93c
test(actions): bound renamed polls by causal guards
seonghobae Sep 2, 2026
b68568a
fix(actions): recognize renamed total poll guards
seonghobae Sep 2, 2026
92c4b63
test(actions): require causal total poll guards
seonghobae Sep 2, 2026
cb12b27
fix(actions): require initialized total poll guards
seonghobae Sep 2, 2026
86eb50c
test(actions): reject late total-bound initialization
seonghobae Sep 2, 2026
95bb935
refactor(actions): isolate generic transport-budget detector
seonghobae Sep 2, 2026
4b0e757
fix(actions): require pre-loop total-bound state
seonghobae Sep 2, 2026
bad9d32
test(security): pin poll-bound review regressions
seonghobae Sep 2, 2026
99a2302
feat(security): structurally analyze Actions poll bounds
seonghobae Sep 2, 2026
d5e5589
chore(security): drop unused analyzer draft
seonghobae Sep 2, 2026
4a948df
fix(security): require causal historical poll bounds
seonghobae Sep 2, 2026
9e54673
fix(security): localize generic poll-bound state
seonghobae Sep 2, 2026
7196e44
docs(security): record poll-bound causal boundaries
seonghobae Sep 2, 2026
ccb8000
docs(security): bind polling safety to exact loop
seonghobae Sep 2, 2026
b7618f6
test(security): pin mixed-name poll safety
seonghobae Sep 2, 2026
3ca23fa
fix(security): accept renamed historical poll bounds
seonghobae Sep 2, 2026
11bcff8
docs(security): record identifier-agnostic poll safety
seonghobae Sep 2, 2026
c3cb796
docs(security): record renamed polling safety state
seonghobae Sep 2, 2026
cdd24c9
perf(security): prefilter generic poll detector
seonghobae Sep 2, 2026
34a37ec
test(security): pin poll command whitespace variants
seonghobae Sep 2, 2026
327b154
fix(security): preserve gh command whitespace detection
seonghobae Sep 2, 2026
23db570
fix(security): preserve generic gh whitespace detection
seonghobae Sep 2, 2026
de84e5b
test(security): pin mutable poll bound bypasses
seonghobae Sep 2, 2026
3fe5249
test(security): keep poll mutation fixtures loop-local
seonghobae Sep 2, 2026
5c64e24
test(security): pin invalid break-zero poll bypass
seonghobae Sep 2, 2026
71e2617
fix(security): detect mutable total poll bounds
seonghobae Sep 2, 2026
a3884b0
feat(security): detect ineffective mutable poll bounds
seonghobae Sep 2, 2026
b4a134e
test(security): bind break-zero regression to companion
seonghobae Sep 2, 2026
b09140b
feat(security): isolate invalid break-zero poll detector
seonghobae Sep 2, 2026
077121b
chore(security): preserve canonical mutable-bound detector
seonghobae Sep 2, 2026
a56f2b3
docs(security): record mutable-bound and break-zero companions
seonghobae Sep 2, 2026
0136677
docs(security): trace mutable poll bounds and break-zero
seonghobae Sep 2, 2026
f293da0
test(security): keep large polling job workflow valid
seonghobae Sep 2, 2026
2529fc5
test(security): pin polling loop locality and quoted command boundaries
seonghobae Sep 2, 2026
d94effe
fix(security): drop unsound break-zero companion
seonghobae Sep 2, 2026
6868b10
test(security): remove shell-ambiguous break-zero oracle
seonghobae Sep 2, 2026
960536f
docs(security): narrow break semantics to proven shell behavior
seonghobae Sep 2, 2026
95d6afc
docs(security): remove unsound break-zero coverage claim
seonghobae Sep 2, 2026
cdd4b90
test(security): distinguish deadline tightening from refresh
seonghobae Sep 2, 2026
8496b21
fix(security): distinguish deadline extension from tightening
seonghobae Sep 2, 2026
f289096
test(security): pin conditional exit and retry declaration boundaries
seonghobae Sep 2, 2026
c86f7c9
fix(security): require executable gh api poll evidence
seonghobae Sep 2, 2026
c49c943
fix(security): preserve conditional poll back edges
seonghobae Sep 2, 2026
e4e4cf8
fix(security): distinguish conditional poll termination
seonghobae Sep 2, 2026
fe312dc
fix(security): accept reversed retry declarations
seonghobae Sep 2, 2026
2f3fd77
test(security): pin reversed safety and deadline guard precision
seonghobae Sep 2, 2026
5ce4114
fix(security): require expiration-direction deadline guard
seonghobae Sep 2, 2026
89efd14
fix(security): accept reversed total-bound declarations
seonghobae Sep 2, 2026
ba2b030
test(security): pin unreachable poll-bound exits
seonghobae Sep 2, 2026
87ec49f
security(actions): detect unreachable poll-bound exits
seonghobae Sep 2, 2026
52ab761
docs(security): trace unreachable poll-bound exits
seonghobae Sep 2, 2026
f0ba2c5
test(actions): reject unreachable polling continues
seonghobae Sep 2, 2026
959809a
fix(actions): require reachable polling continue
seonghobae Sep 2, 2026
34bc96c
test(actions): pin current polling detector review boundaries
seonghobae Sep 2, 2026
27fbf01
fix(actions): honor independent polling bounds
seonghobae Sep 2, 2026
ebc3aaa
test(actions): pin executable poll command semantics
seonghobae Sep 2, 2026
125fb94
fix(actions): require executable historical poll command
seonghobae Sep 2, 2026
1a70e3a
fix(actions): preserve historical reverse bound capture
seonghobae Sep 2, 2026
c8263ed
fix(actions): require executable unreachable poll command
seonghobae Sep 2, 2026
4990691
fix(actions): tighten generic poll execution and termination
seonghobae Sep 2, 2026
ca9ebfa
test(actions): pin post-sleep termination ownership
seonghobae Sep 2, 2026
2610305
fix(actions): preserve poll termination ownership
seonghobae Sep 2, 2026
cea9ddb
test(actions): treat bare exit as polling termination
seonghobae Sep 2, 2026
4cce94e
fix(actions): recognize bare exit as finite poll termination
seonghobae Sep 2, 2026
7c81044
fix(actions): recognize bare exit in transport poll companion
seonghobae Sep 2, 2026
df4ff1c
test(actions): require historical transport counter data flow
seonghobae Sep 2, 2026
a467678
fix(actions): require historical transport budget data flow
seonghobae Sep 2, 2026
5d87c36
test(actions): keep historical scope fixtures causal
seonghobae Sep 2, 2026
696009f
docs: baseline polling detector commercial gaps
seonghobae Sep 2, 2026
93f953a
docs: trace historical polling budget causally
seonghobae Sep 2, 2026
d50f49c
test(actions): cover strict bounds and constant timeouts
seonghobae Sep 2, 2026
b34670b
docs: pin current polling precision RED gaps
seonghobae Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.d/1087-actions-transport-only-poll-bound.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Security

- Add the packaged `github-actions-transport-only-poll-bound` detector for GitHub Actions review/control-plane polling loops whose retry budget covers only transport failures while the successful-no-result path can wait indefinitely. The regression corpus pins the verified `ContextualWisdomLab/.github` vulnerable predecessor `5c561a65cca3b925d533e4b40c5c3ac00f16524e` and protected wall-clock repair `e29302c05eade7da7b0bdbb453e53980bc9d577b`. Historical-name safety suppression requires the compared deadline/attempt state to be causally initialized before the same loop, but the safety variable names themselves are identifier-agnostic; unset or late state remains vulnerable, while an unconditional successful-path termination before the sleep/back edge is finite within the reviewed shell grammar. The companion `github-actions-transport-failure-budget-poll-bound` detects the same causal failure when shell retry identifiers are renamed by requiring the failure counter, retry limit, failing `gh api` branch, increment, threshold exit, and repeatable healthy-transport sleep path to be linked in one reviewed loop. `github-actions-poll-bound-state-reset` retains a HIGH finding when an apparent deadline/total-attempt bound is neutralized by a reviewed state mutation that prevents convergence. Safety evidence is loop-local: a bounded helper loop cannot donate a deadline/attempt bound to a later poll. Negative boundaries also cover explicit same-job timeouts, stable monotonic total bounds, comments/quoted commands, split steps, sibling jobs, and non-workflow paths. Shell-error termination depends on the selected GitHub Actions shell and fail-fast flags; the withdrawn `break 0` companion is not retained because a default Linux Actions shell uses fail-fast Bash semantics, so a shell-agnostic HIGH rule would create blocker-class false positives. Explicit non-errexit invalid-break behavior remains an unclaimed boundary until shell selection/state is modeled. See issue #1087.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
21 changes: 20 additions & 1 deletion docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# AppGuardrail Requirements, Detection, and Evidence Traceability

**Status:** Accepted cross-cutting baseline
**Last reviewed:** 2026-08-12
**Last reviewed:** 2026-09-02

| Requirement / security class | Detector/control boundary | Evidence maturity |
|---|---|---|
Expand All @@ -19,6 +19,7 @@
| every retained issue claim mapped to executable detector obligation | issue-detection audit | PR #911 active-PR |
| authenticated workflow-result detector evidence | issue-detection audit workflow evidence | PR #911 active-PR |
| automatic scanner detection of unsafe stored-webhook SSRF pattern | built-in `python-stored-ssrf-webhook-url` rule | implemented-main through PR #910 for tested Python `set_webhook` direct and one-hop persistence flows; bounded scope |
| GitHub Actions transport-only polling bound | `github-actions-transport-only-poll-bound`, identifier-agnostic `github-actions-transport-failure-budget-poll-bound`, mutable-safety companion `github-actions-poll-bound-state-reset`, unreachable-exit companion `github-actions-poll-bound-unreachable-exit` | Issue #1087 / active PR #1088; verified causal owner wall-clock repair is protected `ContextualWisdomLab/.github@e29302c05eade7da7b0bdbb453e53980bc9d577b`; stronger event-driven runner-release owner work remains Proposed in `.github` PR #1706 |
| structural Semgrep-style `pattern:` execution by lightweight engine | built-in scanner | not implemented unless a real structural matcher is added; fixtures are not execution |

## Promotion rules
Expand Down Expand Up @@ -47,6 +48,24 @@ For stored webhook/callback SSRF, trace separately:

Current protected-branch evidence keeps those controls distinct: PR #924 supplies the fail-closed webhook storage boundary, and PR #910 supplies the packaged `python-stored-ssrf-webhook-url` detector plus focused regression corpus. Neither control expands the detector beyond its declared source/sink and flow contract.

## GitHub Actions polling-bound traceability contract

Issue #1087 records a verified control-plane availability defect from `ContextualWisdomLab/.github`. At vulnerable protected predecessor `5c561a65cca3b925d533e4b40c5c3ac00f16524e`, the required OpenCode verdict step used a transport-failure counter around `gh api` calls but had no total bound for the path where every API call succeeded and no verdict appeared. Protected repair `e29302c05eade7da7b0bdbb453e53980bc9d577b` adds a 10,800-second wall-clock deadline checked on every loop iteration and fails closed when it expires. `.github` PR #1706 separately owns the stronger Proposed event-driven/one-shot runner-release architecture; its open writer branch and temporary source-fix machinery are not protected production evidence.

The AppGuardrail obligation is the reusable causal pattern, not the workflow, issue title, or historical shell identifier spelling. `github-actions-transport-only-poll-bound` preserves the pinned source incident and its reviewed job/run/loop boundaries, but safety semantics are identifier-agnostic: a deadline or total-attempt guard suppresses only when the exact captured state is initialized before that same loop and consumed by the same fail-closed in-loop guard. `github-actions-transport-failure-budget-poll-bound` closes the transport-budget identifier-renaming false-negative boundary by capturing arbitrary shell identifiers only when a zeroed failure counter is incremented on the failing `gh api` branch, compared against a positive retry limit, and causes a nonzero exit at the threshold while the healthy-transport/no-result path can still sleep and repeat. The companion excludes the historical `max_poll_transport_failures` form so one incident is not emitted twice.

Syntactically present safety state is not automatically a real bound. `github-actions-poll-bound-state-reset` retains HIGH evidence only for reviewed non-convergent mutations such as refreshing a deadline from the current clock, resetting a total-attempt counter, or growing a total limit alongside its counter. State mutations that tighten a deadline or otherwise move monotonically toward termination are safe lookalikes and must remain negative. `github-actions-poll-bound-unreachable-exit` separately preserves HIGH evidence when an apparent total deadline or total-attempt guard contains an unconditional `continue` before the nonzero fail-closed exit, so the textual exit is unreachable and cannot establish finiteness. Its reviewed positive path is confined to the same conventional literal-shell polling loop and does not fire when the owning job has a positive `timeout-minutes` or when the fail-closed exit is directly reachable.

The temporary `github-actions-poll-invalid-break-zero` companion is not retained. A current-head review established that GitHub's default Linux Actions shell is fail-fast Bash, so a shell-agnostic HIGH rule for `break 0` creates a blocker-class false positive: the invalid command can terminate the step instead of reaching the loop back edge. Explicit non-errexit invalid-break behavior is an unclaimed false-negative boundary until the detector models the selected Actions shell and fail-fast state. Removing an unsound detector is not evidence that the main #1087 control-flow family covers that shell-specific behavior.

The family is deliberately bounded to conventional two-space GitHub Actions job syntax and literal block shell steps. Evidence split across jobs, across run steps, or after the loop is negative; quoted `gh api`/`sleep` strings and shell comments are not executable evidence. Safety evidence is local to the same candidate job and loop: an explicit positive timeout on that job is a bound, and a loop deadline/attempt guard is a bound only when its state is initialized before the loop, remains convergent for the loop lifetime, is reachable on the relevant path, and the same state is consumed by an in-loop nonzero-exit guard. Uninitialized variables, state first assigned after the guard, and a fail-closed exit made unreachable by an earlier unconditional control transfer do not establish safety. A bounded helper loop cannot donate deadline/attempt evidence to a later vulnerable poll. A comparison that only logs without terminating is not a safety guard, and unrelated numeric variables are not a transport-failure budget unless the failure branch links the counter, limit, increment, threshold, and nonzero exit.

The large regular-expression rules use only cheap `while` / `sleep` prefilters. Executable command evidence remains governed by the detector grammar `gh[ \t]+api`, so repeated spaces or a tab between `gh` and `api` cannot disappear behind a literal `gh api` fast-path filter. Explicit bounded adjacency windows remain declared false-negative boundaries rather than implied structural coverage.

Regression evidence lives in the production `_scan_file` suites for poll bounds, renamed aliases, late initialization, current-review causal safety, mixed safety identifiers, command whitespace, mutable bound state, unreachable fail-closed exits, and the pinned answer-free source fixtures under `tests/fixtures/security_corpus/github_actions_transport_only_poll_{vulnerable,fixed}.yml`. Current actionable review obligations still include finite-loop handling in the mutable-state companion, independent valid total bounds that should suppress mutable-state false positives, executable-token proof inside `$()` command substitution, and structural pairing of `break`/`exit 0` with the conditional block that actually owns a later `fi`. Those are blockers, not suppressed or completed claims.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

Current false-negative boundaries include cross-file/composite-action polling, non-shell control flow, dynamically generated workflows, quoted/nonstandard job-key syntax, noncanonical YAML indentation, declaration/control-flow or multiple-helper shapes outside the reviewed companion grammar, materially different loop/retry frameworks, shell-error behavior whose fail-fast state is not modeled, and relationships beyond bounded adjacency windows. Those limits constitute a concrete Gap for a future structural GitHub Actions + shell control-flow/state analyzer; PR #1088 does not claim that analyzer exists. The detector family remains active-PR evidence until its unchanged exact head passes required checks and ordinary protected integration; the already-protected `.github` wall-clock repair is prevention/control-plane evidence only and does not itself satisfy AppGuardrail scanner coverage.

## Standards/research

Existing repository docs/doctoring/security evidence remain the bibliography/source-of-truth for standards such as SARIF, CycloneDX, GitHub security interfaces, and applicable OWASP/CWE classes. Material new detector classes should add authoritative standard/CWE/OWASP references and APA 7 citations in doctoring where research/standards materially drive implementation.
Expand Down
36 changes: 36 additions & 0 deletions scanner/rules/github_actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,39 @@ rules:
include:
- ".github/workflows/*.yml"
- ".github/workflows/*.yaml"

# A transport-failure counter is not a total polling bound. The verified
# ContextualWisdomLab/.github incident kept every API request successful while
# no review verdict arrived, so the loop never incremented its transport
# failure counter and retained a shared runner for hours. Keep the lightweight
# detector intentionally bounded to conventional two-space Actions job syntax:
# all positive evidence must execute inside one literal-shell run block and
# gh/sleep must occur before that same unbounded loop's `done`. Same-job
# timeout and causally initialized fail-closed deadline/attempt guards are safe
# boundaries regardless of safety-variable spelling. Uninitialized or
# post-guard state is not safety evidence, and a healthy path that
# 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(?>(?:(?!^ 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'
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
prefilter: [while, sleep]

@devin-ai-integration devin-ai-integration Bot Sep 2, 2026

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: Prefilters preserve command whitespace

The while and sleep prefilters leave repeated-space and tab-separated gh api commands for the regex grammar to recognize.

Devin Review

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

message: |
This GitHub Actions job has a remote polling loop whose explicit retry
limit covers transport failures but not the successful-no-result path.
If the API stays reachable while the awaited verdict/state never appears,
the job can retain a runner until an external platform limit, degrading
availability of required review/security controls. Add a total wall-clock
deadline or finite total-attempt guard checked on every polling path and
fail closed. An explicit timeout anywhere on this same job can also bound
the runner. A timeout/counter in a sibling job, command-like text in
another run step, an echoed command string, or a shell comment is not
evidence for this loop. Keep per-request transport timeouts as defense in
depth; they do not replace the total polling bound.
severity: HIGH
languages: [generic]
cwe: [CWE-400]
owasp: [A04:2021]
paths:
include:
- ".github/workflows/*.yml"
- ".github/workflows/*.yaml"
Loading
Loading