diff --git a/app/models/pager_tree/integrations/hetrix_tools/v3.rb b/app/models/pager_tree/integrations/hetrix_tools/v3.rb new file mode 100644 index 0000000..d39c2f1 --- /dev/null +++ b/app/models/pager_tree/integrations/hetrix_tools/v3.rb @@ -0,0 +1,160 @@ +module PagerTree::Integrations + class HetrixTools::V3 < Integration + OPTIONS = [ + {key: :authentication_token, type: :string, default: nil} + ] + store_accessor :options, *OPTIONS.map { |x| x[:key] }.map(&:to_s), prefix: "option" + + after_initialize do + self.option_authentication_token ||= nil + end + + def adapter_should_block_incoming?(request) + self.option_authentication_token.present? && (request.headers["Authorization"] != "Bearer #{self.option_authentication_token}") + end + + def adapter_supports_incoming? + true + end + + def adapter_supports_outgoing? + false + end + + def adapter_incoming_can_defer? + true + end + + def adapter_thirdparty_id + try("_adapter_thirdparty_id_#{_webhook_type}") || SecureRandom.hex(16) + end + + def adapter_action + try("_adapter_action_#{_webhook_type}") || :other + end + + def _adapter_action_uptime + case adapter_incoming_request_params.dig("monitor_status") + when "online" then :resolve + when "offline" then :create + else + :other + end + end + + def _adapter_action_blacklist + :create + end + + def _adapter_action_resource_usage + :create + end + + def adapter_process_create + Alert.new( + title: _title, + description: _description, + thirdparty_id: adapter_thirdparty_id, + dedup_keys: [], + additional_data: _additional_datums + ) + end + + def _adapter_thirdparty_id_uptime + adapter_incoming_request_params.dig("monitor_id") + end + + def _adapter_thirdparty_id_blacklist + SecureRandom.hex(16) + end + + def _adapter_thirdparty_id_resource_usage + adapter_incoming_request_params.dig("monitor_id") + end + + def _webhook_type + @_webhook_type ||= if _webhook_type_uptime? + :uptime + elsif _webhook_type_resource_usage? + :resource_usage + elsif _webhook_type_blacklist? + :blacklist + else + :unknown + end + end + + def _webhook_type_uptime? + adapter_incoming_request_params.dig("monitor_errors").present? + end + + def _webhook_type_blacklist? + json = adapter_incoming_request_params.dig("_json") + json.present? && json.is_a?(Array) + end + + def _webhook_type_resource_usage? + adapter_incoming_request_params.dig("resource_usage").present? + end + + def _title + try("_title_#{_webhook_type}") || "HetrixTools Alert" + end + + def _title_uptime + "#{adapter_incoming_request_params.dig("monitor_name")} is #{adapter_incoming_request_params.dig("monitor_status")}" + end + + def _title_blacklist + "Blacklist Alert" + end + + def _title_resource_usage + "#{adapter_incoming_request_params.dig("monitor_name")} usage alert" + end + + def _description + try("_description_#{_webhook_type}") || "No description provided" + end + + def _description_uptime + "

#{adapter_incoming_request_params.dig("monitor_target")} is #{adapter_incoming_request_params.dig("monitor_status")}

" + + adapter_incoming_request_params.dig("monitor_errors").map { |k, v| "

#{k}: #{v}

" }.join("") + end + + def _description_blacklist + adapter_incoming_request_params.dig("_json").map { |x| "#{x["monitor"]} (#{x["blacklisted_now"]})" }.join("
") + end + + def _description_resource_usage + [ + "

Resource Type: #{adapter_incoming_request_params.dig("resource_usage", "resource_type")}

", + "

Current Usage: #{adapter_incoming_request_params.dig("resource_usage", "current_usage")}

", + "

Average Usage: #{adapter_incoming_request_params.dig("resource_usage", "average_usage")} / #{adapter_incoming_request_params.dig("resource_usage", "average_minutes")}m

