یکپارچه‌سازی نسخه 8.0 آخرین بروزرسانی: ۱۴۰۵/۰۲/۲۰ ⏱ ۲۵ دقیقه

یکپارچه‌سازی Deployment Analyzer

راهنمای کامل اتصال WarnX به pipeline های CI/CD برای اسکن خودکار امنیتی هر بار که کد جدیدی deploy می‌شود. پشتیبانی از GitLab، GitHub، Jenkins، Bitbucket، CircleCI، Azure DevOps و ۷ زبان برنامه‌نویسی.

Non-blocking اسکن: WarnX هرگز pipeline شما را متوقف نمی‌کند. اسکن در پس‌زمینه اجرا می‌شود و نتیجه از طریق پنل یا Polling API در دسترس است.

۱. معرفی و نحوه کارکرد

WarnX Deployment Analyzer هر بار که کد جدیدی به محیط Production یا Staging deploy می‌شود، به‌صورت خودکار یک اسکن امنیتی انجام می‌دهد.

🚀
Deploy شما
📡
فراخوانی API WarnX
ثبت فوری (pending)
🔍
Scanner پس‌زمینه
📊
نتیجه + Action

مراحل اسکن

1
Rule Engine

بررسی مسیرهای تغییریافته با ۱۵+ دسته الگوی امنیتی (payment، auth، file upload، ...)

2
ML Scorer (Python)

تحلیل semantic کد در صورت ارسال diff_b64 — دقت بالاتر

3
Manifest Amplifier

تطبیق با پروفایل امنیتی اختصاصی پروژه — کاهش false positive

تضمین‌های سیستم

🔁 Idempotent

ارسال مجدد با event_id یکسان → 409 بدون ثبت مضاعف

Non-blocking

اسکن هرگز pipeline را متوقف نمی‌کند

📦 Offline-safe

در صورت قطع ارتباط، رویداد در queue محلی ذخیره می‌شود

۲. پیش‌نیازها

ابزارنسخههدف
curl≥ 7.55ارسال درخواست HTTP
jq≥ 1.6پردازش JSON در shell
openssl≥ 1.1امضای HMAC
git≥ 2.xدریافت فایل‌های تغییریافته
sha256sumهر نسخهساخت event_id یکتا
bash
apk add --no-cache git jq curl openssl coreutils
bash
apt-get install -y jq curl openssl coreutils git

۳. ساخت پروژه و دریافت API Key

  1. وارد پنل WarnX شوید: panel.warnx.ir
  2. از منوی Deployments روی + اتصال پروژه جدید کلیک کنید
  3. نام پروژه را وارد کنید (مثال: Backend API - Production)
  4. یک API Key اختصاصی برای این پروژه ایجاد می‌شود
  5. این کلید را در متغیر محیطی CI/CD با نام WARNX_API_KEY ذخیره کنید
امنیت: کلید API را هرگز در کد commit نکنید. همیشه از secrets یا variables سیستم CI/CD استفاده کنید.

۴. فرمت درخواست API

POST https://warnx.ir/api/v1/deploy/ingest
json
{
  "api_key":    "warnx_xxxxxxxxxxxxxxxx",
  "event_id":   "sha256-hash-of-commit+branch+paths",
  "target_url": "https://api.example.com",
  "commit_id":  "a1b2c3d4",
  "branch":     "main",
  "paths":      ["app/Controllers/AuthController.php", "app/Models/User.php"],
  "sequence":   42,
  "timestamp":  1715000000000,
  "description": "توضیح اختیاری این deploy",
  "diff_b64":   "base64-encoded-git-diff (اختیاری)"
}
api_key string ضروری کلید API پروژه
target_url string ضروری آدرس محیط deploy شده
event_id string توصیه شده شناسه یکتا برای جلوگیری از ثبت مضاعف
commit_id string توصیه شده SHA کوتاه commit
branch string توصیه شده نام branch
paths string[] توصیه شده آرایه مسیرهای فایل‌های تغییریافته
diff_b64 string اختیاری git diff به‌صورت base64 — فعال‌سازی ML scorer

۵.۱ GitLab CI/CD

این job به‌عنوان آخرین مرحله pipeline اجرا می‌شود و با allow_failure: true هرگز pipeline را block نمی‌کند.

.gitlab-ci.yml
warnx-security-scan:
  stage: .post
  image: alpine:3.19

  variables:
    WARNX_ENDPOINT: "https://warnx.ir/api/v1/deploy/ingest"
    QUEUE_FILE: "/tmp/warnx-queue.jsonl"

  before_script:
    - apk add --no-cache git jq curl openssl coreutils

  script: |
    [ -z "$WARNX_API_KEY" ] && echo "no api key" && exit 0

    BEFORE="${CI_MERGE_REQUEST_DIFF_BASE_SHA:-$CI_COMMIT_BEFORE_SHA}"
    AFTER="$CI_COMMIT_SHA"

    if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
      CHANGED=$(git log -1 --diff-filter=ACMRT --name-only --pretty=format: 2>/dev/null || echo "")
    else
      CHANGED=$(git diff --name-only "$BEFORE" "$AFTER" 2>/dev/null || echo "")
    fi

    PATHS_JSON=$(printf '%s\n' "$CHANGED" | grep -v '^$' | \
      jq -R -s -c 'split("\n")[:-1]' 2>/dev/null || echo '[]')

    BRANCH="${CI_COMMIT_REF_NAME:-unknown}"
    COMMIT_ID="${CI_COMMIT_SHORT_SHA:-unknown}"
    SEQUENCE="${CI_PIPELINE_ID:-0}"
    TIMESTAMP=$(date +%s%3N 2>/dev/null || date +%s)000

    SORTED_PATHS=$(echo "$PATHS_JSON" | jq -c 'sort' 2>/dev/null || echo '[]')
    EVENT_ID=$(printf '%s' "${COMMIT_ID}${BRANCH}${SORTED_PATHS}" | sha256sum | cut -d' ' -f1)

    PAYLOAD=$(jq -n \
      --arg     event_id  "$EVENT_ID" \
      --arg     api_key   "$WARNX_API_KEY" \
      --arg     url       "${CI_ENVIRONMENT_URL:-Not_Provided}" \
      --arg     commit    "$COMMIT_ID" \
      --arg     branch    "$BRANCH" \
      --argjson paths     "$PATHS_JSON" \
      --argjson sequence  "$SEQUENCE" \
      --argjson timestamp "$TIMESTAMP" \
      '{event_id:$event_id,api_key:$api_key,target_url:$url,
        commit_id:$commit,branch:$branch,paths:$paths,
        sequence:$sequence,timestamp:$timestamp}') || exit 0

    SIGNATURE=$(printf '%s' "$PAYLOAD" | \
      openssl dgst -sha256 -hmac "$WARNX_API_KEY" 2>/dev/null | sed 's/.*= //' || echo "")

    SUCCESS=false
    for ATTEMPT in 1 2 3; do
      case $ATTEMPT in 1) DELAY=2;; 2) DELAY=5;; 3) DELAY=10;; esac
      TH=$(mktemp); TB=$(mktemp)
      HTTP_CODE=$(curl -s --connect-timeout 5 --max-time 15 \
        -D "$TH" -o "$TB" -w "%{http_code}" \
        -X POST "$WARNX_ENDPOINT" \
        -H "Content-Type: application/json" \
        -H "X-WarnX-Signature: sha256=$SIGNATURE" \
        -H "X-WarnX-Event-ID: $EVENT_ID" \
        -d "$PAYLOAD" 2>/dev/null) || HTTP_CODE="000"
      BODY=$(cat "$TB" 2>/dev/null || echo '{}')
      rm -f "$TH" "$TB"
      ACCEPTED=$(echo "$BODY" | jq -r '.accepted // false' 2>/dev/null || echo "false")
      if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ] && [ "$ACCEPTED" = "true" ]; then
        SUCCESS=true; break
      fi
      [ "$HTTP_CODE" = "409" ] && SUCCESS=true && break
      if [ "$HTTP_CODE" -ge 400 ] && [ "$HTTP_CODE" -lt 500 ] && [ "$HTTP_CODE" != "429" ]; then break; fi
      [ $ATTEMPT -lt 3 ] && sleep $DELAY
    done

    if [ "$SUCCESS" = "false" ]; then
      echo "$PAYLOAD" >> "$QUEUE_FILE"
      [ "$BRANCH" = "production" ] && exit 1
    fi

  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - if: '$CI_COMMIT_BRANCH == "production"'
    - if: '$CI_COMMIT_BRANCH =~ /^hotfix\//'
    - if: '$CI_COMMIT_TAG'

  allow_failure: true

تنظیم متغیرها در GitLab

