Giovanni Collazohttps://gcollazo.comRSS Feed for Giovanni CollazoFri, 09 Jan 2026 20:33:32 +0000gcollazo.comPython N00bhttps://gcollazo.com/python-n00b<p> Hace mas de dos años he estado interesado en aprender <a href="proxy.php?url=https://en.wikipedia.org/wiki/Python_%28programming_language%29" >Python</a >. Este lenguaje se caracteriza por ser fácil de escribir y sobre todo fácil de leer. Tiene la capacidad de mezclar poder con <i>friendliness</i>. </p> <p></p> <p> Hace poco menos de un mes decidí que ya era tiempo de añadir esta herramienta a mi arsenal así que me compré unos cuantos libros y desde entonces he estado sumergido en el mundo de Python. He aprendido sobre <a href="proxy.php?url=https://www.python.org/~guido/">Guido van Rossum</a>, <a href="proxy.php?url=http://www.jython.org/">Jython</a>, <a href="proxy.php?url=http://pyjs.org/">Pyjamas</a>, <a href="proxy.php?url=http://ironpython.net/">IronPython</a>, <a href="proxy.php?url=http://codespeak.net/pypy/dist/pypy/doc/">PyPy</a>, <a href="proxy.php?url=https://code.google.com/p/unladen-swallow/">unladen-swallow</a>, <a href="proxy.php?url=https://www.python.org/dev/peps/pep-3000/">Python 3000</a>, <a href="proxy.php?url=http://www.crummy.com/software/BeautifulSoup/">Beautiful Soup</a>, <a href="proxy.php?url=http://www.tornadoweb.org/">Tornado</a> y muchas otras personas, tecnologías y herramientas que nunca hubiera conocido en otras circunstancias. </p> <p></p> <p> De esta experiencia aprendí mucho. Primero que saber programar no tiene nada que ver con lenguajes de programación. Saber programar es una destreza completamente separada del lenguaje que se conozca y que la noción de que se es mejor programador con un lenguaje que con otro es una falacia. La realidad es que se es mejor programador con mas práctica y el ejercicio de aprender un nuevo lenguaje es un gran ejercicio. Como resultado cuando se aprende un nuevo lenguaje se es mejor programador. </p> <p> En mi caso personal Python me enseño a tratar de ser mas económico en la cantidad de código que uso, a ser mas conciso. Ahora cuando escribo JavaScript o PHP en el día a día uso las cosas nuevas que he aprendido al estar expuesto a Python. </p> <p> Finalmente quiero decir que Python me parece una herramienta súper poderosa. Es un lenguaje muy utilizado y con una comunidad vibrante de desarrollares produciendo todo tipo de packages. En las pocas semanas que llevo aprendiendo y practicando me he dado cuenta que hay un package para casi cada cosa que se me ocurre. Además he descubierto un proyecto que se llama <a href="proxy.php?url=http://pyobjc.sourceforge.net/">PyObjC</a> que es un puente entre Python y <a href="proxy.php?url=https://en.wikipedia.org/wiki/Objective-C">Objective-C</a> / <a href="proxy.php?url=https://en.wikipedia.org/wiki/Cocoa_%28API%29">Cocoa</a>, lo que permite escribir aplicaciones nativas para Mac OS X usando solo Python. Un ejemplo de una aplicación escrita de esta manera es <a href="proxy.php?url=http://www.checkoutapp.com">Checkout</a>, un sistema de POS. Pienso que mi primer proyecto será una aplicación para mac escrita en Python. </p> <p> Mas adelante hablaré un poco sobre los libros que compré cuales son buenos y cuales malos. </p> <p> Quiero saber tu experiencia aprendiendo nuevos lenguajes de programación, si sabes Python y que te gusta o no de este lenguaje. Deja un comentario abajo. </p>[email protected] (gcollazo)Tue, 15 Dec 2009 00:00:00 -0000https://gcollazo.com/python-n00bmongoDB.apphttps://gcollazo.com/mongodb-app<p> <img src="proxy.php?url=/public/media/mongodbapp.png" /> </p> <p> A few days ago I released my first ever “real” swift app. The app is called MongoDB.app and it’s just a thin wrapper around the official MongoDB binaries that you can download from their site. The app will add a status menu icon which will allow you to start and stop a local MongoDB server visually. All data saved by the server will be stored to <em>~/Documents/MongoData</em>. </p> <p><!--more--></p> <p> The idea was to create the simplest possible experience for MongoDB beginners. My inspiration was the great <a href="proxy.php?url=http://postgresapp.com/">Postgres.app</a> which does an amazing job at running PostgreSQL for your mac development environment. </p> <p> Currently the app is using the latest stable release of mongo which at this moment is <a href="proxy.php?url=https://www.mongodb.org/downloads">v2.6.7</a>. I plan to keep the app up to date so expect frequent updates which for now I will post on the <a href="proxy.php?url=https://github.com/gcollazo/mongodbapp/">GitHub repo</a> <del datetime="2016-01-10T23:03:35+00:00" >until I figure out how to do automated updates using the <a href="proxy.php?url=http://sparkle-project.org/">Sparkle framework</a>. I might use some help doing this </del> . The app will check for updates at startup and allow one-click download and update using Sparkle framework. </p> <p> Inside the actual application package you will be able to find all the standard binaries that ship with mongodb including: </p> <ul> <li>bsondump</li> <li>mongo</li> <li>mongod</li> <li>mongodump</li> <li>mongoexport</li> <li>mongofiles</li> <li>mongoimport</li> <li>mongooplog</li> <li>mongoperf</li> <li>mongorestore</li> <li>mongos</li> <li>mongosniff</li> <li>mongostat</li> <li>mongotop</li> </ul> <p> If you want to have access to all of these from your terminal you should add the application’s vendor folder to your path by adding the following to ~/<em>.bash_profile</em>. </p> <pre><code class="bash"># Add MongoDB.app to path PATH="/Applications/MongoDB.app/Contents/Resources/Vendor/mongodb:$PATH" </code></pre> <p> <strong ><a href="proxy.php?url=https://github.com/gcollazo/mongodbapp/releases" >Download</a ></strong ><br /> <img decoding="async" src="proxy.php?url=https://img.shields.io/github/release/gcollazo/mongodbapp.svg" /> </p> <p> To get the latest release go to the <a href="proxy.php?url=https://github.com/gcollazo/mongodbapp/releases" >repo&#8217;s releases page</a >. Please let me know if you find this useful. </p>[email protected] (gcollazo)Fri, 16 Jan 2015 00:00:00 -0000https://gcollazo.com/mongodb-app¿Quieres aprender a programar? Empieza aquíhttps://gcollazo.com/quieres-aprender-a-programar-empieza-aqui<p> <span >Con cierta frecuencia amigos, familiares y personas en Twitter o Facebook me preguntan sobre cómo aprender a programar. Algunos me dicen “quiero aprender hacer apps” refiriéndose a las aplicaciones en teléfonos móviles&nbsp;y otros me dicen que quieren hacer un website para su negocio.</span > </p> <p> Mi respuesta casi automática&nbsp;es la que creo que muchos otros programadores tienen, enviar unos cuantos enlaces&nbsp;a uno de los muchos <a href="proxy.php?url=https://www.freecodecamp.org/">sites de cursos online</a> o <a href="proxy.php?url=https://eloquentjavascript.net/">algún libro</a>. Lo que nunca he hecho es sentarme y pensar cuidadosamente como fue que yo aprendí y tratar de extraer lecciones de mi experiencia. </p> <p> Esta es la intención de este post. Compartir&nbsp;las lecciones de mi experiencia en los últimos 15 años tratando de aprender a programar. Este es mi mejor intento de producir una contestación correcta para la pregunta: ¿Cómo aprendo a programar? </p> <p> <span ><b> 1. Necesitas un primer proyecto<br /></b></span >Decide que quieres construir antes de empezar a aprender. Mi recomendación es que trates de clonar un app o website que conoces bien y usas todo el tiempo. Una mala idea es querer hacer un video juego 3D, Microsoft Excel o Facebook. Escoge algo simple, no te compliques la vida. </p> <p> <span >Aprender a programar es suficientemente complicado como para tratar de ser ambiciosa / ambicioso en este punto. Si no se te ocurre nada trata con una calculadora de propinas o haciendo un website o portafolio online.</span > </p> <p> <span >Yo aprendí mucho tratando de clonar <a href="proxy.php?url=https://en.wikipedia.org/wiki/Minesweeper_(video_game)" >Minesweeper</a >, el juego que venía con Windows 95 o 98, no recuerdo bien.</span > Años después hice un website que usaba una base de datos para generar páginas dinámicamente. Esta experiencia me ayudó a aprender a usar bases de datos en el contexto de aplicaciones web. </p> <p> <span ><b> 2. Aprende lo menos posible<br /></b></span >Enfócate en aprender sólo lo necesario para ejecutar tu primer proyecto. Hay una cantidad prácticamente infinita de información sobre programación. Si tratas de entender todo antes de ejecutar algo concreto, nunca terminarás. Este fue mi problema principal tratando de aprender y la razón por la cual me tomó años en vez de meses. La estrategia es aprender de programación general mientras te enfocas en los problemas específicos que plantea cada proyecto que haces. Esta es la única estrategia efectiva que conozco. </p> <p> <span ><b> 3. Define una fecha de entrega<br /></b></span >Necesitas tener una fecha concreta para terminar tu primer proyecto y tienes que esforzarte para lograr cumplir con esa fecha. Piensa que es un trabajo y que te están pagando para hacerlo. Si puedes dejar de tratar y aprender cuando se ponga difícil, nunca vas a terminar. Esto también me pasó muchas veces y como lo resolví fue prometiendo a un amigo que le mostraría la aplicación terminada en 3 semanas. Un poco de presión social ayuda. </p> <p> <span><b> 4. Busca&nbsp;</b></span ><b>ayuda<br /></b>Identifica personas que te puedan ayudar. Todos los programadores que conozco les encanta ayudar a personas que están empezando. Me atrevo a decir que es casi parte de la cultura. No tengas miedo, haz preguntas. Si no conoces a nadie puedes contactar a <a href="proxy.php?url=https://twitter.com/jpadilla_">@jpadilla_</a>, <a href="proxy.php?url=https://twitter.com/skfroi">@skfroi</a> o a mi <a href="proxy.php?url=https://twitter.com/gcollazo">@gcollazo</a>&nbsp;/ <a href="proxy.php?url=mailto:[email protected]">[email protected]</a>&nbsp;y te ayudaremos con gusto. </p> <p> <span ><b> 5. No te rindas<br /></b></span >Aprender a programar es increíblemente difícil, no importa lo que nadie diga. La buena noticia es que ahora hay muchos recursos y comunidades diseñadas especialmente para ayudarte. Cuando sientas que no quieres seguir porque es muy difícil o estás cansada/o, toma un descanso y cuando tengas más energía sigue tratando. Busca ayuda y no te rindas, es difícil pero posible. </p> <p> <span ><b> 6. Proyecto #1: Done<br /></b></span >Una vez completes tu primero proyecto todo se vuelve mucho más fácil. Cuando terminé mi primero proyecto, me di cuenta que todas las aplicaciones son una combinación diferente de los mismos principios que ya había aprendido. Para hacer el segundo proyecto sólo tienes que aprender un poquito más y usar lo que ya sabes. </p> <p> <span ><b> 7. Nunca dejes de aprender<br /></b></span >Ya hiciste uno o dos programas así que es hora de empezar de nuevo y repetir el proceso para poder aprender un poco más. Es lo que todos hacemos. Si nunca dejas de estar en este proceso serás una/uno de las/los mejores en esto. </p> <p> <span ><b> ¿Qué tecnologías debo aprender?<br /></b></span >Aunque para mi lo más importante de este post son las 7 lecciones que están arriba no podía faltar una recomendación específica en cuanto a que tecnologías aprender. La lista de abajo está en el orden que creo se deberían estudiar pero esto puede variar para cada cual. </p> <ol> <li> <b></b><span><b>HTML</b> – Sirve para hacer páginas web estáticas.</span> </li> <li> <b></b ><span ><b>CSS</b> – Sirve para hacer que tus páginas web se vean bien.</span > </li> <li> <b></b ><span ><b>JavaScript </b>para el browser&nbsp;– Sirve para añadir interactividad a tus paginas web. Si dominas JS, podrás aprovecharte de todas las capacidades que tienen los browsers, que son muchísimas.</span > </li> <li> <span ><b>Node.js </b>– es una plataforma y no un lenguaje. Node.js te permitirá crear servers para generar páginas web dinámicamente. El lenguaje que se utiliza para programar con Node.js es JavaScript. La ventaja es que en este punto ya deberías saber suficiente JavaScript.</span > </li> <li> <b></b ><span ><b>PostgreSQL</b> – es un programa de base de datos. Le permitirá a tu servidor guardar y buscar datos de forma fácil.</span ><b></b> </li> </ol>[email protected] (gcollazo)Sat, 21 Mar 2015 00:00:00 -0000https://gcollazo.com/quieres-aprender-a-programar-empieza-aquiThe Security Footgun in etcdhttps://gcollazo.com/the-security-footgun-in-etcd<p> From an application security perspective databases are the most valuable parts of our systems. They store the data that gives value to our apps and companies. This data which has been entrusted to us by our users should be kept safe and away of the hands of criminals. </p> <p> Every developer I talk to is very aware of this. Their MySQL, PostgreSQL and MongoDB databases are treated with caution and security is definitely a thought. It doesn’t always works but at least everyone is aware and tries their best. </p> <p> But what happens with databases that don’t feel like “regular databases”? I’m talking about Memcached, Redis and of course etcd. This kind of databases are often used for a single use case and treated without much care. </p> <p> The other day I was reading the CoreOS documentation and came across etcd. I have read about etcd before but in the context of CoreOS is really interesting. Etcd is an integral part of their clustering system. They use it to do orchestration and a lot of other stuff. </p> <p> Etcd is really cool, It uses the raft consensus algorithm to keep all the nodes in sync, it has a very simple to use CLI tool and best of all it has an HTTP API. I really like etcd and it sure feels like we are going to use it in the near future for some of our orchestration needs. </p> <p> Reading about etcd made me remember the news about <a href="proxy.php?url=https://thenextweb.com/insider/2017/01/08/mongodb-ransomware-exists-people-bad-security/" >MongoDB security issues</a > that turned out to be people not enabling authentication which is not enabled by default. It really beats me why this is the happy path but hey, I don’t build databases I just use them. </p> <p> Here’s an excerpt from the etcd’s documentation talking about their authentication mechanism. </p> <blockquote> <p> “etcd before 2.1 was a completely open system; anyone with access to the API could change keys. In order to preserve backward compatibility and upgradability, this feature is off by default.” <a href="proxy.php?url=https://coreos.com/etcd/docs/latest/v2/authentication.html" >Read more</a > </p> </blockquote> <p> Yes. The same thing, etcd has an authentication mechanism which is disabled by default and it also has a very nice RESTful API as it’s main interface, what could go wrong right. People are smart and they will keep their etcd services from leaking to the open internet. Wrong! </p> <p> I did a <a href="proxy.php?url=https://www.shodan.io/search?query=etcd">simple search on shodan</a> and came up with <strong>2,284 etcd servers on the open internet</strong>. So I clicked a few and on the third try I saw what I was hoping not to see. CREDENTIALS, a lot of CREDENTIALS. Credentials for things like cms_admin, mysql_root, postgres, etc. </p> <p> In order to try to get a sense of the issue I downloaded the full shodan report and wrote a very simple script that basically called the etcd API and requested all keys. That’s basically equivalent to doing a database dump but over their very nice REST API. </p> <pre><code>GET http://&lt;ip address&gt;:2379/v2/keys/?recursive=true</code></pre> <p> This will return all the keys stored on the servers in JSON format. So my script basically went down the list and created a file for each IP (127-0-0-1.json) with the contents of etcd. I stopped the script at about 750 MB of data and 1,485 of the original IP list. </p> <p> Then I performed a few basic searches to get a sense of what was there and found a bunch of credentials. Passwords for databases of all kinds, AWS secret keys, and API keys and secrets for a bunch of services. Also came across a few certificates, you name it. There’s a lot of stuff there. </p> <figure> <table> <tbody> <tr> <td>password</td> <td>8781</td> </tr> <tr> <td>aws_secret_access_key</td> <td>650</td> </tr> <tr> <td>secret_key</td> <td>23</td> </tr> <tr> <td>private_key</td> <td>8</td> </tr> </tbody> </table> </figure> <p> I did not test any of the credentials but if I had to guess I would guess that at least a few of them should work and this is the scary part. Anyone with just a few minutes to spare could end up with a list of hundreds of database credentials which can be used to steal data, or perform a ransomware attacks. </p> <p> In order to prevent this from happening just enable authentication and if is not strictly required please get your etcd servers off the open internet. Use a security group or set a firewall rule to avoid random people from reading and writing to your etcd server. </p> <p> Finally, I did not test this but since all of these etcd servers are completely open I’m almost certain that an attacker could write to them using the same API. An attacker might use it to change the data in etcd and mess with configuration and even maybe authentication or it could be used to store exfiltrated data from other attacks. </p> <p> I really hope that the etcd team would reconsider their position and make a breaking change soon to enable authentication by default. As we learned from the MongoDB experience this is a huge footgun that can be easily removed an otherwise awesome software. </p> <hr /> <p> Update: Twitter user <a href="proxy.php?url=https://twitter.com/BradErzwh">@BradErzwh</a> reminded me to mention that etcd is “used as Kubernetes’ backing store for all cluster data”. In most cases that data includes secrets encoded as base64 strings. <a href="proxy.php?url=https://kubernetes.io/docs/concepts/overview/components/#etcd" >More info</a > </p>[email protected] (gcollazo)Fri, 16 Mar 2018 00:00:00 -0000https://gcollazo.com/the-security-footgun-in-etcdSobre como ser mejor developerhttps://gcollazo.com/sobre-como-ser-mejor-developer<p> En abril pasado mi amigo Froilan Irizarry publicó un <a href="proxy.php?url=https://medium.com/@skfroi/quieres-ser-considerado-un-desarrollador-senior-se-un-mentor-primero-b92d0e330a57" >blog post</a > hablando de sobre como convertirse en un “senior developer” y hace unos días en Facebook me encontré con <a href="proxy.php?url=https://www.facebook.com/groups/programadorespuertorico/permalink/915314515296277/" >un post</a > preguntando “¿Cuál ha sido su estrategia para sobresalir en el campo laboral?” </p> <p> Ambas publicaciones me hicieron pensar en cual es mi respuesta a esta pregunta de sobre como mejorar y sobresalir en la industria de desarrollo de software. </p> <p> A continuación los factores que considero más importantes para lograr ser un mejor developer y con suerte sobresalir. </p> <p> <strong>Enfocarse en el usuario y su experiencia<br /></strong>Todos mis proyectos comienzan con dibujos detallados de lo que será la solución final y de ahí trabajo para determinar cuales son los requisitos técnicos para lograr la solución. El diseño define los requisitos técnicos. Al cliente no le importa si usas C#, Haskell, PHP, VBScript o “whatever” para solucionar el problema. Lo que importa es la solución y en eso hay que enfocarse. </p> <p> <strong >Los “soft skills” son tanto o más importantes que las capacidades técnicas</strong ><br />Entender el problema que quiere resolver el cliente, poderlo explicar a otros, poder plantear posibles soluciones, escuchar otros puntos de vista con respeto y empatía, ponerse en los zapatos de otros y ser tolerante. </p> <p> <strong>Saber de lo que estás hablando<br /></strong>Ningún argumento le gana a una <strong>experiencia del mundo real</strong> en un escenario similar. Hacer muchos proyecto es la mejor forma de tener éxito. Todos los proyectos tendrán problemas y serán difíciles de completar pero si tienes muchos por lo menos algunos serán buenos y poco a poco con el tiempo aprenderás a reconocer y utilizar los patrones que te llevan a lograr proyectos exitosos. </p> <p> <strong>Compartir lo que has aprendido</strong><br />Para mi la mejor forma de aprender y aclarar ideas es tratando de compartirlas. Trato de compartir lo que aprendo con todo el que le interese. Para esto escribo blog posts, hago presentaciones en “meetups” y conferencias y recientemente rants en Twitter. </p> <p> <strong>“Under promise and over deliver”</strong><br />Ser conservador en cuanto a estimados de tiempo y “features”. Luego si el tiempo permite, sorprender al cliente con una experiencia mejor a la esperada. Este funciona todas las veces. </p>[email protected] (gcollazo)Sat, 21 Mar 2015 00:00:00 -0000https://gcollazo.com/sobre-como-ser-mejor-developerUn bug en el app CESCO Digital expone la información de todos los conductores registrados en Puerto Ricohttps://gcollazo.com/bug-app-cesco-digital<h4>Antes de empezar</h4> <p> Reporté este <a href="proxy.php?url=https://en.wikipedia.org/wiki/Software_bug">bug</a> dos veces con la intención de que se resolviera el problema y se protegiera la información de los ciudadanos. La primera vez que reporté el problema recibí confirmación de que sería atendido, pero ocho meses más tarde el problema no había sido resuelto. </p> <p> Recientemente, reporté el bug por segunda vez e hice énfasis en el riesgo potencial de este tipo de vulnerabilidad y en mi intención de hacer público el problema siguiendo el modelo de “<a href="proxy.php?url=https://en.wikipedia.org/wiki/Responsible_disclosure" >responsible disclosure</a >”. Esta vez había gente diferente a cargo del otro lado y se atendió la situación con prontitud hasta corregir el problema. </p> <p> A cambio de este reporte no se pidió recompensa, ni contratos para asesoría, ni contratos para programar, ni ninguna otra forma de compensación directa o indirecta. Tengo trabajo y estoy bastante ocupado con eso pero siento que es mi responsabilidad reportar este tipo de problema cuando lo encuentro para que se pueda resolver y evitar situaciones que lamentar. </p> <p> En el pasado he reportado problemas como este a compañías pequeñas y grandes dentro y fuera de Puerto Rico. Esta es mi primera vez haciendo “responsible disclosure” con una entidad del Gobierno de Puerto Rico, lo que me ha tenido algo nervioso durante los últimos días. </p> <p> Como todos sabemos, en nuestras islas cualquier cosa que hace o no hace el gobierno es una controversia y estoy seguro que algunos me criticarán por mi acercamiento y otros dirán lo bien que lo hice según les convenga para adelantar su punto de vista. </p> <p> Me siento satisfecho y convencido de que hice lo correcto y logré conseguir el resultado deseado. </p> <span id="more-4060"></span> <h4>El bug</h4> <p> Según <a href="proxy.php?url=https://en.wikipedia.org/wiki/OWASP">OWASP</a>, el nombre técnico de este bug es un “<a href="proxy.php?url=https://www.owasp.org/index.php/Top_10_2007-Insecure_Direct_Object_Reference" >insecure direct object reference</a >“. Este tipo de bug permite a un usuario autorizado, cambiar el valor de un parámetro para tener acceso a un objeto para el cual no tiene autorización. </p> <p>Trataré de explicarlo con un ejemplo simple.</p> <p> Cuando usted usa Google Drive y está viendo un documento, el browser muestra arriba la dirección web de ese documento. Para propósitos de esta explicación digamos que la dirección es <em>https://drive.google.com/documentos/101</em>. La última parte de esa dirección (101) muestra el nombre o el valor que identifica a ese documento dentro de Google Drive. </p> <p> Si usted modifica la dirección y reemplaza el <strong>101</strong> por <strong>102</strong> casi seguro verá un error en la pantalla. El error será algo como “no se encontró el documento” o “no tiene permiso para ver este documento”. </p> <p> Lo que está pasando en este ejemplo es que Google Drive usa la última parte de la dirección para identificar cada documento que almacena para sus usuarios. El documento <strong>101</strong> te pertenece, así que lo puedes ver, pero el documento <strong>102</strong> probablemente sea de otro usuario así que no tienes permiso para verlo. </p> <p> El <a href="proxy.php?url=https://en.wikipedia.org/wiki/Application_programming_interface" >API</a > que usa el app de <a href="proxy.php?url=https://itunes.apple.com/us/app/cesco-digital/id1389951990" >CESCO Digital</a > para acceder la información de los ciudadanos tenía un bug que permitía ver la información de cualquier ciudadano con solo cambiar la parte final de la dirección, tal y como expliqué en el ejemplo anterior. Si su información personal era visible en la dirección que termina en <strong>101</strong> y usted cambia esa dirección para que termine en <strong>102</strong>, usted vería la información de otro ciudadano. </p> <p> Un atacante al darse cuenta de esta vulnerabilidad podría escribir un programa que de forma automática modifique la dirección empezando por el número 1, luego 2, 3, 4, 5 y así consecutivamente hasta llegar a ver y almacenar los datos de todos los usuarios. </p> <p> Este ataque se podría completar en horas pero si se tratara de atacante un poco mas sofisticado, podría estirar el proceso a lo largo de multiples días o incluso semanas para asegurarse de no levantar alarmas o sospechas de los administradores del sistema. </p> <p>Entre lo datos de los ciudadanos que quedaron expuestos están:</p> <ul> <li>Nombre legal del ciudadano</li> <li>Fecha de nacimiento</li> <li>Dirección física</li> <li>Últimos 4 dígitos del número de seguro social</li> <li>Número de licencia de conducir</li> <li>Fecha de expiración de licencia de conducir</li> <li> Autos registrados con marca, modelo, color, tablilla y <a href="proxy.php?url=https://en.wikipedia.org/wiki/Vehicle_identification_number" >VIN</a > </li> </ul> <h4>Un poco más técnico</h4> <p> Para detectar el problema lo único que había que hacer era usar un <a href="proxy.php?url=https://en.wikipedia.org/wiki/Proxy_server">proxy</a> e inspeccionar los <a href="proxy.php?url=https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_message" >requests</a > que hace la aplicación al API. En mi caso uso <a href="proxy.php?url=https://itunes.apple.com/us/app/charles-proxy/id1134218562" >Charles Proxy</a > en iOS y <a href="proxy.php?url=https://mitmproxy.org">mitmproxy</a> en macOS. </p> <figure> <video width="100%" controls="" poster="/public/media/[email protected]" src="proxy.php?url=/public/media/Vulnerabilidad-en-app-CESCO-Digital-oHAULBHGjyg.mp4" ></video> <figcaption> En este vídeo se demuestra el bug antes de ser corregido </figcaption> </figure> <p> Lo primero que llama la atención es que desde el primer request al API se puede ver un valor en el <a href="proxy.php?url=https://en.wikipedia.org/wiki/List_of_HTTP_header_fields">header</a> de “Authorization”. Esto es inesperado porque no he entrado ningún dato y el app ya está haciendo llamadas autenticadas al API. Aunque es poco común, no es necesariamente un problema. </p> <pre><code>GET /config HTTP/1.1 Host: cescodigital.dtop.gov.pr Authorization: Basic dGVzdDoxMjM0NTppb3NDZXNjb0RpZ2l0YWw= User-Agent: dtop_mobile/1 CFNetwork/976 Darwin/18.2.0 Connection: keep-alive Accept: application/json Accept-Language: en-us Accept-Encoding: gzip, deflate, br</code></pre> <p> Al inspeccionar el valor que se está enviando me doy cuenta que esta usando “<a href="proxy.php?url=https://en.wikipedia.org/wiki/Basic_access_authentication" >basic access authentication</a >” y se que en esos casos se envía el valor codificado en <a href="proxy.php?url=https://en.wikipedia.org/wiki/Base64">base 64</a>. Copié el valor y lo descodifiqué para darme cuenta que definitivamente era un valor “<a href="proxy.php?url=https://en.wikipedia.org/wiki/Hard_coding" >hard coded</a >” dentro del app. El valor descodificado era “test:12345:iosCescoDigital”. </p> <pre><code>$ echo "dGVzdDoxMjM0NTppb3NDZXNjb0RpZ2l0YWw="|base64 -D test:12345:iosCescoDigital</code></pre> <p> Al darme cuenta de esto pensé que quizás una vez se completara el cuestionario que hace el app para autorizarme se crearía algún tipo de <a href="proxy.php?url=https://en.wikipedia.org/wiki/Session_(computer_science)">sesión</a> que me identificaría en los otros requests del app. </p> <p> Entré mi número de licencia de conducir, últimos 4 dígitos del seguro social y mi fecha de nacimiento. El API validó mis datos y respondió con alguna información personal y un “uid” que me identifica dentro del sistema. </p> <pre><code>{ "uid": "1234567", "name": "DEL PUEBLO, JUAN", "entityType": "ssid" }</code></pre> <p> Inmediatamente el app cambia de pantalla y muestra mis datos personales. Cuando miro el request que hace el app puedo ver que la dirección que usa contiene el mismo “uid” del request anterior y cuando me fijo en los headers me doy cuenta que no hay ninguna información de sesión o algo parecido para identificarme. Lo único que puedo ver es el mismo authorization header “hard coded” del principio. </p> <pre><code>GET /entity/1234567 HTTP/1.1 Host: cescodigital.dtop.gov.pr Authorization: Basic dGVzdDoxMjM0NTppb3NDZXNjb0RpZ2l0YWw= User-Agent: dtop_mobile/1 CFNetwork/976 Darwin/18.2.0 Connection: keep-alive Accept: application/json Accept-Language: en-us Accept-Encoding: gzip, deflate, br { "principal": { "uid": "1234567", "name": "DEL PUEBLO, JUAN", "email": null, "entityType": "ssid" }, "license": { "category": "3", "expirationDate": 946699200000, "dateOfBirth": 946699200000, "address": { "line1": "123 MAIN STREET", "line2": "", "city": "SAN JUAN", "state": "PR", "zipCode": "00901" }, "unpaidFines": 0, "alert": { "type": "Warning", "text": "RenewLicense" }, "id": XXXXXX }, "vehicles": [ { "vinNumber": "XXXXXXXXXXXXXXXXX", "registrationNumber": "XXXXXXXX", "year": XXXX, "make": "XXXXXX", "model": "XXXXXX", "color": "XXXXX", "plate": "XXXXXX", "tag": { "number": "XXXXXXXX", "expirationDate": XXXXXXXXXXX, "id": XXXXXXXX }, "unpaidFines": X, "alert": null, "canRenewTag": false, "id": XXXXXXXX } ] }</code></pre> <p> En este punto ya estaba casi seguro del problema por lo que hice “log out” y le dije a mi esposa que hiciera “login” para confirmar el problema. Siguió el mismo proceso que yo y tan pronto entró pude ver que sus datos personales venían de una dirección diferente pero estaba usando la misma información de autenticación que yo. Esto confirmó el bug. </p> <h4>Primer intento</h4> <p> Tan pronto confirmé el bug traté de reportarlo y logré conseguir una confirmación de que fue recibido y que se atendería a la mayor brevedad posible. Dejé el tema ahí y lo di por resuelto. Me olvidé de este asunto. </p> <p> Hace más o menos una semana tuve que entrar al app para buscar la licencia nueva de mi auto. Me tocaba renovar el marbete. Cuando entro al app veo que todo se ve igual y recordé el bug, así que me dio curiosidad mirar a ver si resolvieron el problema que reporté. </p> <p> Activé Charles Proxy para inspeccionar la comunicación del app con el API. Para mi sorpresa habían pasado <strong>ocho meses</strong> y no se había resuelto el problema. La información de todos los ciudadanos seguía expuesta y disponible para el que la quisiera obtener. </p> <h4>Segundo intento</h4> <p> Esta vez decidí tomar un acercamiento un poco más fuerte y proveer los incentivos necesarios para que las personas a cargo se vieran motivadas a resolver el problema con la prisa que requería. </p> <p> Cuando reporté el problema esta vez expliqué nuevamente los riesgos y mi intención de hacer “responsible disclosure”. En otras palabras, que era importante ponernos de acuerdo en un “timeline” en el que las personas a cargo resolverían el problema y que en un tiempo específico yo publicaría este blog post que estas leyendo, explicando el problema en detalle. </p> <p> Esto significa que si no lograran resolver el problema en un tiempo previamente acordado, la única opción que tendrían sería apagar el servicio por completo para evitar que atacantes que lean sobre la vulnerabilidad la exploten. </p> <p> En ese momento no sabía cómo se tomaría la idea y si se volvería una situación incómoda pero para mi sorpresa se me escuchó y se le prestó la atención que pienso era necesaria. Durante el proceso hubo comunicación y cuando se me pidió, saqué tiempo y compartí todo lo que sabía sobre el problema con los involucrados del otro lado. </p> <p> Para mantenerme honesto, tan pronto reporté el bug a las personas encargadas, también alerté de forma confidencial a miembros de la prensa bajo la condición de que no se publicara nada hasta por lo menos una semana después del reporte original. </p> <p> Una semana me pareció un tiempo justo y suficiente para hacer los ajustes necesarios o apagar el servicio. Recordemos que el problema fue reportado ocho meses antes. </p> <p> Tres días más tarde un equipo de desarrolladores resolvió el problema y al día siguiente y por petición de ese equipo, pude confirmar que el problema estaba resuelto. </p> <h4>La solución</h4> <p> Los desarrolladores implementaron una solución bastante ingeniosa que permite proteger los datos de los ciudadanos de este ataque sin la necesidad de actualizar el app. </p> <p> Ahora, luego del cambio, cuando un usuario completa el proceso de login, el app recibe una respuesta idéntica a la anterior pero en el campo de “uid” el valor es ahora un <a href="proxy.php?url=https://en.wikipedia.org/wiki/Cryptographic_hash_function">hash</a> <a href="proxy.php?url=https://en.wikipedia.org/wiki/SHA-2">SHA256</a> en vez de un número. </p> <pre><code>{ "uid": "afa27b44d43b02a9fea41d13cedc2e4016cfc...", "name": "Juan del Pueblo", "entityType": "ssid" }</code></pre> <p> En vez de tener direcciones como <em>/entity/1234567</em> ahora tienen direcciones como esta <em>/entity/afa27b44d43b02a9fea41d13cedc2e4…</em> </p> <p> No he visto el código pero de mi conversación con los developers y las pruebas que pude hacer me atrevo a adivinar lo siguiente: </p> <ul> <li> El hash se calcula usando algún valor secreto, la fecha y/o data random. </li> <li> El hash expira luego de un tiempo, forzando al usuario a entrar nuevamente su información de login para continuar usando el app. </li> <li> El tiempo de validez es aproximadamente 30 minutos así que no debe tener impacto en la experiencia de los usuarios al usar el app. </li> <li> Posiblemente están persistiendo el hash en la base de datos para poder expirarlo. </li> <li> Es posible que también estén persistiendo la relación entre el hash generado y el ID interno del usuario. </li> </ul> <p> En resumen, es suficientemente difícil adivinar un hash que sea válido para que valga la pena intentarlo. </p> <p> Esta solución está cumpliendo el rol de una sesión que confirma que el usuario completó el proceso de login (autenticación) y que tiene permiso para ver ciertos datos (autorización) en todos los requests al API. </p> <h4>Timeline</h4> <table> <tbody> <tr> <td width="150">2018-06-26</td> <td>Se reportó el problema por primera vez.<br /></td> </tr> <tr> <td>2018-06-26</td> <td> Recibí confirmación de que el reporte fue recibido y que se canalizaría.<br /> </td> </tr> <tr> <td>2019-02-04</td> <td>Se reportó el problema por segunda vez.<br /></td> </tr> <tr> <td>2019-02-04</td> <td> Recibí confirmación de que el reporte fue recibido y que se trabajaría de inmediato.<br /> </td> </tr> <tr> <td>2019-02-04</td> <td> Se compartió el bug confidencialmente con miembros de la prensa.<br /> </td> </tr> <tr> <td>2019-02-07</td> <td> Se le explicó el problema al equipo de desarrollo, confirmaron el bug y dijeron que lo resolverían ese mismo día.<br /> </td> </tr> <tr> <td>2019-02-08</td> <td>Confirmé que se resolvió el problema.</td> </tr> <tr> <td>2019-02-12</td> <td>Se publicó este blog post.</td> </tr> </tbody> </table> <br /> <h4>Hace falta un proceso</h4> <p> Todo software tiene bugs. Esto siempre ha sido cierto y siempre será cierto. Algunos de estos bugs tienen implicaciones de seguridad y deben ser atendidos de una forma diferente. Nuestro gobierno es guardian de muchos datos sensitivos y deberían prepararse mejor para atender situaciones en las que estos datos estén en riesgo. </p> <p> Hay muchas formas de hacer esto, pero hay que empezar por algún lado. Mi recomendación es publicar un proceso para reportar bugs de seguridad en el que los que reportan se sientan a salvo. Este documento debe contener nombres e información de contacto de las personas que atenderán los casos que se reporten. Además debe especificar términos de tiempo y los pasos que se seguirán para manejar la situación. </p> <p> Luego de tener esto, las personas encargadas deben respetar ese proceso y colaborar con las personas que hagan los reportes. Tal y como pasó en mi segundo intento. </p> <p> Como punto de partida pueden copiar <a href="proxy.php?url=https://www.gasolinamovil.com/security/" >el proceso que usamos en Gasolina Móvil</a > que aunque no es perfecto, funciona cuando el volumen de reportes es bajo. Si lo usan recuerden cambiar los nombres e información de contacto. 😉 </p> <p> Una vez tengan algo simple funcionando, se podría pensar en implementar un programa de “<a href="proxy.php?url=https://en.wikipedia.org/wiki/Bug_bounty_program" >bug bounties</a >” aunque no se pague dinero por los reportes. La recompensa podría ser algo tan simple como tener una página web que contenga una lista de las personas que más bugs han reportado. Como los “leaderboards” que tienen algunos juegos de video. </p> <p> Estoy seguro que hacer algo en esta dirección motivaría a personas con la capacidad de buscar, documentar y reportar responsablemente este tipo de problemas, logrando mejorar la seguridad y funcionamiento de los sistemas del gobierno. </p>[email protected] (gcollazo)Tue, 12 Feb 2019 00:00:00 -0000https://gcollazo.com/bug-app-cesco-digitalEspañol de Puerto Ricohttps://gcollazo.com/espanol-de-puerto-rico<p> Hey, si tu o alguien que conoces maneja un website escrito en Español y producido en Puerto Rico, estaría super cool que lo marcan con el idioma y región correcta. </p> <pre><code>&lt;html lang="es-PR"&gt;</code></pre> <p> Esto ayudaría mucho a <a rel="noreferrer noopener" href="proxy.php?url=https://t.co/wY5WxvdIIf?amp=1" target="_blank" >http://tesoro.pr</a > en varios inventos futuros. </p> <p> Por mi parte todavía estoy pensando cómo manejar el tema en este site. Lo mismo escribo en Español que en Inglés. </p> <p>Links con más información técnica:</p> <p> <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang" >https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang</a > </p> <p> <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html#Example" >https://developer.mozilla.org/en-US/docs/Web/HTML/Element/html#Example</a > </p> <p> <a href="proxy.php?url=https://www.ietf.org/rfc/bcp/bcp47.txt" >https://www.ietf.org/rfc/bcp/bcp47.txt</a > </p>[email protected] (gcollazo)Wed, 02 Dec 2020 00:00:00 -0000https://gcollazo.com/espanol-de-puerto-ricoUser-defined type guards in TypeScripthttps://gcollazo.com/user-defined-type-guards-in-typescript<p> Decided to write this blog post because I couldn’t find any documentation or online examples like the one I’m sharing here. Every example I’ve seen the <a href="proxy.php?url=https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates" >user-defined type guard</a > is used to replace the whole type definition and not specific properties. </p> <p>Here’s the code without type guard.</p> <pre><code>interface Config { foo: string; bar: string; } class Thing { name: string; config?: Config; constructor(name: string, config?: Config) { this.name = name; this.config = config; } // 👀 returning boolean, will change on next example hasConfig(): boolean { if (this.config) { return true; } return false; } } let firstThing = new Thing("Apple", { foo:"one", bar: "two" }); if (firstThing.hasConfig()) { // #1 - Here firstThing.config is `Config | undefined` console.log(firstThing.config.foo); } if (firstThing.config) { // #2 - Here firstThing.config is `Config` console.log(firstThing.config.foo); }</code></pre> <p> On #1 <code>firstThing.config</code>. is <code>Config | undefined</code> even though we know it can’t be <code>undefined</code> because our <code>hasConfig</code> method made sure the value is truthy. </p> <p>On #2 <code>firstThing.config</code> is <code>Config</code> as expected.</p> <p> So in order to help TypeScript detect the correct type we can use a <em>type predicate</em> as the return type. </p> <pre><code>interface Config { foo: string; bar: string; } class Thing { name: string; config?: Config; constructor(name: string, config?: Config) { this.name = name; this.config = config; } // 👀 notice the return type hasConfig(): this is { config: Config } { if (this.config) { return true; } return false; } } let firstThing = new Thing("Apple", { foo:"one", bar: "two" }); if (firstThing.hasConfig()) { // Now firstThing.config is `Config` console.log(firstThing.config.foo); }</code></pre> <p> Now <code>firstThing.config</code> is <code>Config</code> as expected. Have in mind that even though the type guard says <code>this is { config: Config }</code>, TypeScript will merge this type with the original type definition of <code>Thing</code>. </p> <p> Thanks to the nice folks at the TypeScript Discord who helped me solve this issue. Hope this helps more people. </p>[email protected] (gcollazo)Tue, 09 Mar 2021 00:00:00 -0000https://gcollazo.com/user-defined-type-guards-in-typescript¿Dónde está el programa?https://gcollazo.com/donde-esta-el-programa<p> ¿Dónde está el programa? Esa fue la pregunta que hice a un programador de nuestro equipo. ¿Dónde está el código que describe como funciona nuestra aplicación? No pudo contestar y no esperaba que pudiera. En el proyecto que estábamos trabajando, la respuesta era bastante complicada. El programa estaba en todas partes y en ninguna a la misma vez. </p> <p> Cuando comenzamos en este proyecto, lo primero que hicimos fue un prototipo. Bueno, el prototipo lo hice yo y sí, era “founder code”. Se escribió en poco tiempo y funcionó para validar la idea y para poder movernos adelante a conseguir clientes. Nunca me detuve a pensar en arquitectura o si podría escalar. </p> <p> El único objetivo era que funcionara y que convenciera a clientes del potencial del producto. Misión cumplida. </p> <p> Cuando le hice la pregunta al programador, al que llamaré Carlos, ya el proyecto era viejo en años de software y del “founder code” original quedaba muy poco, pero la falta de una arquitectura clara y definida todavía estaba presente. Luego de lograr los primeros clientes, en vez de repensar todo, optimizamos para velocidad y acumulamos mucha deuda técnica. Poco a poco los atajos que tomamos para movernos rápido nos estaban pasando factura. </p> <p> Cundo le hice la pregunta a Carlos, ya llevaba unos meses estudiando sobre arquitectura de software. Antes de meterme en este tema cuando me hablaban de arquitectura pensaba en nuestra infraestructura en AWS. Que servicios estamos usando, si usamos Lambda, si estamos haciendo load balancing o estamos usando un “queue” y este tipo de cosas, nunca pensaba en la estructura de nuestro codebase. </p> <p> Antes de esto tenía algunas nociones de arquitectura de software. Cuando conocí Ruby on Rail aprendí sobre Model, View, Controller y cuando empecé a hacer front end “en serio” usando Backbone.js hablábamos de client side MVC y MVVM. Desde entonces todos los proyectos que trabajé usaban algo parecido a MVC pero nunca me detuve a pensar por qué y si había algo mejor. Parecía funcionar y si todo el mundo lo está usando, creo que estamos ok. </p> <p> Luego trabajé mucho con Django y uno de los “best practices” de la comunidad era la idea “fat models”. Lo que esto quiere decir es que lo que se puede implementar como un método de un modelo, lo debemos implementar como un método del modelo. En esos días, estaba muy pendiente a que no tuviéramos lógica regada por la aplicación y cuando había dudas sobre donde poner algo decía casi automáticamente, “fat models”. Tuve mucho éxito haciendo esto pero parecería que no aprendí nada. </p> <p> Hoy llevo mas de un año estudiando sobre arquitectura de software. He aprendido mucho sobre como pensar acerca de la estructura de un proyecto de software y sobre que cosas funcionan en los codebases que trabajo y con los equipos que trabajo. </p> <p> De todo lo que he estudiado lo más que me ha hecho sentido han sido acercamientos que se pueden considerar principalmente “object-oriented” pero vale la pena mencionar que lo que aprendí en mis años en el culto de “functional programming”, sigue siendo útil y lo uso todos los días. Una de las grandes ventajas de JavaScript, el lenguaje que uso principalmente, es que es multi paradigma y nos deja mezclar lo mejor de OOP y FP sin problema. </p> <p> Cuando aprendí sobre domain-drive design o DDD, me hizo click inmediatamente. Todos los problemas que describían los había vivido y las soluciones que planteaban, no solo eran mejor para el codebase si no que también ayudaban a facilitar la comunicación entre el equipo e incluso los clientes. No voy ni a intentar definir DDD, pero es un tema que creo todo el mundo que le interese escribir software “mantenible” y “testiable” debe estudiar. </p> <p> DDD, resultó ser una especie de puerta a un mundo de patrones de diseño y arquitecturas que ponen en el centro el “domain”. </p> <p> El “domain” es la capa que contiene el programa. En el domain hay objetos que están diseñados para representar todo lo que existe en la aplicación, en el domain están todas las acciones que esos objetos pueden hacer y en el domain es imposible tener data en un estado inválido. En el domain no hay código específico de ningún framework o base de datos o cualquier otra dependencia de la infraestructura. Es lógica pura. </p> <p> Esta era la respuesta que estaba buscando. De ahí me moví a aprender más sobre arquitecturas que usan esta idea de el domain en el centro de todo. Aprendí sobre Clean Architecture, Onion Architecture y Hexagonal Architecture o Ports and Adapters Architecture que es como prefiero referirme a este diseño. Todas estas arquitecturas aportan ideas valiosas y todas ponen el domain en el centro. </p> <p> Para nuestro equipo llegamos a un diseño inspirado principalmente por Ports and Adapters Architecture. La experiencia hasta el momento ha sido positiva. Hacer cambios es fácil, es obvio donde están las reglas de negocio, nos obliga a ser mucho más explícitos y sentimos que escribimos menos bugs. Además hacer pruebas es fácil, rápido y casi divertido. </p> <p> Recientemente, trabajando en un proyecto nuevo, después de semanas de haber comenzado, decidimos cambiar el web server library de Express.js a Fastify. El cambio tomó poco más de un día de trabajo y fue fácil lograr que todo funcionara luego del cambio. En el código del domain no cambiamos ni una sola linea. </p> <p> Estoy seguro que ahora estarás pensando: ¿Pero cada cuánto tiempo se cambia de web server library? La repuesta es casi nunca, pero si puedo decir que el diseño que nos ayuda a hacer este tipo de cambios fácilmente, también nos ayuda de otras formas. </p> <p> Por ejemplo, tenemos varias implementaciones de base de datos. Una para la base de datos de producción y otra que usamos en el ambiente de pruebas, que almacena los datos en memoria. También tenemos implementaciones de queues en SQS, Redis y una basada en archivos JSON en el file system. Durante development usamos la versión basada en archivos, en staging la versión Redis y en producción la de SQS y el código del domain siempre es el mismo. Los límites de cada capa y las interfaces entre ellas está claramente definidas y esto tiene muchos beneficios. </p> <p> Esta experiencia me ha dado muchas herramientas nuevas y ha revivido mi entusiasmo por la disciplina de crear software. </p> <p> Hoy todavía es difícil contestar la pregunta ¿dónde está el programa? pero todos en el equipo sabemos donde debe estar y cada día que pasa nos acercamos más a la meta. En algunos meses espero poder responder: “En el centro, el programa está en el centro”. </p>[email protected] (gcollazo)Mon, 15 Mar 2021 00:00:00 -0000https://gcollazo.com/donde-esta-el-programaBolsas llenas de funcioneshttps://gcollazo.com/bolsas-llenas-de-funciones<p> En este post menciono functional programming (FP) y object-oriented programing (OOP) en el contexto de JavaScript (JS). Hablo de mis experiencias con los dos paradigmas en un proyecto bastante grande. El título del post <strong>no </strong>es <em>FP vs OOP</em> y no tengo en interés en ese tipo de discusión. </p> <p> Las ideas de FP llevan varios años tomando fuerza en el mundo de JS. Mi interés en el tema llegó algunos años antes del lanzamiento de React. En eso días ya habían librerías y developers muy activos evangelizando sobre FP en JS y sus beneficios. </p> <p> Aunque había algo de interés antes de que React estuviera en el panorama no es hasta el lanzamiento de React y su enorme popularidad que la gran mayoría de devs se interesaron en el tema. </p> <p> Los vendedores (developer advocates) de React presentaron buenos argumentos sobre las ventajas de FP, compartieron ejemplos convincentes y escribieron librerías super populares justo en la raíz de lo que luego se convirtió en la librería más popular e influyente en el mundo de JS desde jQuery. </p> <p> En este momento no se conocían <em>codebases</em> grades y <em>open source</em> que sirvieran de ejemplo para los millones de developers que ahora decidieron escribir a lo FP. Los ejemplos disponibles en muy pocos casos pasaban del ya trillado <em>to-do list</em> o el casi cómico <em>counter</em> que consiste en dos botones que al presionarlos aumentan o disminuyen un número. Quedaba como asignación para el o la developer el definir e implementar una arquitectura escalable en un paradigma poco familiar. </p> <p> Más o menos para esta misma época ya había escrito algunos proyectos usando un estilo, estrictamente funcional o lo más funcional posible dentro de las limitaciones que impone JS. Admito que eran proyectos pequeños con un solo developer, pero eran prometedores. </p> <p> Organizar todo como <em>pure functions</em>, data inmutable y evitar side effects se sentía limpio. Estos proyectos eran básicamente treinta o cuarenta funciones trabajando en conjunto. Las mayoría de las pruebas eran fáciles de escribir y solo tenía una o dos funciones que creaban side effects así que todo era hermoso. </p> <p> Como muchos me deje seducir por la aparente simpleza de este paradigma. Mi error fue pensar que esto que funcionaba para proyectos pequeños podría funcionar en proyectos mucho más grandes. Estas experiencias me dieron confianza en FP JS y me hice de la idea de que se podría usar en proyectos más grandes con los mismos resultados.&nbsp; </p> <p> No funcionó. Cuando traté de llevar lo que había aprendido de FP JS a un proyecto mucho más grande, todo lo que se sentía limpio poco a poco se volvió complicado y difícil de entender. Tenía muy poca experiencia y no conocía suficientes técnicas para manejar esta escala. Teníamos funciones que llaman funciones, que llaman a otras funciones y cada una llama más funciones. </p> <p> Mientras escribíamos el código no veíamos problemas pero cuando había que leer ese código meses más tarde, daba mucho trabajo entenderlo. Los code reviews tomaban mucho tiempo o simplemente se volvían muy superficiales. Hubo muchas señales de que estábamos haciendo algo mal pero las ignoramos. </p> <p> Como buenos programadores de FP, mantuvimos la data separada del comportamiento. Nuestro app recibía objetos simples (POJOs), los transformaba y producía nuevos objetos simples. Suena fácil, pero cuando tienes decenas de funciones, manipulando estos objetos, saber en que estado está la data en todo momento puede ser complicado. Muchas veces nos veíamos obligados a usar el debugger para poder inspeccionar el estado de un objeto en un lugar específico. En otros momentos teníamos que correr y ejecutar la aplicación para entender las estructuras de data. El código no era fácil de entender.&nbsp; </p> <p> En este proyecto siempre tuvimos una idea de “modelos” pero, no eran realmente modelos. Mas bien eran una capa muy delgada por encima de una pequeña abstracción de la base de datos. Nuestros modelos no mantenían estado, simplemente nos daban acceso a la base de datos y servían para guardar algunas funciones que estaban muy ligadas a la entidad específica. </p> <p> Uno de los síntomas más claros de que teníamos un problema es que en el proyecto teníamos una carpeta que se llamaba <em>utils</em> con cientos de files y cada file con dos o tres funciones. Muchas de estas funciones contenían lógica de negocio y hacían cosas realmente importantes y centrales para la aplicación. Estos archivos terminaron en esta carpeta porque eran funciones que queríamos reutilizar y no teníamos una idea clara de donde ponerlas. Después de todo queríamos tratar de mantener la lógica y la data separada y estas funciones eran lógica. </p> <p> Más de una vez durante un review, encontramos que alguien había escrito una función que ya existía. En este proyecto es difícil encontrar código existente para reutilizar. Es fácil imaginar como terminamos con funciones duplicadas que hacen las cosas un poco diferente la una de la otra. Demás está decir que esto produce bugs. </p> <p> En otros casos las funciones estaban muy ligadas a un pedazo de data o a un evento de la aplicación, así que esas funciones se escribían en el mismo archivo donde se usaban, complicando más aún el encontrar funciones para reutilizar y mantener la lógica centralizada. </p> <p> La combinación de estas cosas y la falta de experiencia sobre como diseñar un proyecto FP de gran escala nos llevó a tener una arquitectura que solo se puede describir como <em>una montaña de bolsas llenas de funciones</em>. </p> <p> Estas bolsas de funciones se organizan de muchas formas, alguna veces de acuerdo al tipo de acciones que hacen, otras de acuerdo a la data que modifican y otras como <em>utils</em> cuando no sabemos que hacer con ellas. Sin duda, haber tenido un mejor esquema de organización pudo haber ayudado mucho a reducir la complejidad pero estábamos aprendiendo mientras construíamos nuestro programa y no teníamos muchas referencias. </p> <p> Nuestro problema con este proyecto no es un problema producido por FP, pero si un problema al que es fácil llegar si se tiene poca experiencia con este paradigma y si no existen suficientes recursos para aprender y tomar ideas sobre como diseñar la arquitectura de tu aplicación. Creo que este es el caso de la mayoría de los y las devs que conozco. Les interesan las ideas de FP, se ven fáciles de entender y aplicar pero no tenemos idea de como usarlas efectivamente en un proyecto de gran escala. </p> <p> Pensar que solo con usar funciones puras, tener data inmutable y evitar los side effects es suficiente para crear software de cualquier escala es una trampa. </p> <p> Aprendemos a usar map, reduce y filter y nos creemos genios del FP. Vemos un proyecto con dos o tres pantallas escritas con React y solo funciones y pensamos que eso puede escalar a un proyecto de cualquier tamaño. Nos decimos que si todo es una función pura y fácil de entender, será imposible que se complique. Es tan simple que esa es la única abstracción que hace falta. </p> <p> Cualquier proyecto de suficiente tamaño se puede salir de control si no se tiene una arquitectura clara que nos ayude a organizar el código y a comunicar de forma efectiva lo que hace nuestro programa. </p> <p> Hoy, sigo usando muchas técnicas que aprendí en mis días de FP pero también uso ideas de OOP y me alegra reportar que nadie ha muerto en el proceso. He encontrado un <em>punto medio</em> que usa ideas de ambos paradigmas y que me permite crear aplicaciones con una arquitectura clara, fácil de entender, que me ayuda a moverme rápido y hacer cambios con tranquilidad. </p> <p> En un post futuro hablaré mas en detalle sobre algunas de estas técnicas e ideas de arquitectura. </p>[email protected] (gcollazo)Sun, 11 Apr 2021 00:00:00 -0000https://gcollazo.com/bolsas-llenas-de-funcionesDebe ser imposible representar estados ilegaleshttps://gcollazo.com/debe-ser-imposible-representar-estados-ilegales<p> Este es el tercer post en esta serie sobre arquitectura de software. En los posts anteriores he hablado de mi experiencia aprendiendo sobre este tema y sobre algunos de los problemas que he enfrentado en situaciones donde no hay una arquitectura clara. </p> <p> En este post espero empezar a introducir algunas ideas y técnicas concretas que he usado para comenzar a resolver mis problemas. </p> <h2>Estados ilegales</h2> <p> Cuando digo <em>“que sea imposible representar estados ilegales”</em>, lo que quiero decir es que nuestra meta debería ser que nuestro programa nunca esté en un estado inválido. O sea que sea imposible hacer algo que llevará a nuestro programa a estar en un estado inesperado o no definido. </p> <p> Eso significa que si nuestro programa es <em>un carrito de compras</em> y tenemos una regla de negocio que permite un máximo de tres artículos en carrito, debería ser imposible añadir un cuarto artículo al carrito. </p> <p> En el pasado he atentido este tipo de reglas de negocio, poniendo validaciones en el perímetro de mi programa. Usualmentemeste trabajo en aplicaciones web, así que esto signfica, validación en un controller o algo directamente relacionado al <em>endpoint</em> que recibe el <em>request</em> del usuario para añadir un artículo al carrito. No hay nada malo con esto, solo que si la petición de añadir un cuarto artículo al carrito llega de otra parte, nada evitará que suceda, poniendo nuestro programa en un estado inválido o ilegal. Este podría ser un efecto deseado en algunos casos pero en este ejemplo estamos supiniendo que no queremos que esto pase nunca. </p> <p> Este ejemplo me ayuda a ilustrar lo que queremos alcanzar, pero este post no cubrirá todo lo necesario para lograrlo. Me voy a enfocar en la pieza mas pequeña de un sistema que no permite representar estados ilegales. </p> <h2>Value Objects</h2> <p> No voy a ponerme a definir términos así que puedes mirar <a href="proxy.php?url=https://en.wikipedia.org/wiki/Value_object">aquí</a> y <a href="proxy.php?url=https://www.martinfowler.com/bliki/ValueObject.html">aquí</a> para buenas definiciones. De lo que si voy a hablar es sobre como uso VOs dentro de mis programas para aplicar reglas de negocio. </p> <p> Lo primero es decidir cuando usar un VO. Mi respuesta es que todo valor dentro de tu programa por defecto, debería ser un VO. La ventaja de este acercamiento es que te da la oportunidad de establecer reglas para cada valor individualmente. El ejemplo obvio de un VO es una cantidad de dinero (<code>Amount</code>). </p> <p> Si tenemos un <em>endpoint</em> en un web app que recibe una cantidad de dinero probablemente tendremos un <em>payload</em> JSON como este. </p> <pre><code>{ "amount": 100 }</code></pre> <p> Este objeto muestra el <code>amount</code> con un valor de <code>100</code> centavos o $1.00. Cómo nuestro programa puede manejar varios tipos de moneda, rápidamente nos damos cuenta que necesitamos saber la moneda de esa cantidad así que añadimos ese campo. </p> <pre><code>{ "amount": 100, "currency": "USD" }</code></pre> <p> Ok, ahora si tenemos todo lo que necesitamos. Lo próximo que hacemos es validar el <em>input</em> en nuestra aplicación así que podemos escribir el siguiente código. </p> <pre><code>app.post(async (req, res) =&gt; { let {amount, currency} = req.body; if (typeof amount !== "number") { return res.send(422); } if (amount &lt; 0 || amount &gt; 1000000) { return res.send(422); } if (typeof currency !== "string") { return res.send(422); } if (!["USD", "EUR"].includes(currency)) { return res.send(422); } try { let response = await controller(amount, currency); return res.json(response); } catch (error) { // do some error handling return res.send(400); } });</code></pre> <pre><code>async function controller(amount, currency) { // do stuff with amount and currency }</code></pre> <p> Obviamente esto se puede hacer de forma mucho mas declarativa usando algo como <a href="proxy.php?url=https://json-schema.org">JSON Schema</a> o cualquier otra librería de validación de objetos. </p> <p> Pero hacer ese tipo de validación no nos ayuda a recordar que ya hicimos esa validación. Las funciones que reciban esto valores no tienen forma de saber que ya fueron validados y por lo menos en mi caso esto crea un poco de duda. ¿Que tal si no valido el <em>input</em> en esta funcion y alguien pasa valores sin verificar? Esto puede producir <em>bugs</em> e incluso problemas de seguridad. Este problema se puede resolver con VOs. </p> <p> De este punto en adelante voy a usar <a href="proxy.php?url=https://www.typescriptlang.org">TypeScript</a> para los ejemplos. El sistema de <em>types</em> de TS nos va a ayudar a garantizar el contenido de nuestro VOs. Primero vamos a definir este VO Amount. </p> <pre><code>class Amount { readonly value: number; readonly currency: string; constructor(value: number, currency: string) { this.value = value; this.currency = currency; } }</code></pre> <p> Este es el primer paso. Si te das cuenta definí una clase con ambos valores. Esto es una decisión de diseño importante. Como sabemos estos dos valores están completamente conectados. No se puede cambiar uno, sin que el otro cambie. $1 USD y 1€ EUR no son lo mismo. Así que estos dos valores en realidad son uno solo. Esto es lo primero que añade este VO a nuestro programa. Nos está dejando representar dos datos como un solo objeto conectándolos para siempre y ayudándonos a recordar que no podemos cambiar uno sin pensar como se afecta el otro. Sigamos. </p> <p> Ahora el problema que veo es que si creo una instancia de esta clase no hay nada impidiendo que ponga valores que no puedo aceptar en mi programa. Por ejemplo si estamos haciendo un carrito de compras, ningún producto puede tener como precio un número negativo pero este VO nos permite pasar un número negativo. Como esta, hay otras reglas de negocio, que podemos implementar en este objeto para asegurarnos de que siempre que lo usemos, estas reglas sean aplicadas y si alguien o algo trata de romper estas reglas nuestro programa no lo permita. </p> <p> A mi me gusta poner estas reglas en el <code>constructor</code> y si alguna de estas reglas no se cumple, lo que hago es <code>throw</code> una excepción para impedir la creación de la instancia. Esto hace imposible que exista una instancia que no cumpla con las reglas que establecimos. </p> <pre><code>class Amount { readonly value: number; readonly currency: string; constructor(value: number, currency: string) { // Check value if (typeof value !== "number") { throw new Error("Amount value must be a number"); } if (value &lt; 1 || value &gt; 1000000) { throw new Error("Amount value must be between 1 and 1,000,000"); } // Check currency if (typeof currency !== "string") { throw new Error("Amount currency must be a string"); } if (currency.length !== 3) { throw new Error("Amount currency must be a valid ISO-4217 alpha code") } if (!["USD", "EUR"].includes(currency)) { throw new Error("Amount currency can only be USD or EUR"); } this.value = value; this.currency = currency; } }</code></pre> <p> Ok, ahora este VO es bastante más estricto, verifica los tipos de los argumentos que recibe y también se asegura que este objeto sigue nuestras reglas de negocio. Como hace <code>throw</code> de una excepción si algo está mal, no es posible crear una instancia en un estado ilegal. </p> <p> Por otro lado, si tenemos una instancia de este objeto, sabemos que tiene que ser válida porque logró pasar por todas las pruebas (guards) que establecimos. Así que una vez tenemos una instancia de este objeto podemos estar seguros que es válida y podemos confiar en lo que contiene. No tenemos que volver a verificar nada porque sabemos todo. </p> <p> Ahora donde quiera que necesitemos este valor en vez de pasar un <code>number</code> y un <code>string</code> podemos pasar un <code>Amount</code> y estar confiados. </p> <p> Se que esto puede parece increíblemente obvio para algunos, especialmente los que están acostumbrados a trabajar en lenguajes <em>strongly typed</em> pero les puedo asegurar que para los que llevamos toda la vida en lenguajes <em>dynamically typed</em> esto es una revelación. </p> <p> Que más podemos hacer con un VO. Pues habrás notado declare los fields de <code>Amount</code> como <code>readonly</code>. Una de las caracteristicas más importantes de un VO es que sea <em>inmutable</em> y que una vez creado, no se pueda cambiar. Esto nos da muchas garantías sobre el estado interno de nuestro objeto. </p> <p> Una acción que seguro vamos a necesitar hacer con nuestro <code>Amount</code> es sumarle otro <code>Amount</code> pero como dije antes no podemos modificar un VO una vez creado. Así que lo que podemos hacer es crear un método que recibe el <code>Amount</code> que queremos sumar y crea un VO nuevo que contiene el resultado. </p> <pre><code>add(amount: Amount): Amount { if (amount.currency !== this.currency) { throw new Error("Amounts of different currency cannot be added"); } return new Amount(amount.value + this.value, this.currency); }</code></pre> <p> Nuevamente aquí podemos ver que estamos validando que ambos VOs son del mismo <code>currency</code> antes de sumarlos. Una vez sabemos que son del mismo <code>currency</code> creamos una nueva instancia de <code>Amount</code> con el resultado de la suma. </p> <p> Otra cosa importante es poder comparar dos VOs por su valor así que vamos a escribir un método para esto. </p> <pre><code>equals(amount: Amount): boolean { return amount.value === this.value &amp;&amp; amount.currency === this.currency; }</code></pre> <p> Fácil. Ahora podemos comparar dos VOs y saber si contienen el mismo valor. Es importante notar que estamos comparando tanto el <code>value</code> como el <code>currency</code> porque sabemos bien que un Euro y un Dólar no son lo mismo. </p> <h2>VOs en mi web app</h2> <p> Creo que esos son suficientes ejemplos simples. Ahora un último ejemplo de como se puede usar este VO en el contexto de nuestra aplicación web. </p> <pre><code>app.post(async (req, res) =&gt; { let {amount, currency} = req.body; try { let amount = new Amount(amount, currency); let response = await controller(amount); return res.json(response); } catch(error) { // do some error handling return res.send(400); } });</code></pre> <pre><code>async function controller(amount: Amount) { // do stuff with Amount }</code></pre> <p> Ahí está. Ahora <code>Amount</code> nos ayuda a validar el input que estamos recibiendo y luego de que tenemos una instancia sabemos que es un valor que no necesitamos validar nuevamente. El resto de nuestro programa, siempre y cuando reciba una instancia de <code>Amount</code>, puede trabajar con el valor de forma segura y sin preocupación. </p> <p> Además este VO nos ayuda a organizar el código y mantener las reglas de negocio cerca del los datos que las necesitan. La clase <code>Amount</code> tiene todo el código necesario para crear una instancia válida y las acciones (método) que podemos tomar con ese objeto. </p> <h2>Result</h2> <p> Como notaste, cada vez que queremos crear un VO, necesitamos hacer <code>new</code> y pasar los argumentos que hagan falta para crear la instancia. Como nuestro <code>constructor</code> puede <code>throw</code> siempre tenemos que asegurarnos de hacer <code>try/catch</code> para manejar cualquier error al momento de la instanciación. </p> <p> Aunque esto no es el fin del mundo, bien rápido se puede hacer algo tedioso. Dicho eso, hay formas de mejorar la experiencia al usar VOs. </p> <p> Todos mis VOs tienen un método estático llamado <code>create</code>. Este método me permite instanciar VOs sin temor a una excepción pero en vez de devolver una instancia de <code>Amount</code>, devulve una instancia de un objeto llamado <code>Result</code> que sirve para envolver el <code>Amount</code> que se creo o un <code>Error</code> si hubo problemas al crear el VO. </p> <p> Esta idea de usar un objecto para envolver el resultado es algo muy común en otros lengajes como <a href="proxy.php?url=https://doc.rust-lang.org/std/result/enum.Result.html">Rust</a> y nos permite hacer cosas interesantes. Los detalles sobre <code>Result</code> los voy a dejar para un post futuro donde también hablare sobre como uso <code>Maybe</code> en mis programas. </p> <h2>Resumen</h2> <ul> <li> Los VOs nos ayudan a implementar reglas de negocio para un valor dentro de nuestro programa. </li> <li>Los VOs deben ser inmutables.</li> <li>Debe ser imposible instanciar un VO en un estado ilegal.</li> <li> Una vez tenemos una instancia de VO podemos estar seguros que contiene un estado válido dentro de nuestro programa. </li> </ul>[email protected] (gcollazo)Sun, 16 May 2021 00:00:00 -0000https://gcollazo.com/debe-ser-imposible-representar-estados-ilegalesResult y Maybehttps://gcollazo.com/result-y-maybe<p> En el post anterior mencioné brevemente que mis <a href="proxy.php?url=https://gcollazo.com/debe-ser-imposible-representar-estados-ilegales/" >value objects</a > implementan un método estático <code>create</code> que utilizo para crear instancias sin temor a excepciones. Este método devuelve un objeto llamado <code>Result</code> que sirve para envolver el resultado de llamar al método <code>create</code>. </p> <p> En este post vamos a ver cómo funciona <code>Result</code> y al final vamos a hablar un poco de <code>Maybe</code> que es muy similar. </p> <h2>Create</h2> <p>Vamos directo al código, así implemento <code>create</code> en mis VOs.</p> <pre><code>// amount.ts static create(value: number, currency: string): Result&lt;Amount, Error&gt; { try { return ok(new Amount(value, currency)); } catch(error) { return err(error); } }</code></pre> <p>Esté método estático se puede llamar escribiendo:</p> <pre><code>Amount.create(value, currency)</code></pre> <p> La única diferencia es que en vez de devolver una instancia de <code>Amount</code> como haría: </p> <pre><code>new Amount(value, currency)</code></pre> <p> Este método devuelve un <code>Result</code> que podría ser un <code>Ok</code> o un <code>Err</code>. </p> <p> Veamos el contenido de <code>create</code> en detalle. En el <code>try</code> intento crear una instancia de <code>Amount</code>. Si no hay problema utilizo la función <code>ok</code> para crear una instancia de <code>Ok</code> que envuelva el <code>Amount</code> que acabo de crear. </p> <p> En el <code>catch</code> atrapo el error si es que el <code>constructor</code> de <code>Amount</code> lanza una excepción. Sabemos que las excepciones que lanza el <code>constructor</code> son de tipo <code>Error</code>. Si este es el caso uso la función <code>err</code> para crear una instancia de <code>Err</code> que envuelva nuestro error. </p> <p> Tanto <code>Ok</code> como <code>Err</code> son <code>Result</code>. Por lo que el tipo que devuelve este método estático siempre es <code>Result</code>. </p> <p> Ahora vamos a mirar <code>Result</code> en detalle para entender mejor. No te preocupes si en este punto no entiendes todo, he omitido algunos detalles que iré revelando según vamos avanzando. </p> <h2>Result</h2> <p> Un <code>Result</code> es un objeto que puede contener solo uno de dos tipos. <code>Ok</code> o <code>Err</code>. El primero lo usamos para envolver resultados positivos o sin problemas y el segundo lo usamos para envolver errores. </p> <p> La idea aquí es que en vez de lanzar una excepción cuando instanciamos un VO, devolvemos un valor que podemos inspeccionar e incluso pasar a otras partes del código sin preocupación de excepciones. </p> <p>Vamos a ver como se implementa <code>Result</code> en TypeScript.</p> <pre><code>// result.ts export type Result&lt;T, E&gt; = Ok&lt;T, E&gt; | Err&lt;T, E&gt;; export class Ok&lt;T, E&gt; { constructor(readonly value: T) { this.value = value; } isOk(): this is Ok&lt;T, E&gt; { return true; } isErr(): this is Err&lt;T, E&gt; { return !this.isOk(); } unwrap(): T { return this.value; } } export class Err&lt;T, E&gt; { constructor(readonly error: E) { this.error = error; } isOk(): this is Ok&lt;T, E&gt; { return false; } isErr(): this is Err&lt;T, E&gt; { return !this.isOk(); } unwrap(): T { throw new Error("Called `unwrap` on an Err"); } } // Utility functions export function ok&lt;T, E&gt;(value: T): Ok&lt;T, E&gt; { return new Ok(value); } export function err&lt;T, E&gt;(err: E): Err&lt;T, E&gt; { return new Err(err); }</code></pre> <p> Vamos a romper este archivo en pedazos para entender todo lo que está pasando. Empecemos con <code>Ok</code>. </p> <pre><code>class Ok&lt;T, E&gt; { constructor(readonly value: T) { this.value = value; } isOk(): this is Ok&lt;T, E&gt; { return true; } isErr(): this is Err&lt;T, E&gt; { return !this.isOk(); } unwrap(): T { return this.value; } }</code></pre> <p> Como ves el <code>constructor</code> recibe un valor y lo guarda en el campo <code>value</code> de la clase. Además tiene algunos métodos simples como <code>isOk</code> que siempre responde <code>true</code> porque todas las intancias de la clase <code>Ok</code> están ok. También tiene lo opuesto <code>isErr</code> que obviamente hace lo contrario que <code>isOk</code>. Estos métodos los usamos para verificar que contiene un <code>Result</code>. </p> <p> Finalmente tenemos el método <code>unwrap</code> que se encarga de sacar el valor de dentro de nuestro <code>Ok</code>. Este método lo usamos para sacar el valor que nos interesa de dentro de un <code>Result</code> que sabemos está <code>Ok</code>. </p> <p>Ahora veamos la clase <code>Err</code>.</p> <pre><code>class Err&lt;T, E&gt; { constructor(readonly error: E) { this.error = error; } isOk(): this is Ok&lt;T, E&gt; { return false; } isErr(): this is Err&lt;T, E&gt; { return !this.isOk(); } unwrap(): T { throw new Error("Called `unwrap` on an Err"); } }</code></pre> <p> <code>Err</code> es muy similar a <code>Ok</code>. En este caso el <code>constructor</code> recibe un error como único valor. Tenemos los mismos métodos que en <code>Ok</code> porque queremos que estas dos clases tengan la misma interface. </p> <p> Algo interesante es que el método <code>unwrap</code> de <code>Err</code> hace <code>throw</code> de una excepción si se llama. Esto es porque cuando tenemos un error no tenemos un valor que podamos sacar así que no hay otra cosa que podamos hacer. </p> <p> Para evitar caer en esta situación siempre tenemos que usar los métodos <code>isOk</code> o <code>isErr</code> para verificar que no vamos a tener problema cuándo llamemos <code>unwrap</code>. </p> <h3>Generics</h3> <p> Lamentablemente este objeto <code>Result</code> tienen que hacer uso de un <em>feature</em> avanzado de TS llamado <a href="proxy.php?url=https://www.typescriptlang.org/docs/handbook/2/generics.html" >generics</a > pero te puedo jurar que vale la pena entender que está pasando. </p> <p> Tanto <code>Ok</code> como <code>Err</code> tienen unos <code>&lt;T, E&gt;</code> al lado del nombre de la clase. Esto lo que hace es que le dice a TS que cuando instanciemos estas clases queremos poder decidir que <code>type</code> van a tener los valores que vamos a envolver más adelante. En resumen es una forma de reutilizar estas clases sin tener que saber los <em>types</em> de antemano. </p> <p>Aquí un ejemplo.</p> <pre><code>new Ok&lt;number, Error&gt;(1);</code></pre> <p> Acabo de crear una instacia de <code>Ok</code> que puede aceptar un <code>number</code> o un <code>Error</code> aunque la clase <code>Ok</code> no usa el tipo <code>Error</code>. Esto va hacer sentido más tarde. </p> <p>Otro ejemplo.</p> <pre><code>new Err&lt;number, Error&gt;(new Error('Something wrong'));</code></pre> <p> Aquí cree un <code>Err</code> y nuevamente pasé <code>number</code> y <code>Error</code> como <em>type variables</em> a la clase para que TS sepa los types que puede aceptar. </p> <h3>Result type alias</h3> <p> Si te fijas el objeto <code>Result</code> no es una clase, en realidad es un <em>type alias</em>. Aquí está la definición. </p> <pre><code>type Result&lt;T, E&gt; = Ok&lt;T, E&gt; | Err&lt;T, E&gt;;</code></pre> <p> Voy a tratar de traducir esto. <code>Result</code> de <code>T</code> y <code>E</code> es igual a <code>Ok</code> de <code>T</code> y <code>E</code> o <code>Err</code> de <code>T</code> y <code>E</code>. Por esto es que ambos <code>Ok</code> y <code>Err</code> aceptan dos <em>type variables</em> aunque cada uno usa solo uno. <code>Result</code> puede ser cualquiera de estos dos tipos. O sea, un <code>Ok</code> que envuelve un valor de tipo <code>T</code> o un <code>Err</code> que envuelve un error de tipo <code>E</code>. </p> <h3>Utility functions</h3> <p> Las últimas piezas de este rompecabezas son las funciones <code>ok</code> y <code>err</code> que están al final del archivo. Nota que son en minúscula a diferencia de las clases <code>Ok</code> y <code>Err</code> que usan pascal case. </p> <p> Estas funciones son con lo que interactuamos la mayoría de las veces. En el ejemplo del método <code>create</code> al principio de este post se puede ver como las usamos. </p> <h2>Usando Result</h2> <p>Regresemos al método <code>create</code> del principio.</p> <pre><code>static create(value: number, currency: string): Result&lt;Amount, Error&gt; { try { return ok(new Amount(value, currency)); } catch(error) { return err(error); } }</code></pre> <p> Cómo se puede ver aquí este método devulve un objeto <code>Result</code> de <code>Amount</code> y <code>Error</code>. En otras palabras este método puede devolver una instancia de <code>Ok</code> que contendrá un valor de tipo <code>Amount</code> o una instancia de <code>Err</code> que contendrá un error de tipo <code>Error</code>. </p> <p> Vuelve a leer esa dos oraciones anteriores. Creo que es importante llamar la atención sobre toda la información que estamos “codificando” dentro del <em>type system</em>. Estamos poniendo los <em>types</em> a ayudarnos a programar y detectar problemas en nuestro programa. </p> <p> Sobre las funciones <code>ok</code> y <code>err</code> no creo que tenga que decir mucho. Simplemente se encargan de crear una instancia de un objeto <code>Ok</code> o <code>Err</code>. Las usamos cuando estamos implementando algún método que queramos devuelva un <code>Result</code>. Siempre usamos <code>ok</code> cuando la operación tiene éxito y usamos <code>err</code> cuando algo falla. </p> <p> Veamos cómo se ve cuando usamos el método <code>create</code> ahora que entendemos mejor como se comporta <code>Result</code>. </p> <pre><code>let priceOrErr = Amount.create(1, "USD"); if (priceOrErr.isErr()) { console.error(priceOrError.error); return res.send(422); } let price = priceOrErr.unwrap();</code></pre> <p> En la primera linea <code>priceOrErr</code> es una instancia de <code>Result&lt;Amount, Error&gt;</code>, así que puede ser un <code>Ok</code> o un <code>Err</code> y no tenemos forma de saber. En la próxima linea hacemos el check con <code>isErr</code>, si es cierto sabemos que el contenido es un error. Pero si logramos pasar ese <em>check</em>, en las lineas subsiguientes sabemos que <code>priceOrErr</code> tiene que ser un <code>Ok</code> y podemos hacer <code>unwrap</code> sin tenemor a ninguna exepción. </p> <p> Lo bueno de esto es que no solo nosotros sabemos que en este punto <code>priceOrErr</code> es un <code>Ok</code>, también TS lo sabe y nos ayuda con los completions. Además como le dimos mucha información a TS, el <em>type system</em> sabe que es un <code>Ok</code> de <code>Amount</code>. Por lo que cuando hacemos <code>unrap</code> TS sabe que lo que devuelve es un <code>Amount</code>. </p> <p> De la misma forma dentro del <code>if</code> TS sabe que <code>priceOrErr</code> es una instancia de <code>Err</code> y que contiene un valor de tipo <code>Error</code>. Gracias TypeScript. </p> <h2>Result en todas partes</h2> <p> Espero que ya veas el valor que puede añadir usar <code>Result</code> en tu programa. </p> <p> El uso de <code>Result</code> no tiene que limitarse a la creación de VOs. Puedes usar un <code>Result</code> en cualquier parte de tu programa donde realizas una operación que puede fallar. O sea en muchas partes. </p> <p> Por ejemplo cuando hacemos queries a la base de datos, en vez de devolver la data directamente, envolvemos el resultado en un <code>Result</code>. De esta forma podemos verificar si está <code>Ok</code> antes de continuar. </p> <p> <code>Result</code> nos permite usar el <em>type system</em> para “codificar” información importante sobre el programa. </p> <h2>Combine</h2> <p> Además de las funciones <code>ok</code> y <code>err</code> uso una función adicional que me ayuda a verificar varios <code>Result</code> en un solo paso. A esta funcion le llamo <code>combine</code> porque combina todos los <code>Result</code> en un solo <code>Result</code> que me dice si todos los <code>Result</code> están <code>Ok</code>. </p> <p> <code>combine</code> recibe un <code>Array</code> de <code>Result</code>, itera por ellos y si alguno es un <code>Err</code> devuelve un <code>Err</code>, si todos son <code>Ok</code>, devuelve un <code>Ok</code>. </p> <pre><code>function combine( results: Result&lt;unknown, Error&gt;[] ): Result&lt;unknown, Error&gt; { for (let result of results) { if (result.isErr()) { return result; } } return ok(results); }</code></pre> <p>Así usamos <code>combine</code>.</p> <pre><code>let priceOrErr = Amount.create(100, "USD"); let discountOrErr = Amount.create(1, "USD"); let taxOrErr = Amount.create(1, "USD"); let validationOrErr = combine([ priceOrErr, discountOrErr, taxOrErr ]); if (validationOrErr.isErr()) { console.error(validationOrErr.error); return res.send(422); } let price = priceOrErr.unwrap(); let discount = discountOrErr.unwrap(); let tax = taxOrErr.unwrap();</code></pre> <p> Una vez pasamos el <code>if</code> sabemos que todos los valores son <code>Ok</code> y podemos <code>unwrap</code> sin problema. </p> <h2>Extendiendo Result</h2> <p> No hay ninguna regla que diga que <code>Result</code> no puede tener más métodos. Incluso si miramos la documentación de <a href="proxy.php?url=https://doc.rust-lang.org/std/result/enum.Result.html">Rust</a> para su tipo <code>Result</code> no damos cuenta que implementa algunos método interesantes como <code>unwrap_or_else</code>. </p> <p> Ese método lo podemos implementar en nuestro <code>Result</code> para poder sacar nuestro valor o un valor default si resulta que nuestro <code>Result</code> es un <code>Err</code>. </p> <pre><code>let defaultAmount = new Amount(0, "USD"); let priceOrErr = Amount.create(req.body.price, "USD"); let price = priceOrErr.unwrapOr(defaultAmount);</code></pre> <p> De esta forma si el <code>Result</code> de <code>priceOrErr</code> es <code>Err</code>, <code>price</code> será el valor de <code>defaultAmount</code>. </p> <p> Hay muchas cosas interesantes que se pueden hacer con <code>Result</code>, por ejemplo podemos añadir un método <code>map</code> que aplique una funcion al valor que contenien el <code>Result</code> y devuelva un nuevo <code>Result</code> con el valor modificado. Este <code>map</code> debe ser lo suficientemente inteligente como para no devolver un error si resulta que el <code>Result</code> lo que contiene es un <code>Err</code>. </p> <p> Con este método de <code>map</code> se pueden hacer muchas cosas interesantes, especialmente hacer cadenas de método que todos devueven un <code>Result</code> y todos esperan como <em>input</em> un <code>Result</code>. Pero de esto podemos hablar otro día. </p> <h2>Maybe</h2> <p> Finalmente hablaré brevemente del objeto <code>Maybe</code>. Este objeto es muy similar a <code>Result</code>. Usamos <code>Maybe</code> como valor para operaciones que pueden devolver o resultado o no. </p> <p> Por ejemplo imagina que tienes una funcion <code>notJuan</code> que recibe una <code>Array</code> de nombres de personas y devuelva un <code>Array</code> de nombres que no son Juan. Esta función podría devolver un <code>Array</code> de resultado pero también podría devovler un <code>Array</code> vació, o sea ningún resultado. </p> <p> No tengo nada en contra de los Juanes, simplemente este fue el mejor ejemplo que se me ocurrió. </p> <p> Esta función la podemos implementar usando <code>Maybe</code> como el <em>return type</em>. </p> <p> Un <code>Maybe</code> puede devolver un <code>Some</code> o un <code>None</code>. Devuelve <code>Some</code> cuando tenemos un valor que devolver y <code>None</code> cuando no tenemos un valor que devoler. Similar a <code>Result</code> tenemos funciones <code>some</code> y <code>none</code> para crear <code>Maybe</code> fácilmente. </p> <p>Así implementaríamos la función <code>notJuan</code>.</p> <pre><code>function notJuan(names: string[]): Maybe&lt;string&gt; { let validNames = names.filter((n) =&gt; n !== "juan"); if (validNames.length === 0) { return none(); } return some(validNames); } let friends = ["María", "Luis", "Juan"]; let filteredFriends = notJuan(friends); if (filteredFriends.isNone()) { return res.send(400); } let friendsExceptJuan = filteredFriends.unwrap();</code></pre> <p> En la última linea sabemos que <code>friendsExceptJuan</code> contiene algunos nombres y que ninguno es “Juan”. </p> <h2>Result y Maybe</h2> <p> En mis programas mas recientes he empezado a utilizar tanto <code>Result</code> como <code>Maybe</code>. Siento que me ayudan a expresar mejor la intención de los métodos en mi programa. </p> <p> Utilizo <code>Result</code> como <em>return value</em> para cualquier operación que puede devolver un error y utilizo <code>Maybe</code> para cualquier operación que puede devolver datos o no. </p> <p> En algunos casos me he visto usando ambos en cojunto por ejemplo puedes imaginar un método <code>findById</code> que busca en la base de datos un <em>record</em> usando el <em>id</em>. </p> <p> Esta al ser una operación de la base de datos puede fallar por muchas razones, podemos tener un error en la conexión o quizás la petición tarda mucho y falla. Para capturar esa información uso un <code>Result</code>. </p> <p> Si todo sale bien lo próximo que puede pasar es que no exista un objeto con ese <em>id</em> en la base de datos y para comunicar esto uso un <code>Maybe</code>. O sea que cuando hago una operación como <code>findById</code> eso devuelve algo como esto. </p> <pre><code>Result&lt;Maybe&lt;User&gt;, Error&gt;</code></pre> <p> Admito que puede ser complicado pero pienso que de esta forma estoy codificando lo más posible dentro del <em>type system</em>. No quiero decir que esta es <em>“the one true way”</em> pero por ahora nos funciona. </p> <p> Seguiremos experimentando con esto y seguirmos evolucionando pero por ahora estamos bastante satisfechos. </p> <h2>Referencias</h2> <p> Es increíble que hemos llegado hasta aquí sin usar la palabra <a href="proxy.php?url=https://en.wikipedia.org/wiki/Monad_(functional_programming)" >monad</a >. Aquí te dejo algunos enlaces que me han ayudado mucho. </p> <ul> <li> <a href="proxy.php?url=https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-class/" >Flexible Error Handling w/ the Result Class</a > </li> <li> <a href="proxy.php?url=https://dev.to/_gdelgado/type-safe-error-handling-in-typescript-1p4n?signin=true" >Type-Safe Error Handling In TypeScript</a > </li> <li> <a href="proxy.php?url=https://gist.github.com/s-panferov/5269524dcf23dad9a1ef" >Result type in TypeScript</a > </li> <li> <a href="proxy.php?url=https://codewithstyle.info/advanced-functional-programming-in-typescript-maybe-monad/" >Advanced functional programming in TypeScript: Maybe monad</a > </li> <li> <a href="proxy.php?url=https://github.com/patrickmichalina/typescript-monads" >TypeScript monads: Better TypeScript Control Flow</a > </li> <li> <a href="proxy.php?url=https://github.com/hqoss/monads" >Monads: Type safe Option, Result, and Either types</a > </li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=F9bznonKc64" >Get value out of your monad</a > </li> </ul> <h2>Próximo</h2> <p> En un próximo post lo que me interesa discutir es el rol de los VOs en la validación de request en un web app y cómo podemos complementar usando otros tipos de validación con librerías como JSON Schema. </p>[email protected] (gcollazo)Sat, 22 May 2021 00:00:00 -0000https://gcollazo.com/result-y-maybeLogging is importanthttps://gcollazo.com/logging-is-important<p> Getting application <a href="proxy.php?url=https://en.wikipedia.org/wiki/Log_file">logging</a> right is crucial for every software project. When done correctly it makes the maintainers work a lot simpler. There’s no worst feeling than getting an error report and searching for logs to try to understand what happened and not finding a single entry. This is especially bad with difficult to reproduce production errors. </p> <p> Like everything else, having good logs requires a little bit of thought and consistency. But is not that hard. </p> <p> I’m not an expert on this subject. This post contains my notes on what has worked for me in the past in the context of backend web applications. </p> <h2>Use a real logger</h2> <p> Every language has a very simple way to send strings to <code>stdout</code>. That’s not a “real” logger. </p> <p> In Node you have <code>console.log()</code> and in Python <code>print()</code>. These work great for simple scripts and print debugging. On a real production application, you want to use a “real” logger. </p> <p> For Node, I prefer using <a href="proxy.php?url=https://getpino.io/#/">pino</a>. It’s fast, has great support, and is very easy to use. </p> <h2>What to log</h2> <ul> <li> <strong>Request/Response</strong> – You must log every request and response. For requests, I like to include all headers, except the <code>Authorization</code> header. For responses, I always include the HTTP status code and the response time. Both request and response include a unique request ID to correlate them. </li> <li> <strong>App Errors</strong> – every <code>catch</code> in your code must include an error log describing what went wrong. Also whenever your app gets into a state that will result in an HTTP status code greater than 299 you should have some sort of application-level error log, describing why your request ended in an error. </li> <li> <strong>Security events</strong> – Always log when a login fails, or when a user tries to access a page or an object is not supposed to. Always log password changes, email changes or any other security credentials change. Make sure you don’t log the actual password or sensitive information. </li> <li> <strong>Business events</strong> – This is optional but it’s a good idea. If your app is a shopping cart, create a log entry every time you close a sale. The idea here is that you might want to create alerts based on these log events. If you usually see 100 sales per hour and suddenly see zero, it might be a good idea to take a look and understand what it’s going on. </li> </ul> <h2>A good log line</h2> <p> Having an unsearchable pile of logs is not useful. All my logs are in JSON format. This is very useful if you use a service that allows you to query JSON logs. I use AWS CloudWatch logs which is not perfect, but it does a decent job of helping me find what I’m looking for if I created the logs correctly. </p> <p>Every log line must have the following.</p> <ul> <li> <strong>Log level</strong> – Usually a number to indicate what kind of log it is. Info, error, and debug are the most common. </li> <li> <strong>Message</strong> – This is the most important part of the log entry. Try to make this explicit and easy to understand for your future self or your fellow team members. Try to make messages unique so they are easy to search on your codebase. Having multiple error logs with the same message makes things harder than they have to be. </li> <li> <strong>Group</strong> – All of my logs have a <code>group</code> field that use to indicate what part of the application is producing the log entry. Most of the time this group is used to indicate which specific file is creating the log entry. This facilitates searching. </li> <li> <strong>Context</strong> – Application error logs usually require some context that will help you determine what is the problem without logging all of your application state. It is very important to include the minimum amount of data possible on every log entry but enough to determine what is going on. For example if your input validation failed it might be a good idea to log what exactly went wrong. For example if you only allow names of a certain length, it might be a good idea to log the length of the submitted name that produced the error. </li> <li> <strong>Timestamp</strong> – Almost every logger adds a timestamp. Just make sure that the format used works for you and make sure you include the time zone or that your are very certain of what zone it’s being used. </li> </ul> <h2>Security concerns</h2> <p> If your system deals with <a href="proxy.php?url=https://en.wikipedia.org/wiki/Personal_data">PII</a> or <a href="proxy.php?url=https://en.wikipedia.org/wiki/Payment_Card_Industry_Data_Security_Standard" >CHD</a >, make sure you are not logging sensitive information. Pino and every decent logger have a feature to <a href="proxy.php?url=https://getpino.io/#/docs/redaction?id=redaction">redact</a> logs. </p> <p> Also, make sure that you periodically check your logs for sensitive data. This is something not obvious and I’ve seen it go wrong multiple times. </p> <p><strong>Don’t log sensitive or secret information</strong>.</p> <h2>Final thoughts</h2> <p> I know logs are boring but they are an essential part of every app. A lot of thought is put into many aspects of software development but I rarely find people writing or discussing this subject. You should put some thought on your logging practices. You future self will thank you. </p> <p>In summary:</p> <ul> <li> Having decent logs will greatly improve the maintenance of your software. </li> <li>Error logs are the most important kind of log.</li> <li>Always include context on error logs.</li> <li>Log every request and response and include status codes.</li> <li>Make sure you don’t log sensitive information.</li> </ul>[email protected] (gcollazo)Tue, 01 Jun 2021 00:00:00 -0000https://gcollazo.com/logging-is-importantEl Lenguaje de Programación Perfectohttps://gcollazo.com/el-lenguaje-de-programacion-perfecto<p> En mi trabajo escribo principalmente <a href="proxy.php?url=https://www.typescriptlang.org/">TypeScript</a> sobre <a href="proxy.php?url=https://nodejs.org/en/">Node.js</a> y aunque no es mi lenguaje favorito, no me molesta, tiene muchas cosas buenas. Para el tipo de proyecto que trabajo, el tener un “<a href="proxy.php?url=https://en.m.wikipedia.org/wiki/Type_system" >type system</a >” es altamente valioso. </p> <p> También escribo bastante <a href="proxy.php?url=https://www.python.org/">Python</a> (de mis lenguajes favoritos) para “scripts” locales y otras cosas que no necesariamente terminan en un ambiente de producción. Algún día escribiré un poco más sobre porque terminamos corriendo en Node.js y <a href="proxy.php?url=https://en.m.wikipedia.org/wiki/MongoDB">MongoDB</a>. </p> <p> No creo que exista el lenguaje de programación perfecto, perdón por el “clickbait”, pero para mi el lenguaje ideal es una mezcla entre <a href="proxy.php?url=https://www.python.org/">Python</a>, <a href="proxy.php?url=https://www.typescriptlang.org/">TypeScript</a>, <a href="proxy.php?url=https://go.dev/">Go</a> y <a href="proxy.php?url=https://www.rust-lang.org/">Rust</a>. </p> <p> En este post enumero los <em>ingredientes</em> que me gustaría tomar prestados para un lenguaje nuevo imaginario. Seguramente este lenguaje jamás existirá y posiblemente eso sea lo mejor. Si hay algo que NO soy, es diseñador de lenguajes de programación. </p> <h2>Python</h2> <p> De Python me encanta que es fácil de leer y escribir y que tiene un “<a href="proxy.php?url=https://docs.python.org/3/library/index.html" >standard library</a >” bastante completo. Cuando quiero resolver un problema que se puede resolver corriendo un pequeño “script” local, Python siempre me da los mejores resultados. Además hay muchas librerías para manejar datos y visualizaciones que son fáciles y útiles. </p> <p> En cuanto “web apps” que es lo mas que hago, creo que <a href="proxy.php?url=https://www.djangoproject.com/">Django</a> es de lo mejor que he usado. Todo lo importante está incluido y lo que falta es fácil de añadir. Me gustaría que fuera un poco más flexible en cuanto a como organizar los proyectos para hacer mas fácil implementar una <a href="proxy.php?url=https://en.m.wikipedia.org/wiki/Hexagonal_architecture_(software)" >arquitectura hexagonal</a > pero al final del día no tengo quejas mayores. </p> <ul> <li>Fácil de leer y escribir.</li> <li>El “standard library” es bastante completo.</li> <li>Hay muchas librerías para análisis y visualización de datos.</li> <li>Me encanta <a href="proxy.php?url=https://jupyter.org/">Jupyter Notebook</a>.</li> <li>Django es mi web framework favorito.</li> </ul> <h2>TypeScript y JavaScript</h2> <p> De TypeScript me gusta mucho el “<a href="proxy.php?url=https://www.typescriptlang.org/docs/" >type system</a >” y principalmente la idea de que puedes añadir los tipos poco a poco en un “codebase” viejo. Se que <a href="proxy.php?url=https://docs.python.org/3/library/typing.html#module-typing" >Python tiene su propio “type system”</a > pero de lo que he visto el de TypeScript es mas poderoso y no es mucho más difícil de aprender. </p> <p> Otro feature de TypeScript y JavaScript que me gusta mucho es la sintaxis para los “<a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions" >arrow functions</a >” <code>(args) =&gt; body</code>. Además de facilitar el crear funciones anónimas creo que hace que el código sea más fácil de leer, especialmente cuando se usa <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map" >map</a > y <a href="proxy.php?url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter" >filter</a >. </p> <p> Python tiene los <a href="proxy.php?url=https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions" >list y dictionary comprehensions</a > que me gustan bastante, pero creo que la sintaxis de los “arrow functions” es un poco mejor. Tambien en Python hay <a href="proxy.php?url=https://docs.python.org/3/tutorial/controlflow.html?#lambda-expressions" >lambdas</a > para definir funciones anónimas pero nunca recuerdo como se escriben. Sin duda los “arrow functions” son mejor. </p> <ul> <li>El “type system” es buenísimo y opcional.</li> <li> Me parece que el “type system” es mas poderoso que lo que ofrece Python en versiones mas recientes. </li> <li>Es fácil de aprender a sacarle provecho a los “types”.</li> <li> Los “arrow functions” en JavaScript me gustan más que los list y dictionary comprehensions de Python. </li> <li>Hacer funciones anónimas es mucho más natural que en Python.</li> </ul> <h2>Go</h2> <p> De Go lo más que me gusta es el modelo de concurrencia. Los goroutines hacen que sea fácil escribir código concurrente sin errores, algo que es un poco más complicado en Node y ni hablar de Python. </p> <p> Otra cosa que me parece buenísima es la posibilidad de generar binarios “statically linked” para Windows, Linux y Mac. Un solo archivo binario con todas sus dependencias hace posible crear contenedores de Docker pequeños usando <code>FROM scratch</code>. </p> <p> El tema de “performance” aunque no es una de las cosas que más me emocionan de un lenguaje, no se debe ignorar. Go al ser un lenguaje compilado produce programas que ejecutan mucho más rápido que programas escritos para Node o Python. Sin duda esto es algo positivo especialmente cuando estás haciendo “hosting” en la nube. </p> <ul> <li> El modelo de concurrencia facilita escribir código concurrente con menos errores. </li> <li> Compilar a un solo binario “statically linked” facilita “deploys” y el manejo de dependencias. </li> <li> Poder hacer un contenedor <code>FROM scratch</code> permite tener imágenes pequeñas y con la menor superficie de ataque posible. </li> <li>Mejor “performance” cuando se compara con Python o Node.</li> </ul> <h2>Rust</h2> <p> De Rust me gustan muchas cosas pero el enfoque del lenguaje en producir programas que sean fácil de analizar estáticamente, ha empujado ese lenguaje a tomar decisiones conservadoras sobre como manejar los datos. </p> <p> El que todo sea <a href="proxy.php?url=https://en.m.wikipedia.org/wiki/Immutable_object">inmutable</a> por “default” sin duda es bueno para facilitar el análisis estático del programa y ayuda al programador a no tener sorpresas. </p> <p> Los tipos <a href="proxy.php?url=https://doc.rust-lang.org/std/option/">Option</a> y <a href="proxy.php?url=https://doc.rust-lang.org/std/result/enum.Result.html" target="_blank" rel="noreferrer noopener" >Result</a > son otra cosa que me encanta de Rust. Estos dos tipos se encargan de manejar errores y funciones que pueden no devolver el valor que esperas. Esto te obliga a ser extremadamente explicito manejando errores y los casos donde el programa puede fallar. Nuevamente esto ayuda al programador a no tener sorpresas. </p> <ul> <li>Toda la data es inmutable por “default”.</li> <li> Al igual que Go los errores son valores que hay que atender de forma explícita. </li> <li> Los tipos <code>Option</code> y <code>Result</code> son enormemente útiles y me gustan mas que el <code>if err != nil {}</code> que se ve en todas partes en Go. </li> <li> Los “features” de “<a href="proxy.php?url=https://doc.rust-lang.org/rust-by-example/flow_control/match.html" >pattern matching</a >” en combinación con <code>Option</code> y <code>Result</code> son una maravilla. </li> </ul> <h2>Conclusión</h2> <p> Ahí está, mi lenguaje de programación ideal es una mezcla de algunas ideas y “features” de estos cuatro lenguajes. ¿Te gustaría un lenguaje así? </p>[email protected] (gcollazo)Mon, 13 Dec 2021 00:00:00 -0000https://gcollazo.com/el-lenguaje-de-programacion-perfectoDespués de Gasolina Móvilhttps://gcollazo.com/despues-de-gasolina-movil<p> Hace mas de siete años y sin avisar me llamó Arnaldo para contarme de un invento que tenía. La idea era simple, vamos a hacer un app para que la gente pueda pagar la gasolina. </p> <p> Inmediatamente me pareció una buena idea. Estaba seguro que los consumidores les encantaría poder pagar en la bomba. Ahí, en esa misma llamada acordamos hacer el app y ver qué pasaba. </p> <h2>Desde cero</h2> <p> Dos o tres semanas más tarde nos encontramos en una estación de gasolina en Caguas. El dueño de la estación nos permitió probar el app ahí en un ambiente real. En este punto Arnaldo tenía casi todo el software de su lado terminado y yo tenía un app que parecía que debería funcionar. Lanzamos la primera transacción pero falló porque yo cometí un error configurando la bomba de gasolina. En el segundo intento la transacción llegó a la estación pero la bomba no encendió. Luego de tres o cuatro intentos más, la bomba encendió y logramos servir gasolina. ¡Funciona! </p> <p> Desde ese día hemos estado pensando y trabajando sobre esta idea. Han pasado muchas cosas desde entonces, en su mayoría buenas y otras menos buenas pero hemos logrado seguir adelante. Hoy nuestros apps funcionan en la mayoría de las estaciones de servicio en Puerto Rico y el negocio sigue creciendo. </p> <p> Tenemos clientes en República Dominicana, Colombia, Islas Vírgenes Americanas, Bahamas y Estados Unidos. Ahora el enfoque es Centro América así que muy pronto habrá noticias sobre este esfuerzo. </p> <h2>Siempre aprendiendo</h2> <p> Trabajando en este proyecto he tenido muchas oportunidades de aprender. Aprendí a hacer servicio al cliente a gran escala, a contratar personal técnico y personal no técnico, sobre ciberseguridad y sobre cómo manejar un equipo remoto de forma efectiva mucho antes que eso fuera la norma. </p> <p> En los últimos años me he enfocado en tres areas: arquitectura de software, ciberseguridad y procesos de desarrollo de software. </p> <p> Mucho de mi aprendizaje ha sido producto del éxito súbito e inesperado de nuestro negocio. Como suele pasar el “demo” se volvió el producto y había que moverse rápido. La velocidad produce deuda técnica que si no se atiende con prontitud se puede volver un problema. </p> <p> Hace un tiempo nos dimos cuenta que nos estábamos moviendo un poco más lento de lo usual. La deuda técnica estaba empezando a ser un tema importante. Desde entonces hemos estado poco a poco “refactorizando” las areas donde hay más deuda y empezando a usar nuevas arquitecturas para evitar problemas en el futuro. Todos en el equipo hemos aprendido mucho, el software está en mucho mejor forma que cuando empezamos y tenemos una ruta clara para seguir mejorando. </p> <p> Sobre el tema de ciberseguridad es de lo menos que sabía y a lo mas que le he dedicado tiempo. Desde antes de entrar en este negocio me interesaba la ciberseguridad pero nunca fue mi enfoque. En este negocio la ciberseguridad es posiblemente el tema más importante. Por suerte lo teníamos claro desde el principio y le hemos dedicado el tiempo y los recursos necesarios. Ha costado mucho dinero y ha tomado mucho tiempo pero lo hemos hecho bastante bien. </p> <p> Pasar por el proceso de auditoría de <a href="proxy.php?url=https://en.m.wikipedia.org/wiki/Payment_Card_Industry_Data_Security_Standard" >PCI</a > por primera vez fue casi traumático pero ya en este punto me parece lo mas normal del mundo. Además, desde el principio y sin saber, muchas de nuestras políticas y medidas de seguridad superaban por mucho los requisitos de PCI. Lo que más trabajo nos dio fue aprender a documentar cómo esperan los auditores, para poder demostrar lo que en gran medida ya estábamos haciendo correctamente. </p> <p> Estos años han sido una escuela y me siento en un gran momento profesional. Tengo mucho conocimiento y muchas experiencias del mundo real que son increíblemente valiosas. Le he sacado el jugo a esta experiencia y estoy muy complacido. </p> <h2>De cero otra vez</h2> <p> <strong >Hace mas o menos un año tomé la decisión de dejar mi posición en Alias Payments (Gasolina Móvil) y moverme a hacer algo nuevo.</strong > </p> <p> En Alias Payments tomamos una simple idea y la llevamos a ser un gran negocio. En este punto ya siento que he logrado todo lo que me propuse y que es momento de llevar lo aprendido a otros lugares. </p> <p>Empezar de cero otra vez.</p> <p> En el momento que escribo esto, todavía no tengo claro que voy a hacer pero hay tres areas generales que me interesan y estoy explorando: </p> <ul> <li> Empezar una compañía de software para negocios (B2B) con un modelo “software as a service” en algo relacionado a ciberseguridad. </li> <li> Armar un equipo de ingenieros especializados en “secure coding practices” para hacer consultoría a empresas y startups. El enfoque sería hacer auditorias de seguridad y ayudar a establecer políticas de “secure coding”. </li> <li> Explorar la idea de hacer consultoría a nivel ejecutivo a tiempo parcial con varias compañías a la vez, comúnmente conocido como “fractional CTO / CISO”. </li> </ul> <p> Para ser honesto no tengo una idea favorita y no sé si terminaré haciendo alguna cosa totalmente diferente. Desde hoy empezaré a hablar con mis contactos y a explorar diferentes ideas para poder ir acercándome a una decisión. </p> <p> Estoy claro que lo que sea que escoja hacer será un proyecto de tres o cuatro años mínimo así que no quiero apresurarme. Dicho eso, antes de que acabe el año espero saber y compartir a qué me voy a dedicar. </p> <h2>Beluga es un “hobby”</h2> <p> Llevo unos meses trabajando en un app para iOS que se llama Beluga. Este app es “un Twitter” que funciona sin necesidad de centralizar todos los datos en los servidores de una sola compañía. Es un Twitter descentralizado y antes de que me pregunten, no usa blockchain. </p> <p> Mi meta es recrear “el Twitter del principio”, donde había mucha menos gente y las interacciones eran mas saludables. Había un momento cuando la gente en Twitter no estaba buscando de qué quejarse o de cómo montarsela a alguien que dijo una tontería. Tampoco había anuncios en cada tres tweets y no había un feed algorítmico tratando de robar toda tu atención para vender anuncios. </p> <p> Otro de los objetivos de este proyecto es diseñar un protocolo que otros apps puedan adoptar y no quedarme en construir un app que solo funciona si una empresa tiene el control total. </p> <p> Beluga no es mi próximo negocio o por lo menos dudo que lo sea. Por diseño, este proyecto hace bastante difícil que una sola entidad pueda “adueñarse” de los datos de los usuarios dificultando la mayoría de los modelos de negocio tradicionales para este tipo de apps. </p> <p> Beluga es un “hobby” y dudo que se vuelva un negocio. Si se vuelve un negocio sería un grata sorpresa pero lo dudo. Pronto compartiré mas información sobre Beluga y anunciaré cuando lo publique. </p> <h2>Agradecimiento</h2> <p> No quiero terminar este post sin darle las gracias a todas las personas que han trabajado y que trabajan en Alias Payments y Artisoft Labs por lo mucho que me han enseñado, lo profesionales que son y sus buenos deseos. También quiero darle gracias a José Padilla quien fue uno de nuestros socios originales cuando arrancamos el negocio. Gracias a todos. </p> <p> Finalmente, quiero agradecer a mi amigo desde por lo menos 1999 y socio en este negocio Arnaldo Rivera. Sin su iniciativa, conocimiento y su ética de trabajo sería imposible haber llegado donde hemos llegado. Durante todos estos años que hemos trabajado juntos hemos tenido sorprendentemente pocas diferencias y cuando han surgido siempre se han resuelto rápidamente y con mucho respeto. Hemos trabajado mucho y la hemos pasado muy bien. Ha sido genuinamente una gran experiencia en gran medida por el liderato de Arnaldo. Gracias. </p> <h2>Próximo</h2> <p> Tan pronto sepa lo que voy a hacer estoy seguro que lo comunicaré por aquí. Por ahora voy a explorar las alternativas y escucharé todas las propuestas que reciba. </p> <p> Estoy increíblemente entusiasmado por empezar algo nuevo y sentir nuevamente la adrenalina de esos primeros días. </p> <p> Déjame tus comentarios escribiendo a <a href="proxy.php?url=mailto:[email protected]">[email protected]</a> </p>[email protected] (gcollazo)Mon, 18 Jul 2022 00:00:00 -0000https://gcollazo.com/despues-de-gasolina-movilFavorite Tiny Desk Concertshttps://gcollazo.com/favorite-tiny-desk-concerts<p>Ordered alphabetically:</p> <ul> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=XfzpYcwiUrA">Adele</a></li> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=O70s9aV_9_Q">Café Tacvba</a></li> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=j82L3pLjb_0">Coldplay</a></li> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=SW6L_lTrIFg">C. Tangana</a></li> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=F4neLJQC1_E">Dua Lipa</a></li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=wTqCthvtL8k">Hermanos Gutiérrez</a> </li> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=rZjRU5ElGrk">iLe</a></li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=wSl5_RDCfrQ">Jessie Reyez</a> </li> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=ze4xcmBFvaE">Jon Batiste</a></li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=aWjihS2LHLs" >Juanes &amp; Mon Laferte</a > </li> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=DFiLdByWIDY">Lizzo</a></li> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=QrR_gm6RqCo">Mac Miller</a></li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=GYJ03MIPoIk" >Megan Thee Stallion</a > </li> <li><a href="proxy.php?url=https://www.youtube.com/watch?v=Dy4pEFFbFsA">Mon Laferte</a></li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=JGL-eQAAxGs">Monsieur Periné</a> </li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=JODaYjDyjyQ">Natalia Lafourcade</a> </li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=KGczofguB0c">Olivia Rodrigo</a> </li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=ysdjtyV_DuY">The Lumineers</a> </li> </ul>[email protected] (gcollazo)Thu, 18 May 2023 00:00:00 -0000https://gcollazo.com/favorite-tiny-desk-concertsRunning a Bug Bounty Program Without Spending a Fortunehttps://gcollazo.com/running-a-bug-bounty-program-without-spending-a-fortune<p> Bug bounty programs have emerged as a crucial cybersecurity measure, allowing organizations to harness the skills of the security community in identifying and resolving vulnerabilities before malicious actors can exploit them. While many opt for popular bug bounty platforms, these can come with hefty price tags. In this blog post, we’ll explore the steps to establish a successful and cost-effective bug bounty initiative without relying on expensive vendors. </p> <h4>Clearly Define Objectives and Scope</h4> <p> Before starting your bug bounty program, set clear objectives and scope. Determine which assets and systems are in-scope for testing and be transparent about your expectations from participants. Outline the types of vulnerabilities you’re interested in and specify any restrictions or rules during testing. </p> <h4>Prepare Legal and Ethical Guidelines</h4> <p> Craft comprehensive legal agreements and ethical guidelines that participants must follow throughout the bug bounty program. These documents should outline the rules, the reward structure, and the responsibilities of both your organization and the researchers. Consult with legal professionals to ensure compliance with local regulations and to safeguard all parties’ interests. </p> <h4>Build a Secure Platform for Submission</h4> <p> Create a secure platform where researchers can submit their findings and communicate with your security team. There are many alternatives here but keeping it simple for researchers while deterring spammers is the holly grail. I have tried a bunch of solutions and to be honest the best so far is a well crafted Google form. </p> <h4>Respond to Reports</h4> <p> Having an internal security team to manage and assess submissions is vital for the success of your program. They should be skilled in cybersecurity, capable of verifying reported vulnerabilities, and assigning appropriate rewards. Utilizing your existing team can be more cost-effective than relying on external vendors. </p> <h4>Set Reasonable Rewards</h4> <p> Offering competitive rewards is essential for attracting talented researchers to your program. Conduct market research to gauge average payouts for various types of vulnerabilities and set rewards accordingly. Remember that recognition and swag, in addition to monetary rewards, can also be appealing incentives for researchers. </p> <h4>Foster a Positive Community Culture</h4> <p> Promote a positive and respectful culture within your bug bounty community. Promptly acknowledge submissions, maintain open lines of communication, and express gratitude for researchers’ efforts. Treat participants as valuable allies in securing your systems, not adversaries. </p> <h4>Utilize Cost-Effective Promotion Strategies</h4> <p> Without the need for popular vendors’ expensive platforms, focus on cost-effective promotion strategies. Utilize your company’s website, blog, and social media to spread the word about your bug bounty program. Engage with the cybersecurity community through forums and mailing lists to raise awareness effectively. </p> <h4>Continuous Improvement</h4> <p> Regularly review and refine your bug bounty program based on feedback from researchers and your internal security team. Continuous improvement is essential to keep the program effective and relevant in the ever-changing cybersecurity landscape, and it doesn’t require additional costs associated with external platforms. </p> <h4>You Can Do It</h4> <p> Running a cost-effective bug bounty program without relying on expensive vendors demands thoughtful planning, commitment, and investment in your internal resources. By clearly defining objectives, setting up a secure submission platform, and fostering a positive community culture, your organization can leverage the security community’s expertise to identify and resolve vulnerabilities without breaking the bank. Remember, the success of your bug bounty program lies in the trust you build with researchers and the dedication to making your digital assets more secure in a financially responsible manner. </p> <p> If you’re looking for a user-friendly and cost-effective solution to create and run your bug bounty program, consider using <a href="proxy.php?url=https://blimp.io">Blimp</a>. We offer a budget-friendly alternative to popular bug bounty platforms, enabling you to efficiently manage submissions, engage with the security community, and continuously improve the security of your systems while keeping costs in check. </p>[email protected] (gcollazo)Wed, 19 Jul 2023 00:00:00 -0000https://gcollazo.com/running-a-bug-bounty-program-without-spending-a-fortuneOptimal SQLite settings for Djangohttps://gcollazo.com/optimal-sqlite-settings-for-django<p> Requires: <strong ><a href="proxy.php?url=https://docs.djangoproject.com/en/5.1/ref/databases/#setting-pragma-options" >Django &gt;= 5.1</a ></strong > </p> <hr /> <p> There’s plenty of information out there on how to scale Django to handle numerous requests per second, but most of it doesn’t really apply to the kind of software I work on. </p> <p> For the past few years, I’ve been developing custom software for smaller organizations. All my deployments have fewer than 100 users and operate on a single AWS instance. Initially, I chose PostgreSQL because I know it well and Django’s documentation suggests that SQLite isn’t suitable for production. </p> <blockquote> <p> SQLite provides an excellent <em>development</em> alternative for applications that are predominantly read-only or require a smaller installation footprint. </p> <cite ><a href="proxy.php?url=https://docs.djangoproject.com/en/5.0/ref/databases/#sqlite-notes" >Django Documentation</a ></cite > </blockquote> <p> However, my previous experience with SQLite has taught me that it’s much more capable than what the Django docs suggest, offering better performance and simpler administration. So, I decided to migrate. To achieve the best possible performance, I had to configure Django and SQLite correctly. </p> <p> After extensive research, testing, and trial and error, here are my recommended settings for Django web apps using SQLite as the primary database. I anticipate these settings will evolve over time, and I will update them accordingly. My goal is to have these settings ready to copy and paste into every new Django project, ensuring they are the most current and optimal for most web apps. </p> <pre><code>DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", "OPTIONS": { "init_command": ( "PRAGMA foreign_keys=ON;" "PRAGMA journal_mode = WAL;" "PRAGMA synchronous = NORMAL;" "PRAGMA busy_timeout = 5000;" "PRAGMA temp_store = MEMORY;" "PRAGMA mmap_size = 134217728;" "PRAGMA journal_size_limit = 67108864;" "PRAGMA cache_size = 2000;" ), "transaction_mode": "IMMEDIATE", }, }, }</code></pre> <p> <em >If you’re looking for help with custom web apps or need cybersecurity consulting, feel free to <a href="proxy.php?url=https://blimp.io">reach out</a>. I’m available for consulting work.</em > </p> <p> The rest of this post contains information about each setting, my suggestion for a simple backup strategy and a lot of reference links. </p> <hr /> <h3>PRAGMA foreign_keys = ON</h3> <p> By default, SQLite does not enforce foreign key constraints. To enable foreign key enforcement, you need to activate it manually. </p> <p><a href="proxy.php?url=https://www.sqlite.org/foreignkeys.html">Read more</a></p> <h3>PRAGMA journal_mode = WAL</h3> <p> The WAL journaling mode uses a write-ahead log instead of a rollback journal to implement transactions. The WAL journaling mode is persistent; after being set it stays in effect across multiple database connections and after closing and reopening the database. </p> <p><a href="proxy.php?url=https://www.sqlite.org/wal.html">Read more</a></p> <h3>PRAGMA synchronous = NORMAL</h3> <p> When synchronous is NORMAL, the SQLite database engine will still sync at the most critical moments, but less often than in FULL mode. </p> <p> <a href="proxy.php?url=https://www.sqlite.org/pragma.html#pragma_synchronous">Read more</a> </p> <h3>PRAGMA busy_timeout = 5000</h3> <p> The&nbsp;busy_timeout&nbsp;setting in SQLite configures the database to wait for a specified amount of time when a database file is locked, rather than returning an error immediately. This is particularly useful in scenarios like web apps where multiple requests are trying to access the database simultaneously. </p> <p> <a href="proxy.php?url=https://www.sqlite.org/pragma.html#pragma_busy_timeout">Read more</a> </p> <h3>PRAGMA temp_store = MEMORY</h3> <p> When temp_store is MEMORY temporary tables and indices are kept as if they were in pure in-memory databases. </p> <p> <a href="proxy.php?url=https://www.sqlite.org/pragma.html#pragma_temp_store">Read more</a> </p> <h3>PRAGMA mmap_size = 134217728</h3> <p> Stolen from <a href="proxy.php?url=https://github.com/rails/rails/blob/ed76d0fbc74545f0275506b4ebf2dfd375fdda18/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L81-L88" >Rails SQLite settings</a >. </p> <p> This is the maximum number of bytes of the database file that will be accessed using memory-mapped I/O </p> <p> <a href="proxy.php?url=https://www.sqlite.org/pragma.html#pragma_mmap_size">Read more</a ><br /><a href="proxy.php?url=https://oldmoe.blog/2024/02/03/turn-on-mmap-support-for-your-sqlite-connections/" >Turn on mmap support for your SQLite connections</a > </p> <h3>PRAGMA journal_size_limit = 67108864</h3> <p> Stolen from <a href="proxy.php?url=https://github.com/rails/rails/blob/ed76d0fbc74545f0275506b4ebf2dfd375fdda18/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L81-L88" >Rails SQLite settings</a >. </p> <p> … in&nbsp;<a href="proxy.php?url=https://www.sqlite.org/wal.html">WAL mode</a>, the write-ahead log file is not truncated following a&nbsp;<a href="proxy.php?url=https://www.sqlite.org/wal.html#ckpt" >checkpoint</a >. Instead, SQLite reuses the existing file for subsequent WAL entries since overwriting is faster than appending. </p> <p> <a href="proxy.php?url=https://www.sqlite.org/pragma.html#pragma_journal_size_limit" >Read more</a > </p> <h3>PRAGMA cache_size = 2000</h3> <p> Stolen from <a href="proxy.php?url=https://github.com/rails/rails/blob/ed76d0fbc74545f0275506b4ebf2dfd375fdda18/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L81-L88" >Rails SQLite settings</a >. </p> <p> … maximum number of database disk pages that SQLite will hold in memory at once per open database file. </p> <p> <a href="proxy.php?url=https://www.sqlite.org/pragma.html#pragma_cache_size">Read more</a> </p> <h3>“transaction_mode”: “IMMEDIATE”</h3> <blockquote> <p> By default, SQLite starts transactions in&nbsp;<code>DEFERRED</code>&nbsp;mode: they are considered read only. They are upgraded to a write transaction that requires a database lock in-flight, when query containing a write/update/delete statement is issued. </p> <p> The problem is that by upgrading a transaction after it has started, SQLite will immediately return a&nbsp;<code>SQLITE_BUSY</code>&nbsp;error without respecting the&nbsp;<code>busy_timeout</code>&nbsp;previously mentioned, if the database is already locked by another connection. </p> <p> This is why you should start your transactions with&nbsp;<code >BEGIN IMMEDIATE</code >&nbsp;instead of only&nbsp;<code>BEGIN</code>. If the database is locked when the transaction starts, SQLite will respect&nbsp;<code>busy_timeout</code>.; </p> <cite><a href="proxy.php?url=https://kerkour.com/sqlite-for-servers">Source</a></cite> </blockquote> <p> <br /><a href="proxy.php?url=https://www.sqlite.org/lang_transaction.html">Read more</a> </p> <h2>Backups</h2> <p> This is the simplest solution for backup. It’s a little bash script that can be scheduled to run every hour using a cron job. This script will create a backup in a safe way even if your database is receiving a high volume of writes, as it captures a snapshot from a single transaction. Once the backup file is created it’s uploaded to an S3 bucket. </p> <pre><code>#!/usr/bin/env bash # # Run using cron to backup db every hour # set -euf -o pipefail sqlite3 /path/to/db "VACUUM INTO '/path/to/backup'" tar --zstd -cf "/path/to/backup.tar.zst" "/path/to/backup" # 1-day, rolling hourly backup aws s3 cp /path/to/backup.tar.zst s3://mybucket/backup-`date +%H`.tar.zst # 1-month, rolling daily backup aws s3 cp /path/to/backup.tar.zst s3://mybucket/backup-`date +%d`.tar.zst # 1-month, rolling hourly backup aws s3 cp /path/to/backup.tar.zst s3://mybucket/backup-`date +%d%H`.tar.zst</code></pre> <p><a href="proxy.php?url=https://litestream.io/alternatives/cron/">Source</a></p> <p> If you need something more robust than that you should look into the great <a href="proxy.php?url=https://litestream.io">Litestream</a>, which will continuously stream changes to an object store like S3 or any of it’s clones. </p> <h2>Other references and related links</h2> <ul> <li> <a href="proxy.php?url=https://kerkour.com/sqlite-for-servers" >Optimizing SQLite for servers</a > </li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=XcAYkriuQ1o" >Building Production Applications Using Go &amp; SQLite</a > </li> <li> <a href="proxy.php?url=https://tailscale.com/blog/database-for-2022" >A database for 2022</a > </li> <li> <a href="proxy.php?url=https://www.youtube.com/watch?v=B-_P0d1el2k" >The change Pieter Levels did to speed up SQLite</a > </li> <li> <a href="proxy.php?url=https://fractaledmind.github.io/2023/09/07/enhancing-rails-sqlite-fine-tuning/" >Enhancing your Rails app with SQLite: Fine-tuning your database</a > </li> <li> <a href="proxy.php?url=https://blog.pecar.me/sqlite-django-config" data-type="link" data-id="https://blog.pecar.me/sqlite-django-config" >This blog post</a > has info on how to implement this on Django&lt;5.1 </li> </ul> <p> Do you have any suggestions on how to improve this configuration? Send me an <a href="proxy.php?url=mailto:[email protected]">email</a>. </p>[email protected] (gcollazo)Wed, 12 Jun 2024 00:00:00 -0000https://gcollazo.com/optimal-sqlite-settings-for-djangoSoftware hecho a la medidahttps://gcollazo.com/software-hecho-a-la-medida<p> Todos los negocios usan software que es <em>casi-casi</em> perfecto, pero siempre falta algo. Siempre hay una parte de algún proceso que depende de una hoja de Excel que se envía por email o se comparte en Google Drive y que nadie sabe exactamente quien modificó o quien tiene la última versión. Como esto hay muchos <em>paper cuts</em> que hacen que los negocios pierdan el tiempo dándole la vuelta a soluciones de software que no están optimizadas para su negocio y su forma de trabajar. </p> <p> Estos negocios deben considerar la opción de adquirir <em>custom software</em>. Gracias a los <em>economics</em> del <strong>cloud</strong> y a que la mayoría de los negocios en Puerto Rico tienen una escala relativamente pequeña, es posible entregar software hecho a la medida a un costo accesible para muchos negocios que en el pasado no tenían esta posibilidad. </p> <p> A diferencia del software enlatado, el software hecho a la medida <strong>se puede adquirir sin necesidad de modelos SAAS</strong> que obligan a los negocios a pagar todos los meses para poder tener acceso. Gracias a una serie de avances tecnológicos (cloud, Docker, Go, SQLite, etc.) y técnicas de desarrollo recientes, es posible desplegar este tipo de software de forma que requiera un mínimo de mantenimiento, permitiendo que el negocio pague una sola vez por una <strong>licencia permanente</strong>. Muchos de los negocios con los que hablo quieren dejar de pagar por rentar el software que necesita su negocio para operar. </p> <p> Poder diseñar y dirigir la dirección del software que usa el negocio abre la puerta a muchas optimizaciones y eficiencias que no son posibles con el software enlatado que usa la mayoría. Es por esto que los negocios grandes siempre crean sus propias soluciones y lo consideran una ventaja competitiva estratégica. </p> <p> Estoy convencido que para que los negocios locales tengan una posibilidad real de competir en el mercado es necesario que tengan herramientas de software similares a las que usan sus competidores más grandes. Me da esperanza que finalmente tenemos un camino para hacerlo posible. </p> <p> Si tienes un negocio y te interesa explorar la posibilidad de adquirir software hecho a la medida me puedes contactar <a href="proxy.php?url=mailto:[email protected]">[email protected]</a> directamente o visitar <a href="proxy.php?url=https://blimp.io/">blimp.io</a> para que conozcas algunos de los clientes para los que he trabajado y los servicios que ofrezco. </p>[email protected] (gcollazo)Mon, 15 Jul 2024 00:00:00 -0000https://gcollazo.com/software-hecho-a-la-medidaEssential BBOT Commands for Reconhttps://gcollazo.com/essential-bbot-commands-for-recon<p> <a href="proxy.php?url=https://github.com/blacklanternsecurity/bbot" rel="noreferrer noopener" target="_blank" >BBOT</a > (BEE·bot) is a powerful recursive internet scanner designed for reconnaissance, bug bounties, and attack surface management. Think of it as your all-in-one tool information gathering and security assessment. </p> <p> I prefer running BBOT through Docker for consistent behavior across environments: </p> <pre><code># BBOT: Automated reconnaissance framework bbot() { docker run --rm -it \ -v "$HOME/.bbot:/root/.bbot" \ -v "$HOME/.config/bbot:/root/.config/bbot" \ blacklanternsecurity/bbot:stable "$@" }</code></pre> <p> Add this function to your shell configuration file, and you’re ready to go. </p> <h2>Essential Commands</h2> <p>Here are some BBOT commands I regularly use in my security assessments:</p> <h3>Full Subdomain Enumeration</h3> <pre><code># Comprehensive subdomain discovery bbot -t example.com -p subdomain-enum</code></pre> <p> Perfect for initial reconnaissance of a target domain. This command leverages multiple sources to build a complete picture of the target’s subdomain landscape. </p> <h3>Passive Subdomain Reconnaissance</h3> <pre><code># Non-intrusive subdomain discovery bbot -t example.com -p subdomain-enum -rf passive</code></pre> <p> Ideal for situations requiring stealth or when active scanning isn’t appropriate. This method relies solely on external data sources without directly interacting with the target. </p> <h3>Enhanced Domain Visualization</h3> <pre><code># Combine subdomain enumeration with port scanning and web screenshots bbot -t example.com -p subdomain-enum -m portscan gowitness</code></pre> <p> This command creates a comprehensive visual map of your target’s attack surface, combining port scanning with web interface documentation. </p> <h3>Basic Web Assessment</h3> <pre><code># Non-intrusive web technology enumeration bbot -t example.com -p subdomain-enum web-basic</code></pre> <p> Gathers essential information about web technologies while maintaining a light touch. Includes technology fingerprinting and robots.txt analysis. </p> <h3>Targeted Web Crawling</h3> <pre><code># Controlled depth web crawling with automated analysis bbot -t www.example.com \ -p spider \ -c web.spider_distance=2 web.spider_depth=2</code></pre> <p> Efficiently maps web application structure while automatically identifying sensitive information like emails and potential secrets. </p> <h3>Comprehensive Scan</h3> <pre><code># Full-spectrum reconnaissance bbot -t example.com -p kitchen-sink</code></pre> <p> When you need the full picture, this command combines subdomain enumeration, email discovery, cloud bucket identification, port scanning, web analysis, and vulnerability scanning with nuclei. </p> <p> This post is just scratching the surface. For more detailed information, check out the <a href="proxy.php?url=https://github.com/blacklanternsecurity/bbot" >official BBOT repository</a > and <a href="proxy.php?url=https://www.blacklanternsecurity.com/bbot/">documentation</a>. </p>[email protected] (gcollazo)Wed, 15 Jan 2025 00:00:00 -0000https://gcollazo.com/essential-bbot-commands-for-reconDemo Days: A Simpler Approach to Engineering Leadershiphttps://gcollazo.com/demo-days<p>Weekly demo days started as an experiment: a regular time when anyone can demo anything to the rest of the team. No slides allowed, no status reports, just a live demo.</p> <p>The format was straightforward: 3 to 4 engineers would each get up to 15 minutes to demonstrate their work in progress or whatever they like. Attendees can ask questions and the facilitator is responsible for maintaining a respectful and professional atmosphere.</p> <p>While most demos focused on current work projects, the format was open to anything that might interest the team - a cool new technology, a competitor's product, a side project, or a game someone built over the weekend.</p> <p>This freedom to share whatever they found exciting kept the sessions fun, engaging and low pressure. Despite how good it worked and felt, demo days gradually faded from our routine as we defaulted back to standard "agile" ceremonies.</p> <p>Reading Ken Kocienda's "<a href="proxy.php?url=http://creativeselection.io">Creative Selection</a>" about Apple's product development process reignited my interest in demo days. His book showed me how this approach could scale beyond small teams. Kocienda describes their process eloquently:</p> <blockquote> "We always started small, with some inspiration. We made demos. We mixed in feedback. We listened to guidance from smart colleagues. We blended in variations. We honed our vision. We followed the initial demo with another and then another. We improved our demos in incremental steps. We evolved our work by slowly converging on better versions of the vision. Round after round of creative selection moved us step by step from the spark of an idea to a finished product." </blockquote> <p>Here was one of the world's most successful technology companies using regular demos to drive their product design and as a cornerstone of their engineering culture.</p> <p>Then I recently learned about Oxide Computer Company's <a href="proxy.php?url=https://oxide.computer/blog/engineering-culture">experience with demo days</a> - a team I deeply respect for their engineering excellence. Oxide's journey with demos particularly resonates with me. As they describe it:</p> <blockquote> "...our weekly Demo Friday, an hour-long unstructured session to demo our work for one another. Demo Friday is such an essential part of Oxide’s culture that it feels like we have always done it, but in fact it took us nearly two years into the company’s life to get there: over the spring and summer of 2021, our colleague Sean Klein had instituted regular demos for the area that he works on (the Oxide control plane), and others around the company — seeing the energy that came from it — asked if they, too, could start regular demos for their domain. But instead of doing it group by group, we instituted it company-wide starting in the fall of 2021: an unstructured hour once a week in which anyone can demo anything." </blockquote> <p>Currently, I'm transitioning to doing more work as factional CTO for startup companies, and I've become increasingly frustrated with the endless stream of meetings that seem to define modern engineering management. I miss demo days. Demos feel easy and fun, and they produce high quality feedback and questions that help with implementation and help engineers focus on the important stuff. </p> <p>The benefits I observed went beyond just better code. Engineers started thinking differently about their work when they knew they'd be showing it to their peers. They naturally prioritized getting to working software faster and thought more carefully about their implementation choices.</p> <p>However, I'm also aware of the potential challenges. Teams might over-optimize for demos at the expense of deeper technical work. Some engineers might feel undue pressure to show progress every week. Without proper facilitation, demo sessions could devolve into critique sessions that harm team morale.</p> <p>As I prepare to implement this approach again with my consulting clients, I'm particularly interested in hearing from other technical leaders who have experience with demo days. How have you scaled this practice in larger organizations? What challenges did you encounter with distributed teams? How do you balance the demo cadence with other engineering processes?</p> <p>If you're using demo days in your organization, I'd love to hear about your experience. What's worked? What hasn't? How have you adapted the practice for your specific context?</p> <p><a href="proxy.php?url=mailto:[email protected]">[email protected]</a></p>[email protected] (gcollazo)Wed, 19 Feb 2025 00:00:00 -0000https://gcollazo.com/demo-daysValue Objects en Gohttps://gcollazo.com/go-value-objects<p> Llevo años experimentando con Value Objects (VO) en varios lenguajes, desde JavaScript y <a href="proxy.php?url=/debe-ser-imposible-representar-estados-ilegales/">TypeScript</a> hasta Python y, más recientemente, Go.</p> <p> Llevo dos años sumergido en Go por lo que he tenido la necesidad de implementar Value Objects en este lenguaje. Abajo comparto el acercamiento que he estado usando recientemente con bastante éxito. </p> <p>Esta implementación cumple con todos mis requisitos:</p> <ul> <li>Es imposible instanciar un VO inválido</li> <li>Métodos <code>New*</code> devuelven un <code>error</code></li> <li>Métodos <code>MustNew*</code> hacen panic si hay un error</li> <li>Inmutabilidad</li> <li>Igualdad basada en valor</li> <li>Encapsulación del comportamiento</li> </ul> <p>Para la mayoría de los casos uso el método <code>New*</code> que devuelve un <code>error</code> y me permite decidir como manejar los casos en que la instanciación falla. El método <code>MustNew*</code> lo uso principalmente en pruebas, dónde puedo estar seguro que estoy creando un VO válido manualmente y de esta forma evito tener que manejar un error que nunca sucederá.</p> <p>En todo caso es imposible crear un VO usando uno de estos métodos sin que cumpla con todos los requisitos de mi dominio. Y donde veo que se crea un VO sin usar uno de estos dos métodos puedo rechazar ese código sin pensarlo dos veces. Aunque esto require un poco de disciplina, es una regla fácil de seguir y hacer cumplir.</p> <p>Este es un ejemplo sintético de como implemento VO en Go. Es intencionalmente simple para que sea fácil de entender y aplicar en otros codebases. Aunque los patrones y el diseño del VO está bien y correcto esto no es una implementación válida de <code>Amount</code> así que no hagas copy y paste y lo uses en producción, te va a doler. </p> <pre><code>package amount import ( "errors" "fmt" ) type Currency string const ( USD Currency = "USD" EUR Currency = "EUR" ) func (c Currency) IsValid() bool { validCurrencies := map[Currency]bool{ USD: true, EUR: true, } return validCurrencies[c] } type Amount struct { value int currency Currency } func NewAmount(value int, currency Currency) (Amount, error) { if value < 0 { return Amount{}, errors.New("amount must be positive") } if value > 1_000_000 { return Amount{}, errors.New("amount must be less than or equal to 1,000,000") } if !currency.IsValid() { return Amount{}, errors.New("invalid currency") } return Amount{ value: value, currency: currency, }, nil } func MustNewAmount(value int, currency Currency) Amount { a, err := NewAmount(value, currency) if err != nil { panic(err) } return a } func (a Amount) Value() int { return a.value } func (a Amount) Currency() Currency { return a.currency } func (a Amount) String() string { switch a.currency { case USD: return fmt.Sprintf("$%d", a.value) case EUR: return fmt.Sprintf("%d€", a.value) default: return fmt.Sprintf("%d %s", a.value, a.currency) } } func (a Amount) Equal(other Amount) bool { return a.value == other.value && a.currency == other.currency } func (a Amount) ConvertTo(newCurrency Currency) (Amount, error) { if a.currency == newCurrency { return a, nil } // Assume 1 USD = 0.85 EUR conversionRate := 0.85 if a.currency == EUR { conversionRate = 1 / conversionRate } newValue := int(float64(a.value) * conversionRate) return NewAmount(newValue, newCurrency) } func (a Amount) Add(other Amount) (Amount, error) { if a.currency != other.currency { return Amount{}, errors.New("cannot add amounts with different currencies") } return NewAmount(a.value+other.value, a.currency) } func (a Amount) Subtract(other Amount) (Amount, error) { if a.currency != other.currency { return Amount{}, errors.New("cannot subtract amounts with different currencies") } return NewAmount(a.value-other.value, a.currency) } </code></pre> <p>Siento que si un veterano de Go mira este código no le encantaría. Pero para mí que llevo solo dos años en Go se siente natural y creo que encaja bien con los otros APIs que he visto en el "standard library". Si tienes una mejor forma de implementar VOs en Go me gustaría verla.</p>[email protected] (gcollazo)Tue, 25 Mar 2025 00:00:00 -0000https://gcollazo.com/go-value-objectsAQUI.PR: Un nuevo newsletter que está cambiando la forma de consumir noticias en Puerto Ricohttps://gcollazo.com/aquipr<p>Recientemente lanzamos <a href="proxy.php?url=https://aqui.pr?utm_source=gcollazo.com&utm_medium=web&utm_campaign=aquipr">aqui.pr</a>, un proyecto que me tiene super entusiasmado y del que quería compartir un poco más.</p> <p>Todos lo hemos vivido, entras a un site de noticias para leer algo que te interesa y te bombardean con anuncios, videos que se reproducen solos y popups que se meten en el medio. La experiencia es tan frustrante que muchos terminamos abandonando antes de leer lo que queríamos o nos vemos obligados a usar "ad blockers" o el "reader mode" que muchos navegadores tienen disponible para lidiar con este problema.</p> <p>Así nació aqui.pr, un newsletter diario que llega a tu correo a las 6:30 de la mañana con las 10 noticias más importantes y comentadas de Puerto Rico. Sin fastasmeo, sin clickbait, sin publicidad invasiva.</p> <h3>¿Cómo funciona?</h3> <p>Lo que hace diferente a aqui.pr es su modelo híbrido que combina lo mejor del periodismo tradicional con las herramientas de inteligencia artificial:</p> <ol> <li>Nuestros editores seleccionan diariamente las 10 noticias más relevantes.</li> <li>Para cada noticia, recopilamos información de hasta tres fuentes periodísticas distintas.</li> <li>Utilizamos IA para resumir ese contenido extenso en párrafos concisos y claros.</li> <li>Los editores revisan, pulen y verifican cada resumen antes de publicarlo.</li> </ol> <p>Ese proceso nos permite ofrecerte en cinco minutos toda la información que necesitas para estar al día con lo que ocurre en la isla.</p> <h3>Respetando al lector</h3> <p>No es ningún secreto que el modelo de negocio de los medios digitales está roto. La dependencia de clicks y publicidad intrusiva ha creado una experiencia horrible para los lectores.</p> <p>En aqui.pr apostamos por un enfoque diferente:</p> <ol> <li>Sin clickbait: No necesitamos titulares sensacionalistas porque no dependemos de que hagas click.</li> <li>Respeto por tu tiempo: Resumimos lo importante para que estés informado en minutos.</li> <li>Publicidad no intrusiva: Sí, tenemos anuncios (tres por newsletter), pero están diseñados para no interrumpir tu lectura. </li> <li>Patrocinios voluntarios: Por solo $3 mensuales puedes apoyar este proyecto y ayudarnos a seguir creciendo.</li> </ol> <h3>Los resultados hasta ahora</h3> <p>Aunque hoy es el lanzamiento oficial, llevamos varias semanas haciendo pruebas y puliendo el producto. Estas primeras semanas han sido increíbles. El feedback que hemos recibido confirma lo que sospechábamos: la gente quiere estar informada, pero no quiere lidiar con lo difícil y fragmentado que se ha vuelto consumir noticias.</p> <p>Muchos suscriptores nos comentan que ahora se sienten más preparados para sus conversaciones diarias y más conectados con lo que pasa en Puerto Rico—especialmente aquellos boricuas que viven fuera de la isla.</p> <p>Suscríbete gratis visitando <a href="proxy.php?url=https://aqui.pr?utm_source=gcollazo.com&utm_medium=web&utm_campaign=aquipr">aqui.pr</a>.</p>[email protected] (gcollazo)Mon, 07 Apr 2025 00:00:00 -0000https://gcollazo.com/aquiprTaking on New Development Workhttps://gcollazo.com/taking-on-new-development-work<p>It's been a while since I've shared any professional updates here.</p> <p>As some of you might know, I stepped down as CTO of Gasolina Móvil (now Alias Payments) in 2022 after more than seven years building what became one of Puerto Rico's largest mobile payment platforms. What started as a simple idea (let people pay for gas with an app) turned into a complex fintech operation spanning multiple countries, with custom hardware, payment processing, and some of the most stringent security requirements I've ever worked with.</p> <p>Those years were an incredible learning experience. I went from knowing very little about cybersecurity to becoming obsessed with it. Going through our first PCI compliance audit was almost traumatic, now it's just routine. I learned to scale teams, handle customer service at enterprise level, and most importantly, how to build secure systems when the stakes are real, when you're processing actual payments and handling real customer data.</p> <p>Now I'm also in a transition period. After spending so many years focused on one product, I'm exploring what comes next. I'm taking my time to figure it out, whatever I choose will likely be a long-term commitment, so I don't want to rush.</p> <p>In the meantime, I'm doing software development work and some consulting.</p> <blockquote>Over the past two years, I've worked on custom software development for a large construction company, product design and development for a nonprofit, and SDLC consulting for a data analytics company. I've also helped a fintech startup with PCI DSS compliance, an insurance startup with SOC 2, and a SaaS company with ISO 27001 implementation. </blockquote> <p>I currently have time available to take on new clients, so now is a great time to reach out if you need help building software. <strong>My primary focus is hands-on software development, backend and frontend work in Python, Go, and TypeScript.</strong> But I have some experience in a bunch of other technologies. I want to write code and ship products alongside other teams, or help entrepreneurs create and launch their product idea.</p> <p>I can also consult on DevSecOps, cloud architecture, and secure development lifecycle practices when needed. I've been through the trenches of building payment systems, implementing security compliance frameworks, and scaling teams under pressure.</p> <p>We can talk about your architectural decisions, security posture, development processes, or dive deep into specific technical challenges. Whether you're a startup trying to build secure-by-design systems or an established company dealing with technical debt and compliance requirements, I enjoy sharing what I've learned from building systems that handle real money and real security threats.</p> <p>The format is flexible, we can do a single consultation call to start, or work together over several sessions if it makes sense. This will probably work best for technical leaders, CTOs, or teams dealing with security and scaling challenges.</p> <p>If this sounds interesting, drop me an email at <a href="proxy.php?url=mailto:[email protected]">[email protected]</a>, and I'll get back to you if I can help.</p> <p>¡Gracias!</p>[email protected] (gcollazo)Wed, 11 Jun 2025 00:00:00 -0000https://gcollazo.com/taking-on-new-development-workTomando Nuevos Proyectos de Softwarehttps://gcollazo.com/tomando-nuevos-proyectos-de-software<p>Ha pasado tiempo desde que he compartido actualizaciones profesionales por aquí.</p> <p>Como algunos ya saben, dejé mi posición como CTO de Gasolina Móvil (ahora Alias Payments) en el 2022 después de más de siete años construyendo lo que se convirtió en una de las plataformas de pagos móviles más grandes de Puerto Rico. Lo que empezó como una idea simple, que la gente pueda pagar la gasolina con un app, se convirtió en una operación fintech compleja que abarca múltiples países, con hardware custom, procesamiento de pagos, y algunos de los requisitos de seguridad más estrictos que he trabajado.</p> <p>Esos años fueron una experiencia de aprendizaje increíble. Pasé de saber muy poco sobre ciberseguridad a obsesionarme con el tema. Pasar por nuestra primera auditoría de cumplimiento PCI fue casi traumático, ahora se siente rutinario. Aprendí a escalar equipos, manejar servicio al cliente, y lo más importante, cómo construir sistemas seguros cuando los riesgos son reales, cuando estás procesando pagos de verdad y manejando datos reales de clientes.</p> <p>Ahora estoy en un período de transición. Después de tantos años enfocado en un solo producto, estoy explorando qué viene después. Me estoy tomando mi tiempo para decidir, lo que sea que escoja probablemente será un compromiso a largo plazo, así que no me quiero apurar.</p> <p>Mientras tanto, estoy haciendo trabajo de desarrollo de software y algo de consultoría.</p> <blockquote>Durante los últimos dos años, he trabajado en desarrollo de software "custom" para una compañía grande de construcción, diseño de productos y desarrollo para una nonprofit, y consultoría de SDLC para una compañía de análisis de datos. También he ayudado a un startup fintech con cumplimiento PCI DSS, un startup de seguros con SOC 2, y una compañía SaaS con implementación de ISO 27001. </blockquote> <p>Actualmente, tengo tiempo disponible para tomar nuevos clientes, así que ahora es un buen momento para contactarme si necesitas ayuda construyendo software. <strong>Mi enfoque principal es desarrollo de software, trabajo de backend y frontend en Python, Go y TypeScript.</strong> Pero tengo experiencia en un montón de otras tecnologías. Quiero escribir código y lanzar productos junto a otros equipos, o ayudar emprendedores a crear y lanzar su idea de producto.</p> <p>También puedo hacer consultoría en DevSecOps, arquitectura de "cloud", y prácticas de "secure software development lifecycle" cuando sea necesario. He pasado por las trincheras construyendo sistemas de pagos, implementando marcos de cumplimiento de seguridad, y escalando equipos bajo presión.</p> <p>Podemos hablar sobre tus decisiones arquitectónicas, postura de seguridad, procesos de desarrollo, o profundizar en desafíos técnicos específicos. Ya sea que tengas un startup tratando de construir sistemas seguros desde el diseño o una compañía establecida lidiando con deuda técnica y requisitos de cumplimiento, disfruto compartir lo que he aprendido construyendo sistemas que manejan dinero real y amenazas de seguridad reales.</p> <p>El formato es flexible, podemos hacer una llamada de consultoría para empezar, o trabajar juntos durante varias sesiones si tiene sentido. Esto probablemente funcionará mejor para líderes técnicos, CTOs, o equipos lidiando con desafíos de seguridad y escalabilidad.</p> <p>Si esto te suena interesante, envíame un email a <a href="proxy.php?url=mailto:[email protected]">[email protected]</a>, y te respondo si puedo ayudar.</p> <p>¡Gracias!</p>[email protected] (gcollazo)Wed, 11 Jun 2025 00:00:00 -0000https://gcollazo.com/tomando-nuevos-proyectos-de-software