feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
158
checks/cbssl/README.md
Обычный файл
158
checks/cbssl/README.md
Обычный файл
@@ -0,0 +1,158 @@
|
||||
# CBSSL - Browser-Based SSL Certificate Chain Checker
|
||||
|
||||
## Overview
|
||||
|
||||
The `cbssl` (Browser SSL) check validates SSL/TLS certificates against actual browser CA root stores. Unlike the standard `cssl` check which uses Go's system certificate pool, this check validates certificates against the same CA roots used by Chrome and Firefox on Linux.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dual Browser Validation**: Validates certificates against both Chrome and Firefox CA roots
|
||||
- **Full Chain Information**: Returns complete certificate chain details for each browser
|
||||
- **Expiration Tracking**: Monitors certificate expiration dates and warns before expiry
|
||||
- **Detailed Metrics**: Provides InfluxDB-compatible metrics with detailed validation results
|
||||
|
||||
## How It Works
|
||||
|
||||
### Browser CA Roots
|
||||
|
||||
On Linux, both Chrome and Firefox use the system's CA certificate store:
|
||||
|
||||
- **Chrome/Chromium**: Uses `/etc/ssl/certs/ca-certificates.crt` (on Alpine/Debian)
|
||||
- **Firefox**: Uses NSS library or falls back to system certificates
|
||||
|
||||
The check loads these CA certificates and validates the target site's certificate chain against each browser's root store independently.
|
||||
|
||||
### Certificate Sources
|
||||
|
||||
The checker looks for CA certificates in the following locations (in order):
|
||||
|
||||
1. `/etc/ssl/certs/ca-certificates.crt` - Alpine/Debian system certificates
|
||||
2. `/etc/ssl/cert.pem` - macOS system certificates
|
||||
3. `/etc/pki/tls/certs/ca-bundle.crt` - RHEL/CentOS system certificates
|
||||
4. `/usr/local/share/ca-certificates/` - Custom certificate directory
|
||||
5. `/data/rsmon/docker/cert-bundles/output/` - Project-specific certificate bundles
|
||||
|
||||
### Mozilla CA Bundle
|
||||
|
||||
The project includes the Mozilla CA certificate bundle which contains the same CA certificates used by Firefox:
|
||||
|
||||
```bash
|
||||
# Downloaded from: https://curl.se/ca/cacert.pem
|
||||
# Location: docker/cert-bundles/output/mozilla-ca-bundle.crt
|
||||
# Certificate count: 144 CA certificates
|
||||
```
|
||||
|
||||
## Result Format
|
||||
|
||||
```go
|
||||
type Result struct {
|
||||
// Standard check result
|
||||
cr.CheckResult
|
||||
|
||||
// Chrome-specific results
|
||||
ChromeValid bool // true if certificate validates against Chrome roots
|
||||
ChromeError string // error message if Chrome validation fails
|
||||
ChromeChain []CertInfo // certificate chain as validated by Chrome
|
||||
|
||||
// Firefox-specific results
|
||||
FirefoxValid bool // true if certificate validates against Firefox roots
|
||||
FirefoxError string // error message if Firefox validation fails
|
||||
FirefoxChain []CertInfo // certificate chain as validated by Firefox
|
||||
|
||||
// Certificate details
|
||||
Expires *time.Time // certificate expiration date
|
||||
Subject string // certificate subject (CN)
|
||||
Issuer string // certificate issuer (CN)
|
||||
DNSNames []string // certificate SANs
|
||||
}
|
||||
```
|
||||
|
||||
## Check Parameters
|
||||
|
||||
Currently, the check validates against both browsers. Future versions may support:
|
||||
|
||||
- `browser` - Specify which browser to validate against: "chrome", "firefox", or "all" (default)
|
||||
|
||||
## InfluxDB Metrics
|
||||
|
||||
The check provides the following metrics:
|
||||
|
||||
**Fields:**
|
||||
- `took` - Request duration in milliseconds
|
||||
- `chrome_valid` - Whether certificate validated against Chrome roots (1/0)
|
||||
- `firefox_valid` - Whether certificate validated against Firefox roots (1/0)
|
||||
- `expires_at` - Unix timestamp of certificate expiration
|
||||
- `days_until_expiry` - Days until certificate expires
|
||||
|
||||
**Tags:**
|
||||
- `check` - Check ID
|
||||
- `state` - Check state (OK, WARN, ERR, FAIL)
|
||||
- `subject` - Certificate subject CN
|
||||
- `issuer` - Certificate issuer CN
|
||||
- `chrome_error` - Chrome validation error (if any)
|
||||
- `firefox_error` - Firefox validation error (if any)
|
||||
- `dns_names` - Comma-separated list of DNS names in certificate
|
||||
|
||||
## Example Usage
|
||||
|
||||
```go
|
||||
import "rsgit.ru/rsmon/rsmon/checks/cbssl"
|
||||
|
||||
// Perform the check
|
||||
result := cbssl.Perform(check)
|
||||
|
||||
// Check results
|
||||
if result.ChromeValid && result.FirefoxValid {
|
||||
// Certificate is valid for both browsers
|
||||
} else if !result.ChromeValid {
|
||||
// Certificate fails Chrome validation
|
||||
fmt.Printf("Chrome error: %s\n", result.ChromeError)
|
||||
}
|
||||
```
|
||||
|
||||
## Differences from cssl
|
||||
|
||||
| Feature | cssl | cbssl |
|
||||
|---------|------|-------|
|
||||
| CA Root Source | Go's system pool | Browser-specific CA roots |
|
||||
| Browser Validation | Single (system) | Dual (Chrome + Firefox) |
|
||||
| Chain Information | Basic leaf cert | Full chain per browser |
|
||||
| Use Case | General SSL validation | Browser compatibility verification |
|
||||
|
||||
## Certificate Bundle Management
|
||||
|
||||
To update the CA certificate bundles:
|
||||
|
||||
```bash
|
||||
# Download latest Mozilla CA bundle
|
||||
cd /data/rsmon
|
||||
curl -fsSL -o docker/cert-bundles/output/mozilla-ca-bundle.crt \
|
||||
https://curl.se/ca/cacert.pem
|
||||
|
||||
# Verify
|
||||
grep -c "BEGIN CERTIFICATE" docker/cert-bundles/output/mozilla-ca-bundle.crt
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "failed to load any CA certificates"
|
||||
|
||||
This error occurs when no CA certificates can be found. Solutions:
|
||||
|
||||
1. Ensure the system has `ca-certificates` package installed
|
||||
2. Place custom CA certificates in `/usr/local/share/ca-certificates/`
|
||||
3. Add certificates to the project bundle at `docker/cert-bundles/output/`
|
||||
|
||||
### Certificate validation failures
|
||||
|
||||
If validation fails for a site that works in browsers:
|
||||
|
||||
1. Check if the site uses a custom CA not in the Mozilla bundle
|
||||
2. Verify the site's intermediate certificates are properly configured
|
||||
3. Check for expired or malformed certificate chains
|
||||
|
||||
## References
|
||||
|
||||
- [Mozilla Included CA Certificate List](https://wiki.mozilla.org/CA/Included_Certificates)
|
||||
- [Chrome Root Certificate Policy](https://www.chromium.org/Home/chromium-security/root-ca-policy)
|
||||
- [curl CA Bundle](https://curl.se/docs/caextract.html)
|
||||
429
checks/cbssl/cbssl.go
Обычный файл
429
checks/cbssl/cbssl.go
Обычный файл
@@ -0,0 +1,429 @@
|
||||
// Package cbssl provides browser-based SSL certificate chain validation.
|
||||
// It validates certificates against actual browser CA root stores (Chrome, Firefox).
|
||||
package cbssl
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
)
|
||||
|
||||
const stateOK = "OK"
|
||||
|
||||
// BrowserType represents the type of browser to use for CA roots
|
||||
type BrowserType string
|
||||
|
||||
// Browser type constants for CA root selection.
|
||||
const (
|
||||
BrowserChrome BrowserType = "chrome"
|
||||
BrowserFirefox BrowserType = "firefox"
|
||||
BrowserAll BrowserType = "all"
|
||||
)
|
||||
|
||||
var (
|
||||
// Certificate pools for different browsers
|
||||
chromeRoots *x509.CertPool
|
||||
firefoxRoots *x509.CertPool
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// BrowserCertPools holds the certificate pools for different browsers
|
||||
type BrowserCertPools struct {
|
||||
Chrome *x509.CertPool
|
||||
Firefox *x509.CertPool
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Initialize certificate pools
|
||||
once.Do(func() {
|
||||
var err error
|
||||
chromeRoots, firefoxRoots, err = LoadBrowserCARoots()
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to load browser CA roots: %v", err)
|
||||
// Fall back to system roots
|
||||
chromeRoots = x509.NewCertPool()
|
||||
firefoxRoots = x509.NewCertPool()
|
||||
systemRoots, err := x509.SystemCertPool()
|
||||
if err == nil {
|
||||
chromeRoots = systemRoots
|
||||
firefoxRoots = systemRoots
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Perform executes the browser-based SSL certificate check
|
||||
func Perform(c *models.Check) *Result {
|
||||
result := &Result{}
|
||||
result.State = stateOK
|
||||
|
||||
// Get URL to check
|
||||
checkURL, err := getCheckURL(c)
|
||||
if err != nil {
|
||||
result.State = "FAIL"
|
||||
result.Error = errors.Wrap(err, "invalid url")
|
||||
return result
|
||||
}
|
||||
log.Println("CBSSL URL:", checkURL)
|
||||
|
||||
// Parse hostname from URL
|
||||
parsedURL, err := url.Parse(checkURL)
|
||||
if err != nil {
|
||||
result.State = "FAIL"
|
||||
result.Error = errors.Wrap(err, "failed to parse url")
|
||||
return result
|
||||
}
|
||||
hostname := parsedURL.Hostname()
|
||||
|
||||
// Get browser type - default to checking all browsers
|
||||
// TODO: Add Browser field to CheckSettings to allow per-check configuration
|
||||
browserType := BrowserAll
|
||||
|
||||
// Create custom TLS client that validates against browser roots
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
ServerName: hostname,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
// We'll set RootCAs per request to test against different browsers
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test against Chrome roots
|
||||
if browserType == BrowserChrome || browserType == BrowserAll {
|
||||
chromeResult := testWithBrowser(client, checkURL, chromeRoots, "Chrome")
|
||||
result.ChromeValid = chromeResult.Valid
|
||||
result.ChromeError = chromeResult.ErrorMsg
|
||||
result.ChromeChain = chromeResult.ChainInfo
|
||||
if !chromeResult.Valid {
|
||||
result.State = "ERR"
|
||||
result.Error = errors.New("certificate validation failed against Chrome roots")
|
||||
if chromeResult.ErrorMsg != "" {
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("Chrome: %s", chromeResult.ErrorMsg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test against Firefox roots
|
||||
if browserType == BrowserFirefox || browserType == BrowserAll {
|
||||
firefoxResult := testWithBrowser(client, checkURL, firefoxRoots, "Firefox")
|
||||
result.FirefoxValid = firefoxResult.Valid
|
||||
result.FirefoxError = firefoxResult.ErrorMsg
|
||||
result.FirefoxChain = firefoxResult.ChainInfo
|
||||
if !firefoxResult.Valid && result.State == stateOK {
|
||||
result.State = "ERR"
|
||||
result.Error = errors.New("certificate validation failed against Firefox roots")
|
||||
}
|
||||
if firefoxResult.ErrorMsg != "" {
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("Firefox: %s", firefoxResult.ErrorMsg))
|
||||
}
|
||||
}
|
||||
|
||||
// Get certificate info for expiration check
|
||||
certInfo, err := getCertificateInfo(checkURL)
|
||||
if err != nil {
|
||||
if result.State == stateOK {
|
||||
result.State = "WARN"
|
||||
}
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("Failed to get certificate info: %v", err))
|
||||
} else {
|
||||
result.Expires = &certInfo.NotAfter
|
||||
result.Subject = certInfo.Subject.CommonName
|
||||
result.Issuer = certInfo.Issuer.CommonName
|
||||
result.DNSNames = certInfo.DNSNames
|
||||
|
||||
// Check expiration
|
||||
exp := time.Until(certInfo.NotAfter).Hours() / 24
|
||||
if exp < 2 {
|
||||
if result.State == stateOK {
|
||||
result.State = "WARN"
|
||||
}
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("Certificate expires in %.1f days", exp))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// testWithBrowser tests the certificate against a specific browser's CA roots
|
||||
func testWithBrowser(client *http.Client, checkURL string, roots *x509.CertPool, browserName string) BrowserTestResult {
|
||||
result := BrowserTestResult{Valid: false}
|
||||
|
||||
// Clone transport and set custom root CAs
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
result.ErrorMsg = fmt.Sprintf("failed to get transport for %s", browserName)
|
||||
return result
|
||||
}
|
||||
|
||||
customTransport := transport.Clone()
|
||||
customTransport.TLSClientConfig.RootCAs = roots
|
||||
|
||||
// Create new client with custom transport
|
||||
customClient := &http.Client{
|
||||
Timeout: client.Timeout,
|
||||
CheckRedirect: client.CheckRedirect,
|
||||
Transport: customTransport,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", checkURL, http.NoBody)
|
||||
if err != nil {
|
||||
result.ErrorMsg = fmt.Sprintf("failed to create request for %s: %v", browserName, err)
|
||||
return result
|
||||
}
|
||||
|
||||
req.Header.Set("Cache-Control", "max-age=0")
|
||||
req.Header.Set("Connection", "close")
|
||||
req.Header.Set("User-Agent", getUserAgent(browserName))
|
||||
|
||||
resp, err := customClient.Do(req)
|
||||
if err != nil {
|
||||
// Check if it's a certificate verification error
|
||||
if strings.Contains(err.Error(), "certificate") || strings.Contains(err.Error(), "x509") {
|
||||
result.ErrorMsg = fmt.Sprintf("certificate verification failed: %v", err)
|
||||
} else {
|
||||
result.ErrorMsg = fmt.Sprintf("connection failed: %v", err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck // accepted lint exception
|
||||
|
||||
result.Valid = true
|
||||
|
||||
// Get chain info
|
||||
if resp.TLS != nil && len(resp.TLS.VerifiedChains) > 0 {
|
||||
chain := resp.TLS.VerifiedChains[0]
|
||||
result.ChainInfo = buildChainInfo(chain)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// BrowserTestResult represents the result of testing against a specific browser
|
||||
type BrowserTestResult struct {
|
||||
Valid bool
|
||||
ErrorMsg string
|
||||
ChainInfo []CertInfo
|
||||
}
|
||||
|
||||
func buildChainInfo(chain []*x509.Certificate) []CertInfo {
|
||||
info := make([]CertInfo, 0, len(chain))
|
||||
for _, cert := range chain {
|
||||
info = append(info, CertInfo{
|
||||
Subject: cert.Subject.CommonName,
|
||||
Issuer: cert.Issuer.CommonName,
|
||||
NotBefore: cert.NotBefore,
|
||||
NotAfter: cert.NotAfter,
|
||||
IsCA: cert.IsCA,
|
||||
PublicKey: cert.PublicKeyAlgorithm.String(),
|
||||
Signature: cert.SignatureAlgorithm.String(),
|
||||
})
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func getUserAgent(browserName string) string {
|
||||
switch browserName {
|
||||
case "Chrome":
|
||||
return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
case "Firefox":
|
||||
return "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0"
|
||||
default:
|
||||
return "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
}
|
||||
}
|
||||
|
||||
// getCertificateInfo retrieves certificate information without validation
|
||||
func getCertificateInfo(checkURL string) (*x509.Certificate, error) {
|
||||
parsedURL, err := url.Parse(checkURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostname := parsedURL.Hostname()
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
ServerName: hostname,
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := client.Get(checkURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck // accepted lint exception
|
||||
|
||||
if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
|
||||
return nil, fmt.Errorf("no TLS certificates found")
|
||||
}
|
||||
|
||||
return resp.TLS.PeerCertificates[0], nil
|
||||
}
|
||||
|
||||
// getCheckURL constructs the URL to check from the check configuration
|
||||
func getCheckURL(c *models.Check) (string, error) {
|
||||
if c.URL != nil && *c.URL != "" {
|
||||
ur := *c.URL
|
||||
u, err := url.Parse(ur)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "bad url")
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "https"
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
return "https://" + c.Monitor.Host, nil
|
||||
}
|
||||
|
||||
// LoadBrowserCARoots loads CA root certificates from browser installations
|
||||
func LoadBrowserCARoots() (chrome, firefox *x509.CertPool, err error) {
|
||||
chrome = x509.NewCertPool()
|
||||
firefox = x509.NewCertPool()
|
||||
|
||||
// Load Chrome/Chromium CA roots (uses system certs on Linux)
|
||||
// On Alpine Linux, this is typically /etc/ssl/certs/ca-certificates.crt
|
||||
chromeLoaded := false
|
||||
|
||||
// Try common certificate bundle locations
|
||||
certPaths := []string{
|
||||
"/etc/ssl/certs/ca-certificates.crt", // Alpine/Debian
|
||||
"/etc/ssl/cert.pem", // macOS
|
||||
"/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS
|
||||
"/usr/local/share/ca-certificates/", // Custom certs
|
||||
"/data/rsmon/docker/cert-bundles/output/", // Our custom bundled certs
|
||||
}
|
||||
|
||||
for _, certPath := range certPaths {
|
||||
if loadCertsFromPath(chrome, certPath) {
|
||||
chromeLoaded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Load Firefox CA roots (NSS database)
|
||||
// Firefox uses its own NSS database at ~/.pki/nssdb/ or uses system roots
|
||||
firefoxLoaded := loadFirefoxCARoots(firefox)
|
||||
|
||||
// If Firefox failed to load, use Chrome/system roots as fallback
|
||||
if !firefoxLoaded && chromeLoaded {
|
||||
firefox = chrome
|
||||
}
|
||||
|
||||
// If both failed, return error
|
||||
if !chromeLoaded {
|
||||
return nil, nil, fmt.Errorf("failed to load any CA certificates")
|
||||
}
|
||||
|
||||
return chrome, firefox, nil
|
||||
}
|
||||
|
||||
// loadCertsFromPath loads certificates from a file or directory
|
||||
func loadCertsFromPath(pool *x509.CertPool, path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// Load all .crt and .pem files from directory
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
loaded := false
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
ext := filepath.Ext(file.Name())
|
||||
if ext == ".crt" || ext == ".pem" {
|
||||
fullPath := filepath.Join(path, file.Name())
|
||||
if pool.AppendCertsFromPEM(readFile(fullPath)) {
|
||||
loaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return loaded
|
||||
}
|
||||
|
||||
// Load single file
|
||||
return pool.AppendCertsFromPEM(readFile(path))
|
||||
}
|
||||
|
||||
func readFile(path string) []byte {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// loadFirefoxCARoots loads Firefox CA roots from NSS database
|
||||
// On Linux, Firefox typically uses the system's CA certificates via libnssckbi.so
|
||||
func loadFirefoxCARoots(pool *x509.CertPool) bool {
|
||||
// Firefox on Linux usually uses the system certificate database
|
||||
// Try to load from Mozilla's built-in certificate bundle if available
|
||||
paths := []string{
|
||||
"/usr/lib/x86_64-linux-gnu/libnssckbi.so", // Debian/Ubuntu NSS module
|
||||
"/usr/lib/libnssckbi.so", // Generic path
|
||||
"/data/rsmon/docker/cert-bundles/output/mozilla/", // Our bundled Mozilla certs
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
if loadCertsFromPath(pool, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ParsePEMCerts parses PEM-encoded certificates from data
|
||||
func ParsePEMCerts(data []byte) ([]*x509.Certificate, error) {
|
||||
var certs []*x509.Certificate
|
||||
var block *pem.Block
|
||||
|
||||
rest := data
|
||||
for {
|
||||
block, rest = pem.Decode(rest)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type == "CERTIFICATE" {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
}
|
||||
}
|
||||
|
||||
if len(certs) == 0 {
|
||||
return nil, fmt.Errorf("no certificates found")
|
||||
}
|
||||
|
||||
return certs, nil
|
||||
}
|
||||
135
checks/cbssl/cbssl_test.go
Обычный файл
135
checks/cbssl/cbssl_test.go
Обычный файл
@@ -0,0 +1,135 @@
|
||||
package cbssl
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParsePEMCerts(t *testing.T) {
|
||||
// Generate a valid self-signed certificate for testing
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: "testca"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
IsCA: true,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create certificate: %v", err)
|
||||
}
|
||||
|
||||
pemData := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
|
||||
certs, err := ParsePEMCerts(pemData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParsePEMCerts failed: %v", err)
|
||||
}
|
||||
|
||||
if len(certs) != 1 {
|
||||
t.Fatalf("expected 1 certificate, got %d", len(certs))
|
||||
}
|
||||
|
||||
if certs[0] == nil {
|
||||
t.Fatal("expected non-nil certificate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePEMCertsInvalid(t *testing.T) {
|
||||
// Test invalid PEM data
|
||||
pemData := []byte(`not a valid PEM`)
|
||||
|
||||
_, err := ParsePEMCerts(pemData)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid PEM data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserAgent(t *testing.T) {
|
||||
tests := []struct {
|
||||
browser string
|
||||
wantStart string
|
||||
}{
|
||||
{"Chrome", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"},
|
||||
{"Firefox", "Mozilla/5.0 (X11; Linux x86_64"},
|
||||
{"Unknown", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.browser, func(t *testing.T) {
|
||||
ua := getUserAgent(tt.browser)
|
||||
if len(ua) < 20 {
|
||||
t.Errorf("getUserAgent() returned too short string: %s", ua)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowserTypeString(t *testing.T) {
|
||||
if BrowserChrome != "chrome" {
|
||||
t.Errorf("expected 'chrome', got %s", BrowserChrome)
|
||||
}
|
||||
if BrowserFirefox != "firefox" {
|
||||
t.Errorf("expected 'firefox', got %s", BrowserFirefox)
|
||||
}
|
||||
if BrowserAll != "all" {
|
||||
t.Errorf("expected 'all', got %s", BrowserAll)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertInfo(t *testing.T) {
|
||||
info := CertInfo{
|
||||
Subject: "example.com",
|
||||
Issuer: "CA Root",
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
IsCA: false,
|
||||
PublicKey: "RSA",
|
||||
Signature: "SHA256-RSA",
|
||||
}
|
||||
|
||||
if info.Subject != "example.com" {
|
||||
t.Errorf("expected subject 'example.com', got %s", info.Subject)
|
||||
}
|
||||
if info.IsCA {
|
||||
t.Error("expected IsCA to be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFile(t *testing.T) {
|
||||
// Test reading non-existent file
|
||||
data := readFile("/nonexistent/file.txt")
|
||||
if data != nil {
|
||||
t.Error("expected nil for non-existent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBrowserCARoots(t *testing.T) {
|
||||
chrome, firefox, err := LoadBrowserCARoots()
|
||||
if err != nil {
|
||||
t.Logf("LoadBrowserCARoots failed (expected in some environments): %v", err)
|
||||
// This is expected in minimal test environments without CA certificates
|
||||
return
|
||||
}
|
||||
|
||||
if chrome == nil {
|
||||
t.Error("expected non-nil chrome pool")
|
||||
}
|
||||
if firefox == nil {
|
||||
t.Error("expected non-nil firefox pool")
|
||||
}
|
||||
}
|
||||
76
checks/cbssl/result.go
Обычный файл
76
checks/cbssl/result.go
Обычный файл
@@ -0,0 +1,76 @@
|
||||
package cbssl
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
"rsgit.ru/rsmon/rsmon/internal/checkresult"
|
||||
)
|
||||
|
||||
// Result is a result of a browser-based SSL certificate check
|
||||
type Result struct {
|
||||
checkresult.CheckResult
|
||||
|
||||
// Chrome-specific results
|
||||
ChromeValid bool `json:"chrome_valid"`
|
||||
ChromeError string `json:"chrome_error,omitempty"`
|
||||
ChromeChain []CertInfo `json:"chrome_chain,omitempty"`
|
||||
|
||||
// Firefox-specific results
|
||||
FirefoxValid bool `json:"firefox_valid"`
|
||||
FirefoxError string `json:"firefox_error,omitempty"`
|
||||
FirefoxChain []CertInfo `json:"firefox_chain,omitempty"`
|
||||
|
||||
// Certificate details
|
||||
Expires *time.Time `json:"expires,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
DNSNames []string `json:"dns_names,omitempty"`
|
||||
}
|
||||
|
||||
// CertInfo represents information about a certificate in the chain
|
||||
type CertInfo struct {
|
||||
Subject string `json:"subject"`
|
||||
Issuer string `json:"issuer"`
|
||||
NotBefore time.Time `json:"not_before"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
IsCA bool `json:"is_ca"`
|
||||
PublicKey string `json:"public_key_algorithm"`
|
||||
Signature string `json:"signature_algorithm"`
|
||||
}
|
||||
|
||||
// InfluxFields returns the fields for InfluxDB metrics
|
||||
func (r *Result) InfluxFields() map[string]interface{} {
|
||||
ret := make(map[string]interface{}, 0)
|
||||
ret["took"] = int64(r.Duration / time.Millisecond)
|
||||
ret["chrome_valid"] = r.ChromeValid
|
||||
ret["firefox_valid"] = r.FirefoxValid
|
||||
if r.Expires != nil {
|
||||
ret["expires_at"] = r.Expires.Unix()
|
||||
ret["days_until_expiry"] = int64(time.Until(*r.Expires).Hours() / 24)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// InfluxTags returns the tags for InfluxDB metrics
|
||||
func (r *Result) InfluxTags(c models.Check) map[string]string { //nolint:gocritic // hugeParam: accepted for interface compatibility
|
||||
ret := make(map[string]string, 0)
|
||||
ret["check"] = strconv.FormatInt(c.ID, 10)
|
||||
ret["state"] = r.State
|
||||
ret["subject"] = r.Subject
|
||||
ret["issuer"] = r.Issuer
|
||||
if r.Error != nil {
|
||||
ret["error"] = r.Error.Error()
|
||||
}
|
||||
if r.ChromeError != "" {
|
||||
ret["chrome_error"] = r.ChromeError
|
||||
}
|
||||
if r.FirefoxError != "" {
|
||||
ret["firefox_error"] = r.FirefoxError
|
||||
}
|
||||
ret["warnings"] = strings.Join(r.Warnings, ",")
|
||||
ret["dns_names"] = strings.Join(r.DNSNames, ",")
|
||||
return ret
|
||||
}
|
||||
Ссылка в новой задаче
Block a user