# Use this template to integrate DeepFactor into your CI Pipeline
# Save this file in your project root as deepfactor.gitlab-ci.yml
# Include the deepfactor.gitlab-ci.yml by making the following
# entry in your .gitlab-ci.yml
#    include:
#       - local: deepfactor.gitlab-ci.yml
#
# Requirements:
# - Operational DeepFactor Portal https://docs.deepfactor.io/hc/en-us
# - A valid DF_API_KEY from your DeepFactor deployment
# - A valid DF_RUN_TOKEN from your DeepFactor deployment
#
# Variables set in the GitLab -> Settings -> CI/CD -> Variables
# - DF_API_KEY
# - DF_RUN_TOKEN
#
# Pipeline Variables
# - DF_APP - name of the application in DeepFactor
# - DF_COMPONENT - name of the component in DeepFactor
# - DF_VERSION - Version of the component can be the build/run number
# - DF_PORTAL_HOST - The FQDN of the DeepFactor portal
#




# This job installs the DeepFactor command line tool dfctl
# This should ideally be inclused in your test job in the before script
#
#  eg.
#   test:
#     stage: test
#     before_script:
#       - !reference [.install_dfctl,script]
#
# The dfctl command can be used to run any of your tests
#  eg.
#   script:
#     - npm install
#     - dfctl run -a $DF_APP -c $DF_COMPONENT --version $DF_VERSION --cmd npm run test
#
.install_dfctl:
  script:
    - |
      curl https://repo.deepfactor.io/install-dfctl.sh | sh -s  -- --no-docker-volume