Settings → CI/CD → Variables
WARNX_API_KEYکلید API پروژه — Masked ✅ Protected ✅
CI_ENVIRONMENT_URLآدرس محیط deploy (مثال: https://api.yourapp.com)

۵.۲ GitHub Actions

.github/workflows/warnx.yml
name: WarnX Security Scan

on:
  push:
    branches: [main, master, production, develop]
    tags: ['v*']
  pull_request:
    branches: [main, master, production]
    types: [closed]

jobs:
  warnx-scan:
    if: github.event_name == 'push' || github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    continue-on-error: true

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: WarnX Security Sync
        env:
          WARNX_API_KEY: ${{ secrets.WARNX_API_KEY }}
          DEPLOY_URL:    ${{ vars.DEPLOY_URL }}
          SEQUENCE:      ${{ github.run_number }}
        run: |
          WARNX_ENDPOINT="https://warnx.ir/api/v1/deploy/ingest"
          [ -z "$WARNX_API_KEY" ] && exit 0

          BEFORE="${{ github.event.before }}"
          AFTER="${{ github.sha }}"

          if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
            CHANGED=$(git log -1 --diff-filter=ACMRT --name-only --pretty=format: 2>/dev/null || echo "")
          else
            CHANGED=$(git diff --name-only "$BEFORE" "$AFTER" 2>/dev/null || echo "")
          fi

          PATHS_JSON=$(printf '%s\n' "$CHANGED" | grep -v '^$' | \
            jq -R -s -c 'split("\n")[:-1]' || echo '[]')

          BRANCH="${{ github.ref_name }}"
          COMMIT_ID="${AFTER:0:8}"
          TIMESTAMP=$(date +%s%3N)000

          EVENT_ID=$(printf '%s' "${COMMIT_ID}${BRANCH}$(echo "$PATHS_JSON" | jq -c 'sort')" | sha256sum | cut -d' ' -f1)

          PAYLOAD=$(jq -n \
            --arg event_id "$EVENT_ID" --arg api_key "$WARNX_API_KEY" \
            --arg url "${DEPLOY_URL:-Not_Provided}" --arg commit "$COMMIT_ID" \
            --arg branch "$BRANCH" --argjson paths "$PATHS_JSON" \
            --argjson sequence "$SEQUENCE" --argjson timestamp "$TIMESTAMP" \
            '{event_id:$event_id,api_key:$api_key,target_url:$url,
              commit_id:$commit,branch:$branch,paths:$paths,
              sequence:$sequence,timestamp:$timestamp}')

          SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$WARNX_API_KEY" | sed 's/.*= //')

          SUCCESS=false
          for ATTEMPT in 1 2 3; do
            case $ATTEMPT in 1) D=2;; 2) D=5;; 3) D=10;; esac
            TH=$(mktemp); TB=$(mktemp)
            CODE=$(curl -s --connect-timeout 5 --max-time 15 \
              -D "$TH" -o "$TB" -w "%{http_code}" \
              -X POST "$WARNX_ENDPOINT" \
              -H "Content-Type: application/json" \
              -H "X-WarnX-Signature: sha256=$SIG" \
              -d "$PAYLOAD") || CODE="000"
            ACC=$(cat "$TB" | jq -r '.accepted // false')
            rm -f "$TH" "$TB"
            if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ] && [ "$ACC" = "true" ]; then SUCCESS=true; break; fi
            [ "$CODE" = "409" ] && SUCCESS=true && break
            if [ "$CODE" -ge 400 ] && [ "$CODE" -lt 500 ] && [ "$CODE" != "429" ]; then break; fi
            [ $ATTEMPT -lt 3 ] && sleep $D
          done

تنظیم در GitHub

Repository → Settings → Secrets and variables → Actions
WARNX_API_KEYSecret — کلید API پروژه
DEPLOY_URLVariable — آدرس محیط (https://api.yourapp.com)

۵.۳ Jenkins

در بلوک post { success { ... } } اضافه کنید تا فقط پس از deploy موفق اجرا شود.

Jenkinsfile
pipeline {
  agent any
  environment {
    WARNX_API_KEY  = credentials('warnx-api-key')
    DEPLOY_URL     = "${env.DEPLOY_URL ?: 'Not_Provided'}"
    WARNX_ENDPOINT = 'https://warnx.ir/api/v1/deploy/ingest'
  }
  stages {
    stage('Build')  { steps { sh 'make build'  } }
    stage('Test')   { steps { sh 'make test'   } }
    stage('Deploy') { steps { sh 'make deploy' } }
  }
  post {
    success {
      script {
        try {
          def commitId = (env.GIT_COMMIT ?: 'unknown').take(8)
          def branch   = env.BRANCH_NAME ?: 'unknown'
          def sequence = env.BUILD_NUMBER?.toLong() ?: 0L
          def prevCommit = env.GIT_PREVIOUS_SUCCESSFUL_COMMIT ?: ''

          def changed = prevCommit ? sh(
            script: "git diff --name-only ${prevCommit} ${env.GIT_COMMIT} 2>/dev/null || echo ''",
            returnStdout: true).trim() : ''

          def pathsJson = sh(
            script: """printf '%s\\n' '${changed}' | grep -v '^\$' | \
              jq -R -s -c 'split("\\n")[:-1]' || echo '[]'""",
            returnStdout: true).trim()

          def eventId = sh(
            script: "printf '%s' '${commitId}${branch}${pathsJson}' | sha256sum | cut -d' ' -f1",
            returnStdout: true).trim()

          def payload = groovy.json.JsonOutput.toJson([
            event_id: eventId, api_key: env.WARNX_API_KEY,
            target_url: DEPLOY_URL, commit_id: commitId,
            branch: branch, paths: new groovy.json.JsonSlurper().parseText(pathsJson),
            sequence: sequence, timestamp: System.currentTimeMillis()
          ])

          // retry loop omitted for brevity — see full example above
          sh """curl -s -X POST '${WARNX_ENDPOINT}' \
            -H 'Content-Type: application/json' \
            -d '${payload}' || true"""

        } catch (Exception e) {
          echo "[WarnX] Error: ${e.message}"
        }
      }
    }
  }
}

تنظیم Credential در Jenkins

Manage Jenkins → Manage Credentials → Add Credential
warnx-api-keyKind: Secret text — مقدار: کلید API

۵.۴ Bitbucket Pipelines

bitbucket-pipelines.yml
image: alpine:3.19

pipelines:
  branches:
    main:
      - step:
          name: Deploy
          script: [ echo "deploying..." ]
      - step:
          name: WarnX Security Scan
          script:
            - apk add --no-cache git jq curl openssl coreutils
            - |
              [ -z "$WARNX_API_KEY" ] && exit 0
              CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || \
                git log -1 --name-only --pretty=format: 2>/dev/null || echo "")
              PATHS_JSON=$(printf '%s\n' "$CHANGED" | grep -v '^$' | \
                jq -R -s -c 'split("\n")[:-1]' || echo '[]')
              BRANCH="${BITBUCKET_BRANCH:-unknown}"
              COMMIT="${BITBUCKET_COMMIT:-unknown}"
              TIMESTAMP=$(date +%s%3N)000
              EVENT_ID=$(printf '%s' "${COMMIT:0:8}${BRANCH}$(echo $PATHS_JSON | jq -c 'sort')" | \
                sha256sum | cut -d' ' -f1)
              PAYLOAD=$(jq -n \
                --arg event_id "$EVENT_ID" --arg api_key "$WARNX_API_KEY" \
                --arg url "${DEPLOY_URL:-Not_Provided}" --arg commit "${COMMIT:0:8}" \
                --arg branch "$BRANCH" --argjson paths "$PATHS_JSON" \
                --argjson sequence "${BITBUCKET_BUILD_NUMBER:-0}" --argjson timestamp "$TIMESTAMP" \
                '{event_id:$event_id,api_key:$api_key,target_url:$url,commit_id:$commit,
                  branch:$branch,paths:$paths,sequence:$sequence,timestamp:$timestamp}')
              curl -s -X POST "https://warnx.ir/api/v1/deploy/ingest" \
                -H "Content-Type: application/json" -d "$PAYLOAD" || true
          after-script:
            - exit 0

