Files
qbz/.github/workflows/release-updater-manifest.yml
T
vicrodh 0461c36912 security: address CodeQL alerts from the v1.2.5 scan
Triaged the 8 open CodeQL alerts raised on the 1.2.5 merge commit.

Critical (1) — real, fixed:

  * .github/workflows/release-updater-manifest.yml:29 — the resolve
    step interpolated `${{ github.event.workflow_run.head_branch }}`
    directly into a shell heredoc. `head_branch` on `workflow_run` is
    attacker-controllable (any outside contributor can push a branch
    whose name embeds a payload). Route the value through an env var
    (HEAD_BRANCH) and validate the shape with a strict regex before
    letting it flow into shell or $GITHUB_OUTPUT.

Warnings (2) — real, fixed:

  * .github/workflows/build-qbzd.yml had no `permissions:` block, so
    every job inherited the repo's default GITHUB_TOKEN scope. Add a
    top-level `permissions: contents: read`; the existing `release`
    job keeps its own `contents: write` override for publishing
    artifacts.

Warnings (3) — real, low severity, mitigated:

  * crates/qbzd/src/{daemon.rs,session.rs} logged `user_id` at info
    level (CodeQL rust/cleartext-logging). Demoted the three lines to
    `log::debug!` so default-level logs stay free of the numeric user
    id, with a comment noting RUST_LOG=debug re-enables them for
    diagnostics.

