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,42 @@
1
+ module NexposeSCCM
2
+ class Collection
3
+ attr_reader :name, :collection_id, :member_count
4
+ attr_accessor :current_members, :members
5
+
6
+ def initialize(name, collection_id=nil, member_count=0)
7
+ @name = name
8
+ @collection_id = collection_id
9
+ @member_count = member_count
10
+ @current_members = Set.new
11
+ @members = Set.new
12
+ end
13
+
14
+ def save(conn)
15
+ if @collection_id.nil?
16
+ NexposeSCCM.logger.info("Creating collection #{@name} prior to populating")
17
+ Powershell.run(conn.conn, :create_collection, conn.location, @name)
18
+
19
+ @collection_id = conn.get_collection_id_by_name(@name)
20
+ end
21
+
22
+ #Devices to be added
23
+ NexposeSCCM.logger.debug("Adding #{(@members - @current_members).length} devices to #{@name} collection")
24
+ (@members - @current_members).each do |member|
25
+ Powershell.run(conn.conn,
26
+ :add_device_to_collection,
27
+ conn.location,
28
+ @collection_id,
29
+ member.resource_id)
30
+ end
31
+ #Devices to be removed
32
+ NexposeSCCM.logger.debug("Removing #{(@current_members - @members).length} devices from #{@name} collection")
33
+ (@current_members - @members).each do |member|
34
+ Powershell.run(conn.conn,
35
+ :remove_device_from_collection,
36
+ conn.location,
37
+ @collection_id,
38
+ member.resource_id)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,165 @@
1
+ require 'winrm'
2
+ require_relative 'collection'
3
+ require_relative 'device'
4
+ require_relative 'powershell'
5
+ require_relative 'software_update_group'
6
+ require_relative 'wql'
7
+
8
+ module NexposeSCCM
9
+ # Connection object to interact with SCCM and SCCM utilities
10
+ #
11
+ # * *Args* :
12
+ # - +protocol+ - A String identifying HTTP or HTTPS for the SCCM web service
13
+ # - +host+ - A String identifying the target host SCCM is running on
14
+ # - +port+ - A String identifying port for the SCCM web service
15
+ # - +path+ - A String identifying the URL path for the SCCM web service
16
+ # - +location+ - A String identifying the SCCM location for running PS commands on filesystem
17
+ # - +user+ - A String identifying username for the SCCM web service
18
+ # - +pass+ - A String identifying the password for the SCCM web service
19
+ # - +namespace+ - A String identifying the namespace for the SCCM server
20
+ # - +staging+ - A String identifying the directory to keep downloaded content updates for SCCM
21
+ #
22
+ class Connection
23
+ attr_reader :conn, :host, :namespace, :location, :staging
24
+
25
+ def initialize(protocol='http', host='localhost', port=5985, path='wsman', location=nil, user=nil, pass=nil,
26
+ namespace=nil, staging=nil, no_ssl_peer_verification=false, ssl_peer_fingerprint=nil)
27
+ @endpoint = "#{protocol}://#{host}:#{port}/#{path}"
28
+ @user = user
29
+ @password = pass
30
+ @namespace = namespace
31
+ @host = host
32
+ @location = location
33
+ @staging = staging.nil? ? nil : staging.chomp("\\")
34
+ @no_ssl_peer_verification = no_ssl_peer_verification
35
+ @ssl_peer_fingerprint = ssl_peer_fingerprint
36
+ end
37
+
38
+ def login
39
+ NexposeSCCM.logger.info("Logging into SCCM via WINRM: #{@endpoint}")
40
+ @conn = WinRM::Connection.new(endpoint: @endpoint,
41
+ user: @user,
42
+ password: @password,
43
+ no_ssl_peer_verification: @no_ssl_peer_verification,
44
+ ssl_peer_fingerprint: @ssl_peer_fingerprint)
45
+ #get_info
46
+ end
47
+
48
+ def get_info
49
+ begin
50
+ results = Powershell.run(@conn, :get_sccm_info)
51
+ info_match = ['Version','Full Version','CU Level']
52
+ results.each do |result|
53
+ if info_match.any?{|word| result.include?(word)}
54
+ NexposeSCCM.logger.info("SCCM #{result.strip.gsub(/\s+/, ' ')}")
55
+ end
56
+ end
57
+ rescue Exception=>e
58
+ NexposeSCCM.logger.error("Unable to retrieve SCCM version details: #{e}")
59
+ end
60
+ end
61
+
62
+ def get_ci_id(ldn)
63
+ NexposeSCCM.logger.debug("Getting CI IDs for solution: #{ldn}")
64
+ ci_ids = []
65
+ results = Wql.run(@conn, @namespace, :get_ci_ids, ldn)
66
+ results.each do |r|
67
+ ci_ids.push(r[:ci_id]) unless ci_ids.include?(r[:ci_id])
68
+ end
69
+ ci_ids
70
+ end
71
+
72
+ def get_software_update_groups
73
+ NexposeSCCM.logger.debug('Getting names and CI IDs for all Rapid7 Software Update Groups')
74
+ groups = []
75
+ results = Wql.run(@conn, @namespace, :get_sups)
76
+ results = results.map {|r| {:ci_id => r[:ci_id], :name => r[:localized_display_name], :description => r[:localized_description]}}
77
+ results.each do |r|
78
+ sup = SoftwareUpdateGroup.new(r[:name], r[:description], nil, r[:ci_id])
79
+ groups.push(sup)
80
+ end
81
+ groups
82
+ end
83
+
84
+ def get_collection_id_by_name(collection_name)
85
+ results = Wql.run(@conn, @namespace, :get_collection_by_name, collection_name)
86
+
87
+ collection_id = nil
88
+ results.each do |result|
89
+ collection_id = result[:collection_id]
90
+ end
91
+ collection_id
92
+ end
93
+
94
+ def get_collections(sccm_devices)
95
+ NexposeSCCM.logger.debug('Getting name of collections for all existing Rapid7 Collections')
96
+ collections = []
97
+ results = Wql.run(@conn, @namespace, :get_collections)
98
+ results.each do |result|
99
+ NexposeSCCM.logger.debug("Getting device membership for #{result[:name]} collection")
100
+ collection = Collection.new(result[:name],result[:collection_id],result[:member_count])
101
+
102
+ collection.current_members = get_collection_devices(collection.collection_id,sccm_devices)
103
+
104
+ collections << collection
105
+ end
106
+ collections
107
+ end
108
+
109
+ def get_collection_devices(collection_id, sccm_devices)
110
+ NexposeSCCM.logger.debug("Getting devices for collection ID: #{collection_id}")
111
+ devices = Set.new
112
+ members = Wql.run(@conn, @namespace, :get_collection_members, collection_id)
113
+ members.each do |member|
114
+ device = sccm_devices.select{|device| device.resource_id.eql?(member[:resource_id])}.first
115
+ devices.add(device) unless device.nil?
116
+ end
117
+ devices
118
+ end
119
+
120
+ def get_devices
121
+ NexposeSCCM.logger.debug('Getting devices known to SCCM with details')
122
+ devices = []
123
+ members = Wql.run(@conn, @namespace, :get_devices)
124
+ members.each do |member|
125
+ if member[:ip_addresses].is_a?(Nori::StringWithAttributes)
126
+ ips = [member[:ip_addresses]]
127
+ else
128
+ ips = member[:ip_addresses]
129
+ end
130
+ device = Device.new(ips,member[:netbios_name],member[:resource_id])
131
+ devices << device
132
+ end
133
+ devices
134
+ end
135
+
136
+ def get_deployment_package(name)
137
+ Wql.run(@conn, @namespace, :get_deployment_package, name)
138
+ end
139
+
140
+ def download_patches(sup_name, ci_ids)
141
+ NexposeSCCM.logger.info("Downloading Software Update Patches for Deployment Packages.")
142
+ contentIds = []
143
+ ## Need to get ContentID using ci_id
144
+ ci_ids.each do |c|
145
+ results = Wql.run(@conn, @namespace, :ci_to_contentid, c)
146
+ results.each do |r|
147
+ contentIds.push(r[:content_id]) unless contentIds.include?(r[:content_id])
148
+ end
149
+ end
150
+ ## Need to get content info using ContentId
151
+ contentIds.each do |c|
152
+ NexposeSCCM.logger.debug("Working on Content Update: #{c}")
153
+ results = Wql.run(@conn, @namespace, :get_update_url, c)
154
+ results.each do |r|
155
+ res = Powershell.run(@conn, :download_content, r[:source_url], "#{@staging}\\#{sup_name}", c, r[:file_name])
156
+ if res
157
+ NexposeSCCM.logger.debug("Successfully downloaded content update: #{c}")
158
+ else
159
+ NexposeSCCM.logger.error("There was an error downloading content update: #{c}")
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,67 @@
1
+ module NexposeSCCM
2
+ module DataSource
3
+ # Connection object to interact with SCCM and SCCM utilities
4
+ #
5
+ # * *Args* :
6
+ # - +data_source+ - A String identifying 'nsc' or 'dwh' for the source of Nexpose data
7
+ # - +pg+ - A Hash identifying the Postgres connection settings
8
+ # - +nx+ - A Hash identifying the Nexpose connection settings
9
+ #
10
+ class Connection
11
+ def initialize(data_source='nsc', pg=nil, nx=nil)
12
+ connection_type = data_source
13
+
14
+ if connection_type.casecmp('dwh') == 0
15
+ @connection_type = :dwh
16
+ unless pg.nil?
17
+ require_relative 'helpers/pg_helper'
18
+ @conn = Helpers::PGHelper::Connection.new(pg)
19
+ end
20
+ else
21
+ @connection_type = :nsc
22
+ unless nx.nil?
23
+ require_relative 'helpers/nexpose_helper'
24
+ @conn = Helpers::NexposeHelper::Connection.new(nx)
25
+ end
26
+ end
27
+ end
28
+
29
+ def fetch_data(query)
30
+ res = @conn.get_data(query)
31
+ Data.new(res, @connection_type)
32
+ end
33
+ end
34
+
35
+ # Universal Data object to send back to client and makes the data source transparent
36
+ #
37
+ # * *Args* :
38
+ # - +data+ - A resultset, either using the pg_helper or nexpose_helper
39
+ # - +connection_type+ - A symbol representing whether to use postgres or nexpose
40
+ #
41
+ class Data
42
+ def initialize(data, connection_type)
43
+ @data = data
44
+ @connection_type = connection_type
45
+ end
46
+
47
+ def each
48
+ @data.each do |p|
49
+ sym_p = p.inject({}){|res,(k,v)| res[k.to_sym] = v; res}
50
+ yield sym_p
51
+ end
52
+ end
53
+
54
+ def length
55
+ case @connection_type
56
+ when :dwh
57
+ @data.cmd_tuples
58
+ when :nsc
59
+ @data.length
60
+ else
61
+ NexposeSCCM.logger.error("An invalid connection type made it's way in here, exiting...")
62
+ exit 1
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,37 @@
1
+ module NexposeSCCM
2
+ class DeploymentPackage
3
+ attr_reader :name, :sug_name, :path
4
+ attr_accessor :id, :collection_name
5
+
6
+ def initialize(name,sug_name,path,id=nil,collection_name=nil,deployment_type='Available')
7
+ @name = name
8
+ @id = id
9
+ @sug_name = sug_name
10
+ @path = path
11
+ @collection_name = collection_name
12
+ @deployment_type = deployment_type
13
+ end
14
+
15
+ def save(conn)
16
+ NexposeSCCM.logger.info("Created Deployment Package: #{@name}")
17
+ NexposeSCCM.logger.debug("Deployment package has path [#{@path}]")
18
+ Powershell.run(conn.conn,
19
+ :create_deployment_package,
20
+ conn.namespace,
21
+ conn.host,
22
+ @name,
23
+ @name,
24
+ @path)
25
+ end
26
+
27
+ def download_updates(conn)
28
+ NexposeSCCM.logger.debug("Downloading updates for #{sug_name}")
29
+ Powershell.run(conn.conn,
30
+ :download_software_updates,
31
+ conn.location,
32
+ @name,
33
+ @sug_name,
34
+ @path)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,11 @@
1
+ class Device
2
+ attr_accessor :remediation_items
3
+ attr_reader :ip_address, :host_name, :resource_id
4
+
5
+ def initialize(ip_address, host_name, resource_id)
6
+ @ip_address = ip_address
7
+ @host_name = host_name
8
+ @resource_id = resource_id
9
+ @remediation_items = Set.new
10
+ end
11
+ end
@@ -0,0 +1,65 @@
1
+ require 'nexpose'
2
+ require 'csv'
3
+
4
+ module NexposeSCCM
5
+ module DataSource
6
+ module Helpers
7
+ module NexposeHelper
8
+ class Connection
9
+ def initialize(settings)
10
+ nexpose_ip = settings[:host]
11
+ nexpose_un = settings[:user]
12
+ nexpose_pw = settings[:pass]
13
+ nexpose_pt = settings[:port]
14
+ nexpose_silo = settings[:silo]
15
+ nexpose_timeout = settings[:timeout]
16
+ @nexpose_query_timeout = settings[:query_timeout]
17
+
18
+ ## New Nexpose Connection -> Requirements are Device IP, Username, Password, Port with optional Silo ID
19
+ @nsc = Nexpose::Connection.new(nexpose_ip, nexpose_un, nexpose_pw, nexpose_pt, nexpose_silo)
20
+ login(nexpose_timeout)
21
+ register_metrics(nexpose_ip, nexpose_pt, {})
22
+ end
23
+
24
+ def login(timeout=120)
25
+ @nsc.login
26
+
27
+ # Check for a valid session.
28
+ unless @nsc.session_id
29
+ NexposeSCCM.logger.error("Login Failed")
30
+ exit 1
31
+ end
32
+
33
+ at_exit do
34
+ NexposeSCCM.logger.debug("Logging out")
35
+ @nsc.logout if @nsc.session_id
36
+ end
37
+
38
+ ## Set the http read timeout.
39
+ NexposeSCCM.logger.info("Setting the web session timeout to #{timeout}")
40
+ @nsc.timeout = timeout.to_i
41
+ end
42
+
43
+ def get_data(query)
44
+ report_config = Nexpose::AdhocReportConfig.new(nil, 'sql')
45
+ report_config.add_filter('version', '2.3.0')
46
+ report_config.add_filter('query', query)
47
+
48
+ NexposeSCCM.logger.info("Running SQL report for a list of windows vulnerabilities.")
49
+ report_output = report_config.generate(@nsc, @nexpose_query_timeout)
50
+
51
+ CSV.parse(report_output.chomp, {:headers => :first_row})
52
+ end
53
+
54
+ def register_metrics(hostname, port, payload)
55
+ NexposeSCCM.logger.debug("Sending_Stats")
56
+ NexposeSCCM.logger.on_connect(hostname,
57
+ port,
58
+ @nsc.session_id,
59
+ payload)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,26 @@
1
+ require 'pg'
2
+
3
+ module NexposeSCCM
4
+ module DataSource
5
+ module Helpers
6
+ module PGHelper
7
+ class Connection
8
+ def initialize(settings)
9
+ host = settings[:host]
10
+ port = settings[:port]
11
+ user = settings[:user]
12
+ pass = settings[:pass]
13
+ db = settings[:db]
14
+ sslmode = settings[:sslmode]
15
+
16
+ @conn = PG::Connection.open(:host => host, :dbname => db, :port => port, :user => user, :password => pass, :sslmode => sslmode)
17
+ end
18
+
19
+ def get_data(query)
20
+ @conn.exec(query)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,111 @@
1
+ require 'winrm'
2
+
3
+ module NexposeSCCM
4
+ module Powershell
5
+
6
+ @cmds = {
7
+ :get_sccm_info => <<~COMMAND,
8
+ Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\SMS\\Setup
9
+ COMMAND
10
+ :download_content => <<~COMMAND,
11
+ $url = "%s"
12
+ $content = "%s\\%s"
13
+ If(!(test-path $content))
14
+ {
15
+ New-Item -ItemType Directory -Force -Path $content
16
+ $output = "$content\\%s"
17
+ $wc = New-Object System.Net.WebClient
18
+ $wc.DownloadFile($url, $output)
19
+ }
20
+ COMMAND
21
+ :sccm_download_content => <<~COMMAND,
22
+
23
+ COMMAND
24
+ :create_software_update_group => <<~COMMAND,
25
+ $PSDefaultParameterValues =@{"get-wmiobject:namespace"="%s";"get-WMIObject:computername"="%s"}
26
+ $class = Get-WmiObject -Class SMS_CI_LocalizedProperties -list
27
+ $LocalizedProperties = $class.CreateInstance()
28
+ $LocalizedProperties.DisplayName="%s"
29
+ $LocalizedProperties.Description="%s"
30
+ $class = $class = Get-WmiObject -Class SMS_AuthorizationList -list
31
+ $UpdateGroup = $class.CreateInstance()
32
+ $UpdateGroup.LocalizedInformation = $LocalizedProperties
33
+ $UpdateGroup.Updates = %s
34
+ $UpdateGroup.put()
35
+ COMMAND
36
+ :update_software_update_group => <<~COMMAND,
37
+ $PSDefaultParameterValues =@{"get-wmiobject:namespace"="%s";"get-WMIObject:computername"="%s"}
38
+ $UpdateGroup = Get-WmiObject -Class SMS_AuthorizationList | Where-Object -Property CI_ID -EQ "%s"
39
+ $UpdateGroup.Updates = %s
40
+ $UpdateGroup.put()
41
+ COMMAND
42
+ :update_software_update_group_empty => <<~COMMAND,
43
+ $PSDefaultParameterValues =@{"get-wmiobject:namespace"="%s";"get-WMIObject:computername"="%s"}
44
+ $UpdateGroup = Get-WmiObject -Class SMS_AuthorizationList | Where-Object -Property CI_ID -EQ "%s"
45
+ $UpdateGroup.Updates = @()
46
+ $UpdateGroup.put()
47
+ COMMAND
48
+ :create_collection => <<~COMMAND,
49
+ Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\\..\\ConfigurationManager.psd1"
50
+ Set-Location %s
51
+ New-CMDeviceCollection -Name "%s" -LimitingCollectionName "All Systems"
52
+ COMMAND
53
+ :add_device_to_collection => <<~COMMAND,
54
+ Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\\..\\ConfigurationManager.psd1"
55
+ Set-Location %s
56
+ Add-CMDeviceCollectionDirectMembershipRule -CollectionId "%s" -ResourceId "%s"
57
+ COMMAND
58
+ :remove_device_from_collection => <<~COMMAND,
59
+ Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\\..\\ConfigurationManager.psd1"
60
+ Set-Location %s
61
+ Remove-CMDeviceCollectionDirectMembershipRule -CollectionId "%s" -ResourceId "%s" –Force
62
+ COMMAND
63
+ :create_deployment_package => <<~COMMAND,
64
+ $PSDefaultParameterValues =@{"get-wmiobject:namespace"="%s";"get-WMIObject:computername"="%s"}
65
+ $class = Get-WmiObject -Class SMS_SoftwareUpdatesPackage -List
66
+ $DeployPackage = $class.CreateInstance()
67
+ $DeployPackage.Name = "%s"
68
+ $DeployPackage.Description = "%s Updates"
69
+ $DeployPackage.PkgSourcePath = "%s"
70
+ $DeployPackage.PkgSourceFlag = [int32]2
71
+ $DeployPackage.put()
72
+ COMMAND
73
+ :delete_deployment_package => <<~COMMAND,
74
+ Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\\..\\ConfigurationManager.psd1"
75
+ Set-Location %s
76
+ Remove-CMSoftwareUpdateDeploymentPackage -Name %s -Force
77
+ COMMAND
78
+ :download_software_updates => <<~COMMAND,
79
+ Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\\..\\ConfigurationManager.psd1"
80
+ Set-Location %s
81
+ Save-CMSoftwareUpdate -DeploymentPackageName "%s" -SoftwareUpdateGroupName "%s" -SoftwareUpdateLanguage English
82
+ COMMAND
83
+ :deploy_package => <<~COMMAND,
84
+ Import-Module "$($ENV:SMS_ADMIN_UI_PATH)\\..\\ConfigurationManager.psd1"
85
+ Set-Location %s
86
+ New-CMSoftwareUpdateDeployment -SoftwareUpdateGroupName %s -CollectionName %s -DeploymentName %s -DeploymentType %s
87
+ COMMAND
88
+ }
89
+
90
+ def self.run(conn, cmd, *args)
91
+ unless @cmds.key?(cmd)
92
+ NexposeSCCM.logger.error("Invalid command supplied: #{cmd}")
93
+ end
94
+ cmd = @cmds[cmd] % [*args]
95
+ result = []
96
+ conn.shell(:powershell) do |shell|
97
+ err = nil
98
+ output = shell.run(cmd) do |response, stderr|
99
+ result << response
100
+ err = stderr
101
+ end
102
+ if output.exitcode != 0
103
+ NexposeSCCM.logger.error(err)
104
+ result = nil
105
+ break
106
+ end
107
+ end
108
+ result
109
+ end
110
+ end
111
+ end