Openport
- 8 Devlogs
- 47 Total hours
a Saas for cybersecurity
a Saas for cybersecurity
One headline: stamp v1.0. Not new scope, just a clean stamp on what already works, so a stranger can clone, install, run, add a domain, publish one TXT record, and get real reports every six hours. The owner added one rule above everything else: commit nothing and push nothing until they say the word, so every git step is prepared and held.
The box rebooted again, the fifth pickup like it, and both handoff processes died with it. Restoring them took one command each. The database wasn’t empty this time, since the owner had seeded two scans, and that led to the best surprise: the seed had made the target six hours stale, so on boot the scheduler enqueued a scan by itself. First scheduled rescan on the real clock since the cadence was proven, no rewind, no hand-inserted row, no request in the log before it. Four independent sightings of the due branch now.
The scratch got cleared: four places, thirteen lines, all dead comments, a duplicate export and an unused constant, with the test count at 56 before and after. The README got read as a stranger would read it, every claim checked against the code, and none lied. One omission fixed: a single line naming the Node version.
The domain question got priced again, and the baseline held on every corner. The cloud CLI lists thirteen projects, and every one answered that the DNS API is disabled. Enabling it and creating a zone is a billable action nobody takes on their own behalf, so two targets on one cadence and two scans at once stay pinned by a race test instead of watched live.
A fresh set of twelve screenshots came out clean, though the browser cache was gone again and had to be refetched. The changes feed still has never shown a populated state on live data: seven done scans, every diff empty, nothing staged to force one.
The release steps are queued in order, waiting for the word: scratch commit, README commit, version stamp, tag, push, release.
The oldest open question in the project closed: the six hour cadence fired on the real clock, no rewind, nothing inserted by hand. The scheduled scan appeared about a second after the mark, got claimed seven milliseconds later, finished in under four seconds, and stored the same empty diff as the one before it. Both feeds stayed honestly quiet. Every process watching it had died and the machine had been rebuilt around them, which didn’t matter, since the clock reads the database, not any process.
One small decision along the way: the politeness check stays on one stored address per target. Matching every address would need an address history table inside the claim, real machinery for round-robin hosts a small company rarely has, and politeness is a courtesy, not a correctness property.
Two lessons for the watching kit. Wiping the database with rm deafens any watcher, since it keeps polling the deleted file and the log just stops with no error, so restart it after every wipe, and after the migrate. And the production server doesn’t migrate, only dev does.
The gap that stands: two owned targets sharing the cadence, and two scans running at once, still pinned by a race test instead of watched live. Two more runs checked, and the second went deeper. No DNS credentials anywhere, then a cloud CLI turned up with two accounts, one with a dead token and one that sees ten projects with the DNS service disabled on every one. There’s a difference between no panel and a panel one billable step away, and enabling it is a purchase on someone’s account, not something to do unprompted.
Meanwhile the loop held every time: add the public test target, scan it, same four findings, port 80 splitting its personality between a 200 on the banner grab and a refused connection on the redirect check. It never flapped, so the populated changes feed has still never been seen on live data, and nothing was staged to force it.
Three jobs landed: a small worker pool with an atomic claim, retention finally observed deleting past its bound on a real run, and a decision about targets that never succeed. Still no second owned domain, so two scans running at once is pinned by a race test instead of watched live.
The pool is three workers, one plain constant. The whole design is the claim: one SQL statement picks a queued row and flips it to running in the same breath, so two workers can never take the same row. A test runs two real processes over one database file with twelve queued rows, and together they claimed all twelve, no duplicates. A politeness rule rides on top: a queued scan waits while any scan of the same target, or any target at the same address, is running, so one host never gets swept by two workers at once.
Watched it live: a burst of four scans on one host ran strictly one at a time, each starting within a millisecond of the previous finish. Boot recovery marked several stranded running rows failed at once, and a real kill mid-sweep recovered cleanly, the next queued scan picking up seconds later.
Retention had never deleted anything, since the real bound is a hundred. Lowered it to five for a soak, then restored it. The prune deleted exactly the five oldest done rows in the same instant, findings and diffs went with them through the cascade, failed rows were untouched, and neither feed noticed.
The never-succeeded decision: a target whose scans always fail used to be requeued every two seconds, forever, hammering a host that’s already failing. Now any finished scan resets the cadence, so a failing target is retried once per cadence like a healthy one. Cost, written down: a failed manual scan delays the next scheduled one.
One tooling incident: leftover watchers went deaf partway through, and my cleanup matched its own command line and killed a mix of shells and watchers. Watchers get a pidfile now.
Planned three things: catch the scheduler firing against a real clock, get a photo of a populated changes feed, and if there was room, add a retention limit to the scans table. The retention part landed clean. The clock-watching part did not, since the whole attempt crashed unexpectedly partway through and came back much later with only a partial trail left behind.
Retention is simple and real: keep the last hundred completed scans per target, prune right after a scan finishes, never let a failed cleanup corrupt a scan that actually succeeded. Deleting an old scan correctly drags its stored history with it, since nothing on screen ever looks back further than the limit already allows anyway.
The near miss is worth writing down. A log line looked exactly like proof the real scheduler behavior had finally been caught live. It was not, a different unrelated row fired for an unrelated reason, and it nearly got written up as the real finding before a second look caught it. Worse, the machine’s own clock had been wrong for the entire stretch, quietly off by a meaningful margin, so no timing claim from that window can be trusted. Every timing tool now stamps both the wall clock and a separate monotonic clock side by side, so drift shows up honestly instead of lying silently. Someone else picked up the real observation properly, on a trustworthy clock, the same day.
One real bug got caught and fixed regardless: a visual check of the live pages turned up literal broken source text rendering in production on every page, leftover from unrelated commits, plus a stray constant pasted into the middle of an unrelated import. Both fixed and disclosed rather than skipped past.
The actual lesson: the product held up fine through all of it. The environment around it, crashes, clock errors, lost caches, was what actually failed this time.
Two real jobs this time, since a monitor that rescans itself is useless if nobody can see what changed without opening every target by hand. Gave the changes feed a real home across all targets at once, and then verified the scheduler against an actual running clock instead of just faking the passage of time.
The feed now lives on the dashboard as one shared section instead of being buried per target. The part that actually matters: both the per-target view and the new dashboard view call the exact same underlying function with the same inputs, so there’s no way for them to ever quietly disagree, a test pins that guarantee directly.
Then watched the scheduler live instead of trusting it on paper. Queued three scans back to back and watched a live monitor poll the database every tenth of a second: each one started within a millisecond of the previous finishing, strictly one at a time, exactly as claimed. Let the scheduler run untouched for a few minutes after and it correctly saw nothing was due yet, zero extra scans fired. Then did a real crash test, killed the server mid-scan for real, and on restart the interrupted scan correctly recorded itself as failed with an honest reason and a real timestamp, while the next queued scan picked up normally right after. Nothing stalled, nothing doubled up.
Also added a small “last changed” column to the main table, reading how long ago a target last actually recorded a real change, falling back to an honest dash instead of a false zero when there’s simply no history to look back through.
Honest gaps left standing: the scheduler’s actual auto-triggered scan path still hasn’t fired against a real clock, since nothing happened to go stale during this run, and the fully populated version of the new feed has never actually been seen live, since the test target just didn’t flap this time.
Three things landed, and one caught a real bug in the tool itself. Targets now get rescanned automatically on a fixed cadence instead of only when someone manually triggers one, with guards so a slow scan never piles another behind it and a crash-interrupted scan gets marked failed on restart instead of stalling the cadence forever.
The bug lived in that scheduler. A raw SQL fragment had a column reference that wasn’t properly scoped, so inside a nested subquery it silently matched against the wrong table entirely, meaning every tick thought every target was already stale and due again. The worker rescanned the test target seventeen times in about three minutes before anyone noticed. Killed it, fixed the query by hand, and pinned the actual generated SQL in a test so the same mistake fails loudly next time instead of quietly hammering a shared target.
That accidental storm also proved the flap-detection design works for real: the scheduled runs watched the flaky test port close three times, catch it opening once with exactly the single expected unconfirmed alert, then stay quiet through seven more scans that all agreed. One honest announcement, then silence, no crying wolf.
Extended certificate checking from just the main port to the mail ports too, and testing it against a real mail server caught another real bug, the handshake was sending the resolved IP address as the server name instead of the actual domain, so every check failed with a false hostname mismatch on a certificate that was actually fine. Fixed to match what a real browser does, and the same server that falsely failed a minute earlier came back clean.
Also did another full visual pass and caught a few smaller things left over: red still marking a normal state, a decorative pulsing dot next to text that already said the same thing, inconsistent spacing across tables, and a rare timing mismatch between server-rendered and client-rendered relative timestamps.
The scanner works and the loop is honest, but the frontend read like it came off an assembly line. Screenshotted every page with real data and judged each one as a skeptical stranger: does one person’s point of view show here, or does it pattern-match a template. Verdict: every shot patterned. Terminal-cosplay monospace headline, a numbered marketing-style “how it works” section, prose before any real output, tinted pill badges doing what a plain word could do, a green accent spent purely on decoration.
That same review caught two real bugs, not the code review. Findings rendered worst-last instead of worst-first, so the most important result sat at the bottom. And a completely expected, normal state rendered in alarm red, exactly the thing that trains people to ignore red.
Rebuilt around one stance: paper, not a dashboard. Off-white background, no rounded corners, no shadows, no gradients. Tables are the interface. Monospace only for actual data, ports, tokens, timestamps. Color appears exactly once, for severity, nothing else. The landing page now shows a real report pulled live from the database instead of marketing copy.
Shipped port banner grabbing too, so an open port says what’s listening instead of a bare number. One real security call won along the way: a first draft quietly disabled certificate validation to read self-signed certs, and that got reverted, since a self-signed cert is exactly what a monitoring tool should flag, not quietly read through.
Also added a real diff feed between scans. A flaky test target’s port flips open and closed between runs, so announcing every change instantly would be worthless noise. Settled on showing a change immediately but marked unconfirmed until two of the last three scans agree, resolving a one-scan flap back to its real state too.
Still missing: scheduled rescans, so the feed only moves when someone manually triggers a scan, and mail TLS ports are identified but not checked for certificate expiry yet.
OpenPorts watches a domain from the outside and reports what it exposes: open ports, TLS certificates on their way out, mail records that let anyone spoof you, headers that give away the stack. The first full loop went end to end: add a domain, publish one TXT record to prove you own it, then the scanner runs. No record, no scan, one exception for the public test target Nmap itself runs for scanner builders.
The scanner stays deliberately modest: one connect sweep over the hundred most likely ports, a TLS handshake to read the certificate, a few DNS lookups for spoofing protection, and three small HTTP checks. Nothing sends a payload, nothing probes services. A full scan takes under three seconds.
Two real bugs worth writing down. First, the hand-assembled port list was just wrong, missing port 3389 entirely, so the single highest-severity finding in the whole catalog could never actually fire. A boring test that just checks a list of numbers felt like pure ceremony right up until it caught exactly that.
Second, the test target’s port 80 turned out flaky in the worst way for a monitoring product: sometimes it answers fully, sometimes the connection opens but the request just hangs until timeout, and reported findings silently change between runs though nothing on the customer’s end did anything. Now recording exactly what the network said instead of pretending every check came back clean; confirming a change twice before reporting it is next.
Architecture choices so far: a simple embedded database instead of a separate one to babysit, and one plain interval-based worker in the main process instead of real job infrastructure, the least moving parts that still survives a restart without double-scanning. What doesn’t work yet: no service identification on open ports, TLS only checked when the main web port answers, and only one scan at a time.
Next: naming what’s running on each open port, scheduled rescans, and diffing between scans, since “this port showed up since yesterday” is the real product.