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.
- checksums.yaml +4 -4
- data/README.md +4 -0
- data/config.ru +1 -0
- data/lib/mihari/analyzers/base.rb +34 -6
- data/lib/mihari/analyzers/censys.rb +37 -9
- data/lib/mihari/analyzers/onyphe.rb +34 -9
- data/lib/mihari/analyzers/shodan.rb +26 -5
- data/lib/mihari/cli/analyzer.rb +4 -0
- data/lib/mihari/cli/base.rb +0 -5
- data/lib/mihari/commands/init.rb +4 -4
- data/lib/mihari/commands/search.rb +17 -8
- data/lib/mihari/commands/web.rb +1 -0
- data/lib/mihari/{constraints.rb → constants.rb} +0 -0
- data/lib/mihari/database.rb +42 -3
- data/lib/mihari/mixins/rule.rb +5 -1
- data/lib/mihari/models/alert.rb +28 -10
- data/lib/mihari/models/artifact.rb +55 -0
- data/lib/mihari/models/autonomous_system.rb +9 -0
- data/lib/mihari/models/dns.rb +53 -0
- data/lib/mihari/models/geolocation.rb +9 -0
- data/lib/mihari/models/reverse_dns.rb +24 -0
- data/lib/mihari/models/whois.rb +119 -0
- data/lib/mihari/schemas/configuration.rb +1 -0
- data/lib/mihari/schemas/rule.rb +5 -15
- data/lib/mihari/serializers/alert.rb +6 -4
- data/lib/mihari/serializers/artifact.rb +11 -2
- data/lib/mihari/serializers/autonomous_system.rb +9 -0
- data/lib/mihari/serializers/dns.rb +11 -0
- data/lib/mihari/serializers/geolocation.rb +11 -0
- data/lib/mihari/serializers/reverse_dns.rb +11 -0
- data/lib/mihari/serializers/tag.rb +4 -2
- data/lib/mihari/serializers/whois.rb +11 -0
- data/lib/mihari/structs/censys.rb +92 -0
- data/lib/mihari/structs/onyphe.rb +47 -0
- data/lib/mihari/structs/shodan.rb +53 -0
- data/lib/mihari/templates/rule.yml.erb +3 -0
- data/lib/mihari/types.rb +21 -0
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/app.rb +2 -0
- data/lib/mihari/web/controllers/alerts_controller.rb +3 -4
- data/lib/mihari/web/controllers/artifacts_controller.rb +46 -2
- data/lib/mihari/web/controllers/ip_address_controller.rb +36 -0
- data/lib/mihari/web/controllers/sources_controller.rb +2 -2
- data/lib/mihari/web/controllers/tags_controller.rb +3 -1
- data/lib/mihari/web/public/index.html +1 -1
- data/lib/mihari/web/public/redoc-static.html +12 -10
- data/lib/mihari/web/public/static/fonts/fa-brands-400.1a575a41.woff +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.513aa607.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.592643a8.eot +0 -0
- data/lib/mihari/web/public/static/fonts/fa-brands-400.ed311c7a.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.766913e6.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.b0e2db3b.eot +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.b91d376b.woff2 +0 -0
- data/lib/mihari/web/public/static/fonts/fa-regular-400.d1d7e3b4.woff +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.0c6bfc66.eot +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.b9625119.ttf +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.d745348d.woff +0 -0
- data/lib/mihari/web/public/static/fonts/fa-solid-900.d824df7e.woff2 +0 -0
- data/lib/mihari/web/public/static/img/fa-brands-400.1d5619cd.svg +3717 -0
- data/lib/mihari/web/public/static/img/fa-regular-400.c5d109be.svg +801 -0
- data/lib/mihari/web/public/static/img/fa-solid-900.37bc7099.svg +5034 -0
- data/lib/mihari/web/public/static/js/app.8e3e5150.js +36 -0
- data/lib/mihari/web/public/static/js/app.8e3e5150.js.map +1 -0
- data/lib/mihari/web/public/static/js/app.b5914c39.js +36 -0
- data/lib/mihari/web/public/static/js/app.b5914c39.js.map +1 -0
- data/lib/mihari.rb +25 -4
- data/mihari.gemspec +9 -2
- metadata +140 -8
@@ -3,7 +3,16 @@
|
|
3
3
|
require "active_model_serializers"
|
4
4
|
|
5
5
|
module Mihari
|
6
|
-
|
7
|
-
|
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,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)
|
data/lib/mihari/types.rb
ADDED
@@ -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
|
data/lib/mihari/version.rb
CHANGED
data/lib/mihari/web/app.rb
CHANGED
@@ -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
|
59
|
-
|
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
|
8
|
-
|
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
|