mihari 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/README.md +2 -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/web_alerts.png +0 -0
  9. data/images/web_config.png +0 -0
  10. data/lib/mihari/analyzers/base.rb +10 -1
  11. data/lib/mihari/analyzers/circl.rb +3 -3
  12. data/lib/mihari/analyzers/onyphe.rb +2 -2
  13. data/lib/mihari/cli.rb +9 -2
  14. data/lib/mihari/commands/config.rb +7 -1
  15. data/lib/mihari/commands/dnstwister.rb +2 -0
  16. data/lib/mihari/commands/json.rb +6 -0
  17. data/lib/mihari/commands/onyphe.rb +2 -0
  18. data/lib/mihari/commands/passive_dns.rb +2 -0
  19. data/lib/mihari/commands/securitytrails.rb +2 -0
  20. data/lib/mihari/commands/shodan.rb +2 -0
  21. data/lib/mihari/commands/spyse.rb +2 -0
  22. data/lib/mihari/commands/urlscan.rb +2 -0
  23. data/lib/mihari/commands/virustotal.rb +2 -0
  24. data/lib/mihari/commands/web.rb +2 -0
  25. data/lib/mihari/commands/zoomeye.rb +2 -0
  26. data/lib/mihari/config.rb +1 -1
  27. data/lib/mihari/models/artifact.rb +13 -2
  28. data/lib/mihari/status.rb +1 -9
  29. data/lib/mihari/version.rb +1 -1
  30. data/lib/mihari/web/app.rb +20 -137
  31. data/lib/mihari/web/controllers/alerts_controller.rb +66 -0
  32. data/lib/mihari/web/controllers/artifacts_controller.rb +26 -0
  33. data/lib/mihari/web/controllers/command_controller.rb +27 -0
  34. data/lib/mihari/web/controllers/config_controller.rb +15 -0
  35. data/lib/mihari/web/controllers/sources_controller.rb +14 -0
  36. data/lib/mihari/web/controllers/tags_controller.rb +30 -0
  37. data/lib/mihari/web/helpers/json.rb +51 -0
  38. data/lib/mihari/web/public/index.html +2 -2
  39. data/lib/mihari/web/public/redoc-static.html +519 -0
  40. data/lib/mihari/web/public/static/js/{app.280cbdb7.js → app.bcc595df.js} +3 -3
  41. data/lib/mihari/web/public/static/js/app.bcc595df.js.map +1 -0
  42. data/mihari.gemspec +2 -1
  43. metadata +41 -19
  44. 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: c5859ab6b5d161359ea07c4af14de087013befa3b51d91844a60127137882e28
4
+ data.tar.gz: 481f34619d14e04f3187a9d2c3a05b3a1d109c7809622accebc47b100a506bc4
5
5
  SHA512:
6
- metadata.gz: 97f8237d491778739e307e15d0b5b96fba510a548a0a42ae1760e41469cef7fad551b6e876025d7daed58e449c77bee6d3573d659c85e4b2886712229e2ee7ef
7
- data.tar.gz: 2d5f9125f2039ce7d8727661a8f71f1a7c0b9021a0eaf613242598371fb6066234fa5a8959528190bc7275b35da026932700d13929cfd9f94aa04eded6881951
6
+ metadata.gz: 13693510f6f7d3560d207bb97dec50bb4851514e3b35918a82924e4e73d18e4bdcf963c3cd8928cd17559187b69419c8aa2a8e11e9d857965473dabfdf12ac59
7
+ data.tar.gz: 8786ad5973687848a89c8737eb92e52dd636e912b6af0ddf339dccccbff28c42f55e4111d1967b1d9a7b8aca07618788e82d98c24a56ca54102b36c84231d21e
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
@@ -12,6 +12,8 @@ Mihari is a framework for continuous OSINT based threat hunting.
12
12
 
13
13
  ## How it works
14
14
 
