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
De Microsoft Labs. Una vez instalada la extensión te marcará la función o la linea en concreto que no le guste.
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:
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:
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.
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.
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:
Y además crea una nueva sección llamada Snyk Report en el resultado de la ejecución con la misma información:
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:
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:
Pero si accedes a su web verás que el detalle es súper rico, con un informe con diferentes perspectivas:
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, 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:
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:
y otro más completo y navegable 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.
- 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:
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:
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.
- 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:
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:
Escaneo de secretos
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:
Lo bueno de tener nuestras pipelines configuradas en YAML, como parte del proyecto, es que también serán analizadas en busca de secretos.
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:
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.
- 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:
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.
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:
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:
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:
¡Saludos!