synthient 1.0.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 +7 -0
- data/LICENSE +202 -0
- data/README.md +420 -0
- data/lib/synthient/client.rb +188 -0
- data/lib/synthient/errors.rb +60 -0
- data/lib/synthient/feed.rb +135 -0
- data/lib/synthient/grpcschema.rb +231 -0
- data/lib/synthient/model.rb +93 -0
- data/lib/synthient/models/account.rb +67 -0
- data/lib/synthient/models/domain.rb +100 -0
- data/lib/synthient/models/events.rb +202 -0
- data/lib/synthient/models/export.rb +65 -0
- data/lib/synthient/models/lookup.rb +86 -0
- data/lib/synthient/response_metadata.rb +16 -0
- data/lib/synthient/transport.rb +340 -0
- data/lib/synthient/version.rb +5 -0
- data/lib/synthient.rb +30 -0
- data/sig/manifest.yaml +4 -0
- data/sig/synthient/models.rbs +555 -0
- data/sig/synthient.rbs +115 -0
- metadata +62 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthient
|
|
4
|
+
# Decoding is strict: a field the API schema requires must be present,
|
|
5
|
+
# non-null, and of its declared type. Optional fields may be absent or null
|
|
6
|
+
# and become nil. Unknown fields are ignored so that additive API changes
|
|
7
|
+
# don't break decoding.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module Model
|
|
11
|
+
Field = Data.define(:name, :wire, :type, :required)
|
|
12
|
+
|
|
13
|
+
class Builder
|
|
14
|
+
attr_reader :fields
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@fields = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def required(name, type, wire: name.to_s)
|
|
21
|
+
@fields << Field.new(name:, wire:, type:, required: true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def optional(name, type, wire: name.to_s)
|
|
25
|
+
@fields << Field.new(name:, wire:, type:, required: false)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.define(&)
|
|
30
|
+
builder = Builder.new
|
|
31
|
+
builder.instance_eval(&)
|
|
32
|
+
fields = builder.fields.freeze
|
|
33
|
+
defaults = fields.reject(&:required).to_h { |field| [field.name, nil] }.freeze
|
|
34
|
+
Data.define(*fields.map(&:name)) do
|
|
35
|
+
define_singleton_method(:fields) { fields }
|
|
36
|
+
define_method(:initialize) { |**attrs| super(**defaults, **attrs) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.decode(model, object, path = model.name.delete_prefix("Synthient::"))
|
|
41
|
+
raise DecodeError, "#{path} must be an object" unless object.is_a?(Hash)
|
|
42
|
+
|
|
43
|
+
attrs = model.fields.to_h do |field|
|
|
44
|
+
value = object[field.wire]
|
|
45
|
+
field_path = "#{path}.#{field.wire}"
|
|
46
|
+
raise DecodeError, "#{field_path} is required" if value.nil? && field.required
|
|
47
|
+
|
|
48
|
+
[field.name, value.nil? ? nil : decode_value(field.type, value, field_path)]
|
|
49
|
+
end
|
|
50
|
+
model.new(**attrs)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.decode_value(type, value, path)
|
|
54
|
+
case type
|
|
55
|
+
when :integer
|
|
56
|
+
return value if value.is_a?(Integer)
|
|
57
|
+
when :float
|
|
58
|
+
return value.to_f if (value.is_a?(Integer) || value.is_a?(Float)) && value.to_f.finite?
|
|
59
|
+
when :string
|
|
60
|
+
return value if value.is_a?(String)
|
|
61
|
+
when :bool
|
|
62
|
+
return value if [true, false].include?(value)
|
|
63
|
+
when :unix_time
|
|
64
|
+
if value.is_a?(Integer)
|
|
65
|
+
# The spec says seconds, but the Helios docs show milliseconds, so
|
|
66
|
+
# values from 1e12 up are read as milliseconds.
|
|
67
|
+
return Time.at(value, in: "UTC") if value < 1_000_000_000_000
|
|
68
|
+
|
|
69
|
+
return Time.at(value / 1000, value % 1000, :millisecond, in: "UTC")
|
|
70
|
+
end
|
|
71
|
+
when :time
|
|
72
|
+
return Time.iso8601(value) if value.is_a?(String)
|
|
73
|
+
when :date
|
|
74
|
+
return Date.iso8601(value) if value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
75
|
+
when :bytes
|
|
76
|
+
return value.unpack1("m0").freeze if value.is_a?(String)
|
|
77
|
+
when :ip
|
|
78
|
+
return IPAddr.new(value) if value.is_a?(String) && !value.include?("/") && !value.include?("%")
|
|
79
|
+
when Array
|
|
80
|
+
if value.is_a?(Array)
|
|
81
|
+
return value.each_with_index.map { |item, index| decode_value(type.first, item, "#{path}[#{index}]") }.freeze
|
|
82
|
+
end
|
|
83
|
+
when Hash
|
|
84
|
+
return value.transform_values { |item| decode_value(type.values.first, item, path) }.freeze if value.is_a?(Hash)
|
|
85
|
+
else
|
|
86
|
+
return decode(type, value, path)
|
|
87
|
+
end
|
|
88
|
+
raise DecodeError, "#{path} is not a valid #{type.is_a?(Symbol) ? type.to_s.tr("_", " ") : type.class.name.downcase}"
|
|
89
|
+
rescue ArgumentError
|
|
90
|
+
raise DecodeError, "#{path} is not a valid #{type.to_s.tr("_", " ")}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthient
|
|
4
|
+
# A member's role in an organization.
|
|
5
|
+
module Relation
|
|
6
|
+
OWNER = "OWNER"
|
|
7
|
+
ADMIN = "ADMIN"
|
|
8
|
+
MEMBER = "MEMBER"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# API key scopes. New values may be added.
|
|
12
|
+
module Scope
|
|
13
|
+
BASIC = "BASIC"
|
|
14
|
+
PROXY_FEEDS = "PROXY_FEEDS"
|
|
15
|
+
PROXY_FIREHOSE = "PROXY_FIREHOSE"
|
|
16
|
+
ANONYMIZERS_FEED = "ANONYMIZERS_FEED"
|
|
17
|
+
ANONYMIZERS_STREAM = "ANONYMIZERS_STREAM"
|
|
18
|
+
JA3_FEED = "JA3_FEED"
|
|
19
|
+
JA3_STREAM = "JA3_STREAM"
|
|
20
|
+
TORRENTS_FEED = "TORRENTS_FEED"
|
|
21
|
+
TORRENTS_STREAM = "TORRENTS_STREAM"
|
|
22
|
+
HONEYPOT_HTTP_FEED = "HONEYPOT_HTTP_FEED"
|
|
23
|
+
HONEYPOT_HTTP_STREAM = "HONEYPOT_HTTP_STREAM"
|
|
24
|
+
HONEYPOT_HTTPS_FEED = "HONEYPOT_HTTPS_FEED"
|
|
25
|
+
HONEYPOT_HTTPS_STREAM = "HONEYPOT_HTTPS_STREAM"
|
|
26
|
+
HONEYPOT_ADB_FEED = "HONEYPOT_ADB_FEED"
|
|
27
|
+
HONEYPOT_ADB_STREAM = "HONEYPOT_ADB_STREAM"
|
|
28
|
+
HONEYPOT_DNS_FEED = "HONEYPOT_DNS_FEED"
|
|
29
|
+
HONEYPOT_DNS_STREAM = "HONEYPOT_DNS_STREAM"
|
|
30
|
+
JA4_FEED = "JA4_FEED"
|
|
31
|
+
JA4_STREAM = "JA4_STREAM"
|
|
32
|
+
JA4T_FEED = "JA4T_FEED"
|
|
33
|
+
JA4T_STREAM = "JA4T_STREAM"
|
|
34
|
+
ADMIN_PROXIES = "ADMIN_PROXIES"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# +relation+ is the account's role, one of Relation.
|
|
38
|
+
Organization = Model.define do
|
|
39
|
+
required :id, :string
|
|
40
|
+
required :name, :string
|
|
41
|
+
required :relation, :string
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# +resets_in+ is the number of seconds until the credits reset.
|
|
45
|
+
LookupQuota = Model.define do
|
|
46
|
+
required :credits, :integer
|
|
47
|
+
required :resets_in, :integer
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The API key that made the request.
|
|
51
|
+
AccountAPIKey = Model.define do
|
|
52
|
+
required :id, :string
|
|
53
|
+
required :name, :string
|
|
54
|
+
required :created_at, :time
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The account that owns the API key. +scopes+ holds Scope values.
|
|
58
|
+
Account = Model.define do
|
|
59
|
+
required :email, :string
|
|
60
|
+
required :first_name, :string
|
|
61
|
+
required :last_name, :string
|
|
62
|
+
required :organization, Organization
|
|
63
|
+
required :scopes, [:string]
|
|
64
|
+
required :lookup_quota, LookupQuota
|
|
65
|
+
optional :api_key, AccountAPIKey
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthient
|
|
4
|
+
# Whether a domain had events in the last 24 hours.
|
|
5
|
+
module DomainStatus
|
|
6
|
+
ACTIVE = "active"
|
|
7
|
+
INACTIVE = "inactive"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# The sentences that describe a domain's activity over the last 30 days.
|
|
11
|
+
module Cadence
|
|
12
|
+
NO_ACTIVITY = "No activity in the last 30 days."
|
|
13
|
+
EVENLY_DISTRIBUTED = "Activity is evenly distributed across the day."
|
|
14
|
+
BUSINESS_HOURS = "Activity follows a weekday business-hours pattern."
|
|
15
|
+
WEEKEND = "Activity follows a weekend pattern."
|
|
16
|
+
BURST_DRIVEN = "Activity is burst-driven with significant peaks."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
DomainStats = Model.define do
|
|
20
|
+
required :events_24h, :integer
|
|
21
|
+
required :total_events_30d, :integer
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Distinct source addresses seen for a domain. +sparkline_24h+ holds hourly
|
|
25
|
+
# counts for the last 24 hours, oldest first.
|
|
26
|
+
UniqueIPs = Model.define do
|
|
27
|
+
required :value_24h, :integer
|
|
28
|
+
required :value_30d, :integer
|
|
29
|
+
required :sparkline_24h, [:integer]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The events and distinct addresses seen for a domain on one day.
|
|
33
|
+
TimeSeriesPoint = Model.define do
|
|
34
|
+
required :date, :unix_time
|
|
35
|
+
required :events, :integer
|
|
36
|
+
required :unique_ips, :integer
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The autonomous system that sent the most events.
|
|
40
|
+
ASNCount = Model.define do
|
|
41
|
+
required :asn, :integer
|
|
42
|
+
required :events, :integer
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
SubdomainCount = Model.define do
|
|
46
|
+
required :subdomain, :string
|
|
47
|
+
required :count, :integer
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
PortCount = Model.define do
|
|
51
|
+
required :port, :integer
|
|
52
|
+
required :count, :integer
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The events and distinct addresses seen from one country.
|
|
56
|
+
CountryCount = Model.define do
|
|
57
|
+
required :country_code, :string
|
|
58
|
+
required :events, :integer
|
|
59
|
+
required :unique_ips, :integer
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# +peak_hour+ and +quiet_hour+ are UTC hours, and +cadence+ is one of
|
|
63
|
+
# Cadence.
|
|
64
|
+
ActivityStats = Model.define do
|
|
65
|
+
required :cadence, :string
|
|
66
|
+
required :median_per_hour, :integer
|
|
67
|
+
required :p95_per_hour, :integer
|
|
68
|
+
required :peak_hour, :integer
|
|
69
|
+
required :quiet_hour, :integer
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# +source_ip+ is the full source address, although the API names it
|
|
73
|
+
# source_ip_masked.
|
|
74
|
+
DomainEvent = Model.define do
|
|
75
|
+
required :timestamp, :unix_time
|
|
76
|
+
required :source_ip, :string, wire: "source_ip_masked"
|
|
77
|
+
required :target_subdomain, :string
|
|
78
|
+
required :country_code, :string
|
|
79
|
+
required :port, :integer
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The honeypot traffic Synthient has observed for a domain. A domain that
|
|
83
|
+
# was never observed is +inactive+ with zero counts. +top_asn+ is nil when
|
|
84
|
+
# there were no events. +hour_dow_heatmap+ has 7 rows, Monday first, of 24
|
|
85
|
+
# UTC hourly counts.
|
|
86
|
+
DomainInfo = Model.define do
|
|
87
|
+
required :domain, :string
|
|
88
|
+
required :status, :string
|
|
89
|
+
required :stats, DomainStats
|
|
90
|
+
required :unique_ips, UniqueIPs
|
|
91
|
+
required :time_series, [TimeSeriesPoint]
|
|
92
|
+
optional :top_asn, ASNCount
|
|
93
|
+
required :top_subdomains, [SubdomainCount]
|
|
94
|
+
required :top_ports, [PortCount]
|
|
95
|
+
required :geo_distribution, [CountryCount]
|
|
96
|
+
required :hour_dow_heatmap, [[:integer]]
|
|
97
|
+
required :activity_stats, ActivityStats
|
|
98
|
+
required :recent_events, [DomainEvent]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthient
|
|
4
|
+
# The kinds of service seen on an address. New values may be added.
|
|
5
|
+
module ProxyType
|
|
6
|
+
FREE_VPN = "FREE_VPN"
|
|
7
|
+
COMMERCIAL_VPN = "COMMERCIAL_VPN"
|
|
8
|
+
ENTERPRISE_VPN = "ENTERPRISE_VPN"
|
|
9
|
+
MOBILE_PROXY = "MOBILE_PROXY"
|
|
10
|
+
BLOCKCHAIN_PROXY = "BLOCKCHAIN_PROXY"
|
|
11
|
+
RESIDENTIAL_PROXY = "RESIDENTIAL_PROXY"
|
|
12
|
+
PUBLIC_PROXY = "PUBLIC_PROXY"
|
|
13
|
+
DATACENTER_PROXY = "DATACENTER_PROXY"
|
|
14
|
+
TOR_NODE = "TOR_NODE"
|
|
15
|
+
PRIVATE_RELAY = "PRIVATE_RELAY"
|
|
16
|
+
BOTNET = "BOTNET"
|
|
17
|
+
SEARCH_ENGINE = "SEARCH_ENGINE"
|
|
18
|
+
AI_CRAWLER = "AI_CRAWLER"
|
|
19
|
+
SOCIAL_MEDIA = "SOCIAL_MEDIA"
|
|
20
|
+
UPTIME_MONITOR = "UPTIME_MONITOR"
|
|
21
|
+
LINK_PREVIEW = "LINK_PREVIEW"
|
|
22
|
+
SEO_CRAWLER = "SEO_CRAWLER"
|
|
23
|
+
WEB_ARCHIVER = "WEB_ARCHIVER"
|
|
24
|
+
WEBHOOK_PROVIDER = "WEBHOOK_PROVIDER"
|
|
25
|
+
PAYMENT_PROCESSOR = "PAYMENT_PROCESSOR"
|
|
26
|
+
INTERNET_SCANNER = "INTERNET_SCANNER"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# +type+ is one of ProxyType.
|
|
30
|
+
ProxyEvent = Model.define do
|
|
31
|
+
optional :ip, :string
|
|
32
|
+
optional :provider, :string
|
|
33
|
+
optional :type, :string
|
|
34
|
+
optional :country_code, :string
|
|
35
|
+
optional :asn, :integer
|
|
36
|
+
optional :timestamp, :unix_time
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# +type+ is one of ProxyType, and +country_code+ and +asn+ describe
|
|
40
|
+
# +range_start+.
|
|
41
|
+
AnonymizerEvent = Model.define do
|
|
42
|
+
optional :range_start, :string
|
|
43
|
+
optional :range_end, :string
|
|
44
|
+
optional :provider, :string
|
|
45
|
+
optional :type, :string
|
|
46
|
+
optional :country_code, :string
|
|
47
|
+
optional :asn, :integer
|
|
48
|
+
optional :timestamp, :unix_time
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# +length+ is in bytes.
|
|
52
|
+
TorrentFile = Model.define do
|
|
53
|
+
optional :path, :string
|
|
54
|
+
optional :length, :integer
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# +source+ is where the peer was found, such as "DHT", "PEX", or "tracker".
|
|
58
|
+
TorrentPeer = Model.define do
|
|
59
|
+
optional :ip, :string
|
|
60
|
+
optional :port, :integer
|
|
61
|
+
optional :source, :string
|
|
62
|
+
optional :encrypted, :bool
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# +info_hash+ is the hex SHA-1 info hash, and sizes are in bytes.
|
|
66
|
+
TorrentEvent = Model.define do
|
|
67
|
+
optional :info_hash, :string
|
|
68
|
+
optional :name, :string
|
|
69
|
+
optional :magnet_uri, :string
|
|
70
|
+
optional :total_size, :integer
|
|
71
|
+
optional :piece_length, :integer
|
|
72
|
+
optional :file_count, :integer
|
|
73
|
+
optional :files, [TorrentFile]
|
|
74
|
+
optional :peers, [TorrentPeer]
|
|
75
|
+
optional :timestamp, :unix_time
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A JA4T TCP fingerprint sighting. +type+ is one of ProxyType.
|
|
79
|
+
JA4TEvent = Model.define do
|
|
80
|
+
optional :ip, :string
|
|
81
|
+
optional :ja4t, :string
|
|
82
|
+
optional :provider, :string
|
|
83
|
+
optional :type, :string
|
|
84
|
+
optional :country_code, :string
|
|
85
|
+
optional :asn, :integer
|
|
86
|
+
optional :timestamp, :unix_time
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The Helios honeypot sensor that captured an event. +proxy_ip+ and
|
|
90
|
+
# +provider+ are the proxy exit address and service the traffic arrived
|
|
91
|
+
# through.
|
|
92
|
+
HeliosMeta = Model.define do
|
|
93
|
+
optional :proxy_ip, :string
|
|
94
|
+
optional :server, :string
|
|
95
|
+
optional :pool_id, :string
|
|
96
|
+
optional :provider, :string
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# +http_method+ is the request method. The API calls it +method+, which
|
|
100
|
+
# would shadow Object#method.
|
|
101
|
+
HTTPRequestDetails = Model.define do
|
|
102
|
+
optional :http_method, :string, wire: "method"
|
|
103
|
+
optional :uri, :string
|
|
104
|
+
optional :version, :string
|
|
105
|
+
optional :headers, { string: :string }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# +domain+ is the host the proxy tunnel was opened to, and +raw+ holds the
|
|
109
|
+
# raw request bytes.
|
|
110
|
+
HeliosHTTPEvent = Model.define do
|
|
111
|
+
required :timestamp, :unix_time
|
|
112
|
+
required :tunnel_id, :integer
|
|
113
|
+
required :domain, :string
|
|
114
|
+
required :port, :integer
|
|
115
|
+
required :protocol, :string
|
|
116
|
+
required :meta, HeliosMeta
|
|
117
|
+
optional :details, HTTPRequestDetails
|
|
118
|
+
optional :raw, :bytes
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A TLS registry code point and its name. GREASE values are named
|
|
122
|
+
# "GREASE(0x....)".
|
|
123
|
+
TLSCodeName = Model.define do
|
|
124
|
+
optional :code, :integer
|
|
125
|
+
optional :name, :string
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# +length+ is the extension body length in bytes.
|
|
129
|
+
TLSExtension = Model.define do
|
|
130
|
+
optional :code, :integer
|
|
131
|
+
optional :name, :string
|
|
132
|
+
optional :length, :integer
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Fields the sensor could not parse are nil.
|
|
136
|
+
TLSClientHello = Model.define do
|
|
137
|
+
optional :record_version, :string
|
|
138
|
+
optional :handshake_version, :string
|
|
139
|
+
optional :client_random, :string
|
|
140
|
+
optional :session_id, :string
|
|
141
|
+
optional :session_id_length, :integer
|
|
142
|
+
optional :cipher_suites, [TLSCodeName]
|
|
143
|
+
optional :compression_methods, [:integer]
|
|
144
|
+
optional :extensions, [TLSExtension]
|
|
145
|
+
optional :sni, :string
|
|
146
|
+
optional :alpn, [:string]
|
|
147
|
+
optional :supported_versions, [:string]
|
|
148
|
+
optional :supported_groups, [TLSCodeName]
|
|
149
|
+
optional :signature_algorithms, [TLSCodeName]
|
|
150
|
+
optional :ec_point_formats, [:string]
|
|
151
|
+
optional :key_share_groups, [:string]
|
|
152
|
+
optional :psk_key_exchange_modes, [:string]
|
|
153
|
+
optional :compress_certificate, [:string]
|
|
154
|
+
optional :has_grease, :bool
|
|
155
|
+
optional :application_settings, :bool
|
|
156
|
+
optional :delegated_credentials, :bool
|
|
157
|
+
optional :encrypt_then_mac, :bool
|
|
158
|
+
optional :extended_master_secret, :bool
|
|
159
|
+
optional :post_handshake_auth, :bool
|
|
160
|
+
optional :renegotiation_info, :bool
|
|
161
|
+
optional :signed_certificate_timestamps, :bool
|
|
162
|
+
optional :status_request, :bool
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# A TLS ClientHello captured by a Helios honeypot. +details+ is nil when the
|
|
166
|
+
# sensor could not parse the handshake. +raw+ is always nil, because the
|
|
167
|
+
# sensors do not capture handshake bytes.
|
|
168
|
+
HeliosHTTPSEvent = Model.define do
|
|
169
|
+
required :timestamp, :unix_time
|
|
170
|
+
required :tunnel_id, :integer
|
|
171
|
+
required :domain, :string
|
|
172
|
+
required :port, :integer
|
|
173
|
+
required :protocol, :string
|
|
174
|
+
required :meta, HeliosMeta
|
|
175
|
+
optional :details, TLSClientHello
|
|
176
|
+
optional :raw, :string
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A destination a Helios honeypot tunnel was asked to reach. Despite the
|
|
180
|
+
# feed's name, it is not a DNS query: names resolved inside a tunneled DNS
|
|
181
|
+
# session are not visible.
|
|
182
|
+
HeliosDNSEvent = Model.define do
|
|
183
|
+
required :timestamp, :unix_time
|
|
184
|
+
required :tunnel_id, :integer
|
|
185
|
+
required :domain, :string
|
|
186
|
+
required :port, :integer
|
|
187
|
+
required :protocol, :string
|
|
188
|
+
required :meta, HeliosMeta
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# An Android Debug Bridge shell command captured by a Helios honeypot. It
|
|
192
|
+
# has no timestamp because the sensor sends none. +sequential_id+ is the
|
|
193
|
+
# 1-based position of the command in its session. +payload_hash+ is the hex
|
|
194
|
+
# SHA-256 the sensor assigns to the session's payload, sent as +hash+: it is
|
|
195
|
+
# not a hash of +command+, and every command in a session shares it.
|
|
196
|
+
HeliosADBEvent = Model.define do
|
|
197
|
+
required :session, :string
|
|
198
|
+
required :sequential_id, :integer
|
|
199
|
+
required :command, :string
|
|
200
|
+
required :payload_hash, :string, wire: "hash"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthient
|
|
4
|
+
module ExportKind
|
|
5
|
+
HOURLY = "hourly"
|
|
6
|
+
DAILY = "daily"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# New values may be added.
|
|
10
|
+
module ParquetType
|
|
11
|
+
STRING = "string"
|
|
12
|
+
INT64 = "int64"
|
|
13
|
+
UINT32 = "uint32"
|
|
14
|
+
UINT64 = "uint64"
|
|
15
|
+
BOOL = "bool"
|
|
16
|
+
BYTES = "bytes"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# +hour+ is nil for a daily export. +checksum+ is the hex SHA-256 of the
|
|
20
|
+
# file. +download_path+ includes the /api/v4 prefix.
|
|
21
|
+
Export = Model.define do
|
|
22
|
+
required :id, :string
|
|
23
|
+
required :kind, :string
|
|
24
|
+
required :date, :date
|
|
25
|
+
optional :hour, :integer
|
|
26
|
+
required :size_bytes, :integer
|
|
27
|
+
required :row_count, :integer
|
|
28
|
+
required :checksum, :string
|
|
29
|
+
required :created_at, :unix_time
|
|
30
|
+
required :download_path, :string
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# +next_cursor+ is nil on the last page.
|
|
34
|
+
ExportPage = Model.define do
|
|
35
|
+
required :stream, :string
|
|
36
|
+
required :exports, [Export], wire: "feeds"
|
|
37
|
+
optional :next_cursor, :string
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# +type+ is one of ParquetType.
|
|
41
|
+
ParquetColumn = Model.define do
|
|
42
|
+
required :name, :string
|
|
43
|
+
required :type, :string
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
ParquetSchema = Model.define do
|
|
47
|
+
required :fields, [ParquetColumn]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# +hour+ is nil for a daily export. +size+ is in bytes and +checksum+ is
|
|
51
|
+
# the hex SHA-256 of the file.
|
|
52
|
+
ExportMeta = Model.define do
|
|
53
|
+
required :id, :string
|
|
54
|
+
required :stream, :string
|
|
55
|
+
required :kind, :string
|
|
56
|
+
required :format, :string
|
|
57
|
+
required :date, :unix_time
|
|
58
|
+
optional :hour, :integer
|
|
59
|
+
required :rows, :integer
|
|
60
|
+
required :size, :integer
|
|
61
|
+
required :checksum, :string
|
|
62
|
+
required :created_at, :unix_time
|
|
63
|
+
required :schema, ParquetSchema
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthient
|
|
4
|
+
# New values may be added.
|
|
5
|
+
module NetworkType
|
|
6
|
+
MOBILE = "MOBILE"
|
|
7
|
+
SATELLITE = "SATELLITE"
|
|
8
|
+
IN_FLIGHT_WIFI = "IN_FLIGHT_WIFI"
|
|
9
|
+
RESIDENTIAL = "RESIDENTIAL"
|
|
10
|
+
CORPORATE = "CORPORATE"
|
|
11
|
+
ACADEMIC = "ACADEMIC"
|
|
12
|
+
DATACENTER = "DATACENTER"
|
|
13
|
+
GOVERNMENT = "GOVERNMENT"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Device operating systems. New values may be added.
|
|
17
|
+
module OS
|
|
18
|
+
ANDROID = "Android"
|
|
19
|
+
CHROME_OS = "Chrome OS"
|
|
20
|
+
FREEBSD = "FreeBSD"
|
|
21
|
+
IOS = "iOS"
|
|
22
|
+
LINUX = "Linux"
|
|
23
|
+
MAC_OS_X = "Mac OS X"
|
|
24
|
+
WINDOWS = "Windows"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The network that announces an address. +asn+ is 0 if the address is not
|
|
28
|
+
# routed. +type+ is one of NetworkType. +org+, +domain+, +abuse_email+, and
|
|
29
|
+
# +abuse_phone+ are nil when unknown.
|
|
30
|
+
Network = Model.define do
|
|
31
|
+
required :asn, :integer
|
|
32
|
+
required :isp, :string
|
|
33
|
+
optional :org, :string
|
|
34
|
+
optional :domain, :string
|
|
35
|
+
required :type, :string
|
|
36
|
+
optional :abuse_email, :string
|
|
37
|
+
optional :abuse_phone, :string
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# +country+ is an ISO 3166-1 alpha-2 code and +timezone+ an IANA time zone
|
|
41
|
+
# name.
|
|
42
|
+
Location = Model.define do
|
|
43
|
+
required :country, :string
|
|
44
|
+
required :state, :string
|
|
45
|
+
required :city, :string
|
|
46
|
+
required :timezone, :string
|
|
47
|
+
required :latitude, :float
|
|
48
|
+
required :longitude, :float
|
|
49
|
+
required :geo_hash, :string
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A proxy, VPN, or anonymizer service seen using an address. +name+ is the
|
|
53
|
+
# service tag, such as "BRIGHTDATA", and +type+ its category.
|
|
54
|
+
Provider = Model.define do
|
|
55
|
+
required :name, :string, wire: "provider"
|
|
56
|
+
required :type, :string
|
|
57
|
+
required :last_seen, :unix_time
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A device type seen behind an address. +os+ is one of OS.
|
|
61
|
+
Device = Model.define do
|
|
62
|
+
required :os, :string
|
|
63
|
+
required :version, :string
|
|
64
|
+
required :last_seen, :unix_time
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Synthient's assessment of an address. +risk_score+ runs from 0 to 100.
|
|
68
|
+
# +categories+ and +behavior+ are open sets of tags. An empty +providers+
|
|
69
|
+
# list does not mean the address is clean.
|
|
70
|
+
Intelligence = Model.define do
|
|
71
|
+
required :risk_score, :integer
|
|
72
|
+
required :categories, [:string]
|
|
73
|
+
required :behavior, [:string]
|
|
74
|
+
required :providers, [Provider]
|
|
75
|
+
required :devices, [Device]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# +network+, +location+, and +intelligence+ are nil when the API has no data
|
|
79
|
+
# for them.
|
|
80
|
+
IPInfo = Model.define do
|
|
81
|
+
required :ip, :ip
|
|
82
|
+
optional :network, Network
|
|
83
|
+
optional :location, Location
|
|
84
|
+
optional :intelligence, Intelligence
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Synthient
|
|
4
|
+
# The rate limit state reported in a response's RateLimit-* headers. +reset+
|
|
5
|
+
# is the number of seconds until the window resets.
|
|
6
|
+
RateLimit = Data.define(:limit, :remaining, :reset)
|
|
7
|
+
|
|
8
|
+
# Describes one API response. A client's +response_hook+ receives one for
|
|
9
|
+
# every response from the API, including retried attempts, export pages,
|
|
10
|
+
# stream handshakes, and responses that later fail to decode. Export
|
|
11
|
+
# downloads from storage are not reported.
|
|
12
|
+
#
|
|
13
|
+
# +headers+ is a frozen copy with lowercase names. +rate_limit+ is nil
|
|
14
|
+
# unless all three RateLimit-* headers are present and valid.
|
|
15
|
+
ResponseMetadata = Data.define(:status, :request_id, :rate_limit, :headers)
|
|
16
|
+
end
|