diff --git a/.github/workflows/make.yaml b/.github/workflows/make.yaml index 881500fce..ecec3f5e9 100644 --- a/.github/workflows/make.yaml +++ b/.github/workflows/make.yaml @@ -281,12 +281,6 @@ jobs: - uses: "./.github/actions/setup" with: node: "false" - - name: "Lint install.sh" - run: | - sudo apt-get update -qq - sudo apt-get install -y -qq shellcheck - sh -n cmd/probo-agent/installer/install.sh - shellcheck cmd/probo-agent/installer/install.sh - name: "Build probo-agent" env: CGO_ENABLED: "0" @@ -396,8 +390,12 @@ jobs: swift-version: "6.0" - uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0 - name: "Install SwiftLint" + env: + SWIFTLINT_VERSION: "0.65.0" + SWIFTLINT_SHA256: "79306a34e5c7cc55a220cd108cbb861dcad5f10138dcdf261e2624ae8b0a486b" run: | - curl -sL "https://github.com/realm/SwiftLint/releases/download/0.65.0/swiftlint_linux_amd64.zip" -o /tmp/swiftlint.zip + curl -sL "https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/swiftlint_linux_amd64.zip" -o /tmp/swiftlint.zip + echo "${SWIFTLINT_SHA256} /tmp/swiftlint.zip" | sha256sum -c - sudo unzip -o /tmp/swiftlint.zip -d /usr/local/bin sudo chmod +x /usr/local/bin/swiftlint swiftlint version @@ -415,6 +413,35 @@ jobs: swiftlint lint --config .swiftlint.yml --cache-path /tmp/swiftlint-cache 2>&1 | \ reviewdog -f=swiftlint -reporter=github-pr-review -filter-mode=nofilter -name="swiftlint" || true + lint-shell: + name: "lint-shell" + runs-on: "runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/extras=s3-cache" + permissions: + contents: "read" + steps: + - uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v6 + with: + submodules: recursive + - uses: "runs-on/action@d141ef83eb66d096ce8afc767e09115a65c63b60" # v2 + - name: "Install shellcheck and shfmt" + env: + SHELLCHECK_VERSION: "v0.11.0" + SHELLCHECK_SHA256: "8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198" + SHFMT_VERSION: "v3.13.1" + SHFMT_SHA256: "fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1" + run: | + curl -sL "https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/shellcheck-${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" -o /tmp/shellcheck.tar.xz + echo "${SHELLCHECK_SHA256} /tmp/shellcheck.tar.xz" | sha256sum -c - + tar -xJf /tmp/shellcheck.tar.xz -C /tmp + sudo install -m 755 "/tmp/shellcheck-${SHELLCHECK_VERSION}/shellcheck" /usr/local/bin/shellcheck + curl -sL "https://github.com/mvdan/sh/releases/download/${SHFMT_VERSION}/shfmt_${SHFMT_VERSION}_linux_amd64" -o /tmp/shfmt + echo "${SHFMT_SHA256} /tmp/shfmt" | sha256sum -c - + sudo install -m 755 /tmp/shfmt /usr/local/bin/shfmt + shellcheck --version + shfmt --version + - name: "Run make lint-shell" + run: make lint-shell + test: name: "test" runs-on: "runs-on=${{ github.run_id }}/runner=4cpu-linux-x64/extras=s3-cache" diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 000000000..9bc70853d --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,5 @@ +# Copyright (c) 2026 Probo Inc . +# SPDX-License-Identifier: MIT + +# Prefer shebang-detected dialect (sh vs bash). +external-sources=false diff --git a/GNUmakefile b/GNUmakefile index f265843f8..71a43fb56 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -25,6 +25,24 @@ SWIFTLINT_CONFIG ?= .swiftlint.yml swift_sources = $(shell find $(SWIFT_ENROLL_UI) \( -name '*.swift' ! -name '*.generated.swift' ! -path '*/.build/*' \) | sort) +SHELLCHECKCMD ?= shellcheck +SHFMTCMD ?= shfmt +SHFMTFLAGS ?= -i 2 -ci -bn + +# First-party shell scripts linted by lint-shell / fmt-shell (CI). +# Add every new first-party *.sh here; do not include vendored/submodule scripts. +SHELL_SCRIPTS := \ + cmd/probo-agent/installer/install.sh \ + cmd/probo-agent/installer/macos/build.sh \ + cmd/probo-agent/installer/macos/reinstall.sh \ + cmd/probo-agent/installer/macos/uninstall.sh \ + compose/postgres/01_probod.sh \ + contrib/lima/provision.sh \ + contrib/lima/sandbox.sh \ + contrib/merge-graphql-schema.sh \ + contrib/seed.sh \ + entrypoint.sh + DOCKER_BUILD_FLAGS?= DOCKER_BUILD= DOCKER_BUILDKIT=1 $(DOCKER) build $(DOCKER_BUILD_FLAGS) @@ -137,6 +155,15 @@ swift-lint: ## Lint Swift with SwiftLint @command -v $(SWIFTLINTCMD) >/dev/null 2>&1 || { echo "error: '$(SWIFTLINTCMD)' not found; install SwiftLint (e.g. brew install swiftlint)"; exit 1; } $(SWIFTLINTCMD) lint --strict --config $(SWIFTLINT_CONFIG) --cache-path .cache/swiftlint +.PHONY: lint-shell +lint-shell: ## Lint first-party shell scripts (shfmt + shellcheck) + @if [ -z "$(SHELL_SCRIPTS)" ]; then \ + echo "error: no shell scripts found"; \ + exit 1; \ + fi + $(SHFMTCMD) -d $(SHFMTFLAGS) $(SHELL_SCRIPTS) + $(SHELLCHECKCMD) $(SHELL_SCRIPTS) + .PHONY: vet vet: generate embed $(GO_VET) ./... @@ -418,6 +445,14 @@ fmt-swift: ## Format Swift enroll-ui sources $(SWIFTLINTCMD) lint --fix --config $(SWIFTLINT_CONFIG) --cache-path .cache/swiftlint; \ fi +.PHONY: fmt-shell +fmt-shell: ## Format first-party shell scripts with shfmt + @if [ -z "$(SHELL_SCRIPTS)" ]; then \ + echo "error: no shell scripts found"; \ + exit 1; \ + fi + $(SHFMTCMD) -w $(SHFMTFLAGS) $(SHELL_SCRIPTS) + .PHONY: clean clean: ## Clean the project (node_modules and build artifacts) $(RM) -rf bin/* diff --git a/cmd/probo-agent/installer/install.sh b/cmd/probo-agent/installer/install.sh index 44ade86f6..154cb54fb 100755 --- a/cmd/probo-agent/installer/install.sh +++ b/cmd/probo-agent/installer/install.sh @@ -47,7 +47,7 @@ GITHUB_RELEASES_URL="https://github.com/getprobo/probo/releases/download" # Injected at release time by .github/workflows/release-probo-agent.yaml RELEASE_TAG="__PROBO_AGENT_RELEASE_TAG__" if [ -n "${PROBO_AGENT_RELEASE_TAG:-}" ]; then - RELEASE_TAG="$PROBO_AGENT_RELEASE_TAG" + RELEASE_TAG="$PROBO_AGENT_RELEASE_TAG" fi RELEASE_BASE="${PROBO_AGENT_RELEASE_BASE:-}" @@ -58,25 +58,25 @@ NO_AUTO_UPDATE="${PROBO_NO_AUTO_UPDATE:-}" SKIP_SERVICE=false die() { - echo "error: $*" >&2 - exit 1 + echo "error: $*" >&2 + exit 1 } can_prompt() { - [ -t 0 ] && return 0 - [ -r /dev/tty ] && [ -w /dev/tty ] + [ -t 0 ] && return 0 + [ -r /dev/tty ] && [ -w /dev/tty ] } read_user() { - if [ -t 0 ]; then - IFS= read -r "$1" - else - IFS= read -r "$1" < /dev/tty - fi + if [ -t 0 ]; then + IFS= read -r "$1" + else + IFS= read -r "$1" /dev/null 2>&1; then - die "required command not found: $1" - fi + if ! command -v "$1" >/dev/null 2>&1; then + die "required command not found: $1" + fi } detect_platform() { - os="$(uname -s)" - arch="$(uname -m)" + os="$(uname -s)" + arch="$(uname -m)" - case "$os" in - Darwin) os_label="Darwin" ;; - Linux) os_label="Linux" ;; - FreeBSD) os_label="Freebsd" ;; - *) die "unsupported operating system: $os (Darwin, Linux, and FreeBSD only)" ;; - esac + case "$os" in + Darwin) os_label="Darwin" ;; + Linux) os_label="Linux" ;; + FreeBSD) os_label="Freebsd" ;; + *) die "unsupported operating system: $os (Darwin, Linux, and FreeBSD only)" ;; + esac - case "$arch" in - x86_64 | amd64) arch_label="x86_64" ;; - arm64 | aarch64) arch_label="arm64" ;; - *) die "unsupported CPU architecture: $arch" ;; - esac + case "$arch" in + x86_64 | amd64) arch_label="x86_64" ;; + arm64 | aarch64) arch_label="arm64" ;; + *) die "unsupported CPU architecture: $arch" ;; + esac - archive_dir="probo-agent_${os_label}_${arch_label}" - archive_name="${archive_dir}.tar.gz" + archive_dir="probo-agent_${os_label}_${arch_label}" + archive_name="${archive_dir}.tar.gz" } sha256_file() { - file="$1" - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$file" | awk '{print $1}' - elif command -v shasum >/dev/null 2>&1; then - shasum -a 256 "$file" | awk '{print $1}' - elif command -v sha256 >/dev/null 2>&1; then - sha256 -q "$file" - else - die "no sha256 tool found (need sha256sum, shasum, or sha256)" - fi + file="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + elif command -v sha256 >/dev/null 2>&1; then + sha256 -q "$file" + else + die "no sha256 tool found (need sha256sum, shasum, or sha256)" + fi } verify_embedded_checksum() { - case "${PROBO_AGENT_SKIP_CHECKSUM_VERIFY:-}" in - 1 | true | TRUE | yes | YES) return 0 ;; - esac + case "${PROBO_AGENT_SKIP_CHECKSUM_VERIFY:-}" in + 1 | true | TRUE | yes | YES) return 0 ;; + esac - archive_file="$1" - archive_basename="$(basename "$archive_file")" + archive_file="$1" + archive_basename="$(basename "$archive_file")" - expected="$( - embedded_checksums | awk -v name="$archive_basename" ' + expected="$( + embedded_checksums | awk -v name="$archive_basename" ' $0 ~ /^#/ { next } $2 == name { print $1; exit } ' - )" - if [ -z "$expected" ]; then - die "archive ${archive_basename} not found in embedded release checksums" - fi + )" + if [ -z "$expected" ]; then + die "archive ${archive_basename} not found in embedded release checksums" + fi - actual="$(sha256_file "$archive_file")" - if [ "$expected" != "$actual" ]; then - die "checksum mismatch for ${archive_file}" - fi + actual="$(sha256_file "$archive_file")" + if [ "$expected" != "$actual" ]; then + die "checksum mismatch for ${archive_file}" + fi } read_secret() { - prompt_text="$1" - printf '%s' "$prompt_text" - if command -v stty >/dev/null 2>&1; then - if [ -t 0 ]; then - old_stty="$(stty -g 2>/dev/null || true)" - stty -echo 2>/dev/null || true - IFS= read -r REPLY || REPLY="" - printf '\n' - if [ -n "${old_stty:-}" ]; then - stty "$old_stty" 2>/dev/null || stty echo 2>/dev/null || true - fi - else - old_stty="$(stty -g < /dev/tty 2>/dev/null || true)" - stty -echo < /dev/tty 2>/dev/null || true - IFS= read -r REPLY < /dev/tty || REPLY="" - printf '\n' >/dev/tty - if [ -n "${old_stty:-}" ]; then - stty "$old_stty" < /dev/tty 2>/dev/null || stty echo < /dev/tty 2>/dev/null || true - fi - fi - else - read_user REPLY - fi - ENROLLMENT_TOKEN="$REPLY" + prompt_text="$1" + printf '%s' "$prompt_text" + if command -v stty >/dev/null 2>&1; then + if [ -t 0 ]; then + old_stty="$(stty -g 2>/dev/null || true)" + stty -echo 2>/dev/null || true + IFS= read -r REPLY || REPLY="" + printf '\n' + if [ -n "${old_stty:-}" ]; then + stty "$old_stty" 2>/dev/null || stty echo 2>/dev/null || true + fi + else + old_stty="$(stty -g /dev/null || true)" + stty -echo /dev/null || true + IFS= read -r REPLY /dev/tty + if [ -n "${old_stty:-}" ]; then + stty "$old_stty" /dev/null || stty echo /dev/null || true + fi + fi + else + read_user REPLY + fi + ENROLLMENT_TOKEN="$REPLY" } prompt_server_url() { - if [ -n "$SERVER_URL" ]; then - return 0 - fi - if ! can_prompt; then - die "PROBO_SERVER_URL is required in non-interactive mode" - fi + if [ -n "$SERVER_URL" ]; then + return 0 + fi + if ! can_prompt; then + die "PROBO_SERVER_URL is required in non-interactive mode" + fi - printf '\nProbo server URL:\n' - printf ' 1) https://us.probo.com (United States)\n' - printf ' 2) https://eu.probo.com (European Union)\n' - printf ' 3) Enter a custom URL\n' - printf 'Choice [1]: ' - read_user choice + printf '\nProbo server URL:\n' + printf ' 1) https://us.probo.com (United States)\n' + printf ' 2) https://eu.probo.com (European Union)\n' + printf ' 3) Enter a custom URL\n' + printf 'Choice [1]: ' + read_user choice - case "${choice:-1}" in - 1 | "") SERVER_URL="https://us.probo.com" ;; - 2) SERVER_URL="https://eu.probo.com" ;; - 3) - printf 'Server URL: ' - read_user SERVER_URL - ;; - *) SERVER_URL="$choice" ;; - esac + case "${choice:-1}" in + 1 | "") SERVER_URL="https://us.probo.com" ;; + 2) SERVER_URL="https://eu.probo.com" ;; + 3) + printf 'Server URL: ' + read_user SERVER_URL + ;; + *) SERVER_URL="$choice" ;; + esac - if [ -z "$SERVER_URL" ]; then - die "server URL is required" - fi + if [ -z "$SERVER_URL" ]; then + die "server URL is required" + fi } prompt_enrollment_token() { - if [ -n "$ENROLLMENT_TOKEN" ]; then - return 0 - fi - if ! can_prompt; then - die "PROBO_ENROLLMENT_TOKEN is required in non-interactive mode" - fi + if [ -n "$ENROLLMENT_TOKEN" ]; then + return 0 + fi + if ! can_prompt; then + die "PROBO_ENROLLMENT_TOKEN is required in non-interactive mode" + fi - read_secret "Enrollment token: " - if [ -z "$ENROLLMENT_TOKEN" ]; then - die "enrollment token is required" - fi + read_secret "Enrollment token: " + if [ -z "$ENROLLMENT_TOKEN" ]; then + die "enrollment token is required" + fi } parse_args() { - while [ $# -gt 0 ]; do - case "$1" in - --server) - [ $# -ge 2 ] || die "--server requires a value" - SERVER_URL="$2" - shift 2 - ;; - --enrollment-token) - [ $# -ge 2 ] || die "--enrollment-token requires a value" - ENROLLMENT_TOKEN="$2" - shift 2 - ;; - --no-auto-update) - NO_AUTO_UPDATE=true - shift - ;; - --skip-service) - SKIP_SERVICE=true - shift - ;; - --dir) - [ $# -ge 2 ] || die "--dir requires a value" - STATE_DIR="$2" - shift 2 - ;; - -h | --help) - usage - exit 0 - ;; - *) - die "unknown option: $1 (try --help)" - ;; - esac - done + while [ $# -gt 0 ]; do + case "$1" in + --server) + [ $# -ge 2 ] || die "--server requires a value" + SERVER_URL="$2" + shift 2 + ;; + --enrollment-token) + [ $# -ge 2 ] || die "--enrollment-token requires a value" + ENROLLMENT_TOKEN="$2" + shift 2 + ;; + --no-auto-update) + NO_AUTO_UPDATE=true + shift + ;; + --skip-service) + SKIP_SERVICE=true + shift + ;; + --dir) + [ $# -ge 2 ] || die "--dir requires a value" + STATE_DIR="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + die "unknown option: $1 (try --help)" + ;; + esac + done } run_agent_install() { - set -- --server "$SERVER_URL" --enrollment-token "$ENROLLMENT_TOKEN" - if [ -n "$STATE_DIR" ]; then - set -- "$@" --dir "$STATE_DIR" - fi - case "$NO_AUTO_UPDATE" in - 1 | true | TRUE | yes | YES) set -- "$@" --no-auto-update ;; - esac - case "$SKIP_SERVICE" in - 1 | true | TRUE | yes | YES) set -- "$@" --skip-service ;; - esac - "$BINARY_PATH" install "$@" + set -- --server "$SERVER_URL" --enrollment-token "$ENROLLMENT_TOKEN" + if [ -n "$STATE_DIR" ]; then + set -- "$@" --dir "$STATE_DIR" + fi + case "$NO_AUTO_UPDATE" in + 1 | true | TRUE | yes | YES) set -- "$@" --no-auto-update ;; + esac + case "$SKIP_SERVICE" in + 1 | true | TRUE | yes | YES) set -- "$@" --skip-service ;; + esac + "$BINARY_PATH" install "$@" } main() { - parse_args "$@" + parse_args "$@" - if [ "$(id -u)" -ne 0 ]; then - die "this installer must run as root; re-run with: curl -fsSL \"…/install.sh\" | sudo sh" - fi + if [ "$(id -u)" -ne 0 ]; then + die "this installer must run as root; re-run with: curl -fsSL \"…/install.sh\" | sudo sh" + fi - require_cmd curl - require_cmd tar - require_cmd install + require_cmd curl + require_cmd tar + require_cmd install - detect_platform - resolve_embedded_release + detect_platform + resolve_embedded_release - workdir="$(mktemp -d "${TMPDIR:-/tmp}/probo-agent-install.XXXXXX")" - trap 'rm -rf "$workdir"' EXIT INT HUP TERM + workdir="$(mktemp -d "${TMPDIR:-/tmp}/probo-agent-install.XXXXXX")" + trap 'rm -rf "$workdir"' EXIT INT HUP TERM - printf 'Downloading probo-agent %s …\n' "$archive_name" + printf 'Downloading probo-agent %s …\n' "$archive_name" - curl -fsSL "${RELEASE_BASE}/${archive_name}" -o "${workdir}/${archive_name}" + curl -fsSL "${RELEASE_BASE}/${archive_name}" -o "${workdir}/${archive_name}" - verify_embedded_checksum "${workdir}/${archive_name}" + verify_embedded_checksum "${workdir}/${archive_name}" - tar -xzf "${workdir}/${archive_name}" -C "$workdir" - if [ ! -f "${workdir}/${archive_dir}/probo-agent" ]; then - die "archive did not contain probo-agent binary" - fi + tar -xzf "${workdir}/${archive_name}" -C "$workdir" + if [ ! -f "${workdir}/${archive_dir}/probo-agent" ]; then + die "archive did not contain probo-agent binary" + fi - install -m 0755 "${workdir}/${archive_dir}/probo-agent" "$BINARY_PATH" - printf 'Installed %s\n' "$BINARY_PATH" + install -m 0755 "${workdir}/${archive_dir}/probo-agent" "$BINARY_PATH" + printf 'Installed %s\n' "$BINARY_PATH" - prompt_server_url - prompt_enrollment_token + prompt_server_url + prompt_enrollment_token - printf 'Enrolling device …\n' - if run_agent_install; then - enroll_ok=true - else - enroll_ok=false - fi + printf 'Enrolling device …\n' + if run_agent_install; then + enroll_ok=true + else + enroll_ok=false + fi - if [ "$enroll_ok" = true ]; then - case "$SKIP_SERVICE" in - 1 | true | TRUE | yes | YES) - printf 'Device enrolled (service installation skipped).\n' - ;; - *) - printf 'Device enrolled and service installed.\n' - ;; - esac - else - printf 'warning: probo-agent install failed; binary is at %s\n' "$BINARY_PATH" >&2 - printf 'Re-run: %s install --server … --enrollment-token …\n' "$BINARY_PATH" >&2 - exit 1 - fi + if [ "$enroll_ok" = true ]; then + case "$SKIP_SERVICE" in + 1 | true | TRUE | yes | YES) + printf 'Device enrolled (service installation skipped).\n' + ;; + *) + printf 'Device enrolled and service installed.\n' + ;; + esac + else + printf 'warning: probo-agent install failed; binary is at %s\n' "$BINARY_PATH" >&2 + printf 'Re-run: %s install --server … --enrollment-token …\n' "$BINARY_PATH" >&2 + exit 1 + fi } main "$@" diff --git a/cmd/probo-agent/installer/macos/build.sh b/cmd/probo-agent/installer/macos/build.sh index 2ffa9bf42..a36c7a281 100755 --- a/cmd/probo-agent/installer/macos/build.sh +++ b/cmd/probo-agent/installer/macos/build.sh @@ -56,97 +56,116 @@ APPLE_TEAM_ID="${APPLE_TEAM_ID:-}" NOTARYTOOL_KEYCHAIN_PROFILE="${NOTARYTOOL_KEYCHAIN_PROFILE:-probo-agent-notary}" usage() { - sed -ne '/^#/!q; s/^# \{0,1\}//; 2,$ p' < "$0" + sed -ne '/^#/!q; s/^# \{0,1\}//; 2,$ p' <"$0" } while [ $# -gt 0 ]; do - case "$1" in - --binary) BINARY="$2"; shift 2 ;; - --version) VERSION="$2"; shift 2 ;; - --output) OUTPUT="$2"; shift 2 ;; - --identifier) IDENTIFIER="$2"; shift 2 ;; - -h|--help) usage; exit 0 ;; - *) echo "unknown flag: $1" >&2; usage >&2; exit 2 ;; - esac + case "$1" in + --binary) + BINARY="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + --output) + OUTPUT="$2" + shift 2 + ;; + --identifier) + IDENTIFIER="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + echo "unknown flag: $1" >&2 + usage >&2 + exit 2 + ;; + esac done if [ -z "${BINARY}" ] || [ ! -x "${BINARY}" ]; then - echo "error: --binary is required and must be executable" >&2 - exit 2 + echo "error: --binary is required and must be executable" >&2 + exit 2 fi # Distribution.xml advertises hostArchitectures=arm64,x86_64. Refuse a # binary that lacks either slice so Installer cannot install on a CPU # the agent cannot run on. if ! command -v lipo >/dev/null 2>&1; then - echo "error: lipo is required to validate --binary architecture (run on macOS)" >&2 - exit 1 + echo "error: lipo is required to validate --binary architecture (run on macOS)" >&2 + exit 1 fi BINARY_ARCHS="$(lipo -archs "${BINARY}")" has_arm64=false has_x86_64=false for arch_slice in ${BINARY_ARCHS}; do - case "${arch_slice}" in - arm64) has_arm64=true ;; - x86_64) has_x86_64=true ;; - esac + case "${arch_slice}" in + arm64) has_arm64=true ;; + x86_64) has_x86_64=true ;; + esac done if [ "${has_arm64}" != true ] || [ "${has_x86_64}" != true ]; then - echo "error: --binary must be a fat binary with arm64 and x86_64 slices (got: ${BINARY_ARCHS}); use lipo -create" >&2 - exit 2 + echo "error: --binary must be a fat binary with arm64 and x86_64 slices (got: ${BINARY_ARCHS}); use lipo -create" >&2 + exit 2 fi if [ -z "${VERSION}" ]; then - VERSION="$(cat "${REPO_ROOT}/cmd/probo-agent/VERSION")" + VERSION="$(cat "${REPO_ROOT}/cmd/probo-agent/VERSION")" fi if [ -z "${OUTPUT}" ]; then - mkdir -p "${REPO_ROOT}/dist" - OUTPUT="${REPO_ROOT}/dist/probo-agent_${VERSION}_darwin.pkg" + mkdir -p "${REPO_ROOT}/dist" + OUTPUT="${REPO_ROOT}/dist/probo-agent_${VERSION}_darwin.pkg" fi if ! command -v pkgbuild >/dev/null 2>&1 || ! command -v productbuild >/dev/null 2>&1; then - echo "error: pkgbuild and productbuild are required (run on macOS)" >&2 - exit 1 + echo "error: pkgbuild and productbuild are required (run on macOS)" >&2 + exit 1 fi if ! command -v swift >/dev/null 2>&1; then - echo "error: swift is required to build Probo Agent.app (run on macOS)" >&2 - exit 1 + echo "error: swift is required to build Probo Agent.app (run on macOS)" >&2 + exit 1 fi if [ -z "${CODESIGN_IDENTITY}" ]; then - echo "error: CODESIGN_IDENTITY is required (privileged helper must be signed)" >&2 - exit 2 + echo "error: CODESIGN_IDENTITY is required (privileged helper must be signed)" >&2 + exit 2 fi if [ -z "${APPLE_TEAM_ID}" ]; then - echo "error: APPLE_TEAM_ID is required (SMAuthorizedClients team requirement)" >&2 - exit 2 + echo "error: APPLE_TEAM_ID is required (SMAuthorizedClients team requirement)" >&2 + exit 2 fi notarize_enabled=false if [ -n "${APPLE_ID}" ] && [ -n "${APPLE_ID_PASSWORD}" ]; then - notarize_enabled=true + notarize_enabled=true fi if [ "${notarize_enabled}" = true ] && [ -z "${INSTALLER_IDENTITY}" ]; then - echo "error: notarization requires INSTALLER_IDENTITY" >&2 - exit 2 + echo "error: notarization requires INSTALLER_IDENTITY" >&2 + exit 2 fi codesign_runtime() { - local path="$1" - codesign \ - --force \ - --options runtime \ - --timestamp \ - --sign "${CODESIGN_IDENTITY}" \ - "${path}" - codesign --verify --verbose=2 "${path}" + local path="$1" + codesign \ + --force \ + --options runtime \ + --timestamp \ + --sign "${CODESIGN_IDENTITY}" \ + "${path}" + codesign --verify --verbose=2 "${path}" } client_requirement() { - printf 'anchor apple generic and identifier "com.probo.agent.url-handler" and certificate leaf[subject.OU] = "%s"' "${APPLE_TEAM_ID}" + printf 'anchor apple generic and identifier "com.probo.agent.url-handler" and certificate leaf[subject.OU] = "%s"' "${APPLE_TEAM_ID}" } team_id_option() { - printf '"%s"' "${APPLE_TEAM_ID}" + printf '"%s"' "${APPLE_TEAM_ID}" } # Build AppIcon.icns from the single committed master PNG @@ -154,213 +173,213 @@ team_id_option() { # pad-then-resize pipeline. Writes under STAGE; does not touch the # source tree. generate_app_icon_icns() { - local icon_original="$1" - local icns_out="$2" - local icon_dir padded tmp iconset + local icon_original="$1" + local icns_out="$2" + local icon_dir padded tmp iconset - if [ ! -f "${icon_original}" ]; then - echo "error: missing app icon source ${icon_original}" >&2 - exit 1 - fi + if [ ! -f "${icon_original}" ]; then + echo "error: missing app icon source ${icon_original}" >&2 + exit 1 + fi - icon_dir="${STAGE}/app-icon" - padded="${icon_dir}/icon-padded.png" - tmp="${icon_dir}/icon-padded.tmp.png" - iconset="${icon_dir}/AppIcon.iconset" - rm -rf "${icon_dir}" - mkdir -p "${iconset}" + icon_dir="${STAGE}/app-icon" + padded="${icon_dir}/icon-padded.png" + tmp="${icon_dir}/icon-padded.tmp.png" + iconset="${icon_dir}/AppIcon.iconset" + rm -rf "${icon_dir}" + mkdir -p "${iconset}" - # 10% padding on all sides (960 content inside 1200 canvas). - sips -z 960 960 "${icon_original}" --out "${tmp}" >/dev/null - sips --padToHeightWidth 1200 1200 "${tmp}" --out "${padded}" >/dev/null - rm -f "${tmp}" + # 10% padding on all sides (960 content inside 1200 canvas). + sips -z 960 960 "${icon_original}" --out "${tmp}" >/dev/null + sips --padToHeightWidth 1200 1200 "${tmp}" --out "${padded}" >/dev/null + rm -f "${tmp}" - # Write through temp names: sips mishandles @2x suffixes in --out paths. - sips -z 16 16 "${padded}" --out "${icon_dir}/16.png" >/dev/null - sips -z 32 32 "${padded}" --out "${icon_dir}/32.png" >/dev/null - sips -z 64 64 "${padded}" --out "${icon_dir}/64.png" >/dev/null - sips -z 128 128 "${padded}" --out "${icon_dir}/128.png" >/dev/null - sips -z 256 256 "${padded}" --out "${icon_dir}/256.png" >/dev/null - sips -z 512 512 "${padded}" --out "${icon_dir}/512.png" >/dev/null - sips -z 1024 1024 "${padded}" --out "${icon_dir}/1024.png" >/dev/null + # Write through temp names: sips mishandles @2x suffixes in --out paths. + sips -z 16 16 "${padded}" --out "${icon_dir}/16.png" >/dev/null + sips -z 32 32 "${padded}" --out "${icon_dir}/32.png" >/dev/null + sips -z 64 64 "${padded}" --out "${icon_dir}/64.png" >/dev/null + sips -z 128 128 "${padded}" --out "${icon_dir}/128.png" >/dev/null + sips -z 256 256 "${padded}" --out "${icon_dir}/256.png" >/dev/null + sips -z 512 512 "${padded}" --out "${icon_dir}/512.png" >/dev/null + sips -z 1024 1024 "${padded}" --out "${icon_dir}/1024.png" >/dev/null - # Build @2x names via concatenation so the shell never treats @ as a - # glob qualifier (and sips is never asked to write those paths). - local at2x - at2x='@2x.png' - cp "${icon_dir}/16.png" "${iconset}/icon_16x16.png" - cp "${icon_dir}/32.png" "${iconset}/icon_16x16${at2x}" - cp "${icon_dir}/32.png" "${iconset}/icon_32x32.png" - cp "${icon_dir}/64.png" "${iconset}/icon_32x32${at2x}" - cp "${icon_dir}/128.png" "${iconset}/icon_128x128.png" - cp "${icon_dir}/256.png" "${iconset}/icon_128x128${at2x}" - cp "${icon_dir}/256.png" "${iconset}/icon_256x256.png" - cp "${icon_dir}/512.png" "${iconset}/icon_256x256${at2x}" - cp "${icon_dir}/512.png" "${iconset}/icon_512x512.png" - cp "${icon_dir}/1024.png" "${iconset}/icon_512x512${at2x}" + # Build @2x names via concatenation so the shell never treats @ as a + # glob qualifier (and sips is never asked to write those paths). + local at2x + at2x='@2x.png' + cp "${icon_dir}/16.png" "${iconset}/icon_16x16.png" + cp "${icon_dir}/32.png" "${iconset}/icon_16x16${at2x}" + cp "${icon_dir}/32.png" "${iconset}/icon_32x32.png" + cp "${icon_dir}/64.png" "${iconset}/icon_32x32${at2x}" + cp "${icon_dir}/128.png" "${iconset}/icon_128x128.png" + cp "${icon_dir}/256.png" "${iconset}/icon_128x128${at2x}" + cp "${icon_dir}/256.png" "${iconset}/icon_256x256.png" + cp "${icon_dir}/512.png" "${iconset}/icon_256x256${at2x}" + cp "${icon_dir}/512.png" "${iconset}/icon_512x512.png" + cp "${icon_dir}/1024.png" "${iconset}/icon_512x512${at2x}" - iconutil -c icns "${iconset}" -o "${icns_out}" + iconutil -c icns "${iconset}" -o "${icns_out}" } # Build Probo Agent.app (URL handler + embedded privileged helper) into # parent_dir. Signs nested Mach-Os then the .app bundle (bottom-up). build_probo_agent_app() { - local parent_dir="$1" - local build_dir render_dir - local helper_info_plist helper_launchd_plist - local helper_binary url_handler_binary bin_dir - local app_root contents macos resources launch_services launch_daemons - local plist embedded_helper embedded_launchd - local helper_requirement app_icon_original app_icon_icns - local -a helper_linker_flags swift_arch_args + local parent_dir="$1" + local build_dir render_dir + local helper_info_plist helper_launchd_plist + local helper_binary url_handler_binary bin_dir + local app_root contents macos resources launch_services launch_daemons + local plist embedded_helper embedded_launchd + local helper_requirement app_icon_original app_icon_icns + local -a helper_linker_flags swift_arch_args - build_dir="${STAGE}/enroll-ui-build" - render_dir="${build_dir}/rendered" - mkdir -p "${render_dir}" + build_dir="${STAGE}/enroll-ui-build" + render_dir="${build_dir}/rendered" + mkdir -p "${render_dir}" - swift_arch_args=(--arch arm64 --arch x86_64) + swift_arch_args=(--arch arm64 --arch x86_64) - sed \ - -e "s|@@VERSION@@|${VERSION}|g" \ - "${ENROLL_UI_DIR}/Shared/HelperVersion.generated.swift.tmpl" \ - > "${ENROLL_UI_DIR}/Shared/HelperVersion.generated.swift" + sed \ + -e "s|@@VERSION@@|${VERSION}|g" \ + "${ENROLL_UI_DIR}/Shared/HelperVersion.generated.swift.tmpl" \ + >"${ENROLL_UI_DIR}/Shared/HelperVersion.generated.swift" - sed \ - -e "s|@@TEAM_ID_OPTION@@|$(team_id_option)|g" \ - "${ENROLL_UI_DIR}/Shared/SigningConstants.generated.swift.tmpl" \ - > "${ENROLL_UI_DIR}/Shared/SigningConstants.generated.swift" + sed \ + -e "s|@@TEAM_ID_OPTION@@|$(team_id_option)|g" \ + "${ENROLL_UI_DIR}/Shared/SigningConstants.generated.swift.tmpl" \ + >"${ENROLL_UI_DIR}/Shared/SigningConstants.generated.swift" - helper_info_plist="${render_dir}/helper-info.plist" - helper_launchd_plist="${render_dir}/helper-launchd.plist" + helper_info_plist="${render_dir}/helper-info.plist" + helper_launchd_plist="${render_dir}/helper-launchd.plist" - sed \ - -e "s|@@VERSION@@|${VERSION}|g" \ - -e "s|@@CLIENT_DESIGNATED_REQUIREMENT@@|$(client_requirement)|g" \ - "${ENROLL_UI_DIR}/HelperTool/Info.plist.tmpl" > "${helper_info_plist}" + sed \ + -e "s|@@VERSION@@|${VERSION}|g" \ + -e "s|@@CLIENT_DESIGNATED_REQUIREMENT@@|$(client_requirement)|g" \ + "${ENROLL_UI_DIR}/HelperTool/Info.plist.tmpl" >"${helper_info_plist}" - cp "${ENROLL_UI_DIR}/HelperTool/Launchd.plist.tmpl" "${helper_launchd_plist}" + cp "${ENROLL_UI_DIR}/HelperTool/Launchd.plist.tmpl" "${helper_launchd_plist}" - helper_linker_flags=( - -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist - -Xlinker "${helper_info_plist}" - -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __launchd_plist - -Xlinker "${helper_launchd_plist}" - ) + helper_linker_flags=( + -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist + -Xlinker "${helper_info_plist}" + -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __launchd_plist + -Xlinker "${helper_launchd_plist}" + ) - pushd "${ENROLL_UI_DIR}" >/dev/null - swift build -c release "${swift_arch_args[@]}" \ - --scratch-path "${build_dir}/swift" \ - --product "${HELPER_LABEL}" \ - "${helper_linker_flags[@]}" + pushd "${ENROLL_UI_DIR}" >/dev/null + swift build -c release "${swift_arch_args[@]}" \ + --scratch-path "${build_dir}/swift" \ + --product "${HELPER_LABEL}" \ + "${helper_linker_flags[@]}" - swift build -c release "${swift_arch_args[@]}" \ - --scratch-path "${build_dir}/swift" \ - --product "${URL_HANDLER_NAME}" + swift build -c release "${swift_arch_args[@]}" \ + --scratch-path "${build_dir}/swift" \ + --product "${URL_HANDLER_NAME}" - bin_dir="$(swift build -c release "${swift_arch_args[@]}" \ - --scratch-path "${build_dir}/swift" --show-bin-path)" - helper_binary="${bin_dir}/${HELPER_LABEL}" - url_handler_binary="${bin_dir}/${URL_HANDLER_NAME}" - popd >/dev/null + bin_dir="$(swift build -c release "${swift_arch_args[@]}" \ + --scratch-path "${build_dir}/swift" --show-bin-path)" + helper_binary="${bin_dir}/${HELPER_LABEL}" + url_handler_binary="${bin_dir}/${URL_HANDLER_NAME}" + popd >/dev/null - if [ ! -x "${helper_binary}" ] || [ ! -x "${url_handler_binary}" ]; then - echo "error: expected release binaries were not produced" >&2 - exit 1 - fi + if [ ! -x "${helper_binary}" ] || [ ! -x "${url_handler_binary}" ]; then + echo "error: expected release binaries were not produced" >&2 + exit 1 + fi - app_root="${parent_dir}/${APP_NAME}" - contents="${app_root}/Contents" - macos="${contents}/MacOS" - resources="${contents}/Resources" - launch_services="${contents}/Library/LaunchServices" - launch_daemons="${contents}/Library/LaunchDaemons" - plist="${contents}/Info.plist" - embedded_helper="${launch_services}/${HELPER_LABEL}" - embedded_launchd="${launch_daemons}/${HELPER_LABEL}.plist" - app_icon_original="${ENROLL_UI_DIR}/Resources/icon-original.png" - app_icon_icns="${STAGE}/AppIcon.icns" + app_root="${parent_dir}/${APP_NAME}" + contents="${app_root}/Contents" + macos="${contents}/MacOS" + resources="${contents}/Resources" + launch_services="${contents}/Library/LaunchServices" + launch_daemons="${contents}/Library/LaunchDaemons" + plist="${contents}/Info.plist" + embedded_helper="${launch_services}/${HELPER_LABEL}" + embedded_launchd="${launch_daemons}/${HELPER_LABEL}.plist" + app_icon_original="${ENROLL_UI_DIR}/Resources/icon-original.png" + app_icon_icns="${STAGE}/AppIcon.icns" - rm -rf "${app_root}" - mkdir -p "${macos}" "${resources}" "${launch_services}" "${launch_daemons}" + rm -rf "${app_root}" + mkdir -p "${macos}" "${resources}" "${launch_services}" "${launch_daemons}" - generate_app_icon_icns "${app_icon_original}" "${app_icon_icns}" + generate_app_icon_icns "${app_icon_original}" "${app_icon_icns}" - install -m 0755 "${url_handler_binary}" "${macos}/${URL_HANDLER_NAME}" - install -m 0755 "${helper_binary}" "${embedded_helper}" - install -m 0644 "${helper_launchd_plist}" "${embedded_launchd}" - ditto --norsrc --noextattr "${app_icon_icns}" "${resources}/AppIcon.icns" + install -m 0755 "${url_handler_binary}" "${macos}/${URL_HANDLER_NAME}" + install -m 0755 "${helper_binary}" "${embedded_helper}" + install -m 0644 "${helper_launchd_plist}" "${embedded_launchd}" + ditto --norsrc --noextattr "${app_icon_icns}" "${resources}/AppIcon.icns" - # Sign helper before writing Info.plist so SMPrivilegedExecutables - # can embed the helper's designated requirement. - codesign_runtime "${embedded_helper}" - # codesign prints "Executable=…" on stderr and either - # "# designated => …" (modern) or "designated => …" (older) on stdout. - helper_requirement="$( - codesign -d -r- "${embedded_helper}" 2>&1 \ - | sed -n -e 's/^# designated => //p' -e 's/^designated => //p' - )" - if [ -z "${helper_requirement}" ]; then - echo "error: cannot extract designated requirement from signed helper" >&2 - codesign -d -r- "${embedded_helper}" 2>&1 >&2 || true - exit 1 - fi - echo "Helper designated requirement: ${helper_requirement}" + # Sign helper before writing Info.plist so SMPrivilegedExecutables + # can embed the helper's designated requirement. + codesign_runtime "${embedded_helper}" + # codesign prints "Executable=…" on stderr and either + # "# designated => …" (modern) or "designated => …" (older) on stdout. + helper_requirement="$( + codesign -d -r- "${embedded_helper}" 2>&1 \ + | sed -n -e 's/^# designated => //p' -e 's/^designated => //p' + )" + if [ -z "${helper_requirement}" ]; then + echo "error: cannot extract designated requirement from signed helper" >&2 + codesign -d -r- "${embedded_helper}" 2>&1 >&2 || true + exit 1 + fi + echo "Helper designated requirement: ${helper_requirement}" - sed \ - -e "s|@@VERSION@@|${VERSION}|g" \ - -e "s|@@HELPER_DESIGNATED_REQUIREMENT@@|${helper_requirement}|g" \ - "${ENROLL_UI_DIR}/Info.plist.tmpl" > "${plist}" + sed \ + -e "s|@@VERSION@@|${VERSION}|g" \ + -e "s|@@HELPER_DESIGNATED_REQUIREMENT@@|${helper_requirement}|g" \ + "${ENROLL_UI_DIR}/Info.plist.tmpl" >"${plist}" - if ! plutil -lint "${plist}" >/dev/null; then - echo "error: rendered Info.plist failed plutil -lint" >&2 - exit 1 - fi - if ! grep -q 'probo' "${plist}"; then - echo "error: Info.plist is missing probo URL scheme" >&2 - exit 1 - fi + if ! plutil -lint "${plist}" >/dev/null; then + echo "error: rendered Info.plist failed plutil -lint" >&2 + exit 1 + fi + if ! grep -q 'probo' "${plist}"; then + echo "error: Info.plist is missing probo URL scheme" >&2 + exit 1 + fi - codesign_runtime "${macos}/${URL_HANDLER_NAME}" - codesign_runtime "${app_root}" + codesign_runtime "${macos}/${URL_HANDLER_NAME}" + codesign_runtime "${app_root}" - echo "Built ${app_root}" + echo "Built ${app_root}" } notarytool_submit() { - local path="$1" - xcrun notarytool submit "${path}" \ - --keychain-profile "${NOTARYTOOL_KEYCHAIN_PROFILE}" \ - --wait + local path="$1" + xcrun notarytool submit "${path}" \ + --keychain-profile "${NOTARYTOOL_KEYCHAIN_PROFILE}" \ + --wait } # pkgbuild records protected com.apple.provenance xattrs as empty # AppleDouble (._*) Bom entries. Rewrite the Bom with mkbom so the # installer does not lay down those stubs next to real files. rewrite_component_bom() { - local pkg="$1" - local expand_dir root_dir flat_pkg + local pkg="$1" + local expand_dir root_dir flat_pkg - expand_dir="${STAGE}/component-expand" - root_dir="${STAGE}/component-root" - flat_pkg="${STAGE}/probo-agent-component-clean.pkg" - rm -rf "${expand_dir}" "${root_dir}" "${flat_pkg}" - # pkgutil --expand creates the destination directory itself. - pkgutil --expand "${pkg}" "${expand_dir}" - find "${expand_dir}/Scripts" -name '._*' -delete 2>/dev/null || true + expand_dir="${STAGE}/component-expand" + root_dir="${STAGE}/component-root" + flat_pkg="${STAGE}/probo-agent-component-clean.pkg" + rm -rf "${expand_dir}" "${root_dir}" "${flat_pkg}" + # pkgutil --expand creates the destination directory itself. + pkgutil --expand "${pkg}" "${expand_dir}" + find "${expand_dir}/Scripts" -name '._*' -delete 2>/dev/null || true - mkdir -p "${root_dir}" - ( - cd "${root_dir}" - gzip -dc "${expand_dir}/Payload" | cpio -idmu 2>/dev/null - ) - find "${root_dir}" -name '._*' -delete 2>/dev/null || true - mkbom "${root_dir}" "${expand_dir}/Bom" - if lsbom "${expand_dir}/Bom" | grep -q '/\._'; then - echo "error: rewritten Bom still contains AppleDouble entries" >&2 - return 1 - fi - pkgutil --flatten "${expand_dir}" "${flat_pkg}" - mv "${flat_pkg}" "${pkg}" + mkdir -p "${root_dir}" + ( + cd "${root_dir}" + gzip -dc "${expand_dir}/Payload" | cpio -idmu 2>/dev/null + ) + find "${root_dir}" -name '._*' -delete 2>/dev/null || true + mkbom "${root_dir}" "${expand_dir}/Bom" + if lsbom "${expand_dir}/Bom" | grep -q '/\._'; then + echo "error: rewritten Bom still contains AppleDouble entries" >&2 + return 1 + fi + pkgutil --flatten "${expand_dir}" "${flat_pkg}" + mv "${flat_pkg}" "${pkg}" } STAGE="$(mktemp -d -t probo-agent-pkg)" @@ -379,18 +398,18 @@ build_probo_agent_app "${PAYLOAD}/Applications" APP_PATH="${PAYLOAD}/Applications/${APP_NAME}" if [ "${notarize_enabled}" = true ]; then - # Password appears on argv only for this short-lived store. Submits - # use --keychain-profile so concurrent processes cannot read it. - xcrun notarytool store-credentials "${NOTARYTOOL_KEYCHAIN_PROFILE}" \ - --apple-id "${APPLE_ID}" \ - --password "${APPLE_ID_PASSWORD}" \ - --team-id "${APPLE_TEAM_ID}" - echo "Notarizing Probo Agent.app before packaging..." - zip_path="${STAGE}/probo-agent-app.zip" - ditto -c -k --keepParent "${APP_PATH}" "${zip_path}" - notarytool_submit "${zip_path}" - rm -f "${zip_path}" - xcrun stapler staple "${APP_PATH}" + # Password appears on argv only for this short-lived store. Submits + # use --keychain-profile so concurrent processes cannot read it. + xcrun notarytool store-credentials "${NOTARYTOOL_KEYCHAIN_PROFILE}" \ + --apple-id "${APPLE_ID}" \ + --password "${APPLE_ID_PASSWORD}" \ + --team-id "${APPLE_TEAM_ID}" + echo "Notarizing Probo Agent.app before packaging..." + zip_path="${STAGE}/probo-agent-app.zip" + ditto -c -k --keepParent "${APP_PATH}" "${zip_path}" + notarytool_submit "${zip_path}" + rm -f "${zip_path}" + xcrun stapler staple "${APP_PATH}" fi # AppleDouble / xattr hygiene: COPYFILE_DISABLE + ditto --norsrc/--noextattr @@ -401,53 +420,53 @@ export COPYFILE_DISABLE=1 ditto --norsrc --noextattr "${SCRIPT_DIR}/scripts/preinstall" "${SCRIPTS}/preinstall" ditto --norsrc --noextattr "${SCRIPT_DIR}/scripts/postinstall" "${SCRIPTS}/postinstall" ditto --norsrc --noextattr \ - "${REPO_ROOT}/pkg/deviceagent/tray/launchagent.plist.tmpl" \ - "${SCRIPTS}/launchagent.plist.tmpl" + "${REPO_ROOT}/pkg/deviceagent/tray/launchagent.plist.tmpl" \ + "${SCRIPTS}/launchagent.plist.tmpl" chmod 0755 "${SCRIPTS}/preinstall" "${SCRIPTS}/postinstall" chmod 0644 "${SCRIPTS}/launchagent.plist.tmpl" -ditto --norsrc --noextattr "${SCRIPT_DIR}/Resources/welcome.html" "${RESOURCES}/welcome.html" +ditto --norsrc --noextattr "${SCRIPT_DIR}/Resources/welcome.html" "${RESOURCES}/welcome.html" ditto --norsrc --noextattr "${SCRIPT_DIR}/Resources/conclusion.html" "${RESOURCES}/conclusion.html" -ditto --norsrc --noextattr "${REPO_ROOT}/LICENSE" "${RESOURCES}/license.txt" +ditto --norsrc --noextattr "${REPO_ROOT}/LICENSE" "${RESOURCES}/license.txt" xattr -cr "${PAYLOAD}" "${SCRIPTS}" "${RESOURCES}" 2>/dev/null || true find "${PAYLOAD}" "${SCRIPTS}" "${RESOURCES}" -name '._*' -delete 2>/dev/null || true COMPONENT_PKG="${STAGE}/probo-agent-component.pkg" pkgbuild \ - --root "${PAYLOAD}" \ - --scripts "${SCRIPTS}" \ - --identifier "${IDENTIFIER}" \ - --version "${VERSION}" \ - --install-location "/" \ - "${COMPONENT_PKG}" + --root "${PAYLOAD}" \ + --scripts "${SCRIPTS}" \ + --identifier "${IDENTIFIER}" \ + --version "${VERSION}" \ + --install-location "/" \ + "${COMPONENT_PKG}" rewrite_component_bom "${COMPONENT_PKG}" DISTRIBUTION="${STAGE}/Distribution.xml" sed \ - -e "s|@@VERSION@@|${VERSION}|g" \ - -e "s|@@IDENTIFIER@@|${IDENTIFIER}|g" \ - "${SCRIPT_DIR}/Distribution.xml.tmpl" > "${DISTRIBUTION}" + -e "s|@@VERSION@@|${VERSION}|g" \ + -e "s|@@IDENTIFIER@@|${IDENTIFIER}|g" \ + "${SCRIPT_DIR}/Distribution.xml.tmpl" >"${DISTRIBUTION}" mkdir -p "$(dirname "${OUTPUT}")" PRODUCTBUILD_ARGS=( - --distribution "${DISTRIBUTION}" - --package-path "${STAGE}" - --resources "${RESOURCES}" + --distribution "${DISTRIBUTION}" + --package-path "${STAGE}" + --resources "${RESOURCES}" ) if [ -n "${INSTALLER_IDENTITY}" ]; then - PRODUCTBUILD_ARGS+=(--sign "${INSTALLER_IDENTITY}") + PRODUCTBUILD_ARGS+=(--sign "${INSTALLER_IDENTITY}") fi PRODUCTBUILD_ARGS+=("${OUTPUT}") productbuild "${PRODUCTBUILD_ARGS[@]}" if [ "${notarize_enabled}" = true ]; then - echo "Notarizing ${OUTPUT}..." - notarytool_submit "${OUTPUT}" - xcrun stapler staple "${OUTPUT}" + echo "Notarizing ${OUTPUT}..." + notarytool_submit "${OUTPUT}" + xcrun stapler staple "${OUTPUT}" fi echo "Built ${OUTPUT}" diff --git a/cmd/probo-agent/installer/macos/reinstall.sh b/cmd/probo-agent/installer/macos/reinstall.sh index 8b9733057..16edd0b36 100755 --- a/cmd/probo-agent/installer/macos/reinstall.sh +++ b/cmd/probo-agent/installer/macos/reinstall.sh @@ -11,13 +11,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PKG="${1:-}" if [ "$(id -u)" -ne 0 ]; then - echo "error: must run as root (try: sudo make -C cmd/probo-agent install)" >&2 - exit 1 + echo "error: must run as root (try: sudo make -C cmd/probo-agent install)" >&2 + exit 1 fi if [ -z "${PKG}" ] || [ ! -f "${PKG}" ]; then - echo "error: usage: $0 /path/to/probo-agent_*.pkg" >&2 - exit 2 + echo "error: usage: $0 /path/to/probo-agent_*.pkg" >&2 + exit 2 fi "${SCRIPT_DIR}/uninstall.sh" diff --git a/cmd/probo-agent/installer/macos/uninstall.sh b/cmd/probo-agent/installer/macos/uninstall.sh index ea578e2e5..430d123b2 100755 --- a/cmd/probo-agent/installer/macos/uninstall.sh +++ b/cmd/probo-agent/installer/macos/uninstall.sh @@ -26,94 +26,93 @@ PKG_ID="com.probo.agent" LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister" log() { - printf '%s\n' "$*" + printf '%s\n' "$*" } die() { - printf 'error: %s\n' "$*" >&2 - exit 1 + printf 'error: %s\n' "$*" >&2 + exit 1 } require_root() { - if [ "$(id -u)" -ne 0 ]; then - die "must run as root (try: sudo make -C cmd/probo-agent uninstall)" - fi + if [ "$(id -u)" -ne 0 ]; then + die "must run as root (try: sudo make -C cmd/probo-agent uninstall)" + fi } bootout_system_plist() { - local plist="$1" - if [ -f "${plist}" ]; then - launchctl bootout system "${plist}" 2>/dev/null || true - log "Booted out ${plist}" - fi + local plist="$1" + if [ -f "${plist}" ]; then + launchctl bootout system "${plist}" 2>/dev/null || true + log "Booted out ${plist}" + fi } bootout_tray_for_user() { - local username="$1" - local user_uid + local username="$1" + local user_uid - if [ -z "${username}" ] || \ - [ "${username}" = "root" ] || \ - [ "${username}" = "loginwindow" ]; then - return 0 - fi + if [ -z "${username}" ] \ + || [ "${username}" = "root" ] \ + || [ "${username}" = "loginwindow" ]; then + return 0 + fi - user_uid="$(id -u "${username}" 2>/dev/null || true)" - if [ -z "${user_uid}" ]; then - return 0 - fi + user_uid="$(id -u "${username}" 2>/dev/null || true)" + if [ -z "${user_uid}" ]; then + return 0 + fi - launchctl bootout "gui/${user_uid}/${TRAY_LABEL}" 2>/dev/null || true + launchctl bootout "gui/${user_uid}/${TRAY_LABEL}" 2>/dev/null || true } unregister_apps() { - local path - for path in \ - "/Applications/Probo Agent.app" \ - "/Applications/Probo Agent.localized/Probo Agent.app" - do - if [ -d "${path}" ] && [ -x "${LSREGISTER}" ]; then - "${LSREGISTER}" -u "${path}" 2>/dev/null || true - log "Unregistered Launch Services entry for ${path}" - fi - done + local path + for path in \ + "/Applications/Probo Agent.app" \ + "/Applications/Probo Agent.localized/Probo Agent.app"; do + if [ -d "${path}" ] && [ -x "${LSREGISTER}" ]; then + "${LSREGISTER}" -u "${path}" 2>/dev/null || true + log "Unregistered Launch Services entry for ${path}" + fi + done } kill_leftovers() { - # Best-effort; deleted-but-running binaries otherwise keep claiming probo://. - pkill -x probo-agent-url-handler 2>/dev/null || true - pkill -f '/usr/local/bin/probo-agent tray' 2>/dev/null || true - pkill -f '/Library/PrivilegedHelperTools/com.probo.agent.helper' 2>/dev/null || true - # Agent daemon may still be running after plist bootout races. - pkill -x probo-agent 2>/dev/null || true + # Best-effort; deleted-but-running binaries otherwise keep claiming probo://. + pkill -x probo-agent-url-handler 2>/dev/null || true + pkill -f '/usr/local/bin/probo-agent tray' 2>/dev/null || true + pkill -f '/Library/PrivilegedHelperTools/com.probo.agent.helper' 2>/dev/null || true + # Agent daemon may still be running after plist bootout races. + pkill -x probo-agent 2>/dev/null || true } require_root if [ "$(uname -s)" != "Darwin" ]; then - die "this uninstall script is macOS-only" + die "this uninstall script is macOS-only" fi log "=== probo-agent macOS uninstall $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" # Prefer the agent's own uninstall for service/tray/state when present. if [ -x "${BINARY}" ]; then - if "${BINARY}" uninstall; then - log "Ran: ${BINARY} uninstall" - else - log "warning: ${BINARY} uninstall failed; continuing with manual cleanup" - fi + if "${BINARY}" uninstall; then + log "Ran: ${BINARY} uninstall" + else + log "warning: ${BINARY} uninstall failed; continuing with manual cleanup" + fi else - log "Binary not found at ${BINARY}; skipping probo-agent uninstall" + log "Binary not found at ${BINARY}; skipping probo-agent uninstall" fi seen_users=" " for username in $(users 2>/dev/null || true); do - case "${seen_users}" in - *" ${username} "*) continue ;; - esac - seen_users="${seen_users}${username} " - bootout_tray_for_user "${username}" + case "${seen_users}" in + *" ${username} "*) continue ;; + esac + seen_users="${seen_users}${username} " + bootout_tray_for_user "${username}" done bootout_tray_for_user "$(stat -f "%Su" /dev/console 2>/dev/null || true)" @@ -127,23 +126,23 @@ log "Removed LaunchDaemon / LaunchAgent / helper files (if present)" unregister_apps rm -rf \ - "/Applications/Probo Agent.app" \ - "/Applications/Probo Agent.localized" + "/Applications/Probo Agent.app" \ + "/Applications/Probo Agent.localized" log "Removed Probo Agent.app (if present)" rm -f "${BINARY}" rm -rf "${STATE_DIR}" "${RUN_DIR}" rm -f \ - /var/log/probo-agent.log \ - /var/log/probo-agent-install.log \ - /tmp/probo-agent.conf + /var/log/probo-agent.log \ + /var/log/probo-agent-install.log \ + /tmp/probo-agent.conf log "Removed binary, state, run dir, logs, and staged conf (if present)" if pkgutil --pkg-info "${PKG_ID}" >/dev/null 2>&1; then - if ! pkgutil --forget "${PKG_ID}" >/dev/null; then - die "failed to forget PKG receipt ${PKG_ID}" - fi - log "Forgot PKG receipt ${PKG_ID}" + if ! pkgutil --forget "${PKG_ID}" >/dev/null; then + die "failed to forget PKG receipt ${PKG_ID}" + fi + log "Forgot PKG receipt ${PKG_ID}" fi log "=== uninstall done ===" diff --git a/compose/postgres/01_probod.sh b/compose/postgres/01_probod.sh index 685f08ca7..0a08c0b8f 100755 --- a/compose/postgres/01_probod.sh +++ b/compose/postgres/01_probod.sh @@ -2,7 +2,7 @@ set -eu -psql -v ON_ERROR_STOP=1 -U $POSTGRES_USER <<-EOF +psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" <<-EOF CREATE USER probod; ALTER USER probod WITH SUPERUSER; ALTER USER probod PASSWORD 'probod'; @@ -12,13 +12,13 @@ CREATE DATABASE probod_test; GRANT ALL PRIVILEGES ON DATABASE probod_test TO probod; EOF -psql -v ON_ERROR_STOP=1 -U $POSTGRES_USER -d probod <<-EOF +psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d probod <<-EOF ALTER SCHEMA public OWNER TO probod; GRANT ALL ON SCHEMA public TO probod; ALTER DATABASE probod SET probo.trust_center_base_domain TO 'probopage.localhost'; EOF -psql -v ON_ERROR_STOP=1 -U $POSTGRES_USER -d probod_test <<-EOF +psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d probod_test <<-EOF ALTER SCHEMA public OWNER TO probod; GRANT ALL ON SCHEMA public TO probod; ALTER DATABASE probod_test SET probo.trust_center_base_domain TO 'probopage.localhost'; diff --git a/contrib/claude/make.md b/contrib/claude/make.md index 9ed59ab1e..f9087095e 100644 --- a/contrib/claude/make.md +++ b/contrib/claude/make.md @@ -16,8 +16,10 @@ The project uses a `GNUmakefile` at the root. Builds run with `--jobs=$(nproc)` | `make test-e2e` | Run console end-to-end tests (requires `bin/probod`) | | `make lint` | Run Go + JS linters: `vet` + `go-fmt` + `go-fix` + `go-lint` + `lint-js` | | `make lint-swift` | Opt-in: lint Swift enroll-ui (`swift-fmt` + `swift-lint`; needs Swift + SwiftLint; CI runs this on Linux) | +| `make lint-shell` | Opt-in: lint `SHELL_SCRIPTS` (`shfmt -d` + `shellcheck`; CI runs this) | | `make fmt` | Format Go code | | `make fmt-swift` | Opt-in: format Swift enroll-ui (`swift format` + SwiftLint `--fix`; needs Swift) | +| `make fmt-shell` | Opt-in: format `SHELL_SCRIPTS` with `shfmt` | | `make clean` | Remove all build artifacts, `node_modules`, generated files, and coverage | | `make help` | List targets with `##` doc comments | @@ -86,3 +88,10 @@ Individual codegen is driven by `go generate`: | `SWIFTLINTCMD` | `swiftlint` | SwiftLint binary | | `SWIFTCMD` | `swift` | Swift toolchain binary (`swift format`) | | `SWIFT_ENROLL_UI` | `cmd/probo-agent/installer/macos/enroll-ui` | Path to the Swift SPM package | +| `SHELLCHECKCMD` | `shellcheck` | ShellCheck binary | +| `SHFMTCMD` | `shfmt` | shfmt binary | +| `SHFMTFLAGS` | `-i 2 -ci -bn` | Flags passed to `shfmt` | + +## Shell scripts + +`make lint-shell` / `make fmt-shell` only touch the static `SHELL_SCRIPTS` list in the root `GNUmakefile` (not a recursive `find`). When you add a new first-party `*.sh` file, append it to that list so CI formats and lint it. Do not add vendored or git-submodule scripts (for example under `pkg/validator/data/disposable-email-domains`). diff --git a/contrib/lima/provision.sh b/contrib/lima/provision.sh index 4f749f2e7..ce1a89776 100755 --- a/contrib/lima/provision.sh +++ b/contrib/lima/provision.sh @@ -15,55 +15,53 @@ GO_VERSION="1.26.5" NODE_MAJOR=24 NPM_VERSION="11.8.0" -GOTESTSUM_VERSION="v1.13.0" -GOLANGCI_LINT_VERSION="v2.11.3" GOW_VERSION="v0.0.0-20260225145757-ff0f6779ab4c" MKCERT_VERSION="v1.4.4" apt-get update -qq apt-get install -y -qq \ - build-essential \ - git \ - curl \ - jq \ - parallel \ - ca-certificates \ - gnupg \ - lsb-release \ - postgresql-client + build-essential \ + git \ + curl \ + jq \ + parallel \ + ca-certificates \ + gnupg \ + lsb-release \ + postgresql-client if ! command -v docker &>/dev/null; then - install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ - | gpg --dearmor -o /etc/apt/keyrings/docker.gpg - chmod a+r /etc/apt/keyrings/docker.gpg + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ + | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ - | tee /etc/apt/sources.list.d/docker.list > /dev/null + | tee /etc/apt/sources.list.d/docker.list >/dev/null - apt-get update -qq - apt-get install -y -qq \ - docker-ce \ - docker-ce-cli \ - containerd.io \ - docker-buildx-plugin \ - docker-compose-plugin + apt-get update -qq + apt-get install -y -qq \ + docker-ce \ + docker-ce-cli \ + containerd.io \ + docker-buildx-plugin \ + docker-compose-plugin - systemctl enable --now docker + systemctl enable --now docker fi usermod -aG docker "${LIMA_CIDATA_USER:-lima}" 2>/dev/null || true if [ ! -d "/usr/local/go" ] || ! /usr/local/go/bin/go version | grep -q "go${GO_VERSION}"; then - rm -rf /usr/local/go - ARCH=$(dpkg --print-architecture) - curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \ - | tar -C /usr/local -xzf - + rm -rf /usr/local/go + ARCH=$(dpkg --print-architecture) + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \ + | tar -C /usr/local -xzf - fi -cat > /etc/profile.d/go.sh << 'GOEOF' +cat >/etc/profile.d/go.sh <<'GOEOF' export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH" GOEOF chmod +x /etc/profile.d/go.sh @@ -74,14 +72,14 @@ export HOME="${HOME:-/root}" GOBIN=/usr/local/bin /usr/local/go/bin/go install "github.com/mitranim/gow@${GOW_VERSION}" if ! command -v node &>/dev/null || ! node --version | grep -q "v${NODE_MAJOR}"; then - curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - - apt-get install -y -qq nodejs + curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - + apt-get install -y -qq nodejs fi npm install -g "npm@${NPM_VERSION}" if ! command -v mkcert &>/dev/null; then - GOBIN=/usr/local/bin /usr/local/go/bin/go install "filippo.io/mkcert@${MKCERT_VERSION}" + GOBIN=/usr/local/bin /usr/local/go/bin/go install "filippo.io/mkcert@${MKCERT_VERSION}" fi mkcert -install 2>/dev/null || true @@ -102,34 +100,35 @@ mkdir -p /etc/probod OAUTH2_SIGNING_KEY_PATH=/etc/probod/oauth2-signing-key.pem if [ ! -f "${OAUTH2_SIGNING_KEY_PATH}" ]; then - openssl genrsa -out "${OAUTH2_SIGNING_KEY_PATH}" 2048 - chmod 600 "${OAUTH2_SIGNING_KEY_PATH}" + openssl genrsa -out "${OAUTH2_SIGNING_KEY_PATH}" 2048 + chmod 600 "${OAUTH2_SIGNING_KEY_PATH}" fi # Load developer-specific overrides (not committed to repo). if [ -f /workspace/.sandbox.env ]; then - set -a - . /workspace/.sandbox.env - set +a + set -a + # shellcheck source=/dev/null + . /workspace/.sandbox.env + set +a fi PROBOD_BASE_URL="http://${VM_IP}:8080" \ -PROBOD_AUTH_COOKIE_DOMAIN="${VM_IP}" \ -PROBOD_AUTH_COOKIE_SECURE=false \ -PROBOD_AUTH_COOKIE_SECRET="this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes" \ -PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes" \ -PROBOD_ENCRYPTION_KEY="thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \ -PROBOD_OAUTH2_SERVER_SIGNING_KEY="$(cat "${OAUTH2_SIGNING_KEY_PATH}")" \ -PROBOD_API_CORS_ALLOWED_ORIGINS="http://${VM_IP}:8080,http://${VM_IP}:5173,http://${VM_IP}:5174" \ -PROBOD_AWS_ENDPOINT="http://127.0.0.1:8333" \ -PROBOD_AWS_ACCESS_KEY_ID="probod" \ -PROBOD_AWS_SECRET_ACCESS_KEY="thisisnotasecret" \ -PROBOD_AWS_USE_PATH_STYLE=true \ -PROBOD_ACME_DIRECTORY="https://127.0.0.1:9000/acme/acme/directory" \ -PROBOD_ACME_EMAIL="admin@probo.com" \ -PROBOD_ACME_KEY_TYPE="EC256" \ -PROBOD_ACME_ROOT_CA="$(cat /workspace/compose/step-ca/certs/root_ca.crt)" \ - /workspace/bin/probod-bootstrap -output /etc/probod/config.yml + PROBOD_AUTH_COOKIE_DOMAIN="${VM_IP}" \ + PROBOD_AUTH_COOKIE_SECURE=false \ + PROBOD_AUTH_COOKIE_SECRET="this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes" \ + PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes" \ + PROBOD_ENCRYPTION_KEY="thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \ + PROBOD_OAUTH2_SERVER_SIGNING_KEY="$(cat "${OAUTH2_SIGNING_KEY_PATH}")" \ + PROBOD_API_CORS_ALLOWED_ORIGINS="http://${VM_IP}:8080,http://${VM_IP}:5173,http://${VM_IP}:5174" \ + PROBOD_AWS_ENDPOINT="http://127.0.0.1:8333" \ + PROBOD_AWS_ACCESS_KEY_ID="probod" \ + PROBOD_AWS_SECRET_ACCESS_KEY="thisisnotasecret" \ + PROBOD_AWS_USE_PATH_STYLE=true \ + PROBOD_ACME_DIRECTORY="https://127.0.0.1:9000/acme/acme/directory" \ + PROBOD_ACME_EMAIL="admin@probo.com" \ + PROBOD_ACME_KEY_TYPE="EC256" \ + PROBOD_ACME_ROOT_CA="$(cat /workspace/compose/step-ca/certs/root_ca.crt)" \ + /workspace/bin/probod-bootstrap -output /etc/probod/config.yml # probod runs as ${LIMA_USER} but bootstrap writes config.yml as root with 0600 # because it contains secrets. Transfer ownership so probod can read it. @@ -137,7 +136,7 @@ chown "${LIMA_USER}:${LIMA_USER}" /etc/probod/config.yml "${OAUTH2_SIGNING_KEY_P # Bind-mount VM-local node_modules over the shared workspace to avoid # platform conflicts between macOS host and Linux VM native binaries. -cat > /etc/systemd/system/probo-node-modules.service << EOF +cat >/etc/systemd/system/probo-node-modules.service < /workspace/apps/console/.env -echo "VITE_API_URL=http://${VM_IP}:8080" > /workspace/apps/compliance-portal/.env +echo "VITE_API_URL=http://${VM_IP}:8080" >/workspace/apps/console/.env +echo "VITE_API_URL=http://${VM_IP}:8080" >/workspace/apps/compliance-portal/.env # Install systemd services for the sandbox -cat > /etc/systemd/system/probo-stack.service << EOF +cat >/etc/systemd/system/probo-stack.service < /etc/systemd/system/probod.service << EOF +cat >/etc/systemd/system/probod.service < /etc/systemd/system/probo-console.service << EOF +cat >/etc/systemd/system/probo-console.service < /etc/systemd/system/probo-compliance-portal.service << EOF +cat >/etc/systemd/system/probo-compliance-portal.service < [options] Commands: @@ -29,151 +29,166 @@ Commands: VM name: ${VM_NAME} (derived from worktree directory) EOF - exit 1 + exit 1 } get_vm_ip() { - limactl shell "${VM_NAME}" ip -4 -j addr show dev lima0 2>/dev/null \ - | jq -r '.[0].addr_info[0].local // empty' 2>/dev/null || true + limactl shell "${VM_NAME}" ip -4 -j addr show dev lima0 2>/dev/null \ + | jq -r '.[0].addr_info[0].local // empty' 2>/dev/null || true } get_vm_status() { - local status - status=$(limactl list --json 2>/dev/null \ - | jq -r "select(.name == \"${VM_NAME}\") | .status" 2>/dev/null) || true - echo "${status:-NotFound}" + local status + status=$(limactl list --json 2>/dev/null \ + | jq -r "select(.name == \"${VM_NAME}\") | .status" 2>/dev/null) || true + echo "${status:-NotFound}" } cmd_create() { - local cpus="" memory="" disk="" + local cpus="" memory="" disk="" - while [[ $# -gt 0 ]]; do - case "$1" in - --cpus) cpus="$2"; shift 2 ;; - --memory) memory="$2"; shift 2 ;; - --disk) disk="$2"; shift 2 ;; - *) echo "Unknown option: $1"; usage ;; - esac - done + while [[ $# -gt 0 ]]; do + case "$1" in + --cpus) + cpus="$2" + shift 2 + ;; + --memory) + memory="$2" + shift 2 + ;; + --disk) + disk="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac + done - echo "Creating sandbox: ${VM_NAME}" - echo "Worktree: ${REPO_ROOT}" + echo "Creating sandbox: ${VM_NAME}" + echo "Worktree: ${REPO_ROOT}" - local -a create_args=( - --name "${VM_NAME}" - --tty=false - --set ".mounts = [{\"location\": \"${REPO_ROOT}\", \"mountPoint\": \"/workspace\", \"writable\": true},{\"location\": \"${HOME}/go\", \"mountPoint\": \"/home/${USER}.guest/go\", \"writable\": true}]" - --mount-type virtiofs - ) + local -a create_args=( + --name "${VM_NAME}" + --tty=false + --set ".mounts = [{\"location\": \"${REPO_ROOT}\", \"mountPoint\": \"/workspace\", \"writable\": true},{\"location\": \"${HOME}/go\", \"mountPoint\": \"/home/${USER}.guest/go\", \"writable\": true}]" + --mount-type virtiofs + ) - if [[ -n "${cpus}" ]]; then - create_args+=(--cpus "${cpus}") - fi - if [[ -n "${memory}" ]]; then - create_args+=(--memory "${memory}") - fi - if [[ -n "${disk}" ]]; then - create_args+=(--disk "${disk}") - fi + if [[ -n "${cpus}" ]]; then + create_args+=(--cpus "${cpus}") + fi + if [[ -n "${memory}" ]]; then + create_args+=(--memory "${memory}") + fi + if [[ -n "${disk}" ]]; then + create_args+=(--disk "${disk}") + fi - limactl create "${create_args[@]}" "${TEMPLATE}" + limactl create "${create_args[@]}" "${TEMPLATE}" } cmd_start() { - echo "Starting sandbox: ${VM_NAME}" - limactl start "${VM_NAME}" - echo "" - cmd_status + echo "Starting sandbox: ${VM_NAME}" + limactl start "${VM_NAME}" + echo "" + cmd_status } cmd_boot_logs() { - limactl shell "${VM_NAME}" -- sudo tail -f /var/log/cloud-init-output.log + limactl shell "${VM_NAME}" -- sudo tail -f /var/log/cloud-init-output.log } cmd_stop() { - echo "Stopping sandbox: ${VM_NAME}" - limactl stop "${VM_NAME}" - echo "Sandbox stopped." + echo "Stopping sandbox: ${VM_NAME}" + limactl stop "${VM_NAME}" + echo "Sandbox stopped." } cmd_restart() { - cmd_stop - echo "" - cmd_start + cmd_stop + echo "" + cmd_start } cmd_delete() { - echo "Deleting sandbox: ${VM_NAME}" - limactl delete --force "${VM_NAME}" - echo "Sandbox deleted." + echo "Deleting sandbox: ${VM_NAME}" + limactl delete --force "${VM_NAME}" + echo "Sandbox deleted." } cmd_ssh() { - exec limactl shell --workdir /workspace "${VM_NAME}" + exec limactl shell --workdir /workspace "${VM_NAME}" } cmd_exec() { - limactl shell --workdir /workspace "${VM_NAME}" "$@" + limactl shell --workdir /workspace "${VM_NAME}" "$@" } cmd_status() { - local status ip - status="$(get_vm_status)" - ip="$(get_vm_ip)" + local status ip + status="$(get_vm_status)" + ip="$(get_vm_ip)" - echo "Sandbox: ${VM_NAME}" - echo "State: ${status}" - echo "IP: ${ip:-"-"}" + echo "Sandbox: ${VM_NAME}" + echo "State: ${status}" + echo "IP: ${ip:-"-"}" - if [[ "${status}" == "Running" && -n "${ip}" ]]; then - echo "" - echo "Services (use VM IP to access from host):" - echo " Console: http://${ip}:5173" - echo " Compliance Portal: http://${ip}:5174" - echo " API: http://${ip}:8080" - echo " Grafana: http://${ip}:3001" - echo " Mailpit: http://${ip}:8025" - echo " Keycloak: http://${ip}:8082" - echo " PostgreSQL: psql -h ${ip} -U probod" - fi + if [[ "${status}" == "Running" && -n "${ip}" ]]; then + echo "" + echo "Services (use VM IP to access from host):" + echo " Console: http://${ip}:5173" + echo " Compliance Portal: http://${ip}:5174" + echo " API: http://${ip}:8080" + echo " Grafana: http://${ip}:3001" + echo " Mailpit: http://${ip}:8025" + echo " Keycloak: http://${ip}:8082" + echo " PostgreSQL: psql -h ${ip} -U probod" + fi } cmd_list() { - printf "%-25s %-12s %s\n" "NAME" "STATE" "IP" + printf "%-25s %-12s %s\n" "NAME" "STATE" "IP" - limactl list --json 2>/dev/null | jq -r ' + limactl list --json 2>/dev/null | jq -r ' select(.name | startswith("probo-")) | [.name, .status] | @tsv ' | while IFS=$'\t' read -r name status; do - local ip="-" - if [[ "${status}" == "Running" ]]; then - ip=$(limactl shell "${name}" ip -4 -j addr show dev lima0 2>/dev/null \ - | jq -r '.[0].addr_info[0].local // "-"' 2>/dev/null || echo "-") - fi - printf "%-25s %-12s %s\n" "${name}" "${status}" "${ip}" - done + local ip="-" + if [[ "${status}" == "Running" ]]; then + ip=$(limactl shell "${name}" ip -4 -j addr show dev lima0 2>/dev/null \ + | jq -r '.[0].addr_info[0].local // "-"' 2>/dev/null || echo "-") + fi + printf "%-25s %-12s %s\n" "${name}" "${status}" "${ip}" + done } if [[ $# -lt 1 ]]; then - usage + usage fi command="$1" shift case "${command}" in - create) cmd_create "$@" ;; - start) cmd_start ;; - boot-logs) cmd_boot_logs ;; - stop) cmd_stop ;; - restart) cmd_restart ;; - delete) cmd_delete ;; - ssh) cmd_ssh ;; - exec) - if [[ "${1:-}" == "--" ]]; then shift; fi - cmd_exec "$@" - ;; - status) cmd_status ;; - list) cmd_list ;; - *) echo "Unknown command: ${command}"; usage ;; + create) cmd_create "$@" ;; + start) cmd_start ;; + boot-logs) cmd_boot_logs ;; + stop) cmd_stop ;; + restart) cmd_restart ;; + delete) cmd_delete ;; + ssh) cmd_ssh ;; + exec) + if [[ "${1:-}" == "--" ]]; then shift; fi + cmd_exec "$@" + ;; + status) cmd_status ;; + list) cmd_list ;; + *) + echo "Unknown command: ${command}" + usage + ;; esac diff --git a/contrib/merge-graphql-schema.sh b/contrib/merge-graphql-schema.sh index 7b82fb5c9..4ea2fb755 100755 --- a/contrib/merge-graphql-schema.sh +++ b/contrib/merge-graphql-schema.sh @@ -16,7 +16,7 @@ schema_body=$(mktemp) trap 'rm -f "$mutation_fields" "$schema_body"' EXIT process_file() { - awk -v mf="$mutation_fields" ' + awk -v mf="$mutation_fields" ' /^type Mutation$/ { next } /^(extend )?type Mutation \{/ { skip=1; depth=1; next } skip { @@ -29,16 +29,16 @@ process_file() { } { - process_file "$base" - for f in "$graphql_dir"/*.graphql; do - [ "$f" = "$base" ] && continue - process_file "$f" - done -} > "$schema_body" + process_file "$base" + for f in "$graphql_dir"/*.graphql; do + [ "$f" = "$base" ] && continue + process_file "$f" + done +} >"$schema_body" { - cat "$schema_body" - printf '\ntype Mutation {\n' - cat "$mutation_fields" - printf '}\n' -} > "$output" + cat "$schema_body" + printf '\ntype Mutation {\n' + cat "$mutation_fields" + printf '}\n' +} >"$output" diff --git a/contrib/seed.sh b/contrib/seed.sh index a6b4e1e28..dc9217e5f 100755 --- a/contrib/seed.sh +++ b/contrib/seed.sh @@ -19,6 +19,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +# GraphQL documents are intentional single-quoted literals (no expansion). +# shellcheck disable=SC2016 + set -euo pipefail BASE_URL="${PROBO_SEED_URL:-http://localhost:8080}" @@ -58,7 +61,8 @@ check_error() { } prb_api() { - local context="$1"; shift + local context="$1" + shift local resp resp=$($PRB api "$@") check_error "$resp" "$context" @@ -66,13 +70,17 @@ prb_api() { } curl -sf -o /dev/null "$BASE_URL/healthz" \ - || { echo "ERROR: API at $BASE_URL is not available" >&2; exit 1; } + || { + echo "ERROR: API at $BASE_URL is not available" >&2 + exit 1 + } echo "==> Bootstrapping user and organization..." -vars=$(jo input="$(jo \ - email="$EMAIL" \ - password="$PASSWORD" \ - fullName="$FULL_NAME" \ +vars=$(jo input="$( + jo \ + email="$EMAIL" \ + password="$PASSWORD" \ + fullName="$FULL_NAME" )") resp=$(gql_connect ' mutation($input: SignUpInput!) { @@ -96,9 +104,10 @@ check_error "$resp" "createOrganization" ORG_ID=$(echo "$resp" | jq -r '.data.createOrganization.organization.id') echo " Created organization $ORG_NAME ($ORG_ID)" -vars=$(jo input="$(jo \ - organizationId="$ORG_ID" \ - continue="$BASE_URL" \ +vars=$(jo input="$( + jo \ + organizationId="$ORG_ID" \ + continue="$BASE_URL" )") resp=$(gql_connect ' mutation($input: AssumeOrganizationSessionInput!) { @@ -116,9 +125,10 @@ echo " Assumed organization session" EXPIRES_AT=$(date -u -v+1y +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \ || date -u -d "+1 year" +"%Y-%m-%dT%H:%M:%SZ") -vars=$(jo input="$(jo \ - name=seed \ - expiresAt="$EXPIRES_AT" \ +vars=$(jo input="$( + jo \ + name=seed \ + expiresAt="$EXPIRES_AT" )") resp=$(gql_connect ' mutation($input: CreatePersonalAPIKeyInput!) { @@ -148,14 +158,15 @@ create_person() { local email="$3" local vars - vars=$(jo input="$(jo \ - organizationId="$ORG_ID" \ - emailAddress="$email" \ - fullName="$full_name" \ - role=EMPLOYEE \ - kind=EMPLOYEE \ - additionalEmailAddresses="$(jo -a < /dev/null)" \ - position="$position" \ + vars=$(jo input="$( + jo \ + organizationId="$ORG_ID" \ + emailAddress="$email" \ + fullName="$full_name" \ + role=EMPLOYEE \ + kind=EMPLOYEE \ + additionalEmailAddresses="$(jo -a /dev/null + --description "$desc" >/dev/null } # ISO 27001:2022 @@ -457,7 +469,7 @@ create_risk() { --category "$category" \ --treatment "$treatment" \ --inherent-likelihood "$likelihood" \ - --inherent-impact "$impact" > /dev/null + --inherent-impact "$impact" >/dev/null } create_risk \ @@ -586,11 +598,12 @@ create_third_party() { } } } - ' -f input="$(jo \ + ' -f input="$( + jo \ organizationId="$ORG_ID" \ name="$name" \ - description="$description" \ - )") + description="$description" + )") local id id=$(echo "$resp" | jq -r '.data.createThirdParty.thirdPartyEdge.node.id // empty') if [ -z "$id" ]; then @@ -657,11 +670,12 @@ create_measure() { } } } - ' -f input="$(jo \ + ' -f input="$( + jo \ organizationId="$ORG_ID" \ name="$name" \ - category="$category" \ - )") + category="$category" + )") local id id=$(echo "$resp" | jq -r '.data.createMeasure.measureEdge.node.id // empty') if [ -z "$id" ]; then @@ -796,7 +810,8 @@ agent_heartbeat() { # agent_postures ... agent_postures() { - local api_key="$1"; shift + local api_key="$1" + shift local now now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") @@ -827,7 +842,7 @@ revoke_device() { device { id } } } - ' -f input="$(jo deviceId="$device_id")" > /dev/null + ' -f input="$(jo deviceId="$device_id")" >/dev/null } # seed_device ... @@ -863,35 +878,35 @@ seed_device() { seed_device "${PROFILE_IDS[0]}" "jane-macbook-pro" "DARWIN" "14.5" "C02XY1Z2JGH7" \ DISK_ENCRYPTION:PASS SCREEN_LOCK:PASS FIREWALL_ENABLED:PASS TIME_SYNC:PASS \ OS_VERSION:PASS AUTO_UPDATE:PASS PASSWORD_POLICY:PASS REMOTE_LOGIN:PASS \ - MALWARE_PROTECTION:PASS > /dev/null + MALWARE_PROTECTION:PASS >/dev/null seed_device "${PROFILE_IDS[1]}" "marcus-thinkpad" "LINUX" "Ubuntu 24.04" "PF3ABCDE" \ DISK_ENCRYPTION:PASS SCREEN_LOCK:PASS FIREWALL_ENABLED:FAIL TIME_SYNC:PASS \ OS_VERSION:PASS AUTO_UPDATE:UNKNOWN PASSWORD_POLICY:PASS REMOTE_LOGIN:FAIL \ - MALWARE_PROTECTION:NOT_APPLICABLE > /dev/null + MALWARE_PROTECTION:NOT_APPLICABLE >/dev/null seed_device "${PROFILE_IDS[4]}" "emily-macbook-air" "DARWIN" "14.4" "C02AB3C4JGH8" \ DISK_ENCRYPTION:PASS SCREEN_LOCK:FAIL FIREWALL_ENABLED:PASS TIME_SYNC:PASS \ OS_VERSION:PASS AUTO_UPDATE:PASS PASSWORD_POLICY:FAIL REMOTE_LOGIN:PASS \ - MALWARE_PROTECTION:PASS > /dev/null + MALWARE_PROTECTION:PASS >/dev/null seed_device "${PROFILE_IDS[7]}" "alex-devbox" "LINUX" "Debian 12" "PF9ZYXWV" \ DISK_ENCRYPTION:FAIL SCREEN_LOCK:PASS FIREWALL_ENABLED:PASS TIME_SYNC:PASS \ OS_VERSION:UNKNOWN AUTO_UPDATE:PASS PASSWORD_POLICY:PASS REMOTE_LOGIN:PASS \ - MALWARE_PROTECTION:NOT_APPLICABLE > /dev/null + MALWARE_PROTECTION:NOT_APPLICABLE >/dev/null seed_device "${PROFILE_IDS[3]}" "david-surface" "WINDOWS" "Windows 11 23H2" "5CD1234ABC" \ DISK_ENCRYPTION:PASS SCREEN_LOCK:PASS FIREWALL_ENABLED:PASS TIME_SYNC:FAIL \ OS_VERSION:PASS AUTO_UPDATE:PASS PASSWORD_POLICY:PASS REMOTE_LOGIN:PASS \ - MALWARE_PROTECTION:PASS > /dev/null + MALWARE_PROTECTION:PASS >/dev/null seed_device "${PROFILE_IDS[2]}" "sofia-latitude" "WINDOWS" "Windows 11 22H2" "5CD9876ZYX" \ DISK_ENCRYPTION:PASS SCREEN_LOCK:PASS FIREWALL_ENABLED:FAIL TIME_SYNC:PASS \ OS_VERSION:FAIL AUTO_UPDATE:FAIL PASSWORD_POLICY:PASS REMOTE_LOGIN:PASS \ - MALWARE_PROTECTION:UNKNOWN > /dev/null + MALWARE_PROTECTION:UNKNOWN >/dev/null # 1 pending device: created and assigned, but never enrolled/activated. -create_device "${PROFILE_IDS[6]}" > /dev/null +create_device "${PROFILE_IDS[6]}" >/dev/null # 1 revoked device: fully activated, then revoked. revoked_id=$(seed_device "${PROFILE_IDS[5]}" "james-old-macbook" "DARWIN" "12.7" "C02OLD1JGH9" \