Skip to content
AurelioAvilaPublic

Repository files navigation

Glarion — Website Security Monitoring for Digital Agencies

For agencies monitoring verified client websites. Turn scan findings into prioritized actions and agency-branded HTML/PDF reports. Full scans require a plan and current ownership proof; the public check reads publicly available information.

Open Glarion · View the fictional sample report · Deployment signing

Report workflow: a verified website → findings with evidence and corrective guidance → an agency-branded HTML report or printable PDF. The linked sample uses fictional findings and an example client; it is not a customer case study or a measured security outcome.

Glarion verification aperture mark

Website security monitoring and client-ready reporting for digital agencies.
Know what changed before your client does.

Check a website at glarion.app  ·  View a sample report

Glarion helps digital agencies monitor verified client websites, identify meaningful changes and turn technical findings into clear reports under the agency's own name. The ungated public check reads only what a website already publishes; paid full scans add controlled active checks, weekly or monthly monitoring and change-focused notifications.

Continuous integration status Rust and Axum API Proprietary license

Built around authorization, not assumptions. Full scans require a plan and current DNS or .well-known ownership proof, checked again at execution time. Targets are checked for private, loopback and cloud-metadata addresses before scanning; external-scanner enforcement has the limitations described below. Every queued scan retains an authorization trail.

Proof point What is enforced
Target authorization Two independent ownership gates, including expiry at execution time
SSRF controls Resolved-address checks, address pinning for internal fetches, and a Nuclei local-network restriction; see network boundaries and known limits
Safe scan policy Detection-only allowlist, rate limits and no fuzzing, brute force or denial-of-service templates
Verified quality Live-database integration tests and mutation checks over the authorization gate
Actionable output Findings are normalized and triaged into self-contained client reports

The source is public for technical review and transparency, not for reuse. Glarion is proprietary software; see LICENSE.

Glarion landing page at glarion.app: a free public website check, with the Check a website form and the sample report link

The constraint everything else is arranged around

A service that scans arbitrary domains on request is an attack tool unless it can prove the requester owns the target. So no scan runs against a domain whose ownership has not been proved and whose proof has not lapsed.

Ownership is proved with a DNS TXT record or a .well-known file and expires after 30 days. The check runs in two independent places:

  • crates/api/src/routes/scans.rs refuses to queue a job for an unverified target.
  • crates/orchestrator/src/runner.rs checks again at execution time, because a job can sit in the queue while its verification lapses, and the runner is what actually sends traffic.

Every queued scan writes an authorization record in the same transaction as the job, so a job cannot exist without a trail of who authorized it.

Two tiers, and why one is gated twice

A full scan probes: it requests paths a site never advertised and tries known vulnerability fingerprints against them. That is the act the gate above exists for.

Reading a site's DNS records, its TLS configuration, the response headers on its front page, and the two files it publishes for automated readers is not that. It is what a browser or a search-engine crawler collects on an ordinary visit, and there is nothing to authorise about reading what a site broadcasts to anyone. POST /api/preview does exactly that much and no more — one front-page request, two well-known files, GET only, no redirects followed — and needs neither an account nor a verified domain.

Keeping them apart matters commercially as much as legally. Before it, nobody could see a single result without first editing DNS for a client's domain, which is to say before we had shown them anything at all.

The split is also where the money is. The free check stays free, with no account and no limit on how many domains you point it at. The full scan answers to two gates rather than one: proving ownership settles whether we may run it, and a plan settles whether it is paid for. Both are checked in create_scan, next to each other, and both fail closed. The free plan gets the check and stops there — it is a real product on its own, and it is not the scanner.

Where traffic can be aimed

Proving ownership of a name is not the same as proving the name is safe to contact. A hostname that passes every syntactic check can still resolve to 127.0.0.1 or 169.254.169.254, which would point the scanner at our own infrastructure or at the cloud metadata service. Two layers handle this:

  • crates/orchestrator/src/domain.rs rejects targets written as an IP literal, as localhost, or under an internal suffix.
  • net_guard.rs checks resolved addresses and rejects the target if any address matches its non-public address filters, including private, loopback and cloud-metadata addresses.

The connection boundary differs by caller. Internal HTTPS fetches configure the client to use a validated address and disable redirects; the TLS probe connects directly to a validated socket address. See verification.rs, preview.rs and tls.rs.

The external Nuclei wrapper checks addresses immediately before spawning the scanner, but passes a hostname, not a pinned address. It always adds -restrict-local-network-access. That is a separate scanner-side control; its enforcement depends on the installed Nuclei implementation. These controls do not establish that every DNS-rebinding path is closed.

Intensity is capped independently of authorization: detection-only tools on an allowlist, six scans per target per day, an enforced request rate, and fuzzing, brute-force and denial-of-service template categories excluded. External tools are spawned with an argument vector, never a shell string.

