mihari 2.4.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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