mihari 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/.overcommit.yml +12 -0
  4. data/README.md +1 -9
  5. data/exe/mihari +1 -1
  6. data/lib/mihari.rb +88 -15
  7. data/lib/mihari/analyzers/base.rb +49 -8
  8. data/lib/mihari/analyzers/basic.rb +1 -2
  9. data/lib/mihari/analyzers/binaryedge.rb +7 -13
  10. data/lib/mihari/analyzers/censys.rb +26 -63
  11. data/lib/mihari/analyzers/circl.rb +20 -17
  12. data/lib/mihari/analyzers/crtsh.rb +6 -13
  13. data/lib/mihari/analyzers/dnpedia.rb +6 -12
  14. data/lib/mihari/analyzers/dnstwister.rb +13 -10
  15. data/lib/mihari/analyzers/onyphe.rb +6 -12
  16. data/lib/mihari/analyzers/otx.rb +22 -19
  17. data/lib/mihari/analyzers/passivetotal.rb +22 -21
  18. data/lib/mihari/analyzers/pulsedive.rb +16 -13
  19. data/lib/mihari/analyzers/rule.rb +99 -0
  20. data/lib/mihari/analyzers/securitytrails.rb +22 -19
  21. data/lib/mihari/analyzers/shodan.rb +7 -13
  22. data/lib/mihari/analyzers/spyse.rb +12 -19
  23. data/lib/mihari/analyzers/urlscan.rb +22 -27
  24. data/lib/mihari/analyzers/virustotal.rb +25 -22
  25. data/lib/mihari/analyzers/zoomeye.rb +14 -20
  26. data/lib/mihari/cli/analyzer.rb +44 -0
  27. data/lib/mihari/cli/base.rb +27 -0
  28. data/lib/mihari/cli/init.rb +13 -0
  29. data/lib/mihari/cli/main.rb +30 -0
  30. data/lib/mihari/cli/mixins/utils.rb +88 -0
  31. data/lib/mihari/cli/validator.rb +11 -0
  32. data/lib/mihari/commands/binaryedge.rb +1 -1
  33. data/lib/mihari/commands/censys.rb +1 -1
  34. data/lib/mihari/commands/circl.rb +2 -2
  35. data/lib/mihari/commands/crtsh.rb +1 -1
  36. data/lib/mihari/commands/dnpedia.rb +1 -1
  37. data/lib/mihari/commands/dnstwister.rb +2 -2
  38. data/lib/mihari/commands/init.rb +46 -0
  39. data/lib/mihari/commands/json.rb +1 -1
  40. data/lib/mihari/commands/onyphe.rb +1 -1
  41. data/lib/mihari/commands/otx.rb +2 -2
  42. data/lib/mihari/commands/passivetotal.rb +2 -2
  43. data/lib/mihari/commands/pulsedive.rb +2 -2
  44. data/lib/mihari/commands/search.rb +77 -0
  45. data/lib/mihari/commands/securitytrails.rb +2 -2
  46. data/lib/mihari/commands/shodan.rb +1 -1
  47. data/lib/mihari/commands/spyse.rb +1 -1
  48. data/lib/mihari/commands/urlscan.rb +2 -2
  49. data/lib/mihari/commands/validator.rb +38 -0
  50. data/lib/mihari/commands/virustotal.rb +2 -2
  51. data/lib/mihari/commands/zoomeye.rb +1 -1
  52. data/lib/mihari/constraints.rb +5 -0
  53. data/lib/mihari/database.rb +13 -2
  54. data/lib/mihari/emitters/base.rb +2 -2
  55. data/lib/mihari/emitters/database.rb +1 -1
  56. data/lib/mihari/emitters/misp.rb +1 -1
  57. data/lib/mihari/emitters/slack.rb +5 -6
  58. data/lib/mihari/emitters/the_hive.rb +1 -1
  59. data/lib/mihari/emitters/webhook.rb +2 -9
  60. data/lib/mihari/mixins/configurable.rb +38 -0
  61. data/lib/mihari/mixins/configuration.rb +85 -0
  62. data/lib/mihari/mixins/hash.rb +20 -0
  63. data/lib/mihari/mixins/refang.rb +21 -0
  64. data/lib/mihari/mixins/retriable.rb +27 -0
  65. data/lib/mihari/mixins/rule.rb +79 -0
  66. data/lib/mihari/models/alert.rb +28 -1
  67. data/lib/mihari/models/artifact.rb +10 -0
  68. data/lib/mihari/notifiers/base.rb +9 -1
  69. data/lib/mihari/notifiers/exception_notifier.rb +50 -0
  70. data/lib/mihari/notifiers/slack.rb +29 -0
  71. data/lib/mihari/schemas/configuration.rb +42 -0
  72. data/lib/mihari/schemas/macros.rb +17 -0
  73. data/lib/mihari/schemas/rule.rb +72 -0
  74. data/lib/mihari/serializers/artifact.rb +1 -1
  75. data/lib/mihari/status.rb +14 -0
  76. data/lib/mihari/templates/rule.yml.erb +19 -0
  77. data/lib/mihari/type_checker.rb +8 -3
  78. data/lib/mihari/version.rb +1 -1
  79. data/lib/mihari/web/controllers/base_controller.rb +1 -1
  80. data/lib/mihari/web/public/index.html +1 -21
  81. data/lib/mihari/web/public/redoc-static.html +2 -2
  82. data/lib/mihari/web/public/static/js/app.ab213f7c.js +12 -0
  83. data/lib/mihari/web/public/static/js/app.ab213f7c.js.map +1 -0
  84. data/mihari.gemspec +12 -5
  85. metadata +123 -50
  86. data/.rubocop.yml +0 -161
  87. data/lib/mihari/analyzers/free_text.rb +0 -48
  88. data/lib/mihari/analyzers/http_hash.rb +0 -100
  89. data/lib/mihari/analyzers/passive_dns.rb +0 -59
  90. data/lib/mihari/analyzers/passive_ssl.rb +0 -55
  91. data/lib/mihari/analyzers/reverse_whois.rb +0 -55
  92. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +0 -59
  93. data/lib/mihari/analyzers/ssh_fingerprint.rb +0 -58
  94. data/lib/mihari/cli.rb +0 -126
  95. data/lib/mihari/commands/config.rb +0 -27
  96. data/lib/mihari/commands/free_text.rb +0 -21
  97. data/lib/mihari/commands/http_hash.rb +0 -25
  98. data/lib/mihari/commands/passive_dns.rb +0 -21
  99. data/lib/mihari/commands/passive_ssl.rb +0 -21
  100. data/lib/mihari/commands/reverse_whois.rb +0 -21
  101. data/lib/mihari/commands/securitytrails_domain_feed.rb +0 -23
  102. data/lib/mihari/commands/ssh_fingerprint.rb +0 -21
  103. data/lib/mihari/config.rb +0 -85
  104. data/lib/mihari/configurable.rb +0 -21
  105. data/lib/mihari/html.rb +0 -43
  106. data/lib/mihari/retriable.rb +0 -17
