diff --git a/.github/workflows/elixir-ci.yml b/.github/workflows/elixir-ci.yml index 343288a..f91f8f7 100644 --- a/.github/workflows/elixir-ci.yml +++ b/.github/workflows/elixir-ci.yml @@ -63,7 +63,15 @@ jobs: - name: Build Zig FFI working-directory: ${{ github.workspace }} - run: just build-ffi + run: | + # erlef/setup-beam installs OTP under the tool cache, not at + # build.zig's hardcoded /usr/lib/erlang default. Derive the real + # NIF header dir from the running Erlang so erl_nif.h is found. + ERL_INCLUDE="$(erl -noshell -eval 'io:format("~ts/usr/include", [code:root_dir()])' -s init stop)" + echo "erl_nif.h include dir: $ERL_INCLUDE" + test -f "$ERL_INCLUDE/erl_nif.h" || { echo "::error::erl_nif.h not found at $ERL_INCLUDE"; exit 1; } + ( cd ffi/zig && zig build -Doptimize=ReleaseFast -Derl-include="$ERL_INCLUDE" ) + cp ffi/zig/zig-out/lib/libburble_coprocessor.so server/priv/ 2>/dev/null || true - name: Run server tests working-directory: ${{ github.workspace }} diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index e805f00..0491431 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -31,13 +31,21 @@ jobs: elixir-version: '1.19.4' otp-version: '28.3' + # Hypatia lives in an external repo; clone/build can fail for reasons + # outside this repository's control. The scan is advisory (it does not + # gate merges and does not feed code-scanning), so external failures + # must degrade to an empty result rather than hard-fail every PR. - name: Clone Hypatia + id: clone + continue-on-error: true run: | if [ ! -d "$HOME/hypatia" ]; then - git clone https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia" + git clone --depth 1 https://github.com/hyperpolymath/hypatia.git "$HOME/hypatia" fi - name: Build Hypatia scanner (if needed) + id: build + continue-on-error: true run: | # Use the shell $HOME, not ${{ env.HOME }}: the latter reads the # workflow `env:` map (never set here) and resolves to an empty @@ -46,7 +54,7 @@ jobs: # scan (below) steps already use the shell "$HOME/hypatia"; this # makes the build step consistent with them. cd "$HOME/hypatia" - if [ ! -f hypatia-v2 ]; then + if [ ! -f hypatia-v2 ] && [ -d scanner ]; then echo "Building hypatia-v2 scanner..." cd scanner mix deps.get @@ -59,8 +67,17 @@ jobs: run: | echo "Scanning repository: ${{ github.repository }}" - # Run scanner - HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json + # Run scanner if it built; otherwise emit an empty (advisory) result. + if [ -x "$HOME/hypatia/hypatia-cli.sh" ]; then + HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json \ + || echo '[]' > hypatia-findings.json + else + echo "::warning::Hypatia scanner unavailable (external clone/build failed) — emitting empty findings. This scan is advisory and does not gate merges." + echo '[]' > hypatia-findings.json + fi + + # Guard against non-JSON / partial output corrupting downstream jq. + jq -e . hypatia-findings.json >/dev/null 2>&1 || echo '[]' > hypatia-findings.json # Count findings FINDING_COUNT=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0) diff --git a/.github/workflows/rhodibot.yml b/.github/workflows/rhodibot.yml index 3b3b01f..78f165b 100644 --- a/.github/workflows/rhodibot.yml +++ b/.github/workflows/rhodibot.yml @@ -186,7 +186,7 @@ jobs: git add -A git commit -m "fix(rhodibot): automated RSR compliance fixes - ${{ steps.fix.outputs.FIXES }} + $FIXES Co-Authored-By: rhodibot " @@ -195,20 +195,20 @@ jobs: BODY="## 🤖 Rhodibot — RSR Compliance Fixes ### Changes Made - ${{ steps.fix.outputs.FIXES }} + $FIXES " - if [ -n "${{ steps.fix.outputs.ISSUES }}" ]; then + if [ -n "$ISSUES" ]; then BODY="$BODY ### Issues Found (manual fix needed) - ${{ steps.fix.outputs.ISSUES }} + $ISSUES " fi - if [ -n "${{ steps.fix.outputs.DANGEROUS }}" ]; then + if [ -n "$DANGEROUS" ]; then BODY="$BODY ### ⚠️ Dangerous Patterns Detected - ${{ steps.fix.outputs.DANGEROUS }} + $DANGEROUS _These bypass formal verification. See \`proven\` repo for alternatives._ " @@ -221,16 +221,22 @@ jobs: --head "$BRANCH" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FIXES: ${{ steps.fix.outputs.FIXES }} + ISSUES: ${{ steps.fix.outputs.ISSUES }} + DANGEROUS: ${{ steps.fix.outputs.DANGEROUS }} - name: Report (no changes needed) if: steps.fix.outputs.CHANGED != 'true' + env: + ISSUES: ${{ steps.fix.outputs.ISSUES }} + DANGEROUS: ${{ steps.fix.outputs.DANGEROUS }} run: | echo "✅ Repository is RSR-compliant. No fixes needed." - if [ -n "${{ steps.fix.outputs.ISSUES }}" ]; then + if [ -n "$ISSUES" ]; then echo "⚠️ Issues found (manual fix needed):" - echo -e "${{ steps.fix.outputs.ISSUES }}" + echo -e "$ISSUES" fi - if [ -n "${{ steps.fix.outputs.DANGEROUS }}" ]; then + if [ -n "$DANGEROUS" ]; then echo "⚠️ Dangerous patterns:" - echo -e "${{ steps.fix.outputs.DANGEROUS }}" + echo -e "$DANGEROUS" fi diff --git a/client/lib/src/BurbleClient.res b/client/lib/src/BurbleClient.res index be9447c..005d846 100644 --- a/client/lib/src/BurbleClient.res +++ b/client/lib/src/BurbleClient.res @@ -231,9 +231,17 @@ let disconnect = (client: client): unit => { // Auth // --------------------------------------------------------------------------- +/// Cryptographically strong guest-id suffix (8 hex chars). +/// Math.random() is not a CSPRNG; guest ids are identity tokens. +let secureIdSuffix: unit => string = %raw(`function () { + const b = new Uint8Array(4); + crypto.getRandomValues(b); + return Array.from(b, x => x.toString(16).padStart(2, "0")).join(""); +}`) + /// Authenticate as a guest. let guestLogin = (client: client, displayName: string): unit => { - let guestId = "guest_" ++ Float.toString(Math.random())->String.slice(~start=2, ~end=10) + let guestId = "guest_" ++ secureIdSuffix() client.auth = Guest({id: guestId, displayName}) client.config.onAuthChange(client.auth) } diff --git a/client/lib/src/BurbleClient.res.mjs b/client/lib/src/BurbleClient.res.mjs index aa4919a..6baf019 100644 --- a/client/lib/src/BurbleClient.res.mjs +++ b/client/lib/src/BurbleClient.res.mjs @@ -78,8 +78,14 @@ function disconnect(client) { client.config.onConnectionChange("Disconnected"); } +function secureIdSuffix() { + const b = new Uint8Array(4); + crypto.getRandomValues(b); + return Array.from(b, x => x.toString(16).padStart(2, "0")).join(""); +} + function guestLogin(client, displayName) { - let guestId = "guest_" + Math.random().toString().slice(2, 10); + let guestId = "guest_" + secureIdSuffix(); client.auth = { TAG: "Guest", id: guestId, diff --git a/client/web/src/room.js b/client/web/src/room.js index fe03285..7be1f20 100644 --- a/client/web/src/room.js +++ b/client/web/src/room.js @@ -30,11 +30,23 @@ export function generateRoomName() { /** * Pick a random word from the word list - * + * + * Uses the Web Crypto CSPRNG rather than Math.random(): room names gate + * access to a private voice room, so they must not be predictable. Uniform + * selection via rejection sampling to avoid modulo bias. + * * @returns {string} Random word */ function pickRandomWord() { - return WORD_LIST[Math.floor(Math.random() * WORD_LIST.length)]; + const range = WORD_LIST.length; + const limit = Math.floor(0x100000000 / range) * range; + const buf = new Uint32Array(1); + let n; + do { + crypto.getRandomValues(buf); + n = buf[0]; + } while (n >= limit); + return WORD_LIST[n % range]; } /**