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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd9123fc8c77d6d2913633bb544aae171f440d3a0558bacda3136fa62d856661
4
- data.tar.gz: b8e558c2c7fa1479725228f1745ff5ed610f6975babc43cb07bbfb6289f41806
3
+ metadata.gz: 5c7ff43a79457e6d900c1e1e293c1e2ad48c6c555863cb816d64ddd96e02f03f
4
+ data.tar.gz: b42bdd2900ac7cc3c43f0df0467def0d46906a51ebfa64de3344b35e7504ae7a
5
5
  SHA512:
6
- metadata.gz: 0ed3576117bf704f08d0170e3f9c9d08e20da634300f02b986aaa8fb18094d0b027d9943bcc04043d986102e641ed7c09a4ed163ca6b9f8de1488d3063043721
7
- data.tar.gz: deb114924bc9f537bb8a8c4df1f0b48936c8b7e081539109b886b4c26f994a6f96b214d514d2109ab75b526e2328473fb56f323e0746a28f8cf4cfce4df8e334
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
@@ -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
- default_options.each do |field, hm|
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"] =~ regex
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
- stdout = res.stdout
39
- stderr = res.stderr
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
- read_wmic
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
- read_wmic
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 = windows_uuid_from_wmic if uuid.nil?
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
- result = @backend.run_command("wmic csproduct get UUID")
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.split("\r\n")[-1].strip
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 or directories to remote host.
181
+ # Uploads local files to remote host.
166
182
  #
167
- # @param locals [Array<String>] paths to local files or directories
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
- unless file(remote).directory?
173
- raise TransportError, "#{self.class} expects remote directory as second upload parameter"
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
- new_content = File.read(local)
178
- remote_file = File.join(remote, File.basename(local))
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 = new_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
- $pipeWriter.WriteLine($encodedResult)
281
- $pipeWriter.Flush()
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
- session.loop
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
@@ -2,5 +2,5 @@
2
2
  # Author:: Dominik Richter (<dominik.richter@gmail.com>)
3
3
 
4
4
  module Train
5
- VERSION = "3.10.8".freeze
5
+ VERSION = "3.16.3".freeze
6
6
  end
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
- cls.default_options unless cls.nil?
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.10.8
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: 2023-06-23 00:00:00.000000000 Z
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.13.0
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.13.0
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: '1.8'
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: '1.8'
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: '2.7'
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.1.4
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.