Automatiza las correcciones de seguridad de paquetes NPM con tareas recurrentes en CI

Cuando desarrollamos con Node.js usamos muchísimos paquetes NPM open source. Cada uno aporta sus ventajas y sus inconvenientes al proyecto. En este artículo veremos:
- Opciones gratuitas para detectar vulnerabilidades en las dependencias NPM
- Configuración de Drone CI para lanzar comprobaciones recurrentes
- Creación automática de Pull Requests con los paquetes corregidos
NPM audit y algo más
Lo primero que viene a la cabeza cuando hablamos de auditar vulnerabilidades es la herramienta NPM audit. Usa un catálogo público de vulnerabilidades para revisar tu proyecto y te propone actualizar las versiones de las librerías para corregir los problemas que encuentre. Tienes más información en el blog oficial de NPM.
Otro informe útil y gratuito que viene de serie es npm outdated. Este comando consulta el registro para ver si alguno de los paquetes instalados está desactualizado. No es una información imprescindible en el día a día, pero conviene tenerla a largo plazo para no caer en la tentación de dejar el proyecto abandonado.
$ npm outdatedPackage Current Wanted Latest Locationglob 5.0.15 5.0.15 6.0.1 test-outdated-outputnothingness 0.0.3 git git test-outdated-outputnpm 3.5.1 3.5.2 3.5.1 test-outdated-outputlocal-dev 0.0.3 linked linked test-outdated-outputonce 1.3.2 1.3.3 1.3.3 test-outdated-output
Informes automáticos de npm outdated
Estas herramientas son muy útiles, pero, claro, los informes automáticos lo son todavía más. Para ello usamos Drone CI (gratuito y open source) y su nueva funcionalidad, Cron Jobs, para programar tareas recurrentes. Puedes usar cualquier otro CI que prefieras; seguramente ofrecerá lo mismo. Si no conoces Drone CI, echa un vistazo a este artículo de introducción.
Como Drone CI admite varios pipelines, cada informe tiene el suyo y no afecta al principal. Para verlo en conjunto, consulta el ejemplo aquí. Mientras tanto, empecemos con npm outdated.
kind: pipelinename: npm outdatedsteps: - name: outdated image: node:10-alpine commands: - npm outdated - name: slack_notification image: plugins/slack settings: webhook: https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX template: > NPN detected outdated packages at *{{repo.name}}* for *{{build.branch}}* branch. Report available by the link {{build.link}} when: status: - failuretrigger: cron: [weekly]
La sintaxis yaml se explica prácticamente sola. En el primer paso usamos node:10-alpine como imagen base y ejecutamos npm outdated. En el segundo, la notificación de Slack solo se envía si hay algo que actualizar (npm outdated termina con un código de salida de error). Para obtener la URL del webhook de Slack, visita esta página
En las últimas líneas, el pipeline completo se dispara con el Cron Job llamado “outdated”. En nuestros proyectos lo programamos para que se ejecute cada semana, ya que no pensamos actualizar los paquetes cada vez que sale una versión nueva.
Para definir la tarea en Drone, ve a Project -> Settings.
Desde esta interfaz puedes elegir el nombre del job (que se usa para filtrar los pipelines), la rama y la periodicidad, que puede ser cada hora, diaria, semanal, mensual o anual.
npm audit automático y creación de la PR con las correcciones
El comando npm audit revisa tu app en busca de vulnerabilidades y actualiza los paquetes a la versión que haga falta. El pipeline es muy parecido al anterior, pero con un paso extra para crear la PR.
kind: pipelinename: npm auditsteps: - name: audit image: node:10-alpine commands: - set -o pipefail && npm audit --force 2>&1 | tee audit.log - name: audit fix image: node:10-alpine commands: - npm audit fix when: status: - failure - name: create_fix_pr image: lnikell/github-hub:2.11.2 environment: GITHUB_TOKEN: from_secret: github_token commands: - git config --global user.email "email@example.com" - git config --global user.name "example" - git checkout -b drone/npm-audit-fix-${DRONE_BUILD_NUMBER} - git add package.json package-lock.json - git commit -m 'npm audit fix' - git push origin drone/npm-audit-fix-${DRONE_BUILD_NUMBER} - hub pull-request -m "[Security] NPM Audit Fix" -m "$(cat audit.log | tail -2)" -m "${DRONE_BUILD_LINK}" when: status: - failure - name: slack_notification image: plugins/slack settings: webhook: https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX template: > NPN detected vulnerable packages at *{{repo.name}}* for *{{build.branch}}* branch. Report available by the link {{build.link}} when: status: - failure
En el primer paso usamos la misma imagen node:10-alpine y ejecutamos NPM audit. Además, guardamos los resultados en un archivo audit.log para mostrarlos después en la PR. Si npm audit encuentra paquetes vulnerables, el paso falla y se lanzan el proceso de npm audit fix y la creación de la pull request.
-name: audit fix image: node:10-alpine commands: - npm audit fix when: status: - failure
Para crear la pull request usamos hub, la herramienta de línea de comandos para trabajar con la API de GitHub. Necesitamos generar un Personal Token de GitHub para usarlo en la llamada a la API. Ve a esta página y crea uno nuevo
Selecciona el ámbito de permisos “repo” y añade el token generado a los secrets de Drone con el nombre “github_token”.
Se usa como variable de entorno en el paso siguiente.
- name: create_fix_pr image: lnikell/github-hub:2.11.2 environment: GITHUB_TOKEN: from_secret: github_token commands: - git config --global user.email "ci@example.com" - git config --global user.name "drone" - git checkout -b drone/npm-audit-fix-${DRONE_BUILD_NUMBER} - git add package.json package-lock.json - git commit -m 'npm audit fix' - git push origin drone/npm-audit-fix-${DRONE_BUILD_NUMBER} - hub pull-request -m "[Security] NPM Audit Fix" -m "$(cat audit.log | tail -2)" -m "${DRONE_BUILD_LINK}" when: status: - failure
En este paso definimos el patrón para crear la rama y abrimos una pull request con las dos últimas líneas de audit.log. El resultado es una PR bien presentada:
Por último, hay que revisar la parte del trigger del pipeline. Como solo quieres que estas comprobaciones se ejecuten dentro del Cron job, añade lo siguiente:
trigger: cron: [name_of_the_job]
Eso sí, no te olvides del pipeline principal. Para que no se ejecute durante las tareas Cron, usa la opción exclude así:
trigger: cron: exclude: [name_of_the_job]
Conclusión
Este es solo un ejemplo de lo útiles que pueden ser las tareas recurrentes en CI para hacer builds, testear y corregir. Lo configuras una vez y recibes información sobre la seguridad de tu proyecto cada día o cada semana. El enfoque de estos ejemplos se puede adaptar fácilmente a Travis CI o GitLab; si lo haces, comparte tu pipeline.