Skip to content

Commit

Permalink
Add "EncryptionStatus" to each volume on Windows
Browse files Browse the repository at this point in the history
- Changes to fetch Encryption Status of a volume using GetConversionStatus method of Win32_EncryptableVolume class
- Handle wmi exceptions when a namespace or class is not accessible
- Added test cases
- Minor code fixes and cleanup
- Fixes MSYS-894

Signed-off-by: Nimesh <[email protected]>
  • Loading branch information
Nimesh-Msys committed Sep 7, 2018
1 parent 68db80a commit 489c34b
Show file tree
Hide file tree
Showing 2 changed files with 341 additions and 22 deletions.
137 changes: 115 additions & 22 deletions lib/ohai/plugins/windows/filesystem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,129 @@
Ohai.plugin(:Filesystem) do
provides "filesystem"

collect_data(:windows) do
# Volume encryption or decryption status
#
# @see https://docs.microsoft.com/en-us/windows/desktop/SecProv/getconversionstatus-win32-encryptablevolume#parameters
#
CONVERSION_STATUS = %w{FullyDecrypted FullyEncrypted EncryptionInProgress
DecryptionInProgress EncryptionPaused DecryptionPaused}.freeze

require "wmi-lite/wmi"
# Returns a Mash loaded with logical details
#
# Uses Win32_LogicalDisk and logical_properties to return encryption details of volumes.
#
# Returns an empty Mash in case of any WMI exception.
#
# @see https://docs.microsoft.com/en-us/windows/desktop/CIMWin32Prov/win32-logicaldisk
#
# @return [Mash]
#
def logical_info
wmi = WmiLite::Wmi.new("Root\\CIMV2")

fs = Mash.new
ld_info = Mash.new
# Note: we should really be parsing Win32_Volume and Win32_Mapped drive.
disks = wmi.instances_of("Win32_LogicalDisk")
logical_properties(disks)
rescue WmiLite::WmiException
Ohai::Log.debug("Unable to access Win32_LogicalDisk. Skipping logical details")
Mash.new
end

wmi = WmiLite::Wmi.new
# Returns a Mash loaded with encryption details
#
# Uses Win32_EncryptableVolume and encryption_properties to return encryption details of volumes.
#
# Returns an empty Mash in case of any WMI exception.
#
# @note We are fetching Encryption Status only as of now
#
# @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa376483(v=vs.85).aspx
#
# @return [Mash]
#
def encryptable_info
wmi = WmiLite::Wmi.new("Root\\CIMV2\\Security\\MicrosoftVolumeEncryption")
disks = wmi.instances_of("Win32_EncryptableVolume")
encryption_properties(disks)
rescue WmiLite::WmiException
Ohai::Log.debug("Unable to access Win32_EncryptableVolume. Skipping encryptable details")
Mash.new
end

# Grab filesystem data from WMI
# Note: we should really be parsing Win32_Volume and Win32_Mapped drive
disks = wmi.instances_of("Win32_LogicalDisk")
# Refines and calculates logical properties out of given instances
#
# @param [WmiLite::Wmi::Instance] disks
#
# @return [Mash] Each drive containing following properties:
#
# * :kb_size (Integer)
# * :kb_available (Integer)
# * :kb_used (Integer)
# * :percent_used (Integer)
# * :mount (String)
# * :fs_type (String)
# * :volume_name (String)
#
def logical_properties(disks)
properties = Mash.new
disks.each do |disk|
property = Mash.new
drive = disk["deviceid"]
property[:kb_size] = disk["size"].to_i / 1000
property[:kb_available] = disk["freespace"].to_i / 1000
property[:kb_used] = property[:kb_size] - property[:kb_available]
property[:percent_used] = (property[:kb_size] == 0 ? 0 : (property[:kb_used] * 100 / property[:kb_size]))
property[:mount] = disk["name"]
property[:fs_type] = disk["filesystem"].to_s.downcase
property[:volume_name] = disk["volumename"].to_s.downcase
properties[drive] = property
end
properties
end

# Refines and calculates encryption properties out of given instances
#
# @param [WmiLite::Wmi::Instance] disks
#
# @return [Mash] Each drive containing following properties:
#
# * :encryption_status (String)
#
def encryption_properties(disks)
properties = Mash.new
disks.each do |disk|
filesystem = disk["deviceid"]
fs[filesystem] = Mash.new
ld_info[filesystem] = Mash.new
disk.wmi_ole_object.properties_.each do |p|
ld_info[filesystem][p.name.wmi_underscore.to_sym] = disk[p.name.downcase]
drive = disk["driveletter"]
property = Mash.new
property[:encryption_status] = disk["conversionstatus"] ? CONVERSION_STATUS[disk["conversionstatus"]] : ""
properties[drive] = property
end
properties
end

