#!/usr/bin/env bash
# Scantide Linux Local Device Check
# Version: 0.18-draft-version-feed-check

set -u

OUTDIR="${OUTDIR:-./scantide-linux-report}"
SCRIPT_VERSION="0.18"
VERSION_FEED_URL="${VERSION_FEED_URL:-https://www.scantide.com/helpfiles/ScantideLAN-version.json}"
SCANTIDE_CVE_API="${SCANTIDE_CVE_API:-https://www.scantide.com/cve_api.php}"
SCANTIDE_LIFECYCLE_API="${SCANTIDE_LIFECYCLE_API:-https://www.scantide.com/lifecycle_api.php}"
SCANTIDE_API_KEY="${SCANTIDE_API_KEY:-}"
SCANTIDE_EMAIL="${SCANTIDE_EMAIL:-}"

USE_SCANTIDE_API=true
USE_SCANTIDE_LIFECYCLE=true
DEEP_SCAN=false
CVE_TIMEOUT=90
LIFECYCLE_TIMEOUT=60
VERSION_TIMEOUT=10
CHECK_VERSION=true

TS="$(date '+%Y%m%d_%H%M%S')"
HOST="$(hostname -f 2>/dev/null || hostname)"
REPORT="$OUTDIR/ScantideLinuxLocalCheck_$TS.html"
PKGFILE="$OUTDIR/packages_$TS.txt"
SERVICEFILE="$OUTDIR/service_inventory_for_cve_$TS.txt"
CVE_PAYLOAD="$OUTDIR/cve_payload_$TS.json"
CVE_RESPONSE="$OUTDIR/cve_response_$TS.json"
CVE_RAW="$OUTDIR/scantide_cve_api_raw_$TS.txt"
LIFECYCLE_PAYLOAD="$OUTDIR/lifecycle_payload_$TS.json"
LIFECYCLE_RESPONSE="$OUTDIR/lifecycle_response_$TS.json"
LIFECYCLE_RAW="$OUTDIR/scantide_lifecycle_api_raw_$TS.txt"
FINDINGS="$OUTDIR/findings_$TS.tsv"
CONTAINERFILE="$OUTDIR/container_inventory_$TS.txt"
CONTAINER_TSV="$OUTDIR/container_inventory_$TS.tsv"
PORTFILE="$OUTDIR/listening_ports_$TS.tsv"
ROLEFILE="$OUTDIR/server_roles_$TS.txt"
HARDENING_RAW="$OUTDIR/hardening_evidence_$TS.txt"
PLATFORMFILE="$OUTDIR/platform_inventory_$TS.txt"
QUICKACTIONS="$OUTDIR/quick_actions_$TS.tsv"
VERSIONFILE="$OUTDIR/version_check_$TS.txt"
VERSIONJSON="$OUTDIR/version_feed_$TS.json"

while [ "$#" -gt 0 ]; do
  case "$1" in
    --deep)
      DEEP_SCAN=true
      shift
      ;;
    --no-api)
      USE_SCANTIDE_API=false
      USE_SCANTIDE_LIFECYCLE=false
      shift
      ;;
    --no-cve)
      USE_SCANTIDE_API=false
      shift
      ;;
    --no-lifecycle)
      USE_SCANTIDE_LIFECYCLE=false
      shift
      ;;
    --no-version-check)
      CHECK_VERSION=false
      shift
      ;;
    --api-key=*)
      SCANTIDE_API_KEY="${1#*=}"
      shift
      ;;
    --api-key)
      if [ "${2:-}" = "" ]; then
        echo "Missing value after --api-key" >&2
        exit 2
      fi
      SCANTIDE_API_KEY="$2"
      shift 2
      ;;
    --email=*)
      SCANTIDE_EMAIL="${1#*=}"
      shift
      ;;
    --email)
      if [ "${2:-}" = "" ]; then
        echo "Missing value after --email" >&2
        exit 2
      fi
      SCANTIDE_EMAIL="$2"
      shift 2
      ;;
    -h|--help)
      echo "Usage: sudo ./scantide-linux-localcheck.sh [--deep] [--no-api] [--no-cve] [--no-lifecycle] [--no-version-check] [--api-key KEY|--api-key=KEY] [--email EMAIL|--email=EMAIL]"
      exit 0
      ;;
    *)
      echo "Unknown argument: $1" >&2
      echo "Use --help for usage." >&2
      exit 2
      ;;
  esac
done

mkdir -p "$OUTDIR"

log(){ echo "[$(date '+%H:%M:%S')] $1"; }
cmd_exists(){ command -v "$1" >/dev/null 2>&1; }

check_linux_version_feed() {
  VERSION_FEED_STATUS="Not checked"
  VERSION_FEED_REMOTE=""
  VERSION_FEED_MINIMUM=""
  VERSION_FEED_RELEASED=""
  VERSION_FEED_WARNING=""

  if command -v curl >/dev/null 2>&1; then
    VERSION_FETCH_TOOL="curl"
  elif command -v wget >/dev/null 2>&1; then
    VERSION_FETCH_TOOL="wget"
  else
    VERSION_FEED_STATUS="Skipped: curl/wget unavailable"
    VERSION_FEED_WARNING="Could not check Linux script version because neither curl nor wget is available."
    return
  fi

  tmp_feed="$OUTDIR/scantide_version_feed_$TS.json"

  if [ "${VERSION_FETCH_TOOL:-}" = "curl" ]; then
    curl -fsSL --max-time 12 "$VERSION_FEED_URL" -o "$tmp_feed" 2>/dev/null || true
  elif [ "${VERSION_FETCH_TOOL:-}" = "wget" ]; then
    wget -q --timeout=12 "$VERSION_FEED_URL" -O "$tmp_feed" 2>/dev/null || true
  fi

  if [ ! -s "$tmp_feed" ]; then
    VERSION_FEED_STATUS="Unavailable"
    VERSION_FEED_WARNING="Could not fetch Scantide version feed. Scan continues, but update status is unknown."
    return
  fi

  if cmd_exists python3; then
    parsed="$(python3 - "$tmp_feed" <<'PY'
import json, sys
p=sys.argv[1]
try:
    d=json.load(open(p, "r", encoding="utf-8"))
except Exception:
    print("|||")
    raise SystemExit
remote = str(d.get("linuxLocalCheckVersion") or d.get("linux",{}).get("version") or "")
minimum = str(d.get("minimumRecommendedLinuxLocalCheckVersion") or d.get("linux",{}).get("minimumRecommendedVersion") or remote)
released = str(d.get("linuxLocalCheckReleased") or d.get("linux",{}).get("released") or d.get("released") or "")
url = str(d.get("linuxLocalCheckUrl") or d.get("linux",{}).get("downloadUrl") or "")
print(remote + "|" + minimum + "|" + released + "|" + url)
PY
)"
    VERSION_FEED_REMOTE="$(printf '%s' "$parsed" | cut -d'|' -f1)"
    VERSION_FEED_MINIMUM="$(printf '%s' "$parsed" | cut -d'|' -f2)"
    VERSION_FEED_RELEASED="$(printf '%s' "$parsed" | cut -d'|' -f3)"
    VERSION_FEED_DOWNLOAD="$(printf '%s' "$parsed" | cut -d'|' -f4)"
  else
    VERSION_FEED_REMOTE="$(grep -E '"linuxLocalCheckVersion"\s*:' "$tmp_feed" | head -1 | sed -E 's/.*"linuxLocalCheckVersion"\s*:\s*"([^"]+)".*/\1/')"
    VERSION_FEED_MINIMUM="$(grep -E '"minimumRecommendedLinuxLocalCheckVersion"\s*:' "$tmp_feed" | head -1 | sed -E 's/.*"minimumRecommendedLinuxLocalCheckVersion"\s*:\s*"([^"]+)".*/\1/')"
    VERSION_FEED_RELEASED="$(grep -E '"linuxLocalCheckReleased"|"released"\s*:' "$tmp_feed" | head -1 | sed -E 's/.*:\s*"([^"]+)".*/\1/')"
    VERSION_FEED_DOWNLOAD="$(grep -E '"linuxLocalCheckUrl"\s*:' "$tmp_feed" | head -1 | sed -E 's/.*"linuxLocalCheckUrl"\s*:\s*"([^"]+)".*/\1/')"
  fi

  [ -z "${VERSION_FEED_MINIMUM:-}" ] && VERSION_FEED_MINIMUM="$VERSION_FEED_REMOTE"

  if [ -z "${VERSION_FEED_REMOTE:-}" ]; then
    VERSION_FEED_STATUS="Feed missing Linux version"
    VERSION_FEED_WARNING="The online version feed was fetched, but it does not contain linuxLocalCheckVersion. Scan continues, but update status is unknown."
    return
  fi

  compare_versions() {
    # returns -1 if $1 < $2, 0 if equal, 1 if $1 > $2
    python3 - "$1" "$2" <<'PY' 2>/dev/null || echo 0
import sys, re
def parts(v):
    return [int(x) for x in re.findall(r'\d+', v)[:6]]
a=parts(sys.argv[1]); b=parts(sys.argv[2])
n=max(len(a),len(b)); a+= [0]*(n-len(a)); b += [0]*(n-len(b))
print(-1 if a<b else 1 if a>b else 0)
PY
  }

  cmp_remote="$(compare_versions "$SCRIPT_VERSION" "$VERSION_FEED_REMOTE")"
  cmp_min="$(compare_versions "$SCRIPT_VERSION" "$VERSION_FEED_MINIMUM")"

  if [ "$cmp_remote" = "0" ]; then
    VERSION_FEED_STATUS="Current"
  elif [ "$cmp_remote" = "-1" ]; then
    VERSION_FEED_STATUS="Update available"
    VERSION_FEED_WARNING="This Linux local check script is older than the online Linux version feed. Local: $SCRIPT_VERSION, online: $VERSION_FEED_REMOTE."
  else
    VERSION_FEED_STATUS="Newer than feed"
    VERSION_FEED_WARNING="This Linux local check script is newer than the online Linux version feed. Local: $SCRIPT_VERSION, online: $VERSION_FEED_REMOTE. Verify that the published version feed is updated."
  fi

  if [ "$cmp_min" = "-1" ]; then
    VERSION_FEED_WARNING="This Linux local check script is below the minimum recommended Linux version. Local: $SCRIPT_VERSION, minimum recommended: $VERSION_FEED_MINIMUM."
  fi
}


html_escape(){ sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g'; }
json_escape(){ printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/	/ /g'; }

run_timeout() {
  local seconds="$1"
  shift
  if cmd_exists timeout; then timeout "$seconds" "$@" 2>&1; else "$@" 2>&1; fi
}

version_base() {
  printf '%s' "$1" | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1
}

version_compare_state() {
  local local_v remote_v
  local_v="$(version_base "$1")"
  remote_v="$(version_base "$2")"

  if [ -z "$local_v" ] || [ -z "$remote_v" ]; then
    echo "unknown"
    return
  fi

  if [ "$local_v" = "$remote_v" ]; then
    echo "current"
    return
  fi

  if printf '%s
%s
' "$local_v" "$remote_v" | sort -V | head -1 | grep -qx "$local_v"; then
    echo "older"
  else
    echo "newer"
  fi
}

json_get_field() {
  local file="$1"
  local key="$2"
  if [ ! -s "$file" ]; then return; fi

  if cmd_exists python3; then
    python3 - "$file" "$key" <<'PY_JSON_GET'
import json, sys
try:
    data=json.load(open(sys.argv[1], encoding='utf-8'))
    value=data
    for part in sys.argv[2].split('.'):
        if isinstance(value, dict):
            value=value.get(part, '')
        else:
            value=''
            break
    if isinstance(value, (dict, list)):
        print(json.dumps(value, ensure_ascii=False))
    elif value is None:
        print('')
    else:
        print(str(value))
except Exception:
    print('')
PY_JSON_GET
  else
    grep -E '"'"$key"'"[[:space:]]*:' "$file" | head -1 | sed -E 's/^.*:[[:space:]]*"?([^",}]*)"?.*$/\1/'
  fi
}

check_scantide_version_feed() {
  : > "$VERSIONFILE"

  {
    echo "local_script_version	$SCRIPT_VERSION"
    echo "version_feed_url	$VERSION_FEED_URL"
  } >> "$VERSIONFILE"

  if [ "$CHECK_VERSION" != "true" ]; then
    echo "status	skipped" >> "$VERSIONFILE"
    echo "message	Version check skipped by --no-version-check." >> "$VERSIONFILE"
    return
  fi

  if ! cmd_exists curl && ! cmd_exists wget; then
    echo "status	unavailable" >> "$VERSIONFILE"
    echo "message	Version check could not run because neither curl nor wget is available." >> "$VERSIONFILE"
    return
  fi

  local fetch_ok="false"
  if cmd_exists curl; then
    if curl --max-time "$VERSION_TIMEOUT" -fsSL "$VERSION_FEED_URL" -o "$VERSIONJSON" 2>/dev/null; then
      fetch_ok="true"
    fi
  elif cmd_exists wget; then
    if wget -q -T "$VERSION_TIMEOUT" "$VERSION_FEED_URL" -O "$VERSIONJSON" 2>/dev/null; then
      fetch_ok="true"
    fi
  fi

  if [ "$fetch_ok" != "true" ] || [ ! -s "$VERSIONJSON" ]; then
    echo "status	failed" >> "$VERSIONFILE"
    echo "message	Could not fetch Scantide version feed. Scan will continue." >> "$VERSIONFILE"
    return
  fi

  local remote_version released product family localcheck_url lifecycle_api_version lifecycle_api_url linux_url linux_sha state message critical breaking
  remote_version="$(json_get_field "$VERSIONJSON" version)"
  released="$(json_get_field "$VERSIONJSON" released)"
  product="$(json_get_field "$VERSIONJSON" product)"
  family="$(json_get_field "$VERSIONJSON" family)"
  localcheck_url="$(json_get_field "$VERSIONJSON" localCheckUrl)"
  lifecycle_api_version="$(json_get_field "$VERSIONJSON" serverLifecycleApiVersion)"
  lifecycle_api_url="$(json_get_field "$VERSIONJSON" lifecycleApiUrl)"
  critical="$(json_get_field "$VERSIONJSON" critical)"
  breaking="$(json_get_field "$VERSIONJSON" breaking)"

  # Future-proof optional Linux-specific fields. The current shared PowerShell feed may not contain these yet.
  linux_url="$(json_get_field "$VERSIONJSON" linuxLocalCheckUrl)"
  linux_sha="$(json_get_field "$VERSIONJSON" sha256.linuxLocalCheck)"

  state="$(version_compare_state "$SCRIPT_VERSION" "$remote_version")"
  case "$state" in
    current) message="Local Linux script is aligned with the Scantide family feed version." ;;
    older) message="A newer Scantide family version is available. Use --update support when the feed exposes linuxLocalCheckUrl, or download the latest published Linux script manually." ;;
    newer) message="Local Linux script is newer than the public feed. This is normal for preview/test builds." ;;
    *) message="Could not compare local and remote versions." ;;
  esac

  {
    echo "status	$state"
    echo "message	$message"
    echo "remote_version	$remote_version"
    echo "released	$released"
    echo "product	$product"
    echo "family	$family"
    echo "critical	$critical"
    echo "breaking	$breaking"
    echo "localCheckUrl	$localcheck_url"
    echo "linuxLocalCheckUrl	$linux_url"
    echo "linuxLocalCheckSha256	$linux_sha"
    echo "serverLifecycleApiVersion	$lifecycle_api_version"
    echo "lifecycleApiUrl	$lifecycle_api_url"
  } >> "$VERSIONFILE"
}

version_value() {
  local key="$1"
  [ -s "$VERSIONFILE" ] || return
  awk -F '\t' -v k="$key" '$1==k{print $2; exit}' "$VERSIONFILE"
}

render_version_check_html() {
  local status cls msg
  status="${VERSION_FEED_STATUS:-Not checked}"
  msg="${VERSION_FEED_WARNING:-The installed Scantide Auditor Linux script matches the online Linux version feed.}"
  cls="source-warn"

  case "$status" in
    Current|"Newer than feed") cls="source-ok" ;;
    "Update available"|"Feed missing Linux version"|"Unavailable"|"Skipped:"*) cls="source-warn" ;;
    *) cls="source-warn" ;;
  esac

  echo "<table><tr><th>Item</th><th>Value</th></tr>"
  echo "<tr><th>status</th><td><span class='source-badge $cls'>$(printf '%s' "$status" | html_escape)</span></td></tr>"
  echo "<tr><th>message</th><td>$(printf '%s' "$msg" | html_escape)</td></tr>"
  echo "<tr><th>localLinuxScriptVersion</th><td>$(printf '%s' "$SCRIPT_VERSION" | html_escape)</td></tr>"
  echo "<tr><th>onlineLinuxScriptVersion</th><td>$(printf '%s' "${VERSION_FEED_REMOTE:-Unknown}" | html_escape)</td></tr>"
  echo "<tr><th>minimumRecommendedLinuxScriptVersion</th><td>$(printf '%s' "${VERSION_FEED_MINIMUM:-Unknown}" | html_escape)</td></tr>"
  echo "<tr><th>released</th><td>$(printf '%s' "${VERSION_FEED_RELEASED:-Unknown}" | html_escape)</td></tr>"
  echo "<tr><th>linuxLocalCheckUrl</th><td>$(printf '%s' "${VERSION_FEED_DOWNLOAD:-}" | html_escape)</td></tr>"
  echo "<tr><th>versionFeedUrl</th><td>$(printf '%s' "$VERSION_FEED_URL" | html_escape)</td></tr>"
  echo "</table>"
}

clean_version() {
  local text="$1"
  text="${text#*:}"
  text="${text%%-*}"
  text="${text%%+*}"
  text="${text%%~*}"

  if echo "$text" | grep -Eq '[0-9]+(\.[0-9]+){1,3}'; then
    echo "$text" | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1
  else
    echo "$text"
  fi
}

add_finding() {
  local severity="$1"
  local area="$2"
  local title="$3"
  local evidence="$4"
  local recommendation="$5"
  printf "%s\t%s\t%s\t%s\t%s\n" "$severity" "$area" "$title" "$evidence" "$recommendation" >> "$FINDINGS"
}

add_service() {
  local product="$1"
  local version="$2"
  local source="$3"

  version="$(clean_version "$version")"
  [ -z "$product" ] && return
  [ -z "$version" ] && return
  [ "$version" = "unknown" ] && return

  printf "%s\t%s\t%s\n" "$product" "$version" "$source" >> "$SERVICEFILE"
}

add_hardening_evidence() {
  local title="$1"
  local command_text="$2"
  local output_text="$3"
  {
    echo "### $title"
    echo "Command: $command_text"
    echo "$output_text"
    echo ""
  } >> "$HARDENING_RAW"
}

collect_container_inventory() {
  : > "$CONTAINERFILE"
  : > "$CONTAINER_TSV"

  printf "engine\tname\timage\tstatus\tports\n" >> "$CONTAINER_TSV"

  if cmd_exists docker; then
    echo "### Docker containers" >> "$CONTAINERFILE"
    docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>&1 >> "$CONTAINERFILE" || true
    docker ps -a --format '{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null | while IFS="$(printf '\t')" read -r name image status ports rest; do
      [ -z "${name:-}" ] && continue
      printf "Docker\t%s\t%s\t%s\t%s\n" "$name" "$image" "$status" "$ports" >> "$CONTAINER_TSV"
    done
    echo "" >> "$CONTAINERFILE"
    echo "### Docker images" >> "$CONTAINERFILE"
    docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}' 2>&1 >> "$CONTAINERFILE" || true
    echo "" >> "$CONTAINERFILE"
  fi

  if cmd_exists podman; then
    echo "### Podman containers" >> "$CONTAINERFILE"
    podman ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>&1 >> "$CONTAINERFILE" || true
    podman ps -a --format '{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null | while IFS="$(printf '\t')" read -r name image status ports rest; do
      [ -z "${name:-}" ] && continue
      printf "Podman\t%s\t%s\t%s\t%s\n" "$name" "$image" "$status" "$ports" >> "$CONTAINER_TSV"
    done
    echo "" >> "$CONTAINERFILE"
    echo "### Podman images" >> "$CONTAINERFILE"
    podman images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Created}}\t{{.Size}}' 2>&1 >> "$CONTAINERFILE" || true
    echo "" >> "$CONTAINERFILE"
  fi

  if [ ! -s "$CONTAINERFILE" ]; then
    echo "No Docker or Podman container inventory captured." > "$CONTAINERFILE"
  fi
}

