mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:31:33 +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.
246 lines
7.6 KiB
Go
246 lines
7.6 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Bundle-6 / Audit H-008 + M-022 / CWE-532 regression suite.
|
|
|
|
func TestRedactDetailsForAudit_NilAndEmpty(t *testing.T) {
|
|
if got := RedactDetailsForAudit(nil); got != nil {
|
|
t.Errorf("nil input → expected nil out, got %v", got)
|
|
}
|
|
if got := RedactDetailsForAudit(map[string]interface{}{}); got != nil {
|
|
t.Errorf("empty input → expected nil out, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestRedactDetailsForAudit_CredentialKeys(t *testing.T) {
|
|
cases := []string{
|
|
"api_key", "ApiKey", "API_KEY", "password", "Passphrase",
|
|
"secret", "client_secret", "token", "access_token",
|
|
"refresh_token", "bootstrap_token", "private_key", "PrivateKey",
|
|
"private_key_pem", "key_pem", "cert_pem", "chain_pem", "full_pem",
|
|
"eab_secret", "eab_kid", "acme_account_key", "hmac",
|
|
"signature", "auth", "authorization", "bearer",
|
|
}
|
|
for _, key := range cases {
|
|
t.Run(key, func(t *testing.T) {
|
|
in := map[string]interface{}{
|
|
key: "sensitive-value-do-not-leak",
|
|
"non_sensitive_id": "ok-public-id",
|
|
}
|
|
out := RedactDetailsForAudit(in)
|
|
if out[key] != "[REDACTED:CREDENTIAL]" {
|
|
t.Errorf("expected credential redaction, got %v", out[key])
|
|
}
|
|
if out["non_sensitive_id"] != "ok-public-id" {
|
|
t.Errorf("non-sensitive field mutated: %v", out["non_sensitive_id"])
|
|
}
|
|
redactedKeys, ok := out["redacted_keys"].([]string)
|
|
if !ok || len(redactedKeys) != 1 || redactedKeys[0] != key {
|
|
t.Errorf("redacted_keys = %v, expected [%q]", out["redacted_keys"], key)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRedactDetailsForAudit_PIIKeys(t *testing.T) {
|
|
cases := []string{
|
|
"email", "Email_Address", "phone", "telephone", "ssn",
|
|
"social_security", "dob", "date_of_birth", "name", "full_name",
|
|
"first_name", "last_name", "surname", "address", "street",
|
|
"street_address", "city", "postal_code", "zip", "ip_address",
|
|
}
|
|
for _, key := range cases {
|
|
t.Run(key, func(t *testing.T) {
|
|
in := map[string]interface{}{key: "personal-data"}
|
|
out := RedactDetailsForAudit(in)
|
|
if out[key] != "[REDACTED:PII]" {
|
|
t.Errorf("expected PII redaction, got %v", out[key])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRedactDetailsForAudit_NestedMap(t *testing.T) {
|
|
in := map[string]interface{}{
|
|
"resource_id": "iss-prod",
|
|
"config": map[string]interface{}{
|
|
"endpoint": "https://acme.example.com",
|
|
"eab_secret": "do-not-leak-this-secret",
|
|
"contact": map[string]interface{}{
|
|
"email": "ops@example.com",
|
|
"role": "admin",
|
|
},
|
|
},
|
|
}
|
|
out := RedactDetailsForAudit(in)
|
|
|
|
cfg, ok := out["config"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("config field shape changed: %T", out["config"])
|
|
}
|
|
if cfg["eab_secret"] != "[REDACTED:CREDENTIAL]" {
|
|
t.Errorf("nested credential not redacted: %v", cfg["eab_secret"])
|
|
}
|
|
if cfg["endpoint"] != "https://acme.example.com" {
|
|
t.Errorf("non-sensitive nested field mutated: %v", cfg["endpoint"])
|
|
}
|
|
contact, ok := cfg["contact"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("contact field shape changed: %T", cfg["contact"])
|
|
}
|
|
if contact["email"] != "[REDACTED:PII]" {
|
|
t.Errorf("nested PII not redacted: %v", contact["email"])
|
|
}
|
|
if contact["role"] != "admin" {
|
|
t.Errorf("non-sensitive nested field mutated: %v", contact["role"])
|
|
}
|
|
|
|
// redacted_keys array surfaces the dotted paths
|
|
redactedKeys, ok := out["redacted_keys"].([]string)
|
|
if !ok {
|
|
t.Fatalf("redacted_keys missing or wrong type: %T", out["redacted_keys"])
|
|
}
|
|
sort.Strings(redactedKeys)
|
|
wantKeys := []string{"config.contact.email", "config.eab_secret"}
|
|
if len(redactedKeys) != len(wantKeys) {
|
|
t.Errorf("redacted_keys len mismatch: got %v want %v", redactedKeys, wantKeys)
|
|
}
|
|
for i, want := range wantKeys {
|
|
if i >= len(redactedKeys) || redactedKeys[i] != want {
|
|
t.Errorf("redacted_keys[%d] = %q want %q", i, redactedKeys[i], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRedactDetailsForAudit_NestedArray(t *testing.T) {
|
|
// Arrays of maps (e.g. SANs with metadata) — credentials inside array
|
|
// elements must also be redacted.
|
|
in := map[string]interface{}{
|
|
"contacts": []interface{}{
|
|
map[string]interface{}{
|
|
"name": "Alice",
|
|
"email": "alice@example.com",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "Bob",
|
|
"email": "bob@example.com",
|
|
},
|
|
},
|
|
}
|
|
out := RedactDetailsForAudit(in)
|
|
contacts, ok := out["contacts"].([]interface{})
|
|
if !ok {
|
|
t.Fatalf("contacts shape changed: %T", out["contacts"])
|
|
}
|
|
if len(contacts) != 2 {
|
|
t.Fatalf("expected 2 contacts, got %d", len(contacts))
|
|
}
|
|
for i, c := range contacts {
|
|
m, ok := c.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("contact %d shape changed: %T", i, c)
|
|
}
|
|
if m["email"] != "[REDACTED:PII]" {
|
|
t.Errorf("contact[%d].email not redacted: %v", i, m["email"])
|
|
}
|
|
if m["name"] != "[REDACTED:PII]" {
|
|
t.Errorf("contact[%d].name not redacted: %v", i, m["name"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRedactDetailsForAudit_NoRedactionPath(t *testing.T) {
|
|
// Maps with no sensitive keys should NOT have a redacted_keys array
|
|
// — clutter-free for the common case.
|
|
in := map[string]interface{}{
|
|
"action": "create_certificate",
|
|
"cert_id": "mc-prod-001",
|
|
"latency_ms": float64(42),
|
|
}
|
|
out := RedactDetailsForAudit(in)
|
|
if _, present := out["redacted_keys"]; present {
|
|
t.Errorf("expected no redacted_keys when no redaction occurred, got %v", out["redacted_keys"])
|
|
}
|
|
}
|
|
|
|
func TestRedactDetailsForAudit_DoesNotMutateInput(t *testing.T) {
|
|
in := map[string]interface{}{
|
|
"api_key": "secret-do-not-leak",
|
|
"resource": "iss-prod",
|
|
}
|
|
_ = RedactDetailsForAudit(in)
|
|
if in["api_key"] != "secret-do-not-leak" {
|
|
t.Errorf("input map was mutated: api_key = %v", in["api_key"])
|
|
}
|
|
}
|
|
|
|
func TestRedactDetailsForAudit_CaseInsensitive(t *testing.T) {
|
|
cases := []string{"API_KEY", "Api_Key", "api_KEY", "EMAIL", "Email"}
|
|
for _, key := range cases {
|
|
t.Run(key, func(t *testing.T) {
|
|
out := RedactDetailsForAudit(map[string]interface{}{key: "leak-me"})
|
|
val, _ := out[key].(string)
|
|
if !strings.HasPrefix(val, "[REDACTED:") {
|
|
t.Errorf("case-insensitive match failed for %q: %v", key, out[key])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRedactDetailsForAudit_JSONRoundTrip(t *testing.T) {
|
|
// The redacted map MUST round-trip through json.Marshal (the
|
|
// AuditService persistence path). Catches type-assertion regressions.
|
|
in := map[string]interface{}{
|
|
"reason": "compromised-key",
|
|
"api_key": "leak-me",
|
|
"contacts": []interface{}{
|
|
map[string]interface{}{"email": "ops@example.com"},
|
|
},
|
|
}
|
|
out := RedactDetailsForAudit(in)
|
|
b, err := json.Marshal(out)
|
|
if err != nil {
|
|
t.Fatalf("redacted map failed json.Marshal: %v", err)
|
|
}
|
|
body := string(b)
|
|
if strings.Contains(body, "leak-me") {
|
|
t.Errorf("credential value leaked through marshal: %s", body)
|
|
}
|
|
if strings.Contains(body, "ops@example.com") {
|
|
t.Errorf("PII value leaked through marshal: %s", body)
|
|
}
|
|
if !strings.Contains(body, "[REDACTED:CREDENTIAL]") {
|
|
t.Errorf("redaction sentinel missing from marshaled output: %s", body)
|
|
}
|
|
if !strings.Contains(body, "[REDACTED:PII]") {
|
|
t.Errorf("PII redaction sentinel missing from marshaled output: %s", body)
|
|
}
|
|
if !strings.Contains(body, "redacted_keys") {
|
|
t.Errorf("redacted_keys array missing from marshaled output: %s", body)
|
|
}
|
|
}
|
|
|
|
// TestRedactDetailsForAudit_ScalarTypes confirms the recursive arm doesn't
|
|
// mishandle non-map non-slice values.
|
|
func TestRedactDetailsForAudit_ScalarTypes(t *testing.T) {
|
|
in := map[string]interface{}{
|
|
"string_field": "hello",
|
|
"int_field": 42,
|
|
"float_field": 3.14,
|
|
"bool_field": true,
|
|
"nil_field": nil,
|
|
}
|
|
out := RedactDetailsForAudit(in)
|
|
if out["string_field"] != "hello" || out["int_field"] != 42 ||
|
|
out["float_field"] != 3.14 || out["bool_field"] != true ||
|
|
out["nil_field"] != nil {
|
|
t.Errorf("scalar pass-through failed: %v", out)
|
|
}
|
|
}
|