What It Is
CIO Security Scanner is an automated web security assessment tool that checks any public website across multiple security dimensions and returns a scored, graded result with plain-language findings. It runs all checks simultaneously, saves every result to PostgreSQL, and exposes a clean REST API with two endpoints — one to run a scan, one to retrieve a stored result by ID.
It is live at webscan.christianoguine.be and the source is on GitHub.
Note: The live deployment has all security headers implemented and configured. You can verify this by running https://webscan.christianoguine.be through the scanner itself.
Why I Built It
Most websites have some form of security vulnerability. Even experienced developers may consider these issues negligible, often because their focus is on building features, not advanced web security. But from a security perspective, these small loopholes are exactly what attackers look for. You don’t need a sophisticated exploit when a misconfigured header or an exposed file is enough.
I built this scanner to make those risks visible. It scans any given URL, excluding private and internal IP ranges to prevent misuse and shows the kinds of weaknesses that usually require expensive enterprise tools or manual review to find. The goal is simple: catch the small things before an attacker does.
How I Built It
The scanner is a Node.js TypeScript REST API built with Express and PostgreSQL using Drizzle ORM. Every scan runs eight checks simultaneously using Promise.allSettled—a deliberate choice over Promise.all because individual check failures should never silence the others. A DNS timeout does not mean the SSL and header checks should return nothing. Every check settles independently and the results are collected from whatever completed.
// Run all checks in parallel and collect results
const findings: FindingResult[] = await Promise.allSettled([
checkStrictTransportSecurity(),
checkContentSecurityPolicy(),
// ...other checks
]).then((results) => results.map((r) => r.value));
Each check is an isolated async function that returns a consistent FindingResult object: check name, status, severity, score contribution, and a plain-language message. This separation means each check can fail, timeout, or succeed without affecting anything else. The scanner collects them all, runs them through a scoring engine, and saves the complete result to PostgreSQL before returning the response.
// Example: Structure of FindingResult
const result: FindingResult = {
check: "Strict-Transport-Security",
status: "fail",
severity: "high",
score: 0,
message:
"Strict-Transport-Security header is missing. Add it to enforce HTTPS.",
};
The scoring model has two layers. Critical findings, exposed environment files, exposed git configuration, missing SSL, bypass score calculation entirely and force an automatic F. The reasoning is straightforward: no combination of correctly configured headers compensates for exposed database credentials. For everything else each passing check contributes weighted points toward a total of 100, mapping to grades A through F.
Because the scanner accepts user-supplied URLs and makes outbound HTTP requests, SSRF protection was a required design consideration from the start. Before any request is made the URL is validated using Zod — scheme checked, hostname extracted, and tested against every private IP range including RFC 1918 addresses, the AWS metadata endpoint, and IPv6 private ranges. The tool scans public infrastructure. It cannot be used as a proxy to reach internal systems.
The frontend is a single HTML page served as a static file by Express. The permission checkbox is required before scanning can begin — the scan button stays disabled until the user confirms they own the domain or have explicit authorisation to test it.
What It Checks
HTTP Security Headers — Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Server version disclosure, and X-Powered-By disclosure. Each header targets a specific attack class. Their presence or absence directly determines the browser-level protection available to your users.
SSL/TLS Certificate — direct TLS connection using Node's built-in tls module. No third-party library. Certificate validity, domain coverage, and days to expiry. Certificates within 30 days of expiry get a warning. Missing certificates get automatic F.
DMARC DNS Record — DNS lookup for the DMARC TXT record at _dmarc.hostname. Policy strength evaluated — p=none, p=quarantine, p=reject — because a DMARC record that only monitors provides no real protection against email spoofing.
Exposed Sensitive Files — requests /.env and /.git/config. Uses content pattern matching not just HTTP status codes to avoid false positives on servers that return 200 for all paths. Either finding is automatic F.
Stack
| Layer | Technology |
|---|---|
| Runtime | Node.js 20 — TypeScript strict mode |
| Framework | Express 5 |
| Database | PostgreSQL 16 with Drizzle ORM |
| Validation | Zod v4 |
| Middleware | Helmet, express-rate-limit |
| Containerisation | Docker Compose |
| Deployment | Hetzner VPS, nginx, PM2, Let's Encrypt |
Skills Demonstrated
- Security Engineering: Designed checks around real attack vectors — XSS, SSL stripping, clickjacking, MIME confusion, email spoofing, credential exposure. Every check exists because of a specific, exploitable vulnerability class.
- Backend Architecture: Runs all checks at once, but keeps going even if some fail; layered scoring model with critical failure bypass; Zod validation at the API boundary; clean separation between check functions, scoring engine, and persistence.
- SSRF Awareness: Built SSRF protection into the design from the start — not as an afterthought. URL validation and private IP blocklist applied before any outbound request.
- TypeScript Discipline: Strict mode throughout. Shared interfaces enforcing consistent structure across all check functions. No any types.
- Production Deployment: Running live on real infrastructure with nginx, PM2, Let's Encrypt SSL, and Docker for the database. The scanner passes its own checks.



