nexpose_sccm 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []