mihari 3.3.0 → 3.6.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/config.ru +1 -0
  4. data/lib/mihari/analyzers/base.rb +34 -6
  5. data/lib/mihari/analyzers/censys.rb +37 -9
  6. data/lib/mihari/analyzers/onyphe.rb +34 -9
  7. data/lib/mihari/analyzers/shodan.rb +26 -5
  8. data/lib/mihari/cli/analyzer.rb +4 -0
  9. data/lib/mihari/cli/base.rb +0 -5
  10. data/lib/mihari/commands/init.rb +4 -4
  11. data/lib/mihari/commands/search.rb +17 -8
  12. data/lib/mihari/commands/web.rb +1 -0
  13. data/lib/mihari/{constraints.rb → constants.rb} +0 -0
  14. data/lib/mihari/database.rb +42 -3
  15. data/lib/mihari/mixins/rule.rb +5 -1
  16. data/lib/mihari/models/alert.rb +28 -10
  17. data/lib/mihari/models/artifact.rb +55 -0
  18. data/lib/mihari/models/autonomous_system.rb +9 -0
  19. data/lib/mihari/models/dns.rb +53 -0
  20. data/lib/mihari/models/geolocation.rb +9 -0
  21. data/lib/mihari/models/reverse_dns.rb +24 -0
  22. data/lib/mihari/models/whois.rb +119 -0
  23. data/lib/mihari/schemas/configuration.rb +1 -0
  24. data/lib/mihari/schemas/rule.rb +5 -15
  25. data/lib/mihari/serializers/alert.rb +6 -4
  26. data/lib/mihari/serializers/artifact.rb +11 -2
  27. data/lib/mihari/serializers/autonomous_system.rb +9 -0
  28. data/lib/mihari/serializers/dns.rb +11 -0
  29. data/lib/mihari/serializers/geolocation.rb +11 -0
  30. data/lib/mihari/serializers/reverse_dns.rb +11 -0
  31. data/lib/mihari/serializers/tag.rb +4 -2
  32. data/lib/mihari/serializers/whois.rb +11 -0
  33. data/lib/mihari/structs/censys.rb +92 -0
  34. data/lib/mihari/structs/onyphe.rb +47 -0
  35. data/lib/mihari/structs/shodan.rb +53 -0
  36. data/lib/mihari/templates/rule.yml.erb +3 -0
  37. data/lib/mihari/types.rb +21 -0
  38. data/lib/mihari/version.rb +1 -1
  39. data/lib/mihari/web/app.rb +2 -0
  40. data/lib/mihari/web/controllers/alerts_controller.rb +3 -4
  41. data/lib/mihari/web/controllers/artifacts_controller.rb +46 -2
  42. data/lib/mihari/web/controllers/ip_address_controller.rb +36 -0
  43. data/lib/mihari/web/controllers/sources_controller.rb +2 -2
  44. data/lib/mihari/web/controllers/tags_controller.rb +3 -1
  45. data/lib/mihari/web/public/index.html +1 -1
  46. data/lib/mihari/web/public/redoc-static.html +12 -10
  47. data/lib/mihari/web/public/static/fonts/fa-brands-400.1a575a41.woff +0 -0
  48. data/lib/mihari/web/public/static/fonts/fa-brands-400.513aa607.ttf +0 -0
  49. data/lib/mihari/web/public/static/fonts/fa-brands-400.592643a8.eot +0 -0
  50. data/lib/mihari/web/public/static/fonts/fa-brands-400.ed311c7a.woff2 +0 -0
  51. data/lib/mihari/web/public/static/fonts/fa-regular-400.766913e6.ttf +0 -0
  52. data/lib/mihari/web/public/static/fonts/fa-regular-400.b0e2db3b.eot +0 -0
  53. data/lib/mihari/web/public/static/fonts/fa-regular-400.b91d376b.woff2 +0 -0
  54. data/lib/mihari/web/public/static/fonts/fa-regular-400.d1d7e3b4.woff +0 -0
  55. data/lib/mihari/web/public/static/fonts/fa-solid-900.0c6bfc66.eot +0 -0
  56. data/lib/mihari/web/public/static/fonts/fa-solid-900.b9625119.ttf +0 -0
  57. data/lib/mihari/web/public/static/fonts/fa-solid-900.d745348d.woff +0 -0
  58. data/lib/mihari/web/public/static/fonts/fa-solid-900.d824df7e.woff2 +0 -0
  59. data/lib/mihari/web/public/static/img/fa-brands-400.1d5619cd.svg +3717 -0
  60. data/lib/mihari/web/public/static/img/fa-regular-400.c5d109be.svg +801 -0
  61. data/lib/mihari/web/public/static/img/fa-solid-900.37bc7099.svg +5034 -0
  62. data/lib/mihari/web/public/static/js/app.8e3e5150.js +36 -0
  63. data/lib/mihari/web/public/static/js/app.8e3e5150.js.map +1 -0
  64. data/lib/mihari/web/public/static/js/app.b5914c39.js +36 -0
  65. data/lib/mihari/web/public/static/js/app.b5914c39.js.map +1 -0
  66. data/lib/mihari.rb +25 -4
  67. data/mihari.gemspec +9 -2
  68. metadata +140 -8
