La mayor parte de las pruebas de seguridad móvil ocurren cuando la build que importaba ya se publicó. Una evaluación trimestral encuentra una credencial embebida que llegó a producción once semanas antes, y el hallazgo aparece como arqueología en vez de como prevención.
Llevar el escaneo al pipeline corrige el momento. También introduce el modo de fallo que hace que los equipos lo desactiven en un mes: una puerta que bloquea entregas por hallazgos con los que nadie pretendía bloquear.
Este artículo trata de acertar en la segunda parte.
Dónde va el escaneo
El escaneo se ejecuta contra el artefacto construido, después del ensamblado y antes de la distribución. Esa posición importa por una razón concreta: lo que se publica es el paquete ensamblado, con todas las dependencias que arrastró la build.
El escaneo a nivel de código fuente no ve lo que entra por una dependencia transitiva, ni lo que inyecta el propio proceso de construcción. El APK, el AAB o el IPA es lo que instala el usuario, así que es lo que hay que analizar.
En la práctica eso es un paso después de assembleRelease, y antes de la subida a Play, App Store Connect o el canal de distribución que se use.
Qué ve el escaneo del artefacto que no ve el del código
El argumento para analizar el paquete y no el repositorio es concreto. Hay tres clases de hallazgo que sólo existen en el artefacto ensamblado:
Contenido de los SDK de terceros. Droidwatch identifica 87 SDK de terceros por firma dentro del paquete construido. Un manifiesto de dependencias declara lo que se pidió; el artefacto declara lo que llegó, incluidos los SDK que arrastran otros SDK de forma transitiva.
Junto a la identificación se consulta OSV.dev en el momento del análisis en busca de vulnerabilidades conocidas. Conviene enunciar el límite con honestidad: OSV cubre 12 de los 87. Los SDK de publicidad y atribución son cerrados y no figuran en las bases de vulnerabilidades públicas, así que para la mayoría del conjunto el pipeline establece qué hay dentro de la build pero no si existe un CVE publicado. Un módulo aparte sigue unos 120 trackers de privacidad, que con frecuencia es el hallazgo que interesa a una revisión de privacidad más que a una de seguridad.
Configuración de build que sólo existe tras el ensamblado. Un android:debuggable="true" que sobrevive a una build de release, android:allowBackup="true" junto a almacenamiento local sensible, un networkSecurityConfig permisivo, un certificado de firma que no es el que el proceso de publicación debía usar. Nada de esto es fiablemente visible en el código.
Resultado del merge de manifiestos. El conjunto final de permisos es la unión del manifiesto de la aplicación y el de cada librería fusionada. Una dependencia puede añadir un permiso que el equipo de desarrollo nunca pidió y no aprobaría.
Qué debe romper la build
Por defecto, poco. El runner de CI de Droidwatch admite un ajuste fail_on_verdict, y por defecto vale sólo Malicious, la opción más conservadora disponible.
Ese valor por defecto es deliberado. Una puerta que salta con cualquier cosa por debajo de malware confirmado saltará con la propia aplicación de la organización, porque una aplicación legítima pide permisos legítimamente, incluye SDK de analítica legítimamente y ofusca su código legítimamente.
La escala de veredicto va de 0 a 100 donde más alto es peor, con cuatro bandas: 65 o más Malicious, de 40 a 64 High Risk, de 20 a 39 Suspicious, por debajo de 20 Benign.
Una progresión razonable:
| Fase | Puerta | Razón |
|---|---|---|
| Semana 1 | Malicious |
Fijar la línea base sin bloquear nada |
| Tras un mes limpio | Malicious\|High Risk |
Ya se conoce la puntuación normal de la propia app |
| Madurez | Añadir Suspicious sólo en ramas de release |
Las ramas de feature siguen rápidas |
El orden es lo importante. Apretar la puerta antes de conocer la puntuación normal de la aplicación produce una primera semana de fallos falsos, y un equipo que la apaga.
Cómo tratar el fallo
Una build que falla por veredicto tiene que decirle al desarrollador qué hacer después, y la distinción que importa es entre esta build introdujo algo y esta aplicación siempre ha puntuado así.
El runner emite cuatro salidas —identificador de ejecución, veredicto, puntuación y una URL para compartir—, y la URL es la que cambia la experiencia. Es un enlace público al informe completo que un desarrollador puede abrir sin cuenta, lo que elimina el paso en el que abre un ticket preguntando qué era el hallazgo.
Tres categorías de fallo y qué significa cada una:
La puntuación se movió. Compárala con la de la build anterior. Un salto suele significar que cambió una dependencia. Éste es el caso para el que existe la puerta.
La puntuación siempre ha sido ésta. La puerta se apretó más allá de la línea base de la aplicación. O se corrigen los hallazgos de fondo o se ensancha la puerta deliberadamente, pero como decisión registrada y no desactivando el paso.
Falló el propio escaneo. Hay que distinguirlo de un fallo de política. El runner usa códigos de salida separados: 1 significa que el veredicto coincidió con la puerta, 2 que el análisis falló, 3 que expiró el tiempo, 4 que falta una dependencia o un fichero, 5 un error HTTP. Tratar un fallo de infraestructura como un fallo de seguridad enseña al equipo a ignorar los dos.
Un pipeline que reporta el código 3 como «falló el escaneo de seguridad» habrá enseñado, en un trimestre, a todos los ingenieros que el escaneo de seguridad es inestable. Asigna los códigos 2 a 5 a un estado distinto —un aviso, una alerta de infraestructura, un reintento— y reserva la build en rojo para el código 1.
Tiempos
El análisis tarda entre dos y cinco minutos para un APK de 50 MB, y de ocho a diez de media por encima de 100 MB. Es suficiente para notarlo en cada commit y lo bastante corto para pasar desapercibido en una rama de release.
Medido sobre informes de producción el 19 de septiembre de 2026:
| Tamaño del paquete | Tiempo de análisis |
|---|---|
| 48 MB | 157 s |
| 97 MB | 503 s |
| 111 MB | 611 s |
| 165 MB | 595 s |
Dos consecuencias prácticas. Ejecuta la puerta completa en ramas de release y en integraciones a la rama principal, no en cada push a una rama de feature. Y pon timeout_seconds por encima de la duración esperada del artefacto más grande: los 300 segundos por defecto van holgados para una aplicación de 50 MB y justos para una de 150 MB.
Hay un atajo que conviene programar en el pipeline. Cuando un artefacto ya se analizó antes, la llamada de análisis devuelve cached: true con el identificador de trabajo a nulo y el informe existente; no hay nada que sondear. Relanzar un pipeline sobre un artefacto sin cambios cuesta segundos en lugar de minutos, lo que abarata el reintento tras un fallo de infraestructura ajeno.
Las integraciones
Droidwatch ofrece seis integraciones de CI/CD: GitHub Actions, GitLab CI/CD, Bitrise, Jenkins, CircleCI y Azure Pipelines.
Las seis llaman al mismo script runner, que cada envoltorio descarga en tiempo de build en vez de incorporarlo al repositorio. El efecto práctico es que una corrección del runner llega a todos los pipelines en su siguiente ejecución, sin versiones que perseguir. Las dependencias son curl y jq: ni Python, ni SDK que instalar.
Un paso de GitHub Actions son cuatro líneas:
- uses: Omar1123/droidwatch-scan@v1
with:
apk_path: app/build/outputs/apk/release/app-release.apk
api_key: ${{ secrets.DROIDWATCH_API_KEY }}
La clave de API va en el almacén de secretos del CI y no en el fichero de workflow, y el acceso a la API arranca en el plan Pro. Los límites diarios de análisis aplican por plan —100 en Pro, 500 en Team—, que es el número a contrastar con el volumen del pipeline antes de desplegar la puerta en todos los repositorios de una organización.
iOS en el mismo pipeline
Los artefactos .ipa se admiten y se analizan de forma estática en siete módulos: binario Mach-O, Info.plist, entitlements, configuración de App Transport Security, class-dump, frameworks enlazados y manifiestos de privacidad declarada.
No hay instrumentación en ejecución para iOS, en ningún plan. Para una puerta de build eso importa menos de lo que parece: la puerta existe para cazar regresiones de configuración y de dependencias, y ésas son propiedades estáticas. Una excepción de ATS añadida para sacar una funcionalidad, un entitlement que no debería estar en una build de release, un framework que nadie revisó: todo visible sin ejecutar nada.
Lo que la puerta de iOS no hará es establecer comportamiento. Conviene dejarlo escrito en la documentación interna del pipeline, porque la alternativa es un equipo que da por supuesta una cobertura que no tiene.
Lo que esto no sustituye
Una puerta de pipeline caza lo que produjo la build. No caza lo que otro publicó bajo la marca de la organización: la versión reempaquetada de una aplicación bancaria distribuida por smishing no pasa por el CI de la entidad en ningún momento.
Eso es otro flujo: vigilar paquetes que suplantan la aplicación y contrastar sus certificados de firma con el legítimo. El flujo de triaje explica cómo se ejecuta ese análisis y qué produce. Merece la pena construirlo, y merece la pena no confundirlo con la puerta de build.
Tampoco sustituye a leer el informe. Un veredicto es una decisión de enrutado; la evidencia que lo sostiene está en los hallazgos, y el recorrido del informe explica dónde vive cada parte de esa evidencia.
Para montar un escaneo en el pipeline, consulta la guía de CI/CD.