You are browsing as a guest. Sign up (or log in) to start making projects!

yuv1

@yuv1

Joined July 8th, 2026

  • 2Devlogs
  • 2Projects
  • 1Ships
  • 15Votes
Open comments for this post

2h 33m 41s logged

Devlog 1: Trawl, and the bug I caught before it cost me a week

Trawl is a CTF toolkit that runs in the browser. Drop a file in, it runs every check I’ve written at once, and tells you what looked wrong. Nothing uploads anywhere, because there is no server to upload to.

It did not start out this big. The first version was only a steganography tool, because that is the category I keep landing in and the workflow is tedious: exiftool, then strings, then binwalk, then zsteg, then StegSolve, a Java applet from 2011. Five tools, five interfaces, all of them installed before you can look at a single PNG. Plenty of beginners skip forensics entirely for that reason.

A couple of weeks in I realized the same complaint covers almost every CTF category, so the scope moved. Trawl is the hub. Cuttlefish is the steganography module inside it, and the only one that exists so far. A trawl is a net you drag through water to see what comes up. Cuttlefish hide by rewriting their own surface, which is what LSB embedding does to an image.

What Cuttlefish runs now

Eight checks, PNG only: pixel decode with verification, chunk walk, CRC validation, post-IEND data, tEXt/zTXt/iTXt metadata, ASCII strings, flag-shape scan, and an LSB sweep across channel order, bit plane and bit order.

In the screenshot one of the eight fired. The sweep pulled testCTF{hello} out of RGB, bit 0, MSB first. One of 42 combinations carried anything readable.

The bug I would not have found without testing for it

Before writing any analysis I wrote a single test: build a PNG with a known pattern in the low bits, decode it through the browser, check the bits survive.

They don’t, by default. Canvas alpha premultiplication and colour management both rewrite pixel values slightly. For ordinary image work nobody notices. For steganography it destroys exactly the data you are looking for, and every detector downstream would have been measuring noise while looking completely functional.

The fix is passing colorSpaceConversion: 'none' and premultiplyAlpha: 'none' to createImageBitmap. Cuttlefish now reports the decode step as verified so I can confirm the guarantee held for the file in front of me. Had I built the sweep before checking, I would have spent days debugging math that was already correct.

On the Rust

The core is Rust compiled to WASM, running in a Web Worker, partly because the sweep is real compute and JavaScript stutters on it, partly because I wanted to learn Rust properly. I wrote the detector logic myself. AI helped me refactor it and add comments, which I am noting because it is true, and because the parts I struggled through are the parts I actually understand now.

Next

The bit-plane wall: all 8 planes across every channel rendered at once, so an anomalous plane becomes something you see rather than something you brute-force into.

Cuttlefish is not finished. The sweep only handles PNG, there is no bit-plane view, and no statistical detection at all yet. What I have proves the pipeline runs end to end on a flag I planted myself, which is a real milestone and also not much more than that. Trawl has exactly one module, which makes “hub” a generous word for it right now.

0
0
32
Ship Pending review

What did you make?

EzRegex is a regex playground that explains patterns in plain English while you type.
You enter a pattern, paste in some text, and it highlights matches live while breaking
the pattern into chunks with a card explaining each one. Hover a chunk and its
explanation lights up, and the other way around too.

The idea came from FBLA. I was competing in Mobile Application Development and one of
the rubric criteria was input validation. I knew regex and I was using it, but when I
sat down to explain it to a judge who might not be technical, I couldn’t. It took me a
while to work out a one line explanation that actually landed. A few weeks later I’d
forgotten it.

That stuck with me. If I had to fight that hard to explain it once, what about
developers who touch regex every day? Nobody is memorizing those patterns. And the
stakes aren’t small. Input validation is what stands between a login form and a SQL
injection. It’s the thing you skip when it’s annoying, and skipping it is how sensitive
pages break.

So I built the tool I wished I’d had while prepping for that event.

What was challenging?

