Aprendizaje automático en análisis estático del código fuente del programa

Aprendizaje automático en análisis estático del código fuente del programa

El aprendizaje automático se ha arraigado firmemente en una variedad de campos humanos, desde el reconocimiento del habla hasta el diagnóstico médico. La popularidad de este enfoque es tan grande que las personas intentan usarlo siempre que pueden. Algunos intentos de reemplazar los enfoques clásicos con redes neuronales resultan infructuosos. Esta vez consideraremos el aprendizaje automático en términos de crear analizadores de código estático efectivos para encontrar errores y vulnerabilidades potenciales.

A menudo se pregunta al equipo de PVS-Studio si queremos comenzar a utilizar el aprendizaje automático para encontrar errores en el código fuente del software. La respuesta corta es sí, pero hasta cierto punto. Creemos que con el aprendizaje automático, existen muchas dificultades al acecho en las tareas de análisis de código. En la segunda parte del artículo, hablaremos sobre ellos. Comencemos con una revisión de nuevas soluciones e ideas.

Nuevos enfoques


Hoy en día hay muchos analizadores estáticos basados ​​en el aprendizaje automático o que lo utilizan, incluido el aprendizaje profundo y la PNL para la detección de errores. Los entusiastas no solo duplicaron el potencial de aprendizaje automático, sino también las grandes empresas, por ejemplo, Facebook, Amazon o Mozilla. Algunos proyectos no son analizadores estáticos completos, ya que solo encuentran algunos errores determinados en las confirmaciones.

Curiosamente, casi todos ellos se posicionan como productos que cambian el juego que harán un gran avance en el proceso de desarrollo debido a la inteligencia artificial.



Veamos algunos de los ejemplos conocidos:

  1. Deepcode
  2. Inferir, Sapienz, SapFix
  3. Embold
  4. Fuente {d}
  5. Compromiso inteligente, asistente de compromiso
  6. CodeGuru

Deepcode


Deep Code es una herramienta de búsqueda de vulnerabilidades para el código de software Java, JavaScript, TypeScript y Python que presenta el aprendizaje automático como un componente. Según Boris Paskalev, ya se han implementado más de 250,000 reglas. Esta herramienta aprende de los cambios realizados por los desarrolladores en el código fuente de proyectos de código abierto (un millón de repositorios). La propia empresa dice que su proyecto es una especie de gramática para los desarrolladores.



De hecho, este analizador compara su solución con su base de proyectos y le ofrece la mejor solución prevista de la experiencia de otros desarrolladores.

En mayo de 2018, los desarrolladores dijeron que el soporte de C ++ está en camino, pero hasta ahora, este lenguaje no es compatible. Aunque, como se indica en el sitio, el nuevo soporte de idioma se puede agregar en cuestión de semanas debido al hecho de que el idioma depende solo de una etapa, que es el análisis.





Una serie de publicaciones sobre métodos básicos del analizador también está disponible en el sitio.

Inferir


Facebook es bastante celoso en sus intentos de introducir nuevos enfoques integrales en sus productos. El aprendizaje automático tampoco se mantuvo al margen. En 2013, compraron una startup que desarrolló un analizador estático basado en aprendizaje automático. Y en 2015, el código fuente del proyecto se abrió .

Infer es un analizador estático para proyectos en Java, C, C ++ y Objective-C, desarrollado por Facebook. Según el sitio, también se usa en Amazon Web Services, Oculus, Uber y otros proyectos populares.

Actualmente, Infer puede encontrar errores relacionados con la desreferencia de puntero nulo y pérdidas de memoria. Infer se basa en la lógica de Hoare, la lógica de separación y la bi-abducción, así como en la teoría de la interpretación abstracta. El uso de estos enfoques permite al analizador dividir el programa en fragmentos y analizarlos de forma independiente.

Puede intentar usar Infer en sus proyectos, pero los desarrolladores advierten que si bien con los proyectos de Facebook genera aproximadamente el 80% de las advertencias útiles, un bajo número de falsos positivos no está garantizado en otros proyectos. Aquí hay algunos errores que Infer no puede detectar hasta ahora, pero los desarrolladores están trabajando en implementar estas advertencias:

  • índice de matriz fuera de límites;
  • excepciones de fundición de tipos;
  • fugas de datos no verificadas;
  • condición de carrera

Sapfix


SapFix es una herramienta de edición automatizada. Recibe información de Sapienz, una herramienta de automatización de pruebas, y el analizador estático Infer. Según los cambios y mensajes recientes, Infer selecciona una de varias estrategias para corregir errores.



En algunos casos, SapFix revierte todos los cambios o partes de ellos. En otros casos, intenta resolver el problema generando un parche a partir de su conjunto de patrones de fijación. Este conjunto se forma a partir de patrones de arreglos recopilados por los propios programadores a partir de un conjunto de arreglos que ya se hicieron. Si dicho patrón no soluciona un error, SapFix intenta ajustarlo a la situación haciendo pequeñas modificaciones en un árbol de sintaxis abstracta hasta que se encuentre la posible solución.

Pero una solución potencial no es suficiente, por lo que SapFix recopila varias soluciones 'sobre la base de un par de puntos: si hay errores de compilación, si falla o si introduce nuevos bloqueos. Una vez que las ediciones se prueban completamente, los parches son revisados ​​por un programador, quien decidirá cuál de las ediciones resuelve mejor el problema.

Embold


Embold es una plataforma de inicio para el análisis estático del código fuente del software que se llamó Gamma antes del cambio de nombre. El analizador estático funciona en función de los diagnósticos propios de la herramienta, así como el uso de analizadores integrados, como Cppcheck, SpotBugs, SQL Check y otros.



Además de los diagnósticos en sí, la plataforma se centra en infografías vívidas sobre la carga de la base de código y la visualización conveniente de los errores encontrados, así como en la búsqueda de una posible refactorización. Además, este analizador tiene un conjunto de antipatrones que le permiten detectar problemas en la estructura del código a nivel de clase y método, y varias métricas para calcular la calidad de un sistema.



Una de las principales ventajas es el sistema inteligente de ofrecer soluciones y ediciones, que, además de los diagnósticos convencionales, verifica las ediciones en función de la información sobre cambios anteriores.



Con NLP, Embold separa el código y busca interconexiones y dependencias entre funciones y métodos, ahorrando tiempo de refactorización.



