<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Matt Franz on Medium]]></title>
        <description><![CDATA[Stories by Matt Franz on Medium]]></description>
        <link>https://medium.com/@mdfranz?source=rss-253c4f2461ff------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*IcMonIlQj4MhWUOljXQyaQ.jpeg</url>
            <title>Stories by Matt Franz on Medium</title>
            <link>https://medium.com/@mdfranz?source=rss-253c4f2461ff------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 21:07:31 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mdfranz/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Mid Year 2026 Reading List]]></title>
            <link>https://mdfranz.medium.com/mid-year-2026-reading-list-0095914ced6f?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/0095914ced6f</guid>
            <category><![CDATA[history]]></category>
            <category><![CDATA[books]]></category>
            <category><![CDATA[reading]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Mon, 25 May 2026 17:13:48 GMT</pubDate>
            <atom:updated>2026-05-25T17:13:48.490Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/533/1*uMz9iCJFJqRA26CXgEWhvA.png" /><figcaption>My wife’s debut short fiction collection sold out at her reading last week!</figcaption></figure><p>(A shameless plug, buy my wife’s book at <a href="https://www.ameliafranz.com/">https://www.ameliafranz.com/</a>)</p><h3>Mid Year 2026</h3><p>As we approach the middle of the year in the first year past the decade’s midpoint, change continues to accelerate. One graduation last week. Another this week. One child returns, another leaves in the Fall for college. The oldest re-evaluates their life and career choices. With our youngest out of high school and her book published, my wife wife re-enters the workforce in her 50’s.</p><p>In a month, I will take a road trip with my parents to help them move to Texas. My brother has already moved. Oh, how the roles reverse — having lived in Texas from 1989 to 2006.</p><p>In <a href="https://matthewfranz.substack.com/p/october-2025-reading-update">my last post</a> I discussed writing poetry again. <em>That did not last</em>. I started a new job in mid-October and the frenetic pace of AI adoption in personal and professional projects has not slowed.</p><p>But I also have not stopped reading.</p><p>Yes, the “books in progress” metric is too high.</p><p>Some other firsts.</p><p><em>Baseball.</em> I’ve been doing Raven’s games for the last few years, but my youngest and I went to an O’s game. It has probably been since the 1980’s since I went to a baseball game. Maybe it was even Memorial Stadium.</p><p>I actually enjoyed it. After 15 years, starting to feel like a native.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*h8dpojKPq07eAAfL.jpeg" /></figure><p>O’s vs. Red Socks: a real home run derby</p><p><em>Beer brats.</em> While my wife was away on her book too in Mississippi, we made some dad food! And, yeah, I probably used too much beer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8OrTn8EPXiar055S.jpeg" /></figure><p>The beer brats were a hit with the “boys”</p><p>But here is the list.</p><h3>History and Politics</h3><ul><li><em>King of Kings: The Iranian Revolution: A Story of Hubris, Delusion and Catastrophic Miscalculation</em> by Scott Anderson (2025)</li><li><em>General and the Jaguar: Pershing’s Hunt for Pancho Villa: A True Story of Revolution and Revenge</em> by Eileen Welsome (2009)</li><li><em>The Hour of the Predator: Encounters with the Autocrats and Tech Billionaires Taking Over the World</em> by Giuliano da Empoli (2025)</li><li><em>Challenger: A True Story of Heroism and Disaster on the Edge of Space</em> by Adam Higginbotham (2024)</li><li><em>Tunisgrad</em> by William Collins (2026)</li><li><em>The Age of Acrimony: How Americans Fought to Fix Their Democracy, 1865–1915</em> by Jon Grinspan (2021)</li><li><em>Road of Bones: The Siege of Kohima 1944 — The Epic Story of the Last Great Stand of Empir</em>e by Fergal Keane (2013)</li><li><em>Bernardo de Gálvez: Spanish Hero of the American Revolution</em> by Gonzalo M. Quintero Saravia (2018)</li><li><em>Washington’s General: Nathanael Greene and the Triumph of the American Revolution</em> by Terry Golway (2006)</li><li><em>The Fate of the Day: The War for America, Fort Ticonderoga to Charleston, 1777–1780</em> by Rick Atkinson (2025)</li><li><em>Race and Reunion: The Civil War in American Memory</em> by David W. Blight (2002)</li></ul><h3>Tech</h3><ul><li><em>Vibe Coding: Building Production-Grade Software With GenAI, Chat, Agents, and Beyond</em> by Gene Kim and Steve Yegge (2025)</li></ul><h3>Fiction</h3><ul><li><em>The Longest Man-Made Beach In The World: Biloxi Stories</em> by Amelia Franz (2025)</li><li><em>I Am a Cat: A new translation of the original Japanese cat classic</em> by Natsume Soseki (Author), Nick Bradley (Translator) in 2005.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0095914ced6f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[2025 Tech Learning in Review]]></title>
            <link>https://mdfranz.medium.com/2025-tech-learning-in-review-b2309dd67122?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/b2309dd67122</guid>
            <category><![CDATA[learning]]></category>
            <category><![CDATA[2025]]></category>
            <category><![CDATA[year-in-review]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Thu, 01 Jan 2026 20:49:20 GMT</pubDate>
            <atom:updated>2026-01-01T20:49:20.356Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DnJx5sluQBWRawcovDYN2w.png" /><figcaption>Least bad AI-Generated Picture I could make</figcaption></figure><p>2025 marked the first full year where I was IC again!</p><p>For most of the year I was able to spend heads-down and hands-on without the distractions of meetings. If 2025 was the year of Kubernetes and Elasticsearch, let’s see where 2025 took me! Some of that did persist into the new year, especially Elastic, but the topics below.</p><p>(NOTE: I used <a href="https://github.com/google-gemini/gemini-cli">gemini-cli</a> to review my commits from <a href="https://github.com/mdfranz/cheetsheetz">cheetzsheetz</a> but this is all humanly-written) with the top tools in each category being:</p><ul><li>Astral UV</li><li>Pydantic AI</li><li>DuckDB</li><li>Suricata</li><li>Trivy</li><li>CrowdStrike FalconPy</li><li>CryptPad</li></ul><h3>Python</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mVcY3qqEbYlFPgnfxlGfCg.png" /></figure><p>Even with AI, Python is one of those things you have to do the time to keep fluency. In the beginning of 2025 I had a terrible round 2 at MongoDB for an SRE Security Lead where I totally froze on a basic Python coding problem. Fortunately, I got better but it was about spending hours each day on dedicated coding tasks.</p><ul><li><a href="https://marimo.io/">Marimo</a> became a first-class tool in my notebook for exploring APIs and data.</li><li><a href="https://pandas.pydata.org/">Pandas</a> became my go-to tool above for slicing and dicing DataFrames in both standalone scripts and in Marimo notebooks.</li><li><a href="https://docs.astral.sh/uv/getting-started/installation/">UV</a> is now the first tool I install for managing Python and packages on Linux or Mac.</li><li><a href="https://docs.gitlab.com/user/packages/pypi_repository/">Python Private Packages</a> on GitLab.</li><li><a href="https://github.com/mdfranz/cheetsheetz/blob/main/python/scheduler.md">Simple Schedulers</a> such as <a href="https://schedule.readthedocs.io/en/stable/">schedule</a> or <a href="https://apscheduler.readthedocs.io/en/stable/#">APSchedular</a> when you don’t want to use something more complex like <a href="https://docs.celeryq.dev/en/main/getting-started/introduction.html">Celery.</a></li><li><a href="https://docs.python.org/3/library/asyncio.html">Asyncio</a> is unavoidable these days due to Agent Frameworks and event loops. Get good at it.</li></ul><h3>AI</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3S00uqSppBvNsbeTgRyM-A.png" /></figure><p>Obviously, give the pace of change in frameworks, models, and capabilities of self-hosted and SaaS models, this is tough to keep up with. I did take a bit of a break mid-year because I was burned out of working with LLMs but by the Fall I was back in the game, refreshed.</p><ul><li>Agent Frameworks: <a href="https://github.com/agno-agi/agno">Agno</a>, <a href="https://ai.pydantic.dev/agents/">Pydantic</a>, <a href="https://strandsagents.com/latest/">Strands</a>, and <a href="https://google.github.io/adk-docs/">Google ADK</a>.</li><li>MCP Servers where I focused on <a href="https://github.com/motherduckdb/mcp-server-motherduck">DuckDB</a> and <a href="https://github.com/ClickHouse/mcp-clickhouse">ClickHouse</a> with a bit of Kubernetes at the end of the year.</li><li>Command-Line Tools: <a href="https://llm.datasette.io/en/stable/">Python-LLM </a>and <a href="https://github.com/sigoden/aichat">AIChat</a></li><li>Agentic Editing (<a href="https://zed.dev/agentic">Zed</a> and <a href="https://github.com/google-gemini/gemini-cli">Gemini</a> and <a href="https://github.com/openai/codex">Codex</a> CLIs)</li></ul><h3>Data and Observability</h3><p>As the saying say, “security is a data problem.” I spent a lot of time with SQL, mostly with ClickHouse or DuckDB but as I tried to find tools that Mac Unified Logs it led me back to some old like NXLog and Syslog-NG which provide varying levels of support. I dusted off my Vector/VRL skills.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ARsPV3EJtElrWD_bgRrvjg.png" /></figure><ul><li>ClickHouse <a href="https://clickhouse.com/docs/operations/utilities/clickhouse-local">Local</a> <a href="https://clickhouse.com/docs/getting-started/quick-start/oss">Server</a> and <a href="https://clickhouse.com/docs/chdb">CHDB.</a></li><li>DuckDB</li><li><a href="https://bindplane.com/">BindPlane</a></li><li><a href="https://openobserve.ai/">Openobserve</a></li><li>Syslog-NG</li><li>NXLog</li><li><a href="https://vector.dev/">Vector</a></li></ul><h3>Networking and Security</h3><p>I got my start in network security and the first 15 years of my career (before I got into Cloud) was heavily focused in this space. I forgot some things and this year I had to re-learn when I had to manage Palo Alto firewalls. Routing protocols and propagation IKE Main Mode and Agreessive Mode, all these things I hadn’t touched since the Mid 2000s. I ended last year setting up a tap on my home network and right before Christmas I discovered that I had a year’s worth of Suricata data to dig into.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VhiEOx8dvdX18Lo5KAq-7Q.png" /></figure><ul><li>IPSEC and IKE</li><li>BGP</li><li>Suricata</li><li>Zeek</li><li>Atomic Red Team</li></ul><h3><strong>DevOps and DevSecOps</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/984/1*KiwKIWDS7PAvSAGSgDzpSA.png" /></figure><p>Given I took a new Cloud Security role (or at least kind of cloud security-ish) role in Apri, I had to get back some of my Terraform and Ansible skills but spent a lot of time in with container and IAC static code scanning with Trivy, which I preferred to any of the hot mess from CrowdStrike.</p><ul><li>European Cloud Providers like <a href="https://learn.microsoft.com/en-us/azure/sentinel/overview?tabs=defender-portal">Scaleway</a> and <a href="https://www.exoscale.com/">Exoscale</a></li><li>Terraform and Bicep</li><li>Trivy</li><li>Ansible</li><li>AWS CodeCommit, CodeBuild and CodeDeploy</li><li>Gitlab CI</li></ul><h3>Security Products &amp; Platforms</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JPJ145iji-3m2GYLV-UkSg.png" /></figure><p>This year I used 3 different SIEM/SOAR platforms professionally (Elastic, CrowdStrike LogScale, and Google Chronicle/SecOps) but I did dip my toes into Sentinel to prepare for a job that I had multiple rounds but eventually got ghosted in January after inauguration day, when the job market seized up.</p><ul><li>Azure Sentinel</li><li>CrowdStrike Vulnerability Management and Cloud Security Posture Management</li><li>FalconPy</li><li>Palo Alto Cloud Firewalls</li><li>Sentinel One</li><li>Google SecOps</li></ul><h3>Miscellaneous</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rX3xuQm112a6XCWQ2CEa_Q.png" /></figure><p>As part of AI you can’t avoid the problem of web search, so I dug into the seedy (and expensive) world of web Scraping. And for my pro-Democracy tech work I set up a web site on Ghost (self-hosting) as well used (and tested local deployment) of CryptPad.</p><ul><li><a href="https://github.com/mdfranz/cheetsheetz/blob/main/ai/scraping.md">Web Scraping</a></li><li><a href="https://cryptpad.org/">CryptPad</a></li><li><a href="https://ghost.org/">Ghost</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b2309dd67122" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Detecting Fraudulent Resumes with Agno, Qdrant and Gemini]]></title>
            <link>https://mdfranz.medium.com/detecting-fraudulent-resumes-with-agno-qdrant-and-gemini-c5647f22a01c?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/c5647f22a01c</guid>
            <category><![CDATA[agents]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[hiring]]></category>
            <category><![CDATA[fraud]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Sun, 21 Dec 2025 14:17:08 GMT</pubDate>
            <atom:updated>2025-12-21T20:24:04.756Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GwFluye7rMdL2UDds7hkpQ.png" /></figure><h3>2025: The Year of AI Applicants</h3><p>I first encountered AI-generated resumes (and candidates using AI live during a screen interview) about a year ago, but this year I’ve seen this practice sky-rocket.</p><p>The pattern I’ve seen is a fully polished resume that has nearly 100% coverage of all the skills in the job description. The crudest form looks like this.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NylhbQ_bpttbpgNRAmKkkg.png" /></figure><p>So now when I’m manually reviewing resumes I <em>start</em> with LinkedIn because candidates are less likely to fake their public profile (unless they are a totally fake profile, but that is a different problem.)</p><p>So I was curious how difficult that would be to implement.</p><p>The basic problem was to detect differences between the submitted resume and a LinkedIn profile.</p><h3>Quick Design Session with Gemini</h3><p>When I posed to problem to Gemini, it first wanted me to extract real data from the sources and do data validation — either with <a href="https://docs.pydantic.dev/latest/concepts/validators/">Pydantic</a> or <a href="https://docs.python.org/3/library/dataclasses.html">Python Dataclasses</a>, I can’t remember.</p><p>I ignored this advise after sleeping on it. But it was useful to talk through the problem and debate whether LLMs and RAG were the most useful.</p><p>The key insight was that I wanted to focus on skill differences between the LinkedIn profile (which I considered accurate) and the resume, which was likely fake.</p><h3>Enter Agno and Qdrant</h3><p>Since I’ve already been using <a href="https://github.com/mdfranz/ai-tomfoolery/blob/main/agno2/agent_with_knowledge.py">Agents with Knowledge</a> (yes, this is a simple script, not a full-blown agent) and ingesting PDF, Markdown, and Website into Qdrant and other vector stores, I thought I’d try that first.</p><p>Which components did I use?</p><ul><li><a href="https://docs.agno.com/reference/storage/sqlite">SQLite</a> for <a href="https://docs.agno.com/basics/memory/overview">Memory</a> and <a href="https://docs.agno.com/basics/knowledge/content-db">ContentDB</a> — memory is for saving session information (I’ll show below) and I’ve found ContentDB helps Knowledge do a better job.</li><li><a href="https://qdrant.tech/documentation/quickstart/">Qdrant</a> for my Vector store which I stood up on Podman.</li></ul><pre>    timestamp_str = datetime.datetime.now().strftime(&quot;%y%m%d%M%S&quot;)<br>    my_embedder = GeminiEmbedder()<br>    memory_db = SqliteDb(db_file=&quot;agno-memory.db&quot;)<br>    contents_db = SqliteDb(db_file=f&quot;agno-knowledge-contents-{timestamp_str}.db&quot;)<br><br>    vector_db = Qdrant(<br>        name=&quot;Qdrant Job Description&quot;,<br>        collection=f&quot;vectors-{timestamp_str}&quot;,<br>        url=&quot;http://localhost:6333&quot;,<br>        embedder=my_embedder,<br>    )</pre><p>Notice the <em>timestamp_str</em> because I right now I’m just running the comparison for each candidate to limit search space. We will end up having timestamped collections like this.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8yLwFb8jVIpMIebiNnlXag.png" /></figure><p>This isn’t super interesting given so little data has been embeded.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XsMMkrjHNR3LYwGKWGwjxQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rJoSbHPAtSXQhWVLHcyQgw.png" /></figure><p>Next load the LinkedIn PDF and Resume with <a href="https://docs.agno.com/basics/knowledge/readers/usage/pdf-reader">PDFReader</a></p><pre>    knowledge = Knowledge(vector_db=vector_db, contents_db=contents_db)<br><br>    knowledge.add_content(<br>        name=&quot;Applicant LinkedIn Profile&quot;,<br>        path=f&quot;{kb_path}/linkedin.pdf&quot;,<br>        reader=PDFReader(),<br>    )<br><br>    knowledge.add_content(<br>        name=&quot;Applicant Resume&quot;,<br>        path=f&quot;{kb_path}/resume.pdf&quot;,<br>        reader=PDFReader(),<br>    )</pre><p>This will be stored in the ContentDB</p><pre>% sqlite3 agno-knowledge-contents-2512215726.db<br>SQLite version 3.51.0 2025-06-12 13:14:41<br>Enter &quot;.help&quot; for usage hints.<br>sqlite&gt; .schema<br>CREATE TABLE agno_knowledge (<br>        id VARCHAR NOT NULL, <br>        name VARCHAR NOT NULL, <br>        description VARCHAR NOT NULL, <br>        metadata JSON, <br>        type VARCHAR, <br>        size BIGINT, <br>        linked_to VARCHAR, <br>        access_count BIGINT, <br>        status VARCHAR, <br>        status_message VARCHAR, <br>        created_at BIGINT, <br>        updated_at BIGINT, <br>        external_id VARCHAR, <br>        PRIMARY KEY (id)<br>);<br>CREATE TABLE agno_schema_versions (<br>        table_name VARCHAR NOT NULL, <br>        version VARCHAR NOT NULL, <br>        created_at VARCHAR NOT NULL, <br>        updated_at VARCHAR, <br>        PRIMARY KEY (table_name)<br>);<br>CREATE INDEX idx_agno_schema_versions_created_at ON agno_schema_versions (created_at);<br>sqlite&gt; select * from agno_knowledge;<br>eb154723-69eb-5931-b8a1-86d2731b0363|Applicant LinkedIn Profile||null||||0|completed||1766321846|1766321847|<br>921c90cf-ac37-501e-bbb4-227b53f4084a|Applicant Resume||null||||0|completed||1766321847|1766321847|</pre><h3>Giving the Agent Knowledge</h3><p>Now all you have to do is pass the objects to the <a href="https://docs.agno.com/basics/agents/overview">Agent</a>.</p><pre>agent = Agent(<br>        name=f&quot;Agno Chat with Applicant Documents&quot;,<br>        model=my_model,<br>        instructions=[<br>            &quot;Include sources in your response.&quot;,<br>            &quot;Search your knowledge before answering the question.&quot;,<br>            &quot;Only include the output in your response. No other text.&quot;,<br>            &quot;The sources contain information from a single person who you can refer to as candidate.&quot;,<br>            &quot;Do NOT make up names for candidates like John Doe, only use real names&quot;,<br>        ],<br>        knowledge=knowledge,<br>        tools=[ReasoningTools(add_instructions=True)],<br>        add_datetime_to_context=False,<br>        markdown=True,<br>        debug_mode=True,<br>        db=memory_db,<br>        add_history_to_context=False,<br>        num_history_runs=3,<br>    )</pre><p>Then run</p><pre>agent.print_response(<br>        &quot;&quot;&quot;Extract the candidates name and location from all sources. Capture all skills and role descriptions listed in both sources<br>            to identify conflicting information between the Resume and the LinkedIn Profile, examining whether the same content is in each source.<br>            The LinkedIn profile is likely to be more accurate and the candidate may have added fake skills they do not possess to the resume.<br>            Provide a confidence score of the likelihood that the resume misrepresents their actual skills where<br>            100 is completely fake and 1 would be the skills match one to one.<br>        &quot;&quot;&quot;,<br>        stream=True,<br>        show_full_reasoning=True,<br>        stream_events=True,<br>    )</pre><p>And if you’ll see something like</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FuDs7iyFdfVi3c6h8dJrxA.png" /><figcaption>gemini-3-flash agent</figcaption></figure><p>And the tool calls</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KnSJCLfA3B8Iuk-pwb8HHQ.png" /></figure><p>The verdict</p><blockquote>The candidate is an iOS/Full-Stack developer by trade (per LinkedIn), but the resume presents them as a veteran Security Engineer.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9JUqkCqYx00L__nGT1g1LA.png" /></figure><p>Here is a different profile with the full analysis, where you can see how nicely Agno compared the skills in the resume.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FODmzM2csta1x3oxuUVTOg.png" /></figure><p>I found good results with Gemini 2.5 Pro and Gemini 3 Flash Preview (Gemini 3 was spotty for some reason) and I thought I’d test <a href="https://essential.ai/research/rnj-1">https://essential.ai/research/rnj-1</a> on Ollama and it broke it’s brain!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S61cVXaeFluKEMgR7Lkuxw.png" /></figure><p>Also published on <a href="https://substack.com/home/post/p-182238002">my Substack</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c5647f22a01c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Build a Low Cost Serverless Linux SIEM with Vector, S3, and ClickHouse (Local)]]></title>
            <link>https://mdfranz.medium.com/build-a-low-cost-serverless-linux-siem-with-vector-s3-and-clickhouse-local-e8b92ed3457c?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/e8b92ed3457c</guid>
            <category><![CDATA[s3]]></category>
            <category><![CDATA[siem]]></category>
            <category><![CDATA[json]]></category>
            <category><![CDATA[rust]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Fri, 14 Mar 2025 14:46:39 GMT</pubDate>
            <atom:updated>2025-03-14T14:46:39.781Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vKFqjlx8_sRuP2NTmcKrAw.jpeg" /></figure><p>It has been over <a href="https://blog.mdfranz.com/what-i-learned-about-rust-on-my-4th-of-july-vacation-this-year/">five years since I first used Vector</a>, but only in the last several months have I started to take advantage of ClickHouse with S3 as <a href="https://www.linkedin.com/feed/update/urn:li:activity:7291511892219490304/?ref=blog.mdfranz.com">I mentioned on LinkedIn</a> last month.</p><p>I also recently stood up a website with Ghost CMS for a non-profit and wanted something cheap and easy (off box) to analyze Nginx and Linux logs.</p><p>Since I know that Vector automatically parsed <a href="https://vector.dev/docs/reference/vrl/examples/?ref=blog.mdfranz.com#parse_nginx_log">nginx </a>and <a href="https://vector.dev/docs/reference/configuration/sources/journald/?ref=blog.mdfranz.com">journald</a> and could easily l<a href="https://vector.dev/docs/reference/configuration/sinks/aws_s3/?ref=blog.mdfranz.com">og to S3</a> so I gave it a shot.</p><h3>Create your S3 Bucket &amp; IAM Keypair</h3><p>Use my <a href="https://github.com/mdfranz/cfun/blob/main/examples/s3/bucket-ssm.yaml?ref=blog.mdfranz.com">CloudFormation template</a> to create the S3 Bucket and IAM credentials that you’ll need to pass to Vector to be able to write JSON events to the bucket.</p><p><strong>NOTE: </strong>Since CloudFormation outputs are unencrypted, I’ve gotten into a habit of writing the Key to a Parameter so you can delete it later.</p><pre>S3UserAccessKey:<br>    Type: AWS::IAM::AccessKey<br>    Properties:<br>      UserName: !Ref S3User</pre><pre>  BasicParameter:<br>    Type: AWS::SSM::Parameter<br>    Properties:<br>      Name: !Sub ${S3Bucket}-${S3UserAccessKey}<br>      Value: !GetAtt S3UserAccessKey.SecretAccessKey<br>      Type: String</pre><h3>Install Vector Set your Environment Variables</h3><p>Install <a href="https://vector.dev/download/?ref=blog.mdfranz.com">the latest vector</a> using your method of choice. I use the APT install script and this as worked across ARM and Intel systems just fine.</p><p>You’ll put these in /etc/default/vector so you can avoid putting in your</p><p>AWS_ACCESS_KEY_ID=AKIAXXX AWS_SECRET_ACCESS_KEY=YYYYY VECTOR_S3_BUCKET=my-log-bucket</p><p>/etc/default/vector</p><h3>Define your Bucket “Path Schema”</h3><p>We’ll start at the end focusing on what is most critical on how you will search logs with <a href="https://clickhouse.com/docs/operations/utilities/clickhouse-local?ref=blog.mdfranz.com">Clickhouse Local</a> where the path matters.</p><p>We’ll get into that later. Trust me. It matters.</p><pre>sinks:<br>    s3_bucket:<br>        type: aws_s3<br>        inputs:<br>            - common<br>        bucket: &quot;${VECTOR_S3_BUCKET}&quot;<br>        key_prefix: &quot;{{hostname}}/{{event_type}}/%D/&quot;<br>        region: &quot;us-east-1&quot;<br>        compression: gzip<br>        encoding:<br>           codec: json</pre><p>This will create a directory structure like this once we have all the event_type&#39;s defined. These are essentially your &quot;tables.&quot;</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*bHt-9Sa9H-tAffj5.png" /></figure><p>The %D will help populated the date structure so you can choose what to search overs, such as:</p><ul><li><strong>asus-pn50/auditd/03/12/25/1741812841-d797e698–3302–4e63-a863–6f4292d04d16.log.gz</strong></li><li><strong>opti3070/journald/03/04/25/1741798470–5f7b4bd9-abb4–42a6–9816-b82c4e7bbe96.log.gz</strong></li><li><strong>ubuntu-s-1vcpu-2gb-nyc3–01/nginx-access/03/13/25/1741824471-f39b039f-be2f-4723-b74f-f5b7e3a495ec.log.gz</strong></li></ul><p>(This is where it is helpful to having hostname conventions!)</p><p>When you form your SELECT or DESCRIBE from clickhouse you&#39;ll be use wildcards to only retrieve a subset of objects.</p><h3>Define your Data Sources</h3><p>For basic file monitoring I use the <a href="https://vector.dev/docs/reference/configuration/sources/file/?ref=blog.mdfranz.com">file source</a> and <a href="https://vector.dev/docs/reference/configuration/sources/journald/?ref=blog.mdfranz.com">journald</a> to avoid parsing syslog-style messages.</p><pre>sources:<br>    journald:<br>        type: journald</pre><pre>    nginx_access:<br>        type: file<br>        include:<br>            - /var/log/nginx/access.log*<br>    nginx_error:<br>        type: file<br>        include:<br>            - /var/log/nginx/error.log*<br>    auditd:<br>        type: file<br>        include:<br>            - /var/log/audit/audit.log<br>    auth_log:<br>        type: file<br>        include:<br>            - /var/log/auth.log</pre><p>This will create state files in /var/lib/vector like the following</p><pre>root@ubuntu-s-1vcpu-2gb-nyc3-01:/var/lib/vector# ls<br>auditd  auth_log  journald  nginx_access  nginx_error  var_log<br>root@ubuntu-s-1vcpu-2gb-nyc3-01:/var/lib/vector# ls -al<br>total 32<br>drwxr-xr-x  8 vector vector 4096 Mar 13 01:35 .<br>drwxr-xr-x 46 root   root   4096 Mar 12 13:09 ..<br>drwxr-xr-x  2 vector vector 4096 Mar 13 01:35 auditd<br>drwxr-xr-x  2 vector vector 4096 Mar 13 01:35 auth_log<br>drwxr-xr-x  2 vector vector 4096 Mar 12 16:23 journald<br>drwxr-xr-x  2 vector vector 4096 Mar 13 01:37 nginx_access<br>drwxr-xr-x  2 vector vector 4096 Mar 13 01:35 nginx_error</pre><p>As I was getting the parsers (and the path structure) just right, I would stop vector, delete from the bucket, then delete whichever file and they would be re-ingested. S3 writes are not immediate and sometimes it does take up to 5–10 minutes during your initial ingestion.</p><h3>Parse and Transform</h3><p>See the end of the blog for the links to the full code, but <a href="https://vector.dev/docs/reference/configuration/transforms/?ref=blog.mdfranz.com">transforms</a> do the heavy lifting in <a href="https://vector.dev/docs/reference/vrl/?ref=blog.mdfranz.com">VRL</a> and I only used some very simple <a href="https://vector.dev/docs/reference/configuration/transforms/remap/?ref=blog.mdfranz.com">remaps</a> to parse key value pairs in auditd as well as add the event_type field to each message. In the case of auditd_parse I did delete the &quot;msg&quot; field because it was not ingesting cleaning in ClickHouse, but I&#39;ll need to address that later because that field is the only part of the event that contains a timestamp.</p><pre>transforms:<br>    auditd_parse:<br>        type: remap<br>        inputs:<br>            - auditd<br>        source: |<br>            . = parse_key_value!(.message)<br>            .event_type = &quot;auditd&quot;<br>            del(.msg)<br>            .hostname = get_hostname!()<br><br>    auth_parse:<br>        type: remap<br>        inputs:<br>            - auth_log<br>        source: |<br>            . = parse_linux_authorization!(.message)<br>            .event_type = &quot;auth&quot;<br><br>    journald_parse:<br>        type: remap<br>        inputs:<br>            - journald<br>        source: |<br>            .event_type = &quot;journald&quot;<br><br>    common:<br>        type: remap<br>        inputs:<br>            - auth_parse<br>            - auditd_parse<br>            - journald_parse<br>        source: |<br>            .hostname = get_hostname!()</pre><p>The common transform at the end allows me to add the common hostname field onto each message.</p><h3>Parser Development &amp; Testing</h3><p>Although you <em>can</em> test your VRL on pipelines the best way to do this is either by running vector vrl with an input JSON stream and the code to show the output like this. See my <a href="https://github.com/mdfranz/vrl-devtest?ref=blog.mdfranz.com">vrl-devtest</a> report scripts and sample data to test this out. This allows you to run across a larger dataset to ensure you are properly handling errors.</p><p>This is just the VRL code used by the remap.</p><pre>.event_type = &quot;auditd&quot;<br>audit_msg = .msg[0]<br>audit_regex, err = parse_regex(audit_msg, r&#39;^audit\((?P&lt;time&gt;\d+).*\):&#39;)<br>if err == null {<br>  .event_timestamp = audit_regex.time<br>}<br>del(.msg)<br>.hostname = get_hostname!()</pre><p>You then execute with the following command.</p><pre>vector vrl -i auditd.json -p auditd.vrl -o --print-warnings</pre><p>When developing the initial parser you can also use the <a href="https://playground.vrl.dev/?ref=blog.mdfranz.com">VRL Playground</a> to test your code but it will not include all the functions such as get_hostname()</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KZo_nFpgWFq6XzQX.png" /></figure><p>LLMs were not very helpful for VRL code examples, but there are lots of GitHub projects so searching for parse_regex vector language:yaml in GitHub code search is the best way to find examples.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*bPHjIw8bOv1AFP5I.png" /></figure><p>Before you wire up S3, it does make sense to test locally with just file sources and sinks. The run.sh is just a simple wrapper for vector -vv -c vector.yaml and clean.sh removes the watcher files that are created so you can rapidly retest.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3XCcvgR7OQv8rSJ2.png" /></figure><h3>Assessing the Data in S3</h3><p>Assuming you’ve set your AWS Environment variables, ClickHouse will honor those.</p><pre>SELECT DISTINCT event_type<br>FROM s3(&#39;https://bucket_name.s3.us-east-1.amazonaws.com/*/*/*/*/*/*.log.gz&#39;)</pre><pre>Query id: cd07ac4e-9640-414b-be84-459fc84b8c3d</pre><pre>   ┌─event_type─┐<br>1. │ auditd     │<br>   └────────────┘<br>   ┌─event_type─┐<br>2. │ auth       │<br>   └────────────┘<br>   ┌─event_type─┐<br>3. │ journald   │<br>   └────────────┘</pre><pre>3 rows in set. Elapsed: 57.546 sec. Processed 6.20 million rows, 162.91 MB (107.76 thousand rows/s., 2.83 MB/s.)<br>Peak memory usage: 1.33 GiB.</pre><p>Remember the path structure allows to you search only subset of the data, for example this counts the various event types in March 2025</p><pre>SELECT<br>    count(*) AS cnt,<br>    event_type<br>FROM s3(&#39;https://bucket_name.s3.us-east-1.amazonaws.com/*/*/03/*/25/*.log.gz&#39;)<br>GROUP BY event_type<br>ORDER BY cnt ASC</pre><pre>Query id: d1de464f-b6f0-406e-9fbd-20a7dcf0f80c</pre><pre>   ┌─────cnt─┬─event_type─┐<br>1. │    7717 │ auth       │<br>2. │   17697 │ auditd     │<br>3. │ 2181709 │ journald   │<br>   └─────────┴────────────┘</pre><pre>3 rows in set. Elapsed: 28.226 sec. Processed 2.21 million rows, 58.29 MB (78.19 thousand rows/s., 2.07 MB/s.)<br>Peak memory usage: 1.33 GiB.</pre><h3>Getting at the Schema</h3><p>Simply use DESCRIBE to have ClickHouse infer the schema</p><pre>DESCRIBE TABLE s3(&#39;https://bucket_name.s3.us-east-1.amazonaws.com/*/journald/*/*/*/*.log.gz&#39;)</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/978/0*JKmrYOGWVyJuOrne.png" /></figure><p>And for auditd</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gU99nQtH99KIEUs3.png" /></figure><p>And auth which is pretty sparse in comparison.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4f5Hm5ZFUpqYh5kH.png" /></figure><h3>Digging into the Data</h3><p>Let’s start with auth… Remember DISTINCT is your friend for starting to characterize.</p><pre>SELECT DISTINCT (appname, hostname)<br>FROM s3(&#39;https://bucket_name.s3.us-east-1.amazonaws.com/*/auth/*/*/*/*.log.gz&#39;)<br>ORDER BY appname ASC</pre><pre>Query id: 0ad7a767-034d-4728-bfb7-731a0db313a0</pre><pre>    ┌─(appname, hostname)────────────────────────────┐<br> 1. │ (&#39;CRON&#39;,&#39;asus-pn50&#39;)                           │<br> 2. │ (&#39;CRON&#39;,&#39;franz-OptiPlex-7050&#39;)                 │<br> 3. │ (&#39;CRON&#39;,&#39;opti3070&#39;)                            │<br> 4. │ (&#39;chfn&#39;,&#39;opti3070&#39;)                            │<br> 5. │ (&#39;dbus-daemon&#39;,&#39;franz-OptiPlex-7050&#39;)          │<br> 6. │ (&#39;gdm-password]&#39;,&#39;franz-OptiPlex-7050&#39;)        │<br> 7. │ (&#39;gnome-keyring-daemon&#39;,&#39;franz-OptiPlex-7050&#39;) │<br> 8. │ (&#39;gpasswd&#39;,&#39;opti3070&#39;)                         │<br> 9. │ (&#39;groupadd&#39;,&#39;opti3070&#39;)                        │<br>10. │ (&#39;passwd&#39;,&#39;opti3070&#39;)                          │<br>11. │ (&#39;pkexec&#39;,&#39;franz-OptiPlex-7050&#39;)               │<br>12. │ (&#39;polkitd&#39;,&#39;opti3070&#39;)                         │<br>13. │ (&#39;polkitd&#39;,&#39;asus-pn50&#39;)                        │<br>14. │ (&#39;sshd&#39;,&#39;asus-pn50&#39;)                           │<br>15. │ (&#39;sshd&#39;,&#39;opti3070&#39;)                            │<br>16. │ (&#39;sshd&#39;,&#39;franz-OptiPlex-7050&#39;)                 │<br>17. │ (&#39;sudo&#39;,&#39;opti3070&#39;)                            │<br>18. │ (&#39;sudo&#39;,&#39;asus-pn50&#39;)                           │<br>19. │ (&#39;sudo&#39;,&#39;franz-OptiPlex-7050&#39;)                 │<br>20. │ (&#39;systemd-logind&#39;,&#39;asus-pn50&#39;)                 │<br>21. │ (&#39;systemd-logind&#39;,&#39;opti3070&#39;)                  │<br>22. │ (&#39;systemd-logind&#39;,&#39;franz-OptiPlex-7050&#39;)       │<br>23. │ (&#39;systemd-machined&#39;,&#39;asus-pn50&#39;)               │<br>24. │ (&#39;useradd&#39;,&#39;opti3070&#39;)                         │<br>25. │ (&#39;userdel&#39;,&#39;opti3070&#39;)                         │<br>26. │ (&#39;usermod&#39;,&#39;opti3070&#39;)                         │<br>    └────────────────────────────────────────────────┘</pre><p>What we can already tell from just this table that</p><ul><li>franz-OptiPlex-7050 is the only system running Ubuntu desktop</li><li>Users were added and deleted on opti3070</li></ul><p>So pkexec was only run on one system let&#39;s take peek</p><pre>SELECT (timestamp, message)<br>FROM s3(&#39;https://bucket.s3.us-east-1.amazonaws.com/*/auth/*/*/*/*.log.gz&#39;)<br>WHERE appname = &#39;pkexec&#39;</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3ndPhkJ4SHiQK2nP.png" /></figure><p>There is obviously a lot more you can do with the data depending on your SQL skills, but this shows you how to get started!</p><h3>Putting it all together</h3><pre>data_dir: &quot;/var/lib/vector&quot;<br>api:<br>    enabled: true<br>sources:<br>    journald:<br>        type: journald<br><br>    auditd:<br>        type: file<br>        include:<br>            - /var/log/audit/audit.log<br><br>    auth_log:<br>        type: file<br>        include:<br>            - /var/log/auth.log<br><br>transforms:<br>    auditd_parse:<br>        type: remap<br>        inputs:<br>            - auditd<br>        source: |<br>            . = parse_key_value!(.message)<br>            .event_type = &quot;auditd&quot;<br>            audit_msg = .msg[0]<br>            audit_regex, err = parse_regex(audit_msg, r&#39;^audit\((?P&lt;time&gt;\d+).*\):&#39;)<br>            if err == null {<br>              .event_timestamp = audit_regex.time<br>            }<br>            del(.msg)<br>            .hostname = get_hostname!()<br><br>    auth_parse:<br>        type: remap<br>        inputs:<br>            - auth_log<br>        source: |<br>            . = parse_linux_authorization!(.message)<br>            .event_type = &quot;auth&quot;<br><br>    journald_parse:<br>        type: remap<br>        inputs:<br>            - journald<br>        source: |<br>            .event_type = &quot;journald&quot;<br><br>    common:<br>        type: remap<br>        inputs:<br>            - auth_parse<br>            - auditd_parse<br>            - journald_parse<br>        source: |<br>            .hostname = get_hostname!()<br><br>sinks:<br>    s3_bucket:<br>        type: aws_s3<br>        inputs:<br>            - common<br>        bucket: &quot;${VECTOR_S3_BUCKET}&quot;<br>        key_prefix: &quot;{{hostname}}/{{event_type}}/%D/&quot;<br>        region: &quot;us-east-1&quot;<br>        compression: gzip<br>        encoding:<br>           codec: json</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e8b92ed3457c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[On a Day]]></title>
            <link>https://mdfranz.medium.com/on-a-day-69292f4375c6?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/69292f4375c6</guid>
            <category><![CDATA[freedom]]></category>
            <category><![CDATA[military]]></category>
            <category><![CDATA[patriotism]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Sat, 15 Feb 2025 23:47:48 GMT</pubDate>
            <atom:updated>2025-02-15T23:47:48.036Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SAAizyGiUej3oVllmqgu2Q.jpeg" /><figcaption>Anti-DOGE Protest outside the Dept of Treasury on 5 February</figcaption></figure><p>This morning when I finished up my workout at the local YMCA, the snow was softly falling around the American flag in front.</p><p>I tried to take a photo on my phone, but it was inadequate for the moment.</p><p>For once, the flag was not at half mast as it is too far too often — usually for dead children or others slain in mass shootings.</p><p>As someone who was raised in a military family (and who served myself for over a decade) seeing our flag sometimes makes me sad. Whether it is on the back of pickups or Jeeps with blue flags or on the houses of NIMBY neighbors proudly upholding the tradition of racial segregation in Baltimore County.</p><p>Less than 30 minutes from where we live now, there is a parade field on Edgewood Area of APG. We lived across the street in the Major’s quarters.</p><p>It was next to the post chapel. These were houses built in the early 1900s. They had steam heat that hissed and clanged in winter. And probably a lot of lead paint. There was a large parade field where we flew kites and made snow angels, watching the flakes fall down onto our faces in the darkness.</p><p>These are fond flag memories.</p><p>I remember crackling speakers on parade fields on the Army bases where I grew up, or marched myself during Basic Training or AIT.</p><p>That Lee Greenwood song that made me feel something in my Class-A’s back then, but now it just makes me sick.</p><p>(He also sells Bibles, I hear.)</p><p>There is real patriotism and there is the fake chest thumping sort that could give a damn about the veterans and their service, where the military is a prop for weak men who want to appear strong. Where service is packaged and sold.</p><p>(Yes, I’m thinking about the Mission BBQ “Deuce and Half” truck with flags flying that I saw today on York Road. And the fake pre-packaged patriotism when minimum wage workers slop your mac and cheese and brisket on your plate and ask what kind of sauces you want. You see patches on the wall for units you actually served in. No one there has a clue what the 49th “Lone Star” Armored Division or the 90th Army Reserve Command and the “TO” patch.)</p><p>On February 5th, I took off work early and attended my first protest outside of the Treasury Department. There was a buzz in the crowd when Senator Warren came up to the podium. I left with the group from Indivisible Baltimore when Jamie Raskin quoted Thomas Paine. I’m still thinking about that.</p><p>On a day when I’ve seen members of CISA resign — or be fired.</p><p>On a day that I’m hearing about measles outbreaks in Texas, a state I called home. (There is a TB outbreak in the state where I was born.)</p><p>On a day when “Phase 1” of cutting the CDC is making the news.</p><p>On a day when news we hear is that the sanctions between the U.S. and Russia put in place following Russia’s invasion into Ukraine, may be lifted.</p><p>On a day when a sitting American president says he is above the law.</p><p>On a day like this, seeing our flag does not make me feel proud.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=69292f4375c6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rapid Data AppDev with Windmill (on K3s) and Serverless Aurora]]></title>
            <link>https://mdfranz.medium.com/rapid-data-appdev-with-windmill-on-k3s-and-serverless-aurora-ae1015eecf0b?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/ae1015eecf0b</guid>
            <category><![CDATA[orchestration]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Sat, 01 Feb 2025 15:15:54 GMT</pubDate>
            <atom:updated>2025-02-01T15:15:54.051Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*AvjYUnNDo47TeF4u.jpeg" /><figcaption>(Imagen Nonsense)</figcaption></figure><p>Over the summer, I went down the <a href="https://github.com/mdfranz/cheetsheetz/blob/main/workflow.md?ref=blog.mdfranz.com">workflow rabbit hole</a> (starting with what I knew like Apache Airflow and Dagster) but ended up deciding on <a href="https://www.windmill.dev/?ref=blog.mdfranz.com">Windmill.dev</a> as the tool of choice.</p><p>It was a compromise between pure code options vs. no code for orchestrating random scripts. What were the factors?</p><ul><li>Open Source and good enough feature set in the free version.</li><li>Support for Bash, Python, Typescript, Golang, and more (Ansible has subsequently been added)</li><li>Solid <a href="https://www.windmill.dev/docs/core_concepts/roles_and_permissions?ref=blog.mdfranz.com">RBAC</a></li><li>Written in Rust for <a href="https://www.windmill.dev/docs/misc/benchmarks/competitors?ref=blog.mdfranz.com">high performance</a></li><li>Async task execution triggered by <a href="https://www.windmill.dev/docs/core_concepts/webhooks?ref=blog.mdfranz.com">webhooks</a></li><li>Built in Secrets Management, often through <a href="https://www.windmill.dev/docs/core_concepts/variables_and_secrets?ref=blog.mdfranz.com">variables</a> and <a href="https://www.windmill.dev/docs/core_concepts/resources_and_types?ref=blog.mdfranz.com">resources</a>.</li><li>AI-codegen (you enter your OpenAI API key when you deploy the instance) and Vim editor support (just kidding!)</li><li>A <a href="https://www.windmill.dev/docs/advanced/cli?ref=blog.mdfranz.com">CLI</a> that allows you do local code development and execution.</li><li>Easy <a href="https://www.windmill.dev/docs/advanced/dependencies_in_python?ref=blog.mdfranz.com">Python dependency management</a>, even though your jobs execute within containers, just import the libraries and they are automatically installed</li></ul><h3>Self-Hosting Windmill on K3s</h3><p>Windmill is super easy to deploy with a reliable <a href="https://github.com/windmill-labs/windmill-helm-charts/?ref=blog.mdfranz.com">helm chart</a> that I’ve used on both EKS and K3s (pictured below)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/907/0*0ogobYsc_xLRbjUk.png" /></figure><p>I expose the web UI with <a href="https://tailscale.com/kb/1236/kubernetes-operator?ref=blog.mdfranz.com">Tailscale</a>, but like most Helm resources you can use it to deploy a load-balancer and then terminate with an ACM cert.</p><pre>apiVersion: v1<br>kind: Service<br>metadata:<br>  annotations:<br>    meta.helm.sh/release-name: rogwindmill<br>    meta.helm.sh/release-namespace: windmill<br>    tailscale.com/expose: &quot;true&quot;<br>  creationTimestamp: &quot;2024-11-13T23:38:20Z&quot;<br>  finalizers:<br>  - tailscale.com/finalizer<br>  labels:<br>    app.kubernetes.io/managed-by: Helm<br>  name: windmill-app<br>  namespace: windmill<br>  resourceVersion: &quot;8843&quot;<br>  uid: 0a5116b1-d48f-42d8-bc40-fd65d842350b</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VrUaY0L78piwXjx7.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*koYVIWoTGBg-zt42.png" /></figure><h3>Creating Aurora Instance</h3><p>This was the first time I’ve used RDS Serverless Aurora. (Turn’s out it was the last because I got a huge bill)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KcMehikkDOgTBHbo.png" /></figure><p>I used Gemini to create a sample dataset</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*U_YEzWuQKW5AxK-Q.png" /></figure><p>Which I entered through the <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/query-editor.html?ref=blog.mdfranz.com">RDS Query editor</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8c6NWzhn1FFL8hWU.png" /></figure><p>Next, you add the resource, entering the credentials retrieved from AWS Secrets manager</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*89IerHZ-dr2X1oWY.png" /></figure><h3>Builders Build!</h3><p>Postgres is a “script type” just like Python, Bash, or other languages and it allows you to view the data as a script.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*nAUsBfnRZiTDFnaR.png" /></figure><p>A simple listing of the Data</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*tdVHoutKZg5lD73Y.png" /></figure><p>When you run it you’ll see the output in JSON or Table format</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9k5__OaJh42nryVP.png" /></figure><p>Next build the app and use the use the Database Studio widget</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7pvj4j2oXVZE4eT5.png" /></figure><p>When you run the “App” you’ll get the CRUD editor</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*iC10WdlBL4rp_nGP.png" /></figure><p>That allows you to insert (and edit) data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*eTjEgkJ2hwQCLbCk.png" /></figure><h3>References</h3><p><a href="https://www.windmill.dev/docs/apps/app_configuration_settings/database_studio?ref=blog.mdfranz.com">Database studio | Windmill</a></p><p><a href="https://github.com/mdfranz/cheetsheetz/tree/main/windmill">https://github.com/mdfranz/cheetsheetz/tree/main/windmill</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ae1015eecf0b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Fall-Winter 2024 Reading List]]></title>
            <link>https://mdfranz.medium.com/fall-winter-2024-reading-list-89af5b90cf03?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/89af5b90cf03</guid>
            <category><![CDATA[books]]></category>
            <category><![CDATA[tv-series]]></category>
            <category><![CDATA[movies]]></category>
            <category><![CDATA[sports]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Sat, 01 Feb 2025 15:10:16 GMT</pubDate>
            <atom:updated>2025-02-01T15:10:16.837Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*q1B4SZlxeOQWXYCQ.jpeg" /><figcaption>(Another Mid-Century tree has lost its limb at the local elementary school)</figcaption></figure><p>There is only one more month left until 2025. <em>I’m not sure what to think about that.</em> But at least the election is over. <em>America decided, </em>even if I’m not thrilled by the result.</p><p>We celebrated two birthdays in the family over the Thanksgiving break. None of us are getting any younger. Our oldest is no longer on our insurance. I turn 55 next year. Speaking of birthdays, we bought tickets for my son and two of his friends to attend the Ravens kickoff back in September. We all got terribly sunburned but it was worth it, even if we lost. First time at a Pro football game since the 1980s.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*g5GtQBzZ2mYen-__.jpeg" /><figcaption>Raven’s opener was a rough one</figcaption></figure><p>I visited Manhattan for the first time since 2022 and ate the most expensive steakhouse (and a Kosher one, at that) I’ve ever eaten, then wandered through Wall Street around midnight and past the 9/11 memorial before catching our Uber back to mid-town.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*R9-LxXJgkGQtq2gA.png" /><figcaption>The Trump Bubble</figcaption></figure><p><em>There are still seasons.</em> We even had snow flurries this month and this morning was the coldest day so far. We finally committed to join a new church.</p><p>(October and November are pledge time in the Episcopal Church.)</p><p>I avoided spending more than $150 on Black Friday specials, but did buy another Raspberry Pi I didn’t really need. You can never have too many home K3s clusters, right?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*1TrPEBLWAgSec4p4.jpg" /><figcaption>I should have listened to the clerk about the M.2 not fitting in the hat!</figcaption></figure><p>The weekend before the election, my youngest and I camped in the Michaux forest west of Gettysburg again. (The first time was in June 2023, before the shit hit the fan at Ping).</p><p>The calm before the storm.</p><p>I’ve stopped listening to <a href="https://www.thebulwark.com/?ref=blog.mdfranz.com">The Bulwark</a> for the most part. I’ve tuned out of Amerian politics because it is too depressing, but I have been trying to get involved in a local <a href="https://indivisible.org/?ref=blog.mdfranz.com">Indivisible</a> group. <em>Canadians at least understand.</em> I would be ashamed to travel to Europe at the moment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*wipyvt0O7srkBBvY.jpeg" /></figure><p>It has been a few weeks and months of sickness. Food poisoning. Chest colds. Concussions. Too many school absent notices. (If you didn’t know, wisdom teeth removal costs around $3K without dental insurance.) <em>Yet we are very fortunate.</em></p><p>This is the first November since 2022 to be part of layoffs, but others have not been so lucky. I have one more trip to Toronto before the end of the year? Is it five or six? <em>I’ve stopped counting</em>. This type I’ll take the train to Newark like I always did to India. The stock market is up (for now) and my 401K has never been higher and I’ve started looking at my social security payout given I’m less than a decade from being able to starting to use it.</p><p>But onto the books.</p><h3>Society &amp; Politics</h3><p>I’ve not yet finished <em>Strongmen</em> and <em>Power and Progress,</em> but both<em> </em>were obviously a reaction to socio-technical events: the reality of a authoritarian regime in the U.S. and the never-ending drumb-beat of techno-optimism. I still hope to finish <em>Out of the Darkness, </em>a post 1942 history of Germany — especially relevant given the resurgence of anti-democratic forces in Europe.</p><ul><li><em>Strongmen: Mussolini to the Present</em> by Ruth Ben-Ghiat (2020)</li><li><em>Power and Progress: Our Thousand-Year Struggle Over Technology and Prosperity</em> by Daron Acemoglu, Simon Johnson (2023)</li><li><em>Out of the Darkness: The Germans, 1942–2022</em> by Frank Trentmann (2024)</li></ul><h3>History</h3><p>Having fond memories of my trip to Europe in September and October 2023 (well, minus the part of getting COVID and hearing about my boss getting let go) I revisited the war in France, completing <em>To Lose a Battle</em> having read this text much faster than <em>A Savage War for Peace. </em>This led me to <em>The Road to Dunkirk</em> and <em>The Mastermind of Dunkirk and D-Day </em>and <em>Brothers in Arms.</em> <em>Nimitz at War </em>has been a treat, once again drawing clear lines between the Patton-MacArthur strain of (generally toxic) military leadership and the Eisenhower/Marshall/Nimitz school.</p><ul><li><em>The Blitzkrieg Myth: How Hitler and the Allies Misread the Strategic Realities of World War II</em> by John Mosier (2011)</li><li><em>Nimitz at War: Command Leadership from Pearl Harbor to Tokyo Bay</em> by Craig L. Symonds (2022)</li><li><em>Boyd: The Fighter Pilot Who Changed the Art of War</em> by Robert Coram (2002)</li><li><em>Brothers in Arms: One Legendary Tank Regiment’s Bloody War from D-Day to V-E Day</em> by James Holland (2021)</li><li><em>The Battle of Arnhem: The Deadliest Airborne Operation of World War II</em> by Antony Beevor (2018)</li><li><em>The Road to Dunkirk: The British Expeditionary Force and the Battle of the Ypres-Comines Canal, 1940</em> by Charles More (2013)</li><li><em>Mastermind of Dunkirk and D-Day: The Vision of Admiral Sir Bertram Ramsay</em> by Brian Izzard (2020)</li><li><em>To Lose a Battle: France 1940</em> by Alistair Horne (2007)</li><li><em>Military Misfortunes: The Anatomy of Failure in War</em> by Eliot A. Cohen (2006)</li></ul><h3>Tech &amp; Business</h3><p>Sometime it is good to review the basics on product development and security. Since I helped review and write an O’Reilly book a few years ago, I still have a subscription and it allows to me to read more technical books. <em>Threat Modeling</em> (Tarandach and Coles) was a nice update on Adam Shostok’s book from 2014. Even before the reality that Elon would be influencing the next administration, I started reading <em>Character Limit</em> and is as depressing as you’d think it would be, but I’ll continue plodding through it.</p><ul><li><em>Threat Modeling: A Practical Guide for Development Teams</em><br>by Izar Tarandach and Matthew J. Coles (2020)</li><li><em>Character Limit: How Elon Musk Destroyed Twitter</em> by Kate Conger, Ryan Mac (2024)</li><li><em>Technical Program Manager’s Handbook</em> by Joshua Alan Teter (2024)</li><li><em>Product Management in Practice </em>by Matt LeMay (2022)</li><li><em>Co-Intelligence: Living and Working with AI</em> by Ethan Mollick (2024)</li><li><em>The Friction Project: How Smart Leaders Make the Right Things Easier and the Wrong Things Harder</em> by Robert I. Sutton, Huggy Rao</li></ul><h3>Film &amp; TV</h3><p>I binged latest <em>The Diplomat</em> season like I did the first season on one of my trips to Canada, but all the others we watched with my wife.</p><ul><li><a href="https://www.imdb.com/title/tt8425404/?ref=blog.mdfranz.com">The Bay</a> (Britbox)</li><li><a href="https://www.imdb.com/title/tt17491088/?ref=blog.mdfranz.com">The Diplomat</a> (Netflix, Season 2)</li><li><a href="https://www.imdb.com/title/tt8855592/?ref=blog.mdfranz.com">The Mire: Millennium</a> (Amazon Prime)</li><li><a href="https://www.imdb.com/title/tt7016936/?ref=blog.mdfranz.com">Killing Eve</a> (Netflix)</li><li><a href="https://www.imdb.com/title/tt13323596/?ref=blog.mdfranz.com">The Long Shadow</a> (Acorn)</li><li><a href="https://www.imdb.com/title/tt17009710/?ref=blog.mdfranz.com">Anatomy of a Fall</a> (2023)</li><li><a href="https://www.imdb.com/title/tt21927594/?ref=blog.mdfranz.com">Girl you know it is True</a> (2023) — a German film about Milli-Vanilli!</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=89af5b90cf03" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Private RAG vs. Gemini Shootout (NIST 800–53 Edition)]]></title>
            <link>https://mdfranz.medium.com/private-rag-vs-gemini-shootout-nist-800-53-edition-b97f94ed355e?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/b97f94ed355e</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[nist-800-53]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Sun, 06 Oct 2024 16:13:07 GMT</pubDate>
            <atom:updated>2024-10-06T16:13:07.379Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*21NOJ5RGqF92udC-Wmt6NA.jpeg" /></figure><p>(This was initially published on <a href="https://blog.mdfranz.com/private-rag-vs-gemini-shootout-nist-800-53-edition/">my blog on Ghost</a>)</p><p>I’ve been using the “free” version of Gemini included with my ChromeBook plus for a few months but recently decided to upgrade to Advanced and testing with documents in my Google Drive and within Google Docs. The advanced version also gives you better image generation capabilities. In the past I’d ingested NIST 800–53 into PrivateGPT on my Mac M3 with mixed results and though I’d try again on my ASUS Nvidia 3060 I added to my son’s old gaming PC this past week, where I had Ollama.</p><h3>Local Hardware Setup</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DLjyFT_Ws2ak4IGT.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*rdCT7vM---RiOJEF.png" /></figure><p>The stats above were generated by <a href="https://github.com/aidatatools/ollama-benchmark/">LLM Benchmark</a></p><h3>Private RAG Configuration</h3><h3>Ollama 0.3.12</h3><p>With the exception of having Ollama listening on 0.0.0.0 I used a default install of the latest Ollama with the CUDA 12 drivers that get installed with the shell script</p><pre>root@rogstrix-b450-ryzen5:/etc/systemd/system/ollama.service.d# cat override.conf <br>[Service]<br>Environment=&quot;OLLAMA_HOST=0.0.0.0&quot;</pre><p>I used the following model for llama3.2</p><pre>$ ollama show llama3.2:latest<br>  Model<br>    architecture        llama     <br>    parameters          3.2B      <br>    context length      131072    <br>    embedding length    3072      <br>    quantization        Q4_K_M</pre><h3>PrivateGPT 0.6.2 (Llama 3.2 + Qdrant + Nomic)</h3><p>Python is a blessing and curse for Development, but <a href="https://docs.privategpt.dev/installation/getting-started/installation">PrivateGPT</a> is the easiest of the local RAG tools that I’ve found to install. It is on my list to get <a href="https://github.com/h2oai/h2ogpt">H2oGPT</a> working with Ollama but it is a little trickier, so I put it off.</p><p>Since I’m running Ubuntu 24.04 (which uses Python 3.12) I had to install Python 3.11 with uv in a virtual environment I created. I tried to make PrivateGPT run with uv, but ended up sticking with Poetry and once I installed the right Python version it worked with no issue. By default</p><pre>ubuntu@privatgpt:~/private-gpt-0.6.2/.venv$ cat pyvenv.cfg <br>home = /home/ubuntu/.local/share/uv/python/cpython-3.11.6-linux-x86_64-gnu/bin<br>implementation = CPython<br>uv = 0.4.18<br>version_info = 3.11.6<br>include-system-site-packages = false<br>prompt = private-gpt-0.6.2</pre><pre>server:<br>  env_name: ${APP_ENV:ollama}</pre><pre>llm:<br>  mode: ollama<br>  max_new_tokens: 512<br>  context_window: 3900<br>  temperature: 0.1     #The temperature of the model. Increasing the temperature will make the model answer more creatively. A value of 0.1 would be more factual. (Default: 0.1)</pre><pre>embedding:<br>  mode: ollama</pre><pre>ollama:<br>  llm_model: llama3.2<br>  embedding_model: nomic-embed-text<br>  api_base: <a href="http://100.115.28.90:11434/">http://100.115.28.90:11434</a><br>  embedding_api_base: <a href="http://100.115.28.90:11434/">http://100.115.28.90:11434</a>  # change if your embedding model runs on another ollama<br>  keep_alive: 5m<br>  tfs_z: 1.0              # Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting.<br>  top_k: 40               # Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)<br>  top_p: 0.9              # Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)<br>  repeat_last_n: 64       # Sets how far back for the model to look back to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx)<br>  repeat_penalty: 1.2     # Sets how strongly to penalize repetitions. A higher value (e.g., 1.5) will penalize repetitions more strongly, while a lower value (e.g., 0.9) will be more lenient. (Default: 1.1)<br>  request_timeout: 120.0  # Time elapsed until ollama times out the request. Default is 120s. Format is float.</pre><pre>vectorstore:<br>  database: qdrant</pre><pre>qdrant:<br>  path: local_data/private_gpt/qdrant</pre><h3>AnythingLLM (Llama 3.2 + LanceDB + Anything LLM Embedder)</h3><p>AnythingLLM Desktop is distributed as an AppImage which installed on an Ubuntu 24.40 desktop without a GPU knowing I would point at a remote Ollama server over my tailnet given it is on a different wireless network.</p><p>I configured Ollama at the Ryzen PI describe above.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xTanyEV3veOAQPxT.png" /></figure><p>Vector store after ingesting the NIST standard.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9lM8noAoLAx0O6FS.png" /></figure><p>This is the first time I’ve used <a href="https://lancedb.com/">LanceDB</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*TekjjgbILa_LZgEH.png" /></figure><p>Anything LLM is based on <a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2">MiniLM</a> (a CPU model, which I assume is far less capable than <a href="https://ollama.com/library/nomic-embed-text">Nomic</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RCLkRWP3FmWpx7ZF.png" /></figure><h3>Document Ingestion</h3><p>Of course on Gemini (ingested from Google Drive or added to the Gemini chat window on my ChromeBook Plus) had no issues with ingestion of the nearly 6 MB NIST standard. Private GPT was the slowest taking at least 30–45 minutes to ingest the document, but it at least shows progress bar to know it was doing something. I left to go grab lunch and it was done when it came back. The initial estimate was 46 seconds but it went way over and about 600MB of GPU VRAM was used during the embedding process. There was minimal CPU usage on the PrivateGPT VM or the Baremetal PC hosting Ollama. AnythingLLM took about 4–5 minutes to ingest, but didn’t indicate how long it would take to ingest. As I discovered after the ingestion, AnythingLLM ingestions and embedding does not even use the GPU.</p><h3>Listing the Top Control Families</h3><pre>What are the top level NIST Control Families</pre><h3>Gemini Advanced</h3><p>Gemini was the only LLM to properly pull out the 20 controls families listed on Page 8 in the standard. None of the other tools could do this “simple” task.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/905/0*llhl3MOnyyfq_bYu.png" /></figure><h3>AnythingLLM</h3><p>I got two different answers from AnythingLLM, the first with examples of controls.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*XBHxUjArg2dm8j5N.png" /></figure><p>I created a new thread and it gave me these 15 controls only getting Access Control right.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*K8b85je_C8hMADUg.png" /></figure><h3>Understanding the (-1) Control in Each Family</h3><p>Anyone that has has had to suffer through NIST 800–53 knows the first control is <em>Policies and Procedures</em>. I was curious if any of the LLM’s would get this so I came up with this not great prompt to see how they would respond.</p><pre>What is the similarity across the -1 control in AC-1, CA-1, AU-1, etc.</pre><h3>Gemini Advanced</h3><p>Gemini quickly and easily answered the prompt, getting the intent of my question.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mFnQTeRWQ44mqw-3.png" /></figure><h3>PrivateGPT</h3><p>PrivateGPT also answers the question with an up-front caveat.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0MpLdCwCfoIonJB8.png" /></figure><h3>AnythingLLM</h3><p>AnythingLLM really struggled to get the question.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mCrgaWXnpRmXB_QP.png" /></figure><h3>Vulnerability Scanning</h3><p>I crafted this intentionally vague prompt to see what the LLMs could understand about vulnerability scanning best practices.</p><pre>What vulnerability scanning practices should be implemented to ensure most controls are covered?</pre><h3>Gemini Advanced</h3><p>Shockingly, I found Gemini’s response was the most “word salad” among the three, although it started off well, but went downhill, not really understanding “organization-defined frequency” which is littered throughout NIST 800–53. However, it did mention CVSS and that scanners needed to updated and did mention classes of assets (meaning scan targets) in reasonable manner.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lho4l5o1EhHYWb8i.png" /></figure><h3>PrivateGPT</h3><p>The markdown response with bold-faced, numbered list provided by llama3.2 + ChromaDB used by PrivateGPT was more help. It also didn’t really understand “organization-defined frequency” but it did suggest monthly. It captured that different types of vulnerabilities exist (software flaws and improper configurations) as well as that the results need to be reviewed and communicated. <em>Genius! </em>It did pick up the idea of “scan coverage” but not “control coverage”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*i-zyQJu2xDcZQZgd.png" /></figure><h3>AnythingLLM</h3><p>Like PrivateGPT, AnythingLLM returned a numbered bulleted list with short explanations. It did the best on this question compared to the others and I actually liked the fact that it capture system vs. component (even though it really doesn’t understand what that means) as well as the idea that scanning should be continuous and that you should focus on most vulnerable systems. Given how poorly this configuration performed on the more basic questions, I surprised that it did this well.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sstYtu-7pw4bFLev.png" /></figure><h3>TLDR: Pass on AnythingLLM and stick with python-llm for inference and native tools for RAG</h3><p>I got better results using nomic with gemma2 but then I tried configuring AnythingLLM with OpenAI and the document uploading and embedding was just as problematic. The Workspace UX and threading interface is just janky and moving the embedded files between workspaces doesn&#39;t work reliably enough!</p><p>Given all the <a href="https://llm.datasette.io/en/stable/plugins/directory.html">Plugins</a> supported by <a href="https://llm.datasette.io/en/stable/index.html">Python-LLM</a> I’ll stick with that. It is much easier to just pick a different model for your query, and obviously allows you to directly pass commands (or cat files) to the llm through the shell.</p><pre>$ llm models<br>OpenAI Chat: gpt-3.5-turbo (aliases: 3.5, chatgpt)<br>OpenAI Chat: gpt-3.5-turbo-16k (aliases: chatgpt-16k, 3.5-16k)<br>OpenAI Chat: gpt-4 (aliases: 4, gpt4)<br>OpenAI Chat: gpt-4-32k (aliases: 4-32k)<br>OpenAI Chat: gpt-4-1106-preview<br>OpenAI Chat: gpt-4-0125-preview<br>OpenAI Chat: gpt-4-turbo-2024-04-09<br>OpenAI Chat: gpt-4-turbo (aliases: gpt-4-turbo-preview, 4-turbo, 4t)<br>OpenAI Chat: gpt-4o (aliases: 4o)<br>OpenAI Chat: gpt-4o-mini (aliases: 4o-mini)<br>OpenAI Chat: o1-preview<br>OpenAI Chat: o1-mini<br>OpenAI Completion: gpt-3.5-turbo-instruct (aliases: 3.5-instruct, chatgpt-instruct)<br>GeminiPro: gemini-pro<br>GeminiPro: gemini-1.5-pro-latest<br>GeminiPro: gemini-1.5-flash-latest<br>GeminiPro: gemini-1.5-pro-001<br>GeminiPro: gemini-1.5-flash-001<br>GeminiPro: gemini-1.5-pro-002<br>GeminiPro: gemini-1.5-flash-002<br>GeminiPro: gemini-1.5-flash-8b-latest<br>GeminiPro: gemini-1.5-flash-8b-001<br>Ollama: codegeex4:latest (aliases: codegeex4)<br>Ollama: hermes3:8b<br>Ollama: nemotron-mini:latest (aliases: nemotron-mini)<br>Ollama: nomic-embed-text:latest (aliases: nomic-embed-text)<br>Ollama: llama3.2:latest (aliases: llama3.2)<br>Ollama: llava:7b<br>Ollama: llava:13b<br>Ollama: llama3.1:8b (aliases: llama3.1:latest, llama3.1)<br>Ollama: mistral:7b (aliases: mistral:latest, mistral)<br>Ollama: gemma2:9b (aliases: gemma2:latest, gemma2)<br>Ollama: phi3:3.8b<br>Ollama: qwen2:7b<br>Ollama: gemma:2b<br>Ollama: qwen2.5:14b<br>Ollama: granite-code:8b<br>Ollama: qwen2.5-coder:latest (aliases: qwen2.5-coder)<br>Ollama: yi-coder:latest (aliases: yi-coder)<br>Ollama: qwen:4b<br>Ollama: codegemma:2b<br>Ollama: phi:latest (aliases: phi)</pre><p>As for RAG (summarization and querying) for now, I’ll stick with native tooling. Primarily with Gemini since I don’t actually have a ChatGPT subscription. It looks like I can get Gemini Plus through my <a href="https://one.google.com/">Google One subscription</a> and I can get metered Gemini through <a href="https://ai.google.dev/gemini-api">Google AI Studio</a>, so nice try AnythingLLM but I’m good!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b97f94ed355e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Get DuckDB swimming in your Packet Pond in 10 Minutes!]]></title>
            <link>https://mdfranz.medium.com/get-duckdb-swimming-in-your-packet-pond-in-10-minutes-142710c1e2ae?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/142710c1e2ae</guid>
            <category><![CDATA[network-security]]></category>
            <category><![CDATA[sql]]></category>
            <category><![CDATA[duckdb]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Sat, 31 Aug 2024 18:37:43 GMT</pubDate>
            <atom:updated>2024-08-31T18:50:59.512Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FPOsPq01L6wY-MldslgulA.jpeg" /></figure><p>At some point over the last few years I discovered <a href="https://duckdb.org/">DuckDB</a> as an alternative lightweight SQL database that is <a href="https://duckdb.org/why_duckdb.html">super fast</a> and has support for a <a href="https://duckdb.org/docs/extensions/core_extensions">wide variety</a> of file formats, but until now I’d only used CSV and Parquet, but recently a <a href="https://github.com/hrbrmstr/duckdb-pcap">pcap extension</a> was released, so I thought I’d try it out! Fortunately now there are binaries available so you don’t have to go through the headache of a building at DuckDB extension.</p><h3>Installation</h3><p>If you don’t already have it installed, install DuckDB 1.0 on on <a href="https://duckdb.org/docs/installation/">your platform</a>.</p><p>Following the instructions from the updated blog post at the bottom, you then install the pre-compiled extension.</p><pre>$ duckdb -unsigned<br>v1.0.0 1f98600c2c<br>Enter &quot;.help&quot; for usage hints.<br>D SET custom_extension_repository=&#39;https://w3c2.c20.e2-5.dev/ppcap/latest&#39;;<br>D install ppcap;<br>D load ppcap;</pre><p>If you want to see all the extensions available, use the following query:</p><pre>D select extension_name from duckdb_extensions() where loaded = True;<br>┌────────────────┐<br>│ extension_name │<br>│    varchar     │<br>├────────────────┤<br>│ autocomplete   │<br>│ fts            │<br>│ icu            │<br>│ inet           │<br>│ jemalloc       │<br>│ json           │<br>│ parquet        │<br>│ ppcap          │<br>│ shell          │<br>│ tpch           │<br>├────────────────┤<br>│    10 rows     │<br>└────────────────┘</pre><p>This creates the following files in ~/.duckdb</p><pre>$ ls -alR<br>.:<br>total 12<br>drwxr-xr-x  3 mfranz mfranz 4096 Aug 31 12:54 .<br>drwxr-x--- 41 mfranz mfranz 4096 Aug 31 12:56 ..<br>drwxr-xr-x  3 mfranz mfranz 4096 Aug 31 12:54 extensions</pre><pre>./extensions:<br>total 12<br>drwxr-xr-x 3 mfranz mfranz 4096 Aug 31 12:54 .<br>drwxr-xr-x 3 mfranz mfranz 4096 Aug 31 12:54 ..<br>drwxr-xr-x 3 mfranz mfranz 4096 Aug 31 12:54 v1.0.0</pre><pre>./extensions/v1.0.0:<br>total 12<br>drwxr-xr-x 3 mfranz mfranz 4096 Aug 31 12:54 .<br>drwxr-xr-x 3 mfranz mfranz 4096 Aug 31 12:54 ..<br>drwxr-xr-x 2 mfranz mfranz 4096 Aug 31 12:55 linux_amd64_gcc4</pre><pre>./extensions/v1.0.0/linux_amd64_gcc4:<br>total 22980<br>drwxr-xr-x 2 mfranz mfranz     4096 Aug 31 12:55 .<br>drwxr-xr-x 3 mfranz mfranz     4096 Aug 31 12:54 ..<br>-rw-rw-r-- 1 mfranz mfranz 19712350 Aug 31 12:54 httpfs.duckdb_extension<br>-rw-rw-r-- 1 mfranz mfranz      131 Aug 31 12:54 httpfs.duckdb_extension.info<br>-rw-rw-r-- 1 mfranz mfranz  3797542 Aug 31 12:55 ppcap.duckdb_extension<br>-rw-rw-r-- 1 mfranz mfranz      144 Aug 31 12:55 ppcap.duckdb_extension.info</pre><h3>Loading your PCAP</h3><p>You don’t have to install the extension each time so the next time you run, you just load it it then create a table from your pcap</p><pre>$ duckdb -unsigned<br>v1.0.0 1f98600c2c<br>Enter &quot;.help&quot; for usage hints.<br>D <br>D load ppcap;<br>D <br>D create table bridged as from read_pcap(&quot;~/bridged.pcap&quot;);</pre><p>All your normal SQL commands work</p><pre>D describe bridged;<br>┌─────────────┬─────────────┬──────┬─────┬─────────┬───────┐<br>│ column_name │ column_type │ null │ key │ default │ extra │<br>├─────────────┼─────────────┼──────┼─────┼─────────┼───────┤<br>│ timestamp   │ TIMESTAMP   │ YES  │     │         │       │<br>│ source_ip   │ VARCHAR     │ YES  │     │         │       │<br>│ dest_ip     │ VARCHAR     │ YES  │     │         │       │<br>│ source_port │ INTEGER     │ YES  │     │         │       │<br>│ dest_port   │ INTEGER     │ YES  │     │         │       │<br>│ length      │ INTEGER     │ YES  │     │         │       │<br>│ tcp_session │ VARCHAR     │ YES  │     │         │       │<br>│ source_mac  │ VARCHAR     │ YES  │     │         │       │<br>│ dest_mac    │ VARCHAR     │ YES  │     │         │       │<br>│ protocols   │ VARCHAR[]   │ YES  │     │         │       │<br>│ payload     │ BLOB        │ YES  │     │         │       │<br>│ tcp_flags   │ VARCHAR[]   │ YES  │     │         │       │<br>│ tcp_seq_num │ UINTEGER    │ YES  │     │         │       │<br>└─────────────┴─────────────┴──────┴─────┴─────────┴───────┘</pre><pre>D select count(*) from bridged;<br>┌──────────────┐<br>│ count_star() │<br>├──────────────┤<br>│ 104851       │<br>└──────────────┘</pre><pre>D .mode line<br>D pragma database_size;<br>database_name = memory<br>database_size = 0 bytes<br>   block_size = 0<br> total_blocks = 0<br>  used_blocks = 0<br>  free_blocks = 0<br>     wal_size = 0 bytes<br> memory_usage = 114.6 MiB<br> memory_limit = 37.5 GiB</pre><h3>Slicing and Dicing with SQL</h3><p>What are the IP protocols active on the network. Mostly UDP! (I do wish this field had been an INTEGER instead of a VARCHAR.</p><pre>D select protocols, count(*) as cnt from bridged group by protocols order by cnt desc;<br>┌─────────────────────────┬────────┐<br>│        protocols        │  cnt   │<br>├─────────────────────────┼────────┤<br>│ [Ethernet, IP, UDP]     │ 101644 │<br>│ [Ethernet, IP, TCP]     │ 2831   │<br>│ [Ethernet, IP, Unknown] │ 346    │<br>│ [Ethernet, IP, ICMP]    │ 30     │<br>└─────────────────────────┴────────┘</pre><p>Let’s find all the IPs on my local network</p><pre>D SELECT distinct (source_ip) from bridged where source_ip like &#39;192.168.12%&#39;;<br>┌────────────────┐<br>│   source_ip    │<br>├────────────────┤<br>│ 192.168.12.131 │<br>│ 192.168.12.159 │<br>│ 192.168.12.113 │<br>│ 192.168.12.1   │<br>└────────────────┘</pre><p>Let’s go down to the link layer and see where most of the frames are going from!</p><pre>D SELECT DISTINCT (source_mac, dest_mac) from bridged;<br>┌────────────────────────────────────────┐<br>│     main.row(source_mac, dest_mac)     │<br>├────────────────────────────────────────┤<br>│ (00:16:3e:9b:21:27, 33:33:00:00:00:16) │<br>│ (00:16:3e:7d:5c:ca, 33:33:00:00:00:16) │<br>│ (d6:06:10:3e:e9:ed, 56:1c:4b:ac:c3:15) │<br>│ (3a:99:83:4e:f6:9f, 33:33:00:00:00:16) │<br>│ (c8:99:b2:e1:47:17, 01:80:c2:00:00:13) │<br>│ (3a:99:83:4e:f6:9f, 33:33:00:00:00:02) │<br>│ (56:1c:4b:ac:c3:15, d6:06:10:3e:e9:ed) │<br>│ (56:1c:4b:ac:c3:15, 01:00:5e:7f:ff:fa) │<br>│ (c8:99:b2:e1:47:17, 33:33:00:00:00:01) │<br>│ (3a:99:83:4e:f6:9f, 01:00:5e:00:00:fb) │<br>│ (84:47:09:19:c3:16, 33:33:00:00:00:16) │<br>│ (00:16:3e:53:19:f5, 33:33:00:00:00:16) │<br>│ (00:16:3e:aa:1b:f3, 33:33:00:00:00:16) │<br>│ (52:54:00:8b:d8:ac, 33:33:00:00:00:16) │<br>│ (56:1c:4b:ac:c3:15, c8:99:b2:e1:47:17) │<br>│ (d6:06:10:3e:e9:ed, 33:33:00:00:00:16) │<br>│ (c8:99:b2:e1:47:1c, 01:80:c2:00:00:13) │<br>│ (c8:99:b2:e1:47:17, 56:1c:4b:ac:c3:15) │<br>│ (3a:99:83:4e:f6:9f, 33:33:00:00:00:fb) │<br>│ (3a:99:83:4e:f6:9f, 01:00:5e:00:00:16) │<br>│ (c8:99:b2:e1:47:17, 33:33:ff:45:46:08) │<br>└────────────────────────────────────────┘</pre><pre>D select dest_mac, count(*) as cnt from bridged group by dest_mac order by cnt desc;<br>┌───────────────────┬───────┐<br>│     dest_mac      │  cnt  │<br>├───────────────────┼───────┤<br>│ 56:1c:4b:ac:c3:15 │ 52469 │<br>│ d6:06:10:3e:e9:ed │ 51857 │<br>| c8:99:b2:e1:47:17 │ 492   │<br>│ 33:33:00:00:00:16 │ 16    │<br>│ 01:00:5e:7f:ff:fa │ 4     │<br>│ 33:33:00:00:00:fb │ 3     │<br>│ 01:00:5e:00:00:fb │ 3     │<br>│ 01:80:c2:00:00:13 │ 2     │<br>│ 01:00:5e:00:00:16 │ 2     │<br>│ 33:33:00:00:00:02 │ 1     │<br>│ 33:33:ff:45:46:08 │ 1     │<br>│ 33:33:00:00:00:01 │ 1     │<br>└───────────────────┴───────┘</pre><p>The first entry is my workstation so it makes sense most of the comms are with my router.</p><pre>D select source_ip, count(*) as cnt from bridged where dest_mac = &#39;56:1c:4b:ac:c3:15&#39; and source_ip like &#39;192.168.12%&#39; group by source_ip order by cnt desc limit 25;<br>┌────────────────┬───────┐<br>│   source_ip    │  cnt  │<br>├────────────────┼───────┤<br>│ 192.168.12.159 │ 51977 │<br>│ 192.168.12.1   │ 36    │<br>└────────────────┴───────┘</pre><p>Let’s see the unique ports</p><pre>D select distinct(dest_port) from bridged where dest_mac = &#39;c8:99:b2:e1:47:17&#39; and source_port != 0;<br>┌───────────┐<br>│ dest_port │<br>├───────────┤<br>│ 1900      │<br>│ 3478      │<br>│ 443       │<br>│ 80        │<br>│ 53        │<br>│ 5351      │<br>└───────────┘<br></pre><pre>D select dest_port, count(*) as cnt from bridged where dest_mac = &#39;c8:99:b2:e1:47:17&#39; and source_port != 0 group by dest_port order by cnt desc;<br>┌───────────┬─────┐<br>│ dest_port │ cnt │<br>├───────────┼─────┤<br>│ 443       │ 181 │<br>│ 3478      │ 71  │<br>│ 80        │ 37  │<br>│ 53        │ 26  │<br>│ 5351      │ 8   │<br>│ 1900      │ 2   │</pre><h3>Looking at the packets!</h3><p>Getting used to switching output <a href="https://duckdb.org/docs/api/cli/output_formats.html">formats</a> is useful depending on your screen resolution.</p><pre>D select * from bridged where length &lt; 15 limit 3;<br>  timestamp = 2024-08-31 16:56:25<br>  source_ip = 192.168.12.131<br>    dest_ip = 209.177.145.120<br>source_port = 46017<br>  dest_port = 443<br>     length = 14<br>tcp_session = 192.168.12.131:46017 -&gt; 209.177.145.120:443<br> source_mac = 56:1c:4b:ac:c3:15<br>   dest_mac = c8:99:b2:e1:47:17<br>  protocols = [Ethernet, IP, TCP]<br>    payload = \x00\x00\x01\x01\x08\x0A\xF5\xB3K.\x8B\xFB\x96\x1C<br>  tcp_flags = [ACK]<br>tcp_seq_num = 3986401156</pre><pre>  timestamp = 2024-08-31 16:56:25<br>  source_ip = 192.168.12.159<br>    dest_ip = 192.168.12.131<br>source_port = 24679<br>  dest_port = 6443<br>     length = 14<br>tcp_session = 192.168.12.159:24679 -&gt; 192.168.12.131:6443<br> source_mac = d6:06:10:3e:e9:ed<br>   dest_mac = 56:1c:4b:ac:c3:15<br>  protocols = [Ethernet, IP, TCP]<br>    payload = \x00\x00\x01\x01\x08\x0A\xAE\x87H\xD5\xBC\xF6d\x12<br>  tcp_flags = [ACK]<br>tcp_seq_num = 1817817319</pre><pre>  timestamp = 2024-08-31 16:56:25<br>  source_ip = 192.168.12.159<br>    dest_ip = 192.168.12.131<br>source_port = 24679<br>  dest_port = 6443<br>     length = 14<br>tcp_session = 192.168.12.159:24679 -&gt; 192.168.12.131:6443<br> source_mac = d6:06:10:3e:e9:ed<br>   dest_mac = 56:1c:4b:ac:c3:15<br>  protocols = [Ethernet, IP, TCP]<br>    payload = \x00\x00\x01\x01\x08\x0A\xAE\x87H\xD5\xBC\xF6d\x12<br>  tcp_flags = [ACK]<br>tcp_seq_num = 1817817319</pre><p>Originally published on <a href="https://blog.mdfranz.com/">https://blog.mdfranz.com/</a></p><p>&lt;script defer data-domain=”mdfranz.medium.com” src=”<a href="https://plausible.io/js/script.js">https://plausible.io/js/script.js</a>&quot;&gt;&lt;/script&gt;</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=142710c1e2ae" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Moving Day]]></title>
            <link>https://mdfranz.medium.com/moving-day-d74916389c81?source=rss-253c4f2461ff------2</link>
            <guid isPermaLink="false">https://medium.com/p/d74916389c81</guid>
            <category><![CDATA[cms-development]]></category>
            <category><![CDATA[blog]]></category>
            <dc:creator><![CDATA[Matt Franz]]></dc:creator>
            <pubDate>Sun, 16 Jun 2024 00:38:06 GMT</pubDate>
            <atom:updated>2024-06-16T00:38:06.736Z</atom:updated>
            <content:encoded><![CDATA[<p>💥 I’ve moved my content over to <a href="https://mdfranz.ghost.io/">https://mdfranz.ghost.io/</a> 👋</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TGRxhqYEpP6gdIhc6ePa8g.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d74916389c81" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>