# Detectify Internal Scanner - GitLab CI/CD Integration
#
# This pipeline triggers security scans via your Internal Scanning Agent
# and enforces guardrails to block pipelines when vulnerabilities are found.
#
# Prerequisites:
#   - Internal Scanning Agent deployed in your environment
#   - Scan profile created in Detectify portal
#
# Setup:
#   1. Copy this file to your repository as .gitlab-ci.yml
#   2. In GitLab, go to Settings > CI/CD > Variables and add:
#      - SCANNER_API_URL: Your scanner endpoint (e.g., https://scanner.internal.example.com)
#      - SCAN_PROFILE_TOKEN: Your scan profile token (mark as "Masked")
#
# Documentation: https://docs.detectify.com/integrations/gitlab-ci

stages:
  - security-scan

variables:
  # =============================================================================
  # Required Variables (set in GitLab CI/CD Settings > Variables)
  # =============================================================================
  # SCANNER_API_URL: ""       # Your Internal Scanner endpoint
  # SCAN_PROFILE_TOKEN: ""    # Your scan profile token (set as masked variable)

  # =============================================================================
  # Polling Configuration
  # =============================================================================
  POLL_INTERVAL: "30"         # Seconds between status checks
  POLL_TIMEOUT: "14400"       # Max wait time (default: 4 hours)

  # =============================================================================
  # Guardrail Thresholds (customize as needed)
  # =============================================================================
  # Pipeline fails if findings count >= threshold. Set to 0 to disable.
  FAIL_ON_HIGH: "1"           # Block on any high severity finding
  FAIL_ON_MEDIUM: "0"         # Disabled by default
  FAIL_ON_LOW: "0"            # Disabled by default

