diff --git a/README.md b/README.md index 77b6a06e..0e64470f 100644 --- a/README.md +++ b/README.md @@ -155,15 +155,77 @@ which returns `0` (zero) or "no error": ### Persistent flags -This section describes some of the important command line flags that apply to most commands that have a `list` subcommand for generating columnar, report-styled output (e.g., `schema`, `license`, `vulnerability`, etc.). +This section describes some of the important command line flags that apply to most of the utility's commands. +- [format flag](#format-flag): with `--format` +- [indent flag](#indent-flag): with `--indent` - [input flag](#input-flag): with `--input` or `-i` - [output flag](#output-flag): with `--output` or `-o` -- [format flag](#format-flag): with `--format` - [quiet flag](#quiet-flag): with `--quiet` or `-q` - [where flag](#where-flag-output-filtering): with `--where` -**Note**: The `validate` command does not have a `list` subcommand and ignores the `format` and `where` flags. +#### Format flag + +All `list` subcommands support the `--format` flag with the following values: + +- `txt`: text (tabbed tables) +- `csv`: Comma Separated Value (CSV), e.g., for spreadsheets +- `md`: Markdown, e.g., for GitHub + +Some commands, which can output lists of JSON objects, also support JSON format using the `json` value. + +##### Example: `--format` flag + +This example uses the `--format` flag on the `schema` command to output in markdown: + +```bash +./sbom-utility schema --format md -q +``` + +```md +|name|format|version|variant|file (local)|url (remote)| +|:--|:--|:--|:--|:--|:--| +|CycloneDX v1.5|CycloneDX|1.5|(latest)|schema/cyclonedx/1.5/bom-1.5.schema.json|https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.5.schema.json| +|CycloneDX v1.4|CycloneDX|1.4|(latest)|schema/cyclonedx/1.4/bom-1.4.schema.json|https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.4.schema.json| +|CycloneDX/specification/master/schema/bom-1.3-strict.schema.json| +|CycloneDX v1.3|CycloneDX|1.3|(latest)|schema/cyclonedx/1.3/bom-1.3.schema.json|https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.3.schema.json| +|CycloneDX/specification/master/schema/bom-1.2-strict.schema.json| +|CycloneDX v1.2|CycloneDX|1.2|(latest)|schema/cyclonedx/1.2/bom-1.2.schema.json|https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.2.schema.json| +|SPDX v2.3.1 (development)|SPDX|SPDX-2.3|development|schema/spdx/2.3.1/spdx-schema.json|https://raw.githubusercontent.com/spdx/spdx-spec/development/v2.3.1/schemas/spdx-schema.json| +|SPDX v2.3|SPDX|SPDX-2.3|(latest)|schema/spdx/2.3/spdx-schema.json|https://raw.githubusercontent.com/spdx/spdx-spec/development/v2.3/schemas/spdx-schema.json| +|SPDX v2.2.2|SPDX|SPDX-2.2|(latest)|schema/spdx/2.2.2/spdx-schema.json|https://raw.githubusercontent.com/spdx/spdx-spec/v2.2.2/schemas/spdx-schema.json| +|SPDX v2.2.1|SPDX|SPDX-2.2|2.2.1|schema/spdx/2.2.1/spdx-schema.json|https://raw.githubusercontent.com/spdx/spdx-spec/v2.2.1/schemas/spdx-schema.json| +``` + +### Indent flag + +This flag supplies an integer to any command that encodes JSON output to determine how many spaces to indent nested JSON elements. If not specified, the default indent is `4` (spaces). + +#### Example: indent flag on the query command + +```bash +./sbom-utility query --select name,version --from metadata.component -i examples/cyclonedx/SBOM/juice-shop-11.1.2/bom.json --indent 2 --quiet +``` + +output with `indent 2`: +``` +{ + "name": "juice-shop", + "version": "11.1.2" +} +``` + +```bash +./sbom-utility query --select name,version --from metadata.component -i examples/cyclonedx/SBOM/juice-shop-11.1.2/bom.json --indent 6 --quiet +``` + +output with `indent 6`: +``` +{ + "name": "juice-shop", + "version": "11.1.2" +} +``` #### Input flag @@ -217,39 +279,6 @@ SPDX v2.2.1,SPDX,SPDX-2.2,2.2.1,schema/spdx/2.2.1/spdx-schema.json,https://raw.g - **Note**: You can verify that `output.csv` loads within a spreadsheet app like MS Excel. -#### Format flag - -All `list` subcommands support the `--format` flag with the following values: - -- `txt`: text (tabbed tables) -- `csv`: Comma Separated Value (CSV), e.g., for spreadsheets -- `md`: Markdown, e.g., for GitHub - -Some commands, which can output lists of JSON objects, also support JSON format using the `json` value. - -##### Example: `--format` flag - -This example uses the `--format` flag on the `schema` command to output in markdown: - -```bash -./sbom-utility schema --format md -q -``` - -```md -|name|format|version|variant|file (local)|url (remote)| -|:--|:--|:--|:--|:--|:--| -|CycloneDX v1.5|CycloneDX|1.5|(latest)|schema/cyclonedx/1.5/bom-1.5.schema.json|https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.5.schema.json| -|CycloneDX v1.4|CycloneDX|1.4|(latest)|schema/cyclonedx/1.4/bom-1.4.schema.json|https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.4.schema.json| -|CycloneDX/specification/master/schema/bom-1.3-strict.schema.json| -|CycloneDX v1.3|CycloneDX|1.3|(latest)|schema/cyclonedx/1.3/bom-1.3.schema.json|https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.3.schema.json| -|CycloneDX/specification/master/schema/bom-1.2-strict.schema.json| -|CycloneDX v1.2|CycloneDX|1.2|(latest)|schema/cyclonedx/1.2/bom-1.2.schema.json|https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.2.schema.json| -|SPDX v2.3.1 (development)|SPDX|SPDX-2.3|development|schema/spdx/2.3.1/spdx-schema.json|https://raw.githubusercontent.com/spdx/spdx-spec/development/v2.3.1/schemas/spdx-schema.json| -|SPDX v2.3|SPDX|SPDX-2.3|(latest)|schema/spdx/2.3/spdx-schema.json|https://raw.githubusercontent.com/spdx/spdx-spec/development/v2.3/schemas/spdx-schema.json| -|SPDX v2.2.2|SPDX|SPDX-2.2|(latest)|schema/spdx/2.2.2/spdx-schema.json|https://raw.githubusercontent.com/spdx/spdx-spec/v2.2.2/schemas/spdx-schema.json| -|SPDX v2.2.1|SPDX|SPDX-2.2|2.2.1|schema/spdx/2.2.1/spdx-schema.json|https://raw.githubusercontent.com/spdx/spdx-spec/v2.2.1/schemas/spdx-schema.json| -``` - #### Quiet flag All commands support the `--quiet` flag. By default, the utility outputs informational (INFO), warning (WARNING) and error (ERROR) text along with the actual command results to `stdout`. If you wish to only see the command results (JSON) or report (tables) you can run any command in "quiet mode" by simply supplying the `--quiet` or its short-form `-q` flag. diff --git a/cmd/license.go b/cmd/license.go index a7d90a2f..a817e054 100644 --- a/cmd/license.go +++ b/cmd/license.go @@ -42,8 +42,8 @@ const ( func NewCommandLicense() *cobra.Command { var command = new(cobra.Command) command.Use = "license" - command.Short = "Process licenses found in SBOM input file" - command.Long = "Process licenses found in SBOM input file" + command.Short = "Process licenses found in the BOM input file" + command.Long = "Process licenses found in the BOM input file" command.RunE = licenseCmdImpl command.ValidArgs = VALID_SUBCOMMANDS_LICENSE command.PreRunE = func(cmd *cobra.Command, args []string) (err error) { diff --git a/cmd/license_list.go b/cmd/license_list.go index c8d71969..ab948d12 100644 --- a/cmd/license_list.go +++ b/cmd/license_list.go @@ -28,7 +28,6 @@ import ( "text/tabwriter" "github.com/CycloneDX/sbom-utility/common" - "github.com/CycloneDX/sbom-utility/log" "github.com/CycloneDX/sbom-utility/schema" "github.com/CycloneDX/sbom-utility/utils" "github.com/spf13/cobra" @@ -266,7 +265,7 @@ func ListLicenses(writer io.Writer, policyConfig *schema.LicensePolicyConfig, // NOTE: if no license are found, the "json.Marshal" method(s) will return a value of "null" // which is valid JSON (and not an empty array) // TODO: Support de-duplication (flag) (which MUST be exact using deep comparison) -func DisplayLicenseListJson(bom *schema.BOM, output io.Writer) { +func DisplayLicenseListJson(bom *schema.BOM, writer io.Writer) { getLogger().Enter() defer getLogger().Exit() @@ -283,21 +282,21 @@ func DisplayLicenseListJson(bom *schema.BOM, output io.Writer) { } } } - json, _ := log.FormatInterfaceAsJson(lc) // Note: JSON data files MUST ends in a newline as this is a POSIX standard - fmt.Fprintf(output, "%s\n", json) + // which is already accounted for by the JSON encoder. + utils.WriteAnyAsEncodedJSONInt(writer, lc, utils.GlobalFlags.PersistentFlags.GetOutputIndentInt()) } // NOTE: This list is NOT de-duplicated -func DisplayLicenseListCSV(bom *schema.BOM, output io.Writer) (err error) { +func DisplayLicenseListCSV(bom *schema.BOM, writer io.Writer) (err error) { getLogger().Enter() defer getLogger().Exit() var licenseInfo schema.LicenseInfo var currentRow []string - w := csv.NewWriter(output) + w := csv.NewWriter(writer) defer w.Flush() // Emit title row @@ -343,7 +342,7 @@ func DisplayLicenseListCSV(bom *schema.BOM, output io.Writer) (err error) { } // NOTE: This list is NOT de-duplicated -func DisplayLicenseListMarkdown(bom *schema.BOM, output io.Writer) { +func DisplayLicenseListMarkdown(bom *schema.BOM, writer io.Writer) { getLogger().Enter() defer getLogger().Exit() @@ -351,11 +350,11 @@ func DisplayLicenseListMarkdown(bom *schema.BOM, output io.Writer) { // create title row titleRow := createMarkdownRow(LICENSE_LIST_TITLES_LICENSE_CHOICE) - fmt.Fprintf(output, "%s\n", titleRow) + fmt.Fprintf(writer, "%s\n", titleRow) alignments := createMarkdownColumnAlignment(LICENSE_LIST_TITLES_LICENSE_CHOICE) alignmentRow := createMarkdownRow(alignments) - fmt.Fprintf(output, "%s\n", alignmentRow) + fmt.Fprintf(writer, "%s\n", alignmentRow) // Display a warning messing in the actual output and return (short-circuit) licenseKeys := bom.LicenseMap.KeySet() @@ -395,7 +394,7 @@ func DisplayLicenseListMarkdown(bom *schema.BOM, output io.Writer) { content) lineRow = createMarkdownRow(line) - fmt.Fprintf(output, "%s\n", lineRow) + fmt.Fprintf(writer, "%s\n", lineRow) } } @@ -406,7 +405,7 @@ func DisplayLicenseListMarkdown(bom *schema.BOM, output io.Writer) { // TODO: Make policy column optional // TODO: Add a --no-title flag to skip title output // TODO: Support a new --sort flag -func DisplayLicenseListSummaryText(bom *schema.BOM, output io.Writer) { +func DisplayLicenseListSummaryText(bom *schema.BOM, writer io.Writer) { getLogger().Enter() defer getLogger().Exit() @@ -415,7 +414,7 @@ func DisplayLicenseListSummaryText(bom *schema.BOM, output io.Writer) { defer w.Flush() // min-width, tab-width, padding, pad-char, flags - w.Init(output, 8, 2, 2, ' ', 0) + w.Init(writer, 8, 2, 2, ' ', 0) var licenseInfo schema.LicenseInfo @@ -458,12 +457,12 @@ func DisplayLicenseListSummaryText(bom *schema.BOM, output io.Writer) { // TODO: Make policy column optional // TODO: Add a --no-title flag to skip title output // TODO: Support a new --sort flag -func DisplayLicenseListSummaryCSV(bom *schema.BOM, output io.Writer) (err error) { +func DisplayLicenseListSummaryCSV(bom *schema.BOM, writer io.Writer) (err error) { getLogger().Enter() defer getLogger().Exit() // initialize writer and prepare the list of entries (i.e., the "rows") - w := csv.NewWriter(output) + w := csv.NewWriter(writer) defer w.Flush() var currentRow []string @@ -526,7 +525,7 @@ func DisplayLicenseListSummaryCSV(bom *schema.BOM, output io.Writer) (err error) // TODO: Make policy column optional // TODO: Add a --no-title flag to skip title output // TODO: Support a new --sort flag -func DisplayLicenseListSummaryMarkdown(bom *schema.BOM, output io.Writer) { +func DisplayLicenseListSummaryMarkdown(bom *schema.BOM, writer io.Writer) { getLogger().Enter() defer getLogger().Exit() @@ -534,11 +533,11 @@ func DisplayLicenseListSummaryMarkdown(bom *schema.BOM, output io.Writer) { // create title row titleRow := createMarkdownRow(LICENSE_SUMMARY_TITLES) - fmt.Fprintf(output, "%s\n", titleRow) + fmt.Fprintf(writer, "%s\n", titleRow) alignments := createMarkdownColumnAlignment(LICENSE_SUMMARY_TITLES) alignmentRow := createMarkdownRow(alignments) - fmt.Fprintf(output, "%s\n", alignmentRow) + fmt.Fprintf(writer, "%s\n", alignmentRow) // Display a warning messing in the actual output and return (short-circuit) licenseKeys := bom.LicenseMap.KeySet() @@ -572,7 +571,7 @@ func DisplayLicenseListSummaryMarkdown(bom *schema.BOM, output io.Writer) { ) lineRow = createMarkdownRow(line) - fmt.Fprintf(output, "%s\n", lineRow) + fmt.Fprintf(writer, "%s\n", lineRow) } } } diff --git a/cmd/license_policy.go b/cmd/license_policy.go index 51cafcb6..07631f15 100644 --- a/cmd/license_policy.go +++ b/cmd/license_policy.go @@ -245,7 +245,7 @@ func ListLicensePolicies(writer io.Writer, policyConfig *schema.LicensePolicyCon // NOTE: assumes all entries in the policy config file MUST have family names // TODO: Allow caller to pass flag to truncate or not (perhaps with value) // TODO: Add a --no-title flag to skip title output -func DisplayLicensePoliciesTabbedText(output io.Writer, filteredPolicyMap *slicemultimap.MultiMap, flags utils.LicenseCommandFlags) (err error) { +func DisplayLicensePoliciesTabbedText(writer io.Writer, filteredPolicyMap *slicemultimap.MultiMap, flags utils.LicenseCommandFlags) (err error) { getLogger().Enter() defer getLogger().Exit() @@ -254,7 +254,7 @@ func DisplayLicensePoliciesTabbedText(output io.Writer, filteredPolicyMap *slice defer w.Flush() // min-width, tab-width, padding, pad-char, flags - w.Init(output, 8, 2, 2, ' ', 0) + w.Init(writer, 8, 2, 2, ' ', 0) // create title row and underline row from slices of optional and compulsory titles titles, underlines := prepareReportTitleData(LICENSE_POLICY_LIST_ROW_DATA, flags.Summary) @@ -330,12 +330,12 @@ func DisplayLicensePoliciesTabbedText(output io.Writer, filteredPolicyMap *slice } // TODO: Add a --no-title flag to skip title output -func DisplayLicensePoliciesCSV(output io.Writer, filteredPolicyMap *slicemultimap.MultiMap, flags utils.LicenseCommandFlags) (err error) { +func DisplayLicensePoliciesCSV(writer io.Writer, filteredPolicyMap *slicemultimap.MultiMap, flags utils.LicenseCommandFlags) (err error) { getLogger().Enter() defer getLogger().Exit() // initialize writer and prepare the list of entries (i.e., the "rows") - w := csv.NewWriter(output) + w := csv.NewWriter(writer) defer w.Flush() // Create title row data as []string @@ -351,7 +351,7 @@ func DisplayLicensePoliciesCSV(output io.Writer, filteredPolicyMap *slicemultima // Emit no schemas found warning into output // TODO Use only for Warning messages, do not emit in output table if len(keyNames) == 0 { - fmt.Fprintf(output, "%s\n", MSG_OUTPUT_NO_POLICIES_FOUND) + fmt.Fprintf(writer, "%s\n", MSG_OUTPUT_NO_POLICIES_FOUND) return fmt.Errorf(MSG_OUTPUT_NO_POLICIES_FOUND) } @@ -382,7 +382,7 @@ func DisplayLicensePoliciesCSV(output io.Writer, filteredPolicyMap *slicemultima } // TODO: Add a --no-title flag to skip title output -func DisplayLicensePoliciesMarkdown(output io.Writer, filteredPolicyMap *slicemultimap.MultiMap, flags utils.LicenseCommandFlags) (err error) { +func DisplayLicensePoliciesMarkdown(writer io.Writer, filteredPolicyMap *slicemultimap.MultiMap, flags utils.LicenseCommandFlags) (err error) { getLogger().Enter() defer getLogger().Exit() @@ -391,11 +391,11 @@ func DisplayLicensePoliciesMarkdown(output io.Writer, filteredPolicyMap *slicemu // create title row titleRow := createMarkdownRow(titles) - fmt.Fprintf(output, "%s\n", titleRow) + fmt.Fprintf(writer, "%s\n", titleRow) alignments := createMarkdownColumnAlignment(titles) alignmentRow := createMarkdownRow(alignments) - fmt.Fprintf(output, "%s\n", alignmentRow) + fmt.Fprintf(writer, "%s\n", alignmentRow) // Retrieve keys for policies to list keyNames := filteredPolicyMap.KeySet() @@ -404,7 +404,7 @@ func DisplayLicensePoliciesMarkdown(output io.Writer, filteredPolicyMap *slicemu // Emit no schemas found warning into output // TODO Use only for Warning messages, do not emit in output table if len(keyNames) == 0 { - fmt.Fprintf(output, "%s\n", MSG_OUTPUT_NO_POLICIES_FOUND) + fmt.Fprintf(writer, "%s\n", MSG_OUTPUT_NO_POLICIES_FOUND) return fmt.Errorf(MSG_OUTPUT_NO_POLICIES_FOUND) } @@ -428,7 +428,7 @@ func DisplayLicensePoliciesMarkdown(output io.Writer, filteredPolicyMap *slicemu flags.Summary, ) lineRow = createMarkdownRow(line) - fmt.Fprintf(output, "%s\n", lineRow) + fmt.Fprintf(writer, "%s\n", lineRow) } } return diff --git a/cmd/license_policy_test.go b/cmd/license_policy_test.go index 32c25f01..c14bfe93 100644 --- a/cmd/license_policy_test.go +++ b/cmd/license_policy_test.go @@ -55,7 +55,7 @@ const ( // ------------------------------------------- func NewLicensePolicyTestInfoBasic(format string, listLineWrap bool) *LicenseTestInfo { - lti := NewLicenseTestInfoBasic("", format, TI_LIST_SUMMARY_FALSE) + lti := NewLicenseTestInfo("", format, TI_LIST_SUMMARY_FALSE) lti.ListLineWrap = listLineWrap return lti } @@ -341,7 +341,7 @@ func TestLicensePolicyCustomListGoodBadMaybe(t *testing.T) { // test for sep. row TEST_LINE_NUM := 1 TEST_VALUES := []string{REPORT_LIST_TITLE_ROW_SEPARATOR} - matchFoundLine, matchFound := lineContainsValues(outputBuffer, TEST_LINE_NUM, TEST_VALUES...) + matchFoundLine, matchFound := bufferLineContainsValues(outputBuffer, TEST_LINE_NUM, TEST_VALUES...) if !matchFound { t.Errorf("policy file does not contain expected values: `%v` at line: %v\n", TEST_VALUES, TEST_LINE_NUM) return @@ -352,7 +352,7 @@ func TestLicensePolicyCustomListGoodBadMaybe(t *testing.T) { // Assure "bad" policy has usage "deny" TEST_LINE_NUM = 2 TEST_VALUES = []string{LICENSE_ID_BAD, schema.POLICY_DENY} - matchFoundLine, matchFound = lineContainsValues(outputBuffer, TEST_LINE_NUM, TEST_VALUES...) + matchFoundLine, matchFound = bufferLineContainsValues(outputBuffer, TEST_LINE_NUM, TEST_VALUES...) if !matchFound { t.Errorf("policy file does not contain expected values: `%v` at line: %v\n", TEST_VALUES, TEST_LINE_NUM) return @@ -363,7 +363,7 @@ func TestLicensePolicyCustomListGoodBadMaybe(t *testing.T) { // Assure "good" policy has usage "allow" TEST_LINE_NUM = 3 TEST_VALUES = []string{LICENSE_ID_GOOD, schema.POLICY_ALLOW} - matchFoundLine, matchFound = lineContainsValues(outputBuffer, TEST_LINE_NUM, TEST_VALUES...) + matchFoundLine, matchFound = bufferLineContainsValues(outputBuffer, TEST_LINE_NUM, TEST_VALUES...) if !matchFound { t.Errorf("policy file does not contain expected values: `%v` at line: %v\n", TEST_VALUES, TEST_LINE_NUM) return @@ -374,7 +374,7 @@ func TestLicensePolicyCustomListGoodBadMaybe(t *testing.T) { // Assure "maybe" policy has usage "needs-review" TEST_LINE_NUM = 4 TEST_VALUES = []string{LICENSE_ID_MAYBE, schema.POLICY_NEEDS_REVIEW} - matchFoundLine, matchFound = lineContainsValues(outputBuffer, TEST_LINE_NUM, TEST_VALUES...) + matchFoundLine, matchFound = bufferLineContainsValues(outputBuffer, TEST_LINE_NUM, TEST_VALUES...) if !matchFound { t.Errorf("policy file does not contain expected values: `%v` at line: %v\n", TEST_VALUES, TEST_LINE_NUM) return diff --git a/cmd/license_test.go b/cmd/license_test.go index 77e06e68..1bf18ac5 100644 --- a/cmd/license_test.go +++ b/cmd/license_test.go @@ -52,26 +52,11 @@ type LicenseTestInfo struct { } func (ti *LicenseTestInfo) String() string { - pParent := &ti.CommonTestInfo - return pParent.String() + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti) + return buffer.String() } -func NewLicenseTestInfo(inputFile string, listFormat string, listSummary bool, whereClause string, - resultContainsValues []string, resultExpectedLineCount int, resultExpectedError error, - listLineWrap bool, policyFile string) *LicenseTestInfo { - - var ti = new(LicenseTestInfo) - var pCommon = &ti.CommonTestInfo - // initialize common fields - pCommon.Init(inputFile, listFormat, listSummary, whereClause, - resultContainsValues, resultExpectedLineCount, resultExpectedError) - // Initialize resource-unique fields - ti.ListLineWrap = listLineWrap - ti.PolicyFile = policyFile - return ti -} - -func NewLicenseTestInfoBasic(inputFile string, listFormat string, listSummary bool) *LicenseTestInfo { +func NewLicenseTestInfo(inputFile string, listFormat string, listSummary bool) *LicenseTestInfo { var ti = new(LicenseTestInfo) var pCommon = &ti.CommonTestInfo pCommon.InitBasic(inputFile, listFormat, nil) @@ -90,10 +75,10 @@ func innerTestLicenseListBuffered(t *testing.T, testInfo *LicenseTestInfo, where defer outputWriter.Flush() // Use a test input SBOM formatted in SPDX - // TODO: see if we can use global flags (i.e., policy filename as a persistent flag) - // >>> utils.GlobalFlags.ConfigLicensePolicyFile = testInfo.PolicyFile utils.GlobalFlags.PersistentFlags.InputFile = testInfo.InputFile utils.GlobalFlags.PersistentFlags.OutputFormat = testInfo.OutputFormat + utils.GlobalFlags.PersistentFlags.OutputFile = testInfo.OutputFile + utils.GlobalFlags.PersistentFlags.OutputIndent = testInfo.OutputIndent utils.GlobalFlags.LicenseFlags.Summary = testInfo.ListSummary // set license policy config. per-test @@ -123,11 +108,16 @@ func innerTestLicenseList(t *testing.T, testInfo *LicenseTestInfo) (outputBuffer // Perform the test with buffered output outputBuffer, err = innerTestLicenseListBuffered(t, testInfo, whereFilters) + if err != nil { + getLogger().Tracef("%s", err) + return + } // Run all common tests against "result" values in the CommonTestInfo struct err = innerRunReportResultTests(t, &testInfo.CommonTestInfo, outputBuffer, err) if err != nil { getLogger().Tracef("%s", err) + return } return @@ -153,7 +143,7 @@ func innerTestLicenseExpressionParsing(t *testing.T, expression string, expected // ---------------------------------------- func TestLicenseListInvalidInputFileLoad(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_INPUT_FILE_NON_EXISTENT, FORMAT_DEFAULT, false) + lti := NewLicenseTestInfo(TEST_INPUT_FILE_NON_EXISTENT, FORMAT_DEFAULT, false) lti.ResultExpectedError = &fs.PathError{} innerTestLicenseList(t, lti) } @@ -162,13 +152,13 @@ func TestLicenseListInvalidInputFileLoad(t *testing.T) { // Test format unsupported (SPDX) // ------------------------------------------- func TestLicenseListFormatUnsupportedSPDX1(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_SPDX_2_2_MIN_REQUIRED, FORMAT_DEFAULT, false) + lti := NewLicenseTestInfo(TEST_SPDX_2_2_MIN_REQUIRED, FORMAT_DEFAULT, false) lti.ResultExpectedError = &schema.UnsupportedFormatError{} innerTestLicenseList(t, lti) } func TestLicenseListFormatUnsupportedSPDX2(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_SPDX_2_2_EXAMPLE_1, FORMAT_DEFAULT, false) + lti := NewLicenseTestInfo(TEST_SPDX_2_2_EXAMPLE_1, FORMAT_DEFAULT, false) lti.ResultExpectedError = &schema.UnsupportedFormatError{} innerTestLicenseList(t, lti) } @@ -182,48 +172,61 @@ func TestLicenseListFormatUnsupportedSPDX2(t *testing.T) { // Note: this includes licenses in ANY hierarchical nesting of components as well. func TestLicenseListCdx13JsonNoneFound(t *testing.T) { // Test CDX 1.3 document - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3_NONE_FOUND, FORMAT_JSON, false) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3_NONE_FOUND, FORMAT_JSON, false) lti.ResultExpectedLineCount = 1 // null (valid json) innerTestLicenseList(t, lti) } func TestLicenseListCdx14JsonNoneFound(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_4_NONE_FOUND, FORMAT_JSON, false) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_4_NONE_FOUND, FORMAT_JSON, false) lti.ResultExpectedLineCount = 1 // null (valid json) innerTestLicenseList(t, lti) } func TestLicenseListCdx13CsvNoneFound(t *testing.T) { // Test CDX 1.3 document - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3_NONE_FOUND, FORMAT_CSV, false) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3_NONE_FOUND, FORMAT_CSV, false) lti.ResultExpectedLineCount = 1 // title only innerTestLicenseList(t, lti) } func TestLicenseListCdx14CsvNoneFound(t *testing.T) { // Test CDX 1.4 document - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_4_NONE_FOUND, FORMAT_CSV, false) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_4_NONE_FOUND, FORMAT_CSV, false) lti.ResultExpectedLineCount = 1 // title only innerTestLicenseList(t, lti) } func TestLicenseListCdx13MarkdownNoneFound(t *testing.T) { // Test CDX 1.3 document - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3_NONE_FOUND, FORMAT_MARKDOWN, false) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3_NONE_FOUND, FORMAT_MARKDOWN, false) lti.ResultExpectedLineCount = 2 // title and separator rows innerTestLicenseList(t, lti) } func TestLicenseListCdx14MarkdownNoneFound(t *testing.T) { // Test CDX 1.4 document - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_4_NONE_FOUND, FORMAT_MARKDOWN, false) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_4_NONE_FOUND, FORMAT_MARKDOWN, false) lti.ResultExpectedLineCount = 2 // title and separator rows innerTestLicenseList(t, lti) } func TestLicenseListCdx13Json(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3, FORMAT_JSON, false) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3, FORMAT_JSON, false) lti.ResultExpectedLineCount = 92 // array of LicenseChoice JSON objects - innerTestLicenseList(t, lti) + lti.OutputIndent = 6 + buffer := innerTestLicenseList(t, lti) + + numLines, lines := getBufferLinesAndCount(buffer) + + // if numLines != cti.ResultExpectedLineCount { + // t.Errorf("invalid test result: expected: `%v` lines, actual: `%v", cti.ResultExpectedLineCount, numLines) + // } + if numLines > lti.ResultExpectedIndentAtLineNum { + line := lines[lti.ResultExpectedIndentAtLineNum] + if spaceCount := numberOfLeadingSpaces(line); spaceCount != lti.ResultExpectedIndentLength { + t.Errorf("invalid test result: expected indent:`%v`, actual: `%v", lti.ResultExpectedIndentLength, spaceCount) + } + } } //--------------------------- @@ -232,25 +235,25 @@ func TestLicenseListCdx13Json(t *testing.T) { // Assure listing (report) works with summary flag (i.e., format: "txt") func TestLicenseListSummaryCdx13Text(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3, FORMAT_TEXT, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3, FORMAT_TEXT, true) lti.ResultExpectedLineCount = 20 // title, separator and data rows innerTestLicenseList(t, lti) } func TestLicenseListSummaryCdx13Markdown(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3, FORMAT_MARKDOWN, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3, FORMAT_MARKDOWN, true) lti.ResultExpectedLineCount = 20 // title, separator and data rows innerTestLicenseList(t, lti) } func TestLicenseListSummaryCdx13Csv(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3, FORMAT_CSV, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3, FORMAT_CSV, true) lti.ResultExpectedLineCount = 19 // title and data rows innerTestLicenseList(t, lti) } func TestLicenseListTextSummaryCdx14ContainsUndefined(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_4_NONE_FOUND, FORMAT_DEFAULT, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_4_NONE_FOUND, FORMAT_DEFAULT, true) lti.ResultExpectedLineCount = 4 // 2 title, 2 with UNDEFINED unknownLCValue := schema.GetLicenseChoiceTypeName(schema.LC_LOC_UNKNOWN) lti.ResultLineContainsValues = []string{schema.POLICY_UNDEFINED, unknownLCValue, LICENSE_NO_ASSERTION, "package-lock.json"} @@ -260,7 +263,7 @@ func TestLicenseListTextSummaryCdx14ContainsUndefined(t *testing.T) { func TestLicenseListPolicyCdx14InvalidLicenseId(t *testing.T) { TEST_LICENSE_ID_OR_NAME := "foo" - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_TEXT_CDX_1_4_INVALID_LICENSE_ID, FORMAT_TEXT, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_TEXT_CDX_1_4_INVALID_LICENSE_ID, FORMAT_TEXT, true) lti.ResultLineContainsValues = []string{schema.POLICY_UNDEFINED, schema.LC_VALUE_ID, TEST_LICENSE_ID_OR_NAME} lti.ResultLineContainsValuesAtLineNum = 3 innerTestLicenseList(t, lti) @@ -268,7 +271,7 @@ func TestLicenseListPolicyCdx14InvalidLicenseId(t *testing.T) { func TestLicenseListPolicyCdx14InvalidLicenseName(t *testing.T) { TEST_LICENSE_ID_OR_NAME := "bar" - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_TEXT_CDX_1_4_INVALID_LICENSE_NAME, FORMAT_TEXT, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_TEXT_CDX_1_4_INVALID_LICENSE_NAME, FORMAT_TEXT, true) lti.ResultLineContainsValues = []string{schema.POLICY_UNDEFINED, schema.LC_VALUE_NAME, TEST_LICENSE_ID_OR_NAME} lti.ResultLineContainsValuesAtLineNum = 3 innerTestLicenseList(t, lti) @@ -278,28 +281,28 @@ func TestLicenseListPolicyCdx14InvalidLicenseName(t *testing.T) { // Where filter tests // --------------------------- func TestLicenseListSummaryTextCdx13WhereUsageNeedsReview(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3, FORMAT_TEXT, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3, FORMAT_TEXT, true) lti.WhereClause = "usage-policy=needs-review" lti.ResultExpectedLineCount = 8 // title and data rows innerTestLicenseList(t, lti) } func TestLicenseListSummaryTextCdx13WhereUsageUndefined(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3, FORMAT_TEXT, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3, FORMAT_TEXT, true) lti.WhereClause = "usage-policy=UNDEFINED" lti.ResultExpectedLineCount = 4 // title and data rows innerTestLicenseList(t, lti) } func TestLicenseListSummaryTextCdx13WhereLicenseTypeName(t *testing.T) { - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_CDX_1_3, FORMAT_TEXT, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_CDX_1_3, FORMAT_TEXT, true) lti.WhereClause = "license-type=name" lti.ResultExpectedLineCount = 8 // title and data rows innerTestLicenseList(t, lti) } func TestLicenseListSummaryTextCdx14LicenseExpInName(t *testing.T) { - lti := NewLicenseTestInfoBasic( + lti := NewLicenseTestInfo( TEST_LICENSE_LIST_CDX_1_4_LICENSE_EXPRESSION_IN_NAME, FORMAT_TEXT, true) lti.WhereClause = "license-type=name" @@ -311,13 +314,13 @@ func TestLicenseListSummaryTextCdx14LicenseExpInName(t *testing.T) { // Test custom marshal of CDXLicense (empty CDXAttachment) func TestLicenseListCdx13JsonEmptyAttachment(t *testing.T) { - lti := NewLicenseTestInfoBasic( + lti := NewLicenseTestInfo( "test/cyclonedx/cdx-1-3-license-list-no-attachment.json", FORMAT_JSON, false) lti.ResultExpectedLineCount = 36 lti.ResultLineContainsValues = []string{"\"content\": \"CiAgICAgICAgICAgICA...\""} - lti.ResultLineContainsValuesAtLineNum = -1 // JSON Hashmaps in Go are not ordered + lti.ResultLineContainsValuesAtLineNum = -1 // JSON Hashmaps in Go are not ordered, match any line innerTestLicenseList(t, lti) } @@ -422,7 +425,7 @@ const ( func TestLicenseListPolicyCdx14CustomPolicy(t *testing.T) { TEST_LICENSE_ID_OR_NAME := "(MIT OR CC0-1.0)" - lti := NewLicenseTestInfoBasic(TEST_LICENSE_LIST_TEXT_CDX_1_4_CUSTOM_POLICY_1, FORMAT_TEXT, true) + lti := NewLicenseTestInfo(TEST_LICENSE_LIST_TEXT_CDX_1_4_CUSTOM_POLICY_1, FORMAT_TEXT, true) lti.ResultLineContainsValues = []string{schema.POLICY_ALLOW, schema.LC_VALUE_EXPRESSION, TEST_LICENSE_ID_OR_NAME} lti.ResultLineContainsValuesAtLineNum = 2 lti.PolicyFile = TEST_CUSTOM_POLICY_1 diff --git a/cmd/query.go b/cmd/query.go index fa6894d6..1044dc89 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -207,16 +207,16 @@ func Query(writer io.Writer, request *common.QueryRequest, response *common.Quer return } - // Convert query results to formatted JSON for output - // TODO: we MAY want to use a JSON Encoder to avoid unicode encoding - var formattedResult string - if formattedResult, err = utils.MarshalAnyToFormattedJsonString(resultJson); err != nil { - getLogger().Debugf("unhandled error: %s, QueryRequest: %s", err, request.String()) - return - } - // Use the selected output device (e.g., default stdout or the specified --output-file) - fmt.Fprintf(writer, "%s\n", formattedResult) + // Note: JSON data files MUST ends in a newline as this is a POSIX standard + // which is already accounted for by the JSON encoder. + _, err = utils.WriteAnyAsEncodedJSONInt(writer, resultJson, + utils.GlobalFlags.PersistentFlags.GetOutputIndentInt()) + + // NOTE: previously, query results defaulted to an indent of 2 spaces which could be done + // just for this command as follows: + // flags := rootCmd.PersistentFlags() + // flags.Set(FLAG_OUTPUT_INDENT, "2") return } diff --git a/cmd/query_test.go b/cmd/query_test.go index f5706a41..45a55249 100644 --- a/cmd/query_test.go +++ b/cmd/query_test.go @@ -23,7 +23,9 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/fs" + "os" "testing" "github.com/CycloneDX/sbom-utility/common" @@ -31,52 +33,93 @@ import ( "github.com/CycloneDX/sbom-utility/utils" ) -// TODO: Consolidate query request declarations here +// ------------------------------------------- +// test helper functions +// ------------------------------------------- + +func innerQueryError(t *testing.T, cti *CommonTestInfo, queryRequest *common.QueryRequest, expectedError error) (result interface{}, actualError error) { + getLogger().Enter() + defer getLogger().Exit() + + result, _, actualError = innerQuery(t, cti, queryRequest) + + // if the query resulted in a failure + if !ErrorTypesMatch(actualError, expectedError) { + getLogger().Tracef("expected error type: `%T`, actual type: `%T`", expectedError, actualError) + t.Errorf("expected error type: `%T`, actual type: `%T`", expectedError, actualError) + } + return +} // NOTE: This function "mocks" what the "queryCmdImpl()" function would do -func innerQuery(t *testing.T, filename string, queryRequest *common.QueryRequest, autofail bool) (result interface{}, err error) { +func innerQuery(t *testing.T, cti *CommonTestInfo, queryRequest *common.QueryRequest) (resultJson interface{}, outputBuffer bytes.Buffer, err error) { getLogger().Enter() defer getLogger().Exit() // Copy the test filename to the command line flags were the code looks for it - utils.GlobalFlags.PersistentFlags.InputFile = filename - - // Declare an output outputBuffer/outputWriter to use used during tests - var outputBuffer bytes.Buffer - var outputWriter = bufio.NewWriter(&outputBuffer) - // ensure all data is written to buffer before further validation - defer outputWriter.Flush() + utils.GlobalFlags.PersistentFlags.InputFile = cti.InputFile // allocate response/result object and invoke query - var response = new(common.QueryResponse) - result, err = Query(outputWriter, queryRequest, response) + var queryResponse = new(common.QueryResponse) + resultJson, outputBuffer, err = innerBufferedTestQuery(t, cti, queryRequest, queryResponse) - // if the query resulted in a failure + // if the command resulted in a failure if err != nil { // if tests asks us to report a FAIL to the test framework - if autofail { - t.Errorf("%s: failed: %v\nquery:\n%s", filename, err, queryRequest) + if cti.Autofail { + encodedTestInfo, _ := utils.EncodeAnyToDefaultIndentedJSONStr(queryRequest) + t.Errorf("%s: failed: %v\nQueryRequest:\n%s", cti.InputFile, err, encodedTestInfo.String()) } return } - // This will print results ONLY if --quiet mode is `false` - printMarshaledResultOnlyIfNotQuiet(result) + // Log results if trace enabled + if err != nil { + var buffer bytes.Buffer + buffer, err = utils.EncodeAnyToDefaultIndentedJSONStr(resultJson) + // Output the JSON data directly to stdout (not subject to log-level) + getLogger().Tracef("%s\n", buffer.String()) + } return } -func innerQueryError(t *testing.T, filename string, queryRequest *common.QueryRequest, expectedError error) (result interface{}, actualError error) { - getLogger().Enter() - defer getLogger().Exit() - - result, actualError = innerQuery(t, filename, queryRequest, false) - - // if the query resulted in a failure - if !ErrorTypesMatch(actualError, expectedError) { - getLogger().Tracef("expected error type: `%T`, actual type: `%T`", expectedError, actualError) - t.Errorf("expected error type: `%T`, actual type: `%T`", expectedError, actualError) +func innerBufferedTestQuery(t *testing.T, testInfo *CommonTestInfo, queryRequest *common.QueryRequest, queryResponse *common.QueryResponse) (resultJson interface{}, outputBuffer bytes.Buffer, err error) { + + // The command looks for the input & output filename in global flags struct + utils.GlobalFlags.PersistentFlags.InputFile = testInfo.InputFile + utils.GlobalFlags.PersistentFlags.OutputFile = testInfo.OutputFile + utils.GlobalFlags.PersistentFlags.OutputFormat = testInfo.OutputFormat + utils.GlobalFlags.PersistentFlags.OutputIndent = testInfo.OutputIndent + var outputWriter io.Writer + var outputFile *os.File + + // TODO: centralize this logic to a function all Commands can use... + // Note: Any "Mocking" of os.Stdin/os.Stdout should be done in functions that call this one + if testInfo.OutputFile == "" { + // Declare an output outputBuffer/outputWriter to use used during tests + bufferedWriter := bufio.NewWriter(&outputBuffer) + outputWriter = bufferedWriter + // MUST ensure all data is written to buffer before further testing + defer bufferedWriter.Flush() + } else { + outputFile, outputWriter, err = createOutputFile(testInfo.OutputFile) + getLogger().Tracef("outputFile: `%v`; writer: `%v`", testInfo.OutputFile, outputWriter) + + // use function closure to assure consistent error output based upon error type + defer func() { + // always close the output file (even if error, as long as file handle returned) + if outputFile != nil { + outputFile.Close() + getLogger().Infof("Closed output file: `%s`", testInfo.OutputFile) + } + }() + + if err != nil { + return + } } + resultJson, err = Query(outputWriter, queryRequest, queryResponse) return } @@ -127,10 +170,11 @@ func VerifySelectedFieldsInJsonMap(t *testing.T, keys []string, results interfac // ---------------------------------------- func TestQueryFailInvalidInputFileLoad(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_INPUT_FILE_NON_EXISTENT) request, _ := common.NewQueryRequestSelectWildcardFrom( "metadata.properties") // Assure we return path error - _, _ = innerQueryError(t, TEST_INPUT_FILE_NON_EXISTENT, request, &fs.PathError{}) + _, _ = innerQueryError(t, cti, request, &fs.PathError{}) } // ---------------------------------------- @@ -138,25 +182,28 @@ func TestQueryFailInvalidInputFileLoad(t *testing.T) { // ---------------------------------------- func TestQueryCdx14BomFormatSpecVersion(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( "bomFormat,specVersion", "") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) _, _ = VerifySelectedFieldsInJsonMap(t, request.GetSelectKeys(), results) } func TestQueryCdx14MetadataTimestampField(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( "timestamp", "metadata") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) _, _ = VerifySelectedFieldsInJsonMap(t, request.GetSelectKeys(), results) } func TestQueryCdx14MetadataComponentAll(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectWildcardFrom( "metadata.component") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) // Test for concrete keys that SHOULD have been found using wildcard fields := []string{ "type", "bom-ref", "purl", "version", "externalReferences", @@ -166,61 +213,68 @@ func TestQueryCdx14MetadataComponentAll(t *testing.T) { } func TestQueryCdx14MetadataComponentNameDescriptionVersion(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( "name,description,version", "metadata.component") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) _, _ = VerifySelectedFieldsInJsonMap(t, request.GetSelectKeys(), results) } func TestQueryCdx14MetadataSupplier(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( common.QUERY_TOKEN_WILDCARD, "metadata.supplier") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) // Test for concrete keys that SHOULD have been found using wildcard fields := []string{"name", "url", "contact"} _, _ = VerifySelectedFieldsInJsonMap(t, fields, results) } func TestQueryCdx14MetadataManufacturer(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( common.QUERY_TOKEN_WILDCARD, "metadata.manufacture") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) // Test for concrete keys that SHOULD have been found using wildcard fields := []string{"name", "url", "contact"} _, _ = VerifySelectedFieldsInJsonMap(t, fields, results) } func TestQueryCdx14MetadataComponentLicenses(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( "licenses", "metadata.component") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) _, _ = VerifySelectedFieldsInJsonMap(t, request.GetSelectKeys(), results) } func TestQueryCdx14MetadataComponentSupplier(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( "supplier", "metadata.component") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) _, _ = VerifySelectedFieldsInJsonMap(t, request.GetSelectKeys(), results) } func TestQueryCdx14MetadataComponentPublisher(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( "publisher", "metadata.component") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) _, _ = VerifySelectedFieldsInJsonMap(t, request.GetSelectKeys(), results) } func TestQueryCdx14MetadataAllWithWildcard(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectWildcardFrom( "metadata.component") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) // Check for all known values that should be on the FROM object fields := []string{"type", "bom-ref", "licenses", "properties", "publisher", "purl", "name", "description", "version", "externalReferences"} _, _ = VerifySelectedFieldsInJsonMap(t, fields, results) @@ -228,10 +282,11 @@ func TestQueryCdx14MetadataAllWithWildcard(t *testing.T) { // NOTE: properties is an []interface func TestQueryCdx14MetadataComponentProperties(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( "properties", "metadata.component") - results, _ := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + results, _ := innerQueryError(t, cti, request, nil) _, _ = VerifySelectedFieldsInJsonMap(t, request.GetSelectKeys(), results) } @@ -240,14 +295,16 @@ func TestQueryCdx14MetadataComponentProperties(t *testing.T) { // ---------------------------------------- func TestQueryFailSpdx22Metadata(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_SPDX_2_2_MIN_REQUIRED) request, _ := common.NewQueryRequestSelectFrom( common.QUERY_TOKEN_WILDCARD, "metadata") // Expect a QueryError - _, _ = innerQueryError(t, TEST_SPDX_2_2_MIN_REQUIRED, request, &schema.UnsupportedFormatError{}) + _, _ = innerQueryError(t, cti, request, &schema.UnsupportedFormatError{}) } func TestQueryFailCdx14MetadataComponentInvalidKey(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( common.QUERY_TOKEN_WILDCARD, "metadata.component.foo") @@ -257,13 +314,14 @@ func TestQueryFailCdx14MetadataComponentInvalidKey(t *testing.T) { } // Expect a QueryError - _, err := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, &common.QueryError{}) + _, err := innerQueryError(t, cti, request, &common.QueryError{}) // Assure we received an error with the expected key phrases EvaluateErrorAndKeyPhrases(t, err, expectedErrorStrings) } func TestQueryFailCdx14MetadataComponentInvalidDataType(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( common.QUERY_TOKEN_WILDCARD, "metadata.component.name") @@ -272,12 +330,13 @@ func TestQueryFailCdx14MetadataComponentInvalidDataType(t *testing.T) { MSG_QUERY_INVALID_DATATYPE, } // Expect a QueryError - _, err := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, &common.QueryError{}) + _, err := innerQueryError(t, cti, request, &common.QueryError{}) // Assure we received an error with the expected key phrases EvaluateErrorAndKeyPhrases(t, err, expectedErrorStrings) } func TestQueryFailCdx14MetadataComponentInvalidSelectClause(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( "name,*", "metadata.component") @@ -286,12 +345,13 @@ func TestQueryFailCdx14MetadataComponentInvalidSelectClause(t *testing.T) { MSG_QUERY_ERROR_SELECT_WILDCARD, } // Expect a QueryError - _, err := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, &common.QueryError{}) + _, err := innerQueryError(t, cti, request, &common.QueryError{}) // Assure we received an error with the expected key phrases EvaluateErrorAndKeyPhrases(t, err, expectedErrorStrings) } func TestQueryFailCdx14InvalidFromClauseWithArray(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFrom( common.QUERY_TOKEN_WILDCARD, "metadata.properties.name") @@ -300,7 +360,7 @@ func TestQueryFailCdx14InvalidFromClauseWithArray(t *testing.T) { MSG_QUERY_ERROR_FROM_KEY_SLICE_DEREFERENCE, } // Expect a QueryError - _, err := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, &common.QueryError{}) + _, err := innerQueryError(t, cti, request, &common.QueryError{}) // Assure we received an error with the expected key phrases EvaluateErrorAndKeyPhrases(t, err, expectedErrorStrings) } @@ -358,6 +418,7 @@ func TestQueryCdx14InvalidWhereClauseEmptyRegex(t *testing.T) { } func TestQueryCdx14RequiredDataLegalDisclaimer(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, errNew := common.NewQueryRequestSelectFromWhere( common.QUERY_TOKEN_WILDCARD, "metadata.properties", @@ -367,7 +428,7 @@ func TestQueryCdx14RequiredDataLegalDisclaimer(t *testing.T) { } // WARN!!!! TODO: handle error tests locally until code is complete - result, err := innerQuery(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, false) + result, _, err := innerQuery(t, cti, request) if err != nil { t.Errorf("%s: %v", ERR_TYPE_UNEXPECTED_ERROR, err) } @@ -384,28 +445,30 @@ func TestQueryCdx14RequiredDataLegalDisclaimer(t *testing.T) { } func TestQueryCdx14InvalidWhereClauseOnFromSingleton(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFromWhere( common.QUERY_TOKEN_WILDCARD, "metadata.component", "name=foo") // Note: this produces a warning, not an error - _, err := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + _, err := innerQueryError(t, cti, request, nil) if err != nil { t.Error(err) } } func TestQueryCdx14MetadataToolsSlice(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFromWhere( common.QUERY_TOKEN_WILDCARD, "metadata.tools", "") - result, err := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + result, err := innerQueryError(t, cti, request, nil) if err != nil { t.Error(err) } if !utils.IsJsonSliceType(result) { - fResult, _ := utils.EncodeAnyToIndentedJSON(result) + fResult, _ := utils.EncodeAnyToDefaultIndentedJSONStr(result) t.Error(fmt.Errorf("expected JSON slice. Actual result: %s", fResult.String())) } @@ -413,22 +476,23 @@ func TestQueryCdx14MetadataToolsSlice(t *testing.T) { slice := result.([]interface{}) EXPECTED_SLICE_LENGTH := 2 if actualLength := len(slice); actualLength != EXPECTED_SLICE_LENGTH { - fResult, _ := utils.EncodeAnyToIndentedJSON(result) + fResult, _ := utils.EncodeAnyToDefaultIndentedJSONStr(result) t.Error(fmt.Errorf("expected slice length: %v, actual length: %v. Actual result: %s", EXPECTED_SLICE_LENGTH, actualLength, fResult.String())) } } func TestQueryCdx14MetadataToolsSliceWhereName(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) request, _ := common.NewQueryRequestSelectFromWhere( common.QUERY_TOKEN_WILDCARD, "components", "name=body-parser") - result, err := innerQueryError(t, TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE, request, nil) + result, err := innerQueryError(t, cti, request, nil) if err != nil { t.Error(err) } if !utils.IsJsonSliceType(result) { - fResult, _ := utils.EncodeAnyToIndentedJSON(result) + fResult, _ := utils.EncodeAnyToDefaultIndentedJSONStr(result) t.Error(fmt.Errorf("expected JSON slice. Actual result: %s", fResult.String())) } @@ -436,7 +500,44 @@ func TestQueryCdx14MetadataToolsSliceWhereName(t *testing.T) { slice := result.([]interface{}) EXPECTED_SLICE_LENGTH := 1 if actualLength := len(slice); actualLength != EXPECTED_SLICE_LENGTH { - fResult, _ := utils.EncodeAnyToIndentedJSON(result) + fResult, _ := utils.EncodeAnyToDefaultIndentedJSONStr(result) t.Error(fmt.Errorf("expected slice length: %v, actual length: %v. Actual result: %s", EXPECTED_SLICE_LENGTH, actualLength, fResult.String())) } } + +func TestQueryCdx14MetadataComponentIndent(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) + cti.ResultExpectedLineCount = 6 + cti.ResultExpectedIndentLength = 4 + cti.ResultExpectedIndentAtLineNum = 1 + request, _ := common.NewQueryRequestSelectFrom( + "name,description,version", + "metadata.component") + + // Verify that JSON returned by the query command is able to apply default space indent + results, _ := innerQueryError(t, cti, request, nil) + buffer, _ := utils.EncodeAnyToIndentedJSONStr(results, utils.DEFAULT_JSON_INDENT_STRING) + numLines, lines := getBufferLinesAndCount(buffer) + + if numLines != cti.ResultExpectedLineCount { + t.Errorf("invalid test result: expected: `%v` lines, actual: `%v", cti.ResultExpectedLineCount, numLines) + } + if numLines > cti.ResultExpectedIndentAtLineNum { + line := lines[cti.ResultExpectedIndentAtLineNum] + if spaceCount := numberOfLeadingSpaces(line); spaceCount != cti.ResultExpectedIndentLength { + t.Errorf("invalid test result: expected indent:`%v`, actual: `%v", cti.ResultExpectedIndentLength, spaceCount) + } + } +} + +func TestQueryCdx14MetadataComponentIndentedFileWrite(t *testing.T) { + cti := NewCommonTestInfoBasic(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) + cti.OutputFile = cti.CreateTemporaryTestOutputFilename(TEST_CDX_1_4_MATURITY_EXAMPLE_1_BASE) + request, _ := common.NewQueryRequestSelectFrom( + "name,description,version", + "metadata.component") + _, err := innerQueryError(t, cti, request, nil) + if err != nil { + t.Error(err) + } +} diff --git a/cmd/report_test.go b/cmd/report_test.go index 7f069d2a..442e622f 100644 --- a/cmd/report_test.go +++ b/cmd/report_test.go @@ -51,7 +51,7 @@ func innerRunReportResultTests(t *testing.T, testInfo *CommonTestInfo, outputBuf // TEST: Line contains a set of string values // TODO: support any number of row/values in test info. structure if len(testInfo.ResultLineContainsValues) > 0 { - matchFoundLine, matchFound := lineContainsValues(outputBuffer, testInfo.ResultLineContainsValuesAtLineNum, testInfo.ResultLineContainsValues...) + matchFoundLine, matchFound := bufferLineContainsValues(outputBuffer, testInfo.ResultLineContainsValuesAtLineNum, testInfo.ResultLineContainsValues...) if !matchFound { err = getLogger().Errorf("output does not contain expected values: `%v` at line: %v\n", strings.Join(testInfo.ResultLineContainsValues, ","), testInfo.ResultLineContainsValuesAtLineNum) t.Error(err.Error()) @@ -61,7 +61,7 @@ func innerRunReportResultTests(t *testing.T, testInfo *CommonTestInfo, outputBuf } // TEST: Line Count - if testInfo.ResultExpectedLineCount != TI_DEFAULT_LINE_COUNT { + if testInfo.ResultExpectedLineCount != TI_RESULT_DEFAULT_LINE_COUNT { outputResults := outputBuffer.String() outputLineCount := strings.Count(outputResults, "\n") if outputLineCount != testInfo.ResultExpectedLineCount { diff --git a/cmd/resource.go b/cmd/resource.go index 3d134516..2b845704 100644 --- a/cmd/resource.go +++ b/cmd/resource.go @@ -258,7 +258,7 @@ func loadDocumentResources(document *schema.BOM, resourceType string, whereFilte // NOTE: This list is NOT de-duplicated // TODO: Add a --no-title flag to skip title output -func DisplayResourceListText(bom *schema.BOM, output io.Writer) { +func DisplayResourceListText(bom *schema.BOM, writer io.Writer) { getLogger().Enter() defer getLogger().Exit() @@ -267,7 +267,7 @@ func DisplayResourceListText(bom *schema.BOM, output io.Writer) { defer w.Flush() // min-width, tab-width, padding, pad-char, flags - w.Init(output, 8, 2, 2, ' ', 0) + w.Init(writer, 8, 2, 2, ' ', 0) // create underline row from compulsory titles underlines := createTitleTextSeparators(RESOURCE_LIST_TITLES) @@ -312,12 +312,12 @@ func DisplayResourceListText(bom *schema.BOM, output io.Writer) { } // TODO: Add a --no-title flag to skip title output -func DisplayResourceListCSV(bom *schema.BOM, output io.Writer) (err error) { +func DisplayResourceListCSV(bom *schema.BOM, writer io.Writer) (err error) { getLogger().Enter() defer getLogger().Exit() // initialize writer and prepare the list of entries (i.e., the "rows") - w := csv.NewWriter(output) + w := csv.NewWriter(writer) defer w.Flush() if err = w.Write(RESOURCE_LIST_TITLES); err != nil { @@ -371,24 +371,24 @@ func DisplayResourceListCSV(bom *schema.BOM, output io.Writer) (err error) { } // TODO: Add a --no-title flag to skip title output -func DisplayResourceListMarkdown(bom *schema.BOM, output io.Writer) (err error) { +func DisplayResourceListMarkdown(bom *schema.BOM, writer io.Writer) (err error) { getLogger().Enter() defer getLogger().Exit() // create title row titleRow := createMarkdownRow(RESOURCE_LIST_TITLES) - fmt.Fprintf(output, "%s\n", titleRow) + fmt.Fprintf(writer, "%s\n", titleRow) alignments := createMarkdownColumnAlignment(RESOURCE_LIST_TITLES) alignmentRow := createMarkdownRow(alignments) - fmt.Fprintf(output, "%s\n", alignmentRow) + fmt.Fprintf(writer, "%s\n", alignmentRow) // Display a warning "missing" in the actual output and return (short-circuit) entries := bom.ResourceMap.Entries() // Emit no resource found warning into output if len(entries) == 0 { - fmt.Fprintf(output, "%s\n", MSG_OUTPUT_NO_RESOURCES_FOUND) + fmt.Fprintf(writer, "%s\n", MSG_OUTPUT_NO_RESOURCES_FOUND) return fmt.Errorf(MSG_OUTPUT_NO_RESOURCES_FOUND) } @@ -421,7 +421,7 @@ func DisplayResourceListMarkdown(bom *schema.BOM, output io.Writer) (err error) ) lineRow = createMarkdownRow(line) - fmt.Fprintf(output, "%s\n", lineRow) + fmt.Fprintf(writer, "%s\n", lineRow) } return diff --git a/cmd/resource_test.go b/cmd/resource_test.go index e3b45066..0ca34f78 100644 --- a/cmd/resource_test.go +++ b/cmd/resource_test.go @@ -21,7 +21,6 @@ package cmd import ( "bufio" "bytes" - "fmt" "io/fs" "log" "os" @@ -45,18 +44,18 @@ type ResourceTestInfo struct { } func (ti *ResourceTestInfo) String() string { - pParent := &ti.CommonTestInfo - return fmt.Sprintf("%s, %s", pParent.String(), ti.ResourceType) + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti) + return buffer.String() } func NewResourceTestInfo(inputFile string, outputFormat string, listSummary bool, whereClause string, - resultContainsValues []string, resultExpectedLineCount int, resultExpectedError error, resourceType string) *ResourceTestInfo { + resultExpectedLineCount int, resourceType string) *ResourceTestInfo { var ti = new(ResourceTestInfo) var pCommon = &ti.CommonTestInfo // initialize common fields pCommon.Init(inputFile, outputFormat, listSummary, whereClause, - resultContainsValues, resultExpectedLineCount, resultExpectedError) + nil, resultExpectedLineCount, nil) // Initialize resource-unique fields ti.ResourceType = resourceType return ti @@ -205,8 +204,7 @@ func TestResourceListTextCdx14SaaS(t *testing.T) { TEST_RESOURCE_LIST_CDX_1_4_SAAS_1, FORMAT_TEXT, nil, // no error - schema.RESOURCE_TYPE_COMPONENT, - ) + schema.RESOURCE_TYPE_COMPONENT) innerTestResourceList(t, rti) } @@ -224,11 +222,8 @@ func TestResourceListTextCdx13WhereClauseAndResultsByNameStartswith(t *testing.T FORMAT_TEXT, TI_LIST_SUMMARY_FALSE, TEST_INPUT_WHERE_CLAUSE, - nil, TEST_OUTPUT_LINES, - nil, - schema.RESOURCE_TYPE_COMPONENT, - ) + schema.RESOURCE_TYPE_COMPONENT) rti.ResultLineContainsValues = TEST_OUTPUT_CONTAINS rti.ResultLineContainsValuesAtLineNum = 2 innerTestResourceList(t, rti) @@ -244,11 +239,8 @@ func TestResourceListTextCdx13WhereClauseAndResultsByNameContains(t *testing.T) FORMAT_TEXT, TI_LIST_SUMMARY_FALSE, TEST_INPUT_WHERE_CLAUSE, - nil, TEST_OUTPUT_LINES, - nil, - schema.RESOURCE_TYPE_COMPONENT, - ) + schema.RESOURCE_TYPE_COMPONENT) rti.ResultLineContainsValues = TEST_OUTPUT_CONTAINS rti.ResultLineContainsValuesAtLineNum = 2 innerTestResourceList(t, rti) @@ -264,11 +256,8 @@ func TestResourceListTextCdx13WhereClauseAndResultsBomRefContains(t *testing.T) FORMAT_TEXT, TI_LIST_SUMMARY_FALSE, TEST_INPUT_WHERE_CLAUSE, - nil, TEST_OUTPUT_LINES, - nil, - schema.RESOURCE_TYPE_COMPONENT, - ) + schema.RESOURCE_TYPE_COMPONENT) rti.ResultLineContainsValues = TEST_OUTPUT_CONTAINS rti.ResultLineContainsValuesAtLineNum = 10 innerTestResourceList(t, rti) @@ -284,11 +273,8 @@ func TestResourceListTextCdx13WhereClauseAndResultsVersionStartswith(t *testing. FORMAT_TEXT, TI_LIST_SUMMARY_FALSE, TEST_INPUT_WHERE_CLAUSE, - nil, TEST_OUTPUT_LINES, - nil, - schema.RESOURCE_TYPE_COMPONENT, - ) + schema.RESOURCE_TYPE_COMPONENT) rti.ResultLineContainsValues = TEST_OUTPUT_CONTAINS rti.ResultLineContainsValuesAtLineNum = 2 innerTestResourceList(t, rti) @@ -304,11 +290,8 @@ func TestResourceListTextCdx13WhereClauseAndResultsNone(t *testing.T) { FORMAT_TEXT, TI_LIST_SUMMARY_FALSE, TEST_INPUT_WHERE_CLAUSE, - nil, TEST_OUTPUT_LINES, - nil, - schema.RESOURCE_TYPE_SERVICE, - ) + schema.RESOURCE_TYPE_SERVICE) rti.ResultLineContainsValues = TEST_OUTPUT_CONTAINS rti.ResultLineContainsValuesAtLineNum = 2 diff --git a/cmd/root.go b/cmd/root.go index 5067318e..0a44d109 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -78,14 +78,15 @@ const ( FLAG_FILENAME_OUTPUT_SHORT = "o" FLAG_QUIET_MODE = "quiet" FLAG_QUIET_MODE_SHORT = "q" - FLAG_LOG_OUTPUT_INDENT = "indent" + FLAG_OUTPUT_INDENT = "indent" + FLAG_LOG_OUTPUT_INDENT = "log-indent" FLAG_FILE_OUTPUT_FORMAT = "format" FLAG_COLORIZE_OUTPUT = "colorize" ) const ( MSG_APP_NAME = "Bill-of-Materials (BOM) utility." - MSG_APP_DESCRIPTION = "This utility serves as centralized command line interface into various Software Bill-of-Materials (SBOM) helper utilities." + MSG_APP_DESCRIPTION = "This utility serves as centralized command-line interface for various Bill-of-Materials (BOM) helper utilities." MSG_FLAG_TRACE = "enable trace logging" MSG_FLAG_DEBUG = "enable debug logging" MSG_FLAG_INPUT = "input filename (e.g., \"path/sbom.json\")" @@ -94,6 +95,7 @@ const ( MSG_FLAG_LOG_INDENT = "enable log indentation of functional callstack" MSG_FLAG_CONFIG_SCHEMA = "provide custom application schema configuration file (i.e., overrides default `config.json`)" MSG_FLAG_CONFIG_LICENSE = "provide custom application license policy configuration file (i.e., overrides default `license.json`)" + MSG_FLAG_OUTPUT_INDENT = "number of space characters used to indent JSON formatted output" ) const ( @@ -117,6 +119,13 @@ const ( FORMAT_ANY = "" // Used for test errors ) +// TODO: make flag configurable: +// NOTE: 4-space indent is accepted convention: +// https://docs.openstack.org/doc-contrib-guide/json-conv.html +const ( + DEFAULT_OUTPUT_INDENT_LENGTH = 4 +) + // Command reserved values const ( INPUT_TYPE_STDIN = "-" @@ -180,6 +189,9 @@ func init() { // Optionally, allow log callstack trace to be indented rootCmd.PersistentFlags().BoolVarP(&utils.GlobalFlags.LogOutputIndentCallstack, FLAG_LOG_OUTPUT_INDENT, "", false, MSG_FLAG_LOG_INDENT) + // Output (JSON) indent + rootCmd.PersistentFlags().Uint8VarP(&utils.GlobalFlags.PersistentFlags.OutputIndent, FLAG_OUTPUT_INDENT, "", DEFAULT_OUTPUT_INDENT_LENGTH, MSG_FLAG_OUTPUT_INDENT) + // Add root commands rootCmd.AddCommand(NewCommandVersion()) rootCmd.AddCommand(NewCommandSchema()) diff --git a/cmd/root_test.go b/cmd/root_test.go index 84a3b9bf..b5b0ea3b 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -21,7 +21,6 @@ package cmd import ( "bytes" "flag" - "fmt" "os" "strconv" "strings" @@ -59,25 +58,48 @@ var TestLogQuiet = flag.Bool(FLAG_QUIET_MODE, false, "") type CommonTestInfo struct { InputFile string + ListSummary bool OutputFile string OutputFormat string - ListSummary bool + OutputIndent uint8 WhereClause string + ResultExpectedByteSize int ResultExpectedError error + ResultExpectedIndentLength int + ResultExpectedIndentAtLineNum int ResultExpectedLineCount int - ResultLineContainsValuesAtLineNum int ResultLineContainsValues []string + ResultLineContainsValuesAtLineNum int + Autofail bool MockStdin bool - TestOutputVariantName string - TestOutputExpectedByteSize int } +// defaults for TestInfo struct values +const ( + TI_LIST_SUMMARY_FALSE = false + TI_LIST_LINE_WRAP = false + TI_DEFAULT_WHERE_CLAUSE = "" + TI_DEFAULT_POLICY_FILE = "" + TI_DEFAULT_JSON_INDENT = DEFAULT_OUTPUT_INDENT_LENGTH // 4 + TI_RESULT_DEFAULT_LINE_COUNT = -1 + TI_RESULT_DEFAULT_LINE_CONTAINS = -1 // NOTE: -1 means "any" line +) + func NewCommonTestInfo() *CommonTestInfo { var ti = new(CommonTestInfo) + ti.OutputIndent = TI_DEFAULT_JSON_INDENT + ti.ResultExpectedLineCount = TI_RESULT_DEFAULT_LINE_COUNT + ti.ResultLineContainsValuesAtLineNum = TI_RESULT_DEFAULT_LINE_CONTAINS + return ti +} + +func NewCommonTestInfoBasic(inputFile string) *CommonTestInfo { + var ti = NewCommonTestInfo() + ti.InputFile = inputFile return ti } -func NewCommonTestInfoBasic(inputFile string, whereClause string, listFormat string, listSummary bool) *CommonTestInfo { +func NewCommonTestInfoBasicList(inputFile string, whereClause string, listFormat string, listSummary bool) *CommonTestInfo { var ti = NewCommonTestInfo() ti.InputFile = inputFile ti.WhereClause = whereClause @@ -86,27 +108,20 @@ func NewCommonTestInfoBasic(inputFile string, whereClause string, listFormat str return ti } -// default (empty) TestInfo struct values -const ( - TI_LIST_SUMMARY_FALSE = false - TI_LIST_LINE_WRAP = false - TI_DEFAULT_WHERE_CLAUSE = "" - TI_DEFAULT_LINE_COUNT = -1 - TI_DEFAULT_POLICY_FILE = "" -) - // Stringer interface for ResourceTestInfo (just display subset of key values) func (ti *CommonTestInfo) String() string { - return fmt.Sprintf("InputFile: `%s`, Format: `%s`, WhereClause: `%s`, ListSummary: `%v`", - ti.InputFile, ti.OutputFormat, ti.WhereClause, ti.ListSummary) + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti) + return buffer.String() } func (ti *CommonTestInfo) Init(inputFile string, listFormat string, listSummary bool, whereClause string, resultContainsValues []string, resultExpectedLineCount int, resultExpectedError error) *CommonTestInfo { ti.InputFile = inputFile ti.OutputFormat = listFormat + ti.OutputIndent = TI_DEFAULT_JSON_INDENT ti.ListSummary = listSummary ti.WhereClause = whereClause + ti.ResultLineContainsValuesAtLineNum = TI_RESULT_DEFAULT_LINE_CONTAINS ti.ResultExpectedLineCount = resultExpectedLineCount ti.ResultExpectedError = resultExpectedError return ti @@ -114,19 +129,20 @@ func (ti *CommonTestInfo) Init(inputFile string, listFormat string, listSummary func (ti *CommonTestInfo) InitBasic(inputFile string, format string, expectedError error) *CommonTestInfo { ti.Init(inputFile, format, TI_LIST_SUMMARY_FALSE, TI_DEFAULT_WHERE_CLAUSE, - nil, TI_DEFAULT_LINE_COUNT, expectedError) + nil, TI_RESULT_DEFAULT_LINE_COUNT, expectedError) return ti } -func (ti *CommonTestInfo) CreateTemporaryFilename(relativeFilename string) (tempFilename string) { +func (ti *CommonTestInfo) CreateTemporaryTestOutputFilename(relativeFilename string) (tempFilename string) { + testFunctionName := utils.GetCallerFunctionName(3) trimmedFilename := strings.TrimLeft(relativeFilename, strconv.QuoteRune(os.PathSeparator)) - if ti.TestOutputVariantName != "" { + if testFunctionName != "" { lastIndex := strings.LastIndex(trimmedFilename, string(os.PathSeparator)) // insert variant as last path... if lastIndex > 0 { path := trimmedFilename[0:lastIndex] base := trimmedFilename[lastIndex:] - trimmedFilename = path + string(os.PathSeparator) + ti.TestOutputVariantName + base + trimmedFilename = path + string(os.PathSeparator) + testFunctionName + base } } return DEFAULT_TEMP_OUTPUT_PATH + trimmedFilename @@ -235,7 +251,7 @@ func prepareWhereFilters(t *testing.T, testInfo *CommonTestInfo) (whereFilters [ if testInfo.WhereClause != "" { whereFilters, err = retrieveWhereFilters(testInfo.WhereClause) if err != nil { - t.Errorf("test failed: %s: detail: %s ", testInfo.String(), err.Error()) + t.Errorf("test failed: %s: detail: %s ", testInfo, err.Error()) return } } @@ -244,7 +260,8 @@ func prepareWhereFilters(t *testing.T, testInfo *CommonTestInfo) (whereFilters [ const RESULT_LINE_CONTAINS_ANY = -1 -func lineContainsValues(buffer bytes.Buffer, lineNum int, values ...string) (int, bool) { +func bufferLineContainsValues(buffer bytes.Buffer, lineNum int, values ...string) (int, bool) { + lines := strings.Split(buffer.String(), "\n") getLogger().Tracef("output: %s", lines) @@ -284,12 +301,21 @@ func bufferContainsValues(buffer bytes.Buffer, values ...string) bool { return true } -// TODO: find a better way using some log package feature -func printMarshaledResultOnlyIfNotQuiet(iResult interface{}) { - if !*TestLogQuiet { - // Format results in JSON - fResult, _ := utils.MarshalAnyToFormattedJsonString(iResult) - // Output the JSON data directly to stdout (not subject to log-level) - fmt.Printf("%s\n", fResult) +func numberOfLeadingSpaces(line string) (numSpaces int) { + for _, ch := range line { + if ch == ' ' { + numSpaces++ + } else { + break + } } + return +} + +func getBufferLinesAndCount(buffer bytes.Buffer) (numLines int, lines []string) { + if buffer.Len() > 0 { + lines = strings.Split(buffer.String(), "\n") + numLines = len(lines) + } + return } diff --git a/cmd/schema.go b/cmd/schema.go index 13ec57b7..9f83b8d9 100644 --- a/cmd/schema.go +++ b/cmd/schema.go @@ -74,7 +74,7 @@ func NewCommandSchema() *cobra.Command { var command = new(cobra.Command) command.Use = CMD_USAGE_SCHEMA_LIST // "schema" command.Short = "View supported SBOM schemas" - command.Long = fmt.Sprintf("View built-in SBOM schemas supported by the utility. The default command produces a list based upon `%s`.", DEFAULT_SCHEMA_CONFIG) + command.Long = fmt.Sprintf("View built-in BOM schemas supported by the utility. The default command produces a list based upon `%s`.", DEFAULT_SCHEMA_CONFIG) command.Flags().StringVarP(&utils.GlobalFlags.PersistentFlags.OutputFormat, FLAG_FILE_OUTPUT_FORMAT, "", FORMAT_TEXT, FLAG_SCHEMA_OUTPUT_FORMAT_HELP+SCHEMA_LIST_SUPPORTED_FORMATS) command.Flags().StringP(FLAG_REPORT_WHERE, "", "", FLAG_REPORT_WHERE_HELP) @@ -229,7 +229,7 @@ func ListSchemas(writer io.Writer, persistentFlags utils.PersistentCommandFlags, } // TODO: Add a --no-title flag to skip title output -func DisplaySchemasTabbedText(output io.Writer, filteredSchemas []schema.FormatSchemaInstance) (err error) { +func DisplaySchemasTabbedText(writer io.Writer, filteredSchemas []schema.FormatSchemaInstance) (err error) { getLogger().Enter() defer getLogger().Exit() @@ -237,7 +237,7 @@ func DisplaySchemasTabbedText(output io.Writer, filteredSchemas []schema.FormatS w := new(tabwriter.Writer) // min-width, tab-width, padding, pad-char, flags - w.Init(output, 8, 2, 2, ' ', 0) + w.Init(writer, 8, 2, 2, ' ', 0) defer w.Flush() // Emit no schemas found warning into output @@ -275,7 +275,7 @@ func DisplaySchemasTabbedText(output io.Writer, filteredSchemas []schema.FormatS } // TODO: Add a --no-title flag to skip title output -func DisplaySchemasMarkdown(output io.Writer, filteredSchemas []schema.FormatSchemaInstance) (err error) { +func DisplaySchemasMarkdown(writer io.Writer, filteredSchemas []schema.FormatSchemaInstance) (err error) { getLogger().Enter() defer getLogger().Exit() @@ -284,12 +284,12 @@ func DisplaySchemasMarkdown(output io.Writer, filteredSchemas []schema.FormatSch titleRow := createMarkdownRow(titles) alignments := createMarkdownColumnAlignment(titles) alignmentRow := createMarkdownRow(alignments) - fmt.Fprintf(output, "%s\n", titleRow) - fmt.Fprintf(output, "%s\n", alignmentRow) + fmt.Fprintf(writer, "%s\n", titleRow) + fmt.Fprintf(writer, "%s\n", alignmentRow) // Emit no schemas found warning into output if len(filteredSchemas) == 0 { - fmt.Fprintf(output, "%s\n", MSG_OUTPUT_NO_SCHEMAS_FOUND) + fmt.Fprintf(writer, "%s\n", MSG_OUTPUT_NO_SCHEMAS_FOUND) return fmt.Errorf(MSG_OUTPUT_NO_SCHEMAS_FOUND) } @@ -315,18 +315,18 @@ func DisplaySchemasMarkdown(output io.Writer, filteredSchemas []schema.FormatSch ) lineRow = createMarkdownRow(line) - fmt.Fprintf(output, "%s\n", lineRow) + fmt.Fprintf(writer, "%s\n", lineRow) } return } // TODO: Add a --no-title flag to skip title output -func DisplaySchemasCSV(output io.Writer, filteredSchemas []schema.FormatSchemaInstance) (err error) { +func DisplaySchemasCSV(writer io.Writer, filteredSchemas []schema.FormatSchemaInstance) (err error) { getLogger().Enter() defer getLogger().Exit() // initialize writer and prepare the list of entries (i.e., the "rows") - w := csv.NewWriter(output) + w := csv.NewWriter(writer) defer w.Flush() // create title row from slices of optional and compulsory titles diff --git a/cmd/schema_test.go b/cmd/schema_test.go index 186bcdbd..2f0770ec 100644 --- a/cmd/schema_test.go +++ b/cmd/schema_test.go @@ -68,8 +68,6 @@ func innerTestSchemaList(t *testing.T, pTestInfo *CommonTestInfo) (outputBuffer func TestSchemaListText(t *testing.T) { ti := NewCommonTestInfo() ti.OutputFormat = FORMAT_TEXT - ti.ResultExpectedLineCount = TI_DEFAULT_LINE_COUNT - // verify correct error is returned innerTestSchemaList(t, ti) } diff --git a/cmd/stats.go b/cmd/stats.go index 402976dd..a505bdbd 100644 --- a/cmd/stats.go +++ b/cmd/stats.go @@ -201,7 +201,7 @@ func loadDocumentStatisticalEntities(document *schema.BOM, statsFlags utils.Stat // NOTE: This list is NOT de-duplicated // TODO: Add a --no-title flag to skip title output -func DisplayStatsText(bom *schema.BOM, output io.Writer) { +func DisplayStatsText(bom *schema.BOM, writer io.Writer) { getLogger().Enter() defer getLogger().Exit() @@ -210,7 +210,7 @@ func DisplayStatsText(bom *schema.BOM, output io.Writer) { defer w.Flush() // min-width, tab-width, padding, pad-char, flags - w.Init(output, 8, 2, 2, ' ', 0) + w.Init(writer, 8, 2, 2, ' ', 0) // create underline row from compulsory titles underlines := createTitleTextSeparators(RESOURCE_LIST_TITLES) diff --git a/cmd/stats_test.go b/cmd/stats_test.go index 0469d77b..24b58f74 100644 --- a/cmd/stats_test.go +++ b/cmd/stats_test.go @@ -40,8 +40,8 @@ type StatsTestInfo struct { } func (ti *StatsTestInfo) String() string { - pParent := &ti.CommonTestInfo - return pParent.String() + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti) + return buffer.String() } func NewStatsTestInfoBasic(inputFile string, listFormat string, resultExpectedError error) *StatsTestInfo { diff --git a/cmd/trim.go b/cmd/trim.go index 98a2e3f1..d82ea02a 100644 --- a/cmd/trim.go +++ b/cmd/trim.go @@ -34,14 +34,6 @@ const ( FLAG_TRIM_MAP_KEYS = "keys" ) -// TODO: make flag configurable: -// NOTE: 4-space indent is accepted convention: -// https://docs.openstack.org/doc-contrib-guide/json-conv.html -const ( - TRIM_OUTPUT_PREFIX = "" - TRIM_OUTPUT_INDENT = " " -) - // flag help (translate) const ( FLAG_TRIM_OUTPUT_FORMAT_HELP = "format output using the specified type" @@ -64,8 +56,8 @@ const ( func NewCommandTrim() *cobra.Command { var command = new(cobra.Command) command.Use = CMD_USAGE_TRIM - command.Short = "(experimental) Trim elements from the BOM input file and write to output file" - command.Long = "(experimental) Trim elements from the BOM input file and write to output file" + command.Short = "(experimental) Trim elements from the BOM input file and write resultant BOM to output" + command.Long = "(experimental) Trim elements from the BOM input file and write resultant BOM to output" command.RunE = trimCmdImpl command.PreRunE = func(cmd *cobra.Command, args []string) (err error) { // Test for required flags (parameters) @@ -190,7 +182,7 @@ func Trim(writer io.Writer, persistentFlags utils.PersistentCommandFlags, trimFl if errQuery != nil { getLogger().Errorf("query error: invalid path: %s", path) - buffer, errEncode := utils.EncodeAnyToIndentedJSON(result) + buffer, errEncode := utils.EncodeAnyToDefaultIndentedJSONStr(result) if errEncode != nil { getLogger().Tracef("result: %s", buffer.String()) } @@ -209,18 +201,18 @@ func Trim(writer io.Writer, persistentFlags utils.PersistentCommandFlags, trimFl return } - // TODO: include formatting (i.e., prefix, indent) as command line flags // Output the "trimmed" version of the Input BOM format := persistentFlags.OutputFormat getLogger().Infof("Outputting listing (`%s` format)...", format) + indentString := utils.GenerateIndentString(int(utils.GlobalFlags.PersistentFlags.OutputIndent)) switch format { case FORMAT_JSON: - err = document.EncodeAsFormattedJSON(writer, TRIM_OUTPUT_PREFIX, TRIM_OUTPUT_INDENT) + err = document.EncodeAsFormattedJSON(writer, utils.DEFAULT_JSON_PREFIX_STRING, indentString) default: // Default to Text output for anything else (set as flag default) getLogger().Warningf("Trim not supported for `%s` format; defaulting to `%s` format...", format, FORMAT_JSON) - err = document.EncodeAsFormattedJSON(writer, TRIM_OUTPUT_PREFIX, TRIM_OUTPUT_INDENT) + err = document.EncodeAsFormattedJSON(writer, utils.DEFAULT_JSON_PREFIX_STRING, indentString) } return diff --git a/cmd/trim_test.go b/cmd/trim_test.go index 8643478e..8781ce22 100644 --- a/cmd/trim_test.go +++ b/cmd/trim_test.go @@ -48,11 +48,11 @@ type TrimTestInfo struct { } func (ti *TrimTestInfo) String() string { - pParent := &ti.CommonTestInfo - return pParent.String() + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti) + return buffer.String() } -func NewTrimTestInfoBasic(inputFile string, resultExpectedError error) *TrimTestInfo { +func NewTrimTestInfo(inputFile string, resultExpectedError error) *TrimTestInfo { var ti = new(TrimTestInfo) var pCommon = &ti.CommonTestInfo pCommon.InitBasic(inputFile, FORMAT_JSON, resultExpectedError) @@ -60,14 +60,50 @@ func NewTrimTestInfoBasic(inputFile string, resultExpectedError error) *TrimTest } // ------------------------------------------- -// resource list test helper functions +// test helper functions // ------------------------------------------- + +func innerTestTrim(t *testing.T, testInfo *TrimTestInfo) (outputBuffer bytes.Buffer, basicTestInfo string, err error) { + getLogger().Tracef("TestInfo: %s", testInfo) + + // Mock stdin if requested + if testInfo.MockStdin == true { + utils.GlobalFlags.PersistentFlags.InputFile = INPUT_TYPE_STDIN + file, err := os.Open(testInfo.InputFile) // For read access. + if err != nil { + log.Fatal(err) + } + + // convert byte slice to io.Reader + savedStdIn := os.Stdin + // !!!Important restore stdin + defer func() { os.Stdin = savedStdIn }() + os.Stdin = file + } + + // invoke resource list command with a byte buffer + outputBuffer, err = innerBufferedTestTrim(t, testInfo) + // if the command resulted in a failure + if err != nil { + // if tests asks us to report a FAIL to the test framework + cti := &testInfo.CommonTestInfo + if cti.Autofail { + encodedTestInfo, _ := utils.EncodeAnyToDefaultIndentedJSONStr(testInfo) + t.Errorf("%s: failed: %v\n%s", cti.InputFile, err, encodedTestInfo.String()) + } + return + } + + return +} + func innerBufferedTestTrim(t *testing.T, testInfo *TrimTestInfo) (outputBuffer bytes.Buffer, err error) { // The command looks for the input & output filename in global flags struct utils.GlobalFlags.PersistentFlags.InputFile = testInfo.InputFile utils.GlobalFlags.PersistentFlags.OutputFile = testInfo.OutputFile utils.GlobalFlags.PersistentFlags.OutputFormat = testInfo.OutputFormat + utils.GlobalFlags.PersistentFlags.OutputIndent = testInfo.OutputIndent utils.GlobalFlags.TrimFlags.Keys = testInfo.Keys utils.GlobalFlags.TrimFlags.FromPaths = testInfo.FromPaths var outputWriter io.Writer @@ -103,45 +139,32 @@ func innerBufferedTestTrim(t *testing.T, testInfo *TrimTestInfo) (outputBuffer b return } -func innerTestTrim(t *testing.T, testInfo *TrimTestInfo) (outputBuffer bytes.Buffer, basicTestInfo string, err error) { - getLogger().Tracef("TestInfo: %s", testInfo) +func VerifyTrimOutputFileResult(t *testing.T, originalTest TrimTestInfo) (err error) { - // Mock stdin if requested - if testInfo.MockStdin == true { - utils.GlobalFlags.PersistentFlags.InputFile = INPUT_TYPE_STDIN - file, err := os.Open(testInfo.InputFile) // For read access. - if err != nil { - log.Fatal(err) - } + // Create a new test info. structure copying in data from the original test + queryTestInfo := NewCommonTestInfo() + queryTestInfo.InputFile = originalTest.OutputFile - // convert byte slice to io.Reader - savedStdIn := os.Stdin - // !!!Important restore stdin - defer func() { os.Stdin = savedStdIn }() - os.Stdin = file + // Load an Query temporary "trimmed" output BOM file using the "from" path + // Default to "root" (i.e,, "") path if none selected. + fromPath := "" + if len(originalTest.FromPaths) > 0 { + fromPath = originalTest.FromPaths[0] } - // invoke resource list command with a byte buffer - outputBuffer, err = innerBufferedTestTrim(t, testInfo) - return -} - -func VerifyTrimOutputFileResult(t *testing.T, ti *TrimTestInfo, keys []string, fromPath string) (err error) { - // Query temporary "trimmed" BOM to assure known fields were removed request, err := common.NewQueryRequestSelectFromWhere( - common.QUERY_TOKEN_WILDCARD, - fromPath, - "") + common.QUERY_TOKEN_WILDCARD, fromPath, "") if err != nil { t.Errorf("%s: %v", ERR_TYPE_UNEXPECTED_ERROR, err) return } - for _, key := range keys { + // Verify each key was removed + var pResult interface{} + for _, key := range originalTest.Keys { // use a buffered query on the temp. output file on the (parent) path - var pResult interface{} - pResult, err = innerQuery(t, ti.OutputFile, request, true) + pResult, _, err = innerQuery(t, queryTestInfo, request) if err != nil { t.Errorf("%s: %v", ERR_TYPE_UNEXPECTED_ERROR, err) return @@ -198,8 +221,8 @@ func VerifyTrimmed(pResult interface{}, key string) (err error) { // are specified for both formats. We need to assure any commands that // rewrite BOMs (after edits) preserve original characters. func TestTrimCdx14PreserveUnencodedChars(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_4_ENCODED_CHARS, nil) - ti.OutputFile = ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_4_ENCODED_CHARS) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_4_ENCODED_CHARS, nil) + ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_4_ENCODED_CHARS) ti.Keys = append(ti.Keys, "name") outputBuffer, _ := innerBufferedTestTrim(t, ti) TEST1 := "" @@ -220,7 +243,7 @@ func TestTrimCdx14PreserveUnencodedChars(t *testing.T) { // Trim "keys" globally (entire BOM) // ---------------------------------------- func TestTrimCdx14ComponentPropertiesSampleXXLBuffered(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_4_SAMPLE_XXL_1, nil) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_4_SAMPLE_XXL_1, nil) ti.Keys = append(ti.Keys, "properties") outputBuffer, _ := innerBufferedTestTrim(t, ti) // TODO: verify "after" trim lengths and content have removed properties @@ -229,51 +252,41 @@ func TestTrimCdx14ComponentPropertiesSampleXXLBuffered(t *testing.T) { // TODO: enable for when we have a "from" parameter to limit trim scope func TestTrimCdx14ComponentPropertiesSampleXXL(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_4_SAMPLE_XXL_1, nil) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_4_SAMPLE_XXL_1, nil) ti.Keys = append(ti.Keys, "properties") - ti.OutputFile = ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_4_SAMPLE_XXL_1) + ti.FromPaths = []string{"metadata.component"} + ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_4_SAMPLE_XXL_1) innerTestTrim(t, ti) // Assure JSON map does not contain the trimmed key(s) - err := VerifyTrimOutputFileResult(t, ti, ti.Keys, "metadata.component") + err := VerifyTrimOutputFileResult(t, *ti) if err != nil { t.Error(err) } } func TestTrimCdx15MultipleKeys(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY, nil) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY, nil) ti.Keys = append(ti.Keys, "properties", "hashes", "version", "description", "name") - ti.OutputFile = ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY) + ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY) innerTestTrim(t, ti) // Assure JSON map does not contain the trimmed key(s) - err := VerifyTrimOutputFileResult(t, ti, []string{"hashes"}, "") + err := VerifyTrimOutputFileResult(t, *ti) if err != nil { t.Error(err) } - err = VerifyTrimOutputFileResult(t, ti, []string{"version"}, "") + err = VerifyTrimOutputFileResult(t, *ti) if err != nil { t.Error(err) } } func TestTrimCdx15Properties(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil) ti.Keys = append(ti.Keys, "properties") - ti.OutputFile = ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1) + ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1) innerTestTrim(t, ti) // Assure JSON map does not contain the trimmed key(s) - // Document "root" properties - err := VerifyTrimOutputFileResult(t, ti, ti.Keys, "") // document root - if err != nil { - t.Error(err) - } - // metadata properties - err = VerifyTrimOutputFileResult(t, ti, ti.Keys, "metadata") // document root - if err != nil { - t.Error(err) - } - // metadata.component properties - err = VerifyTrimOutputFileResult(t, ti, ti.Keys, "metadata.component") // document root + err := VerifyTrimOutputFileResult(t, *ti) if err != nil { t.Error(err) } @@ -284,66 +297,63 @@ func TestTrimCdx15Properties(t *testing.T) { // ---------------------------------------- func TestTrimCdx15PropertiesFromMetadataComponent(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil) ti.Keys = append(ti.Keys, "properties") ti.FromPaths = []string{"metadata.component"} - ti.TestOutputVariantName = utils.GetCallerFunctionName(2) - ti.OutputFile = ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1) + ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1) innerTestTrim(t, ti) // Assure JSON map does not contain the trimmed key(s) - err := VerifyTrimOutputFileResult(t, ti, ti.Keys, "metadata.component") // document root + err := VerifyTrimOutputFileResult(t, *ti) if err != nil { t.Error(err) } } func TestTrimCdx15HashesFromTools(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil) ti.Keys = append(ti.Keys, "hashes") ti.FromPaths = []string{"metadata.tools"} - ti.TestOutputVariantName = utils.GetCallerFunctionName(2) - ti.OutputFile = ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1) + ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1) innerTestTrim(t, ti) // Assure JSON map does not contain the trimmed key(s) - err := VerifyTrimOutputFileResult(t, ti, ti.Keys, "metadata.tools") // document root + err := VerifyTrimOutputFileResult(t, *ti) if err != nil { t.Error(err) } } func TestTrimCdx15AllIncrementallyFromSmallSample(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY, nil) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY, nil) ti.Keys = append(ti.Keys, "type", "purl", "bom-ref", "serialNumber", "components", "name", "description", "properties") ti.FromPaths = []string{""} - ti.TestOutputVariantName = utils.GetCallerFunctionName(2) - ti.OutputFile = ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY) + ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_5_SAMPLE_SMALL_COMPS_ONLY) _, _, err := innerTestTrim(t, ti) if err != nil { t.Error(err) } // Assure JSON map does not contain the trimmed key(s) - err = VerifyTrimOutputFileResult(t, ti, ti.Keys, "") // document root + err = VerifyTrimOutputFileResult(t, *ti) if err != nil { t.Error(err) } } func TestTrimCdx15FooFromTools(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1, nil) ti.Keys = append(ti.Keys, "foo") ti.FromPaths = []string{"metadata.tools"} - ti.TestOutputVariantName = utils.GetCallerFunctionName(2) - ti.OutputFile = "" // ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1) - ti.TestOutputExpectedByteSize = 5351 + ti.OutputFile = "" // ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_5_SAMPLE_MEDIUM_1) + ti.OutputIndent = 2 // Matches the space indent of the test input file + ti.ResultExpectedByteSize = 4292 buffer, _, err := innerTestTrim(t, ti) if err != nil { t.Error(err) } - // Validate expected output file size in bytes (assumes 4 space indent) - if actualSize := buffer.Len(); actualSize != ti.TestOutputExpectedByteSize { - t.Error(fmt.Errorf("invalid trim result (output size (byte)): expected size: %v, actual size: %v", ti.TestOutputExpectedByteSize, actualSize)) + // Validate expected output file size in bytes (assumes 2-space indent) + if actualSize := buffer.Len(); actualSize != ti.ResultExpectedByteSize { + t.Error(fmt.Errorf("invalid trim result (output size (byte)): expected size: %v, actual size: %v", ti.ResultExpectedByteSize, actualSize)) } // validate test-specific strings still exist @@ -355,11 +365,10 @@ func TestTrimCdx15FooFromTools(t *testing.T) { } func TestTrimCdx14SourceFromVulnerabilities(t *testing.T) { - ti := NewTrimTestInfoBasic(TEST_TRIM_CDX_1_4_SAMPLE_VEX, nil) + ti := NewTrimTestInfo(TEST_TRIM_CDX_1_4_SAMPLE_VEX, nil) ti.Keys = append(ti.Keys, "source") ti.FromPaths = []string{"vulnerabilities"} - ti.TestOutputVariantName = utils.GetCallerFunctionName(2) - ti.OutputFile = ti.CreateTemporaryFilename(TEST_TRIM_CDX_1_4_SAMPLE_VEX) + ti.OutputFile = ti.CreateTemporaryTestOutputFilename(TEST_TRIM_CDX_1_4_SAMPLE_VEX) buffer, _, err := innerTestTrim(t, ti) s := buffer.String() @@ -369,7 +378,7 @@ func TestTrimCdx14SourceFromVulnerabilities(t *testing.T) { } // Assure JSON map does not contain the trimmed key(s) - err = VerifyTrimOutputFileResult(t, ti, ti.Keys, ti.FromPaths[0]) + err = VerifyTrimOutputFileResult(t, *ti) if err != nil { t.Error(err) } diff --git a/cmd/validate_format.go b/cmd/validate_format.go index fdecc459..2d53f1b4 100644 --- a/cmd/validate_format.go +++ b/cmd/validate_format.go @@ -154,22 +154,22 @@ func (result *ValidationErrorResult) MapItemsMustBeUniqueError(flags utils.Valid } } -func FormatSchemaErrors(output io.Writer, schemaErrors []gojsonschema.ResultError, flags utils.ValidateCommandFlags, format string) (formattedSchemaErrors string) { +func FormatSchemaErrors(writer io.Writer, schemaErrors []gojsonschema.ResultError, flags utils.ValidateCommandFlags, format string) (formattedSchemaErrors string) { if lenErrs := len(schemaErrors); lenErrs > 0 { getLogger().Infof(MSG_INFO_SCHEMA_ERRORS_DETECTED, lenErrs) getLogger().Infof(MSG_INFO_FORMATTING_ERROR_RESULTS, format) switch format { case FORMAT_JSON: - DisplaySchemaErrorsJson(output, schemaErrors, utils.GlobalFlags.ValidateFlags) + DisplaySchemaErrorsJson(writer, schemaErrors, utils.GlobalFlags.ValidateFlags) case FORMAT_TEXT: - DisplaySchemaErrorsText(output, schemaErrors, utils.GlobalFlags.ValidateFlags) + DisplaySchemaErrorsText(writer, schemaErrors, utils.GlobalFlags.ValidateFlags) case FORMAT_CSV: - DisplaySchemaErrorsCsv(output, schemaErrors, utils.GlobalFlags.ValidateFlags) + DisplaySchemaErrorsCsv(writer, schemaErrors, utils.GlobalFlags.ValidateFlags) default: getLogger().Warningf(MSG_WARN_INVALID_FORMAT, format, FORMAT_TEXT) - DisplaySchemaErrorsText(output, schemaErrors, utils.GlobalFlags.ValidateFlags) - fmt.Fprintf(output, "%s", formattedSchemaErrors) + DisplaySchemaErrorsText(writer, schemaErrors, utils.GlobalFlags.ValidateFlags) + fmt.Fprintf(writer, "%s", formattedSchemaErrors) } } @@ -251,7 +251,7 @@ func (result *ValidationErrorResult) formatResultMap(flags utils.ValidateCommand return formattedResult } -func DisplaySchemaErrorsJson(output io.Writer, errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) { +func DisplaySchemaErrorsJson(writer io.Writer, errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) { getLogger().Enter() defer getLogger().Exit() @@ -293,10 +293,10 @@ func DisplaySchemaErrorsJson(output io.Writer, errs []gojsonschema.ResultError, } // Note: JSON data files MUST ends in a newline as this is a POSIX standard - fmt.Fprintf(output, "%s\n", sb.String()) + fmt.Fprintf(writer, "%s\n", sb.String()) } -func DisplaySchemaErrorsText(output io.Writer, errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) { +func DisplaySchemaErrorsText(writer io.Writer, errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) { getLogger().Enter() defer getLogger().Exit() @@ -334,16 +334,16 @@ func DisplaySchemaErrorsText(output io.Writer, errs []gojsonschema.ResultError, } } - fmt.Fprintf(output, "%s", sb.String()) + fmt.Fprintf(writer, "%s", sb.String()) } -func DisplaySchemaErrorsCsv(output io.Writer, errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) { +func DisplaySchemaErrorsCsv(writer io.Writer, errs []gojsonschema.ResultError, flags utils.ValidateCommandFlags) { getLogger().Enter() defer getLogger().Exit() var currentRow []string - w := csv.NewWriter(output) + w := csv.NewWriter(writer) defer w.Flush() // Emit title row diff --git a/cmd/validate_test.go b/cmd/validate_test.go index e8b3c43e..fdf4761b 100644 --- a/cmd/validate_test.go +++ b/cmd/validate_test.go @@ -57,8 +57,8 @@ type ValidateTestInfo struct { } func (ti *ValidateTestInfo) String() string { - pParent := &ti.CommonTestInfo - return fmt.Sprintf("%s, %s, %s", pParent.String(), ti.SchemaVariant, ti.CustomSchema) + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti) + return buffer.String() } func NewValidateTestInfoMinimum(inputFile string) *ValidateTestInfo { diff --git a/cmd/vulnerability.go b/cmd/vulnerability.go index 59c68ad9..40ac11e9 100644 --- a/cmd/vulnerability.go +++ b/cmd/vulnerability.go @@ -27,7 +27,6 @@ import ( "text/tabwriter" "github.com/CycloneDX/sbom-utility/common" - "github.com/CycloneDX/sbom-utility/log" "github.com/CycloneDX/sbom-utility/schema" "github.com/CycloneDX/sbom-utility/utils" "github.com/jwangsadinata/go-multimap" @@ -269,7 +268,7 @@ func loadDocumentVulnerabilities(document *schema.BOM, whereFilters []common.Whe // NOTE: This list is NOT de-duplicated // TODO: Add a --no-title flag to skip title output -func DisplayVulnListText(bom *schema.BOM, output io.Writer, flags utils.VulnerabilityCommandFlags) { +func DisplayVulnListText(bom *schema.BOM, writer io.Writer, flags utils.VulnerabilityCommandFlags) { getLogger().Enter() defer getLogger().Exit() @@ -278,7 +277,7 @@ func DisplayVulnListText(bom *schema.BOM, output io.Writer, flags utils.Vulnerab defer w.Flush() // min-width, tab-width, padding, pad-char, flags - w.Init(output, 8, 2, 2, ' ', 0) + w.Init(writer, 8, 2, 2, ' ', 0) // create title row and underline row from slices of optional and compulsory titles titles, underlines := prepareReportTitleData(VULNERABILITY_LIST_ROW_DATA, flags.Summary) @@ -313,12 +312,12 @@ func DisplayVulnListText(bom *schema.BOM, output io.Writer, flags utils.Vulnerab } // TODO: Add a --no-title flag to skip title output -func DisplayVulnListCSV(bom *schema.BOM, output io.Writer, flags utils.VulnerabilityCommandFlags) (err error) { +func DisplayVulnListCSV(bom *schema.BOM, writer io.Writer, flags utils.VulnerabilityCommandFlags) (err error) { getLogger().Enter() defer getLogger().Exit() // initialize writer and prepare the list of entries (i.e., the "rows") - w := csv.NewWriter(output) + w := csv.NewWriter(writer) defer w.Flush() // Create title row data as []string @@ -363,7 +362,7 @@ func DisplayVulnListCSV(bom *schema.BOM, output io.Writer, flags utils.Vulnerabi } // TODO: Add a --no-title flag to skip title output -func DisplayVulnListMarkdown(bom *schema.BOM, output io.Writer, flags utils.VulnerabilityCommandFlags) (err error) { +func DisplayVulnListMarkdown(bom *schema.BOM, writer io.Writer, flags utils.VulnerabilityCommandFlags) (err error) { getLogger().Enter() defer getLogger().Exit() @@ -372,18 +371,18 @@ func DisplayVulnListMarkdown(bom *schema.BOM, output io.Writer, flags utils.Vuln // create title row titleRow := createMarkdownRow(titles) - fmt.Fprintf(output, "%s\n", titleRow) + fmt.Fprintf(writer, "%s\n", titleRow) alignments := createMarkdownColumnAlignment(titles) alignmentRow := createMarkdownRow(alignments) - fmt.Fprintf(output, "%s\n", alignmentRow) + fmt.Fprintf(writer, "%s\n", alignmentRow) // Display a warning "missing" in the actual output and return (short-circuit) entries := bom.VulnerabilityMap.Entries() // Emit no vuln. found warning into output if len(entries) == 0 { - fmt.Fprintf(output, "%s\n", MSG_OUTPUT_NO_VULNERABILITIES_FOUND) + fmt.Fprintf(writer, "%s\n", MSG_OUTPUT_NO_VULNERABILITIES_FOUND) return fmt.Errorf(MSG_OUTPUT_NO_VULNERABILITIES_FOUND) } @@ -401,7 +400,7 @@ func DisplayVulnListMarkdown(bom *schema.BOM, output io.Writer, flags utils.Vuln flags.Summary, ) lineRow = createMarkdownRow(line) - fmt.Fprintf(output, "%s\n", lineRow) + fmt.Fprintf(writer, "%s\n", lineRow) } @@ -409,7 +408,7 @@ func DisplayVulnListMarkdown(bom *schema.BOM, output io.Writer, flags utils.Vuln } // Output filtered list of vulnerabilities as JSON -func DisplayVulnListJson(bom *schema.BOM, output io.Writer, flags utils.VulnerabilityCommandFlags) { +func DisplayVulnListJson(bom *schema.BOM, writer io.Writer, flags utils.VulnerabilityCommandFlags) { getLogger().Enter() defer getLogger().Exit() @@ -424,8 +423,8 @@ func DisplayVulnListJson(bom *schema.BOM, output io.Writer, flags utils.Vulnerab vulnList = append(vulnList, vulnInfo.Vulnerability) } } - json, _ := log.FormatInterfaceAsJson(vulnList) - // Note: JSON data files MUST ends in a newline s as this is a POSIX standard - fmt.Fprintf(output, "%s\n", json) + // Note: JSON data files MUST ends in a newline as this is a POSIX standard + // which is already accounted for by the JSON encoder. + utils.WriteAnyAsEncodedJSONInt(writer, vulnList, utils.GlobalFlags.PersistentFlags.GetOutputIndentInt()) } diff --git a/cmd/vulnerability_test.go b/cmd/vulnerability_test.go index 2f00da1b..2243cb2f 100644 --- a/cmd/vulnerability_test.go +++ b/cmd/vulnerability_test.go @@ -45,17 +45,22 @@ var VULN_TEST_DEFAULT_FLAGS utils.VulnerabilityCommandFlags // Stringer interface for ResourceTestInfo (just display subset of key values) func (ti *VulnTestInfo) String() string { - pParent := &ti.CommonTestInfo - return pParent.String() + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(ti) + return buffer.String() } func NewVulnTestInfo(inputFile string, listFormat string, listSummary bool, whereClause string, - resultContainsValues []string, resultExpectedLineCount int, resultExpectedError error) *VulnTestInfo { - + resultExpectedLineCount int) *VulnTestInfo { var ti = new(VulnTestInfo) var pCommon = &ti.CommonTestInfo - pCommon.Init(inputFile, listFormat, listSummary, whereClause, - resultContainsValues, resultExpectedLineCount, resultExpectedError) + pCommon.Init( + inputFile, + listFormat, + listSummary, + whereClause, + nil, + resultExpectedLineCount, + nil) return ti } @@ -248,9 +253,7 @@ func TestVulnListTextCdx14WhereClauseAndResultsByIdStartsWith(t *testing.T) { FORMAT_TEXT, TI_LIST_SUMMARY_FALSE, TEST_INPUT_WHERE_CLAUSE, - nil, - TEST_OUTPUT_LINES, - nil) + TEST_OUTPUT_LINES) testInfo.ResultLineContainsValues = TEST_OUTPUT_CONTAINS testInfo.ResultLineContainsValuesAtLineNum = 2 result, _, _ := innerTestVulnList(t, testInfo, VULN_TEST_DEFAULT_FLAGS) @@ -267,9 +270,7 @@ func TestVulnListTextCdx14WhereClauseDescContains(t *testing.T) { FORMAT_TEXT, TI_LIST_SUMMARY_FALSE, TEST_INPUT_WHERE_CLAUSE, - nil, - TEST_OUTPUT_LINES, - nil) + TEST_OUTPUT_LINES) testInfo.ResultLineContainsValues = TEST_OUTPUT_CONTAINS testInfo.ResultLineContainsValuesAtLineNum = 2 innerTestVulnList(t, testInfo, VULN_TEST_DEFAULT_FLAGS) @@ -285,9 +286,7 @@ func TestVulnListTextCdx14WhereClauseSourceNameNVD(t *testing.T) { FORMAT_TEXT, TI_LIST_SUMMARY_FALSE, TEST_INPUT_WHERE_CLAUSE, - nil, - TEST_OUTPUT_LINES, - nil) + TEST_OUTPUT_LINES) testInfo.ResultLineContainsValues = TEST_OUTPUT_CONTAINS testInfo.ResultLineContainsValuesAtLineNum = 3 innerTestVulnList(t, testInfo, VULN_TEST_DEFAULT_FLAGS) @@ -303,9 +302,7 @@ func TestVulnListTextCdx14WhereClauseSourceUrlCVE2022(t *testing.T) { FORMAT_TEXT, TI_LIST_SUMMARY_FALSE, TEST_INPUT_WHERE_CLAUSE, - nil, - TEST_OUTPUT_LINES, - nil) + TEST_OUTPUT_LINES) testInfo.ResultLineContainsValues = TEST_OUTPUT_CONTAINS testInfo.ResultLineContainsValuesAtLineNum = 3 innerTestVulnList(t, testInfo, VULN_TEST_DEFAULT_FLAGS) diff --git a/common/query_types.go b/common/query_types.go index 1e4c4ba1..52369e6c 100644 --- a/common/query_types.go +++ b/common/query_types.go @@ -59,7 +59,7 @@ type QueryRequest struct { // Implement the Stringer interface for QueryRequest func (qr *QueryRequest) String() string { - buffer, _ := utils.EncodeAnyToIndentedJSON(qr) + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(qr) return buffer.String() } @@ -279,7 +279,7 @@ type QueryResponse struct { // Implement the Stringer interface for QueryRequest func (qr *QueryResponse) String() string { - buffer, _ := utils.EncodeAnyToIndentedJSON(qr) + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(qr) return buffer.String() } @@ -301,7 +301,7 @@ type WhereFilter struct { // Implement the Stringer interface for QueryRequest func (filter *WhereFilter) String() string { - buffer, _ := utils.EncodeAnyToIndentedJSON(filter) + buffer, _ := utils.EncodeAnyToDefaultIndentedJSONStr(filter) return buffer.String() } diff --git a/schema/bom.go b/schema/bom.go index a71a3a40..7d7ef9fa 100644 --- a/schema/bom.go +++ b/schema/bom.go @@ -375,7 +375,7 @@ func (bom *BOM) EncodeAsFormattedJSON(writer io.Writer, prefix string, indent st defer getLogger().Exit() var outputBuffer bytes.Buffer - outputBuffer, err = utils.EncodeAnyToIndentedJSON(bom.CdxBom) + outputBuffer, err = utils.EncodeAnyToIndentedJSONStr(bom.CdxBom, indent) if err != nil { return } diff --git a/schema/bom_traversal.go b/schema/bom_traversal.go index d5bb5ca0..42770dca 100644 --- a/schema/bom_traversal.go +++ b/schema/bom_traversal.go @@ -71,5 +71,4 @@ func (bom *BOM) TrimEntityKey(entity interface{}, key string) { // if type is other than above getLogger().Debugf("unhandled type: [%T]", typedEntity) } - } diff --git a/schema/cyclonedx.go b/schema/cyclonedx.go index 1f551978..5d5c1369 100644 --- a/schema/cyclonedx.go +++ b/schema/cyclonedx.go @@ -63,7 +63,7 @@ type CDXMetadata struct { Tools interface{} `json:"tools,omitempty"` // v1.2: added.v1.5: "tools" is now an interface{} Authors *[]CDXOrganizationalContact `json:"authors,omitempty"` Component *CDXComponent `json:"component,omitempty"` - Manufacturer *CDXOrganizationalEntity `json:"manufacturer,omitempty"` + Manufacturer *CDXOrganizationalEntity `json:"manufacture,omitempty"` // NOTE: Typo is in spec. Supplier *CDXOrganizationalEntity `json:"supplier,omitempty"` Licenses *[]CDXLicenseChoice `json:"licenses,omitempty"` // v1.3 added Properties *[]CDXProperty `json:"properties,omitempty"` // v1.3 added diff --git a/test/trim/trim-cdx-1-5-sample-medium-1.sbom.json b/test/trim/trim-cdx-1-5-sample-medium-1.sbom.json index 57530608..ecdfead6 100644 --- a/test/trim/trim-cdx-1-5-sample-medium-1.sbom.json +++ b/test/trim/trim-cdx-1-5-sample-medium-1.sbom.json @@ -190,4 +190,4 @@ "value": "123" } ] -} \ No newline at end of file +} diff --git a/utils/flags.go b/utils/flags.go index 9eda4ba5..da881e01 100644 --- a/utils/flags.go +++ b/utils/flags.go @@ -66,6 +66,11 @@ type PersistentCommandFlags struct { InputFile string OutputFile string // TODO: TODO: Note: not used by `validate` command, which emits a warning if supplied OutputFormat string // e.g., "txt", "csv"", "md" (markdown) (normalized to lowercase) + OutputIndent uint8 +} + +func (persistentFlags PersistentCommandFlags) GetOutputIndentInt() int { + return int(persistentFlags.OutputIndent) } type LicenseCommandFlags struct { diff --git a/utils/json.go b/utils/json.go index f03e572b..fd835ff8 100644 --- a/utils/json.go +++ b/utils/json.go @@ -22,6 +22,13 @@ import ( "bufio" "bytes" "encoding/json" + "io" + "strings" +) + +const ( + DEFAULT_JSON_INDENT_STRING = " " + DEFAULT_JSON_PREFIX_STRING = "" ) func IsJsonMapType(any interface{}) (isMapType bool) { @@ -67,35 +74,41 @@ func MarshalStructToJsonMap(any interface{}) (mapOut map[string]interface{}, err return } +// Creates strings of spaces based upon provided integer length (e.g., the --indent flag) +func GenerateIndentString(length int) (prefix string) { + var sb strings.Builder + for i := 0; i < length; i++ { + sb.WriteString(" ") + } + return sb.String() +} + // NOTE: Using this custom encoder avoids the json.Marshal() default -// behavior of encoding utf8 characters such as: '@', '<', '>', etc. -// as unicode. -func EncodeAnyToIndentedJSON(any interface{}) (outputBuffer bytes.Buffer, err error) { +// behavior of encoding utf8 characters such as: '@', '<', '>', etc. as unicode. +func EncodeAnyToIndentedJSONStr(any interface{}, indent string) (outputBuffer bytes.Buffer, err error) { bufferedWriter := bufio.NewWriter(&outputBuffer) encoder := json.NewEncoder(bufferedWriter) encoder.SetEscapeHTML(false) - encoder.SetIndent("", " ") + encoder.SetIndent(DEFAULT_JSON_PREFIX_STRING, indent) err = encoder.Encode(any) // MUST ensure all data is written to buffer before further testing bufferedWriter.Flush() return } -// TODO: function NOT complete, only placeholder type switch -// TODO: allow generic function to be applied to types -// func PrintTypes(values ...interface{}) { -// for index, value := range values { -// switch t := value.(type) { -// case nil: -// case int: -// case uint: -// case int32: -// case int64: -// case uint64: -// case float32: -// case float64: -// case string: -// case bool: -// } -// } -// } +func EncodeAnyToDefaultIndentedJSONStr(any interface{}) (outputBuffer bytes.Buffer, err error) { + return EncodeAnyToIndentedJSONStr(any, DEFAULT_JSON_INDENT_STRING) +} + +func EncodeAnyToIndentedJSONInt(any interface{}, numSpaces int) (outputBuffer bytes.Buffer, err error) { + indentString := GenerateIndentString(numSpaces) + return EncodeAnyToIndentedJSONStr(any, indentString) +} + +func WriteAnyAsEncodedJSONInt(writer io.Writer, any interface{}, numSpaces int) (outputBuffer bytes.Buffer, err error) { + outputBuffer, err = EncodeAnyToIndentedJSONInt(any, numSpaces) + if writer != nil && err == nil { + _, err = writer.Write(outputBuffer.Bytes()) + } + return +} diff --git a/utils/runtime.go b/utils/runtime.go index 788cfd95..cf089506 100644 --- a/utils/runtime.go +++ b/utils/runtime.go @@ -24,15 +24,18 @@ import ( "strings" ) -func GetCallerFunctionName(index uint64) (fxName string) { +func GetCallerFunctionName(skip int) (fxName string) { pCallers := make([]uintptr, 4) // Note: immediate caller is at index "2" on the stack - runtime.Callers(2, pCallers) + runtime.Callers(skip, pCallers) if len(pCallers) > 0 { fx := runtime.FuncForPC(pCallers[0]) fxName = fx.Name() if index := strings.LastIndex(fxName, string(os.PathSeparator)); index > -1 { - fxName = fxName[index:] + fxName = fxName[index+1:] + } + if index := strings.LastIndex(fxName, "."); index > -1 { + fxName = fxName[index+1:] } } return