From fb1056e7d71344d9345fe3a0d5056a3b81b1145f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 21 Jul 2023 14:56:39 +0200 Subject: [PATCH 1/4] Json/Validator: add findNamedElements() to detect duplicate "bom-ref" definitions Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 99 ++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 04506afe..dd4a5e57 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -166,6 +166,88 @@ public static ValidationResult Validate(string jsonString, SpecificationVersion } } + 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)) + { + 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 the seeked JsonElement as keys, + /// and a List of "parent" JsonElement (which contain such key) 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) { + if (!hits.ContainsKey(property.Value)) + { + hits.Add(property.Value, new List()); + } + hits[property.Value].Add(element); + } + + // 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(); @@ -190,6 +272,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.GetString()}: expected 1 mention, actual {KVP.Value.Count}"); + } + } } else { From 3fea2a11bb8168c3763e0e4d1e582358a9903772 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 21 Jul 2023 17:44:21 +0200 Subject: [PATCH 2/4] Json/Validator.cs: when checking for (bom-ref) uniqueness, consider string representation of the JsonElement, not object (which is in fact unique for each node) Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 39 +++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index dd4a5e57..681106d7 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -166,9 +166,9 @@ public static ValidationResult Validate(string jsonString, SpecificationVersion } } - private static Dictionary> addDictList( - Dictionary> dict1, - Dictionary> dict2) + private static Dictionary> addDictList( + Dictionary> dict1, + Dictionary> dict2) { if (dict2 == null || dict2.Count == 0) { @@ -180,10 +180,12 @@ private static Dictionary> addDictList( return dict2; } - foreach (KeyValuePair> KVP in dict2) + foreach (KeyValuePair> KVP in dict2) { if (dict1.ContainsKey(KVP.Key)) { + // NOTE: Possibly different object, but same string representation! + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Adding duplicate for: {KVP.Key}"); dict1[KVP.Key].AddRange(KVP.Value); } else @@ -203,25 +205,29 @@ private static Dictionary> addDictList( /// for the original caller, probably. Then used to recurse. /// /// The property name we seek. - /// A Dictionary with distinct values of the seeked JsonElement as keys, - /// and a List of "parent" JsonElement (which contain such key) as mapped values. + /// 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) + private static Dictionary> findNamedElements(JsonElement element, string name) { - Dictionary> hits = new Dictionary>(); - Dictionary> nestedHits = null; + Dictionary> hits = new Dictionary>(); + Dictionary> nestedHits = null; // Can we iterate further? switch (element.ValueKind) { case JsonValueKind.Object: foreach (JsonProperty property in element.EnumerateObject()) { + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at object property: {property.Name}"); if (property.Name == name) { - if (!hits.ContainsKey(property.Value)) + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: GOT A HIT: {property.Name} => ({property.Value.ValueKind}) {property.Value.ToString()}"); + string key = property.Value.ToString(); + if (!(hits.ContainsKey(key))) { - hits.Add(property.Value, new List()); + hits.Add(key, new List()); } - hits[property.Value].Add(element); + hits[key].Add(property.Value); } // Note: Here we can recurse into same property that @@ -232,6 +238,7 @@ private static Dictionary> findNamedElements(Json break; case JsonValueKind.Array: + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at List"); foreach (JsonElement nestedElem in element.EnumerateArray()) { nestedHits = findNamedElements(nestedElem, name); @@ -283,10 +290,12 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc // 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) { + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at bom-ref uniqueness"); + Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); + foreach (KeyValuePair> KVP in bomRefs) { + // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: [{KVP.Value.Count}] '{KVP.Key}' => {KVP.Value.ToString()}"); if (KVP.Value != null && KVP.Value.Count != 1) { - validationMessages.Add($"'bom-ref' value of {KVP.Key.GetString()}: expected 1 mention, actual {KVP.Value.Count}"); + validationMessages.Add($"'bom-ref' value of {KVP.Key}: expected 1 mention, actual {KVP.Value.Count}"); } } } From c7f4ba202b74b40bfb649ddc09a514f3bc8b80bd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 21 Jul 2023 18:09:07 +0200 Subject: [PATCH 3/4] Json/Validator.cs: drop #COMMENTED-DEBUG# for findNamedElements() troubleshooting Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 681106d7..0d2cc76b 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -185,7 +185,6 @@ private static Dictionary> addDictList( if (dict1.ContainsKey(KVP.Key)) { // NOTE: Possibly different object, but same string representation! - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Adding duplicate for: {KVP.Key}"); dict1[KVP.Key].AddRange(KVP.Value); } else @@ -219,9 +218,7 @@ private static Dictionary> findNamedElements(JsonEleme case JsonValueKind.Object: foreach (JsonProperty property in element.EnumerateObject()) { - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at object property: {property.Name}"); if (property.Name == name) { - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: GOT A HIT: {property.Name} => ({property.Value.ValueKind}) {property.Value.ToString()}"); string key = property.Value.ToString(); if (!(hits.ContainsKey(key))) { @@ -238,7 +235,6 @@ private static Dictionary> findNamedElements(JsonEleme break; case JsonValueKind.Array: - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at List"); foreach (JsonElement nestedElem in element.EnumerateArray()) { nestedHits = findNamedElements(nestedElem, name); @@ -290,10 +286,8 @@ private static ValidationResult Validate(JsonSchema schema, JsonDocument jsonDoc // document seems structurally intact otherwise. // Note that this is not a problem for the XML schema with // its explicit constraint. - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: Looking at bom-ref uniqueness"); Dictionary> bomRefs = findNamedElements(jsonDocument.RootElement, "bom-ref"); foreach (KeyValuePair> KVP in bomRefs) { - // #COMMENTED-DEBUG# Console.WriteLine($"VALIDATE: [{KVP.Value.Count}] '{KVP.Key}' => {KVP.Value.ToString()}"); if (KVP.Value != null && KVP.Value.Count != 1) { validationMessages.Add($"'bom-ref' value of {KVP.Key}: expected 1 mention, actual {KVP.Value.Count}"); } From aff0ea5de42eda64f35872ab4882a1a82a773846 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Mon, 14 Aug 2023 20:44:18 +0200 Subject: [PATCH 4/4] Validator.cs: document addDictList() Signed-off-by: Jim Klimov --- src/CycloneDX.Core/Json/Validator.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/CycloneDX.Core/Json/Validator.cs b/src/CycloneDX.Core/Json/Validator.cs index 0d2cc76b..5f4acc09 100644 --- a/src/CycloneDX.Core/Json/Validator.cs +++ b/src/CycloneDX.Core/Json/Validator.cs @@ -166,6 +166,15 @@ 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)