# This job generates the DeepFactor Report with any vulnerabilties or issues
# detected by deepfactor.
# To add the deepfactor_report to you .gitlab-ci.yml add the following stage and the corresponding job
#
#    stages:
#      - deepfactor-report
#
#     deepfactor-report:
#       stage: deepfactor-report
#       when: delayed
#       start_in: 30 minutes # the delay would depend on the time it normally takes for a scan to complete on your application.
#     extends:
#       - .deepfactor_report
#
# The deepfactor report stage could be run as a scheduled job that runs on a daily basis
# and generates the artifacts in the report directory
.deepfactor_report:
  artifacts:
    paths:
      - report/
    reports:
      dast: report/df-dast-report.json
      dependency_scanning: report/df-dependency-report.json
  script:
    - |
      wget https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o jq
      chmod +x jq
      apt update && apt install -y curl jq wget
      if [[ -z "$DF_API_KEY" ]]; then
          echo "DF_API_KEY environment variable missing"
          exit 1
      fi
      if [[ -z "$DF_PORTAL_HOST" ]]; then
          echo "Param \"DF_PORTAL_HOST\" is missing"
          exit 1
      fi
      if [[ -z "$DF_APP" ]]; then
          echo "Param \"DF_APP\" is missing"
          exit 1
      fi
      if [[ -z "$DF_COMPONENT" ]]; then
          echo "Param \"DF_COMPONENT\" is missing"
          exit 1
      fi
      # check the portal url is valid
      DF_PORTAL_URL="https://${DF_PORTAL_HOST}"
      curl -kfs "${DF_PORTAL_URL}/api/health" > /dev/null
      status="$?"
      if [[ "$status" != "0" ]]; then
        echo "The param DeepFactor portal_url is incorrect or is not reachable"
        exit 1
      fi
      mkdir report
      # Get application
      application_id=$( curl -ks -X GET "${DF_PORTAL_URL}/api/services/v1/applications" \
                          -H "Authorization: Bearer ${DF_API_KEY}" | \
                          jq -r --arg v "${DF_APP}" '.data.apps[] | select (.name==$v) | .application_id' )

      echo "application_id=${application_id}"
      # Get components
      component_version_tuple=$( curl -ks -X GET \
                                  "${DF_PORTAL_URL}/api/services/v1/applications/${application_id}/components" \
                                  -H "Authorization: Bearer ${DF_API_KEY}" | \
                                  jq -r --arg c "${DF_COMPONENT}" \
                                  '.data.components[] | select (.name==$c) | {component_id,latest_component_version,security}')
      component_id=$( jq -r '.component_id' <<< "$component_version_tuple" )
      echo "component_id=${component_id}"
      component_security_summary=$( jq -r '.security' <<< "$component_version_tuple")
      summary=$(jq -r '{ total: .total, p1: .p1, p2: .p2, p3: .p3, p4: .p4 }' <<< "$component_security_summary")
      cat <<< "$summary" > report/deepfactor_summary.json
      alert_details=$(curl -ks -X GET "${DF_PORTAL_URL}/api/services/v1/alerts?application_id=${application_id}&component_id=${component_id}&version_type=latest" \
                          -H "Authorization: Bearer ${DF_API_KEY}" \
                          | jq -r --arg portal "${DF_PORTAL_URL}" \
                          '[.data.alerts[] | 
                          {
                            alertType: .alert_type,
                            alertRuleType: .alert_rule_type,
                            severity: .severity, 
                            title: .title ,
                            dfa: .dfa, 
                            link: ($portal + "/alerts/" + .dfa)
                          }]' )
      cat <<< "$alert_details" > report/deepfactor_alert_list.json
      if [[ -f "report/deepfactor_alert_list.json" ]]; then
          dep_alerts=$( cat report/deepfactor_alert_list.json | jq -c '.[] | select(.alertRuleType | contains("DEP"))' )
          for dfa in $(echo "$dep_alerts" | jq -r '.dfa'); do
              alert_occurrences=$(curl -ks -X GET "${DF_PORTAL_URL}/api/services/v1/alerts/${dfa}/occurrences?version_type=latest" \
                              -H "Authorization: Bearer ${DF_API_KEY}" \
                              | jq -r --arg portal "${DF_PORTAL_URL}" \
                              '{version:"2.0", vulnerabilities:[.data.occurrences[] | 
                              {
                                  id: .dep_sha1, 
                                  category: "dependency_scanning",
                                  name: .cve_name,
                                  description: .cve_description,
                                  severity: (if .severity=="P1" then "High" elif .severity=="P2" then "Medium"  elif .severity=="P3" then "Low" else "Unknown" end), 
                                  solution: "", 
                                  scanner: 
                                  { 
                                      id: "deepfactor",
                                      name:"DeepFactor",
                                      version: "1.0",
                                      vendor:{
                                          name: "DeepFactor"
                                      }
                                  },
                                  location:
                                  {
                                      file: .dep_file_path,
                                      dependency: {
                                          package: {
                                              name: (.dep_file_name|split(":")[0])
                                          },
                                          version: (.dep_file_name|split(":")[1])
                                      }
                                  },
                                  identifiers: [
                                      {
                                          type:"deepfactor",
                                          name: .dfa_occurrence_number,
                                          value: .dfa_occurrence_number,
                                          url: ($portal + "/alerts/"+.dfa+"?occurrence_id="+.dfa_occurrence_number+"&versionType=latest")
                                      }
                                  ],
                                  links:[
                                      {
                                          url: ($portal + "/alerts/"+.dfa+"?occurrence_id="+.dfa_occurrence_number+"&versionType=latest")
                                      }
                                  ]
                              }]}' )
          done
      fi
      if [[ ! -z "$alert_occurrences" ]]; then
        cat <<< "$alert_occurrences" > report/df-dependency-report.json
      fi
      if [[ -f "scan-data/scan_out.json" ]]; then
        scan_id=$( jq -r '.data.scan_id' < scan-data/scan_out.json )
        if [[ ! -z "${scan_id}" ]]; then
          dast_report=$(curl -ks -X GET "${DF_PORTAL_URL}/api/services/v1/scans/${scan_id}/result" \
                              -H "Authorization: Bearer ${DF_API_KEY}" \
                              | jq -r .data.webservice_scan_result)
          cat <<< "$dast_report" > report/df-dast-report.json
                    
        fi
      fi
      wget http://df-ci-assets-test1.s3-website-us-west-2.amazonaws.com/index.html -O report/index.html
      echo "${DF_FAIL_SEVERITY}"
      if [[ "${DF_FAIL_SEVERITY}" != 'disabled' ]]; then
        p1=$( jq '.p1' < report/deepfactor_summary.json )
        if [[ ${DF_FAIL_SEVERITY} == 'p1' ]] && [[ "$p1" -gt 0 ]]; then
          echo 'DeepFactor detected alerts above threshold P1. Failing build'
          exit 1
        fi
        p2=$( jq '.p2' < report/deepfactor_summary.json )
        if [[ "${DF_FAIL_SEVERITY}" == 'p2' ]] && [[ "$p2" -gt 0 || "$p1" -gt 0 ]]; then
          echo 'DeepFactor detected alerts above threshold P2. Failing build'
          exit 1
        fi
        p3=$( jq '.p3' < report/deepfactor_summary.json )
        if [[ "${DF_FAIL_SEVERITY}" == 'p3' ]] && [[ "$p3" -gt 0 ||  "$p2" -gt 0 ||  "$p1" -gt 0 ]]; then
          echo 'DeepFactor detected alerts above threshold P3. Failing build'
          exit 1
        fi
      fi

