Pass/fail was the main device UI signal, but operators need the agent's observed value. Expose a formatted value per check, show current postures on the device page, and replace the Postures tab with paginated report history grouped by agent push time. Status stays in the model for later rulesets. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
1161 lines
36 KiB
Bash
Executable File
1161 lines
36 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# 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}"
|
|
CONNECT_API="$BASE_URL/api/connect/v1/graphql"
|
|
COOKIE_JAR=$(mktemp)
|
|
trap 'rm -f "$COOKIE_JAR"' EXIT
|
|
|
|
EMAIL="seed@dev.probo.test"
|
|
PASSWORD="seed@dev.probo.test"
|
|
FULL_NAME="Seed Admin"
|
|
ORG_NAME="Acme Corp"
|
|
|
|
# Helpers
|
|
gql_connect() {
|
|
local query="$1"
|
|
local variables="${2:-"{}"}"
|
|
|
|
local payload
|
|
payload=$(jq -n --arg q "$query" --argjson v "$variables" \
|
|
'{query: $q, variables: $v}')
|
|
|
|
curl -s -b "$COOKIE_JAR" -c "$COOKIE_JAR" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$payload" \
|
|
"$CONNECT_API"
|
|
}
|
|
|
|
check_error() {
|
|
local response="$1"
|
|
local context="$2"
|
|
local errors
|
|
errors=$(echo "$response" | jq -r '.errors[0].message // empty')
|
|
if [ -n "$errors" ]; then
|
|
echo "ERROR ($context): $errors" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
prb_api() {
|
|
local context="$1"
|
|
shift
|
|
local resp
|
|
resp=$($PRB api "$@")
|
|
check_error "$resp" "$context"
|
|
echo "$resp"
|
|
}
|
|
|
|
curl -sf -o /dev/null "$BASE_URL/healthz" \
|
|
|| {
|
|
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"
|
|
)")
|
|
resp=$(gql_connect '
|
|
mutation($input: SignUpInput!) {
|
|
signUp(input: $input) {
|
|
identity { id }
|
|
}
|
|
}
|
|
' "$vars")
|
|
check_error "$resp" "signUp"
|
|
echo " Created user $EMAIL"
|
|
|
|
vars=$(jo input="$(jo name="$ORG_NAME")")
|
|
resp=$(gql_connect '
|
|
mutation($input: CreateOrganizationInput!) {
|
|
createOrganization(input: $input) {
|
|
organization { id }
|
|
}
|
|
}
|
|
' "$vars")
|
|
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"
|
|
)")
|
|
resp=$(gql_connect '
|
|
mutation($input: AssumeOrganizationSessionInput!) {
|
|
assumeOrganizationSession(input: $input) {
|
|
result {
|
|
... on OrganizationSessionCreated {
|
|
session { id }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
' "$vars")
|
|
check_error "$resp" "assumeOrganizationSession"
|
|
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"
|
|
)")
|
|
resp=$(gql_connect '
|
|
mutation($input: CreatePersonalAPIKeyInput!) {
|
|
createPersonalAPIKey(input: $input) {
|
|
token
|
|
}
|
|
}
|
|
' "$vars")
|
|
check_error "$resp" "createPersonalAPIKey"
|
|
PROBO_TOKEN=$(echo "$resp" | jq -r '.data.createPersonalAPIKey.token')
|
|
export PROBO_TOKEN
|
|
export PROBO_HOST="$BASE_URL"
|
|
echo " Created API key"
|
|
|
|
echo ""
|
|
echo "==> Seeding data..."
|
|
|
|
PRB="./bin/prb"
|
|
|
|
echo " Creating people..."
|
|
|
|
PROFILE_IDS=()
|
|
|
|
create_person() {
|
|
local full_name="$1"
|
|
local position="$2"
|
|
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"
|
|
)")
|
|
resp=$(gql_connect '
|
|
mutation($input: CreateUserInput!) {
|
|
createUser(input: $input) {
|
|
profileEdge {
|
|
node { id }
|
|
}
|
|
}
|
|
}
|
|
' "$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" \
|
|
"Chief Information Security Officer" \
|
|
"jane.cooper@dev.probo.test"
|
|
create_person "Marcus Chen" \
|
|
"Security Engineer" \
|
|
"marcus.chen@dev.probo.test"
|
|
create_person "Sofia Rodriguez" \
|
|
"Compliance Manager" \
|
|
"sofia.rodriguez@dev.probo.test"
|
|
create_person "David Kim" \
|
|
"IT Administrator" \
|
|
"david.kim@dev.probo.test"
|
|
create_person "Emily Nakamura" \
|
|
"VP Engineering" \
|
|
"emily.nakamura@dev.probo.test"
|
|
create_person "James O'Brien" \
|
|
"Head of People" \
|
|
"james.obrien@dev.probo.test"
|
|
create_person "Priya Patel" \
|
|
"Data Protection Officer" \
|
|
"priya.patel@dev.probo.test"
|
|
create_person "Alex Thompson" \
|
|
"DevOps Lead" \
|
|
"alex.thompson@dev.probo.test"
|
|
|
|
echo " 8 people created"
|
|
|
|
echo " Creating frameworks and controls..."
|
|
|
|
create_framework() {
|
|
local name="$1"
|
|
local desc="$2"
|
|
|
|
local resp
|
|
resp=$(prb_api "createFramework: $name" '
|
|
mutation($input: CreateFrameworkInput!) {
|
|
createFramework(input: $input) {
|
|
frameworkEdge {
|
|
node { id }
|
|
}
|
|
}
|
|
}
|
|
' -f input="$(
|
|
jo \
|
|
organizationId="$ORG_ID" \
|
|
name="$name" \
|
|
description="$desc"
|
|
)")
|
|
local id
|
|
id=$(echo "$resp" | jq -r '.data.createFramework.frameworkEdge.node.id // empty')
|
|
if [ -z "$id" ]; then
|
|
echo "ERROR (createFramework: $name): no framework id in response" >&2
|
|
exit 1
|
|
fi
|
|
echo "$id"
|
|
}
|
|
|
|
create_control() {
|
|
local framework_id="$1"
|
|
local section="$2"
|
|
local name="$3"
|
|
local desc="$4"
|
|
|
|
$PRB control create \
|
|
--framework "$framework_id" \
|
|
--section-title "$section" \
|
|
--name "$name" \
|
|
--description "$desc" >/dev/null
|
|
}
|
|
|
|
# ISO 27001:2022
|
|
ISO_ID=$(create_framework \
|
|
"ISO 27001:2022" \
|
|
"International standard for information security management systems")
|
|
|
|
create_control "$ISO_ID" \
|
|
"A.5.1" \
|
|
"Policies for information security" \
|
|
"Management direction for information security shall be established"
|
|
create_control "$ISO_ID" \
|
|
"A.5.2" \
|
|
"Information security roles and responsibilities" \
|
|
"Information security roles and responsibilities shall be defined and allocated"
|
|
create_control "$ISO_ID" \
|
|
"A.5.3" \
|
|
"Segregation of duties" \
|
|
"Conflicting duties and areas of responsibility shall be segregated"
|
|
create_control "$ISO_ID" \
|
|
"A.5.10" \
|
|
"Acceptable use of information" \
|
|
"Rules for the acceptable use of information shall be identified and documented"
|
|
create_control "$ISO_ID" \
|
|
"A.5.23" \
|
|
"Information security for cloud services" \
|
|
"Processes for acquisition, use, management and exit from cloud services shall be established"
|
|
create_control "$ISO_ID" \
|
|
"A.5.34" \
|
|
"Privacy and protection of PII" \
|
|
"The organization shall identify and meet requirements regarding privacy and protection of PII"
|
|
create_control "$ISO_ID" \
|
|
"A.6.1" \
|
|
"Screening" \
|
|
"Background verification checks on all candidates for employment shall be carried out"
|
|
create_control "$ISO_ID" \
|
|
"A.6.2" \
|
|
"Terms and conditions of employment" \
|
|
"Employment contractual agreements shall state personnel and organization responsibilities for information security"
|
|
create_control "$ISO_ID" \
|
|
"A.6.3" \
|
|
"Information security awareness training" \
|
|
"Personnel and relevant interested parties shall receive appropriate information security awareness education and training"
|
|
create_control "$ISO_ID" \
|
|
"A.7.1" \
|
|
"Physical security perimeters" \
|
|
"Security perimeters shall be defined and used to protect areas that contain information and information processing facilities"
|
|
create_control "$ISO_ID" \
|
|
"A.7.2" \
|
|
"Physical entry" \
|
|
"Secure areas shall be protected by appropriate entry controls and access points"
|
|
create_control "$ISO_ID" \
|
|
"A.8.1" \
|
|
"User endpoint devices" \
|
|
"Information stored on, processed by or accessible via user endpoint devices shall be protected"
|
|
create_control "$ISO_ID" \
|
|
"A.8.2" \
|
|
"Privileged access rights" \
|
|
"The allocation and use of privileged access rights shall be restricted and managed"
|
|
create_control "$ISO_ID" \
|
|
"A.8.3" \
|
|
"Information access restriction" \
|
|
"Access to information and other associated assets shall be restricted"
|
|
create_control "$ISO_ID" \
|
|
"A.8.5" \
|
|
"Secure authentication" \
|
|
"Secure authentication technologies and procedures shall be established and implemented"
|
|
create_control "$ISO_ID" \
|
|
"A.8.9" \
|
|
"Configuration management" \
|
|
"Configurations, including security configurations, of hardware, software, services and networks shall be established and managed"
|
|
create_control "$ISO_ID" \
|
|
"A.8.15" \
|
|
"Logging" \
|
|
"Logs that record activities, exceptions, faults and other relevant events shall be produced and stored"
|
|
create_control "$ISO_ID" \
|
|
"A.8.16" \
|
|
"Monitoring activities" \
|
|
"Networks, systems and applications shall be monitored for anomalous behavior"
|
|
|
|
echo " ISO 27001:2022 — 18 controls"
|
|
|
|
# SOC 2
|
|
SOC2_ID=$(create_framework \
|
|
"SOC 2" \
|
|
"Service Organization Control 2 trust services criteria")
|
|
|
|
create_control "$SOC2_ID" \
|
|
"CC1.1" \
|
|
"Integrity and Ethical Values" \
|
|
"The entity demonstrates a commitment to integrity and ethical values"
|
|
create_control "$SOC2_ID" \
|
|
"CC1.2" \
|
|
"Board Independence" \
|
|
"The board of directors demonstrates independence from management"
|
|
create_control "$SOC2_ID" \
|
|
"CC1.3" \
|
|
"Management Structure" \
|
|
"Management establishes structures, reporting lines, and authorities"
|
|
create_control "$SOC2_ID" \
|
|
"CC2.1" \
|
|
"Internal Communication" \
|
|
"The entity obtains or generates and uses relevant information to support internal control"
|
|
create_control "$SOC2_ID" \
|
|
"CC3.1" \
|
|
"Objective Specification" \
|
|
"The entity specifies objectives with sufficient clarity"
|
|
create_control "$SOC2_ID" \
|
|
"CC3.2" \
|
|
"Risk Identification" \
|
|
"The entity identifies risks to the achievement of its objectives"
|
|
create_control "$SOC2_ID" \
|
|
"CC3.3" \
|
|
"Fraud Risk Assessment" \
|
|
"The entity considers the potential for fraud in assessing risks"
|
|
create_control "$SOC2_ID" \
|
|
"CC4.1" \
|
|
"Monitoring Activities" \
|
|
"The entity selects, develops, and performs ongoing evaluations"
|
|
create_control "$SOC2_ID" \
|
|
"CC5.1" \
|
|
"Control Selection" \
|
|
"The entity selects and develops control activities that mitigate risks"
|
|
create_control "$SOC2_ID" \
|
|
"CC5.2" \
|
|
"Technology General Controls" \
|
|
"The entity selects and develops general control activities over technology"
|
|
create_control "$SOC2_ID" \
|
|
"CC6.1" \
|
|
"Logical and Physical Access" \
|
|
"The entity implements logical access security over protected assets"
|
|
create_control "$SOC2_ID" \
|
|
"CC6.2" \
|
|
"User Registration and Authorization" \
|
|
"The entity registers and authorizes new internal and external users"
|
|
create_control "$SOC2_ID" \
|
|
"CC6.3" \
|
|
"Role-Based Access" \
|
|
"The entity authorizes access based on authorization credentials and role"
|
|
create_control "$SOC2_ID" \
|
|
"CC7.1" \
|
|
"Infrastructure and Software Monitoring" \
|
|
"The entity uses detection and monitoring procedures to identify changes to configurations"
|
|
create_control "$SOC2_ID" \
|
|
"CC7.2" \
|
|
"Change Management" \
|
|
"The entity monitors system components for anomalies indicative of malicious acts"
|
|
|
|
echo " SOC 2 — 15 controls"
|
|
|
|
# GDPR
|
|
GDPR_ID=$(create_framework \
|
|
"GDPR" \
|
|
"General Data Protection Regulation")
|
|
|
|
create_control "$GDPR_ID" \
|
|
"Art.5" \
|
|
"Principles relating to processing" \
|
|
"Personal data shall be processed lawfully, fairly and in a transparent manner"
|
|
create_control "$GDPR_ID" \
|
|
"Art.6" \
|
|
"Lawfulness of processing" \
|
|
"Processing shall be lawful only if at least one legal basis applies"
|
|
create_control "$GDPR_ID" \
|
|
"Art.7" \
|
|
"Conditions for consent" \
|
|
"Where processing is based on consent, the controller shall be able to demonstrate consent"
|
|
create_control "$GDPR_ID" \
|
|
"Art.13" \
|
|
"Information to be provided" \
|
|
"Information to be provided where personal data are collected from the data subject"
|
|
create_control "$GDPR_ID" \
|
|
"Art.15" \
|
|
"Right of access" \
|
|
"The data subject shall have the right to obtain confirmation of processing"
|
|
create_control "$GDPR_ID" \
|
|
"Art.17" \
|
|
"Right to erasure" \
|
|
"The data subject shall have the right to obtain erasure of personal data"
|
|
create_control "$GDPR_ID" \
|
|
"Art.25" \
|
|
"Data protection by design" \
|
|
"The controller shall implement appropriate technical and organizational measures"
|
|
create_control "$GDPR_ID" \
|
|
"Art.30" \
|
|
"Records of processing activities" \
|
|
"Each controller shall maintain a record of processing activities"
|
|
create_control "$GDPR_ID" \
|
|
"Art.32" \
|
|
"Security of processing" \
|
|
"The controller shall implement appropriate technical and organizational measures to ensure security"
|
|
create_control "$GDPR_ID" \
|
|
"Art.33" \
|
|
"Notification of personal data breach" \
|
|
"The controller shall notify the supervisory authority within 72 hours"
|
|
|
|
echo " GDPR — 10 controls"
|
|
|
|
echo " Creating risks..."
|
|
|
|
create_risk() {
|
|
local name="$1"
|
|
local category="$2"
|
|
local treatment="$3"
|
|
local likelihood="$4"
|
|
local impact="$5"
|
|
|
|
$PRB risk create \
|
|
--org "$ORG_ID" \
|
|
--name "$name" \
|
|
--category "$category" \
|
|
--treatment "$treatment" \
|
|
--inherent-likelihood "$likelihood" \
|
|
--inherent-impact "$impact" >/dev/null
|
|
}
|
|
|
|
create_risk \
|
|
"Unauthorized access to production systems" \
|
|
SECURITY MITIGATED 4 5
|
|
create_risk \
|
|
"Sensitive data exfiltration by insider threat" \
|
|
SECURITY MITIGATED 2 5
|
|
create_risk \
|
|
"Phishing campaign targeting employees" \
|
|
SECURITY MITIGATED 4 3
|
|
create_risk \
|
|
"Ransomware via supply chain compromise" \
|
|
SECURITY MITIGATED 2 5
|
|
create_risk \
|
|
"Credential stuffing on customer login portal" \
|
|
SECURITY MITIGATED 3 4
|
|
create_risk \
|
|
"Stolen developer laptop with source code" \
|
|
SECURITY MITIGATED 3 3
|
|
create_risk \
|
|
"API key leaked in public repository" \
|
|
SECURITY MITIGATED 3 4
|
|
create_risk \
|
|
"Social engineering of support staff" \
|
|
SECURITY MITIGATED 3 3
|
|
create_risk \
|
|
"Brute force attack on admin panel" \
|
|
SECURITY MITIGATED 2 4
|
|
create_risk \
|
|
"Man-in-the-middle attack on internal network" \
|
|
SECURITY MITIGATED 1 4
|
|
create_risk \
|
|
"Malicious browser extension on corporate devices" \
|
|
SECURITY MITIGATED 2 3
|
|
create_risk \
|
|
"Unauthorized physical access to server room" \
|
|
SECURITY MITIGATED 1 5
|
|
|
|
create_risk \
|
|
"Third-party SaaS data breach" \
|
|
OPERATIONAL TRANSFERRED 3 4
|
|
create_risk \
|
|
"Cloud region outage causing service disruption" \
|
|
OPERATIONAL ACCEPTED 2 4
|
|
create_risk \
|
|
"Database corruption from failed migration" \
|
|
OPERATIONAL MITIGATED 2 5
|
|
create_risk \
|
|
"Loss of key personnel with critical knowledge" \
|
|
OPERATIONAL MITIGATED 3 3
|
|
create_risk \
|
|
"Backup restoration failure during disaster recovery" \
|
|
OPERATIONAL MITIGATED 2 5
|
|
create_risk \
|
|
"DNS hijacking of company domain" \
|
|
OPERATIONAL MITIGATED 1 5
|
|
create_risk \
|
|
"CDN provider outage affecting content delivery" \
|
|
OPERATIONAL ACCEPTED 2 3
|
|
create_risk \
|
|
"Email service compromise leaking internal comms" \
|
|
OPERATIONAL MITIGATED 2 4
|
|
|
|
create_risk \
|
|
"Non-compliance with GDPR data subject rights" \
|
|
COMPLIANCE MITIGATED 2 4
|
|
create_risk \
|
|
"Failure to meet SOC 2 audit requirements" \
|
|
COMPLIANCE MITIGATED 2 4
|
|
create_risk \
|
|
"Breach notification deadline missed" \
|
|
COMPLIANCE MITIGATED 1 5
|
|
create_risk \
|
|
"Inadequate data processing agreements with third parties" \
|
|
COMPLIANCE MITIGATED 3 3
|
|
create_risk \
|
|
"Employee data retained beyond legal period" \
|
|
COMPLIANCE MITIGATED 2 3
|
|
create_risk \
|
|
"Cross-border data transfer without safeguards" \
|
|
COMPLIANCE MITIGATED 2 4
|
|
create_risk \
|
|
"Cookie consent mechanism non-compliant" \
|
|
COMPLIANCE MITIGATED 3 2
|
|
create_risk \
|
|
"Incomplete records of processing activities" \
|
|
COMPLIANCE MITIGATED 3 3
|
|
|
|
create_risk \
|
|
"Cloud infrastructure misconfiguration exposing data" \
|
|
TECHNICAL MITIGATED 3 4
|
|
create_risk \
|
|
"Loss of encryption keys for production database" \
|
|
TECHNICAL MITIGATED 1 5
|
|
create_risk \
|
|
"Denial of service attack on customer-facing APIs" \
|
|
TECHNICAL MITIGATED 3 3
|
|
create_risk \
|
|
"TLS certificate expiration causing service outage" \
|
|
TECHNICAL MITIGATED 2 3
|
|
create_risk \
|
|
"Container escape vulnerability in production cluster" \
|
|
TECHNICAL MITIGATED 1 5
|
|
create_risk \
|
|
"Dependency with known CVE deployed to production" \
|
|
TECHNICAL MITIGATED 3 4
|
|
create_risk \
|
|
"Logging pipeline failure hiding security incidents" \
|
|
TECHNICAL MITIGATED 2 4
|
|
|
|
echo " 35 risks created"
|
|
|
|
echo " Creating third parties..."
|
|
|
|
create_third_party() {
|
|
local name="$1"
|
|
local description="$2"
|
|
|
|
local resp
|
|
resp=$(prb_api "createThirdParty: $name" '
|
|
mutation($input: CreateThirdPartyInput!) {
|
|
createThirdParty(input: $input) {
|
|
thirdPartyEdge {
|
|
node { id }
|
|
}
|
|
}
|
|
}
|
|
' -f input="$(
|
|
jo \
|
|
organizationId="$ORG_ID" \
|
|
name="$name" \
|
|
description="$description"
|
|
)")
|
|
local id
|
|
id=$(echo "$resp" | jq -r '.data.createThirdParty.thirdPartyEdge.node.id // empty')
|
|
if [ -z "$id" ]; then
|
|
echo "ERROR (createThirdParty: $name): no third party id in response" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
create_third_party "Amazon Web Services" \
|
|
"Cloud infrastructure and compute"
|
|
create_third_party "Google Cloud Platform" \
|
|
"BigQuery analytics and AI services"
|
|
create_third_party "Google Workspace" \
|
|
"Email, calendar, and productivity suite"
|
|
create_third_party "Microsoft 365" \
|
|
"Office productivity and collaboration"
|
|
create_third_party "Datadog" \
|
|
"Application monitoring and observability"
|
|
create_third_party "PagerDuty" \
|
|
"Incident management and on-call scheduling"
|
|
create_third_party "Slack" \
|
|
"Team communication and messaging"
|
|
create_third_party "GitHub" \
|
|
"Source code management and CI/CD"
|
|
create_third_party "Stripe" \
|
|
"Payment processing and billing"
|
|
create_third_party "Salesforce" \
|
|
"Customer relationship management"
|
|
create_third_party "HubSpot" \
|
|
"Marketing automation and CRM"
|
|
create_third_party "Notion" \
|
|
"Documentation and knowledge management"
|
|
create_third_party "1Password" \
|
|
"Enterprise password management"
|
|
create_third_party "Okta" \
|
|
"Identity and access management"
|
|
create_third_party "CrowdStrike" \
|
|
"Endpoint protection and threat intelligence"
|
|
create_third_party "Vanta" \
|
|
"Compliance automation and monitoring"
|
|
create_third_party "Jira" \
|
|
"Project management and issue tracking"
|
|
create_third_party "Cloudflare" \
|
|
"CDN, DNS, and DDoS protection"
|
|
create_third_party "Twilio SendGrid" \
|
|
"Transactional email delivery"
|
|
create_third_party "Snowflake" \
|
|
"Cloud data warehouse"
|
|
|
|
echo " 20 third parties created"
|
|
|
|
echo " Creating measures..."
|
|
|
|
create_measure() {
|
|
local name="$1"
|
|
local category="$2"
|
|
|
|
local resp
|
|
resp=$(prb_api "createMeasure: $name" '
|
|
mutation($input: CreateMeasureInput!) {
|
|
createMeasure(input: $input) {
|
|
measureEdge {
|
|
node { id }
|
|
}
|
|
}
|
|
}
|
|
' -f input="$(
|
|
jo \
|
|
organizationId="$ORG_ID" \
|
|
name="$name" \
|
|
category="$category"
|
|
)")
|
|
local id
|
|
id=$(echo "$resp" | jq -r '.data.createMeasure.measureEdge.node.id // empty')
|
|
if [ -z "$id" ]; then
|
|
echo "ERROR (createMeasure: $name): no measure id in response" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
create_measure "Information Security Policy" \
|
|
"POLICY"
|
|
create_measure "Access Control Policy" \
|
|
"POLICY"
|
|
create_measure "Incident Response Plan" \
|
|
"POLICY"
|
|
create_measure "Data Classification Policy" \
|
|
"POLICY"
|
|
create_measure "Acceptable Use Policy" \
|
|
"POLICY"
|
|
|
|
create_measure "Multi-Factor Authentication" \
|
|
"TECHNICAL"
|
|
create_measure "Endpoint Detection and Response" \
|
|
"TECHNICAL"
|
|
create_measure "Full Disk Encryption" \
|
|
"TECHNICAL"
|
|
create_measure "Automated Vulnerability Scanning" \
|
|
"TECHNICAL"
|
|
create_measure "Network Segmentation and Firewall Rules" \
|
|
"TECHNICAL"
|
|
create_measure "TLS Encryption in Transit" \
|
|
"TECHNICAL"
|
|
|
|
create_measure "Annual Security Awareness Training" \
|
|
"ORGANIZATIONAL"
|
|
create_measure "Quarterly Access Reviews" \
|
|
"ORGANIZATIONAL"
|
|
create_measure "Background Checks for New Hires" \
|
|
"ORGANIZATIONAL"
|
|
create_measure "Tabletop Disaster Recovery Exercises" \
|
|
"ORGANIZATIONAL"
|
|
|
|
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
|
|
}
|
|
|
|
# posture_evidence <platform> <check_key> <status> <os_version>
|
|
# Emits platform-shaped evidence JSON that ParseDevicePostureValue can turn
|
|
# into a non-UNKNOWN value for PASS/FAIL, or UNKNOWN/None for the rest.
|
|
posture_evidence() {
|
|
local platform="$1"
|
|
local check_key="$2"
|
|
local status="$3"
|
|
local os_version="$4"
|
|
|
|
case "$platform:$check_key" in
|
|
DARWIN:DISK_ENCRYPTION)
|
|
case "$status" in
|
|
PASS) jq -nc '{raw:"FileVault is On."}' ;;
|
|
FAIL) jq -nc '{raw:"FileVault is Off."}' ;;
|
|
*) jq -nc '{note:"fdesetup unavailable"}' ;;
|
|
esac
|
|
;;
|
|
LINUX:DISK_ENCRYPTION)
|
|
case "$status" in
|
|
PASS) jq -nc '{crypttab_present:true,crypttab_lines:["nvme0n1p3_crypt UUID=6c2f none luks,discard"]}' ;;
|
|
FAIL) jq -nc '{crypttab_present:false,lsblk:"nvme0n1 disk\nnvme0n1p2 part ext4 /"}' ;;
|
|
*) jq -nc '{crypttab_present:false,lsblk_error:"lsblk not found"}' ;;
|
|
esac
|
|
;;
|
|
WINDOWS:DISK_ENCRYPTION)
|
|
case "$status" in
|
|
PASS) jq -nc '{raw:"Conversion Status: Fully Encrypted\n Percentage Encrypted: 100%"}' ;;
|
|
FAIL) jq -nc '{raw:"Conversion Status: Fully Decrypted\n Percentage Encrypted: 0%"}' ;;
|
|
*) jq -nc '{note:"manage-bde not found"}' ;;
|
|
esac
|
|
;;
|
|
DARWIN:SCREEN_LOCK)
|
|
case "$status" in
|
|
PASS) jq -nc '{backend:"sysadminctl",mode:"seconds",seconds:900,raw:"screenLock delay is 900 seconds"}' ;;
|
|
FAIL) jq -nc '{backend:"sysadminctl",mode:"off",raw:"screenLock is off"}' ;;
|
|
*) jq -nc '{backend:"sysadminctl",error:"sysadminctl failed"}' ;;
|
|
esac
|
|
;;
|
|
LINUX:SCREEN_LOCK)
|
|
case "$status" in
|
|
PASS) jq -nc '{backend:"gnome",schema:"org.gnome.desktop.screensaver",lock_enabled:"true"}' ;;
|
|
FAIL) jq -nc '{backend:"gnome",schema:"org.gnome.desktop.screensaver",lock_enabled:"false"}' ;;
|
|
*) jq -nc '{backend:"gnome",error:"gsettings failed"}' ;;
|
|
esac
|
|
;;
|
|
WINDOWS:SCREEN_LOCK)
|
|
case "$status" in
|
|
PASS) jq -nc '{backend:"hkey_users",users:{"S-1-5-21-1004336348-1177238915-682003330-1001":"1"}}' ;;
|
|
FAIL) jq -nc '{backend:"hkey_users",users:{"S-1-5-21-1004336348-1177238915-682003330-1001":"0"}}' ;;
|
|
*) jq -nc '{backend:"hkey_users",users:{},note:"no interactive user hives loaded"}' ;;
|
|
esac
|
|
;;
|
|
DARWIN:FIREWALL_ENABLED)
|
|
case "$status" in
|
|
PASS) jq -nc '{backend:"defaults",global_state:"1"}' ;;
|
|
FAIL) jq -nc '{backend:"defaults",global_state:"0"}' ;;
|
|
*) jq -nc '{note:"no known firewall tool found"}' ;;
|
|
esac
|
|
;;
|
|
LINUX:FIREWALL_ENABLED)
|
|
case "$status" in
|
|
PASS) jq -nc '{backend:"ufw",raw:"Status: active"}' ;;
|
|
FAIL) jq -nc '{backend:"ufw",raw:"Status: inactive"}' ;;
|
|
*) jq -nc '{note:"no known firewall tool found"}' ;;
|
|
esac
|
|
;;
|
|
WINDOWS:FIREWALL_ENABLED)
|
|
case "$status" in
|
|
PASS) jq -nc '{backend:"Get-NetFirewallProfile",raw:"Domain=True;Private=True;Public=True",profiles:{Domain:"True",Private:"True",Public:"True"}}' ;;
|
|
FAIL) jq -nc '{backend:"Get-NetFirewallProfile",raw:"Domain=True;Private=True;Public=False",profiles:{Domain:"True",Private:"True",Public:"False"}}' ;;
|
|
*) jq -nc '{backend:"netsh",state_lines:[]}' ;;
|
|
esac
|
|
;;
|
|
DARWIN:TIME_SYNC)
|
|
case "$status" in
|
|
PASS) jq -nc '{raw:"Network Time: On"}' ;;
|
|
FAIL) jq -nc '{raw:"Network Time: Off"}' ;;
|
|
*) jq -nc '{note:"systemsetup unavailable"}' ;;
|
|
esac
|
|
;;
|
|
LINUX:TIME_SYNC)
|
|
case "$status" in
|
|
PASS) jq -nc '{raw:"Timezone=Europe/Paris\nLocalRTC=no\nCanNTP=yes\nNTP=yes\nNTPSynchronized=yes"}' ;;
|
|
FAIL) jq -nc '{raw:"Timezone=Europe/Paris\nLocalRTC=no\nCanNTP=yes\nNTP=yes\nNTPSynchronized=no"}' ;;
|
|
*) jq -nc '{note:"timedatectl not installed"}' ;;
|
|
esac
|
|
;;
|
|
WINDOWS:TIME_SYNC)
|
|
case "$status" in
|
|
PASS) jq -nc '{raw:"Leap Indicator: 0(no warning)\nStratum: 4 (secondary reference)\nSource: time.windows.com,0x8\nPoll Interval: 10"}' ;;
|
|
FAIL) jq -nc '{raw:"Leap Indicator: 3(not synchronized)\nStratum: 0 (unspecified)\nSource: Local CMOS Clock\nPoll Interval: 10"}' ;;
|
|
*) jq -nc '{note:"w32tm unavailable"}' ;;
|
|
esac
|
|
;;
|
|
DARWIN:OS_VERSION)
|
|
case "$status" in
|
|
UNKNOWN | NOT_APPLICABLE) jq -nc '{error:"sw_vers failed"}' ;;
|
|
*) jq -nc --arg v "$os_version" '{product_version:$v,build_version:"24E248"}' ;;
|
|
esac
|
|
;;
|
|
LINUX:OS_VERSION)
|
|
case "$status" in
|
|
UNKNOWN | NOT_APPLICABLE) jq -nc '{error:"os-release unreadable"}' ;;
|
|
*) jq -nc --arg v "$os_version" '{pretty_name:$v,version_id:$v,id:"linux"}' ;;
|
|
esac
|
|
;;
|
|
WINDOWS:OS_VERSION)
|
|
case "$status" in
|
|
UNKNOWN | NOT_APPLICABLE) jq -nc '{error:"wmic failed"}' ;;
|
|
*) jq -nc --arg v "$os_version" '{caption:$v}' ;;
|
|
esac
|
|
;;
|
|
DARWIN:AUTO_UPDATE)
|
|
case "$status" in
|
|
PASS) jq -nc '{backend:"defaults",AutomaticCheckEnabled:{source:"system",value:"1",enabled:true},AutomaticDownload:{source:"default",enabled:true}}' ;;
|
|
FAIL) jq -nc '{backend:"defaults",disabled_keys:["AutomaticDownload"]}' ;;
|
|
*) jq -nc '{backend:"defaults",indeterminate_keys:["ConfigDataInstall"]}' ;;
|
|
esac
|
|
;;
|
|
LINUX:AUTO_UPDATE)
|
|
case "$status" in
|
|
PASS) jq -nc '{backend:"unattended-upgrades",raw:"APT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Unattended-Upgrade \"1\";\n"}' ;;
|
|
FAIL) jq -nc '{backend:"unattended-upgrades",raw:"APT::Periodic::Update-Package-Lists \"0\";\nAPT::Periodic::Unattended-Upgrade \"0\";\n"}' ;;
|
|
*) jq -nc '{note:"unattended-upgrades not installed"}' ;;
|
|
esac
|
|
;;
|
|
WINDOWS:AUTO_UPDATE)
|
|
case "$status" in
|
|
PASS) jq -nc '{no_auto_update:"0",au_options:"4"}' ;;
|
|
FAIL) jq -nc '{no_auto_update:"",au_options:"2"}' ;;
|
|
*) jq -nc '{no_auto_update:"",au_options:"",wuauserv:""}' ;;
|
|
esac
|
|
;;
|
|
DARWIN:PASSWORD_POLICY)
|
|
case "$status" in
|
|
PASS) jq -nc '{raw_truncated:"<dict><key>policyCategoryPasswordContent</key><array/></dict>"}' ;;
|
|
FAIL) jq -nc '{raw_truncated:"There are no account policies for all users."}' ;;
|
|
*) jq -nc '{error:"pwpolicy failed"}' ;;
|
|
esac
|
|
;;
|
|
LINUX:PASSWORD_POLICY)
|
|
case "$status" in
|
|
PASS) jq -nc '{pass_min_len:"12",pass_max_days:"90",pass_min_len_value:12}' ;;
|
|
FAIL) jq -nc '{pass_min_len:"",pass_max_days:"99999",parse_error:"PASS_MIN_LEN not set"}' ;;
|
|
*) jq -nc '{error:"login.defs unreadable"}' ;;
|
|
esac
|
|
;;
|
|
WINDOWS:PASSWORD_POLICY)
|
|
case "$status" in
|
|
PASS) jq -nc '{raw:"Minimum password length: 8\n"}' ;;
|
|
FAIL) jq -nc '{raw:"Minimum password length: 0\n"}' ;;
|
|
*) jq -nc '{error:"net accounts failed"}' ;;
|
|
esac
|
|
;;
|
|
# REMOTE_LOGIN is inverted: PASS means remote access is Off / denied.
|
|
DARWIN:REMOTE_LOGIN)
|
|
case "$status" in
|
|
PASS) jq -nc '{raw:"Remote Login: Off"}' ;;
|
|
FAIL) jq -nc '{raw:"Remote Login: On"}' ;;
|
|
*) jq -nc '{error:"systemsetup failed"}' ;;
|
|
esac
|
|
;;
|
|
LINUX:REMOTE_LOGIN)
|
|
case "$status" in
|
|
PASS) jq -nc '{is_active:"inactive"}' ;;
|
|
FAIL) jq -nc '{is_active:"active"}' ;;
|
|
*) jq -nc '{is_active:""}' ;;
|
|
esac
|
|
;;
|
|
WINDOWS:REMOTE_LOGIN)
|
|
case "$status" in
|
|
PASS) jq -nc '{fdeny_ts_connections:"1"}' ;;
|
|
FAIL) jq -nc '{fdeny_ts_connections:"0"}' ;;
|
|
*) jq -nc '{error:"registry read failed"}' ;;
|
|
esac
|
|
;;
|
|
DARWIN:MALWARE_PROTECTION)
|
|
case "$status" in
|
|
PASS) jq -nc '{engine:"XProtect",version:"5260"}' ;;
|
|
FAIL) jq -nc '{engine:"XProtect",note:"XProtect.meta.plist not found in expected locations"}' ;;
|
|
*) jq -nc '{note:"XProtect check skipped"}' ;;
|
|
esac
|
|
;;
|
|
LINUX:MALWARE_PROTECTION)
|
|
case "$status" in
|
|
PASS) jq -nc '{active:["ClamAV"],installed:[]}' ;;
|
|
FAIL) jq -nc '{active:[],installed:["ClamAV"]}' ;;
|
|
*) jq -nc '{active:[],installed:[]}' ;;
|
|
esac
|
|
;;
|
|
WINDOWS:MALWARE_PROTECTION)
|
|
case "$status" in
|
|
PASS) jq -nc '{antivirus_enabled:true,real_time_protection:true,am_service_enabled:true}' ;;
|
|
FAIL) jq -nc '{antivirus_enabled:false,real_time_protection:false,am_service_enabled:false}' ;;
|
|
*) jq -nc '{note:"defender status unavailable"}' ;;
|
|
esac
|
|
;;
|
|
*)
|
|
jq -nc '{}'
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# agent_postures <api_key> <platform> <os_version> <CHECK_KEY:STATUS>...
|
|
agent_postures() {
|
|
local api_key="$1"
|
|
local platform="$2"
|
|
local os_version="$3"
|
|
shift 3
|
|
|
|
local now
|
|
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
local results='[]'
|
|
local pair check_key status evidence
|
|
for pair in "$@"; do
|
|
check_key="${pair%%:*}"
|
|
status="${pair#*:}"
|
|
evidence=$(posture_evidence "$platform" "$check_key" "$status" "$os_version")
|
|
results=$(jq -nc \
|
|
--argjson results "$results" \
|
|
--arg check_key "$check_key" \
|
|
--arg status "$status" \
|
|
--arg observed_at "$now" \
|
|
--argjson evidence "$evidence" \
|
|
'$results + [{
|
|
check_key: $check_key,
|
|
status: $status,
|
|
observed_at: $observed_at,
|
|
evidence: $evidence
|
|
}]')
|
|
done
|
|
|
|
local body
|
|
body=$(jq -nc --argjson results "$results" '{results: $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
|
|
echo " request: $body" >&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 <owner_id> <hostname> <platform> <os_version> <serial> <CHECK_KEY:STATUS>...
|
|
# 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" "$platform" "$os_version" "$@"
|
|
|
|
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"
|
|
echo " Password: $PASSWORD"
|
|
echo " Org: $ORG_NAME"
|
|
echo ""
|
|
echo " Created:"
|
|
echo " 3 frameworks, 43 controls"
|
|
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"
|
|
echo " export PROBO_TOKEN=$PROBO_TOKEN"
|