113 lines
3.4 KiB
TypeScript
113 lines
3.4 KiB
TypeScript
import type { CertificateDto } from "@mmapp/contracts/certificates"
|
|
|
|
export type RosCertificateRow = Record<string, string | undefined>
|
|
|
|
function parseRosDate(raw: string | undefined): Date | null {
|
|
if (!raw?.trim()) return null
|
|
const d = new Date(raw)
|
|
return Number.isNaN(d.getTime()) ? null : d
|
|
}
|
|
|
|
function fmtDate(d: Date): string {
|
|
return d.toISOString().slice(0, 10)
|
|
}
|
|
|
|
function parseSans(raw: string | undefined): string[] {
|
|
if (!raw?.trim()) return []
|
|
return raw
|
|
.split(",")
|
|
.map((part) => part.trim())
|
|
.map((part) => {
|
|
const m = part.match(/^(?:DNS|IP|email):(.+)$/i)
|
|
return (m?.[1] ?? part).trim()
|
|
})
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function parseUsage(raw: string | undefined): CertificateDto["usage"] {
|
|
if (!raw?.trim()) return []
|
|
const out = new Set<CertificateDto["usage"][number]>()
|
|
for (const part of raw.split(",")) {
|
|
const token = part.trim().toLowerCase()
|
|
if (token === "tls-server") out.add("server")
|
|
else if (token === "tls-client") out.add("client")
|
|
else if (token === "key-cert-sign") out.add("ca")
|
|
else if (token === "crl-sign") out.add("crl-sign")
|
|
}
|
|
return [...out]
|
|
}
|
|
|
|
function parseFlags(raw: string | undefined): { revoked: boolean; expired: boolean } {
|
|
const flags = (raw ?? "").toUpperCase()
|
|
return {
|
|
revoked: flags.includes("R"),
|
|
expired: flags.includes("E"),
|
|
}
|
|
}
|
|
|
|
export function mapRosCertificateRow(
|
|
serverId: number,
|
|
serverName: string,
|
|
row: RosCertificateRow,
|
|
): CertificateDto | null {
|
|
const name = (row.name ?? "").trim()
|
|
if (!name) return null
|
|
|
|
const rosId = (row[".id"] ?? "").trim()
|
|
const fingerprint = (row.fingerprint ?? "").trim()
|
|
const serialNumber = (row["serial-number"] ?? "").trim()
|
|
const stableId = rosId || fingerprint || serialNumber || name
|
|
|
|
const { revoked, expired: expiredFlag } = parseFlags(row.flags)
|
|
const validFrom = parseRosDate(row["invalid-before"])
|
|
const validUntil = parseRosDate(row["invalid-after"])
|
|
const now = new Date()
|
|
const daysLeft = validUntil
|
|
? Math.ceil((validUntil.getTime() - now.getTime()) / 86_400_000)
|
|
: 0
|
|
|
|
let status: CertificateDto["status"] = "valid"
|
|
if (revoked) status = "revoked"
|
|
else if (expiredFlag || (validUntil != null && validUntil.getTime() < now.getTime())) status = "expired"
|
|
|
|
const keySize = Number.parseInt(String(row["key-size"] ?? "0"), 10)
|
|
|
|
return {
|
|
id: `${serverId}:${stableId}`,
|
|
name,
|
|
serverId: String(serverId),
|
|
serverName,
|
|
commonName: (row["common-name"] ?? name).trim(),
|
|
sans: parseSans(row["subject-alt-name"]),
|
|
issuedBy: (row.issuer ?? "—").trim() || "—",
|
|
validFrom: validFrom ? fmtDate(validFrom) : "—",
|
|
validUntil: validUntil ? fmtDate(validUntil) : "—",
|
|
daysLeft,
|
|
keySize: Number.isFinite(keySize) ? keySize : 0,
|
|
usage: parseUsage(row["key-usage"]),
|
|
trusted: row.trusted === "true",
|
|
status,
|
|
acmeStatus: row["acme-status"]?.trim() || undefined,
|
|
}
|
|
}
|
|
|
|
export function mapRosCertificates(
|
|
serverId: number,
|
|
serverName: string,
|
|
rows: RosCertificateRow[],
|
|
): CertificateDto[] {
|
|
const seen = new Set<string>()
|
|
return rows
|
|
.map((row) => mapRosCertificateRow(serverId, serverName, row))
|
|
.filter((row): row is CertificateDto => row != null)
|
|
.map((cert, index) => {
|
|
if (!seen.has(cert.id)) {
|
|
seen.add(cert.id)
|
|
return cert
|
|
}
|
|
const uniqueId = `${cert.id}#${index}`
|
|
seen.add(uniqueId)
|
|
return { ...cert, id: uniqueId }
|
|
})
|
|
}
|