feat(support-bundle): add host certificate collector and analyzer (#1132)

This commit is contained in:
Dexter Yan
2023-07-17 17:05:05 +12:00
committed by GitHub
parent f69ae77bc3
commit 531edf42e2
17 changed files with 1080 additions and 45 deletions

View File

@@ -1671,6 +1671,55 @@ spec:
required:
- outcomes
type: object
certificatesCollection:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- outcomes
type: object
cpu:
properties:
annotations:

View File

@@ -143,6 +143,55 @@ spec:
required:
- outcomes
type: object
certificatesCollection:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- outcomes
type: object
cpu:
properties:
annotations:
@@ -1002,6 +1051,19 @@ spec:
- certificatePath
- keyPath
type: object
certificatesCollection:
properties:
collectorName:
type: string
exclude:
type: BoolString
paths:
items:
type: string
type: array
required:
- paths
type: object
copy:
properties:
collectorName:

View File

@@ -143,6 +143,55 @@ spec:
required:
- outcomes
type: object
certificatesCollection:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- outcomes
type: object
cpu:
properties:
annotations:
@@ -1002,6 +1051,19 @@ spec:
- certificatePath
- keyPath
type: object
certificatesCollection:
properties:
collectorName:
type: string
exclude:
type: BoolString
paths:
items:
type: string
type: array
required:
- paths
type: object
copy:
properties:
collectorName:

View File

@@ -10403,6 +10403,55 @@ spec:
required:
- outcomes
type: object
certificatesCollection:
properties:
annotations:
additionalProperties:
type: string
type: object
checkName:
type: string
collectorName:
type: string
exclude:
type: BoolString
outcomes:
items:
properties:
fail:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
pass:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
warn:
properties:
message:
type: string
uri:
type: string
when:
type: string
type: object
type: object
type: array
strict:
type: BoolString
required:
- outcomes
type: object
cpu:
properties:
annotations:
@@ -11262,6 +11311,19 @@ spec:
- certificatePath
- keyPath
type: object
certificatesCollection:
properties:
collectorName:
type: string
exclude:
type: BoolString
paths:
items:
type: string
type: array
required:
- paths
type: object
copy:
properties:
collectorName:

View File

@@ -44,6 +44,8 @@ func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, b
return &AnalyzeHostFilesystemPerformance{analyzer.FilesystemPerformance}, true
case analyzer.Certificate != nil:
return &AnalyzeHostCertificate{analyzer.Certificate}, true
case analyzer.CertificatesCollection != nil:
return &AnalyzeHostCertificatesCollection{analyzer.CertificatesCollection}, true
case analyzer.HostServices != nil:
return &AnalyzeHostServices{analyzer.HostServices}, true
case analyzer.HostOS != nil:

View File

@@ -0,0 +1,144 @@
package analyzer
import (
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strconv"
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
type AnalyzeHostCertificatesCollection struct {
hostAnalyzer *troubleshootv1beta2.HostCertificatesCollectionAnalyze
}
func (a *AnalyzeHostCertificatesCollection) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Host Certificates Collection")
}
func (a *AnalyzeHostCertificatesCollection) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostCertificatesCollection) Analyze(getCollectedFileContents func(string) ([]byte, error)) ([]*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
collectorName := hostAnalyzer.CollectorName
if collectorName == "" {
collectorName = "certificatesCollection"
}
name := filepath.Join("host-collectors/certificatesCollection", collectorName+".json")
certificatesInfo, err := getCollectedFileContents(name)
if err != nil {
return nil, errors.Wrap(err, "failed to get contents of certificatesCollection.json")
}
collectorCertificates := []collect.HostCertificatesCollection{}
if err := json.Unmarshal(certificatesInfo, &collectorCertificates); err != nil {
return nil, errors.Wrap(err, "failed to parse certificatesCollection.json")
}
var coll resultCollector
for _, cert := range collectorCertificates {
source := ""
if cert.CertificatePath != "" {
source = fmt.Sprintf("obtained from %s", cert.CertificatePath)
}
if cert.Message == collect.CertMissing {
// return the result immediately if the certificate is missing
coll.push(&AnalyzeResult{
Title: a.Title(),
IsFail: true,
Message: fmt.Sprintf("Certificate is missing, cannot be %s", source),
})
} else {
results, err := a.analyzeHostAnalyzeCertificatesResult(cert.CertificateChain, hostAnalyzer.Outcomes, source)
if err != nil {
return nil, err
}
for _, result := range results {
coll.push(result)
}
}
}
return coll.get(a.Title()), nil
}
func (a *AnalyzeHostCertificatesCollection) analyzeHostAnalyzeCertificatesResult(certificateChains []collect.ParsedCertificate, outcomes []*troubleshootv1beta2.Outcome, source string) ([]*AnalyzeResult, error) {
var coll resultCollector
var passResults []*AnalyzeResult
when := ""
message := ""
for _, certChain := range certificateChains {
for _, outcome := range outcomes {
result := &AnalyzeResult{
Title: a.Title(),
}
if outcome.Fail != nil {
result.IsFail = true
when = outcome.Fail.When
message = outcome.Fail.Message
} else if outcome.Warn != nil {
result.IsWarn = true
when = outcome.Warn.When
message = outcome.Warn.Message
} else if outcome.Pass != nil {
result.IsPass = true
when = outcome.Pass.When
message = outcome.Pass.Message
} else {
return nil, errors.New("empty outcome")
}
if result.IsPass && certChain.IsValid {
result.Message = fmt.Sprintf("%s, %s", message, source)
// if the certificate is valid, we need to wait for the warning check whether the certificate is going to expire
passResults = append(passResults, result)
}
if result.IsFail && !certChain.IsValid {
result.Message = fmt.Sprintf("%s, %s", message, source)
// return the result immediately if the certificate is invalid
coll.push(result)
}
if result.IsWarn && certChain.IsValid {
warnDate, _ := regexp.Compile(`notAfter \< Today \+ (\d+) days`)
warnMatch := warnDate.FindStringSubmatch(when)
if warnMatch != nil {
warnMatchDays, err := strconv.Atoi(warnMatch[1])
if err != nil {
return nil, errors.Wrap(err, "failed to convert string to integer")
}
targetTime := time.Now().AddDate(0, 0, warnMatchDays)
if targetTime.After(certChain.NotAfter) {
result.Message = fmt.Sprintf("%s in %d days, %s", message, warnMatchDays, source)
// discard passResults if the certificate is going to expire in certain days
passResults = []*AnalyzeResult{}
coll.push(result)
}
}
}
}
// append passResults if the certificate is valid and not going to expire in certain days
for _, passResult := range passResults {
coll.push(passResult)
}
}
return coll.results, nil
}

