From 056412a311f065ac44aa3e099bfd43854e0c6000 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Thu, 3 Sep 2026 14:01:56 -0500 Subject: [PATCH] feat(cmdb): inventory reconcile + GLPI software catalog seeder [#705] #307 s6 rewritten post-verification: zone was ALREADY clean (zero DNS deletes); 12 stale CIs purged from GLPI, 3 alive off-cluster systems given proper CIs (tsys-cloudron, ultix-field/highside), 75-row seed regenerated. software-catalog.sh: 21 products + 59 installs (wazuh agents linked from live manager); version lookup fixed to client-side parent filter (GLPI ignores softwares_id search param). Detail: https://projects.knownelement.com/issues/705 --- cmdb/seed/software-catalog.sh | 169 ++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 cmdb/seed/software-catalog.sh diff --git a/cmdb/seed/software-catalog.sh b/cmdb/seed/software-catalog.sh new file mode 100644 index 0000000..1fcf029 --- /dev/null +++ b/cmdb/seed/software-catalog.sh @@ -0,0 +1,169 @@ +#!/bin/bash +# +# software-catalog.sh — seed the GLPI software catalog [#705] +# +# Usage: software-catalog.sh [--dry-run] +# Needs: ~/.creds/glpi.env (GLPI_URL, GLPI_APP_TOKEN, GLPI_USER_TOKEN) +# ~/.creds/wazuh.env optional (WAZUH_API_PASS) — for live agent installs +# +# Creates Software assets + SoftwareVersions and links installations to +# Computer CIs. Idempotent at all three levels (software, version, install). +# Versions are recorded ONLY where verified; unknowns say so — GLPI Agent +# auto-inventory (proposed) is the durable fix for version drift. +# +set -euo pipefail + +DRY=0 +[ "${1:-}" = "--dry-run" ] && DRY=1 + +CREDS="${HOME}/.creds/glpi.env" +# shellcheck disable=SC1090 +. "$CREDS" +: "${GLPI_URL:?}" "${GLPI_APP_TOKEN:?}" "${GLPI_USER_TOKEN:?}" +API="$GLPI_URL/apirest.php" + +api() { + local method="$1" path="$2" body="${3:-}" + if [ -n "$body" ]; then + curl -sS --max-time 30 -X "$method" -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Session-Token: $SESSION" -H "Content-Type: application/json" \ + -d "$body" "$API/$path" + else + curl -sS --max-time 30 -X "$method" -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Session-Token: $SESSION" "$API/$path" + fi +} + +SESSION=$(curl -sS --max-time 20 -H "Content-Type: application/json" \ + -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Authorization: user_token $GLPI_USER_TOKEN" \ + "$API/initSession" | jq -re '.session_token') +echo "— session ok" + +# software_id — existing id or empty +sw_id() { + api GET "Software?searchText%5Bname%5D=$(printf %s "$1" | sed 's/ /%20/g')" \ + | jq -r --arg n "$1" '[.[] | select(.name==$n)][0].id // empty' +} + +# version_id — GLPI ignores softwares_id as a search param, +# so pull the full list and filter client-side (parent AND name) +ver_id() { + api GET "SoftwareVersion?range=0-999" \ + | jq -r --arg s "$1" --arg v "$2" \ + '[.[] | select((.softwares_id|tostring)==$s and .name==$v)][0].id // empty' +} + +# computer_id +pc_id() { + api GET "Computer?searchText%5Bname%5D=$(printf %s "$1" | sed 's/ /%20/g')" \ + | jq -r --arg n "$1" '[.[] | select(.name==$n)][0].id // empty' +} + +# install_exists +install_exists() { + api GET "Item_SoftwareVersion?range=0-4999" \ + | jq -e --arg v "$1" --arg p "$2" \ + '[.[] | select(.softwareversions_id==($v|tonumber) and .items_id==($p|tonumber) and .itemtype=="Computer")] | length > 0' \ + >/dev/null 2>&1 +} + +# ensure — '-' = catalog only +ensure() { + local sw="$1" ver="$2" targets="$3" sid vid cid + sid=$(sw_id "$sw") + if [ -z "$sid" ]; then + if [ "$DRY" = 1 ]; then echo " DRY: software '$sw'"; sid=0; else + sid=$(api POST Software "{\"input\":{\"name\":\"$sw\",\"manufacturers_id\":0,\"comment\":\"seeded by software-catalog.sh (#705)\"}}" \ + | jq -r '.id // empty') + echo " + software '$sw' (id=$sid)" + fi + else + echo " = software '$sw' (id=$sid)" + fi + [ "$sid" = 0 ] && [ "$DRY" = 1 ] && return 0 + + vid=$(ver_id "$sid" "$ver") + if [ -z "$vid" ]; then + if [ "$DRY" = 1 ]; then echo " DRY: version '$ver'"; else + vid=$(api POST SoftwareVersion "{\"input\":{\"softwares_id\":\"$sid\",\"name\":\"$ver\",\"state\":0}}" \ + | jq -r '.id // empty') + echo " + version '$ver' (id=$vid)" + fi + else + echo " = version '$ver' (id=$vid)" + fi + [ -z "$vid" ] && return 0 + + [ "$targets" = "-" ] && return 0 + local IFS=',' + for t in $targets; do + cid=$(pc_id "$t") + if [ -z "$cid" ]; then echo " ! no CI named '$t' (skipped)"; continue; fi + if install_exists "$vid" "$cid"; then echo " = installed on $t"; continue; fi + if [ "$DRY" = 1 ]; then echo " DRY: install on $t"; else + api POST Item_SoftwareVersion \ + "{\"input\":{\"itemtype\":\"Computer\",\"items_id\":\"$cid\",\"softwareversions_id\":\"$vid\"}}" \ + | jq -re 'if type=="array" or .id then empty else . end' >/dev/null 2>&1 \ + && echo " ! install POST failed on $t" || echo " + installed on $t" + fi + done +} + +echo "— catalog rows" +# name | version | targets (comma list, '-' = catalog only) +while IFS='|' read -r sw ver targets; do + [ -z "$sw" ] && continue + ensure "$(echo "$sw" | xargs)" "$(echo "$ver" | xargs)" "$(echo "$targets" | xargs)" +done < /tmp/wz-agents.txt + while IFS= read -r a; do + [ "$a" = "000" ] && continue + cid=$(pc_id "$a") + if [ -z "$cid" ]; then echo " ! agent '$a' has no CI (skipped)"; continue; fi + if install_exists "$vid" "$cid"; then echo " = $a"; continue; fi + if [ "$DRY" = 1 ]; then echo " DRY: install on $a"; else + api POST Item_SoftwareVersion \ + "{\"input\":{\"itemtype\":\"Computer\",\"items_id\":\"$cid\",\"softwareversions_id\":\"$vid\"}}" >/dev/null \ + && echo " + $a" + fi + done < /tmp/wz-agents.txt +fi + +echo "— done"