Module/STIG/Convert/Functions.PowerStigXml.ps1

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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
#region Main Function
<#
    .SYNOPSIS
        Identifies the type of STIG that has been input and selects the proper private function to
        further convert the STIG strings into usable objects.
 
    .DESCRIPTION
        This function enables the core translation of the raw xccdf file by reading the benchmark
        title property to determine where to send the data for processing.
 
        When a ruleset match is found, the xccdf data is sent to private functions that are
        dedicated to processing individual STIG setting types, such as registry settings or
        security policy.
 
        If the function is unable to find a rule set match, an error is returned.
 
    .PARAMETER Path
        The path to the xccdf file to be processed.
 
    .PARAMETER IncludeRawString
        This will add the 'Check-Content' from the xcccdf to the output for any additional validation
        or spot checking that may be needed.
 
    .PARAMETER RuleIdFilter
        Filters the list rules that are converted to simplify debugging the conversion process.
 
    .EXAMPLE
        ConvertFrom-StigXccdf -Path C:\Stig\U_Windows_2012_and_2012_R2_MS_STIG_V2R8_Manual-xccdf.xml
 
    .OUTPUTS
        Custom objects are created from the STIG base class that are provided in the module
 
    .NOTES
        This is an ongoing project that should be retested with each iteration of the STIG. This is
        due to the non-standard way, the content is published. Each version of the STIG may require
        a rule to be updated to account for a new string format. All the formatting rules are heavily
        tested, so making changes is a simple task.
 
    .LINK
        http://iase.disa.mil/stigs/Lists/stigs-masterlist/AllItems.aspx
#>

function ConvertFrom-StigXccdf
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string[]]
        $RuleIdFilter
    )

    # Get the xml data from the file path provided.
    $stigBenchmarkXml = Get-StigXccdfBenchmarkContent -Path $path

    # Global variable needed to distinguish between the IIS server and site stigs. Server Stig needs xIISLogging resource, Site Stig needs xWebsite
    $global:stigTitle = $stigBenchmarkXml.title

    # Global variable needed to set and get specific logic needed for filtering and parsing FileContentRules
    switch ($true)
    {
        {$global:stigXccdfName -and -join ((Split-Path -Path $path -Leaf).Split('_') | Select-Object -Index (1, 2)) -eq ''}
        {
            break;
        }
        {!$global:stigXccdfName -or $global:stigXccdfName -ne -join ((Split-Path -Path $path -Leaf).Split('_') | Select-Object -Index (1, 2))}
        {
            $global:stigXccdfName = -join ((Split-Path -Path $path -Leaf).Split('_') | Select-Object -Index (1, 2))
            break;
        }
    }
    # Read in the root stig data from the xml additional functions will dig in deeper
    $stigRuleParams = @{
        StigGroupListChangeLog = Get-RuleChangeLog -Path $Path
    }

    if ($RuleIdFilter)
    {
        $stigRuleParams.StigGroupList = $stigBenchmarkXml.Group | Where-Object {$RuleIdFilter -contains $PSItem.Id}
    }
    else
    {
        $stigRuleParams.StigGroupList = $stigBenchmarkXml.Group
    }

    # The benchmark title drives the rest of the function and must exist to continue.
    if ( $null -eq $stigBenchmarkXml.title )
    {
        Write-Error -Message 'The Benchmark title property is null. Unable to determine ruleset target.'
        return
    }

    Get-RegistryRuleExpressions -Path $Path -StigBenchmarkXml $stigBenchmarkXml

    return Get-StigRuleList @stigRuleParams
}

<#
    .SYNOPSIS
        Loads the regular expressions files
 
    .DESCRIPTION
        This function loads the regular expression sets to process registry rules in the xccdf file.
 
    .PARAMETER Path
        The path to the xccdf file to be processed.
 
    .PARAMETER StigBenchmarkXml
        The xml for the xccdf file to be processed.
#>

