rmm2/agent-bsd/collector/certificates.go

204 lines
5.7 KiB
Go

package collector
import (
"crypto/x509"
"encoding/pem"
"os/exec"
"path/filepath"
"strings"
"time"
)
type Certificate struct {
Name string `json:"name"`
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotBefore string `json:"not_before"`
NotAfter string `json:"not_after"`
DaysLeft int `json:"days_left"`
Serial string `json:"serial"`
SAN string `json:"san,omitempty"`
KeyUsage string `json:"key_usage,omitempty"`
IsCA bool `json:"is_ca"`
Status string `json:"status"` // "valid", "expiring", "expired"
Source string `json:"source"` // "opnsense", "file"
}
// CollectCertificates liest Zertifikate aus OPNsense config.xml und gaengigen Pfaden
func CollectCertificates() []Certificate {
var certs []Certificate
// 1. OPNsense config.xml Zertifikate
certs = append(certs, collectOPNsenseCerts()...)
// 2. Zertifikate aus /var/etc/cert/ (aktiv genutzte)
certs = append(certs, collectCertDir("/var/etc/cert/")...)
return certs
}
func collectOPNsenseCerts() []Certificate {
// configctl: Zertifikate als Liste ausgeben
// Alternativ: direkt config.xml parsen
out, err := exec.Command("grep", "-A2", "<crt>", "/conf/config.xml").Output()
if err != nil || len(out) == 0 {
return collectFromConfigXML()
}
return collectFromConfigXML()
}
func collectFromConfigXML() []Certificate {
// PHP-basiertes Auslesen der Zertifikate (OPNsense hat php verfuegbar)
// Skript via stdin uebergeben, da -r bei langen Strings Probleme macht
script := `<?php
require_once("config.inc");
$config = parse_config();
if (isset($config['cert'])) {
foreach ($config['cert'] as $cert) {
$x509 = openssl_x509_parse(base64_decode($cert['crt']));
if ($x509 === false) continue;
$name = isset($cert['descr']) ? $cert['descr'] : 'unnamed';
$subject = isset($x509['subject']['CN']) ? $x509['subject']['CN'] : '';
$issuer = isset($x509['issuer']['CN']) ? $x509['issuer']['CN'] : '';
$notBefore = date('Y-m-d H:i:s', $x509['validFrom_time_t']);
$notAfter = date('Y-m-d H:i:s', $x509['validTo_time_t']);
$serial = $x509['serialNumberHex'];
$san = '';
if (isset($x509['extensions']['subjectAltName'])) {
$san = $x509['extensions']['subjectAltName'];
}
$isCA = isset($x509['extensions']['basicConstraints']) &&
strpos($x509['extensions']['basicConstraints'], 'CA:TRUE') !== false;
echo "$name|$subject|$issuer|$notBefore|$notAfter|$serial|$san|" . ($isCA ? "1" : "0") . "\n";
}
}
if (isset($config['ca'])) {
foreach ($config['ca'] as $ca) {
$x509 = openssl_x509_parse(base64_decode($ca['crt']));
if ($x509 === false) continue;
$name = isset($ca['descr']) ? $ca['descr'] : 'unnamed-ca';
$subject = isset($x509['subject']['CN']) ? $x509['subject']['CN'] : '';
$issuer = isset($x509['issuer']['CN']) ? $x509['issuer']['CN'] : '';
$notBefore = date('Y-m-d H:i:s', $x509['validFrom_time_t']);
$notAfter = date('Y-m-d H:i:s', $x509['validTo_time_t']);
$serial = $x509['serialNumberHex'];
echo "$name|$subject|$issuer|$notBefore|$notAfter|$serial||1\n";
}
}
`
cmd := exec.Command("/usr/local/bin/php")
cmd.Stdin = strings.NewReader(script)
out, err := cmd.Output()
if err != nil {
// Fallback: PEM-Dateien scannen
return collectCertDir("/var/etc/cert/")
}
var certs []Certificate
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if line == "" {
continue
}
fields := strings.SplitN(line, "|", 8)
if len(fields) < 8 {
continue
}
cert := Certificate{
Name: fields[0],
Subject: fields[1],
Issuer: fields[2],
NotBefore: fields[3],
NotAfter: fields[4],
Serial: fields[5],
SAN: fields[6],
IsCA: fields[7] == "1",
Source: "opnsense",
}
// Status berechnen
if notAfter, err := time.Parse("2006-01-02 15:04:05", cert.NotAfter); err == nil {
daysLeft := int(time.Until(notAfter).Hours() / 24)
cert.DaysLeft = daysLeft
if daysLeft < 0 {
cert.Status = "expired"
} else if daysLeft < 30 {
cert.Status = "expiring"
} else {
cert.Status = "valid"
}
}
certs = append(certs, cert)
}
return certs
}
func collectCertDir(dir string) []Certificate {
matches, err := filepath.Glob(filepath.Join(dir, "*.crt"))
if err != nil {
return nil
}
// Auch .pem Dateien
pemMatches, _ := filepath.Glob(filepath.Join(dir, "*.pem"))
matches = append(matches, pemMatches...)
var certs []Certificate
for _, path := range matches {
out, err := exec.Command("cat", path).Output()
if err != nil {
continue
}
block, _ := pem.Decode(out)
if block == nil || block.Type != "CERTIFICATE" {
continue
}
x509Cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
continue
}
name := filepath.Base(path)
name = strings.TrimSuffix(name, filepath.Ext(name))
daysLeft := int(time.Until(x509Cert.NotAfter).Hours() / 24)
status := "valid"
if daysLeft < 0 {
status = "expired"
} else if daysLeft < 30 {
status = "expiring"
}
var sans []string
for _, dns := range x509Cert.DNSNames {
sans = append(sans, "DNS:"+dns)
}
for _, ip := range x509Cert.IPAddresses {
sans = append(sans, "IP:"+ip.String())
}
cert := Certificate{
Name: name,
Subject: x509Cert.Subject.CommonName,
Issuer: x509Cert.Issuer.CommonName,
NotBefore: x509Cert.NotBefore.Format("2006-01-02 15:04:05"),
NotAfter: x509Cert.NotAfter.Format("2006-01-02 15:04:05"),
DaysLeft: daysLeft,
Serial: x509Cert.SerialNumber.Text(16),
SAN: strings.Join(sans, ", "),
IsCA: x509Cert.IsCA,
Status: status,
Source: "file",
}
certs = append(certs, cert)
}
return certs
}