" + ].join("") + end + + def _additional_datums + try("_additional_datums_#{_webhook_type}") || [] + end + + def _additional_datums_uptime + [ + AdditionalDatum.new(format: "datetime", label: "Timestamp", value: Time.at(adapter_incoming_request_params.dig("timestamp"))), + AdditionalDatum.new(format: "text", label: "Monitor Type", value: adapter_incoming_request_params.dig("monitor_type")), + AdditionalDatum.new(format: "link", label: "Monitor Target", value: adapter_incoming_request_params.dig("monitor_target")) + ] + end + + def _additional_datums_blacklist + [] + end + + def _additional_datums_resource_usage + [ + AdditionalDatum.new(format: "text", label: "Resource Type", value: adapter_incoming_request_params.dig("resource_usage", "resource_type")), + AdditionalDatum.new(format: "text", label: "Current Usage", value: adapter_incoming_request_params.dig("resource_usage", "current_usage")) + ] + end + end +end diff --git a/app/views/pager_tree/integrations/hetrix_tools/v3/_form_options.html.erb b/app/views/pager_tree/integrations/hetrix_tools/v3/_form_options.html.erb new file mode 100644 index 0000000..8176cd0 --- /dev/null +++ b/app/views/pager_tree/integrations/hetrix_tools/v3/_form_options.html.erb @@ -0,0 +1,7 @@ +
+
+ <%= form.label :option_authentication_token %> + <%= form.text_field :option_authentication_token, class: "form-control" %> +

<%== t(".option_authentication_token_hint_html") %>

+
+
\ No newline at end of file diff --git a/app/views/pager_tree/integrations/hetrix_tools/v3/_show_options.html.erb b/app/views/pager_tree/integrations/hetrix_tools/v3/_show_options.html.erb new file mode 100644 index 0000000..054fe34 --- /dev/null +++ b/app/views/pager_tree/integrations/hetrix_tools/v3/_show_options.html.erb @@ -0,0 +1,12 @@ +
+
+ <%= t("activerecord.attributes.pager_tree/integrations/hetrix_tools/v3.option_authentication_token") %> +
+
+
+

+ <%= mask integration.option_authentication_token %> +

