mihari 3.3.0 → 3.6.0

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