зеркало из
1
0
Форкнуть 0

Export rule data from templates #145 (#147)

This commit is contained in:
Bernie White 2019-11-09 16:19:07 +10:00 коммит произвёл GitHub
Родитель c0d08c876d
Коммит ad9230dffa
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
41 изменённых файлов: 6363 добавлений и 78 удалений

Просмотреть файл

@ -32,10 +32,10 @@ stages:
matrix:
Linux:
displayName: 'Linux'
imageName: 'ubuntu-16.04'
imageName: 'ubuntu-latest'
MacOS:
displayName: 'MacOS'
imageName: 'macos-10.13'
imageName: 'macOS-latest'
Windows:
displayName: 'Windows'
imageName: 'vs2017-win2016'
@ -55,6 +55,36 @@ stages:
- powershell: Invoke-Build -Configuration $(buildConfiguration) -Build $(Build.BuildNumber)
displayName: 'Build module'
# Run SonarCloud analysis
- powershell: dotnet tool install --global dotnet-sonarscanner
displayName: 'Install Sonar scanner'
condition: and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['analysis'], 'true'))
- script: dotnet sonarscanner begin /k:"BernieWhite_PSRule_Rules_Azure" /o:"berniewhite-github" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.login=$(sonarQubeToken) /v:"$(Build.BuildNumber)" /d:sonar.cs.vscoveragexml.reportsPaths="reports/" /d:sonar.cs.xunit.reportsPaths="reports/"
displayName: 'Prepare SonarCloud'
condition: and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['analysis'], 'true'))
- script: dotnet build
displayName: 'Build solution for analysis'
condition: and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['analysis'], 'true'))
- script: dotnet sonarscanner end /d:sonar.login=$(sonarQubeToken)
displayName: 'Complete SonarCloud'
condition: and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['analysis'], 'true'))
# DotNet test results
- task: PublishTestResults@2
displayName: 'Publish unit test results'
inputs:
testRunTitle: 'DotNet on $(imageName)'
testRunner: VSTest
testResultsFiles: 'reports/*.trx'
mergeTestResults: true
platform: $(imageName)
configuration: $(buildConfiguration)
publishRunAttachments: true
condition: succeededOrFailed()
# Pester test results
- task: PublishTestResults@2
displayName: 'Publish Pester results'

67
.markdownlint.json Normal file
Просмотреть файл

@ -0,0 +1,67 @@
{
"default": true,
"header-increment": true,
"first-header-h1": {
"level": 1
},
"header-style": {
"style": "atx"
},
"ul-style": {
"style": "dash"
},
"list-indent": true,
"ul-start-left": true,
"ul-indent": {
"indent": 2
},
"no-trailing-spaces": true,
"no-hard-tabs": true,
"no-reversed-links": true,
"no-multiple-blanks": true,
"line-length": {
"line_length": 100,
"code_blocks": false,
"tables": false,
"headers": true
},
"commands-show-output": true,
"no-missing-space-atx": true,
"no-multiple-space-atx": true,
"no-missing-space-closed-atx": true,
"no-multiple-space-closed-atx": true,
"blanks-around-headers": true,
"header-start-left": true,
"no-duplicate-header": true,
"single-h1": true,
"no-trailing-punctuation": {
"punctuation": ".,;:!"
},
"no-multiple-space-blockquote": true,
"no-blanks-blockquote": true,
"ol-prefix": {
"style": "one_or_ordered"
},
"list-marker-space": true,
"blanks-around-fences": true,
"blanks-around-lists": true,
"no-bare-urls": true,
"hr-style": {
"style": "---"
},
"no-emphasis-as-header": true,
"no-space-in-emphasis": true,
"no-space-in-code": true,
"no-space-in-links": true,
"fenced-code-language": false,
"first-line-h1": false,
"no-empty-links": true,
"proper-names": {
"names": [
"PowerShell",
"JavaScript"
],
"code_blocks": false
},
"no-alt-text": true
}

Просмотреть файл

@ -4,6 +4,9 @@
- Updated `Azure.AKS.Version` to 1.14.8. [#140](https://github.com/BernieWhite/PSRule.Rules.Azure/issues/140)
- Updated rules to use type pre-conditions. [#144](https://github.com/BernieWhite/PSRule.Rules.Azure/issues/144)
- **Experimental**: Added support for exporting rule data from templates. [#145](https://github.com/BernieWhite/PSRule.Rules.Azure/issues/145)
- Added `Export-AzTemplateRuleData` cmdlet to export templates. See cmdlet help for limitations.
- Template and parameters are merged, resolving functions, copy loops and conditions.
## v0.5.0

31
PSRule.Rules.Azure.sln Normal file
Просмотреть файл

@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29215.179
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Rules.Azure", "src\PSRule.Rules.Azure\PSRule.Rules.Azure.csproj", "{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Rules.Azure.Tests", "tests\PSRule.Rules.Azure.Tests\PSRule.Rules.Azure.Tests.csproj", "{DE4D9717-0307-41F3-897D-1B40ED8235CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C6CA67C-5B17-47BD-AD84-F5D0F9D152F2}.Release|Any CPU.Build.0 = Release|Any CPU
{DE4D9717-0307-41F3-897D-1B40ED8235CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE4D9717-0307-41F3-897D-1B40ED8235CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE4D9717-0307-41F3-897D-1B40ED8235CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE4D9717-0307-41F3-897D-1B40ED8235CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DB365C69-CCDD-47D3-8526-97400A01E2EE}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -8,7 +8,8 @@ A suite of rules to validate Azure resources using PSRule.
This project is to be considered a **proof-of-concept** and **not a supported product**.
For issues with rules and documentation please check our GitHub [issues](https://github.com/BernieWhite/PSRule.Rules.Azure/issues) page. If you do not see your problem captured, please file a new issue and follow the provided template.
For issues with rules and documentation please check our GitHub [issues](https://github.com/BernieWhite/PSRule.Rules.Azure/issues) page.
If you do not see your problem captured, please file a new issue and follow the provided template.
If you have any problems with the [PSRule][engine] engine, please check the project GitHub [issues](https://github.com/Microsoft/PSRule/issues) page instead.
@ -26,7 +27,8 @@ PSRule.Rules.Azure | Validate Azure resources | [latest][module] / [instructions
### Export resource data
To validate Azure resources running in a subscription, export the resource data with the `Export-AzRuleData` cmdlet. The `Export-AzRuleData` cmdlet exports a resource graph for one or more subscriptions that can be used for analysis with the rules in this module.
To validate Azure resources running in a subscription, export the resource data with the `Export-AzRuleData` cmdlet.
The `Export-AzRuleData` cmdlet exports a resource graph for one or more subscriptions that can be used for analysis with the rules in this module.
By default, resources for the current subscription context are exported. See below for more options.
@ -91,7 +93,8 @@ For example:
Export-AzRuleData -All;
```
To filter results to only failed rules, use `Invoke-PSRule -Outcome Fail`. Passed, failed and error results are shown by default.
To filter results to only failed rules, use `Invoke-PSRule -Outcome Fail`.
Passed, failed and error results are shown by default.
For example:
@ -145,17 +148,20 @@ The following rules are included in the `PSRule.Rules.Azure` module:
## Language reference
PSRule.Rules.Azure extends PowerShell the following cmdlets.
PSRule.Rules.Azure extends PowerShell with the following cmdlets.
### Commands
The following commands exist in the `PSRule.Rules.Azure` module:
- [Export-AzRuleData](docs/commands/PSRule.Rules.Azure/en-US/Export-AzRuleData.md) - Export resource configuration data from one or more Azure subscriptions.
- [Export-AzTemplateRuleData](docs/commands/PSRule.Rules.Azure/en-US/Export-AzTemplateRuleData.md) - Export resource configuration data from Azure templates.
## Changes and versioning
Modules in this repository will use the [semantic versioning](http://semver.org/) model to declare breaking changes from v1.0.0. Prior to v1.0.0, breaking changes may be introduced in minor (0.x.0) version increments. For a list of module changes please see the [change log](CHANGELOG.md).
Modules in this repository will use the [semantic versioning](http://semver.org/) model to declare breaking changes from v1.0.0.
Prior to v1.0.0, breaking changes may be introduced in minor (0.x.0) version increments.
For a list of module changes please see the [change log](CHANGELOG.md).
> Pre-release module versions are created on major commits and can be installed from the PowerShell Gallery. Pre-release versions should be considered experimental. Modules and change log details for pre-releases will be removed as standard releases are made available.

Просмотреть файл

@ -16,7 +16,7 @@ Export resource configuration data from one or more Azure subscriptions.
### Default (Default)
```text
Export-AzRuleData [[-OutputPath] <String>] [[-Subscription] <String[]>] [[-Tenant] <String[]>]
Export-AzRuleData [[-OutputPath] <String>] [-Subscription <String[]>] [-Tenant <String[]>]
[-ResourceGroupName <String[]>] [-Tag <Hashtable>] [-PassThru] [-WhatIf] [-Confirm] [<CommonParameters>]
```
@ -29,11 +29,12 @@ Export-AzRuleData [[-OutputPath] <String>] [-ResourceGroupName <String[]>] [-Tag
## DESCRIPTION
Export resource configuration data from one or more Azure subscriptions.
Export resource configuration data from deployed resources in one or more Azure subscriptions.
If no filters are specified then the current subscription context will be exported. i.e. `Get-AzContext`
To export all subscriptions contexts use the `-All` switch. When the `-All` switch is used, all subscriptions contexts will be exported. i.e. `Get-AzContext -ListAvailable`
To export all subscriptions contexts use the `-All` switch.
When the `-All` switch is used, all subscriptions contexts will be exported. i.e. `Get-AzContext -ListAvailable`
Resource data will be exported to the current working directory by default as JSON files, one per subscription.
@ -46,14 +47,14 @@ PS C:\> Export-AzRuleData
```
```text
Directory: C:\
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 1/07/2019 10:03 AM 7304948 00000000-0000-0000-0000-000000000001.json
```
Export information from current subscription context.
Export resource configuration data from current subscription context.
### Example 2
@ -62,7 +63,7 @@ PS C:\> Export-AzRuleData -Subscription 'Contoso Production', 'Contoso Non-produ
```
```text
Directory: C:\
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
@ -70,7 +71,7 @@ Mode LastWriteTime Length Name
-a---- 1/07/2019 10:03 AM 7304948 00000000-0000-0000-0000-000000000002.json
```
Export information from subscriptions by name.
Export resource configuration data from subscriptions by name.
### Example 3
@ -79,17 +80,34 @@ PS C:\> Export-AzRuleData -ResourceGroupName 'rg-app1-web', 'rg-app1-db'
```
```text
Directory: C:\
Directory: C:\
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 1/07/2019 10:03 AM 7304948 00000000-0000-0000-0000-000000000001.json
```
Export information from two resource groups within the current subscription context.
Export resource configuration data from two resource groups within the current subscription context.
## PARAMETERS
### -All
By default, resources from the current subscription context are extracted.
Use `-All` to extract resource data for all subscription contexts instead.
```yaml
Type: SwitchParameter
Parameter Sets: All
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -OutputPath
The path to store generated JSON files containing resources.
@ -106,33 +124,18 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -Subscription
### -PassThru
Optionally filter resources by subscription, Id or Name.
By default, FileInfo objects are returned to the pipeline for each JSON file created.
When `-PassThru` is specified, JSON files are not created and Azure resource objects are returned to the pipeline instead.
```yaml
Type: String[]
Parameter Sets: Default
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: 1
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -Tenant
Optionally filter resources by a unique Tenant identifer.
```yaml
Type: String[]
Parameter Sets: Default
Aliases:
Required: False
Position: 2
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
@ -154,6 +157,22 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -Subscription
Optionally filter resources by subscription, Id or Name.
```yaml
Type: String[]
Parameter Sets: Default
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -Tag
Optionally filter resources based on tag.
@ -170,29 +189,13 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -PassThru
### -Tenant
By default, FileInfo objects are returned to the pipeline for each JSON file created. When `-PassThru` is specified, JSON files are not created and Azure resource objects are returned to the pipeline instead.
Optionally filter resources by a unique Tenant identifer.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -All
By default, resources from the current subscription context are extracted. Use `-All` to extract resource data for all subscription contexts instead.
```yaml
Type: SwitchParameter
Parameter Sets: All
Type: String[]
Parameter Sets: Default
Aliases:
Required: False
@ -246,8 +249,14 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
### System.IO.FileInfo
Return `FileInfo` for each of the output files created, one per subscription.
This is the default.
### PSObject
Return an object for each Azure resource, and configuration exported.
This is returned when the `-PassThru` switch is used.
## NOTES
## RELATED LINKS

Просмотреть файл

@ -0,0 +1,177 @@
---
external help file: PSRule.Rules.Azure-help.xml
Module Name: PSRule.Rules.Azure
online version: https://github.com/BernieWhite/PSRule.Rules.Azure/blob/master/docs/commands/PSRule.Rules.Azure/en-US/Export-AzTemplateRuleData.md
schema: 2.0.0
---
# Export-AzTemplateRuleData
## SYNOPSIS
Export resource configuration data from Azure templates.
## SYNTAX
```text
Export-AzTemplateRuleData [-TemplateFile] <String> [[-ParameterFile] <String[]>]
[[-ResourceGroupName] <String>] [[-Subscription] <String>] [[-OutputPath] <String>] [-PassThru]
[<CommonParameters>]
```
## DESCRIPTION
Export resource configuration data by merging Azure Resource Manager (ARM) template and parameter files.
Template and parameters are merged by resolving template parameters, variables and functions.
By default this is an offline process, requiring no connectivity to Azure.
Some functions that may be included in templates dynamically query Azure for current state.
For these functions standard placeholder values are used by default.
Functions that use placeholders include `subscription`, `resourceGroup`, `reference`, `list*`.
This function does not check template files for strict compliance with Azure schemas.
Currently the following limitations also apply:
- Deployments are not expanded. Deployment is returned instead of resources in deployment.
- The following functions are not supported:
- Array: `array`, `coalesce`, `createArray`, `intersection`
- Deployment: `deployment`
- Resource: `providers`
- String: `dataUri`, `dataUriToString`
- References to Key Vault secrets are not expanded. A placeholder value is used instead.
- Multi-line strings are not supported.
## EXAMPLES
### Example 1
```powershell
PS C:\> Export-AzTemplateRuleData -TemplateFile .\azuredeploy.json -ParameterFile .\azuredeploy.parameters.json -OutputPath .\out-deploy.json
```
Export resource configuration data based on merging a template and parameter file together.
## PARAMETERS
### -TemplateFile
The absolute or relative file path to an Azure Resource Manager template file.
```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: True
Position: 0
Default value: None
Accept pipeline input: True (ByPropertyName)
Accept wildcard characters: False
```
### -ParameterFile
The absolute or relative file path to one or more Azure Resource Manager template parameter files.
```yaml
Type: String[]
Parameter Sets: (All)
Aliases: TemplateParameterFile
Required: False
Position: 1
Default value: None
Accept pipeline input: True (ByPropertyName)
Accept wildcard characters: False
```
### -OutputPath
The path to store generated JSON files containing resources.
```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: 4
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -PassThru
By default, FileInfo objects are returned to the pipeline for each JSON file created.
When `-PassThru` is specified, JSON files are not created and Azure resource objects are returned to the pipeline instead.
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -ResourceGroupName
The name of the Resource Group where the deployment will occur.
If this option is specified, the properties of the Resource Group will be looked up and used during export.
This Resource Group specified here will be used to resolve the `resourceGroup()` function.
```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: 2
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### -Subscription
The name of the Subscription where the deployment will occur.
If this option is specified, the properties of the Subscription will be looked up and used during export.
This subscription specified here will be used to resolve the `subscription()` function.
```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: 3
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
## INPUTS
### System.String
### System.String[]
## OUTPUTS
### System.Object
## NOTES
## RELATED LINKS

Просмотреть файл

@ -151,8 +151,8 @@ task PSScriptAnalyzer NuGet, {
# Synopsis: Install PSRule
task PSRule NuGet, {
if ($Null -eq (Get-InstalledModule -Name PSRule -MinimumVersion 0.10.0 -ErrorAction Ignore)) {
Install-Module -Name PSRule -Repository PSGallery -MinimumVersion 0.10.0 -Scope CurrentUser -Force;
if ($Null -eq (Get-InstalledModule -Name PSRule -MinimumVersion 0.11.0 -ErrorAction Ignore)) {
Install-Module -Name PSRule -Repository PSGallery -MinimumVersion 0.11.0 -Scope CurrentUser -Force;
}
Import-Module -Name PSRule -Verbose:$False;
}
@ -186,14 +186,37 @@ task ModuleDependencies NuGet, PSRule, {
}
}
task BuildDotNet {
exec {
# Build library
# Add build version -p:versionPrefix=$ModuleVersion
dotnet publish src/PSRule.Rules.Azure -c $Configuration -f netstandard2.0 -o $(Join-Path -Path $PWD -ChildPath out/modules/PSRule.Rules.Azure)
}
}
task TestDotNet {
if ($CodeCoverage) {
exec {
# Test library
dotnet test --collect:"Code Coverage" --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Rules.Azure.Tests
}
}
else {
exec {
# Test library
dotnet test --logger trx -r (Join-Path $PWD -ChildPath reports/) tests/PSRule.Rules.Azure.Tests
}
}
}
task CopyModule {
CopyModuleFiles -Path src/PSRule.Rules.Azure -DestinationPath out/modules/PSRule.Rules.Azure;
}
# Synopsis: Build modules only
task BuildModule CopyModule
task BuildModule BuildDotNet, CopyModule
task TestRules PSRule, Pester, PSScriptAnalyzer, {
task TestRules TestDotNet, PSRule, Pester, PSScriptAnalyzer, {
# Run Pester tests
$pesterParams = @{ Path = $PWD; OutputFile = 'reports/pester-unit.xml'; OutputFormat = 'NUnitXml'; PesterOption = @{ IncludeVSCodeMarker = $True }; PassThru = $True; };

Просмотреть файл

@ -15,6 +15,9 @@ repository:
bugs:
url: https://github.com/BernieWhite/PSRule.Rules.Azure/issues
modules:
PSRule: ^0.11.0
tasks:
clear:
steps:

Просмотреть файл

@ -0,0 +1,5 @@
[*.cs]
# Suppressed, issue with Azure DevOps build on MacOS, not supported
# IDE0034: Simplify 'default' expression
csharp_prefer_simple_default_expression = true:silent

Просмотреть файл

@ -0,0 +1,221 @@
using Newtonsoft.Json;
using PSRule.Rules.Azure.Pipeline;
using PSRule.Rules.Azure.Resources;
using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
namespace PSRule.Rules.Azure
{
/// <summary>
/// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML.
/// </summary>
internal sealed class PSObjectJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(PSObject);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (!(value is PSObject obj))
throw new ArgumentException(message: PSRuleResources.SerializeNullPSObject, paramName: nameof(value));
if (value is FileSystemInfo fileSystemInfo)
{
WriteFileSystemInfo(writer, fileSystemInfo, serializer);
return;
}
writer.WriteStartObject();
foreach (var property in obj.Properties)
{
// Ignore properties that are not readable or can cause race condition
if (!property.IsGettable || property.Value is PSDriveInfo || property.Value is ProviderInfo || property.Value is DirectoryInfo)
continue;
writer.WritePropertyName(property.Name);
serializer.Serialize(writer, property.Value);
}
writer.WriteEndObject();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Create target object based on JObject
var result = existingValue as PSObject ?? new PSObject();
// Read tokens
ReadObject(value: result, reader: reader);
return result;
}
private void ReadObject(PSObject value, JsonReader reader)
{
if (reader.TokenType != JsonToken.StartObject)
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
reader.Read();
string name = null;
// Read each token
while (reader.TokenType != JsonToken.EndObject)
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
name = reader.Value.ToString();
break;
case JsonToken.StartObject:
var child = new PSObject();
ReadObject(value: child, reader: reader);
value.Properties.Add(new PSNoteProperty(name: name, value: child));
break;
case JsonToken.StartArray:
var items = new List<object>();
reader.Read();
while (reader.TokenType != JsonToken.EndArray)
{
items.Add(ReadValue(reader));
reader.Read();
}
value.Properties.Add(new PSNoteProperty(name: name, value: items.ToArray()));
break;
default:
value.Properties.Add(new PSNoteProperty(name: name, value: reader.Value));
break;
}
reader.Read();
}
}
private object ReadValue(JsonReader reader)
{
if (reader.TokenType != JsonToken.StartObject)
return reader.Value;
var value = new PSObject();
ReadObject(value, reader);
return value;
}
private void WriteFileSystemInfo(JsonWriter writer, FileSystemInfo value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.FullName);
}
}
/// <summary>
/// A custom serializer to convert PSObjects that may or maynot be in a JSON array to an a PSObject array.
/// </summary>
internal sealed class PSObjectArrayJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(PSObject[]);
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject && reader.TokenType != JsonToken.StartArray)
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
var result = new List<PSObject>();
var isArray = reader.TokenType == JsonToken.StartArray;
if (isArray)
reader.Read();
while (!isArray || (isArray && reader.TokenType != JsonToken.EndArray))
{
var value = ReadObject(reader: reader);
result.Add(value);
// Consume the EndObject token
if (isArray)
{
reader.Read();
}
}
return result.ToArray();
}
private PSObject ReadObject(JsonReader reader)
{
if (reader.TokenType != JsonToken.StartObject)
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
reader.Read();
var result = new PSObject();
string name = null;
// Read each token
while (reader.TokenType != JsonToken.EndObject)
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
name = reader.Value.ToString();
break;
case JsonToken.StartObject:
var value = ReadObject(reader: reader);
result.Properties.Add(new PSNoteProperty(name: name, value: value));
break;
case JsonToken.StartArray:
var items = ReadArray(reader: reader);
result.Properties.Add(new PSNoteProperty(name: name, value: items));
break;
default:
result.Properties.Add(new PSNoteProperty(name: name, value: reader.Value));
break;
}
reader.Read();
}
return result;
}
private PSObject[] ReadArray(JsonReader reader)
{
if (reader.TokenType != JsonToken.StartArray)
throw new PipelineSerializationException(PSRuleResources.ReadJsonFailed);
reader.Read();
var result = new List<PSObject>();
while (reader.TokenType != JsonToken.EndArray)
{
if (reader.TokenType == JsonToken.StartObject)
{
result.Add(ReadObject(reader: reader));
}
else if (reader.TokenType == JsonToken.StartArray)
{
result.Add(PSObject.AsPSObject(ReadArray(reader)));
}
else
{
result.Add(PSObject.AsPSObject(reader.Value));
}
reader.Read();
}
return result.ToArray();
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
using System.Management.Automation;
namespace PSRule.Rules.Azure
{
internal static class PSObjectExtensions
{
internal static T GetPropertyValue<T>(this PSObject obj, string propertyName)
{
return (T)obj.Properties[propertyName].Value;
}
}
}

Просмотреть файл

@ -0,0 +1,53 @@
using System.IO;
using System.Management.Automation;
namespace PSRule.Rules.Azure.Configuration
{
/// <summary>
/// A delgate to allow callback to PowerShell to get current working path.
/// </summary>
internal delegate string PathDelegate();
public sealed class PSRuleOption
{
/// <summary>
/// A callback that is overridden by PowerShell so that the current working path can be retrieved.
/// </summary>
private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory();
/// <summary>
/// Set working path from PowerShell host environment.
/// </summary>
/// <param name="executionContext">An $ExecutionContext object.</param>
/// <remarks>
/// Called from PowerShell.
/// </remarks>
public static void UseExecutionContext(EngineIntrinsics executionContext)
{
if (executionContext == null)
{
_GetWorkingPath = () => Directory.GetCurrentDirectory();
return;
}
_GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path;
}
public static string GetWorkingPath()
{
return _GetWorkingPath();
}
/// <summary>
/// Get a full path instead of a relative path that may be passed from PowerShell.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
internal static string GetRootedPath(string path)
{
return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(GetWorkingPath(), path));
}
}
}

Просмотреть файл

@ -0,0 +1,220 @@
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Resources;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using static PSRule.Rules.Azure.Data.Template.TemplateVisitor;
namespace PSRule.Rules.Azure.Data.Template
{
internal delegate object ExpressionFnOuter(TemplateContext context);
internal delegate object ExpressionFn(TemplateContext context, object[] args);
internal sealed class ExpressionBuilder
{
private readonly ExpressionFactory _Functions;
internal ExpressionBuilder()
{
_Functions = new ExpressionFactory();
}
internal ExpressionFnOuter Build(string s)
{
return Lexer(Parse(s));
}
private TokenStream Parse(string s)
{
return ExpressionParser.Parse(s);
}
private ExpressionFnOuter Lexer(TokenStream stream)
{
return stream.TryTokenType(ExpressionTokenType.Element, out ExpressionToken element) ? Element(stream, element) : null;
}
private ExpressionFnOuter Element(TokenStream stream, ExpressionToken element)
{
ExpressionFnOuter result = null;
// function
if (stream.Skip(ExpressionTokenType.GroupStart))
{
if (!_Functions.TryDescriptor(element.Content, out IFunctionDescriptor descriptor))
throw new NotImplementedException(string.Format(PSRuleResources.FunctionNotFound, element.Content));
var fnParams = new List<ExpressionFnOuter>();
while (!stream.Skip(ExpressionTokenType.GroupEnd))
{
fnParams.Add(Inner(stream));
}
var aParams = fnParams.ToArray();
result = (context) => descriptor.Invoke(context, aParams);
while (stream.TryTokenType(ExpressionTokenType.IndexStart, out ExpressionToken token) || stream.TryTokenType(ExpressionTokenType.Property, out token))
{
if (token.Type == ExpressionTokenType.IndexStart)
{
// Invert: index(fn1(p1, p2), 0)
var inner = Inner(stream);
ExpressionFnOuter outer = AddIndex(result, inner);
result = outer;
stream.Skip(ExpressionTokenType.IndexEnd);
}
else if (token.Type == ExpressionTokenType.Property)
{
// Invert: property(fn1(p1, p2), "name")
ExpressionFnOuter outer = AddProperty(result, token.Content);
result = outer;
}
}
}
// integer
else
{
result = (context) => element.Content;
}
return result;
}
private ExpressionFnOuter Inner(TokenStream stream)
{
ExpressionFnOuter result = null;
if (stream.TryTokenType(ExpressionTokenType.String, out ExpressionToken token))
{
result = (context) => token.Content;
}
else if (stream.TryTokenType(ExpressionTokenType.Element, out token))
{
result = Element(stream, token);
}
return result;
}
private static ExpressionFnOuter AddIndex(ExpressionFnOuter inner, ExpressionFnOuter innerInner)
{
return (context) => Index(context, inner, innerInner);
}
private static object Index(TemplateContext context, ExpressionFnOuter inner, ExpressionFnOuter index)
{
var array = inner(context);
var indexResult = index(context);
if (indexResult is string ixs)
return Index(array, int.Parse(ixs));
else
return Index(array, (int)indexResult);
}
private static object Index(object value, int index)
{
if (value is JArray jArray)
return jArray[index];
else if (value is Array array)
return array.GetValue(index);
return null;
}
private static ExpressionFnOuter AddProperty(ExpressionFnOuter inner, string propertyName)
{
return (context) => Property(context, inner, propertyName);
}
private static object Property(TemplateContext context, ExpressionFnOuter inner, string propertyName)
{
var result = inner(context);
if (result is JToken jt)
return jt[propertyName].Value<object>();
if (result is JObject jobj)
return jobj[propertyName].Value<object>();
if (result is MockResource mockResource)
return JToken.FromObject(string.Concat("{{Resource.", propertyName, "}}"));
if (result is MockResourceList mockResourceList)
return JToken.FromObject(string.Concat("{{List.", propertyName, "}}"));
return PropertyOrField(result, propertyName);
}
private static object PropertyOrField(object obj, string propertyName)
{
// Try property
var resultType = obj.GetType();
var property = resultType.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public);
if (property != null)
return property.GetValue(obj);
// Try field
var field = resultType.GetField(propertyName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.GetField | BindingFlags.Public);
if (field == null)
throw new ArgumentException();
return field.GetValue(obj);
}
}
internal sealed class ExpressionFactory
{
private readonly Dictionary<string, IFunctionDescriptor> _Descriptors;
public ExpressionFactory()
{
_Descriptors = new Dictionary<string, IFunctionDescriptor>();
foreach (var d in Functions.Builtin)
With(d);
}
public bool TryDescriptor(string name, out IFunctionDescriptor descriptor)
{
return IsList(name) ? _Descriptors.TryGetValue("list", out descriptor) : _Descriptors.TryGetValue(name, out descriptor);
}
private void With(IFunctionDescriptor descriptor)
{
_Descriptors.Add(descriptor.Name, descriptor);
}
private static bool IsList(string name)
{
return name.StartsWith("list", StringComparison.OrdinalIgnoreCase);
}
}
[DebuggerDisplay("Function: {Name}")]
internal sealed class FunctionDescriptor : IFunctionDescriptor
{
private readonly ExpressionFn Fn;
public FunctionDescriptor(string name, ExpressionFn fn)
{
Name = name;
Fn = fn;
}
public string Name { get; }
public object Invoke(TemplateContext context, ExpressionFnOuter[] args)
{
var parameters = new object[args.Length];
for (var i = 0; i < args.Length; i++)
parameters[i] = args[i](context);
return Fn(context, parameters);
}
}
internal interface IFunctionDescriptor
{
string Name { get; }
object Invoke(TemplateContext context, ExpressionFnOuter[] args);
}
}

Просмотреть файл

@ -0,0 +1,111 @@
namespace PSRule.Rules.Azure.Data.Template
{
/// <summary>
/// A helper class used to parse template expressions.
/// </summary>
internal static class ExpressionParser
{
/// <summary>
/// Tokenize an expression.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns>A stream of tokens representing the expression.</returns>
/// <example>
/// [parameters('vnetName')]
/// [concat('route-', parameters('subnets')[copyIndex('routeIndex')].name)]
/// [concat(split(parameters('addressPrefix')[0], '/')[0], '/27')]
/// </example>
internal static TokenStream Parse(string expression)
{
var stream = new ExpressionStream(expression);
var output = new TokenStream();
stream.Start();
while (!stream.End())
{
if (stream.TryElement(out string element))
{
output.Function(element);
Function(stream, output, element);
}
if (Index(stream, output))
{
stream.Separator();
}
if (stream.CaptureProperty(out string propertyName))
{
output.Property(propertyName);
stream.Separator();
}
}
output.MoveTo(0);
return output;
}
/// <summary>
/// Enter a function.
/// </summary>
/// <example>
/// function()
/// </example>
private static bool Function(ExpressionStream stream, TokenStream output, string element)
{
// Look for '('
if (!stream.IsGroupStart())
return false;
output.GroupStart();
while (!stream.IsGroupEnd())
Inner(stream, output);
output.GroupEnd();
return true;
}
/// <summary>
/// Enter an index.
/// </summary>
/// <example>
/// function()[0]
/// </example>
private static bool Index(ExpressionStream stream, TokenStream output)
{
// Look for '['
if (!stream.IsIndexStart())
return false;
output.IndexStart();
while (!stream.IsIndexEnd())
Inner(stream, output);
output.IndexEnd();
return true;
}
/// <summary>
/// Parse inner tokens.
/// </summary>
private static void Inner(ExpressionStream stream, TokenStream output)
{
if (stream.CaptureString(out string s))
{
output.String(s);
stream.Separator();
}
else if (Index(stream, output))
{
stream.Separator();
}
else if (stream.CaptureProperty(out string propertyName))
{
output.Property(propertyName);
stream.Separator();
}
else if (stream.TryElement(out string element))
{
output.Function(element);
Function(stream, output, element);
stream.Separator();
}
}
}
}

Просмотреть файл

@ -0,0 +1,312 @@
using System;
using System.Diagnostics;
using System.Linq;
namespace PSRule.Rules.Azure.Data.Template
{
/// <summary>
/// A string stream for reading tokenizing template expressions.
/// </summary>
[DebuggerDisplay("Position = {Position}, Current = {Current}")]
internal sealed class ExpressionStream
{
private readonly string _Source;
private readonly int _Length;
/// <summary>
/// The current character position in the expression string. Call Next() to change the position.
/// </summary>
private int _Position;
private int _Line;
private int _Column;
private char _Current;
private char _Previous;
private int _EscapeLength;
// The maximum length of a template expression
private const int MaxLength = 24576;
private const char Whitespace = ' ';
private const char Backslash = '\\';
private const char Apostrophe = '\'';
private const char Comma = ',';
private const char Period = '.';
private const char ParenthesesOpen = '(';
private const char ParenthesesClose = ')';
private const char BracketOpen = '[';
private const char BracketClose = ']';
private readonly static char[] FunctionNameStopCharacter = new char[] { '(', ']', '[', ')', '\'', ' ', ',' };
private readonly static char[] StringStopCharacters = new char[] { '\'' };
private readonly static char[] PropertyStopCharacters = new char[] { '(', ']', '[', ',', ')', ' ', '\'' };
internal ExpressionStream(string expression)
{
_Source = expression;
_Length = _Source.Length;
_Position = 0;
_Line = 0;
_Column = 0;
_EscapeLength = 0;
if (_Length < 0 || _Length > MaxLength)
throw new ArgumentOutOfRangeException(nameof(expression));
UpdateCurrent();
if (_Source.Length > 0)
_Line = 1;
}
#region Properties
public bool EOF
{
get { return _Position >= _Length; }
}
public bool IsStartOfLine
{
get { return _Column == 0; }
}
/// <summary>
/// The character at the current position in the stream.
/// </summary>
public char Current
{
get { return _Current; }
}
public char Previous
{
get { return _Previous; }
}
public int Line
{
get { return _Line; }
}
public int Column
{
get { return _Column; }
}
#if DEBUG
/// <summary>
/// Used for interactive debugging of current position and next characters in the stream.
/// </summary>
public string Preview
{
get { return _Source.Substring(_Position); }
}
#endif
public int Position
{
get { return _Position; }
}
private int Remaining
{
get { return _Length - Position; }
}
public bool IsEscaped
{
get { return _EscapeLength > 0; }
}
#endregion Properties
public bool Start()
{
return Skip(BracketOpen);
}
public bool End()
{
return Skip(BracketClose);
}
public bool TryElement(out string element)
{
SkipWhitespace();
element = CaptureUntil(FunctionNameStopCharacter);
return true;
}
public bool IsGroupStart()
{
return Skip(ParenthesesOpen);
}
public bool IsGroupEnd()
{
return Skip(ParenthesesClose);
}
public bool IsString()
{
return Skip(Apostrophe);
}
public bool CaptureString(out string s)
{
s = null;
if (!IsString())
return false;
s = CaptureUntil(StringStopCharacters);
IsString();
return true;
}
public void Separator()
{
SkipWhitespace();
Skip(Comma);
SkipWhitespace();
}
public bool IsIndexStart()
{
return Skip(BracketOpen);
}
public bool IsIndexEnd()
{
return Skip(BracketClose);
}
public bool IsProperty()
{
return Skip(Period);
}
public bool CaptureProperty(out string propertyName)
{
propertyName = null;
if (!IsProperty())
return false;
propertyName = CaptureUntil(PropertyStopCharacters);
return true;
}
/// <summary>
/// Skip if the current character is whitespace.
/// </summary>
public void SkipWhitespace()
{
Skip(Whitespace, max: 0);
}
public bool Skip(char c)
{
if (_Current != c)
return false;
Next();
return true;
}
/// <summary>
/// Skip ahead if the current character is expected. Keep skipping when the character is repeated.
/// </summary>
/// <param name="c">The character to skip.</param>
/// <returns>The number of characters that where skipped.</returns>
public int Skip(char c, int max)
{
var skipped = 0;
while (Current == c && (max == 0 || skipped < max))
{
Next();
skipped++;
}
return skipped;
}
/// <summary>
/// Move to the next character in the stream.
/// </summary>
/// <returns>Is True when more characters exist in the stream.</returns>
public bool Next(bool ignoreEscaping = false)
{
_Position += _EscapeLength > 0 ? _EscapeLength + 1 : 1;
if (_Position >= _Length)
{
_Current = char.MinValue;
return false;
}
UpdateCurrent(ignoreEscaping);
return true;
}
private void UpdateCurrent(bool ignoreEscaping = false)
{
// Handle escape sequences
_EscapeLength = ignoreEscaping ? 0 : GetEscapeCount(_Position);
_Previous = _Current;
_Current = _Source[_Position + _EscapeLength];
}
private int GetEscapeCount(int position)
{
// Check for escape sequences
if (position < _Length && _Source[position] == Backslash)
{
var next = _Source[position + 1];
// Check against list of escapable characters
if (next == Backslash || next == BracketOpen || next == ParenthesesOpen || next == BracketClose || next == ParenthesesClose)
return 1;
}
return 0;
}
public string CaptureUntil(char[] c, bool ignoreEscaping = false)
{
var start = Position;
var length = 0;
while (!EOF)
{
if (!IsEscaped && c.Contains(Current))
break;
length++;
Next(ignoreEscaping);
}
return Substring(start, length, ignoreEscaping);
}
private string Substring(int start, int length, bool ignoreEscaping = false)
{
if (ignoreEscaping)
return _Source.Substring(start, length);
var position = start;
var i = 0;
var buffer = new char[length];
while (i < length)
{
var offset = GetEscapeCount(position);
buffer[i] = _Source[position + offset];
position += offset + 1;
i++;
}
return new string(buffer);
}
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,593 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Management.Automation;
using static PSRule.Rules.Azure.Data.Template.DeploymentTemplate;
namespace PSRule.Rules.Azure.Data.Template
{
public delegate T StringExpression<T>();
/// <summary>
/// The base class for a template visitor.
/// </summary>
internal abstract class TemplateVisitor
{
private readonly Subscription _Subscription;
private readonly ResourceGroup _ResourceGroup;
private TemplateContext _Context;
internal TemplateVisitor(Subscription subscription, ResourceGroup resourceGroup)
{
_Subscription = subscription;
_ResourceGroup = resourceGroup;
}
public sealed class TemplateContext
{
internal TemplateContext()
{
Resources = new List<JObject>();
Parameters = new Dictionary<string, object>();
Variables = new Dictionary<string, object>();
CopyIndex = new CopyIndexStore();
ResourceGroup = new ResourceGroup();
Subscription = new Subscription();
}
internal TemplateContext(Subscription subscription, ResourceGroup resourceGroup)
: this()
{
if (subscription != null)
Subscription = subscription;
if (resourceGroup != null)
ResourceGroup = resourceGroup;
}
public List<JObject> Resources { get; }
public Dictionary<string, object> Parameters { get; }
public Dictionary<string, object> Variables { get; }
public CopyIndexStore CopyIndex { get; }
public ResourceGroup ResourceGroup { get; internal set; }
public Subscription Subscription { get; internal set; }
internal void Load(DeploymentParameters parameters)
{
foreach (var parameter in parameters.Parameters)
{
if (parameter.Value.Reference != null)
{
Parameters.Add(parameter.Key, SecretPlaceholder(parameter.Value.Reference.SecretName));
}
else if (parameter.Value.Value != null)
{
Parameters.Add(parameter.Key, parameter.Value.Value);
}
}
}
private string SecretPlaceholder(string secretName)
{
return string.Concat("{{SecretReference:", secretName, "}}");
}
public sealed class CopyIndexState
{
internal CopyIndexState()
{
Index = -1;
Count = 1;
Input = null;
Name = null;
}
internal int Index { get; set; }
internal int Count { get; set; }
internal string Name { get; set; }
internal JToken Input { get; set; }
internal bool IsCopy()
{
return Name != null;
}
internal bool Next()
{
Index++;
return Index < Count;
}
}
public sealed class CopyIndexStore
{
private readonly Dictionary<string, CopyIndexState> _Index;
private readonly Stack<CopyIndexState> _Current;
public CopyIndexStore()
{
_Index = new Dictionary<string, CopyIndexState>();
_Current = new Stack<CopyIndexState>();
}
public CopyIndexState Current
{
get { return _Current.Count > 0 ? _Current.Peek() : null; }
}
public void Push(CopyIndexState state)
{
_Current.Push(state);
_Index[state.Name] = state;
}
public void Pop()
{
var state = _Current.Pop();
_Index.Remove(state.Name);
}
public bool TryGetValue(string name, out CopyIndexState state)
{
if (name == null)
{
state = Current;
return state != null;
}
return _Index.TryGetValue(name, out state);
}
}
}
public PSObject[] Visit(DeploymentTemplate template, DeploymentParameters parameters)
{
// Load context
_Context = new TemplateContext(_Subscription, _ResourceGroup);
_Context.Load(parameters);
// Process template sections
Schema(_Context, template.Schema);
ContentVersion(_Context, template.ContentVersion);
Parameters(_Context, template.Parameters);
Variables(_Context, template.Variables);
Resources(_Context, template.Resources);
// Return results
var results = new List<PSObject>();
var serializer = new JsonSerializer();
serializer.Converters.Add(new PSObjectJsonConverter());
foreach (var resource in _Context.Resources)
{
results.Add(resource.ToObject<PSObject>(serializer));
}
return results.ToArray();
}
protected virtual void Schema(TemplateContext context, string schema)
{
}
protected virtual void ContentVersion(TemplateContext context, string contentVersion)
{
}
protected virtual void Parameters(TemplateContext context, Dictionary<string, TemplateParameter> parameters)
{
if (parameters == null || parameters.Count == 0)
return;
foreach (var parameter in parameters)
{
Parameter(context, parameter.Key, parameter.Value);
}
}
protected virtual void Parameter(TemplateContext context, string parameterName, TemplateParameter parameter)
{
if (parameter.DefaultValue == null)
return;
if (!context.Parameters.ContainsKey(parameterName))
context.Parameters.Add(parameterName, ExpandProperty<object>(context, parameter.DefaultValue));
}
protected virtual void Functions(TemplateContext context)
{
}
protected virtual void Function(TemplateContext context)
{
}
protected virtual void Resources(TemplateContext context, IEnumerable<JObject> resources)
{
foreach (var resource in resources)
{
Resource(context, resource);
}
}
protected virtual void Resource(TemplateContext context, JObject resource)
{
var condition = !resource.ContainsKey("condition") || ExpandProperty<bool>(context, resource, "condition");
if (!condition)
return;
var copyIndex = GetResourceIterator(context, resource);
while (copyIndex.Next())
{
var instance = copyIndex.Input.DeepClone() as JObject;
ResourceInstance(context, instance);
}
if (copyIndex.IsCopy())
context.CopyIndex.Pop();
}
protected virtual void ResourceInstance(TemplateContext context, JObject resource)
{
// Expand resource properties
foreach (var property in resource.Properties())
ResolveProperty(context, resource, property.Name);
Emit(context, resource);
}
#region Variables
protected virtual void Variables(TemplateContext context, Dictionary<string, JToken> variables)
{
if (variables == null || variables.Count == 0)
return;
foreach (var variable in variables)
{
Variable(context, variable.Key, variable.Value);
}
}
protected virtual void Variable(TemplateContext context, string variableName, JToken value)
{
if (value.Type == JTokenType.Object)
{
var jobj = value.Value<JObject>();
if (TryArrayProperty(jobj, "copy", out JArray copyArray))
{
var arrayValue = new List<JToken>();
foreach (var copy in copyArray)
{
var copyObject = copy.Value<JObject>();
var copyIndex = new TemplateContext.CopyIndexState();
copyIndex.Count = ExpandProperty<int>(context, copyObject, "count");
copyIndex.Name = ExpandProperty<string>(context, copyObject, "name");
copyIndex.Input = copyObject["input"];
context.CopyIndex.Push(copyIndex);
for (var i = 0; i < copyIndex.Count; i++)
{
copyIndex.Index = i;
var instance = copyIndex.Input.DeepClone();
arrayValue.Add(VariableInstance(context, instance));
}
context.CopyIndex.Pop();
jobj.Remove("copy");
jobj.Add(copyIndex.Name, new JArray(arrayValue.ToArray()));
}
}
context.Variables.Add(variableName, jobj);
return;
}
context.Variables.Add(variableName, VariableInstance(context, value));
}
private bool TryArrayProperty(JObject obj, string propertyName, out JArray propertyValue)
{
propertyValue = null;
if (!obj.TryGetValue(propertyName, out JToken value) || value.Type != JTokenType.Array)
return false;
propertyValue = value.Value<JArray>();
return true;
}
protected virtual JToken VariableInstance(TemplateContext context, JToken value)
{
if (value.Type == JTokenType.Object)
return VariableObject(context, value.Value<JObject>());
else if (value.Type == JTokenType.Array)
{
return VariableArray(context, value.Value<JArray>());
}
else
return VariableSimple(context, value.Value<JValue>());
}
protected virtual JToken VariableObject(TemplateContext context, JObject value)
{
ExpandObjectInstance2(context, value);
return value;
}
protected virtual JToken VariableArray(TemplateContext context, JArray value)
{
return ExpandArray(context, value);
}
protected virtual JToken VariableSimple(TemplateContext context, JValue value)
{
var result = ExpandProperty<object>(context, value.Value<string>());
return result == null ? JValue.CreateNull() : JToken.FromObject(result);
}
#endregion Variables
protected virtual void Outputs()
{
}
protected StringExpression<T> Expression<T>(TemplateContext context, string s)
{
var builder = new ExpressionBuilder();
return () => EvaluateExpression<T>(builder.Build(s));
}
private T EvaluateExpression<T>(ExpressionFnOuter fn)
{
return (T)fn(_Context);
}
private T ExpandProperty<T>(TemplateContext context, object value)
{
return value is string s && IsExpressionString(s) ? EvaluteExpression<T>(context, s) : (T)value;
}
private T ExpandProperty<T>(TemplateContext context, JToken value)
{
if (value.Type != JTokenType.String)
return value.Value<T>();
var svalue = value.Value<string>();
return IsExpressionString(svalue) ? EvaluteExpression<T>(context, svalue) : value.Value<T>();
}
private JToken ExpandPropertyToken(TemplateContext context, JToken value)
{
if (!TryExpressionString(value, out string svalue))
return value;
return JToken.FromObject(EvaluteExpression<object>(context, svalue));
}
private T ExpandProperty<T>(TemplateContext context, JObject value, string propertyName)
{
if (!value.ContainsKey(propertyName))
return default(T);
var propertyValue = value[propertyName].Value<JValue>().Value;
return (propertyValue is string svalue) ? ExpandProperty<T>(context, propertyValue) : (T)propertyValue;
}
private void ResolvePropertyExpression(TemplateContext context, JObject value, string propertyName)
{
if (!value.ContainsKey(propertyName))
return;
value[propertyName] = ExpandProperty<string>(context, value, propertyName);
}
private void ResolveProperty(TemplateContext context, JObject obj, string propertyName)
{
if (!obj.ContainsKey(propertyName))
return;
var value = obj[propertyName];
if (value is JObject jObject)
{
var copyIndex = GetPropertyIterator(context, jObject);
if (copyIndex.IsCopy())
{
while (copyIndex.Next())
{
var instance = copyIndex.Input.DeepClone();
jObject[copyIndex.Name] = ResolveToken(context, instance);
}
context.CopyIndex.Pop();
}
else
{
obj[propertyName] = ResolveToken(context, jObject);
}
}
else if (value is JArray jArray)
{
obj[propertyName] = ExpandArray(context, jArray);
}
else if (value is JToken jToken && jToken.Type == JTokenType.String)
{
obj[propertyName] = ExpandPropertyToken(context, jToken);
}
}
private JToken ResolveToken(TemplateContext context, JToken token)
{
if (token is JObject jObject)
{
return ExpandObject2(context, jObject);
}
else if (token is JArray jArray)
{
return ExpandArray(context, jArray);
}
else
{
return ExpandPropertyToken(context, token);
}
}
/// <summary>
/// Get a property based iterator copy.
/// </summary>
private TemplateContext.CopyIndexState GetPropertyIterator(TemplateContext context, JObject value)
{
var result = new TemplateContext.CopyIndexState();
result.Input = value;
if (value.ContainsKey("copy"))
{
var copyObject = value["copy"].Value<JObject>();
result.Name = ExpandProperty<string>(context, copyObject, "name");
result.Input = copyObject["input"];
result.Count = ExpandProperty<int>(context, copyObject, "count");
context.CopyIndex.Push(result);
value.Remove("copy");
}
return result;
}
/// <summary>
/// Get a resource based iterator copy.
/// </summary>
private TemplateContext.CopyIndexState GetResourceIterator(TemplateContext context, JObject value)
{
var result = new TemplateContext.CopyIndexState();
result.Input = value;
if (value.ContainsKey("copy"))
{
var copyObject = value["copy"].Value<JObject>();
result.Name = ExpandProperty<string>(context, copyObject, "name");
result.Count = ExpandProperty<int>(context, copyObject, "count");
context.CopyIndex.Push(result);
value.Remove("copy");
}
return result;
}
private JToken ExpandObject2(TemplateContext context, JObject obj)
{
var copyIndex = GetPropertyIterator(context, obj);
if (copyIndex.IsCopy())
{
var array = new JArray();
while (copyIndex.Next())
{
var instance = copyIndex.Input.DeepClone();
array.Add(ResolveToken(context, instance));
}
obj[copyIndex.Name] = array;
context.CopyIndex.Pop();
}
else
{
foreach (var property in obj.Properties())
{
ResolveProperty(context, obj, property.Name);
}
}
return obj;
}
private void ExpandObjectInstance2(TemplateContext context, JObject obj)
{
foreach (var property in obj.Properties())
{
ResolveProperty(context, obj, property.Name);
}
}
private JToken ExpandArray(TemplateContext context, JArray array)
{
var result = new JArray();
for (var i = 0; i < array.Count; i++)
{
if (array[i] is JObject jObject)
{
result.Add(ExpandObject2(context, jObject));
}
else if (array[i] is JArray jArray)
{
result.Add(ExpandArray(context, jArray));
}
else if (array[i] is JToken jToken && jToken.Type == JTokenType.String)
{
result.Add(ExpandPropertyToken(context, jToken));
}
else
{
result.Add(array[i]);
}
}
return result;
}
protected T EvaluteExpression<T>(TemplateContext context, string value)
{
var exp = Expression<T>(context, value);
return exp();
}
protected bool IsExpressionString(string value)
{
return value != null &&
value.Length >= 5 && // [f()]
value[0] == '[' &&
value[value.Length - 1] == ']';
}
protected bool TryExpressionString(JToken token, out string value)
{
value = null;
if (token.Type != JTokenType.String)
return false;
value = token.Value<string>();
return IsExpressionString(value);
}
/// <summary>
/// Emit a resource object.
/// </summary>
protected void Emit(TemplateContext context, JObject resource)
{
context.Resources.Add(resource);
}
}
/// <summary>
/// A template visitor for generating rule data.
/// </summary>
internal sealed class RuleDataExportVisitor : TemplateVisitor
{
internal RuleDataExportVisitor(Subscription subscription, ResourceGroup resourceGroup)
: base (subscription, resourceGroup) { }
protected override void ResourceInstance(TemplateContext context, JObject resource)
{
// Remove resource properties that not required in rule data
if (resource.ContainsKey("apiVersion"))
resource.Remove("apiVersion");
if (resource.ContainsKey("condition"))
resource.Remove("condition");
if (resource.ContainsKey("comments"))
resource.Remove("comments");
if (resource.ContainsKey("dependsOn"))
resource.Remove("dependsOn");
base.ResourceInstance(context, resource);
}
}
}

Просмотреть файл

@ -0,0 +1,189 @@
using System.Collections.Generic;
using System.Diagnostics;
namespace PSRule.Rules.Azure.Data.Template
{
public enum ExpressionTokenType : byte
{
None,
/// <summary>
/// A function name.
/// </summary>
Element,
Property,
/// <summary>
/// A string literal.
/// </summary>
String,
/// <summary>
/// A numeric literal.
/// </summary>
Numeric,
/// <summary>
/// Start a grouping '('.
/// </summary>
GroupStart,
/// <summary>
/// End a grouping ')'.
/// </summary>
GroupEnd,
/// <summary>
/// Start an index '['.
/// </summary>
IndexStart,
/// <summary>
/// End an index ']'.
/// </summary>
IndexEnd
}
/// <summary>
/// An individual expression token.
/// </summary>
[DebuggerDisplay("Type = {Type}, Content = {Content}")]
internal sealed class ExpressionToken
{
internal readonly ExpressionTokenType Type;
internal readonly string Content;
internal ExpressionToken(ExpressionTokenType type, string content)
{
Type = type;
Content = content;
}
}
/// <summary>
/// Add an expression token to a token stream.
/// </summary>
internal static class TokenStreamExtensions
{
internal static void Function(this TokenStream stream, string functionName)
{
stream.Add(new ExpressionToken(ExpressionTokenType.Element, functionName));
}
internal static void String(this TokenStream stream, string s)
{
stream.Add(new ExpressionToken(ExpressionTokenType.String, s));
}
internal static void Property(this TokenStream stream, string propertyName)
{
stream.Add(new ExpressionToken(ExpressionTokenType.Property, propertyName));
}
internal static void GroupStart(this TokenStream stream)
{
stream.Add(new ExpressionToken(ExpressionTokenType.GroupStart, null));
}
internal static void GroupEnd(this TokenStream stream)
{
stream.Add(new ExpressionToken(ExpressionTokenType.GroupEnd, null));
}
internal static void IndexStart(this TokenStream stream)
{
stream.Add(new ExpressionToken(ExpressionTokenType.IndexStart, null));
}
internal static void IndexEnd(this TokenStream stream)
{
stream.Add(new ExpressionToken(ExpressionTokenType.IndexEnd, null));
}
}
/// <summary>
/// A stream of template expression tokens.
/// </summary>
[DebuggerDisplay("Current = (Type = {Current.Type}, Content = {Current.Content})")]
internal sealed class TokenStream
{
private readonly List<ExpressionToken> _Token;
private int _Position;
internal TokenStream()
{
_Token = new List<ExpressionToken>();
}
internal TokenStream(IEnumerable<ExpressionToken> tokens)
: this()
{
foreach (var token in tokens)
Add(token);
}
#region Properties
public ExpressionToken Current
{
get
{
return (_Token.Count <= _Position) ? null : _Token[_Position];
}
}
public int Count
{
get { return _Token.Count; }
}
#endregion Properties
public bool TryTokenType(ExpressionTokenType tokenType, out ExpressionToken token)
{
token = null;
if (Current == null || Current.Type != tokenType)
return false;
token = Pop();
return true;
}
public bool Skip(ExpressionTokenType tokenType)
{
if (Current == null || Current.Type != tokenType)
return false;
Pop();
return true;
}
public void Add(ExpressionToken token)
{
_Token.Add(token);
_Position = _Token.Count - 1;
}
public ExpressionToken Pop()
{
if (Count == 0)
return null;
var token = _Token[_Position];
_Token.RemoveAt(_Position);
return token;
}
public void MoveTo(int position)
{
_Position = position;
}
internal ExpressionToken[] ToArray()
{
return _Token.ToArray();
}
}
}

Просмотреть файл

@ -0,0 +1,213 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Collections;
using System.Collections.Generic;
namespace PSRule.Rules.Azure.Data.Template
{
public sealed class Subscription
{
private const string DEFAULT_ID = "/subscriptions/{{Subscription.SubscriptionId}}";
private const string DEFAULT_SUBSCRIPTIONID = "{{Subscription.SubscriptionId}}";
private const string DEFAULT_TENANTID = "{{Subscription.TenantId}}";
private const string DEFAULT_DISPLAYNAME = "{{Subscription.Name}}";
internal readonly static Subscription Default = new Subscription();
internal Subscription()
{
SubscriptionId = DEFAULT_SUBSCRIPTIONID;
TenantId = DEFAULT_TENANTID;
DisplayName = DEFAULT_DISPLAYNAME;
Id = DEFAULT_ID;
}
internal Subscription(string subscriptionId, string tenantId, string displayName)
{
SubscriptionId = subscriptionId;
TenantId = tenantId;
DisplayName = displayName;
Id = string.Concat("/subscriptions/", SubscriptionId);
}
public readonly string Id;
public readonly string SubscriptionId;
public readonly string TenantId;
public readonly string DisplayName;
}
public sealed class ResourceGroup
{
private const string DEFAULT_ID = "/subscriptions/{{Subscription.SubscriptionId}}/resourceGroups/{{ResourceGroup.Name}}";
private const string DEFAULT_NAME = "{{ResourceGroup.Name}}";
private const string DEFAULT_TYPE = "Microsoft.Resources/resourceGroups";
private const string DEFAULT_LOCATION = "{{ResourceGroup.Location}}";
private const string DEFAULT_MANAGEDBY = "{{ResourceGroup.ManagedBy}}";
private const Hashtable DEFAULT_TAGS = null;
private const string DEFAULT_PROVISIONINGSTATE = "Succeeded";
internal readonly static ResourceGroup Default = new ResourceGroup();
internal ResourceGroup()
{
Id = DEFAULT_ID;
Name = DEFAULT_NAME;
Type = DEFAULT_TYPE;
Location = DEFAULT_LOCATION;
ManagedBy = DEFAULT_MANAGEDBY;
Tags = DEFAULT_TAGS;
Properties = new ResourceGroupProperties(DEFAULT_PROVISIONINGSTATE);
}
internal ResourceGroup(string id, string name, string location, string managedBy, Hashtable tags)
: this()
{
Id = id;
Name = name;
Location = location;
ManagedBy = managedBy;
Tags = tags;
}
public sealed class ResourceGroupProperties
{
public readonly string ProvisioningState;
internal ResourceGroupProperties(string provisioningState)
{
ProvisioningState = provisioningState;
}
}
public readonly string Id;
public readonly string Name;
public readonly string Type;
public readonly string Location;
public readonly string ManagedBy;
public readonly Hashtable Tags;
public readonly ResourceGroupProperties Properties;
}
public sealed class MockResource
{
public MockResource()
{
}
}
public sealed class MockResourceList
{
public MockResourceList()
{
}
}
public sealed class DeploymentTemplate
{
public sealed class TemplateResourceSku
{
}
public sealed class TemplateParameter
{
public string Type { get; set; }
public JToken DefaultValue { get; set; }
}
public sealed class TemplateResource
{
public string Condition { get; set; }
public string Type { get; set; }
public string Name { get; set; }
public string ApiVersion { get; set; }
public string Location { get; set; }
public TemplateResourceSku Sku { get; set; }
public string Kind { get; set; }
public Dictionary<string, object> Properties { get; set; }
public TemplateResourceCopy Copy { get; set; }
}
[JsonProperty("$schema")]
public string Schema { get; set; }
[JsonProperty("contentVersion")]
public string ContentVersion { get; set; }
public Dictionary<string, TemplateParameter> Parameters { get; set; }
[JsonProperty("variables")]
public Dictionary<string, JToken> Variables { get; set; }
//public Dictionary<string, object> Functions { get; set; }
[JsonProperty("resources")]
public List<JObject> Resources { get; set; }
[JsonProperty("outputs")]
public Dictionary<string, object> Outputs { get; set; }
}
public sealed class TemplateResourceCopy
{
public string Name { get; set; }
public int Count { get; set; }
public string Mode { get; set; }
public int BatchSize { get; set; }
}
public sealed class DeploymentParameters
{
[JsonProperty("$schema")]
public string Schema { get; set; }
[JsonProperty("contentVersion")]
public string ContentVersion { get; set; }
[JsonProperty("parameters")]
public Dictionary<string, DeploymentParameterValue> Parameters { get; set; }
public sealed class DeploymentParameterValue
{
[JsonProperty("value")]
public object Value { get; set; }
public DeploymentParameterKeyVaultReference Reference {get;set;}
}
public class DeploymentParameterKeyVaultReference
{
public string SecretName { get; set; }
public ResourceReference KeyVault { get; set; }
}
}
public sealed class ResourceReference
{
public string Id { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.27.2" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="PowerShellStandard.Library" Version="5.1.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\PSRuleResources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>PSRuleResources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\PSRuleResources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>PSRuleResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

Просмотреть файл

@ -56,7 +56,9 @@ RequiredModules = @(
)
# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()
RequiredAssemblies = @(
'PSRule.Rules.Azure.dll'
)
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
@ -73,6 +75,7 @@ RequiredModules = @(
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @(
'Export-AzRuleData'
'Export-AzTemplateRuleData'
)
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.

Просмотреть файл

@ -4,6 +4,8 @@
Set-StrictMode -Version latest;
[PSRule.Rules.Azure.Configuration.PSRuleOption]::UseExecutionContext($ExecutionContext);
#
# Localization
#
@ -45,7 +47,6 @@ function Export-AzRuleData {
[Parameter(Mandatory = $False, ParameterSetName = 'All')]
[Switch]$All = $False
)
process {
# Get subscriptions
$context = FindAzureContext -Subscription $Subscription -Tenant $Tenant -All:$All -Verbose:$VerbosePreference;
@ -80,12 +81,118 @@ function Export-AzRuleData {
}
}
# .ExternalHelp PSRule.Rules.Azure-Help.xml
function Export-AzTemplateRuleData {
[CmdletBinding()]
[OutputType([System.IO.FileInfo])]
[OutputType([PSObject])]
param (
[Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)]
[String]$TemplateFile,
[Parameter(Mandatory = $False, ValueFromPipelineByPropertyName = $True)]
[Alias('TemplateParameterFile')]
[String[]]$ParameterFile,
[Parameter(Mandatory = $False)]
[String]$ResourceGroupName,
[Parameter(Mandatory = $False)]
[String]$Subscription,
[Parameter(Mandatory = $False)]
[String]$OutputPath = $PWD,
[Parameter(Mandatory = $False)]
[Switch]$PassThru
)
begin {
Write-Verbose -Message '[Export-AzTemplateRuleData] BEGIN::';
# Build the pipeline
$builder = [PSRule.Rules.Azure.Pipeline.PipelineBuilder]::Template();
$builder.PassThru($PassThru);
$builder.OutputPath($OutputPath);
# Bind to subscription context
if ($PSBoundParameters.ContainsKey('Subscription')) {
$subscriptionObject = GetSubscription -Subscription $Subscription -ErrorAction SilentlyContinue;
if ($Null -ne $subscriptionObject) {
$builder.Subscription($subscriptionObject);
}
}
# Bind to resource group
if ($PSBoundParameters.ContainsKey('ResourceGroupName')) {
$resourceGroupObject = GetResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue;
if ($Null -ne $resourceGroupObject) {
$builder.ResourceGroup($resourceGroupObject);
}
}
$builder.UseCommandRuntime($PSCmdlet.CommandRuntime);
$builder.UseExecutionContext($ExecutionContext);
try {
$pipeline = $builder.Build();
$pipeline.Begin();
}
catch {
$pipeline.Dispose();
}
}
process {
if ($Null -ne (Get-Variable -Name pipeline -ErrorAction SilentlyContinue)) {
try {
$source = [PSRule.Rules.Azure.Pipeline.TemplateSource]::new($TemplateFile, $ParameterFile);
$pipeline.Process($source);
}
catch {
$pipeline.Dispose();
throw;
}
}
}
end {
if ($Null -ne (Get-Variable -Name pipeline -ErrorAction SilentlyContinue)) {
try {
$pipeline.End();
}
finally {
$pipeline.Dispose();
}
}
Write-Verbose -Message '[Export-AzTemplateRuleData] END::';
}
}
#endregion Public functions
#
# Helper functions
#
function GetResourceGroup {
[CmdletBinding()]
param (
[Parameter(Mandatory = $True)]
[String]$Name
)
process {
return Get-AzResourceGroup -Name $Name -ErrorAction SilentlyContinue;
}
}
function GetSubscription {
[CmdletBinding()]
param (
[Parameter(Mandatory = $True)]
[String]$Subscription
)
process {
return (Set-AzContext -Subscription $Subscription -ErrorAction SilentlyContinue).Subscription;
}
}
function FindAzureContext {
[CmdletBinding()]
[OutputType([Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer[]])]
@ -502,4 +609,4 @@ function SetResourceType {
# Export module
#
Export-ModuleMember -Function 'Export-AzRuleData';
Export-ModuleMember -Function 'Export-AzRuleData', 'Export-AzTemplateRuleData';

Просмотреть файл

@ -0,0 +1,83 @@
using System;
using System.Runtime.Serialization;
using System.Security.Permissions;
namespace PSRule.Rules.Azure.Pipeline
{
/// <summary>
/// A base class for all pipeline exceptions.
/// </summary>
public abstract class PipelineException : Exception
{
/// <summary>
/// Creates a pipeline exception.
/// </summary>
protected PipelineException()
{
}
/// <summary>
/// Creates a pipeline exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
protected PipelineException(string message) : base(message)
{
}
/// <summary>
/// Creates a pipeline exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
protected PipelineException(string message, Exception innerException) : base(message, innerException)
{
}
protected PipelineException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
/// <summary>
/// A serialization exception.
/// </summary>
[Serializable]
public sealed class PipelineSerializationException : PipelineException
{
/// <summary>
/// Creates a serialization exception.
/// </summary>
public PipelineSerializationException()
{
}
/// <summary>
/// Creates a serialization exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
public PipelineSerializationException(string message) : base(message)
{
}
/// <summary>
/// Creates a serialization exception.
/// </summary>
/// <param name="message">The detail of the exception.</param>
/// <param name="innerException">A nested exception that caused the issue.</param>
public PipelineSerializationException(string message, Exception innerException) : base(message, innerException)
{
}
private PipelineSerializationException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
if (info == null) throw new ArgumentNullException("info");
base.GetObjectData(info, context);
}
}
}

Просмотреть файл

@ -0,0 +1,159 @@
using PSRule.Rules.Azure.Configuration;
using PSRule.Rules.Azure.Resources;
using System;
using System.IO;
using System.Management.Automation;
using System.Text;
namespace PSRule.Rules.Azure.Pipeline
{
internal delegate bool ShouldProcess(string target, string action);
/// <summary>
/// A helper class for building a pipeline from PowerShell.
/// </summary>
public static class PipelineBuilder
{
public static ITemplatePipelineBuilder Template()
{
return new TemplatePipelineBuilder();
}
}
public interface IPipelineBuilder
{
void UseCommandRuntime(ICommandRuntime2 commandRuntime);
void UseExecutionContext(EngineIntrinsics executionContext);
IPipelineBuilder Configure(PSRuleOption option);
IPipeline Build();
}
public interface IPipeline
{
void Begin();
void Process(PSObject sourceObject);
void End();
}
internal abstract class PipelineBuilderBase : IPipelineBuilder
{
protected readonly PSRuleOption Option;
protected WriteOutput Output;
protected ShouldProcess ShouldProcess;
protected PipelineBuilderBase()
{
Option = new PSRuleOption();
}
public virtual void UseCommandRuntime(ICommandRuntime2 commandRuntime)
{
//Logger.UseCommandRuntime(commandRuntime);
Output = commandRuntime.WriteObject;
ShouldProcess = commandRuntime.ShouldProcess;
}
public void UseExecutionContext(EngineIntrinsics executionContext)
{
//Logger.UseExecutionContext(executionContext);
}
public virtual IPipelineBuilder Configure(PSRuleOption option)
{
return this;
}
public abstract IPipeline Build();
protected PipelineContext PrepareContext()
{
return new PipelineContext(Option);
}
protected virtual PipelineWriter PrepareWriter()
{
return new PowerShellWriter(Output);
}
/// <summary>
/// Write output to file.
/// </summary>
/// <param name="path">The file path to write.</param>
/// <param name="encoding">The file encoding to use.</param>
/// <param name="o">The text to write.</param>
protected static void WriteToFile(string path, ShouldProcess shouldProcess, Encoding encoding, object o)
{
var rootedPath = PSRuleOption.GetRootedPath(path: path);
var parentPath = Directory.GetParent(rootedPath);
if (!parentPath.Exists && shouldProcess(target: parentPath.FullName, action: PSRuleResources.ShouldCreatePath))
{
Directory.CreateDirectory(path: parentPath.FullName);
}
if (shouldProcess(target: rootedPath, action: PSRuleResources.ShouldWriteFile))
{
File.WriteAllText(path: rootedPath, contents: o.ToString(), encoding: encoding);
}
}
}
internal abstract class PipelineBase : IDisposable, IPipeline
{
protected readonly PipelineContext Context;
// Track whether Dispose has been called.
private bool _Disposed = false;
protected PipelineBase(PipelineContext context)
{
Context = context;
}
#region IPipeline
public virtual void Begin()
{
//Reader.Open();
}
public virtual void Process(PSObject sourceObject)
{
// Do nothing
}
public virtual void End()
{
//Writer.End();
}
#endregion IPipeline
#region IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_Disposed)
{
if (disposing)
{
//Context.Dispose();
}
_Disposed = true;
}
}
#endregion IDisposable
}
}

Просмотреть файл

@ -0,0 +1,14 @@
using PSRule.Rules.Azure.Configuration;
namespace PSRule.Rules.Azure.Pipeline
{
internal sealed class PipelineContext
{
internal readonly PSRuleOption Option;
public PipelineContext(PSRuleOption option)
{
Option = option;
}
}
}

Просмотреть файл

@ -0,0 +1,75 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace PSRule.Rules.Azure.Pipeline
{
internal delegate void WriteOutput(object o, bool enumerate);
internal abstract class PipelineWriter
{
private readonly WriteOutput _Output;
protected PipelineWriter(WriteOutput output)
{
_Output = output;
}
public virtual void Write(object o, bool enumerate)
{
_Output(o, enumerate);
}
public virtual void End()
{
// Do nothing
}
}
internal sealed class PowerShellWriter : PipelineWriter
{
internal PowerShellWriter(WriteOutput output)
: base(output) { }
public override void Write(object o, bool enumerate)
{
base.Write(o, enumerate);
}
}
internal sealed class JsonPipelineWriter : PipelineWriter
{
private readonly List<object> _Result;
internal JsonPipelineWriter(WriteOutput output)
: base(output)
{
_Result = new List<object>();
}
public override void Write(object o, bool enumerate)
{
if (enumerate && o is IEnumerable<object> items)
{
_Result.AddRange(items);
return;
}
_Result.Add(o);
}
public override void End()
{
WriteObjectJson();
}
private void WriteObjectJson()
{
var settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
};
settings.Converters.Add(new PSObjectJsonConverter());
var json = JsonConvert.SerializeObject(_Result.ToArray(), settings: settings);
base.Write(json, false);
}
}
}

Просмотреть файл

@ -0,0 +1,133 @@
using Newtonsoft.Json;
using PSRule.Rules.Azure.Configuration;
using PSRule.Rules.Azure.Data.Template;
using System.Collections;
using System.IO;
using System.Management.Automation;
using System.Text;
namespace PSRule.Rules.Azure.Pipeline
{
public interface ITemplatePipelineBuilder : IPipelineBuilder
{
void ResourceGroup(PSObject resourceGroup);
void Subscription(PSObject subscription);
void PassThru(bool passThru);
void OutputPath(string outputPath);
}
internal sealed class TemplatePipelineBuilder : PipelineBuilderBase, ITemplatePipelineBuilder
{
private Subscription _Subscription;
private ResourceGroup _ResourceGroup;
private bool _PassThru;
private string _OutputPath;
internal TemplatePipelineBuilder()
: base()
{
_Subscription = Data.Template.Subscription.Default;
_ResourceGroup = Data.Template.ResourceGroup.Default;
}
public void ResourceGroup(PSObject resourceGroup)
{
_ResourceGroup = new ResourceGroup(
id: GetProperty<string>(resourceGroup, "ResourceId"),
name: GetProperty<string>(resourceGroup, "ResourceGroupName"),
location: GetProperty<string>(resourceGroup, "Location"),
managedBy: GetProperty<string>(resourceGroup, "ManagedBy"),
tags: GetProperty<Hashtable>(resourceGroup, "Tags")
);
}
public void Subscription(PSObject subscription)
{
_Subscription = new Subscription(
subscriptionId: GetProperty<string>(subscription, "SubscriptionId"),
tenantId: GetProperty<string>(subscription, "TenantId"),
displayName: GetProperty<string>(subscription, "Name")
);
}
public void PassThru(bool passThru)
{
_PassThru = passThru;
}
public void OutputPath(string outputPath)
{
_OutputPath = outputPath;
}
private T GetProperty<T>(PSObject obj, string propertyName)
{
return null == obj.Properties[propertyName] ? default(T) : (T)obj.Properties[propertyName].Value;
}
protected override PipelineWriter PrepareWriter()
{
WriteOutput output = (o, enumerate) => WriteToFile(_OutputPath, ShouldProcess, Encoding.UTF8, o);
return _PassThru ? base.PrepareWriter() : new JsonPipelineWriter(output);
}
public override IPipeline Build()
{
return new TemplatePipeline(PrepareContext(), PrepareWriter(), _ResourceGroup, _Subscription);
}
}
internal sealed class TemplatePipeline : PipelineBase
{
private readonly PipelineWriter _Writer;
private readonly ResourceGroup _ResourceGroup;
private readonly Subscription _Subscription;
internal TemplatePipeline(PipelineContext context, PipelineWriter writer, ResourceGroup resourceGroup, Subscription subscription)
: base(context)
{
_Writer = writer;
_ResourceGroup = resourceGroup;
_Subscription = subscription;
}
public override void Process(PSObject sourceObject)
{
if (sourceObject == null || !(sourceObject.BaseObject is TemplateSource source))
return;
for (var i = 0; i < source.ParametersFile.Length; i++)
{
var output = ProcessTemplate(source.TemplateFile, source.ParametersFile[i]);
_Writer.Write(output, true);
}
}
public override void End()
{
_Writer.End();
}
internal PSObject[] ProcessTemplate(string templateFile, string parametersFile)
{
var templateObject = ReadFile<DeploymentTemplate>(PSRuleOption.GetRootedPath(templateFile));
if (templateObject == null)
throw new FileNotFoundException();
var parametersObject = ReadFile<DeploymentParameters>(PSRuleOption.GetRootedPath(parametersFile));
var visitor = new RuleDataExportVisitor(_Subscription, _ResourceGroup);
return visitor.Visit(templateObject, parametersObject);
}
private static T ReadFile<T>(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
return default(T);
return JsonConvert.DeserializeObject<T>(File.ReadAllText(path));
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
namespace PSRule.Rules.Azure.Pipeline
{
public sealed class TemplateSource
{
public readonly string TemplateFile;
public readonly string[] ParametersFile;
public TemplateSource(string templateFile, string[] parametersFile)
{
TemplateFile = templateFile;
ParametersFile = parametersFile;
}
}
}

Просмотреть файл

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("PSRule.Rules.Azure.Tests")]

126
src/PSRule.Rules.Azure/Resources/PSRuleResources.Designer.cs сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PSRule.Rules.Azure.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class PSRuleResources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal PSRuleResources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSRule.Rules.Azure.Resources.PSRuleResources", typeof(PSRuleResources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to The specified parameter is not a valid integer..
/// </summary>
internal static string FunctionInvalidInteger {
get {
return ResourceManager.GetString("FunctionInvalidInteger", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The specified parameter is not a valid string..
/// </summary>
internal static string FunctionInvalidString {
get {
return ResourceManager.GetString("FunctionInvalidString", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The function &quot;{0}&quot; was not found..
/// </summary>
internal static string FunctionNotFound {
get {
return ResourceManager.GetString("FunctionNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Read JSON failed..
/// </summary>
internal static string ReadJsonFailed {
get {
return ResourceManager.GetString("ReadJsonFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Can not serialize a null PSObject..
/// </summary>
internal static string SerializeNullPSObject {
get {
return ResourceManager.GetString("SerializeNullPSObject", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create path.
/// </summary>
internal static string ShouldCreatePath {
get {
return ResourceManager.GetString("ShouldCreatePath", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Write file.
/// </summary>
internal static string ShouldWriteFile {
get {
return ResourceManager.GetString("ShouldWriteFile", resourceCulture);
}
}
}
}

Просмотреть файл

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="FunctionInvalidInteger" xml:space="preserve">
<value>The specified parameter is not a valid integer.</value>
</data>
<data name="FunctionInvalidString" xml:space="preserve">
<value>The specified parameter is not a valid string.</value>
</data>
<data name="FunctionNotFound" xml:space="preserve">
<value>The function "{0}" was not found.</value>
</data>
<data name="ReadJsonFailed" xml:space="preserve">
<value>Read JSON failed.</value>
</data>
<data name="SerializeNullPSObject" xml:space="preserve">
<value>Can not serialize a null PSObject.</value>
</data>
<data name="ShouldCreatePath" xml:space="preserve">
<value>Create path</value>
</data>
<data name="ShouldWriteFile" xml:space="preserve">
<value>Write file</value>
</data>
</root>

Просмотреть файл

@ -67,7 +67,7 @@ Rule 'Azure.VM.UniqueDns' -Type 'Microsoft.Network/networkInterfaces' -Tag @{ se
}
# Synopsis: Managed disks should be attached to virtual machines
Rule 'Azure.VM.DiskAttached' -If { (ResourceType 'Microsoft.Compute/disks') -and ($TargetObject.ResourceName -notlike '*-ASRReplica') } -Tag @{ severity = 'Awareness'; category = 'Operations management' } {
Rule 'Azure.VM.DiskAttached' -Type 'Microsoft.Compute/disks' -If { ($TargetObject.ResourceName -notlike '*-ASRReplica') } -Tag @{ severity = 'Awareness'; category = 'Operations management' } {
# Disks should be attached unless they are used by ASR, which are not attached until fail over
# Disks for VMs that are off are marked as Reserved
Within 'properties.diskState' 'Attached', 'Reserved'

Просмотреть файл

@ -5,7 +5,7 @@
#region Virtual Network
# Synopsis: Subnets should have NSGs assigned, except for the GatewaySubnet
Rule 'Azure.VirtualNetwork.UseNSGs' -If { ResourceType 'Microsoft.Network/virtualNetworks' } -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
Rule 'Azure.VirtualNetwork.UseNSGs' -Type 'Microsoft.Network/virtualNetworks' -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
Recommend 'Subnets should have NSGs assigned'
# Get subnets
@ -22,7 +22,7 @@ Rule 'Azure.VirtualNetwork.UseNSGs' -If { ResourceType 'Microsoft.Network/virtua
# TODO: Check that NSG on GatewaySubnet is not defined
# Synopsis: VNETs should have at least two DNS servers assigned
Rule 'Azure.VirtualNetwork.SingleDNS' -If { ResourceType 'Microsoft.Network/virtualNetworks' } -Tag @{ severity = 'Single point of failure'; category = 'Reliability' } {
Rule 'Azure.VirtualNetwork.SingleDNS' -Type 'Microsoft.Network/virtualNetworks' -Tag @{ severity = 'Single point of failure'; category = 'Reliability' } {
# If DNS servers are customized, at least two IP addresses should be defined
if ($Assert.NullOrEmpty($TargetObject, 'properties.dhcpOptions.dnsServers').Result) {
$True;
@ -33,7 +33,7 @@ Rule 'Azure.VirtualNetwork.SingleDNS' -If { ResourceType 'Microsoft.Network/virt
}
# Synopsis: VNETs should use Azure local DNS servers
Rule 'Azure.VirtualNetwork.LocalDNS' -If { ResourceType 'Microsoft.Network/virtualNetworks' } {
Rule 'Azure.VirtualNetwork.LocalDNS' -Type 'Microsoft.Network/virtualNetworks' {
# If DNS servers are customized, check what range the IPs are in
if ($Assert.NullOrEmpty($TargetObject, 'properties.dhcpOptions.dnsServers').Result) {
$True;
@ -66,7 +66,7 @@ Rule 'Azure.VirtualNetwork.PeerState' -If { (HasPeerNetwork) } {
#region Network Security Group
# Synopsis: Network security groups should avoid any inbound rules
Rule 'Azure.VirtualNetwork.NSGAnyInboundSource' -If { ResourceType 'Microsoft.Network/networkSecurityGroups' } -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
Rule 'Azure.VirtualNetwork.NSGAnyInboundSource' -Type 'Microsoft.Network/networkSecurityGroups' -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
Recommend 'Avoid rules that apply to all source addresses'
$rules = $TargetObject.properties.securityRules | Where-Object {
@ -79,7 +79,7 @@ Rule 'Azure.VirtualNetwork.NSGAnyInboundSource' -If { ResourceType 'Microsoft.Ne
}
# Synopsis: Avoid blocking all inbound network traffic
Rule 'Azure.VirtualNetwork.NSGDenyAllInbound' -If { ResourceType 'Microsoft.Network/networkSecurityGroups' } {
Rule 'Azure.VirtualNetwork.NSGDenyAllInbound' -Type 'Microsoft.Network/networkSecurityGroups' {
$denyRules = @(GetOrderedNSGRules | Where-Object {
$_.properties.direction -eq 'Inbound' -and
$_.properties.access -eq 'Deny' -and
@ -93,7 +93,7 @@ Rule 'Azure.VirtualNetwork.NSGDenyAllInbound' -If { ResourceType 'Microsoft.Netw
}
# Synopsis: Lateral traversal from application servers should be blocked
Rule 'Azure.VirtualNetwork.LateralTraversal' -If { ResourceType 'Microsoft.Network/networkSecurityGroups' } {
Rule 'Azure.VirtualNetwork.LateralTraversal' -Type 'Microsoft.Network/networkSecurityGroups' {
$rules = @($TargetObject.properties.securityRules | Where-Object {
$_.properties.direction -eq 'Outbound' -and
$_.properties.access -eq 'Deny' -and
@ -107,7 +107,7 @@ Rule 'Azure.VirtualNetwork.LateralTraversal' -If { ResourceType 'Microsoft.Netwo
}
# Synopsis: Network security groups should be associated to either a subnet or network interface
Rule 'Azure.VirtualNetwork.NSGAssociated' -If { ResourceType 'Microsoft.Network/networkSecurityGroups' } {
Rule 'Azure.VirtualNetwork.NSGAssociated' -Type 'Microsoft.Network/networkSecurityGroups' {
$subnets = ($TargetObject.Properties.subnets | Measure-Object).Count;
$interfaces = ($TargetObject.Properties.networkInterfaces | Measure-Object).Count;
@ -120,7 +120,7 @@ Rule 'Azure.VirtualNetwork.NSGAssociated' -If { ResourceType 'Microsoft.Network/
#region Application Gateway
# Synopsis: Application Gateway should use a minimum of two instances
Rule 'Azure.VirtualNetwork.AppGwMinInstance' -If { ResourceType 'Microsoft.Network/applicationGateways' } -Tag @{ severity = 'Important'; category = 'Reliability' } {
Rule 'Azure.VirtualNetwork.AppGwMinInstance' -Type 'Microsoft.Network/applicationGateways' -Tag @{ severity = 'Important'; category = 'Reliability' } {
AnyOf {
# Applies to v1 and v2 without autoscale
$TargetObject.Properties.sku.capacity -ge 2
@ -131,7 +131,7 @@ Rule 'Azure.VirtualNetwork.AppGwMinInstance' -If { ResourceType 'Microsoft.Netwo
}
# Synopsis: Application Gateway should use a minimum of Medium
Rule 'Azure.VirtualNetwork.AppGwMinSku' -If { ResourceType 'Microsoft.Network/applicationGateways' } -Tag @{ severity = 'Important'; category = 'Performance' } {
Rule 'Azure.VirtualNetwork.AppGwMinSku' -Type 'Microsoft.Network/applicationGateways' -Tag @{ severity = 'Important'; category = 'Performance' } {
Within 'Properties.sku.name' 'WAF_Medium', 'Standard_Medium', 'WAF_Large', 'Standard_Large', 'WAF_v2', 'Standard_v2'
}
@ -141,7 +141,7 @@ Rule 'Azure.VirtualNetwork.AppGwUseWAF' -If { (IsAppGwPublic) } -Tag @{ severity
}
# Synopsis: Application Gateway should only accept a minimum of TLS 1.2
Rule 'Azure.VirtualNetwork.AppGwSSLPolicy' -If { ResourceType 'Microsoft.Network/applicationGateways' } -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
Rule 'Azure.VirtualNetwork.AppGwSSLPolicy' -Type 'Microsoft.Network/applicationGateways' -Tag @{ severity = 'Critical'; category = 'Security configuration' } {
Exists 'Properties.sslPolicy'
AnyOf {
Within 'Properties.sslPolicy.policyName' 'AppGwSslPolicy20170401S'
@ -176,7 +176,7 @@ Rule 'Azure.VirtualNetwork.AppGwWAFRules' -If { (IsAppGwWAF) } {
#region Network Interface
# Synopsis: Network interfaces should be attached
Rule 'Azure.VirtualNetwork.NICAttached' -If { ResourceType 'Microsoft.Network/networkInterfaces' } {
Rule 'Azure.VirtualNetwork.NICAttached' -Type 'Microsoft.Network/networkInterfaces' {
Exists 'Properties.virtualMachine.id'
}
@ -185,7 +185,7 @@ Rule 'Azure.VirtualNetwork.NICAttached' -If { ResourceType 'Microsoft.Network/ne
#region Load Balancer
# Synopsis: Use specific network probe
Rule 'Azure.VirtualNetwork.LBProbe' -If { ResourceType 'Microsoft.Network/loadBalancers' } {
Rule 'Azure.VirtualNetwork.LBProbe' -Type 'Microsoft.Network/loadBalancers' {
$probes = $TargetObject.Properties.probes;
foreach ($probe in $probes) {
if ($probe.properties.port -in 80, 443, 8080) {

Просмотреть файл

@ -8,3 +8,4 @@ spec:
binding:
targetType:
- ResourceType
- type

Просмотреть файл

@ -21,6 +21,7 @@ Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSRule.Rules.Azu
$outputPath = Join-Path -Path $rootPath -ChildPath out/tests/PSRule.Rules.Azure.Tests/Cmdlet;
Remove-Item -Path $outputPath -Force -Recurse -Confirm:$False -ErrorAction Ignore;
$Null = New-Item -Path $outputPath -ItemType Directory -Force;
$here = (Resolve-Path $PSScriptRoot).Path;
#region Mocks
@ -71,7 +72,7 @@ function MockContext {
#region Export-AzRuleData
Describe 'Export-AzRuleData' -Tag 'Cmdlet' {
Describe 'Export-AzRuleData' -Tag 'Cmdlet','Export-AzRuleData' {
Context 'With defaults' {
Mock -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith ${function:MockContext};
Mock -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith {
@ -174,3 +175,92 @@ Describe 'Export-AzRuleData' -Tag 'Cmdlet' {
}
#endregion Export-AzRuleData
#region Export-AzTemplateRuleData
Describe 'Export-AzTemplateRuleData' -Tag 'Cmdlet','Export-AzTemplateRuleData' {
$templatePath = Join-Path -Path $here -ChildPath 'Resources.Template.json';
$parametersPath = Join-Path -Path $here -ChildPath 'Resources.Parameters.json';
Context 'With defaults' {
It 'Exports template' {
$outputFile = Join-Path -Path $outputPath -ChildPath 'template-with-defaults.json'
$exportParams = @{
TemplateFile = $templatePath
ParameterFile = $parametersPath
OutputPath = $outputFile
}
$Null = Export-AzTemplateRuleData @exportParams;
$result = Get-Content -Path $outputFile -Raw | ConvertFrom-Json;
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 5;
$result[0].name | Should -Be 'vnet-001';
$result[0].properties.subnets.Length | Should -Be 3;
$result[0].properties.subnets[0].name | Should -Be 'GatewaySubnet';
$result[0].properties.subnets[0].properties.addressPrefix | Should -Be '10.1.0.0/27';
$result[0].properties.subnets[2].name | Should -Be 'subnet2';
$result[0].properties.subnets[2].properties.addressPrefix | Should -Be '10.1.0.64/28';
$result[0].properties.subnets[2].properties.networkSecurityGroup.id | Should -Match '^/subscriptions/[\w\{\}\-\.]{1,}/resourceGroups/[\w\{\}\-\.]{1,}/providers/Microsoft\.Network/networkSecurityGroups/nsg-subnet2$';
$result[0].properties.subnets[2].properties.routeTable.id | Should -Match '^/subscriptions/[\w\{\}\-\.]{1,}/resourceGroups/[\w\{\}\-\.]{1,}/providers/Microsoft\.Network/routeTables/route-subnet2$';
}
}
Context 'With -Subscription' {
Mock -CommandName 'GetSubscription' -ModuleName 'PSRule.Rules.Azure' -MockWith {
return [PSCustomObject]@{
SubscriptionId = '00000000-0000-0000-0000-000000000000'
TenantId = '00000000-0000-0000-0000-000000000000'
Name = 'test-sub'
}
}
It 'Exports template' {
$outputFile = Join-Path -Path $outputPath -ChildPath 'template-with-sub.json'
$exportParams = @{
TemplateFile = $templatePath
ParameterFile = $parametersPath
OutputPath = $outputFile
Subscription = 'test-sub'
}
$Null = Export-AzTemplateRuleData @exportParams;
$result = Get-Content -Path $outputFile -Raw | ConvertFrom-Json;
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 5;
$result[0].properties.subnets.Length | Should -Be 3;
$result[0].properties.subnets[2].properties.networkSecurityGroup.id | Should -Match '^/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/[\w\{\}\-\.]{1,}/providers/Microsoft\.Network/networkSecurityGroups/nsg-subnet2$';
$result[0].properties.subnets[2].properties.routeTable.id | Should -Match '^/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/[\w\{\}\-\.]{1,}/providers/Microsoft\.Network/routeTables/route-subnet2$';
}
}
Context 'With -ResourceGroup' {
Mock -CommandName 'GetResourceGroup' -ModuleName 'PSRule.Rules.Azure' -MockWith {
return [PSCustomObject]@{
ResourceId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg'
ResourceGroupName = 'test-rg'
Location = 'region'
ManagedBy = 'testuser'
Tags = @{
test = 'true'
}
}
}
It 'Exports template' {
$outputFile = Join-Path -Path $outputPath -ChildPath 'template-with-rg.json'
$exportParams = @{
TemplateFile = $templatePath
ParameterFile = $parametersPath
OutputPath = $outputFile
ResourceGroupName = 'test-rg'
}
$Null = Export-AzTemplateRuleData @exportParams;
$result = Get-Content -Path $outputFile -Raw | ConvertFrom-Json;
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 5;
$result[0].properties.subnets.Length | Should -Be 3;
$result[0].properties.subnets[2].properties.networkSecurityGroup.id | Should -Match '^/subscriptions/[\w\{\}\-\.]{1,}/resourceGroups/test-rg/providers/Microsoft\.Network/networkSecurityGroups/nsg-subnet2$';
$result[0].properties.subnets[2].properties.routeTable.id | Should -Match '^/subscriptions/[\w\{\}\-\.]{1,}/resourceGroups/test-rg/providers/Microsoft\.Network/routeTables/route-subnet2$';
}
}
}
#endregion Export-AzTemplateRuleData

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<IsPackable>false</IsPackable>
<RootNamespace>PSRule.Rules.Azure</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="6.2.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\PSRule.Rules.Azure\PSRule.Rules.Azure.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Resources.Parameters.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Resources.Template.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,60 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"vnetName": {
"value": "vnet-001"
},
"addressPrefix": {
"value": [
"10.1.0.0/24"
]
},
"subnets": {
"value": [
{
"name": "subnet1",
"addressPrefix": "10.1.0.32/28",
"securityRules": [
{
"name": "deny-rdp-inbound",
"properties": {
"protocol": "Tcp",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389"
],
"access": "Deny",
"priority": 200,
"direction": "Inbound",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "VirtualNetwork"
}
},
{
"name": "deny-hop-outbound",
"properties": {
"protocol": "*",
"sourcePortRange": "*",
"destinationPortRanges": [
"3389",
"22"
],
"access": "Deny",
"priority": 200,
"direction": "Outbound",
"sourceAddressPrefix": "VirtualNetwork",
"destinationAddressPrefix": "*"
}
}
]
},
{
"name": "subnet2",
"addressPrefix": "10.1.0.64/28",
"securityRules": []
}
]
}
}
}

Просмотреть файл

@ -0,0 +1,108 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"name": "vnet-hub",
"description": "This template creates a hub virtual network."
},
"parameters": {
"vnetName": {
"type": "string",
"metadata": {
"description": "The name of the virtual network."
}
},
"addressPrefix": {
"type": "array",
"minLength": 1,
"metadata": {
"description": ""
}
},
"subnets": {
"type": "array",
"metadata": {
"description": ""
}
}
},
"variables": {
"gatewaySubnet": [
{
"name": "GatewaySubnet",
"properties": {
"addressPrefix": "[concat(split(parameters('addressPrefix')[0], '/')[0], '/27')]"
}
}
],
"definedSubnets": {
"copy": [
{
"name": "subnets",
"count": "[length(parameters('subnets'))]",
"input": {
"name": "[parameters('subnets')[copyIndex('subnets')].name]",
"properties": {
"addressPrefix": "[parameters('subnets')[copyIndex('subnets')].addressPrefix]",
"networkSecurityGroup": {
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', concat('nsg-', parameters('subnets')[copyIndex('subnets')].name))]"
},
"routeTable": {
"id": "[resourceId('Microsoft.Network/routeTables', concat('route-', parameters('subnets')[copyIndex('subnets')].name))]"
}
}
}
}
]
},
"allSubnets": "[union(variables('gatewaySubnet'), variables('definedSubnets').subnets)]"
},
"resources": [
{
"comments": "Hub virtual network",
"type": "Microsoft.Network/virtualNetworks",
"name": "[parameters('vnetName')]",
"apiVersion": "2019-04-01",
"location": "[resourceGroup().location]",
"dependsOn": [
"routeIndex",
"nsgIndex"
],
"properties": {
"addressSpace": {
"addressPrefixes": "[parameters('addressPrefix')]"
},
"subnets": "[variables('allSubnets')]"
}
},
{
"comments": "A subnet Route Table",
"type": "Microsoft.Network/routeTables",
"name": "[concat('route-', parameters('subnets')[copyIndex('routeIndex')].name)]",
"apiVersion": "2019-04-01",
"location": "[resourceGroup().location]",
"copy": {
"name": "routeIndex",
"count": "[length(parameters('subnets'))]"
},
"properties": {
"disableBgpRoutePropagation": false
}
},
{
"comments": "A subnet Network Security Group",
"type": "Microsoft.Network/networkSecurityGroups",
"name": "[concat('nsg-', parameters('subnets')[copyIndex()].name)]",
"apiVersion": "2019-04-01",
"location": "[resourceGroup().location]",
"copy": {
"name": "nsgIndex",
"count": "[length(parameters('subnets'))]"
},
"properties": {
"securityRules": "[parameters('subnets')[copyIndex()].securityRules]"
},
"dependsOn": []
}
]
}

Просмотреть файл

@ -0,0 +1,227 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Data.Template;
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;
using static PSRule.Rules.Azure.Data.Template.TemplateVisitor;
namespace PSRule.Rules.Azure
{
public sealed class TemplateResolverTests
{
[Fact]
public void ResolveTemplateTest()
{
var resources = ProcessTemplate(GetSourcePath("Resources.Template.json"), GetSourcePath("Resources.Parameters.json"));
Assert.Equal(5, resources.Length);
var actual1 = resources[0];
Assert.Equal("vnet-001", actual1["name"]);
Assert.Equal("10.1.0.0/24", actual1["properties"]["addressSpace"]["addressPrefixes"][0]);
Assert.Equal(3, actual1["properties"]["subnets"].Value<JArray>().Count);
Assert.Equal("10.1.0.32/28", actual1["properties"]["subnets"][1]["properties"]["addressPrefix"]);
}
[Fact]
public void ParseExpression1()
{
var expression = "[parameters('vnetName')]";
var actual = ExpressionParser.Parse(expression).ToArray();
Assert.Equal(ExpressionTokenType.Element, actual[0].Type); // parameters
Assert.Equal("parameters", actual[0].Content);
Assert.Equal(ExpressionTokenType.GroupStart, actual[1].Type);
Assert.Equal(ExpressionTokenType.String, actual[2].Type); // 'vnetName'
Assert.Equal("vnetName", actual[2].Content);
Assert.Equal(ExpressionTokenType.GroupEnd, actual[3].Type);
}
[Fact]
public void ParseExpression2()
{
var expression = "[concat('route-', parameters('subnets')[copyIndex('routeIndex')].name)]";
var actual = ExpressionParser.Parse(expression).ToArray();
Assert.Equal(ExpressionTokenType.Element, actual[0].Type); // concat
Assert.Equal("concat", actual[0].Content);
Assert.Equal(ExpressionTokenType.GroupStart, actual[1].Type);
Assert.Equal(ExpressionTokenType.String, actual[2].Type); // 'route-'
Assert.Equal("route-", actual[2].Content);
Assert.Equal(ExpressionTokenType.Element, actual[3].Type); // parameters
Assert.Equal("parameters", actual[3].Content);
Assert.Equal(ExpressionTokenType.GroupStart, actual[4].Type);
Assert.Equal(ExpressionTokenType.String, actual[5].Type); // 'subnets'
Assert.Equal("subnets", actual[5].Content);
Assert.Equal(ExpressionTokenType.GroupEnd, actual[6].Type);
Assert.Equal(ExpressionTokenType.IndexStart, actual[7].Type);
Assert.Equal(ExpressionTokenType.Element, actual[8].Type); // copyIndex
Assert.Equal("copyIndex", actual[8].Content);
Assert.Equal(ExpressionTokenType.GroupStart, actual[9].Type);
Assert.Equal(ExpressionTokenType.String, actual[10].Type); // 'routeIndex'
Assert.Equal("routeIndex", actual[10].Content);
Assert.Equal(ExpressionTokenType.GroupEnd, actual[11].Type);
Assert.Equal(ExpressionTokenType.IndexEnd, actual[12].Type);
Assert.Equal(ExpressionTokenType.Property, actual[13].Type); // .name
Assert.Equal("name", actual[13].Content);
Assert.Equal(ExpressionTokenType.GroupEnd, actual[14].Type);
}
[Fact]
public void ParseExpression3()
{
var expression = "[concat(split(parameters('addressPrefix')[0], '/')[0], '/27')]";
var actual = ExpressionParser.Parse(expression).ToArray();
Assert.Equal(ExpressionTokenType.Element, actual[0].Type); // concat
Assert.Equal("concat", actual[0].Content);
Assert.Equal(ExpressionTokenType.GroupStart, actual[1].Type);
Assert.Equal(ExpressionTokenType.Element, actual[2].Type); // split
Assert.Equal("split", actual[2].Content);
Assert.Equal(ExpressionTokenType.GroupStart, actual[3].Type);
Assert.Equal(ExpressionTokenType.Element, actual[4].Type); // parameters
Assert.Equal("parameters", actual[4].Content);
Assert.Equal(ExpressionTokenType.GroupStart, actual[5].Type);
Assert.Equal(ExpressionTokenType.String, actual[6].Type); // 'addressPrefix'
Assert.Equal("addressPrefix", actual[6].Content);
Assert.Equal(ExpressionTokenType.GroupEnd, actual[7].Type);
Assert.Equal(ExpressionTokenType.IndexStart, actual[8].Type);
Assert.Equal(ExpressionTokenType.Element, actual[9].Type); // 0
Assert.Equal("0", actual[9].Content);
Assert.Equal(ExpressionTokenType.IndexEnd, actual[10].Type);
Assert.Equal(ExpressionTokenType.String, actual[11].Type); // '/'
Assert.Equal("/", actual[11].Content);
Assert.Equal(ExpressionTokenType.GroupEnd, actual[12].Type);
Assert.Equal(ExpressionTokenType.IndexStart, actual[13].Type);
Assert.Equal(ExpressionTokenType.Element, actual[14].Type); // 0
Assert.Equal("0", actual[14].Content);
Assert.Equal(ExpressionTokenType.IndexEnd, actual[15].Type);
Assert.Equal(ExpressionTokenType.String, actual[16].Type); // '/27'
Assert.Equal("/27", actual[16].Content);
Assert.Equal(ExpressionTokenType.GroupEnd, actual[17].Type);
}
[Fact]
public void ParseExpression4()
{
var expression = "[concat('route-', parameters('subnets')[0].route[1])]";
var actual = ExpressionParser.Parse(expression).ToArray();
Assert.Equal(ExpressionTokenType.Element, actual[0].Type); // concat
Assert.Equal("concat", actual[0].Content);
Assert.Equal(ExpressionTokenType.GroupStart, actual[1].Type);
Assert.Equal(ExpressionTokenType.String, actual[2].Type); // 'route-'
Assert.Equal("route-", actual[2].Content);
Assert.Equal(ExpressionTokenType.Element, actual[3].Type); // parameters
Assert.Equal("parameters", actual[3].Content);
Assert.Equal(ExpressionTokenType.GroupStart, actual[4].Type);
Assert.Equal(ExpressionTokenType.String, actual[5].Type); // 'subnets'
Assert.Equal("subnets", actual[5].Content);
Assert.Equal(ExpressionTokenType.GroupEnd, actual[6].Type);
Assert.Equal(ExpressionTokenType.IndexStart, actual[7].Type);
Assert.Equal(ExpressionTokenType.Element, actual[8].Type); // 0
Assert.Equal("0", actual[8].Content);
Assert.Equal(ExpressionTokenType.IndexEnd, actual[9].Type);
Assert.Equal(ExpressionTokenType.Property, actual[10].Type); // .route
Assert.Equal("route", actual[10].Content);
Assert.Equal(ExpressionTokenType.IndexStart, actual[11].Type);
Assert.Equal(ExpressionTokenType.Element, actual[12].Type); // 1
Assert.Equal("1", actual[12].Content);
Assert.Equal(ExpressionTokenType.IndexEnd, actual[13].Type);
Assert.Equal(ExpressionTokenType.GroupEnd, actual[14].Type);
}
[Fact]
public void BuildExpression1()
{
var expression = "[parameters('vnetName')]";
var builder = new ExpressionBuilder();
var context = new TemplateContext();
context.Parameters["vnetName"] = "vnet1";
var fn = builder.Build(expression);
var actual = fn(context);
Assert.Equal("vnet1", actual);
}
[Fact]
public void BuildExpression2()
{
var expression = "[concat('route-', parameters('subnets')[copyIndex('routeIndex')].name)]";
var builder = new ExpressionBuilder();
var context = new TemplateContext();
context.CopyIndex.Push(new TemplateContext.CopyIndexState() { Name = "routeIndex", Index = 0 });
context.Parameters["subnets"] = new TestSubnet[] { new TestSubnet("subnet1", new string[] { "routeA", "routeB" }) };
var fn = builder.Build(expression);
var actual = fn(context);
Assert.Equal("route-subnet1", actual);
}
[Fact]
public void BuildExpression4()
{
var expression = "[concat('route-', parameters('subnets')[0].route[1])]";
var builder = new ExpressionBuilder();
var context = new TemplateContext();
context.Parameters["subnets"] = new TestSubnet[] { new TestSubnet("subnet1", new string[] { "routeA", "routeB" }) };
var fn = builder.Build(expression);
var actual = fn(context);
Assert.Equal("route-routeB", actual);
}
private static string GetSourcePath(string fileName)
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
}
private static JObject[] ProcessTemplate(string templateFile, string parametersFile)
{
var templateObject = ReadFile<DeploymentTemplate>(templateFile);
var parametersObject = ReadFile<DeploymentParameters>(parametersFile);
var visitor = new TestTemplateVisitor();
visitor.Visit(templateObject, parametersObject);
return visitor.TestResources.ToArray();
}
private static T ReadFile<T>(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
return default(T);
return JsonConvert.DeserializeObject<T>(File.ReadAllText(path));
}
}
internal sealed class TestSubnet
{
internal TestSubnet(string n, string[] r)
{
name = n;
route = r;
}
public string name { get; private set; }
public string[] route { get; private set; }
}
internal sealed class TestTemplateVisitor : TemplateVisitor
{
internal TestTemplateVisitor()
: base(null, null)
{
TestResources = new List<JObject>();
}
public List<JObject> TestResources { get; }
protected override void ResourceInstance(TemplateContext context, JObject resource)
{
base.ResourceInstance(context, resource);
TestResources.Add(resource);
}
}
}