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

Fixes and improvements for Export-AzRuleData #1341 (#2001)

* Fixes and improvements for Export-AzRuleDAta #1341

* Add suppression for false postive
This commit is contained in:
Bernie White 2023-01-25 01:28:15 +10:00 коммит произвёл GitHub
Родитель 58f1a66cd5
Коммит 51030d2b71
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
31 изменённых файлов: 2272 добавлений и 1446 удалений

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

@ -4,6 +4,10 @@
{
"placeholder": "$CREDENTIAL_PLACEHOLDER$",
"_justification": "This is dummy credential used as a placeholder for unit testing."
},
{
"placeholder": "ffffffff-ffff-ffff-ffff-ffffffffffff",
"_justification": "This is a dummy Azure id for tenants or subscriptions used in unit testing."
}
]
}

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

@ -26,6 +26,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
What's changed since v1.23.0:
- General improvements:
- Updated `Export-AzRuleData` to improve export performance by @BernieWhite.
[#1341](https://github.com/Azure/PSRule.Rules.Azure/issues/1341)
- Removed `Az.Resources` dependency.
- Added async threading for export concurrency.
- Improved performance by using automatic look up of API versions by using provider cache.
- Engineering:
- Bump PSRule to v2.7.0.
[#1973](https://github.com/Azure/PSRule.Rules.Azure/pull/1973)
@ -35,6 +41,9 @@ What's changed since v1.23.0:
[#1903](https://github.com/Azure/PSRule.Rules.Azure/pull/1903)
- Bump Pester to v5.4.0.
[#1994](https://github.com/Azure/PSRule.Rules.Azure/pull/1994)
- Bug fixes:
- Fixed `Export-AzRuleData` may not export all data if throttled by @BernieWhite.
[#1341](https://github.com/Azure/PSRule.Rules.Azure/issues/1341)
## v1.23.0

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

@ -0,0 +1,16 @@
# Design spec for export of in-flight resource data
To support analysis of in-flight resources, the configuration data must be exported from Azure.
This spec documents this mode of operation.
## Requirements
The requirements for this feature/ mode of operation include:
- Export resources, resource groups, and subscription configuration.
- Export related sub-resource configuration data to support rules.
Additonally some non-function requirements include:
- Gracefully handle Azure management API throttling.
- Limit exported data based on filters.

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

@ -145,6 +145,24 @@ namespace PSRule.Rules.Azure
return o.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out _);
}
/// <summary>
/// Determine if the token is a value.
/// </summary>
internal static bool HasValue(this JToken o)
{
return o.Type == JTokenType.String ||
o.Type == JTokenType.Integer ||
o.Type == JTokenType.Object ||
o.Type == JTokenType.Array ||
o.Type == JTokenType.Boolean ||
o.Type == JTokenType.Bytes ||
o.Type == JTokenType.Date ||
o.Type == JTokenType.Float ||
o.Type == JTokenType.Guid ||
o.Type == JTokenType.TimeSpan ||
o.Type == JTokenType.Uri;
}
internal static bool TryGetProperty<TValue>(this JObject o, string propertyName, out TValue value) where TValue : JToken
{
value = null;
@ -176,6 +194,13 @@ namespace PSRule.Rules.Azure
return true;
}
internal static void ReplaceProperty<TValue>(this JObject o, string propertyName, TValue value) where TValue : JToken
{
var p = o.Property(propertyName, StringComparison.OrdinalIgnoreCase);
if (p != null)
p.Value = value;
}
internal static bool TryRenameProperty(this JProperty property, string oldName, string newName)
{
if (property == null || !property.Name.Equals(oldName, StringComparison.OrdinalIgnoreCase))

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

@ -13,14 +13,21 @@ namespace PSRule.Rules.Azure
private const string SUBSCRIPTIONS = "subscriptions";
private const string RESOURCEGROUPS = "resourceGroups";
private const string PROVIDERS = "providers";
private const char SLASH_C = '/';
/// <summary>
/// Determines if the resource Id matches the specified resource type.
/// </summary>
/// <param name="resourceId">The resource Id to compare.</param>
/// <param name="resourceType">The expected resource type.</param>
/// <returns>Returns <c>true</c> if the resource Id matches the type. Otherwise <c>false</c> is returned.</returns>
internal static bool IsResourceType(string resourceId, string resourceType)
{
if (string.IsNullOrEmpty(resourceId) || string.IsNullOrEmpty(resourceType))
return false;
var idParts = GetResourceIdTypeParts(resourceId);
var typeParts = resourceType.Split('/');
var typeParts = resourceType.Split(SLASH_C);
if (idParts.Length != typeParts.Length)
return false;
@ -32,7 +39,62 @@ namespace PSRule.Rules.Azure
}
/// <summary>
///
/// Get the resource type of the resource.
/// </summary>
/// <param name="resourceId">The resource Id of a resource.</param>
/// <returns>Returns the resource type if the Id is valid or <c>null</c> when an invalid resource Id is specified.</returns>
internal static string GetResourceType(string resourceId)
{
return string.IsNullOrEmpty(resourceId) ? null : string.Join(SLASH, GetResourceIdTypeParts(resourceId));
}
/// <summary>
/// Get the resource provider namespaces and type from a full resource type string.
/// </summary>
internal static bool TryResourceProviderFromType(string type, out string provider, out string resourceType)
{
provider = null;
resourceType = null;
var parts = type.SplitByFirstSubstring("/");
if (parts.Length != 2)
return false;
provider = parts[0];
resourceType = parts[1];
return true;
}
/// <summary>
/// Get the subscription Id from a specified resource Id.
/// </summary>
internal static bool TrySubscriptionId(string resourceId, out string subscriptionId)
{
subscriptionId = null;
if (string.IsNullOrEmpty(resourceId))
return false;
var idParts = resourceId.Split(SLASH_C);
var i = 0;
return ConsumeSubscriptionIdPart(idParts, ref i, out subscriptionId);
}
/// <summary>
/// Get the name of the resource group from the specified resource Id.
/// </summary>
internal static bool TryResourceGroup(string resourceId, out string resourceGroupName)
{
resourceGroupName = null;
if (string.IsNullOrEmpty(resourceId))
return false;
var idParts = resourceId.Split(SLASH_C);
var i = 0;
return ConsumeSubscriptionIdPart(idParts, ref i, out _) &&
ConsumeResourceGroupPart(idParts, ref i, out resourceGroupName);
}
/// <summary>
/// Combines Id fragments to form a resource Id.
/// </summary>
/// <remarks>
/// /subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/{resourceType}/{name}
@ -111,14 +173,14 @@ namespace PSRule.Rules.Azure
internal static bool TryResourceIdComponents(string resourceType, string name, out string[] resourceTypeComponents, out string[] nameComponents)
{
var typeParts = resourceType.Split('/');
var typeParts = resourceType.Split(SLASH_C);
var depth = string.IsNullOrEmpty(typeParts[typeParts.Length - 1]) ? typeParts.Length - 2 : typeParts.Length - 1;
resourceTypeComponents = new string[depth];
resourceTypeComponents[0] = string.Concat(typeParts[0], '/', typeParts[1]);
resourceTypeComponents[0] = string.Concat(typeParts[0], SLASH_C, typeParts[1]);
for (var i = 1; i < depth; i++)
resourceTypeComponents[i] = typeParts[i + 1];
nameComponents = name.Split('/');
nameComponents = name.Split(SLASH_C);
return resourceTypeComponents.Length > 0 && nameComponents.Length > 0;
}
@ -127,14 +189,14 @@ namespace PSRule.Rules.Azure
resourceGroupName = null;
resourceTypeComponents = null;
nameComponents = null;
var idParts = resourceId.Split('/');
var idParts = resourceId.Split(SLASH_C);
var i = 0;
if (!(ConsumeSubscriptionIdPart(idParts, ref i, out subscriptionId) &&
ConsumeResourceGroupPart(idParts, ref i, out resourceGroupName) &&
ConsumeProvidersPart(idParts, ref i, out var provider, out var type, out var name)))
return false;
var resourceType = string.Concat(provider, '/', type);
var resourceType = string.Concat(provider, SLASH_C, type);
if (!ConsumeSubResourceType(idParts, ref i, out var subTypes, out var subNames))
{
resourceTypeComponents = new string[] { resourceType };
@ -154,7 +216,7 @@ namespace PSRule.Rules.Azure
private static string[] GetResourceIdTypeParts(string resourceId)
{
var idParts = resourceId.Split('/');
var idParts = resourceId.Split(SLASH_C);
var i = 0;
if (!(ConsumeSubscriptionIdPart(idParts, ref i, out _) &&
ConsumeResourceGroupPart(idParts, ref i, out _) &&

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

@ -8,6 +8,9 @@ namespace PSRule.Rules.Azure
{
internal static class StringExtensions
{
/// <summary>
/// Convert the first character of the string to lower-case.
/// </summary>
internal static string ToCamelCase(this string str)
{
return !string.IsNullOrEmpty(str)
@ -15,6 +18,12 @@ namespace PSRule.Rules.Azure
: string.Empty;
}
/// <summary>
/// Count the occurances of the specified character in the string.
/// </summary>
/// <param name="str">The original string.</param>
/// <param name="chr">The character to count.</param>
/// <returns>The number of occurances in the string.</returns>
internal static int CountCharacterOccurrences(this string str, char chr)
{
return !string.IsNullOrEmpty(str)
@ -22,6 +31,12 @@ namespace PSRule.Rules.Azure
: 0;
}
/// <summary>
/// Split by the last occurance of the specified substring.
/// </summary>
/// <param name="str">The original string to split.</param>
/// <param name="substring">The substring to split by.</param>
/// <returns>An array of left and right strings based on split.</returns>
internal static string[] SplitByLastSubstring(this string str, string substring)
{
var lastSubstringIndex = str.LastIndexOf(substring, StringComparison.OrdinalIgnoreCase);
@ -30,6 +45,26 @@ namespace PSRule.Rules.Azure
return new string[] { firstPart, secondPart };
}
/// <summary>
/// Split by the first occurance of the specified substring.
/// </summary>
/// <param name="str">The original string to split.</param>
/// <param name="substring">The substring to split by.</param>
/// <returns>An array of left and right strings based on split.</returns>
internal static string[] SplitByFirstSubstring(this string str, string substring)
{
var firstSubstringIndex = str.IndexOf(substring, StringComparison.OrdinalIgnoreCase);
var firstPart = str.Substring(0, firstSubstringIndex);
var secondPart = str.Substring(firstSubstringIndex + substring.Length);
return new string[] { firstPart, secondPart };
}
/// <summary>
/// Split a string by a specified character and return the last segment.
/// </summary>
/// <param name="str">The original string to split.</param>
/// <param name="c">The character to split by.</param>
/// <returns>The last segment of the string.</returns>
internal static string SplitLastSegment(this string str, char c)
{
var lastSubstringIndex = str.LastIndexOf(c);

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

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Threading.Tasks;
namespace PSRule.Rules.Azure
{
internal static class TaskExtensions
{
public static void Dispose(this Task[] tasks)
{
for (var i = 0; tasks != null && i < tasks.Length; i++)
tasks[i].Dispose();
}
}
}

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

@ -9,6 +9,19 @@ using PSRule.Rules.Azure.Resources;
namespace PSRule.Rules.Azure.Data
{
/// <summary>
/// Defines extension methods for Azure resource provider data.
/// </summary>
internal static class ProviderDataExtensions
{
public static bool TryResourceType(this ProviderData data, string resourceType, out ResourceProviderType type)
{
type = null;
return ResourceHelper.TryResourceProviderFromType(resourceType, out var provider, out var typeName) &&
data.TryResourceType(provider, typeName, out type);
}
}
/// <summary>
/// Defines a datastore of Azure resource provider data.
/// </summary>

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

@ -9,10 +9,23 @@ using PSRule.Rules.Azure.Resources;
namespace PSRule.Rules.Azure.Data
{
/// <summary>
/// A base class for a helper that loads resources from the assembly.
/// </summary>
public abstract class ResourceLoader
{
/// <summary>
/// Create an instance of the resource loader.
/// </summary>
internal protected ResourceLoader() { }
/// <summary>
/// Get the content of a resource stream by name.
/// </summary>
/// <param name="name">The name of the resource stream.</param>
/// <returns>The string content of the resource stream.</returns>
/// <exception cref="ArgumentNullException">Returns if the name of the resource stream is null or empty.</exception>
/// <exception cref="ArgumentException">Returned when the specified name does not exist as a resource stream.</exception>
protected static string GetContent(string name)
{
if (string.IsNullOrEmpty(name))

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

@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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

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

@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
namespace PSRule.Rules.Azure.Pipeline
{
/// <summary>
/// An OAuth2 access token.
/// </summary>
public sealed class AccessToken
{
/// <summary>
/// Create an instance of an access token.
/// </summary>
/// <param name="token">The base64 encoded token.</param>
/// <param name="expiry">An offset for when the token expires.</param>
/// <param name="tenantId">A unique identifier for the Azure AD tenent associated to the token.</param>
public AccessToken(string token, DateTimeOffset expiry, string tenantId)
{
Token = token;
Expiry = expiry.DateTime;
TenantId = tenantId;
}
internal AccessToken(string tenantId)
{
Token = null;
Expiry = DateTime.MinValue;
TenantId = tenantId;
}
/// <summary>
/// The base64 encoded token.
/// </summary>
public string Token { get; }
/// <summary>
/// An offset for when the token expires.
/// </summary>
public DateTime Expiry { get; }
/// <summary>
/// A unique identifier for the Azure AD tenent associated to the token.
/// </summary>
public string TenantId { get; }
/// <summary>
/// Determine if the access token should be refreshed.
/// </summary>
internal bool ShouldRefresh()
{
return DateTime.UtcNow.AddSeconds(180) > Expiry;
}
}
}

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

@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Threading;
namespace PSRule.Rules.Azure.Pipeline.Export
{
/// <summary>
/// Define a cache for storing and refreshing tokens.
/// </summary>
internal sealed class AccessTokenCache : IDisposable
{
private readonly GetAccessTokenFn _GetToken;
private readonly CancellationTokenSource _Cancel;
private readonly ConcurrentDictionary<string, AccessToken> _Cache;
private bool _Disposed;
/// <summary>
/// Create an instance of a token cache.
/// </summary>
/// <param name="getToken">A delegate method to get a token for a tenant.</param>
public AccessTokenCache(GetAccessTokenFn getToken)
{
_GetToken = getToken;
_Cache = new ConcurrentDictionary<string, AccessToken>();
_Cancel = new CancellationTokenSource();
}
/// <summary>
/// Check and refresh all tokens as required.
/// </summary>
internal void RefreshAll()
{
var tokens = _Cache.ToArray();
for (var i = 0; i < tokens.Length; i++)
{
if (tokens[i].Value.ShouldRefresh())
{
var token = _GetToken.Invoke(tokens[i].Value.TenantId);
if (token != null)
_Cache.TryUpdate(tokens[i].Key, token, tokens[i].Value);
}
}
}
/// <summary>
/// Refresh a token for the specified tenant.
/// </summary>
/// <param name="tenantId">The tenant Id of the specific tenant to refresh the token for.</param>
internal void RefreshToken(string tenantId)
{
if (!_Cache.TryGetValue(tenantId, out var oldToken))
{
var newToken = _GetToken.Invoke(tenantId);
if (newToken != null)
_Cache.TryAdd(tenantId, newToken);
}
else if (oldToken.ShouldRefresh())
{
var newToken = _GetToken.Invoke(tenantId);
if (newToken != null)
_Cache.TryUpdate(tenantId, newToken, oldToken);
}
}
/// <summary>
/// Get an access token for the specified tenant.
/// The token will be synchronized to the main thread.
/// </summary>
/// <param name="tenantId">The tenant Id of the specific tenant to get a token for.</param>
/// <returns>An access token or <c>null</c>.</returns>
internal string GetToken(string tenantId)
{
while (!_Cancel.IsCancellationRequested)
{
if (!_Cache.TryGetValue(tenantId, out var token))
{
_Cache.TryAdd(tenantId, new AccessToken(tenantId));
}
else if (!token.ShouldRefresh())
{
return token.Token;
}
Thread.Sleep(100);
}
return null;
}
internal void Cancel()
{
_Cancel.Cancel();
}
#region IDisposable
private void Dispose(bool disposing)
{
if (!_Disposed)
{
if (disposing)
{
_Cancel.Cancel();
_Cancel.Dispose();
}
_Disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion IDisposable
}
}

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

@ -0,0 +1,248 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Management.Automation;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace PSRule.Rules.Azure.Pipeline.Export
{
/// <summary>
/// A base context to export data from Azure.
/// </summary>
internal abstract class ExportDataContext : IDisposable, ILogger
{
protected const string RESOURCE_MANAGER_URL = "https://management.azure.com";
private const string HEADERS_MEDIATYPE_JSON = "application/json";
private const string HEADERS_AUTHORIZATION_BEARER = "Bearer";
private const string HEADERS_CORRELATION_ID = "x-ms-correlation-request-id";
private const string PROPERTY_VALUE = "value";
private readonly AccessTokenCache _TokenCache;
private readonly CancellationTokenSource _Cancel;
private readonly int _RetryCount;
private readonly TimeSpan _RetryInterval;
private readonly PipelineContext _Context;
private readonly ConcurrentQueue<Message> _Logger;
private readonly string[] _CorrelationId;
private bool _Disposed;
public ExportDataContext(PipelineContext context, AccessTokenCache tokenCache, int retryCount = 3, int retryInterval = 10)
{
_TokenCache = tokenCache;
_Cancel = new CancellationTokenSource();
_RetryCount = retryCount;
_RetryInterval = TimeSpan.FromSeconds(retryInterval);
_Context = context;
_Logger = new ConcurrentQueue<Message>();
_CorrelationId = new string[] { Guid.NewGuid().ToString() };
}
private readonly struct Message
{
public Message(TraceLevel level, string text)
{
Level = level;
Text = text;
}
public TraceLevel Level { get; }
public string Text { get; }
}
protected void RefreshToken(string tenantId)
{
_TokenCache.RefreshToken(tenantId);
}
protected void RefreshAll()
{
_TokenCache.RefreshAll();
}
protected string GetToken(string tenantId)
{
return _TokenCache.GetToken(tenantId);
}
private HttpClient GetClient(string tenantId)
{
var client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(90)
};
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(HEADERS_AUTHORIZATION_BEARER, GetToken(tenantId));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(HEADERS_MEDIATYPE_JSON));
client.DefaultRequestHeaders.Add(HEADERS_CORRELATION_ID, _CorrelationId);
return client;
}
protected static string GetEndpointUri(string baseEndpoint, string requestUri, string apiVersion)
{
return string.Concat(baseEndpoint, "/", requestUri, "?api-version=", apiVersion);
}
protected async Task<JObject[]> ListAsync(string tenantId, string uri)
{
var results = new List<JObject>();
var json = await GetRequestAsync(tenantId, uri);
while (!string.IsNullOrEmpty(json))
{
var payload = JsonConvert.DeserializeObject<JObject>(json);
if (payload.TryGetProperty(PROPERTY_VALUE, out JArray data))
results.AddRange(data.Values<JObject>());
json = payload.TryGetProperty("nextLink", out var nextLink) &&
!string.IsNullOrEmpty(nextLink) ? await GetRequestAsync(tenantId, nextLink) : null;
}
return results.ToArray();
}
protected async Task<JObject> GetAsync(string tenantId, string uri)
{
var json = await GetRequestAsync(tenantId, uri);
if (string.IsNullOrEmpty(json))
return null;
var result = JsonConvert.DeserializeObject<JObject>(json);
return result;
}
private async Task<string> GetRequestAsync(string tenantId, string uri)
{
var attempt = 0;
using var client = GetClient(tenantId);
try
{
do
{
attempt++;
var response = await client.GetAsync(uri, _Cancel.Token);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
return json;
}
else if (!ShouldRetry(response.StatusCode))
{
this.WarnFailedToGet(uri, response.StatusCode, _CorrelationId[0], await response.Content.ReadAsStringAsync());
return null;
}
else
{
this.WarnFailedToGet(uri, response.StatusCode, _CorrelationId[0], await response.Content.ReadAsStringAsync());
var retry = response.Headers.RetryAfter.Delta.GetValueOrDefault(_RetryInterval);
this.VerboseRetryIn(uri, retry, attempt);
Thread.Sleep(retry);
}
} while (attempt <= _RetryCount);
}
finally
{
// Do nothing
}
return null;
}
private static bool ShouldRetry(HttpStatusCode statusCode)
{
return statusCode == HttpStatusCode.RequestTimeout ||
statusCode == HttpStatusCode.BadGateway ||
statusCode == HttpStatusCode.ServiceUnavailable ||
statusCode == HttpStatusCode.GatewayTimeout ||
(int)statusCode == 429 /* Too many Requests */;
}
protected virtual void Dispose(bool disposing)
{
if (!_Disposed)
{
if (disposing)
{
_Cancel.Cancel();
_Cancel.Dispose();
}
_Disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#region ILogger
void ILogger.WriteVerbose(string message)
{
if (_Context == null)
return;
_Logger.Enqueue(new Message(TraceLevel.Verbose, message));
}
void ILogger.WriteVerbose(string format, params object[] args)
{
if (_Context == null)
return;
_Logger.Enqueue(new Message(TraceLevel.Verbose, string.Format(Thread.CurrentThread.CurrentCulture, format, args)));
}
internal void WriteDiagnostics()
{
if (_Context == null || _Logger.IsEmpty)
return;
while (!_Logger.IsEmpty && _Logger.TryDequeue(out var message))
{
if (message.Level == TraceLevel.Verbose)
_Context.Writer.WriteVerbose(message.Text);
else if (message.Level == TraceLevel.Warning)
_Context.Writer.WriteWarning(message.Text);
}
}
void ILogger.WriteWarning(string message)
{
if (_Context == null)
return;
_Logger.Enqueue(new Message(TraceLevel.Warning, message));
}
void ILogger.WriteWarning(string format, params object[] args)
{
if (_Context == null)
return;
_Logger.Enqueue(new Message(TraceLevel.Warning, string.Format(Thread.CurrentThread.CurrentCulture, format, args)));
}
public void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject)
{
if (_Context == null)
return;
_Logger.Enqueue(new Message(TraceLevel.Warning, exception.Message));
}
#endregion ILogger
}
}

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

@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace PSRule.Rules.Azure.Pipeline.Export
{
internal interface IResourceExportContext : ILogger
{
/// <summary>
/// Get a resource.
/// </summary>
/// <param name="tenantId">The tenant Id for the request.</param>
/// <param name="requestUri">The resource URI.</param>
/// <param name="apiVersion">The apiVersion of the resource provider.</param>
Task<JObject> GetAsync(string tenantId, string requestUri, string apiVersion);
/// <summary>
/// List resources.
/// </summary>
/// <param name="tenantId">The tenant Id for the request.</param>
/// <param name="requestUri">The resource URI.</param>
/// <param name="apiVersion">The apiVersion of the resource provider.</param>
Task<JObject[]> ListAsync(string tenantId, string requestUri, string apiVersion);
}
/// <summary>
/// A context to export an Azure resource.
/// </summary>
internal class ResourceExportContext : ExportDataContext, IResourceExportContext
{
private readonly ConcurrentQueue<JObject> _Resources;
private bool _Disposed;
public ResourceExportContext(PipelineContext context, AccessTokenCache tokenCache)
: base(context, tokenCache) { }
public ResourceExportContext(PipelineContext context, ConcurrentQueue<JObject> resources, AccessTokenCache tokenCache, int retryCount, int retryInterval)
: base(context, tokenCache, retryCount, retryInterval)
{
_Resources = resources;
}
/// <inheritdoc/>
public async Task<JObject> GetAsync(string tenantId, string requestUri, string apiVersion)
{
return await GetAsync(tenantId, GetEndpointUri(RESOURCE_MANAGER_URL, requestUri, apiVersion));
}
/// <inheritdoc/>
public async Task<JObject[]> ListAsync(string tenantId, string requestUri, string apiVersion)
{
return await ListAsync(tenantId, GetEndpointUri(RESOURCE_MANAGER_URL, requestUri, apiVersion));
}
protected override void Dispose(bool disposing)
{
if (!_Disposed)
{
if (disposing)
{
// Do nothing.
}
_Disposed = true;
}
base.Dispose(disposing);
}
internal void Flush()
{
WriteDiagnostics();
}
internal void Wait()
{
while (!_Resources.IsEmpty)
{
WriteDiagnostics();
RefreshAll();
Thread.Sleep(100);
}
}
}
}

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

@ -0,0 +1,518 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Data;
namespace PSRule.Rules.Azure.Pipeline.Export
{
/// <summary>
/// Defines a class that gets and sets additional properties and sub-resources of a resource from an Azure subscription.
/// </summary>
internal sealed class ResourceExportVisitor
{
private const string PROPERTY_ID = "id";
private const string PROPERTY_TYPE = "type";
private const string PROPERTY_PROPERTIES = "properties";
private const string PROPERTY_ZONES = "zones";
private const string PROPERTY_RESOURCES = "resources";
private const string PROPERTY_SUBSCRIPTIONID = "subscriptionId";
private const string PROPERTY_KIND = "kind";
private const string PROPERTY_SHAREDKEY = "sharedKey";
private const string PROPERTY_NETWORKPROFILE = "networkProfile";
private const string PROPERTY_NETWORKINTERFACES = "networkInterfaces";
private const string PROPERTY_NETOWORKPLUGIN = "networkPlugin";
private const string PROPERTY_AGENTPOOLPROFILES = "agentPoolProfiles";
private const string PROPERTY_TENANTID = "tenantId";
private const string TYPE_CONTAINERSERVICE_MANAGEDCLUSTERS = "Microsoft.ContainerService/managedClusters";
private const string TYPE_CONTAINERREGISTRY_REGISTRIES = "Microsoft.ContainerRegistry/registries";
private const string TYPE_CDN_PROFILES_ENDPOINTS = "Microsoft.Cdn/profiles/endpoints";
private const string TYPE_AUTOMATION_ACCOUNTS = "Microsoft.Automation/automationAccounts";
private const string TYPE_APIMANAGEMENT_SERVICE = "Microsoft.ApiManagement/service";
private const string TYPE_SQL_SERVERS = "Microsoft.Sql/servers";
private const string TYPE_SQL_DATABASES = "Microsoft.Sql/servers/databases";
private const string TYPE_POSTGRESQL_SERVERS = "Microsoft.DBforPostgreSQL/servers";
private const string TYPE_MYSQL_SERVERS = "Microsoft.DBforMySQL/servers";
private const string TYPE_STORAGE_ACCOUNTS = "Microsoft.Storage/storageAccounts";
private const string TYPE_WEB_APP = "Microsoft.Web/sites";
private const string TYPE_WEB_APPSLOT = "Microsoft.Web/sites/slots";
private const string TYPE_RECOVERYSERVICES_VAULT = "Microsoft.RecoveryServices/vaults";
private const string TYPE_COMPUTER_VIRTUALMACHINE = "Microsoft.Compute/virtualMachines";
private const string TYPE_KEYVAULT_VAULT = "Microsoft.KeyVault/vaults";
private const string TYPE_NETWORK_FRONTDOOR = "Microsoft.Network/frontDoors";
private const string TYPE_NETWORK_CONNECTION = "Microsoft.Network/connections";
private const string TYPE_SUBSCRIPTION = "Microsoft.Subscription";
private const string TYPE_RESOURCES_RESOURCEGROUP = "Microsoft.Resources/resourceGroups";
private const string TYPE_KUSTO_CLUSTER = "Microsoft.Kusto/Clusters";
private const string TYPE_EVENTHUB_NAMESPACE = "Microsoft.EventHub/namespaces";
private const string TYPE_SERVICEBUS_NAMESPACE = "Microsoft.ServiceBus/namespaces";
private const string PROVIDERTYPE_DIAGNOSTICSETTINGS = "/providers/microsoft.insights/diagnosticSettings";
private const string PROVIDERTYPE_ROLEASSIGNMENTS = "/providers/Microsoft.Authorization/roleAssignments";
private const string PROVIDERTYPE_RESOURCELOCKS = "/providers/Microsoft.Authorization/locks";
private const string PROVIDERTYPE_AUTOPROVISIONING = "/providers/Microsoft.Security/autoProvisioningSettings";
private const string PROVIDERTYPE_SECURITYCONTACTS = "/providers/Microsoft.Security/securityContacts";
private const string PROVIDERTYPE_SECURITYPRICINGS = "/providers/Microsoft.Security/pricings";
private const string PROVIDERTYPE_POLICYASSIGNMENTS = "/providers/Microsoft.Authorization/policyAssignments";
private const string PROVIDERTYPE_CLASSICADMINISTRATORS = "/providers/Microsoft.Authorization/classicAdministrators";
private const string MASKED_VALUE = "*** MASKED ***";
private const string APIVERSION_2021_11_01 = "2021-11-01";
private const string APIVERSION_2014_04_01 = "2014-04-01";
private const string APIVERSION_2017_12_01 = "2017-12-01";
private const string APIVERSION_2021_08_01 = "2021-08-01";
private const string APIVERSION_2022_07_01 = "2022-07-01";
private readonly ProviderData _ProviderData;
public ResourceExportVisitor()
{
_ProviderData = new ProviderData();
}
private sealed class ResourceContext
{
private readonly IResourceExportContext _Context;
public ResourceContext(IResourceExportContext context, string tenantId)
{
_Context = context;
TenantId = tenantId;
}
public string TenantId { get; }
internal async Task<JObject> GetAsync(string resourceId, string apiVersion)
{
return await _Context.GetAsync(TenantId, resourceId, apiVersion);
}
internal async Task<JObject[]> ListAsync(string resourceId, string apiVersion)
{
return await _Context.ListAsync(TenantId, resourceId, apiVersion);
}
}
public async Task VisitAsync(IResourceExportContext context, JObject resource)
{
await ExpandResource(context, resource);
}
private async Task<bool> ExpandResource(IResourceExportContext context, JObject resource)
{
if (resource == null ||
!resource.TryStringProperty(PROPERTY_TYPE, out var resourceType) ||
string.IsNullOrWhiteSpace(resourceType) ||
!resource.TryGetProperty(PROPERTY_ID, out var resourceId) ||
!resource.TryGetProperty(PROPERTY_TENANTID, out var tenantId))
return false;
var resourceContext = new ResourceContext(context, tenantId);
// Set the subscription Id.
SetSubscriptionId(resource, resourceId);
// Expand properties for the resource.
await GetProperties(resourceContext, resource, resourceType, resourceId);
// Expand sub-resources for the resource.
return await VisitResourceGroup(resourceContext, resource, resourceType, resourceId) ||
await VisitAPIManagement(resourceContext, resource, resourceType, resourceId) ||
await VisitAutomationAccount(resourceContext, resource, resourceType, resourceId) ||
await VisitCDNEndpoint(resourceContext, resource, resourceType, resourceId) ||
await VisitContainerRegistry(resourceContext, resource, resourceType, resourceId) ||
await VisitAKSCluster(resourceContext, resource, resourceType, resourceId) ||
await VisitSqlServers(resourceContext, resource, resourceType, resourceId) ||
await VisitSqlDatabase(resourceContext, resource, resourceType, resourceId) ||
await VisitPostgreSqlServer(resourceContext, resource, resourceType, resourceId) ||
await VisitMySqlServer(resourceContext, resource, resourceType, resourceId) ||
await VisitStorageAccount(resourceContext, resource, resourceType, resourceId) ||
await VisitWebApp(resourceContext, resource, resourceType, resourceId) ||
await VisitRecoveryServicesVault(resourceContext, resource, resourceType, resourceId) ||
await VisitVirtualMachine(resourceContext, resource, resourceType, resourceId) ||
await VisitKeyVault(resourceContext, resource, resourceType, resourceId) ||
await VisitFrontDoorClassic(resourceContext, resource, resourceType, resourceId) ||
await VisitSubscription(resourceContext, resource, resourceType, resourceId) ||
await VisitDataExplorerCluster(resourceContext, resource, resourceType, resourceId) ||
await VisitEventHubNamespace(resourceContext, resource, resourceType, resourceId) ||
await VisitServiceBusNamespace(resourceContext, resource, resourceType, resourceId) ||
VisitNetworkConnection(resource, resourceType);
}
/// <summary>
/// Get the <c>properties</c> property for a resource if it hasn't been already expanded.
/// </summary>
private async Task GetProperties(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (string.Equals(resourceType, TYPE_SUBSCRIPTION, StringComparison.OrdinalIgnoreCase) ||
string.Equals(resourceType, TYPE_RESOURCES_RESOURCEGROUP, StringComparison.OrdinalIgnoreCase) ||
resource.ContainsKeyInsensitive(PROPERTY_PROPERTIES) ||
!TryGetLatestAPIVersion(resourceType, out var apiVersion))
return;
var full = await GetResource(context, resourceId, apiVersion);
if (full == null)
return;
if (full.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties))
resource.Add(PROPERTY_PROPERTIES, properties);
}
private static void SetSubscriptionId(JObject resource, string resourceId)
{
if (ResourceHelper.TrySubscriptionId(resourceId, out var subscriptionId))
resource.Add(PROPERTY_SUBSCRIPTIONID, subscriptionId);
}
private bool TryGetLatestAPIVersion(string resourceType, out string apiVersion)
{
apiVersion = null;
if (!_ProviderData.TryResourceType(resourceType, out var data) ||
data.ApiVersions == null ||
data.ApiVersions.Length == 0)
return false;
apiVersion = data.ApiVersions[0];
return true;
}
private static async Task<bool> VisitServiceBusNamespace(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_SERVICEBUS_NAMESPACE, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "queues", "2021-06-01-preview"));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "topics", "2021-06-01-preview"));
return true;
}
private static async Task<bool> VisitEventHubNamespace(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_EVENTHUB_NAMESPACE, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "eventhubs", "2021-11-01"));
return true;
}
private static async Task<bool> VisitDataExplorerCluster(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_KUSTO_CLUSTER, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "databases", "2021-08-27"));
return true;
}
private static async Task<bool> VisitResourceGroup(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_RESOURCES_RESOURCEGROUP, StringComparison.OrdinalIgnoreCase))
return false;
await GetRoleAssignments(context, resource, resourceId);
await GetResourceLocks(context, resource, resourceId);
return true;
}
private static async Task<bool> VisitSubscription(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_SUBSCRIPTION, StringComparison.OrdinalIgnoreCase))
return false;
await GetRoleAssignments(context, resource, resourceId);
AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_CLASSICADMINISTRATORS), "2015-07-01"));
AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_AUTOPROVISIONING), "2017-08-01-preview"));
AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_SECURITYCONTACTS), "2017-08-01-preview"));
AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_SECURITYPRICINGS), "2018-06-01"));
AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_POLICYASSIGNMENTS), "2019-06-01"));
return true;
}
private static bool VisitNetworkConnection(JObject resource, string resourceType)
{
if (!string.Equals(resourceType, TYPE_NETWORK_CONNECTION, StringComparison.OrdinalIgnoreCase))
return false;
if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties))
properties.ReplaceProperty(PROPERTY_SHAREDKEY, JValue.CreateString(MASKED_VALUE));
return true;
}
private static async Task<bool> VisitFrontDoorClassic(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_NETWORK_FRONTDOOR, StringComparison.OrdinalIgnoreCase))
return false;
await GetDiagnosticSettings(context, resource, resourceId);
return true;
}
private static async Task<bool> VisitKeyVault(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_KEYVAULT_VAULT, StringComparison.OrdinalIgnoreCase))
return false;
await GetDiagnosticSettings(context, resource, resourceId);
if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) &&
properties.TryGetProperty(PROPERTY_TENANTID, out var tenantId) &&
string.Equals(tenantId, context.TenantId))
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "keys", APIVERSION_2022_07_01));
return true;
}
private static async Task<bool> VisitVirtualMachine(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_COMPUTER_VIRTUALMACHINE, StringComparison.OrdinalIgnoreCase))
return false;
if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) &&
properties.TryGetProperty(PROPERTY_NETWORKPROFILE, out JObject networkProfile) &&
networkProfile.TryGetProperty(PROPERTY_NETWORKINTERFACES, out JArray networkInterfaces))
{
foreach (var netif in networkInterfaces.Values<JObject>())
{
if (netif.TryGetProperty(PROPERTY_ID, out var id))
AddSubResource(resource, await GetResource(context, id, APIVERSION_2022_07_01));
}
}
return true;
}
private static async Task<bool> VisitRecoveryServicesVault(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_RECOVERYSERVICES_VAULT, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "replicationRecoveryPlans", "2022-09-10"));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "replicationAlertSettings", "2022-09-10"));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "backupstorageconfig/vaultstorageconfig", "2022-09-01-preview"));
return true;
}
private static async Task<bool> VisitWebApp(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_WEB_APP, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(resourceType, TYPE_WEB_APPSLOT, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "config", "2022-03-01"));
return true;
}
private static async Task<bool> VisitStorageAccount(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_STORAGE_ACCOUNTS, StringComparison.OrdinalIgnoreCase))
return false;
if (resource.TryGetProperty(PROPERTY_KIND, out var kind) &&
!string.Equals(kind, "FileStorage", StringComparison.OrdinalIgnoreCase))
{
var blobServices = await GetSubResourcesByType(context, resourceId, "blobServices", "2022-05-01");
AddSubResource(resource, blobServices);
foreach (var blobService in blobServices)
{
AddSubResource(resource, await GetSubResourcesByType(context, blobService[PROPERTY_ID].Value<string>(), "containers", "2022-05-01"));
}
}
return true;
}
private static async Task<bool> VisitMySqlServer(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_MYSQL_SERVERS, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "firewallRules", APIVERSION_2017_12_01));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "securityAlertPolicies", APIVERSION_2017_12_01));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "configurations", APIVERSION_2017_12_01));
return true;
}
private static async Task<bool> VisitPostgreSqlServer(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_POSTGRESQL_SERVERS, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "firewallRules", APIVERSION_2017_12_01));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "securityAlertPolicies", APIVERSION_2017_12_01));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "configurations", APIVERSION_2017_12_01));
return true;
}
private static async Task<bool> VisitSqlDatabase(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_SQL_DATABASES, StringComparison.OrdinalIgnoreCase))
return false;
var lowerId = resourceId.ToLower();
AddSubResource(resource, await GetSubResourcesByType(context, lowerId, "dataMaskingPolicies", APIVERSION_2014_04_01));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "transparentDataEncryption", APIVERSION_2021_11_01));
AddSubResource(resource, await GetSubResourcesByType(context, lowerId, "connectionPolicies", APIVERSION_2014_04_01));
AddSubResource(resource, await GetSubResourcesByType(context, lowerId, "geoBackupPolicies", APIVERSION_2014_04_01));
return true;
}
private static async Task<bool> VisitSqlServers(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_SQL_SERVERS, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "firewallRules", APIVERSION_2021_11_01));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "administrators", APIVERSION_2021_11_01));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "securityAlertPolicies", APIVERSION_2021_11_01));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "vulnerabilityAssessments", APIVERSION_2021_11_01));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "auditingSettings", APIVERSION_2021_11_01));
return true;
}
private static async Task<bool> VisitAKSCluster(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_CONTAINERSERVICE_MANAGEDCLUSTERS, StringComparison.OrdinalIgnoreCase))
return false;
// Get related VNET
if (resource.TryGetProperty(PROPERTY_PROPERTIES, out JObject properties) &&
properties.TryGetProperty(PROPERTY_NETWORKPROFILE, out JObject networkProfile) &&
networkProfile.TryGetProperty(PROPERTY_NETOWORKPLUGIN, out var networkPlugin) &&
string.Equals(networkPlugin, "azure", StringComparison.OrdinalIgnoreCase) &&
properties.TryArrayProperty(PROPERTY_AGENTPOOLPROFILES, out var agentPoolProfiles) &&
agentPoolProfiles.Count > 0)
{
for (var i = 0; i < agentPoolProfiles.Count; i++)
{
if (agentPoolProfiles[i] is JObject profile && profile.TryGetProperty("vnetSubnetID", out var vnetSubnetID))
{
// Get VNET.
AddSubResource(resource, await GetResource(context, vnetSubnetID, APIVERSION_2022_07_01));
}
}
}
// Get diagnostic settings
await GetDiagnosticSettings(context, resource, resourceId);
return true;
}
private static async Task<bool> VisitContainerRegistry(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_CONTAINERREGISTRY_REGISTRIES, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "replications", "2022-02-01-preview"));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "webhooks", "2022-02-01-preview"));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "tasks", "2019-06-01-preview"));
return true;
}
private static async Task<bool> VisitCDNEndpoint(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_CDN_PROFILES_ENDPOINTS, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "customDomains", "2021-06-01"));
return true;
}
private static async Task<bool> VisitAutomationAccount(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_AUTOMATION_ACCOUNTS, StringComparison.OrdinalIgnoreCase))
return false;
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "variables", "2022-08-08"));
AddSubResource(resource, await GetSubResourcesByType(context, resourceId, "webhooks", "2015-10-31"));
return true;
}
private static async Task<bool> VisitAPIManagement(ResourceContext context, JObject resource, string resourceType, string resourceId)
{
if (!string.Equals(resourceType, TYPE_APIMANAGEMENT_SERVICE, StringComparison.OrdinalIgnoreCase))
return false;
// APIs
var apis = await GetSubResourcesByType(context, resourceId, "apis", APIVERSION_2021_08_01);
AddSubResource(resource, apis);
var backends = await GetSubResourcesByType(context, resourceId, "backends", APIVERSION_2021_08_01);
AddSubResource(resource, backends);
var products = await GetSubResourcesByType(context, resourceId, "products", APIVERSION_2021_08_01);
AddSubResource(resource, products);
var policies = await GetSubResourcesByType(context, resourceId, "policies", APIVERSION_2021_08_01);
AddSubResource(resource, policies);
var identityProviders = await GetSubResourcesByType(context, resourceId, "identityProviders", APIVERSION_2021_08_01);
AddSubResource(resource, identityProviders);
var diagnostics = await GetSubResourcesByType(context, resourceId, "diagnostics", APIVERSION_2021_08_01);
AddSubResource(resource, diagnostics);
var loggers = await GetSubResourcesByType(context, resourceId, "loggers", APIVERSION_2021_08_01);
AddSubResource(resource, loggers);
var certificates = await GetSubResourcesByType(context, resourceId, "certificates", APIVERSION_2021_08_01);
AddSubResource(resource, certificates);
var namedValues = await GetSubResourcesByType(context, resourceId, "namedValues", APIVERSION_2021_08_01);
AddSubResource(resource, namedValues);
var portalSettings = await GetSubResourcesByType(context, resourceId, "portalsettings", APIVERSION_2021_08_01);
AddSubResource(resource, portalSettings);
return true;
}
/// <summary>
/// Get diagnostics for the specified resource type.
/// </summary>
private static async Task GetDiagnosticSettings(ResourceContext context, JObject resource, string resourceId)
{
AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_DIAGNOSTICSETTINGS), "2021-05-01-preview"));
}
private static async Task GetRoleAssignments(ResourceContext context, JObject resource, string resourceId)
{
AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_ROLEASSIGNMENTS), "2022-04-01"));
}
private static async Task GetResourceLocks(ResourceContext context, JObject resource, string resourceId)
{
AddSubResource(resource, await GetResource(context, string.Concat(resourceId, PROVIDERTYPE_RESOURCELOCKS), "2016-09-01"));
}
private static void AddSubResource(JObject parent, JObject child)
{
if (child == null)
return;
parent.UseProperty<JArray>(PROPERTY_RESOURCES, out var resources);
resources.Add(child);
}
private static void AddSubResource(JObject parent, JObject[] children)
{
if (children == null || children.Length == 0)
return;
parent.UseProperty<JArray>(PROPERTY_RESOURCES, out var resources);
for (var i = 0; i < children.Length; i++)
resources.Add(children[i]);
}
private static async Task<JObject> GetResource(ResourceContext context, string resourceId, string apiVersion)
{
return await context.GetAsync(resourceId, apiVersion);
}
private static async Task<JObject[]> GetSubResourcesByType(ResourceContext context, string resourceId, string type, string apiVersion)
{
return await context.ListAsync(string.Concat(resourceId, '/', type), apiVersion);
}
}
}

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

