Sentinel

Unmask the internet. Real-time scam, virus and malware protection for search results, email and downloads. Brand domain: www.usesentinel.technology (configured in brand.json).

Sentinel marks risky links with three masks, each coloured by severity:

Mask Threat Yellow Orange Red
Smiling mask Scam Suspicious Likely scam Confirmed scam
Mask with spores Virus Possible virus Likely virus Virus detected
Horned mask Malware Possible malware Likely malware Malware detected

Red is only ever shown with evidence (a threat-feed match, a known scam kit on an impersonating page, a known malicious file, or unmistakable hostile behaviour). Heuristics alone top out at orange.

How a verdict is made

  1. Knowledge: Sentinel’s own threat database, community reports (3 independent reports make a site confirmed), 15 live public lists (see Threat lists: about 430,000 listed hosts and addresses, each refreshed on its own interval) and optional Google Safe Browsing. No record anywhere lowers the risk (scam −20%, virus/malware −15%).
  2. Checklist: 101 named checks (look-alikes, typosquats, combosquats, abused TLDs, free-hosting throwaways, disguised downloads, bait wording in several languages…). Every check explains itself in the UI.
  3. Compare: domain skeleton and name-pattern matching against known scam domains; page content against 11 known scam/malware kit families and copies of confirmed scam pages.
  4. Research (Pro and up): RDAP registration age and status (asked of the registry IANA lists for the ending, with a User-Agent: rdap.org refuses clients without one, and until 1.6.0 that silently cost every verdict its domain age), DNS, TLS certificate, redirect chain, page forms, scripts, hidden frames, miners, ClickFix lures, forced downloads, and full file analysis of anything the link downloads.

Plans (enforced server-side, reset Mondays 00:00 UTC)

  Free Pro Max Ultimate
Price / month $0 $15 $40 $100
Link scans / week 10 (no research, scam mask only) 40 (researched, all masks) 100 (researched, all masks) 500 (researched, all masks)
Virus & malware scans / week 5 40 100 500
Fast live scanning / week 15 min 24 h unlimited unlimited
Delicate live scanning / week (every result researched) No 4 h 24 h 96 h
Email masks (Gmail/Outlook) No ✓ ✓ ✓
Paste-in email scans No No ✓ (counts as a link scan) ✓ (counts as a link scan)
Download protection (app) No ✓ ✓ ✓

Prices and entitlements live in server/lib/plans.js and are enforced there: the client only renders what the server returns. An uncapped allowance is stored as null.

What’s in the repo

server/            Node API + web server (zero npm dependencies, built-in SQLite)
  lib/scan/        knowledge, checklist, compare, research, netguard (SSRF), filescan, email, feeds, engine
  lib/             auth (scrypt, sessions, 2FA, Google OAuth), security, plans, mailer, db (migrations)
  routes/          auth, account, scan/live, optional AI assistants
web/               marketing site, installable web app (/app), auth pages, partials, service worker
extension/         Browser companion (MV3): search masks, email masks, page warnings
desktop/           Sentinel desktop app (Electron): tray, download protection, unlocks live features
tests/             node:test suites with local scam/malware fixture sites
scripts/           evaluate, admin, icons, fonts, packaging
deploy/            docker-compose + Caddy (automatic HTTPS for www.usesentinel.technology)

Run locally

Requires Node 22.5+.

npm start

Open http://localhost:8787. With no .env, billing runs in demo mode (plans switch instantly, no payment) and emails such as password resets are printed to the console.

npm test
npm run evaluate

npm run evaluate measures detection on today’s live phishing feed and false alarms on 127 popular sites. At last run: 0 false alarms on the 127 sites checklist-only and 0/32 with live research; 62% of live phishing addresses were flagged by the checklist alone, with no list lookup at all, and listed ones are confirmed red. Details under Testing the scanner.

The desktop app

