train-core 1.4.4
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 +7 -0
- data/CHANGELOG.md +780 -0
- data/Gemfile +36 -0
- data/LICENSE +201 -0
- data/README.md +197 -0
- data/lib/train.rb +158 -0
- data/lib/train/errors.rb +32 -0
- data/lib/train/extras.rb +11 -0
- data/lib/train/extras/command_wrapper.rb +137 -0
- data/lib/train/extras/stat.rb +132 -0
- data/lib/train/file.rb +151 -0
- data/lib/train/file/local.rb +75 -0
- data/lib/train/file/local/unix.rb +96 -0
- data/lib/train/file/local/windows.rb +63 -0
- data/lib/train/file/remote.rb +36 -0
- data/lib/train/file/remote/aix.rb +21 -0
- data/lib/train/file/remote/linux.rb +19 -0
- data/lib/train/file/remote/qnx.rb +41 -0
- data/lib/train/file/remote/unix.rb +106 -0
- data/lib/train/file/remote/windows.rb +94 -0
- data/lib/train/options.rb +80 -0
- data/lib/train/platforms.rb +84 -0
- data/lib/train/platforms/common.rb +34 -0
- data/lib/train/platforms/detect.rb +12 -0
- data/lib/train/platforms/detect/helpers/os_common.rb +145 -0
- data/lib/train/platforms/detect/helpers/os_linux.rb +75 -0
- data/lib/train/platforms/detect/helpers/os_windows.rb +120 -0
- data/lib/train/platforms/detect/scanner.rb +84 -0
- data/lib/train/platforms/detect/specifications/api.rb +15 -0
- data/lib/train/platforms/detect/specifications/os.rb +578 -0
- data/lib/train/platforms/detect/uuid.rb +34 -0
- data/lib/train/platforms/family.rb +26 -0
- data/lib/train/platforms/platform.rb +101 -0
- data/lib/train/plugins.rb +40 -0
- data/lib/train/plugins/base_connection.rb +169 -0
- data/lib/train/plugins/transport.rb +49 -0
- data/lib/train/transports/local.rb +232 -0
- data/lib/train/version.rb +7 -0
- data/train-core.gemspec +27 -0
- metadata +116 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'digest/sha1'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Train::Platforms::Detect
|
8
|
+
class UUID
|
9
|
+
include Train::Platforms::Detect::Helpers::OSCommon
|
10
|
+
|
11
|
+
def initialize(platform)
|
12
|
+
@platform = platform
|
13
|
+
@backend = @platform.backend
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_or_create_uuid
|
17
|
+
# for api transports uuid is defined on the connection
|
18
|
+
if defined?(@backend.unique_identifier)
|
19
|
+
uuid_from_string(@backend.unique_identifier)
|
20
|
+
elsif @platform.unix?
|
21
|
+
unix_uuid
|
22
|
+
elsif @platform.windows?
|
23
|
+
windows_uuid
|
24
|
+
else
|
25
|
+
if @platform[:uuid_command]
|
26
|
+
result = @backend.run_command(@platform[:uuid_command])
|
27
|
+
return uuid_from_string(result.stdout.chomp) if result.exit_status.zero? && !result.stdout.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
raise 'Could not find platform uuid! Please set a uuid_command for your platform.'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Train::Platforms
|
4
|
+
class Family
|
5
|
+
include Train::Platforms::Common
|
6
|
+
attr_accessor :children, :condition, :families, :name
|
7
|
+
|
8
|
+
def initialize(name, condition)
|
9
|
+
@name = name
|
10
|
+
@condition = condition
|
11
|
+
@families = {}
|
12
|
+
@children = {}
|
13
|
+
@detect = nil
|
14
|
+
@title = "#{name.to_s.capitalize} Family"
|
15
|
+
|
16
|
+
# add itself to the families list
|
17
|
+
Train::Platforms.families[@name.to_s] = self
|
18
|
+
end
|
19
|
+
|
20
|
+
def title(title = nil)
|
21
|
+
return @title if title.nil?
|
22
|
+
@title = title
|
23
|
+
self
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Train::Platforms
|
4
|
+
class Platform
|
5
|
+
include Train::Platforms::Common
|
6
|
+
attr_accessor :backend, :condition, :families, :family_hierarchy, :platform
|
7
|
+
|
8
|
+
def initialize(name, condition = {})
|
9
|
+
@name = name
|
10
|
+
@condition = condition
|
11
|
+
@families = {}
|
12
|
+
@family_hierarchy = []
|
13
|
+
@platform = {}
|
14
|
+
@detect = nil
|
15
|
+
@title = name.to_s.capitalize
|
16
|
+
|
17
|
+
# add itself to the platform list
|
18
|
+
Train::Platforms.list[name] = self
|
19
|
+
end
|
20
|
+
|
21
|
+
def direct_families
|
22
|
+
@families.collect { |k, _v| k.name }
|
23
|
+
end
|
24
|
+
|
25
|
+
def family
|
26
|
+
@platform[:family] || @family_hierarchy[0]
|
27
|
+
end
|
28
|
+
|
29
|
+
def name
|
30
|
+
# Override here incase a updated name was set
|
31
|
+
# during the detect logic
|
32
|
+
clean_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def clean_name(force: false)
|
36
|
+
@cleaned_name = nil if force
|
37
|
+
@cleaned_name ||= begin
|
38
|
+
name = (@platform[:name] || @name)
|
39
|
+
name.downcase!.tr!(' ', '_') if name =~ /[A-Z ]/
|
40
|
+
name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def uuid
|
45
|
+
@uuid ||= Train::Platforms::Detect::UUID.new(self).find_or_create_uuid.downcase
|
46
|
+
end
|
47
|
+
|
48
|
+
# This is for backwords compatability with
|
49
|
+
# the current inspec os resource.
|
50
|
+
def[](name)
|
51
|
+
if respond_to?(name)
|
52
|
+
send(name)
|
53
|
+
else
|
54
|
+
'unknown'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def title(title = nil)
|
59
|
+
return @title if title.nil?
|
60
|
+
@title = title
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_hash
|
65
|
+
@platform
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add generic family? and platform methods to an existing platform
|
69
|
+
#
|
70
|
+
# This is done later to add any custom
|
71
|
+
# families/properties that were created
|
72
|
+
def add_platform_methods
|
73
|
+
# Redo clean name if there is a detect override
|
74
|
+
clean_name(force: true) unless @platform[:name].nil?
|
75
|
+
|
76
|
+
# Add in family methods
|
77
|
+
family_list = Train::Platforms.families
|
78
|
+
family_list.each_value do |k|
|
79
|
+
next if respond_to?(k.name + '?')
|
80
|
+
define_singleton_method(k.name + '?') do
|
81
|
+
family_hierarchy.include?(k.name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Helper methods for direct platform info
|
86
|
+
@platform.each_key do |m|
|
87
|
+
next if respond_to?(m)
|
88
|
+
define_singleton_method(m) do
|
89
|
+
@platform[m]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Create method for name if its not already true
|
94
|
+
m = name + '?'
|
95
|
+
return if respond_to?(m)
|
96
|
+
define_singleton_method(m) do
|
97
|
+
true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
|
+
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
5
|
+
|
6
|
+
require 'train/errors'
|
7
|
+
|
8
|
+
module Train
|
9
|
+
class Plugins
|
10
|
+
require 'train/plugins/transport'
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# Retrieve the current plugin registry, containing all plugin names
|
14
|
+
# and their transport handlers.
|
15
|
+
#
|
16
|
+
# @return [Hash] map with plugin names and plugins
|
17
|
+
def registry
|
18
|
+
@registry ||= {}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create a new plugin by inheriting from the class returned by this method.
|
24
|
+
# Create a versioned plugin by providing the transport layer plugin version
|
25
|
+
# to this method. It will then select the correct class to inherit from.
|
26
|
+
#
|
27
|
+
# The plugin version determins what methods will be available to your plugin.
|
28
|
+
#
|
29
|
+
# @param [Int] version = 1 the plugin version to use
|
30
|
+
# @return [Transport] the versioned transport base class
|
31
|
+
def self.plugin(version = 1)
|
32
|
+
if version != 1
|
33
|
+
fail ClientError,
|
34
|
+
'Only understand train plugin version 1. You are trying to '\
|
35
|
+
"initialize a train plugin #{version}, which is not supported "\
|
36
|
+
'in the current release of train.'
|
37
|
+
end
|
38
|
+
::Train::Plugins::Transport
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'train/errors'
|
4
|
+
require 'train/extras'
|
5
|
+
require 'train/file'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
class Train::Plugins::Transport
|
9
|
+
# A Connection instance can be generated and re-generated, given new
|
10
|
+
# connection details such as connection port, hostname, credentials, etc.
|
11
|
+
# This object is responsible for carrying out the actions on the remote
|
12
|
+
# host such as executing commands, transferring files, etc.
|
13
|
+
#
|
14
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
15
|
+
class BaseConnection
|
16
|
+
include Train::Extras
|
17
|
+
|
18
|
+
# Create a new Connection instance.
|
19
|
+
#
|
20
|
+
# @param options [Hash] connection options
|
21
|
+
# @yield [self] yields itself for block-style invocation
|
22
|
+
def initialize(options = nil)
|
23
|
+
@options = options || {}
|
24
|
+
@logger = @options.delete(:logger) || Logger.new(STDOUT)
|
25
|
+
Train::Platforms::Detect::Specifications::OS.load
|
26
|
+
Train::Platforms::Detect::Specifications::Api.load
|
27
|
+
|
28
|
+
# default caching options
|
29
|
+
@cache_enabled = {
|
30
|
+
file: true,
|
31
|
+
command: false,
|
32
|
+
}
|
33
|
+
|
34
|
+
@cache = {}
|
35
|
+
@cache_enabled.each_key do |type|
|
36
|
+
clear_cache(type)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache_enabled?(type)
|
41
|
+
@cache_enabled[type.to_sym]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Enable caching types for Train. Currently we support
|
45
|
+
# :file and :command types
|
46
|
+
def enable_cache(type)
|
47
|
+
fail Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)
|
48
|
+
@cache_enabled[type.to_sym] = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def disable_cache(type)
|
52
|
+
fail Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)
|
53
|
+
@cache_enabled[type.to_sym] = false
|
54
|
+
clear_cache(type.to_sym)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Closes the session connection, if it is still active.
|
58
|
+
def close
|
59
|
+
# this method may be left unimplemented if that is applicable
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_json
|
63
|
+
{
|
64
|
+
'files' => Hash[@cache[:file].map { |x, y| [x, y.to_json] }],
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_json(j)
|
69
|
+
require 'train/transports/mock'
|
70
|
+
j['files'].each do |path, jf|
|
71
|
+
@cache[:file][path] = Train::Transports::Mock::Connection::File.from_json(jf)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Is this a local transport?
|
76
|
+
def local?
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
def direct_platform(name, platform_details = nil)
|
81
|
+
plat = Train::Platforms.name(name)
|
82
|
+
plat.backend = self
|
83
|
+
plat.platform = platform_details unless platform_details.nil?
|
84
|
+
plat.family_hierarchy = family_hierarchy(plat).flatten
|
85
|
+
plat.add_platform_methods
|
86
|
+
plat
|
87
|
+
end
|
88
|
+
|
89
|
+
def family_hierarchy(plat)
|
90
|
+
plat.families.each_with_object([]) do |(k, _v), memo|
|
91
|
+
memo << k.name
|
92
|
+
memo << family_hierarchy(k) unless k.families.empty?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get information on the operating system which this transport connects to.
|
97
|
+
#
|
98
|
+
# @return [Platform] system information
|
99
|
+
def platform
|
100
|
+
@platform ||= Train::Platforms::Detect.scan(self)
|
101
|
+
end
|
102
|
+
# we need to keep os as a method for backwards compatibility with inspec
|
103
|
+
alias os platform
|
104
|
+
|
105
|
+
# This is the main command call for all connections. This will call the private
|
106
|
+
# run_command_via_connection on the connection with optional caching
|
107
|
+
def run_command(cmd)
|
108
|
+
return run_command_via_connection(cmd) unless cache_enabled?(:command)
|
109
|
+
|
110
|
+
@cache[:command][cmd] ||= run_command_via_connection(cmd)
|
111
|
+
end
|
112
|
+
|
113
|
+
# This is the main file call for all connections. This will call the private
|
114
|
+
# file_via_connection on the connection with optional caching
|
115
|
+
def file(path, *args)
|
116
|
+
return file_via_connection(path, *args) unless cache_enabled?(:file)
|
117
|
+
|
118
|
+
@cache[:file][path] ||= file_via_connection(path, *args)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Builds a LoginCommand which can be used to open an interactive
|
122
|
+
# session on the remote host.
|
123
|
+
#
|
124
|
+
# @return [LoginCommand] array of command line tokens
|
125
|
+
def login_command
|
126
|
+
fail NotImplementedError, "#{self.class} does not implement #login_command()"
|
127
|
+
end
|
128
|
+
|
129
|
+
# Block and return only when the remote host is prepared and ready to
|
130
|
+
# execute command and upload files. The semantics and details will
|
131
|
+
# vary by implementation, but a round trip through the hosted
|
132
|
+
# service is preferred to simply waiting on a socket to become
|
133
|
+
# available.
|
134
|
+
def wait_until_ready
|
135
|
+
# this method may be left unimplemented if that is applicablelog
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
# Execute a command using this connection.
|
141
|
+
#
|
142
|
+
# @param command [String] command string to execute
|
143
|
+
# @return [CommandResult] contains the result of running the command
|
144
|
+
def run_command_via_connection(_command)
|
145
|
+
fail NotImplementedError, "#{self.class} does not implement #run_command_via_connection()"
|
146
|
+
end
|
147
|
+
|
148
|
+
# Interact with files on the target. Read, write, and get metadata
|
149
|
+
# from files via the transport.
|
150
|
+
#
|
151
|
+
# @param [String] path which is being inspected
|
152
|
+
# @return [FileCommon] file object that allows for interaction
|
153
|
+
def file_via_connection(_path, *_args)
|
154
|
+
fail NotImplementedError, "#{self.class} does not implement #file_via_connection(...)"
|
155
|
+
end
|
156
|
+
|
157
|
+
def clear_cache(type)
|
158
|
+
@cache[type.to_sym] = {}
|
159
|
+
end
|
160
|
+
|
161
|
+
# @return [Logger] logger for reporting information
|
162
|
+
# @api private
|
163
|
+
attr_reader :logger
|
164
|
+
|
165
|
+
# @return [Hash] connection options
|
166
|
+
# @api private
|
167
|
+
attr_reader :options
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
|
+
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
5
|
+
|
6
|
+
require 'logger'
|
7
|
+
require 'train/errors'
|
8
|
+
require 'train/extras'
|
9
|
+
require 'train/options'
|
10
|
+
|
11
|
+
class Train::Plugins
|
12
|
+
class Transport
|
13
|
+
include Train::Extras
|
14
|
+
Train::Options.attach(self)
|
15
|
+
|
16
|
+
require 'train/plugins/base_connection'
|
17
|
+
|
18
|
+
# Initialize a new Transport object
|
19
|
+
#
|
20
|
+
# @param [Hash] config = nil the configuration for this transport
|
21
|
+
# @return [Transport] the transport object
|
22
|
+
def initialize(options = {})
|
23
|
+
@options = merge_options({}, options || {})
|
24
|
+
@logger = @options[:logger] || Logger.new(STDOUT)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create a connection to the target. Options may be provided
|
28
|
+
# for additional configuration.
|
29
|
+
#
|
30
|
+
# @param [Hash] _options = nil provide optional configuration params
|
31
|
+
# @return [Connection] the connection for this configuration
|
32
|
+
def connection(_options = nil)
|
33
|
+
fail Train::ClientError, "#{self.class} does not implement #connection()"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Register the inheriting class with as a train plugin using the
|
37
|
+
# provided name.
|
38
|
+
#
|
39
|
+
# @param [String] name of the plugin, by which it will be found
|
40
|
+
def self.name(name)
|
41
|
+
Train::Plugins.registry[name] = self
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# @return [Logger] logger for reporting information
|
47
|
+
attr_reader :logger
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# author: Dominik Richter
|
4
|
+
# author: Christoph Hartmann
|
5
|
+
|
6
|
+
require 'train/plugins'
|
7
|
+
require 'train/errors'
|
8
|
+
require 'mixlib/shellout'
|
9
|
+
|
10
|
+
module Train::Transports
|
11
|
+
class Local < Train.plugin(1)
|
12
|
+
name 'local'
|
13
|
+
|
14
|
+
class PipeError < Train::TransportError; end
|
15
|
+
|
16
|
+
def connection(_ = nil)
|
17
|
+
@connection ||= Connection.new(@options)
|
18
|
+
end
|
19
|
+
|
20
|
+
class Connection < BaseConnection
|
21
|
+
def initialize(options)
|
22
|
+
super(options)
|
23
|
+
|
24
|
+
@runner = if options[:command_runner]
|
25
|
+
force_runner(options[:command_runner])
|
26
|
+
else
|
27
|
+
select_runner(options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def local?
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
def login_command
|
36
|
+
nil # none, open your shell
|
37
|
+
end
|
38
|
+
|
39
|
+
def uri
|
40
|
+
'local://'
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def select_runner(options)
|
46
|
+
if os.windows?
|
47
|
+
# Force a 64 bit poweshell if needed
|
48
|
+
if RUBY_PLATFORM == 'i386-mingw32' && os.arch == 'x86_64'
|
49
|
+
powershell_cmd = "#{ENV['SystemRoot']}\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe"
|
50
|
+
else
|
51
|
+
powershell_cmd = 'powershell'
|
52
|
+
end
|
53
|
+
|
54
|
+
# Attempt to use a named pipe but fallback to ShellOut if that fails
|
55
|
+
begin
|
56
|
+
WindowsPipeRunner.new(powershell_cmd)
|
57
|
+
rescue PipeError
|
58
|
+
WindowsShellRunner.new(powershell_cmd)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
GenericRunner.new(self, options)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def force_runner(command_runner)
|
66
|
+
case command_runner
|
67
|
+
when :generic
|
68
|
+
GenericRunner.new(self, options)
|
69
|
+
when :windows_pipe
|
70
|
+
WindowsPipeRunner.new
|
71
|
+
when :windows_shell
|
72
|
+
WindowsShellRunner.new
|
73
|
+
else
|
74
|
+
fail "Runner type `#{command_runner}` not supported"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def run_command_via_connection(cmd)
|
79
|
+
# Use the runner if it is available
|
80
|
+
return @runner.run_command(cmd) if defined?(@runner)
|
81
|
+
|
82
|
+
# If we don't have a runner, such as at the beginning of setting up the
|
83
|
+
# transport and performing the first few steps of OS detection, fall
|
84
|
+
# back to shelling out.
|
85
|
+
res = Mixlib::ShellOut.new(cmd)
|
86
|
+
res.run_command
|
87
|
+
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
88
|
+
rescue Errno::ENOENT => _
|
89
|
+
CommandResult.new('', '', 1)
|
90
|
+
end
|
91
|
+
|
92
|
+
def file_via_connection(path)
|
93
|
+
if os.windows?
|
94
|
+
Train::File::Local::Windows.new(self, path)
|
95
|
+
else
|
96
|
+
Train::File::Local::Unix.new(self, path)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class GenericRunner
|
101
|
+
include_options Train::Extras::CommandWrapper
|
102
|
+
|
103
|
+
def initialize(connection, options)
|
104
|
+
@cmd_wrapper = Local::CommandWrapper.load(connection, options)
|
105
|
+
end
|
106
|
+
|
107
|
+
def run_command(cmd)
|
108
|
+
if defined?(@cmd_wrapper) && !@cmd_wrapper.nil?
|
109
|
+
cmd = @cmd_wrapper.run(cmd)
|
110
|
+
end
|
111
|
+
|
112
|
+
res = Mixlib::ShellOut.new(cmd)
|
113
|
+
res.run_command
|
114
|
+
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class WindowsShellRunner
|
119
|
+
require 'json'
|
120
|
+
require 'base64'
|
121
|
+
|
122
|
+
def initialize(powershell_cmd = 'powershell')
|
123
|
+
@powershell_cmd = powershell_cmd
|
124
|
+
end
|
125
|
+
|
126
|
+
def run_command(script)
|
127
|
+
# Prevent progress stream from leaking into stderr
|
128
|
+
script = "$ProgressPreference='SilentlyContinue';" + script
|
129
|
+
|
130
|
+
# Encode script so PowerShell can use it
|
131
|
+
script = script.encode('UTF-16LE', 'UTF-8')
|
132
|
+
base64_script = Base64.strict_encode64(script)
|
133
|
+
|
134
|
+
cmd = "#{@powershell_cmd} -NoProfile -EncodedCommand #{base64_script}"
|
135
|
+
|
136
|
+
res = Mixlib::ShellOut.new(cmd)
|
137
|
+
res.run_command
|
138
|
+
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class WindowsPipeRunner
|
143
|
+
require 'json'
|
144
|
+
require 'base64'
|
145
|
+
require 'securerandom'
|
146
|
+
|
147
|
+
def initialize(powershell_cmd = 'powershell')
|
148
|
+
@powershell_cmd = powershell_cmd
|
149
|
+
@pipe = acquire_pipe
|
150
|
+
fail PipeError if @pipe.nil?
|
151
|
+
end
|
152
|
+
|
153
|
+
def run_command(cmd)
|
154
|
+
script = "$ProgressPreference='SilentlyContinue';" + cmd
|
155
|
+
encoded_script = Base64.strict_encode64(script)
|
156
|
+
@pipe.puts(encoded_script)
|
157
|
+
@pipe.flush
|
158
|
+
res = OpenStruct.new(JSON.parse(Base64.decode64(@pipe.readline)))
|
159
|
+
Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def acquire_pipe
|
165
|
+
pipe_name = "inspec_#{SecureRandom.hex}"
|
166
|
+
|
167
|
+
start_pipe_server(pipe_name)
|
168
|
+
|
169
|
+
pipe = nil
|
170
|
+
|
171
|
+
# PowerShell needs time to create pipe.
|
172
|
+
100.times do
|
173
|
+
begin
|
174
|
+
pipe = open("//./pipe/#{pipe_name}", 'r+')
|
175
|
+
break
|
176
|
+
rescue
|
177
|
+
sleep 0.1
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
pipe
|
182
|
+
end
|
183
|
+
|
184
|
+
def start_pipe_server(pipe_name)
|
185
|
+
require 'win32/process'
|
186
|
+
|
187
|
+
script = <<-EOF
|
188
|
+
$ErrorActionPreference = 'Stop'
|
189
|
+
|
190
|
+
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream('#{pipe_name}')
|
191
|
+
$pipeReader = New-Object System.IO.StreamReader($pipeServer)
|
192
|
+
$pipeWriter = New-Object System.IO.StreamWriter($pipeServer)
|
193
|
+
|
194
|
+
$pipeServer.WaitForConnection()
|
195
|
+
|
196
|
+
# Create loop to receive and process user commands/scripts
|
197
|
+
$clientConnected = $true
|
198
|
+
while($clientConnected) {
|
199
|
+
$input = $pipeReader.ReadLine()
|
200
|
+
$command = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($input))
|
201
|
+
|
202
|
+
# Execute user command/script and convert result to JSON
|
203
|
+
$scriptBlock = $ExecutionContext.InvokeCommand.NewScriptBlock($command)
|
204
|
+
try {
|
205
|
+
$stdout = & $scriptBlock | Out-String
|
206
|
+
$result = @{ 'stdout' = $stdout ; 'stderr' = ''; 'exitstatus' = 0 }
|
207
|
+
} catch {
|
208
|
+
$stderr = $_ | Out-String
|
209
|
+
$result = @{ 'stdout' = ''; 'stderr' = $_; 'exitstatus' = 1 }
|
210
|
+
}
|
211
|
+
$resultJSON = $result | ConvertTo-JSON
|
212
|
+
|
213
|
+
# Encode JSON in Base64 and write to pipe
|
214
|
+
$encodedResult = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($resultJSON))
|
215
|
+
$pipeWriter.WriteLine($encodedResult)
|
216
|
+
$pipeWriter.Flush()
|
217
|
+
}
|
218
|
+
EOF
|
219
|
+
|
220
|
+
utf8_script = script.encode('UTF-16LE', 'UTF-8')
|
221
|
+
base64_script = Base64.strict_encode64(utf8_script)
|
222
|
+
cmd = "#{@powershell_cmd} -NoProfile -ExecutionPolicy bypass -NonInteractive -EncodedCommand #{base64_script}"
|
223
|
+
|
224
|
+
server_pid = Process.create(command_line: cmd).process_id
|
225
|
+
|
226
|
+
# Ensure process is killed when the Train process exits
|
227
|
+
at_exit { Process.kill('KILL', server_pid) }
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|