collect_listening_ports() {
  : > "$PORTFILE"
  printf "proto\tlocal_address\tport\tprocess\tpid\tuser\trisk_note\n" >> "$PORTFILE"

  if ! cmd_exists ss; then
    return
  fi

  ss -H -tulpen 2>/dev/null | while IFS= read -r line; do
    proto="$(printf '%s' "$line" | awk '{print $1}')"
    local_field="$(printf '%s' "$line" | awk '{print $5}')"
    users_field="$(printf '%s' "$line" | grep -Eo 'users:\(.*' || true)"

    # Split address and port. Handles 0.0.0.0:22, [::]:22 and *:80.
    port="$(printf '%s' "$local_field" | sed -E 's/^.*:([0-9]+)$/\1/')"
    addr="$(printf '%s' "$local_field" | sed -E 's/:([0-9]+)$//' | sed 's/^\[//;s/\]$//')"

    process="$(printf '%s' "$users_field" | grep -Eo '"[^"]+"' | head -1 | tr -d '"')"
    pid="$(printf '%s' "$users_field" | grep -Eo 'pid=[0-9]+' | head -1 | cut -d= -f2)"
    user="$(printf '%s' "$line" | grep -Eo 'uid:[0-9]+' | head -1 | cut -d: -f2)"

    [ -z "$port" ] && continue
    [ "$addr" = "*" ] && addr="0.0.0.0"
    [ -z "$process" ] && process="unknown"
    [ -z "$pid" ] && pid=""
    [ -z "$user" ] && user=""

    risk=""
    case "$port" in
      21|23|25|110|143|389|6379|9200|9300|11211|27017|3306|5432|5900|5901|10000)
        risk="Review exposure: service is commonly sensitive or abused if Internet-facing."
        ;;
      22)
        risk="SSH: expected on many servers, but should be restricted and hardened."
        ;;
      80|443|8080|8443)
        risk="Web service: verify TLS, headers, exposure and patching."
        ;;
    esac

    printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$proto" "$addr" "$port" "$process" "$pid" "$user" "$risk" >> "$PORTFILE"
  done
}

collect_platform_inventory() {
  : > "$PLATFORMFILE"

  virt="unknown"
  virt_detail=""
  platform_label="Unknown / Bare Metal"

  if [ -f /.dockerenv ]; then
    virt="container"
    platform_label="Docker/LXC-style Container"
    virt_detail="/.dockerenv present"
  elif cmd_exists systemd-detect-virt; then
    virt="$(systemd-detect-virt 2>/dev/null || true)"
    virt_detail="systemd-detect-virt=$virt"
    case "$virt" in
      vmware) platform_label="VMware Virtual Machine" ;;
      oracle) platform_label="VirtualBox Virtual Machine" ;;
      microsoft|hyperv) platform_label="Hyper-V / Microsoft Virtual Machine" ;;
      kvm|qemu) platform_label="KVM/QEMU Virtual Machine" ;;
      xen) platform_label="Xen Virtual Machine" ;;
      lxc|lxc-libvirt|systemd-nspawn|docker|podman) platform_label="Linux Container" ;;
      none|"") platform_label="Physical or undetected virtualization" ;;
      *) platform_label="$virt virtualized platform" ;;
    esac
  fi

  if [ "$platform_label" = "Physical or undetected virtualization" ] || [ "$platform_label" = "Unknown / Bare Metal" ]; then
    if cmd_exists lscpu && lscpu 2>/dev/null | grep -qi 'Hypervisor vendor'; then
      hv="$(lscpu 2>/dev/null | awk -F: '/Hypervisor vendor/{gsub(/^ +/,"",$2); print $2; exit}')"
      [ -n "$hv" ] && platform_label="$hv Virtual Machine" && virt_detail="lscpu Hypervisor vendor=$hv"
    fi
  fi

  if cmd_exists vmware-toolbox-cmd || cmd_exists vmtoolsd || pkg_version open-vm-tools >/dev/null 2>&1; then
    vt="$( (vmware-toolbox-cmd -v 2>/dev/null || vmtoolsd -v 2>&1 || pkg_version open-vm-tools) | head -1 )"
    [ -n "$vt" ] && virt_detail="$virt_detail; VMware Tools/open-vm-tools detected: $vt"
  fi

  printf "platform_label\t%s\n" "$platform_label" >> "$PLATFORMFILE"
  printf "virtualization\t%s\n" "$virt" >> "$PLATFORMFILE"
  printf "evidence\t%s\n" "$virt_detail" >> "$PLATFORMFILE"
}

platform_value() {
  local key="$1"
  [ -s "$PLATFORMFILE" ] && awk -F '\t' -v k="$key" '$1==k{print $2; exit}' "$PLATFORMFILE"
}

port_listens() {
  local port="$1"
  [ -s "$PORTFILE" ] && awk -F '\t' -v p="$port" 'NR>1 && $3==p {found=1} END{exit found?0:1}' "$PORTFILE"
}

port_evidence() {
  local ports="$1"
  local out=""
  for p in $ports; do
    if port_listens "$p"; then
      out="$out TCP/$p listening;"
    fi
  done
  printf '%s' "$out"
}