# Merges all the various properties of filesystems
#
# @param [Array<Mash>] disks_info
# Array of the Mashes containing disk properties
#
# @return [Mash]
#
def merge_info(disks_info)
fs = Mash.new
disks_info.each do |info|
info.each do |disk, data|
if fs[disk]
fs[disk].merge!(data)
else
fs[disk] = data.dup
end
end
fs[filesystem][:kb_size] = ld_info[filesystem][:size].to_i / 1000
fs[filesystem][:kb_available] = ld_info[filesystem][:free_space].to_i / 1000
fs[filesystem][:kb_used] = fs[filesystem][:kb_size].to_i - fs[filesystem][:kb_available].to_i
fs[filesystem][:percent_used] = (fs[filesystem][:kb_size].to_i != 0 ? fs[filesystem][:kb_used].to_i * 100 / fs[filesystem][:kb_size].to_i : 0)
fs[filesystem][:mount] = ld_info[filesystem][:name]
fs[filesystem][:fs_type] = ld_info[filesystem][:file_system].downcase unless ld_info[filesystem][:file_system].nil?
fs[filesystem][:volume_name] = ld_info[filesystem][:volume_name]
end
fs
end

# Set the filesystem data
filesystem fs
collect_data(:windows) do
require "wmi-lite/wmi"
filesystem merge_info([logical_info,
encryptable_info])
end
end
226 changes: 226 additions & 0 deletions spec/unit/plugins/windows/filesystem_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#
# Author:: Nimesh Pathi <[email protected]>
# Copyright:: Copyright (c) 2018 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative "../../../spec_helper.rb"
require "wmi-lite/wmi"

describe Ohai::System, "Windows Filesystem Plugin", :windows_only do
let(:plugin) { get_plugin("windows/filesystem") }

let(:success) { true }

let(:logical_disks_instances) do
[
{
"caption" => "C:",
"deviceid" => "C:",
"size" => "10000000",
"filesystem" => "NTFS",
"freespace" => "100000",
"name" => "C:",
"volumename " => "",
},
{
"caption" => "D:",
"deviceid" => "D:",
"size" => "10000000",
"filesystem" => "FAT32",
"freespace" => "100000",
"name" => "D:"
# Lets not pass "volumename" for this drive
}
]
end

let(:encryptable_volume_instances) do
[
{
"conversionstatus" => 0,
"driveletter" => "C:",
},
{
"conversionstatus" => 2,
"driveletter" => "D:",
}
]
end

let(:wmi_exception) do
namespace = "Exception while testing"
exception = WIN32OLERuntimeError.new(namespace)
WmiLite::WmiException.new(exception, :ConnectServer, @namespace)
end

before(:each) do
allow(plugin).to receive(:collect_os).and_return(:windows)
end

describe "#logical_properties" do
let(:disks) { logical_disks_instances }
let(:logical_props) { %i{kb_size kb_available kb_used percent_used mount fs_type volume_name} }

it "Returns a mash" do
expect(plugin.logical_properties(disks)).to be_a(Mash)
end

it "Returns an empty mash when blank array is passed" do
expect(plugin.logical_properties([])).to be_a(Mash)
expect(plugin.logical_properties([])).to be_empty
end

it "Returns properties without values when there is no disk information" do
data = plugin.logical_properties([{}])
expect(data[nil].symbolize_keys.keys).to eq(logical_props)
expect(data[nil]["kb_used"]).to eq(0)
expect(data[nil]["fs_type"]).to be_empty
end

it "Refines required logical properties out of given instance" do
data = plugin.logical_properties(disks)
expect(data["C:"].symbolize_keys.keys).to eq(logical_props)
expect(data["D:"].symbolize_keys.keys).to eq(logical_props)
end

it "Calculates logical properties out of given instance" do
data = plugin.logical_properties(disks)
expect(data["C:"]["kb_used"]).to eq(data["D:"]["kb_used"]).and eq(9900)
expect(data["C:"]["percent_used"]).to eq(data["D:"]["percent_used"]).and eq(99)
expect(data["C:"]["fs_type"]).to eq("ntfs")
expect(data["D:"]["fs_type"]).to eq("fat32")
end
end

describe "#logical_info" do
it "Returns an empty mash when wmi namespace does not exists" do
allow(WmiLite::Wmi).to receive(:new).and_raise(wmi_exception)
expect(plugin.logical_info).to be_a(Mash)
expect(plugin.logical_info).to be_empty
end

it "Returns an empty mash when Win32_LogicalDisk could not be processed" do
allow(WmiLite::Wmi).to receive(:new).and_return(success)
allow(success)
.to receive(:instances_of)
.with("Win32_LogicalDisk")
.and_raise(wmi_exception)
expect(plugin.logical_info).to be_a(Mash)
expect(plugin.logical_info).to be_empty
end

it "Returns a Mash loaded with logical details" do
allow(WmiLite::Wmi).to receive(:new).and_return(success)
allow(success)
.to receive(:instances_of)
.with("Win32_LogicalDisk")
.and_return(logical_disks_instances)
expect(plugin.logical_info).to be_a(Mash)
expect(plugin.logical_info).not_to be_empty
end
end

describe "#encryption_properties" do
let(:disks) { encryptable_volume_instances }
let(:encryption_props) { [:encryption_status] }

it "Returns a mash" do
expect(plugin.encryption_properties(disks)).to be_a(Mash)
end

it "Returns an empty mash when blank array is passed" do
expect(plugin.encryption_properties([])).to be_a(Mash)
expect(plugin.encryption_properties([])).to be_empty
end

it "Returns properties without values when there is no disk information" do
data = plugin.encryption_properties([{}])
expect(data[nil].symbolize_keys.keys).to eq(encryption_props)
expect(data[nil]["encryption_status"]).to be_empty
end

it "Refines required encryption properties out of given instance" do
data = plugin.encryption_properties(disks)
expect(data["C:"].symbolize_keys.keys).to eq(encryption_props)
expect(data["D:"].symbolize_keys.keys).to eq(encryption_props)
end

it "Calculates encryption properties out of given instance" do
data = plugin.encryption_properties(disks)
expect(data["C:"]["encryption_status"]).to eq("FullyDecrypted")
expect(data["D:"]["encryption_status"]).to eq("EncryptionInProgress")
end
end

describe "#encryptable_info" do
it "Returns an empty mash when wmi namespace does not exists" do
allow(WmiLite::Wmi).to receive(:new).and_raise(wmi_exception)
expect(plugin.encryptable_info).to be_a(Mash)
expect(plugin.encryptable_info).to be_empty
end

it "Returns an empty mash when Win32_EncryptableVolume could not be processed" do
allow(WmiLite::Wmi).to receive(:new).and_return(success)
allow(success)
.to receive(:instances_of)
.with("Win32_EncryptableVolume")
.and_raise(wmi_exception)
expect(plugin.encryptable_info).to be_a(Mash)
expect(plugin.encryptable_info).to be_empty
end

it "Returns a Mash loaded with encryption details" do
allow(WmiLite::Wmi).to receive(:new).and_return(success)
allow(success)
.to receive(:instances_of)
.with("Win32_EncryptableVolume")
.and_return(encryptable_volume_instances)
expect(plugin.encryptable_info).to be_a(Mash)
expect(plugin.encryptable_info).not_to be_empty
end
end

describe "#merge_info" do
let(:info1) do
{ "drive1" => { "x" => 10, "y" => "test1" },
"drive2" => { "x" => 20, "z" => "test2" } }
end
let(:info2) do
{ "drive1" => { "k" => 10, "l" => "test1" },
"drive2" => { "l" => 20, "m" => "test2" } }
end
let(:info3) { { "drive1" => { "o" => 10, "p" => "test1" } } }
let(:info4) { { "drive2" => { "q" => 10, "r" => "test1" } } }

it "Returns an empty mash when no info is passed" do
expect(plugin.merge_info([])).to be_a(Mash)
expect(plugin.merge_info([])).to be_empty
end

it "Merges all the various properties of filesystems" do
expect(plugin.merge_info([info1, info2, info3, info4]))
.to eq("drive1" => { "x" => 10, "y" => "test1", "k" => 10, "l" => "test1", "o" => 10, "p" => "test1" },
"drive2" => { "x" => 20, "z" => "test2", "l" => 20, "m" => "test2", "q" => 10, "r" => "test1" })
end

it "Does not affect any core information after processing" do
expect(plugin.merge_info([info3, info4])).to eq("drive1" => { "o" => 10, "p" => "test1" },
"drive2" => { "q" => 10, "r" => "test1" })
expect(info3).to eq("drive1" => { "o" => 10, "p" => "test1" })
expect(info4).to eq("drive2" => { "q" => 10, "r" => "test1" })
end
end
end

0 comments on commit 489c34b

Please sign in to comment.