#!/usr/bin/env bash
# SBOM to Secure60 software inventory – runs SBOM generator, flattens CycloneDX to events, POSTs to Collector or Ingest.
# Usage: ./s60-software-inv-agent-linux-1.03.sh [path]
#   path  Optional scan target (default: /). Config from .env-s60-client-script or prompts (Collector vs Ingest, URL, Project ID, Token).
#   --scripted  Non-interactive: require config from env or .env; use S60_SETUP_CRON=1 to add cron. No prompts. Installed cron runs with --scripted so it uses .env only.
#   S60_INSTALL_DIR  When set (e.g. /opt/secure60), the cron entry uses this path so crontab points to the canonical install dir. Script and .env-s60-client-script must be in the same directory.
#   S60_CRON_LOG  When installing cron, if this path (default /var/log/s60-sbom.log) is writable, cron output is appended there and a logrotate drop-in (/etc/logrotate.d/s60-sbom) is installed so the log is rotated (weekly, keep 4). If not writable, cron is still installed without logging. Works on Rocky and Ubuntu.
#   --sbom-file=<path>  Use this SBOM file instead of running Trivy (for E2E testing; Trivy not required).
#   --install-trivy-if-missing=Y  If Trivy is not on PATH, install it via official package repo (RHEL/CentOS/Rocky or Debian/Ubuntu). Use =N to decline. In scripted mode, set to Y/1 to install non-interactively.
#   S60_INSTALL_TRIVY_IF_MISSING  Env or .env: 1/Y/yes = install Trivy if missing; 0/N/no = do not install; unset = prompt when missing.
#   --install-trivy-only  Install Trivy via package manager if missing, then exit (no config, no scan). Useful for Ansible/rollout to ensure Trivy is present.
#   Package-manager install uses official Aqua Trivy repos and may require sudo (or run as root). Supported: RHEL/CentOS/Rocky (yum/dnf), Debian/Ubuntu (apt).
# First run (interactive): you will be prompted for Ingest or Collector, URL, Project ID, and JWT Token; values are saved to .env-s60-client-script in the script directory. To reconfigure, edit that file or remove it and run again.
# Requires: trivy (on PATH) unless --sbom-file is used. curl. No jq required.
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
ENV_FILE_NAME=".env-s60-client-script"

# --- Parse --scripted, --sbom-file, --install-trivy-if-missing, --install-trivy-only, and scan path
S60_SCRIPTED_INSTALL="${S60_SCRIPTED_INSTALL:-}"
S60_INSTALL_TRIVY_IF_MISSING="${S60_INSTALL_TRIVY_IF_MISSING:-}"
S60_INSTALL_TRIVY_ONLY=""
SCAN_PATH="/"
S60_SBOM_FILE=""
for arg in "$@"; do
  if [ "$arg" = "--scripted" ]; then
    S60_SCRIPTED_INSTALL=1
  elif [ "${arg#--sbom-file=}" != "$arg" ]; then
    S60_SBOM_FILE="${arg#--sbom-file=}"
  elif [ "${arg#--install-trivy-if-missing=}" != "$arg" ]; then
    val="${arg#--install-trivy-if-missing=}"
    case "${val^^}" in
      Y|YES|1) S60_INSTALL_TRIVY_IF_MISSING=1 ;;
      N|NO|0)  S60_INSTALL_TRIVY_IF_MISSING=0 ;;
      *)       S60_INSTALL_TRIVY_IF_MISSING="" ;;
    esac
  elif [ "$arg" = "--install-trivy-only" ]; then
    S60_INSTALL_TRIVY_ONLY=1
  elif [ "$arg" != "--scripted" ] && [ "${arg#--sbom-file=}" = "$arg" ] && [ "${arg#--install-trivy-if-missing=}" = "$arg" ] && [ "$arg" != "--install-trivy-only" ] && [ -z "${SCAN_PATH_SET:-}" ]; then
    SCAN_PATH="$arg"
    SCAN_PATH_SET=1
  fi
done
[ -n "$S60_SCRIPTED_INSTALL" ] && [ "$S60_SCRIPTED_INSTALL" != "0" ] || S60_SCRIPTED_INSTALL=""

# Resolve env file: prefer .env-s60-client-script in script dir, then cwd, then legacy .env
ENV_FILE=""
if [ -f "${SCRIPT_DIR}/${ENV_FILE_NAME}" ]; then
  ENV_FILE="${SCRIPT_DIR}/${ENV_FILE_NAME}"
elif [ -f "${PWD}/${ENV_FILE_NAME}" ]; then
  ENV_FILE="${PWD}/${ENV_FILE_NAME}"
elif [ -f "${SCRIPT_DIR}/.env" ]; then
  ENV_FILE="${SCRIPT_DIR}/.env"
elif [ -f ".env" ]; then
  ENV_FILE=".env"
fi

if [ -n "$ENV_FILE" ]; then
  set -a
  # shellcheck source=/dev/null
  source "$ENV_FILE"
  set +a
fi

# Allow token from vulnerability-manager .env (ENV_S60APITOKEN) if TOKEN not set
if [ -z "${TOKEN:-}" ] && [ -n "${ENV_S60APITOKEN:-}" ]; then
  export TOKEN="$ENV_S60APITOKEN"
fi

