name: "ZIRAN Agent Security Scan"
description: "Run ZIRAN automated security scans on AI agents in your CI/CD pipeline"
author: "ZIRAN Project"

branding:
  icon: "shield"
  color: "red"

inputs:
  command:
    description: >
      The ZIRAN sub-command to run.
      Supported: scan, audit, policy, ci
    required: false
    default: "ci"

  result-file:
    description: >
      Path to a previously generated ZIRAN campaign result JSON file.
      Required for: policy, ci (when not running a live scan).
    required: false
    default: ""

  source-path:
    description: >
      Path to the agent source code directory.
      Required for: audit.
    required: false
    default: "."

  # ── Live scan inputs ──────────────────────────────────────────────
  framework:
    description: >
      Agent framework to scan.
      Required for: scan.
      Options: langchain, crewai, bedrock
    required: false
    default: ""

  agent-path:
    description: >
      Path to the agent source file or configuration.
      Required for: scan.
    required: false
    default: ""

  coverage:
    description: >
      Scan coverage level.
      Options: essential, standard, comprehensive
    required: false
    default: "standard"

  concurrency:
    description: "Maximum number of concurrent attack workers."
    required: false
    default: "5"

  stop-on-critical:
    description: "Stop the scan immediately when a critical finding is detected."
    required: false
    default: "true"

  custom-attacks:
    description: >
      Path to a directory of custom YAML attack vectors.
      If omitted, only built-in vectors are used.
    required: false
    default: ""

  # ── Common inputs ─────────────────────────────────────────────────
  policy-file:
    description: >
      Path to a custom YAML policy file.
      If omitted the built-in ZIRAN policy is used.
    required: false
    default: ""

  gate-config:
    description: >
      Path to a quality-gate YAML config.
      If omitted, default thresholds are used.
    required: false
    default: ""

  sarif-output:
    description: >
      Path where the SARIF report will be written.
      Set to empty string to disable SARIF generation.
    required: false
    default: "ziran-results.sarif"

  severity-threshold:
    description: >
      Only report findings at or above this severity.
      Options: critical, high, medium, low
    required: false
    default: "low"

  python-version:
    description: "Python version to use."
    required: false
    default: "3.12"

  ziran-version:
    description: >
      ZIRAN package version or install spec.
      Use "." to install from local checkout (e.g. for testing).
    required: false
    default: "ziran"

outputs:
  status:
    description: "Gate result: passed or failed"
    value: ${{ steps.run.outputs.status }}
  trust-score:
    description: "Agent trust score (0.0-1.0)"
    value: ${{ steps.run.outputs.trust_score }}
  total-findings:
    description: "Total number of security findings"
    value: ${{ steps.run.outputs.total_findings }}
  critical-findings:
    description: "Number of critical findings"
    value: ${{ steps.run.outputs.critical_findings }}
  sarif-file:
    description: "Path to the generated SARIF file"
    value: ${{ steps.run.outputs.sarif_file }}

