From 3de71f0b612533ce915fd2cae24aecbf312086a4 Mon Sep 17 00:00:00 2001 From: Augusto Bastos <67170506+augbastos@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:05:26 +0100 Subject: [PATCH] fix(hook): linear git-commit detection; skip Vitest majors and Dependabot seals The flag group in _GIT_COMMIT could split '-a -b' two ways and backtracked exponentially on repeated flags (CodeQL py/redos). A flag value may no longer start with '-'; results are unchanged, and the regression test times out against the old expression. Dependabot ignores major Vitest updates while @cloudflare/vitest-plugin requires vitest ^4, and scpe-seal skips Dependabot's pull requests, which verify already exempts. --- .github/dependabot.yml | 6 ++++++ .github/workflows/scpe-seal.yml | 4 +++- CHANGELOG.md | 2 ++ hook/devcard_lib.py | 5 ++++- hook/test_devcard_lib.py | 13 +++++++++++++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cfb4994..8914d30 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,6 +14,12 @@ updates: interval: weekly cooldown: default-days: 7 + ignore: + # @cloudflare/vitest-plugin declares a peer of vitest ^4. A major Vitest + # bump cannot install until the plugin supports it; the plugin's own + # update will say when. + - dependency-name: vitest + update-types: ["version-update:semver-major"] groups: # Wrangler, the Vitest plugin, workers-types and Vitest move together; # updating one alone is how the lockfile ended up unresolvable before. diff --git a/.github/workflows/scpe-seal.yml b/.github/workflows/scpe-seal.yml index 265db4e..262da4f 100644 --- a/.github/workflows/scpe-seal.yml +++ b/.github/workflows/scpe-seal.yml @@ -40,7 +40,9 @@ jobs: name: seal # A failed verify is usually a missing disclosure, which is exactly when the # explanation is needed. If verify failed before uploading, the download fails. - if: ${{ github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure' }} + # Dependabot is exempt from the disclosure in scpe.yml, so there is nothing to + # post on its pull requests. + if: ${{ (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') && github.event.workflow_run.actor.login != 'dependabot[bot]' }} runs-on: ubuntu-latest timeout-minutes: 5 # A job-level permissions block REPLACES the workflow default (it does not merge), diff --git a/CHANGELOG.md b/CHANGELOG.md index 19b5164..ddfb203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,8 @@ What "public API" means for devcard, since it is not a library: pull requests, and CodeQL for TypeScript, Python and the workflows. Dependabot version updates for npm and Actions, with a seven-day cooldown. - npm dependency install scripts are disabled for the Worker toolchain. +- The capture hook's `git commit` detector no longer backtracks exponentially on + repeated flags (CodeQL `py/redos`); a crafted command could stall the hook. ### Added diff --git a/hook/devcard_lib.py b/hook/devcard_lib.py index 3b272bb..1798ea3 100644 --- a/hook/devcard_lib.py +++ b/hook/devcard_lib.py @@ -254,7 +254,10 @@ def count_lines(text): _SHELL_SPLIT = re.compile(r"&&|\|\||[;\n|]") # A segment that actually *invokes* `git commit`, tolerating a path-qualified # binary and leading flags (`git -C /repo commit`, `git --no-pager commit`). -_GIT_COMMIT = re.compile(r"^\s*(?:\S*[/\\])?git(?:\.exe)?\s+(?:-\S+\s+\S+\s+|-\S+\s+)*commit\b") +# A flag's value may not itself start with `-`. Without that rule the flag group +# could split `-a -b` two ways, and a command like `git -x -x -x … x` backtracked +# exponentially — enough to stall the capture hook (CodeQL py/redos). +_GIT_COMMIT = re.compile(r"^\s*(?:\S*[/\\])?git(?:\.exe)?\s+(?:-\S+\s+(?:[^-\s]\S*\s+)?)*commit\b") def counts_as_commit(command): diff --git a/hook/test_devcard_lib.py b/hook/test_devcard_lib.py index fda5c7b..5169a46 100644 --- a/hook/test_devcard_lib.py +++ b/hook/test_devcard_lib.py @@ -1,5 +1,7 @@ import os import shutil +import subprocess +import sys import tempfile import unittest from unittest import mock @@ -146,6 +148,17 @@ def test_unrelated_command(self): self.assertFalse(lib.counts_as_commit("git status")) self.assertFalse(lib.counts_as_commit("")) + def test_a_pathological_command_cannot_stall_the_hook(self): + # The flag group used to backtrack exponentially on repeated flags. Run + # in a child process so a regression fails on the timeout instead of + # hanging the whole suite. + code = "import devcard_lib as lib; print(lib.counts_as_commit('git ' + '-! ' * 200 + 'x'))" + out = subprocess.run( + [sys.executable, "-S", "-c", code], cwd=os.path.dirname(os.path.abspath(__file__)), + capture_output=True, text=True, timeout=10, + ) + self.assertEqual(out.stdout.strip(), "False", out.stderr) + class TestToolFailed(unittest.TestCase): def test_missing_response_is_not_a_failure(self):