diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3f59c1b2..b5135898 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -412,6 +412,9 @@ jobs: run: | # Install dummy-ssh app helm -n demo-apps install dummy-ssh ./demo-apps/dummy-ssh/ --wait + # Install plain nginx server + kubectl create deployment --image nginx:alpine nginx --namespace demo-apps + kubectl expose deployment nginx --port 80 --namespace demo-apps - name: "nmap Integration Tests" run: | helm -n integration-tests install nmap ./scanners/nmap/ --set="parserImage.tag=sha-$(git rev-parse --short HEAD)" @@ -432,6 +435,11 @@ jobs: helm -n integration-tests install ssh-scan ./scanners/ssh_scan/ --set="parserImage.tag=sha-$(git rev-parse --short HEAD)" cd tests/integration/ npx jest --ci --color ssh-scan + - name: "zap Integration Tests" + run: | + helm -n integration-tests install zap ./scanners/zap/ --set="parserImage.tag=sha-$(git rev-parse --short HEAD)" + cd tests/integration/ + npx jest --ci --color zap - name: Inspect Post Failure if: failure() run: | diff --git a/scanners/amass/templates/amass-scan-type.yaml b/scanners/amass/templates/amass-scan-type.yaml index 6387eb03..a52b7cb7 100644 --- a/scanners/amass/templates/amass-scan-type.yaml +++ b/scanners/amass/templates/amass-scan-type.yaml @@ -10,7 +10,9 @@ spec: location: "/home/securecodebox/amass-results.jsonl" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: OnFailure diff --git a/scanners/amass/values.yaml b/scanners/amass/values.yaml index f41a8499..2cf199b6 100644 --- a/scanners/amass/values.yaml +++ b/scanners/amass/values.yaml @@ -4,6 +4,7 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} # scannerJob: # resources: diff --git a/scanners/kube-hunter/templates/kubehunter-scan-type.yaml b/scanners/kube-hunter/templates/kubehunter-scan-type.yaml index 34ecc29f..dff32f2b 100644 --- a/scanners/kube-hunter/templates/kubehunter-scan-type.yaml +++ b/scanners/kube-hunter/templates/kubehunter-scan-type.yaml @@ -8,7 +8,9 @@ spec: location: '/home/securecodebox/kube-hunter-results.json' jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: Never diff --git a/scanners/kube-hunter/values.yaml b/scanners/kube-hunter/values.yaml index 68cedd55..c70b3bad 100644 --- a/scanners/kube-hunter/values.yaml +++ b/scanners/kube-hunter/values.yaml @@ -4,4 +4,5 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} diff --git a/scanners/ncrack/templates/ncrack-scan-type.yaml b/scanners/ncrack/templates/ncrack-scan-type.yaml index 84a6dc9e..13e25e52 100644 --- a/scanners/ncrack/templates/ncrack-scan-type.yaml +++ b/scanners/ncrack/templates/ncrack-scan-type.yaml @@ -8,7 +8,9 @@ spec: location: "/home/securecodebox/ncrack-results.xml" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} backoffLimit: 3 template: spec: diff --git a/scanners/ncrack/values.yaml b/scanners/ncrack/values.yaml index 8be7b1f5..cec6e9c9 100644 --- a/scanners/ncrack/values.yaml +++ b/scanners/ncrack/values.yaml @@ -4,6 +4,7 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} extraVolumes: [] extraVolumeMounts: [] diff --git a/scanners/nikto/templates/nikto-scan-type.yaml b/scanners/nikto/templates/nikto-scan-type.yaml index f6d0066d..e59604bb 100644 --- a/scanners/nikto/templates/nikto-scan-type.yaml +++ b/scanners/nikto/templates/nikto-scan-type.yaml @@ -8,7 +8,9 @@ spec: location: '/home/securecodebox/nikto-results.json' jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: Never diff --git a/scanners/nikto/values.yaml b/scanners/nikto/values.yaml index a7ecdec8..45640420 100644 --- a/scanners/nikto/values.yaml +++ b/scanners/nikto/values.yaml @@ -4,6 +4,7 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} # scannerJob: # resources: diff --git a/scanners/nmap/templates/nmap-scan-type.yaml b/scanners/nmap/templates/nmap-scan-type.yaml index e273234b..9be99c02 100644 --- a/scanners/nmap/templates/nmap-scan-type.yaml +++ b/scanners/nmap/templates/nmap-scan-type.yaml @@ -8,7 +8,9 @@ spec: location: "/home/securecodebox/nmap-results.xml" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} backoffLimit: 3 template: spec: diff --git a/scanners/nmap/values.yaml b/scanners/nmap/values.yaml index 0ebd1b90..1413156d 100644 --- a/scanners/nmap/values.yaml +++ b/scanners/nmap/values.yaml @@ -4,6 +4,7 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} # scannerJob: # resources: diff --git a/scanners/ssh_scan/templates/ssh-scan-scan-type.yaml b/scanners/ssh_scan/templates/ssh-scan-scan-type.yaml index 787c16cd..f06cf0c6 100644 --- a/scanners/ssh_scan/templates/ssh-scan-scan-type.yaml +++ b/scanners/ssh_scan/templates/ssh-scan-scan-type.yaml @@ -9,7 +9,9 @@ spec: location: "/home/securecodebox/ssh-scan-results.json" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: OnFailure diff --git a/scanners/ssh_scan/values.yaml b/scanners/ssh_scan/values.yaml index b36eb3ba..3304a1fb 100644 --- a/scanners/ssh_scan/values.yaml +++ b/scanners/ssh_scan/values.yaml @@ -4,6 +4,7 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} # scannerJob: # resources: diff --git a/scanners/sslyze/templates/sslyze-scan-type.yaml b/scanners/sslyze/templates/sslyze-scan-type.yaml index a5d48cce..4536159c 100644 --- a/scanners/sslyze/templates/sslyze-scan-type.yaml +++ b/scanners/sslyze/templates/sslyze-scan-type.yaml @@ -8,7 +8,9 @@ spec: location: '/home/securecodebox/sslyze-results.json' jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: OnFailure diff --git a/scanners/sslyze/values.yaml b/scanners/sslyze/values.yaml index 2a59b032..4f3b8e38 100644 --- a/scanners/sslyze/values.yaml +++ b/scanners/sslyze/values.yaml @@ -4,6 +4,7 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} # scannerJob: # resources: diff --git a/scanners/test-scan/templates/test-scan-scan-type.yaml b/scanners/test-scan/templates/test-scan-scan-type.yaml index 9cc0b25d..72053da3 100644 --- a/scanners/test-scan/templates/test-scan-scan-type.yaml +++ b/scanners/test-scan/templates/test-scan-scan-type.yaml @@ -8,7 +8,9 @@ spec: location: "/home/securecodebox/hello-world.txt" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} backoffLimit: 3 template: spec: diff --git a/scanners/test-scan/values.yaml b/scanners/test-scan/values.yaml index 2bc4f998..f4c510f6 100644 --- a/scanners/test-scan/values.yaml +++ b/scanners/test-scan/values.yaml @@ -4,6 +4,7 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} # scannerJob: # resources: diff --git a/scanners/trivy/templates/trivy-scan-type.yaml b/scanners/trivy/templates/trivy-scan-type.yaml index 200318f1..854ec819 100644 --- a/scanners/trivy/templates/trivy-scan-type.yaml +++ b/scanners/trivy/templates/trivy-scan-type.yaml @@ -9,7 +9,9 @@ spec: location: "/home/securecodebox/trivy-results.json" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: OnFailure diff --git a/scanners/trivy/values.yaml b/scanners/trivy/values.yaml index 165ac509..c6e25b8d 100644 --- a/scanners/trivy/values.yaml +++ b/scanners/trivy/values.yaml @@ -4,6 +4,7 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} # scannerJob: # resources: diff --git a/scanners/wpscan/templates/wpscan-scan-type.yaml b/scanners/wpscan/templates/wpscan-scan-type.yaml index 9da6b8f0..530f9d03 100644 --- a/scanners/wpscan/templates/wpscan-scan-type.yaml +++ b/scanners/wpscan/templates/wpscan-scan-type.yaml @@ -9,7 +9,9 @@ spec: location: "/home/securecodebox/wpscan-results.json" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: OnFailure diff --git a/scanners/wpscan/values.yaml b/scanners/wpscan/values.yaml index f6a735fc..6099ed4d 100644 --- a/scanners/wpscan/values.yaml +++ b/scanners/wpscan/values.yaml @@ -4,6 +4,7 @@ parserImage: tag: null scannerJob: + ttlSecondsAfterFinished: null resources: {} # scannerJob: # resources: diff --git a/scanners/zap/templates/zap-scan-type.yaml b/scanners/zap/templates/zap-scan-type.yaml index 12d77aae..b04a1210 100644 --- a/scanners/zap/templates/zap-scan-type.yaml +++ b/scanners/zap/templates/zap-scan-type.yaml @@ -8,13 +8,15 @@ spec: location: "/home/securecodebox/zap-results.json" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: Never containers: - name: zap-baseline - image: owasp/zap2docker-stable:2.9.0 + image: owasp/zap2docker-weekly:w2020-09-15 command: - "zap-baseline.py" # Force Zap to always return a zero exit code. k8s would otherwise try to restart zap. @@ -42,13 +44,15 @@ spec: location: "/home/securecodebox/zap-results.json" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: Never containers: - name: zap-api-scan - image: owasp/zap2docker-weekly:w2020-09-08 + image: owasp/zap2docker-weekly:w2020-09-15 command: - "zap-api-scan.py" # Force Zap to always return a zero exit code. k8s would otherwise try to restart zap. @@ -76,13 +80,15 @@ spec: location: "/home/securecodebox/zap-results.json" jobTemplate: spec: - ttlSecondsAfterFinished: 10 + {{- if .Values.scannerJob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.scannerJob.ttlSecondsAfterFinished }} + {{- end }} template: spec: restartPolicy: Never containers: - name: zap-full-scan - image: owasp/zap2docker-weekly:w2020-09-08 + image: owasp/zap2docker-weekly:w2020-09-15 command: - "zap-full-scan.py" # Force Zap to always return a zero exit code. k8s would otherwise try to restart zap. diff --git a/tests/integration/helpers.js b/tests/integration/helpers.js index 7beba406..7781a7cd 100644 --- a/tests/integration/helpers.js +++ b/tests/integration/helpers.js @@ -5,11 +5,11 @@ kc.loadFromDefault(); const k8sCRDApi = kc.makeApiClient(k8s.CustomObjectsApi); const k8sBatchApi = kc.makeApiClient(k8s.BatchV1Api); +const k8sPodsApi = kc.makeApiClient(k8s.CoreV1Api); const namespace = "integration-tests"; -const sleep = (duration) => - new Promise((resolve) => setTimeout(resolve, duration * 1000)); +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms * 1000)); async function deleteScan(name) { await k8sCRDApi.deleteNamespacedCustomObject( @@ -33,28 +33,80 @@ async function getScan(name) { return scan; } +async function displayAllLogsForJob(jobName) { + console.log(`Listing logs for Job '${jobName}':`); + const { + body: { items: pods }, + } = await k8sPodsApi.listNamespacedPod( + namespace, + true, + undefined, + undefined, + undefined, + `job-name=${jobName}` + ); + + if (pods.length === 0) { + console.log(`No Pods found for Job '${jobName}'`); + } + + for (const pod of pods) { + console.log( + `Listing logs for Job '${jobName}' > Pod '${pod.metadata.name}':` + ); + + for (const container of pod.spec.containers) { + try { + const response = await k8sPodsApi.readNamespacedPodLog( + pod.metadata.name, + namespace, + container.name + ); + console.log(`Container ${container.name}:`); + console.log(response.body); + } catch (exception) { + console.error( + `Failed to display logs of container ${container.name}: ${exception.body.message}` + ); + } + } + } +} + async function logJobs() { try { const { body: jobs } = await k8sBatchApi.listNamespacedJob(namespace); + console.log("Logging spec & status of jobs in namespace"); + for (const job of jobs.items) { console.log(`Job: '${job.metadata.name}' Spec:`); - console.dir(job.spec); + console.log(JSON.stringify(job.spec, null, 2)); console.log(`Job: '${job.metadata.name}' Status:`); - console.dir(job.status); + console.log(JSON.stringify(job.status, null, 2)); + + await displayAllLogsForJob(job.metadata.name); } } catch (error) { - console.info(`Failed to list Jobs'`); + console.error("Failed to list Jobs"); + console.error(error); } } +async function disasterRecovery(scanName) { + const scan = await getScan(scanName); + console.error("Last Scan State:"); + console.dir(scan); + await logJobs(); +} + /** * * @param {string} name name of the scan. Actual name will be sufixed with a random number to avoid conflicts * @param {string} scanType type of the scan. Must match the name of a ScanType CRD * @param {string[]} parameters cli argument to be passed to the scanner * @param {number} timeout in seconds - * @returns {scan.findings} returns findings { categories, severities, count } + * @returns {scan.findings} returns findings { categories, severities, count } */ async function scan(name, scanType, parameters = [], timeout = 180) { const scanDefinition = { @@ -85,22 +137,22 @@ async function scan(name, scanType, parameters = [], timeout = 180) { const { status } = await getScan(actualName); if (status && status.state === "Done") { + // Wait a couple seconds to give kubernetes more time to update the fields + await sleep(2); + const { status } = await getScan(actualName); await deleteScan(actualName); return status.findings; } else if (status && status.state === "Errored") { - await deleteScan(actualName); + console.error("Scan Errored"); + await disasterRecovery(actualName); + throw new Error( `Scan failed with description "${status.errorDescription}"` ); } } - console.error("Scan Timed out!"); - - const scan = await getScan(actualName); - console.log("Last Scan State:"); - console.dir(scan); - await logJobs(); + await disasterRecovery(actualName); throw new Error("timed out while waiting for scan results"); } diff --git a/tests/integration/scanner/zap.test.js b/tests/integration/scanner/zap.test.js new file mode 100644 index 00000000..9d4f601d --- /dev/null +++ b/tests/integration/scanner/zap.test.js @@ -0,0 +1,25 @@ +const { scan } = require("../helpers"); + +test( + "zap baseline scan against a plain nginx container should only find couple findings", + async () => { + const { categories, severities } = await scan( + "zap-nginx-baseline", + "zap-baseline", + ["-t", "http://nginx.demo-apps.svc"], + 60 * 4 + ); + + expect(categories).toMatchObject({ + "Content Security Policy (CSP) Header Not Set": 1, + 'Server Leaks Version Information via "Server" HTTP Response Header Field': 1, + "X-Content-Type-Options Header Missing": 1, + "X-Frame-Options Header Not Set": 1, + }); + expect(severities).toMatchObject({ + low: 3, + medium: 1, + }); + }, + 5 * 60 * 1000 +);