mihari 3.2.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -2
  3. data/config.ru +6 -0
  4. data/images/overview.jpg +0 -0
  5. data/lib/mihari.rb +2 -0
  6. data/lib/mihari/analyzers/rule.rb +27 -0
  7. data/lib/mihari/cli/analyzer.rb +4 -0
  8. data/lib/mihari/cli/base.rb +0 -5
  9. data/lib/mihari/commands/init.rb +4 -4
  10. data/lib/mihari/commands/search.rb +20 -8
  11. data/lib/mihari/commands/web.rb +5 -0
  12. data/lib/mihari/mixins/disallowed_data_value.rb +42 -0
  13. data/lib/mihari/mixins/rule.rb +5 -1
  14. data/lib/mihari/models/alert.rb +24 -10
  15. data/lib/mihari/schemas/configuration.rb +1 -0
  16. data/lib/mihari/schemas/rule.rb +14 -0
  17. data/lib/mihari/templates/rule.yml.erb +5 -1
  18. data/lib/mihari/version.rb +1 -1
  19. data/lib/mihari/web/app.rb +3 -0
  20. data/lib/mihari/web/controllers/alerts_controller.rb +3 -4
  21. data/lib/mihari/web/controllers/artifacts_controller.rb +27 -2
  22. data/lib/mihari/web/controllers/ip_address_controller.rb +36 -0
  23. data/lib/mihari/web/controllers/tags_controller.rb +3 -1
  24. data/lib/mihari/web/public/index.html +1 -1
  25. data/lib/mihari/web/public/redoc-static.html +12 -10
  26. data/lib/mihari/web/public/static/fonts/fa-brands-400.1a575a41.woff +0 -0
  27. data/lib/mihari/web/public/static/fonts/fa-brands-400.513aa607.ttf +0 -0
  28. data/lib/mihari/web/public/static/fonts/fa-brands-400.592643a8.eot +0 -0
  29. data/lib/mihari/web/public/static/fonts/fa-brands-400.ed311c7a.woff2 +0 -0
  30. data/lib/mihari/web/public/static/fonts/fa-regular-400.766913e6.ttf +0 -0
  31. data/lib/mihari/web/public/static/fonts/fa-regular-400.b0e2db3b.eot +0 -0
  32. data/lib/mihari/web/public/static/fonts/fa-regular-400.b91d376b.woff2 +0 -0
  33. data/lib/mihari/web/public/static/fonts/fa-regular-400.d1d7e3b4.woff +0 -0
  34. data/lib/mihari/web/public/static/fonts/fa-solid-900.0c6bfc66.eot +0 -0
  35. data/lib/mihari/web/public/static/fonts/fa-solid-900.b9625119.ttf +0 -0
  36. data/lib/mihari/web/public/static/fonts/fa-solid-900.d745348d.woff +0 -0
  37. data/lib/mihari/web/public/static/fonts/fa-solid-900.d824df7e.woff2 +0 -0
  38. data/lib/mihari/web/public/static/img/fa-brands-400.1d5619cd.svg +3717 -0
  39. data/lib/mihari/web/public/static/img/fa-regular-400.c5d109be.svg +801 -0
  40. data/lib/mihari/web/public/static/img/fa-solid-900.37bc7099.svg +5034 -0
  41. data/lib/mihari/web/public/static/js/app.b5914c39.js +36 -0
  42. data/lib/mihari/web/public/static/js/app.b5914c39.js.map +1 -0
  43. data/mihari.gemspec +3 -2
  44. metadata +41 -7
  45. data/images/overview.png +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eeee7ec511b59cc1e6d07f47df8b2edc29860dbeeeafebc79ba876efe17f2954
4
- data.tar.gz: e3c4fe0b49b20efa5ebdafaecdadbfb35135cbbdf3ff41dd36c68d2eaae644f8
3
+ metadata.gz: a2fe0203f89908abbc21df8d595b7167a561c826b933166e531298b83f67e085
4
+ data.tar.gz: 03a455ebd71f5d3c228041351b6ee8a6a2d89b74a7fc2f0d7609293bc82da7d6
5
5
  SHA512:
6
- metadata.gz: eff2de53ad20576849a81a5ebe96155c6b8747ab4b1775b26816ae6eff7eeb8324f2ad360755e867741a1b6cc778f1dec63d80bbb5439b9e8894536988971183
7
- data.tar.gz: 2b1effec63ad4119f7cb521db866e0e9deb2dca7971036530d4cb16ea4a8669d591981ad5517784da13b98b7a3d3aa732538d040b53590b371d8f51acde849e0
6
+ metadata.gz: aa4a28142eb109d46109d960c1de935ac4632136bc7696b405bfda3d691de2658764e7f796a8e9b1715973e0fc3cc9887997f3ab0dd054e4caa6baf6da40cb8e
7
+ data.tar.gz: 057c5df7efd59ebf285b4a8d6a17a8857d8fa5df7b0c61f4ddbaf67d3688aaa4c34187af88acfe151a27a21b1d33b5e659347968ba6881e98e3b81114d113eb9
data/README.md CHANGED
@@ -14,11 +14,12 @@ Mihari is a framework for continuous OSINT based threat hunting.
14
14
 
15
15
  ## How it works
16
16
 
17
- ![img](https://github.com/ninoseki/mihari/raw/master/images/overview.png)
17
+ ![img](https://github.com/ninoseki/mihari/raw/master/images/overview.jpg)
18
18
 
19
19
  - Mihari makes a query against Shodan, Censys, VirusTotal, SecurityTrails, etc. and extracts artifacts (IP addresses, domains, URLs or hashes).
20
- - Mihari checks whether a DB (SQLite3, PostgreSQL or MySQL) contains the artifacts or not.
20
+ - Mihari checks whether the database (SQLite3, PostgreSQL or MySQL) contains the artifacts or not.
21
21
  - If it doesn't contain the artifacts:
22
+ - Mihari saves artifacts in the database.
22
23
  - Mihari creates an alert on TheHive.
23
24
  - Mihari sends a notification to Slack.
24
25
  - Mihari creates an event on MISP.
@@ -52,6 +53,10 @@ Mihari supports the following services by default.
52
53
 
53
54
  - [Mihari Knowledge Base](https://www.notion.so/Mihari-Knowledge-Base-266994ff61204428ba6cfcebe40b0bd1)
54
55
 
56
+ ## Presentations
57
+
58
+ - [Adversary Infrastructure Tracking with Mihari](https://ninoseki.github.io/presentations/Adversary%20Infrastructure%20Tracking%20with%20Mihari.pdf)
59
+
55
60
  ## License
56
61
 
57
62
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/config.ru ADDED
@@ -0,0 +1,6 @@
1
+ require "./lib/mihari"
2
+
3
+ # set rack env as development
4
+ ENV["RACK_ENV"] ||= "development"
5
+
6
+ run Mihari::App
Binary file
data/lib/mihari.rb CHANGED
@@ -9,6 +9,7 @@ require "yaml"
9
9
  # Mixins
10
10
  require "mihari/mixins/configurable"
11
11
  require "mihari/mixins/configuration"
12
+ require "mihari/mixins/disallowed_data_value"
12
13
  require "mihari/mixins/hash"
13
14
  require "mihari/mixins/refang"
14
15
  require "mihari/mixins/retriable"
@@ -30,6 +31,7 @@ module Mihari
30
31
  setting :censys_secret, ENV["CENSYS_SECRET"]
31
32
  setting :circl_passive_password, ENV["CIRCL_PASSIVE_PASSWORD"]
32
33
  setting :circl_passive_username, ENV["CIRCL_PASSIVE_USERNAME"]
34
+ setting :ipinfo_api_key, ENV["ipinfo_api_key"]
33
35
  setting :misp_api_endpoint, ENV["MISP_API_ENDPOINT"]
34
36
  setting :misp_api_key, ENV["MISP_API_KEY"]
35
37
  setting :onyphe_api_key, ENV["ONYPHE_API_KEY"]
@@ -5,6 +5,8 @@ require "uuidtools"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class Rule < Base
8
+ include Mihari::Mixins::DisallowedDataValue
9
+
8
10
  option :title
9
11
  option :description
10
12
  option :queries
@@ -12,6 +14,7 @@ module Mihari
12
14
  option :id, default: proc {}
13
15
  option :tags, default: proc { [] }
14
16
  option :allowed_data_types, default: proc { ALLOWED_DATA_TYPES }
17
+ option :disallowed_data_values, default: proc { [] }
15
18
 
16
19
  attr_reader :source
17
20
 
@@ -68,12 +71,36 @@ module Mihari
68
71
  # - Uniquefy artifacts by #uniq(&:data)
69
72
  # - Reject an invalid artifact (for just in case)
70
73
  # - Select artifacts with allowed data types
74
+ # - Reject artifacts with disallowed data values
71
75
  #
72
76
  # @return [Array<Mihari::Artifact>]
73
77
  #
74
78
  def normalized_artifacts
75
79
  @normalized_artifacts ||= artifacts.uniq(&:data).select(&:valid?).select do |artifact|
76
80
  allowed_data_types.include? artifact.data_type
81
+ end.reject do |artifact|
82
+ disallowed_data_value? artifact.data
83
+ end
84
+ end
85
+
86
+ #
87
+ # Normalized disallowed data values
88
+ #
89
+ # @return [Array<Regexp, String>]
90
+ #
91
+ def normalized_disallowed_data_values
92
+ @normalized_disallowed_data_values ||= disallowed_data_values.map { |v| normalize_disallowed_data_value v }
93
+ end
94
+
95
+ #
96
+ # Check whether a value is a disallowed data value or not
97
+ #
98
+ # @return [Boolean]
99
+ #
100
+ def disallowed_data_value?(value)
101
+ normalized_disallowed_data_values.any? do |disallowed_data_value|
102
+ return value == disallowed_data_value if disallowed_data_value.is_a?(String)
103
+ return disallowed_data_value.match?(value) if disallowed_data_value.is_a?(Regexp)
77
104
  end
78
105
  end
79
106
 
@@ -22,6 +22,10 @@ require "mihari/commands/json"
22
22
  module Mihari
23
23
  module CLI
24
24
  class Analyzer < Base
25
+ class_option :ignore_old_artifacts, type: :boolean, default: false, desc: "Whether to ignore old artifacts from checking or not."
26
+ class_option :ignore_threshold, type: :numeric, default: 0, desc: "Number of days to define whether an artifact is old or not."
27
+ class_option :config, type: :string, desc: "Path to the config file"
28
+
25
29
  include Mihari::Commands::BinaryEdge
26
30
  include Mihari::Commands::Censys
27
31
  include Mihari::Commands::CIRCL
@@ -12,11 +12,6 @@ module Mihari
12
12
  include Mihari::Mixins::Hash
13
13
  include Mixins::Utils
14
14
 
15
- class_option :config, type: :string, desc: "Path to the config file"
16
-
17
- class_option :ignore_old_artifacts, type: :boolean, default: false, desc: "Whether to ignore old artifacts from checking or not. Only affects with analyze commands."
18
- class_option :ignore_threshold, type: :numeric, default: 0, desc: "Number of days to define whether an artifact is old or not. Only affects with analyze commands."
19
-
20
15
  class << self
21
16
  def exit_on_failure?
22
17
  true
@@ -5,10 +5,10 @@ require "colorize"
5
5
  module Mihari
6
6
  module Commands
7
7
  module Initialization
8
- def self.included(thor)
9
- include Mixins::Configuration
10
- include Mixins::Rule
8
+ include Mixins::Configuration
9
+ include Mixins::Rule
11
10
 
11
+ def self.included(thor)
12
12
  thor.class_eval do
13
13
  desc "config", "Create a config file"
14
14
  method_option :filename, type: :string, default: "mihari.yml"
@@ -37,7 +37,7 @@ module Mihari
37
37
 
38
38
  initialize_rule_yaml filename
39
39
 
40
- puts "The rule file is created as #{filename}.".colorize(:blue)
40
+ puts "The rule file is initialized as #{filename}.".colorize(:blue)
41
41
  end
42
42
  end
43
43
  end
@@ -8,17 +8,27 @@ module Mihari
8
8
  def self.included(thor)
9
9
  thor.class_eval do
10
10
  desc "search [RULE]", "Search by a rule"
11
+ method_option :config, type: :string, desc: "Path to the config file"
11
12
  def search_by_rule(rule)
12
13
  # convert str(YAML) to hash or str(path/YAML file) to hash
13
14
  rule = load_rule(rule)
14
15
 
15
16
  # validate rule schema
16
- validate_rule rule
17
+ rule = validate_rule(rule)
17
18
 
18
- analyzer = build_rule_analyzer(**rule)
19
+ analyzer = build_rule_analyzer(
20
+ title: rule[:title],
21
+ description: rule[:description],
22
+ queries: rule[:queries],
23
+ tags: rule[:tags],
24
+ allowed_data_types: rule[:allowed_data_types],
25
+ disallowed_data_values: rule[:disallowed_data_values],
26
+ source: rule[:source],
27
+ id: rule[:id]
28
+ )
19
29
 
20
- ignore_old_artifacts = options["ignore_old_artifacts"] || false
21
- ignore_threshold = options["ignore_threshold"] || 0
30
+ ignore_old_artifacts = rule[:ignore_old_artifacts]
31
+ ignore_threshold = rule[:ignore_threshold]
22
32
 
23
33
  with_error_handling do
24
34
  run_rule_analyzer analyzer, ignore_old_artifacts: ignore_old_artifacts, ignore_threshold: ignore_threshold
@@ -37,13 +47,15 @@ module Mihari
37
47
  # @param [Array<Hash>] queries
38
48
  # @param [Array<String>, nil] tags
39
49
  # @param [Array<String>, nil] allowed_data_types
50
+ # @param [Array<String>, nil] disallowed_data_values
40
51
  # @param [String, nil] source
41
52
  #
42
53
  # @return [Mihari::Analyzers::Rule]
43
54
  #
44
- def build_rule_analyzer(title:, description:, queries:, tags: nil, allowed_data_types: nil, source: nil)
55
+ def build_rule_analyzer(title:, description:, queries:, tags: nil, allowed_data_types: nil, disallowed_data_values: nil, source: nil, id: nil)
45
56
  tags = [] if tags.nil?
46
57
  allowed_data_types = ALLOWED_DATA_TYPES if allowed_data_types.nil?
58
+ disallowed_data_values = [] if disallowed_data_values.nil?
47
59
 
48
60
  Analyzers::Rule.new(
49
61
  title: title,
@@ -51,7 +63,9 @@ module Mihari
51
63
  tags: tags,
52
64
  queries: queries,
53
65
  allowed_data_types: allowed_data_types,
54
- source: source
66
+ disallowed_data_values: disallowed_data_values,
67
+ source: source,
68
+ id: id
55
69
  )
56
70
  end
57
71
 
@@ -59,8 +73,6 @@ module Mihari
59
73
  # Run rule analyzer
60
74
  #
61
75
  # @param [Mihari::Analyzer::Rule] analyzer
62
- # @param [Boolean] ignore_old_artifacts
63
- # @param [Integer] ignore_threshold
64
76
  #
65
77
  # @return [nil]
66
78
  #
@@ -8,11 +8,16 @@ module Mihari
8
8
  desc "web", "Launch the web app"
9
9
  method_option :port, type: :numeric, default: 9292
10
10
  method_option :host, type: :string, default: "localhost"
11
+ method_option :config, type: :string, desc: "Path to the config file"
11
12
  def web
12
13
  port = options["port"].to_i || 9292
13
14
  host = options["host"] || "localhost"
14
15
 
15
16
  load_configuration
17
+
18
+ # set rack env as production
19
+ ENV["RACK_ENV"] ||= "production"
20
+
16
21
  Mihari::App.run!(port: port, host: host)
17
22
  end
18
23
  end
@@ -0,0 +1,42 @@
1
+ require "mem"
2
+
3
+ module Mihari
4
+ module Mixins
5
+ module DisallowedDataValue
6
+ include Mem
7
+
8
+ #
9
+ # Normalize a value as a disallowed data value
10
+ #
11
+ # @param [String] value Data value
12
+ #
13
+ # @return [String, Regexp] Normalized value
14
+ #
15
+ def normalize_disallowed_data_value(value)
16
+ return value if !value.start_with?("/") || !value.end_with?("/")
17
+
18
+ # if a value is surrounded by slashes, take it as a regexp
19
+ value_without_slashes = value[1..-2]
20
+ Regexp.compile value_without_slashes
21
+ end
22
+
23
+ memoize :normalize_disallowed_data_value
24
+
25
+ #
26
+ # Check whetehr a value is valid format as a disallowed data value
27
+ #
28
+ # @param [String] value Data value
29
+ #
30
+ # @return [Boolean] true if it is valid, otherwise false
31
+ #
32
+ def valid_disallowed_data_value?(value)
33
+ begin
34
+ normalize_disallowed_data_value value
35
+ rescue RegexpError
36
+ return false
37
+ end
38
+ true
39
+ end
40
+ end
41
+ end
42
+ end
@@ -20,10 +20,12 @@ module Mihari
20
20
  end
21
21
 
22
22
  #
23
- # Validate rule schema
23
+ # Validate rule schema and return a normalized rule
24
24
  #
25
25
  # @param [Hash] rule
26
26
  #
27
+ # @return [Hash]
28
+ #
27
29
  def validate_rule(rule)
28
30
  error_message = "Failed to parse the input as a rule!"
29
31
 
@@ -42,6 +44,8 @@ module Mihari
42
44
  puts error_message.colorize(:red)
43
45
  raise ArgumentError, "Invalid rule schema"
44
46
  end
47
+
48
+ result.to_h
45
49
  end
46
50
 
47
51
  #
@@ -18,8 +18,8 @@ module Mihari
18
18
  # @param [String, nil] source
19
19
  # @param [String, nil] tag_name
20
20
  # @param [String, nil] title
21
- # @param [String, nil] from_at
22
- # @param [String, nil] to_at
21
+ # @param [DateTime, nil] from_at
22
+ # @param [DateTime, nil] to_at
23
23
  # @param [Integer, nil] limit
24
24
  # @param [Integer, nil] page
25
25
  #
@@ -34,7 +34,15 @@ module Mihari
34
34
 
35
35
  offset = (page - 1) * limit
36
36
 
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)
37
+ relation = build_relation(
38
+ artifact_data: artifact_data,
39
+ title: title,
40
+ description: description,
41
+ source: source,
42
+ tag_name: tag_name,
43
+ from_at: from_at,
44
+ to_at: to_at
45
+ )
38
46
 
39
47
  alerts = relation.limit(limit).offset(offset).order(id: :desc)
40
48
 
@@ -54,13 +62,21 @@ module Mihari
54
62
  # @param [String, nil] source
55
63
  # @param [String, nil] tag_name
56
64
  # @param [String, nil] title
57
- # @param [String, nil] from_at
58
- # @param [String, nil] to_at
65
+ # @param [DateTime, nil] from_at
66
+ # @param [DateTime, nil] to_at
59
67
  #
60
68
  # @return [Integer]
61
69
  #
62
70
  def count(artifact_data: nil, description: nil, source: nil, tag_name: nil, title: nil, from_at: nil, to_at: nil)
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)
71
+ relation = build_relation(
72
+ artifact_data: artifact_data,
73
+ title: title,
74
+ description: description,
75
+ source: source,
76
+ tag_name: tag_name,
77
+ from_at: from_at,
78
+ to_at: to_at
79
+ )
64
80
  relation.distinct("alerts.id").count
65
81
  end
66
82
 
@@ -68,11 +84,9 @@ module Mihari
68
84
 
69
85
  def build_relation(artifact_data: nil, title: nil, description: nil, source: nil, tag_name: nil, from_at: nil, to_at: nil)
70
86
  relation = self
71
- relation = joins(:tags) if tag_name
72
- relation = joins(:artifacts) if artifact_data
73
87
 
74
- relation = relation.where(artifacts: { data: artifact_data }) if artifact_data
75
- relation = relation.where(tags: { name: tag_name }) if tag_name
88
+ relation = relation.joins(:artifacts).where(artifacts: { data: artifact_data }) if artifact_data
89
+ relation = relation.joins(:tags).where(tags: { name: tag_name }) if tag_name
76
90
 
77
91
  relation = relation.where(source: source) if source
78
92
  relation = relation.where(title: title) if title
@@ -13,6 +13,7 @@ module Mihari
13
13
  optional(:censys_secret).value(:string)
14
14
  optional(:circl_passive_password).value(:string)
15
15
  optional(:circl_passive_username).value(:string)
16
+ optional(:ipinfo_api_key).value(:string)
16
17
  optional(:misp_api_endpoint).value(:string)
17
18
  optional(:misp_api_key).value(:string)
18
19
  optional(:onyphe_api_key).value(:string)
@@ -63,10 +63,24 @@ module Mihari
63
63
  required(:queries).value(:array).each { Analyzer | Spyse | ZoomEye | Urlscan | Crtsh }
64
64
 
65
65
  optional(:allowed_data_types).value(array[DataTypes]).default(ALLOWED_DATA_TYPES)
66
+ optional(:disallowed_data_values).value(array[:string]).default([])
67
+
68
+ optional(:ignore_old_artifacts).value(:bool).default(false)
69
+ optional(:ignore_threshold).value(:integer).default(0)
66
70
  end
67
71
 
68
72
  class RuleContract < Dry::Validation::Contract
73
+ include Mihari::Mixins::DisallowedDataValue
74
+
69
75
  params(Rule)
76
+
77
+ rule(:disallowed_data_values) do
78
+ value.each do |v|
79
+ unless valid_disallowed_data_value?(v)
80
+ key.failure("#{v} is not a valid format.")
81
+ end
82
+ end
83
+ end
70
84
  end
71
85
  end
72
86
  end
@@ -2,7 +2,7 @@ title: ... # String (required)
2
2
  description: ... # String (required)
3
3
 
4
4
  id: ... # String (optional)
5
- author: .. # String (optional)
5
+ author: ... # String (optional)
6
6
  created_on: <%= Date.today %> # Date (optional)
7
7
  updated_on: <%= Date.today %> # Date (optional)
8
8
 
@@ -13,6 +13,10 @@ allowed_data_types: # Array<String> (Optional, defaults to ["hash", "ip", "domai
13
13
  - domain
14
14
  - url
15
15
  - mail
16
+ disallowed_data_values: [] # Array<String> (Optional, defaults to [])
17
+
18
+ ignore_old_artifacts: true # Whether to ignore old artifacts from checking or not (Optional, defaults to true)
19
+ ignore_threshold: 0 # Number of days to define whether an artifact is old or not (Optional, defaults to 0)
16
20
 
17
21
  queries: # Array<Hash> (required)
18
22
  - analyzer: shodan # String (required)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "3.2.0"
4
+ VERSION = "3.5.0"
5
5
  end