mihari 5.1.0 → 5.1.2

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.gitmodules +0 -3
  3. data/.rubocop.yml +6 -0
  4. data/README.md +0 -1
  5. data/lib/mihari/analyzers/base.rb +32 -27
  6. data/lib/mihari/analyzers/binaryedge.rb +17 -9
  7. data/lib/mihari/analyzers/censys.rb +10 -54
  8. data/lib/mihari/analyzers/circl.rb +7 -6
  9. data/lib/mihari/analyzers/crtsh.rb +12 -7
  10. data/lib/mihari/analyzers/dnstwister.rb +7 -7
  11. data/lib/mihari/analyzers/feed.rb +33 -10
  12. data/lib/mihari/analyzers/greynoise.rb +8 -33
  13. data/lib/mihari/analyzers/onyphe.rb +10 -36
  14. data/lib/mihari/analyzers/otx.rb +4 -3
  15. data/lib/mihari/analyzers/passivetotal.rb +8 -7
  16. data/lib/mihari/analyzers/pulsedive.rb +8 -7
  17. data/lib/mihari/analyzers/rule.rb +0 -1
  18. data/lib/mihari/analyzers/securitytrails.rb +8 -10
  19. data/lib/mihari/analyzers/shodan.rb +16 -90
  20. data/lib/mihari/analyzers/urlscan.rb +16 -6
  21. data/lib/mihari/analyzers/virustotal.rb +8 -6
  22. data/lib/mihari/analyzers/virustotal_intelligence.rb +12 -7
  23. data/lib/mihari/analyzers/zoomeye.rb +13 -10
  24. data/lib/mihari/clients/base.rb +53 -0
  25. data/lib/mihari/clients/binaryedge.rb +38 -0
  26. data/lib/mihari/clients/censys.rb +42 -0
  27. data/lib/mihari/clients/circl.rb +59 -0
  28. data/lib/mihari/clients/crtsh.rb +31 -0
  29. data/lib/mihari/clients/dnstwister.rb +40 -0
  30. data/lib/mihari/clients/greynoise.rb +34 -0
  31. data/lib/mihari/clients/misp.rb +29 -0
  32. data/lib/mihari/clients/onyphe.rb +35 -0
  33. data/lib/mihari/clients/otx.rb +49 -0
  34. data/lib/mihari/clients/passivetotal.rb +69 -0
  35. data/lib/mihari/clients/publsedive.rb +56 -0
  36. data/lib/mihari/clients/securitytrails.rb +94 -0
  37. data/lib/mihari/clients/shodan.rb +41 -0
  38. data/lib/mihari/clients/the_hive.rb +33 -0
  39. data/lib/mihari/clients/urlscan.rb +33 -0
  40. data/lib/mihari/clients/virustotal.rb +62 -0
  41. data/lib/mihari/clients/zoomeye.rb +74 -0
  42. data/lib/mihari/commands/database.rb +1 -6
  43. data/lib/mihari/commands/searcher.rb +1 -2
  44. data/lib/mihari/database.rb +9 -0
  45. data/lib/mihari/emitters/misp.rb +13 -20
  46. data/lib/mihari/emitters/the_hive.rb +3 -5
  47. data/lib/mihari/emitters/webhook.rb +2 -2
  48. data/lib/mihari/feed/reader.rb +14 -11
  49. data/lib/mihari/http.rb +29 -21
  50. data/lib/mihari/mixins/retriable.rb +3 -1
  51. data/lib/mihari/schemas/analyzer.rb +5 -4
  52. data/lib/mihari/structs/censys.rb +62 -0
  53. data/lib/mihari/structs/greynoise.rb +43 -0
  54. data/lib/mihari/structs/onyphe.rb +45 -0
  55. data/lib/mihari/structs/shodan.rb +83 -0
  56. data/lib/mihari/version.rb +1 -1
  57. data/lib/mihari/web/middleware/connection_adapter.rb +1 -3
  58. data/lib/mihari/web/public/assets/{index-63900d73.js → index-7d0fb8c4.js} +2 -2
  59. data/lib/mihari/web/public/index.html +1 -1
  60. data/lib/mihari/web/public/redoc-static.html +2 -2
  61. data/lib/mihari.rb +21 -2
  62. data/mihari.gemspec +15 -23
  63. metadata +55 -264
  64. data/lib/mihari/analyzers/clients/otx.rb +0 -36
  65. data/lib/mihari/analyzers/dnpedia.rb +0 -37
  66. data/lib/mihari/mixins/database.rb +0 -16
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ class ZoomEye < Base
6
+ attr_reader :api_key
7
+
8
+ #
9
+ # @param [String] base_url
10
+ # @param [String, nil] api_key
11
+ # @param [Hash] headers
12
+ #
13
+ def initialize(base_url = "https://api.zoomeye.org", api_key:, headers: {})
14
+ raise(ArgumentError, "'api_key' argument is required") unless api_key
15
+
16
+ headers["api-key"] = api_key
17
+ super(base_url, headers: headers)
18
+ end
19
+
20
+ #
21
+ # Search the Host devices
22
+ #
23
+ # @param [String] query Query string
24
+ # @param [Integer, nil] page The page number to paging(default:1)
25
+ # @param [String, nil] facets A comma-separated list of properties to get summary information on query
26
+ #
27
+ # @return [Hash]
28
+ #
29
+ def host_search(query, page: nil, facets: nil)
30
+ params = {
31
+ query: query,
32
+ page: page,
33
+ facets: facets
34
+ }.compact
35
+
36
+ _get("/host/search", params: params)
37
+ end
38
+
39
+ #
40
+ # Search the Web technologies
41
+ #
42
+ # @param [String] query Query string
43
+ # @param [Integer, nil] page The page number to paging(default:1)
44
+ # @param [String, nil] facets A comma-separated list of properties to get summary information on query
45
+ #
46
+ # @return [Hash]
47
+ #
48
+ def web_search(query, page: nil, facets: nil)
49
+ params = {
50
+ query: query,
51
+ page: page,
52
+ facets: facets
53
+ }.compact
54
+
55
+ _get("/web/search", params: params)
56
+ end
57
+
58
+ private
59
+
60
+ #
61
+ # @param [String] path
62
+ # @param [Hash] params
63
+ #
64
+ # @return [Hash, nil]
65
+ #
66
+ def _get(path, params: {})
67
+ res = get(path, params: params)
68
+ JSON.parse(res.body.to_s)
69
+ rescue HTTPError
70
+ nil
71
+ end
72
+ end
73
+ end
74
+ end
@@ -3,8 +3,6 @@
3
3
  module Mihari