15
+ ![img](https://github.com/ninoseki/mihari/raw/master/images/overview.png)
16
+
15
17
  - Mihari makes a query against Shodan, Censys, VirusTotal, SecurityTrails, etc. and extracts artifacts (IP addresses, domains, URLs or hashes).
16
18
  - Mihari checks whether a DB (SQLite3, PostgreSQL or MySQL) contains the artifacts or not.
17
19
  - If it doesn't contain the artifacts:
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
Binary file
Binary file
@@ -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
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,11 +99,15 @@ 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)
@@ -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
@@ -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
@@ -58,7 +58,7 @@ module Mihari
58
58
 
59
59
  def initialize_yaml(filename)
60
60
  keys = new.instance_variables.map do |key|
61
- key.to_s[1..-1]
61
+ key.to_s[1..]
62
62
  end
63
63
 
64
64
  config = keys.map do |key|
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record"
4
+ require "active_record/filter"
5
+ require "active_support/core_ext/integer/time"
6
+ require "active_support/core_ext/numeric/time"
4
7
 
5
8
  class ArtifactValidator < ActiveModel::Validator
6
9
  def validate(record)
@@ -20,8 +23,16 @@ module Mihari
20
23
  self.data_type = TypeChecker.type(data)
21
24
  end
22
25
 
23
- def unique?
24
- self.class.find_by(data: data).nil?
26
+ def unique?(ignore_old_artifacts: false, ignore_threshold: 0)
27
+ artifact = self.class.where(data: data).order(created_at: :desc).first
28
+ return true if artifact.nil?
29
+
30
+ return false unless ignore_old_artifacts
31
+
32
+ days_before = (-ignore_threshold).days.from_now
33
+ # if an artifact is created before {ignore_threshold} days, ignore it
34
+ # within {ignore_threshold} days, do not ignore it
35
+ artifact.created_at < days_before
25
36
  end
26
37
  end
27
38
  end
data/lib/mihari/status.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Mihari
4
4
  class Status
5
5
  def check
6
- statuses.transform_values { |value| convert(**value) }
6
+ statuses
7
7
  end
8
8
 
9
9
  def self.check
@@ -12,14 +12,6 @@ module Mihari
12
12
 
13
13
  private
14
14
 
15
- def convert(is_configured:, values:, type:)
16
- {
17
- is_configured: is_configured,
18
- values: values,
19
- type: type
20
- }
21
- end
22
-
23
15
  def statuses
24
16
  (Mihari.analyzers + Mihari.emitters).map do |klass|
25
17
  name = klass.to_s.split("::").last.to_s
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "2.1.0"
4
+ VERSION = "2.2.0"
5
5
  end
@@ -1,16 +1,21 @@
1
- require "awrence"
2
- require "colorize"
1
+ # frozen_string_literal: true
2
+
3
3
  require "launchy"
4
4
  require "rack"
5
- require "safe_shell"
5
+ require "rack/handler/puma"
6
6
  require "sinatra"
7
- require "sinatra/json"
8
- require "sinatra/reloader"
7
+
8
+ require "mihari/web/helpers/json"
9
+
10
+ require "mihari/web/controllers/alerts_controller"
11
+ require "mihari/web/controllers/artifacts_controller"
12
+ require "mihari/web/controllers/command_controller"
13
+ require "mihari/web/controllers/config_controller"
14
+ require "mihari/web/controllers/sources_controller"
15
+ require "mihari/web/controllers/tags_controller"
9
16
 
10
17
  module Mihari
11
18
  class App < Sinatra::Base
12
- register Sinatra::Reloader
13
-
14
19
  set :root, File.dirname(__FILE__)
15
20
  set :public_folder, File.join(root, "public")
16
21
 
@@ -18,140 +23,18 @@ module Mihari
18
23
  send_file File.join(settings.public_folder, "index.html")
19
24
  end
20
25
 
