mihari 3.2.0 → 3.5.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 (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