GitHub Actions: Prisma AIRS Model Security

Add pre-deployment AI model scanning to GitHub Actions CI/CD pipelines using the Prisma AIRS Model Security SDK

Model Security, Not Runtime Security

This guide integrates Prisma AIRS Model Security: pre-deployment scanning of AI model files (weights, binaries, configs) before they reach production infrastructure. This is different from AI Runtime Security, which scans prompts and responses at inference time.

Model Security uses OAuth2 client credentials (Client ID + Client Secret from a Prisma Cloud Service Account), not an AIRS API key. The authentication flow, SDK, and API endpoints are distinct from the Runtime Security integrations covered in the other AIRS guides.

Scanning PhaseSupportedDescription
Model file scan✅Scans model weights/binaries for security risks before deployment
PromptN/AUse an AI Runtime Security integration for prompt scanning
ResponseN/AUse an AI Runtime Security integration for response scanning
StreamingN/ANot applicable to model file scanning
Pre-tool callN/ANot applicable to model file scanning
Post-tool callN/ANot applicable to model file scanning

Architecture Overview

The GitHub Actions integration adds a security gate to the CI/CD pipeline. When an AI model configuration changes, the workflow authenticates with Prisma AIRS via OAuth2, installs the Model Security SDK from a private PyPI repository, and scans the model. A scan result of ALLOWED permits deployment; BLOCKED halts the pipeline and prevents the model from reaching production.

Git Push model-config.yaml GitHub Actions Detect Changes Setup Python 3.11 Install SDK OAuth2 Auth Client ID + Secret Token Endpoint Private PyPI URL AIRS Model Scan Malware detection Vulnerability check Supply chain risk Compliance review ALLOWED Deploy BLOCKED Pipeline fails PR branches: scan only (no deploy) | Main branch: scan + deploy on ALLOWED
What Gets Scanned

Prisma AIRS Model Security evaluates AI model files for:

  • Model provenance and supply chain risks — verifying model source and integrity
  • Known vulnerabilities — checking against CVEs and security advisories
  • Malicious payloads — detecting embedded malicious code or backdoors in model weights
  • Compliance violations — ensuring models meet organizational security policies
  • Training data risks — identifying potential data poisoning indicators

Phase 1: Prerequisites

▶ Step 1.1: Confirm GitHub Repository Access

The GitHub Actions workflow requires a repository with Actions enabled and sufficient permissions to create secrets.

  1. Open the target GitHub repository.
  2. Navigate to Settings > Actions > General.
  3. Confirm Actions permissions is set to Allow all actions and reusable workflows (or a policy that permits the required actions).
  4. Confirm the account has Admin or Maintain role on the repository (required to create repository secrets).
Verification

The Settings > Actions page loads without permission errors, and the Secrets and variables submenu is visible in the left sidebar.

▶ Step 1.2: Confirm Prisma AIRS Model Security License

Model Security scanning requires an active Prisma AIRS subscription with the Model Security add-on enabled.

  1. Log in to Strata Cloud Manager.
  2. Navigate to AI Access Security.
  3. Confirm the Model Security section is visible and accessible.
Note

If Model Security is not visible, contact the Palo Alto Networks account team to enable the add-on. The Model Security documentation covers licensing prerequisites.

Verification

The Model Security dashboard loads and displays scan activity logs (empty is fine for new deployments).

▶ Step 1.3: Create a Prisma Cloud Service Account

The CI/CD pipeline authenticates with Prisma AIRS using OAuth2 client credentials from a dedicated service account.

  1. In Strata Cloud Manager, navigate to Settings > Identity & Access > Service Accounts.
  2. Click Add > Add Service Account.
  3. Set Name to a descriptive identifier (e.g., github-actions-model-security).
  4. Assign the appropriate role for Model Security API access.
  5. Click Save.
  6. Record the Client ID and Client Secret immediately — the secret is shown only once.
  7. Note the TSG ID (Tenant Service Group ID) from the account details page.
Store Credentials Securely

The Client Secret cannot be retrieved after initial creation. Store it in a password manager or secrets vault before navigating away from the page.

Verification

The service account appears in the Service Accounts list with status Active. The Client ID, Client Secret, and TSG ID are recorded.

▶ Step 1.4: Configure a Model Security Profile

