From 2501666eb5f43d53ede9b74c1a2053bc6ff43951 Mon Sep 17 00:00:00 2001 From: Brian Levine Date: Tue, 27 Jan 2026 08:43:42 -0500 Subject: [PATCH 01/12] Add n8n CVE-2026-21858 vulnerable examples for SCA and container scanning - Add vulnerable_packages/ directory for SCA testing - Add n8n-workflow example with vulnerable n8n v1.100.0 - Add Dockerfile.n8n-vulnerable with CVE-2026-21858 - Update README with new vulnerable_packages section --- README.md | 7 +++ .../Dockerfile.n8n-vulnerable | 47 +++++++++++++++++++ vulnerable_packages/README.md | 29 ++++++++++++ vulnerable_packages/n8n-workflow/README.md | 40 ++++++++++++++++ vulnerable_packages/n8n-workflow/package.json | 22 +++++++++ 5 files changed, 145 insertions(+) create mode 100644 vulnerable_dockerfiles/Dockerfile.n8n-vulnerable create mode 100644 vulnerable_packages/README.md create mode 100644 vulnerable_packages/n8n-workflow/README.md create mode 100644 vulnerable_packages/n8n-workflow/package.json diff --git a/README.md b/README.md index ffe19e7..1ee1fa2 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,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/`) @@ -63,6 +64,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: 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" + } +} From 8b6e561ff6b90709d23bd03be578706bf728e7ec Mon Sep 17 00:00:00 2001 From: Brian Levine Date: Tue, 17 Feb 2026 23:08:50 -0500 Subject: [PATCH 02/12] Add API configuration for service integrations --- config/api_config.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 config/api_config.py 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" From 68b78f78c10e4f2fef5a06fc69c33d4643f656a3 Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Mon, 6 Apr 2026 23:32:31 -0400 Subject: [PATCH 03/12] Add custom Semgrep rules for GitHub Actions security - gha-excessive-permissions: detects permissions: write-all and broad write scope grants at workflow and job level - gha-dangerous-pr-target-checkout: detects pull_request_target trigger combined with checkout of incoming PR code (pwn request pattern) --- .../gha-dangerous-pr-target-checkout.yml | 97 ++++++++++++++ semgrep-rules/gha-excessive-permissions.yml | 125 ++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 semgrep-rules/gha-dangerous-pr-target-checkout.yml create mode 100644 semgrep-rules/gha-excessive-permissions.yml 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" From 2ac1f0da318f6c6ea3d8185a55a29571f37b0b94 Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Mon, 13 Apr 2026 08:29:54 -0400 Subject: [PATCH 04/12] Add Cycode CLI SAST scan workflow Scans vulnerable_apps/ for SAST findings on push, PR, and manual trigger. Blocks build on any finding (default CLI exit code 1). --- .github/workflows/cycode-sast-scan.yml | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/cycode-sast-scan.yml diff --git a/.github/workflows/cycode-sast-scan.yml b/.github/workflows/cycode-sast-scan.yml new file mode 100644 index 0000000..212dd9a --- /dev/null +++ b/.github/workflows/cycode-sast-scan.yml @@ -0,0 +1,30 @@ +name: "Cycode SAST Scan" + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + cycode-sast: + name: "SAST Security Scan" + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + 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/ From 78692bb363c8a2d68e729f68f8af55e3ff76ff22 Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Mon, 13 Apr 2026 08:51:19 -0400 Subject: [PATCH 05/12] Pin GitHub Actions to commit SHAs for supply chain security --- .github/workflows/cycode-sast-scan.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cycode-sast-scan.yml b/.github/workflows/cycode-sast-scan.yml index 212dd9a..31acc27 100644 --- a/.github/workflows/cycode-sast-scan.yml +++ b/.github/workflows/cycode-sast-scan.yml @@ -13,10 +13,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" From 7e09b25528cff7ca8748ebfa45504c7530157f0b Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Mon, 13 Apr 2026 09:20:58 -0400 Subject: [PATCH 06/12] Add Cycode SAST delta scan workflow for PRs Scans only the diff between PR base and head commits, not the full codebase. Falls back to HEAD~1 on manual trigger. --- .github/workflows/cycode-sast-pr-scan.yml | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/cycode-sast-pr-scan.yml diff --git a/.github/workflows/cycode-sast-pr-scan.yml b/.github/workflows/cycode-sast-pr-scan.yml new file mode 100644 index 0000000..44dde20 --- /dev/null +++ b/.github/workflows/cycode-sast-pr-scan.yml @@ -0,0 +1,39 @@ +name: "Cycode SAST PR Scan (Delta)" + +on: + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + cycode-sast-delta: + name: "SAST Delta Scan — PR Changes Only" + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + 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 From 3cfda9cb5e82d5529b9b27eac5527990499e5abb Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Mon, 13 Apr 2026 10:03:03 -0400 Subject: [PATCH 07/12] Skip delta scan on fork PRs to protect secrets --- .github/workflows/cycode-sast-pr-scan.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cycode-sast-pr-scan.yml b/.github/workflows/cycode-sast-pr-scan.yml index 44dde20..4803c26 100644 --- a/.github/workflows/cycode-sast-pr-scan.yml +++ b/.github/workflows/cycode-sast-pr-scan.yml @@ -8,6 +8,7 @@ on: 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 From 62e174137f094e2c7c0d873468863c92684dc712 Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Mon, 13 Apr 2026 11:07:15 -0400 Subject: [PATCH 08/12] Remove PR trigger from full scan workflow Full scan runs on push to main only. PRs use the delta scan workflow. --- .github/workflows/cycode-sast-scan.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/cycode-sast-scan.yml b/.github/workflows/cycode-sast-scan.yml index 31acc27..d52e9aa 100644 --- a/.github/workflows/cycode-sast-scan.yml +++ b/.github/workflows/cycode-sast-scan.yml @@ -3,8 +3,6 @@ name: "Cycode SAST Scan" on: push: branches: [main] - pull_request: - branches: [main] workflow_dispatch: jobs: From 36212ef6cc9de992ba940907c0653bafd56ac142 Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Mon, 13 Apr 2026 11:26:04 -0400 Subject: [PATCH 09/12] Update pinned action SHAs to v6 (Node.js 24 compatible) --- .github/workflows/cycode-sast-pr-scan.yml | 4 ++-- .github/workflows/cycode-sast-scan.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cycode-sast-pr-scan.yml b/.github/workflows/cycode-sast-pr-scan.yml index 4803c26..2255ac7 100644 --- a/.github/workflows/cycode-sast-pr-scan.yml +++ b/.github/workflows/cycode-sast-pr-scan.yml @@ -12,12 +12,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" diff --git a/.github/workflows/cycode-sast-scan.yml b/.github/workflows/cycode-sast-scan.yml index d52e9aa..0e28b7c 100644 --- a/.github/workflows/cycode-sast-scan.yml +++ b/.github/workflows/cycode-sast-scan.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.12" From fd18dad86b0323716dfb164281873727db76f855 Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Wed, 22 Apr 2026 00:06:55 -0400 Subject: [PATCH 10/12] Add Azure Pipelines advanced patterns: API gate, results publishing, centralized template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new patterns complementing the existing CLI gate in azure-pipelines.yml: - scripts/cycode-gate.sh — queries the Cycode RIG Graph API for Open violations in a named repo; fails the build if any match. Filters by severity, category, and risk score via env vars. Emits ##vso[task.logissue] for Azure Pipelines. - scripts/cycode-json-to-junit.py + scripts/cycode-summary.py — converters that take `cycode -o json scan ...` output and produce JUnit XML (for PublishTestResults@2 → Tests tab) and a Markdown report (for ##vso[task.uploadsummary] → custom tab on build summary). - templates/cycode-scan.yml — centralized template consumed via `extends`. App pipelines pass parameters (scanPath, scanTypeFlags, severityThreshold, repoName, gateMode) and inherit the scan + publish + gate logic. Cross-repo usage documented in the header. Example pipelines: - azure-pipelines-api-gate.yml — API gate standalone - azure-pipelines-publish-results.yml — Tests tab + summary + artifact - azure-pipelines-template-consumer.yml — minimal consumer of the template --- README.md | 24 +++++ azure-pipelines-api-gate.yml | 40 ++++++++ azure-pipelines-publish-results.yml | 81 ++++++++++++++++ azure-pipelines-template-consumer.yml | 33 +++++++ scripts/cycode-gate.sh | 114 ++++++++++++++++++++++ scripts/cycode-json-to-junit.py | 84 ++++++++++++++++ scripts/cycode-summary.py | 68 +++++++++++++ templates/cycode-scan.yml | 135 ++++++++++++++++++++++++++ 8 files changed, 579 insertions(+) create mode 100644 azure-pipelines-api-gate.yml create mode 100644 azure-pipelines-publish-results.yml create mode 100644 azure-pipelines-template-consumer.yml create mode 100755 scripts/cycode-gate.sh create mode 100755 scripts/cycode-json-to-junit.py create mode 100755 scripts/cycode-summary.py create mode 100644 templates/cycode-scan.yml diff --git a/README.md b/README.md index 3b21b3e..64e8829 100644 --- a/README.md +++ b/README.md @@ -83,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/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/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 }} From cffbf602ff4d900e2f3ca0f3bbb2ba5c205545d2 Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Wed, 22 Apr 2026 00:41:12 -0400 Subject: [PATCH 11/12] Fix cycode-gate.sh jq paths and use bare repo name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RIG .result[] items wrap detections in a .resource object; updated the jq filter to drill through it so severity/risk_score/policy/file_path render correctly instead of all dashes. - Use bare repo name ("vectorvictor") in defaults and docs — the owner/repo form returns 0 results in RIG. - Check .fast_query_has_more and surface "at least N (page cap hit)" when results are paginated, so the gate message is truthful when the tenant has more than page_size findings. --- azure-pipelines-api-gate.yml | 3 ++- azure-pipelines-template-consumer.yml | 2 +- scripts/cycode-gate.sh | 16 ++++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/azure-pipelines-api-gate.yml b/azure-pipelines-api-gate.yml index 0d4dd47..69dc86d 100644 --- a/azure-pipelines-api-gate.yml +++ b/azure-pipelines-api-gate.yml @@ -16,7 +16,8 @@ pool: variables: # Must match the repo name shown in Cycode's Violations UI. - REPO_NAME: "AppSecHQ/vectorvictor" + # Bare repo name as stored in Cycode's RIG — NOT "owner/repo". + REPO_NAME: "vectorvictor" steps: - checkout: self diff --git a/azure-pipelines-template-consumer.yml b/azure-pipelines-template-consumer.yml index b06ef41..9be7938 100644 --- a/azure-pipelines-template-consumer.yml +++ b/azure-pipelines-template-consumer.yml @@ -27,7 +27,7 @@ extends: scanPath: "./vulnerable_apps/" scanTypeFlags: "-t sast" severityThreshold: "high" - repoName: "AppSecHQ/vectorvictor" + repoName: "vectorvictor" # bare repo name (not "owner/repo") gateMode: "both" # defense in depth: CLI + API publishResults: true poolName: "Default" diff --git a/scripts/cycode-gate.sh b/scripts/cycode-gate.sh index 7c0e7d0..828d36a 100755 --- a/scripts/cycode-gate.sh +++ b/scripts/cycode-gate.sh @@ -3,7 +3,8 @@ # # Required env: # CYCODE_CLIENT_ID, CYCODE_CLIENT_SECRET -# REPO_NAME — repo name as shown in Cycode's Violations UI, e.g. "AppSecHQ/vectorvictor" +# REPO_NAME — BARE repo name as stored in Cycode's RIG (e.g. "vectorvictor", +# NOT "AppSecHQ/vectorvictor"). Check the Violations UI to confirm. # # Optional env (any combination): # SEVERITY_MIN Critical | High | Medium | Low (inclusive threshold) @@ -98,16 +99,23 @@ if ! jq -e '.result' >/dev/null 2>&1 <<<"$RESPONSE"; then fi COUNT=$(jq '.result | length' <<<"$RESPONSE") -echo "Open violations matching filters for ${REPO_NAME}: ${COUNT}" +HAS_MORE=$(jq -r '.fast_query_has_more // false' <<<"$RESPONSE") +if [[ "$HAS_MORE" == "true" ]]; then + COUNT_LABEL="at least ${COUNT} (page cap hit)" +else + COUNT_LABEL="${COUNT}" +fi +echo "Open violations matching filters for ${REPO_NAME}: ${COUNT_LABEL}" # --- 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 // "-"))"' \ + # Each .result[] item wraps the detection in a .resource object. + jq -r '.result[] | .resource | " [\(.severity // "-") / risk \(.risk_score // "-")] \(.source_policy_name // "-") — \(.detection_details.file_path // .detection_details.package_name // .source_entity_name // "-"):\(.detection_details.line // "")"' \ <<<"$RESPONSE" | head -20 echo - echo "##vso[task.logissue type=error]Cycode gate failed: ${COUNT} Open violation(s) in ${REPO_NAME}" + echo "##vso[task.logissue type=error]Cycode gate failed: ${COUNT_LABEL} Open violation(s) in ${REPO_NAME}" exit 1 fi From 9cb537a340b863513cc5b03e286c536774c88934 Mon Sep 17 00:00:00 2001 From: appsechq-brian Date: Wed, 22 Apr 2026 17:15:22 -0400 Subject: [PATCH 12/12] Use ADO Library variable group for credentials; harden jq install Two small improvements based on end-to-end testing in Azure DevOps: Variable group for credentials - All three top-level pipelines now reference an ADO Library variable group named 'cycode-credentials' via 'variables: - group: ...' instead of expecting CYCODE_CLIENT_ID / CYCODE_CLIENT_SECRET as bare secret pipeline variables. Single source of truth for credential rotation (ideally backed by Key Vault) and fewer pipelines to update when the service account changes. jq install hardening - The 'apt-get || true' form silently masked failures on agents without apt-get (macOS, RHEL, Windows, distroless). Now: detect apt-get, fall back to brew, and fail loudly with a clear 'Preinstall jq on the agent' message if neither is available. --- azure-pipelines-api-gate.yml | 22 ++++++++++++++++++---- azure-pipelines-publish-results.yml | 6 +++++- azure-pipelines-template-consumer.yml | 5 +++++ templates/cycode-scan.yml | 9 ++++++++- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/azure-pipelines-api-gate.yml b/azure-pipelines-api-gate.yml index 69dc86d..f42d121 100644 --- a/azure-pipelines-api-gate.yml +++ b/azure-pipelines-api-gate.yml @@ -4,7 +4,8 @@ # 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 +# - Library variable group 'cycode-credentials' with CYCODE_CLIENT_ID and +# CYCODE_CLIENT_SECRET (both marked secret). # - Agent pool 'Default' (self-hosted) or change to 'ubuntu-latest' # # Run manually: Pipelines → this pipeline → Run @@ -15,16 +16,29 @@ pool: name: Default variables: + # Pulls CYCODE_CLIENT_ID and CYCODE_CLIENT_SECRET from ADO Library. + - group: cycode-credentials # Must match the repo name shown in Cycode's Violations UI. # Bare repo name as stored in Cycode's RIG — NOT "owner/repo". - REPO_NAME: "vectorvictor" + - name: REPO_NAME + value: "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 + set -e + if command -v jq >/dev/null 2>&1; then + jq --version + exit 0 + fi + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq jq + elif command -v brew >/dev/null 2>&1; then + brew install jq + else + echo "##vso[task.logissue type=error]jq is not installed and no supported package manager (apt-get, brew) was found. Preinstall jq on the agent." + exit 1 fi jq --version displayName: "Ensure jq is available" diff --git a/azure-pipelines-publish-results.yml b/azure-pipelines-publish-results.yml index 56904da..66262a3 100644 --- a/azure-pipelines-publish-results.yml +++ b/azure-pipelines-publish-results.yml @@ -15,7 +15,11 @@ pool: name: Default variables: - SCAN_PATH: "./vulnerable_apps/" + # Pulls CYCODE_CLIENT_ID and CYCODE_CLIENT_SECRET from ADO Library. + # Create via Pipelines → Library → + Variable group → 'cycode-credentials'. + - group: cycode-credentials + - name: SCAN_PATH + value: "./vulnerable_apps/" steps: - checkout: self diff --git a/azure-pipelines-template-consumer.yml b/azure-pipelines-template-consumer.yml index 9be7938..8d61dc5 100644 --- a/azure-pipelines-template-consumer.yml +++ b/azure-pipelines-template-consumer.yml @@ -21,6 +21,11 @@ trigger: none pr: none +variables: + # Pulls CYCODE_CLIENT_ID and CYCODE_CLIENT_SECRET from ADO Library. + # Create via Pipelines → Library → + Variable group → 'cycode-credentials'. + - group: cycode-credentials + extends: template: templates/cycode-scan.yml parameters: diff --git a/templates/cycode-scan.yml b/templates/cycode-scan.yml index 9afbf64..2b117d2 100644 --- a/templates/cycode-scan.yml +++ b/templates/cycode-scan.yml @@ -66,7 +66,14 @@ stages: 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 + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -qq && sudo apt-get install -y -qq jq + elif command -v brew >/dev/null 2>&1; then + brew install jq + else + echo "##vso[task.logissue type=error]jq is not installed and no supported package manager (apt-get, brew) was found. Preinstall jq on the agent." + exit 1 + fi fi displayName: "Install Cycode CLI + jq"