function Get-RegistryRuleExpressions
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter(Mandatory = $true)]
        [object]
        $StigBenchmarkXml
    )

    begin
    {
        # Use $stigBenchmarkXml.id to determine the stig file
        $benchmarkId = Split-BenchmarkId -Id $StigBenchmarkXml.id -FilePath $Path
        if ([string]::IsNullOrEmpty($benchmarkId.TechnologyRole))
        {
            $benchmarkId.TechnologyRole = $StigBenchmarkXml.id
        }

        # Handles testing and production
        $xccdfFileName = Split-Path $Path -Leaf
        $spInclude = @('Data.Core.ps1')
        if ($xccdfFileName -eq 'TextData.xml')
        {
            # Query TechnologyRole and map to file
            $officeApps = @('Outlook', 'Excel', 'PowerPoint', 'Word')
            $mcafeeApps = @('VirusScan')
            $spExclude = @($MyInvocation.MyCommand.Name, 'Template.*.txt', 'Data.ps1', 'Functions.*.ps1', 'Methods.ps1')

            switch ($benchmarkId.TechnologyRole)
            {
                { $null -ne ($officeApps | Where-Object { $benchmarkId.TechnologyRole -match $_ }) }
                {
                    $spInclude += "Data.Office.ps1"
                }
                { $null -ne ($mcafeeApps | Where-Object { $benchmarkId.TechnologyRole -match $_ }) }
                {
                    $spInclude += "Data.Mcafee.ps1"
                }
            }
        }
        else
        {
            # Query directory of xccdf file
            $spResult = Split-Path (Split-Path $Path -Parent) -Leaf
            if ($spResult)
            {
                $spInclude += "Data." + $spResult + ".ps1"
            }
        }
    }

    process
    {
        # Load specific and core expression sets
        $childItemParams = @{
            Path = "$PSScriptRoot\..\..\Rule\Convert"
            Exclude = $spExclude
            Include = $spInclude
            Recurse = $true
        }

        $spSupportFileList = Get-ChildItem @childItemParams | Sort-Object -Descending
        Clear-Variable SingleLine* -Scope Global
        foreach ($supportFile in $spSupportFileList)
        {
            Write-Verbose "Loading $($supportFile.FullName)"
            . $supportFile.FullName
        }
    }
}

<#
    .SYNOPSIS
        This function generates a new xml file based on the convert objects from ConvertFrom-StigXccdf.
    .PARAMETER Path
        The full path to the xccdf to convert.
    .PARAMETER Destination
        The full path to save the converted xml to.
    .PARAMETER CreateOrgSettingsFile
        Creates the orginazational settings files associated with the version of the STIG.
    .PARAMETER DoNotExportRawString
        Excludes the check-content elemet content from the converted object.
    .PARAMETER RuleIdFilter
        Filters the list rules that are converted to simplify debugging the conversion process.
    .PARAMETER DoNotExportDescription
        Excludes the Description elemet content from the converted object.
#>

