Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 113 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ jobs:
steps:
- name: Checkout Codebase
uses: actions/checkout@v4
with:
fetch-depth: 0 # commit-range signature checks (#619)

- name: Setup Node.js Environment
uses: actions/setup-node@v4
Expand Down Expand Up @@ -185,34 +187,13 @@ jobs:
tests/e2e/layout.spec.ts-snapshots/
if-no-files-found: ignore

# Closes #540 — license gate (fails on GPL/unauthorized copyleft),
# install-script allowlist, registry/integrity hijack checks, and a
# committed licenses.json freshness check. Zero dependencies: no install.
supply-chain-audit:
name: Supply Chain & License Audit
runs-on: ubuntu-latest
steps:
- name: Checkout Codebase
uses: actions/checkout@v4

- name: Setup Node.js Environment
uses: actions/setup-node@v4
with:
node-version: 22.x

- name: Audit dependencies
run: node scripts/audit-deps.js --check

# Build-pipeline network egress monitor: every outbound connection made
# while dependencies are fetched and while `npm run build` runs is captured
# (tcpdump) and classified against the allowlist in
# scripts/monitor-build-egress.sh. Unauthorized destinations fail the job
# and the audit logs are uploaded for security review.
build-egress-monitor:
name: Build Egress Monitor & Data Exfiltration Gate
# #541 / #543 — Soroban contract unit tests (aegis_vault treasury, DAO oracle).
contracts:
name: Soroban Contract Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Codebase
Expand Down Expand Up @@ -250,6 +231,116 @@ jobs:
path: artifacts/build-egress/
if-no-files-found: warn

# #541 / #543 — Soroban contract unit tests (aegis_vault treasury, DAO oracle).
contracts:
name: Soroban Contract Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32v1-none

- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: |
contracts/aegis_vault
contracts/maintainer_vault
contracts/helphone_dao
contract

- name: Run cargo test (aegis_vault)
working-directory: contracts/aegis_vault
run: cargo test --locked

- name: Run cargo test (maintainer_vault)
working-directory: contracts/maintainer_vault
run: cargo test --locked

# Closes #587 — DAO + open source sustainability reserve (1% fee,
# DAO-voted maintainer grants).
- name: Run cargo test (helphone_dao)
working-directory: contracts/helphone_dao
run: cargo test --locked

# The `soroban` CLI was renamed to `stellar` (soroban-cli -> stellar-cli);
# `stellar contract build` is the current equivalent of the issue's
# `soroban contract build` and is what compiles contracts to the
# wasm32v1-none target actually deployed (see soroban-contract.md's
# documented deploy steps).
- name: Install Stellar CLI
run: cargo install --locked stellar-cli --version ^23

- name: Build aegis_vault contract (stellar contract build)
working-directory: contracts/aegis_vault
run: stellar contract build

- name: Build helphone-contract (stellar contract build)
working-directory: contract
run: stellar contract build

# Supply-chain security: typosquatting gate, transitive vulnerability scan,
# WASM reproducibility, dep health, license compliance, CVE patch plan, and
# maintainer key revocation check (#586 #587 #588 #589 #590 #591 #599 #619).
supply-chain:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # commit-range signature checks (#619)

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'npm'

- name: Install dependencies
run: npm ci

# Closes #588 — blocks PRs introducing typosquatted / hijacked packages.
- name: Typosquatting gate
run: node scripts/detect-typosquatting.js

# Closes #589 — transitive DAG audit (offline graph + depth stats;
# live OSV lookup is best-effort via --audit).
- name: Transitive vulnerability scan
run: node scripts/transitive-vulnerability-scanner.js

# Closes #590 — asserts committed ZK WASM artifact hash + flags.
- name: WASM reproducibility check
run: bash scripts/verify-wasm-build.sh

# Closes #591 — informational health index; never blocks merges.
- name: Dependency health monitor
run: node scripts/monitor-dep-health.js --offline
continue-on-error: true

# Closes #586 — npm + Cargo license scan; fails on unapproved
# GPL/AGPL/strong-copyleft licenses (see docs/legal-compliance.md).
- name: License compliance & copyleft gate
run: node scripts/license-compliance.js --no-write

# Closes #599 — GitHub Security Advisory scan + minimum-patch plan.
# Report-only here; scheduled patch PRs come from cve-patch-bot.yml.
- name: CVE patch plan
run: node scripts/auto-patch-cve.js --fail-on critical