@@ -3,7 +3,16 @@
3
3
  require "active_model_serializers"
4
4
 
5
5
  module Mihari
6
- class ArtifactSerializer < ActiveModel::Serializer
7
- attributes :id, :data, :data_type, :source
6
+ module Serializers
7
+ class ArtifactSerializer < ActiveModel::Serializer
8
+ attributes :id, :data, :data_type, :source
9
+
10
+ has_one :autonomous_system, serializer: AutonomousSystemSerializer
11
+ has_one :geolocation, serializer: GeolocationSerializer
12
+ has_one :whois_record, serializer: WhoisRecordSerializer
13
+
14
+ has_many :dns_records, serializer: DnsRecordSerializer
15
+ has_many :reverse_dns_names, serializer: ReverseDnsNameSerializer
16
+ end
8
17
  end
9
18
  end
@@ -0,0 +1,9 @@
1
+ require "active_model_serializers"
2
+
3
+ module Mihari
4
+ module Serializers
5
+ class AutonomousSystemSerializer < ActiveModel::Serializer
6
+ attributes :asn
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model_serializers"
4
+
5
+ module Mihari
6
+ module Serializers
7
+ class DnsRecordSerializer < ActiveModel::Serializer
8
+ attributes :resource, :value
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model_serializers"
4
+
5
+ module Mihari
6
+ module Serializers
7
+ class GeolocationSerializer < ActiveModel::Serializer
8
+ attributes :country, :country_code
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model_serializers"
4
+
5
+ module Mihari
6
+ module Serializers
7
+ class ReverseDnsNameSerializer < ActiveModel::Serializer
8
+ attributes :name
9
+ end
10
+ end
11
+ end
@@ -3,7 +3,9 @@
3
3
  require "active_model_serializers"
4
4
 
5
5
  module Mihari
6
- class TagSerializer < ActiveModel::Serializer
7
- attributes :id, :name
6
+ module Serializers
7
+ class TagSerializer < ActiveModel::Serializer
8
+ attributes :id, :name
9
+ end
8
10
  end