+
+
+
diff --git a/config/locales/en.yml b/config/locales/en.yml index 8f4e1bf..8e1bc66 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -46,6 +46,10 @@ en: option_form_phone_required_hint_html: "Should the phone field be required to submit?" option_form_description_required_hint_html: "Should the description field be required to submit?" option_form_urgency_required_hint_html: "Should the urgency field be required to submit?" + hetrix_tools: + v3: + form_options: + option_authentication_token_hint_html: "Authentication Token to be used to authenticate requests from Hetrix Tools servers (optional, see docs)" honeybadger: v3: form_options: @@ -161,6 +165,8 @@ en: option_form_phone_required: "Phone Required?" option_form_description_required: "Description Required?" option_form_urgency_required: "Urgency Required?" + "pager_tree/integrations/hetrix_tools/v3": + option_authentication_token: "Authentication Token" "pager_tree/integrations/honeybadger/v3": option_token: "Honeybadger Token" "pager_tree/integrations/jira_server/v3": diff --git a/test/fixtures/pager_tree/integrations/integrations.yml b/test/fixtures/pager_tree/integrations/integrations.yml index dacf80d..b67e93f 100644 --- a/test/fixtures/pager_tree/integrations/integrations.yml +++ b/test/fixtures/pager_tree/integrations/integrations.yml @@ -70,6 +70,10 @@ healthchecks_v3: type: "PagerTree::Integrations::Healthchecks::V3" # options: no_options +hetrix_tools_v3: + type: "PagerTree::Integrations::HetrixTools::V3" + # options: no_options + honeybadger_v3: type: "PagerTree::Integrations::Honeybadger::V3" options: diff --git a/test/models/pager_tree/integrations/hetrix_tools/v3_test.rb b/test/models/pager_tree/integrations/hetrix_tools/v3_test.rb new file mode 100644 index 0000000..cc2e910 --- /dev/null +++ b/test/models/pager_tree/integrations/hetrix_tools/v3_test.rb @@ -0,0 +1,215 @@ +require "test_helper" + +module PagerTree::Integrations + class HetrixTools::V3Test < ActiveSupport::TestCase + include Integrateable + + setup do + @integration = pager_tree_integrations_integrations(:hetrix_tools_v3) + + # https://docs.hetrixtools.com/uptime-monitoring-webhook-notifications/ + @uptime_request = { + monitor_id: "ThisWillBeTheMonitorID32CharLong", + monitor_name: "Test Monitor Label", + monitor_target: "http:\/\/this-is-a-test.com\/", + monitor_type: "website", + monitor_category: "Test Category", + monitor_status: "offline", + timestamp: 1499666192, + monitor_errors: { + "New York": "http code 403", + "San Francisco": "http code 403", + Dallas: "timeout", + Amsterdam: "http code 403", + London: "http code 403", + Frankfurt: "http code 403", + Singapore: "timeout", + Sydney: "http code 403", + "Sao Paulo": "http code 403", + Tokyo: "keyword not found", + Mumbai: "http code 403", + Moscow: "keyword not found" + } + }.with_indifferent_access + + # https://docs.hetrixtools.com/blacklist-monitoring-webhook-notifications/ + @blacklist_request = {_json: [ + { + monitor: "98.88.89.102", + label: "some label", + blacklisted_before: "7", + blacklisted_now: "6", + blacklisted_on: [ + { + rbl: "bl.nszones.com", + delist: "http://www.nszones.com/contact.shtml" + }, + { + rbl: "bl.score.senderscore.com", + delist: "https://www.senderscore.org/blacklistlookup/" + }, + { + rbl: "cidr.bl.mcafee.com", + delist: "https://kc.mcafee.com/corporate/index?page=content&id=KB53783" + }, + { + rbl: "dyn.nszones.com", + delist: "http://db.nszones.com/dyn.ip?98.88.89.102" + }, + { + rbl: "pbl.spamhaus.org", + delist: "https://www.spamhaus.org/query/ip/98.88.89.102" + }, + { + rbl: "zen.spamhaus.org", + delist: "https://www.spamhaus.org/query/ip/98.88.89.102" + } + ], + links: { + report_link: "https://hetrixtools.com/report/blacklist/c855b5712bd63a3c8153690b56d5385e/", + whitelabel_report_link: "http://status.hetrixtools.com/report/blacklist/c855b5712bd63a3c8153690b56d5385e/" + } + }, + { + monitor: "190.129.206.24", + label: "another label", + blacklisted_before: "2", + blacklisted_now: "0", + blacklisted_on: nil, + links: { + report_link: "https://hetrixtools.com/report/blacklist/4255d1931a5c5547a0fce88e6cdff008/", + whitelabel_report_link: "http://status.hetrixtools.com/report/blacklist/4255d1931a5c5547a0fce88e6cdff008/" + } + } + ]}.with_indifferent_access + + @resource_usage_request = { + monitor_id: "ThisWillBeTheMonitorID32CharLong", + monitor_name: "Test Monitor Label", + timestamp: 1499666613, + resource_usage: { + resource_type: "cpu", + current_usage: "24.60", + average_usage: "25.29", + average_minutes: "3" + } + }.with_indifferent_access + + @resolve_request = @uptime_request.deep_dup + @resolve_request["monitor_status"] = "online" + end + + test "sanity" do + assert @integration.adapter_supports_incoming? + assert @integration.adapter_incoming_can_defer? + assert_not @integration.adapter_supports_outgoing? + assert @integration.adapter_show_alerts? + assert @integration.adapter_show_logs? + assert_not @integration.adapter_show_outgoing_webhook_delivery? + end + + test "adapter_action_uptime_create" do + @integration.adapter_incoming_request_params = @uptime_request + assert_equal :create, @integration.adapter_action + end + + test "adapter_action_uptime_resolve" do + @integration.adapter_incoming_request_params = @resolve_request + assert_equal :resolve, @integration.adapter_action + end + + test "adapter_action_blacklist_create" do + @integration.adapter_incoming_request_params = @blacklist_request + assert_equal :create, @integration.adapter_action + end + + test "adapter_action_resource_usage_create" do + @integration.adapter_incoming_request_params = @resource_usage_request + assert_equal :create, @integration.adapter_action + end + + test "adapter_thirdparty_id_uptime" do + @integration.adapter_incoming_request_params = @uptime_request + assert_equal @uptime_request.dig("monitor_id"), @integration.adapter_thirdparty_id + end + + test "adapter_thirdparty_id_blacklist" do + @integration.adapter_incoming_request_params = @blacklist_request + assert_equal 32, @integration.adapter_thirdparty_id.length # each hex is 2 chars + end + + test "adapter_thirdparty_id_resource_usage" do + @integration.adapter_incoming_request_params = @resource_usage_request + assert_equal 32, @integration.adapter_thirdparty_id.length # each hex is 2 chars + end + + test "adapter_process_create_uptime" do + @integration.adapter_incoming_request_params = @uptime_request + + true_alert = Alert.new( + title: "#{@uptime_request.dig("monitor_name")} is #{@uptime_request.dig("monitor_status")}", + description: "

