DSCResources/gcInSpec/gcInSpec.psm1
<# .SYNOPSIS Returns an object with details of InSpec installation .DESCRIPTION Queries WMI to get currently installed InSpec versions. Returns object with installation Status and versions. #> function Get-InstalledInSpecVersions { [cmdletbinding()] param( ) Write-Verbose "[$((get-date).getdatetimeformats()[45])] Checking for InSpec..." $installedInSpec = Get-CimInstance -ClassName win32_product -Filter "Name LIKE 'InSpec%'" $installedInSpec_Version = $installedInSpec.Version $installedInSpec = if ($null -eq $installedInSpec_Version) { $false } else { $true } $returnStatus = New-Object -TypeName PSObject -ArgumentList @{ Installed = $installedInSpec Version = $installedInSpec_Version } Write-Verbose "[$((get-date).getdatetimeformats()[45])] InSpec installed: $installedInSpec" Write-Verbose "[$((get-date).getdatetimeformats()[45])] InSpec versions: $installedInSpec_Version" return $returnStatus } <# .SYNOPSIS Download and install InSpec .DESCRIPTION Downloads the InSpec installation for Windows and installs it to the current directory. #> function Install-InSpec { [cmdletbinding()] param( [Parameter(Mandatory = $true)] [version]$InSpecVersion, [Parameter(Mandatory = $true)] [ValidateSet('2012r2','2016','2019')] # '2012r2' aligns to Windows 10 [string]$WindowsServerVersion ) $InSpecPackage_Version = "$($InSpecVersion.Major).$($InSpecVersion.Minor).$($InSpecVersion.Build)" # the url requires a revision number. an example would be '3.9.3.1'. let's set this if the user doesn't provide it since it is not included in the display text on the download page for InSpec. the first revision is '1'. $InSpecPackage_Name = "inspec-$InSpecPackage_Version$($InSpecVersion.Revision)-x64.msi" $InSpecDownloadUri = "https://packages.chef.io/files/stable/inspec/$InSpecPackage_Version/windows/$WindowsServerVersion/$InSpecPackage_Name" Write-Verbose "download url: $InSpecDownloadUri" $outFile = "$Env:TEMP/$InSpecPackage_Name" Write-Verbose "[$((get-date).getdatetimeformats()[45])] Downloading InSpec to $outFile" try { Invoke-WebRequest -Uri $InSpecDownloadUri -TimeoutSec 120 -OutFile $outFile } catch { throw "Error occured downloading InSpec from $InSpecDownloadUri" } $msiArguments = @( '/i' ('"{0}"' -f "$Env:TEMP/$InSpecPackage_Name") '/qn' "/L*v `"$Env:TEMP/$InSpecPackage_Name.log`"" ) Write-Verbose "[$((get-date).getdatetimeformats()[45])] Installing InSpec with arguments: $msiArguments" try { Start-Process -FilePath 'C:/Windows/System32/msiexec.exe' -ArgumentList $msiArguments -Wait -NoNewWindow } catch { throw "Error occured while installing InSpec from $($Env:TEMP/$InSpecPackage_Name)" } Write-Verbose "[$((get-date).getdatetimeformats()[45])] InSpec installation process ended" } <# .SYNOPSIS Runs InSpec with parameters .DESCRIPTION This function executes the .bat file provided with InSpec, using parameter input for the path to profiles and desitnation for json/cli output. #> function Invoke-InSpec { param( [Parameter(Mandatory = $true)] [string]$InSpecProfilePath, [string]$AttributesFilePath ) # InSpec prefers paths with no spaces # path to the InSpec bat file $InSpecExec_Path = "$env:SystemDrive/opscode/InSpec/bin/InSpec.bat" @" @ECHO OFF SET HOMEDRIVE=%SystemDrive% "%~dp0../embedded/bin/ruby.exe" "%~dpn0" %* "@ | Set-Content $InSpecExec_Path $profileName = (Get-ChildItem -Path $InSpecProfilePath).Parent.Name $InSpecExec_Arguements = @( "exec $InSpecProfilePath" "--reporter=json-min:$InSpecProfilePath$profileName.json cli:$InSpecProfilePath$profileName.cli" # the license accept parameter might have issues in some versions? it is not needed in 3.9.3. # "--chef-license=accept" ) # add attributes reference if input is provided if ('' -ne $AttributesFilePath) { $InSpecExec_Arguements += " --attrs $AttributesFilePath" } Write-Verbose "[$((get-date).getdatetimeformats()[45])] Starting the InSpec process with the command $InSpecExec_Path $InSpecExec_Arguements" Start-Process -FilePath $InSpecExec_Path -ArgumentList $InSpecExec_Arguements -Wait -NoNewWindow } <# .SYNOPSIS Creates a PowerShell object based on InSpec output. .DESCRIPTION Takes location of json-min and cli output files and converts the information to a PowerShell object with properties for use in the DSC resource. #> function ConvertFrom-InSpec { [cmdletbinding()] param( [Parameter(Mandatory = $true)] [string]$InSpecOutputPath ) $profileName = (Get-Item $InSpecOutputPath).Name $json = "$InSpecOutputPath$profileName.json" $cli = "$InSpecOutputPath$profileName.cli" # get JSON file containing InSpec output Write-Verbose "[$((get-date).getdatetimeformats()[45])] Reading json output from $InSpecOutputPath$profileName.json" $InSpecJson = Get-Content $json | ConvertFrom-Json # get CLI file containing InSpec output Write-Verbose "[$((get-date).getdatetimeformats()[45])] Reading cli output from $InSpecOutputPath$profileName.cli" [string]$InSpecCli = (Get-Content $cli) -replace '/x1b/[[0-9;]*m', '' # Reasons code/phrase for Get $Reasons = @() # results are compliant until a failed test is returned [bool]$profileCompliant = $true # loop through each control and create objects for the array; set compliance foreach ($control in $InSpecJson.controls) { Write-Verbose "[$((get-date).getdatetimeformats()[45])] Processing Reasons data for: $($control.code_desc)" [bool]$testCompliant = $true [bool]$testSkipped = $false Write-Verbose "[$((get-date).getdatetimeformats()[45])] Control Status: $($control.Status)" if ('failed' -eq $control.Status) { $profileCompliant = $false $testCompliant = $false } if ('skipped' -eq $control.Status) { $testSkipped = $true } } Write-Verbose "[$((get-date).getdatetimeformats()[45])] Overall Status: $($profileCompliant)" $Reasons += @{ Code = 'gcInSpec:gcInSpec:InSpecRawOutput' Phrase = $InSpecCli } $InSpec = @{ profileName = $profileName InSpecVersion = $InSpecJson.version Status = $profileCompliant Reasons = $Reasons } return $InSpec } function Get-TargetResource { [CmdletBinding()] [OutputType([Hashtable])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $InSpecProfileName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $InSpecVersion, [Parameter(Mandatory = $true)] [ValidateSet('2012r2','2016','2019')] # '2012r2' aligns to Windows 10 [string]$WindowsServerVersion ) Write-Verbose "[$((get-date).getdatetimeformats()[45])] required InSpec version: $InSpecVersion" $installedInSpec_Version = (Get-InstalledInSpecVersions).version if ($installedInSpec_Version -ne $InSpecVersion) { Install-InSpec $InSpecVersion $WindowsServerVersion } $InSpecProfile_Path = "$env:SystemDrive:/ProgramData/GuestConfig/Configuration/$InSpecProfileName/$InSpecProfileName/" Invoke-InSpec $InSpecProfile_Path $InSpec = ConvertFrom-InSpec $InSpecProfile_Path $get = @{ InSpecProfileName = $InSpecProfileName InSpecVersion = $installedInSpec_Version Status = $InSpec.Status Reasons = $InSpec.Reasons } return $get } function Test-TargetResource { [CmdletBinding()] [OutputType([Boolean])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $InSpecProfileName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $InSpecVersion, [Parameter(Mandatory = $true)] [ValidateSet('2012r2','2016','2019')] # '2012r2' aligns to Windows 10 [string]$WindowsServerVersion ) $Status = (Get-TargetResource -InSpecProfileName $InSpecProfileName -InSpecVersion $InSpecVersion -WindowsServerVersion $WindowsServerVersion).Status return $Status } function Set-TargetResource { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $InSpecProfileName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $InSpecVersion, [Parameter(Mandatory = $true)] [ValidateSet('2012r2','2016','2019')] # '2012r2' aligns to Windows 10 [string]$WindowsServerVersion ) throw 'Set functionality is not supported in this version of the DSC resource.' } |