The app ships the whole Sentinel server inside it and runs it privately on 127.0.0.1:47821 when it starts. Accounts, scanning, threat feeds and history all work on a computer with nothing hosted anywhere; the database and logs live in the user’s app-data folder (%APPDATA%\Sentinel on Windows). Set SENTINEL_ORIGIN=https://your-sentinel.example before launching to use a hosted Sentinel instead, and npm run dev in desktop/ to use the dev server.

Beyond running the product, the app protects on its own, with no browser add-on involved:

Pre-launch builds run billing in demo mode so every plan can be exercised.

cd desktop && npm install && npm run dist:win

That produces desktop/dist/Sentinel-Setup-<version>.exe, with the server, site and browser companion bundled in.

Releases

Downloads come from GitHub Releases, built by .github/workflows/release.yml with GitHub’s own token. To ship a version, bump desktop/package.json and:

git tag v1.5.0 && git push origin v1.5.0

The workflow runs the tests, builds the installer and both companion builds, and publishes Sentinel-Setup.exe, sentinel-companion.zip, sentinel-companion-firefox.zip, latest.yml and SHA256SUMS.txt.

latest.yml is what installed copies read to update themselves, so a release reaches everyone who already has the app without them downloading anything. The download page always links to the latest release and shows its real version and size from the GitHub API. The installer is not code-signed, so Windows SmartScreen warns on first run until a signing certificate is bought.

The browser companion (optional)

On Windows the app needs no add-on: live scanning above does the search marks, the page warnings and the overlay by itself, in every browser including private windows and the DuckDuckGo browser. The companion remains for people who are not on the Windows app. It is no longer bundled with the installer or offered in the app.

One codebase, two packages, built by npm run build:ext:

Package Browsers Background
sentinel-companion-latest.zip Chrome, Edge, Brave, Opera, Vivaldi service worker (src/background.js)
sentinel-companion-firefox-latest.zip Firefox 128+, LibreWolf event page (src/background.firefox.js, generated)

Every script talks to one ext namespace (browser where it exists, otherwise chrome) and uses promises, which both engines support; tests/extension.test.js fails the build on a bare chrome.* call or a callback. Firefox has no externally_connectable, so pairing runs entirely through the /connect page’s content script, and it grants host access on request, so the popup asks for it the first time. Firefox also runs content scripts in a sandbox where globalThis is not window, so shared objects are read from globalThis. Sign the Firefox build with npx web-ext sign before distributing it outside the app.

What the companion’s live scanning looks like

Inside the browser, the companion draws the same thing the app draws from outside: a golden line under a faint golden tint while results are checked, then a mark on every result (a yellow, orange or red mask exactly as the engine returned it, or a quiet tick). Its overlay lives in a closed shadow root with pointer-events: none, respects reduced motion, and only renders: verdicts come from the background worker, and nothing in the page script can assign red.

Live hours are spent only when Sentinel actually checks something, and only for a tab that is in use. On Max and Ultimate every result is researched as a second pass; the plan gate is server/lib/plans.js.

Browser support

Browser Companion add-on Live scanning with no add-on (Windows app) How the companion was checked
Chrome yes yes verify-companion.js, Chrome 153: 10 of 10 results marked, overlay seen
Edge yes yes same, Edge 153
Firefox yes (temporary add-on until signed) yes verify-companion-firefox.js, Firefox 156: 10 of 10 marked, overlay seen
Brave yes, the Chromium package yes verify-companion.js in a Windows 11 virtual machine, Brave 153: 10 of 10 marked, overlay seen
Opera yes, the Chromium package yes same, Opera (Chromium 152): 10 of 10 marked, overlay seen
Vivaldi yes, the Chromium package yes not run; it takes the same package through the same engine
LibreWolf yes, the Firefox package yes not run; same engine and package as Firefox
DuckDuckGo browser no yes (Windows) it has no extension support at all, on any platform

