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
121 changes: 66 additions & 55 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ on:
schedule:
# Weekly re-scan (Trivy misconfig + gitleaks) against updated policy/rule DBs
# even when the repo is dormant. Monday 04:00 UTC, staggered from CodeQL (03:00).
# Only trivy-scan + secrets-scan run on this trigger — the deterministic jobs
# (shell/secrets/vendored/dockerfile/squid/egress-test) are gated off with
# `if: github.event_name != 'schedule'` since their output cannot change on a
# dormant repo; the gate job tolerates their `skipped` result on schedule runs.
- cron: "0 4 * * 1"
workflow_dispatch:

Expand All @@ -48,6 +52,10 @@ jobs:
name: shell scripts (syntax + shellcheck)
runs-on: ubuntu-24.04
timeout-minutes: 10
# Deterministic on a dormant repo — skip on the weekly schedule (which exists
# only to re-scan against fresh Trivy/gitleaks rule DBs). Same gate on the
# other deterministic jobs below.
if: github.event_name != 'schedule'
permissions:
contents: read
steps:
Expand Down Expand Up @@ -76,6 +84,7 @@ jobs:
name: leak audit (no baked secrets)
runs-on: ubuntu-24.04
timeout-minutes: 10
if: github.event_name != 'schedule'
permissions:
contents: read
steps:
Expand Down Expand Up @@ -105,6 +114,7 @@ jobs:
name: in-repo vendored copies + generated allowlist match canonical
runs-on: ubuntu-24.04
timeout-minutes: 10
if: github.event_name != 'schedule'
permissions:
contents: read
steps:
Expand All @@ -122,6 +132,7 @@ jobs:
name: dockerfile lint
runs-on: ubuntu-24.04
timeout-minutes: 10
if: github.event_name != 'schedule'
permissions:
contents: read
steps:
Expand All @@ -139,6 +150,7 @@ jobs:
name: squid.conf syntax (squid -k parse)
runs-on: ubuntu-24.04
timeout-minutes: 10
if: github.event_name != 'schedule'
permissions:
contents: read
steps:
Expand Down Expand Up @@ -166,20 +178,36 @@ jobs:
name: devcontainer builds + egress lock enforces
runs-on: ubuntu-24.04
timeout-minutes: 30
if: github.event_name != 'schedule'
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
- name: build image + functional egress smoke test
# Two things at once: (a) the Dockerfile is BUILT (hadolint/Trivy only
# lint/scan it), catching a build that lints clean but breaks; (b) the image
# is BOOTED and the egress lock is asserted end-to-end. The gating assertions
# (firewall sentinel up, off-allowlist host blocked, non-CONNECT cleartext
# refused) are internet-INDEPENDENT — squid denies them locally — so this is a
# deterministic gate; only the on-allowlist reachability check needs egress and
# it is a soft warning. Needs NET_ADMIN for iptables, hence a real boot here
# rather than in the self-contained lint jobs above.
run: RUNTIME=docker IMAGE=dev-sandbox:ci ./test/egress-smoke.sh
- name: set up buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: build image (GHA layer cache)
# The Dockerfile is BUILT here (hadolint/Trivy only lint/scan it), catching a
# build that lints clean but breaks. Layers are cached in the GHA cache
# backend: the Dockerfile is deliberately ordered so routine sync.sh edits
# touch only the final cheap COPY layers, and without a persisted cache CI
# re-paid the full apt + Node/Python download + npm install build (~4-8 min,
# the long pole of the pipeline) on every run. Cache-hit builds take seconds.
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: sandbox/.devcontainer
tags: dev-sandbox:ci
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: functional egress smoke test
# The image is BOOTED and the egress lock asserted end-to-end. The gating
# assertions (firewall sentinel up, off-allowlist host blocked, non-CONNECT
# cleartext refused) are internet-INDEPENDENT — squid denies them locally — so
# this is a deterministic gate; only the on-allowlist reachability check needs
# egress and it is a soft warning. Needs NET_ADMIN for iptables, hence a real
# boot here rather than in the self-contained lint jobs above. SKIP_BUILD=1
# reuses the image built (with layer caching) in the previous step.
run: RUNTIME=docker IMAGE=dev-sandbox:ci SKIP_BUILD=1 ./test/egress-smoke.sh

