Skip to content

Latest commit

 

History

History
283 lines (233 loc) · 13.4 KB

File metadata and controls

283 lines (233 loc) · 13.4 KB

InfraLens — Developer Documentation

InfraLens is Randy Code's open-source website inspection tool: give it a URL, it runs 20 passive, read-only checks server-side and returns a scored, readable report — no exploitation, no brute force, no port scanning, no crawling beyond the page itself.

This document is developer/maintainer documentation — implementation, architecture, and how to work on the code. For the product story (why it was built, engineering decisions, migration), see the case study at /projects/infralens. For end-user documentation (how to use it, what results mean), see /tools/infralens/docs and /tools/infralens/privacy.

InfraLens lives natively inside this repository (app/tools/infralens/, src/infralens/) — it isn't a separate deployment or a proxied app. It started as a standalone product; that history is preserved in CHANGELOG.md (frozen at the point of migration).

Purpose

Give a fast, honest read on a site's public technical posture — DNS, TLS, HTTP security headers, infrastructure, structure, metadata, and performance signals — without requiring an account, storing anything server-side, or doing anything a normal visitor's browser and DNS resolver couldn't already do.

Architecture

flowchart LR
    subgraph Browser
        UI["React UI\n(hero, results, compare, history)"]
    end
    subgraph "Next.js server (this repo)"
        SA["Server Action\nrunInfraChecks()"]
        RL["Rate limiter\n(Upstash, shared policy)"]
        SSRF["Target validation\n(SSRF guard, DNS pinning)"]
        Checks["20 checks\n(concurrency-limited pool)"]
        Score["Scoring + recommendations"]
    end
    Target(["Target website\nDNS / HTTP / TLS"])
    ipapi(["ipapi.co\n(optional, IP of target only)"])
    LocalStorage[("localStorage\n(this browser only)")]

    UI -- "URL" --> SA
    SA --> RL
    RL --> SSRF
    SSRF --> Checks
    Checks -- "passive, read-only requests" --> Target
    Checks -.->|"IP hosting lookup"| ipapi
    Checks --> Score
    Score -- "report" --> UI
    UI -- "up to 10 entries" --> LocalStorage
Loading

Nothing about a specific analysis is persisted server-side — the report goes straight back to the browser, and the only thing that outlives the request is the local history in the browser's own localStorage.

Core principles

  • Server Actions only — every check runs server-side (CORS makes most of this impossible from the browser anyway, and running it client-side would leak the visitor's own IP to every site analyzed).
  • SSRF-guarded — target resolution is validated and pinned before any check runs; private/loopback/link-local ranges and DNS-rebinding attempts are rejected.
  • Modular checks — each check is an independent, typed module implementing a shared interface.
  • Ephemeral by design — no accounts, no server-side analysis storage, capped local history only.

Namespacing

InfraLens's code is isolated under src/infralens/ (not src/) with dedicated path aliases (@infralens, @infralens-lib, @infralens-config, @infralens-components, @infralens-hooks — see tsconfig.json and vitest.config.ts) so it can't accidentally collide with the rest of the portfolio's code. Its Tailwind classes are scoped under .infralens-scope in app/globals.css, which inherits Randy Code's design tokens and overrides only what's intentionally distinct (InfraLens's green primary/ ring accent).

Project structure

app/tools/infralens/
├── page.tsx                  # Landing + analysis
├── compare/page.tsx          # Local report comparator
├── docs/page.tsx             # End-user documentation
├── privacy/page.tsx          # What's sent, stored, and contacted
├── layout.tsx, not-found.tsx, opengraph-image.tsx
├── [...slug]/page.tsx        # Catch-all → styled 404 (no basePath to route it automatically)
└── actions/run-checks.ts     # Server action: rate limit → validate → run checks

src/infralens/
├── lib/
│   ├── checks/
│   │   ├── run-checks.ts             # Orchestration (concurrency pool, timeouts)
│   │   ├── calculate-score.ts        # Scoring
│   │   ├── export.ts / export-markdown.ts
│   │   └── checks/                   # The 20 individual check modules
│   ├── dns/                          # Resolver + cache
│   ├── security/                     # SSRF guard, target validation, TLS inspection
│   ├── compare/                      # Report diffing + Markdown export
│   ├── history/                      # Local history storage (versioned format)
│   ├── recommendations/
│   ├── concurrency.ts                # Bounded-concurrency check pool
├── hooks/use-analysis-history.ts
├── config/{constants,env,site-config}.ts
└── components/
    ├── landing/                      # hero, cta, results-preview, open-source, what-it-checks
    ├── results/                      # report header, category sections, filters, ...
    ├── compare/compare-client.tsx
    └── history/history-section.tsx

public/infralens/                     # Namespaced brand assets and fonts

What InfraLens analyzes

20 checks, each with its own point weight — not a fixed category budget. Weights sum to exactly 100 across every genuinely scoreable check; informational-only checks (WAF/CDN detection, stack fingerprinting, IP/ASN/hosting inventory) have weight 0 and never move the score. A category's total is simply the sum of its checks' weights, shown below for a report where every check runs normally (a check that's excluded for a specific report — inconclusive, unavailable, not applicable, or errored — drops out of that report's denominator instead, see calculate-score.ts):

Category Total Checks (weight)
HTTP & Security 45 HTTPS/TLS incl. HSTS (18), HTTP security headers (16), redirect behavior (7), security.txt, RFC 9116 (4)
Network & DNS 14 DNS security, SPF/DMARC (8), DNS records (2), DKIM (2), DNSSEC (2), IP/ASN/hosting — informational (0)
Infrastructure 0 WAF/CDN header-fingerprint detection — always probabilistic, informational
Website Structure 11 robots.txt (4), sitemap discovery (4), internal/external link reachability (3)
Metadata & Stack 17 Accessibility hints (6), HTML metadata (5), server header leak detection (4), social tags (2), stack detection — informational (0)
Performance Signals 13 Reachability snapshot (7), response time/size/compression/Cache-Control (6)

