Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 41 additions & 0 deletions azure-pipelines-api-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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.
# Bare repo name as stored in Cycode's RIG — NOT "owner/repo".
REPO_NAME: "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
81 changes: 81 additions & 0 deletions azure-pipelines-publish-results.yml
Original file line number Diff line number Diff line change
@@ -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)
33 changes: 33 additions & 0 deletions azure-pipelines-template-consumer.yml
Original file line number Diff line number Diff line change
@@ -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: "vectorvictor" # bare repo name (not "owner/repo")
gateMode: "both" # defense in depth: CLI + API
publishResults: true
poolName: "Default"
122 changes: 122 additions & 0 deletions scripts/cycode-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/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 — 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)
# 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")
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:"
# 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_LABEL} Open violation(s) in ${REPO_NAME}"
exit 1
fi

echo "Cycode gate passed: no Open violations matched the filters"
84 changes: 84 additions & 0 deletions scripts/cycode-json-to-junit.py
Original file line number Diff line number Diff line change
@@ -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:

Check failure on line 46 in scripts/cycode-json-to-junit.py

View check run for this annotation

Cycode Security / Cycode: SAST

scripts/cycode-json-to-junit.py#L46

Unsanitized user input in file path found
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cycode: SAST violation: 'Unsanitized user input in file path'.

Severity: High

Description

Unsanitized user input in file path resolution can lead to security vulnerabilities. This issue arises when an application directly uses input from the user to determine file paths or names without proper validation or sanitization. Attackers can exploit this to access unauthorized files or directories, leading to data breaches or other security compromises.

Cycode Remediation Guideline

✅ Do


  • Do use a safelist to define accessible paths or directories. Only allow user input to influence file paths within these predefined, safe boundaries.
  • Do sanitize user input used in file path resolution. For example, use absolute paths and check against the expected base directory
      BASE_DIRECTORY = '/path/to/safe/directory'
      my_path = os.path.abspath(os.path.join(BASE_DIRECTORY, user_input))
    
      if my_path.startswith(BASE_DIRECTORY):
        open(my_path)

❌ Don't


  • Do not directly use user input in file paths without sanitization. Failure to sanitize could allow attackers to manipulate file paths and to access or manipulate unauthorized files.

🎥 Learning materials (by Secure Code Warrior)


Tell us how you wish to proceed using one of the following commands:

Tag Short Description
#cycode_sast_ignore_here <reason> Ignore this violation — applies to this violation only
#cycode_ai_remediation Request remediation guidance using Cycode AI
#cycode_sast_false_positive <reason> Mark as false positive — applies to this violation only

⚠️ When commenting on Github, you may need to refresh the page to see the latest updates.

data = json.load(f)

detections = extract_detections(data)
total = len(detections)

out = ['<?xml version="1.0" encoding="UTF-8"?>']
out.append(
f'<testsuite name="Cycode" tests="{total}" '
f'failures="{total}" errors="0" skipped="0">'
)

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("]]>", "]]]]><![CDATA[>")
out.append(f' <testcase classname="{escape(classname)}" name="{escape(name)}">')
out.append(
f' <failure type="{escape(severity)}" '
f'message="{escape(str(message))[:200]}"><![CDATA[\n{cdata}\n]]></failure>'
)
out.append(" </testcase>")

out.append("</testsuite>")

with open(dst, "w") as f:

Check failure on line 73 in scripts/cycode-json-to-junit.py

View check run for this annotation

Cycode Security / Cycode: SAST

scripts/cycode-json-to-junit.py#L73

Unsanitized user input in file path found
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cycode: SAST violation: 'Unsanitized user input in file path'.

Severity: High

Description

Unsanitized user input in file path resolution can lead to security vulnerabilities. This issue arises when an application directly uses input from the user to determine file paths or names without proper validation or sanitization. Attackers can exploit this to access unauthorized files or directories, leading to data breaches or other security compromises.

Cycode Remediation Guideline

✅ Do


  • Do use a safelist to define accessible paths or directories. Only allow user input to influence file paths within these predefined, safe boundaries.
  • Do sanitize user input used in file path resolution. For example, use absolute paths and check against the expected base directory
      BASE_DIRECTORY = '/path/to/safe/directory'
      my_path = os.path.abspath(os.path.join(BASE_DIRECTORY, user_input))
    
      if my_path.startswith(BASE_DIRECTORY):
        open(my_path)

❌ Don't


  • Do not directly use user input in file paths without sanitization. Failure to sanitize could allow attackers to manipulate file paths and to access or manipulate unauthorized files.

🎥 Learning materials (by Secure Code Warrior)


Tell us how you wish to proceed using one of the following commands:

Tag Short Description
#cycode_sast_ignore_here <reason> Ignore this violation — applies to this violation only
#cycode_ai_remediation Request remediation guidance using Cycode AI
#cycode_sast_false_positive <reason> Mark as false positive — applies to this violation only

⚠️ When commenting on Github, you may need to refresh the page to see the latest updates.

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 <input.json> <output.xml>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1], sys.argv[2]))
Loading
Loading