Skip to content

Commit 51640ac

Browse files
committed
Improving docs and logging
1 parent 112c341 commit 51640ac

3 files changed

Lines changed: 89 additions & 6 deletions

File tree

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
# sops-cop
1+
# SOPS-Cop
22

33
[![Go Version](https://img.shields.io/badge/Go-1.23%2B-00ADD8?logo=go)](https://go.dev/)
44
[![CI](https://github.com/binbashing/sops-cop/actions/workflows/ci.yml/badge.svg)](https://github.com/binbashing/sops-cop/actions/workflows/ci.yml)
55
[![Tests](https://img.shields.io/badge/tests-go%20test%20.%2F...-brightgreen)](https://github.com/binbashing/sops-cop/actions/workflows/ci.yml)
66

7-
A small, fast Go CLI that enforces SOPS encryption rules without requiring the SOPS binary or encryption keys; designed for commit hooks and CI jobs.
7+
SOPS-Cop is a CLI tool to enforce SOPS encryption rules without requiring the SOPS binary or encryption keys; designed for commit hooks and CI jobs.
8+
89

910
## How it works
1011

11-
- Locates `.sops.yaml` by walking up from the provided path (or current directory).
12-
- Scans files matched by `creation_rules` and checks that selected values are encrypted.
12+
- Discovers your existing SOPS configuration and verifies encryption rules are followed.
1313
- Reports each unencrypted key path to `stderr` with file path and line:column location.
1414

1515
## Exit codes
1616

1717
- `0`: all checked values are encrypted
18+
- `2`: invalid arguments (for example, unresolvable target path)
1819
- `3`: file read error (for example, file missing or permission denied)
1920
- `4`: invalid YAML input
2021
- `5`: one or more unencrypted values were found
@@ -56,6 +57,12 @@ Or start from any path inside the project:
5657
./sops-cop -target path/to/any/subdir
5758
```
5859

60+
Print version:
61+
62+
```bash
63+
./sops-cop -version
64+
```
65+
5966
Help:
6067

6168
```bash

main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func run(target string, stderr io.Writer) int {
8989

9090
config, configDir, err := loadSopsConfig(startDir)
9191
if err != nil {
92-
fmt.Fprintf(stderr, "error: %v\n", err)
92+
fmt.Fprintf(stderr, "error: failed to load .sops.yaml config: %v\n", err)
9393
return exitConfigError
9494
}
9595

@@ -117,7 +117,7 @@ func validateProject(config *SopsConfig, configDir string, stderr io.Writer) int
117117

118118
rule, matched, err := loadCreationRuleForFile(config, path)
119119
if err != nil {
120-
fmt.Fprintf(stderr, "error: %v\n", err)
120+
fmt.Fprintf(stderr, "error: failed to match creation rule for %s: %v\n", path, err)
121121
exitCode = exitConfigError
122122
return nil
123123
}

main_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,82 @@ func TestRunExitCodes(t *testing.T) {
352352
wantExitCode: exitUnencryptedValue,
353353
wantErrSubstr: "secrets/bad.yaml",
354354
},
355+
{
356+
name: "multiple creation rules apply different encryption to different paths",
357+
setup: func(t *testing.T) string {
358+
tempDir := t.TempDir()
359+
// Rule 1: secrets/ files must encrypt everything
360+
// Rule 2: configs/ files only encrypt keys matching "password"
361+
sopsConfig := `creation_rules:
362+
- path_regex: "^secrets/.*\\.yaml$"
363+
encrypted_regex: ""
364+
- path_regex: "^configs/.*\\.yaml$"
365+
encrypted_regex: "^password$"
366+
`
367+
if err := os.WriteFile(filepath.Join(tempDir, ".sops.yaml"), []byte(sopsConfig), 0o600); err != nil {
368+
t.Fatalf("write .sops.yaml: %v", err)
369+
}
370+
371+
if err := os.MkdirAll(filepath.Join(tempDir, "secrets"), 0o755); err != nil {
372+
t.Fatalf("mkdir secrets: %v", err)
373+
}
374+
if err := os.MkdirAll(filepath.Join(tempDir, "configs"), 0o755); err != nil {
375+
t.Fatalf("mkdir configs: %v", err)
376+
}
377+
378+
// secrets/creds.yaml: fully encrypted — should pass
379+
if err := os.WriteFile(filepath.Join(tempDir, "secrets", "creds.yaml"),
380+
[]byte("token: ENC[AES256_GCM,data:abc]\n"), 0o600); err != nil {
381+
t.Fatalf("write secrets creds.yaml: %v", err)
382+
}
383+
384+
// configs/db.yaml: host is plaintext (allowed), password is plaintext (violation)
385+
if err := os.WriteFile(filepath.Join(tempDir, "configs", "db.yaml"),
386+
[]byte("host: localhost\npassword: plaintext\n"), 0o600); err != nil {
387+
t.Fatalf("write configs db.yaml: %v", err)
388+
}
389+
390+
return tempDir
391+
},
392+
wantExitCode: exitUnencryptedValue,
393+
wantErrSubstr: "configs/db.yaml",
394+
},
395+
{
396+
name: "multiple creation rules all passing",
397+
setup: func(t *testing.T) string {
398+
tempDir := t.TempDir()
399+
sopsConfig := `creation_rules:
400+
- path_regex: "^secrets/.*\\.yaml$"
401+
encrypted_regex: ""
402+
- path_regex: "^configs/.*\\.yaml$"
403+
encrypted_regex: "^password$"
404+
`
405+
if err := os.WriteFile(filepath.Join(tempDir, ".sops.yaml"), []byte(sopsConfig), 0o600); err != nil {
406+
t.Fatalf("write .sops.yaml: %v", err)
407+
}
408+
409+
if err := os.MkdirAll(filepath.Join(tempDir, "secrets"), 0o755); err != nil {
410+
t.Fatalf("mkdir secrets: %v", err)
411+
}
412+
if err := os.MkdirAll(filepath.Join(tempDir, "configs"), 0o755); err != nil {
413+
t.Fatalf("mkdir configs: %v", err)
414+
}
415+
416+
if err := os.WriteFile(filepath.Join(tempDir, "secrets", "creds.yaml"),
417+
[]byte("token: ENC[AES256_GCM,data:abc]\n"), 0o600); err != nil {
418+
t.Fatalf("write secrets creds.yaml: %v", err)
419+
}
420+
421+
if err := os.WriteFile(filepath.Join(tempDir, "configs", "db.yaml"),
422+
[]byte("host: localhost\npassword: ENC[AES256_GCM,data:xyz]\n"), 0o600); err != nil {
423+
t.Fatalf("write configs db.yaml: %v", err)
424+
}
425+
426+
return tempDir
427+
},
428+
wantExitCode: exitSuccess,
429+
wantErrSubstr: "All files compliant",
430+
},
355431
{
356432
name: "invalid rule when both encrypted_regex and unencrypted_regex are set",
357433
setup: func(t *testing.T) string {

0 commit comments

Comments
 (0)