# Closes #619 — commit signatures vs live key revocation lists + WoT.
- name: Maintainer key revocation check
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
RANGE="origin/${{ github.base_ref }}..HEAD"
else
RANGE="HEAD"
fi
node scripts/security/verify_maintainer_keys.js --pinned --range "$RANGE" --max 100

# Automated dependency version drift & breaking API change analyzer: the
# exported TypeScript surface of every protected package (functions, class
# members, call signatures) is extracted from its .d.ts entry point with the
Expand Down Expand Up @@ -289,6 +380,7 @@ jobs:
name: api-drift-report
path: api-drift-report.txt
if-no-files-found: ignore

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable

Expand Down
54 changes: 54 additions & 0 deletions .github/workflows/cve-patch-bot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CVE Patch Bot

# Closes #599 — parses GitHub Security Advisories for the npm tree, applies
# the minimum non-vulnerable versions, runs the full regression suite and
# opens a security PR. Major-version fixes are listed for manual follow-up.
on:
schedule:
- cron: "0 5 * * 1" # weekly, Monday 05:00 UTC
workflow_dispatch:
inputs:
min-severity:
description: "Lowest severity to patch (low|moderate|high|critical)"
default: "low"

permissions:
contents: write
pull-requests: write

concurrency:
group: cve-patch-bot
cancel-in-progress: false

jobs:
patch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: npm

- run: npm ci

- name: Configure bot identity
run: |
git config user.name "helphone-security-bot"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