تنظیم متغیرها

Repository Settings → Pipelines → Repository variables
WARNX_API_KEYSecured ✅
DEPLOY_URLآدرس محیط production

۵.۵ CircleCI

.circleci/config.yml
version: 2.1

jobs:
  warnx-scan:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout:
          fetch-depth: 0
      - run:
          name: Install tools
          command: sudo apt-get install -y jq curl openssl coreutils
      - run:
          name: WarnX Security Scan
          no_output_timeout: 2m
          command: |
            [ -z "$WARNX_API_KEY" ] && exit 0
            CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || \
              git log -1 --diff-filter=ACMRT --name-only --pretty=format: 2>/dev/null || echo "")
            PATHS_JSON=$(printf '%s\n' "$CHANGED" | grep -v '^$' | \
              jq -R -s -c 'split("\n")[:-1]' || echo '[]')
            EVENT_ID=$(printf '%s' "${CIRCLE_SHA1:0:8}${CIRCLE_BRANCH}$(echo "$PATHS_JSON" | jq -c 'sort')" | \
              sha256sum | cut -d' ' -f1)
            PAYLOAD=$(jq -n \
              --arg event_id "$EVENT_ID" --arg api_key "$WARNX_API_KEY" \
              --arg url "${DEPLOY_URL:-Not_Provided}" --arg commit "${CIRCLE_SHA1:0:8}" \
              --arg branch "$CIRCLE_BRANCH" --argjson paths "$PATHS_JSON" \
              --argjson sequence "${CIRCLE_BUILD_NUM:-0}" --argjson timestamp "$(date +%s%3N)000" \
              '{event_id:$event_id,api_key:$api_key,target_url:$url,commit_id:$commit,
                branch:$branch,paths:$paths,sequence:$sequence,timestamp:$timestamp}')
            curl -s -X POST "https://warnx.ir/api/v1/deploy/ingest" \
              -H "Content-Type: application/json" -d "$PAYLOAD" || true

workflows:
  main:
    jobs:
      - build-and-deploy
      - warnx-scan:
          requires: [ build-and-deploy ]

۵.۶ Azure DevOps

azure-pipelines.yml
trigger:
  branches:
    include: [ main, master, production ]

pool:
  vmImage: 'ubuntu-latest'

stages:
  - stage: WarnXScan
    dependsOn: BuildAndDeploy
    condition: succeeded()
    jobs:
      - job: Scan
        continueOnError: true
        steps:
          - checkout: self
            fetchDepth: 0
          - bash: |
              [ -z "$WARNX_API_KEY" ] && exit 0
              CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
              PATHS_JSON=$(printf '%s\n' "$CHANGED" | grep -v '^$' | \
                jq -R -s -c 'split("\n")[:-1]' || echo '[]')
              BRANCH="$(Build.SourceBranchName)"
              COMMIT_ID="$(Build.SourceVersion)"
              COMMIT_ID="${COMMIT_ID:0:8}"
              EVENT_ID=$(printf '%s' "${COMMIT_ID}${BRANCH}$(echo "$PATHS_JSON" | jq -c 'sort')" | \
                sha256sum | cut -d' ' -f1)
              PAYLOAD=$(jq -n \
                --arg event_id "$EVENT_ID" --arg api_key "$WARNX_API_KEY" \
                --arg url "${DEPLOY_URL:-Not_Provided}" --arg commit "$COMMIT_ID" \
                --arg branch "$BRANCH" --argjson paths "$PATHS_JSON" \
                --argjson sequence "$(Build.BuildId)" --argjson timestamp "$(date +%s%3N)000" \
                '{event_id:$event_id,api_key:$api_key,target_url:$url,commit_id:$commit,
                  branch:$branch,paths:$paths,sequence:$sequence,timestamp:$timestamp}')
              curl -s -X POST "https://warnx.ir/api/v1/deploy/ingest" \
                -H "Content-Type: application/json" -d "$PAYLOAD" || true
            displayName: 'WarnX Sync'
            env:
              WARNX_API_KEY: $(WARNX_API_KEY)
              DEPLOY_URL:    $(DEPLOY_URL)