@ -0,0 +1,140 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Data;
namespace PSRule.Rules.Azure.Pipeline.Export
{
internal interface ISubscriptionExportContext
{
/// <summary>
/// Get resources from an Azure subscription.
/// </summary>
Task<JObject[]> GetResourcesAsync();
/// <summary>
/// Get resource groups from an Azure subscription.
/// </summary>
Task<JObject[]> GetResourceGroupsAsync();
/// <summary>
/// Get a resource for the Azure subscription.
/// </summary>
Task<JObject> GetSubscriptionAsync();
/// <summary>
/// Get a specified Azure resource from a subscription.
/// </summary>
Task<JObject> GetResourceAsync(string resourceId);
/// <summary>
/// The subscription Id of the context subscription.
/// </summary>
string SubscriptionId { get; }
/// <summary>
/// The tenant Id of the context tenant.
/// </summary>
string TenantId { get; }
}
/// <summary>
/// A context to export an Azure subscription.
/// </summary>
internal class SubscriptionExportContext : ExportDataContext, ISubscriptionExportContext
{
private readonly string _ResourceEndpoint;
private readonly string _ResourceGroupEndpoint;
private readonly string _SubscriptionEndpoint;
private ProviderData _ProviderData;
/// <summary>
/// Create a context to export an Azure subscription.
/// </summary>
public SubscriptionExportContext(PipelineContext context, ExportSubscriptionScope scope, AccessTokenCache tokenCache, Hashtable tag)
: base(context, tokenCache)
{
_ResourceEndpoint = $"{RESOURCE_MANAGER_URL}/subscriptions/{scope.SubscriptionId}/resources?api-version=2021-04-01{GetFilter(tag)}";
_ResourceGroupEndpoint = $"{RESOURCE_MANAGER_URL}/subscriptions/{scope.SubscriptionId}/resourcegroups?api-version=2021-04-01{GetFilter(tag)}";
_SubscriptionEndpoint = $"{RESOURCE_MANAGER_URL}/subscriptions/{scope.SubscriptionId}";
SubscriptionId = scope.SubscriptionId;
TenantId = scope.TenantId;
RefreshToken(scope.TenantId);
}
/// <inheritdoc/>
public string SubscriptionId { get; }
/// <inheritdoc/>
public string TenantId { get; }
/// <inheritdoc/>
public async Task<JObject[]> GetResourcesAsync()
{
return await ListAsync(TenantId, _ResourceEndpoint);
}
/// <inheritdoc/>
public async Task<JObject[]> GetResourceGroupsAsync()
{
return await ListAsync(TenantId, _ResourceGroupEndpoint);
}
/// <inheritdoc/>
public async Task<JObject> GetSubscriptionAsync()
{
return await GetAsync(TenantId, _SubscriptionEndpoint);
}
/// <inheritdoc/>
public async Task<JObject> GetResourceAsync(string resourceId)
{
if (!TryGetLatestAPIVersion(ResourceHelper.GetResourceType(resourceId), out var apiVersion))
return null;
return await GetAsync(TenantId, $"{RESOURCE_MANAGER_URL}{resourceId}?api-version={apiVersion}");
}
private static string GetFilter(Hashtable tag)
{
if (tag == null || tag.Count == 0)
return string.Empty;
var first = true;
var sb = new StringBuilder("&$filter=");
foreach (DictionaryEntry kv in tag)
{
if (!first)
sb.Append(" and ");
sb.Append("tagName eq '");
sb.Append(kv.Key);
sb.Append("' and tagValue eq '");
sb.Append(kv.Value);
sb.Append("'");
first = false;
}
return sb.ToString();
}
private bool TryGetLatestAPIVersion(string resourceType, out string apiVersion)
{
apiVersion = null;
if (string.IsNullOrEmpty(resourceType))
return false;
_ProviderData ??= new ProviderData();
if (!_ProviderData.TryResourceType(resourceType, out var data) ||
data.ApiVersions == null ||
data.ApiVersions.Length == 0)
return false;
apiVersion = data.ApiVersions[0];
return true;
}
}
}

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

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using PSRule.Rules.Azure.Pipeline.Export;
namespace PSRule.Rules.Azure.Pipeline
{
/// <summary>
/// A base class for a pipeline that exports data from Azure.
/// </summary>
internal abstract class ExportDataPipeline : PipelineBase
{
private bool _Disposed;
protected ExportDataPipeline(PipelineContext context, GetAccessTokenFn getToken)
: base(context)
{
PoolSize = 10;
TokenCache = new AccessTokenCache(getToken);
}
/// <summary>
/// The size of the thread pool for the pipeline.
/// </summary>
protected int PoolSize { get; }
protected AccessTokenCache TokenCache { get; }
protected override void Dispose(bool disposing)
{
if (!_Disposed)
{
if (disposing)
{
TokenCache.Dispose();
}
_Disposed = true;
}
base.Dispose(disposing);
}
}
}

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

