<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Keelen Carrera</title>
    <description>The latest articles on DEV Community by Keelen Carrera (@keelencarrera).</description>
    <link>https://dev.to/keelencarrera</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F894738%2Fd1d8ff4d-4422-46d6-94ad-1a98312637e5.jpeg</url>
      <title>DEV Community: Keelen Carrera</title>
      <link>https://dev.to/keelencarrera</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/keelencarrera"/>
    <language>en</language>
    <item>
      <title>From Backend to Breach-Aware - Part 1: My CI Said "No Leaks Found." I Verified It Anyway.</title>
      <dc:creator>Keelen Carrera</dc:creator>
      <pubDate>Tue, 13 Oct 2026 19:59:11 +0000</pubDate>
      <link>https://dev.to/keelencarrera/from-backend-to-breach-aware-part-1-my-ci-said-no-leaks-found-i-verified-it-anyway-89p</link>
      <guid>https://dev.to/keelencarrera/from-backend-to-breach-aware-part-1-my-ci-said-no-leaks-found-i-verified-it-anyway-89p</guid>
      <description>&lt;p&gt;Continuing from the part of my series that explains the program that I helped create with a team of developers, here I pressed forward with my discovery into software engineering and cybersecurity, where I wired three security scans; dependency (SCA), static analysis (SAST), and secrets, into GitHub Actions for a codebase I'd already hand-scanned once. All three came back clean, and that should have been the end of it. Instead, I made myself answer a harder question: does "no leaks found" actually mean no leaks exist, or does it just mean the scanner didn't find any &lt;em&gt;the way it's currently configured&lt;/em&gt;? We ought to be aware that these are not the same claim, and treating them as interchangeable is how a real gap survives behind a passing pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the scanner instead of trusting it
&lt;/h2&gt;

&lt;p&gt;I ran two constructed AWS-key test cases through gitleaks to see whether its detection actually behaved the way I assumed.&lt;/p&gt;

&lt;p&gt;The first was AWS's own documentation placeholder, &lt;code&gt;AKIAIOSFODNN7EXAMPLE&lt;/code&gt;. It didn't get flagged, which was by design. Gitleaks' default config carries a rule-level allowlist that exempts any match ending in &lt;code&gt;EXAMPLE&lt;/code&gt;, a deliberate carve-out for AWS's own docs. This was worth knowing, but overall not alarming on its own.&lt;/p&gt;

&lt;p&gt;The second was a key I fabricated myself, &lt;code&gt;AKIAABCD1234EFGH5678&lt;/code&gt;. This one &lt;em&gt;should&lt;/em&gt; have tripped the AWS-specific rule. Instead, it was only caught by the generic &lt;code&gt;generic-api-key&lt;/code&gt; rule. This was a real signal that something was off. Digging into the why led somewhere I didn't expect: this repo's CI, using &lt;code&gt;gitleaks-action@v2&lt;/code&gt; with no &lt;code&gt;GITLEAKS_VERSION&lt;/code&gt; pinned, was silently running &lt;strong&gt;gitleaks 8.24.3&lt;/strong&gt;. That version predates a regex-tightening fix (merged 2025-05-27) that restricts the AWS key charset to match how AWS actually encodes tokens. Checking the Action's own source confirmed the root cause: both &lt;code&gt;v2&lt;/code&gt; and current &lt;code&gt;v3.0.0&lt;/code&gt; hardcode the same fallback version string. As a result, the Action's major version tag has nothing to do with which gitleaks binary actually runs underneath it.&lt;/p&gt;

&lt;p&gt;The honest version of the finding isn't "we were about to leak an AWS key, and gitleaks would've missed it"; the prefix regex was actually &lt;em&gt;looser&lt;/em&gt;, so that specific miss didn't happen. The real exposure was quieter and worse: over a year of unrelated rule improvements, new token formats for entirely different services, that this pipeline had never picked up, with nothing in place that would have surfaced it short of deliberately going and checking. I fixed it by pinning the Action to an exact tag and setting &lt;code&gt;GITLEAKS_VERSION&lt;/code&gt; explicitly, then verified against a live CI run showing the correct version and a clean scan against the actual diff, not just "trust the new config"; I confirmed it did what it was supposed to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake that shipped anyway
&lt;/h2&gt;