21
- get "/api/alerts" do
22
- page = params["page"] || 1
23
- page = page.to_i
24
- limit = 10
25
-
26
- artifact_data = params["artifact"]
27
- description = params["description"]
28
- source = params["source"]
29
- tag_name = params["tag"]
30
- title = params["title"]
31
-
32
- from_at = params["from_at"] || params["fromAt"]
33
- from_at = DateTime.parse(from_at) if from_at
34
- to_at = params["to_at"] || params["toAt"]
35
- to_at = DateTime.parse(to_at) if to_at
36
-
37
- alerts = Mihari::Alert.search(
38
- artifact_data: artifact_data,
39
- description: description,
40
- from_at: from_at,
41
- limit: limit,
42
- page: page,
43
- source: source,
44
- tag_name: tag_name,
45
- title: title,
46
- to_at: to_at
47
- )
48
- total = Mihari::Alert.count(
49
- artifact_data: artifact_data,
50
- description: description,
51
- from_at: from_at,
52
- source: source,
53
- tag_name: tag_name,
54
- title: title,
55
- to_at: to_at
56
- )
57
-
58
- json = { alerts: alerts, total: total, current_page: page, page_size: limit }
59
- json json.to_camelback_keys
60
- end
61
-
62
- delete "/api/alerts/:id" do
63
- id = params["id"]
64
- id = id.to_i
65
-
66
- begin
67
- alert = Mihari::Alert.find(id)
68
- alert.destroy
69
-
70
- status 204
71
- body ""
72
- rescue ActiveRecord::RecordNotFound
73
- status 404
74
-
75
- message = { message: "ID:#{id} is not found" }
76
- json message
77
- end
78
- end
79
-
80
- delete "/api/artifacts/:id" do
81
- id = params["id"]
82
- id = id.to_i
83
-
84
- begin
85
- alert = Mihari::Artifact.find(id)
86
- alert.delete
87
-
88
- status 204
89
- body ""
90
- rescue ActiveRecord::RecordNotFound
91
- status 404
92
-
93
- message = { message: "ID:#{id} is not found" }
94
- json message
95
- end
96
- end
97
-
98
- delete "/api/tags/:name" do
99
- name = params["name"]
100
-
101
- begin
102
- Mihari::Tag.where(name: name).destroy_all
103
-
104
- status 204
105
- body ""
106
- rescue ActiveRecord::RecordNotFound
107
- status 404
108
-
109
- message = { message: "Name:#{name} is not found" }
110
- json message
111
- end
112
- end
113
-
114
- get "/api/sources" do
115
- tags = Mihari::Alert.distinct.pluck(:source)
116
- json tags
117
- end
118
-
119
- get "/api/tags" do
120
- tags = Mihari::Tag.distinct.pluck(:name)
121
- json tags
122
- end
123
-
124
- get "/api/config" do
125
- report = Status.check
126
-
127
- json report.to_camelback_keys
128
- end
129
-
130
- post "/api/command" do
131
- payload = JSON.parse(request.body.read)
132
-
133
- command = payload["command"]
134
- if command.nil?
135
- status 400
136
- return json( { message: "command is required" })
137
- end
138
-
139
- command = command.split
140
-
141
- output = SafeShell.execute("mihari", *command)
142
- success = $?.success?
143
-
144
- json({ output: output, success: success })
145
- end
26
+ use Mihari::Controllers::AlertsController
27
+ use Mihari::Controllers::ArtifactsController
28
+ use Mihari::Controllers::CommandController
29
+ use Mihari::Controllers::ConfigController
30
+ use Mihari::Controllers::SourcesController
31
+ use Mihari::Controllers::TagsController
146
32
 
147
33
  class << self
148
34
  def run!(port: 9292, host: "localhost")
149
- url = "http://#{host}:#{port}"
150
-
151
- puts "The app will be available at #{url}.".colorize(:blue)
152
- puts "(Press Ctrl+C to quit)".colorize(:blue)
35
+ url = "http://#{host}:#{port}"
153
36
 
154
- Rack::Handler::WEBrick.run self, Port: port, Host: host do |server|
37
+ Rack::Handler::Puma.run self, Port: port, Host: host do |server|
155
38
  Launchy.open url
156
39
 
157
40
  [:INT, :TERM].each do |sig|