train-core 3.10.8 → 3.16.3
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/lib/train/audit_log.rb +21 -0
- data/lib/train/extras/stat.rb +1 -1
- data/lib/train/logger_ext.rb +8 -0
- data/lib/train/options.rb +30 -1
- data/lib/train/platforms/detect/helpers/os_common.rb +4 -3
- data/lib/train/platforms/detect/helpers/os_windows.rb +163 -5
- data/lib/train/platforms/detect/specifications/os.rb +21 -0
- data/lib/train/plugins/base_connection.rb +25 -9
- data/lib/train/plugins/transport.rb +6 -0
- data/lib/train/transports/local.rb +65 -5
- data/lib/train/transports/ssh_connection.rb +6 -1
- data/lib/train/version.rb +1 -1
- data/lib/train.rb +2 -1
- metadata +18 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c7ff43a79457e6d900c1e1e293c1e2ad48c6c555863cb816d64ddd96e02f03f
|
|
4
|
+
data.tar.gz: b42bdd2900ac7cc3c43f0df0467def0d46906a51ebfa64de3344b35e7504ae7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27b1d7e67fed6319322a3928ed0ef0b24a5846bfa32a2998627ece0c943fa712205086fafb0a063214fe6c7659dd7979e8182e2984a41e9d0983590b7d5a9556
|
|
7
|
+
data.tar.gz: 13a9e5d5da5bdda8468f95c2bbb1b259c4052c4c6af4e0191038027f3f7ad226b269093e487f593a47418defda016151e33bad08cf0f01b19ae25a5520d5ac5a
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Train
|
|
2
|
+
class AuditLog
|
|
3
|
+
# Default values for audit log options are set in the options.rb
|
|
4
|
+
def self.create(options = {})
|
|
5
|
+
# Load monkey-patch to disable leading comment in logfiles
|
|
6
|
+
require_relative "logger_ext"
|
|
7
|
+
|
|
8
|
+
logger = Logger.new(options[:audit_log_location], options[:audit_log_frequency], options[:audit_log_size])
|
|
9
|
+
logger.level = options[:level] || Logger::INFO
|
|
10
|
+
logger.progname = options[:audit_log_app_name]
|
|
11
|
+
logger.datetime_format = "%Y-%m-%d %H:%M:%S"
|
|
12
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
|
13
|
+
{
|
|
14
|
+
timestamp: datetime.to_s,
|
|
15
|
+
app: progname,
|
|
16
|
+
}.merge(msg).compact.to_json + $/
|
|
17
|
+
end
|
|
18
|
+
logger
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/train/extras/stat.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Train::Extras
|
|
|
34
34
|
|
|
35
35
|
def self.linux_stat(shell_escaped_path, backend, follow_symlink)
|
|
36
36
|
lstat = follow_symlink ? " -L" : ""
|
|
37
|
-
format = (backend.os.esx? || %w{alpine yocto ubios}.include?(backend.os[:name])) ? "-c" : "--printf"
|
|
37
|
+
format = (backend.os.esx? || %w{alpine yocto ubios}.include?(backend.os[:name]) || %w{chainguard}.include?(backend.os[:family])) ? "-c" : "--printf"
|
|
38
38
|
res = backend.run_command("stat#{lstat} #{shell_escaped_path} 2>/dev/null #{format} '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'")
|
|
39
39
|
# ignore the exit_code: it is != 0 if selinux labels are not supported
|
|
40
40
|
# on the system.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
# Part of Audit Log.
|
|
3
|
+
# The default logger implementation injects a comment as the first line of the
|
|
4
|
+
# log file, which makes it an invalid JSON file. No way to disable that other than monkey-patching.
|
|
5
|
+
|
|
6
|
+
class Logger::LogDevice
|
|
7
|
+
def add_log_header(file); end
|
|
8
|
+
end
|
data/lib/train/options.rb
CHANGED
|
@@ -34,6 +34,18 @@ module Train
|
|
|
34
34
|
@default_options
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Created separate method to set the default audit log options so that it will be handled separately
|
|
38
|
+
# and will not break any existing functionality
|
|
39
|
+
def default_audit_log_options
|
|
40
|
+
{
|
|
41
|
+
enable_audit_log: { default: false },
|
|
42
|
+
audit_log_location: { required: true, default: nil },
|
|
43
|
+
audit_log_app_name: { default: "train" },
|
|
44
|
+
audit_log_size: { default: nil },
|
|
45
|
+
audit_log_frequency: { default: 0 },
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
37
49
|
def include_options(other)
|
|
38
50
|
unless other.respond_to?(:default_options)
|
|
39
51
|
raise "Trying to include options from module #{other.inspect}, "\
|
|
@@ -47,13 +59,18 @@ module Train
|
|
|
47
59
|
# @return [Hash] options, which created this Transport
|
|
48
60
|
attr_reader :options
|
|
49
61
|
|
|
62
|
+
def default_audit_log_options
|
|
63
|
+
self.class.default_audit_log_options
|
|
64
|
+
end
|
|
65
|
+
|
|
50
66
|
def default_options
|
|
51
67
|
self.class.default_options
|
|
52
68
|
end
|
|
53
69
|
|
|
54
70
|
def merge_options(base, opts)
|
|
55
71
|
res = base.merge(opts || {})
|
|
56
|
-
|
|
72
|
+
# Also merge the default audit log options into the options so that those are available at the time of validation.
|
|
73
|
+
default_options.merge(default_audit_log_options).each do |field, hm|
|
|
57
74
|
next unless res[field].nil? && hm.key?(:default)
|
|
58
75
|
|
|
59
76
|
default = hm[:default]
|
|
@@ -78,6 +95,18 @@ module Train
|
|
|
78
95
|
end
|
|
79
96
|
opts
|
|
80
97
|
end
|
|
98
|
+
|
|
99
|
+
# Introduced this method to validate only audit log options and avoiding call to validate_options so
|
|
100
|
+
# that it will no break existing implementation.
|
|
101
|
+
def validate_audit_log_options(opts)
|
|
102
|
+
default_audit_log_options.each do |field, hm|
|
|
103
|
+
if opts[field].nil? && hm[:required]
|
|
104
|
+
raise Train::ClientError,
|
|
105
|
+
"You must provide a value for #{field.to_s.inspect}."
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
opts
|
|
109
|
+
end
|
|
81
110
|
end
|
|
82
111
|
end
|
|
83
112
|
end
|
|
@@ -8,7 +8,7 @@ module Train::Platforms::Detect::Helpers
|
|
|
8
8
|
include Train::Platforms::Detect::Helpers::Windows
|
|
9
9
|
|
|
10
10
|
def ruby_host_os(regex)
|
|
11
|
-
::RbConfig::CONFIG["host_os"]
|
|
11
|
+
regex.match?(::RbConfig::CONFIG["host_os"])
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def winrm?
|
|
@@ -35,8 +35,9 @@ module Train::Platforms::Detect::Helpers
|
|
|
35
35
|
|
|
36
36
|
def command_output(cmd)
|
|
37
37
|
res = @backend.run_command(cmd)
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
# To suppress warning: literal string will be frozen in the future
|
|
39
|
+
stdout = String.new(res.stdout)
|
|
40
|
+
stderr = String.new(res.stderr)
|
|
40
41
|
# When you try to execute command using ssh connection as root user and you have provided ssh user identity file
|
|
41
42
|
# it gives standard output to login as authorized user other than root. To show this standard output as an error
|
|
42
43
|
# to user we are matching the string of stdout and raising the error here so that user gets exact information.
|
|
@@ -25,7 +25,9 @@ module Train::Platforms::Detect::Helpers
|
|
|
25
25
|
@platform[:name] = "Windows #{@platform[:release]}"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# Prefer retrieving the OS details via `wmic` if available on the system to retain existing behavior.
|
|
29
|
+
# If `wmic` is not available, fall back to using cmd-only commands as an alternative method.
|
|
30
|
+
wmic_available? ? read_wmic : read_cmd_os
|
|
29
31
|
true
|
|
30
32
|
end
|
|
31
33
|
|
|
@@ -42,7 +44,9 @@ module Train::Platforms::Detect::Helpers
|
|
|
42
44
|
@platform[:release] = payload["Version"]
|
|
43
45
|
@platform[:name] = payload["Caption"]
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
# Prefer retrieving the OS details via `wmic` if available on the system to retain existing behavior.
|
|
48
|
+
# If `wmic` is not available, fall back to using CIM as an alternative method.
|
|
49
|
+
wmic_available? ? read_wmic : read_cim_os
|
|
46
50
|
true
|
|
47
51
|
rescue
|
|
48
52
|
false
|
|
@@ -108,7 +112,7 @@ module Train::Platforms::Detect::Helpers
|
|
|
108
112
|
def windows_uuid
|
|
109
113
|
uuid = windows_uuid_from_chef
|
|
110
114
|
uuid = windows_uuid_from_machine_file if uuid.nil?
|
|
111
|
-
uuid =
|
|
115
|
+
uuid = windows_uuid_from_wmic_or_cim if uuid.nil?
|
|
112
116
|
uuid = windows_uuid_from_registry if uuid.nil?
|
|
113
117
|
raise Train::TransportError, "Cannot find a UUID for your node." if uuid.nil?
|
|
114
118
|
|
|
@@ -134,11 +138,51 @@ module Train::Platforms::Detect::Helpers
|
|
|
134
138
|
json["node_uuid"]
|
|
135
139
|
end
|
|
136
140
|
|
|
141
|
+
def windows_uuid_from_wmic_or_cim
|
|
142
|
+
# Retrieve the Windows UUID using `wmic` if it is available and not marked as deprecated, maintaining compatibility with older systems.
|
|
143
|
+
# If `wmic` is unavailable or deprecated, use the `Get-CimInstance` command, which is the modern and recommended approach by Microsoft.
|
|
144
|
+
wmic_available? ? windows_uuid_from_wmic : windows_uuid_from_cim
|
|
145
|
+
end
|
|
146
|
+
|
|
137
147
|
def windows_uuid_from_wmic
|
|
138
|
-
|
|
148
|
+
# Switched from `wmic csproduct get UUID` to `wmic csproduct get UUID /value`
|
|
149
|
+
# to make the parsing of the UUID more reliable and consistent.
|
|
150
|
+
#
|
|
151
|
+
# When using the original `wmic csproduct get UUID` command, the output includes
|
|
152
|
+
# a header line and spacing that can vary depending on the system, making it harder
|
|
153
|
+
# to reliably extract the UUID. In some cases, splitting by line and taking the last
|
|
154
|
+
# element returns an empty string, even when exit_status is 0.
|
|
155
|
+
#
|
|
156
|
+
# Example:
|
|
157
|
+
#
|
|
158
|
+
# (byebug) result = @backend.run_command("wmic csproduct get UUID")
|
|
159
|
+
# #<struct Train::Extras::CommandResult stdout="UUID \r\r\nEC20EBD7-8E03-06A8-645F-2D22E5A3BA4B \r\r\n\r\r\n", stderr="", exit_status=0>
|
|
160
|
+
# (byebug) result.stdout
|
|
161
|
+
# "UUID \r\r\nEC20EBD7-8E03-06A8-645F-2D22E5A3BA4B \r\r\n\r\r\n"
|
|
162
|
+
# (byebug) result.exit_status
|
|
163
|
+
# 0
|
|
164
|
+
# (byebug) result.stdout.split("\r\n")[-1].strip
|
|
165
|
+
# ""
|
|
166
|
+
#
|
|
167
|
+
# In contrast, `wmic csproduct get UUID /value` returns a consistent `UUID=<value>` format,
|
|
168
|
+
# which is more suitable for regex matching.
|
|
169
|
+
#
|
|
170
|
+
# Example:
|
|
171
|
+
#
|
|
172
|
+
# byebug) result = @backend.run_command("wmic csproduct get UUID /value")
|
|
173
|
+
# #<struct Train::Extras::CommandResult stdout="\r\r\n\r\r\nUUID=EC20EBD7-8E03-06A8-645F-2D22E5A3BA4B\r\r\n\r\r\n\r\r\n\r\r\n", stderr="", exit_status=0>
|
|
174
|
+
# (byebug) result.stdout
|
|
175
|
+
# "\r\r\n\r\r\nUUID=EC20EBD7-8E03-06A8-645F-2D22E5A3BA4B\r\r\n\r\r\n\r\r\n\r\r\n"
|
|
176
|
+
# (byebug) result.stdout&.match(/UUID=([A-F0-9\-]+)/i)&.captures&.first
|
|
177
|
+
# "EC20EBD7-8E03-06A8-645F-2D22E5A3BA4B"
|
|
178
|
+
#
|
|
179
|
+
# This change improves parsing reliability and handles edge cases where the previous
|
|
180
|
+
# approach would return `nil` or raise errors on empty output lines.
|
|
181
|
+
|
|
182
|
+
result = @backend.run_command("wmic csproduct get UUID /value")
|
|
139
183
|
return unless result.exit_status == 0
|
|
140
184
|
|
|
141
|
-
result.stdout
|
|
185
|
+
result.stdout&.match(/UUID=([A-F0-9\-]+)/i)&.captures&.first
|
|
142
186
|
end
|
|
143
187
|
|
|
144
188
|
def windows_uuid_from_registry
|
|
@@ -148,5 +192,119 @@ module Train::Platforms::Detect::Helpers
|
|
|
148
192
|
|
|
149
193
|
result.stdout.chomp
|
|
150
194
|
end
|
|
195
|
+
|
|
196
|
+
# Checks if `wmic` is available and not deprecated
|
|
197
|
+
def wmic_available?
|
|
198
|
+
# Return memoized value if already checked
|
|
199
|
+
return @wmic_available unless @wmic_available.nil?
|
|
200
|
+
|
|
201
|
+
# Runs the `wmic /?`` command, which provides help information for the WMIC (Windows Management Instrumentation Command-line) tool.
|
|
202
|
+
# It displays a list of available global switches and aliases, as well as details about their usage.
|
|
203
|
+
# The output also includes information about deprecated status for the 'wmic' tool.
|
|
204
|
+
result = @backend.run_command("wmic /?")
|
|
205
|
+
|
|
206
|
+
# Check if command ran successfully and output does not contain 'wmic is deprecated'
|
|
207
|
+
@wmic_available = result.exit_status == 0 && !(result.stdout.downcase.include?("wmic is deprecated"))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def read_cim_os
|
|
211
|
+
cmd = 'powershell -Command "Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, BuildNumber | ConvertTo-Json"'
|
|
212
|
+
res = @backend.run_command(cmd)
|
|
213
|
+
return unless res.exit_status == 0
|
|
214
|
+
|
|
215
|
+
begin
|
|
216
|
+
sys_info = JSON.parse(res.stdout)
|
|
217
|
+
@platform[:release] = sys_info["Version"]
|
|
218
|
+
@platform[:build] = sys_info["BuildNumber"]
|
|
219
|
+
@platform[:name] = sys_info["Caption"]
|
|
220
|
+
@platform[:name] = @platform[:name].gsub("Microsoft", "").strip unless @platform[:name].empty?
|
|
221
|
+
@platform[:arch] = read_cim_cpu
|
|
222
|
+
rescue
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def read_cim_cpu
|
|
228
|
+
cmd = 'powershell -Command "(Get-CimInstance Win32_Processor).Architecture"'
|
|
229
|
+
res = @backend.run_command(cmd)
|
|
230
|
+
return unless res.exit_status == 0
|
|
231
|
+
|
|
232
|
+
arch_map = {
|
|
233
|
+
0 => "i386",
|
|
234
|
+
1 => "mips",
|
|
235
|
+
2 => "alpha",
|
|
236
|
+
3 => "powerpc",
|
|
237
|
+
5 => "arm",
|
|
238
|
+
6 => "ia64",
|
|
239
|
+
9 => "x86_64",
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
arch_map[res.stdout.strip.to_i]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Fallback method for reading OS info using cmd-only commands when wmic is not available
|
|
246
|
+
def read_cmd_os
|
|
247
|
+
# Try to get architecture from PROCESSOR_ARCHITECTURE environment variable
|
|
248
|
+
# This covers the same architectures as wmic CPU detection but uses environment variables
|
|
249
|
+
# which are available on all Windows versions since NT
|
|
250
|
+
arch_res = @backend.run_command("echo %PROCESSOR_ARCHITECTURE%")
|
|
251
|
+
if arch_res.exit_status == 0
|
|
252
|
+
arch_string = arch_res.stdout.strip.downcase
|
|
253
|
+
# Only set architecture if we got actual output
|
|
254
|
+
unless arch_string.empty?
|
|
255
|
+
@platform[:arch] = case arch_string
|
|
256
|
+
when "x86"
|
|
257
|
+
"i386"
|
|
258
|
+
when "amd64", "x64"
|
|
259
|
+
"x86_64"
|
|
260
|
+
when "ppc", "powerpc"
|
|
261
|
+
"powerpc"
|
|
262
|
+
else
|
|
263
|
+
# For any unknown architecture, preserve the original value
|
|
264
|
+
# This handles: arm64, ia64, arm, mips, alpha, and future architectures
|
|
265
|
+
arch_string
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
# If PROCESSOR_ARCHITECTURE fails, architecture remains unset (consistent with other methods)
|
|
270
|
+
|
|
271
|
+
# Try to get more detailed OS info from systeminfo command as fallback
|
|
272
|
+
# This is slower than wmic but works without PowerShell
|
|
273
|
+
# Only override the basic info from check_cmd if systeminfo provides better data
|
|
274
|
+
sysinfo_res = @backend.run_command("systeminfo")
|
|
275
|
+
if sysinfo_res.exit_status == 0
|
|
276
|
+
sysinfo_res.stdout.lines.each do |line|
|
|
277
|
+
line = line.strip
|
|
278
|
+
if line =~ /^OS Name:\s*(.+)$/i
|
|
279
|
+
os_name = $1.strip
|
|
280
|
+
# Only override if we get a more detailed name than the basic "Windows X.X.X" from check_cmd
|
|
281
|
+
detailed_name = os_name.gsub("Microsoft", "").strip
|
|
282
|
+
@platform[:name] = detailed_name unless detailed_name.empty?
|
|
283
|
+
elsif line =~ /^OS Version:\s*(.+)$/i
|
|
284
|
+
version_info = $1.strip
|
|
285
|
+
# Extract version number from format like "10.0.19044 N/A Build 19044"
|
|
286
|
+
if version_info =~ /^(\d+\.\d+\.\d+)/
|
|
287
|
+
# Only override release if systeminfo provides the same or more detailed version
|
|
288
|
+
systeminfo_release = $1
|
|
289
|
+
@platform[:release] = systeminfo_release if systeminfo_release
|
|
290
|
+
end
|
|
291
|
+
# Extract build number (this is additional info not available from check_cmd)
|
|
292
|
+
if version_info =~ /Build (\d+)/
|
|
293
|
+
@platform[:build] = $1
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
# If systeminfo fails, we keep the basic info from check_cmd method
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def windows_uuid_from_cim
|
|
302
|
+
cmd = 'powershell -Command "(Get-CimInstance -Class Win32_ComputerSystemProduct).UUID"'
|
|
303
|
+
res = @backend.run_command(cmd)
|
|
304
|
+
return unless res.exit_status == 0
|
|
305
|
+
|
|
306
|
+
res.stdout.strip
|
|
307
|
+
end
|
|
308
|
+
|
|
151
309
|
end
|
|
152
310
|
end
|
|
@@ -232,6 +232,27 @@ module Train::Platforms::Detect::Specifications
|
|
|
232
232
|
end
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
+
declare_category("chainguard", "linux") do
|
|
236
|
+
rel = linux_os_release
|
|
237
|
+
rel && rel["ID"] =~ /(wolfi|chainguard)/
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
declare_instance("wolfi", "Wolfi Linux", "chainguard") do
|
|
241
|
+
rel = linux_os_release
|
|
242
|
+
if rel && rel["ID"] =~ /wolfi/
|
|
243
|
+
@platform[:release] = rel["VERSION_ID"]
|
|
244
|
+
true
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
declare_instance("chainguard", "Chainguard Linux", "chainguard") do
|
|
249
|
+
rel = linux_os_release
|
|
250
|
+
if rel && rel["ID"] =~ /chainguard/
|
|
251
|
+
@platform[:release] = rel["VERSION_ID"]
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
235
256
|
# brocade family detected here if device responds to 'uname' command,
|
|
236
257
|
# happens when logging in as root
|
|
237
258
|
plat.family("brocade").title("Brocade Family").in_family("linux")
|
|
@@ -3,6 +3,7 @@ require_relative "../extras"
|
|
|
3
3
|
require_relative "../file"
|
|
4
4
|
require "fileutils" unless defined?(FileUtils)
|
|
5
5
|
require "logger"
|
|
6
|
+
require_relative "../audit_log"
|
|
6
7
|
|
|
7
8
|
class Train::Plugins::Transport
|
|
8
9
|
# A Connection instance can be generated and re-generated, given new
|
|
@@ -20,10 +21,21 @@ class Train::Plugins::Transport
|
|
|
20
21
|
# @yield [self] yields itself for block-style invocation
|
|
21
22
|
def initialize(options = nil)
|
|
22
23
|
@options = options || {}
|
|
24
|
+
|
|
23
25
|
@logger = @options.delete(:logger) || Logger.new($stdout, level: :fatal)
|
|
24
26
|
Train::Platforms::Detect::Specifications::OS.load
|
|
25
27
|
Train::Platforms::Detect::Specifications::Api.load
|
|
26
28
|
|
|
29
|
+
# In run_command all options are not accessible as some of them gets deleted in transit.
|
|
30
|
+
# To make the data like hostname, username available to aduit logs dup the options
|
|
31
|
+
@audit_log_data = options.dup || {}
|
|
32
|
+
# For transport other than local all audit log options accessible inside transport_options key
|
|
33
|
+
if !@options.empty? && @options[:transport_options] && @options[:transport_options][:enable_audit_log]
|
|
34
|
+
@audit_log = Train::AuditLog.create(options[:transport_options])
|
|
35
|
+
elsif !@options.empty? && @options[:enable_audit_log]
|
|
36
|
+
@audit_log = Train::AuditLog.create(@options)
|
|
37
|
+
end
|
|
38
|
+
|
|
27
39
|
# default caching options
|
|
28
40
|
@cache_enabled = {
|
|
29
41
|
file: true,
|
|
@@ -140,11 +152,14 @@ class Train::Plugins::Transport
|
|
|
140
152
|
# Some implementations do not accept an opts argument.
|
|
141
153
|
# We cannot update all implementations to accept opts due to them being separate plugins.
|
|
142
154
|
# Therefore here we check the implementation's arity to maintain compatibility.
|
|
155
|
+
@audit_log.info({ type: "cmd", command: "#{cmd}", user: @audit_log_data[:username], hostname: @audit_log_data[:hostname] }) if @audit_log
|
|
156
|
+
|
|
143
157
|
case method(:run_command_via_connection).arity.abs
|
|
144
158
|
when 1
|
|
145
159
|
return run_command_via_connection(cmd, &data_handler) unless cache_enabled?(:command)
|
|
146
160
|
|
|
147
161
|
@cache[:command][cmd] ||= run_command_via_connection(cmd, &data_handler)
|
|
162
|
+
|
|
148
163
|
when 2
|
|
149
164
|
return run_command_via_connection(cmd, opts, &data_handler) unless cache_enabled?(:command)
|
|
150
165
|
|
|
@@ -157,29 +172,31 @@ class Train::Plugins::Transport
|
|
|
157
172
|
# This is the main file call for all connections. This will call the private
|
|
158
173
|
# file_via_connection on the connection with optional caching
|
|
159
174
|
def file(path, *args)
|
|
175
|
+
@audit_log.info({ type: "file", path: "#{path}", user: @audit_log_data[:username], hostname: @audit_log_data[:hostname] }) if @audit_log
|
|
160
176
|
return file_via_connection(path, *args) unless cache_enabled?(:file)
|
|
161
177
|
|
|
162
178
|
@cache[:file][path] ||= file_via_connection(path, *args)
|
|
163
179
|
end
|
|
164
180
|
|
|
165
|
-
# Uploads local files
|
|
181
|
+
# Uploads local files to remote host.
|
|
166
182
|
#
|
|
167
|
-
# @param locals [Array<String>]
|
|
183
|
+
# @param locals [String, Array<String>] path to local files
|
|
168
184
|
# @param remote [String] path to remote destination
|
|
169
185
|
# @raise [TransportFailed] if the files could not all be uploaded
|
|
170
186
|
# successfully, which may vary by implementation
|
|
171
187
|
def upload(locals, remote)
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
remote_directory = file(remote).directory?
|
|
189
|
+
|
|
190
|
+
if locals.is_a?(Array) && !remote_directory
|
|
191
|
+
raise Train::TransportError, "#{self.class} expects remote directory as second upload parameter for multi-file uploads"
|
|
174
192
|
end
|
|
175
193
|
|
|
176
194
|
Array(locals).each do |local|
|
|
177
|
-
|
|
178
|
-
remote_file
|
|
179
|
-
|
|
195
|
+
remote_file = remote_directory ? File.join(remote, File.basename(local)) : remote
|
|
196
|
+
@audit_log.info({ type: "file upload", source: local, destination: remote_file, user: @audit_log_data[:username], hostname: @audit_log_data[:hostname] }) if @audit_log
|
|
180
197
|
logger.debug("Attempting to upload '#{local}' as file #{remote_file}")
|
|
181
198
|
|
|
182
|
-
file(remote_file).content =
|
|
199
|
+
file(remote_file).content = File.read(local)
|
|
183
200
|
end
|
|
184
201
|
end
|
|
185
202
|
|
|
@@ -197,7 +214,6 @@ class Train::Plugins::Transport
|
|
|
197
214
|
Array(remotes).each do |remote|
|
|
198
215
|
new_content = file(remote).content
|
|
199
216
|
local_file = File.join(local, File.basename(remote))
|
|
200
|
-
|
|
201
217
|
logger.debug("Attempting to download '#{remote}' as file #{local_file}")
|
|
202
218
|
|
|
203
219
|
File.open(local_file, "w") { |fp| fp.write(new_content) }
|
|
@@ -21,6 +21,12 @@ class Train::Plugins
|
|
|
21
21
|
def initialize(options = {})
|
|
22
22
|
@options = merge_options({}, options || {})
|
|
23
23
|
@logger = @options[:logger] || Logger.new($stdout, level: :fatal)
|
|
24
|
+
# Validates audit log configuration options if audit log is enabled
|
|
25
|
+
# The reason to implement different validate method for audit log options is
|
|
26
|
+
# to validate only audit log options and not to break any existing validate_option implementation.
|
|
27
|
+
if !@options.empty? && @options[:enable_audit_log]
|
|
28
|
+
validate_audit_log_options(options)
|
|
29
|
+
end
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
# Create a connection to the target. Options may be provided
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
require_relative "../plugins"
|
|
6
6
|
require_relative "../errors"
|
|
7
7
|
require "mixlib/shellout" unless defined?(Mixlib::ShellOut)
|
|
8
|
+
require "ostruct" unless defined?(OpenStruct)
|
|
8
9
|
|
|
9
10
|
module Train::Transports
|
|
10
11
|
class Local < Train.plugin(1)
|
|
@@ -228,11 +229,28 @@ module Train::Transports
|
|
|
228
229
|
at_exit { close rescue Errno::EIO }
|
|
229
230
|
|
|
230
231
|
pipe = nil
|
|
232
|
+
ownership_verified = false
|
|
231
233
|
|
|
232
234
|
# PowerShell needs time to create pipe.
|
|
233
235
|
100.times do
|
|
236
|
+
unless ownership_verified
|
|
237
|
+
owner, current_user, is_owner = pipe_owned_by_current_user?(pipe_name)
|
|
238
|
+
if owner.nil?
|
|
239
|
+
sleep 0.1
|
|
240
|
+
next
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
unless is_owner
|
|
244
|
+
raise PipeError, "Unauthorized user '#{current_user}' tried to connect to pipe '#{pipe_name}'. Pipe is owned by '#{owner}'."
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
ownership_verified = true
|
|
248
|
+
end
|
|
249
|
+
|
|
234
250
|
pipe = open("//./pipe/#{pipe_name}", "r+")
|
|
235
251
|
break
|
|
252
|
+
rescue PipeError
|
|
253
|
+
raise
|
|
236
254
|
rescue
|
|
237
255
|
sleep 0.1
|
|
238
256
|
end
|
|
@@ -245,17 +263,30 @@ module Train::Transports
|
|
|
245
263
|
|
|
246
264
|
script = <<-EOF
|
|
247
265
|
$ErrorActionPreference = 'Stop'
|
|
266
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
267
|
+
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
268
|
+
$pipeSecurity = New-Object System.IO.Pipes.PipeSecurity
|
|
269
|
+
$rule = New-Object System.IO.Pipes.PipeAccessRule($user, "FullControl", "Allow")
|
|
270
|
+
$pipeSecurity.AddAccessRule($rule)
|
|
271
|
+
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream('#{pipe_name}', [System.IO.Pipes.PipeDirection]::InOut, 1, [System.IO.Pipes.PipeTransmissionMode]::Byte, [System.IO.Pipes.PipeOptions]::None, 4096, 4096, $pipeSecurity)
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
$pipeServer.WaitForConnection()
|
|
275
|
+
} catch {
|
|
276
|
+
exit 1
|
|
277
|
+
}
|
|
248
278
|
|
|
249
|
-
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream('#{pipe_name}')
|
|
250
279
|
$pipeReader = New-Object System.IO.StreamReader($pipeServer)
|
|
251
280
|
$pipeWriter = New-Object System.IO.StreamWriter($pipeServer)
|
|
252
281
|
|
|
253
|
-
$pipeServer.WaitForConnection()
|
|
254
|
-
|
|
255
282
|
# Create loop to receive and process user commands/scripts
|
|
256
283
|
$clientConnected = $true
|
|
257
284
|
while($clientConnected) {
|
|
258
285
|
$input = $pipeReader.ReadLine()
|
|
286
|
+
if ($input -eq $null) {
|
|
287
|
+
$clientConnected = $false
|
|
288
|
+
break
|
|
289
|
+
}
|
|
259
290
|
$command = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($input))
|
|
260
291
|
|
|
261
292
|
# Execute user command/script and convert result to JSON
|
|
@@ -277,8 +308,14 @@ module Train::Transports
|
|
|
277
308
|
|
|
278
309
|
# Encode JSON in Base64 and write to pipe
|
|
279
310
|
$encodedResult = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($resultJSON))
|
|
280
|
-
|
|
281
|
-
|
|
311
|
+
try {
|
|
312
|
+
$pipeWriter.WriteLine($encodedResult)
|
|
313
|
+
$pipeWriter.Flush()
|
|
314
|
+
} catch [System.IO.IOException] {
|
|
315
|
+
# Pipe was closed by client, exit gracefully
|
|
316
|
+
$clientConnected = $false
|
|
317
|
+
break
|
|
318
|
+
}
|
|
282
319
|
}
|
|
283
320
|
EOF
|
|
284
321
|
|
|
@@ -287,6 +324,29 @@ module Train::Transports
|
|
|
287
324
|
cmd = "#{@powershell_cmd} -NoProfile -ExecutionPolicy bypass -NonInteractive -EncodedCommand #{base64_script}"
|
|
288
325
|
Process.create(command_line: cmd).process_id
|
|
289
326
|
end
|
|
327
|
+
|
|
328
|
+
def current_windows_user
|
|
329
|
+
user = `powershell -Command "[System.Security.Principal.WindowsIdentity]::GetCurrent().Name"`.strip
|
|
330
|
+
if user.nil? || user.empty?
|
|
331
|
+
user = `whoami`.strip
|
|
332
|
+
end
|
|
333
|
+
if user.nil? || user.empty?
|
|
334
|
+
raise "Unable to determine current Windows user"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
user
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Verify pipe ownership before connecting
|
|
341
|
+
def pipe_owned_by_current_user?(pipe_name)
|
|
342
|
+
exists = `powershell -Command "Test-Path \\\\.\\pipe\\#{pipe_name}"`.strip.downcase == "true"
|
|
343
|
+
current_user = current_windows_user
|
|
344
|
+
return [nil, current_user, false] unless exists
|
|
345
|
+
|
|
346
|
+
owner = `powershell -Command "(Get-Acl \\\\.\\pipe\\#{pipe_name}).Owner" 2>&1`.strip
|
|
347
|
+
is_owner = !owner.nil? && !current_user.nil? && owner.casecmp(current_user) == 0
|
|
348
|
+
[owner, current_user, is_owner]
|
|
349
|
+
end
|
|
290
350
|
end
|
|
291
351
|
end
|
|
292
352
|
end
|
|
@@ -364,9 +364,14 @@ class Train::Transports::SSH
|
|
|
364
364
|
channel.on_request("exit-signal") do |_, data|
|
|
365
365
|
exit_status = data.read_long
|
|
366
366
|
end
|
|
367
|
+
|
|
368
|
+
channel.on_close do
|
|
369
|
+
session.channels.each { |_, session_channel| session_channel.close }
|
|
370
|
+
end
|
|
367
371
|
end
|
|
368
372
|
end
|
|
369
|
-
|
|
373
|
+
|
|
374
|
+
session.loop { session.busy? }
|
|
370
375
|
|
|
371
376
|
if timeout && timeoutable?(cmd) && exit_status == GNU_TIMEOUT_EXIT_STATUS
|
|
372
377
|
logger.debug("train ssh command '#{cmd}' reached requested timeout (#{timeout}s)")
|
data/lib/train/version.rb
CHANGED
data/lib/train.rb
CHANGED
|
@@ -26,7 +26,8 @@ module Train
|
|
|
26
26
|
# @return [Hash] map of default options
|
|
27
27
|
def self.options(name)
|
|
28
28
|
cls = load_transport(name)
|
|
29
|
-
|
|
29
|
+
# Merging default_audit_log_options so that they will get listed in the options that are available.
|
|
30
|
+
cls.default_options.merge(cls.default_audit_log_options) unless cls.nil?
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# Load the transport plugin indicated by name. If the plugin is not
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: train-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.16.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chef InSpec Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-04-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: addressable
|
|
@@ -28,23 +28,29 @@ dependencies:
|
|
|
28
28
|
name: ffi
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - "
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 1.16.0
|
|
34
|
+
- - "<"
|
|
32
35
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 1.
|
|
36
|
+
version: '1.18'
|
|
34
37
|
type: :runtime
|
|
35
38
|
prerelease: false
|
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
40
|
requirements:
|
|
38
|
-
- - "
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 1.16.0
|
|
44
|
+
- - "<"
|
|
39
45
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 1.
|
|
46
|
+
version: '1.18'
|
|
41
47
|
- !ruby/object:Gem::Dependency
|
|
42
48
|
name: json
|
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
|
44
50
|
requirements:
|
|
45
51
|
- - ">="
|
|
46
52
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
53
|
+
version: 2.19.2
|
|
48
54
|
- - "<"
|
|
49
55
|
- !ruby/object:Gem::Version
|
|
50
56
|
version: '3.0'
|
|
@@ -54,7 +60,7 @@ dependencies:
|
|
|
54
60
|
requirements:
|
|
55
61
|
- - ">="
|
|
56
62
|
- !ruby/object:Gem::Version
|
|
57
|
-
version:
|
|
63
|
+
version: 2.19.2
|
|
58
64
|
- - "<"
|
|
59
65
|
- !ruby/object:Gem::Version
|
|
60
66
|
version: '3.0'
|
|
@@ -127,6 +133,7 @@ extra_rdoc_files: []
|
|
|
127
133
|
files:
|
|
128
134
|
- LICENSE
|
|
129
135
|
- lib/train.rb
|
|
136
|
+
- lib/train/audit_log.rb
|
|
130
137
|
- lib/train/errors.rb
|
|
131
138
|
- lib/train/extras.rb
|
|
132
139
|
- lib/train/extras/command_wrapper.rb
|
|
@@ -142,6 +149,7 @@ files:
|
|
|
142
149
|
- lib/train/file/remote/unix.rb
|
|
143
150
|
- lib/train/file/remote/windows.rb
|
|
144
151
|
- lib/train/globals.rb
|
|
152
|
+
- lib/train/logger_ext.rb
|
|
145
153
|
- lib/train/options.rb
|
|
146
154
|
- lib/train/platforms.rb
|
|
147
155
|
- lib/train/platforms/common.rb
|
|
@@ -181,14 +189,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
181
189
|
requirements:
|
|
182
190
|
- - ">="
|
|
183
191
|
- !ruby/object:Gem::Version
|
|
184
|
-
version:
|
|
192
|
+
version: 3.1.0
|
|
185
193
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
186
194
|
requirements:
|
|
187
195
|
- - ">="
|
|
188
196
|
- !ruby/object:Gem::Version
|
|
189
197
|
version: '0'
|
|
190
198
|
requirements: []
|
|
191
|
-
rubygems_version: 3.
|
|
199
|
+
rubygems_version: 3.3.27
|
|
192
200
|
signing_key:
|
|
193
201
|
specification_version: 4
|
|
194
202
|
summary: Transport interface to talk to a selected set of backends.
|