Skip to content

Commit

Permalink
Support schema error results to be ouptut in JSON format including cu…
Browse files Browse the repository at this point in the history
…stom format flags (#40)

* Create a framework for validation error special case handling

Signed-off-by: Matt Rutkowski <[email protected]>

* Create a framework for validation error special case handling

Signed-off-by: Matt Rutkowski <[email protected]>

* Adjust JSON output formatting as an array

Signed-off-by: Matt Rutkowski <[email protected]>

* Use an ordered map to control JSON output marshaling order

Signed-off-by: Matt Rutkowski <[email protected]>

* Use an ordered map to control JSON output marshaling order

Signed-off-by: Matt Rutkowski <[email protected]>

* Use an ordered map to control JSON output marshaling order

Signed-off-by: Matt Rutkowski <[email protected]>

* Use an ordered map to control JSON output marshaling order

Signed-off-by: Matt Rutkowski <[email protected]>

* Separate format related functions into their own file

Signed-off-by: Matt Rutkowski <[email protected]>

* Separate format related functions into their own file

Signed-off-by: Matt Rutkowski <[email protected]>

* Format value for unique item error

Signed-off-by: Matt Rutkowski <[email protected]>

* Consolidate validation flags and use on top-level API call

Signed-off-by: Matt Rutkowski <[email protected]>

* Adjust JSON error result output prefix and indent

Signed-off-by: Matt Rutkowski <[email protected]>

* Add validation test case for bad iri-format

Signed-off-by: Matt Rutkowski <[email protected]>

* Add validation test case for bad iri-format

Signed-off-by: Matt Rutkowski <[email protected]>

* Consolidate persistent command flags into a struct

Signed-off-by: Matt Rutkowski <[email protected]>

* represent array type, index and item as a map in json error results

Signed-off-by: Matt Rutkowski <[email protected]>

* Support flag  true|false on validate command

Signed-off-by: Matt Rutkowski <[email protected]>

* Fix even more Sonatype errors that seem to chnage every time I touch an old file

Signed-off-by: Matt Rutkowski <[email protected]>

* Adjust help for validate given new formats/flags

Signed-off-by: Matt Rutkowski <[email protected]>

* Update README to show validate JSON output and new flags

Signed-off-by: Matt Rutkowski <[email protected]>

* buffer JSON output for unit tests

Signed-off-by: Matt Rutkowski <[email protected]>

* Update the text format logic to mirror new json formatting

Signed-off-by: Matt Rutkowski <[email protected]>

* Update the text format logic to mirror new json formatting

Signed-off-by: Matt Rutkowski <[email protected]>

* Update the text format logic to mirror new json formatting

Signed-off-by: Matt Rutkowski <[email protected]>

* Streamline json and text formatting paths

Signed-off-by: Matt Rutkowski <[email protected]>

* Adjust colorized indent to match normal indent

Signed-off-by: Matt Rutkowski <[email protected]>

* Add additional test assertions to validate # errs and error conext

Signed-off-by: Matt Rutkowski <[email protected]>

* Assure forced schema file tests reset to default schema

Signed-off-by: Matt Rutkowski <[email protected]>

---------

Signed-off-by: Matt Rutkowski <[email protected]>
  • Loading branch information
mrutkows authored Jun 26, 2023
1 parent 0b1af15 commit 5c10806
Show file tree
Hide file tree
Showing 37 changed files with 1,235 additions and 337 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
extra_files: LICENSE config.json license.json custom.json ${{env.SBOM_NAME}}
extra_files: LICENSE README.md config.json license.json custom.json ${{env.SBOM_NAME}}
# "auto" will use ZIP for Windows, otherwise default is TAR
compress_assets: auto
# NOTE: This verbose flag may be removed
Expand Down
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"multimap",
"myservices",
"NOASSERTION",
"nolint",
"nosec",
"NTIA",
"Nyffenegger",
Expand Down Expand Up @@ -102,5 +103,8 @@
],
"files.watcherExclude": {
"**/target": true
}
},
"cSpell.ignoreWords": [
"iancoleman"
]
}
110 changes: 108 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -804,11 +804,15 @@ The following flags can be used to improve performance when formatting error out

##### `--error-limit` flag

Use the `--error-limit x` flag to reduce the formatted error result output to the first `x` errors. By default, only the first 10 errors are output with an informational messaging indicating `x/y` errors were shown.
Use the `--error-limit x` (default: `10`) flag to reduce the formatted error result output to the first `x` errors. By default, only the first 10 errors are output with an informational messaging indicating `x/y` errors were shown.

