DSCResources/gcInSpec/gcInSpec.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286

<#
    .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.'
}