ironeye 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 +21 -0
- data/README.md +25 -0
- data/lib/ironeye/client.rb +244 -0
- data/lib/ironeye/errors.rb +76 -0
- data/lib/ironeye.rb +29 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6ba74ebdbb15ba9cf99a51a33dce867568f31a3d7bd8fb74757d3348615b9da3
|
|
4
|
+
data.tar.gz: 046d3ce24e2a3c2ab5719eaef763631cbe749f362ad0c71fb2c008a7b569154c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0023cd6e36599c503993f4e240a239eb53a1e6ef41d97399666bbb156b336079669ec2e3463bc38885ec8ee92a6626931cb35943edb3d689e8939da4a35e51c7
|
|
7
|
+
data.tar.gz: c7b6ca607461c6b68b2f93601ae6019be00386e6df7ce92e0255731dd7b8d77c3f8c4066d01b1d19a7060770f1d06c0f98fbc6d8106721514d91671492d5d64b
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Direct Softworks SRL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# IronEye for Ruby
|
|
2
|
+
|
|
3
|
+
The official Ruby client for the [IronEye](https://ironeye.org) API: document
|
|
4
|
+
analysis over bytes you send, and normalised collection from public sources,
|
|
5
|
+
behind one key.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install ironeye
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Every analysis route, the async job path with `await_job`, the collection
|
|
14
|
+
catalogue and the data-subject-rights endpoints.
|
|
15
|
+
- One exception class per refusal family, each carrying the server's verdict.
|
|
16
|
+
- Retries on the server's own `retryable` flag, honouring `Retry-After`.
|
|
17
|
+
- Standard library only: `Net::HTTP` and `JSON`, no gem tree behind it.
|
|
18
|
+
- Takes any `Logger`. No credential, no payload.
|
|
19
|
+
|
|
20
|
+
Full documentation, including every endpoint and every option, is at
|
|
21
|
+
**https://ironeye.org/docs/sdk/ruby**.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Direct Softworks · [MIT](LICENSE) · issues and pull requests welcome
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "logger"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
module IronEye
|
|
10
|
+
# The IronEye client.
|
|
11
|
+
#
|
|
12
|
+
# One instance per process is the intended shape: it keeps no per-request
|
|
13
|
+
# state, and Net::HTTP opens a connection per call.
|
|
14
|
+
class Client
|
|
15
|
+
DEFAULT_BASE_URL = "https://ironeye.org"
|
|
16
|
+
RETRYABLE_STATUS = [408, 425, 429, 500, 502, 503, 504].freeze
|
|
17
|
+
|
|
18
|
+
ANALYSIS_ROUTES = {
|
|
19
|
+
analyze: "/v1/analyze",
|
|
20
|
+
extract: "/v1/extract",
|
|
21
|
+
classify: "/v1/classify",
|
|
22
|
+
pii: "/v1/pii/analyze",
|
|
23
|
+
moderation: "/v1/moderation/analyze",
|
|
24
|
+
malware: "/v1/malware/scan",
|
|
25
|
+
secrets: "/v1/secrets/scan",
|
|
26
|
+
validate: "/v1/validate",
|
|
27
|
+
deduplicate: "/v1/deduplicate",
|
|
28
|
+
invoices: "/v1/invoices/parse"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
DECLARATION_HEADERS = {
|
|
32
|
+
legal_basis: "X-Legal-Basis",
|
|
33
|
+
purpose: "X-Purpose",
|
|
34
|
+
controller: "X-Controller",
|
|
35
|
+
basis_evidence: "X-Basis-Evidence",
|
|
36
|
+
special_condition: "X-Special-Condition",
|
|
37
|
+
projection: "X-Projection"
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
attr_reader :base_url, :timeout, :max_retries
|
|
41
|
+
|
|
42
|
+
# The key comes from +api_key:+ or from IRONEYE_API_KEY; the base URL from
|
|
43
|
+
# +base_url:+, IRONEYE_BASE_URL, or the public host.
|
|
44
|
+
def initialize(api_key: nil, base_url: nil, timeout: 60, max_retries: 2, logger: nil)
|
|
45
|
+
@api_key = api_key || ENV["IRONEYE_API_KEY"]
|
|
46
|
+
raise ArgumentError, "An API key is required: pass api_key: or set IRONEYE_API_KEY." if @api_key.to_s.empty?
|
|
47
|
+
|
|
48
|
+
@base_url = (base_url || ENV["IRONEYE_BASE_URL"] || DEFAULT_BASE_URL).sub(%r{/+\z}, "")
|
|
49
|
+
@timeout = timeout
|
|
50
|
+
@max_retries = max_retries
|
|
51
|
+
@logger = logger || Logger.new(File::NULL)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# -- analysis ------------------------------------------------------------
|
|
55
|
+
ANALYSIS_ROUTES.each do |name, path|
|
|
56
|
+
define_method(name) do |payload, idempotency_key: nil|
|
|
57
|
+
headers = idempotency_key ? { "Idempotency-Key" => idempotency_key } : {}
|
|
58
|
+
send(:request, :post, path, body: payload, headers: headers)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Multipart, for bytes you hold already rather than base64 in a body.
|
|
63
|
+
def analyze_upload(file, filename: "document", content_type: "application/octet-stream",
|
|
64
|
+
features: nil, preset: nil, options: nil, output_mode: nil,
|
|
65
|
+
retention_seconds: nil, idempotency_key: nil)
|
|
66
|
+
fields = {
|
|
67
|
+
"features" => features&.join(","),
|
|
68
|
+
"preset" => preset,
|
|
69
|
+
"options" => (JSON.generate(options) if options),
|
|
70
|
+
"output_mode" => output_mode,
|
|
71
|
+
"retention_seconds" => retention_seconds&.to_s
|
|
72
|
+
}.compact
|
|
73
|
+
boundary = "ironeye-#{SecureRandom.hex(16)}"
|
|
74
|
+
headers = { "Content-Type" => "multipart/form-data; boundary=#{boundary}" }
|
|
75
|
+
headers["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
76
|
+
request(:post, "/v1/analyze/upload",
|
|
77
|
+
raw: multipart(boundary, file, filename, content_type, fields),
|
|
78
|
+
headers: headers)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# -- jobs ----------------------------------------------------------------
|
|
82
|
+
def create_job(payload) = request(:post, "/v1/jobs", body: payload)
|
|
83
|
+
def job(job_id) = request(:get, "/v1/jobs/#{escape(job_id)}")
|
|
84
|
+
def delete_job(job_id) = request(:delete, "/v1/jobs/#{escape(job_id)}")
|
|
85
|
+
|
|
86
|
+
# Polls until the job settles. Nothing in the service dispatches to a
|
|
87
|
+
# callback URL, so polling is the whole asynchronous contract.
|
|
88
|
+
def await_job(job_id, interval: 2, timeout: 300)
|
|
89
|
+
deadline = monotonic + timeout
|
|
90
|
+
loop do
|
|
91
|
+
record = job(job_id)
|
|
92
|
+
return record if %w[completed failed].include?(record["status"])
|
|
93
|
+
raise Error, "Job #{job_id} was still #{record["status"]} after #{timeout}s." if monotonic + interval > deadline
|
|
94
|
+
|
|
95
|
+
sleep(interval)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# -- collection ----------------------------------------------------------
|
|
100
|
+
def catalogue = request(:get, "/v1/harvest/catalogue")
|
|
101
|
+
def operations(platform = nil) = request(:get, "/v1/harvest/operations", query: { platform: platform }.compact)
|
|
102
|
+
def operation(op_id) = request(:get, "/v1/harvest/operations/#{escape(op_id)}")
|
|
103
|
+
|
|
104
|
+
# Runs one operation, addressed by its own route as the catalogue gives it:
|
|
105
|
+
# "/v1/harvest/reddit/subreddit", say.
|
|
106
|
+
def collect(path, params = {}, declaration = {})
|
|
107
|
+
request(:get, path, query: params, headers: declaration_headers(declaration))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# +collect+ for the operations the registry declares as POST. The parameters
|
|
111
|
+
# are identical; only where they travel changes.
|
|
112
|
+
def collect_post(path, params = {}, declaration = {})
|
|
113
|
+
request(:post, path, body: params, headers: declaration_headers(declaration))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# -- data subject rights -------------------------------------------------
|
|
117
|
+
def gdpr_notice = request(:get, "/v1/gdpr/notice")
|
|
118
|
+
def erasure(subject) = request(:post, "/v1/gdpr/erasure", body: subject)
|
|
119
|
+
def objection(subject) = request(:post, "/v1/gdpr/objections", body: subject)
|
|
120
|
+
def access_request(subject) = request(:post, "/v1/gdpr/access", body: subject)
|
|
121
|
+
def suppression = request(:get, "/v1/gdpr/suppression")
|
|
122
|
+
def unsuppress(subject_key) = request(:delete, "/v1/gdpr/suppression/#{escape(subject_key)}")
|
|
123
|
+
|
|
124
|
+
# -- service -------------------------------------------------------------
|
|
125
|
+
def health = request(:get, "/healthz")
|
|
126
|
+
def ready = request(:get, "/readyz")
|
|
127
|
+
def features = request(:get, "/v1/features")
|
|
128
|
+
def status = request(:get, "/v1/status")
|
|
129
|
+
def audit_head = request(:get, "/v1/audit/head")
|
|
130
|
+
|
|
131
|
+
def inspect = "#<IronEye::Client base_url=#{@base_url.inspect} key=#{masked_key.inspect}>"
|
|
132
|
+
|
|
133
|
+
def to_s = "#{inspect} # everything is an object; the key is not one you get"
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def request(method, path, query: {}, body: nil, raw: nil, headers: {})
|
|
138
|
+
uri = URI.parse(@base_url + path)
|
|
139
|
+
uri.query = URI.encode_www_form(query) unless query.nil? || query.empty?
|
|
140
|
+
attempt = 0
|
|
141
|
+
begin
|
|
142
|
+
started = monotonic
|
|
143
|
+
response = dispatch(method, uri, body, raw, headers)
|
|
144
|
+
interpret(response, method, path, monotonic - started)
|
|
145
|
+
rescue APIError => e
|
|
146
|
+
raise unless retry?(e, attempt)
|
|
147
|
+
|
|
148
|
+
wait(attempt, response, e.code, path)
|
|
149
|
+
attempt += 1
|
|
150
|
+
retry
|
|
151
|
+
rescue *NETWORK_ERRORS => e
|
|
152
|
+
raise ConnectionError, "#{method.upcase} #{path} failed: #{e.message}" if attempt >= @max_retries
|
|
153
|
+
|
|
154
|
+
wait(attempt, nil, "CONNECTION", path)
|
|
155
|
+
attempt += 1
|
|
156
|
+
retry
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
NETWORK_ERRORS = [
|
|
161
|
+
Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, IOError,
|
|
162
|
+
Net::OpenTimeout, Net::ReadTimeout, SocketError
|
|
163
|
+
].freeze
|
|
164
|
+
private_constant :NETWORK_ERRORS
|
|
165
|
+
|
|
166
|
+
def dispatch(method, uri, body, raw, headers)
|
|
167
|
+
klass = { get: Net::HTTP::Get, post: Net::HTTP::Post, delete: Net::HTTP::Delete }.fetch(method)
|
|
168
|
+
http_request = klass.new(uri)
|
|
169
|
+
http_request["Accept"] = "application/json"
|
|
170
|
+
http_request["Authorization"] = "Bearer #{@api_key}"
|
|
171
|
+
http_request["User-Agent"] = "ironeye-ruby/#{IronEye::VERSION}"
|
|
172
|
+
headers.each { |name, value| http_request[name] = value }
|
|
173
|
+
if raw
|
|
174
|
+
http_request.body = raw
|
|
175
|
+
elsif body
|
|
176
|
+
http_request["Content-Type"] = "application/json"
|
|
177
|
+
http_request.body = JSON.generate(body)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
Net::HTTP.start(uri.hostname, uri.port,
|
|
181
|
+
use_ssl: uri.scheme == "https",
|
|
182
|
+
open_timeout: @timeout,
|
|
183
|
+
read_timeout: @timeout) { |http| http.request(http_request) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def interpret(response, method, path, elapsed)
|
|
187
|
+
status = response.code.to_i
|
|
188
|
+
@logger.debug do
|
|
189
|
+
format("ironeye %<method>s %<path>s -> %<status>d in %<ms>dms (request_id=%<id>s)",
|
|
190
|
+
method: method.to_s.upcase, path: path, status: status,
|
|
191
|
+
ms: elapsed * 1000, id: response["x-request-id"] || "-")
|
|
192
|
+
end
|
|
193
|
+
return nil if [204, 205].include?(status)
|
|
194
|
+
|
|
195
|
+
payload = parse(response.body)
|
|
196
|
+
return payload if status < 400
|
|
197
|
+
|
|
198
|
+
raise IronEye.error_from(status, payload)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def parse(body)
|
|
202
|
+
return nil if body.nil? || body.empty?
|
|
203
|
+
|
|
204
|
+
JSON.parse(body)
|
|
205
|
+
rescue JSON::ParserError
|
|
206
|
+
{ "error" => { "code" => "INTERNAL", "message" => body[0, 200] } }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def retry?(error, attempt)
|
|
210
|
+
attempt < @max_retries && error.retryable? && RETRYABLE_STATUS.include?(error.status)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Retry-After is the server's own number, so it wins over the curve.
|
|
214
|
+
def wait(attempt, response, code, path)
|
|
215
|
+
advised = response && response["retry-after"].to_s
|
|
216
|
+
seconds = advised.to_s.match?(/\A\d+\z/) ? advised.to_i : (0.25 * (2**attempt)) + rand * 0.25
|
|
217
|
+
@logger.warn { format("ironeye %<path>s retrying after %<code>s in %<ms>dms", path: path, code: code, ms: seconds * 1000) }
|
|
218
|
+
sleep(seconds)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def declaration_headers(declaration)
|
|
222
|
+
declaration.each_with_object({}) do |(key, value), out|
|
|
223
|
+
name = DECLARATION_HEADERS[key.to_sym]
|
|
224
|
+
out[name] = value.to_s if name && value
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def multipart(boundary, file, filename, content_type, fields)
|
|
229
|
+
parts = fields.map do |name, value|
|
|
230
|
+
"--#{boundary}\r\nContent-Disposition: form-data; name=\"#{name}\"\r\n\r\n#{value}\r\n"
|
|
231
|
+
end
|
|
232
|
+
parts << "--#{boundary}\r\nContent-Disposition: form-data; name=\"file\"; " \
|
|
233
|
+
"filename=\"#{filename}\"\r\nContent-Type: #{content_type}\r\n\r\n"
|
|
234
|
+
"#{parts.join}#{file}\r\n--#{boundary}--\r\n".b
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def escape(value) = URI.encode_www_form_component(value.to_s)
|
|
238
|
+
|
|
239
|
+
# Enough of the key to recognise it in a log, never enough to use it.
|
|
240
|
+
def masked_key = @api_key.length > 12 ? "#{@api_key[0, 9]}..." : "..."
|
|
241
|
+
|
|
242
|
+
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IronEye
|
|
4
|
+
# Base of every error this gem raises.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# An error the server described in its response body.
|
|
8
|
+
#
|
|
9
|
+
# +retryable+ is the server's own verdict rather than an inference from the
|
|
10
|
+
# status code: a 429 from a spent monthly allowance is not the same wait as a
|
|
11
|
+
# 429 from a rate limiter, and only the body tells them apart.
|
|
12
|
+
class APIError < Error
|
|
13
|
+
attr_reader :status, :code, :retryable, :request_id, :suggested_action, :doc, :path, :meta
|
|
14
|
+
|
|
15
|
+
def initialize(status, body)
|
|
16
|
+
@status = status
|
|
17
|
+
@code = body["code"] || "INTERNAL"
|
|
18
|
+
@retryable = body.fetch("retryable", false)
|
|
19
|
+
@request_id = body["request_id"] || "-"
|
|
20
|
+
@suggested_action = body["suggested_action"].to_s
|
|
21
|
+
@doc = body["doc"].to_s
|
|
22
|
+
@path = body["path"]
|
|
23
|
+
@meta = body["meta"] || {}
|
|
24
|
+
super("#{@code}: #{body["message"]} (request_id=#{@request_id})")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
alias retryable? retryable
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class AuthenticationError < APIError; end
|
|
31
|
+
class PermissionError < APIError; end
|
|
32
|
+
class RateLimitError < APIError; end
|
|
33
|
+
class InvalidRequestError < APIError; end
|
|
34
|
+
class NotFoundError < APIError; end
|
|
35
|
+
class ComplianceError < APIError; end
|
|
36
|
+
class UpstreamError < APIError; end
|
|
37
|
+
class ServerError < APIError; end
|
|
38
|
+
|
|
39
|
+
# A transport failure, where there is no server verdict to read.
|
|
40
|
+
class ConnectionError < Error
|
|
41
|
+
def retryable? = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
FAMILIES = {
|
|
45
|
+
"UNAUTHENTICATED" => AuthenticationError,
|
|
46
|
+
"FORBIDDEN_SCOPE" => PermissionError,
|
|
47
|
+
"PLAN_LIMITED" => PermissionError,
|
|
48
|
+
"RATE_LIMITED" => RateLimitError,
|
|
49
|
+
"QUOTA_EXHAUSTED" => RateLimitError,
|
|
50
|
+
"TENANT_BUSY" => RateLimitError,
|
|
51
|
+
"NOT_FOUND" => NotFoundError,
|
|
52
|
+
"COMPLIANCE_REFUSED" => ComplianceError,
|
|
53
|
+
"COLLECTION_BLOCKED" => ComplianceError,
|
|
54
|
+
"SOURCE_NOT_CONFIGURED" => UpstreamError,
|
|
55
|
+
"UPSTREAM_REFUSED" => UpstreamError,
|
|
56
|
+
"UPSTREAM_THROTTLED" => UpstreamError,
|
|
57
|
+
"INTERNAL" => ServerError,
|
|
58
|
+
"DEPENDENCY_UNAVAILABLE" => ServerError,
|
|
59
|
+
"SERVER_DRAINING" => ServerError
|
|
60
|
+
}.freeze
|
|
61
|
+
private_constant :FAMILIES
|
|
62
|
+
|
|
63
|
+
# Builds the narrowest error class the response body justifies.
|
|
64
|
+
def self.error_from(status, payload)
|
|
65
|
+
body = payload.is_a?(Hash) ? payload["error"] : nil
|
|
66
|
+
unless body.is_a?(Hash) && body["code"]
|
|
67
|
+
return ServerError.new(status, {
|
|
68
|
+
"code" => "INTERNAL",
|
|
69
|
+
"message" => "The server returned #{status} with no error body.",
|
|
70
|
+
"retryable" => status >= 500,
|
|
71
|
+
"suggested_action" => "Retry, and quote the status if it persists."
|
|
72
|
+
})
|
|
73
|
+
end
|
|
74
|
+
FAMILIES.fetch(body["code"], InvalidRequestError).new(status, body)
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/ironeye.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Official Ruby client for the IronEye API.
|
|
4
|
+
#
|
|
5
|
+
# eye = IronEye::Client.new # IRONEYE_API_KEY from the environment
|
|
6
|
+
# result = eye.secrets(input: { text: File.read("config.env") })
|
|
7
|
+
# result.dig("security", "secrets", "secret_count")
|
|
8
|
+
#
|
|
9
|
+
# Pass +logger:+ to see one line per request: method, route, status, duration
|
|
10
|
+
# and request id. No credential and no payload is ever written to it.
|
|
11
|
+
module IronEye
|
|
12
|
+
VERSION = "1.0.0"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative "ironeye/errors"
|
|
16
|
+
require_relative "ironeye/client"
|
|
17
|
+
|
|
18
|
+
module IronEye
|
|
19
|
+
def self.matz
|
|
20
|
+
puts <<~CREED
|
|
21
|
+
Optimise for the reader, not the writer.
|
|
22
|
+
A finding without evidence is a rumour.
|
|
23
|
+
Refuse loudly rather than guess quietly.
|
|
24
|
+
Nothing touches disk.
|
|
25
|
+
|
|
26
|
+
...forged at Direct Softworks.
|
|
27
|
+
CREED
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ironeye
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Direct Softworks
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email:
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- LICENSE
|
|
20
|
+
- README.md
|
|
21
|
+
- lib/ironeye.rb
|
|
22
|
+
- lib/ironeye/client.rb
|
|
23
|
+
- lib/ironeye/errors.rb
|
|
24
|
+
homepage: https://ironeye.org
|
|
25
|
+
licenses:
|
|
26
|
+
- MIT
|
|
27
|
+
metadata:
|
|
28
|
+
documentation_uri: https://ironeye.org/docs/sdk/ruby
|
|
29
|
+
homepage_uri: https://ironeye.org
|
|
30
|
+
source_code_uri: https://github.com/IronEyeAPI/ironeye-ruby
|
|
31
|
+
bug_tracker_uri: https://github.com/IronEyeAPI/ironeye-ruby/issues
|
|
32
|
+
rubygems_mfa_required: 'true'
|
|
33
|
+
post_install_message:
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '3.1'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.5.22
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Official Ruby client for the IronEye document intelligence and collection
|
|
52
|
+
API.
|
|
53
|
+
test_files: []
|