DevSecOps con Azure DevOps

Durante la última semana y media he estado recopilando, probando y jugando con diferentes extensiones, tareas y configuraciones que me permitan usar Azure DevOps como herramienta para implementar una cultura DevSecOps en los proyectos más comunes a día de hoy. Te advierto que va a ser un artículo largo, pero que resume todo lo que he visto que puede resultarte de utilidad si utilizas este servicio con tus proyectos.

Antes de empezar

Antes de comenzar a enseñarte herramientas y configuraciones, creo que es importante que sepas que una cultura DevSecOps lo que pretende es llevarte al ciclo de desarrollo de las aplicaciones la parte de seguridad que hasta ahora, en algunas empresas, y hace muy bien poco en otras, se trataban estos temas en la última milla, una vez desplegada la aplicación, a veces ya hasta en producción. Gracias a la evolución de las herramientas y servicios con los que hoy hemos implementado en numerosas empresas la integración continua (Continuous Integration o CI en inglés) o despliegue continuo (Continous Delivery o CD) estamos ya en posición de plantearnos estas buenas prácticas a nivel de seguridad como parte de nuestro día a día. Con ello conseguimos que se atajen a la mayor brevedad posible ciertos problemas que pueda tener nuestra aplicación desde el punto de vista de seguridad y no tener que esperar a que pasen semanas para ser notificados por el equipo competente y que ni nos acordamos de qué fue lo que desarrollamos, haciendo que nuestra productividad baje.

Dicho todo esto, cuando pensamos en mover a la izquierda (shift left como se dice en inglés) la parte de seguridad en nuestros proyectos, podemos pensar en los siguientes puntos que podríamos abordar:

Como ves, hay diferentes prácticas que se pueden llevar a cabo en cada una de las fases que se mecionan: pre-commit, commit o CI, deploy o CD, y operate y monitor. En este artículo nos vamos a quedar en las tres primeras.

Pre-commit

En la primera fase, cuando tu código todavía no ha sido publicado como parte del repositorio central, es decir que todavía no ha salido de tu máquina o entorno de desarrollo, puedes utilizar diferentes mecanismos para evitar en la medida de lo posible que no te equivoques y subas información sensible al repo. Por un lado tenemos plugins en nuestros IDEs favoritos que nos ayudan en esta tarea. Vamos a ver qué encontramos para Visual Studio Code.

Plugins de seguridad para Visual Studio Code

DevSkim

De Microsoft Labs. Una vez instalada la extensión te marcará la función o la linea en concreto que no le guste.

DevSkim

Snyk Vulnerability Scanner

Snyk es una de las compañías que más variedad tiene a la hora de analizar vulnerabilidades, desde todos los aspectos. Es por ello que no podía faltar en nuestro IDE. Soporta JavaScript, TypeScript, Java, Python and C#. El resultado será parecido al siguiente:

Snyk Vulnerability Scanner – Visual Studio Code

pre-commit git hook

Ya te adelanté algo en el artículo anterior, donde utilizaba GitGuardian con este mecanismo de git, el cual te permite ejecutar un script antes de que puedas efectuar el commit. En realidad, aquí podrías ejecutar la tarea o herramienta que quisieras.

Commit (CI)

Una vez que hemos decidido que nuestro código está listo para compartir con el resto del equipo, llega el momento de hacer el commit del mismo y con ello podemos lanzar diferentes tipos de tareas que analicen los tres focos principales a día de hoy: análisis de código estático (SAST en inglés), inventariado de las dependencias, y con ello saber si algunas de ellas pueden ser vulnerables, y el escaneo de secretos, por si en la fase anterior se nos ha pasado algo.

He creado un pipeline extra largo que compartiré contigo, pero me gustaría ir comentado cada unas de las herramientas con las que me he quedado:

SAST

En cuanto a análisis de código estático me he quedado con las siguientes:

SonarCloud

Posiblemente sea la más conocida, ya que ofrece mucha información interesante del código de nuestro proyecto. Si ya has trabajado con ella anteriormente en Azure DevOps, sabrás que tienes una tarea de preparación y otra de ejecución del análisis. Para este ejemplo la he podido configurar tanto para .NET:

    jobs:
      - job: "SAST"
        displayName: "Static Code Analysis job"
        pool:
          vmImage: ubuntu-latest
        variables:
          - group: tools
        steps:
          - task: SonarCloudPrepare@1
            inputs:
              SonarCloud: 'SonarCloud'
              organization: 'gis'
              scannerMode: 'MSBuild'
              projectKey: 'gis_Tour-Of-Heroes-Web-API'
              projectName: 'Tour Of Heroes Web API'
          - script: dotnet build --configuration Release
            displayName: "dotnet build"
          - task: SonarCloudAnalyze@1
          - task: SnykSecurityScan@1

Como para un proyecto de Angular:

        - task: Npm@1
          inputs:
            command: 'install'
        - task: SonarCloudPrepare@1
          inputs:
            SonarCloud: 'SonarCloud'
            organization: 'gis'
            scannerMode: 'CLI'
            configMode: 'manual'
            cliProjectKey: 'gis_Tour-Of-Heroes-Angular'
            cliProjectName: 'Tour Of Heroes Angular'
            cliSources: '.'
        - task: Npm@1
          inputs:
            command: 'ci'
        - task: SonarCloudAnalyze@1
        - task: SnykSecurityScan@1
          inputs:
            serviceConnectionEndpoint: 'Snyk Connection'
            testType: 'app'
            monitorWhen: 'always'
            failOnIssues: true
            projectName: 'tour-of-heroes-angular'
            organization: '0gis0'

El resultado de este no queda integrado con Azure DevOps sino que tendrás un enlace a su web donde podrás revisar la información de manera muy detallada.

Resumen del análisis con SonarCloud en su web

Como ves en este resumen, el servicio cubre diferentes áreas donde puedes ir revisando con detalle cada una de ellas y a qué parte del código se refiere.

Snyk

Como te comentaba antes, Snyk ofrece diferentes servicios para el escaneo de vulnerabilidades. Por supuesto no podía faltar en el área del escaneo de código fuente. Además de configurar una Service connection a nivel de proyecto con la API Key de este, la tarea a configurar es tan sencilla como esta:

          - task: SnykSecurityScan@1
            displayName: "Snyk Security Scan"
            inputs:
              serviceConnectionEndpoint: "Snyk Connection"
              testType: "app"
              monitorWhen: "always"
              failOnIssues: false
              organization: 0gis0
              projectName: tour-of-heroes

En el caso de este servicio te permite ver directamente el resultado en la salida de la pipeline:

Salida de Snyk en la pipeline de Azure DevOps

Y además crea una nueva sección llamada Snyk Report en el resultado de la ejecución con la misma información:

Snyk Report en el resultado de la ejecución de la pipeline

WhiteSource Bolt

Otra herramienta, de la compañía WhiteSource, que nos aporta muchísima información y tiene un modo gratuito:

          - task: WhiteSource@21
            displayName: "WhiteSource Bolt"
            inputs:
              cwd: "$(System.DefaultWorkingDirectory)"
              projectName: "Tour Of Heroes Web API"

Esta también nos proporciona un informe como resultado de la ejecución de la pipeline:

Informe de WhiteSource Bolt en el resultado de la ejecución de la pipeline

42Crunch

Si lo que estás desarrollando son API REST te recomiendo sin duda este servicio de 42Crunch. Para que este funcione debes generar el archivo Swagger de la definición de tu API. En el caso de .NET lo he hecho usando estos comando:

dotnet tool install --version 5.3.1 Swashbuckle.AspNetCore.Cli
dotnet swagger tofile --output api.json bin/Debug/net5.0/tour-of-heroes-api.dll v1

Puedes integrarlo como parte de la pipeline de Azure DevOps para tener siempre la última definición antes de ejecutar 42Crunch.

La configuración de la tarea solo necesita el token como parámetro para funcionar:

          - task: APIContractSecurityAudit@3
            inputs:
              apiToken: '$(42C_API_TOKEN)'

La información que te da en la salida de la pipeline es simplemente el score:

42crunch output – ejecución de la pipeline

Pero si accedes a su web verás que el detalle es súper rico, con un informe con diferentes perspectivas:

Informe de 42crunch en su web
Vulnerabilidades en contenedores Docker

Si lo que tienes entre manos son contenedores de Docker, tampoco estos se escapan de pasar por el escáner.

Trivy

Trivy, de Aquasec, se trata de una herramienta open source que nos da información detallada, en formato tabla, de todas las vulnerabilidades que encuentra en tu imagen y en la base. La tarea que la configura podría ser como la siguiente:

          - script: |
              docker build -t tour-of-heroes-image .
            displayName: "Build image"
          - script: |              
              docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/Library/Caches:/root/.cache/ aquasec/trivy image tour-of-heroes-image
            displayName: "Run trivy"

Como ves, en la tarea he definido la ejecución de trivy en modo dockerizado que devolverá el resumen de todas las vulnerabilidades que ha encontrado y su criticidad:

Salida de la ejecución de trivy

Snyk

Respecto a Docker, también podemos usar Snyk con la misma tarea que usamos anteriormente, con un cambio en al configuración de esta:

          - task: SnykSecurityScan@1
            inputs:
              serviceConnectionEndpoint: 'Snyk Connection'
              testType: 'container'
              dockerImageName: 'tour-of-heroes-image'
              dockerfilePath: 'Dockerfile'
              monitorWhen: 'always'
              failOnIssues: true
              organization: 0gis0
              projectName: tour-of-heroes

Al igual que en el caso anterior, tenemos un informe nuevo en la pestaña Snyk Report de la ejecución:

Informe integrado de Snyk en Azure DevOps para la imagen de Docker

y otro más completo y navegable en su web:

Informe de Snyk para la imagen de Docker en su web
Vulnerabilidades en los manifiestos de Kubernetes

Si estás usando Docker posiblemente también estés usando Kubernetes y es por ello que el análisis de los manifiestos que utilizas para desplegar los recursos en este también puede estar sujeto a vulnerabilidades de diferentes tipos. Como te puedes imaginar, para esto también tenemos herramientas que nos ayudan a entender qué se nos puede estar pasando.

