#!/usr/bin/env bash # Build the Phase 06 plugin runtime external-audit package. # # Produces a single tar.gz containing: # - the in-scope source code (a curated subset, NOT the whole repo) # - the threat model, attack-surface inventory, internal findings # - the RFP / sign-off template # - the manifest of what's included (with SHA-256 per file) # # Operator workflow: # 1. Run from the repo root: scripts/build-plugin-audit-package.sh # 2. Output lands in artifacts/plugin-audit//plugin-audit-.tar.gz # 3. Email the archive to the vendor per the RFP § 8. # 4. Keep the archive AND its checksum locally for forensics — the vendor # receives the same bytes. # # Dependencies: bash, tar, sha256sum (or shasum -a 256), git. macOS + Linux compatible. set -euo pipefail # ---- locate repo root ------------------------------------------------------ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" if [ -z "${REPO_ROOT}" ] || [ ! -d "${REPO_ROOT}/.git" ]; then echo "ERROR: must run inside the boilerplate git repo." >&2 exit 1 fi cd "${REPO_ROOT}" # ---- pick sha256 binary ---------------------------------------------------- if command -v sha256sum >/dev/null 2>&1; then SHA256="sha256sum" elif command -v shasum >/dev/null 2>&1; then SHA256="shasum -a 256" else echo "ERROR: need sha256sum or shasum on PATH." >&2 exit 1 fi # ---- destination ----------------------------------------------------------- SHORT_SHA="$(git rev-parse --short HEAD)" DATE_ISO="$(date -u +%Y-%m-%d)" OUT_DIR="artifacts/plugin-audit/${DATE_ISO}" ARCHIVE_NAME="plugin-audit-${SHORT_SHA}.tar.gz" ARCHIVE_PATH="${OUT_DIR}/${ARCHIVE_NAME}" MANIFEST_PATH="${OUT_DIR}/MANIFEST-${SHORT_SHA}.txt" STAGE_DIR="$(mktemp -d -t plugin-audit-XXXXXX)" trap 'rm -rf "${STAGE_DIR}"' EXIT mkdir -p "${OUT_DIR}" echo "→ Building plugin audit package" echo " repo: ${REPO_ROOT}" echo " HEAD: ${SHORT_SHA}" echo " out: ${ARCHIVE_PATH}" echo " stage: ${STAGE_DIR}" # ---- in-scope file list ---------------------------------------------------- # Append-only list. Keep aligned with docs/security/plugin-attack-surface.md § 1-6. # Each path is relative to the repo root. Glob patterns expanded by bash. INSCOPE_PATHS=( # Runtime "packages/plugin-runtime/src" "packages/plugin-runtime/package.json" "packages/plugin-runtime/tsconfig.json" # SDK "packages/plugin-sdk/src" "packages/plugin-sdk/examples" "packages/plugin-sdk/package.json" "packages/plugin-sdk/tsconfig.json" # Worker handlers "apps/worker/src/jobs/plugin-invoke.ts" "apps/worker/src/jobs/plugin-invoke.test.ts" "apps/worker/src/jobs/plugin-capability-use.ts" "apps/worker/src/jobs/plugin-capability-use.test.ts" "apps/worker/src/jobs/plugin-audit-retention.ts" "apps/worker/src/jobs/plugin-audit-retention.test.ts" # Web routes + lifecycle service. `$tenant` and `$id` here are literal # React Router path-segment names, NOT shell variables — we read array # elements without `eval` (see staging loop below) so they stay literal. 'apps/web/app/routes/api.tenants.$tenant.plugins.tsx' 'apps/web/app/routes/api.tenants.$tenant.plugins.test.ts' 'apps/web/app/routes/app.$tenant/settings/plugins.tsx' 'apps/web/app/routes/app.$tenant/settings/plugins.test.ts' 'apps/web/app/routes/app.$tenant/settings/plugins.$id.audit.tsx' "apps/web/app/modules/plugins" # CLI plugin commands "packages/cli/src/commands/plugins" # Prisma schema for plugin tables "prisma/schema/plugins.prisma" # Documentation "docs/security/plugin-threat-model.md" "docs/security/plugin-attack-surface.md" "docs/security/plugin-internal-findings.md" "docs/security/plugin-audit-rfp.md" "docs/security/plugin-runtime-signoff.md" "docs/architecture/plugin-system.md" "docs/architecture/plugin-hooks.md" "docs/plugins" "docs/improvement-plan/06-plugin-system" # Env example (operator-visible config surface) ".env.example" ) # ---- stage files ----------------------------------------------------------- # We copy paths preserving directory structure under ${STAGE_DIR}/code/. PKG_ROOT="${STAGE_DIR}/plugin-audit-${SHORT_SHA}" mkdir -p "${PKG_ROOT}/code" MISSING=() for path in "${INSCOPE_PATHS[@]}"; do # No eval / re-expansion — the path is taken literally. React Router # filenames contain literal `$tenant` / `$id` which we must NOT interpret # as shell variables. if [ ! -e "${path}" ]; then MISSING+=("${path}") continue fi parent_dir="$(dirname "${path}")" mkdir -p "${PKG_ROOT}/code/${parent_dir}" cp -R "${path}" "${PKG_ROOT}/code/${parent_dir}/" done # ---- include git HEAD metadata -------------------------------------------- git rev-parse HEAD > "${PKG_ROOT}/HEAD.txt" git log -1 --format="%H%n%an <%ae>%n%aI%n%s" > "${PKG_ROOT}/HEAD-meta.txt" git status --porcelain > "${PKG_ROOT}/WORKTREE-DIRTY.txt" if [ -s "${PKG_ROOT}/WORKTREE-DIRTY.txt" ]; then echo " WARNING: worktree is dirty — package reflects uncommitted state." >&2 echo " See ${PKG_ROOT}/WORKTREE-DIRTY.txt inside the archive." >&2 fi # ---- readme inside the archive -------------------------------------------- cat > "${PKG_ROOT}/README.txt" <<'PKG_README' Plugin Runtime External Audit Package ===================================== This archive contains the Phase 06 plugin runtime code surface plus the internal documentation we have produced. It is the input to the external pen-test described in docs/security/plugin-audit-rfp.md. What's inside ------------- code/ Curated subset of the repo (see scripts/build-plugin-audit-package.sh for the in-scope list). HEAD.txt git HEAD SHA the archive was built from. HEAD-meta.txt Committer + date + subject of HEAD. WORKTREE-DIRTY.txt Uncommitted changes at build time (should be empty). MANIFEST.txt SHA-256 of every file in this archive. Where to start reading ---------------------- 1. code/docs/security/plugin-audit-rfp.md — what we're asking for 2. code/docs/security/plugin-threat-model.md — what we worry about 3. code/docs/security/plugin-attack-surface.md — interface inventory 4. code/docs/security/plugin-internal-findings.md — what we've already found 5. code/packages/plugin-runtime/src — the runtime itself 6. code/apps/worker/src/jobs/plugin-*.ts — worker entry points 7. code/prisma/schema/plugins.prisma — data model Re-test workflow ---------------- After remediation lands on `main`, re-run the build script and send the new archive (with its delta vs. the previous SHORT_SHA). PKG_README # ---- per-file SHA-256 manifest -------------------------------------------- ( cd "${PKG_ROOT}" if [ "${SHA256}" = "sha256sum" ]; then find . -type f ! -name "MANIFEST.txt" -print0 | sort -z | xargs -0 sha256sum > MANIFEST.txt else # shasum -a 256 expects file args; use find -print0 + xargs find . -type f ! -name "MANIFEST.txt" -print0 | sort -z | xargs -0 shasum -a 256 > MANIFEST.txt fi ) # ---- pack ------------------------------------------------------------------ tar -czf "${ARCHIVE_PATH}" -C "${STAGE_DIR}" "plugin-audit-${SHORT_SHA}" ${SHA256} "${ARCHIVE_PATH}" > "${MANIFEST_PATH}" # ---- summary -------------------------------------------------------------- echo echo "✔ Archive built:" echo " ${ARCHIVE_PATH}" echo " $(wc -c < "${ARCHIVE_PATH}") bytes" echo echo "✔ Outer-archive SHA-256:" cat "${MANIFEST_PATH}" echo if [ ${#MISSING[@]} -gt 0 ]; then echo "⚠ Missing in-scope paths (omitted from archive):" for m in "${MISSING[@]}"; do echo " ${m}"; done fi echo echo "Next steps:" echo " 1. Verify the archive: tar -tzf ${ARCHIVE_PATH} | head" echo " 2. Email the archive to the vendor per docs/security/plugin-audit-rfp.md § 8." echo " 3. Record the archive checksum in your engagement tracker."