From b8196fb18d94b7a9ab89e68dabf5244afd4b086f Mon Sep 17 00:00:00 2001 From: devops Date: Thu, 17 Sep 2026 03:01:20 +0000 Subject: [PATCH] DAST (ZAP) + SLSA provenance attestation --- vars/cosignAttest.groovy | 72 ++++++++++++++++++++++++++++++++++++++ vars/dastScan.groovy | 28 +++++++++++++++ vars/devopsPipeline.groovy | 21 +++++++++-- vars/platformConfig.groovy | 3 +- 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 vars/cosignAttest.groovy create mode 100644 vars/dastScan.groovy diff --git a/vars/cosignAttest.groovy b/vars/cosignAttest.groovy new file mode 100644 index 0000000..9cbcba5 --- /dev/null +++ b/vars/cosignAttest.groovy @@ -0,0 +1,72 @@ +// Sirf signature ye batata hai ki "image hamne banayi". Provenance attestation +// ye batata hai ki "kaunse commit se, kis machine par, kab, kis pipeline se". +// Audit me yahi sabse pehle poochha jaata hai. +def call(Map info) { + withCredentials([string(credentialsId: 'vault-deployer-token', variable: 'VAULT_TOKEN')]) { + withEnv([ + "P_REPO=${info.repo}", "P_COMMIT=${info.commit}", + "P_BUILD=${info.build}", "P_JOB=${info.job}", "P_BUILDER=${info.builder}" + ]) { + sh ''' + set +x + umask 077 + WORK=$(mktemp -d); trap "rm -rf $WORK" EXIT + + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/platform/data/cosign" > $WORK/c.json + python3 - "$WORK" <<'PY' +import json, sys, os +w = sys.argv[1] +d = json.load(open(w + '/c.json'))['data']['data'] +for k, f in (('private_key', 'cosign.key'), ('password', 'pw')): + open(os.path.join(w, f), 'w').write(d[k]) +PY + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/$VAULT_MOUNT/data/registry" > $WORK/r.json + python3 - "$WORK" "$REGISTRY" <<'PY' +import base64, json, os, sys +w, reg = sys.argv[1], sys.argv[2] +d = json.load(open(w + '/r.json'))['data']['data'] +auth = base64.b64encode(('%s:%s' % (d['username'], d['password'])).encode()).decode() +os.makedirs(os.path.expanduser('~/.docker'), exist_ok=True) +json.dump({'auths': {reg: {'auth': auth}}}, open(os.path.expanduser('~/.docker/config.json'), 'w')) +PY + + # SLSA v0.2 predicate - kis source se kaise bani + python3 - "$WORK" <<'PY' +import json, os, sys, datetime +w = sys.argv[1] +pred = { + "builder": {"id": os.environ["P_BUILDER"]}, + "buildType": "https://ns31240276.ip-51-195-4.eu/devops-platform/jenkins@v1", + "invocation": { + "configSource": { + "uri": os.environ["P_REPO"], + "digest": {"sha1": os.environ["P_COMMIT"]}, + "entryPoint": os.environ["P_JOB"], + }, + "environment": {"buildNumber": os.environ["P_BUILD"]}, + }, + "metadata": { + "buildInvocationId": os.environ["P_BUILD"], + "buildFinishedOn": datetime.datetime.now(datetime.timezone.utc) + .strftime("%Y-%m-%dT%H:%M:%SZ"), + "completeness": {"parameters": True, "environment": False, "materials": True}, + "reproducible": False, + }, + "materials": [{ + "uri": os.environ["P_REPO"], + "digest": {"sha1": os.environ["P_COMMIT"]}, + }], +} +open(w + '/predicate.json', 'w').write(json.dumps(pred, indent=2)) +PY + + COSIGN_PASSWORD=$(cat $WORK/pw) cosign attest \ + --key $WORK/cosign.key --yes --tlog-upload=false \ + --type slsaprovenance --predicate $WORK/predicate.json \ + "$FULL_IMAGE" + rm -f ~/.docker/config.json + echo " provenance attach ho gaya: $FULL_IMAGE" + ''' + } + } +} diff --git a/vars/dastScan.groovy b/vars/dastScan.groovy new file mode 100644 index 0000000..374333f --- /dev/null +++ b/vars/dastScan.groovy @@ -0,0 +1,28 @@ +// Chalti hui app par hamla karke dekhta hai (dynamic testing). +// SAST code padhta hai, Trivy packages dekhta hai - ye dono wo nahi pakadte +// jo sirf running app me dikhta hai: missing security headers, khuli +// directory listing, cookie flags, etc. +def call() { + sh ''' + set +e + CID=$(docker run -d --name "dast-$BUILD_NUMBER" "$FULL_IMAGE") + trap "docker rm -f $CID >/dev/null 2>&1" EXIT + sleep 6 + + mkdir -p zap-report && chmod 777 zap-report + + # ZAP app ke hi network namespace me chalta hai - to 127.0.0.1 par + # app seedha mil jaata hai, koi extra network banane ki zarurat nahi. + docker run --rm \ + --network "container:$CID" \ + -v "$PWD/zap-report":/zap/wrk:rw \ + "$IMG_ZAP" zap-baseline.py \ + -t "http://127.0.0.1:${APP_PORT}" \ + -r zap-report.html -J zap-report.json \ + -I -m 2 + RC=$? + echo " zap exit: $RC (0=clean, 1=FAIL mila, 2=warning only)" + exit $RC + ''' + archiveArtifacts artifacts: 'zap-report/zap-report.*', allowEmptyArchive: true +} diff --git a/vars/devopsPipeline.groovy b/vars/devopsPipeline.groovy index f03290e..467dba1 100644 --- a/vars/devopsPipeline.groovy +++ b/vars/devopsPipeline.groovy @@ -25,6 +25,7 @@ def call(Map cfg = [:]) { cfg.inventory = cfg.inventory ?: 'deploy/inventory.ini' cfg.playbook = cfg.playbook ?: 'deploy/deploy.yml' cfg.runSonar = cfg.containsKey('runSonar') ? cfg.runSonar : true + cfg.runDast = cfg.containsKey('runDast') ? cfg.runDast : true cfg.runDeploy = cfg.containsKey('runDeploy') ? cfg.runDeploy : true cfg.approval = cfg.containsKey('approval') ? cfg.approval : true cfg.gitops = cfg.containsKey('gitops') ? cfg.gitops : false @@ -80,7 +81,8 @@ def call(Map cfg = [:]) { "HEALTH_PATH=${cfg.healthPath}", "INVENTORY=${cfg.inventory}", "PLAYBOOK=${cfg.playbook}", "IMG_GITLEAKS=${P.gitleaks}", "IMG_TRIVY=${P.trivy}", "IMG_SYFT=${P.syft}", - "IMG_SCANNER=${P.sonarScanner}", "TEST_IMAGE=${cfg.testImage}" + "IMG_SCANNER=${P.sonarScanner}", "TEST_IMAGE=${cfg.testImage}", + "IMG_ZAP=${P.zap}" ] currentBuild.displayName = "#${env.BUILD_NUMBER} ${imageTag}" echo "module=${cfg.module} node=${env.NODE_NAME} branch=${branch} " + @@ -172,6 +174,10 @@ assert r['version'] == '${IMAGE_TAG}', 'version mismatch' ''' } + if (cfg.runDast) { + stage('DAST (ZAP)') { dastScan() } + } + stage('Push') { withCredentials([string(credentialsId: 'vault-builder-token', variable: 'VAULT_TOKEN')]) { sh ''' @@ -199,7 +205,18 @@ assert r['version'] == '${IMAGE_TAG}', 'version mismatch' // ============ Sign: controller par (key builder ko nahi milti) ============ if (!isRollback) { node(P.deployLabel) { - withEnv(commonEnv) { stage('Sign image') { cosignSign() } } + withEnv(commonEnv) { + stage('Sign image') { cosignSign() } + stage('Provenance attest') { + cosignAttest([ + repo : scm.userRemoteConfigs ? scm.userRemoteConfigs[0].url : 'unknown', + commit : env.GIT_COMMIT ?: 'unknown', + build : env.BUILD_NUMBER, + job : env.JOB_NAME, + builder: "https://ns31240276.ip-51-195-4.eu:8443/${env.JOB_NAME}" + ]) + } + } } } diff --git a/vars/platformConfig.groovy b/vars/platformConfig.groovy index eb7665d..3ab9fdc 100644 --- a/vars/platformConfig.groovy +++ b/vars/platformConfig.groovy @@ -15,6 +15,7 @@ def call() { gitleaks : 'zricethezav/gitleaks:v8.21.2', trivy : 'aquasec/trivy:0.58.1', syft : 'anchore/syft:v1.18.1', - sonarScanner: 'sonarsource/sonar-scanner-cli:11' + sonarScanner: 'sonarsource/sonar-scanner-cli:11', + zap : 'ghcr.io/zaproxy/zaproxy:2.16.1' ] }