mihari 2.1.0 → 2.4.0

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