Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 38 additions & 46 deletions .github/workflows/maint-dependabot-auto-lock.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
name: Dependabot Auto-Lock
# Automatically regenerate requirements.lock when dependabot updates any
# declared lock input file so the checked-in lock stays in sync.
# Backstop that regenerates requirements.lock when a dependency bot updates a
# declared lock input file, so the checked-in lock stays in sync.
#
# Primary mechanism is now the fleet Renovate preset's pip-compile manager
# (renovate-presets/fleet.json), which regenerates the lock IN the same PR by
# re-running the command recorded in the lock header. This workflow is a
# Workflows-local backstop for when that in-PR regeneration is unavailable
# (e.g. Dependabot PRs, or a Renovate run that could not run uv). To avoid
# fighting Renovate it re-runs the SAME header command verbatim, producing
# byte-identical output. (Not synced to consumers; consumers rely on the preset.)

on:
pull_request:
Expand All @@ -10,6 +18,7 @@ on:
- 'tools/requirements-llm.txt'
branches:
- 'dependabot/**'
- 'renovate/**'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Filter Renovate PRs with head_ref instead

For pull_request, the branches filter matches the PR target/base branch, not the source branch (GitHub docs describe this as PRs that target matching branches). Normal Renovate PRs come from renovate/... into main, so this new pattern prevents the workflow from being queued before the job-level actor check can run, and the advertised Renovate backstop never runs when Renovate fails to regenerate the lock. Use a base branch filter such as main (or remove it) and gate source branches with github.head_ref in the job if.

Useful? React with 👍 / 👎.

Comment on lines 19 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/maint-dependabot-auto-lock.yml | head -50

Repository: stranske/Workflows

Length of output: 1926


🌐 Web query:

GitHub Actions pull_request branches filter base branch or head branch documentation

💡 Result:

In GitHub Actions, the branches and branches-ignore filters used within the on.pull_request (or on.pull_request_target) configuration exclusively target the pull request's base branch (the branch you are merging into) [1][2][3]. There is no native syntax in the on configuration to filter by the head branch (the source branch or the branch being merged from) [4]. To filter by the head branch, you must use a conditional if expression at the job or step level [1][5][4]. 1. Filtering by Base Branch (on configuration): Use the branches filter under the pull_request event. This will cause the workflow to run only when a pull request targets a branch matching the specified pattern [1][2]. on: pull_request: branches: - main - 'releases/' 2. Filtering by Head Branch (if condition): To filter by the head branch, use the github.head_ref context within a job's if condition [1][6]. This allows you to restrict specific jobs to only run when the pull request originates from a particular source branch [1][4]. jobs: my_job: if: github.head_ref == 'feature-branch' runs-on: ubuntu-latest steps: - run: echo "This job only runs if the head branch is 'feature-branch'" You can also combine these techniques to filter by both base and head branches simultaneously [5][4]. For example: on: pull_request: branches: - main jobs: my_job: if: startsWith(github.head_ref, 'feature/') runs-on: ubuntu-latest steps: - run: echo "Runs for PRs from 'feature/*' into 'main'"

Citations:


🏁 Script executed:

cat -n .github/workflows/maint-dependabot-auto-lock.yml | sed -n '50,80p'

Repository: stranske/Workflows

Length of output: 1624


🏁 Script executed:

head -n 3 requirements.lock

Repository: stranske/Template

Length of output: 236


pull_request.branches filters the base branch, not the source branch—this prevents the intended bot-PR detection.

The branches filter at lines 19–21 applies to the PR's target branch (e.g., main), not the source branch (dependabot/**, renovate/**). This means the filter will not reliably trigger on bot PRs originating from those branches. Additionally, the job-level actor check (github.actor == 'dependabot[bot]') is spoofable.

Use github.head_ref in the job if condition to detect the source branch, and remove the branches filter:

Suggested fix
 on:
   pull_request:
     paths:
       - 'pyproject.toml'
       - 'requirements.txt'
       - 'tools/requirements-llm.txt'
-    branches:
-      - 'dependabot/**'
-      - 'renovate/**'
-    if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'
+    if: >
+      startsWith(github.head_ref, 'dependabot/') || startsWith(github.head_ref, 'renovate/')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/maint-dependabot-auto-lock.yml around lines 19 - 21, The
`branches` filter in the `pull_request` trigger (lines 19-21) incorrectly
filters the base branch rather than the source branch, which prevents proper
detection of dependabot and renovate PRs. Remove the entire `branches` filter
from the pull_request configuration, and instead add an `if` condition at the
job level that uses `github.head_ref` to match against the patterns
'dependabot/**' and 'renovate/**'. This will properly detect when the source
branch of the PR originates from dependabot or renovate, which is the intended
behavior for automatically locking these bot-generated pull requests.


