@checkstack/healthcheck-tls-backend 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/package.json +1 -1
- package/src/certificate-collector.test.ts +172 -0
- package/src/certificate-collector.ts +170 -0
- package/src/index.ts +7 -5
- package/src/strategy.test.ts +41 -88
- package/src/strategy.ts +109 -191
- package/src/transport-client.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @checkstack/healthcheck-tls-backend
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f5b1f49: Refactored health check strategies to use `createClient()` pattern with built-in collectors.
|
|
8
|
+
|
|
9
|
+
**Strategy Changes:**
|
|
10
|
+
|
|
11
|
+
- Replaced `execute()` with `createClient()` that returns a transport client
|
|
12
|
+
- Strategy configs now only contain connection parameters
|
|
13
|
+
- Collector configs handle what to do with the connection
|
|
14
|
+
|
|
15
|
+
**Built-in Collectors Added:**
|
|
16
|
+
|
|
17
|
+
- DNS: `LookupCollector` for hostname resolution
|
|
18
|
+
- gRPC: `HealthCollector` for gRPC health protocol
|
|
19
|
+
- HTTP: `RequestCollector` for HTTP requests
|
|
20
|
+
- MySQL: `QueryCollector` for database queries
|
|
21
|
+
- Ping: `PingCollector` for ICMP ping
|
|
22
|
+
- Postgres: `QueryCollector` for database queries
|
|
23
|
+
- Redis: `CommandCollector` for Redis commands
|
|
24
|
+
- Script: `ExecuteCollector` for script execution
|
|
25
|
+
- SSH: `CommandCollector` for SSH commands
|
|
26
|
+
- TCP: `BannerCollector` for TCP banner grabbing
|
|
27
|
+
- TLS: `CertificateCollector` for certificate inspection
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [f5b1f49]
|
|
32
|
+
- Updated dependencies [f5b1f49]
|
|
33
|
+
- Updated dependencies [f5b1f49]
|
|
34
|
+
- Updated dependencies [f5b1f49]
|
|
35
|
+
- @checkstack/backend-api@0.1.0
|
|
36
|
+
- @checkstack/healthcheck-common@0.1.0
|
|
37
|
+
- @checkstack/common@0.0.3
|
|
38
|
+
|
|
3
39
|
## 0.0.3
|
|
4
40
|
|
|
5
41
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { CertificateCollector } from "./certificate-collector";
|
|
3
|
+
import type {
|
|
4
|
+
TlsTransportClient,
|
|
5
|
+
TlsCertificateInfo,
|
|
6
|
+
} from "./transport-client";
|
|
7
|
+
|
|
8
|
+
describe("CertificateCollector", () => {
|
|
9
|
+
const createMockClient = (
|
|
10
|
+
response: Partial<TlsCertificateInfo> = {}
|
|
11
|
+
): TlsTransportClient => ({
|
|
12
|
+
exec: mock(() =>
|
|
13
|
+
Promise.resolve({
|
|
14
|
+
isValid: response.isValid ?? true,
|
|
15
|
+
isSelfSigned: response.isSelfSigned ?? false,
|
|
16
|
+
subject: response.subject ?? "CN=example.com",
|
|
17
|
+
issuer: response.issuer ?? "CN=Let's Encrypt",
|
|
18
|
+
validFrom: response.validFrom ?? "2024-01-01T00:00:00Z",
|
|
19
|
+
validTo: response.validTo ?? "2025-01-01T00:00:00Z",
|
|
20
|
+
daysUntilExpiry: response.daysUntilExpiry ?? 365,
|
|
21
|
+
daysRemaining: response.daysRemaining ?? 365,
|
|
22
|
+
error: response.error,
|
|
23
|
+
})
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("execute", () => {
|
|
28
|
+
it("should get certificate info successfully", async () => {
|
|
29
|
+
const collector = new CertificateCollector();
|
|
30
|
+
const client = createMockClient({ daysRemaining: 90 });
|
|
31
|
+
|
|
32
|
+
const result = await collector.execute({
|
|
33
|
+
config: {},
|
|
34
|
+
client,
|
|
35
|
+
pluginId: "test",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result.result.subject).toBe("CN=example.com");
|
|
39
|
+
expect(result.result.issuer).toBe("CN=Let's Encrypt");
|
|
40
|
+
expect(result.result.daysRemaining).toBe(90);
|
|
41
|
+
expect(result.result.valid).toBe(true);
|
|
42
|
+
expect(result.error).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should return error for failed TLS connection", async () => {
|
|
46
|
+
const collector = new CertificateCollector();
|
|
47
|
+
const client = createMockClient({
|
|
48
|
+
error: "Connection refused",
|
|
49
|
+
isValid: false,
|
|
50
|
+
daysRemaining: 0,
|
|
51
|
+
daysUntilExpiry: 0,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await collector.execute({
|
|
55
|
+
config: {},
|
|
56
|
+
client,
|
|
57
|
+
pluginId: "test",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(result.result.valid).toBe(false);
|
|
61
|
+
expect(result.error).toBe("Connection refused");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should mark expired certificate as invalid", async () => {
|
|
65
|
+
const collector = new CertificateCollector();
|
|
66
|
+
const client = createMockClient({ daysRemaining: 0 });
|
|
67
|
+
|
|
68
|
+
const result = await collector.execute({
|
|
69
|
+
config: {},
|
|
70
|
+
client,
|
|
71
|
+
pluginId: "test",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(result.result.valid).toBe(false);
|
|
75
|
+
expect(result.result.daysRemaining).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("aggregateResult", () => {
|
|
80
|
+
it("should calculate average days remaining", () => {
|
|
81
|
+
const collector = new CertificateCollector();
|
|
82
|
+
const runs = [
|
|
83
|
+
{
|
|
84
|
+
id: "1",
|
|
85
|
+
status: "healthy" as const,
|
|
86
|
+
latencyMs: 100,
|
|
87
|
+
checkId: "c1",
|
|
88
|
+
timestamp: new Date(),
|
|
89
|
+
metadata: {
|
|
90
|
+
subject: "CN=a.com",
|
|
91
|
+
issuer: "CN=CA",
|
|
92
|
+
validFrom: "",
|
|
93
|
+
validTo: "",
|
|
94
|
+
daysRemaining: 30,
|
|
95
|
+
valid: true,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "2",
|
|
100
|
+
status: "healthy" as const,
|
|
101
|
+
latencyMs: 100,
|
|
102
|
+
checkId: "c1",
|
|
103
|
+
timestamp: new Date(),
|
|
104
|
+
metadata: {
|
|
105
|
+
subject: "CN=a.com",
|
|
106
|
+
issuer: "CN=CA",
|
|
107
|
+
validFrom: "",
|
|
108
|
+
validTo: "",
|
|
109
|
+
daysRemaining: 60,
|
|
110
|
+
valid: true,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const aggregated = collector.aggregateResult(runs);
|
|
116
|
+
|
|
117
|
+
expect(aggregated.avgDaysRemaining).toBe(45);
|
|
118
|
+
expect(aggregated.validRate).toBe(100);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should calculate valid rate correctly", () => {
|
|
122
|
+
const collector = new CertificateCollector();
|
|
123
|
+
const runs = [
|
|
124
|
+
{
|
|
125
|
+
id: "1",
|
|
126
|
+
status: "healthy" as const,
|
|
127
|
+
latencyMs: 100,
|
|
128
|
+
checkId: "c1",
|
|
129
|
+
timestamp: new Date(),
|
|
130
|
+
metadata: {
|
|
131
|
+
subject: "CN=a.com",
|
|
132
|
+
issuer: "CN=CA",
|
|
133
|
+
validFrom: "",
|
|
134
|
+
validTo: "",
|
|
135
|
+
daysRemaining: 30,
|
|
136
|
+
valid: true,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "2",
|
|
141
|
+
status: "unhealthy" as const,
|
|
142
|
+
latencyMs: 100,
|
|
143
|
+
checkId: "c1",
|
|
144
|
+
timestamp: new Date(),
|
|
145
|
+
metadata: {
|
|
146
|
+
subject: "CN=a.com",
|
|
147
|
+
issuer: "CN=CA",
|
|
148
|
+
validFrom: "",
|
|
149
|
+
validTo: "",
|
|
150
|
+
daysRemaining: 0,
|
|
151
|
+
valid: false,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const aggregated = collector.aggregateResult(runs);
|
|
157
|
+
|
|
158
|
+
expect(aggregated.validRate).toBe(50);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("metadata", () => {
|
|
163
|
+
it("should have correct static properties", () => {
|
|
164
|
+
const collector = new CertificateCollector();
|
|
165
|
+
|
|
166
|
+
expect(collector.id).toBe("certificate");
|
|
167
|
+
expect(collector.displayName).toBe("TLS Certificate");
|
|
168
|
+
expect(collector.allowMultiple).toBe(false);
|
|
169
|
+
expect(collector.supportedPlugins).toHaveLength(1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Versioned,
|
|
3
|
+
z,
|
|
4
|
+
type HealthCheckRunForAggregation,
|
|
5
|
+
type CollectorResult,
|
|
6
|
+
type CollectorStrategy,
|
|
7
|
+
} from "@checkstack/backend-api";
|
|
8
|
+
import {
|
|
9
|
+
healthResultNumber,
|
|
10
|
+
healthResultString,
|
|
11
|
+
healthResultBoolean,
|
|
12
|
+
} from "@checkstack/healthcheck-common";
|
|
13
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
14
|
+
import type { TlsTransportClient } from "./transport-client";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// CONFIGURATION SCHEMA
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
const certificateConfigSchema = z.object({
|
|
21
|
+
// No config needed - just returns cert info from connection
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type CertificateConfig = z.infer<typeof certificateConfigSchema>;
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// RESULT SCHEMAS
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
const certificateResultSchema = z.object({
|
|
31
|
+
subject: healthResultString({
|
|
32
|
+
"x-chart-type": "text",
|
|
33
|
+
"x-chart-label": "Subject",
|
|
34
|
+
}),
|
|
35
|
+
issuer: healthResultString({
|
|
36
|
+
"x-chart-type": "text",
|
|
37
|
+
"x-chart-label": "Issuer",
|
|
38
|
+
}),
|
|
39
|
+
validFrom: healthResultString({
|
|
40
|
+
"x-chart-type": "text",
|
|
41
|
+
"x-chart-label": "Valid From",
|
|
42
|
+
}),
|
|
43
|
+
validTo: healthResultString({
|
|
44
|
+
"x-chart-type": "text",
|
|
45
|
+
"x-chart-label": "Valid To",
|
|
46
|
+
}),
|
|
47
|
+
daysRemaining: healthResultNumber({
|
|
48
|
+
"x-chart-type": "gauge",
|
|
49
|
+
"x-chart-label": "Days Remaining",
|
|
50
|
+
"x-chart-unit": "days",
|
|
51
|
+
}),
|
|
52
|
+
valid: healthResultBoolean({
|
|
53
|
+
"x-chart-type": "boolean",
|
|
54
|
+
"x-chart-label": "Valid",
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export type CertificateResult = z.infer<typeof certificateResultSchema>;
|
|
59
|
+
|
|
60
|
+
const certificateAggregatedSchema = z.object({
|
|
61
|
+
avgDaysRemaining: healthResultNumber({
|
|
62
|
+
"x-chart-type": "gauge",
|
|
63
|
+
"x-chart-label": "Avg Days Remaining",
|
|
64
|
+
"x-chart-unit": "days",
|
|
65
|
+
}),
|
|
66
|
+
validRate: healthResultNumber({
|
|
67
|
+
"x-chart-type": "gauge",
|
|
68
|
+
"x-chart-label": "Valid Rate",
|
|
69
|
+
"x-chart-unit": "%",
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export type CertificateAggregatedResult = z.infer<
|
|
74
|
+
typeof certificateAggregatedSchema
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// CERTIFICATE COLLECTOR
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Built-in TLS certificate collector.
|
|
83
|
+
* Returns certificate information from the TLS connection.
|
|
84
|
+
*/
|
|
85
|
+
export class CertificateCollector
|
|
86
|
+
implements
|
|
87
|
+
CollectorStrategy<
|
|
88
|
+
TlsTransportClient,
|
|
89
|
+
CertificateConfig,
|
|
90
|
+
CertificateResult,
|
|
91
|
+
CertificateAggregatedResult
|
|
92
|
+
>
|
|
93
|
+
{
|
|
94
|
+
id = "certificate";
|
|
95
|
+
displayName = "TLS Certificate";
|
|
96
|
+
description = "Check TLS certificate validity and expiration";
|
|
97
|
+
|
|
98
|
+
supportedPlugins = [pluginMetadata];
|
|
99
|
+
|
|
100
|
+
allowMultiple = false;
|
|
101
|
+
|
|
102
|
+
config = new Versioned({ version: 1, schema: certificateConfigSchema });
|
|
103
|
+
result = new Versioned({ version: 1, schema: certificateResultSchema });
|
|
104
|
+
aggregatedResult = new Versioned({
|
|
105
|
+
version: 1,
|
|
106
|
+
schema: certificateAggregatedSchema,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
async execute({
|
|
110
|
+
client,
|
|
111
|
+
}: {
|
|
112
|
+
config: CertificateConfig;
|
|
113
|
+
client: TlsTransportClient;
|
|
114
|
+
pluginId: string;
|
|
115
|
+
}): Promise<CollectorResult<CertificateResult>> {
|
|
116
|
+
const response = await client.exec({ action: "inspect" });
|
|
117
|
+
|
|
118
|
+
if (response.error) {
|
|
119
|
+
return {
|
|
120
|
+
result: {
|
|
121
|
+
subject: "",
|
|
122
|
+
issuer: "",
|
|
123
|
+
validFrom: "",
|
|
124
|
+
validTo: "",
|
|
125
|
+
daysRemaining: 0,
|
|
126
|
+
valid: false,
|
|
127
|
+
},
|
|
128
|
+
error: response.error,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
result: {
|
|
134
|
+
subject: response.subject ?? "",
|
|
135
|
+
issuer: response.issuer ?? "",
|
|
136
|
+
validFrom: response.validFrom ?? "",
|
|
137
|
+
validTo: response.validTo ?? "",
|
|
138
|
+
daysRemaining: response.daysRemaining ?? 0,
|
|
139
|
+
valid: (response.daysRemaining ?? 0) > 0,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
aggregateResult(
|
|
145
|
+
runs: HealthCheckRunForAggregation<CertificateResult>[]
|
|
146
|
+
): CertificateAggregatedResult {
|
|
147
|
+
const daysRemaining = runs
|
|
148
|
+
.map((r) => r.metadata?.daysRemaining)
|
|
149
|
+
.filter((v): v is number => typeof v === "number");
|
|
150
|
+
|
|
151
|
+
const validResults = runs
|
|
152
|
+
.map((r) => r.metadata?.valid)
|
|
153
|
+
.filter((v): v is boolean => typeof v === "boolean");
|
|
154
|
+
|
|
155
|
+
const validCount = validResults.filter(Boolean).length;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
avgDaysRemaining:
|
|
159
|
+
daysRemaining.length > 0
|
|
160
|
+
? Math.round(
|
|
161
|
+
daysRemaining.reduce((a, b) => a + b, 0) / daysRemaining.length
|
|
162
|
+
)
|
|
163
|
+
: 0,
|
|
164
|
+
validRate:
|
|
165
|
+
validResults.length > 0
|
|
166
|
+
? Math.round((validCount / validResults.length) * 100)
|
|
167
|
+
: 0,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createBackendPlugin,
|
|
3
|
-
coreServices,
|
|
4
|
-
} from "@checkstack/backend-api";
|
|
1
|
+
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
5
2
|
import { TlsHealthCheckStrategy } from "./strategy";
|
|
6
3
|
import { pluginMetadata } from "./plugin-metadata";
|
|
4
|
+
import { CertificateCollector } from "./certificate-collector";
|
|
7
5
|
|
|
8
6
|
export default createBackendPlugin({
|
|
9
7
|
metadata: pluginMetadata,
|
|
@@ -11,13 +9,17 @@ export default createBackendPlugin({
|
|
|
11
9
|
env.registerInit({
|
|
12
10
|
deps: {
|
|
13
11
|
healthCheckRegistry: coreServices.healthCheckRegistry,
|
|
12
|
+
collectorRegistry: coreServices.collectorRegistry,
|
|
14
13
|
logger: coreServices.logger,
|
|
15
14
|
},
|
|
16
|
-
init: async ({ healthCheckRegistry, logger }) => {
|
|
15
|
+
init: async ({ healthCheckRegistry, collectorRegistry, logger }) => {
|
|
17
16
|
logger.debug("🔌 Registering TLS/SSL Health Check Strategy...");
|
|
18
17
|
const strategy = new TlsHealthCheckStrategy();
|
|
19
18
|
healthCheckRegistry.register(strategy);
|
|
19
|
+
collectorRegistry.register(new CertificateCollector());
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
|
+
|
|
25
|
+
export { pluginMetadata } from "./plugin-metadata";
|
package/src/strategy.test.ts
CHANGED
|
@@ -60,11 +60,11 @@ describe("TlsHealthCheckStrategy", () => {
|
|
|
60
60
|
),
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
describe("
|
|
64
|
-
it("should return
|
|
63
|
+
describe("createClient", () => {
|
|
64
|
+
it("should return a connected client", async () => {
|
|
65
65
|
const strategy = new TlsHealthCheckStrategy(createMockClient());
|
|
66
66
|
|
|
67
|
-
const
|
|
67
|
+
const connectedClient = await strategy.createClient({
|
|
68
68
|
host: "example.com",
|
|
69
69
|
port: 443,
|
|
70
70
|
timeout: 5000,
|
|
@@ -72,120 +72,69 @@ describe("TlsHealthCheckStrategy", () => {
|
|
|
72
72
|
rejectUnauthorized: true,
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
expect(
|
|
76
|
-
expect(
|
|
77
|
-
expect(
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("should return unhealthy for unauthorized certificate", async () => {
|
|
81
|
-
const strategy = new TlsHealthCheckStrategy(
|
|
82
|
-
createMockClient({ authorized: false })
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const result = await strategy.execute({
|
|
86
|
-
host: "example.com",
|
|
87
|
-
port: 443,
|
|
88
|
-
timeout: 5000,
|
|
89
|
-
minDaysUntilExpiry: 7,
|
|
90
|
-
rejectUnauthorized: true,
|
|
91
|
-
});
|
|
75
|
+
expect(connectedClient.client).toBeDefined();
|
|
76
|
+
expect(connectedClient.client.exec).toBeDefined();
|
|
77
|
+
expect(connectedClient.close).toBeDefined();
|
|
92
78
|
|
|
93
|
-
|
|
94
|
-
expect(result.message).toContain("not valid");
|
|
79
|
+
connectedClient.close();
|
|
95
80
|
});
|
|
96
81
|
|
|
97
|
-
it("should
|
|
98
|
-
const expiringCert = createCertInfo({
|
|
99
|
-
validTo: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // 5 days
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const strategy = new TlsHealthCheckStrategy(
|
|
103
|
-
createMockClient({ cert: expiringCert })
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const result = await strategy.execute({
|
|
107
|
-
host: "example.com",
|
|
108
|
-
port: 443,
|
|
109
|
-
timeout: 5000,
|
|
110
|
-
minDaysUntilExpiry: 14,
|
|
111
|
-
rejectUnauthorized: true,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
expect(result.status).toBe("unhealthy");
|
|
115
|
-
expect(result.message).toContain("expires in");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("should return unhealthy for connection error", async () => {
|
|
82
|
+
it("should throw for connection error during client creation", async () => {
|
|
119
83
|
const strategy = new TlsHealthCheckStrategy(
|
|
120
84
|
createMockClient({ error: new Error("Connection refused") })
|
|
121
85
|
);
|
|
122
86
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
expect(result.message).toContain("Connection refused");
|
|
87
|
+
await expect(
|
|
88
|
+
strategy.createClient({
|
|
89
|
+
host: "example.com",
|
|
90
|
+
port: 443,
|
|
91
|
+
timeout: 5000,
|
|
92
|
+
minDaysUntilExpiry: 7,
|
|
93
|
+
rejectUnauthorized: true,
|
|
94
|
+
})
|
|
95
|
+
).rejects.toThrow("Connection refused");
|
|
133
96
|
});
|
|
97
|
+
});
|
|
134
98
|
|
|
135
|
-
|
|
99
|
+
describe("client.exec (inspect action)", () => {
|
|
100
|
+
it("should return valid certificate info", async () => {
|
|
136
101
|
const strategy = new TlsHealthCheckStrategy(createMockClient());
|
|
137
102
|
|
|
138
|
-
const
|
|
103
|
+
const connectedClient = await strategy.createClient({
|
|
139
104
|
host: "example.com",
|
|
140
105
|
port: 443,
|
|
141
106
|
timeout: 5000,
|
|
142
107
|
minDaysUntilExpiry: 7,
|
|
143
108
|
rejectUnauthorized: true,
|
|
144
|
-
assertions: [
|
|
145
|
-
{
|
|
146
|
-
field: "daysUntilExpiry",
|
|
147
|
-
operator: "greaterThanOrEqual",
|
|
148
|
-
value: 7,
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
109
|
});
|
|
152
110
|
|
|
153
|
-
|
|
154
|
-
});
|
|
111
|
+
const result = await connectedClient.client.exec({ action: "inspect" });
|
|
155
112
|
|
|
156
|
-
|
|
157
|
-
|
|
113
|
+
expect(result.isValid).toBe(true);
|
|
114
|
+
expect(result.daysRemaining).toBeGreaterThan(0);
|
|
115
|
+
expect(result.subject).toBe("example.com");
|
|
158
116
|
|
|
159
|
-
|
|
160
|
-
host: "example.com",
|
|
161
|
-
port: 443,
|
|
162
|
-
timeout: 5000,
|
|
163
|
-
minDaysUntilExpiry: 7,
|
|
164
|
-
rejectUnauthorized: true,
|
|
165
|
-
assertions: [
|
|
166
|
-
{ field: "issuer", operator: "contains", value: "DigiCert" },
|
|
167
|
-
],
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
expect(result.status).toBe("healthy");
|
|
117
|
+
connectedClient.close();
|
|
171
118
|
});
|
|
172
119
|
|
|
173
|
-
it("should
|
|
120
|
+
it("should return invalid for unauthorized certificate", async () => {
|
|
174
121
|
const strategy = new TlsHealthCheckStrategy(
|
|
175
122
|
createMockClient({ authorized: false })
|
|
176
123
|
);
|
|
177
124
|
|
|
178
|
-
const
|
|
125
|
+
const connectedClient = await strategy.createClient({
|
|
179
126
|
host: "example.com",
|
|
180
127
|
port: 443,
|
|
181
128
|
timeout: 5000,
|
|
182
129
|
minDaysUntilExpiry: 7,
|
|
183
|
-
rejectUnauthorized: false, // Don't reject to
|
|
184
|
-
assertions: [{ field: "isValid", operator: "isTrue" }],
|
|
130
|
+
rejectUnauthorized: false, // Don't reject to allow connection
|
|
185
131
|
});
|
|
186
132
|
|
|
187
|
-
|
|
188
|
-
|
|
133
|
+
const result = await connectedClient.client.exec({ action: "inspect" });
|
|
134
|
+
|
|
135
|
+
expect(result.isValid).toBe(false);
|
|
136
|
+
|
|
137
|
+
connectedClient.close();
|
|
189
138
|
});
|
|
190
139
|
|
|
191
140
|
it("should detect self-signed certificates", async () => {
|
|
@@ -199,7 +148,7 @@ describe("TlsHealthCheckStrategy", () => {
|
|
|
199
148
|
createMockClient({ cert: selfSignedCert, authorized: false })
|
|
200
149
|
);
|
|
201
150
|
|
|
202
|
-
const
|
|
151
|
+
const connectedClient = await strategy.createClient({
|
|
203
152
|
host: "localhost",
|
|
204
153
|
port: 443,
|
|
205
154
|
timeout: 5000,
|
|
@@ -207,7 +156,11 @@ describe("TlsHealthCheckStrategy", () => {
|
|
|
207
156
|
rejectUnauthorized: false,
|
|
208
157
|
});
|
|
209
158
|
|
|
210
|
-
|
|
159
|
+
const result = await connectedClient.client.exec({ action: "inspect" });
|
|
160
|
+
|
|
161
|
+
expect(result.isSelfSigned).toBe(true);
|
|
162
|
+
|
|
163
|
+
connectedClient.close();
|
|
211
164
|
});
|
|
212
165
|
});
|
|
213
166
|
|
|
@@ -301,7 +254,7 @@ describe("TlsHealthCheckStrategy", () => {
|
|
|
301
254
|
|
|
302
255
|
const aggregated = strategy.aggregateResult(runs);
|
|
303
256
|
|
|
304
|
-
expect(aggregated.invalidCount).toBe(
|
|
257
|
+
expect(aggregated.invalidCount).toBe(2);
|
|
305
258
|
expect(aggregated.errorCount).toBe(1);
|
|
306
259
|
});
|
|
307
260
|
});
|
package/src/strategy.ts
CHANGED
|
@@ -1,38 +1,26 @@
|
|
|
1
1
|
import * as tls from "node:tls";
|
|
2
2
|
import {
|
|
3
3
|
HealthCheckStrategy,
|
|
4
|
-
HealthCheckResult,
|
|
5
4
|
HealthCheckRunForAggregation,
|
|
6
5
|
Versioned,
|
|
7
6
|
z,
|
|
8
|
-
|
|
9
|
-
stringField,
|
|
10
|
-
booleanField,
|
|
11
|
-
evaluateAssertions,
|
|
7
|
+
type ConnectedClient,
|
|
12
8
|
} from "@checkstack/backend-api";
|
|
13
9
|
import {
|
|
14
10
|
healthResultBoolean,
|
|
15
11
|
healthResultNumber,
|
|
16
12
|
healthResultString,
|
|
17
13
|
} from "@checkstack/healthcheck-common";
|
|
14
|
+
import type {
|
|
15
|
+
TlsTransportClient,
|
|
16
|
+
TlsInspectRequest,
|
|
17
|
+
TlsCertificateInfo,
|
|
18
|
+
} from "./transport-client";
|
|
18
19
|
|
|
19
20
|
// ============================================================================
|
|
20
21
|
// SCHEMAS
|
|
21
22
|
// ============================================================================
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
* Assertion schema for TLS health checks using shared factories.
|
|
25
|
-
*/
|
|
26
|
-
const tlsAssertionSchema = z.discriminatedUnion("field", [
|
|
27
|
-
numericField("daysUntilExpiry", { min: 0 }),
|
|
28
|
-
stringField("issuer"),
|
|
29
|
-
stringField("subject"),
|
|
30
|
-
booleanField("isValid"),
|
|
31
|
-
booleanField("isSelfSigned"),
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
export type TlsAssertion = z.infer<typeof tlsAssertionSchema>;
|
|
35
|
-
|
|
36
24
|
/**
|
|
37
25
|
* Configuration schema for TLS health checks.
|
|
38
26
|
*/
|
|
@@ -42,7 +30,7 @@ export const tlsConfigSchema = z.object({
|
|
|
42
30
|
servername: z
|
|
43
31
|
.string()
|
|
44
32
|
.optional()
|
|
45
|
-
.describe("SNI
|
|
33
|
+
.describe("Server name for SNI (defaults to host)"),
|
|
46
34
|
timeout: z
|
|
47
35
|
.number()
|
|
48
36
|
.min(100)
|
|
@@ -50,17 +38,14 @@ export const tlsConfigSchema = z.object({
|
|
|
50
38
|
.describe("Connection timeout in milliseconds"),
|
|
51
39
|
minDaysUntilExpiry: z
|
|
52
40
|
.number()
|
|
41
|
+
.int()
|
|
53
42
|
.min(0)
|
|
54
|
-
.default(
|
|
55
|
-
.describe("Minimum days
|
|
43
|
+
.default(30)
|
|
44
|
+
.describe("Minimum days before certificate expiry to consider healthy"),
|
|
56
45
|
rejectUnauthorized: z
|
|
57
46
|
.boolean()
|
|
58
47
|
.default(true)
|
|
59
48
|
.describe("Reject invalid/self-signed certificates"),
|
|
60
|
-
assertions: z
|
|
61
|
-
.array(tlsAssertionSchema)
|
|
62
|
-
.optional()
|
|
63
|
-
.describe("Validation conditions"),
|
|
64
49
|
});
|
|
65
50
|
|
|
66
51
|
export type TlsConfig = z.infer<typeof tlsConfigSchema>;
|
|
@@ -75,49 +60,24 @@ const tlsResultSchema = z.object({
|
|
|
75
60
|
}),
|
|
76
61
|
isValid: healthResultBoolean({
|
|
77
62
|
"x-chart-type": "boolean",
|
|
78
|
-
"x-chart-label": "
|
|
63
|
+
"x-chart-label": "Valid",
|
|
79
64
|
}),
|
|
80
65
|
isSelfSigned: healthResultBoolean({
|
|
81
66
|
"x-chart-type": "boolean",
|
|
82
67
|
"x-chart-label": "Self-Signed",
|
|
83
68
|
}),
|
|
84
|
-
issuer: healthResultString({
|
|
85
|
-
"x-chart-type": "text",
|
|
86
|
-
"x-chart-label": "Issuer",
|
|
87
|
-
}),
|
|
88
|
-
subject: healthResultString({
|
|
89
|
-
"x-chart-type": "text",
|
|
90
|
-
"x-chart-label": "Subject",
|
|
91
|
-
}),
|
|
92
|
-
validFrom: healthResultString({
|
|
93
|
-
"x-chart-type": "text",
|
|
94
|
-
"x-chart-label": "Valid From",
|
|
95
|
-
}),
|
|
96
|
-
validTo: healthResultString({
|
|
97
|
-
"x-chart-type": "text",
|
|
98
|
-
"x-chart-label": "Valid To",
|
|
99
|
-
}),
|
|
100
69
|
daysUntilExpiry: healthResultNumber({
|
|
101
|
-
"x-chart-type": "
|
|
70
|
+
"x-chart-type": "line",
|
|
102
71
|
"x-chart-label": "Days Until Expiry",
|
|
103
72
|
"x-chart-unit": "days",
|
|
104
73
|
}),
|
|
105
|
-
protocol: healthResultString({
|
|
106
|
-
"x-chart-type": "text",
|
|
107
|
-
"x-chart-label": "Protocol",
|
|
108
|
-
}).optional(),
|
|
109
|
-
cipher: healthResultString({
|
|
110
|
-
"x-chart-type": "text",
|
|
111
|
-
"x-chart-label": "Cipher",
|
|
112
|
-
}).optional(),
|
|
113
|
-
failedAssertion: tlsAssertionSchema.optional(),
|
|
114
74
|
error: healthResultString({
|
|
115
75
|
"x-chart-type": "status",
|
|
116
76
|
"x-chart-label": "Error",
|
|
117
77
|
}).optional(),
|
|
118
78
|
});
|
|
119
79
|
|
|
120
|
-
|
|
80
|
+
type TlsResult = z.infer<typeof tlsResultSchema>;
|
|
121
81
|
|
|
122
82
|
/**
|
|
123
83
|
* Aggregated metadata for buckets.
|
|
@@ -143,7 +103,7 @@ const tlsAggregatedSchema = z.object({
|
|
|
143
103
|
}),
|
|
144
104
|
});
|
|
145
105
|
|
|
146
|
-
|
|
106
|
+
type TlsAggregatedResult = z.infer<typeof tlsAggregatedSchema>;
|
|
147
107
|
|
|
148
108
|
// ============================================================================
|
|
149
109
|
// TLS CLIENT INTERFACE (for testability)
|
|
@@ -199,7 +159,7 @@ const defaultTlsClient: TlsClient = {
|
|
|
199
159
|
);
|
|
200
160
|
|
|
201
161
|
socket.on("error", reject);
|
|
202
|
-
socket.
|
|
162
|
+
socket.setTimeout(options.timeout, () => {
|
|
203
163
|
socket.destroy();
|
|
204
164
|
reject(new Error("Connection timeout"));
|
|
205
165
|
});
|
|
@@ -212,7 +172,13 @@ const defaultTlsClient: TlsClient = {
|
|
|
212
172
|
// ============================================================================
|
|
213
173
|
|
|
214
174
|
export class TlsHealthCheckStrategy
|
|
215
|
-
implements
|
|
175
|
+
implements
|
|
176
|
+
HealthCheckStrategy<
|
|
177
|
+
TlsConfig,
|
|
178
|
+
TlsTransportClient,
|
|
179
|
+
TlsResult,
|
|
180
|
+
TlsAggregatedResult
|
|
181
|
+
>
|
|
216
182
|
{
|
|
217
183
|
id = "tls";
|
|
218
184
|
displayName = "TLS/SSL Health Check";
|
|
@@ -225,13 +191,29 @@ export class TlsHealthCheckStrategy
|
|
|
225
191
|
}
|
|
226
192
|
|
|
227
193
|
config: Versioned<TlsConfig> = new Versioned({
|
|
228
|
-
version:
|
|
194
|
+
version: 2, // Bumped for createClient pattern
|
|
229
195
|
schema: tlsConfigSchema,
|
|
196
|
+
migrations: [
|
|
197
|
+
{
|
|
198
|
+
fromVersion: 1,
|
|
199
|
+
toVersion: 2,
|
|
200
|
+
description: "Migrate to createClient pattern (no config changes)",
|
|
201
|
+
migrate: (data: unknown) => data,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
230
204
|
});
|
|
231
205
|
|
|
232
206
|
result: Versioned<TlsResult> = new Versioned({
|
|
233
|
-
version:
|
|
207
|
+
version: 2,
|
|
234
208
|
schema: tlsResultSchema,
|
|
209
|
+
migrations: [
|
|
210
|
+
{
|
|
211
|
+
fromVersion: 1,
|
|
212
|
+
toVersion: 2,
|
|
213
|
+
description: "Migrate to createClient pattern (no result changes)",
|
|
214
|
+
migrate: (data: unknown) => data,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
235
217
|
});
|
|
236
218
|
|
|
237
219
|
aggregatedResult: Versioned<TlsAggregatedResult> = new Versioned({
|
|
@@ -242,151 +224,87 @@ export class TlsHealthCheckStrategy
|
|
|
242
224
|
aggregateResult(
|
|
243
225
|
runs: HealthCheckRunForAggregation<TlsResult>[]
|
|
244
226
|
): TlsAggregatedResult {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (run.metadata && !run.metadata.isValid) {
|
|
257
|
-
invalidCount++;
|
|
258
|
-
}
|
|
259
|
-
if (run.metadata) {
|
|
260
|
-
totalDaysUntilExpiry += run.metadata.daysUntilExpiry;
|
|
261
|
-
if (run.metadata.daysUntilExpiry < minDaysUntilExpiry) {
|
|
262
|
-
minDaysUntilExpiry = run.metadata.daysUntilExpiry;
|
|
263
|
-
}
|
|
264
|
-
validRuns++;
|
|
265
|
-
}
|
|
227
|
+
const validRuns = runs.filter((r) => r.metadata);
|
|
228
|
+
|
|
229
|
+
if (validRuns.length === 0) {
|
|
230
|
+
return {
|
|
231
|
+
avgDaysUntilExpiry: 0,
|
|
232
|
+
minDaysUntilExpiry: 0,
|
|
233
|
+
invalidCount: 0,
|
|
234
|
+
errorCount: 0,
|
|
235
|
+
};
|
|
266
236
|
}
|
|
267
237
|
|
|
238
|
+
const daysValues = validRuns
|
|
239
|
+
.map((r) => r.metadata?.daysUntilExpiry)
|
|
240
|
+
.filter((d): d is number => typeof d === "number");
|
|
241
|
+
|
|
242
|
+
const avgDaysUntilExpiry =
|
|
243
|
+
daysValues.length > 0
|
|
244
|
+
? Math.round(daysValues.reduce((a, b) => a + b, 0) / daysValues.length)
|
|
245
|
+
: 0;
|
|
246
|
+
|
|
247
|
+
const minDaysUntilExpiry =
|
|
248
|
+
daysValues.length > 0 ? Math.min(...daysValues) : 0;
|
|
249
|
+
|
|
250
|
+
const invalidCount = validRuns.filter(
|
|
251
|
+
(r) => r.metadata?.isValid === false
|
|
252
|
+
).length;
|
|
253
|
+
|
|
254
|
+
const errorCount = validRuns.filter(
|
|
255
|
+
(r) => r.metadata?.error !== undefined
|
|
256
|
+
).length;
|
|
257
|
+
|
|
268
258
|
return {
|
|
269
|
-
avgDaysUntilExpiry
|
|
270
|
-
minDaysUntilExpiry
|
|
271
|
-
minDaysUntilExpiry === Number.POSITIVE_INFINITY
|
|
272
|
-
? 0
|
|
273
|
-
: minDaysUntilExpiry,
|
|
259
|
+
avgDaysUntilExpiry,
|
|
260
|
+
minDaysUntilExpiry,
|
|
274
261
|
invalidCount,
|
|
275
262
|
errorCount,
|
|
276
263
|
};
|
|
277
264
|
}
|
|
278
265
|
|
|
279
|
-
async
|
|
266
|
+
async createClient(
|
|
267
|
+
config: TlsConfig
|
|
268
|
+
): Promise<ConnectedClient<TlsTransportClient>> {
|
|
280
269
|
const validatedConfig = this.config.validate(config);
|
|
281
|
-
const start = performance.now();
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
const connection = await this.tlsClient.connect({
|
|
285
|
-
host: validatedConfig.host,
|
|
286
|
-
port: validatedConfig.port,
|
|
287
|
-
servername: validatedConfig.servername ?? validatedConfig.host,
|
|
288
|
-
rejectUnauthorized: validatedConfig.rejectUnauthorized,
|
|
289
|
-
timeout: validatedConfig.timeout,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
const cert = connection.getPeerCertificate();
|
|
293
|
-
const protocol = connection.getProtocol();
|
|
294
|
-
const cipher = connection.getCipher();
|
|
295
|
-
|
|
296
|
-
connection.end();
|
|
297
|
-
|
|
298
|
-
const end = performance.now();
|
|
299
|
-
const latencyMs = Math.round(end - start);
|
|
300
270
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
// Determine if self-signed
|
|
309
|
-
const isSelfSigned =
|
|
310
|
-
cert.issuer.CN === cert.subject.CN && cert.issuer.O === undefined;
|
|
311
|
-
|
|
312
|
-
const result: Omit<TlsResult, "failedAssertion" | "error"> = {
|
|
313
|
-
connected: true,
|
|
314
|
-
isValid: connection.authorized,
|
|
315
|
-
isSelfSigned,
|
|
316
|
-
issuer: cert.issuer.CN ?? cert.issuer.O ?? "Unknown",
|
|
317
|
-
subject: cert.subject.CN ?? "Unknown",
|
|
318
|
-
validFrom: cert.valid_from,
|
|
319
|
-
validTo: cert.valid_to,
|
|
320
|
-
daysUntilExpiry,
|
|
321
|
-
protocol: protocol ?? undefined,
|
|
322
|
-
cipher: cipher?.name,
|
|
323
|
-
};
|
|
271
|
+
const connection = await this.tlsClient.connect({
|
|
272
|
+
host: validatedConfig.host,
|
|
273
|
+
port: validatedConfig.port,
|
|
274
|
+
servername: validatedConfig.servername ?? validatedConfig.host,
|
|
275
|
+
rejectUnauthorized: validatedConfig.rejectUnauthorized,
|
|
276
|
+
timeout: validatedConfig.timeout,
|
|
277
|
+
});
|
|
324
278
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
279
|
+
const cert = connection.getPeerCertificate();
|
|
280
|
+
const validTo = new Date(cert.valid_to);
|
|
281
|
+
const daysUntilExpiry = Math.floor(
|
|
282
|
+
(validTo.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const certInfo: TlsCertificateInfo = {
|
|
286
|
+
isValid: connection.authorized,
|
|
287
|
+
isSelfSigned: cert.issuer?.CN === cert.subject?.CN,
|
|
288
|
+
issuer: cert.issuer?.O || cert.issuer?.CN || "Unknown",
|
|
289
|
+
subject: cert.subject?.CN || "Unknown",
|
|
290
|
+
validFrom: cert.valid_from,
|
|
291
|
+
validTo: cert.valid_to,
|
|
292
|
+
daysUntilExpiry,
|
|
293
|
+
daysRemaining: daysUntilExpiry,
|
|
294
|
+
protocol: connection.getProtocol() ?? undefined,
|
|
295
|
+
cipher: connection.getCipher()?.name,
|
|
296
|
+
};
|
|
333
297
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}${"value" in failedAssertion ? ` ${failedAssertion.value}` : ""}`,
|
|
341
|
-
metadata: { ...result, failedAssertion },
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Check minimum days until expiry
|
|
346
|
-
if (daysUntilExpiry < validatedConfig.minDaysUntilExpiry) {
|
|
347
|
-
return {
|
|
348
|
-
status: "unhealthy",
|
|
349
|
-
latencyMs,
|
|
350
|
-
message: `Certificate expires in ${daysUntilExpiry} days (minimum: ${validatedConfig.minDaysUntilExpiry})`,
|
|
351
|
-
metadata: result,
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Check certificate validity
|
|
356
|
-
if (!connection.authorized && validatedConfig.rejectUnauthorized) {
|
|
357
|
-
return {
|
|
358
|
-
status: "unhealthy",
|
|
359
|
-
latencyMs,
|
|
360
|
-
message: "Certificate is not valid or not trusted",
|
|
361
|
-
metadata: result,
|
|
362
|
-
};
|
|
363
|
-
}
|
|
298
|
+
const client: TlsTransportClient = {
|
|
299
|
+
async exec(_request: TlsInspectRequest): Promise<TlsCertificateInfo> {
|
|
300
|
+
// Certificate info is captured at connection time
|
|
301
|
+
return certInfo;
|
|
302
|
+
},
|
|
303
|
+
};
|
|
364
304
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
metadata: result,
|
|
370
|
-
};
|
|
371
|
-
} catch (error: unknown) {
|
|
372
|
-
const end = performance.now();
|
|
373
|
-
const isError = error instanceof Error;
|
|
374
|
-
return {
|
|
375
|
-
status: "unhealthy",
|
|
376
|
-
latencyMs: Math.round(end - start),
|
|
377
|
-
message: isError ? error.message : "TLS connection failed",
|
|
378
|
-
metadata: {
|
|
379
|
-
connected: false,
|
|
380
|
-
isValid: false,
|
|
381
|
-
isSelfSigned: false,
|
|
382
|
-
issuer: "",
|
|
383
|
-
subject: "",
|
|
384
|
-
validFrom: "",
|
|
385
|
-
validTo: "",
|
|
386
|
-
daysUntilExpiry: 0,
|
|
387
|
-
error: isError ? error.name : "UnknownError",
|
|
388
|
-
},
|
|
389
|
-
};
|
|
390
|
-
}
|
|
305
|
+
return {
|
|
306
|
+
client,
|
|
307
|
+
close: () => connection.end(),
|
|
308
|
+
};
|
|
391
309
|
}
|
|
392
310
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TransportClient } from "@checkstack/common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TLS inspection request.
|
|
5
|
+
*/
|
|
6
|
+
export interface TlsInspectRequest {
|
|
7
|
+
/** Action to perform (inspect is the default and only action for now) */
|
|
8
|
+
action?: "inspect";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* TLS certificate information.
|
|
13
|
+
*/
|
|
14
|
+
export interface TlsCertificateInfo {
|
|
15
|
+
isValid: boolean;
|
|
16
|
+
isSelfSigned: boolean;
|
|
17
|
+
issuer: string;
|
|
18
|
+
subject: string;
|
|
19
|
+
validFrom: string;
|
|
20
|
+
validTo: string;
|
|
21
|
+
daysUntilExpiry: number;
|
|
22
|
+
daysRemaining: number; // Alias for daysUntilExpiry
|
|
23
|
+
protocol?: string;
|
|
24
|
+
cipher?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* TLS transport client for certificate inspection.
|
|
30
|
+
*/
|
|
31
|
+
export type TlsTransportClient = TransportClient<
|
|
32
|
+
TlsInspectRequest,
|
|
33
|
+
TlsCertificateInfo
|
|
34
|
+
>;
|