function ConvertTo-PowerStigXml
{
    [CmdletBinding()]
    [OutputType([xml])]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $Path,

        [Parameter()]
        [string]
        $Destination,

        [Parameter()]
        [switch]
        $CreateOrgSettingsFile,

        [Parameter()]
        [switch]
        $DoNotExportRawString,

        [Parameter()]
        [string[]]
        $RuleIdFilter,

        [Parameter()]
        [switch]
        $DoNotExportDescription
    )

    begin
    {
        $CurrentVerbosePreference = $global:VerbosePreference

        if ($PSBoundParameters.ContainsKey('Verbose'))
        {
            $global:VerbosePreference = 'Continue'
        }
    }
    process
    {
        $convertedStigObjects = ConvertFrom-StigXccdf -Path $Path -RuleIdFilter $RuleIdFilter

        # Get the raw xccdf xml to pull additional details from the root node.
        [xml] $xccdfXml = Get-Content -Path $Path -Encoding UTF8
        [version] $stigVersionNumber = Get-StigVersionNumber -StigDetails $xccdfXml

        $ruleTypeList = Get-RuleTypeList -StigSettings $convertedStigObjects

        # Start the XML doc and add the root element
        $xmlDocument = [System.XML.XMLDocument]::New()
        [System.XML.XMLElement] $xmlRoot = $xmlDocument.CreateElement( 'DISASTIG' )

        <#
            Append as child to an existing node. This method will 'leak' an object out of the function
            so DO NOT remove the [void]
        #>

        [void] $xmlDocument.appendChild( $xmlRoot )
        $xmlRoot.SetAttribute( 'version' , $xccdfXml.Benchmark.version )
        $xmlRoot.SetAttribute( 'classification', 'UNCLASSIFIED' )
        $xmlRoot.SetAttribute( 'customname' , '' )
        $xmlRoot.SetAttribute( 'stigid' , $xccdfXml.Benchmark.ID )
        $xmlRoot.SetAttribute( 'description' , $xccdfXml.Benchmark.description )
        $xmlRoot.SetAttribute( 'filename' , (Split-Path -Path $Path -Leaf) )
        $xmlRoot.SetAttribute( 'releaseinfo' , $xccdfXml.Benchmark.'plain-text'.InnerText )
        $xmlRoot.SetAttribute( 'title' , $xccdfXml.Benchmark.title )
        $xmlRoot.SetAttribute( 'notice' , $xccdfXml.Benchmark.notice.Id )
        $xmlRoot.SetAttribute( 'source' , $xccdfXml.Benchmark.reference.source )
        $xmlRoot.SetAttribute( 'fullversion', $stigVersionNumber )
        $xmlRoot.SetAttribute( 'created', $(Get-Date).ToShortDateString() )

        # Add the STIG types as child elements
        foreach ( $ruleType in $ruleTypeList )
        {
            # Create the rule type node
            [System.XML.XMLElement] $xmlRuleType = $xmlDocument.CreateElement( $ruleType )

            # Append as child to an existing node. DO NOT remove the [void]
            [void] $xmlRoot.appendChild( $xmlRuleType )
            $XmlRuleType.SetAttribute( $xmlattribute.ruleDscResourceModule, $dscResourceModule.$ruleType )

            # Get the rules for the current STIG type.
            $rules = $convertedStigObjects | Where-Object { $PSItem.GetType().ToString() -eq $ruleType }

            # Get the list of properties of the current object type to use as child elements
            [System.Collections.ArrayList] $properties = $rules |
                Get-Member |
                Where-Object MemberType -eq Property |
                Select-Object Name -ExpandProperty Name

            <#
                The $properties array is used to set the child elements of the rule. Remove the base
                class properties from the array list that we do not want added as child elements.
            #>

            $propertiesToRemove = @($xmlattribute.ruleId, $xmlattribute.ruleSeverity,
                $xmlattribute.ruleConversionStatus, $xmlattribute.ruleTitle,
                $xmlattribute.ruleDscResource)

            <#
                Because the Remove method on an array is case sensitive and the properties names
                in $propertiesToRemove are in different case from $properties we use the -in comparison
                operator to filter and return the proper case
            #>

            $propertiesToRemove = $properties | Where-Object -FilterScript {$PSItem -in $propertiesToRemove}

            ### [TODO] ###
            <#
                Remove the Description if explicited requested. Once all PowerSTIG
                data files are updated with the description attribute, this and
                the $DoNotExportDescription can be removed from the function. This
                field is used to automatically generate a populated STIG checklist.
            #>

            if ( $DoNotExportDescription )
            {
                $propertiesToRemove += 'Description'
            }
            ### END TODO ###

            # Remove the raw string from the output if it was not requested.
            if ( $DoNotExportRawString )
            {
                $propertiesToRemove += 'RawString'
            }

            # These properties are removed becasue they are attributes of the object, not elements
            foreach ( $propertyToRemove in $propertiesToRemove )
            {
                [void] $properties.Remove( $propertyToRemove )
            }

            # Add the STIG details to the xml document.
            foreach ( $rule in $rules )
            {
                # Replace TAB(s) with 3 spaces in rule.description before adding to xml document.
                $rule.Description = $rule.Description -replace("`t"," ")

                [System.XML.XMLElement] $xmlRuleTypeProperty = $xmlDocument.CreateElement( 'Rule' )
                # Append as child to an existing node. DO NOT remove the [void]
                [void] $xmlRuleType.appendChild( $xmlRuleTypeProperty )
                # Set the base class properties
                $xmlRuleTypeProperty.SetAttribute( $xmlattribute.ruleId, $rule.ID )
                $xmlRuleTypeProperty.SetAttribute( $xmlattribute.ruleSeverity, $rule.severity )
                $xmlRuleTypeProperty.SetAttribute( $xmlattribute.ruleConversionStatus, $rule.conversionstatus )
                $xmlRuleTypeProperty.SetAttribute( $xmlattribute.ruleTitle, $rule.title )
                $xmlRuleTypeProperty.SetAttribute( $xmlattribute.ruleDscResource, $rule.dscresource )

                foreach ( $property in $properties )
                {
                    [System.XML.XMLElement] $xmlRuleTypePropertyUnique = $xmlDocument.CreateElement( $property )
                    # Append as child to an existing node. DO NOT remove the [void]
                    [void] $xmlRuleTypeProperty.appendChild( $xmlRuleTypePropertyUnique )

                    # Skip any blank vaules
                    if ($null -eq $rule.$property)
                    {
                        continue
                    }
                    <#
                        The Permission rule returns an ACE list that needs to be serialized on a second
                        level. This will pick that up and expand the object in the xml.
                    #>

                    if ($property -eq 'AccessControlEntry')
                    {
                        foreach ($ace in $rule.$property)
                        {
                            [System.XML.XMLElement] $aceEntry = $xmlDocument.CreateElement( 'Entry' )
                            [void] $xmlRuleTypePropertyUnique.appendChild( $aceEntry )

                            # Add the ace entry Type
                            [System.XML.XMLElement] $aceEntryType = $xmlDocument.CreateElement( 'Type' )
                            [void] $aceEntry.appendChild( $aceEntryType )
                            $aceEntryType.InnerText = $ace.Type

                            # Add the ace entry Principal
                            [System.XML.XMLElement] $aceEntryPrincipal = $xmlDocument.CreateElement( 'Principal' )
                            [void] $aceEntry.appendChild( $aceEntryPrincipal )
                            $aceEntryPrincipal.InnerText = $ace.Principal

                            # Add the ace entry Principal
                            [System.XML.XMLElement] $aceEntryForcePrincipal = $xmlDocument.CreateElement( 'ForcePrincipal' )
                            [void] $aceEntry.appendChild( $aceEntryForcePrincipal )
                            $aceEntryForcePrincipal.InnerText = $ace.ForcePrincipal

                            # Add the ace entry Inheritance flag
                            [System.XML.XMLElement] $aceEntryInheritance = $xmlDocument.CreateElement( 'Inheritance' )
                            [void] $aceEntry.appendChild( $aceEntryInheritance )
                            $aceEntryInheritance.InnerText = $ace.Inheritance

                            # Add the ace entery FileSystemRights
                            [System.XML.XMLElement] $aceEntryRights = $xmlDocument.CreateElement( 'Rights' )
                            [void] $aceEntry.appendChild( $aceEntryRights )
                            $aceEntryRights.InnerText = $ace.Rights
                        }
                    }
                    elseif ($property -eq 'LogCustomFieldEntry')
                    {
                        foreach ($entry in $rule.$property)
                        {
                            [System.XML.XMLElement] $logCustomFieldEntry = $xmlDocument.CreateElement( 'Entry' )
                            [void] $xmlRuleTypePropertyUnique.appendChild( $logCustomFieldEntry )

                            [System.XML.XMLElement] $entrySourceType = $xmlDocument.CreateElement( 'SourceType' )
                            [void] $logCustomFieldEntry.appendChild( $entrySourceType )
                            $entrySourceType.InnerText = $entry.SourceType

                            [System.XML.XMLElement] $entrySourceName = $xmlDocument.CreateElement( 'SourceName' )
                            [void] $logCustomFieldEntry.appendChild( $entrySourceName )
                            $entrySourceName.InnerText = $entry.SourceName
                        }
                    }
                    else
                    {
                        $xmlRuleTypePropertyUnique.InnerText = $rule.$property
                    }
                }
            }
        }

        $fileList = Get-PowerStigFileList -StigDetails $xccdfXml -Destination $Destination -Path $Path

        try
        {
            $xmlDocument.save($fileList.Settings.FullName)
            # The save method does not add the required blank line to the file
            Write-Output -InputObject "`r`n" | Out-File -FilePath $fileList.Settings.FullName -Append -Encoding utf8 -NoNewline
            Write-Output "Converted Output: $($fileList.Settings.FullName)"
        }
        catch [System.Exception]
        {
            Write-Error -Message $error[0]
        }

        if ($CreateOrgSettingsFile)
        {
            $OrganizationalSettingsXmlFileParameters = @{
                'convertedStigObjects' = $convertedStigObjects
                'StigVersionNumber'    = $stigVersionNumber
                'Destination'          = $fileList.OrgSettings.FullName
            }
            New-OrganizationalSettingsXmlFile @OrganizationalSettingsXmlFileParameters

            Write-Output "Org Settings Output: $($fileList.OrgSettings.FullName)"
        }
    }
    end
    {
        $global:VerbosePreference = $CurrentVerbosePreference
    }
}

