<?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"><channel><title><![CDATA[Nartan Naik]]></title><description><![CDATA[Nartan Naik]]></description><link>https://nrtnnaik.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 11:22:17 GMT</lastBuildDate><atom:link href="https://nrtnnaik.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building CyberCube: An AI-Powered SOC Platform from Packets to Alerts]]></title><description><![CDATA[How I built CyberCube, an AI-powered SOC platform that parses PCAPs, flags threats with Random Forest, and explains alerts with SHAP and MITRE ATT&CK.]]></description><link>https://nrtnnaik.hashnode.dev/building-cybercube-soc-platform</link><guid isPermaLink="true">https://nrtnnaik.hashnode.dev/building-cybercube-soc-platform</guid><category><![CDATA[cybersecurity]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[Python]]></category><category><![CDATA[FastAPI]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Nartan Naik]]></dc:creator><pubDate>Mon, 02 Nov 2026 12:21:46 GMT</pubDate><content:encoded><![CDATA[<p>Most security tools tell you something is wrong. A good one also tells you why, and what to do next. CyberCube is a security operations (SOC) platform I built to do all three: it reads network traffic, flags threats with a machine-learning model, and explains each alert well enough for an analyst to act on it.</p>
<h2>Where it started</h2>
<p>CyberCube began as our major project. We could have built another full-stack app, but we wanted to try something different, so we moved into cybersecurity as an experiment. This post covers what we built and what its numbers do and do not mean.</p>
<h2>From packets to alerts</h2>
<p>Traffic can enter the platform in two ways:</p>
<ul>
<li><strong>PCAP upload.</strong> You drag a packet-capture file into the web app. A parser I wrote unpacks the binary data field by field, reading the Ethernet, IP and TCP headers by hand. It then groups packets into two-way flows and runs the model on each one.</li>
<li><strong>Live capture.</strong> The sniffer tries to capture packets directly from the machine's network interface. That needs administrator privileges. When they are missing, it falls back to generated traffic so the live view keeps working. That fallback is meant for demos, not as real capture.</li>
</ul>
<p>Detected threats appear on a React dashboard through a WebSocket connection (<code>/ws/threats</code>), so new alerts arrive without refreshing the page. The dashboard includes alert tables and a global threat map that plots where attackers appear to come from.</p>
<h2>The dataset, and an honest caveat</h2>
<p>Training a detection model needs labelled traffic. I generated a dataset of 12,000 records with 22 features, designed to follow the CIC-IDS2017 schema, and included attacker locations and MITRE ATT&amp;CK labels. <strong>The data is synthetic.</strong> That means the accuracy numbers below show the pipeline works from end to end, but they are not a claim about performance on real-world traffic.</p>
<h2>Choosing a model</h2>
<p>I needed a classifier that is accurate, fast, and explainable, because analysts will only act on alerts they trust. I benchmarked four on an 80/20 split of the data (2,400 test samples):</p>
<ul>
<li><strong>Gradient Boosting</strong> matched the accuracy but was about 1.8 times slower, roughly 30 microseconds per prediction against 17.</li>
<li><strong>The neural network</strong> was faster but less accurate, and hard to explain.</li>
<li><strong>Logistic Regression</strong> was the fastest but the least accurate.</li>
<li><strong>Random Forest</strong> gave the best balance.</li>
</ul>
<p>I deployed Random Forest: about <strong>99.5% accuracy</strong>, <strong>99.37% macro-F1</strong>, around <strong>17 microseconds</strong> per prediction, and explanations that show why each flow was flagged.</p>
<h2>Explaining every alert</h2>
<p>A label like "DDoS, 98% confidence" is not enough for an analyst, so each alert comes with more:</p>
<ul>
<li><strong>SHAP waterfall charts.</strong> SHAP shows which features of a flow, such as the timing between packets or the count of TCP flags, pushed the model toward "attack" or "benign".</li>
<li><strong>MITRE ATT&amp;CK mapping.</strong> Each threat type is tagged with a standard technique ID. For example, DDoS maps to T1498, port scans to T1046, brute force to T1110, web attacks to T1190 and botnet traffic to T1071.</li>
<li><strong>Plain-language summaries.</strong> The Gemini API turns an alert into a short root-cause explanation, sent through a backend proxy so the key stays on the server.</li>
</ul>
<h2>From detection to response</h2>
<p>CyberCube also helps you act:</p>
<ul>
<li><strong>Firewall rules.</strong> One click generates commands to block an attacker's IP: <code>netsh advfirewall</code> for Windows, <code>iptables</code> for Linux, and rules for Suricata and Snort.</li>
<li><strong>PDF incident reports.</strong> A report contains the severity, attacker location, MITRE technique, all 22 flow metrics, the SHAP-based root cause, and a space for the analyst's signature.</li>
</ul>
<h2>Multi-tenancy and deployment</h2>
<p>Several organizations can use the same platform without seeing each other's data. Users sign in with Google OAuth, and every threat record carries a tenant key derived from the user's email using HMAC. Session tokens are signed and expire after 12 hours, and the WebSocket only delivers alerts that match the connected user's key.</p>
<p>The backend is FastAPI with SQLAlchemy, using SQLite by default and PostgreSQL (Neon) in production. A script injects sampled flows from the dataset into the system on a schedule, run by GitHub Actions, which keeps the dashboard populated. The app is deployed on Render and Vercel.</p>
<h2>What I would improve next</h2>
<ul>
<li>Train and test on real CIC-IDS2017 data and compare it with the synthetic results.</li>
<li>Test the live sniffer on real network traffic instead of relying on the demo fallback.</li>
<li>Add more attack types beyond the five currently mapped.</li>
<li>Write automated tests for the PCAP parser and the prediction pipeline.</li>
</ul>
<h2>Takeaways</h2>
<p>A detection model is only useful if an analyst can trust it and act on its output. The explanations, the standard threat labels and the ready-made firewall rules were as much of the project as the classifier itself. And being clear about what synthetic data can and cannot prove makes the results more believable.</p>
<ul>
<li>Live app: <a href="https://ai-powered-cyber-threat-detection-soc.onrender.com">ai-powered-cyber-threat-detection-soc.onrender.com</a></li>
<li>Code: <a href="https://github.com/NartanNaik/CyberCube">github.com/NartanNaik/CyberCube</a></li>
<li>Stack: Python, Scikit-learn, FastAPI, React, PostgreSQL, SHAP, Gemini API</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Cutting LLM Input by 98%: How I Built Unpack]]></title><description><![CDATA[How I built Unpack, an AI tool that turns codebases into architecture docs, and a custom scanner that cut LLM input from 6.45M to ~125K tokens.]]></description><link>https://nrtnnaik.hashnode.dev/how-i-built-unpack</link><guid isPermaLink="true">https://nrtnnaik.hashnode.dev/how-i-built-unpack</guid><category><![CDATA[llm]]></category><category><![CDATA[Python]]></category><category><![CDATA[FastAPI]]></category><category><![CDATA[React]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Nartan Naik]]></dc:creator><pubDate>Mon, 02 Nov 2026 12:18:09 GMT</pubDate><content:encoded><![CDATA[<p>Every time I wanted an AI's help with one of my projects, I had to explain the project from scratch: the file structure, how the pieces connect, what I had changed since last time. It took a lot of time, and the explanation was never fully accurate. Unpack is the tool I built to stop doing that. You give it a codebase, and it streams back architecture documentation.</p>
<h2>The problem: a codebase does not fit in a prompt</h2>
<p>The obvious fix is to send the whole repository to an AI model. That fails quickly. On a large raw repository, the input came to about 6.45 million tokens, far beyond any model's context window. My early attempts with a big project like CyberCube ended with an AI telling me it was too huge and to try a smaller one.</p>
<p>That is not a useful answer for a tool whose job is to understand big projects. So the real problem became deciding what the model should never see.</p>
<h2>The scanner: cutting 98% of the noise</h2>
<p>Most of a repository is not worth reading. Build artifacts, binaries, lockfiles and generated files add size and no understanding. I wrote a custom scanner in Python that:</p>
<ul>
<li>Skips ignored directories, file names and extensions (build output, binaries, lockfiles).</li>
<li>Enforces size caps on individual files, on the raw upload and on the final text sent to the model.</li>
<li>Ranks files by importance so the most useful code is kept when the cap is reached.</li>
</ul>
<table>
<thead>
<tr>
<th></th>
<th>Tokens</th>
</tr>
</thead>
<tbody><tr>
<td>Raw repository</td>
<td>6.45M</td>
</tr>
<tr>
<td>After scanning</td>
<td>~125K</td>
</tr>
</tbody></table>
<p>That is a 98% reduction in model input, and it removes the context-window failures.</p>
<h2>Three ways to bring in code</h2>
<ul>
<li><strong>Folder picker.</strong> The browser reads the folder directly. The files are filtered on the client side first, so less data is uploaded, and then zipped with JSZip.</li>
<li><strong>ZIP upload.</strong> The archive is opened in the browser, filtered, and zipped again.</li>
<li><strong>GitHub URL.</strong> The backend fetches a public repository, up to a size limit, and processes it the same way.</li>
</ul>
<h2>Four depths of analysis</h2>
<p>Not every question needs a 20-page report, so Unpack offers four analysis depths. Each uses its own prompt and token budget, and the backend picks a model to match:</p>
<ol>
<li><strong>Quick Summary</strong>: a short overview of what the project is.</li>
<li><strong>Standard Architecture</strong>: the main components and how they connect.</li>
<li><strong>Deep Dive</strong>: everything: stack, data flow, schema, file breakdown.</li>
<li><strong>Diagnose</strong>: a full audit that also lists security and technical-debt issues.</li>
</ol>
<h2>Streaming the answer</h2>
<p>A detailed analysis takes a while to generate, so the backend streams it back as newline-delimited JSON events instead of making you wait for one big response. There are four event types: <code>delta</code> carries new text, <code>ping</code> shows the model is still working, <code>done</code> returns the final report, and <code>error</code> reports failures. On the frontend, a small XMLHttpRequest helper tracks both the upload and the streaming response, so the user sees live progress.</p>
<h2>Accounts and cost control</h2>
<p>Users sign in with Google or GitHub through Supabase Auth. The first time someone logs in, the backend validates their session, creates an OpenRouter key for that user capped at $0, and stores it in Supabase along with a SHA-256 hash of the key. The key is tied to the user's email, so each person gets their own.</p>
<p>A $0 cap means free models only, so the app cannot produce a surprise bill, and one person's usage cannot use up another's allowance.</p>
<p>Users can also choose between two modes: <strong>Temp</strong>, where the analysis is not saved, and <strong>Persistent</strong>, where it is stored in a history table in Supabase so they can come back to it.</p>
<h2>Exporting in the browser</h2>
<p>Reports can be downloaded as PDF, DOCX or PPTX. All three are generated entirely in the browser, with jsPDF, docx and pptxgenjs, so nothing goes through the server. Every export has the same branding: a purple and gold watermark, an UNPACK footer and page numbers.</p>
<p>Nothing in this project was dramatically hard, but the PDF was the fiddliest, and generating the watermark for all three formats took more time than I expected.</p>
<h2>Does it work?</h2>
<p>Yes, and I can prove it with my own projects. The architecture notes I keep for CreatorLens AI and CyberCube were generated by Unpack. They include the system architecture, tech stack, database schema, features, file breakdown and a list of technical-debt issues, and that is exactly what I built it to produce.</p>
<h2>What I would improve next</h2>
<ul>
<li>Add rate limiting to the key-provisioning and analysis endpoints.</li>
<li>Redact secrets such as API keys from source files before they reach the model.</li>
<li>Add automated tests, starting with the scanner's filtering rules.</li>
<li>Validate uploaded archives more strictly, beyond checking their size.</li>
</ul>
<h2>Takeaways</h2>
<p>With LLM apps, deciding what not to send matters as much as the prompt. Filtering the input removed 98% of the tokens and made large repositories workable, and it came from understanding what a model actually needs to see to describe a project.</p>
<ul>
<li>Live app: <a href="https://unpack-tau.vercel.app">unpack-tau.vercel.app</a></li>
<li>Code: <a href="https://github.com/NartanNaik/Unpack">github.com/NartanNaik/Unpack</a></li>
<li>Stack: React, FastAPI, Python, Supabase, OpenRouter</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[From Copy - Pasting Usernames to One - Click Imports: Building CreatorLens AI]]></title><description><![CDATA[How I built CreatorLens AI, a Flutter, React and Firestore app that imports Instagram creators in bulk, and got past Vercel's 10-second limit.]]></description><link>https://nrtnnaik.hashnode.dev/building-creatorlens-ai</link><guid isPermaLink="true">https://nrtnnaik.hashnode.dev/building-creatorlens-ai</guid><category><![CDATA[serverless]]></category><category><![CDATA[Vercel]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Nartan Naik]]></dc:creator><pubDate>Mon, 02 Nov 2026 12:13:12 GMT</pubDate><content:encoded><![CDATA[<p>I wanted to store a lot of Instagram creators in a database, along with details I cared about, like their nationality and other attributes. The first version of the workflow was slow, and I spent the next few months making it faster. CreatorLens AI is the result: a platform to ingest, enrich and curate creator profiles, now managing 2,377 of them.</p>
<h2>Step one: pasting usernames by hand</h2>
<p>I started by pasting each creator's username into the app one at a time. That worked for ten profiles and became painful at a hundred.</p>
<p>So I added <strong>mass upload</strong>: collect usernames in a list, then add them all at once. It was faster, but I was still filling in every attribute myself, and that took even longer than collecting the names.</p>
<h2>Step two: letting AI fill in the details</h2>
<p>I connected the Gemini API so I no longer had to type attributes like nationality by hand. The AI reads the categories I have defined and fills them in for each profile. The part I like most is that if I add a new column, the AI fills that one in too, so the schema is not hard-coded.</p>
<h2>Step three: a mobile app with a share button</h2>
<p>The workflow was still slow because I had to open the web app and enter things. So I built a <strong>Flutter mobile app</strong> with a built-in share target. When I find a creator's reel, I share it to my app the same way I would send a reel to a friend, and it lands in the website's queue. The app signs in with Google, stores tokens securely on the phone, and sends the URL to the REST API with a bearer token.</p>
<p>It was better, but each share still took more than 5 seconds from tapping share until the app closed itself.</p>
<h2>Step four: the Garbage Eater</h2>
<p>While working on the app I had been saving reels, and I realised those saved reels were already a list of everything I wanted to add. So I used a scraper to export all my saved reels as a JSON file, then built a feature to paste that JSON in and import everything at once. I named it the <strong>Garbage Eater</strong>, because I throw a big messy file at it and it sorts everything out.</p>
<p>Now my whole job is to save a reel and keep scrolling. Everything else happens afterwards, with each reel taking under a second in my own use.</p>
<p>CreatorLens now has four ways to add profiles:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>How it works</th>
</tr>
</thead>
<tbody><tr>
<td>Manual entry</td>
<td>Add one profile by hand</td>
</tr>
<tr>
<td>Mass upload</td>
<td>Paste a list of usernames</td>
</tr>
<tr>
<td>Mobile share</td>
<td>Share a reel from Instagram to the Flutter app</td>
</tr>
<tr>
<td>Garbage Eater</td>
<td>Import a JSON export of saved reels</td>
</tr>
</tbody></table>
<h2>The staging pipeline</h2>
<p>Raw URLs do not go straight into the main database. They pass through a <strong>staging queue</strong>, which is a two-step pipeline:</p>
<ol>
<li><strong>Queue.</strong> A shared reel (URL, username, thumbnail, caption) is saved in a staging collection and the request returns immediately.</li>
<li><strong>Process.</strong> A separate request enriches that item: it fetches metadata such as followers, likes, comments and views, and runs the AI step. After that, I approve the item and it moves into the main profiles collection.</li>
</ol>
<p>Splitting it this way means no single request has to do everything, and it lets me review items before they become real profiles.</p>
<h2>Hitting Vercel's 10-second limit</h2>
<p>I first hit Vercel's 10-second function limit with the Garbage Eater. Small files were fine, but a large JSON file took too long to process in one request, and the request was cut off.</p>
<p>The fix was to break the work into batches. The importer now sends items in <strong>chunks of at most 50 per request</strong>, and the staging pipeline processes them separately, so each request stays well inside the limit. The batches are throttled as well, so a big import does not flood the database.</p>
<h2>Protecting a free-tier database</h2>
<p>I run on Firestore's free tier, so every read and write counts against a daily quota. Three things keep usage down:</p>
<ul>
<li><strong>A Bloom filter</strong> is a small in-memory structure that answers "have I seen this username before?" without touching the database. It can say "probably yes" by mistake, but it never says "no" for something it has seen, which makes it a safe quick check. Because it skips a database read, it is faster and saves quota.</li>
<li><strong>Per-route rate limiting</strong> caps how often each group of endpoints can be called. For example, mass upload allows 60 requests a minute, bulk actions 30, and the video refresh job only 5.</li>
<li><strong>Chunked, batched writes</strong> group many writes into transactions instead of sending one write per profile.</li>
</ul>
<p>The Bloom filter has a limit I should be honest about: it lives in memory on each running server instance and is not loaded from the database. After a restart, or on another instance, it starts empty, so it reduces duplicates and database reads but does not guarantee zero duplicates.</p>
<h2>A security detail worth sharing: column names</h2>
<p>Because users can create custom columns, those column names end up in database queries. Firestore treats a dot in a field name as a path into nested data, so a malicious name could write to places it should not reach. A validation step now allows only letters, numbers, underscores and hyphens in column names, and I added middleware that blocks NoSQL and dot-notation injection before any input reaches a query.</p>
<p>The rest of the API follows the same careful approach:</p>
<ul>
<li>Short-lived JWTs with refresh-token rotation, so a leaked token expires quickly.</li>
<li>AES-256-GCM encryption for sensitive fields.</li>
<li>Google OAuth sign-in with per-user data isolation, so no one can see another user's profiles.</li>
</ul>
<h2>What I would improve next</h2>
<ul>
<li>Share duplicate checks across server instances, so deduplication survives restarts.</li>
<li>Add automated tests. The project currently has none, and the importer and staging logic would benefit most.</li>
<li>Stream-parse very large JSON files instead of loading them whole.</li>
<li>Move long-running enrichment to a proper background job queue.</li>
</ul>
<h2>Takeaways</h2>
<p>Most of this project was removing the slowest step, one step at a time: first typing, then pasting, then AI, then the phone, and finally the importer. A serverless time limit does not have to block you if you split the work into small, separate requests.</p>
<ul>
<li>Live app: <a href="https://createrlens.vercel.app">createrlens.vercel.app</a></li>
<li>Code: <a href="https://github.com/NartanNaik/CreaterLens">github.com/NartanNaik/CreaterLens</a></li>
<li>Stack: React, Node.js, Express, Firebase Firestore, Flutter, Vercel, Gemini API</li>
</ul>
]]></content:encoded></item></channel></rss>