The security profile defines which scan rules and thresholds apply to model evaluations. The scan script references this profile by its UUID.

  1. In Strata Cloud Manager, navigate to AI Access Security > Model Security.
  2. Create or select a Security Profile (also called a Security Group).
  3. Configure the desired scan rules — malware detection, vulnerability checks, compliance policies.
  4. Save the profile and record the Profile UUID from the profile details.
  5. Note the API Endpoint URL (e.g., https://api.sase.paloaltonetworks.com/aims).
Verification

The security profile appears in the list with at least one scan rule enabled. The Profile UUID and API Endpoint URL are recorded.

Phase 2: Credential Configuration

▶ Step 2.1: Create GitHub Actions Secrets for Model Security

GitHub Actions secrets store the OAuth2 credentials and scan configuration. The workflow references these secrets as environment variables at runtime.

  1. Open the GitHub repository and navigate to Settings > Secrets and variables > Actions.
  2. Click New repository secret and create each secret listed below.
Secret NameValueDescription
MODEL_SECURITY_CLIENT_IDClient ID from Step 1.3OAuth2 client ID for Prisma AIRS authentication
MODEL_SECURITY_CLIENT_SECRETClient Secret from Step 1.3OAuth2 client secret for Prisma AIRS authentication
MODEL_SECURITY_PROFILE_IDProfile UUID from Step 1.4Security profile UUID that defines scan rules
MODEL_SECURITY_API_ENDPOINTAPI Endpoint from Step 1.4Prisma AIRS API base URL
TSG_IDTSG ID from Step 1.3Tenant Service Group identifier
Different From Runtime Security Secrets

These secrets are not the same as PRISMA_AIRS_API_KEY, PRISMA_AIRS_PROFILE_NAME, or PRISMA_AIRS_URL used by AI Runtime Security integrations. Model Security uses OAuth2 client credentials and communicates through the Model Security SDK, not the Runtime Scan API.

Verification

All five secrets appear in the Repository secrets list with Updated timestamps. Each shows as a masked value; exact values are never displayed after creation.

Phase 3: Workflow Setup

▶ Step 3.1: Understand the Repository Structure

The integration requires three categories of files: the GitHub Actions workflow definition, the authentication and scan scripts, and a model configuration file that triggers the pipeline.

. ├── .github/ │ └── workflows/ │ └── model-security-scan.yml # GitHub Actions workflow definition ├── config/ │ └── model-config.yaml # Model configuration (pipeline trigger) ├── scripts/ │ ├── scan_model.py # Prisma AIRS Model Security scan script │ └── get_pypi_url.sh # Private PyPI authentication script ├── requirements.txt # Python dependencies (pyyaml, python-dotenv) └── README.md
Note

The reference implementation includes optional deployment scripts (deploy_model.sh, test_model.py, undeploy_model.sh) targeting Google Cloud Vertex AI. Replace these with deployment logic for the target infrastructure (AWS SageMaker, Azure ML, on-premises, etc.).

Verification

The file structure is understood. Proceed to create each file in the following steps.

▶ Step 3.2: Create the PyPI Authentication Script

The Model Security SDK is distributed through a private PyPI repository. This script authenticates with the Prisma AIRS token endpoint to obtain an access token, then uses that token to retrieve the private PyPI URL.

Create scripts/get_pypi_url.sh:

#!/bin/bash
#
# Model Security Private PyPI Authentication Script
# Authenticates with SCM and retrieves PyPI repository URL
#

set -euo pipefail

# Check required environment variables
: "${MODEL_SECURITY_CLIENT_ID:?Error: MODEL_SECURITY_CLIENT_ID not set}"
: "${MODEL_SECURITY_CLIENT_SECRET:?Error: MODEL_SECURITY_CLIENT_SECRET not set}"
: "${TSG_ID:?Error: TSG_ID not set}"

# Set default endpoints
API_ENDPOINT="${MODEL_SECURITY_API_ENDPOINT:-https://api.sase.paloaltonetworks.com/aims}"
TOKEN_ENDPOINT="${MODEL_SECURITY_TOKEN_ENDPOINT:-https://auth.apps.paloaltonetworks.com/oauth2/access_token}"

# Get SCM access token via OAuth2 client credentials
TOKEN_RESPONSE=$(curl -sf -X POST "$TOKEN_ENDPOINT" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -u "$MODEL_SECURITY_CLIENT_ID:$MODEL_SECURITY_CLIENT_SECRET" \
    -d "grant_type=client_credentials&scope=tsg_id:$TSG_ID") || {
    echo "Error: Failed to obtain SCM access token" >&2
    exit 1
}

SCM_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
if [[ -z "$SCM_TOKEN" || "$SCM_TOKEN" == "null" ]]; then
    echo "Error: Failed to extract access token from response" >&2
    exit 1
fi

# Use access token to retrieve private PyPI URL
PYPI_RESPONSE=$(curl -sf -X GET "$API_ENDPOINT/mgmt/v1/pypi/authenticate" \
    -H "Authorization: Bearer $SCM_TOKEN") || {
    echo "Error: Failed to retrieve PyPI URL" >&2
    exit 1
}

PYPI_URL=$(echo "$PYPI_RESPONSE" | jq -r '.url')
if [[ -z "$PYPI_URL" || "$PYPI_URL" == "null" ]]; then
    echo "Error: Failed to extract PyPI URL from response" >&2
    exit 1
fi

echo "$PYPI_URL"

Make the script executable:

chmod +x scripts/get_pypi_url.sh
OAuth2 Authentication Flow

The script performs two API calls: (1) POST to the token endpoint at https://auth.apps.paloaltonetworks.com/oauth2/access_token with client credentials to get an access token, then (2) GET to the API endpoint's /mgmt/v1/pypi/authenticate path using that token to retrieve the private PyPI URL. The jq tool must be available on the runner.

Verification

The file scripts/get_pypi_url.sh exists, is executable, and contains the OAuth2 authentication logic.

▶ Step 3.3: Create the Model Scan Script

The scan script loads the model configuration, initializes the Model Security SDK client, and calls the scan API. A result of BLOCKED causes a non-zero exit, which fails the GitHub Actions job and prevents deployment.

Create scripts/scan_model.py:

#!/usr/bin/env python3
"""
Prisma AIRS Model Security Scanner

Scans an AI model against Palo Alto Prisma AIRS security policies
using the official Model Security SDK. Exits non-zero if the model
fails the security assessment, blocking the CI/CD pipeline.
"""

import argparse
import os
import sys

import yaml
from dotenv import load_dotenv
from model_security_client.api import ModelSecurityAPIClient

load_dotenv()


def print_scan_results(result, model_name):
    """Pretty-print the scan results for CI log visibility."""
    print()
    print("=" * 64)
    print("  PRISMA AIRS MODEL SECURITY SCAN RESULTS")
    print("=" * 64)
    print(f"  Model:        {model_name}")
    print(f"  Scan UUID:    {getattr(result, 'uuid', 'N/A')}")
    print(f"  Outcome:      {result.eval_outcome}")
    print(f"  Summary:      {getattr(result, 'eval_summary', 'N/A')}")
    print(f"  Sec. Group:   {getattr(result, 'security_group_name', 'N/A')}")
    print(f"  Rules:        {getattr(result, 'enabled_rule_count_snapshot', 'N/A')}")
    print(f"  Files Scanned:{getattr(result, 'total_files_scanned', 'N/A')}")
    print(f"  Files Skipped:{getattr(result, 'total_files_skipped', 'N/A')}")

    violations = (
        getattr(result, 'rule_violations', None)
        or getattr(result, 'findings', [])
    )
    if violations:
        print(f"  Violations:   {len(violations)}")
        for v in violations:
            severity = getattr(v, 'severity', getattr(v, 'level', 'unknown'))
            desc = getattr(v, 'description', getattr(v, 'rule_name', 'No description'))
            print(f"    [{str(severity).upper()}] {desc}")

    print("=" * 64)
    print()


def main():
    parser = argparse.ArgumentParser(
        description="Scan an AI model with Prisma AIRS Model Security"
    )
    parser.add_argument(
        "--config", required=True,
        help="Path to the model configuration YAML file",
    )
    args = parser.parse_args()

    with open(args.config) as f:
        config = yaml.safe_load(f)

    if not config.get("security", {}).get("scan_enabled", True):
        print("Security scan is disabled in model config. Skipping.")
        sys.exit(0)

    # Allow overriding security_profile_id from environment variable
    profile_id = os.environ.get("MODEL_SECURITY_PROFILE_ID")
    if profile_id:
        config.setdefault("security", {})["security_profile_id"] = profile_id

    security_group_uuid = config["security"]["security_profile_id"]
    if not security_group_uuid:
        print("ERROR: No security_profile_id configured.")
        sys.exit(1)

    model_name = config["model"]["huggingface_id"]
    model_uri = f"https://huggingface.co/{model_name}"
    api_endpoint = os.environ.get(
        "MODEL_SECURITY_API_ENDPOINT",
        "https://api.sase.paloaltonetworks.com/aims",
    )

    print("Initializing Prisma AIRS Model Security SDK...")
    client = ModelSecurityAPIClient(base_url=api_endpoint)

    labels = {
        "deployment_target": "vertex_ai",
        "machine_type": config["deployment"]["machine_type"],
        "version": config["model"].get("version", "unknown").replace(".", "-"),
        "pipeline": "github-actions",
    }

    print(f"Scanning model: {model_name}")
    print(f"  URI: {model_uri}")
    print(f"  Security Group: {security_group_uuid}")
    print(f"  Labels: {labels}")

    result = client.scan(
        security_group_uuid=security_group_uuid,
        model_uri=model_uri,
        labels=labels,
    )

    print_scan_results(result, model_name)

    outcome = str(result.eval_outcome)
    if "BLOCKED" in outcome:
        print("BLOCKED: Model blocked by Prisma AIRS security policy.")
        print("The model will NOT be deployed.")
        sys.exit(1)
    elif "ALLOWED" in outcome:
        print("ALLOWED: Model approved by Prisma AIRS security policy.")
        print("The model is cleared for deployment.")
    else:
        print(f"WARNING: Unexpected scan outcome '{outcome}'. Blocking deployment.")
        sys.exit(1)


if __name__ == "__main__":
    main()
Verification

The file scripts/scan_model.py exists. Running python scripts/scan_model.py --help outputs the argument parser usage without errors (assuming dependencies are installed).

▶ Step 3.4: Create the GitHub Actions Workflow

The workflow file defines the CI/CD pipeline. It triggers on changes to the model configuration file, runs the security scan, and conditionally deploys the model.

Create .github/workflows/model-security-scan.yml:

name: Model Security Scan & Deploy

on:
  push:
    branches: [main]
    paths:
      - 'config/model-config.yaml'
  pull_request:
    branches: [main]
    paths:
      - 'config/model-config.yaml'
  workflow_dispatch:

jobs:

  # Detect whether the model config file changed
  detect-changes:
    name: Detect Model Changes
    runs-on: ubuntu-latest
    outputs:
      model_changed: ${{ steps.filter.outputs.model }}
    steps:
      - uses: actions/checkout@v4

      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            model:
              - 'config/model-config.yaml'

  # ── Prisma AIRS Model Security Scan ──────────────────────────
  # This job authenticates with Prisma AIRS via OAuth2,
  # installs the Model Security SDK from a private PyPI,
  # and scans the model. BLOCKED = pipeline failure.
  security-scan:
    name: Prisma AIRS Model Security Scan
    needs: detect-changes
    if: >
      needs.detect-changes.outputs.model_changed == 'true'
      || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      # Authenticate and get private PyPI URL
      - name: Get Model Security SDK PyPI URL
        id: pypi
        env:
          MODEL_SECURITY_CLIENT_ID: ${{ secrets.MODEL_SECURITY_CLIENT_ID }}
          MODEL_SECURITY_CLIENT_SECRET: ${{ secrets.MODEL_SECURITY_CLIENT_SECRET }}
          MODEL_SECURITY_API_ENDPOINT: ${{ secrets.MODEL_SECURITY_API_ENDPOINT }}
          TSG_ID: ${{ secrets.TSG_ID }}
        run: echo "url=$(bash scripts/get_pypi_url.sh)" >> "$GITHUB_OUTPUT"

      # Install SDK from private PyPI
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install "model-security-client[all]" \
            --extra-index-url "${{ steps.pypi.outputs.url }}"

      # Run the scan — exits non-zero on BLOCKED
      - name: Run Prisma AIRS Model Security Scan
        env:
          MODEL_SECURITY_CLIENT_ID: ${{ secrets.MODEL_SECURITY_CLIENT_ID }}
          MODEL_SECURITY_CLIENT_SECRET: ${{ secrets.MODEL_SECURITY_CLIENT_SECRET }}
          MODEL_SECURITY_API_ENDPOINT: ${{ secrets.MODEL_SECURITY_API_ENDPOINT }}
          MODEL_SECURITY_PROFILE_ID: ${{ secrets.MODEL_SECURITY_PROFILE_ID }}
          TSG_ID: ${{ secrets.TSG_ID }}
        run: python scripts/scan_model.py --config config/model-config.yaml

  # ── Deploy (replace with your infrastructure) ────────────────
  deploy-model:
    name: Deploy Model
    needs: security-scan
    if: >
      (github.event_name == 'push' && github.ref == 'refs/heads/main')
      || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Add your deployment logic here"
Workflow Triggers

The workflow triggers on three events: (1) a push to main that modifies config/model-config.yaml, (2) a pull request targeting main with the same path filter, and (3) a manual workflow_dispatch. On pull requests, only the scan runs; deployment is skipped. On pushes to main, both scan and deploy run if the scan passes.

Verification

The file .github/workflows/model-security-scan.yml exists. Navigate to the repository's Actions tab in GitHub, and the workflow Model Security Scan & Deploy appears in the left sidebar.

Phase 4: Scan Configuration

▶ Step 4.1: Create the Model Configuration File

The model configuration file specifies which model to scan and whether scanning is enabled. Changes to this file trigger the GitHub Actions workflow.

Create config/model-config.yaml:

# Model Configuration
# Changing this file triggers the Prisma AIRS Model Security Scan pipeline.

model:
  huggingface_id: "google/gemma-3-1b-it"
  display_name: "gemma-3-1b-it"
  description: "Google Gemma 3 1B Instruction-Tuned model deployed via Vertex AI Model Garden"
  version: "2.1"

deployment:
  machine_type: "g2-standard-12"
  accelerator_type: "NVIDIA_L4"
  accelerator_count: 1
  region: "us-central1"

security:
  scan_enabled: true
  security_profile_id: ""  # Set via MODEL_SECURITY_PROFILE_ID env var or GitHub Variable
FieldDescriptionExample
model.huggingface_idThe HuggingFace model identifier. The scan script constructs the model URI from this value.google/gemma-3-1b-it
model.display_nameHuman-readable name for log output.gemma-3-1b-it
model.versionVersion string applied as a scan label for tracking.2.1
deployment.machine_typeGCP machine type for the model endpoint. Applied as a scan label.g2-standard-12
deployment.accelerator_typeGPU accelerator type for inference.NVIDIA_L4
deployment.accelerator_countNumber of accelerators attached to the endpoint.1
deployment.regionGCP region for deployment.us-central1
security.scan_enabledSet to false to skip scanning (the scan script exits 0 immediately).true
security.security_profile_idProfile UUID. Leave empty when overriding via the MODEL_SECURITY_PROFILE_ID environment variable.a1b2c3d4-...
Verification

The file config/model-config.yaml passes YAML lint validation. Running python -c "import yaml; yaml.safe_load(open('config/model-config.yaml'))" produces no errors.

▶ Step 4.2: Create the Python Dependencies File

The scan script requires pyyaml for configuration parsing and python-dotenv for environment variable loading. The Model Security SDK itself is installed separately from the private PyPI.

Create requirements.txt:

pyyaml>=6.0
python-dotenv>=1.0
Note

The model-security-client[all] SDK is not listed in requirements.txt. It is installed from a private PyPI repository using the authenticated URL retrieved by get_pypi_url.sh. See Model Security SDK installation guide for details.

Verification

The file requirements.txt exists with pyyaml and python-dotenv listed.

▶ Step 4.3: Configure Scan Labels for Tracking

The scan script applies labels to each scan result, enabling filtering and correlation in Strata Cloud Manager. Modify the labels dictionary in scripts/scan_model.py to match the deployment environment.

labels = {
    "pipeline": "github-actions",
    "deployment_target": "vertex_ai",        # Replace with target platform
    "version": config["model"].get("version", "unknown").replace(".", "-"),
    "team": "ml-platform",                   # Optional: owning team
    "environment": "production",             # Optional: target environment
}

These labels appear in the Strata Cloud Manager scan activity logs and support filtering scans by pipeline source, deployment target, and team ownership.

Verification

The labels dictionary in scan_model.py includes at minimum a pipeline key set to github-actions.

Phase 5: Verification

▶ Step 5.1: Trigger the Pipeline

Trigger the workflow by modifying the model configuration and pushing the change, or by using the manual dispatch option.

Option A: Push a Configuration Change

  1. Edit config/model-config.yaml (e.g., update the version field).
  2. Commit and push the change to the main branch (or open a pull request).
git add config/model-config.yaml
git commit -m "chore: bump model version to trigger security scan"
git push origin main

Option B: Manual Dispatch

  1. Navigate to the repository's Actions tab.
  2. Select the Model Security Scan & Deploy workflow.
  3. Click Run workflow > select the branch > click Run workflow.
Verification

The Actions tab shows a new workflow run in In progress or Queued state for the Model Security Scan & Deploy workflow.

▶ Step 5.2: Verify Scan Results in GitHub Actions

The scan script produces structured output in the GitHub Actions logs showing the model name, scan UUID, outcome, and any violations.

  1. Open the workflow run from the Actions tab.
  2. Click the Prisma AIRS Model Security Scan job.
  3. Expand the Run Prisma AIRS Model Security Scan step.
  4. Locate the scan results block in the log output.

Expected output for a passing scan:

================================================================
  PRISMA AIRS MODEL SECURITY SCAN RESULTS
================================================================
  Model:        google/gemma-3-1b-it
  Scan UUID:    a1b2c3d4-e5f6-7890-abcd-ef1234567890
  Outcome:      EvalOutcome.ALLOWED
  Summary:      Model passed all security checks
  ...
================================================================

ALLOWED: Model approved by Prisma AIRS security policy.
The model is cleared for deployment.
Verification

The security-scan job shows a green checkmark. The log output contains ALLOWED: Model approved by Prisma AIRS security policy and a valid Scan UUID.

▶ Step 5.3: Verify Results in Strata Cloud Manager

All scan results are recorded in Strata Cloud Manager for audit and compliance tracking, regardless of the pipeline outcome.

  1. Log in to Strata Cloud Manager.
  2. Navigate to AI Access Security > Model Security.
  3. Locate the scan entry matching the Scan UUID from the GitHub Actions log.
  4. Confirm the scan outcome, rule evaluations, and model details match expectations.
Note

Scan results include the pipeline: github-actions label, enabling filtering to isolate CI/CD-originated scans from manual or SDK-initiated scans.

Verification

The scan entry appears in the Model Security activity log with the correct model name, outcome, and github-actions pipeline label.

Troubleshooting

▶ Common Issues and Resolutions
IssueCauseResolution
get_pypi_url.sh fails with Failed to obtain SCM access token Invalid Client ID, Client Secret, or TSG ID Verify the MODEL_SECURITY_CLIENT_ID, MODEL_SECURITY_CLIENT_SECRET, and TSG_ID secrets match the values from the service account in Strata Cloud Manager.
SDK installation fails with pip install error Private PyPI URL was not retrieved in the previous step Check the Get Model Security SDK PyPI URL step log. If the PyPI URL step failed silently, the subsequent pip install cannot find the model-security-client package.
Scan returns unexpected BLOCKED result Security profile rules are more restrictive than expected Check the MODEL_SECURITY_PROFILE_ID points to the intended security profile. Review the profile's rules in Strata Cloud Manager and adjust thresholds.
Workflow does not trigger on push The changed file does not match the paths filter The workflow triggers only on changes to config/model-config.yaml. Confirm the file path matches exactly. Use workflow_dispatch for manual testing.
jq: command not found The runner image does not include jq The ubuntu-latest runner includes jq by default. If using a custom runner, add sudo apt-get install -y jq before the PyPI authentication step.
Scan passes but deploy job does not run The deploy job runs only on pushes to main or manual dispatch Pull request triggers run the scan only. Merge the PR to main to trigger the deploy job.
Verification

The troubleshooting table covers the issue encountered. If the issue persists, collect the full GitHub Actions log output and the Scan UUID for a support case with Palo Alto Networks.