mihari 2.3.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) 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/docker/Dockerfile +1 -1
  6. data/exe/mihari +1 -1
  7. data/lib/mihari.rb +89 -15
  8. data/lib/mihari/analyzers/base.rb +49 -8
  9. data/lib/mihari/analyzers/basic.rb +1 -2
  10. data/lib/mihari/analyzers/binaryedge.rb +7 -13
  11. data/lib/mihari/analyzers/censys.rb +26 -63
  12. data/lib/mihari/analyzers/circl.rb +20 -17
  13. data/lib/mihari/analyzers/crtsh.rb +6 -13
  14. data/lib/mihari/analyzers/dnpedia.rb +6 -12
  15. data/lib/mihari/analyzers/dnstwister.rb +13 -10
  16. data/lib/mihari/analyzers/onyphe.rb +6 -12
  17. data/lib/mihari/analyzers/otx.rb +22 -19
  18. data/lib/mihari/analyzers/passivetotal.rb +22 -21
  19. data/lib/mihari/analyzers/pulsedive.rb +16 -13
  20. data/lib/mihari/analyzers/rule.rb +97 -0
  21. data/lib/mihari/analyzers/securitytrails.rb +22 -19
  22. data/lib/mihari/analyzers/shodan.rb +7 -13
  23. data/lib/mihari/analyzers/spyse.rb +12 -19
  24. data/lib/mihari/analyzers/urlscan.rb +22 -27
  25. data/lib/mihari/analyzers/virustotal.rb +25 -22
  26. data/lib/mihari/analyzers/zoomeye.rb +14 -20
  27. data/lib/mihari/cli/analyzer.rb +44 -0
  28. data/lib/mihari/cli/base.rb +27 -0
  29. data/lib/mihari/cli/init.rb +13 -0
  30. data/lib/mihari/cli/main.rb +30 -0
  31. data/lib/mihari/cli/mixins/utils.rb +88 -0
  32. data/lib/mihari/cli/validator.rb +11 -0
  33. data/lib/mihari/commands/binaryedge.rb +1 -1
  34. data/lib/mihari/commands/censys.rb +1 -1
  35. data/lib/mihari/commands/circl.rb +2 -2
  36. data/lib/mihari/commands/crtsh.rb +1 -1
  37. data/lib/mihari/commands/dnpedia.rb +1 -1
  38. data/lib/mihari/commands/dnstwister.rb +2 -2
  39. data/lib/mihari/commands/init.rb +46 -0
  40. data/lib/mihari/commands/json.rb +1 -1
  41. data/lib/mihari/commands/onyphe.rb +1 -1
  42. data/lib/mihari/commands/otx.rb +2 -2
  43. data/lib/mihari/commands/passivetotal.rb +2 -2
  44. data/lib/mihari/commands/pulsedive.rb +2 -2
  45. data/lib/mihari/commands/search.rb +77 -0
  46. data/lib/mihari/commands/securitytrails.rb +2 -2
  47. data/lib/mihari/commands/shodan.rb +1 -1
  48. data/lib/mihari/commands/spyse.rb +1 -1
  49. data/lib/mihari/commands/urlscan.rb +2 -2
  50. data/lib/mihari/commands/validator.rb +38 -0
  51. data/lib/mihari/commands/virustotal.rb +2 -2
  52. data/lib/mihari/commands/zoomeye.rb +1 -1
  53. data/lib/mihari/constraints.rb +5 -0
  54. data/lib/mihari/database.rb +13 -2
  55. data/lib/mihari/emitters/base.rb +2 -2
  56. data/lib/mihari/emitters/database.rb +1 -1
  57. data/lib/mihari/emitters/misp.rb +3 -1
  58. data/lib/mihari/emitters/slack.rb +6 -10
  59. data/lib/mihari/emitters/the_hive.rb +1 -1
  60. data/lib/mihari/emitters/webhook.rb +53 -0
  61. data/lib/mihari/mixins/configurable.rb +38 -0
  62. data/lib/mihari/mixins/configuration.rb +90 -0
  63. data/lib/mihari/mixins/hash.rb +20 -0
  64. data/lib/mihari/mixins/refang.rb +21 -0
  65. data/lib/mihari/mixins/retriable.rb +27 -0
  66. data/lib/mihari/mixins/rule.rb +79 -0
  67. data/lib/mihari/models/alert.rb +28 -1
  68. data/lib/mihari/models/artifact.rb +11 -1
  69. data/lib/mihari/notifiers/base.rb +9 -1
  70. data/lib/mihari/notifiers/exception_notifier.rb +50 -0
  71. data/lib/mihari/notifiers/slack.rb +29 -1
  72. data/lib/mihari/schemas/configuration.rb +42 -0
  73. data/lib/mihari/schemas/macros.rb +17 -0
  74. data/lib/mihari/schemas/rule.rb +72 -0
  75. data/lib/mihari/serializers/artifact.rb +1 -1
  76. data/lib/mihari/status.rb +14 -0
  77. data/lib/mihari/templates/rule.yml.erb +19 -0
  78. data/lib/mihari/type_checker.rb +8 -3
  79. data/lib/mihari/version.rb +1 -1
  80. data/lib/mihari/web/controllers/base_controller.rb +1 -1
  81. data/lib/mihari/web/public/index.html +1 -21
  82. data/lib/mihari/web/public/redoc-static.html +2 -2
  83. data/lib/mihari/web/public/static/js/app.ab213f7c.js +12 -0
  84. data/lib/mihari/web/public/static/js/app.ab213f7c.js.map +1 -0
  85. data/mihari.gemspec +19 -12
  86. metadata +138 -65
  87. data/.rubocop.yml +0 -161
  88. data/lib/mihari/analyzers/free_text.rb +0 -48
  89. data/lib/mihari/analyzers/http_hash.rb +0 -100
  90. data/lib/mihari/analyzers/passive_dns.rb +0 -59
  91. data/lib/mihari/analyzers/passive_ssl.rb +0 -55
  92. data/lib/mihari/analyzers/reverse_whois.rb +0 -55
  93. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +0 -59
  94. data/lib/mihari/analyzers/ssh_fingerprint.rb +0 -58
  95. data/lib/mihari/cli.rb +0 -126
  96. data/lib/mihari/commands/config.rb +0 -27
  97. data/lib/mihari/commands/free_text.rb +0 -21
  98. data/lib/mihari/commands/http_hash.rb +0 -25
  99. data/lib/mihari/commands/passive_dns.rb +0 -21
  100. data/lib/mihari/commands/passive_ssl.rb +0 -21
  101. data/lib/mihari/commands/reverse_whois.rb +0 -21
  102. data/lib/mihari/commands/securitytrails_domain_feed.rb +0 -23
  103. data/lib/mihari/commands/ssh_fingerprint.rb +0 -21
  104. data/lib/mihari/config.rb +0 -83
  105. data/lib/mihari/configurable.rb +0 -21
  106. data/lib/mihari/html.rb +0 -43
  107. data/lib/mihari/retriable.rb +0 -17
  108. data/lib/mihari/slack_monkeypatch.rb +0 -16
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cymbal"
4
+
5
+ module Mihari
6
+ module Mixins
7
+ module Hash
8
+ #
9
+ # Symbolize hash keys
10
+ #
11
+ # @param [Hash] hash
12
+ #
13
+ # @return [Hash]
14
+ #
15
+ def symbolize_hash(hash)
16
+ Cymbal.symbolize hash
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Mixins
5
+ module Refang
6
+ #
7
+ # Refang defanged indicator
8
+ #
9
+ # @param [String] indicator
10
+ #
11
+ # @return [String]
12
+ #
13
+ def refang(indicator)
14
+ return indicator.gsub("[.]", ".").gsub("(.)", ".") if indicator.is_a?(String)
15
+
16
+ # for RSpec & Ruby 2.7
17
+ indicator
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Mixins
5
+ module Retriable
6
+ #
7
+ # Retry on error
8
+ #
9
+ # @param [Integer] times
10
+ # @param [Integer] interval
11
+ #
12
+ # @return [nil]
13
+ #
14
+ def retry_on_error(times: 3, interval: 10)
15
+ try = 0
16
+ begin
17
+ try += 1
18
+ yield
19
+ rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError, Timeout::Error, RetryableError => e
20
+ sleep interval
21
+ retry if try < times
22
+ raise e
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -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,20 +16,30 @@ 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?
29
39
 
