Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/go_modules/github.com/fatih/color…
Browse files Browse the repository at this point in the history
…-1.18.0
  • Loading branch information
kehoecj authored Dec 6, 2024
2 parents 3915dbb + 2a5f513 commit a1d1260
Show file tree
Hide file tree
Showing 13 changed files with 323 additions and 206 deletions.
25 changes: 11 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</div>

<p align="center">
<img id="cov" src="https://img.shields.io/badge/Coverage-94.6%25-brightgreen" alt="Code Coverage">
<img id="cov" src="https://img.shields.io/badge/Coverage-94.7%25-brightgreen" alt="Code Coverage">

<a href="https://scorecard.dev/viewer/?uri=github.com/Boeing/config-file-validator">
<img src="https://api.scorecard.dev/projects/github.com/Boeing/config-file-validator/badge" alt="OpenSSF Scorecard">
Expand Down Expand Up @@ -106,12 +106,14 @@ optional flags:
A comma separated list of file types to ignore
-groupby string
Group output by filetype, directory, pass-fail. Supported for Standard and JSON reports
-output string
Destination to a file to output results
-quiet
If quiet flag is set. It doesn't print any output to stdout.
-reporter string
Format of the printed report. Options are standard and json (default "standard")
A string representing report format and optional output file path separated by colon if present.
Usage: --reporter <format>:<optional_file_path>
Multiple reporters can be specified: --reporter json:file_path.json --reporter junit:another_file_path.xml
Omit the file path to output to stdout: --reporter json or explicitly specify stdout using "-": --reporter json:-
Supported formats: standard, json, junit (default: "standard")
-version
Version prints the release version of validator
```
Expand All @@ -125,7 +127,6 @@ The config-file-validator supports setting options via environment variables. If
| `CFV_DEPTH` | `-depth` |
| `CFV_EXCLUDE_DIRS` | `-exclude-dirs` |
| `CFV_EXCLUDE_FILE_TYPES` | `-exclude-file-types` |
| `CFV_OUTPUT` | `-output` |
| `CFV_REPORTER` | `-reporter` |
| `CFV_GROUPBY` | `-groupby` |
| `CFV_QUIET` | `-quiet` |
Expand Down Expand Up @@ -175,10 +176,13 @@ validator --depth=0 /path/to/search
![Custom Recursion Run](./img/custom_recursion.gif)

#### Customize report output
Customize the report output. Available options are `standard`, `junit`, and `json`
You can customize the report output and save the results to a file (default name is result.{extension}). The available report types are `standard`, `junit`, and `json`. You can specify multiple report types by chaining the `--reporter` flags.

Providing an output file is optional, the results will be printed to stdout by default. To explicitly direct the output to stdout, use file path as `-`.

```
validator --reporter=json /path/to/search
validator --reporter=json:- /path/to/search
validator --reporter=json:output.json --reporter=standard /path/to/search
```

![Exclude File Types Run](./img/custom_reporter.gif)
Expand All @@ -199,13 +203,6 @@ validator -groupby directory,pass-fail

![Groupby File Type and Pass/Fail](./img/gb-filetype-and-pass-fail.gif)

### Output results to a file
Output report results to a file (default name is `result.{extension}`). Must provide reporter flag with a supported extension format. Available options are `junit` and `json`. If an existing directory is provided, create a file named default name in the given directory. If a file name is provided, create a file named the given name at the current working directory.

```
validator --reporter=json --output=/path/to/dir
```

### Suppress output
Passing the `--quiet` flag suppresses all output to stdout. If there are invalid config files the validator tool will exit with 1. Any errors in execution such as an invalid path will still be displayed.

Expand Down
209 changes: 151 additions & 58 deletions cmd/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ optional flags:
Subdirectories to exclude when searching for configuration files
-exclude-file-types string
A comma separated list of file types to ignore
-output
Destination of a file to outputting results
-reporter string
Format of the printed report. Options are standard, json, junit and sarif (default "standard")
A string representing report format and optional output file path separated by colon if present.
Usage: --reporter <format>:<optional_file_path>
Multiple reporters can be specified: --reporter json:file_path.json --reporter junit:another_file_path.xml
Omit the file path to output to stdout: --reporter json or explicitly specify stdout using "-": --reporter json:-
Supported formats: standard, json, junit (default: "standard")
-version
Version prints the release version of validator
*/
Expand Down Expand Up @@ -48,14 +50,24 @@ type validatorConfig struct {
searchPaths []string
excludeDirs *string
excludeFileTypes *string
reportType *string
reportType map[string]string
depth *int
versionQuery *bool
output *string
groupOutput *string
quiet *bool
}

type reporterFlags []string

func (rf *reporterFlags) String() string {
return fmt.Sprint(*rf)
}

func (rf *reporterFlags) Set(value string) error {
*rf = append(*rf, value)
return nil
}

// Custom Usage function to cover
func validatorUsage() {
fmt.Printf("Usage: validator [OPTIONS] [<search_path>...]\n\n")
Expand All @@ -71,7 +83,9 @@ func validatorUsage() {
func getFileTypes() []string {
options := make([]string, 0, len(filetype.FileTypes))
for _, typ := range filetype.FileTypes {
options = append(options, typ.Name)
for extName := range typ.Extensions {
options = append(options, extName)
}
}
sort.Strings(options)
return options
Expand All @@ -98,99 +112,153 @@ func validateFileTypeList(input []string) bool {
// will return with exit = 1
func getFlags() (validatorConfig, error) {
flag.Usage = validatorUsage
reporterConfigFlags := reporterFlags{}

var (
depthPtr = flag.Int("depth", 0, "Depth of recursion for the provided search paths. Set depth to 0 to disable recursive path traversal")
excludeDirsPtr = flag.String("exclude-dirs", "", "Subdirectories to exclude when searching for configuration files")
excludeFileTypesPtr = flag.String("exclude-file-types", "", "A comma separated list of file types to ignore.\nValid options: "+strings.Join(getFileTypes(), ", "))
outputPtr = flag.String("output", "", "Destination to a file to output results")
reportTypePtr = flag.String("reporter", "standard", "Format of the printed report. Options are standard, json, junit and sarif")
excludeFileTypesPtr = flag.String("exclude-file-types", "", "A comma separated list of file types to ignore")
versionPtr = flag.Bool("version", false, "Version prints the release version of validator")
groupOutputPtr = flag.String("groupby", "", "Group output by filetype, directory, pass-fail. Supported for Standard and JSON reports")
quietPtr = flag.Bool("quiet", false, "If quiet flag is set. It doesn't print any output to stdout.")
)

flagsEnvMap := map[string]string{
"depth": "CFV_DEPTH",
"exclude-dirs": "CFV_EXCLUDE_DIRS",
"exclude-file-types": "CFV_EXCLUDE_FILE_TYPES",
"output": "CFV_OUTPUT",
"reporter": "CFV_REPORTER",
"groupby": "CFV_GROUPBY",
"quiet": "CFV_QUIET",
}

for flagName, envVar := range flagsEnvMap {
if err := setFlagFromEnvIfNotSet(flagName, envVar); err != nil {
return validatorConfig{}, err
}
}
flag.Var(
&reporterConfigFlags,
"reporter",
`A string representing report format and optional output file path separated by colon if present.
Usage: --reporter <format>:<optional_file_path>
Multiple reporters can be specified: --reporter json:file_path.json --reporter junit:another_file_path.xml
Omit the file path to output to stdout: --reporter json or explicitly specify stdout using "-": --reporter json:-
Supported formats: standard, json, junit (default: "standard")`,
)

flag.Parse()

searchPaths := make([]string, 0)

// If search path arg is empty, set it to the cwd
// if not, set it to the arg. Supports n number of
// paths
if flag.NArg() == 0 {
searchPaths = append(searchPaths, ".")
} else {
searchPaths = append(searchPaths, flag.Args()...)
err := applyDefaultFlagsFromEnv()
if err != nil {
return validatorConfig{}, err
}

acceptedReportTypes := map[string]bool{"standard": true, "json": true, "junit": true, "sarif": true}

if !acceptedReportTypes[*reportTypePtr] {
return validatorConfig{}, errors.New("Wrong parameter value for reporter, only supports standard, json, junit or sarif")
reporterConf, err := parseReporterFlags(reporterConfigFlags)
if err != nil {
return validatorConfig{}, err
}

groupOutputReportTypes := map[string]bool{"standard": true, "json": true}
searchPaths := parseSearchPath()

if !groupOutputReportTypes[*reportTypePtr] && *groupOutputPtr != "" {
return validatorConfig{}, errors.New("Wrong parameter value for reporter, groupby is only supported for standard and JSON reports")
err = validateReporterConf(reporterConf, groupOutputPtr)
if err != nil {
return validatorConfig{}, err
}

if depthPtr != nil && isFlagSet("depth") && *depthPtr < 0 {
return validatorConfig{}, errors.New("Wrong parameter value for depth, value cannot be negative")
}

if *excludeFileTypesPtr != "" {
*excludeFileTypesPtr = strings.ToLower(*excludeFileTypesPtr)
if !validateFileTypeList(strings.Split(*excludeFileTypesPtr, ",")) {
return validatorConfig{}, errors.New("Invalid exclude file type")
}
}

err = validateGroupByConf(groupOutputPtr)
if err != nil {
return validatorConfig{}, err
}

config := validatorConfig{
searchPaths,
excludeDirsPtr,
excludeFileTypesPtr,
reporterConf,
depthPtr,
versionPtr,
groupOutputPtr,
quietPtr,
}

return config, nil
}

func validateReporterConf(conf map[string]string, groupBy *string) error {
acceptedReportTypes := map[string]bool{"standard": true, "json": true, "junit": true, "sarif": true}
groupOutputReportTypes := map[string]bool{"standard": true, "json": true}

for reportType := range conf {
_, ok := acceptedReportTypes[reportType]
if !ok {
return errors.New("Wrong parameter value for reporter, only supports standard, json, junit, or sarif")
}

if !groupOutputReportTypes[reportType] && groupBy != nil && *groupBy != "" {
return errors.New("Wrong parameter value for reporter, groupby is only supported for standard and JSON reports")
}
}

return nil
}

func validateGroupByConf(groupBy *string) error {
groupByCleanString := cleanString("groupby")
groupByUserInput := strings.Split(groupByCleanString, ",")
groupByAllowedValues := []string{"filetype", "directory", "pass-fail"}
seenValues := make(map[string]bool)

// Check that the groupby values are valid and not duplicates
if groupOutputPtr != nil && isFlagSet("groupby") {
if groupBy != nil && isFlagSet("groupby") {
for _, groupBy := range groupByUserInput {
if !slices.Contains(groupByAllowedValues, groupBy) {
return validatorConfig{}, errors.New("Wrong parameter value for groupby, only supports filetype, directory, pass-fail")
return errors.New("Wrong parameter value for groupby, only supports filetype, directory, pass-fail")
}
if _, ok := seenValues[groupBy]; ok {
return validatorConfig{}, errors.New("Wrong parameter value for groupby, duplicate values are not allowed")
return errors.New("Wrong parameter value for groupby, duplicate values are not allowed")
}
seenValues[groupBy] = true
}
}

config := validatorConfig{
searchPaths,
excludeDirsPtr,
excludeFileTypesPtr,
reportTypePtr,
depthPtr,
versionPtr,
outputPtr,
groupOutputPtr,
quietPtr,
return nil
}

func parseSearchPath() []string {
searchPaths := make([]string, 0)

// If search path arg is empty, set it to the cwd
// if not, set it to the arg. Supports n number of
// paths
if flag.NArg() == 0 {
searchPaths = append(searchPaths, ".")
} else {
searchPaths = append(searchPaths, flag.Args()...)
}

return config, nil
return searchPaths
}

func parseReporterFlags(flags reporterFlags) (map[string]string, error) {
conf := make(map[string]string)
for _, reportFlag := range flags {
parts := strings.Split(reportFlag, ":")
switch len(parts) {
case 1:
conf[parts[0]] = ""
case 2:
if parts[1] == "-" {
conf[parts[0]] = ""
} else {
conf[parts[0]] = parts[1]
}
default:
return nil, errors.New("Wrong parameter value format for reporter, expected format is `report_type:optional_file_path`")
}
}

if len(conf) == 0 {
conf["standard"] = ""
}

return conf, nil
}

// isFlagSet verifies if a given flag has been set or not
Expand All @@ -206,6 +274,25 @@ func isFlagSet(flagName string) bool {
return isSet
}

func applyDefaultFlagsFromEnv() error {
flagsEnvMap := map[string]string{
"depth": "CFV_DEPTH",
"exclude-dirs": "CFV_EXCLUDE_DIRS",
"exclude-file-types": "CFV_EXCLUDE_FILE_TYPES",
"reporter": "CFV_REPORTER",
"groupby": "CFV_GROUPBY",
"quiet": "CFV_QUIET",
}

for flagName, envVar := range flagsEnvMap {
if err := setFlagFromEnvIfNotSet(flagName, envVar); err != nil {
return err
}
}

return nil
}

func setFlagFromEnvIfNotSet(flagName string, envVar string) error {
if isFlagSet(flagName) {
return nil
Expand All @@ -231,7 +318,7 @@ func getReporter(reportType, outputDest *string) reporter.Reporter {
case "sarif":
return reporter.NewSARIFReporter(*outputDest)
default:
return reporter.StdoutReporter{}
return reporter.NewStdoutReporter(*outputDest)
}
}

Expand Down Expand Up @@ -261,7 +348,13 @@ func mainInit() int {
// since the exclude dirs are a comma separated string
// it needs to be split into a slice of strings
excludeDirs := strings.Split(*validatorConfig.excludeDirs, ",")
choosenReporter := getReporter(validatorConfig.reportType, validatorConfig.output)

chosenReporters := make([]reporter.Reporter, 0)
for reportType, outputFile := range validatorConfig.reportType {
rt, of := reportType, outputFile // avoid "Implicit memory aliasing in for loop"
chosenReporters = append(chosenReporters, getReporter(&rt, &of))
}

excludeFileTypes := strings.Split(*validatorConfig.excludeFileTypes, ",")
groupOutput := strings.Split(*validatorConfig.groupOutput, ",")
fsOpts := []finder.FSFinderOptions{
Expand All @@ -280,7 +373,7 @@ func mainInit() int {

// Initialize the CLI
c := cli.Init(
cli.WithReporter(choosenReporter),
cli.WithReporters(chosenReporters...),
cli.WithFinder(fileSystemFinder),
cli.WithGroupOutput(groupOutput),
cli.WithQuiet(quiet),
Expand Down
Loading

0 comments on commit a1d1260

Please sign in to comment.