mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:01:32 +00:00
7cb453a336
Mechanical reformat. The new 'gofmt drift' CI step (added in
ci-pipeline-cleanup Phase 4, commit 0f205a8) surfaced 111 files
with accumulated gofmt drift across cmd/, internal/, and deploy/test/.
Each file's diff is gofmt-standard: whitespace adjustments, intra-
group import sorting (alphabetical by import path within blank-line-
separated groups), and struct-tag column alignment. No semantic
changes — verified via 'git diff --ignore-all-space' which shows only
the line-position deltas from import reordering.
The gate stays in place after this commit. Going forward it catches
gofmt drift at PR time.
372 lines
10 KiB
Go
372 lines
10 KiB
Go
package awssm
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shankar0123/certctl/internal/config"
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
)
|
|
|
|
// mockSMClient is a mock implementation of SMClient for testing.
|
|
type mockSMClient struct {
|
|
secrets map[string]string // secret name -> secret value
|
|
secretMetadata map[string]SecretMetadata // secret name -> metadata
|
|
listError error
|
|
getErrors map[string]error // secret name -> error
|
|
}
|
|
|
|
func newMockSMClient() *mockSMClient {
|
|
return &mockSMClient{
|
|
secrets: make(map[string]string),
|
|
secretMetadata: make(map[string]SecretMetadata),
|
|
getErrors: make(map[string]error),
|
|
}
|
|
}
|
|
|
|
func (m *mockSMClient) ListSecrets(ctx context.Context, filters string) ([]SecretMetadata, error) {
|
|
if m.listError != nil {
|
|
return nil, m.listError
|
|
}
|
|
|
|
var result []SecretMetadata
|
|
for _, meta := range m.secretMetadata {
|
|
result = append(result, meta)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (m *mockSMClient) GetSecretValue(ctx context.Context, secretID string) (string, error) {
|
|
if err, ok := m.getErrors[secretID]; ok {
|
|
return "", err
|
|
}
|
|
return m.secrets[secretID], nil
|
|
}
|
|
|
|
// generateTestCert generates a test certificate with the given subject and returns it as PEM.
|
|
func generateTestCert(commonName string, sans []string) (string, *x509.Certificate, error) {
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{CommonName: commonName},
|
|
NotBefore: time.Now().Add(-1 * time.Hour),
|
|
NotAfter: time.Now().Add(24 * time.Hour),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
DNSNames: sans,
|
|
}
|
|
|
|
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
|
|
|
cert, err := x509.ParseCertificate(certDER)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
return string(certPEM), cert, nil
|
|
}
|
|
|
|
func TestSource_ValidateConfig_Success(t *testing.T) {
|
|
cfg := &config.AWSSecretsMgrDiscoveryConfig{
|
|
Enabled: true,
|
|
Region: "us-east-1",
|
|
}
|
|
source := NewWithClient(cfg, newMockSMClient(), nil)
|
|
|
|
err := source.ValidateConfig()
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSource_ValidateConfig_MissingRegion(t *testing.T) {
|
|
cfg := &config.AWSSecretsMgrDiscoveryConfig{
|
|
Enabled: true,
|
|
Region: "",
|
|
}
|
|
source := NewWithClient(cfg, newMockSMClient(), nil)
|
|
|
|
err := source.ValidateConfig()
|
|
if err == nil {
|
|
t.Fatal("expected error for missing region")
|
|
}
|
|
if err.Error() != "aws secrets manager region is required" {
|
|
t.Fatalf("unexpected error message: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSource_Name(t *testing.T) {
|
|
source := NewWithClient(&config.AWSSecretsMgrDiscoveryConfig{Region: "us-east-1"}, newMockSMClient(), nil)
|
|
if source.Name() != "AWS Secrets Manager" {
|
|
t.Errorf("expected 'AWS Secrets Manager', got %s", source.Name())
|
|
}
|
|
}
|
|
|
|
func TestSource_Type(t *testing.T) {
|
|
source := NewWithClient(&config.AWSSecretsMgrDiscoveryConfig{Region: "us-east-1"}, newMockSMClient(), nil)
|
|
if source.Type() != "aws-sm" {
|
|
t.Errorf("expected 'aws-sm', got %s", source.Type())
|
|
}
|
|
}
|
|
|
|
func TestSource_Discover_Success(t *testing.T) {
|
|
// Generate test certificates
|
|
certPEM1, _, err := generateTestCert("test1.example.com", []string{"www.test1.example.com"})
|
|
if err != nil {
|
|
t.Fatalf("failed to generate test cert 1: %v", err)
|
|
}
|
|
|
|
certPEM2, _, err := generateTestCert("test2.example.com", []string{"mail.test2.example.com", "smtp.test2.example.com"})
|
|
if err != nil {
|
|
t.Fatalf("failed to generate test cert 2: %v", err)
|
|
}
|
|
|
|
// Set up mock client
|
|
mockClient := newMockSMClient()
|
|
mockClient.secrets["cert1"] = certPEM1
|
|
mockClient.secrets["cert2"] = certPEM2
|
|
mockClient.secretMetadata["cert1"] = SecretMetadata{
|
|
Name: "cert1",
|
|
ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:cert1",
|
|
Tags: map[string]string{"type": "certificate"},
|
|
}
|
|
mockClient.secretMetadata["cert2"] = SecretMetadata{
|
|
Name: "cert2",
|
|
ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:cert2",
|
|
Tags: map[string]string{"type": "certificate"},
|
|
}
|
|
|
|
cfg := &config.AWSSecretsMgrDiscoveryConfig{
|
|
Enabled: true,
|
|
Region: "us-east-1",
|
|
TagFilter: "type=certificate",
|
|
}
|
|
source := NewWithClient(cfg, mockClient, nil)
|
|
|
|
report, err := source.Discover(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if report.AgentID != "cloud-aws-sm" {
|
|
t.Errorf("expected agent ID 'cloud-aws-sm', got %s", report.AgentID)
|
|
}
|
|
|
|
if len(report.Certificates) != 2 {
|
|
t.Errorf("expected 2 certificates, got %d", len(report.Certificates))
|
|
}
|
|
|
|
// Find the certificates by common name (order is not guaranteed)
|
|
var cert1, cert2 *domain.DiscoveredCertEntry
|
|
for i := range report.Certificates {
|
|
if report.Certificates[i].CommonName == "test1.example.com" {
|
|
cert1 = &report.Certificates[i]
|
|
} else if report.Certificates[i].CommonName == "test2.example.com" {
|
|
cert2 = &report.Certificates[i]
|
|
}
|
|
}
|
|
|
|
if cert1 == nil {
|
|
t.Fatalf("certificate with CN 'test1.example.com' not found")
|
|
}
|
|
if cert2 == nil {
|
|
t.Fatalf("certificate with CN 'test2.example.com' not found")
|
|
}
|
|
|
|
// Check first certificate
|
|
if len(cert1.SANs) != 1 || cert1.SANs[0] != "www.test1.example.com" {
|
|
t.Errorf("unexpected SANs for cert1: %v", cert1.SANs)
|
|
}
|
|
|
|
// Check second certificate has 2 SANs
|
|
if len(cert2.SANs) != 2 {
|
|
t.Errorf("expected 2 SANs for cert2, got %d", len(cert2.SANs))
|
|
}
|
|
|
|
// Check source path format for first cert
|
|
if cert1.SourcePath != "aws-sm://us-east-1/cert1" {
|
|
t.Errorf("unexpected source path for cert1: %s", cert1.SourcePath)
|
|
}
|
|
|
|
// Check that scan duration is reasonable
|
|
if report.ScanDurationMs < 0 {
|
|
t.Errorf("unexpected negative scan duration: %d", report.ScanDurationMs)
|
|
}
|
|
}
|
|
|
|
func TestSource_Discover_EmptyResults(t *testing.T) {
|
|
mockClient := newMockSMClient()
|
|
|
|
cfg := &config.AWSSecretsMgrDiscoveryConfig{
|
|
Enabled: true,
|
|
Region: "us-east-1",
|
|
}
|
|
source := NewWithClient(cfg, mockClient, nil)
|
|
|
|
report, err := source.Discover(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if report.AgentID != "cloud-aws-sm" {
|
|
t.Errorf("expected agent ID 'cloud-aws-sm', got %s", report.AgentID)
|
|
}
|
|
|
|
if len(report.Certificates) != 0 {
|
|
t.Errorf("expected 0 certificates, got %d", len(report.Certificates))
|
|
}
|
|
|
|
if len(report.Errors) != 0 {
|
|
t.Errorf("expected 0 errors, got %d", len(report.Errors))
|
|
}
|
|
}
|
|
|
|
func TestSource_Discover_ListError(t *testing.T) {
|
|
mockClient := newMockSMClient()
|
|
mockClient.listError = fmt.Errorf("ListSecrets failed")
|
|
|
|
cfg := &config.AWSSecretsMgrDiscoveryConfig{
|
|
Enabled: true,
|
|
Region: "us-east-1",
|
|
}
|
|
source := NewWithClient(cfg, mockClient, nil)
|
|
|
|
report, err := source.Discover(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Discover should not return error for list failure: %v", err)
|
|
}
|
|
|
|
// Should have recorded the error but still return a report
|
|
if len(report.Errors) != 1 {
|
|
t.Errorf("expected 1 error, got %d", len(report.Errors))
|
|
}
|
|
}
|
|
|
|
func TestSource_Discover_GetSecretError(t *testing.T) {
|
|
// Generate test certificate
|
|
certPEM, _, err := generateTestCert("good.example.com", nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to generate test cert: %v", err)
|
|
}
|
|
|
|
mockClient := newMockSMClient()
|
|
mockClient.secrets["good-secret"] = certPEM
|
|
mockClient.secretMetadata["good-secret"] = SecretMetadata{
|
|
Name: "good-secret",
|
|
Tags: map[string]string{"type": "certificate"},
|
|
}
|
|
mockClient.secrets["bad-secret"] = "dummy"
|
|
mockClient.secretMetadata["bad-secret"] = SecretMetadata{
|
|
Name: "bad-secret",
|
|
Tags: map[string]string{"type": "certificate"},
|
|
}
|
|
mockClient.getErrors["bad-secret"] = fmt.Errorf("GetSecretValue failed")
|
|
|
|
cfg := &config.AWSSecretsMgrDiscoveryConfig{
|
|
Enabled: true,
|
|
Region: "us-east-1",
|
|
}
|
|
source := NewWithClient(cfg, mockClient, nil)
|
|
|
|
report, err := source.Discover(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Should have 1 good certificate and 1 error
|
|
if len(report.Certificates) != 1 {
|
|
t.Errorf("expected 1 certificate, got %d", len(report.Certificates))
|
|
}
|
|
if len(report.Errors) != 1 {
|
|
t.Errorf("expected 1 error, got %d", len(report.Errors))
|
|
}
|
|
}
|
|
|
|
func TestSource_Discover_DERCert(t *testing.T) {
|
|
// Generate test certificate in DER format, then base64 encode it
|
|
_, parsedCert, err := generateTestCert("der.example.com", nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to generate test cert: %v", err)
|
|
}
|
|
|
|
derEncoded := base64.StdEncoding.EncodeToString(parsedCert.Raw)
|
|
|
|
mockClient := newMockSMClient()
|
|
mockClient.secrets["der-cert"] = derEncoded
|
|
mockClient.secretMetadata["der-cert"] = SecretMetadata{
|
|
Name: "der-cert",
|
|
Tags: map[string]string{"type": "certificate"},
|
|
}
|
|
|
|
cfg := &config.AWSSecretsMgrDiscoveryConfig{
|
|
Enabled: true,
|
|
Region: "us-east-1",
|
|
}
|
|
source := NewWithClient(cfg, mockClient, nil)
|
|
|
|
report, err := source.Discover(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(report.Certificates) != 1 {
|
|
t.Errorf("expected 1 certificate, got %d", len(report.Certificates))
|
|
}
|
|
|
|
if report.Certificates[0].CommonName != "der.example.com" {
|
|
t.Errorf("expected CN 'der.example.com', got %s", report.Certificates[0].CommonName)
|
|
}
|
|
}
|
|
|
|
func TestSource_Discover_AgentIDAndSourcePath(t *testing.T) {
|
|
// Generate test certificate
|
|
certPEM, _, err := generateTestCert("source-path.example.com", nil)
|
|
if err != nil {
|
|
t.Fatalf("failed to generate test cert: %v", err)
|
|
}
|
|
|
|
mockClient := newMockSMClient()
|
|
mockClient.secrets["my-secret"] = certPEM
|
|
mockClient.secretMetadata["my-secret"] = SecretMetadata{
|
|
Name: "my-secret",
|
|
Tags: map[string]string{"type": "certificate"},
|
|
}
|
|
|
|
cfg := &config.AWSSecretsMgrDiscoveryConfig{
|
|
Enabled: true,
|
|
Region: "eu-west-1",
|
|
}
|
|
source := NewWithClient(cfg, mockClient, nil)
|
|
|
|
report, err := source.Discover(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if report.AgentID != "cloud-aws-sm" {
|
|
t.Errorf("expected agent ID 'cloud-aws-sm', got %s", report.AgentID)
|
|
}
|
|
|
|
if report.Certificates[0].SourcePath != "aws-sm://eu-west-1/my-secret" {
|
|
t.Errorf("expected source path 'aws-sm://eu-west-1/my-secret', got %s", report.Certificates[0].SourcePath)
|
|
}
|
|
}
|