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
52 changes: 51 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,58 @@ jobs:
- name: Docs drift check
run: pnpm docs:check:ci

# Supply-chain audit. Full-tree (dev + build + transitive) on every push to
# main and on release (release.yml). On a pull_request it runs ONLY when the
# PR changes a dependency manifest — so an unrelated feature PR is not blocked
# by a newly-published advisory on a dep it never touched. Safe because, with
# `--frozen-lockfile`, an unchanged manifest means an unchanged installed tree
# (nothing new to audit), and that standing tree is still covered by the
# main-push audit, the release audit, the daily Security Audit workflow, and
# Dependabot security updates. Fails CLOSED: any uncertainty runs the audit.
- name: Audit dependencies
run: pnpm audit --audit-level=high
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
# GitHub runs `shell: bash` with `-e` (errexit); disable it so the
# no-match `grep` (rc=1, the skip case) doesn't abort before we read
# `rc`. Exit codes are handled explicitly below (rc checks + exit $?),
# and the audit's own failure still propagates via `run_audit; exit $?`.
set -uo pipefail
set +e
run_audit() {
echo "Running full-tree dependency audit (pnpm audit --audit-level=high)"
pnpm audit --audit-level=high
}
if [ "$EVENT_NAME" != "pull_request" ]; then
run_audit; exit $?
fi
if [ -z "${BASE_SHA:-}" ]; then
echo "::notice::No PR base SHA available — running full audit (fail-closed)."
run_audit; exit $?
fi
if ! changed="$(git diff --name-only "$BASE_SHA" HEAD 2>/dev/null)"; then
echo "::notice::Could not compute changed files — running full audit (fail-closed)."
run_audit; exit $?
fi
# Any file that can affect what `pnpm install` produces or executes:
# declared/resolved deps (package.json, pnpm-lock.yaml, pnpm-workspace.yaml),
# registry/install config (.npmrc), install hooks (.pnpmfile.cjs/.js/.mjs),
# and applied patches (patches/**). Missing any of these would be a
# fail-open (a PR could change the installed tree while the audit skips).
# Branch on grep's exit code explicitly: 0 = matched (run), 1 = no match
# (skip), anything else = grep error → run (fail-closed).
printf '%s\n' "$changed" | grep -Eq '(^|/)(package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|\.npmrc|\.pnpmfile\.(c|m)?js)$|(^|/)patches/'
rc=$?
if [ "$rc" -eq 0 ]; then
echo "Dependency manifest / install input changed in this PR — running full audit."
run_audit; exit $?
elif [ "$rc" -ne 1 ]; then
echo "::notice::Manifest-change detection errored (grep rc=$rc) — running full audit (fail-closed)."
run_audit; exit $?
fi
echo "::notice title=Dependency audit skipped::No dependency manifest or install input changed in this PR (package.json / pnpm-lock.yaml / pnpm-workspace.yaml / .npmrc / .pnpmfile.* / patches/). The full-tree audit is enforced on main, on release, and by the daily Security Audit workflow."

- name: Build
run: pnpm build
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/security-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Security Audit

# Standing-tree supply-chain backstop. The per-PR audit (ci.yml) only runs when a
# PR changes a dependency manifest, so this daily full-tree audit catches a
# newly-published advisory on an already-installed dependency during quiet periods
# (no pushes to main). Dependabot security updates is the proactive remediator;
# this is the visible fail-loud signal. Full tree (dev + build + transitive).

on:
schedule:
- cron: '17 7 * * *' # daily at 07:17 UTC
workflow_dispatch:

concurrency:
group: security-audit
cancel-in-progress: true

permissions:
contents: read
issues: write # open/append a tracking issue when the audit fails (see below)

jobs:
audit:
name: Full-tree dependency audit
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Audit dependencies (full tree)
run: pnpm audit --audit-level=high