De esta manera, Embold básicamente ofrece una visualización conveniente de los resultados de su análisis de código fuente por varios analizadores, así como por sus propios diagnósticos, algunos de los cuales se basan en el aprendizaje automático.

Fuente {d}


Source {d} es la herramienta más abierta en términos de las formas de su implementación en comparación con los analizadores que hemos revisado. También es una solución de código fuente abierto . En su sitio web, a cambio de su dirección de correo electrónico, puede obtener un folleto del producto que describe las tecnologías que utilizan. Además, el sitio web proporciona un enlace a la base de datos de publicaciones relacionadas con el uso del aprendizaje automático para el análisis de código, así como el repositorio con el conjunto de datos para el aprendizaje basado en código. El producto en sí es una plataforma completa para analizar el código fuente y el producto de software, y no se centra en los desarrolladores, sino en los gerentes. Entre sus capacidades está el cálculo del tamaño de la deuda técnica, los cuellos de botella en el proceso de desarrollo y otras estadísticas globales sobre el proyecto.



Su enfoque para el análisis de código a través del aprendizaje automático se basa en la hipótesis natural, como se describe en el artículo " Sobre la naturalidad del software ".

"Los lenguajes de programación, en teoría, son complejos, flexibles y potentes, pero los programas que las personas reales escriben son en su mayoría simples y bastante repetitivos, y por lo tanto tienen propiedades estadísticas predecibles que pueden capturarse en modelos de lenguaje estadístico y aprovecharse para la ingeniería de software tareas ".

Según esta hipótesis, cuanto más grande sea la base del código, mayores serán las propiedades estadísticas y más precisas serán las métricas obtenidas mediante el aprendizaje.

Para analizar el código en la fuente {d}, se utiliza el servicio Babelfish, que puede analizar el archivo de código en cualquiera de los idiomas disponibles, obtener un árbol de sintaxis abstracta y convertirlo en un árbol de sintaxis universal.



Sin embargo, la fuente {d} no busca errores en el código. Basado en el árbol que usa ML en todo el proyecto, la fuente {d} detecta el formato del código, el estilo aplicado en el proyecto y en una confirmación. Si el nuevo código no se corresponde con el estilo del código del proyecto, realiza algunas modificaciones.





El aprendizaje se centra en varios elementos básicos: espacios, tabulación, saltos de línea, etc.



Lea más sobre esto en su publicación: " STYLE-ANALYZER: arreglando inconsistencias de estilo de código con algoritmos interpretables no supervisados ".

En general, source {d} es una plataforma amplia para recopilar estadísticas diversas sobre el código fuente y el proceso de desarrollo del proyecto: desde los cálculos de eficiencia de los desarrolladores hasta los costos de tiempo para la revisión del código.

Compromiso inteligente


Clever-Commit es un analizador creado por Mozilla en colaboración con Ubisoft. Se basa en un estudio CLEVER (Combinación de niveles de prevención de errores y técnicas de resolución) realizado por Ubisoft y su asistente de compromiso de producto secundario, que detecta confirmaciones sospechosas que pueden contener un error. Dado que CLEVER se basa en la comparación de códigos, puede apuntar a códigos peligrosos y hacer sugerencias para posibles ediciones. Según la descripción, en 60-70% de los casos, Clever-Commit encuentra lugares problemáticos y ofrece ediciones correctas con la misma probabilidad. En general, hay poca información sobre este proyecto y sobre los errores que puede encontrar.

CodeGuru


Recientemente CodeGuru, que es un producto de Amazon, se ha alineado con los analizadores que utilizan el aprendizaje automático. Es un servicio de aprendizaje automático que le permite encontrar errores en el código, así como identificar áreas costosas en él. El análisis solo está disponible para el código Java hasta ahora, pero los autores prometen admitir otros idiomas en el futuro. Aunque se anunció recientemente, Andy Jassy, ​​CEO de AWS (Amazon Web Services) dice que se ha utilizado en Amazon durante mucho tiempo.

El sitio web dice que CodeGuru estaba aprendiendo en la base de código de Amazon, así como en más de 10,000 proyectos de código abierto.

Básicamente, el servicio se divide en dos partes: CodeGuru Reviewer, que se enseña utilizando la búsqueda de reglas asociativas y busca errores en el código, y CodeGuru Profiler, que supervisa el rendimiento de las aplicaciones.



En general, no hay mucha información disponible sobre este proyecto. Como dice el sitio web, el Revisor analiza las bases de códigos de Amazon y busca solicitudes de extracción, que contienen las llamadas a la API de AWS para aprender cómo detectar las desviaciones de las "mejores prácticas". A continuación, analiza los cambios realizados y los compara con los datos de la documentación, que se analiza al mismo tiempo. El resultado es un modelo de "mejores prácticas".

También se dice que las recomendaciones para el código del usuario tienden a mejorar después de recibir comentarios sobre ellas.

La lista de errores a los que responde Reviewer es bastante borrosa, ya que no se ha publicado documentación específica de errores:

  • Mejores prácticas AWS
  • Concurrencia
  • Fugas de recursos
  • Fuga de información confidencial.
  • "Mejores prácticas" generales de codificación

Nuestro escepticismo


Ahora consideremos la búsqueda de errores desde el punto de vista de nuestro equipo, que ha estado desarrollando analizadores estáticos durante muchos años. Vemos una serie de problemas de alto nivel de la aplicación de métodos de aprendizaje, que nos gustaría cubrir. Para comenzar, dividiremos todos los enfoques de ML en dos tipos:

  1. Los que enseñan manualmente un analizador estático para buscar diversos problemas, utilizando ejemplos de código sintético y real;
  2. Aquellos que enseñan algoritmos en una gran cantidad de código abierto e historial de revisión (GitHub), después de lo cual el analizador comenzará a detectar errores e incluso ofrecerá ediciones.

Hablaremos de cada dirección por separado, ya que tienen diferentes inconvenientes. Después de eso, creo que los lectores entenderán por qué no negamos las posibilidades del aprendizaje automático, pero aún así no compartimos el entusiasmo.

Nota Miramos desde la perspectiva del desarrollo de un analizador universal estático de propósito general. Estamos enfocados en desarrollar el analizador, que cualquier equipo podrá usar, no el enfocado en una base de código específica.

Enseñanza manual de un analizador estático


Digamos que queremos usar ML para comenzar a buscar los siguientes tipos de fallas en el código:

if (A == A) 

Es extraño comparar una variable consigo misma. Podemos escribir muchos ejemplos de código correcto e incorrecto y enseñarle al analizador a buscar tales errores. Además, puede agregar ejemplos reales de errores ya encontrados a las pruebas. Bueno, la pregunta es dónde encontrar esos ejemplos. Ok, supongamos que es posible. Por ejemplo, tenemos varios ejemplos de tales errores: V501 , V3001 , V6001 .

Entonces, ¿es posible identificar tales defectos en el código utilizando los algoritmos ML? Si lo es. La cuestión es: ¿por qué lo necesitamos?

Vea, para enseñar al analizador necesitaremos dedicar muchos esfuerzos a preparar los ejemplos para la enseñanza. Otra opción es marcar el código de las aplicaciones reales, indicando los fragmentos donde el analizador debe emitir una advertencia. En cualquier caso, habrá que trabajar mucho, ya que debe haber miles de ejemplos para aprender. O decenas de miles.

Después de todo, queremos detectar no solo los casos (A == A), sino también:

  • si (X && A == A)
  • si (A + 1 == A + 1)
  • si (A [i] == A [i])
  • si ((A) == (A))
  • Y así sucesivamente.


Veamos la posible implementación de un diagnóstico tan simple en PVS-Studio:

 void RulePrototype_V501(VivaWalker &walker, const Ptree *left, const Ptree *right, const Ptree *operation) { if (SafeEq(operation, "==") && SafeEqual(left, right)) { walker.AddError("Oh boy! Holy cow!", left, 501, Level_1, "CWE-571"); } } 

Y eso es todo! ¡No necesitas ninguna base de ejemplos para ML!

En el futuro, el diagnóstico debe aprender a tener en cuenta una serie de excepciones y emitir advertencias para (A [0] == A [1-1]). Como sabemos, se puede programar fácilmente. Por el contrario, en este caso, las cosas van a ser malas con la base de los ejemplos.

Tenga en cuenta que en ambos casos necesitaremos un sistema de pruebas, documentación, etc. En cuanto a la contribución laboral en la creación de un nuevo diagnóstico, el enfoque clásico, donde la regla está rígidamente programada en el código, toma la delantera.

Ok, es hora de otra regla. Por ejemplo, aquella en la que se debe utilizar el resultado de algunas funciones. No tiene sentido llamarlos y no usar su resultado. Estas son algunas de esas funciones:

  • malloc
  • memcmp
  • cadena :: vacío

Esto es lo que hace el diagnóstico PVS-Studio V530 .

Entonces, lo que queremos es detectar llamadas a tales funciones, cuyo resultado no se utiliza. Para hacer esto, puede generar muchas pruebas. Y creemos que todo funcionará bien. Pero, de nuevo, no está claro por qué es necesario.

La implementación de diagnóstico V530 con todas las excepciones tomó 258 líneas de código en el analizador PVS-Studio, 64 de los cuales son comentarios. También hay una tabla con anotaciones de funciones, donde se observa que se debe usar su resultado. Es mucho más fácil completar esta tabla que crear ejemplos sintéticos.

Las cosas empeorarán aún más con los diagnósticos que usan análisis de flujo de datos. Por ejemplo, el analizador PVS-Studio puede rastrear el valor de los punteros, lo que le permite encontrar una pérdida de memoria:

 uint32_t* BnNew() { uint32_t* result = new uint32_t[kBigIntSize]; memset(result, 0, kBigIntSize * sizeof(uint32_t)); return result; } std::string AndroidRSAPublicKey(crypto::RSAPrivateKey* key) { .... uint32_t* n = BnNew(); .... RSAPublicKey pkey; pkey.len = kRSANumWords; pkey.exponent = 65537; // Fixed public exponent pkey.n0inv = 0 - ModInverse(n0, 0x100000000LL); if (pkey.n0inv == 0) return kDummyRSAPublicKey; // <= .... } 

El ejemplo está tomado del artículo " Cromo: Fugas de memoria ". Si la condición (pkey.n0inv == 0) es verdadera, la función sale sin liberar el búfer, cuyo puntero se almacena en la variable n .

Desde el punto de vista del PVS-Studio, aquí no hay nada complicado. El analizador ha estudiado la función BnNew y recordó que devolvió un puntero al bloque de memoria asignado. En otra función, notó que el búfer podría no liberarse y el puntero hacia él se pierde al momento de salir de la función.

Es un algoritmo común de seguimiento de valores de trabajo. No importa cómo se escriba el código. No importa qué más hay en la función que no se relacione con el trabajo del puntero. El algoritmo es universal y el diagnóstico V773 encuentra muchos errores en varios proyectos. ¡Vea cuán diferentes son los fragmentos de código con errores detectados!

No somos expertos en ML, pero tenemos la sensación de que aquí hay grandes problemas a la vuelta de la esquina. Hay una increíble cantidad de formas en que puede escribir código con pérdidas de memoria. Incluso si la máquina aprendiera bien cómo rastrear valores de variables, necesitaría comprender que también hay llamadas a funciones.

Sospechamos que requeriría tantos ejemplos para aprender que la tarea se vuelve indescifrable. No estamos diciendo que no sea realista. Dudamos de que el costo de crear el analizador valga la pena.

Analogía Lo que viene a mi mente es la analogía con una calculadora, donde en lugar de diagnósticos, uno tiene que programar acciones aritméticas. Estamos seguros de que puede enseñarle a una calculadora basada en ML a resumir bien los números alimentándolos con los resultados de las operaciones 1 + 1 = 2, 1 + 2 = 3, 2 + 1 = 3, 100 + 200 = 300, etc. . Como comprenderá, la viabilidad de desarrollar una calculadora de este tipo es una gran pregunta (a menos que se le asigne una subvención :). Se puede escribir una calculadora mucho más simple, más rápida, más precisa y confiable usando la operación simple "+" en el código.

Conclusión Bueno, de esta manera funcionará. Pero usarlo, en nuestra opinión, no tiene sentido práctico. El desarrollo requerirá más tiempo, pero el resultado será menos confiable y preciso, especialmente cuando se trata de implementar diagnósticos complejos basados ​​en análisis de flujo de datos.

Aprender sobre una gran cantidad de código fuente abierto


Bien, hemos resuelto con ejemplos sintéticos manuales, pero también está GitHub. Puede realizar un seguimiento del historial de confirmaciones y deducir el cambio de código / patrones de fijación. Entonces puede señalar no solo fragmentos de código sospechoso, sino incluso sugerir una forma de arreglar el código.

Si se detiene en este nivel de detalle, todo se ve bien. El diablo, como siempre, está en los detalles. Así que hablemos bien de estos detalles.

El primer matiz. Fuente de datos

Las ediciones de GitHub son bastante aleatorias y diversas. Las personas a menudo son flojas para realizar confirmaciones atómicas y realizar varias ediciones en el código al mismo tiempo. Ya sabes cómo sucede: corregirías el error y al mismo tiempo lo refactorizarías un poco ("Y aquí agregaré el manejo de tal caso ..."). Incluso una persona puede ser incomprensible, ya sea que estos arreglos estén relacionados entre sí o no.

El desafío es cómo distinguir los errores reales de agregar nueva funcionalidad u otra cosa. Por supuesto, puede obtener 1000 personas que marcarán manualmente las confirmaciones. La gente tendrá que señalar: aquí se corrigió un error, aquí se está refactorizando, aquí hay alguna funcionalidad nueva, aquí los requisitos han cambiado, etc.

¿Es posible tal marcado? Si! Pero observe qué tan rápido ocurre la suplantación de identidad. En lugar de "el algoritmo se aprende sobre la base de GitHub", ya estamos discutiendo cómo confundir a cientos de personas durante mucho tiempo. El trabajo y el costo de crear la herramienta está aumentando dramáticamente.

Puede intentar identificar automáticamente dónde se solucionaron los errores. Para hacer esto, debe analizar los comentarios a los commits, prestar atención a las pequeñas ediciones locales, que probablemente sean esas correcciones de errores. Es difícil saber qué tan bien puede buscar automáticamente las correcciones de errores. En cualquier caso, esta es una gran tarea que requiere investigación y programación por separado.

Entonces, aún no hemos llegado a aprender, y ya hay matices :).

