123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- require 'ostruct'
- # This is a helper class for managing Scenario imports, used by the ScenarioImportsController. This class behaves much
- # like a normal ActiveRecord object, with validations and callbacks. However, it is never persisted to the database.
- class ScenarioImport
- include ActiveModel::Model
- include ActiveModel::Callbacks
- include ActiveModel::Validations::Callbacks
- DANGEROUS_AGENT_TYPES = %w[Agents::ShellCommandAgent]
- URL_REGEX = /\Ahttps?:\/\//i
- attr_accessor :file, :url, :data, :do_import, :merges
- attr_reader :user
- before_validation :parse_file
- before_validation :fetch_url
- validate :validate_presence_of_file_url_or_data
- validates_format_of :url, :with => URL_REGEX, :allow_nil => true, :allow_blank => true, :message => "appears to be invalid"
- validate :validate_data
- validate :generate_diff
- def step_one?
- data.blank?
- end
- def step_two?
- data.present?
- end
- def set_user(user)
- @user = user
- end
- def existing_scenario
- @existing_scenario ||= user.scenarios.find_by(:guid => parsed_data["guid"])
- end
- def dangerous?
- (parsed_data['agents'] || []).any? { |agent| DANGEROUS_AGENT_TYPES.include?(agent['type']) }
- end
- def parsed_data
- @parsed_data ||= (data && JSON.parse(data) rescue {}) || {}
- end
- def agent_diffs
- @agent_diffs || generate_diff
- end
- def import_confirmed?
- do_import == "1"
- end
- def import(options = {})
- success = true
- guid = parsed_data['guid']
- description = parsed_data['description']
- name = parsed_data['name']
- links = parsed_data['links']
- control_links = parsed_data['control_links'] || []
- tag_fg_color = parsed_data['tag_fg_color']
- tag_bg_color = parsed_data['tag_bg_color']
- icon = parsed_data['icon']
- source_url = parsed_data['source_url'].presence || nil
- @scenario = user.scenarios.where(:guid => guid).first_or_initialize
- @scenario.update!(name: name, description: description,
- source_url: source_url, public: false,
- tag_fg_color: tag_fg_color,
- tag_bg_color: tag_bg_color,
- icon: icon)
- unless options[:skip_agents]
- created_agents = agent_diffs.map do |agent_diff|
- agent = agent_diff.agent || Agent.build_for_type("Agents::" + agent_diff.type.incoming, user)
- agent.guid = agent_diff.guid.incoming
- agent.attributes = { :name => agent_diff.name.updated,
- :disabled => agent_diff.disabled.updated, # == "true"
- :options => agent_diff.options.updated,
- :scenario_ids => [@scenario.id] }
- agent.schedule = agent_diff.schedule.updated if agent_diff.schedule.present?
- agent.keep_events_for = agent_diff.keep_events_for.updated if agent_diff.keep_events_for.present?
- agent.propagate_immediately = agent_diff.propagate_immediately.updated if agent_diff.propagate_immediately.present? # == "true"
- agent.service_id = agent_diff.service_id.updated if agent_diff.service_id.present?
- unless agent.save
- success = false
- errors.add(:base, "Errors when saving '#{agent_diff.name.incoming}': #{agent.errors.full_messages.to_sentence}")
- end
- agent
- end
- if success
- links.each do |link|
- receiver = created_agents[link['receiver']]
- source = created_agents[link['source']]
- receiver.sources << source unless receiver.sources.include?(source)
- end
- control_links.each do |control_link|
- controller = created_agents[control_link['controller']]
- control_target = created_agents[control_link['control_target']]
- controller.control_targets << control_target unless controller.control_targets.include?(control_target)
- end
- end
- end
- success
- end
- def scenario
- @scenario || @existing_scenario
- end
- protected
- def parse_file
- if data.blank? && file.present?
- self.data = file.read.force_encoding(Encoding::UTF_8)
- end
- end
- def fetch_url
- if data.blank? && url.present? && url =~ URL_REGEX
- self.data = Faraday.get(url).body
- end
- end
- def validate_data
- if data.present?
- @parsed_data = JSON.parse(data) rescue {}
- if (%w[name guid agents] - @parsed_data.keys).length > 0
- errors.add(:base, "The provided data does not appear to be a valid Scenario.")
- self.data = nil
- end
- else
- @parsed_data = nil
- end
- end
- def validate_presence_of_file_url_or_data
- unless file.present? || url.present? || data.present?
- errors.add(:base, "Please provide either a Scenario JSON File or a Public Scenario URL.")
- end
- end
- def generate_diff
- @agent_diffs = (parsed_data['agents'] || []).map.with_index do |agent_data, index|
- # AgentDiff is defined at the end of this file.
- agent_diff = AgentDiff.new(agent_data, parsed_data['schema_version'])
- if existing_scenario
- # If this Agent exists already, update the AgentDiff with the local version's information.
- agent_diff.diff_with! existing_scenario.agents.find_by(:guid => agent_data['guid'])
- begin
- # Update the AgentDiff with any hand-merged changes coming from the UI. This only happens when this
- # Agent already exists locally and has conflicting changes.
- agent_diff.update_from! merges[index.to_s] if merges
- rescue JSON::ParserError
- errors.add(:base, "Your updated options for '#{agent_data['name']}' were unparsable.")
- end
- end
- if agent_diff.requires_service? && merges.present? && merges[index.to_s].present? && merges[index.to_s]['service_id'].present?
- agent_diff.service_id = AgentDiff::FieldDiff.new(merges[index.to_s]['service_id'].to_i)
- end
- agent_diff
- end
- end
- # AgentDiff is a helper object that encapsulates an incoming Agent. All fields will be returned as an array
- # of either one or two values. The first value is the incoming value, the second is the existing value, if
- # it differs from the incoming value.
- class AgentDiff < OpenStruct
- class FieldDiff
- attr_accessor :incoming, :current, :updated
- def initialize(incoming)
- @incoming = incoming
- @updated = incoming
- end
- def set_current(current)
- @current = current
- @requires_merge = (incoming != current)
- end
- def requires_merge?
- @requires_merge
- end
- end
- def initialize(agent_data, schema_version)
- super()
- @schema_version = schema_version
- @requires_merge = false
- self.agent = nil
- store! agent_data
- end
- BASE_FIELDS = %w[name schedule keep_events_for propagate_immediately disabled guid]
- FIELDS_REQUIRING_TRANSLATION = %w[keep_events_for]
- def agent_exists?
- !!agent
- end
- def requires_merge?
- @requires_merge
- end
- def requires_service?
- !!agent_instance.try(:oauthable?)
- end
- def store!(agent_data)
- self.type = FieldDiff.new(agent_data["type"].split("::").pop)
- self.options = FieldDiff.new(agent_data['options'] || {})
- BASE_FIELDS.each do |option|
- if agent_data.has_key?(option)
- value = agent_data[option]
- value = send(:"translate_#{option}", value) if option.in?(FIELDS_REQUIRING_TRANSLATION)
- self[option] = FieldDiff.new(value)
- end
- end
- end
- def translate_keep_events_for(old_value)
- if schema_version < 1
- # Was stored in days, now is stored in seconds.
- old_value.to_i.days
- else
- old_value
- end
- end
- def schema_version
- (@schema_version || 0).to_i
- end
- def diff_with!(agent)
- return unless agent.present?
- self.agent = agent
- type.set_current(agent.short_type)
- options.set_current(agent.options || {})
- @requires_merge ||= type.requires_merge?
- @requires_merge ||= options.requires_merge?
- BASE_FIELDS.each do |field|
- next unless self[field].present?
- self[field].set_current(agent.send(field))
- @requires_merge ||= self[field].requires_merge?
- end
- end
- def update_from!(merges)
- each_field do |field, value, selection_options|
- value.updated = merges[field]
- end
- if options.requires_merge?
- options.updated = JSON.parse(merges['options'])
- end
- end
- def each_field
- boolean = [["True", "true"], ["False", "false"]]
- yield 'name', name if name.requires_merge?
- yield 'schedule', schedule, Agent::SCHEDULES.map {|s| [AgentHelper.builtin_schedule_name(s), s] } if self['schedule'].present? && schedule.requires_merge?
- yield 'keep_events_for', keep_events_for, Agent::EVENT_RETENTION_SCHEDULES if self['keep_events_for'].present? && keep_events_for.requires_merge?
- yield 'propagate_immediately', propagate_immediately, boolean if self['propagate_immediately'].present? && propagate_immediately.requires_merge?
- yield 'disabled', disabled, boolean if disabled.requires_merge?
- end
- def agent_instance
- "Agents::#{self.type.updated}".constantize.new
- end
- end
- end
|