<#
    .SYNOPSIS
        Compares the converted xml files from ConvertFrom-StigXccdf.
    .PARAMETER OldStigPath
        The full path to the previous PowerStigXml file to convert.
    .PARAMETER NewStigPath
        The full path to the current PowerStigXml file to convert.
#>

function Compare-PowerStigXml
{
    [CmdletBinding()]
    [OutputType([psobject])]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $OldStigPath,

        [Parameter(Mandatory = $true)]
        [string]
        $NewStigPath,

        [Parameter()]
        [switch]
        $IgnoreRawString
    )
    begin
    {
        $CurrentVerbosePreference = $global:VerbosePreference

        if ($PSBoundParameters.ContainsKey('Verbose'))
        {
            $global:VerbosePreference = 'Continue'
        }
    }
    process
    {

        [xml] $OldStigContent = Get-Content -Path $OldStigPath -Encoding UTF8
        [xml] $NewStigContent = Get-Content -Path $NewStigPath -Encoding UTF8

        $rules = $OldStigContent.DISASTIG.ChildNodes.ToString() -split "\s"

        $returnCompareList = @{}
        $compareObjects = @()
        $propsToIgnore = @()
        if ($ignoreRawString)
        {
            $propsToIgnore += "rawString"
        }
        foreach ( $rule in $rules )
        {
            $OldStigXml = Select-Xml -Xml $OldStigContent -XPath "//$rule/*"
            $NewStigXml = Select-Xml -Xml $NewStigContent -XPath "//$rule/*"

            if ($OldStigXml.Count -lt 2)
            {
                $prop = (Get-Member -MemberType Properties -InputObject $OldStigXml.Node).Name
            }
            else
            {
                $prop = (Get-Member -MemberType Properties -InputObject $OldStigXml.Node[0]).Name
            }
            $OldStigXml = $OldStigXml.Node | Select-Object $prop -ExcludeProperty $propsToIgnore

            if ($NewStigXml.Count -lt 2)
            {
                $prop = (Get-Member -MemberType Properties -InputObject $NewStigXml.Node).Name
            }
            else
            {
                $prop = (Get-Member -MemberType Properties -InputObject $NewStigXml.Node[0]).Name
            }
            $NewStigXml = $NewStigXml.Node | Select-Object $prop -ExcludeProperty $propsToIgnore

            $compareObjects += Compare-Object -ReferenceObject $OldStigXml -DifferenceObject $NewStigXml -Property $prop
        }

        $compareIdList = $compareObjects.Id

        foreach ($stig in $compareObjects)
        {
            $compareIdListFilter = $compareIdList |
                Where-Object {$PSitem -eq $stig.Id}

            if ($compareIdListFilter.Count -gt "1")
            {
                $delta = "changed"
            }
            else
            {
                if ($stig.SideIndicator -eq "=>")
                {
                    $delta = "added"
                }
                elseif ($stig.SideIndicator -eq "<=")
                {
                    $delta = "deleted"
                }
            }

            if ( -not $returnCompareList.ContainsKey($stig.Id))
            {
                [void] $returnCompareList.Add($stig.Id, $delta)
            }
        }
        $returnCompareList.GetEnumerator() | Sort-Object Name
    }
    end
    {
        $global:VerbosePreference = $CurrentVerbosePreference
    }
}
#endregion