El segundo matiz. Un retraso en el desarrollo.

Analizadores que aprenderán en base a tales plataformas, ya que GitHub siempre estará sujeto a dicho síndrome, como "retraso por retraso mental". Esto se debe a que los lenguajes de programación cambian con el tiempo.

Desde C # 8.0 ha habido tipos de referencia anulables, lo que ayuda a luchar contra las excepciones de referencia nula (NRE). En JDK 12, apareció un nuevo operador de conmutador ( JEP 325 ). En C ++ 17, existe la posibilidad de realizar construcciones condicionales en tiempo de compilación ( constexpr si ). Y así sucesivamente.

Los lenguajes de programación están evolucionando. Además, los que, como C ++, se desarrollan muy rápido. Aparecen nuevas construcciones, se agregan nuevas funciones estándar y así sucesivamente. Junto con las nuevas características, hay nuevos patrones de error que también nos gustaría identificar con el análisis de código estático.

En este punto, el método ML enfrenta un problema: el patrón de error ya está claro, nos gustaría detectarlo, pero no hay una base de código para el aprendizaje.

Miremos este problema usando un ejemplo particular. El rango basado en el rango apareció en C ++ 11. Puede escribir el siguiente código, atravesando todos los elementos en el contenedor:

 std::vector<int> numbers; .... for (int num : numbers) foo(num); 

El nuevo bucle ha traído consigo el nuevo patrón de error. Si cambiamos el contenedor dentro del bucle, esto conducirá a la invalidación de los iteradores "sombra".

Echemos un vistazo al siguiente código incorrecto:

 for (int num : numbers) { numbers.push_back(num * 2); } 

El compilador lo convertirá en algo como esto:

 for (auto __begin = begin(numbers), __end = end(numbers); __begin != __end; ++__begin) { int num = *__begin; numbers.push_back(num * 2); } 

Durante push_back , los iteradores __begin y __end pueden ser invalidados, si la memoria se reubica dentro del vector. El resultado será el comportamiento indefinido del programa.

Por lo tanto, el patrón de error se conoce y describe desde hace tiempo en la literatura. El analizador PVS-Studio lo diagnostica con el diagnóstico V789 y ya ha encontrado errores reales en proyectos de código abierto.

¿Qué tan pronto GitHub obtendrá suficiente código nuevo para notar este patrón? Buena pregunta ... Es importante tener en cuenta que si hay un bucle for basado en rango, no significa que todos los programadores comenzarán a usarlo de inmediato. Pueden pasar años antes de que haya mucho código usando el nuevo bucle. Además, se deben cometer muchos errores, y luego se deben corregir para que el algoritmo pueda notar el patrón en las ediciones.

¿Cuántos años llevará? Cinco? Diez?

Diez es demasiado, ¿o es una predicción pesimista? Lejos de eso. Cuando se escribió el artículo, habían pasado ocho años desde que apareció un bucle basado en rango para C ++ 11. Pero hasta ahora en nuestra base de datos solo hay tres casos de tal error. Tres errores no son muchos ni pocos. Uno no debería sacar ninguna conclusión de este número. Lo principal es confirmar que dicho patrón de error es real y tiene sentido detectarlo.

Ahora compare este número, por ejemplo, con este patrón de error: el puntero se desreferencia antes de la verificación . En total, ya hemos identificado 1.716 casos de este tipo al verificar proyectos de código abierto.

¿Quizás no deberíamos buscar errores en los bucles basados ​​en rangos? No Es solo que los programadores son inerciales, y este operador se está volviendo popular muy lentamente. Gradualmente, habrá más código y errores, respectivamente.

Es probable que esto suceda solo 10-15 años después de la aparición de C ++ 11. Esto lleva a una pregunta filosófica. Supongamos que ya conocemos el patrón de error, solo esperaremos muchos años hasta que tengamos muchos errores en proyectos de código abierto. ¿Será así?

En caso afirmativo, es seguro diagnosticar el "retraso del desarrollo mental" para todos los analizadores basados ​​en ML.

Si "no", ¿qué debemos hacer? No hay ejemplos ¿Escribirlos manualmente? Pero de esta manera, volvemos al capítulo anterior, donde hemos dado una descripción detallada de la opción cuando la gente escribiría un paquete completo de ejemplos para aprender.