service_summary_counts() {
  [ ! -s "$SERVICEFILE" ] && return
  web=0; mail=0; data=0; proxy=0; net=0; vpn=0; security=0; containers=0; admin=0; virt=0
  while IFS="$(printf '\t')" read -r product version source; do
    case "$product" in
      "Apache HTTP Server"|nginx|PHP|Caddy|Lighttpd|phpMyAdmin) web=$((web+1)) ;;
      Postfix|Exim|OpenSMTPD|Dovecot) mail=$((mail+1)) ;;
      Redis|MySQL|MariaDB|PostgreSQL|MongoDB|Memcached|Elasticsearch|RabbitMQ) data=$((data+1)) ;;
      Squid|HAProxy|Varnish|Traefik) proxy=$((proxy+1)) ;;
      BIND|dnsmasq|"ISC DHCP Server") net=$((net+1)) ;;
      OpenVPN|WireGuard|strongSwan) vpn=$((vpn+1)) ;;
      Fail2ban|ClamAV) security=$((security+1)) ;;
      Docker|Podman|containerd|runc|"Kubernetes kubelet"|kubectl) containers=$((containers+1)) ;;
      Webmin|Cockpit|Grafana|"Zabbix Agent"|"Prometheus Node Exporter") admin=$((admin+1)) ;;
      QEMU|libvirt|"VMware Tools") virt=$((virt+1)) ;;
    esac
  done < "$SERVICEFILE"
  echo "$web	$mail	$data	$proxy	$net	$vpn	$security	$containers	$admin	$virt"
}

render_services_summary_html() {
  vals="$(service_summary_counts)"
  if [ -z "$vals" ]; then
    echo "<div class='note'>No service summary available.</div>"
    return
  fi
  IFS="$(printf '\t')" read -r web mail data proxy net vpn security containers admin virt <<EOF_SUMMARY
$vals
EOF_SUMMARY
  echo "<table><tr><th>Category</th><th>Count</th><th>Meaning</th></tr>"
  echo "<tr><td>Web/Application</td><td>$web</td><td>Web servers, PHP/runtime or web admin apps.</td></tr>"
  echo "<tr><td>Mail</td><td>$mail</td><td>SMTP/IMAP/POP components.</td></tr>"
  echo "<tr><td>Database/Cache</td><td>$data</td><td>Databases, caches and message/search services.</td></tr>"
  echo "<tr><td>Proxy/Load Balancer</td><td>$proxy</td><td>Proxy, cache or load-balancing components.</td></tr>"
  echo "<tr><td>DNS/DHCP</td><td>$net</td><td>Network name/addressing services.</td></tr>"
  echo "<tr><td>VPN</td><td>$vpn</td><td>VPN/tunnel components.</td></tr>"
  echo "<tr><td>Security/Monitoring</td><td>$security</td><td>Security agents and local monitoring components.</td></tr>"
  echo "<tr><td>Containers</td><td>$containers</td><td>Container runtime/orchestration components.</td></tr>"
  echo "<tr><td>Admin/Monitoring UI</td><td>$admin</td><td>Web or agent-based admin/monitoring tools.</td></tr>"
  echo "<tr><td>Virtualization/Guest Tools</td><td>$virt</td><td>Hypervisor, virtualization or guest-agent tooling.</td></tr>"
  echo "</table>"
}

detect_server_roles() {
  : > "$ROLEFILE"

  has_service() {
    [ -s "$SERVICEFILE" ] && awk -F '\t' -v s="$1" 'tolower($1)==tolower(s){found=1} END{exit found?0:1}' "$SERVICEFILE"
  }

  add_role() {
    local role="$1"
    local confidence="$2"
    local evidence="$3"
    local score="$4"
    printf "%s\t%s\t%s\t%s\n" "$role" "$confidence" "$evidence" "$score" >> "$ROLEFILE"
  }

  # Role confidence is based on detected software plus listening ports.
  if has_service "Apache HTTP Server" || has_service "nginx" || has_service "PHP" || has_service "Caddy" || has_service "Lighttpd"; then
    ev="Detected web/runtime components:"
    score=0
    for svc in "Apache HTTP Server" nginx PHP Caddy Lighttpd; do has_service "$svc" && ev="$ev $svc;" && score=$((score+2)); done
    pe="$(port_evidence '80 443 8080 8443 8000 9000')"
    [ -n "$pe" ] && ev="$ev Ports: $pe" && score=$((score+3))
    conf="Medium"; [ "$score" -ge 5 ] && conf="High"
    add_role "Web / Application Server" "$conf" "$ev" "$score"
  fi

  if has_service "Postfix" || has_service "Exim" || has_service "OpenSMTPD" || has_service "Dovecot"; then
    ev="Detected mail components:"
    score=0
    for svc in Postfix Exim OpenSMTPD Dovecot; do has_service "$svc" && ev="$ev $svc;" && score=$((score+2)); done
    pe="$(port_evidence '25 465 587 110 143 993 995')"
    [ -n "$pe" ] && ev="$ev Ports: $pe" && score=$((score+3))
    conf="Medium"; [ "$score" -ge 5 ] && conf="High"
    add_role "Mail Server" "$conf" "$ev" "$score"
  fi

  if has_service "Squid" || has_service "HAProxy" || has_service "Varnish" || has_service "Traefik"; then
    ev="Detected proxy/load-balancer components:"
    score=0
    for svc in Squid HAProxy Varnish Traefik; do has_service "$svc" && ev="$ev $svc;" && score=$((score+2)); done
    pe="$(port_evidence '80 443 3128 8080 8443 6081')"
    [ -n "$pe" ] && ev="$ev Ports: $pe" && score=$((score+3))
    conf="Medium"; [ "$score" -ge 5 ] && conf="High"
    add_role "Proxy / Cache / Load Balancer" "$conf" "$ev" "$score"
  fi

  if has_service "Redis" || has_service "MySQL" || has_service "MariaDB" || has_service "PostgreSQL" || has_service "MongoDB" || has_service "Memcached" || has_service "Elasticsearch"; then
    ev="Detected data services:"
    score=0
    for svc in Redis MySQL MariaDB PostgreSQL MongoDB Memcached Elasticsearch; do has_service "$svc" && ev="$ev $svc;" && score=$((score+2)); done
    pe="$(port_evidence '6379 3306 5432 27017 11211 9200 9300')"
    [ -n "$pe" ] && ev="$ev Ports: $pe" && score=$((score+3))
    conf="Medium"; [ "$score" -ge 5 ] && conf="High"
    add_role "Database / Cache Server" "$conf" "$ev" "$score"
  fi

  if has_service "Docker" || has_service "Podman" || has_service "containerd" || has_service "Kubernetes kubelet"; then
    ev="Detected container components:"
    score=0
    for svc in Docker Podman containerd "Kubernetes kubelet"; do has_service "$svc" && ev="$ev $svc;" && score=$((score+2)); done
    pe="$(port_evidence '2375 2376 6443 10250')"
    [ -n "$pe" ] && ev="$ev Ports: $pe" && score=$((score+3))
    conf="Medium"; [ "$score" -ge 5 ] && conf="High"
    add_role "Container Host" "$conf" "$ev" "$score"
  fi

  if has_service "BIND" || has_service "dnsmasq" || has_service "ISC DHCP Server"; then
    ev="Detected DNS/DHCP components:"
    score=0
    for svc in BIND dnsmasq "ISC DHCP Server"; do has_service "$svc" && ev="$ev $svc;" && score=$((score+2)); done
    pe="$(port_evidence '53 67 68')"
    [ -n "$pe" ] && ev="$ev Ports: $pe" && score=$((score+3))
    conf="Medium"; [ "$score" -ge 5 ] && conf="High"
    add_role "DNS / DHCP Server" "$conf" "$ev" "$score"
  fi

  if has_service "OpenVPN" || has_service "WireGuard" || has_service "strongSwan"; then
    ev="Detected VPN components:"
    score=0
    for svc in OpenVPN WireGuard strongSwan; do has_service "$svc" && ev="$ev $svc;" && score=$((score+2)); done
    pe="$(port_evidence '1194 51820 500 4500')"
    [ -n "$pe" ] && ev="$ev Ports: $pe" && score=$((score+3))
    conf="Medium"; [ "$score" -ge 5 ] && conf="High"
    add_role "VPN Server" "$conf" "$ev" "$score"
  fi

  if has_service "Samba" || has_service "NFS" || has_service "CUPS"; then
    ev="Detected file/print services:"
    score=0
    for svc in Samba NFS CUPS; do has_service "$svc" && ev="$ev $svc;" && score=$((score+2)); done
    pe="$(port_evidence '139 445 2049 631')"
    [ -n "$pe" ] && ev="$ev Ports: $pe" && score=$((score+3))
    conf="Medium"; [ "$score" -ge 5 ] && conf="High"
    add_role "File / Print Server" "$conf" "$ev" "$score"
  fi

  if has_service "Webmin" || has_service "Cockpit" || has_service "Grafana" || has_service "Zabbix Agent" || has_service "Prometheus Node Exporter"; then
    ev="Detected admin/monitoring components:"
    score=0
    for svc in Webmin Cockpit Grafana "Zabbix Agent" "Prometheus Node Exporter"; do has_service "$svc" && ev="$ev $svc;" && score=$((score+2)); done
    pe="$(port_evidence '10000 9090 3000 9100 10050')"
    [ -n "$pe" ] && ev="$ev Ports: $pe" && score=$((score+3))
    conf="Medium"; [ "$score" -ge 5 ] && conf="High"
    add_role "Admin / Monitoring Node" "$conf" "$ev" "$score"
  fi

  # VMware/open-vm-tools is platform context, not a business/server role.
  # It is shown under Platform / Virtualization and Software Inventory instead.

  # Scantide-like SaaS/application platform role when several components combine.
  if (has_service "Apache HTTP Server" || has_service nginx) && has_service PHP && (has_service Redis || has_service HAProxy || has_service Postfix); then
    add_role "Likely SaaS / Web Platform" "High" "Combined application stack detected: web server + PHP + supporting service such as Redis/HAProxy/Postfix." "10"
  fi

  if [ ! -s "$ROLEFILE" ]; then
    echo "General Linux Server\tLow\tNo strong role-specific service combination was detected.\t1" > "$ROLEFILE"
  fi
}

render_listening_ports_html() {
  if [ ! -s "$PORTFILE" ]; then
    echo "<div class='note'>No parsed listening-port inventory was available. Raw ss output is shown below if available.</div>"
    cmd_exists ss && echo "<pre>$(ss -tulpen 2>&1 | html_escape)</pre>"
    return
  fi

  echo "<table><tr><th>Proto</th><th>Address</th><th>Port</th><th>Process</th><th>PID</th><th>User/UID</th><th>Comment</th></tr>"
  tail -n +2 "$PORTFILE" | while IFS="$(printf '\t')" read -r proto addr port process pid user risk; do
    [ -z "${port:-}" ] && continue
    badge="source-ok"
    [ -n "${risk:-}" ] && badge="source-warn"
    echo "<tr><td>$(printf '%s' "$proto" | html_escape)</td><td>$(printf '%s' "$addr" | html_escape)</td><td><b>$(printf '%s' "$port" | html_escape)</b></td><td>$(printf '%s' "$process" | html_escape)</td><td>$(printf '%s' "$pid" | html_escape)</td><td>$(printf '%s' "$user" | html_escape)</td><td><span class='source-badge $badge'>$(printf '%s' "${risk:-Expected/unknown}" | html_escape)</span></td></tr>"
  done
  echo "</table>"
}

render_container_inventory_html() {
  if [ ! -s "$CONTAINER_TSV" ] || [ "$(wc -l < "$CONTAINER_TSV" | tr -d ' ')" -le 1 ]; then
    echo "<div class='note'>No Docker or Podman containers were detected.</div>"
    return
  fi

  echo "<table><tr><th>Engine</th><th>Name</th><th>Image</th><th>Status</th><th>Ports</th></tr>"
  tail -n +2 "$CONTAINER_TSV" | while IFS="$(printf '\t')" read -r engine name image status ports; do
    [ -z "${name:-}" ] && continue
    badge="source-ok"
    printf '%s' "$ports" | grep -Eq '0\.0\.0\.0|::|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' && badge="source-warn"
    echo "<tr><td>$(printf '%s' "$engine" | html_escape)</td><td>$(printf '%s' "$name" | html_escape)</td><td>$(printf '%s' "$image" | html_escape)</td><td>$(printf '%s' "$status" | html_escape)</td><td><span class='source-badge $badge'>$(printf '%s' "${ports:-none}" | html_escape)</span></td></tr>"
  done
  echo "</table>"
}

primary_role() {
  if [ -s "$ROLEFILE" ]; then
    awk -F '\t' '$1 !~ /^(VMware Guest|Virtual Machine|Container Host|Physical Server|Platform Context)$/ {print}' "$ROLEFILE" | sort -t "$(printf '\t')" -k4,4nr | head -1 | awk -F '\t' '{print $1}'
  else
    echo "Unknown"
  fi
}

primary_role_confidence() {
  if [ -s "$ROLEFILE" ]; then
    awk -F '\t' '$1 !~ /^(VMware Guest|Virtual Machine|Container Host|Physical Server|Platform Context)$/ {print}' "$ROLEFILE" | sort -t "$(printf '\t')" -k4,4nr | head -1 | awk -F '\t' '{print $2}'
  else
    echo "Unknown"
  fi
}

