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
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,15 @@ jobs:
- run: npm run db:migrate
- run: npm run db:seed:ci
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e:smoke
- name: Integration E2E
run: ./scripts/integration-e2e.sh

- name: Adversarial tier
run: |
npm run build
node .next/standalone/server.js &
for _ in $(seq 1 60); do
curl -fsS http://localhost:3000 >/dev/null 2>&1 && break
sleep 2
done
bash ./scripts/adversarial.sh
5 changes: 5 additions & 0 deletions .harness/profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ requires:
- .cursor/hooks.json
- .harness/profile.yaml
- .harness/VERSION
- specs/threat-model.yaml
- docs/adr/0000-threat-model.md
hooks:
stop: verify-on-stop
commands:
verify: ./scripts/verify.sh
integration: ./scripts/integration-e2e.sh
adversarial: ./scripts/adversarial.sh
bundles_applied:
- cloud-guards
- verify-on-stop
Expand All @@ -38,7 +41,9 @@ extensions:
compose:
deny_compose_from_worktree: true
integration_cmd: ./scripts/integration-e2e.sh
adversarial_cmd: ./scripts/adversarial.sh
integration_ci_required: true
adversarial_ci_required: true
harness_disclosure:
schema: harness-disclosure/v1
scope: repo
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Full check including smoke E2E (signed-in smoke **skips locally** unless `PLAYWR

```bash
npm run verify:all
# Or integration script (same as CI e2e-smoke job):
./scripts/integration-e2e.sh
```

## Smoke E2E (must stay green on PR)
Expand Down
33 changes: 33 additions & 0 deletions docs/adr/0000-threat-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ADR 0000: Threat Model — Caller, Trust Boundary, Authentication

**Status:** Accepted
**Date:** 2026-07-13
**Product:** Autonomous EHS Management System

## Context

EHS inbound integration and cron routes accept authenticated callers only. Cooperative Playwright smoke validates configured flows. Tier-3 adversarial tests unauthenticated inbound integration posts.

See `specs/threat-model.yaml` and `scripts/adversarial.sh`.

## Decision

### Principals

| Principal | Routes |
|-----------|--------|
| `anonymous` | none on integration inbound |
| `integration_service` | bearer/integration secret on inbound webhook |
| `cron_scheduler` | `CRON_SECRET` bearer on cron routes |

### Trust boundary

`POST /api/integration/inbound` requires integration authentication before payload processing.

### Authentication

API route validates integration credentials; missing auth → `401 Unauthorized`.

## References

- `specs/threat-model.yaml`, `scripts/adversarial.sh`
28 changes: 28 additions & 0 deletions scripts/adversarial.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Tier-3 adversarial oracle — unauthenticated inbound integration.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

BASE="${ADVERSARIAL_BASE_URL:-http://localhost:3000}"

log() { echo ""; echo "== adversarial: $* =="; }

if ! curl -fsS "$BASE" >/dev/null 2>&1; then
echo "App not running at $BASE — start dev server or run integration-e2e first" >&2
exit 1
fi

# deny_case: anonymous_integration_inbound
log "anonymous_integration_inbound (expect 401)"
code=$(curl -s -o /tmp/ehs-adversarial.json -w "%{http_code}" \
-X POST "$BASE/api/integration/inbound" \
-H "Content-Type: application/json" \
-d '{"kind":"hris_membership_sync","organizationId":"00000000-0000-4000-8000-000000000001","workerEmail":"adversarial@example.com"}')
[[ "$code" == "401" ]]
grep -qi 'Unauthorized' /tmp/ehs-adversarial.json
echo " ${code} (as expected)"

echo ""
echo "adversarial: ok"
112 changes: 112 additions & 0 deletions scripts/check-threat-model.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# Validate threat-model artifact + adversarial tier for integration repos.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

errors=0
PROFILE="$(grep '^profile:' .harness/profile.yaml 2>/dev/null | awk '{print $2}' || echo solo)"
INTEGRATION_CMD="$(grep -A5 'commands:' .harness/profile.yaml 2>/dev/null | grep 'integration:' | head -1 | awk '{print $2}' || true)"

if [[ -z "$INTEGRATION_CMD" || "$INTEGRATION_CMD" == "null" ]]; then
for candidate in scripts/demo.sh scripts/integration-e2e.sh scripts/integration-smoke.sh scripts/smoke-test.sh; do
if [[ -f "$ROOT/$candidate" ]]; then
INTEGRATION_CMD="./$candidate"
break
fi
done
if [[ -z "$INTEGRATION_CMD" ]]; then
shopt -s nullglob
for candidate in "$ROOT"/scripts/smoke-test*.sh; do
INTEGRATION_CMD="./scripts/$(basename "$candidate")"
break
done
shopt -u nullglob
fi
fi

EXEMPT=0
case "$PROFILE" in
harness-lab|eval) EXEMPT=1 ;;
esac
if [[ -z "$INTEGRATION_CMD" || "$INTEGRATION_CMD" == "null" ]]; then
echo "check-threat-model: skip (no integration tier; profile=$PROFILE)"
exit 0
fi
if [[ "$EXEMPT" -eq 1 ]]; then
echo "check-threat-model: skip (exempt profile=$PROFILE)"
exit 0
fi

echo "== threat-model: integration=$INTEGRATION_CMD profile=$PROFILE =="

require_file() {
local path="$1"
if [[ ! -f "$path" ]]; then
echo "MISSING: $path" >&2
errors=$((errors + 1))
fi
}

require_file "specs/threat-model.yaml"
if [[ ! -f docs/adr/0000-threat-model.md && ! -f specs/decisions/0000-threat-model.md ]]; then
echo "MISSING: docs/adr/0000-threat-model.md or specs/decisions/0000-threat-model.md" >&2
errors=$((errors + 1))
fi
require_file "scripts/adversarial.sh"

if ! grep -q 'adversarial:' .harness/profile.yaml; then
echo "MISSING: requires.commands.adversarial in .harness/profile.yaml" >&2
errors=$((errors + 1))
fi

echo "== threat-model: yaml schema =="
python3 - <<'PY' || errors=$((errors + 1))
import pathlib, re, sys

root = pathlib.Path(".")
text = (root / "specs/threat-model.yaml").read_text()
if "schema: threat-model/v1" not in text:
print("BAD SCHEMA: specs/threat-model.yaml (want threat-model/v1)", file=sys.stderr)
raise SystemExit(1)
cells_part = text.split("deny_cases:")[0]
cell_ids = re.findall(r"^\s*- id: (\S+)", cells_part, re.M)
deny_part = text.split("deny_cases:")[-1] if "deny_cases:" in text else ""
deny_ids = re.findall(r"^\s*- id: (\S+)", deny_part, re.M)
deny_cells = re.findall(r"^\s*cell: (\S+)", deny_part, re.M)
if not cell_ids or not deny_ids:
print("EMPTY: cells or deny_cases", file=sys.stderr)
raise SystemExit(1)
cell_set = set(cell_ids)
for cid in deny_cells:
if cid not in cell_set:
print(f"BAD deny_case cell: {cid!r}", file=sys.stderr)
raise SystemExit(1)
adv = (root / "scripts/adversarial.sh").read_text()
for did in deny_ids:
if did not in adv:
print(f"TRACEABILITY: deny_case {did!r} not in adversarial.sh", file=sys.stderr)
raise SystemExit(1)
print(f"yaml ok: {len(cell_ids)} cells, {len(deny_ids)} deny_cases")
PY

ADR=""
[[ -f docs/adr/0000-threat-model.md ]] && ADR="docs/adr/0000-threat-model.md"
[[ -z "$ADR" && -f specs/decisions/0000-threat-model.md ]] && ADR="specs/decisions/0000-threat-model.md"
if [[ -n "$ADR" ]]; then
lower="$(tr '[:upper:]' '[:lower:]' < "$ADR")"
for kw in principal "trust bound" authentication; do
if ! grep -qi "$kw" <<< "$lower"; then
echo "ADR-0000 missing keyword: $kw ($ADR)" >&2
errors=$((errors + 1))
fi
done
fi

if [[ "$errors" -gt 0 ]]; then
echo "check-threat-model: FAILED ($errors errors)" >&2
exit 1
fi

echo "check-threat-model: ok"
5 changes: 5 additions & 0 deletions scripts/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ echo "==> verify (lint + typecheck + unit tests)"
npm run verify

echo "verify: ok (ci/web parity; add DATABASE_URL + test:e2e:smoke for full QA)"

if [[ -f ./scripts/check-threat-model.sh ]]; then
echo "==> threat model gate"
bash ./scripts/check-threat-model.sh
fi
23 changes: 23 additions & 0 deletions specs/threat-model.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
schema: threat-model/v1
product: Autonomous EHS Management System
updated: "2026-07-13"

cells:
- id: integration_inbound
method: POST
path: /api/integration/inbound
principals: [integration_service]
auth_mechanism: integration_bearer
trust_boundary: integration_authenticated
establishment_failure: "401 Unauthorized"

deny_cases:
- id: anonymous_integration_inbound
cell: integration_inbound
principal: anonymous
fixture:
path: /api/integration/inbound
body: '{}'
expect:
status: 401
reason_substring: Unauthorized
Loading