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
182 lines
7.0 KiB
YAML
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 }}"
|