train-juniper 0.5.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/.env.example +29 -0
- data/CHANGELOG.md +60 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +155 -0
- data/LICENSE.md +9 -0
- data/NOTICE.md +9 -0
- data/README.md +399 -0
- data/Rakefile +94 -0
- data/SECURITY.md +79 -0
- data/lib/train-juniper/connection.rb +408 -0
- data/lib/train-juniper/platform.rb +191 -0
- data/lib/train-juniper/transport.rb +55 -0
- data/lib/train-juniper/version.rb +12 -0
- data/lib/train-juniper.rb +23 -0
- data/train-juniper.gemspec +72 -0
- metadata +115 -0
data/Rakefile
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A Rakefile defines tasks to help maintain your project.
|
4
|
+
# Rake provides several task templates that are useful.
|
5
|
+
|
6
|
+
#------------------------------------------------------------------#
|
7
|
+
# Test Runner Tasks
|
8
|
+
#------------------------------------------------------------------#
|
9
|
+
|
10
|
+
# This task template will make a task named 'test', and run
|
11
|
+
# the tests that it finds.
|
12
|
+
require 'rake/testtask'
|
13
|
+
|
14
|
+
Rake::TestTask.new do |t|
|
15
|
+
t.libs.push 'lib'
|
16
|
+
t.test_files = FileList[
|
17
|
+
'test/unit/*_test.rb',
|
18
|
+
'test/integration/*_test.rb',
|
19
|
+
'test/functional/*_test.rb',
|
20
|
+
'test/security/*_test.rb'
|
21
|
+
]
|
22
|
+
t.verbose = true
|
23
|
+
# Ideally, we'd run tests with warnings enabled,
|
24
|
+
# but the dependent gems have many warnings. As this
|
25
|
+
# is an example, let's disable them so the testing
|
26
|
+
# experience is cleaner.
|
27
|
+
t.warning = false
|
28
|
+
end
|
29
|
+
|
30
|
+
#------------------------------------------------------------------#
|
31
|
+
# Code Style Tasks
|
32
|
+
#------------------------------------------------------------------#
|
33
|
+
require 'rubocop/rake_task'
|
34
|
+
|
35
|
+
RuboCop::RakeTask.new(:lint) do |t|
|
36
|
+
# Choices of rubocop rules to enforce are deeply personal.
|
37
|
+
# Here, we set things up so that your plugin will use the Bundler-installed
|
38
|
+
# train gem's copy of the Train project's rubocop.yml file (which
|
39
|
+
# is indeed packaged with the train gem).
|
40
|
+
require 'train/globals'
|
41
|
+
train_rubocop_yml = File.join(Train.src_root, '.rubocop.yml')
|
42
|
+
|
43
|
+
t.options = ['--display-cop-names', '--config', train_rubocop_yml]
|
44
|
+
end
|
45
|
+
|
46
|
+
#------------------------------------------------------------------#
|
47
|
+
# Security Tasks
|
48
|
+
#------------------------------------------------------------------#
|
49
|
+
|
50
|
+
desc 'Run security tests'
|
51
|
+
task 'test:security' do
|
52
|
+
ruby '-Ilib:test test/security/security_test.rb'
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'Run dependency vulnerability scan'
|
56
|
+
task 'security:dependencies' do
|
57
|
+
puts 'Running bundler-audit dependency scan...'
|
58
|
+
system('bundle exec bundle-audit update') or puts 'Failed to update vulnerability database'
|
59
|
+
system('bundle exec bundle-audit check') or abort('Vulnerable dependencies found')
|
60
|
+
end
|
61
|
+
|
62
|
+
desc 'Run secrets scanning'
|
63
|
+
task 'security:secrets' do
|
64
|
+
puts 'Running TruffleHog secrets scan...'
|
65
|
+
if system('which trufflehog > /dev/null 2>&1')
|
66
|
+
system('trufflehog filesystem --config=.trufflehog.yml --no-verification --no-update .') or abort('Secrets detected')
|
67
|
+
else
|
68
|
+
puts 'TruffleHog not installed. Install with: brew install trufflehog'
|
69
|
+
abort('TruffleHog required for secrets scanning')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
desc 'Run Brakeman security vulnerability scan'
|
74
|
+
task 'security:brakeman' do
|
75
|
+
puts 'Running Brakeman security scan...'
|
76
|
+
system('bundle exec brakeman --exit-on-warn --quiet --force') or abort('Security vulnerabilities found')
|
77
|
+
end
|
78
|
+
|
79
|
+
desc 'Run comprehensive security scan'
|
80
|
+
task 'security:scan' do
|
81
|
+
puts 'Running comprehensive security scan...'
|
82
|
+
system('ruby security/security_scan.rb') or abort('Security scan failed')
|
83
|
+
end
|
84
|
+
|
85
|
+
desc 'Run all security checks'
|
86
|
+
task security: %w[security:dependencies security:brakeman test:security]
|
87
|
+
|
88
|
+
desc 'Run all tests including security'
|
89
|
+
task 'test:all' => %w[test security]
|
90
|
+
|
91
|
+
#------------------------------------------------------------------#
|
92
|
+
# Load Additional Tasks
|
93
|
+
#------------------------------------------------------------------#
|
94
|
+
Dir['tasks/*.rake'].each { |f| load f }
|
data/SECURITY.md
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# Security Policy
|
2
|
+
|
3
|
+
## Reporting Security Issues
|
4
|
+
|
5
|
+
The MITRE SAF team takes security seriously. If you discover a security vulnerability in the Train-Juniper plugin, please report it responsibly.
|
6
|
+
|
7
|
+
### Contact Information
|
8
|
+
|
9
|
+
- **Email**: [saf-security@mitre.org](mailto:saf-security@mitre.org)
|
10
|
+
- **GitHub**: Use the [Security tab](https://github.com/mitre/train-juniper/security) to report vulnerabilities privately
|
11
|
+
|
12
|
+
### What to Include
|
13
|
+
|
14
|
+
When reporting security issues, please provide:
|
15
|
+
|
16
|
+
1. **Description** of the vulnerability
|
17
|
+
2. **Steps to reproduce** the issue
|
18
|
+
3. **Potential impact** assessment
|
19
|
+
4. **Suggested fix** (if you have one)
|
20
|
+
|
21
|
+
### Response Timeline
|
22
|
+
|
23
|
+
- **Acknowledgment**: Within 48 hours
|
24
|
+
- **Initial Assessment**: Within 7 days
|
25
|
+
- **Fix Timeline**: Varies by severity
|
26
|
+
|
27
|
+
## Security Best Practices
|
28
|
+
|
29
|
+
### For Users
|
30
|
+
|
31
|
+
- **Keep Updated**: Use the latest version of the plugin
|
32
|
+
- **Secure Credentials**: Never commit passwords or SSH keys to version control
|
33
|
+
- **Use SSH Keys**: Prefer SSH key authentication over passwords
|
34
|
+
- **Network Security**: Use VPNs and secure networks when connecting to network devices
|
35
|
+
|
36
|
+
### For Contributors
|
37
|
+
|
38
|
+
- **Dependency Scanning**: Run `bundle audit` before submitting PRs
|
39
|
+
- **Credential Handling**: Never log or expose credentials in code
|
40
|
+
- **Input Validation**: Sanitize all user inputs
|
41
|
+
- **Test Security**: Include security tests for new features
|
42
|
+
|
43
|
+
## Supported Versions
|
44
|
+
|
45
|
+
| Version | Supported |
|
46
|
+
|---------|-----------|
|
47
|
+
| 0.1.x | ✅ Yes |
|
48
|
+
|
49
|
+
## Security Testing
|
50
|
+
|
51
|
+
The plugin includes comprehensive security testing:
|
52
|
+
|
53
|
+
```bash
|
54
|
+
# Run security test suite
|
55
|
+
bundle exec ruby test/security/security_test.rb
|
56
|
+
|
57
|
+
# Check for vulnerable dependencies
|
58
|
+
bundle exec bundle-audit check
|
59
|
+
|
60
|
+
# Scan for potential security issues
|
61
|
+
bundle exec brakeman --no-pager
|
62
|
+
```
|
63
|
+
|
64
|
+
## Known Security Considerations
|
65
|
+
|
66
|
+
### Network Device Access
|
67
|
+
- Train-Juniper requires SSH access to network infrastructure
|
68
|
+
- Ensure proper network segmentation and access controls
|
69
|
+
- Use dedicated service accounts with minimal required privileges
|
70
|
+
|
71
|
+
### Credential Management
|
72
|
+
- Plugin supports environment variables for credential management
|
73
|
+
- Consider using secrets management systems in production
|
74
|
+
- Rotate credentials regularly
|
75
|
+
|
76
|
+
### Logging and Debugging
|
77
|
+
- Debug mode may log sensitive command outputs
|
78
|
+
- Review log files for credential exposure
|
79
|
+
- Use `-l debug` sparingly in production environments
|
@@ -0,0 +1,408 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Connection definition for Juniper Train plugin.
|
4
|
+
|
5
|
+
# This plugin provides SSH connectivity to Juniper network devices,
|
6
|
+
# enabling InSpec to connect to and inspect Juniper routers and switches.
|
7
|
+
# Key capabilities:
|
8
|
+
# * SSH authentication to Juniper devices
|
9
|
+
# * Platform detection for JunOS devices
|
10
|
+
# * Command execution via SSH with prompt handling
|
11
|
+
# * File operations for configuration inspection
|
12
|
+
|
13
|
+
# Base Train transport functionality
|
14
|
+
require 'train'
|
15
|
+
require 'logger'
|
16
|
+
|
17
|
+
# Juniper-specific platform detection
|
18
|
+
require 'train-juniper/platform'
|
19
|
+
|
20
|
+
# Using Train's SSH transport for connectivity
|
21
|
+
|
22
|
+
module TrainPlugins
|
23
|
+
module Juniper
|
24
|
+
# Main connection class for Juniper devices
|
25
|
+
class Connection < Train::Plugins::Transport::BaseConnection
|
26
|
+
# Include Juniper-specific platform detection
|
27
|
+
include TrainPlugins::Juniper::Platform
|
28
|
+
|
29
|
+
attr_reader :ssh_session
|
30
|
+
|
31
|
+
def initialize(options)
|
32
|
+
# Configure SSH connection options for Juniper devices
|
33
|
+
# Support environment variables for authentication (following train-vsphere pattern)
|
34
|
+
@options = options.dup
|
35
|
+
@options[:host] ||= ENV.fetch('JUNIPER_HOST', nil)
|
36
|
+
@options[:user] ||= ENV.fetch('JUNIPER_USER', nil)
|
37
|
+
@options[:password] ||= ENV.fetch('JUNIPER_PASSWORD', nil)
|
38
|
+
@options[:port] ||= ENV['JUNIPER_PORT']&.to_i || 22
|
39
|
+
@options[:timeout] ||= ENV['JUNIPER_TIMEOUT']&.to_i || 30
|
40
|
+
|
41
|
+
# Proxy/bastion environment variables (Train standard)
|
42
|
+
# Only set from environment if not explicitly provided
|
43
|
+
@options[:bastion_host] = @options[:bastion_host] || ENV.fetch('JUNIPER_BASTION_HOST', nil)
|
44
|
+
@options[:bastion_user] = @options[:bastion_user] || ENV['JUNIPER_BASTION_USER'] || 'root'
|
45
|
+
@options[:bastion_port] = @options[:bastion_port] || ENV['JUNIPER_BASTION_PORT']&.to_i || 22
|
46
|
+
@options[:proxy_command] = @options[:proxy_command] || ENV.fetch('JUNIPER_PROXY_COMMAND', nil)
|
47
|
+
|
48
|
+
@options[:keepalive] = true
|
49
|
+
@options[:keepalive_interval] = 60
|
50
|
+
|
51
|
+
# Setup logger
|
52
|
+
@logger = @options[:logger] || Logger.new(STDOUT, level: Logger::WARN)
|
53
|
+
|
54
|
+
# JunOS CLI prompt patterns
|
55
|
+
@cli_prompt = /[%>$#]\s*$/
|
56
|
+
@config_prompt = /[%#]\s*$/
|
57
|
+
|
58
|
+
# Log connection info without exposing credentials
|
59
|
+
safe_options = @options.except(:password, :proxy_command, :key_files)
|
60
|
+
@logger.debug("Juniper connection initialized with options: #{safe_options.inspect}")
|
61
|
+
@logger.debug("Environment: JUNIPER_BASTION_USER=#{ENV.fetch('JUNIPER_BASTION_USER',
|
62
|
+
nil)} -> bastion_user=#{@options[:bastion_user]}")
|
63
|
+
|
64
|
+
# Validate proxy configuration early (Train standard)
|
65
|
+
validate_proxy_options
|
66
|
+
|
67
|
+
super(@options)
|
68
|
+
|
69
|
+
# Establish SSH connection to Juniper device (unless in mock mode)
|
70
|
+
@logger.debug('Attempting to connect to Juniper device...')
|
71
|
+
connect unless @options[:mock]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Secure string representation (never expose credentials)
|
75
|
+
def to_s
|
76
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} @host=#{@options[:host]} @user=#{@options[:user]}>"
|
77
|
+
end
|
78
|
+
|
79
|
+
def inspect
|
80
|
+
to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
# File operations for Juniper configuration files
|
84
|
+
# Supports reading configuration files and operational data
|
85
|
+
def file_via_connection(path)
|
86
|
+
# For Juniper devices, "files" are typically configuration sections
|
87
|
+
# or operational command outputs rather than traditional filesystem paths
|
88
|
+
JuniperFile.new(self, path)
|
89
|
+
end
|
90
|
+
|
91
|
+
# File transfer operations (following network device pattern)
|
92
|
+
# Network devices don't support traditional file upload/download
|
93
|
+
# Use run_command() for configuration management instead
|
94
|
+
def upload(locals, remote)
|
95
|
+
raise NotImplementedError, "#{self.class} does not implement #upload() - network devices use command-based configuration"
|
96
|
+
end
|
97
|
+
|
98
|
+
def download(remotes, local)
|
99
|
+
raise NotImplementedError, "#{self.class} does not implement #download() - use run_command() to retrieve configuration data"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Execute commands on Juniper device via SSH
|
103
|
+
def run_command_via_connection(cmd)
|
104
|
+
return mock_command_result(cmd) if @options[:mock]
|
105
|
+
|
106
|
+
begin
|
107
|
+
# Ensure we're connected
|
108
|
+
connect unless connected?
|
109
|
+
|
110
|
+
@logger.debug("Executing command: #{cmd}")
|
111
|
+
|
112
|
+
# Execute command via SSH session
|
113
|
+
output = @ssh_session.exec!(cmd)
|
114
|
+
|
115
|
+
@logger.debug("Command output: #{output}")
|
116
|
+
|
117
|
+
# Format JunOS result
|
118
|
+
format_junos_result(output, cmd)
|
119
|
+
rescue StandardError => e
|
120
|
+
@logger.error("Command execution failed: #{e.message}")
|
121
|
+
# Handle connection errors gracefully
|
122
|
+
CommandResult.new('', e.message, 1)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# JunOS error patterns from implementation plan
|
127
|
+
JUNOS_ERROR_PATTERNS = [
|
128
|
+
/^error:/i,
|
129
|
+
/syntax error/i,
|
130
|
+
/invalid command/i,
|
131
|
+
/unknown command/i,
|
132
|
+
/missing argument/i
|
133
|
+
].freeze
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
# Establish SSH connection to Juniper device
|
138
|
+
def connect
|
139
|
+
return if connected?
|
140
|
+
|
141
|
+
begin
|
142
|
+
# Use direct SSH connection (network device pattern)
|
143
|
+
# Defensive loading - only require if not fully loaded
|
144
|
+
require 'net/ssh' unless defined?(Net::SSH) && Net::SSH.respond_to?(:start)
|
145
|
+
|
146
|
+
@logger.debug('Establishing SSH connection to Juniper device')
|
147
|
+
|
148
|
+
ssh_options = {
|
149
|
+
port: @options[:port] || 22,
|
150
|
+
password: @options[:password],
|
151
|
+
timeout: @options[:timeout] || 30,
|
152
|
+
verify_host_key: :never,
|
153
|
+
keepalive: @options[:keepalive],
|
154
|
+
keepalive_interval: @options[:keepalive_interval]
|
155
|
+
}
|
156
|
+
|
157
|
+
# Add SSH key authentication if specified
|
158
|
+
if @options[:key_files]
|
159
|
+
ssh_options[:keys] = Array(@options[:key_files])
|
160
|
+
ssh_options[:keys_only] = @options[:keys_only]
|
161
|
+
end
|
162
|
+
|
163
|
+
# Add bastion host support if configured
|
164
|
+
if @options[:bastion_host]
|
165
|
+
require 'net/ssh/proxy/jump' unless defined?(Net::SSH::Proxy::Jump)
|
166
|
+
|
167
|
+
# Build proxy jump string from bastion options
|
168
|
+
bastion_user = @options[:bastion_user] || 'root'
|
169
|
+
bastion_port = @options[:bastion_port] || 22
|
170
|
+
|
171
|
+
proxy_jump = if bastion_port == 22
|
172
|
+
"#{bastion_user}@#{@options[:bastion_host]}"
|
173
|
+
else
|
174
|
+
"#{bastion_user}@#{@options[:bastion_host]}:#{bastion_port}"
|
175
|
+
end
|
176
|
+
|
177
|
+
@logger.debug("Using bastion host: #{proxy_jump}")
|
178
|
+
|
179
|
+
# Set up automated password authentication via SSH_ASKPASS
|
180
|
+
bastion_password = @options[:bastion_password] || @options[:password] # Use explicit bastion password or fallback
|
181
|
+
if bastion_password
|
182
|
+
@ssh_askpass_script = create_ssh_askpass_script(bastion_password)
|
183
|
+
ENV['SSH_ASKPASS'] = @ssh_askpass_script
|
184
|
+
ENV['SSH_ASKPASS_REQUIRE'] = 'force' # Force use of SSH_ASKPASS even with terminal
|
185
|
+
@logger.debug('Configured SSH_ASKPASS for automated bastion authentication')
|
186
|
+
end
|
187
|
+
|
188
|
+
ssh_options[:proxy] = Net::SSH::Proxy::Jump.new(proxy_jump)
|
189
|
+
end
|
190
|
+
|
191
|
+
@logger.debug("Connecting to #{@options[:host]}:#{@options[:port]} as #{@options[:user]}")
|
192
|
+
|
193
|
+
# Direct SSH connection
|
194
|
+
@ssh_session = Net::SSH.start(@options[:host], @options[:user], ssh_options)
|
195
|
+
@logger.debug('SSH connection established successfully')
|
196
|
+
|
197
|
+
# Configure JunOS session for automation
|
198
|
+
test_and_configure_session
|
199
|
+
rescue StandardError => e
|
200
|
+
@logger.error("SSH connection failed: #{e.message}")
|
201
|
+
|
202
|
+
# Provide helpful error messages for common authentication issues
|
203
|
+
if (e.message.include?('Permission denied') || e.message.include?('command failed')) && @options[:bastion_host]
|
204
|
+
raise Train::TransportError, <<~ERROR
|
205
|
+
Failed to connect to Juniper device #{@options[:host]} via bastion #{@options[:bastion_host]}: #{e.message}
|
206
|
+
|
207
|
+
SSH bastion authentication with passwords is not supported due to ProxyCommand limitations.
|
208
|
+
Please use one of these alternatives:
|
209
|
+
|
210
|
+
1. SSH Key Authentication (Recommended):
|
211
|
+
Use --key-files option to specify SSH private key files
|
212
|
+
#{' '}
|
213
|
+
2. SSH Agent:
|
214
|
+
Ensure your SSH agent has the required keys loaded
|
215
|
+
#{' '}
|
216
|
+
3. Direct Connection:
|
217
|
+
Connect directly to the device if network allows (remove bastion options)
|
218
|
+
|
219
|
+
For more details, see: https://mitre.github.io/train-juniper/troubleshooting/#bastion-authentication
|
220
|
+
ERROR
|
221
|
+
else
|
222
|
+
raise Train::TransportError, "Failed to connect to Juniper device #{@options[:host]}: #{e.message}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Check if SSH connection is active
|
228
|
+
def connected?
|
229
|
+
!@ssh_session.nil?
|
230
|
+
rescue StandardError
|
231
|
+
false
|
232
|
+
end
|
233
|
+
|
234
|
+
# Test connection and configure JunOS session
|
235
|
+
def test_and_configure_session
|
236
|
+
@logger.debug('Testing SSH connection and configuring JunOS session')
|
237
|
+
|
238
|
+
# Test connection first
|
239
|
+
@ssh_session.exec!('echo "connection test"')
|
240
|
+
@logger.debug('SSH connection test successful')
|
241
|
+
|
242
|
+
# Optimize CLI for automation
|
243
|
+
@ssh_session.exec!('set cli screen-length 0')
|
244
|
+
@ssh_session.exec!('set cli screen-width 0')
|
245
|
+
@ssh_session.exec!('set cli complete-on-space off') if @options[:disable_complete_on_space]
|
246
|
+
|
247
|
+
@logger.debug('JunOS session configured successfully')
|
248
|
+
rescue StandardError => e
|
249
|
+
@logger.warn("Failed to configure JunOS session: #{e.message}")
|
250
|
+
end
|
251
|
+
|
252
|
+
# Format JunOS command results (from implementation plan)
|
253
|
+
def format_junos_result(output, cmd)
|
254
|
+
# Parse JunOS-specific error patterns
|
255
|
+
if junos_error?(output)
|
256
|
+
CommandResult.new('', output, 1)
|
257
|
+
else
|
258
|
+
CommandResult.new(clean_output(output, cmd), '', 0)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Check for JunOS error patterns (from implementation plan)
|
263
|
+
def junos_error?(output)
|
264
|
+
JUNOS_ERROR_PATTERNS.any? { |pattern| output.match?(pattern) }
|
265
|
+
end
|
266
|
+
|
267
|
+
# Clean command output
|
268
|
+
def clean_output(output, cmd)
|
269
|
+
# Handle nil output gracefully
|
270
|
+
return '' if output.nil?
|
271
|
+
|
272
|
+
# Remove command echo and prompts
|
273
|
+
lines = output.to_s.split("\n")
|
274
|
+
lines.reject! { |line| line.strip == cmd.strip }
|
275
|
+
|
276
|
+
# Remove JunOS prompt patterns from the end
|
277
|
+
lines.pop while lines.last && lines.last.strip.match?(/^[%>$#]+\s*$/)
|
278
|
+
|
279
|
+
lines.join("\n")
|
280
|
+
end
|
281
|
+
|
282
|
+
# Validate proxy configuration options (Train standard)
|
283
|
+
def validate_proxy_options
|
284
|
+
# Cannot use both bastion_host and proxy_command simultaneously
|
285
|
+
if @options[:bastion_host] && @options[:proxy_command]
|
286
|
+
raise Train::ClientError, 'Cannot specify both bastion_host and proxy_command'
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Create temporary SSH_ASKPASS script for automated password authentication
|
291
|
+
def create_ssh_askpass_script(password)
|
292
|
+
require 'tempfile'
|
293
|
+
|
294
|
+
script = Tempfile.new(['ssh_askpass', '.sh'])
|
295
|
+
script.write("#!/bin/bash\necho '#{password}'\n")
|
296
|
+
script.close
|
297
|
+
File.chmod(0o755, script.path)
|
298
|
+
|
299
|
+
@logger.debug("Created SSH_ASKPASS script at #{script.path}")
|
300
|
+
script.path
|
301
|
+
end
|
302
|
+
|
303
|
+
# Generate SSH proxy command for bastion host using ProxyJump (-J)
|
304
|
+
def generate_bastion_proxy_command(bastion_user, bastion_port)
|
305
|
+
args = ['ssh']
|
306
|
+
|
307
|
+
# SSH options for connection
|
308
|
+
args += ['-o', 'UserKnownHostsFile=/dev/null']
|
309
|
+
args += ['-o', 'StrictHostKeyChecking=no']
|
310
|
+
args += ['-o', 'LogLevel=ERROR']
|
311
|
+
args += ['-o', 'ForwardAgent=no']
|
312
|
+
|
313
|
+
# Use ProxyJump (-J) which handles password authentication properly
|
314
|
+
jump_host = if bastion_port == 22
|
315
|
+
"#{bastion_user}@#{@options[:bastion_host]}"
|
316
|
+
else
|
317
|
+
"#{bastion_user}@#{@options[:bastion_host]}:#{bastion_port}"
|
318
|
+
end
|
319
|
+
args += ['-J', jump_host]
|
320
|
+
|
321
|
+
# Add SSH keys if specified
|
322
|
+
if @options[:key_files]
|
323
|
+
Array(@options[:key_files]).each do |key_file|
|
324
|
+
args += ['-i', key_file]
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Target connection - %h and %p will be replaced by Net::SSH
|
329
|
+
args += ['%h', '-p', '%p']
|
330
|
+
|
331
|
+
args.join(' ')
|
332
|
+
end
|
333
|
+
|
334
|
+
# Mock command execution for testing
|
335
|
+
def mock_command_result(cmd)
|
336
|
+
case cmd
|
337
|
+
when /show version/
|
338
|
+
CommandResult.new(mock_show_version_output, '', 0)
|
339
|
+
when /show chassis hardware/
|
340
|
+
CommandResult.new(mock_chassis_output, '', 0)
|
341
|
+
when /show configuration/
|
342
|
+
CommandResult.new("interfaces {\n ge-0/0/0 {\n unit 0;\n }\n}", '', 0)
|
343
|
+
when /show route/
|
344
|
+
CommandResult.new("inet.0: 5 destinations, 5 routes\n0.0.0.0/0 *[Static/5] 00:00:01\n", '', 0)
|
345
|
+
when /show system information/
|
346
|
+
CommandResult.new("Hardware: SRX240H2\nOS: JUNOS 12.1X47-D15.4\n", '', 0)
|
347
|
+
when /show interfaces/
|
348
|
+
CommandResult.new("Physical interface: ge-0/0/0, Enabled, Physical link is Up\n", '', 0)
|
349
|
+
else
|
350
|
+
CommandResult.new("% Unknown command: #{cmd}", '', 1)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
# Mock JunOS version output for testing
|
355
|
+
def mock_show_version_output
|
356
|
+
<<~OUTPUT
|
357
|
+
Hostname: lab-srx
|
358
|
+
Model: SRX240H2
|
359
|
+
Junos: 12.1X47-D15.4
|
360
|
+
JUNOS Software Release [12.1X47-D15.4]
|
361
|
+
OUTPUT
|
362
|
+
end
|
363
|
+
|
364
|
+
# Mock chassis output for testing
|
365
|
+
def mock_chassis_output
|
366
|
+
<<~OUTPUT
|
367
|
+
Hardware inventory:
|
368
|
+
Item Version Part number Serial number Description
|
369
|
+
Chassis JN123456 SRX240H2
|
370
|
+
OUTPUT
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# File abstraction for Juniper configuration and operational data
|
375
|
+
class JuniperFile
|
376
|
+
def initialize(connection, path)
|
377
|
+
@connection = connection
|
378
|
+
@path = path
|
379
|
+
end
|
380
|
+
|
381
|
+
def content
|
382
|
+
# For Juniper devices, translate file paths to appropriate commands
|
383
|
+
case @path
|
384
|
+
when %r{/config/(.*)}
|
385
|
+
# Configuration sections: /config/interfaces -> show configuration interfaces
|
386
|
+
section = ::Regexp.last_match(1)
|
387
|
+
result = @connection.run_command("show configuration #{section}")
|
388
|
+
result.stdout
|
389
|
+
when %r{/operational/(.*)}
|
390
|
+
# Operational data: /operational/interfaces -> show interfaces
|
391
|
+
section = ::Regexp.last_match(1)
|
392
|
+
result = @connection.run_command("show #{section}")
|
393
|
+
result.stdout
|
394
|
+
else
|
395
|
+
# Default to treating path as a show command
|
396
|
+
result = @connection.run_command("show #{@path}")
|
397
|
+
result.stdout
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def exist?
|
402
|
+
!content.empty?
|
403
|
+
rescue StandardError
|
404
|
+
false
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|