Skip to content

Commit

Permalink
feat: basic CLI implementation (#15)
Browse files Browse the repository at this point in the history
* feat: basic CLI implementation

* docs: update README.md with install instructions

* fix: address code review comments
  • Loading branch information
martinohmann authored Nov 28, 2024
1 parent 744da10 commit ef18cf2
Show file tree
Hide file tree
Showing 14 changed files with 428 additions and 30 deletions.
4 changes: 4 additions & 0 deletions .sops-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
rules:
- description: The AGE key used as part of the testdata
match: age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
# sops-check

We are following a design-first approach, please take a look at [the design document](docs/design.md) and we are happy to hear your thoughts about it.
[![Build Status](https://github.com/Bonial-International-GmbH/sops-check/actions/workflows/ci.yml/badge.svg)](https://github.com/Bonial-International-GmbH/sops-check/actions/workflows/ci.yml)

WIP
> [!NOTE]
> This project is still in an early development stage and a lot of the desired
> features are not implemented yet.
Check SOPS files for correct and compliant usage without decrypting them to
ensure that all SOPS files are configured in the desired fashion. The goal is
to provide a security linter that safeguards the security of the data protected
by the SOPS files against common mistakes and against malicious configurations.

We are following a design-first approach, please take a look at [the design
document](docs/design.md). We are happy to hear your thoughts about it.

## Installation

The simplest way is to install the latest version via:

```sh
go install github.com/Bonial-International-GmbH/sops-check@latest
```

Finally, consult the help for usage instructions:

```sh
sops-check --help
```

## Development

Run the tests:

```sh
make coverage
```

Lint the codebase:

```sh
make lint
```

Build locally:

```sh
make build
```
4 changes: 4 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ definitions:
type: array
description: Schema of the sops-check configuration file
properties:
allowUnmatched:
default: false
description: Allow SOPS files to contain trust anchors that are not matched by any rule.
type: boolean
rules:
$ref: "#/definitions/rules"
description: A list of matching rules.
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.23
toolchain go1.23.3

require (
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/aws/aws-sdk-go-v2 v1.31.0
github.com/getsops/sops/v3 v3.9.1
github.com/goccy/go-yaml v1.14.3
Expand All @@ -29,6 +30,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.39 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.37 // indirect
Expand Down Expand Up @@ -88,6 +90,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton h1:ZGewsAoeSirbUS5cO8L0FMQA+iSop9xR1nmFYifDBPo=
github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U=
github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 h1:xDAuZTn4IMm8o1LnBZvmrL8JA1io4o3YWNXgohbf20g=
Expand Down Expand Up @@ -244,6 +248,7 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
Expand All @@ -256,6 +261,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.55.0 h1:hCq2hNMwsegUvPzI7sPOvtO9cqyy5GbWt/Ybp2xrx8Q=
Expand Down Expand Up @@ -354,6 +361,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
50 changes: 50 additions & 0 deletions internal/cli/args.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cli

import "github.com/alecthomas/kingpin/v2"

// Version is the current version of the app, generated at build time.
var Version = "unknown"

// Args are configuration options parsed from CLI args.
type Args struct {
// CheckPath is the filesystem path to search for SOPS files.
CheckPath string
// ConfigPath is the path of the sops-check configuration file.
ConfigPath string
}

// Defaults apply to arguments not provided explicitly.
var Defaults = &Args{
CheckPath: ".",
ConfigPath: ".sops-check.yaml",
}

// ParseArgs parses arguments from the command line.
func ParseArgs(commandLine []string) (*Args, error) {
args := &Args{}

app := kingpin.New(
"sops-check",
"A tool that looks for SOPS files within a directory tree and ensures they are configured in the desired fashion.",
)
app.Version(Version)
app.DefaultEnvars()

// Flags.
app.HelpFlag.Short('h')
app.Flag("config", "Path to the sops-check configuration file.").
Short('c').
Default(Defaults.ConfigPath).
StringVar(&args.ConfigPath)

// Positional arguments.
app.Arg("path", "Directory to run the checks in. If omitted, checks are run in the current working directory.").
Default(Defaults.CheckPath).
StringVar(&args.CheckPath)

if _, err := app.Parse(commandLine); err != nil {
return nil, err
}

return args, nil
}
39 changes: 39 additions & 0 deletions internal/cli/args_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cli

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseArgs(t *testing.T) {
t.Run("no args", func(t *testing.T) {
args, err := ParseArgs(nil)
require.NoError(t, err)

expected := &Args{
ConfigPath: Defaults.ConfigPath,
CheckPath: Defaults.CheckPath,
}

assert.Equal(t, expected, args)
})

t.Run("args", func(t *testing.T) {
args, err := ParseArgs([]string{"--config", "the-config.yaml"})
require.NoError(t, err)

expected := &Args{
ConfigPath: "the-config.yaml",
CheckPath: Defaults.CheckPath,
}

assert.Equal(t, expected, args)
})

t.Run("invalid args", func(t *testing.T) {
_, err := ParseArgs([]string{"--nonexistent"})
require.Error(t, err)
})
}
27 changes: 2 additions & 25 deletions internal/rules/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"sort"
"strings"

"github.com/Bonial-International-GmbH/sops-check/internal/stringutils"
"github.com/hashicorp/go-set/v3"
)

Expand All @@ -22,7 +23,7 @@ func (b *formatBuffer) writeIndented(indentFirst bool, fn func(*formatBuffer)) {

// Indent the captured bytes and write them to the underlying
// strings.Builder.
writeIndented(&b.Builder, buf.String(), 2, indentFirst)
b.WriteString(stringutils.Indent(buf.String(), 2, indentFirst))
}

// writeIndentedList iterates the list of results and invokes fn for each
Expand Down Expand Up @@ -139,27 +140,3 @@ func formatTrustAnchors(buf *formatBuffer, items set.Collection[string]) {
buf.WriteRune('\n')
}
}

// writeIndented writes a string indented by `count` spaces to a strings.Builder.
func writeIndented(sb *strings.Builder, s string, count int, indentFirst bool) {
if count == 0 || s == "" {
return
}

lines := strings.SplitAfter(s, "\n")

if len(lines[len(lines)-1]) == 0 {
lines = lines[:len(lines)-1]
}

indent := strings.Repeat(" ", count)

for i, line := range lines {
if line != "\n" && line != "\r\n" && (i != 0 || indentFirst) {
// Only indent non-empty lines.
sb.WriteString(indent)
}

sb.WriteString(line)
}
}
31 changes: 31 additions & 0 deletions internal/stringutils/indent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package stringutils

import "strings"

// Indent indents a string by `count` spaces.
func Indent(s string, count int, indentFirst bool) string {
if count == 0 || s == "" {
return s
}

var sb strings.Builder

lines := strings.SplitAfter(s, "\n")

if len(lines[len(lines)-1]) == 0 {
lines = lines[:len(lines)-1]
}

indent := strings.Repeat(" ", count)

for i, line := range lines {
if line != "\n" && line != "\r\n" && (i != 0 || indentFirst) {
// Only indent non-empty lines.
sb.WriteString(indent)
}

sb.WriteString(line)
}

return sb.String()
}
50 changes: 50 additions & 0 deletions internal/stringutils/indent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package stringutils

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestIndent(t *testing.T) {
t.Run("no indent", func(t *testing.T) {
assert.Equal(t, "foo", Indent("foo", 0, true))
})

t.Run("empty string", func(t *testing.T) {
assert.Equal(t, "", Indent("", 2, true))
})

t.Run("multiline string", func(t *testing.T) {
given := `foo
bar
baz`
expected := ` foo
bar
baz`

assert.Equal(t, expected, Indent(given, 2, true))
})

t.Run("skip indent first", func(t *testing.T) {
given := `foo
bar
baz`
expected := `foo
bar
baz`

assert.Equal(t, expected, Indent(given, 2, false))
})

t.Run("trailing newline", func(t *testing.T) {
given := "foo\nbar\n"
expected := " foo\n bar\n"

assert.Equal(t, expected, Indent(given, 2, true))
})
}
Loading

0 comments on commit ef18cf2

Please sign in to comment.