Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: split validation and formatting #1101

Merged
merged 4 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
31 changes: 27 additions & 4 deletions lib/mihari/commands/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>] paths
#
def validate(*paths)
# @type [Array<Mihari::ValidationError>]
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

Expand Down
28 changes: 23 additions & 5 deletions lib/mihari/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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

#
Expand All @@ -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

Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion mihari.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
9 changes: 4 additions & 5 deletions spec/cli/rule_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 3 additions & 32 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down