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.
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Platform definition file for Juniper network devices.
4
+ # This defines the "juniper" platform within Train's platform detection system.
5
+
6
+ module TrainPlugins::Juniper
7
+ # Platform detection mixin for Juniper network devices
8
+ module Platform
9
+ # Platform name constant for consistency
10
+ PLATFORM_NAME = 'juniper'
11
+
12
+ # Platform detection for Juniper network devices
13
+ #
14
+ # For dedicated transport plugins, we use force_platform! to bypass
15
+ # Train's automatic platform detection, which might run commands before
16
+ # the connection is ready. This is the standard pattern used by official
17
+ # Train plugins like train-k8s-container.
18
+ def platform
19
+ # Return cached platform if already computed
20
+ return @platform if defined?(@platform)
21
+
22
+ # Register the juniper platform in Train's platform registry
23
+ # JunOS devices are FreeBSD-based, so inherit from bsd family for InSpec resource compatibility
24
+ # This allows InSpec resources like 'command' to work with Juniper devices
25
+ Train::Platforms.name(PLATFORM_NAME).title('Juniper JunOS').in_family('bsd')
26
+
27
+ # Try to detect actual JunOS version and architecture from device
28
+ device_version = detect_junos_version || TrainPlugins::Juniper::VERSION
29
+ device_arch = detect_junos_architecture || 'unknown'
30
+ logger&.debug("Detected device architecture: #{device_arch}")
31
+
32
+ # Bypass Train's platform detection and declare our known platform
33
+ # Include architecture in the platform details to ensure it's properly set
34
+ platform_details = {
35
+ release: device_version,
36
+ arch: device_arch
37
+ }
38
+
39
+ platform_obj = force_platform!(PLATFORM_NAME, platform_details)
40
+ logger&.debug("Set platform data: #{platform_obj.platform}")
41
+
42
+ # Cache the platform object to prevent repeated calls
43
+ @platform = platform_obj
44
+ end
45
+
46
+ private
47
+
48
+ # Detect JunOS version from device output
49
+ # This runs safely after the connection is established
50
+ def detect_junos_version
51
+ # Return cached version if already detected
52
+ return @detected_junos_version if defined?(@detected_junos_version)
53
+
54
+ # Only try version detection if we have an active connection
55
+ return @detected_junos_version = nil unless respond_to?(:run_command_via_connection)
56
+ return @detected_junos_version = nil if @options&.dig(:mock) # Skip in mock mode
57
+
58
+ begin
59
+ # Check if connection is ready before running commands
60
+ return @detected_junos_version = nil unless connected?
61
+
62
+ # Execute 'show version' command to get JunOS information
63
+ result = run_command_via_connection('show version')
64
+ return @detected_junos_version = nil unless result&.exit_status&.zero?
65
+
66
+ # Cache the result for architecture detection to avoid duplicate calls
67
+ @cached_show_version_result = result
68
+
69
+ # Parse JunOS version from output using multiple patterns
70
+ version = extract_version_from_output(result.stdout)
71
+
72
+ if version
73
+ logger&.debug("Detected JunOS version: #{version}")
74
+ @detected_junos_version = version
75
+ else
76
+ logger&.debug("Could not parse JunOS version from: #{result.stdout[0..100]}")
77
+ @detected_junos_version = nil
78
+ end
79
+ rescue StandardError => e
80
+ # If version detection fails, log and return nil
81
+ logger&.debug("JunOS version detection failed: #{e.message}")
82
+ @detected_junos_version = nil
83
+ end
84
+ end
85
+
86
+ # Extract version string from JunOS show version output
87
+ def extract_version_from_output(output)
88
+ return nil if output.nil? || output.empty?
89
+
90
+ # Try multiple JunOS version patterns
91
+ patterns = [
92
+ /Junos:\s+([\d\w\.-]+)/, # "Junos: 12.1X47-D15.4"
93
+ /JUNOS Software Release \[([\d\w\.-]+)\]/, # "JUNOS Software Release [12.1X47-D15.4]"
94
+ /junos version ([\d\w\.-]+)/i, # "junos version 21.4R3"
95
+ /Model: \S+, JUNOS Base OS boot \[([\d\w\.-]+)\]/, # Some hardware variants
96
+ /([\d]+\.[\d]+[\w\.-]*)/ # Generic version pattern
97
+ ]
98
+
99
+ patterns.each do |pattern|
100
+ match = output.match(pattern)
101
+ return match[1] if match
102
+ end
103
+
104
+ nil
105
+ end
106
+
107
+ # Detect JunOS architecture from device output
108
+ # This runs safely after the connection is established
109
+ def detect_junos_architecture
110
+ # Return cached architecture if already detected
111
+ return @detected_junos_architecture if defined?(@detected_junos_architecture)
112
+
113
+ # Only try architecture detection if we have an active connection
114
+ return @detected_junos_architecture = nil unless respond_to?(:run_command_via_connection)
115
+ return @detected_junos_architecture = nil if @options&.dig(:mock) # Skip in mock mode
116
+
117
+ begin
118
+ # Check if connection is ready before running commands
119
+ return @detected_junos_architecture = nil unless connected?
120
+
121
+ # Reuse version detection result to avoid duplicate 'show version' calls
122
+ # Both version and architecture come from the same command output
123
+ if defined?(@detected_junos_version) && @detected_junos_version
124
+ # We already have the output from version detection, parse architecture from it
125
+ result = @cached_show_version_result
126
+ else
127
+ # Execute 'show version' command and cache the result
128
+ result = run_command_via_connection('show version')
129
+ @cached_show_version_result = result if result&.exit_status&.zero?
130
+ end
131
+
132
+ return @detected_junos_architecture = nil unless result&.exit_status&.zero?
133
+
134
+ # Parse architecture from output using multiple patterns
135
+ arch = extract_architecture_from_output(result.stdout)
136
+
137
+ if arch
138
+ logger&.debug("Detected JunOS architecture: #{arch}")
139
+ @detected_junos_architecture = arch
140
+ else
141
+ logger&.debug("Could not parse JunOS architecture from: #{result.stdout[0..100]}")
142
+ @detected_junos_architecture = nil
143
+ end
144
+ rescue StandardError => e
145
+ # If architecture detection fails, log and return nil
146
+ logger&.debug("JunOS architecture detection failed: #{e.message}")
147
+ @detected_junos_architecture = nil
148
+ end
149
+ end
150
+
151
+ # Extract architecture string from JunOS show version output
152
+ def extract_architecture_from_output(output)
153
+ return nil if output.nil? || output.empty?
154
+
155
+ # Try multiple JunOS architecture patterns
156
+ patterns = [
157
+ /Model:\s+(\S+)/, # "Model: SRX240H2" -> extract model as arch indicator
158
+ /Junos:\s+[\d\w\.-]+\s+built\s+[\d-]+\s+[\d:]+\s+by\s+builder\s+on\s+(\S+)/, # Build architecture
159
+ /JUNOS.*\[([\w-]+)\]/, # JUNOS package architecture
160
+ /Architecture:\s+(\S+)/i, # Direct architecture line
161
+ /Platform:\s+(\S+)/i, # Platform designation
162
+ /Processor.*:\s*(\S+)/i # Processor type
163
+ ]
164
+
165
+ patterns.each do |pattern|
166
+ match = output.match(pattern)
167
+ next unless match
168
+
169
+ arch_value = match[1]
170
+ # Convert model names to architecture indicators
171
+ case arch_value
172
+ when /SRX\d+/i
173
+ return 'x86_64' # Most SRX models are x86_64
174
+ when /MX\d+/i
175
+ return 'x86_64' # MX routers are typically x86_64
176
+ when /EX\d+/i
177
+ return 'arm64' # Many EX switches use ARM
178
+ when /QFX\d+/i
179
+ return 'x86_64' # QFX switches typically x86_64
180
+ when /^(x86_64|amd64|i386|arm64|aarch64|sparc|mips)$/i
181
+ return arch_value.downcase
182
+ else
183
+ # Return the model as-is if we can't map it
184
+ return arch_value
185
+ end
186
+ end
187
+
188
+ nil
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Juniper Train Plugin Transport Definition
4
+ # Defines the main transport class for connecting to Juniper network devices.
5
+ # This transport enables SSH connectivity to JunOS devices for InSpec.
6
+ require 'train-juniper/connection'
7
+
8
+ module TrainPlugins
9
+ module Juniper
10
+ class Transport < Train.plugin(1)
11
+ name 'juniper'
12
+
13
+ # Connection options for Juniper devices
14
+ # Following Train SSH transport standard options
15
+ option :host, required: true
16
+ option :port, default: 22
17
+ option :user, required: true
18
+ option :password, default: nil
19
+ option :timeout, default: 30
20
+
21
+ # Proxy/Bastion host support (Train standard options)
22
+ option :bastion_host, default: nil
23
+ option :bastion_user, default: nil # Let connection handle env vars and defaults
24
+ option :bastion_port, default: 22
25
+ option :bastion_password, default: nil # Separate password for bastion authentication
26
+ option :proxy_command, default: nil
27
+
28
+ # SSH key authentication options
29
+ option :key_files, default: nil
30
+ option :keys_only, default: false
31
+
32
+ # Advanced SSH options
33
+ option :keepalive, default: true
34
+ option :keepalive_interval, default: 60
35
+ option :connection_timeout, default: 30
36
+ option :connection_retries, default: 5
37
+ option :connection_retry_sleep, default: 1
38
+
39
+ # Standard Train options for compatibility
40
+ option :insecure, default: false
41
+ option :self_signed, default: false
42
+
43
+ # Juniper-specific options
44
+ option :mock, default: false
45
+ option :disable_complete_on_space, default: false
46
+
47
+ # Create and return a connection to a Juniper device
48
+ def connection(_instance_opts = nil)
49
+ # Cache the connection instance for reuse
50
+ # @options contains parsed connection details from train URI
51
+ @connection ||= TrainPlugins::Juniper::Connection.new(@options)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file exists simply to record the version number of the plugin.
4
+ # It is kept in a separate file, so that your gemspec can load it and
5
+ # learn the current version without loading the whole plugin. Also,
6
+ # many CI servers can update this file when "version bumping".
7
+
8
+ module TrainPlugins
9
+ module Juniper
10
+ VERSION = '0.5.4'
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is known as the "entry point."
4
+ # This is the file Train will try to load if it
5
+ # thinks your plugin is needed.
6
+
7
+ # The *only* thing this file should do is setup the
8
+ # load path, then load plugin files.
9
+
10
+ # Next two lines simply add the path of the gem to the load path.
11
+ # This is not needed when being loaded as a gem; but when doing
12
+ # plugin development, you may need it. Either way, it's harmless.
13
+ libdir = __dir__
14
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
15
+
16
+ # It's traditional to keep your gem version in a separate file, so CI can find it easier.
17
+ require 'train-juniper/version'
18
+
19
+ # A train plugin has three components: Transport, Connection, and Platform.
20
+ # Transport acts as the glue.
21
+ require 'train-juniper/transport'
22
+ require 'train-juniper/platform'
23
+ require 'train-juniper/connection'
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # As plugins are usually packaged and distributed as a RubyGem,
4
+ # we have to provide a .gemspec file, which controls the gembuild
5
+ # and publish process. This is a fairly generic gemspec.
6
+
7
+ # It is traditional in a gemspec to dynamically load the current version
8
+ # from a file in the source tree. The next three lines make that happen.
9
+ lib = File.expand_path('lib', __dir__)
10
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
11
+ require 'train-juniper/version'
12
+
13
+ Gem::Specification.new do |spec|
14
+ # Importantly, all Train plugins must be prefixed with `train-`
15
+ spec.name = 'train-juniper'
16
+
17
+ # It is polite to namespace your plugin under TrainPlugins::YourPluginInCamelCase
18
+ spec.version = TrainPlugins::Juniper::VERSION
19
+ spec.authors = ['MITRE Corporation']
20
+ spec.email = ['saf@mitre.org']
21
+ spec.summary = 'Train transport for Juniper Networks JunOS devices'
22
+ spec.description = 'Provides SSH connectivity to Juniper Networks devices running JunOS for InSpec compliance testing and ' \
23
+ 'infrastructure inspection. Supports platform detection, command execution, and configuration file access.'
24
+ spec.homepage = 'https://github.com/mitre/train-juniper'
25
+ spec.license = 'Apache-2.0'
26
+
27
+ # Metadata for better gem discovery
28
+ spec.metadata = {
29
+ 'bug_tracker_uri' => 'https://github.com/mitre/train-juniper/issues',
30
+ 'changelog_uri' => 'https://github.com/mitre/train-juniper/blob/main/CHANGELOG.md',
31
+ 'documentation_uri' => 'https://mitre.github.io/train-juniper/',
32
+ 'homepage_uri' => 'https://github.com/mitre/train-juniper',
33
+ 'source_code_uri' => 'https://github.com/mitre/train-juniper',
34
+ 'security_policy_uri' => 'https://github.com/mitre/train-juniper/security/policy',
35
+ 'rubygems_mfa_required' => 'true'
36
+ }
37
+
38
+ # Though complicated-looking, this is pretty standard for a gemspec.
39
+ # It just filters what will actually be packaged in the gem (leaving
40
+ # out tests, etc)
41
+ # Standard pattern for Train plugins - include all lib files and key docs
42
+ spec.files = %w[
43
+ README.md train-juniper.gemspec LICENSE.md NOTICE.md CHANGELOG.md
44
+ CODE_OF_CONDUCT.md CONTRIBUTING.md SECURITY.md
45
+ .env.example Rakefile
46
+ ] + Dir.glob(
47
+ 'lib/**/*', File::FNM_DOTMATCH
48
+ ).reject { |f| File.directory?(f) }
49
+ spec.require_paths = ['lib']
50
+
51
+ # If you rely on any other gems, list them here with any constraints.
52
+ # This is how `inspec plugin install` is able to manage your dependencies.
53
+ # For example, perhaps you are writing a thing that talks to AWS, and you
54
+ # want to ensure you have `aws-sdk` in a certain version.
55
+
56
+ # If you only need certain gems during development or testing, list
57
+ # them in Gemfile, not here.
58
+ # Do not list inspec as a dependency of the train plugin.
59
+
60
+ # All plugins should mention train, > 1.4
61
+ spec.required_ruby_version = '>= 3.1.0'
62
+
63
+ # Community plugins typically use train-core for smaller footprint
64
+ # train-core provides core functionality without cloud dependencies
65
+ spec.add_dependency 'train-core', '~> 3.12.13'
66
+
67
+ # SSH connectivity dependencies - match train-core's exact version range
68
+ spec.add_dependency 'net-ssh', '>= 2.9', '< 8.0'
69
+
70
+ # Force compatible FFI version to avoid conflicts with InSpec
71
+ spec.add_dependency 'ffi', '~> 1.16.0'
72
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: train-juniper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.4
5
+ platform: ruby
6
+ authors:
7
+ - MITRE Corporation
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: train-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.12.13
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.12.13
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-ssh
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.9'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '8.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '2.9'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '8.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: ffi
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 1.16.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 1.16.0
61
+ description: Provides SSH connectivity to Juniper Networks devices running JunOS for
62
+ InSpec compliance testing and infrastructure inspection. Supports platform detection,
63
+ command execution, and configuration file access.
64
+ email:
65
+ - saf@mitre.org
66
+ executables: []
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - ".env.example"
71
+ - CHANGELOG.md
72
+ - CODE_OF_CONDUCT.md
73
+ - CONTRIBUTING.md
74
+ - LICENSE.md
75
+ - NOTICE.md
76
+ - README.md
77
+ - Rakefile
78
+ - SECURITY.md
79
+ - lib/train-juniper.rb
80
+ - lib/train-juniper/connection.rb
81
+ - lib/train-juniper/platform.rb
82
+ - lib/train-juniper/transport.rb
83
+ - lib/train-juniper/version.rb
84
+ - train-juniper.gemspec
85
+ homepage: https://github.com/mitre/train-juniper
86
+ licenses:
87
+ - Apache-2.0
88
+ metadata:
89
+ bug_tracker_uri: https://github.com/mitre/train-juniper/issues
90
+ changelog_uri: https://github.com/mitre/train-juniper/blob/main/CHANGELOG.md
91
+ documentation_uri: https://mitre.github.io/train-juniper/
92
+ homepage_uri: https://github.com/mitre/train-juniper
93
+ source_code_uri: https://github.com/mitre/train-juniper
94
+ security_policy_uri: https://github.com/mitre/train-juniper/security/policy
95
+ rubygems_mfa_required: 'true'
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 3.1.0
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.3.27
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Train transport for Juniper Networks JunOS devices
115
+ test_files: []