render_server_roles_html() {
  if [ ! -s "$ROLEFILE" ]; then
    echo "<div class='note'>No role detection was performed.</div>"
    return
  fi
  echo "<table><tr><th>Likely role</th><th>Confidence</th><th>Evidence</th></tr>"
  sort -t "$(printf '\t')" -k4,4nr "$ROLEFILE" | while IFS="$(printf '\t')" read -r role confidence evidence score; do
    [ -z "${role:-}" ] && continue
    cls="source-warn"
    [ "$confidence" = "High" ] && cls="source-ok"
    [ "$confidence" = "Low" ] && cls="source-warn"
    echo "<tr><td><b>$(printf '%s' "$role" | html_escape)</b></td><td><span class='source-badge $cls'>$(printf '%s' "$confidence" | html_escape)</span></td><td>$(printf '%s' "$evidence" | html_escape)</td></tr>"
  done
  echo "</table>"
}

render_quick_actions_html() {
  if [ ! -s "$QUICKACTIONS" ]; then
    echo "<div class='scope-ok'><b>No quick actions generated by the current rule set.</b></div>"
    return
  fi
  echo "<table><tr><th>Priority</th><th>Action</th><th>Reason</th></tr>"
  sort -t "$(printf '\t')" -k1,1n "$QUICKACTIONS" | while IFS="$(printf '\t')" read -r prio action reason; do
    [ -z "${action:-}" ] && continue
    echo "<tr><td>$(printf '%s' "$prio" | html_escape)</td><td><b>$(printf '%s' "$action" | html_escape)</b></td><td>$(printf '%s' "$reason" | html_escape)</td></tr>"
  done
  echo "</table>"
}


detect_distro() {
  DISTRO_NAME="Unknown Linux"
  DISTRO_ID="unknown"
  DISTRO_FAMILY="unknown"
  DISTRO_VERSION_ID=""
  DISTRO_VERSION_CODENAME=""

  if [ -f /etc/os-release ]; then
    . /etc/os-release
    DISTRO_NAME="${PRETTY_NAME:-${NAME:-Unknown Linux}}"
    DISTRO_ID="${ID:-unknown}"
    DISTRO_FAMILY="${ID_LIKE:-$DISTRO_ID}"
    DISTRO_VERSION_ID="${VERSION_ID:-}"
    DISTRO_VERSION_CODENAME="${VERSION_CODENAME:-}"
  fi
}

collect_packages() {
  case "$DISTRO_ID" in
    ubuntu|debian|linuxmint)
      cmd_exists dpkg-query && run_timeout 60 dpkg-query -W -f='${binary:Package}\t${Version}\n' > "$PKGFILE" 2>/dev/null || true
      ;;
    fedora|rhel|centos|rocky|almalinux|ol)
      cmd_exists rpm && run_timeout 60 rpm -qa --qf '%{NAME}\t%{VERSION}-%{RELEASE}\n' > "$PKGFILE" 2>/dev/null || true
      ;;
    alpine)
      cmd_exists apk && run_timeout 60 apk info -vv | awk '{print $1 "\t" $2}' > "$PKGFILE" 2>/dev/null || true
      ;;
  esac
}

pkg_version() {
  local pkg="$1"
  [ -s "$PKGFILE" ] && awk -F '\t' -v p="$pkg" '$1 == p {print $2; exit}' "$PKGFILE"
}

build_service_inventory() {
  : > "$SERVICEFILE"

  add_service "Linux kernel" "$(uname -r)" "uname -r"

  detect_cmd_version() {
    local product="$1"
    local cmd="$2"
    local source="$3"
    local pattern="${4:-[0-9]+(\.[0-9]+){1,3}}"

    if cmd_exists "$cmd"; then
      v="$("$cmd" --version 2>&1 | grep -Eo "$pattern" | head -1)"
      add_service "$product" "$v" "$source"
    fi
  }

  # Web servers / web admin
  if cmd_exists apache2; then
    v="$(apache2 -v 2>/dev/null | grep -Eo 'Apache/[0-9.]+' | head -1 | cut -d/ -f2)"
    add_service "Apache HTTP Server" "$v" "apache2 -v"
  elif cmd_exists httpd; then
    v="$(httpd -v 2>/dev/null | grep -Eo 'Apache/[0-9.]+' | head -1 | cut -d/ -f2)"
    add_service "Apache HTTP Server" "$v" "httpd -v"
  fi

  if cmd_exists nginx; then
    v="$(nginx -v 2>&1 | grep -Eo 'nginx/[0-9.]+' | cut -d/ -f2)"
    add_service "nginx" "$v" "nginx -v"
  fi

  detect_cmd_version "Lighttpd" "lighttpd" "lighttpd -v"
  detect_cmd_version "Caddy" "caddy" "caddy version"
  detect_cmd_version "Webmin" "webmin" "webmin --version"
  detect_cmd_version "Cockpit" "cockpit-bridge" "cockpit-bridge --version"

  # PHP / web apps
  if cmd_exists php; then
    v="$(php -v 2>/dev/null | head -1 | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "PHP" "$v" "php -v"
  fi

  if [ -d /usr/share/phpmyadmin ] || [ -d /var/www/html/phpmyadmin ]; then
    v="$(pkg_version phpmyadmin)"
    add_service "phpMyAdmin" "$v" "phpmyadmin package/path"
  fi

  # TLS / SSH
  if cmd_exists openssl; then
    v="$(openssl version 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "OpenSSL" "$v" "openssl version"
  fi

  if cmd_exists ssh; then
    v="$(ssh -V 2>&1 | grep -Eo 'OpenSSH_[0-9]+(\.[0-9]+){1,3}' | sed 's/OpenSSH_//' | head -1)"
    add_service "OpenSSH" "$v" "ssh -V"
  fi

  # Mail: SMTP / IMAP / POP
  if cmd_exists postconf; then
    v="$(postconf -h mail_version 2>/dev/null | head -1)"
    add_service "Postfix" "$v" "postconf mail_version"
  fi

  if cmd_exists exim || cmd_exists exim4; then
    eximcmd="$(command -v exim 2>/dev/null || command -v exim4 2>/dev/null)"
    v="$("$eximcmd" -bV 2>/dev/null | head -1 | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "Exim" "$v" "exim -bV"
  fi

  if cmd_exists smtpd; then
    v="$(smtpd -h 2>&1 | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "OpenSMTPD" "$v" "smtpd"
  fi

  if cmd_exists dovecot; then
    v="$(dovecot --version 2>/dev/null | head -1)"
    add_service "Dovecot" "$v" "dovecot --version"
  fi

  # FTP
  detect_cmd_version "vsftpd" "vsftpd" "vsftpd --version"
  detect_cmd_version "ProFTPD" "proftpd" "proftpd --version"
  detect_cmd_version "Pure-FTPd" "pure-ftpd" "pure-ftpd --version"

  # Databases / caches
  if cmd_exists redis-server; then
    v="$(redis-server --version 2>/dev/null | grep -Eo 'v=[0-9]+(\.[0-9]+){1,3}' | cut -d= -f2 | head -1)"
    add_service "Redis" "$v" "redis-server --version"
  fi

  if cmd_exists mysqld; then
    v="$(mysqld --version 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "MySQL" "$v" "mysqld --version"
  fi

  if cmd_exists mariadbd; then
    v="$(mariadbd --version 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "MariaDB" "$v" "mariadbd --version"
  elif cmd_exists mysql && mysql --version 2>/dev/null | grep -qi mariadb; then
    v="$(mysql --version 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "MariaDB" "$v" "mysql --version"
  fi

  if cmd_exists postgres; then
    v="$(postgres --version 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "PostgreSQL" "$v" "postgres --version"
  elif cmd_exists psql; then
    v="$(psql --version 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "PostgreSQL" "$v" "psql --version"
  fi

  detect_cmd_version "MongoDB" "mongod" "mongod --version"
  detect_cmd_version "Memcached" "memcached" "memcached -h"
  detect_cmd_version "Elasticsearch" "elasticsearch" "elasticsearch --version"
  detect_cmd_version "RabbitMQ" "rabbitmqctl" "rabbitmqctl version"

  # DNS / DHCP
  if cmd_exists named; then
    v="$(named -v 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "BIND" "$v" "named -v"
  elif cmd_exists dig; then
    v="$(dig -v 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "BIND" "$v" "dig -v"
  fi

  detect_cmd_version "dnsmasq" "dnsmasq" "dnsmasq --version"
  detect_cmd_version "ISC DHCP Server" "dhcpd" "dhcpd --version"

  # Proxy / load balancer
  if cmd_exists haproxy; then
    v="$(haproxy -v 2>/dev/null | head -1 | grep -Eo 'version [0-9]+(\.[0-9]+){1,3}' | awk '{print $2}')"
    add_service "HAProxy" "$v" "haproxy -v"
  fi

  if cmd_exists squid; then
    v="$(squid -v 2>/dev/null | head -1 | grep -Eo 'Version [0-9]+(\.[0-9]+){1,3}' | awk '{print $2}')"
    add_service "Squid" "$v" "squid -v"
  fi

  detect_cmd_version "Varnish" "varnishd" "varnishd -V"
  detect_cmd_version "Traefik" "traefik" "traefik version"

  # Containers / virtualization
  detect_cmd_version "Docker" "docker" "docker --version"
  detect_cmd_version "Podman" "podman" "podman --version"
  detect_cmd_version "containerd" "containerd" "containerd --version"
  detect_cmd_version "runc" "runc" "runc --version"
  detect_cmd_version "Kubernetes kubelet" "kubelet" "kubelet --version"
  detect_cmd_version "kubectl" "kubectl" "kubectl version"
  detect_cmd_version "QEMU" "qemu-system-x86_64" "qemu-system-x86_64 --version"
  detect_cmd_version "libvirt" "libvirtd" "libvirtd --version"

  # VMware Tools / open-vm-tools
  if cmd_exists vmware-toolbox-cmd; then
    v="$(vmware-toolbox-cmd -v 2>/dev/null | grep -Eo '[0-9]+(\.[0-9]+){1,4}' | head -1)"
    add_service "VMware Tools" "$v" "vmware-toolbox-cmd -v"
  elif cmd_exists vmtoolsd; then
    v="$(vmtoolsd -v 2>&1 | grep -Eo '[0-9]+(\.[0-9]+){1,4}' | head -1)"
    [ -z "$v" ] && v="$(pkg_version open-vm-tools)"
    add_service "VMware Tools" "$v" "vmtoolsd -v / open-vm-tools package"
  else
    v="$(pkg_version open-vm-tools)"
    [ -n "$v" ] && add_service "VMware Tools" "$v" "open-vm-tools package"
  fi

  # Runtimes / languages
  detect_cmd_version "Node.js" "node" "node --version"
  detect_cmd_version "npm" "npm" "npm --version"
  detect_cmd_version "Python" "python3" "python3 --version"
  detect_cmd_version "Ruby" "ruby" "ruby --version"
  detect_cmd_version "Go" "go" "go version"
  detect_cmd_version "Rust" "rustc" "rustc --version"

  if cmd_exists perl; then
    v="$(perl -e 'printf "%vd\n",$^V' 2>/dev/null)"
    add_service "Perl" "$v" "perl version"
  fi

  if cmd_exists java; then
    v="$(java -version 2>&1 | head -1 | grep -Eo '[0-9]+(\.[0-9]+){1,3}' | head -1)"
    add_service "Java" "$v" "java -version"
  fi

  # VPN / security / monitoring
  detect_cmd_version "OpenVPN" "openvpn" "openvpn --version"
  detect_cmd_version "WireGuard" "wg" "wg"
  detect_cmd_version "strongSwan" "ipsec" "ipsec version"
  detect_cmd_version "Fail2ban" "fail2ban-client" "fail2ban-client version"
  detect_cmd_version "ClamAV" "clamscan" "clamscan --version"
  detect_cmd_version "Zabbix Agent" "zabbix_agentd" "zabbix_agentd --version"
  detect_cmd_version "Prometheus Node Exporter" "node_exporter" "node_exporter --version"
  detect_cmd_version "Grafana" "grafana-server" "grafana-server -v"

  # Print / file services
  detect_cmd_version "CUPS" "cupsd" "cupsd -v"
  detect_cmd_version "Samba" "smbd" "smbd --version"
  detect_cmd_version "NFS" "rpc.nfsd" "rpc.nfsd --version"

  if [ -s "$SERVICEFILE" ]; then
    awk -F '\t' '!seen[$1]++ {print}' "$SERVICEFILE" > "$SERVICEFILE.tmp"
    mv "$SERVICEFILE.tmp" "$SERVICEFILE"
  fi
}

build_cve_payload() {
  {
    printf '{'
    printf '"api_key":"%s",' "$(json_escape "$SCANTIDE_API_KEY")"
    printf '"email":"%s",' "$(json_escape "$SCANTIDE_EMAIL")"
    printf '"allow_remote_nvd":true,'
    printf '"mode":"local_batch",'
    printf '"client_mode":"local_batch",'
    printf '"source":"linux-local-service-inventory",'
    printf '"os":"%s",' "$(json_escape "$DISTRO_NAME")"
    printf '"host":"%s",' "$(json_escape "$HOST")"
    printf '"queries":['

    first=1
    while IFS="$(printf '\t')" read -r product version source; do
      [ -z "${product:-}" ] && continue
      [ -z "${version:-}" ] && continue
      [ "$first" -eq 0 ] && printf ','
      first=0
      printf '{"product":"%s","version":"%s"}' "$(json_escape "$product")" "$(json_escape "$version")"
    done < "$SERVICEFILE"

    printf ']}'
  } > "$CVE_PAYLOAD"
}