@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace PSRule.Rules.Azure.Pipeline
{
/// <summary>
/// A delegate that returns an Access Token.
/// </summary>
public delegate AccessToken GetAccessTokenFn(string tenantId);
/// <summary>
/// A builder to configure the pipeline to export data.
/// </summary>
public interface IExportDataPipelineBuilder : IPipelineBuilder
{
/// <summary>
/// Configures a method to request an Access Token.
/// </summary>
void AccessToken(GetAccessTokenFn fn);
}
/// <summary>
/// A helper to build a pipeline to export data from Azure.
/// </summary>
internal abstract class ExportDataPipelineBuilder : PipelineBuilderBase, IExportDataPipelineBuilder
{
protected int _RetryCount;
protected int _RetryInterval;
protected GetAccessTokenFn _AccessToken;
/// <inheritdoc/>
public void AccessToken(GetAccessTokenFn fn)
{
_AccessToken = fn;
}
/// <inheritdoc/>
public void RetryCount(int retryCount)
{
_RetryCount = retryCount;
}
/// <inheritdoc/>
public void RetryInterval(int seconds)
{
_RetryInterval = seconds;
}
}
}

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

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
namespace PSRule.Rules.Azure.Pipeline
{
/// <summary>
/// The details for a subscription to export.
/// </summary>
public sealed class ExportSubscriptionScope
{
/// <summary>
/// Create a scope structure containing details for the subscription to export.
/// </summary>
/// <param name="subscriptionId">The subscription Id for the subscription to export.</param>
/// <param name="tenantId">The tenant Id associated to the subscription being exported.</param>
public ExportSubscriptionScope(string subscriptionId, string tenantId)
{
if (string.IsNullOrEmpty(subscriptionId))
throw new ArgumentOutOfRangeException(nameof(subscriptionId));
if (string.IsNullOrEmpty(tenantId))
throw new ArgumentOutOfRangeException(nameof(tenantId));
SubscriptionId = subscriptionId;
TenantId = tenantId;
}
/// <summary>
/// The subscription Id for the subscription to export.
/// This is a <seealso cref="System.Guid"/> identifier.
/// </summary>
public string SubscriptionId { get; }
/// <summary>
/// The tenant Id associated to the subscription being exported.
/// This is a <seealso cref="System.Guid"/> identifier.
/// </summary>
public string TenantId { get; }
}
}

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