Warnings (2) — false positives, dismissed in GitHub:

  * crates/qbzd/src/login.rs:103 — `println!` to stdout inside the
    interactive `qbzd login` subcommand; login confirmation UX, not a
    persistent log. (alert #63 dismissed, reason: false positive)
  * crates/qbzd/src/qconnect.rs:540 — `session_uuid` is an ephemeral
    per-connection identifier QConnect exchanges in cleartext itself;
    not a credential or token. (alert #65 dismissed, reason: false
    positive)

cc
2026-04-15 16:10:06 -06:00

182 lines
7.0 KiB
YAML

name: Generate Updater Manifest
on:
workflow_run:
workflows:
- release-macos
- release-linux
- "Linux ARM64 (aarch64) Build"
types:
- completed
concurrency:
group: updater-manifest-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
generate-manifest:
# Only run on successful tag-triggered builds (not manual dispatches)
if: >
github.event.workflow_run.conclusion == 'success' &&
startsWith(github.event.workflow_run.head_branch, 'v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Resolve version from tag
id: version
# `head_branch` on a `workflow_run` event is attacker-controllable
# (an outside contributor could push a branch with an injection
# payload as its name). Route it through an env var and validate
# the shape before letting it flow into shell or $GITHUB_OUTPUT.
env:
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
set -euo pipefail
if ! printf '%s' "$HEAD_BRANCH" | grep -Eq '^v[0-9][0-9A-Za-z._-]{0,63}$'; then
echo "::error::Rejected head_branch for manifest job: $HEAD_BRANCH"
exit 1
fi
TAG="$HEAD_BRANCH"
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Wait for all platform builds to finish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
run: |
# Wait up to 20 minutes for .sig files from all platforms
REQUIRED_SIGS=("aarch64.app.tar.gz.sig" "x64.app.tar.gz.sig" "AppImage.sig")
MAX_ATTEMPTS=40
SLEEP_SECONDS=30
for attempt in $(seq 1 $MAX_ATTEMPTS); do
ASSETS=$(gh release view "$TAG" --repo "${{ github.repository }}" --json assets -q '.assets[].name' 2>/dev/null || echo "")
FOUND_ALL=true
for sig_suffix in "${REQUIRED_SIGS[@]}"; do
if ! echo "$ASSETS" | grep -q "$sig_suffix"; then
FOUND_ALL=false
break
fi
done
if [ "$FOUND_ALL" = true ]; then
echo "All signature files found on attempt $attempt"
break
fi
if [ "$attempt" -eq "$MAX_ATTEMPTS" ]; then
echo "Warning: Not all .sig files found after $MAX_ATTEMPTS attempts. Generating manifest with available assets."
break
fi
echo "Attempt $attempt/$MAX_ATTEMPTS: waiting for all .sig files..."
sleep $SLEEP_SECONDS
done
- name: Generate latest.json
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ steps.version.outputs.version }}
run: |
REPO="${{ github.repository }}"
RELEASE_URL="https://github.com/${REPO}/releases/download/${TAG}"
# Get release info
RELEASE_JSON=$(gh release view "$TAG" --repo "$REPO" --json body,publishedAt)
NOTES=$(echo "$RELEASE_JSON" | jq -r '.body // ""')
PUB_DATE=$(echo "$RELEASE_JSON" | jq -r '.publishedAt // ""')
# Download all .sig files
mkdir -p sigs
ASSETS=$(gh release view "$TAG" --repo "$REPO" --json assets -q '.assets[].name')
for asset in $ASSETS; do
if [[ "$asset" == *.sig ]]; then
gh release download "$TAG" --repo "$REPO" --pattern "$asset" --dir sigs
fi
done
ls -la sigs/
# Build platform entries
# Helper: read sig content for a glob pattern, return empty string if not found
read_sig() {
local sig_file
for pattern in "$@"; do
sig_file=$(find sigs -name "$pattern" -print -quit 2>/dev/null)
if [ -n "$sig_file" ] && [ -f "$sig_file" ]; then
cat "$sig_file"
return
fi
done
echo ""
}
# Find asset names matching patterns (extended regex)
find_asset() {
local pattern="$1"
echo "$ASSETS" | grep -E "$pattern" | head -1
}
# Build platforms JSON
PLATFORMS="{}"
# macOS aarch64 (.app.tar.gz for updater)
MACOS_ARM_TAR=$(find_asset "aarch64.app.tar.gz$")
MACOS_ARM_SIG=$(read_sig "*aarch64.app.tar.gz.sig")
if [ -n "$MACOS_ARM_TAR" ] && [ -n "$MACOS_ARM_SIG" ]; then
PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${MACOS_ARM_TAR}" --arg sig "$MACOS_ARM_SIG" \
'. + {"darwin-aarch64": {"signature": $sig, "url": $url}}')
fi
# macOS x86_64 (.app.tar.gz for updater)
MACOS_X64_TAR=$(find_asset "(x64|x86_64).app.tar.gz$")
MACOS_X64_SIG=$(read_sig "*x64.app.tar.gz.sig" "*x86_64.app.tar.gz.sig")
if [ -n "$MACOS_X64_TAR" ] && [ -n "$MACOS_X64_SIG" ]; then
PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${MACOS_X64_TAR}" --arg sig "$MACOS_X64_SIG" \
'. + {"darwin-x86_64": {"signature": $sig, "url": $url}}')
fi
# Linux x86_64 AppImage
LINUX_X64_APPIMAGE=$(find_asset "(amd64|x86_64).AppImage$")
LINUX_X64_SIG=$(read_sig "*amd64.AppImage.sig" "*x86_64.AppImage.sig")
if [ -n "$LINUX_X64_APPIMAGE" ] && [ -n "$LINUX_X64_SIG" ]; then
PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${LINUX_X64_APPIMAGE}" --arg sig "$LINUX_X64_SIG" \
'. + {"linux-x86_64": {"signature": $sig, "url": $url}}')
fi
# Linux aarch64 AppImage
LINUX_ARM_APPIMAGE=$(find_asset "aarch64.AppImage$")
LINUX_ARM_SIG=$(read_sig "*aarch64.AppImage.sig")
if [ -n "$LINUX_ARM_APPIMAGE" ] && [ -n "$LINUX_ARM_SIG" ]; then
PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "${RELEASE_URL}/${LINUX_ARM_APPIMAGE}" --arg sig "$LINUX_ARM_SIG" \
'. + {"linux-aarch64": {"signature": $sig, "url": $url}}')
fi
# Assemble final latest.json
jq -n \
--arg version "$VERSION" \
--arg notes "$NOTES" \
--arg pub_date "$PUB_DATE" \
--argjson platforms "$PLATFORMS" \
'{version: $version, notes: $notes, pub_date: $pub_date, platforms: $platforms}' \
> latest.json
echo "Generated latest.json:"
cat latest.json
- name: Upload latest.json to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.version.outputs.tag }}
run: |
# Delete existing latest.json if present (from a previous run)
gh release delete-asset "$TAG" latest.json --repo "${{ github.repository }}" --yes 2>/dev/null || true
# Upload the new one
gh release upload "$TAG" latest.json --repo "${{ github.repository }}"