[#389] GVM target import: gvm-script (gmp.send_command, lxml parse) + IANA port-list injection; feed jq rewrite (record-bound, no IFS collapse)
ci / audit (push) Successful in 24s

63/63 GLPI targets imported into gvmd (0 failures). Admin creds in
~/.creds/gvm-test.env. Import is idempotent (deletes glpi-* first).
https://projects.knownelement.com/issues/389
This commit is contained in:
2026-09-05 11:21:36 -05:00
parent 2d75a3865f
commit ffc82467a9
2 changed files with 48 additions and 22 deletions
+10 -19
View File
@@ -56,34 +56,25 @@ else
fetch_inventory > "$INVENTORY_JSON" || exit 1
fi
# Normalize: one record per line with parsed IPs
NORMALIZED=$(jq -r '
case "$FORMAT" in
csv)
printf 'glpi_id,name,ts_ip,mgmt_ip,serial,last_inventory,agent_fed\n'
jq -r '
.[]
| select( ($ONLY_DYNAMIC == 0 or .is_dynamic == 1) )
| ( .comment // "" ) as $c
| ( [$c | scan("ts=([0-9.]+)")] | first | first // "" ) as $ts
| ( [$c | scan("mgmt[ =]([0-9.]+)")] | first | first // "" ) as $mgmt
| select( $INCLUDE_NO_IP == 1 or $ts != "" or $mgmt != "" )
| [ (.id|tostring), .name, $ts, $mgmt, (.serial // "-"),
(.last_inventory_update // "-"), ((.is_dynamic // 0)|tostring) ]
| @tsv' --argjson ONLY_DYNAMIC "$ONLY_DYNAMIC" --argjson INCLUDE_NO_IP "$INCLUDE_NO_IP" "$INVENTORY_JSON")
[ -n "$NORMALIZED" ] || { echo "no scan targets parsed from GLPI" >&2; exit 2; }
case "$FORMAT" in
csv)
printf 'glpi_id,name,ts_ip,mgmt_ip,serial,last_inventory,agent_fed\n'
printf '%s\n' "$NORMALIZED"
| [ (.id|tostring), .name, $ts, $mgmt, (.serial // ""),
(.last_inventory_update // ""), ((.is_dynamic // 0)|tostring) ]
| @csv' --argjson ONLY_DYNAMIC "$ONLY_DYNAMIC" --argjson INCLUDE_NO_IP "$INCLUDE_NO_IP" "$INVENTORY_JSON"
;;
gmp)
printf '<!-- GMP create_target fragments generated from GLPI CMDB %s. Review hosts before importing. -->\n' "$(date -Is)"
printf '%s\n' "$NORMALIZED" | while IFS="$(printf '\t')" read -r id name ts mgmt _serial inv dyn; do
hosts="$ts"
if [ -n "$mgmt" ]; then hosts="${hosts:+$hosts,}$mgmt"; fi
[ -n "$hosts" ] || continue
printf '<create_target><name>glpi-%s-%s</name><hosts>%s</hosts><comment>GLPI #%s inv=%s agent=%s</comment><alive_tests>Scan Config Default</alive_tests></create_target>\n' \
"$id" "$(printf '%s' "$name" | tr -c '[:alnum:].-' '-')" "$hosts" "$id" "$inv" "$dyn"
done
# jq emits the complete element per record (host list built in jq — avoids
# bash read() IFS-collapse on empty TSV fields; record bound via . as $rec)
jq -r ' .[] | . as $rec | ( $rec.comment // "" ) as $c | ( [$c | scan("ts=([0-9.]+)")] | first | first // "" ) as $ts | ( [$c | scan("mgmt[ =]([0-9.]+)")] | first | first // "" ) as $mgmt | [ $ts, $mgmt ] | map(select(length > 0)) | join(",") as $hosts | select( $hosts != "" ) | $rec.name as $n | "<create_target><name>glpi-\($rec.id)-\($n | gsub("[^a-zA-Z0-9.-]"; "-"))</name><hosts>\($hosts)</hosts><comment>GLPI #\($rec.id) agent=\($rec.is_dynamic // 0)</comment></create_target>"' --argjson ONLY_DYNAMIC "$ONLY_DYNAMIC" --argjson INCLUDE_NO_IP "$INCLUDE_NO_IP" "$INVENTORY_JSON"
;;
*) echo "bad format: $FORMAT" >&2; exit 1 ;;
esac
+35
View File
@@ -0,0 +1,35 @@
import re
from lxml import etree
data = open('/tmp/targets.gmp').read()
cmds = re.findall(r'<create_target>.*?</create_target>', data, re.S)
pl = etree.fromstring(gmp.send_command('<get_port_lists filter="rows=-1"/>').encode())
plist_id = None
for p in pl.findall('.//port_list'):
if 'All IANA assigned TCP' in (p.findtext('name') or ''):
plist_id = p.get('id')
break
if not plist_id:
raise SystemExit('no IANA TCP port list found')
# clean previous glpi-* targets (idempotent re-import)
existing = etree.fromstring(gmp.send_command('<get_targets filter="rows=-1"/>').encode())
for t in existing.findall('.//target'):
if (t.findtext('name') or '').startswith('glpi-'):
gmp.send_command(f'<delete_target target_id="{t.get("id")}"/>')
ok = fail = 0
for c in cmds:
frag = etree.fromstring(c.encode())
el = etree.SubElement(frag, 'port_list')
el.set('id', plist_id)
root = etree.fromstring(gmp.send_command(etree.tostring(frag).decode()).encode())
st = root.get('status')
if st == '201':
ok += 1
else:
fail += 1
if fail <= 3:
resp = root.findtext('.//response')
print('FAIL', st, (resp or etree.tostring(root).decode())[:180])
print(f'imported={ok} failed={fail}')