&lt;p&gt;The same pull request that carried this fix also carried something that should never have reached &lt;code&gt;main&lt;/code&gt;: a deliberately vulnerable test fixture, an &lt;code&gt;eval()&lt;/code&gt; on unsanitized input, a pinned-vulnerable dependency, a fake AWS key which was built specifically to confirm the CI jobs actually &lt;em&gt;fail&lt;/em&gt; on bad input. That commit got merged anyway, with 2 of the 3 security jobs failing. GitHub allowed it because this repo had no required-status-check branch protection on &lt;code&gt;main&lt;/code&gt;; nothing automated was actually gating the merge button. I only caught it by manually diffing the branch against &lt;code&gt;main&lt;/code&gt; before treating the PR as done. Fixed with a straight revert, and all three jobs are green again in the corrected state. After the fix, I decided to leave that mistake in the write-up instead of quietly cleaning the history, because "diff the PR before you trust it" is a more convincing demonstration of the whole point than any scanner finding above it. Also, it exposed a real, still-open gap: no required checks on &lt;code&gt;main&lt;/code&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a clean SAST result isn't a gap, either
&lt;/h2&gt;

&lt;p&gt;This is the same fixture, but a different tool: semgrep reported &lt;strong&gt;zero findings&lt;/strong&gt; on a file containing a literal &lt;code&gt;eval(userInput)&lt;/code&gt; call. Before assuming that was a miss, I pulled the actual community rule (&lt;code&gt;eval-detected&lt;/code&gt;) and ran it locally against five constructed variants. The pattern that emerged: semgrep performs constant propagation before matching, so when &lt;code&gt;userInput&lt;/code&gt; is assigned a literal string and never reassigned, &lt;code&gt;eval(userInput)&lt;/code&gt; resolves to the equivalent of &lt;code&gt;eval("literal")&lt;/code&gt;, which the rule's own exemption is written to treat as safe, deliberately, since a hardcoded literal isn't attacker-controlled. My test fixture's shape happened to land exactly on the one form of "bad eval" this rule is built to wave through. Not a tool gap, not a misconfiguration. It was a lesson that a hand-built adversarial test case needs the same scrutiny as a real one before its result counts as verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual takeaway
&lt;/h2&gt;

