- Add units tests for Export-AzRuleData #28 - Update wording for Azure.Storage rule descriptions. - Add code coverage reporting. #29
This commit is contained in:
Родитель
4a08cee862
Коммит
39051188bc
|
@ -1,6 +1,8 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Add units tests for Export-AzRuleData and update filters. [#28](https://github.com/BernieWhite/PSRule.Rules.Azure/issues/28)
|
||||
|
||||
## v0.1.0-B190543
|
||||
|
||||
- Fix cannot find the type for custom attribute error. [#21](https://github.com/BernieWhite/PSRule.Rules.Azure/issues/21)
|
||||
|
|
|
@ -85,8 +85,8 @@ The output of this example is:
|
|||
RuleName Outcome Message
|
||||
-------- ------- -------
|
||||
Azure.Storage.UseReplication Fail Storage accounts not using GRS may be at risk
|
||||
Azure.Storage.SecureTransferRequ... Fail Storage accounts should only allow secure traffic
|
||||
Azure.Storage.SoftDelete Fail Soft delete is enabled on Storage Accounts
|
||||
Azure.Storage.SecureTransferRequ... Fail Storage accounts should only accept secure traffic
|
||||
Azure.Storage.SoftDelete Fail Enable soft delete on Storage Accounts
|
||||
```
|
||||
|
||||
A summary of results can be displayed by using `Invoke-PSRule -As Summary`.
|
||||
|
|
|
@ -26,10 +26,10 @@ Azure.SQL.FirewallRuleCount | Determine if there is an excessive number of firew
|
|||
Azure.SQL.AllowAzureAccess | Determine if access from Azure servers is required
|
||||
Azure.SQL.ThreatDetection | Enable threat detection for Azure SQL logical server
|
||||
Azure.SQL.Auditing | Enable auditing for Azure SQL logical server
|
||||
Azure.Storage.UseReplication | Use GRS for storage accounts that don't store disks
|
||||
Azure.Storage.SecureTransferRequired | Configure storage accounts to only access encrypted traffic i.e. HTTPS/SMB
|
||||
Azure.Storage.UseReplication | Storage accounts not using GRS may be at risk
|
||||
Azure.Storage.SecureTransferRequired | Storage accounts should only accept secure traffic
|
||||
Azure.Storage.UseEncryption | Storage Service Encryption (SSE) should be enabled
|
||||
Azure.Storage.SoftDelete | Soft delete is enabled on Storage Accounts
|
||||
Azure.Storage.SoftDelete | Enable soft delete on Storage Accounts
|
||||
Azure.Subscription.SecurityCenterContact | Security Center email and phone contact details should be set
|
||||
Azure.Subscription.SecurityCenterProvisioning | Enable auto-provisioning on VMs to improve Security Center insights
|
||||
Azure.VirtualMachine.UseManagedDisks | Virtual machines should use managed disks
|
||||
|
|
|
@ -62,7 +62,7 @@ function CopyModuleFiles {
|
|||
}
|
||||
}
|
||||
|
||||
task VersionModule PSRule, {
|
||||
task VersionModule ModuleDependencies, {
|
||||
$modulePath = Join-Path -Path $ArtifactPath -ChildPath PSRule.Rules.Azure;
|
||||
$manifestPath = Join-Path -Path $modulePath -ChildPath PSRule.Rules.Azure.psd1;
|
||||
Write-Verbose -Message "[VersionModule] -- Checking module path: $modulePath";
|
||||
|
@ -103,11 +103,9 @@ task VersionModule PSRule, {
|
|||
Update-ModuleManifest -Path $manifestPath -Prerelease $revision;
|
||||
}
|
||||
}
|
||||
|
||||
$manifest = Get-Content -Path $manifestPath -Raw;
|
||||
$manifest.Replace('RequiredModules = @()', "RequiredModules = @(@{ ModuleName = 'PSRule'; ModuleVersion = '0.5.0' }, @{ ModuleName = 'Az.Accounts'; ModuleVersion = '1.4.0' }, @{ ModuleName = 'Az.StorageSync'; ModuleVersion = '0.8.0' }, @{ ModuleName = 'Az.Security'; ModuleVersion = '0.7.4' }, @{ ModuleName = 'Az.Storage'; ModuleVersion = '1.1.1' }, @{ ModuleName = 'Az.Websites'; ModuleVersion = '1.1.2' }, @{ ModuleName = 'Az.Sql'; ModuleVersion = '1.7.0' })") | Set-Content -Path $manifestPath;
|
||||
}
|
||||
|
||||
# Synopsis: Publish to PowerShell Gallery
|
||||
task ReleaseModule VersionModule, {
|
||||
$modulePath = (Join-Path -Path $ArtifactPath -ChildPath PSRule.Rules.Azure);
|
||||
Write-Verbose -Message "[ReleaseModule] -- Checking module path: $modulePath";
|
||||
|
@ -116,7 +114,6 @@ task ReleaseModule VersionModule, {
|
|||
Write-Error -Message "[ReleaseModule] -- Module path does not exist";
|
||||
}
|
||||
elseif (![String]::IsNullOrEmpty($NuGetApiKey)) {
|
||||
# Publish to PowerShell Gallery
|
||||
Publish-Module -Path $modulePath -NuGetApiKey $NuGetApiKey;
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +165,28 @@ task platyPS {
|
|||
Import-Module -Name PlatyPS -Verbose:$False;
|
||||
}
|
||||
|
||||
# Synopsis: Install module dependencies
|
||||
task ModuleDependencies NuGet, PSRule, {
|
||||
if ($Null -eq (Get-InstalledModule -Name Az.Accounts -ErrorAction Ignore)) {
|
||||
Install-Module -Name Az.Accounts -Scope CurrentUser -Force;
|
||||
}
|
||||
if ($Null -eq (Get-InstalledModule -Name Az.Resources -ErrorAction Ignore)) {
|
||||
Install-Module -Name Az.Resources -Scope CurrentUser -Force;
|
||||
}
|
||||
if ($Null -eq (Get-InstalledModule -Name Az.Storage -ErrorAction Ignore)) {
|
||||
Install-Module -Name Az.Storage -Scope CurrentUser -Force -AllowClobber;
|
||||
}
|
||||
if ($Null -eq (Get-InstalledModule -Name Az.Security -ErrorAction Ignore)) {
|
||||
Install-Module -Name Az.Security -Scope CurrentUser -Force;
|
||||
}
|
||||
if ($Null -eq (Get-InstalledModule -Name Az.Sql -ErrorAction Ignore)) {
|
||||
Install-Module -Name Az.Sql -Scope CurrentUser -Force;
|
||||
}
|
||||
if ($Null -eq (Get-InstalledModule -Name Az.Websites -ErrorAction Ignore)) {
|
||||
Install-Module -Name Az.Websites -Scope CurrentUser -Force;
|
||||
}
|
||||
}
|
||||
|
||||
task CopyModule {
|
||||
CopyModuleFiles -Path src/PSRule.Rules.Azure -DestinationPath out/modules/PSRule.Rules.Azure;
|
||||
|
||||
|
@ -183,6 +202,11 @@ task TestRules PSRule, Pester, PSScriptAnalyzer, {
|
|||
# Run Pester tests
|
||||
$pesterParams = @{ Path = $PWD; OutputFile = 'reports/pester-unit.xml'; OutputFormat = 'NUnitXml'; PesterOption = @{ IncludeVSCodeMarker = $True }; PassThru = $True; };
|
||||
|
||||
if ($CodeCoverage) {
|
||||
$pesterParams.Add('CodeCoverage', (Join-Path -Path $PWD -ChildPath 'out/modules/**/*.psm1'));
|
||||
$pesterParams.Add('CodeCoverageOutputFile', (Join-Path -Path $PWD -ChildPath reports/pester-coverage.xml));
|
||||
}
|
||||
|
||||
if (!(Test-Path -Path reports)) {
|
||||
$Null = New-Item -Path reports -ItemType Directory -Force;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,14 @@ DotNetFrameworkVersion = '4.7.2'
|
|||
# ProcessorArchitecture = ''
|
||||
|
||||
# Modules that must be imported into the global environment prior to importing this module
|
||||
RequiredModules = @()
|
||||
RequiredModules = @(
|
||||
@{ ModuleName = 'PSRule'; ModuleVersion = '0.5.0' }
|
||||
@{ ModuleName = 'Az.Accounts'; ModuleVersion = '1.5.2' }
|
||||
@{ ModuleName = 'Az.Security'; ModuleVersion = '0.7.4' }
|
||||
@{ ModuleName = 'Az.Storage'; ModuleVersion = '1.3.0' }
|
||||
@{ ModuleName = 'Az.Sql'; ModuleVersion = '1.9.0' }
|
||||
@{ ModuleName = 'Az.Websites'; ModuleVersion = '1.2.1' }
|
||||
)
|
||||
|
||||
# Assemblies that must be loaded prior to importing this module
|
||||
# RequiredAssemblies = @()
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
|
||||
Set-StrictMode -Version latest;
|
||||
|
||||
# Set up some helper variables to make it easier to work with the module
|
||||
# $PSModule = $ExecutionContext.SessionState.Module;
|
||||
# $PSModuleRoot = $PSModule.ModuleBase;
|
||||
|
||||
#
|
||||
# Localization
|
||||
#
|
||||
|
@ -36,7 +32,7 @@ function Export-AzRuleData {
|
|||
process {
|
||||
|
||||
# Get subscriptions
|
||||
$context = GetAzureContext -Subscription $Subscription -Tenant $Tenant -Verbose:$VerbosePreference;
|
||||
$context = FindAzureContext -Subscription $Subscription -Tenant $Tenant -Verbose:$VerbosePreference;
|
||||
|
||||
if ($Null -eq $context) {
|
||||
return;
|
||||
|
@ -48,30 +44,31 @@ function Export-AzRuleData {
|
|||
|
||||
foreach ($c in $context) {
|
||||
$filePath = Join-Path -Path $OutputPath -ChildPath "$($c.Subscription.Id).json";
|
||||
GetAzureResource -Context $c -Verbose:$VerbosePreference | ConvertTo-Json -Depth 10 | Set-Content -Path $filePath;
|
||||
GetAzureResource -Context $c -Verbose:$VerbosePreference | ExportAzureResource -Path $filePath -Verbose:$VerbosePreference
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endreigon Public functions
|
||||
#endregion Public functions
|
||||
|
||||
#
|
||||
# Helper functions
|
||||
#
|
||||
|
||||
function GetAzureContext {
|
||||
function FindAzureContext {
|
||||
[CmdletBinding()]
|
||||
[OutputType([Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer[]])]
|
||||
param (
|
||||
[Parameter(Mandatory = $False)]
|
||||
[String[]]$Subscription,
|
||||
[String[]]$Subscription = $Null,
|
||||
|
||||
[Parameter(Mandatory = $False)]
|
||||
[String[]]$Tenant
|
||||
[String[]]$Tenant = $Null
|
||||
)
|
||||
|
||||
process {
|
||||
# Get subscriptions
|
||||
$context = Get-AzContext -ListAvailable;
|
||||
# Get subscription contexts
|
||||
$context = GetAzureContext;
|
||||
|
||||
if ($Null -eq $context) {
|
||||
Write-Error -Message "Could not find an existing context. Use Connect-AzAccount to establish a PowerShell context with Azure.";
|
||||
|
@ -79,14 +76,24 @@ function GetAzureContext {
|
|||
}
|
||||
|
||||
$filteredContext = $context | Where-Object -FilterScript {
|
||||
($Null -eq $Tenant -or $Tenant.Length -eq 0 -or $Tenant -contains $_.Tenant.Id) -and
|
||||
($Null -eq $Subscription -or $Subscription.Length -eq 0 -or $Subscription -contains $_.Subscription.Id -or $Subscription -contains $_.Subscription.Name)
|
||||
($Null -eq $Tenant -or $Tenant.Length -eq 0 -or ($_.Tenant.Id -in $Tenant)) -and
|
||||
($Null -eq $Subscription -or $Subscription.Length -eq 0 -or ($_.Subscription.Id -in $Subscription) -or ($_.Subscription.Name -in $Subscription))
|
||||
}
|
||||
|
||||
return $filteredContext;
|
||||
}
|
||||
}
|
||||
|
||||
function GetAzureContext {
|
||||
[CmdletBinding()]
|
||||
[OutputType([Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer[]])]
|
||||
param ( )
|
||||
process {
|
||||
# Get contexts
|
||||
return Get-AzContext -ListAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
function GetAzureResource {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
|
@ -100,6 +107,22 @@ function GetAzureResource {
|
|||
}
|
||||
}
|
||||
|
||||
function ExportAzureResource {
|
||||
[CmdletBinding()]
|
||||
[OutputType([void])]
|
||||
param (
|
||||
[Parameter(Mandatory = $True)]
|
||||
[String]$Path,
|
||||
|
||||
[Parameter(Mandatory = $False, ValueFromPipeline = $True)]
|
||||
[PSObject]$InputObject
|
||||
)
|
||||
|
||||
process {
|
||||
$InputObject | ConvertTo-Json -Depth 100 | Set-Content -Path $Path;
|
||||
}
|
||||
}
|
||||
|
||||
function VisitSqlServer {
|
||||
param (
|
||||
[Parameter(Mandatory = $True, ValueFromPipeline = $True)]
|
||||
|
@ -282,7 +305,7 @@ function ExpandResource {
|
|||
"Microsoft.Sql/servers" { VisitSqlServer @PSBoundParameters; }
|
||||
"Microsoft.DataFactory/factories" { VisitDataFactoryV2 @PSBoundParameters; }
|
||||
# "Microsoft.Storage/storageAccounts" { VisitStorageAccount @PSBoundParameters; }
|
||||
"Microsoft.StorageSync/storageSyncServices" { VisitStorageSyncService @PSBoundParameters; }
|
||||
# "Microsoft.StorageSync/storageSyncServices" { VisitStorageSyncService @PSBoundParameters; }
|
||||
# "Microsoft.Web/sites" { VisitWebApp @PSBoundParameters; }
|
||||
"Microsoft.RecoveryServices/vaults" { VisitRecoveryServices @PSBoundParameters; }
|
||||
"Microsoft.Compute/virtualMachines" { VisitVirtualMachine @PSBoundParameters; }
|
||||
|
@ -310,4 +333,4 @@ function SetResourceType {
|
|||
# Export module
|
||||
#
|
||||
|
||||
Export-ModuleMember -Function 'Export-AzRuleData'
|
||||
Export-ModuleMember -Function 'Export-AzRuleData';
|
||||
|
|
|
@ -2,35 +2,28 @@
|
|||
# Validation rules for Azure Storage Accounts
|
||||
#
|
||||
|
||||
# Description: Use GRS for storage accounts that don't store disks
|
||||
# Description: Storage accounts not using GRS may be at risk
|
||||
Rule 'Azure.Storage.UseReplication' -If { ResourceType 'Microsoft.Storage/storageAccounts' } -Tag @{ severity = 'Single point of failure'; category = 'Reliability' } {
|
||||
Hint 'Storage accounts not using GRS may be at risk'
|
||||
|
||||
$TargetObject.sku.name -eq 'Standard_GRS'
|
||||
Within 'sku.name' 'Standard_GRS', 'Standard_RAGRS'
|
||||
}
|
||||
|
||||
# Description: Configure storage accounts to only access encrypted traffic i.e. HTTPS/SMB
|
||||
# Description: Storage accounts should only accept secure traffic
|
||||
Rule 'Azure.Storage.SecureTransferRequired' -If { ResourceType 'Microsoft.Storage/storageAccounts' } -Tag @{ severity = 'Important'; category = 'Security configuration' } {
|
||||
Hint 'Storage accounts should only allow secure traffic'
|
||||
|
||||
$TargetObject.Properties.supportsHttpsTrafficOnly
|
||||
}
|
||||
|
||||
# Description: Storage Service Encryption (SSE) should be enabled
|
||||
Rule 'Azure.Storage.UseEncryption' -If { ResourceType 'Microsoft.Storage/storageAccounts' } -Tag @{ severity = 'Important'; category = 'Security configuration' } {
|
||||
Hint 'Storage accounts should have encryption enabled'
|
||||
|
||||
($Null -ne $TargetObject.Properties.encryption) -and
|
||||
($Null -ne $TargetObject.Properties.encryption.services.blob) -and
|
||||
($Null -ne $TargetObject.Properties.encryption.services.file) -and
|
||||
($TargetObject.Properties.encryption.services.blob.enabled -and $TargetObject.Properties.encryption.services.file.enabled)
|
||||
}
|
||||
|
||||
# Description: Soft delete is enabled on Storage Accounts
|
||||
# Description: Enable soft delete on Storage Accounts
|
||||
Rule 'Azure.Storage.SoftDelete' -If { ResourceType 'Microsoft.Storage/storageAccounts' } -Tag @{ severity = 'Important'; category = 'Data recovery' } {
|
||||
$serviceProperties = $TargetObject.resources | Where-Object -FilterScript {
|
||||
$_.ResourceType -eq 'serviceProperties'
|
||||
}
|
||||
|
||||
$serviceProperties.DeleteRetentionPolicy.Enabled
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
#
|
||||
# Unit tests for module cmdlets
|
||||
#
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
|
||||
)
|
||||
|
||||
# Setup error handling
|
||||
$ErrorActionPreference = 'Stop';
|
||||
Set-StrictMode -Version latest;
|
||||
|
||||
if ($Env:SYSTEM_DEBUG -eq 'true') {
|
||||
$VerbosePreference = 'Continue';
|
||||
}
|
||||
|
||||
# Setup tests paths
|
||||
$rootPath = $PWD;
|
||||
Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSRule.Rules.Azure) -Force;
|
||||
|
||||
#region Mocks
|
||||
|
||||
function MockContext {
|
||||
process {
|
||||
return @(
|
||||
(New-Object -TypeName Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext -ArgumentList @(
|
||||
[PSCustomObject]@{
|
||||
Subscription = [PSCustomObject]@{
|
||||
Id = '00000000-0000-0000-0000-000000000001'
|
||||
Name = 'Test subscription 1'
|
||||
State = 'Enabled'
|
||||
}
|
||||
Tenant = [PSCustomObject]@{
|
||||
Id = '00000000-0000-0000-0000-000000000001'
|
||||
}
|
||||
}
|
||||
)),
|
||||
(New-Object -TypeName Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext -ArgumentList @(
|
||||
[PSCustomObject]@{
|
||||
Subscription = [PSCustomObject]@{
|
||||
Id = '00000000-0000-0000-0000-000000000002'
|
||||
Name = 'Test subscription 2'
|
||||
State = 'Enabled'
|
||||
}
|
||||
Tenant = [PSCustomObject]@{
|
||||
Id = '00000000-0000-0000-0000-000000000002'
|
||||
}
|
||||
}
|
||||
))
|
||||
(New-Object -TypeName Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext -ArgumentList @(
|
||||
[PSCustomObject]@{
|
||||
Subscription = [PSCustomObject]@{
|
||||
Id = '00000000-0000-0000-0000-000000000003'
|
||||
Name = 'Test subscription 3'
|
||||
State = 'Enabled'
|
||||
}
|
||||
Tenant = [PSCustomObject]@{
|
||||
Id = '00000000-0000-0000-0000-000000000002'
|
||||
}
|
||||
}
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Mocks
|
||||
|
||||
#region Export-AzRuleData
|
||||
|
||||
Describe 'Export-AzRuleData' -Tag 'Cmdlet' {
|
||||
Context 'With defaults' {
|
||||
Mock -CommandName 'FindAzureContext' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith ${function:MockContext};
|
||||
Mock -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith {
|
||||
return [PSCustomObject]@{
|
||||
Name = 'Resource1'
|
||||
}
|
||||
}
|
||||
Mock -CommandName 'ExportAzureResource' -ModuleName 'PSRule.Rules.Azure' -Verifiable;
|
||||
$Null = Export-AzRuleData;
|
||||
|
||||
It 'Exports resources' {
|
||||
Assert-VerifiableMock;
|
||||
Assert-MockCalled -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Times 3;
|
||||
}
|
||||
}
|
||||
|
||||
Context 'With filters' {
|
||||
Mock -CommandName 'ExportAzureResource' -ModuleName 'PSRule.Rules.Azure';
|
||||
Mock -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -MockWith ${function:MockContext};
|
||||
|
||||
It 'Uses subscription name filter' {
|
||||
Mock -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure';
|
||||
$Null = Export-AzRuleData -Subscription 'Test subscription 1';
|
||||
Assert-MockCalled -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Times 1;
|
||||
}
|
||||
|
||||
It 'Uses subscription Id filter' {
|
||||
Mock -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure';
|
||||
$Null = Export-AzRuleData -Subscription '00000000-0000-0000-0000-000000000002';
|
||||
Assert-MockCalled -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Times 1;
|
||||
}
|
||||
|
||||
It 'Uses tenant Id filter' {
|
||||
Mock -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure';
|
||||
$Null = Export-AzRuleData -Tenant '00000000-0000-0000-0000-000000000002';
|
||||
Assert-MockCalled -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Times 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Export-AzRuleData
|
Загрузка…
Ссылка в новой задаче