Structure

  • crates/api — HTTP API (Axum): auth, targets, verification, scans, results. Also builds the runner binary.
  • crates/orchestrator — verification, target validation, scan policy, job runner, tool wrappers.
  • crates/report — report rendering. Triage lives in orchestrator, since deciding which findings matter is domain knowledge rather than presentation, and the worker needs it too.
  • web/ — the dashboard: TypeScript compiled with tsc, no framework and no bundler, matching the rest of the portfolio. Driven from the keyboard: ⌘K/Ctrl-K or / opens the command palette, j/k walk the list.
  • migrations/ — Postgres schema, applied with sqlx.

Running

bash scripts/dev-api.sh                              # API + database
cargo run --bin runner                               # scan worker
cd web && npm install && npm run build               # dashboard, once
python -m http.server 5173 --directory web           # serve it

scripts/dev-api.sh starts the development database, generates a JWT secret on first run, and opens CORS to the local dashboard. In production the API needs DATABASE_URL and JWT_SECRET (32+ characters); PORT and CORS_EXTRA_ORIGINS are optional. Both binaries refuse to start rather than fall back to a default.

Email uses Resend, the provider already used elsewhere in the portfolio: set RESEND_API_KEY, MAIL_FROM, and PUBLIC_URL (where confirmation links should point — taken from configuration rather than from a request header, since a link built from an attacker-supplied Host is how confirmation mail becomes phishing). With no key configured, messages are logged instead of sent and the confirmation link appears in the API output, so the signup flow can be exercised locally without a mail account.

The dashboard talks to the same origin it is served from, including local previews on a custom port. If frontend and API are served separately, set the glarion-api meta tag explicitly to the API origin.

The runner needs nuclei on PATH. A job whose tool is missing fails with a recorded reason rather than hanging.

Testing

bash scripts/dev-db.sh test

Creates and starts a Postgres cluster dedicated to this project, then runs the whole suite with the integration tests active. 270 tests at present.

Plain cargo test --workspace also works, but the integration tests in crates/api/tests/scan_gate.rs skip silently without a database, so a green run on a machine with no Postgres does not mean the gate was exercised. Before pushing, run the full set of checks:

bash scripts/ci-local.sh

The local script runs formatting, clippy, the integration suite and an assertion that the gate tests ran rather than skipped. It also runs cargo audit when installed, and frontend checks when their dependencies are installed. GitHub Actions runs formatting, clippy, frontend checks and the database-backed suite, including the gate-test assertion; it does not currently run cargo audit. Check the run for the commit you are evaluating rather than relying on a previously successful build. One advisory is deliberately ignored by the local audit — see .cargo/audit.toml.

Since 2026-10-21 production is self-hosted behind a Cloudflare Tunnel and deployed by hand from master: a clean clone, the release build from the Dockerfile, a health check, and an automatic switch back to the previous build if the new one does not come up. Production runs DATABASE_MIGRATIONS=verify, so a new migration is applied by hand before the deploy. A CI run does not deploy by itself. A scheduled workflow (uptime.yml) asks https://glarion.app/health from outside every 15 minutes and fails, so GitHub sends an email, when it does not answer 200 after three tries.

The development database

scripts/dev-db.sh manages a cluster in ~/.glarion-devdb on port 5433, created under your normal user account with a generated password. It is deliberately separate from any system PostgreSQL install: it needs no superuser credentials and cannot disturb an existing database.

bash scripts/dev-db.sh start     # create and start
bash scripts/dev-db.sh stop
bash scripts/dev-db.sh status
bash scripts/dev-db.sh url       # connection URL for the test database
bash scripts/dev-db.sh url dev   # connection URL for the app's database
bash scripts/dev-db.sh destroy   # delete it entirely

The cluster holds two databases. glarion_test is what the integration tests use, and they TRUNCATE every table in it; glarion_dev is what the running app uses. Keeping them apart is why a test run no longer destroys the accounts and scan history you were working with.

Status

Working: ownership verification, target and resolved-address validation, authentication, scan policy and both gate checks, the Nuclei wrapper, and the job runner.

Registration collects a name and date of birth alongside the address; the date of birth exists to check the account holder is old enough to enter a paid contract, which is the only reason to hold it. An account cannot be used until the address is confirmed through an emailed link, and neither signup nor sign-in reveals whether an address is already registered.

Authentication uses Argon2 with JWTs pinned to HS256 and a token_version column for revocation. Login performs the same work whether or not the account exists, so response timing does not reveal which addresses are registered. Confirmation links are stored only as a hash, expire after 24 hours, and stop working once used. Authentication and verification endpoints are rate limited.

The gate has been verified against a live database and mutation-checked: disabling the expiry check makes scan_is_refused_when_verification_has_expired fail, so the test is known to be capable of catching a regression rather than merely passing.

Findings are triaged rather than listed: a real scan of a small production site returned 32 results, of which three warranted action and nineteen were inventory. The scanner's own severity is not used as the priority — it rates a missing Content-Security-Policy as informational, which is wrong for a report going to a client. Reports are rendered as a single self-contained HTML file under the agency's name, printable to PDF.

Sites can be put on a weekly or monthly schedule. A schedule is standing authorization, so it can only be set while ownership is proved, and the scheduler refuses — loudly — once that proof lapses rather than quietly scanning on. Notification is sent only when a result differs from the one before it: a weekly "still three issues" teaches people to filter the sender, and then the message that mattered goes unread too.

