diff --git a/lib/schema/cache.ex b/lib/schema/cache.ex index 0da30bf..c0b8679 100644 --- a/lib/schema/cache.ex +++ b/lib/schema/cache.ex @@ -379,8 +379,7 @@ defmodule Schema.Cache do defp read_objects(observable_type_id_map) do objects = JsonReader.read_objects() - observable_type_id_map = - observables_from_objects(objects, observable_type_id_map) + observable_type_id_map = observables_from_objects(observable_type_id_map, objects) objects = objects @@ -414,6 +413,7 @@ defmodule Schema.Cache do {objects, all_objects, observable_type_id_map} end + @spec observables_from_classes(map()) :: map() defp observables_from_classes(classes) do Enum.reduce( classes, @@ -422,8 +422,8 @@ defmodule Schema.Cache do validate_class_observables(class_key, class) observable_type_id_map - |> observables_from_item_attributes(class, "Class") - |> observables_from_item_observables(class, "Class") + |> observables_from_item_attributes(classes, class_key, class, "Class") + |> observables_from_item_observables(classes, class_key, class, "Class") end ) end @@ -469,37 +469,12 @@ defmodule Schema.Cache do System.stop(1) end end - - if patch_extends?(class) do - if Map.has_key?(class, :attributes) and - Enum.any?( - class[:attributes], - fn {_attribute_key, attribute} -> - Map.has_key?(attribute, :observable) - end - ) do - Logger.error( - "Illegal definition of one or more attributes with \"#{:observable}\" definition in" <> - " patch extends class \"#{class_key}\". Observable definitions in patch extends are" <> - " not supported. Please file an issue if you find this feature necessary." - ) - - System.stop(1) - end - - if Map.has_key?(class, :observables) do - Logger.error( - "Illegal \"#{:observables}\" definition in patch extends class \"#{class_key}\"." <> - " Observable definitions in patch extends are not supported." <> - " Please file an issue if you find this feature necessary." - ) - - System.stop(1) - end - end end - defp observables_from_item_attributes(observable_type_id_map, item, kind) do + @spec observables_from_item_attributes(map(), map(), atom(), map(), String.t()) :: map() + defp observables_from_item_attributes(observable_type_id_map, items, item_key, item, kind) do + {caption, _description} = find_item_caption_and_description(items, item_key, item) + if Map.has_key?(item, :attributes) do Enum.reduce( item[:attributes], @@ -508,10 +483,10 @@ defmodule Schema.Cache do if Map.has_key?(attribute, :observable) do observable_type_id = Utils.observable_type_id_to_atom(attribute[:observable]) - if(Map.has_key?(observable_type_id_map, observable_type_id)) do + if Map.has_key?(observable_type_id_map, observable_type_id) do Logger.error( "Collision of observable type_id #{observable_type_id} between" <> - " \"#{item[:caption]}\" #{kind} attribute \"#{attribute_key}\" and" <> + " \"#{caption}\" #{kind} attribute \"#{attribute_key}\" and" <> " \"#{observable_type_id_map[observable_type_id][:caption]}\"" ) @@ -523,11 +498,9 @@ defmodule Schema.Cache do observable_type_id_map, observable_type_id, %{ - caption: - "#{item[:caption]} #{kind}: #{attribute_key} (#{kind}-Specific Attribute)", + caption: "#{caption} #{kind}: #{attribute_key} (#{kind}-Specific Attribute)", description: - "#{kind}-specific attribute \"#{attribute_key}\"" <> - " for the #{item[:caption]} #{kind}." + "#{kind}-specific attribute \"#{attribute_key}\" for the #{caption} #{kind}." } ) end @@ -541,7 +514,10 @@ defmodule Schema.Cache do end end - defp observables_from_item_observables(observable_type_id_map, item, kind) do + @spec observables_from_item_observables(map(), map(), atom(), map(), String.t()) :: map() + defp observables_from_item_observables(observable_type_id_map, items, item_key, item, kind) do + {caption, _description} = find_item_caption_and_description(items, item_key, item) + if Map.has_key?(item, :observables) do Enum.reduce( item[:observables], @@ -552,7 +528,7 @@ defmodule Schema.Cache do if(Map.has_key?(observable_type_id_map, observable_type_id)) do Logger.error( "Collision of observable type_id #{observable_type_id} between" <> - " \"#{item[:caption]}\" #{kind} attribute path \"#{attribute_path}\" and" <> + " \"#{caption}\" #{kind} attribute path \"#{attribute_path}\" and" <> " \"#{observable_type_id_map[observable_type_id][:caption]}\"" ) @@ -565,11 +541,10 @@ defmodule Schema.Cache do observable_type_id, %{ caption: - "#{item[:caption]} #{kind}: #{attribute_path}" <> - " (#{kind}-Specific Attribute Path)", + "#{caption} #{kind}: #{attribute_path} (#{kind}-Specific Attribute Path)", description: "#{kind}-specific attribute on path \"#{attribute_path}\"" <> - " for the #{item[:caption]} #{kind}." + " for the #{caption} #{kind}." } ) end @@ -580,7 +555,8 @@ defmodule Schema.Cache do end end - defp observables_from_objects(objects, observable_type_id_map) do + @spec observables_from_objects(map(), map()) :: map() + defp observables_from_objects(observable_type_id_map, objects) do Enum.reduce( objects, observable_type_id_map, @@ -588,10 +564,10 @@ defmodule Schema.Cache do validate_object_observables(object_key, object) observable_type_id_map - |> observable_from_object(object) - |> observables_from_item_attributes(object, "Object") + |> observable_from_object(objects, object_key, object) + |> observables_from_item_attributes(objects, object_key, object, "Object") - # Not supported: |> observables_from_item_observables(object, "Object") + # Not supported: |> observables_from_item_observables(objects, object_key, object, "Object") end ) end @@ -639,44 +615,19 @@ defmodule Schema.Cache do System.stop(1) end end - - if patch_extends?(object) do - if Map.has_key?(object, :attributes) and - Enum.any?( - object[:attributes], - fn {_attribute_key, attribute} -> - Map.has_key?(attribute, :observable) - end - ) do - Logger.error( - "Illegal definition of one or more attributes with \"#{:observable}\" in patch extends" <> - " object \"#{object_key}\". Observable definitions in patch extends are not" <> - " supported. Please file an issue if you find this feature necessary." - ) - - System.stop(1) - end - - if Map.has_key?(object, :observable) do - Logger.error( - "Illegal \"#{:observable}\" definition in patch extends object \"#{object_key}\"." <> - " Observable definitions in patch extends are not supported." <> - " Please file an issue if you find this feature necessary." - ) - - System.stop(1) - end - end end - defp observable_from_object(observable_type_id_map, object) do + @spec observable_from_object(map(), map(), atom(), map()) :: map() + defp observable_from_object(observable_type_id_map, objects, object_key, object) do + {caption, description} = find_item_caption_and_description(objects, object_key, object) + if Map.has_key?(object, :observable) do observable_type_id = Utils.observable_type_id_to_atom(object[:observable]) if(Map.has_key?(observable_type_id_map, observable_type_id)) do Logger.error( "Collision of observable type_id #{observable_type_id} between" <> - " \"#{object[:caption]}\" Object \"#{:observable}\" and" <> + " \"#{caption}\" Object \"#{:observable}\" and" <> " \"#{observable_type_id_map[observable_type_id][:caption]}\"" ) @@ -687,10 +638,7 @@ defmodule Schema.Cache do Map.put( observable_type_id_map, observable_type_id, - %{ - caption: "#{object[:caption]} (Object)", - description: object[:description] - } + %{caption: "#{caption} (Object)", description: description} ) end else @@ -700,11 +648,11 @@ defmodule Schema.Cache do defp observables_from_dictionary(dictionary, observable_type_id_map) do observable_type_id_map - |> observables_from_items(dictionary[:types][:attributes], "Dictionary Type") - |> observables_from_items(dictionary[:attributes], "Dictionary Attribute") + |> observables_from_dictionary_items(dictionary[:types][:attributes], "Dictionary Type") + |> observables_from_dictionary_items(dictionary[:attributes], "Dictionary Attribute") end - defp observables_from_items(observable_type_id_map, items, kind) do + defp observables_from_dictionary_items(observable_type_id_map, items, kind) do if items do Enum.reduce( items, @@ -743,6 +691,44 @@ defmodule Schema.Cache do end end + @spec find_item_caption_and_description(map(), atom(), map() | nil) :: {String.t(), String.t()} + defp find_item_caption_and_description(items, item_key, item) + when is_map(items) and is_atom(item_key) do + cond do + item == nil -> + caption = Atom.to_string(item_key) + {caption, caption} + + patch_extends?(item) -> + find_item_parent_caption_and_description(items, item_key, item) + + item[:caption] != nil -> + caption = item[:caption] + {caption, item[:description] || caption} + + item[:extends] != nil -> + find_item_parent_caption_and_description(items, item_key, item) + + true -> + caption = Atom.to_string(item_key) + {caption, caption} + end + end + + @spec find_item_parent_caption_and_description(map(), atom(), map() | nil) :: + {String.t(), String.t()} + defp find_item_parent_caption_and_description(items, item_key, item) + when is_map(items) and is_atom(item_key) do + {parent_key, parent_item} = Utils.find_parent(items, item[:extends], item[:extension]) + + if parent_key do + find_item_caption_and_description(items, parent_key, parent_item) + else + caption = Atom.to_string(item_key) + {caption, caption} + end + end + @spec hidden_object?(atom() | String.t()) :: boolean() defp hidden_object?(object_name) when is_binary(object_name) do String.starts_with?(object_name, "_") @@ -986,8 +972,13 @@ defmodule Schema.Cache do |> Map.put(:profiles, profiles) |> Map.put(:attributes, attributes) - # Note: :observable, :observables, and :observable in attributes are not supported, - # so this code does not attempt to patch (merge) them. + # Top-level observable. + # Only occurs in objects, but is safe to do for classes too. + patched_base = Utils.put_non_nil(patched_base, :observable, item[:observable]) + + # Top-level path-based observables. + # Only occurs in classes, but is safe to do for objects too. + patched_base = Utils.put_non_nil(patched_base, :observables, item[:observables]) Map.put(acc, base_key, patched_base) end diff --git a/lib/schema/utils.ex b/lib/schema/utils.ex index 8897268..be9cf4f 100644 --- a/lib/schema/utils.ex +++ b/lib/schema/utils.ex @@ -348,8 +348,17 @@ defmodule Schema.Utils do right end + @spec put_non_nil(map(), any(), any()) :: map() + def put_non_nil(map, _key, nil) when is_map(map) do + map + end + + def put_non_nil(map, key, value) when is_map(map) do + Map.put(map, key, value) + end + @doc """ - Filter attributes based on the given profiles. + Filter attributes based on the given profiles. """ @spec apply_profiles(Enum.t(), nil | list() | MapSet.t()) :: Enum.t() def apply_profiles(attributes, nil) do diff --git a/mix.exs b/mix.exs index b65bf97..7757993 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,7 @@ defmodule Schema.MixProject do use Mix.Project - @version "2.69.0" + @version "2.70.0" def project do build = System.get_env("GITHUB_RUN_NUMBER") || "SNAPSHOT" diff --git a/test/test_ocsf_schema/dictionary.json b/test/test_ocsf_schema/dictionary.json index 74e1881..7dc4866 100644 --- a/test/test_ocsf_schema/dictionary.json +++ b/test/test_ocsf_schema/dictionary.json @@ -134,6 +134,11 @@ "description": "The name of the entity. See specific usage.", "type": "string_t" }, + "numeric_value": { + "caption": "Numeric Value", + "description": "A numeric value.", + "type": "float_t" + }, "ob_by_dict_type_1": { "caption": "Ob By Dict Type 1", "description": "Example 1 of attribute of an observable by dictionary type ob_by_type_t.", @@ -169,7 +174,7 @@ }, "service": { "caption": "Service", - "description": "A network service.", + "description": "A service.", "type": "service" }, "source_node": { @@ -254,6 +259,10 @@ "type": "string_t", "type_name": "String" }, + "float_t": { + "caption": "Float", + "description": "Real floating-point value." + }, "integer_t": { "caption": "Integer", "description": "Signed 32-bit integer value." diff --git a/test/test_ocsf_schema/events/alpha.json b/test/test_ocsf_schema/events/alpha.json index 3cf6fbc..29cab2e 100644 --- a/test/test_ocsf_schema/events/alpha.json +++ b/test/test_ocsf_schema/events/alpha.json @@ -4,7 +4,7 @@ "description": "The Alpha example event class.", "name": "alpha", "extends": "ghost", - "uid": 2, + "uid": 1, "profiles": [], "attributes": { "alpha": { diff --git a/test/test_ocsf_schema/events/beta.json b/test/test_ocsf_schema/events/beta.json index 0045dbb..72b150b 100644 --- a/test/test_ocsf_schema/events/beta.json +++ b/test/test_ocsf_schema/events/beta.json @@ -4,7 +4,7 @@ "description": "The Beta example event class.", "name": "beta", "extends": "ghost", - "uid": 3, + "uid": 2, "profiles": [], "attributes": { "beta": { diff --git a/test/test_ocsf_schema/events/eta.json b/test/test_ocsf_schema/events/eta.json new file mode 100644 index 0000000..b0e09a0 --- /dev/null +++ b/test/test_ocsf_schema/events/eta.json @@ -0,0 +1,22 @@ +{ + "caption": "Eta", + "category": "system", + "description": "The Eta example event class.", + "name": "eta", + "extends": "base_event", + "uid": 3, + "profiles": [], + "attributes": { + "name": { + "description": "The name of this eta.", + "requirement": "required", + "observable": 104 + }, + "service": { + "requirement": "recommended" + } + }, + "observables": { + "service.name": 105 + } +} diff --git a/test/test_ocsf_schema/events/network.json b/test/test_ocsf_schema/events/network.json index 9a4a186..fea0d67 100644 --- a/test/test_ocsf_schema/events/network.json +++ b/test/test_ocsf_schema/events/network.json @@ -4,7 +4,7 @@ "description": "The Network event represents a network connection.", "name": "network", "extends": "alpha", - "uid": 1, + "uid": 4, "profiles": [ "host" ], diff --git a/test/test_ocsf_schema/extensions/rpg/dictionary.json b/test/test_ocsf_schema/extensions/rpg/dictionary.json index 6284f0d..b047167 100644 --- a/test/test_ocsf_schema/extensions/rpg/dictionary.json +++ b/test/test_ocsf_schema/extensions/rpg/dictionary.json @@ -33,6 +33,11 @@ "description": "Current hit points.", "type": "hp_t" }, + "kind": { + "caption": "Kind", + "description": "A kind.", + "type": "string_t" + }, "mana": { "caption": "Mana", "description": "Current mana, a measure of magical reserves.", diff --git a/test/test_ocsf_schema/extensions/rpg/events/eta_(patch).json b/test/test_ocsf_schema/extensions/rpg/events/eta_(patch).json new file mode 100644 index 0000000..a75b6d5 --- /dev/null +++ b/test/test_ocsf_schema/extensions/rpg/events/eta_(patch).json @@ -0,0 +1,13 @@ +{ + "extends": "eta", + "attributes": { + "service": { + "requirement": "recommended", + "observable": 42103 + } + }, + "observables": { + "name": 42104, + "service.name": 42105 + } +} diff --git a/test/test_ocsf_schema/extensions/rpg/objects/zeta_(patch).json b/test/test_ocsf_schema/extensions/rpg/objects/zeta_(patch).json new file mode 100644 index 0000000..5728bd8 --- /dev/null +++ b/test/test_ocsf_schema/extensions/rpg/objects/zeta_(patch).json @@ -0,0 +1,20 @@ +{ + "caption": "RPG Zeta", + "description": "Patch extends of Zeta for the RPG extension.", + "extends": "zeta", + "observable": 42202, + "attributes": { + "name": { + "description": "Patched name.", + "observable": 42203 + }, + "numeric_value": { + "description": "Patched numeric value.", + "observable": 42204 + }, + "kind": { + "requirement": "recommended", + "observable": 42205 + } + } +} diff --git a/test/test_ocsf_schema/objects/_zeta_base_(hidden).json b/test/test_ocsf_schema/objects/_zeta_base_(hidden).json new file mode 100644 index 0000000..a59bef3 --- /dev/null +++ b/test/test_ocsf_schema/objects/_zeta_base_(hidden).json @@ -0,0 +1,14 @@ +{ + "caption": "Zeta Base", + "description": "A zeta base.", + "name": "_zeta_base", + "attributes": { + "name": { + "description": "The zeta base's human-friendly name", + "requirement": "recommended" + }, + "numeric_value": { + "requirement": "required" + } + } +} diff --git a/test/test_ocsf_schema/objects/network_node.json b/test/test_ocsf_schema/objects/network_node.json index 488ad0e..85a1839 100644 --- a/test/test_ocsf_schema/objects/network_node.json +++ b/test/test_ocsf_schema/objects/network_node.json @@ -3,7 +3,7 @@ "description": "The Network Node object represents a computing device on a network. This is simplified view; the actual OCSF Schema uses far more elaborate model.", "name": "network_node", "extends": "_entity", - "observable": 204, + "observable": 203, "attributes": { "ip": { "requirement": "required" diff --git a/test/test_ocsf_schema/objects/service.json b/test/test_ocsf_schema/objects/service.json index 7ab0240..e8489cd 100644 --- a/test/test_ocsf_schema/objects/service.json +++ b/test/test_ocsf_schema/objects/service.json @@ -1,6 +1,6 @@ { "caption": "Service", - "description": "A network service.", + "description": "A service.", "name": "service", "attributes": { "name": { diff --git a/test/test_ocsf_schema/objects/zeta.json b/test/test_ocsf_schema/objects/zeta.json new file mode 100644 index 0000000..c847eac --- /dev/null +++ b/test/test_ocsf_schema/objects/zeta.json @@ -0,0 +1,15 @@ +{ + "name": "zeta", + "extends": "_zeta_base", + "observable": 204, + "attributes": { + "name": { + "description": "The zeta's human-friendly name", + "requirement": "recommended" + }, + "numeric_value": { + "requirement": "required", + "observable": 205 + } + } +}