4
4
  module Commands
5
5
  module Database
6
- include Mixins::Database
7
-
8
6
  def self.included(thor)
9
7
  thor.class_eval do
10
8
  desc "migrate", "Migrate DB schemas"
@@ -12,14 +10,11 @@ module Mihari
12
10
  #
13
11
  # @param [String] direction
14
12
  #
15
- #
16
13
  def migrate(direction = "up")
17
14
  verbose = options["verbose"]
18
15
  ActiveRecord::Migration.verbose = verbose
19
16
 
20
- with_db_connection do
21
- Mihari::Database.migrate(direction.to_sym)
22
- end
17
+ Mihari::Database.with_db_connection { Mihari::Database.migrate(direction.to_sym) }
23
18
  end
24
19
  end
25
20
  end
@@ -3,7 +3,6 @@
3
3
  module Mihari
4
4
  module Commands
5
5
  module Searcher
6
- include Mixins::Database
7
6
  include Mixins::ErrorNotification
8
7
 
9
8
  def self.included(thor)
@@ -16,7 +15,7 @@ module Mihari
16
15
  # @param [String] path_or_id
17
16
  #
18
17
  def search(path_or_id)
19
- with_db_connection do
18
+ Mihari::Database.with_db_connection do
20
19
  rule = Structs::Rule.from_path_or_id path_or_id
21
20
 
22
21
  # validate
@@ -164,6 +164,15 @@ module Mihari
164
164
 
165
165
  ActiveRecord::Base.clear_active_connections!
166
166
  end
167
+
168
+ def with_db_connection
169
+ Mihari::Database.connect
170
+ yield
171
+ rescue ActiveRecord::StatementInvalid
172
+ Mihari.logger.error("You haven't finished the DB migration! Please run 'mihari db migrate'.")
173
+ ensure
174
+ Mihari::Database.close
175
+ end
167
176
  end
168
177
  end
169
178
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "misp"
4
-
5
3
  module Mihari
6
4
  module Emitters
7
5
  class MISP < Base
@@ -16,11 +14,6 @@ module Mihari
16
14
 
17
15
  @url = kwargs[:url] || Mihari.config.misp_url
18
16
  @api_key = kwargs[:api_key] || Mihari.config.misp_api_key
19
-
20
- ::MISP.configure do |config|
21
- config.api_endpoint = url
22
- config.api_key = api_key
23
- end
24
17
  end
25
18
 
26
19
  # @return [Boolean]
@@ -50,17 +43,13 @@ module Mihari
50
43
  def emit(rule:, artifacts:, **_options)
51
44
  return if artifacts.empty?
52
45
 
53
- event = ::MISP::Event.new(info: rule.title)
54
-
55
- artifacts.each do |artifact|
56
- event.attributes << build_attribute(artifact)
57
- end
58
-
59
- rule.tags.each do |tag|
60
- event.add_tag name: tag
61
- end
62
-
63
- event.create
46
+ client.create_event({
47
+ Event: {
48
+ info: rule.title
49
+ },
50
+ Attribute: artifacts.map { |artifact| build_attribute(artifact) },
51
+ Tag: rule.tags.map { |tag| { name: tag } }
52
+ })
64
53
  end
65
54
 
66
55
  private
@@ -69,15 +58,19 @@ module Mihari
69
58
  %w[misp_url misp_api_key]
70
59
  end
71
60
 
61
+ def client
62
+ @client ||= Clients::MISP.new(url, api_key: api_key)
63
+ end
64
+
72
65
  #
73
66
  # Build a MISP attribute
74
67
  #
75
68
  # @param [Mihari::Artifact] artifact
76
69
  #
77
- # @return [::MISP::Attribute]
70
+ # @return [Hash]
78
71
  #
79
72
  def build_attribute(artifact)
80
- ::MISP::Attribute.new(value: artifact.data, type: to_misp_type(type: artifact.data_type, value: artifact.data))
73
+ { value: artifact.data, type: to_misp_type(type: artifact.data_type, value: artifact.data) }
81
74
  end
82
75
 
83
76
  #
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "hachi"
4
-
5
3
  module Mihari
6
4
  module Emitters
7
5
  class TheHive < Base
@@ -50,7 +48,7 @@ module Mihari
50
48
  return if artifacts.empty?
51
49
 
52
50
  payload = payload(rule: rule, artifacts: artifacts)
53
- api.alert.create(**payload)
51
+ client.alert(payload)
54
52
  end
55
53
 
56
54
  #
@@ -79,8 +77,8 @@ module Mihari
79
77
  %w[thehive_url thehive_api_key]
80
78
  end
81
79
 
82
- def api
83
- @api ||= Hachi::API.new(api_endpoint: url, api_key: api_key, api_version: normalized_api_version)
80
+ def client
81
+ @client ||= Clients::TheHive.new(url, api_key: api_key, api_version: normalized_api_version)
84
82
  end
85
83
 
86
84
  #
@@ -77,13 +77,13 @@ module Mihari
77
77
  payload_ = payload_as_string(artifacts: artifacts, rule: rule)
78
78
  payload = JSON.parse(payload_)
79
79
 
80
- client = Mihari::HTTP.new(url, headers: headers, payload: payload)
80
+ client = Mihari::HTTP.new(url, headers: headers)
81
81
 
82
82
  case method
83
83
  when "GET"
84
84
  res = client.get
85
85
  when "POST"
86
- res = client.post
86
+ res = client.post(json: payload)
87
87
  end
88
88
 
89
89
  res
@@ -6,25 +6,28 @@ require "insensitive_hash"
6
6
  module Mihari
7
7
  module Feed
8
8
  class Reader
9
- attr_reader :uri, :http_request_headers, :http_request_method, :http_request_payload
9
+ attr_reader :url, :headers, :params, :json, :data, :method
10
10
 
11
- def initialize(uri, http_request_headers: {}, http_request_method: "GET", http_request_payload_type: nil, http_request_payload: {})
12
- @uri = Addressable::URI.parse(uri)
13
- @http_request_headers = http_request_headers.insensitive
14
- @http_request_method = http_request_method
15
- @http_request_payload = http_request_payload
11
+ def initialize(url, headers: {}, method: "GET", params: nil, json: nil, data: nil)
12
+ @url = Addressable::URI.parse(url)
13
+ @headers = headers.insensitive
14
+ @method = method
16
15
 
17
- http_request_headers["content-type"] = http_request_payload_type if http_request_payload_type
16
+ @params = params
17
+ @json = json
18
+ @data = data
19
+
20
+ headers["content-type"] = "application/json" unless json.nil?
18
21
  end
19
22
 
20
23
  def read
21
- return read_file(uri.path) if uri.scheme == "file"
24
+ return read_file(url.path) if url.scheme == "file"
22
25
 
23
26
  res = nil
24
- client = HTTP.new(uri, headers: http_request_headers, payload: http_request_payload)
27
+ client = HTTP.new(url, headers: headers)
25
28
 
26
- res = client.get if http_request_method == "GET"
27
- res = client.post if http_request_method == "POST"
29
+ res = client.get(params: params) if method == "GET"
30
+ res = client.post(params: params, json: json, data: data) if method == "POST"
28
31
 
29
32
  return [] if res.nil?
30
33
 
data/lib/mihari/http.rb CHANGED
@@ -4,54 +4,62 @@ require "insensitive_hash"
4
4
 
5
5
  module Mihari
6
6
  class HTTP
7
- attr_reader :url, :headers, :payload
7
+ # @return [String]
8
+ attr_reader :url
8
9
 
9
- def initialize(url, headers: {}, payload: {})
10
+ # @return [Hash]
11
+ attr_reader :headers
12
+
13
+ def initialize(url, headers: {})
10
14
  @url = url.is_a?(URI) ? url : URI(url.to_s)
11
15
  @headers = headers.insensitive
12
- @payload = payload
13
16
  end
14
17
 
15
18
  #
16
19
  # Make a GET request
17
20
  #
21
+ # @param [Hash, nil] params
22
+ #
18
23
  # @return [Net::HTTPResponse]
19
24
  #
20
- def get
25
+ def get(params: nil)
21
26
  new_url = url.deep_dup
22
- new_url.query = Addressable::URI.form_encode(payload) unless payload.empty?
27
+ new_url.query = Addressable::URI.form_encode(params) unless (params || {}).empty?
23
28
 
24
29
  get = Net::HTTP::Get.new(new_url)
25
30
  request get
26
31
  end
27
32
 
28
33
  #
29
- # Make a POST request
34
+ # Make a POST requesti
35
+ #
36
+ # @param [Hash, nil] params
37
+ # @param [Hash, nil] json
38
+ # @param [Hash, nil] data
30
39
  #
31
40
  # @return [Net::HTTPResponse]
32
41
  #
33
- def post
34
- post = Net::HTTP::Post.new(url)
35
-
36
- case content_type
37
- when "application/json"
38
- post.body = JSON.generate(payload)
39
- when "application/x-www-form-urlencoded"
40
- post.set_form_data(payload)
41
- end
42
+ def post(params: nil, json: nil, data: nil)
43
+ new_url = url.deep_dup
44
+ new_url.query = Addressable::URI.form_encode(params) unless (params || {}).empty?
45
+
46
+ post = Net::HTTP::Post.new(new_url)
47
+
48
+ post.body = JSON.generate(json) if json
49
+ post.set_form_data(data) if data
42
50
 
43
51
  request post
44
52
  end
45
53
 
46
54
  class << self
47
- def get(url, headers: {}, params: {})
48
- client = new(url, headers: headers, payload: params)
49
- client.get
55
+ def get(url, headers: {}, params: nil)
56
+ client = new(url, headers: headers)
57
+ client.get(params: params)
50
58
  end
51
59
 
52
- def post(url, headers: {}, payload: {})
53
- client = new(url, headers: headers, payload: payload)
54
- client.post
60
+ def post(url, headers: {}, params: nil, json: nil, data: nil)
61
+ client = new(url, headers: headers)
62
+ client.post(params: params, json: json, data: data)
55
63
  end
56
64
  end
57
65
 
@@ -9,7 +9,9 @@ module Mihari
9
9
  Errno::EPIPE,
10
10
  OpenSSL::SSL::SSLError,
11
11
  Timeout::Error,
12
- RetryableError
12
+ RetryableError,
13
+ NetworkError,
14
+ TimeoutError
13
15
  ]
14
16
 
15
17
  #
@@ -82,10 +82,11 @@ module Mihari
82
82
  required(:analyzer).value(Types::String.enum("feed"))
83
83
  required(:query).value(:string)
84
84
  required(:selector).value(:string)
85
- optional(:http_request_method).value(Types::HTTPRequestMethods).default("GET")
86
- optional(:http_request_headers).value(:hash).default({})
87
- optional(:http_request_payload).value(:hash).default({})
88
- optional(:http_request_payload_type).value(Types::HTTPRequestPayloadTypes)
85
+ optional(:method).value(Types::HTTPRequestMethods).default("GET")
86
+ optional(:headers).value(:hash).default({})
87
+ optional(:params).value(:hash)
88
+ optional(:data).value(:hash)
89
+ optional(:json).value(:hash)
89
90
  optional(:options).hash(AnalyzerOptions)
90
91
  end
91
92
  end
@@ -4,8 +4,17 @@ module Mihari
4
4
  module Structs
5
5
  module Censys
6
6
  class AutonomousSystem < Dry::Struct
7
+ include Mixins::AutonomousSystem
8
+
7
9
  attribute :asn, Types::Int
8
10
 
11
+ #
12
+ # @return [Mihari::AutonomousSystem]
13
+ #
14
+ def to_as
15
+ Mihari::AutonomousSystem.new(asn: normalize_asn(asn))
16
+ end
17
+
9
18
  def self.from_dynamic!(d)
10
19
  d = Types::Hash[d]
11
20
  new(
@@ -18,6 +27,20 @@ module Mihari
18
27
  attribute :country, Types::String.optional
19
28
  attribute :country_code, Types::String.optional
20
29
 
30
+ #
31
+ # @return [Mihari::Geolocation] <description>
32
+ #
33
+ def to_geolocation
34
+ # sometimes Censys overlooks country
35
+ # then set geolocation as nil
36
+ return nil if country.nil?
37
+
38
+ Mihari::Geolocation.new(
39
+ country: country,
40
+ country_code: country_code
41
+ )
42
+ end
43
+
21
44
  def self.from_dynamic!(d)
22
45
  d = Types::Hash[d]
23
46
  new(
@@ -30,6 +53,13 @@ module Mihari
30
53
  class Service < Dry::Struct
31
54
  attribute :port, Types::Integer
32
55
 
56
+ #
57
+ # @return [Mihari::Port]
58
+ #
59
+ def to_port
60
+ Port.new(port: port)
61
+ end
62
+
33
63
  def self.from_dynamic!(d)
34
64
  d = Types::Hash[d]
35
65
  new(
@@ -45,6 +75,29 @@ module Mihari
45
75
  attribute :metadata, Types::Hash
46
76
  attribute :services, Types.Array(Service)
47
77
 
78
+ #
79
+ # @return [Array<Mihari::Port>]
80
+ #
81
+ def to_ports
82
+ services.map(&:to_port)
83
+ end
84
+
85
+ #
86
+ # @param [String] source
87
+ #
88
+ # @return [Mihari::Artifact]
89
+ #
90
+ def to_artifact(source = "Censys")
91
+ Artifact.new(
92
+ data: ip,
93
+ source: source,
94
+ metadata: metadata,
95
+ autonomous_system: autonomous_system.to_as,
96
+ geolocation: location.to_geolocation,
97
+ ports: to_ports
98
+ )
99
+ end
100
+
48
101
  def self.from_dynamic!(d)
49
102
  d = Types::Hash[d]
50
103
  new(
@@ -76,6 +129,15 @@ module Mihari
76
129
  attribute :hits, Types.Array(Hit)
77
130
  attribute :links, Links
78
131
 
132
+ #
133
+ # @param [String] source
134
+ #
135
+ # @return [Array<Mihari::Artifact>]
136
+ #
137
+ def to_artifacts(source = "Censys")
138
+ hits.map { |hit| hit.to_artifact(source) }
139
+ end
140
+
79
141
  def self.from_dynamic!(d)
80
142
  d = Types::Hash[d]
81
143
  new(
@@ -4,10 +4,29 @@ module Mihari
4
4
  module Structs
5
5
  module GreyNoise
6
6
  class Metadata < Dry::Struct
7
+ include Mixins::AutonomousSystem
8
+
7
9
  attribute :country, Types::String
8
10
  attribute :country_code, Types::String
9
11
  attribute :asn, Types::String
10
12
 
13
+ #
14
+ # @return [Mihari::AutonomousSystem]
15
+ #
16
+ def to_as
17
+ Mihari::AutonomousSystem.new(asn: normalize_asn(asn))
18
+ end
19
+
20
+ #
21
+ # @return [Mihari::Geolocation]
22
+ #
23
+ def to_geolocation
24
+ Mihari::Geolocation.new(
25
+ country: country,
26
+ country_code: country_code
27
+ )
28
+ end
29
+
11
30
  def self.from_dynamic!(d)
12
31
  d = Types::Hash[d]
13
32
  new(
@@ -23,6 +42,21 @@ module Mihari
23
42
  attribute :metadata, Metadata
24
43
  attribute :metadata_, Types::Hash
25
44
 
45
+ #
46
+ # @param [String] source
47
+ #
48
+ # @return [Mihari::Artifact]
49
+ #
50
+ def to_artifact(source = "GreyNoise")
51
+ Mihari::Artifact.new(
52
+ data: ip,
53
+ source: source,
54
+ metadata: metadata_,
55
+ autonomous_system: metadata.to_as,
56
+ geolocation: metadata.to_geolocation
57
+ )
58
+ end
59
+
26
60
  def self.from_dynamic!(d)
27
61
  d = Types::Hash[d]
28
62
  new(
@@ -40,6 +74,15 @@ module Mihari
40
74
  attribute :message, Types::String
41
75
  attribute :query, Types::String
42
76
 
77
+ #
78
+ # @param [String] source
79
+ #
80
+ # @return [Array<Mihari::Artifact>]
81
+ #
82
+ def to_artifacts(source = "GreyNoise")
83
+ data.map { |datum| datum.to_artifact(source) }
84
+ end
85
+
43
86
  def self.from_dynamic!(d)
44
87
  d = Types::Hash[d]
45
88
  new(
@@ -4,11 +4,47 @@ module Mihari
4
4
  module Structs
5
5
  module Onyphe
6
6
  class Result < Dry::Struct
7
+ include Mixins::AutonomousSystem
8
+
7
9
  attribute :asn, Types::String
8
10
  attribute :country_code, Types::String.optional
9
11
  attribute :ip, Types::String
10
12
  attribute :metadata, Types::Hash
11
13
 
14
+ #
15
+ # @param [String] source
16
+ #
17
+ # @return [Mihari::Artifact]
18
+ #
19
+ def to_artifact(source = "Onyphe")
20
+ Mihari::Artifact.new(
21
+ data: ip,
22
+ source: source,
23
+ metadata: metadata,
24
+ autonomous_system: to_as,
25
+ geolocation: to_geolocation
26
+ )
27
+ end
28
+
29
+ #
30
+ # @return [Mihari::Geolocation, nil]
31
+ #
32
+ def to_geolocation
33
+ return nil if country_code.nil?
34
+
35
+ Mihari::Geolocation.new(
36
+ country: NormalizeCountry(country_code, to: :short),
37
+ country_code: country_code
38
+ )
39
+ end
40
+
41
+ #
42
+ # @return [Mihari::AutonomousSystem]
43
+ #
44
+ def to_as
45
+ Mihari::AutonomousSystem.new(asn: normalize_asn(asn))
46
+ end
47
+
12
48
  def self.from_dynamic!(d)
13
49
  d = Types::Hash[d]
14
50
  new(
@@ -30,6 +66,15 @@ module Mihari
30
66
  attribute :status, Types::String
31
67
  attribute :total, Types::Int
32
68
 
69
+ #
70
+ # @param [String] source
71
+ #
72
+ # @return [Array<Mihari::Artifact>]
73
+ #
74
+ def to_artifacts(source = "Onyphe")
75
+ results.map { |result| result.to_artifact(source) }
76
+ end
77
+
33
78
  def self.from_dynamic!(d)
34
79
  d = Types::Hash[d]
35
80
  new(