# Normalize S60_INSTALL_TRIVY_IF_MISSING (from CLI or env) to 1/0/""
case "${S60_INSTALL_TRIVY_IF_MISSING:-}" in
  Y|YES|1) S60_INSTALL_TRIVY_IF_MISSING=1 ;;
  N|NO|0)  S60_INSTALL_TRIVY_IF_MISSING=0 ;;
  *)       S60_INSTALL_TRIVY_IF_MISSING="" ;;
esac

# --- Trivy: OS detection and package-manager install (official Aqua repos)
S60_PKG_OS=""
_detect_pkg_os() {
  S60_PKG_OS=""
  if [ -f /etc/rocky-release ] || [ -f /etc/redhat-release ] || ( [ -f /etc/os-release ] && grep -qEi "rocky|rhel|centos" /etc/os-release 2>/dev/null ); then
    S60_PKG_OS="rhel"
  elif [ -f /etc/debian_version ] || ( [ -f /etc/os-release ] && grep -qEi "debian|ubuntu" /etc/os-release 2>/dev/null ); then
    S60_PKG_OS="debian"
  fi
}

_trivy_installed() {
  command -v trivy &>/dev/null
}

# Run command with sudo if not root (for package-manager install).
_run_priv() {
  if [ "$(id -u)" -eq 0 ]; then
    "$@"
  else
    sudo "$@"
  fi
}

_install_trivy_rhel() {
  local repo_file="/etc/yum.repos.d/trivy.repo"
  if [ -f "$repo_file" ]; then
    echo "Trivy repo already present: $repo_file"
  else
    echo "Adding Trivy official repository..."
    cat << 'TRIVY_REPO_EOF' | _run_priv tee "$repo_file" >/dev/null
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key
TRIVY_REPO_EOF
    [ -f "$repo_file" ] || { echo "Error: failed to add Trivy repo." >&2; return 1; }
  fi
  if command -v dnf &>/dev/null; then
    _run_priv dnf -y install --refresh trivy
  else
    _run_priv yum -y makecache && _run_priv yum -y install trivy
  fi
}

_install_trivy_debian() {
  local keyring="/usr/share/keyrings/trivy.gpg"
  local list_file="/etc/apt/sources.list.d/trivy.list"
  echo "Adding Trivy official repository..."
  _run_priv apt-get install -y wget gnupg
  wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | _run_priv gpg --dearmor -o "$keyring"
  echo "deb [signed-by=$keyring] https://aquasecurity.github.io/trivy-repo/deb generic main" | _run_priv tee "$list_file" >/dev/null
  _run_priv apt-get update && _run_priv apt-get install -y trivy
}

_install_trivy() {
  if [ "$S60_PKG_OS" = "rhel" ]; then
    _install_trivy_rhel
  elif [ "$S60_PKG_OS" = "debian" ]; then
    _install_trivy_debian
  else
    echo "Error: package-manager install not supported on this OS (S60_PKG_OS=$S60_PKG_OS)." >&2
    return 1
  fi
}

# --install-trivy-only: install Trivy via package manager if missing, then exit (no config, no scan).
if [ -n "$S60_INSTALL_TRIVY_ONLY" ]; then
  _detect_pkg_os
  if _trivy_installed; then
    echo "Trivy is already installed: $(command -v trivy)"
    exit 0
  fi
  if [ -z "$S60_PKG_OS" ]; then
    echo "Error: Trivy not found and package-manager install not supported on this OS. Install Trivy manually or see https://trivy.dev/docs/latest/getting-started/installation/" >&2
    exit 1
  fi
  if _install_trivy; then
    hash -r 2>/dev/null || true
    echo "Trivy installed successfully."
    exit 0
  else
    echo "Error: Trivy installation failed." >&2
    exit 1
  fi
fi

export HOST="${HOST:-$(hostname 2>/dev/null || echo unknown)}"
export IP="${IP:-}"
export FQDN="${FQDN:-}"
export ENVIRONMENT="${environment:-Production}"
readonly INV_PKG_SOURCE="sbom"
readonly APP_NAME_VULN="s60-vulnerability-scanning"

# --- Collector vs Ingest (and related config)
# Detect if we need to prompt (interactive and missing required config)
NEED_CONFIG=""
if [ -z "$S60_SCRIPTED_INSTALL" ]; then
  if [ -z "${S60_SEND_MODE:-}" ] || [ "$S60_SEND_MODE" != "collector" ] && [ "$S60_SEND_MODE" != "ingest" ]; then
    NEED_CONFIG=1
  elif [ "$S60_SEND_MODE" = "ingest" ] && { [ -z "${PROJECT_ID:-}" ] || [ -z "${TOKEN:-}" ]; }; then
    NEED_CONFIG=1
  elif [ "$S60_SEND_MODE" = "collector" ] && [ -z "${S60_COLLECTOR_BASE:-}" ]; then
    NEED_CONFIG=1
  fi
fi

if [ -n "$NEED_CONFIG" ]; then
  echo ""
  echo "--- Secure60 configuration (first run or missing settings) ---"
  echo "Where to send scan results: Collector (on‑prem) or Ingest (cloud)."
  echo "You will be asked for: mode, URL, and for Ingest: Project ID and JWT Token."
  echo "Values are saved in .env-s60-client-script in this directory for next run."
  echo ""
fi