# Make a failure actionable rather than a quiet red run: open a tracking
# issue (or comment on the existing open one, to avoid daily duplicates).
- name: Report audit failure
if: failure()
env:
GH_TOKEN: ${{ github.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
shell: bash
run: |
set -uo pipefail
title='Scheduled security audit failing'
body="The daily full-tree \`pnpm audit --audit-level=high\` failed. Triage per CONTRIBUTING.md (Dependencies & supply-chain audits). Run: ${RUN_URL}"
existing="$(gh issue list --state open --search "in:title \"${title}\"" --json number --jq '.[0].number // empty' 2>/dev/null || true)"
if [ -n "${existing:-}" ]; then
gh issue comment "$existing" --body "$body" || true
else
gh issue create --title "$title" --body "$body" || true
fi
49 changes: 49 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,55 @@ The proxy sits in the critical path of every agent tool call. Performance matter
- Audit writes must be **async and non-blocking**: never add latency to the tool call path.
- If you're adding a dependency, consider its impact on startup time and memory.

### Dependencies & supply-chain audits

Helio is a security tool, so the dependency tree is part of the threat model — npm
supply-chain campaigns (e.g. the 2026 TeamPCP wave) compromise **dev, build, and
transitive** packages, not just production ones, with install-time code execution.
Our audit posture reflects that:

- **Coverage is always the full tree** (dev + build + transitive). We never scope
the audit to production-only — build tooling produces the shipped bundle and runs
in CI with credentials, so it is in scope.
- **`pnpm audit --audit-level=high` is enforced unconditionally on `main`, on every
release (`release.yml`), and daily (`security-audit.yml`).** These are the
guarantees: a flagged dependency can never be merged to `main` unnoticed or
shipped in a release.
- **On a pull request, the audit runs only when the PR changes a dependency
manifest or install input** — `package.json`, `pnpm-lock.yaml`,
`pnpm-workspace.yaml`, `.npmrc`, `.pnpmfile.*` (install hooks), or anything under
`patches/`. With `--frozen-lockfile`, leaving all of these unchanged means an
unchanged installed tree, so an unrelated feature PR is not blocked by a
newly-published advisory on a dependency it never touched. The check still reports
green with a notice explaining the skip. The guard **fails closed** — any
uncertainty (unknown event, missing base SHA, diff/grep error) runs the audit. If
you add a new install-affecting input (e.g. a new pnpm hook mechanism), add it to
the trigger set in `ci.yml`.
- **Dependabot security updates** is enabled so advisories on the standing tree are
auto-PR'd within hours rather than discovered by a CI failure.
- **This gate's integrity depends on branch protection.** `main` must require the
`ci` status check and code-owner review (workflow files are owned via CODEOWNERS),
with admin bypass disabled — otherwise a PR could weaken the workflow itself. Treat
those settings as part of the control, not optional.

**Handling an advisory — triage by _type_, not just dev-vs-prod:**

- A **malicious package, install-time RCE, or credential-exfiltration** advisory is
an **incident** regardless of whether the package is dev-only or shipped — remediate
immediately (upgrade, or remove), do not ignore.
- A **benign vulnerability with no exploit path in our usage** (e.g. a dev-server
SSRF or ReDoS in a build tool we only invoke on trusted input) may be **time-boxed
ignored** via `pnpm.auditConfig.ignoreGhsas`, but **only** with a tracking issue to
remove it. Prefer a real upgrade over an ignore.

Current dev-only ignores (each tracked for removal):

| GHSA | Package (path) | Why ignored | Tracking |
| --------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------- |
| `GHSA-gv7w-rqvm-qjhr` | esbuild (via vite, dashboard build) | dev-server request vuln; not in the shipped bundle | #64 |
| `GHSA-fx2h-pf6j-xcff` | vite (dashboard build) | dev-server only; vite is not run in production | #64 |
| `GHSA-vmh5-mc38-953g` | undici (via `jsdom`, test env) | SOCKS5 ProxyAgent TLS path, not exercised in tests; no patched undici is compatible with `jsdom@29`'s internal layout | #64 |

## Issue Labels

| Label | Meaning |
Expand Down
Loading