# ─────────────────────────────────────────────────────────────────────────────
# Trivy — Dockerfile / IaC misconfiguration scan (HIGH/CRITICAL). The `misconfig`
Expand All @@ -200,18 +228,11 @@ jobs:
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4

- name: Run Trivy scan (table — fail on HIGH/CRITICAL)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: fs
scan-ref: .
scanners: misconfig
format: table
severity: HIGH,CRITICAL
exit-code: "1"

- name: Run Trivy scan (SARIF — upload to GitHub Security)
if: always()
- name: Run Trivy scan (SARIF — fail on HIGH/CRITICAL)
# ONE scan does both duties: Trivy writes the SARIF report BEFORE applying
# exit-code, so this step gates the pipeline on HIGH/CRITICAL findings AND
# produces the Security-tab upload — the previous separate table + SARIF
# steps re-ran an identical scan (~20-40s) for no additional signal.
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: fs
Expand All @@ -220,7 +241,7 @@ jobs:
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
exit-code: "0"
exit-code: "1"

- name: Upload Trivy SARIF to GitHub Security
if: always()
Expand All @@ -230,16 +251,19 @@ jobs:
category: trivy

# ─────────────────────────────────────────────────────────────────────────────
# Quality Gate — every check above must pass. Aggregates results so a failure
# in any job surfaces as one status.
# CI Complete — single required status check covering the whole pipeline: every
# job above must pass. Set "CI Complete" as the required check in branch
# protection settings. (Formerly a quality-gate → ci-complete chain of two jobs
# doing the same aggregation — the second runner allocation added ~20-40s to
# time-to-green on every run for no additional signal.)
# ─────────────────────────────────────────────────────────────────────────────
quality-gate:
name: Quality Gate
ci-complete:
name: CI Complete
runs-on: ubuntu-24.04
timeout-minutes: 5
# INVARIANT: every job above (except ci-complete) MUST be listed here. A job
# that runs but is omitted from `needs` can fail while this gate stays green —
# so when you add a job, add it to this list too.
# INVARIANT: every job above MUST be listed here. A job that runs but is
# omitted from `needs` can fail while this gate stays green — so when you add
# a job, add it to this list too.
needs:
- shell
- secrets
Expand All @@ -254,31 +278,18 @@ jobs:
- name: Check all gates passed
run: |
results='${{ toJson(needs.*.result) }}'
# `skipped` counts as a failure too: a required job that never ran (a
# future `if:` evaluating false, or a needs-skip from an upstream failure)
# must NOT pass the gate silently.
if echo "$results" | grep -qE '"(failure|cancelled|skipped)"'; then
echo "Quality gate failed. Job results: $results"
exit 1
# `skipped` counts as a failure too: a required job that never ran (an
# `if:` evaluating false, or a needs-skip from an upstream failure) must
# NOT pass the gate silently. EXCEPTION: on the weekly schedule the
# deterministic jobs are skipped BY DESIGN (their `github.event_name !=
# 'schedule'` gate), so only tolerate `skipped` there.
if [ "${{ github.event_name }}" = "schedule" ]; then
bad='"(failure|cancelled)"'
else
bad='"(failure|cancelled|skipped)"'
fi
echo "All quality gates passed."

