auditea 0.1.0.beta.3 → 0.1.0.beta.5
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/CHANGELOG.md +40 -0
- data/README.md +16 -3
- data/RELEASE.md +21 -5
- data/examples/support/windows_smoke_gate.rb +43 -0
- data/examples/windows_inventory_smoke.rb +65 -0
- data/lib/auditea/client.rb +10 -0
- data/lib/auditea/configuration.rb +8 -1
- data/lib/auditea/diagnostic.rb +86 -0
- data/lib/auditea/host_inventory/collector.rb +63 -1
- data/lib/auditea/host_inventory/component_detectors.rb +315 -0
- data/lib/auditea/host_inventory/windows_components.rb +285 -0
- data/lib/auditea/inventory/collector.rb +126 -0
- data/lib/auditea/inventory/components.rb +80 -0
- data/lib/auditea/inventory/database_server.rb +57 -0
- data/lib/auditea/railtie.rb +31 -3
- data/lib/auditea/sanitizer.rb +40 -1
- data/lib/auditea/transport/queue.rb +78 -2
- data/lib/auditea/version.rb +1 -1
- data/lib/auditea.rb +16 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ed0a68e5bfed09c0f764146e65c9b995d11c7070224a19fc6e682367626d0d1
|
|
4
|
+
data.tar.gz: cfdc642f25cab8125b3c8479b9aacbbaa13613650fa12a9b8160e21d92c2881f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35cd56bc10b1010767e8078d5719639e12a056bc13aacd9472d1cda46d9679ddefba7519988af49faa26f7efd8ee664bdb1fe3f78eb27655b74c391a556406b6
|
|
7
|
+
data.tar.gz: 7911d88e0322ceba880eaeb09f0825d74079107612022aa60fbfff1e6e974a43a44b31d386ee304a5ff1d855d17ea1ca657198c4c3637eb7db6700ac2cb03dd6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0.beta.5 (unreleased)
|
|
4
|
+
|
|
5
|
+
### SDK readiness diagnostic
|
|
6
|
+
|
|
7
|
+
- `Auditea.diagnostic` returns a machine-readable readiness snapshot (config flags, endpoint, token presence, release, transport health, queue counters, warnings) — never the token value
|
|
8
|
+
- `Auditea.emit_diagnostic!` emits the snapshot as a `diagnostic.sdk_ready` event (category `diagnostic`)
|
|
9
|
+
- Rails boot emits one automatic **boot/startup diagnostic observation** when configured, in asynchronous mode only (fail-open, non-blocking). In synchronous mode the automatic emission is skipped so boot never performs inline network retry work; explicit user-requested synchronous delivery is unchanged. This is not a heartbeat and does not prove ongoing process liveness.
|
|
10
|
+
- The snapshot describes transport state **before** the delivery of the diagnostic event that carries it; the first received diagnostic therefore has no prior delivery result.
|
|
11
|
+
- `Transport::Queue` now tracks `delivered` count and last delivery `outcome`/`error` (`Client#health` / `Client#stats`); batched delivery is aggregated truthfully (all-accepted vs partial/failed)
|
|
12
|
+
|
|
13
|
+
## 0.1.0.beta.4
|
|
14
|
+
|
|
15
|
+
### Platform component inventory (AUD-PLATFORM-INVENTORY01)
|
|
16
|
+
|
|
17
|
+
- Additive optional `evidence.inventory.components[]` on `runtime.inventory` and `host.inventory` (schema_version remains 1)
|
|
18
|
+
- Component fields: `product`, `version`, `scope`, `observation_method`, optional `package` / `evidence`
|
|
19
|
+
- Scopes: `application_runtime`, `application_server`, `database_server`, `linked_library`, `host_os`, `host_component`, `reverse_proxy`, `externally_observed`
|
|
20
|
+
- Runtime: promote Ruby/Rails/Bundler/RubyGems/linked OpenSSL; promote Puma (and peers) from packages; PostgreSQL server via fail-open `SHOW server_version` (never host-attached)
|
|
21
|
+
- Host: OS/kernel components; allowlisted detectors (`openssl version`, `apache2`/`httpd -v`, `nginx -v`) with timeout, bounded I/O, no shell, no sudo
|
|
22
|
+
- Absent reverse proxies are simply not observed (never claimed “not installed”)
|
|
23
|
+
|
|
24
|
+
### FIX01 hardening
|
|
25
|
+
|
|
26
|
+
- Detectors resolve allowlisted basenames from fixed trusted system directories only (realpath boundary); never `ENV["PATH"]`
|
|
27
|
+
- Detector evidence is basename + observation metadata only — no raw stdout/stderr
|
|
28
|
+
- Aggregate detector wall-clock budget; hanging children are process-group killed and reaped
|
|
29
|
+
- Rails `after_initialize` schedules existing background inventory refresh (does not block boot)
|
|
30
|
+
- PostgreSQL version uses `connection_pool.with_connection` and releases the checkout
|
|
31
|
+
|
|
32
|
+
### FIX02 executable trust
|
|
33
|
+
|
|
34
|
+
- After realpath containment, require root-owned (uid 0) directory and executable
|
|
35
|
+
- Reject group/world-writable directories or executables; reject app-UID-owned material
|
|
36
|
+
- Ordinary `/bin` → `/usr/bin` realpath equivalence remains supported; symlink escapes fail open as not observed
|
|
37
|
+
|
|
38
|
+
### FIX03 process lifecycle
|
|
39
|
+
|
|
40
|
+
- After `Process.spawn`, every exit path terminates/reaps the child (ensure-owned cleanup; not timeout-only)
|
|
41
|
+
- Fail-open cleanup tolerates ESRCH / ECHILD / EINTR; no double-wait escape
|
|
42
|
+
|
|
3
43
|
## 0.1.0.beta.3 (unreleased)
|
|
4
44
|
|
|
5
45
|
### Exception capture parity (AUD-ERROR-PARITY01)
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Ruby / Rails SDK for [AudiTea](https://github.com/reznis/AudiTea) event collection.
|
|
4
4
|
|
|
5
|
-
Version: **0.1.0.beta.
|
|
5
|
+
Version: **0.1.0.beta.5** (proposed, not yet published)
|
|
6
6
|
|
|
7
7
|
Captures durable technical and business evidence. AudiTea **server** owns taxonomy, dictionary, human labels, vulnerability matching, correlation, and reinterpretation.
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ The SDK does **not** perform vulnerability scanning, OS package/CVE inventory, d
|
|
|
12
12
|
|
|
13
13
|
```ruby
|
|
14
14
|
# Gemfile
|
|
15
|
-
gem "auditea", "0.1.0.beta.
|
|
15
|
+
gem "auditea", "0.1.0.beta.5"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
```bash
|
|
@@ -48,6 +48,19 @@ Without a Source token (and with the SDK enabled), capture is skipped (`not_conf
|
|
|
48
48
|
| `capture_host_inventory` | `true` only if multi-host/container identity matters | Default OFF; enable per environment decision |
|
|
49
49
|
| `capture_requests` | `false` initially | Avoid high-volume request spam on first connect |
|
|
50
50
|
|
|
51
|
+
Example environment (replace the secret and release id; never commit real tokens):
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
AUDITEA_ENDPOINT=https://auditea.ca
|
|
55
|
+
AUDITEA_SOURCE_TOKEN=<secret>
|
|
56
|
+
AUDITEA_RELEASE=<actual deployed SHA>
|
|
57
|
+
AUDITEA_CAPTURE_EXCEPTIONS=true
|
|
58
|
+
AUDITEA_CAPTURE_RUNTIME_INVENTORY=true
|
|
59
|
+
AUDITEA_CAPTURE_HOST_INVENTORY=true
|
|
60
|
+
AUDITEA_RAISE_ERRORS=false
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
|
|
51
64
|
### Optional
|
|
52
65
|
|
|
53
66
|
Async tuning (`AUDITEA_ASYNC`, batch/flush/queue/timeouts), `AUDITEA_INVENTORY_REFRESH_INTERVAL`, `AUDITEA_HOST_KEY` / `AUDITEA_HOST_LABEL`, `capture_request_params`, `raise_errors` (debug only).
|
|
@@ -155,7 +168,7 @@ AUDITEA_CAPTURE_RUNTIME_INVENTORY=true
|
|
|
155
168
|
# optional: AUDITEA_INVENTORY_REFRESH_INTERVAL=86400
|
|
156
169
|
```
|
|
157
170
|
|
|
158
|
-
With Rails, when capture is enabled, the Railtie
|
|
171
|
+
With Rails, when capture is enabled, the Railtie **schedules** a single-flight background inventory refresh after `after_initialize` (boot is not blocked on detectors or database work). Opportunistic Rack refresh uses the same scheduler when due — collection/delivery are never inline on the request path.
|
|
159
172
|
|
|
160
173
|
Enabling inventory reveals internal gem names to AudiTea. Keep it off unless that is intentional.
|
|
161
174
|
|
data/RELEASE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Release: auditea 0.1.0.beta.
|
|
1
|
+
# Release: auditea 0.1.0.beta.5
|
|
2
2
|
|
|
3
3
|
Authoritative runbook for controlled beta publication of the Ruby SDK.
|
|
4
4
|
|
|
@@ -29,6 +29,22 @@ If AudiTea `main` advances after strict interop and before publication in a way
|
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
+
## Ruby SDK setup catalog (AudiTea Source page)
|
|
33
|
+
|
|
34
|
+
Any auditea-ruby release that changes the recommended gem version, configuration
|
|
35
|
+
keys (ENV names), defaults, or capture capabilities **MUST** evaluate — and, when
|
|
36
|
+
required, update — AudiTea's Ruby SDK setup catalog
|
|
37
|
+
(`config/auditea_sdks/ruby.yml` in `reznis/AudiTea`) before that release is
|
|
38
|
+
considered fully rollout-ready.
|
|
39
|
+
|
|
40
|
+
The catalog is the single source of truth for the setup instructions rendered on
|
|
41
|
+
the AudiTea Source page. A release whose advertised settings diverge from the
|
|
42
|
+
catalog produces stale or misleading customer onboarding instructions. Updating
|
|
43
|
+
the recommended supported version remains an intentional release action — never a
|
|
44
|
+
runtime RubyGems query.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
32
48
|
## Two interoperability gates (do not conflate)
|
|
33
49
|
|
|
34
50
|
### Gate A — pinned AudiTea envelope compatibility
|
|
@@ -153,10 +169,10 @@ PR status may say `READY FOR HUMAN PUBLICATION REVIEW` while still listing exter
|
|
|
153
169
|
```bash
|
|
154
170
|
git checkout main
|
|
155
171
|
git pull
|
|
156
|
-
# confirm VERSION and CHANGELOG still say 0.1.0.beta.
|
|
172
|
+
# confirm VERSION and CHANGELOG still say 0.1.0.beta.5
|
|
157
173
|
gem build auditea.gemspec
|
|
158
174
|
# inspect package once more
|
|
159
|
-
gem push auditea-0.1.0.beta.
|
|
175
|
+
gem push auditea-0.1.0.beta.5.gem
|
|
160
176
|
```
|
|
161
177
|
|
|
162
178
|
Then mark CHANGELOG released with the publication date in a follow-up commit.
|
|
@@ -166,9 +182,9 @@ Then mark CHANGELOG released with the publication date in a follow-up commit.
|
|
|
166
182
|
## Post-publication verification
|
|
167
183
|
|
|
168
184
|
```bash
|
|
169
|
-
gem install auditea -v 0.1.0.beta.
|
|
185
|
+
gem install auditea -v 0.1.0.beta.5 --pre
|
|
170
186
|
ruby -rauditea -e 'puts Auditea::VERSION'
|
|
171
|
-
# expect: 0.1.0.beta.
|
|
187
|
+
# expect: 0.1.0.beta.5
|
|
172
188
|
```
|
|
173
189
|
|
|
174
190
|
Prefer installing the RubyGems artifact (not a git branch) into the controlled TEST app.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Gate logic for examples/windows_inventory_smoke.rb — extracted so the exit
|
|
4
|
+
# behavior can be unit-tested without running a real Windows host.
|
|
5
|
+
module WindowsSmokeGate
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
Result = Data.define(:label, :ok)
|
|
9
|
+
|
|
10
|
+
# platform: the inventory "platform" hash (os_name / os_version).
|
|
11
|
+
# components: the inventory "components" array.
|
|
12
|
+
# require_edition: when set, the os_name must equal this exact edition.
|
|
13
|
+
# require_products: product names that MUST be present in components.
|
|
14
|
+
def checks(platform:, components:, require_edition: nil, require_products: [])
|
|
15
|
+
platform = {} unless platform.is_a?(Hash)
|
|
16
|
+
components = Array(components)
|
|
17
|
+
|
|
18
|
+
results = []
|
|
19
|
+
results << Result.new(label: "windows edition detected", ok: edition_ok?(platform["os_name"].to_s, require_edition))
|
|
20
|
+
results << Result.new(label: "os build present", ok: !platform["os_version"].to_s.strip.empty?)
|
|
21
|
+
|
|
22
|
+
Array(require_products).each do |product|
|
|
23
|
+
results << Result.new(label: "product detected: #{product}", ok: components.any? { |c|
|
|
24
|
+
c["product"].to_s == product.to_s
|
|
25
|
+
})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
results
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def edition_ok?(edition, require_edition)
|
|
32
|
+
return false if edition.empty?
|
|
33
|
+
return false if edition == "Windows_NT" # kernel-only: edition unknown
|
|
34
|
+
|
|
35
|
+
return edition == require_edition if require_edition && !require_edition.to_s.empty?
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def failed?(results)
|
|
41
|
+
Array(results).any? { |result| !result.ok }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Windows host-inventory smoke — run ON the Windows host (e.g. IRCM). No server needed.
|
|
4
|
+
#
|
|
5
|
+
# Validates the full detection path end-to-end:
|
|
6
|
+
# Gem.win_platform? -> windows? -> reg.exe -> parse -> components + OS edition.
|
|
7
|
+
#
|
|
8
|
+
# NEVER transmits anything to AudiTea: it calls Collector directly and prints
|
|
9
|
+
# only. Do not require AUDITEA_CAPTURE_HOST_INVENTORY=1 to run it.
|
|
10
|
+
#
|
|
11
|
+
# Usage (from an auditea-ruby checkout):
|
|
12
|
+
# bundle exec ruby examples/windows_inventory_smoke.rb
|
|
13
|
+
#
|
|
14
|
+
# Gate (exit 1 when a required expectation fails):
|
|
15
|
+
# REQUIRE_EDITION="Windows Server 2016 Standard" \
|
|
16
|
+
# REQUIRE_PRODUCTS="Apache HTTP Server,OpenSSL" \
|
|
17
|
+
# bundle exec ruby examples/windows_inventory_smoke.rb
|
|
18
|
+
#
|
|
19
|
+
# Expected on IRCM:
|
|
20
|
+
# platform.os_name == "Windows Server 2016 Standard" (NOT "Windows_NT")
|
|
21
|
+
# platform.os_version == "10.0.14393" (NOT "unknown")
|
|
22
|
+
# a component { "product" => "Apache HTTP Server", "scope" => "reverse_proxy", ... }
|
|
23
|
+
# a component { "product" => "OpenSSL", "scope" => "host_component", ... }
|
|
24
|
+
|
|
25
|
+
require "auditea"
|
|
26
|
+
require "json"
|
|
27
|
+
require_relative "support/windows_smoke_gate"
|
|
28
|
+
|
|
29
|
+
inventory = Auditea::HostInventory::Collector.collect
|
|
30
|
+
|
|
31
|
+
platform = inventory["platform"] || {}
|
|
32
|
+
components = inventory["components"] || []
|
|
33
|
+
|
|
34
|
+
puts "== platform =="
|
|
35
|
+
puts JSON.pretty_generate(platform)
|
|
36
|
+
|
|
37
|
+
puts "== sdk =="
|
|
38
|
+
puts JSON.pretty_generate(inventory["sdk"])
|
|
39
|
+
|
|
40
|
+
puts "== components (#{components.size}) =="
|
|
41
|
+
components.each { |component| puts JSON.pretty_generate(component) }
|
|
42
|
+
puts "(no components)" if components.empty?
|
|
43
|
+
|
|
44
|
+
require_edition = ENV["REQUIRE_EDITION"].to_s.strip
|
|
45
|
+
require_products = ENV["REQUIRE_PRODUCTS"].to_s.split(",").map(&:strip).reject(&:empty?)
|
|
46
|
+
|
|
47
|
+
results = WindowsSmokeGate.checks(
|
|
48
|
+
platform: platform,
|
|
49
|
+
components: components,
|
|
50
|
+
require_edition: require_edition.empty? ? nil : require_edition,
|
|
51
|
+
require_products: require_products
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
puts
|
|
55
|
+
puts "== checks =="
|
|
56
|
+
results.each do |result|
|
|
57
|
+
puts format("%-40<label>s %<verdict>s", label: result.label, verdict: (result.ok ? "PASS" : "FAIL"))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if WindowsSmokeGate.failed?(results)
|
|
61
|
+
warn "smoke gate FAILED — see checks above."
|
|
62
|
+
exit 1
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
puts "smoke gate passed."
|
data/lib/auditea/client.rb
CHANGED
|
@@ -25,6 +25,16 @@ module Auditea
|
|
|
25
25
|
queue.shutdown
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Queue depth/drop/inflight counters (see Transport::Queue#stats).
|
|
29
|
+
def stats
|
|
30
|
+
queue.stats
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Recent transport health: last delivery outcome + error (see Transport::Queue#health).
|
|
34
|
+
def health
|
|
35
|
+
queue.health
|
|
36
|
+
end
|
|
37
|
+
|
|
28
38
|
def reset!
|
|
29
39
|
shutdown
|
|
30
40
|
rebuild!
|
|
@@ -20,7 +20,7 @@ module Auditea
|
|
|
20
20
|
:open_timeout, :write_timeout, :max_retries, :logger, :release, :sdk_name,
|
|
21
21
|
:capture_requests, :capture_exceptions, :capture_request_params,
|
|
22
22
|
:capture_active_job, :capture_runtime_inventory, :capture_host_inventory,
|
|
23
|
-
:inventory_refresh_interval, :host_key, :host_label,
|
|
23
|
+
:emit_boot_diagnostic, :inventory_refresh_interval, :host_key, :host_label,
|
|
24
24
|
:message_max_bytes, :nesting_max_depth
|
|
25
25
|
|
|
26
26
|
def initialize
|
|
@@ -47,6 +47,7 @@ module Auditea
|
|
|
47
47
|
@capture_runtime_inventory = truthy?(ENV.fetch("AUDITEA_CAPTURE_RUNTIME_INVENTORY", "false"))
|
|
48
48
|
# Opt-in: host/platform inventory (opaque node id + OS facts).
|
|
49
49
|
@capture_host_inventory = truthy?(ENV.fetch("AUDITEA_CAPTURE_HOST_INVENTORY", "false"))
|
|
50
|
+
@emit_boot_diagnostic = truthy?(ENV.fetch("AUDITEA_EMIT_BOOT_DIAGNOSTIC", "true"))
|
|
50
51
|
@host_key = ENV.fetch("AUDITEA_HOST_KEY", nil)
|
|
51
52
|
@host_label = ENV.fetch("AUDITEA_HOST_LABEL", nil)
|
|
52
53
|
@inventory_refresh_interval = float_env(
|
|
@@ -98,6 +99,12 @@ module Auditea
|
|
|
98
99
|
host = normalize_host(uri.host)
|
|
99
100
|
return false if host.empty?
|
|
100
101
|
|
|
102
|
+
# A base endpoint has no legitimate userinfo (credentials), query, or
|
|
103
|
+
# fragment. Reject these dangerous URL forms rather than silently
|
|
104
|
+
# supporting them — they can embed secrets in a place the generic key
|
|
105
|
+
# sanitizer cannot reach.
|
|
106
|
+
return false if uri.userinfo || uri.query || uri.fragment
|
|
107
|
+
|
|
101
108
|
case uri
|
|
102
109
|
when URI::HTTPS
|
|
103
110
|
true
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
# Builds a machine-readable readiness snapshot for the SDK: what is configured,
|
|
5
|
+
# what is missing, and recent transport health.
|
|
6
|
+
#
|
|
7
|
+
# Never includes the source token value — only its presence. Intended both for
|
|
8
|
+
# direct inspection (`Auditea.diagnostic`) and for transport as event metadata
|
|
9
|
+
# (`Auditea.emit_diagnostic!` → `diagnostic.sdk_ready`).
|
|
10
|
+
class Diagnostic
|
|
11
|
+
def self.call
|
|
12
|
+
new.call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
cfg = Auditea.configuration
|
|
17
|
+
{
|
|
18
|
+
gem_version: VERSION,
|
|
19
|
+
configured: cfg.configured?,
|
|
20
|
+
endpoint: safe_endpoint(cfg.endpoint),
|
|
21
|
+
token_present: token_present?(cfg),
|
|
22
|
+
release: presence(cfg.release),
|
|
23
|
+
enabled: cfg.enabled?,
|
|
24
|
+
async: cfg.async?,
|
|
25
|
+
raise_errors: cfg.raise_errors,
|
|
26
|
+
capture_exceptions: cfg.capture_exceptions,
|
|
27
|
+
capture_runtime_inventory: cfg.capture_runtime_inventory?,
|
|
28
|
+
capture_host_inventory: cfg.capture_host_inventory?,
|
|
29
|
+
capture_requests: cfg.capture_requests,
|
|
30
|
+
capture_request_params: cfg.capture_request_params,
|
|
31
|
+
capture_active_job: cfg.capture_active_job,
|
|
32
|
+
valid: valid?(cfg),
|
|
33
|
+
transport: Auditea.client.health,
|
|
34
|
+
queue: Auditea.client.stats,
|
|
35
|
+
warnings: warnings(cfg)
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def token_present?(cfg)
|
|
42
|
+
!cfg.source_token.to_s.strip.empty?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Endpoint representation safe-by-construction: strips userinfo (credentials),
|
|
46
|
+
# query, and fragment so secrets embedded in the URL never enter the snapshot.
|
|
47
|
+
# Returns nil for an unparseable endpoint (nothing sensitive is exposed).
|
|
48
|
+
def safe_endpoint(value)
|
|
49
|
+
uri = URI.parse(value.to_s)
|
|
50
|
+
return nil if uri.host.to_s.empty?
|
|
51
|
+
|
|
52
|
+
uri.user = nil
|
|
53
|
+
uri.password = nil
|
|
54
|
+
uri.query = nil
|
|
55
|
+
uri.fragment = nil
|
|
56
|
+
uri.to_s
|
|
57
|
+
rescue URI::InvalidURIError, ArgumentError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def valid?(cfg)
|
|
62
|
+
cfg.validate!
|
|
63
|
+
true
|
|
64
|
+
rescue ConfigurationError
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def warnings(cfg)
|
|
69
|
+
[].tap do |warnings|
|
|
70
|
+
warnings << "source_token unset" unless token_present?(cfg)
|
|
71
|
+
warnings << "release unset — events will lack a release" if presence(cfg.release).nil?
|
|
72
|
+
if cfg.endpoint.to_s == Configuration::DEFAULT_ENDPOINT
|
|
73
|
+
warnings << "endpoint is the SDK default (#{Configuration::DEFAULT_ENDPOINT})"
|
|
74
|
+
end
|
|
75
|
+
warnings << "runtime inventory disabled" unless cfg.capture_runtime_inventory?
|
|
76
|
+
warnings << "host inventory disabled" unless cfg.capture_host_inventory?
|
|
77
|
+
warnings << "raise_errors enabled — SDK errors may raise in the application" if cfg.raise_errors
|
|
78
|
+
warnings << "SDK disabled" unless cfg.enabled?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def presence(value)
|
|
83
|
+
value.to_s.strip.empty? ? nil : value.to_s.strip
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -6,6 +6,8 @@ require "rbconfig"
|
|
|
6
6
|
require "socket"
|
|
7
7
|
require_relative "../inventory/string_bound"
|
|
8
8
|
require_relative "../inventory/digest"
|
|
9
|
+
require_relative "../inventory/components"
|
|
10
|
+
require_relative "component_detectors"
|
|
9
11
|
|
|
10
12
|
module Auditea
|
|
11
13
|
module HostInventory
|
|
@@ -17,7 +19,7 @@ module Auditea
|
|
|
17
19
|
MAX_FIELD_BYTES = 128
|
|
18
20
|
MAX_HOST_KEY = 128
|
|
19
21
|
HOST_KEY_FORMAT = /\A[A-Za-z0-9._:-]{8,128}\z/
|
|
20
|
-
MAX_INVENTORY_BYTES =
|
|
22
|
+
MAX_INVENTORY_BYTES = 16_384
|
|
21
23
|
OS_RELEASE_PATH = "/etc/os-release"
|
|
22
24
|
|
|
23
25
|
module_function
|
|
@@ -38,11 +40,62 @@ module Auditea
|
|
|
38
40
|
label = configured_label
|
|
39
41
|
inventory["label"] = label if label
|
|
40
42
|
|
|
43
|
+
components = Inventory::Components.compact_list(
|
|
44
|
+
host_os_components(inventory["platform"]) + ComponentDetectors.collect
|
|
45
|
+
)
|
|
46
|
+
inventory["components"] = components unless components.empty?
|
|
47
|
+
|
|
41
48
|
inventory = enforce_size_bound(inventory)
|
|
42
49
|
inventory["digest"] = Inventory::DigestComputer.compute(inventory)
|
|
43
50
|
inventory
|
|
44
51
|
end
|
|
45
52
|
|
|
53
|
+
def host_os_components(platform)
|
|
54
|
+
list = []
|
|
55
|
+
os_name = platform["os_name"]
|
|
56
|
+
os_version = platform["os_version"]
|
|
57
|
+
if present?(os_name) || present?(os_version)
|
|
58
|
+
product =
|
|
59
|
+
if os_name.to_s.match?(/ubuntu/i)
|
|
60
|
+
"Ubuntu"
|
|
61
|
+
elsif os_name.to_s.match?(/debian/i)
|
|
62
|
+
"Debian"
|
|
63
|
+
elsif present?(os_name)
|
|
64
|
+
os_name
|
|
65
|
+
else
|
|
66
|
+
"Host OS"
|
|
67
|
+
end
|
|
68
|
+
version = present?(os_version) ? os_version : "unknown"
|
|
69
|
+
list << Inventory::Components.build(
|
|
70
|
+
product: product,
|
|
71
|
+
version: version,
|
|
72
|
+
scope: "host_os",
|
|
73
|
+
observation_method: host_os_provenance(platform)
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
kernel_name = platform["kernel_name"]
|
|
78
|
+
kernel_release = platform["kernel_release"]
|
|
79
|
+
if present?(kernel_release)
|
|
80
|
+
list << Inventory::Components.build(
|
|
81
|
+
product: present?(kernel_name) ? "#{kernel_name} kernel" : "Linux kernel",
|
|
82
|
+
version: kernel_release,
|
|
83
|
+
scope: "host_os",
|
|
84
|
+
observation_method: "uname"
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
list
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The OS-edition row's provenance differs by platform: /etc/os-release on
|
|
92
|
+
# Linux, the Windows registry ProductName on Windows. Never claim os_release
|
|
93
|
+
# for a Windows host.
|
|
94
|
+
def host_os_provenance(platform)
|
|
95
|
+
platform["os_family"].to_s == "windows" ? "windows.registry" : "os_release"
|
|
96
|
+
end
|
|
97
|
+
private_class_method :host_os_provenance
|
|
98
|
+
|
|
46
99
|
def resolve_host_key!
|
|
47
100
|
configured = Auditea.configuration.host_key.to_s.strip
|
|
48
101
|
configured = ENV.fetch("AUDITEA_HOST_KEY", "").to_s.strip if configured.empty?
|
|
@@ -127,6 +180,14 @@ module Auditea
|
|
|
127
180
|
}
|
|
128
181
|
end
|
|
129
182
|
|
|
183
|
+
if os_family == "windows"
|
|
184
|
+
return {
|
|
185
|
+
name: first_present(WindowsComponents.product_name, uname[:sysname], "Windows"),
|
|
186
|
+
# Windows kernel release doubles as the OS build (e.g. "10.0.14393").
|
|
187
|
+
version: first_present(uname[:release])
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
130
191
|
{
|
|
131
192
|
name: first_present(uname[:sysname], RbConfig::CONFIG["host_os"]),
|
|
132
193
|
# Never substitute kernel build metadata (uname.version) as product OS version.
|
|
@@ -224,6 +285,7 @@ module Auditea
|
|
|
224
285
|
# Drop optional fields first; keep identity + required platform.
|
|
225
286
|
trimmed = inventory.dup
|
|
226
287
|
trimmed.delete("label")
|
|
288
|
+
trimmed.delete("components")
|
|
227
289
|
trimmed["hardware"] = {}
|
|
228
290
|
trimmed["runtime_context"] = {}
|
|
229
291
|
trimmed
|