From 46570563209cbb2a73069b13cb334cebc614799f Mon Sep 17 00:00:00 2001 From: Edward Date: Mon, 19 Oct 2026 11:01:19 -0500 Subject: [PATCH 1/5] Add a util script for a common recommendation from /code-risk --- scripts/pin-git-references.sh | 154 ++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100755 scripts/pin-git-references.sh diff --git a/scripts/pin-git-references.sh b/scripts/pin-git-references.sh new file mode 100755 index 0000000..f9adb04 --- /dev/null +++ b/scripts/pin-git-references.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash + +set -euo pipefail + +mode="check" +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +usage() { + printf 'Usage: %s [--check|--write]\n' "${0##*/}" +} + +case "${1:---check}" in + --check) ;; + --write) mode="write" ;; + --help|-h) usage; exit 0 ;; + *) usage >&2; exit 2 ;; +esac + +is_sha() { + [[ "$1" =~ ^[0-9a-f]{40}$ ]] +} + +git_url() { + printf 'https://github.com/%s.git' "$1" +} + +resolve_ref() { + local repository="$1" + local reference="$2" + local remote_url + + remote_url="$(git_url "$repository")" + if [[ "$reference" == "HEAD" ]]; then + git ls-remote "$remote_url" HEAD | awk 'NR == 1 { print $1 }' + return + fi + git ls-remote "$remote_url" "refs/tags/$reference" "refs/heads/$reference" \ + | awk 'NR == 1 { print $1 }' +} + +release_tag() { + local repository="$1" + local revision="$2" + local remote_url + + remote_url="$(git_url "$repository")" + git ls-remote --tags "$remote_url" | awk -v revision="$revision" ' + $1 == revision { + sub("refs/tags/", "", $2) + sub("\\^\\{\\}$", "", $2) + print $2 + exit + }' +} + +rewrite_workflow() { + local workflow="$1" + local temp_file line prefix repository reference suffix revision tag findings=0 + + temp_file="$(mktemp)" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^([[:space:]]*-?[[:space:]]*uses:[[:space:]]*)([^@[:space:]#]+)@([^[:space:]#]+)([[:space:]]*(#.*)?)$ ]]; then + prefix="${BASH_REMATCH[1]}" + repository="${BASH_REMATCH[2]}" + reference="${BASH_REMATCH[3]}" + suffix="${BASH_REMATCH[4]}" + + if ! is_sha "$reference"; then + revision="$(resolve_ref "$repository" "$reference")" + if [[ -z "$revision" ]]; then + printf 'Unable to resolve %s@%s\n' "$repository" "$reference" >&2 + rm -f "$temp_file" + return 1 + fi + + if [[ "$mode" == "write" ]]; then + printf '%s%s@%s # %s\n' "$prefix" "$repository" "$revision" "$reference" >> "$temp_file" + else + printf 'Mutable action reference: %s@%s\n' "$repository" "$reference" >&2 + findings=1 + fi + continue + fi + fi + printf '%s\n' "$line" >> "$temp_file" + done < "$workflow" + + if [[ "$mode" == "write" ]]; then + mv "$temp_file" "$workflow" + else + rm -f "$temp_file" + fi + + return "$findings" +} + +rewrite_requirements() { + local requirements="$1" + local temp_file line prefix organization project reference revision tag findings=0 + + temp_file="$(mktemp)" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ ^(.*git\+https://github\.com/([^/]+)/([^/@[:space:]#]+)\.git)(@([^[:space:]#]+))?([[:space:]]*(#.*)?)$ ]]; then + prefix="${BASH_REMATCH[1]}" + organization="${BASH_REMATCH[2]}" + project="${BASH_REMATCH[3]}" + reference="${BASH_REMATCH[5]:-HEAD}" + + if ! is_sha "$reference"; then + revision="$(resolve_ref "$organization/$project" "$reference")" + if [[ -z "$revision" ]]; then + printf 'Unable to resolve %s@%s\n' "$organization/$project" "$reference" >&2 + rm -f "$temp_file" + return 1 + fi + tag="$(release_tag "$organization/$project" "$revision")" + + if [[ "$mode" == "write" ]]; then + if [[ -n "$tag" ]]; then + printf '%s@%s # %s\n' "$prefix" "$revision" "$tag" >> "$temp_file" + else + printf '%s@%s # No matching upstream release tag\n' "$prefix" "$revision" >> "$temp_file" + fi + else + printf 'Mutable dependency reference: %s@%s\n' "$organization/$project" "$reference" >&2 + findings=1 + fi + continue + fi + fi + printf '%s\n' "$line" >> "$temp_file" + done < "$requirements" + + if [[ "$mode" == "write" ]]; then + mv "$temp_file" "$requirements" + else + rm -f "$temp_file" + fi + + return "$findings" +} + +status=0 +while IFS= read -r workflow; do + rewrite_workflow "$workflow" || status=1 +done < <(find "$root_dir/.github/workflows" -type f \( -name '*.yml' -o -name '*.yaml' \) | sort) + +rewrite_requirements "$root_dir/requirements.in" || status=1 + +if [[ "$mode" == "check" ]] && [[ "$status" -eq 0 ]]; then + printf 'All Git dependencies and workflow actions are pinned.\n' +fi + +exit "$status" \ No newline at end of file From 0ef62a59474b593e237908c85bbe7b7ee837de51 Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 20 Oct 2026 15:49:54 -0500 Subject: [PATCH 2/5] Fix for bad work root assumption --- scripts/pin-git-references.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/pin-git-references.sh b/scripts/pin-git-references.sh index f9adb04..41cdf96 100755 --- a/scripts/pin-git-references.sh +++ b/scripts/pin-git-references.sh @@ -3,7 +3,7 @@ set -euo pipefail mode="check" -root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +root_dir="$(pwd)" usage() { printf 'Usage: %s [--check|--write]\n' "${0##*/}" From 4261edc9fc977f06759918a0160d0b2e3450fd7d Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 20 Oct 2026 15:50:17 -0500 Subject: [PATCH 3/5] Shorted by about 2/3 --- scripts/pin-git-references.sh | 158 +++++++--------------------------- 1 file changed, 29 insertions(+), 129 deletions(-) diff --git a/scripts/pin-git-references.sh b/scripts/pin-git-references.sh index 41cdf96..ab7e841 100755 --- a/scripts/pin-git-references.sh +++ b/scripts/pin-git-references.sh @@ -1,154 +1,54 @@ #!/usr/bin/env bash - set -euo pipefail -mode="check" +mode=check root_dir="$(pwd)" - -usage() { - printf 'Usage: %s [--check|--write]\n' "${0##*/}" -} - case "${1:---check}" in --check) ;; - --write) mode="write" ;; - --help|-h) usage; exit 0 ;; - *) usage >&2; exit 2 ;; + --write) mode=write ;; + --help|-h) echo "Usage: ${0##*/} [--check|--write]"; exit 0 ;; + *) echo "Usage: ${0##*/} [--check|--write]" >&2; exit 2 ;; esac -is_sha() { - [[ "$1" =~ ^[0-9a-f]{40}$ ]] -} - -git_url() { - printf 'https://github.com/%s.git' "$1" -} - -resolve_ref() { - local repository="$1" - local reference="$2" - local remote_url - - remote_url="$(git_url "$repository")" - if [[ "$reference" == "HEAD" ]]; then - git ls-remote "$remote_url" HEAD | awk 'NR == 1 { print $1 }' - return - fi - git ls-remote "$remote_url" "refs/tags/$reference" "refs/heads/$reference" \ - | awk 'NR == 1 { print $1 }' -} - -release_tag() { - local repository="$1" - local revision="$2" - local remote_url - - remote_url="$(git_url "$repository")" - git ls-remote --tags "$remote_url" | awk -v revision="$revision" ' - $1 == revision { - sub("refs/tags/", "", $2) - sub("\\^\\{\\}$", "", $2) - print $2 - exit - }' -} - -rewrite_workflow() { - local workflow="$1" - local temp_file line prefix repository reference suffix revision tag findings=0 +resolve_ref() { local url="https://github.com/$1.git"; if [[ "$2" == HEAD ]]; then git ls-remote "$url" HEAD; else git ls-remote "$url" "refs/tags/$2" "refs/heads/$2"; fi | awk 'NR==1{print $1}'; } +release_tag() { git ls-remote --tags "https://github.com/$1.git" | awk -v s="$2" '$1==s{sub("refs/tags/","",$2);sub(/\^\{\}$/,"",$2);print $2;exit}'; } - temp_file="$(mktemp)" +# args: file regex ref-group kind("action"|"dependency") +process() { + local file="$1" re="$2" g="$3" kind="$4" out line findings=0 + out=$(mktemp) while IFS= read -r line || [[ -n "$line" ]]; do - if [[ "$line" =~ ^([[:space:]]*-?[[:space:]]*uses:[[:space:]]*)([^@[:space:]#]+)@([^[:space:]#]+)([[:space:]]*(#.*)?)$ ]]; then - prefix="${BASH_REMATCH[1]}" - repository="${BASH_REMATCH[2]}" - reference="${BASH_REMATCH[3]}" - suffix="${BASH_REMATCH[4]}" - - if ! is_sha "$reference"; then - revision="$(resolve_ref "$repository" "$reference")" - if [[ -z "$revision" ]]; then - printf 'Unable to resolve %s@%s\n' "$repository" "$reference" >&2 - rm -f "$temp_file" - return 1 - fi - - if [[ "$mode" == "write" ]]; then - printf '%s%s@%s # %s\n' "$prefix" "$repository" "$revision" "$reference" >> "$temp_file" - else - printf 'Mutable action reference: %s@%s\n' "$repository" "$reference" >&2 - findings=1 - fi - continue - fi - fi - printf '%s\n' "$line" >> "$temp_file" - done < "$workflow" - - if [[ "$mode" == "write" ]]; then - mv "$temp_file" "$workflow" - else - rm -f "$temp_file" - fi - - return "$findings" -} - -rewrite_requirements() { - local requirements="$1" - local temp_file line prefix organization project reference revision tag findings=0 - - temp_file="$(mktemp)" - while IFS= read -r line || [[ -n "$line" ]]; do - if [[ "$line" =~ ^(.*git\+https://github\.com/([^/]+)/([^/@[:space:]#]+)\.git)(@([^[:space:]#]+))?([[:space:]]*(#.*)?)$ ]]; then - prefix="${BASH_REMATCH[1]}" - organization="${BASH_REMATCH[2]}" - project="${BASH_REMATCH[3]}" - reference="${BASH_REMATCH[5]:-HEAD}" - - if ! is_sha "$reference"; then - revision="$(resolve_ref "$organization/$project" "$reference")" - if [[ -z "$revision" ]]; then - printf 'Unable to resolve %s@%s\n' "$organization/$project" "$reference" >&2 - rm -f "$temp_file" - return 1 - fi - tag="$(release_tag "$organization/$project" "$revision")" - - if [[ "$mode" == "write" ]]; then - if [[ -n "$tag" ]]; then - printf '%s@%s # %s\n' "$prefix" "$revision" "$tag" >> "$temp_file" + if [[ "$line" =~ $re ]]; then + local prefix="${BASH_REMATCH[1]}" repo="${BASH_REMATCH[2]}" ref="${BASH_REMATCH[$g]:-HEAD}" + if [[ ! "$ref" =~ ^[0-9a-f]{40}$ ]]; then + local sha; sha=$(resolve_ref "$repo" "$ref") + if [[ -z "$sha" ]]; then + echo "Unable to resolve $repo@$ref" >&2; rm -f "$out"; return 1 + elif [[ "$mode" == write ]]; then + if [[ "$kind" == action ]]; then + printf '%s%s@%s # %s\n' "$prefix" "$repo" "$sha" "$ref" >>"$out" else - printf '%s@%s # No matching upstream release tag\n' "$prefix" "$revision" >> "$temp_file" + local tag; tag=$(release_tag "$repo" "$sha") + printf '%s@%s # %s\n' "$prefix" "$sha" "${tag:-No matching upstream release tag}" >>"$out" fi else - printf 'Mutable dependency reference: %s@%s\n' "$organization/$project" "$reference" >&2 - findings=1 + echo "Mutable $kind reference: $repo@$ref" >&2; findings=1 fi continue fi fi - printf '%s\n' "$line" >> "$temp_file" - done < "$requirements" - - if [[ "$mode" == "write" ]]; then - mv "$temp_file" "$requirements" - else - rm -f "$temp_file" - fi - + printf '%s\n' "$line" >>"$out" + done <"$file" + [[ "$mode" == write ]] && mv "$out" "$file" || rm -f "$out" return "$findings" } status=0 -while IFS= read -r workflow; do - rewrite_workflow "$workflow" || status=1 +while IFS= read -r wf; do + process "$wf" '^([[:space:]]*-?[[:space:]]*uses:[[:space:]]*)([^@[:space:]#]+)@([^[:space:]#]+)([[:space:]]*(#.*)?)$' 3 action || status=1 done < <(find "$root_dir/.github/workflows" -type f \( -name '*.yml' -o -name '*.yaml' \) | sort) -rewrite_requirements "$root_dir/requirements.in" || status=1 - -if [[ "$mode" == "check" ]] && [[ "$status" -eq 0 ]]; then - printf 'All Git dependencies and workflow actions are pinned.\n' -fi +process "$root_dir/requirements.in" '^(.*git\+https://github\.com/([^/]+/[^/@[:space:]#]+)\.git)(@([^[:space:]#]+))?([[:space:]]*(#.*)?)$' 4 dependency || status=1 +[[ "$mode" == check && "$status" -eq 0 ]] && echo "All Git dependencies and workflow actions are pinned." exit "$status" \ No newline at end of file From 3d558898ed7d1ca3f6c8ddd9584f1cbd04ee8ab0 Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 20 Oct 2026 15:55:54 -0500 Subject: [PATCH 4/5] Do only .github/workflows files --- scripts/pin-git-references.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/pin-git-references.sh b/scripts/pin-git-references.sh index ab7e841..fce89c5 100755 --- a/scripts/pin-git-references.sh +++ b/scripts/pin-git-references.sh @@ -48,7 +48,5 @@ while IFS= read -r wf; do process "$wf" '^([[:space:]]*-?[[:space:]]*uses:[[:space:]]*)([^@[:space:]#]+)@([^[:space:]#]+)([[:space:]]*(#.*)?)$' 3 action || status=1 done < <(find "$root_dir/.github/workflows" -type f \( -name '*.yml' -o -name '*.yaml' \) | sort) -process "$root_dir/requirements.in" '^(.*git\+https://github\.com/([^/]+/[^/@[:space:]#]+)\.git)(@([^[:space:]#]+))?([[:space:]]*(#.*)?)$' 4 dependency || status=1 - [[ "$mode" == check && "$status" -eq 0 ]] && echo "All Git dependencies and workflow actions are pinned." exit "$status" \ No newline at end of file From b681f3f3238e2bc73c8a61bd36b0a59bcf3ba9b5 Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 20 Oct 2026 16:00:23 -0500 Subject: [PATCH 5/5] Prototype python version of pin-git-references --- scripts/pin-git-references.py | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100755 scripts/pin-git-references.py diff --git a/scripts/pin-git-references.py b/scripts/pin-git-references.py new file mode 100755 index 0000000..13fdc90 --- /dev/null +++ b/scripts/pin-git-references.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Pin mutable `uses:` GitHub Actions references in workflows to a commit SHA.""" + +import re +import subprocess +import sys +from pathlib import Path + +USES_RE = re.compile(r"^(\s*-?\s*uses:\s*)([^@\s#]+)@([^\s#]+)(\s*(#.*)?)$") +SHA_RE = re.compile(r"^[0-9a-f]{40}$") + + +def resolve_ref(repo: str, ref: str) -> str: + """Resolves `ref` (a tag, branch, or HEAD) in `repo` to a commit SHA, or "" if unresolvable.""" + url = f"https://github.com/{repo}.git" + refspecs = ["HEAD"] if ref == "HEAD" else [f"refs/tags/{ref}", f"refs/heads/{ref}"] + result = subprocess.run(["git", "ls-remote", url, *refspecs], capture_output=True, text=True) + lines = result.stdout.splitlines() + return lines[0].split()[0] if result.returncode == 0 and lines else "" + + +def process_workflow(path: Path, write: bool) -> bool: + """Rewrites or checks one workflow file. Returns False if a finding or an unresolvable ref was hit.""" + output = [] + ok = True + for line in path.read_text().splitlines(): + match = USES_RE.match(line) + if match: + prefix, repo, ref = match.group(1), match.group(2), match.group(3) + if not SHA_RE.match(ref): + sha = resolve_ref(repo, ref) + if not sha: + print(f"Unable to resolve {repo}@{ref}", file=sys.stderr) + return False + if write: + output.append(f"{prefix}{repo}@{sha} # {ref}") + else: + print(f"Mutable action reference: {repo}@{ref}", file=sys.stderr) + ok = False + continue + output.append(line) + + if write: + path.write_text("".join(f"{line}\n" for line in output)) + return ok + + +def main() -> int: + mode = "check" + arg = sys.argv[1] if len(sys.argv) > 1 else "--check" + if arg == "--write": + mode = "write" + elif arg in ("--help", "-h"): + print(f"Usage: {Path(sys.argv[0]).name} [--check|--write]") + return 0 + elif arg != "--check": + print(f"Usage: {Path(sys.argv[0]).name} [--check|--write]", file=sys.stderr) + return 2 + + workflows_dir = Path.cwd() / ".github" / "workflows" + workflows = sorted(p for p in workflows_dir.glob("*") if p.suffix in (".yml", ".yaml") and p.is_file()) + + status = 0 + for workflow in workflows: + if not process_workflow(workflow, write=mode == "write"): + status = 1 + + if mode == "check" and status == 0: + print("All Git dependencies and workflow actions are pinned.") + + return status + + +if __name__ == "__main__": + sys.exit(main())