run_scantide_cve_api() {
  if [ "$USE_SCANTIDE_API" != "true" ]; then
    return
  fi

  if [ ! -s "$SERVICEFILE" ] || ! cmd_exists curl; then
    return
  fi

  build_cve_payload

  HTTP_STATUS="$(curl --http1.1 --max-time "$CVE_TIMEOUT" -sS \
    -o "$CVE_RESPONSE" \
    -w "%{http_code}" \
    -X POST "$SCANTIDE_CVE_API" \
    -H "Content-Type: application/json; charset=utf-8" \
    -H "X-API-Key: $SCANTIDE_API_KEY" \
    -H "User-Agent: ScantideLinuxLocalCheck/0.9-Bash" \
    --data-binary @"$CVE_PAYLOAD" \
    2>"$OUTDIR/cve_curl_error_$TS.txt")"

  {
    echo "HTTP Status: $HTTP_STATUS"
    echo ""
    echo "Payload:"
    cat "$CVE_PAYLOAD"
    echo ""
    echo ""
    echo "Response:"
    if [ -s "$CVE_RESPONSE" ]; then
      cat "$CVE_RESPONSE"
    else
      echo "No response body."
      cat "$OUTDIR/cve_curl_error_$TS.txt" 2>/dev/null
    fi
  } > "$CVE_RAW"
}


build_lifecycle_payload() {
  {
    printf '{'
    printf '"api_key":"%s",' "$(json_escape "$SCANTIDE_API_KEY")"
    printf '"email":"%s",' "$(json_escape "$SCANTIDE_EMAIL")"
    printf '"platform":"linux",'
    printf '"os_family":"linux",'
    printf '"source":"linux-local-service-inventory",'
    printf '"os":"%s",' "$(json_escape "$DISTRO_NAME")"
    printf '"distro_id":"%s",' "$(json_escape "$DISTRO_ID")"
    printf '"host":"%s",' "$(json_escape "$HOST")"
    printf '"items":['

    first=1

    # OS lifecycle item. Use short distro product names that match the Linux lifecycle catalog.
    os_product=""
    case "$DISTRO_ID" in
      ubuntu) os_product="Ubuntu" ;;
      debian) os_product="Debian" ;;
      rhel) os_product="Red Hat Enterprise Linux" ;;
      rocky) os_product="Rocky Linux" ;;
      almalinux) os_product="AlmaLinux" ;;
      centos) os_product="CentOS Stream" ;;
      ol) os_product="Oracle Linux" ;;
      fedora) os_product="Fedora" ;;
      sles|suse|opensuse*) os_product="SUSE Linux Enterprise Server" ;;
      amzn|amazon) os_product="Amazon Linux" ;;
      alpine) os_product="Alpine Linux" ;;
    esac

    if [ -n "$os_product" ] && [ -n "$DISTRO_VERSION_ID" ]; then
      printf '{"product":"%s","version":"%s","platform":"linux","os":"%s","source":"/etc/os-release"}' \
        "$(json_escape "$os_product")" \
        "$(json_escape "$DISTRO_VERSION_ID")" \
        "$(json_escape "$DISTRO_NAME")"
      first=0
    fi

    while IFS="$(printf '\t')" read -r product version source; do
      [ -z "${product:-}" ] && continue
      [ -z "${version:-}" ] && continue
      [ "$first" -eq 0 ] && printf ','
      first=0
      printf '{"product":"%s","version":"%s","platform":"linux","os":"%s","source":"%s"}' \
        "$(json_escape "$product")" \
        "$(json_escape "$version")" \
        "$(json_escape "$DISTRO_NAME")" \
        "$(json_escape "$source")"
    done < "$SERVICEFILE"

    printf ']}'
  } > "$LIFECYCLE_PAYLOAD"
}

run_scantide_lifecycle_api() {
  if [ "$USE_SCANTIDE_LIFECYCLE" != "true" ]; then
    return
  fi

  if ! cmd_exists curl; then
    return
  fi

  build_lifecycle_payload

  LIFECYCLE_HTTP_STATUS="$(curl --http1.1 --max-time "$LIFECYCLE_TIMEOUT" -sS \
    -o "$LIFECYCLE_RESPONSE" \
    -w "%{http_code}" \
    -X POST "$SCANTIDE_LIFECYCLE_API" \
    -H "Content-Type: application/json; charset=utf-8" \
    -H "X-API-Key: $SCANTIDE_API_KEY" \
    -H "X-Scantide-Email: $SCANTIDE_EMAIL" \
    -H "User-Agent: ScantideLinuxLocalCheck/0.10-Bash" \
    --data-binary @"$LIFECYCLE_PAYLOAD" \
    2>"$OUTDIR/lifecycle_curl_error_$TS.txt")"

  {
    echo "HTTP Status: $LIFECYCLE_HTTP_STATUS"
    echo ""
    echo "Payload:"
    cat "$LIFECYCLE_PAYLOAD"
    echo ""
    echo ""
    echo "Response:"
    if [ -s "$LIFECYCLE_RESPONSE" ]; then
      cat "$LIFECYCLE_RESPONSE"
    else
      echo "No response body."
      cat "$OUTDIR/lifecycle_curl_error_$TS.txt" 2>/dev/null
    fi
  } > "$LIFECYCLE_RAW"
}

render_lifecycle_table_html() {
  if [ ! -s "$LIFECYCLE_RESPONSE" ] || ! cmd_exists python3; then
    echo "<div class='note'>Lifecycle result table could not be rendered. Raw evidence is available under Raw / Evidence.</div>"
    return
  fi

  python3 - "$LIFECYCLE_RESPONSE" <<'PY_RENDER_LIFECYCLE'
import json, sys, html
path = sys.argv[1]
try:
    data = json.load(open(path, "r", encoding="utf-8"))
except Exception as e:
    print("<div class='scope-warn'>Could not parse lifecycle JSON: %s</div>" % html.escape(str(e)))
    sys.exit(0)

items = data.get("results") or data.get("items") or data.get("data") or []
summary = data.get("summary") or {}
platform = data.get("platform_selected") or ""
files = data.get("catalog_files") or []

def esc(x):
    return html.escape("" if x is None else str(x))

def sev_class(sev):
    sev = (sev or "Info").lower()
    if sev == "critical": return "sev-critical"
    if sev == "high": return "sev-high"
    if sev == "medium": return "sev-medium"
    if sev == "low": return "sev-low"
    return "sev-info"

def status_class(status):
    s = (status or "").lower()
    if s in ("end_of_support", "unsupported", "unsupported_major_version", "eol"):
        return "cve-high"
    if s in ("update_available", "outdated", "legacy_review", "extended_support"):
        return "cve-review"
    if s in ("current", "supported"):
        return "cve-suppressed"
    return "cve-review"

print("<div class='note'><b>Lifecycle API:</b> platform_selected=%s | catalog_files=%s | summary=%s</div>" % (
    esc(platform), esc(", ".join(map(str, files))), esc(summary)
))
print("<table>")
print("<tr><th>Product</th><th>Installed</th><th>Matched product</th><th>Category</th><th>Status</th><th>Severity</th><th>Latest / EOL</th><th>Evidence</th><th>Recommendation</th><th>Source</th></tr>")
for r in items:
    product = r.get("product", "")
    version = r.get("installed_version") or r.get("version") or r.get("normalized_version") or ""
    matched = r.get("matched_product") or ""
    category = r.get("category") or ""
    status = r.get("status_label") or r.get("status") or "Unknown"
    raw_status = r.get("status") or ""
    severity = r.get("severity") or "Info"
    latest = r.get("latest_version") or ""
    eol = r.get("eol_date") or ""
    latest_eol = []
    if latest: latest_eol.append("Latest: " + str(latest))
    if eol: latest_eol.append("EOL: " + str(eol))
    evidence = r.get("evidence") or ""
    rec = r.get("recommended_action") or ""
    source = r.get("source_label") or r.get("source_strategy") or ""
    confidence = r.get("source_confidence") or ""
    conf_class = "source-warn"
    if str(confidence).lower() == "high":
        conf_class = "source-ok"
    elif str(confidence).lower() in ("low", "unknown", ""):
        conf_class = "source-warn"
    source_html = esc(source)
    if confidence:
        source_html += "<br><span class='source-badge %s'>%s confidence</span>" % (conf_class, esc(confidence))
    print("<tr>")
    print("<td>%s</td>" % esc(product))
    print("<td>%s</td>" % esc(version))
    print("<td>%s</td>" % esc(matched))
    print("<td>%s</td>" % esc(category))
    print("<td><span class='cve-flag %s'>%s</span><br><span class='muted'>%s</span></td>" % (status_class(raw_status), esc(status), esc(raw_status)))
    print("<td><span class='sev %s'>%s</span></td>" % (sev_class(severity), esc(severity)))
    print("<td>%s</td>" % esc(" | ".join(latest_eol) if latest_eol else ""))
    print("<td>%s</td>" % esc(evidence))
    print("<td>%s</td>" % esc(rec))
    print("<td>%s</td>" % source_html)
    print("</tr>")
print("</table>")
PY_RENDER_LIFECYCLE
}

render_cve_table_html() {
  if [ ! -s "$CVE_RESPONSE" ] || ! cmd_exists python3; then
    echo "<div class='note'>CVE result table could not be rendered. Raw evidence is available under Raw / Evidence.</div>"
    return
  fi

  python3 - "$CVE_RESPONSE" <<'PY_RENDER_CVE'
import json, sys, html

path = sys.argv[1]
try:
    data = json.load(open(path, "r", encoding="utf-8"))
except Exception as e:
    print("<div class='scope-warn'>Could not parse CVE JSON: %s</div>" % html.escape(str(e)))
    sys.exit(0)

items = data.get("results") or data.get("items") or data.get("data") or []

def esc(x):
    return html.escape("" if x is None else str(x))

def sev_class(sev):
    sev = (sev or "Info").lower()
    if sev == "critical": return "sev-critical"
    if sev == "high": return "sev-high"
    if sev == "medium": return "sev-medium"
    if sev == "low": return "sev-low"
    return "sev-info"

print("<table>")
print("<tr><th>Product</th><th>Version</th><th>CVE Review</th><th>Evidence status</th><th>Highest risk</th><th>Top CVEs / candidates</th><th>Scantide CVE</th></tr>")

for r in items:
    product = r.get("product", "")
    version = r.get("version", "")
    total = int(r.get("total_cves") or 0)
    highest = r.get("highest_severity") or "Info"
    score = r.get("highest_score") or 0
    match_status = r.get("match_status") or ""
    sw = r.get("software_status") or {}
    msg = sw.get("message") or ""

    if total > 0:
        label = "Review-only candidate (%d)" % total
        flag_class = "cve-review"
        evidence = "Review only: service/version match needs distro advisory confirmation"
    elif sw:
        label = sw.get("warning_level") or "Review"
        flag_class = "cve-review" if label != "LOW" else "cve-suppressed"
        evidence = msg
    else:
        label = "No CVE match"
        flag_class = "cve-suppressed"
        evidence = "No confirmed version match returned"

    top = []
    for c in (r.get("top_cves") or [])[:5]:
        cid = c.get("cve_id", "")
        sev = c.get("severity", "")
        cvss = c.get("cvss_score") or c.get("score") or ""
        top.append("%s (%s %s)" % (cid, sev, cvss))
    top_text = "<br>".join(esc(x) for x in top) if top else "<span class='muted'>None</span>"

    status = "OK"
    source_class = "source-ok"
    if total > 0:
        status = "Review"
        source_class = "source-warn"
    elif sw and (sw.get("warning_level") or "").upper() in ("HIGH", "REVIEW", "UNKNOWN"):
        status = "Review"
        source_class = "source-warn"

    print("<tr>")
    print("<td>%s</td>" % esc(product))
    print("<td>%s</td>" % esc(version))
    print("<td><span class='cve-flag %s'>%s</span></td>" % (flag_class, esc(label)))
    print("<td>%s<br><span class='muted'>match_status=%s</span></td>" % (esc(evidence), esc(match_status)))
    print("<td><span class='sev %s'>%s</span> %s</td>" % (sev_class(highest), esc(highest), esc(score)))
    print("<td>%s</td>" % top_text)
    print("<td><span class='source-badge %s'>%s</span></td>" % (source_class, esc(status)))
    print("</tr>")

print("</table>")
PY_RENDER_CVE
}