۵.۷ Docker-based CLI

ساده‌ترین روش — استفاده از image رسمی WarnX CLI:

warnx-security-scan:
  stage: .post
  image: warnx/cli:1.0.0
  script: [ warnx-sync ]
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
  allow_failure: true
- name: WarnX Scan
  uses: docker://warnx/cli:1.0.0
  env:
    WARNX_API_KEY: ${{ secrets.WARNX_API_KEY }}
    DEPLOY_URL: ${{ vars.DEPLOY_URL }}
stage('WarnX') {
  agent { docker { image 'warnx/cli:1.0.0' } }
  environment { WARNX_API_KEY = credentials('warnx-api-key') }
  steps { sh 'warnx-sync' }
}

۵.۸ ارسال Diff برای تحلیل دقیق‌تر (اختیاری)

با ارسال diff_b64، موتور ML فعال می‌شود و کد تغییریافته به‌صورت semantic تحلیل می‌شود. حداکثر اندازه diff: ۲ مگابایت.

bash
# دریافت diff
DIFF_CONTENT=$(git diff "$BEFORE_SHA" "$AFTER_SHA" 2>/dev/null || \
               git diff HEAD~1 HEAD 2>/dev/null || echo "")

# بررسی حجم (حداکثر 2MB)
DIFF_SIZE=$(printf '%s' "$DIFF_CONTENT" | wc -c)
if [ "$DIFF_SIZE" -lt 2097152 ]; then
  DIFF_B64=$(printf '%s' "$DIFF_CONTENT" | base64 -w 0 2>/dev/null || \
             printf '%s' "$DIFF_CONTENT" | base64 2>/dev/null || echo "")
fi

# اضافه کردن به payload
if [ -n "$DIFF_B64" ]; then
  PAYLOAD=$(echo "$PAYLOAD" | jq --arg diff "$DIFF_B64" '. + {diff_b64: $diff}')
fi

۶.۱ Node.js / TypeScript

warnx.ts
import * as crypto from 'crypto';
import * as https  from 'https';
import { execSync } from 'child_process';

interface WarnxConfig {
  apiKey:    string;
  targetUrl: string;
}

function getChangedPaths(before?: string, after?: string): string[] {
  try {
    const cmd = before && before !== '0'.repeat(40) && after
      ? `git diff --name-only ${before} ${after}`
      : 'git log -1 --diff-filter=ACMRT --name-only --pretty=format:';
    return execSync(cmd, { encoding: 'utf8', stdio: ['pipe','pipe','ignore'] })
      .split('\n').filter(Boolean);
  } catch { return []; }
}

function buildEventId(commitId: string, branch: string, paths: string[]): string {
  const raw = `${commitId}${branch}${JSON.stringify([...paths].sort())}`;
  return crypto.createHash('sha256').update(raw).digest('hex');
}

export async function notifyWarnx(config: WarnxConfig, options?: {
  before?: string; after?: string; branch?: string;
  sequence?: number; includeDiff?: boolean;
}): Promise {
  const commitId = (process.env.COMMIT_SHA ?? '').slice(0, 8) || 'unknown';
  const branch   = options?.branch ?? process.env.BRANCH_NAME ?? 'unknown';
  const paths    = getChangedPaths(options?.before, options?.after);
  const eventId  = buildEventId(commitId, branch, paths);

  const payload: Record<string, unknown> = {
    event_id: eventId, api_key: config.apiKey,
    target_url: config.targetUrl, commit_id: commitId,
    branch, paths, sequence: options?.sequence ?? 0,
    timestamp: Date.now(),
  };

  if (options?.includeDiff) {
    try {
      const diff = execSync('git diff HEAD~1 HEAD', { encoding: 'utf8' });
      if (diff.length < 2 * 1024 * 1024)
        payload.diff_b64 = Buffer.from(diff).toString('base64');
    } catch { /* ignore */ }
  }

  const body = JSON.stringify(payload);
  const sig  = crypto.createHmac('sha256', config.apiKey).update(body).digest('hex');
  // ... HTTP send with retry (see full example in docs)
}
استفاده
await notifyWarnx({
  apiKey:    process.env.WARNX_API_KEY!,
  targetUrl: process.env.DEPLOY_URL ?? 'https://api.example.com',
}, {
  before:      process.env.GIT_BEFORE_SHA,
  after:       process.env.GIT_AFTER_SHA,
  branch:      process.env.BRANCH,
  includeDiff: true,
});

۶.۲ Python

warnx.py
import os, json, hmac, hashlib, base64, subprocess, urllib.request, time
from typing import Optional

WARNX_ENDPOINT = "https://warnx.ir/api/v1/deploy/ingest"

def get_changed_paths(before=None, after=None):
    null_sha = "0" * 40
    cmd = (["git", "diff", "--name-only", before, after]
           if before and before != null_sha and after
           else ["git", "log", "-1", "--diff-filter=ACMRT", "--name-only", "--pretty=format:"])
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
    return [p for p in result.stdout.strip().split("\n") if p]