kube-score

          - script: brew install kube-score
            displayName: "Install kube-score"
          - script: kube-score score k8s/*.yaml
            displayName: "Run kube-score"

Utilizando un pool de Ubuntu, podemos utilizar también Homebrew como herramienta de instalación de paquetes, en este caso de kube-score. Una vez hecho basta con lanzar el comando kube-score score y el directorio donde se encuentran los manifiestos a analizar. El resultado será como el que sigue:

Resultado de la ejecución de kube-score

kube-linter

Existe otra alternativa más ligera que nos permite también el escaneo tanto de YAML como charts de Helm:

          - script: brew install kube-linter
            condition: always()
            displayName: "Install kube-linter"
          - script: kube-linter lint k8s/*.yaml
            condition: always()
            displayName: "Run kube-linter"

Sin embargo, esta devuelve información más pobre que el anterior:

Salida de la ejecución de kube-linter
Vulnerabilidades en Terraform

Si, también es posible 😃 y existen varias herramientas que además entienden de proveedores cloud lo cual hace que sea muy útil la información que nos proporcionan en este sentido.

Terrascan

          - script: |
              curl --location https://github.com/accurics/terrascan/releases/download/v1.13.0/terrascan_1.13.0_Linux_x86_64.tar.gz --output terrascan.tar.gz
              tar -xvf terrascan.tar.gz
              sudo install terrascan /usr/local/bin    
            displayName: 'Install terrascan'
          - script: |
              terrascan scan -t azure -i terraform -o junit-xml > terrascan.xml              
            workingDirectory: $(System.DefaultWorkingDirectory)/terraform
            displayName: 'Run terrascan' 
          - task: PublishTestResults@2
            displayName: "Publish Terrascan Results"
            condition: always()
            inputs:
              testResultsFormat: JUnit
              testResultsFiles: terrascan.xml
              searchFolder: $(System.DefaultWorkingDirectory)/terraform
              testRunTitle: "Terrascan"

En este ejemplo se ha instalado la herramienta como parte del proceso, se ejecutara para que busque por sí sola los archivos de terrafom en nuestro proyecto y acto seguido subirá el resultado en formato JUnit. Siempre que puedas exportar el resultado a uno de los formatos compatibles podrás ver el resultado después en el apartado Tests:

Resultado de Terrascan en el apartado de Test de la ejecución del pipeline

Snyk

Otra vez aquí, Snyk, también podemos ejecutarlo para ver qué tal lo estamos haciendo con Terraform. En este caso, todavía no hay una tarea de Azure DevOps que nos permita la configuración en este supuesto, pero podemos utilizar su CLI y ejecutarlo igualmente:

          - script: |
              npm install -g snyk
            displayName: "Install Snyk"
            condition: always()            
          - script: |              
              snyk iac test . --severity-threshold=low 
            workingDirectory: terraform
            condition: always()
            displayName: "Snyk IaC"
            env:
              SNYK_TOKEN: $(SNYK_TOKEN)

La información que nos devuelve es tan interesante como esta:

Resultado del escaneo de Snyk para Terraform
Escaneo de secretos

GitGuardian

Si bien es cierto que podemos hacer uso en el pre-commit git hook de esta herramienta de GitHub, también podemos incorporarla como parte de nuestro pipeline en Azure DevOps. Para poder utilizarlo de manera sencilla lo primero que hago es utilizar el apartado resources del pipeline para configurar el contenedor que usaré en el job:

resources:
  containers:
    - container: gitguardian
      image: gitguardian/ggshield:latest
      options: --user 0:0

y después en el stage que he llamado SecretScanning configuro un job donde utilizo este contenedor para poder invocar en él la herramienta ggshield:

stages:
  - stage: "SecretScanning"
    displayName: "Secret scanning"
    condition: always()
    jobs:
      - job: "GitGuardian"
        variables:
          - group: tools
        pool:
          vmImage: ubuntu-latest
        container: gitguardian
        steps:
          - script: ggshield scan repo .
            displayName: "ggshield scan repo"
            env:
              GITGUARDIAN_API_KEY: $(GIT_GUARDIAN_API_KEY)

Nota: si te fijas, en el job que se encarga de utilizar GitGuardian también he utilizado un grupo de variables llamado tools donde tengo almacenada la llamada GIT_GUARDIAN_API_KEY para no exponer la clave directamente en el flujo.

El resultado será parecido a lo siguiente:

Resultado del escaneo de secretos con GitGuardian

Lo bueno de tener nuestras pipelines configuradas en YAML, como parte del proyecto, es que también serán analizadas en busca de secretos.

YELP detect secrets

GitGuardian está sujeto a número de llamadas limitadas en el caso gratuito (1000/mes) que es posible ampliar en el modo de pago. Si no quieres pagar por un servicio de este tipo también puedes utilizar detect-secrets de YELP.

      - job: ubuntu
        displayName: "detect-secrets on Ubuntu Linux agent"
        pool:
          vmImage: ubuntu-latest
        steps:
          - task: UsePythonVersion@0
            displayName: "Set Python 3 as default"
            inputs:
              versionSpec: "3"
              addToPath: true
              architecture: "x64"
          - bash: pip install detect-secrets
            displayName: "Install detect-secrets using pip"
          - bash: |
              detect-secrets --version
              detect-secrets scan --all-files --force-use-all-plugins --exclude-files FETCH_HEAD > $(Pipeline.Workspace)/detect-secrets.json
            displayName: "Run detect-secrets tool"
          - task: PublishPipelineArtifact@1
            displayName: "Publish results in the Pipeline Artifact"
            inputs:
              targetPath: "$(Pipeline.Workspace)/detect-secrets.json"
              artifact: "detect-secrets-ubuntu"
              publishLocation: "pipeline"
          - bash: |
              dsjson=$(cat $(Pipeline.Workspace)/detect-secrets.json)
              echo "${dsjson}"
              count=$(echo "${dsjson}" | jq -c -r '.results | length')
              if [ $count -gt 0 ]; then
                msg="Secrets were detected in code. ${count} file(s) affected."
                echo "##vso[task.logissue type=error]${msg}"
                echo "##vso[task.complete result=Failed;]${msg}."
              else
                echo "##vso[task.complete result=Succeeded;]No secrets detected."
              fi
            displayName: "Analyzing detect-secrets results"

En este caso devuelve el resultado en formato JSON:

Resultado del escaneo con detect-secrets de YELP
Gestión de las dependencias

Además de analizar las vulnerabilidades de nuestro código, y la posible fuga de secretos también, es importante controlar las dependencias de que usas en tus proyectos, ya que estas también son susceptibles de tener vulnerabilidades que posiblemente ya sean conocidas.

OWSAP Dependency Check

          - task: dependency-check-build-task@6
            inputs:
              projectName: "Tour Of Heroes Web API"
              scanPath: "**/*.csproj"
              format: "HTML, JSON, JUNIT"          
          - task: PublishTestResults@2
            inputs:
              testResultsFormat: "JUnit"
              testResultsFiles: "dependency-check/*junit.xml"
              searchFolder: "$(Common.TestResultsDirectory)"
              testRunTitle: "OWASP Dependency Check"

El resultado se vería en el apartado de Tests:

Resultado de OWSAP Dependency Check
Despliegue (CD)

