Modules/Support/ScubaBaselineSchemaHelper.psm1
|
<#
# Example usage: [string]$ResourceRoot = ($PWD.ProviderPath, $PSScriptRoot)[[bool]$PSScriptRoot] # Get the markdown mappings $markdownMappings = Get-ScubaConfigExclusionMappingsFromMarkdown -BaselineDirectory "..\..\..\baselines" # Update configuration using markdown mappings from GitHub Update-ScubaConfigBaselineWithMarkdown -BaselineFilePath ".\ScubaBaselines_en-US.json" -GitHubDirectoryUrl "https://github.com/cisagov/ScubaGear/tree/main/PowerShell/ScubaGear/baselines" # Update configuration using local markdown files Update-ScubaConfigBaselineWithMarkdown -BaselineFilePath ".\ScubaBaselines_en-US.tests.json" -BaselineDirectory "..\..\..\baselines" # Filter specific products - Update-ScubaConfigBaselineWithMarkdown -BaselineFilePath ".\ScubaBaselines_en-US.tests.json" -GitHubDirectoryUrl "https://github.com/cisagov/ScubaGear/tree/main/PowerShell/ScubaGear/baselines" -ProductFilter @("aad", "defender", "exo") # Update configuration with additional fields Update-ScubaConfigBaselineWithMarkdown -BaselineFilePath ".\ScubaBaselines_en-US.tests.json" -GitHubDirectoryUrl "https://github.com/cisagov/ScubaGear/tree/main/PowerShell/ScubaGear/baselines" -AdditionalFields @('criticality') #> function Get-ScubaConfigExclusionMappingsFromMarkdown { <# .SYNOPSIS Extracts exclusion type mappings from hidden markers in baseline markdown files. .DESCRIPTION This function parses baseline markdown files to extract exclusion type mappings using hidden markers in the format <!--ExclusionType: TypeName-->. This approach is simpler and more maintainable than parsing Rego files. .PARAMETER BaselineDirectory The directory containing baseline markdown files. Defaults to the standard ScubaGear baselines directory. .PARAMETER GitHubDirectoryUrl The URL of the GitHub directory containing baseline policy files. .EXAMPLE $mappings = Get-ScubaConfigExclusionMappingsFromMarkdown -BaselineDirectory "C:\Path\To\ScubaGear\baselines" #> param( [Parameter(Mandatory=$false)] [string]$BaselineDirectory, [Parameter(Mandatory=$false)] [string]$GitHubDirectoryUrl ) $exclusionMappings = @{} $files = @() # Get markdown files from GitHub or local directory if ($GitHubDirectoryUrl) { # Convert GitHub URL to API URL if ($GitHubDirectoryUrl -match '^https://github.com/([^/]+)/([^/]+)/tree/([^/]+)(?:/(.*))?$') { $owner = $matches[1] $repo = $matches[2] $branch = $matches[3] $path = if ($matches[4]) { $matches[4] } else { "" } $apiUrl = "https://api.github.com/repos/$owner/$repo/contents/$path`?ref=$branch" } else { throw "Invalid GitHub URL format. Expected format: https://github.com/owner/repo/tree/branch/path" } # Get list of markdown files in the directory $response = Invoke-RestMethod -Uri $apiUrl $files = $response | Where-Object { $_.name -like "*.md" } } elseif ($BaselineDirectory) { $files = Get-ChildItem -Path $BaselineDirectory -Filter *.md } else { throw "You must provide either -BaselineDirectory or -GitHubDirectoryUrl." } # Process each markdown file foreach ($file in $files) { if ($GitHubDirectoryUrl) { $content = (Invoke-WebRequest -Uri $file.download_url).Content $lines = $content -split "`n" } else { $lines = Get-Content -Path $file.FullName -Encoding UTF8 } # Normalize problematic Unicode characters to standard ASCII equivalents for ($j = 0; $j -lt $lines.Count; $j++) { $lines[$j] = $lines[$j] -replace [char]0x2019, "'" # Right single quotation mark to apostrophe $lines[$j] = $lines[$j] -replace [char]0x201C, '"' # Left double quotation mark $lines[$j] = $lines[$j] -replace [char]0x201D, '"' # Right double quotation mark $lines[$j] = $lines[$j] -replace [char]0x2013, '-' # En dash to hyphen $lines[$j] = $lines[$j] -replace [char]0x2014, '--' # Em dash to double hyphen $lines[$j] = $lines[$j] -replace '’', "'" # Fix UTF-8 encoding error $lines[$j] = $lines[$j] -replace '“', '"' # Fix UTF-8 encoding error $lines[$j] = $lines[$j] -replace 'â€\x9d', '"' # Fix UTF-8 encoding error } $currentPolicyId = $null for ($i = 0; $i -lt $lines.Count; $i++) { $line = $lines[$i] # Look for policy ID headers (#### MS.XXX.#.#v#) if ($line -match '^####\s+(MS\.[A-Z]+\.[0-9]+\.[0-9]+v[0-9]+)\s*$') { $currentPolicyId = $matches[1] } # Look for hidden exclusion type marker if ($currentPolicyId -and $line -match '<!--\s*ExclusionType:\s*(\w+)\s*-->') { $exclusionType = $matches[1] if (-not $exclusionMappings.ContainsKey($currentPolicyId)) { $exclusionMappings[$currentPolicyId] = $exclusionType Write-Verbose "Found mapping: $currentPolicyId -> $exclusionType (from $($file.Name))" } } } } return $exclusionMappings } function Update-ScubaConfigBaselineWithMarkdown { <# .SYNOPSIS Updates the baseline configuration using exclusion mappings extracted from markdown file markers. .DESCRIPTION This function uses hidden markers in the baseline markdown files to determine the exclusion types used by each policy, providing a simpler approach than parsing Rego files. Also handles version increment, debug mode reset, and adds required fields to all baselines. .PARAMETER BaselineFilePath The path to the configuration file that will be updated. .PARAMETER BaselineDirectory The local directory containing baseline policy files. .PARAMETER GitHubDirectoryUrl The URL of the GitHub directory containing baseline policy files. .PARAMETER ProductFilter An array of product names to filter the policies. .PARAMETER AdditionalFields An array of additional fields to include in the policy objects. Available fields: criticality, lastModified, implementation, mitreMapping, resources, link, badges. .EXAMPLE Update-ScubaConfigBaselineWithMarkdown -BaselineFilePath ".\ScubaBaselines_en-US.json" -GitHubDirectoryUrl "https://github.com/cisagov/ScubaGear/tree/main/PowerShell/ScubaGear/baselines" #> param( [Parameter(Mandatory=$false)] [string]$BaselineFilePath = "$env:Temp\ScubaBaselines.json", [Parameter(Mandatory=$false)] [string]$BaselineDirectory, [Parameter(Mandatory=$false)] [string]$GitHubDirectoryUrl, [Parameter(Mandatory=$false)] [string[]]$ProductFilter = @(), [Parameter(Mandatory=$false)] [string[]]$AdditionalFields = @("criticality", "lastModified", "implementation", "mitreMapping", "resources", "licenseRequirements", "link", "badges") ) # Get the exclusion mappings from markdown file markers Write-Output "Analyzing markdown files for exclusion mappings..." #if Paramter is BaselineDirectory use that, else use GitHubDirectoryUrl if ($BaselineDirectory) { $markdownMappings = Get-ScubaConfigExclusionMappingsFromMarkdown -BaselineDirectory $BaselineDirectory } else { $markdownMappings = Get-ScubaConfigExclusionMappingsFromMarkdown -GitHubDirectoryUrl $GitHubDirectoryUrl } Write-Output "Found $($markdownMappings.Keys.Count) policy exclusion mappings from markdown files" # Extract common header content from the first baseline markdown file Write-Output "Extracting common header content from baseline markdown files..." if ($BaselineDirectory) { $headerContent = Get-ScubaBaselineHeaderContent -BaselineDirectory $BaselineDirectory } else { $headerContent = Get-ScubaBaselineHeaderContent -GitHubDirectoryUrl $GitHubDirectoryUrl } # Create new configuration structure everytime! $configContent = [PSCustomObject]@{ Version = "1.0.0" DebugMode = "None" baselines = @{} } # Add header content if extracted successfully if ($headerContent) { $configContent | Add-Member -NotePropertyName "Introduction" -NotePropertyValue $headerContent.Introduction $configContent | Add-Member -NotePropertyName "LicenseCompliance" -NotePropertyValue $headerContent.LicenseCompliance $configContent | Add-Member -NotePropertyName "Assumptions" -NotePropertyValue $headerContent.Assumptions $configContent | Add-Member -NotePropertyName "KeyTerminology" -NotePropertyValue $headerContent.KeyTerminology Write-Output "Added common header content to configuration" } Write-Output "Creating new configuration file: $BaselineFilePath" # 1. Update version using current date (year offset from 2025, month, day) if ($configContent.Version) { $currentVersion = $configContent.Version $currentDate = Get-Date # Calculate year offset from 2025 (UI development start year) $uiStartYear = 2025 $currentYear = $currentDate.Year $yearOffset = $currentYear - $uiStartYear + 1 # +1 so 2025 = 1, 2026 = 2, etc. $month = $currentDate.Month # Current month (1-12) $day = $currentDate.Day # Current day (1-31) $dateString = Get-Date -Format "M/d/yyyy" $newVersion = "$yearOffset.$month.$day" $configContent.Version = "$newVersion [updated $dateString]" Write-Output "Updated version from '$currentVersion' to '$($configContent.Version)'" } # Get baseline policies $baselinePolicies = Get-ScubaBaselinePolicy -BaselineDirectory $BaselineDirectory -GitHubDirectoryUrl $GitHubDirectoryUrl # Filter products if specified if ($ProductFilter.Count -gt 0) { $filteredPolicies = @{} foreach ($product in $ProductFilter) { if ($baselinePolicies.ContainsKey($product)) { $filteredPolicies[$product] = $baselinePolicies[$product] } } $baselinePolicies = $filteredPolicies } # Create new baselines structure with alphabetical ordering $newBaselines = [ordered]@{} $mappingStats = @{} # Sort products alphabetically for consistent output $sortedProducts = $baselinePolicies.Keys | Sort-Object foreach ($product in $sortedProducts) { $policies = $baselinePolicies[$product] $productBaseline = @() foreach ($policy in $policies) { # Use markdown mapping if available, otherwise default to "none" $exclusionField = if ($markdownMappings.ContainsKey($policy.PolicyId)) { $markdownMappings[$policy.PolicyId] } else { "none" } # Track mapping statistics if (-not $mappingStats.ContainsKey($exclusionField)) { $mappingStats[$exclusionField] = 0 } $mappingStats[$exclusionField]++ # 3. Create policy object with required fields including omissionField and annotationField $policyObj = [ordered]@{ id = $policy.PolicyId name = $policy.Title policySection = $policy.PolicySection sectionDescription = $policy.SectionDescription exclusionField = $exclusionField omissionField = "Omissions" annotationField = "Annotations" } # Add rationale if available (always try to include this) if ($policy.Rationale) { $policyObj['rationale'] = $policy.Rationale } # Add additional fields if specified and they exist foreach ($field in $AdditionalFields) { switch ($field) { "criticality" { if ($policy.Criticality) { $policyObj['criticality'] = $policy.Criticality } } "lastModified" { if ($policy.LastModified) { $policyObj['lastModified'] = $policy.LastModified } } "implementation" { if ($policy.Implementation) { $policyObj['implementation'] = $policy.Implementation } } "mitreMapping" { if ($policy.MITRE_Mapping -and $policy.MITRE_Mapping.Count -gt 0) { $policyObj['mitreMapping'] = $policy.MITRE_Mapping } } "resources" { if ($policy.SectionResources -and $policy.SectionResources.Count -gt 0) { $policyObj['resources'] = $policy.SectionResources } } "licenseRequirements" { if ($policy.SectionLicenseRequirements -and $policy.SectionLicenseRequirements.Count -gt 0) { # Ensure licenseRequirements is always an array if ($policy.SectionLicenseRequirements -is [array]) { $policyObj['licenseRequirements'] = $policy.SectionLicenseRequirements } else { $policyObj['licenseRequirements'] = @($policy.SectionLicenseRequirements) } } else { # Default to "N/A" as a single-item array for consistency $policyObj['licenseRequirements'] = @("N/A") } } "link" { if ($policy.Link) { $policyObj['link'] = $policy.Link } } "badges" { if ($policy.Badges -and $policy.Badges.Count -gt 0) { $policyObj['badges'] = $policy.Badges } } } } $productBaseline += $policyObj } if ($productBaseline.Count -gt 0) { $newBaselines[$product] = $productBaseline } } # Update the configuration $configContent.baselines = $newBaselines # Save the updated configuration with proper character encoding $jsonOutput = $configContent | ConvertTo-Json -Depth 10 # Fix common UTF-8 encoding issues in the JSON output using character codes $jsonOutput = $jsonOutput -replace ([char]0x2019), "'" # Right single quotation mark $jsonOutput = $jsonOutput -replace ([char]0x201C), '"' # Left double quotation mark $jsonOutput = $jsonOutput -replace ([char]0x201D), '"' # Right double quotation mark $jsonOutput = $jsonOutput -replace 'â€"', "—" # Fix malformed double dash $jsonOutput = $jsonOutput -replace '’', "'" # Fix malformed apostrophe $jsonOutput = $jsonOutput -replace '“', '"' # Fix malformed left quote $jsonOutput = $jsonOutput -replace 'â€', '"' # Fix malformed right quote $jsonOutput | Set-Content $BaselineFilePath -Encoding UTF8 Write-Output "Successfully updated baselines in configuration file: $BaselineFilePath" Write-Output "Updated products: $($newBaselines.Keys -join ', ')" # Show exclusion type statistics Write-Output "`nExclusion Type Statistics:" foreach ($exclusionField in ($mappingStats.Keys | Sort-Object)) { Write-Output " $exclusionField`: $($mappingStats[$exclusionField]) policies" } # Detailed summary foreach ($product in $newBaselines.Keys) { $policies = $newBaselines[$product] $policyCount = $policies.Count $exclusionCounts = $policies | Group-Object exclusionField | ForEach-Object { "$($_.Name): $($_.Count)" } Write-Output " $product`: $policyCount policies ($($exclusionCounts -join ', '))" } return $newBaselines } function Get-ScubaBaselineHeaderContent { <# .SYNOPSIS Extracts the common header content from the first baseline markdown file. .DESCRIPTION Extracts content from the introduction section, specifically the last 3 paragraphs that start with "The Secure Cloud Business Applications (SCuBA) project...", "The CISA SCuBA SCBs...", and "For non-Federal users...". Also extracts License Compliance, Assumptions, and Key Terminology sections. .PARAMETER BaselineDirectory The local directory containing baseline policy files. .PARAMETER GitHubDirectoryUrl The URL of the GitHub directory containing baseline policy files. #> param( [Parameter(Mandatory=$false)] [string]$BaselineDirectory, [Parameter(Mandatory=$false)] [string]$GitHubDirectoryUrl ) $files = @() if ($GitHubDirectoryUrl) { # Convert GitHub URL to API URL if ($GitHubDirectoryUrl -match '^https://github.com/([^/]+)/([^/]+)/tree/([^/]+)(?:/(.*))?$') { $owner = $matches[1] $repo = $matches[2] $branch = $matches[3] $path = if ($matches[4]) { $matches[4] } else { "" } $apiUrl = "https://api.github.com/repos/$owner/$repo/contents/$path`?ref=$branch" } else { throw "Invalid GitHub URL format. Expected format: https://github.com/owner/repo/tree/branch/path" } # Get list of markdown files in the directory $response = Invoke-RestMethod -Uri $apiUrl $files = $response | Where-Object { $_.name -like "*.md" } } elseif ($BaselineDirectory) { $files = Get-ChildItem -Path $BaselineDirectory -Filter *.md } else { throw "You must provide either -BaselineDirectory or -GitHubDirectoryUrl." } # Use the first markdown file (e.g., aad.md) if ($files.Count -eq 0) { return $null } $firstFile = $files[0] $content = "" if ($GitHubDirectoryUrl) { $content = (Invoke-WebRequest -Uri $firstFile.download_url).Content } else { $content = Get-Content -Path $firstFile.FullName -Raw -Encoding UTF8 } # Normalize problematic Unicode characters in the header content $content = $content -replace [char]0x2019, "'" # Right single quotation mark to apostrophe $content = $content -replace [char]0x201C, '\"' # Left double quotation mark $content = $content -replace [char]0x201D, '\"' # Right double quotation mark $content = $content -replace [char]0x2013, '-' # En dash to hyphen $content = $content -replace [char]0x2014, '--' # Em dash to double hyphen $content = $content -replace '’', "'" # Fix UTF-8 encoding error $content = $content -replace '“', '\"' # Fix UTF-8 encoding error $content = $content -replace 'â€\x9d', '\"' # Fix UTF-8 encoding error $headerContent = @{ introduction = "" licenseCompliance = "" assumptions = "" keyTerminology = "" } # Extract introduction content - get only the common SCuBA project description if ($content -match '(?s)The Secure Cloud Business Applications \(SCuBA\) project.*?(?=## License Compliance and Copyright)') { $introSection = $matches[0].Trim() $headerContent.introduction = $introSection } # Extract License Compliance and Copyright section if ($content -match '(?s)## License Compliance and Copyright\s*\n(.*?)(?=\n## |$)') { $headerContent.licenseCompliance = $matches[1].Trim() } # Extract Assumptions section if ($content -match '(?s)## Assumptions\s*\n(.*?)(?=\n## |$)') { $headerContent.assumptions = $matches[1].Trim() } # Extract Key Terminology section if ($content -match '(?s)## Key Terminology\s*\n(.*?)(?=\n## |$)') { $headerContent.keyTerminology = $matches[1].Trim() } return $headerContent } function Get-ScubaBaselinePolicy { <# .SYNOPSIS Retrieves the baseline policy for a specific product using hierarchical section parsing. .PARAMETER BaselineDirectory Specifies the directory containing baseline policy files. .PARAMETER GitHubDirectoryUrl Specifies the GitHub directory URL containing baseline policy files. .EXAMPLE Get-ScubaBaselinePolicy -BaselineDirectory $BaselineDirectory -GitHubDirectoryUrl $GitHubDirectoryUrl #> param( [Parameter(Mandatory=$false)] [string]$BaselineDirectory, [Parameter(Mandatory=$false)] [string]$GitHubDirectoryUrl ) $policiesByProduct = @{} $files = @() if ($GitHubDirectoryUrl) { # Convert GitHub URL to API URL if ($GitHubDirectoryUrl -match '^https://github.com/([^/]+)/([^/]+)/tree/([^/]+)(?:/(.*))?$') { $owner = $matches[1] $repo = $matches[2] $branch = $matches[3] $path = if ($matches[4]) { $matches[4] } else { "" } $apiUrl = "https://api.github.com/repos/$owner/$repo/contents/$path`?ref=$branch" } else { throw "Invalid GitHub URL format. Expected format: https://github.com/owner/repo/tree/branch/path" } # Get list of markdown files in the directory $response = Invoke-RestMethod -Uri $apiUrl $files = $response | Where-Object { $_.name -like "*.md" } } elseif ($BaselineDirectory) { $files = Get-ChildItem -Path $BaselineDirectory -Filter *.md } else { throw "You must provide either -BaselineDirectory or -GitHubDirectoryUrl." } foreach ($file in $files) { if ($GitHubDirectoryUrl) { $content = (Invoke-WebRequest -Uri $file.download_url).Content $lines = $content -split "`n" } else { $lines = Get-Content -Path $file.FullName -Encoding UTF8 } # Normalize problematic Unicode characters to standard ASCII equivalents for ($j = 0; $j -lt $lines.Count; $j++) { $lines[$j] = $lines[$j] -replace [char]0x2019, "'" # Right single quotation mark to apostrophe $lines[$j] = $lines[$j] -replace [char]0x201C, '"' # Left double quotation mark $lines[$j] = $lines[$j] -replace [char]0x201D, '"' # Right double quotation mark $lines[$j] = $lines[$j] -replace [char]0x2013, '-' # En dash to hyphen $lines[$j] = $lines[$j] -replace [char]0x2014, '--' # Em dash to double hyphen $lines[$j] = $lines[$j] -replace '’', "'" # Fix UTF-8 encoding error $lines[$j] = $lines[$j] -replace '“', '"' # Fix UTF-8 encoding error $lines[$j] = $lines[$j] -replace 'â€\x9d', '"' # Fix UTF-8 encoding error } # Parse hierarchically by sections $fullContent = $lines -join "`n" $sections = Get-ScubaBaselineSections -Content $fullContent $policies = @() foreach ($section in $sections) { foreach ($policy in $section.Policies) { # Add section context to each policy $policy.PolicySection = $section.Name $policy.SectionDescription = $section.Description $policy.SectionResources = $section.Resources $policy.SectionLicenseRequirements = $section.LicenseRequirements $policies += $policy } } if ($policies.Count -gt 0) { $productName = ($file.Name -replace '\.md$', '').ToLower() $policiesByProduct[$productName] = $policies } } return $policiesByProduct } function Get-ScubaBaselineSections { <# .SYNOPSIS Parses baseline markdown content hierarchically by sections (## level headers) .PARAMETER Content The full markdown content to parse #> param([string]$Content) $sections = @() $lines = $Content -split "`n" $currentSection = $null $currentSubSection = $null $currentPolicy = $null $currentContent = @() for ($i = 0; $i -lt $lines.Count; $i++) { $line = $lines[$i] # ## Section headers (e.g., "## 1. Legacy Authentication") if ($line -match '^##\s+\d+\.\s*(.+)$') { # Save previous section if ($currentSection) { $sections += $currentSection } # Start new section $currentSection = @{ Name = $matches[1].Trim() Description = "" Resources = @() LicenseRequirements = @() Policies = [System.Collections.ArrayList]::new() } $currentSubSection = $null $currentPolicy = $null $currentContent = @() continue } # ### Sub-section headers (Policies, Resources, License Requirements, Implementation) if ($line -match '^###\s+(.+)$') { $subSectionName = $matches[1].Trim() # Save current policy if we were in one if ($currentPolicy -and $currentSubSection -eq "Policies" -and $currentSection) { $currentPolicy.Content = ($currentContent -join "`n").Trim() $policyDetails = Get-ScubaPolicyContent -Content $currentPolicy.Content $currentPolicy.Criticality = $policyDetails.Criticality $currentPolicy.LastModified = $policyDetails.LastModified $currentPolicy.Rationale = $policyDetails.Rationale $currentPolicy.MITRE_Mapping = $policyDetails.MITRE_Mapping $currentPolicy.Badges = $policyDetails.Badges # Extract implementation instructions for this policy $currentPolicy.Implementation = Get-ScubaPolicyImplementation -Content $Content -PolicyId $currentPolicy.PolicyId $null = $currentSection.Policies.Add($currentPolicy) $currentPolicy = $null $currentContent = @() } # Process section-level content based on subsection type if ($currentSubSection -eq "Resources") { $currentSection.Resources = Get-ScubaSectionContent -Content ($currentContent -join "`n") -ContentType "Resources" } elseif ($currentSubSection -eq "License Requirements") { $sectionLicenseReq = Get-ScubaSectionContent -Content ($currentContent -join "`n") -ContentType "LicenseRequirements" $currentSection.LicenseRequirements = if ($sectionLicenseReq -and $sectionLicenseReq.Count -gt 0) { $sectionLicenseReq } else { @("N/A") } } $currentSubSection = $subSectionName $currentContent = @() continue } # #### Policy headers (e.g., "#### MS.AAD.1.1v1") if ($line -match '^####\s+(MS\.[A-Z]+\.[0-9]+\.[0-9]+v[0-9]+)\s*$') { # Save previous policy if ($currentPolicy -and $currentSection) { $currentPolicy.Content = ($currentContent -join "`n").Trim() $policyDetails = Get-ScubaPolicyContent -Content $currentPolicy.Content $currentPolicy.Criticality = $policyDetails.Criticality $currentPolicy.LastModified = $policyDetails.LastModified $currentPolicy.Rationale = $policyDetails.Rationale $currentPolicy.MITRE_Mapping = $policyDetails.MITRE_Mapping $currentPolicy.Badges = $policyDetails.Badges # Extract implementation instructions for this policy $currentPolicy.Implementation = Get-ScubaPolicyImplementation -Content $Content -PolicyId $currentPolicy.PolicyId $null = $currentSection.Policies.Add($currentPolicy) $currentContent = @() } # Start new policy $currentPolicy = @{ PolicyId = $matches[1] Title = "" Content = "" } continue } # Policy title (first non-empty line after policy header) if ($currentPolicy -and $currentSubSection -eq "Policies" -and -not $currentPolicy.Title -and $line.Trim() -ne '') { $currentPolicy.Title = $line.Trim() continue } # Description content (between ## section header and first ### subsection) if ($currentSection -and -not $currentSubSection -and $line.Trim() -ne '') { if ($currentSection.Description -eq "") { $currentSection.Description = $line.Trim() } else { $currentSection.Description += " " + $line.Trim() } continue } # Collect content for current context $currentContent += $line } # Save final policy and section if ($currentPolicy -and $currentSection) { $currentPolicy.Content = ($currentContent -join "`n").Trim() $policyDetails = Get-ScubaPolicyContent -Content $currentPolicy.Content $currentPolicy.Criticality = $policyDetails.Criticality $currentPolicy.LastModified = $policyDetails.LastModified $currentPolicy.Rationale = $policyDetails.Rationale $currentPolicy.MITRE_Mapping = $policyDetails.MITRE_Mapping $currentPolicy.Badges = $policyDetails.Badges $currentPolicy.Implementation = Get-ScubaPolicyImplementation -Content $Content -PolicyId $currentPolicy.PolicyId $null = $currentSection.Policies.Add($currentPolicy) } # Process final section content if ($currentSection) { if ($currentSubSection -eq "Resources") { $currentSection.Resources = Get-ScubaSectionContent -Content ($currentContent -join "`n") -ContentType "Resources" } elseif ($currentSubSection -eq "License Requirements") { $sectionLicenseReq = Get-ScubaSectionContent -Content ($currentContent -join "`n") -ContentType "LicenseRequirements" $currentSection.LicenseRequirements = if ($sectionLicenseReq -and $sectionLicenseReq.Count -gt 0) { $sectionLicenseReq } else { @("N/A") } } $sections += $currentSection } return $sections } function Get-ScubaSectionContent { <# .SYNOPSIS Parses section content for Resources or License Requirements #> param( [string]$Content, [string]$ContentType ) $result = @() if ($ContentType -eq "Resources") { # Parse links for Resources $linkPattern = '\[([^\]]+)\]\(([^)]+)\)' $linkMatches = [regex]::Matches($Content, $linkPattern) foreach ($match in $linkMatches) { $linkName = $match.Groups[1].Value.Trim() $linkUrl = $match.Groups[2].Value.Trim() $result += @{ Name = $linkName; Url = $linkUrl } } } elseif ($ContentType -eq "LicenseRequirements") { # Parse bullet points for License Requirements - only split on actual bullets # Use regex to find lines that start with bullet markers (- at beginning of line, possibly with whitespace) $lines = $Content -split "`r?`n" $currentBullet = "" foreach ($line in $lines) { $line = $line.Trim() if ($line -match '^-\s*(.*)$') { # This is a new bullet point - save previous one if it exists if (-not [string]::IsNullOrWhiteSpace($currentBullet)) { $result += $currentBullet.Trim() } # Start new bullet with the content after the dash $currentBullet = $matches[1].Trim() } elseif (-not [string]::IsNullOrWhiteSpace($line) -and -not [string]::IsNullOrWhiteSpace($currentBullet)) { # This is a continuation of the current bullet point $currentBullet += " " + $line } } # Don't forget the last bullet point if (-not [string]::IsNullOrWhiteSpace($currentBullet)) { $result += $currentBullet.Trim() } # If no bullets found, ensure we still return an array (even if empty) if ($result.Count -eq 0) { $result = @() } } return $result } function Get-ScubaPolicyImplementation { <# .SYNOPSIS Extracts implementation instructions for a specific policy #> param( [string]$Content, [string]$PolicyId ) # Look for implementation section for this policy $pattern = "(?ms)^####\s+$PolicyId\s+Instructions\s*\n(.*?)(?=^####|^###|^##|\z)" if ($Content -match $pattern) { $implementation = $matches[1].Trim() # Normalize smart quotes to straight quotes $implementation = $implementation -replace [char]0x201C, '"' # Left double quotation mark $implementation = $implementation -replace [char]0x201D, '"' # Right double quotation mark $implementation = $implementation -replace [char]0x2019, "'" # Right single quotation mark # Clean up markdown code blocks # First, handle indented code blocks (4+ spaces or 1+ tabs followed by backticks) $implementation = $implementation -replace '(?ms)^[ \t]*```[\w]*\r?\n(.*?)\r?\n[ \t]*```[ \t]*$', '$1' # Handle regular code blocks at start of line $implementation = $implementation -replace '(?ms)^```[\w]*\r?\n(.*?)\r?\n```[ \t]*$', '$1' # Handle inline code blocks and remaining artifacts $implementation = $implementation -replace '```[\w]*\r?\n?', '' $implementation = $implementation -replace '\r?\n?```', '' $implementation = $implementation -replace '```', '' # Clean up extra whitespace that might remain $implementation = $implementation -replace '^\s+', '' $implementation = $implementation -replace '\s+$', '' return $implementation } return "" } function Get-ScubaPolicyContent { <# .SYNOPSIS Retrieves the content of a specific policy from the markdown documentation .PARAMETER Content Import markdown content #> param([string]$Content) $result = [ordered]@{ Criticality = $null LastModified = $null Rationale = $null MITRE_Mapping = @() Badges = @() LicenseRequirements = @() } if ($Content -match '<!--Policy:\s*[^;]+;\s*Criticality:\s*([A-Z]+)\s*-->') { $result.Criticality = $matches[1] } if ($Content -match '- _Last modified:_\s*(.+)') { $result.LastModified = $matches[1].Trim() } if ($Content -match '(?s)- _Rationale:_\s*(.+?)(?=\n\s*-|\n\s*\n|\z)') { $rationale = $matches[1].Trim() $result.Rationale = $rationale -replace '\s+', ' ' } if ($Content -match '(_MITRE ATT&CK TTP Mapping:_[\s\S]+?)(\n\s*\n|###|$)') { $mitreBlock = $matches[1] $mitreList = @() foreach ($line in $mitreBlock -split "`n") { if ($line -match '\[([^\]]+)\]\(([^)]+)\)') { $mitreList += [ordered]@{ Name = $matches[1]; Url = $matches[2] } } } $result.MITRE_Mapping = $mitreList } # Parse policy-level license requirements if ($Content -match '(?ms)>\s*### License Requirements\s*\n+(?<Block>.*?)(?=\n\s*###|\n\s*\n\s*####|\z)') { $licenseBlock = $matches['Block'] $cleanedBlock = $licenseBlock -replace "`n", " " -replace '\s+', ' ' $bulletItems = $cleanedBlock -split '-' | Where-Object { $_.Trim() -ne '' } foreach ($item in $bulletItems) { $item = $item.Trim() # Filter out section headers and empty items, but keep N/A if ($item -and -not [string]::IsNullOrWhiteSpace($item) -and $item -notmatch '^###\s+') { $result.LicenseRequirements += $item } } } # Parse badges from shield.io patterns - handle both full URLs and anchor links $badgePattern = '\[!\[(?<Title>[^\]]+)\]\((?<ImageUrl>https?://img\.shields\.io/badge/[^)]+)\)\](?:\((?<LinkUrl>[^\)]+)\))?' $badgeMatches = [regex]::Matches($Content, $badgePattern) $badges = @() foreach ($match in $badgeMatches) { # Extract color and label from the image URL $imageUrl = $match.Groups['ImageUrl'].Value $label = "" $color = "" # Parse badge URL to extract label and color - handle various patterns including double dashes if ($imageUrl -match 'https?://img\.shields\.io/badge/(.+)-([A-Fa-f0-9]{3,6})$') { $fullLabelPart = $matches[1] -replace '%20', ' ' # Replace URL encoded spaces $color = $matches[2] # Extract just the readable label (remove underscores and technical formatting) $label = $fullLabelPart -replace '_', ' ' -replace '--', '-' } $linkUrl = $match.Groups['LinkUrl'].Value if (-not $linkUrl) { $linkUrl = "" } # Handle badges without links $badges += [ordered]@{ label = $label linkUrl = $linkUrl color = $color } } $result.Badges = $badges return $result } # Export module members Export-ModuleMember -Function Get-ScubaBaselinePolicy, Get-ScubaConfigExclusionMappingsFromMarkdown, Update-ScubaConfigBaselineWithMarkdown, Get-ScubaBaselineSections, Get-ScubaBaselineHeaderContent # SIG # Begin signature block # MIIu9wYJKoZIhvcNAQcCoIIu6DCCLuQCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCLNtfKrsTCuEyc # 0ItXRBTaC3Uh3lFbmwCPArj1dzelkaCCE6MwggWQMIIDeKADAgECAhAFmxtXno4h # MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV # BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z # ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z # G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ # anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s # Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL # 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb # BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3 # JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c # AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx # YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0 # viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL # T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud # EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf # Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk # aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS # PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK # 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB # cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp # 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg # dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri # RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7 # 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5 # nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3 # i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H # EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G # CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C # 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce # 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da # E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T # SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA # FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh # D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM # 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z # 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05 # huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY # mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP # /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN # BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry # sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL # IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf # Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh # OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh # dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV # 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j # wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH # Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC # XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l # /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW # eE4wggdXMIIFP6ADAgECAhAMM6tnPejLgA9WVhXroQvSMA0GCSqGSIb3DQEBCwUA # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwHhcNMjYwMTE0MDAwMDAwWhcNMjcwMTEzMjM1OTU5WjBfMQsw # CQYDVQQGEwJVUzEdMBsGA1UECBMURGlzdHJpY3Qgb2YgQ29sdW1iaWExEzARBgNV # BAcTCldhc2hpbmd0b24xDTALBgNVBAoTBENJU0ExDTALBgNVBAMTBENJU0EwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCuXYolNHqlh6smLTE592waXheZ # 8VHzxeds4pMaepGuwmjf8d1jG9wUNuJX9/qb0a1dgGz5D/EAz5NRTIin4SZYQEE8 # qvdl2yQJ5uWxXIjsFbrOyc1fWscUXw0Kt7OPLOafcEkdDoe8K0tO4h2GL3RWRzjp # uLfQhhnAmD6NT1l+ughnfmarV/ODgIn/RFR4YORlu4YP2xQX6KRxeTDslg7F+z6X # +t87/U8m8gQ9XTm5kBmteP4GcE/ytnyI+ScIxNRybzGomWIBm848XDE5yYhlYQ2R # SnCoo6M4CRqp9WFGVyoLkoPP0OlxzryKWaE1/nuPbYG/kf/rUB1OhqxvSSGwmNhs # vkkjsC0Z9H5Jy6heFdoxOu/+ZQksKoP/fMvHxuCCtkIJbV8tk0oT6MQ8EJbgsWDZ # TKhui1wxW6JIZyBOMPWoZUOouOzo2h5Cz7LBPKME5FkcUzcs47lpRlDkJco4PLcj # wJSo4XPnx3G/2DIjNEFNyfKCWfH8uW6nJjmDBiveFZ2j0YvgdQ+7MOjQnw7R/MAD # DTagrKl3rLV60+X2TY6/onKhCUuU3pMAjVbOwZ3PkzDLZnsEGRfm6hgp6014aXml # t8h4nu+uC41U8vUSGHl0vqKuzvmShLmnI+Iv0l95pmnqomuCZzRDrjEoaLPx7OxL # Dy/Id9E7yQDip4jBdQIDAQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8R # hvv+YXsIiGX0TkIwHQYDVR0OBBYEFN30sVU+fpfQQfPQWMhkO1qd+MxoMD4GA1Ud # IAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNl # cnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMw # gbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j # cmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 # ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMIGUBggrBgEF # BQcBAQSBhzCBhDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t # MFwGCCsGAQUFBzAChlBodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNl # cnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJ # BgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQA77T42YiOx5wWPItgo+kvB+Gzb # ZFCRHRFfTAZZNQt9o/0CqNmyA2xFklX4t5Z9VNdiIOx14AnmJdQkcRdk3vsU5gby # jKEup7LTWtvcWrl6hQwGNt3l892BgUbPKsPBE+AriktVqn5yMSXVVzeboqsqAG8e # Syei0B54/QdgR4whfHvQ/qpCACsJTlJgAykXVgDPJNKnQ7wc17loLQutqF1JUcbO # XKWt1AA9Zas5q30LDZzZeK4B56yojK68CTQXN7toSRFIuZDMKKDfIZpCX8cmbaaO # DFOOu44/QWMv+Xc6+ISYGkrTTzqWhOqiXgLVBeXGn/WrOJJ8R29mZMneCpBesCLs # YII1gCFOo7Vt6mvOKxAPQ3KhJYBFEHkp+GI65koaQkO2xv50iLS0+/j2YC66uviU # MFe0JEOdXuE7Rn/OmWNSzQ+6kPNYDJQASQ974C3wUejJoMtGZEzoTbly/HufQTrd # rhcL2aC69CxSN+idTXPLC9UT3xo4sFdOw+hXkmbXtoB1GDsd5p1TWFgRbnTXDkbM # YMBWYVB6/Tk1bzwj4iTp4g0YrtB628FXPX/ko+JWZlv0Ea865S23w1uGlnDNVxIi # +8oi74G5DM66Q6ENt6+3WoRGRrdoyE6uCh1haY+oPYSgumb0ozzrp8tw89TRKVrK # KSXGBxlExEvjQ6dYwjGCGqowghqmAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNV # BAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0 # IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQDDOrZz3oy4AP # VlYV66EL0jANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh # AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM # BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCACTH7yQWiDEIMzydsSmCSdozzf # ew4KYc1aD0tuJynIHjANBgkqhkiG9w0BAQEFAASCAgAQ3SkgkTD/Vao5X3rJF4xg # PhxDgiAVhEN/Tiy8JDiiI7DNCqDhvJ89fARPpf4OaZ924259S5wYSpENtEnRv+yp # FG/G/GaYYgoYcmHLOfQdD+k+/Vd5SNscV1eQVeCgIc0rI5FbAA/3d1M5DQnRmWGA # 1l4qIILg+My8bUcMjWogaTyWVYmKXg7LIzwA1SIwPW4LcnPFG01t10Qr/e3q1jmW # MFoMcIO7aWyY4we7wG7CK7mSoO4H8KPMmFk4zJINIkWap+ocu6EU+VGkH7xLyzBJ # udMd99yTcmsrxwjGbssLIbipndDq/ygawOgv9lARDyvn0hqm0tKNSH4V7o0ZUfPZ # nfbUTkmLi0JSZlwpv5xvAgh/T5wbuGGW6ssgyjZHg8a5hQqupETagbw9+qMozz2N # zGhSfO2oXCAXwUU08vSxN/TvPo/fiPB5kfd2l5SvFPcG/3LNpkPaxBTHrbLNarEE # hdmQIkd++2yTK50aeCT/eHEOVK/fUg4jAkPpZuqmyLcmaYdBXrhjOSKFbcmJrV4U # vrRk1a5kSDYU7NuD4mv5Rimsp3pt6niwh4cqoGYQNmF1O0V0B08/b2S4I6DOIgQJ # 6eoRjP8kB6xT0C8wVM6SzYaZuhjhzZNZeKd2sOfshW1JAMEmGUf+eY4o+GBg56TK # xJuJ8hXsG8oO4Jksl0VUKKGCF3cwghdzBgorBgEEAYI3AwMBMYIXYzCCF18GCSqG # SIb3DQEHAqCCF1AwghdMAgEDMQ8wDQYJYIZIAWUDBAIBBQAweAYLKoZIhvcNAQkQ # AQSgaQRnMGUCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCCf3mNH7Opk # s8w7LifpQtds0fYXQFzDWgLoO7heIjU2CQIRAOdmmsEqQOzbN9FOlkJDt3cYDzIw # MjYxMDA4MTkxNDIxWqCCEzowggbtMIIE1aADAgECAhAIT9wzT35FTtvDD4/5khg1 # MA0GCSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy # dCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBp # bmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwHhcNMjYwODA1MDAwMDAwWhcNMzcx # MTA0MjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIElu # Yy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFNIQTI1NiBSU0E0MDk2IFRpbWVzdGFtcCBS # ZXNwb25kZXIgMjAyNiAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA # tnum8sn+zUr41JtMZbP9OMYw+HwJDpG5xkIu/lqcfNYmMX81YmsUiHLbh9ykpeWB # GKTLhYBrAN9Tdg/QEzG32XcObmgIblnr0CoQ3WSAeDZ6nH6X6VkFyYkJw3QBJREw # vm4UhLzSxmwPA7cFKRTEOMsmEEj6qJk/dqLEAL+oQYuOwE2UuiX1Vnul8YReIyWd # 4kgLn9gq6LNXM0UplkR6jL/QHxmb6fMoGBJYbnaUI7XD6cKDpekK2SVMld4iDbze # HDtOaaxldH5IxuNusQ69nd8/ZXEiB5Hbxj3RlK13cX1W4DlFXKdv/CEhM8Cj1vvl # mvhNroyPdRGbbpBlgyf8Wdu5N6ByhFwURn0U6ozlPoxN22v+fviUhP+6DR547OZn # pBMWDfei1f5sVGwiiW/KQTWOK97g+4RJpPzPNV4VYMAwO2jM2Aty2QYPVmOQTJm0 # msuXnJrSbl2gf9JylpkJlWXqk1Q4LJsxz+TELoQCZIljbgvTJgoPU2R12ydv8i1U # qL/adelA0y7U9Pmmtbze9Xx3rtajC5SzQd1jgfwAwsa90v9YcSPdmeoyoBBA/27c # CL237l5DTYYPDLQ4ON3OLTGWnvRb6jDrf/T75gMRfUzSLCBQfBusm9+mSWRlC/Df # 6S/e9Q8i13CuhzOT2Jx+V/nlbXM4QoBwlUAhelwwJT0CAwEAAaOCAZUwggGRMAwG # A1UdEwEB/wQCMAAwHQYDVR0OBBYEFBTJY4owLtRK+26U8+bjQH717M3iMB8GA1Ud # IwQYMBaAFO9vU0rp5AZ8esrikFb2L9RJ7MtOMA4GA1UdDwEB/wQEAwIHgDAWBgNV # HSUBAf8EDDAKBggrBgEFBQcDCDCBlQYIKwYBBQUHAQEEgYgwgYUwJAYIKwYBBQUH # MAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBdBggrBgEFBQcwAoZRaHR0cDov # L2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0VGltZVN0YW1w # aW5nUlNBNDA5NlNIQTI1NjIwMjVDQTEuY3J0MF8GA1UdHwRYMFYwVKBSoFCGTmh0 # dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFt # cGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNybDAgBgNVHSAEGTAXMAgGBmeBDAEE # AjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBAI3FOmEenVIK35msCYB+ # fShAsWvSYvLBItoNdAgQ2jIqrGsVsluXMJU/+mRebBc52s6lbKAvOVPXaizmKkML # LflEEKDZQx4CkS2t8aHPjkXha3hYZ010htFa3dhNgmalH5vuWvh3tTCf4frTS7gP # tGc4Z/xaPhQ2AB1mR8eEe/WbH0RWHvVIl6VwQ3+g5FKNfN2N/DWJkf13w2H+2Gfq # Efbd35Ww8CvoYBjLNIDTadcPWdgsjsiOaK/7EsKJgLjUNIVgvcaFOLLQ/GlrA+0Z # HJoFUbOr5SJN8zykPspXIXlpDJY/gqFUZRROeab9GVgmhbdOJcD/63RhxPahFUGb # ckRONqMe6DYAv6/mOG0pWd3cPStsdcS7buj5DyniwRY8yooMH6ptx5vpP/pZzBPB # eZD2U4IsthyxB5Jaa8qrOkB5z160TXiM5ADMspZ0TfD9MJoq0tFpFPssKRFhWeED # YPvcUuN7U7lvcdHl4ezQ3NT/7Ffs1sR1yh/LRbdZ3B3Vc6q2WmD8mDC0p9kzl2o7 # 3iVtS946IkEj7FkRsZGww1teYxERROC745xrtjvcw9ZyyUjHZWGRIpJeMNsPquCD # f0fkyHtB+J4AiNZqCQk23rxh+KbpyMTNVKItJ5l92Svl20U9NbqMBOVYl1h54NEY # LJq1/xHWFKPNK903zJZA9P2DMIIGtDCCBJygAwIBAgIQDcesVwX/IZkuQEMiDDpJ # hjANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNl # cnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdp # Q2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjUwNTA3MDAwMDAwWhcNMzgwMTE0MjM1 # OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA # tHgx0wqYQXK+PEbAHKx126NGaHS0URedTa2NDZS1mZaDLFTtQ2oRjzUXMmxCqvkb # sDpz4aH+qbxeLho8I6jY3xL1IusLopuW2qftJYJaDNs1+JH7Z+QdSKWM06qchUP+ # AbdJgMQB3h2DZ0Mal5kYp77jYMVQXSZH++0trj6Ao+xh/AS7sQRuQL37QXbDhAkt # VJMQbzIBHYJBYgzWIjk8eDrYhXDEpKk7RdoX0M980EpLtlrNyHw0Xm+nt5pnYJU3 # Gmq6bNMI1I7Gb5IBZK4ivbVCiZv7PNBYqHEpNVWC2ZQ8BbfnFRQVESYOszFI2Wv8 # 2wnJRfN20VRS3hpLgIR4hjzL0hpoYGk81coWJ+KdPvMvaB0WkE/2qHxJ0ucS638Z # xqU14lDnki7CcoKCz6eum5A19WZQHkqUJfdkDjHkccpL6uoG8pbF0LJAQQZxst7V # vwDDjAmSFTUms+wV/FbWBqi7fTJnjq3hj0XbQcd8hjj/q8d6ylgxCZSKi17yVp2N # L+cnT6Toy+rN+nM8M7LnLqCrO2JP3oW//1sfuZDKiDEb1AQ8es9Xr/u6bDTnYCTK # IsDq1BtmXUqEG1NqzJKS4kOmxkYp2WyODi7vQTCBZtVFJfVZ3j7OgWmnhFr4yUoz # ZtqgPrHRVHhGNKlYzyjlroPxul+bgIspzOwbtmsgY1MCAwEAAaOCAV0wggFZMBIG # A1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO9vU0rp5AZ8esrikFb2L9RJ7MtO # MB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIB # hjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUH # MAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDov # L2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQw # QwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lD # ZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZI # AYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQAXzvsWgBz+Bz0RdnEwvb4LyLU0pn/N # 0IfFiBowf0/Dm1wGc/Do7oVMY2mhXZXjDNJQa8j00DNqhCT3t+s8G0iP5kvN2n7J # d2E4/iEIUBO41P5F448rSYJ59Ib61eoalhnd6ywFLerycvZTAz40y8S4F3/a+Z1j # EMK/DMm/axFSgoR8n6c3nuZB9BfBwAQYK9FHaoq2e26MHvVY9gCDA/JYsq7pGdog # P8HRtrYfctSLANEBfHU16r3J05qX3kId+ZOczgj5kjatVB+NdADVZKON/gnZruMv # NYY2o1f4MXRJDMdTSlOLh0HCn2cQLwQCqjFbqrXuvTPSegOOzr4EWj7PtspIHBld # NE2K9i697cvaiIo2p61Ed2p8xMJb82Yosn0z4y25xUbI7GIN/TpVfHIqQ6Ku/qjT # Y6hc3hsXMrS+U0yy+GWqAXam4ToWd2UQ1KYT70kZjE4YtL8Pbzg0c1ugMZyZZd/B # dHLiRu7hAWE6bTEm4XYRkA6Tl4KSFLFk43esaUeqGkH/wyW4N7OigizwJWeukcyI # PbAvjSabnf7+Pu0VrFgoiovRDiyx3zEdmcif/sYQsfch28bZeUz2rtY/9TCA6TD8 # dC3JE3rYkrhLULy7Dc90G6e8BlqmyIjlgp2+VqsS9/wQD7yFylIz0scmbKvFoW2j # NrbM1pD2T7m3XDCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI # hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz # dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow # YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290 # IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww # IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5 # 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH # hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6 # Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ # ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b # A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9 # WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU # tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo # ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J # vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP # orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB # Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr # oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt # MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF # BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw # BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH # vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8 # UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn # f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU # jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j # LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDGCA3ww # ggN4AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu # MUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IFRpbWVTdGFtcGluZyBSU0E0 # MDk2IFNIQTI1NiAyMDI1IENBMQIQCE/cM09+RU7bww+P+ZIYNTANBglghkgBZQME # AgEFAKCB0TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkF # MQ8XDTI2MTAwODE5MTQyMVowKwYLKoZIhvcNAQkQAgwxHDAaMBgwFgQUUdmr2gNJ # c9hPQmaspIJI5rNpxDkwLwYJKoZIhvcNAQkEMSIEIJQosuxvm0PObBngFKDndUiV # TqfBL48RtnF4ZyEj3ATuMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIC2gnaf0Ex+f # 5y22xebpyWVnVa8EPx6nQswNISDhQev8MA0GCSqGSIb3DQEBAQUABIICAHjdCehf # DJCHIFTpJ2pP0LeF/VHK0KzMq7NXuGizf3vEGvv5mLMyvTYfpa3SICOO/Nm8f/H5 # jHM5xRbSGBflTo2yJ9ym4Biv6I4n9vsS3Zj8vjYLXxgEnLrtCsVB/MSsni7QH0yM # VjCasCG4m+qBlj9uxpCsGLQjbiWSZWWeChk0xmHeHD4uuo+29v7hGwEARdiJJjQY # AMYIwEFs4qpMLviDy8gt5ACMDLjx1e++RrPVtLdVih2FTdaQ3SGzpvxhciLGZsw/ # wWqjk5u516YiEmjxtKzkqziqmq8hin9s+76K2cYuQF+pMqI1GLHX3ObC/YPXnJ9A # G4XNH4ZfXHTZE5+vNAxpNzwjgoZpHwc4JwfO/jsmlNL5bJFgTLuh4Vz6viS1HvFV # dZtUJSiV6NQbi/cYJGzgFpqxjkv+EN2kcPGrL0aQY/DGMlFm3Sf3jlWZqrlnmD7R # lCd+kCcokst7WY7mN6qX4KZEwwaQWdH1Hu8RnOc6RJ+OHvEKluyC0qOtYgmCThxs # EsWYzoXP/TE2Wa6OVszoo8EWj27L6ZXwXpeifXpW79c+MCvEXpY2UeZjHEq7D9RU # CmCXCyzDIaVPUfMDJpA4vpBaLF7cJSoquLfg2ANX2ZZb46/nMjMDGv+TrVTSpl1z # hpvR5DJNhYI0Ep4Ik6BNOYP/fI6WKrqzg3Tg # SIG # End signature block |