# Expanso Pipeline: cve-scan (CLI mode)
# =======================================
#
# Scan SBOM for CVE vulnerabilities using the OSV (Open Source Vulnerabilities) API.
# Free, no API key required.
#
# Usage:
#   cat sbom.json | expanso-edge run pipeline-cli.yaml
#
# Input: CycloneDX SBOM JSON with components array
# Output: List of vulnerabilities with CVE IDs and severity

name: cve-scan-cli
type: pipeline

config:
  input:
    stdin:
      codec: all

  pipeline:
    processors:
      # 1. Parse SBOM and prepare for scanning
      - mapping: |
          meta trace_id = uuid_v4()
          meta start_time = now()

          let sbom = content().parse_json()
          let components = $sbom.components.or([])

          # Filter to packages with name and version
          let packages = $components.filter(c -> c.name != "" && c.version != "").map_each(c -> {
            "name": c.name,
            "version": c.version,
            "purl": c.purl.or(""),
            "type": c.type.or("library")
          })

          meta package_count = $packages.length()
          root.packages = $packages
          root.current_index = 0
          root.vulnerabilities = []

      # 2. Query OSV API for each package (batch query)
      - mapping: |
          # OSV batch query format
          root = {
            "queries": this.packages.map_each(p -> {
              "package": {
                "name": p.name,
                "ecosystem": "npm"  # Default to npm, could be inferred from purl
              },
              "version": p.version
            }).slice(0, 100)  # OSV limit: 100 packages per batch
          }

      - http:
          url: "https://api.osv.dev/v1/querybatch"
          verb: POST
          headers:
            Content-Type: "application/json"

      # 3. Process OSV response and format vulnerabilities
      - mapping: |
          let results = this.results.or([])

          let vulns = $results.enumerate().map_each(item -> {
            let idx = item.index
            let result = item.value
            let osv_vulns = result.vulns.or([])

            osv_vulns.map_each(v -> {
              "cve_id": v.aliases.filter(a -> a.has_prefix("CVE-")).index(0).or(v.id),
              "osv_id": v.id,
              "severity": v.severity.or([]).map_each(s -> s.score).sort().reverse().index(0).or(0),
              "severity_label": if v.database_specific.severity != null {
                v.database_specific.severity
              } else if v.severity.or([]).length() > 0 {
                let score = v.severity.0.score.or(0)
                if score >= 9.0 { "CRITICAL" }
                else if score >= 7.0 { "HIGH" }
                else if score >= 4.0 { "MEDIUM" }
                else { "LOW" }
              } else { "UNKNOWN" },
              "summary": v.summary.or(v.details.or("")).slice(0, 200),
              "package": v.affected.or([]).index(0).package.name.or("unknown"),
              "fixed_version": v.affected.or([]).index(0).ranges.or([]).index(0).events.or([]).filter(e -> e.fixed != null).index(0).fixed.or(""),
              "references": v.references.or([]).map_each(r -> r.url).slice(0, 3)
            })
          }).flatten()

          # Count by severity
          let critical = $vulns.filter(v -> v.severity_label == "CRITICAL").length()
          let high = $vulns.filter(v -> v.severity_label == "HIGH").length()
          let medium = $vulns.filter(v -> v.severity_label == "MEDIUM").length()
          let low = $vulns.filter(v -> v.severity_label == "LOW" || v.severity_label == "UNKNOWN").length()

          root.vulnerabilities = $vulns
          root.summary = {
            "critical": $critical,
            "high": $high,
            "medium": $medium,
            "low": $low,
            "total": $vulns.length()
          }
          root.scanned_packages = meta("package_count")
          root.metadata = {
            "skill": "cve-scan",
            "mode": "cli",
            "database": "OSV",
            "packages_scanned": meta("package_count"),
            "vulnerabilities_found": $vulns.length(),
            "trace_id": meta("trace_id"),
            "started_at": meta("start_time"),
            "completed_at": now()
          }

      - log:
          level: INFO
          message: |
            [cve-scan] Scanned ${! this.scanned_packages } packages, found ${! this.summary.total } vulnerabilities (${! this.summary.critical } critical, ${! this.summary.high } high)

  output:
    stdout:
      codec: json_object