npm run verify:companion loads the built packages into real browsers, headless, in throwaway profiles (DevTools pipe and Extensions.loadUnpacked for Chromium, Marionette for Firefox), pairs them with the running desktop app, and checks a results page end to end. The page is served locally at the search engine’s address; its links are honest sites and made-up scam-style addresses, and none of them is ever opened.

The static site

npm run build:static exports web/ to dist-static/ for hosts with no Node server (GitHub Pages and the like). It expands the same @include partials the server renders, rewrites root-absolute links so the site works from a subpath, and bakes today’s demo verdicts in, so the search demo, mask explorer and Spot-the-scam game still run against real scan results. Anything that needs the API - sign-in, live scans, the app - links back to the full site instead.

The public site is served from wyattbombara/sentinel (GitHub Pages at wyattbombara.github.io/sentinel). That repository only ever holds build output:

npm run publish:site

builds web/ for that address and pushes one commit there, using the git credentials already on this machine. .github/workflows/pages.yml also publishes a copy to this repository’s own Pages site on every push that touches web/. Turn it on once under Settings -> Pages -> Source: GitHub Actions. Keeping the static copy as a build output is what stops it drifting from the real site.

Deploy to www.usesentinel.technology

cd deploy && docker compose up -d --build

Before that: point DNS for usesentinel.technology and www.usesentinel.technology at the server, copy .env.example to .env, and set SESSION_SECRET. Optional: GOOGLE_CLIENT_ID/SECRET, SAFE_BROWSING_API_KEY, RESEND_API_KEY (password-reset emails).

Accounts and sessions

Security

scrypt password hashing · account lockout · TOTP two-factor with replay protection · session list with per-session revoke · rolling session expiry · password reset with single-use expiring links · HttpOnly/__Host- cookies · CSRF origin checks + JSON-only bodies · strict CSP, HSTS, COOP/CORP, frame denial · per-IP and per-account rate limits · SSRF-safe research fetcher (private/metadata IPs blocked, DNS pinned per hop) · path-traversal-safe static server · AES-256-GCM for stored secrets · audit log · no admin HTTP surface (npm run admin) · extension message-origin validation · desktop IPC origin checks, sandboxed renderer.

Operator commands:

node scripts/admin.js set-plan user@example.com max

Threat lists

Every scan starts by looking the address up in 15 free, keyless public lists (server/lib/scan/feeds.js): OpenPhish, PhishTank, Phishing.Database, the malware-filter phishing and URLhaus lists, CERT Polska, DurableNapkin, Spam404, MetaMask, ScamSniffer, Polkadot.js, URLhaus, ThreatFox, Feodo Tracker and Blackbook. A hit in any of them is evidence, and evidence means a confirmed (red) verdict rather than a guess. A hit is: the exact address is listed, the domain is listed, or the site’s own front page is listed.

A different page on a host that serves a listed address is an inference, not a listing, and it is treated as one: the verdict is “Likely” (orange) and says so in words (“Another page on this site is listed by PhishTank; this address is not”). It becomes red only when the address also fails checks of its own, which is what separates a throwaway host from a large service where one tenant misbehaved. Before 1.4.3 any such page was red, and with the lists loaded that made app.hubspot.com/login a confirmed scam because of one PhishTank entry elsewhere on the host. Services that carry other people’s pages (form builders, site builders, mail-tracking links, document shares; the lists themselves show which: new.express.adobe.com alone has over a thousand listed pages) are judged page by page. Each list refreshes on its own interval, and a verdict says how many of them had loaded when it was made.

When nothing is listed, the name is still compared with at least ten known scam domains by edit distance and shared distinctive tokens (server/lib/scan/compare.js), and the result reports how many it was measured against.

Testing the scanner

Three layers, none of which download or run malware:

What was deliberately not done: no confirmed-malicious URL was fetched, no malware sample was downloaded or placed on disk, and no protection was disabled to make a test pass.

Known limits