/**
* SSL Labs SSL/TLS Configuration Analyzer
* API v4: https://api.ssllabs.com/api/v4/analyze
* Docs: https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v4.md
*/
export interface SSLLabsOptions {
/** Maximum age of cached report in hours (default: 24) */
maxAge?: number;
/** Force a new assessment (default: false) */
startNew?: boolean;
/** Get all endpoint details (default: true) */
all?: boolean;
/** Your email address (required by API) */
email: string;
}
export interface SSLLabsEndpoint {
ipAddress: string;
serverName?: string;
statusMessage: string;
grade?: string;
gradeTrustIgnored?: string;
hasWarnings: boolean;
isExceptional: boolean;
progress: number;
duration: number;
eta?: number;
delegation?: number;
}
export interface SSLLabsResponse {
host: string;
port: number;
protocol: string;
isPublic: boolean;
status: string;
statusMessage?: string;
startTime: number;
testTime?: number;
engineVersion: string;
criteriaVersion: string;
endpoints?: SSLLabsEndpoint[];
}
export interface SSLLabsResult {
tool: 'ssl_labs';
success: boolean;
status: string;
grade?: string;
host: string;
endpoints: Array<{
ip: string;
grade?: string;
hasWarnings: boolean;
progress: number;
}>;
testTime?: string;
error?: string;
raw?: SSLLabsResponse;
}
/**
* Analyze SSL/TLS configuration using SSL Labs API
*
* IMPORTANT: This is a long-running operation. The initial request starts the scan,
* and you need to poll the API until status is 'READY' or 'ERROR'.
*
* Rate limits: Please be considerate - poll at 5-10 second intervals.
* Required: Email address in header for API access.
*/
export async function analyzeSSLLabs(
url: string,
options: SSLLabsOptions
): Promise<SSLLabsResult> {
try {
// Extract hostname from URL
const hostname = new URL(url).hostname;
// Build API URL with parameters
const params = new URLSearchParams({
host: hostname,
all: options.all !== false ? 'on' : 'off',
});
if (options.maxAge !== undefined) {
params.set('maxAge', options.maxAge.toString());
}
if (options.startNew) {
params.set('startNew', 'on');
}
const apiUrl = `https://api.ssllabs.com/api/v4/analyze?${params.toString()}`;
// Make GET request with email header
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'email': options.email,
},
});
if (!response.ok) {
if (response.status === 429) {
throw new Error('SSL Labs rate limit exceeded. Please wait and try again.');
}
throw new Error(`SSL Labs API error: ${response.status} ${response.statusText}`);
}
const data: SSLLabsResponse = await response.json();
// Check if assessment is complete
if (data.status === 'ERROR') {
return {
tool: 'ssl_labs',
success: false,
status: data.status,
host: hostname,
endpoints: [],
error: data.statusMessage || 'Assessment failed',
raw: data,
};
}
// Format endpoints
const endpoints = (data.endpoints || []).map(ep => ({
ip: ep.ipAddress,
grade: ep.grade,
hasWarnings: ep.hasWarnings,
progress: ep.progress,
}));
// Get best grade from all endpoints
const grades = data.endpoints?.filter(ep => ep.grade).map(ep => ep.grade!) || [];
const bestGrade = grades.length > 0 ? grades.sort()[0] : undefined;
return {
tool: 'ssl_labs',
success: data.status === 'READY',
status: data.status,
grade: bestGrade,
host: hostname,
endpoints,
testTime: data.testTime ? new Date(data.testTime).toISOString() : undefined,
raw: data,
};
} catch (error) {
return {
tool: 'ssl_labs',
success: false,
status: 'ERROR',
host: new URL(url).hostname,
endpoints: [],
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Poll SSL Labs until assessment is complete
* Use this wrapper to automatically handle the polling loop
*/
export async function analyzeSSLLabsComplete(
url: string,
options: SSLLabsOptions,
maxWaitMinutes: number = 5
): Promise<SSLLabsResult> {
const maxWaitMs = maxWaitMinutes * 60 * 1000;
const startTime = Date.now();
const pollInterval = 10000; // 10 seconds
let result = await analyzeSSLLabs(url, options);
// Keep polling until complete or timeout
while (result.status !== 'READY' && result.status !== 'ERROR') {
if (Date.now() - startTime > maxWaitMs) {
return {
...result,
success: false,
error: `Timeout: Assessment did not complete within ${maxWaitMinutes} minutes`,
};
}
// Wait before next poll
await new Promise(resolve => setTimeout(resolve, pollInterval));
// Poll again (fromCache to get latest status)
result = await analyzeSSLLabs(url, { ...options, maxAge: 0 });
}
return result;
}