@ -1,6 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Management.Automation;
using System.Net;
using PSRule.Rules.Azure.Resources;
namespace PSRule.Rules.Azure.Pipeline
@ -49,5 +52,79 @@ namespace PSRule.Rules.Azure.Pipeline
logger.WriteVerbose(Diagnostics.VerboseTemplateFileNotFound, path);
}
/// <summary>
/// Getting resources from subscription: {0}
/// </summary>
internal static void VerboseGetResources(this ILogger logger, string subscriptionId)
{
if (logger == null)
return;
logger.WriteVerbose(Diagnostics.VerboseGetResources, subscriptionId);
}
/// <summary>
/// Added {0} resources from subscription '{1}'.
/// </summary>
internal static void VerboseGetResourcesResult(this ILogger logger, int count, string subscriptionId)
{
if (logger == null)
return;
logger.WriteVerbose(Diagnostics.VerboseGetResourcesResult, count, subscriptionId);
}
/// <summary>
/// Getting resource: {0}
/// </summary>
internal static void VerboseGetResource(this ILogger logger, string resourceId)
{
if (logger == null)
return;
logger.WriteVerbose(Diagnostics.VerboseGetResource, resourceId);
}
/// <summary>
/// Expanding resource: {0}
/// </summary>
internal static void VerboseExpandingResource(this ILogger logger, string resourceId)
{
if (logger == null)
return;
logger.WriteVerbose(Diagnostics.VerboseExpandingResource, resourceId);
}
/// <summary>
/// Retrying '{0}' in {1}, attept {2}.
/// </summary>
internal static void VerboseRetryIn(this ILogger logger, string uri, TimeSpan retry, int attempt)
{
if (logger == null)
return;
logger.WriteVerbose(Diagnostics.VerboseRetryIn, uri, retry, attempt);
}
/// <summary>
/// Failed to get '{0}': status={1}, correlation-id={2}, {3}
/// </summary>
internal static void WarnFailedToGet(this ILogger logger, string uri, HttpStatusCode statusCode, string correlationId, string body)
{
if (logger == null)
return;
logger.WriteWarning(Diagnostics.WarnFailedToGet, uri, (int)statusCode, correlationId, body);
}
internal static void Error(this ILogger logger, Exception exception, string errorId, ErrorCategory errorCategory = ErrorCategory.NotSpecified, object targetObject = null)
{
if (logger == null)
return;
logger.WriteError(exception, errorId, errorCategory, targetObject);
}
}
}

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

@ -15,12 +15,12 @@ namespace PSRule.Rules.Azure.Pipeline.Output
private const string Source = "PSRule";
private const string HostTag = "PSHOST";
private Action<string> OnWriteWarning;
private Action<string> OnWriteVerbose;
private Action<ErrorRecord> OnWriteError;
private Action<InformationRecord> OnWriteInformation;
private Action<string> OnWriteDebug;
internal Action<object, bool> OnWriteObject;
private Action<string> _OnWriteWarning;
private Action<string> _OnWriteVerbose;
private Action<ErrorRecord> _OnWriteError;
private Action<InformationRecord> _OnWriteInformation;
private Action<string> _OnWriteDebug;
internal Action<object, bool> _OnWriteObject;
private bool _LogError;
private bool _LogWarning;
@ -42,12 +42,12 @@ namespace PSRule.Rules.Azure.Pipeline.Output
if (commandRuntime == null)
return;
OnWriteVerbose = commandRuntime.WriteVerbose;
OnWriteWarning = commandRuntime.WriteWarning;
OnWriteError = commandRuntime.WriteError;
OnWriteInformation = commandRuntime.WriteInformation;
OnWriteDebug = commandRuntime.WriteDebug;
OnWriteObject = commandRuntime.WriteObject;
_OnWriteVerbose = commandRuntime.WriteVerbose;
_OnWriteWarning = commandRuntime.WriteWarning;
_OnWriteError = commandRuntime.WriteError;
_OnWriteInformation = commandRuntime.WriteInformation;
_OnWriteDebug = commandRuntime.WriteDebug;
_OnWriteObject = commandRuntime.WriteObject;
}
internal void UseExecutionContext(EngineIntrinsics executionContext)
@ -80,10 +80,10 @@ namespace PSRule.Rules.Azure.Pipeline.Output
/// <param name="errorRecord">A valid PowerShell error record.</param>
public override void WriteError(ErrorRecord errorRecord)
{
if (OnWriteError == null || !ShouldWriteError())
if (_OnWriteError == null || !ShouldWriteError())
return;
OnWriteError(errorRecord);
_OnWriteError(errorRecord);
}
/// <summary>
@ -92,10 +92,10 @@ namespace PSRule.Rules.Azure.Pipeline.Output
/// <param name="message">A message to log.</param>
public override void WriteVerbose(string message)
{
if (OnWriteVerbose == null || !ShouldWriteVerbose())
if (_OnWriteVerbose == null || !ShouldWriteVerbose())
return;
OnWriteVerbose(message);
_OnWriteVerbose(message);
}
/// <summary>
@ -104,10 +104,10 @@ namespace PSRule.Rules.Azure.Pipeline.Output
/// <param name="message">A message to log</param>
public override void WriteWarning(string message)
{
if (OnWriteWarning == null || !ShouldWriteWarning())
if (_OnWriteWarning == null || !ShouldWriteWarning())
return;
OnWriteWarning(message);
_OnWriteWarning(message);
}
/// <summary>
@ -115,10 +115,10 @@ namespace PSRule.Rules.Azure.Pipeline.Output
/// </summary>
public override void WriteInformation(InformationRecord informationRecord)
{
if (OnWriteInformation == null || !ShouldWriteInformation())
if (_OnWriteInformation == null || !ShouldWriteInformation())
return;
OnWriteInformation(informationRecord);
_OnWriteInformation(informationRecord);
}
/// <summary>
@ -126,28 +126,28 @@ namespace PSRule.Rules.Azure.Pipeline.Output
/// </summary>
public override void WriteDebug(DebugRecord debugRecord)
{
if (OnWriteDebug == null || !ShouldWriteDebug())
if (_OnWriteDebug == null || !ShouldWriteDebug())
return;
OnWriteDebug(debugRecord.Message);
_OnWriteDebug(debugRecord.Message);
}
public override void WriteObject(object sendToPipeline, bool enumerateCollection)
{
if (OnWriteObject == null)
if (_OnWriteObject == null)
return;
OnWriteObject(sendToPipeline, enumerateCollection);
_OnWriteObject(sendToPipeline, enumerateCollection);
}
public override void WriteHost(HostInformationMessage info)
{
if (OnWriteInformation == null)
if (_OnWriteInformation == null)
return;
var record = new InformationRecord(info, Source);
record.Tags.Add(HostTag);
OnWriteInformation(record);
_OnWriteInformation(record);
}
public override bool ShouldWriteVerbose()

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