30
40
  return false unless ignore_old_artifacts
31
41
 
32
- days_before = (-ignore_threshold).days.from_now
42
+ days_before = (-ignore_threshold).days.from_now.utc
33
43
  # if an artifact is created before {ignore_threshold} days, ignore it
34
44
  # within {ignore_threshold} days, do not ignore it
35
45
  artifact.created_at < days_before
@@ -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
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "slack-notifier"
4
- require "mihari/slack_monkeypatch"
5
4
 
6
5
  module Mihari
7
6
  module Notifiers
@@ -10,22 +9,51 @@ module Mihari
10
9
  SLACK_CHANNEL_KEY = "SLACK_CHANNEL"
11
10
  DEFAULT_USERNAME = "mihari"
12
11
 
12
+ #
13
+ # Slack channel to post
14
+ #
15
+ # @return [String]
16
+ #
13
17
  def slack_channel
14
18
  Mihari.config.slack_channel || "#general"
15
19
  end
16
20
 
21
+ #
22
+ # Slack webhook URL
23
+ #
24
+ # @return [String]
25
+ #
17
26
  def slack_webhook_url
18
27
  Mihari.config.slack_webhook_url
19
28
  end
20
29
 
30
+ #
31
+ # Check Slack webhook URL is set
32
+ #
33
+ # @return [Boolean]
34
+ #
21
35
  def slack_webhook_url?
22
36
  !Mihari.config.slack_webhook_url.nil?
23
37
  end
24
38
 
39
+ #
40
+ # Check Slack webhook URL is set. Alias of #slack_webhook_url?.
41
+ #
42
+ # @return [Boolean]
43
+ #
25
44
  def valid?
26
45
  slack_webhook_url?
27
46
  end
28
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
+ #
29
57
  def notify(text:, attachments: [], mrkdwn: true)
30
58
  notifier = ::Slack::Notifier.new(slack_webhook_url, channel: slack_channel, username: DEFAULT_USERNAME)
31
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