diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 07e1c5248..95e8e05f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,6 +106,84 @@ func (l *caCRLSignNotSet) Execute(c *x509.Certificate) *lint.LintResult { } ``` +Making your Lint Configurable +------------- +Lints may implement an optional interface - `Configurable`... + +```go +type Configurable interface { + Configure() interface{} +} +``` + +...where the returned `interface{}` is a pointer to the target struct to deserialize your configuration into. + +This struct may encode any arbitrary data that may be deserialized from [TOML](https://toml.io/en/). Examples may include: + +* PEM encoded certificates or certificate chains +* File paths +* Resolvable DNS entries or URIs +* Dates or Unix timestamps + +...and so on. How stable and/or appropriate a given configuration field is is left as a code review exercise on a per-lint basis. + +If a lint is `Configurable` then a new step is injected at the beginning of its lifecycle. + +--- +##### Non-Configurable Lifecycle +> * CheckApplies +> * CheckEffective +> * Execute + +##### Configurable Lifecycle +> * Configure +> * CheckApplies +> * CheckEffective +> * Execute + +### Higher Scoped Configurations + +Lints may embed within theselves either pointers or structs to the following definitions within the `lint` package. + +```go +type Global struct {} +type RFC5280Config struct{} +type RFC5480Config struct{} +type RFC5891Config struct{} +type CABFBaselineRequirementsConfig struct {} +type CABFEVGuidelinesConfig struct{} +type MozillaRootStorePolicyConfig struct{} +type AppleRootStorePolicyConfig struct{} +type CommunityConfig struct{} +type EtsiEsiConfig struct{} +``` + +Doing so will enable receiving a _copy_ of any such defintions from a higher scope within the configuration. + +```toml +# Top level (non-scoped) fields will be copied into any Global struct that you declare within your lint. +something_global = 5 +something_else_global = "The funniest joke in the world." + +[RFC5280] +# Top level (non-scoped) fields will be copied into any RFC5280Config struct that you declare within your lint. +wildcard_allowed = true + +[MyLint] +# You can also embed comments! +my_config = "Some arbitrary data." +``` + +An example of the above might be... + +```go +type MyLint struct { + Global lint.Global + RFC lint.RFC5280Config + MyConfig string `toml:"my_config",comment:"You can also embed comments!"` +} +``` + Testing Lints ------------- @@ -167,6 +245,26 @@ Please see the [integration tests README] for more information. [CI]: https://travis-ci.org/zmap/zlint [integration tests README]: https://github.com/zmap/zlint/blob/master/v3/integration/README.md +### Testing Configurable Lints + +Testing a lint that is configurable is much the same as testing one that is not. However, if you wish to exercise +various configurations then you may do so by utilizing the `test.TestLintWithConfig` function which takes in an extra +string which is the raw TOML of your target test configuration. + +```go +func TestCaCommonNameNotMissing2(t *testing.T) { + inputPath := "caCommonNameNotMissing.pem" + expected := lint.Pass + config := ` + [e_ca_common_name_missing2] + BeerHall = "liedershousen" + ` + out := test.TestLintWithConfig("e_ca_common_name_missing2", inputPath, config) + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } +} +``` Updating the TLD Map -------------------- diff --git a/README.md b/README.md index 91c779270..7fbec7440 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,12 @@ Example ZLint CLI usage: echo "Lint mycert.pem with all of the lints except for ETSI ESI sourced lints" zlint -excludeSources=ETSI_ESI mycert.pem + echo "Receive a copy of the full (default) configuration for all configurable lints" + zlint -exampleConfig + + echo "Lint mycert.pem using a custom configuration for any configurable lints" + zlint -config configFile.toml mycert.pem + See `zlint -h` for all available command line options. @@ -149,6 +155,45 @@ if err != nil { zlintResultSet := zlint.LintCertificateEx(parsed, registry) ``` +To lint a certificate in the presence of a particular configuration file, you must first construct the configuration and then make a call to `SetConfiguration` in the `Registry` interface. + +A `Configuration` may be constructed using any of the following functions: + +* `lint.NewConfig(r io.Reader) (Configuration, error)` +* `lint.NewConfigFromFile(path string) (Configuration, error)` +* `lint.NewConfigFromString(config string) (Configuration, error)` + +The contents of the input to all three constructors must be a valid TOML document. + +```go +import ( + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3" +) + +var certDER []byte = ... +parsed, err := x509.ParseCertificate(certDER) +if err != nil { + // If x509.ParseCertificate fails, the certificate is too broken to lint. + // This should be treated as ZLint rejecting the certificate + log.Fatal("unable to parse certificate:", err) +} +configuration, err := lint.NewConfigFromString(` + [some_configurable_lint] + IsWebPki = true + NumIterations = 42 + + [some_configurable_lint.AnySubMapping] + something = "else" + anything = "at all" +`) +if err != nil { + log.Fatal("unable to parse configuration:", err) +} +lint.GlobalRegistry().SetConfigutration(configuration) +zlintResultSet := zlint.LintCertificate(parsed) +``` + See [the `zlint` command][zlint cmd]'s source code for an example. [zlint cmd]: https://github.com/zmap/zlint/blob/master/v3/cmd/zlint/main.go diff --git a/v3/cmd/zlint/main.go b/v3/cmd/zlint/main.go index 080ad9abf..15ce2b287 100644 --- a/v3/cmd/zlint/main.go +++ b/v3/cmd/zlint/main.go @@ -47,6 +47,8 @@ var ( // flags includeSources string excludeSources string printVersion bool + config string + exampleConfig bool // version is replaced by GoReleaser or `make` using an LDFlags option at // build time. Here we supply a default value for folks that `go install` or @@ -66,6 +68,8 @@ func init() { flag.StringVar(&includeSources, "includeSources", "", "Comma-separated list of lint sources to include") flag.StringVar(&excludeSources, "excludeSources", "", "Comma-separated list of lint sources to exclude") flag.BoolVar(&printVersion, "version", false, "Print ZLint version and exit") + flag.StringVar(&config, "config", "", "A path to valid a TOML file that is to service as the configuration for a single run of ZLint") + flag.BoolVar(&exampleConfig, "exampleConfig", false, "Print a complete example of a configuration that is usable via the '-config' flag and exit. All values listed in this example will be set to their default.") flag.BoolVar(&prettyprint, "pretty", false, "Pretty-print JSON output") flag.Usage = func() { @@ -96,6 +100,15 @@ func main() { return } + if exampleConfig { + b, err := registry.DefaultConfiguration() + if err != nil { + log.Fatalf("a critical error occurred while generating a configuration file, %s", err) + } + fmt.Println(string(b)) + return + } + if listLintSources { sources := registry.Sources() sort.Sort(sources) @@ -203,6 +216,11 @@ func trimmedList(raw string) []string { // use. //nolint:cyclop func setLints() (lint.Registry, error) { + configuration, err := lint.NewConfigFromFile(config) + if err != nil { + return nil, err + } + lint.GlobalRegistry().SetConfiguration(configuration) // If there's no filter options set, use the global registry as-is if nameFilter == "" && includeNames == "" && excludeNames == "" && includeSources == "" && excludeSources == "" { return lint.GlobalRegistry(), nil diff --git a/v3/go.mod b/v3/go.mod index 86540fd00..af450c37a 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -3,6 +3,8 @@ module github.com/zmap/zlint/v3 go 1.18 require ( + github.com/kr/text v0.2.0 // indirect + github.com/pelletier/go-toml v1.9.3 github.com/sirupsen/logrus v1.8.1 github.com/zmap/zcrypto v0.0.0-20220402174210-599ec18ecbac golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 diff --git a/v3/go.sum b/v3/go.sum index 0ef17583e..0d25dba65 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -1,3 +1,4 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -5,10 +6,13 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/v3/lint/base.go b/v3/lint/base.go index 7b48efac0..c5afe8a31 100644 --- a/v3/lint/base.go +++ b/v3/lint/base.go @@ -15,6 +15,7 @@ package lint */ import ( + "fmt" "time" "github.com/zmap/zcrypto/x509" @@ -34,6 +35,11 @@ type LintInterface interface { //nolint:revive Execute(c *x509.Certificate) *LintResult } +// Configurable lints return a pointer into a struct that they wish to receive their configuration into. +type Configurable interface { + Configure() interface{} +} + // A Lint struct represents a single lint, e.g. // "e_basic_constraints_not_critical". It contains an implementation of LintInterface. type Lint struct { @@ -87,14 +93,33 @@ func (l *Lint) CheckEffective(c *x509.Certificate) bool { // if they are within the purview of the BRs. See LintInterface for details // about the other methods called. The ordering is as follows: // +// Configure() ----> only if the lint implements Configurable // CheckApplies() // CheckEffective() // Execute() -func (l *Lint) Execute(cert *x509.Certificate) *LintResult { +func (l *Lint) Execute(cert *x509.Certificate, config Configuration) *LintResult { if l.Source == CABFBaselineRequirements && !util.IsServerAuthCert(cert) { return &LintResult{Status: NA} } - lint := l.Lint() + return l.execute(l.Lint(), cert, config) +} + +func (l *Lint) execute(lint LintInterface, cert *x509.Certificate, config Configuration) *LintResult { + configurable, ok := lint.(Configurable) + if ok { + err := config.Configure(configurable.Configure(), l.Name) + if err != nil { + details := fmt.Sprintf( + "A fatal error occurred while attempting to configure %s. Please visit the [%s] section of "+ + "your provided configuration and compare it with the output of `zlint -exampleConfig`. Error: %s", + l.Name, + l.Name, + err.Error()) + return &LintResult{ + Status: Fatal, + Details: details} + } + } if !lint.CheckApplies(cert) { return &LintResult{Status: NA} } else if !l.CheckEffective(cert) { diff --git a/v3/lint/configuration.go b/v3/lint/configuration.go new file mode 100644 index 000000000..d60f3aec7 --- /dev/null +++ b/v3/lint/configuration.go @@ -0,0 +1,248 @@ +/* + * ZLint Copyright 2021 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package lint + +import ( + "fmt" + "io" + "os" + "reflect" + "strings" + + "github.com/pelletier/go-toml" +) + +// Configuration is a ZLint configuration which serves as a target +// to hold the full TOML tree that is a physical ZLint configuration./ +type Configuration struct { + tree *toml.Tree +} + +// Configure attempts to deserialize the provided namespace into the provided empty interface. +// +// For example, let's say that the name of your lint is MyLint, then the configuration +// file might look something like the following... +// +// ``` +// [MyLint] +// A = 1 +// B = 2 +// ``` +// +// Given this, our target struct may look like the following... +// +// ``` +// type MytLint struct { +// A int +// B uint +// } +// ``` +// +// So deserializing into this struct would look like... +// +// ``` +// configuration.Configure(&myLint, myLint.Name()) +// ``` +func (c Configuration) Configure(lint interface{}, namespace string) error { + return c.deserializeConfigInto(lint, namespace) +} + +// NewConfig attempts to instantiate a configuration by consuming the contents of the provided reader. +// +// The contents of the provided reader MUST be in a valid TOML format. The caller of this function +// is responsible for closing the reader, if appropriate. +func NewConfig(r io.Reader) (Configuration, error) { + tree, err := toml.LoadReader(r) + if err != nil { + return Configuration{}, err + } + return Configuration{tree}, nil +} + +// NewConfigFromFile attempts to instantiate a configuration from the provided filesystem path. +// +// The file pointed to by `path` MUST be valid TOML file. If `path` is the empty string then +// an empty configuration is returned. +func NewConfigFromFile(path string) (Configuration, error) { + if path == "" { + return NewEmptyConfig(), nil + } + f, err := os.Open(path) + if err != nil { + return Configuration{}, fmt.Errorf("failed to open the provided configuration at %s. Error: %s", path, err.Error()) + } + defer f.Close() + return NewConfig(f) +} + +// NewConfigFromString attempts to instantiate a configuration from the provided string. +// +// The provided string MUST be in a valid TOML format. +func NewConfigFromString(config string) (Configuration, error) { + return NewConfig(strings.NewReader(config)) +} + +// NewEmptyConfig returns a configuration that is backed by an entirely empty TOML tree. +// +// This is useful if no particular configuration is set at all by the user of ZLint as +// any attempt to resolve a namespace in `deserializeConfigInto` fails and thus results +// in all defaults for all lints being maintained. +func NewEmptyConfig() Configuration { + cfg, _ := NewConfigFromString("") + return cfg +} + +// deserializeConfigInto deserializes the section labeled by the provided `namespace` +// into the provided target `interface{}`. +// +// For example, given the following configuration... +// +// ``` +// [e_some_lint] +// field = 1 +// flag = false +// +// [w_some_other_lint] +// is_web_pki = true +// ``` +// +// And the following struct definition... +// +// ``` +// type SomeOtherLint { +// IsWebPKI bool `toml:"is_web_pki"` +// } +// ``` +// +// Then the invocation of this function should be... +// +// ``` +// lint := &SomeOtherLint{} +// deserializeConfigInto(lint, "w_some_other_lint") +// ``` +// +// If there is no such namespace found in this configuration then provided the namespace specific data encoded +// within `target` is left unmodified. However, configuration of higher scoped fields will still be attempted. +func (c Configuration) deserializeConfigInto(target interface{}, namespace string) error { + if tree := c.tree.Get(namespace); tree != nil { + err := tree.(*toml.Tree).Unmarshal(target) + if err != nil { + return err + } + } + return c.resolveHigherScopedReferences(target) +} + +// resolveHigherScopeReferences takes in an interface{} value and attempts to +// find any field within its inner value that is either a struct or a pointer +// to a struct that is one of our global configurable types. If such a field +// exists then that higher scoped configuration will be copied into the value +// held by the provided interface{}. +// +// This procedure is recursive. +func (c Configuration) resolveHigherScopedReferences(i interface{}) error { + value := reflect.Indirect(reflect.ValueOf(i)) + if value.Kind() != reflect.Struct { + // Our target higher scoped configurations are either structs + // or are fields of structs. Any other Kind simply cannot + // be a target for deserialization here. For example, an interface + // does not make sense since an interface cannot have fields nor + // are any of our higher scoped configurations interfaces themselves. + // + // For a comprehensive list of Kinds, please see `type.go` in the `reflect` package. + return nil + } + // Iterate through every field within the struct held by the provided interface{}. + // If the field is either one of our higher scoped configurations (or a pointer to one) + // then deserialize that higher scoped configuration into that field. If the field + // is not one of our higher scoped configurations then recursively pass it to this function + // in an attempt to resolve it. + for field := 0; field < value.NumField(); field++ { + field := value.Field(field) + if !field.CanSet() { + // This skips fields that are either not addressable or are private data members. + continue + } + if _, ok := field.Interface().(GlobalConfiguration); ok { + // It's one of our higher level configurations, so we need to pull out a different + // subtree from our TOML document and inject it int othis struct. + config := initializePtr(field).Interface().(GlobalConfiguration) + err := c.deserializeConfigInto(config, config.namespace()) + if err != nil { + return err + } + field.Set(reflect.ValueOf(config)) + } else { + // This is just another member of some kind that is not one of our higher level configurations. + err := c.resolveHigherScopedReferences(field.Addr().Interface()) + if err != nil { + return err + } + } + } + return nil +} + +// stripGlobalsFromExample takes in an interface{} and returns a mapping that is +// the provided struct but with all references to higher scoped configurations scrubbed. +// +// This is intended only for use when constructing an example configuration file via the +// `-exampleConfig` flag. This is to avoid visually redundant, and possibly incorrect, +// examples such as the following... +// +// ``` +// [Global] +// something = false +// something_else = "" +// +// [e_some_lint] +// my_data = 0 +// my_flag = false +// globals = { something = false, something_else = "" } +// ``` +// +// Notice how the above has Global effectively listed twice - once externally and once internally, which +// defeats the whole point of having globals to begin with. +func stripGlobalsFromExample(i interface{}) interface{} { + value := reflect.Indirect(reflect.ValueOf(i)) + if value.Kind() != reflect.Struct { + return i + } + m := map[string]interface{}{} + for field := 0; field < value.NumField(); field++ { + name := value.Type().Field(field).Name + field := value.Field(field) + if !field.CanInterface() { + continue + } + if _, ok := field.Interface().(GlobalConfiguration); ok { + continue + } + field = initializePtr(field) + m[name] = stripGlobalsFromExample(field.Interface()) + } + return m +} + +// initializePtr checks whether the provided reflect.Value is a pointer type and is nil. If so, it returns +// a new reflect.Value that has an initialized pointer. +// +// If the provided reflect.Value is not a nil pointer, then the original reflect.Value is returned. +func initializePtr(value reflect.Value) reflect.Value { + if value.Kind() == reflect.Ptr && value.IsZero() { + return reflect.New(value.Type().Elem()) + } + return value +} diff --git a/v3/lint/configuration_test.go b/v3/lint/configuration_test.go new file mode 100644 index 000000000..e306d067e --- /dev/null +++ b/v3/lint/configuration_test.go @@ -0,0 +1,1188 @@ +/* + * ZLint Copyright 2021 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package lint + +import ( + "io" + "io/ioutil" + "os" + "reflect" + "sync" + "testing" + + "github.com/pelletier/go-toml" +) + +func TestInt(t *testing.T) { + type Test struct { + A int + } + c, err := NewConfigFromString(` +[Test] +A = 5`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if test.A != 5 { + t.Fatalf("wanted 5 got %d", test.A) + } +} + +func TestIntNegative(t *testing.T) { + type Test struct { + A int + } + c, err := NewConfigFromString(` +[Test] +A = -5`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if test.A != -5 { + t.Fatalf("wanted -5 got %d", test.A) + } +} + +func TestUint(t *testing.T) { + type Test struct { + A uint + } + c, err := NewConfigFromString(` +[Test] +A = 5`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if test.A != 5 { + t.Fatalf("wanted 5 got %d", test.A) + } +} + +func TestUintNegative(t *testing.T) { + type Test struct { + A uint + } + c, err := NewConfigFromString(` +[Test] +A = -5`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err == nil { + t.Fatalf("expected an error when deserializing a negative number into a uint, got %v", test) + } +} + +func TestSmallInt(t *testing.T) { + type Test struct { + A uint8 + } + c, err := NewConfigFromString(` +[Test] +A = 300`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err == nil { + t.Fatalf("expected an error when deserializing a number too large to fit in a uint8, got %v", test) + } +} + +func TestByte(t *testing.T) { + type Test struct { + A byte + } + c, err := NewConfigFromString(` +[Test] +A = 255`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if test.A != 255 { + t.Fatalf("wanted 255 got %d", test.A) + } +} + +func TestBool(t *testing.T) { + type Test struct { + A bool + } + c, err := NewConfigFromString(` +[Test] +A = true`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !test.A { + t.Fatalf("wanted true got %v", test.A) + } +} + +func TestString(t *testing.T) { + type Test struct { + A string + } + c, err := NewConfigFromString(` +[Test] +A = "the greatest song in the world"`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if test.A != "the greatest song in the world" { + t.Fatalf("wanted \"the greatest song in the world\" got %v", test.A) + } +} + +func TestArrayInt(t *testing.T) { + type Test struct { + A []int + } + c, err := NewConfigFromString(` +[Test] +A = [1, 2, 3, 4]`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.A, []int{1, 2, 3, 4}) { + t.Fatalf("wanted [1, 2, 3, 4] got %v", test.A) + } +} + +func TestArrayString(t *testing.T) { + type Test struct { + A []string + } + c, err := NewConfigFromString(` +[Test] +A = ["1", "2", "3", "4"]`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.A, []string{"1", "2", "3", "4"}) { + t.Fatalf("wanted [\"1\", \"2\", \"3\", \"4\"] got %v", test.A) + } +} + +func TestMapInt(t *testing.T) { + type Test struct { + A map[string]int + } + c, err := NewConfigFromString(` +[Test] +A = { version = 42 }`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.A, map[string]int{"version": 42}) { + t.Fatalf("wanted { \"version\": 42 } got %v", test.A) + } +} + +func TestMapString(t *testing.T) { + type Test struct { + A map[string]string + } + c, err := NewConfigFromString(` +[Test] +A = { version = "1.2.3" }`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.A, map[string]string{"version": "1.2.3"}) { + t.Fatalf("wanted { \"version\": \"1.2.3\" } got %v", test.A) + } +} + +func TestMapArray(t *testing.T) { + type Test struct { + A map[string][]int + } + c, err := NewConfigFromString(` +[Test] +A = { version = [1, 2 ,3] }`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.A, map[string][]int{"version": {1, 2, 3}}) { + t.Fatalf("wanted { \"version\": [1, 2 ,3] } got %v", test.A) + } +} + +func TestMapMap(t *testing.T) { + type Test struct { + A map[string]map[string]string + } + c, err := NewConfigFromString(` +[Test] +A = { version = { commit = "29c848e565ebfa2a376767919bb0880be46b3c0f" } }`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test.A, map[string]map[string]string{"version": {"commit": "29c848e565ebfa2a376767919bb0880be46b3c0f"}}) { + t.Fatalf("wanted {\"versio\": { \"commit\": \"29c848e565ebfa2a376767919bb0880be46b3c0f\" } } got %v", test.A) + } +} + +func TestStruct(t *testing.T) { + type Inner struct { + B int + } + type Test struct { + A Inner + } + c, err := NewConfigFromString(` +[Test] +A = { B = 1 }`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{Inner{1}}) { + t.Fatalf("wanted {A {1}} got %v", test) + } +} + +func TestPointer(t *testing.T) { + type Test struct { + A *int + } + c, err := NewConfigFromString(` +[Test] +A = 1`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if test.A == nil { + t.Fatal("wanted a pointer to 1, got nil") + } + if *test.A != 1 { + t.Fatalf("wanted a pointer to 1, got a point to %d", *test.A) + } +} + +func TestInterface(t *testing.T) { + type Test struct { + A bool + B io.Reader + } + c, err := NewConfigFromString(` +[Test] +A = true`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{true, nil}) { + t.Fatalf("wanted {true nil} got %v", test) + } +} + +func TestSmokeExamplePrinting(t *testing.T) { + type Inner struct { + Things []int + } + type Test struct { + A bool + B io.Reader + C *int + D Inner + } + mapping := stripGlobalsFromExample(&Test{}) + rr, w := io.Pipe() + var err error + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + defer w.Close() + err = toml.NewEncoder(w).Indentation("").CompactComments(true).Encode(mapping) + }() + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadAll(rr) + if err != nil { + t.Fatal(err) + } + want := `A = false +C = 0 + +[D] +Things = [] +` + if want != string(b) { + t.Fatalf("wanted `%s` got '%s'", want, string(b)) + } +} + +func TestRecursiveStruct(t *testing.T) { + type Test struct { + A *Test + B bool + } + c, err := NewConfigFromString(` +[Test] +A = { B = true } +B = true +`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{&Test{nil, true}, true}) { + t.Fatalf("wanted Test{&Test{nil, true}, true} got %v", test) + } +} + +func TestBadToml(t *testing.T) { + _, err := NewConfigFromString(`(┛ಠ_ಠ)┛彡┻━┻`) + if err == nil { + t.Fatal("expected a parsing, however received a nil error") + } +} + +func TestPrivateMembers(t *testing.T) { + type Test struct { + private string + NotPrivate string + } + c, err := NewConfigFromString(` +[Test] +private = "this still should not show up" +NotPrivate = "just a string" +`) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if test.private != "" { + t.Errorf("wanted '' got '%s'", test.private) + } + if test.NotPrivate != "just a string" { + t.Errorf("wanted 'just a string' got '%s'", test.NotPrivate) + } +} + +func TestEmbedGlobal(t *testing.T) { + type Test struct { + Global Global + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{Global: Global{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{Global: Global{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedRFC5280Config(t *testing.T) { + type Test struct { + RFC5280Config RFC5280Config + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{RFC5280Config: RFC5280Config{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{RFC5280Config: RFC5280Config{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedRFC5480Config(t *testing.T) { + type Test struct { + RFC5480Config RFC5480Config + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{RFC5480Config: RFC5480Config{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{RFC5480Config: RFC5480Config{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedRFC5891Config(t *testing.T) { + type Test struct { + RFC5891Config RFC5891Config + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{RFC5891Config: RFC5891Config{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{RFC5891Config: RFC5891Config{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedCABFBaselineRequirementsConfig(t *testing.T) { + type Test struct { + CABFBaselineRequirementsConfig CABFBaselineRequirementsConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{CABFBaselineRequirementsConfig: CABFBaselineRequirementsConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{CABFBaselineRequirementsConfig: CABFBaselineRequirementsConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedCABFEVGuidelinesConfig(t *testing.T) { + type Test struct { + CABFEVGuidelinesConfig CABFEVGuidelinesConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{CABFEVGuidelinesConfig: CABFEVGuidelinesConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{CABFEVGuidelinesConfig: CABFEVGuidelinesConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedMozillaRootStorePolicyConfig(t *testing.T) { + type Test struct { + MozillaRootStorePolicyConfig MozillaRootStorePolicyConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{MozillaRootStorePolicyConfig: MozillaRootStorePolicyConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{MozillaRootStorePolicyConfig: MozillaRootStorePolicyConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedAppleRootStorePolicyConfig(t *testing.T) { + type Test struct { + AppleRootStorePolicyConfig AppleRootStorePolicyConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{AppleRootStorePolicyConfig: AppleRootStorePolicyConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{AppleRootStorePolicyConfig: AppleRootStorePolicyConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedCommunityConfig(t *testing.T) { + type Test struct { + CommunityConfig CommunityConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{CommunityConfig: CommunityConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{CommunityConfig: CommunityConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedEtsiEsiConfig(t *testing.T) { + type Test struct { + EtsiEsiConfig EtsiEsiConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{EtsiEsiConfig: EtsiEsiConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{EtsiEsiConfig: EtsiEsiConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToGlobal(t *testing.T) { + type Test struct { + Global *Global + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{Global: &Global{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{Global: &Global{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToRFC5280Config(t *testing.T) { + type Test struct { + RFC5280Config *RFC5280Config + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{RFC5280Config: &RFC5280Config{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{RFC5280Config: &RFC5280Config{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToRFC5480Config(t *testing.T) { + type Test struct { + RFC5480Config *RFC5480Config + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{RFC5480Config: &RFC5480Config{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{RFC5480Config: &RFC5480Config{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToRFC5891Config(t *testing.T) { + type Test struct { + RFC5891Config *RFC5891Config + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{RFC5891Config: &RFC5891Config{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{RFC5891Config: &RFC5891Config{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToCABFBaselineRequirementsConfig(t *testing.T) { + type Test struct { + CABFBaselineRequirementsConfig *CABFBaselineRequirementsConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{CABFBaselineRequirementsConfig: &CABFBaselineRequirementsConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{CABFBaselineRequirementsConfig: &CABFBaselineRequirementsConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToCABFEVGuidelinesConfig(t *testing.T) { + type Test struct { + CABFEVGuidelinesConfig *CABFEVGuidelinesConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{CABFEVGuidelinesConfig: &CABFEVGuidelinesConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{CABFEVGuidelinesConfig: &CABFEVGuidelinesConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToMozillaRootStorePolicyConfig(t *testing.T) { + type Test struct { + MozillaRootStorePolicyConfig *MozillaRootStorePolicyConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{MozillaRootStorePolicyConfig: &MozillaRootStorePolicyConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{MozillaRootStorePolicyConfig: &MozillaRootStorePolicyConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToAppleRootStorePolicyConfig(t *testing.T) { + type Test struct { + AppleRootStorePolicyConfig *AppleRootStorePolicyConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{AppleRootStorePolicyConfig: &AppleRootStorePolicyConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{AppleRootStorePolicyConfig: &AppleRootStorePolicyConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToCommunityConfig(t *testing.T) { + type Test struct { + CommunityConfig *CommunityConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{CommunityConfig: &CommunityConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{CommunityConfig: &CommunityConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestEmbedPtrToEtsiEsiConfig(t *testing.T) { + type Test struct { + EtsiEsiConfig *EtsiEsiConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{EtsiEsiConfig: &EtsiEsiConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{EtsiEsiConfig: &EtsiEsiConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestGlobalStripper(t *testing.T) { + type Test struct { + EtsiEsiConfig *EtsiEsiConfig + SomethingElse string + } + c, err := NewConfigFromString(` + [Test] + SomethingElse = "cool" + `) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{EtsiEsiConfig: &EtsiEsiConfig{}, SomethingElse: "cool"}) { + t.Fatalf("wanted Test{EtsiEsiConfig: &EtsiEsiConfig{}, SomethingElse: \"cool\"}} got %v", test) + } +} + +func TestPrintConfiguration(t *testing.T) { + gotBytes, err := NewRegistry().DefaultConfiguration() + if err != nil { + t.Fatal(err) + } + got := string(gotBytes) + // I'm not a huge fan of this sort of test since it will have to be updated + // on the slightest change, but it's better than not have a test for printing + // out the configuration file. + want := ` +[AppleRootStorePolicyConfig] + +[CABFBaselineRequirementsConfig] + +[CABFEVGuidelinesConfig] + +[CommunityConfig] + +[MozillaRootStorePolicyConfig] + +[RFC5280Config] + +[RFC5480Config] + +[RFC5891Config] +` + if got != want { + t.Fatalf("wanted '%s' but got '%s'", want, got) + } +} + +type TestGlobalConfigurable struct { + A int + B string +} + +func (t *TestGlobalConfigurable) namespace() string { + return "this_is_a_test" +} + +func TestNewGlobal(t *testing.T) { + type test struct { + SomethingElse string `toml:"something_else"` + T *TestGlobalConfigurable + } + c, err := NewConfigFromString(` +[this_is_a_test] +A = 1 +B = "the temples of syrinx" + +[Test] +something_else = "fills our hallowed halls" +`) + if err != nil { + t.Fatal(err) + } + got := test{} + err = c.Configure(&got, "Test") + if err != nil { + t.Fatal(err) + } + if got.SomethingElse != "fills our hallowed halls" { + t.Errorf("got '%s' want 'fills our hallowed halls", got.SomethingElse) + } + if got.T.A != 1 { + t.Errorf("got %d want 1", got.T.A) + } + if got.T.B != "the temples of syrinx" { + t.Errorf("got '%s' want 'the temples of syrinx", got.T.B) + } +} + +type TestGlobalConfigurableWithPrivates struct { + A int + B string + c string +} + +func (t *TestGlobalConfigurableWithPrivates) namespace() string { + return "this_is_a_test" +} + +func TestNewGlobalWithPrivateMembersDontGetPrinted(t *testing.T) { + gotBytes, err := NewRegistry().defaultConfiguration([]GlobalConfiguration{&TestGlobalConfigurableWithPrivates{ + 1, "2", "3", + }}) + if err != nil { + t.Fatal(err) + } + got := string(gotBytes) + // I'm not a huge fan of this sort of test since it will have to be updated + // on the slightest change, but it's better than not have a test for printing + // out the configuration file. + want := ` +[this_is_a_test] +A = 1 +B = "2" +` + if got != want { + t.Fatalf("wanted '%s' but got '%s'", want, got) + } +} + +func TestFailedGlobalDeser(t *testing.T) { + type test struct { + SomethingElse string `toml:"something_else"` + T *TestGlobalConfigurable + } + c, err := NewConfigFromString(` +[this_is_a_test] +A = "1" # It should be an int, not a string +B = "the temples of syrinx" + +[Test] +something_else = "fills our hallowed halls" +`) + if err != nil { + t.Fatal(err) + } + got := test{} + err = c.Configure(&got, "Test") + if err == nil { + t.Fatalf("expected error, but got %v", got) + } +} + +func TestFailedNestedGlobalDeser(t *testing.T) { + type test struct { + SomethingElse string `toml:"something_else"` + Inner struct { + T *TestGlobalConfigurable + } + } + c, err := NewConfigFromString(` +[this_is_a_test] +A = "1" # It should be an int, not a string +B = "the temples of syrinx" + +[Test] +something_else = "fills our hallowed halls" +`) + if err != nil { + t.Fatal(err) + } + got := test{} + err = c.Configure(&got, "Test") + if err == nil { + t.Fatalf("expected error, but got %v", got) + } +} + +func TestStripGlobalsFromStructWithPrivates(t *testing.T) { + //nolint:staticheck + type Test struct { + A string + B Global + C int + d int + } + test := Test{} + got := stripGlobalsFromExample(&test).(map[string]interface{}) + want := map[string]interface{}{ + "A": "", + "C": 0, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("wanted map[A: C:0], got %v", got) + } +} + +func TestNewEmptyConfig(t *testing.T) { + c := NewEmptyConfig() + got, err := c.tree.Marshal() + if err != nil { + t.Fatal(err) + } + if got != nil { + t.Fatalf("wanted nil byte slice, got %s", string(got)) + } +} + +func TestConfigFromFile(t *testing.T) { + type Test struct { + A *Test + B bool + } + f, err := os.CreateTemp("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + _, err = f.WriteString(` +[Test] +A = { B = true } +B = true +`) + if err != nil { + f.Close() + t.Fatal(err) + } + err = f.Close() + if err != nil { + t.Fatal(err) + } + c, err := NewConfigFromFile(f.Name()) + if err != nil { + t.Fatal(err) + } + test := Test{} + err = c.Configure(&test, "Test") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(test, Test{&Test{nil, true}, true}) { + t.Fatalf("wanted Test{&Test{nil, true}, true} got %v", test) + } +} + +func TestBadConfigFromFile(t *testing.T) { + f, err := os.CreateTemp("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + _, err = f.WriteString(` +nope not gonna work +[Test] +A = { B = true } +B = true +`) + if err != nil { + f.Close() + t.Fatal(err) + } + err = f.Close() + if err != nil { + t.Fatal(err) + } + c, err := NewConfigFromFile(f.Name()) + if err == nil { + t.Fatalf("expected error, got %v", c) + } +} + +func TestEmptyConfigFromEmptyPath(t *testing.T) { + c, err := NewConfigFromFile("") + if err != nil { + t.Fatal(err) + } + got, err := c.tree.Marshal() + if err != nil { + t.Fatal(err) + } + if got != nil { + t.Fatalf("wanted nil byte slice, got %s", string(got)) + } +} + +func TestFailedToOpenConfigFile(t *testing.T) { + c, err := NewConfigFromFile("lol no not likely") + if err == nil { + t.Fatalf("expected an error got %v", c) + } +} diff --git a/v3/lint/global_configurations.go b/v3/lint/global_configurations.go new file mode 100644 index 000000000..826734ed3 --- /dev/null +++ b/v3/lint/global_configurations.go @@ -0,0 +1,155 @@ +/* + * ZLint Copyright 2021 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package lint + +// Global is what one would intuitive think of as being the global context of the configuration file. +// That is, given the following configuration... +// +// some_flag = true +// some_string = "the greatest song in the world" +// +// [e_some_lint] +// some_other_flag = false +// +// The fields `some_flag` and `some_string` will be targeted to land into this struct. +type Global struct{} + +func (g Global) namespace() string { + return "Global" +} + +// RFC5280Config is the higher scoped configuration which services as the deserialization target for... +// +// [RFC5280Config] +// ... +// ... +type RFC5280Config struct{} + +func (r RFC5280Config) namespace() string { + return "RFC5280Config" +} + +// RFC5480Config is the higher scoped configuration which services as the deserialization target for... +// +// [RFC5480Config] +// ... +// ... +type RFC5480Config struct{} + +func (r RFC5480Config) namespace() string { + return "RFC5480Config" +} + +// RFC5891Config is the higher scoped configuration which services as the deserialization target for... +// +// [RFC5891Config] +// ... +// ... +type RFC5891Config struct{} + +func (r RFC5891Config) namespace() string { + return "RFC5891Config" +} + +// CABFBaselineRequirementsConfig is the higher scoped configuration which services as the deserialization target for... +// +// [CABFBaselineRequirementsConfig] +// ... +// ... +type CABFBaselineRequirementsConfig struct{} + +func (c CABFBaselineRequirementsConfig) namespace() string { + return "CABFBaselineRequirementsConfig" +} + +// CABFEVGuidelinesConfig is the higher scoped configuration which services as the deserialization target for... +// +// [CABFEVGuidelinesConfig] +// ... +// ... +type CABFEVGuidelinesConfig struct{} + +func (c CABFEVGuidelinesConfig) namespace() string { + return "CABFEVGuidelinesConfig" +} + +// MozillaRootStorePolicyConfig is the higher scoped configuration which services as the deserialization target for... +// +// [MozillaRootStorePolicyConfig] +// ... +// ... +type MozillaRootStorePolicyConfig struct{} + +func (m MozillaRootStorePolicyConfig) namespace() string { + return "MozillaRootStorePolicyConfig" +} + +// AppleRootStorePolicyConfig is the higher scoped configuration which services as the deserialization target for... +// +// [AppleRootStorePolicyConfig] +// ... +// ... +type AppleRootStorePolicyConfig struct{} + +func (a AppleRootStorePolicyConfig) namespace() string { + return "AppleRootStorePolicyConfig" +} + +// CommunityConfig is the higher scoped configuration which services as the deserialization target for... +// +// [CommunityConfig] +// ... +// ... +type CommunityConfig struct{} + +func (c CommunityConfig) namespace() string { + return "CommunityConfig" +} + +// EtsiEsiConfig is the higher scoped configuration which services as the deserialization target for... +// +// [EtsiEsiConfig] +// ... +// ... +type EtsiEsiConfig struct{} + +func (e EtsiEsiConfig) namespace() string { + return "EtsiEsiConfig" +} + +// GlobalConfiguration acts both as an interface that can be used to obtain the TOML namespace of configuration +// as well as a way to mark a fielf in a struct as one of our own, higher scoped, configurations. +// +// the interface itself is public, however the singular `namespace` method is package private, meaning that +// normal lint struct cannot accidentally implement this. +type GlobalConfiguration interface { + namespace() string +} + +// defaultGlobals are used by other locations in the codebase that may want to iterate over all currently know +// global configuration types. Most notably, Registry.DefaultConfiguration uses it because it wants to print +// out a TOML document that is the full default configuration for ZLint. +var defaultGlobals = []GlobalConfiguration{ + &Global{}, + &CABFBaselineRequirementsConfig{}, + &RFC5280Config{}, + &RFC5480Config{}, + &RFC5891Config{}, + &CABFBaselineRequirementsConfig{}, + &CABFEVGuidelinesConfig{}, + &MozillaRootStorePolicyConfig{}, + &AppleRootStorePolicyConfig{}, + &CommunityConfig{}, +} diff --git a/v3/lint/registration.go b/v3/lint/registration.go index 11ac6ceea..2d6d96939 100644 --- a/v3/lint/registration.go +++ b/v3/lint/registration.go @@ -15,6 +15,7 @@ package lint import ( + "bytes" "encoding/json" "errors" "fmt" @@ -23,6 +24,8 @@ import ( "sort" "strings" "sync" + + "github.com/pelletier/go-toml" ) // FilterOptions is a struct used by Registry.Filter to create a sub registry @@ -75,6 +78,8 @@ type Registry interface { // Sources returns a SourceList of registered LintSources. The list is not // sorted but can be sorted by the caller with sort.Sort() if required. Sources() SourceList + // @TODO + DefaultConfiguration() ([]byte, error) // ByName returns a pointer to the registered lint with the given name, or nil // if there is no such lint registered in the registry. ByName(name string) *Lint @@ -87,6 +92,8 @@ type Registry interface { // WriteJSON writes a description of each registered lint as // a JSON object, one object per line, to the provided writer. WriteJSON(w io.Writer) + SetConfiguration(config Configuration) + GetConfiguration() Configuration } // registryImpl implements the Registry interface to provide a global collection @@ -102,6 +109,7 @@ type registryImpl struct { // lintsBySource is a map of all registered lints by source category. Lints // are added to the lintsBySource map by RegisterLint. lintsBySource map[LintSource][]*Lint + configuration Configuration } var ( @@ -136,7 +144,7 @@ func (r *registryImpl) register(l *Lint) error { if l == nil { return errNilLint } - if l.Lint == nil { + if l.Lint() == nil { return errNilLintPtr } if l.Name == "" { @@ -233,6 +241,7 @@ func (r *registryImpl) Filter(opts FilterOptions) (Registry, error) { } filteredRegistry := NewRegistry() + filteredRegistry.SetConfiguration(r.configuration) sourceExcludes := sourceListToMap(opts.ExcludeSources) sourceIncludes := sourceListToMap(opts.IncludeSources) @@ -292,14 +301,73 @@ func (r *registryImpl) WriteJSON(w io.Writer) { } } +func (r *registryImpl) SetConfiguration(cfg Configuration) { + r.configuration = cfg +} + +func (r *registryImpl) GetConfiguration() Configuration { + return r.configuration +} + +// DefaultConfiguration returns a serialized copy of the default configuration for ZLint. +// +// This is especially useful combined with the -exampleConfig CLI argument which prints this +// to stdout. In this way, operators can quickly see what lints are configurable and what their +// fields are without having to dig through documentation or, even worse, code. +func (r *registryImpl) DefaultConfiguration() ([]byte, error) { + return r.defaultConfiguration(defaultGlobals) +} + +// defaultConfiguration is abstracted out to a private function that takes in a slice of globals +// for the sake of making unit testing easier. +func (r *registryImpl) defaultConfiguration(globals []GlobalConfiguration) ([]byte, error) { + configurables := map[string]interface{}{} + for name, lint := range r.lintsByName { + switch configurable := lint.Lint().(type) { + case Configurable: + configurables[name] = stripGlobalsFromExample(configurable.Configure()) + default: + } + } + for _, config := range globals { + switch config.(type) { + case *Global: + // We're just using stripGlobalsFromExample here as a convenient way to + // recursively turn the `Global` struct type into a map. + // + // We have to do this because if we simply followed the pattern above and did... + // + // configurables["Global"] = &Global{} + // + // ...then we would end up with a [Global] section in the resulting configuration, + // which is not what we are looking for (we simply want it to be flattened out into + // the top most context of the configuration file). + for k, v := range stripGlobalsFromExample(config).(map[string]interface{}) { + configurables[k] = v + } + default: + configurables[config.namespace()] = config + } + + } + w := &bytes.Buffer{} + err := toml.NewEncoder(w).Indentation("").CompactComments(true).Encode(configurables) + if err != nil { + return nil, err + } + return w.Bytes(), nil +} + // NewRegistry constructs a Registry implementation that can be used to register // lints. //nolint:revive func NewRegistry() *registryImpl { - return ®istryImpl{ + registry := ®istryImpl{ lintsByName: make(map[string]*Lint), lintsBySource: make(map[LintSource][]*Lint), } + registry.SetConfiguration(NewEmptyConfig()) + return registry } // globalRegistry is the Registry used by all loaded lints that call diff --git a/v3/lint/registration_test.go b/v3/lint/registration_test.go index 3976b69ce..615afb319 100644 --- a/v3/lint/registration_test.go +++ b/v3/lint/registration_test.go @@ -92,8 +92,10 @@ func TestRegister(t *testing.T) { expectErr: errNilLint, }, { - name: "nil lint ptr", - lint: &Lint{}, + name: "nil lint ptr", + lint: &Lint{ + Lint: func() LintInterface { return nil }, + }, expectErr: errNilLintPtr, }, { diff --git a/v3/lints/cabf_br/lint_dsa_correct_order_in_subgroup_test.go b/v3/lints/cabf_br/lint_dsa_correct_order_in_subgroup_test.go index c271caed0..97c8c09b6 100644 --- a/v3/lints/cabf_br/lint_dsa_correct_order_in_subgroup_test.go +++ b/v3/lints/cabf_br/lint_dsa_correct_order_in_subgroup_test.go @@ -41,7 +41,7 @@ func TestDSANotCorrectOrderSubgroup(t *testing.T) { pMinusOne.Sub(dsaKey.P, big.NewInt(1)) dsaKey.Y = pMinusOne expected := lint.Error - out := test.TestLintCert("e_dsa_correct_order_in_subgroup", c) + out := test.TestLintCert("e_dsa_correct_order_in_subgroup", c, lint.NewEmptyConfig()) if out.Status != expected { t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) } diff --git a/v3/lints/cabf_br/lint_dsa_unique_correct_representation_test.go b/v3/lints/cabf_br/lint_dsa_unique_correct_representation_test.go index 990fedc6d..b663f565a 100644 --- a/v3/lints/cabf_br/lint_dsa_unique_correct_representation_test.go +++ b/v3/lints/cabf_br/lint_dsa_unique_correct_representation_test.go @@ -45,7 +45,7 @@ func TestDSANotUniqueCorrectRepresentation(t *testing.T) { // Expect failure expected := lint.Error - out := test.TestLintCert("e_dsa_unique_correct_representation", c) + out := test.TestLintCert("e_dsa_unique_correct_representation", c, lint.NewEmptyConfig()) if out.Status != expected { t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) } diff --git a/v3/resultset.go b/v3/resultset.go index f1e4db3c9..0fb3c7594 100644 --- a/v3/resultset.go +++ b/v3/resultset.go @@ -38,7 +38,7 @@ func (z *ResultSet) execute(cert *x509.Certificate, registry lint.Registry) { z.Results = make(map[string]*lint.LintResult, len(registry.Names())) // Run each lints from the registry. for _, name := range registry.Names() { - res := registry.ByName(name).Execute(cert) + res := registry.ByName(name).Execute(cert, registry.GetConfiguration()) z.Results[name] = res z.updateErrorStatePresent(res) } diff --git a/v3/test/configuration_test_framework_test.go b/v3/test/configuration_test_framework_test.go new file mode 100644 index 000000000..688a2f832 --- /dev/null +++ b/v3/test/configuration_test_framework_test.go @@ -0,0 +1,203 @@ +package test + +/* + * ZLint Copyright 2021 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import ( + "fmt" + "math/rand" + "strconv" + "sync" + "testing" + + "github.com/zmap/zcrypto/x509" + "github.com/zmap/zlint/v3/util" + + "github.com/zmap/zlint/v3/lint" +) + +type caCommonNameMissing struct { + BeerHall string + Working *lint.CABFBaselineRequirementsConfig +} + +func init() { + lint.RegisterLint(&lint.Lint{ + Name: "e_ca_common_name_missing2", + Description: "CA Certificates common name MUST be included.", + Citation: "BRs: 7.1.4.3.1", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABV148Date, + Lint: NewCaCommonNameMissing, + }) +} + +func (l *caCommonNameMissing) Configure() interface{} { + return l +} + +func NewCaCommonNameMissing() lint.LintInterface { + return &caCommonNameMissing{} +} + +func (l *caCommonNameMissing) CheckApplies(c *x509.Certificate) bool { + return util.IsCACert(c) +} + +func (l *caCommonNameMissing) Execute(c *x509.Certificate) *lint.LintResult { + if c.Subject.CommonName == "" { + return &lint.LintResult{Status: lint.Error, Details: l.BeerHall} + } else { + return &lint.LintResult{Status: lint.Pass, Details: l.BeerHall} + } +} + +func TestCaCommonNameMissing(t *testing.T) { + inputPath := "caCommonNameMissing.pem" + expected := lint.Error + out := TestLint("e_ca_common_name_missing2", inputPath) + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } +} + +func TestCaCommonNameNotMissing(t *testing.T) { + inputPath := "caCommonNameNotMissing.pem" + expected := lint.Pass + out := TestLint("e_ca_common_name_missing2", inputPath) + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } +} + +func TestCaCommonNameNotMissing2(t *testing.T) { + inputPath := "caCommonNameNotMissing.pem" + expected := lint.Pass + config := ` +[e_ca_common_name_missing2] +BeerHall = "liedershousen" +` + out := TestLintWithConfig("e_ca_common_name_missing2", inputPath, config) + if out.Details != "liedershousen" { + t.Fatalf("unexpected output details, got '%s' want %s", out.Details, "liedershousen") + } + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } +} + +func TestCaCommonNameNotMissing3(t *testing.T) { + inputPath := "caCommonNameNotMissing.pem" + expected := lint.Pass + config := ` +[e_ca_common_name_missing2] +BeerHall = "liedershousenssss" +` + out := TestLintWithConfig("e_ca_common_name_missing2", inputPath, config) + if out.Details != "liedershousenssss" { + t.Fatalf("unexpected output details, got '%s' want %s", out.Details, "liedershousenssss") + } + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } +} + +// This exercises the thread safety our configurable lints. This is because +// the lints use to be global singletons before we swapped them over to +// running as single instances. However, it is a good exercise to keep around. +func TestConcurrency(t *testing.T) { + inputPath := "caCommonNameNotMissing.pem" + expected := lint.Pass + wg := sync.WaitGroup{} + wg.Add(1000) + for i := 0; i < 1000; i++ { + go func() { + defer wg.Done() + num := strconv.Itoa(rand.Intn(9999)) + config := fmt.Sprintf(` +[e_ca_common_name_missing2] +BeerHall = "%s" +`, num) + out := TestLintWithConfig("e_ca_common_name_missing2", inputPath, config) + if out.Details != num { + t.Errorf("wanted %s got %s", num, out.Details) + } + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } + }() + } + wg.Wait() +} + +func TestCaCommonNameNotMissing4(t *testing.T) { + inputPath := "caCommonNameNotMissing.pem" + expected := lint.Pass + config := ` +[CABF_BR] +DoesItWork = "yes, yes it does" + +[e_ca_common_name_missing2] +BeerHall = "liedershousenssss" +` + out := TestLintWithConfig("e_ca_common_name_missing2", inputPath, config) + if out.Status != expected { + t.Errorf("%s: expected %s, got %s", inputPath, expected, out.Status) + } + if out.Details != "liedershousenssss" { + t.Fatalf("unexpected output details, got '%s' want %s", out.Details, "liedershousenssss") + } +} + +type LintEmbedsAConfiguration struct { + configuration embeddedConfiguration + SomeOtherFieldThatWeDontWantToExpose int +} + +type embeddedConfiguration struct { + IsWebPKI bool `toml:"is_web_pki" comment:"Indicates that the certificate is intended for the Web PKI."` +} + +func init() { + lint.RegisterLint(&lint.Lint{ + Name: "w_web_pki_cert", + Description: "CA Certificates SHOULD....something....about the web pki", + Citation: "BRs: 7.1.4.3.1", + Source: lint.CABFBaselineRequirements, + EffectiveDate: util.CABV148Date, + Lint: NewLintEmbedsAConfiguration, + }) +} + +// A pointer to an embedded struct may be passed to the framework +// if the author does not wish to expose certain fields in their primary struct. +func (l *LintEmbedsAConfiguration) Configure() interface{} { + return &l.configuration +} + +func NewLintEmbedsAConfiguration() lint.LintInterface { + return &LintEmbedsAConfiguration{configuration: embeddedConfiguration{}} +} + +func (l *LintEmbedsAConfiguration) CheckApplies(c *x509.Certificate) bool { + return util.IsCACert(c) +} + +func (l *LintEmbedsAConfiguration) Execute(c *x509.Certificate) *lint.LintResult { + if l.configuration.IsWebPKI { + return &lint.LintResult{Status: lint.Warn, Details: "Time for a beer run!"} + } else { + return &lint.LintResult{Status: lint.Pass} + } +} diff --git a/v3/test/helpers.go b/v3/test/helpers.go index d6841939f..1f2428989 100644 --- a/v3/test/helpers.go +++ b/v3/test/helpers.go @@ -17,9 +17,13 @@ package test // Contains resources necessary to the Unit Test Cases import ( + "bytes" "encoding/pem" "fmt" "os" + + "os/exec" + "path" "strings" "github.com/zmap/zcrypto/x509" @@ -35,7 +39,15 @@ import ( // lint result is nil. //nolint:revive func TestLint(lintName string, testCertFilename string) *lint.LintResult { - return TestLintCert(lintName, ReadTestCert(testCertFilename)) + return TestLintWithConfig(lintName, testCertFilename, "") +} + +func TestLintWithConfig(lintName string, testCertFilename string, configuration string) *lint.LintResult { + config, err := lint.NewConfigFromString(configuration) + if err != nil { + panic(err) + } + return TestLintCert(lintName, ReadTestCert(testCertFilename), config) } // TestLintCert executes a lint with the given name against an already parsed @@ -45,7 +57,7 @@ func TestLint(lintName string, testCertFilename string) *lint.LintResult { // Important: TestLintCert is only appropriate for unit tests. It will panic if // the lintName is not known or if the lint result is nil. //nolint:revive -func TestLintCert(lintName string, cert *x509.Certificate) *lint.LintResult { +func TestLintCert(lintName string, cert *x509.Certificate, ctx lint.Configuration) *lint.LintResult { l := lint.GlobalRegistry().ByName(lintName) if l == nil { panic(fmt.Sprintf( @@ -53,8 +65,7 @@ func TestLintCert(lintName string, cert *x509.Certificate) *lint.LintResult { "Did you forget to RegisterLint?\n", lintName)) } - - res := l.Execute(cert) + res := l.Execute(cert, ctx) // We never expect a lint to return a nil LintResult if res == nil { panic(fmt.Sprintf( @@ -64,13 +75,23 @@ func TestLintCert(lintName string, cert *x509.Certificate) *lint.LintResult { return res } +var testDir = "" + // ReadTestCert loads a x509.Certificate from the given inPath which is assumed // to be relative to `testdata/`. // // Important: ReadTestCert is only appropriate for unit tests. It will panic if // the inPath file can not be loaded. func ReadTestCert(inPath string) *x509.Certificate { - fullPath := fmt.Sprintf("../../testdata/%s", inPath) + if testDir == "" { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + out, err := cmd.CombinedOutput() + if err != nil { + panic(fmt.Sprintf("error when attempting to find the root directory of the repository: %v, output: '%s'", err, out)) + } + testDir = path.Join(string(bytes.TrimSpace(out)), "v3", "testdata") + } + fullPath := path.Join(testDir, inPath) data, err := os.ReadFile(fullPath) if err != nil { diff --git a/v3/zlint_test.go b/v3/zlint_test.go index f791ff894..2779c11c9 100644 --- a/v3/zlint_test.go +++ b/v3/zlint_test.go @@ -1,9 +1,15 @@ package zlint import ( + "fmt" + "reflect" "strings" "testing" + "time" + "github.com/zmap/zlint/v3/util" + + "github.com/zmap/zcrypto/x509" "github.com/zmap/zlint/v3/lint" ) @@ -28,3 +34,115 @@ func TestLintNames(t *testing.T) { } } } + +type configurableTestLint struct { + A string + B int + C map[string]string + wantA string + wantB int + wantC map[string]string +} + +func NewConfigurableTestLint() lint.LintInterface { + return &configurableTestLint{C: make(map[string]string, 0), wantC: make(map[string]string, 0)} +} + +func (l *configurableTestLint) Configure() interface{} { + return l +} + +func (l *configurableTestLint) CheckApplies(c *x509.Certificate) bool { + return true +} + +func (l *configurableTestLint) Execute(c *x509.Certificate) *lint.LintResult { + if l.A != l.wantA { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("A got %v, want %v", l.A, l.wantA)} + } + if l.B != l.wantB { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("B got %v, want %v", l.B, l.wantB)} + } + if !reflect.DeepEqual(l.C, l.wantC) { + return &lint.LintResult{Status: lint.Error, Details: fmt.Sprintf("C got %v, want %v", l.C, l.wantC)} + } + return &lint.LintResult{Status: lint.Pass} +} + +func TestWithDefaultConfiguration(t *testing.T) { + lint.RegisterLint(&lint.Lint{ + Name: "library_usage_test_default_config", + Description: "CA Certificates subject field MUST not be empty and MUST have a non-empty distinguished name", + Citation: "RFC 5280: 4.1.2.6", + Source: lint.RFC5280, + EffectiveDate: util.RFC2459Date, + Lint: NewConfigurableTestLint, + }) + registry, err := lint.GlobalRegistry().Filter(lint.FilterOptions{ + IncludeNames: []string{"library_usage_test_default_config"}, + }) + if err != nil { + t.Fatal(err) + } + got := LintCertificateEx(&x509.Certificate{ + NotAfter: time.Now().Add(time.Hour), + NotBefore: time.Now().Add(-time.Hour), + }, registry) + result, ok := got.Results["library_usage_test_default_config"] + if !ok { + t.Fatal("no results found, perhaps the lint never ran?") + } + if result.Status != lint.Pass { + t.Fatalf("expected lint to pass, got %v (%s)", result.Status, result.Details) + } +} + +func TestWithNonDefaultConfiguration(t *testing.T) { + lint.RegisterLint(&lint.Lint{ + Name: "library_usage_test_non_default_config", + Description: "CA Certificates subject field MUST not be empty and MUST have a non-empty distinguished name", + Citation: "RFC 5280: 4.1.2.6", + Source: lint.RFC5280, + EffectiveDate: util.RFC2459Date, + Lint: func() lint.LintInterface { + return &configurableTestLint{ + C: make(map[string]string, 0), + wantA: "the greatest song in the world", + wantB: 42, + wantC: map[string]string{ + "something": "else", + "anything": "at all", + }} + }, + }) + registry, err := lint.GlobalRegistry().Filter(lint.FilterOptions{ + IncludeNames: []string{"library_usage_test_non_default_config"}, + }) + if err != nil { + t.Fatal(err) + } + config, err := lint.NewConfigFromString(` +[library_usage_test_non_default_config] +A = "the greatest song in the world" +B = 42 + +[library_usage_test_non_default_config.C] +something = "else" +anything = "at all" +`) + if err != nil { + t.Fatal(err) + } + registry.SetConfiguration(config) + got := LintCertificateEx(&x509.Certificate{ + NotAfter: time.Now().Add(time.Hour), + NotBefore: time.Now().Add(-time.Hour), + }, registry) + result, ok := got.Results["library_usage_test_non_default_config"] + if !ok { + t.Fatal("no results found, perhaps the lint never ran?") + } + if result.Status != lint.Pass { + t.Fatalf("expected lint to pass, got %v (%s)", result.Status, result.Details) + } +}