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,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