mihari 2.1.0 → 2.4.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/README.md +8 -0
  4. data/bin/console +1 -0
  5. data/docker/Dockerfile +1 -1
  6. data/examples/ipinfo_hosted_domains.rb +1 -1
  7. data/images/{eyecatch.png → overview.png} +0 -0
  8. data/images/tines.png +0 -0
  9. data/images/web_alerts.png +0 -0
  10. data/images/web_config.png +0 -0
  11. data/lib/mihari.rb +1 -0
  12. data/lib/mihari/analyzers/base.rb +10 -1
  13. data/lib/mihari/analyzers/circl.rb +3 -3
  14. data/lib/mihari/analyzers/onyphe.rb +2 -2
  15. data/lib/mihari/analyzers/urlscan.rb +1 -6
  16. data/lib/mihari/analyzers/zoomeye.rb +2 -2
  17. data/lib/mihari/cli.rb +12 -3
  18. data/lib/mihari/commands/config.rb +7 -1
  19. data/lib/mihari/commands/dnstwister.rb +2 -0
  20. data/lib/mihari/commands/json.rb +6 -0
  21. data/lib/mihari/commands/onyphe.rb +2 -0
  22. data/lib/mihari/commands/passive_dns.rb +2 -0
  23. data/lib/mihari/commands/securitytrails.rb +2 -0
  24. data/lib/mihari/commands/shodan.rb +2 -0
  25. data/lib/mihari/commands/spyse.rb +2 -0
  26. data/lib/mihari/commands/urlscan.rb +2 -2
  27. data/lib/mihari/commands/virustotal.rb +2 -0
  28. data/lib/mihari/commands/web.rb +2 -0
  29. data/lib/mihari/commands/zoomeye.rb +2 -0
  30. data/lib/mihari/config.rb +5 -4
  31. data/lib/mihari/emitters/slack.rb +0 -3
  32. data/lib/mihari/emitters/webhook.rb +60 -0
  33. data/lib/mihari/models/artifact.rb +13 -2
  34. data/lib/mihari/notifiers/slack.rb +0 -1
  35. data/lib/mihari/status.rb +1 -9
  36. data/lib/mihari/version.rb +1 -1
  37. data/lib/mihari/web/app.rb +22 -137
  38. data/lib/mihari/web/controllers/alerts_controller.rb +75 -0
  39. data/lib/mihari/web/controllers/artifacts_controller.rb +24 -0
  40. data/lib/mihari/web/controllers/base_controller.rb +22 -0
  41. data/lib/mihari/web/controllers/command_controller.rb +26 -0
  42. data/lib/mihari/web/controllers/config_controller.rb +13 -0
  43. data/lib/mihari/web/controllers/sources_controller.rb +12 -0
  44. data/lib/mihari/web/controllers/tags_controller.rb +28 -0
  45. data/lib/mihari/web/helpers/json.rb +53 -0
  46. data/lib/mihari/web/public/index.html +2 -2
  47. data/lib/mihari/web/public/redoc-static.html +519 -0
  48. data/lib/mihari/web/public/static/js/{app.280cbdb7.js → app.cccddb2b.js} +3 -3
  49. data/lib/mihari/web/public/static/js/app.cccddb2b.js.map +1 -0
  50. data/mihari.gemspec +10 -6
  51. metadata +96 -30
  52. data/lib/mihari/slack_monkeypatch.rb +0 -16
  53. data/lib/mihari/web/public/static/js/app.280cbdb7.js.map +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dff04f8065ceff1c6b9fa68bd606db9cc4789ed034a330019143a69c4ec2c037
4
- data.tar.gz: d307e3e48eedf5d17245da14a20bd578468e03a70b2ba302fcc4ea68aab5651c
3
+ metadata.gz: 388f48a9001d38fd83f4a6d527d7c5826a490be1d339596d174a9f619a78cb0c
4
+ data.tar.gz: 7e0a6e2fcbe9ad1792c21472b8be7706a86b09fbbce951edb1829a76493aaedc
5
5
  SHA512:
6
- metadata.gz: 97f8237d491778739e307e15d0b5b96fba510a548a0a42ae1760e41469cef7fad551b6e876025d7daed58e449c77bee6d3573d659c85e4b2886712229e2ee7ef
7
- data.tar.gz: 2d5f9125f2039ce7d8727661a8f71f1a7c0b9021a0eaf613242598371fb6066234fa5a8959528190bc7275b35da026932700d13929cfd9f94aa04eded6881951
6
+ metadata.gz: dfcde6c4fa80ae12c56606157c6800c7e321cef71ed3e4aa9250805ea51126c74a19b3f73040630d169966fb17d834d8c45b37cc6f7baa808d7eea3e7c585fb9
7
+ data.tar.gz: d477cdcc4b4075e7671263f32ed5e81daad42e499eded5dffcecfba2d7568b779e99010a64a271a652db3f928800506cb6a4c7cf4060816742d4bb8d5bbec86a
data/.rubocop.yml CHANGED
@@ -4,6 +4,9 @@
4
4
  require:
5
5
  - rubocop-performance
6
6
 
7
+ AllCops:
8
+ NewCops: enable
9
+
7
10
  Style/Alias:
8
11
  Enabled: false
9
12
  StyleGuide: https://relaxed.ruby.style/#stylealias
@@ -151,5 +154,8 @@ Lint/AssignmentInCondition:
151
154
  Layout/LineLength:
152
155
  Enabled: false
153
156
 
157
+ Style/StringLiteralsInInterpolation:
158
+ Enabled: false
159
+
154
160
  Metrics:
155
161
  Enabled: false
data/README.md CHANGED
@@ -8,10 +8,14 @@
8
8
 
9
9
  ![img](https://github.com/ninoseki/mihari/raw/master/images/logo.png)
10
10
 
11
+ [![](images/tines.png)](https://tines.io?utm_source=github&utm_medium=sponsorship&utm_campaign=ninoseki)
12
+
11
13
  Mihari is a framework for continuous OSINT based threat hunting.
12
14
 
13
15
  ## How it works
14
16
 
17
+ ![img](https://github.com/ninoseki/mihari/raw/master/images/overview.png)
18
+
15
19
  - Mihari makes a query against Shodan, Censys, VirusTotal, SecurityTrails, etc. and extracts artifacts (IP addresses, domains, URLs or hashes).
16
20
  - Mihari checks whether a DB (SQLite3, PostgreSQL or MySQL) contains the artifacts or not.
17
21
  - If it doesn't contain the artifacts:
@@ -59,3 +63,7 @@ See [Usage](https://github.com/ninoseki/mihari/wiki/Usage) for more information.
59
63
  ## License
60
64
 
61
65
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
66
+
67
+ ## Acknowledgement
68
+
69
+ Mihari is proudly supported by [Tines.io](https://tines.io?utm_source=github&utm_medium=sponsorship&utm_campaign=ninoseki), The SOAR Platform for Enterprise Security Teams.
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "mihari"
data/docker/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM ruby:3.0.0-alpine3.13
1
+ FROM ruby:3.0.1-alpine3.13
2
2
 
3
3
  RUN apk --no-cache add git build-base ruby-dev sqlite-dev postgresql-dev mysql-client mysql-dev \
4
4
  && gem install pg mysql2 \
@@ -34,7 +34,7 @@ module Mihari
34
34
  uri = URI("#{IPINFO_API_ENDPOINT}/domains/#{ip}?token=#{token}")
35
35
  res = uri.read
36
36
  json = JSON.parse(res)
37
- json.dig("domains") || []
37
+ json["domains"] || []
38
38
  end
39
39
  end
40
40
  end
File without changes
data/images/tines.png ADDED
Binary file
Binary file
Binary file
data/lib/mihari.rb CHANGED
@@ -78,6 +78,7 @@ require "mihari/emitters/misp"
78
78
  require "mihari/emitters/slack"
79
79
  require "mihari/emitters/stdout"
80
80
  require "mihari/emitters/the_hive"
81
+ require "mihari/emitters/webhook"
81
82
 
82
83
  require "mihari/status"
83
84
 
@@ -8,6 +8,13 @@ module Mihari
8
8
  include Configurable
9
9
  include Retriable
10
10
 
11
+ attr_accessor :ignore_old_artifacts, :ignore_threshold
12
+
13
+ def initialize
14
+ @ignore_old_artifacts = false
15
+ @ignore_threshold = 0
16
+ end
17
+
11
18
  # @return [Array<String>, Array<Mihari::Artifact>]
12
19
  def artifacts
13
20
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
@@ -61,7 +68,9 @@ module Mihari
61
68
 
62
69
  # @return [Array<Mihari::Artifact>]
63
70
  def unique_artifacts
64
- @unique_artifacts ||= normalized_artifacts.select(&:unique?)
71
+ @unique_artifacts ||= normalized_artifacts.select do |artifact|
72
+ artifact.unique?(ignore_old_artifacts: ignore_old_artifacts, ignore_threshold: ignore_threshold)
73
+ end
65
74
  end
66
75
 
67
76
  def set_unique_artifacts
@@ -46,14 +46,14 @@ module Mihari
46
46
  def passive_dns_lookup
47
47
  results = api.dns.query(@query)
48
48
  results.map do |result|
49
- type = result.dig("rrtype")
50
- type == "A" ? result.dig("rdata") : nil
49
+ type = result["rrtype"]
50
+ type == "A" ? result["rdata"] : nil
51
51
  end.compact.uniq
52
52
  end
53
53
 
54
54
  def passive_ssl_lookup
55
55
  result = api.ssl.cquery(@query)
56
- seen = result.dig("seen") || []
56
+ seen = result["seen"] || []
57
57
  seen.uniq
58
58
  end
59
59
  end
@@ -21,10 +21,10 @@ module Mihari
21
21
  return [] unless results
22
22
 
23
23
  flat_results = results.map do |result|
24
- result.dig("results")
24
+ result["results"]
25
25
  end.flatten.compact
26
26
 
27
- flat_results.map { |result| result.dig("ip") }.compact.uniq
27
+ flat_results.map { |result| result["ip"] }.compact.uniq
28
28
  end
29
29
 
30
30
  private
@@ -5,16 +5,14 @@ require "urlscan"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class Urlscan < Base
8
- attr_reader :title, :description, :query, :tags, :filter, :target_type, :use_pro, :use_similarity
8
+ attr_reader :title, :description, :query, :tags, :target_type, :use_similarity
9
9
 
10
10
  def initialize(
11
11
  query,
12
12
  description: nil,
13
- filter: nil,
14
13
  tags: [],
15
14
  target_type: "url",
16
15
  title: nil,
17
- use_pro: false,
18
16
  use_similarity: false
19
17
  )
20
18
  super()
@@ -24,9 +22,7 @@ module Mihari
24
22
  @description = description || "query = #{query}"
25
23
  @tags = tags
26
24
 
27
- @filter = filter
28
25
  @target_type = target_type
29
- @use_pro = use_pro
30
26
  @use_similarity = use_similarity
31
27
 
32
28
  raise InvalidInputError, "type should be url, domain or ip." unless valid_target_type?
@@ -54,7 +50,6 @@ module Mihari
54
50
 
55
51
  def search
56
52
  return api.pro.similar(query) if use_similarity
57
- return api.pro.search(query: query, filter: filter, size: 10_000) if use_pro
58
53
 
59
54
  api.search(query, size: 10_000)
60
55
  end
@@ -37,11 +37,11 @@ module Mihari
37
37
  end
38
38
 
39
39
  def config_keys
40
- %w[zoomeye_password zoomeye_username]
40
+ %w[zoomeye_api_key]
41
41
  end
42
42
 
43
43
  def api
44
- @api ||= ::ZoomEye::API.new(username: Mihari.config.zoomeye_username, password: Mihari.config.zoomeye_password)
44
+ @api ||= ::ZoomEye::API.new(api_key: Mihari.config.zoomeye_api_key)
45
45
  end
46
46
 
47
47
  def convert_responses(responses)
data/lib/mihari/cli.rb CHANGED
@@ -33,7 +33,10 @@ require "mihari/commands/web"
33
33
 
34
34
  module Mihari
35
35
  class CLI < Thor
36
- class_option :config, type: :string, desc: "path to the config file"
36
+ class_option :config, type: :string, desc: "Path to the config file"
37
+
38
+ class_option :ignore_old_artifacts, type: :boolean, default: false, desc: "Whether to ignore old artifacts from checking or not. Only affects with analyze commands."
39
+ 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."
37
40
 
38
41
  include Mihari::Commands::BinaryEdge
39
42
  include Mihari::Commands::Censys
@@ -96,16 +99,22 @@ module Mihari
96
99
  options = normalize_options(options)
97
100
 
98
101
  analyzer = analyzer_class.new(query, **options)
102
+
103
+ analyzer.ignore_old_artifacts = options[:ignore_old_artifacts] || false
104
+ analyzer.ignore_threshold = options[:ignore_threshold] || 0
105
+
99
106
  analyzer.run
100
107
  end
101
108
 
102
109
  def symbolize_hash_keys(hash)
103
- hash.map { |k, v| [k.to_sym, v] }.to_h
110
+ hash.transform_keys(&:to_sym)
104
111
  end
105
112
 
106
113
  def normalize_options(options)
107
114
  # Delete :config because it is not intended to use for running an analyzer
108
- options.delete(:config)
115
+ [:config, :ignore_old_artifacts, :ignore_threshold].each do |ignore_key|
116
+ options.delete(ignore_key)
117
+ end
109
118
  options
110
119
  end
111
120
 
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "colorize"
4
+
1
5
  module Mihari
2
6
  module Commands
3
7
  module Config
@@ -8,11 +12,13 @@ module Mihari
8
12
  def init_config
9
13
  filename = options["filename"]
10
14
 
11
- if File.exist?(filename) && !(yes? "#{filename} exists. Do you want to overwrite it? (y/n)")
15
+ warning = "#{filename} exists. Do you want to overwrite it? (y/n)"
16
+ if File.exist?(filename) && !(yes? warning)
12
17
  return
13
18
  end
14
19
 
15
20
  Mihari::Config.initialize_yaml filename
21
+ puts "The config file is initialized as #{filename}.".colorize(:blue)
16
22
  end
17
23
  end
18
24
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module DNSTwister
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module JSON
@@ -18,6 +20,10 @@ module Mihari
18
20
  tags = json["tags"] || []
19
21
 
20
22
  basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, source: "json", tags: tags)
23
+
24
+ basic.ignore_old_artifacts = options["ignore_old_artifacts"] || false
25
+ basic.ignore_threshold = options["ignore_threshold"] || 0
26
+
21
27
  basic.run
22
28
  end
23
29
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module Onyphe
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module PassiveDNS
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module SecurityTrails
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module Shodan
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module Spyse
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module Urlscan
@@ -7,9 +9,7 @@ module Mihari
7
9
  method_option :title, type: :string, desc: "title"
8
10
  method_option :description, type: :string, desc: "description"
9
11
  method_option :tags, type: :array, desc: "tags"
10
- method_option :filter, type: :string, desc: "filter for urlscan pro search"
11
12
  method_option :target_type, type: :string, default: "url", desc: "target type to fetch from lookup results (target type should be 'url', 'domain' or 'ip')"
12
- method_option :use_pro, type: :boolean, default: false, desc: "use pro search API or not"
13
13
  method_option :use_similarity, type: :boolean, default: false, desc: "use similarity API or not"
14
14
  def urlscan(query)
15
15
  with_error_handling do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module VirusTotal
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module Web
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mihari
2
4
  module Commands
3
5
  module ZoomEye
data/lib/mihari/config.rb CHANGED
@@ -4,7 +4,7 @@ require "yaml"
4
4
 
5
5
  module Mihari
6
6
  class Config
7
- attr_accessor :binaryedge_api_key, :censys_id, :censys_secret, :circl_passive_password, :circl_passive_username, :misp_api_endpoint, :misp_api_key, :onyphe_api_key, :otx_api_key, :passivetotal_api_key, :passivetotal_username, :pulsedive_api_key, :securitytrails_api_key, :shodan_api_key, :slack_channel, :slack_webhook_url, :spyse_api_key, :thehive_api_endpoint, :thehive_api_key, :urlscan_api_key, :virustotal_api_key, :zoomeye_password, :zoomeye_username, :database
7
+ attr_accessor :binaryedge_api_key, :censys_id, :censys_secret, :circl_passive_password, :circl_passive_username, :misp_api_endpoint, :misp_api_key, :onyphe_api_key, :otx_api_key, :passivetotal_api_key, :passivetotal_username, :pulsedive_api_key, :securitytrails_api_key, :shodan_api_key, :slack_channel, :slack_webhook_url, :spyse_api_key, :thehive_api_endpoint, :thehive_api_key, :urlscan_api_key, :virustotal_api_key, :zoomeye_api_key, :webhook_url, :webhook_use_json_body, :database
8
8
 
9
9
  def initialize
10
10
  load_from_env
@@ -32,8 +32,9 @@ module Mihari
32
32
  @thehive_api_key = ENV["THEHIVE_API_KEY"]
33
33
  @urlscan_api_key = ENV["URLSCAN_API_KEY"]
34
34
  @virustotal_api_key = ENV["VIRUSTOTAL_API_KEY"]
35
- @zoomeye_password = ENV["ZOOMEYE_PASSWORD"]
36
- @zoomeye_username = ENV["ZOOMEYE_USERNAME"]
35
+ @zoomeye_api_key = ENV["ZOOMEYE_API_KEY"]
36
+ @webhook_url = ENV["WEBHOOK_URL"]
37
+ @webhook_use_json_body = ENV["WEBHOOK_USE_JSON_BODY"]
37
38
 
38
39
  @database = ENV["DATABASE"] || "mihari.db"
39
40
  end
@@ -58,7 +59,7 @@ module Mihari
58
59
 
59
60
  def initialize_yaml(filename)
60
61
  keys = new.instance_variables.map do |key|
61
- key.to_s[1..-1]
62
+ key.to_s[1..]
62
63
  end
63
64
 
64
65
  config = keys.map do |key|
@@ -2,9 +2,6 @@
2
2
 
3
3
  require "slack-notifier"
4
4
  require "digest/sha2"
5
- require "mem"
6
-
7
- require "mihari/slack_monkeypatch"
8
5
 
9
6
  module Mihari
10
7
  module Emitters
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Mihari
8
+ module Emitters
9
+ class Webhook < Base
10
+ # @return [true, false]
11
+ def valid?
12
+ webhook_url?
13
+ end
14
+
15
+ def emit(title:, description:, artifacts:, source:, tags:)
16
+ return if artifacts.empty?
17
+
18
+ uri = URI(Mihari.config.webhook_url)
19
+ data = {
20
+ title: title,
21
+ description: description,
22
+ artifacts: artifacts.map(&:data),
23
+ source: source,
24
+ tags: tags
25
+ }
26
+
27
+ if use_json_body
28
+ Net::HTTP.post(uri, data.to_json, "Content-Type" => "application/json")
29
+ else
30
+ Net::HTTP.post_form(uri, data)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def config_keys
37
+ %w[webhook_url]
38
+ end
39
+
40
+ def webhook_url
41
+ @webhook_url ||= Mihari.config.webhook_url
42
+ end
43
+
44
+ def webhook_url?
45
+ !webhook_url.nil?
46
+ end
47
+
48
+ def use_json_body
49
+ @use_json_body ||= truthy?(Mihari.config.webhook_use_json_body || 'false')
50
+ end
51
+
52
+ def truthy?(value)
53
+ return true if value == "true"
54
+ return true if value == true
55
+
56
+ false
57
+ end
58
+ end
59
+ end
60
+ end