Esto se puede hacer, pero la cuestión de la conveniencia surge nuevamente. La implementación del diagnóstico V789 con todas las excepciones en el analizador PVS-Studio toma solo 118 líneas de código, de las cuales 13 líneas son comentarios. Es decir, es un diagnóstico muy simple, que se puede programar fácilmente de una manera clásica.

La situación será similar a cualquier otra innovación que aparezca en otros idiomas. Como dicen, hay algo en qué pensar.

El tercer matiz. Documentación

Un componente importante de cualquier analizador estático es la documentación que describe cada diagnóstico. Sin él, será extremadamente difícil o imposible usar el analizador. En la documentación de PVS-Studio, tenemos una descripción de cada diagnóstico, que ofrece un ejemplo de código erróneo y cómo solucionarlo. También damos el enlace a CWE , donde se puede leer una descripción alternativa del problema. Y aún así, a veces los usuarios no entienden algo y nos hacen preguntas aclaratorias.

En el caso de los analizadores estáticos basados ​​en ML, el problema de la documentación se oculta de alguna manera. Se supone que el analizador simplemente señalará un lugar que le parece sospechoso e incluso puede sugerir cómo solucionarlo. La decisión de hacer una edición o no depende de la persona. Ahí es donde comienza el problema ... No es fácil tomar una decisión sin poder leer, lo que hace que el analizador parezca sospechoso de un lugar particular en el código.

Por supuesto, en algunos casos, todo será obvio. Supongamos que el analizador apunta a este código:

 char *p = (char *)malloc(strlen(src + 1)); strcpy(p, src); 

Y sugiera que lo reemplacemos con:

 char *p = (char *)malloc(strlen(src) + 1); strcpy(p, src); 

Está claro de inmediato que el programador cometió un error tipográfico y agregó 1 en el lugar equivocado. Como resultado, se asignará menos memoria de la necesaria.

Aquí está todo claro, incluso sin documentación. Sin embargo, este no siempre será el caso.

Imagine que el analizador "silenciosamente" señala este código:

 char check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

Y sugiere que cambiemos el tipo char del valor de retorno para int:

 int check(const uint8 *hash_stage2) { .... return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE); } 

No hay documentación para la advertencia. Aparentemente, tampoco habrá texto en el mensaje de advertencia si hablamos de un analizador completamente independiente.

Que haremos Cual es la diferencia ¿Vale la pena hacer tal reemplazo?

En realidad, podría arriesgarme y aceptar arreglar el código. Aunque aceptar arreglos sin entenderlos es una práctica poco convincente ... :) Puede consultar la descripción de la función memcmp y descubrir que la función realmente devuelve valores como int : 0, más de cero y menos de cero. Pero aún no está claro por qué hacer ediciones, si el código ya está funcionando bien.

Ahora, si no sabe cuál es la edición, consulte la descripción del diagnóstico V642 . De inmediato queda claro que este es un error real. Además, puede causar una vulnerabilidad.

Quizás, el ejemplo parecía poco convincente. Después de todo, el analizador sugirió un código que probablemente sea mejor. Ok Veamos otro ejemplo de pseudocódigo, esta vez, para variar, en Java.

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj.state = 200; out.writeObject(obj); out.close(); 

Hay un objeto Está serializando. Luego, el estado del objeto cambia y se vuelve a serializar. Se ve bien Ahora imagine que, de repente, al analizador no le gusta el código y quiere reemplazarlo con lo siguiente:

 ObjectOutputStream out = new ObjectOutputStream(....); SerializedObject obj = new SerializedObject(); obj.state = 100; out.writeObject(obj); obj = new SerializedObject(); // The line is added obj.state = 200; out.writeObject(obj); out.close(); 

En lugar de cambiar el objeto y reescribirlo, se crea un nuevo objeto y se serializará.

No hay una descripción del problema. Sin documentación El código se ha vuelto más largo. Por alguna razón, se crea un nuevo objeto. ¿Estás listo para hacer tal edición en tu código?

Dirás que no está claro. De hecho, es incomprensible. Y así será todo el tiempo. Trabajar con un analizador tan "silencioso" será un estudio interminable en un intento por comprender por qué al analizador no le gusta nada.

Si hay documentación, todo se vuelve transparente. La clase java.io.ObjectOuputStream que se utiliza para la serialización almacena en caché los objetos escritos. Esto significa que el mismo objeto no se serializará dos veces. La clase serializa el objeto una vez, y la segunda solo escribe en la secuencia una referencia al mismo primer objeto. Leer más: V6076 : la serialización recurrente utilizará el estado del objeto en caché desde la primera serialización.

Esperamos haber logrado explicar la importancia de la documentación. Aquí viene la pregunta. ¿Cómo aparecerá la documentación para el analizador basado en ML?

Cuando se desarrolla un analizador de código clásico, todo es simple y claro. Hay un patrón de errores. Lo describimos en la documentación e implementamos el diagnóstico.

En el caso de ML, el proceso es inverso. Sí, el analizador puede notar una anomalía en el código y señalarlo. Pero no sabe nada sobre la esencia del defecto. No entiende y no le dirá por qué no puede escribir código como ese. Estas son abstracciones de alto nivel. De esta manera, el analizador también debe aprender a leer y comprender la documentación de las funciones.

Como dije, dado que el problema de la documentación se evita en los artículos sobre aprendizaje automático, no estamos listos para profundizar más en él. Solo otro gran matiz que hemos hablado.

Nota Se podría argumentar que la documentación es opcional. El analizador puede referirse a muchos ejemplos de correcciones en GitHub y la persona, mirando a través de las confirmaciones y comentarios a ellos, comprenderá qué es qué. Si es asi. Pero la idea no parece atractiva. Aquí, el analizador es el tipo malo, que más bien desconcertará a un programador que lo ayudará.

Cuarto matiz. Idiomas altamente especializados.

El enfoque descrito no es aplicable a lenguajes altamente especializados, para los cuales el análisis estático también puede ser extremadamente útil. La razón es que GitHub y otras fuentes simplemente no tienen una base de código fuente lo suficientemente grande como para proporcionar un aprendizaje efectivo.

Miremos esto usando un ejemplo concreto. Primero, vayamos a GitHub y busquemos repositorios para el popular lenguaje Java.

Resultado: lenguaje: "Java": 3.128.884 resultados de repositorio disponibles

Ahora tome el lenguaje especializado "1C Enterprise" utilizado en aplicaciones de contabilidad producidas por la compañía rusa 1C .

