train-core 1.4.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|