Skip to content

Commit

Permalink
Enable loading of remote JSON schemas (e.g., "https") on --force flag (
Browse files Browse the repository at this point in the history
…#112)

* Enable loading of remote JSON schemas on --force flag

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

* Enable loading of remote JSON schemas on --force flag

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

* Change use of accent char. to single quote char. in log msgs.

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

---------

Signed-off-by: Matt Rutkowski <[email protected]>
  • Loading branch information
mrutkows authored Nov 8, 2024
1 parent c56e9dc commit 39bfb28
Show file tree
Hide file tree
Showing 50 changed files with 402 additions and 335 deletions.
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [

{
"showGlobalVariables": true,
"name": "Debug: validate",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "main.go", // "program": "${file}",
"args": ["validate", "-i", "examples/cyclonedx/SBOM/protonmail-webclient-v4-0912dff/bom.json"],
"dlvFlags": ["--check-go-version=false"]
},
{
"showGlobalVariables": true,
"name": "Debug: validate",
Expand Down
87 changes: 60 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,13 +328,20 @@ Use the [schema](#schema) command to list supported schemas formats, versions an

Customized JSON schemas can also be permanently configured as named schema "variants" within the utility's configuration file. See [adding schemas](#adding-schemas).

- **"Customized" schema** variants, perhaps derived from standard BOM schemas, can be used for validation using the `--variant` flag (e.g., industry or company-specific schemas).
- **Overriding default schema** - You can override an BOM's declared BOM version using the `--force` flag (e.g., verify a BOM against a newer specification version).
- **Overriding default schema**
- Using the [`--force` flag](#--force-flag) and passing in a URI to an alternative JSON schema.
- **"Customized" schema** variants, perhaps derived from standard BOM schemas, can be used for validation using the `--variant` flag (e.g., industry or company-specific schemas).
- **Note**: *These variants need to be built into the utility binary as a resource.*

#### Validate flags

The following flags can be used to improve performance when formatting error output results:

##### `--force` flag

You can override the schema used for validation *(which defaults to the schema that matches the declared format and version found in the input BOM file)* by providing a different one using the `--force` flag. This may be useful to verify a BOM contents against a newer specification version or provide a customized schema.
- **Note**: *The `--force` flag works with schema files with valid URIs which include URLs (e.g., 'https://') and files (e.g., 'file://').*

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

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.
Expand All @@ -358,17 +365,15 @@ Validating the "juice shop" SBOM (CycloneDX 1.2) example provided in this reposi
```

```bash
[INFO] Loading (embedded) default schema config file: `config.json`...
[INFO] Loading (embedded) default license policy file: `license.json`...
[INFO] Attempting to load and unmarshal data from: `examples/cyclonedx/SBOM/juice-shop-11.1.2/bom.json`...
[INFO] Successfully unmarshalled data from: `examples/cyclonedx/SBOM/juice-shop-11.1.2/bom.json`
[INFO] Attempting to load and unmarshal data from: 'examples/cyclonedx/SBOM/juice-shop-11.1.2/bom.json'...
[INFO] Successfully unmarshalled data from: 'examples/cyclonedx/SBOM/juice-shop-11.1.2/bom.json'
[INFO] Determining file's BOM format and version...
[INFO] Determined BOM format, version (variant): `CycloneDX`, `1.2` (latest)
[INFO] Determined BOM format, version (variant): 'CycloneDX', '1.2' (latest)
[INFO] Matching BOM schema (for validation): schema/cyclonedx/1.2/bom-1.2.schema.json
[INFO] Loading schema `schema/cyclonedx/1.2/bom-1.2.schema.json`...
[INFO] Schema `schema/cyclonedx/1.2/bom-1.2.schema.json` loaded.
[INFO] Validating `examples/cyclonedx/SBOM/juice-shop-11.1.2/bom.json`...
[INFO] BOM valid against JSON schema: `true`
[INFO] Loading schema 'schema/cyclonedx/1.2/bom-1.2.schema.json'...
[INFO] Schema 'schema/cyclonedx/1.2/bom-1.2.schema.json' loaded.
[INFO] Validating 'examples/cyclonedx/SBOM/juice-shop-11.1.2/bom.json'...
[INFO] BOM valid against JSON schema: 'true'
```
You can also verify the [exit code](#exit-codes) from the validate command:
Expand All @@ -381,7 +386,37 @@ echo $?
0 // no error (valid)
```
#### Example: Validate using "custom" schema variants
##### Example: Validate using a remote JSON schema file using '--force' flag
```bash
./sbom-utility validate -i test/cyclonedx/1.6/cdx-1-6-valid-cbom-full-1.6.json --force https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.6.schema.json
```
```bash
[INFO] Attempting to load and unmarshal data from: 'test/cyclonedx/1.6/cdx-1-6-valid-cbom-full-1.6.json'...
[INFO] Successfully unmarshalled data from: 'test/cyclonedx/1.6/cdx-1-6-valid-cbom-full-1.6.json'
[INFO] Determining file's BOM format and version...
[INFO] Determined BOM format, version (variant): 'CycloneDX', '1.6' (latest)
[INFO] Matching BOM schema (for validation): schema/cyclonedx/1.6/bom-1.6.schema.json
[INFO] Loading schema from '--force' flag: 'https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.6.schema.json'...
[INFO] Validating document using forced schema (i.e., '--force https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.6.schema.json')
[INFO] Schema 'schema/cyclonedx/1.6/bom-1.6.schema.json' loaded.
[INFO] Validating 'test/cyclonedx/1.6/cdx-1-6-valid-cbom-full-1.6.json'...
[INFO] BOM valid against JSON schema: 'true'
```

You can also verify the [exit code](#exit-codes) from the validate command:

```bash
echo $?
```

```bash
0 // no error (valid)
```


##### Example: Validate using "custom" schema variants

The validation command will use the declared format and version found within the SBOM JSON file itself to lookup the default (latest) matching schema version (as declared in`config.json`; however, if variants of that same schema (same format and version) are declared, they can be requested via the `--variant` command line flag:

Expand All @@ -392,19 +427,17 @@ The validation command will use the declared format and version found within the
If you run the sample command above, you would see several "custom" schema errors resulting in an invalid SBOM determination (i.e., `exit status 2`):

```text
[INFO] Loading (embedded) default schema config file: `config.json`...
[INFO] Loading (embedded) default license policy file: `license.json`...
[INFO] Attempting to load and unmarshal data from: `test/custom/cdx-1-4-test-custom-metadata-property-disclaimer-invalid.json`...
[INFO] Successfully unmarshalled data from: `test/custom/cdx-1-4-test-custom-metadata-property-disclaimer-invalid.json`
[INFO] Attempting to load and unmarshal data from: 'test/custom/cdx-1-4-test-custom-metadata-property-disclaimer-invalid.json'...
[INFO] Successfully unmarshalled data from: 'test/custom/cdx-1-4-test-custom-metadata-property-disclaimer-invalid.json'
[INFO] Determining file's BOM format and version...
[INFO] Determined BOM format, version (variant): `CycloneDX`, `1.4` custom
[INFO] Determined BOM format, version (variant): 'CycloneDX', '1.4' custom
[INFO] Matching BOM schema (for validation): schema/test/bom-1.4-custom.schema.json
[INFO] Loading schema `schema/test/bom-1.4-custom.schema.json`...
[INFO] Schema `schema/test/bom-1.4-custom.schema.json` loaded.
[INFO] Validating `test/custom/cdx-1-4-test-custom-metadata-property-disclaimer-invalid.json`...
[INFO] BOM valid against JSON schema: `false`
[INFO] Loading schema 'schema/test/bom-1.4-custom.schema.json'...
[INFO] Schema 'schema/test/bom-1.4-custom.schema.json' loaded.
[INFO] Validating 'test/custom/cdx-1-4-test-custom-metadata-property-disclaimer-invalid.json'...
[INFO] BOM valid against JSON schema: 'false'
[INFO] (3) schema errors detected.
[INFO] Formatting error results (`txt` format)...
[INFO] Formatting error results ('txt' format)...
1. {
"type": "contains",
"field": "metadata.properties",
Expand Down Expand Up @@ -445,7 +478,7 @@ If you run the sample command above, you would see several "custom" schema error
]
}
[ERROR] invalid SBOM: schema errors found (test/custom/cdx-1-4-test-custom-metadata-property-disclaimer-invalid.json)
[INFO] document `test/custom/cdx-1-4-test-custom-metadata-property-disclaimer-invalid.json`: valid=[false]
[INFO] document 'test/custom/cdx-1-4-test-custom-metadata-property-disclaimer-invalid.json': valid=[false]
```

confirming the exit code:
Expand All @@ -458,7 +491,7 @@ echo $?
2 // SBOM error
```

##### Why validation failed
###### Why validation failed

The output shows a first schema error indicating the failing JSON object; in this case,

Expand All @@ -467,7 +500,7 @@ The output shows a first schema error indicating the failing JSON object; in thi
- the `value` field SHOULD have had a constant value of `"This SBOM is current as of the date it was generated and is subject to change."` (as was required by the custom schema's regex).
- However, it was found to have only a partial match of `"This SBOM is current as of the date it was generated."`.

##### Details of the schema error
###### Details of the schema error

Use the `--debug` or `-d` flag to see all schema error details:

Expand Down Expand Up @@ -496,7 +529,7 @@ The details include the full context of the failing `metadata.properties` object
}
```

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

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

Expand Down Expand Up @@ -571,7 +604,7 @@ The JSON format will provide an `array` of schema error results that can be post
]
```

##### Reducing output size using `error-value=false` flag
###### 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`.

Expand Down
12 changes: 6 additions & 6 deletions cmd/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,12 @@ func NewCommandComponent() *cobra.Command {
// Make sure (optional) subcommand is known/valid
if len(args) == 1 {
if !preRunTestForSubcommand(VALID_SUBCOMMANDS_COMPONENT, args[0]) {
return getLogger().Errorf("Subcommand provided is not valid: `%v`", args[0])
return getLogger().Errorf("Subcommand provided is not valid: '%v'", args[0])
}
}

if len(args) == 0 {
getLogger().Tracef("No subcommands provided; defaulting to: `%s` subcommand", SUBCOMMAND_SCHEMA_LIST)
getLogger().Tracef("No subcommands provided; defaulting to: '%s' subcommand", SUBCOMMAND_SCHEMA_LIST)
}

// Test for required flags (parameters)
Expand All @@ -200,14 +200,14 @@ func componentCmdImpl(cmd *cobra.Command, args []string) (err error) {
// Create output writer
outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile
outputFile, writer, err := createOutputFile(outputFilename)
getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFilename, writer)
getLogger().Tracef("outputFile: '%v'; writer: '%v'", outputFilename, writer)

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

Expand Down Expand Up @@ -258,7 +258,7 @@ func ListComponents(writer io.Writer, persistentFlags utils.PersistentCommandFla
}

format := persistentFlags.OutputFormat
getLogger().Infof("Outputting listing (`%s` format)...", format)
getLogger().Infof("Outputting listing ('%s' format)...", format)
switch format {
case FORMAT_TEXT:
err = DisplayComponentListText(document, writer, flags)
Expand All @@ -268,7 +268,7 @@ func ListComponents(writer io.Writer, persistentFlags utils.PersistentCommandFla
err = DisplayComponentListMarkdown(document, writer, flags)
default:
// Default to Text output for anything else (set as flag default)
getLogger().Warningf("Listing not supported for `%s` format; defaulting to `%s` format...",
getLogger().Warningf("Listing not supported for '%s' format; defaulting to '%s' format...",
format, FORMAT_TEXT)
err = DisplayComponentListText(document, writer, flags)
}
Expand Down
22 changes: 11 additions & 11 deletions cmd/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@ func preRunTestForFiles(args []string) error {
if baseFilename == "" {
return getLogger().Errorf("Missing required argument(s): %s", FLAG_FILENAME_INPUT)
} else if _, err := os.Stat(baseFilename); err != nil {
return getLogger().Errorf("File not found: `%s`", baseFilename)
return getLogger().Errorf("File not found: '%s'", baseFilename)
}

// Make sure the revision file is present and exists
revisedFilename := utils.GlobalFlags.DiffFlags.RevisedFile
if revisedFilename == "" {
return getLogger().Errorf("Missing required argument(s): %s", FLAG_DIFF_FILENAME_REVISION)
} else if _, err := os.Stat(revisedFilename); err != nil {
return getLogger().Errorf("File not found: `%s`", revisedFilename)
return getLogger().Errorf("File not found: '%s'", revisedFilename)
}

return nil
Expand All @@ -100,7 +100,7 @@ func diffCmdImpl(cmd *cobra.Command, args []string) (err error) {
// Create output writer
outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile
outputFile, writer, err := createOutputFile(outputFilename)
getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFile, writer)
getLogger().Tracef("outputFile: '%v'; writer: '%v'", outputFile, writer)

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

Expand Down Expand Up @@ -143,11 +143,11 @@ func Diff(persistentFlags utils.PersistentCommandFlags, flags utils.DiffCommandF
// always close the output file
if outputFile != nil {
err = outputFile.Close()
getLogger().Infof("Closed output file: `%s`", outputFilename)
getLogger().Infof("Closed output file: '%s'", outputFilename)
}
}()

getLogger().Infof("Reading file (--input-file): `%s` ...", inputFilename)
getLogger().Infof("Reading file (--input-file): '%s' ...", inputFilename)
// #nosec G304 (suppress warning)
bBaseData, errReadBase := os.ReadFile(inputFilename)
if errReadBase != nil {
Expand All @@ -158,7 +158,7 @@ func Diff(persistentFlags utils.PersistentCommandFlags, flags utils.DiffCommandF
return
}

getLogger().Infof("Reading file (--input-revision): `%s` ...", revisedFilename)
getLogger().Infof("Reading file (--input-revision): '%s' ...", revisedFilename)
// #nosec G304 (suppress warning)
bRevisedData, errReadDelta := os.ReadFile(revisedFilename)
if errReadDelta != nil {
Expand All @@ -170,7 +170,7 @@ func Diff(persistentFlags utils.PersistentCommandFlags, flags utils.DiffCommandF
}

// Compare the base with the revision
getLogger().Infof("Comparing files: `%s` (base) to `%s` (revised) ...", inputFilename, revisedFilename)
getLogger().Infof("Comparing files: '%s' (base) to '%s' (revised) ...", inputFilename, revisedFilename)
diffResults, errCompare := compareBinaryData(bBaseData, bRevisedData)
if errCompare != nil {
return errCompare
Expand All @@ -179,7 +179,7 @@ func Diff(persistentFlags utils.PersistentCommandFlags, flags utils.DiffCommandF
// Output the result
var diffString string
if diffResults.Modified() {
getLogger().Infof("Outputting listing (`%s` format)...", format)
getLogger().Infof("Outputting listing ('%s' format)...", format)
switch outputFormat {
case FORMAT_TEXT:
var aJson map[string]interface{}
Expand All @@ -202,14 +202,14 @@ func Diff(persistentFlags utils.PersistentCommandFlags, flags utils.DiffCommandF
// Note: JSON data files MUST ends in a newline as this is a POSIX standard
default:
// Default to Text output for anything else (set as flag default)
getLogger().Warningf("Diff output format not supported for `%s` format.", format)
getLogger().Warningf("Diff output format not supported for '%s' format.", format)
}

// Output complete diff in either supported format
fmt.Fprintf(output, "%s\n", diffString)

} else {
getLogger().Infof("No deltas found. baseFilename: `%s`, revisedFilename=`%s` match.",
getLogger().Infof("No deltas found. baseFilename: '%s', revisedFilename='%s' match.",
inputFilename, revisedFilename)
}

Expand Down
6 changes: 3 additions & 3 deletions cmd/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func innerDiffTest(t *testing.T, testInfo *DiffTestInfo) (actualError error) {
utils.GlobalFlags.DiffFlags.RevisedFile = testInfo.RevisedFilename
utils.GlobalFlags.DiffFlags.Colorize = testInfo.Colorize

getLogger().Tracef("baseFilename: `%s`, revisedFilename=`%s`, actualError=`%T`",
getLogger().Tracef("baseFilename: '%s', revisedFilename='%s', actualError=`%T`",
utils.GlobalFlags.PersistentFlags.InputFile,
utils.GlobalFlags.DiffFlags.RevisedFile,
actualError)
Expand All @@ -101,8 +101,8 @@ func innerDiffTest(t *testing.T, testInfo *DiffTestInfo) (actualError error) {
if !ErrorTypesMatch(actualError, testInfo.ResultExpectedError) {
switch t := actualError.(type) {
default:
fmt.Printf("unhandled error type: `%v`\n", t)
fmt.Printf(">> value: `%v`\n", t)
fmt.Printf("unhandled error type: '%v'\n", t)
fmt.Printf(">> value: '%v'\n", t)
getLogger().Error(actualError)
}
t.Errorf("expected error type: `%T`, actual type: `%T`", testInfo.ResultExpectedError, actualError)
Expand Down
Loading

0 comments on commit 39bfb28

Please sign in to comment.