#region Private Functions
$organizationalSettingRootComment = @'
 
    The organizational settings file is used to define the local organizations
    preferred setting within an allowed range of the STIG.
 
    Each setting in this file is linked by STIG ID and the valid range is in an
    associated comment.
 
'@


<#
    .SYNOPSIS
        Creates the Organizational settings file that accompanies the converted STIG data.
    .PARAMETER convertedStigObjects
        The Converted Stig Objects to sort through
    .PARAMETER StigVersionNumber
        The version number of the xccdf that is being processed.
    .PARAMETER Destination
        The path to store the output file.
#>

function New-OrganizationalSettingsXmlFile
{
    [CmdletBinding()]
    [OutputType()]
    param
    (
        [Parameter(Mandatory = $true)]
        [psobject]
        $ConvertedStigObjects,

        [Parameter(Mandatory = $true)]
        [version]
        $StigVersionNumber,

        [Parameter(Mandatory = $true)]
        [string]
        $Destination
    )

    $OrgSettings = Get-StigObjectsWithOrgSettings -ConvertedStigObjects $ConvertedStigObjects

    $xmlDocument = [System.XML.XMLDocument]::New()

    ######################################### Root object ##########################################

    [System.XML.XMLElement] $xmlRootElement = $xmlDocument.CreateElement('OrganizationalSettings')

    [void] $xmlDocument.appendChild($xmlRootElement)
    [void] $xmlRootElement.SetAttribute('fullversion', $StigVersionNumber)

    $rootComment = $xmlDocument.CreateComment($organizationalSettingRootComment)
    [void] $xmlDocument.InsertBefore($rootComment, $xmlRootElement)

    ######################################### Root object ##########################################
    ######################################### ID object ##########################################

    foreach ($orgSetting in $OrgSettings)
    {
        $orgSettingProperty = Get-OrgSettingPropertyFromStigRule -ConvertedStig $orgSetting

        [System.XML.XMLElement] $xmlSettingChildElement = $xmlDocument.CreateElement('OrganizationalSetting')

        [void] $xmlRootElement.appendChild($xmlSettingChildElement)

        $xmlSettingChildElement.SetAttribute($xmlAttribute.ruleId , $orgSetting.id)

        foreach ($property in $orgSettingProperty)
        {
            $xmlAttribute.Add($property, $property)
            $xmlSettingChildElement.SetAttribute($xmlAttribute.$property , [string]::Empty)
            $xmlAttribute.Remove($property)
        }

        $settingComment = " Ensure $(($orgSetting.OrganizationValueTestString) -f "'$($orgSetting.Id)'")"

        $rangeNameComment = $xmlDocument.CreateComment($settingComment)
        [void] $xmlRootElement.InsertBefore($rangeNameComment, $xmlSettingChildElement)
    }

    ######################################### ID object ##########################################

    $xmlDocument.Save($Destination)
    Write-Output -InputObject "`r`n" | Out-File -FilePath $Destination -Append -Encoding utf8 -NoNewline
}

