
Falsa Competencia: Grafo de (posible) fraude en licitaciones públicas
Hola! Soy el pibe de Reporté errores y me llamaron del Senado : r/devsarg y ¿Qué dijo? - Senadores. Hoy hice Falsa Competencia
En las últimas tres semanas estuve scrappeando la página de COMPR.AR que contiene las compras de la Administración Pública Nacional (esto es el Poder Ejecutivo, sin contar obras públicas) y el BOLETIN OFICIAL REPUBLICA ARGENTINA, particularmente su buscador de sociedades, que contiene los cambios en los directorios de las empresas argentinas. El objetivo era armar un grafo que relacione personas->empresas->licitaciones públicas de forma que se pueda detectar cuándo una misma persona hace ofertas desde empresas distintas, por ejemplo
Este esquema de falsa competencia es el cuadro delictivo en causas como Vialidad u Odebrecht. Como emerge del gráfico, el patrón que estamos buscando son ciclos Persona-Empresa-Compra en un grafo direccionado, por lo que la idea inicial fue scrappear distintas fuentes, recolectar estos datos, armar el grafo y proyectar los datos en Neo4j con una query cypher para armar el grafo.
El grafo final que contiene todas las relaciones (sospechosas y no sospechosas) es de unos 180k nodos con 550k relaciones, por lo que no es posible visualizarlo en tiempo real.
También agregué algunas estadísticas que me parecieron interesantes, como proporción de Licitaciones Públicas vs Privadas vs Contrataciones Directas, o el gasto acumulado por Ministerio
Algo que no me esperaba de estas estadísticas fue que el Correo Argentino sea el proveedor que más dinero recibe, y que la mayoría de procesos de compra son por Contratación Directa; es decir, a dedo.
Finalmente hay un "ágora" donde se muestra evidencia de posible falsa competencia con links a las fuentes oficiales (la licitación, el cuadro de oferta con los pliegos, las actas en el bora) y el usuario puede votar si considera que podría haber existido falsa competencia.
Algunos de los problemas que me encontré en el camino
- La página de COMPR.AR está hecha de forma adrede para que sea difícil de scrappear. Todas las requests usan un query string cifrado cuya clave de cifrado se arma según las páginas anteriormente visitadas. Por ejemplo VistaPreviaPliegoCiudadano.aspx?qs=BQoBko.... (string cifrado de cientos de caracteres)|BoX(...otro string...). Esto previene enumerar la base de datos y obliga a hacer un headless browser que manualmente clickee los links, dado que estos se calculan en tiempo de ejecución. Si bien esto hace un poco más difícil el scrapping, no es imposible porque el cifrado es estable, por lo que la misma sucesión de links generan siempre el mismo query string para todos los usuarios. A efectos prácticos se comporta igual que cualquier otro path traversal con el efecto de que nunca sabes que parte del path representa qué cosa, pero sí obliga a hacer N requests por proceso donde N es el link depth hasta ese recurso.
- La página del BORA contiene los extractos de las actas, pero para actas anteriores a 2011 se suben PDFs sobre los cuales habría que hacer OCR; sin embargo esos pdfs son de cientos de páginas. Por esta razón excluí el período anterior a 2011
- La fecha de las Actas no es representativa del cambio de directorio, las empresas suelen anunciar sus cambios de directorios un año después de que realmente sucedieron. Si bien esto es ilegal, es la normal porque la multa es muy baja. Esto implica que es necesario procesar el texto del acta que en algún lugar indica la fecha en la cual se designó una autoridad. Para esto usé GPT-5mini de forma que extrajera el movimiento (designación/cese/renuncia), cargo (gerente, director, suplente; etc), persona involucrada por cuit/dni/nombre y fecha del movimiento.
- El grafo final es muy denso, puesto que considera todos los movimientos de todas las empresas que en algún momento hubieran hecho alguna oferta al Estado. Hay empresas como Telecom o Telmex que tienen más de 600 personas nombradas en el período 2011-2026. Esto hizo que Neo4J no responda a la velocidad que me gustaría, por lo que precalculé la el grafo de "casos sospechosos". Para esto implementé un multi-point BFS que terminó en un tiempo razonable. Este grafo se recalcula en cada build. Para filtros específicos (por ejemplo, los nodos de Movistar) se sigue usando Neo4J.
- La página COMPR.AR parece tener un límite duro/hardcodeado de responder como mínimo en 0.1 segundos al mismo cliente, lo que implicó hacer el scrapping a lo largo de 1 semana con aproximadamente 100 hilos, aunque este tiempo seguro se puede reducir si se tienen varias IPs distintas
El stack es
- Neo4j para los grafos
- selectolax para parsear el html y headless browser
- PostgreSQL para la bdd de proveedores, procesos de compra, personas y votaciones
- RabbitMQ para encolar votaciones en el ágora
- Vite para el front, altamente vibecodeado porque no soy UI/UX
Para casos "sospechosos" de falsa competencia se considera
- Dos o más empresas participan en un proceso de compra
- Hay una o más personas en común entre estas empresas
- Dicha persona NO es el escribano del acta del BORA (es común que se compartan, hay pocos y no necesariamente forman parte de la empresa)
- Se excluyen Actas históricas por estar en PDF y requerir OCR de baja fiabilidad
- Se excluyen relaciones de hace más de 4 años de antigüedad. Las empresas suelen tener cargos temporales que se renuevan y generan un acta nueva
- Se excluyen movimientos de personas posteriores a la oferta/compra
- Sólo "ofertas confirmadas" por el gobierno, no necesariamente compradas/aceptadas aunque hay un filtro opcional para ello
Con esos filtros se encontraron 241 casos sospechados de los cuales hay al menos 26 que tienen Órdenes de Compra asociados (es decir, el Estado compró a una de las empresas sospechadas), esto es una vara baja porque hay órdenes de compra que no se suben sino que se adjuntan en pdfs anexados que por su complejidad quedó fuera de análisis.
Si se incluyen movimientos de personas posteriores a la oferta/compra el grafo aumenta a 2753 casos, lo que muestra un flujo alto de mismas personas entre empresas que trabajan con el Estado a lo largo del tiempo.