Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for defining observables in "patch extends" #83

Merged
merged 1 commit into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 78 additions & 87 deletions lib/schema/cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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],
Expand All @@ -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]}\""
)

Expand All @@ -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
Expand All @@ -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],
Expand All @@ -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]}\""
)

Expand All @@ -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
Expand All @@ -580,18 +555,19 @@ 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,
fn {object_key, object}, observable_type_id_map ->
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
Expand Down Expand Up @@ -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]}\""
)

Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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, "_")
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion lib/schema/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 10 additions & 1 deletion test/test_ocsf_schema/dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -169,7 +174,7 @@
},
"service": {
"caption": "Service",
"description": "A network service.",
"description": "A service.",
"type": "service"
},
"source_node": {
Expand Down Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion test/test_ocsf_schema/events/alpha.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "The Alpha example event class.",
"name": "alpha",
"extends": "ghost",
"uid": 2,
"uid": 1,
"profiles": [],
"attributes": {
"alpha": {
Expand Down
2 changes: 1 addition & 1 deletion test/test_ocsf_schema/events/beta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "The Beta example event class.",
"name": "beta",
"extends": "ghost",
"uid": 3,
"uid": 2,
"profiles": [],
"attributes": {
"beta": {
Expand Down
22 changes: 22 additions & 0 deletions test/test_ocsf_schema/events/eta.json
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Loading