evaluate_findings() {
  : > "$FINDINGS"
  : > "$HARDENING_RAW"

  if [ "$(id -u)" -ne 0 ]; then
    add_finding "Medium" "Scan Scope" "Linux check was not run as root" "Some local checks may be incomplete." "Re-run with sudo for complete visibility."
  fi

  if cmd_exists ufw; then
    UFW_OUT="$(ufw status verbose 2>&1 || true)"
    add_hardening_evidence "UFW status" "ufw status verbose" "$UFW_OUT"
    if printf '%s' "$UFW_OUT" | grep -qi "inactive"; then
      add_finding "Medium" "Firewall" "UFW is inactive" "Host firewall is not active according to ufw." "Enable UFW or verify that an upstream firewall intentionally provides filtering."
    fi
  fi

  if cmd_exists iptables; then
    IPT_OUT="$(iptables -L -n 2>&1 || true)"
    add_hardening_evidence "iptables policy" "iptables -L -n" "$IPT_OUT"
    if printf '%s' "$IPT_OUT" | grep -q "Chain INPUT (policy ACCEPT"; then
      add_finding "Medium" "Firewall" "iptables INPUT policy is ACCEPT" "Default INPUT policy accepts traffic." "Use explicit firewall policy or document external firewall dependency."
    fi
  fi

  if [ -f /etc/ssh/sshd_config ]; then
    SSH_HIGHLIGHTS="$(grep -Ei 'PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|PermitEmptyPasswords|X11Forwarding|AllowUsers|AllowGroups|Port' /etc/ssh/sshd_config 2>/dev/null || true)"
    add_hardening_evidence "SSH hardening highlights" "grep sshd_config" "$SSH_HIGHLIGHTS"
    if grep -Eiq '^\s*PermitRootLogin\s+yes' /etc/ssh/sshd_config; then
      add_finding "High" "SSH Hardening" "SSH root login is enabled" "PermitRootLogin yes" "Set PermitRootLogin no and use named admin accounts with sudo."
    fi
    if grep -Eiq '^\s*PasswordAuthentication\s+yes' /etc/ssh/sshd_config; then
      add_finding "Medium" "SSH Hardening" "SSH password authentication is enabled" "PasswordAuthentication yes" "Prefer SSH keys and set PasswordAuthentication no where practical."
    fi
    if grep -Eiq '^\s*PermitEmptyPasswords\s+yes' /etc/ssh/sshd_config; then
      add_finding "High" "SSH Hardening" "SSH empty passwords are allowed" "PermitEmptyPasswords yes" "Set PermitEmptyPasswords no."
    fi
  fi

  if cmd_exists apache2ctl || cmd_exists apachectl; then
    APACHE_CMD="$(command -v apache2ctl 2>/dev/null || command -v apachectl 2>/dev/null || true)"
    APACHE_OUT="$($APACHE_CMD -t -D DUMP_RUN_CFG 2>&1 || true)"
    add_hardening_evidence "Apache runtime config" "$APACHE_CMD -t -D DUMP_RUN_CFG" "$APACHE_OUT"
    APACHE_TOKENS="$(grep -RihE '^\s*ServerTokens\s+' /etc/apache2 /etc/httpd 2>/dev/null | tail -1 | awk '{print tolower($2)}' || true)"
    APACHE_SIG="$(grep -RihE '^\s*ServerSignature\s+' /etc/apache2 /etc/httpd 2>/dev/null | tail -1 | awk '{print tolower($2)}' || true)"
    [ -z "$APACHE_TOKENS" ] && APACHE_TOKENS="default"
    [ -z "$APACHE_SIG" ] && APACHE_SIG="default"
    if [ "$APACHE_TOKENS" != "prod" ]; then
      add_finding "Low" "Apache Hardening" "Apache ServerTokens is not set to Prod" "ServerTokens=$APACHE_TOKENS" "Set ServerTokens Prod to reduce banner detail."
    fi
    if [ "$APACHE_SIG" != "off" ]; then
      add_finding "Low" "Apache Hardening" "Apache ServerSignature is not Off" "ServerSignature=$APACHE_SIG" "Set ServerSignature Off to avoid verbose error-page signatures."
    fi
  fi

  if cmd_exists nginx; then
    NGINX_CONF="$(nginx -T 2>&1 || true)"
    add_hardening_evidence "Nginx config dump" "nginx -T" "$(printf '%s' "$NGINX_CONF" | head -300)"
    if ! printf '%s' "$NGINX_CONF" | grep -Eiq 'server_tokens\s+off\s*;'; then
      add_finding "Low" "Nginx Hardening" "Nginx server_tokens is not clearly disabled" "server_tokens off was not found in nginx -T output." "Set server_tokens off; in the http/server context."
    fi
  fi

  if cmd_exists redis-server || cmd_exists redis-cli; then
    REDIS_CONF=""
    for f in /etc/redis/redis.conf /etc/redis.conf; do
      [ -f "$f" ] && REDIS_CONF="$f" && break
    done
    if [ -n "$REDIS_CONF" ]; then
      REDIS_RELEVANT="$(grep -Ei '^\s*(bind|protected-mode|requirepass|aclfile|port)\b' "$REDIS_CONF" 2>/dev/null || true)"
      add_hardening_evidence "Redis config highlights" "grep redis.conf" "$REDIS_RELEVANT"
      if ! printf '%s' "$REDIS_RELEVANT" | grep -Eiq '^\s*protected-mode\s+yes'; then
        add_finding "High" "Redis Hardening" "Redis protected-mode is not clearly enabled" "protected-mode yes was not found." "Enable protected-mode and restrict bind/listen addresses."
      fi
      if ! printf '%s' "$REDIS_RELEVANT" | grep -Eiq '^\s*(requirepass|aclfile)\b'; then
        add_finding "Medium" "Redis Hardening" "Redis authentication is not clearly configured" "No requirepass or aclfile setting found in redis.conf highlights." "Configure Redis ACLs/authentication and limit network exposure."
      fi
    fi
  fi

  if cmd_exists ss && ss -tulpen 2>/dev/null | grep -E 'redis-server.*(0\.0\.0\.0|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):6379' >/dev/null; then
    add_finding "High" "Exposed Services" "Redis appears reachable on a non-loopback address" "Redis is listening on TCP 6379 outside localhost." "Restrict Redis to localhost/private trusted hosts, require authentication, and firewall the port."
  fi

  if [ -S /var/run/docker.sock ]; then
    DOCKER_SOCK_LS="$(ls -l /var/run/docker.sock 2>/dev/null || true)"
    add_hardening_evidence "Docker socket" "ls -l /var/run/docker.sock" "$DOCKER_SOCK_LS"
    if printf '%s' "$DOCKER_SOCK_LS" | grep -Eq 'srw.rw.rw.'; then
      add_finding "Critical" "Docker Hardening" "Docker socket appears world-writable" "$DOCKER_SOCK_LS" "Restrict docker.sock permissions immediately. Access to docker.sock is effectively root-equivalent."
    elif printf '%s' "$DOCKER_SOCK_LS" | grep -q ' docker '; then
      add_finding "Medium" "Docker Hardening" "Docker socket grants root-equivalent access to docker group" "$DOCKER_SOCK_LS" "Review docker group membership and treat it as privileged/root-equivalent access."
    fi
  fi
}

build_quick_actions() {
  : > "$QUICKACTIONS"
  prio=1

  if [ -s "$FINDINGS" ]; then
    awk -F '\t' '$1=="Critical" || $1=="High" || $1=="Medium" {print $1 "\t" $3 "\t" $5}' "$FINDINGS" | head -8 | while IFS="$(printf '\t')" read -r sev title rec; do
      [ -z "$title" ] && continue
      case "$sev" in Critical) weight=1 ;; High) weight=2 ;; Medium) weight=3 ;; *) weight=4 ;; esac
      printf "%s\t%s\t%s\n" "$weight" "$title" "$rec" >> "$QUICKACTIONS"
    done
  fi

  if [ -s "$LIFECYCLE_RESPONSE" ] && cmd_exists python3; then
    python3 - "$LIFECYCLE_RESPONSE" >> "$QUICKACTIONS" <<'PY_QUICK_ACTIONS'
import json, sys
try:
    data=json.load(open(sys.argv[1], encoding='utf-8'))
except Exception:
    sys.exit(0)
items=data.get('results') or data.get('items') or data.get('data') or []
for r in items:
    st=(r.get('status') or '').lower()
    sev=(r.get('severity') or '').lower()
    if st in ('end_of_support','unsupported_major_version','update_available','extended_support','legacy_review') or sev in ('critical','high'):
        product=r.get('product') or r.get('matched_product') or 'software'
        version=r.get('installed_version') or r.get('normalized_version') or ''
        rec=r.get('recommended_action') or r.get('evidence') or 'Review lifecycle status.'
        weight='2' if st in ('end_of_support','unsupported_major_version') else '4'
        print(f"{weight}\tReview lifecycle for {product} {version}\t{rec}")
PY_QUICK_ACTIONS
  fi

  if [ ! -s "$QUICKACTIONS" ]; then
    printf "9\tReview report evidence\tNo urgent quick actions were generated, but review exposed services, lifecycle and CVE sections.\n" > "$QUICKACTIONS"
  fi
}

sev_badge() {
  local sev="$1"
  case "$sev" in
    Critical) echo "sev-critical" ;;
    High) echo "sev-high" ;;
    Medium) echo "sev-medium" ;;
    Low) echo "sev-low" ;;
    *) echo "sev-info" ;;
  esac
}

render_findings_table() {
  if [ ! -s "$FINDINGS" ]; then
    echo "<div class='scope-ok'><b>No local findings generated by the current rule set.</b></div>" >> "$REPORT"
    return
  fi

  echo "<table><tr><th>Severity</th><th>Area</th><th>Finding / Evidence</th><th>Recommendation</th></tr>" >> "$REPORT"
  while IFS="$(printf '\t')" read -r sev area title evidence rec; do
    cls="$(sev_badge "$sev")"
    echo "<tr><td><span class='sev $cls'>$(printf '%s' "$sev" | html_escape)</span></td><td>$(printf '%s' "$area" | html_escape)</td><td><b>$(printf '%s' "$title" | html_escape)</b><br><span class='muted'>$(printf '%s' "$evidence" | html_escape)</span></td><td>$(printf '%s' "$rec" | html_escape)</td></tr>" >> "$REPORT"
  done < "$FINDINGS"
  echo "</table>" >> "$REPORT"
}

detect_distro

check_linux_version_feed
log "Starting Scantide Linux local check on $HOST"
log "Checking Scantide version feed"
check_scantide_version_feed
log "Collecting packages"
collect_packages

log "Building service inventory"
build_service_inventory

log "Collecting container inventory"
collect_container_inventory

log "Collecting listening ports"
collect_listening_ports

log "Collecting platform inventory"
collect_platform_inventory

log "Detecting likely server roles"
detect_server_roles

log "Running CVE API"
run_scantide_cve_api

log "Running lifecycle API"
run_scantide_lifecycle_api

log "Evaluating local findings"
evaluate_findings

log "Building quick actions"
build_quick_actions

PKGCOUNT=0
SERVICECOUNT=0
FINDINGCOUNT=0
HTTP_STATUS_DISPLAY="Not run"
LIFECYCLE_STATUS_DISPLAY="Not run"
LIFECYCLE_FINDINGS=0
VERSION_STATUS_DISPLAY="${VERSION_FEED_STATUS:-Not checked}"
VERSION_STATUS_CLASS="warn"

[ -s "$PKGFILE" ] && PKGCOUNT="$(wc -l < "$PKGFILE" | tr -d ' ')"
[ -s "$SERVICEFILE" ] && SERVICECOUNT="$(wc -l < "$SERVICEFILE" | tr -d ' ')"
[ -s "$FINDINGS" ] && FINDINGCOUNT="$(wc -l < "$FINDINGS" | tr -d ' ')"
[ -s "$CVE_RAW" ] && HTTP_STATUS_DISPLAY="$(grep -m1 '^HTTP Status:' "$CVE_RAW" | cut -d: -f2- | xargs)"
[ -s "$LIFECYCLE_RAW" ] && LIFECYCLE_STATUS_DISPLAY="$(grep -m1 '^HTTP Status:' "$LIFECYCLE_RAW" | cut -d: -f2- | xargs)"
case "$VERSION_STATUS_DISPLAY" in
  Current|"Newer than feed") VERSION_STATUS_CLASS="ok" ;;
  Unavailable|"Feed missing Linux version"|"Update available") VERSION_STATUS_CLASS="warn" ;;
  *) VERSION_STATUS_CLASS="warn" ;;
esac
HTTP_STATUS_CLASS="warn"
LIFECYCLE_STATUS_CLASS="warn"
case "$HTTP_STATUS_DISPLAY" in
  200) HTTP_STATUS_CLASS="ok" ;;
  "Not run") HTTP_STATUS_CLASS="warn" ;;
  *) HTTP_STATUS_CLASS="risk" ;;
esac
case "$LIFECYCLE_STATUS_DISPLAY" in
  200) LIFECYCLE_STATUS_CLASS="ok" ;;
  "Not run") LIFECYCLE_STATUS_CLASS="warn" ;;
  *) LIFECYCLE_STATUS_CLASS="risk" ;;
esac
if [ -s "$LIFECYCLE_RESPONSE" ] && cmd_exists python3; then
  LIFECYCLE_FINDINGS="$(python3 - "$LIFECYCLE_RESPONSE" <<'PY_COUNT_LIFE'
import json, sys
try:
    d=json.load(open(sys.argv[1], encoding='utf-8'))
    s=d.get('summary') or {}
    print(s.get('findings', 0))
except Exception:
    print(0)
PY_COUNT_LIFE
)"
fi

