-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathlograge.rb
More file actions
169 lines (146 loc) · 7.61 KB
/
Copy pathlograge.rb
File metadata and controls
169 lines (146 loc) · 7.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# frozen_string_literal: true
# SPDX-FileCopyrightText: 2024 UncleSp1d3r
# SPDX-License-Identifier: MPL-2.0
# Lograge configuration for structured logging in production.
# Replaces verbose Rails logs with single-line JSON entries per request.
# This enables easier parsing by log aggregation services (ELK, Datadog, etc.).
module CipherSwarm
module Logging
# Cap exception messages to bound log line size and prevent log flooding
# from attacker-controlled error text. 500 characters preserves enough
# context for debugging (class name + first sentence of message) while
# staying well under typical line-length limits for log aggregators.
EXCEPTION_MESSAGE_MAX_LEN = 500
# Marker appended when an exception message is truncated. Keeps a
# truncated log line visually distinguishable from a complete one.
TRUNCATION_MARKER = "…"
# Characters that must be stripped from exception messages before they
# enter the single-line JSON envelope, with rationale:
# - \r, \n: ASCII line terminators that break the envelope.
# - , (U+2028 LINE SEPARATOR, U+2029 PARAGRAPH SEPARATOR):
# Unicode separators that some JS-based log parsers (Kibana,
# Loki push clients) treat as newline equivalents.
# - (U+0000 NUL): some log shippers truncate at NUL, silently
# dropping the rest of the line.
# Deliberately NOT stripped: \t, U+000B (VT), U+000C (FF), U+0007 (BEL),
# U+0008 (BS). These do not break the JSON envelope and may carry useful
# whitespace in error messages.
LINE_BREAKING_CHARS = /[\r\n
]+/
# Sanitize an exception message string against log injection.
# Strips control chars that would break the single-line JSON envelope,
# truncates to EXCEPTION_MESSAGE_MAX_LEN (appending TRUNCATION_MARKER),
# and scrubs invalid UTF-8 sequences so downstream encoding cannot raise.
# Returns a String. Never raises — see {#safe_exception_message}.
def self.sanitize_exception_message(raw)
utf8_safe = raw.to_s.dup.force_encoding(Encoding::UTF_8).scrub
stripped = utf8_safe.gsub(LINE_BREAKING_CHARS, " ").strip
if stripped.length > EXCEPTION_MESSAGE_MAX_LEN
truncated_to = EXCEPTION_MESSAGE_MAX_LEN - TRUNCATION_MARKER.length
"#{stripped[0, truncated_to]}#{TRUNCATION_MARKER}"
else
stripped
end
end
# Defensive wrapper: never let a pathological exception message drop the
# log line. If sanitization itself raises (encoding corner case, frozen
# string in a future Ruby, etc.), emit a fallback envelope so the JSON
# log line is always written.
def self.safe_exception_message(raw)
sanitize_exception_message(raw)
rescue StandardError => e
"<sanitize_failed: #{e.class}>"
end
end
end
Rails.application.configure do
# Enable in production for structured logging and in development for testing purposes
config.lograge.enabled = Rails.env.production? || Rails.env.development?
# Use JSON formatter for machine-readable logs
config.lograge.formatter = Lograge::Formatters::Json.new
# Augment each log entry with timestamp + exception context.
#
# Note: only fields that actually appear in `process_action.action_controller`
# event.payload belong here. Request metadata (host, request_id, user_agent,
# ip) and domain IDs (agent_id, task_id, attack_id, user_id) come in via
# `custom_payload` below, which lograge merges on top of this hash. Trying
# to read those keys off `event.payload` directly returns nil — see
# github.com/roidrage/lograge log_subscribers/base.rb for the merge order.
config.lograge.custom_options = lambda do |event|
options = { time: Time.zone.now.iso8601 }
# OWASP Logging Cheat Sheet: sanitize exception messages before writing
# to logs to prevent log injection. See
# `CipherSwarm::Logging.sanitize_exception_message` for the full chain
# (CR/LF/U+2028/U+2029/NUL stripping, UTF-8 scrubbing, length cap with
# truncation marker). Coerce class to String so a raw exception Class
# constant cannot surface.
if event.payload[:exception].present?
exception = event.payload[:exception]
options[:exception_class] = exception.first.to_s
options[:exception_message] = CipherSwarm::Logging.safe_exception_message(exception.second)
end
if event.payload[:exception_object].present?
options[:backtrace] = event.payload[:exception_object].backtrace&.first(5)
end
options.compact
end
# Keep original Rails log for errors (useful for debugging)
config.lograge.keep_original_rails_log = false
# Log to STDOUT for containerized environments
config.lograge.logger = ActiveSupport::Logger.new($stdout)
# OWASP / PII policy for the request payload below:
# - `ip` and `user_agent` are logged on every request to support incident
# response and agent-traffic correlation. Acceptable trade-off in
# CipherSwarm's air-gapped deployment model; operators in GDPR/CCPA
# contexts must ensure their log retention policy covers these fields.
# - This block extracts only IDs and request metadata. Sensitive submission
# fields (`hash`, `hash_value`, `plain_text`, credentials) are redacted
# upstream by `config.filter_parameters` in `filter_parameter_logging.rb`
# — lograge does not log raw request bodies, so the filter list is the
# only path those values could leak through.
config.lograge.custom_payload do |controller|
payload = {
host: controller.request.host,
request_id: controller.request.request_id,
user_agent: controller.request.user_agent,
ip: controller.request.remote_ip
}
# Extract agent_id from various sources
if controller.respond_to?(:current_agent, true) && controller.send(:current_agent).present?
payload[:agent_id] = controller.send(:current_agent).id
elsif controller.params[:agent_id].present?
payload[:agent_id] = controller.params[:agent_id]
elsif controller.instance_variable_defined?(:@agent) && controller.instance_variable_get(:@agent).present?
payload[:agent_id] = controller.instance_variable_get(:@agent).id
end
# Extract task_id from params or instance variable
if controller.params[:id].present? && controller.controller_name == "tasks"
payload[:task_id] = controller.params[:id]
elsif controller.params[:task_id].present?
payload[:task_id] = controller.params[:task_id]
elsif controller.instance_variable_defined?(:@task) && controller.instance_variable_get(:@task).present?
payload[:task_id] = controller.instance_variable_get(:@task).id
end
# Extract attack_id from params or instance variable
if controller.params[:id].present? && controller.controller_name == "attacks"
payload[:attack_id] = controller.params[:id]
elsif controller.params[:attack_id].present?
payload[:attack_id] = controller.params[:attack_id]
elsif controller.instance_variable_defined?(:@attack) && controller.instance_variable_get(:@attack).present?
payload[:attack_id] = controller.instance_variable_get(:@attack).id
end
# Extract user_id from Devise current_user (web UI requests)
if controller.respond_to?(:current_user, true) && controller.send(:current_user).present?
payload[:user_id] = controller.send(:current_user).id
end
payload
end
# Ignore certain paths from logging (health checks, assets, etc.)
config.lograge.ignore_actions = [
"Rails::HealthController#show"
]
# Reduce noise by ignoring certain formats
config.lograge.ignore_custom = lambda do |event|
# Ignore asset requests in development if lograge is enabled there for testing
event.payload[:path]&.start_with?("/assets")
end
end