Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zlint: refactor lint reg., allow filtering lints used. #372

Merged
merged 9 commits into from
Feb 11, 2020
17 changes: 10 additions & 7 deletions benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,20 @@ func BenchmarkZlint(b *testing.B) {
globalLintResult = lintResult
})

names := lint.GlobalRegistry().Names()

b.Run("Fast lints", func(b *testing.B) {
globalLintResult = &ResultSet{}
globalLintResult.Results = make(map[string]*lint.LintResult, len(lint.Lints))
globalLintResult.Results = make(map[string]*lint.LintResult, len(names))
b.ResetTimer()
for i := 0; i < b.N; i++ {
for key, value := range lint.Lints {
for _, key := range names {
switch key {
case "w_dnsname_underscore_in_trd", "e_dnsname_underscore_in_sld", "e_dnsname_hyphen_in_sld",
"w_dnsname_wildcard_left_of_public_suffix", "w_san_iana_pub_suffix_empty":
continue
}

value := lint.GlobalRegistry().ByName(key)
if !value.Lint.CheckApplies(x509Cert) {
continue
}
Expand All @@ -131,10 +133,10 @@ func BenchmarkZlint(b *testing.B) {

b.Run("Fastest lints", func(b *testing.B) {
globalLintResult = &ResultSet{}
globalLintResult.Results = make(map[string]*lint.LintResult, len(lint.Lints))
globalLintResult.Results = make(map[string]*lint.LintResult, len(names))
b.ResetTimer()
for i := 0; i < b.N; i++ {
for key, value := range lint.Lints {
for _, key := range names {
switch key {
case "w_dnsname_underscore_in_trd", "e_dnsname_underscore_in_sld", "e_dnsname_hyphen_in_sld",
"w_dnsname_wildcard_left_of_public_suffix", "w_san_iana_pub_suffix_empty",
Expand All @@ -148,7 +150,7 @@ func BenchmarkZlint(b *testing.B) {
"e_path_len_constraint_zero_or_less", "e_san_dns_name_includes_null_char":
continue
}

value := lint.GlobalRegistry().ByName(key)
if !value.Lint.CheckApplies(x509Cert) {
continue
}
Expand All @@ -157,8 +159,9 @@ func BenchmarkZlint(b *testing.B) {
}
})

for key, value := range lint.Lints {
for _, key := range names {
b.Run(key, func(b *testing.B) {
value := lint.GlobalRegistry().ByName(key)
if !value.Lint.CheckApplies(x509Cert) {
b.Skip("Check doesn't apply")
}
Expand Down
131 changes: 64 additions & 67 deletions cmd/zlint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"io/ioutil"
"os"
"regexp"
"sort"
"strings"

Expand All @@ -34,22 +35,28 @@ import (

var ( // flags
listLintsJSON bool
listLintsSchema bool
listLintSources bool
prettyprint bool
format string
include string
exclude string
nameFilter string
includeNames string
excludeNames string
includeSources string
excludeSources string

// version is replaced by GoReleaser using an LDFlags option at release time.
version = "dev"
)

func init() {
flag.BoolVar(&listLintsJSON, "list-lints-json", false, "Print supported lints in JSON format, one per line")
flag.BoolVar(&listLintsSchema, "list-lints-schema", false, "Print supported lints as a ZSchema")
flag.BoolVar(&listLintsJSON, "list-lints-json", false, "Print lints in JSON format, one per line")
flag.BoolVar(&listLintSources, "list-lints-source", false, "Print list of lint sources, one per line")
flag.StringVar(&format, "format", "pem", "One of {pem, der, base64}")
flag.StringVar(&include, "include", "", "Comma-separated list of lints to include by name")
flag.StringVar(&exclude, "exclude", "", "Comma-separated list of lints to exclude by name")
flag.StringVar(&nameFilter, "nameFilter", "", "Only run lints with a name matching the provided regex. (Can not be used with -includeNames/-excludeNames)")
flag.StringVar(&includeNames, "includeNames", "", "Comma-separated list of lints to include by name")
flag.StringVar(&excludeNames, "excludeNames", "", "Comma-separated list of lints to exclude by name")
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(&prettyprint, "pretty", false, "Pretty-print output")
flag.Usage = func() {
Expand All @@ -62,32 +69,30 @@ func init() {
}

func main() {
// Build a registry of lints using the include/exclude lint name and source
// flags.
registry, err := setLints()
if err != nil {
log.Fatalf("unable to configure included/exclude lints: %v\n", err)
}

if listLintsJSON {
zlint.EncodeLintDescriptionsToJSON(os.Stdout)
registry.WriteJSON(os.Stdout)
return
}

if listLintsSchema {
names := make([]string, 0, len(lint.Lints))
for lintName := range lint.Lints {
names = append(names, lintName)
}
sort.Strings(names)
fmt.Printf("Lints = SubRecord({\n")
for _, lintName := range names {
fmt.Printf(" \"%s\":LintBool(),\n", lintName)
if listLintSources {
sources := registry.Sources()
sort.Sort(sources)
for _, source := range sources {
fmt.Printf(" %s\n", source)
}
fmt.Printf("})\n")
return
}

// include/exclude lints based on flags
setLints()

var inform = strings.ToLower(format)
if flag.NArg() < 1 || flag.Arg(0) == "-" {
doLint(os.Stdin, inform)
doLint(os.Stdin, inform, registry)
} else {
for _, filePath := range flag.Args() {
var inputFile *os.File
Expand All @@ -104,13 +109,13 @@ func main() {
fileInform = "pem"
}

doLint(inputFile, fileInform)
doLint(inputFile, fileInform, registry)
inputFile.Close()
}
}
}

func doLint(inputFile *os.File, inform string) {
func doLint(inputFile *os.File, inform string, registry lint.Registry) {
fileBytes, err := ioutil.ReadAll(inputFile)
if err != nil {
log.Fatalf("unable to read file %s: %s", inputFile.Name(), err)
Expand Down Expand Up @@ -140,7 +145,7 @@ func doLint(inputFile *os.File, inform string) {
log.Fatalf("unable to parse certificate: %s", err)
}

zlintResult := zlint.LintCertificate(c)
zlintResult := zlint.LintCertificateEx(c, registry)
jsonBytes, err := json.Marshal(zlintResult.Results)
if err != nil {
log.Fatalf("unable to encode lints JSON: %s", err)
Expand All @@ -158,58 +163,50 @@ func doLint(inputFile *os.File, inform string) {
os.Stdout.Sync()
}

func setLints() {
if include != "" && exclude != "" {
log.Fatal("unable to use include and exclude flag at the same time")
// trimmedList takes a comma separated string argument in raw, splits it by
// comma, and returns a list of the separated elements after trimming spaces
// from each element.
func trimmedList(raw string) []string {
var list []string
for _, item := range strings.Split(raw, ",") {
list = append(list, strings.TrimSpace(item))
}

includeLints()
excludeLints()
return list
}

func includeLints() {
if include == "" {
return
// setLints returns a filtered registry to use based on the nameFilter,
// includeNames, excludeNames, includeSources, and excludeSources flag values in
// use.
func setLints() (lint.Registry, error) {
// If there's no filter options set, use the global registry as-is
if nameFilter == "" && includeNames == "" && excludeNames == "" && includeSources == "" && excludeSources == "" {
return nil, nil
}

// parse includes to map for easier matching
var includes = strings.Split(include, ",")
var includesMap = make(map[string]struct{}, len(includes))
for _, includeName := range includes {
includeName = strings.TrimSpace(includeName)
if _, ok := lint.Lints[includeName]; !ok {
log.Fatalf("unknown lint %q in include list", includeName)
filterOpts := lint.FilterOptions{}
if nameFilter != "" {
r, err := regexp.Compile(nameFilter)
if err != nil {
return nil, fmt.Errorf("bad -nameFilter: %v", err)
}

includesMap[includeName] = struct{}{}
filterOpts.NameFilter = r
}

// clear all initialized lints except for includes
for lintName := range lint.Lints {
if _, ok := includesMap[lintName]; !ok {
delete(lint.Lints, lintName)
if excludeSources != "" {
if err := filterOpts.ExcludeSources.FromString(excludeSources); err != nil {
log.Fatalf("invalid -excludeSources: %v", err)
}
}
}

func excludeLints() {
if exclude == "" {
return
if includeSources != "" {
if err := filterOpts.IncludeSources.FromString(includeSources); err != nil {
log.Fatalf("invalid -includeSources: %v\n", err)
}
}

// parse excludes to map to get rid of duplicates
var excludes = strings.Split(exclude, ",")
var excludesMap = make(map[string]struct{}, len(excludes))
for _, excludeName := range excludes {
excludesMap[strings.TrimSpace(excludeName)] = struct{}{}
if excludeNames != "" {
filterOpts.ExcludeNames = trimmedList(excludeNames)
}

// exclude lints
for excludeName := range excludesMap {
if _, ok := lint.Lints[excludeName]; !ok {
log.Fatalf("unknown lint %q in exclude list", excludeName)
}

delete(lint.Lints, excludeName)
if includeNames != "" {
filterOpts.IncludeNames = trimmedList(includeNames)
}

return lint.GlobalRegistry().Filter(filterOpts)
}
14 changes: 9 additions & 5 deletions integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,23 @@ Config options

* `-parallelism` - number of linting Go routines to spawn (_Default: 5_)

* `-configFile` - integration test config file (_Default `integration/config.json`_)
* `-config` - integration test config file (_Default `integration/config.json`_)

* `-forceDownload` - ignore cached data files on disk forcing it to be downloaded fresh (_Default false_)

* `-overwriteExpected` - overwrite the expected results map in the `-configFile` with the results of the test run. This is useful when new lints or bugfixes are added and the changes in the results map have been vetted and are ready to be committed to the repository. (_Default false_)

* `-fingerprintSummarize` - print a summary of all certificate fingerprints that had lint findings. Can be quite spammy with the default data set. (_Default false_)
* `-fingerprintSummary` - print a summary of all certificate fingerprints that had lint findings. Can be quite spammy with the default data set. (_Default false_)

* `-fingerprintFilterString` - only lint certificates with hex encoded fingerprints that match the provided regular expression (_Default none_)
* `-fingerprintFilter` - only lint certificates with hex encoded fingerprints that match the provided regular expression (_Default none_)

* `-lintSummarize` - print a summary of result type counts by lint name. (_Default false_)
* `-lintSummary` - print a summary of result type counts by lint name. (_Default false_)

* `-lintFilterString` - only lint certificates with lints that have a name that matches the provided regular expression (_Default: none_)
* `-lintFilter` - only lint certificates with lints that have a name that matches the provided regular expression (_Default: none_)

* `-includeSources` - only lint certificates with lints that specify a Source present in the comma separated list provided (case sensitive) (_Default: none_)

* `-excludeSources` - only lint certificates with lints that do not specify a Source present in the comma separated list provided (case sensitive) (_Default: none_)

* `-outputTick` - number of certificates to lint before printing a "." marker to output (_Default 1000_)

Expand Down
2 changes: 1 addition & 1 deletion integration/corpus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func lintCertificate(work workItem) certResult {
Fingerprint: work.Fingerprint,
LintSummary: make(map[string]lint.LintStatus),
}
resultSet := zlint.LintCertificateFiltered(work.Certificate, lintFilter)
resultSet := zlint.LintCertificateEx(work.Certificate, registry)
for lintName, r := range resultSet.Results {
cr.LintSummary[lintName] = r.Status
cr.Result.Inc(r.Status)
Expand Down
32 changes: 32 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"os"
"regexp"
"testing"

"github.com/zmap/zlint/lint"
)

var (
Expand Down Expand Up @@ -37,6 +39,8 @@ var (
// lintFilterString is a flag for controlling which lints are run against the test
// corpus.
lintFilterString = flag.String("lintFilter", "", "if not-empty only lints with a name that match the provided regexp will be run")
includeSources = flag.String("includeSources", "", "Comma-separated list of lint sources to include")
excludeSources = flag.String("excludeSources", "", "Comma-separated list of lint sources to exclude")
// outputTick is a flag for controlling the number of certificates that are
// linted before a '.' is printed in the console. This controls the mechanism
// used to keep Travis from thinking the job is dead because there hasn't been
Expand All @@ -52,6 +56,9 @@ var (
// fpFilter and lintFilter are regexps for filtering certificate fingerprints
// to be linted and lints to be run.
fpFilter, lintFilter *regexp.Regexp

// registry is the lint registry used. It may be filtered based on command line flags.
registry = lint.GlobalRegistry()
)

// TestMain loads the integration test config, validates it, and prepares the
Expand Down Expand Up @@ -84,6 +91,31 @@ func TestMain(m *testing.M) {
log.Fatalf("error processing config file %q: %v", *configFile, err)
}

// Configure filter options
filterOpts := lint.FilterOptions{}
if *excludeSources != "" {
if err := filterOpts.ExcludeSources.FromString(*excludeSources); err != nil {
log.Fatalf("invalid -excludeSources: %v", err)
}
}
if *includeSources != "" {
if err := filterOpts.IncludeSources.FromString(*includeSources); err != nil {
log.Fatalf("invalid -includeSources: %v\n", err)
}
}
if lintFilter != nil {
filterOpts.NameFilter = lintFilter
}

// If there were filter options configured apply them and update the registry
if !filterOpts.Empty() {
r, err := registry.Filter(filterOpts)
if err != nil {
log.Fatalf("failed to filter lint registry: %v\n", err)
}
registry = r
}

// Prepare cache, downloading data files if required (or if forced by user
// request with forceDownload)
if err := c.PrepareCache(*forceDownload); err != nil {
Expand Down
Loading