CRITICAL_FINDINGS=0
HIGH_FINDINGS=0
MEDIUM_FINDINGS=0
LOW_FINDINGS=0
if [ -s "$FINDINGS" ]; then
  CRITICAL_FINDINGS="$(awk -F '	' '$1=="Critical"{c++} END{print c+0}' "$FINDINGS")"
  HIGH_FINDINGS="$(awk -F '	' '$1=="High"{c++} END{print c+0}' "$FINDINGS")"
  MEDIUM_FINDINGS="$(awk -F '	' '$1=="Medium"{c++} END{print c+0}' "$FINDINGS")"
  LOW_FINDINGS="$(awk -F '	' '$1=="Low"{c++} END{print c+0}' "$FINDINGS")"
fi
SCORE_PENALTY=$((CRITICAL_FINDINGS*25 + HIGH_FINDINGS*15 + MEDIUM_FINDINGS*8 + LOW_FINDINGS*3 + LIFECYCLE_FINDINGS*3))
SECURITY_SCORE=$((100 - SCORE_PENALTY))
[ "$SECURITY_SCORE" -lt 0 ] && SECURITY_SCORE=0
SCORE_CLASS="ok"
[ "$SECURITY_SCORE" -lt 80 ] && SCORE_CLASS="warn"
[ "$SECURITY_SCORE" -lt 60 ] && SCORE_CLASS="risk"
CONTAINERCOUNT=0
PORTCOUNT=0
ROLECOUNT=0
[ -s "$PORTFILE" ] && PORTCOUNT="$(( $(wc -l < "$PORTFILE" | tr -d ' ') - 1 ))" && [ "$PORTCOUNT" -lt 0 ] && PORTCOUNT=0
[ -s "$ROLEFILE" ] && ROLECOUNT="$(wc -l < "$ROLEFILE" | tr -d ' ')"
if [ -s "$CONTAINER_TSV" ]; then
  CONTAINERCOUNT="$(( $(wc -l < "$CONTAINER_TSV" | tr -d ' ') - 1 ))"
  [ "$CONTAINERCOUNT" -lt 0 ] && CONTAINERCOUNT=0
fi
PLATFORM_LABEL="$(platform_value platform_label)"
[ -z "$PLATFORM_LABEL" ] && PLATFORM_LABEL="Unknown"
PRIMARY_ROLE="$(primary_role)"
PRIMARY_ROLE_CONFIDENCE="$(primary_role_confidence)"
[ -z "$PRIMARY_ROLE" ] && PRIMARY_ROLE="General Linux Server"
[ -z "$PRIMARY_ROLE_CONFIDENCE" ] && PRIMARY_ROLE_CONFIDENCE="Low"

cat > "$REPORT" <<EOF
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<title>Scantide Linux Local Check</title>
<style>
body{font-family:Segoe UI,Arial;background:#eef2f7;margin:0;padding:22px;color:#172033}
.container{max-width:1500px;margin:auto;background:white;border-radius:12px;box-shadow:0 10px 32px #0002;overflow:hidden}
.header{background:linear-gradient(135deg,#1f4f8f,#667eea);color:white;padding:28px 32px}
.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;background:#f8fafc;padding:22px}
.card{background:white;border:1px solid #e5e7eb;border-radius:12px;padding:16px;box-shadow:0 2px 8px #0001}
.card small{display:block;color:#64748b;margin-top:4px;font-size:.72em}
.label{text-transform:uppercase;color:#64748b;font-size:.78em;font-weight:700}
.value{font-size:2em;font-weight:800}
.risk{color:#dc2626}.ok{color:#16a34a}.warn{color:#d97706}
.section{padding:20px 28px;border-top:1px solid #e5e7eb}
table{width:100%;border-collapse:collapse;font-size:.86em;margin-top:10px}
th{background:#334155;color:white;text-align:left;padding:9px}
td{border-bottom:1px solid #e5e7eb;padding:8px;vertical-align:top;word-break:break-word}
pre{background:#0b1020;color:#e5e7eb;padding:14px;border-radius:8px;overflow:auto;max-height:520px}
.muted{color:#64748b}
.sev{display:inline-block;border-radius:999px;padding:4px 9px;font-weight:800;font-size:.78em}
.sev-critical{background:#7f1d1d;color:white}.sev-high{background:#fee2e2;color:#991b1b}.sev-medium{background:#ffedd5;color:#9a3412}.sev-low{background:#fef9c3;color:#854d0e}.sev-info{background:#dbeafe;color:#1e40af}.sev-ok{background:#dcfce7;color:#166534}
.cve-flag{display:inline-block;border-radius:999px;padding:3px 8px;font-weight:800;font-size:.76em}
.cve-critical{background:#991b1b;color:white}.cve-high{background:#fee2e2;color:#991b1b}.cve-medium{background:#ffedd5;color:#9a3412}.cve-review{background:#e0f2fe;color:#075985}.cve-suppressed{background:#e2e8f0;color:#475569}
.source-badge{display:inline-block;border-radius:999px;padding:4px 9px;font-weight:900;font-size:.74em;white-space:nowrap;border:1px solid transparent}
.source-ok{background:#dcfce7;color:#166534;border-color:#bbf7d0}.source-warn{background:#fef3c7;color:#92400e;border-color:#fde68a}.source-fail{background:#fee2e2;color:#991b1b;border-color:#fecaca}
.note{background:#eff6ff;border-left:5px solid #2563eb;padding:12px;border-radius:8px;margin:10px 0}
.disclaimer{background:#fff7ed;border-left:6px solid #f97316;padding:14px;border-radius:8px;margin:10px 0;color:#7c2d12}
.scope-warn{background:#fff7ed;border-left:6px solid #f97316;padding:14px;border-radius:8px;margin:10px 0;color:#7c2d12}
.scope-ok{background:#ecfdf5;border-left:6px solid #16a34a;padding:14px;border-radius:8px;margin:10px 0;color:#14532d}
.tabs{display:flex;gap:8px;flex-wrap:wrap;background:#f8fafc;border-top:1px solid #e5e7eb;border-bottom:1px solid #e5e7eb;padding:12px 20px}
.tabbtn{border:1px solid #cbd5e1;background:white;color:#334155;border-radius:999px;padding:8px 12px;font-weight:700;cursor:pointer}
.tabbtn.active{background:#1f4f8f;color:white;border-color:#1f4f8f}
.tabpane{display:none}.tabpane.active{display:block}
.toolbar{position:sticky;top:0;z-index:30;background:#ffffffea;backdrop-filter:blur(8px);border-bottom:1px solid #e5e7eb;padding:12px 20px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.toolbar input{border:1px solid #cbd5e1;border-radius:8px;padding:8px 10px;font-size:.9em}
.btn{display:inline-block;border:1px solid #1d4ed8;background:#2563eb;color:#fff;border-radius:8px;padding:8px 12px;font-weight:700;cursor:pointer;text-decoration:none}
.btn.secondary{background:#f8fafc;color:#1e293b;border-color:#cbd5e1}
details{border:1px solid #e5e7eb;border-radius:10px;padding:10px;margin:10px 0;background:#fff}summary{cursor:pointer;font-weight:800;color:#334155}
@media print{.toolbar,.tabs,.btn{display:none!important}.tabpane{display:block!important}}
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<h1>Scantide Linux Local Security Check</h1>
<div>Host: $HOST | Distro: $DISTRO_NAME | Generated: $(date '+%Y-%m-%d %H:%M:%S') | v$SCRIPT_VERSION</div>
</div>

<div class='summary'>
<div class='card'><div class='label'>Scanner Version</div><div class='value $VERSION_STATUS_CLASS' style='font-size:1.15em'>$VERSION_STATUS_DISPLAY</div><small>Local $SCRIPT_VERSION</small></div>
<div class='card'><div class='label'>Security Score</div><div class='value $SCORE_CLASS'>$SECURITY_SCORE</div><small>0-100 local posture</small></div>
<div class='card'><div class='label'>Primary Role</div><div class='value' style='font-size:1.15em'>$PRIMARY_ROLE</div><small>$PRIMARY_ROLE_CONFIDENCE confidence</small></div>
<div class='card'><div class='label'>Platform</div><div class='value' style='font-size:1.15em'>$PLATFORM_LABEL</div><small>Virtualization / host context</small></div>
<div class='card'><div class='label'>Local Findings</div><div class='value warn'>$FINDINGCOUNT</div></div>
<div class='card'><div class='label'>Service CVE Checks</div><div class='value'>$SERVICECOUNT</div><small>Service/runtime inventory</small></div>
<div class='card'><div class='label'>Lifecycle Findings</div><div class='value warn'>$LIFECYCLE_FINDINGS</div><small>Linux lifecycle catalog</small></div>
<div class='card'><div class='label'>Packages</div><div class='value'>$PKGCOUNT</div><small>Evidence only</small></div>
<div class='card'><div class='label'>Containers</div><div class='value'>$CONTAINERCOUNT</div><small>Docker/Podman inventory</small></div>
<div class='card'><div class='label'>Listening Ports</div><div class='value'>$PORTCOUNT</div><small>ss parsed inventory</small></div>
<div class='card'><div class='label'>Likely Roles</div><div class='value'>$ROLECOUNT</div><small>Detected server roles</small></div>
<div class='card'><div class='label'>CVE API</div><div class='value $HTTP_STATUS_CLASS'>$HTTP_STATUS_DISPLAY</div><small>HTTP status</small></div>
<div class='card'><div class='label'>Lifecycle API</div><div class='value $LIFECYCLE_STATUS_CLASS'>$LIFECYCLE_STATUS_DISPLAY</div><small>HTTP status</small></div>
<div class='card'><div class='label'>Root Context</div><div class='value ok'>$([ "$(id -u)" -eq 0 ] && echo "Yes" || echo "No")</div></div>
<div class='card'><div class='label'>Kernel</div><div class='value'>$(uname -r | cut -d- -f1)</div><small>$(uname -r)</small></div>
</div>

<div class='toolbar'>
<input id='globalFilter' type='search' placeholder='Filter visible report rows...'>
<button class='btn secondary' onclick='clearFilter()'>Clear filters</button>
<button class='btn secondary' onclick='showTab("all")'>All sections</button>
</div>

<div class='tabs'>
<button class='tabbtn active' onclick='showTab("overview")'>Overview</button>
<button class='tabbtn' onclick='showTab("findings")'>Findings</button>
<button class='tabbtn' onclick='showTab("software")'>Software / CVE</button>
<button class='tabbtn' onclick='showTab("lifecycle")'>Lifecycle</button>
<button class='tabbtn' onclick='showTab("endpoint")'>Endpoint</button>
<button class='tabbtn' onclick='showTab("raw")'>Raw / Evidence</button>
<button class='tabbtn' onclick='showTab("all")'>All sections</button>
</div>

<div class='section'>
<h2>Important Disclaimer</h2>
<div class='disclaimer'><b>Read this first:</b> Scantide Linux Local Check is an assessment and awareness tool. It is not an EDR, antivirus, patch manager, or compliance certification. CVE matches are review leads based on detected service/runtime versions. Linux distributions often backport security fixes, so apparent CVEs must be confirmed against distro advisory data before being treated as confirmed.</div>
</div>
EOF

echo "<div class='section'><h2>Linux Script Version Check</h2>" >> "$REPORT"
if [ -n "${VERSION_FEED_WARNING:-}" ]; then
  echo "<div class='scope-warn'><b>Version warning:</b> $(printf '%s' "$VERSION_FEED_WARNING" | html_escape)</div>" >> "$REPORT"
else
  echo "<div class='scope-ok'><b>Version status:</b> $(printf '%s' "$VERSION_FEED_STATUS" | html_escape)</div>" >> "$REPORT"
fi
echo "<table><tr><th>Local Linux script</th><td>$(printf '%s' "$SCRIPT_VERSION" | html_escape)</td><th>Online Linux script</th><td>$(printf '%s' "${VERSION_FEED_REMOTE:-Unknown}" | html_escape)</td></tr><tr><th>Minimum recommended Linux script</th><td>$(printf '%s' "${VERSION_FEED_MINIMUM:-Unknown}" | html_escape)</td><th>Released</th><td>$(printf '%s' "${VERSION_FEED_RELEASED:-Unknown}" | html_escape)</td></tr><tr><th>Feed URL</th><td colspan='3'>$(printf '%s' "$VERSION_FEED_URL" | html_escape)</td></tr></table>" >> "$REPORT"
echo "</div>" >> "$REPORT"

echo "<div class='tabpane active' data-tab='overview'>" >> "$REPORT"
echo "<div class='section'><h2>System Overview</h2><table>" >> "$REPORT"
echo "<tr><th>Host</th><td>$HOST</td><th>Distro</th><td>$DISTRO_NAME</td></tr>" >> "$REPORT"
echo "<tr><th>Kernel</th><td>$(uname -r)</td><th>Architecture</th><td>$(uname -m)</td></tr>" >> "$REPORT"
echo "<tr><th>Root</th><td>$([ "$(id -u)" -eq 0 ] && echo Yes || echo No)</td><th>Deep scan</th><td>$DEEP_SCAN</td></tr>" >> "$REPORT"
echo "<tr><th>Platform</th><td>$PLATFORM_LABEL</td><th>Primary role</th><td>$PRIMARY_ROLE ($PRIMARY_ROLE_CONFIDENCE)</td></tr>" >> "$REPORT"
echo "<tr><th>Script version</th><td>$SCRIPT_VERSION</td><th>Version feed status</th><td>$VERSION_STATUS_DISPLAY</td></tr>" >> "$REPORT"
echo "</table></div>" >> "$REPORT"

echo "<div class='section'><h2>Scantide Version Check</h2><div class='note'>The Linux local check reuses the Scantide Auditor version feed. If the shared feed later exposes <code>linuxLocalCheckUrl</code> and a SHA256 value, the Linux runner can use those fields for safe update/download workflows.</div>" >> "$REPORT"
render_version_check_html >> "$REPORT"
echo "</div>" >> "$REPORT"

echo "<div class='section'><h2>Quick Actions</h2><div class='note'>Top actions are generated from high/medium local findings and lifecycle review items. This gives the admin a short fix list before digging into evidence.</div>" >> "$REPORT"
render_quick_actions_html >> "$REPORT"
echo "</div>" >> "$REPORT"

echo "<div class='section'><h2>Likely Server Role Detection</h2><div class='note'>Roles are inferred from detected application/network services and boosted by listening-port evidence. Virtualization/guest tools are shown as platform context, not as the server's primary business role.</div>" >> "$REPORT"
render_server_roles_html >> "$REPORT"
echo "</div>" >> "$REPORT"

echo "<div class='section'><h2>Platform / Virtualization</h2><div class='note'>Shows whether this appears to be a physical server, VM, container or guest system. VMware Tools/open-vm-tools is also included in software inventory when detected.</div><table>" >> "$REPORT"
[ -s "$PLATFORMFILE" ] && while IFS="$(printf '	')" read -r k v; do echo "<tr><th>$(printf '%s' "$k" | html_escape)</th><td>$(printf '%s' "$v" | html_escape)</td></tr>" >> "$REPORT"; done < "$PLATFORMFILE"
echo "</table></div>" >> "$REPORT"

echo "<div class='section'><h2>Services Summary</h2><div class='note'>Summarizes detected service categories so the report quickly explains what the server appears to do.</div>" >> "$REPORT"
render_services_summary_html >> "$REPORT"
echo "</div>" >> "$REPORT"

echo "<div class='section'><h2>Scantide Linux Security Score</h2><div class='note'>Score starts at 100 and subtracts weighted points for local findings and lifecycle review items. It is a quick triage signal, not a compliance grade.</div><table><tr><th>Score</th><td><span class='value $SCORE_CLASS'>$SECURITY_SCORE</span></td><th>Penalty</th><td>$SCORE_PENALTY</td></tr><tr><th>Critical</th><td>$CRITICAL_FINDINGS</td><th>High</th><td>$HIGH_FINDINGS</td></tr><tr><th>Medium</th><td>$MEDIUM_FINDINGS</td><th>Low</th><td>$LOW_FINDINGS</td></tr><tr><th>Lifecycle review items</th><td>$LIFECYCLE_FINDINGS</td><th>Containers</th><td>$CONTAINERCOUNT</td></tr></table></div>" >> "$REPORT"

echo "<div class='section'><h2>System Information</h2>" >> "$REPORT"
cmd_exists lscpu && echo "<details><summary>CPU information</summary><pre>$(lscpu 2>&1 | html_escape)</pre></details>" >> "$REPORT"
cmd_exists free && echo "<details><summary>Memory information</summary><pre>$(free -h 2>&1 | html_escape)</pre></details>" >> "$REPORT"
cmd_exists df && echo "<details><summary>Disk usage</summary><pre>$(df -h 2>&1 | html_escape)</pre></details>" >> "$REPORT"
echo "</div></div>" >> "$REPORT"

echo "<div class='tabpane' data-tab='findings'><div class='section'><h2>Executive Findings</h2>" >> "$REPORT"
render_findings_table
echo "</div></div>" >> "$REPORT"

echo "<div class='tabpane' data-tab='software'>" >> "$REPORT"
echo "<div class='section'><h2>Service Inventory Used For CVE Checks</h2><div class='note'>This is preferred over raw package inventory. It uses actual service/runtime binaries such as apache2 -v, php -v, openssl version and ssh -V.</div><table><tr><th>Product</th><th>Version</th><th>Source</th></tr>" >> "$REPORT"
if [ -s "$SERVICEFILE" ]; then
  while IFS="$(printf '\t')" read -r product version source; do
    echo "<tr><td>$(printf '%s' "$product" | html_escape)</td><td>$(printf '%s' "$version" | html_escape)</td><td>$(printf '%s' "$source" | html_escape)</td></tr>" >> "$REPORT"
  done < "$SERVICEFILE"
else
  echo "<tr><td colspan='3' class='muted'>No service inventory captured.</td></tr>" >> "$REPORT"
fi
echo "</table></div>" >> "$REPORT"

echo "<div class='section'><h2>Installed Software CVE Review</h2><div class='note'><b>What this means:</b> Linux CVE checks are conservative. Backend CVE matches are shown as review evidence unless confirmed by distro advisory data. Ancient or broad product-name CVEs should not be treated as confirmed without exact affected-version validation.</div>" >> "$REPORT"
render_cve_table_html >> "$REPORT"
echo "</div></div>" >> "$REPORT"


echo "<div class='tabpane' data-tab='lifecycle'>" >> "$REPORT"
echo "<div class='section'><h2>Linux Lifecycle Review</h2><div class='note'><b>What this means:</b> Lifecycle checks are separate from CVE checks. The Linux scanner sends OS, kernel and detected service/runtime versions with <code>platform=linux</code>, so the API loads the Linux lifecycle catalog instead of the Windows/user-app catalog.</div>" >> "$REPORT"
render_lifecycle_table_html >> "$REPORT"
echo "</div></div>" >> "$REPORT"

echo "<div class='tabpane' data-tab='endpoint'>" >> "$REPORT"
echo "<div class='section'><h2>Listening Ports</h2><div class='note'>A listening port is not automatically bad, but it should be expected, patched and scoped by firewall policy. Sensitive ports are marked for exposure review.</div>" >> "$REPORT"
render_listening_ports_html >> "$REPORT"
echo "</div>" >> "$REPORT"

echo "<div class='section'><h2>Container Inventory</h2><div class='note'>Shows Docker/Podman containers when available. Container images can carry their own lifecycle/CVE risk and should be reviewed separately from the host OS.</div>" >> "$REPORT"
render_container_inventory_html >> "$REPORT"
echo "<details><summary>Raw container evidence</summary><pre>" >> "$REPORT"
[ -s "$CONTAINERFILE" ] && cat "$CONTAINERFILE" | html_escape >> "$REPORT" || echo "No container inventory captured." >> "$REPORT"
echo "</pre></details></div>" >> "$REPORT"

echo "<div class='section'><h2>Firewall</h2>" >> "$REPORT"
cmd_exists ufw && echo "<details><summary>UFW status</summary><pre>$(ufw status verbose 2>&1 | html_escape)</pre></details>" >> "$REPORT"
cmd_exists nft && echo "<details><summary>nftables ruleset</summary><pre>$(nft list ruleset 2>&1 | html_escape)</pre></details>" >> "$REPORT"
cmd_exists iptables && echo "<details><summary>iptables rules</summary><pre>$(iptables -L -n -v 2>&1 | html_escape)</pre></details>" >> "$REPORT"
echo "</div>" >> "$REPORT"

echo "<div class='section'><h2>SSH Hardening</h2>" >> "$REPORT"
if [ -f /etc/ssh/sshd_config ]; then
  echo "<details open><summary>sshd_config highlights</summary><pre>$(grep -Ei 'PermitRootLogin|PasswordAuthentication|PermitEmptyPasswords|X11Forwarding|Port' /etc/ssh/sshd_config 2>/dev/null | html_escape)</pre></details>" >> "$REPORT"
else
  echo "<div class='note'>No sshd_config found.</div>" >> "$REPORT"
fi
echo "</div>" >> "$REPORT"

echo "<div class='section'><h2>Service Hardening Evidence</h2><div class='note'>Collected configuration highlights for SSH, Apache, Nginx, Redis and Docker where available.</div><details><summary>Show hardening evidence</summary><pre>" >> "$REPORT"
[ -s "$HARDENING_RAW" ] && cat "$HARDENING_RAW" | html_escape >> "$REPORT" || echo "No hardening evidence captured." >> "$REPORT"
echo "</pre></details></div></div>" >> "$REPORT"

echo "<div class='tabpane' data-tab='raw'>" >> "$REPORT"
echo "<div class='section'><h2>Version Feed Evidence</h2><details><summary>Show version feed raw JSON</summary><pre>" >> "$REPORT"
[ -s "$VERSIONJSON" ] && cat "$VERSIONJSON" | html_escape >> "$REPORT" || echo "No version feed JSON captured." >> "$REPORT"
echo "</pre></details></div>" >> "$REPORT"

echo "<div class='section'><h2>Raw CVE API Evidence</h2><details><summary>Show raw CVE API payload/response</summary><pre>" >> "$REPORT"
[ -s "$CVE_RAW" ] && head -2000 "$CVE_RAW" | html_escape >> "$REPORT" || echo "No CVE raw evidence." >> "$REPORT"
echo "</pre></details></div>" >> "$REPORT"

echo "<div class='section'><h2>Raw Lifecycle API Evidence</h2><details><summary>Show raw lifecycle API payload/response</summary><pre>" >> "$REPORT"
[ -s "$LIFECYCLE_RAW" ] && head -2000 "$LIFECYCLE_RAW" | html_escape >> "$REPORT" || echo "No lifecycle raw evidence." >> "$REPORT"
echo "</pre></details></div>" >> "$REPORT"

echo "<div class='section'><h2>Parsed Listening Port Inventory</h2><details><summary>Show parsed listening-port TSV</summary><pre>" >> "$REPORT"
[ -s "$PORTFILE" ] && cat "$PORTFILE" | html_escape >> "$REPORT" || echo "No parsed port inventory." >> "$REPORT"
echo "</pre></details></div>" >> "$REPORT"

echo "<div class='section'><h2>Installed Package Inventory</h2><div class='note'>Package inventory is included as evidence, but CVE checks use service/runtime inventory to reduce false positives.</div><details><summary>Show first 500 installed packages</summary><pre>" >> "$REPORT"
[ -s "$PKGFILE" ] && head -500 "$PKGFILE" | html_escape >> "$REPORT" || echo "No package inventory." >> "$REPORT"
echo "</pre></details></div>" >> "$REPORT"

echo "<div class='section'><h2>Local Risk Indicators</h2>" >> "$REPORT"
if [ "$DEEP_SCAN" = true ]; then
  echo "<h3>SUID binaries</h3><pre>$(find / -xdev -perm -4000 -type f 2>/dev/null | head -300 | html_escape)</pre>" >> "$REPORT"
  echo "<h3>World-writable directories</h3><pre>$(find / -xdev -type d -perm -0002 2>/dev/null | head -300 | html_escape)</pre>" >> "$REPORT"
else
  echo "<div class='note'>Fast mode. Use --deep for fuller filesystem checks.</div>" >> "$REPORT"
  echo "<h3>SUID binaries in common paths</h3><pre>$(find /usr/bin /usr/sbin /bin /sbin /usr/local/bin -xdev -perm -4000 -type f 2>/dev/null | head -150 | html_escape)</pre>" >> "$REPORT"
fi
echo "</div></div>" >> "$REPORT"

cat >> "$REPORT" <<'EOF'
<script>
function showTab(name){
  document.querySelectorAll('.tabbtn').forEach(b=>b.classList.remove('active'));
  document.querySelectorAll('.tabpane').forEach(p=>{
    if(name==='all'){ p.classList.add('active'); }
    else { p.classList.toggle('active', p.dataset.tab===name); }
  });
  document.querySelectorAll('.tabbtn').forEach(b=>{
    if((name==='all' && b.textContent.includes('All')) || b.textContent.toLowerCase().includes(name)){
      b.classList.add('active');
    }
  });
}
function clearFilter(){
  document.getElementById('globalFilter').value='';
  filterRows('');
}
function filterRows(q){
  q=(q||'').toLowerCase();
  document.querySelectorAll('tr').forEach(tr=>{
    if(!q){ tr.style.display=''; return; }
    tr.style.display=tr.textContent.toLowerCase().includes(q) ? '' : 'none';
  });
}
document.getElementById('globalFilter').addEventListener('input', e=>filterRows(e.target.value));
</script>
</div>
</body>
</html>
EOF

log "Done"
echo "Report created: $REPORT"
echo "Package inventory: $PKGFILE"
echo "Service CVE inventory: $SERVICEFILE"
echo "CVE payload: $CVE_PAYLOAD"
echo "CVE raw output: $CVE_RAW"
echo "Lifecycle payload: $LIFECYCLE_PAYLOAD"
echo "Lifecycle raw output: $LIFECYCLE_RAW"
echo "Container inventory: $CONTAINERFILE"
echo "Container table: $CONTAINER_TSV"
echo "Listening ports: $PORTFILE"
echo "Detected roles: $ROLEFILE"
echo "Platform inventory: $PLATFORMFILE"
echo "Quick actions: $QUICKACTIONS"
echo "Hardening evidence: $HARDENING_RAW"