Introduce make lint-shell / fmt-shell with shellcheck and shfmt (-i 2 -ci -bn), normalize first-party scripts, and document the new targets. Keep them out of make lint / fmt. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
937 lines
27 KiB
Bash
Executable File
937 lines
27 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
|
|
}
|
|
|
|
# agent_postures <api_key> <CHECK_KEY:STATUS>...
|
|
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 <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" "$@"
|
|
|
|
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"
|