-
Notifications
You must be signed in to change notification settings - Fork 1
fix(scripts): pin self-update + SPM dependency to immutable revision (DEVA11Y-475,478,477) #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fc2a1ef
12fc128
870b4f7
4293136
796017a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| name: Verify self-update checksums | ||
|
|
||
| # Self-update fetches each launcher script from `main` and verifies it against a | ||
| # committed `<script>.sha256` sidecar. If a script is edited without regenerating | ||
| # its sidecar, self-update silently breaks for every user (checksum mismatch → | ||
| # update refused). This workflow fails the PR/push when a sidecar is missing or | ||
| # out of sync, keeping the two in lockstep. (DEVA11Y-475 review follow-up.) | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'scripts/**' | ||
| - '.github/workflows/verify-selfupdate-checksums.yml' | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'scripts/**' | ||
| - '.github/workflows/verify-selfupdate-checksums.yml' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| verify-sidecars: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Verify scripts and .sha256 sidecars are in sync | ||
| run: | | ||
| set -uo pipefail | ||
| shopt -s globstar nullglob | ||
| status=0 | ||
|
|
||
| # 1. Every self-updating script must have a sidecar. | ||
| for script in scripts/**/*.sh; do | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Low] Add a zero-scripts guard (defensive) With Suggestion: Before the loop, collect into an array and Reviewer: stack:code-reviewer |
||
| if [ ! -f "${script}.sha256" ]; then | ||
| echo "::error file=${script}::Missing checksum sidecar ${script}.sha256. Generate it from the script's directory: shasum -a 256 <name>.sh | awk '{print \$1\" <name>.sh\"}' > <name>.sh.sha256" | ||
| status=1 | ||
| fi | ||
| done | ||
|
|
||
| # 2. Every sidecar must match its script. | ||
| sidecars=(scripts/**/*.sha256) | ||
| if [ ${#sidecars[@]} -eq 0 ]; then | ||
| echo "::error::No .sha256 sidecars found under scripts/." | ||
| exit 1 | ||
| fi | ||
| for sidecar in "${sidecars[@]}"; do | ||
| dir=$(dirname "$sidecar") | ||
| script="${sidecar%.sha256}" | ||
| if [ ! -f "$script" ]; then | ||
| echo "::error file=${sidecar}::Sidecar references missing script ${script}." | ||
| status=1 | ||
| continue | ||
| fi | ||
| # Sidecars store "<sha256> <basename>", so verify from the script's dir. | ||
| if ( cd "$dir" && sha256sum -c "$(basename "$sidecar")" ); then | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Low] Silence
Suggestion: Redirect the check's stdout to Reviewer: stack:code-reviewer |
||
| echo "OK: $sidecar" | ||
| else | ||
| echo "::error file=${script}::Checksum mismatch — regenerate ${sidecar} after editing ${script} (run from ${dir}): shasum -a 256 <name>.sh | awk '{print \$1\" <name>.sh\"}' > <name>.sh.sha256" | ||
| status=1 | ||
| fi | ||
| done | ||
|
|
||
| if [ "$status" -ne 0 ]; then | ||
| echo "::error::Self-update checksum verification failed. Regenerate the affected .sha256 sidecar(s) and commit them." | ||
| fi | ||
| exit "$status" | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,12 +78,94 @@ a11y_scan() { | |
| $BINARY_PATH a11y $EXTRA_ARGS | ||
| } | ||
|
|
||
| # Self-update tracks the latest launcher on `main` so users always run the | ||
| # newest version. DEVA11Y-475/477/478: we deliberately follow main HEAD rather | ||
| # than a pinned revision (per maintainer intent: always take the latest). | ||
| # Hardening retained from the pinning work: download to a temp dir, verify a | ||
| # SHA-256 sidecar (a download-integrity check, NOT an authenticity signature -- | ||
| # script and checksum share one origin), sanity-check the shebang, then | ||
| # atomically replace the on-disk script. Keep scripts/bash/cli.sh.sha256 on main in | ||
| # sync with this file (regenerate on every change) or updates will abort. | ||
| SELF_UPDATE_BRANCH="main" | ||
| readonly SELF_UPDATE_BRANCH | ||
| SELF_UPDATE_RELPATH="scripts/bash/cli.sh" | ||
| readonly SELF_UPDATE_RELPATH | ||
|
|
||
| # sha256 with a portable fallback: GNU `sha256sum` (Linux) or `shasum -a 256` | ||
| # (macOS / Perl Digest::SHA). | ||
| _self_update_sha256() { | ||
| if command -v sha256sum >/dev/null 2>&1; then | ||
| sha256sum "$1" | awk '{print $1}' | ||
| else | ||
| shasum -a 256 "$1" | awk '{print $1}' | ||
| fi | ||
| } | ||
|
|
||
| script_self_update() { | ||
| local remote_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/main/scripts/bash/cli.sh" | ||
| local base_url="https://raw.githubusercontent.com/browserstack/AccessibilityDevTools/refs/heads/${SELF_UPDATE_BRANCH}/${SELF_UPDATE_RELPATH}" | ||
| local tmp_dir tmp_script tmp_sum expected_sum actual_sum local_sum target_path stage_file | ||
|
|
||
| updated_script=$(curl -R -z "$SCRIPT_PATH" "$remote_url") | ||
| if [[ $updated_script =~ ^#! ]]; then | ||
| echo "$updated_script" > "$SCRIPT_PATH" | ||
| # Resolve the on-disk target absolutely so the replace never depends on CWD. | ||
| if [[ -n "$GIT_ROOT" && "$SCRIPT_PATH" != /* ]]; then | ||
| target_path="${GIT_ROOT}/${SCRIPT_PATH}" | ||
| else | ||
| target_path="$SCRIPT_PATH" | ||
| fi | ||
|
|
||
| tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/bs-a11y-selfupdate.XXXXXX") || { | ||
| echo "Self-update: failed to create temp dir." >&2 | ||
| return 1 | ||
| } | ||
| # shellcheck disable=SC2064 | ||
| trap "rm -rf -- '${tmp_dir}'" RETURN | ||
| tmp_script="${tmp_dir}/cli.sh" | ||
| tmp_sum="${tmp_dir}/cli.sh.sha256" | ||
|
|
||
| # Fetch the checksum first; if our on-disk copy already matches, we're current. | ||
| if ! curl -fsSL --connect-timeout 10 --max-time 30 "${base_url}.sha256" -o "$tmp_sum"; then | ||
| echo "Self-update: could not fetch checksum from ${SELF_UPDATE_BRANCH}; skipping update." >&2 | ||
| return 0 | ||
| fi | ||
| # Published sidecar is "<sha256> <filename>"; take the first field. | ||
| expected_sum=$(awk '{print $1; exit}' "$tmp_sum") | ||
| if [[ -f "$target_path" ]]; then | ||
| local_sum=$(_self_update_sha256 "$target_path") | ||
| if [[ -n "$expected_sum" && "$local_sum" == "$expected_sum" ]]; then | ||
| return 0 | ||
| fi | ||
| fi | ||
|
|
||
| if ! curl -fsSL --connect-timeout 10 --max-time 30 "$base_url" -o "$tmp_script"; then | ||
| echo "Self-update: could not download latest script; skipping update." >&2 | ||
| return 0 | ||
| fi | ||
|
|
||
| actual_sum=$(_self_update_sha256 "$tmp_script") | ||
| if [[ -z "$expected_sum" || -z "$actual_sum" || "$expected_sum" != "$actual_sum" ]]; then | ||
| echo "Self-update: checksum mismatch; refusing to apply." >&2 | ||
| echo " expected: ${expected_sum:-<empty>}" >&2 | ||
| echo " actual: ${actual_sum:-<empty>}" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| # Sanity check AFTER integrity: ensure the verified payload is a script. | ||
| if ! head -c2 "$tmp_script" | grep -q '^#!'; then | ||
| echo "Self-update: downloaded file is not a script; aborting." >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| # Stage inside the target's directory so the rename is atomic (mv across | ||
| # filesystems would degrade to a non-atomic copy). | ||
| stage_file=$(mktemp "$(dirname "$target_path")/.bs-a11y-update.XXXXXX") || { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should-fix: Stale temp files on SIGINT/SIGTERM.
Fix: Add |
||
| echo "Self-update: failed to stage update next to ${target_path}." >&2 | ||
| return 1 | ||
| } | ||
| if cp "$tmp_script" "$stage_file" && chmod 0755 "$stage_file" && mv -f "$stage_file" "$target_path"; then | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
| echo "Self-update: updated ${target_path} to latest ${SELF_UPDATE_BRANCH}." | ||
| else | ||
| rm -f -- "$stage_file" | ||
| echo "Self-update: failed to replace ${target_path}." >&2 | ||
| return 1 | ||
| fi | ||
| } | ||
|
|
||
|
|
@@ -92,7 +174,11 @@ download_binary() { | |
| bsdtar -xvf "$BINARY_ZIP_PATH" -O > "$BINARY_PATH" && chmod 0775 "$BINARY_PATH" | ||
| } | ||
|
|
||
| script_self_update | ||
| # Best-effort auto-update: always fetch the latest launcher from main before | ||
| # running. Failures (offline, integrity) are non-fatal -- the current script | ||
| # keeps working and any update applies on the next invocation. | ||
| script_self_update || true | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should-fix: User sees stderr message but script continues normally. Consider differentiating return codes:
Caller decides based on code. Currently a corrupted download and a flaky network look identical to the caller. |
||
|
|
||
| if [[ $SUBCOMMAND == "register-pre-commit-hook" ]]; then | ||
| register_git_hook | ||
| exit 0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 9af5ce77ada28741e91d2323e4664c47e7e7531e10b34168cfe6bc50a74f5d62 cli.sh |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| dceb8b3a2f8b464bcd8e6c1894ee605b3fbc9714e2cbb874ccfdcacc19240232 spm.sh |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Low] Pin
actions/checkoutto a full SHA (repo convention)The existing
Semgrep.ymlSHA-pins all actions (actions/checkout@c85c95e3… # v3.5.3). This@v4floating tag breaks that convention and GitHub's supply-chain hardening guidance.Suggestion:
Reviewer: stack:code-reviewer