* Fixes and improvements for Export-AzRuleDAta #1341 * Add suppression for false postive
This commit is contained in:
Родитель
58f1a66cd5
Коммит
51030d2b71
|
@ -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 '{0}'..
|
||||
/// </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 '{0}'..
|
||||
/// </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 '{0}'..
|
||||
/// </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 '{1}'..
|
||||
/// </summary>
|
||||
internal static string VerboseGetResourcesResult {
|
||||
get {
|
||||
return ResourceManager.GetString("VerboseGetResourcesResult", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The parameter file '{0}' 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 '{0}' 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 '{0}'..
|
||||
/// </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 '{0}' 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 '{0}': 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);
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче