mihari 3.8.1 → 3.10.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +6 -7
  3. data/config.ru +1 -1
  4. data/lib/mihari/analyzers/greynoise.rb +65 -0
  5. data/lib/mihari/analyzers/rule.rb +1 -0
  6. data/lib/mihari/analyzers/shodan.rb +3 -1
  7. data/lib/mihari/cli/analyzer.rb +2 -0
  8. data/lib/mihari/commands/greynoise.rb +21 -0
  9. data/lib/mihari/commands/search.rb +3 -2
  10. data/lib/mihari/commands/web.rb +9 -5
  11. data/lib/mihari/database.rb +1 -1
  12. data/lib/mihari/errors.rb +2 -0
  13. data/lib/mihari/mixins/configuration.rb +12 -2
  14. data/lib/mihari/models/alert.rb +29 -54
  15. data/lib/mihari/models/artifact.rb +3 -0
  16. data/lib/mihari/schemas/configuration.rb +3 -2
  17. data/lib/mihari/structs/alert.rb +45 -0
  18. data/lib/mihari/structs/greynoise.rb +55 -0
  19. data/lib/mihari/structs/ipinfo.rb +3 -4
  20. data/lib/mihari/structs/shodan.rb +2 -2
  21. data/lib/mihari/types.rb +2 -0
  22. data/lib/mihari/version.rb +1 -1
  23. data/lib/mihari/web/api.rb +43 -0
  24. data/lib/mihari/web/app.rb +48 -30
  25. data/lib/mihari/web/endpoints/alerts.rb +74 -0
  26. data/lib/mihari/web/endpoints/artifacts.rb +92 -0
  27. data/lib/mihari/web/endpoints/command.rb +32 -0
  28. data/lib/mihari/web/endpoints/configs.rb +22 -0
  29. data/lib/mihari/web/endpoints/ip_addresses.rb +27 -0
  30. data/lib/mihari/web/endpoints/sources.rb +18 -0
  31. data/lib/mihari/web/endpoints/tags.rb +38 -0
  32. data/lib/mihari/web/entities/alert.rb +23 -0
  33. data/lib/mihari/web/entities/artifact.rb +24 -0
  34. data/lib/mihari/web/entities/autonomous_system.rb +9 -0
  35. data/lib/mihari/web/entities/command.rb +14 -0
  36. data/lib/mihari/web/entities/config.rb +16 -0
  37. data/lib/mihari/web/entities/dns.rb +10 -0
  38. data/lib/mihari/web/entities/geolocation.rb +10 -0
  39. data/lib/mihari/web/entities/ip_address.rb +13 -0
  40. data/lib/mihari/web/entities/message.rb +9 -0
  41. data/lib/mihari/web/entities/reverse_dns.rb +9 -0
  42. data/lib/mihari/web/entities/source.rb +9 -0
  43. data/lib/mihari/web/entities/tag.rb +13 -0
  44. data/lib/mihari/web/entities/whois.rb +16 -0
  45. data/lib/mihari/web/public/grape.rb +73 -0
  46. data/lib/mihari/web/public/index.html +1 -1
  47. data/lib/mihari/web/public/redoc-static.html +53 -27
  48. data/lib/mihari/web/public/static/js/app.0a0cc502.js +21 -0
  49. data/lib/mihari/web/public/static/js/app.0a0cc502.js.map +1 -0
  50. data/lib/mihari/web/public/static/js/app.14008741.js +21 -0
  51. data/lib/mihari/web/public/static/js/app.14008741.js.map +1 -0
  52. data/lib/mihari/web/public/static/js/app.378da3dc.js +50 -0
  53. data/lib/mihari/web/public/static/js/app.378da3dc.js.map +1 -0
  54. data/lib/mihari/web/public/static/js/app.6b636b62.js +50 -0
  55. data/lib/mihari/web/public/static/js/app.6b636b62.js.map +1 -0
  56. data/lib/mihari.rb +8 -14
  57. data/mihari.gemspec +10 -6
  58. data/sig/lib/mihari/analyzers/rule.rbs +1 -1
  59. data/sig/lib/mihari/models/alert.rbs +3 -31
  60. data/sig/lib/mihari/structs/alert.rbs +27 -0
  61. data/sig/lib/mihari/structs/greynoise.rbs +30 -0
  62. data/sig/lib/mihari/structs/shodan.rbs +1 -1
  63. data/sig/lib/mihari/web/app.rbs +2 -2
  64. metadata +150 -76
  65. data/lib/mihari/serializers/alert.rb +0 -14
  66. data/lib/mihari/serializers/artifact.rb +0 -18
  67. data/lib/mihari/serializers/autonomous_system.rb +0 -9
  68. data/lib/mihari/serializers/dns.rb +0 -11
  69. data/lib/mihari/serializers/geolocation.rb +0 -11
  70. data/lib/mihari/serializers/reverse_dns.rb +0 -11
  71. data/lib/mihari/serializers/tag.rb +0 -11
  72. data/lib/mihari/serializers/whois.rb +0 -11
  73. data/lib/mihari/web/controllers/alerts_controller.rb +0 -74
  74. data/lib/mihari/web/controllers/analyzers_controller.rb +0 -38
  75. data/lib/mihari/web/controllers/artifacts_controller.rb +0 -94
  76. data/lib/mihari/web/controllers/base_controller.rb +0 -22
  77. data/lib/mihari/web/controllers/command_controller.rb +0 -26
  78. data/lib/mihari/web/controllers/config_controller.rb +0 -13
  79. data/lib/mihari/web/controllers/ip_address_controller.rb +0 -21
  80. data/lib/mihari/web/controllers/sources_controller.rb +0 -12
  81. data/lib/mihari/web/controllers/tags_controller.rb +0 -30
  82. data/lib/mihari/web/helpers/json.rb +0 -53
@@ -2,46 +2,47 @@
2
2
 
3
3
  require "launchy"
4
4
  require "rack"
5
+ require "rack/contrib"
5
6
  require "rack/handler/puma"
6
- require "sinatra"
7
+ require "rack/cors"
7
8
 
8
- require "mihari/web/helpers/json"
9
+ require "grape"
10
+ require "grape-entity"
11
+ require "grape-swagger"
12
+ require "grape-swagger-entity"
9
13
 
10
- require "mihari/web/controllers/base_controller"
11
-
12
- require "mihari/web/controllers/alerts_controller"
13
- require "mihari/web/controllers/analyzers_controller"
14
- require "mihari/web/controllers/artifacts_controller"
15
- require "mihari/web/controllers/command_controller"
16
- require "mihari/web/controllers/config_controller"
17
- require "mihari/web/controllers/ip_address_controller"
18
- require "mihari/web/controllers/sources_controller"
19
- require "mihari/web/controllers/tags_controller"
14
+ require "mihari/web/api"
20
15
 
21
16
  module Mihari
22
- class App < Sinatra::Base
23
- set :root, File.dirname(__FILE__)
24
- set :public_folder, File.join(root, "public")
25
-
26
- get "/" do
27
- send_file File.join(settings.public_folder, "index.html")
17
+ class App
18
+ def initialize
19
+ @filenames = ["", ".html", "index.html", "/index.html"]
20
+ @rack_static = ::Rack::Static.new(
21
+ -> { [404, {}, []] },
22
+ root: File.expand_path("./public", __dir__),
23
+ urls: ["/"]
24
+ )
28
25
  end
29
26
 
30
- use Mihari::Controllers::AlertsController
31
- use Mihari::Controllers::AnalyzersController
32
- use Mihari::Controllers::ArtifactsController
33
- use Mihari::Controllers::CommandController
34
- use Mihari::Controllers::ConfigController
35
- use Mihari::Controllers::IPAddressController
36
- use Mihari::Controllers::SourcesController
37
- use Mihari::Controllers::TagsController
38
-
39
27
  class << self
40
- def run!(port: 9292, host: "localhost")
28
+ def instance
29
+ @instance ||= Rack::Builder.new do
30
+ use Rack::Cors do
31
+ allow do
32
+ origins "*"
33
+ resource "*", headers: :any, methods: [:get, :post, :put, :delete, :options]
34
+ end
35
+ end
36
+
37
+ run App.new
38
+ end.to_app
39
+ end
40
+
41
+ def run!(port: 9292, host: "localhost", threads: "0:16", verbose: false)
41
42
  url = "http://#{host}:#{port}"
42
43
 
43
- Rack::Handler::Puma.run self, Port: port, Host: host do |server|
44
- Launchy.open url
44
+ Rack::Handler::Puma.run(instance, Port: port, Host: host, Threads: threads, Verbose: verbose) do |server|
45
+ Launchy.open(url) if ENV["RACK_ENV"] != "development"
45
46
 
46
47
  [:INT, :TERM].each do |sig|
47
48
  trap(sig) do
@@ -51,5 +52,22 @@ module Mihari
51
52
  end
52
53
  end
53
54
  end
55
+
56
+ def call(env)
57
+ # api
58
+ api_response = API.call(env)
59
+
60
+ # Check if the App wants us to pass the response along to others
61
+ if api_response[1]["X-Cascade"] == "pass"
62
+ # static files
63
+ request_path = env["PATH_INFO"]
64
+ @filenames.each do |path|
65
+ response = @rack_static.call(env.merge("PATH_INFO" => request_path + path))
66
+ return response if response[0] != 404
67
+ end
68
+ end
69
+
70
+ api_response
71
+ end
54
72
  end
55
73
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Endpoints
5
+ class Alerts < Grape::API
6
+ namespace :alerts do
7
+ desc "Search alerts", {
8
+ is_array: true,
9
+ success: Entities::Alert,
10
+ failure: [{ code: 404, message: "Not found", model: Entities::Message }]
11
+ }
12
+ params do
13
+ optional :page, type: Integer
14
+ optional :artifact, type: String
15
+ optional :description, type: String
16
+ optional :source, type: String
17
+ optional :tag, type: String
18
+
19
+ optional :fromAt, type: DateTime
20
+ optional :toAt, type: DateTime
21
+
22
+ optional :asn, type: Integer
23
+ optional :dnsRecord, type: String
24
+ optional :reverseDnsName, type: String
25
+ end
26
+ get "/" do
27
+ filter = params.to_h.to_snake_keys
28
+
29
+ # set page & limit
30
+ page = filter["page"] || 1
31
+ filter["page"] = page.to_i
32
+
33
+ limit = 10
34
+ filter["limit"] = 10
35
+
36
+ # normalize keys
37
+ filter["artifact_data"] = filter["artifact"]
38
+ filter["tag_name"] = filter["tag"]
39
+
40
+ # symbolize hash keys
41
+ filter = filter.to_h.transform_keys(&:to_sym)
42
+
43
+ search_filter_with_pagenation = Structs::Alert::SearchFilterWithPagination.new(**filter)
44
+ alerts = Mihari::Alert.search(search_filter_with_pagenation)
45
+ total = Mihari::Alert.count(search_filter_with_pagenation.without_pagination)
46
+
47
+ present({ alerts: alerts, total: total, current_page: page, page_size: limit }, with: Entities::AlertsWithPagination)
48
+ end
49
+
50
+ desc "Delete an alert", {
51
+ success: Entities::Message,
52
+ failure: [{ code: 404, message: "Not found", model: Entities::Message }]
53
+ }
54
+ params do
55
+ requires :id, type: Integer
56
+ end
57
+ delete "/:id" do
58
+ id = params["id"].to_i
59
+
60
+ begin
61
+ alert = Mihari::Alert.find(id)
62
+ rescue ActiveRecord::RecordNotFound
63
+ error!({ message: "ID:#{id} is not found" }, 404)
64
+ end
65
+
66
+ alert.destroy
67
+
68
+ status 204
69
+ present({ message: "" }, with: Entities::Message)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Endpoints
5
+ class Artifacts < Grape::API
6
+ namespace :artifacts do
7
+ desc "Get an artifact", {
8
+ success: Entities::Artifact,
9
+ failure: [{ code: 404, message: "Not found", model: Entities::Message }]
10
+ }
11
+ params do
12
+ requires :id, type: Integer
13
+ end
14
+ get "/:id" do
15
+ id = params[:id].to_i
16
+
17
+ begin
18
+ artifact = Mihari::Artifact.includes(
19
+ :autonomous_system,
20
+ :geolocation,
21
+ :whois_record,
22
+ :dns_records,
23
+ :reverse_dns_names
24
+ ).find(id)
25
+ rescue ActiveRecord::RecordNotFound
26
+ error!({ message: "ID:#{id} is not found" }, 404)
27
+ end
28
+
29
+ # TODO: improve queries
30
+ alert_ids = Mihari::Artifact.where(data: artifact.data).pluck(:alert_id)
31
+ tag_ids = Mihari::Tagging.where(alert_id: alert_ids).pluck(:tag_id)
32
+ tag_names = Mihari::Tag.where(id: tag_ids).distinct.pluck(:name)
33
+
34
+ artifact.tags = tag_names
35
+
36
+ present artifact, with: Entities::Artifact
37
+ end
38
+
39
+ desc "Enrich an artifact", {
40
+ success: Entities::Message,
41
+ failure: [{ code: 404, message: "Not found", model: Entities::Message }]
42
+ }
43
+ params do
44
+ requires :id, type: Integer
45
+ end
46
+ get "/:id/enrich" do
47
+ id = params["id"].to_i
48
+
49
+ begin
50
+ artifact = Mihari::Artifact.includes(
51
+ :autonomous_system,
52
+ :geolocation,
53
+ :whois_record,
54
+ :dns_records,
55
+ :reverse_dns_names
56
+ ).find(id)
57
+ rescue ActiveRecord::RecordNotFound
58
+ error!({ message: "ID:#{id} is not found" }, 404)
59
+ end
60
+
61
+ artifact.enrich_all
62
+ artifact.save
63
+
64
+ status 201
65
+ present({ message: "" }, with: Entities::Message)
66
+ end
67
+
68
+ desc "Delete an artifact", {
69
+ success: Entities::Message,
70
+ failure: [{ code: 404, message: "Not found", model: Entities::Message }]
71
+ }
72
+ params do
73
+ requires :id, type: Integer
74
+ end
75
+ delete "/:id" do
76
+ id = params["id"].to_i
77
+
78
+ begin
79
+ alert = Mihari::Artifact.find(id)
80
+ rescue ActiveRecord::RecordNotFound
81
+ error!({ message: "ID:#{id} is not found" }, 404)
82
+ end
83
+
84
+ alert.destroy
85
+
86
+ status 204
87
+ present({ message: "" }, with: Entities::Message)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "safe_shell"
4
+
5
+ module Mihari
6
+ module Endpoints
7
+ class Command < Grape::API
8
+ namespace :command do
9
+ desc "Run a command", {
10
+ success: Entities::CommandResult,
11
+ failure: [{ code: 400, message: "Bad request", model: Entities::Message }]
12
+ }
13
+ params do
14
+ requires :command, type: String, documentation: { param_type: "body" }
15
+ end
16
+ post "/" do
17
+ command = params[:command]
18
+ if command.nil?
19
+ error!({ message: "command is required" }, 400)
20
+ end
21
+
22
+ command = command.split
23
+
24
+ output = SafeShell.execute("mihari", *command)
25
+ success = $?.success?
26
+
27
+ present({ output: output, success: success }, with: Entities::CommandResult)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Endpoints
5
+ class Configs < Grape::API
6
+ namespace :configs do
7
+ desc "Get configs", {
8
+ is_array: true,
9
+ success: Entities::Config
10
+ }
11
+ get "/" do
12
+ statuses = Status.check
13
+
14
+ configs = statuses.map do |key, value|
15
+ { name: key, status: value }
16
+ end
17
+ present(configs, with: Entities::Config)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Endpoints
5
+ class IPAddresses < Grape::API
6
+ namespace :ip_addresses do
7
+ desc "Get an IP address", {
8
+ success: Entities::IPAddress,
9
+ failure: [{ code: 404, message: "Not found", model: Entities::Message }]
10
+ }
11
+ params do
12
+ requires :ip, type: String, regexp: /\A[0-9.]+\z/
13
+ end
14
+ get "/:ip", requirements: { ip: %r{[^/]+} } do
15
+ ip = params[:ip].to_s
16
+
17
+ data = Enrichers::IPInfo.query(ip)
18
+ if data.nil?
19
+ error!({ message: "IP:#{ip} is not found" }, 404)
20
+ else
21
+ present data, with: Entities::IPAddress
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Endpoints
5
+ class Sources < Grape::API
6
+ namespace :sources do
7
+ desc "Get sources", {
8
+ is_array: true,
9
+ success: Entities::Sources
10
+ }
11
+ get "/" do
12
+ sources = Mihari::Alert.distinct.pluck(:source)
13
+ present({ sources: sources }, with: Entities::Sources)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Endpoints
5
+ class Tags < Grape::API
6
+ namespace :tags do
7
+ desc "Get tags", {
8
+ is_array: true,
9
+ success: Entities::Tags
10
+ }
11
+ get "/" do
12
+ tags = Mihari::Tag.distinct.pluck(:name)
13
+ present({ tags: tags }, with: Entities::Tags)
14
+ end
15
+
16
+ desc "Delete a tag", {
17
+ success: Entities::Message,
18
+ failure: [{ code: 404, message: "Not found", model: Entities::Message }]
19
+ }
20
+ params do
21
+ requires :name, type: String
22
+ end
23
+ delete "/:name" do
24
+ name = params[:name].to_s
25
+
26
+ begin
27
+ Mihari::Tag.where(name: name).destroy_all
28
+ rescue ActiveRecord::RecordNotFound
29
+ error!({ message: "Name:#{name} is not found" }, 404)
30
+ end
31
+
32
+ status 204
33
+ present({ message: "" }, with: Entities::Message)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class Alert < Grape::Entity
6
+ expose :id, documentation: { type: Integer, required: true }
7
+ expose :title, documentation: { type: String, required: true }
8
+ expose :description, documentation: { type: String, required: true }
9
+ expose :source, documentation: { type: String, required: true }
10
+ expose :created_at, documentation: { type: DateTime, required: true }, as: :createdAt
11
+
12
+ expose :artifacts, using: Entities::Artifact, documentation: { type: Entities::Artifact, is_array: true }
13
+ expose :tags, using: Entities::Tag, documentation: { type: Entities::Tag, is_array: true, required: true }
14
+ end
15
+
16
+ class AlertsWithPagination < Grape::Entity
17
+ expose :alerts, using: Entities::Alert, documentation: { type: Entities::Alert, is_array: true, required: true }
18
+ expose :total, documentation: { type: Integer, required: true }
19
+ expose :current_page, documentation: { type: Integer, required: true }, as: :currentPage
20
+ expose :page_size, documentation: { type: Integer, required: true }, as: :pageSize
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class Artifact < Grape::Entity
6
+ expose :id, documentation: { type: Integer, required: true }
7
+ expose :data, documentation: { type: String, required: true }
8
+ expose :data_type, documentation: { type: String, required: true }, as: :dataType
9
+ expose :source, documentation: { type: String, required: true }
10
+ expose :tags, documentation: { type: String, is_array: true }
11
+
12
+ expose :autonomous_system, using: Entities::AutonomousSystem, documentation: { type: Entities::AutonomousSystem, required: false }, as: :autonomousSystem
13
+ expose :geolocation, using: Entities::Geolocation, documentation: { type: Entities::Geolocation, required: false }
14
+ expose :whois_record, using: Entities::WhoisRecord, documentation: { type: Entities::WhoisRecord, required: false }, as: :whoisRecord
15
+
16
+ expose :reverse_dns_names, using: Entities::ReverseDnsName, documentation: { type: Entities::ReverseDnsName, is_array: true, required: false }, as: :reverseDnsNames do |status, _options|
17
+ status.reverse_dns_names.length > 0 ? status.reverse_dns_names : nil
18
+ end
19
+ expose :dns_records, using: Entities::DnsRecord, documentation: { type: Entities::DnsRecord, is_array: true, required: false }, as: :dnsRecords do |status, _options|
20
+ status.dns_records.length > 0 ? status.dns_records : nil
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class AutonomousSystem < Grape::Entity
6
+ expose :asn, documentation: { type: Integer, required: true }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class CommandInput < Grape::Entity
6
+ expose :command, documentation: { type: String, required: true }
7
+ end
8
+
9
+ class CommandResult < Grape::Entity
10
+ expose :output, documentation: { type: String, required: true }
11
+ expose :success, documentation: { type: Grape::API::Boolean, required: true }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class ConfigStatus < Grape::Entity
6
+ expose :type, documentation: { type: String, required: true }
7
+ expose :values, documentation: { type: String, is_array: true, required: true }
8
+ expose :is_configured, documentation: { type: Grape::API::Boolean, required: true }, as: :isConfigured
9
+ end
10
+
11
+ class Config < Grape::Entity
12
+ expose :name, documentation: { type: String, required: true }
13
+ expose :status, using: Entities::ConfigStatus, documentation: { type: ConfigStatus, required: true }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class DnsRecord < Grape::Entity
6
+ expose :resource, documentation: { type: String, required: true }
7
+ expose :value, documentation: { type: String, required: true }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class Geolocation < Grape::Entity
6
+ expose :country, documentation: { type: String, required: true }
7
+ expose :country_code, documentation: { type: String, required: true }, as: :countryCode
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class IPAddress < Grape::Entity
6
+ expose :ip, documentation: { type: String, required: true }
7
+ expose :country_code, documentation: { type: String, required: true }, as: :countryCode
8
+ expose :hostname, documentation: { type: String, required: false }
9
+ expose :loc, documentation: { type: String, required: true }
10
+ expose :asn, documentation: { type: Integer, required: false }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class Message < Grape::Entity
6
+ expose :message, documentation: { type: String, required: true }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class ReverseDnsName < Grape::Entity
6
+ expose :name, documentation: { type: String, required: true }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class Sources < Grape::Entity
6
+ expose :sources, documentation: { type: Array[String], required: true }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class Tag < Grape::Entity
6
+ expose :name, documentation: { type: String, required: true }
7
+ end
8
+
9
+ class Tags < Grape::Entity
10
+ expose :tags, documentation: { type: Array[String], required: true }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Entities
5
+ class WhoisRecord < Grape::Entity
6
+ expose :domain, documentation: { type: String, required: true }
7
+ expose :created_on, documentation: { type: Date, required: false }, as: :createdOn
8
+ expose :updated_on, documentation: { type: Date, required: false }, as: :updatedOn
9
+ expose :expires_on, documentation: { type: Date, required: false }, as: :expiresOn
10
+ expose :registrar, documentation: { type: Hash, required: false }
11
+ expose :contacts, documentation: { type: Hash, is_array: true, required: true } do |whois_record, _options|
12
+ whois_record.contacts.map { |h| h.to_camelback_keys }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "launchy"
4
+ require "rack"
5
+ require "rack/contrib"
6
+ require "rack/handler/puma"
7
+ require "rack/cors"
8
+
9
+ require "grape"
10
+ require "grape-swagger"
11
+
12
+ require "mihari/web/apis/ping"
13
+
14
+ module Mihari
15
+ class API < Grape::API
16
+ prefix "api"
17
+ format :json
18
+ mount Apis::Ping
19
+ add_swagger_documentation api_version: "v1"
20
+ end
21
+
22
+ class GrapeApp
23
+ def initialize
24
+ @filenames = ["", ".html", "index.html", "/index.html"]
25
+ @rack_static = ::Rack::Static.new(
26
+ lambda { [404, {}, []] },
27
+ root: File.expand_path("public", __dir__),
28
+ urls: ["/"]
29
+ )
30
+ end
31
+
32
+ class << self
33
+ def instance
34
+ @instance ||= Rack::Builder.new do
35
+ run GrapeApp.new
36
+ end.to_app
37
+ end
38
+
39
+ def run!(port: 9292, host: "localhost", threads: "0:16", verbose: false)
40
+ url = "http://#{host}:#{port}"
41
+
42
+ Rack::Handler::Puma.run(instance, Port: port, Host: host, Threads: threads, Verbose: verbose) do |server|
43
+ p ENV["RACK_ENV"]
44
+ p instance.class
45
+
46
+ Launchy.open(url) if ENV["RACK_ENV"] != "development"
47
+
48
+ [:INT, :TERM].each do |sig|
49
+ trap(sig) do
50
+ server.shutdown
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def call(env)
58
+ # api
59
+ p GrapeApp.instance
60
+ response = API.call(env)
61
+
62
+ # Check if the App wants us to pass the response along to others
63
+ if response[1]["X-Cascade"] == "pass"
64
+ # static files
65
+ request_path = env["PATH_INFO"]
66
+ @filenames.each do |path|
67
+ response = @rack_static.call(env.merge("PATH_INFO" => request_path + path))
68
+ return response if response[0] != 404
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end