<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kiara Holly Zapana Murillo</title>
    <description>The latest articles on DEV Community by Kiara Holly Zapana Murillo (@kiarazmurillo).</description>
    <link>https://dev.to/kiarazmurillo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4005377%2Fbdcb4cb2-178a-4276-91a5-ee12bb4287ae.png</url>
      <title>DEV Community: Kiara Holly Zapana Murillo</title>
      <link>https://dev.to/kiarazmurillo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kiarazmurillo"/>
    <language>en</language>
    <item>
      <title>SQL seguro con IA: cómo construir un asistente que valida las consultas antes de ejecutarlas</title>
      <dc:creator>Kiara Holly Zapana Murillo</dc:creator>
      <pubDate>Sun, 05 Jul 2026 06:33:44 +0000</pubDate>
      <link>https://dev.to/kiarazmurillo/sql-seguro-con-ia-como-construir-un-asistente-que-valida-las-consultas-antes-de-ejecutarlas-54g7</link>
      <guid>https://dev.to/kiarazmurillo/sql-seguro-con-ia-como-construir-un-asistente-que-valida-las-consultas-antes-de-ejecutarlas-54g7</guid>
      <description>&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;Escribir SQL a mano es una de esas habilidades que, sin querer, deja fuera a mucha gente del mundo de los datos. Product managers, agentes de soporte e incluso desarrolladores junior muchas veces saben exactamente qué quieren preguntarle a una base de datos, pero no siempre saben cómo expresarlo sintácticamente en SQL.&lt;/p&gt;

&lt;p&gt;Los sistemas &lt;strong&gt;Text-to-SQL&lt;/strong&gt; intentan resolver justamente ese problema: convertir una pregunta en lenguaje natural en una consulta real, devolviendo los datos sin fricción.&lt;/p&gt;

&lt;p&gt;Sin embargo, generar SQL automáticamente no es el final del problema, es apenas el principio. Un modelo de lenguaje (LLM) puede producir una consulta sintácticamente correcta y aun así &lt;strong&gt;completamente equivocada&lt;/strong&gt; a nivel semántico: un &lt;em&gt;join&lt;/em&gt; mal hecho, un filtro incorrecto, o —en el peor de los casos— un intento de modificar datos que solo debían leerse.&lt;/p&gt;

&lt;p&gt;En este artículo construyo un &lt;strong&gt;asistente de IA para SQL&lt;/strong&gt; que no solo genera consultas, sino que las &lt;strong&gt;valida de forma determinista antes de ejecutarlas&lt;/strong&gt;. Para ello utilizo &lt;a href="https://huggingface.co/docs/smolagents/examples/text_to_sql" rel="noopener noreferrer"&gt;smolagents de Hugging Face&lt;/a&gt; como capa de razonamiento, y una capa de seguridad propia (un &lt;em&gt;SQL Guard&lt;/em&gt;) como filtro final antes de que cualquier consulta toque la base de datos.&lt;/p&gt;

&lt;p&gt;El código fuente completo de este proyecto está disponible en el repositorio público enlazado al final del artículo.&lt;/p&gt;




&lt;h2&gt;
  
  
  ¿Por qué no basta con pedirle SQL a un LLM?
&lt;/h2&gt;

&lt;p&gt;El &lt;em&gt;pipeline&lt;/em&gt; más simple de Text-to-SQL se ve así:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enviar el esquema de la base de datos y la pregunta del usuario a un LLM.&lt;/li&gt;
&lt;li&gt;Pedirle al LLM que devuelva una consulta SQL.&lt;/li&gt;
&lt;li&gt;Ejecutar esa consulta a ciegas y mostrar el resultado.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Esto funciona… hasta que deja de funcionar. El problema radica en que una sola llamada al LLM es &lt;strong&gt;frágil&lt;/strong&gt;: el modelo puede generar una consulta válida en sintaxis pero incorrecta en significado, y el sistema la ejecutará igual, sin ningún mecanismo para detectar el error.&lt;/p&gt;

&lt;p&gt;Peor aún, en un entorno real nada impide que el modelo intente inyectar un &lt;code&gt;DELETE&lt;/code&gt; o un &lt;code&gt;UPDATE&lt;/code&gt; si no se le prohíbe explícitamente a nivel de sistema. Decírselo únicamente en el &lt;em&gt;prompt&lt;/em&gt; no es una garantía de seguridad, es una sugerencia que el modelo puede ignorar. Ese es exactamente el vacío arquitectónico que este proyecto busca cerrar.&lt;/p&gt;