permissions:
contents: write
Expand All @@ -23,7 +32,7 @@ jobs:
regenerate-lock:
name: Regenerate requirements.lock
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find and inspect the workflow file
find . -name "maint-dependabot-auto-lock.yml" -type f | head -5

Repository: stranske/Workflows

Length of output: 113


🏁 Script executed:

#!/bin/bash
# Read the entire workflow file with line numbers
cat -n .github/workflows/maint-dependabot-auto-lock.yml

Repository: stranske/Workflows

Length of output: 4483


🌐 Web query:

GitHub Actions github.actor context spoofable rerun security

💡 Result:

The github.actor context in GitHub Actions is considered spoofable and insecure for gatekeeping privileged operations [1][2][3]. This is because it refers to the user who triggered the specific event that initiated the current workflow run [4], not necessarily the original creator of a pull request or the author of the code being executed [5][3]. Attackers can exploit this behavior, particularly in workflows using events like pull_request_target, by performing actions—such as commenting on a pull request or triggering a re-run—that cause the workflow to re-trigger [4]. If an attacker controls an event that triggers a re-run, they can manipulate the workflow into executing with a github.actor value that appears to be a trusted account (e.g., 'dependabot[bot]') [4][6]. Relying on this value to grant permissions or execute code can lead to a Confused Deputy attack, where a privileged process is tricked into performing malicious actions [5]. To securely validate actors, you should avoid relying on github.actor for security-critical decisions [2][3]. Instead, use immutable, event-specific contexts that represent the true initiator or author of the event [1][7][6]: 1. For pull request events, use github.event.pull_request.user.login to verify the original PR author [1][7]. 2. For comment-based triggers, use github.event.comment.user.login [1]. 3. For general security-critical validation, consider comparing against the actor's unique numeric ID (github.actor_id or event-specific IDs) rather than their username, which can be impersonated or registered by attackers [1][8]. 4. Always implement additional defense-in-depth measures for workflows that handle secrets or privileged operations, such as explicitly checking that the pull request does not originate from a fork (github.event.pull_request.head.repo.fork == false) [7][9].

Citations:


🏁 Script executed:

#!/bin/bash
# Search for any comments, issues, or discussions about github.actor in workflows
rg "github\.actor|github\.event\.pull_request\.user" --type yaml -C 2

Repository: stranske/Workflows

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Check if other workflows in this repo use the safer pattern
find .github/workflows -name "*.yml" -o -name "*.yaml" | xargs grep -l "if:" | head -5 | xargs cat | grep -A 2 "github.event.pull_request"

Repository: stranske/Workflows

Length of output: 251


Use PR author identity for bot gating, not github.actor.

Line 35 uses github.actor, which is exploitable through workflow reruns. For a pull request event, gate on github.event.pull_request.user.login instead, following GitHub Actions security best practices.

Suggested fix
-    if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'
+    if: contains(fromJSON('["dependabot[bot]","renovate[bot]"]'), github.event.pull_request.user.login)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if: github.actor == 'dependabot[bot]' || github.actor == 'renovate[bot]'
if: contains(fromJSON('["dependabot[bot]","renovate[bot]"]'), github.event.pull_request.user.login)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/maint-dependabot-auto-lock.yml at line 35, The `if`
condition uses `github.actor` which is exploitable through workflow reruns and
does not securely verify the actual pull request author. Replace the
`github.actor` checks in the condition with
`github.event.pull_request.user.login` to gate the workflow on the actual PR
author identity instead, following GitHub Actions security best practices for
pull request events.

Source: Coding guidelines


steps:
- uses: actions/checkout@v6
Expand All @@ -39,61 +48,44 @@ jobs:
- name: Install uv
run: pip install uv

- name: Build compile arguments
id: compile_args
- name: Regenerate requirements.lock from its recorded header command
id: check
run: |
python - <<'PY'
import shlex
import tomllib
from pathlib import Path

pyproject_path = Path("pyproject.toml")
pyproject = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
extras = sorted(pyproject.get("project", {}).get("optional-dependencies", {}))

args = ["--python-version", "3.12", "pyproject.toml"]
for extra in extras:
args.extend(["--extra", extra])

for candidate in ("requirements.txt", "tools/requirements-llm.txt"):
if Path(candidate).is_file():
args.append(candidate)

args.extend(["--universal", "--output-file"])
set -euo pipefail
if [ ! -f requirements.lock ]; then
echo "needs_update=false" >> "$GITHUB_OUTPUT"
echo "No requirements.lock present; nothing to regenerate."
exit 0
fi