Resultado: idioma: "1C Enterprise": 551 resultados de repositorio disponibles

¿Quizás no se necesitan analizadores para este idioma? No lo son. Existe una necesidad práctica de analizar dichos programas y ya hay analizadores apropiados. Por ejemplo, hay SonarQube 1C (BSL) Plugin, producido por la compañía " Silver Bullet ".

Creo que no se necesitan explicaciones específicas de por qué el enfoque ML será difícil para los idiomas especializados.

El quinto matiz. C, C ++, #include .

Los artículos sobre análisis de código estático basados ​​en ML tratan principalmente de lenguajes como Java, JavaScript y Python. Esto se explica por su extrema popularidad. En cuanto a C y C ++, se ignoran, aunque no se les puede llamar impopulares.

Sugerimos que no se trata de su popularidad / perspectiva prometedora, sino de los problemas con los lenguajes C y C ++. Y ahora vamos a sacar a la luz un problema incómodo.

Un archivo c / cpp abstracto puede ser muy difícil de compilar. Al menos no puede cargar un proyecto desde GitHub, elija un archivo cpp aleatorio y simplemente compílelo. Ahora explicaremos qué tiene que ver todo esto con ML.

Por eso queremos enseñarle al analizador. Descargamos un proyecto de GitHub. Conocemos el parche y asumimos que corrige el error. Queremos que esta edición sea un ejemplo para aprender. En otras palabras, tenemos un archivo .cpp antes y después de editarlo.

Ahí es donde comienza el problema. No es suficiente solo estudiar las soluciones. También se requiere un contexto completo. Debe conocer la declaración de las clases utilizadas, debe conocer los prototipos de las funciones utilizadas, debe saber cómo se expanden las macros, etc. Y para hacer esto, debe realizar el preprocesamiento completo de los archivos.

Veamos el ejemplo. Al principio, el código se veía así:

 bool Class::IsMagicWord() { return m_name == "ML"; } 

Se solucionó de esta manera:

 bool Class::IsMagicWord() { return strcmp(m_name, "ML") == 0; } 

¿Debería el analizador comenzar a aprender para sugerir (x == "y") reemplazo de strtrmp (x, "y")?

No puede responder esa pregunta sin saber cómo se declara el miembro m_name en la clase. Puede haber, por ejemplo, tales opciones:

 class Class { .... char *m_name; }; class Class { .... std::string m_name; }; 

Se realizarán ediciones en caso de que estemos hablando de un puntero ordinario. Si no tenemos en cuenta el tipo de variable, el analizador podría aprender a emitir advertencias buenas y malas (para el caso de std :: string ).

Las declaraciones de clase generalmente se encuentran en archivos de encabezado. Aquí se enfrentan a la necesidad de realizar un preprocesamiento para tener toda la información necesaria. Es extremadamente importante para C y C ++.

Si alguien dice que es posible hacerlo sin preprocesamiento, es un fraude o simplemente no está familiarizado con los lenguajes C o C ++.

Para recopilar toda la información necesaria, necesita un preprocesamiento correcto. Para hacer esto, necesita saber dónde y qué archivos de encabezado se encuentran, qué macros se configuran durante el proceso de compilación. También necesita saber cómo se compila un archivo cpp en particular.

Ese es el problema Uno no solo compila el archivo (o, más bien, especifica la clave del compilador para que genere un archivo de preproceso). Necesitamos descubrir cómo se compila este archivo. Esta información está en los scripts de compilación, pero la pregunta es cómo obtenerla desde allí. En general, la tarea es complicada.



Además, muchos proyectos en GitHub son un desastre. Si tomas un proyecto abstracto desde allí, a menudo tienes que jugar para compilarlo. Un día te falta una biblioteca y necesitas encontrarla y descargarla manualmente. Otro día, se utiliza algún tipo de sistema de construcción autoescrito, que debe tratarse. Podría ser cualquier cosa. A veces, el proyecto descargado simplemente se niega a compilar y debe modificarse de alguna manera. No puede simplemente tomar y obtener automáticamente la representación preprocesada (.i) para los archivos .cpp. Puede ser complicado incluso cuando lo haces manualmente.

Podemos decir, bueno, el problema con los proyectos que no son de construcción es comprensible, pero no crucial. Solo trabajemos con proyectos que se puedan construir. Todavía existe la tarea de preprocesar un archivo en particular. Sin mencionar los casos en que tratamos con algunos compiladores especializados, por ejemplo, para sistemas embebidos.

Después de todo, el problema descrito no es insuperable. Sin embargo, todo esto es muy difícil y requiere mucha mano de obra. En el caso de C y C ++, el código fuente ubicado en GitHub no hace nada. Hay mucho trabajo por hacer para aprender a ejecutar compiladores automáticamente.

Nota Si el lector aún no entiende la profundidad del problema, lo invitamos a participar en el siguiente experimento. Tome diez proyectos aleatorios de tamaño medio de GitHub e intente compilarlos y luego obtenga su versión preprocesada para archivos .cpp. Después de eso, la pregunta sobre la laboriosidad de esta tarea desaparecerá :).

Puede haber problemas similares con otros lenguajes, pero son particularmente obvios en C y C ++.

Sexto matiz. El precio de eliminar los falsos positivos.

Los analizadores estáticos son propensos a generar falsos positivos y tenemos que refinar constantemente los diagnósticos para reducir el número de falsas advertencias.

