diff --git a/Dockerfile b/Dockerfile
index 7e707c4..c430357 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-ARG elixir_image=elixir:1.17.2-alpine
+ARG elixir_image=elixir:1.17.3-alpine
FROM ${elixir_image} AS builder
diff --git a/lib/schema/cache.ex b/lib/schema/cache.ex
index 52a10c2..0e939fb 100644
--- a/lib/schema/cache.ex
+++ b/lib/schema/cache.ex
@@ -1013,6 +1013,7 @@ defmodule Schema.Cache do
# Top-level path-based observables.
# Only occurs in classes, but is safe to do for objects too.
|> Utils.put_non_nil(:observables, item[:observables])
+ |> Utils.put_non_nil(:references, item[:references])
|> patch_constraints(item)
Map.put(acc, base_key, patched_base)
@@ -1366,6 +1367,8 @@ defmodule Schema.Cache do
|> copy_new(from, :object_name)
|> copy_new(from, :object_type)
|> copy_new(from, :observable)
+ |> copy_new(from, :source)
+ |> copy_new(from, :references)
end
defp copy_new(to, from, key) do
diff --git a/lib/schema_web/templates/page/class.html.eex b/lib/schema_web/templates/page/class.html.eex
index 96d6aa8..77d4aab 100644
--- a/lib/schema_web/templates/page/class.html.eex
+++ b/lib/schema_web/templates/page/class.html.eex
@@ -19,6 +19,7 @@ limitations under the License.
<% category_name = @data[:category_name] %>
<% extension = @data[:extension] %>
<% observables = @data[:observables] %>
+<% references = @data[:references] %>
@@ -45,6 +46,12 @@ limitations under the License.
<%= if observables != nil and !Enum.empty?(observables) do %>Class-specific attribute path observables
are
at the bottom of this page .<% end %>
+ <%= if references != nil and !Enum.empty?(references) do %>
+
+ References
+ <%= raw Enum.map(references, fn ref -> [" ", reference_anchor(ref)] end) %>
+
+ <% end %>
diff --git a/lib/schema_web/templates/page/object.html.eex b/lib/schema_web/templates/page/object.html.eex
index 1de018f..c1885d4 100644
--- a/lib/schema_web/templates/page/object.html.eex
+++ b/lib/schema_web/templates/page/object.html.eex
@@ -14,6 +14,8 @@
show("#json-schema");
+<% references = @data[:references] %>
+
@@ -45,6 +47,12 @@
Note: a superscript "O" after a caption indicates attribute is an observable,
for example: Device O .
+ <%= if references != nil and !Enum.empty?(references) do %>
+
+ References
+ <%= raw Enum.map(references, fn ref -> [" ", reference_anchor(ref)] end) %>
+
+ <% end %>
diff --git a/lib/schema_web/views/page_view.ex b/lib/schema_web/views/page_view.ex
index e2e2b68..132f89b 100644
--- a/lib/schema_web/views/page_view.ex
+++ b/lib/schema_web/views/page_view.ex
@@ -470,11 +470,41 @@ defmodule SchemaWeb.PageView do
@spec format_desc(String.t() | atom(), map()) :: any
def format_desc(key, obj) do
+ source = obj[:source]
+ references = obj[:references]
+
+ source_html =
+ if source != nil do
+ # source can have embedded markup
+ ["
Source ", source]
+ else
+ ""
+ end
+
+ refs_html =
+ if references != nil and !Enum.empty?(references) do
+ [
+ " References",
+ Enum.map(references, fn ref -> [" ", reference_anchor(ref)] end)
+ ]
+ else
+ ""
+ end
+
+ if source_html != "" or refs_html != "" do
+ [base_format_desc(key, obj), "
", source_html, refs_html, " "]
+ else
+ base_format_desc(key, obj)
+ end
+ end
+
+ @spec base_format_desc(String.t() | atom(), map()) :: any
+ defp base_format_desc(key, obj) do
description = description(obj)
case Map.get(obj, :enum) do
nil ->
- description
+ [description]
values ->
sorted =
@@ -1162,4 +1192,16 @@ defmodule SchemaWeb.PageView do
Map.get(deprecated, :message)
]
end
+
+ @spec reference_anchor(map()) :: any()
+ def reference_anchor(reference) do
+ # The url and description attributes of reference are not meant to have markup
+ [
+ "",
+ reference[:description] |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string(),
+ " "
+ ]
+ end
end
diff --git a/mix.exs b/mix.exs
index cc5a550..c9ce7e1 100644
--- a/mix.exs
+++ b/mix.exs
@@ -10,7 +10,7 @@
defmodule Schema.MixProject do
use Mix.Project
- @version "2.74.0"
+ @version "2.75.0"
def project do
build = System.get_env("GITHUB_RUN_NUMBER") || "SNAPSHOT"
diff --git a/mix.lock b/mix.lock
index 5c66588..a43df30 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,10 +1,10 @@
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
- "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
+ "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
- "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
+ "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"},
"elixir_uuid": {:hex, :uuid_utils, "1.6.5", "bafd6ffcbec895513a7c10855df3954f29909fb5d05ee52681e30e84297b1a80", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "36aaeee10740eae4d357231f48571a2687cb541730f94f47cbd3f186dc07899c"},
diff --git a/test/test_ocsf_schema/dictionary.json b/test/test_ocsf_schema/dictionary.json
index 7dc4866..f7cd588 100644
--- a/test/test_ocsf_schema/dictionary.json
+++ b/test/test_ocsf_schema/dictionary.json
@@ -3,7 +3,7 @@
"description": "The Attribute Dictionary defines attributes and includes references to the events and objects in which they are used.",
"name": "dictionary",
"attributes": {
- "activity_id": {
+ "activity_id": {
"caption": "Activity ID",
"description": "The normalized identifier of the activity that triggered the event.",
"enum": {
@@ -17,12 +17,28 @@
}
},
"sibling": "activity_name",
- "type": "integer_t"
+ "type": "integer_t",
+ "references": [
+ {
+ "url": "https://example.com/activity_id",
+ "description": "Activity ID at example.com "
+ }
+ ]
},
"activity_name": {
"caption": "Activity",
"description": "The event activity name, as defined by the activity_id.",
- "type": "string_t"
+ "type": "string_t",
+ "references": [
+ {
+ "url": "https://example.com/activity_name?s=1",
+ "description": "Activity Name (1) at example.com "
+ },
+ {
+ "url": "https://example.com/activity_name?s=2",
+ "description": "Activity Name (2) at example.com "
+ }
+ ]
},
"alpha": {
"caption": "Alpha",
@@ -32,7 +48,8 @@
"beta": {
"caption": "Beta",
"description": "The beta. Whatever that is. This is for testing. Sheesh.",
- "type": "string_t"
+ "type": "string_t",
+ "source": "Beta is from atomic decay (dictionary attribute)"
},
"category_name": {
"caption": "Category",
@@ -85,7 +102,14 @@
"device": {
"caption": "Device",
"description": "An addressable device, computer system or host.",
- "type": "device"
+ "type": "device",
+ "source": "Devices are sourced from suppliers.",
+ "references": [
+ {
+ "url": "https://example.com/device",
+ "description": "Device at example.com "
+ }
+ ]
},
"entity_thing": {
"caption": "Entity Thing",
diff --git a/test/test_ocsf_schema/events/alpha.json b/test/test_ocsf_schema/events/alpha.json
index 29cab2e..c88f7aa 100644
--- a/test/test_ocsf_schema/events/alpha.json
+++ b/test/test_ocsf_schema/events/alpha.json
@@ -6,6 +6,12 @@
"extends": "ghost",
"uid": 1,
"profiles": [],
+ "references": [
+ {
+ "url": "https://example.com/alpha",
+ "description": "Alpha at example.com "
+ }
+ ],
"attributes": {
"alpha": {
"requirement": "required",
@@ -13,7 +19,14 @@
},
"delta": {
"requirement": "optional",
- "observable": 101
+ "observable": 101,
+ "source": "This is from physics! (class attribute)",
+ "references": [
+ {
+ "url": "https://example.com/delta",
+ "description": "Delta at example.com "
+ }
+ ]
},
"ob_by_dict_type_1": {
"requirement": "optional",
diff --git a/test/test_ocsf_schema/events/beta.json b/test/test_ocsf_schema/events/beta.json
index 72b150b..07566ae 100644
--- a/test/test_ocsf_schema/events/beta.json
+++ b/test/test_ocsf_schema/events/beta.json
@@ -6,6 +6,10 @@
"extends": "ghost",
"uid": 2,
"profiles": [],
+ "references": [
+ {"url": "https://example.com/beta?n=1", "description": "Beta (1) on example.com "},
+ {"url": "https://example.com/beta?n=2", "description": "Beta (2) on example.com "}
+ ],
"attributes": {
"beta": {
"requirement": "required",
diff --git a/test/test_ocsf_schema/extensions/rpg/events/alpha_(patch).json b/test/test_ocsf_schema/extensions/rpg/events/alpha_(patch).json
index ba9c11d..2959781 100644
--- a/test/test_ocsf_schema/extensions/rpg/events/alpha_(patch).json
+++ b/test/test_ocsf_schema/extensions/rpg/events/alpha_(patch).json
@@ -3,6 +3,12 @@
"description": "Patch extends of the Alpha example event class.",
"extends": "alpha",
"profiles": [],
+ "references": [
+ {
+ "url": "https://example.com/alpha?patched=1",
+ "description": "Alpha (patch) at example.com "
+ }
+ ],
"attributes": {
"alpha_extra": {
"requirement": "required"
diff --git a/test/test_ocsf_schema/objects/device.json b/test/test_ocsf_schema/objects/device.json
index 671937a..3114154 100644
--- a/test/test_ocsf_schema/objects/device.json
+++ b/test/test_ocsf_schema/objects/device.json
@@ -4,6 +4,12 @@
"extends": "endpoint",
"name": "device",
"observable": 200,
+ "references": [
+ {
+ "url": "https://example.com/device",
+ "description": "Device at example.com "
+ }
+ ],
"attributes": {
"desc": {
"caption": "Description",
@@ -12,7 +18,13 @@
},
"hostname": {
"description": "The device hostname.",
- "requirement": "recommended"
+ "requirement": "recommended",
+ "references": [
+ {
+ "url": "https://example.com/device_hostname",
+ "description": "Device hostname at example.com "
+ }
+ ]
},
"ip": {
"description": "The device IP address, in either IPv4 or IPv6 format.",
diff --git a/test/test_ocsf_schema/profiles/host.json b/test/test_ocsf_schema/profiles/host.json
index d6c86ad..f0c0239 100644
--- a/test/test_ocsf_schema/profiles/host.json
+++ b/test/test_ocsf_schema/profiles/host.json
@@ -8,7 +8,13 @@
},
"attributes": {
"device": {
- "requirement": "recommended"
+ "requirement": "recommended",
+ "references": [
+ {
+ "url": "https://example.com/device?profile=host",
+ "description": "Device from host profile on example.com "
+ }
+ ]
}
}
}
\ No newline at end of file