github_output = Path(__import__("os").environ["GITHUB_OUTPUT"])
with github_output.open("a", encoding="utf-8") as fh:
fh.write(f"compile_args={shlex.join(args)}\n")
fh.write(f"extras={','.join(extras)}\n")
PY
# Re-run the EXACT command recorded in the lock header. Renovate's
# pip-compile manager regenerates the lock the same way (it reads this
# header), so producing byte-identical output keeps this backstop from
# fighting Renovate with flip-flop commits. The header carries only
# flags/paths (including the --output-file / -o target), so the
# unquoted word-splitting below is intentional.
args=$(sed -n 's/^#[[:space:]]*uv pip compile[[:space:]]\{1,\}\(.*\)$/\1/p' requirements.lock | head -n1)
if [ -z "${args}" ]; then
echo "::error::Could not parse a 'uv pip compile' command from the requirements.lock header."
exit 1
fi
echo "Recorded command: uv pip compile ${args}"

- name: Check if lock file needs update
id: check
run: |
out=requirements.check
log=compile.log
compile_args="${{ steps.compile_args.outputs.compile_args }}"
uv pip compile ${compile_args} "$out" 2>&1 | tee "$log" || {
# shellcheck disable=SC2086
uv pip compile ${args} 2>&1 | tee compile.log || {
Comment on lines +67 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the file exists and check the exact code at lines 67-75
echo "=== Checking file existence and content ==="
if [ -f ".github/workflows/maint-dependabot-auto-lock.yml" ]; then
  echo "File found"
  echo "Lines 67-75:"
  sed -n '67,75p' .github/workflows/maint-dependabot-auto-lock.yml
else
  echo "File not found"
fi

# Check if requirements.lock exists
echo -e "\n=== Checking for requirements.lock ==="
find . -name "requirements.lock" -type f | head -20

Repository: stranske/Workflows

Length of output: 638


🏁 Script executed:

#!/bin/bash
# Examine actual requirements.lock files to understand the header format
echo "=== Sample requirements.lock headers ==="
find . -name "requirements.lock" -type f | while read -r file; do
  echo "--- $file ---"
  head -n 3 "$file"
  echo ""
done | head -50

Repository: stranske/Workflows

Length of output: 367


🏁 Script executed:

#!/bin/bash
# Check the full context around lines 60-80 to understand the workflow better
echo "=== Full context of the workflow (lines 60-85) ==="
sed -n '60,85p' .github/workflows/maint-dependabot-auto-lock.yml

Repository: stranske/Workflows

Length of output: 1372


🏁 Script executed:

#!/bin/bash
# Check Template's requirements.lock header format to understand what gets stored
echo "=== Template requirements.lock header ==="
if [ -f "requirements.lock" ]; then
  head -n 5 requirements.lock
else
  echo "No requirements.lock found"
fi

Repository: stranske/Template

Length of output: 307


Harden header-command replay against shell injection.

Lines 67-75 parse command text from requirements.lock and execute it via unquoted expansion. Although the lock file is generated by uv itself and commits are subject to review, this is still exploitable if a malicious requirements.lock reaches main. Validate safe tokens and use argv-array semantics to eliminate the vector.

Suggested fix
-          args=$(sed -n 's/^#[[:space:]]*uv pip compile[[:space:]]\{1,\}\(.*\)$/\1/p' requirements.lock | head -n1)
-          if [ -z "${args}" ]; then
+          args=$(sed -n 's/^#[[:space:]]*uv pip compile[[:space:]]\{1,\}\(.*\)$/\1/p' requirements.lock | head -n1)
+          if [ -z "${args}" ]; then
             echo "::error::Could not parse a 'uv pip compile' command from the requirements.lock header."
             exit 1
           fi
-          echo "Recorded command: uv pip compile ${args}"
+          if printf '%s' "${args}" | grep -Eq '[;&|`$()<>]'; then
+            echo "::error::Unsafe token detected in lock header command."
+            exit 1
+          fi
+          read -r -a uv_args <<< "${args}"
+          echo "Recorded command: uv pip compile ${args}"
 
           # shellcheck disable=SC2086
-          uv pip compile ${args} 2>&1 | tee compile.log || {
+          uv pip compile "${uv_args[@]}" 2>&1 | tee compile.log || {
             echo "❌ Compilation failed:"
             cat compile.log
             exit 1
           }

As per coding guidelines, ".github/workflows/**: Flag template-injection, unpinned third-party actions, and spoofable bot-actor checks — this workflow YAML is synced to 9 consumer repos, so one bug replicates fleet-wide."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/maint-dependabot-auto-lock.yml around lines 67 - 75, The
`args` variable extracted from the `requirements.lock` file header via sed is
directly expanded unquoted in the `uv pip compile ${args}` command invocation,
creating a shell injection vulnerability. To fix this, validate that the
extracted `args` contains only safe, whitelisted tokens (such as package names,
version operators like ==, >=, etc., and no shell metacharacters), and then use
proper quoting or bash array semantics when passing the arguments to the uv
command to prevent unintended command execution. Consider splitting the args
into an array and using the safe argument expansion pattern to eliminate the
injection vector.

Source: Coding guidelines

echo "❌ Compilation failed:"
cat "$log"
cat compile.log
exit 1
}

if git diff --exit-code requirements.lock requirements.check > /dev/null 2>&1; then
if git diff --exit-code requirements.lock > /dev/null 2>&1; then
echo "needs_update=false" >> "$GITHUB_OUTPUT"
echo "✓ Lock file is already up to date"
else
echo "needs_update=true" >> "$GITHUB_OUTPUT"
echo "⚠ Lock file needs regeneration"
echo "⚠ Lock file regenerated; committing the refreshed lock"
fi

- name: Regenerate lock file
if: steps.check.outputs.needs_update == 'true'
run: |
compile_args="${{ steps.compile_args.outputs.compile_args }}"
uv pip compile ${compile_args} requirements.lock
echo "✓ Generated new lock file"

- name: Commit updated lock file
if: steps.check.outputs.needs_update == 'true'
run: |
Expand All @@ -102,6 +94,6 @@ jobs:
git add requirements.lock
git commit \
-m "chore(deps): regenerate requirements.lock" \
-m "Includes all optional dependencies"
-m "Backstop regeneration via the command recorded in the lock header."
git push
echo "✓ Committed and pushed updated lock file"
5 changes: 4 additions & 1 deletion renovate-presets/fleet.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"description": "stranske fleet-shared Renovate preset — single source of truth for dependency automation across all stranske/* repos. Replaces Dependabot for runtime pip deps + GitHub Actions, grouped with automerge-on-green. Dev-tool pins (ruff/black/mypy/pytest/...) are EXCLUDED: they move through autofix-versions.env via the maint workflows, NOT the bumper (mirrors the retired Dependabot ignore list). The vendored .github/scripts npm cascade (minimatch/brace-expansion/balanced-match) is kept in one PR. Each repo's renovate.json extends this via `github>stranske/Workflows//renovate-presets/fleet`, so a fleet-wide policy change is one edit here — no per-repo re-sync.",
"description": "stranske fleet-shared Renovate preset — single source of truth for dependency automation across all stranske/* repos. Replaces Dependabot for runtime pip deps + GitHub Actions, grouped with automerge-on-green. Dev-tool pins (ruff/black/mypy/pytest/...) are EXCLUDED: they move through autofix-versions.env via the maint workflows, NOT the bumper (mirrors the retired Dependabot ignore list). The vendored .github/scripts npm cascade (minimatch/brace-expansion/balanced-match) is kept in one PR. The pip-compile manager is enabled against requirements.lock so that uv-compiled lock (a `uv pip compile` output) is regenerated in-PR whenever a source dep changes — this is what kept consumers' locks fresh under Dependabot's native lock update; consumers have NO auto-lock workflow and rely on this preset. Each repo's renovate.json extends this via `github>stranske/Workflows//renovate-presets/fleet`, so a fleet-wide policy change is one edit here — no per-repo re-sync.",
"extends": ["config:recommended"],
"platformAutomerge": true,
"labels": ["dependencies"],
"pip-compile": {
"managerFilePatterns": ["/(^|/)requirements\\.lock$/"]
},
"packageRules": [
{
"description": "Dev-tool pins are owned by autofix-versions.env + the maint workflows — Renovate must not bump them",
Expand Down
1 change: 0 additions & 1 deletion requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ pygments==2.20.0
# via pytest
pytest==9.1.0
# via
# -r requirements.txt
# workflows (pyproject.toml)
# pytest-cov
# pytest-xdist
Expand Down
8 changes: 7 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Core runtime dependencies
# Note: pytest and other dev/test tools are NOT pinned here — they are owned by
# autofix-versions.env and the pyproject.toml [dev] extra (synced via maint-52 /
# scripts/sync_dev_dependencies.py, which updates pyproject + requirements.lock
# but NOT this file). A pytest pin here drifted out of sync and made the lock
# unsatisfiable (fixed once in #2404); removing it keeps the dev-tool pin in a
# single owner so the now-enabled Renovate pip-compile manager can always
# recompile the lock.
requests==2.34.2
pytest==9.1.0
pyyaml==6.0.3
pydantic==2.13.4
numpy==2.4.6
Expand Down
Loading