Building an End-to-End DevSecOps CI/CD Pipeline with Azure DevOps
π Building an End-to-End DevSecOps CI/CD Pipeline with Azure DevOps
A comprehensive walkthrough of designing, building, and automating secure CI/CD workflows with static and dynamic security testing, secrets detection, dependency scanning, container security, and centralized vulnerability management using DefectDojo.
This guide details the creation of a production-grade DevSecOps CI/CD pipeline in Azure DevOps, developed through the collaboration between Michael and Hossamβhence the 0xCoolSAM. The pipeline automates security scanning across source code, dependencies, containers, and runtime environments, with all findings centralized in DefectDojo for unified vulnerability management.
β¨ Overview
Modern software development demands security at every stage. Our pipeline integrates multiple security tools to enforce Security by Design, covering:
- Secrets Scanning: Detect hardcoded credentials using GitLeaks and TruffleHog.
- Software Composition Analysis (SCA): Identify vulnerabilities in dependencies with Sonatype Nexus IQ.
- Static Application Security Testing (SAST): Analyze code with Fortify ScanCentral SAST.
- Container Security: Scan Docker images using Trivy.
- Dynamic Application Security Testing (DAST): Test runtime vulnerabilities with Fortify ScanCentral DAST.
- Vulnerability Management: Aggregate and track findings using DefectDojo.
- Custom Automation: A private Azure DevOps extension for seamless DefectDojo integration.
This post provides a step-by-step guide, complete with pipeline configurations and insights from our implementation.
π οΈ Stack Overview
Stage | Tool | Purpose |
---|---|---|
Secrets Scanning | GitLeaks, TruffleHog | Detect hardcoded secrets and sensitive data. |
SCA (Dependency) Scanning | Sonatype Nexus IQ | Identify open-source components, their versions, and associated security vulnerabilities, license risks, and quality issues. |
SAST | Fortify ScanCentral SAST | Perform static code analysis to identify security vulnerabilities in source code. |
Container Image Scanning | Trivy | Finds and detects vulnerabilities, misconfigurations, and secrets in containers. |
DAST | Fortify ScanCentral DAST | Analyzes a running application to identify security vulnerabilities by simulating real-world attacks. |
Vulnerability Aggregation | DefectDojo | Centralize, track, and correlate findings |
π§± Pipeline Architecture
The pipeline, defined in an Azure DevOps YAML file, runs on a self-hosted agent (named βContaboβ) and consists of the following stages:
- Secrets Detection: GitLeaks and TruffleHog scan the source code, producing JSON reports.
- Build & SCA: Maven builds the Java application, and Nexus IQ scans dependencies.
- SAST: Fortify ScanCentral SAST analyzes the codebase, generating an
.fpr
file. - Container Scanning: Trivy scans Docker images for vulnerabilities.
- DAST: The application is deployed via Docker Compose, scanned with Fortify SC DAST, and torn down post-scan.
- Vulnerability Aggregation: A custom Azure DevOps extension uploads all scan results to DefectDojo.
π§© Prerequisites
Before setting up the pipeline, ensure the following are in place:
- Azure DevOps Account: Configured with a project and repository.
- Self-Hosted Agent: Named βContaboβ with the following tools installed:
- GitLeaks (
gitleaks
) - TruffleHog (
trufflehog
) - Maven (
mvn
) - Docker (
docker
) - Fortify CLI (
fcli
) - Trivy (
trivy
)
- GitLeaks (
- Dependencies:
- Sonatype Nexus IQ server.
- Fortify SSC and ScanCentral servers.
- DefectDojo instance (configured via environment variable
DEFECTDOJO_HOST
).
π Pipeline Configuration
Below are the detailed configurations for each pipeline stage, with sensitive values replaced by environment variables.
1. Secrets Detection
GitLeaks
Detects sensitive data in the source code.
- job: gitleaksScan
displayName: "GitLeaks Scan"
continueOnError: true
pool:
name: 'DevSecOps'
demands:
- agent.name -equals Contabo
steps:
- script: |
echo "π Running GitLeaks..."
mkdir -p $(Build.ArtifactStagingDirectory)/gitleaks
gitleaks detect \
--source "$(Build.SourcesDirectory)" \
--report-format json \
--report-path "$(Build.ArtifactStagingDirectory)/gitleaks/gitleaks-report.json" \
|| true
displayName: 'Run GitLeaks (Ignore Warnings)'
- task: PublishPipelineArtifact@1
displayName: "Publish GitLeaks Scan Report"
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/gitleaks'
artifact: 'GitLeaksScanReport'
We named it Run GitLeaks (Ignore Warnings)
because we used the locally installed GitLeaks on our Contabo agent to avoid the warnings that are triggered when secrets are detected using the GitLeaks extension in Azure DevOps.
TruffleHog
Complements GitLeaks for additional secret detection.
- job: trufflehogScan
displayName: "TruffleHog Scan"
continueOnError: true
pool:
name: 'DevSecOps'
demands:
- agent.name -equals Contabo
steps:
- checkout: self
- script: |
echo "π Running TruffleHog..."
mkdir -p $(Build.ArtifactStagingDirectory)/trufflehog
trufflehog git file://$(Build.SourcesDirectory) --json --no-update \
> $(Build.ArtifactStagingDirectory)/trufflehog/trufflehog-report.json
displayName: "Secrets Detection Using TruffleHog Scan"
- task: PublishPipelineArtifact@1
displayName: "Publish TruffleHog Scan Report"
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/trufflehog'
artifact: 'TruffleHogScanReport'
2. Build & SCA
Builds the Java application using Maven and scans dependencies with Nexus IQ.
- job: buildAndNexusScan
displayName: "Maven Build & Nexus IQ SCA Scan"
pool:
name: 'DevSecOps'
demands:
- agent.name -equals Contabo
steps:
- checkout: self
- task: Maven@4
displayName: 'Maven Build'
inputs:
mavenPomFile: 'pom.xml'
goals: 'clean package -DskipTests'
publishJUnitResults: true
testResultsFiles: '**/surefire-reports/TEST-*.xml'
- task: NexusIqPipelineTask@2
displayName: 'SCA Using Nexus IQ Scan'
inputs:
nexusIqService: 'Sonatype'
scanTargets: '**/target/**/*.jar, **/target/**/*.war'
organizationId: '$(NEXUS_IQ_ORG_ID)'
applicationId: 'java'
stage: 'Build'
resultFile: 'javasec.json'
ignoreSystemError: true
enableDebugLogging: true
acceptIqServerSelfSignedCertificates: true
- script: |
echo "Copying Nexus IQ result..."
mkdir -p $(Build.ArtifactStagingDirectory)/nexus
find $(Agent.WorkFolder)/_tasks/NexusIqPipelineTask*/ -name javasec.json -exec cp {} $(Build.ArtifactStagingDirectory)/nexus/javasec.json \;
displayName: 'Copy Nexus IQ Result to Staging Directory'
- script: |
echo "π Extracting reportDataUrl from javasec.json..."
JSON_FILE="$(Build.ArtifactStagingDirectory)/nexus/javasec.json"
RAW_URL=$(python3 -c "import json; f=open('${JSON_FILE}'); j=json.load(f); print(j['reportDataUrl'])")
echo "π₯ Downloading raw report from: $RAW_URL"
curl -u "$(NEXUS_IQ_USERNAME):$(NEXUS_IQ_PASSWORD)" \
-o "$(Build.ArtifactStagingDirectory)/nexus/raw-report.json" \
"$RAW_URL"
displayName: 'Download Nexus Raw Scan JSON with Authentication'
- task: PublishPipelineArtifact@1
displayName: "Publish All Nexus IQ Scan Artifacts"
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/nexus'
artifact: 'NexusFullScanReport'
3. SAST with Fortify
Performs static code analysis using Fortify ScanCentral SAST.
- job: fortifySAST
displayName: "SAST Using Fortify SC SAST Scan"
pool:
name: 'DevSecOps'
demands:
- agent.name -equals Contabo
steps:
- checkout: self
- task: FortifyScanCentralSAST@7
displayName: "Run Fortify SAST Scan"
inputs:
scanCentralCtrlUrl: '$(SCANCENTRAL_CTRL_URL)'
scanCentralClientToken: '$(SCANCENTRAL_CLIENT_TOKEN)'
sscUrl: '$(SSC_URL)'
sscCiToken: '$(SSC_CI_TOKEN)'
uploadToSSC: true
applicationIdentifierType: 'byId'
applicationVersionId: '$(FORTIFY_APP_VERSION_ID)'
autoDetectBuildTool: true
block: true
outputFile: 'java_sec_sast.fpr'
- script: |
mkdir -p $(Build.ArtifactStagingDirectory)/SAST
mv $(Build.SourcesDirectory)/java_sec_sast.fpr $(Build.ArtifactStagingDirectory)/SAST/
displayName: 'Move Fortify FPR to Staging Directory'
- task: PublishPipelineArtifact@1
displayName: "Publish Fortify FPR Report"
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/SAST/java_sec_sast.fpr'
artifact: 'FortifyFPRReport'
4. Container Image Scanning with Trivy
Scans Docker images for vulnerabilities.
- job: trivyScan
displayName: "Container Image Scanning Using Trivy Scan"
continueOnError: true
pool:
name: 'DevSecOps'
demands:
- agent.name -equals Contabo
steps:
- checkout: self
- script: |
mkdir -p $(Build.ArtifactStagingDirectory)/Trivy
trivy image $(DOCKER_IMAGE_NAME) -f json --output $(Build.ArtifactStagingDirectory)/Trivy/trivyreport.json
displayName: 'Run Trivy Scan'
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/Trivy'
artifact: 'TrivyScanReport'
5. DAST with Fortify
Deploys the application using Docker Compose and performs dynamic scanning.
- job: fortifyDAST
displayName: "Start App for Fortify DAST"
pool:
name: 'DevSecOps'
demands:
- agent.name -equals Contabo
steps:
- checkout: self
- task: DockerCompose@1
displayName: 'Start App via Docker Compose'
inputs:
containerregistrytype: 'Container Registry'
dockerComposeFile: 'docker-compose.yml'
action: 'Run a Docker Compose command'
dockerComposeCommand: 'up -d'
- script: |
echo "Waiting for services..."
sleep 15
displayName: 'Wait for Containers'
- task: DockerCompose@1
displayName: 'Check Running Containers'
inputs:
containerregistrytype: 'Container Registry'
dockerComposeFile: 'docker-compose.yml'
action: 'Run a Docker Compose command'
dockerComposeCommand: 'ps'
- job: fortifyDAST_Start
displayName: "DAST Using Fortify SC DAST Scan"
dependsOn: fortifyDAST
pool:
name: 'DevSecOps'
demands:
- agent.name -equals Contabo
steps:
- task: FortifyScanCentralDAST@7
displayName: 'Trigger DAST Scan'
inputs:
scanCentralDastApiUrl: '$(SCANCENTRAL_DAST_API_URL)'
scanCentralCiCdToken: '$(SCANCENTRAL_CICD_TOKEN)'
sscCiToken: '$(SSC_CI_TOKEN)'
overrides: '{"name": "Java-Sec"}'
- job: fortifyDAST_Wait
displayName: "Wait for Fortify SC DAST Completion"
dependsOn: fortifyDAST_Start
pool:
name: 'DevSecOps'
demands:
- agent.name -equals Contabo
steps:
- script: |
echo "π Authenticating to SSC..."
fcli ssc session login \
--token "$(SSC_CI_TOKEN)" \
--url "$(SSC_URL)" \
--insecure
echo "π Listing available scans..."
scan_id=$(fcli sc-dast scan list --store=id --output json | jq 'sort_by(.id) | reverse | .[0].id')
echo "β
Latest Scan ID: $scan_id"
echo "β³ Waiting for scan to finish..."
fcli sc-dast scan wait-for "$scan_id" --interval=30s
displayName: 'Wait for DAST Scan Completion'
- task: DockerCompose@1
displayName: 'Tear Down Containers'
inputs:
containerregistrytype: 'Container Registry'
dockerComposeFile: 'docker-compose.yml'
action: 'Run a Docker Compose command'
dockerComposeCommand: 'down'
6. Vulnerability Aggregation in DefectDojo
Uploads all scan results to DefectDojo using a custom Azure DevOps extension.
- job: DefectDojo
displayName: "Upload All Reports to DefectDojo"
dependsOn:
- gitleaksScan
- trufflehogScan
- trivyScan
- buildAndNexusScan
- fortifySAST
- fortifyDAST_Wait
pool:
name: 'DevSecOps'
demands:
- agent.name -equals Contabo
steps:
- task: DownloadPipelineArtifact@2
displayName: "Download GitLeaks Scan Report"
inputs:
artifact: 'GitLeaksScanReport'
path: '$(Build.ArtifactStagingDirectory)/gitleaks'
- task: UploadToDefectDojo@1
displayName: "Upload GitLeaks Report"
continueOnError: true
inputs:
host: '$(DEFECTDOJO_HOST)'
api_key: '$(DEFECTDOJO_API_KEY)'
engagement_id: '$(DEFECTDOJO_ENGAGEMENT_ID)'
product_id: '$(DEFECTDOJO_PRODUCT_ID)'
lead_id: '$(DEFECTDOJO_LEAD_ID)'
environment: 'Development'
result_file: '$(Build.ArtifactStagingDirectory)/gitleaks/gitleaks-report.json'
scanner: 'Gitleaks Scan'
- task: DownloadPipelineArtifact@2
displayName: "Download TruffleHog Scan Report"
inputs:
artifact: 'TruffleHogScanReport'
path: '$(Build.ArtifactStagingDirectory)/trufflehog'
- task: UploadToDefectDojo@1
displayName: "Upload TruffleHog Report"
continueOnError: true
inputs:
host: '$(DEFECTDOJO_HOST)'
api_key: '$(DEFECTDOJO_API_KEY)'
engagement_id: '$(DEFECTDOJO_ENGAGEMENT_ID)'
product_id: '$(DEFECTDOJO_PRODUCT_ID)'
lead_id: '$(DEFECTDOJO_LEAD_ID)'
environment: 'Development'
result_file: '$(Build.ArtifactStagingDirectory)/trufflehog/trufflehog-report.json'
scanner: 'Trufflehog Scan'
- task: DownloadPipelineArtifact@2
displayName: "Download Nexus Scan Report"
inputs:
artifact: 'NexusFullScanReport'
path: '$(Build.ArtifactStagingDirectory)/nexus'
- task: UploadToDefectDojo@1
displayName: "Upload Nexus IQ SCA Report"
continueOnError: true
inputs:
host: '$(DEFECTDOJO_HOST)'
api_key: '$(DEFECTDOJO_API_KEY)'
engagement_id: '$(DEFECTDOJO_ENGAGEMENT_ID)'
product_id: '$(DEFECTDOJO_PRODUCT_ID)'
lead_id: '$(DEFECTDOJO_LEAD_ID)'
environment: 'Development'
result_file: '$(Build.ArtifactStagingDirectory)/nexus/raw-report.json'
scanner: 'Sonatype Application Scan'
- task: DownloadPipelineArtifact@2
displayName: "Download SAST Scan Report"
inputs:
artifact: 'FortifyFPRReport'
path: '$(Build.ArtifactStagingDirectory)/SAST'
- task: UploadToDefectDojo@1
displayName: "Upload Fortify SAST FPR"
continueOnError: true
inputs:
host: '$(DEFECTDOJO_HOST)'
api_key: '$(DEFECTDOJO_API_KEY)'
engagement_id: '$(DEFECTDOJO_ENGAGEMENT_ID)'
product_id: '$(DEFECTDOJO_PRODUCT_ID)'
lead_id: '$(DEFECTDOJO_LEAD_ID)'
environment: 'Development'
result_file: '$(Build.ArtifactStagingDirectory)/SAST/java_sec_sast.fpr'
scanner: 'Fortify Scan'
- task: DownloadPipelineArtifact@2
displayName: "Download Trivy Scan Report"
inputs:
artifact: 'TrivyScanReport'
path: '$(Build.ArtifactStagingDirectory)/Trivy'
- task: UploadToDefectDojo@1
displayName: "Upload Trivy Container Scan"
continueOnError: true
inputs:
host: '$(DEFECTDOJO_HOST)'
api_key: '$(DEFECTDOJO_API_KEY)'
engagement_id: '$(DEFECTDOJO_ENGAGEMENT_ID)'
product_id: '$(DEFECTDOJO_PRODUCT_ID)'
lead_id: '$(DEFECTDOJO_LEAD_ID)'
environment: 'Development'
result_file: '$(Build.ArtifactStagingDirectory)/Trivy/trivyreport.json'
scanner: 'Trivy Scan'
π§© Custom Azure DevOps Extension
Initially, scan results were manually uploaded to DefectDojo, which was time-consuming. To address this, we developed a custom Azure DevOps extension to automate the upload of GitLeaks, TruffleHog, Nexus IQ, Fortify SAST, and Trivy reports. The extension:
- Fetches pipeline artifacts.
- Uses the DefectDojo API to post results.
- Tags each result with product and environment metadata (e.g.,
Development
,$(DEFECTDOJO_ENGAGEMENT_ID)
).
This extension is currently private but is being enhanced for potential public release.
π Verifying the Pipeline
Check Pipeline Status
Monitor the pipeline execution in Azure DevOps:
az pipelines runs list --project $(AZURE_DEVOPS_PROJECT_NAME)
View DefectDojo Reports
Access the DefectDojo dashboard to review consolidated vulnerabilities
π οΈ Troubleshooting
- Secrets Scanning Failures: Verify GitLeaks and TruffleHog are installed on the agent and paths are correct.
- Nexus IQ Issues: Check
reportDataUrl
extraction and ensure$(NEXUS_IQ_USERNAME)
and$(NEXUS_IQ_PASSWORD)
are set. - Fortify Errors: Validate
$(SCANCENTRAL_CTRL_URL)
,$(SSC_URL)
,$(SCANCENTRAL_CLIENT_TOKEN)
, and$(SSC_CI_TOKEN)
. - DefectDojo Uploads: Confirm
$(DEFECTDOJO_HOST)
,$(DEFECTDOJO_API_KEY)
,$(DEFECTDOJO_ENGAGEMENT_ID)
, and$(DEFECTDOJO_PRODUCT_ID)
are correctly configured. - Container Issues: Ensure Docker Compose file accuracy and registry credentials are set in Azure DevOps secrets.
π Key Takeaways
This project demonstrates a fully automated DevSecOps pipeline enforcing Security by Design. Key lessons:
- Automate Everything: From scans to cleanups, automation reduces manual effort.
- Integrate Early: Security checks at the commit level catch issues sooner.
- Centralize Results: DefectDojo provides a single pane of glass for vulnerability management.
- Continuous Improvement: Each scan offers insights to harden the pipeline.
π Useful References
- GitLeaks
- TruffleHog
- Sonatype Nexus IQ
- Sonatype for Azure DevOps
- Fortify SSC
- Fortify SAST
- Fortify SC DAST
- Fortify Azure DevOps Extension Documentation
- MicroFocus Fortify for Azure DevOps
- Trivy
- DefectDojo Documentation
π Conclusion
This end-to-end DevSecOps pipeline demonstrates a resilient and automated approach to secure software delivery. By seamlessly integrating industry-standard security tools and a custom Azure DevOps extension, we established a pipeline that ensures continuous security validation and effective vulnerability management throughout the development lifecycle.
We invite your feedback, suggestions, and collaboration ideasβletβs continue advancing secure software practices together.
π Connect with us:
- Hossam Ibraheem on LinkedIn
- Michael Milad on LinkedIn
- Explore the project on GitHub