Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "EncryptionStatus" to each volume on Windows #1238

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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