@@ -0,0 +1,79 @@
1
+ require "date"
2
+ require "pathname"
3
+ require "yaml"
4
+ require "erb"
5
+
6
+ module Mihari
7
+ module Mixins
8
+ module Rule
9
+ #
10
+ # Load rule into hash
11
+ #
12
+ # @param [String] path Path to YAML file or YAML string
13
+ #
14
+ # @return [Hash]
15
+ #
16
+ def load_rule(path)
17
+ return YAML.safe_load(File.read(path), permitted_classes: [Date], symbolize_names: true) if Pathname(path).exist?
18
+
19
+ YAML.safe_load(path, symbolize_names: true)
20
+ end
21
+
22
+ #
23
+ # Validate rule schema
24
+ #
25
+ # @param [Hash] rule
26
+ #
27
+ def validate_rule(rule)
28
+ error_message = "Failed to parse the input as a rule!"
29
+
30
+ contract = Schemas::RuleContract.new
31
+ begin
32
+ result = contract.call(rule)
33
+ unless result.errors.empty?
34
+ messages = result.errors.messages.map do |message|
35
+ path = message.path.map(&:to_s).join
36
+ "#{path} #{message.text}"
37
+ end
38
+ puts error_message.colorize(:red)
39
+ raise ArgumentError, messages.join("\n")
40
+ end
41
+ rescue NoMethodError
42
+ puts error_message.colorize(:red)
43
+ raise ArgumentError, "Invalid rule schema"
44
+ end
45
+ end
46
+
47
+ #
48
+ # Returns a template for rule
49
+ #
50
+ # @return [String] A template for rule
51
+ #
52
+ def rule_template
53
+ # Use ERB to fill created_on and updated_on with Date.today
54
+ data = File.read(File.expand_path("../templates/rule.yml.erb", __dir__))
55
+ template = ERB.new(data)
56
+ data = template.result
57
+
58
+ # validate the template of rule for just in case
59
+ rule = YAML.safe_load(data, permitted_classes: [Date], symbolize_names: true)
60
+ validate_rule rule
61
+
62
+ data
63
+ end
64
+
65
+ #
66
+ # Create (blank) rule file
67
+ #
68
+ # @param [String] filename
69
+ # @param [Dry::Files] files
70
+ # @param [String] template
71
+ #
72
+ # @return [nil]
73
+ #
74
+ def initialize_rule_yaml(filename, files = Dry::Files.new, template: rule_template)
75
+ files.write(filename, template)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -10,6 +10,21 @@ module Mihari
10
10
  has_many :tags, through: :taggings