#{@uptime_request.dig("monitor_target")} is #{@uptime_request.dig("monitor_status")}

" + @uptime_request.dig("monitor_errors").map { |k, v| "

#{k}: #{v}

" }.join(""), + urgency: nil, + thirdparty_id: @uptime_request.dig("monitor_id"), + dedup_keys: [], + additional_data: [ + AdditionalDatum.new(format: "datetime", label: "Timestamp", value: Time.at(@uptime_request.dig("timestamp"))), + AdditionalDatum.new(format: "text", label: "Monitor Type", value: @uptime_request.dig("monitor_type")), + AdditionalDatum.new(format: "link", label: "Monitor Target", value: @uptime_request.dig("monitor_target")) + ] + ) + + assert_equal true_alert.to_json, @integration.adapter_process_create.to_json + end + + test "adapter_process_create_blacklist" do + @integration.adapter_incoming_request_params = @blacklist_request + + true_alert = Alert.new( + title: "Blacklist Alert", + description: @blacklist_request.dig("_json").map { |x| "#{x["monitor"]} (#{x["blacklisted_now"]})" }.join("
"), + urgency: nil, + thirdparty_id: nil, + dedup_keys: [], + additional_data: [] + ) + + actual = @integration.adapter_process_create + actual.thirdparty_id = nil + assert_equal true_alert.to_json, actual.to_json + end + + test "adapter_process_create_resource_usage" do + @integration.adapter_incoming_request_params = @resource_usage_request + + true_alert = Alert.new( + title: "#{@resource_usage_request.dig("monitor_name")} usage alert", + description: [ + "

Resource Type: #{@resource_usage_request.dig("resource_usage", "resource_type")}

", + "

Current Usage: #{@resource_usage_request.dig("resource_usage", "current_usage")}

", + "

Average Usage: #{@resource_usage_request.dig("resource_usage", "average_usage")} / #{@resource_usage_request.dig("resource_usage", "average_minutes")}m

" + ].join(""), + urgency: nil, + thirdparty_id: nil, + dedup_keys: [], + additional_data: [ + AdditionalDatum.new(format: "text", label: "Resource Type", value: @resource_usage_request.dig("resource_usage", "resource_type")), + AdditionalDatum.new(format: "text", label: "Current Usage", value: @resource_usage_request.dig("resource_usage", "current_usage")) + ] + ) + + actual = @integration.adapter_process_create + actual.thirdparty_id = nil + assert_equal true_alert.to_json, actual.to_json + end + + test "blocking_incoming" do + assert @integration.option_authentication_token.blank? + assert_not @integration.adapter_should_block_incoming?(OpenStruct.new({headers: {"Authorization" => ""}})) + + @integration.option_authentication_token = "abc123456" + assert @integration.adapter_should_block_incoming?(OpenStruct.new({headers: {"Authorization" => ""}})) + assert_not @integration.adapter_should_block_incoming?(OpenStruct.new({headers: {"Authorization" => "Bearer abc123456"}})) + end + end +end