inspec 0.9.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.
Files changed (247) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +65 -0
  4. data/.travis.yml +23 -0
  5. data/CHANGELOG.md +38 -0
  6. data/Gemfile +33 -0
  7. data/LICENSE +201 -0
  8. data/MAINTAINERS.md +28 -0
  9. data/MAINTAINERS.toml +42 -0
  10. data/README.md +257 -0
  11. data/Rakefile +47 -0
  12. data/bin/inspec +109 -0
  13. data/docs/ctl_inspec.rst +195 -0
  14. data/docs/dsl_inspec.rst +182 -0
  15. data/docs/readme.rst +100 -0
  16. data/docs/resources.rst +4319 -0
  17. data/docs/template.rst +51 -0
  18. data/examples/test-kitchen/.kitchen.yml +20 -0
  19. data/examples/test-kitchen/Berksfile +3 -0
  20. data/examples/test-kitchen/Gemfile +21 -0
  21. data/examples/test-kitchen/README.md +27 -0
  22. data/examples/test-kitchen/metadata.rb +7 -0
  23. data/examples/test-kitchen/recipes/default.rb +6 -0
  24. data/examples/test-kitchen/recipes/nginx.rb +30 -0
  25. data/examples/test-kitchen/test/integration/default/web_spec.rb +28 -0
  26. data/inspec.gemspec +30 -0
  27. data/lib/inspec.rb +20 -0
  28. data/lib/inspec/backend.rb +42 -0
  29. data/lib/inspec/dsl.rb +151 -0
  30. data/lib/inspec/log.rb +34 -0
  31. data/lib/inspec/metadata.rb +79 -0
  32. data/lib/inspec/plugins.rb +9 -0
  33. data/lib/inspec/plugins/resource.rb +62 -0
  34. data/lib/inspec/profile.rb +138 -0
  35. data/lib/inspec/profile_context.rb +170 -0
  36. data/lib/inspec/resource.rb +76 -0
  37. data/lib/inspec/rspec_json_formatter.rb +27 -0
  38. data/lib/inspec/rule.rb +170 -0
  39. data/lib/inspec/runner.rb +154 -0
  40. data/lib/inspec/shell.rb +66 -0
  41. data/lib/inspec/targets.rb +9 -0
  42. data/lib/inspec/targets/core.rb +27 -0
  43. data/lib/inspec/targets/dir.rb +67 -0
  44. data/lib/inspec/targets/file.rb +29 -0
  45. data/lib/inspec/targets/folder.rb +43 -0
  46. data/lib/inspec/targets/tar.rb +34 -0
  47. data/lib/inspec/targets/url.rb +39 -0
  48. data/lib/inspec/targets/zip.rb +47 -0
  49. data/lib/inspec/version.rb +7 -0
  50. data/lib/matchers/matchers.rb +221 -0
  51. data/lib/resources/apache.rb +29 -0
  52. data/lib/resources/apache_conf.rb +113 -0
  53. data/lib/resources/apt.rb +140 -0
  54. data/lib/resources/audit_policy.rb +63 -0
  55. data/lib/resources/auditd_conf.rb +56 -0
  56. data/lib/resources/auditd_rules.rb +53 -0
  57. data/lib/resources/bond.rb +65 -0
  58. data/lib/resources/bridge.rb +114 -0
  59. data/lib/resources/command.rb +57 -0
  60. data/lib/resources/csv.rb +32 -0
  61. data/lib/resources/directory.rb +15 -0
  62. data/lib/resources/etc_group.rb +150 -0
  63. data/lib/resources/file.rb +110 -0
  64. data/lib/resources/gem.rb +46 -0
  65. data/lib/resources/group.rb +132 -0
  66. data/lib/resources/host.rb +143 -0
  67. data/lib/resources/inetd_conf.rb +56 -0
  68. data/lib/resources/interface.rb +127 -0
  69. data/lib/resources/iptables.rb +65 -0
  70. data/lib/resources/json.rb +64 -0
  71. data/lib/resources/kernel_module.rb +40 -0
  72. data/lib/resources/kernel_parameter.rb +55 -0
  73. data/lib/resources/limits_conf.rb +55 -0
  74. data/lib/resources/login_def.rb +60 -0
  75. data/lib/resources/mysql.rb +81 -0
  76. data/lib/resources/mysql_conf.rb +116 -0
  77. data/lib/resources/mysql_session.rb +52 -0
  78. data/lib/resources/npm.rb +44 -0
  79. data/lib/resources/ntp_conf.rb +58 -0
  80. data/lib/resources/oneget.rb +63 -0
  81. data/lib/resources/os.rb +22 -0
  82. data/lib/resources/os_env.rb +34 -0
  83. data/lib/resources/package.rb +169 -0
  84. data/lib/resources/parse_config.rb +75 -0
  85. data/lib/resources/passwd.rb +93 -0
  86. data/lib/resources/pip.rb +75 -0
  87. data/lib/resources/port.rb +296 -0
  88. data/lib/resources/postgres.rb +37 -0
  89. data/lib/resources/postgres_conf.rb +87 -0
  90. data/lib/resources/postgres_session.rb +59 -0
  91. data/lib/resources/processes.rb +57 -0
  92. data/lib/resources/registry_key.rb +54 -0
  93. data/lib/resources/script.rb +34 -0
  94. data/lib/resources/security_policy.rb +73 -0
  95. data/lib/resources/service.rb +379 -0
  96. data/lib/resources/ssh_conf.rb +75 -0
  97. data/lib/resources/user.rb +374 -0
  98. data/lib/resources/windows_feature.rb +77 -0
  99. data/lib/resources/yaml.rb +23 -0
  100. data/lib/resources/yum.rb +154 -0
  101. data/lib/utils/convert.rb +12 -0
  102. data/lib/utils/detect.rb +15 -0
  103. data/lib/utils/find_files.rb +36 -0
  104. data/lib/utils/hash.rb +13 -0
  105. data/lib/utils/modulator.rb +12 -0
  106. data/lib/utils/parser.rb +61 -0
  107. data/lib/utils/simpleconfig.rb +115 -0
  108. data/tasks/maintainers.rb +213 -0
  109. data/test/docker_run.rb +156 -0
  110. data/test/docker_test.rb +51 -0
  111. data/test/helper.rb +200 -0
  112. data/test/integration/.kitchen.yml +42 -0
  113. data/test/integration/Berksfile +4 -0
  114. data/test/integration/cookbooks/os_prepare/metadata.rb +8 -0
  115. data/test/integration/cookbooks/os_prepare/recipes/apt.rb +20 -0
  116. data/test/integration/cookbooks/os_prepare/recipes/default.rb +9 -0
  117. data/test/integration/cookbooks/os_prepare/recipes/file.rb +21 -0
  118. data/test/integration/cookbooks/os_prepare/recipes/package.rb +26 -0
  119. data/test/integration/default/_debug_spec.rb +1 -0
  120. data/test/integration/default/apt_spec.rb +42 -0
  121. data/test/integration/default/file_spec.rb +109 -0
  122. data/test/integration/default/group_spec.rb +32 -0
  123. data/test/integration/default/kernel_module_spec.rb +17 -0
  124. data/test/integration/default/kernel_parameter_spec.rb +56 -0
  125. data/test/integration/default/package_spec.rb +11 -0
  126. data/test/integration/default/service_spec.rb +28 -0
  127. data/test/integration/default/user_spec.rb +44 -0
  128. data/test/resource/command_test.rb +33 -0
  129. data/test/resource/dsl_test.rb +45 -0
  130. data/test/resource/file_test.rb +130 -0
  131. data/test/resource/ssh_config.rb +9 -0
  132. data/test/resource/sshd_config.rb +9 -0
  133. data/test/test-extra.yaml +11 -0
  134. data/test/test.yaml +11 -0
  135. data/test/unit/mock/cmd/Get-NetAdapter +24 -0
  136. data/test/unit/mock/cmd/GetUserAccount +33 -0
  137. data/test/unit/mock/cmd/GetWin32Group +23 -0
  138. data/test/unit/mock/cmd/PATH +1 -0
  139. data/test/unit/mock/cmd/Resolve-DnsName +26 -0
  140. data/test/unit/mock/cmd/Test-NetConnection +4 -0
  141. data/test/unit/mock/cmd/auditctl +7 -0
  142. data/test/unit/mock/cmd/auditpol +2 -0
  143. data/test/unit/mock/cmd/brew-info-jq +1 -0
  144. data/test/unit/mock/cmd/chage-l-root +7 -0
  145. data/test/unit/mock/cmd/dpkg-s-curl +21 -0
  146. data/test/unit/mock/cmd/dscl +5 -0
  147. data/test/unit/mock/cmd/etc-apt +7 -0
  148. data/test/unit/mock/cmd/find-etc-rc-d-name-S +12 -0
  149. data/test/unit/mock/cmd/find-net-interface +9 -0
  150. data/test/unit/mock/cmd/gem-list-local-a-q-rubocop +1 -0
  151. data/test/unit/mock/cmd/get-net-tcpconnection +24 -0
  152. data/test/unit/mock/cmd/get-netadapter-binding-bridge +4 -0
  153. data/test/unit/mock/cmd/get-package-firefox +30 -0
  154. data/test/unit/mock/cmd/get-package-ruby +18 -0
  155. data/test/unit/mock/cmd/get-service-dhcp +10 -0
  156. data/test/unit/mock/cmd/get-windows-feature +7 -0
  157. data/test/unit/mock/cmd/getent-hosts-example.com +1 -0
  158. data/test/unit/mock/cmd/getent-passwd-root +1 -0
  159. data/test/unit/mock/cmd/id-chartmann +1 -0
  160. data/test/unit/mock/cmd/id-root +1 -0
  161. data/test/unit/mock/cmd/initctl-show-config-ssh +3 -0
  162. data/test/unit/mock/cmd/initctl-status-ssh +1 -0
  163. data/test/unit/mock/cmd/iptables-s +6 -0
  164. data/test/unit/mock/cmd/launchctl-list +3 -0
  165. data/test/unit/mock/cmd/ls-1-etc-init.d +2 -0
  166. data/test/unit/mock/cmd/ls-sys-class-net-br +2 -0
  167. data/test/unit/mock/cmd/lsmod +2 -0
  168. data/test/unit/mock/cmd/lsof-np-itcp +4 -0
  169. data/test/unit/mock/cmd/netstat-tulpen +5 -0
  170. data/test/unit/mock/cmd/npm-ls-g--json-bower +9 -0
  171. data/test/unit/mock/cmd/pacman-qi-curl +21 -0
  172. data/test/unit/mock/cmd/ping-example.com +6 -0
  173. data/test/unit/mock/cmd/pip-show-jinja2 +11 -0
  174. data/test/unit/mock/cmd/ps-aux +3 -0
  175. data/test/unit/mock/cmd/pw-usershow-root-7 +1 -0
  176. data/test/unit/mock/cmd/reg_schedule +1 -0
  177. data/test/unit/mock/cmd/rpm-qia-curl +24 -0
  178. data/test/unit/mock/cmd/sbin_sysctl +1 -0
  179. data/test/unit/mock/cmd/secedit-export +7 -0
  180. data/test/unit/mock/cmd/service-e +2 -0
  181. data/test/unit/mock/cmd/service-sendmail-onestatus +3 -0
  182. data/test/unit/mock/cmd/service-sshd-status +1 -0
  183. data/test/unit/mock/cmd/sockstat +5 -0
  184. data/test/unit/mock/cmd/success +0 -0
  185. data/test/unit/mock/cmd/systemctl-show-all-sshd +6 -0
  186. data/test/unit/mock/cmd/win32_product +8 -0
  187. data/test/unit/mock/cmd/yum-repolist-all +52 -0
  188. data/test/unit/mock/files/auditd.conf +4 -0
  189. data/test/unit/mock/files/bond0 +37 -0
  190. data/test/unit/mock/files/etcgroup +3 -0
  191. data/test/unit/mock/files/example.csv +6 -0
  192. data/test/unit/mock/files/inetd.conf +2 -0
  193. data/test/unit/mock/files/kitchen.yml +7 -0
  194. data/test/unit/mock/files/limits.conf +5 -0
  195. data/test/unit/mock/files/login.defs +5 -0
  196. data/test/unit/mock/files/mysql.conf +8 -0
  197. data/test/unit/mock/files/mysql2.conf +2 -0
  198. data/test/unit/mock/files/ntp.conf +5 -0
  199. data/test/unit/mock/files/passwd +2 -0
  200. data/test/unit/mock/files/policyfile.lock.json +12 -0
  201. data/test/unit/mock/files/ssh_config +5 -0
  202. data/test/unit/mock/files/sshd_config +7 -0
  203. data/test/unit/mock/profiles/empty/metadata.rb +0 -0
  204. data/test/unit/mock/profiles/metadata/metadata.rb +1 -0
  205. data/test/unit/profile_context_test.rb +140 -0
  206. data/test/unit/profile_test.rb +49 -0
  207. data/test/unit/resources/apt_test.rb +46 -0
  208. data/test/unit/resources/audit_policy_test.rb +13 -0
  209. data/test/unit/resources/auditd_conf_test.rb +15 -0
  210. data/test/unit/resources/auditd_rules_test.rb +21 -0
  211. data/test/unit/resources/bond_test.rb +24 -0
  212. data/test/unit/resources/bridge_test.rb +56 -0
  213. data/test/unit/resources/csv_test.rb +35 -0
  214. data/test/unit/resources/etc_group_test.rb +37 -0
  215. data/test/unit/resources/gem_test.rb +20 -0
  216. data/test/unit/resources/group_test.rb +96 -0
  217. data/test/unit/resources/host_test.rb +38 -0
  218. data/test/unit/resources/inetd_conf_test.rb +15 -0
  219. data/test/unit/resources/interface_test.rb +54 -0
  220. data/test/unit/resources/iptables_test.rb +30 -0
  221. data/test/unit/resources/json_test.rb +36 -0
  222. data/test/unit/resources/kernel_module_test.rb +23 -0
  223. data/test/unit/resources/kernel_parameter_test.rb +13 -0
  224. data/test/unit/resources/limits_conf_test.rb +14 -0
  225. data/test/unit/resources/login_def_test.rb +16 -0
  226. data/test/unit/resources/mysql_conf_test.rb +14 -0
  227. data/test/unit/resources/npm_test.rb +20 -0
  228. data/test/unit/resources/ntp_conf_test.rb +16 -0
  229. data/test/unit/resources/oneget_test.rb +45 -0
  230. data/test/unit/resources/os_env_test.rb +13 -0
  231. data/test/unit/resources/package_test.rb +51 -0
  232. data/test/unit/resources/passwd_test.rb +24 -0
  233. data/test/unit/resources/pip_test.rb +15 -0
  234. data/test/unit/resources/port_test.rb +46 -0
  235. data/test/unit/resources/processes_test.rb +32 -0
  236. data/test/unit/resources/registry_key_test.rb +19 -0
  237. data/test/unit/resources/script_test.rb +19 -0
  238. data/test/unit/resources/security_policy_test.rb +16 -0
  239. data/test/unit/resources/service_test.rb +116 -0
  240. data/test/unit/resources/ssh_conf_test.rb +33 -0
  241. data/test/unit/resources/user_test.rb +93 -0
  242. data/test/unit/resources/windows_feature.rb +17 -0
  243. data/test/unit/resources/yaml_test.rb +34 -0
  244. data/test/unit/resources/yum_test.rb +68 -0
  245. data/test/unit/simpleconfig_test.rb +80 -0
  246. data/test/unit/utils/content_parser_test.rb +30 -0
  247. metadata +555 -0
