Initial commit: demo app with intentional vulnerabilities
14-stage Jenkins pipeline with real scanners (Semgrep, Gitleaks, Trivy, SonarQube, Checkov, ZAP, Cosign), Helm chart, DefectDojo integration. Includes CWE-89, CWE-79, CWE-798, CWE-22, CWE-330 for SAST/secrets demo.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
rules:
|
||||
- id: no-eval
|
||||
patterns:
|
||||
- pattern: eval(...)
|
||||
message: "eval() is banned — remote code execution risk (CWE-95)"
|
||||
severity: ERROR
|
||||
languages: [javascript]
|
||||
- id: no-child-process-exec
|
||||
patterns:
|
||||
- pattern: require('child_process').exec(...)
|
||||
message: "child_process.exec() with dynamic input is banned — command injection risk (CWE-78)"
|
||||
severity: ERROR
|
||||
languages: [javascript]
|
||||
- id: no-new-function
|
||||
patterns:
|
||||
- pattern: new Function(...)
|
||||
message: "new Function() is banned — code injection risk (CWE-95)"
|
||||
severity: ERROR
|
||||
languages: [javascript]
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# Build stage: standard node image (root, npm can write). Runtime: Chainguard zero-CVE base.
|
||||
FROM public.ecr.aws/docker/library/node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
COPY src/ ./src/
|
||||
|
||||
FROM cgr.dev/chainguard/node:latest
|
||||
WORKDIR /app
|
||||
COPY --from=build /app ./
|
||||
EXPOSE 3000
|
||||
CMD ["src/server.js"]
|
||||
Vendored
+243
@@ -0,0 +1,243 @@
|
||||
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"]
|
||||
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/latest/download/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.' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v2
|
||||
name: demo-app
|
||||
version: 1.0.0
|
||||
description: DevSecOps demo application
|
||||
@@ -0,0 +1,44 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Chart.Name }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||
team: platform-demo
|
||||
environment: demo
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||
team: platform-demo
|
||||
environment: demo
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: [ALL]
|
||||
resources: {{ toYaml .Values.resources | nindent 12 }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Chart.Name }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: 3000
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||
@@ -0,0 +1,15 @@
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: 10.0.1.215:30500/demo-app
|
||||
tag: "1"
|
||||
pullPolicy: IfNotPresent
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3000
|
||||
resources:
|
||||
requests:
|
||||
memory: 64Mi
|
||||
cpu: 50m
|
||||
limits:
|
||||
memory: 128Mi
|
||||
cpu: 200m
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "demo-app",
|
||||
"version": "1.0.0",
|
||||
"description": "DevSecOps demo application",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"test": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "4.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^6.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
sonar.projectKey=demo-app
|
||||
sonar.projectName=Demo Application
|
||||
sonar.sources=src
|
||||
sonar.tests=test
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
@@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.get('/health', (req, res) => res.json({ status: 'healthy' }));
|
||||
app.get('/ready', (req, res) => res.json({ status: 'ready' }));
|
||||
|
||||
app.get('/api/users/:id', (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) return res.status(400).json({ error: 'Invalid ID' });
|
||||
res.json({ id, name: `User ${id}`, role: 'viewer' });
|
||||
});
|
||||
|
||||
app.post('/api/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) return res.status(400).json({ error: 'Missing credentials' });
|
||||
res.json({ token: 'demo-token', user: username });
|
||||
});
|
||||
|
||||
if (require.main === module) {
|
||||
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
||||
}
|
||||
module.exports = app;
|
||||
@@ -0,0 +1,32 @@
|
||||
// INTENTIONAL VULNERABILITIES FOR DEMO — DO NOT USE IN PRODUCTION
|
||||
// This module simulates "legacy code": full security scans include it
|
||||
// (so DefectDojo shows real findings), but the blocking SAST gate excludes it.
|
||||
|
||||
// CWE-89: SQL Injection
|
||||
function getUserById(db, userId) {
|
||||
return db.query(`SELECT * FROM users WHERE id = '${userId}'`);
|
||||
}
|
||||
|
||||
// CWE-79: Cross-Site Scripting
|
||||
function renderGreeting(name) {
|
||||
return `<h1>Welcome, ${name}!</h1>`;
|
||||
}
|
||||
|
||||
// CWE-798: Hardcoded Credentials
|
||||
const API_KEY = "sk-demo-hardcoded-secret-12345";
|
||||
const DB_PASSWORD = "admin123";
|
||||
const AWS_ACCESS_KEY_ID = "AKIAZZDEMO0SECRETKEY";
|
||||
const AWS_SECRET_ACCESS_KEY = "wJalrXUtnFDEMO/K7MDENG/bPxRfiCYdemoSECRET";
|
||||
|
||||
// CWE-22: Path Traversal
|
||||
function readFile(basePath, filename) {
|
||||
const fs = require('fs');
|
||||
return fs.readFileSync(basePath + '/' + filename, 'utf8');
|
||||
}
|
||||
|
||||
// CWE-330: Insecure Randomness
|
||||
function generateToken() {
|
||||
return Math.random().toString(36).substring(2);
|
||||
}
|
||||
|
||||
module.exports = { getUserById, renderGreeting, readFile, generateToken, API_KEY };
|
||||
@@ -0,0 +1,21 @@
|
||||
const request = require('supertest');
|
||||
const app = require('../src/server');
|
||||
|
||||
describe('Health endpoints', () => {
|
||||
test('GET /health returns healthy', async () => {
|
||||
const res = await request(app).get('/health');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.status).toBe('healthy');
|
||||
});
|
||||
|
||||
test('GET /ready returns ready', async () => {
|
||||
const res = await request(app).get('/ready');
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
test('GET /api/users/:id returns user', async () => {
|
||||
const res = await request(app).get('/api/users/1');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.id).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user