<#
    .SYNOPSIS
        Filters the list of STIG objects and returns anything that requires an organizational decision.
    .PARAMETER convertedStigObjects
        A reference to the object that contains the converted stig data.
    .NOTES
        This function should only be called from the public ConvertTo-DscStigXml function.
#>

function Get-StigObjectsWithOrgSettings
{
    [CmdletBinding()]
    [OutputType([psobject])]
    param
    (
        [Parameter(Mandatory = $true)]
        [psobject]
        $ConvertedStigObjects
    )

    $ConvertedStigObjects |
        Where-Object { $PSitem.OrganizationValueRequired -eq $true}
}

function Get-OrgSettingPropertyFromStigRule
{
    [CmdletBinding()]
    [OutputType([string[]])]
    param
    (
        [Parameter(Mandatory = $true)]
        [psobject]
        $ConvertedStig
    )

    $propertiesToRemove = Get-BaseRulePropertyName
    [System.Collections.ArrayList] $rulePropertyNames = (Get-Member -InputObject $ConvertedStig -MemberType Property).Name
    foreach ($property in $propertiesToRemove)
    {
        $rulePropertyNames.RemoveAt($rulePropertyNames.IndexOf($property))
    }
    foreach ($propertyName in $rulePropertyNames)
    {
        if ([string]::IsNullOrEmpty($ConvertedStig.$propertyName))
        {
            [array] $orgSettingProperties += $propertyName
        }
    }

    return $orgSettingProperties
}

