245 lines
7.9 KiB
Groovy
245 lines
7.9 KiB
Groovy
pipeline {
|
|
agent {
|
|
kubernetes {
|
|
yaml '''
|
|
apiVersion: v1
|
|
kind: Pod
|
|
spec:
|
|
containers:
|
|
- name: node
|
|
image: public.ecr.aws/docker/library/node:20
|
|
command: ["sleep", "999999"]
|
|
resources: { limits: { memory: 1Gi } }
|
|
- name: semgrep
|
|
image: semgrep/semgrep:latest
|
|
command: ["sleep", "999999"]
|
|
resources: { limits: { memory: 1Gi } }
|
|
- name: gitleaks
|
|
image: ghcr.io/gitleaks/gitleaks:latest
|
|
command: ["sleep", "999999"]
|
|
resources: { limits: { memory: 256Mi } }
|
|
- name: trivy
|
|
image: ghcr.io/aquasecurity/trivy:latest
|
|
command: ["sleep", "999999"]
|
|
resources: { limits: { memory: 1Gi } }
|
|
- name: sonar
|
|
image: sonarsource/sonar-scanner-cli:latest
|
|
command: ["sleep", "999999"]
|
|
resources: { limits: { memory: 2Gi } }
|
|
env:
|
|
- name: SONAR_TOKEN
|
|
valueFrom: { secretKeyRef: { name: sonar-token, key: SONAR_TOKEN } }
|
|
- name: kaniko
|
|
image: gcr.io/kaniko-project/executor:debug
|
|
command: ["/busybox/sh", "-c", "sleep 999999"]
|
|
resources: { limits: { memory: 1Gi } }
|
|
- name: checkov
|
|
image: bridgecrew/checkov:latest
|
|
command: ["sleep", "999999"]
|
|
resources: { limits: { memory: 512Mi } }
|
|
- name: cosign
|
|
image: public.ecr.aws/docker/library/alpine:3.20
|
|
command: ["sleep", "999999"]
|
|
resources: { limits: { memory: 256Mi } }
|
|
env:
|
|
- name: COSIGN_PASSWORD
|
|
valueFrom: { secretKeyRef: { name: cosign-keys, key: password } }
|
|
volumeMounts:
|
|
- { name: cosign-keys, mountPath: /keys, readOnly: true }
|
|
- name: zap
|
|
image: ghcr.io/zaproxy/zaproxy:stable
|
|
command: ["sleep", "999999"]
|
|
resources: { limits: { memory: 1Gi } }
|
|
- name: git
|
|
image: alpine/git:latest
|
|
command: ["sleep", "999999"]
|
|
resources: { limits: { memory: 128Mi } }
|
|
- name: uploader
|
|
image: curlimages/curl:latest
|
|
command: ["sleep", "999999"]
|
|
securityContext: { runAsUser: 1000 }
|
|
resources: { limits: { memory: 128Mi } }
|
|
env:
|
|
- name: DD_TOKEN
|
|
valueFrom: { secretKeyRef: { name: defectdojo-token, key: DD_TOKEN } }
|
|
volumes:
|
|
- name: cosign-keys
|
|
secret: { secretName: cosign-keys }
|
|
'''
|
|
}
|
|
}
|
|
environment {
|
|
REGISTRY = "10.0.1.215:30500"
|
|
IMAGE = "demo-app"
|
|
SONAR_HOST = "http://sonarqube-sonarqube.platform.svc:9000"
|
|
DD_URL = "http://defectdojo-django.security.svc"
|
|
APP_URL = "http://demo-app.demo-app.svc:3000"
|
|
}
|
|
stages {
|
|
stage('Install & Test') {
|
|
steps {
|
|
container('node') {
|
|
sh 'npm install --no-audit --no-fund'
|
|
sh 'npm test'
|
|
}
|
|
}
|
|
}
|
|
stage('SAST — Semgrep audit') {
|
|
steps {
|
|
container('semgrep') {
|
|
sh 'semgrep scan --config auto --json --output semgrep.json --metrics=off || true'
|
|
sh 'semgrep scan --config auto --quiet || true'
|
|
}
|
|
}
|
|
}
|
|
stage('SAST Gate') {
|
|
steps {
|
|
container('semgrep') {
|
|
// Deterministic gate: vulnerable.js is legacy-excluded, everything else must be clean
|
|
sh 'semgrep scan --config .semgrep-gate.yml --error --exclude=vulnerable.js --metrics=off src'
|
|
}
|
|
}
|
|
}
|
|
stage('Secrets — Gitleaks') {
|
|
steps {
|
|
container('gitleaks') {
|
|
sh 'gitleaks detect --source . --no-git --report-format json --report-path gitleaks.json || true'
|
|
}
|
|
}
|
|
}
|
|
stage('SCA — Trivy deps') {
|
|
steps {
|
|
container('trivy') {
|
|
sh 'trivy fs --scanners vuln,license --format json --output trivy-fs.json .'
|
|
sh 'trivy fs --scanners vuln --severity HIGH,CRITICAL .'
|
|
}
|
|
}
|
|
}
|
|
stage('SonarQube') {
|
|
steps {
|
|
container('sonar') {
|
|
sh 'sonar-scanner -Dsonar.host.url=$SONAR_HOST -Dsonar.token=$SONAR_TOKEN -Dsonar.qualitygate.wait=false'
|
|
}
|
|
}
|
|
}
|
|
stage('Build — Kaniko') {
|
|
steps {
|
|
container('kaniko') {
|
|
sh '''/kaniko/executor \
|
|
--context "dir://$(pwd)" \
|
|
--dockerfile Dockerfile \
|
|
--destination "$REGISTRY/$IMAGE:$BUILD_NUMBER" \
|
|
--tar-path image.tar \
|
|
--insecure --insecure-pull'''
|
|
}
|
|
}
|
|
}
|
|
stage('Image Scan — Trivy') {
|
|
steps {
|
|
container('trivy') {
|
|
sh 'trivy image --input image.tar --format json --output trivy-image.json'
|
|
sh 'trivy image --input image.tar --severity HIGH,CRITICAL'
|
|
}
|
|
}
|
|
}
|
|
stage('SBOM — CycloneDX') {
|
|
steps {
|
|
container('trivy') {
|
|
sh 'trivy image --input image.tar --format cyclonedx --output sbom.cdx.json'
|
|
}
|
|
}
|
|
}
|
|
stage('IaC — Checkov') {
|
|
steps {
|
|
container('checkov') {
|
|
sh 'checkov -d . --quiet -o json > checkov.json || true'
|
|
sh 'checkov -d . --quiet --compact || true'
|
|
}
|
|
}
|
|
}
|
|
stage('Sign — Cosign') {
|
|
steps {
|
|
container('cosign') {
|
|
sh '''
|
|
wget -qO /tmp/cosign https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64
|
|
chmod +x /tmp/cosign
|
|
/tmp/cosign sign --key /keys/cosign.key --allow-insecure-registry --tlog-upload=false --yes "$REGISTRY/$IMAGE:$BUILD_NUMBER"
|
|
'''
|
|
}
|
|
}
|
|
}
|
|
stage('Deploy — GitOps bump') {
|
|
steps {
|
|
container('git') {
|
|
sh '''
|
|
git config --global --add safe.directory "$(pwd)"
|
|
git config user.name "Jenkins CI"
|
|
git config user.email "jenkins@demo.local"
|
|
sed -i "s/^ tag: .*/ tag: \\"$BUILD_NUMBER\\"/" helm/values.yaml
|
|
if git diff --quiet helm/values.yaml; then
|
|
echo "values.yaml already points at build $BUILD_NUMBER — nothing to push"
|
|
else
|
|
git add helm/values.yaml
|
|
git commit -m "ci: deploy build $BUILD_NUMBER [ci skip]"
|
|
git push http://demo:devsecops@gitea-http.platform.svc:3000/demo/demo-app.git HEAD:main
|
|
fi
|
|
'''
|
|
}
|
|
}
|
|
}
|
|
stage('DAST — OWASP ZAP') {
|
|
steps {
|
|
container('uploader') {
|
|
// wait until ArgoCD has rolled out the new build
|
|
sh '''
|
|
echo "Waiting for deployment to become healthy..."
|
|
for i in $(seq 1 60); do
|
|
if curl -sf "$APP_URL/health" > /dev/null; then echo "App is up"; break; fi
|
|
sleep 10
|
|
done
|
|
'''
|
|
}
|
|
container('zap') {
|
|
sh '''
|
|
rm -rf /zap/wrk && ln -s "$(pwd)" /zap/wrk
|
|
zap-baseline.py -t "$APP_URL" -J zap.json -I || true
|
|
'''
|
|
}
|
|
}
|
|
}
|
|
stage('Publish — DefectDojo') {
|
|
steps {
|
|
container('uploader') {
|
|
sh '''
|
|
upload() {
|
|
[ -f "$2" ] || { echo "skip $1 ($2 missing)"; return 0; }
|
|
curl -sf -X POST "$DD_URL/api/v2/import-scan/" \
|
|
-H "Authorization: Token $DD_TOKEN" \
|
|
-F scan_type="$1" \
|
|
-F file=@"$2" \
|
|
-F product_name=demo-app \
|
|
-F engagement_name="CI Build $BUILD_NUMBER" \
|
|
-F auto_create_context=true \
|
|
-F active=true -F verified=true \
|
|
> /dev/null && echo "uploaded: $1" || echo "FAILED: $1"
|
|
}
|
|
upload "Semgrep JSON Report" semgrep.json
|
|
upload "Gitleaks Scan" gitleaks.json
|
|
upload "Trivy Scan" trivy-fs.json
|
|
upload "Trivy Scan" trivy-image.json
|
|
upload "Checkov Scan" checkov.json
|
|
upload "ZAP Scan" zap.json
|
|
'''
|
|
}
|
|
}
|
|
}
|
|
}
|
|
post {
|
|
always {
|
|
archiveArtifacts artifacts: '*.json,sbom.cdx.json', allowEmptyArchive: true
|
|
}
|
|
success { echo 'Pipeline PASSED — all gates cleared. Findings aggregated in DefectDojo.' }
|
|
failure { echo 'Pipeline FAILED — a security gate blocked the release.' }
|
|
}
|
|
}
|