S60_SEND_MODE="${S60_SEND_MODE:-}"
if [ -z "$S60_SEND_MODE" ] || [ "$S60_SEND_MODE" != "collector" ] && [ "$S60_SEND_MODE" != "ingest" ]; then
  if [ -n "$S60_SCRIPTED_INSTALL" ]; then
    echo "Error: S60_SCRIPTED_INSTALL=1 but S60_SEND_MODE not set. Set S60_SEND_MODE=collector or ingest." >&2
    exit 1
  fi
  read -rp "Send to Secure60 Collector or Ingest directly? (C)ollector / (I)ngest [I]: " choice
  choice="${choice:-I}"
  if [ "${choice^^}" = "C" ]; then
    S60_SEND_MODE="collector"
  else
    S60_SEND_MODE="ingest"
  fi
fi

AUTH_HEADER=""
if [ "$S60_SEND_MODE" = "collector" ]; then
  S60_COLLECTOR_BASE="${S60_COLLECTOR_BASE:-}"
  if [ -z "$S60_COLLECTOR_BASE" ]; then
    if [ -n "$S60_SCRIPTED_INSTALL" ]; then
      echo "Error: S60_SCRIPTED_INSTALL=1 but S60_COLLECTOR_BASE not set." >&2
      exit 1
    fi
    read -rp "Collector base URL (e.g. https://COLLECTOR_IP): " S60_COLLECTOR_BASE
    [ -n "$S60_COLLECTOR_BASE" ] || { echo "Collector URL is required."; exit 1; }
  fi
  S60_COLLECTOR_BASE="${S60_COLLECTOR_BASE%/}"
  [[ "$S60_COLLECTOR_BASE" =~ ^https?:// ]] || S60_COLLECTOR_BASE="https://${S60_COLLECTOR_BASE}"
  INGEST_URL="$S60_COLLECTOR_BASE/"
  PROJECT_ID=""
  TOKEN=""
else
  S60_INGEST_BASE="${S60_INGEST_BASE:-${S60_BASE:-}}"
  if [ -z "$S60_INGEST_BASE" ]; then
    if [ -n "$S60_SCRIPTED_INSTALL" ]; then
      S60_INGEST_BASE="https://ingest.secure60.io"
    else
      read -rp "Ingest base URL [https://ingest.secure60.io]: " input_base
      S60_INGEST_BASE="${input_base:-https://ingest.secure60.io}"
    fi
  fi
  S60_INGEST_BASE="${S60_INGEST_BASE%/}"
  export PROJECT_ID="${PROJECT_ID:-}"
  export TOKEN="${TOKEN:-}"
  if [ -z "$PROJECT_ID" ]; then
    if [ -n "$S60_SCRIPTED_INSTALL" ]; then
      echo "Error: S60_SCRIPTED_INSTALL=1 but PROJECT_ID not set. Set PROJECT_ID in .env-s60-client-script or env." >&2
      exit 1
    fi
    read -rp "Secure60 Project ID (number, e.g. 313): " PROJECT_ID
    [ -n "$PROJECT_ID" ] || { echo "PROJECT_ID is required."; exit 1; }
  fi
  if [ -z "$TOKEN" ]; then
    if [ -n "$S60_SCRIPTED_INSTALL" ]; then
      echo "Error: S60_SCRIPTED_INSTALL=1 but TOKEN not set. Set TOKEN (or ENV_S60APITOKEN) in .env-s60-client-script or env." >&2
      exit 1
    fi
    read -rp "Secure60 JWT Token (from Secure60 UI or API): " TOKEN
    [ -n "$TOKEN" ] || { echo "TOKEN is required."; exit 1; }
  fi
  INGEST_URL="${S60_INGEST_BASE%/}/ingest/1.0/http/project/${PROJECT_ID}"
  AUTH_HEADER="Authorization: Bearer $TOKEN"
fi

# Save resolved config so next run (and cron) can load it without env vars (create or update .env-s60-client-script in script dir)
ENV_FILE="${SCRIPT_DIR}/${ENV_FILE_NAME}"
{
  echo "# Secure60 client script config. Created/updated by ${SCRIPT_NAME}. chmod 600 recommended (contains TOKEN for ingest)."
  echo "# Edit this file to change Ingest/Collector URL, Project ID, or Token."
  echo "# S60_SEND_MODE=collector or ingest"
  echo "S60_SEND_MODE=$S60_SEND_MODE"
  if [ "$S60_SEND_MODE" = "collector" ]; then
    echo "S60_COLLECTOR_BASE=$S60_COLLECTOR_BASE"
  else
    echo "S60_INGEST_BASE=$S60_INGEST_BASE"
    echo "PROJECT_ID=$PROJECT_ID"
    echo "TOKEN=$TOKEN"
  fi
  echo "# Optional: HOST= IP= FQDN= environment="
} > "$ENV_FILE"
if [ -z "$S60_SCRIPTED_INSTALL" ]; then
  echo "Config saved to ${ENV_FILE} (use for next run; edit to change Ingest URL, Project ID, or Token)."
fi

SCAN_ID="$(uuidgen 2>/dev/null | tr '[:upper:]' '[:lower:]' || echo "s60-scan-$(date +%s)")"
TMP_SBOM=""
S60_CREATED_TMP_SBOM=""

# --- Use SBOM file or run Trivy
if [ -n "$S60_SBOM_FILE" ]; then
  if [ ! -f "$S60_SBOM_FILE" ]; then
    echo "Error: --sbom-file path does not exist: $S60_SBOM_FILE" >&2
    exit 1
  fi
  TMP_SBOM="$S60_SBOM_FILE"
  echo "Using SBOM file: $TMP_SBOM"
else
  if ! _trivy_installed; then
    _detect_pkg_os
    if [ -z "$S60_PKG_OS" ]; then
      echo "Error: trivy is not installed or not on PATH. Package-manager install not supported on this OS." >&2
      echo "Install Trivy manually or use --sbom-file=<path> for testing. See https://trivy.dev/docs/latest/getting-started/installation/" >&2
      exit 1
    fi
    do_install=""
    if [ -n "$S60_SCRIPTED_INSTALL" ]; then
      if [ "$S60_INSTALL_TRIVY_IF_MISSING" = "1" ]; then
        do_install=1
      else
        echo "Error: trivy is not installed. Set S60_INSTALL_TRIVY_IF_MISSING=1 or use --install-trivy-if-missing=Y to install via package manager." >&2
        exit 1
      fi
    else
      read -rp "Trivy not found. Install via package manager (official repo)? (Y/n): " trivy_choice
      trivy_choice="${trivy_choice:-Y}"
      if [ "${trivy_choice^^}" = "Y" ] || [ "${trivy_choice^^}" = "YES" ]; then
        do_install=1
      fi
    fi
    if [ -n "$do_install" ]; then
      if ! _install_trivy; then
        echo "Error: Trivy package-manager install failed. Install manually or use --sbom-file=<path>." >&2
        exit 1
      fi
      hash -r 2>/dev/null || true
    else
      echo "Error: trivy is not installed. Install manually or use --sbom-file=<path>. See https://trivy.dev/docs/latest/getting-started/installation/" >&2
      exit 1
    fi
  fi
  TMP_SBOM="$(mktemp -t sbom.XXXXXX.json)"
  S60_CREATED_TMP_SBOM=1
  echo "Generating SBOM for path: $SCAN_PATH"
  if ! trivy fs --format cyclonedx --output "$TMP_SBOM" "$SCAN_PATH"; then
    echo "Error: SBOM generation failed." >&2
    exit 1
  fi
fi

# --- Pure-bash JSON helpers (no jq). Assumes Trivy CycloneDX; no escaped " in values.
json_escape() {
  local s="$1"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  printf '%s' "$s"
}

# Get first string value for key "key" or "key": from blob. Key can be bom-ref, type, name, version, etc.
get_json_string() {
  local blob="$1"
  local key="$2"
  local pattern
  case "$key" in
    bom-ref) pattern='"bom-ref"[[:space:]]*:[[:space:]]*"([^"]*)"' ;;
    *)       pattern="\"${key}\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"" ;;
  esac
  if [[ "$blob" =~ $pattern ]]; then
    printf '%s' "${BASH_REMATCH[1]}"
  fi
}