def build_event_id(commit_id, branch, paths):
    raw = f"{commit_id}{branch}{json.dumps(sorted(paths))}"
    return hashlib.sha256(raw.encode()).hexdigest()

def notify_warnx(api_key, target_url, before_sha=None, after_sha=None,
                 branch=None, sequence=0, include_diff=False):
    commit_id = (os.environ.get("GIT_COMMIT", "") or "")[:8] or "unknown"
    branch    = branch or os.environ.get("BRANCH_NAME", "unknown")
    paths     = get_changed_paths(before_sha, after_sha)
    event_id  = build_event_id(commit_id, branch, paths)

    payload = {
        "event_id": event_id, "api_key": api_key,
        "target_url": target_url, "commit_id": commit_id,
        "branch": branch, "paths": paths,
        "sequence": sequence, "timestamp": int(time.time() * 1000),
    }

    if include_diff:
        r = subprocess.run(["git", "diff", "HEAD~1", "HEAD"], capture_output=True, timeout=15)
        if len(r.stdout) < 2 * 1024 * 1024:
            payload["diff_b64"] = base64.b64encode(r.stdout).decode()

    body = json.dumps(payload, ensure_ascii=False).encode()
    sig  = hmac.new(api_key.encode(), body.decode().encode(), hashlib.sha256).hexdigest()

    req = urllib.request.Request(WARNX_ENDPOINT, data=body, method="POST",
        headers={"Content-Type": "application/json", "X-WarnX-Signature": f"sha256={sig}"})
    urllib.request.urlopen(req, timeout=15)

۶.۳ Go

warnx/client.go
package warnx

import (
    "bytes"; "crypto/hmac"; "crypto/sha256"; "encoding/base64"
    "encoding/hex"; "encoding/json"; "fmt"; "net/http"
    "os/exec"; "sort"; "strings"; "time"
)

type Payload struct {
    EventID   string   `json:"event_id"`
    APIKey    string   `json:"api_key"`
    TargetURL string   `json:"target_url"`
    CommitID  string   `json:"commit_id"`
    Branch    string   `json:"branch"`
    Paths     []string `json:"paths"`
    Sequence  int64    `json:"sequence"`
    Timestamp int64    `json:"timestamp"`
    DiffB64   string   `json:"diff_b64,omitempty"`
}

func Notify(apiKey, targetURL, before, after, branch string, seq int64, diff bool) error {
    commit, _ := exec.Command("git", "rev-parse", "--short=8", "HEAD").Output()
    commitID  := strings.TrimSpace(string(commit))
    paths     := getChangedPaths(before, after)
    eventID   := buildEventID(commitID, branch, paths)

    p := Payload{
        EventID: eventID, APIKey: apiKey, TargetURL: targetURL,
        CommitID: commitID, Branch: branch, Paths: paths,
        Sequence: seq, Timestamp: time.Now().UnixMilli(),
    }
    if diff {
        out, err := exec.Command("git", "diff", "HEAD~1", "HEAD").Output()
        if err == nil && len(out) < 2*1024*1024 {
            p.DiffB64 = base64.StdEncoding.EncodeToString(out)
        }
    }
    body, _ := json.Marshal(p)
    mac := hmac.New(sha256.New, []byte(apiKey))
    mac.Write(body)
    sig := hex.EncodeToString(mac.Sum(nil))

    req, _ := http.NewRequest("POST", "https://warnx.ir/api/v1/deploy/ingest", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-WarnX-Signature", "sha256="+sig)

    client := &http.Client{Timeout: 15 * time.Second}
    resp, err := client.Do(req)
    if err != nil { return err }
    defer resp.Body.Close()
    fmt.Printf("[WarnX] status=%d event=%s\n", resp.StatusCode, eventID)
    return nil
}

۶.۴ Java (Spring Boot)

WarnxDeployNotifier.java
@Service
public class WarnxDeployNotifier {

    private static final String ENDPOINT = "https://warnx.ir/api/v1/deploy/ingest";
    private final HttpClient httpClient = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5)).build();

    @Value("${warnx.api-key:}")  private String apiKey;
    @Value("${warnx.target-url:Not_Provided}") private String targetUrl;

    public void notifyDeploy(String commitId, String branch, List<String> paths) throws Exception {
        var eventId = buildEventId(commitId, branch, paths);
        var payload = Map.of(
            "event_id", eventId, "api_key", apiKey,
            "target_url", targetUrl, "commit_id", commitId.substring(0, 8),
            "branch", branch, "paths", paths,
            "sequence", 0L, "timestamp", Instant.now().toEpochMilli()
        );
        var body = new ObjectMapper().writeValueAsString(payload);
        var sig  = hmacSha256(body, apiKey);

        var req = HttpRequest.newBuilder()
            .uri(URI.create(ENDPOINT))
            .header("Content-Type", "application/json")
            .header("X-WarnX-Signature", "sha256=" + sig)
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .timeout(Duration.ofSeconds(15))
            .build();
        httpClient.send(req, HttpResponse.BodyHandlers.ofString());
    }

    private String buildEventId(String commit, String branch, List<String> paths) throws Exception {
        var sorted = new ArrayList<>(paths); Collections.sort(sorted);
        var raw    = commit + branch + new ObjectMapper().writeValueAsString(sorted);
        return Hex.encodeHexString(MessageDigest.getInstance("SHA-256")
            .digest(raw.getBytes(StandardCharsets.UTF_8)));
    }
}