# ─────────────────────────────────────────────────────────────────────────────
# CI Complete — single required status check covering the whole pipeline.
# Set "CI Complete" as the required check in branch protection settings.
# ─────────────────────────────────────────────────────────────────────────────
ci-complete:
name: CI Complete
runs-on: ubuntu-24.04
timeout-minutes: 5
needs: [quality-gate]
if: always()
steps:
- name: Check pipeline passed
run: |
results='${{ toJson(needs.*.result) }}'
if echo "$results" | grep -qE '"(failure|cancelled|skipped)"'; then
echo "CI pipeline failed. Job results: $results"
if echo "$results" | grep -qE "$bad"; then
echo "CI gate failed. Job results: $results"
exit 1
fi
echo "All CI stages complete."
echo "All CI gates passed."
18 changes: 12 additions & 6 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,23 @@ name: CodeQL
# Advanced setup: GitHub's "default" code-scanning setup must be OFF in repo
# settings (Settings → Code security → Code scanning → Advanced) or Analyze errors.

# Triggers are scoped to the ONLY paths this analysis reads (the `actions`
# language surface below + its config): a PR touching only shell scripts or the
# Dockerfile cannot produce different CodeQL results, and previously still ran a
# full ~1.5-3 min analysis. Safe because "CI Complete" (in ci.yml), not CodeQL,
# is the required branch-protection check — a skipped run can't deadlock merges.
# The weekly schedule still covers query-pack updates on an unchanged repo.
on:
push:
branches: [main]
paths-ignore:
- "docs/**"
- "*.md"
paths:
- ".github/workflows/**"
- ".github/codeql/**"
pull_request:
branches: [main]
paths-ignore:
- "docs/**"
- "*.md"
paths:
- ".github/workflows/**"
- ".github/codeql/**"
schedule:
- cron: "0 3 * * 1" # weekly, Monday 03:00 UTC — full scan even without code changes

Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,36 @@ All notable changes to LockBox are documented here. The format is based on
[`VERSION`](VERSION); `sync.sh` stamps it (with each canonical file's SHA-256) into
every vendored copy, so a baked container self-identifies its egress-lock generation.

## [Unreleased]

### Changed — performance (TODO.md Pass 8 findings, no behavior changes intended)
- **sync.sh** — each vendored reference copy is generated once up front instead of
once (check) or twice (sync) per file *per target*, cutting 18–36 `gen_vendored`/
`shasum` runs to 3; this is per-commit latency via the pre-commit `--check`
(~1–2s on macOS). Atomic install + write-verify semantics kept.
- **audit.sh** — the secret scan is a single `git grep --cached -I -nE` over the
whole index instead of ~5–6 processes + a temp file per tracked file; same
index-not-worktree semantics, `file:line`-only reporting. Also fixed: a bare
`id_rsa`/`id_ed25519` at the repo *root* now trips the filename check (the old
`*/id_rsa` pattern required a leading directory).
- **launcher-common.sh** — plugin/statusline staging excludes `.git` at copy time
(`tar --exclude .git`) instead of copying multi-MB git dirs and deleting them,
on the interactive sandbox-start path.
- **Dockerfile** — the image-wide setuid/setgid strip moved before the
frequently-edited COPY block (a routine allowlist/script edit no longer re-pays
a ~10–40s full-filesystem traversal per rebuild); a scoped strip over the
COPY'd paths remains the last layer for the same defense-in-depth coverage.
- **bin/dev** — the proxy-wait folded into the lifecycle `container exec` (one
fewer container-CLI round-trip per launch, ~100–300ms on warm reuse); proxy
polls are 0.2s instead of 1s here and in `post-create.sh`.
- **CI** — the `egress-test` image build uses buildx with the GHA layer cache +
`SKIP_BUILD=1` (cache-hit builds drop from ~4–8 min to seconds); CodeQL
triggers scoped to the workflow/config paths it actually analyzes; the weekly
schedule now runs only the rule-DB–dependent scans (Trivy + gitleaks), with the
gate tolerating the by-design skips; Trivy scans once (SARIF + gating in one
pass); the `quality-gate`/`ci-complete` pair collapsed into a single
"CI Complete" job.

## [0.1.0] — 2026-07-07

First tagged release. Consolidates the 2026-07-07 hardening pass (the research
Expand Down
Loading
Loading