legion-logging 1.2.5 → 1.2.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5c68f56d267c3653df2d9a713072d2ab81445f437f64761e23ebd4bc8323e73
4
- data.tar.gz: f78f23e438594a7eab25499a154b27d9fb0d043c54773c72c2f63affb9aa76b6
3
+ metadata.gz: d4a463a1d341069c5c5ed0f15e98dfc25a3572e440bbba3f09d63ffc825c7f0f
4
+ data.tar.gz: 5e6a1dbbec201fb5fcd4e4c6d000ecba84f53bf78a5f25f74e5b75145ff11683
5
5
  SHA512:
6
- metadata.gz: 05a90fe04d73b187c72bb1d5bdc732ae2b314670306174bf0430f0e996bab0ed7e76fa51209a7dd295c94f5b4bac173a67032de51f5d7d2e89fee5ddcf26c361
7
- data.tar.gz: 2a3b358b39146cb6e7bb6971f5cd7e881a528df16bd8823bea4cfa7cd8877f09bb9d8c57770176274e1a44e09b2731a86ae39354cc1ba4966aa76685b7d36eeb
6
+ metadata.gz: dae39ad0842e8fdf132993d465a9ff6bedab0e6174bf42c9f9ab4ffad98377d3352b6ecc3aca218480fb2c6be9eb36c9efb80c0230e0c95a2dbe2011da6461ad
7
+ data.tar.gz: 07c5e18d680c68e83bd03a3d20f64a2cf9ab62c513a00773d5a55a58e9f7c3fcf0f98268b8b3a63de8e18509fe0a237922e4eec6de64132a815e970da96d2a33
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Legion::Logging Changelog
2
2
 
3
+ ## v1.2.6
4
+
5
+ ### Added
6
+ - `Legion::Logging::Redactor`: PII/PHI redaction module with built-in patterns for SSN, email, phone, MRN, DOB, and credit card numbers
7
+ - Sensitive field-name redaction: fields named `password`, `secret`, `token`, `api_key`, `authorization` are always fully redacted
8
+ - Recursive redaction of nested hashes and arrays
9
+ - Custom pattern support via `Legion::Settings[:logging, :redactor, :custom_patterns]`
10
+ - `Legion::Logging::Shipper`: structured log event forwarding to external collectors with batch buffering and level filtering
11
+ - `Legion::Logging::Shipper::FileTransport`: writes JSON-lines to rotated log files for pickup by Filebeat/Fluentd
12
+ - `Legion::Logging::Shipper::HttpTransport`: POSTs JSON batches to HTTP endpoints (Splunk HEC, ELK Logstash)
13
+ - All SIEM shipping features disabled by default; opt-in via `logging.shipper.enabled: true`
14
+
3
15
  ## v1.2.5
4
16
 
5
17
  ### Fixed
data/CLAUDE.md CHANGED
@@ -8,16 +8,19 @@
8
8
  Ruby logging class for the LegionIO framework. Provides colorized console output via Rainbow, structured JSON logging (`format: :json`), and a consistent logging interface across all Legion gems and extensions.
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/legion-logging
11
+ **Version**: 1.2.5
11
12
  **License**: Apache-2.0
12
13
 
13
14
  ## Architecture
14
15
 
15
16
  ```
16
17
  Legion::Logging (singleton module)
17
- ├── Methods # Log level methods: debug, info, warn, error, fatal, unknown
18
- ├── Builder # Output destination (stdout/file), log level, formatter
19
- ├── Logger # Core logger configuration and setup
20
- └── Version # VERSION constant
18
+ ├── Methods # Log level methods: debug, info, warn, error, fatal, unknown
19
+ ├── Builder # Output destination (stdout/file), log level, formatter
20
+ ├── Logger # Core logger configuration and setup
21
+ ├── MultiIO # Write to multiple destinations simultaneously
22
+ ├── SIEMExporter # PHI-redacting SIEM export (Splunk HEC, ELK/OpenSearch)
23
+ └── Version # VERSION constant (1.2.5)
21
24
  ```
22
25
 
23
26
  ### Key Design Patterns
@@ -27,6 +30,8 @@ Legion::Logging (singleton module)
27
30
  - **Setup Method**: `Legion::Logging.setup(log_file:, level:)` configures output destination and level
28
31
  - **Structured JSON**: `format: :json` in settings outputs machine-parseable JSON log lines
29
32
  - **Shared Interface**: Same method signature (`info`, `warn`, `error`, etc.) across all Legion components