۶.۵ PHP (Laravel)

app/Services/WarnxNotifier.php
<?php
namespace App\Services;

use GuzzleHttp\Client;

class WarnxNotifier
{
    private const ENDPOINT = 'https://warnx.ir/api/v1/deploy/ingest';

    public function __construct(
        private string $apiKey    = '',
        private string $targetUrl = 'Not_Provided'
    ) {}

    public function notifyDeploy(string $commitId, string $branch, array $paths = []): bool
    {
        $eventId = $this->buildEventId($commitId, $branch, $paths);
        $payload = json_encode([
            'event_id'   => $eventId,
            'api_key'    => $this->apiKey,
            'target_url' => $this->targetUrl,
            'commit_id'  => substr($commitId, 0, 8),
            'branch'     => $branch,
            'paths'      => $paths,
            'sequence'   => 0,
            'timestamp'  => (int)(microtime(true) * 1000),
        ], JSON_UNESCAPED_UNICODE);

        $sig = hash_hmac('sha256', $payload, $this->apiKey);

        $client = new Client(['timeout' => 15]);
        $res = $client->post(self::ENDPOINT, [
            'body'    => $payload,
            'headers' => [
                'Content-Type'      => 'application/json',
                'X-WarnX-Signature' => "sha256={$sig}",
            ],
        ]);
        return $res->getStatusCode() === 200;
    }

    private function buildEventId(string $commit, string $branch, array $paths): string
    {
        $sorted = $paths; sort($sorted);
        return hash('sha256', $commit . $branch . json_encode($sorted));
    }
}

۶.۶ .NET / C#

WarnxNotifier.cs
public class WarnxNotifier
{
    private const string Endpoint = "https://warnx.ir/api/v1/deploy/ingest";
    private readonly HttpClient _client = new() { Timeout = TimeSpan.FromSeconds(15) };

    public async Task<bool> NotifyDeployAsync(string commitId, string branch, IEnumerable<string> paths)
    {
        var list    = paths.ToList();
        var eventId = BuildEventId(commitId, branch, list);
        var payload = JsonSerializer.Serialize(new {
            event_id = eventId, api_key = _apiKey,
            target_url = _targetUrl, commit_id = commitId[..Math.Min(8, commitId.Length)],
            branch, paths = list, sequence = 0,
            timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
        });
        var sig = HmacSha256(payload, _apiKey);

        using var req = new HttpRequestMessage(HttpMethod.Post, Endpoint)
            { Content = new StringContent(payload, Encoding.UTF8, "application/json") };
        req.Headers.Add("X-WarnX-Signature", $"sha256={sig}");

        var resp = await _client.SendAsync(req);
        return resp.IsSuccessStatusCode;
    }

    private string BuildEventId(string c, string b, List<string> paths) {
        var sorted = paths.OrderBy(p => p).ToList();
        var raw    = Encoding.UTF8.GetBytes(c + b + JsonSerializer.Serialize(sorted));
        return Convert.ToHexString(SHA256.HashData(raw)).ToLower();
    }
}

۶.۷ Ruby / Rails

lib/warnx/notifier.rb
require 'net/http'; require 'json'; require 'openssl'; require 'digest'; require 'base64'

module Warnx
  ENDPOINT = 'https://warnx.ir/api/v1/deploy/ingest'

  def self.notify(api_key:, target_url:, commit_id: nil, branch: nil,
                  before_sha: nil, after_sha: nil, include_diff: false)
    commit_id ||= `git rev-parse --short=8 HEAD`.strip
    branch    ||= `git rev-parse --abbrev-ref HEAD`.strip
    paths       = get_changed_paths(before: before_sha, after: after_sha)
    event_id    = build_event_id(commit_id, branch, paths)

    payload = { event_id: event_id, api_key: api_key, target_url: target_url,
                commit_id: commit_id[0, 8], branch: branch, paths: paths,
                sequence: 0, timestamp: (Time.now.to_f * 1000).to_i }

    if include_diff
      diff = `git diff HEAD~1 HEAD 2>/dev/null`
      payload[:diff_b64] = Base64.strict_encode64(diff) if diff.bytesize < 2 * 1024 * 1024
    end

    body = JSON.generate(payload)
    sig  = OpenSSL::HMAC.hexdigest('SHA256', api_key, body)

    uri  = URI(ENDPOINT)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    req  = Net::HTTP::Post.new(uri)
    req['Content-Type']      = 'application/json'
    req['X-WarnX-Signature'] = "sha256=#{sig}"
    req.body = body
    http.request(req)
  end

  def self.build_event_id(c, b, paths)
    Digest::SHA256.hexdigest("#{c}#{b}#{JSON.generate(paths.sort)}")
  end
end

۷. بررسی وضعیت اسکن (Polling)

GET /api/deployments/status/{deployment_id}?api_key=<KEY>
bash
POLL_URL=$(echo "$RESPONSE" | jq -r '.poll_url')

for i in $(seq 1 30); do
  RESULT=$(curl -s "$POLL_URL")
  STATUS=$(echo "$RESULT" | jq -r '.status')

  if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
    RISK=$(echo "$RESULT" | jq -r '.risk_level')
    ACTION=$(echo "$RESULT" | jq -r '.action')
    echo "Scan complete: risk=$RISK action=$ACTION"
    break
  fi

  sleep 10
