hookd-client 1.2.2 → 1.4.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 +54 -4
- data/lib/hookd/client.rb +88 -23
- data/lib/hookd/error.rb +3 -0
- data/lib/hookd/hook.rb +12 -4
- data/lib/hookd/hook_activity.rb +30 -0
- data/lib/hookd/interaction.rb +6 -1
- data/lib/hookd/version.rb +1 -1
- data/lib/hookd.rb +1 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ff2c6d9ea446cd5631e2c3f4d3f27a0586a130acaf6bf9e10f9a7fb6613efc3
|
|
4
|
+
data.tar.gz: 38021e8c18baee9ff374c60d305cc6312cf024a2fff120e926cfa89d9d44ea52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cc77be594c69256a2de2e31a23a7654dddbf94c50f0402fa327e3865e2fa25af4c4c4dfab6e9ca641aa611b20da6e22760646d570676a4a87e92d08382f3af8
|
|
7
|
+
data.tar.gz: 1d2da85d42bfff6ba7bcf2e1211b418c7bc8944ce8119169388e1b51cec085d64a54ceb4372ea5b5869714c792836cd193b1600b526c40021ead72cf8a492d28
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Hookd Ruby Client
|
|
2
2
|
|
|
3
|
-
Ruby client library for [Hookd](https://github.com/JoshuaMart/hookd/server), a DNS/HTTP interaction server for security testing and debugging.
|
|
3
|
+
Ruby client library for [Hookd](https://github.com/JoshuaMart/hookd/server), a DNS/HTTP/SMTP interaction server for security testing and debugging.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -35,6 +35,7 @@ hook = client.register
|
|
|
35
35
|
puts "DNS endpoint: #{hook.dns}"
|
|
36
36
|
puts "HTTP endpoint: #{hook.http}"
|
|
37
37
|
puts "HTTPS endpoint: #{hook.https}"
|
|
38
|
+
puts "Mail endpoint: #{hook.smtp}" if hook.smtp
|
|
38
39
|
|
|
39
40
|
# Make a request to the HTTP endpoint to simulate an interaction
|
|
40
41
|
Typhoeus.get(hook.http)
|
|
@@ -46,6 +47,8 @@ interactions.each do |interaction|
|
|
|
46
47
|
puts "DNS query: #{interaction.data}"
|
|
47
48
|
elsif interaction.http?
|
|
48
49
|
puts "HTTP request: #{interaction.data}"
|
|
50
|
+
elsif interaction.smtp?
|
|
51
|
+
puts "Mail from #{interaction.data['mail_from']}: #{interaction.data['subject']}"
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
```
|
|
@@ -95,6 +98,8 @@ results.each do |hook_id, result|
|
|
|
95
98
|
puts " - DNS: #{interaction.data['qname']} (#{interaction.data['qtype']})"
|
|
96
99
|
elsif interaction.http?
|
|
97
100
|
puts " - HTTP: #{interaction.data['method']} #{interaction.data['path']}"
|
|
101
|
+
elsif interaction.smtp?
|
|
102
|
+
puts " - SMTP: #{interaction.data['mail_from']} (#{interaction.data['subject']})"
|
|
98
103
|
end
|
|
99
104
|
end
|
|
100
105
|
end
|
|
@@ -108,6 +113,10 @@ The client requires two configuration parameters:
|
|
|
108
113
|
- `server`: The Hookd server URL (e.g., `https://hookd.example.com`)
|
|
109
114
|
- `token`: Authentication token for API access
|
|
110
115
|
|
|
116
|
+
An optional `max_response_bytes:` caps the response payload the client will read
|
|
117
|
+
(64 MiB by default; zero or less disables it). Exceeding it raises
|
|
118
|
+
`Hookd::ResponseTooLargeError`.
|
|
119
|
+
|
|
111
120
|
### API Reference
|
|
112
121
|
|
|
113
122
|
#### `Hookd::Client`
|
|
@@ -116,7 +125,7 @@ Main client class for interacting with the Hookd server.
|
|
|
116
125
|
|
|
117
126
|
##### `#register(count: nil)`
|
|
118
127
|
|
|
119
|
-
Register one or more hooks and get DNS
|
|
128
|
+
Register one or more hooks and get their DNS, HTTP and (when the server runs a mail listener) SMTP endpoints.
|
|
120
129
|
|
|
121
130
|
**Single hook (default):**
|
|
122
131
|
```ruby
|
|
@@ -130,8 +139,20 @@ hooks = client.register(count: 5)
|
|
|
130
139
|
# => [#<Hookd::Hook id="abc123" ...>, #<Hookd::Hook id="def456" ...>, ...]
|
|
131
140
|
```
|
|
132
141
|
|
|
142
|
+
**Long-lived hook (survives restarts, for stored-XSS style detection):**
|
|
143
|
+
```ruby
|
|
144
|
+
hook = client.register(ttl: '7d', metadata: { target: 'acme', field: 'profile.bio' })
|
|
145
|
+
# => #<Hookd::Hook id="abc123" ...>
|
|
146
|
+
hook.expires_at # => "2025-10-08T10:30:00Z"
|
|
147
|
+
hook.metadata # => {"target"=>"acme", "field"=>"profile.bio"}
|
|
148
|
+
```
|
|
149
|
+
|
|
133
150
|
Parameters:
|
|
134
151
|
- `count` (Integer, optional) - Number of hooks to create (default: 1)
|
|
152
|
+
- `ttl` (String, optional) - Lifetime as a Go duration (`"168h"`) or day count
|
|
153
|
+
(`"7d"`); a value above the server's ephemeral `hook_ttl` registers a durable
|
|
154
|
+
long-lived hook. Omit for an ephemeral hook.
|
|
155
|
+
- `metadata` (Hash, optional) - Stored with the hook and echoed back on poll
|
|
135
156
|
|
|
136
157
|
Returns:
|
|
137
158
|
- `Hookd::Hook` object when `count` is 1 or not specified
|
|
@@ -143,6 +164,21 @@ Raises:
|
|
|
143
164
|
- `Hookd::ServerError` - Server error (5xx)
|
|
144
165
|
- `Hookd::ConnectionError` - Connection failed
|
|
145
166
|
|
|
167
|
+
##### `#activity`
|
|
168
|
+
|
|
169
|
+
List the long-lived hooks that currently have pending interactions, so you can
|
|
170
|
+
discover which fired without polling each one; drain the details with `#poll`.
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
client.activity.each do |a|
|
|
174
|
+
puts "#{a.hook.id} fired #{a.pending_count} time(s), meta=#{a.hook.metadata}"
|
|
175
|
+
client.poll(a.hook.id)
|
|
176
|
+
end
|
|
177
|
+
# => [#<Hookd::HookActivity hook=abc123 pending=3>, ...]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Returns: Array of `Hookd::HookActivity` (empty when none fired or long-lived is disabled)
|
|
181
|
+
|
|
146
182
|
##### `#poll(hook_id)`
|
|
147
183
|
|
|
148
184
|
Poll for interactions captured by a single hook.
|
|
@@ -232,20 +268,33 @@ Attributes:
|
|
|
232
268
|
- `dns` (String) - DNS endpoint
|
|
233
269
|
- `http` (String) - HTTP endpoint
|
|
234
270
|
- `https` (String) - HTTPS endpoint
|
|
271
|
+
- `smtp` (String, nil) - Mail address (nil unless the server runs a mail listener)
|
|
235
272
|
- `created_at` (String) - Creation timestamp
|
|
273
|
+
- `expires_at` (String, nil) - Expiry timestamp (long-lived hooks)
|
|
274
|
+
- `metadata` (Hash, nil) - Metadata attached at registration
|
|
275
|
+
|
|
276
|
+
#### `Hookd::HookActivity`
|
|
277
|
+
|
|
278
|
+
Represents a long-lived hook that has pending interactions (returned by `#activity`).
|
|
279
|
+
|
|
280
|
+
Attributes:
|
|
281
|
+
- `hook` (`Hookd::Hook`) - The long-lived hook that fired
|
|
282
|
+
- `pending_count` (Integer) - Number of interactions awaiting poll
|
|
283
|
+
- `last_interaction_at` (String) - Timestamp of the most recent interaction
|
|
236
284
|
|
|
237
285
|
#### `Hookd::Interaction`
|
|
238
286
|
|
|
239
|
-
Represents a captured DNS or
|
|
287
|
+
Represents a captured DNS, HTTP or SMTP interaction.
|
|
240
288
|
|
|
241
289
|
Attributes:
|
|
242
|
-
- `type` (String) - Interaction type ("dns" or "
|
|
290
|
+
- `type` (String) - Interaction type ("dns", "http" or "smtp")
|
|
243
291
|
- `timestamp` (String) - When the interaction was captured
|
|
244
292
|
- `data` (Hash) - Interaction details
|
|
245
293
|
|
|
246
294
|
Methods:
|
|
247
295
|
- `#dns?` - Returns true if this is a DNS interaction
|
|
248
296
|
- `#http?` - Returns true if this is an HTTP interaction
|
|
297
|
+
- `#smtp?` - Returns true if this is an SMTP interaction
|
|
249
298
|
|
|
250
299
|
### Error Handling
|
|
251
300
|
|
|
@@ -269,3 +318,4 @@ Exception hierarchy:
|
|
|
269
318
|
- `Hookd::NotFoundError` - 404 Not Found
|
|
270
319
|
- `Hookd::ServerError` - 5xx Server Error
|
|
271
320
|
- `Hookd::ConnectionError` - Network/connection errors
|
|
321
|
+
- `Hookd::ResponseTooLargeError` - Response above `max_response_bytes`
|
data/lib/hookd/client.rb
CHANGED
|
@@ -6,11 +6,19 @@ require 'json'
|
|
|
6
6
|
module Hookd
|
|
7
7
|
# HTTP client for interacting with Hookd server
|
|
8
8
|
class Client
|
|
9
|
-
|
|
9
|
+
# Caps the payload materialised as a String. Generous, since a poll can
|
|
10
|
+
# return many interactions with full bodies. Zero or less disables it.
|
|
11
|
+
DEFAULT_MAX_RESPONSE_BYTES = 64 * 1024 * 1024
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
# How much of an error response is quoted back in the raised message.
|
|
14
|
+
ERROR_BODY_EXCERPT_BYTES = 1024
|
|
15
|
+
|
|
16
|
+
attr_reader :server, :token, :max_response_bytes
|
|
17
|
+
|
|
18
|
+
def initialize(server:, token:, max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES)
|
|
12
19
|
@server = server
|
|
13
20
|
@token = token
|
|
21
|
+
@max_response_bytes = max_response_bytes
|
|
14
22
|
@http = HTTPX.with(
|
|
15
23
|
headers: { 'X-API-Key' => token },
|
|
16
24
|
timeout: {
|
|
@@ -22,25 +30,20 @@ module Hookd
|
|
|
22
30
|
|
|
23
31
|
# Register one or more hooks
|
|
24
32
|
# @param count [Integer, nil] number of hooks to register (default: 1)
|
|
33
|
+
# @param ttl [String, nil] lifetime as a Go duration ("168h") or day count
|
|
34
|
+
# ("7d"); a value above the server's ephemeral hook_ttl registers a durable
|
|
35
|
+
# long-lived hook. Omit for an ephemeral hook.
|
|
36
|
+
# @param metadata [Hash, nil] arbitrary data stored with the hook and echoed
|
|
37
|
+
# back on poll
|
|
25
38
|
# @return [Hookd::Hook, Array<Hookd::Hook>] single hook or array of hooks
|
|
26
39
|
# @raise [Hookd::AuthenticationError] if authentication fails
|
|
27
40
|
# @raise [Hookd::ServerError] if server returns 5xx
|
|
28
41
|
# @raise [Hookd::ConnectionError] if connection fails
|
|
29
42
|
# @raise [ArgumentError] if count is invalid
|
|
30
|
-
def register(count: nil)
|
|
31
|
-
body = count.nil? ? nil : { count: count }
|
|
32
|
-
|
|
43
|
+
def register(count: nil, ttl: nil, metadata: nil)
|
|
33
44
|
raise ArgumentError, 'count must be a positive integer' if count && (!count.is_a?(Integer) || count < 1)
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Single hook response (backward compatible)
|
|
38
|
-
return Hook.from_hash(response) if response.key?('id')
|
|
39
|
-
|
|
40
|
-
# Multiple hooks response
|
|
41
|
-
return [] if response['hooks'].nil? || response['hooks'].empty?
|
|
42
|
-
|
|
43
|
-
response['hooks'].map { |h| Hook.from_hash(h) }
|
|
46
|
+
parse_register_response(post('/register', register_body(count, ttl, metadata)))
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
# Poll for interactions on a hook
|
|
@@ -92,8 +95,45 @@ module Hookd
|
|
|
92
95
|
get('/metrics')
|
|
93
96
|
end
|
|
94
97
|
|
|
98
|
+
# List long-lived hooks that currently have pending interactions, so you can
|
|
99
|
+
# discover which of your long-lived hooks fired without polling each one.
|
|
100
|
+
# Drain the details with #poll. Returns an empty array when none have fired
|
|
101
|
+
# (or the server has long-lived hooks disabled).
|
|
102
|
+
# @return [Array<Hookd::HookActivity>]
|
|
103
|
+
# @raise [Hookd::AuthenticationError] if authentication fails
|
|
104
|
+
# @raise [Hookd::ServerError] if server returns 5xx
|
|
105
|
+
# @raise [Hookd::ConnectionError] if connection fails
|
|
106
|
+
def activity
|
|
107
|
+
response = get('/activity')
|
|
108
|
+
|
|
109
|
+
hooks = response['hooks']
|
|
110
|
+
return [] if hooks.nil? || hooks.empty? || !hooks.is_a?(Array)
|
|
111
|
+
|
|
112
|
+
hooks.map { |h| HookActivity.from_hash(h) }
|
|
113
|
+
rescue NoMethodError => e
|
|
114
|
+
raise Error, "Invalid response format: #{e.message}"
|
|
115
|
+
end
|
|
116
|
+
|
|
95
117
|
private
|
|
96
118
|
|
|
119
|
+
def register_body(count, ttl, metadata)
|
|
120
|
+
body = {}
|
|
121
|
+
body[:count] = count unless count.nil?
|
|
122
|
+
body[:ttl] = ttl unless ttl.nil?
|
|
123
|
+
body[:metadata] = metadata unless metadata.nil?
|
|
124
|
+
body.empty? ? nil : body
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_register_response(response)
|
|
128
|
+
# Single hook response (backward compatible)
|
|
129
|
+
return Hook.from_hash(response) if response.key?('id')
|
|
130
|
+
|
|
131
|
+
# Multiple hooks response
|
|
132
|
+
return [] if response['hooks'].nil? || response['hooks'].empty?
|
|
133
|
+
|
|
134
|
+
response['hooks'].map { |h| Hook.from_hash(h) }
|
|
135
|
+
end
|
|
136
|
+
|
|
97
137
|
def get(path)
|
|
98
138
|
url = "#{@server}#{path}"
|
|
99
139
|
response = @http.get(url)
|
|
@@ -138,24 +178,49 @@ module Hookd
|
|
|
138
178
|
raise ConnectionError, "Connection failed: #{error.message}"
|
|
139
179
|
end
|
|
140
180
|
|
|
141
|
-
body = response.body.to_s
|
|
142
|
-
|
|
143
181
|
case response.status
|
|
144
182
|
when 200, 201
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
JSON.parse(body)
|
|
183
|
+
parse_body(response)
|
|
148
184
|
when 401
|
|
149
|
-
raise AuthenticationError, "Authentication failed: #{
|
|
185
|
+
raise AuthenticationError, "Authentication failed: #{error_excerpt(response)}"
|
|
150
186
|
when 404
|
|
151
|
-
raise NotFoundError, "Resource not found: #{
|
|
187
|
+
raise NotFoundError, "Resource not found: #{error_excerpt(response)}"
|
|
152
188
|
when 500..599
|
|
153
|
-
raise ServerError, "Server error (#{response.status}): #{
|
|
189
|
+
raise ServerError, "Server error (#{response.status}): #{error_excerpt(response)}"
|
|
154
190
|
else
|
|
155
|
-
raise Error, "Unexpected response (#{response.status}): #{
|
|
191
|
+
raise Error, "Unexpected response (#{response.status}): #{error_excerpt(response)}"
|
|
156
192
|
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def parse_body(response)
|
|
196
|
+
body = read_body(response, @max_response_bytes)
|
|
197
|
+
raise Error, 'Empty response body from server' if body.empty?
|
|
198
|
+
|
|
199
|
+
JSON.parse(body)
|
|
157
200
|
rescue JSON::ParserError => e
|
|
158
201
|
raise Error, "Invalid JSON response: #{e.message}"
|
|
159
202
|
end
|
|
203
|
+
|
|
204
|
+
# Accumulates chunk by chunk and stops at the ceiling, raising unless
|
|
205
|
+
# truncate is set, in which case it returns the bounded slice.
|
|
206
|
+
def read_body(response, limit, truncate: false)
|
|
207
|
+
return response.body.to_s if limit <= 0
|
|
208
|
+
|
|
209
|
+
body = nil
|
|
210
|
+
response.body.each do |chunk|
|
|
211
|
+
# Seed from the first chunk to keep the payload's own encoding.
|
|
212
|
+
body = body ? body << chunk : chunk.dup
|
|
213
|
+
next if body.bytesize <= limit
|
|
214
|
+
return body.byteslice(0, limit).scrub if truncate
|
|
215
|
+
|
|
216
|
+
raise ResponseTooLargeError, "Response exceeds the #{limit} byte limit"
|
|
217
|
+
end
|
|
218
|
+
body || ''
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# A large error page must not become a large exception message.
|
|
222
|
+
def error_excerpt(response)
|
|
223
|
+
read_body(response, ERROR_BODY_EXCERPT_BYTES, truncate: true)
|
|
224
|
+
end
|
|
160
225
|
end
|
|
161
226
|
end
|
data/lib/hookd/error.rb
CHANGED
data/lib/hookd/hook.rb
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Hookd
|
|
4
|
-
# Represents a registered hook with
|
|
4
|
+
# Represents a registered hook with its endpoints. smtp is set only when the
|
|
5
|
+
# server runs a mail listener. expires_at and metadata are populated for
|
|
6
|
+
# long-lived hooks (registered with a ttl) and nil otherwise.
|
|
5
7
|
class Hook
|
|
6
|
-
attr_reader :id, :dns, :http, :https, :created_at
|
|
8
|
+
attr_reader :id, :dns, :http, :https, :smtp, :created_at, :expires_at, :metadata
|
|
7
9
|
|
|
8
|
-
def initialize(id:, dns:, http:, https:, created_at:)
|
|
10
|
+
def initialize(id:, dns:, http:, https:, created_at:, smtp: nil, expires_at: nil, metadata: nil)
|
|
9
11
|
@id = id
|
|
10
12
|
@dns = dns
|
|
11
13
|
@http = http
|
|
12
14
|
@https = https
|
|
15
|
+
@smtp = smtp
|
|
13
16
|
@created_at = created_at
|
|
17
|
+
@expires_at = expires_at
|
|
18
|
+
@metadata = metadata
|
|
14
19
|
end
|
|
15
20
|
|
|
16
21
|
# Create a Hook from API response hash
|
|
@@ -22,7 +27,10 @@ module Hookd
|
|
|
22
27
|
dns: hash['dns'],
|
|
23
28
|
http: hash['http'],
|
|
24
29
|
https: hash['https'],
|
|
25
|
-
|
|
30
|
+
smtp: hash['smtp'],
|
|
31
|
+
created_at: hash['created_at'],
|
|
32
|
+
expires_at: hash['expires_at'],
|
|
33
|
+
metadata: hash['metadata']
|
|
26
34
|
)
|
|
27
35
|
end
|
|
28
36
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hookd
|
|
4
|
+
# Summarises a long-lived hook that currently has pending interactions,
|
|
5
|
+
# returned by Client#activity.
|
|
6
|
+
class HookActivity
|
|
7
|
+
attr_reader :hook, :pending_count, :last_interaction_at
|
|
8
|
+
|
|
9
|
+
def initialize(hook:, pending_count:, last_interaction_at:)
|
|
10
|
+
@hook = hook
|
|
11
|
+
@pending_count = pending_count
|
|
12
|
+
@last_interaction_at = last_interaction_at
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Create a HookActivity from an API response hash
|
|
16
|
+
def self.from_hash(hash)
|
|
17
|
+
raise ArgumentError, "Invalid hash: expected Hash, got #{hash.class}" unless hash.is_a?(Hash)
|
|
18
|
+
|
|
19
|
+
new(
|
|
20
|
+
hook: Hook.from_hash(hash['hook']),
|
|
21
|
+
pending_count: hash['pending_count'],
|
|
22
|
+
last_interaction_at: hash['last_interaction_at']
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_s
|
|
27
|
+
"#<Hookd::HookActivity hook=#{hook.id} pending=#{pending_count}>"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/hookd/interaction.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Hookd
|
|
4
|
-
# Represents a captured DNS or
|
|
4
|
+
# Represents a captured DNS, HTTP or SMTP interaction
|
|
5
5
|
class Interaction
|
|
6
6
|
attr_reader :type, :timestamp, :source_ip, :data
|
|
7
7
|
|
|
@@ -32,6 +32,11 @@ module Hookd
|
|
|
32
32
|
type == 'http'
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Check if this is an SMTP interaction
|
|
36
|
+
def smtp?
|
|
37
|
+
type == 'smtp'
|
|
38
|
+
end
|
|
39
|
+
|
|
35
40
|
def to_s
|
|
36
41
|
"#<Hookd::Interaction type=#{type} timestamp=#{timestamp} source_ip=#{source_ip}>"
|
|
37
42
|
end
|
data/lib/hookd/version.rb
CHANGED
data/lib/hookd.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hookd-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua MARTINELLE
|
|
@@ -23,8 +23,8 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.0'
|
|
26
|
-
description: Ruby client library for Hookd, a DNS/HTTP interaction server for
|
|
27
|
-
testing and debugging
|
|
26
|
+
description: Ruby client library for Hookd, a DNS/SMTP/HTTP interaction server for
|
|
27
|
+
security testing and debugging
|
|
28
28
|
email:
|
|
29
29
|
- contact@jomar.fr
|
|
30
30
|
executables: []
|
|
@@ -36,6 +36,7 @@ files:
|
|
|
36
36
|
- lib/hookd/client.rb
|
|
37
37
|
- lib/hookd/error.rb
|
|
38
38
|
- lib/hookd/hook.rb
|
|
39
|
+
- lib/hookd/hook_activity.rb
|
|
39
40
|
- lib/hookd/interaction.rb
|
|
40
41
|
- lib/hookd/version.rb
|
|
41
42
|
homepage: https://github.com/JoshuaMart/Hookd
|