diff --git a/.github/workflows/cycode-release-gate.yml b/.github/workflows/cycode-release-gate.yml
new file mode 100644
index 0000000..bd7349b
--- /dev/null
+++ b/.github/workflows/cycode-release-gate.yml
@@ -0,0 +1,52 @@
+name: "Cycode Release Security Gate"
+
+on:
+ push:
+ branches:
+ - "release/**"
+ workflow_dispatch:
+
+jobs:
+ security-gate:
+ name: "Security Gate — SAST, SCA, Secrets"
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Set up Python
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ with:
+ python-version: "3.12"
+
+ - name: Install Cycode CLI
+ run: pip install cycode
+
+ - name: SAST scan
+ env:
+ CYCODE_CLIENT_ID: ${{ secrets.CYCODE_CLIENT_ID }}
+ CYCODE_CLIENT_SECRET: ${{ secrets.CYCODE_CLIENT_SECRET }}
+ run: cycode scan --severity-threshold HIGH -t sast path ./
+
+ - name: SCA scan
+ env:
+ CYCODE_CLIENT_ID: ${{ secrets.CYCODE_CLIENT_ID }}
+ CYCODE_CLIENT_SECRET: ${{ secrets.CYCODE_CLIENT_SECRET }}
+ run: cycode scan --severity-threshold HIGH -t sca path ./
+
+ - name: Secrets scan
+ env:
+ CYCODE_CLIENT_ID: ${{ secrets.CYCODE_CLIENT_ID }}
+ CYCODE_CLIENT_SECRET: ${{ secrets.CYCODE_CLIENT_SECRET }}
+ run: cycode scan --severity-threshold HIGH -t secret path ./
+
+ build:
+ name: "Build & Package"
+ needs: security-gate
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Build
+ run: echo "Build, compile, package, test — your build steps here"
diff --git a/.github/workflows/cycode-sast-pr-scan.yml b/.github/workflows/cycode-sast-pr-scan.yml
new file mode 100644
index 0000000..2255ac7
--- /dev/null
+++ b/.github/workflows/cycode-sast-pr-scan.yml
@@ -0,0 +1,40 @@
+name: "Cycode SAST PR Scan (Delta)"
+
+on:
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+jobs:
+ cycode-sast-delta:
+ name: "SAST Delta Scan — PR Changes Only"
+ if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'workflow_dispatch'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ with:
+ python-version: "3.12"
+
+ - name: Install Cycode CLI
+ run: pip install cycode
+
+ - name: Run Cycode SAST delta scan
+ env:
+ CYCODE_CLIENT_ID: ${{ secrets.CYCODE_CLIENT_ID }}
+ CYCODE_CLIENT_SECRET: ${{ secrets.CYCODE_CLIENT_SECRET }}
+ run: |
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
+ BASE=${{ github.event.pull_request.base.sha }}
+ HEAD=${{ github.event.pull_request.head.sha }}
+ echo "Scanning PR delta: ${BASE}..${HEAD}"
+ cycode scan -t sast commit-history -r "${BASE}..${HEAD}" .
+ else
+ echo "Scanning last commit: HEAD~1"
+ cycode scan -t sast commit-history -r HEAD~1 .
+ fi
diff --git a/.github/workflows/cycode-sast-scan.yml b/.github/workflows/cycode-sast-scan.yml
new file mode 100644
index 0000000..0e28b7c
--- /dev/null
+++ b/.github/workflows/cycode-sast-scan.yml
@@ -0,0 +1,28 @@
+name: "Cycode SAST Scan"
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+jobs:
+ cycode-sast:
+ name: "SAST Security Scan"
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Set up Python
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ with:
+ python-version: "3.12"
+
+ - name: Install Cycode CLI
+ run: pip install cycode
+
+ - name: Run Cycode SAST scan
+ env:
+ CYCODE_CLIENT_ID: ${{ secrets.CYCODE_CLIENT_ID }}
+ CYCODE_CLIENT_SECRET: ${{ secrets.CYCODE_CLIENT_SECRET }}
+ run: cycode scan -t sast path ./vulnerable_apps/
diff --git a/README.md b/README.md
index e98859b..64e8829 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ Examples of insecure Docker configurations and container practices:
- **Dockerfile.secrets-exposed** - Hardcoded secrets and credentials
- **Dockerfile.rootful-privileged** - Privileged containers running as root
- **Dockerfile.multistage-bad** - Insecure multi-stage builds
+- **Dockerfile.n8n-vulnerable** - n8n with CVE-2026-21858 (CVSS 10.0)
- **docker-compose.vulnerable.yml** - Insecure Docker Compose configuration
### 🏗️ Vulnerable Terraform (`vulnerable_terraform/`)
@@ -60,6 +61,12 @@ Infrastructure-as-Code examples with security misconfigurations:
- **aws_iam_vulnerable.tf** - Overly permissive IAM policies and roles
- **aws_misc_vulnerable.tf** - Additional AWS security issues
+### 📦 Vulnerable Packages (`vulnerable_packages/`)
+
+Examples of applications using vulnerable open-source dependencies for SCA testing:
+
+- **n8n-workflow/** - Workflow automation with n8n v1.100.0 (CVE-2026-21858, CVSS 10.0)
+
### 🌐 Vulnerable Web Applications (`vulnerable_apps/`)
Python web application examples demonstrating **OWASP Top 10 (2021)** vulnerabilities:
@@ -76,6 +83,30 @@ Python web application examples demonstrating **OWASP Top 10 (2021)** vulnerabil
- Hardcoded Secrets
+## 🔧 Cycode CI/CD Integration Examples
+
+Working examples of Cycode as a CI/CD gate in Azure Pipelines and GitHub Actions.
+
+### GitHub Actions (`.github/workflows/`)
+- `cycode-sast-scan.yml` — full SAST scan on push to main
+- `cycode-sast-pr-scan.yml` — delta scan on pull requests
+- `cycode-release-gate.yml` — release-stage gate (SAST + SCA + Secrets)
+
+### Azure Pipelines
+
+| File | Pattern |
+|------|---------|
+| `azure-pipelines.yml` | **CLI gate** — full scan on push, delta on PR (original pattern) |
+| `azure-pipelines-api-gate.yml` | **API gate** — query Cycode RIG for Open violations in the repo |
+| `azure-pipelines-publish-results.yml` | **Surface results in Azure UI** — Tests tab + custom summary tab + downloadable artifact |
+| `azure-pipelines-template-consumer.yml` | **Centralized template** — consumer that `extends: templates/cycode-scan.yml` |
+| `templates/cycode-scan.yml` | Master template (scan + publish + gate) consumed by many pipelines |
+| `scripts/cycode-gate.sh` | Standalone API-gate script (works from any shell) |
+| `scripts/cycode-json-to-junit.py` | Converts `cycode -o json` output to JUnit XML |
+| `scripts/cycode-summary.py` | Generates a short Markdown summary for the build summary tab |
+
+Secrets needed in Azure DevOps (Pipelines → Library or pipeline variables with lock icon): `CYCODE_CLIENT_ID`, `CYCODE_CLIENT_SECRET`.
+
## 🚀 Getting Started
Each directory contains its own README with specific vulnerability descriptions.
diff --git a/azure-pipelines-api-gate.yml b/azure-pipelines-api-gate.yml
new file mode 100644
index 0000000..0d4dd47
--- /dev/null
+++ b/azure-pipelines-api-gate.yml
@@ -0,0 +1,40 @@
+# Pattern 2 — Cycode API gate (standalone example).
+#
+# Queries Cycode's Risk Intelligence Graph for Open violations in this repo.
+# Fails the build if any match the filters below.
+#
+# Prereqs (one-time in Azure DevOps):
+# - Secret pipeline variables CYCODE_CLIENT_ID and CYCODE_CLIENT_SECRET
+# - Agent pool 'Default' (self-hosted) or change to 'ubuntu-latest'
+#
+# Run manually: Pipelines → this pipeline → Run
+trigger: none
+pr: none
+
+pool:
+ name: Default
+
+variables:
+ # Must match the repo name shown in Cycode's Violations UI.
+ REPO_NAME: "AppSecHQ/vectorvictor"
+
+steps:
+ - checkout: self
+
+ - script: |
+ if ! command -v jq >/dev/null 2>&1; then
+ sudo apt-get update -qq && sudo apt-get install -y -qq jq || true
+ fi
+ jq --version
+ displayName: "Ensure jq is available"
+
+ - script: bash scripts/cycode-gate.sh
+ displayName: "Cycode API gate — fail on Open violations"
+ env:
+ CYCODE_CLIENT_ID: $(CYCODE_CLIENT_ID)
+ CYCODE_CLIENT_SECRET: $(CYCODE_CLIENT_SECRET)
+ REPO_NAME: $(REPO_NAME)
+ # Optional tuning (uncomment to enable):
+ # SEVERITY_MIN: "High" # only High + Critical fail the build
+ # CATEGORY: "SAST" # scope to one scan type
+ # RISK_SCORE_MIN: "70" # ignore findings below risk score 70
diff --git a/azure-pipelines-publish-results.yml b/azure-pipelines-publish-results.yml
new file mode 100644
index 0000000..56904da
--- /dev/null
+++ b/azure-pipelines-publish-results.yml
@@ -0,0 +1,81 @@
+# Pattern 3 — Publish Cycode results to the Azure Pipelines UI.
+#
+# Populates three surfaces from a single Cycode scan:
+# - Tests tab (each finding = failed test, via JUnit)
+# - Custom summary (Markdown report as a tab on the build summary page)
+# - Artifact (raw cycode-results.json for download)
+#
+# After the results are published, a final CLI gate step enforces pass/fail.
+# Swap the gate to `bash scripts/cycode-gate.sh` to gate on the platform API
+# instead (see azure-pipelines-api-gate.yml).
+trigger: none
+pr: none
+
+pool:
+ name: Default
+
+variables:
+ SCAN_PATH: "./vulnerable_apps/"
+
+steps:
+ - checkout: self
+ fetchDepth: 0
+
+ - task: UsePythonVersion@0
+ displayName: "Use Python 3.12"
+ inputs:
+ versionSpec: "3.12"
+
+ - script: |
+ set -e
+ python3 -m pip install --upgrade pip
+ pip install cycode
+ displayName: "Install Cycode CLI"
+
+ # ---- Scan with --soft-fail so we can publish results first ------------
+ - script: |
+ set +e
+ cycode -o json scan --soft-fail -t sast path $(SCAN_PATH) > cycode-results.json
+ echo "scan exit: $?"
+ ls -la cycode-results.json
+ displayName: "Cycode SAST scan (JSON)"
+ env:
+ CYCODE_CLIENT_ID: $(CYCODE_CLIENT_ID)
+ CYCODE_CLIENT_SECRET: $(CYCODE_CLIENT_SECRET)
+
+ # ---- Surface 1: Tests tab --------------------------------------------
+ - script: python3 scripts/cycode-json-to-junit.py cycode-results.json cycode-junit.xml
+ displayName: "Convert JSON → JUnit"
+ condition: succeededOrFailed()
+
+ - task: PublishTestResults@2
+ displayName: 'Publish to "Tests" tab'
+ condition: succeededOrFailed()
+ inputs:
+ testResultsFormat: "JUnit"
+ testResultsFiles: "cycode-junit.xml"
+ testRunTitle: "Cycode SAST"
+ mergeTestResults: true
+ failTaskOnFailedTests: false
+
+ # ---- Surface 2: Build summary tab ------------------------------------
+ - script: |
+ python3 scripts/cycode-summary.py cycode-results.json > cycode-summary.md
+ echo "##vso[task.uploadsummary]$(System.DefaultWorkingDirectory)/cycode-summary.md"
+ displayName: 'Publish "Cycode Scan Summary" tab'
+ condition: succeededOrFailed()
+
+ # ---- Surface 3: Downloadable artifact --------------------------------
+ - task: PublishBuildArtifacts@1
+ displayName: "Publish raw Cycode JSON"
+ condition: succeededOrFailed()
+ inputs:
+ pathToPublish: "cycode-results.json"
+ artifactName: "cycode-report"
+
+ # ---- Final gate -------------------------------------------------------
+ - script: cycode scan -t sast path $(SCAN_PATH)
+ displayName: "Cycode SAST gate (fails build on findings)"
+ env:
+ CYCODE_CLIENT_ID: $(CYCODE_CLIENT_ID)
+ CYCODE_CLIENT_SECRET: $(CYCODE_CLIENT_SECRET)
diff --git a/azure-pipelines-template-consumer.yml b/azure-pipelines-template-consumer.yml
new file mode 100644
index 0000000..b06ef41
--- /dev/null
+++ b/azure-pipelines-template-consumer.yml
@@ -0,0 +1,33 @@
+# Pattern 4 — Consumer pipeline that extends the centralized Cycode template.
+#
+# This same-repo example demonstrates the consumer shape. In production the
+# template lives in a separate repo owned by the security team; consumers
+# reference it via `resources.repositories` and `extends: ...@security-templates`.
+#
+# Cross-repo version (replace the local `extends` below):
+#
+# resources:
+# repositories:
+# - repository: security-templates
+# type: git
+# name: SecurityTeam/cycode-pipeline-templates
+# ref: refs/tags/v1
+# extends:
+# template: templates/cycode-scan.yml@security-templates
+# parameters: { ... }
+#
+# App teams customize only the parameters — the scan logic, publishing, and
+# gate behavior all live in the template and evolve centrally.
+trigger: none
+pr: none
+
+extends:
+ template: templates/cycode-scan.yml
+ parameters:
+ scanPath: "./vulnerable_apps/"
+ scanTypeFlags: "-t sast"
+ severityThreshold: "high"
+ repoName: "AppSecHQ/vectorvictor"
+ gateMode: "both" # defense in depth: CLI + API
+ publishResults: true
+ poolName: "Default"
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
new file mode 100644
index 0000000..929fd69
--- /dev/null
+++ b/azure-pipelines.yml
@@ -0,0 +1,49 @@
+trigger:
+ branches:
+ include:
+ - main
+
+pr:
+ branches:
+ include:
+ - main
+
+pool:
+ name: Default
+
+steps:
+ - checkout: self
+ fetchDepth: 0
+
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: "3.12"
+ displayName: Set up Python
+
+ - script: pip install cycode
+ displayName: Install Cycode CLI
+
+ # Full scan — runs on push to main
+ - script: cycode scan -t sast path ./vulnerable_apps/
+ displayName: Run SAST full scan
+ condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
+ env:
+ CYCODE_CLIENT_ID: $(CYCODE_CLIENT_ID)
+ CYCODE_CLIENT_SECRET: $(CYCODE_CLIENT_SECRET)
+
+ # Delta scan — runs on pull requests only
+ - bash: |
+ TARGET=$(echo "$(System.PullRequest.TargetBranch)" | sed 's|refs/heads/||')
+ git fetch origin "${TARGET}"
+ BASE=$(git merge-base "$(System.PullRequest.SourceCommitId)" FETCH_HEAD)
+ HEAD="$(System.PullRequest.SourceCommitId)"
+ echo "Target branch : ${TARGET}"
+ echo "Base SHA : ${BASE}"
+ echo "Head SHA : ${HEAD}"
+ echo "Range : ${BASE}..${HEAD}"
+ cycode scan -t sast commit-history -r "${BASE}..${HEAD}" .
+ displayName: Run SAST delta scan
+ condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
+ env:
+ CYCODE_CLIENT_ID: $(CYCODE_CLIENT_ID)
+ CYCODE_CLIENT_SECRET: $(CYCODE_CLIENT_SECRET)
diff --git a/config/api_config.py b/config/api_config.py
new file mode 100644
index 0000000..27acab0
--- /dev/null
+++ b/config/api_config.py
@@ -0,0 +1,11 @@
+"""API configuration for external service integrations."""
+
+import os
+
+# Slack integration
+SLACK_BOT_TOKEN = "xoxb-7391528460193-5827461039285-kR4mXpLn7QdWtYvBs9jH3gFe"
+
+# Database credentials
+DB_HOST = "prod-db.internal.example.com"
+DB_USER = "app_service"
+DB_PASSWORD = "Pr0d_S3cure!P@ssw0rd_2025_xK9m"
diff --git a/scripts/cycode-gate.sh b/scripts/cycode-gate.sh
new file mode 100755
index 0000000..7c0e7d0
--- /dev/null
+++ b/scripts/cycode-gate.sh
@@ -0,0 +1,114 @@
+#!/usr/bin/env bash
+# Cycode API gate — fail the build if Open violations exist for $REPO_NAME.
+#
+# Required env:
+# CYCODE_CLIENT_ID, CYCODE_CLIENT_SECRET
+# REPO_NAME — repo name as shown in Cycode's Violations UI, e.g. "AppSecHQ/vectorvictor"
+#
+# Optional env (any combination):
+# SEVERITY_MIN Critical | High | Medium | Low (inclusive threshold)
+# CATEGORY SAST | SCA | Secrets | IaC | ContainerScanning
+# RISK_SCORE_MIN 0–100
+#
+# Exit codes:
+# 0 no Open violations matching the filters
+# 1 one or more Open violations found → build should fail
+# 2 invalid input
+# 10 auth or API error
+set -euo pipefail
+
+: "${CYCODE_CLIENT_ID:?CYCODE_CLIENT_ID is required}"
+: "${CYCODE_CLIENT_SECRET:?CYCODE_CLIENT_SECRET is required}"
+: "${REPO_NAME:?REPO_NAME is required (e.g. 'AppSecHQ/vectorvictor')}"
+
+SEVERITY_MIN="${SEVERITY_MIN:-}"
+CATEGORY="${CATEGORY:-}"
+RISK_SCORE_MIN="${RISK_SCORE_MIN:-}"
+
+API_BASE="https://api.cycode.com"
+
+echo "Cycode API gate: checking Open violations for ${REPO_NAME}"
+
+# --- Authenticate ---------------------------------------------------------
+AUTH_RESP=$(curl -sS -X POST "${API_BASE}/api/v1/auth/api-token" \
+ -H "Content-Type: application/json" \
+ -d "{\"clientId\":\"${CYCODE_CLIENT_ID}\",\"secret\":\"${CYCODE_CLIENT_SECRET}\"}") || {
+ echo "##vso[task.logissue type=error]Auth request failed"
+ exit 10
+}
+
+TOKEN=$(jq -r '.token // empty' <<<"$AUTH_RESP")
+if [[ -z "$TOKEN" ]]; then
+ echo "##vso[task.logissue type=error]Cycode authentication returned no token"
+ exit 10
+fi
+
+# --- Build filters --------------------------------------------------------
+FILTERS=$(jq -n --arg repo "$REPO_NAME" '
+ [
+ {name:"status", operator:"Eq", value:"Open", type:"String"},
+ {name:"detection_details.repository_name", operator:"Eq", value:$repo, type:"String"}
+ ]')
+
+if [[ -n "$SEVERITY_MIN" ]]; then
+ case "$SEVERITY_MIN" in
+ Critical|critical) SEVS="Critical" ;;
+ High|high) SEVS="Critical,High" ;;
+ Medium|medium) SEVS="Critical,High,Medium" ;;
+ Low|low) SEVS="Critical,High,Medium,Low" ;;
+ *) echo "Invalid SEVERITY_MIN='$SEVERITY_MIN'"; exit 2 ;;
+ esac
+ FILTERS=$(jq --arg v "$SEVS" '. + [{name:"severity", operator:"In", value:$v, type:"String"}]' <<<"$FILTERS")
+fi
+
+if [[ -n "$CATEGORY" ]]; then
+ FILTERS=$(jq --arg v "$CATEGORY" '. + [{name:"category", operator:"Eq", value:$v, type:"String"}]' <<<"$FILTERS")
+fi
+
+if [[ -n "$RISK_SCORE_MIN" ]]; then
+ FILTERS=$(jq --arg v "$RISK_SCORE_MIN" '. + [{name:"risk_score", operator:"Gte", value:$v, type:"Numeric"}]' <<<"$FILTERS")
+fi
+
+BODY=$(jq -n --argjson filters "$FILTERS" '{
+ resource_type: "detection",
+ filters: [{mode:"And", filters: $filters}],
+ sort_by: "risk_score",
+ sort_order: "desc",
+ limit: -1,
+ fast_query: true,
+ connections: [], exists: true, is_optional: false, edge_type: "",
+ variables: [], edge_filters: [], edge_columns: [],
+ parent_resource_type: "", optional_connections_minimum_count: 0
+}')
+
+# --- Query RIG ------------------------------------------------------------
+RESPONSE=$(curl -sS -X POST \
+ "${API_BASE}/graph/api/v1/graph/query?mode=AlertWhen&page_number=0&page_size=200" \
+ -H "Authorization: Bearer ${TOKEN}" \
+ -H "Content-Type: application/json" \
+ -d "$BODY") || {
+ echo "##vso[task.logissue type=error]RIG query failed"
+ exit 10
+}
+
+if ! jq -e '.result' >/dev/null 2>&1 <<<"$RESPONSE"; then
+ echo "##vso[task.logissue type=error]Unexpected API response"
+ head -c 500 <<<"$RESPONSE"; echo
+ exit 10
+fi
+
+COUNT=$(jq '.result | length' <<<"$RESPONSE")
+echo "Open violations matching filters for ${REPO_NAME}: ${COUNT}"
+
+# --- Decision -------------------------------------------------------------
+if (( COUNT > 0 )); then
+ echo
+ echo "Top findings:"
+ jq -r '.result[] | " [\(.severity // "-")] \(.source_policy_name // "-") — \(.detection_details.file_path // .detection_details.package_name // "-") (risk \(.risk_score // "-"))"' \
+ <<<"$RESPONSE" | head -20
+ echo
+ echo "##vso[task.logissue type=error]Cycode gate failed: ${COUNT} Open violation(s) in ${REPO_NAME}"
+ exit 1
+fi
+
+echo "Cycode gate passed: no Open violations matched the filters"
diff --git a/scripts/cycode-json-to-junit.py b/scripts/cycode-json-to-junit.py
new file mode 100755
index 0000000..9ad7ee7
--- /dev/null
+++ b/scripts/cycode-json-to-junit.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+"""Convert Cycode CLI JSON output (cycode -o json scan ...) to JUnit XML.
+
+Azure Pipelines renders JUnit via PublishTestResults@2 on the Tests tab.
+Each Cycode finding becomes a failed testcase so reviewers can drill into
+individual violations by severity and file.
+
+Usage:
+ cycode -o json scan -t sast path ./src > cycode.json
+ cycode-json-to-junit.py cycode.json cycode-junit.xml
+"""
+from __future__ import annotations
+
+import json
+import sys
+from xml.sax.saxutils import escape
+
+
+def extract_detections(data):
+ detections = []
+ if isinstance(data, dict):
+ for block in data.get("scan_results", []) or []:
+ detections.extend(block.get("detections", []) or [])
+ detections.extend(data.get("detections", []) or [])
+ elif isinstance(data, list):
+ detections = list(data)
+ return detections
+
+
+def detection_fields(d):
+ dd = d.get("detection_details") or {}
+ severity = d.get("severity") or "UNKNOWN"
+ path = dd.get("file_path") or d.get("file_path") or "unknown"
+ line = dd.get("line") or d.get("line") or ""
+ policy = (
+ d.get("policy_display_name")
+ or d.get("detection_rule_id")
+ or d.get("policy_id")
+ or "Cycode"
+ )
+ message = d.get("message") or policy
+ return severity, policy, path, line, message
+
+
+def main(src: str, dst: str) -> int:
+ with open(src) as f:
+ data = json.load(f)
+
+ detections = extract_detections(data)
+ total = len(detections)
+
+ out = ['']
+ out.append(
+ f''
+ )
+
+ for d in detections:
+ severity, policy, path, line, message = detection_fields(d)
+ classname = f"{severity}.{policy}"
+ name = f"{path}:{line}" if line else path
+ detail = json.dumps(d, indent=2, default=str)
+ cdata = detail.replace("]]>", "]]]]>")
+ out.append(f' ')
+ out.append(
+ f' '
+ )
+ out.append(" ")
+
+ out.append("")
+
+ with open(dst, "w") as f:
+ f.write("\n".join(out))
+
+ print(f"Wrote {total} findings to {dst}")
+ return 0
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 3:
+ print("Usage: cycode-json-to-junit.py ", file=sys.stderr)
+ sys.exit(2)
+ sys.exit(main(sys.argv[1], sys.argv[2]))
diff --git a/scripts/cycode-summary.py b/scripts/cycode-summary.py
new file mode 100755
index 0000000..8f8c73e
--- /dev/null
+++ b/scripts/cycode-summary.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+"""Generate a short Markdown summary of a Cycode JSON scan result.
+
+The output is used with `##vso[task.uploadsummary]` so the summary appears
+as a tab on the Azure Pipelines build summary page.
+
+Usage:
+ cycode -o json scan -t sast path ./src > cycode.json
+ cycode-summary.py cycode.json > cycode-summary.md
+"""
+from __future__ import annotations
+
+import json
+import sys
+
+
+def extract_detections(data):
+ detections = []
+ if isinstance(data, dict):
+ for block in data.get("scan_results", []) or []:
+ detections.extend(block.get("detections", []) or [])
+ detections.extend(data.get("detections", []) or [])
+ elif isinstance(data, list):
+ detections = list(data)
+ return detections
+
+
+def main(src: str) -> int:
+ with open(src) as f:
+ data = json.load(f)
+
+ detections = extract_detections(data)
+ counts: dict[str, int] = {}
+ for d in detections:
+ sev = d.get("severity") or "Unknown"
+ counts[sev] = counts.get(sev, 0) + 1
+
+ lines = []
+ lines.append("## Cycode Scan Summary")
+ lines.append("")
+ lines.append(f"**Total findings:** {len(detections)}")
+ lines.append("")
+ lines.append("| Severity | Count |")
+ lines.append("|---|---|")
+ for sev in ["Critical", "High", "Medium", "Low", "Info", "Unknown"]:
+ if counts.get(sev):
+ lines.append(f"| {sev} | {counts[sev]} |")
+ lines.append("")
+
+ if detections:
+ lines.append("### Top findings")
+ for d in detections[:10]:
+ dd = d.get("detection_details") or {}
+ path = dd.get("file_path") or d.get("file_path") or "?"
+ line = dd.get("line") or d.get("line") or ""
+ msg = (d.get("message") or d.get("detection_rule_id") or "")[:120]
+ loc = f"`{path}:{line}`" if line else f"`{path}`"
+ lines.append(f"- **[{d.get('severity', '?')}]** {msg} — {loc}")
+
+ print("\n".join(lines))
+ return 0
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("Usage: cycode-summary.py ", file=sys.stderr)
+ sys.exit(2)
+ sys.exit(main(sys.argv[1]))
diff --git a/semgrep-rules/gha-dangerous-pr-target-checkout.yml b/semgrep-rules/gha-dangerous-pr-target-checkout.yml
new file mode 100644
index 0000000..77cdd3f
--- /dev/null
+++ b/semgrep-rules/gha-dangerous-pr-target-checkout.yml
@@ -0,0 +1,97 @@
+rules:
+
+ # ---------------------------------------------------------------
+ # Dangerous pull_request_target + PR code checkout
+ # ---------------------------------------------------------------
+ # Based on the official Semgrep rule:
+ # yaml.github-actions.security.pull-request-target-code-checkout
+ # Source: https://github.com/semgrep/semgrep-rules/blob/develop/yaml/github-actions/security/pull-request-target-code-checkout.yaml
+ #
+ # The pull_request_target event runs in the context of the BASE
+ # (target) repository, which means it has access to all repo
+ # secrets. Checking out the PR head then executes untrusted
+ # code from the fork with those secrets available.
+ #
+ # Covers all three trigger syntax variants:
+ # on:
+ # pull_request_target: ... (mapping)
+ # on: [push, pull_request_target] (list)
+ # on: pull_request_target (bare string)
+ #
+ # Covers all ref variants via metavariable-pattern:
+ # github.event.pull_request.head.sha
+ # github.event.pull_request.head.ref
+ # github.head_ref
+ # ---------------------------------------------------------------
+ - id: gha-pr-target-code-checkout
+ languages: [yaml]
+ message: >-
+ This GitHub Actions workflow uses `pull_request_target` and checks
+ out code from the incoming pull request. When using
+ `pull_request_target`, the Action runs in the context of the target
+ repository, which includes access to all repository secrets.
+ By checking out the incoming PR code, any build scripts
+ (npm install, make, pip install) execute attacker-controlled code
+ with access to those secrets, allowing an attacker to steal them.
+ Audit your workflow to ensure no incoming PR code is executed.
+ See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+ severity: ERROR
+ metadata:
+ category: security
+ owasp:
+ - "A08:2021 - Software and Data Integrity Failures"
+ cwe:
+ - "CWE-829: Inclusion of Functionality from Untrusted Control Sphere"
+ references:
+ - https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
+ - https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
+ likelihood: MEDIUM
+ impact: HIGH
+ confidence: HIGH
+ patterns:
+ # Match any of the three trigger syntax forms
+ - pattern-either:
+ - pattern-inside: |
+ on:
+ ...
+ pull_request_target: ...
+ ...
+ ...
+ - pattern-inside: |
+ on: [..., pull_request_target, ...]
+ ...
+ - pattern-inside: |
+ on: pull_request_target
+ ...
+ # Scope to within a job's steps block
+ - pattern-inside: |
+ jobs:
+ ...
+ $JOBNAME:
+ ...
+ steps:
+ ...
+ # Match the checkout action with a ref parameter
+ - pattern: |
+ ...
+ uses: "$ACTION"
+ with:
+ ...
+ ref: $EXPR
+ # Ensure it's an actions/checkout action
+ - metavariable-regex:
+ metavariable: $ACTION
+ regex: actions/checkout@.*
+ # Ensure the ref expression references the incoming PR
+ - metavariable-pattern:
+ language: generic
+ metavariable: $EXPR
+ patterns:
+ - pattern-inside: "${{ ... }}"
+ - pattern-either:
+ - pattern: github.event.pull_request ...
+ - pattern: github.head_ref ...
+ paths:
+ include:
+ - "*.yml"
+ - "*.yaml"
diff --git a/semgrep-rules/gha-excessive-permissions.yml b/semgrep-rules/gha-excessive-permissions.yml
new file mode 100644
index 0000000..0aa4f47
--- /dev/null
+++ b/semgrep-rules/gha-excessive-permissions.yml
@@ -0,0 +1,125 @@
+rules:
+
+ # ---------------------------------------------------------------
+ # Rule 1: Workflow-level permissions: write-all
+ # ---------------------------------------------------------------
+ # Catches the most obvious anti-pattern: granting the GITHUB_TOKEN
+ # full write access to every scope at the workflow level.
+ #
+ # Example match:
+ # permissions: write-all
+ # jobs:
+ # build:
+ # runs-on: ubuntu-latest
+ # ---------------------------------------------------------------
+ - id: gha-workflow-permissions-write-all
+ message: >
+ GitHub Actions workflow grants 'permissions: write-all' at the
+ workflow level, giving the GITHUB_TOKEN unrestricted write access
+ to all scopes (contents, packages, pull-requests, etc.).
+ Apply least-privilege by declaring only the permissions each job
+ actually needs.
+ severity: ERROR
+ languages: [yaml]
+ metadata:
+ cwe:
+ - "CWE-250: Execution with Unnecessary Privileges"
+ references:
+ - https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
+ category: security
+ patterns:
+ - pattern: |
+ permissions: write-all
+ - pattern-not-inside: |
+ jobs:
+ ...
+ $JOB:
+ ...
+ permissions: write-all
+ ...
+ paths:
+ include:
+ - "*.yml"
+ - "*.yaml"
+
+ # ---------------------------------------------------------------
+ # Rule 2: Job-level permissions: write-all
+ # ---------------------------------------------------------------
+ # Same problem scoped to a single job. Still overly broad since
+ # it grants write to every scope for that job.
+ #
+ # Example match:
+ # jobs:
+ # deploy:
+ # permissions: write-all
+ # runs-on: ubuntu-latest
+ # ---------------------------------------------------------------
+ - id: gha-job-permissions-write-all
+ message: >
+ GitHub Actions job grants 'permissions: write-all', giving the
+ GITHUB_TOKEN unrestricted write access for this job. Narrow the
+ permissions to only those required (e.g., contents: read).
+ severity: WARNING
+ languages: [yaml]
+ metadata:
+ cwe:
+ - "CWE-250: Execution with Unnecessary Privileges"
+ references:
+ - https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
+ category: security
+ pattern: |
+ jobs:
+ ...
+ $JOB:
+ ...
+ permissions: write-all
+ ...
+ paths:
+ include:
+ - "*.yml"
+ - "*.yaml"
+
+ # ---------------------------------------------------------------
+ # Rule 3: Workflow-level broad write scopes
+ # ---------------------------------------------------------------
+ # Catches explicit per-scope write grants at the workflow level
+ # that are commonly over-provisioned. The metavariable $SCOPE
+ # matches any scope key (contents, packages, etc.) set to write.
+ #
+ # Example match:
+ # permissions:
+ # contents: write
+ # packages: write
+ # pull-requests: write
+ # ---------------------------------------------------------------
+ - id: gha-workflow-broad-write-permissions
+ message: >
+ GitHub Actions workflow grants write access to scope '$SCOPE'
+ at the workflow level. Verify this is required. Prefer setting
+ permissions at the job level so each job gets only what it needs.
+ severity: WARNING
+ languages: [yaml]
+ metadata:
+ cwe:
+ - "CWE-250: Execution with Unnecessary Privileges"
+ references:
+ - https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication
+ category: security
+ patterns:
+ - pattern: |
+ permissions:
+ ...
+ $SCOPE: write
+ ...
+ - pattern-not-inside: |
+ jobs:
+ ...
+ $JOB:
+ ...
+ permissions:
+ ...
+ ...
+ paths:
+ include:
+ - "*.yml"
+ - "*.yaml"
diff --git a/templates/cycode-scan.yml b/templates/cycode-scan.yml
new file mode 100644
index 0000000..9afbf64
--- /dev/null
+++ b/templates/cycode-scan.yml
@@ -0,0 +1,135 @@
+# Centralized Cycode scan template.
+#
+# Single source of truth for the Cycode CI/CD gate across many pipelines.
+# Consumers `extends:` this template and pass app-specific parameters.
+#
+# Runs a scan in JSON mode, publishes results to the Tests tab + build summary
+# tab + artifact, then enforces a pass/fail gate via the CLI, the API, or both.
+#
+# Cross-repo usage pattern (production):
+# resources:
+# repositories:
+# - repository: security-templates
+# type: git
+# name: SecurityTeam/cycode-pipeline-templates
+# ref: refs/tags/v1
+# extends:
+# template: templates/cycode-scan.yml@security-templates
+# parameters: { ... }
+#
+# Single-repo usage (this demo): see ../azure-pipelines-template-consumer.yml
+
+parameters:
+ - name: scanPath
+ type: string
+ default: "./"
+ - name: scanTypeFlags
+ type: string
+ default: "-t sast" # e.g. "-t sast -t sca -t secret"
+ - name: severityThreshold
+ type: string
+ default: "info" # info | low | medium | high | critical
+ - name: repoName
+ type: string # required — repo name as shown in Cycode
+ - name: gateMode
+ type: string
+ default: "cli" # cli | api | both | none
+ - name: publishResults
+ type: boolean
+ default: true
+ - name: poolName
+ type: string
+ default: "Default" # self-hosted pool; use 'ubuntu-latest' for MS-hosted
+ - name: pythonVersion
+ type: string
+ default: "3.12"
+
+stages:
+ - stage: Security
+ displayName: "Security — Cycode"
+ jobs:
+ - job: CycodeScan
+ displayName: "Cycode scan + gate"
+ pool:
+ name: ${{ parameters.poolName }}
+ steps:
+ - checkout: self
+ fetchDepth: 0
+
+ - task: UsePythonVersion@0
+ displayName: "Use Python ${{ parameters.pythonVersion }}"
+ inputs:
+ versionSpec: ${{ parameters.pythonVersion }}
+
+ - script: |
+ set -e
+ python3 -m pip install --upgrade pip
+ pip install cycode
+ if ! command -v jq >/dev/null 2>&1; then
+ sudo apt-get update -qq && sudo apt-get install -y -qq jq || true
+ fi
+ displayName: "Install Cycode CLI + jq"
+
+ # ---- Scan (soft-fail so we can publish results, then gate) ------
+ - script: |
+ set +e
+ cycode -o json scan --soft-fail \
+ ${{ parameters.scanTypeFlags }} \
+ --severity-threshold ${{ parameters.severityThreshold }} \
+ path ${{ parameters.scanPath }} > cycode-results.json
+ echo "scan exit: $?"
+ ls -la cycode-results.json
+ displayName: "Cycode scan (JSON)"
+ env:
+ CYCODE_CLIENT_ID: $(CYCODE_CLIENT_ID)
+ CYCODE_CLIENT_SECRET: $(CYCODE_CLIENT_SECRET)
+
+ # ---- Publish results --------------------------------------------
+ - ${{ if eq(parameters.publishResults, true) }}:
+ - script: python3 scripts/cycode-json-to-junit.py cycode-results.json cycode-junit.xml
+ displayName: "Convert JSON → JUnit"
+ condition: succeededOrFailed()
+
+ - task: PublishTestResults@2
+ displayName: "Publish to Tests tab"
+ condition: succeededOrFailed()
+ inputs:
+ testResultsFormat: "JUnit"
+ testResultsFiles: "cycode-junit.xml"
+ testRunTitle: "Cycode"
+ mergeTestResults: true
+ failTaskOnFailedTests: false
+
+ - script: |
+ python3 scripts/cycode-summary.py cycode-results.json > cycode-summary.md
+ echo "##vso[task.uploadsummary]$(System.DefaultWorkingDirectory)/cycode-summary.md"
+ displayName: "Publish custom summary tab"
+ condition: succeededOrFailed()
+
+ - task: PublishBuildArtifacts@1
+ displayName: "Publish raw Cycode JSON"
+ condition: succeededOrFailed()
+ inputs:
+ pathToPublish: "cycode-results.json"
+ artifactName: "cycode-report"
+
+ # ---- Gates ------------------------------------------------------
+ - ${{ if or(eq(parameters.gateMode, 'cli'), eq(parameters.gateMode, 'both')) }}:
+ - script: |
+ cycode scan \
+ ${{ parameters.scanTypeFlags }} \
+ --severity-threshold ${{ parameters.severityThreshold }} \
+ path ${{ parameters.scanPath }}
+ displayName: "Gate: CLI scan (fails on findings)"
+ env:
+ CYCODE_CLIENT_ID: $(CYCODE_CLIENT_ID)
+ CYCODE_CLIENT_SECRET: $(CYCODE_CLIENT_SECRET)
+
+ - ${{ if or(eq(parameters.gateMode, 'api'), eq(parameters.gateMode, 'both')) }}:
+ - script: bash scripts/cycode-gate.sh
+ displayName: "Gate: API (fails on Open violations)"
+ env:
+ CYCODE_CLIENT_ID: $(CYCODE_CLIENT_ID)
+ CYCODE_CLIENT_SECRET: $(CYCODE_CLIENT_SECRET)
+ REPO_NAME: ${{ parameters.repoName }}
+ SEVERITY_MIN: ${{ parameters.severityThreshold }}
diff --git a/vulnerable_dockerfiles/Dockerfile.n8n-vulnerable b/vulnerable_dockerfiles/Dockerfile.n8n-vulnerable
new file mode 100644
index 0000000..f006ea0
--- /dev/null
+++ b/vulnerable_dockerfiles/Dockerfile.n8n-vulnerable
@@ -0,0 +1,47 @@
+# Vulnerable n8n Container
+# This Dockerfile uses a vulnerable version of n8n affected by CVE-2026-21858
+
+FROM n8nio/n8n:1.100.0
+
+LABEL maintainer="demo@example.com"
+LABEL description="Workflow automation service"
+LABEL vulnerability="CVE-2026-21858"
+
+# Environment configuration
+ENV N8N_BASIC_AUTH_ACTIVE=false
+ENV N8N_HOST=0.0.0.0
+ENV N8N_PORT=5678
+ENV N8N_PROTOCOL=http
+ENV GENERIC_TIMEZONE=UTC
+
+# Expose the n8n port
+EXPOSE 5678
+
+# Start n8n
+CMD ["n8n", "start"]
+
+# =============================================================================
+# VULNERABILITY INFORMATION
+# =============================================================================
+#
+# CVE-2026-21858: Critical Content-Type Confusion RCE in n8n
+#
+# Affected: n8n versions >= 1.65.0 and < 1.121.0
+# CVSS Score: 10.0 (Critical)
+# CWE: CWE-20 (Improper Input Validation)
+#
+# The n8n:1.100.0 base image contains a critical vulnerability in Form Webhook
+# handling that allows unauthenticated attackers to:
+# - Read arbitrary files from the container
+# - Extract database and session secrets
+# - Forge administrator sessions
+# - Execute arbitrary commands
+#
+# REMEDIATION:
+# Update to a fixed version:
+# FROM n8nio/n8n:1.121.0
+#
+# References:
+# - https://nvd.nist.gov/vuln/detail/CVE-2026-21858
+# - https://github.com/n8n-io/n8n/security/advisories/GHSA-v4pr-fm98-w9pg
+# =============================================================================
diff --git a/vulnerable_packages/README.md b/vulnerable_packages/README.md
new file mode 100644
index 0000000..4b63231
--- /dev/null
+++ b/vulnerable_packages/README.md
@@ -0,0 +1,29 @@
+# Vulnerable Packages
+
+Examples of applications using vulnerable open-source packages for SCA (Software Composition Analysis) testing.
+
+## Contents
+
+### n8n-workflow/
+
+Workflow automation service using a vulnerable version of n8n affected by **CVE-2026-21858** (CVSS 10.0 Critical).
+
+- **Vulnerability**: Content-Type confusion enabling unauthenticated RCE
+- **Affected versions**: n8n >= 1.65.0 and < 1.121.0
+- **Fixed version**: 1.121.0
+
+## Use Cases
+
+- Testing SCA scanner detection of critical CVEs
+- Validating vulnerability enrichment (CVSS, EPSS, KEV status)
+- Demonstrating supply chain risk from vulnerable dependencies
+- Training on dependency vulnerability remediation
+
+## Adding New Examples
+
+When adding vulnerable package examples:
+
+1. Create a subdirectory with a descriptive name
+2. Include the package manifest (package.json, requirements.txt, go.mod, etc.)
+3. Document the specific CVE(s) and affected versions in a README
+4. Include remediation guidance
diff --git a/vulnerable_packages/n8n-workflow/README.md b/vulnerable_packages/n8n-workflow/README.md
new file mode 100644
index 0000000..5a8a4c6
--- /dev/null
+++ b/vulnerable_packages/n8n-workflow/README.md
@@ -0,0 +1,40 @@
+# N8N Workflow Service
+
+Example workflow automation service using n8n.
+
+## Vulnerability
+
+This package includes **n8n v1.100.0**, which is affected by **CVE-2026-21858** (CVSS 10.0 Critical).
+
+### CVE-2026-21858: Content-Type Confusion RCE
+
+- **Affected versions**: n8n >= 1.65.0 and < 1.121.0
+- **Fixed version**: 1.121.0
+- **CWE**: CWE-20 (Improper Input Validation)
+- **Attack vector**: Unauthenticated remote code execution via Form Webhook
+
+### Description
+
+A content-type confusion vulnerability in n8n's Form Webhook handling allows an unauthenticated attacker to:
+
+1. Read arbitrary files from the n8n instance
+2. Extract database credentials and session secrets
+3. Forge administrator sessions
+4. Execute arbitrary commands on the host
+
+### Remediation
+
+Upgrade to n8n version 1.121.0 or later:
+
+```json
+{
+ "dependencies": {
+ "n8n": "1.121.0"
+ }
+}
+```
+
+### References
+
+- [NVD - CVE-2026-21858](https://nvd.nist.gov/vuln/detail/CVE-2026-21858)
+- [GitHub Security Advisory GHSA-v4pr-fm98-w9pg](https://github.com/n8n-io/n8n/security/advisories/GHSA-v4pr-fm98-w9pg)
diff --git a/vulnerable_packages/n8n-workflow/package.json b/vulnerable_packages/n8n-workflow/package.json
new file mode 100644
index 0000000..17616fa
--- /dev/null
+++ b/vulnerable_packages/n8n-workflow/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "n8n-workflow-service",
+ "version": "1.0.0",
+ "description": "Workflow automation service using n8n",
+ "main": "index.js",
+ "scripts": {
+ "start": "n8n start",
+ "dev": "n8n start --tunnel"
+ },
+ "keywords": [
+ "workflow",
+ "automation",
+ "n8n"
+ ],
+ "author": "Demo Project",
+ "license": "MIT",
+ "dependencies": {
+ "n8n": "1.100.0",
+ "express": "^4.18.2",
+ "dotenv": "^16.3.1"
+ }
+}