Ya estamos llegando al final de este artículo, que si bien ha sido muy largo espero que te resulte útil a la hora de ver todo lo que se puede hacer y algunas herramientas que te sirvan de referencia. Para terminar , quería compartir contigo un par para esta fase donde el código o el contenedor ya está desplegado en algún servicio y necesitamos validar «en caliente» que el mismo tiene la configuración correcta y si existe alguna vulnerabilidad que no se vea a simple vista en el código.

OWSAP ZAP Scanner

Si ya tenemos un entorno desplegado, gracias a esta tarea podremos analizar la URL expuesta a nuestro agente para ejecutar un gran número de pruebas:

          - task: owaspzap@1
            inputs:
              scantype: "targetedScan"
              url: "$(WEB_APP_URL)"
              aggressivemode: true
              threshold: 70
          - script: ls  owaspzap
            condition: always()
          - bash: |
              sudo npm install -g handlebars-cmd
              cat <<EOF > owaspzap/nunit-template.hbs
              {{#each site}}
              <test-run
                  id="2"
                  name="Owasp test"
                  start-time="{{../[@generated]}}"  >
                  <test-suite
                      id="{{@index}}"
                      type="Assembly"
                      name="{{[@name]}}"
                      result="Failed"
                      failed="{{alerts.length}}">
                      <attachments>
                          <attachment>
                              <filePath>owaspzap/report.html</filePath>
                          </attachment>
                      </attachments>
                  {{#each alerts}}<test-case
                      id="{{@index}}"
                      name="{{alert}}"
                      result="Failed"
                      fullname="{{alert}}"
                      time="1">
                          <failure>
                              <message>
                                  <![CDATA[{{{desc}}}]]>
                              </message>
                              <stack-trace>
                                  <![CDATA[
              Solution:
              {{{solution}}}
              Reference:
              {{{reference}}}
              instances:{{#each instances}}
              * {{uri}}
                  - {{method}}
                  {{#if evidence}}- {{{evidence}}}{{/if}}
                                  {{/each}}]]>
                              </stack-trace>
                          </failure>
                  </test-case>
                  {{/each}}
                  </test-suite>
              </test-run>
              {{/each}}
              EOF
            displayName: "owasp nunit template"
            condition: always()
          - bash: " handlebars owaspzap/report.json < owaspzap/nunit-template.hbs > owaspzap/test-results.xml"
            displayName: "generate nunit type file"
            condition: always()
          - task: PublishTestResults@2
            displayName: "Publish Test Results **/TEST-*.xml"
            condition: always()
            inputs:
              testResultsFormat: NUnit
              testResultsFiles: "owaspzap/test-results.xml"
              testRunTitle: "OWASP ZAP Scanner"

Al igual que con Terrascan, en este caso hemos publicado el resultado en formato JUnit por lo que podremos verlo de forma cómoda en la sección Tests:

Resultados de OWASP ZAP Scanner en la sección Tests

ADO Security Scanner

Por último, muy ligado a Azure DevOps tenemos una herramienta llamada ADO Security Scanner que nos permite conocer el estado de configuración a nivel de Azure DevOps. En mi caso la he configurado en una pipeline aparte que se ejecuta cada cierto tiempo:

steps:
- task: azsdktm.ADOSecurityScanner.custom-build-task.ADOSecurityScanner@1
  displayName: 'ADO Security Scanner'
  inputs:
    ADOConnectionName: 'Azure DevOps - gis organization'

El resultado que te proporciona es un resumen de cómo tienes configurada la organización y el proyecto a nivel de seguridad:

Widget generado por ADO con el resumen a nivel de organización y de proyecto

Y también tienes otro análisis más exhaustivo donde puedes revisar uno, varios o todos los recursos que se manejan dentro de este entorno:

steps:
- task: azsdktm.ADOSecurityScanner.custom-build-task.ADOSecurityScanner@1
  displayName: 'ADO Security Scanner all'
  inputs:
    ADOConnectionName: 'Azure DevOps - gis organization'
    ScanFilter: All
    BuildNames: '*'
    ReleaseNames: '*'
    ServiceConnectionNames: '*'
    AgentPoolNames: '*'
    VariableGroupNames: '*'
    RepositoryNames: '*'
    SecureFileNames: '*'
    FeedNames: '*'
    EnvironmentNames: '*'

El resultado será mucho más extenso y lo verás de la siguiente forma:

Widget generado por ADO cuando escaneas a nivel de recursos

¡Saludos!