diff --git a/cmd/validate.go b/cmd/validate.go index 8c7088b..d75a6a0 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -181,6 +181,66 @@ func validationError(document *schema.BOM, valid bool, err error) { getLogger().Info(message) } +func LoadSchemaDependencies(depSchemaLoader *gojsonschema.SchemaLoader, schemas []schema.FormatSchemaInstance, schemaNames []string) (err error) { + for _, schemaName := range schemaNames { + formatSchemaInstance, errMatch := schema.FindMatchingFormatSchemaInstance( + schemas, schemaName) + if errMatch != nil { + return fmt.Errorf("schema '%s' match not found in resources: '%s'", schemaName, schema.SCHEMA_FORMAT_COMMON) + } + getLogger().Debugf("Found: '%s': %v", schemaName, formatSchemaInstance) + + getLogger().Debugf("Added schema '%s' to loader:...", formatSchemaInstance.File) + err = AddSharedSchemaToLoader(depSchemaLoader, formatSchemaInstance) + if err != nil { + return + } + } + return +} + +func AddSharedSchemaToLoader(depSchemaLoader *gojsonschema.SchemaLoader, formatSchemaInstance schema.FormatSchemaInstance) (err error) { + getLogger().Debugf("Reading schema: '%s'...", formatSchemaInstance.File) + bSchema, errRead := resources.BOMSchemaFiles.ReadFile(formatSchemaInstance.File) + + if errRead != nil { + return errRead + } + getLogger().Tracef("schema: %s", bSchema) + sharedSchemaLoader := gojsonschema.NewBytesLoader(bSchema) + depSchemaLoader.AddSchema(formatSchemaInstance.Url, sharedSchemaLoader) + return +} + +func LoadCompileSchemaDependencies(bomSchemaLoader gojsonschema.JSONLoader, bomSchemaDependencies []string) (jsonBOMSchema *gojsonschema.Schema, err error) { + + if len(bomSchemaDependencies) > 0 { + getLogger().Infof("Found schema dependencies: %v", bomSchemaDependencies) + // Create a schema loader we will add all dep. schemas into + depSchemaLoader := gojsonschema.NewSchemaLoader() + + // Load common schema from application resources + var commonSchemas schema.FormatSchema + commonSchemas, err = SupportedFormatConfig.FindMatchingFormatSchema(schema.SCHEMA_FORMAT_COMMON) + if err != nil { + return + } + getLogger().Tracef("Found '%s' schemas: %v", schema.SCHEMA_FORMAT_COMMON, commonSchemas) + + err = LoadSchemaDependencies(depSchemaLoader, commonSchemas.Schemas, bomSchemaDependencies) + if err != nil { + return + } + + // Compile BOM schema (JSON) with the dependency schemas and return it + jsonBOMSchema, err = depSchemaLoader.Compile(bomSchemaLoader) + if err != nil { + return + } + } + return +} + func Validate(writer io.Writer, persistentFlags utils.PersistentCommandFlags, validateFlags utils.ValidateCommandFlags) (valid bool, bom *schema.BOM, schemaErrors []gojsonschema.ResultError, err error) { getLogger().Enter() defer getLogger().Exit() @@ -213,8 +273,8 @@ func Validate(writer io.Writer, persistentFlags utils.PersistentCommandFlags, va // Create a loader for the BOM (JSON) document var documentLoader gojsonschema.JSONLoader - var schemaLoader gojsonschema.JSONLoader - var errRead error + var jsonBOMSchemaLoader gojsonschema.JSONLoader + var errRead, errLoadCompile error var bSchema, bDocument []byte if bDocument = bom.GetRawBytes(); len(bDocument) > 0 { @@ -240,10 +300,8 @@ func Validate(writer io.Writer, persistentFlags utils.PersistentCommandFlags, va // If caller "forced" a specific schema file (version), load it instead of // any SchemaInfo found in config.json - // TODO: support remote schema load (via URL) with a flag (default should always be local file for security) forcedSchemaFile := validateFlags.ForcedJsonSchemaFile if forcedSchemaFile != "" { - if !isValidURIPrefix(forcedSchemaFile) { // attempt to load as a local file forcedSchemaFile = "file://" + forcedSchemaFile @@ -257,7 +315,7 @@ func Validate(writer io.Writer, persistentFlags utils.PersistentCommandFlags, va } getLogger().Infof("Loading schema from '--force' flag: '%s'...", forcedSchemaFile) - schemaLoader = gojsonschema.NewReferenceLoader(forcedSchemaFile) + jsonBOMSchemaLoader = gojsonschema.NewReferenceLoader(forcedSchemaFile) getLogger().Infof("Validating document using forced schema (i.e., '--force %s')", forcedSchemaFile) } else { // Load the matching JSON schema (format, version and variant) from embedded resources @@ -271,33 +329,19 @@ func Validate(writer io.Writer, persistentFlags utils.PersistentCommandFlags, va return INVALID, bom, schemaErrors, errRead } - sl := gojsonschema.NewSchemaLoader() - - schemaLoader = gojsonschema.NewBytesLoader(bSchema) - - // NEW NEW NEW - JSF := "schema/cyclonedx/common/jsf-0.82.schema.json" - getLogger().Infof("Loading schema '%s'...", JSF) - bJsfSchema, _ := resources.BOMSchemaFiles.ReadFile(JSF) - getLogger().Tracef("schema: %s", bJsfSchema) - jsfSchemaLoader := gojsonschema.NewBytesLoader(bJsfSchema) - sl.AddSchema("http://cyclonedx.org/schema/jsf-0.82.schema.json", jsfSchemaLoader) - - SPDXID := "schema/cyclonedx/common/spdx.schema.json" - getLogger().Infof("Loading schema '%s'...", SPDXID) - bSpdxIdSchema, _ := resources.BOMSchemaFiles.ReadFile(SPDXID) - getLogger().Tracef("schema: %s", bSpdxIdSchema) - spdxSchemaLoader := gojsonschema.NewBytesLoader(bSpdxIdSchema) - sl.AddSchema("http://cyclonedx.org/schema/spdx.schema.json", spdxSchemaLoader) - - var errCompile error - jsonBOMSchema, errCompile = sl.Compile(schemaLoader) - if errCompile != nil { - getLogger().Error(errCompile) + // Create a schema loader for the primary BOM schema file + jsonBOMSchemaLoader = gojsonschema.NewBytesLoader(bSchema) + + // If the BOM schema has $refs to other schemas, attempt to load and compile + // them from those included as built-in resources + jsonBOMSchema, errLoadCompile = LoadCompileSchemaDependencies(jsonBOMSchemaLoader, bom.SchemaInfo.Dependencies) + if err != nil { + return INVALID, bom, schemaErrors, errLoadCompile } } - if schemaLoader == nil { + // At this point we should have a BOM schema loader + if jsonBOMSchemaLoader == nil { // we force result to INVALID as any errors from the library means // we could NOT actually confirm the input documents validity return INVALID, bom, schemaErrors, fmt.Errorf("unable to read schema: '%s'", schemaName) @@ -314,7 +358,7 @@ func Validate(writer io.Writer, persistentFlags utils.PersistentCommandFlags, va // externally referenced schemas over network)... attempt fixed retry... if jsonBOMSchema == nil { for i := 0; i < RETRY; i++ { - jsonBOMSchema, errLoad = gojsonschema.NewSchema(schemaLoader) + jsonBOMSchema, errLoad = gojsonschema.NewSchema(jsonBOMSchemaLoader) if errLoad == nil { break @@ -327,7 +371,7 @@ func Validate(writer io.Writer, persistentFlags utils.PersistentCommandFlags, va } } - getLogger().Infof("Schema '%s' loaded.", schemaName) + getLogger().Infof("Schema '%s' loaded", schemaName) // Validate against the schema and save result determination getLogger().Infof("Validating '%s'...", bom.GetFilenameInterpolated()) diff --git a/resources/config/config.json b/resources/config/config.json index 96967dd..d247b68 100644 --- a/resources/config/config.json +++ b/resources/config/config.json @@ -1,16 +1,16 @@ { "formats": [ { - "canonicalName": "http://cyclonedx.org/schema/", - "propertyKeyFormat": "http://cyclonedx.org/schema/", - "propertyKeyVersion": "latest", + "canonicalName": "common", + "propertyKeyFormat": "", + "propertyKeyVersion": "version", "propertyValueFormat": "latest", "schemas": [ { "version": "", "variant": "", "name": "jsf-0.82.schema.json", - "file": "resources/schema/cyclonedx/common/jsf-0.82.schema.json", + "file": "schema/cyclonedx/common/jsf-0.82.schema.json", "development": "", "url": "http://cyclonedx.org/schema/jsf-0.82.schema.json", "default": false @@ -19,7 +19,7 @@ "version": "", "variant": "", "name": "spdx.schema.json", - "file": "resources/schema/cyclonedx/common/spdx.schema.json", + "file": "schema/cyclonedx/common/spdx.schema.json", "development": "", "url": "http://cyclonedx.org/schema/spdx.schema.json", "default": false @@ -119,7 +119,8 @@ "file": "schema/cyclonedx/1.4/bom-1.4.schema.json", "development": "https://github.com/CycloneDX/specification/blob/master/schema/bom-1.4.schema.json", "url": "https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.4.schema.json", - "default": false + "default": false, + "dependencies": ["jsf-0.82.schema.json", "spdx.schema.json"] }, { "version": "1.5", @@ -128,7 +129,8 @@ "file": "schema/cyclonedx/1.5/bom-1.5.schema.json", "development": "https://github.com/CycloneDX/specification/blob/master/schema/bom-1.5.schema.json", "url": "https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.5.schema.json", - "default": false + "default": false, + "dependencies": ["jsf-0.82.schema.json", "spdx.schema.json"] }, { "version": "1.6", @@ -137,7 +139,8 @@ "file": "schema/cyclonedx/1.6/bom-1.6.schema.json", "development": "https://github.com/CycloneDX/specification/blob/master/schema/bom-1.6.schema.json", "url": "https://raw.githubusercontent.com/CycloneDX/specification/master/schema/bom-1.6.schema.json", - "default": true + "default": true, + "dependencies": ["jsf-0.82.schema.json", "spdx.schema.json"] }, { "version": "1.3", @@ -155,7 +158,8 @@ "file": "schema/test/bom-1.4-custom.schema.json", "development":"https://github.com/CycloneDX/sbom-utility/blob/main/resources/schema/test/bom-1.4-custom.schema.json", "url": "", - "default": false + "default": false, + "dependencies": ["jsf-0.82.schema.json", "spdx.schema.json"] } ] } diff --git a/schema/schema_formats.go b/schema/schema_formats.go index 2a71369..d5dd96a 100644 --- a/schema/schema_formats.go +++ b/schema/schema_formats.go @@ -32,6 +32,7 @@ import ( const ( SCHEMA_FORMAT_SPDX = "SPDX" SCHEMA_FORMAT_CYCLONEDX = "CycloneDX" + SCHEMA_FORMAT_COMMON = "common" ) const ( @@ -93,14 +94,15 @@ type FormatSchema struct { // e.g., key string // where key: SchemaKey{ID_CYCLONEDX, VERSION_CYCLONEDX_1_3, false}, type FormatSchemaInstance struct { - Name string `json:"name"` - Version string `json:"version"` - Development string `json:"development"` - File string `json:"file"` - Url string `json:"url"` - Default bool `json:"default"` - Variant string `json:"variant"` - Format string `json:"format"` // value set from parent FormatSchema's `CanonicalName` + Name string `json:"name"` + Version string `json:"version"` + Development string `json:"development"` + File string `json:"file"` + Url string `json:"url"` + Default bool `json:"default"` + Variant string `json:"variant"` + Format string `json:"format"` // value set from parent FormatSchema's `CanonicalName` + Dependencies []string `json:"dependencies"` } func (config *BOMFormatAndSchemaConfig) Reset() { @@ -201,6 +203,38 @@ func (schemaConfig *BOMFormatAndSchemaConfig) FindFormatAndSchema(bom *BOM) (err return } +func (schemaConfig *BOMFormatAndSchemaConfig) FindMatchingFormatSchema(name string) (formatSchema FormatSchema, err error) { + getLogger().Enter() + defer getLogger().Exit() + + // Iterate over configured schema formats to find matching name + for _, formatSchema = range schemaConfig.Formats { + if name == formatSchema.CanonicalName { + return + } + } + + // Inform user we could not find a matching schema + err = NewUnsupportedSchemaError(MSG_CONFIG_SCHEMA_VERSION_NOT_FOUND, name, "", "") + return +} + +func FindMatchingFormatSchemaInstance(formatSchemas []FormatSchemaInstance, schemaName string) (formatSchemaInstance FormatSchemaInstance, err error) { + getLogger().Enter() + defer getLogger().Exit() + + // Iterate over known formats to see if SBOM document contains a known value + for _, formatSchemaInstance = range formatSchemas { + if schemaName == formatSchemaInstance.Name { + return + } + } + + // if we reach here, we did not find the format in our configuration (list) + err = NewUnsupportedSchemaError(MSG_CONFIG_SCHEMA_VERSION_NOT_FOUND, schemaName, "", "") + return +} + // There are multiple variants possible within a given version func (bom *BOM) findSchemaVersionWithVariant(format FormatSchema, version string, variant string) (err error) { getLogger().Enter()