# PRs opened with the default GITHUB_TOKEN do not trigger CI; set a
# CVE_BOT_TOKEN secret (fine-grained PAT / GitHub App) so they do.
- name: Patch, verify and open PR
env:
GH_TOKEN: ${{ secrets.CVE_BOT_TOKEN || secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node scripts/auto-patch-cve.js \
--source github \
--min-severity "${{ github.event.inputs.min-severity || 'low' }}" \
--verify "npm test" \
--verify "npm run build" \
--verify "node scripts/license-compliance.js --no-write" \
--open-pr --base "${{ github.event.repository.default_branch }}"
13 changes: 12 additions & 1 deletion .github/workflows/slsa-provenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,18 @@ jobs:

- run: npm ci

- run: npm run build
# #619 — refuse to release when the tag / recent commits are signed by
# a key that has since been revoked as compromised (live KRL lookup).
- name: Verify release signing keys
run: |
ARGS="--pinned --refresh --range HEAD --max 50"
if [ "${{ github.ref_type }}" = "tag" ]; then ARGS="$ARGS --tags ${{ github.ref_name }}"; fi
if [ "${{ vars.REQUIRE_SIGNED_RELEASES }}" = "true" ]; then ARGS="$ARGS --strict"; fi
node scripts/security/verify_maintainer_keys.js $ARGS

# #586 — production build + licenses.json attribution manifest, so the
# manifest is covered by the signed digests below.
- run: npm run build:release

# Digest manifest of every shipped file. This exact file is what gets
# signed and logged in Rekor; the browser re-hashes it and its own entry
Expand Down
61 changes: 61 additions & 0 deletions .github/workflows/verify-keys.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Maintainer Key Verification

# Closes #619 — validates OpenPGP signatures on commits, tags and pinned
# dependency maintainer keys against live key revocation data from
# keys.openpgp.org + keyserver.ubuntu.com, and evaluates web-of-trust
# certifications (config/maintainer-keys.json).
#
# The nightly run is the important one: a key revoked as COMPROMISED after a
# release invalidates every signature it made, so every release tag is
# re-checked against a freshly refreshed revocation list.
on:
push:
branches: [main, develop]
tags: ["v*"]
pull_request:
branches: [main, develop]
schedule:
- cron: "17 3 * * *" # nightly KRL refresh
workflow_dispatch:

permissions:
contents: read

jobs:
verify-keys:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history + tags

- uses: actions/setup-node@v4
with:
node-version: 22.x

# Validated public keys are cached between runs; entries older than
# cacheTtlHours are refetched, and --refresh (nightly) forces a refetch.
- uses: actions/cache@v4
with:
path: .cache/maintainer-keys
key: maintainer-keys-${{ github.run_id }}
restore-keys: maintainer-keys-

- name: Pull request commits
if: github.event_name == 'pull_request'
run: node scripts/security/verify_maintainer_keys.js --pinned --range "origin/${{ github.base_ref }}..HEAD"

- name: Pushed commits
if: github.event_name == 'push' && github.ref_type == 'branch'
run: node scripts/security/verify_maintainer_keys.js --pinned --range HEAD --max 50

- name: Release tag
if: github.event_name == 'push' && github.ref_type == 'tag'
run: |
STRICT=""
if [ "${{ vars.REQUIRE_SIGNED_RELEASES }}" = "true" ]; then STRICT="--strict"; fi
node scripts/security/verify_maintainer_keys.js --pinned --refresh --tags "${{ github.ref_name }}" $STRICT

- name: Nightly revocation sweep (all release tags + pinned keys)
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
run: node scripts/security/verify_maintainer_keys.js --pinned --refresh --tags "v*" --range HEAD --max 100
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@ src/wasm/**/target/
# Python tooling cache
__pycache__/
*.py[cod]
/target/
# CVE patch bot scratch output (#599)
.cve-patch-pr.md
# maintainer key cache (#619)
.cache/
22 changes: 15 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

[workspace]
resolver = "2"
members = ["contracts/*"]
members = [
"contracts/aegis_vault",
"contracts/helphone_dao",
"contracts/maintainer_vault",
]
exclude = [
# Own workspace root.
"contract",
Expand All @@ -14,22 +18,26 @@ exclude = [
"src/wasm/telemetry_reader",
]

# Member crates' [profile.release] sections are ignored inside a workspace;
# the Soroban size/safety profile has to live at the root.
# Member crates declare this same release profile; it is mirrored here because
# Cargo only honours `[profile]` tables at the workspace root, so building from
# the repo root must produce the same wasm artifact as building in a crate dir.
[workspace]
resolver = "2"
members = ["contracts/aegis_vault", "contracts/helphone_dao"]
# Standalone crates that keep their own lockfiles / workspaces.
exclude = ["contract", "contracts/emergency_vault", "contracts/maintainer_vault"]

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true
overflow-checks = true
debug-assertions = false

# #586: Cargo licenses are gated with the npm tree by
# scripts/license-compliance.js (via `cargo metadata --locked`); see
# docs/legal-compliance.md for the approved/denied policy.
[workspace.metadata.license-compliance]
gate = "node scripts/license-compliance.js --no-write"
manifest = "licenses.json"

# ---------------------------------------------------------------------------
# Version pinning guard (scripts/detect-api-drift.js) — Rust half.
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

HelPhone is a React + Vite community emergency response application built on Stellar. It combines wallet-gated help requests, Soroban smart contracts, local ZK privacy proofs, WebAuthn Passkeys, and automated contract storage state backups.

## The 3-minute story

Someone is in trouble and needs help from nearby people — but broadcasting "I'm hurt, here is my exact address and my name" to a public blockchain is dangerous. HelPhone fixes that:

1. **Emergency.** A person taps _Get help_ and picks what happened (lost, fallen, medical, danger…).
2. **Identity protected.** Their name and contact never leave the browser. Only a pseudonymous `Private request #N` is written on-chain.
3. **Location proven, not revealed.** The exact GPS coordinate is used as a _private witness_. A Noir ZK proof is generated **locally** to prove "I am inside this zone" without disclosing where. Only a coarse ~1 km point and a 3 km proof box go on-chain.
4. **Stellar verifies.** The proof fingerprint (nullifier) and transaction hash are recorded on Soroban testnet, visible in the live `ZK PRIVACY CHECKPOINT` panel.
5. **Double-claim blocked.** The nullifier is `Poseidon2(secret_id, campaign_id)` — one claim per user per campaign, so the same proof can't be replayed.

Privacy here is real, not theater: see [`anonymizeLocation`](src/pages/Help.jsx) (coarsens coordinates) and `createRequest(..., '', '', ...)` in [`handleSubmit`](src/pages/Help.jsx) (empty name/contact on-chain).

---

## Technical Subsystems & Architecture
Expand Down Expand Up @@ -105,6 +117,7 @@ Configure `.env`:
```bash
VITE_MAPBOX_TOKEN=...
VITE_AEGIS_VAULT_ID=...
VITE_ZK_PROVER_URL=/zk
SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
CONTRACT_ID=CC325F37QW7N2F5M3QGHL4A4O7J2K9L0M1N2O3P4Q5R6S7T8U9V0
```
Expand Down
Loading
Loading