app/models/foreman_openscap/arf_report.rb
require 'foreman_openscap/helper'
module ForemanOpenscap
class ArfReport < ::Report
include Taxonomix
include OpenscapProxyExtensions
# attr_accessible :host_id, :reported_at, :status, :metrics
METRIC = %w[passed othered failed].freeze
BIT_NUM = 10
MAX = (1 << BIT_NUM) - 1
include ComplianceStatusScopedSearch
scoped_search :on => :status, :offset => 0, :word_size => 4 * BIT_NUM, :complete_value => { :true => true, :false => false }, :rename => :eventful
has_one :policy_arf_report
has_one :policy, :through => :policy_arf_report, :dependent => :destroy
has_one :asset, :through => :host, :class_name => 'ForemanOpenscap::Asset', :as => :assetable
has_one :log, :foreign_key => :report_id
belongs_to :openscap_proxy, :class_name => "SmartProxy"
after_save :assign_locations_organizations
before_destroy :destroy_from_proxy
delegate :asset=, :to => :host
default_scope do
with_taxonomy_scope do
order("#{self.table_name}.created_at DESC")
end
end
scope :hosts, lambda { includes(:policy) }
scope :of_policy, lambda { |policy_id| joins(:policy_arf_report).merge(PolicyArfReport.of_policy(policy_id)) }
scope :latest, -> {
joins('INNER JOIN (SELECT host_id, policy_id, max(reports.id) AS id
FROM reports INNER JOIN foreman_openscap_policy_arf_reports
ON reports.id = foreman_openscap_policy_arf_reports.arf_report_id
GROUP BY host_id, policy_id) latest
ON reports.id = latest.id')
}
scope :latest_of_policy, lambda { |policy|
joins("INNER JOIN (SELECT host_id, policy_id, max(reports.id) AS id
FROM reports INNER JOIN foreman_openscap_policy_arf_reports
ON reports.id = foreman_openscap_policy_arf_reports.arf_report_id
WHERE policy_id = #{policy.id}
GROUP BY host_id, policy_id) latest
ON reports.id = latest.id")
}
scope :failed, lambda { where("(#{report_status_column} >> #{bit_mask 'failed'}) > 0") }
scope :not_failed, lambda { where("(#{report_status_column} >> #{bit_mask 'failed'}) = 0") }
scope :othered, lambda { where("(#{report_status_column} >> #{bit_mask 'othered'}) > 0").merge(not_failed) }
scope :not_othered, lambda { where("(#{report_status_column} >> #{bit_mask 'othered'}) = 0") }
scope :passed, lambda { where("(#{report_status_column} >> #{bit_mask 'passed'}) > 0").merge(not_failed).merge(not_othered) }
scope :by_rule_result, lambda { |rule_name, rule_result| joins(:sources).where(:sources => { :value => rule_name }, :logs => { :result => rule_result }) }
def self.bit_mask(status)
ComplianceStatus.bit_mask(status)
end
def self.report_status_column
"status"
end
def status=(st)
s = case st
when Integer
st
when Hash, ActionController::Parameters
ArfReportStatusCalculator.new(:counters => st).calculate
else
raise "Unsupported report status format #{st.class}"
end
write_attribute(:status, s)
end
delegate :status, :status_of, :to => :calculator
delegate(*METRIC, :to => :calculator)
def calculator
ArfReportStatusCalculator.new(:bit_field => read_attribute(self.class.report_status_column))
end
def passed
status_of "passed"
end
def failed
status_of "failed"
end
def othered
status_of "othered"
end
def rules_count
status.values.sum
end
def self.create_arf(asset, proxy, params)
arf_report = nil
policy = Policy.find_by :id => params[:policy_id]
return unless policy
ArfReport.transaction do
arf_report = ArfReport.create(:host => asset.host,
:reported_at => Time.at(params[:date].to_i),
:status => params[:metrics],
:metrics => params[:metrics],
:openscap_proxy => proxy)
return arf_report unless arf_report.persisted?
PolicyArfReport.where(:arf_report_id => arf_report.id, :policy_id => policy.id, :digest => params[:digest]).first_or_create!
if params[:logs]
params[:logs].each do |log|
src = Source.find_or_create_by(value: log[:source])
msg = nil
if src.logs.count > 0
msg = Log.where(:source_id => src.id).order(:id => :desc).first.message
update_msg_with_changes(msg, log)
else
if (msg = Message.find_by(:value => log[:title]))
msg.attributes = {
:value => N_(log[:title]),
:severity => log[:severity],
:description => newline_to_space(log[:description]),
:rationale => newline_to_space(log[:rationale]),
:scap_references => references_links(log[:references]),
:fixes => fixes(log[:fixes])
}
else
msg = Message.new(:value => N_(log[:title]),
:severity => log[:severity],
:description => newline_to_space(log[:description]),
:rationale => newline_to_space(log[:rationale]),
:scap_references => references_links(log[:references]),
:fixes => fixes(log[:fixes]))
end
msg.save!
end
# TODO: log level
Log.create!(:source_id => src.id,
:message_id => msg.id,
:level => :info,
:result => log[:result],
:report => arf_report)
end
end
end
arf_report
end
def assign_locations_organizations
if host
self.location_ids = [host.location_id]
self.organization_ids = [host.organization_id]
end
end
def failed?
failed > 0
end
def passed?
passed > 0 && failed == 0 && othered == 0
end
def othered?
!passed? && !failed?
end
def to_html
openscap_proxy_api.arf_report_html(self, ForemanOpenscap::Helper::find_name_or_uuid_by_host(host))
end
def to_bzip
openscap_proxy_api.arf_report_bzip(self, ForemanOpenscap::Helper::find_name_or_uuid_by_host(host))
end
def equal?(other)
results = [logs, other.logs].flatten.group_by(&:source_id).values
# for each rule, there should be one result from both reports
return false unless results.map(&:length).all? { |item| item == 2 }
results.all? { |result| result.first.source_id == result.last.source_id } &&
host_id == other.host_id &&
policy.id == other.policy.id
end
def destroy_from_proxy
if !host
destroy_from_proxy_warning "host"
elsif !policy
destroy_from_proxy_warning "policy"
elsif !openscap_proxy
destroy_from_proxy_warning "OpenSCAP proxy"
else
openscap_proxy_api.destroy_report(self, ForemanOpenscap::Helper::find_name_or_uuid_by_host(host))
end
end
def destroy_from_proxy_warning(associated)
logger.warn "Skipping deletion of report with id #{id} from proxy, no #{associated} associated with report"
true
end
def self.newline_to_space(string)
string.gsub(/ *\n+/, " ")
end
def self.references_links(references)
return if references.nil?
html_links = []
references.each do |reference|
next if reference['title'] == 'test_attestation' # A blank url created by OpenSCAP
reference['html_link'] = "<a href='#{reference['href']}'>#{reference['href']}</a>" if reference['title'].blank?
html_links << reference['html_link']
end
html_links.join(', ')
end
def self.fixes(raw_fixes)
return if raw_fixes.empty?
JSON.fast_generate(raw_fixes)
end
def self.update_msg_with_changes(msg, incoming_data)
msg.severity = incoming_data['severity']
msg.description = incoming_data['description']
msg.rationale = incoming_data['rationale']
msg.scap_references = incoming_data['references']
msg.value = incoming_data['title']
msg.fixes = fixes(incoming_data['fixes'])
return unless msg.changed?
msg.save
end
end
end