33
+ - **MultiIO**: Splits writes to stdout and a log file simultaneously (used by Builder when `log_file` is set)
34
+ - **SIEMExporter**: PHI redaction (SSN, phone, MRN, DOB patterns), `export_to_splunk` (HEC), `format_for_elk`
30
35
 
31
36
  ## Dependencies
32
37
 
@@ -43,6 +48,7 @@ Legion::Logging (singleton module)
43
48
  | `lib/legion/logging/builder.rb` | Output config and formatter |
44
49
  | `lib/legion/logging/logger.rb` | Core logger setup |
45
50
  | `lib/legion/logging/multi_io.rb` | Multi-output IO (write to multiple destinations simultaneously) |
51
+ | `lib/legion/logging/siem_exporter.rb` | PHI-redacting SIEM export helpers (Splunk HEC, ELK format) |
46
52
  | `lib/legion/logging/version.rb` | VERSION constant |
47
53
 
48
54
  ## Role in LegionIO
data/Gemfile CHANGED
@@ -3,6 +3,9 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
+
7
+ gem 'logger'
8
+
6
9
  group :test do
7
10
  gem 'rake'
8
11
  gem 'rspec'
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # legion-logging
2
2
 
3
- Logging module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides colorized console output via Rainbow and a consistent logging interface across all Legion gems and extensions.
3
+ Logging module for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides colorized console output via Rainbow, structured JSON logging, multi-output IO, and a consistent logging interface across all Legion gems and extensions.
4
+
5
+ **Version**: 1.2.5
4
6
 
5
7
  ## Installation
6
8
 
@@ -39,6 +41,27 @@ Legion::Logging.setup(level: 'info', format: :json)
39
41
 
40
42
  This is useful for log aggregation pipelines (Elasticsearch, Splunk, etc.).
41
43
 
44
+ ### Multi-Output IO
45
+
46
+ `Legion::Logging::MultiIO` writes to multiple destinations simultaneously — for example, stdout and a file at the same time. Used internally by the Builder when `log_file` is set alongside console output.
47
+
48
+ ### SIEM Export
49
+
50
+ `Legion::Logging::SIEMExporter` provides PHI-redacting export helpers for security event pipelines:
51
+
52
+ ```ruby
53
+ # Redact PHI patterns (SSN, phone, MRN, DOB) from a string
54
+ clean = Legion::Logging::SIEMExporter.redact_phi(raw_message)
55
+
56
+ # Export to Splunk HEC
57
+ Legion::Logging::SIEMExporter.export_to_splunk(event, hec_url: url, token: token)
58
+
59
+ # Format for ELK/OpenSearch
60
+ Legion::Logging::SIEMExporter.format_for_elk(event, index: 'legion')
61
+ ```
62
+
63
+ PHI patterns redacted: SSN (`###-##-####`), phone (`###-###-####`), MRN (`XX#######`), DOB (`##/##/####`).
64
+
42
65
  ## Requirements
43
66
 
44
67
  - Ruby >= 3.4
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Logging
5
+ module Redactor
6
+ PATTERNS = {
7
+ ssn: /\b\d{3}-\d{2}-\d{4}\b/,
8
+ email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/,
9
+ phone: /\b(?:\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/,
10
+ mrn: /\bMRN[:\s]*\d{6,10}\b/i,
11
+ dob: %r{\bDOB[:\s]*\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b}i,
12
+ credit_card: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/
13
+ }.freeze
14
+
15
+ SENSITIVE_FIELDS = %w[password secret token api_key authorization].freeze
16
+
17
+ REDACTED = '[REDACTED]'
18
+
19
+ class << self
20
+ def redact(event)
21
+ return event unless event.is_a?(Hash)
22
+
23
+ event.each_with_object({}) do |(key, value), result|
24
+ result[key] = sensitive_field?(key) ? REDACTED : redact_value(value)
25
+ end
26
+ end
27
+
28
+ def redact_value(value)
29
+ case value
30
+ when String then redact_string(value)
31
+ when Hash then redact(value)
32
+ when Array then value.map { |v| redact_value(v) }
33
+ else value
34
+ end
35
+ end
36
+
37
+ def redact_string(str)
38
+ result = str.dup
39
+ all_patterns.each_value { |pattern| result.gsub!(pattern, REDACTED) }
40
+ result
41
+ end
42
+
43
+ private
44
+
45
+ def sensitive_field?(key)
46
+ SENSITIVE_FIELDS.include?(key.to_s.downcase)
47
+ end
48
+
49
+ def all_patterns
50
+ @all_patterns ||= build_patterns
51
+ end
52
+
53
+ def build_patterns
54
+ patterns = PATTERNS.dup
55
+ custom = custom_patterns
56
+ custom.each { |name, regex| patterns[name.to_sym] = regex }
57
+ patterns
58
+ end
59
+
60
+ def custom_patterns
61
+ return {} unless defined?(Legion::Settings)
62
+
63
+ raw = Legion::Settings[:logging, :redactor, :custom_patterns]
64
+ return {} unless raw.is_a?(Hash)
65
+
66
+ raw.each_with_object({}) do |(name, pattern_str), acc|
67
+ acc[name] = Regexp.new(pattern_str)
68
+ rescue RegexpError
69
+ # skip invalid patterns
70
+ end
71
+ end
72
+
73
+ def reset_pattern_cache!
74
+ @all_patterns = nil
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module Legion
7
+ module Logging
8
+ module Shipper
9
+ module FileTransport
10
+ DEFAULT_PATH = '/var/log/legion/siem.log'
11
+
12
+ class << self
13
+ def ship(event)
14
+ path = resolve_path
15
+ FileUtils.mkdir_p(File.dirname(path))
16
+ File.open(path, 'a') do |f|
17
+ f.puts(::JSON.generate(event))
18
+ end
19
+ true
20
+ rescue StandardError => e
21
+ Legion::Logging.error("FileTransport ship failed: #{e.message}") if defined?(Legion::Logging)
22
+ false
23
+ end
24
+
25
+ private
26
+
27
+ def resolve_path
28
+ return settings_path if settings_path
29
+
30
+ DEFAULT_PATH
31
+ end
32
+
33
+ def settings_path
34
+ return nil unless defined?(Legion::Settings)
35
+
36
+ Legion::Settings[:logging, :shipper, :file, :path]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+
7
+ module Legion
8
+ module Logging
9
+ module Shipper
10
+ module HttpTransport
11
+ class << self
12
+ def ship(events)
13
+ endpoint = resolve_endpoint
14
+ return false unless endpoint
15
+
16
+ uri = URI(endpoint)
17
+ batch = Array(events)
18
+ body = build_body(batch, uri)
19
+
20
+ response = post(uri, body)
21
+ response.is_a?(Net::HTTPSuccess)
22
+ rescue StandardError => e
23
+ Legion::Logging.error("HttpTransport ship failed: #{e.message}") if defined?(Legion::Logging)
24
+ false
25
+ end
26
+
27
+ private
28
+
29
+ def post(uri, body)
30
+ req = Net::HTTP::Post.new(uri)
31
+ req['Content-Type'] = 'application/json'
32
+ apply_auth(req)
33
+
34
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https',
35
+ open_timeout: 5, read_timeout: 10) do |http|
36
+ http.request(req, body)
37
+ end
38
+ end
39
+
40
+ def build_body(events, uri)
41
+ # Splunk HEC expects { event: ... } per event; others expect an array
42
+ if splunk_hec?(uri)
43
+ events.map { |e| ::JSON.generate({ event: e, time: Time.now.to_f }) }.join("\n")
44
+ else
45
+ ::JSON.generate(events)
46
+ end
47
+ end
48
+
49
+ def splunk_hec?(uri)
50
+ uri.path.include?('/services/collector')
51
+ end
52
+
53
+ def apply_auth(req)
54
+ token = auth_token
55
+ return unless token
56
+
57
+ req['Authorization'] = if splunk_hec?(URI(req.path.empty? ? '/' : req.uri&.to_s || '/'))
58
+ "Splunk #{token}"
59
+ else
60
+ "Bearer #{token}"
61
+ end
62
+ end
63
+
64
+ def auth_token
65
+ return nil unless defined?(Legion::Settings)
66
+
67
+ Legion::Settings[:logging, :shipper, :auth_token]
68
+ end
69
+
70
+ def resolve_endpoint
71
+ return nil unless defined?(Legion::Settings)
72
+
73
+ Legion::Settings[:logging, :shipper, :endpoint]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'redactor'
4
+ require_relative 'shipper/file_transport'
5
+ require_relative 'shipper/http_transport'
6
+
7
+ module Legion
8
+ module Logging
9
+ module Shipper
10
+ LEVEL_ORDER = %w[debug info warn error fatal].freeze
11
+
12
+ TRANSPORTS = {
13
+ file: FileTransport,
14
+ http: HttpTransport
15
+ }.freeze
16
+
17
+ class << self
18
+ def ship(event)
19
+ return unless enabled?
20
+ return unless shippable_level?(event[:level] || event['level'])
21
+
22
+ redacted = Redactor.redact(event)
23
+ transport = TRANSPORTS[transport_type]
24
+ buffer_event(redacted) if transport
25
+ end
26
+
27
+ def flush
28
+ return if @buffer.nil? || @buffer.empty?
29
+
30
+ transport = TRANSPORTS[transport_type]
31
+ return unless transport
32
+
33
+ batch = nil
34
+ @mutex.synchronize do
35
+ batch = @buffer.dup
36
+ @buffer.clear
37
+ end
38
+
39
+ deliver(transport, batch)
40
+ end
41
+
42
+ def start
43
+ return unless enabled?
44
+ return if @flush_thread&.alive?
45
+
46
+ @buffer = []
47
+ @mutex = Mutex.new
48
+ interval = flush_interval
49
+ @flush_thread = Thread.new do
50
+ loop do
51
+ sleep interval
52
+ flush
53
+ end
54
+ end
55
+ @flush_thread.abort_on_exception = false
56
+ end
57
+
58
+ def stop
59
+ @flush_thread&.kill
60
+ @flush_thread = nil
61
+ flush
62
+ end
63
+
64
+ def enabled?
65
+ return false unless defined?(Legion::Settings)
66
+
67
+ Legion::Settings[:logging, :shipper, :enabled] == true
68
+ end
69
+
70
+ private
71
+
72
+ def buffer_event(event)
73
+ @buffer ||= []
74
+ @mutex ||= Mutex.new
75
+
76
+ full = false
77
+ @mutex.synchronize do
78
+ @buffer << event
79
+ full = @buffer.size >= batch_size
80
+ end
81
+
82
+ flush if full
83
+ end
84
+
85
+ def deliver(transport, batch)
86
+ if transport.method(:ship).arity == 1
87
+ # HttpTransport accepts a batch array
88
+ transport.ship(batch)
89
+ else
90
+ batch.each { |e| transport.ship(e) }
91
+ end
92
+ rescue StandardError => e
93
+ Legion::Logging.error("Shipper deliver failed: #{e.message}") if defined?(Legion::Logging)
94
+ end
95
+
96
+ def shippable_level?(level)
97
+ return true if level.nil?
98
+
99
+ min = minimum_level
100
+ LEVEL_ORDER.index(level.to_s.downcase).to_i >= LEVEL_ORDER.index(min).to_i
101
+ end
102
+
103
+ def transport_type
104
+ return :file unless defined?(Legion::Settings)
105
+
106
+ key = Legion::Settings[:logging, :shipper, :transport]
107
+ key ? key.to_sym : :file
108
+ end
109
+
110
+ def batch_size
111
+ return 100 unless defined?(Legion::Settings)
112
+
113
+ Legion::Settings[:logging, :shipper, :batch_size] || 100
114
+ end
115
+
116
+ def flush_interval
117
+ return 5 unless defined?(Legion::Settings)
118
+
119
+ Legion::Settings[:logging, :shipper, :flush_interval] || 5
120
+ end
121
+
122
+ def minimum_level
123
+ return 'warn' unless defined?(Legion::Settings)
124
+
125
+ levels = Legion::Settings[:logging, :shipper, :levels]
126
+ return 'warn' unless levels.is_a?(Array) && !levels.empty?
127
+
128
+ levels.min_by { |l| LEVEL_ORDER.index(l.to_s) || 99 }.to_s
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Logging
5
- VERSION = '1.2.5'
5
+ VERSION = '1.2.6'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.5
4
+ version: 1.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -61,6 +61,10 @@ files:
61
61
  - lib/legion/logging/logger.rb
62
62
  - lib/legion/logging/methods.rb
63
63
  - lib/legion/logging/multi_io.rb
64
+ - lib/legion/logging/redactor.rb
65
+ - lib/legion/logging/shipper.rb
66
+ - lib/legion/logging/shipper/file_transport.rb
67
+ - lib/legion/logging/shipper/http_transport.rb
64
68
  - lib/legion/logging/siem_exporter.rb
65
69
  - lib/legion/logging/version.rb
66
70
  - sonar-project.properties