@@ -0,0 +1,75 @@
1
+ # encoding: utf-8
2
+ # copyright: 2015, Vulcano Security GmbH
3
+ # author: Dominik Richter
4
+ # author: Christoph Hartmann
5
+ # license: All rights reserved
6
+
7
+ # Usage example:
8
+ #
9
+ # audit = command('/sbin/auditctl -l').stdout
10
+ # options = {
11
+ # assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
12
+ # multiple_values: true
13
+ # }
14
+ # describe parse_config(audit, options ) do
15
+
16
+ class PConfig < Inspec.resource(1)
17
+ name 'parse_config'
18
+
19
+ def initialize(content = nil, useropts = nil)
20
+ @opts = {}
21
+ @opts = useropts.dup unless useropts.nil?
22
+ @files_contents = {}
23
+ @params = nil
24
+
25
+ @content = content
26
+ read_content if @content.nil?
27
+ end
28
+
29
+ def method_missing(name)
30
+ @params || read_content
31
+ @params[name.to_s]
32
+ end
33
+
34
+ def parse_file(conf_path)
35
+ @conf_path = conf_path
36
+
37
+ # read the file
38
+ if !inspec.file(conf_path).file?
39
+ return skip_resource "Can't find file \"#{conf_path}\""
40
+ end
41
+ @content = read_file(conf_path)
42
+ if @content.empty? && inspec.file(conf_path).size > 0
43
+ return skip_resource "Can't read file \"#{conf_path}\""
44
+ end
45
+
46
+ read_content
47
+ end
48
+
49
+ def read_file(path)
50
+ @files_contents[path] ||= inspec.file(path).content
51
+ end
52
+
53
+ def read_content
54
+ # parse the file
55
+ @params = SimpleConfig.new(@content, @opts).params
56
+ @content
57
+ end
58
+
59
+ def to_s
60
+ "Parse Config #{@conf_path}"
61
+ end
62
+ end
63
+
64
+ class PConfigFile < PConfig
65
+ name 'parse_config_file'
66
+
67
+ def initialize(path, opts = nil)
68
+ super(nil, opts)
69
+ parse_file(path)
70
+ end
71
+
72
+ def to_s
73
+ "Parse Config File #{@conf_path}"
74
+ end
75
+ end
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+ # copyright: 2015, Vulcano Security GmbH
3
+ # author: Christoph Hartmann
4
+ # author: Dominik Richter
5
+ # license: All rights reserved
6
+
7
+ # The file format consists of
8
+ # - username
9
+ # - password
10
+ # - userid
11
+ # - groupid
12
+ # - user id info
13
+ # - home directory
14
+ # - command
15
+
16
+ # usage:
17
+ #
18
+ # describe passwd do
19
+ # its(:usernames) { should eq ['root'] }
20
+ # its(:uids) { should eq [0] }
21
+ # end
22
+ #
23
+ # describe passwd.uid(0) do
24
+ # its(:username) { should eq 'root' }
25
+ # its(:count) { should eq 1 }
26
+ # end
27
+
28
+ require 'utils/parser'
29
+
30
+ class Passwd < Inspec.resource(1)
31
+ name 'passwd'
32
+
33
+ include ContentParser
34
+
35
+ attr_reader :uid
36
+ attr_reader :parsed
37
+
38
+ def initialize(path = nil)
39
+ @path = path || '/etc/passwd'
40
+ @content = inspec.file(@path).content
41
+ @parsed = parse_passwd(@content)
42
+ end
43
+
44
+ # call passwd().uid(0)
45
+ # returns a seperate object with reference to this object
46
+ def uid(uid)
47
+ PasswdUid.new(self, uid)
48
+ end
49
+
50
+ def usernames
51
+ map_data('name')
52
+ end
53
+
54
+ def passwords
55
+ map_data('password')
56
+ end
57
+
58
+ def uids
59
+ map_data('uid')
60
+ end
61
+
62
+ def gids
63
+ map_data('gid')
64
+ end
65
+
66
+ def to_s
67
+ '/etc/passwd'
68
+ end
69
+
70
+ private
71
+
72
+ def map_data(id)
73
+ @parsed.map {|x|
74
+ x[id]
75
+ }
76
+ end
77
+ end
78
+
79
+ # object that hold a specifc uid view on passwd
80
+ class PasswdUid
81
+ def initialize(passwd, uid)
82
+ @passwd = passwd
83
+ @users = @passwd.parsed.select { |x| x['uid'] == "#{uid}" }
84
+ end
85
+
86
+ def username
87
+ @users.at(0)['name']
88
+ end
89
+
90
+ def count
91
+ @users.size
92
+ end
93
+ end
@@ -0,0 +1,75 @@
1
+ # encoding: utf-8
2
+ # author: Christoph Hartmann
3
+ # author: Dominik Richter
4
+
5
+ # Usage:
6
+ # describe pip('Jinja2') do
7
+ # it { should be_installed }
8
+ # end
9
+ #
10
+ class PipPackage < Inspec.resource(1)
11
+ name 'pip'
12
+
13
+ def initialize(package_name)
14
+ @package_name = package_name
15
+ end
16
+
17
+ def info
18
+ return @info if defined?(@info)
19
+
20
+ @info = {}
21
+ @info[:type] = 'pip'
22
+ cmd = inspec.command("#{pip_cmd} show #{@package_name}")
23
+ return @info if cmd.exit_status != 0
24
+
25
+ params = SimpleConfig.new(
26
+ cmd.stdout,
27
+ assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
28
+ multiple_values: false,
29
+ ).params
30
+ @info[:name] = params['Name']
31
+ @info[:version] = params['Version']
32
+ @info[:installed] = true
33
+ @info
34
+ end
35
+
36
+ def installed?
37
+ info[:installed] == true
38
+ end
39
+
40
+ def version
41
+ info[:version]
42
+ end
43
+
44
+ def to_s
45
+ "Pip Package #{@package_name}"
46
+ end
47
+
48
+ private
49
+
50
+ def pip_cmd
51
+ # Pip is not on the default path for Windows, therefore we do some logic
52
+ # to find the binary on Windows
53
+ family = inspec.os[:family]
54
+ case family
55
+ when 'windows'
56
+ # we need to detect the pip command on Windows
57
+ cmd = inspec.command('New-Object -Type PSObject | Add-Member -MemberType NoteProperty -Name Pip -Value (Invoke-Command -ScriptBlock {where.exe pip}) -PassThru | Add-Member -MemberType NoteProperty -Name Python -Value (Invoke-Command -ScriptBlock {where.exe python}) -PassThru | ConvertTo-Json')
58
+ begin
59
+ paths = JSON.parse(cmd.stdout)
60
+ # use pip if it on system path
61
+ pipcmd = paths['Pip']
62
+ # calculate path on windows
63
+ if defined?(paths['Python']) && pipcmd.nil?
64
+ pipdir = paths['Python'].split('\\')
65
+ # remove python.exe
66
+ pipdir.pop
67
+ pipcmd = pipdir.push('Scripts').push('pip.exe').join('/')
68
+ end
69
+ rescue JSON::ParserError => _e
70
+ return nil
71
+ end
72
+ end
73
+ pipcmd || 'pip'
74
+ end
75
+ end
@@ -0,0 +1,296 @@
1
+ # encoding: utf-8
2
+ # author: Christoph Hartmann
3
+ # author: Dominik Richter
4
+
5
+ # Usage:
6
+ # describe port(80) do
7
+ # it { should be_listening }
8
+ # its('protocol') {should eq 'tcp'}
9
+ # end
10
+ #
11
+ # not supported serverspec syntax
12
+ # describe port(80) do
13
+ # it { should be_listening.with('tcp') }
14
+ # end
15
+ #
16
+ # TODO: currently we return local ip only
17
+ # TODO: improve handling of same port on multiple interfaces
18
+ class Port < Inspec.resource(1)
19
+ name 'port'
20
+
21
+ def initialize(port)
22
+ @port = port
23
+ @port_manager = nil
24
+ @cache = nil
25
+
26
+ case inspec.os[:family]
27
+ when 'ubuntu', 'debian', 'redhat', 'fedora', 'arch'
28
+ @port_manager = LinuxPorts.new(inspec)
29
+ when 'darwin'
30
+ @port_manager = DarwinPorts.new(inspec)
31
+ when 'windows'
32
+ @port_manager = WindowsPorts.new(inspec)
33
+ when 'freebsd'
34
+ @port_manager = FreeBsdPorts.new(inspec)
35
+ else
36
+ return skip_resource 'The `port` resource is not supported on your OS yet.'
37
+ end
38
+ end
39
+
40
+ def listening?(_protocol = nil, _local_address = nil)
41
+ info.size > 0
42
+ end
43
+
44
+ def protocol
45
+ res = info.map { |x| x[:protocol] }.uniq.compact
46
+ res.size > 0 ? res : nil
47
+ end
48
+
49
+ def process
50
+ res = info.map { |x| x[:process] }.uniq.compact
51
+ res.size > 0 ? res : nil
52
+ end
53
+
54
+ def pid
55
+ res = info.map { |x| x[:pid] }.uniq.compact
56
+ res.size > 0 ? res : nil
57
+ end
58
+
59
+ def to_s
60
+ "Port #{@port}"
61
+ end
62
+
63
+ private
64
+
65
+ def info
66
+ return @cache if !@cache.nil?
67
+ # abort if os detection has not worked
68
+ return @cache = [] if @port_manager.nil?
69
+ # query ports
70
+ ports = @port_manager.info || []
71
+ @cache = ports.select { |p| p[:port] == @port }
72
+ end
73
+ end
74
+
75
+ # implements an info method and returns all ip adresses and protocols for
76
+ # each port
77
+ # [{
78
+ # port: 80,
79
+ # address: [{
80
+ # ip: '0.0.0.0'
81
+ # protocol: 'tcp'
82
+ # }],
83
+ # }]
84
+ class PortsInfo
85
+ attr_reader :inspec
86
+ def initialize(inspec)
87
+ @inspec = inspec
88
+ end
89
+ end
90
+
91
+ # TODO: Add UDP infromation Get-NetUDPEndpoint
92
+ # TODO: currently Windows only supports tcp ports
93
+ # TODO: Get-NetTCPConnection does not return PIDs
94
+ # TODO: double-check output with 'netstat -ano'
95
+ # @see https://connect.microsoft.com/PowerShell/feedback/details/1349420/get-nettcpconnection-does-not-show-processid
96
+ class WindowsPorts < PortsInfo
97
+ def info
98
+ # get all port information
99
+ cmd = inspec.command('Get-NetTCPConnection | Select-Object -Property State, Caption, Description, LocalAddress, LocalPort, RemoteAddress, RemotePort, DisplayName, Status | ConvertTo-Json')
100
+
101
+ begin
102
+ ports = JSON.parse(cmd.stdout)
103
+ rescue JSON::ParserError => _e
104
+ return nil
105
+ end
106
+
107
+ return nil if ports.nil?
108
+
109
+ ports.map { |x|
110
+ {
111
+ port: x['LocalPort'],
112
+ address: x['LocalAddress'],
113
+ protocol: 'tcp',
114
+ process: nil,
115
+ pid: nil,
116
+ }
117
+ }
118
+ end
119
+ end
120
+
121
+ # extracts udp and tcp ports from macos
122
+ class DarwinPorts < PortsInfo
123
+ def info
124
+ # collects UDP and TCP information
125
+ cmd = inspec.command('lsof -nP -iTCP -iUDP -sTCP:LISTEN')
126
+ return nil if cmd.exit_status.to_i != 0
127
+
128
+ ports = []
129
+ # split on each newline
130
+ cmd.stdout.each_line do |line|
131
+ # parse each line
132
+ # 1 - COMMAND, 2 - PID, 3 - USER, 4 - FD, 5 - TYPE, 6 - DEVICE, 7 - SIZE/OFF, 8 - NODE, 9 - NAME
133
+ parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*$/.match(line)
134
+ # extract network info
135
+ net_addr = parsed[9].split(':')
136
+ # convert to number if possible
137
+ net_port = net_addr[1]
138
+ net_port = net_port.to_i if /^\d+$/.match(net_port)
139
+ protocol = parsed[8].downcase
140
+
141
+ # add version to protocol
142
+ type = parsed[5].downcase
143
+ protocol += '6' if type == 'IPv6'
144
+
145
+ # map data
146
+ port_info = {
147
+ port: net_port,
148
+ address: net_addr[0],
149
+ protocol: protocol,
150
+ process: parsed[1],
151
+ pid: parsed[2].to_i,
152
+ }
153
+
154
+ # push data, if not headerfile
155
+ ports.push(port_info) if %w{tcp tcp6 udp udp6}.include?(protocol)
156
+ end
157
+ ports
158
+ end
159
+ end
160
+
161
+ # extract port information from netstat
162
+ class LinuxPorts < PortsInfo
163
+ def info
164
+ cmd = inspec.command('netstat -tulpen')
165
+ return nil if cmd.exit_status.to_i != 0
166
+
167
+ ports = []
168
+ # parse all lines
169
+ cmd.stdout.each_line do |line|
170
+ port_info = parse_netstat_line(line)
171
+
172
+ # only push protocols we are interested in
173
+ next unless %w{tcp tcp6 udp udp6}.include?(port_info[:protocol])
174
+ ports.push(port_info)
175
+ end
176
+ ports
177
+ end
178
+
179
+ def parse_net_address(net_addr, protocol)
180
+ if protocol.eql?('tcp6') || protocol.eql?('udp6')
181
+ # prep for URI parsing, parse ip6 port
182
+ ip6 = /^(\S+:)(\d+)$/.match(net_addr)
183
+ ip6addr = ip6[1]
184
+ ip6addr = '::' if /^:::$/.match(ip6addr)
185
+ # build uri
186
+ ip_addr = URI("addr://[#{ip6addr}]:#{ip6[2]}")
187
+ # replace []
188
+ host = ip_addr.host[1..ip_addr.host.size-2]
189
+ port = ip_addr.port
190
+ else
191
+ ip_addr = URI('addr://'+net_addr)
192
+ host = ip_addr.host
193
+ port = ip_addr.port
194
+ end
195
+ [host, port]
196
+ end
197
+
198
+ def parse_netstat_line(line)
199
+ # parse each line
200
+ # 1 - Proto, 2 - Recv-Q, 3 - Send-Q, 4 - Local Address, 5 - Foreign Address, 6 - State, 7 - Inode, 8 - PID/Program name
201
+ parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/.match(line)
202
+ return {} if parsed.nil?
203
+
204
+ # parse ip4 and ip6 addresses
205
+ protocol = parsed[1].downcase
206
+ host, port = parse_net_address(parsed[4], protocol)
207
+
208
+ # extract PID
209
+ process = parsed[9].split('/')
210
+ pid = process[0]
211
+ pid = pid.to_i if /^\d+$/.match(pid)
212
+ process = process[1]
213
+
214
+ # map data
215
+ {
216
+ port: port,
217
+ address: host,
218
+ protocol: protocol,
219
+ process: process,
220
+ pid: pid,
221
+ }
222
+ end
223
+ end
224
+
225
+ # extracts information from sockstat
226
+ class FreeBsdPorts < PortsInfo
227
+ def info
228
+ cmd = inspec.command('sockstat -46l')
229
+ return nil if cmd.exit_status.to_i != 0
230
+
231
+ ports = []
232
+ # split on each newline
233
+ cmd.stdout.each_line do |line|
234
+ port_info = parse_sockstat_line(line)
235
+
236
+ # push data, if not headerfile
237
+ next unless %w{tcp tcp6 udp udp6}.include?(port_info[:protocol])
238
+ ports.push(port_info)
239
+ end
240
+ ports
241
+ end
242
+
243
+ def parse_net_address(net_addr, protocol)
244
+ case protocol
245
+ when 'tcp4', 'udp4'
246
+ # replace * with 0.0.0.0
247
+ net_addr = net_addr.gsub(/^\*:/, '0.0.0.0:') if /^*:(\d+)$/.match(net_addr)
248
+ ip_addr = URI('addr://'+net_addr)
249
+ host = ip_addr.host
250
+ port = ip_addr.port
251
+ when 'tcp6', 'udp6'
252
+ return [] if net_addr == '*:*' # abort for now
253
+ # replace * with 0:0:0:0:0:0:0:0
254
+ net_addr = net_addr.gsub(/^\*:/, '0:0:0:0:0:0:0:0:') if /^*:(\d+)$/.match(net_addr)
255
+ # extract port
256
+ ip6 = /^(\S+):(\d+)$/.match(net_addr)
257
+ ip6addr = ip6[1]
258
+ ip_addr = URI("addr://[#{ip6addr}]:#{ip6[2]}")
259
+ # replace []
260
+ host = ip_addr.host[1..ip_addr.host.size-2]
261
+ port = ip_addr.port
262
+ end
263
+ [host, port]
264
+ end
265
+
266
+ def parse_sockstat_line(line)
267
+ # 1 - USER, 2 - COMMAND, 3 - PID, 4 - FD 5 - PROTO, 6 - LOCAL ADDRESS, 7 - FOREIGN ADDRESS
268
+ parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/.match(line)
269
+ return {} if parsed.nil?
270
+
271
+ # extract ip information
272
+ protocol = parsed[5].downcase
273
+ host, port = parse_net_address(parsed[6], protocol)
274
+ return {} if host.nil? or port.nil?
275
+
276
+ # extract process
277
+ process = parsed[2]
278
+
279
+ # extract PID
280
+ pid = parsed[3]
281
+ pid = pid.to_i if /^\d+$/.match(pid)
282
+
283
+ # map tcp4 and udp4
284
+ protocol = 'tcp' if protocol.eql?('tcp4')
285
+ protocol = 'udp' if protocol.eql?('udp4')
286
+
287
+ # map data
288
+ {
289
+ port: port,
290
+ address: host,
291
+ protocol: protocol,
292
+ process: process,
293
+ pid: pid,
294
+ }
295
+ end
296
+ end