یکپارچهسازی Deployment Analyzer
راهنمای کامل اتصال WarnX به pipeline های CI/CD برای اسکن خودکار امنیتی هر بار که کد جدیدی deploy میشود. پشتیبانی از GitLab، GitHub، Jenkins، Bitbucket، CircleCI، Azure DevOps و ۷ زبان برنامهنویسی.
۱. معرفی و نحوه کارکرد
WarnX Deployment Analyzer هر بار که کد جدیدی به محیط Production یا Staging deploy میشود، بهصورت خودکار یک اسکن امنیتی انجام میدهد.
مراحل اسکن
بررسی مسیرهای تغییریافته با ۱۵+ دسته الگوی امنیتی (payment، auth، file upload، ...)
تحلیل semantic کد در صورت ارسال diff_b64 — دقت بالاتر
تطبیق با پروفایل امنیتی اختصاصی پروژه — کاهش false positive
تضمینهای سیستم
ارسال مجدد با event_id یکسان → 409 بدون ثبت مضاعف
اسکن هرگز pipeline را متوقف نمیکند
در صورت قطع ارتباط، رویداد در queue محلی ذخیره میشود
۲. پیشنیازها
| ابزار | نسخه | هدف |
|---|---|---|
curl | ≥ 7.55 | ارسال درخواست HTTP |
jq | ≥ 1.6 | پردازش JSON در shell |
openssl | ≥ 1.1 | امضای HMAC |
git | ≥ 2.x | دریافت فایلهای تغییریافته |
sha256sum | هر نسخه | ساخت event_id یکتا |
apk add --no-cache git jq curl openssl coreutils
apt-get install -y jq curl openssl coreutils git
۳. ساخت پروژه و دریافت API Key
- وارد پنل WarnX شوید: panel.warnx.ir
- از منوی Deployments روی + اتصال پروژه جدید کلیک کنید
- نام پروژه را وارد کنید (مثال:
Backend API - Production) - یک API Key اختصاصی برای این پروژه ایجاد میشود
- این کلید را در متغیر محیطی CI/CD با نام
WARNX_API_KEYذخیره کنید
۴. فرمت درخواست API
https://warnx.ir/api/v1/deploy/ingest
{
"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 نمیکند.
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
WARNX_API_KEYکلید API پروژه — Masked ✅ Protected ✅CI_ENVIRONMENT_URLآدرس محیط deploy (مثال: https://api.yourapp.com)۵.۲ GitHub Actions
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
WARNX_API_KEYSecret — کلید API پروژهDEPLOY_URLVariable — آدرس محیط (https://api.yourapp.com)۵.۳ Jenkins
در بلوک post { success { ... } } اضافه کنید تا فقط پس از deploy موفق اجرا شود.
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
warnx-api-keyKind: Secret text — مقدار: کلید API۵.۴ Bitbucket Pipelines
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
تنظیم متغیرها
WARNX_API_KEYSecured ✅DEPLOY_URLآدرس محیط production۵.۵ CircleCI
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
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: ۲ مگابایت.
# دریافت 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
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
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
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)
@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)
<?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#
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
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)
/api/deployments/status/{deployment_id}?api_key=<KEY>
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
مقادیر 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
# ❌ اشتباه — 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
# GitLab
allow_failure: true
# GitHub Actions
continue-on-error: true
# Jenkins — در post { success } نه stages
زمان ارسال
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 فعال، ممکن است تا ۱۰ دقیقه طول بکشد.