Adding a Check
Adding a new check to recon-web involves creating a handler in core, registering it, building a renderer in web, and writing tests. Follow the steps below.
-
Create the handler
Section titled “Create the handler”Create a new file at
packages/core/src/handlers/my-check.ts.Every handler must satisfy the
AnalysisHandler<T>signature and return aHandlerResult<T>. Import helpers from the shared utilities.packages/core/src/handlers/my-check.ts import type { AnalysisHandler, HandlerResult } from '../types.js';import { extractHostname } from '../utils.js';export interface MyCheckResult {headerPresent: boolean;headerValue: string | null;}export const myCheckHandler: AnalysisHandler<MyCheckResult> = async (url, options) => {const hostname = extractHostname(url);try {const response = await fetch(url, {signal: options?.signal,});const value = response.headers.get('x-custom-header');return {data: {headerPresent: value !== null,headerValue: value,},};} catch (error) {return {error: error instanceof Error ? error.message : String(error),errorCode: 'TIMEOUT',errorCategory: 'site',};}};Error codes reference
Section titled “Error codes reference”Use the appropriate
ErrorCodewhen returning an error:ErrorCode When to use TIMEOUTThe request exceeded the allowed time DNS_FAILUREThe hostname could not be resolved CONNECTION_REFUSEDTCP connection was rejected MISSING_API_KEYA required third-party API key is not configured NO_DATAThe check ran successfully but produced no meaningful result INVALID_URLThe input URL is malformed SSRF_BLOCKEDThe URL resolved to a private/internal IP REQUIRES_CHROMIUMThe check needs a headless browser that is not available NOT_FOUNDThe target resource was not found (404) Error categories
Section titled “Error categories”ErrorCategory Meaning toolA problem with the recon-web framework or infrastructure siteA problem with the target website infoThe check was intentionally skipped (e.g., not applicable) -
Export from the handlers barrel
Section titled “Export from the handlers barrel”Add the export to
packages/core/src/handlers/index.ts:packages/core/src/handlers/index.ts // ... existing exportsexport { myCheckHandler } from './my-check.js';export type { MyCheckResult } from './my-check.js'; -
Register in the registry
Section titled “Register in the registry”Open
packages/core/src/registry.tsand add an entry:packages/core/src/registry.ts import { myCheckHandler } from './handlers/my-check.js';registry.register({name: 'my-check',description: 'Check for the presence of X-Custom-Header',category: 'http',requires: [],handler: myCheckHandler,});The
requiresarray declares runtime dependencies. Common values:'chromium'— needs a headless browser'apiKey:shodan'— needs a third-party API key
Leave the array empty if the handler only needs outbound HTTP.
-
Add presentation metadata
Section titled “Add presentation metadata”Open
packages/core/src/presentation.tsand add a display name and short description:packages/core/src/presentation.ts // ... existing entries'my-check': {displayName: 'Custom Header Check',shortDescription: 'Looks for X-Custom-Header in the HTTP response',}, -
Create a web renderer
Section titled “Create a web renderer”Create
packages/web/src/components/results/renderers/MyCheckRenderer.tsx:packages/web/src/components/results/renderers/MyCheckRenderer.tsx import type { MyCheckResult } from '@recon-web/core';import { ChecklistItem } from '../primitives/ChecklistItem';import { KeyValueTable } from '../primitives/KeyValueTable';import { SectionLabel } from '../primitives/SectionLabel';interface Props {data: MyCheckResult;}export function MyCheckRenderer({ data }: Props) {return (<div><SectionLabel>Custom Header</SectionLabel><ChecklistItemlabel="X-Custom-Header present"passed={data.headerPresent}/>{data.headerValue && (<KeyValueTablerows={[{ key: 'Value', value: data.headerValue }]}/>)}</div>);}Then register it in
packages/web/src/components/results/renderers/index.ts:packages/web/src/components/results/renderers/index.ts import { MyCheckRenderer } from './MyCheckRenderer';// ... inside the renderers map'my-check': MyCheckRenderer,Available renderer primitives
Section titled “Available renderer primitives”Component Purpose ChecklistItemA pass/fail item with a label and boolean status KeyValueTableA two-column table of key-value pairs SectionLabelA heading for grouping related results ChipA small label/badge (e.g., severity, status) CodeBlockA syntax-highlighted block for raw output -
Write tests
Section titled “Write tests”Create
packages/core/src/handlers/my-check.test.ts. Use Vitest and MSW to mock HTTP responses:packages/core/src/handlers/my-check.test.ts import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';import { setupServer } from 'msw/node';import { http, HttpResponse } from 'msw';import { myCheckHandler } from './my-check.js';const server = setupServer();beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());describe('myCheckHandler', () => {it('detects the header when present', async () => {server.use(http.get('https://example.com/', () =>HttpResponse.text('OK', {headers: { 'x-custom-header': 'some-value' },}),),);const result = await myCheckHandler('https://example.com/');expect(result.data?.headerPresent).toBe(true);expect(result.data?.headerValue).toBe('some-value');});it('reports absence when the header is missing', async () => {server.use(http.get('https://example.com/', () => HttpResponse.text('OK')),);const result = await myCheckHandler('https://example.com/');expect(result.data?.headerPresent).toBe(false);expect(result.data?.headerValue).toBeNull();});it('returns an error result on network failure', async () => {server.use(http.get('https://example.com/', () => HttpResponse.error()),);const result = await myCheckHandler('https://example.com/');expect(result.error).toBeDefined();expect(result.errorCategory).toBe('site');});}); -
Build and verify
Section titled “Build and verify”Terminal window # Rebuild core with the new handlernpx tsc -b packages/core# Run core testsnpm test -w @recon-web/core# Confirm the full build still worksnpx tsc --build
Complete example: HTTP header checker
Section titled “Complete example: HTTP header checker”Below is a self-contained handler that checks whether a site sends common security headers. This is a realistic example you can use as a starting template.
import type { AnalysisHandler, HandlerResult } from '../types.js';import { normalizeUrl } from '../utils.js';
const SECURITY_HEADERS = [ 'strict-transport-security', 'content-security-policy', 'x-content-type-options', 'x-frame-options', 'referrer-policy', 'permissions-policy',] as const;
export interface SecurityHeadersResult { headers: Array<{ name: string; present: boolean; value: string | null; }>; score: number; // 0-100}
export const securityHeadersHandler: AnalysisHandler<SecurityHeadersResult> = async ( url, options,) => { const normalized = normalizeUrl(url);
try { const response = await fetch(normalized, { method: 'HEAD', redirect: 'follow', signal: options?.signal, });
const headers = SECURITY_HEADERS.map((name) => { const value = response.headers.get(name); return { name, present: value !== null, value }; });
const presentCount = headers.filter((h) => h.present).length; const score = Math.round((presentCount / SECURITY_HEADERS.length) * 100);
return { data: { headers, score } }; } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { return { error: 'Request timed out', errorCode: 'TIMEOUT', errorCategory: 'site' }; } return { error: error instanceof Error ? error.message : String(error), errorCode: 'CONNECTION_REFUSED', errorCategory: 'site', }; }};