feat(fixtures): curated demo targets + seed script + golden PLC baselines (#187)
CI / Check (push) Skipped
CI / Check (pull_request) Failing after 3m1s
CI / Detect Changes (pull_request) Skipped
CI / Deploy Agent (pull_request) Skipped
CI / Deploy Dashboard (pull_request) Skipped
CI / Deploy Docs (pull_request) Skipped
CI / Deploy MCP (pull_request) Skipped

Versioned, reproducible set of representative targets so every scan path can
be exercised repeatably and the nightly regression (#188) has a baseline.

- fixtures/demo-targets/targets.json: 5 targets — PlcSps composite
  (pump_station.st + pump_fbd.xml + optional Modbus live URL + optional
  firmware image), PlcSps pure (conveyor.xml + traffic_light.st), plain git
  SAST (sharang/cra-vuln-demo, pinned), WebApp (juice-shop v19.2.1 + live
  URL), FirmwareRtos (zephyr example-application, pinned). Each carries an
  `expect` golden baseline (min_findings, sast_rule_ids, cwes, control_refs,
  min_sbom_components, scans_offered, pentest_supported, detected_facts).
- compliance-agent::fixtures: typed loader (DemoTargets/DemoTarget/
  DemoArtifact/Expect) + lib tests that keep the manifest well-formed and
  assert the PLC baselines offline by running analyze_tree over the checked-in
  fixtures (runs in the normal --lib CI job).
- scripts/seed-demo-targets.sh: curl+jq seeder over the public onboarding API
  (create → upload → detect → optional --scan), --only, --reset (deletes only
  the "Demo · " prefix), env overrides for infra-dependent artifacts.
- docs/guide/demo-targets.md + sidebar entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgxGHn22YEfQz5fLHSHkLv
This commit is contained in:
Sharang Parnerkar
2026-08-31 14:33:01 +02:00
co-authored by Claude Fable 5
parent 0834547a74
commit 3ac4b34c29
6 changed files with 732 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
# Seed the curated demo targets (#187) into a running compliance-agent.
#
# Reads fixtures/demo-targets/targets.json and, for every target:
# 1. POST /api/v1/targets (name = name_prefix + name)
# 2. POST /api/v1/targets/{id}/artifacts/upload for each `upload` artifact
# 3. POST /api/v1/targets/{id}/detect (surface detected facts)
# 4. POST /api/v1/targets/{id}/scan (only with --scan)
#
# Artifact env overrides (see the manifest): DEMO_WEB_URL, DEMO_PLC_MODBUS_URL,
# DEMO_PLC_FIRMWARE_IMAGE. Optional artifacts whose env var is unset are
# skipped, so the set seeds on a bare laptop; set them on comp-dev / Orca.
#
# Usage:
# scripts/seed-demo-targets.sh # seed all
# scripts/seed-demo-targets.sh --scan # seed + trigger first scan
# scripts/seed-demo-targets.sh --only web-juice-shop,git-cra-vuln-demo
# scripts/seed-demo-targets.sh --reset # delete every "Demo · " target
# scripts/seed-demo-targets.sh --reset --scan # reset, reseed, scan
#
# Env:
# AGENT_URL base URL of the agent (default http://localhost:3011)
# AGENT_TOKEN bearer token; omit for dev mode (no Keycloak → dev tenant)
# MANIFEST alternative manifest path
set -euo pipefail
AGENT_URL="${AGENT_URL:-http://localhost:3011}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MANIFEST="${MANIFEST:-$ROOT/fixtures/demo-targets/targets.json}"
DO_SCAN=0
DO_RESET=0
ONLY=""
while [[ $# -gt 0 ]]; do
case "$1" in
--scan) DO_SCAN=1 ;;
--reset) DO_RESET=1 ;;
--only) ONLY="$2"; shift ;;
-h|--help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
shift
done
for bin in jq curl; do
command -v "$bin" >/dev/null || { echo "need $bin" >&2; exit 1; }
done
[[ -f "$MANIFEST" ]] || { echo "manifest not found: $MANIFEST" >&2; exit 1; }
AUTH=()
[[ -n "${AGENT_TOKEN:-}" ]] && AUTH=(-H "Authorization: Bearer ${AGENT_TOKEN}")
green() { printf '\033[32m%s\033[0m' "$*"; }
yellow() { printf '\033[33m%s\033[0m' "$*"; }
red() { printf '\033[31m%s\033[0m' "$*"; }
# api METHOD PATH [curl args...] → body on stdout; non-2xx → exit 1 with body.
api() {
local method="$1" path="$2"; shift 2
local out code
out=$(curl -sS -X "$method" "${AGENT_URL}${path}" "${AUTH[@]}" -w '\n%{http_code}' "$@")
code="${out##*$'\n'}"
out="${out%$'\n'*}"
if [[ "$code" != 2* ]]; then
echo "$(red "HTTP $code") $method $path" >&2
echo "$out" >&2
return 1
fi
printf '%s' "$out"
}
PREFIX="$(jq -r '.name_prefix' "$MANIFEST")"
reset_targets() {
echo "== reset: deleting targets named '${PREFIX}*'"
local list ids
list=$(api GET "/api/v1/targets?limit=500")
ids=$(jq -r --arg p "$PREFIX" '.data[] | select(.name | startswith($p)) | ._id."$oid"' <<<"$list")
local n=0
for id in $ids; do
api DELETE "/api/v1/targets/${id}" >/dev/null && n=$((n + 1))
done
echo " deleted $n"
}
# Build the JSON artifact list for reference-style (non-upload) artifacts.
# Prints one JSON object per artifact that should be created inline.
inline_artifacts() {
local target_json="$1"
jq -c '.artifacts[] | select(.upload == null and .upload_env == null)' <<<"$target_json" |
while IFS= read -r a; do
local kind env ref optional branch
kind=$(jq -r '.kind' <<<"$a")
env=$(jq -r '.source_ref_env // empty' <<<"$a")
ref=$(jq -r '.source_ref // empty' <<<"$a")
optional=$(jq -r '.optional // false' <<<"$a")
branch=$(jq -r '.branch // empty' <<<"$a")
if [[ -n "$env" && -n "${!env:-}" ]]; then
ref="${!env}"
elif [[ -n "$env" && "$optional" == "true" ]]; then
echo " $(yellow skip) $kind (set \$$env to include)" >&2
continue
fi
[[ -n "$ref" ]] || continue
jq -cn --arg k "$kind" --arg r "$ref" --arg b "$branch" \
'{kind:$k, source_ref:$r} + (if $b != "" then {branch:$b} else {} end)'
done
}
# Upload every `upload` / `upload_env` artifact of the target.
upload_artifacts() {
local id="$1" target_json="$2"
jq -c '.artifacts[] | select(.upload != null or .upload_env != null)' <<<"$target_json" |
while IFS= read -r a; do
local kind path env fmt optional
kind=$(jq -r '.kind' <<<"$a")
env=$(jq -r '.upload_env // empty' <<<"$a")
path=$(jq -r '.upload // empty' <<<"$a")
fmt=$(jq -r '.plc_format // empty' <<<"$a")
optional=$(jq -r '.optional // false' <<<"$a")
if [[ -n "$env" && -n "${!env:-}" ]]; then
path="${!env}"
elif [[ -n "$path" ]]; then
path="$ROOT/$path"
elif [[ "$optional" == "true" ]]; then
echo " $(yellow skip) $kind (set \$$env to include)" >&2
continue
fi
[[ -f "$path" ]] || { echo " $(red missing) $path" >&2; return 1; }
local form=(-F "file=@${path}" -F "kind=${kind}")
[[ -n "$fmt" ]] && form+=(-F "plc_format=${fmt}")
api POST "/api/v1/targets/${id}/artifacts/upload" "${form[@]}" >/dev/null
echo " $(green upload) $kind $(basename "$path")"
done
}
seed_target() {
local t="$1"
local key name type desc
key=$(jq -r '.key' <<<"$t")
name="${PREFIX}$(jq -r '.name' <<<"$t")"
type=$(jq -r '.target_type' <<<"$t")
desc=$(jq -r '.description // ""' <<<"$t")
echo "== $key ($type)"
local arts body resp id
arts=$(inline_artifacts "$t" | jq -cs '.')
body=$(jq -cn --arg n "$name" --arg tt "$type" --arg d "$desc" --argjson a "$arts" \
'{name:$n, target_type:$tt, description:$d, artifacts:$a}')
resp=$(api POST "/api/v1/targets" -H 'Content-Type: application/json' -d "$body")
id=$(jq -r '.data._id."$oid"' <<<"$resp")
echo " $(green created) $id $(jq -r '.data.artifacts|length' <<<"$resp") inline artifact(s)"
upload_artifacts "$id" "$t"
local det
det=$(api POST "/api/v1/targets/${id}/detect" -H 'Content-Type: application/json' -d '{}' || true)
if [[ -n "$det" ]]; then
echo " detect → $(jq -r '.data.classification.suggested // "n/a"' <<<"$det")"
fi
if [[ "$DO_SCAN" == 1 ]]; then
api POST "/api/v1/targets/${id}/scan" -H 'Content-Type: application/json' -d '{}' >/dev/null
echo " $(green scan) triggered"
fi
}
[[ "$DO_RESET" == 1 ]] && reset_targets
echo "== seeding from $MANIFEST$AGENT_URL"
jq -c '.targets[]' "$MANIFEST" | while IFS= read -r t; do
key=$(jq -r '.key' <<<"$t")
if [[ -n "$ONLY" && ",$ONLY," != *",$key,"* ]]; then
continue
fi
seed_target "$t"
done
echo "== done"