auditea 0.1.0.beta.1
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/CHANGELOG.md +8 -0
- data/DESIGN.md +48 -0
- data/LICENSE +21 -0
- data/README.md +117 -0
- data/RELEASE.md +43 -0
- data/auditea.gemspec +36 -0
- data/examples/manual_capture.rb +50 -0
- data/lib/auditea/actor.rb +26 -0
- data/lib/auditea/client.rb +46 -0
- data/lib/auditea/configuration.rb +125 -0
- data/lib/auditea/current_context.rb +34 -0
- data/lib/auditea/event_builder.rb +81 -0
- data/lib/auditea/instrumentation/active_job.rb +136 -0
- data/lib/auditea/instrumentation/exception.rb +75 -0
- data/lib/auditea/instrumentation/logger_adapter.rb +28 -0
- data/lib/auditea/instrumentation/request.rb +76 -0
- data/lib/auditea/log.rb +35 -0
- data/lib/auditea/middleware/context.rb +35 -0
- data/lib/auditea/railtie.rb +27 -0
- data/lib/auditea/sanitizer.rb +85 -0
- data/lib/auditea/target.rb +25 -0
- data/lib/auditea/transport/delivery_result.rb +68 -0
- data/lib/auditea/transport/http.rb +253 -0
- data/lib/auditea/transport/limits.rb +28 -0
- data/lib/auditea/transport/queue.rb +232 -0
- data/lib/auditea/version.rb +5 -0
- data/lib/auditea.rb +149 -0
- metadata +73 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 308c6d6709fed09f200bcfc60829af1b7bd470a018691f456576818ccadc87ee
|
|
4
|
+
data.tar.gz: 27a669b879d0d864d427359bae7c01daf27e2a2d492e45ed73b492136d659196
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 18517d6f3b825b1f1c97daab0bce3d6da97f68b157d87fb6c6aa302d0b6d79e1e202a682d4c427824df2ae71e7a7f51004124b8cc5ee7246d4d898e0e78cdc06
|
|
7
|
+
data.tar.gz: c8faa401027af8fadaa8f565994a875c047bed3840722ef101673c7a4565e554965e13e7f1d9b313bd9245499ffb02110145650ade188891278ec06e5aa87d6d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0.beta.1 (unreleased)
|
|
4
|
+
|
|
5
|
+
- Initial SDK: capture, structured logs, Rails request/exception observation, ActiveJob failures
|
|
6
|
+
- EVENT_CONTRACT_V1 transport with HTTPS/loopback policy, status classification, byte limits, batch index results
|
|
7
|
+
- Process-aware async queue, flush waits for in-flight work, privacy.redacted_fields
|
|
8
|
+
- Manual smoke script: `examples/manual_capture.rb`
|
data/DESIGN.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# AUD-P009 / HR01 Design notes
|
|
2
|
+
|
|
3
|
+
Issue: https://github.com/reznis/auditea-ruby/issues/1
|
|
4
|
+
PR: https://github.com/reznis/auditea-ruby/pull/2
|
|
5
|
+
|
|
6
|
+
## Authority
|
|
7
|
+
|
|
8
|
+
- Wire contract: AudiTea vNext `EVENT_CONTRACT_V1`
|
|
9
|
+
- Legacy SDK: design input, not a blind port
|
|
10
|
+
|
|
11
|
+
## HR01 transport foundations
|
|
12
|
+
|
|
13
|
+
- HTTPS required; HTTP only for localhost / 127.0.0.1 / ::1
|
|
14
|
+
- Explicit status classification (201/200/409/422/401/413/415/3xx/429/5xx)
|
|
15
|
+
- No redirect following; Authorization never forwarded elsewhere
|
|
16
|
+
- Injectable sleeper/random for bounded exponential backoff + jitter
|
|
17
|
+
- Client-side 128 KiB / 1 MiB / 100-event limits with byte-aware batch splitting
|
|
18
|
+
- Batch results mapped by `index`; accepted/duplicate siblings not retried
|
|
19
|
+
- Process PID ownership for pre-fork safety
|
|
20
|
+
- `flush` waits for buffer empty **and** in-flight delivery
|
|
21
|
+
- `privacy.redacted_fields` records filtered paths
|
|
22
|
+
- ActiveJob final-failure → `job.failed` without job arguments
|
|
23
|
+
|
|
24
|
+
## Legacy classification (summary)
|
|
25
|
+
|
|
26
|
+
| Capability | Class |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Explicit capture / track API | KEEP CONCEPT + PORT |
|
|
29
|
+
| Actor / Target helpers | REFACTOR (no email-by-default) |
|
|
30
|
+
| Configuration + Railtie | PORT (observe opt-in) |
|
|
31
|
+
| HTTP Bearer + retries | REFACTOR |
|
|
32
|
+
| Fail-open | KEEP CONCEPT + PORT |
|
|
33
|
+
| Client `event_id` | KEEP CONCEPT + PORT |
|
|
34
|
+
| Client `contract_version` / tenancy fields | DROP / REJECT (vNext conflict) |
|
|
35
|
+
| Request context middleware | PORT + REFACTOR (no session_id) |
|
|
36
|
+
| Rails auto observe | REFACTOR (single subscriber; requests opt-in) |
|
|
37
|
+
| ActionMappingConfig YAML dictionary | REFERENCE ONLY → server taxonomy |
|
|
38
|
+
| Privacy filter | KEEP CONCEPT + REFACTOR (all egress + redacted_fields) |
|
|
39
|
+
| Exception capture | REFACTOR (fixed frames) |
|
|
40
|
+
| Security surface | KEEP CONCEPT (deferred beyond beta.1 core) |
|
|
41
|
+
| `Thread.new` per event | REWRITE → bounded queue |
|
|
42
|
+
| `AUDITEA_DEBUG` full payload logs | DROP |
|
|
43
|
+
| Auto Rails.logger forward | DROP for beta.1; adapter boundary reserved |
|
|
44
|
+
|
|
45
|
+
## Compatibility
|
|
46
|
+
|
|
47
|
+
- Ruby >= 3.2, including current Ruby 4.0.x (CI: 3.2, 3.3, 3.4, 4.0.6)
|
|
48
|
+
- Rails 7.2 and Rails 8.1 gemfiles under `gemfiles/`
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AudiTea contributors
|
|
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,117 @@
|
|
|
1
|
+
# auditea
|
|
2
|
+
|
|
3
|
+
Ruby / Rails SDK for [AudiTea](https://github.com/reznis/AudiTea) event collection.
|
|
4
|
+
|
|
5
|
+
Version: **0.1.0.beta.1**
|
|
6
|
+
|
|
7
|
+
Captures durable technical and business evidence. AudiTea **server** owns taxonomy, dictionary, human labels, and reinterpretation.
|
|
8
|
+
|
|
9
|
+
## Install (after RubyGems beta publication)
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "auditea", "0.1.0.beta.1"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bundle install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Contributor / pre-release fallback
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "auditea", github: "reznis/auditea-ruby", branch: "main"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Prefer the RubyGems artifact for ordinary TEST deployments once published.
|
|
27
|
+
|
|
28
|
+
## Configure
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# config/initializers/auditea.rb
|
|
32
|
+
Auditea.configure do |config|
|
|
33
|
+
config.endpoint = ENV.fetch("AUDITEA_ENDPOINT", "https://app.auditea.com")
|
|
34
|
+
config.source_token = ENV.fetch("AUDITEA_SOURCE_TOKEN") # at_src_<public_id>_<secret>
|
|
35
|
+
config.capture_requests = true # opt-in Rails request observation
|
|
36
|
+
config.capture_exceptions = true
|
|
37
|
+
config.release = ENV["RELEASE"]
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Environment variables: `AUDITEA_ENDPOINT`, `AUDITEA_SOURCE_TOKEN` (or `AUDITEA_API_KEY`), `AUDITEA_CAPTURE_REQUESTS`, `AUDITEA_CAPTURE_EXCEPTIONS`, `AUDITEA_ASYNC`, `AUDITEA_RELEASE`.
|
|
42
|
+
|
|
43
|
+
## Capture APIs
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
Auditea.capture(
|
|
47
|
+
"project.approved",
|
|
48
|
+
actor: { type: "user", id: current_user.id },
|
|
49
|
+
target: { type: "Project", id: project.id },
|
|
50
|
+
evidence: { business: { from_status: "draft", to_status: "approved" } }
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
Auditea.log(:info, "Export finished", context: { export_id: export.id })
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Wire format follows AudiTea **EVENT_CONTRACT_V1**:
|
|
57
|
+
|
|
58
|
+
- Bearer Source credential authentication
|
|
59
|
+
- No client `contract_version`
|
|
60
|
+
- No client tenancy/scope fields (`workspace_id`, `environment`, …)
|
|
61
|
+
- Extensible `evidence` / `context` / `provenance` / `metadata` bags
|
|
62
|
+
|
|
63
|
+
## Rails
|
|
64
|
+
|
|
65
|
+
The Railtie installs:
|
|
66
|
+
|
|
67
|
+
- request context middleware (path **without** query string, request_id, method)
|
|
68
|
+
- optional `process_action` observation → `network.request.completed`
|
|
69
|
+
- optional exception observation → `exception.raised`
|
|
70
|
+
|
|
71
|
+
Structural evidence is preserved so the server can later distinguish routes such as `/researchers/projects/18` vs `/bcer/1/projects/18` without SDK redeploy.
|
|
72
|
+
|
|
73
|
+
### Structured logs
|
|
74
|
+
|
|
75
|
+
Use `Auditea.log` explicitly.
|
|
76
|
+
|
|
77
|
+
**Automatic `Rails.logger` forwarding is intentionally disabled in beta.1** (`Auditea::Instrumentation::LoggerAdapter`).
|
|
78
|
+
|
|
79
|
+
## Transport
|
|
80
|
+
|
|
81
|
+
- Sync or single-worker async queue (not one thread per event)
|
|
82
|
+
- HTTPS required (HTTP allowed only for localhost / 127.0.0.1 / ::1)
|
|
83
|
+
- Single events → `POST /api/v1/events`
|
|
84
|
+
- Batches → `POST /api/v1/event_batches` with per-index result handling
|
|
85
|
+
- Client-side 128 KiB / 1 MiB / 100-event limits
|
|
86
|
+
- Retries with injectable exponential backoff+jitter on 5xx / 429 / network
|
|
87
|
+
- Process-aware queue for pre-fork servers
|
|
88
|
+
- Fail-open by default (`raise_errors: false`)
|
|
89
|
+
|
|
90
|
+
## Manual smoke (local AudiTea)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
AUDITEA_ENDPOINT=http://localhost:3000 \
|
|
94
|
+
AUDITEA_SOURCE_TOKEN=at_src_... \
|
|
95
|
+
bundle exec ruby examples/manual_capture.rb
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Never commit a real token. The script prints a minimal safe result only.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT. See `LICENSE`.
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
bundle install
|
|
108
|
+
bundle exec rspec
|
|
109
|
+
bundle exec rubocop
|
|
110
|
+
gem build auditea.gemspec
|
|
111
|
+
|
|
112
|
+
# Rails compatibility (optional local)
|
|
113
|
+
BUNDLE_GEMFILE=gemfiles/rails_7.2.gemfile bundle install
|
|
114
|
+
BUNDLE_GEMFILE=gemfiles/rails_7.2.gemfile bundle exec rspec
|
|
115
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.1.gemfile bundle install
|
|
116
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.1.gemfile bundle exec rspec
|
|
117
|
+
```
|
data/RELEASE.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Release: auditea 0.1.0.beta.1
|
|
2
|
+
|
|
3
|
+
## Preconditions (blockers)
|
|
4
|
+
|
|
5
|
+
1. Independent human review of the PR
|
|
6
|
+
2. Merge to `main`
|
|
7
|
+
3. Exact-head CI green on merge commit
|
|
8
|
+
4. License approved: **MIT**
|
|
9
|
+
5. RubyGems MFA / owner account for `reznis` with push rights
|
|
10
|
+
6. OTP / API key for `gem push` stored outside the repository (never commit secrets)
|
|
11
|
+
|
|
12
|
+
## Build
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git checkout main
|
|
16
|
+
git pull
|
|
17
|
+
bundle install
|
|
18
|
+
bundle exec rspec
|
|
19
|
+
gem build auditea.gemspec
|
|
20
|
+
# => auditea-0.1.0.beta.1.gem
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Publish
|
|
24
|
+
|
|
25
|
+
Publish only from reviewed `main` after its exact-head CI is green:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
gem push auditea-0.1.0.beta.1.gem
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Optional GitHub Actions: use `rubygems/configure-rubygems-credentials` with a repository secret `RUBYGEMS_API_KEY` — never commit the secret.
|
|
32
|
+
|
|
33
|
+
## Verify
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gem install auditea -v 0.1.0.beta.1 --pre
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then install in Semiweb TEST via:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
gem "auditea", "0.1.0.beta.1"
|
|
43
|
+
```
|
data/auditea.gemspec
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/auditea/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "auditea"
|
|
7
|
+
spec.version = Auditea::VERSION
|
|
8
|
+
spec.authors = ["AudiTea"]
|
|
9
|
+
spec.email = ["opensource@auditea.app"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Ruby/Rails SDK for AudiTea event collection"
|
|
12
|
+
spec.description =
|
|
13
|
+
"Capture business events, Rails request evidence, exceptions, and structured logs " \
|
|
14
|
+
"for AudiTea /api/v1."
|
|
15
|
+
spec.homepage = "https://github.com/reznis/auditea-ruby"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/reznis/auditea-ruby"
|
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/reznis/auditea-ruby/blob/main/CHANGELOG.md"
|
|
22
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
23
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
24
|
+
|
|
25
|
+
spec.files = Dir[
|
|
26
|
+
"lib/**/*",
|
|
27
|
+
"examples/**/*",
|
|
28
|
+
"CHANGELOG.md",
|
|
29
|
+
"README.md",
|
|
30
|
+
"DESIGN.md",
|
|
31
|
+
"RELEASE.md",
|
|
32
|
+
"LICENSE*",
|
|
33
|
+
"auditea.gemspec"
|
|
34
|
+
]
|
|
35
|
+
spec.require_paths = ["lib"]
|
|
36
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Manual smoke against a local AudiTea server. Never commit tokens.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# AUDITEA_ENDPOINT=http://localhost:3000 \
|
|
7
|
+
# AUDITEA_SOURCE_TOKEN=at_src_... \
|
|
8
|
+
# bundle exec ruby examples/manual_capture.rb
|
|
9
|
+
|
|
10
|
+
require "bundler/setup"
|
|
11
|
+
require "auditea"
|
|
12
|
+
|
|
13
|
+
endpoint = ENV.fetch("AUDITEA_ENDPOINT")
|
|
14
|
+
token = ENV.fetch("AUDITEA_SOURCE_TOKEN")
|
|
15
|
+
|
|
16
|
+
Auditea.configure do |config|
|
|
17
|
+
config.endpoint = endpoint
|
|
18
|
+
config.source_token = token
|
|
19
|
+
config.async = false
|
|
20
|
+
config.raise_errors = true
|
|
21
|
+
config.logger = Logger.new($stdout)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
result = Auditea.capture(
|
|
25
|
+
"sdk.manual_smoke",
|
|
26
|
+
category: "debug",
|
|
27
|
+
severity: "info",
|
|
28
|
+
evidence: {
|
|
29
|
+
"smoke" => {
|
|
30
|
+
"script" => "examples/manual_capture.rb",
|
|
31
|
+
"note" => "manual capture from auditea-ruby"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
metadata: {
|
|
35
|
+
"sdk_event_kind" => "manual_smoke"
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
Auditea.flush(timeout: 5)
|
|
40
|
+
|
|
41
|
+
safe = {
|
|
42
|
+
ok: result[:ok],
|
|
43
|
+
event_id: result[:event_id],
|
|
44
|
+
outcome: result[:outcome],
|
|
45
|
+
status: result[:status],
|
|
46
|
+
queued: result[:queued],
|
|
47
|
+
reason: result[:reason]
|
|
48
|
+
}.compact
|
|
49
|
+
|
|
50
|
+
puts "auditea manual_capture result=#{safe.inspect}"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
module Actor
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# No email-by-default (REWRITE of legacy Actor.from_user PII default).
|
|
8
|
+
def from_user(user, type: nil)
|
|
9
|
+
return nil unless user
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"type" => type || user.class&.name,
|
|
13
|
+
"id" => safe(user, :id) || safe(user, :to_param)
|
|
14
|
+
}.compact
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def safe(object, method_name)
|
|
18
|
+
return nil unless object.respond_to?(method_name)
|
|
19
|
+
|
|
20
|
+
value = object.public_send(method_name)
|
|
21
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?) ? nil : value.to_s
|
|
22
|
+
rescue StandardError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
class Client
|
|
5
|
+
def initialize(configuration = nil, http: nil, queue: nil)
|
|
6
|
+
@configuration = configuration
|
|
7
|
+
@injected_http = http
|
|
8
|
+
@injected_queue = queue
|
|
9
|
+
rebuild!
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def configuration
|
|
13
|
+
@configuration || Auditea.configuration
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def enqueue(event)
|
|
17
|
+
queue.enqueue(event)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def flush(timeout: 5)
|
|
21
|
+
queue.flush(timeout: timeout)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def shutdown
|
|
25
|
+
queue.shutdown
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset!
|
|
29
|
+
shutdown
|
|
30
|
+
rebuild!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def queue
|
|
36
|
+
@queue || rebuild!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def rebuild!
|
|
40
|
+
cfg = configuration
|
|
41
|
+
@http = @injected_http || Transport::Http.new(cfg)
|
|
42
|
+
@queue = @injected_queue || Transport::Queue.new(cfg, http: @http)
|
|
43
|
+
@queue
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
class Configuration
|
|
5
|
+
DEFAULT_ENDPOINT = "https://app.auditea.com"
|
|
6
|
+
DEFAULT_BATCH_SIZE = 50
|
|
7
|
+
DEFAULT_FLUSH_INTERVAL = 2.0
|
|
8
|
+
DEFAULT_QUEUE_CAPACITY = 1_000
|
|
9
|
+
DEFAULT_TIMEOUT = 3.0
|
|
10
|
+
DEFAULT_OPEN_TIMEOUT = 2.0
|
|
11
|
+
DEFAULT_WRITE_TIMEOUT = 3.0
|
|
12
|
+
DEFAULT_MAX_RETRIES = 3
|
|
13
|
+
DEFAULT_MESSAGE_MAX = 8_192
|
|
14
|
+
DEFAULT_NESTING_MAX = 16
|
|
15
|
+
LOOPBACK_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
|
|
16
|
+
|
|
17
|
+
attr_accessor :endpoint, :source_token, :enabled, :async, :raise_errors,
|
|
18
|
+
:batch_size, :flush_interval, :queue_capacity, :timeout,
|
|
19
|
+
:open_timeout, :write_timeout, :max_retries, :logger, :release, :sdk_name,
|
|
20
|
+
:capture_requests, :capture_exceptions, :capture_request_params,
|
|
21
|
+
:capture_active_job, :message_max_bytes, :nesting_max_depth
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@endpoint = ENV.fetch("AUDITEA_ENDPOINT", DEFAULT_ENDPOINT)
|
|
25
|
+
@source_token = ENV["AUDITEA_SOURCE_TOKEN"] || ENV.fetch("AUDITEA_API_KEY", nil)
|
|
26
|
+
@enabled = truthy?(ENV.fetch("AUDITEA_ENABLED", "true"))
|
|
27
|
+
@async = truthy?(ENV.fetch("AUDITEA_ASYNC", "true"))
|
|
28
|
+
@raise_errors = truthy?(ENV.fetch("AUDITEA_RAISE_ERRORS", "false"))
|
|
29
|
+
@batch_size = integer_env("AUDITEA_BATCH_SIZE", DEFAULT_BATCH_SIZE)
|
|
30
|
+
@flush_interval = float_env("AUDITEA_FLUSH_INTERVAL", DEFAULT_FLUSH_INTERVAL)
|
|
31
|
+
@queue_capacity = integer_env("AUDITEA_QUEUE_CAPACITY", DEFAULT_QUEUE_CAPACITY)
|
|
32
|
+
@timeout = float_env("AUDITEA_TIMEOUT", DEFAULT_TIMEOUT)
|
|
33
|
+
@open_timeout = float_env("AUDITEA_OPEN_TIMEOUT", DEFAULT_OPEN_TIMEOUT)
|
|
34
|
+
@write_timeout = float_env("AUDITEA_WRITE_TIMEOUT", DEFAULT_WRITE_TIMEOUT)
|
|
35
|
+
@max_retries = integer_env("AUDITEA_MAX_RETRIES", DEFAULT_MAX_RETRIES)
|
|
36
|
+
@logger = default_logger
|
|
37
|
+
@release = ENV["AUDITEA_RELEASE"] || ENV["RELEASE"] || ENV.fetch("HEROKU_RELEASE_VERSION", nil)
|
|
38
|
+
@sdk_name = "auditea-ruby"
|
|
39
|
+
@capture_requests = truthy?(ENV.fetch("AUDITEA_CAPTURE_REQUESTS", "false"))
|
|
40
|
+
@capture_exceptions = truthy?(ENV.fetch("AUDITEA_CAPTURE_EXCEPTIONS", "true"))
|
|
41
|
+
@capture_request_params = truthy?(ENV.fetch("AUDITEA_CAPTURE_REQUEST_PARAMS", "false"))
|
|
42
|
+
@capture_active_job = truthy?(ENV.fetch("AUDITEA_CAPTURE_ACTIVE_JOB", "true"))
|
|
43
|
+
@message_max_bytes = integer_env("AUDITEA_MESSAGE_MAX_BYTES", DEFAULT_MESSAGE_MAX)
|
|
44
|
+
@nesting_max_depth = integer_env("AUDITEA_NESTING_MAX_DEPTH", DEFAULT_NESTING_MAX)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def enabled?
|
|
48
|
+
!!@enabled
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def async?
|
|
52
|
+
!!@async
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def configured?
|
|
56
|
+
source_token.to_s.strip != "" && endpoint.to_s.strip != ""
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate!
|
|
60
|
+
raise ConfigurationError, "endpoint must be HTTPS (or HTTP to localhost/127.0.0.1/::1)" unless valid_endpoint?
|
|
61
|
+
raise ConfigurationError, "batch_size must be 1..100" unless batch_size.between?(1, 100)
|
|
62
|
+
raise ConfigurationError, "queue_capacity must be >= 1" unless queue_capacity >= 1
|
|
63
|
+
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def events_url
|
|
68
|
+
join_path("/api/v1/events")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def event_batches_url
|
|
72
|
+
join_path("/api/v1/event_batches")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def valid_endpoint?
|
|
76
|
+
uri = URI.parse(endpoint.to_s)
|
|
77
|
+
host = normalize_host(uri.host)
|
|
78
|
+
return false if host.empty?
|
|
79
|
+
|
|
80
|
+
case uri
|
|
81
|
+
when URI::HTTPS
|
|
82
|
+
true
|
|
83
|
+
when URI::HTTP
|
|
84
|
+
LOOPBACK_HOSTS.include?(host)
|
|
85
|
+
else
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
rescue URI::InvalidURIError
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def normalize_host(host)
|
|
95
|
+
host.to_s.downcase.delete_prefix("[").delete_suffix("]")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def join_path(path)
|
|
99
|
+
base = endpoint.to_s.sub(%r{/\z}, "")
|
|
100
|
+
"#{base}#{path}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def default_logger
|
|
104
|
+
return Rails.logger if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
105
|
+
|
|
106
|
+
Logger.new($stdout)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def truthy?(value)
|
|
110
|
+
%w[1 true yes on].include?(value.to_s.strip.downcase)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def integer_env(name, default)
|
|
114
|
+
Integer(ENV.fetch(name, default))
|
|
115
|
+
rescue ArgumentError, TypeError
|
|
116
|
+
default
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def float_env(name, default)
|
|
120
|
+
Float(ENV.fetch(name, default))
|
|
121
|
+
rescue ArgumentError, TypeError
|
|
122
|
+
default
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
# Request-scoped enrichment bag. Stored without session_id / cookies / credentials.
|
|
5
|
+
module CurrentContext
|
|
6
|
+
class << self
|
|
7
|
+
def store
|
|
8
|
+
Thread.current[:auditea_current_context] ||= {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def clear!
|
|
12
|
+
Thread.current[:auditea_current_context] = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def set(attrs)
|
|
16
|
+
store.merge!(stringify(attrs))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
store.dup
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def stringify(attrs)
|
|
26
|
+
attrs.each_with_object({}) do |(key, value), memo|
|
|
27
|
+
next if value.nil?
|
|
28
|
+
|
|
29
|
+
memo[key.to_s] = value.is_a?(Hash) ? stringify(value) : value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
module EventBuilder
|
|
5
|
+
ACTION_FORMAT = /\A[a-z][a-z0-9_.]{0,126}\z/
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def build(action:, category: nil, severity: nil, actor: nil, target: nil,
|
|
10
|
+
context: nil, evidence: nil, provenance: nil, metadata: nil,
|
|
11
|
+
privacy: nil, occurred_at: nil, event_id: nil)
|
|
12
|
+
action_key = action.to_s
|
|
13
|
+
raise ArgumentError, "invalid action: #{action_key.inspect}" unless action_key.match?(ACTION_FORMAT)
|
|
14
|
+
|
|
15
|
+
event = {
|
|
16
|
+
"event_id" => presence(event_id) || SecureRandom.uuid,
|
|
17
|
+
"occurred_at" => format_time(occurred_at || Time.now.utc),
|
|
18
|
+
"action" => action_key
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
event["category"] = category.to_s if presence(category)
|
|
22
|
+
event["severity"] = severity.to_s if presence(severity)
|
|
23
|
+
event["actor"] = stringify_keys(actor) if actor
|
|
24
|
+
event["target"] = stringify_keys(target) if target
|
|
25
|
+
|
|
26
|
+
merged_context = merge_context(context)
|
|
27
|
+
event["context"] = merged_context if merged_context
|
|
28
|
+
|
|
29
|
+
event["evidence"] = stringify_keys(evidence) if evidence
|
|
30
|
+
event["provenance"] = merge_provenance(provenance)
|
|
31
|
+
event["metadata"] = stringify_keys(metadata) if metadata
|
|
32
|
+
event["privacy"] = stringify_keys(privacy) if privacy
|
|
33
|
+
|
|
34
|
+
# Never include contract_version or tenancy/scope selectors (server-owned).
|
|
35
|
+
event
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def merge_context(context)
|
|
39
|
+
base = CurrentContext.to_h
|
|
40
|
+
extra = stringify_keys(context || {})
|
|
41
|
+
merged = base.merge(extra)
|
|
42
|
+
merged.empty? ? nil : merged
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def merge_provenance(provenance)
|
|
46
|
+
base = {
|
|
47
|
+
"sdk" => Auditea.configuration.sdk_name,
|
|
48
|
+
"sdk_version" => VERSION,
|
|
49
|
+
"runtime" => "ruby",
|
|
50
|
+
"ruby_version" => RUBY_VERSION
|
|
51
|
+
}
|
|
52
|
+
release = Auditea.configuration.release
|
|
53
|
+
base["release"] = release if presence(release)
|
|
54
|
+
base["rails_version"] = Rails.version if defined?(Rails) && Rails.respond_to?(:version)
|
|
55
|
+
base.merge(stringify_keys(provenance || {}))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def format_time(time)
|
|
59
|
+
time = Time.parse(time.to_s) unless time.respond_to?(:utc)
|
|
60
|
+
time.utc.iso8601
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def stringify_keys(value)
|
|
64
|
+
case value
|
|
65
|
+
when Hash
|
|
66
|
+
value.each_with_object({}) { |(k, v), memo| memo[k.to_s] = stringify_keys(v) }
|
|
67
|
+
when Array
|
|
68
|
+
value.map { |v| stringify_keys(v) }
|
|
69
|
+
else
|
|
70
|
+
value
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def presence(value)
|
|
75
|
+
return nil if value.nil?
|
|
76
|
+
return nil if value.respond_to?(:empty?) && value.empty?
|
|
77
|
+
|
|
78
|
+
value
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|