# Get property value by name (e.g. aquasecurity:trivy:Class) from blob. Looks for "name":"...","value":"..."
get_json_property_value() {
  local blob="$1"
  local propname="$2"
  local escaped="${propname//:/\\:}"
  # Match "name":"aquasecurity:trivy:Class" or similar then "value":"..."
  if [[ "$blob" =~ \"name\"[[:space:]]*:[[:space:]]*\"${propname}\"[^}]*\"value\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
    printf '%s' "${BASH_REMATCH[1]}"
  fi
}

# Extract supplier.name from blob (nested "supplier": { "name": "..." })
get_json_supplier_name() {
  local blob="$1"
  if [[ "$blob" =~ \"supplier\"[[:space:]]*:[[:space:]]*\{[^}]*\"name\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; then
    printf '%s' "${BASH_REMATCH[1]}"
  fi
}

# Extract arch from purl qualifier (e.g. ?arch=x86_64&distro=... -> x86_64)
get_purl_arch() {
  local purl="$1"
  if [[ "$purl" =~ \?([^\x22]*)$ ]] || [[ "$purl" =~ \?(.*) ]]; then
    local q="${BASH_REMATCH[1]}"
    if [[ "$q" =~ (^|&)arch=([^&]+) ]]; then
      printf '%s' "${BASH_REMATCH[2]}"
    fi
  fi
}

# Extract hashes[i].alg and hashes[i].content for i in 0..4. want = alg | content
get_json_hashN() {
  local blob="$1"
  local idx="$2"
  local want="$3"
  local rest="$blob"
  [[ "$rest" =~ \"hashes\"[[:space:]]*:[[:space:]]*\[(.*) ]] || return
  rest="${BASH_REMATCH[1]}"
  local count=0
  while [[ "$rest" =~ \{[^}]*\"alg\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"[^}]*\"content\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"[^}]*\} ]]; do
    if [ "$count" -eq "$idx" ]; then
      if [ "$want" = "alg" ]; then printf '%s' "${BASH_REMATCH[1]}"; else printf '%s' "${BASH_REMATCH[2]}"; fi
      return
    fi
    rest="${rest:${#BASH_REMATCH[0]}}"
    count=$((count + 1))
  done
}

# Backward compat: hashes[0]
get_json_hash0() {
  get_json_hashN "$1" 0 "$2"
}

# Extract licenses[i].license.id, .name, or licenses[i].expression for i in 0..4
# Restrict search to the licenses array (between [ and matching ]) to avoid matching component "name"
get_json_licenseN() {
  local blob="$1"
  local idx="$2"
  local want="$3"
  local rest="$blob"
  [[ "$rest" =~ \"licenses\"[[:space:]]*:[[:space:]]*\[(.*) ]] || return
  rest="${BASH_REMATCH[1]}"
  local depth=1
  local pos=0
  local len=${#rest}
  while [ "$pos" -lt "$len" ] && [ "$depth" -gt 0 ]; do
    local c="${rest:$pos:1}"
    if [ "$c" = "[" ] || [ "$c" = "{" ]; then depth=$((depth + 1)); fi
    if [ "$c" = "]" ] || [ "$c" = "}" ]; then depth=$((depth - 1)); fi
    pos=$((pos + 1))
  done
  rest="${rest:0:$((pos - 1))}"
  local count=0
  if [ "$want" = "id" ]; then
    while [[ "$rest" =~ \"id\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; do
      if [ "$count" -eq "$idx" ]; then printf '%s' "${BASH_REMATCH[1]}"; return; fi
      rest="${rest:${#BASH_REMATCH[0]}}"
      count=$((count + 1))
    done
  elif [ "$want" = "name" ]; then
    while [[ "$rest" =~ \"name\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; do
      if [ "$count" -eq "$idx" ]; then printf '%s' "${BASH_REMATCH[1]}"; return; fi
      rest="${rest:${#BASH_REMATCH[0]}}"
      count=$((count + 1))
    done
  elif [ "$want" = "expression" ]; then
    while [[ "$rest" =~ \"expression\"[[:space:]]*:[[:space:]]*\"([^\"]*)\" ]]; do
      if [ "$count" -eq "$idx" ]; then printf '%s' "${BASH_REMATCH[1]}"; return; fi
      rest="${rest:${#BASH_REMATCH[0]}}"
      count=$((count + 1))
    done
  fi
}

# Backward compat: licenses[0]
get_json_license0() {
  get_json_licenseN "$1" 0 "$2"
}

# Split SBOM file into component blobs. Finds root "components": [ (line with two leading spaces in pretty-printed CycloneDX), then outputs each { } object.
split_sbom_components() {
  local file="$1"
  local start_line
  start_line=$(grep -n '^  "components": \[' "$file" 2>/dev/null | tail -1 | cut -d: -f1)
  [ -z "$start_line" ] && start_line=$(grep -n '"components": \[' "$file" 2>/dev/null | tail -1 | cut -d: -f1)
  [ -z "$start_line" ] && { echo "Error: Could not find components array in SBOM." >&2; return 1; }
  sed -n "${start_line},\$p" "$file" | awk '
    BEGIN { in_string=0; escape=0; depth=0; in_array=0; blob=""; started=0 }
    {
      line=$0
      for (i=1; i<=length(line); i++) {
        c = substr(line,i,1)
        if (escape) { escape=0; if (started) blob=blob c }
        else if (in_string) {
          if (c=="\\") escape=1
          else if (c=="\x22") in_string=0
          if (started) blob=blob c
        }
        else if (c=="\x22") { in_string=1; if (started) blob=blob c }
        else if (c=="[" && depth==0) in_array=1
        else if (c=="]" && depth==0 && in_array) exit
        else if (c=="{") {
          depth++
          if (in_array && depth==1) started=1
          if (started) blob=blob c
        }
        else if (c=="}") {
          depth--
          if (started) blob=blob c
          if (depth==0) { printf "%s%c", blob, 30; blob=""; started=0 }
        }
        else if (started) blob=blob c
      }
      if (started) blob=blob "\n"
    }
  '
}

# Record separator for component blobs (ASCII 30 = RS)
readonly S60_RS=$'\x1e'

# Parse components: find OS and package-type blobs. Package types: library, application, framework, container, platform, firmware.
COMPONENTS_TMP="$(mktemp -t components.XXXXXX)"
cleanup_all() {
  rm -f "$COMPONENTS_TMP"
  [ -n "${S60_CREATED_TMP_SBOM:-}" ] && [ -n "${TMP_SBOM:-}" ] && [ -f "$TMP_SBOM" ] && rm -f "$TMP_SBOM"
}
trap cleanup_all EXIT
split_sbom_components "$TMP_SBOM" > "$COMPONENTS_TMP"

OS_BLOB=""
PKG_BLOBS=()
while IFS= read -r -d "$S60_RS" blob; do
  [ -z "$blob" ] && continue
  ctype="$(get_json_string "$blob" "type")"
  case "$ctype" in
    operating-system) OS_BLOB="$blob" ;;
    library|application|framework|container|platform|firmware) PKG_BLOBS+=("$blob") ;;
  esac
done < "$COMPONENTS_TMP"

if [ -z "$OS_BLOB" ]; then
  echo "Error: No operating-system component found in SBOM." >&2
  exit 1
fi

sbom_os_bom_ref="$(get_json_string "$OS_BLOB" "bom-ref")"
sbom_os_type="$(get_json_string "$OS_BLOB" "type")"
sbom_os_name="$(get_json_string "$OS_BLOB" "name")"
sbom_os_version="$(get_json_string "$OS_BLOB" "version")"
sbom_os_prop_class="$(get_json_property_value "$OS_BLOB" "aquasecurity:trivy:Class")"
sbom_os_prop_type="$(get_json_property_value "$OS_BLOB" "aquasecurity:trivy:Type")"
sbom_os_family="${sbom_os_name}"
sbom_os_major=""
if [ -n "$sbom_os_version" ] && [[ "$sbom_os_version" =~ ^([0-9]+) ]]; then
  sbom_os_major="${BASH_REMATCH[1]}"
fi

HOST_OS="${host_os:-$sbom_os_name}"
HOST_OS_VERSION="${inv_host_os_version:-$sbom_os_version}"
PKG_COUNT=${#PKG_BLOBS[@]}

# --- Build scan-completed JSON (only non-empty fields)
build_scan_body() {
  local parts=""
  append() { [ -z "$2" ] && return; local _v; _v="$(json_escape "$2")"; [ -n "$parts" ] && parts="$parts,"; parts="$parts\"$1\":\"$_v\""; }
  append "type" "endpoint"
  append "operation" "software-scan-completed"
  append "outcome" "success"
  append "host_name" "$HOST"
  append "app_name" "$APP_NAME_VULN"
  append "inv_pkg_source" "$INV_PKG_SOURCE"
  append "host_os" "$HOST_OS"
  append "environment" "$ENVIRONMENT"
  append "inv_scan_id" "$SCAN_ID"
  append "inv_package_count" "$PKG_COUNT"
  append "inv_host_ip" "$IP"
  append "inv_host_fqdn" "$FQDN"
  append "inv_host_os_version" "$HOST_OS_VERSION"
  append "sbom_os_bom_ref" "$sbom_os_bom_ref"
  append "sbom_os_type" "$sbom_os_type"
  append "sbom_os_name" "$sbom_os_name"
  append "sbom_os_version" "$sbom_os_version"
  append "sbom_os_prop_class" "$sbom_os_prop_class"
  append "sbom_os_prop_type" "$sbom_os_prop_type"
  printf '{%s}' "$parts"
}

# POST to Ingest and on failure print HTTP status, curl exit code, and response body (for debug).
# Usage: do_post "description" "$json_body"
# Returns 0 on success (HTTP 2xx), non-zero otherwise.
do_post() {
  local desc="$1"
  local body="$2"
  local tmp_body tmp_code
  tmp_body="$(mktemp -t s60_curl_body.XXXXXX)"
  tmp_code="$(mktemp -t s60_curl_code.XXXXXX)"
  # -k: always ignore invalid/self-signed SSL so we always attempt to send
  if [ -n "$AUTH_HEADER" ]; then
    curl -sS -k -X POST "$INGEST_URL" -H "Content-Type: application/json" -H "$AUTH_HEADER" -d "$body" -o "$tmp_body" -w "%{http_code}" > "$tmp_code" 2>&1
  else
    curl -sS -k -X POST "$INGEST_URL" -H "Content-Type: application/json" -d "$body" -o "$tmp_body" -w "%{http_code}" > "$tmp_code" 2>&1
  fi
  local curl_exit=$?
  local http_code
  http_code="$(head -1 "$tmp_code" 2>/dev/null | tr -d '\r\n')"
  rm -f "$tmp_code"
  if [ "$curl_exit" -ne 0 ]; then
    echo "Warning: $desc POST failed (curl exit $curl_exit)." >&2
    case "$curl_exit" in
      6)  echo "  Cause: Could not resolve host (DNS failure)." >&2 ;;
      7)  echo "  Cause: Connection refused or failed to connect." >&2 ;;
      28) echo "  Cause: Timeout." >&2 ;;
      35|51|60) echo "  Cause: SSL/TLS certificate verification failed (e.g. self-signed cert, wrong hostname, expired, or corporate proxy). Try: curl -v $INGEST_URL or use a CA bundle that trusts the server." >&2 ;;
      *)  echo "  Possible cause: connection refused, DNS failure, TLS error, or timeout. See 'man curl' for exit code $curl_exit." >&2 ;;
    esac
    [ -s "$tmp_body" ] && echo "  Response body: $(head -c 500 "$tmp_body")" >&2
    rm -f "$tmp_body"
    return 1
  fi
  if [ -z "$http_code" ] || [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
    echo "Warning: $desc POST failed (HTTP $http_code)." >&2
    echo "  Response body: $(head -c 500 "$tmp_body" 2>/dev/null)" >&2
    rm -f "$tmp_body"
    return 1
  fi
  rm -f "$tmp_body"
  return 0
}

echo "POST software-scan-completed (host=$HOST scan_id=$SCAN_ID packages=$PKG_COUNT)"
SCAN_BODY="$(build_scan_body)"
if ! do_post "scan-completed" "$SCAN_BODY"; then
  echo "Continuing with package events."
fi

# --- Build one inventory-package-present per package-type component (flat JSON, optional fields when present)
build_package_payload() {
  local blob="$1"
  local name inv_pkg_version_raw inv_pkg_version_normalised purl ecosystem inv_pkg_arch
  local sbom_bom_ref sbom_comp_type sbom_component_type sbom_supplier_name inv_cpe
  local hash_alg hash_content lic_expression lic_id lic_name
  local trivy_pkg_id trivy_pkg_type trivy_src_name trivy_src_version trivy_src_release trivy_src_epoch trivy_file_path
  name="$(get_json_string "$blob" "name")"
  [ -z "$name" ] && name="$(get_json_string "$blob" "bom-ref")"
  [ -z "$name" ] && name="unknown"
  inv_pkg_version_raw="$(get_json_string "$blob" "version")"
  if [ -n "$inv_pkg_version_raw" ]; then
    inv_pkg_version_normalised="${inv_pkg_version_raw#*:}"
    inv_pkg_version_normalised="${inv_pkg_version_normalised%%-*}"
    [ -z "$inv_pkg_version_normalised" ] && inv_pkg_version_normalised="$inv_pkg_version_raw"
  fi
  purl="$(get_json_string "$blob" "purl")"
  if [ -n "$purl" ] && [[ "$purl" =~ pkg:([^/]+)/ ]]; then
    ecosystem="${BASH_REMATCH[1]}"
  else
    ecosystem="unknown"
  fi
  inv_pkg_arch="$(get_purl_arch "$purl")"
  sbom_bom_ref="$(get_json_string "$blob" "bom-ref")"
  sbom_comp_type="$(get_json_string "$blob" "type")"
  sbom_component_type="$sbom_comp_type"
  sbom_supplier_name="$(get_json_supplier_name "$blob")"
  trivy_pkg_id="$(get_json_property_value "$blob" "aquasecurity:trivy:PkgID")"
  trivy_pkg_type="$(get_json_property_value "$blob" "aquasecurity:trivy:PkgType")"
  trivy_src_name="$(get_json_property_value "$blob" "aquasecurity:trivy:SrcName")"
  trivy_src_version="$(get_json_property_value "$blob" "aquasecurity:trivy:SrcVersion")"
  trivy_src_release="$(get_json_property_value "$blob" "aquasecurity:trivy:SrcRelease")"
  trivy_src_epoch="$(get_json_property_value "$blob" "aquasecurity:trivy:SrcEpoch")"
  trivy_file_path="$(get_json_property_value "$blob" "aquasecurity:trivy:FilePath")"
  inv_cpe="$(get_json_string "$blob" "cpe")"

  # Use actual package name for Portal UI: prefer name from trivy PkgID (e.g. "sssd-client@2.8.2-2.el9.x86_64" -> "sssd-client") when SBOM "name" is vendor (e.g. "Rocky Enterprise Software Foundation")
  if [ -n "$trivy_pkg_id" ] && [[ "$trivy_pkg_id" =~ ^([^@]+)@ ]]; then
    pkg_name_from_id="${BASH_REMATCH[1]}"
    if [ -n "$pkg_name_from_id" ]; then
      name="$pkg_name_from_id"
    fi
  fi
  if [ -z "$name" ] || [ "$name" = "Rocky Enterprise Software Foundation" ] || [ "$name" = "Rocky" ]; then
    [ -n "$trivy_src_name" ] && name="$trivy_src_name"
  fi

  local parts=""
  append() { [ -z "$2" ] && return; local _v; _v="$(json_escape "$2")"; [ -n "$parts" ] && parts="$parts,"; parts="$parts\"$1\":\"$_v\""; }
  append "type" "endpoint"
  append "operation" "inventory-package-present"
  append "outcome" "success"
  append "host_name" "$HOST"
  append "inv_scan_id" "$SCAN_ID"
  append "app_name" "$APP_NAME_VULN"
  append "inv_pkg_name" "$name"
  append "inv_pkg_version_normalised" "$inv_pkg_version_normalised"
  append "inv_pkg_version_raw" "$inv_pkg_version_raw"
  append "inv_purl" "$purl"
  append "inv_pkg_ecosystem" "$ecosystem"
  append "inv_pkg_arch" "$inv_pkg_arch"
  append "inv_pkg_source" "$INV_PKG_SOURCE"
  append "inv_cpe" "$inv_cpe"
  append "sbom_bom_ref" "$sbom_bom_ref"
  append "sbom_comp_type" "$sbom_comp_type"
  append "sbom_component_type" "$sbom_component_type"
  append "sbom_supplier_name" "$sbom_supplier_name"
  append "sbom_os_family" "$sbom_os_family"
  append "sbom_os_version" "$sbom_os_version"
  append "sbom_os_major" "$sbom_os_major"

  for i in 0 1 2 3 4; do
    hash_alg="$(get_json_hashN "$blob" "$i" "alg")"
    hash_content="$(get_json_hashN "$blob" "$i" "content")"
    if [ -n "$hash_alg" ] && [ -n "$hash_content" ]; then
      if [ "$i" -eq 0 ]; then
        append "hash_alg" "$hash_alg"
        append "hash_content" "$hash_content"
      else
        append "hash_alg$((i+1))" "$hash_alg"
        append "hash_content$((i+1))" "$hash_content"
      fi
    fi
  done

  lic_expression="$(get_json_licenseN "$blob" 0 "expression")"
  lic_id="$(get_json_licenseN "$blob" 0 "id")"
  lic_name="$(get_json_licenseN "$blob" 0 "name")"
  append "lic_expression" "$lic_expression"
  append "lic_id" "$lic_id"
  append "lic_name" "$lic_name"
  for i in 1 2 3 4; do
    lic_expression="$(get_json_licenseN "$blob" "$i" "expression")"
    lic_id="$(get_json_licenseN "$blob" "$i" "id")"
    if [ -n "$lic_expression" ]; then append "lic_expression$((i+1))" "$lic_expression"; fi
    if [ -n "$lic_id" ]; then append "lic_id$((i+1))" "$lic_id"; fi
  done

  append "trivy_pkg_id" "$trivy_pkg_id"
  append "trivy_pkg_type" "$trivy_pkg_type"
  append "trivy_src_name" "$trivy_src_name"
  append "trivy_src_version" "$trivy_src_version"
  append "trivy_src_release" "$trivy_src_release"
  append "trivy_src_epoch" "$trivy_src_epoch"
  append "trivy_file_path" "$trivy_file_path"

  # sbom_pkgtype for backward compat (same as trivy_pkg_type for OS packages)
  append "sbom_pkgtype" "$trivy_pkg_type"

  # Add any extra aquasecurity:trivy:* properties (trivy_<key>)
  local skip_keys="pkgid pkgtype srcname srcversion srcrelease srcepoch filepath"
  local trivy_regex='\"name\"[[:space:]]*:[[:space:]]*\"aquasecurity:trivy:([^\"]+)\"[^}]*\"value\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"'
  while [[ "$blob" =~ "$trivy_regex" ]]; do
    key="${BASH_REMATCH[1],,}"
    val="${BASH_REMATCH[2]}"
    if [ -n "$val" ] && [[ " $skip_keys " != *" $key "* ]]; then
      [ -n "$parts" ] && parts="$parts,"
      val_esc="$(json_escape "$val")"
      parts="${parts}\"trivy_${key}\":\"${val_esc}\""
    fi
    blob="${blob:${#BASH_REMATCH[0]}}"
  done
  printf '{%s}' "$parts"
}

SENT=0
FAILED=0
for blob in "${PKG_BLOBS[@]}"; do
  [ -z "$blob" ] && continue
  payload="$(build_package_payload "$blob")"
  pkg_name="$(get_json_string "$blob" "name")"
  [ -z "$pkg_name" ] && pkg_name="$(get_json_string "$blob" "bom-ref")"
  echo "POST inventory-package-present (inv_pkg_name=$pkg_name) ..."
  if do_post "inventory-package-present (inv_pkg_name=$pkg_name)" "$payload"; then
    SENT=$((SENT + 1))
  else
    FAILED=$((FAILED + 1))
  fi
done

echo "Done. Scan ID: $SCAN_ID | Packages sent: $SENT | Failed: $FAILED"
echo "Validate (api.secure60.io): data_type=events_detail, query operation='software-scan-completed' or 'inventory-package-present', inv_scan_id='$SCAN_ID'"

# --- Cron: prompt (interactive) or use S60_SETUP_CRON (scripted)
# Optional logging: if S60_CRON_LOG is writable, cron line appends output there (safe on Rocky, Ubuntu).
_cron_install() {
  if [ -n "${S60_INSTALL_DIR:-}" ]; then
    SCRIPT_ABS="${S60_INSTALL_DIR}/${SCRIPT_NAME}"
  else
    SCRIPT_ABS="${SCRIPT_DIR}/${SCRIPT_NAME}"
  fi
  CRON_LINE="0 */6 * * * ${SCRIPT_ABS} --scripted"
  S60_CRON_LOG="${S60_CRON_LOG:-/var/log/s60-sbom.log}"
  if echo >> "${S60_CRON_LOG}" 2>/dev/null; then
    CRON_LINE="${CRON_LINE} >> ${S60_CRON_LOG} 2>&1"
    echo "Cron output will be logged to ${S60_CRON_LOG}"
    # Install logrotate drop-in so the log is rotated (weekly, keep 4) and does not fill disk.
    if ! {
      printf '%s\n' "${S60_CRON_LOG} {" "    weekly" "    rotate 4" "    compress" "    missingok" "    notifempty" "}"
    } | _run_priv tee /etc/logrotate.d/s60-sbom >/dev/null 2>&1; then
      echo "Warning: Could not install logrotate config for ${S60_CRON_LOG}. Consider adding rotation for this path to avoid disk fill (e.g. in /etc/logrotate.d/)." >&2
    fi
  fi
  EXISTING="$(crontab -l 2>/dev/null | grep -v -F "${SCRIPT_ABS}" || true)"
  if [ -z "$EXISTING" ]; then
    (echo "SHELL=/bin/bash"; echo "PATH=/usr/local/bin:/usr/bin:/bin"; echo "$CRON_LINE") | crontab -
  else
    (echo "$EXISTING"; echo "$CRON_LINE") | crontab -
  fi
  echo "Cron installed: $CRON_LINE"
}
if [ -n "$S60_SCRIPTED_INSTALL" ]; then
  if [ -n "${S60_SETUP_CRON:-}" ] && [ "$S60_SETUP_CRON" != "0" ]; then
    _cron_install
  fi
else
  read -rp "Setup a cron job to run this script every 6 hours? (Y/n): " cron_choice
  cron_choice="${cron_choice:-Y}"
  if [ "${cron_choice^^}" = "Y" ] || [ "${cron_choice^^}" = "YES" ]; then
    _cron_install
  fi
fi
