diff --git a/README.md b/README.md index 24308a8..279f42f 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,6 @@ import { HardenedHttpsValidationKit, defaultAgentOptions } from 'hardened-https- // Create a validation kit with hardened defaults const kit = new HardenedHttpsValidationKit({ ...defaultAgentOptions(), - enableLogging: true, }); // Define your HTTPS proxy agent options as usual @@ -181,7 +180,7 @@ this.#kit.attachToSocket(socket, callback); ### `HardenedHttpsAgent` & `HardenedHttpsValidationKit` Options -The options below are used to configure the security policies of the `HardenedHttpsAgent`. When using the `HardenedHttpsValidationKit`, only a subset of these options are available (`ctPolicy`, `ocspPolicy`, `crlSetPolicy`, and `enableLogging`). +The options below are used to configure the security policies of the `HardenedHttpsAgent`. When using the `HardenedHttpsValidationKit`, only a subset of these options are available (`ctPolicy`, `ocspPolicy`, `crlSetPolicy`, and `loggerOptions`). | **Property** | **Type** | **Required / Variants** | **Helper(s)** | | ------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | @@ -189,7 +188,7 @@ The options below are used to configure the security policies of the `HardenedHt | `ctPolicy` | `CertificateTransparencyPolicy` | Optional. Enables CT when present. Fields: `logList: UnifiedCTLogList`, `minEmbeddedScts?: number`, `minDistinctOperators?: number`. | `basicCtPolicy()`, `embeddedUnifiedCtLogList` | | `ocspPolicy` | `OCSPPolicy` | Optional. Enables OCSP when present. Fields: `mode: 'mixed' \| 'stapling' \| 'direct'`, `failHard: boolean`. | `basicStaplingOcspPolicy()`, `basicDirectOcspPolicy()` | | `crlSetPolicy` | `CRLSetPolicy` | Optional. Enables CRLSet when present. Fields: `crlSet?: CRLSet`, `verifySignature?: boolean`, `updateStrategy?: 'always' \| 'on-expiry'`. | `basicCrlSetPolicy()` | -| `enableLogging` | `boolean` | Optional (default: `false`). | | +| `loggerOptions` | `LoggerOptions` | Optional logging configuration (level, sink, formatter, template). | `defaultLoggerOptions()` | | Standard HTTPS opts | `https.AgentOptions` | Optional. Any standard Node.js `https.Agent` options (e.g., `keepAlive`, `maxSockets`, `timeout`, `maxFreeSockets`, `maxCachedSessions`) can be merged alongside the hardened options. | | _All options are thoroughly documented directly in the library via JSDoc comments for easy in-editor reference and autocomplete._ @@ -283,7 +282,7 @@ new HardenedHttpsAgent({ Enable detailed logs: ```typescript -new HardenedHttpsAgent({ ...defaultAgentOptions(), enableLogging: true }); +new HardenedHttpsAgent({ ...defaultAgentOptions(), loggerOptions: { level: 'debug' } }); ``` ## Contributing diff --git a/examples/axios.ts b/examples/axios.ts index 0096b9f..ab4567a 100644 --- a/examples/axios.ts +++ b/examples/axios.ts @@ -16,7 +16,9 @@ async function main() { const agent = new HardenedHttpsAgent({ ...httpsAgentOptions, ...defaultAgentOptions(), - enableLogging: true, // Enable logging to see the validation process (disabled with defaultAgentOptions()) + loggerOptions: { + level: 'debug', + } }); const client = axios.create({ httpsAgent: agent, timeout: 15000 }); diff --git a/examples/custom-options.ts b/examples/custom-options.ts index 1d42ad2..ced45fb 100644 --- a/examples/custom-options.ts +++ b/examples/custom-options.ts @@ -1,6 +1,10 @@ import axios from 'axios'; import https from 'node:https'; -import { HardenedHttpsAgent, embeddedUnifiedCtLogList, embeddedCfsslCaBundle } from '../dist'; +import { HardenedHttpsAgent, embeddedUnifiedCtLogList, embeddedCfsslCaBundle, LogFormatter } from '../dist'; + +const customLogFormatter: LogFormatter = (level, component, message, args) => { + return { message: `[${level.toUpperCase()}] ${component}: ${message}`, args }; +}; async function main() { // Customize standard agent options if required @@ -14,27 +18,29 @@ async function main() { // Merge standard agent options with hardened defaults and some custom policies // Here we use values from the default options, but you can customize them as you want - const agent = new HardenedHttpsAgent( - { - ...httpsAgentOptions, - ca: embeddedCfsslCaBundle, // or *your custom ca bundle* | useNodeDefaultCaBundle() - ctPolicy: { - logList: embeddedUnifiedCtLogList, // or *your custom log list* - minEmbeddedScts: 2, - minDistinctOperators: 2, - }, - ocspPolicy: { - mode: 'mixed', // or 'stapling' | 'direct' - failHard: true, - }, - crlSetPolicy: { - verifySignature: true, - updateStrategy: 'always', // or 'on-expiry' - }, - enableLogging: true, // Enable logging to see the validation process (disabled with defaultAgentOptions()) + const agent = new HardenedHttpsAgent({ + ...httpsAgentOptions, + ca: embeddedCfsslCaBundle, // or *your custom ca bundle* | useNodeDefaultCaBundle() + ctPolicy: { + logList: embeddedUnifiedCtLogList, // or *your custom log list* + minEmbeddedScts: 2, + minDistinctOperators: 2, + }, + ocspPolicy: { + mode: 'mixed', // or 'stapling' | 'direct' + failHard: true, + }, + crlSetPolicy: { + verifySignature: true, + updateStrategy: 'always', // or 'on-expiry' }, - console, // or your own `LogSink` (default is `console`) - ); + loggerOptions: { + level: 'debug', // or 'info' | 'warn' | 'error' | 'silent' + sink: console, // or *your custom sink* + template: '{time} [{name}] {level}: {message}', // or *your custom template* + formatter: customLogFormatter, // or *your custom formatter* + } + }); const client = axios.create({ httpsAgent: agent, timeout: 15000 }); try { diff --git a/examples/got.ts b/examples/got.ts index 1157f50..1192d4b 100644 --- a/examples/got.ts +++ b/examples/got.ts @@ -16,7 +16,9 @@ async function main() { const agent = new HardenedHttpsAgent({ ...httpsAgentOptions, ...defaultAgentOptions(), - enableLogging: true, // Enable logging to see the validation process (disabled with defaultAgentOptions()) + loggerOptions: { + level: 'debug', + } }); const client = got.extend({ diff --git a/examples/https-native.ts b/examples/https-native.ts index 92ffb35..51f0a12 100644 --- a/examples/https-native.ts +++ b/examples/https-native.ts @@ -15,7 +15,9 @@ async function main() { const agent = new HardenedHttpsAgent({ ...httpsAgentOptions, ...defaultAgentOptions(), - enableLogging: true, // Enable logging to see the validation process (disabled with defaultAgentOptions()) + loggerOptions: { + level: 'debug', + } }); try { diff --git a/examples/validation-kit.ts b/examples/validation-kit.ts index d6b21fa..04222a6 100644 --- a/examples/validation-kit.ts +++ b/examples/validation-kit.ts @@ -7,7 +7,9 @@ async function main() { // Create a validation kit with hardened defaults const kit = new HardenedHttpsValidationKit({ ...defaultAgentOptions(), - enableLogging: true, + loggerOptions: { + level: 'debug', + } }); // Define your HTTPS proxy agent options as usual diff --git a/src/agent.ts b/src/agent.ts index 01155ff..b5f75ed 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -1,7 +1,7 @@ import { Agent } from 'node:https'; import tls from 'node:tls'; import type { Duplex } from 'node:stream'; -import { Logger, LogSink } from './logger'; +import { Logger } from './logger'; import { HardenedHttpsAgentOptions } from './interfaces'; import { HardenedHttpsValidationKit } from './validation-kit'; import { NODE_DEFAULT_CA_SENTINEL } from './options'; @@ -11,7 +11,7 @@ export class HardenedHttpsAgent extends Agent { #logger: Logger | undefined; #kit: HardenedHttpsValidationKit; - constructor(options: HardenedHttpsAgentOptions, sink?: LogSink) { + constructor(options: HardenedHttpsAgentOptions) { const useNodeDefaultCaBundle = (options as any)?.ca === NODE_DEFAULT_CA_SENTINEL; const optionsForSuper = useNodeDefaultCaBundle ? (({ ca, ...rest }) => rest)(options as any) : options; super(optionsForSuper); @@ -24,16 +24,16 @@ export class HardenedHttpsAgent extends Agent { throw new Error('The `ca` property cannot be empty.'); } - const { enableLogging, ctPolicy, ocspPolicy, crlSetPolicy } = this.#options; - if (enableLogging) this.#logger = new Logger(this.constructor.name, sink); - this.#kit = new HardenedHttpsValidationKit({ enableLogging, ctPolicy, ocspPolicy, crlSetPolicy }, sink); + const { ctPolicy, ocspPolicy, crlSetPolicy, loggerOptions } = this.#options; + if (loggerOptions) this.#logger = new Logger(this.constructor.name, loggerOptions); + this.#kit = new HardenedHttpsValidationKit({ ctPolicy, ocspPolicy, crlSetPolicy, loggerOptions }); } override createConnection( options: tls.ConnectionOptions, callback: (err: Error | null, stream: Duplex) => void, ): Duplex { - this.#logger?.log('Initiating new TLS connection...'); + this.#logger?.info('Initiating new TLS connection...'); // Allow validators to modify the connection options const finalOptions = this.#kit.applyBeforeConnect(options); @@ -42,11 +42,12 @@ export class HardenedHttpsAgent extends Agent { const tlsSocket = tls.connect(finalOptions); // Handle validation success tlsSocket.on('hardened:validation:success', () => { + this.#logger?.info('TLS connection established and validated.'); callback(null, tlsSocket); }); // Handle socket errors tlsSocket.on('error', (err: Error) => { - this.#logger?.error('A socket error occurred during connection setup.', err); + this.#logger?.error('An error occurred during TLS connection setup', err); callback(err, undefined as any); }); diff --git a/src/index.ts b/src/index.ts index 5a1f9b0..e11532c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ export { HardenedHttpsAgent } from './agent'; +export { HardenedHttpsValidationKit } from './validation-kit'; + export type { HardenedHttpsAgentOptions, CertificateTransparencyPolicy, @@ -19,4 +21,5 @@ export { defaultAgentOptions, } from './options'; -export { HardenedHttpsValidationKit, type ValidationKitEvents } from './validation-kit'; +export { createTemplateFormatter } from './logger'; +export type { LogSink, BindableLogSink, LogFormatter, LogLevel } from './logger'; diff --git a/src/interfaces.ts b/src/interfaces.ts index be5c985..acb100e 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,6 +1,7 @@ import type { AgentOptions } from 'node:https'; import type { UnifiedCertificateTransparencyLogList as UnifiedCTLogList } from './types/uni-ct-log-list-schema'; import type { CRLSet } from '@gldywn/crlset.js'; +import { LoggerOptions } from './logger'; export interface HardenedHttpsAgentOptions extends AgentOptions { /** @@ -31,19 +32,20 @@ export interface HardenedHttpsAgentOptions extends AgentOptions { crlSetPolicy?: CRLSetPolicy; /** - * An optional boolean to enable or disable logging. - * - * @default false + * Optional logger options. */ - enableLogging?: boolean; + loggerOptions?: LoggerOptions; } // A minimal subset of options required for validation behavior only export type HardenedHttpsValidationKitOptions = Pick< HardenedHttpsAgentOptions, - 'ctPolicy' | 'ocspPolicy' | 'crlSetPolicy' | 'enableLogging' + 'ctPolicy' | 'ocspPolicy' | 'crlSetPolicy' | 'loggerOptions' >; +// Options passed down to the validators to perform their checks +export type ValidatorsOptions = Omit; + export interface CertificateTransparencyPolicy { /** * The complete Certificate Transparency log list object. diff --git a/src/logger.ts b/src/logger.ts index 552e246..8c53a80 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,27 +1,142 @@ -/* istanbul ignore next */ +/* istanbul ignore file */ + +const DEFAULT_TEMPLATE = '{time} [{name}] {level}: {message}'; export class Logger { - private name: string; - private sink: LogSink; - constructor(name: string, sink?: LogSink) { + private readonly name: string; + private readonly sink: LogSink; + private readonly formatter?: LogFormatter; + private readonly minLevel: EffectiveLogLevel; + + constructor(name: string, options?: LoggerOptions) { this.name = name; - this.sink = sink ?? console; + + const sink = options?.sink; + const formatter = + options?.formatter ?? + (options?.template ? createTemplateFormatter(options.template) : createTemplateFormatter(DEFAULT_TEMPLATE)); + this.minLevel = options?.level ?? 'info'; + + if (sink && typeof (sink as any).bind === 'function') { + const bound = (sink as BindableLogSink).bind(this.name); + this.sink = bound; + } else { + this.sink = sink ?? console; + } + + this.formatter = formatter; + } + + public debug(message: any, ...args: any[]): void { + if (!this.shouldLog('debug')) return; + const { outMessage, outArgs } = this.prepare('debug', message, args); + if (typeof this.sink.debug === 'function') { + this.sink.debug(outMessage, ...outArgs); + } else { + this.sink.info(outMessage, ...outArgs); + } } - public log(message: string, ...args: any[]): void { - this.sink.log(`[Log] ${this.name}: ${message}`, ...args); + public info(message: any, ...args: any[]): void { + if (!this.shouldLog('info')) return; + const { outMessage, outArgs } = this.prepare('info', message, args); + this.sink.info(outMessage, ...outArgs); } - public warn(message: string, ...args: any[]): void { - this.sink.warn(`[Warning] ${this.name}: ${message}`, ...args); + public warn(message: any, ...args: any[]): void { + if (!this.shouldLog('warn')) return; + const { outMessage, outArgs } = this.prepare('warn', message, args); + this.sink.warn(outMessage, ...outArgs); } - public error(message: string, ...args: any[]): void { - this.sink.error(`[Error] ${this.name}: ${message}`, ...args); + public error(message: any, ...args: any[]): void { + if (!this.shouldLog('error')) return; + const { outMessage, outArgs } = this.prepare('error', message, args); + this.sink.error(outMessage, ...outArgs); + } + + private prepare(level: LogLevel, message: any, args: any[]) { + if (!this.formatter) throw new Error('No formatter set'); + + const { message: formatted, args: formattedArgs } = this.formatter(level, this.name, message, args); + return { outMessage: formatted, outArgs: formattedArgs }; + } + + private shouldLog(level: LogLevel): boolean { + return priority(level) >= priority(this.minLevel); } } +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; +export type EffectiveLogLevel = LogLevel | 'silent'; + export interface LogSink { - log(message: string, ...args: any[]): void; - warn(message: string, ...args: any[]): void; - error(message: string, ...args: any[]): void; + debug?(message: any, ...args: any[]): void; + info(message: any, ...args: any[]): void; + warn(message: any, ...args: any[]): void; + error(message: any, ...args: any[]): void; +} + +export interface BindableLogSink extends LogSink { + bind(component: string): LogSink; +} + +export type LogFormatter = ( + level: LogLevel, + component: string, + message: any, + args: any[], +) => { message: any; args: any[] }; + +export interface LoggerOptions { + sink?: LogSink | BindableLogSink; + formatter?: LogFormatter; + template?: string; + level?: EffectiveLogLevel; +} + +export function createTemplateFormatter(template: string): LogFormatter { + return (level, component, message, args) => { + const now = new Date().toISOString(); + const tokens: Record = { + time: now, + level: level.toUpperCase(), + name: component, + message: toSingleLineString(message, args), + }; + + const out = template.replace(/\{(time|level|name|message)\}/g, (_, key: keyof typeof tokens) => tokens[key]); + return { message: out, args: [] }; + }; +} + +function toSingleLineString(message: any, args: any[]): string { + const parts = [message, ...args].map((v) => formatValue(v)); + return parts.join(' '); +} + +function formatValue(v: any): string { + if (typeof v === 'string') return v; + if (v instanceof Error) return v.stack || v.message || String(v); + try { + return JSON.stringify(v); + } catch { + return String(v); + } +} + +function priority(level: EffectiveLogLevel): number { + switch (level) { + case 'silent': + return 99; + case 'error': + return 40; + case 'warn': + return 30; + case 'info': + return 20; + case 'debug': + return 10; + default: + return 20; + } } diff --git a/src/options.ts b/src/options.ts index a77a27b..0134799 100644 --- a/src/options.ts +++ b/src/options.ts @@ -9,6 +9,7 @@ import { import { type UnifiedCertificateTransparencyLogList as UnifiedCTLogList } from './types/uni-ct-log-list-schema'; import * as _cfsslCaBundle from './resources/cfssl-ca-bundle.crt'; import unifiedCtLogListJson from './resources/unified-log-list.json'; +import { LoggerOptions } from './logger'; export const NODE_DEFAULT_CA_SENTINEL = '__USE_NODE_DEFAULT_CA_BUNDLE__'; @@ -74,12 +75,18 @@ export const basicCrlSetPolicy = (): CRLSetPolicy => { }; }; +export const defaultLoggerOptions = (): LoggerOptions => { + return { + level: 'info', + }; +}; + export const defaultAgentOptions = (): HardenedHttpsAgentOptions => { return { ca: embeddedCfsslCaBundle, ctPolicy: basicCtPolicy(), ocspPolicy: basicMixedOcspPolicy(), crlSetPolicy: basicCrlSetPolicy(), - enableLogging: false, + loggerOptions: undefined, }; }; diff --git a/src/validation-kit.ts b/src/validation-kit.ts index 5b43d3a..200fa05 100644 --- a/src/validation-kit.ts +++ b/src/validation-kit.ts @@ -1,8 +1,8 @@ import http from 'node:http'; import https from 'node:https'; import tls from 'node:tls'; -import { Logger, LogSink } from './logger'; -import type { HardenedHttpsValidationKitOptions } from './interfaces'; +import { Logger } from './logger'; +import type { HardenedHttpsValidationKitOptions, ValidatorsOptions } from './interfaces'; import { BaseValidator } from './validators/base'; import { CTValidator, @@ -13,15 +13,15 @@ import { } from './validators'; export class HardenedHttpsValidationKit { - private readonly options: HardenedHttpsValidationKitOptions; private readonly logger: Logger | undefined; + private readonly validatorsOpts: ValidatorsOptions; private readonly validators: BaseValidator[]; private readonly validatedSockets: WeakSet = new WeakSet(); - constructor(options: HardenedHttpsValidationKitOptions, sink?: LogSink) { - this.options = options; - if (options.enableLogging) this.logger = new Logger(this.constructor.name, sink); + constructor({ loggerOptions, ...options }: HardenedHttpsValidationKitOptions) { + if (loggerOptions) this.logger = new Logger(this.constructor.name, loggerOptions); + this.validatorsOpts = options; this.validators = [ new CTValidator(this.logger), new OCSPStaplingValidator(this.logger), @@ -32,7 +32,7 @@ export class HardenedHttpsValidationKit { } private getActiveValidators(): BaseValidator[] { - return this.validators.filter((v) => v.shouldRun(this.options)); + return this.validators.filter((v) => v.shouldRun(this.validatorsOpts)); } public applyBeforeConnect(options: T): T { @@ -52,28 +52,33 @@ export class HardenedHttpsValidationKit { const active = this.getActiveValidators(); if (active.length === 0) { + this.logger?.info('No validators enabled, skipping validation...'); tlsSocket.emit('hardened:validation:success'); return; } + this.logger?.info( + `Running validation with ${active.length} enabled validator(s): ${active.map((v) => v.constructor.name).join(', ')}...`, + ); + let shouldResume = false; try { // TODO: Check if best to pause the socket right after `secureConnect` event tlsSocket.pause(); - this.logger?.log('Socket read paused'); + this.logger?.debug('Socket read paused'); shouldResume = true; } catch (err) { /* istanbul ignore next */ this.logger?.warn('Failed to pause socket', err); } - Promise.all(active.map((v) => v.validate(tlsSocket, this.options))) + Promise.all(active.map((v) => v.validate(tlsSocket, this.validatorsOpts))) .then(() => { - this.logger?.log('All enabled validators passed.'); + this.logger?.info('All enabled validators passed.'); if (shouldResume) { try { tlsSocket.resume(); - this.logger?.log('Socket read resumed'); + this.logger?.debug('Socket read resumed'); } catch (err) { /* istanbul ignore next */ this.logger?.warn('Failed to resume socket', err); diff --git a/src/validators/base.ts b/src/validators/base.ts index 382ab02..4af63a9 100644 --- a/src/validators/base.ts +++ b/src/validators/base.ts @@ -1,6 +1,6 @@ import * as tls from 'tls'; import { Logger } from '../logger'; -import { HardenedHttpsValidationKitOptions } from '../interfaces'; +import { ValidatorsOptions } from '../interfaces'; export class WrappedError extends Error { public cause?: unknown; @@ -20,6 +20,7 @@ export class WrappedError extends Error { * It handles the injection of the agent context, which provides access * to logging and other shared agent methods. */ +/* istanbul ignore next */ export abstract class BaseValidator { private logger: Logger | undefined; @@ -27,8 +28,12 @@ export abstract class BaseValidator { this.logger = logger; } - protected log(message: string, ...args: any[]): void { - this.logger?.log(`[${this.constructor.name}] ${message}`, ...args); + protected debug(message: string, ...args: any[]): void { + this.logger?.debug(`[${this.constructor.name}] ${message}`, ...args); + } + + protected info(message: string, ...args: any[]): void { + this.logger?.info(`[${this.constructor.name}] ${message}`, ...args); } protected warn(message: string, ...args: any[]): void { @@ -51,12 +56,12 @@ export abstract class BaseValidator { * Checks if this validation should run based on the agent's options. * This must be implemented by all concrete validator classes. */ - abstract shouldRun(options: HardenedHttpsValidationKitOptions): boolean; + abstract shouldRun(options: ValidatorsOptions): boolean; /** * Runs the validation logic for this validator. * Returns a Promise that resolves if validation passes, or rejects if it fails. * This must be implemented by all concrete validator classes. */ - abstract validate(socket: tls.TLSSocket, options: HardenedHttpsValidationKitOptions): Promise; + abstract validate(socket: tls.TLSSocket, options: ValidatorsOptions): Promise; } diff --git a/src/validators/crlset.ts b/src/validators/crlset.ts index 035552a..5ae35ce 100644 --- a/src/validators/crlset.ts +++ b/src/validators/crlset.ts @@ -24,7 +24,7 @@ export class CRLSetValidator extends BaseValidator { return new Promise((resolve, reject) => { socket.once('secureConnect', async () => { - this.log('Secure connection established, performing validation...'); + this.debug('Secure connection established, performing validation...'); try { const { leafCert, issuerCert } = getLeafAndIssuerCertificates(socket); @@ -41,12 +41,12 @@ export class CRLSetValidator extends BaseValidator { if (policy.crlSet) { crlSet = policy.crlSet; } else { - this.log('Downloading latest CRLSet...'); + this.debug('Downloading latest CRLSet...'); crlSet = await loadLatestCRLSet({ verifySignature: policy.verifySignature, updateStrategy: policy.updateStrategy, }); - this.log('Latest CRLSet downloaded successfully.'); + this.debug('Latest CRLSet downloaded successfully.'); } const revocationStatus = crlSet.check(issuerSpkiHash, leafSerialNumber); @@ -60,7 +60,7 @@ export class CRLSetValidator extends BaseValidator { ); } - this.log(`Certificate is not revoked according to CRLSet ${crlSet.sequence}.`); + this.debug(`Certificate is not revoked according to CRLSet ${crlSet.sequence}.`); resolve(); } catch (err: any) { reject(this.wrapError(err)); diff --git a/src/validators/ct.ts b/src/validators/ct.ts index 80832b9..a22822a 100644 --- a/src/validators/ct.ts +++ b/src/validators/ct.ts @@ -23,7 +23,7 @@ export class CTValidator extends BaseValidator { public validate(socket: tls.TLSSocket, options: HardenedHttpsValidationKitOptions): Promise { return new Promise((resolve, reject) => { socket.once('secureConnect', () => { - this.log('Secure connection established, performing validation...'); + this.debug('Secure connection established, performing validation...'); try { const ctError = this.validateCertificateTransparency(socket, options.ctPolicy!); @@ -109,13 +109,13 @@ export class CTValidator extends BaseValidator { scts.push(sctData); offset += sctLen; } - this.log(`Found ${scts.length} embedded SCT(s).`); + this.debug(`Found ${scts.length} embedded SCT(s).`); const trustedLogs = fromUnifiedCtLogList(policy.logList); if (trustedLogs.length === 0) { return makeError(new Error('Empty trusted CT log list.')); } - this.log(`Found ${trustedLogs.length} trusted CT logs.`); + this.debug(`Found ${trustedLogs.length} trusted CT logs.`); let signedEntry: Buffer; try { @@ -136,7 +136,7 @@ export class CTValidator extends BaseValidator { } } - this.log(`Successfully validated ${validScts.length} out of ${scts.length} embedded SCT(s).`); + this.debug(`Successfully validated ${validScts.length} out of ${scts.length} embedded SCT(s).`); return { totalScts: scts.length, validScts }; } catch (error) /* istanbul ignore next */ { return makeError(new Error('Failed to parse SCT list from certificate.', { cause: error })); @@ -169,7 +169,7 @@ export class CTValidator extends BaseValidator { // If we have any valid SCTs and all policy requirements are met, we're compliant if (validScts.length > 0) { - this.log( + this.debug( `Certificate is CT compliant with ${validScts.length} valid embedded SCT(s) from ${new Set(validScts.map((sct) => sct.logOperator)).size} distinct operator(s).`, ); return undefined; diff --git a/src/validators/ocsp-direct.ts b/src/validators/ocsp-direct.ts index 4779021..cb2d084 100644 --- a/src/validators/ocsp-direct.ts +++ b/src/validators/ocsp-direct.ts @@ -19,11 +19,11 @@ export class OCSPDirectValidator extends OCSPBaseValidator { return new Promise((resolve, reject) => { socket.once('secureConnect', async () => { - this.log('Secure connection established, performing direct OCSP validation...'); + this.debug('Secure connection established, performing direct OCSP validation...'); try { await this._performDirectOCSPCheck(socket); - this.log(`Certificate is not revoked.`); + this.debug(`Certificate is not revoked.`); resolve(); } catch (err: any) { this._handleOCSPError(err, failHard, reject, resolve); diff --git a/src/validators/ocsp-mixed.ts b/src/validators/ocsp-mixed.ts index 0a9f7f4..8233425 100644 --- a/src/validators/ocsp-mixed.ts +++ b/src/validators/ocsp-mixed.ts @@ -37,11 +37,11 @@ export class OCSPMixedValidator extends OCSPBaseValidator { // First, try to validate a stapled response. socket.once('OCSPResponse', async (response: Buffer) => { staplingAttempted = true; - this.log('OCSP stapling response received, performing validation...'); + this.debug('OCSP stapling response received, performing validation...'); try { await this._validateStapledResponse(response, socket); - this.log('OCSP stapling validation succeeded. Certificate is not revoked.'); + this.debug('OCSP stapling validation succeeded. Certificate is not revoked.'); validationComplete = true; resolve(); } catch (err: any) { @@ -63,11 +63,11 @@ export class OCSPMixedValidator extends OCSPBaseValidator { const fallbackLogMessage = staplingAttempted ? 'Falling back to direct OCSP check after failed stapling attempt.' : 'No OCSP staple received. Falling back to direct OCSP check.'; - this.log(fallbackLogMessage); + this.debug(fallbackLogMessage); try { await this._performDirectOCSPCheck(socket); - this.log('Direct OCSP validation succeeded. Certificate is not revoked.'); + this.debug('Direct OCSP validation succeeded. Certificate is not revoked.'); resolve(); } catch (err: any) { this._handleOCSPError(err, failHard, reject, resolve); diff --git a/src/validators/ocsp-stapling.ts b/src/validators/ocsp-stapling.ts index d33de31..d9a329c 100644 --- a/src/validators/ocsp-stapling.ts +++ b/src/validators/ocsp-stapling.ts @@ -32,12 +32,12 @@ export class OCSPStaplingValidator extends OCSPBaseValidator { let ocspReceived = false; socket.once('OCSPResponse', async (response: Buffer) => { - this.log('OCSP stapling response received, performing validation...'); + this.debug('OCSP stapling response received, performing validation...'); ocspReceived = true; try { await this._validateStapledResponse(response, socket); - this.log(`Certificate is not revoked.`); + this.debug(`Certificate is not revoked.`); resolve(); } catch (err: any) { this._handleOCSPError(err, failHard, reject, resolve); diff --git a/test/agent.test.ts b/test/agent.test.ts index c580083..33af21f 100644 --- a/test/agent.test.ts +++ b/test/agent.test.ts @@ -54,9 +54,8 @@ describe('HardenedHttpsAgent', () => { ctPolicy: baseOptions.ctPolicy, ocspPolicy: baseOptions.ocspPolicy, crlSetPolicy: baseOptions.crlSetPolicy, - enableLogging: baseOptions.enableLogging, - }, - undefined, // LogSink instance + loggerOptions: baseOptions.loggerOptions, + } ); }); diff --git a/test/e2e/acceptance.test.ts b/test/e2e/acceptance.test.ts index ac84caf..7acece3 100644 --- a/test/e2e/acceptance.test.ts +++ b/test/e2e/acceptance.test.ts @@ -73,7 +73,9 @@ describe('End-to-end policy validation on known acceptance scenarios', () => { const agent = new HardenedHttpsAgent({ ca: embeddedCfsslCaBundle, ...agentOptions, - enableLogging: true, + loggerOptions: { + level: 'debug', + } }); const config: AxiosRequestConfig = { diff --git a/test/e2e/failure.test.ts b/test/e2e/failure.test.ts index c620989..1a11eb0 100644 --- a/test/e2e/failure.test.ts +++ b/test/e2e/failure.test.ts @@ -115,7 +115,9 @@ describe('End-to-end policy validation on known failure scenarios', () => { const agent = new HardenedHttpsAgent({ ca: embeddedCfsslCaBundle, ...agentOptions, - enableLogging: true, + loggerOptions: { + level: 'debug', + } }); const config: AxiosRequestConfig = { diff --git a/test/e2e/validation-kit.test.ts b/test/e2e/validation-kit.test.ts index ba307d5..afd081c 100644 --- a/test/e2e/validation-kit.test.ts +++ b/test/e2e/validation-kit.test.ts @@ -26,7 +26,6 @@ describe('End-to-end HardenedHttpsValidationKit integration', () => { test('should allow ValidationKit to be attached to a standard https.Agent', (done) => { const kit = new HardenedHttpsValidationKit({ ctPolicy: basicCtPolicy(), - enableLogging: false, }); const agent = new https.Agent({ ca: getCa(), @@ -58,7 +57,6 @@ describe('End-to-end HardenedHttpsValidationKit integration', () => { test('should allow ValidationKit to be attached directly to a TLSSocket', (done) => { const kit = new HardenedHttpsValidationKit({ ctPolicy: basicCtPolicy(), - enableLogging: false, }); const socket = tls.connect({ @@ -87,4 +85,4 @@ describe('End-to-end HardenedHttpsValidationKit integration', () => { done(err); }); }); -}); \ No newline at end of file +}); diff --git a/test/utils/index.ts b/test/utils/index.ts index 26f77fe..33bfb42 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -61,18 +61,16 @@ export function getTestHardenedHttpsAgent( ctPolicy?: CertificateTransparencyPolicy | undefined; ocspPolicy?: OCSPPolicy | undefined; crlSetPolicy?: CRLSetPolicy | undefined; - enableLogging?: boolean; rejectUnauthorized?: boolean; } = {}, ) { - const { ca = TEST_CFSSL_CA_BUNDLE, ctPolicy, ocspPolicy, crlSetPolicy, enableLogging = false, rejectUnauthorized = true } = options; + const { ca = TEST_CFSSL_CA_BUNDLE, ctPolicy, ocspPolicy, crlSetPolicy, rejectUnauthorized = true } = options; return new HardenedHttpsAgent({ ca, ctPolicy, ocspPolicy, crlSetPolicy, - enableLogging, rejectUnauthorized, }); } diff --git a/test/validation-kit.test.ts b/test/validation-kit.test.ts index 77fcc28..f837736 100644 --- a/test/validation-kit.test.ts +++ b/test/validation-kit.test.ts @@ -45,7 +45,9 @@ describe('HardenedHttpsValidationKit', () => { let mockCrlSetValidator: MockValidator; const baseOptions: HardenedHttpsValidationKitOptions = { - enableLogging: false, + loggerOptions: { + level: 'silent', + } }; beforeEach(() => { @@ -70,11 +72,13 @@ describe('HardenedHttpsValidationKit', () => { const kit = new HardenedHttpsValidationKit(baseOptions); kit.attachToSocket(mockSocket); - expect(mockCtValidator.shouldRun).toHaveBeenCalledWith(baseOptions); - expect(mockOcspStaplingValidator.shouldRun).toHaveBeenCalledWith(baseOptions); - expect(mockOcspDirectValidator.shouldRun).toHaveBeenCalledWith(baseOptions); - expect(mockOcspMixedValidator.shouldRun).toHaveBeenCalledWith(baseOptions); - expect(mockCrlSetValidator.shouldRun).toHaveBeenCalledWith(baseOptions); + const baseOptionsWithoutLoggerOptions = { ...baseOptions, loggerOptions: undefined }; + + expect(mockCtValidator.shouldRun).toHaveBeenCalledWith(baseOptionsWithoutLoggerOptions); + expect(mockOcspStaplingValidator.shouldRun).toHaveBeenCalledWith(baseOptionsWithoutLoggerOptions); + expect(mockOcspDirectValidator.shouldRun).toHaveBeenCalledWith(baseOptionsWithoutLoggerOptions); + expect(mockOcspMixedValidator.shouldRun).toHaveBeenCalledWith(baseOptionsWithoutLoggerOptions); + expect(mockCrlSetValidator.shouldRun).toHaveBeenCalledWith(baseOptionsWithoutLoggerOptions); }); test('should only run validation for validators where shouldRun returns true', () => {