##### `--error-value` flag

Use the `--error-value=true|false` (default: `true`) flag to reduce the formatted error result output by not showing the `value` field which shows detailed information about the failing data in the BOM.

##### `--colorize` flag

Use the `--colorize=true|false` flag to add/remove color formatting to error result output. By default, formatted error output is colorized to help with human readability; for automated use, it can be turned off.
Use the `--colorize=true|false` (default: `false`) flag to add/remove color formatting to error result `txt` formatted output. By default, `txt` formatted error output is colorized to help with human readability; for automated use, it can be turned off.

#### Validate Examples

Expand Down Expand Up @@ -911,6 +915,108 @@ The details include the full context of the failing `metadata.properties` object
]]
```

#### Example: Validate using "JSON" format

The JSON format will provide an `array` of schema error results that can be post-processed as part of validation toolchain.

```bash
./sbom-utility validate -i test/validation/cdx-1-4-validate-err-components-unique-items-1.json --format json --quiet
```

```json
[
{
"type": "unique",
"field": "components",
"context": "(root).components",
"description": "array items[1,2] must be unique",
"value": {
"type": "array",
"index": 1,
"item": {
"bom-ref": "pkg:npm/[email protected]",
"description": "Node.js body parsing middleware",
"hashes": [
{
"alg": "SHA-1",
"content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
}
],
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"name": "body-parser",
"purl": "pkg:npm/[email protected]",
"type": "library",
"version": "1.19.0"
}
}
},
{
"type": "unique",
"field": "components",
"context": "(root).components",
"description": "array items[2,4] must be unique",
"value": {
"type": "array",
"index": 2,
"item": {
"bom-ref": "pkg:npm/[email protected]",
"description": "Node.js body parsing middleware",
"hashes": [
{
"alg": "SHA-1",
"content": "96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
}
],
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"name": "body-parser",
"purl": "pkg:npm/[email protected]",
"type": "library",
"version": "1.19.0"
}
}
}
]
```

##### Reducing output size using `error-value=false` flag

In many cases, BOMs may have many errors and having the `value` information details included can be too verbose and lead to large output files to inspect. In those cases, simply set the `error-value` flag to `false`.

Rerunning the same command with this flag set to false yields a reduced set of information.

```bash
./sbom-utility validate -i test/validation/cdx-1-4-validate-err-components-unique-items-1.json --format json --error-value=false --quiet
```

```json
[
{
"type": "unique",
"field": "components",
"context": "(root).components",
"description": "array items[1,2] must be unique"
},
{
"type": "unique",
"field": "components",
"context": "(root).components",
"description": "array items[2,4] must be unique"
}
]
```

---

### Vulnerability
Expand Down
41 changes: 20 additions & 21 deletions cmd/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package cmd
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"

Expand Down Expand Up @@ -51,7 +50,7 @@ func NewCommandDiff() *cobra.Command {
command.Use = CMD_USAGE_DIFF
command.Short = "Report on differences between two BOM files using RFC 6902 format"
command.Long = "Report on differences between two BOM files using RFC 6902 format"
command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT,
command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT,
FLAG_DIFF_OUTPUT_FORMAT_HELP+DIFF_OUTPUT_SUPPORTED_FORMATS)
command.Flags().StringVarP(&utils.GlobalFlags.DiffFlags.RevisedFile,
FLAG_DIFF_FILENAME_REVISION,
Expand All @@ -75,7 +74,7 @@ func preRunTestForFiles(cmd *cobra.Command, args []string) error {
getLogger().Tracef("args: %v", args)

// Make sure the base (input) file is present and exists
baseFilename := utils.GlobalFlags.InputFile
baseFilename := utils.GlobalFlags.PersistentFlags.InputFile
if baseFilename == "" {
return getLogger().Errorf("Missing required argument(s): %s", FLAG_FILENAME_INPUT)
} else if _, err := os.Stat(baseFilename); err != nil {
Expand All @@ -98,7 +97,8 @@ func diffCmdImpl(cmd *cobra.Command, args []string) (err error) {
defer getLogger().Exit()

// Create output writer
outputFile, writer, err := createOutputFile(utils.GlobalFlags.OutputFile)
outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile
outputFile, writer, err := createOutputFile(outputFilename)
getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFile, writer)

// use function closure to assure consistent error output based upon error type
Expand All @@ -109,7 +109,7 @@ func diffCmdImpl(cmd *cobra.Command, args []string) (err error) {
if err != nil {
return
}
getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile)
getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.PersistentFlags.OutputFile)
}
}()

Expand All @@ -123,11 +123,11 @@ func Diff(flags utils.CommandFlags) (err error) {
defer getLogger().Exit()

// create locals
format := utils.GlobalFlags.OutputFormat
baseFilename := utils.GlobalFlags.InputFile
outputFilename := utils.GlobalFlags.OutputFile
outputFormat := utils.GlobalFlags.OutputFormat
deltaFilename := utils.GlobalFlags.DiffFlags.RevisedFile
format := utils.GlobalFlags.PersistentFlags.OutputFormat
inputFilename := utils.GlobalFlags.PersistentFlags.InputFile
outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile
outputFormat := utils.GlobalFlags.PersistentFlags.OutputFormat
revisedFilename := utils.GlobalFlags.DiffFlags.RevisedFile
deltaColorize := utils.GlobalFlags.DiffFlags.Colorize

// Create output writer
Expand All @@ -138,31 +138,31 @@ func Diff(flags utils.CommandFlags) (err error) {
// always close the output file
if outputFile != nil {
err = outputFile.Close()
getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile)
getLogger().Infof("Closed output file: `%s`", outputFilename)
}
}()

getLogger().Infof("Reading file (--input-file): `%s` ...", baseFilename)
getLogger().Infof("Reading file (--input-file): `%s` ...", inputFilename)
// #nosec G304 (suppress warning)
bBaseData, errReadBase := ioutil.ReadFile(baseFilename)
bBaseData, errReadBase := os.ReadFile(inputFilename)
if errReadBase != nil {
getLogger().Debugf("%v", bBaseData[:255])
err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", utils.GlobalFlags.InputFile, err.Error())
err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", inputFilename, err.Error())
return
}

getLogger().Infof("Reading file (--input-revision): `%s` ...", deltaFilename)
getLogger().Infof("Reading file (--input-revision): `%s` ...", revisedFilename)
// #nosec G304 (suppress warning)
bRevisedData, errReadDelta := ioutil.ReadFile(deltaFilename)
bRevisedData, errReadDelta := os.ReadFile(revisedFilename)
if errReadDelta != nil {
getLogger().Debugf("%v", bRevisedData[:255])
err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", utils.GlobalFlags.InputFile, err.Error())
err = getLogger().Errorf("Failed to ReadFile '%s': %s\n", inputFilename, err.Error())
return
}

// Compare the base with the revision
differ := diff.New()
getLogger().Infof("Comparing files: `%s` (base) to `%s` (revised) ...", baseFilename, deltaFilename)
getLogger().Infof("Comparing files: `%s` (base) to `%s` (revised) ...", inputFilename, revisedFilename)
d, err := differ.Compare(bBaseData, bRevisedData)
if err != nil {
err = getLogger().Errorf("Failed to Compare data: %s\n", err.Error())
Expand All @@ -178,7 +178,7 @@ func Diff(flags utils.CommandFlags) (err error) {
err = json.Unmarshal(bBaseData, &aJson)

if err != nil {
err = getLogger().Errorf("json.Unmarshal() failed '%s': %s\n", utils.GlobalFlags.InputFile, err.Error())
err = getLogger().Errorf("json.Unmarshal() failed '%s': %s\n", inputFilename, err.Error())
return
}

Expand All @@ -201,8 +201,7 @@ func Diff(flags utils.CommandFlags) (err error) {

} else {
getLogger().Infof("No deltas found. baseFilename: `%s`, revisedFilename=`%s` match.",
utils.GlobalFlags.InputFile,
utils.GlobalFlags.DiffFlags.RevisedFile)
inputFilename, revisedFilename)
}

return
Expand Down
6 changes: 3 additions & 3 deletions cmd/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ func innerDiffError(t *testing.T, baseFilename string, revisedFilename string, f
defer getLogger().Exit()

// Copy the test filename to the command line flags where the code looks for it
utils.GlobalFlags.OutputFormat = format
utils.GlobalFlags.InputFile = baseFilename
utils.GlobalFlags.PersistentFlags.OutputFormat = format
utils.GlobalFlags.PersistentFlags.InputFile = baseFilename
utils.GlobalFlags.DiffFlags.RevisedFile = revisedFilename
utils.GlobalFlags.DiffFlags.Colorize = true

actualError = Diff(utils.GlobalFlags)

getLogger().Tracef("baseFilename: `%s`, revisedFilename=`%s`, actualError=`%T`",
utils.GlobalFlags.InputFile,
utils.GlobalFlags.PersistentFlags.InputFile,
utils.GlobalFlags.DiffFlags.RevisedFile,
actualError)

Expand Down
16 changes: 9 additions & 7 deletions cmd/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,28 @@ func LoadInputSbomFileAndDetectSchema() (document *schema.Sbom, err error) {
getLogger().Enter()
defer getLogger().Exit()

inputFile := utils.GlobalFlags.PersistentFlags.InputFile

// check for required fields on command
getLogger().Tracef("utils.Flags.InputFile: `%s`", utils.GlobalFlags.InputFile)
if utils.GlobalFlags.InputFile == "" {
return nil, fmt.Errorf("invalid input file (-%s): `%s` ", FLAG_FILENAME_INPUT_SHORT, utils.GlobalFlags.InputFile)
getLogger().Tracef("utils.Flags.InputFile: `%s`", inputFile)
if inputFile == "" {
return nil, fmt.Errorf("invalid input file (-%s): `%s` ", FLAG_FILENAME_INPUT_SHORT, inputFile)
}

// Construct an Sbom object around the input file
document = schema.NewSbom(utils.GlobalFlags.InputFile)
document = schema.NewSbom(inputFile)

// Load the raw, candidate SBOM (file) as JSON data
getLogger().Infof("Attempting to load and unmarshal file `%s`...", utils.GlobalFlags.InputFile)
getLogger().Infof("Attempting to load and unmarshal file `%s`...", inputFile)
err = document.UnmarshalSBOMAsJsonMap() // i.e., utils.Flags.InputFile
if err != nil {
return
}
getLogger().Infof("Successfully unmarshalled data from: `%s`", utils.GlobalFlags.InputFile)
getLogger().Infof("Successfully unmarshalled data from: `%s`", inputFile)

// Search the document keys/values for known SBOM formats and schema in the config. file
getLogger().Infof("Determining file's SBOM format and version...")
err = document.FindFormatAndSchema()
err = document.FindFormatAndSchema(utils.GlobalFlags.PersistentFlags.InputFile)
if err != nil {
return
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ func (err BaseError) Error() string {
return formattedMessage
}

func (base BaseError) AppendMessage(addendum string) {
// Ignore (invalid) static linting message:
// "ineffective assignment to field (SA4005)"
base.Message += addendum
func (err *BaseError) AppendMessage(addendum string) {
if addendum != "" {
err.Message += addendum
}
}

type UtilityError struct {
Expand Down
11 changes: 6 additions & 5 deletions cmd/license_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const (
MSG_OUTPUT_NO_LICENSES_ONLY_NOASSERTION = "no valid licenses found in BOM document (only licenses marked NOASSERTION)"
)

//"Type", "ID/Name/Expression", "Component(s)", "BOM ref.", "Document location"
// "Type", "ID/Name/Expression", "Component(s)", "BOM ref.", "Document location"
// filter keys
const (
LICENSE_FILTER_KEY_USAGE_POLICY = "usage-policy"
Expand Down Expand Up @@ -106,7 +106,7 @@ func NewCommandList() *cobra.Command {
command.Use = CMD_USAGE_LICENSE_LIST
command.Short = "List licenses found in the BOM input file"
command.Long = "List licenses and associated policies found in the BOM input file"
command.Flags().StringVarP(&utils.GlobalFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", "",
command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", "",
FLAG_LICENSE_LIST_OUTPUT_FORMAT_HELP+
LICENSE_LIST_SUPPORTED_FORMATS+
LICENSE_LIST_SUMMARY_SUPPORTED_FORMATS)
Expand Down Expand Up @@ -162,22 +162,23 @@ func listCmdImpl(cmd *cobra.Command, args []string) (err error) {
defer getLogger().Exit()

// Create output writer
outputFile, writer, err := createOutputFile(utils.GlobalFlags.OutputFile)
outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile
outputFile, writer, err := createOutputFile(outputFilename)

// use function closure to assure consistent error output based upon error type
defer func() {
// always close the output file
if outputFile != nil {
err = outputFile.Close()
getLogger().Infof("Closed output file: `%s`", utils.GlobalFlags.OutputFile)
getLogger().Infof("Closed output file: `%s`", outputFilename)
}
}()

// process filters supplied on the --where command flag
whereFilters, err := processWhereFlag(cmd)

if err == nil {
err = ListLicenses(writer, utils.GlobalFlags.OutputFormat, whereFilters, utils.GlobalFlags.LicenseFlags.Summary)
err = ListLicenses(writer, utils.GlobalFlags.PersistentFlags.OutputFormat, whereFilters, utils.GlobalFlags.LicenseFlags.Summary)
}

return
Expand Down
Loading

0 comments on commit 5c10806

Please sign in to comment.