nexpose_sccm 0.4.0

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,5 @@
1
+ class RemediationItem
2
+ def initialize(solution_id, vuln_ids)
3
+
4
+ end
5
+ end
@@ -0,0 +1,54 @@
1
+ require 'winrm'
2
+ require_relative 'powershell'
3
+
4
+ module NexposeSCCM
5
+ # Configuration settings for creating a Software Update Group
6
+ #
7
+ # * *Args* :
8
+ # - +name+ - A String identifying the display name of the Software Update Group
9
+ # - +description+ - A String identifying the description of the Software Update Group
10
+ # - +ci_ids+ - An Array identifying the list of ci_ids to add to a Software Update Group
11
+ #
12
+ class SoftwareUpdateGroup
13
+ attr_accessor :name, :description, :ci_ids
14
+
15
+ def initialize(name, description, ci_ids, ci_id=nil)
16
+ @name = name
17
+ @description = description
18
+ @ci_ids = ci_ids
19
+ @ci_id = ci_id
20
+ end
21
+
22
+ def save(conn)
23
+ if @ci_id.nil?
24
+ if @ci_ids.length > 0
25
+ Powershell.run(conn.conn,
26
+ :create_software_update_group,
27
+ conn.namespace,
28
+ conn.host,
29
+ @name,
30
+ @description,
31
+ @ci_ids.join(','))
32
+ else
33
+ NexposeSCCM.logger.debug('No CI_IDs to add to Software Update Group, going to next...')
34
+ return false
35
+ end
36
+ else
37
+ if @ci_ids.length > 0
38
+ Powershell.run(conn.conn,
39
+ :update_software_update_group,
40
+ conn.namespace,
41
+ conn.host,
42
+ @ci_id,
43
+ @ci_ids.join(','))
44
+ else
45
+ Powershell.run(conn.conn,
46
+ :update_software_update_group_empty,
47
+ conn.namespace,
48
+ conn.host,
49
+ @ci_id)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,171 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'singleton'
5
+
6
+ module NexposeSCCM
7
+ class NxLogger
8
+ include Singleton
9
+ LOG_PATH = "../logs/rapid7_%s.log"
10
+ KEY_FORMAT = "external.integration.%s"
11
+ PRODUCT_FORMAT = "%s_%s"
12
+
13
+ DEFAULT_LOG = 'integration'
14
+ PRODUCT_RANGE = 4..30
15
+ KEY_RANGE = 3..15
16
+
17
+ ENDPOINT = '/data/external/statistic/'
18
+
19
+ def initialize()
20
+ create_calls
21
+ @logger_file = get_log_path @product
22
+ setup_logging(true, 'info')
23
+ end
24
+
25
+ def setup_statistics_collection(vendor, product_name, gem_version)
26
+ begin
27
+ @statistic_key = get_statistic_key vendor
28
+ @product = get_product product_name, gem_version
29
+ rescue => e
30
+ #Continue
31
+ end
32
+ end
33
+
34
+ def setup_logging(enabled,
35
+ log_level = 'info',
36
+ stdout=false,
37
+ rotation='weekly',
38
+ log_size=nil)
39
+ @stdout = stdout
40
+
41
+ log_message('Logging disabled.') unless enabled || @log.nil?
42
+ @enabled = enabled
43
+ return unless @enabled
44
+
45
+ @logger_file = get_log_path @product
46
+
47
+ require 'logger'
48
+ directory = File.dirname(@logger_file)
49
+ FileUtils.mkdir_p(directory) unless File.directory?(directory)
50
+ io = IO.for_fd(IO.sysopen(@logger_file, 'a'), 'a')
51
+ io.autoclose = false
52
+ io.sync = true
53
+ @log = Logger.new(io, rotation, log_size)
54
+ @log.level = if log_level.to_s.casecmp('info') == 0
55
+ Logger::INFO
56
+ else
57
+ Logger::DEBUG
58
+ end
59
+ log_message("Logging enabled at level <#{log_level}>")
60
+ end
61
+
62
+ def create_calls
63
+ levels = [:info, :debug, :error, :warn]
64
+ levels.each do |level|
65
+ method_name = "log_#{level.to_s}_message"
66
+ define_singleton_method(method_name) do |message|
67
+ puts message if @stdout
68
+ @log.send(level, message) unless !@enabled || @log.nil?
69
+ end
70
+ singleton_class.send(:alias_method, level, method_name.to_sym)
71
+ end
72
+ end
73
+
74
+ def log_message(message)
75
+ log_info_message message
76
+ end
77
+
78
+ def log_stat_message(message)
79
+ end
80
+
81
+ def get_log_path(product)
82
+ product.downcase! unless product.nil?
83
+ File.join(File.dirname(__FILE__), LOG_PATH % (product || DEFAULT_LOG))
84
+ end
85
+
86
+ def get_statistic_key(vendor)
87
+ if vendor.nil? || vendor.length < KEY_RANGE.min
88
+ log_stat_message("Vendor length is below minimum of <#{KEY_RANGE}>")
89
+ return nil
90
+ end
91
+
92
+ vendor.gsub!('-', '_')
93
+ vendor.slice! vendor.rindex('_') until vendor.count('_') <= 1
94
+
95
+ vendor.delete! "^A-Za-z0-9\_"
96
+
97
+ KEY_FORMAT % vendor[0...KEY_RANGE.max].downcase
98
+ end
99
+
100
+ def get_product(product, version)
101
+ return nil if ((product.nil? || product.empty?) ||
102
+ (version.nil? || version.empty?))
103
+
104
+ product.gsub!('-', '_')
105
+ product.slice! product.rindex('_') until product.count('_') <= 1
106
+
107
+ product.delete! "^A-Za-z0-9\_"
108
+ version.delete! "^A-Za-z0-9\.\-"
109
+
110
+ product = (PRODUCT_FORMAT % [product, version])[0...PRODUCT_RANGE.max]
111
+
112
+ product.slice! product.rindex(/[A-Z0-9]/i)+1..-1
113
+
114
+ if product.length < PRODUCT_RANGE.min
115
+ log_stat_message("Product length below minimum <#{PRODUCT_RANGE.min}>.")
116
+ return nil
117
+ end
118
+ product.downcase
119
+ end
120
+
121
+ def generate_payload(statistic_value='')
122
+ product_name, separator, version = @product.to_s.rpartition('_')
123
+ payload_value = {'version' => version}.to_json
124
+
125
+ payload = {'statistic-key' => @statistic_key.to_s,
126
+ 'statistic-value' => payload_value,
127
+ 'product' => product_name}
128
+ JSON.generate(payload)
129
+ end
130
+
131
+ def send(nexpose_address, nexpose_port, session_id, payload)
132
+ header = {'Content-Type' => 'application/json',
133
+ 'nexposeCCSessionID' => session_id,
134
+ 'Cookie' => "nexposeCCSessionID=#{session_id}"}
135
+ req = Net::HTTP::Put.new(ENDPOINT, header)
136
+ req.body = payload
137
+ http_instance = Net::HTTP.new(nexpose_address, nexpose_port)
138
+ http_instance.use_ssl = true
139
+ http_instance.verify_mode = OpenSSL::SSL::VERIFY_NONE
140
+ response = http_instance.start { |http| http.request(req) }
141
+ log_stat_message "Received code #{response.code} from Nexpose console."
142
+ log_stat_message "Received message #{response.msg} from Nexpose console."
143
+ log_stat_message 'Finished sending statistics data to Nexpose.'
144
+
145
+ response.code
146
+ end
147
+
148
+ def on_connect(nexpose_address, nexpose_port, session_id, value)
149
+ log_stat_message 'Sending statistics data to Nexpose'
150
+
151
+ if @product.nil? || @statistic_key.nil?
152
+ log_stat_message('Invalid product name and/or statistics key.')
153
+ log_stat_message('Statistics collection not enabled.')
154
+ return
155
+ end
156
+
157
+ begin
158
+ payload = generate_payload value
159
+ send(nexpose_address, nexpose_port, session_id, payload)
160
+ rescue => e
161
+ #Let the program continue
162
+ end
163
+ end
164
+
165
+ #Used by net library for debugging
166
+ def <<(value)
167
+ log_debug_message(value)
168
+ end
169
+
170
+ end
171
+ end
@@ -0,0 +1,59 @@
1
+ module UtilityConfig
2
+ def self.load_config(settings_locations, settings_dir=nil)
3
+ # Load settings.yml from -s option or parse known locations for configuration file
4
+ begin
5
+ settings_dir = settings_dir.nil? ? settings_locations : [settings_dir]
6
+ settings = nil
7
+
8
+ settings_dir.each do |dir|
9
+ Dir.glob(File.join(dir, '*.yml')) do |yml_file|
10
+ tmp_settings = YAML::load_file(yml_file)
11
+ next if tmp_settings.nil?
12
+ settings = settings.nil? ? tmp_settings.dup : settings.merge(tmp_settings.to_h)
13
+ end
14
+ end
15
+ rescue Exception => e
16
+ abort("Could not parse or load settings file: #{e}")
17
+ end
18
+ settings
19
+ end
20
+
21
+ def self.load_encrypt(settings)
22
+ # Load encrypted settings if defined
23
+ begin
24
+ if settings[:secret]
25
+ priv_key = OpenSSL::PKey::RSA.new(File.read(File.join(settings[:secret][:encrypt_key_dir], 'rsa_key')))
26
+ dec_buf = priv_key.private_decrypt(Base64.decode64(File.read(settings[:secret][:encrypt_cred_file])))
27
+ YAML::load(dec_buf).each do |key,value|
28
+ value.each do |k,v|
29
+ if v.class == String
30
+ settings[key][k] = v
31
+ elsif v.class == Hash
32
+ v.each do |k2,v2|
33
+ settings[key][k][k2] = v2
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ return settings
40
+ rescue Exception => e
41
+ abort("Could not parse or load encrypted file #{settings[:secret][:encrypt_cred_file]} : #{e}")
42
+ end
43
+ end
44
+
45
+ def self.save_encrypt(settings_locations, settings_dir=nil)
46
+ begin
47
+ settings_dir = settings_dir.nil? ? settings_locations : [settings_dir]
48
+ settings_dir.each do |dir|
49
+ Dir.glob(File.join(dir, '*.yml')) do |yml_file|
50
+ text = File.read(yml_file)
51
+ new_contents = text.gsub(/:\s+[\'\"]\(enc\).*[\'\"][\r\n]*/, ": 'secret'\n")
52
+ File.open(yml_file, "w") {|file| file.puts new_contents}
53
+ end
54
+ end
55
+ rescue Exception => e
56
+ abort("Could not parse or save settings file: #{e}")
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,5 @@
1
+ module NexposeSCCM
2
+ VERSION = '0.4.0'
3
+ VENDOR = 'Microsoft'
4
+ PRODUCT = 'SCCM'
5
+ end
@@ -0,0 +1,35 @@
1
+ require 'winrm'
2
+
3
+ module NexposeSCCM
4
+ module Wql
5
+
6
+ @queries = {
7
+ :get_ci_ids => "SELECT ci_id FROM SMS_SoftwareUpdate WHERE CI_UniqueID=\"%s\"",
8
+ :get_sups => "SELECT ci_id, localizeddisplayname, localizeddescription FROM SMS_AuthorizationList WHERE LocalizedDisplayName like \"Rapid7%%\"",
9
+ :get_collections => "SELECT * FROM SMS_Collection WHERE Name LIKE \"Rapid7%%\"",
10
+ :get_collection_by_name => "SELECT * FROM SMS_Collection WHERE Name = \"%s\"",
11
+ :get_collection_members => "SELECT * FROM SMS_FullCollectionMembership WHERE CollectionId=\"%s\"",
12
+ :get_devices => "SELECT * FROM SMS_R_System WHERE client IS NOT NULL",
13
+ :get_device_by_id => "SELECT * FROM SMS_R_System WHERE ResourceID = \"%s\"",
14
+ :get_asset_mac => "SELECT * FROM SMS_R_System WHERE MACAddresses = \"%s\"",
15
+ :get_asset_ip => "SELECT * FROM SMS_R_System WHERE IPAddresses = \"%s\"",
16
+ :get_asset_hostname => "SELECT * FROM SMS_R_System WHERE Name = \"%s\"",
17
+ :get_update_url => "SELECT FileName,SourceURL from SMS_CIContentfiles WHERE ContentID=\"%s\"",
18
+ :ci_to_contentid => "SELECT contentid FROM SMS_CIToContent WHERE ci_id = \"%s\"",
19
+ :get_deployment_package => "SELECT * FROM SMS_SoftwareUpdatesPackage WHERE Name = \"%s\""
20
+ }
21
+
22
+ def self.run(conn, namespace, query, *args)
23
+ unless @queries.key?(query)
24
+ NexposeSCCM.logger.error("Invalid query supplied: #{query}")
25
+ end
26
+ wql = @queries[query] % [*args]
27
+ results = []
28
+ conn.run_wql(wql, "#{namespace.gsub('\\','/')}/*") do |_, item|
29
+ item = item.inject({}){|i,(k,v)| i[k.to_sym] = v; i}
30
+ results.push(item)
31
+ end
32
+ results
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+
3
+ require_relative './lib/nexpose_sccm/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'nexpose_sccm'
7
+ spec.version = NexposeSCCM::VERSION
8
+ spec.authors = ['Ben Glass', 'Zac Youtz', 'Adam Robinson']
9
+ spec.email = ['support@rapid7.com']
10
+ spec.summary = %q{Ruby Gem for Nexpose SCCM Integration}
11
+ spec.description = %q{This gem pulls data from InsightVM/Nexpose and creates content in SCCM for remediation}
12
+ spec.homepage = 'http://www.rapid7.com/'
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = Dir.glob("{bin,conf,lib}/**/*")
16
+ spec.files += Dir['README.md', 'nexpose_sccm.gemspec', 'MIT-LICENSE', 'Gemfile']
17
+ spec.files.reject! { |f| f.match(%r{(logs)/}) }
18
+ spec.require_paths = ['lib']
19
+ spec.executables = ['nexpose_sccm']
20
+ spec.bindir = 'bin'
21
+
22
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
23
+ # delete this section to allow pushing this gem to any host.
24
+ if spec.respond_to?(:metadata)
25
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
26
+ else
27
+ raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
28
+ end
29
+
30
+ spec.add_runtime_dependency 'nexpose', '~> 7.2'
31
+ spec.add_runtime_dependency 'pg', '~> 0.21.0'
32
+ spec.add_runtime_dependency 'winrm', '~> 2.2', '>= 2.2.3'
33
+
34
+ spec.required_ruby_version = ['>= 2.3.1', '< 2.5.0']
35
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nexpose_sccm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben Glass
8
+ - Zac Youtz
9
+ - Adam Robinson
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2018-04-13 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: nexpose
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '7.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '7.2'
29
+ - !ruby/object:Gem::Dependency
30
+ name: pg
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: 0.21.0
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: 0.21.0
43
+ - !ruby/object:Gem::Dependency
44
+ name: winrm
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '2.2'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 2.2.3
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '2.2'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.2.3
63
+ description: This gem pulls data from InsightVM/Nexpose and creates content in SCCM
64
+ for remediation
65
+ email:
66
+ - support@rapid7.com
67
+ executables:
68
+ - nexpose_sccm
69
+ extensions: []
70
+ extra_rdoc_files: []
71
+ files:
72
+ - Gemfile
73
+ - MIT-LICENSE
74
+ - README.md
75
+ - bin/encrypt_settings.rb
76
+ - bin/nexpose_sccm
77
+ - bin/vuln_ids.csv
78
+ - conf/logging.yml
79
+ - conf/nexpose.yml
80
+ - conf/postgres.yml
81
+ - conf/queries.yml
82
+ - conf/sccm.yml
83
+ - conf/secret.yml
84
+ - lib/nexpose_sccm.rb
85
+ - lib/nexpose_sccm/collection.rb
86
+ - lib/nexpose_sccm/connection.rb
87
+ - lib/nexpose_sccm/data_source.rb
88
+ - lib/nexpose_sccm/deployment_package.rb
89
+ - lib/nexpose_sccm/device.rb
90
+ - lib/nexpose_sccm/helpers/nexpose_helper.rb
91
+ - lib/nexpose_sccm/helpers/pg_helper.rb
92
+ - lib/nexpose_sccm/powershell.rb
93
+ - lib/nexpose_sccm/remediation_item.rb
94
+ - lib/nexpose_sccm/software_update_group.rb
95
+ - lib/nexpose_sccm/utilities/nx_logger.rb
96
+ - lib/nexpose_sccm/utilities/utility_config.rb
97
+ - lib/nexpose_sccm/version.rb
98
+ - lib/nexpose_sccm/wql.rb
99
+ - nexpose_sccm.gemspec
100
+ homepage: http://www.rapid7.com/
101
+ licenses:
102
+ - MIT
103
+ metadata:
104
+ allowed_push_host: https://rubygems.org
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: 2.3.1
114
+ - - "<"
115
+ - !ruby/object:Gem::Version
116
+ version: 2.5.0
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 2.6.11
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Ruby Gem for Nexpose SCCM Integration
128
+ test_files: []