&lt;p&gt;So, none of these is really about gitleaks or semgrep specifically. It's that a passing CI job is a claim, not a fact, and claims are worth checking against the thing that actually produced them: the rule source, the pinned tool version, and the diff that got merged. These all were before they get treated as evidence of anything. That standard doesn't stop at the code under test; it applies to the pipeline testing it as well. It's also the same lesson I first ran into on a much smaller scale, building this app's inventory logic in the first place. A response coming back successfully and a response coming back correct turned out to be the same distinction, just at a different layer. (That story's Part 0 of this series, if you want the origin.)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full technical write-up with commit links: &lt;a href="https://github.com/Keelen-Carrera/product-security-labs/blob/main/dine-flow-management-service/lab-2-ci-pipeline.md" rel="noopener noreferrer"&gt;product-security-labs&lt;/a&gt;. Target codebase: &lt;a href="https://github.com/allaboutmike/smart-kitchen-mgmt" rel="noopener noreferrer"&gt;smart-kitchen-mgmt&lt;/a&gt; (original team project).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>github</category>
      <category>testing</category>
    </item>
    <item>
      <title>From Backend to Breach-Aware Part 0: The Router That Was Built</title>
      <dc:creator>Keelen Carrera</dc:creator>
      <pubDate>Tue, 08 Sep 2026 21:04:32 +0000</pubDate>
      <link>https://dev.to/keelencarrera/from-backend-to-breach-aware-part-0-the-router-that-was-built-3i3m</link>
      <guid>https://dev.to/keelencarrera/from-backend-to-breach-aware-part-0-the-router-that-was-built-3i3m</guid>
      <description>&lt;p&gt;Dine Flow was a six-person cohort project, a restaurant inventory and order-management platform, built in about six weeks. Six people means six sets of hands in the codebase, and I want to be specific about which hands built what, rather than let a team project blur into a vague personal credit. My piece was the Express/API backend: the routers and the backend logic for stock, menu items, and orders. The strongest, most fully-owned piece of that work was the stock router, so that's what this is actually about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the stock router does
&lt;/h2&gt;

&lt;p&gt;The stock router (Express + TypeScript, backed by Prisma/PostgreSQL) manages the inventory that everything else in the app depends on; every ingredient a kitchen has on hand, tied to expiration dates, suppliers, and the orders that consume it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /&lt;/code&gt; returns the full ingredient/stock list with nested details, everything a kitchen manager needs to see current inventory at a glance. &lt;code&gt;GET /: id&lt;/code&gt; returns a single stock item plus its associated ingredient name. &lt;code&gt;POST /&lt;/code&gt; is where the real logic lives: it creates a new stock order and calculates an expiration date from the ingredient's shelf life, then branches depending on whether that ingredient has a supplier API URL on file. If it doesn't, the route creates the stock record and a matching expense record directly. That branch was part of my work. If it does, the route fires an automated reorder request to the supplier's API instead.&lt;/p&gt;

&lt;p&gt;Inventory doesn't just sit static, either. Placing an order through the orders router automatically decrements the relevant stock, and stock can also be adjusted manually outside the order flow through the same router. The other deliberate choice here: &lt;code&gt;PUT /:ingredient&lt;/code&gt; creates a new one, since every delivery is its own batch with its own expiration date.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bug that changed how I read "it's fixed"
&lt;/h2&gt;

&lt;p&gt;Early on, the &lt;code&gt;GET /: id&lt;/code&gt; endpoint had a bug that's stuck with me since: requesting a single stock item was returning the &lt;em&gt;entire&lt;/em&gt; stock table instead of the one item asked for. The response wasn't actually an error; it was a 200 with data in it, which is exactly the kind of result that's easy to glance at and assume is fine. Tracing it back, the route wasn't actually referencing the ID variable it should have been keying off of; once I mapped it to the correct field (Prisma's &lt;code&gt;findUnique&lt;/code&gt; against the actual stock ID), it returned exactly what was requested. It's a small bug, but it's the first time I really internalized that a response coming back successfully and a response coming back &lt;em&gt;correct&lt;/em&gt; are two different claims, and only one of them is worth trusting without a second look. That instinct has followed me directly into the security-focused work I do now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I verified, and what I didn't
&lt;/h2&gt;

&lt;p&gt;Testing here was manual, hitting endpoints through VS Code's Thunder Client and watching browser console output during integration testing with the frontend. That's a full gap, and it's narrower on purpose: this piece is an honest account of the backend logic that was written.&lt;/p&gt;

&lt;p&gt;One more honest note, since the whole point of this post is not trusting a clean-looking response: the fix I described above only covers the wrong-ID bug. &lt;code&gt;GET /:id&lt;/code&gt; still has a commented-out 404 check as of today. Request an ID that doesn't exist, and it returns a 200 with null data instead of an error. Same failure shape as the bug this post is about; it is still sitting there to be fixed later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I went back
&lt;/h2&gt;

&lt;p&gt;That &lt;code&gt;GET /: id&lt;/code&gt; bug is the actual reason this became a series instead of a single post. A clean 200 hiding the wrong data is a very specific kind of lie, and is not necessarily an error. It's a result that looks fine until you check what's actually in it. Months later, I went back into this exact codebase and asked a different version of the same question: "Is this actually secure?" and "If I were to place scanning methods throughout the development process, what would it return?" A scanner reporting zero findings is the same shape of claim as an endpoint returning a 200, technically true, and not automatically the same thing as correct, or safe. That's where this series actually goes next: I wired dependency, static-analysis, and secrets scanning into this app's CI pipeline, then caught the pipeline itself lying to me differently. That will be Part 1.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Dine Flow: &lt;a href="https://github.com/allaboutmike/smart-kitchen-mgmt" rel="noopener noreferrer"&gt;github.com/allaboutmike/smart-kitchen-mgmt&lt;/a&gt; (original team repo). My fork, with later scan/remediation work: &lt;a href="https://github.com/Keelen-Carrera/smart-kitchen-mgmt" rel="noopener noreferrer"&gt;github.com/Keelen-Carrera/smart-kitchen-mgmt&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>backenddevelopment</category>
      <category>typescript</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