security-scan:
  stage: security-scan
  # GitLab job timeout - set higher than POLL_TIMEOUT
  timeout: 5h
  image: alpine:latest

  before_script:
    - apk add --no-cache curl jq

  script:
    - |
      set -e

      # Validate required variables
      if [ -z "$SCANNER_API_URL" ]; then
        echo "ERROR: SCANNER_API_URL is not set"
        echo "Please configure it in GitLab CI/CD Variables"
        exit 1
      fi

      if [ -z "$SCAN_PROFILE_TOKEN" ]; then
        echo "ERROR: SCAN_PROFILE_TOKEN is not set"
        echo "Please configure it in GitLab CI/CD Variables"
        exit 1
      fi

      echo "==================================="
      echo "Internal Security Scanner"
      echo "==================================="
      echo "API URL: $SCANNER_API_URL"
      echo "Poll Interval: ${POLL_INTERVAL}s"
      echo "Poll Timeout: ${POLL_TIMEOUT}s"
      echo ""

      # Health check
      echo "Checking scanner availability..."
      HEALTH=$(curl -s -o /dev/null -w "%{http_code}" "${SCANNER_API_URL}/health" || echo "000")
      if [ "$HEALTH" != "200" ]; then
        echo "ERROR: Scanner API is not reachable (HTTP $HEALTH)"
        exit 1
      fi
      echo "Scanner API is healthy"
      echo ""

      # Start the scan
      echo "Starting security scan..."
      RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
        "${SCANNER_API_URL}/scans" \
        -H "Content-Type: application/json" \
        -d "{\"scan_profile_token\": \"${SCAN_PROFILE_TOKEN}\"}")

      HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
      BODY=$(echo "$RESPONSE" | sed '$d')

      if [ "$HTTP_CODE" != "201" ]; then
        echo "ERROR: Failed to start scan (HTTP $HTTP_CODE)"
        echo "$BODY" | jq '.' 2>/dev/null || echo "$BODY"
        exit 1
      fi

      SCAN_ID=$(echo "$BODY" | jq -r '.id')
      SCAN_STATUS=$(echo "$BODY" | jq -r '.status')

      echo "Scan ID: $SCAN_ID"
      echo "Initial Status: $SCAN_STATUS"
      echo ""

      # Save scan ID for potential manual inspection
      echo "$SCAN_ID" > scan-id.txt

      # Poll for completion
      echo "Waiting for scan to complete..."
      echo "-----------------------------------"
      ELAPSED=0
      STATUS="$SCAN_STATUS"

      while [ $ELAPSED -lt $POLL_TIMEOUT ]; do
        sleep $POLL_INTERVAL
        ELAPSED=$((ELAPSED + POLL_INTERVAL))

        STATUS_RESPONSE=$(curl -s "${SCANNER_API_URL}/scans/${SCAN_ID}")
        STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.status')

        # Format elapsed time
        MINS=$((ELAPSED / 60))
        SECS=$((ELAPSED % 60))
        printf "[%02d:%02d] Status: %s\n" $MINS $SECS "$STATUS"

        case "$STATUS" in
          "completed")
            echo ""
            echo "Scan completed successfully!"
            break
            ;;
          "failed")
            echo ""
            ERROR=$(echo "$STATUS_RESPONSE" | jq -r '.error // "Unknown error"')
            FATAL=$(echo "$STATUS_RESPONSE" | jq -r '.scanFatal // empty')
            echo "ERROR: Scan failed"
            echo "Error: $ERROR"
            if [ -n "$FATAL" ]; then
              echo "Fatal Type: $FATAL"
            fi
            # Save the status response for debugging
            echo "$STATUS_RESPONSE" | jq '.' > scan-status.json
            exit 1
            ;;
          "cancelled")
            echo ""
            echo "ERROR: Scan was cancelled"
            exit 1
            ;;
          "queued"|"running")
            # Continue polling
            ;;
          *)
            echo ""
            echo "ERROR: Unknown scan status: $STATUS"
            echo "$STATUS_RESPONSE" | jq '.' 2>/dev/null || echo "$STATUS_RESPONSE"
            exit 1
            ;;
        esac
      done

      # Check for timeout
      if [ "$STATUS" != "completed" ]; then
        echo ""
        echo "ERROR: Scan timed out after ${POLL_TIMEOUT}s"
        echo "The scan is still running. Check scan ID: $SCAN_ID"
        exit 1
      fi

      echo ""
      echo "-----------------------------------"
      echo "Fetching scan results..."

      # Get results
      RESULTS=$(curl -s "${SCANNER_API_URL}/scans/${SCAN_ID}/results")

      # Validate results
      if ! echo "$RESULTS" | jq -e '.' > /dev/null 2>&1; then
        echo "ERROR: Invalid JSON response from results endpoint"
        echo "$RESULTS"
        exit 1
      fi

      # Save raw results as artifact
      echo "$RESULTS" | jq '.' > scan-results.json
      echo "Results saved to scan-results.json"

      # Generate a clean, readable report
      echo "$RESULTS" | jq '[.[] | {
        id: .finding_data.uuid,
        severity: .finding_data.scores[0].severity,
        cvss_score: .finding_data.scores[0].score,
        title: ((.finding_data.renderer_meta[] | select(.type == "FindingTitle") | .title_single) // "Unknown"),
        target_url: ((.finding_data.renderer_meta[] | select(.type == "FoundAt") | .target) // .finding_data.target.url),
        module: ((.finding_data.renderer_meta[] | select(.type == "FoundBy") | .module) // "unknown"),
        references: [(.finding_data.renderer_meta[] | select(.type == "FindingReference") | {source: .source, link: .link})]
      }]' > scan-report.json
      echo "Readable report saved to scan-report.json"

      # Count findings by severity
      # Note: Severity is nested at .finding_data.scores[0].severity in FindingWrapper structure
      TOTAL_COUNT=$(echo "$RESULTS" | jq 'length')
      HIGH_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.finding_data.scores[0].severity == "high")] | length')
      MEDIUM_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.finding_data.scores[0].severity == "medium")] | length')
      LOW_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.finding_data.scores[0].severity == "low")] | length')
      INFO_COUNT=$(echo "$RESULTS" | jq '[.[] | select(.finding_data.scores[0].severity == "info" or .finding_data.scores[0].severity == "information")] | length')

      echo ""
      echo "==================================="
      echo "Scan Summary"
      echo "==================================="
      echo "Total Findings: $TOTAL_COUNT"
      echo ""
      echo "  High:         $HIGH_COUNT"
      echo "  Medium:       $MEDIUM_COUNT"
      echo "  Low:          $LOW_COUNT"
      echo "  Info:         $INFO_COUNT"
      echo "==================================="

      # List high severity findings if any
      if [ "$HIGH_COUNT" -gt 0 ]; then
        echo ""
        echo "High Severity Findings:"
        echo "-----------------------------------"
        echo "$RESULTS" | jq -r '.[] | select(.finding_data.scores[0].severity == "high") | "- \((.finding_data.renderer_meta[] | select(.type == "FindingTitle") | .title_single) // "Unknown")"'
      fi

      echo ""
      echo "==================================="
      echo "Guardrail Evaluation"
      echo "==================================="

      # Apply guardrails
      FAILED=0

      if [ "$FAIL_ON_HIGH" -gt 0 ]; then
        if [ "$HIGH_COUNT" -ge "$FAIL_ON_HIGH" ]; then
          echo "FAIL: High severity findings ($HIGH_COUNT) >= threshold ($FAIL_ON_HIGH)"
          FAILED=1
        else
          echo "PASS: High severity findings ($HIGH_COUNT) < threshold ($FAIL_ON_HIGH)"
        fi
      else
        echo "SKIP: High severity threshold disabled"
      fi

      if [ "$FAIL_ON_MEDIUM" -gt 0 ]; then
        if [ "$MEDIUM_COUNT" -ge "$FAIL_ON_MEDIUM" ]; then
          echo "FAIL: Medium severity findings ($MEDIUM_COUNT) >= threshold ($FAIL_ON_MEDIUM)"
          FAILED=1
        else
          echo "PASS: Medium severity findings ($MEDIUM_COUNT) < threshold ($FAIL_ON_MEDIUM)"
        fi
      else
        echo "SKIP: Medium severity threshold disabled"
      fi

      if [ "$FAIL_ON_LOW" -gt 0 ]; then
        if [ "$LOW_COUNT" -ge "$FAIL_ON_LOW" ]; then
          echo "FAIL: Low severity findings ($LOW_COUNT) >= threshold ($FAIL_ON_LOW)"
          FAILED=1
        else
          echo "PASS: Low severity findings ($LOW_COUNT) < threshold ($FAIL_ON_LOW)"
        fi
      else
        echo "SKIP: Low severity threshold disabled"
      fi

      echo "==================================="
      echo ""

      if [ "$FAILED" -eq 1 ]; then
        echo "PIPELINE BLOCKED: Security findings exceed configured thresholds"
        echo ""
        echo "Review the findings in scan-report.json (readable) or scan-results.json (raw)"
        echo "Scan ID: $SCAN_ID"
        exit 1
      fi

      echo "PIPELINE PASSED: All security guardrails satisfied"

  artifacts:
    when: always
    paths:
      - scan-results.json
      - scan-report.json
      - scan-id.txt
      - scan-status.json
    expire_in: 30 days

  # When to run this job
  rules:
    # Run on merge requests
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    # Run on pushes to default branch
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    # Allow manual trigger on any branch
    - if: $CI_PIPELINE_SOURCE == "web"
      when: manual

# =============================================================================
# Alternative Configurations (uncomment as needed)
# =============================================================================

# Non-blocking scan (pipeline continues even if guardrails fail)
# security-scan-non-blocking:
#   extends: security-scan
#   allow_failure: true

# Manual-only scan
# security-scan-manual:
#   extends: security-scan
#   rules:
#     - when: manual

# Scheduled scan (configure schedule in GitLab CI/CD > Schedules)
# security-scan-scheduled:
#   extends: security-scan
#   rules:
#     - if: $CI_PIPELINE_SOURCE == "schedule"
