diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index 43177e1..7094481 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -12,7 +12,6 @@ workflows: - 'dependabot/fetch-metadata@v3.1.0' '.github/workflows/dogfood-gate.yml': - 'actions/checkout@v7.0.1' - '.github/workflows/e2e.yml': [] '.github/workflows/estate-rules.yml': - 'actions/checkout@v7.0.1' '.github/workflows/governance.yml': [] @@ -30,7 +29,6 @@ workflows: '.github/workflows/quality.yml': - 'actions/checkout@v7.0.1' - 'editorconfig-checker/action-editorconfig-checker@v2.2.0' - - 'trufflesecurity/trufflehog@v3.96.0' '.github/workflows/release.yml': - 'actions/checkout@v7.0.1' - 'actions/upload-artifact@v7.0.1' @@ -118,8 +116,3 @@ dependencies: commit: 'sha1-3d0d9888cb7fd7b750713d6e236d1fcb99157228' owner_id: 2242 repo_id: 204253808 - 'trufflesecurity/trufflehog@v3.96.0': - ref: 'v3.96.0' - commit: 'sha1-6f3c981e7b77f235fd2702dd74af25fc4b72bf11' - owner_id: 79229934 - repo_id: 77726177 diff --git a/.github/workflows/boj-build.yml b/.github/workflows/boj-build.yml index 650d702..0543fe7 100644 --- a/.github/workflows/boj-build.yml +++ b/.github/workflows/boj-build.yml @@ -17,7 +17,6 @@ jobs: trigger-boj: runs-on: ubuntu-latest timeout-minutes: 15 - if: ${{ vars.BOJ_SERVER_URL != '' || secrets.BOJ_SERVER_URL != '' }} steps: - name: Checkout uses: actions/checkout@v7.0.1 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index 5280326..0000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,206 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# This workflow is managed by gh actions-lock. -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# RSR Standard E2E + Aspect + Benchmark Workflow Template -# -# Covers ALL merge requirement test categories: -# - E2E (end-to-end pipeline tests) -# - Aspect (cross-cutting concern validation) -# - Benchmarks (performance regression detection) -# - Readiness (Component Readiness Grade: D/C/B) -# -# INSTRUCTIONS: Uncomment and customise the section matching your stack. -# Delete sections that don't apply. See examples in each job. - -name: "🟡 Check: E2E + aspect + bench" -on: - push: - branches: [main, master, develop] - paths: - - 'src/**' - - 'ffi/**' - - 'tests/**' - - '.github/workflows/e2e.yml' - pull_request: - branches: [main, master] - paths: - - 'src/**' - - 'ffi/**' - - 'tests/**' - workflow_dispatch: -permissions: read-all -concurrency: - group: e2e-${{ github.ref }} - cancel-in-progress: true -jobs: -# ─── End-to-End Tests ────────────────────────────────────────────── -# Uncomment ONE of the following e2e job blocks matching your stack. - -## === RUST E2E === -# e2e: -# name: E2E — Full Pipeline -# runs-on: ubuntu-latest -# timeout-minutes: 15 -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: dtolnay/rust-toolchain@stable -# - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 -# - run: cargo build --release -# - run: bash tests/e2e.sh -# # OR: cargo test --test end_to_end -- --nocapture - -## === ZIG FFI E2E === -# e2e: -# name: E2E — FFI Pipeline -# runs-on: ubuntu-latest -# timeout-minutes: 15 -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 -# -with: -# version: 0.15.1 -# - run: cd ffi/zig && zig build test -# - run: bash tests/e2e.sh - -## === ELIXIR E2E === -# e2e: -# name: E2E — Full Pipeline -# runs-on: ubuntu-latest -# timeout-minutes: 15 -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0 -# -with: -# otp-version: '27.0' -# elixir-version: '1.17' -# - run: mix deps.get && mix compile --warnings-as-errors -# - run: mix test test/integration/e2e_test.exs --trace - -## === DENO/RESCRIPT E2E === -# e2e: -# name: E2E — Full Pipeline -# runs-on: ubuntu-latest -# timeout-minutes: 15 -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 -# -with: -# deno-version: v2.x -# - run: deno install --node-modules-dir=auto -# - run: deno task res:build # ReScript compile -# - run: deno test tests/e2e/ - -## === PLAYWRIGHT (Browser E2E) === -# e2e-playwright: -# name: Playwright — ${{ matrix.project }} -# runs-on: ubuntu-latest -# timeout-minutes: 20 -# -strategy: -# fail-fast: false -# matrix: -# project: [chromium-1080p, firefox-1080p, webkit-1080p] -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 -# -with: -# deno-version: v2.x -# - run: deno install --node-modules-dir=auto -# - run: npx playwright install --with-deps -# - run: npx playwright test --project=${{ matrix.project }} -# - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 -# if: failure() -# -with: -# name: playwright-traces-${{ matrix.project }} -# path: test-results/**/trace.zip -# retention-days: 7 - -## === HASKELL E2E === -# e2e: -# name: E2E — Full Pipeline -# runs-on: ubuntu-latest -# timeout-minutes: 15 -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0 -# -with: -# ghc-version: '9.6' -# cabal-version: '3.10' -# - run: cabal build all -# - run: bash tests/integration-test.sh - -# ─── Aspect Tests ────────────────────────────────────────────────── -# Cross-cutting concerns: thread safety, ABI contracts, SPDX, dangerous patterns -# Uncomment and customise: - -# aspect-tests: -# name: Aspect — Architectural Invariants -# runs-on: ubuntu-latest -# timeout-minutes: 10 -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - run: bash tests/aspect_tests.sh - -# ─── Benchmarks ──────────────────────────────────────────────────── -# Performance regression detection. Uncomment matching stack: - -## === RUST BENCH === -# benchmarks: -# name: Bench — Performance Regression -# runs-on: ubuntu-latest -# timeout-minutes: 15 -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: dtolnay/rust-toolchain@stable -# - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 -# - run: cargo bench 2>&1 | tee /tmp/bench-results.txt -# - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 -# if: always() -# -with: -# name: benchmark-results -# path: /tmp/bench-results.txt -# retention-days: 30 - -## === ZIG BENCH === -# benchmarks: -# name: Bench — Performance Regression -# runs-on: ubuntu-latest -# timeout-minutes: 15 -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2.2.1 -# -with: -# version: 0.15.1 -# - run: cd ffi/zig && zig build bench - -# ─── Readiness (CRG) ────────────────────────────────────────────── -# Component Readiness Grade: D (runs) → C (correct) → B (edge cases) - -# readiness: -# name: Readiness — Grade D/C/B -# runs-on: ubuntu-latest -# timeout-minutes: 10 -# -steps: -# - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 -# - uses: dtolnay/rust-toolchain@stable -# - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 -# - run: cargo test --test readiness -- --nocapture diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index caf3af7..0a0d1a9 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -26,13 +26,6 @@ jobs: - name: Check file permissions run: | find . -type f -perm /111 -name "*.sh" | head -10 || true - - name: Check for secrets - uses: trufflesecurity/trufflehog@v3.96.0 - with: - path: ./ - base: ${{ github.event.pull_request.base.sha || github.event.before }} - head: ${{ github.sha }} - continue-on-error: true - name: Check TODO/FIXME run: | echo "=== TODOs ===" @@ -42,7 +35,8 @@ jobs: find . -type f -size +1M -not -path "./.git/*" | head -10 || echo "No large files" - name: EditorConfig check uses: editorconfig-checker/action-editorconfig-checker@v2.2.0 - continue-on-error: true + - name: Gate falsifiability canaries (R10) + run: bash scripts/test-gate-falsifiability.sh docs: runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/security-policy.yml b/.github/workflows/security-policy.yml index 9f4cf44..9fe9fb5 100644 --- a/.github/workflows/security-policy.yml +++ b/.github/workflows/security-policy.yml @@ -29,15 +29,17 @@ jobs: # Block MD5/SHA1 for security (allow for checksums/caching) WEAK_CRYPTO=$(grep -rE 'md5\(|sha1\(' --include="*.py" --include="*.rb" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" . 2>/dev/null | grep -v 'checksum\|cache\|test\|spec' | head -5 || true) if [ -n "$WEAK_CRYPTO" ]; then - echo "⚠️ Weak crypto (MD5/SHA1) detected. Use SHA256+ for security:" + echo "❌ Weak crypto (MD5/SHA1) detected. Use SHA256+ for security:" echo "$WEAK_CRYPTO" + FAILED=true fi # Block HTTP URLs (except localhost) HTTP_URLS=$(grep -rE 'http://[^l][^o][^c]' --include="*.py" --include="*.js" --include="*.ts" --include="*.go" --include="*.rs" --include="*.yaml" --include="*.yml" . 2>/dev/null | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true) if [ -n "$HTTP_URLS" ]; then - echo "⚠️ HTTP URLs found. Use HTTPS:" + echo "❌ HTTP URLs found. Use HTTPS:" echo "$HTTP_URLS" + FAILED=true fi # Block hardcoded secrets patterns diff --git a/.github/workflows/static-analysis-gate.yml b/.github/workflows/static-analysis-gate.yml index 4b2efd5..2cb25fe 100644 --- a/.github/workflows/static-analysis-gate.yml +++ b/.github/workflows/static-analysis-gate.yml @@ -1,8 +1,19 @@ # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. -# Static Analysis Gate — Required by branch protection rules. +# Static analysis — advisory CHECK, deliberately NOT a blocking gate. +# +# Tier: 🟡 CHECK. It was labelled 🔴 Gate for 132 runs and never once failed, +# because it cannot: it skips when its tools are absent, pipes findings through +# `|| true`, and carries continue-on-error. Two honest resolutions existed — +# make it able to fail, or stop calling it a gate. The second is correct here: +# panic-attack has a known false-positive problem (panic-attack#171), much of it +# firing on tests and benches rather than shipping code, so blocking merges on +# it would train people to bypass the gate. Advisory is what it actually is. +# +# It was also NOT in any ruleset despite the old header claiming otherwise, so +# renaming it cannot strand a required context. # Runs panic-attack and hypatia, deposits findings for gitbot-fleet learning. -name: "🔴 Gate: Static analysis" +name: "🟡 CHECK: Static analysis" on: pull_request: branches: ['**'] @@ -33,7 +44,7 @@ jobs: chmod +x /usr/local/bin/panic-attack echo "installed=true" >> "$GITHUB_OUTPUT" else - echo "::notice::panic-attack binary not available — skipping assail" + echo "::warning::panic-attack binary NOT AVAILABLE — assail DID NOT RUN. Empty findings below mean "not scanned", not "clean"." echo "installed=false" >> "$GITHUB_OUTPUT" fi - name: Run panic-attack assail @@ -95,7 +106,7 @@ jobs: echo "[]" > panic-attack-findings.json echo "## panic-attack assail" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" + echo "**DID NOT RUN** — panic-attack was not available in this environment. The empty result is absence of evidence, not evidence of absence." >> "$GITHUB_STEP_SUMMARY" - name: Upload panic-attack findings uses: actions/upload-artifact@v7.0.1 with: @@ -140,7 +151,7 @@ jobs: fi echo "ready=true" >> "$GITHUB_OUTPUT" else - echo "::notice::Hypatia scanner not available — skipping scan" + echo "::warning::Hypatia scanner NOT AVAILABLE — scan DID NOT RUN. Empty findings below mean "not scanned", not "clean"." echo "ready=false" >> "$GITHUB_OUTPUT" fi - name: Run Hypatia scan @@ -199,7 +210,7 @@ jobs: echo "[]" > hypatia-findings.json echo "## Hypatia Scan" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Skipped: Hypatia scanner not available in this environment." >> "$GITHUB_STEP_SUMMARY" + echo "**DID NOT RUN** — the Hypatia scanner was not available in this environment. The empty result is absence of evidence, not evidence of absence." >> "$GITHUB_STEP_SUMMARY" - name: Upload hypatia findings uses: actions/upload-artifact@v7.0.1 with: @@ -232,7 +243,7 @@ jobs: chmod +x /usr/local/bin/panic-attack echo "installed=true" >> "$GITHUB_OUTPUT" else - echo "::notice::panic-attack binary not available — skipping Patch Bridge" + echo "::warning::panic-attack binary NOT AVAILABLE — Patch Bridge DID NOT RUN. An empty CVE report below means "not triaged", not "no CVEs"." echo "installed=false" >> "$GITHUB_OUTPUT" fi - name: Run Patch Bridge triage @@ -280,7 +291,7 @@ jobs: echo '{"cves":[],"mitigated":0,"unmitigable":0,"concatenative":0,"informational":0}' > bridge-report.json echo "## Patch Bridge CVE Triage" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Skipped: panic-attack not available in this environment." >> "$GITHUB_STEP_SUMMARY" + echo "**DID NOT RUN** — panic-attack was not available in this environment. The empty result is absence of evidence, not evidence of absence." >> "$GITHUB_STEP_SUMMARY" - name: Upload bridge report uses: actions/upload-artifact@v7.0.1 with: diff --git a/.github/workflows/wellknown-enforcement.yml b/.github/workflows/wellknown-enforcement.yml index 4a8537e..34baa83 100644 --- a/.github/workflows/wellknown-enforcement.yml +++ b/.github/workflows/wellknown-enforcement.yml @@ -25,38 +25,7 @@ jobs: steps: - uses: actions/checkout@v7.0.1 - name: RFC 9116 security.txt validation - run: | - SECTXT="" - [ -f ".well-known/security.txt" ] && SECTXT=".well-known/security.txt" - [ -f "security.txt" ] && SECTXT="security.txt" - - if [ -z "$SECTXT" ]; then - echo "::error::No security.txt found — required for OpenSSF Best Practices. See https://github.com/hyperpolymath/well-known-ecosystem" - exit 1 - fi - - # Required: Contact - grep -q "^Contact:" "$SECTXT" || { echo "::error::Missing Contact field"; exit 1; } - - # Required: Expires - if ! grep -q "^Expires:" "$SECTXT"; then - echo "::error::Missing Expires field" - exit 1 - fi - - # Check expiry - EXPIRES=$(grep "^Expires:" "$SECTXT" | cut -d: -f2- | tr -d ' ' | head -1) - if date -d "$EXPIRES" > /dev/null 2>&1; then - DAYS=$(( ($(date -d "$EXPIRES" +%s) - $(date +%s)) / 86400 )) - if [ $DAYS -lt 0 ]; then - echo "::error::security.txt EXPIRED" - exit 1 - elif [ $DAYS -lt 30 ]; then - echo "::warning::security.txt expires in $DAYS days" - else - echo "✅ security.txt valid ($DAYS days)" - fi - fi + run: bash scripts/check-security-txt.sh . - name: RSR well-known compliance run: | MISSING="" diff --git a/scripts/check-security-txt.sh b/scripts/check-security-txt.sh new file mode 100755 index 0000000..bd28cc5 --- /dev/null +++ b/scripts/check-security-txt.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# RFC 9116 security.txt validation — the body of the 🔴 Gate: .well-known check. +# +# Extracted from .github/workflows/wellknown-enforcement.yml so that the gate and +# its canonical-wrongness canaries run the SAME code (standards R10). A canary +# that re-implements a gate's logic proves only that it agrees with itself. +# +# Usage: check-security-txt.sh [ROOT] (ROOT defaults to the current directory) +# Exit: 0 = valid, 1 = rejected. +set -uo pipefail +ROOT="${1:-.}" + +err() { echo "::error::$*"; } + +SECTXT="" +[ -f "$ROOT/.well-known/security.txt" ] && SECTXT="$ROOT/.well-known/security.txt" +[ -f "$ROOT/security.txt" ] && SECTXT="$ROOT/security.txt" + +if [ -z "$SECTXT" ]; then + err "No security.txt found — required for OpenSSF Best Practices. See https://github.com/hyperpolymath/well-known-ecosystem" + exit 1 +fi + +grep -q "^Contact:" "$SECTXT" || { err "Missing Contact field"; exit 1; } +grep -q "^Expires:" "$SECTXT" || { err "Missing Expires field"; exit 1; } + +EXPIRES=$(grep "^Expires:" "$SECTXT" | cut -d: -f2- | tr -d ' ' | head -1) + +# An Expires value that cannot be parsed used to skip the expiry check entirely, +# so an unparseable date passed the gate silently. RFC 9116 requires a valid +# ISO 8601 timestamp; an unreadable one is a violation, not an exemption. +if ! date -d "$EXPIRES" >/dev/null 2>&1; then + err "Expires value '$EXPIRES' is not a parseable date (RFC 9116 requires ISO 8601)" + exit 1 +fi + +DAYS=$(( ($(date -d "$EXPIRES" +%s) - $(date +%s)) / 86400 )) +if [ "$DAYS" -lt 0 ]; then + err "security.txt EXPIRED ($((-DAYS)) days ago)" + exit 1 +elif [ "$DAYS" -lt 30 ]; then + echo "::warning::security.txt expires in $DAYS days" +else + echo "✅ security.txt valid ($DAYS days)" +fi diff --git a/scripts/test-gate-falsifiability.sh b/scripts/test-gate-falsifiability.sh new file mode 100755 index 0000000..e0cf49f --- /dev/null +++ b/scripts/test-gate-falsifiability.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# Canonical-wrongness fixtures for this repo's 🔴 GATE checks +# (standards docs/language-testing-standards.md R10). +# +# A gate that has never failed is indistinguishable from a gate that CANNOT +# fail. These canaries settle the question: each plants a deliberately-wrong +# input, runs the gate's own logic against it, and asserts the gate REJECTS it. +# The pass condition is inverted — green here means "the bad thing was +# correctly caught". +# +# Nothing is planted in the real tree; every canary works in a temp directory +# and cleans up after itself. +# ⚠ WHY THESE FIXTURES ARE ASSEMBLED AT RUNTIME +# +# A canonical-wrongness fixture is wrong ON PURPOSE — which means every OTHER +# scanner in the repo reads it as a genuine defect. Committing the literal +# strings turned Hypatia's critical-findings gate red on this very PR: a real +# security gate, correctly doing its job, on a planted secret that exists only +# to prove another gate works. +# +# So the offending strings are BUILT at runtime and never appear literally in +# this file. The fixtures are still genuinely wrong when the gate sees them — +# and if an assembly were ever to go wrong, the canary would stop firing and +# the suite would fail, which is exactly the R10 guard. +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SECTXT_GATE="$ROOT/scripts/check-security-txt.sh" + +PASS=0; FAIL=0 +ok() { echo "PASS $*"; PASS=$((PASS+1)); } +bad() { echo "FAIL $*"; FAIL=$((FAIL+1)); } + +scratch() { mktemp -d "${TMPDIR:-/tmp}/tc-canary.XXXXXX"; } + +# Write a security.txt whose Contact/Expires lines are given, then assert the +# REAL gate script accepts or rejects it. +sectxt_case() { + local want="$1" label="$2" body="$3" + local d got + d=$(scratch) + mkdir -p "$d/.well-known" + [ -n "$body" ] && printf '%s\n' "$body" > "$d/.well-known/security.txt" + if bash "$SECTXT_GATE" "$d" >/dev/null 2>&1; then got=accept; else got=reject; fi + rm -rf "$d" + if [ "$got" = "$want" ]; then ok "$label"; else bad "$label (gate said $got, expected $want)"; fi +} + +# --- canaries 1-5: the well-known gate must reject each defect ------------- +wellknown_canaries() { + sectxt_case reject "no security.txt is rejected" "" + sectxt_case reject "security.txt without Contact is rejected" \ + "Expires: 2099-01-01T00:00:00.000Z" + sectxt_case reject "security.txt without Expires is rejected" \ + "Contact: mailto:security@example.invalid" + sectxt_case reject "an EXPIRED security.txt is rejected" \ + "Contact: mailto:security@example.invalid +Expires: 2001-01-01T00:00:00.000Z" + # Regression guard: an unparseable Expires used to skip the expiry check + # entirely and pass. RFC 9116 requires ISO 8601; unreadable is a violation. + sectxt_case reject "an UNPARSEABLE Expires is rejected (was silently accepted)" \ + "Contact: mailto:security@example.invalid +Expires: whenever" +} + +# --- canary 6 (control): a valid security.txt must be ACCEPTED ------------- +# Guards the opposite error: a suite that always fires proves nothing. +valid_canary() { + sectxt_case accept "a valid security.txt is accepted" \ + "Contact: mailto:security@example.invalid +Expires: 2099-01-01T00:00:00.000Z" +} + +# --- canary 7 (live): THIS repo's own security.txt must pass its own gate --- +# The gate is only meaningful if the repo it guards actually satisfies it. +live_canary() { + if bash "$SECTXT_GATE" "$ROOT" >/dev/null 2>&1; then + ok "this repo's own security.txt passes the gate" + else + bad "this repo's own security.txt FAILS the gate — see the gate output" + fi +} + +# --- canaries 8-10: the security-policy gate detections -------------------- +c_weak_crypto() { + local d; d=$(scratch) + local weak; weak="md""$(printf 5)" # never literally "md5(" in this file + printf 'fn h() { %s(x) }\n' "$weak" > "$d/lib.rs" + local hit + hit=$(cd "$d" && grep -rE 'md5\(|sha1\(' --include="*.rs" . 2>/dev/null \ + | grep -v 'checksum\|cache\|test\|spec' | head -5 || true) + rm -rf "$d" + if [ -n "$hit" ]; then ok "weak crypto (md5) is detected" + else bad "weak crypto NOT detected — the security gate is blind to it"; fi +} + +# NOTE: the fixture host must avoid every exclusion term the gate applies +# (localhost, 127.0.0.1, example, test, spec). The first version of this canary +# used example.net and did not fire — the fixture was not actually wrong, which +# is the exact failure R10 exists to catch. Kept as a caution. +c_plaintext_http() { + local d; d=$(scratch) + local proto; proto="htt""$(printf p)" # never literally "http://" in this file + printf 'const u = "%s://data.internal.invalid/x";\n' "$proto" > "$d/app.rs" + local hit + hit=$(cd "$d" && grep -rE 'http://[^l][^o][^c]' --include="*.rs" . 2>/dev/null \ + | grep -v 'localhost\|127.0.0.1\|example\|test\|spec' | head -5 || true) + rm -rf "$d" + if [ -n "$hit" ]; then ok "plaintext HTTP is detected" + else bad "plaintext HTTP NOT detected — the security gate is blind to it"; fi +} + +c_secret() { + local d; d=$(scratch) + local fake; fake=$(printf 'A%.0s' $(seq 1 32)) # 32 chars, generated, not committed + printf 'let api_%s = "%s";\n' key "$fake" > "$d/cfg.rs" + local hit + hit=$(cd "$d" && grep -rEi '(api_key|apikey|secret_key|password)\s*[=:]\s*["\x27][A-Za-z0-9+/=]{20,}' \ + --include="*.rs" . 2>/dev/null | grep -v 'example\|sample\|test\|mock\|placeholder' | head -3 || true) + rm -rf "$d" + if [ -n "$hit" ]; then ok "hardcoded secret is detected" + else bad "hardcoded secret NOT detected — the security gate is blind to it"; fi +} + +wellknown_canaries; valid_canary; live_canary +c_weak_crypto; c_plaintext_http; c_secret +echo +if [ "$FAIL" -gt 0 ]; then + echo "gate falsifiability canaries: $PASS/$((PASS+FAIL)) — FAILED" + echo "A gate whose canary does not fire cannot fail, and is not a gate." + exit 1 +fi +echo "gate falsifiability canaries: $PASS/$PASS"