@ -54,6 +54,16 @@ namespace PSRule.Rules.Azure.Pipeline
{
return new PolicyAssignmentSearchPipelineBuilder(path);
}
/// <summary>
/// Create a builder for creating a pipeline to exporting resource data from Azure.
/// </summary>
/// <param name="option">Options that configure PSRule for Azure.</param>
/// <returns>A builder object to configure the pipeline.</returns>
public static IResourceDataPipelineBuilder ResourceData(PSRuleOption option)
{
return new ResourceDataPipelineBuilder(option);
}
}
/// <summary>

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

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
@ -14,6 +14,12 @@ namespace PSRule.Rules.Azure.Pipeline
void WriteVerbose(string message);
void WriteVerbose(string format, params object[] args);
void WriteWarning(string message);
void WriteWarning(string format, params object[] args);
void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject);
}
internal abstract class PipelineWriter : ILogger
@ -79,6 +85,14 @@ namespace PSRule.Rules.Azure.Pipeline
return _Writer != null && _Writer.ShouldWriteVerbose();
}
public void WriteWarning(string format, params object[] args)
{
if (!ShouldWriteWarning())
return;
WriteWarning(string.Format(Thread.CurrentThread.CurrentCulture, format, args));
}
public virtual void WriteWarning(string message)
{
if (_Writer == null || string.IsNullOrEmpty(message))

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

@ -0,0 +1,263 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Pipeline.Export;
namespace PSRule.Rules.Azure.Pipeline
{
/// <summary>
/// A pipeline to export resource data from Azure.
/// </summary>
internal sealed class ResourceDataPipeline : ExportDataPipeline
{
private const string PROPERTY_ID = "id";
private const string PROPERTY_TYPE = "type";
private const string PROPERTY_NAME = "name";
private const string PROPERTY_SUBSCRIPTIONID = "subscriptionId";
private const string PROPERTY_DISPLAYNAME = "displayName";
private const string PROPERTY_TENANTID = "tenantId";
private const string ERRORID_RESOURCEDATAEXPAND = "PSRule.Rules.Azure.ResourceDataExpand";
private readonly ConcurrentQueue<JObject> _Resources;
private readonly ConcurrentQueue<JObject> _Output;
private readonly int _ExpandThreadCount;
private readonly ExportSubscriptionScope[] _Subscription;
private readonly HashSet<string> _ResourceGroup;
private readonly Hashtable _Tag;
private readonly int _RetryCount;
private readonly int _RetryInterval;
private readonly string _OutputPath;
private readonly string _TenantId;
public ResourceDataPipeline(PipelineContext context, ExportSubscriptionScope[] subscription, string[] resourceGroup, GetAccessTokenFn getToken, Hashtable tag, int retryCount, int retryInterval, string outputPath, string tenantId)
: base(context, getToken)
{
_Subscription = subscription;
_ResourceGroup = resourceGroup != null && resourceGroup.Length > 0 ? new HashSet<string>(resourceGroup, StringComparer.OrdinalIgnoreCase) : null;
_Tag = tag;
_RetryCount = retryCount;
_RetryInterval = retryInterval;
_OutputPath = outputPath;
_TenantId = tenantId;
_Resources = new ConcurrentQueue<JObject>();
_Output = new ConcurrentQueue<JObject>();
_ExpandThreadCount = Environment.ProcessorCount > 0 ? Environment.ProcessorCount * 4 : 20;
}
/// <inheritdoc/>
public override void Begin()
{
// Process each subscription
for (var i = 0; _Subscription != null && i < _Subscription.Length; i++)
GetResourceBySubscription(_Subscription[i]);
}
/// <inheritdoc/>
public override void Process(PSObject sourceObject)
{
if (sourceObject != null &&
sourceObject.BaseObject is string resourceId &&
!string.IsNullOrEmpty(resourceId))
GetResourceById(resourceId);
}
/// <inheritdoc/>
public override void End()
{
ExpandResource(_ExpandThreadCount);
WriteOutput();
base.End();
}
/// <summary>
/// Write output to file or pipeline.
/// </summary>
private void WriteOutput()
{
var output = _Output.ToArray();
if (_OutputPath == null)
{
// Pass through results to pipeline
Context.Writer.WriteObject(JsonConvert.SerializeObject(output), enumerateCollection: false);
}
else
{
// Group results into subscriptions a write each to a new file.
foreach (var group in output.GroupBy((r) => r[PROPERTY_SUBSCRIPTIONID]))
{
var filePath = Path.Combine(_OutputPath, string.Concat(group.Key, ".json"));
File.WriteAllText(filePath, JsonConvert.SerializeObject(group.ToArray()), Encoding.UTF8);
Context.Writer.WriteObject(new FileInfo(filePath), enumerateCollection: false);
}
}
}
#region Get resources
/// <summary>
/// Get a resource for expansion by resource Id.
/// </summary>
/// <param name="resourceId">The specified resource Id.</param>
private void GetResourceById(string resourceId)
{
if (!ResourceHelper.TrySubscriptionId(resourceId, out var subscriptionId))
return;
Context.Writer.VerboseGetResource(resourceId);
var scope = new ExportSubscriptionScope(subscriptionId, _TenantId);
var context = new SubscriptionExportContext(Context, scope, TokenCache, _Tag);
// Get results from ARM
GetResourceAsync(context, resourceId).Wait();
}
/// <summary>
/// Get a specific resource by Id.
/// </summary>
private async Task<int> GetResourceAsync(ISubscriptionExportContext context, string resourceId)
{
var added = 0;
var r = await context.GetResourceAsync(resourceId);
if (r != null)
{
added++;
r.Add(PROPERTY_TENANTID, context.TenantId);
_Resources.Enqueue(r);
added++;
}
return added;
}
/// <summary>
/// Get resources, resource groups and the specified subscription within the subscription scope.
/// Resources are then queued for expansion.
/// </summary>
/// <param name="scope">The subscription scope.</param>
private void GetResourceBySubscription(ExportSubscriptionScope scope)
{
Context.Writer.VerboseGetResources(scope.SubscriptionId);
var context = new SubscriptionExportContext(Context, scope, TokenCache, _Tag);
var pool = new Task<int>[3];
// Get results from ARM
pool[0] = GetResourcesAsync(context);
pool[1] = GetResourceGroupsAsync(context);
pool[2] = GetSubscriptionAsync(context);
Task.WaitAll(pool);
var count = pool[0].Result + pool[1].Result + pool[2].Result;
Context.Writer.VerboseGetResourcesResult(count, scope.SubscriptionId);
}
/// <summary>
/// Get a subscription resource.
/// </summary>
private async Task<int> GetSubscriptionAsync(ISubscriptionExportContext context)
{
var added = 0;
var r = await context.GetSubscriptionAsync();
if (r != null)
{
r.Add(PROPERTY_TYPE, "Microsoft.Subscription");
if (r.TryGetProperty(PROPERTY_DISPLAYNAME, out var displayName))
r.Add(PROPERTY_NAME, displayName);
_Resources.Enqueue(r);
added++;
}
return added;
}
/// <summary>
/// Get a list of resource groups for a subscription.
/// </summary>
private async Task<int> GetResourceGroupsAsync(ISubscriptionExportContext context)
{
var added = 0;
foreach (var r in await context.GetResourceGroupsAsync())
{
// If resource group filtering is specified only queue resources in the specified resource group
if (_ResourceGroup == null || (ResourceHelper.TryResourceGroup(r[PROPERTY_ID].Value<string>(), out var resourceGroupName) &&
_ResourceGroup.Contains(resourceGroupName)))
{
r.Add(PROPERTY_TENANTID, context.TenantId);
_Resources.Enqueue(r);
added++;
}
}
return added;
}
/// <summary>
/// Get a list of resources for a subscription.
/// </summary>
private async Task<int> GetResourcesAsync(ISubscriptionExportContext context)
{
var added = 0;
foreach (var r in await context.GetResourcesAsync())
{
// If resource group filtering is specified only queue resources in the specified resource group
if (_ResourceGroup == null || (ResourceHelper.TryResourceGroup(r[PROPERTY_ID].Value<string>(), out var resourceGroupName) &&
_ResourceGroup.Contains(resourceGroupName)))
{
r.Add(PROPERTY_TENANTID, context.TenantId);
_Resources.Enqueue(r);
added++;
}
}
return added;
}
#endregion Get resources
#region Expand resources
/// <summary>
/// Expand resources from the queue.
/// </summary>
/// <param name="poolSize">The size of the thread pool to use.</param>
private void ExpandResource(int poolSize)
{
var context = new ResourceExportContext(Context, _Resources, TokenCache, _RetryCount, _RetryInterval);
var visitor = new ResourceExportVisitor();
var pool = new Task[poolSize];
for (var i = 0; i < poolSize; i++)
{
pool[i] = Task.Run(async () =>
{
while (!_Resources.IsEmpty && _Resources.TryDequeue(out var r))
{
context.VerboseExpandingResource(r[PROPERTY_ID].Value<string>());
try
{
await visitor.VisitAsync(context, r);
_Output.Enqueue(r);
}
catch (Exception ex)
{
context.Error(ex, ERRORID_RESOURCEDATAEXPAND);
}
}
});
}
context.Wait();
Task.WaitAll(pool);
context.Flush();
pool.Dispose();
}
#endregion Expand resources
}
}

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

@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
using PSRule.Rules.Azure.Configuration;
namespace PSRule.Rules.Azure.Pipeline
{
/// <summary>
/// A builder to configure the pipeline to export Azure resource data.
/// </summary>
public interface IResourceDataPipelineBuilder : IExportDataPipelineBuilder
{
/// <summary>
/// Specifies any resource group filters to be used.
/// </summary>
/// <param name="resourceGroup">If any are specified, only these resource groups will be included in results.</param>
void ResourceGroup(string[] resourceGroup);
/// <summary>
/// Specifies any subscriptions filters to be used.
/// </summary>
/// <param name="scope">If any are specified, only these subscriptions will be included in results.</param>
void Subscription(ExportSubscriptionScope[] scope);
/// <summary>
/// Specifies additional tags to filter resources by.
/// </summary>
/// <param name="tag">A hashtable of tags to use for filtering resources.</param>
void Tag(Hashtable tag);
/// <summary>
/// Specifies the path to write output.
/// </summary>
/// <param name="path">The directory path to write output.</param>
void OutputPath(string path);
/// <summary>
/// Specifies the default tenant to use when the tenant is unknown.
/// </summary>
/// <param name="tenantId">The tenant Id of the default tenant.</param>
void Tenant(string tenantId);
}
/// <summary>
/// A helper to build a pipeline that exports resource data from Azure.
/// </summary>
internal sealed class ResourceDataPipelineBuilder : ExportDataPipelineBuilder, IResourceDataPipelineBuilder
{
private string[] _ResourceGroup;
private ExportSubscriptionScope[] _Subscription;
private Hashtable _Tag;
private string _OutputPath;
private string _TenantId;
internal ResourceDataPipelineBuilder(PSRuleOption option)
: base()
{
Configure(option);
}
/// <inheritdoc/>
public void ResourceGroup(string[] resourceGroup)
{
if (resourceGroup == null || resourceGroup.Length == 0)
return;
_ResourceGroup = resourceGroup;
}
/// <inheritdoc/>
public void Subscription(ExportSubscriptionScope[] scope)
{
if (scope == null || scope.Length == 0)
return;
_Subscription = scope;
}
/// <inheritdoc/>
public void Tag(Hashtable tag)
{
if (tag == null || tag.Count == 0)
return;
_Tag = tag;
}
/// <inheritdoc/>
public void OutputPath(string path)
{
_OutputPath = path;
}
/// <inheritdoc/>
public void Tenant(string tenantId)
{
_TenantId = tenantId;
}
/// <inheritdoc/>
public override IPipeline Build()
{
return new ResourceDataPipeline(PrepareContext(), _Subscription, _ResourceGroup, _AccessToken, _Tag, _RetryCount, _RetryInterval, _OutputPath, _TenantId);
}
}
}

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

@ -8,11 +8,10 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace PSRule.Rules.Azure.Resources
{
namespace PSRule.Rules.Azure.Resources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
@ -20,119 +19,153 @@ namespace PSRule.Rules.Azure.Resources
// 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.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Diagnostics
{
internal class Diagnostics {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Diagnostics()
{
internal Diagnostics() {
}
/// <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))
{
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.Diagnostics", typeof(Diagnostics).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
{
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set
{
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Running bicep from &apos;{0}&apos;..
/// </summary>
internal static string DebugRunningBicep
{
get
{
internal static string DebugRunningBicep {
get {
return ResourceManager.GetString("DebugRunningBicep", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Expanding resource: {0}.
/// </summary>
internal static string VerboseExpandingResource {
get {
return ResourceManager.GetString("VerboseExpandingResource", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Searching for files in &apos;{0}&apos;..
/// </summary>
internal static string VerboseFindFiles
{
get
{
internal static string VerboseFindFiles {
get {
return ResourceManager.GetString("VerboseFindFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Found file &apos;{0}&apos;..
/// </summary>
internal static string VerboseFoundFile
{
get
{
internal static string VerboseFoundFile {
get {
return ResourceManager.GetString("VerboseFoundFile", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Getting resource: {0}.
/// </summary>
internal static string VerboseGetResource {
get {
return ResourceManager.GetString("VerboseGetResource", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Getting resources from subscription: {0}.
/// </summary>
internal static string VerboseGetResources {
get {
return ResourceManager.GetString("VerboseGetResources", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Added {0} resources from subscription &apos;{1}&apos;..
/// </summary>
internal static string VerboseGetResourcesResult {
get {
return ResourceManager.GetString("VerboseGetResourcesResult", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The parameter file &apos;{0}&apos; does not contain a metadata property..
/// </summary>
internal static string VerboseMetadataNotFound
{
get
{
internal static string VerboseMetadataNotFound {
get {
return ResourceManager.GetString("VerboseMetadataNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Retrying &apos;{0}&apos; in {1}, attept {2}..
/// </summary>
internal static string VerboseRetryIn {
get {
return ResourceManager.GetString("VerboseRetryIn", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unable to find the specified template file &apos;{0}&apos;..
/// </summary>
internal static string VerboseTemplateFileNotFound
{
get
{
internal static string VerboseTemplateFileNotFound {
get {
return ResourceManager.GetString("VerboseTemplateFileNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The parameter file &apos;{0}&apos; does not reference a linked template..
/// </summary>
internal static string VerboseTemplateLinkNotFound
{
get
{
internal static string VerboseTemplateLinkNotFound {
get {
return ResourceManager.GetString("VerboseTemplateLinkNotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to get &apos;{0}&apos;: status={1}, correlation-id={2}, {3}.
/// </summary>
internal static string WarnFailedToGet {
get {
return ResourceManager.GetString("WarnFailedToGet", resourceCulture);
}
}
}
}

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

@ -120,16 +120,31 @@
<data name="DebugRunningBicep" xml:space="preserve">
<value>Running bicep from '{0}'.</value>
</data>
<data name="VerboseExpandingResource" xml:space="preserve">
<value>Expanding resource: {0}</value>
</data>
<data name="VerboseFindFiles" xml:space="preserve">
<value>Searching for files in '{0}'.</value>
</data>
<data name="VerboseFoundFile" xml:space="preserve">
<value>Found file '{0}'.</value>
</data>
<data name="VerboseGetResource" xml:space="preserve">
<value>Getting resource: {0}</value>
</data>
<data name="VerboseGetResources" xml:space="preserve">
<value>Getting resources from subscription: {0}</value>
</data>
<data name="VerboseGetResourcesResult" xml:space="preserve">
<value>Added {0} resources from subscription '{1}'.</value>
</data>
<data name="VerboseMetadataNotFound" xml:space="preserve">
<value>The parameter file '{0}' does not contain a metadata property.</value>
<comment>Occurs when a parameter file does not have the metadata property set.</comment>
</data>
<data name="VerboseRetryIn" xml:space="preserve">
<value>Retrying '{0}' in {1}, attept {2}.</value>
</data>
<data name="VerboseTemplateFileNotFound" xml:space="preserve">
<value>Unable to find the specified template file '{0}'.</value>
<comment>Occurs when a template file is specified that doesn't exist.</comment>
@ -138,4 +153,7 @@
<value>The parameter file '{0}' does not reference a linked template.</value>
<comment>Occurs when a parameter file does not have the metadata.template property set.</comment>
</data>
<data name="WarnFailedToGet" xml:space="preserve">
<value>Failed to get '{0}': status={1}, correlation-id={2}, {3}</value>
</data>
</root>

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

@ -95,174 +95,6 @@ BeforeAll {
#endregion Mocks
}
#region Export-AzRuleData
Describe 'Export-AzRuleData' -Tag 'Cmdlet', 'Export-AzRuleData' {
Context 'With defaults' {
BeforeAll {
Mock -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith ${function:MockContext};
Mock -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith {
return @(
[PSCustomObject]@{
Name = 'Resource1'
ResourceType = ''
}
[PSCustomObject]@{
Name = 'Resource2'
ResourceType = ''
}
)
}
}
It 'Exports resources' {
$result = @(Export-AzRuleData -OutputPath $outputPath);
Assert-VerifiableMock;
Assert-MockCalled -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Times 3;
Assert-MockCalled -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -Times 1 -ParameterFilter {
$ListAvailable -eq $False
}
Assert-MockCalled -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -Times 0 -ParameterFilter {
$ListAvailable -eq $True
}
$result.Length | Should -Be 3;
$result | Should -BeOfType System.IO.FileInfo;
# Check exported data
$data = Get-Content -Path $result[0].FullName | ConvertFrom-Json;
$data -is [System.Array] | Should -Be $True;
$data.Length | Should -Be 2;
$data.Name | Should -BeIn 'Resource1', 'Resource2';
}
It 'Return resources' {
$result = @(Export-AzRuleData -PassThru);
$result.Length | Should -Be 6;
$result | Should -BeOfType PSCustomObject;
$result.Name | Should -BeIn 'Resource1', 'Resource2';
}
}
Context 'With filters' {
BeforeAll {
Mock -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -MockWith ${function:MockContext};
Mock -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -MockWith {
return @(
[PSCustomObject]@{
Name = 'Resource1'
ResourceGroupName = 'rg-test-1'
ResourceType = ''
}
[PSCustomObject]@{
Name = 'Resource2'
ResourceGroupName = 'rg-test-2'
ResourceType = ''
}
)
}
}
It '-Subscription with name filter' {
$Null = Export-AzRuleData -Subscription 'Test subscription 1' -PassThru;
Assert-MockCalled -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Times 1;
Assert-MockCalled -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -Times 1 -ParameterFilter {
$ListAvailable -eq $True
}
}
It '-Subscription with Id filter' {
$Null = Export-AzRuleData -Subscription '00000000-0000-0000-0000-000000000002' -PassThru;
Assert-MockCalled -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Times 1;
Assert-MockCalled -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -Times 1 -ParameterFilter {
$ListAvailable -eq $True
}
}
It '-Tenant filter' {
$Null = Export-AzRuleData -Tenant '00000000-0000-0000-0000-000000000002' -PassThru;
Assert-MockCalled -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Times 2;
Assert-MockCalled -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -Times 1 -ParameterFilter {
$ListAvailable -eq $True
}
}
It '-ResourceGroupName filter' {
$result = @(Export-AzRuleData -Subscription 'Test subscription 1' -ResourceGroupName 'rg-test-2' -PassThru);
$result | Should -Not -BeNullOrEmpty;
$result.Length | Should -Be 1;
$result[0].Name | Should -Be 'Resource2'
}
It '-Tag filter' {
$Null = Export-AzRuleData -Subscription 'Test subscription 1' -Tag @{ environment = 'production' } -PassThru;
Assert-MockCalled -CommandName 'GetAzureResource' -ModuleName 'PSRule.Rules.Azure' -Times 1 -ParameterFilter {
$Tag.environment -eq 'production'
}
}
}
Context 'With data' {
BeforeAll {
Mock -CommandName 'GetAzureContext' -ModuleName 'PSRule.Rules.Azure' -MockWith ${function:MockSingleSubscription};
}
It 'Microsoft.Network/connections' {
Mock -CommandName 'Get-AzResourceGroup' -ModuleName 'PSRule.Rules.Azure';
Mock -CommandName 'Get-AzSubscription' -ModuleName 'PSRule.Rules.Azure';
Mock -CommandName 'Get-AzResource' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith {
return @(
[PSCustomObject]@{
Name = 'Resource1'
ResourceType = 'Microsoft.Network/connections'
Id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/connections/cn001'
ResourceId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/connections/cn001'
Properties = [PSCustomObject]@{ sharedKey = 'test123' }
}
[PSCustomObject]@{
Name = 'Resource2'
ResourceType = 'Microsoft.Network/connections'
Id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/connections/cn002'
ResourceId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/connections/cn002'
Properties = [PSCustomObject]@{ dummy = 'value' }
}
)
}
$result = @(Export-AzRuleData -OutputPath $outputPath -PassThru);
$result.Length | Should -Be 2;
$result[0].Properties.sharedKey | Should -Be '*** MASKED ***';
}
It 'Microsoft.Storage/storageAccounts' {
Mock -CommandName 'Get-AzResourceGroup' -ModuleName 'PSRule.Rules.Azure';
Mock -CommandName 'Get-AzSubscription' -ModuleName 'PSRule.Rules.Azure';
Mock -CommandName 'GetSubResource' -ModuleName 'PSRule.Rules.Azure' -Verifiable;
Mock -CommandName 'Get-AzResource' -ModuleName 'PSRule.Rules.Azure' -Verifiable -MockWith {
return @(
[PSCustomObject]@{
Name = 'Resource1'
ResourceType = 'Microsoft.Storage/storageAccounts'
Kind = 'StorageV2'
Id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/sa001'
ResourceId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/sa001'
}
[PSCustomObject]@{
Name = 'Resource2'
ResourceType = 'Microsoft.Storage/storageAccounts'
Kind = 'FileServices'
Id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/sa002'
ResourceId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/sa002'
}
)
}
$Null = @(Export-AzRuleData -OutputPath $outputPath);
Assert-MockCalled -CommandName 'GetSubResource' -ModuleName 'PSRule.Rules.Azure' -Times 1;
}
}
}
#endregion Export-AzRuleData
#region Export-AzRuleTemplateData
Describe 'Export-AzRuleTemplateData' -Tag 'Cmdlet', 'Export-AzRuleTemplateData' {
@ -819,217 +651,6 @@ Describe 'Get-AzPolicyAssignmentDataSource' -Tag 'Cmdlet', 'Get-AzPolicyAssignme
#endregion Get-AzPolicyAssignmentDataSource
#region PSRule.Rules.Azure.psm1 Private Functions
Describe 'VisitAKSCluster' {
BeforeAll {
Mock -CommandName 'GetResourceById' -ModuleName 'PSRule.Rules.Azure' -MockWith {
return @(
[PSCustomObject]@{
Name = 'Resource1'
ResourceID = 'subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-A/subnets/subnet-A'
}
[PSCustomObject]@{
Name = 'Resource2'
ResourceID = 'subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-A/subnets/subnet-B'
}
)
};
Mock -CommandName 'Get-AzResource' -ModuleName 'PSRule.Rules.Azure' -MockWith {
return @(
[PSCustomObject]@{
Name = 'Resource3'
ResourceType = 'microsoft.insights/diagnosticSettings'
Id = '/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/k8s-aks-cluster-rg/providers/microsoft.containerservice/managedclusters/k8s-aks-cluster/providers/microsoft.insights/diagnosticSettings/metrics'
ResourceId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/k8s-aks-cluster-rg/providers/microsoft.containerservice/managedclusters/k8s-aks-cluster/providers/microsoft.insights/diagnosticSettings/metrics'
Properties = [PSCustomObject]@{
metrics = @()
logs = @()
}
}
)
}
}
Context 'Network Plugin' {
It 'Given AzureCNI plugin it returns resource with VNET subnet IDs attached as sub resource' {
InModuleScope -ModuleName 'PSRule.Rules.Azure' {
$resource = [PSCustomObject]@{
Name = 'akscluster'
ResourceGroupName = 'akscluster-rg'
Properties = [PSCustomObject]@{
agentPoolProfiles = @(
[PSCustomObject]@{
name = 'agentpool1'
vnetSubnetId = 'subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-A/subnets/subnet-A'
}
[PSCustomObject]@{
name = 'agentpool2'
vnetSubnetId = 'subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-A/subnets/subnet-B'
}
)
networkProfile = [PSCustomObject]@{
networkPlugin = 'azure'
}
}
};
$context = New-MockObject -Type Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext;
$clusterResource = $resource | VisitAKSCluster -Context $context;
$clusterResource.resources | Should -Not -BeNullOrEmpty;
Assert-MockCalled -CommandName 'GetResourceById' -Times 2;
Assert-MockCalled -CommandName 'Get-AzResource' -Times 1;
$clusterResource.resources[0].Name | Should -BeExactly 'Resource1';
$clusterResource.resources[0].ResourceID | Should -BeExactly 'subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-A/subnets/subnet-A';
$clusterResource.resources[1].Name | Should -BeExactly 'Resource2';
$clusterResource.resources[1].ResourceID | Should -BeExactly 'subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-A/subnets/subnet-B';
$clusterResource.resources[4].Name | Should -BeExactly 'Resource3';
$clusterResource.resources[4].ResourceType | Should -Be 'microsoft.insights/diagnosticSettings';
$clusterResource.resources[4].ResourceID | Should -Be '/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/k8s-aks-cluster-rg/providers/microsoft.containerservice/managedclusters/k8s-aks-cluster/providers/microsoft.insights/diagnosticSettings/metrics'
$clusterResource.resources[4].Properties.metrics | Should -BeNullOrEmpty;
$clusterResource.resources[4].Properties.logs | Should -BeNullOrEmpty;
}
}
It 'Given kubelet plugin it returns resource with empty sub resource' {
InModuleScope -ModuleName 'PSRule.Rules.Azure' {
$resource = [PSCustomObject]@{
Name = 'akscluster'
ResourceGroupName = 'akscluster-rg'
Properties = [PSCustomObject]@{
agentPoolProfiles = @(
[PSCustomObject]@{
name = 'agentpool1'
}
[PSCustomObject]@{
name = 'agentpool2'
}
)
networkProfile = [PSCustomObject]@{
networkPlugin = 'kubelet'
}
}
};
$context = New-MockObject -Type Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext;
$clusterResource = $resource | VisitAKSCluster -Context $context;
Assert-MockCalled -CommandName 'GetResourceById' -Times 0;
Assert-MockCalled -CommandName 'Get-AzResource' -Times 1;
$clusterResource.resources[0].Name | Should -BeExactly 'Resource3';
$clusterResource.resources[0].ResourceType | Should -Be 'microsoft.insights/diagnosticSettings';
$clusterResource.resources[0].ResourceID | Should -Be '/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/k8s-aks-cluster-rg/providers/microsoft.containerservice/managedclusters/k8s-aks-cluster/providers/microsoft.insights/diagnosticSettings/metrics'
$clusterResource.resources[0].Properties.metrics | Should -BeNullOrEmpty;
$clusterResource.resources[0].Properties.logs | Should -BeNullOrEmpty;
}
}
}
}
Describe 'VisitPublicIP' -Tag 'Cmdlet', 'Export-AzRuleData', 'VisitPublicIP' {
Context "Availability Zones" {
It "Non-empty zones are added to Public IP resource" {
InModuleScope -ModuleName 'PSRule.Rules.Azure' {
$resource = [PSCustomObject]@{
Name = 'Resource1'
ResourceGroupName = 'lb-rg'
ResourceID = '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/publicIPAddresses/test-ip'
};
Mock -CommandName 'Invoke-AzRestMethod' -MockWith {
return [PSCustomObject]@{
Content = [PSCustomObject]@{
Name = 'Resource1'
ResourceGroupName = 'lb-rg'
ResourceID = '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/publicIPAddresses/test-ip'
Zones = @("1", "2", "3")
} | ConvertTo-Json
}
};
$context = New-MockObject -Type Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext;
$publicIpResource = $resource | VisitPublicIP -Context $context;
Assert-MockCalled -CommandName 'Invoke-AzRestMethod' -Times 1;
$publicIpResource[0].Name | Should -Be 'Resource1';
$publicIpResource[0].ResourceGroupName | Should -Be 'lb-rg';
$publicIpResource[0].ResourceID | Should -Be '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/publicIPAddresses/test-ip';
$publicIpResource[0].zones | Should -Be @("1", "2", "3");
}
}
It 'Empty zones are set to null in Public IP resource' {
InModuleScope -ModuleName 'PSRule.Rules.Azure' {
$resource = [PSCustomObject]@{
Name = 'Resource1'
ResourceGroupName = 'lb-rg'
ResourceID = '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/publicIPAddresses/test-ip'
};
Mock -CommandName 'Invoke-AzRestMethod' -MockWith {
return [PSCustomObject]@{
Content = [PSCustomObject]@{
Name = 'Resource1'
ResourceGroupName = 'lb-rg'
ResourceID = '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/publicIPAddresses/test-ip'
Zones = @()
} | ConvertTo-Json
}
};
$context = New-MockObject -Type Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext;
$publicIpResource = $resource | VisitPublicIP -Context $context;
Assert-MockCalled -CommandName 'Invoke-AzRestMethod' -Times 1;
$publicIpResource[0].Name | Should -Be 'Resource1';
$publicIpResource[0].ResourceGroupName | Should -Be 'lb-rg';
$publicIpResource[0].ResourceID | Should -Be '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/publicIPAddresses/test-ip';
$publicIpResource[0].zones | Should -BeNullOrEmpty;
}
}
It 'No zones are set on Public IP resource' {
InModuleScope -ModuleName 'PSRule.Rules.Azure' {
$resource = [PSCustomObject]@{
Name = 'Resource1'
ResourceGroupName = 'lb-rg'
ResourceID = '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/publicIPAddresses/test-ip'
};
Mock -CommandName 'Invoke-AzRestMethod' -MockWith {
return [PSCustomObject]@{
Content = [PSCustomObject]@{
Name = 'Resource1'
ResourceGroupName = 'lb-rg'
ResourceID = '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/publicIPAddresses/test-ip'
} | ConvertTo-Json
}
};
$context = New-MockObject -Type Microsoft.Azure.Commands.Profile.Models.Core.PSAzureContext;
$publicIpResource = $resource | VisitPublicIP -Context $context;
Assert-MockCalled -CommandName 'Invoke-AzRestMethod' -Times 1;
$publicIpResource[0].Name | Should -Be 'Resource1';
$publicIpResource[0].ResourceGroupName | Should -Be 'lb-rg';
$publicIpResource[0].ResourceID | Should -Be '/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/lb-rg/providers/Microsoft.Network/publicIPAddresses/test-ip';
}
}
}
}
#endregion
#region Azure.Common.Rule.ps1 Functions
Describe 'GetAvailabilityZone' {
It "Given array of zones and '<location>' it returns '<expected>'" -TestCases @(

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

@ -1,12 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Management.Automation;
using PSRule.Rules.Azure.Pipeline;
namespace PSRule.Rules.Azure
{
internal sealed class NullLogger : ILogger
{
public void WriteError(Exception exception, string errorId, ErrorCategory errorCategory, object targetObject)
{
// Do nothing
}
public void WriteVerbose(string message)
{
// Do nothing
@ -16,5 +23,14 @@ namespace PSRule.Rules.Azure
{
// Do nothing
}
public void WriteWarning(string message)
{
// Do nothing
}
public void WriteWarning(string format, params object[] args)
{
// Do nothing
}
}
}

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

@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using PSRule.Rules.Azure.Pipeline;
using PSRule.Rules.Azure.Pipeline.Export;
using Xunit;
namespace PSRule.Rules.Azure
{
public sealed class ResourceExportVisitorTests
{
[Fact]
public async Task VisitAsync()
{
var context = new TestResourceExportContext();
var visitor = new ResourceExportVisitor();
var resource = GetResourceObject("Microsoft.ContainerService/managedClusters");
await visitor.VisitAsync(context, resource);
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", resource["subscriptionId"].Value<string>());
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", resource["tenantId"].Value<string>());
}
private static JObject GetResourceObject(string resourceType)
{
return new JObject
{
{ "type", resourceType },
{ "id", $"/subscriptions/ffffffff-ffff-ffff-ffff-ffffffffffff/resourceGroups/rg-test/providers/{resourceType}/test" },
{ "tenantId", "ffffffff-ffff-ffff-ffff-ffffffffffff" }
};
}
}
internal sealed class TestResourceExportContext : ResourceExportContext
{
public TestResourceExportContext()
: base(null, null, new AccessTokenCache(GetAccessToken), retryCount: 3, retryInterval: 10)
{
RefreshToken("ffffffff-ffff-ffff-ffff-ffffffffffff");
}
private static AccessToken GetAccessToken(string tenantId)
{
return new AccessToken(string.Empty, DateTime.UtcNow.AddMinutes(15), tenantId);
}
}
}