runs:
  using: "composite"
  steps:
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ inputs.python-version }}

    - name: Install ZIRAN
      shell: bash
      run: |
        pip install --quiet "${{ inputs.ziran-version }}"

    - name: Run ZIRAN
      id: run
      shell: bash
      run: |
        set +e

        CMD="${{ inputs.command }}"
        EXTRA_ARGS=""

        case "$CMD" in
          scan)
            # ── Validate required scan inputs ───────────────────────
            if [ -z "${{ inputs.framework }}" ]; then
              echo "::error::framework is required for the 'scan' command"
              exit 1
            fi
            if [ -z "${{ inputs.agent-path }}" ]; then
              echo "::error::agent-path is required for the 'scan' command"
              exit 1
            fi

            SCAN_ARGS="--framework ${{ inputs.framework }}"
            SCAN_ARGS="$SCAN_ARGS --agent-path ${{ inputs.agent-path }}"
            SCAN_ARGS="$SCAN_ARGS --coverage ${{ inputs.coverage }}"
            SCAN_ARGS="$SCAN_ARGS --concurrency ${{ inputs.concurrency }}"
            SCAN_ARGS="$SCAN_ARGS -o ziran_results"

            if [ "${{ inputs.stop-on-critical }}" = "false" ]; then
              SCAN_ARGS="$SCAN_ARGS --no-stop-on-critical"
            fi
            if [ -n "${{ inputs.custom-attacks }}" ]; then
              SCAN_ARGS="$SCAN_ARGS --custom-attacks ${{ inputs.custom-attacks }}"
            fi

            echo "::group::Running ZIRAN scan"
            ziran scan $SCAN_ARGS
            SCAN_EXIT=$?
            echo "::endgroup::"

            # ── Chain into CI gate for outputs & SARIF ──────────────
            RESULT_JSON=$(find ziran_results -name 'campaign_*_report.json' | head -n1)
            if [ -z "$RESULT_JSON" ]; then
              echo "::error::Scan completed but no campaign result JSON was found"
              exit ${SCAN_EXIT:-1}
            fi

            CI_ARGS="$RESULT_JSON"
            if [ -n "${{ inputs.gate-config }}" ]; then
              CI_ARGS="$CI_ARGS --gate-config ${{ inputs.gate-config }}"
            fi
            if [ -n "${{ inputs.policy-file }}" ]; then
              CI_ARGS="$CI_ARGS --policy ${{ inputs.policy-file }}"
            fi
            if [ -n "${{ inputs.sarif-output }}" ]; then
              CI_ARGS="$CI_ARGS --sarif ${{ inputs.sarif-output }}"
            fi

            echo "::group::Running ZIRAN CI gate"
            ziran ci $CI_ARGS
            EXIT_CODE=$?
            echo "::endgroup::"
            ;;
          ci)
            if [ -n "${{ inputs.result-file }}" ]; then
              EXTRA_ARGS="${{ inputs.result-file }}"
            else
              echo "::error::result-file is required for the 'ci' command"
              exit 1
            fi
            if [ -n "${{ inputs.gate-config }}" ]; then
              EXTRA_ARGS="$EXTRA_ARGS --gate-config ${{ inputs.gate-config }}"
            fi
            if [ -n "${{ inputs.policy-file }}" ]; then
              EXTRA_ARGS="$EXTRA_ARGS --policy ${{ inputs.policy-file }}"
            fi
            if [ -n "${{ inputs.sarif-output }}" ]; then
              EXTRA_ARGS="$EXTRA_ARGS --sarif ${{ inputs.sarif-output }}"
            fi
            ;;
          audit)
            EXTRA_ARGS="${{ inputs.source-path }}"
            if [ -n "${{ inputs.severity-threshold }}" ]; then
              EXTRA_ARGS="$EXTRA_ARGS --severity ${{ inputs.severity-threshold }}"
            fi
            ;;
          policy)
            EXTRA_ARGS="${{ inputs.result-file }}"
            if [ -n "${{ inputs.policy-file }}" ]; then
              EXTRA_ARGS="$EXTRA_ARGS --policy ${{ inputs.policy-file }}"
            fi
            ;;
          *)
            echo "::error::Unknown command '$CMD'. Supported: scan, ci, audit, policy"
            exit 1
            ;;
        esac

        # For scan, EXIT_CODE is already set above; skip the generic run
        if [ "$CMD" != "scan" ]; then
          ziran $CMD $EXTRA_ARGS
          EXIT_CODE=$?
        fi

        if [ $EXIT_CODE -eq 0 ]; then
          echo "status=passed" >> "$GITHUB_OUTPUT"
        else
          echo "status=failed" >> "$GITHUB_OUTPUT"
        fi

        # Upload SARIF if it exists
        if [ -n "${{ inputs.sarif-output }}" ] && [ -f "${{ inputs.sarif-output }}" ]; then
          echo "sarif_file=${{ inputs.sarif-output }}" >> "$GITHUB_OUTPUT"
        fi

        exit $EXIT_CODE

    - name: Upload SARIF
      if: always() && inputs.sarif-output != '' && hashFiles(inputs.sarif-output) != ''
      uses: github/codeql-action/upload-sarif@v3
      with:
        sarif_file: ${{ inputs.sarif-output }}
      continue-on-error: true
