diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 205c5290..b04eb736 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -170,6 +170,100 @@ public static ValidationResult Validate(string jsonString, SpecificationVersion } } + /// + /// Merge two dictionaries whose values are lists of JsonElements, + /// adding all entries from list in dict2 for the same key as in + /// dict1 (or adds a new entry for a new key). Manipulates a COPY + /// of dict1, then returns this copy. + /// + /// Dict with lists as values + /// Dict with lists as values + /// Copy of dict1+dict2 + private static Dictionary> addDictList( + Dictionary> dict1, + Dictionary> dict2) + { + if (dict2 == null || dict2.Count == 0) + { + return dict1; + } + + if (dict1 == null || dict1.Count == 0) + { + return dict2; + } + + foreach (KeyValuePair> KVP in dict2) + { + if (dict1.ContainsKey(KVP.Key)) + { + // NOTE: Possibly different object, but same string representation! + dict1[KVP.Key].AddRange(KVP.Value); + } + else + { + dict1.Add(KVP.Key, KVP.Value); + } + } + + return dict1; + } + + /// + /// Iterate through the JSON document to find JSON objects whose property names + /// match the one we seek, and add such hits to returned list. Recurse and repeat. + /// + /// A JsonElement, starting from JsonDocument.RootElement + /// for the original caller, probably. Then used to recurse. + /// + /// The property name we seek. + /// A Dictionary with distinct values of string representation of the + /// seeked JsonElement as keys, and a List of actual JsonElement objects as + /// mapped values. + /// + private static Dictionary> findNamedElements(JsonElement element, string name) + { + Dictionary> hits = new Dictionary>(); + Dictionary> nestedHits = null; + + // Can we iterate further? + switch (element.ValueKind) { + case JsonValueKind.Object: + foreach (JsonProperty property in element.EnumerateObject()) + { + if (property.Name == name) { + string key = property.Value.ToString(); + if (!(hits.ContainsKey(key))) + { + hits.Add(key, new List()); + } + hits[key].Add(property.Value); + } + + // Note: Here we can recurse into same property that + // we've just listed, if it is not of a simple kind. + nestedHits = findNamedElements(property.Value, name); + hits = addDictList(hits, nestedHits); + } + break; + + case JsonValueKind.Array: + foreach (JsonElement nestedElem in element.EnumerateArray()) + { + nestedHits = findNamedElements(nestedElem, name); + hits = addDictList(hits, nestedHits); + } + break; + + default: + // No-op for simple types: these values per se have no name + // to learn, and we can not iterate deeper into them. + break; + } + + return hits; + } + private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDocument, string schemaVersionString) { var validationMessages = new List(); @@ -194,6 +288,23 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc } } } + + // The JSON Schema, at least the ones defined by CycloneDX + // and handled by current parser in dotnet ecosystem, can + // not specify or check the uniqueness requirement for the + // "bom-ref" assignments in the overall document (e.g. in + // "metadata/component" and list of "components", as well + // as in "services" and "vulnerabilities", as of CycloneDX + // spec v1.4), so this is checked separately here if the + // document seems structurally intact otherwise. + // Note that this is not a problem for the XML schema with + // its explicit constraint. + Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); + foreach (KeyValuePair> KVP in bomRefs) { + if (KVP.Value != null && KVP.Value.Count != 1) { + validationMessages.Add($"'bom-ref' value of {KVP.Key}: expected 1 mention, actual {KVP.Value.Count}"); + } + } } else {