<#
    .SYNOPSIS
        Creates HardCodedRule log file entry example
    .DESCRIPTION
        Queries a specific RuleType and generates an example log file entry for
        HardCodedRules in PowerSTIG.
    .PARAMETER RuleId
        The STIG RuleId that should be included with the HardCodedRule log file
        example.
    .PARAMETER RuleType
        The RuleType(s) that should be used when creating a HardCodedRule log file
        entry.
    .EXAMPLE
        Get-HardCodedRuleLogFileEntry -RuleId V-1000 -RuleType WindowsFeatureRule
 
        Outputs the following single HardCodedRule log entry example:
        V-1000::*::HardCodedRule(WindowsFeatureRule)@{DscResource = 'WindowsFeature'; Ensure = $null; Name = $null}
    .EXAMPLE
        Get-HardCodedRuleLogFileEntry -RuleId V-1000 -RuleType WindowsFeatureRule, FileContentRule
 
        Outputs the following split HardCodedRule log entry example:
        V-1000::*::HardCodedRule(WindowsFeatureRule)@{DscResource = 'WindowsFeature'; Ensure = $null; Name = $null}<splitRule>HardCodedRule(FileContentRule)@{DscResource = 'ReplaceText'; Key = $null; Value = $null}
#>

function Get-HardCodedRuleLogFileEntry
{
    [CmdletBinding()]
    [OutputType([string])]
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        $RuleId
    )
    DynamicParam {
        Get-DynamicParameterRuleTypeName
    }

    begin
    {
        # Bind the specified parameter values to RuleType var
        $RuleType = $PSBoundParameters['RuleType']
        $counter = 0

        # Dynamically query the base rule common properties to remove
        $commonPropertiesToRemove = Get-BaseRulePropertyName

        # Log file patterns to build log file string
        $logFileRuleId = '{0}::*::' -f $RuleId
        $logFileHardCodedRulePattern = "{0}HardCodedRule({1}){4}DscResource = '{2}'{3}{5}"
        $keyValuePairPattern = '; {0} = $null'
        $splitRulePattern = '<splitRule>'
        $open, $close = '@{', '}'
    }

    process
    {
        $returnString = foreach ($type in $RuleType)
        {
            # Create convert rule of the given type in order to obtain rule specific properties
            $ruleTypeConvert = New-Object -TypeName ("$type`Convert")
            $ruleTypeConvert.SetDscResource()
            $ruleTypeDscResource = $ruleTypeConvert.DscResource

            # Query all valid non-base rule property names
            $ruleProperties = (Get-Member -InputObject $ruleTypeConvert -MemberType Property).Name |
                Where-Object -FilterScript {$PSItem -notin $commonPropertiesToRemove}

            # Build a string for DSC Resource specific parameters, without values
            $keyValuePair = @()
            foreach ($dscKey in $ruleProperties)
            {
                $keyValuePair += $keyValuePairPattern -f $dscKey
            }
            $keyValuePair = -join $keyValuePair

            # First time through, add the rule id, second and more will add the split delimiter
            if ($counter -eq 0)
            {
                $logFileHardCodedRulePattern -f $logFileRuleId, $type, $ruleTypeDscResource, $keyValuePair, $open, $close
                $counter++
            }
            else
            {
                $logFileHardCodedRulePattern -f $splitRulePattern, $type, $ruleTypeDscResource, $keyValuePair, $open, $close
            }
        }
        return -join $returnString
    }
}

