diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..dcf038e8 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,169 @@ +@Library('flexy') _ + +// rename build +def userId = currentBuild.rawBuild.getCause(hudson.model.Cause$UserIdCause)?.userId +if (userId) { + currentBuild.displayName = userId +} + +def RETURNSTATUS = "default" +def output = "" +pipeline { + agent none + parameters { + string(name: 'BUILD_NUMBER', defaultValue: '', description: 'Build number of job that has installed the cluster.') + string(name: "DAST_IMAGE", defaultValue: "quay.io/redhatproductsecurity/rapidast", description: 'Image to use as the base for running zap.') + string(name: "DAST_IMAGE_TAG", defaultValue: "latest", description: 'Image tag to use as the base for running zap.') + string(name: 'DAST_TOOL_URL', defaultValue: 'https://github.com/RedHatProductSecurity/rapidast.git', description: 'Rapidast tool github url .') + string(name: 'DAST_TOOL_BRANCH', defaultValue: 'development', description: 'Rapdiast tool github barnch to checkout.') + string(name: 'SE_TOOL_URL', defaultValue: 'https://github.com/openshift-qe/ocpqe-security-tools.git', description: 'OCPQE security tool github url.') + string(name: 'SEC_TOOL_BRANCH', defaultValue: 'main', description: 'OCPQE security tool github barnch to checkout.') + string(name: 'API_URL_LIST', defaultValue: 'admissionregistration.k8s.io/v1', description: + '''List of api files to scan against. + Api docs you can find using kubectl api-versions''') + string(name: 'POLICY_FILE', defaultValue: 'API-scan-minimal', description: 'List of policies to check apis against.') + string(name:'JENKINS_AGENT_LABEL',defaultValue:'oc415',description: + ''' + scale-ci-static: for static agent that is specific to scale-ci, useful when the jenkins dynamic agent isn't stable
+ 4.y: oc4y || mac-installer || rhel8-installer-4y
+ e.g, for 4.8, use oc48 || mac-installer || rhel8-installer-48
+ 3.11: ansible-2.6
+ 3.9~3.10: ansible-2.4
+ 3.4~3.7: ansible-2.4-extra || ansible-2.3
+ ''' + ) + text(name: 'ENV_VARS', defaultValue: '', description:'''

+ Enter list of additional (optional) Env Vars you'd want to pass to the script, one pair on each line.
+ See https://github.com/cloud-bulldozer/kraken-hub/blob/main/docs/cerberus.md for list of variables to pass
+ e.g.
+ SOMEVAR1='env-test'
+ SOMEVAR2='env2-test'
+ ...
+ SOMEVARn='envn-test'
+