11
11
 
12
12
  class << self
13
+ #
14
+ # Search alerts
15
+ #
16
+ # @param [String, nil] artifact_data
17
+ # @param [String, nil] description
18
+ # @param [String, nil] source
19
+ # @param [String, nil] tag_name
20
+ # @param [String, nil] title
21
+ # @param [String, nil] from_at
22
+ # @param [String, nil] to_at
23
+ # @param [Integer, nil] limit
24
+ # @param [Integer, nil] page
25
+ #
26
+ # @return [Array<Hash>]
27
+ #
13
28
  def search(artifact_data: nil, description: nil, source: nil, tag_name: nil, title: nil, from_at: nil, to_at: nil, limit: 10, page: 1)
14
29
  limit = limit.to_i
15
30
  raise ArgumentError, "limit should be bigger than zero" unless limit.positive?
@@ -20,7 +35,6 @@ module Mihari
20
35
  offset = (page - 1) * limit
21
36
 
22
37
  relation = build_relation(artifact_data: artifact_data, title: title, description: description, source: source, tag_name: tag_name, from_at: from_at, to_at: to_at)
23
- # relation = relation.group("alerts.id")
24
38
 
25
39
  alerts = relation.limit(limit).offset(offset).order(id: :desc)
26
40
 
@@ -32,6 +46,19 @@ module Mihari
32
46
  end
33
47
  end
34
48
 
49
+ #
50
+ # Count alerts
51
+ #
52
+ # @param [String, nil] artifact_data
53
+ # @param [String, nil] description
54
+ # @param [String, nil] source
55
+ # @param [String, nil] tag_name
56
+ # @param [String, nil] title
57
+ # @param [String, nil] from_at
58
+ # @param [String, nil] to_at
59
+ #
60
+ # @return [Integer]
61
+ #
35
62
  def count(artifact_data: nil, description: nil, source: nil, tag_name: nil, title: nil, from_at: nil, to_at: nil)
36
63
  relation = build_relation(artifact_data: artifact_data, title: title, description: description, source: source, tag_name: tag_name, from_at: from_at, to_at: to_at)
37
64
  relation.distinct("alerts.id").count
@@ -16,13 +16,23 @@ end
16
16
  module Mihari
17
17
  class Artifact < ActiveRecord::Base
18
18
  include ActiveModel::Validations
19
+
19
20
  validates_with ArtifactValidator
20
21
 
21
22
  def initialize(attributes)
22
23
  super
24
+
23
25
  self.data_type = TypeChecker.type(data)
24
26
  end
25
27
 
28
+ #
29
+ # Check uniqueness of artifact
30
+ #
31
+ # @param [Boolean] ignore_old_artifacts
32
+ # @param [Integer] ignore_threshold
33
+ #
34
+ # @return [Boolean] true if it is unique. Otherwise false.
35
+ #
26
36
  def unique?(ignore_old_artifacts: false, ignore_threshold: 0)
27
37
  artifact = self.class.where(data: data).order(created_at: :desc).first
28
38
  return true if artifact.nil?
@@ -3,11 +3,19 @@
3
3
  module Mihari
4
4
  module Notifiers
5
5
  class Base
6
- # @return [true, false]
6
+ # Validate notifier availability
7
+ #
8
+ # @return [Boolean]
9
+ #
7
10
  def valid?
8
11
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
9
12
  end
10
13
 
14
+ #
15
+ # Send a notification
16
+ #
17
+ # @return [nil]
18
+ #
11
19
  def notify
12
20
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
13
21
  end
@@ -24,10 +24,25 @@ module Mihari
24
24
  notify_to_slack(text: clean_message, attachments: attachments) if @slack.valid?
25
25
  end
26
26
 
27
+ #
28
+ # Send notification to Slack
29
+ #
30
+ # @param [String] text
31
+ # @param [Array<Hash>] attachments
32
+ #
33
+ # @return [nil]
34
+ #
27
35
  def notify_to_slack(text:, attachments:)