DKIM and DNSSEC are separate checks from DNS security (SPF/DMARC) because neither can ever reach a confirmed pass/warning/fail the way SPF/DMARC can: DKIM only confirms presence-if-found (a miss is inconclusive, not proof of absence), and DNSSEC is always inconclusive — Node's built-in resolver has no DS/DNSKEY/RRSIG support.

Port scanning, traceroute, and any active/intrusive technique are intentionally excluded — see Limitations and SECURITY.md.

Security model

Every outbound request to a user-supplied target goes through src/infralens/lib/security/:

  1. URL normalization — strict protocol (http/https only), credentials, and port allowlist (src/infralens/config/constants.ts).
  2. DNS resolution + IP classification — private, loopback, link-local, cloud-metadata, and other non-public ranges blocked for both IPv4 and IPv6, including alternate notations.
  3. Pinned connection — the resolved IP is pinned for the actual request, closing the gap between DNS validation and the real network connection (DNS-rebinding protection).
  4. Redirects followed manually, each hop independently revalidated through the same pipeline — a redirect can never reach a target the initial validation wouldn't have allowed on its own.

Response bodies are capped while streaming (MAX_RESPONSE_BYTES, 2 MB); every check has its own timeout that shrinks to fit the remaining analysis-wide deadline (ANALYSIS_TIMEOUT_MS, 20s) as the analysis progresses; checks run through a bounded concurrency pool (MAX_CONCURRENT_CHECKS, 6) instead of unbounded Promise.all.

Rate limiting is per-IP, via the shared Upstash-backed limiter (src/lib/rate-limit/, policy "infralens": 5 requests/minute burst, 30/ hour) — a real cross-instance guarantee once Redis is configured, not an in-memory approximation. Without UPSTASH_REDIS_REST_URL/ UPSTASH_REDIS_REST_TOKEN (local dev, CI), it runs allow-all instead of blocking real usage; a genuine backend error fails closed rather than silently letting requests through. See SECURITY.md for how to report a vulnerability.

DNS and TLS

  • DNS resolution uses Node's native dns/promises, with an in-memory TTL cache (src/infralens/lib/dns/) to avoid redundant lookups within an analysis.
  • TLS inspection (src/infralens/lib/security/inspect-tls.ts) performs a raw handshake to report the negotiated protocol version, certificate issuer, expiration, and validity — surfaced as a failure if invalid, a warning if expiring within 30 days.
  • DNSSEC is reported as not-evaluated — Node's built-in resolver has no DS/DNSKEY/RRSIG support.

Scoring

Each category has a fixed weight (table above, sums to 100). Within a category:

  • pass — 100% of its share of the category weight
  • warning — 60%
  • fail — 0%
  • info / unavailable / error — excluded entirely, never counted for or against the score

The 0–100 score maps to a letter grade (A–E), worded deliberately as a visual aid ("Strong configuration signals" … "Major public configuration issues detected") rather than a certification. The report's "Why this score?" panel shows the live breakdown for that specific analysis.

Local history and comparison

  • History (src/infralens/lib/history/) — the last 10 analyses, versioned in localStorage; a missing or mismatched schema version is treated as incompatible and reset cleanly rather than risking a half-parsed shape reaching the UI. Never sent to a server.
  • Compare (/tools/infralens/compare, src/infralens/lib/compare/) — import two exported JSON reports, see score/category deltas and every check that improved, regressed, or changed, and export the diff as Markdown. Entirely client-side.
  • Export — JSON or Markdown, from src/infralens/lib/checks/export.ts and export-markdown.ts. Structural validation (validate-export.ts) rejects malformed files and refuses reports from an incompatible major schema version instead of silently comparing them.

Environment variables

Everything is optional — InfraLens runs with zero configuration.

# .env.local

# Optional: raises the IP/ASN lookup limit via ipapi.co (1,000 free req/day without a key)
IPAPI_KEY=

# Optional: canonical URL override, only needed if deploying somewhere
# other than randy-code.dev
NEXT_PUBLIC_SITE_URL=

Validated once at module load (src/infralens/config/env.ts) — consumers read the parsed env object instead of process.env directly.

Rate limiting is configured separately, shared with the rest of the repo (UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN) — see the root README.md.

Development

InfraLens uses the same toolchain as the rest of the repository — see the root README.md for setup. InfraLens-specific commands:

pnpm dev             # http://localhost:3000/tools/infralens
pnpm test            # unit tests (includes InfraLens's ~260 tests)
pnpm e2e:infralens    # Playwright E2E (rate-limited flow — runs single-worker)

pnpm e2e:infralens starts its own server and runs against real DNS/ network (deliberately not mocked), including one real analysis against example.com — see CONTRIBUTING.md.

Limitations

  • Read-only — passive analysis only, no exploitation, no intrusive scanning, no modification of the target.
  • Heuristic where it matters — stack and WAF/CDN detection are confidence-graded, never presented as certain, and never affect the score.
  • Single snapshot — reachability and performance checks represent one point in time, not historical monitoring.
  • Indicators, not guarantees — results are a starting point for investigation, not a security certification.
  • Only run analyses on sites you're authorized to inspect — the target sees InfraLens's requests in its own logs, same as any other visitor.

License

InfraLens is MIT licensed, scoped to this directory — LICENSE. This does not extend to the rest of the Randy Code repository, which has no repository-wide open-source license (see the root README.md).