&lt;h2&gt;
  
  
  ¿Qué es smolagents y por qué es relevante aquí?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;smolagents&lt;/code&gt; es un &lt;em&gt;framework&lt;/em&gt; de agentes ligero de Hugging Face, descrito por sus propios creadores como "una librería minimalista para agentes que piensan en código". En lugar de forzar al modelo a producir llamadas a herramientas en un JSON rígido, un &lt;code&gt;CodeAgent&lt;/code&gt; escribe y ejecuta código Python real como parte de su ciclo iterativo de razonamiento:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pensar&lt;/strong&gt;: el modelo razona sobre qué hacer a continuación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actuar&lt;/strong&gt;: escribe código Python que invoca una herramienta (en nuestro caso, un motor SQL).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observar&lt;/strong&gt;: el resultado de esa herramienta vuelve al contexto del modelo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repetir o responder&lt;/strong&gt;: el agente decide si necesita ajustar la consulta (por ejemplo, si hubo un error de sintaxis) o si ya puede entregar la respuesta final.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese ciclo es extremadamente útil, pero &lt;strong&gt;no es suficiente por sí solo&lt;/strong&gt;. Un agente puede autocorregirse ante resultados sospechosos, pero no sabe, por diseño, qué acciones son destructivas para tu lógica de negocio. Esa responsabilidad la debe asumir una capa adicional y determinista: &lt;strong&gt;el validador&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Arquitectura de la solución
&lt;/h2&gt;

&lt;p&gt;En lugar de ejecutar directamente cualquier SQL que el modelo proponga, diseñamos un sistema con el siguiente flujo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El usuario hace una pregunta en lenguaje natural.&lt;/li&gt;
&lt;li&gt;El agente de IA propone una consulta SQL tras razonar sobre el esquema.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Un validador estricto intercepta y revisa esa consulta.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Solo se ejecutan consultas &lt;code&gt;SELECT&lt;/code&gt; catalogadas como seguras.&lt;/li&gt;
&lt;li&gt;El resultado se procesa y se devuelve al usuario.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ese paso adicional —la validación— es lo que eleva una simple demostración técnica a un asistente que realmente podrías conectar a datos reales en producción.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usuario ──▶ Agente (smolagents) ──▶ SQL Guard ──▶ Base de datos
                 │                       │
             razona y                rechaza si
             propone SQL             no es seguro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Construyendo el proyecto: base de datos de ventas
&lt;/h2&gt;

&lt;p&gt;Para mantener el ejemplo reproducible, utilizo una base de datos SQLite con dos tablas sencillas: &lt;code&gt;customers&lt;/code&gt; y &lt;code&gt;sales&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Definiendo el esquema
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="nb"&gt;REAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Cargando datos de ejemplo (semilla)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Alan Payne'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Lima'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Marta Lopez'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Arequipa'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'2026-07-02'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;89&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Conexión en Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_connection&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safe_sql_demo.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  La pieza clave: el validador de SQL (SQL Guard)
&lt;/h2&gt;

&lt;p&gt;Esta es la parte fundamental del proyecto y lo que marca la diferencia frente a un Text-to-SQL básico. Antes de ejecutar cualquier consulta generada por el agente, la instrucción debe sortear este filtro en Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BLOCKED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALTER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_safe_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Prevenir ataques de inyección encadenada
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="c1"&gt;# Exigir explícitamente operaciones de solo lectura
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="c1"&gt;# Bloquear palabras clave destructivas, incluso en subconsultas
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;BLOCKED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este validador aplica tres reglas deterministas innegociables:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Solo se permite &lt;code&gt;SELECT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No se permiten múltiples sentencias encadenadas.&lt;/li&gt;
&lt;li&gt;Se bloquean palabras destructivas en cualquier parte de la cadena, incluso dentro de subconsultas.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Integrando el agente con el validador
&lt;/h2&gt;

&lt;p&gt;El flujo de ejecución intercepta la petición del agente de esta forma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_connection&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;is_safe_sql&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generated_sql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Capa de seguridad
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_safe_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_sql&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Consulta bloqueada por el validador: operación no permitida.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Ejecución aprobada
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;get_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_sql&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error SQL: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí radica la diferencia arquitectónica: el modelo LLM puede alucinar, o el usuario puede intentar inyectar código malicioso a través del &lt;em&gt;prompt&lt;/em&gt;, pero &lt;strong&gt;la aplicación retiene la autoridad final&lt;/strong&gt; de decidir si esa sugerencia llega a ejecutarse. El agente asiste; el sistema controla.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ejemplo de uso
&lt;/h2&gt;

&lt;p&gt;Si un usuario pregunta de buena fe:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;¿Cuál es el total de ventas por mes?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;El agente generará algo como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%Y-%m'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El validador revisa que sea un &lt;code&gt;SELECT&lt;/code&gt; seguro, la aprueba y el sistema devuelve el resultado real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué pasa ante un ataque o una alucinación del modelo?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si el agente, o el usuario a través del &lt;em&gt;prompt&lt;/em&gt;, intentara ejecutar &lt;code&gt;DELETE FROM sales WHERE total_amount &amp;gt; 100;&lt;/code&gt;, el validador lo rechazaría en milisegundos, devolviendo la alerta de seguridad sin llegar siquiera a tocar el motor de base de datos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prompt directo al LLM vs. agente con validación
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterio&lt;/th&gt;
&lt;th&gt;LLM directo (&lt;em&gt;zero-shot&lt;/em&gt;)&lt;/th&gt;
&lt;th&gt;Agente + validador&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generación&lt;/td&gt;
&lt;td&gt;Un solo intento, sin retroalimentación&lt;/td&gt;
&lt;td&gt;Iterativa, con capacidad de corrección&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manejo de errores&lt;/td&gt;
&lt;td&gt;Devuelve respuestas erróneas en silencio&lt;/td&gt;
&lt;td&gt;Se detiene y bloquea ante operaciones dudosas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seguridad&lt;/td&gt;
&lt;td&gt;Depende de la "buena voluntad" del &lt;em&gt;prompt&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Aplicada estrictamente a nivel de código&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auditabilidad&lt;/td&gt;
&lt;td&gt;Consulta oculta en la respuesta&lt;/td&gt;
&lt;td&gt;Consulta explícita y lista para hacer &lt;em&gt;log&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confiabilidad en producción&lt;/td&gt;
&lt;td&gt;Baja&lt;/td&gt;
&lt;td&gt;Considerablemente más alta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La conclusión es clara: un validador no elimina el riesgo inherente de la IA, pero lo mitiga, transformando una decisión implícita del modelo en una &lt;strong&gt;regla explícita del sistema&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seguridad: consideraciones adicionales para producción
&lt;/h2&gt;

&lt;p&gt;Además del &lt;code&gt;SQL Guard&lt;/code&gt; propuesto, cualquier solución real a nivel corporativo debe contemplar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mínimo privilegio (DB roles):&lt;/strong&gt; conectar a la base de datos usando un rol restringido que físicamente solo tenga permisos de &lt;code&gt;SELECT&lt;/code&gt;, jamás como administrador.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vistas en lugar de tablas crudas:&lt;/strong&gt; exponer a la IA únicamente &lt;em&gt;views&lt;/em&gt; curadas, ocultando columnas con contraseñas o datos personales sensibles (PII).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Límites inyectados (&lt;code&gt;LIMIT&lt;/code&gt;):&lt;/strong&gt; forzar programáticamente un &lt;code&gt;LIMIT 50&lt;/code&gt; al final de cada consulta válida para evitar que un modelo escanee accidentalmente millones de filas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registro (logging):&lt;/strong&gt; guardar cada pregunta, la consulta generada y su latencia, para poder auditar el uso de la herramienta con el tiempo.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Casos de uso reales
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dashboards dinámicos&lt;/strong&gt; — dejar que los usuarios de negocio generen agregaciones seguras sobre la marcha, sin depender del equipo de ingeniería.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plataformas de soporte&lt;/strong&gt; — permitir a los agentes buscar datos operativos de clientes mediante lenguaje natural, bajo políticas de lectura estricta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proyectos académicos y de investigación&lt;/strong&gt; — fomentar el uso de bases de datos relacionales eliminando la fricción de la sintaxis SQL, sin comprometer la integridad de los datos.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;El uso de arquitecturas Text-to-SQL demuestra que delegar tareas a modelos fundacionales requiere de ingeniería de software defensiva. La diferencia entre un &lt;em&gt;prompt&lt;/em&gt; llamativo y un sistema validado es, en última instancia, la diferencia entre una demo y un sistema productivo.&lt;/p&gt;

&lt;p&gt;El enfoque basado en código de &lt;code&gt;smolagents&lt;/code&gt; vuelve el razonamiento del LLM transparente y auditable. Si le sumamos una capa de validación SQL, logramos trasladar la seguridad del &lt;em&gt;prompt&lt;/em&gt; volátil a una regla de infraestructura confiable, que no depende de que el modelo se comporte bien.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repositorio público:&lt;/strong&gt;&lt;br&gt;
El código funcional completo de este proyecto (base de datos, validador, orquestación y demostración) está disponible en mi cuenta de GitHub:&lt;br&gt;
🔗 &lt;a href="https://github.com/KiaaraZM/safe-sql-ai" rel="noopener noreferrer"&gt;https://github.com/KiaaraZM/safe-sql-ai&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/docs/smolagents/examples/text_to_sql" rel="noopener noreferrer"&gt;Hugging Face smolagents – Ejemplos Text-to-SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/huggingface/smolagents" rel="noopener noreferrer"&gt;Repositorio oficial de smolagents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>python</category>
      <category>ai</category>
      <category>database</category>
    </item>
    <item>
      <title>Postman + Newman: How to Build a Practical API Testing Workflow with Real Code</title>
      <dc:creator>Kiara Holly Zapana Murillo</dc:creator>
      <pubDate>Tue, 30 Jun 2026 03:57:58 +0000</pubDate>
      <link>https://dev.to/kiarazmurillo/postman-newman-how-to-build-a-practical-api-testing-workflow-with-real-code-10f8</link>
      <guid>https://dev.to/kiarazmurillo/postman-newman-how-to-build-a-practical-api-testing-workflow-with-real-code-10f8</guid>
      <description>&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;API testing is one of the fastest ways to verify that a system works the way it should. A well-designed API test does not only check whether a request returns a response; it also validates status codes, response bodies, headers, environment variables, and end-to-end behavior across several requests. In this article, I use &lt;strong&gt;Postman&lt;/strong&gt; to design and organize an API test collection and &lt;strong&gt;Newman&lt;/strong&gt; to run those tests from the command line as part of a repeatable workflow. The example focuses on a small real-world style API for users and tasks, with requests that create data, read it back, and validate the results. I also show how the same collection can be executed locally and inside a CI/CD pipeline. The purpose is to demonstrate that API testing can be practical, readable, and automation-friendly without becoming overly complex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Modern applications depend heavily on APIs. Frontend applications consume them, mobile apps rely on them, and microservices use them to exchange data. Because of that, API testing is no longer optional. If an endpoint returns the wrong status code, exposes an unexpected field, or fails under a basic workflow, the problem can reach users very quickly.&lt;/p&gt;

&lt;p&gt;A good API testing strategy should cover more than one request. It should verify that the API accepts the right inputs, returns the correct outputs, and behaves consistently across a sequence of actions. Postman is useful for this because it lets you create requests, add assertions, organize them into collections, and run them in different environments.&lt;/p&gt;

&lt;p&gt;Newman complements Postman by making those same collections runnable from the terminal. That is important because once the tests live in a command-line workflow, they can be placed inside CI/CD jobs, scheduled runs, or simple local scripts.&lt;/p&gt;

&lt;p&gt;In this article, I will show how to apply both tools to a small API workflow so the process feels realistic instead of theoretical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Postman and Newman
&lt;/h2&gt;

&lt;p&gt;Postman is a strong choice for API testing because it is easy to understand and it supports the full cycle of request creation, test scripting, environment handling, and collection organization. A collection can group related requests, which makes it ideal for scenarios like creating a user, reading the same user back, updating it, and checking the final state.&lt;/p&gt;

&lt;p&gt;Newman is the execution layer. It allows the same Postman collection to be run from the command line instead of only inside the app. That means the tests can be automated and repeated in a consistent way. In practice, this is what transforms a manual checklist into a reusable testing workflow.&lt;/p&gt;

&lt;p&gt;This combination is attractive because it separates concerns cleanly: Postman is where you design and validate the test logic, and Newman is where you automate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API scenario
&lt;/h2&gt;

&lt;p&gt;For the article, I use a simple fictional API that manages users and tasks. The API exposes a few endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /users&lt;/code&gt; to create a user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /users/{id}&lt;/code&gt; to retrieve that user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /tasks&lt;/code&gt; to create a task for the user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /tasks/{id}&lt;/code&gt; to verify task details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of flow works well because it lets us test both isolated requests and a full sequence. It also reflects what teams often do in real projects: create data, reuse returned values, and assert that the system behaves correctly from one step to the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the collection
&lt;/h2&gt;

&lt;p&gt;A Postman collection can hold all related requests in one place. In this example, the collection could be called &lt;strong&gt;User and Task API&lt;/strong&gt;. It would include an environment with variables such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;base_url = http://localhost:3000
user_id = 
user_name = Kiara
user_email = kiara@example.com
task_id = 
task_title = Review API tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using variables keeps the requests reusable. If the same collection needs to run in development, staging, or production, only the environment values change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request 1: Create a user
&lt;/h2&gt;

&lt;p&gt;The first request creates a user and stores the returned identifier in an environment variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST {{base_url}}/users
Content-Type: application/json

{
  "name": "{{user_name}}",
  "email": "{{user_email}}"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test script checks that the response is valid and extracts the ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User created successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;withBody&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jsonData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jsonData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a common Postman pattern: send a request, validate the response, and store data for the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request 2: Read the user
&lt;/h2&gt;

&lt;p&gt;Once the user exists, the second request verifies that the API can retrieve the same resource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET {{base_url}}/users/{{user_id}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple test script might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Get user returns 200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Response contains user email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step is important because it checks that the data created in one request can be read back correctly in another request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request 3: Create a task
&lt;/h2&gt;

&lt;p&gt;After confirming the user exists, the collection can create a task linked to that user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST {{base_url}}/tasks
Content-Type: application/json

{
  "title": "{{task_title}}",
  "userId": "{{user_id}}"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test script can verify both the response and the business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Task created successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;task_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;taskData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where Postman becomes more than a request sender. It becomes a workflow tester because it can connect one response to the next request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request 4: Read the task
&lt;/h2&gt;

&lt;p&gt;The final request confirms that the task was stored correctly and is associated with the right user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET {{base_url}}/tasks/{{task_id}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A validation script may include checks like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Task returns 200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Task belongs to the right user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;task_title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the collection has tested an end-to-end flow: create user, read user, create task, and verify task. That is a small but realistic example of how API testing can validate behavior across several endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the collection with Newman
&lt;/h2&gt;

&lt;p&gt;Once the collection works in Postman, the same tests can be executed with Newman from the command line. That is useful when you want to automate the run or include it in a build pipeline.&lt;/p&gt;

&lt;p&gt;A typical command looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;newman run user-task-api.postman_collection.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; local.postman_environment.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--reporters&lt;/span&gt; cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the collection and environment are stored in Postman cloud, Newman can also run them through the Postman API in a CI environment.&lt;/p&gt;

&lt;p&gt;That matters because it allows the same tests to run every time the code is pushed, instead of depending on someone to open Postman manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Newman in CI/CD
&lt;/h2&gt;

&lt;p&gt;Newman becomes especially useful when combined with automation. In a CI/CD pipeline, the test suite can run after code changes, before deployment, or on a schedule.&lt;/p&gt;

&lt;p&gt;A simple pipeline could follow this logic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Node.js and Newman.&lt;/li&gt;
&lt;li&gt;Download or load the Postman collection.&lt;/li&gt;
&lt;li&gt;Load the environment file.&lt;/li&gt;
&lt;li&gt;Run the API tests.&lt;/li&gt;
&lt;li&gt;Fail the pipeline if any assertion fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal GitHub Actions example could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Newman&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g newman&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run collection&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;newman run user-task-api.postman_collection.json -e local.postman_environment.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns API testing into a repeatable quality gate rather than a manual afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes this approach practical
&lt;/h2&gt;

&lt;p&gt;The combination of Postman and Newman is practical because it covers both design and execution. Postman makes it easy to model requests and assertions, while Newman makes the same checks runnable from the terminal.&lt;/p&gt;

&lt;p&gt;Another advantage is readability. A new team member can inspect the collection and understand the expected flow without needing to read a large testing framework immediately. That is especially helpful in small and medium-sized projects where clarity matters just as much as coverage.&lt;/p&gt;

&lt;p&gt;Finally, the approach scales. A collection can start with four requests and later grow into a larger suite with authentication, pagination, error handling, and role-based checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This approach is helpful, but it is not perfect. Postman and Newman are excellent for API-level validation, but they do not replace every kind of test.&lt;/p&gt;

&lt;p&gt;They do not fully cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performance behavior under heavy load.&lt;/li&gt;
&lt;li&gt;Deep security testing.&lt;/li&gt;
&lt;li&gt;Complex backend logic that requires unit testing.&lt;/li&gt;
&lt;li&gt;Database internals or service implementation details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For that reason, API tests should be part of a broader testing strategy that includes unit tests, integration tests, and sometimes performance or security checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;API testing becomes much more powerful when it moves from manual checking to a repeatable workflow. Postman gives you the structure to define requests, environments, and assertions. Newman gives you the ability to run that work from the command line and automate it in CI/CD.&lt;/p&gt;

&lt;p&gt;In practice, that means better consistency, faster feedback, and fewer surprises before deployment. For real-world projects, especially team-based ones, that combination is one of the most accessible ways to build a practical API testing framework.&lt;/p&gt;

</description>
      <category>postman</category>
      <category>newman</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>Bandit in a File Automation Script: Security Risks Hidden in Small Python Tools</title>
      <dc:creator>Kiara Holly Zapana Murillo</dc:creator>
      <pubDate>Mon, 29 Jun 2026 21:04:56 +0000</pubDate>
      <link>https://dev.to/kiarazmurillo/bandit-in-a-file-automation-script-security-risks-hidden-in-small-python-tools-2308</link>
      <guid>https://dev.to/kiarazmurillo/bandit-in-a-file-automation-script-security-risks-hidden-in-small-python-tools-2308</guid>
      <description>&lt;p&gt;Small Python scripts are often treated as low-risk tools because they are short, local, and built to solve a very specific task. However, a script that manipulates files, interprets input, or launches operating-system commands can still introduce serious security problems if it is written carelessly. Bandit is a static analysis tool for Python that helps detect insecure coding patterns before the script is executed. In this article, I use Bandit to review a simple file automation script that generates directory summaries. &lt;/p&gt;

&lt;p&gt;The script looks useful at first glance, but it includes unsafe command construction, &lt;code&gt;shell=True&lt;/code&gt;, and &lt;code&gt;eval()&lt;/code&gt;. After identifying those risks, I rewrite the script using safer Python patterns. This example shows that even small automation tools deserve a security review and that SAST can provide useful feedback early in development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Many Python scripts are written quickly to automate repetitive work: list files, rename documents, generate reports, clean directories, or move content from one place to another. Because these tools are usually short and practical, it is easy to assume they are harmless.&lt;/p&gt;

&lt;p&gt;That assumption can be misleading. A script does not need to be part of a large web platform to become risky. If it reads user input, builds shell commands, or writes files without enough care, it can expose the system to avoidable problems.&lt;/p&gt;

&lt;p&gt;This is why Static Application Security Testing, or SAST, is still relevant for small programs. OWASP includes Bandit among its source code analysis tools, and Bandit is specifically designed to detect common security issues in Python code without executing the program.&lt;/p&gt;

&lt;p&gt;In this article, I apply Bandit to a small file automation script. The goal is not to create a dramatic vulnerability demo, but to show how common coding habits in local utilities can still produce unsafe behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this topic matters
&lt;/h2&gt;

&lt;p&gt;When security is discussed in software projects, the attention usually goes to APIs, authentication, databases, or cloud infrastructure. Those areas are important, but smaller tools also deserve attention. In many teams, simple scripts become part of the daily workflow and are reused over time.&lt;/p&gt;

&lt;p&gt;For example, a script that summarizes a directory for reporting purposes may later be integrated into a scheduled task, shared with another teammate, or adapted into a larger internal tool. If the original script contains unsafe patterns, those patterns can spread just as easily as bugs in any other part of the system.&lt;/p&gt;

&lt;p&gt;That is one reason static analysis matters. It helps developers notice risky decisions before those decisions are normalized in future versions of the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bandit is a practical choice
&lt;/h2&gt;

&lt;p&gt;Bandit is a practical choice for Python because it focuses on source code scanning and looks for patterns that are often associated with vulnerabilities. According to the Bandit documentation, the tool parses Python files, builds an abstract syntax tree, applies security checks, and generates a report with the issues it detects.&lt;/p&gt;

&lt;p&gt;This makes it useful in two ways. First, it gives fast feedback during development. Second, it can be integrated into automated workflows such as GitHub-based scanning through the official Bandit Action, which helps security checks happen consistently instead of relying only on memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The example script
&lt;/h2&gt;

&lt;p&gt;For this experiment, I created a simple Python script that receives a folder path and generates a basic summary of its content. This is the type of utility many students or developers might actually write during a project.&lt;/p&gt;

&lt;p&gt;At first glance, the script seems functional. It lists files, saves a report, and includes a helper function to process a user-provided expression. But several implementation choices make it unsafe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ls &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;folder&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_expression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script contains at least three important concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It builds an operating-system command by concatenating user input.&lt;/li&gt;
&lt;li&gt;It executes that command with &lt;code&gt;shell=True&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It evaluates arbitrary input with &lt;code&gt;eval()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these choices prevent the script from working. That is exactly what makes them useful for a SAST exercise: the code appears valid, but the implementation includes risky behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the code is risky
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;summarize()&lt;/code&gt; function is the most obvious problem. It constructs a shell command by joining &lt;code&gt;ls&lt;/code&gt; with the value of &lt;code&gt;folder&lt;/code&gt;, then passes that string to &lt;code&gt;subprocess.check_output(..., shell=True)&lt;/code&gt;. Bandit documents this pattern as dangerous because shell-based subprocess execution can be vulnerable to injection attacks, especially when the command string is built dynamically.&lt;/p&gt;

&lt;p&gt;The use of &lt;code&gt;eval()&lt;/code&gt; is also problematic. Even though it may look convenient for converting or calculating values, it can execute arbitrary Python expressions if it receives untrusted input. In a small utility script, that kind of flexibility is usually unnecessary.&lt;/p&gt;

&lt;p&gt;The file-writing logic is less dramatic, but still worth improving. Using a context manager is safer and more maintainable because it ensures proper file handling and makes the code clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Bandit
&lt;/h2&gt;

&lt;p&gt;After saving the file as &lt;code&gt;automation_tool.py&lt;/code&gt;, the scan command is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; bandit automation_tool.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the code were part of a larger project, Bandit could also scan directories recursively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; bandit &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bandit typically reports subprocess-related issues when external commands are used in unsafe ways, and its documentation includes a specific rule for &lt;code&gt;shell=True&lt;/code&gt; in subprocess calls.&lt;/p&gt;

&lt;p&gt;A simplified example of the kind of result Bandit may produce is the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt; Issue: [B602] subprocess call with shell=True identified
   Severity: High   Confidence: High

&amp;gt;&amp;gt; Issue: [B404] Consider possible security implications associated with subprocess
   Severity: Low    Confidence: High
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact output may vary depending on the Bandit version, but the important point is that the report helps connect each warning to a concrete coding pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the findings mean
&lt;/h2&gt;

&lt;p&gt;One useful aspect of Bandit is that it does not just say that the script is “bad.” It points to specific practices that deserve review.&lt;/p&gt;

&lt;p&gt;In this example, the most important finding is the shell-based subprocess execution. Bandit explains that calls using &lt;code&gt;shell=True&lt;/code&gt; are risky because they may allow shell injection if input is not carefully controlled.&lt;/p&gt;

&lt;p&gt;A practical example makes this easier to understand. Imagine that a teammate runs the script with a folder name that contains shell metacharacters or unexpected concatenated content. Even if the input is not intentionally malicious, command construction through string concatenation creates unnecessary exposure.&lt;/p&gt;

&lt;p&gt;In other words, the script is not dangerous because it lists files; it becomes dangerous because of how it chooses to do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer rewrite
&lt;/h2&gt;

&lt;p&gt;The same goal can be achieved with safer Python features and without invoking the system shell.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_dir&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid folder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterdir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This new version changes the design in several important ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It uses &lt;code&gt;Path&lt;/code&gt; objects instead of building shell commands.&lt;/li&gt;
&lt;li&gt;It validates that the provided path is actually a directory.&lt;/li&gt;
&lt;li&gt;It writes files using a context manager.&lt;/li&gt;
&lt;li&gt;It replaces &lt;code&gt;eval()&lt;/code&gt; with a specific conversion function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script still solves the same task, but it does so with a more controlled and predictable approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical comparison
&lt;/h2&gt;

&lt;p&gt;The contrast between both versions is useful because it shows how insecure code is not always complex code. In the first version, the script depends on system command execution for something Python can do directly. In the second version, the logic stays inside the language and becomes easier to reason about.&lt;/p&gt;

&lt;p&gt;That difference matters in real development work. The more a script relies on raw command strings or flexible execution features, the harder it becomes to guarantee safe behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this applies in real projects
&lt;/h2&gt;

&lt;p&gt;This example may look small, but the pattern is common. A developer creates a quick utility, another person reuses it, and then the script becomes part of a shared workflow.&lt;/p&gt;

&lt;p&gt;For instance, imagine an internal tool that generates nightly summaries of uploaded files. If the original implementation trusts input too much or depends on shell execution, the risk can remain hidden for a long time because the script “works.” Bandit helps uncover those kinds of problems before the code becomes permanent.&lt;/p&gt;

&lt;p&gt;That is also why CI/CD integration can be valuable. The official PyCQA Bandit Action allows teams to run scans automatically in GitHub-based workflows, reducing the chance that insecure patterns are introduced silently during future changes.&lt;/p&gt;

&lt;p&gt;A minimal workflow example is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bandit Scan&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;analyze&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Perform Bandit Analysis&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PyCQA/bandit-action@v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Although Bandit is useful, it is not enough by itself. It focuses on Python source code and mainly detects patterns that are already known to be risky. That means it cannot fully understand business logic problems, misuse of permissions, or every contextual issue in a script.&lt;/p&gt;

&lt;p&gt;It may also produce warnings that require human judgment. For example, some subprocess usage may be intentional and controlled, but still deserves review because process execution is a sensitive action.&lt;/p&gt;

&lt;p&gt;For that reason, Bandit works best as one layer in a broader secure development process that also includes code review, input validation, safer defaults, and runtime testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This exercise reinforced an important idea for me: a script does not need to be large to deserve security analysis. In fact, small automation tools are often trusted too quickly because they look simple.&lt;/p&gt;

&lt;p&gt;Bandit is helpful precisely because it slows that assumption down. By scanning the source code before execution, it helps reveal insecure choices that are easy to overlook during normal development.&lt;/p&gt;

&lt;p&gt;The most useful lesson was not only that Bandit can flag a dangerous pattern like &lt;code&gt;shell=True&lt;/code&gt;, but that it encourages a better question while writing Python utilities: not just “does this work?”, but also “is this the safest way to solve the task?”&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>devsecops</category>
    </item>
    <item>
      <title>Your Terraform Can Be Insecure: A Practical Look at Checkov</title>
      <dc:creator>Kiara Holly Zapana Murillo</dc:creator>
      <pubDate>Sat, 27 Jun 2026 16:19:01 +0000</pubDate>
      <link>https://dev.to/kiarazmurillo/your-terraform-can-be-insecure-a-practical-look-at-checkov-4pel</link>
      <guid>https://dev.to/kiarazmurillo/your-terraform-can-be-insecure-a-practical-look-at-checkov-4pel</guid>
      <description>&lt;p&gt;Infrastructure as Code makes cloud deployment faster and more consistent, but it also makes mistakes repeatable. A single insecure setting can be deployed again and again if nobody catches it early.&lt;/p&gt;

&lt;p&gt;When developers think about bugs, they usually imagine broken features, failed requests, or unexpected behavior in application code. But infrastructure can also have bugs, especially when it is defined with tools like Terraform.&lt;/p&gt;

&lt;p&gt;That matters because a Terraform file can be valid and still be unsafe. A resource may deploy correctly, but if it exposes data, opens unnecessary access, or skips important security controls, the real problem is not syntax — it is configuration.&lt;/p&gt;

&lt;p&gt;This article focuses on a practical question: how can developers detect insecure infrastructure definitions before deployment? One useful answer is &lt;strong&gt;Checkov&lt;/strong&gt;, a static analysis tool that scans Infrastructure as Code and helps identify cloud misconfigurations early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Infrastructure Can Also Have Bugs
&lt;/h2&gt;

&lt;p&gt;When infrastructure is written as code, it becomes part of the software lifecycle. It is reviewed, versioned, reused, and deployed automatically, just like backend or frontend code.&lt;/p&gt;

&lt;p&gt;That also means it can include mistakes such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public storage buckets.&lt;/li&gt;
&lt;li&gt;Overly permissive security groups.&lt;/li&gt;
&lt;li&gt;Databases exposed to the internet.&lt;/li&gt;
&lt;li&gt;Missing encryption.&lt;/li&gt;
&lt;li&gt;Excessive IAM permissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These issues are dangerous because they often do not break deployment. Terraform may apply the configuration successfully, but the cloud environment can still be insecure.&lt;/p&gt;

&lt;p&gt;This is why infrastructure bugs are different from application bugs. They do not always crash the system — sometimes they quietly create risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Static Analysis Matters for Terraform
&lt;/h2&gt;

&lt;p&gt;Static Application Security Testing, or SAST, means analyzing code without executing it. OWASP explains that source code analysis tools help identify possible vulnerabilities before software is released.&lt;/p&gt;

&lt;p&gt;That idea also applies to Infrastructure as Code. Terraform files are still source files, and they describe how systems will be provisioned, connected, and exposed. If a bad practice is written into the code, the same bad practice can be reproduced every time the infrastructure is deployed.&lt;/p&gt;

&lt;p&gt;This is one of the reasons DevSecOps promotes shift-left security. Instead of waiting for an audit or a production incident, teams detect problems while building the infrastructure.&lt;/p&gt;

&lt;p&gt;In simple terms: if application code deserves early security checks, infrastructure code does too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Checkov Is a Practical Choice
&lt;/h2&gt;

&lt;p&gt;Checkov is a static analysis tool focused on Infrastructure as Code. Its goal is to detect misconfigurations and policy violations before cloud resources are created.&lt;/p&gt;

&lt;p&gt;One reason it is attractive for developers is that it is practical. It gives direct feedback about what is wrong, where it appears, and why it matters. That makes it useful not only for security specialists, but also for developers and students learning to build safer cloud environments.&lt;/p&gt;

&lt;p&gt;Another advantage is that it fits modern workflows. It can be executed locally while coding or integrated into CI/CD pipelines so every infrastructure change is scanned automatically. That turns security into part of the development process instead of leaving it for the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Example: Valid Terraform, Insecure Result
&lt;/h2&gt;

&lt;p&gt;Consider this Terraform snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"assets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"project-assets-demo"&lt;/span&gt;
  &lt;span class="nx"&gt;acl&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public-read"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From Terraform’s perspective, this is valid. The bucket can be created without syntax errors, and the deployment may succeed. But from a security perspective, &lt;code&gt;public-read&lt;/code&gt; may expose files that should not be accessible to everyone. Checkov includes Terraform policies for public S3 access and is designed to detect this kind of issue before infrastructure reaches production.&lt;/p&gt;

&lt;p&gt;This example shows an important lesson:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Valid code is not always secure code.&lt;/li&gt;
&lt;li&gt;Successful deployment is not always safe deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly why static analysis matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Checkov Helps You Discover
&lt;/h2&gt;

&lt;p&gt;When Checkov scans Terraform files, it typically reports the file where the issue appears, the rule that was triggered, and the reason the configuration may be risky.&lt;/p&gt;

&lt;p&gt;That kind of feedback is valuable because it is actionable. Instead of a vague warning, developers get concrete information they can use immediately to improve the infrastructure.&lt;/p&gt;

&lt;p&gt;This is especially useful in real projects where teams work fast and small mistakes can spread across multiple environments. For example, if the same Terraform module is reused in development, testing, and production, a single insecure pattern can be repeated several times. Static scanning helps catch that before it scales into a larger problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters in DevOps and CI/CD
&lt;/h2&gt;

&lt;p&gt;In many teams, infrastructure changes are deployed through automated pipelines. That means risky configurations can move from repository to cloud very quickly if there is no security validation in the workflow.&lt;/p&gt;

&lt;p&gt;Running Checkov in CI/CD helps teams review infrastructure before merge or deployment. This supports a shift-left model, where issues are fixed earlier, faster, and usually at lower cost.&lt;/p&gt;

&lt;p&gt;A practical workflow would look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A developer writes or updates Terraform code.&lt;/li&gt;
&lt;li&gt;Checkov scans the files locally or in a pull request pipeline.&lt;/li&gt;
&lt;li&gt;The tool reports risky configurations.&lt;/li&gt;
&lt;li&gt;The team fixes the findings before deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a simple but effective way to reduce avoidable cloud exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Is Helpful, but Not Magical
&lt;/h2&gt;

&lt;p&gt;Static analysis is powerful, but it is not perfect. It cannot fully understand runtime behavior, business logic, or every relationship between services.&lt;/p&gt;

&lt;p&gt;It can also produce false positives, especially in complex environments. Because of that, Checkov should be treated as one layer of security, not the only layer.&lt;/p&gt;

&lt;p&gt;A stronger approach combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static analysis.&lt;/li&gt;
&lt;li&gt;Code review.&lt;/li&gt;
&lt;li&gt;Least privilege.&lt;/li&gt;
&lt;li&gt;Secure defaults.&lt;/li&gt;
&lt;li&gt;Runtime monitoring.&lt;/li&gt;
&lt;li&gt;Broader security validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security works better when multiple layers support each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Should Care
&lt;/h2&gt;

&lt;p&gt;This is not only a topic for security teams. Anyone who writes Terraform is also making security decisions, even if that is not always obvious.&lt;/p&gt;

&lt;p&gt;Learning to scan IaC early helps developers build better habits. It creates awareness about permissions, exposure, encryption, and cloud design from the beginning of the project rather than at the end.&lt;/p&gt;

&lt;p&gt;That habit matters because secure infrastructure is not created by accident. It is created by reviewing code carefully and validating it before deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Terraform is powerful because it makes infrastructure repeatable. But that same strength can also repeat insecure configurations if they are not caught in time.&lt;/p&gt;

&lt;p&gt;Using Checkov helps detect those risks before deployment. It does not replace human review, but it does provide an early warning system that improves security where it matters most: in the code itself.&lt;/p&gt;

&lt;p&gt;If the cloud is built from code, then cloud security must also begin in code.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>security</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