28
36
  @slack.notify(text: text, attachments: attachments)
29
37
  end
30
38
 
39
+ #
40
+ # Send notification to STDOUT
41
+ #
42
+ # @param [Exception] exception
43
+ #
44
+ # @return [nil]
45
+ #
31
46
  def notify_to_stdout(exception)
32
47
  text = to_text(exception.class).chomp
33
48
  message = "#{text}: #{exception.message}"
@@ -35,6 +50,14 @@ module Mihari
35
50
  puts format_backtrace(exception.backtrace) if exception.backtrace
36
51
  end
37
52
 
53
+ #
54
+ # Convert exception to attachments (for Slack)
55
+ #
56
+ # @param [Exception] exception
57
+ # @param [String] clean_message
58
+ #
59
+ # @return [Array<Hash>]
60
+ #
38
61
  def to_attachments(exception, clean_message)
39
62
  text = to_text(exception.class)
40
63
  backtrace = exception.backtrace
@@ -43,12 +66,27 @@ module Mihari
43
66
  [color: @color, text: text, fields: fields, mrkdwn_in: %w[text fields]]
44
67
  end
45
68
 
69
+ #
70
+ # Convert exception class to text
71
+ #
72
+ # @param [Class<Exception>] exception_class
73
+ #
74
+ # @return [String]
75
+ #
46
76
  def to_text(exception_class)
47
77
  measure_word = /^[aeiou]/i.match?(exception_class.to_s) ? "An" : "A"
48
78
  exception_name = "*#{measure_word}* `#{exception_class}`"
49
79
  "#{exception_name} *occured in background*\n"
50
80
  end
51
81
 
82
+ #
83
+ # Convert clean_message and backtrace into fields (for Slack)
84
+ #
85
+ # @param [String] clean_message
86
+ # @param [Array] backtrace
87
+ #
88
+ # @return [Array<Hash>]
89
+ #
52
90
  def to_fields(clean_message, backtrace)