done
async function waitForScan(
  deploymentId: number, apiKey: string, maxWaitMs = 300_000
): Promise<ScanResult> {
  const start   = Date.now();
  const pollUrl = `https://warnx.ir/api/deployments/status/${deploymentId}?api_key=${apiKey}`;

  while (Date.now() - start < maxWaitMs) {
    const res  = await fetch(pollUrl);
    const data = await res.json();
    if (data.status === 'completed' || data.status === 'failed') return data;
    await new Promise(r => setTimeout(r, 10_000));
  }
  throw new Error('Scan timeout');
}

۸. ساختار پاسخ API

{
  "success":             true,
  "deployment_id":       1234,
  "status":             "pending",
  "estimated_scan_time": "2-5 دقیقه",
  "poll_url":           "https://warnx.ir/api/deployments/status/1234?api_key=xxx",
  "view_url":           "https://panel.warnx.ir/deployments/view/1234",
  "diff_received":      true,
  "diff_size_bytes":     15420
}
{
  "success":         true,
  "deployment_id":   1234,
  "status":         "completed",
  "risk_level":     "HIGH",
  "score":          78,
  "action":         "auto_pentest",
  "finding_count":  3,
  "analysis_method":"semantic_ml",
  "decision_reasons": {
    "summary":    "تغییر در مسیرهای پرداخت با امتیاز ۷۸/۹۹",
    "action":     "auto_pentest",
    "triggered_by": ["payment_logic", "auth_logic"]
  }
}

مقادیر risk_level

HIGHریسک بالا — معمولاً منجر به pentest می‌شود
MEDIUMریسک متوسط — نیاز به بررسی
LOWریسک پایین — تغییرات ایمن

مقادیر action

auto_pentestپنتست انسانی اجباری اجرا می‌شود
suggest_pentestپنتست پیشنهادی
no_actionنیازی به اقدام نیست

۹. کدهای خطا

کد HTTPتوضیحاقدام
200 موفق — deploy ثبت شد پیگیری با poll_url
400 پارامترهای ناقص بررسی api_key و target_url
401 API Key نامعتبر کلید API را بررسی کنید
402 موجودی کیف پول ناکافی از پنل شارژ کنید
409 رویداد تکراری نرمال است — deploy قبلاً ثبت شده
413 diff بیش از ۲MB diff را کوچک‌تر کنید یا حذف کنید
429 Too Many Requests Retry با exponential backoff
5xx خطای سرور Retry خودکار — در صف محلی ذخیره می‌شود

۱۰. بهترین روش‌ها و امنیت

نگهداری API Key

bash
# ❌ اشتباه — commit در کد
WARNX_API_KEY="warnx_xxxx"

# ✅ درست — از environment variables
export WARNX_API_KEY="warnx_xxxx"

# ✅ درست — از secret manager
WARNX_API_KEY=$(aws secretsmanager get-secret-value \
  --secret-id warnx-key --query SecretString --output text)

تنظیم Non-blocking

yaml / groovy
# GitLab
allow_failure: true

# GitHub Actions
continue-on-error: true

# Jenkins — در post { success } نه stages

زمان ارسال

ارسال را بعد از deploy موفق انجام دهید، نه قبل. فقط روی branch های مهم: main، production، hotfix/* و tag های نسخه.

۱۱. سؤالات متداول

آیا WarnX می‌تواند deploy را متوقف کند؟

خیر. تمام تمپلیت‌ها با allow_failure: true یا continue-on-error: true پیکربندی شده‌اند. WarnX یک observer است، نه gate.

اگر سرور WarnX در دسترس نباشد چه می‌شود؟

رویداد در فایل /tmp/warnx-queue.jsonl ذخیره می‌شود. اسکریپت با ۳ بار retry و exponential backoff تلاش می‌کند. در صورت شکست همه retryها، pipeline فقط در branch production با exit code 1 متوقف می‌شود.

آیا ارسال diff اجباری است؟

خیر. بدون diff، scanner از مسیرهای فایل‌های تغییریافته استفاده می‌کند. با diff، موتور ML فعال می‌شود و دقت بالاتری دارد.

آیا می‌توانم چند پروژه داشته باشم؟

بله. هر پروژه API Key مجزا دارد. می‌توانید برای Backend، Frontend و Mobile API سه پروژه جداگانه بسازید.

event_id چیست و چرا مهم است؟

یک شناسه یکتا بر اساس sha256(commit_id + branch + sorted_paths) است. از ثبت مضاعف جلوگیری می‌کند. اگر pipeline دوبار اجرا شود، WarnX HTTP 409 برمی‌گرداند.

آیا داده‌های کد من روی سرور WarnX ذخیره می‌شود؟

فقط متادیتا (مسیر فایل‌ها، commit ID، branch) و diff در صورت ارسال. محتوای کامل مخزن هرگز ارسال نمی‌شود.

چقدر طول می‌کشد تا نتیجه اسکن آماده شود؟

معمولاً ۲ تا ۵ دقیقه. با diff و ML scorer فعال، ممکن است تا ۱۰ دقیقه طول بکشد.