Ahora volveremos al diagnóstico V789 previamente considerado, detectando cambios de contenedor dentro del bucle for basado en rango. Digamos que no fuimos lo suficientemente cuidadosos al escribirlo, y el cliente informa un falso positivo. Él escribe que el analizador no tiene en cuenta el escenario cuando el ciclo termina después de que se cambia el contenedor, y por lo tanto no hay problema. Luego da el siguiente ejemplo de código donde el analizador da un falso positivo:

 std::vector<int> numbers; .... for (int num : numbers) { if (num < 5) { numbers.push_back(0); break; // or, for example, return } } 

Sí, es un defecto. En un analizador clásico, su eliminación es extremadamente rápida y económica. En PVS-Studio, la implementación de esta excepción consta de 26 líneas de código.

Esta falla también se puede corregir cuando el analizador se basa en algoritmos de aprendizaje. Por supuesto, se puede enseñar recolectando docenas o cientos de ejemplos de código que se deben considerar correctos.

Nuevamente, la pregunta no está en la viabilidad, sino en el enfoque práctico. Sospechamos que luchar contra falsos positivos específicos, que molestan a los clientes, es mucho más costoso en caso de LD. Es decir, la atención al cliente en términos de eliminación de falsos positivos costará más dinero.

Séptimo matiz. Características raramente utilizadas y cola larga.

Anteriormente, hemos lidiado con el problema de los lenguajes altamente especializados, para los cuales puede no haber suficiente código fuente para el aprendizaje. Un problema similar tiene lugar con funciones raramente utilizadas (sistema, WinAPI, de bibliotecas populares, etc.).

Si estamos hablando de tales funciones del lenguaje C, como strcmp , entonces en realidad hay una base para aprender. GitHub, resultados de código disponibles:

  • strcmp - 40,462,158
  • stricmp - 1,256,053

Sí, hay muchos ejemplos de uso. Quizás el analizador aprenda a notar, por ejemplo, los siguientes patrones:

  • Es extraño si la cadena se compara con sí misma. Se arregla.
  • Es extraño si uno de los punteros es NULL. Se arregla.
  • Es extraño que el resultado de esta función no se use. Se arregla.
  • Y así sucesivamente.

¿No es genial? No Aquí nos enfrentamos al problema de la "cola larga". Muy brevemente el punto de la "cola larga" en el siguiente. No es práctico vender solo el Top50 de los libros más populares y leídos en una librería. Sí, cada libro se comprará, digamos, 100 veces más a menudo que los libros que no están en esta lista. Sin embargo, la mayoría de las ganancias se compondrán de otros libros que, como dicen, encuentran a su lector. Por ejemplo, una tienda en línea Amazon.com recibe más de la mitad de las ganancias de lo que está fuera de 130,000 "artículos más populares".

Hay funciones populares y hay pocas de ellas. Hay impopulares, pero hay muchos de ellos. Por ejemplo, existen las siguientes variaciones de la función de comparación de cadenas:

  • g_ascii_strncasecmp - 35,695
  • lstrcmpiA - 27,512
  • _wcsicmp_l - 5,737
  • _strnicmp_l - 5,848
  • _mbscmp_l - 2,458
  • y otros

Como puede ver, se usan con mucha menos frecuencia, pero cuando los usa, puede cometer los mismos errores. Hay muy pocos ejemplos para identificar patrones. Sin embargo, estas funciones no pueden ser ignoradas. Individualmente, rara vez se usan, pero se escribe mucho código con su uso, lo que es mejor verificar. Ahí es donde se muestra la "cola larga".

En PVS-Studio, anotamos manualmente las características. Por ejemplo, por el momento se habían anotado alrededor de 7.200 funciones para C y C ++. Esto es lo que marcamos:

  • Winapi
  • Biblioteca estándar de C,
  • Biblioteca de plantillas estándar (STL),
  • glibc (biblioteca GNU C)
  • Qt
  • MFC
  • zlib
  • libpng
  • Openssl
  • y otros

Por un lado, parece un camino sin salida. No puedes anotar todo. Por otro lado, funciona.

Ahora aquí está la pregunta. ¿Qué beneficios puede tener ML? Las ventajas significativas no son tan obvias, pero puedes ver la complejidad.

Se podría argumentar que los algoritmos creados sobre ML encontrarán patrones con funciones de uso frecuente y no tienen que ser anotados. Si es verdad. Sin embargo, no hay problema para anotar independientemente funciones populares como strcmp o malloc .

Sin embargo, la larga cola causa problemas. Puedes enseñar haciendo ejemplos sintéticos. Sin embargo, aquí volvemos a la parte del artículo, donde decíamos que era más fácil y rápido escribir diagnósticos clásicos, en lugar de generar muchos ejemplos.

Tomemos, por ejemplo, una función, como _fread_nolock . Por supuesto, se usa con menos frecuencia que fread . Pero cuando lo usa, puede cometer los mismos errores. Por ejemplo, el búfer debe ser lo suficientemente grande. Este tamaño no debe ser menor que el resultado de multiplicar el segundo y el tercer argumento. Es decir, desea encontrar un código incorrecto:

 int buffer[10]; size_t n = _fread_nolock(buffer, size_of(int), 100, stream); 

Así es como se ve la anotación de esta función en PVS-Studio:

 C_"size_t _fread_nolock" "(void * _DstBuf, size_t _ElementSize, size_t _Count, FILE * _File);" ADD(HAVE_STATE | RET_SKIP | F_MODIFY_PTR_1, nullptr, nullptr, "_fread_nolock", POINTER_1, BYTE_COUNT, COUNT, POINTER_2). Add_Read(from_2_3, to_return, buf_1). Add_DataSafetyStatusRelations(0, 3); 

A primera vista, tal anotación puede parecer difícil, pero de hecho, cuando comienza a escribirlas, se vuelve simple. Además, es código de solo escritura. Escribió y olvidó. Las anotaciones cambian raramente.

Ahora hablemos de esta función desde el punto de vista de ML. GitHub no nos ayudará. Hay alrededor de 15,000 menciones de esta función. Incluso hay menos código bueno. Una parte importante de los resultados de búsqueda abarca lo siguiente:

 #define fread_unlocked _fread_nolock 

Cuales son las opciones?
  1. No hagas nada Es un camino a ninguna parte.
  2. Imagínese, enseñe al analizador escribiendo cientos de ejemplos solo para una función para que el analizador comprenda la interconexión entre el búfer y otros argumentos. Sí, puedes hacer eso, pero es económicamente irracional. Es una calle sin salida.
  3. Puede llegar a una forma similar a la nuestra cuando las anotaciones a las funciones se configurarán manualmente. Es una forma buena y sensata. Eso es solo ML, que no tiene nada que ver con eso :). Esto es un retroceso a la forma clásica de escribir analizadores estáticos.

Como puede ver, ML y la larga cola de las funciones raramente utilizadas no van juntas.

En este punto, había personas relacionadas con ML que se opusieron y dijeron que no habíamos tenido en cuenta la opción cuando el analizador aprendería todas las funciones y sacaría conclusiones de lo que estaban haciendo. Aquí, aparentemente, no entendemos a los expertos o no entienden nuestro punto.

Los cuerpos de funciones pueden ser desconocidos. Por ejemplo, podría ser una función relacionada con WinAPI. Si esta es una función raramente utilizada, ¿cómo comprenderá el analizador lo que está haciendo? Podemos fantasear con que el analizador usará Google mismo, encontrará una descripción de la función, la leerá y la comprenderá . Además, tendría que sacar conclusiones de alto nivel de la documentación. La descripción _fread_nolock no dice nada sobre la interconexión entre el búfer, el segundo y el tercer argumento. Esta comparación debe deducirse por inteligencia artificial por sí sola, basada en una comprensión de los principios generales de programación y cómo funciona el lenguaje C ++. Creo que deberíamos pensar en todo esto seriamente en 20 años.

Los cuerpos de funciones pueden estar disponibles, pero puede que esto no sirva de nada. Veamos una función, como memmove . A menudo se implementa en algo como esto:

 void *memmove (void *dest, const void *src, size_t len) { return __builtin___memmove_chk(dest, src, len, __builtin_object_size(dest, 0)); } 

¿Qué es __builtin___memmove_chk ? Esta es una función intrínseca que el compilador ya está implementando. Esta función no tiene el código fuente.

O memmove podría verse así: la primera versión de ensamblaje . Puede enseñarle al analizador a comprender las diferentes opciones de ensamblaje, pero este enfoque parece incorrecto.

Ok, a veces los cuerpos de funciones son realmente conocidos. Además, también conocemos cuerpos de funciones en el código del usuario. Parecería que en este caso ML obtiene enormes ventajas al leer y comprender lo que hacen todas estas funciones.

Sin embargo, incluso en este caso estamos llenos de pesimismo. Esta tarea es demasiado compleja. Es complicado incluso para un humano. Piensa en lo difícil que es para ti entender el código que no escribiste. Si es difícil para una persona, ¿por qué esta tarea debería ser fácil para una IA? En realidad, la IA tiene un gran problema para comprender conceptos de alto nivel.Si estamos hablando de comprender el código, no podemos prescindir de la capacidad de abstraernos de los detalles de implementación y considerar el algoritmo a un alto nivel. Parece que esta discusión también puede posponerse durante 20 años.

Otros matices

Hay otros puntos que también deben tenerse en cuenta, pero no hemos profundizado en ellos. Por cierto, el artículo resulta ser bastante largo. Por lo tanto, enumeraremos brevemente algunos otros matices, dejándolos para la reflexión del lector.

  • Outdated recommendations. As mentioned, languages change, and recommendations for their use change, respectively. If the analyzer learns on old source code, it might start issuing outdated recommendations at some point. Example. Formerly, C++ programmers have been recommended using auto_ptr instead of half-done pointers. This smart pointer is now considered obsolete and it is recommended that you use unique_ptr .
  • Data models. At the very least, C and C++ languages have such a thing as a data model . This means that data types have different number of bits across platforms. If you don't take this into account, you can incorrectly teach the analyzer. For example, in Windows 32/64 the long type always has 32 bits. But in Linux, its size will vary and take 32/64 bits depending on the platform's number of bits. Without taking all this into account, the analyzer can learn to miscalculate the size of the types and structures it forms. But the types also align in different ways. All this, of course, can be taken into account. You can teach the analyzer to know about the size of the types, their alignment and mark the projects (indicate how they are building). However, all this is an additional complexity, which is not mentioned in the research articles.
  • Behavioral unambiguousness. Since we're talking about ML, the analysis result is more likely to have probabilistic nature. That is, sometimes the erroneous pattern will be recognized, and sometimes not, depending on how the code is written. From our experience, we know that the user is extremely irritated by the ambiguity of the analyzer's behavior. He wants to know exactly which pattern will be considered erroneous and which will not, and why. In the case of the classical analyzer developing approach, this problem is poorly expressed. Only sometimes we need to explain our clients why there is a/there is no analyzer warning and how the algorithm works, what exceptions are handled in it. Algorithms are clear and everything can always be easily explained. An example of this kind of communication: " False Positives in PVS-Studio: How Deep the Rabbit Hole Goes ". It's not clear how the described problem will be solved in the analyzers built on ML.

Conclusions


No negamos las perspectivas de la dirección ML, incluida su aplicación en términos de análisis de código estático. ML puede utilizarse potencialmente en tareas de búsqueda de errores tipográficos, al filtrar falsos positivos, al buscar nuevos patrones de error (aún no descritos), etc. Sin embargo, no compartimos el optimismo que impregna los artículos dedicados a ML en términos de análisis de código.

En este artículo, hemos esbozado algunos problemas en los que uno tendrá que trabajar si va a usar ML. Los matices descritos niegan en gran medida los beneficios del nuevo enfoque. Además, los viejos enfoques clásicos de implementación de analizadores son más rentables y económicamente más factibles.

Curiosamente, los artículos de los adherentes de la metodología ML no mencionan estas trampas. Pues nada nuevo. ML provoca ciertas exageraciones y probablemente no deberíamos esperar una evaluación equilibrada de sus apologistas con respecto a la aplicabilidad de ML en las tareas de análisis de código estático.

Desde nuestro punto de vista, el aprendizaje automático llenará un nicho en tecnologías, utilizadas en analizadores estáticos junto con análisis de flujo de control, ejecuciones simbólicas y otros.

La metodología del análisis estático puede beneficiarse de la introducción de ML, pero no exagere las posibilidades de esta tecnología.

PS


Como el artículo es generalmente crítico, algunos podrían pensar que tememos lo nuevo y que Luddites se volvió contra ML por temor a perder el mercado de herramientas de análisis estático.

Luditas


No, no tenemos miedo. Simplemente no vemos el punto de gastar dinero en enfoques ineficientes en el desarrollo del analizador de código PVS-Studio. De una forma u otra, adoptaremos ML. Además, algunos diagnósticos ya contienen elementos de algoritmos de autoaprendizaje. Sin embargo, definitivamente seremos muy conservadores y tomaremos solo lo que claramente tendrá un mayor efecto que los enfoques clásicos, construidos en bucles e ifs :). Después de todo, necesitamos crear una herramienta efectiva, no trabajar con una subvención :).

El artículo está escrito porque cada vez se hacen más preguntas sobre el tema y queríamos tener un artículo expositivo que pusiera todo en su lugar.

Gracias por su atencion Te invitamos a leer el artículo "Por qué debería elegir el analizador estático PVS-Studio para integrarlo en su proceso de desarrollo ".

Source: https://habr.com/ru/post/484202/


All Articles