53
91
  fields = [
54
92
  { title: "Exception", value: clean_message },
@@ -62,12 +100,24 @@ module Mihari
62
100
  fields
63
101
  end
64
102
 
103
+ #
104
+ # Hostname of runnning instance
105
+ #
106
+ # @return [String]
107
+ #
65
108
  def hostname
66
109
  Socket.gethostname
67
110
  rescue StandardError => _e
68
111
  "N/A"
69
112
  end
70
113
 
114
+ #
115
+ # Format backtrace in string
116
+ #
117
+ # @param [Array] backtrace
118
+ #
119
+ # @return [String]
120
+ #
71
121
  def format_backtrace(backtrace)
72
122
  return nil unless backtrace
73
123
 
@@ -9,22 +9,51 @@ module Mihari
9
9
  SLACK_CHANNEL_KEY = "SLACK_CHANNEL"
10
10
  DEFAULT_USERNAME = "mihari"
11
11
 
12
+ #
13
+ # Slack channel to post
14
+ #
15
+ # @return [String]
16
+ #
12
17
  def slack_channel
13
18
  Mihari.config.slack_channel || "#general"
14
19
  end
15
20
 
21
+ #
22
+ # Slack webhook URL
23
+ #
24
+ # @return [String]
25
+ #
16
26
  def slack_webhook_url
17
27
  Mihari.config.slack_webhook_url
18
28
  end
19
29
 
30
+ #
31
+ # Check Slack webhook URL is set
32
+ #
33
+ # @return [Boolean]
34
+ #
20
35
  def slack_webhook_url?
21
36
  !Mihari.config.slack_webhook_url.nil?
22
37
  end
23
38
 
39
+ #
40
+ # Check Slack webhook URL is set. Alias of #slack_webhook_url?.
41
+ #
42
+ # @return [Boolean]
43
+ #
24
44
  def valid?
25
45
  slack_webhook_url?
26
46
  end
27
47
 
48
+ #
49
+ # Send notification to Slack
50
+ #
51
+ # @param [String] text
52
+ # @param [Array<Hash>] attachments
53
+ # @param [Boolean] mrkdwn
54
+ #
55
+ # @return [nil]
56
+ #
28
57
  def notify(text:, attachments: [], mrkdwn: true)
29
58
  notifier = ::Slack::Notifier.new(slack_webhook_url, channel: slack_channel, username: DEFAULT_USERNAME)
30
59
  notifier.post(text: text, attachments: attachments, mrkdwn: mrkdwn)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+ require "dry/validation"
5
+
6
+ require "mihari/schemas/macros"
7
+
8
+ module Mihari
9
+ module Schemas
10
+ Configuration = Dry::Schema.Params do
11
+ optional(:binaryedge_api_key).value(:string)
12
+ optional(:censys_id).value(:string)
13
+ optional(:censys_secret).value(:string)
14
+ optional(:circl_passive_password).value(:string)
15
+ optional(:circl_passive_username).value(:string)
16
+ optional(:misp_api_endpoint).value(:string)
17
+ optional(:misp_api_key).value(:string)
18
+ optional(:onyphe_api_key).value(:string)
19
+ optional(:otx_api_key).value(:string)
20
+ optional(:passivetotal_api_key).value(:string)
21
+ optional(:passivetotal_username).value(:string)
22
+ optional(:pulsedive_api_key).value(:string)
23
+ optional(:securitytrails_api_key).value(:string)
24
+ optional(:shodan_api_key).value(:string)
25
+ optional(:slack_channel).value(:string)
26
+ optional(:slack_webhook_url).value(:string)
27
+ optional(:spyse_api_key).value(:string)
28
+ optional(:thehive_api_endpoint).value(:string)
29
+ optional(:thehive_api_key).value(:string)
30
+ optional(:urlscan_api_key).value(:string)
31
+ optional(:virustotal_api_key).value(:string)
32
+ optional(:zoomeye_api_key).value(:string)
33
+ optional(:webhook_url).value(:string)
34
+ optional(:webhook_use_json_body).value(:bool)
35
+ optional(:database).value(:string)
36
+ end
37
+
38
+ class ConfigurationContract < Dry::Validation::Contract
39
+ params(Configuration)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types"
4
+
5
+ module Dry
6
+ module Schema
7
+ module Macros
8
+ class DSL
9
+ def default(value)
10
+ schema_dsl.before(:rule_applier) do |result|
11
+ result.update(name => value) unless result[name]
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+ require "dry/validation"
5
+ require "dry/types"
6
+
7
+ require "mihari/schemas/macros"
8
+
9
+ module Mihari
10
+ module Types
11
+ include Dry.Types()
12
+ end
13
+
14
+ DataTypes = Types::String.enum(*ALLOWED_DATA_TYPES)
15
+
16
+ AnalyzerTypes = Types::String.enum(
17
+ "binaryedge", "censys", "circl", "dnpedia", "dnstwister",
18
+ "onyphe", "otx", "passivetotal", "pulsedive", "securitytrails",
19
+ "shodan", "virustotal"
20
+ )
21
+
22
+ module Schemas
23
+ Analyzer = Dry::Schema.Params do
24
+ required(:analyzer).value(AnalyzerTypes)
25
+ required(:query).value(:string)
26
+ end
27
+
28
+ Spyse = Dry::Schema.Params do
29
+ required(:analyzer).value(Types::String.enum("spyse"))
30
+ required(:query).value(:string)
31
+ required(:type).value(Types::String.enum("ip", "domain"))
32
+ end
33
+
34
+ ZoomEye = Dry::Schema.Params do
35
+ required(:analyzer).value(Types::String.enum("zoomeye"))
36
+ required(:query).value(:string)
37
+ required(:type).value(Types::String.enum("host", "web"))
38
+ end
39
+
40
+ Crtsh = Dry::Schema.Params do
41
+ required(:analyzer).value(Types::String.enum("crtsh"))
42
+ required(:query).value(:string)
43
+ optional(:exclude_expired).value(:bool).default(true)
44
+ end
45
+
46
+ Urlscan = Dry::Schema.Params do
47
+ required(:analyzer).value(Types::String.enum("urlscan"))
48
+ required(:query).value(:string)
49
+ optional(:use_similarity).value(:bool).default(true)
50
+ end
51
+
52
+ Rule = Dry::Schema.Params do
53
+ required(:title).value(:string)
54
+ required(:description).value(:string)
55
+
56
+ optional(:tags).value(array[:string]).default([])
57
+ optional(:id).value(:string)
58
+
59
+ optional(:author).value(:string)
60
+ optional(:created_on).value(:date)
61
+ optional(:updated_on).value(:date)
62
+
63
+ required(:queries).value(:array).each { Analyzer | Spyse | ZoomEye | Urlscan | Crtsh }
64
+
65
+ optional(:allowed_data_types).value(array[DataTypes]).default(ALLOWED_DATA_TYPES)
66
+ end
67
+
68
+ class RuleContract < Dry::Validation::Contract
69
+ params(Rule)
70
+ end
71
+ end
72
+ end