9
11
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model_serializers"
4
+
5
+ module Mihari
6
+ module Serializers
7
+ class WhoisRecordSerializer < ActiveModel::Serializer
8
+ attributes :domain, :created_on, :updated_on, :expires_on, :registrar, :contacts
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,92 @@
1
+ require "json"
2
+ require "dry/struct"
3
+
4
+ module Mihari
5
+ module Structs
6
+ module Censys
7
+ class AutonomousSystem < Dry::Struct
8
+ attribute :asn, Types::Int
9
+
10
+ def self.from_dynamic!(d)
11
+ d = Types::Hash[d]
12
+ new(
13
+ asn: d.fetch("asn")
14
+ )
15
+ end
16
+ end
17
+
18
+ class Location < Dry::Struct
19
+ attribute :country, Types::String.optional
20
+ attribute :country_code, Types::String.optional
21
+
22
+ def self.from_dynamic!(d)
23
+ d = Types::Hash[d]
24
+ new(
25
+ country: d["country"],
26
+ country_code: d["country_code"]
27
+ )
28
+ end
29
+ end
30
+
31
+ class Hit < Dry::Struct
32
+ attribute :ip, Types::String
33
+ attribute :location, Location
34
+ attribute :autonomous_system, AutonomousSystem
35
+
36
+ def self.from_dynamic!(d)
37
+ d = Types::Hash[d]
38
+ new(
39
+ ip: d.fetch("ip"),
40
+ location: Location.from_dynamic!(d.fetch("location")),
41
+ autonomous_system: AutonomousSystem.from_dynamic!(d.fetch("autonomous_system"))
42
+ )
43
+ end
44
+ end
45
+
46
+ class Links < Dry::Struct
47
+ attribute :next, Types::String
48
+ attribute :prev, Types::String
49
+
50
+ def self.from_dynamic!(d)
51
+ d = Types::Hash[d]
52
+ new(
53
+ next: d.fetch("next"),
54
+ prev: d.fetch("prev")
55
+ )
56
+ end
57
+ end
58
+
59
+ class Result < Dry::Struct
60
+ attribute :query, Types::String
61
+ attribute :total, Types::Int
62
+ attribute :hits, Types.Array(Hit)
63
+ attribute :links, Links
64
+
65
+ def self.from_dynamic!(d)
66
+ d = Types::Hash[d]
67
+ new(
68
+ query: d.fetch("query"),
69
+ total: d.fetch("total"),
70
+ hits: d.fetch("hits", []).map { |x| Hit.from_dynamic!(x) },
71
+ links: Links.from_dynamic!(d.fetch("links"))
72
+ )
73
+ end
74
+ end
75
+
76
+ class Response < Dry::Struct
77
+ attribute :code, Types::Int
78
+ attribute :status, Types::String
79
+ attribute :result, Result
80
+
81
+ def self.from_dynamic!(d)
82
+ d = Types::Hash[d]
83
+ new(
84
+ code: d.fetch("code"),
85
+ status: d.fetch("status"),
86
+ result: Result.from_dynamic!(d.fetch("result"))
87
+ )
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,47 @@
1
+ require "json"
2
+ require "dry/struct"
3
+
4
+ module Mihari
5
+ module Structs
6
+ module Onyphe
7
+ class Result < Dry::Struct
8
+ attribute :asn, Types::String
9
+ attribute :country_code, Types::String
10
+ attribute :ip, Types::String
11
+
12
+ def self.from_dynamic!(d)
13
+ d = Types::Hash[d]
14
+ new(
15
+ asn: d.fetch("asn"),
16
+ ip: d.fetch("ip"),
17
+ # Onyphe's country = 2-letter country code
18
+ country_code: d.fetch("country")
19
+ )
20
+ end
21
+ end
22
+
23
+ class Response < Dry::Struct
24
+ attribute :count, Types::Int
25
+ attribute :error, Types::Int
26
+ attribute :max_page, Types::Int
27
+ attribute :page, Types::String
28
+ attribute :results, Types.Array(Result)
29
+ attribute :status, Types::String
30
+ attribute :total, Types::Int
31
+
32
+ def self.from_dynamic!(d)
33
+ d = Types::Hash[d]
34
+ new(
35
+ count: d.fetch("count"),
36
+ error: d.fetch("error"),
37
+ max_page: d.fetch("max_page"),
38
+ page: d.fetch("page"),
39
+ results: d.fetch("results").map { |x| Result.from_dynamic!(x) },
40
+ status: d.fetch("status"),
41
+ total: d.fetch("total")
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,53 @@
1
+ require "json"
2
+ require "dry/struct"
3
+
4
+ module Mihari
5
+ module Structs
6
+ module Shodan
7
+ class Location < Dry::Struct
8
+ attribute :country_code, Types::String
9
+ attribute :country_name, Types::String
10
+
11
+ def self.from_dynamic!(d)
12
+ d = Types::Hash[d]
13
+ new(
14
+ country_code: d.fetch("country_code"),
15
+ country_name: d.fetch("country_name")
16
+ )
17
+ end
18
+ end
19
+
20
+ class Match < Dry::Struct
21
+ attribute :asn, Types::String
22
+ attribute :hostnames, Types.Array(Types::String)
23
+ attribute :location, Location
24
+ attribute :domains, Types.Array(Types::String)
25
+ attribute :ip_str, Types::String
26
+
27
+ def self.from_dynamic!(d)
28
+ d = Types::Hash[d]
29
+ new(
30
+ asn: d.fetch("asn"),
31
+ hostnames: d.fetch("hostnames"),
32
+ location: Location.from_dynamic!(d.fetch("location")),
33
+ domains: d.fetch("domains"),
34
+ ip_str: d.fetch("ip_str")
35
+ )
36
+ end
37
+ end
38
+
39
+ class Result < Dry::Struct
40
+ attribute :matches, Types.Array(Match)
41
+ attribute :total, Types::Int
42
+
43
+ def self.from_dynamic!(d)
44
+ d = Types::Hash[d]
45
+ new(
46
+ matches: d.fetch("matches", []).map { |x| Match.from_dynamic!(x) },
47
+ total: d.fetch("total")
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -15,6 +15,9 @@ allowed_data_types: # Array<String> (Optional, defaults to ["hash", "ip", "domai
15
15
  - mail
16
16
  disallowed_data_values: [] # Array<String> (Optional, defaults to [])
17
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)
20
+
18
21
  queries: # Array<Hash> (required)
19
22
  - analyzer: shodan # String (required)
20
23
  query: ... # String (required)
@@ -0,0 +1,21 @@
1
+ require "dry/types"
2
+
3
+ module Mihari
4
+ module Types
5
+ include Dry.Types()
6
+
7
+ Int = Strict::Integer
8
+ Nil = Strict::Nil
9
+ Hash = Strict::Hash
10
+ String = Strict::String
11
+ Double = Strict::Float | Strict::Integer
12
+
13
+ DataTypes = Types::String.enum(*ALLOWED_DATA_TYPES)
14
+
15
+ AnalyzerTypes = Types::String.enum(
16
+ "binaryedge", "censys", "circl", "dnpedia", "dnstwister",
17
+ "onyphe", "otx", "passivetotal", "pulsedive", "securitytrails",
18
+ "shodan", "virustotal"
19
+ )
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "3.3.0"
4
+ VERSION = "3.6.0"
5
5
  end
@@ -14,6 +14,7 @@ require "mihari/web/controllers/analyzers_controller"
14
14
  require "mihari/web/controllers/artifacts_controller"
15
15
  require "mihari/web/controllers/command_controller"
16
16
  require "mihari/web/controllers/config_controller"
17
+ require "mihari/web/controllers/ip_address_controller"
17
18
  require "mihari/web/controllers/sources_controller"
18
19
  require "mihari/web/controllers/tags_controller"
19
20
 
@@ -31,6 +32,7 @@ module Mihari
31
32
  use Mihari::Controllers::ArtifactsController
32
33
  use Mihari::Controllers::CommandController
33
34
  use Mihari::Controllers::ConfigController
35
+ use Mihari::Controllers::IPAddressController
34
36
  use Mihari::Controllers::SourcesController
35
37
  use Mihari::Controllers::TagsController
36
38
 
@@ -26,9 +26,7 @@ module Mihari
26
26
  title = params["title"]
27
27
 
28
28
  from_at = params["from_at"] || params["fromAt"]
29
- from_at = DateTime.parse(from_at) if from_at
30
29
  to_at = params["to_at"] || params["toAt"]
31
- to_at = DateTime.parse(to_at) if to_at
32
30
 
33
31
  alerts = Mihari::Alert.search(
34
32
  artifact_data: artifact_data,
@@ -55,8 +53,9 @@ module Mihari
55
53
  end
56
54
 
57
55
  delete "/api/alerts/:id" do
58
- id = params["id"]
59
- id = id.to_i
56
+ param :id, Integer, required: true
57
+
58
+ id = params["id"].to_i
60
59
 
61
60
  begin
62
61
  alert = Mihari::Alert.find(id)
@@ -3,9 +3,53 @@
3
3
  module Mihari
4
4
  module Controllers
5
5
  class ArtifactsController < BaseController
6
+ get "/api/artifacts/:id" do
7
+ param :id, Integer, required: true
8
+
9
+ id = params["id"].to_i
10
+
11
+ begin
12
+ artifact = Mihari::Artifact.includes(
13
+ :autonomous_system,
14
+ :geolocation,
15
+ :whois_record,
16
+ :dns_records,
17
+ :reverse_dns_names
18
+ ).find(id)
19
+ rescue ActiveRecord::RecordNotFound
20
+ status 404
21
+
22
+ return json({ message: "ID:#{id} is not found" })
23
+ end
24
+
25
+ # TODO: improve queries
26
+ alert_ids = Mihari::Artifact.where(data: artifact.data).pluck(:alert_id)
27
+ tag_ids = Mihari::Tagging.where(alert_id: alert_ids).pluck(:tag_id)
28
+ tag_names = Mihari::Tag.where(id: tag_ids).distinct.pluck(:name)
29
+
30
+ artifact_json = Serializers::ArtifactSerializer.new(artifact).as_json
31
+
32
+ # convert reverse DNS names into an array of string
33
+ # also change it as nil if it is empty
34
+ reverse_dns_names = (artifact_json[:reverse_dns_names] || []).filter_map { |v| v[:name] }
35
+ reverse_dns_names = nil if reverse_dns_names.empty?
36
+ artifact_json[:reverse_dns_names] = reverse_dns_names
37
+
38
+ # change DNS records as nil if it is empty
39
+ dns_records = artifact_json[:dns_records] || []
40
+ dns_records = nil if dns_records.empty?
41
+ artifact_json[:dns_records] = dns_records
42
+
43
+ # set tags
44
+ artifact_json[:tags] = tag_names
45
+
46
+ json artifact_json
47
+ end
48
+
6
49
  delete "/api/artifacts/:id" do
7
- id = params["id"]
8
- id = id.to_i
50
+ param :id, Integer, required: true
51
+
52
+ id = params["id"].to_i
9
53
 
10
54
  begin
11
55
  alert = Mihari::Artifact.find(id)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http"
4
+ require "json"
5
+
6
+ module Mihari
7
+ module Controllers
8
+ class IPAddressController < BaseController
9
+ def query(ip)
10
+ headers = {}
11
+ token = Mihari.config.ipinfo_api_key
12
+ unless token.nil?
13
+ headers[:authorization] = "Bearer #{token}"
14
+ end
15
+
16
+ res = HTTP.headers(headers).get("https://ipinfo.io/#{ip}/json")
17
+ JSON.parse res.to_s
18
+ end
19
+
20
+ get "/api/ip_addresses/:ip" do
21
+ param :ip, String, required: true
22
+
23
+ ip = params["ip"].to_s
24
+
25
+ begin
26
+ data = query(ip)
27
+ json data
28
+ rescue HTTP::Error
29
+ status 404
30
+
31
+ json({ message: "IP:#{ip} is not found" })
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end