''' + ) + } + stages { + stage('SSMl Run'){ + agent { + kubernetes { + cloud 'PSI OCP-C1 agents' + yaml """\ + apiVersion: v1 + kind: Pod + metadata: + labels: + label: ${JENKINS_AGENT_LABEL} + spec: + containers: + - name: "jnlp" + image: "image-registry.openshift-image-registry.svc:5000/aosqe/cucushift:${JENKINS_AGENT_LABEL}-rhel8" + resources: + requests: + memory: "8Gi" + cpu: "2" + limits: + memory: "8Gi" + cpu: "2" + imagePullPolicy: Always + workingDir: "/home/jenkins/ws" + tty: true + """.stripIndent() + } + } + steps{ + deleteDir() + checkout([ + $class: 'GitSCM', + branches: [[name: params.SEC_TOOL_BRANCH ]], + doGenerateSubmoduleConfigurations: false, + userRemoteConfigs: [[url: params.SE_TOOL_URL ] + ]]) + checkout([ + $class: 'GitSCM', + branches: [[name: params.DAST_TOOL_BRANCH ]], + doGenerateSubmoduleConfigurations: false, + extensions: [ + [$class: 'CloneOption', noTags: true, reference: '', shallow: true], + [$class: 'PruneStaleBranch'], + [$class: 'CleanCheckout'], + [$class: 'IgnoreNotifyCommit'], + [$class: 'RelativeTargetDirectory', relativeTargetDir: 'dast_tool'] + ], + userRemoteConfigs: [[url: params.DAST_TOOL_URL ]] + ]) + copyArtifacts( + filter: '', + fingerprintArtifacts: true, + projectName: 'ocp-common/Flexy-install', + selector: specific(params.BUILD_NUMBER), + target: 'flexy-artifacts' + ) + script { + buildinfo = readYaml file: "flexy-artifacts/BUILDINFO.yml" + currentBuild.displayName = "${currentBuild.displayName}-${params.BUILD_NUMBER}" + currentBuild.description = "Copying Artifact from Flexy-install build Flexy-install#${params.BUILD_NUMBER}" + buildinfo.params.each { env.setProperty(it.key, it.value) } + } + script { + RETURNSTATUS = sh(returnStatus: true, script: ''' + # Get ENV VARS Supplied by the user to this job and store in .env_override + echo "$ENV_VARS" > .env_override + # Export those env vars so they could be used by CI Job + set -a && source .env_override && set +a + mkdir -p ~/.kube + cp $WORKSPACE/flexy-artifacts/workdir/install-dir/auth/kubeconfig ~/.kube/config + ls + oc login -u kubeadmin -p $(cat $WORKSPACE/flexy-artifacts/workdir/install-dir/auth/kubeadmin-password) + HELM_DIR=$(mktemp -d) + curl -sS -L https://get.helm.sh/helm-v3.11.2-linux-amd64.tar.gz | tar -xzC ${HELM_DIR}/ linux-amd64/helm + + ${HELM_DIR}/linux-amd64/helm version + + mv ${HELM_DIR}/linux-amd64/helm $WORKSPACE/helm + PATH=$PATH:$WORKSPACE + helm version + + cd dast + ls + export DAST_PATH=../dast_tool + set +e + ./deploy_ssml_api.sh + api_run_status=$? + + echo "api_run_status $api_run_status" + exit $api_run_status + + ''') + sh "echo $RETURNSTATUS" + archiveArtifacts( + artifacts: 'dast/results/**', + allowEmptyArchive: true, + fingerprint: true + ) + } + script{ + def status = "FAIL" + sh "echo $RETURNSTATUS" + if( RETURNSTATUS.toString() == "0") { + status = "PASS" + }else { + currentBuild.result = "FAILURE" + } + } + } + } + } + post { + always { + script { + build job: 'scale-ci/e2e-benchmarking-multibranch-pipeline/post-to-slack', + parameters: [ + string(name: 'BUILD_NUMBER', value: BUILD_NUMBER), string(name: 'WORKLOAD', value: "ssml"), + text(name: "BUILD_URL", value: env.BUILD_URL), string(name: 'BUILD_ID', value: currentBuild.number.toString()), + string(name: 'RESULT', value:currentBuild.currentResult) + ], propagate: false + } + } + } +} \ No newline at end of file diff --git a/_helpers.tpl b/_helpers.tpl new file mode 100644 index 00000000..1618d407 --- /dev/null +++ b/_helpers.tpl @@ -0,0 +1,66 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "rapidast-chart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "rapidast-chart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "rapidast-chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create job spec +*/}} + +{{- define "rapidast-chart.job" -}} +template: + metadata: + name: {{ .Release.Name }}-job + spec: + containers: + - name: "{{ .Chart.Name }}" + securityContext: {{ .Values.secContext }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + # Since Helm configmap cannot handle the dash character but the policy name undner scanPolicyXML' in 'values.yaml' is 'helm-custom-scan', the dest file name of the copy command is 'helm-custom-scan.policy'. + # This file will be used if the rapidast config specifies 'helm-custom-scan' for the activeScan policy. + # Otherwise, '/home/rapidast/.ZAP/policies/API-scan-minimal.policy' will be used by default. + command: ["sh", "-c", "cp /helm/config/helmcustomscan.policy /opt/rapidast/scanners/zap/policies/helm-custom-scan.policy && rapidast.py --config /helm/config/rapidastconfig.yaml"] + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + {{- toYaml .Values.resources | nindent 8 }} + volumeMounts: + - name: config-volume + mountPath: /helm/config + - name: results-volume + mountPath: /zap/results/ + volumes: + - name: config-volume + configMap: + name: {{ .Release.Name }}-configmap + - name: results-volume + persistentVolumeClaim: + claimName: {{ .Values.pvc }} + restartPolicy: Never +{{- end }} diff --git a/deploy_ssml.sh b/deploy_ssml.sh new file mode 100755 index 00000000..9e27be65 --- /dev/null +++ b/deploy_ssml.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +oc label ns default security.openshift.io/scc.podSecurityLabelSync=false pod-security.kubernetes.io/enforce=privileged pod-security.kubernetes.io/audit=privileged pod-security.kubernetes.io/warn=privileged --overwrite + +export CONSOLE_URL=$(oc get routes console -n openshift-console -o jsonpath='{.spec.host}') + +export TOKEN=$(oc whoami -t) + +# path for local testing +#dast_tool_path=../rapidast/ +dast_tool_path=./dast_tool +echo "$CONSOLE_URL" +#curl -k "https://${CONSOLE_URL}/api/kubernetes/openapi/v2" -H "Cookie: openshift-session-token=${TOKEN}" -H "Accept: application/json" >> openapi.json +mkdir results +for api_doc in ${API_URL_LIST}; do + echo "api doc $api_doc" + export API_URL="https://raw.githubusercontent.com/paigerube14/ocp-qe-perfscale-ci/ssml/apidocs/$api_doc" + echo "api url: $API_URL" + #edit rapidast config file + envsubst < values.yaml.template > $dast_tool_path/helm/chart/value_test.yaml + + helm install rapidast $dast_tool_path/helm/chart -f $dast_tool_path/helm/chart/value_test.yaml + + # wait for pod to be completed or error + rapidast_pod=$(oc get pods -n default -l job-name=rapidast-job -o name) + echo "rapidast current pod $rapidast_pod" + oc wait --for=condition=Ready $rapidast_pod --timeout=120s + oc get $rapidast_pod -o 'jsonpath={..status.conditions}' + while [[ $(oc get $rapidast_pod -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') == "True" ]]; do + echo "sleeping 5" + sleep 5 + + done + mkdir results/$api_doc + cp $dast_tool_path/helm/chart/value_test.yaml results/$api_doc/value.yaml + + oc logs $rapidast_pod -n default >> results/$api_doc/pod_logs.out + + ./results.sh rapidast-pvc results/$api_doc + + phase=$(oc get $rapidast_pod -o jsonpath='{.status.phase}') + helm uninstall rapidast + oc delete pvc rapidast-pvc +done + +if [ $phase != "Succeeded" ]; then + echo "Pod $rapidast_pod failed. Look at pod logs in archives (results/*/pod_logs.out)" + exit 1 +fi diff --git a/deploy_ssml_api.sh b/deploy_ssml_api.sh new file mode 100755 index 00000000..85b88df7 --- /dev/null +++ b/deploy_ssml_api.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +export CONSOLE_URL=$(oc get routes console -n openshift-console -o jsonpath='{.spec.host}') + +export CLUSTER_NAME=$(oc get machineset -n openshift-machine-api -o=go-template='{{(index (index .items 0).metadata.labels "machine.openshift.io/cluster-api-cluster" )}}') + +export BASE_API_URL=$(oc get infrastructure -o jsonpath="{.items[*].status.apiServerURL}") +export TOKEN=$(oc whoami -t) +export NAMESPACE=${NAMESPACE:-default} + +oc label ns $NAMESPACE security.openshift.io/scc.podSecurityLabelSync=false pod-security.kubernetes.io/enforce=privileged pod-security.kubernetes.io/audit=privileged pod-security.kubernetes.io/warn=privileged --overwrite + +# path for local testing +#dast_tool_path=../rapidast/ +dast_tool_path=${DAST_PATH:-./dast_tool} +echo "$CONSOLE_URL" +#curl -k "https://${CONSOLE_URL}/api/kubernetes/openapi/v2" -H "Cookie: openshift-session-token=${TOKEN}" -H "Accept: application/json" >> openapi.json +mkdir results + +counter=0 +#for api_doc in $(kubectl api-versions); do +for api_doc in ${API_URL_LIST}; do + echo "api doc $api_doc" + # export API_URL="https://raw.githubusercontent.com/paigerube14/ocp-qe-perfscale-ci/ssml/apidocs/$api_doc" + if [[ "$api_doc" == *"/"* ]]; then + export API_URL="$BASE_API_URL/openapi/v3/apis/$api_doc" + else # e.g. 'v1' + export API_URL="$BASE_API_URL/openapi/v3/api/$api_doc" + fi + + echo "api url: $API_URL" + #edit rapidast config file + envsubst < values.yaml.template > $dast_tool_path/helm/chart/value_test.yaml + helm install rapidast $dast_tool_path/helm/chart -f $dast_tool_path/helm/chart/value_test.yaml + + # wait for pod to be completed or error + rapidast_pod=$(oc get pods -n default -l job-name=rapidast-job -o name) + echo "rapidast current pod $rapidast_pod" + oc wait --for=condition=Ready $rapidast_pod --timeout=120s + + folder_api_name=$(echo "$api_doc" | tr "/" .) + mkdir results/$folder_api_name + + #oc get $rapidast_pod -n default -o yaml >> results/$folder_api_name/pod_yaml.yaml + + oc get $rapidast_pod -o 'jsonpath={..status.conditions}' + while [[ $(oc get $rapidast_pod -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') == "True" ]]; do + echo "sleeping 5" + sleep 5 + + done + + #cp $dast_tool_path/helm/chart/value_test.yaml results/$folder_api_name/value.yaml + + oc logs $rapidast_pod -n default >> results/$folder_api_name/pod_logs.out + + ./results.sh rapidast-pvc results/$folder_api_name + ls results + + ls results/$folder_api_name + + phase=$(oc get $rapidast_pod -o jsonpath='{.status.phase}') + helm uninstall rapidast + oc delete pvc rapidast-pvc + (( counter++ )) +done + +python find_alert_types.py + +if [ $phase != "Succeeded" ]; then + echo "Pod $rapidast_pod failed. Look at pod logs in archives (results/*/pod_logs.out)" + exit 1 +fi diff --git a/find_alert_types.py b/find_alert_types.py new file mode 100644 index 00000000..9be919ea --- /dev/null +++ b/find_alert_types.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +import subprocess +import json + +# Invokes a given command and returns the stdout +def invoke(command): + try: + output = subprocess.check_output(command, shell=True, universal_newlines=True) + except subprocess.CalledProcessError as exc: + print("Status : FAIL", exc.returncode, exc.output) + return exc.returncode, exc.output + return 0, output + + +def get_results(folder_name): + + + try: + folder_zap = invoke(f'cat {folder_name}/*/*/*/zap-report.json') + if folder_zap[0] != 0: + return + zap_str = folder_zap[1] + except: + return + total_alerts = {"High": 0, "Medium": 0, "Low":0} + zap_json = json.loads(zap_str) + for site in zap_json['site']: + if "alerts" in site.keys(): + for alert in site['alerts']: + risk_type = alert['riskdesc'].split(" ")[0] + total_alerts[risk_type] += 1 + + print(f'total alerts for {folder_name} : ' + str(total_alerts)) + + +result_folder = "./results" +folders = invoke('ls ' + str(result_folder))[1].split('\n') +for folder in folders: + if folder != "": + get_results(result_folder + "/" +folder) + diff --git a/get_api_list.py b/get_api_list.py new file mode 100644 index 00000000..8de3a018 --- /dev/null +++ b/get_api_list.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +import time +import yaml +import subprocess +import sys +import json + +# Invokes a given command and returns the stdout +def invoke(command): + try: + output = subprocess.check_output(command, shell=True, universal_newlines=True) + except subprocess.CalledProcessError as exc: + print("Status : FAIL", exc.returncode, exc.output) + return exc.returncode, exc.output + return 0, output + +api_file_list = 'apilist' +invoke("mkdir "+ str(api_file_list)) + +folder_name = "apidocs/" +api_docs_file_names = invoke("ls "+ str(folder_name))[1].split("\n") +print("api_docs_file_names" + str(api_docs_file_names)) +for file_name in api_docs_file_names: + if file_name != "": + with open(folder_name + file_name) as f: + file_str = f.read() + file_json = json.loads(file_str) + path_list= [] + for path in file_json['paths'].keys(): + path_list.append(path) + with open(api_file_list +"/"+ file_name, "a+") as r: + path_list_str = '\n'.join(path_list) + r.write(path_list_str) diff --git a/results.sh b/results.sh new file mode 100755 index 00000000..b3604201 --- /dev/null +++ b/results.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Temp directory to store generated pod yaml +TMP_DIR=/tmp + +# Where to store sync'd results -- defaults to current dir +RESULTS_DIR=${2:-.} + +# Name for rapiterm pod +RANDOM_NAME=rapiterm-$RANDOM + +# Name of PVC in RapiDAST Resource, i.e. which PVC to mount to grab results +PVC=${1:-rapidast-pvc} + +IMAGE_REPOSITORY=quay.io/redhatproductsecurity/rapidast-term + +IMAGE_TAG=latest + +cat < $TMP_DIR/$RANDOM_NAME +apiVersion: v1 +kind: Pod +metadata: + name: $RANDOM_NAME +spec: + containers: + - name: terminal + image: '$IMAGE_REPOSITORY:$IMAGE_TAG' + command: ['sleep', '300'] + imagePullPolicy: Always + volumeMounts: + - name: results-volume + mountPath: /opt/rapidast/results + resources: + limits: + cpu: 100m + memory: 500Mi + requests: + cpu: 50m + memory: 100Mi + volumes: + - name: results-volume + persistentVolumeClaim: + claimName: $PVC +EOF + +kubectl apply -f $TMP_DIR/$RANDOM_NAME +rm $TMP_DIR/$RANDOM_NAME +kubectl wait --for=condition=Ready pod/$RANDOM_NAME +kubectl cp $RANDOM_NAME:/opt/rapidast/results $RESULTS_DIR +kubectl delete pod $RANDOM_NAME diff --git a/values.yaml.template b/values.yaml.template new file mode 100644 index 00000000..b357681b --- /dev/null +++ b/values.yaml.template @@ -0,0 +1,94 @@ +# Default values for rapidast-chart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +image: + repository: $DAST_IMAGE + pullPolicy: Always + tag: "$DAST_IMAGE_TAG" + +job: + cron: false + schedule: "0 22 * * *" # used when job.cron is true, e.g. at 10pm daily + +secContext: '{ "privileged": true }' +resources: {} + # limits: + # cpu: 400m + # memory: 1Gi + # requests: + # cpu: 200m + # memory: 500Mi + # It is recommended not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + + +pvc: rapidast-pvc + +# config is (currently) same as config file -- must be multiline string +rapidastConfig: | + config: + # WARNING: `configVersion` indicates the schema version of the config file. + # This value tells RapiDAST what schema should be used to read this configuration. + # Therefore you should only change it if you update the configuration to a newer schema + # It is intended to keep backward compatibility (newer RapiDAST running an older config) + configVersion: 5 + base_results_dir: "/opt/rapidast/results" + + # `application` contains data related to the application, not to the scans. + application: + shortName: "MyApp-1.0" + url: "$BASE_API_URL" + + # `general` is a section that will be applied to all scanners. + general: + + #authentication: + # type: "cookie" + # parameters: + # name: "openshift-session-token" + # value: "$TOKEN" # referring to a env defined in general.environ.envFile + + authentication: + type: "http_header" + parameters: + name: "Authorization" + value: "Bearer $TOKEN" + + container: + # currently supported: `podman` and `none` + type: "none" + + scanners: + zap: + # define a scan through the ZAP scanner + apiScan: + apis: + apiUrl: "$API_URL" + + results: "*stdout" + + passiveScan: + # optional list of passive rules to disable + disabledRules: "2,10015,10027,10096,10024" + + miscOptions: + enableUI: False + updateAddons: False + memMaxHeap: "6144m" + + activeScan: + # If no policy is chosen, a default ("API-scan-minimal") will be selected + # The list of policies can be found in scanners/zap/policies/ + policy: "$POLICY_FILE" + + report: + format: ["json", "html"] + # format: ["json","html","sarif","xml"] # default: "json" only + + overrideConfigs: + # to set the value 'default' for {namespace} in the API path + - formhandler.fields.field(0).fieldId=namespace + - formhandler.fields.field(0).value=$NAMESPACE \ No newline at end of file