From 952851c743712c11f0593568982736031d4ab0d6 Mon Sep 17 00:00:00 2001 From: Ludovic Vielle Date: Tue, 14 Jul 2026 20:40:51 +0200 Subject: [PATCH] Seed device fleet and document agent Populate varied device posture states in seed data and link the probo-agent release guide from AGENTS.md. Signed-off-by: Ludovic Vielle --- AGENTS.md | 1 + contrib/seed.sh | 208 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 2e3c8ccbf..ae9ed7197 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,7 @@ Detailed guides for specific subsystems live in `contrib/claude/`: - [`contrib/claude/httpclient.md`](contrib/claude/httpclient.md) — HTTP client (kit/httpclient, SSRF protection by default, connector wiring) - [`contrib/claude/gid.md`](contrib/claude/gid.md) — Global identifiers (GID layout, TenantID, entity type registry) - [`contrib/claude/coredata.md`](contrib/claude/coredata.md) — Data access layer (Scoper, SQL patterns, filters, order fields, migrations) +- [`contrib/claude/ownership.md`](contrib/claude/ownership.md) — Resource ownership (membership profile vs identity; provisional) - [`contrib/claude/logging.md`](contrib/claude/logging.md) — Structured logging (PII-free rules, field helpers, logger wiring) - [`contrib/claude/graphql.md`](contrib/claude/graphql.md) — Go GraphQL backend (gqlgen, @goModel, connection types, cursor pagination) - [`contrib/claude/mcp.md`](contrib/claude/mcp.md) — MCP API patterns (specification.yaml, mcpgen, resolvers, type helpers) diff --git a/contrib/seed.sh b/contrib/seed.sh index 541a1d9e9..a6b4e1e28 100755 --- a/contrib/seed.sh +++ b/contrib/seed.sh @@ -140,6 +140,8 @@ PRB="./bin/prb" echo " Creating people..." +PROFILE_IDS=() + create_person() { local full_name="$1" local position="$2" @@ -165,6 +167,14 @@ create_person() { } ' "$vars") check_error "$resp" "createPerson: $full_name" + + local profile_id + profile_id=$(echo "$resp" | jq -r '.data.createUser.profileEdge.node.id // empty') + if [ -z "$profile_id" ]; then + echo "ERROR (createPerson: $full_name): no profile id in response" >&2 + exit 1 + fi + PROFILE_IDS+=("$profile_id") } create_person "Jane Cooper" \ @@ -695,6 +705,203 @@ create_measure "Tabletop Disaster Recovery Exercises" \ echo " 15 measures created" +echo " Creating devices..." + +AGENT_API="$BASE_URL/api/agent/v1" + +create_device() { + local owner_id="$1" + + local input + if [ -n "$owner_id" ]; then + input=$(jo organizationId="$ORG_ID" ownerId="$owner_id") + else + input=$(jo organizationId="$ORG_ID") + fi + + local resp + resp=$(prb_api "createDevice" ' + mutation($input: CreateDeviceInput!) { + createDevice(input: $input) { + device { id } + enrollmentToken + } + } + ' -f input="$input") + + local device_id token + device_id=$(echo "$resp" | jq -r '.data.createDevice.device.id // empty') + token=$(echo "$resp" | jq -r '.data.createDevice.enrollmentToken // empty') + if [ -z "$device_id" ] || [ -z "$token" ]; then + echo "ERROR (createDevice): missing device id or enrollment token in response" >&2 + exit 1 + fi + + echo "$device_id $token" +} + +agent_enroll() { + local token="$1" + + local resp api_key + resp=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg t "$token" '{token: $t}')" \ + "$AGENT_API/enroll") + api_key=$(echo "$resp" | jq -r '.api_key // empty') + if [ -z "$api_key" ]; then + echo "ERROR (agent_enroll): no api_key in response: $resp" >&2 + exit 1 + fi + + echo "$api_key" +} + +agent_heartbeat() { + local api_key="$1" + local hardware_uuid="$2" + local hostname="$3" + local platform="$4" + local os_version="$5" + local agent_version="$6" + local serial="${7:-}" + + # Build with jq (not jo) so numeric-looking values like os_version "14.5" + # stay JSON strings; jo would coerce them to numbers and fail decoding. + local body + body=$(jq -n \ + --arg hw "$hardware_uuid" \ + --arg sn "$serial" \ + --arg hn "$hostname" \ + --arg pf "$platform" \ + --arg ov "$os_version" \ + --arg av "$agent_version" \ + '{hardware_uuid: $hw, hostname: $hn, platform: $pf, os_version: $ov, agent_version: $av} + + (if $sn == "" then {} else {serial_number: $sn} end)') + + local resp code + resp=$(curl -s -w '\n%{http_code}' -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $api_key" \ + -d "$body" \ + "$AGENT_API/heartbeat") + code=$(echo "$resp" | tail -n1) + if [ "$code" != "200" ]; then + echo "ERROR (agent_heartbeat $hostname): HTTP $code" >&2 + echo " request: $body" >&2 + echo " response: $(echo "$resp" | sed '$d')" >&2 + exit 1 + fi +} + +# agent_postures ... +agent_postures() { + local api_key="$1"; shift + + local now + now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + local body + body=$(printf '%s\n' "$@" \ + | jq -R --arg o "$now" 'split(":") | {check_key: .[0], status: .[1], observed_at: $o}' \ + | jq -s '{results: .}') + + local code + code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $api_key" \ + -d "$body" \ + "$AGENT_API/postures") + if [ "$code" != "204" ] && [ "$code" != "200" ]; then + echo "ERROR (agent_postures): HTTP $code" >&2 + exit 1 + fi +} + +revoke_device() { + local device_id="$1" + + prb_api "revokeDevice" ' + mutation($input: RevokeDeviceInput!) { + revokeDevice(input: $input) { + device { id } + } + } + ' -f input="$(jo deviceId="$device_id")" > /dev/null +} + +# seed_device ... +# Creates a device, enrolls it, sends a heartbeat (activating it) and posts +# posture results. Prints the device id. +seed_device() { + local owner_id="$1" + local hostname="$2" + local platform="$3" + local os_version="$4" + local serial="$5" + shift 5 + + local out device_id token + out=$(create_device "$owner_id") + device_id="${out%% *}" + token="${out#* }" + + local api_key + api_key=$(agent_enroll "$token") + + local hardware_uuid + hardware_uuid="hw-$(echo "$hostname" | tr '[:upper:]' '[:lower:]')" + + agent_heartbeat "$api_key" "$hardware_uuid" "$hostname" "$platform" "$os_version" "1.0.0" "$serial" + agent_postures "$api_key" "$@" + + echo "$device_id" +} + +# 6 active devices across platforms, each owned by a seeded person with a +# varied posture mix so the UI shows every badge state. +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 + +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 + +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 + +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 + +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 + +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 + +# 1 pending device: created and assigned, but never enrolled/activated. +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" \ + DISK_ENCRYPTION:PASS SCREEN_LOCK:PASS FIREWALL_ENABLED:PASS TIME_SYNC:PASS \ + OS_VERSION:FAIL AUTO_UPDATE:FAIL PASSWORD_POLICY:PASS REMOTE_LOGIN:PASS \ + MALWARE_PROTECTION:PASS) +revoke_device "$revoked_id" + +echo " 8 devices created (6 active, 1 pending, 1 revoked)" + echo "" echo "Seed complete!" echo " Email: $EMAIL" @@ -707,6 +914,7 @@ echo " 35 risks" echo " 20 third parties" echo " 15 measures" echo " 8 people" +echo " 8 devices (6 active, 1 pending, 1 revoked)" echo "" echo " To use the CLI:" echo " export PROBO_HOST=$BASE_URL"