diff --git a/docs/usage.md b/docs/usage.md index 82ca7c86..5a5c91fd 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -68,13 +68,14 @@ See [Database](./emitters/database.md) for detailed database configuration. $ mihari rule Commands: mihari rule delete ID # Delete a rule + mihari rule format PATH # format a rule mihari rule get ID # Get a rule mihari rule help [COMMAND] # Describe subcommands or one specific subcommand - mihari rule init PATH # Initialize a new rule file + mihari rule init PATH # Initialize a new rule mihari rule list QUERY # List/search rules mihari rule list-transform QUERY -t, --template=TEMPLATE # List/search rules with transformation mihari rule search PATH_OR_ID # Search by a rule - mihari rule validate PATH # Validate a rule file + mihari rule validate PATH # Validate rule(s) ``` ### `mihari search` diff --git a/lib/mihari/commands/rule.rb b/lib/mihari/commands/rule.rb index 85d38beb..d2047661 100644 --- a/lib/mihari/commands/rule.rb +++ b/lib/mihari/commands/rule.rb @@ -26,14 +26,37 @@ def _search(q, page: 1, limit: 10) end end - desc "validate PATH", "Validate a rule" + desc "validate PATH", "Validate rule(s)" # - # Validate format of a rule + # Validate rule(s) + # + # @param [Array] paths + # + def validate(*paths) + # @type [Array] + errors = paths.flat_map { |path| Dir.glob(path) }.map do |path| + Dry::Monads::Try[ValidationError] { Mihari::Rule.from_file(path) } + end.filter_map do |result| + result.exception if result.error? + end + return if errors.empty? + + errors.each do |error| + data = Entities::ErrorMessage.represent(message: error.message, detail: error.detail) + warn JSON.pretty_generate(data.as_json) + end + + exit 1 + end + + desc "format PATH", "format a rule" + # + # Format a rule file # # @param [String] path # - def validate(path) - rule = Dry::Monads::Try[ValidationError] { Mihari::Rule.from_yaml File.read(path) }.value! + def format(path) + rule = Dry::Monads::Try[ValidationError] { Mihari::Rule.from_file(path) }.value! puts rule.data.to_yaml end diff --git a/lib/mihari/rule.rb b/lib/mihari/rule.rb index 6da374c7..db0b748a 100644 --- a/lib/mihari/rule.rb +++ b/lib/mihari/rule.rb @@ -5,6 +5,9 @@ class Rule < Service include Concerns::FalsePositiveNormalizable include Concerns::FalsePositiveValidatable + # @return [String, nil] + attr_reader :path_or_id + # @return [Hash] attr_reader :data @@ -19,9 +22,11 @@ class Rule < Service # # @param [Hash] data # - def initialize(**data) + # @param [Object] path_or_id + def initialize(path_or_id: nil, **data) super() + @path_or_id = path_or_id @data = data.deep_symbolize_keys @errors = nil @base_time = Time.now.utc @@ -251,16 +256,29 @@ def update_or_create end class << self + # + # Load rule from YAML file + # + # @param [String] path + # + # @return [Mihari::Rule] + # + def from_file(path) + yaml = File.read(path) + from_yaml(yaml, path: path) + end + # # Load rule from YAML string # # @param [String] yaml + # @param [String, nil] path # # @return [Mihari::Rule] # - def from_yaml(yaml) + def from_yaml(yaml, path: nil) data = YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol]) - new(**data) + new(path_or_id: path, **data) end # @@ -269,7 +287,7 @@ def from_yaml(yaml) # @return [Mihari::Rule] # def from_model(model) - new(**model.data) + new(path_or_id: model.id, **model.data) end end @@ -409,7 +427,7 @@ def validate! @data = result.to_h @errors = result.errors - raise ValidationError.new("Validation failed", errors) if errors? + raise ValidationError.new("#{path_or_id}: validation failed", errors) if errors? end end end diff --git a/mihari.gemspec b/mihari.gemspec index 16d142f6..9f1d2982 100644 --- a/mihari.gemspec +++ b/mihari.gemspec @@ -40,7 +40,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency "binding_of_caller", "~> 1.0.1" spec.add_development_dependency "bundler", "~> 2.5" spec.add_development_dependency "capybara", "~> 3.40" - spec.add_development_dependency "coveralls_reborn", "~> 0.28" spec.add_development_dependency "factory_bot", "~> 6.4.6" spec.add_development_dependency "fakefs", "~> 2.5" spec.add_development_dependency "faker", "~> 3.4.1" @@ -60,6 +59,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rubocop-rspec", "~> 3.0.2" spec.add_development_dependency "rubocop-yard", "~> 0.9.3" spec.add_development_dependency "simplecov-lcov", "~> 0.8" + spec.add_development_dependency "simplecov", "~> 0.22" spec.add_development_dependency "standard", "~> 1.39.1" spec.add_development_dependency "test-prof", "~> 1.3.3" spec.add_development_dependency "timecop", "~> 0.9.10" diff --git a/spec/cli/rule_spec.rb b/spec/cli/rule_spec.rb index ce9a68ae..13c99681 100644 --- a/spec/cli/rule_spec.rb +++ b/spec/cli/rule_spec.rb @@ -17,19 +17,18 @@ describe "#validate" do let!(:path) { File.expand_path("../fixtures/rules/valid_rule.yml", __dir__) } - let!(:data) { File.read path } - let!(:rule_id) { YAML.safe_load(data)["id"] } it do - expect { described_class.new.invoke(:validate, [path]) }.to output(include(rule_id)).to_stdout + expect { described_class.new.invoke(:validate, [path]) }.not_to output.to_stdout end context "with invalid rule" do let!(:path) { File.expand_path("../fixtures/rules/invalid_rule.yml", __dir__) } it do - # TODO: assert UnwrapError - expect { described_class.new.invoke(:validate, [path]) }.to raise_error(Dry::Monads::UnwrapError) + expect do + expect { described_class.new.invoke(:validate, [path]) }.to output(include(path)).to_stderr + end.to raise_error(SystemExit) # ref. https://pocke.hatenablog.com/entry/2016/07/17/085928 end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 95fe0b02..c5bdb692 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,48 +21,19 @@ def ci_env? ENV["CI"] end -# setup simplecov formatter for coveralls -class InceptionFormatter - def format(result) - Coveralls::SimpleCov::Formatter.new.format(result) - end -end - -def formatter - if ENV["CI"] || ENV["COVERALLS_REPO_TOKEN"] - if ENV["GITHUB_ACTIONS"] - SimpleCov::Formatter::MultiFormatter.new([InceptionFormatter, SimpleCov::Formatter::LcovFormatter]) - else - InceptionFormatter - end - else - SimpleCov::Formatter::HTMLFormatter - end -end - -def setup_formatter - if ENV["GITHUB_ACTIONS"] +SimpleCov.start "rails" do + if ENV["CI"] require "simplecov-lcov" SimpleCov::Formatter::LcovFormatter.config do |c| c.report_with_single_file = true c.single_report_path = "coverage/lcov.info" end - end - SimpleCov.formatter = formatter -end -setup_formatter - -SimpleCov.start do - add_filter do |source_file| - source_file.filename.include?("spec") && !source_file.filename.include?("fixture") + formatter SimpleCov::Formatter::LcovFormatter end - add_filter %r{/.bundle/} end -require "coveralls" - # for Rack app / Sinatra controllers ENV["APP_ENV"] = "test" # Use in-memory SQLite in local test