# This job runs a DeepFactor DAST scan against the specified deployment
# To add the DeepFactor DAST scan in your .gitlab-ci.yml add the following stage and the job
#   stages:
#     - deepfactor-dast-scan
#
#   deppfactor-dast-scan:
#     stage: deepfactor-dast-scan
#     extends: .deepfactor_webscan
#
# Variables
#   DF_APP - The name of the application in DeepFactor
#   DF_COMPONENT - The name of component in DeepFactor
#   DF_VERSION   - The component version
#   DF_SCAN_URL - The application web endpoint to run the DAST Scan against
#   DF_SCAN_TYPE - The scan type to use. Valid values are "web" or "api"
#   DF_SCAN_STRENGTH - The strength of the scan. Valid values are "Low","Medium" and "High"
#   DF_SCAN_API_DOCS_PATH - The relative path of the swagger or openapi document. Required for API Scan
#   DF_SCAN_AUTH_TYPE - The authentication mechanism to use for the scan. Valid values are "none", "form", "custom"
#
#   for DF_SCAN_AUTH_TYPE="form" the following variables are required
#       DF_SCAN_AUTH_FORM_LOGIN_URI - The relative path of the login page
#       DF_SCAN_AUTH_FORM_USERNAME  - The username to use to authenticate ( store as Project CI/CD Variable )
#       DF_SCAN_AUTH_FORM_PASSWORD  - The password to use to authenticate ( store as Project CI/CD Variable )
#       DF_SCAN_AUTH_FORM_DATA      - The post data for the login eg. username={%username%}&password={%password%}
#       DF_SCAN_AUTH_FORM_LOGGEDIN  - The LoggedIn indicator html eg. <a href=&quot;logout.jsp&quot;>Logout</a>
#       DF_SCAN_AUTH_FORM_LOGGEDOUT - The LoggedOut indicator html eg. <a href=&quot;login.jsp&quot;>Login</a>
#
#   for DF_SCAN_AUTH_TYPE="custom" the following variables are required
#       DF_SCAN_AUTH_CUSTOM_TOKEN   - This token will be sent in Authorization header of each request made by the scanner. Please note, you may need to add Bearer before the token if you are using JWT
#
.deepfactor_webscan:
  artifacts:
    paths:
      - scan-data/
  script:
    - |
      if [[ -z "$DF_API_KEY" ]]; then
          echo "DF_API_KEY environment variable missing"
          exit 1
      fi
      if [[ -z "$DF_PORTAL_HOST" ]]; then
          echo "Variable \"DF_PORTAL_HOST\" is missing"
          exit 1
      fi
      if [[ -z "$DF_APP" ]]; then
          echo "Param \"DF_APP\" is missing"
          exit 1
      fi
      if [[ -z "$DF_COMPONENT" ]]; then
          echo "Param \"DF_COMPONENT\" is missing"
          exit 1
      fi
      # check the portal url is valid
      scan_type_web="full"
      scan_type_api="api"
      scan_strength_low="low"
      scan_strength_medium="medium"
      scan_strength_high="high"
      auth_type_none="none"
      auth_type_custom="custom"
      auth_type_form="form"
      DF_PORTAL_URL="https://${DF_PORTAL_HOST}"
      curl -kfs "${DF_PORTAL_URL}/api/health" > /dev/null
      status="$?"
      if [[ "$status" != "0" ]]; then
        echo "The param DeepFactor portal_url is incorrect or is not reachable"
        exit 1
      fi
      if [[ -z "$DF_SCAN_TYPE" ]]; then
          echo "Param \"DF_SCAN_TYPE\" is missing"
          exit 1
      elif [[ "$DF_SCAN_TYPE" != "$scan_type_web" ]] && [[ "$DF_SCAN_TYPE" != "$scan_type_api" ]]; then
        echo "Unknown DF_SCAN_TYPE , valid values are \"$scan_type_web\" or \"$scan_type_api\" "
        exit 1
      fi
      SCAN_TYPE="owasp-zap-full"
      case "$DF_SCAN_TYPE" in
              "$scan_type_web") SCAN_TYPE="owasp-zap-full";;
              "$scan_type_api") SCAN_TYPE="owasp-zap-api";;
      esac
      if [[ "$SCAN_TYPE" == "owasp-zap-api" ]] && [[ -z "$DF_SCAN_API_DOCS_PATH" ]]; then
        echo "Missing \"DF_SCAN_API_DOCS_PATH\" required for API Scans"
        exit 1
      fi
      if [[ -z "$DF_SCAN_STRENGTH" ]]; then 
          echo "Param \"DF_SCAN_STRENGTH\" is missing"
          exit 1
      fi
      shopt -s nocasematch
      case "$DF_SCAN_STRENGTH" in
              "$scan_strength_low") DF_SCAN_STRENGTH="Low";;
              "$scan_strength_medium") DF_SCAN_STRENGTH="Medium";;
              "$scan_strength_high") DF_SCAN_STRENGTH="High";;
              *) DF_SCAN_STRENGTH="Low";
      esac
      if [[ -z "$DF_SCAN_AUTH_TYPE" ]]; then
          echo "Param \"DF_SCAN_AUTH_TYPE\" is missing"
          exit 1
      fi
      if [[ -n "$DF_SCAN_AUTH_TYPE" ]] && [ "$DF_SCAN_AUTH_TYPE" == "$auth_type_form" ]; then
          if [[ -z "$DF_SCAN_AUTH_FORM_LOGIN_URI" ]] ||
            [[ -z "$DF_SCAN_AUTH_FORM_USERNAME" ]]  ||
            [[ -z "$DF_SCAN_AUTH_FORM_PASSWORD" ]]  ||
            [[ -z "$DF_SCAN_AUTH_FORM_DATA" ]]      ||
            [[ -z "$DF_SCAN_AUTH_FORM_LOGGEDIN" ]]  ||
            [[ -z "$DF_SCAN_AUTH_FORM_LOGGEDOUT" ]]; then
            echo "Parameters for Form Authentication missing"
            exit 1
          fi
      fi
      if [[ -n "$DF_SCAN_AUTH_TYPE" ]] && [ "$DF_SCAN_AUTH_TYPE" == "$auth_type_custom" ]; then
          if [[ -z "$DF_SCAN_AUTH_CUSTOM_TOKEN" ]]; then
            echo "Parameters \"DF_SCAN_AUTH_CUSTOM_TOKEN\" missing for custom token authentication"
            exit 1
          fi
      fi 
      if [[ -z "$DF_SCAN_URL" ]]; then
          echo "Param \"DF_SCAN_URL\" is missing"
          exit 1
      fi 

      SCAN_HOST=$( echo "$DF_SCAN_URL" | sed -e 's/[^/]*\/\/\([^@]*@\)\?\([^:/]*\).*/\2/' )
      echo "ScanURL:${SCAN_HOST}"
      generate_start_scan_request()
      {
          DF_AUTH_STRING=""
          case $DF_SCAN_AUTH_TYPE in 
              "$auth_type_form")
                  DF_AUTH_STRING=$(cat <<EOF
          "login_uri":"$DF_SCAN_AUTH_FORM_LOGIN_URI",
          "scan_user_name":"$DF_SCAN_AUTH_FORM_USERNAME",
          "scan_user_password":"$DF_SCAN_AUTH_FORM_PASSWORD",
          "logged_in_indicator":"$DF_SCAN_AUTH_FORM_LOGGEDIN",
          "logged_out_indicator":"$DF_SCAN_AUTH_FORM_LOGGEDOUT",
          "auth_type":"formBasedAuthentication",
      EOF
      )
                  ;;
              "$auth_type_custom")
                  DF_AUTH_STRING=$(cat <<EOF 
                  "custom_token":"$DF_SCAN_AUTH_CUSTOM_TOKEN",
                  "auth_type":"custom",
      EOF
      )
                  ;;
              *)
              DF_AUTH_STRING=$(cat <<EOF
              "auth_type":"none",
      EOF
      )
              ;;
          esac

          cat <<EOF
      {
          "application_name":"$DF_APP",
          "component_name":"$DF_COMPONENT",
          "component_version":"$DF_VERSION",
          "scan_config":
          {
              "scan_type":"$SCAN_TYPE",
              "active_scan_policy_strength":"$DF_SCAN_STRENGTH",
              $DF_AUTH_STRING
              "external_scan_url":"$DF_SCAN_URL",
              "api_scan_url":"$DF_SCAN_API_DOCS_PATH",
              "host":"$SCAN_HOST"
          }
      }
      EOF

      }

      mkdir -p scan-data
      echo "Starting Scan now.."
      status=$( curl -ks -X POST --data \
                  "$(generate_start_scan_request)" \
                  --write-out '%{http_code}' \
                  --output scan-data/scan_out.json \
                  "${DF_PORTAL_URL}/api/services/v1/webservices/scans" \
                  -H "Content-Type: application/json" \
                  -H "Authorization: Bearer ${DF_API_KEY}" )
      if [ "$status" != 200 ]; then
          echo "Error starting scan got status ${status}"
          exit 1
      fi
