-
Notifications
You must be signed in to change notification settings - Fork 1
Azure Pipelines advanced patterns: API gate, UI publishing, central template #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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) |
| 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" |
| 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" |
| 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: | ||||||||||
| 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: | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❗Cycode: SAST violation: 'Unsanitized user input in file path'. Severity: High DescriptionUnsanitized 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
❌ Don't
🎥 Learning materials (by Secure Code Warrior)Tell us how you wish to proceed using one of the following commands:
|
||||||||||
| 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])) | ||||||||||
There was a problem hiding this comment.
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
❌ Don't
🎥 Learning materials (by Secure Code Warrior)
Tell us how you wish to proceed using one of the following commands: