Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions packages/cli/src/constructs/__tests__/grpc-monitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,23 +109,36 @@ describe('GrpcMonitor', () => {
})

describe('validation', () => {
it('should error if degradedResponseTime is above 30000', async () => {
it('should error if degradedResponseTime is above 180000', async () => {
setupProject()
const check = new GrpcMonitor('test-check', {
name: 'Test Check',
request,
degradedResponseTime: 30001,
degradedResponseTime: 180001,
})
const diags = new Diagnostics()
await check.validate(diags)
expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('The value of "degradedResponseTime" must be 30000 or lower.'),
message: expect.stringContaining('The value of "degradedResponseTime" must be 180000 or lower.'),
}),
]))
})

it('should not error for a response time above 30000 (gRPC allows up to 180000)', async () => {
setupProject()
const check = new GrpcMonitor('test-check', {
name: 'Test Check',
request,
degradedResponseTime: 90000,
maxResponseTime: 120000,
})
const diags = new Diagnostics()
await check.validate(diags)
expect(diags.isFatal()).toEqual(false)
})

it('should not error within limits', async () => {
setupProject()
const check = new GrpcMonitor('test-check', {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest'

import { valueForSslAssertion } from '../ssl-assertion-codegen.js'
import { SslAssertion } from '../ssl-assertion.js'
import { GeneratedFile, Output } from '../../sourcegen/index.js'

function render (assertion: SslAssertion): string {
const output = new Output()
const file = new GeneratedFile('foo.ts')
const result = valueForSslAssertion(file, assertion)
result.render(output)
return output.finalize()
}

describe('SSL Assertion Codegen', () => {
it('generates the new SSL assertion sources', () => {
const cases: { input: SslAssertion, expected: string }[] = [
{
input: { source: 'OCSP_STAPLED', property: '', comparison: 'EQUALS', target: 'true', regex: null },
expected: 'SslAssertionBuilder.ocspStapled().equals(\'true\')\n',
},
]
for (const test of cases) {
expect(render(test.input)).toEqual(test.expected)
}
})
})
83 changes: 83 additions & 0 deletions packages/cli/src/constructs/__tests__/ssl-monitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,52 @@ describe('SslMonitor', () => {
expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } })
})

describe('assertions', () => {
it('synthesizes the new SSL assertion sources with backend-compatible operators', () => {
setupProject()
const check = new SslMonitor('test-check', {
name: 'Test Check',
request: {
hostname: 'example.com',
sslConfig: {},
assertions: [
SslAssertionBuilder.keySizeBits().equals(2048),
SslAssertionBuilder.tlsVersion().equals('TLS1.3'),
SslAssertionBuilder.cipherSuite().equals('TLS_AES_256_GCM_SHA384'),
SslAssertionBuilder.issuerCn().equals('Let\'s Encrypt'),
SslAssertionBuilder.ocspStapled().equals(true),
],
},
})

const payload = check.synthesize() as any
expect(payload.request.assertions).toEqual([
expect.objectContaining({ source: 'KEY_SIZE_BITS', comparison: 'EQUALS', target: '2048' }),
expect.objectContaining({ source: 'TLS_VERSION', comparison: 'EQUALS', target: 'TLS1.3' }),
expect.objectContaining({ source: 'CIPHER_SUITE', comparison: 'EQUALS', target: 'TLS_AES_256_GCM_SHA384' }),
expect.objectContaining({ source: 'ISSUER_CN', comparison: 'EQUALS', target: 'Let\'s Encrypt' }),
expect.objectContaining({ source: 'OCSP_STAPLED', comparison: 'EQUALS', target: 'true' }),
])
})

it('accepts a "degrade" baseline severity', () => {
setupProject()
const check = new SslMonitor('test-check', {
name: 'Test Check',
request: {
hostname: 'example.com',
sslConfig: {
securityBaseline: {
minTLSVersion: { value: 'TLS1.2', severity: 'degrade' },
},
},
},
})
const payload = check.synthesize() as any
expect(payload.request.sslConfig.securityBaseline.minTLSVersion.severity).toBe('degrade')
})
})

describe('validation', () => {
it('should error when clientCertificateMode is explicit but no certificate id is set', async () => {
setupProject()
Expand Down Expand Up @@ -176,6 +222,43 @@ describe('SslMonitor', () => {
]))
})

it('should error when degradedResponseTime exceeds the default maxResponseTime and max is omitted', async () => {
setupProject()
const check = new SslMonitor('test-check', {
name: 'Test Check',
// 20000 is within the 30000 degraded bound but above the 10000 default
// maxResponseTime the backend applies when max is omitted.
degradedResponseTime: 20000,
request: {
hostname: 'example.com',
sslConfig: {},
},
})
const diags = new Diagnostics()
await check.validate(diags)
expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('must be less than or equal to the default "maxResponseTime" of 10000'),
}),
]))
})

