From 6328a2cf64e0894545fe6093b5e3401fcfe23f43 Mon Sep 17 00:00:00 2001 From: awais786 Date: Sat, 16 May 2026 15:15:56 +0500 Subject: [PATCH 1/7] chore(ci): add fork-side SSO audit script + workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a per-fork deterministic audit that catches regressions of the cross-app SSO contract in Pressingly/twenty BEFORE they reach foss-server-bundle-devstack. Mirrors the pattern landed in Pressingly/plane#32. Rows covered (per awais786/sso-rules-moneta:openspec/specs/proxy-auth- middleware/spec.md): Row 14 — SPA logout (packages/twenty-front/src/modules/auth/hooks/ useAuth.ts) MUST NOT call /oauth2/sign_out. Per-app Logout is navigation-only via buildPortalUrl. Row 20 — JwtAuthGuard (packages/twenty-server/src/engine/guards/ jwt-auth.guard.ts) MUST call response.clearCookie('tokenPair', ...) on identity mismatch, gated on AUTH_TYPE === 'SSO'. SECURITY-CRITICAL. Row 21 — Email-shape detection MUST NOT use polynomial-backtracking regex. Twenty uses indexOf-based detection in normalizeProxyEmail (post-PR #8); this row is a regression guard. Workflow (.github/workflows/sso-audit.yml): - Runs on PRs touching auth code or the audit itself - Pushes to foss-main, weekly cron, manual dispatch - Posts sticky PR comment via marocchino/sticky-pull-request-comment@v2 - Skips comment on fork PRs (read-only token), continue-on-error guard - Exits 1 on security-critical violations → merge blocked Local dry-run on foss-main: row 20 ❌ (jwt-auth.guard.ts lacks the clearCookie call). Exit 1. Confirms the gate works. Will go green once Pressingly/twenty#8 (or the equivalent fix) merges. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sso-audit.yml | 72 +++++++++++ scripts/sso-audit.sh | 204 ++++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 .github/workflows/sso-audit.yml create mode 100755 scripts/sso-audit.sh diff --git a/.github/workflows/sso-audit.yml b/.github/workflows/sso-audit.yml new file mode 100644 index 0000000000000..d76aad496a8b1 --- /dev/null +++ b/.github/workflows/sso-audit.yml @@ -0,0 +1,72 @@ +name: SSO fork audit + +# Runs scripts/sso-audit.sh against this fork's auth code to verify +# satisfaction of the cross-app SSO contract at +# awais786/sso-rules-moneta:openspec/specs/proxy-auth-middleware/spec.md. +# +# Covers fork-side rows 14, 20, 21 from SKILL.md §5. Exits 1 on security- +# critical violations (row 20 — session-identity reconciliation) so the +# merge gate blocks the stale-session leak class of bug. +# +# When upstream sso-rules-moneta adds a new fork-side row, vendor the +# updated check into scripts/sso-audit.sh and re-run this workflow. + +on: + pull_request: + paths: + - 'packages/twenty-server/src/engine/guards/**' + - 'packages/twenty-server/src/engine/core-modules/auth/**' + - 'packages/twenty-front/src/modules/auth/**' + - 'scripts/sso-audit.sh' + - '.github/workflows/sso-audit.yml' + push: + branches: [foss-main] + schedule: + # 09:00 UTC every Monday — pre-week sanity check + - cron: '0 9 * * 1' + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + sso-fork-audit: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run fork audit + id: audit + run: | + set -o pipefail + if bash scripts/sso-audit.sh | tee audit-output.md; then + echo "audit_exit=0" >> "$GITHUB_OUTPUT" + else + echo "audit_exit=$?" >> "$GITHUB_OUTPUT" + fi + + - name: Publish to job summary + if: always() + run: cat audit-output.md >> "$GITHUB_STEP_SUMMARY" + + - name: Post sticky PR comment + # Skip on fork PRs — read-only token would fail the action even + # when the audit passes. `continue-on-error` is a defensive belt + # for any other comment-posting failure (rate limit, API blip). + if: | + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + always() + continue-on-error: true + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: sso-fork-audit + path: audit-output.md + + - name: Fail on security-critical violations + if: steps.audit.outputs.audit_exit != '0' + run: | + echo "::error::Security-critical SSO contract violation in fork audit. See table for the failing row and fix." + exit 1 diff --git a/scripts/sso-audit.sh b/scripts/sso-audit.sh new file mode 100755 index 0000000000000..286f76b010088 --- /dev/null +++ b/scripts/sso-audit.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# +# sso-audit.sh — Twenty-side fork audit against the cross-app SSO contract. +# +# ============================================================================ +# Covers the fork-side rows of awais786/sso-rules-moneta:openspec/specs/ +# proxy-auth-middleware/spec.md that a deterministic bash check can verify +# against Twenty's source tree. Catches regressions BEFORE they reach +# foss-server-bundle-devstack, where the same rows currently emit `?` (no +# fork code on disk in the bundle CI). +# +# Together with the bundle-side audit and the per-fork audits in Plane / +# Outline / Penpot / SurfSense, the cross-app contract is now ~14 of 21 +# rows deterministic without any LLM invocation. +# +# Exit codes: +# 0 — all rows ✅ or n/a / informational +# 1 — at least one SECURITY-CRITICAL row failed (today: row 20 session- +# identity reconciliation). These violations re-open the cross-user +# identity-leak class of bug. +# +# Rows covered: +# Row 14 — logout shape: SPA logout file (packages/twenty-front/src/ +# modules/auth/hooks/useAuth.ts) MUST NOT call /oauth2/sign_out. +# (The portal-host redirect target uses buildPortalUrl — verified +# elsewhere; this row only enforces no /oauth2/sign_out literal.) +# Row 20 — session-identity reconciliation (Rule 2 mismatch flush): +# JwtAuthGuard MUST call response.clearCookie('tokenPair', ...) +# on identity mismatch when AUTH_TYPE === 'SSO'. Without this, +# the stale-session-on-user-switch leak returns. SECURITY-CRITICAL. +# Row 21 — email-shape detection MUST NOT use polynomial-backtracking +# regex. Twenty uses indexOf-based detection in +# normalizeProxyEmail today; this row fires only if a regex is +# reintroduced. +# +# Source of truth: awais786/sso-rules-moneta:openspec/specs/proxy-auth- +# middleware/spec.md. When upstream adds a new fork-side row, vendor the +# updated check here. +# ============================================================================ + +set -euo pipefail + +JWT_GUARD="packages/twenty-server/src/engine/guards/jwt-auth.guard.ts" +USE_AUTH="packages/twenty-front/src/modules/auth/hooks/useAuth.ts" + +declare -a ROW_STATUS=() +declare -a ROW_TITLES=( + "logout shape: SPA logout does not call /oauth2/sign_out" + "session-identity reconciliation present (Rule 2 mismatch flush)" + "email-shape detection uses indexOf, not polynomial regex" +) +declare -a ROW_NOTES=() +declare -a ROW_NUMBERS=(14 20 21) + +# Security-critical row indices (0-indexed into ROW_NUMBERS above). +SECURITY_CRITICAL=(1) # index 1 → row 20 +SECURITY_CRITICAL_FAILS=0 + +record() { + local idx=$1 status=$2 note=$3 + ROW_STATUS[$idx]="$status" + ROW_NOTES[$idx]="$note" + if [[ "$status" == "❌" ]]; then + for c in "${SECURITY_CRITICAL[@]}"; do + if [[ "$c" -eq "$idx" ]]; then + SECURITY_CRITICAL_FAILS=$((SECURITY_CRITICAL_FAILS + 1)) + return + fi + done + fi +} + +# ============================================================================ +# Row 14 (idx 0): logout shape — narrow check +# +# SPA logout MUST NOT contain `/oauth2/sign_out` literal. Twenty's logout +# uses `buildPortalUrl` to compute the redirect target; this audit only +# verifies the SPA doesn't try to clear the upstream proxy cookie itself. +# ============================================================================ +check_row_14() { + if [[ ! -f "$USE_AUTH" ]]; then + record 0 "?" "$USE_AUTH not found — skipping" + return + fi + + if grep -qE '/oauth2/sign_?out' "$USE_AUTH"; then + local line + line=$(grep -nE '/oauth2/sign_?out' "$USE_AUTH" | head -1) + record 0 "❌" "Logout calls \`/oauth2/sign_out\` at $USE_AUTH:$line — per logout-flow spec, per-app Logout is navigation-only. Drop the call; portal 'Logout all' handles oauth2-proxy clearing." + return + fi + + record 0 "✅" "$USE_AUTH does not invoke \`/oauth2/sign_out\` (this row verifies only that the SPA doesn't try to clear the upstream proxy cookie itself; that's the portal's job)" +} + +# ============================================================================ +# Row 20 (idx 1): session-identity reconciliation +# +# JwtAuthGuard MUST clear the tokenPair cookie on identity mismatch: +# response.clearCookie('tokenPair', { path: '/' }) +# +# AND it MUST gate the mismatch check on AUTH_TYPE === 'SSO' so non-SSO +# deployments are unaffected. +# +# The presence of both `clearCookie('tokenPair'` AND `AUTH_TYPE` references +# in the same file is the deterministic signal. The test suite at +# packages/twenty-server/src/engine/guards/__tests__/jwt-auth.guard.spec.ts +# pins the exact behaviour. +# +# SECURITY-CRITICAL: without this, the stale-session leak returns. +# ============================================================================ +check_row_20() { + if [[ ! -f "$JWT_GUARD" ]]; then + record 1 "?" "$JWT_GUARD not found — skipping" + return + fi + + local has_clear_cookie + has_clear_cookie=$(grep -cE "clearCookie\(\s*['\"]tokenPair['\"]" "$JWT_GUARD" || true) + + local has_auth_type + has_auth_type=$(grep -cE "AUTH_TYPE" "$JWT_GUARD" || true) + + if [[ "$has_clear_cookie" -gt 0 && "$has_auth_type" -gt 0 ]]; then + record 1 "✅" "$JWT_GUARD contains \`clearCookie('tokenPair'\` gated on AUTH_TYPE — Rule 2 mismatch flush in place" + return + fi + + if [[ "$has_clear_cookie" -eq 0 ]]; then + record 1 "❌" "$JWT_GUARD does NOT call \`response.clearCookie('tokenPair', ...)\`. The cross-app spec (proxy-auth-middleware Rule 2) requires the guard to clear the tokenPair cookie when oauth2-proxy asserts a different identity than the JWT user. Without it, the stale-session-on-user-switch leak returns. Fix: add a comparison of \`X-Auth-Request-Email\` vs \`data.user.email\` after \`validateTokenByRequest\`, and on mismatch call \`response.clearCookie('tokenPair', { path: '/' })\` then return false. Gate the check on \`AUTH_TYPE === 'SSO'\`." + return + fi + + record 1 "❌" "$JWT_GUARD calls \`clearCookie('tokenPair'\` but does NOT reference AUTH_TYPE. The mismatch check MUST be gated on \`AUTH_TYPE === 'SSO'\` so non-SSO deployments are unaffected. Fix: wrap the clearCookie call inside an \`if (twentyConfigService.get('AUTH_TYPE') === 'SSO' && ...)\` condition." +} + +# ============================================================================ +# Row 21 (idx 2): polynomial-regex avoidance in email-shape detection +# +# Twenty uses indexOf-based detection in normalizeProxyEmail (added in +# Pressingly/twenty#8). This is a regression guard — fires only if a +# polynomial-backtracking regex like /^[^\s@]+@[^\s@]+\.[^\s@]+$/ is +# reintroduced. CodeQL's `js/polynomial-redos` flags it on adversarial input. +# ============================================================================ +check_row_21() { + if [[ ! -f "$JWT_GUARD" ]]; then + record 2 "?" "$JWT_GUARD not found — skipping" + return + fi + + # Look for the canonical polynomial-backtracking shape used in JS regex + # literals: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ and variations. + local hits + hits=$(grep -nE '\[\^[\\\\]s@\]\+@\[\^[\\\\]s@\]\+\\\.\[\^[\\\\]s@\]\+' "$JWT_GUARD" 2>/dev/null || true) + + if [[ -n "$hits" ]]; then + record 2 "❌" "Polynomial-backtracking email-shape regex detected in $JWT_GUARD: $hits. Rewrite to indexOf-based check per openspec proxy-auth-middleware §'email-shape detection SHALL avoid polynomial-backtracking regex'. Reference: \`normalizeProxyEmail\` in this file." + return + fi + + record 2 "✅" "No polynomial-backtracking email-shape regex in $JWT_GUARD; using indexOf-based detection" +} + +# ============================================================================ +# Run checks +# ============================================================================ +check_row_14 +check_row_20 +check_row_21 + +# ============================================================================ +# Print table +# ============================================================================ +echo "## Twenty SSO Fork Audit" +echo +echo "Cross-app contract: https://github.com/awais786/sso-rules-moneta/blob/main/openspec/specs/proxy-auth-middleware/spec.md" +echo "Row numbers match the 21-row table at https://github.com/awais786/sso-rules-moneta/blob/main/skills/app-rules/SKILL.md#5-report" +echo +echo "| Row | Invariant | Status | Notes |" +echo "|-----|-----------|--------|-------|" +for i in "${!ROW_TITLES[@]}"; do + printf "| %d | %s | %s | %s |\n" \ + "${ROW_NUMBERS[$i]}" "${ROW_TITLES[$i]}" "${ROW_STATUS[$i]:-?}" "${ROW_NOTES[$i]:-}" +done +echo + +# ============================================================================ +# Summary + exit code +# ============================================================================ +TOTAL_FAILS=0 +for s in "${ROW_STATUS[@]}"; do + [[ "$s" == "❌" ]] && TOTAL_FAILS=$((TOTAL_FAILS + 1)) +done + +if [[ "$TOTAL_FAILS" -eq 0 ]]; then + echo "**All fork-side invariants hold.**" + exit 0 +fi + +echo "**$TOTAL_FAILS violations.** Security-critical (row 20): $SECURITY_CRITICAL_FAILS." +if [[ "$SECURITY_CRITICAL_FAILS" -gt 0 ]]; then + exit 1 +fi +exit 0 From dd2be34e19c2bb29d935915c67fa39561f61f54b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 11:40:45 +0000 Subject: [PATCH 2/7] fix(ci): tighten row 20 audit matching and scope workflow permissions Agent-Logs-Url: https://github.com/Pressingly/twenty/sessions/c800f651-f223-422d-be1b-32d95363495e Co-authored-by: awais786 <445320+awais786@users.noreply.github.com> --- .github/workflows/sso-audit.yml | 44 +++++++++++++++--------- scripts/sso-audit.sh | 61 +++++++++++++++++++++++++-------- 2 files changed, 73 insertions(+), 32 deletions(-) diff --git a/.github/workflows/sso-audit.yml b/.github/workflows/sso-audit.yml index d76aad496a8b1..896768c9ca220 100644 --- a/.github/workflows/sso-audit.yml +++ b/.github/workflows/sso-audit.yml @@ -19,16 +19,10 @@ on: - 'packages/twenty-front/src/modules/auth/**' - 'scripts/sso-audit.sh' - '.github/workflows/sso-audit.yml' - push: - branches: [foss-main] - schedule: - # 09:00 UTC every Monday — pre-week sanity check - - cron: '0 9 * * 1' workflow_dispatch: permissions: contents: read - pull-requests: write jobs: sso-fork-audit: @@ -51,18 +45,11 @@ jobs: if: always() run: cat audit-output.md >> "$GITHUB_STEP_SUMMARY" - - name: Post sticky PR comment - # Skip on fork PRs — read-only token would fail the action even - # when the audit passes. `continue-on-error` is a defensive belt - # for any other comment-posting failure (rate limit, API blip). - if: | - github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository && - always() - continue-on-error: true - uses: marocchino/sticky-pull-request-comment@v2 + - name: Upload audit output + if: always() + uses: actions/upload-artifact@v4 with: - header: sso-fork-audit + name: sso-fork-audit-output path: audit-output.md - name: Fail on security-critical violations @@ -70,3 +57,26 @@ jobs: run: | echo "::error::Security-critical SSO contract violation in fork audit. See table for the failing row and fix." exit 1 + + sso-fork-audit-comment: + if: | + always() && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + needs: sso-fork-audit + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Download audit output + uses: actions/download-artifact@v4 + with: + name: sso-fork-audit-output + + - name: Post sticky PR comment + continue-on-error: true + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: sso-fork-audit + path: sso-fork-audit-output/audit-output.md diff --git a/scripts/sso-audit.sh b/scripts/sso-audit.sh index 286f76b010088..f795799a4988a 100755 --- a/scripts/sso-audit.sh +++ b/scripts/sso-audit.sh @@ -102,10 +102,12 @@ check_row_14() { # AND it MUST gate the mismatch check on AUTH_TYPE === 'SSO' so non-SSO # deployments are unaffected. # -# The presence of both `clearCookie('tokenPair'` AND `AUTH_TYPE` references -# in the same file is the deterministic signal. The test suite at -# packages/twenty-server/src/engine/guards/__tests__/jwt-auth.guard.spec.ts -# pins the exact behaviour. +# Deterministic signal: +# - SSO gate: get('AUTH_TYPE') === 'SSO' +# - mismatch operands: X-Auth-Request-Email + data.user.email +# - token flush: clearCookie('tokenPair', ...) +# All must exist in the same nearby block (within ±25 lines), so unrelated +# AUTH_TYPE/clearCookie references elsewhere in the file cannot false-pass. # # SECURITY-CRITICAL: without this, the stale-session leak returns. # ============================================================================ @@ -115,23 +117,52 @@ check_row_20() { return fi - local has_clear_cookie - has_clear_cookie=$(grep -cE "clearCookie\(\s*['\"]tokenPair['\"]" "$JWT_GUARD" || true) - - local has_auth_type - has_auth_type=$(grep -cE "AUTH_TYPE" "$JWT_GUARD" || true) - - if [[ "$has_clear_cookie" -gt 0 && "$has_auth_type" -gt 0 ]]; then - record 1 "✅" "$JWT_GUARD contains \`clearCookie('tokenPair'\` gated on AUTH_TYPE — Rule 2 mismatch flush in place" - return + local clear_cookie_lines + clear_cookie_lines=$(grep -nE "clearCookie\(\s*['\"]tokenPair['\"]" "$JWT_GUARD" || true) + + local auth_type_lines + auth_type_lines=$(grep -nE "get\(\s*['\"]AUTH_TYPE['\"]\s*\)\s*===\s*['\"]SSO['\"]" "$JWT_GUARD" || true) + + local mismatch_header_lines + mismatch_header_lines=$(grep -nE "X-Auth-Request-Email" "$JWT_GUARD" || true) + + local mismatch_user_lines + mismatch_user_lines=$(grep -nE "data\.user\.email" "$JWT_GUARD" || true) + + if [[ -n "$clear_cookie_lines" && -n "$auth_type_lines" && -n "$mismatch_header_lines" && -n "$mismatch_user_lines" ]]; then + local nearby_match=0 + local clear_line auth_line header_line user_line + + while IFS=: read -r clear_line _; do + [[ -z "$clear_line" ]] && continue + while IFS=: read -r auth_line _; do + [[ -z "$auth_line" ]] && continue + (( clear_line - auth_line > 25 || auth_line - clear_line > 25 )) && continue + while IFS=: read -r header_line _; do + [[ -z "$header_line" ]] && continue + (( clear_line - header_line > 25 || header_line - clear_line > 25 )) && continue + while IFS=: read -r user_line _; do + [[ -z "$user_line" ]] && continue + (( clear_line - user_line > 25 || user_line - clear_line > 25 )) && continue + nearby_match=1 + break 4 + done <<< "$mismatch_user_lines" + done <<< "$mismatch_header_lines" + done <<< "$auth_type_lines" + done <<< "$clear_cookie_lines" + + if [[ "$nearby_match" -eq 1 ]]; then + record 1 "✅" "$JWT_GUARD contains nearby SSO gate + identity-mismatch comparison + \`clearCookie('tokenPair'\` (within ±25 lines) — Rule 2 mismatch flush in place" + return + fi fi - if [[ "$has_clear_cookie" -eq 0 ]]; then + if [[ -z "$clear_cookie_lines" ]]; then record 1 "❌" "$JWT_GUARD does NOT call \`response.clearCookie('tokenPair', ...)\`. The cross-app spec (proxy-auth-middleware Rule 2) requires the guard to clear the tokenPair cookie when oauth2-proxy asserts a different identity than the JWT user. Without it, the stale-session-on-user-switch leak returns. Fix: add a comparison of \`X-Auth-Request-Email\` vs \`data.user.email\` after \`validateTokenByRequest\`, and on mismatch call \`response.clearCookie('tokenPair', { path: '/' })\` then return false. Gate the check on \`AUTH_TYPE === 'SSO'\`." return fi - record 1 "❌" "$JWT_GUARD calls \`clearCookie('tokenPair'\` but does NOT reference AUTH_TYPE. The mismatch check MUST be gated on \`AUTH_TYPE === 'SSO'\` so non-SSO deployments are unaffected. Fix: wrap the clearCookie call inside an \`if (twentyConfigService.get('AUTH_TYPE') === 'SSO' && ...)\` condition." + record 1 "❌" "$JWT_GUARD has \`clearCookie('tokenPair'\` but missing a nearby full Rule 2 block: \`get('AUTH_TYPE') === 'SSO'\` + \`X-Auth-Request-Email\` vs \`data.user.email\` comparison in the same conditional path. Keep these checks colocated to avoid false passes from unrelated references." } # ============================================================================ From 397b288d11379a68a4ee116b9f4b612831c43855 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 11:41:43 +0000 Subject: [PATCH 3/7] fix(ci): address review follow-ups for audit matching and artifact path Agent-Logs-Url: https://github.com/Pressingly/twenty/sessions/c800f651-f223-422d-be1b-32d95363495e Co-authored-by: awais786 <445320+awais786@users.noreply.github.com> --- .github/workflows/sso-audit.yml | 3 +- scripts/sso-audit.sh | 49 ++++++++++++++++++--------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/.github/workflows/sso-audit.yml b/.github/workflows/sso-audit.yml index 896768c9ca220..691eb486d3580 100644 --- a/.github/workflows/sso-audit.yml +++ b/.github/workflows/sso-audit.yml @@ -73,10 +73,11 @@ jobs: uses: actions/download-artifact@v4 with: name: sso-fork-audit-output + path: . - name: Post sticky PR comment continue-on-error: true uses: marocchino/sticky-pull-request-comment@v2 with: header: sso-fork-audit - path: sso-fork-audit-output/audit-output.md + path: audit-output.md diff --git a/scripts/sso-audit.sh b/scripts/sso-audit.sh index f795799a4988a..cef02bc21c65f 100755 --- a/scripts/sso-audit.sh +++ b/scripts/sso-audit.sh @@ -130,28 +130,33 @@ check_row_20() { mismatch_user_lines=$(grep -nE "data\.user\.email" "$JWT_GUARD" || true) if [[ -n "$clear_cookie_lines" && -n "$auth_type_lines" && -n "$mismatch_header_lines" && -n "$mismatch_user_lines" ]]; then - local nearby_match=0 - local clear_line auth_line header_line user_line - - while IFS=: read -r clear_line _; do - [[ -z "$clear_line" ]] && continue - while IFS=: read -r auth_line _; do - [[ -z "$auth_line" ]] && continue - (( clear_line - auth_line > 25 || auth_line - clear_line > 25 )) && continue - while IFS=: read -r header_line _; do - [[ -z "$header_line" ]] && continue - (( clear_line - header_line > 25 || header_line - clear_line > 25 )) && continue - while IFS=: read -r user_line _; do - [[ -z "$user_line" ]] && continue - (( clear_line - user_line > 25 || user_line - clear_line > 25 )) && continue - nearby_match=1 - break 4 - done <<< "$mismatch_user_lines" - done <<< "$mismatch_header_lines" - done <<< "$auth_type_lines" - done <<< "$clear_cookie_lines" - - if [[ "$nearby_match" -eq 1 ]]; then + local nearby_match + nearby_match=$( + awk ' + /clearCookie\(\s*['\''"]tokenPair['\''"]/ { clear[NR]=1 } + /get\(\s*['\''"]AUTH_TYPE['\''"]\s*\)\s*===\s*['\''"]SSO['\''"]/ { auth[NR]=1 } + /X-Auth-Request-Email/ { header[NR]=1 } + /data\.user\.email/ { user[NR]=1 } + END { + for (line = 1; line <= NR; line++) { + hasClear = hasAuth = hasHeader = hasUser = 0 + for (i = line - 25; i <= line + 25; i++) { + if (clear[i]) hasClear = 1 + if (auth[i]) hasAuth = 1 + if (header[i]) hasHeader = 1 + if (user[i]) hasUser = 1 + } + if (hasClear && hasAuth && hasHeader && hasUser) { + print 1 + exit + } + } + print 0 + } + ' "$JWT_GUARD" + ) + + if [[ "$nearby_match" == "1" ]]; then record 1 "✅" "$JWT_GUARD contains nearby SSO gate + identity-mismatch comparison + \`clearCookie('tokenPair'\` (within ±25 lines) — Rule 2 mismatch flush in place" return fi From 1ae5de3e4af555ae44be61ecf0266592a40f85fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 11:42:27 +0000 Subject: [PATCH 4/7] fix(ci): dedupe row20 patterns and use explicit audit artifact directory Agent-Logs-Url: https://github.com/Pressingly/twenty/sessions/c800f651-f223-422d-be1b-32d95363495e Co-authored-by: awais786 <445320+awais786@users.noreply.github.com> --- .github/workflows/sso-audit.yml | 4 ++-- scripts/sso-audit.sh | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/.github/workflows/sso-audit.yml b/.github/workflows/sso-audit.yml index 691eb486d3580..4a4cb6118d67c 100644 --- a/.github/workflows/sso-audit.yml +++ b/.github/workflows/sso-audit.yml @@ -73,11 +73,11 @@ jobs: uses: actions/download-artifact@v4 with: name: sso-fork-audit-output - path: . + path: audit-results - name: Post sticky PR comment continue-on-error: true uses: marocchino/sticky-pull-request-comment@v2 with: header: sso-fork-audit - path: audit-output.md + path: audit-results/audit-output.md diff --git a/scripts/sso-audit.sh b/scripts/sso-audit.sh index cef02bc21c65f..08de43bc95208 100755 --- a/scripts/sso-audit.sh +++ b/scripts/sso-audit.sh @@ -42,6 +42,10 @@ set -euo pipefail JWT_GUARD="packages/twenty-server/src/engine/guards/jwt-auth.guard.ts" USE_AUTH="packages/twenty-front/src/modules/auth/hooks/useAuth.ts" +CLEAR_COOKIE_PATTERN="clearCookie\\(\\s*['\\\"]tokenPair['\\\"]" +AUTH_TYPE_SSO_PATTERN="get\\(\\s*['\\\"]AUTH_TYPE['\\\"]\\s*\\)\\s*===\\s*['\\\"]SSO['\\\"]" +MISMATCH_HEADER_PATTERN="X-Auth-Request-Email" +MISMATCH_USER_PATTERN="data\\.user\\.email" declare -a ROW_STATUS=() declare -a ROW_TITLES=( @@ -118,25 +122,29 @@ check_row_20() { fi local clear_cookie_lines - clear_cookie_lines=$(grep -nE "clearCookie\(\s*['\"]tokenPair['\"]" "$JWT_GUARD" || true) + clear_cookie_lines=$(grep -nE "$CLEAR_COOKIE_PATTERN" "$JWT_GUARD" || true) local auth_type_lines - auth_type_lines=$(grep -nE "get\(\s*['\"]AUTH_TYPE['\"]\s*\)\s*===\s*['\"]SSO['\"]" "$JWT_GUARD" || true) + auth_type_lines=$(grep -nE "$AUTH_TYPE_SSO_PATTERN" "$JWT_GUARD" || true) local mismatch_header_lines - mismatch_header_lines=$(grep -nE "X-Auth-Request-Email" "$JWT_GUARD" || true) + mismatch_header_lines=$(grep -nE "$MISMATCH_HEADER_PATTERN" "$JWT_GUARD" || true) local mismatch_user_lines - mismatch_user_lines=$(grep -nE "data\.user\.email" "$JWT_GUARD" || true) + mismatch_user_lines=$(grep -nE "$MISMATCH_USER_PATTERN" "$JWT_GUARD" || true) if [[ -n "$clear_cookie_lines" && -n "$auth_type_lines" && -n "$mismatch_header_lines" && -n "$mismatch_user_lines" ]]; then local nearby_match nearby_match=$( - awk ' - /clearCookie\(\s*['\''"]tokenPair['\''"]/ { clear[NR]=1 } - /get\(\s*['\''"]AUTH_TYPE['\''"]\s*\)\s*===\s*['\''"]SSO['\''"]/ { auth[NR]=1 } - /X-Auth-Request-Email/ { header[NR]=1 } - /data\.user\.email/ { user[NR]=1 } + awk \ + -v clearPattern="$CLEAR_COOKIE_PATTERN" \ + -v authPattern="$AUTH_TYPE_SSO_PATTERN" \ + -v headerPattern="$MISMATCH_HEADER_PATTERN" \ + -v userPattern="$MISMATCH_USER_PATTERN" ' + $0 ~ clearPattern { clear[NR]=1 } + $0 ~ authPattern { auth[NR]=1 } + $0 ~ headerPattern { header[NR]=1 } + $0 ~ userPattern { user[NR]=1 } END { for (line = 1; line <= NR; line++) { hasClear = hasAuth = hasHeader = hasUser = 0 From c8742457b0e361624a3f96cfb92b5d9f65fea8d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 11:43:11 +0000 Subject: [PATCH 5/7] fix(ci): use proximity constant and harden artifact upload step Agent-Logs-Url: https://github.com/Pressingly/twenty/sessions/c800f651-f223-422d-be1b-32d95363495e Co-authored-by: awais786 <445320+awais786@users.noreply.github.com> --- .github/workflows/sso-audit.yml | 1 + scripts/sso-audit.sh | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sso-audit.yml b/.github/workflows/sso-audit.yml index 4a4cb6118d67c..9d54f8bc8219f 100644 --- a/.github/workflows/sso-audit.yml +++ b/.github/workflows/sso-audit.yml @@ -51,6 +51,7 @@ jobs: with: name: sso-fork-audit-output path: audit-output.md + if-no-files-found: warn - name: Fail on security-critical violations if: steps.audit.outputs.audit_exit != '0' diff --git a/scripts/sso-audit.sh b/scripts/sso-audit.sh index 08de43bc95208..da3a475e3b89d 100755 --- a/scripts/sso-audit.sh +++ b/scripts/sso-audit.sh @@ -46,6 +46,7 @@ CLEAR_COOKIE_PATTERN="clearCookie\\(\\s*['\\\"]tokenPair['\\\"]" AUTH_TYPE_SSO_PATTERN="get\\(\\s*['\\\"]AUTH_TYPE['\\\"]\\s*\\)\\s*===\\s*['\\\"]SSO['\\\"]" MISMATCH_HEADER_PATTERN="X-Auth-Request-Email" MISMATCH_USER_PATTERN="data\\.user\\.email" +PROXIMITY_WINDOW=25 declare -a ROW_STATUS=() declare -a ROW_TITLES=( @@ -110,7 +111,7 @@ check_row_14() { # - SSO gate: get('AUTH_TYPE') === 'SSO' # - mismatch operands: X-Auth-Request-Email + data.user.email # - token flush: clearCookie('tokenPair', ...) -# All must exist in the same nearby block (within ±25 lines), so unrelated +# All must exist in the same nearby block (within ±PROXIMITY_WINDOW lines), so unrelated # AUTH_TYPE/clearCookie references elsewhere in the file cannot false-pass. # # SECURITY-CRITICAL: without this, the stale-session leak returns. @@ -140,7 +141,8 @@ check_row_20() { -v clearPattern="$CLEAR_COOKIE_PATTERN" \ -v authPattern="$AUTH_TYPE_SSO_PATTERN" \ -v headerPattern="$MISMATCH_HEADER_PATTERN" \ - -v userPattern="$MISMATCH_USER_PATTERN" ' + -v userPattern="$MISMATCH_USER_PATTERN" \ + -v window="$PROXIMITY_WINDOW" ' $0 ~ clearPattern { clear[NR]=1 } $0 ~ authPattern { auth[NR]=1 } $0 ~ headerPattern { header[NR]=1 } @@ -148,7 +150,7 @@ check_row_20() { END { for (line = 1; line <= NR; line++) { hasClear = hasAuth = hasHeader = hasUser = 0 - for (i = line - 25; i <= line + 25; i++) { + for (i = line - window; i <= line + window; i++) { if (clear[i]) hasClear = 1 if (auth[i]) hasAuth = 1 if (header[i]) hasHeader = 1 @@ -165,7 +167,7 @@ check_row_20() { ) if [[ "$nearby_match" == "1" ]]; then - record 1 "✅" "$JWT_GUARD contains nearby SSO gate + identity-mismatch comparison + \`clearCookie('tokenPair'\` (within ±25 lines) — Rule 2 mismatch flush in place" + record 1 "✅" "$JWT_GUARD contains nearby SSO gate + identity-mismatch comparison + \`clearCookie('tokenPair'\` (within ±$PROXIMITY_WINDOW lines) — Rule 2 mismatch flush in place" return fi fi From 51e5e1dcc504b17fbb68948c595ad974bc291e4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 11:43:54 +0000 Subject: [PATCH 6/7] chore(ci): document regex intent and fork-pr comment job scope Agent-Logs-Url: https://github.com/Pressingly/twenty/sessions/c800f651-f223-422d-be1b-32d95363495e Co-authored-by: awais786 <445320+awais786@users.noreply.github.com> --- .github/workflows/sso-audit.yml | 2 ++ scripts/sso-audit.sh | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/sso-audit.yml b/.github/workflows/sso-audit.yml index 9d54f8bc8219f..f29c9a1dc2be3 100644 --- a/.github/workflows/sso-audit.yml +++ b/.github/workflows/sso-audit.yml @@ -60,6 +60,8 @@ jobs: exit 1 sso-fork-audit-comment: + # Run comment updates only on same-repo PRs. Fork PR tokens are read-only + # and cannot write PR comments. if: | always() && github.event_name == 'pull_request' && diff --git a/scripts/sso-audit.sh b/scripts/sso-audit.sh index da3a475e3b89d..cae2a53bbd768 100755 --- a/scripts/sso-audit.sh +++ b/scripts/sso-audit.sh @@ -42,10 +42,14 @@ set -euo pipefail JWT_GUARD="packages/twenty-server/src/engine/guards/jwt-auth.guard.ts" USE_AUTH="packages/twenty-front/src/modules/auth/hooks/useAuth.ts" +# clearCookie('tokenPair', ...) CLEAR_COOKIE_PATTERN="clearCookie\\(\\s*['\\\"]tokenPair['\\\"]" +# get('AUTH_TYPE') === 'SSO' AUTH_TYPE_SSO_PATTERN="get\\(\\s*['\\\"]AUTH_TYPE['\\\"]\\s*\\)\\s*===\\s*['\\\"]SSO['\\\"]" MISMATCH_HEADER_PATTERN="X-Auth-Request-Email" MISMATCH_USER_PATTERN="data\\.user\\.email" +# 25 lines keeps the required signals in one logical guard block while +# tolerating normal formatting/wrapping drift. PROXIMITY_WINDOW=25 declare -a ROW_STATUS=() From 3ef61b84e5bafefe986f60f5f222932f096e5ca7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 11:44:30 +0000 Subject: [PATCH 7/7] chore(ci): simplify regex quoting and awk variable initialization Agent-Logs-Url: https://github.com/Pressingly/twenty/sessions/c800f651-f223-422d-be1b-32d95363495e Co-authored-by: awais786 <445320+awais786@users.noreply.github.com> --- scripts/sso-audit.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/sso-audit.sh b/scripts/sso-audit.sh index cae2a53bbd768..4999207d7c5d3 100755 --- a/scripts/sso-audit.sh +++ b/scripts/sso-audit.sh @@ -43,9 +43,9 @@ set -euo pipefail JWT_GUARD="packages/twenty-server/src/engine/guards/jwt-auth.guard.ts" USE_AUTH="packages/twenty-front/src/modules/auth/hooks/useAuth.ts" # clearCookie('tokenPair', ...) -CLEAR_COOKIE_PATTERN="clearCookie\\(\\s*['\\\"]tokenPair['\\\"]" +CLEAR_COOKIE_PATTERN='clearCookie\(\s*['"'"'"]tokenPair['"'"'"]' # get('AUTH_TYPE') === 'SSO' -AUTH_TYPE_SSO_PATTERN="get\\(\\s*['\\\"]AUTH_TYPE['\\\"]\\s*\\)\\s*===\\s*['\\\"]SSO['\\\"]" +AUTH_TYPE_SSO_PATTERN='get\(\s*['"'"'"]AUTH_TYPE['"'"'"]\s*\)\s*===\s*['"'"'"]SSO['"'"'"]' MISMATCH_HEADER_PATTERN="X-Auth-Request-Email" MISMATCH_USER_PATTERN="data\\.user\\.email" # 25 lines keeps the required signals in one logical guard block while @@ -153,7 +153,10 @@ check_row_20() { $0 ~ userPattern { user[NR]=1 } END { for (line = 1; line <= NR; line++) { - hasClear = hasAuth = hasHeader = hasUser = 0 + hasClear = 0 + hasAuth = 0 + hasHeader = 0 + hasUser = 0 for (i = line - window; i <= line + window; i++) { if (clear[i]) hasClear = 1 if (auth[i]) hasAuth = 1