<#
    .SYNOPSIS
        Helper function to return the base rule property names.
#>

function Get-BaseRulePropertyName
{
    [CmdletBinding()]
    [OutputType([string[]])]
    param()

    $baseRule = [Rule]::new()
    return (Get-Member -InputObject $baseRule -MemberType Property).Name
}

<#
    .SYNOPSIS
        Returns a list of all PowerSTIG RuleTypes.
        Used to dynamically provide Values to Get-HardCodedRuleLogFileEntry
        RuleType parameter.
#>

function Get-DynamicParameterRuleTypeName
{
    [CmdletBinding()]
    [OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])]
    param()

    $parameterName = 'RuleType'
    $paramAttribute = [System.Management.Automation.ParameterAttribute]::new()
    $paramAttribute.Mandatory = $true
    $paramAttribute.Position = 1
    $getChildItemParams = @{
        Path    = "$PSScriptRoot\..\.."
        File    = $true
        Exclude = 'ManualRule.psm1', 'DocumentRule.psm1'
        Filter  = '*?Rule.psm1'
        Recurse = $true
    }
    [string[]]$validRuleTypes = (Get-ChildItem @getChildItemParams).Name -replace '.psm1'
    $validateSet = [System.Management.Automation.ValidateSetAttribute]::new($validRuleTypes)
    $attribCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
    $attribCollection.Add($paramAttribute)
    $attribCollection.Add($validateSet)
    $runtimeDefinedParam = [System.Management.Automation.RuntimeDefinedParameter]::new($parameterName, [string[]], $attribCollection)
    $runtimeDefinedParamDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
    $runtimeDefinedParamDictionary.Add($parameterName, $runtimeDefinedParam)
    return $runtimeDefinedParamDictionary
}

<#
    .SYNOPSIS
        Looks up the change log for a given xccdf file and loads the changes
#>

function Get-RuleChangeLog
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $Path
    )

    $path = $Path -replace '\.xml', '.log'

    try
    {
        $updateLog = Get-Content -Path $path -Encoding UTF8 -Raw -ErrorAction Stop
    }
    catch
    {
        Write-Warning "$path not found. Please create it if needed."
        return @{}
    }

    # regex matches is used to capture the log content directly to the changes variable
    $changeList = [regex]::Matches(
        $updateLog, '(?<id>V-\d+)(?:::)(?<oldText>.+)(?:::)(?<newText>.+)'
    )

    # The function returns a hastable
    $updateList = @{}
    foreach ($change in $changeList)
    {
        $id = $change.Groups.Item('id').value
        $oldText = $change.Groups.Item('oldText').value
        # The trim removes any potential CRLF entries that will show up in a regex escape sequence.
        # The replace replaces `r`n with an actual new line. This is useful if you need to add data on a separate line.
        $newText = $change.Groups.Item('newText').value.Trim().Replace('`r`n',[Environment]::NewLine)

        $changeObject = [pscustomobject] @{
            OldText = $oldText
            NewText = $newText
        }

        <#
           Some rule have multiple changes that need to be made, so if a rule already
           has a change, then add the next change to the value (array)
        #>

        if ($updateList.ContainsKey($id))
        {
            $null = $updateList[$id] += $changeObject
        }
        else
        {
            $null = $updateList.Add($id, @($changeObject))
        }
    }

    $updateList
}

#endregion