it('should not error when degradedResponseTime is within the default maxResponseTime and max is omitted', async () => {
setupProject()
const check = new SslMonitor('test-check', {
name: 'Test Check',
degradedResponseTime: 5000,
request: {
hostname: 'example.com',
sslConfig: {},
},
})
const diags = new Diagnostics()
await check.validate(diags)
expect(diags.isFatal()).toEqual(false)
})

it('should not error for a valid config', async () => {
setupProject()
const check = new SslMonitor('test-check', { name: 'Test Check', request })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest'

import { valueForTracerouteAssertion } from '../traceroute-assertion-codegen.js'
import { TracerouteAssertion } from '../traceroute-assertion.js'
import { GeneratedFile, Output } from '../../sourcegen/index.js'

function render (assertion: TracerouteAssertion): string {
const output = new Output()
const file = new GeneratedFile('foo.ts')
const result = valueForTracerouteAssertion(file, assertion)
result.render(output)
return output.finalize()
}

describe('Traceroute Assertion Codegen', () => {
it('preserves the response-time property as an argument', () => {
const cases: { input: TracerouteAssertion, expected: string }[] = [
{
input: { source: 'RESPONSE_TIME', property: 'max', comparison: 'LESS_THAN', target: '1000', regex: null },
expected: 'TracerouteAssertionBuilder.responseTime(\'max\').lessThan(1000)\n',
},
{
input: { source: 'RESPONSE_TIME', property: 'min', comparison: 'GREATER_THAN', target: '10', regex: null },
expected: 'TracerouteAssertionBuilder.responseTime(\'min\').greaterThan(10)\n',
},
{
input: { source: 'RESPONSE_TIME', property: 'stdDev', comparison: 'LESS_THAN', target: '50', regex: null },
expected: 'TracerouteAssertionBuilder.responseTime(\'stdDev\').lessThan(50)\n',
},
{
input: { source: 'RESPONSE_TIME', property: 'avg', comparison: 'LESS_THAN', target: '1000', regex: null },
expected: 'TracerouteAssertionBuilder.responseTime(\'avg\').lessThan(1000)\n',
},
]
for (const test of cases) {
expect(render(test.input)).toEqual(test.expected)
}
})

it('emits a bare responseTime() when no property is set', () => {
const input: TracerouteAssertion =
{ source: 'RESPONSE_TIME', property: '', comparison: 'LESS_THAN', target: '1000', regex: null }
expect(render(input)).toEqual('TracerouteAssertionBuilder.responseTime().lessThan(1000)\n')
})

it('does not emit a property for HOP_COUNT / PACKET_LOSS', () => {
const hop: TracerouteAssertion =
{ source: 'HOP_COUNT', property: '', comparison: 'LESS_THAN', target: '20', regex: null }
expect(render(hop)).toEqual('TracerouteAssertionBuilder.hopCount().lessThan(20)\n')

const loss: TracerouteAssertion =
{ source: 'PACKET_LOSS', property: '', comparison: 'LESS_THAN', target: '10', regex: null }
expect(render(loss)).toEqual('TracerouteAssertionBuilder.packetLoss().lessThan(10)\n')
})
})
43 changes: 43 additions & 0 deletions packages/cli/src/constructs/__tests__/traceroute-monitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,50 @@ describe('TracerouteMonitor', () => {
expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } })
})

describe('responseTime() assertions', () => {
it('should support selecting a specific response-time property', () => {
expect(TracerouteAssertionBuilder.responseTime('max').lessThan(2000)).toMatchObject({
source: 'RESPONSE_TIME',
property: 'max',
comparison: 'LESS_THAN',
target: '2000',
})
expect(TracerouteAssertionBuilder.responseTime('min').greaterThan(1)).toMatchObject({
source: 'RESPONSE_TIME',
property: 'min',
comparison: 'GREATER_THAN',
})
expect(TracerouteAssertionBuilder.responseTime('stdDev').lessThan(50)).toMatchObject({
source: 'RESPONSE_TIME',
property: 'stdDev',
})
expect(TracerouteAssertionBuilder.responseTime('avg').equals(100)).toMatchObject({
source: 'RESPONSE_TIME',
property: 'avg',
comparison: 'EQUALS',
})
})
})

