Repository: replicatedhq/troubleshoot
Pull request #1107 (2023-04-11 00:04:00)

chore(deps): bump golang.org/x/text from 0.8.0 to 0.9.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.8.0 to 0.9.0.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golang/text/commit/48e4a4a957429d31328a685863b594ca9a06b552"><code>48e4a4a</code></a> all: fix some comments</li>
<li>See full diff in <a href="https://github.com/golang/text/compare/v0.8.0...v0.9.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/text&package-manager=go_modules&previous-version=0.8.0&new-version=0.9.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/config/crds/troubleshoot.sh_collectors.yaml b/config/crds/troubleshoot.sh_collectors.yaml
index 56426279..7cf96242 100644
--- a/config/crds/troubleshoot.sh_collectors.yaml
+++ b/config/crds/troubleshoot.sh_collectors.yaml
@@ -82,6 +82,27 @@ spec:
                       required:
                       - namespace
                       type: object
+                    certificates:
+                      properties:
+                        collectorName:
+                          type: string
+                        configMaps:
+                          additionalProperties:
+                            items:
+                              type: string
+                            type: array
+                          type: object
+                        exclude:
+                          type: BoolString
+                        name:
+                          type: string
+                        secrets:
+                          additionalProperties:
+                            items:
+                              type: string
+                            type: array
+                          type: object
+                      type: object
                     clusterInfo:
                       properties:
                         collectorName:
diff --git a/config/crds/troubleshoot.sh_preflights.yaml b/config/crds/troubleshoot.sh_preflights.yaml
index 9c4bc949..7e697839 100644
--- a/config/crds/troubleshoot.sh_preflights.yaml
+++ b/config/crds/troubleshoot.sh_preflights.yaml
@@ -1526,6 +1526,27 @@ spec:
                       required:
                       - namespace
                       type: object
+                    certificates:
+                      properties:
+                        collectorName:
+                          type: string
+                        configMaps:
+                          additionalProperties:
+                            items:
+                              type: string
+                            type: array
+                          type: object
+                        exclude:
+                          type: BoolString
+                        name:
+                          type: string
+                        secrets:
+                          additionalProperties:
+                            items:
+                              type: string
+                            type: array
+                          type: object
+                      type: object
                     clusterInfo:
                       properties:
                         collectorName:
diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml
index 43b0a29e..d8a9a2b6 100644
--- a/config/crds/troubleshoot.sh_supportbundles.yaml
+++ b/config/crds/troubleshoot.sh_supportbundles.yaml
@@ -1557,6 +1557,27 @@ spec:
                       required:
                       - namespace
                       type: object
+                    certificates:
+                      properties:
+                        collectorName:
+                          type: string
+                        configMaps:
+                          additionalProperties:
+                            items:
+                              type: string
+                            type: array
+                          type: object
+                        exclude:
+                          type: BoolString
+                        name:
+                          type: string
+                        secrets:
+                          additionalProperties:
+                            items:
+                              type: string
+                            type: array
+                          type: object
+                      type: object
                     clusterInfo:
                       properties:
                         collectorName:
diff --git a/go.mod b/go.mod
index 4d92acd4..02453f0b 100644
--- a/go.mod
+++ b/go.mod
@@ -31,7 +31,7 @@ require (
 	github.com/segmentio/ksuid v1.0.4
 	github.com/shirou/gopsutil/v3 v3.23.3
 	github.com/sirupsen/logrus v1.9.0
-	github.com/spf13/cobra v1.6.1
+	github.com/spf13/cobra v1.7.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/viper v1.15.0
 	github.com/stretchr/testify v1.8.2
@@ -134,7 +134,7 @@ require (
 	github.com/hashicorp/go-version v1.6.0
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/imdario/mergo v0.3.13 // indirect
-	github.com/inconshreveable/mousetrap v1.0.1 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -196,7 +196,7 @@ require (
 	golang.org/x/oauth2 v0.5.0 // indirect
 	golang.org/x/sys v0.6.0 // indirect
 	golang.org/x/term v0.6.0 // indirect
-	golang.org/x/text v0.8.0
+	golang.org/x/text v0.9.0
 	golang.org/x/time v0.3.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/api v0.110.0 // indirect
diff --git a/go.sum b/go.sum
index f5899daf..4b10b30c 100644
--- a/go.sum
+++ b/go.sum
@@ -545,8 +545,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
 github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
-github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
-github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
@@ -785,8 +785,8 @@ github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
 github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
-github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -1138,8 +1138,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
-golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/pkg/analyze/host_tcp_connect.go b/pkg/analyze/host_tcp_connect.go
index 1ebfc1fc..ef16681b 100644
--- a/pkg/analyze/host_tcp_connect.go
+++ b/pkg/analyze/host_tcp_connect.go
@@ -40,9 +40,9 @@ func (a *AnalyzeHostTCPConnect) Analyze(getCollectedFileContents func(string) ([
 	}
 
 	var coll resultCollector
+	result := &AnalyzeResult{Title: a.Title()}
 
 	for _, outcome := range hostAnalyzer.Outcomes {
-		result := &AnalyzeResult{Title: a.Title()}
 
 		if outcome.Fail != nil {
 			if outcome.Fail.When == "" {
@@ -51,7 +51,7 @@ func (a *AnalyzeHostTCPConnect) Analyze(getCollectedFileContents func(string) ([
 				result.URI = outcome.Fail.URI
 
 				coll.push(result)
-
+				break
 			}
 
 			if string(actual.Status) == outcome.Fail.When {
@@ -60,6 +60,7 @@ func (a *AnalyzeHostTCPConnect) Analyze(getCollectedFileContents func(string) ([
 				result.URI = outcome.Fail.URI
 
 				coll.push(result)
+				break
 			}
 		} else if outcome.Warn != nil {
 			if outcome.Warn.When == "" {
@@ -68,7 +69,7 @@ func (a *AnalyzeHostTCPConnect) Analyze(getCollectedFileContents func(string) ([
 				result.URI = outcome.Warn.URI
 
 				coll.push(result)
-
+				break
 			}
 
 			if string(actual.Status) == outcome.Warn.When {
@@ -77,6 +78,7 @@ func (a *AnalyzeHostTCPConnect) Analyze(getCollectedFileContents func(string) ([
 				result.URI = outcome.Warn.URI
 
 				coll.push(result)
+				break
 			}
 		} else if outcome.Pass != nil {
 			if outcome.Pass.When == "" {
@@ -85,7 +87,7 @@ func (a *AnalyzeHostTCPConnect) Analyze(getCollectedFileContents func(string) ([
 				result.URI = outcome.Pass.URI
 
 				coll.push(result)
-
+				break
 			}
 
 			if string(actual.Status) == outcome.Pass.When {
@@ -94,6 +96,7 @@ func (a *AnalyzeHostTCPConnect) Analyze(getCollectedFileContents func(string) ([
 				result.URI = outcome.Pass.URI
 
 				coll.push(result)
+				break
 			}
 		}
 	}
diff --git a/pkg/apis/troubleshoot/v1beta2/collector_shared.go b/pkg/apis/troubleshoot/v1beta2/collector_shared.go
index 70f4387b..506c0595 100644
--- a/pkg/apis/troubleshoot/v1beta2/collector_shared.go
+++ b/pkg/apis/troubleshoot/v1beta2/collector_shared.go
@@ -216,6 +216,13 @@ type RegistryImages struct {
 	ImagePullSecrets *ImagePullSecrets `json:"imagePullSecret,omitempty" yaml:"imagePullSecret,omitempty"`
 }
 
+type Certificates struct {
+	CollectorMeta `json:",inline" yaml:",inline"`
+	Name          string              `json:"name,omitempty" yaml:"name,omitempty"`
+	Secrets       map[string][]string `json:"secrets,omitempty" yaml:"secrets,omitempty"`
+	ConfigMaps    map[string][]string `json:"configMaps,omitempty" yaml:"configMaps,omitempty"`
+}
+
 type Collect struct {
 	ClusterInfo      *ClusterInfo      `json:"clusterInfo,omitempty" yaml:"clusterInfo,omitempty"`
 	ClusterResources *ClusterResources `json:"clusterResources,omitempty" yaml:"clusterResources,omitempty"`
@@ -238,6 +245,7 @@ type Collect struct {
 	Longhorn         *Longhorn         `json:"longhorn,omitempty" yaml:"longhorn,omitempty"`
 	RegistryImages   *RegistryImages   `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
 	Sysctl           *Sysctl           `json:"sysctl,omitempty" yaml:"sysctl,omitempty"`
+	Certificates     *Certificates     `json:"certificates,omitempty" yaml:"certificates,omitempty"`
 }
 
 func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSubjectAccessReviewSpec {
@@ -539,6 +547,10 @@ func (c *Collect) GetName() string {
 		collector = "sysctl"
 		name = c.Sysctl.Name
 	}
+	if c.Certificates != nil {
+		collector = "certificates"
+		name = c.Certificates.CollectorName
+	}
 
 	if collector == "" {
 		return "<none>"
diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
index 48f4dd5d..7a6068ff 100644
--- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
+++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go
@@ -559,6 +559,52 @@ func (in *CertificateAnalyze) DeepCopy() *CertificateAnalyze {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Certificates) DeepCopyInto(out *Certificates) {
+	*out = *in
+	in.CollectorMeta.DeepCopyInto(&out.CollectorMeta)
+	if in.Secrets != nil {
+		in, out := &in.Secrets, &out.Secrets
+		*out = make(map[string][]string, len(*in))
+		for key, val := range *in {
+			var outVal []string
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = make([]string, len(*in))
+				copy(*out, *in)
+			}
+			(*out)[key] = outVal
+		}
+	}
+	if in.ConfigMaps != nil {
+		in, out := &in.ConfigMaps, &out.ConfigMaps
+		*out = make(map[string][]string, len(*in))
+		for key, val := range *in {
+			var outVal []string
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = make([]string, len(*in))
+				copy(*out, *in)
+			}
+			(*out)[key] = outVal
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificates.
+func (in *Certificates) DeepCopy() *Certificates {
+	if in == nil {
+		return nil
+	}
+	out := new(Certificates)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterInfo) DeepCopyInto(out *ClusterInfo) {
 	*out = *in
@@ -790,6 +836,11 @@ func (in *Collect) DeepCopyInto(out *Collect) {
 		*out = new(Sysctl)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Certificates != nil {
+		in, out := &in.Certificates, &out.Certificates
+		*out = new(Certificates)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collect.
diff --git a/pkg/collect/certificates.go b/pkg/collect/certificates.go
new file mode 100644
index 00000000..86776bba
--- /dev/null
+++ b/pkg/collect/certificates.go
@@ -0,0 +1,254 @@
+package collect
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"encoding/pem"
+	"time"
+
+	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+)
+
+type CollectCertificates struct {
+	Collector    *troubleshootv1beta2.Certificates
+	BundlePath   string
+	Namespace    string
+	ClientConfig *rest.Config
+	Client       kubernetes.Interface
+	Context      context.Context
+	RBACErrors
+}
+
+// Certificate collection struct
+type CertCollection struct {
+	Source           *CertificateSource  `json:"source"`
+	Errors           []string            `json:"errors"`
+	CertificateChain []ParsedCertificate `json:"certificateChain"`
+}
+
+// Certificate source
+type CertificateSource struct {
+	SecretName    string `json:"secret,omitempty"`
+	ConfigMapName string `json:"configMap,omitempty"`
+	Namespace     string `json:"namespace,omitempty"`
+}
+
+// Certificate Struct
+type ParsedCertificate struct {
+	CertName                string    `json:"certificate"`
+	Subject                 string    `json:"subject"`
+	SubjectAlternativeNames []string  `json:"subjectAlternativeNames"`
+	Issuer                  string    `json:"issuer"`
+	NotAfter                time.Time `json:"notAfter"`
+	NotBefore               time.Time `json:"notBefore"`
+	IsValid                 bool      `json:"isValid"`
+	IsCA                    bool      `json:"isCA"`
+}
+
+func (c *CollectCertificates) Title() string {
+	return getCollectorName(c)
+}
+
+func (c *CollectCertificates) IsExcluded() (bool, error) {
+	return isExcluded(c.Collector.Exclude)
+}
+
+func (c *CollectCertificates) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
+
+	output := NewResult()
+	results := []CertCollection{}
+
+	// collect certificates from secrets
+	for secretName, namespaces := range c.Collector.Secrets {
+		for _, namespace := range namespaces {
+
+			secretCollections := secretCertCollector(secretName, namespace, c.Client)
+			results = append(results, secretCollections...)
+		}
+	}
+
+	// collect certificates from configMaps
+	for configMapName, namespaces := range c.Collector.ConfigMaps {
+		for _, namespace := range namespaces {
+			configMapCollections := configMapCertCollector(configMapName, namespace, c.Client)
+			results = append(results, configMapCollections...)
+		}
+	}
+
+	certsJson, errCertJson := json.MarshalIndent(results, "", "\t")
+	if errCertJson != nil {
+		return nil, errCertJson
+	}
+
+	filePath := "certificates/certificates.json"
+
+	err := output.SaveResult(c.BundlePath, filePath, bytes.NewBuffer(certsJson))
+	if err != nil {
+		return nil, err
+	}
+
+	return output, nil
+}
+
+// configmap certificate collector
+func configMapCertCollector(configMapName string, namespace string, client kubernetes.Interface) []CertCollection {
+
+	results := []CertCollection{}
+	trackErrors := []string{}
+	collection := []ParsedCertificate{}
+
+	getOptions := metav1.GetOptions{}
+
+	// Collect from configMaps
+	configMap, err := client.CoreV1().ConfigMaps(namespace).Get(context.Background(), configMapName, getOptions)
+	if err != nil {
+
+		// collect certificate source information
+		source := &CertificateSource{
+			SecretName: configMapName,
+			Namespace:  namespace,
+		}
+		trackErrors = append(trackErrors, "Either the configMap does not exist in this namespace or RBAC permissions are preventing certificate collection")
+
+		results = append(results, CertCollection{
+			Source:           source,
+			Errors:           trackErrors,
+			CertificateChain: collection,
+		})
+
+		return results
+
+	}
+
+	//Collect from configMap
+	source := &CertificateSource{
+		ConfigMapName: configMap.Name,
+		Namespace:     configMap.Namespace,
+	}
+	for certName, c := range configMap.Data {
+
+		certs := []byte(c)
+
+		certInfo, _ := CertParser(certName, certs)
+
+		collection = append(collection, certInfo...)
+	}
+	results = append(results, CertCollection{
+		Source:           source,
+		Errors:           trackErrors,
+		CertificateChain: collection,
+	})
+
+	return results
+}
+
+// secret certificate collector
+func secretCertCollector(secretName string, namespace string, client kubernetes.Interface) []CertCollection {
+
+	results := []CertCollection{}
+	trackErrors := []string{}
+	collection := []ParsedCertificate{}
+
+	getOptions := metav1.GetOptions{}
+	// Collect from secrets
+	secret, err := client.CoreV1().Secrets(namespace).Get(context.Background(), secretName, getOptions)
+	if err != nil {
+
+		// collect certificate source information
+		source := &CertificateSource{
+			SecretName: secretName,
+			Namespace:  namespace,
+		}
+		trackErrors = append(trackErrors, "Either the secret does not exist in this namespace or RBAC permissions are prenventing certificate collection")
+
+		results = append(results, CertCollection{
+			Source:           source,
+			Errors:           trackErrors,
+			CertificateChain: collection,
+		})
+
+		return results
+
+	}
+
+	// Collect from secret
+	source := &CertificateSource{
+		SecretName: secret.Name,
+		Namespace:  secret.Namespace,
+	}
+
+	for certName, certs := range secret.Data {
+		certInfo, _ := CertParser(certName, certs)
+
+		collection = append(collection, certInfo...)
+
+	}
+	results = append(results, CertCollection{
+		Source:           source,
+		Errors:           trackErrors,
+		CertificateChain: collection,
+	})
+
+	return results
+}
+
+// decode pem and validate data source contains
+func decodePem(certInput string) (tls.Certificate, []string) {
+	var cert tls.Certificate
+	trackErrors := []string{}
+	certPEMBlock := []byte(certInput)
+	var certDERBlock *pem.Block
+	for {
+		certDERBlock, certPEMBlock = pem.Decode(certPEMBlock)
+		if certDERBlock == nil {
+			trackErrors = append(trackErrors, "decodePem function error: cert block is empty")
+
+			break
+		}
+		if certDERBlock.Type == "CERTIFICATE" {
+			cert.Certificate = append(cert.Certificate, certDERBlock.Bytes)
+		}
+	}
+	return cert, trackErrors
+}
+
+// Certificate parser
+func CertParser(certName string, certs []byte) ([]ParsedCertificate, []string) {
+	//TODO: return trackErrors as well.
+	currentTime := time.Now()
+	data := string(certs)
+	certInfo := []ParsedCertificate{}
+	trackErrors := []string{}
+
+	certChain, decodePemTrackErrors := decodePem(data)
+
+	trackErrors = append(trackErrors, decodePemTrackErrors...)
+
+	for _, cert := range certChain.Certificate {
+
+		//parsed SSL certificate
+		parsedCert, errParse := x509.ParseCertificate(cert)
+		if errParse != nil {
+			trackErrors = append(trackErrors, errParse.Error())
+			continue
+		}
+
+		certInfo = append(certInfo, ParsedCertificate{
+			CertName:                certName,
+			Subject:                 parsedCert.Subject.ToRDNSequence().String(),
+			SubjectAlternativeNames: parsedCert.DNSNames,
+			Issuer:                  parsedCert.Issuer.ToRDNSequence().String(),
+			NotAfter:                parsedCert.NotAfter,
+			NotBefore:               parsedCert.NotBefore,
+			IsValid:                 currentTime.Before(parsedCert.NotAfter),
+			IsCA:                    parsedCert.IsCA,
+		})
+	}
+	return certInfo, trackErrors
+}
diff --git a/pkg/collect/collector.go b/pkg/collect/collector.go
index 72ffde1c..90d6c08c 100644
--- a/pkg/collect/collector.go
+++ b/pkg/collect/collector.go
@@ -101,6 +101,8 @@ func GetCollector(collector *troubleshootv1beta2.Collect, bundlePath string, nam
 		return &CollectRegistry{collector.RegistryImages, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
 	case collector.Sysctl != nil:
 		return &CollectSysctl{collector.Sysctl, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
+	case collector.Certificates != nil:
+		return &CollectCertificates{collector.Certificates, bundlePath, namespace, clientConfig, client, ctx, RBACErrors}, true
 	default:
 		return nil, false
 	}
@@ -176,6 +178,9 @@ func getCollectorName(c interface{}) string {
 	case *CollectSysctl:
 		collector = "sysctl"
 		name = v.Collector.Name
+	case *CollectCertificates:
+		collector = "certificates"
+		name = v.Collector.Name
 	default:
 		collector = "<none>"
 	}
diff --git a/schemas/collector-troubleshoot-v1beta2.json b/schemas/collector-troubleshoot-v1beta2.json
index e0c8b7fb..69975137 100644
--- a/schemas/collector-troubleshoot-v1beta2.json
+++ b/schemas/collector-troubleshoot-v1beta2.json
@@ -88,6 +88,38 @@
                   }
                 }
               },
+              "certificates": {
+                "type": "object",
+                "properties": {
+                  "collectorName": {
+                    "type": "string"
+                  },
+                  "configMaps": {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "array",
+                      "items": {
+                        "type": "string"
+                      }
+                    }
+                  },
+                  "exclude": {
+                    "oneOf": [{"type": "string"},{"type": "boolean"}]
+                  },
+                  "name": {
+                    "type": "string"
+                  },
+                  "secrets": {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "array",
+                      "items": {
+                        "type": "string"
+                      }
+                    }
+                  }
+                }
+              },
               "clusterInfo": {
                 "type": "object",
                 "properties": {
diff --git a/schemas/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json
index 8a9f3ffa..162e38f5 100644
--- a/schemas/preflight-troubleshoot-v1beta2.json
+++ b/schemas/preflight-troubleshoot-v1beta2.json
@@ -2312,6 +2312,38 @@
                   }
                 }
               },
+              "certificates": {
+                "type": "object",
+                "properties": {
+                  "collectorName": {
+                    "type": "string"
+                  },
+                  "configMaps": {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "array",
+                      "items": {
+                        "type": "string"
+                      }
+                    }
+                  },
+                  "exclude": {
+                    "oneOf": [{"type": "string"},{"type": "boolean"}]
+                  },
+                  "name": {
+                    "type": "string"
+                  },
+                  "secrets": {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "array",
+                      "items": {
+                        "type": "string"
+                      }
+                    }
+                  }
+                }
+              },
               "clusterInfo": {
                 "type": "object",
                 "properties": {
diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json
index 170b19df..d646288e 100644
--- a/schemas/supportbundle-troubleshoot-v1beta2.json
+++ b/schemas/supportbundle-troubleshoot-v1beta2.json
@@ -2358,6 +2358,38 @@
                   }
                 }
               },
+              "certificates": {
+                "type": "object",
+                "properties": {
+                  "collectorName": {
+                    "type": "string"
+                  },
+                  "configMaps": {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "array",
+                      "items": {
+                        "type": "string"
+                      }
+                    }
+                  },
+                  "exclude": {
+                    "oneOf": [{"type": "string"},{"type": "boolean"}]
+                  },
+                  "name": {
+                    "type": "string"
+                  },
+                  "secrets": {
+                    "type": "object",
+                    "additionalProperties": {
+                      "type": "array",
+                      "items": {
+                        "type": "string"
+                      }
+                    }
+                  }
+                }
+              },
               "clusterInfo": {
                 "type": "object",
                 "properties": {


Test patch:
diff --git a/.github/workflows/build-test-deploy.yaml b/.github/workflows/build-test-deploy.yaml
index a9f0b9c9..403b1821 100644
--- a/.github/workflows/build-test-deploy.yaml
+++ b/.github/workflows/build-test-deploy.yaml
@@ -75,6 +75,8 @@ jobs:
           echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
         shell: bash
       - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
       - run: make check-schemas
 
   compile-preflight:
diff --git a/pkg/analyze/host_tcp_connect_test.go b/pkg/analyze/host_tcp_connect_test.go
index df30a3bb..08897016 100644
--- a/pkg/analyze/host_tcp_connect_test.go
+++ b/pkg/analyze/host_tcp_connect_test.go
@@ -31,6 +31,11 @@ func TestAnalyzeTCPConnect(t *testing.T) {
 							Message: "Connection was refused",
 						},
 					},
+					{
+						Warn: &troubleshootv1beta2.SingleOutcome{
+							Message: "Unexpected TCP connection status",
+						},
+					},
 				},
 			},
 			result: []*AnalyzeResult{
@@ -60,6 +65,11 @@ func TestAnalyzeTCPConnect(t *testing.T) {
 							Message: "Connection was successful",
 						},
 					},
+					{
+						Warn: &troubleshootv1beta2.SingleOutcome{
+							Message: "Unexpected TCP connection status",
+						},
+					},
 				},
 			},
 			result: []*AnalyzeResult{
diff --git a/pkg/collect/certificates_test.go b/pkg/collect/certificates_test.go
new file mode 100644
index 00000000..ccaa5a04
--- /dev/null
+++ b/pkg/collect/certificates_test.go
@@ -0,0 +1,222 @@
+package collect
+
+import (
+	"strings"
+	"testing"
+)
+
+var chain = `-----BEGIN CERTIFICATE-----
+MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ
+hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa
+rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv
+zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF
+MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW
+r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V
+-----END CERTIFICATE-----
+`
+
+var chain2 = `-----BEGIN CERTIFICATE-----
+MIIG5jCCBc6gAwIBAgIQAze5KDR8YKauxa2xIX84YDANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTA3MTEwOTEyMDAwMFoXDTIxMTExMDAwMDAwMFowaTEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTEoMCYGA1UEAxMfRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
+RVYgQ0EtMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPOWYth1bhn/
+PzR8SU8xfg0ETpmB4rOFVZEwscCvcLssqOcYqj9495BoUoYBiJfiOwZlkKq9ZXbC
+7L4QWzd4g2B1Rca9dKq2n6Q6AVAXxDlpufFP74LByvNK28yeUE9NQKM6kOeGZrzw
+PnYoTNF1gJ5qNRQ1A57bDIzCKK1Qss72kaPDpQpYSfZ1RGy6+c7pqzoC4E3zrOJ6
+4GAiBTyC01Li85xH+DvYskuTVkq/cKs+6WjIHY9YHSpNXic9rQpZL1oRIEDZaARo
+LfTAhAsKG3jf7RpY3PtBWm1r8u0c7lwytlzs16YDMqbo3rcoJ1mIgP97rYlY1R4U
+pPKwcNSgPqcCAwEAAaOCA4UwggOBMA4GA1UdDwEB/wQEAwIBhjA7BgNVHSUENDAy
+BggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUH
+AwgwggHEBgNVHSAEggG7MIIBtzCCAbMGCWCGSAGG/WwCATCCAaQwOgYIKwYBBQUH
+AgEWLmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5o
+dG0wggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0
+AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1
+AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABp
+AGcAaQBDAGUAcgB0ACAARQBWACAAQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBl
+AGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBo
+AGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAg
+AGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAg
+AGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wEgYDVR0TAQH/BAgwBgEB/wIBADCB
+gwYIKwYBBQUHAQEEdzB1MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy
+dC5jb20wTQYIKwYBBQUHMAKGQWh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NBQ2Vy
+dHMvRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3J0MIGPBgNVHR8EgYcw
+gYQwQKA+oDyGOmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hB
+c3N1cmFuY2VFVlJvb3RDQS5jcmwwQKA+oDyGOmh0dHA6Ly9jcmw0LmRpZ2ljZXJ0
+LmNvbS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwHQYDVR0OBBYE
+FExYyyXwQU9S9CjIgUObpqig5pLlMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSYJhoI
+Au9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQBMeheHKF0XvLIyc7/NLvVYMR3wsXFU
+nNabZ5PbLwM+Fm8eA8lThKNWYB54lBuiqG+jpItSkdfdXJW777UWSemlQk808kf/
+roF/E1S3IMRwFcuBCoHLdFfcnN8kpCkMGPAc5K4HM+zxST5Vz25PDVR708noFUjU
+xbvcNRx3RQdIRYW9135TuMAW2ZXNi419yWBP0aKb49Aw1rRzNubS+QOy46T15bg+
+BEkAui6mSnKDcp33C4ypieez12Qf1uNgywPE3IjpnSUBAHHLA7QpYCWP+UbRe3Gu
+zVMSW4SOwg/H7ZMZ2cn6j1g0djIvruFQFGHUqFijyDATI+/GJYw2jxyA
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
+RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
+PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
+xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
+Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
+hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
+EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
+FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
+nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
+eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
+hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
+Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
+vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
++OkuE6N36B9K
+-----END CERTIFICATE-----`
+
+var chain3 = `-----BEGIN CERTIFICATE-----
+MIIDejCCAmKgAwIBAgIEAZaq0DANBgkqhkiG9w0BAQsFADAuMRgwFgYDVQQDEw9Q
+cm9qZWN0IENvbnRvdXIxEjAQBgNVBAUTCTYxNTkyOTg5MTAeFw0yMzAyMjQwNDI3
+MThaFw0yNDAyMjUwNDI3MTZaMBAxDjAMBgNVBAMTBWVudm95MIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqsNmmxb1ICso6Ay25lapcRyvLxAX/5u422uV
+eiNn5jseCVXfg1jJr2Symrgou2dgMtIpZoVKT7w0el8sNpD+az5oMOWUNfTGEpYI
+5zNAhtiedxCWcX15gfezOZ/DECL7HP8U37JFVdazm0CjvlQWI+8rFGvFQJJFDLYJ
+h0fHGfbX6L/ST6cANtkXZyIU6CYFSgniuuDjHmQnQr6CC8lkisJxY5QVS7MZ02RR
+nU/dK14ABY+mo/0ZeBKR5si04hr4i18nJJnk4DnHN+jQ/WWWSO1yLqr9kAOj37dA
+nRAeKuzkx/VbU8DC/3sh0otcazoWO470D+irOy1is0whDArLNwIDAQABo4G9MIG6
+MA4GA1UdDwEB/wQEAwIE8DAdBgNVHQ4EFgQUbAavOY+vIXgc44k8GvLHH6mdzzYw
+HwYDVR0jBBgwFoAUb+S3Mu7cbwZiqZaEHTEMyUhLyH4waAYDVR0RBGEwX4IFZW52
+b3mCFGVudm95LnByb2plY3Rjb250b3VyghhlbnZveS5wcm9qZWN0Y29udG91ci5z
+dmOCJmVudm95LnByb2plY3Rjb250b3VyLnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqG
+SIb3DQEBCwUAA4IBAQBIKpBD1T9tugzJF7lajbdulXTb9qGibwQALqauskX9Sq57
+po/R2TjyxywLn4DgM7BAzzu9qfHWf+S4eQjRUHQshPbUEX9CEsSd5tCu8ZHVbBds
+6qFagl2+YQ9ng0Xwta9ezvctM3T6Dy9Kkf5OOe9ysMEsBX7s8NFxe68Qku+cExr3
+78oERlIoNOlT0cNbFLAlH2svNv1uB4qOThRDha52L+mlUdZfTMYZAwNDJWm52t/M
+NCIm5NJ5jAJpcJmoEb+JMP3j0x6wydHDXFtGm3WRggZRcrjasyodSKK6szbf96+9
+6syzAwvg9xxNtFxwbhRqqplMEz2sDWaggTrxCQzd
+-----END CERTIFICATE-----
+`
+
+var chain4 = `-----BEGIN CERTIFICATE-----
+Oy1is0whDArLNwIDAQABo4G9MIG6
+MA4GA1UdDwEB/wQEAwIE8DAdBgNVHQ4EFgQUbAavOY+vIXgc44k8GvLHH6mdzzYw
+HwYDVR0jBBgwFoAUb+S3Mu7cbwZiqZaEHTEMyUhLyH4waAYDVR0RBGEwX4IFZW52
+b3mCFGVudm95LnByb2plY3Rjb250b3VyghhlbnZveS5wcm9qZWN0Y29udG91ci5z
+dmOCJmVudm95LnByb2plY3Rjb250b3VyLnN2Yy5jbHVzdGVyLmxvY2FsMA0GCSqG
+SIb3DQEBCwUAA4IBAQBIKpBD1T9tugzJF7lajbdulXTb9qGibwQALqauskX9Sq57
+po/R2TjyxywLn4DgM7BAzzu9qfHWf+S4eQjRUHQshPbUEX9CEsSd5tCu8ZHVbBds
+6qFagl2+YQ9ng0Xwta9ezvctM3T6Dy9Kkf5OOe9ysMEsBX7s8NFxe68Qku+cExr3
+78oERlIoNOlT0cNbFLAlH2svNv1uB4qOThRDha52L+mlUdZfTMYZAwNDJWm52t/M
+NCIm5NJ5jAJpcJmoEb+JMP3j0x6wydHDXFtGm3WRggZRcrjasyodSKK6szbf96+9
+6syzAwvg9xxNtFxwbhRqqplMEz2sDWaggTrxCQzd
+-----END CERTIFICATE-----
+`
+
+// tests validate that the certParser function correctly parses a certificate
+func TestCertParser(t *testing.T) {
+
+	expiredCert := []byte(chain)
+	multiCert := []byte(chain2)
+	validCert := []byte(chain3)
+	nonCert := []byte(chain4)
+	//add docs
+
+	certParserTests := []struct {
+		Name      string
+		Certs     []byte
+		CertQty   int
+		IsSubject bool
+		IsCert    bool
+		IsValid   bool
+	}{
+		// Name, Certs, CertQty, IsSubject, IsCert, IsValid
+		{"Widgits", expiredCert, 1, true, true, false},
+		{"digicert", multiCert, 2, true, true, false}, //IsValid will not be evaluated for multicerts
+		{"envoy", validCert, 1, true, true, true},
+		{"non.crt", nonCert, 1, false, false, false}, // IsSubject and IsValid n/a for non.crt
+	}
+
+	for _, e := range certParserTests {
+
+		results, _ := CertParser(e.Name, e.Certs)
+
+		// tests if results contains a list of parsed certificates
+		var isCert bool
+		if len(results) == 0 {
+			isCert = false
+		} else {
+			isCert = true
+		}
+
+		if isCert != e.IsCert {
+			t.Errorf("Expected %v, but got %v that %s is a certificate", e.IsCert, isCert, e.Name)
+			continue
+		}
+
+		for _, cert := range results {
+
+			t.Log(e.Name, cert.Subject)
+
+			// test checks if certificate subject contains a matching string; validates that can parse cert and pull back information
+			if !strings.Contains(cert.Subject, e.Name) {
+				t.Error("unable to parse certificate: ", e.Name)
+
+			} else {
+				isSubject := true
+				if e.IsSubject != isSubject {
+					t.Errorf("You expected that %s certificate contains %s in the Subject line but it was not present; here is the entire subject string: %v", e.Name, e.Name, cert.Subject)
+				}
+			}
+
+			// test checks for expected quantity of certificates in a certificate file
+			numCerts := len(results)
+			if numCerts != e.CertQty {
+				t.Errorf("expected %d certificates in slice but got %d", e.CertQty, numCerts)
+			}
+			if numCerts > 1 {
+				continue
+			}
+
+			// test checks for expected certificate expired
+			if cert.IsValid != e.IsValid {
+				t.Errorf("When checing that %v certificate is expired, you expected %v, but got %v ", e.Name, e.IsValid, cert.IsValid)
+			}
+
+		}
+
+	}
+
+}
+
+// validates that certificate count is correct when parsing a certificate input string.
+func Test_decodePem(t *testing.T) {
+	certDecoderTests := []struct {
+		Name      string
+		certInput string
+		certCount int
+	}{
+		{"widgets-cert", chain, 1},
+		{"digi-cert", chain2, 2},
+	}
+	for _, e := range certDecoderTests {
+		results, _ := decodePem(e.certInput)
+		certCount := 0
+
+		for _, cert := range results.Certificate {
+			t.Log(cert)
+			certCount++
+		}
+		if certCount != e.certCount {
+			t.Errorf("cert count -- expected %d, but got %d", e.certCount, certCount)
+		}
+
+	}
+}