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
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "string_bound"
|
|
6
|
+
require_relative "components"
|
|
7
|
+
require_relative "database_server"
|
|
6
8
|
|
|
7
9
|
module Auditea
|
|
8
10
|
module Inventory
|
|
9
11
|
# Collects a normalized runtime + dependency inventory for AudiTea.
|
|
10
12
|
# Never includes filesystem paths, remote URLs, tokens, or env vars.
|
|
13
|
+
# rubocop:disable-next Metrics/ModuleLength -- cohesive collector + component promotion
|
|
11
14
|
module Collector
|
|
12
15
|
SCHEMA_VERSION = 1
|
|
13
16
|
MAX_PACKAGES = 400
|
|
@@ -15,6 +18,16 @@ module Auditea
|
|
|
15
18
|
MAX_REQUIREMENT_BYTES = 256
|
|
16
19
|
# Leave headroom under Transport::Limits::SINGLE_MAX_BYTES after envelope/provenance.
|
|
17
20
|
MAX_INVENTORY_BYTES = 110_000
|
|
21
|
+
PROMOTED_SERVER_GEMS = {
|
|
22
|
+
"puma" => { "product" => "Puma", "scope" => "application_server" },
|
|
23
|
+
"unicorn" => { "product" => "Unicorn", "scope" => "application_server" },
|
|
24
|
+
"passenger" => { "product" => "Phusion Passenger", "scope" => "application_server" }
|
|
25
|
+
}.freeze
|
|
26
|
+
PROMOTED_ADAPTER_GEMS = {
|
|
27
|
+
"pg" => { "product" => "pg (PostgreSQL adapter)", "scope" => "linked_library" },
|
|
28
|
+
"trilogy" => { "product" => "Trilogy", "scope" => "linked_library" },
|
|
29
|
+
"mysql2" => { "product" => "mysql2", "scope" => "linked_library" }
|
|
30
|
+
}.freeze
|
|
18
31
|
|
|
19
32
|
module_function
|
|
20
33
|
|
|
@@ -41,11 +54,124 @@ module Auditea
|
|
|
41
54
|
release = Auditea.configuration.release
|
|
42
55
|
inventory["application"] = { "release" => bound_string(release) } if present?(release)
|
|
43
56
|
|
|
57
|
+
components = Components.compact_list(
|
|
58
|
+
runtime_components(inventory) +
|
|
59
|
+
promoted_gem_components(packages) +
|
|
60
|
+
[DatabaseServer.collect]
|
|
61
|
+
)
|
|
62
|
+
inventory["components"] = components unless components.empty?
|
|
63
|
+
|
|
44
64
|
inventory = enforce_size_bound(inventory)
|
|
45
65
|
inventory["digest"] = DigestComputer.compute(inventory)
|
|
46
66
|
inventory
|
|
47
67
|
end
|
|
48
68
|
|
|
69
|
+
def runtime_components(inventory)
|
|
70
|
+
list = []
|
|
71
|
+
runtime = inventory["runtime"] || {}
|
|
72
|
+
list << Components.build(
|
|
73
|
+
product: "Ruby",
|
|
74
|
+
version: runtime["version"],
|
|
75
|
+
scope: "application_runtime",
|
|
76
|
+
observation_method: "ruby_constants"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
framework = inventory["framework"]
|
|
80
|
+
if framework
|
|
81
|
+
list << Components.build(
|
|
82
|
+
product: "Rails",
|
|
83
|
+
version: framework["version"],
|
|
84
|
+
scope: "application_runtime",
|
|
85
|
+
observation_method: "rails_version"
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
pm = inventory["package_manager"] || {}
|
|
90
|
+
if present?(pm["rubygems_version"])
|
|
91
|
+
list << Components.build(
|
|
92
|
+
product: "RubyGems",
|
|
93
|
+
version: pm["rubygems_version"],
|
|
94
|
+
scope: "application_runtime",
|
|
95
|
+
observation_method: "rubygems_version"
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
if present?(pm["bundler_version"])
|
|
99
|
+
list << Components.build(
|
|
100
|
+
product: "Bundler",
|
|
101
|
+
version: pm["bundler_version"],
|
|
102
|
+
scope: "application_runtime",
|
|
103
|
+
observation_method: "bundler_version"
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
openssl = inventory["openssl"] || {}
|
|
108
|
+
openssl_version = first_present(
|
|
109
|
+
openssl["library_version"],
|
|
110
|
+
openssl["build_version"],
|
|
111
|
+
openssl["extension_version"]
|
|
112
|
+
)
|
|
113
|
+
if present?(openssl_version)
|
|
114
|
+
list << Components.build(
|
|
115
|
+
product: "OpenSSL",
|
|
116
|
+
version: openssl_version,
|
|
117
|
+
scope: "linked_library",
|
|
118
|
+
observation_method: "ruby_openssl_constants",
|
|
119
|
+
evidence: {
|
|
120
|
+
"extension_version" => openssl["extension_version"],
|
|
121
|
+
"build_version" => openssl["build_version"],
|
|
122
|
+
"library_version" => openssl["library_version"]
|
|
123
|
+
}.compact
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
list
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def promoted_gem_components(packages)
|
|
131
|
+
by_name = Array(packages).to_h { |pkg| [pkg["name"].to_s, pkg] }
|
|
132
|
+
list = []
|
|
133
|
+
|
|
134
|
+
PROMOTED_SERVER_GEMS.each do |gem_name, meta|
|
|
135
|
+
pkg = by_name[gem_name]
|
|
136
|
+
next unless pkg
|
|
137
|
+
|
|
138
|
+
list << Components.build(
|
|
139
|
+
product: meta["product"],
|
|
140
|
+
version: pkg["version"],
|
|
141
|
+
scope: meta["scope"],
|
|
142
|
+
observation_method: "bundler_package",
|
|
143
|
+
package: {
|
|
144
|
+
"ecosystem" => pkg["ecosystem"],
|
|
145
|
+
"name" => pkg["name"],
|
|
146
|
+
"version" => pkg["version"]
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
PROMOTED_ADAPTER_GEMS.each do |gem_name, meta|
|
|
152
|
+
pkg = by_name[gem_name]
|
|
153
|
+
next unless pkg
|
|
154
|
+
|
|
155
|
+
list << Components.build(
|
|
156
|
+
product: meta["product"],
|
|
157
|
+
version: pkg["version"],
|
|
158
|
+
scope: meta["scope"],
|
|
159
|
+
observation_method: "bundler_package",
|
|
160
|
+
package: {
|
|
161
|
+
"ecosystem" => pkg["ecosystem"],
|
|
162
|
+
"name" => pkg["name"],
|
|
163
|
+
"version" => pkg["version"]
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
list
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def first_present(*values)
|
|
172
|
+
values.find { |value| present?(value) }
|
|
173
|
+
end
|
|
174
|
+
|
|
49
175
|
def runtime_metadata
|
|
50
176
|
{
|
|
51
177
|
"language" => "ruby",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
module Inventory
|
|
5
|
+
# Normalized platform/software component observations (inventory only — not vulnerability).
|
|
6
|
+
module Components
|
|
7
|
+
SCOPES = %w[
|
|
8
|
+
application_runtime
|
|
9
|
+
application_server
|
|
10
|
+
database_server
|
|
11
|
+
linked_library
|
|
12
|
+
host_os
|
|
13
|
+
host_component
|
|
14
|
+
reverse_proxy
|
|
15
|
+
externally_observed
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
MAX_COMPONENTS = 40
|
|
19
|
+
MAX_PRODUCT = 128
|
|
20
|
+
MAX_VERSION = 128
|
|
21
|
+
MAX_METHOD = 64
|
|
22
|
+
MAX_EVIDENCE_VALUE = 64
|
|
23
|
+
# Strict allowlist — never transmit raw command output, paths, or host details.
|
|
24
|
+
EVIDENCE_KEYS = %w[command adapter].freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def build(product:, version:, scope:, observation_method:, package: nil, evidence: nil)
|
|
29
|
+
return nil unless SCOPES.include?(scope.to_s)
|
|
30
|
+
return nil if blank?(product) || blank?(version)
|
|
31
|
+
|
|
32
|
+
entry = {
|
|
33
|
+
"product" => truncate(product, MAX_PRODUCT),
|
|
34
|
+
"version" => truncate(version, MAX_VERSION),
|
|
35
|
+
"scope" => scope.to_s,
|
|
36
|
+
"observation_method" => truncate(observation_method, MAX_METHOD)
|
|
37
|
+
}
|
|
38
|
+
entry["package"] = package if package.is_a?(Hash) && !package.empty?
|
|
39
|
+
sanitized = sanitize_evidence(evidence)
|
|
40
|
+
entry["evidence"] = sanitized if sanitized
|
|
41
|
+
entry
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sanitize_evidence(value)
|
|
45
|
+
return nil unless value.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
out = {}
|
|
48
|
+
value.each do |key, child|
|
|
49
|
+
key_s = key.to_s
|
|
50
|
+
next unless EVIDENCE_KEYS.include?(key_s)
|
|
51
|
+
next if child.nil?
|
|
52
|
+
|
|
53
|
+
case child
|
|
54
|
+
when String
|
|
55
|
+
next if child.empty?
|
|
56
|
+
|
|
57
|
+
out[key_s] = truncate(child, MAX_EVIDENCE_VALUE)
|
|
58
|
+
when Numeric, TrueClass, FalseClass
|
|
59
|
+
out[key_s] = child
|
|
60
|
+
else
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
out.empty? ? nil : out
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def compact_list(list)
|
|
68
|
+
Array(list).compact.first(MAX_COMPONENTS)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def truncate(value, max)
|
|
72
|
+
StringBound.truncate(value.to_s, max)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def blank?(value)
|
|
76
|
+
value.nil? || value.to_s.strip.empty?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Auditea
|
|
4
|
+
module Inventory
|
|
5
|
+
# Fail-open database server version collectors.
|
|
6
|
+
# Never transmits credentials, connection strings, or DB hostnames.
|
|
7
|
+
module DatabaseServer
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def collect
|
|
11
|
+
return nil unless defined?(::ActiveRecord::Base)
|
|
12
|
+
return nil unless ::ActiveRecord::Base.respond_to?(:connection_pool)
|
|
13
|
+
|
|
14
|
+
pool = ::ActiveRecord::Base.connection_pool
|
|
15
|
+
return nil unless pool.respond_to?(:with_connection)
|
|
16
|
+
|
|
17
|
+
pool.with_connection do |connection|
|
|
18
|
+
return nil unless connection
|
|
19
|
+
|
|
20
|
+
adapter = connection.adapter_name.to_s.downcase
|
|
21
|
+
collector = collector_for(adapter)
|
|
22
|
+
return nil unless collector
|
|
23
|
+
|
|
24
|
+
collector.call(connection)
|
|
25
|
+
end
|
|
26
|
+
rescue StandardError
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def collector_for(adapter)
|
|
31
|
+
case adapter
|
|
32
|
+
when /postgre/
|
|
33
|
+
method(:postgresql)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def postgresql(connection)
|
|
38
|
+
version = nil
|
|
39
|
+
version = connection.select_value("SHOW server_version") if connection.respond_to?(:select_value)
|
|
40
|
+
version = version.to_s.strip
|
|
41
|
+
return nil if version.empty?
|
|
42
|
+
|
|
43
|
+
Components.build(
|
|
44
|
+
product: "PostgreSQL",
|
|
45
|
+
version: version,
|
|
46
|
+
scope: "database_server",
|
|
47
|
+
observation_method: "active_record.show_server_version",
|
|
48
|
+
evidence: {
|
|
49
|
+
"adapter" => "postgresql"
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
rescue StandardError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/auditea/railtie.rb
CHANGED
|
@@ -24,12 +24,40 @@ module Auditea
|
|
|
24
24
|
config.after_initialize do
|
|
25
25
|
at_exit { Auditea.shutdown }
|
|
26
26
|
begin
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
Auditea.
|
|
27
|
+
# Schedule via existing single-flight background refresh — never block boot on
|
|
28
|
+
# detector processes or database inventory work.
|
|
29
|
+
Auditea::Inventory.schedule_refresh! if Auditea.configuration.capture_runtime_inventory?
|
|
30
|
+
Auditea::HostInventory.schedule_refresh! if Auditea.configuration.capture_host_inventory?
|
|
30
31
|
rescue StandardError => error
|
|
31
32
|
Auditea.handle_error(error)
|
|
32
33
|
end
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
# One automatic boot/startup diagnostic observation when configured.
|
|
37
|
+
# Never crash boot: fully fail-open (swallow, log, continue) regardless of
|
|
38
|
+
# raise_errors.
|
|
39
|
+
Auditea::Railtie.emit_boot_diagnostic!
|
|
40
|
+
rescue StandardError => error
|
|
41
|
+
Rails.logger&.warn("[auditea] boot diagnostic skipped: #{error.class}: #{error.message.to_s.byteslice(0, 200)}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Emits a single automatic boot/startup diagnostic observation when the SDK is
|
|
46
|
+
# configured and the flag is enabled.
|
|
47
|
+
#
|
|
48
|
+
# Emitted only in asynchronous mode. In synchronous mode the automatic
|
|
49
|
+
# observation is skipped entirely: enqueue would perform inline HTTP (including
|
|
50
|
+
# retries/timeouts), adding an external-network dependency to Rails startup.
|
|
51
|
+
# This does NOT weaken explicit user-requested synchronous delivery
|
|
52
|
+
# (Auditea.emit_diagnostic! / capture with async:false still deliver inline);
|
|
53
|
+
# it only gates the automatic boot path. The local snapshot
|
|
54
|
+
# (Auditea.diagnostic) remains available regardless.
|
|
55
|
+
def self.emit_boot_diagnostic!
|
|
56
|
+
cfg = Auditea.configuration
|
|
57
|
+
return unless cfg.configured? && cfg.emit_boot_diagnostic
|
|
58
|
+
return unless cfg.async?
|
|
59
|
+
|
|
60
|
+
Auditea.emit_diagnostic!(flush: false)
|
|
33
61
|
end
|
|
34
62
|
end
|
|
35
63
|
end
|
data/lib/auditea/sanitizer.rb
CHANGED
|
@@ -16,8 +16,37 @@ module Auditea
|
|
|
16
16
|
otp otp_secret session_id credit_card card_number cvv ssn
|
|
17
17
|
].freeze
|
|
18
18
|
|
|
19
|
+
# URI credential forms are stripped from diagnostic value strings (endpoint,
|
|
20
|
+
# transport errors, warnings). This is a bounded field-specific redaction for
|
|
21
|
+
# the diagnostic contract, NOT a general secret scanner.
|
|
22
|
+
URI_USERINFO_RE = %r{(https?://)[^/\s@]+@}i
|
|
23
|
+
SENSITIVE_QUERY_RE = /
|
|
24
|
+
([?&]
|
|
25
|
+
(?:token|password|passwd|secret|api_?key|access_token|refresh_token|
|
|
26
|
+
auth|authorization|key|signature|sig|credential)
|
|
27
|
+
=
|
|
28
|
+
)
|
|
29
|
+
[^&\s]*
|
|
30
|
+
/ix
|
|
31
|
+
|
|
19
32
|
module_function
|
|
20
33
|
|
|
34
|
+
# Scrub URI userinfo (`scheme://user:pass@host`) and sensitive query values
|
|
35
|
+
# from a free-form diagnostic string, then bound its length.
|
|
36
|
+
def sanitize_diagnostic_string(value, max_bytes:)
|
|
37
|
+
return nil if value.nil?
|
|
38
|
+
|
|
39
|
+
scrub_uri_credentials(value.to_s).byteslice(0, max_bytes)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Remove `user:password@` from any embedded http(s) URL and redact the value
|
|
43
|
+
# of sensitive query parameters. Field-specific: only URI credential shapes.
|
|
44
|
+
def scrub_uri_credentials(text)
|
|
45
|
+
text.to_s
|
|
46
|
+
.gsub(URI_USERINFO_RE) { Regexp.last_match(1) }
|
|
47
|
+
.gsub(SENSITIVE_QUERY_RE) { "#{Regexp.last_match(1)}[REDACTED]" }
|
|
48
|
+
end
|
|
49
|
+
|
|
21
50
|
def sanitize_event(event)
|
|
22
51
|
redacted = []
|
|
23
52
|
sanitized = deep_sanitize(event, depth: 0, path: nil, redacted: redacted)
|
|
@@ -50,8 +79,18 @@ module Auditea
|
|
|
50
79
|
path.to_s == "evidence.inventory.packages"
|
|
51
80
|
end
|
|
52
81
|
|
|
82
|
+
def inventory_components_path?(path)
|
|
83
|
+
path.to_s == "evidence.inventory.components"
|
|
84
|
+
end
|
|
85
|
+
|
|
53
86
|
def array_limit_for(path)
|
|
54
|
-
inventory_packages_path?(path)
|
|
87
|
+
if inventory_packages_path?(path)
|
|
88
|
+
INVENTORY_PACKAGES_ARRAY_MAX
|
|
89
|
+
elsif inventory_components_path?(path)
|
|
90
|
+
40
|
|
91
|
+
else
|
|
92
|
+
DEFAULT_ARRAY_MAX
|
|
93
|
+
end
|
|
55
94
|
end
|
|
56
95
|
|
|
57
96
|
def deep_sanitize(value, depth:, path:, redacted:)
|
|
@@ -18,7 +18,10 @@ module Auditea
|
|
|
18
18
|
@buffer = []
|
|
19
19
|
@stopped = false
|
|
20
20
|
@dropped = 0
|
|
21
|
+
@delivered = 0
|
|
21
22
|
@inflight = 0
|
|
23
|
+
@last_outcome = nil
|
|
24
|
+
@last_error = nil
|
|
22
25
|
@thread = nil
|
|
23
26
|
@owner_pid = nil
|
|
24
27
|
end
|
|
@@ -95,7 +98,22 @@ module Auditea
|
|
|
95
98
|
|
|
96
99
|
def stats
|
|
97
100
|
ensure_process!
|
|
98
|
-
@mutex.synchronize
|
|
101
|
+
@mutex.synchronize do
|
|
102
|
+
{
|
|
103
|
+
size: @buffer.size,
|
|
104
|
+
dropped: @dropped,
|
|
105
|
+
delivered: @delivered,
|
|
106
|
+
inflight: @inflight,
|
|
107
|
+
pid: @owner_pid
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Recent transport health for diagnostics: what happened to the last delivery
|
|
113
|
+
# attempt (accepted / unauthorized / exhausted / error / …) and, on failure, why.
|
|
114
|
+
def health
|
|
115
|
+
ensure_process!
|
|
116
|
+
@mutex.synchronize { { last_outcome: @last_outcome, last_error: @last_error } }
|
|
99
117
|
end
|
|
100
118
|
|
|
101
119
|
private
|
|
@@ -106,10 +124,15 @@ module Auditea
|
|
|
106
124
|
if @owner_pid.nil?
|
|
107
125
|
@owner_pid = pid
|
|
108
126
|
elsif @owner_pid != pid
|
|
109
|
-
# Child after fork: drop inherited buffer/worker; do not deliver parent
|
|
127
|
+
# Child after fork: drop inherited buffer/worker; do not deliver parent
|
|
128
|
+
# data. Also reset the diagnostic transport counters/state so the child
|
|
129
|
+
# describes its own process, not the one it forked from.
|
|
110
130
|
@buffer = []
|
|
111
131
|
@inflight = 0
|
|
112
132
|
@dropped = 0
|
|
133
|
+
@delivered = 0
|
|
134
|
+
@last_outcome = nil
|
|
135
|
+
@last_error = nil
|
|
113
136
|
@stopped = false
|
|
114
137
|
@thread = nil
|
|
115
138
|
@owner_pid = pid
|
|
@@ -183,9 +206,11 @@ module Auditea
|
|
|
183
206
|
else
|
|
184
207
|
@http.post_batch(events)
|
|
185
208
|
end
|
|
209
|
+
record_delivery(result)
|
|
186
210
|
handle_result(events, result)
|
|
187
211
|
result
|
|
188
212
|
rescue StandardError => error
|
|
213
|
+
record_delivery_error(error)
|
|
189
214
|
@configuration.logger&.warn("[auditea] delivery failed: #{error.class}: #{error.message.to_s.byteslice(0,
|
|
190
215
|
200)}")
|
|
191
216
|
raise if @configuration.raise_errors
|
|
@@ -220,6 +245,57 @@ module Auditea
|
|
|
220
245
|
nil
|
|
221
246
|
end
|
|
222
247
|
|
|
248
|
+
def record_delivery(result)
|
|
249
|
+
delivered, outcome, error = aggregate_delivery(result)
|
|
250
|
+
@mutex.synchronize do
|
|
251
|
+
@last_outcome = outcome
|
|
252
|
+
@last_error = error
|
|
253
|
+
@delivered += delivered
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def record_delivery_error(error)
|
|
258
|
+
@mutex.synchronize do
|
|
259
|
+
@last_outcome = "error"
|
|
260
|
+
@last_error = Sanitizer.sanitize_diagnostic_string("#{error.class}: #{error.message}", max_bytes: 200)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Truthful aggregate outcome for a single-event or batch DeliveryResult.
|
|
265
|
+
# `delivered` counts only events the server accepted/treated as success.
|
|
266
|
+
def aggregate_delivery(result)
|
|
267
|
+
return [0, "accepted", nil] unless result.is_a?(DeliveryResult)
|
|
268
|
+
|
|
269
|
+
if result.results
|
|
270
|
+
rows = result.results
|
|
271
|
+
delivered = rows.count { |row| row["ok"] }
|
|
272
|
+
[delivered, aggregate_batch_outcome(rows), aggregate_batch_error(rows)]
|
|
273
|
+
else
|
|
274
|
+
delivered = result.ok? ? 1 : 0
|
|
275
|
+
error = result.ok? ? nil : Sanitizer.sanitize_diagnostic_string(result.message, max_bytes: 200)
|
|
276
|
+
[delivered, result.outcome.to_s, error]
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def aggregate_batch_outcome(rows)
|
|
281
|
+
return "error" if rows.empty?
|
|
282
|
+
|
|
283
|
+
ok = rows.count { |row| row["ok"] }
|
|
284
|
+
return "accepted" if ok == rows.size
|
|
285
|
+
|
|
286
|
+
bad_outcomes = rows.reject { |row| row["ok"] }.map { |row| row["outcome"] }.compact.uniq
|
|
287
|
+
return "partial_failure" if ok.positive?
|
|
288
|
+
|
|
289
|
+
# Total failure: surface the single uniform outcome, else a generic error.
|
|
290
|
+
bad_outcomes.size == 1 ? bad_outcomes.first : "error"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def aggregate_batch_error(rows)
|
|
294
|
+
return nil if rows.all? { |row| row["ok"] }
|
|
295
|
+
|
|
296
|
+
Sanitizer.sanitize_diagnostic_string("mixed_batch_results", max_bytes: 200)
|
|
297
|
+
end
|
|
298
|
+
|
|
223
299
|
def log_drop(event_id, reason)
|
|
224
300
|
@configuration.logger&.warn("[auditea] event_id=#{event_id} reason=#{reason}")
|
|
225
301
|
end
|
data/lib/auditea/version.rb
CHANGED
data/lib/auditea.rb
CHANGED
|
@@ -22,6 +22,7 @@ require_relative "auditea/event_builder"
|
|
|
22
22
|
require_relative "auditea/actor"
|
|
23
23
|
require_relative "auditea/target"
|
|
24
24
|
require_relative "auditea/current_context"
|
|
25
|
+
require_relative "auditea/diagnostic"
|
|
25
26
|
require_relative "auditea/transport/limits"
|
|
26
27
|
require_relative "auditea/transport/delivery_result"
|
|
27
28
|
require_relative "auditea/transport/http"
|
|
@@ -142,6 +143,21 @@ module Auditea
|
|
|
142
143
|
HostInventory.maybe_capture!
|
|
143
144
|
end
|
|
144
145
|
|
|
146
|
+
# Machine-readable readiness snapshot (config + transport health + warnings).
|
|
147
|
+
# Never includes the source token value.
|
|
148
|
+
def diagnostic
|
|
149
|
+
Diagnostic.call
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Emit the readiness snapshot as a `diagnostic.sdk_ready` event, then flush.
|
|
153
|
+
# Returns the capture result (fail-open: { ok: false, skipped: true, reason: … }
|
|
154
|
+
# when disabled or not configured).
|
|
155
|
+
def emit_diagnostic!(flush: true)
|
|
156
|
+
result = capture("diagnostic.sdk_ready", category: "diagnostic", severity: "info", metadata: Diagnostic.call)
|
|
157
|
+
flush(timeout: 10) if flush
|
|
158
|
+
result
|
|
159
|
+
end
|
|
160
|
+
|
|
145
161
|
def flush(timeout: 5)
|
|
146
162
|
client.flush(timeout: timeout)
|
|
147
163
|
rescue StandardError => error
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: auditea
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.beta.
|
|
4
|
+
version: 0.1.0.beta.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AudiTea
|
|
@@ -24,16 +24,21 @@ files:
|
|
|
24
24
|
- RELEASE.md
|
|
25
25
|
- auditea.gemspec
|
|
26
26
|
- examples/manual_capture.rb
|
|
27
|
+
- examples/support/windows_smoke_gate.rb
|
|
28
|
+
- examples/windows_inventory_smoke.rb
|
|
27
29
|
- lib/auditea.rb
|
|
28
30
|
- lib/auditea/actor.rb
|
|
29
31
|
- lib/auditea/client.rb
|
|
30
32
|
- lib/auditea/configuration.rb
|
|
31
33
|
- lib/auditea/current_context.rb
|
|
34
|
+
- lib/auditea/diagnostic.rb
|
|
32
35
|
- lib/auditea/event_builder.rb
|
|
33
36
|
- lib/auditea/host_inventory.rb
|
|
34
37
|
- lib/auditea/host_inventory/background_refresh.rb
|
|
35
38
|
- lib/auditea/host_inventory/collector.rb
|
|
39
|
+
- lib/auditea/host_inventory/component_detectors.rb
|
|
36
40
|
- lib/auditea/host_inventory/emitter.rb
|
|
41
|
+
- lib/auditea/host_inventory/windows_components.rb
|
|
37
42
|
- lib/auditea/instrumentation/active_job.rb
|
|
38
43
|
- lib/auditea/instrumentation/error_subscriber.rb
|
|
39
44
|
- lib/auditea/instrumentation/exception.rb
|
|
@@ -43,6 +48,8 @@ files:
|
|
|
43
48
|
- lib/auditea/inventory.rb
|
|
44
49
|
- lib/auditea/inventory/background_refresh.rb
|
|
45
50
|
- lib/auditea/inventory/collector.rb
|
|
51
|
+
- lib/auditea/inventory/components.rb
|
|
52
|
+
- lib/auditea/inventory/database_server.rb
|
|
46
53
|
- lib/auditea/inventory/digest.rb
|
|
47
54
|
- lib/auditea/inventory/emitter.rb
|
|
48
55
|
- lib/auditea/inventory/string_bound.rb
|