The tokenizer. I went in thinking “split the regex up, write a description for each
piece, done.” That is not what it is. I ended up writing the front end of a small
compiler: walking the pattern character by character, holding state, deciding where one
token ends and the next begins, handling escapes inside character classes where the
rules change, attaching quantifiers backwards onto whatever they modify.

A few rules ate hours on their own. A ] as the first character inside a character
class is a literal ], not a closing bracket. A hyphen at the start or end of a class
is literal, but in the middle it’s a range. {2,6} never stands alone, it belongs to
the token before it. Every one of those I had to discover and then handle.

Rendering had its own problem. The highlighting is a transparent textarea sitting on top
of a div, and if the two layers wrap text even slightly differently, the highlights
slide off the words. Getting both layers to agree on every metric that affects wrapping
took longer than I want to admit.

What are you proud of?

The tokenizer is mine. I used AI for scaffolding and boilerplate and I say so openly in
the repo, but the parser is hand written and I understand every line of it. That was the
whole point of building this instead of just using regex101.

I’m also proud that I scoped it. Recursive group expansion would have been cool. I cut
it, wrote down why in the roadmap, and shipped. Knowing what isn’t in v1 turned out to
matter as much as knowing what is.

How to test it

Open https://ezregex.vercel.app. There’s no signup or install, and nothing you type
leaves your browser.

If you don’t have a pattern in mind, use the examples panel on the left. There are 29 of
them across validation, extraction, cleanup, and code. Click one and it loads with a
test string that actually produces matches.

Worth trying:

  • Type your own pattern and watch the breakdown update on every keystroke
  • Hover a coloured chunk in the pattern to see its card highlight, then hover a card to
    see the chunk highlight back
  • Break a pattern on purpose ([a- works) to see the error handling
  • Toggle the flags and switch themes
  • Shrink the window under 768px, the panels turn into tabs and the examples become a
    drawer

Source and build notes: https://github.com/Yuv1s/EzRegex

  • 1 devlog
  • 1h
Try project → See source code →
Open comments for this post

46m 53s logged

Devlog - rebuilding the EzRegex UI

Why

The app worked. It just looked generated.

The tell was the colour. Everything was indigo; buttons, focus rings, flag pills, match highlights, links, scrollbars. Then a violet, an emerald, a sky and an amber for the example categories, and eight more hues for the regex tokens. About a dozen colours, none of them meaning anything.

The rule

Colour is treated as a scarce resource and spent only where it carries meaning.

Everything else went neutral. What survived:

Colour Means Ochre a match, a selection, focus Red an invalid pattern Pink / green / blue / cyan the four regex token families Neutral everything else

Category badges lost their colours. Token types went from eight hues down to four, grouped by what they do rather than one colour each.

The change I like most: plain literals are uncoloured now. They’re most of any pattern, and colouring them is what turns a regex display into confetti. Greying them out is what made the other four colours mean something.

Also changed

  • Custom warm-grey ramp in OKLCH instead of Tailwind’s stock gray-*
  • Geist instead of Inter
  • The pattern field moved to its own full-width band — it’s the point of the app, it shouldn’t be one item in a column
  • The match count is visible. It existed before, but only as sr-only text

Bugs I hit on the way

  • Highlights drifted off the text. The textarea and the layer behind it wrapped differently, and a trailing newline made the backdrop one line short
  • /a*/g on a big paste could hang the page. Capped at 5,000 matches
  • []] was described as "["
  • The explainer said: Matches the literal character “the @ symbol”
  • You could tab into the off-screen mobile drawer. It also didn’t trap focus or close on Escape
  • The PWA manifest was untouched boilerplate — the app installed as “MyWebSite”

The part I nearly got wrong

I wrote a script to check every text-on-background pair against WCAG AA instead of trusting my eyes.

The first palette failed 7 checks. A muted label was at 2.75:1 where it needed 4.5:1. Button text was at 4.02:1. All of it looked fine on my monitor.

58 pairs pass now. One visible cost: tertiary text sits closer to secondary than I’d like, because anything lighter fails. Size and weight carry the hierarchy instead.

0
0
2

Followers

Loading…