View File

@@ -0,0 +1,221 @@
package analyzer
import (
"fmt"
"testing"
"time"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAnalyzeHostCertificatesCollection(t *testing.T) {
tests := []struct {
name string
file string
hostAnalyzer *troubleshootv1beta2.HostCertificatesCollectionAnalyze
result []*AnalyzeResult
expectErr bool
}{
{
name: "certificate-valid",
file: fmt.Sprintf(`[{
"certificatePath": "apiserver-kubelet-client.crt",
"certificateChain": [
{
"certificate": "ca.crt",
"subject": "CN=kubernetes",
"subjectAlternativeNames": [
"kubernetes"
],
"issuer": "CN=kubernetes",
"notAfter": "%s",
"notBefore": "2023-04-19T00:30:20Z",
"isValid": true,
"isCA": true
}
],
"message": "cert-valid"
}]`, time.Now().AddDate(1, 0, 0).Format("2006-01-02T15:04:05Z")),
hostAnalyzer: &troubleshootv1beta2.HostCertificatesCollectionAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "Certificate is valid",
},
},
},
},
result: []*AnalyzeResult{
{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "Host Certificates Collection",
Message: "Certificate is valid, obtained from apiserver-kubelet-client.crt",
},
},
},
{
name: "certificate-invalid",
file: `[{
"certificatePath": "apiserver-kubelet-client.crt",
"certificateChain": [
{
"certificate": "ca.crt",
"subject": "CN=kubernetes",
"subjectAlternativeNames": [
"kubernetes"
],
"issuer": "CN=kubernetes",
"notAfter": "2022-04-16T00:30:20Z",
"notBefore": "2021-04-19T00:30:20Z",
"isValid": false,
"isCA": true
}
],
"message": "cert-invalid"
}]`,
hostAnalyzer: &troubleshootv1beta2.HostCertificatesCollectionAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "notAfter < Today",
Message: "Certificate has expired",
},
},
},
},
result: []*AnalyzeResult{
{
IsPass: false,
IsWarn: false,
IsFail: true,
Title: "Host Certificates Collection",
Message: "Certificate has expired, obtained from apiserver-kubelet-client.crt",
},
},
},
{
name: "certificate-about-to-expire",
file: fmt.Sprintf(`[{
"certificatePath": "apiserver-kubelet-client.crt",
"certificateChain": [
{
"certificate": "ca.crt",
"subject": "CN=kubernetes",
"subjectAlternativeNames": [
"kubernetes"
],
"issuer": "CN=kubernetes",
"notAfter": "%s",
"notBefore": "2021-04-19T00:30:20Z",
"isValid": true,
"isCA": true
}
],
"message": "cert-valid"
}]`, time.Now().AddDate(0, 0, 5).Format("2006-01-02T15:04:05Z")),
hostAnalyzer: &troubleshootv1beta2.HostCertificatesCollectionAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "notAfter < Today + 15 days",
Message: "Certificate is about to expire",
},
},
},
},
result: []*AnalyzeResult{
{
IsPass: false,
IsWarn: true,
IsFail: false,
Title: "Host Certificates Collection",
Message: "Certificate is about to expire in 15 days, obtained from apiserver-kubelet-client.crt",
},
},
},
{
name: "certificate-missing",
file: `[{
"certificatePath": "apiserver-kubelet-client.crt",
"message": "cert-missing"
}]`,
hostAnalyzer: &troubleshootv1beta2.HostCertificatesCollectionAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{},
},
result: []*AnalyzeResult{
{
IsPass: false,
IsWarn: false,
IsFail: true,
Title: "Host Certificates Collection",
Message: "Certificate is missing, cannot be obtained from apiserver-kubelet-client.crt",
},
},
},
{
name: "certificate-valid-and-about-to-expire",
file: fmt.Sprintf(`[{
"certificatePath": "apiserver-kubelet-client.crt",
"certificateChain": [
{
"certificate": "ca.crt",
"subject": "CN=kubernetes",
"subjectAlternativeNames": [
"kubernetes"
],
"issuer": "CN=kubernetes",
"notAfter": "%s",
"notBefore": "2021-04-19T00:30:20Z",
"isValid": true,
"isCA": true
}
],
"message": "cert-valid"
}]`, time.Now().AddDate(0, 0, 5).Format("2006-01-02T15:04:05Z")),
hostAnalyzer: &troubleshootv1beta2.HostCertificatesCollectionAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "Certificate is valid",
},
Warn: &troubleshootv1beta2.SingleOutcome{
When: "notAfter < Today + 15 days",
Message: "Certificate is about to expire",
},
},
},
},
result: []*AnalyzeResult{
{
IsPass: false,
IsWarn: true,
IsFail: false,
Title: "Host Certificates Collection",
Message: "Certificate is about to expire in 15 days, obtained from apiserver-kubelet-client.crt",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
getCollectedFileContents := func(filename string) ([]byte, error) {
return []byte(test.file), nil
}
result, err := (&AnalyzeHostCertificatesCollection{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}

View File

@@ -104,6 +104,12 @@ type CertificateAnalyze struct {
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostCertificatesCollectionAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostServicesAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
@@ -149,6 +155,8 @@ type HostAnalyze struct {
Certificate *CertificateAnalyze `json:"certificate,omitempty" yaml:"certificate,omitempty"`
CertificatesCollection *HostCertificatesCollectionAnalyze `json:"certificatesCollection,omitempty" yaml:"certificatesCollection,omitempty"`
HostServices *HostServicesAnalyze `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
HostOS *HostOSAnalyze `json:"hostOS,omitempty" yaml:"hostOS,omitempty"`

View File

@@ -169,6 +169,11 @@ type Certificate struct {
KeyPath string `json:"keyPath" yaml:"keyPath"`
}
type HostCertificatesCollection struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
Paths []string `json:"paths" yaml:"paths"`
}
type HostServices struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
@@ -180,28 +185,29 @@ type HostRun struct {
}
type HostCollect struct {
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
TCPLoadBalancer *TCPLoadBalancer `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"`
HTTPLoadBalancer *HTTPLoadBalancer `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"`
TCPPortStatus *TCPPortStatus `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"`
UDPPortStatus *UDPPortStatus `json:"udpPortStatus,omitempty" yaml:"udpPortStatus,omitempty"`
Kubernetes *Kubernetes `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty"`
IPV4Interfaces *IPV4Interfaces `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"`
SubnetAvailable *SubnetAvailable `json:"subnetAvailable,omitempty" yaml:"subnetAvailable,omitempty"`
DiskUsage *DiskUsage `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"`
HTTP *HostHTTP `json:"http,omitempty" yaml:"http,omitempty"`
Time *HostTime `json:"time,omitempty" yaml:"time,omitempty"`
BlockDevices *HostBlockDevices `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"`
SystemPackages *HostSystemPackages `json:"systemPackages,omitempty" yaml:"systemPackages,omitempty"`
KernelModules *HostKernelModules `json:"kernelModules,omitempty" yaml:"kernelModules,omitempty"`
TCPConnect *TCPConnect `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"`
FilesystemPerformance *FilesystemPerformance `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"`
Certificate *Certificate `json:"certificate,omitempty" yaml:"certificate,omitempty"`
HostServices *HostServices `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
HostOS *HostOS `json:"hostOS,omitempty" yaml:"hostOS,omitempty"`
HostRun *HostRun `json:"run,omitempty" yaml:"run,omitempty"`
HostCopy *HostCopy `json:"copy,omitempty" yaml:"copy,omitempty"`
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
TCPLoadBalancer *TCPLoadBalancer `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"`
HTTPLoadBalancer *HTTPLoadBalancer `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"`
TCPPortStatus *TCPPortStatus `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"`
UDPPortStatus *UDPPortStatus `json:"udpPortStatus,omitempty" yaml:"udpPortStatus,omitempty"`
Kubernetes *Kubernetes `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty"`
IPV4Interfaces *IPV4Interfaces `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"`
SubnetAvailable *SubnetAvailable `json:"subnetAvailable,omitempty" yaml:"subnetAvailable,omitempty"`
DiskUsage *DiskUsage `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"`
HTTP *HostHTTP `json:"http,omitempty" yaml:"http,omitempty"`
Time *HostTime `json:"time,omitempty" yaml:"time,omitempty"`
BlockDevices *HostBlockDevices `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"`
SystemPackages *HostSystemPackages `json:"systemPackages,omitempty" yaml:"systemPackages,omitempty"`
KernelModules *HostKernelModules `json:"kernelModules,omitempty" yaml:"kernelModules,omitempty"`
TCPConnect *TCPConnect `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"`
FilesystemPerformance *FilesystemPerformance `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"`
Certificate *Certificate `json:"certificate,omitempty" yaml:"certificate,omitempty"`
CertificatesCollection *HostCertificatesCollection `json:"certificatesCollection,omitempty" yaml:"certificatesCollection,omitempty"`
HostServices *HostServices `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
HostOS *HostOS `json:"hostOS,omitempty" yaml:"hostOS,omitempty"`
HostRun *HostRun `json:"run,omitempty" yaml:"run,omitempty"`
HostCopy *HostCopy `json:"copy,omitempty" yaml:"copy,omitempty"`
}
func (c *HostCollect) GetName() string {

View File

@@ -1687,6 +1687,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*out = new(CertificateAnalyze)
(*in).DeepCopyInto(*out)
}
if in.CertificatesCollection != nil {
in, out := &in.CertificatesCollection, &out.CertificatesCollection
*out = new(HostCertificatesCollectionAnalyze)
(*in).DeepCopyInto(*out)
}
if in.HostServices != nil {
in, out := &in.HostServices, &out.HostServices
*out = new(HostServicesAnalyze)
@@ -1725,6 +1730,54 @@ func (in *HostBlockDevices) DeepCopy() *HostBlockDevices {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostCertificatesCollection) DeepCopyInto(out *HostCertificatesCollection) {
*out = *in
in.HostCollectorMeta.DeepCopyInto(&out.HostCollectorMeta)
if in.Paths != nil {
in, out := &in.Paths, &out.Paths
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCertificatesCollection.
func (in *HostCertificatesCollection) DeepCopy() *HostCertificatesCollection {
if in == nil {
return nil
}
out := new(HostCertificatesCollection)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostCertificatesCollectionAnalyze) DeepCopyInto(out *HostCertificatesCollectionAnalyze) {
*out = *in
in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta)
if in.Outcomes != nil {
in, out := &in.Outcomes, &out.Outcomes
*out = make([]*Outcome, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Outcome)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCertificatesCollectionAnalyze.
func (in *HostCertificatesCollectionAnalyze) DeepCopy() *HostCertificatesCollectionAnalyze {
if in == nil {
return nil
}
out := new(HostCertificatesCollectionAnalyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = *in
@@ -1818,6 +1871,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = new(Certificate)
(*in).DeepCopyInto(*out)
}
if in.CertificatesCollection != nil {
in, out := &in.CertificatesCollection, &out.CertificatesCollection
*out = new(HostCertificatesCollection)
(*in).DeepCopyInto(*out)
}
if in.HostServices != nil {
in, out := &in.HostServices, &out.HostServices
*out = new(HostServices)

View File

@@ -3,10 +3,8 @@ package collect
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"time"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
@@ -194,27 +192,6 @@ func secretCertCollector(secretName string, namespace string, client kubernetes.
return results
}
// decode pem and validate data source contains
func decodePem(certPEMBlock []byte) (tls.Certificate, string) {
var cert tls.Certificate
var trackErrors string
var certDERBlock *pem.Block
for {
certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
if certDERBlock == nil {
break
}
if certDERBlock.Type == "CERTIFICATE" {
cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
}
}
if len(cert.Certificate) == 0 {
trackErrors = "No certificates found in"
}
return cert, trackErrors
}
// Certificate parser
func CertParser(certName string, certs []byte, currentTime time.Time) ([]ParsedCertificate, []string) {
certInfo := []ParsedCertificate{}

View File

@@ -0,0 +1,106 @@
package collect
import (
"bytes"
"crypto/x509"
"encoding/json"
"io/ioutil"
"path/filepath"
"time"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
const CertMissing = "cert-missing"
const CertValid = "cert-valid"
const CertInvalid = "cert-invalid"
type CollectHostCertificatesCollection struct {
hostCollector *troubleshootv1beta2.HostCertificatesCollection
BundlePath string
}
type HostCertificatesCollection struct {
CertificatePath string `json:"certificatePath,omitempty"`
CertificateChain []ParsedCertificate `json:"certificateChain,omitempty"`
Message string `json:"message,omitempty"`
}
func (c *CollectHostCertificatesCollection) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Host Certificates Collection")
}
func (c *CollectHostCertificatesCollection) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostCertificatesCollection) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
var results []HostCertificatesCollection
for _, certPath := range c.hostCollector.Paths {
results = append(results, HostCertsParser(certPath))
}
resultsJson, errResultJson := json.MarshalIndent(results, "", "\t")
if errResultJson != nil {
return nil, errResultJson
}
collectorName := c.hostCollector.CollectorName
if collectorName == "" {
collectorName = "certificatesCollection"
}
name := filepath.Join("host-collectors/certificatesCollection", collectorName+".json")
output := NewResult()
output.SaveResult(c.BundlePath, name, bytes.NewBuffer(resultsJson))
return output, nil
}
func HostCertsParser(certPath string) HostCertificatesCollection {
var certInfo []ParsedCertificate
cert, err := ioutil.ReadFile(certPath)
if err != nil {
return HostCertificatesCollection{
CertificatePath: certPath,
Message: CertMissing,
}
}
certChain, _ := decodePem(cert)
if len(certChain.Certificate) == 0 {
return HostCertificatesCollection{
CertificatePath: certPath,
Message: CertInvalid,
}
}
for _, cert := range certChain.Certificate {
parsedCert, errParse := x509.ParseCertificate(cert)
if errParse != nil {
return HostCertificatesCollection{
CertificatePath: certPath,
Message: CertInvalid,
}
}
currentTime := time.Now()
certInfo = append(certInfo, ParsedCertificate{
Subject: parsedCert.Subject.ToRDNSequence().String(),
SubjectAlternativeNames: parsedCert.DNSNames,
Issuer: parsedCert.Issuer.ToRDNSequence().String(),
NotAfter: parsedCert.NotAfter,
NotBefore: parsedCert.NotBefore,
IsValid: currentTime.Before(parsedCert.NotAfter) && currentTime.After(parsedCert.NotBefore),
IsCA: parsedCert.IsCA,
})
}
return HostCertificatesCollection{
CertificatePath: certPath,
CertificateChain: certInfo,
Message: CertValid,
}
}

View File

@@ -0,0 +1,82 @@
package collect
import (
"path/filepath"
"testing"
"time"
"github.com/replicatedhq/troubleshoot/internal/testutils"
"github.com/stretchr/testify/assert"
)
func Test_HostCertParser(t *testing.T) {
path := filepath.Join(t.TempDir(), "test.txt")
tests := []struct {
name string
filePath, certChain string
want HostCertificatesCollection
}{
{
name: "valid certificate",
filePath: path,
certChain: certChains["validCert"],
want: HostCertificatesCollection{
CertificatePath: path,
CertificateChain: []ParsedCertificate{
{
Subject: "CN=envoy",
SubjectAlternativeNames: []string{
"envoy",
"envoy.projectcontour",
"envoy.projectcontour.svc",
"envoy.projectcontour.svc.cluster.local",
},
Issuer: "SERIALNUMBER=615929891,CN=Project Contour",
NotAfter: time.Date(2024, time.February, 25, 4, 27, 16, 0, time.UTC),
NotBefore: time.Date(2023, time.February, 24, 4, 27, 18, 0, time.UTC),
IsValid: true,
IsCA: false,
},
},
Message: "cert-valid",
},
},
{
name: "expired certificate",
filePath: path,
certChain: certChains["expiredCert"],
want: HostCertificatesCollection{
CertificatePath: path,
CertificateChain: []ParsedCertificate{
{
Subject: "O=Internet Widgits Pty Ltd,ST=Some-State,C=AU",
SubjectAlternativeNames: nil,
Issuer: "O=Internet Widgits Pty Ltd,ST=Some-State,C=AU",
NotAfter: time.Date(2015, time.September, 12, 21, 52, 2, 0, time.UTC),
NotBefore: time.Date(2012, time.September, 12, 21, 52, 2, 0, time.UTC),
IsValid: false,
IsCA: true,
},
},
Message: "cert-valid",
},
},
{
name: "missing certificate",
filePath: "",
certChain: "",
want: HostCertificatesCollection{
CertificatePath: "",
Message: "cert-missing",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testutils.CreateTestFileWithData(t, path, tt.certChain)
got := HostCertsParser(tt.filePath)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -51,6 +51,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str
return &CollectHostFilesystemPerformance{collector.FilesystemPerformance, bundlePath}, true
case collector.Certificate != nil:
return &CollectHostCertificate{collector.Certificate, bundlePath}, true
case collector.CertificatesCollection != nil:
return &CollectHostCertificatesCollection{collector.CertificatesCollection, bundlePath}, true
case collector.HostServices != nil:
return &CollectHostServices{collector.HostServices, bundlePath}, true
case collector.HostOS != nil:

View File

@@ -6,6 +6,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"reflect"
@@ -236,3 +237,24 @@ func checkForExistingServiceAccount(client kubernetes.Interface, namespace strin
}
return nil
}
// decode pem and validate certificate data source contains
func decodePem(certPEMBlock []byte) (tls.Certificate, string) {
var cert tls.Certificate
var trackErrors string
var certDERBlock *pem.Block
for {
certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
if certDERBlock == nil {
break
}
if certDERBlock.Type == "CERTIFICATE" {
cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
}
}
if len(cert.Certificate) == 0 {
trackErrors = "No certificates found in"
}
return cert, trackErrors
}

View File

@@ -2535,6 +2535,82 @@
}
}
},
"certificatesCollection": {
"type": "object",
"required": [
"outcomes"
],
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"checkName": {
"type": "string"
},
"collectorName": {
"type": "string"
},
"exclude": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
},
"outcomes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"fail": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"uri": {
"type": "string"
},
"when": {
"type": "string"
}
}
},
"pass": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"uri": {
"type": "string"
},
"when": {
"type": "string"
}
}
},
"warn": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"uri": {
"type": "string"
},
"when": {
"type": "string"
}
}
}
}
}
},
"strict": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
}
}
},
"cpu": {
"type": "object",
"required": [

View File

@@ -9588,6 +9588,82 @@
}
}
},
"certificatesCollection": {
"type": "object",
"required": [
"outcomes"
],
"properties": {
"annotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"checkName": {
"type": "string"
},
"collectorName": {
"type": "string"
},
"exclude": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
},
"outcomes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"fail": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"uri": {
"type": "string"
},
"when": {
"type": "string"
}
}
},
"pass": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"uri": {
"type": "string"
},
"when": {
"type": "string"
}
}
},
"warn": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"uri": {
"type": "string"
},
"when": {
"type": "string"
}
}
}
}
}
},
"strict": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
}
}
},
"cpu": {
"type": "object",
"required": [
@@ -10920,6 +10996,26 @@
}
}
},
"certificatesCollection": {
"type": "object",
"required": [
"paths"
],
"properties": {
"collectorName": {
"type": "string"
},
"exclude": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
},
"paths": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"copy": {
"type": "object",
"required": [