Plans

Plan Price Sites Full scan Automatic checks
Free — 1 No — the free check only No
Solo 19 EUR / month, 170 / year 5 Yes Weekly or monthly
Studio 39 EUR / month, 350 / year 10 Yes Weekly or monthly
Agency 99 EUR / month, 750 / year 40 Yes Weekly or monthly

Prices exclude VAT, which Stripe adds at checkout by country.

Priced per account with a site allowance, not per site. That structure is intended for agencies managing a portfolio of client websites rather than a single application.

The site allowance is derived from the plan rather than stored beside it: two copies of the same number drift apart the moment either is written without the other, and the one that drifts is the one enforcing a paid limit.

Checkout and cancellation are hosted by Stripe, so no card detail reaches these servers.

Free trial

TRIAL_ENABLED=true turns on the three-day trial; unset or anything else leaves billing exactly as before (the 14-day Solo trial for a first subscription, and a failed payment keeping the plan while Stripe retries). With it on:

  • Every paid plan, monthly or yearly, offers two buttons: Start 3-day free trial (start_trial: true) and Subscribe now (false, charged at once). The trial needs a payment method; Stripe charges automatically when it ends.
  • One trial per account, per normalised mailbox (email_hash) and per card (card_fingerprint), held in trial_claims and enforced when Stripe creates the subscription: a duplicate is cancelled before any charge, with cancellation_details.comment = trial_already_used. A site already scanned during another account's trial is not scanned free again.
  • During the trial: 3 sites at most, 2 full scans per site a day, reports without agency branding. The ownership gate is unchanged.
  • A failed payment blocks the plan at once (past_due is no longer grace); a later successful payment restores it. Schedules pause rather than reset.
  • GET /api/billing/prices (public) publishes the Stripe Prices; GET /api/billing adds state, blocked_reason, trial_ends_at, can_start_trial, trial_limits and first_charge (Stripe's preview).
  • The worker sends the reminder a day before the trial ends.

The webhook endpoint needs invoice.paid, invoice.payment_failed, invoice.payment_action_required and customer.subscription.trial_will_end besides the customer.subscription.* events. The behaviour is pinned by crates/api/tests/trial.rs, which plays Stripe back through a local stand-in.

Copy .env.local.example to .env.local and fill it in; dev-api.sh loads it. That file is gitignored, and secrets belong in it rather than on a command line, where they end up in shell history. With none of it set the billing endpoints refuse rather than granting anything, and the rest of the product works unchanged.

Live at glarion.app, including paid checkout. A subscription is continuous business activity, which in Italy needs a P.IVA and VAT handling — Stripe Tax computes the rates, but the filing obligation is not something software solves, and is being handled outside this repository rather than a blocker on shipping.

A Privacy Policy and Terms of Service cover what the product collects and why. Deleting an account (DELETE /api/account) anonymizes rather than removes the row: the audit trail described above — who authorized a scan, and when — has to survive the account that authorized it, which is exactly the retention GDPR Article 17(3)(e) allows for establishing or defending a legal claim.

Not built yet: the httpx/subfinder tool wrappers. Certificate expiry — originally planned as a testssl.sh wrapper — is read directly over a raw TLS handshake instead (crates/orchestrator/src/tools/tls.rs), which avoids both the shell dependency and duplicating what Nuclei's own ssl-* templates already report.

Known limits

  • Production rate limits share PostgreSQL counters. Client attribution depends on a trusted proxy rewriting the client-address header (TRUST_PROXY_CLIENT_IP=cloudflare reads CF-Connecting-IP, 1 reads Fly-Client-IP); a chain of proxies needs an operational check to avoid grouping unrelated visitors together.

  • Own HTTP requests pin validated addresses. Nuclei resolves independently and is launched with its local-network restriction; worker egress isolation is an additional operational boundary to verify.

  • The IPv6 destination policy deliberately excludes special-purpose and transition ranges, including some legitimate non-website protocol services.

  • Passing tests and dependency audits does not establish complete security.

  • Nuclei resolves the hostname independently after Glarion's address check, so a DNS change can occur between that check and a scanner connection. Glarion passes -restrict-local-network-access, but its unit test checks the presence of the flag, not end-to-end DNS-rebinding resistance of the installed scanner. Do not treat this as a guarantee that scanning is safe against every SSRF or DNS-rebinding scenario.

More tools by Aurelio Avila

  • PC Tweaker: Windows tuning for performance, gaming and privacy, with hardware monitoring and restore tools for supported settings.
  • PC Tweaker Uninstaller: Windows uninstaller with evidence-based removal scores, rollback snapshots and a local removal ledger.
  • Tweaky Driver: Windows driver inventory and update review, showing detected devices, installed versions and available driver offers.
  • Redaxa: Sensitive-data checks and redaction for AI prompts, plus local repository secret reviews on Windows.
  • Redexa Social: Private YouTube analytics for Windows, kept in a database on your PC.
  • Frame Witness: Windows photo and video recovery with source offsets, SHA-256 hashes and dated findings.

Releases

Packages

Used by

Contributors

Languages