describe('validation', () => {
it('should error when degradedResponseTime exceeds maxResponseTime', async () => {
setupProject()
const check = new TracerouteMonitor('test-check', {
name: 'Test Check',
request,
degradedResponseTime: 20000,
maxResponseTime: 10000,
})
const diags = new Diagnostics()
await check.validate(diags)
expect(diags.isFatal()).toEqual(true)
expect(diags.observations).toEqual(expect.arrayContaining([
expect.objectContaining({
message: expect.stringContaining('must be less than or equal to "maxResponseTime"'),
}),
]))
})

it('should error if maxResponseTime is above 30000', async () => {
setupProject()
const check = new TracerouteMonitor('test-check', {
Expand Down
12 changes: 8 additions & 4 deletions packages/cli/src/constructs/grpc-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface GrpcMonitorProps extends MonitorProps {
*
* @defaultValue 4000
* @minimum 0
* @maximum 30000
* @maximum 180000
* @example
* ```typescript
* degradedResponseTime: 3000 // Alert when the gRPC call takes longer than 3 seconds
Expand All @@ -30,7 +30,7 @@ export interface GrpcMonitorProps extends MonitorProps {
*
* @defaultValue 5000
* @minimum 0
* @maximum 30000
* @maximum 180000
* @example
* ```typescript
* maxResponseTime: 10000 // Fail if the gRPC call takes longer than 10 seconds
Expand Down Expand Up @@ -76,8 +76,12 @@ export class GrpcMonitor extends Monitor {
await super.validate(diagnostics)

await validateResponseTimes(diagnostics, this, {
degradedResponseTime: 30_000,
maxResponseTime: 30_000,
// gRPC allows thresholds up to 180s (calls can run to the 180s timeout),
// matching grpcResponseTimeLimitFields in response-time-limit-schema.ts.
degradedResponseTime: 180_000,
maxResponseTime: 180_000,
// Backend default applied when maxResponseTime is omitted.
defaultMaxResponseTime: 20_000,
})
}

Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/constructs/internal/common-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ type ResponseTimeProps = {
type ResponseTimeLimits = {
degradedResponseTime: number
maxResponseTime: number
// The per-type default that the backend applies to `maxResponseTime` when the
// caller omits it. Used so the degraded <= max cross-check still runs (against
// the effective max) when only `degradedResponseTime` is set explicitly.
defaultMaxResponseTime?: number
}

// eslint-disable-next-line require-await
Expand Down Expand Up @@ -166,4 +170,26 @@ export async function validateResponseTimes (
))
}
}

// When `maxResponseTime` is omitted the backend applies a per-type default, so
// compare `degradedResponseTime` against that effective max. This catches the
// case where an explicit degraded value exceeds the default max (which would
// otherwise pass CLI validation but produce a broken check server-side).
const effectiveMaxResponseTime = props.maxResponseTime ?? limits.defaultMaxResponseTime
if (
props.degradedResponseTime !== undefined
&& effectiveMaxResponseTime !== undefined
&& props.degradedResponseTime > effectiveMaxResponseTime
) {
diagnostics.add(new InvalidPropertyValueDiagnostic(
'degradedResponseTime',
new Error(
props.maxResponseTime !== undefined
? `The value of "degradedResponseTime" must be less than or equal to "maxResponseTime".`
: `The value of "degradedResponseTime" must be less than or equal to the default `
+ `"maxResponseTime" of ${effectiveMaxResponseTime}. Set an explicit "maxResponseTime" `
+ `if you need a higher limit.`,
),
))
}
}
22 changes: 9 additions & 13 deletions packages/cli/src/constructs/ssl-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Monitor, MonitorProps } from './monitor.js'
import { Session } from './session.js'
import { Diagnostics } from './diagnostics.js'
import { SslRequest } from './ssl-request.js'
import { InvalidPropertyValueDiagnostic, RequiredPropertyDiagnostic } from './construct-diagnostics.js'
import { validateResponseTimes } from './internal/common-diagnostics.js'
import { RequiredPropertyDiagnostic } from './construct-diagnostics.js'

export interface SslMonitorProps extends MonitorProps {
/**
Expand Down Expand Up @@ -78,18 +79,13 @@ export class SslMonitor extends Monitor {
))
}

if (
this.degradedResponseTime !== undefined
&& this.maxResponseTime !== undefined
&& this.degradedResponseTime > this.maxResponseTime
) {
diagnostics.add(new InvalidPropertyValueDiagnostic(
'degradedResponseTime',
new Error(
`The value of "degradedResponseTime" must be less than or equal to "maxResponseTime".`,
),
))
}
await validateResponseTimes(diagnostics, this, {
degradedResponseTime: 30_000,
maxResponseTime: 30_000,
// Backend default applied when maxResponseTime is omitted (see
// sslMonitorResponseTimeLimitFields in response-time-limit-schema.ts).
defaultMaxResponseTime: 10_000,
})
}

synthesize () {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/constructs/ssl-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { IPFamily } from './ip.js'
* The severity applied to an SSL security-baseline rule.
*
* - `fail` marks the monitor as failing when the rule is violated.
* - `warn` marks the monitor as degraded when the rule is violated.
* - `degrade` marks the monitor as degraded when the rule is violated.
* - `ignore` disables the rule.
*/
export type SslBaselineSeverity = 'fail' | 'warn' | 'ignore'
export type SslBaselineSeverity = 'fail' | 'degrade' | 'ignore'

/**
* A baseline rule whose value is a TLS version string (e.g. `TLS1.2`/`TLS1.3`).
Expand Down
Loading
Loading