securl 1.8.0 → 1.9.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/README.md +19 -0
- package/dist/certificate.d.ts +2 -1
- package/dist/certificate.js +132 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +16 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -194,6 +194,23 @@ console.log({
|
|
|
194
194
|
|
|
195
195
|
Action-plan items include owner, effort, impact, confidence, score impact where available, evidence references, and verification guidance.
|
|
196
196
|
|
|
197
|
+
### 7. Live certificate checks
|
|
198
|
+
|
|
199
|
+
Version `1.9.0+` includes a lightweight certificate helper for Cert Watch-style clients that only need the currently served TLS certificate.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { scanLiveCertificate } from "securl/live-certificate";
|
|
203
|
+
|
|
204
|
+
const certificate = await scanLiveCertificate(new URL("https://example.com"));
|
|
205
|
+
|
|
206
|
+
console.log({
|
|
207
|
+
issuer: certificate.issuer,
|
|
208
|
+
daysRemaining: certificate.daysRemaining,
|
|
209
|
+
protocol: certificate.protocol,
|
|
210
|
+
chainLength: certificate.chain.length,
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
197
214
|
### 6. Evidence-backed remediation plans
|
|
198
215
|
|
|
199
216
|
Version `1.4.0+` includes a remediation plan helper that turns score drivers and findings into prioritized, owner-aware fix guidance. Findings can also carry structured evidence references so clients can show why a finding was raised.
|
|
@@ -304,6 +321,7 @@ Primary exports:
|
|
|
304
321
|
- `buildPostureRiskEventsFromSnapshots(current, previous, diff)` - classify scan changes into alert-friendly risk events.
|
|
305
322
|
- `buildPostureDigest(result)` - reduce a full scan result to a compact API/mobile-friendly digest.
|
|
306
323
|
- `buildActionPlan(result)` - turn remediation, score drivers, exposure, and vendor context into prioritized fix actions.
|
|
324
|
+
- `scanLiveCertificate(url)` - perform a TLS handshake-only certificate read for lightweight cert monitoring.
|
|
307
325
|
- `buildPostureDriftReportFromSnapshots(current, previous)` - produce a complete scan-to-scan drift report for monitoring, alerting, and history views.
|
|
308
326
|
- `buildPostureRemediationPlan(result)` - generate prioritized, owner-aware remediation actions from findings and score drivers.
|
|
309
327
|
- `attachIssueEvidence(result)` - add structured evidence references to findings without changing their existing fields.
|
|
@@ -314,6 +332,7 @@ Package subpath exports:
|
|
|
314
332
|
- `securl/history-diff`
|
|
315
333
|
- `securl/posture-digest`
|
|
316
334
|
- `securl/action-plan`
|
|
335
|
+
- `securl/live-certificate`
|
|
317
336
|
- `securl/posture-drift`
|
|
318
337
|
- `securl/remediation-plan`
|
|
319
338
|
- `securl/risk-events`
|
package/dist/certificate.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { CertificateResult } from "./types.js";
|
|
1
|
+
import type { CertificateResult, LiveCertificateResult } from "./types.js";
|
|
2
2
|
export declare const OBSERVATIONAL_TLS_OPTIONS: {
|
|
3
3
|
rejectUnauthorized: boolean;
|
|
4
4
|
};
|
|
5
5
|
export declare const scanTls: (targetUrl: URL) => Promise<CertificateResult>;
|
|
6
|
+
export declare const scanLiveCertificate: (targetUrl: URL) => Promise<LiveCertificateResult>;
|
package/dist/certificate.js
CHANGED
|
@@ -16,6 +16,48 @@ export const OBSERVATIONAL_TLS_OPTIONS = {
|
|
|
16
16
|
// Set EXTERNAL_POSTURE_ALLOW_INSECURE_TLS=1 only for controlled observational runs.
|
|
17
17
|
rejectUnauthorized: !allowInsecureTls,
|
|
18
18
|
};
|
|
19
|
+
function certificateName(certificate, field) {
|
|
20
|
+
return firstStringValue(certificate?.[field]?.O) ?? firstStringValue(certificate?.[field]?.CN);
|
|
21
|
+
}
|
|
22
|
+
function chainFromCertificate(certificate) {
|
|
23
|
+
const chain = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
let current = certificate;
|
|
26
|
+
while (current && Object.keys(current).length > 0) {
|
|
27
|
+
const fingerprint = current.fingerprint256 || current.fingerprint || null;
|
|
28
|
+
const key = fingerprint || `${current.subject?.CN || ""}:${current.issuer?.CN || ""}:${current.valid_to || ""}`;
|
|
29
|
+
if (seen.has(key)) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
seen.add(key);
|
|
33
|
+
chain.push({
|
|
34
|
+
subject: certificateName(current, "subject"),
|
|
35
|
+
issuer: certificateName(current, "issuer"),
|
|
36
|
+
validFrom: current.valid_from || null,
|
|
37
|
+
validTo: current.valid_to || null,
|
|
38
|
+
fingerprint,
|
|
39
|
+
});
|
|
40
|
+
if (!current.issuerCertificate || current.issuerCertificate === current) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
current = current.issuerCertificate;
|
|
44
|
+
}
|
|
45
|
+
return chain;
|
|
46
|
+
}
|
|
47
|
+
function keyBitsFromCertificate(certificate) {
|
|
48
|
+
const value = certificate?.bits;
|
|
49
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
50
|
+
}
|
|
51
|
+
function keyTypeFromCertificate(certificate) {
|
|
52
|
+
const value = certificate?.asn1Curve || certificate?.nistCurve;
|
|
53
|
+
if (typeof value === "string" && value.trim()) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
if (typeof certificate?.bits === "number") {
|
|
57
|
+
return "rsa";
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
19
61
|
export const scanTls = (targetUrl) => {
|
|
20
62
|
if (targetUrl.protocol !== "https:") {
|
|
21
63
|
return Promise.resolve({
|
|
@@ -71,8 +113,89 @@ export const scanTls = (targetUrl) => {
|
|
|
71
113
|
available: true,
|
|
72
114
|
valid: Boolean(socket.authorized),
|
|
73
115
|
authorized: Boolean(socket.authorized),
|
|
74
|
-
issuer:
|
|
75
|
-
subject:
|
|
116
|
+
issuer: certificateName(certificate, "issuer"),
|
|
117
|
+
subject: certificateName(certificate, "subject"),
|
|
118
|
+
validFrom,
|
|
119
|
+
validTo,
|
|
120
|
+
daysRemaining,
|
|
121
|
+
protocol,
|
|
122
|
+
cipher: cipherInfo?.name || null,
|
|
123
|
+
fingerprint: certificate?.fingerprint256 || null,
|
|
124
|
+
subjectAltName,
|
|
125
|
+
issues,
|
|
126
|
+
});
|
|
127
|
+
socket.end();
|
|
128
|
+
});
|
|
129
|
+
socket.once("timeout", () => {
|
|
130
|
+
socket.destroy(new Error("TLS handshake timed out."));
|
|
131
|
+
});
|
|
132
|
+
socket.once("error", reject);
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
export const scanLiveCertificate = (targetUrl) => {
|
|
136
|
+
if (targetUrl.protocol !== "https:") {
|
|
137
|
+
return Promise.resolve({
|
|
138
|
+
available: false,
|
|
139
|
+
valid: false,
|
|
140
|
+
authorized: false,
|
|
141
|
+
issuer: null,
|
|
142
|
+
subject: null,
|
|
143
|
+
validFrom: null,
|
|
144
|
+
validTo: null,
|
|
145
|
+
daysRemaining: null,
|
|
146
|
+
protocol: null,
|
|
147
|
+
cipher: null,
|
|
148
|
+
fingerprint: null,
|
|
149
|
+
subjectAltName: [],
|
|
150
|
+
issues: ["TLS certificate data is only available for HTTPS targets."],
|
|
151
|
+
host: targetUrl.hostname,
|
|
152
|
+
port: Number(targetUrl.port || 443),
|
|
153
|
+
checkedAt: new Date().toISOString(),
|
|
154
|
+
serialNumber: null,
|
|
155
|
+
keyBits: null,
|
|
156
|
+
keyType: null,
|
|
157
|
+
chain: [],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const socket = tls.connect({
|
|
162
|
+
host: targetUrl.hostname,
|
|
163
|
+
port: Number(targetUrl.port || 443),
|
|
164
|
+
servername: targetUrl.hostname,
|
|
165
|
+
...OBSERVATIONAL_TLS_OPTIONS,
|
|
166
|
+
timeout: TLS_HANDSHAKE_TIMEOUT_MS,
|
|
167
|
+
});
|
|
168
|
+
socket.once("secureConnect", () => {
|
|
169
|
+
const certificate = socket.getPeerCertificate(true);
|
|
170
|
+
const protocol = socket.getProtocol?.() || null;
|
|
171
|
+
const cipherInfo = socket.getCipher?.();
|
|
172
|
+
const validTo = certificate?.valid_to || null;
|
|
173
|
+
const validFrom = certificate?.valid_from || null;
|
|
174
|
+
const daysRemaining = validTo
|
|
175
|
+
? Math.ceil((new Date(validTo).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
|
|
176
|
+
: null;
|
|
177
|
+
const subjectAltName = typeof certificate?.subjectaltname === "string"
|
|
178
|
+
? certificate.subjectaltname.split(",").map((entry) => entry.trim().replace(/^DNS:/, ""))
|
|
179
|
+
: [];
|
|
180
|
+
const issues = [];
|
|
181
|
+
if (!socket.authorized) {
|
|
182
|
+
issues.push(typeof socket.authorizationError === "string"
|
|
183
|
+
? socket.authorizationError
|
|
184
|
+
: "Certificate is not trusted.");
|
|
185
|
+
}
|
|
186
|
+
if (allowInsecureTls) {
|
|
187
|
+
issues.push("Insecure TLS observation mode is enabled via EXTERNAL_POSTURE_ALLOW_INSECURE_TLS.");
|
|
188
|
+
}
|
|
189
|
+
if (daysRemaining !== null && daysRemaining <= 14)
|
|
190
|
+
issues.push("Certificate expires very soon.");
|
|
191
|
+
if (protocol && /tlsv1(\.0|\.1)?$/i.test(protocol))
|
|
192
|
+
issues.push("TLS protocol is outdated.");
|
|
193
|
+
resolve({
|
|
194
|
+
available: true,
|
|
195
|
+
valid: Boolean(socket.authorized),
|
|
196
|
+
authorized: Boolean(socket.authorized),
|
|
197
|
+
issuer: certificateName(certificate, "issuer"),
|
|
198
|
+
subject: certificateName(certificate, "subject"),
|
|
76
199
|
validFrom,
|
|
77
200
|
validTo,
|
|
78
201
|
daysRemaining,
|
|
@@ -81,6 +204,13 @@ export const scanTls = (targetUrl) => {
|
|
|
81
204
|
fingerprint: certificate?.fingerprint256 || null,
|
|
82
205
|
subjectAltName,
|
|
83
206
|
issues,
|
|
207
|
+
host: targetUrl.hostname,
|
|
208
|
+
port: Number(targetUrl.port || 443),
|
|
209
|
+
checkedAt: new Date().toISOString(),
|
|
210
|
+
serialNumber: certificate?.serialNumber || null,
|
|
211
|
+
keyBits: keyBitsFromCertificate(certificate),
|
|
212
|
+
keyType: keyTypeFromCertificate(certificate),
|
|
213
|
+
chain: chainFromCertificate(certificate),
|
|
84
214
|
});
|
|
85
215
|
socket.end();
|
|
86
216
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const analyzeTarget: typeof analyzeUrl;
|
|
|
12
12
|
export { formatErrorMessage };
|
|
13
13
|
export { buildCompromiseSignals, emptyCompromiseSignals } from "./compromiseSignals.js";
|
|
14
14
|
export { buildActionPlan } from "./actionPlan.js";
|
|
15
|
+
export { scanLiveCertificate } from "./certificate.js";
|
|
15
16
|
export { buildExposureBrief } from "./exposureBrief.js";
|
|
16
17
|
export { buildVendorExposureBrief } from "./vendorExposure.js";
|
|
17
18
|
export { analyzeInfrastructure } from "./infrastructure.js";
|
package/dist/index.js
CHANGED
|
@@ -1040,6 +1040,7 @@ export const analyzeTarget = analyzeUrl;
|
|
|
1040
1040
|
export { formatErrorMessage };
|
|
1041
1041
|
export { buildCompromiseSignals, emptyCompromiseSignals } from "./compromiseSignals.js";
|
|
1042
1042
|
export { buildActionPlan } from "./actionPlan.js";
|
|
1043
|
+
export { scanLiveCertificate } from "./certificate.js";
|
|
1043
1044
|
export { buildExposureBrief } from "./exposureBrief.js";
|
|
1044
1045
|
export { buildVendorExposureBrief } from "./vendorExposure.js";
|
|
1045
1046
|
export { analyzeInfrastructure } from "./infrastructure.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -67,6 +67,22 @@ export interface CertificateResult {
|
|
|
67
67
|
subjectAltName: string[];
|
|
68
68
|
issues: string[];
|
|
69
69
|
}
|
|
70
|
+
export interface LiveCertificateChainEntry {
|
|
71
|
+
subject: string | null;
|
|
72
|
+
issuer: string | null;
|
|
73
|
+
validFrom: string | null;
|
|
74
|
+
validTo: string | null;
|
|
75
|
+
fingerprint: string | null;
|
|
76
|
+
}
|
|
77
|
+
export interface LiveCertificateResult extends CertificateResult {
|
|
78
|
+
host: string;
|
|
79
|
+
port: number;
|
|
80
|
+
checkedAt: string;
|
|
81
|
+
serialNumber: string | null;
|
|
82
|
+
keyBits: number | null;
|
|
83
|
+
keyType: string | null;
|
|
84
|
+
chain: LiveCertificateChainEntry[];
|
|
85
|
+
}
|
|
70
86
|
export interface RedirectHop {
|
|
71
87
|
url: string;
|
|
72
88
|
status: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Passive external security posture scanner for public URLs and web services.",
|
|
6
6
|
"author": {
|
|
@@ -77,6 +77,10 @@
|
|
|
77
77
|
"./action-plan": {
|
|
78
78
|
"types": "./dist/actionPlan.d.ts",
|
|
79
79
|
"default": "./dist/actionPlan.js"
|
|
80
|
+
},
|
|
81
|
+
"./live-certificate": {
|
|
82
|
+
"types": "./dist/certificate.d.ts",
|
|
83
|
+
"default": "./dist/certificate.js"
|
|
80
84
|
}
|
|
81
85
|
},
|
|
82
86
|
"files": [
|